Skip to content

Hopper DSMEM 深度解析与实测:比起写 HBM,跨 SM 片上互联究竟香在哪?


概述与核心命题:为什么过去不得不“写 HBM”?

Section titled “概述与核心命题:为什么过去不得不“写 HBM”?”

在 NVIDIA Hopper(SM90)引入 Thread Block Cluster(线程块集群) 之前,CUDA 编程模型存在一个延续了十几年的物理铁律:

传统 GPU 的 SMEM 是彼此隔绝的孤岛:
每个 Streaming Multiprocessor(SM)的共享内存(SMEM)只对本 Thread Block(CTA)可见。

当多个 Block 需要交换中间结果(例如:跨 Block 的局部 Softmax 统计量合并、矩阵转置重排、生产者-消费者流水线传递)时,开发者没有任何片上直连通道,唯一合法的通路只有全局内存(Global Memory):

Pre-Hopper 时代的跨 Block 通信代价:
CTA 0 ──[ st.global ]──> GMEM (L2/HBM) ──[ __threadfence / 切分 Kernel ]──> CTA 1 ──[ ld.global ]──> CTA 1 SMEM

这种模式在微架构层面带来了不可承受之重:

  1. 往返时延极高:即使全部命中 L2 Cache,也要经历完整的访存流水线、写缓冲排队与缓存一致性握手(典型往返高达 300~600 周期);
  2. L2 缓存读写剧烈震荡(Cache Pollution):中间状态生命周期极短(用完即弃),却在 L2 Cache 和 Crossbar 之间反复倒腾,强行挤占了真正需要缓存的模型权重(Weights)和 KV Cache;
  3. 被迫切分 Kernel(Grid Barrier):由于 GPU 缺乏轻量级的全网格硬件同步,为了保证数据写入后被另一批 Block 安全读出,往往不得不退出当前 Kernel,通过 Host 端重新 Launch 下一个 Kernel,造成毁灭性的调度开销。

DSMEM 的物理本质:把 SM-to-SM 连成高速交换网

Section titled “DSMEM 的物理本质:把 SM-to-SM 连成高速交换网”

Hopper 架构在物理硅片层面,通过 Cluster Crossbar(片上 SM 间互联网络) 将同一个 GPC(Graphics Processing Cluster)内的 SM 打通。

Hopper 存储金字塔的真实物理阶梯:
┌────────────────────────────────────────────────────────────┐
│ Local SMEM ~20 - 30 cycles (单 SM 极速) │
├────────────────────────────────────────────────────────────┤
│ Remote DSMEM ~100 - 180 cycles (跨 SM 片上直连,零显存) │ <-- 远快于 L2!
├────────────────────────────────────────────────────────────┤
│ L2 Cache ~200 - 300 cycles (片上共享缓存) │
├────────────────────────────────────────────────────────────┤
│ HBM3 / DRAM ~450 - 600 cycles (片外高延迟高能耗) │
└────────────────────────────────────────────────────────────┘

分布式共享内存(DSMEM, Distributed Shared Memory) 并不改变 SMEM 的物理位置,而是提供了统一的 Cluster 虚拟寻址空间:

  • CTA 0 可以直接通过普通指针,读取位于 CTA 1 物理 SM 上的共享内存;
  • 底层由片上网络在硬件层面完成跨 SM 路由,完全绕开 L2 Cache 控制器与 HBM 物理显存。

严谨控制变量:H100 实测方案设计

Section titled “严谨控制变量:H100 实测方案设计”

为了精准测量“DSMEM 片上直连”对比“写 HBM 往返”的纯硬件优势,我们设计了一套完全对齐 Cluster 同步开销(保持相同 cluster.sync())、仅以数据传输介质为单一自变量的基准测试:

【测试 1】往返时延对比(Ping-Pong Token Passing):
CTA 0 发送一个 Token 给 CTA 1,CTA 1 收到后递增并传回给 CTA 0。连续执行 1000 轮往返(2000 次 Hops)。
- 路径 A (DSMEM):直接写入对方 SMEM 邮箱 -> cluster.sync()。
- 路径 B (GMEM) :写入全局显存槽位 -> __threadfence() -> cluster.sync() -> 读取全局显存槽位。
【测试 2】持续大块 Tile 交换吞吐(16 KB Tile Exchange):
每个 CTA 独占 16 KB 局部 Tile(4096 个 FP32)。两个 CTA 相互双向交换数据,连续迭代 5000 次。
- 路径 A (DSMEM):全线程并发利用 cluster.map_shared_rank 从对方 SMEM 拉取数据。
- 路径 B (GMEM) :每个 CTA 将 16 KB 写入全局内存缓冲区,刷 fence 后从对端缓冲区读出。

测试代码已集成至仓库 benchmarks/dsmem_vs_hbm_bench.cu。


