Index: sasmodels/kernelcl.py
===================================================================
--- sasmodels/kernelcl.py	(revision f872fd13522af1df2bf929529d37c828956eb8ef)
+++ sasmodels/kernelcl.py	(revision 7126c043266fd657e70b79fa34fc92da51d68894)
@@ -265,5 +265,4 @@
         # Cache for compiled programs, and for items in context
         self.compiled = {}
-        self.cache = {}
 
     def has_type(self, dtype):
@@ -297,21 +296,5 @@
         return program
 
-    def free_buffer(self, key):
-        if key in self.cache:
-            self.cache[key].release()
-            del self.cache[key]
-
-    def __del__(self):
-        for v in self.cache.values():
-            release = getattr(v, 'release', lambda: None)
-            release()
-        self.cache = {}
-
 _CURRENT_ID = 0
-def unique_id():
-    global _CURRENT_ID
-    _CURRENT_ID += 1
-    return _CURRENT_ID
-
 def _create_some_context():
     # type: () -> cl.Context
@@ -413,4 +396,11 @@
     that the compiler is allowed to take shortcuts.
     """
+    info = None # type: ModelInfo
+    source = "" # type: str
+    dtype = None # type: np.dtype
+    fast = False # type: bool
+    _program = None # type: cl.Program
+    _kernels = None # type: Dict[str, cl.Kernel]
+
     def __init__(self, source, model_info, dtype=generate.F32, fast=False):
         # type: (Dict[str,str], ModelInfo, np.dtype, bool) -> None
@@ -419,6 +409,4 @@
         self.dtype = dtype
         self.fast = fast
-        self.timestamp = generate.ocl_timestamp(self.info)
-        self._cache_key = unique_id()
 
     def __getstate__(self):
@@ -429,4 +417,5 @@
         # type: (Tuple[ModelInfo, str, np.dtype, bool]) -> None
         self.info, self.source, self.dtype, self.fast = state
+        self._program = self._kernels = None
 
     def make_kernel(self, q_vectors):
@@ -434,9 +423,5 @@
         return GpuKernel(self, q_vectors)
 
-    @property
-    def Iq(self):
-        return self._fetch_kernel('Iq')
-
-    def fetch_kernel(self, name):
+    def get_function(self, name):
         # type: (str) -> cl.Kernel
         """
@@ -444,23 +429,24 @@
         does not already exist.
         """
-        gpu = environment()
-        key = self._cache_key
-        if key not in gpu.cache:
-            program = gpu.compile_program(
-                self.info.name,
-                self.source['opencl'],
-                self.dtype,
-                self.fast,
-                self.timestamp)
-            variants = ['Iq', 'Iqxy', 'Imagnetic']
-            names = [generate.kernel_name(self.info, k) for k in variants]
-            kernels = [getattr(program, k) for k in names]
-            data = dict((k, v) for k, v in zip(variants, kernels))
-            # keep a handle to program so GC doesn't collect
-            data['program'] = program
-            gpu.cache[key] = data
-        else:
-            data = gpu.cache[key]
-        return data[name]
+        if self._program is None:
+            self._prepare_program()
+        return self._kernels[name]
+
+    def _prepare_program(self):
+        # type: (str) -> None
+        env = environment()
+        timestamp = generate.ocl_timestamp(self.info)
+        program = env.compile_program(
+            self.info.name,
+            self.source['opencl'],
+            self.dtype,
+            self.fast,
+            timestamp)
+        variants = ['Iq', 'Iqxy', 'Imagnetic']
+        names = [generate.kernel_name(self.info, k) for k in variants]
+        handles = [getattr(program, k) for k in names]
+        self._kernels = {k: v for k, v in zip(variants, handles)}
+        # keep a handle to program so GC doesn't collect
+        self._program = program
 
 # TODO: check that we don't need a destructor for buffers which go out of scope
@@ -504,18 +490,11 @@
             self.q[:self.nq] = q_vectors[0]
         self.global_size = [self.q.shape[0]]
-        self._cache_key = unique_id()
-
-    @property
-    def q_b(self):
-        """Lazy creation of q buffer so it can survive context reset"""
+        #print("creating inputs of size", self.global_size)
+
+        # transfer input value to gpu
         env = environment()
-        key = self._cache_key
-        if key not in env.cache:
-            context = env.context[self.dtype]
-            #print("creating inputs of size", self.global_size)
-            buffer = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,
-                               hostbuf=self.q)
-            env.cache[key] = buffer
-        return env.cache[key]
+        context = env.context[self.dtype]
+        self.q_b = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,
+                             hostbuf=self.q)
 
     def release(self):
@@ -524,5 +503,7 @@
         Free the buffer associated with the q value
         """
