【infra之路】03--每个线程分配多少寄存器是如何决定的

寄存器数量:编译器决定的,不是你自己分配的

你写代码时只声明变量,编译器自动决定每个线程用多少个寄存器。比如你写:

cuda 复制代码
float sum = 0.0f;          // 编译器分配 1 个寄存器
float regA[4];             // 编译器分配 4 个寄存器
float threadResults[4][4]; // 编译器分配 16 个寄存器

你在代码里写多少局部变量,编译器就分配多少寄存器(假设放得下)。你不能cudaMalloc 那样手动申请一块寄存器。

但你可以影响编译器的决策,有三种方式:

方式一:__launch_bounds__(最常用)

cuda 复制代码
// 告诉编译器:每个 block 最多 256 线程,每个 SM 至少跑 2 个 block
__global__ __launch_bounds__(256, 2)
void myKernel(...) { ... }

这会引导编译器优化寄存器分配------minBlocksPerMultiprocessor=2 意味着编译器知道每 SM 至少要容纳 2 个 block,会控制每线程的寄存器数量别太多,以保留 occupancy。

方式二:编译选项 -maxrregcount

bash 复制代码
nvcc -O3 -maxrregcount=64 my_kernel.cu

强制限制每线程最多用 64 个寄存器。超出的变量会被溢出到 Local Memory(慢!)。

方式三:什么都不写(默认行为)

编译器自己权衡寄存器使用量和 occupancy,通常效果不错。

关键约束

不管编译器怎么分配,都有硬上限:

约束
每线程最多寄存器数 255 个(几乎所有架构)
每 SM 总寄存器数 A100: 65536,H100: 65536
寄存器分配粒度 2 的倍数或 4 的倍数(不能分配奇数个)

如果变量太多超过 255 个寄存器,多余的会被 spill 到 Local Memory------名字叫 Local,实际是 Global Memory 的一部分,非常慢(~300 cycles)。这是要避免的。

每个寄存器存多少?

一个寄存器是 32 bit,所以:

数据类型 占几个寄存器
float(32 bit) 1 个
int(32 bit) 1 个
half(16 bit) 1 个(浪费了一半,但最小单位就是 1 个寄存器)
double(64 bit) 2 个
float *(指针,64 bit) 2 个

所以 float regA[4] 就是 4 个寄存器,每个存 1 个 float。float threadResults[4][4] 就是 16 个寄存器。

寄存器不能存多个元素 ------即使 half 只有 16 bit,也独占一个 32 bit 寄存器。这也是为什么 AI Infra 领域很多量化工作(FP16→INT8)不仅省显存,在 kernel 层面也有寄存器效率的收益。

回到 Register Tiling 的上下文

我们的 kernel 里每线程用了多少寄存器?

cuda 复制代码
float threadResults[4][4];  // 16 个寄存器(累积结果)
float regA[4];              //  4 个寄存器(A 的一行)
float regB[4];              //  4 个寄存器(B 的一列)
float sum (循环变量等);       // ~4-6 个寄存器(索引、临时值)
// 合计约 28-30 个寄存器

30 个寄存器,远低于 255 的上限。对于 RTX 5060 Ti(假设每 SM 65536 个寄存器),每线程 30 个寄存器 → 每 SM 可以容纳 65536/30 ≈ 2184 个线程。而 SM 最大线程数是 2048,所以寄存器不是瓶颈------瓶颈在别的地方(比如 Shared Memory 容量)。

这就是为什么理解寄存器分配很重要:它决定了你的 SM 能同时跑多少线程(occupancy),进而影响 GPU 隐藏内存延迟的能力。