diff --git a/doc/source/misc.rst b/doc/source/misc.rst index 6d9c7f198107acc63dcd55b856f0d44ea2215bcf..819866ac9f9a4f9677c3ef32b79d6acc5c856873 100644 --- a/doc/source/misc.rst +++ b/doc/source/misc.rst @@ -106,6 +106,7 @@ Version 2013.1 * :class:`pyopencl.CommandQueue` may be used as a context manager (in a ``with`` statement) * Add :func:`pyopencl.clmath.atan2`, :func:`pyopencl.clmath.atan2pi`. * Add :func:`pyopencl.array.concatenate`. +* Add :meth:`pyopencl.Kernel.capture_call`. Version 2012.1 -------------- diff --git a/doc/source/runtime.rst b/doc/source/runtime.rst index bbc5f6bb8774b21102d76229ad51e24e2ed33321..fc34c1f8fd4153294554640634551cd28f44cbe1 100644 --- a/doc/source/runtime.rst +++ b/doc/source/runtime.rst @@ -797,6 +797,28 @@ Programs and Kernels .. versionchanged:: 2011.1 Added the *g_times_l* keyword arg. + .. method:: capture_call(filename, queue, global_size, local_size, *args, global_offset=None, wait_for=None, g_times_l=False) + + This method supports the exact same interface as :meth:`__call__`, but + instead of invoking the kernel, it writes a self-contained PyOpenCL program + to *filename* that reproduces this invocation. Data and kernel source code + will be packaged up in *filename*'s source code. + + This is mainly intended as a debugging aid. For example, it can be used + to automate the task of creating a small, self-contained test case for + an observed problem. It can also help separate a misbehaving kernel from + a potentially large or time-consuming outer code. + + To use, simply change:: + + evt = my_kernel(queue, gsize, lsize, arg1, arg2, ...) + + to:: + + evt = my_kernel.capture_call("bug.py", queue, gsize, lsize, arg1, arg2, ...) + + .. versionadded:: 2013.1 + |comparable| .. class:: LocalMemory(size) diff --git a/pyopencl/__init__.py b/pyopencl/__init__.py index 20a07460af84383f43e53fe8fb31afb350fb2d8c..6ac18e6fd7a1ae55b63ad5ee156b26333fd0e6d6 100644 --- a/pyopencl/__init__.py +++ b/pyopencl/__init__.py @@ -69,6 +69,13 @@ class Kernel(_cl._Kernel): prg = prg._get_prg() _cl._Kernel.__init__(self, prg, name) + self._source = getattr(prg, "_source", None) + + def capture_call(self, filename, queue, global_size, local_size, + *args, **kwargs): + from pyopencl.capture_call import capture_kernel_call + capture_kernel_call(self, filename, queue, global_size, local_size, + *args, **kwargs) # }}} diff --git a/pyopencl/capture_call.py b/pyopencl/capture_call.py new file mode 100644 index 0000000000000000000000000000000000000000..7ecfa2487d89b56ae85171cfee00390a9c96c735 --- /dev/null +++ b/pyopencl/capture_call.py @@ -0,0 +1,152 @@ +from __future__ import with_statement, division + +__copyright__ = "Copyright (C) 2013 Andreas Kloeckner" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + + +import pyopencl as cl +import numpy as np +from pytools.py_codegen import PythonCodeGenerator, Indentation + + +def capture_kernel_call(kernel, filename, queue, g_size, l_size, *args, **kwargs): + try: + source = kernel._source + except AttributeError: + raise RuntimeError("cannot capture call, kernel source not available") + + if source is None: + raise RuntimeError("cannot capture call, kernel source not available") + + cg = PythonCodeGenerator() + + cg("# generated by pyopencl.capture_call") + cg("") + cg("import numpy as np") + cg("import pyopencl as cl") + cg("from base64 import b64decode") + cg("from zlib import decompress") + cg("mf = cl.mem_flags") + cg("") + + cg('CODE = r"""//CL//') + for l in source.split("\n"): + cg(l) + cg('"""') + + # {{{ invocation + + arg_data = [] + + cg("") + cg("") + cg("def main():") + with Indentation(cg): + cg("ctx = cl.create_some_context()") + cg("queue = cl.CommandQueue(ctx)") + cg("") + + kernel_args = [] + + for i, arg in enumerate(args): + if isinstance(arg, cl.Buffer): + buf = bytearray(arg.size) + cl.enqueue_copy(queue, buf, arg) + arg_data.append(("arg%d_data" % i, buf)) + cg("arg%d = cl.Buffer(ctx, " + "mf.READ_WRITE | cl.mem_flags.COPY_HOST_PTR," + % i) + cg(" hostbuf=decompress(b64decode(arg%d_data)))" + % i) + kernel_args.append("arg%d" % i) + elif isinstance(arg, (int, float)): + kernel_args.append(repr(arg)) + elif isinstance(arg, np.integer): + kernel_args.append("np.%s(%s)" % ( + arg.dtype.type.__name__, repr(int(arg)))) + elif isinstance(arg, np.floating): + kernel_args.append("np.%s(%s)" % ( + arg.dtype.type.__name__, repr(float(arg)))) + elif isinstance(arg, np.complexfloating): + kernel_args.append("np.%s(%s)" % ( + arg.dtype.type.__name__, repr(complex(arg)))) + else: + raise RuntimeError("cannot capture: " + "unsupported arg nr %d (0-based)" % i) + + cg("") + + g_times_l = kwargs.get("g_times_l", False) + if g_times_l: + dim = max(len(g_size), len(l_size)) + l_size = l_size + (1,) * (dim-len(l_size)) + g_size = g_size + (1,) * (dim-len(g_size)) + g_size = tuple( + gs*ls for gs, ls in zip(g_size, l_size)) + + global_offset = kwargs.get("global_offset", None) + if global_offset is not None: + kernel_args.append("global_offset=%s" % repr(global_offset)) + + cg("prg = cl.Program(ctx, CODE).build()") + cg("knl = prg.%s" % kernel.function_name) + if hasattr(kernel, "_arg_type_chars"): + cg("knl._arg_type_chars = %s" % repr(kernel._arg_type_chars)) + cg("knl(queue, %s, %s," % (repr(g_size), repr(l_size))) + cg(" %s)" % ", ".join(kernel_args)) + + # }}} + + # {{{ data + + from zlib import compress + from base64 import b64encode + cg("") + line_len = 70 + + for name, val in arg_data: + cg("%s = (" % name) + with Indentation(cg): + val = str(b64encode(compress(buffer(val)))) + i = 0 + while i < len(val): + cg(repr(val[i:i+line_len])) + i += line_len + + cg(")") + + # }}} + + # {{{ file trailer + + cg("") + cg("if __name__ == \"__main__\":") + with Indentation(cg): + cg("main()") + cg("") + + cg("# vim: filetype=pyopencl") + + # }}} + + with open(filename, "w") as outf: + outf.write(cg.get()) diff --git a/setup.py b/setup.py index 52a1ff5b187fd61e76b770fa055ceb764d65c7ed..55f8fcfdd93ef6c07212fabf54c254058ad9c780 100644 --- a/setup.py +++ b/setup.py @@ -201,7 +201,7 @@ def main(): packages=["pyopencl", "pyopencl.characterize", "pyopencl.compyte"], install_requires=[ - "pytools>=2011.2", + "pytools>=2013.5.2", "pytest>=2", "decorator>=3.2.0", # "Mako>=0.3.6",