-        environment().free_buffer(id(self))
+        if self.q_b is not None:
+            self.q_b.release()
+            self.q_b = None
 
     def __del__(self):
@@ -536,24 +517,23 @@
     *model* is the GpuModel object to call
 
-    The following attributes are defined:
-
-    *info* is the module information
-
-    *dtype* is the kernel precision
-
-    *dim* is '1d' or '2d'
-
-    *result* is a vector to contain the results of the call
-
-    The resulting call method takes the *pars*, a list of values for
-    the fixed parameters to the kernel, and *pd_pars*, a list of (value,weight)
-    vectors for the polydisperse parameters.  *cutoff* determines the
-    integration limits: any points with combined weight less than *cutoff*
-    will not be calculated.
+    The kernel is derived from :class:`Kernel`, providing the
+    :meth:`call_kernel` method to evaluate the kernel for a given set of
+    parameters.  Because of the need to move the q values to the GPU before
+    evaluation, the kernel is instantiated for a particular set of q vectors,
+    and can be called many times without transfering q each time.
 
     Call :meth:`release` when done with the kernel instance.
     """
+    #: SAS model information structure
+    info = None # type: ModelInfo
+    #: kernel precision
+    dtype = None # type: np.dtype
+    #: kernel dimensions (1d or 2d)
+    dim = "" # type: str
+    #: calculation results, updated after each call to :meth:`_call_kernel`
+    result = None # type: np.ndarray
+
     def __init__(self, model, q_vectors):
-        # type: (cl.Kernel, np.dtype, ModelInfo, List[np.ndarray]) -> None
+        # type: (GpuModel, List[np.ndarray]) -> None
         dtype = model.dtype
         self.q_input = GpuInput(q_vectors, dtype)
@@ -561,5 +541,4 @@
         # F16 isn't sufficient, so don't support it
         self._as_dtype = np.float64 if dtype == generate.F64 else np.float32
-        self._cache_key = unique_id()
 
         # attributes accessed from the outside
@@ -573,15 +552,9 @@
         self.result = np.empty(self.q_input.nq*nout+extra_q, dtype)
 
-    @property
-    def _result_b(self):
-        """Lazy creation of result buffer so it can survive context reset"""
+        # allocate result value on gpu
         env = environment()
-        key = self._cache_key
-        if key not in env.cache:
-            context = env.context[self.dtype]
-            width = ((self.result.size+31)//32)*32 * self.dtype.itemsize
-            buffer = cl.Buffer(context, mf.READ_WRITE, width)
-            env.cache[key] = buffer
-        return env.cache[key]
+        context = env.context[self.dtype]
+        width = ((self.result.size+31)//32)*32 * self.dtype.itemsize
+        self._result_b = cl.Buffer(context, mf.READ_WRITE, width)
 
     def _call_kernel(self, call_details, values, cutoff, magnetic, effective_radius_type):
@@ -592,6 +565,4 @@
 
         # Arrange data transfer to/from card
-        q_b = self.q_input.q_b
-        result_b = self._result_b
         details_b = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,
                               hostbuf=call_details.buffer)
@@ -600,8 +571,8 @@
 
         name = 'Iq' if self.dim == '1d' else 'Imagnetic' if magnetic else 'Iqxy'
-        kernel = self._model.fetch_kernel(name)
+        kernel = self._model.get_function(name)
         kernel_args = [
             np.uint32(self.q_input.nq), None, None,
-            details_b, values_b, q_b, result_b,
+            details_b, values_b, self.q_input.q_b, self._result_b,
             self._as_dtype(cutoff),
             np.uint32(effective_radius_type),
@@ -626,11 +597,10 @@
                     time.sleep(0.001)
                     last_nap = current_time
-        cl.enqueue_copy(queue, self.result, result_b, wait_for=wait_for)
+        cl.enqueue_copy(queue, self.result, self._result_b, wait_for=wait_for)
         #print("result", self.result)
 
         # Free buffers
-        for v in (details_b, values_b):
-            if v is not None:
-                v.release()
+        details_b.release()
+        values_b.release()
 
     def release(self):
@@ -639,6 +609,8 @@
         Release resources associated with the kernel.
         """
