source: sasmodels/sasmodels/kerneldll.py @ 5efe850

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 5efe850 was 5efe850, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

Use tinycc if available; support float32 models in tinycc

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