1
运行时
DCU K100 节点初探:从硬件配置到性能调优全记录
2026-06-22 15:29:10
305
# DCU K100 节点初探:从硬件配置到性能调优全记录 > **摘要**:本文以海光 DCU K100 加速卡为目标平台,从开箱即用的硬件环境出发,系统梳理了 ROCm 软件栈、NUMA 拓扑与 PCIe 带宽分析、Hipprof 性能剖析工具以及 TensorCore 编程模型。文章结合实测数据,给出了带宽利用率分析和 Kernel 性能优化方向,旨在为首次接触 DCU 平台的开发者提供一份可操作的上手指南。 --- ## 1. 引言 随着国产异构计算平台的快速发展,海光(Hygon)推出的 DCU(Deep Computing Unit)系列加速卡在科学计算、分子动力学模拟等领域逐渐崭露头角。DCU 兼容 AMD ROCm(Radeon Open Compute platforM)开放式软件栈,开发者可以沿用 HIP(Heterogeneous-Compute Interface for Portability)编程模型,在较低的迁移成本下将现有 GPU 应用移植到国产平台。 本文将基于一台搭载 8 张 DCU K100 的服务器节点,逐层展开:从最基础的硬件信息采集,到 NUMA 拓扑与 PCIe 带宽的实测分析,再到 Hipprof 性能剖析工具的使用,最后探讨 TensorCore 的编程调用方式。希望这篇记录能帮助读者快速建立起对 DCU 平台的整体认知。 --- ## 2. 硬件环境概览 ### 2.1 CPU 与 NUMA 拓扑 登录节点后,首先使用 `lscpu` 命令查看 CPU 信息: ``` Architecture: x86_64 CPU(s): 64 Model name: Hygon C86 7381 32-core Processor Socket(s): 2 NUMA node(s): 8 ``` ![lscpu 输出结果](/api-forum/file/RedirectImage?path=20260622/1782112926863_1782110373418.png 'lscpu 输出结果') 节点搭载两颗 **Hygon C86 7381** 处理器,每颗 32 核,总计 64 物理核心。值得注意的是,系统被划分为 **8 个 NUMA 节点**——这一拓扑结构将直接影响后续 DCU 加速卡的亲和性绑定和跨 NUMA 通信性能。 > **关键认知**:在 NUMA 架构下,CPU 访问本地内存与远端内存的延迟差异显著。同理,DCU 与 CPU 之间的数据传输也应尽量保持在同一个 NUMA 节点内,避免跨节点 PCIe 桥接带来的性能损耗。 ### 2.2 DCU 硬件信息 使用 `rocminfo` 命令可以列举 ROCm 平台下所有可见的硬件设备: ``` Agent 8 (DCU K100): Name: gfx926 Max Threads Per Block: 1024 ... ``` ![rocminfo 输出结果](/api-forum/file/RedirectImage?path=20260622/1782112960460_1782110373483.png 'rocminfo 输出结果') 关键参数提炼如下: | 参数 | 值 | 影响 | | ----------------------- | --------------------- | ------------------------------------ | | **型号** | DCU K100 | — | | **架构代号** | gfx926 | 编译时需指定 `--offload-arch=gfx926` | | **单 Block 最大线程数** | 1024 | 决定 Kernel 的 work-group size 上限 | | **PCIe 接口** | PCIe 4.0 ×16, 16 GT/s | 决定主机与设备间的数据传输上限 | ### 2.3 ROCm 软件栈简介 ROCm 是 AMD 发起的开放式 GPU 计算平台,对标 NVIDIA CUDA,其核心组件包括: - **HIP**:异构计算可移植接口,提供与 CUDA 语法高度相似的 C++ API,支持 `hipify` 工具自动转换 CUDA 代码。 - **ROCk / ROCt**:内核驱动与运行时,负责设备管理、内存分配、Kernel 分发。 - **rocBLAS / rocFFT / rocSOLVER**:对标 cuBLAS / cuFFT / cuSOLVER 的基础数学库。 - **Hipprof**:DCU 平台的性能剖析工具(详见第 4 节)。 这些组件共同构成了一套完整的"编译器—运行时—数学库—调试工具"工具链。 --- ## 3. 带宽测试与 NUMA 拓扑分析 带宽是衡量异构计算平台数据传输能力的核心指标。本节通过 `rocm-bandwidth-test` 工具进行系统化的点对点带宽测试,并结合 NUMA 拓扑给出分析。 ### 3.1 测试环境与方法 测试节点中共有 **16 个设备**:编号 `0~7` 为 CPU 端(8 个 NUMA 节点的处理器侧),编号 `8~15` 为 8 张 DCU K100 加速卡。 使用如下命令进行测试: ```bash # 所有设备间点对点单向带宽 rocm_bandwidth_test -a # 所有设备间点对点双向带宽 rocm_bandwidth_test -A # 指定设备对之间的延迟测试 rocm_bandwidth_test -s -d -l ``` ### 3.2 单向带宽结果分析 首先,通过 DCU 之间的"NUMA 距离"图可以直观看出设备的物理分组关系: ![1782110373494.png](/api-forum/file/RedirectImage?path=20260622/1782112985054_1782110373494.png '1782110373494.png') 从上图可以看出 8~11 号为一组,12~15 号为另一组。下表展示了 8 张 DCU(设备 8~15)之间的单向点对点带宽(单位:GB/s): | 分组 | DCU 8~11(组内) | DCU 12~15(组内) | 跨组 | | ------------ | ---------------- | ----------------- | -------- | | **单向带宽** | ~28 GB/s | ~28 GB/s | ~11 GB/s | ![1782110373503.png](/api-forum/file/RedirectImage?path=20260622/1782113001232_1782110373503.png '1782110373503.png') 这一结果恰好与 `rocm-smi --showtopo` 揭示的 NUMA 归属一致: ``` DCU 0~3 (设备 8~11) → 从属 NUMA 3 DCU 4~7 (设备 12~15) → 从属 NUMA 7 ``` **结论**:同一 NUMA 组内的 DCU 间通信带宽约为 28 GB/s,跨组带宽骤降至 11 GB/s,降幅达 **60%**。 ### 3.3 双向带宽与理论峰值对比 双向带宽测试结果: | 分组 | 组内 | 跨组 | | ------------ | -------- | -------- | | **双向带宽** | ~55 GB/s | ~22 GB/s | ![1782110373513.png](/api-forum/file/RedirectImage?path=20260622/1782113016035_1782110373513.png '1782110373513.png') 进一步通过 `rocm-smi --showtopo` 查看拓扑结构: ![1782110373521.png](/api-forum/file/RedirectImage?path=20260622/1782113023596_1782110373521.png '1782110373521.png') 可以看到 0~3 号 DCU 从属于 NUMA 3,4~7 号 DCU 从属于 NUMA 7,所有 DCU 均通过 PCIe 连接。 再通过 `rocm-smi --detail` 确认 PCIe 规格: ``` PCIe Version: 4.0 PCIe Speed: 16 GT/s PCIe Width: ×16 ``` 参考 PCIe 4.0 ×16 的理论带宽(编码方式为 128b/130b): ![1782110373530.png](/api-forum/file/RedirectImage?path=20260622/1782113032490_1782110373530.png '理论带宽[3]') | 指标 | 理论峰值 | 实测值 | 利用率 | | ------------ | --------- | ------- | --------- | | **单向带宽** | 31.5 GB/s | 28 GB/s | **88.9%** | | **双向带宽** | 63.0 GB/s | 55 GB/s | **87.0%** | > 实测带宽达到理论峰值的 **87%–89%**,属于 PCIe 4.0 ×16 的正常水平。剩余开销主要来自 PCIe 协议层(TLP 头、DLLP、流控等)以及 ROCm 运行时的软件开销。 ### 3.4 实践建议 基于上述分析,在应用部署时应注意: 1. **进程绑定**:将 MPI 进程绑定到与目标 DCU 相同 NUMA 节点的 CPU 核心上,避免跨 NUMA 数据传输。 2. **数据传输聚合**:尽量减少跨组 DCU 之间的直接数据交换,优先在组内完成归约等集合通信操作。 3. **PCIe 带宽利用**:通过流水线(Pipelining)技术将数据传输与计算重叠,进一步隐藏 PCIe 延迟。 --- ## 4. 性能剖析工具:Hipprof Hipprof 是 DCU 平台提供的性能分析工具,具备三大核心能力: - **可视化性能时间线**:展示各 Kernel 和 API 调用的时间分布。 - **HIP API 追踪**:记录所有 HIP API 调用及其参数和耗时。 - **PMC 硬件计数器采集**:读取 GPU 内部的性能监控计数器(Performance Monitor Counter)。 ### 4.1 硬件计数器采集 以下命令以 8 进程并行运行目标程序,并采集 PMC 数据: ```bash mpirun -np 8 hipprof --pmc ./clmd-mpi-dcu ``` 下图展示了描述符导数计算 Kernel 的 PMC 性能数据: ![ PMC 性能数据](/api-forum/file/RedirectImage?path=20260622/1782113045605_1782110373539.png 'PMC 性能数据') 从图中可知该 Kernel:1. 寄存器使用较少(vgpr、sgpr);2. ALU 利用率较低(7.8%)。对于上述两点,可以增加循环展开,同时优化访存以增加计算密度。 | 参数 | 当前值 | 诊断 | | ------------------------------ | ------ | ------------------------------------------ | | **vgpr count** | 较少 | 每个线程的矢量寄存器压力不大 | | **sgpr count** | 较少 | 标量寄存器使用同样较轻 | | **processed ALU instructions** | 7.8% | ALU 利用率低,处于访存瓶颈(Memory-Bound) | **优化方向**: - **增加循环展开**:减少循环控制开销,提高指令级并行度。 - **优化访存模式**:使用共享内存(LDS)缓存复用数据,提升计算密度(Compute Intensity)。 - **合并内存访问**:确保相邻线程访问相邻的内存地址(Coalesced Access),充分利用内存带宽。 ### 4.2 可视化性能分析 ```bash mpirun -np 8 hipprof --hip-trace --mpi-trace --group-stream ./clmd-mpi-dcu ``` 该命令会生成包含 HIP API 轨迹、MPI 通信轨迹、以及 Kernel 执行时间线的可视化报告: ![1782110373551.png](/api-forum/file/RedirectImage?path=20260622/1782113056106_1782110373551.png '1782110373551.png') 可直观定位: - **Kernel 启动延迟**:API 调用到 Kernel 实际执行的时间间隔。 - **通信与计算重叠度**:MPI 通信是否与 GPU 计算有效重叠。 - **Kernel 串行化问题**:多个 Kernel 是否被意外地分配到同一 Stream 导致串行执行。 ### 4.3 Hipprof 参数速查表 #### Kernel 调度信息 | 参数 | 含义 | | ------------------------- | --------------------------------------------------- | | **kernel dispatch index** | Kernel 分发序号——程序启动后驱动分发的第 N 个 Kernel | | **gpu id** | 目标 GPU 编号——多卡环境下用于定位 Kernel 运行位置 | | **dispatch queue id** | 分发队列 ID——驱动内部的任务流硬件/软件标识符 | | **dispatch queue index** | 分发队列索引——Kernel 在当前队列中的排队位置 | | **thread id** | 主机端线程 ID——发出 Kernel 启动命令的 CPU 线程 | #### Kernel 资源占用 | 参数 | 含义 | | ---------------------- | ------------------------------------------------------------ | | **grid size** | 网格总线程数 = `Block 数 × work-group size` | | **work group size** | 每个 Block(线程块)内的线程数 | | **shared memory size** | 每个 Block 申请的 LDS 大小(字节) | | **scratch size** | 寄存器溢出(Spill)导致的暂存区大小——**非 0 时性能严重受损** | | **vgpr count** | **每线程**占用的矢量通用寄存器数 | | **sgpr count** | **每 Wavefront**占用的标量通用寄存器数 | | **fbarrier count** | 硬件栅栏槽位/状态计数 | | **signal handle** | 异步操作间同步与通信的内部句柄指针 | #### Kernel 性能指标 | 参数 | 含义 | | ------------------------------ | ------------------------------------------------ | | **kernel time** | GPU 上纯 Kernel 运行时间(秒) | | **processed ALU instructions** | ALU 指令执行时间占比——**极低通常意味着访存瓶颈** | | **performance** | 算力吞吐量(Gflops) | --- ## 5. DCU TensorCore 编程实战 DCU K100 集成了矩阵计算加速单元(TensorCore),可在一条指令内完成小规模矩阵乘加运算,大幅提升 AI 和 HPC 场景的吞吐量。本节从标准 GPU Kernel 出发,逐步演进到高性能 TensorCore 实现,完整呈现开发过程中的关键代码与优化思路。 ### 5.1 基础知识 #### 数据类型与宏定义 在编写 TensorCore 代码前,需要先定义向量化的数据类型和累加器清零宏: ```cpp // 4 元素向量类型:用于 TensorCore 累加器 typedef float float4_ __attribute__((ext_vector_type(4))); typedef double double4_ __attribute__((ext_vector_type(4))); typedef double double2_ __attribute__((ext_vector_type(2))); // 累加器清零宏 #define CLEAR_ACC(x4) \ x4.x = 0.0; \ x4.y = 0.0; \ x4.z = 0.0; \ x4.w = 0.0; ``` `float4_` 是 TensorCore 的核心数据结构——每次矩阵乘加指令的输出和累加都通过它完成,每个线程持有 `float4_` 中的 4 个元素。 #### TensorCore 计算模型 TensorCore 的计算以 **Wavefront(64 线程)** 为基本单位: - 每个 Warp 中的 **64 个线程**协同完成一次矩阵乘加:**D = A × B + C** - 使用 TensorCore 的 Kernel,其 Block 大小至少为 **64**(且应是 64 的倍数) - 单条指令完成 **16×16×4** 的矩阵乘(M=16, N=16, K=4),每个线程得到 4 个结果值,所有线程合计产出 **4 × 64 = 256** 个值 - 数据分布如图所示:T0表示wave中0号线程,以此类推T63表示wave中63号线程。T0中V0表示0号线程拿到的数据v0;T0中V1表示0号线程拿到的数据v1。64个数据的内存存储按照M/N的方向。如上图所示线程0~15号取M/N方向的16个数据;16~31号线程取第k+1列/行的16个数据;32~47号线程取第k+2列/行的16个数据;48~63号线程取第k+3列/行16个数据。64个数据分配到64个线程上,单个线程取一个数据。 - 输出数据大小为16x16的矩阵块。单个线程存储的数据大小为16x16/64个线程=4个数据。如图所示T0线程 输出的数据为T0-V0、T0-V1、T0-V2、T0-V3。4个输出数据不连续,每4个数据存放一个数据。 ![1782110373428.png](/api-forum/file/RedirectImage?path=20260622/1782113081499_1782110373428.jpg '1782110373428.png') --- ### 5.2 起点:标准 HIP 矩阵乘 Kernel 在引入 TensorCore 之前,先看一个标准的矩阵乘 GPU Kernel,作为对比基准。矩阵 A 为 **列主序(col-major)**,矩阵 B 为 **行主序(row-major)**: ```cpp // A: col-major [M×K], B: row-major [K×N], C/D: col-major [M×N] __global__ void MatMulAdd_kernel2( data_type *A, data_type *B, data_type *C, data_type *D, int M, int N, int K) { int row = blockIdx.y * blockDim.y + threadIdx.y; int col = blockIdx.x * blockDim.x + threadIdx.x; if (row < M && col < N) { data_type sum = 0; for (int k = 0; k < K; k++) { sum += A[row + k * M] * B[k * N + col]; } D[row + col * M] = sum + C[row + col * M]; } } // Host 端调用 void MatMulAdd_gpu(data_type *A, data_type *B, data_type *C, data_type *D, int M, int N, int K) { dim3 block(16, 16); dim3 grid((N + 15) / 16, (M + 15) / 16); hipLaunchKernelGGL(MatMulAdd_kernel2, grid, block, 0, 0, A, B, C, D, M, N, K); } ``` 这个 Kernel 每个线程负责计算输出矩阵的一个元素,通过内层循环遍历 K 维度完成点积。其计算密度受限于全局内存带宽,是典型的 **Memory-Bound** 实现。 --- ### 5.3 两种调用方式 以 `M=16, N=16, K=4` 的矩阵乘为例,DCU TensorCore 提供两种调用方式。 #### 方式一:Builtin 内建函数 Builtin 函数接收三个参数:映射矩阵 A 元素的变量、映射矩阵 B 元素的变量、储存结果的 `float4_` 变量: ![1782110373438.png](/api-forum/file/RedirectImage?path=20260622/1782113090445_1782110373438.png '1782110373438.png') ![1782110373446.png](/api-forum/file/RedirectImage?path=20260622/1782113099108_1782110373446.png '1782110373446.png') ```cpp float4_ D_d; CLEAR_ACC(D_d); // __builtin_amdgcn_mmac_f32_16x16x4f32: 单精度矩阵乘加 // M=16, N=16, K=4, 结果累加到 D_d D_d = __builtin_amdgcn_mmac_f32_16x16x4f32(A_val, B_val, D_d); ``` **Tips**:builtin 的正确指令名是 `__builtin_amdgcn_mmac_f32_16x16x4f32`(注意 `mmac` 而非 `mfma`,前者是 AMD CDNA 架构的矩阵乘加指令)。 #### 方式二:内联汇编(Inline Assembly) 当需要更精细的寄存器控制或使用双精度时,采用内联汇编: ![内联汇编调用方式](/api-forum/file/RedirectImage?path=20260622/1782113119062_1782110373459.png '1782110373459.png') ![内联汇编代码示例](/api-forum/file/RedirectImage?path=20260622/1782113136963_1782110373471.jpg '1782110373471.png') ```cpp float4_ D_d; CLEAR_ACC(D_d); // 单精度: v_mmac_16x16x4_f32 asm volatile( "s_waitcnt lgkmcnt(0)\n\t" "v_mmac_16x16x4_f32 %0, %1, %2, %0\n\t" : "+v"(D_d) // %0: 累加器(输入+输出) : "v"(A_val), "v"(B_val) // %1,%2: 矩阵 A、B 元素 ); // 双精度: v_mmac_16x16x4_f64 asm volatile( "s_waitcnt lgkmcnt(0)\n\t" "v_mmac_16x16x4_f64 %0, %1, %2, %0\n\t" : "+v"(D_d) : "v"(A_val), "v"(B_val) ); ``` > **关于 `s_waitcnt lgkmcnt(0)`**: > > `s_waitcnt` 全称 **S**calar **Wait** **C**ou**nt**(标量等待计数),是一条标量单元控制指令,作用是让 GPU 执行单元"停一下",直到指定的异步操作全部完成。 > > `lgkmcnt` 是三个监控目标的缩写组合: > > - **L**(LDS)— 追踪从共享内存读取数据的指令进度 > - **G**(Global / Generic)— 追踪从全局内存读取数据的指令进度 > - **KM**(K-cache / Constant / Message)— 追踪从常量缓存或标量消息通路读取数据的进度 > > `lgkmcnt(0)` 表示等待计数器归零——即所有被追踪的访存操作均已完成,数据已真正落入寄存器,此时发射依赖这些数据的 `v_mmac_*` 计算指令才是安全的。**省略这一条指令可能导致计算单元读到"半生不熟"的旧数据,引发数值错误且极难排查。** > > **选择建议**:优先使用 Builtin 方式,代码简洁可移植;双精度场景或需要精确排布指令时使用内联汇编。 --- ### 5.4 最小 TensorCore Kernel:16×16×4 下面是最精简的 TensorCore Kernel——每个 Block 只有 64 个线程,全局内存只加载一次 A 和 B(K=4),然后直接调用 TensorCore 指令: ```cpp __global__ void Matrix4_dot_gpu_tc_kernel( data_type *A, data_type *B, data_type *C, data_type *D) { int ip = blockIdx.x * blockDim.x + threadIdx.x; // 0~63 __shared__ data_type A_s[64]; __shared__ data_type B_s[64]; // 加载到共享内存(coalesced access) A_s[ip] = A[ip]; B_s[ip] = B[ip]; __syncthreads(); float4_ D_d; CLEAR_ACC(D_d); #ifdef USE_FLOAT D_d = __builtin_amdgcn_mmac_f32_16x16x4f32(A_s[ip], B_s[ip], D_d); #endif int row = ip % 16; int col = ip / 16; // 将 float4_ 的 4 个元素写回全局内存 D[row * 16 + col + 0 * 4] = D_d.x; D[row * 16 + col + 1 * 4] = D_d.y; D[row * 16 + col + 2 * 4] = D_d.z; D[row * 16 + col + 3 * 4] = D_d.w; } // Host 端调用:只需 1 个 Block,64 个线程 dim3 blocks(64, 1, 1); dim3 grids(1, 1, 1); hipLaunchKernelGGL(Matrix4_dot_gpu_tc_kernel, grids, blocks, 0, 0, A, B, C, D); ``` 这个 Kernel 清晰地展示了 TensorCore 的编程范式:**加载到共享内存 → 调用一条 TensorCore 指令 → 写回结果**。但它只处理了 K=4 的固定问题,无法发挥实际性能。 --- ### 5.5 K 维度展开:从 16×16×4 到 16×16×32 实际场景中 K 通常远大于 4。通过循环遍历 K 维度,每次处理 K=4 的一个分片(tile),并将结果累加到同一组 `float4_` 中: ```cpp __global__ void vmmac_16x16x32kernel( data_type *A, data_type *B, data_type *C, data_type *D) { int ip = blockIdx.x * blockDim.x + threadIdx.x; __shared__ data_type A_s[16*32]; __shared__ data_type B_s[32*16]; int row = ip % 16; int col = ip / 16; // K=32,每次处理 K_tile=4,所以需要 32/4 = 8 次迭代 int iter = 32 / 4; // 从全局内存加载 A 和 B 到共享内存 for (int i = 0; i < iter; i++) { int sm_offset = i * 64; A_s[ip + sm_offset] = A[ip + sm_offset]; B_s[ip + sm_offset] = B[ip + sm_offset]; } __syncthreads(); // K 维度循环累加 float4_ D_d; for (int i = 0; i < iter; i++) { int sm_offset = i * 64; CLEAR_ACC(D_d); // 注意:每条 mmac 指令前清零 D_d = __builtin_amdgcn_mmac_f32_16x16x4f32( A_s[ip + sm_offset], B_s[ip + sm_offset], D_d); // 累加到全局内存(注意这里是 +=) D[row * 16 + col + 0 * 4] += D_d.x; D[row * 16 + col + 1 * 4] += D_d.y; D[row * 16 + col + 2 * 4] += D_d.z; D[row * 16 + col + 3 * 4] += D_d.w; } } ``` > **注意**:此示例中每次迭代清零再累加是为了演示 K 维度分片的基本模式。更高效的实现中,`CLEAR_ACC` 只调用一次,后续迭代直接累加。 --- ### 5.6 通用 M×N×K 与软件流水线 为了支持任意 K 值并隐藏访存延迟,采用 **双缓冲(double buffering)** 策略——在计算当前 tile 的同时预取下一个 tile: ```cpp __global__ void vmmac_MxNxK_tc_1wrap_kernel( data_type *A, data_type *B, data_type *C, data_type *D, int m, int n, int k) { int ip = blockIdx.x * blockDim.x + threadIdx.x; int row = ip % 16; int col = ip / 16; // ---- 双缓冲共享内存 ---- __shared__ data_type A_s[2][32*4]; __shared__ data_type B_s[2][4*64]; int iter_k = k / 4; // 预加载第一批数据到 buffer[0] #pragma unroll for (int i = 0; i < 2; i++) A_s[0][ip + i*64] = A[(ip % 32) + (ip/32 + 2*i) * 32]; #pragma unroll for (int i = 0; i < 4; i++) B_s[0][ip + i*64] = B[ip + i*64]; __syncthreads(); float4_ D_d[8]; CLEAR_ACC(D_d[0]); /* ... D_d[1..7] */ ; // ---- 主循环:计算 + 预取交替 ---- for (int k_step = 0; k_step < iter_k; k_step++) { int read_idx = k_step % 2; // 当前使用 buffer[read_idx] int write_idx = (k_step + 1) % 2; // 预取到 buffer[write_idx] if (k_step < iter_k - 1) { // 预取下一批数据(与当前计算重叠) #pragma unroll for (int i = 0; i < 2; i++) A_s[write_idx][ip + i*64] = A[(ip % 32) + (k_step+1)*32*4 + (ip/32 + 2*i) * 32]; #pragma unroll for (int i = 0; i < 4; i++) B_s[write_idx][ip + i*64] = B[ip + (k_step+1)*64*4 + i*64]; } // 使用 buffer[read_idx] 发射 8 条 TensorCore 指令 asm volatile("v_mmac_16x16x4_f32 %0, %1, %2, %0\n\t" : "+v"(D_d[0]) : "v"(A_s[read_idx][row + col*32]), "v"(B_s[read_idx][row + col*64])); // ... 其余 7 条指令类似 ... __syncthreads(); } // ---- 写回结果 ---- // D_d[0..3] → 行 0~15;D_d[4..7] → 行 16~31 // 每个 float4_ 覆盖 N 方向 16 列 ... } ``` 通过双缓冲,全局内存加载与 TensorCore 计算重叠执行,有效隐藏了访存延迟。 --- ### 5.7 双精度 TensorCore DCU K100 同样支持双精度(f64)TensorCore,指令为 `v_mmac_16x16x4_f64`。与单精度的关键区别在于必须使用 **内联汇编** 调用,且累加器类型为 `double4_`: ```cpp __device__ void vmmac_32x64x32_microkernel( data_type *A_s, data_type *B_s, data_type *D_s) { double4_ D_d[8]; // ... 清零 8 个累加器 ... double *A_ptr = A_s + row + col * 32; double *B_ptr = B_s + row + col * 64; for (int i = 0; i < 8; i++) { double a0 = A_ptr[0]; double a1 = A_ptr[16]; double b0 = B_ptr[0]; double b1 = B_ptr[16]; double b2 = B_ptr[32]; double b3 = B_ptr[48]; // 双精度内联汇编:4 条指令,每条使用 4 个 B 寄存器 asm volatile( "s_waitcnt lgkmcnt(0)\n\t" "v_mmac_16x16x4_f64 %0, %4, %5, %0\n\t" "v_mmac_16x16x4_f64 %1, %4, %6, %1\n\t" "v_mmac_16x16x4_f64 %2, %4, %7, %2\n\t" "v_mmac_16x16x4_f64 %3, %4, %8, %3\n\t" : "+v"(D_d[0]), "+v"(D_d[1]), "+v"(D_d[2]), "+v"(D_d[3]) : "v"(a0), "v"(b0), "v"(b1), "v"(b2), "v"(b3)); asm volatile( "s_waitcnt lgkmcnt(0)\n\t" "v_mmac_16x16x4_f64 %0, %4, %5, %0\n\t" "v_mmac_16x16x4_f64 %1, %4, %6, %1\n\t" "v_mmac_16x16x4_f64 %2, %4, %7, %2\n\t" "v_mmac_16x16x4_f64 %3, %4, %8, %3\n\t" : "+v"(D_d[4]), "+v"(D_d[5]), "+v"(D_d[6]), "+v"(D_d[7]) : "v"(a1), "v"(b0), "v"(b1), "v"(b2), "v"(b3)); A_ptr += 4 * 32; // 移动到下一个 K tile B_ptr += 4 * 64; } // ... 写回结果 ... } ``` **单精度 vs 双精度对比**: | 特性 | f32 | f64 | | -------------- | -------------------- | ----------------------- | | **指令** | `v_mmac_16x16x4_f32` | `v_mmac_16x16x4_f64` | | **累加器类型** | `float4_` | `double4_` | | **调用方式** | Builtin 或内联汇编 | 仅内联汇编 | | **每周期吞吐** | 更高 | 较低(约为 f32 的 1/2) | --- ### 5.8 TensorCore总结 1. **Block 大小 = 64 的倍数**:与 Warp 大小对齐,充分利用 TensorCore 并行度。 2. **数据布局**:A 用 col-major,B 用 row-major,使共享内存加载为 coalesced access。 3. **多累加器**:根据 M 和 N 扩展累加器数量(数量 = `ceil(M/16) × ceil(N/16)`),提升指令级并行度。 4. **双缓冲**:使用两套共享内存交替读写,将全局内存加载与 TensorCore 计算重叠。 5. **优先 Builtin,按需切汇编**:f32 场景用 Builtin;f64 或需要精细控制寄存器时用内联汇编。 6. **`s_waitcnt` 保序**:在发射第一条 mmac 指令前用 `s_waitcnt lgkmcnt(0)` 确保 LDS/全局/常量内存的异步读取全部完成(详见 5.3 节说明),避免读到未就绪的脏数据。 --- ## 6. 总结与优化路线图 本文从硬件配置、拓扑分析、带宽测试、性能剖析到 TensorCore 编程,完整走了一遍 DCU K100 节点的上手流程。核心结论与优化建议汇总如下: ### 硬件与拓扑 | 要点 | 关键数据 | | --------------------- | -------------------------------- | | CPU | Hygon C86 7381 ×2,64 核,8 NUMA | | DCU | K100 ×8,架构 gfx926 | | 组内带宽(单向/双向) | 28 / 55 GB/s | | 跨组带宽(单向/双向) | 11 / 22 GB/s | | PCIe 利用率 | 87%~89% | ### 优化路线图 1. **NUMA 亲和性**:确保进程与 DCU 在同一 NUMA 节点内,消除跨节点通信瓶颈。 2. **访存优化**:针对 ALU 利用率低的 Kernel(如文中的描述符导数计算),优先改善访存模式,利用共享内存(LDS)和合并访存。 3. **循环展开**:增加指令级并行度,配合寄存器压力监控,在 vgpr 不溢出的前提下适度展开。 4. **TensorCore 引入**:以 64 的倍数为粒度重构 Kernel 计算框架,按"标准 Kernel → 单条 TC → K 展开 → 多累加器 → 双缓冲软件流水线"的路线逐步优化。 5. **计算-通信重叠**:利用多 Stream 机制,将 PCIe 数据传输与 Kernel 执行流水线化,隐藏通信延迟。 --- ## 参考文献 1. AMD. *AMD ROCm 软件* [EB/OL]. [2025-11-20]. https://www.amd.com/zh-cn/products/software/rocm.html 2. AMD. *Using ROCm Bandwidth Test — ROCm Bandwidth Test Documentation* [EB/OL]. [2025-11-20]. https://rocm.docs.amd.com/projects/rocm_bandwidth_test/en/master/how-to/using_rocm_bandwidth_test.html 3. CUI Chen, WU Di, TAO Yerong, 等. *多 GPU 系统高速互联技术与拓扑结构发展现状研究* [J]. Aero Weaponry(航空兵器), 2024, 31(1): 23-31. --- *本文基于 DCU K100 节点的实际测试数据整理而成,数据和结论仅供参考。不同硬件批次和驱动版本可能导致性能差异,建议以实际测量结果为准。*
DCU入门
全部评论(0)
只看楼主
暂无数据
--
楼主
Level.2
0 帖子
楼主热帖
暂无数据