-        environment().free_buffer(id(self))
         self.q_input.release()
+        if self._result_b is not None:
+            self._result_b.release()
+            self._result_b = None
 
     def __del__(self):
Index: sasmodels/kernelcuda.py
===================================================================
--- sasmodels/kernelcuda.py	(revision f872fd13522af1df2bf929529d37c828956eb8ef)
+++ sasmodels/kernelcuda.py	(revision 7126c043266fd657e70b79fa34fc92da51d68894)
@@ -296,6 +296,6 @@
     dtype = None # type: np.dtype
     fast = False # type: bool
-    program = None # type: SourceModule
-    _kernels = None # type: List[cuda.Function]
+    _program = None # type: SourceModule
+    _kernels = None # type: Dict[str, cuda.Function]
 
     def __init__(self, source, model_info, dtype=generate.F32, fast=False):
@@ -305,6 +305,4 @@
         self.dtype = dtype
         self.fast = fast
-        self.program = None # delay program creation
-        self._kernels = None
 
     def __getstate__(self):
@@ -315,27 +313,36 @@
         # type: (Tuple[ModelInfo, str, np.dtype, bool]) -> None
         self.info, self.source, self.dtype, self.fast = state
-        self.program = None
+        self._program = self._kernels = None
 
     def make_kernel(self, q_vectors):
         # type: (List[np.ndarray]) -> "GpuKernel"
-        if self.program is None:
-            compile_program = environment().compile_program
-            timestamp = generate.ocl_timestamp(self.info)
-            self.program = compile_program(
-                self.info.name,
-                self.source['opencl'],
-                self.dtype,
-                self.fast,
-                timestamp)
-            variants = ['Iq', 'Iqxy', 'Imagnetic']
-            names = [generate.kernel_name(self.info, k) for k in variants]
-            kernels = [self.program.get_function(k) for k in names]
-            self._kernels = dict((k, v) for k, v in zip(variants, kernels))
-        is_2d = len(q_vectors) == 2
-        if is_2d:
-            kernel = [self._kernels['Iqxy'], self._kernels['Imagnetic']]
-        else:
-            kernel = [self._kernels['Iq']]*2
-        return GpuKernel(kernel, self.dtype, self.info, q_vectors)
+        return GpuKernel(self, q_vectors)
+
+    def get_function(self, name):
+        # type: (str) -> cuda.Function
+        """
+        Fetch the kernel from the environment by name, compiling it if it
+        does not already exist.
+        """
+        if self._program is None:
+            self._prepare_program()
+        return self._kernels[name]
+
+    def _prepare_program(self):
+        # type: (str) -> None
+        env = environment()
+        timestamp = generate.ocl_timestamp(self.info)
+        program = env.compile_program(
+            self.info.name,
+            self.source['opencl'],
+            self.dtype,
+            self.fast,
+            timestamp)
+        variants = ['Iq', 'Iqxy', 'Imagnetic']
+        names = [generate.kernel_name(self.info, k) for k in variants]
+        handles = [program.get_function(k) for k in names]
+        self._kernels = {k: v for k, v in zip(variants, kernels)}
+        # keep a handle to program so GC doesn't collect
+        self._program = program
 
     def release(self):
