source: sasmodels/sasmodels/kerneldll.py @ ff97458

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since ff97458 was ff97458, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

Precompiled models are a Windows-only feature.

  • Property mode set to 100644
File size: 11.8 KB
Line 
1r"""
2DLL driver for C kernels
3
4The global attribute *ALLOW_SINGLE_PRECISION_DLLS* should be set to *True* if
5you wish to allow single precision floating point evaluation for the compiled
6models, otherwise it defaults to *False*.
7
8The compiler command line is stored in the attribute *COMPILE*, with string
9substitutions for %(source)s and %(output)s indicating what to compile and
10where to store it.  The actual command is system dependent.
11
12On windows systems, you have a choice of compilers.  *MinGW* is the GNU
13compiler toolchain, available in packages such as anaconda and PythonXY,
14or available stand alone. This toolchain has had difficulties on some
15systems, and may or may not work for you.  In order to build DLLs, *gcc*
16must be on your path.  If the environment variable *SAS_OPENMP* is given
17then -fopenmp is added to the compiler flags.  This requires a version
18of MinGW compiled with OpenMP support.
19
20An alternative toolchain uses the Microsoft Visual C++ compiler, available
21free from microsoft:
22
23    `<http://www.microsoft.com/en-us/download/details.aspx?id=44266>`_
24
25Again, this requires that the compiler is available on your path.  This is
26done by running vcvarsall.bat in a windows terminal.  Install locations are
27system dependent, such as:
28
29    C:\Program Files (x86)\Common Files\Microsoft\Visual C++ for Python\9.0\vcvarsall.bat
30
31or maybe
32
33    C:\Users\yourname\AppData\Local\Programs\Common\Microsoft\Visual C++ for Python\9.0\vcvarsall.bat
34
35And again, the environment variable *SAS_OPENMP* controls whether OpenMP is
36used to compile the C code.  This requires the Microsoft vcomp90.dll library,
37which doesn't seem to be included with the compiler, nor does there appear
38to be a public download location.  There may be one on your machine already
39in a location such as:
40
41    C:\Windows\winsxs\x86_microsoft.vc90.openmp*\vcomp90.dll
42
43If you copy this onto your path, such as the python directory or the install
44directory for this application, then OpenMP should be supported.
45"""
46from __future__ import print_function
47
48import sys
49import os
50from os.path import join as joinpath, split as splitpath, realpath, splitext
51import tempfile
52import ctypes as ct
53from ctypes import c_void_p, c_int, c_longdouble, c_double, c_float
54import _ctypes
55
56import numpy as np
57
58from . import generate
59from .kernelpy import PyInput, PyModel
60from .exception import annotate_exception
61
62# Compiler platform details
63if sys.platform == 'darwin':
64    #COMPILE = "gcc-mp-4.7 -shared -fPIC -std=c99 -fopenmp -O2 -Wall %s -o %s -lm -lgomp"
65    COMPILE = "gcc -shared -fPIC -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
66elif os.name == 'nt':
67    # call vcvarsall.bat before compiling to set path, headers, libs, etc.
68    if "VCINSTALLDIR" in os.environ:
69        # MSVC compiler is available, so use it.  OpenMP requires a copy of
70        # vcomp90.dll on the path.  One may be found here:
71        #       C:/Windows/winsxs/x86_microsoft.vc90.openmp*/vcomp90.dll
72        # Copy this to the python directory and uncomment the OpenMP COMPILE
73        # TODO: remove intermediate OBJ file created in the directory
74        # TODO: maybe don't use randomized name for the c file
75        CC = "cl /nologo /Ox /MD /W3 /GS- /DNDEBUG /Tp%(source)s "
76        LN = "/link /DLL /INCREMENTAL:NO /MANIFEST /OUT:%(output)s"
77        if "SAS_OPENMP" in os.environ:
78            COMPILE = " ".join((CC, "/openmp", LN))
79        else:
80            COMPILE = " ".join((CC, LN))
81    elif True:  # Don't use mingw
82        # fPIC is not needed on windows
83        COMPILE = "gcc -shared -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
84        if "SAS_OPENMP" in os.environ:
85            COMPILE = COMPILE + " -fopenmp"
86    else:
87        from tinycc import TCC
88        COMPILE = TCC + " -shared -rdynamic -Wall %(source)s -o %(output)s"
89else:
90    COMPILE = "cc -shared -fPIC -fopenmp -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
91
92# Windows-specific solution
93if os.name == 'nt':
94    # Assume the default location of module DLLs is in top level /models dir.
95    # Relying on the Windows installer placing target items in the right place
96    DLL_PATH = os.path.join(os.path.split(os.path.realpath(sys.argv[0]))[0], "models")
97else:
98    # Set up the default path for compiled modules.
99    DLL_PATH = tempfile.gettempdir()
100
101ALLOW_SINGLE_PRECISION_DLLS = True
102
103
104def dll_path(model_info, dtype="double"):
105    """
106    Path to the compiled model defined by *model_info*.
107    """
108    basename = splitext(splitpath(model_info['filename'])[1])[0]
109    if np.dtype(dtype) == generate.F32:
110        basename += "32"
111    elif np.dtype(dtype) == generate.F64:
112        basename += "64"
113    else:
114        basename += "128"
115    return joinpath(DLL_PATH, basename+'.so')
116
117def make_dll(source, model_info, dtype="double"):
118    """
119    Load the compiled model defined by *kernel_module*.
120
121    Recompile if any files are newer than the model file.
122
123    *dtype* is a numpy floating point precision specifier indicating whether
124    the model should be single or double precision.  The default is double
125    precision.
126
127    The DLL is not loaded until the kernel is called so models can
128    be defined without using too many resources.
129
130    Set *sasmodels.kerneldll.DLL_PATH* to the compiled dll output path.
131    The default is the system temporary directory.
132
133    Set *sasmodels.ALLOW_SINGLE_PRECISION_DLLS* to True if single precision
134    models are allowed as DLLs.
135    """
136    if callable(model_info.get('Iq', None)):
137        return PyModel(model_info)
138   
139    dtype = np.dtype(dtype)
140    if dtype == generate.F16:
141        raise ValueError("16 bit floats not supported")
142    if dtype == generate.F32 and not ALLOW_SINGLE_PRECISION_DLLS:
143        dtype = generate.F64  # Force 64-bit dll
144
145    if dtype == generate.F32: # 32-bit dll
146        tempfile_prefix = 'sas_' + model_info['name'] + '32_'
147    elif dtype == generate.F64:
148        tempfile_prefix = 'sas_' + model_info['name'] + '64_'
149    else:
150        tempfile_prefix = 'sas_' + model_info['name'] + '128_'
151 
152    source = generate.convert_type(source, dtype)
153    source_files = generate.model_sources(model_info) + [model_info['filename']]
154    dll = dll_path(model_info, dtype)
155
156    #newest = max(os.path.getmtime(f) for f in source_files)
157    #if not os.path.exists(dll) or os.path.getmtime(dll) < newest:
158    if not os.path.exists(dll):
159        # Replace with a proper temp file
160        fid, filename = tempfile.mkstemp(suffix=".c", prefix=tempfile_prefix)
161        os.fdopen(fid, "w").write(source)
162        command = COMPILE%{"source":filename, "output":dll}
163        status = os.system(command)
164        if status != 0 or not os.path.exists(dll):
165            raise RuntimeError("compile failed.  File is in %r"%filename)
166        else:
167            ## comment the following to keep the generated c file
168            os.unlink(filename)
169            #print("saving compiled file in %r"%filename)
170    return dll
171
172
173def load_dll(source, model_info, dtype="double"):
174    """
175    Create and load a dll corresponding to the source, info pair returned
176    from :func:`sasmodels.generate.make` compiled for the target precision.
177
178    See :func:`make_dll` for details on controlling the dll path and the
179    allowed floating point precision.
180    """
181    filename = make_dll(source, model_info, dtype=dtype)
182    return DllModel(filename, model_info, dtype=dtype)
183
184
185IQ_ARGS = [c_void_p, c_void_p, c_int]
186IQXY_ARGS = [c_void_p, c_void_p, c_void_p, c_int]
187
188class DllModel(object):
189    """
190    ctypes wrapper for a single model.
191
192    *source* and *model_info* are the model source and interface as returned
193    from :func:`gen.make`.
194
195    *dtype* is the desired model precision.  Any numpy dtype for single
196    or double precision floats will do, such as 'f', 'float32' or 'single'
197    for single and 'd', 'float64' or 'double' for double.  Double precision
198    is an optional extension which may not be available on all devices.
199
200    Call :meth:`release` when done with the kernel.
201    """
202   
203    def __init__(self, dllpath, model_info, dtype=generate.F32):
204        self.info = model_info
205        self.dllpath = dllpath
206        self.dll = None
207        self.dtype = np.dtype(dtype)
208
209    def _load_dll(self):
210        Nfixed1d = len(self.info['partype']['fixed-1d'])
211        Nfixed2d = len(self.info['partype']['fixed-2d'])
212        Npd1d = len(self.info['partype']['pd-1d'])
213        Npd2d = len(self.info['partype']['pd-2d'])
214
215        #print("dll", self.dllpath)
216        try:
217            self.dll = ct.CDLL(self.dllpath)
218        except:
219            annotate_exception("while loading "+self.dllpath)
220            raise
221
222        fp = (c_float if self.dtype == generate.F32
223              else c_double if self.dtype == generate.F64
224              else c_longdouble)
225        pd_args_1d = [c_void_p, fp] + [c_int]*Npd1d if Npd1d else []
226        pd_args_2d = [c_void_p, fp] + [c_int]*Npd2d if Npd2d else []
227        self.Iq = self.dll[generate.kernel_name(self.info, False)]
228        self.Iq.argtypes = IQ_ARGS + pd_args_1d + [fp]*Nfixed1d
229
230        self.Iqxy = self.dll[generate.kernel_name(self.info, True)]
231        self.Iqxy.argtypes = IQXY_ARGS + pd_args_2d + [fp]*Nfixed2d
232       
233        self.release()
234
235    def __getstate__(self):
236        return self.info, self.dllpath
237
238    def __setstate__(self, state):
239        self.info, self.dllpath = state
240        self.dll = None
241
242    def make_kernel(self, q_vectors):
243        q_input = PyInput(q_vectors, self.dtype)
244        if self.dll is None: self._load_dll()
245        kernel = self.Iqxy if q_input.is_2d else self.Iq
246        return DllKernel(kernel, self.info, q_input)
247
248    def release(self):
249        """
250        Release any resources associated with the model.
251        """
252        if os.name == 'nt':
253            #dll = ct.cdll.LoadLibrary(self.dllpath)
254            dll = ct.CDLL(self.dllpath)
255            libHandle = dll._handle
256            #libHandle = ct.c_void_p(dll._handle)
257            del dll, self.dll
258            self.dll = None
259            #_ctypes.FreeLibrary(libHandle)
260            ct.windll.kernel32.FreeLibrary(libHandle)
261        else:   
262            pass 
263
264
265class DllKernel(object):
266    """
267    Callable SAS kernel.
268
269    *kernel* is the c function to call.
270
271    *model_info* is the module information
272
273    *q_input* is the DllInput q vectors at which the kernel should be
274    evaluated.
275
276    The resulting call method takes the *pars*, a list of values for
277    the fixed parameters to the kernel, and *pd_pars*, a list of (value, weight)
278    vectors for the polydisperse parameters.  *cutoff* determines the
279    integration limits: any points with combined weight less than *cutoff*
280    will not be calculated.
281
282    Call :meth:`release` when done with the kernel instance.
283    """
284    def __init__(self, kernel, model_info, q_input):
285        self.info = model_info
286        self.q_input = q_input
287        self.kernel = kernel
288        self.res = np.empty(q_input.nq, q_input.dtype)
289        dim = '2d' if q_input.is_2d else '1d'
290        self.fixed_pars = model_info['partype']['fixed-' + dim]
291        self.pd_pars = model_info['partype']['pd-' + dim]
292
293        # In dll kernel, but not in opencl kernel
294        self.p_res = self.res.ctypes.data
295
296    def __call__(self, fixed_pars, pd_pars, cutoff):
297        real = (np.float32 if self.q_input.dtype == generate.F32
298                else np.float64 if self.q_input.dtype == generate.F64
299                else np.float128)
300
301        nq = c_int(self.q_input.nq)
302        if pd_pars:
303            cutoff = real(cutoff)
304            loops_N = [np.uint32(len(p[0])) for p in pd_pars]
305            loops = np.hstack(pd_pars)
306            loops = np.ascontiguousarray(loops.T, self.q_input.dtype).flatten()
307            p_loops = loops.ctypes.data
308            dispersed = [p_loops, cutoff] + loops_N
309        else:
310            dispersed = []
311        fixed = [real(p) for p in fixed_pars]
312        args = self.q_input.q_pointers + [self.p_res, nq] + dispersed + fixed
313        #print(pars)
314        self.kernel(*args)
315
316        return self.res
317
318    def release(self):
319        """
320        Release any resources associated with the kernel.
321        """
322        pass
Note: See TracBrowser for help on using the repository browser.