内存优化
内存优化核心原则
- 最大化全局内存带宽利用率(合并访存、向量化)
- 最小化全局内存访问次数(共享内存缓存、寄存器重用)
- 避免共享内存 Bank Conflict(padding、swizzle 地址映射)
全局内存合并访问
合并访存原理
在 GPU 编程中,合并访存是指线程束中的线程按照特定规则访问全局内存时,硬件能够将这些分散的内存请求合并为更少的内存事务(transaction),从而显著提高内存访问效率。
MT GPU S5000 的合并机制:
- 一个线程束包含 32 个线程
- 每 4 个或 8 个线程为粒度,经过合并单元聚合
- 聚合为多个 128B 的请求(L2 缓存行大小)
- 内存子系统下游均以 128B 事务为单位处理
合并访存规则
完全合并访问(推荐):
// ✅ Good: 完全合并访问
// 线程 0,1,2,3... 访问地址 0,1,2,3...(连续对齐)
__global__ void coalescedAccess(float* data) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
data[idx] = data[idx] * 2.0f;
}
非合并访问(避免):
// ❌ Bad: 跨步访问,带宽利用率极低
// 线程 0,1,2,3... 访问地址 0,1024,2048,3072...
__global__ void stridedAccess(float* data, int stride) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
data[idx * stride] = data[idx * stride] * 2.0f;
}
// 带宽利用率:4B/128B = 1/32 ≈ 3%
访存模式对比
| 访问模式 | 描述 | 事务数 | 带宽利用率 |
|---|---|---|---|
| 连续 32 线程访问连续 32 地址 | 完全合并 | 1 个事务 | 100% |
| 连续 32 线程访问间隔 2 地址 | 2-way 跨步 | 2 个事务 | 50% |
| 连续 32 线程访问间隔 4 地址 | 4-way 跨步 | 4 个事务 | 25% |
| 随机访问 | 无规律 | 32 个事务 | ~3% |
向量化访存
为什么需要向量化?
即使满足合并访存,固定的访问数据量会产生固定数量的 128B 事务(transaction)。但向量化访存仍有以下优势:
- 减少访存指令数量:一条
float4指令替代 4 条float指令 - 降低 LSU(加载/存储单元):Load/Store Unit 处理能力有上限,指令过多会导致瓶颈
- 提高带宽利用率:MT GPU S5000 支持最大 1024bit 单指令访问
向量化访存示例
// ✅ 使用 float4 向量化加载(128bit)
__global__ void vectorLoad(float* in, float* out, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
// 将 float 指针转换为 float4 指针
float4* in_vec = (float4*)in;
float4* out_vec = (float4*)out;
// 一次加载 4 个 float
float4 v = in_vec[idx];
// 计算
v.x = v.x * 2.0f;
v.y = v.y * 2.0f;
v.z = v.z * 2.0f;
v.w = v.w * 2.0f;
// 一次存储 4 个 float
out_vec[idx] = v;
}
// 启动配置(线程数减少为 1/4)
int n = 4096;
int threads = n / 4; // 每个线程处理 4 个元素
VectorLoad<<<1, threads>>>(d_in, d_out, n);
向量化类型选择
| 数据类型 | 单指令加载 | 适用场景 |
|---|---|---|
float | 32-bit | 基础类型,灵活性高 |
float2 | 64-bit | 小型向量运算 |
float4 | 128-bit | 推荐,平衡性能与灵活性 |
int4 | 128-bit | INT8 量化场景 |
共享内存存储体冲突消除
存储体冲突(Bank Conflict)原理
共享内存分为 32 个存储体(bank),每个存储体宽度为 4 字节(32 位)。
存储体冲突(Bank Conflict):
- 同一线程束(Warp)中多个线程访问同一存储体的不同地址 → 序列化访问
- 同一线程束(Warp)中多个线程访问同一存储体的同一地址 → 广播(无冲突)
存储体冲突(Bank Conflict)示例
// ❌ Bad: 32 路存储体冲突
// 所有线程访问第 0 列 → 所有线程访问 bank 0
__shared__ float matrix[32][32];
__global__ void badAccess() {
int col = 0;
float value = matrix[threadIdx.x][col]; // 32 路冲突!
}
// 性能下降:串行为 32 次访问
// ✅ Good: 无冲突访问
// 每个线程访问不同 bank
__shared__ float matrix[32][32];
__global__ void goodAccess() {
int row = 0;
float value = matrix[row][threadIdx.x]; // 无冲突
}