强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

OpenGL / OpenCL 编程指南 / 第 12 章:内核编程

第 12 章:内核编程

内核(Kernel)是 OpenCL 中运行在设备端的计算函数。理解 OpenCL 的内存层次是写出高性能内核的关键。


12.1 内核函数基础

12.1.1 内核声明

// OpenCL C 内核函数
__kernel void my_kernel(
    __global float *input,      // 全局内存
    __global float *output,     // 全局内存
    __constant float *weights,  // 常量内存
    __local float *scratch,     // 局部内存
    const int count             // 标量参数(通过值传递)
) {
    // ...
}

12.1.2 地址空间限定符

限定符缩写作用域存储位置典型用途
__globalglobal全局显存(VRAM)输入/输出数据
__locallocal工作组片上 SRAM工作组内共享
__constantconstant全局(只读)常量缓存查找表、参数
__privateprivate线程寄存器局部变量
__genericgeneric自动推断取决于赋值OpenCL 2.0+

12.1.3 内核参数传递

// C++ 主机端设置参数
cl_kernel kernel = clCreateKernel(program, "my_kernel", &err);

// 内存对象参数:传递 cl_mem 的地址
cl_mem buffer = clCreateBuffer(...);
clSetKernelArg(kernel, 0, sizeof(cl_mem), &buffer);

// 标量参数:传递值的地址
int count = 1024;
clSetKernelArg(kernel, 4, sizeof(int), &count);

// 局部内存:只分配大小,不传数据
clSetKernelArg(kernel, 3, 256 * sizeof(float), NULL);

// 设置完参数后,内核可以多次执行(参数不变的情况下)

12.2 内存层次结构

12.2.1 内存层次图

┌─────────────────────────────────────────────┐
│               主机内存 (Host Memory)          │
│              DDR4/DDR5, ~50 GB/s             │
├────────────┬────────────┬───────────────────┤
│  数据传输    │  数据传输    │  映射 (Map)        │
├────────────┴────────────┴───────────────────┤
│                全局内存 (Global Memory)        │
│               VRAM GDDR6X, ~500 GB/s         │
├───────────────────┬─────────────────────────┤
│   常量缓存          │                         │
│   (Constant Cache) │                         │
│   ~数 TB/s, 只读   │                         │
├───────────────────┴─────────────────────────┤
│            局部内存 (Local Memory)             │
│            片上 SRAM, ~数 TB/s                │
│            工作组内共享,~32-64 KB             │
├─────────────────────────────────────────────┤
│              私有内存 (Private Memory)         │
│              寄存器, ~数十 TB/s                │
│              每线程独占                        │
└─────────────────────────────────────────────┘

12.2.2 各层内存规格

内存类型典型大小带宽延迟可见范围
私有(寄存器)数 KB/线程最高~1 周期单线程
局部32-64 KB/组~数 TB/s~20 周期工作组
常量64 KB~数 TB/s~20 周期全局只读
全局数 GB~500 GB/s~200 周期全局可读写

12.3 全局内存优化

12.3.1 合并访问(Coalesced Access)

全局内存的性能关键:相邻线程应访问相邻地址

// ✅ 合并访问:线程 i 访问元素 i
__kernel void good_access(__global float *data) {
    int gid = get_global_id(0);
    float val = data[gid];  // 连续访问
}

// ❌ 步长访问:线程 i 访问元素 i*stride
__kernel void bad_access(__global float *data, int stride) {
    int gid = get_global_id(0);
    float val = data[gid * stride];  // 非连续,缓存不友好
}
合并访问(好):              步长访问(差):
线程 0 → 地址 0             线程 0 → 地址 0
线程 1 → 地址 1             线程 1 → 地址 4
线程 2 → 地址 2             线程 2 → 地址 8
线程 3 → 地址 3             线程 3 → 地址 12
        ↓                           ↓
  1 次内存事务              4 次内存事务

12.3.2 避免全局内存银行冲突

// ❌ 冲突:所有线程访问同一银行
float val = data[0];

// ✅ 无冲突:不同线程访问不同银行
float val = data[get_local_id(0)];

12.3.3 向量化加载

// 使用向量类型提高内存吞吐
__kernel void vectorized(__global float4 *input, __global float4 *output) {
    int gid = get_global_id(0);
    float4 val = input[gid];  // 一次加载 16 字节
    val = val * 2.0f;
    output[gid] = val;
}