@@ -394,4 +401,6 @@
         self.global_size = [self.q.shape[0]]
         #print("creating inputs of size", self.global_size)
+
+        # transfer input value to gpu
         self.q_b = cuda.to_device(self.q)
 
@@ -413,24 +422,28 @@
     Callable SAS kernel.
 
-    *kernel* is the GpuKernel object to call
-
-    *model_info* is the module information
-
-    *q_vectors* is the q vectors at which the kernel should be evaluated
-
-    *dtype* is the kernel precision
-
-    The resulting call method takes the *pars*, a list of values for
-    the fixed parameters to the kernel, and *pd_pars*, a list of (value,weight)
-    vectors for the polydisperse parameters.  *cutoff* determines the
-    integration limits: any points with combined weight less than *cutoff*
-    will not be calculated.
+    *model* is the GpuModel object to call
+
+    The kernel is derived from :class:`Kernel`, providing the
+    :meth:`call_kernel` method to evaluate the kernel for a given set of
+    parameters.  Because of the need to move the q values to the GPU before
+    evaluation, the kernel is instantiated for a particular set of q vectors,
+    and can be called many times without transfering q each time.
 
     Call :meth:`release` when done with the kernel instance.
     """
-    def __init__(self, kernel, dtype, model_info, q_vectors):
-        # type: (cl.Kernel, np.dtype, ModelInfo, List[np.ndarray]) -> None
+    #: SAS model information structure
+    info = None # type: ModelInfo
+    #: kernel precision
+    dtype = None # type: np.dtype
+    #: kernel dimensions (1d or 2d)
+    dim = "" # type: str
+    #: calculation results, updated after each call to :meth:`_call_kernel`
+    result = None # type: np.ndarray
+
+    def __init__(self, model, q_vectors):
+        # type: (GpuModel, List[np.ndarray]) -> None
+        dtype = model.dtype
         self.q_input = GpuInput(q_vectors, dtype)
-        self.kernel = kernel
+        self._model = model
         # F16 isn't sufficient, so don't support it
         self._as_dtype = np.float64 if dtype == generate.F64 else np.float32
@@ -438,6 +451,6 @@
         # attributes accessed from the outside
         self.dim = '2d' if self.q_input.is_2d else '1d'
-        self.info = model_info
-        self.dtype = dtype
+        self.info = model.info
+        self.dtype = model.dtype
 
         # holding place for the returned value
@@ -446,9 +459,7 @@
         self.result = np.empty(self.q_input.nq*nout+extra_q, dtype)
 
-        # Inputs and outputs for each kernel call
-        # Note: res may be shorter than res_b if global_size != nq
+        # allocate result value on gpu
         width = ((self.result.size+31)//32)*32 * self.dtype.itemsize
-        self.result_b = cuda.mem_alloc(width)
-        self._need_release = [self.result_b]
+        self._result_b = cuda.mem_alloc(width)
 
     def _call_kernel(self, call_details, values, cutoff, magnetic, effective_radius_type):
@@ -458,6 +469,7 @@
         values_b = cuda.to_device(values)
 
-        kernel = self.kernel[1 if magnetic else 0]
-        args = [
+        name = 'Iq' if self.dim == '1d' else 'Imagnetic' if magnetic else 'Iqxy'
+        kernel = self._model.get_function(name)
+        kernel_args = [
             np.uint32(self.q_input.nq), None, None,
             details_b, values_b, self.q_input.q_b, self.result_b,
@@ -496,7 +508,8 @@
         Release resources associated with the kernel.
         """
-        for p in self._need_release:
-            p.free()
-        self._need_release = []
+        self.q_input.release()
+        if self._result_b is not None:
+            self._result_b.free()
+            self._result_b = None
 
     def __del__(self):
