source: sasmodels/sasmodels/kernelcl.py @ 88b92e1

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

monkey patch pyopencl so that include path is quoted

  • Property mode set to 100644
File size: 19.1 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 PYOPENCL_CTX environment variable.
40
41Some graphics cards have multiple devices on the same card.  You cannot
42yet use both of them concurrently to evaluate models, but you can run
43the program twice using a different device for each session.
44
45OpenCL kernels are compiled when needed by the device driver.  Some
46drivers produce compiler output even when there is no error.  You
47can see the output by setting PYOPENCL_COMPILER_OUTPUT=1.  It should be
48harmless, albeit annoying.
49"""
50import os
51import warnings
52import logging
53
54import numpy as np
55
56try:
57    import pyopencl as cl
58    # Ask OpenCL for the default context so that we know that one exists
59    cl.create_some_context(interactive=False)
60except Exception as exc:
61    warnings.warn(str(exc))
62    raise RuntimeError("OpenCL not available")
63
64# CRUFT: pyopencl as of June 2016 needs quotes around include path
65def _quote_path(v):
66    return '"'+v+'"' if ' ' in v and not v.startswith('-') else v
67
68if hasattr(cl, '_DEFAULT_INCLUDE_OPTIONS'):
69    cl._DEFAULT_INCLUDE_OPTIONS = [_quote_path(v) for v in cl._DEFAULT_INCLUDE_OPTIONS]
70
71from pyopencl import mem_flags as mf
72from pyopencl.characterize import get_fast_inaccurate_build_options
73
74from . import generate
75
76# The max loops number is limited by the amount of local memory available
77# on the device.  You don't want to make this value too big because it will
78# waste resources, nor too small because it may interfere with users trying
79# to do their polydispersity calculations.  A value of 1024 should be much
80# larger than necessary given that cost grows as npts^k where k is the number
81# of polydisperse parameters.
82MAX_LOOPS = 2048
83
84
85ENV = None
86def environment():
87    """
88    Returns a singleton :class:`GpuEnvironment`.
89
90    This provides an OpenCL context and one queue per device.
91    """
92    global ENV
93    if ENV is None:
94        ENV = GpuEnvironment()
95    return ENV
96
97def has_type(device, dtype):
98    """
99    Return true if device supports the requested precision.
100    """
101    if dtype == generate.F32:
102        return True
103    elif dtype == generate.F64:
104        return "cl_khr_fp64" in device.extensions
105    elif dtype == generate.F16:
106        return "cl_khr_fp16" in device.extensions
107    else:
108        return False
109
110def get_warp(kernel, queue):
111    """
112    Return the size of an execution batch for *kernel* running on *queue*.
113    """
114    return kernel.get_work_group_info(
115        cl.kernel_work_group_info.PREFERRED_WORK_GROUP_SIZE_MULTIPLE,
116        queue.device)
117
118def _stretch_input(vector, dtype, extra=1e-3, boundary=32):
119    """
120    Stretch an input vector to the correct boundary.
121
122    Performance on the kernels can drop by a factor of two or more if the
123    number of values to compute does not fall on a nice power of two
124    boundary.   The trailing additional vector elements are given a
125    value of *extra*, and so f(*extra*) will be computed for each of
126    them.  The returned array will thus be a subset of the computed array.
127
128    *boundary* should be a power of 2 which is at least 32 for good
129    performance on current platforms (as of Jan 2015).  It should
130    probably be the max of get_warp(kernel,queue) and
131    device.min_data_type_align_size//4.
132    """
133    remainder = vector.size % boundary
134    if remainder != 0:
135        size = vector.size + (boundary - remainder)
136        vector = np.hstack((vector, [extra] * (size - vector.size)))
137    return np.ascontiguousarray(vector, dtype=dtype)
138
139
140def compile_model(context, source, dtype, fast=False):
141    """
142    Build a model to run on the gpu.
143
144    Returns the compiled program and its type.  The returned type will
145    be float32 even if the desired type is float64 if any of the
146    devices in the context do not support the cl_khr_fp64 extension.
147    """
148    dtype = np.dtype(dtype)
149    if not all(has_type(d, dtype) for d in context.devices):
150        raise RuntimeError("%s not supported for devices"%dtype)
151
152    source = generate.convert_type(source, dtype)
153    # Note: USE_SINCOS makes the intel cpu slower under opencl
154    if context.devices[0].type == cl.device_type.GPU:
155        source = "#define USE_SINCOS\n" + source
156    options = (get_fast_inaccurate_build_options(context.devices[0])
157               if fast else [])
158    program = cl.Program(context, source).build(options=options)
159    #print("done with "+program)
160    return program
161
162
163# for now, this returns one device in the context
164# TODO: create a context that contains all devices on all platforms
165class GpuEnvironment(object):
166    """
167    GPU context, with possibly many devices, and one queue per device.
168    """
169    def __init__(self):
170        # find gpu context
171        #self.context = cl.create_some_context()
172
173        self.context = None
174        if 'PYOPENCL_CTX' in os.environ:
175            self._create_some_context()
176
177        if not self.context:
178            self.context = _get_default_context()
179
180        # Byte boundary for data alignment
181        #self.data_boundary = max(d.min_data_type_align_size
182        #                         for d in self.context.devices)
183        self.queues = [cl.CommandQueue(context, context.devices[0])
184                       for context in self.context]
185        self.compiled = {}
186
187    def has_type(self, dtype):
188        """
189        Return True if all devices support a given type.
190        """
191        dtype = generate.F32 if dtype == 'fast' else np.dtype(dtype)
192        return any(has_type(d, dtype)
193                   for context in self.context
194                   for d in context.devices)
195
196    def get_queue(self, dtype):
197        """
198        Return a command queue for the kernels of type dtype.
199        """
200        for context, queue in zip(self.context, self.queues):
201            if all(has_type(d, dtype) for d in context.devices):
202                return queue
203
204    def get_context(self, dtype):
205        """
206        Return a OpenCL context for the kernels of type dtype.
207        """
208        for context, queue in zip(self.context, self.queues):
209            if all(has_type(d, dtype) for d in context.devices):
210                return context
211
212    def _create_some_context(self):
213        """
214        Protected call to cl.create_some_context without interactivity.  Use
215        this if PYOPENCL_CTX is set in the environment.  Sets the *context*
216        attribute.
217        """
218        try:
219            self.context = [cl.create_some_context(interactive=False)]
220        except Exception as exc:
221            warnings.warn(str(exc))
222            warnings.warn("pyopencl.create_some_context() failed")
223            warnings.warn("the environment variable 'PYOPENCL_CTX' might not be set correctly")
224
225    def compile_program(self, name, source, dtype, fast=False):
226        """
227        Compile the program for the device in the given context.
228        """
229        key = "%s-%s%s"%(name, dtype, ("-fast" if fast else ""))
230        if key not in self.compiled:
231            context = self.get_context(dtype)
232            logging.info("building %s for OpenCL %s"
233                         % (key, context.devices[0].name.strip()))
234            program = compile_model(context, source, np.dtype(dtype), fast)
235            self.compiled[key] = program
236        return self.compiled[key]
237
238    def release_program(self, name):
239        """
240        Free memory associated with the program on the device.
241        """
242        if name in self.compiled:
243            self.compiled[name].release()
244            del self.compiled[name]
245
246def _get_default_context():
247    """
248    Get an OpenCL context, preferring GPU over CPU, and preferring Intel
249    drivers over AMD drivers.
250    """
251    # Note: on mobile devices there is automatic clock scaling if either the
252    # CPU or the GPU is underutilized; probably doesn't affect us, but we if
253    # it did, it would mean that putting a busy loop on the CPU while the GPU
254    # is running may increase throughput.
255    #
256    # Macbook pro, base install:
257    #     {'Apple': [Intel CPU, NVIDIA GPU]}
258    # Macbook pro, base install:
259    #     {'Apple': [Intel CPU, Intel GPU]}
260    # 2 x nvidia 295 with Intel and NVIDIA opencl drivers installed
261    #     {'Intel': [CPU], 'NVIDIA': [GPU, GPU, GPU, GPU]}
262    gpu, cpu = None, None
263    for platform in cl.get_platforms():
264        # AMD provides a much weaker CPU driver than Intel/Apple, so avoid it.
265        # If someone has bothered to install the AMD/NVIDIA drivers, prefer them over the integrated
266        # graphics driver that may have been supplied with the CPU chipset.
267        preferred_cpu = platform.vendor.startswith('Intel') or platform.vendor.startswith('Apple')
268        preferred_gpu = platform.vendor.startswith('Advanced') or platform.vendor.startswith('NVIDIA')
269        for device in platform.get_devices():
270            if device.type == cl.device_type.GPU:
271                # If the existing type is not GPU then it will be CUSTOM or ACCELERATOR,
272                # so don't override it.
273                if gpu is None or (preferred_gpu and gpu.type == cl.device_type.GPU):
274                    gpu = device
275            elif device.type == cl.device_type.CPU:
276                if cpu is None or preferred_cpu:
277                    cpu = device
278            else:
279                # System has cl.device_type.ACCELERATOR or cl.device_type.CUSTOM
280                # Intel Phi for example registers as an accelerator
281                # Since the user installed a custom device on their system and went through the
282                # pain of sorting out OpenCL drivers for it, lets assume they really do want to
283                # use it as their primary compute device.
284                gpu = device
285
286    # order the devices by gpu then by cpu; when searching for an available device by data type they
287    # will be checked in this order, which means that if the gpu supports double then the cpu will never
288    # be used (though we may make it possible to explicitly request the cpu at some point).
289    devices = []
290    if gpu is not None:
291        devices.append(gpu)
292    if cpu is not None:
293        devices.append(cpu)
294    return [cl.Context([d]) for d in devices]
295
296
297class GpuModel(object):
298    """
299    GPU wrapper for a single model.
300
301    *source* and *model_info* are the model source and interface as returned
302    from :func:`generate.make_source` and :func:`generate.make_model_info`.
303
304    *dtype* is the desired model precision.  Any numpy dtype for single
305    or double precision floats will do, such as 'f', 'float32' or 'single'
306    for single and 'd', 'float64' or 'double' for double.  Double precision
307    is an optional extension which may not be available on all devices.
308    Half precision ('float16','half') may be available on some devices.
309    Fast precision ('fast') is a loose version of single precision, indicating
310    that the compiler is allowed to take shortcuts.
311    """
312    def __init__(self, source, model_info, dtype=generate.F32):
313        self.info = model_info
314        self.source = source
315        self.dtype = generate.F32 if dtype == 'fast' else np.dtype(dtype)
316        self.fast = (dtype == 'fast')
317        self.program = None # delay program creation
318
319    def __getstate__(self):
320        return self.info, self.source, self.dtype, self.fast
321
322    def __setstate__(self, state):
323        self.info, self.source, self.dtype, self.fast = state
324        self.program = None
325
326    def make_kernel(self, q_vectors):
327        if self.program is None:
328            compiler = environment().compile_program
329            self.program = compiler(self.info['name'], self.source, self.dtype,
330                                    self.fast)
331        is_2d = len(q_vectors) == 2
332        kernel_name = generate.kernel_name(self.info, is_2d)
333        kernel = getattr(self.program, kernel_name)
334        return GpuKernel(kernel, self.info, q_vectors, self.dtype)
335
336    def release(self):
337        """
338        Free the resources associated with the model.
339        """
340        if self.program is not None:
341            environment().release_program(self.info['name'])
342            self.program = None
343
344    def __del__(self):
345        self.release()
346
347# TODO: check that we don't need a destructor for buffers which go out of scope
348class GpuInput(object):
349    """
350    Make q data available to the gpu.
351
352    *q_vectors* is a list of q vectors, which will be *[q]* for 1-D data,
353    and *[qx, qy]* for 2-D data.  Internally, the vectors will be reallocated
354    to get the best performance on OpenCL, which may involve shifting and
355    stretching the array to better match the memory architecture.  Additional
356    points will be evaluated with *q=1e-3*.
357
358    *dtype* is the data type for the q vectors. The data type should be
359    set to match that of the kernel, which is an attribute of
360    :class:`GpuProgram`.  Note that not all kernels support double
361    precision, so even if the program was created for double precision,
362    the *GpuProgram.dtype* may be single precision.
363
364    Call :meth:`release` when complete.  Even if not called directly, the
365    buffer will be released when the data object is freed.
366    """
367    def __init__(self, q_vectors, dtype=generate.F32):
368        # TODO: do we ever need double precision q?
369        env = environment()
370        self.nq = q_vectors[0].size
371        self.dtype = np.dtype(dtype)
372        self.is_2d = (len(q_vectors) == 2)
373        # TODO: stretch input based on get_warp()
374        # not doing it now since warp depends on kernel, which is not known
375        # at this point, so instead using 32, which is good on the set of
376        # architectures tested so far.
377        self.q_vectors = [_stretch_input(q, self.dtype, 32) for q in q_vectors]
378        context = env.get_context(self.dtype)
379        self.global_size = [self.q_vectors[0].size]
380        #print("creating inputs of size", self.global_size)
381        self.q_buffers = [
382            cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=q)
383            for q in self.q_vectors
384        ]
385
386    def release(self):
387        """
388        Free the memory.
389        """
390        for b in self.q_buffers:
391            b.release()
392        self.q_buffers = []
393
394    def __del__(self):
395        self.release()
396
397class GpuKernel(object):
398    """
399    Callable SAS kernel.
400
401    *kernel* is the GpuKernel object to call
402
403    *model_info* is the module information
404
405    *q_vectors* is the q vectors at which the kernel should be evaluated
406
407    *dtype* is the kernel precision
408
409    The resulting call method takes the *pars*, a list of values for
410    the fixed parameters to the kernel, and *pd_pars*, a list of (value,weight)
411    vectors for the polydisperse parameters.  *cutoff* determines the
412    integration limits: any points with combined weight less than *cutoff*
413    will not be calculated.
414
415    Call :meth:`release` when done with the kernel instance.
416    """
417    def __init__(self, kernel, model_info, q_vectors, dtype):
418        q_input = GpuInput(q_vectors, dtype)
419        self.kernel = kernel
420        self.info = model_info
421        self.res = np.empty(q_input.nq, q_input.dtype)
422        dim = '2d' if q_input.is_2d else '1d'
423        self.fixed_pars = model_info['partype']['fixed-' + dim]
424        self.pd_pars = model_info['partype']['pd-' + dim]
425
426        # Inputs and outputs for each kernel call
427        # Note: res may be shorter than res_b if global_size != nq
428        env = environment()
429        self.queue = env.get_queue(dtype)
430        self.loops_b = cl.Buffer(self.queue.context, mf.READ_WRITE,
431                                 2 * MAX_LOOPS * q_input.dtype.itemsize)
432        self.res_b = cl.Buffer(self.queue.context, mf.READ_WRITE,
433                               q_input.global_size[0] * q_input.dtype.itemsize)
434        self.q_input = q_input
435
436        self._need_release = [self.loops_b, self.res_b, self.q_input]
437
438    def __call__(self, fixed_pars, pd_pars, cutoff):
439        real = (np.float32 if self.q_input.dtype == generate.F32
440                else np.float64 if self.q_input.dtype == generate.F64
441                else np.float16 if self.q_input.dtype == generate.F16
442                else np.float32)  # will never get here, so use np.float32
443
444        #print "pars", fixed_pars, pd_pars
445        res_bi = self.res_b
446        nq = np.uint32(self.q_input.nq)
447        if pd_pars:
448            cutoff = real(cutoff)
449            loops_N = [np.uint32(len(p[0])) for p in pd_pars]
450            loops = np.hstack(pd_pars) \
451                if pd_pars else np.empty(0, dtype=self.q_input.dtype)
452            loops = np.ascontiguousarray(loops.T, self.q_input.dtype).flatten()
453            #print("loops",Nloops, loops)
454
455            #import sys; print("opencl eval",pars)
456            #print("opencl eval",pars)
457            if len(loops) > 2 * MAX_LOOPS:
458                raise ValueError("too many polydispersity points")
459
460            loops_bi = self.loops_b
461            cl.enqueue_copy(self.queue, loops_bi, loops)
462            loops_l = cl.LocalMemory(len(loops.data))
463            #ctx = environment().context
464            #loops_bi = cl.Buffer(ctx, mf.READ_ONLY|mf.COPY_HOST_PTR, hostbuf=loops)
465            dispersed = [loops_bi, loops_l, cutoff] + loops_N
466        else:
467            dispersed = []
468        fixed = [real(p) for p in fixed_pars]
469        args = self.q_input.q_buffers + [res_bi, nq] + dispersed + fixed
470        self.kernel(self.queue, self.q_input.global_size, None, *args)
471        cl.enqueue_copy(self.queue, self.res, res_bi)
472
473        return self.res
474
475    def release(self):
476        """
477        Release resources associated with the kernel.
478        """
479        for v in self._need_release:
480            v.release()
481        self._need_release = []
482
483    def __del__(self):
484        self.release()
Note: See TracBrowser for help on using the repository browser.