12.4 局部内存

12.4.1 使用模式

局部内存用于工作组内线程间的数据共享和复用:

__kernel void local_example(
    __global float *input,
    __global float *output,
    __local float *scratch  // 局部内存(主机端传 NULL)
) {
    int lid = get_local_id(0);
    int gid = get_global_id(0);

    // 1. 从全局内存加载到局部内存
    scratch[lid] = input[gid];
    barrier(CLK_LOCAL_MEM_FENCE);  // 同步工作组

    // 2. 在局部内存上操作(快速)
    float sum = 0.0f;
    for (int i = 0; i < get_local_size(0); i++) {
        sum += scratch[i];
    }

    // 3. 写回全局内存
    if (lid == 0) {
        output[get_group_id(0)] = sum;
    }
}

12.4.2 并行规约优化

// 工作组内并行求和
__kernel void parallel_sum(
    __global float *input,
    __global float *output,
    __local float *scratch
) {
    int lid = get_local_id(0);
    int size = get_local_size(0);

    scratch[lid] = input[get_global_id(0)];
    barrier(CLK_LOCAL_MEM_FENCE);

    for (int stride = size / 2; stride > 0; stride >>= 1) {
        if (lid < stride) {
            scratch[lid] += scratch[lid + stride];
        }
        barrier(CLK_LOCAL_MEM_FENCE);
    }

    if (lid == 0) {
        output[get_group_id(0)] = scratch[0];
    }
}

12.4.3 局部内存大小查询

cl_ulong local_mem_size;
clGetDeviceInfo(device, CL_DEVICE_LOCAL_MEM_SIZE, sizeof(local_mem_size), &local_mem_size, NULL);
printf("Local memory: %lu KB\n", local_mem_size / 1024);  // 通常 32-64 KB

12.5 常量内存

12.5.1 使用方式

// OpenCL C 中声明
__constant float weights[256];  // 只读,64 KB 限制

__kernel void use_weights(__global float *data, __global float *out) {
    int gid = get_global_id(0);
    float sum = 0.0f;
    for (int i = 0; i < 256; i++) {
        sum += data[gid * 256 + i] * weights[i];
    }
    out[gid] = sum;
}
// 主机端:创建常量缓冲区
cl_mem weights_buf = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
                                     sizeof(weights), weights_data, &err);

// 绑定到内核的 __constant 参数
clSetKernelArg(kernel, 1, sizeof(cl_mem), &weights_buf);

12.5.2 何时使用常量内存

场景使用常量内存
所有线程读取相同的少量数据✅ 高效(广播模式)
查找表(LUT)✅ 适合
不同线程读取不同地址❌ 串行化,性能差
数据量 > 64 KB❌ 超出限制,使用全局内存

12.6 私有内存

12.6.1 默认行为

__kernel void private_example(__global float *data) {
    int gid = get_global_id(0);

    // 以下变量默认在私有内存(寄存器)
    float temp = data[gid];
    int count = 100;
    float results[4];  // 小数组可能在寄存器中

    // 如果寄存器溢出,编译器会自动溢出到"私有内存"(实际上是局部/全局内存)
    float large_array[1024];  // ⚠️ 可能导致寄存器压力,影响占用率
}

⚠️ 寄存器溢出(Register Spill):当每个线程使用的寄存器过多时,编译器会将变量溢出到慢速内存。使用 __attribute__((register_num(N))) 可以提示编译器限制寄存器使用,但最终由编译器决定。


12.7 内存模型对照

操作延迟吞吐建议
寄存器访问~1 周期最高优先使用
局部内存读写~20 周期工作组内共享数据
全局内存合并读~200 周期尽量合并访问
全局内存随机读~400+ 周期避免
常量内存广播~20 周期高(同地址)所有线程读同一地址

12.8 内核编程最佳实践

12.8.1 工作组大小选择

// 通常选择 64、128、256、512、1024
size_t local_size = 256;

// 必须是 2 的幂(某些设备要求)
// 必须 ≤ CL_DEVICE_MAX_WORK_GROUP_SIZE
// 必须能整除 global_size

12.8.2 条件分支优化

