Chapter 11: Testing, Debugging, and Benchmarking GPU Kernels

This chapter covers strategies for verifying correctness, diagnosing failures, and measuring performance of OpenCL kernel code developed on top of nmathopencl.

Correctness testing

Because every kernel wrapper contains a CPU fallback path, the most reliable testing strategy compares the OpenCL output against the CPU reference output on the same inputs. Standard R unit-test frameworks (testthat, tinytest) work directly — write tests that call the wrapper function and assert numerical agreement within an appropriate tolerance (typically .Machine$double.eps^0.5 for double-precision kernels).

Key points:

  • Always run the full test suite with OpenCL disabled (no driver, or nmathopencl_has_opencl() == FALSE) as well as with it enabled. This ensures the fallback path is also covered.
  • Use opencltools::verify_opencl_runtime() as a pre-condition guard in any test that requires an active OpenCL device.
  • Numerical differences between GPU and CPU results arise from non-associative floating-point reduction order and from float vs double precision. Document your tolerance assumptions.

Debugging kernel failures

When a kernel fails to compile or execute, the OpenCL runtime reports an error code. nmathopencl propagates these as R errors via stop(). Common causes:

  • Build failure — syntax error in the .cl source. Inspect the build log returned by clGetProgramBuildInfo; nmathopencl includes it in the error message.
  • Device not found — no ICD-registered device matches the requested type. Call opencltools::gpu_names() to list available devices.
  • Buffer size mismatch — the NDRange size does not match the buffer allocation. Check that global work size equals the number of output elements.
  • Precision loss — intermediate results computed in float instead of double. Verify that the cl_khr_fp64 pragma is present and that all literals are written as 1.0 (not 1.0f).

Benchmarking

Use bench::mark() or microbenchmark::microbenchmark() to compare the GPU path against the CPU fallback. A few guidelines:

  • Warm up — the first call to any kernel incurs compilation overhead (clBuildProgram). Exclude the first iteration or run a warm-up call before timing.
  • Problem size — GPU parallelism pays off only for large work sizes (typically \(N \gtrsim 10^4\)). Benchmark across a range of \(N\) values.
  • Transfer cost — host-to-device and device-to-host buffer copies (clEnqueueWriteBuffer / clEnqueueReadBuffer) are included in the wrapper timing. For latency-sensitive use cases, consider whether the data can remain on the device between calls.
  • Baseline — compare against both the nmathopencl CPU fallback and the upstream stats:: function to understand relative overheads.