七月初,超频爱好者们又给老黄带来了新的惊喜:RTX 5090 的若干非公开的温度传感器的 PCIe BAR 地址被逆向了出来: [1][2][3]。这些传感器包含了四个热点(hotspot)CH0 到 CH3,以及每一片 GDDR7 显存模块的温度(根据型号不同,可能有 8 或者 16 个传感器)。这些热点传感器比公开 API(比如 nvidia-smi 或者 NVML)能读到的信息更丰富,更接近芯片的实际温度(比如结温),刷新率也更高,自然能给超频玩家更多的玩法。但我并不超频,它还有什么用呢?有的,这两天,在 GPT-5.6 Sol 的帮助下,我还是做了一些有趣的探究。
注:以下大部分实验是 AI 做的,结果数据、图表由 AI 收集生成,我已尽力进行审阅。
背景:传感器信息
5090 的 PCIe BAR 中,如下的地址有六个核心温度寄存器:
0xAD0A90..0xAD0A9C:四个独立通道 CH0-CH30xAD0AA0:硬件聚合最大值(Hotspot)0xAD0AA4:硬件聚合平均值
它们都使用低 16 位,以 1/256 摄氏度为单位的定点数表示温度。此外,还有 8 或者 16 个寄存器保存了当前的显存颗粒温度,格式和地址范围都比较复杂。这些寄存器具体的读取逻辑都可以参见 gpuwatch 项目。此外还需注意,还需要打开 iomem=relaxed 内核命令行选项,才能从用户态映射 PCIe BAR 空间。
实验:负载与温度变化
一些简单的 CUDA 负载测试结果:
| 负载 | 功耗 W | Hotspot C | 硬件平均 C | CH0 | CH1 | CH2 | CH3 | 显存最高 |
|---|---|---|---|---|---|---|---|---|
| 空闲基线 | 22 | 26.4 | 24.1 | 26.3 | 22.1 | 23.4 | 26.4 | 32.0 |
| FP32,1 block | 102 | 37.9 | 33.2 | 37.9 | 30.9 | 29.4 | 33.2 | 33.7 |
| FP32,22 blocks | 137 | 57.2 | 37.6 | 57.2 | 33.6 | 33.1 | 36.2 | 35.0 |
| FP32,44 blocks | 188 | 71.3 | 42.8 | 71.3 | 36.3 | 36.2 | 40.6 | 37.0 |
| FP32,88 blocks | 284 | 84.0 | 54.4 | 84.0 | 44.4 | 40.7 | 55.7 | 40.1 |
| FP32,170 blocks | 421 | 104.2 | 61.9 | 104.2 | 48.6 | 42.7 | 60.6 | 44.2 |
| FP32,680 blocks | 414 | 104.2 | 61.9 | 104.2 | 48.6 | 42.9 | 60.5 | 44.8 |
| INT32,全负载 | 414 | 104.0 | 61.9 | 104.0 | 48.4 | 42.7 | 60.1 | 44.8 |
| L2,32 MiB | 328 | 71.4 | 55.9 | 71.4 | 45.6 | 42.2 | 54.8 | 43.8 |
| GDDR7,2 GiB | 319 | 65.1 | 51.7 | 65.1 | 44.4 | 40.8 | 49.3 | 51.5 |
| Tensor Core | 449 | 93.0 | 62.9 | 93.0 | 51.0 | 45.3 | 59.5 | 51.5 |
简单观察这些 CH 传感器的变化,可以得到如下结果:
- Hotspot 是各通道最大值,而计算负载几乎总会使 CH0 成为最大值(远超平均温度,甚至超过 100 度)。
- 几乎总是有 CH0 > CH1 > CH3 > CH2。
- 读到的 “平均温度”并不是四个通道的简单平均,但是与功耗的相关性更高,可能是某种加权平均。
接着进行了一些脉冲测试,也就是启动较短时间的 kernel,观察温度变化,结果如下:
| 脉冲长度 | Hotspot 增量 C | 硬件平均增量 C | Hotspot 峰值时间中位数 |
|---|---|---|---|
| 50 ms | 13.9 | 8.6 | 27 ms |
| 100 ms | 25.5 | 13.5 | 90 ms |
| 200 ms | 36.0 | 18.3 | 197 ms |
| 500 ms | 50.0 | 24.7 | 508 ms |
| 1 s | 59.8 | 29.5 | 981 ms |
| 2 s | 64.9 | 30.4 | 1.994 s |
| 5 s | 75.7 | 33.8 | 4.916 s |
| 10 s | 74.6 | 31.2 | 5.032 s |
看起来 Hotspot 的测量非常灵敏。在 kernel 结束后,其下降也非常快。通过更多的测试,可以确定这些传感器的更新频率在 50Hz 左右。
接下来,在单个固定的 SM 上,测试了 32-1024 个线程、1-6 个 256 线程 block,以及 32x8、64x4、128x2 等 warp 总数相同的配置。单 warp 场景消耗 99.6 W;其他所有场景约为 100.6-102.5 W。除第一个、温度最低的冷态实验以外,每种配置扣除基线后的 CH0 增量都是 8.89-9.04 C。其他通道同样几乎不变。也就是说,空闲功耗以上约 80 W 的大部分增量都来自使整个设备进入高性能时钟/功耗状态。只改变 170 个 SM 中一个 SM 的工作量,影响太小,几乎无法通过温度传感器区分。
注:此处的“单个 SM”并不是说我们能控制 CUDA 程序运行在某个物理 SM 上。事实上,我们总是可以占满 GPU,并通过读到的 SM ID 决定来哪些 SM 实际运行负载,让剩下的 SM 处于空闲状态。下面的实验均使用了类似的方法:
smid = read_special_register("%smid");
if (!active_sms[smid])
return;
selected = atomicAdd(&per_sm_counter[smid], 1) < 4;
if (!selected)
return;
while (globaltimer < deadline) {
// 大量寄存器内 FP32 FMA
}
也就是说,最先抵达这个 SM 的四个 block(每个 256 线程,已经能足够打满 SM 的功耗)会一直执行,直到全局计时器到达截止时间。
实验:温度与降频
可以确定的是,CH0 的温度参与了 GPU 对温度降频(thermal throttling)的决策。在一个持续 90 s、使用 170 个 block 的 FP32 负载中,第一次报告硬件温度降频事件是在 kernel 启动 6.50 s 后可观察到:
- 隐藏 hotspot:99.94 C
- 硬件平均值:59.69 C
- 驱动 GPU 温度:59 C
- 板卡功耗:446 W
- 图形时钟:2775 MHz
此时驱动温度还比较低,只有 60 度,但 GPU 已经开始报告温度降频事件发生。随后过一段时间,hotspot 保持在约 104.5 C,而负载的三个三等分区间变化如下:
| 区间 | Hotspot 平均 C | 硬件平均 C | 功耗 W | 图形时钟 MHz |
|---|---|---|---|---|
| 前 30 s | 103.3 | 61.2 | 421.6 | 2719 |
| 中间 30 s | 104.6 | 63.0 | 399.0 | 2597 |
| 后 30 s | 104.6 | 63.5 | 391.6 | 2570 |
可以推测,当 hotspot 超过 100 度后,就可能触发降频保护机制。此时 GPU 整体(或者说外界可见)温度和功耗都远远没有达到极限。
我还尝试了基于 hotspot 温度的简单计算控制,也就是在温度即将到达阈值时暂停计算,来试图避免降频,获得更高的计算性能。然而由于同步和调度等开销,这种“占空比”的控制方法并没有获得太好的效果。
实验:温度与 SM 映射
由于单活动 SM 的实验没有什么效果(背景温升盖过了单个 SM),我选择用一组 42 个 SM 来测试(这个数字是随便选的,大致是 170 个 SM / 4 个传感器),每次只在一组上施加负载。除了连续的四组 SM 之外,还有一个实验,是每隔四个 SM 选一个,同样有 42 个。结果如下:
| SM ID 集合 | 稳态功耗 W | CH0 温升 C | CH1 温升 C | CH2 温升 C | CH3 温升 C | 平均温升 C |
|---|---|---|---|---|---|---|
| q0,0-41 | 188.24 | 50.80 | 13.75 | 11.76 | 12.88 | 18.98 |
| q1,42-83 | 191.15 | 33.95 | 12.23 | 9.38 | 19.86 | 19.72 |
| q2,84-125 | 191.34 | 44.83 | 12.96 | 9.57 | 14.21 | 18.86 |
| q3,126-167 | 189.45 | 29.76 | 11.15 | 8.94 | 23.19 | 19.59 |
| 条纹,0,4,…,164 | 189.92 | 36.60 | 13.40 | 9.23 | 18.41 | 18.24 |
很明显,不同的活跃 SM 集合对不同的传感器影响不同。因此我猜测这些传感器在物理上的分布并不一致。
实验:温度传感器对少量 SM 负载的响应
我让 Codex 用不同的组大小进行了充分的扫描,发现在组大小为 1 或者 2 的时候,对传感器的影响最为显著。
需要注意的是,这张图里的线代表了“传感器的温度变化减去基线温度变化”的值,也就是说,纵轴的零点代表了某个传感器在负载开始前的温度均值。从图中可以看到,CH0 对某几个 SM 的响应非常明显,CH3 也有一些点比较明显,并且这些点的编号似乎有某种周期性。这些信号都增强了我的猜测,即这些温升的变化,和传感器与 SM 的物理距离有关系。
然而,想要直接弄明白传感器的位置,还需要先知道 SM 的物理位置,这还需要更多的实验。
实验:SM ID 到 GPC/TPC 的映射
根据如下图中 5090 的 die shot,它由 12 个 GPC 组成,每个 GPC 上有 8 个 TPC,每个 TPC 上又有两个 SM。虽然总共有 192 个 SM,但由于良率或者其他原因,实际只启用了 170 个。根据分析,其中有 3 个 GPC 只启用了 7 个 TPC,有 8 个 GPC 启用了全部的 8 个 TPC,还有一个 GPC 完全被禁用。
因此,第一步是把 SM ID 映射到 GPC 中,才能对应到 die shot。这一步并用不到温度传感器。
CUDA 编程模型保证,thread-block cluster 中所有 block 会同时调度在同一个 GPC。因此只要让 cluster 内每个 block 读取 %smid,就可以把这些对应的 SM ID 归类到同一个 GPC 中。经过充分的数据获取和处理处理(一些简单的图上连通分量分析),可以获得 11 个逻辑 GPC 对应的 SM ID 集合:
| 逻辑 GPC | TPC 数 | SM 数 | 每个 TPC 的 SM ID 对 |
|---|---|---|---|
| 0 | 7 | 14 | 0/1, 22/23, 44/45, 66/67, 88/89, 110/111, 132/133 |
| 1 | 7 | 14 | 2/3, 24/25, 46/47, 68/69, 90/91, 112/113, 134/135 |
| 2 | 7 | 14 | 4/5, 26/27, 48/49, 70/71, 92/93, 114/115, 136/137 |
| 3 | 8 | 16 | 6/7, 28/29, 50/51, 72/73, 94/95, 116/117, 138/139, 154/155 |
| 4 | 8 | 16 | 8/9, 30/31, 52/53, 74/75, 96/97, 118/119, 140/141, 156/157 |
| 5 | 8 | 16 | 10/11, 32/33, 54/55, 76/77, 98/99, 120/121, 142/143, 158/159 |
| 6 | 8 | 16 | 12/13, 34/35, 56/57, 78/79, 100/101, 122/123, 144/145, 160/161 |
| 7 | 8 | 16 | 14/15, 36/37, 58/59, 80/81, 102/103, 124/125, 146/147, 162/163 |
| 8 | 8 | 16 | 16/17, 38/39, 60/61, 82/83, 104/105, 126/127, 148/149, 164/165 |
| 9 | 8 | 16 | 18/19, 40/41, 62/63, 84/85, 106/107, 128/129, 150/151, 166/167 |
| 10 | 8 | 16 | 20/21, 42/43, 64/65, 86/87, 108/109, 130/131, 152/153, 168/169 |
之所以称为“逻辑 GPC”,是因为我们依旧不知道它的物理位置,只是赋予了一个唯一编号(按照 SM ID 排列)。这与上面的 die shot、以及温度传感器对 SM 的响应分析能互相验证:编号相邻的两个 SM 对温度的影响几乎是相同的,因此应当确实属于同一个 TPC。
我们将每个 TPC 中的两个 SM 视作一个整体,用上述的逻辑 GPC、TPC 编号,绘制所有温度传感器对不同 SM 的响应图示:
显然,从这张图中,我们已经能获得很多有用的信息,甚至已经可以进行一些基于人工智能(指人脑)的重新排布。但为了让结论更有说服力,我们还需要进行一些定量的分析。
分析:GPC/TPC/SM ID 到物理位置的映射
现在就剩下了最后的两个问题:
- 8 个 TPC 在 GPC 中的物理排布是怎么样的?
- 12 个 GPC 在整个 die 上的物理排布是怎么样的?
为了解决这个问题,我们可以先进行一些符合直觉的假设来缩小搜索空间:
- GPC 的编号应当在物理上连续;不同的 GPC 中,TPC 的排布方式应当相同;
- TPC 在 GPC 中的排布应该使得 SM 编号遵循一定的顺序:如从左到右、从上到下递增,相邻两个编号连续或者有固定的 stride。注:规定一种排列方向会消除很多种可能的镜像排布,我们将它们视作相同的方案。
- 由物理定律可知:离热源越远的地方,温升越小,并且会形成连续的温度梯度。
我们不妨假设温度传感器就位于响应最高的 SM 附近(ID=96,GPC=4,TPC=4)。接着,尝试枚举所有遵循上述编号方式约束的可能 TPC、GPC 排布方式,并对以下的指标进行优化:
- 每个热点附近的温度梯度,也就是温升与物理距离(曼哈顿距离,单位为 SM 单位格)的相关性(Spearman 相关系数)
- 热点附近其他 SM 诱发的 1 秒内的温升比例(温度升高值与稳态温度的比例)与物理距离的相关性。
最终,可以获得如下的“镜像列主序”的排列方案,满足所有约束,并且在上述指标上表现良好:
也就是说,12 个 GPC 很可能是从左到右、从上到下依次编号。每个 GPC 中的八个 TPC 编号方式可能是:
TPC4 TPC0
TPC5 TPC1
TPC6 TPC2
TPC7 TPC3
如上所述,还有几种全局镜像的排布方式,我们视同等价。从这张按位置映射重绘的热力图上,我们能获得以下的观察:
- CH0、CH2、CH3 都有比较直观的温度梯度,因此我们的排布应当是基本合理的。
- 图上的几个标记是我推测的传感器可能的位置(CH1 无法判断)。然而这几个传感器的响应绝对幅度并不相同,因此一定还有其他的差异(比如垂直距离区别)。
- 每个传感器对 SM0/1 对应的 TPC 都有明显高于其所在位置的响应,这意味着 SM0/1 对于这四个传感器都有更高的升温作用。目前我对此尚没有合理的解释。
事实上,还有一种更符合温度梯度的方案,是 [4,0] [5,1] [6,2] [3,7];但是这样的编号实在太扭曲,再考虑到温度测量可能带有不确定的噪音,我最终舍弃了这种方案。
总结
在 GPT-5.6 的帮助下,我(基本)成功从高精度的温度传感器读数中,推测出了 5090 的一种可能的物理 SM 排布方式。尽管这些信息或许可以从其他渠道简单地获得,也很可能还有错误,对日常使用也没有太多的帮助,但依旧是个非常有趣的过程。
附录
实验过程中还进行了一些可能有用的测试,摘录如下。
| 负载 | 稳态功耗 W | Hotspot 峰值 C | 硬件温度事件占比 |
|---|---|---|---|
| FP32 FMA | 452 | 107.6 | 1.7% |
| INT32 MAD | 431 | 109.2 | 31.1% |
| FP64 FMA | 167 | 55.8 | 0% |
| SFU sine | 323 | 90.2 | 0% |
| 共享内存 | 363 | 97.4 | 0% |
| 全局原子操作 | 162 | 51.5 | 0% |
| L2 读写 | 324 | 71.5 | 0% |
| GDDR7 读写 | 318 | 65.4 | 0% |
| FP16 Tensor GEMM | 446 | 94.0 | 0% |