source: sasmodels/sasmodels/kerneldll.py @ bf94e6e

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

move compiled dlls from /tmp to ~/.sasmodels/compiled_models on unix

  • Property mode set to 100644
File size: 15.2 KB
Line 
1r"""
2DLL driver for C kernels
3
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.
8
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::
12
13    pip install tinycc
14
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:
18
19    `<http://www.microsoft.com/en-us/download/details.aspx?id=44266>`_
20
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:
40
41    C:\Program Files (x86)\Common Files\Microsoft\Visual
42    C++ for Python\9.0\vcvarsall.bat
43
44or maybe
45
46    C:\Users\yourname\AppData\Local\Programs\Common\Microsoft\Visual
47    C++ for Python\9.0\vcvarsall.bat
48
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:
53
54    C:\Windows\winsxs\x86_microsoft.vc90.openmp*\vcomp90.dll
55
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*.
68"""
69from __future__ import print_function
70
71import sys
72import os
73from os.path import join as joinpath, splitext
74import subprocess
75import tempfile
76import ctypes as ct  # type: ignore
77import _ctypes as _ct
78import logging
79
80import numpy as np  # type: ignore
81
82try:
83    import tinycc
84except ImportError:
85    tinycc = None
86
87from . import generate
88from .kernel import KernelModel, Kernel
89from .kernelpy import PyInput
90from .exception import annotate_exception
91from .generate import F16, F32, F64
92
93# pylint: disable=unused-import
94try:
95    from typing import Tuple, Callable, Any
96    from .modelinfo import ModelInfo
97    from .details import CallDetails
98except ImportError:
99    pass
100# pylint: enable=unused-import
101
102if "SAS_COMPILER" in os.environ:
103    COMPILER = os.environ["SAS_COMPILER"]
104elif os.name == 'nt':
105    if tinycc is not None:
106        COMPILER = "tinycc"
107    elif "VCINSTALLDIR" in os.environ:
108        # If vcvarsall.bat has been called, then VCINSTALLDIR is in the environment
109        # and we can use the MSVC compiler.  Otherwise, if tinycc is available
110        # the use it.  Otherwise, hope that mingw is available.
111        COMPILER = "msvc"
112    else:
113        COMPILER = "mingw"
114else:
115    COMPILER = "unix"
116
117ARCH = "" if ct.sizeof(ct.c_void_p) > 4 else "x86"  # 4 byte pointers on x86
118if COMPILER == "unix":
119    # Generic unix compile
120    # On mac users will need the X code command line tools installed
121    #COMPILE = "gcc-mp-4.7 -shared -fPIC -std=c99 -fopenmp -O2 -Wall %s -o %s -lm -lgomp"
122    CC = "cc -shared -fPIC -std=c99 -O2 -Wall".split()
123    # add openmp support if not running on a mac
124    if sys.platform != "darwin":
125        CC.append("-fopenmp")
126    def compile_command(source, output):
127        """unix compiler command"""
128        return CC + [source, "-o", output, "-lm"]
129elif COMPILER == "msvc":
130    # Call vcvarsall.bat before compiling to set path, headers, libs, etc.
131    # MSVC compiler is available, so use it.  OpenMP requires a copy of
132    # vcomp90.dll on the path.  One may be found here:
133    #       C:/Windows/winsxs/x86_microsoft.vc90.openmp*/vcomp90.dll
134    # Copy this to the python directory and uncomment the OpenMP COMPILE
135    # TODO: remove intermediate OBJ file created in the directory
136    # TODO: maybe don't use randomized name for the c file
137    # TODO: maybe ask distutils to find MSVC
138    CC = "cl /nologo /Ox /MD /W3 /GS- /DNDEBUG".split()
139    if "SAS_OPENMP" in os.environ:
140        CC.append("/openmp")
141    LN = "/link /DLL /INCREMENTAL:NO /MANIFEST".split()
142    def compile_command(source, output):
143        """MSVC compiler command"""
144        return CC + ["/Tp%s"%source] + LN + ["/OUT:%s"%output]
145elif COMPILER == "tinycc":
146    # TinyCC compiler.
147    CC = [tinycc.TCC] + "-shared -rdynamic -Wall".split()
148    def compile_command(source, output):
149        """tinycc compiler command"""
150        return CC + [source, "-o", output]
151elif COMPILER == "mingw":
152    # MinGW compiler.
153    CC = "gcc -shared -std=c99 -O2 -Wall".split()
154    if "SAS_OPENMP" in os.environ:
155        CC.append("-fopenmp")
156    def compile_command(source, output):
157        """mingw compiler command"""
158        return CC + [source, "-o", output, "-lm"]
159
160# Assume the default location of module DLLs is in .sasmodels/compiled_models.
161DLL_PATH = os.path.join(os.path.expanduser("~"), ".sasmodels", "compiled_models")
162
163ALLOW_SINGLE_PRECISION_DLLS = True
164
165def compile(source, output):
166    # type: (str, str) -> None
167    """
168    Compile *source* producing *output*.
169
170    Raises RuntimeError if the compile failed or the output wasn't produced.
171    """
172    command = compile_command(source=source, output=output)
173    command_str = " ".join('"%s"'%p if ' ' in p else p for p in command)
174    logging.info(command_str)
175    try:
176        # need shell=True on windows to keep console box from popping up
177        shell = (os.name == 'nt')
178        subprocess.check_output(command, shell=shell, stderr=subprocess.STDOUT)
179    except subprocess.CalledProcessError as exc:
180        raise RuntimeError("compile failed.\n%s\n%s"%(command_str, exc.output))
181    if not os.path.exists(output):
182        raise RuntimeError("compile failed.  File is in %r"%source)
183
184def dll_name(model_info, dtype):
185    # type: (ModelInfo, np.dtype) ->  str
186    """
187    Name of the dll containing the model.  This is the base file name without
188    any path or extension, with a form such as 'sas_sphere32'.
189    """
190    bits = 8*dtype.itemsize
191    basename = "sas%d_%s"%(bits, model_info.id)
192    basename += ARCH + ".so"
193
194    # Hack to find precompiled dlls
195    path = joinpath(generate.DATA_PATH, '..', 'compiled_models', basename)
196    if os.path.exists(path):
197        return path
198
199    return joinpath(DLL_PATH, basename)
200
201
202def dll_path(model_info, dtype):
203    # type: (ModelInfo, np.dtype) -> str
204    """
205    Complete path to the dll for the model.  Note that the dll may not
206    exist yet if it hasn't been compiled.
207    """
208    return os.path.join(DLL_PATH, dll_name(model_info, dtype))
209
210
211def make_dll(source, model_info, dtype=F64):
212    # type: (str, ModelInfo, np.dtype) -> str
213    """
214    Returns the path to the compiled model defined by *kernel_module*.
215
216    If the model has not been compiled, or if the source file(s) are newer
217    than the dll, then *make_dll* will compile the model before returning.
218    This routine does not load the resulting dll.
219
220    *dtype* is a numpy floating point precision specifier indicating whether
221    the model should be single, double or long double precision.  The default
222    is double precision, *np.dtype('d')*.
223
224    Set *sasmodels.ALLOW_SINGLE_PRECISION_DLLS* to False if single precision
225    models are not allowed as DLLs.
226
227    Set *sasmodels.kerneldll.DLL_PATH* to the compiled dll output path.
228    The default is in ~/.sasmodels/compiled_models.
229    """
230    if dtype == F16:
231        raise ValueError("16 bit floats not supported")
232    if dtype == F32 and not ALLOW_SINGLE_PRECISION_DLLS:
233        dtype = F64  # Force 64-bit dll
234    # Note: dtype may be F128 for long double precision
235
236    dll = dll_path(model_info, dtype)
237
238    if not os.path.exists(dll):
239        need_recompile = True
240    else:
241        dll_time = os.path.getmtime(dll)
242        newest_source = generate.dll_timestamp(model_info)
243        need_recompile = dll_time < newest_source
244    if need_recompile:
245        # Make sure the DLL path exists
246        if not os.path.exists(DLL_PATH):
247            os.makedirs(DLL_PATH)
248        basename = splitext(os.path.basename(dll))[0] + "_"
249        system_fd, filename = tempfile.mkstemp(suffix=".c", prefix=basename)
250        source = generate.convert_type(source, dtype)
251        with os.fdopen(system_fd, "w") as file_handle:
252            file_handle.write(source)
253        compile(source=filename, output=dll)
254        # comment the following to keep the generated c file
255        # Note: if there is a syntax error then compile raises an error
256        # and the source file will not be deleted.
257        os.unlink(filename)
258        #print("saving compiled file in %r"%filename)
259    return dll
260
261
262def load_dll(source, model_info, dtype=F64):
263    # type: (str, ModelInfo, np.dtype) -> "DllModel"
264    """
265    Create and load a dll corresponding to the source, info pair returned
266    from :func:`sasmodels.generate.make` compiled for the target precision.
267
268    See :func:`make_dll` for details on controlling the dll path and the
269    allowed floating point precision.
270    """
271    filename = make_dll(source, model_info, dtype=dtype)
272    return DllModel(filename, model_info, dtype=dtype)
273
274
275class DllModel(KernelModel):
276    """
277    ctypes wrapper for a single model.
278
279    *source* and *model_info* are the model source and interface as returned
280    from :func:`gen.make`.
281
282    *dtype* is the desired model precision.  Any numpy dtype for single
283    or double precision floats will do, such as 'f', 'float32' or 'single'
284    for single and 'd', 'float64' or 'double' for double.  Double precision
285    is an optional extension which may not be available on all devices.
286
287    Call :meth:`release` when done with the kernel.
288    """
289    def __init__(self, dllpath, model_info, dtype=generate.F32):
290        # type: (str, ModelInfo, np.dtype) -> None
291        self.info = model_info
292        self.dllpath = dllpath
293        self._dll = None  # type: ct.CDLL
294        self._kernels = None # type: List[Callable, Callable]
295        self.dtype = np.dtype(dtype)
296
297    def _load_dll(self):
298        # type: () -> None
299        try:
300            self._dll = ct.CDLL(self.dllpath)
301        except:
302            annotate_exception("while loading "+self.dllpath)
303            raise
304
305        float_type = (ct.c_float if self.dtype == generate.F32
306                      else ct.c_double if self.dtype == generate.F64
307                      else ct.c_longdouble)
308
309        # int, int, int, int*, double*, double*, double*, double*, double
310        argtypes = [ct.c_int32]*3 + [ct.c_void_p]*4 + [float_type]
311        names = [generate.kernel_name(self.info, variant)
312                 for variant in ("Iq", "Iqxy", "Imagnetic")]
313        self._kernels = [self._dll[name] for name in names]
314        for k in self._kernels:
315            k.argtypes = argtypes
316
317    def __getstate__(self):
318        # type: () -> Tuple[ModelInfo, str]
319        return self.info, self.dllpath
320
321    def __setstate__(self, state):
322        # type: (Tuple[ModelInfo, str]) -> None
323        self.info, self.dllpath = state
324        self._dll = None
325
326    def make_kernel(self, q_vectors):
327        # type: (List[np.ndarray]) -> DllKernel
328        q_input = PyInput(q_vectors, self.dtype)
329        # Note: pickle not supported for DllKernel
330        if self._dll is None:
331            self._load_dll()
332        is_2d = len(q_vectors) == 2
333        kernel = self._kernels[1:3] if is_2d else [self._kernels[0]]*2
334        return DllKernel(kernel, self.info, q_input)
335
336    def release(self):
337        # type: () -> None
338        """
339        Release any resources associated with the model.
340        """
341        dll_handle = self._dll._handle
342        if os.name == 'nt':
343            ct.windll.kernel32.FreeLibrary(dll_handle)
344        else:
345            _ct.dlclose(dll_handle)
346        del self._dll
347        self._dll = None
348
349class DllKernel(Kernel):
350    """
351    Callable SAS kernel.
352
353    *kernel* is the c function to call.
354
355    *model_info* is the module information
356
357    *q_input* is the DllInput q vectors at which the kernel should be
358    evaluated.
359
360    The resulting call method takes the *pars*, a list of values for
361    the fixed parameters to the kernel, and *pd_pars*, a list of (value, weight)
362    vectors for the polydisperse parameters.  *cutoff* determines the
363    integration limits: any points with combined weight less than *cutoff*
364    will not be calculated.
365
366    Call :meth:`release` when done with the kernel instance.
367    """
368    def __init__(self, kernel, model_info, q_input):
369        # type: (Callable[[], np.ndarray], ModelInfo, PyInput) -> None
370        self.kernel = kernel
371        self.info = model_info
372        self.q_input = q_input
373        self.dtype = q_input.dtype
374        self.dim = '2d' if q_input.is_2d else '1d'
375        self.result = np.empty(q_input.nq+1, q_input.dtype)
376        self.real = (np.float32 if self.q_input.dtype == generate.F32
377                     else np.float64 if self.q_input.dtype == generate.F64
378                     else np.float128)
379
380    def __call__(self, call_details, values, cutoff, magnetic):
381        # type: (CallDetails, np.ndarray, np.ndarray, float, bool) -> np.ndarray
382
383        kernel = self.kernel[1 if magnetic else 0]
384        args = [
385            self.q_input.nq, # nq
386            None, # pd_start
387            None, # pd_stop pd_stride[MAX_PD]
388            call_details.buffer.ctypes.data, # problem
389            values.ctypes.data,  #pars
390            self.q_input.q.ctypes.data, #q
391            self.result.ctypes.data,   # results
392            self.real(cutoff), # cutoff
393        ]
394        #print("Calling DLL")
395        #call_details.show(values)
396        step = 100
397        for start in range(0, call_details.num_eval, step):
398            stop = min(start + step, call_details.num_eval)
399            args[1:3] = [start, stop]
400            kernel(*args) # type: ignore
401
402        #print("returned",self.q_input.q, self.result)
403        pd_norm = self.result[self.q_input.nq]
404        scale = values[0]/(pd_norm if pd_norm != 0.0 else 1.0)
405        background = values[1]
406        #print("scale",scale,background)
407        return scale*self.result[:self.q_input.nq] + background
408
409    def release(self):
410        # type: () -> None
411        """
412        Release any resources associated with the kernel.
413        """
414        self.q_input.release()
Note: See TracBrowser for help on using the repository browser.