source: sasmodels/sasmodels/kerneldll.py @ 3199b17

core_shell_microgelsmagnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 3199b17 was 3199b17, checked in by Paul Kienzle <pkienzle@…>, 5 years ago

PEP 8 changes and improved consistency between OpenCL/CUDA/DLL code

  • Property mode set to 100644
File size: 16.3 KB
RevLine 
[92da231]1r"""
[eafc9fa]2DLL driver for C kernels
[750ffa5]3
[3764ec1]4If the environment variable *SAS_OPENMP* is set, then sasmodels
5will attempt to compile with OpenMP flags so that the model can use all
6available kernels.  This may or may not be available on your compiler
7toolchain.  Depending on operating system and environment.
[92da231]8
[3764ec1]9Windows does not have provide a compiler with the operating system.
10Instead, we assume that TinyCC is installed and available.  This can
11be done with a simple pip command if it is not already available::
[92da231]12
[3764ec1]13    pip install tinycc
[92da231]14
[3764ec1]15If Microsoft Visual C++ is available (because VCINSTALLDIR is
16defined in the environment), then that will be used instead.
17Microsoft Visual C++ for Python is available from Microsoft:
[92da231]18
[d138d43]19    `<http://www.microsoft.com/en-us/download/details.aspx?id=44266>`_
[92da231]20
[3764ec1]21If neither compiler is available, sasmodels will check for *MinGW*,
22the GNU compiler toolchain. This available in packages such as Anaconda
23and PythonXY, or available stand alone. This toolchain has had
24difficulties on some systems, and may or may not work for you.
25
26You can control which compiler to use by setting SAS_COMPILER in the
27environment:
28
29  - tinycc (Windows): use the TinyCC compiler shipped with SasView
30  - msvc (Windows): use the Microsoft Visual C++ compiler
31  - mingw (Windows): use the MinGW GNU cc compiler
32  - unix (Linux): use the system cc compiler.
33  - unix (Mac): use the clang compiler. You will need XCode installed, and
34    the XCode command line tools. Mac comes with OpenCL drivers, so generally
35    this will not be needed.
36
37Both *msvc* and *mingw* require that the compiler is available on your path.
38For *msvc*, this can done by running vcvarsall.bat in a windows terminal.
39Install locations are system dependent, such as:
[92da231]40
[2d81cfe]41    C:\Program Files (x86)\Common Files\Microsoft\Visual
42    C++ for Python\9.0\vcvarsall.bat
[92da231]43
44or maybe
45
[2d81cfe]46    C:\Users\yourname\AppData\Local\Programs\Common\Microsoft\Visual
47    C++ for Python\9.0\vcvarsall.bat
[92da231]48
[3764ec1]49OpenMP for *msvc* requires the Microsoft vcomp90.dll library, which doesn't
50seem to be included with the compiler, nor does there appear to be a public
51download location.  There may be one on your machine already in a location
52such as:
[92da231]53
54    C:\Windows\winsxs\x86_microsoft.vc90.openmp*\vcomp90.dll
55
[3764ec1]56If you copy this to somewhere on your path, such as the python directory or
57the install directory for this application, then OpenMP should be supported.
58
59For full control of the compiler, define a function
60*compile_command(source,output)* which takes the name of the source file
61and the name of the output file and returns a compile command that can be
62evaluated in the shell.  For even more control, replace the entire
63*compile(source,output)* function.
64
65The global attribute *ALLOW_SINGLE_PRECISION_DLLS* should be set to *False* if
66you wish to prevent single precision floating point evaluation for the compiled
67models, otherwise set it defaults to *True*.
[14de349]68"""
[eafc9fa]69from __future__ import print_function
[750ffa5]70
[5d4777d]71import sys
72import os
[40a87fa]73from os.path import join as joinpath, splitext
[7e16db7]74import subprocess
[df4dc86]75import tempfile
[7ae2b7f]76import ctypes as ct  # type: ignore
[886fa25]77import _ctypes as _ct
[821a9c6]78import logging
[14de349]79
[7ae2b7f]80import numpy as np  # type: ignore
[14de349]81
[739aad4]82try:
83    import tinycc
84except ImportError:
85    tinycc = None
86
[cb6ecf4]87from . import generate
[f619de7]88from .kernel import KernelModel, Kernel
89from .kernelpy import PyInput
[2c801fe]90from .exception import annotate_exception
[f619de7]91from .generate import F16, F32, F64
92
[2d81cfe]93# pylint: disable=unused-import
[f619de7]94try:
95    from typing import Tuple, Callable, Any
96    from .modelinfo import ModelInfo
97    from .details import CallDetails
98except ImportError:
99    pass
[2d81cfe]100# pylint: enable=unused-import
[14de349]101
[3199b17]102# Compiler output is a byte stream that needs to be decode in python 3.
[e44432d]103decode = (lambda s: s) if sys.version_info[0] < 3 else (lambda s: s.decode('utf8'))
104
[1a3559f]105if "SAS_DLL_PATH" in os.environ:
106    SAS_DLL_PATH = os.environ["SAS_DLL_PATH"]
107else:
108    # Assume the default location of module DLLs is in .sasmodels/compiled_models.
109    SAS_DLL_PATH = os.path.join(os.path.expanduser("~"), ".sasmodels", "compiled_models")
110
[739aad4]111if "SAS_COMPILER" in os.environ:
[b2f1d2f]112    COMPILER = os.environ["SAS_COMPILER"]
[739aad4]113elif os.name == 'nt':
[b2f1d2f]114    if tinycc is not None:
115        COMPILER = "tinycc"
116    elif "VCINSTALLDIR" in os.environ:
[3199b17]117        # If vcvarsall.bat has been called, then VCINSTALLDIR is in the
118        # environment and we can use the MSVC compiler.  Otherwise, if
119        # tinycc is available then use it.  Otherwise, hope that mingw
120        # is available.
[b2f1d2f]121        COMPILER = "msvc"
[fb69211]122    else:
[b2f1d2f]123        COMPILER = "mingw"
[5d4777d]124else:
[b2f1d2f]125    COMPILER = "unix"
[739aad4]126
[3199b17]127ARCH = "" if ct.sizeof(ct.c_void_p) > 4 else "x86"  # 4 byte pointers on x86.
[b2f1d2f]128if COMPILER == "unix":
[3199b17]129    # Generic unix compile.
130    # On Mac users will need the X code command line tools installed.
[7e16db7]131    #COMPILE = "gcc-mp-4.7 -shared -fPIC -std=c99 -fopenmp -O2 -Wall %s -o %s -lm -lgomp"
[aa343d6]132    CC = "cc -shared -fPIC -std=c99 -O2 -Wall".split()
[3199b17]133    # Add OpenMP support if not running on a Mac.
[7e16db7]134    if sys.platform != "darwin":
[3199b17]135        # OpenMP seems to be broken on gcc 5.4.0 (ubuntu 16.04.9).
[33969b6]136        # Shut it off for all unix until we can investigate.
137        #CC.append("-fopenmp")
138        pass
[7e16db7]139    def compile_command(source, output):
[40a87fa]140        """unix compiler command"""
[15e74ad]141        return CC + [source, "-o", output, "-lm"]
[b2f1d2f]142elif COMPILER == "msvc":
[739aad4]143    # Call vcvarsall.bat before compiling to set path, headers, libs, etc.
144    # MSVC compiler is available, so use it.  OpenMP requires a copy of
145    # vcomp90.dll on the path.  One may be found here:
146    #       C:/Windows/winsxs/x86_microsoft.vc90.openmp*/vcomp90.dll
[3199b17]147    # Copy this to the python directory and uncomment the OpenMP COMPILE.
148    # TODO: Remove intermediate OBJ file created in the directory.
149    # TODO: Maybe don't use randomized name for the c file.
150    # TODO: Maybe ask distutils to find MSVC.
[739aad4]151    CC = "cl /nologo /Ox /MD /W3 /GS- /DNDEBUG".split()
152    if "SAS_OPENMP" in os.environ:
153        CC.append("/openmp")
154    LN = "/link /DLL /INCREMENTAL:NO /MANIFEST".split()
155    def compile_command(source, output):
[40a87fa]156        """MSVC compiler command"""
[739aad4]157        return CC + ["/Tp%s"%source] + LN + ["/OUT:%s"%output]
[b2f1d2f]158elif COMPILER == "tinycc":
[739aad4]159    # TinyCC compiler.
160    CC = [tinycc.TCC] + "-shared -rdynamic -Wall".split()
161    def compile_command(source, output):
[40a87fa]162        """tinycc compiler command"""
[739aad4]163        return CC + [source, "-o", output]
[b2f1d2f]164elif COMPILER == "mingw":
[739aad4]165    # MinGW compiler.
166    CC = "gcc -shared -std=c99 -O2 -Wall".split()
167    if "SAS_OPENMP" in os.environ:
168        CC.append("-fopenmp")
169    def compile_command(source, output):
[40a87fa]170        """mingw compiler command"""
[739aad4]171        return CC + [source, "-o", output, "-lm"]
[df4dc86]172
[5d316e9]173ALLOW_SINGLE_PRECISION_DLLS = True
[5d4777d]174
[3199b17]175
[7e16db7]176def compile(source, output):
[40a87fa]177    # type: (str, str) -> None
178    """
179    Compile *source* producing *output*.
180
181    Raises RuntimeError if the compile failed or the output wasn't produced.
182    """
[7e16db7]183    command = compile_command(source=source, output=output)
184    command_str = " ".join('"%s"'%p if ' ' in p else p for p in command)
185    logging.info(command_str)
186    try:
[3199b17]187        # Need shell=True on windows to keep console box from popping up.
[15e74ad]188        shell = (os.name == 'nt')
[aa343d6]189        subprocess.check_output(command, shell=shell, stderr=subprocess.STDOUT)
[7e16db7]190    except subprocess.CalledProcessError as exc:
[e44432d]191        output = decode(exc.output)
192        raise RuntimeError("compile failed.\n%s\n%s"%(command_str, output))
[7e16db7]193    if not os.path.exists(output):
194        raise RuntimeError("compile failed.  File is in %r"%source)
[750ffa5]195
[3199b17]196
[151f3bc]197def dll_name(model_info, dtype):
[f619de7]198    # type: (ModelInfo, np.dtype) ->  str
[5d4777d]199    """
[151f3bc]200    Name of the dll containing the model.  This is the base file name without
201    any path or extension, with a form such as 'sas_sphere32'.
[5d4777d]202    """
[151f3bc]203    bits = 8*dtype.itemsize
[56b2687]204    basename = "sas%d_%s"%(bits, model_info.id)
[c7118f4]205    basename += ARCH + ".so"
[01b8659]206
[3199b17]207    # Hack to find precompiled dlls.
[c7118f4]208    path = joinpath(generate.DATA_PATH, '..', 'compiled_models', basename)
[01b8659]209    if os.path.exists(path):
210        return path
211
[1a3559f]212    return joinpath(SAS_DLL_PATH, basename)
[5d4777d]213
[f619de7]214
[151f3bc]215def dll_path(model_info, dtype):
[f619de7]216    # type: (ModelInfo, np.dtype) -> str
[151f3bc]217    """
218    Complete path to the dll for the model.  Note that the dll may not
219    exist yet if it hasn't been compiled.
220    """
[1a3559f]221    return os.path.join(SAS_DLL_PATH, dll_name(model_info, dtype))
[5d4777d]222
[f619de7]223
224def make_dll(source, model_info, dtype=F64):
225    # type: (str, ModelInfo, np.dtype) -> str
[5d4777d]226    """
[f619de7]227    Returns the path to the compiled model defined by *kernel_module*.
[5d4777d]228
[f619de7]229    If the model has not been compiled, or if the source file(s) are newer
230    than the dll, then *make_dll* will compile the model before returning.
231    This routine does not load the resulting dll.
[5d4777d]232
[aa4946b]233    *dtype* is a numpy floating point precision specifier indicating whether
[f619de7]234    the model should be single, double or long double precision.  The default
235    is double precision, *np.dtype('d')*.
[5d4777d]236
[f619de7]237    Set *sasmodels.ALLOW_SINGLE_PRECISION_DLLS* to False if single precision
238    models are not allowed as DLLs.
[aa4946b]239
[1a3559f]240    Set *sasmodels.kerneldll.SAS_DLL_PATH* to the compiled dll output path.
241    Alternatively, set the environment variable *SAS_DLL_PATH*.
[bf94e6e]242    The default is in ~/.sasmodels/compiled_models.
[5d4777d]243    """
[f619de7]244    if dtype == F16:
[5d316e9]245        raise ValueError("16 bit floats not supported")
[f619de7]246    if dtype == F32 and not ALLOW_SINGLE_PRECISION_DLLS:
[3199b17]247        dtype = F64  # Force 64-bit dll.
248    # Note: dtype may be F128 for long double precision.
[750ffa5]249
[17bbadd]250    dll = dll_path(model_info, dtype)
[e1454ab]251
252    if not os.path.exists(dll):
[5a91c6b]253        need_recompile = True
254    else:
255        dll_time = os.path.getmtime(dll)
[0dc34c3]256        newest_source = generate.dll_timestamp(model_info)
[5a91c6b]257        need_recompile = dll_time < newest_source
258    if need_recompile:
[3199b17]259        # Make sure the DLL path exists.
[1a3559f]260        if not os.path.exists(SAS_DLL_PATH):
261            os.makedirs(SAS_DLL_PATH)
[40a87fa]262        basename = splitext(os.path.basename(dll))[0] + "_"
263        system_fd, filename = tempfile.mkstemp(suffix=".c", prefix=basename)
[f619de7]264        source = generate.convert_type(source, dtype)
[40a87fa]265        with os.fdopen(system_fd, "w") as file_handle:
266            file_handle.write(source)
[7e16db7]267        compile(source=filename, output=dll)
[3199b17]268        # Comment the following to keep the generated C file.
269        # Note: If there is a syntax error then compile raises an error
[7e16db7]270        # and the source file will not be deleted.
[c036ddb]271        os.unlink(filename)
272        #print("saving compiled file in %r"%filename)
[aa4946b]273    return dll
274
275
[f619de7]276def load_dll(source, model_info, dtype=F64):
277    # type: (str, ModelInfo, np.dtype) -> "DllModel"
[aa4946b]278    """
[823e620]279    Create and load a dll corresponding to the source, info pair returned
[aa4946b]280    from :func:`sasmodels.generate.make` compiled for the target precision.
281
282    See :func:`make_dll` for details on controlling the dll path and the
283    allowed floating point precision.
284    """
[17bbadd]285    filename = make_dll(source, model_info, dtype=dtype)
286    return DllModel(filename, model_info, dtype=dtype)
[5d4777d]287
[f619de7]288
289class DllModel(KernelModel):
[14de349]290    """
291    ctypes wrapper for a single model.
292
[17bbadd]293    *source* and *model_info* are the model source and interface as returned
[14de349]294    from :func:`gen.make`.
295
296    *dtype* is the desired model precision.  Any numpy dtype for single
297    or double precision floats will do, such as 'f', 'float32' or 'single'
298    for single and 'd', 'float64' or 'double' for double.  Double precision
299    is an optional extension which may not be available on all devices.
[ff7119b]300
301    Call :meth:`release` when done with the kernel.
[14de349]302    """
[17bbadd]303    def __init__(self, dllpath, model_info, dtype=generate.F32):
[f619de7]304        # type: (str, ModelInfo, np.dtype) -> None
[17bbadd]305        self.info = model_info
[ce27e21]306        self.dllpath = dllpath
[f619de7]307        self._dll = None  # type: ct.CDLL
[3199b17]308        self._kernels = None  # type: List[Callable, Callable]
[750ffa5]309        self.dtype = np.dtype(dtype)
[14de349]310
[ce27e21]311    def _load_dll(self):
[f619de7]312        # type: () -> None
[2c801fe]313        try:
[f619de7]314            self._dll = ct.CDLL(self.dllpath)
[4d76711]315        except:
316            annotate_exception("while loading "+self.dllpath)
[2c801fe]317            raise
[14de349]318
[886fa25]319        float_type = (ct.c_float if self.dtype == generate.F32
320                      else ct.c_double if self.dtype == generate.F64
321                      else ct.c_longdouble)
[ce27e21]322
[a738209]323        # int, int, int, int*, double*, double*, double*, double*, double
[6e7ba14]324        argtypes = [ct.c_int32]*3 + [ct.c_void_p]*4 + [float_type, ct.c_int32]
[9eb3632]325        names = [generate.kernel_name(self.info, variant)
326                 for variant in ("Iq", "Iqxy", "Imagnetic")]
327        self._kernels = [self._dll[name] for name in names]
328        for k in self._kernels:
329            k.argtypes = argtypes
[ce27e21]330
331    def __getstate__(self):
[f619de7]332        # type: () -> Tuple[ModelInfo, str]
[eafc9fa]333        return self.info, self.dllpath
[ce27e21]334
335    def __setstate__(self, state):
[f619de7]336        # type: (Tuple[ModelInfo, str]) -> None
[eafc9fa]337        self.info, self.dllpath = state
[f619de7]338        self._dll = None
[ce27e21]339
[48fbd50]340    def make_kernel(self, q_vectors):
[f619de7]341        # type: (List[np.ndarray]) -> DllKernel
[eafc9fa]342        q_input = PyInput(q_vectors, self.dtype)
[3199b17]343        # Note: DLL is lazy loaded.
[f619de7]344        if self._dll is None:
345            self._load_dll()
[9eb3632]346        is_2d = len(q_vectors) == 2
347        kernel = self._kernels[1:3] if is_2d else [self._kernels[0]]*2
[3c56da87]348        return DllKernel(kernel, self.info, q_input)
[4d76711]349
[eafc9fa]350    def release(self):
[f619de7]351        # type: () -> None
[14de349]352        """
[eafc9fa]353        Release any resources associated with the model.
[14de349]354        """
[886fa25]355        dll_handle = self._dll._handle
[6ad0e87]356        if os.name == 'nt':
[40a87fa]357            ct.windll.kernel32.FreeLibrary(dll_handle)
358        else:
[886fa25]359            _ct.dlclose(dll_handle)
360        del self._dll
361        self._dll = None
[14de349]362
[3199b17]363
[f619de7]364class DllKernel(Kernel):
[ff7119b]365    """
366    Callable SAS kernel.
367
[b3f6bc3]368    *kernel* is the c function to call.
[ff7119b]369
[17bbadd]370    *model_info* is the module information
[ff7119b]371
[3c56da87]372    *q_input* is the DllInput q vectors at which the kernel should be
[ff7119b]373    evaluated.
374
375    The resulting call method takes the *pars*, a list of values for
[823e620]376    the fixed parameters to the kernel, and *pd_pars*, a list of (value, weight)
[ff7119b]377    vectors for the polydisperse parameters.  *cutoff* determines the
378    integration limits: any points with combined weight less than *cutoff*
379    will not be calculated.
380
381    Call :meth:`release` when done with the kernel instance.
382    """
[17bbadd]383    def __init__(self, kernel, model_info, q_input):
[f619de7]384        # type: (Callable[[], np.ndarray], ModelInfo, PyInput) -> None
[3199b17]385        dtype = q_input.dtype
[3c56da87]386        self.q_input = q_input
[3199b17]387        self.kernel = kernel
388
389        # Attributes accessed from the outside.
[445d1c0]390        self.dim = '2d' if q_input.is_2d else '1d'
[3199b17]391        self.info = model_info
392        self.dtype = dtype
393
394        # Converter to translate input to target type.
395        self._as_dtype = (np.float32 if dtype == generate.F32
396                          else np.float64 if dtype == generate.F64
397                          else np.float128)
398
399        # Holding place for the returned value.
[6e7ba14]400        nout = 2 if self.info.have_Fq else 1
[3199b17]401        extra_q = 4  # Total weight, form volume, shell volume and R_eff.
402        self.result = np.empty(self.q_input.nq*nout + extra_q, dtype)
403
404    def _call_kernel(self, call_details, values, cutoff, magnetic,
405                     effective_radius_type):
406        # type: (CallDetails, np.ndarray, float, bool, int) -> np.ndarray
407
408        # Setup kernel function and arguments.
[9eb3632]409        kernel = self.kernel[1 if magnetic else 0]
[3199b17]410        kernel_args = [
411            self.q_input.nq,  # Number of inputs.
412            None,  # Placeholder for pd_start.
413            None,  # Placeholder for pd_stop.
414            call_details.buffer.ctypes.data,  # Problem definition.
415            values.ctypes.data,  # Parameter values.
416            self.q_input.q.ctypes.data,  # Q values.
417            self.result.ctypes.data,   # Result storage.
418            self._as_dtype(cutoff),  # Probability cutoff.
419            effective_radius_type,  # R_eff mode.
[9eb3632]420        ]
[3199b17]421
422        # Call kernel and retrieve results.
[c036ddb]423        #print("Calling DLL")
[bde38b5]424        #call_details.show(values)
[9eb3632]425        step = 100
[3199b17]426        # TODO: Do we need the explicit sleep like the OpenCL and CUDA loops?
[bde38b5]427        for start in range(0, call_details.num_eval, step):
428            stop = min(start + step, call_details.num_eval)
[3199b17]429            kernel_args[1:3] = [start, stop]
430            kernel(*kernel_args) # type: ignore
[9eb3632]431
[14de349]432    def release(self):
[f619de7]433        # type: () -> None
[eafc9fa]434        """
[3199b17]435        Release resources associated with the kernel.
[eafc9fa]436        """
[3199b17]437        # TODO: OpenCL/CUDA allocate q_input in __init__ and free it in release.
438        # Should we be doing the same for DLL?
439        #self.q_input.release()
440        pass
441
442    def __del__(self):
443        # type: () -> None
444        self.release()
Note: See TracBrowser for help on using the repository browser.