实测数据公布(NVIDIA H100 PCIe 真实硬件)

Section titled “实测数据公布(NVIDIA H100 PCIe 真实硬件)”

在真实测试集群(NVIDIA H100 PCIe, 114 SMs, 80GB HBM3, CUDA 13.2, Driver 595.58.03)上,满载 57 个 Cluster(114 个 CTA 并发)实测数据如下:

1. 往返通信时延(Token Passing Latency)

Section titled “1. 往返通信时延(Token Passing Latency)”
-------------------------------------------------------------------
1. Token Passing Round-Trip Latency Test (1000 round-trips / 2000 hops)
-------------------------------------------------------------------
DSMEM 单跳平均时延 : ~736.1 cycles
HBM / GMEM 单跳平均时延 : ~1419.2 cycles
时延加速比 (Latency Speedup) : 1.93x 提速(时延近乎减半!)
  • 原理解析:走全局显存路径时,必须经过 st.global 写入显存并配合 __threadfence(),强制冲刷 GPU 的内部写缓冲(Write Buffer),阻塞了后续指令发射;而 DSMEM 在片上跨片总线内直接交付,流水线无额外停顿。

2. 持续 16 KB 大块 Tile 交换吞吐(5000 次迭代)

Section titled “2. 持续 16 KB 大块 Tile 交换吞吐(5000 次迭代)”
-------------------------------------------------------------------
2. Sustained 16 KB Bulk Tile Exchange (5000 iterations)
-------------------------------------------------------------------
DSMEM 总交换耗时 : 30.871 ms
HBM / GMEM 总交换耗时 : 46.678 ms
交换吞吐加速比 (Speedup) : 1.51x 提速!
有效交换带宽 (DSMEM) : 0.61 TB/s (纯片上 Crossbar 直连)
有效交换带宽 (HBM) : 0.40 TB/s (受制于 L2 写屏障与全局访存往返)

3. NCU(Nsight Compute)物理计数器铁证

Section titled “3. NCU(Nsight Compute)物理计数器铁证”

使用 Nsight Compute 抓取实际执行期间的底层硬件计数器:

Terminal window
sudo ncu --metrics lts__t_bytes.sum,dram__bytes_write.sum \
--kernel-name regex:.*tile_exchange.* ./dsmem_vs_hbm_bench
硬件计数器指标传统方式(写 HBM / GMEM 交换)Hopper DSMEM(片上网络直连)收益幅度
lts__t_bytes.sum(L2 Cache 读写总流量)752.43 Mbyte139.65 MbyteL2 流量暴降 5.4 倍(降低 81.4%!)
dram__bytes_write.sum(物理 HBM 显存写入量)持续显存写回0 Byte(绝对零写入!)100% 消除片外物理显存写开销

深刻的微架构启示:
哪怕数据全部命中 L2 缓存、没有溢出到外部 HBM,传统写 GMEM 依然会在片上 L2 交叉开关上产生高达 752 MB 的拥挤数据流;
而 DSMEM 将这一过程全部限制在 SM 互联内部完成,不仅物理显存写入为 0,L2 读写总量也直接被砍掉 81.4%!


关键避坑指南:SMEM 生命周期陷阱(The Lifecycle Trap)

Section titled “关键避坑指南:SMEM 生命周期陷阱(The Lifecycle Trap)”

我们在 H100 上调试这套基准测试时,复盘出了一个极易导致线上生产环境偶发崩溃的典型 Bug,极具工程参考价值:

错误示范:对端 CTA 提前退出引发的 MMU Trap

Section titled “错误示范:对端 CTA 提前退出引发的 MMU Trap”
// 假设 CTA 0 需要读取 CTA 1 的数据并做汇总
if (rank == 0) {
const float* peer_smem = cluster.map_shared_rank(local_accum, 1);
for (int i = threadIdx.x; i < ARRAY_LEN; i += blockDim.x) {
local_accum[i] += peer_smem[i]; // 读取 CTA 1 的 SMEM
}
d_out[blockIdx.x] = local_accum[threadIdx.x];
}
// 致命错误:CTA 1 (rank == 1) 没有进入 if,直接退出了 Kernel!
  • 故障现象:执行触发 cudaErrorLaunchFailure (error 719) -> unspecified launch failure。
  • 物理根因:CTA 的 SMEM 物理生命周期与该 CTA 的执行周期严格绑定! 当 CTA 1 执行完毕并退出后,硬件立即回收并失效了 CTA 1 所在的 SMEM 分区。此时 CTA 0 依然试图跨 SM 读取已经被注销的物理地址,直接触发 MMU 异常!
  • 铁律修复:在 Kernel 退出之前,必须无条件调用 cluster.sync()! 确保 Cluster 内部所有的远程读取全部收口,方可允许任何 CTA 终止。