// ❌ 差:工作项发散
if (gid % 2 == 0) {
    // 路径 A
} else {
    // 路径 B → GPU 执行两路
}

// ✅ 好:让工作项走同一路径
// 数据预处理,将偶数/奇数分到不同工作组

12.8.3 除法与取模优化

// ❌ 差:运行时除法
int row = gid / width;

// ✅ 好:如果 width 是 2 的幂,使用位运算
int row = gid >> log2_width;
int col = gid & (width - 1);

12.9 完整示例:矩阵乘法

// matrix_mul.cl - 利用局部内存的矩阵乘法
#define TILE_SIZE 16

__kernel void matrix_mul(
    __global const float *A,
    __global const float *B,
    __global float *C,
    const int M,    // A 的行数
    const int N,    // B 的列数
    const int K     // A 的列数 = B 的行数
) {
    int row = get_global_id(0);
    int col = get_global_id(1);

    int lid_row = get_local_id(0);
    int lid_col = get_local_id(1);

    __local float tileA[TILE_SIZE][TILE_SIZE];
    __local float tileB[TILE_SIZE][TILE_SIZE];

    float sum = 0.0f;

    // 分块计算
    for (int t = 0; t < K; t += TILE_SIZE) {
        // 协作加载到局部内存
        tileA[lid_row][lid_col] = A[row * K + t + lid_col];
        tileB[lid_row][lid_col] = B[(t + lid_row) * N + col];
        barrier(CLK_LOCAL_MEM_FENCE);

        // 计算当前块的部分积
        for (int k = 0; k < TILE_SIZE; k++) {
            sum += tileA[lid_row][k] * tileB[k][lid_col];
        }
        barrier(CLK_LOCAL_MEM_FENCE);
    }

    if (row < M && col < N) {
        C[row * N + col] = sum;
    }
}
矩阵乘法分块:
A (M×K)           B (K×N)            C (M×N)
┌─────┬─────┐    ┌─────┬─────┐     ┌─────┬─────┐
│ tile│ tile│    │ tile│ tile│     │     │     │
│  A  │  A  │ ×  │  B  │  B  │  =  │  C  │  C  │
├─────┼─────┤    ├─────┼─────┤     ├─────┼─────┤
│ tile│ tile│    │ tile│ tile│     │     │     │
│  A  │  A  │    │  B  │  B  │     │  C  │  C  │
└─────┴─────┘    └─────┴─────┘     └─────┴─────┘

12.10 注意事项

⚠️ barrier 的正确使用barrier(CLK_LOCAL_MEM_FENCE) 必须在所有工作项中统一执行。不能放在条件分支中(除非所有分支都到达 barrier)。

⚠️ 局部内存大小限制:通常 32-48 KB。超出会导致内核启动失败。通过 clGetDeviceInfo 查询 CL_DEVICE_LOCAL_MEM_SIZE

⚠️ bank conflict:当多个线程访问同一内存 bank 的不同地址时,会发生 bank conflict。可以通过填充(padding)避免。

⚠️ 内核参数大小限制:单个内核参数的大小有限制(通常几 KB)。大量数据必须通过缓冲区传递。


12.11 业务场景

场景 1:图像卷积

将图像分块加载到局部内存,每个工作组处理一个图块,利用局部内存的高带宽加速卷积计算。

场景 2:直方图计算

使用局部内存累积工作组内的直方图,最后原子操作合并到全局直方图。

场景 3:稀疏矩阵求解器

SpMV(稀疏矩阵向量乘法)中,利用局部内存缓存共享的向量片段。


12.12 扩展阅读

资源说明
OpenCL Best Practices GuideOpenCL C 规范
AMD OpenCL OptimizationAMD GPU 优化指南
NVIDIA OpenCL Best PracticesNVIDIA 优化建议

本章小结

  • 内核参数通过 clSetKernelArg 设置,内存对象传递地址,标量传递值
  • 内存层次:寄存器 → 局部 → 常量 → 全局,速度递减
  • 全局内存优化关键:合并访问、避免 bank conflict、使用向量类型
  • 局部内存用于工作组内数据共享和复用,带宽远高于全局内存
  • 常量内存适合所有线程读取相同数据的场景
  • 并行规约是利用局部内存的经典模式

上一章第 11 章:OpenCL 基础 下一章第 13 章:OpenCL 进阶