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
RevLine 
[92da231]1r"""
[eafc9fa]2DLL driver for C kernels
[750ffa5]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*.
[92da231]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
[d138d43]23    `<http://www.microsoft.com/en-us/download/details.aspx?id=44266>`_
[92da231]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.
[14de349]45"""
[eafc9fa]46from __future__ import print_function
[750ffa5]47
[5d4777d]48import sys
49import os
[5a91c6b]50from os.path import join as joinpath, split as splitpath, splitext
[df4dc86]51import tempfile
[14de349]52import ctypes as ct
[5edfe12]53from ctypes import c_void_p, c_int, c_longdouble, c_double, c_float
[821a9c6]54import logging
[14de349]55
56import numpy as np
57
[cb6ecf4]58from . import generate
[f734e7d]59from .kernelpy import PyInput, PyModel
[2c801fe]60from .exception import annotate_exception
[14de349]61
[5d4777d]62# Compiler platform details
63if sys.platform == 'darwin':
[216a9e1]64    #COMPILE = "gcc-mp-4.7 -shared -fPIC -std=c99 -fopenmp -O2 -Wall %s -o %s -lm -lgomp"
[df4dc86]65    COMPILE = "gcc -shared -fPIC -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
[5d4777d]66elif os.name == 'nt':
[3c56da87]67    # call vcvarsall.bat before compiling to set path, headers, libs, etc.
[68d3c1b]68    if "VCINSTALLDIR" in os.environ:
[a30cdd5]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
[f734e7d]73        # TODO: remove intermediate OBJ file created in the directory
74        # TODO: maybe don't use randomized name for the c file
[a30cdd5]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))
[5a91c6b]81    elif True:
82        # If MSVC compiler is not available, try using mingw
[fb69211]83        # fPIC is not needed on windows
[e1454ab]84        COMPILE = "gcc -shared -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
[a30cdd5]85        if "SAS_OPENMP" in os.environ:
86            COMPILE = COMPILE + " -fopenmp"
[fb69211]87    else:
[5a91c6b]88        # If MSVC compiler is not available, try using tinycc
[fb69211]89        from tinycc import TCC
90        COMPILE = TCC + " -shared -rdynamic -Wall %(source)s -o %(output)s"
[5d4777d]91else:
[a30cdd5]92    COMPILE = "cc -shared -fPIC -fopenmp -std=c99 -O2 -Wall %(source)s -o %(output)s -lm"
[df4dc86]93
[ff97458]94# Windows-specific solution
95if os.name == 'nt':
[3ecf034]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)
[ff97458]100else:
101    # Set up the default path for compiled modules.
102    DLL_PATH = tempfile.gettempdir()
[5d4777d]103
[5d316e9]104ALLOW_SINGLE_PRECISION_DLLS = True
[5d4777d]105
[750ffa5]106
[17bbadd]107def dll_path(model_info, dtype="double"):
[5d4777d]108    """
[17bbadd]109    Path to the compiled model defined by *model_info*.
[5d4777d]110    """
[17bbadd]111    basename = splitext(splitpath(model_info['filename'])[1])[0]
[750ffa5]112    if np.dtype(dtype) == generate.F32:
113        basename += "32"
[5edfe12]114    elif np.dtype(dtype) == generate.F64:
115        basename += "64"
116    else:
117        basename += "128"
[01b8659]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
[5d4777d]124    return joinpath(DLL_PATH, basename+'.so')
125
[17bbadd]126def make_dll(source, model_info, dtype="double"):
[5d4777d]127    """
128    Load the compiled model defined by *kernel_module*.
129
130    Recompile if any files are newer than the model file.
131
[aa4946b]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.
[5d4777d]135
[aa4946b]136    The DLL is not loaded until the kernel is called so models can
[5d4777d]137    be defined without using too many resources.
[aa4946b]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.
[5d4777d]144    """
[17bbadd]145    if callable(model_info.get('Iq', None)):
146        return PyModel(model_info)
[6ad0e87]147   
[750ffa5]148    dtype = np.dtype(dtype)
[5d316e9]149    if dtype == generate.F16:
150        raise ValueError("16 bit floats not supported")
[5edfe12]151    if dtype == generate.F32 and not ALLOW_SINGLE_PRECISION_DLLS:
152        dtype = generate.F64  # Force 64-bit dll
[750ffa5]153
154    if dtype == generate.F32: # 32-bit dll
[17bbadd]155        tempfile_prefix = 'sas_' + model_info['name'] + '32_'
[5edfe12]156    elif dtype == generate.F64:
[17bbadd]157        tempfile_prefix = 'sas_' + model_info['name'] + '64_'
[750ffa5]158    else:
[17bbadd]159        tempfile_prefix = 'sas_' + model_info['name'] + '128_'
[01b8659]160
[17bbadd]161    dll = dll_path(model_info, dtype)
[e1454ab]162
163    if not os.path.exists(dll):
[5a91c6b]164        need_recompile = True
[31bc9bf]165    elif getattr(sys, 'frozen', None) is not None:
[5a91c6b]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:
[823e620]175        fid, filename = tempfile.mkstemp(suffix=".c", prefix=tempfile_prefix)
[5a91c6b]176        source = generate.convert_type(source, dtype)
[823e620]177        os.fdopen(fid, "w").write(source)
[aa4946b]178        command = COMPILE%{"source":filename, "output":dll}
[821a9c6]179        logging.info(command)
[df4dc86]180        status = os.system(command)
[aa4946b]181        if status != 0 or not os.path.exists(dll):
[f734e7d]182            raise RuntimeError("compile failed.  File is in %r"%filename)
[5d4777d]183        else:
[74667d3]184            ## comment the following to keep the generated c file
185            os.unlink(filename)
186            #print("saving compiled file in %r"%filename)
[aa4946b]187    return dll
188
189
[17bbadd]190def load_dll(source, model_info, dtype="double"):
[aa4946b]191    """
[823e620]192    Create and load a dll corresponding to the source, info pair returned
[aa4946b]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    """
[17bbadd]198    filename = make_dll(source, model_info, dtype=dtype)
199    return DllModel(filename, model_info, dtype=dtype)
[5d4777d]200
[14de349]201
[f734e7d]202IQ_ARGS = [c_void_p, c_void_p, c_int]
203IQXY_ARGS = [c_void_p, c_void_p, c_void_p, c_int]
[14de349]204
205class DllModel(object):
206    """
207    ctypes wrapper for a single model.
208
[17bbadd]209    *source* and *model_info* are the model source and interface as returned
[14de349]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.
[ff7119b]216
217    Call :meth:`release` when done with the kernel.
[14de349]218    """
[6ad0e87]219   
[17bbadd]220    def __init__(self, dllpath, model_info, dtype=generate.F32):
221        self.info = model_info
[ce27e21]222        self.dllpath = dllpath
223        self.dll = None
[750ffa5]224        self.dtype = np.dtype(dtype)
[14de349]225
[ce27e21]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'])
[14de349]231
[823e620]232        #print("dll", self.dllpath)
[2c801fe]233        try:
234            self.dll = ct.CDLL(self.dllpath)
[4d76711]235        except:
236            annotate_exception("while loading "+self.dllpath)
[2c801fe]237            raise
[14de349]238
[5edfe12]239        fp = (c_float if self.dtype == generate.F32
240              else c_double if self.dtype == generate.F64
241              else c_longdouble)
[750ffa5]242        pd_args_1d = [c_void_p, fp] + [c_int]*Npd1d if Npd1d else []
[823e620]243        pd_args_2d = [c_void_p, fp] + [c_int]*Npd2d if Npd2d else []
[cb6ecf4]244        self.Iq = self.dll[generate.kernel_name(self.info, False)]
[750ffa5]245        self.Iq.argtypes = IQ_ARGS + pd_args_1d + [fp]*Nfixed1d
[ce27e21]246
[cb6ecf4]247        self.Iqxy = self.dll[generate.kernel_name(self.info, True)]
[750ffa5]248        self.Iqxy.argtypes = IQXY_ARGS + pd_args_2d + [fp]*Nfixed2d
[6ad0e87]249       
250        self.release()
[ce27e21]251
252    def __getstate__(self):
[eafc9fa]253        return self.info, self.dllpath
[ce27e21]254
255    def __setstate__(self, state):
[eafc9fa]256        self.info, self.dllpath = state
257        self.dll = None
[ce27e21]258
[4d76711]259    def make_kernel(self, q_vectors):
[eafc9fa]260        q_input = PyInput(q_vectors, self.dtype)
[b3f6bc3]261        if self.dll is None: self._load_dll()
[eafc9fa]262        kernel = self.Iqxy if q_input.is_2d else self.Iq
[3c56da87]263        return DllKernel(kernel, self.info, q_input)
[4d76711]264
[eafc9fa]265    def release(self):
[14de349]266        """
[eafc9fa]267        Release any resources associated with the model.
[14de349]268        """
[6ad0e87]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 
[ff7119b]279
[14de349]280
281class DllKernel(object):
[ff7119b]282    """
283    Callable SAS kernel.
284
[b3f6bc3]285    *kernel* is the c function to call.
[ff7119b]286
[17bbadd]287    *model_info* is the module information
[ff7119b]288
[3c56da87]289    *q_input* is the DllInput q vectors at which the kernel should be
[ff7119b]290    evaluated.
291
292    The resulting call method takes the *pars*, a list of values for
[823e620]293    the fixed parameters to the kernel, and *pd_pars*, a list of (value, weight)
[ff7119b]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    """
[17bbadd]300    def __init__(self, kernel, model_info, q_input):
301        self.info = model_info
[3c56da87]302        self.q_input = q_input
[14de349]303        self.kernel = kernel
[3c56da87]304        self.res = np.empty(q_input.nq, q_input.dtype)
[eafc9fa]305        dim = '2d' if q_input.is_2d else '1d'
[17bbadd]306        self.fixed_pars = model_info['partype']['fixed-' + dim]
307        self.pd_pars = model_info['partype']['pd-' + dim]
[14de349]308
[ce27e21]309        # In dll kernel, but not in opencl kernel
310        self.p_res = self.res.ctypes.data
[14de349]311
[f734e7d]312    def __call__(self, fixed_pars, pd_pars, cutoff):
[5edfe12]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)
[14de349]316
[3c56da87]317        nq = c_int(self.q_input.nq)
[f734e7d]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)
[3c56da87]322            loops = np.ascontiguousarray(loops.T, self.q_input.dtype).flatten()
[f734e7d]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]
[3c56da87]328        args = self.q_input.q_pointers + [self.p_res, nq] + dispersed + fixed
[9404dd3]329        #print(pars)
[ce27e21]330        self.kernel(*args)
[14de349]331
332        return self.res
333
334    def release(self):
[eafc9fa]335        """
336        Release any resources associated with the kernel.
337        """
[14de349]338        pass
Note: See TracBrowser for help on using the repository browser.