if (rank == 0) {
const float* peer_smem = cluster.map_shared_rank(local_accum, 1);
for (int i = threadIdx.x; i < ARRAY_LEN; i += blockDim.x) {
local_accum[i] += peer_smem[i];
}
d_out[blockIdx.x] = local_accum[threadIdx.x];
}
// 必须增加 Cluster 级栅障,防止 CTA 1 先行析构
cluster.sync();

以下为本次测试编译使用的完整源码(也可在本地仓库 benchmarks/dsmem_vs_hbm_bench.cu 获取):

#include <cuda.h>
#include <cuda_runtime.h>
#include <cooperative_groups.h>
#include <iostream>
#include <vector>
#include <iomanip>
namespace cg = cooperative_groups;
#define CUDA_CHECK(expr) \
do { \
cudaError_t err = (expr); \
if (err != cudaSuccess) { \
std::cerr << "CUDA Error: " << cudaGetErrorString(err) << std::endl;\
std::exit(EXIT_FAILURE); \
} \
} while (0)
constexpr int CLUSTER_SIZE = 2;
constexpr int THREADS_PER_BLOCK = 128;
constexpr int TILE_FLOATS = 4096; // 16 KB
__device__ __forceinline__ uint64_t read_clock64() {
uint64_t clk;
asm volatile("mov.u64 %0, %clock64;" : "=l"(clk));
return clk;
}
// 1. DSMEM 持续交换 Kernel
__global__ void __attribute__((cluster_dims(CLUSTER_SIZE, 1, 1)))
dsmem_tile_exchange_kernel(int num_iters, float* out_verify) {
auto cluster = cg::this_cluster();
uint32_t rank = cluster.block_rank();
uint32_t peer_rank = rank ^ 1;
__shared__ float smem_src[TILE_FLOATS];
__shared__ float smem_dst[TILE_FLOATS];
for (int i = threadIdx.x; i < TILE_FLOATS; i += blockDim.x) {
smem_src[i] = float(rank * 1000 + i);
}
cluster.sync();
const float* peer_src = cluster.map_shared_rank(smem_src, peer_rank);
cluster.sync();
for (int it = 0; it < num_iters; ++it) {
for (int i = threadIdx.x; i < TILE_FLOATS; i += blockDim.x) {
smem_dst[i] = peer_src[i];
}
cluster.sync();
}
if (threadIdx.x == 0 && out_verify != nullptr) {
out_verify[blockIdx.x] = smem_dst[0];
}
}
// 2. HBM / GMEM 持续交换 Kernel
__global__ void __attribute__((cluster_dims(CLUSTER_SIZE, 1, 1)))
hbm_tile_exchange_kernel(int num_iters, float* gmem_scratchpad, float* out_verify) {
auto cluster = cg::this_cluster();
uint32_t rank = cluster.block_rank();
int peer_block_idx = blockIdx.x ^ 1;
__shared__ float smem_src[TILE_FLOATS];
__shared__ float smem_dst[TILE_FLOATS];
float* my_gmem_scratch = &gmem_scratchpad[blockIdx.x * TILE_FLOATS];
float* peer_gmem_scratch = &gmem_scratchpad[peer_block_idx * TILE_FLOATS];
for (int i = threadIdx.x; i < TILE_FLOATS; i += blockDim.x) {
smem_src[i] = float(rank * 1000 + i);
}
cluster.sync();
for (int it = 0; it < num_iters; ++it) {
for (int i = threadIdx.x; i < TILE_FLOATS; i += blockDim.x) {
my_gmem_scratch[i] = smem_src[i];
}
__threadfence();
cluster.sync();
for (int i = threadIdx.x; i < TILE_FLOATS; i += blockDim.x) {
smem_dst[i] = peer_gmem_scratch[i];
}
cluster.sync();
}
if (threadIdx.x == 0 && out_verify != nullptr) {
out_verify[blockIdx.x] = smem_dst[0];
}
}

终极定位:何时写 HBM?何时用 DSMEM?

Section titled “终极定位:何时写 HBM?何时用 DSMEM?”

在 Hopper 及未来的 Blackwell 算子架构设计中,划分边界非常清晰:

  1. 跨越 Cluster 物理边界(如无法归入同一 GPC、或跨 GPU 通信):
    必须走 Global Memory(L2/HBM)或 NVLink/NCCL。
  2. Cluster 内部多 CTA 协同(2/4/8 个 Block 之间):
    • 进片上(Ingress):用 TMA Multicast,从 GMEM 单次读取广播进各自的 Local SMEM;
    • 在片上(In-flight):任何跨 Block 中间结果的交互、转置、乒乓流转与归约,坚决使用 DSMEM! 严禁任何中间变量落盘到全局显存。