src/grid/opencl/opencl.c

    OpenCL backend for GPUs

    This is the OpenCL equivalent of the CUDA backend in Basilisk. It uses the OpenCL runtime API, which can be installed on Debian systems using:

    apt install ocl-icd-opencl-dev clinfo
    cd $BASILISK/grid/opencl
    make libocl.a

    The default Makefile provides a recipe to run a program using the opencl grid:

    make myprogram.ocl.tst

    The tests cases are:

    cd $BASILISK/test
    make ocl-tests
    #include <stdlib.h>
    #include <stdio.h>
    #include <stdbool.h>
    #include <string.h>
    #include <math.h>
    #include <assert.h>
    #include <sys/stat.h>
    #include <errno.h>
    #include "a32.h"
    
    typedef struct { double x, y, z; } coord;
    typedef struct { double x, y, z; } _coord;
    typedef struct { float r, g, b, a; } vec4;
    typedef float real;
    typedef struct { int i; } scalar;
    extern int datasize;
    
    typedef struct {
      coord p, * box, n; // region
      int level; // level
    } RegionParameters;
    
    extern int N;
    extern double X0, Y0, Z0, L0;
    extern struct { int x, y, z; } Dimensions;
    
    #include "../../ast/symbols.h"
    
    enum typedef_kind_t {
      sym_SCALAR = sym_root + 1,
      sym_VECTOR,
      sym_TENSOR,
      sym_COORD,
      sym__COORD,
      sym_VEC4,
      sym_IVEC
    };
    
    #define min(a,b) ((a) < (b) ? (a) : (b))
    #define max(a,b) ((a) > (b) ? (a) : (b))
    
    #define sysrealloc realloc
    
    #define _GPU 1
    #define _OPENCL 1
    #include "../externals.h"
    #include "../gpu/backend.h"
    
    #define CL_TARGET_OPENCL_VERSION 200
    #include <CL/cl.h>
    
    // OpenCL globals
    static cl_mem ssbo = NULL;
    static cl_device_id dev = NULL;
    static cl_context ctx = NULL;
    static cl_command_queue queue = NULL;
    static cl_platform_id platform = NULL;
    
    #define CL_CHECK(x) \
      do { \
        cl_int _err = (x); \
        if (_err != CL_SUCCESS) { \
          fprintf(stderr, "%s:%d: OpenCL error: %d\n", __FILE__, __LINE__, _err); \
          exit(1); \
        } \
      } while(0)
    
    typedef struct {
      int type, nd, local;
      size_t size, esize;
      void * pointer;
    } MyUniform;
    
    struct _Ctx_ {
      cl_mem _data;
    };
    
    struct _Shader {
      unsigned ng[3], nwg[3];
      int dimension;
      struct _Ctx_ * hctx, * tmp;
      cl_mem dctx;
      MyUniform * uniforms, * locals;
      char * args;
      size_t size, lsize;
      cl_kernel kernel;
      cl_program program;
    };
    
    void free_shader (Shader * s)
    {
      if (s->kernel) clReleaseKernel(s->kernel);
      if (s->program) clReleaseProgram(s->program);
      if (s->dctx) clReleaseMemObject(s->dctx);
      free (s->hctx);
      free (s->tmp);
      free (s->uniforms);
      free (s->locals);
      free (s->args);
      free (s);
    }
    
    static
    void architecture (char * arch)
    {
      // OpenCL doesn't use sm_XX like CUDA, but we can use generic flags
      sprintf(arch, "-cl-fast-relaxed-math");
    }
    
    static
    int create_tmpdir (const char * path)
    {
      struct stat st;
      if (stat (path, &st) == 0) {
        if (S_ISDIR (st.st_mode))
          return 0; // Directory exists
        else {
          errno = ENOTDIR;
          return -1; // Path exists but is not a directory
        }
      }
      // Directory does not exist, try to create it
      if (mkdir (path, 0755) == 0)
        return 0; // Successfully created
      return -1; // Failed to create
    }
    
    Shader * load_normal_shader (const char * fs,
                                 const char * func, const char * file, int line)
    {
      // ---------------------------------------------------------------
      // Try to read from a compilation cache (by default in /tmp/buda/)
      // ---------------------------------------------------------------
      
      Adler32Hash hasha;
      a32_hash_init (&hasha);
      a32_hash_add (&hasha, fs, strlen (fs));
      uint32_t hash = a32_hash (&hasha);
    
      const char * tmpdir = getenv ("TMPDIR"), * tmp = tmpdir ? tmpdir : "/tmp";
      char cache[strlen(tmp) + strlen("/buda/ffffffff") + 1];
      sprintf (cache, "%s/buda", tmp);
      if (create_tmpdir (cache)) {
        fprintf (stderr, "%s:%d: cannot create temporary directory '%s'\n", \
                 __FILE__, __LINE__, cache);
        perror ("");
        exit (1);
      }
      sprintf (cache, "%s/buda/%x", tmp, hash);
      
      // Check if cached binary exists
      struct stat st;
      cl_program program = NULL;
      
      if (stat (cache, &st) == 0) { // found in cache
        FILE * fp = fopen (cache, "rb");
        if (fp) {
          size_t binary_size = st.st_size;
          unsigned char * binary = malloc (binary_size);
          if (binary && fread (binary, 1, binary_size, fp) == binary_size) {
            cl_int err;
            program = clCreateProgramWithBinary (ctx, 1, &dev, &binary_size, 
                                                  (const unsigned char **)&binary, NULL, &err);
            if (err == CL_SUCCESS)
              err = clBuildProgram (program, 1, &dev, "", NULL, NULL);
            if (err != CL_SUCCESS) {
              clReleaseProgram (program);
              program = NULL;
            }
          }
          if (binary) free (binary);
          fclose (fp);
        }
      }
      
      if (!program) { // not found in cache or failed to load
        char arch[256];
        architecture (arch);
        const char * sources[] = { fs };
        cl_int err;
        program = clCreateProgramWithSource (ctx, 1, sources, NULL, &err);
        CL_CHECK (err);
        err = clBuildProgram (program, 1, &dev, arch, NULL, NULL);
        if (err != CL_SUCCESS) {
          size_t log_size;
          clGetProgramBuildInfo (program, dev, CL_PROGRAM_BUILD_LOG, 0, NULL, &log_size);
          if (log_size > 1) {
            char * log = (char *) malloc (log_size);
            clGetProgramBuildInfo (program, dev, CL_PROGRAM_BUILD_LOG, log_size, log, NULL);
            char * error = gpu_errors (log, fs, NULL, "OpenCL");
            fputs (error, stderr);
            free (error);
            free (log);
          }
          clReleaseProgram (program);
          return NULL;
        }
    
    #if 0
        {
          size_t log_size;
          clGetProgramBuildInfo(program, dev,
                                CL_PROGRAM_BUILD_LOG,
                                0, NULL, &log_size);
          
          char *log = malloc(log_size);
          clGetProgramBuildInfo(program, dev,
                                CL_PROGRAM_BUILD_LOG,
                                log_size, log, NULL);
          
          fprintf (stderr, "%s:%d: %s\n", __FILE__, __LINE__, log);
          free (log);
        }
    #endif
        
        // Cache the program binary
        FILE * fp = fopen (cache, "wb");
        if (fp) {
          size_t * binary_sizes = malloc (sizeof (size_t));
          unsigned char ** binaries = malloc (sizeof (unsigned char *));
          if (binary_sizes && binaries) {
            CL_CHECK (clGetProgramInfo (program, CL_PROGRAM_BINARY_SIZES, sizeof (size_t), binary_sizes, NULL));
            binaries[0] = malloc (binary_sizes[0]);
            if (binaries[0])
              CL_CHECK (clGetProgramInfo (program, CL_PROGRAM_BINARIES, sizeof (unsigned char *), binaries, NULL));
            if (binaries[0] && binary_sizes[0] > 0)
              fwrite (binaries[0], 1, binary_sizes[0], fp);
            free (binaries[0]);
          }
          free (binaries);
          free (binary_sizes);
          fclose (fp);
        }
      }
      
      // Create kernel
      cl_int err;
      cl_kernel kernel = clCreateKernel (program, func, &err);
      CL_CHECK (err);
    
      Shader * shader = calloc (1, sizeof (Shader));
      shader->program = program;
      shader->kernel = kernel;
      return shader;
    }
    
    bool gpu_init_context (GPUData ** data)
    {
      bool initialized = (ctx != NULL);
      if (!initialized) {
        cl_uint num_platforms, num_devices;
        cl_int err;
        
        // Get platform
        err = clGetPlatformIDs (1, &platform, &num_platforms);
        CL_CHECK (err);
        
        // Get GPU device
        err = clGetDeviceIDs (platform, CL_DEVICE_TYPE_GPU, 1, &dev, &num_devices);
        if (err != CL_SUCCESS) {
          // Try default device
          err = clGetDeviceIDs (platform, CL_DEVICE_TYPE_DEFAULT, 1, &dev, &num_devices);
          CL_CHECK (err);
        }
        
        // Create context
        ctx = clCreateContext (NULL, 1, &dev, NULL, NULL, &err);
        CL_CHECK (err);
        
        // Create command queue with properties for OpenCL 2.0+
        cl_queue_properties properties[] = { CL_QUEUE_PROPERTIES, CL_QUEUE_PROFILING_ENABLE, 0 };
        queue = clCreateCommandQueueWithProperties (ctx, dev, properties, &err);
        CL_CHECK (err);
      }
      *data = NULL;
      return !initialized;
      if (false) str_append_array (NULL, NULL); // just to avoid a -Wunused-function
    }
    
    void gpu_free_context (GPUData * data)
    {
      if (ssbo) {
        CL_CHECK (clReleaseMemObject (ssbo));
        ssbo = NULL;
      }
      GPUContext.current_size = 0;
    }
    
    void realloc_ssbo (size_t field_size)
    {
      if (!datasize)
        return;
      size_t totalsize = field_size * datasize;
      assert (totalsize > GPUContext.current_size);
      if (ssbo)
        CL_CHECK (clReleaseMemObject (ssbo));
      ssbo = clCreateBuffer (ctx, CL_MEM_READ_WRITE, totalsize, NULL, NULL);
      assert (ssbo);
      GPUContext.current_size = totalsize;
    }
    
    void gpu_cpu_sync_scalar (int i, int block, char * data, size_t field_size, SyncMode mode)
    {
      size_t size = field_size * sizeof (real);
      size_t offset = i * size;
      size_t totalsize = block * size;
      char * cd = data + offset;
      
      if (mode == GPU_READ) {
        CL_CHECK (clEnqueueReadBuffer (queue, ssbo, CL_TRUE, offset, totalsize, cd, 0, NULL, NULL));
        CL_CHECK (clFinish(queue));    
      }
      else if (mode == GPU_WRITE)
        CL_CHECK (clEnqueueWriteBuffer (queue, ssbo, CL_TRUE, offset, totalsize, cd, 0, NULL, NULL));
      else
        assert (false);
    }
    
    void reset_scalar (int i, int block, size_t field_size, double val)
    {
      size_t size = field_size * sizeof (real);
      size_t offset = i * size;
      size_t totalsize = max (block, 1) * size;
    
    #if SINGLE_PRECISION
      float fval = val;
      CL_CHECK (clEnqueueFillBuffer (queue, ssbo, &fval, sizeof (float), offset, totalsize, 0, NULL, NULL));
      CL_CHECK (clFinish (queue));
    #else
      fprintf (stderr, "%s:%d: error: not implemented yet\n", __FILE__, __LINE__);
    #endif
    }
    
    // Calculate padding needed to align 'current_offset' to 'alignment'
    static size_t pad_to_align (size_t current_offset, size_t alignment) {
      return (alignment - (current_offset % alignment)) % alignment;
    }
    
    void finalize_shader (Shader * shader, External * externals, External * merged,
                          unsigned ng[3], unsigned nwg[3], int dim)
    {
      for (int i = 0; i < 3; i++)
        shader->ng[i] = ng[i], shader->nwg[i] = nwg[i];
      shader->dimension = dim;
    • Make list of local and global uniforms
      for (External * g = merged; g; g = g->next)
        g->used = 0;
      int index = 1;
      for (External * g = externals; g && g->name; g++)
        g->used = index++;
      int nglobals = 0, nlocals = 0;
      for (const External * g = merged; g; g = g->next)
        if (is_external_variable (g)) {
          int nd = g->data ? ((int *)g->data)[0] : 1;
          size_t esize;
          switch (g->type) {
          case sym_INT: case sym_LONG:
            esize = sizeof (int); break;
          case sym_FLOAT:
            esize = sizeof (float); break;
          case sym_VEC4:
            nd *= 4;
            esize = sizeof (float); break;
          case sym_BOOL:
            esize = sizeof (int); break;
    #if SINGLE_PRECISION
          case sym_DOUBLE:
            esize = sizeof (float); break;
          case sym__COORD:
            nd *= dim;
            esize = sizeof (float); break;
          case sym_COORD:
            nd *= 3;
            esize = sizeof (float); break;
    #else // DOUBLE_PRECISION
          case sym_DOUBLE:
            esize = sizeof (double); break;
          case sym__COORD:
            nd *= dim;
            esize = sizeof (double); break;
          case sym_COORD:
            nd *= 3;
            esize = sizeof (double); break;
    #endif // DOUBLE_PRECISION
          default:
            assert (false);
          }
          MyUniform uniform = {
            .type = g->type, .nd = nd, .size = nd*esize, .esize = esize,
            .local = g->global == 1 ? -1 : g->used - 1,
            .pointer = g->global == 1 ? g->pointer : NULL
          };
          if (g->global) {
            shader->uniforms = realloc (shader->uniforms, (nglobals + 2)*sizeof (MyUniform));
            shader->uniforms[nglobals] = uniform;
            shader->uniforms[nglobals + 1].type = 0;
            nglobals++;
          }
          else {
            shader->locals = realloc (shader->locals, (nlocals + 2)*sizeof (MyUniform));
            shader->locals[nlocals] = uniform;
            shader->locals[nlocals + 1].type = 0;
            nlocals++;
          }
        }
    • Allocate host and device buffers to hold uniforms.
      shader->size = sizeof (struct _Ctx_);
      for (const MyUniform * g = shader->uniforms; g && g->type; g++)
        shader->size += pad_to_align (shader->size, g->esize) + g->size;
      shader->hctx = calloc (1, shader->size);
      shader->tmp = calloc (1, shader->size);
      shader->dctx = clCreateBuffer (ctx, CL_MEM_READ_ONLY, shader->size, NULL, NULL);
    • Allocate host buffer to hold locals.
      if (shader->locals) {
        for (const MyUniform * g = shader->locals; g && g->type; g++)
          shader->lsize += pad_to_align (shader->lsize, g->esize) + g->size;
        shader->args = calloc (1, shader->lsize);
      }
    }
    
    static char * set_uniforms (const MyUniform * uniforms,
                                const External * externals,
                                char * buffer, char * start)
    {
      for (const MyUniform * g = uniforms; g && g->type; g++) {
        void * pointer = g->pointer;
        if (!pointer) {
          assert (g->local >= 0);
          pointer = externals[g->local].pointer;
        }
        buffer += pad_to_align (buffer - start, g->esize);
        switch (g->type) {
        case sym_INT: case sym_FLOAT: case sym_VEC4:
          memcpy (buffer, pointer, g->size);
          break;
        case sym_BOOL: {
          int b = *((bool *)pointer);
          memcpy (buffer, &b, g->size);
          break;
        }
        case sym_LONG: {
          int p[g->nd];
          long * data = pointer;
          for (int i = 0; i < g->nd; i++)
            p[i] = data[i];
          memcpy (buffer, p, g->size);
          break;
        }
    #if SINGLE_PRECISION
        case sym_DOUBLE: case sym__COORD: case sym_COORD: {
          float p[g->nd];
          double * data = pointer;
          for (int i = 0; i < g->nd; i++)
            p[i] = data[i];
          memcpy (buffer, p, g->size);
          break;
        }
    #else // DOUBLE_PRECISION
        case sym_DOUBLE: case sym__COORD: case sym_COORD:
          memcpy (buffer, pointer, g->size);
          break;
    #endif // DOUBLE_PRECISION
        default:
          assert (false);
        }
        buffer += g->size;
      }
      return buffer;
    }
    
    void post_setup_shader (Shader * shader, External * externals)
    {
    • Set SSBO pointer.
      assert (ssbo);
      shader->tmp->_data = ssbo;
      char * buffer = ((char *)shader->tmp) + sizeof (struct _Ctx_);
    • Set globals
      buffer = set_uniforms (shader->uniforms, externals, buffer, (char *)shader->tmp);
      assert (shader->size == buffer - (char *)shader->tmp);
    
      if (memcmp (shader->hctx, shader->tmp, shader->size)) {
        struct _Ctx_ * tmp = shader->tmp; shader->tmp = shader->hctx; shader->hctx = tmp;
        CL_CHECK (clEnqueueWriteBuffer (queue, shader->dctx, CL_TRUE, 0, shader->size, shader->hctx, 0, NULL, NULL));
      }
    • Set locals
      buffer = set_uniforms (shader->locals, externals, shader->args, shader->args);
      assert (shader->lsize == buffer - shader->args);
    }
    
    int run_shader (const Shader * shader, const RegionParameters * region)
    {
      struct { int x, y, z; } csOrigin = {0,0,0};
      size_t csOriginSize = shader->dimension*sizeof(int);
    • Set kernel arguments
      CL_CHECK (clFinish (queue)); // fixme: which synchro are necessary?
    
      // fixme: possible optimisation, only set args which have changed
      CL_CHECK (clSetKernelArg (shader->kernel, 0, sizeof (cl_mem), &ssbo));
      CL_CHECK (clSetKernelArg (shader->kernel, 1, sizeof (cl_mem), &shader->dctx));
      CL_CHECK (clSetKernelArg (shader->kernel, 2, csOriginSize, &csOrigin));
      if (shader->args)
        CL_CHECK (clSetKernelArg (shader->kernel, 3, shader->lsize, shader->args));
    • Render
    • If this is a foreach_point() iteration, we access a single point
      int Nl = region->level > 0 ? 1 << (region->level - 1) : N/Dimensions.x;
      if (region->n.x == 1 && region->n.y == 1 &&
          (shader->dimension == 2 || region->n.z == 1)) {
        assert (!GPUContext.fragment_shader);
        csOrigin.x = (region->p.x - X0)/L0*Nl*Dimensions.x;
        csOrigin.y = (region->p.y - Y0)/L0*Nl*Dimensions.x;
        if (shader->dimension == 3)
          csOrigin.z = (region->p.z - Z0)/L0*Nl*Dimensions.x;
        CL_CHECK (clSetKernelArg (shader->kernel, 2, csOriginSize, &csOrigin));
        size_t global_work_size[3] = {1, 1, 1};
        size_t local_work_size[3] = {1, 1, 1};
        CL_CHECK (clEnqueueNDRangeKernel (queue, shader->kernel, shader->dimension, NULL,
                                          global_work_size, local_work_size,
                                          0, NULL, NULL));
      }
    • This is a region
      else if (region->n.x || region->n.y || (shader->dimension == 3 && region->n.z)) {
        // OpenCL doesn't use glDrawArrays, so we need to use compute shaders
        // This would require a different approach for region rendering
        assert (false);
      }
    
      else {
        assert (!GPUContext.fragment_shader);
        size_t global_work_size[3] = {
          shader->ng[0]*shader->nwg[0],
          shader->ng[1]*shader->nwg[1],
          shader->ng[2]*shader->nwg[2]
        };
        size_t local_work_size[3] = {
          shader->nwg[0],
          shader->nwg[1],
          shader->nwg[2]
        };
        CL_CHECK (clEnqueueNDRangeKernel (queue, shader->kernel, shader->dimension, NULL,
                                          global_work_size, local_work_size,
                                          0, NULL, NULL));
      }
      CL_CHECK (clFinish (queue));
      return Nl;
    }
    
    void gpu_free_solver (void)
    {
      CL_CHECK (clFinish (queue));
      if (queue) CL_CHECK (clReleaseCommandQueue (queue));
      if (ctx) CL_CHECK (clReleaseContext (ctx));
      ctx = NULL;
      queue = NULL;
    }
    
    void gpu_synchronize (void)
    {
      if (ctx)
        CL_CHECK (clFinish (queue));
    }
    
    /* -------------------------------------------------------------------------
       OpenCL Kernel Source String
       Modified to accept an 'offset' parameter and apply it to the input indices.
       ------------------------------------------------------------------------- */
    static char kernel_source[] =
    "#define REDUCE(reduced,rhs)                                                     \n"
    "__kernel void reduce (__global const float* input, __global float* output, const int n, const int offset){\n"
    "    __local float sdata[256];                                                   \n"
    "    unsigned int tid = get_local_id(0);                                         \n"
    "    unsigned int i = get_group_id(0) * get_local_size(0) * 2 + tid;             \n"
    "    float reduced =                                                             \n" /* Patched dynamically */
    "    if (i < n)                                                                  \n"
    "        REDUCE (reduced, input[i + offset]);                                    \n" /* Offset applied here */
    "    if (i + get_local_size(0) < n)                                              \n"
    "        REDUCE (reduced, input[i + get_local_size(0) + offset]);                \n" /* Offset applied here */
    "    sdata[tid] = reduced;                                                       \n"
    "    barrier(CLK_LOCAL_MEM_FENCE);                                               \n"
    "                                                                                \n"
    #if 1 // Completely portable loop: does not depend on a warp size of 32
    "    for (unsigned int s = get_local_size(0) / 2; s > 0; s >>= 1) {              \n"
    "      if (tid < s) {                                                            \n"
    "        REDUCE(sdata[tid], sdata[tid + s]);                                     \n"
    "      }                                                                         \n"
    "      barrier(CLK_LOCAL_MEM_FENCE); // Forces all hardware types to sync safely \n"
    "    }                                                                           \n"
    #else // optimised for NVIDIA's warp size of 32
    "    for (unsigned int s = get_local_size(0)/2; s > 32; s >>= 1){\n"
    "        if (tid < s)                                                            \n"
    "            REDUCE (sdata[tid], sdata[tid + s]);                                 \n"
    "        barrier(CLK_LOCAL_MEM_FENCE);                                           \n"
    "    }                                                                           \n"
    "    if (tid < 32) {                                                             \n"
    "        __local volatile float* smem = sdata;                                   \n"
    "        REDUCE (smem[tid], smem[tid + 32]);                                     \n"
    "        REDUCE (smem[tid], smem[tid + 16]);                                     \n"
    "        REDUCE (smem[tid], smem[tid + 8]);                                      \n"
    "        REDUCE (smem[tid], smem[tid + 4]);                                      \n"
    "        REDUCE (smem[tid], smem[tid + 2]);                                      \n"
    "        REDUCE (smem[tid], smem[tid + 1]);                                      \n"
    "    }                                                                           \n"
    #endif
    "    if (tid == 0)                                                               \n"
    "        output[get_group_id(0)] = sdata[0];                                     \n"
    "}                                                                               \n";
    
    /* -------------------------------------------------------------------------
       Dynamic Runtime Kernel Compilation
       ------------------------------------------------------------------------- */
    static cl_kernel compile_kernel (const char * start, const char * op)
    {
        // 1. Patch the macro definition string
        char * s = kernel_source + strlen("#define REDUCE(reduced,rhs) ");
        memcpy (s, op, strlen (op));
        s += strlen(op); while (*s != '\n') *s++ = ' ';
        
        // 2. Patch the variable initialization line
        s = strstr (kernel_source, "float reduced = ");
        s += strlen ("float reduced = ");
        memcpy (s, start, strlen (start));
        s += strlen(start); while (*s != '\n') *s++ = ' '; 
    
        // 3. Create OpenCL Program object from the source string
        cl_int err;
        const char* src_ptr = kernel_source;
        size_t src_len = strlen(kernel_source);
        cl_program program = clCreateProgramWithSource(ctx, 1, &src_ptr, &src_len, &err);
        if (err != CL_SUCCESS) {
            fprintf(stderr, "Error creating program from source: %d\n", err);
            exit(EXIT_FAILURE);
        }
        
        // 4. Compile the OpenCL source code
        err = clBuildProgram(program, 0, NULL, "-cl-fast-relaxed-math", NULL, NULL);
        if (err != CL_SUCCESS) {
            size_t log_size;
            cl_device_id device;
            clGetCommandQueueInfo(queue, CL_QUEUE_DEVICE, sizeof(cl_device_id), &device, NULL);
            clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0, NULL, &log_size);
            
            char *log = (char *) malloc(log_size);
            clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, log_size, log, NULL);
            fprintf(stderr, "OpenCL Compilation Failure:\n%s\n", log);
            free(log);
            exit(EXIT_FAILURE);
        }
    
        // 5. Extract the compiled executable entry point
        cl_kernel kernel = clCreateKernel(program, "reduce", &err);
        if (err != CL_SUCCESS) {
            fprintf(stderr, "Error creating kernel object: %d\n", err);
            exit(EXIT_FAILURE);
        }
        
        clReleaseProgram(program); 
        return kernel;
    }
    
    /* -------------------------------------------------------------------------
       Multi-Pass Reduction Execution
       ------------------------------------------------------------------------- */
    static float opencl_reduce (cl_mem d_input, const size_t N, const char op, const int initial_offset)
    {
        cl_kernel kernel;
        switch (op) {
        case '+': {
            static cl_kernel k = 0;
            if (!k) k = compile_kernel ("0.0f;", "reduced += rhs");
            kernel = k;
            break;
        }
        case 'm': {
            static cl_kernel k = 0;
            if (!k) k = compile_kernel ("3e38f;", "reduced = min(reduced,rhs)");
            kernel = k;
            break;
        }
        case 'M': {
            static cl_kernel k = 0;
            if (!k) k = compile_kernel ("-3e38f;", "reduced = max(reduced,rhs)");
            kernel = k;
            break;
        }
        default:
            assert (0);
        }
    
        /* Track state allocations globally across sequential steps */
        static size_t Np = 0;
        static cl_mem d_output_a = NULL, d_output_b = NULL;
        cl_int err;
    
        if (N > Np) {
            const size_t max_blocks = (N + 511)/512;
            if (d_output_a) {
                clReleaseMemObject(d_output_a);
                clReleaseMemObject(d_output_b);
            }
            d_output_a = clCreateBuffer(ctx, CL_MEM_READ_WRITE, max_blocks * sizeof(float), NULL, &err);
            d_output_b = clCreateBuffer(ctx, CL_MEM_READ_WRITE, max_blocks * sizeof(float), NULL, &err);
            Np = N;
        }
            
        /* Execute down-scaling loops */
        cl_mem input = d_input;
        cl_mem output = d_output_a;
        size_t current_n = N;
        
        // The initial pass applies your calculated data offset.
        // Subsequent multi-pass reductions read from intermediate arrays starting at 0.
        int current_offset = initial_offset; 
    
        while (current_n > 1) {
            const size_t local_size = 256;
            size_t blocks = (current_n + local_size*2 - 1)/(local_size*2);
            size_t global_size = blocks * local_size; 
            int n_param = (int)current_n;
    
            // Pass arguments sequentially: input, output, n, offset
            clSetKernelArg(kernel, 0, sizeof(cl_mem), &input);
            clSetKernelArg(kernel, 1, sizeof(cl_mem), &output);
            clSetKernelArg(kernel, 2, sizeof(int), &n_param);
            clSetKernelArg(kernel, 3, sizeof(int), &current_offset);
    
            err = clEnqueueNDRangeKernel(queue, kernel, 1, NULL, &global_size, &local_size, 0, NULL, NULL);
            if (err != CL_SUCCESS) {
                fprintf(stderr, "Error enqueuing reduction kernel stage: %d\n", err);
                exit(EXIT_FAILURE);
            }
    
            current_n = blocks;
            input = output;
            output = (output == d_output_a) ? d_output_b : d_output_a;
            
            // After the first loop pass, the output is packed cleanly starting at index 0
            current_offset = 0; 
        }
    
        /* Pull the single final result back to host CPU */
        float gpu_reduce;
        clEnqueueReadBuffer(queue, input, CL_TRUE, 0, sizeof(float), &gpu_reduce, 0, NULL, NULL);
    
        return gpu_reduce;
    }
    
    /* -------------------------------------------------------------------------
       Main Top-Level API Interface Wrapper
       ------------------------------------------------------------------------- */
    double gpu_reduction (size_t offset,
                          const char op,
                          const RegionParameters * region,
                          GPUData * data,
                          size_t nb,
                          int dim)
    {
      if (region->n.x == 1 && region->n.y == 1 && (dim == 2 || region->n.z == 1)) {
        int i = (region->p.x - X0)/L0*N;
        int j = (region->p.y - Y0)/L0*N;
        int k = dim == 3 ? (region->p.z - Z0)/L0*N : 0;
        if (i < 0 || i >= N || j < 0 || j >= N || (dim == 3 && (k < 0 || k >= N)))
          return 0.;
        offset += dim == 2 ? i*N + j : (i*N + j)*N + k;
        nb = 1;
      }
        
      float result;
      if (nb == 1)
        CL_CHECK (clEnqueueReadBuffer (queue, ssbo,
                                       CL_TRUE, // Blocking read
                                       offset*sizeof(float), sizeof(float), &result, 0, NULL, NULL));
      else
        result = opencl_reduce (ssbo, nb, op, (int)offset); // fixme: what about very large offsets??
      return result;
    }