source: sasmodels/sasmodels/gpu.py @ 994d77f

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

Convert double to float rather than using real

  • Property mode set to 100644
File size: 12.0 KB
Line 
1"""
2GPU support through OpenCL
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"""
25import numpy as np
26import pyopencl as cl
27from pyopencl import mem_flags as mf
28
29from . import gen
30
31F64_DEFS = """\
32#ifdef cl_khr_fp64
33#  pragma OPENCL EXTENSION cl_khr_fp64: enable
34#endif
35"""
36
37# The max loops number is limited by the amount of local memory available
38# on the device.  You don't want to make this value too big because it will
39# waste resources, nor too small because it may interfere with users trying
40# to do their polydispersity calculations.  A value of 1024 should be much
41# larger than necessary given that cost grows as npts^k where k is the number
42# of polydisperse parameters.
43MAX_LOOPS = 2048
44
45def load_model(kernel_module, dtype="single"):
46    """
47    Load the OpenCL model defined by *kernel_module*.
48
49    Access to the OpenCL device is delayed until the kernel is called
50    so models can be defined without using too many resources.
51    """
52    source, info = gen.make(kernel_module)
53    ## for debugging, save source to a .cl file, edit it, and reload as model
54    #open(info['name']+'.cl','w').write(source)
55    #source = open(info['name']+'.cl','r').read()
56    return GpuModel(source, info, dtype)
57
58ENV = None
59def environment():
60    """
61    Returns a singleton :class:`GpuEnvironment`.
62
63    This provides an OpenCL context and one queue per device.
64    """
65    global ENV
66    if ENV is None:
67        ENV = GpuEnvironment()
68    return ENV
69
70def has_double(device):
71    """
72    Return true if device supports double precision.
73    """
74    return "cl_khr_fp64" in device.extensions
75
76
77def _stretch_input(vector, dtype, extra=1e-3, boundary=128):
78    """
79    Stretch an input vector to the correct boundary.
80
81    Performance on the kernels can drop by a factor of two or more if the
82    number of values to compute does not fall on a nice power of two
83    boundary.  A good choice for the boundary value is the
84    min_data_type_align_size property of the OpenCL device.  The usual
85    value of 128 gives a working size as a multiple of 32.  The trailing
86    additional vector elements are given a value of *extra*, and so
87    f(*extra*) will be computed for each of them.  The returned array
88    will thus be a subset of the computed array.
89    """
90    boundary // dtype.itemsize
91    remainder = vector.size%boundary
92    size = vector.size + (boundary - remainder if remainder != 0 else 0)
93    if size != vector.size:
94        vector = np.hstack((vector, [extra]*(size-vector.size)))
95    return np.ascontiguousarray(vector, dtype=dtype)
96
97
98def compile_model(context, source, dtype):
99    """
100    Build a model to run on the gpu.
101
102    Returns the compiled program and its type.  The returned type will
103    be float32 even if the desired type is float64 if any of the
104    devices in the context do not support the cl_khr_fp64 extension.
105    """
106    dtype = np.dtype(dtype)
107    if dtype==gen.F64 and not all(has_double(d) for d in context.devices):
108        raise RuntimeError("Double precision not supported for devices")
109
110    header = F64_DEFS if dtype == gen.F64 else ""
111    if dtype == gen.F32:
112        source = gen.use_single(source)
113    # Note: USE_SINCOS makes the intel cpu slower under opencl
114    if context.devices[0].type == cl.device_type.GPU:
115        header += "#define USE_SINCOS\n"
116    program  = cl.Program(context, header+source).build()
117    return program
118
119
120def make_result(self, size):
121    self.res = np.empty(size, dtype=self.dtype)
122    self.res_b = cl.Buffer(self.program.context, mf.READ_WRITE, self.res.nbytes)
123    return self.res, self.res_b
124
125
126# for now, this returns one device in the context
127# TODO: create a context that contains all devices on all platforms
128class GpuEnvironment(object):
129    """
130    GPU context, with possibly many devices, and one queue per device.
131    """
132    def __init__(self):
133        self.context = cl.create_some_context()
134        self.queues = [cl.CommandQueue(self.context, d)
135                       for d in self.context.devices]
136        self.boundary = max(d.min_data_type_align_size
137                            for d in self.context.devices)
138        self.has_double = all(has_double(d) for d in self.context.devices)
139        self.compiled = {}
140
141    def compile_program(self, name, source, dtype):
142        if name not in self.compiled:
143            #print "compiling",name
144            self.compiled[name] = compile_model(self.context, source, dtype)
145        return self.compiled[name]
146
147    def release_program(self, name):
148        if name in self.compiled:
149            self.compiled[name].release()
150            del self.compiled[name]
151
152class GpuModel(object):
153    """
154    GPU wrapper for a single model.
155
156    *source* and *info* are the model source and interface as returned
157    from :func:`gen.make`.
158
159    *dtype* is the desired model precision.  Any numpy dtype for single
160    or double precision floats will do, such as 'f', 'float32' or 'single'
161    for single and 'd', 'float64' or 'double' for double.  Double precision
162    is an optional extension which may not be available on all devices.
163    """
164    def __init__(self, source, info, dtype=gen.F32):
165        self.info = info
166        self.source = source
167        self.dtype = dtype
168        self.program = None # delay program creation
169
170    def __getstate__(self):
171        state = self.__dict__.copy()
172        state['program'] = None
173        return state
174
175    def __setstate__(self, state):
176        self.__dict__ = state.copy()
177
178    def __call__(self, input):
179        if self.dtype != input.dtype:
180            raise TypeError("data and kernel have different types")
181        if self.program is None:
182            self.program = environment().compile_program(self.info['name'],self.source, self.dtype)
183        kernel_name = gen.kernel_name(self.info, input.is_2D)
184        kernel = getattr(self.program, kernel_name)
185        return GpuKernel(kernel, self.info, input)
186
187    def release(self):
188        if self.program is not None:
189            environment().release_program(self.info['name'])
190            self.program = None
191
192    def make_input(self, q_vectors):
193        """
194        Make q input vectors available to the model.
195
196        This only needs to be done once for all models that operate on the
197        same input.  So for example, if you are adding two different models
198        together to compare to a data set, then only one model needs to
199        needs to call make_input, so long as the models have the same dtype.
200        """
201        # Note: the weird interface, where make_input doesn't care which
202        # model calls it, allows us to ask the model to define the data
203        # and the caller does not need to know if it is opencl or ctypes.
204        # The returned data object is opaque.
205        return GpuInput(q_vectors, dtype=self.dtype)
206
207# TODO: check that we don't need a destructor for buffers which go out of scope
208class GpuInput(object):
209    """
210    Make q data available to the gpu.
211
212    *q_vectors* is a list of q vectors, which will be *[q]* for 1-D data,
213    and *[qx, qy]* for 2-D data.  Internally, the vectors will be reallocated
214    to get the best performance on OpenCL, which may involve shifting and
215    stretching the array to better match the memory architecture.  Additional
216    points will be evaluated with *q=1e-3*.
217
218    *dtype* is the data type for the q vectors. The data type should be
219    set to match that of the kernel, which is an attribute of
220    :class:`GpuProgram`.  Note that not all kernels support double
221    precision, so even if the program was created for double precision,
222    the *GpuProgram.dtype* may be single precision.
223
224    Call :meth:`release` when complete.  Even if not called directly, the
225    buffer will be released when the data object is freed.
226    """
227    def __init__(self, q_vectors, dtype=gen.F32):
228        env = environment()
229        self.nq = q_vectors[0].size
230        self.dtype = np.dtype(dtype)
231        self.is_2D = (len(q_vectors) == 2)
232        self.q_vectors = [
233            _stretch_input(q, self.dtype, boundary=env.boundary)
234            for q in q_vectors
235        ]
236        self.q_buffers = [
237            cl.Buffer(env.context,  mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=q)
238            for q in self.q_vectors
239        ]
240        self.global_size = [self.q_vectors[0].size]
241
242    def release(self):
243        for b in self.q_buffers:
244            b.release()
245        self.q_buffers = []
246
247class GpuKernel(object):
248    """
249    Callable SAS kernel.
250
251    *kernel* is the GpuKernel object to call.
252
253    *info* is the module information
254
255    *input* is the DllInput q vectors at which the kernel should be
256    evaluated.
257
258    The resulting call method takes the *pars*, a list of values for
259    the fixed parameters to the kernel, and *pd_pars*, a list of (value,weight)
260    vectors for the polydisperse parameters.  *cutoff* determines the
261    integration limits: any points with combined weight less than *cutoff*
262    will not be calculated.
263
264    Call :meth:`release` when done with the kernel instance.
265    """
266    def __init__(self, kernel, info, input):
267        self.input = input
268        self.kernel = kernel
269        self.info = info
270        self.res = np.empty(input.nq, input.dtype)
271        dim = '2d' if input.is_2D else '1d'
272        self.fixed_pars = info['partype']['fixed-'+dim]
273        self.pd_pars = info['partype']['pd-'+dim]
274
275        # Inputs and outputs for each kernel call
276        # Note: res may be shorter than res_b if global_size != nq
277        env = environment()
278        self.loops_b = [cl.Buffer(env.context, mf.READ_WRITE,
279                                  2*MAX_LOOPS*input.dtype.itemsize)
280                        for _ in env.queues]
281        self.res_b = [cl.Buffer(env.context, mf.READ_WRITE,
282                                input.global_size[0]*input.dtype.itemsize)
283                      for _ in env.queues]
284
285
286    def __call__(self, pars, pd_pars, cutoff=1e-5):
287        real = np.float32 if self.input.dtype == gen.F32 else np.float64
288        fixed = [real(p) for p in pars]
289        cutoff = real(cutoff)
290        loops = np.hstack(pd_pars)
291        loops = np.ascontiguousarray(loops.T, self.input.dtype).flatten()
292        Nloops = [np.uint32(len(p[0])) for p in pd_pars]
293        #print "loops",Nloops, loops
294
295        #import sys; print >>sys.stderr,"opencl eval",pars
296        #print "opencl eval",pars
297        if len(loops) > 2*MAX_LOOPS:
298            raise ValueError("too many polydispersity points")
299        device_num = 0
300        res_bi = self.res_b[device_num]
301        queuei = environment().queues[device_num]
302        loops_bi = self.loops_b[device_num]
303        loops_l = cl.LocalMemory(len(loops.data))
304        cl.enqueue_copy(queuei, loops_bi, loops)
305        #ctx = environment().context
306        #loops_bi = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=loops)
307        args = self.input.q_buffers + [res_bi,loops_bi,loops_l,cutoff] + fixed + Nloops
308        self.kernel(queuei, self.input.global_size, None, *args)
309        cl.enqueue_copy(queuei, self.res, res_bi)
310
311        return self.res
312
313    def release(self):
314        for b in self.loops_b:
315            b.release()
316        self.loops_b = []
317        for b in self.res_b:
318            b.release()
319        self.res_b = []
320
321    def __del__(self):
322        self.release()
Note: See TracBrowser for help on using the repository browser.