source: sasmodels/sasmodels/kernelcl.py @ 7d97437

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

fix test warnings from py3 and numpy

  • Property mode set to 100644
File size: 24.0 KB
Line 
1"""
2GPU driver for C kernels
3
4There should be a single GPU environment running on the system.  This
5environment is constructed on the first call to :func:`env`, and the
6same environment is returned on each call.
7
8After retrieving the environment, the next step is to create the kernel.
9This is done with a call to :meth:`GpuEnvironment.make_kernel`, which
10returns the type of data used by the kernel.
11
12Next a :class:`GpuData` object should be created with the correct kind
13of data.  This data object can be used by multiple kernels, for example,
14if the target model is a weighted sum of multiple kernels.  The data
15should include any extra evaluation points required to compute the proper
16data smearing.  This need not match the square grid for 2D data if there
17is an index saying which q points are active.
18
19Together the GpuData, the program, and a device form a :class:`GpuKernel`.
20This kernel is used during fitting, receiving new sets of parameters and
21evaluating them.  The output value is stored in an output buffer on the
22devices, where it can be combined with other structure factors and form
23factors and have instrumental resolution effects applied.
24
25In order to use OpenCL for your models, you will need OpenCL drivers for
26your machine.  These should be available from your graphics card vendor.
27Intel provides OpenCL drivers for CPUs as well as their integrated HD
28graphics chipsets.  AMD also provides drivers for Intel CPUs, but as of
29this writing the performance is lacking compared to the Intel drivers.
30NVidia combines drivers for CUDA and OpenCL in one package.  The result
31is a bit messy if you have multiple drivers installed.  You can see which
32drivers are available by starting python and running:
33
34    import pyopencl as cl
35    cl.create_some_context(interactive=True)
36
37Once you have done that, it will show the available drivers which you
38can select.  It will then tell you that you can use these drivers
39automatically by setting the SAS_OPENCL environment variable, which is
40PYOPENCL_CTX equivalent but not conflicting with other pyopnecl programs.
41
42Some graphics cards have multiple devices on the same card.  You cannot
43yet use both of them concurrently to evaluate models, but you can run
44the program twice using a different device for each session.
45
46OpenCL kernels are compiled when needed by the device driver.  Some
47drivers produce compiler output even when there is no error.  You
48can see the output by setting PYOPENCL_COMPILER_OUTPUT=1.  It should be
49harmless, albeit annoying.
50"""
51from __future__ import print_function
52
53import os
54import warnings
55import logging
56import time
57
58try:
59    from time import perf_counter as clock
60except ImportError: # CRUFT: python < 3.3
61    if sys.platform.count("darwin") > 0:
62        from time import time as clock
63    else:
64        from time import clock
65
66import numpy as np  # type: ignore
67
68# Attempt to setup opencl. This may fail if the opencl package is not
69# installed or if it is installed but there are no devices available.
70try:
71    import pyopencl as cl  # type: ignore
72    from pyopencl import mem_flags as mf
73    from pyopencl.characterize import get_fast_inaccurate_build_options
74    # Ask OpenCL for the default context so that we know that one exists
75    cl.create_some_context(interactive=False)
76    HAVE_OPENCL = True
77    OPENCL_ERROR = ""
78except Exception as exc:
79    HAVE_OPENCL = False
80    OPENCL_ERROR = str(exc)
81
82from . import generate
83from .kernel import KernelModel, Kernel
84
85# pylint: disable=unused-import
86try:
87    from typing import Tuple, Callable, Any
88    from .modelinfo import ModelInfo
89    from .details import CallDetails
90except ImportError:
91    pass
92# pylint: enable=unused-import
93
94# CRUFT: pyopencl < 2017.1  (as of June 2016 needs quotes around include path)
95def quote_path(v):
96    """
97    Quote the path if it is not already quoted.
98
99    If v starts with '-', then assume that it is a -I option or similar
100    and do not quote it.  This is fragile:  -Ipath with space needs to
101    be quoted.
102    """
103    return '"'+v+'"' if v and ' ' in v and not v[0] in "\"'-" else v
104
105def fix_pyopencl_include():
106    """
107    Monkey patch pyopencl to allow spaces in include file path.
108    """
109    import pyopencl as cl
110    if hasattr(cl, '_DEFAULT_INCLUDE_OPTIONS'):
111        cl._DEFAULT_INCLUDE_OPTIONS = [quote_path(v) for v in cl._DEFAULT_INCLUDE_OPTIONS]
112
113if HAVE_OPENCL:
114    fix_pyopencl_include()
115
116# The max loops number is limited by the amount of local memory available
117# on the device.  You don't want to make this value too big because it will
118# waste resources, nor too small because it may interfere with users trying
119# to do their polydispersity calculations.  A value of 1024 should be much
120# larger than necessary given that cost grows as npts^k where k is the number
121# of polydisperse parameters.
122MAX_LOOPS = 2048
123
124
125# Pragmas for enable OpenCL features.  Be sure to protect them so that they
126# still compile even if OpenCL is not present.
127_F16_PRAGMA = """\
128#if defined(__OPENCL_VERSION__) // && !defined(cl_khr_fp16)
129#  pragma OPENCL EXTENSION cl_khr_fp16: enable
130#endif
131"""
132
133_F64_PRAGMA = """\
134#if defined(__OPENCL_VERSION__) // && !defined(cl_khr_fp64)
135#  pragma OPENCL EXTENSION cl_khr_fp64: enable
136#endif
137"""
138
139def use_opencl():
140    return HAVE_OPENCL and os.environ.get("SAS_OPENCL", "").lower() != "none"
141
142ENV = None
143def reset_environment():
144    """
145    Call to create a new OpenCL context, such as after a change to SAS_OPENCL.
146    """
147    global ENV
148    ENV = GpuEnvironment() if use_opencl() else None
149
150def environment():
151    # type: () -> "GpuEnvironment"
152    """
153    Returns a singleton :class:`GpuEnvironment`.
154
155    This provides an OpenCL context and one queue per device.
156    """
157    if ENV is None:
158        if not HAVE_OPENCL:
159            raise RuntimeError("OpenCL startup failed with ***"
160                               + OPENCL_ERROR + "***; using C compiler instead")
161        reset_environment()
162        if ENV is None:
163            raise RuntimeError("SAS_OPENCL=None in environment")
164    return ENV
165
166def has_type(device, dtype):
167    # type: (cl.Device, np.dtype) -> bool
168    """
169    Return true if device supports the requested precision.
170    """
171    if dtype == generate.F32:
172        return True
173    elif dtype == generate.F64:
174        return "cl_khr_fp64" in device.extensions
175    elif dtype == generate.F16:
176        return "cl_khr_fp16" in device.extensions
177    else:
178        return False
179
180def get_warp(kernel, queue):
181    # type: (cl.Kernel, cl.CommandQueue) -> int
182    """
183    Return the size of an execution batch for *kernel* running on *queue*.
184    """
185    return kernel.get_work_group_info(
186        cl.kernel_work_group_info.PREFERRED_WORK_GROUP_SIZE_MULTIPLE,
187        queue.device)
188
189def _stretch_input(vector, dtype, extra=1e-3, boundary=32):
190    # type: (np.ndarray, np.dtype, float, int) -> np.ndarray
191    """
192    Stretch an input vector to the correct boundary.
193
194    Performance on the kernels can drop by a factor of two or more if the
195    number of values to compute does not fall on a nice power of two
196    boundary.   The trailing additional vector elements are given a
197    value of *extra*, and so f(*extra*) will be computed for each of
198    them.  The returned array will thus be a subset of the computed array.
199
200    *boundary* should be a power of 2 which is at least 32 for good
201    performance on current platforms (as of Jan 2015).  It should
202    probably be the max of get_warp(kernel,queue) and
203    device.min_data_type_align_size//4.
204    """
205    remainder = vector.size % boundary
206    if remainder != 0:
207        size = vector.size + (boundary - remainder)
208        vector = np.hstack((vector, [extra] * (size - vector.size)))
209    return np.ascontiguousarray(vector, dtype=dtype)
210
211
212def compile_model(context, source, dtype, fast=False):
213    # type: (cl.Context, str, np.dtype, bool) -> cl.Program
214    """
215    Build a model to run on the gpu.
216
217    Returns the compiled program and its type.
218
219    Raises an error if the desired precision is not available.
220    """
221    dtype = np.dtype(dtype)
222    if not all(has_type(d, dtype) for d in context.devices):
223        raise RuntimeError("%s not supported for devices"%dtype)
224
225    source_list = [generate.convert_type(source, dtype)]
226
227    if dtype == generate.F16:
228        source_list.insert(0, _F16_PRAGMA)
229    elif dtype == generate.F64:
230        source_list.insert(0, _F64_PRAGMA)
231
232    # Note: USE_SINCOS makes the intel cpu slower under opencl
233    if context.devices[0].type == cl.device_type.GPU:
234        source_list.insert(0, "#define USE_SINCOS\n")
235    options = (get_fast_inaccurate_build_options(context.devices[0])
236               if fast else [])
237    source = "\n".join(source_list)
238    program = cl.Program(context, source).build(options=options)
239    #print("done with "+program)
240    return program
241
242
243# for now, this returns one device in the context
244# TODO: create a context that contains all devices on all platforms
245class GpuEnvironment(object):
246    """
247    GPU context, with possibly many devices, and one queue per device.
248    """
249    def __init__(self):
250        # type: () -> None
251        # find gpu context
252        #self.context = cl.create_some_context()
253
254        self.context = None
255        if 'SAS_OPENCL' in os.environ:
256            #Setting PYOPENCL_CTX as a SAS_OPENCL to create cl context
257            os.environ["PYOPENCL_CTX"] = os.environ["SAS_OPENCL"]
258        if 'PYOPENCL_CTX' in os.environ:
259            self._create_some_context()
260
261        if not self.context:
262            self.context = _get_default_context()
263
264        # Byte boundary for data alignment
265        #self.data_boundary = max(d.min_data_type_align_size
266        #                         for d in self.context.devices)
267        self.queues = [cl.CommandQueue(context, context.devices[0])
268                       for context in self.context]
269        self.compiled = {}
270
271    def has_type(self, dtype):
272        # type: (np.dtype) -> bool
273        """
274        Return True if all devices support a given type.
275        """
276        return any(has_type(d, dtype)
277                   for context in self.context
278                   for d in context.devices)
279
280    def get_queue(self, dtype):
281        # type: (np.dtype) -> cl.CommandQueue
282        """
283        Return a command queue for the kernels of type dtype.
284        """
285        for context, queue in zip(self.context, self.queues):
286            if all(has_type(d, dtype) for d in context.devices):
287                return queue
288
289    def get_context(self, dtype):
290        # type: (np.dtype) -> cl.Context
291        """
292        Return a OpenCL context for the kernels of type dtype.
293        """
294        for context in self.context:
295            if all(has_type(d, dtype) for d in context.devices):
296                return context
297
298    def _create_some_context(self):
299        # type: () -> cl.Context
300        """
301        Protected call to cl.create_some_context without interactivity.  Use
302        this if SAS_OPENCL is set in the environment.  Sets the *context*
303        attribute.
304        """
305        try:
306            self.context = [cl.create_some_context(interactive=False)]
307        except Exception as exc:
308            warnings.warn(str(exc))
309            warnings.warn("pyopencl.create_some_context() failed")
310            warnings.warn("the environment variable 'SAS_OPENCL' might not be set correctly")
311
312    def compile_program(self, name, source, dtype, fast, timestamp):
313        # type: (str, str, np.dtype, bool, float) -> cl.Program
314        """
315        Compile the program for the device in the given context.
316        """
317        # Note: PyOpenCL caches based on md5 hash of source, options and device
318        # so we don't really need to cache things for ourselves.  I'll do so
319        # anyway just to save some data munging time.
320        tag = generate.tag_source(source)
321        key = "%s-%s-%s%s"%(name, dtype, tag, ("-fast" if fast else ""))
322        # Check timestamp on program
323        program, program_timestamp = self.compiled.get(key, (None, np.inf))
324        if program_timestamp < timestamp:
325            del self.compiled[key]
326        if key not in self.compiled:
327            context = self.get_context(dtype)
328            logging.info("building %s for OpenCL %s", key,
329                         context.devices[0].name.strip())
330            program = compile_model(self.get_context(dtype),
331                                    str(source), dtype, fast)
332            self.compiled[key] = (program, timestamp)
333        return program
334
335def _get_default_context():
336    # type: () -> List[cl.Context]
337    """
338    Get an OpenCL context, preferring GPU over CPU, and preferring Intel
339    drivers over AMD drivers.
340    """
341    # Note: on mobile devices there is automatic clock scaling if either the
342    # CPU or the GPU is underutilized; probably doesn't affect us, but we if
343    # it did, it would mean that putting a busy loop on the CPU while the GPU
344    # is running may increase throughput.
345    #
346    # Macbook pro, base install:
347    #     {'Apple': [Intel CPU, NVIDIA GPU]}
348    # Macbook pro, base install:
349    #     {'Apple': [Intel CPU, Intel GPU]}
350    # 2 x nvidia 295 with Intel and NVIDIA opencl drivers installed
351    #     {'Intel': [CPU], 'NVIDIA': [GPU, GPU, GPU, GPU]}
352    gpu, cpu = None, None
353    for platform in cl.get_platforms():
354        # AMD provides a much weaker CPU driver than Intel/Apple, so avoid it.
355        # If someone has bothered to install the AMD/NVIDIA drivers, prefer
356        # them over the integrated graphics driver that may have been supplied
357        # with the CPU chipset.
358        preferred_cpu = (platform.vendor.startswith('Intel')
359                         or platform.vendor.startswith('Apple'))
360        preferred_gpu = (platform.vendor.startswith('Advanced')
361                         or platform.vendor.startswith('NVIDIA'))
362        for device in platform.get_devices():
363            if device.type == cl.device_type.GPU:
364                # If the existing type is not GPU then it will be CUSTOM
365                # or ACCELERATOR so don't override it.
366                if gpu is None or (preferred_gpu and gpu.type == cl.device_type.GPU):
367                    gpu = device
368            elif device.type == cl.device_type.CPU:
369                if cpu is None or preferred_cpu:
370                    cpu = device
371            else:
372                # System has cl.device_type.ACCELERATOR or cl.device_type.CUSTOM
373                # Intel Phi for example registers as an accelerator
374                # Since the user installed a custom device on their system
375                # and went through the pain of sorting out OpenCL drivers for
376                # it, lets assume they really do want to use it as their
377                # primary compute device.
378                gpu = device
379
380    # order the devices by gpu then by cpu; when searching for an available
381    # device by data type they will be checked in this order, which means
382    # that if the gpu supports double then the cpu will never be used (though
383    # we may make it possible to explicitly request the cpu at some point).
384    devices = []
385    if gpu is not None:
386        devices.append(gpu)
387    if cpu is not None:
388        devices.append(cpu)
389    return [cl.Context([d]) for d in devices]
390
391
392class GpuModel(KernelModel):
393    """
394    GPU wrapper for a single model.
395
396    *source* and *model_info* are the model source and interface as returned
397    from :func:`generate.make_source` and :func:`generate.make_model_info`.
398
399    *dtype* is the desired model precision.  Any numpy dtype for single
400    or double precision floats will do, such as 'f', 'float32' or 'single'
401    for single and 'd', 'float64' or 'double' for double.  Double precision
402    is an optional extension which may not be available on all devices.
403    Half precision ('float16','half') may be available on some devices.
404    Fast precision ('fast') is a loose version of single precision, indicating
405    that the compiler is allowed to take shortcuts.
406    """
407    def __init__(self, source, model_info, dtype=generate.F32, fast=False):
408        # type: (Dict[str,str], ModelInfo, np.dtype, bool) -> None
409        self.info = model_info
410        self.source = source
411        self.dtype = dtype
412        self.fast = fast
413        self.program = None # delay program creation
414        self._kernels = None
415
416    def __getstate__(self):
417        # type: () -> Tuple[ModelInfo, str, np.dtype, bool]
418        return self.info, self.source, self.dtype, self.fast
419
420    def __setstate__(self, state):
421        # type: (Tuple[ModelInfo, str, np.dtype, bool]) -> None
422        self.info, self.source, self.dtype, self.fast = state
423        self.program = None
424
425    def make_kernel(self, q_vectors):
426        # type: (List[np.ndarray]) -> "GpuKernel"
427        if self.program is None:
428            compile_program = environment().compile_program
429            timestamp = generate.ocl_timestamp(self.info)
430            self.program = compile_program(
431                self.info.name,
432                self.source['opencl'],
433                self.dtype,
434                self.fast,
435                timestamp)
436            variants = ['Iq', 'Iqxy', 'Imagnetic']
437            names = [generate.kernel_name(self.info, k) for k in variants]
438            kernels = [getattr(self.program, k) for k in names]
439            self._kernels = dict((k, v) for k, v in zip(variants, kernels))
440        is_2d = len(q_vectors) == 2
441        if is_2d:
442            kernel = [self._kernels['Iqxy'], self._kernels['Imagnetic']]
443        else:
444            kernel = [self._kernels['Iq']]*2
445        return GpuKernel(kernel, self.dtype, self.info, q_vectors)
446
447    def release(self):
448        # type: () -> None
449        """
450        Free the resources associated with the model.
451        """
452        if self.program is not None:
453            self.program = None
454
455    def __del__(self):
456        # type: () -> None
457        self.release()
458
459# TODO: check that we don't need a destructor for buffers which go out of scope
460class GpuInput(object):
461    """
462    Make q data available to the gpu.
463
464    *q_vectors* is a list of q vectors, which will be *[q]* for 1-D data,
465    and *[qx, qy]* for 2-D data.  Internally, the vectors will be reallocated
466    to get the best performance on OpenCL, which may involve shifting and
467    stretching the array to better match the memory architecture.  Additional
468    points will be evaluated with *q=1e-3*.
469
470    *dtype* is the data type for the q vectors. The data type should be
471    set to match that of the kernel, which is an attribute of
472    :class:`GpuProgram`.  Note that not all kernels support double
473    precision, so even if the program was created for double precision,
474    the *GpuProgram.dtype* may be single precision.
475
476    Call :meth:`release` when complete.  Even if not called directly, the
477    buffer will be released when the data object is freed.
478    """
479    def __init__(self, q_vectors, dtype=generate.F32):
480        # type: (List[np.ndarray], np.dtype) -> None
481        # TODO: do we ever need double precision q?
482        env = environment()
483        self.nq = q_vectors[0].size
484        self.dtype = np.dtype(dtype)
485        self.is_2d = (len(q_vectors) == 2)
486        # TODO: stretch input based on get_warp()
487        # not doing it now since warp depends on kernel, which is not known
488        # at this point, so instead using 32, which is good on the set of
489        # architectures tested so far.
490        if self.is_2d:
491            # Note: 16 rather than 15 because result is 1 longer than input.
492            width = ((self.nq+16)//16)*16
493            self.q = np.empty((width, 2), dtype=dtype)
494            self.q[:self.nq, 0] = q_vectors[0]
495            self.q[:self.nq, 1] = q_vectors[1]
496        else:
497            # Note: 32 rather than 31 because result is 1 longer than input.
498            width = ((self.nq+32)//32)*32
499            self.q = np.empty(width, dtype=dtype)
500            self.q[:self.nq] = q_vectors[0]
501        self.global_size = [self.q.shape[0]]
502        context = env.get_context(self.dtype)
503        #print("creating inputs of size", self.global_size)
504        self.q_b = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,
505                             hostbuf=self.q)
506
507    def release(self):
508        # type: () -> None
509        """
510        Free the memory.
511        """
512        if self.q_b is not None:
513            self.q_b.release()
514            self.q_b = None
515
516    def __del__(self):
517        # type: () -> None
518        self.release()
519
520class GpuKernel(Kernel):
521    """
522    Callable SAS kernel.
523
524    *kernel* is the GpuKernel object to call
525
526    *model_info* is the module information
527
528    *q_vectors* is the q vectors at which the kernel should be evaluated
529
530    *dtype* is the kernel precision
531
532    The resulting call method takes the *pars*, a list of values for
533    the fixed parameters to the kernel, and *pd_pars*, a list of (value,weight)
534    vectors for the polydisperse parameters.  *cutoff* determines the
535    integration limits: any points with combined weight less than *cutoff*
536    will not be calculated.
537
538    Call :meth:`release` when done with the kernel instance.
539    """
540    def __init__(self, kernel, dtype, model_info, q_vectors):
541        # type: (cl.Kernel, np.dtype, ModelInfo, List[np.ndarray]) -> None
542        q_input = GpuInput(q_vectors, dtype)
543        self.kernel = kernel
544        self.info = model_info
545        self.dtype = dtype
546        self.dim = '2d' if q_input.is_2d else '1d'
547        # plus three for the normalization values
548        self.result = np.empty(q_input.nq+1, dtype)
549
550        # Inputs and outputs for each kernel call
551        # Note: res may be shorter than res_b if global_size != nq
552        env = environment()
553        self.queue = env.get_queue(dtype)
554
555        self.result_b = cl.Buffer(self.queue.context, mf.READ_WRITE,
556                                  q_input.global_size[0] * dtype.itemsize)
557        self.q_input = q_input # allocated by GpuInput above
558
559        self._need_release = [self.result_b, self.q_input]
560        self.real = (np.float32 if dtype == generate.F32
561                     else np.float64 if dtype == generate.F64
562                     else np.float16 if dtype == generate.F16
563                     else np.float32)  # will never get here, so use np.float32
564
565    def __call__(self, call_details, values, cutoff, magnetic):
566        # type: (CallDetails, np.ndarray, np.ndarray, float, bool) -> np.ndarray
567        context = self.queue.context
568        # Arrange data transfer to card
569        details_b = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,
570                              hostbuf=call_details.buffer)
571        values_b = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,
572                             hostbuf=values)
573
574        kernel = self.kernel[1 if magnetic else 0]
575        args = [
576            np.uint32(self.q_input.nq), None, None,
577            details_b, values_b, self.q_input.q_b, self.result_b,
578            self.real(cutoff),
579        ]
580        #print("Calling OpenCL")
581        #call_details.show(values)
582        # Call kernel and retrieve results
583        wait_for = None
584        last_nap = clock()
585        step = 1000000//self.q_input.nq + 1
586        for start in range(0, call_details.num_eval, step):
587            stop = min(start + step, call_details.num_eval)
588            #print("queuing",start,stop)
589            args[1:3] = [np.int32(start), np.int32(stop)]
590            wait_for = [kernel(self.queue, self.q_input.global_size, None,
591                               *args, wait_for=wait_for)]
592            if stop < call_details.num_eval:
593                # Allow other processes to run
594                wait_for[0].wait()
595                current_time = clock()
596                if current_time - last_nap > 0.5:
597                    time.sleep(0.05)
598                    last_nap = current_time
599        cl.enqueue_copy(self.queue, self.result, self.result_b)
600        #print("result", self.result)
601
602        # Free buffers
603        for v in (details_b, values_b):
604            if v is not None:
605                v.release()
606
607        pd_norm = self.result[self.q_input.nq]
608        scale = values[0]/(pd_norm if pd_norm != 0.0 else 1.0)
609        background = values[1]
610        #print("scale",scale,values[0],self.result[self.q_input.nq],background)
611        return scale*self.result[:self.q_input.nq] + background
612        # return self.result[:self.q_input.nq]
613
614    def release(self):
615        # type: () -> None
616        """
617        Release resources associated with the kernel.
618        """
619        for v in self._need_release:
620            v.release()
621        self._need_release = []
622
623    def __del__(self):
624        # type: () -> None
625        self.release()
Note: See TracBrowser for help on using the repository browser.