source: sasmodels/sasmodels/kerneldll.py @ 9acbd37

Last change on this file since 9acbd37 was 9acbd37, checked in by wojciech, 8 years ago

Unloading lib from ctypes by triggering release function in kerneldll ref #576

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