source: sasmodels/sasmodels/kerneldll.py @ 33af590

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 33af590 was 821a9c6, checked in by Paul Kienzle <pkienzle@…>, 8 years ago

log the compile commands

  • Property mode set to 100644
File size: 12.3 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, splitext
51import tempfile
52import ctypes as ct
53from ctypes import c_void_p, c_int, c_longdouble, c_double, c_float
54import logging
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:
82        # If MSVC compiler is not available, try using mingw
83        # fPIC is not needed on windows
84        COMPILE = "gcc -shared -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
85        if "SAS_OPENMP" in os.environ:
86            COMPILE = COMPILE + " -fopenmp"
87    else:
88        # If MSVC compiler is not available, try using tinycc
89        from tinycc import TCC
90        COMPILE = TCC + " -shared -rdynamic -Wall %(source)s -o %(output)s"
91else:
92    COMPILE = "cc -shared -fPIC -fopenmp -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
93
94# Windows-specific solution
95if os.name == 'nt':
96    # Assume the default location of module DLLs is in .sasmodels/compiled_models.
97    DLL_PATH = os.path.join(os.path.expanduser("~"), ".sasmodels", "compiled_models")
98    if not os.path.exists(DLL_PATH):
99        os.makedirs(DLL_PATH)
100else:
101    # Set up the default path for compiled modules.
102    DLL_PATH = tempfile.gettempdir()
103
104ALLOW_SINGLE_PRECISION_DLLS = True
105
106
107def dll_path(model_info, dtype="double"):
108    """
109    Path to the compiled model defined by *model_info*.
110    """
111    basename = splitext(splitpath(model_info['filename'])[1])[0]
112    if np.dtype(dtype) == generate.F32:
113        basename += "32"
114    elif np.dtype(dtype) == generate.F64:
115        basename += "64"
116    else:
117        basename += "128"
118
119    # Hack to find precompiled dlls
120    path = joinpath(generate.DATA_PATH, '..', 'compiled_models', basename+'.so')
121    if os.path.exists(path):
122        return path
123
124    return joinpath(DLL_PATH, basename+'.so')
125
126def make_dll(source, model_info, dtype="double"):
127    """
128    Load the compiled model defined by *kernel_module*.
129
130    Recompile if any files are newer than the model file.
131
132    *dtype* is a numpy floating point precision specifier indicating whether
133    the model should be single or double precision.  The default is double
134    precision.
135
136    The DLL is not loaded until the kernel is called so models can
137    be defined without using too many resources.
138
139    Set *sasmodels.kerneldll.DLL_PATH* to the compiled dll output path.
140    The default is the system temporary directory.
141
142    Set *sasmodels.ALLOW_SINGLE_PRECISION_DLLS* to True if single precision
143    models are allowed as DLLs.
144    """
145    if callable(model_info.get('Iq', None)):
146        return PyModel(model_info)
147   
148    dtype = np.dtype(dtype)
149    if dtype == generate.F16:
150        raise ValueError("16 bit floats not supported")
151    if dtype == generate.F32 and not ALLOW_SINGLE_PRECISION_DLLS:
152        dtype = generate.F64  # Force 64-bit dll
153
154    if dtype == generate.F32: # 32-bit dll
155        tempfile_prefix = 'sas_' + model_info['name'] + '32_'
156    elif dtype == generate.F64:
157        tempfile_prefix = 'sas_' + model_info['name'] + '64_'
158    else:
159        tempfile_prefix = 'sas_' + model_info['name'] + '128_'
160
161    dll = dll_path(model_info, dtype)
162
163    if not os.path.exists(dll):
164        need_recompile = True
165    elif getattr(sys, 'frozen', None) is not None:
166        # TODO: don't suppress time stamp
167        # Currently suppressing recompile when running in a frozen environment
168        need_recompile = False
169    else:
170        dll_time = os.path.getmtime(dll)
171        source_files = generate.model_sources(model_info) + [model_info['filename']]
172        newest_source = max(os.path.getmtime(f) for f in source_files)
173        need_recompile = dll_time < newest_source
174    if need_recompile:
175        fid, filename = tempfile.mkstemp(suffix=".c", prefix=tempfile_prefix)
176        source = generate.convert_type(source, dtype)
177        os.fdopen(fid, "w").write(source)
178        command = COMPILE%{"source":filename, "output":dll}
179        logging.info(command)
180        status = os.system(command)
181        if status != 0 or not os.path.exists(dll):
182            raise RuntimeError("compile failed.  File is in %r"%filename)
183        else:
184            ## comment the following to keep the generated c file
185            os.unlink(filename)
186            #print("saving compiled file in %r"%filename)
187    return dll
188
189
190def load_dll(source, model_info, dtype="double"):
191    """
192    Create and load a dll corresponding to the source, info pair returned
193    from :func:`sasmodels.generate.make` compiled for the target precision.
194
195    See :func:`make_dll` for details on controlling the dll path and the
196    allowed floating point precision.
197    """
198    filename = make_dll(source, model_info, dtype=dtype)
199    return DllModel(filename, model_info, dtype=dtype)
200
201
202IQ_ARGS = [c_void_p, c_void_p, c_int]
203IQXY_ARGS = [c_void_p, c_void_p, c_void_p, c_int]
204
205class DllModel(object):
206    """
207    ctypes wrapper for a single model.
208
209    *source* and *model_info* are the model source and interface as returned
210    from :func:`gen.make`.
211
212    *dtype* is the desired model precision.  Any numpy dtype for single
213    or double precision floats will do, such as 'f', 'float32' or 'single'
214    for single and 'd', 'float64' or 'double' for double.  Double precision
215    is an optional extension which may not be available on all devices.
216
217    Call :meth:`release` when done with the kernel.
218    """
219   
220    def __init__(self, dllpath, model_info, dtype=generate.F32):
221        self.info = model_info
222        self.dllpath = dllpath
223        self.dll = None
224        self.dtype = np.dtype(dtype)
225
226    def _load_dll(self):
227        Nfixed1d = len(self.info['partype']['fixed-1d'])
228        Nfixed2d = len(self.info['partype']['fixed-2d'])
229        Npd1d = len(self.info['partype']['pd-1d'])
230        Npd2d = len(self.info['partype']['pd-2d'])
231
232        #print("dll", self.dllpath)
233        try:
234            self.dll = ct.CDLL(self.dllpath)
235        except:
236            annotate_exception("while loading "+self.dllpath)
237            raise
238
239        fp = (c_float if self.dtype == generate.F32
240              else c_double if self.dtype == generate.F64
241              else c_longdouble)
242        pd_args_1d = [c_void_p, fp] + [c_int]*Npd1d if Npd1d else []
243        pd_args_2d = [c_void_p, fp] + [c_int]*Npd2d if Npd2d else []
244        self.Iq = self.dll[generate.kernel_name(self.info, False)]
245        self.Iq.argtypes = IQ_ARGS + pd_args_1d + [fp]*Nfixed1d
246
247        self.Iqxy = self.dll[generate.kernel_name(self.info, True)]
248        self.Iqxy.argtypes = IQXY_ARGS + pd_args_2d + [fp]*Nfixed2d
249       
250        self.release()
251
252    def __getstate__(self):
253        return self.info, self.dllpath
254
255    def __setstate__(self, state):
256        self.info, self.dllpath = state
257        self.dll = None
258
259    def make_kernel(self, q_vectors):
260        q_input = PyInput(q_vectors, self.dtype)
261        if self.dll is None: self._load_dll()
262        kernel = self.Iqxy if q_input.is_2d else self.Iq
263        return DllKernel(kernel, self.info, q_input)
264
265    def release(self):
266        """
267        Release any resources associated with the model.
268        """
269        if os.name == 'nt':
270            #dll = ct.cdll.LoadLibrary(self.dllpath)
271            dll = ct.CDLL(self.dllpath)
272            libHandle = dll._handle
273            #libHandle = ct.c_void_p(dll._handle)
274            del dll, self.dll
275            self.dll = None
276            ct.windll.kernel32.FreeLibrary(libHandle)
277        else:   
278            pass 
279
280
281class DllKernel(object):
282    """
283    Callable SAS kernel.
284
285    *kernel* is the c function to call.
286
287    *model_info* is the module information
288
289    *q_input* is the DllInput q vectors at which the kernel should be
290    evaluated.
291
292    The resulting call method takes the *pars*, a list of values for
293    the fixed parameters to the kernel, and *pd_pars*, a list of (value, weight)
294    vectors for the polydisperse parameters.  *cutoff* determines the
295    integration limits: any points with combined weight less than *cutoff*
296    will not be calculated.
297
298    Call :meth:`release` when done with the kernel instance.
299    """
300    def __init__(self, kernel, model_info, q_input):
301        self.info = model_info
302        self.q_input = q_input
303        self.kernel = kernel
304        self.res = np.empty(q_input.nq, q_input.dtype)
305        dim = '2d' if q_input.is_2d else '1d'
306        self.fixed_pars = model_info['partype']['fixed-' + dim]
307        self.pd_pars = model_info['partype']['pd-' + dim]
308
309        # In dll kernel, but not in opencl kernel
310        self.p_res = self.res.ctypes.data
311
312    def __call__(self, fixed_pars, pd_pars, cutoff):
313        real = (np.float32 if self.q_input.dtype == generate.F32
314                else np.float64 if self.q_input.dtype == generate.F64
315                else np.float128)
316
317        nq = c_int(self.q_input.nq)
318        if pd_pars:
319            cutoff = real(cutoff)
320            loops_N = [np.uint32(len(p[0])) for p in pd_pars]
321            loops = np.hstack(pd_pars)
322            loops = np.ascontiguousarray(loops.T, self.q_input.dtype).flatten()
323            p_loops = loops.ctypes.data
324            dispersed = [p_loops, cutoff] + loops_N
325        else:
326            dispersed = []
327        fixed = [real(p) for p in fixed_pars]
328        args = self.q_input.q_pointers + [self.p_res, nq] + dispersed + fixed
329        #print(pars)
330        self.kernel(*args)
331
332        return self.res
333
334    def release(self):
335        """
336        Release any resources associated with the kernel.
337        """
338        pass
Note: See TracBrowser for help on using the repository browser.