source: sasmodels/sasmodels/kerneldll.py @ b2f1d2f

core_shell_microgelscostrafo411magnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since b2f1d2f was b2f1d2f, checked in by Paul Kienzle <pkienzle@…>, 7 years ago

default to tinycc compiler on windows; override with SAS_COMPILER in the environment

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