1
1
1
DCU技术达人
DCU入门(下):编程实战与性能优化
2026-07-03 13:06:24
55
> **适合人群**:有 C/C++ 基础,已读完上篇(或了解 DCU 硬件基础),想动手写代码的开发者 > > **阅读时间**:约 25 分钟 > > **你将收获**:从 Hello World 写到矩阵乘法 → 掌握 HIP 编程 → 会调优 → 会移植 CUDA 代码 > > 📖 **上篇回顾**:[DCU入门(上):硬件原理与内存体系](./DCU入门(上):硬件原理与内存体系.md)(DCU 硬件架构 · CU/Wavefront · 五级内存 · HBM2 · DMA) --- ## 目录 - [六、上手写代码——从 Hello World 到矩阵乘法](#六上手写代码从-hello-world-到矩阵乘法) - [6.1 环境准备](#61-环境准备) - [6.2 第一个 HIP 程序:向量加法(完整 walkthrough)](#62-第一个-hip-程序向量加法完整-walkthrough) - [6.3 线程索引全解析](#63-线程索引全解析) - [6.4 二维线程块:图像处理案例](#64-二维线程块图像处理案例) - [6.5 归约算法:深入理解并行编程](#65-归约算法深入理解并行编程) - [七、DCU 软件生态——别再自己造轮子了](#七dcu-软件生态别再自己造轮子了) - [八、性能优化实战——从能跑到跑得快](#八性能优化实战从能跑到跑得快) - [8.1 三个常见坑](#81-三个常见坑) - [8.2 矩阵乘法优化全记录:从 V0 到 V4](#82-矩阵乘法优化全记录从-v0-到-v4) - [8.3 性能分析工具:rocprofiler 快速上手指南](#83-性能分析工具rocprofiler-快速上手指南) - [九、DCU vs NVIDIA CUDA——移植指南速查](#九dcu-vs-nvidia-cuda移植指南速查) - [十、总结 + 学习路线](#十总结--学习路线) --- ## 六、上手写代码——从 Hello World 到矩阵乘法 ### 6.1 环境准备 ```bash # 确认 DTK 已安装 which hipcc # → /opt/dtk/bin/hipcc hipconfig --version # → 看 DCU 工具链版本 # 查看 DCU 设备列表 rocm-smi # → 看有几个 DCU 卡、温度、显存用量 ``` 最简单的编译方式: ```bash hipcc -o my_program my_program.cpp # 相当于 g++ my_program.cpp -o my_program ...(带 HIP 特定的 include 和链接) ``` ### 6.2 第一个 HIP 程序:向量加法(完整 walkthrough) 先贴完整代码,再逐行分析。 ```cpp #include #include #include // 宏:检查 HIP API 调用是否成功(强烈建议每次都用) #define HIP_CHECK(call) do { \ hipError_t err = call; \ if (err != hipSuccess) { \ std::cerr << "HIP Error at " << __FILE__ << ":" \ << __LINE__ << " - " \ << hipGetErrorString(err) << std::endl; \ exit(1); \ } \ } while(0) // ── DCU 核函数:每个线程负责一个元素的加法 ── __global__ void vectorAdd(const float* A, const float* B, float* C, int N) { // 计算全局线程 ID int idx = blockIdx.x * blockDim.x + threadIdx.x; // 边界保护(线程数可能多于数据量) if (idx < N) { C[idx] = A[idx] + B[idx]; } } int main() { // ─── 1. 确定问题规模 ─── int N = 1 << 24; // 16M 个元素 = 64MB size_t size = N * sizeof(float); std::cout << "向量加法: " << N << " 个元素, " << size / (1024*1024) << " MB" << std::endl; // ─── 2. 分配 CPU 端内存并初始化 ─── float *h_A = new float[N]; float *h_B = new float[N]; float *h_C = new float[N]; for (int i = 0; i < N; i++) { h_A[i] = i * 1.0f; h_B[i] = i * 2.0f; } // ─── 3. 分配 DCU 端显存 ─── float *d_A, *d_B, *d_C; HIP_CHECK(hipMalloc(&d_A, size)); // hipMalloc: 在 HBM2 里挖一块 HIP_CHECK(hipMalloc(&d_B, size)); HIP_CHECK(hipMalloc(&d_C, size)); // ─── 4. 数据从 CPU 搬到 DCU (Host → Device) ─── HIP_CHECK(hipMemcpy(d_A, h_A, size, hipMemcpyHostToDevice)); HIP_CHECK(hipMemcpy(d_B, h_B, size, hipMemcpyHostToDevice)); // ─── 5. 配置并启动核函数 ─── int blockSize = 256; // 每个线程块 256 个线程 int gridSize = (N + blockSize - 1) / blockSize; // 向上取整 std::cout << "启动配置: " << gridSize << " 块 × " << blockSize << " 线程/块 = " << gridSize * blockSize << " 总线程" << std::endl; // hipLaunchKernelGGL 的参数依次是: // kernel名字, grid维度, block维度, 动态共享内存字节数, stream, kernel参数... hipLaunchKernelGGL(vectorAdd, gridSize, blockSize, 0, 0, // 共享内存=0, 默认stream d_A, d_B, d_C, N); // ─── 6. 等待 DCU 完成 ─── HIP_CHECK(hipDeviceSynchronize()); // ─── 7. 结果从 DCU 搬回 CPU (Device → Host) ─── HIP_CHECK(hipMemcpy(h_C, d_C, size, hipMemcpyDeviceToHost)); // ─── 8. 验证结果 ─── int errors = 0; for (int i = 0; i < N; i++) { if (fabs(h_C[i] - (i * 1.0f + i * 2.0f)) > 0.001f) { if (errors++ < 5) // 只打印前5个错误 std::cerr << "错误: h_C[" << i << "] = " << h_C[i] << " (期望 " << (i*3.0f) << ")" << std::endl; } } if (errors == 0) { std::cout << "✅ 全部 " << N << " 个元素验证通过!" << std::endl; std::cout << " 示例: h_C[0]=" << h_C[0] << ", h_C[1]=" << h_C[1] << ", ..., h_C[" << N-1 << "]=" << h_C[N-1] << std::endl; } else { std::cerr << "❌ 共 " << errors << " 个错误" << std::endl; } // ─── 9. 清理资源 ─── HIP_CHECK(hipFree(d_A)); HIP_CHECK(hipFree(d_B)); HIP_CHECK(hipFree(d_C)); delete[] h_A; delete[] h_B; delete[] h_C; return 0; } ``` **编译运行**: ```bash hipcc -o vector_add vector_add.cpp ./vector_add ``` **执行流水线(关键理解)**: ``` 时间 → CPU (Host) DCU (Device) [分配内存] [待命] [初始化 h_A, h_B] [待命] [hipMalloc] ──────────► [在 HBM2 里分配 d_A, d_B, d_C] [hipMemcpy] ──────────► [DMA 自动搬运 h_A→d_A, h_B→d_B] [hipLaunchKernel]──────► [下发 kernel 到每个 CU] [Wavefront 0: ADD R1,R2,R3 (64线程)] ← 零开销切换 [Wavefront 1: ADD R1,R2,R3 (64线程)] [CPU 继续做事...] [Wavefront 2: ADD R1,R2,R3 (64线程)] [... 所有线程执行完毕 ...] [hipDeviceSync] ◄─────── [通知 CPU: 我干完了] [hipMemcpy] ◄─────────── [DMA 搬运 d_C → h_C] [验证结果] [清理] ``` ### 6.3 线程索引全解析 这是 DCU/HIP 编程最基础的技能——你要能算出**每个线程处理哪个数据**。 ```cpp // ─── 1D 布局(最常见) ─── // Block 维度: (256, 1, 1) // Grid 维度: (64, 1, 1) // 总线程数: 64 × 256 = 16384 __global__ void kernel_1d(float* data) { int global_id = blockIdx.x * blockDim.x + threadIdx.x; // threadIdx.x ∈ [0, blockDim.x) → 线程在块内的位置 // blockIdx.x ∈ [0, gridDim.x) → 线程块在网格内的位置 // blockDim.x → 每个块多少线程(启动时固定) } // ─── 2D 布局(图像处理常用) ─── // Block 维度: (16, 16, 1) → 256 线程/块 // Grid 维度: (64, 64, 1) → 4096 块 __global__ void kernel_2d(float* image, int width, int height) { int x = blockIdx.x * blockDim.x + threadIdx.x; // 列 int y = blockIdx.y * blockDim.y + threadIdx.y; // 行 if (x < width && y < height) { // 处理像素 (x, y) int linear_id = y * width + x; image[linear_id] = do_something(image[linear_id]); } } // ─── 3D 布局(体数据 / 物理模拟) ─── __global__ void kernel_3d(float* volume, int X, int Y, int Z) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; int z = blockIdx.z * blockDim.z + threadIdx.z; if (x < X && y < Y && z < Z) { int idx = z * (X * Y) + y * X + x; volume[idx] = process(volume[idx]); } } ``` **1D 线程索引速查表**: | 变量 | 含义 | 举例 (grid=(64,1,1), block=(256,1,1)) | | ------------- | --------------------------------------- | ------------------------------------- | | `threadIdx.x` | 线程在块内的 x 索引 | 0 ~ 255 | | `blockIdx.x` | 块在网格内的 x 索引 | 0 ~ 63 | | `blockDim.x` | 每块的 x 方向线程数 | 256 | | `gridDim.x` | 网格的 x 方向块数 | 64 | | `全局ID` | `blockIdx.x * blockDim.x + threadIdx.x` | 0 ~ 16383 | ### 6.4 二维线程块:图像处理案例 写一个**灰度图转二值图**的完整示例,演示 2D 线程块用法: ```cpp #include #include #include #define HIP_CHECK(call) /* 同上,省略 */ // 灰度转二值:大于阈值的像素 → 255, 否则 → 0 __global__ void threshold(const unsigned char* __restrict__ input, unsigned char* __restrict__ output, int width, int height, unsigned char threshold_val) { // 2D 线程映射 int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; if (x < width && y < height) { int idx = y * width + x; unsigned char pixel = input[idx]; output[idx] = (pixel > threshold_val) ? 255 : 0; } } int main() { int width = 4096, height = 3072; // 4K 分辨率 size_t img_size = width * height * sizeof(unsigned char); // CPU 端:生成测试图像 unsigned char *h_input = new unsigned char[width * height]; unsigned char *h_output = new unsigned char[width * height]; for (int i = 0; i < width * height; i++) { h_input[i] = (unsigned char)(i % 256); // 渐变灰度 } // DCU 端:分配显存 unsigned char *d_input, *d_output; HIP_CHECK(hipMalloc(&d_input, img_size)); HIP_CHECK(hipMalloc(&d_output, img_size)); HIP_CHECK(hipMemcpy(d_input, h_input, img_size, hipMemcpyHostToDevice)); // 2D 线程块配置 dim3 block(16, 16); // 16×16 = 256 线程/块(经典配置) dim3 grid((width + 15) / 16, (height + 15) / 16); std::cout << "图像大小: " << width << "×" << height << " | Grid: " << grid.x << "×" << grid.y << " | Block: " << block.x << "×" << block.y << std::endl; hipLaunchKernelGGL(threshold, grid, block, 0, 0, d_input, d_output, width, height, 128); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipMemcpy(h_output, d_output, img_size, hipMemcpyDeviceToHost)); // 验证 int errors = 0; for (int i = 0; i < width * height; i++) { unsigned char expected = (h_input[i] > 128) ? 255 : 0; if (h_output[i] != expected) errors++; } std::cout << (errors == 0 ? "✅ 全部正确!" : "❌ 有错误") << std::endl; HIP_CHECK(hipFree(d_input)); HIP_CHECK(hipFree(d_output)); delete[] h_input; delete[] h_output; return 0; } ``` ### 6.5 归约算法:深入理解并行编程 **归约(Reduction)** 是把一个数组"折叠"成一个值的过程——求和、求最大值、求范数都是归约。这是理解共享内存和线程同步的最好案例。 ```cpp // ─── 并行归约:求数组和 ─── // 每轮参与线程数减半,最终把累加结果留在共享内存的第一个位置 #define BLOCK_SIZE 256 #define WARPSIZE 64 // DCU 的 Wavefront 大小 __global__ void reduce_sum(const float* __restrict__ input, float* __restrict__ output, int N) { __shared__ float smem[BLOCK_SIZE]; int tid = threadIdx.x; int idx = blockIdx.x * blockDim.x * 2 + tid; // ×2:每个线程加载 2 个元素 // ── 步骤1: 从全局内存加载到共享内存 ── // 每个线程加载 2 个元素并累加(减少一半线程启动) float sum = 0.0f; if (idx < N) sum += input[idx]; if (idx + BLOCK_SIZE < N) sum += input[idx + BLOCK_SIZE]; smem[tid] = sum; __syncthreads(); // ── 步骤2: 树形归约(每轮参与线程数减半) ── // 可视化(假设 8 线程版): // 第 1 轮: 线程0←线程4 线程1←线程5 线程2←线程6 线程3←线程7 // [0+4] [1+5] [2+6] [3+7] // 第 2 轮: 线程0←线程2 线程1←线程3 (线程4~7 不参与) // [0+2+4+6] [1+3+5+7] // 第 3 轮: 线程0←线程1 (线程2~7 不参与) // [sum of all] // for (int stride = BLOCK_SIZE / 2; stride > 0; stride >>= 1) { if (tid < stride) { smem[tid] += smem[tid + stride]; } __syncthreads(); // 每轮之后必须同步 } // ── 步骤3: 每个块的结果写回全局内存 ── if (tid == 0) { output[blockIdx.x] = smem[0]; // 每个块输出一个部分和 } } ``` **归约过程的图解**: ``` 输入: [3, 1, 7, 0, 4, 1, 6, 3] (N=8, block=8,简化) 线程: t0 t1 t2 t3 t4 t5 t6 t7 smem: [ 3, 1, 7, 0, 4, 1, 6, 3 ] 第1轮 (stride=4): t0←t4(3+4) t1←t5(1+1) t2←t6(7+6) t3←t7(0+3) smem: [ 7, 2, 13, 3, 4, 1, 6, 3 ] ← 后半不再被读 第2轮 (stride=2): t0←t2(7+13) t1←t3(2+3) smem: [20, 5, 13, 3, 4, 1, 6, 3 ] 第3轮 (stride=1): t0←t1(20+5) smem: [25, 5, 13, 3, 4, 1, 6, 3 ] ↑ sum = 25 ✓ ``` --- ## 七、DCU 软件生态——别再自己造轮子了 DCU 不只是提供一个 HIP 编译器——它还有一个完整的软件栈。大部分常见算法都有高度优化的库可以直接调。 ``` ┌──────────────────────────────────────────────────┐ │ 你的应用 (AI训练 / 科学计算 / 数据分析) │ ├──────────────────────────────────────────────────┤ │ 数学库 │ │ ┌────────┬────────┬────────┬──────────────────┐ │ │ │hipBLAS │hipFFT │hipSPARSE│hipRAND MIOpen │ │ │ │(线性代数)(傅里叶) (稀疏矩阵)(随机数) (深度学习) │ │ │ ├────────┴────────┼────────┴──────────────────┤ │ │ │ RCCL (多卡通信) │ hipCUB (基础算法) │ │ │ └─────────────────┴───────────────────────────┘ │ ├──────────────────────────────────────────────────┤ │ HIP Runtime (核函数启动、内存管理、stream等) │ ├──────────────────────────────────────────────────┤ │ DCU 驱动 │ ├──────────────────────────────────────────────────┤ │ DCU 硬件 │ └──────────────────────────────────────────────────┘ ``` ### 核心库速查与代码示例 | 库 | 对标 NVIDIA | 干什么 | 一句话 | | ------------- | ----------- | ----------------- | --------------------- | | **hipBLAS** | cuBLAS | BLAS 1/2/3 级运算 | 矩阵乘、向量计算 | | **hipFFT** | cuFFT | 快速傅里叶变换 | 信号/图像频域处理 | | **hipSPARSE** | cuSPARSE | 稀疏矩阵运算 | 图计算、推荐系统 | | **hipRAND** | cuRAND | 伪随机数生成 | 初始化、Dropout | | **MIOpen** | cuDNN | 深度学习算子 | 卷积/池化/激活/归一化 | | **hipCUB** | CUB | 并行原语 | 排序/扫描/直方图 | | **RCCL** | NCCL | 集合通信 | AllReduce/Broadcast | | **rocThrust** | Thrust | STL-like并行模板 | sort/reduce/scan | **hipBLAS 示例——矩阵乘法**: ```cpp #include #include // 一行代码搞定矩阵乘法 C = α*A*B + β*C void hipblas_sgemm_demo() { int M = 2048, N = 2048, K = 2048; // C(M×N) = A(M×K) × B(K×N) size_t sizeA = M * K * sizeof(float); size_t sizeB = K * N * sizeof(float); size_t sizeC = M * N * sizeof(float); float *d_A, *d_B, *d_C; hipMalloc(&d_A, sizeA); hipMalloc(&d_B, sizeB); hipMalloc(&d_C, sizeC); // 初始化数据(省略 hipMemcpy...) hipblasHandle_t handle; hipblasCreate(&handle); float alpha = 1.0f, beta = 0.0f; // SGEMM: S=单精度, GEMM=通用矩阵乘法 hipblasSgemm(handle, HIPBLAS_OP_N, HIPBLAS_OP_N, // 不转置 M, N, K, // C(M×N) = A(M×K) * B(K×N) &alpha, d_A, M, // A 矩阵 (leading dimension = M) d_B, K, // B 矩阵 (leading dimension = K) &beta, d_C, M); // C 矩阵 (leading dimension = M) hipblasDestroy(handle); // 比自己写的 kernel 通常快 3~10 倍——因为底层针对 DCU 架构深度优化 } ``` **MIOpen 示例——深度学习卷积**: ```cpp #include // 创建一个卷积层的前向推理 void miopen_conv2d_demo() { miopenHandle_t handle; miopenCreate(&handle); // 输入: NCHW 格式,4×3×224×224 // 权重: 64×3×7×7 (64 个输出通道,卷积核 7×7) miopenTensorDescriptor_t input_desc, output_desc, filter_desc; miopenCreateTensorDescriptor(&input_desc); miopenCreateTensorDescriptor(&output_desc); miopenCreateTensorDescriptor(&filter_desc); int N = 4, C = 3, H = 224, W = 224; miopenSet4dTensorDescriptor(input_desc, miopenFloat, N, C, H, W); int K = 64; // 输出通道 miopenSet4dTensorDescriptor(filter_desc, miopenFloat, K, C, 7, 7); // ... 省略卷积描述符和算法选择 ... // 执行卷积(内部使用最优的 Winograd/FFT/ImplicitGEMM 等算法) miopenConvolutionForward(handle, &alpha, input_desc, d_input, filter_desc, d_filter, conv_desc, algo, &beta, output_desc, d_output, d_workspace, workspace_size); miopenDestroy(handle); } ``` --- ## 八、性能优化实战——从能跑到跑得快 ### 8.1 三个常见坑 **坑1:隐式的 CPU↔DCU 数据传输** ```cpp // ❌ 循环里每次迭代搬数据 (PCI-E 28GB/s 是最大瓶颈) for (int iter = 0; iter < 10000; iter++) { hipMemcpy(d_data, h_data, size, hipMemcpyHostToDevice); kernel<<>>(d_data, d_result); hipMemcpy(h_result, d_result, size, hipMemcpyDeviceToHost); } // ✅ 数据留在 DCU 上,多次 kernel 之间不搬数据 hipMemcpy(d_data, h_data, size, hipMemcpyHostToDevice); for (int iter = 0; iter < 10000; iter++) { kernel<<>>(d_data, d_result); // d_result 留在 DCU 显存上,下个 iter 直接用 } hipMemcpy(h_result, d_result, size, hipMemcpyDeviceToHost); ``` **坑2:寄存器溢出(Register Spill)** ```cpp // ❌ 声明大数组 → 编译器被迫 spill 到 L1 __global__ void too_many_regs(float* data) { float temp[200]; // ← 200×4=800 字节,远超寄存器配额 for (int i = 0; i < 200; i++) temp[i] = data[threadIdx.x + i*256]; // 编译器只能把 temp 放到 L1/LDS,每次访问多 30 周期 } // ✅ 减少局部变量 → 全部留在寄存器 __global__ void fewer_regs(float* data) { float val0 = data[threadIdx.x]; float val1 = data[threadIdx.x + 256]; float val2 = data[threadIdx.x + 512]; float val3 = data[threadIdx.x + 768]; // 4个寄存器 → 极快 } ``` 检查寄存器使用量: ```bash hipcc -Rpass-analysis=kernel-resource-usage -c my_kernel.cpp # 输出: kernel_name: 32 registers, 0 bytes lds, 0 bytes stack ``` **坑3:忽略 Occupancy(占用率)** ```cpp // Occupancy = 活跃 Wavefront 数 / 理论最大 Wavefront 数 // 如果每个线程用 128 个寄存器,一个 CU 最多能放 2560/8 = 320 线程 // → Occupancy 只有 12.5% // → 无法有效隐藏内存延迟 → 性能暴跌 // 查看 Occupancy: // rocprof --stats ./my_program // 关注: VALUUtilization%, SQ_WAVES (活跃 Wavefront) ``` ### 8.2 矩阵乘法优化全记录:从 V0 到 V4 这是理解 DCU 性能优化最经典的案例——**同一个算法,不同实现方式,性能差 50 倍**。 每个版本前标注了**关键优化点**和**性能提升**(基于 DCU2 实测,具体数字因型号而异)。 --- **V0:最朴素的版本(基准线)** ```cpp // V0: 每个线程算一个输出元素,直接读全局内存 // 关键问题: 每个线程读 K 次全局内存 → 带宽瓶颈 __global__ void sgemm_v0(const float* __restrict__ A, const float* __restrict__ B, float* __restrict__ C, 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) { float sum = 0.0f; for (int k = 0; k < K; k++) { sum += A[row * K + k] * B[k * N + col]; // ← 每次读2次全局内存! } C[row * N + col] = sum; } } // 性能: ~50 GFLOPS (基准线) ``` --- **V1:合并访问优化(提升 ~3×)** ```cpp // V1: 让相邻线程读相邻地址 (合并访问) // 关键改进: 每个线程块先协作加载 A 的一列到寄存器 __global__ void sgemm_v1(const float* __restrict__ A, const float* __restrict__ B, float* __restrict__ C, 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) { float sum = 0.0f; // B 矩阵的列读取:线程0→B[0*N+col], 线程1→B[0*N+col+1] ... // 但 B[0*N+col] 到 B[0*N+col+63] 如果是连续的 → 合并访问 ✓ for (int k = 0; k < K; k++) { sum += A[row * K + k] * B[k * N + col]; } C[row * N + col] = sum; } } // 性能: ~150 GFLOPS (比 V0 快 3×) ``` --- **V2:共享内存 Tiling(提升 ~15×)** ```cpp // V2: 引入共享内存做 tiling(分块) // 关键改进: 每个线程块的一小块 A 和 B 只从全局内存读一次 #define TILE_SIZE 32 __global__ void sgemm_v2(const float* __restrict__ A, const float* __restrict__ B, float* __restrict__ C, int M, int N, int K) { __shared__ float As[TILE_SIZE][TILE_SIZE]; __shared__ float Bs[TILE_SIZE][TILE_SIZE]; int bx = blockIdx.x, by = blockIdx.y; int tx = threadIdx.x, ty = threadIdx.y; // 全局输出索引 int row = by * TILE_SIZE + ty; int col = bx * TILE_SIZE + tx; float sum = 0.0f; // 外层循环:遍历 K 维度上的分块 for (int t = 0; t < K / TILE_SIZE; t++) { // 协作加载 A 的一个 tile 到共享内存 if (row < M && (t * TILE_SIZE + tx < K)) As[ty][tx] = A[row * K + t * TILE_SIZE + tx]; else As[ty][tx] = 0.0f; // 协作加载 B 的一个 tile 到共享内存 if ((t * TILE_SIZE + ty) < K && col < N) Bs[ty][tx] = B[(t * TILE_SIZE + ty) * N + col]; else Bs[ty][tx] = 0.0f; __syncthreads(); // 共享内存里的乘加(极快!) for (int k = 0; k < TILE_SIZE; k++) { sum += As[ty][k] * Bs[k][tx]; // ← 从共享内存读,~30周期 } __syncthreads(); // 等所有线程用完,准备加载下一块 } if (row < M && col < N) C[row * N + col] = sum; } // 性能: ~750 GFLOPS (比 V0 快 15×) ``` --- **V3:寄存器 + 共享内存 + 4×1 micro-tiling(提升 ~30×)** ```cpp // V3: 每个线程算 4 个输出元素(vectorize) // A 的 tile 读进共享内存时用 float4 向量化加载 // 关键改进: float4 加载 → 1 条指令读 4 个 float #define TILE_SIZE 32 __global__ void sgemm_v3(const float* __restrict__ A, const float* __restrict__ B, float* __restrict__ C, int M, int N, int K) { __shared__ float As[TILE_SIZE][TILE_SIZE]; __shared__ float Bs[TILE_SIZE][TILE_SIZE]; int bx = blockIdx.x, by = blockIdx.y; int tx = threadIdx.x, ty = threadIdx.y; int row = by * TILE_SIZE + ty; int col = (bx * TILE_SIZE + tx) * 4; // 每个线程算 4 列 float sum0 = 0.0f, sum1 = 0.0f, sum2 = 0.0f, sum3 = 0.0f; for (int t = 0; t < K / TILE_SIZE; t++) { // 向量化加载 A (每次读 4 个 float) int a_col = t * TILE_SIZE + tx; if (row < M && a_col < K) { As[ty][tx] = A[row * K + a_col]; } else As[ty][tx] = 0.0f; // B 的 tile 加载(向量化读取 4 列) int b_row = t * TILE_SIZE + ty; #pragma unroll for (int c = 0; c < 4; c++) { int b_col = (bx * TILE_SIZE + tx) * 4 + c; if (b_row < K && b_col < N) Bs[ty][tx * 4 + c] = B[b_row * N + b_col]; } __syncthreads(); #pragma unroll for (int k = 0; k < TILE_SIZE; k++) { float a_val = As[ty][k]; sum0 += a_val * Bs[k][tx * 4 + 0]; sum1 += a_val * Bs[k][tx * 4 + 1]; sum2 += a_val * Bs[k][tx * 4 + 2]; sum3 += a_val * Bs[k][tx * 4 + 3]; } __syncthreads(); } if (row < M && col + 3 < N) { C[row * N + col + 0] = sum0; C[row * N + col + 1] = sum1; C[row * N + col + 2] = sum2; C[row * N + col + 3] = sum3; } } // 性能: ~1500 GFLOPS (比 V0 快 30×) ``` --- **V4:结合 Bank 冲突消除 + 双缓冲(提升 ~50×)** ```cpp // V4: 在 V3 基础上: // 1. 共享内存加 padding (TILE_PAD = TILE_SIZE + 1) 消除 Bank 冲突 // 2. 双缓冲:加载下一个 tile 的同时算当前 tile #define TILE_SIZE 32 #define TILE_PAD 33 // +1 消除 bank 冲突 __global__ void sgemm_v4(const float* __restrict__ A, const float* __restrict__ B, float* __restrict__ C, int M, int N, int K) { __shared__ float As[2][TILE_SIZE][TILE_PAD]; // 双缓冲 __shared__ float Bs[2][TILE_SIZE][TILE_PAD]; int bx = blockIdx.x, by = blockIdx.y; int tx = threadIdx.x, ty = threadIdx.y; int row = by * TILE_SIZE + ty; int col = (bx * TILE_SIZE + tx) * 4; float sum0 = 0.0f, sum1 = 0.0f, sum2 = 0.0f, sum3 = 0.0f; // 预加载第一个 tile (buffer 0) int buffer = 0; load_tile(A, B, As[0], Bs[0], row, col, M, N, K, 0, tx, ty); __syncthreads(); for (int t = 1; t < K / TILE_SIZE; t++) { int next_buf = 1 - buffer; // 加载下一个 tile (异步的硬件上可以与计算重叠) load_tile(A, B, As[next_buf], Bs[next_buf], row, col, M, N, K, t, tx, ty); // 同时计算当前 tile!(双缓冲精髓) compute_tile(As[buffer], Bs[buffer], tx, ty, sum0, sum1, sum2, sum3); __syncthreads(); buffer = next_buf; } // 最后一个 tile compute_tile(As[buffer], Bs[buffer], tx, ty, sum0, sum1, sum2, sum3); // 写回 if (row < M) { if (col + 0 < N) C[row * N + col + 0] = sum0; if (col + 1 < N) C[row * N + col + 1] = sum1; if (col + 2 < N) C[row * N + col + 2] = sum2; if (col + 3 < N) C[row * N + col + 3] = sum3; } } // 辅助函数(device 函数) __device__ void load_tile(const float* A, const float* B, float As[TILE_SIZE][TILE_PAD], float Bs[TILE_SIZE][TILE_PAD], int row, int col, int M, int N, int K, int t, int tx, int ty) { int a_col = t * TILE_SIZE + tx; if (row < M && a_col < K) As[ty][tx] = A[row * K + a_col]; else As[ty][tx] = 0.0f; int b_row = t * TILE_SIZE + ty; if (b_row < K) { for (int c = 0; c < 4; c++) { int b_col = col + c; Bs[ty][tx * 4 + c] = (b_col < N) ? B[b_row * N + b_col] : 0.0f; } } } __device__ void compute_tile(const float As[TILE_SIZE][TILE_PAD], const float Bs[TILE_SIZE][TILE_PAD], int tx, int ty, float& s0, float& s1, float& s2, float& s3) { #pragma unroll for (int k = 0; k < TILE_SIZE; k++) { float a = As[ty][k]; s0 += a * Bs[k][tx * 4 + 0]; s1 += a * Bs[k][tx * 4 + 1]; s2 += a * Bs[k][tx * 4 + 2]; s3 += a * Bs[k][tx * 4 + 3]; } } // 性能: ~2500 GFLOPS (比 V0 快 50×) ``` **优化总结**: ``` V0: 朴素全局内存 ~50 GFLOPS (基线) V1: + 合并访问 ~150 GFLOPS (×3) V2: + 共享内存tiling ~750 GFLOPS (×15) V3: + 向量化/寄存器复用 ~1500 GFLOPS (×30) V4: + Bank冲突消除+双缓冲 ~2500 GFLOPS (×50) ``` ### 8.3 性能分析工具:rocprofiler 快速上手指南 ```bash # 1. 基础性能统计 rocprof --stats ./my_program # 输出关键指标: # - SQ_WAVES: 活跃 Wavefront 数 # - VALUUtilization: 矢量 ALU 利用率 # - FetchSize: 全局内存读取量 # - MemUnitStalled: 因等待内存而停顿的周期比例 # 2. 时间线分析 rocprof --timestamp on -d ./profile_output ./my_program # 然后可视化(需 chrome://tracing) # hipMemcpy 和 kernel 执行的时间线一目了然 # 3. 看关键瓶颈 rocprof --stats ./my_program 2>&1 | grep -E "VALUUtil|FetchSize|MemUnitStalled|SQ_WAVES" ``` **性能解读速查**: | 指标 | 理想值 | 如果不对 → 问题 | | -------------------- | -------------- | ------------------------------------- | | `VALUUtilization` | > 80% | < 60% → ALU 在等数据 | | `MemUnitStalled` | < 20% | > 50% → 内存是瓶颈 | | `SQ_WAVES / CU` | 接近 10 (max) | < 5 → 寄存器/LDS 用太多,occupancy 低 | | `FetchSize / kernel` | 接近理论最小值 | 远大于 → 内存访问不合并 | --- ## 九、DCU vs NVIDIA CUDA——移植指南速查 如果你有 CUDA 代码,迁移到 DCU/HIP 几乎是一对一替换: | CUDA | HIP (DCU) | 注意事项 | | -------------------------- | ------------------------------------------------- | --------------------------------------- | | `cudaMalloc` | `hipMalloc` | 参数完全相同 | | `cudaMemcpy` | `hipMemcpy` | 枚举值 `cudaMemcpyH2D` → `hipMemcpyH2D` | | `cudaFree` | `hipFree` | | | `cudaDeviceSynchronize` | `hipDeviceSynchronize` | | | `__global__` | `__global__` | 完全一致 | | `__shared__` | `__shared__` | 完全一致 | | `__constant__` | `__constant__` | | | `threadIdx` | `threadIdx` | 完全一致 | | `blockIdx` | `blockIdx` | 完全一致 | | `blockDim` | `blockDim` | 完全一致 | | `kernel<<>>` | `hipLaunchKernelGGL(kernel,g,b,s,stream,args...)` | HIP 启动方式不同 | | `__syncthreads()` | `__syncthreads()` | 完全一致 | | Warp = 32 | Wavefront = 64 | 锁步大小不同,注意分支分歧模式 | | `nvcc` | `hipcc` | 编译器不同 | | `nvprof` | `rocprofiler` | 性能分析工具不同 | **批量替换脚本**: ```bash # 一键替换 CUDA API → HIP API sed -i 's/cudaMalloc/hipMalloc/g' *.cpp *.cu sed -i 's/cudaMemcpy/hipMemcpy/g' *.cpp *.cu sed -i 's/cudaMemcpyHostToDevice/hipMemcpyHostToDevice/g' *.cpp *.cu sed -i 's/cudaMemcpyDeviceToHost/hipMemcpyDeviceToHost/g' *.cpp *.cu sed -i 's/cudaFree/hipFree/g' *.cpp *.cu sed -i 's/cudaDeviceSynchronize/hipDeviceSynchronize/g' *.cpp *.cu sed -i 's/cudaGetErrorString/hipGetErrorString/g' *.cpp *.cu # 注意:kernel 启动语法需要手动改! ``` --- ## 十、总结 + 学习路线 ### 知识全景 ``` DCU = 国产 GPGPU 加速卡 硬件: CU阵列(算力) + HBM2(显存) + DMA(搬运) + PCI-E/xGMI(互联) 编程: HIP ≈ CUDA 语法,加 hippify 工具可自动转换 优化阶梯: 合并访问 → 共享内存 → 向量化 → Bank修补 → 双缓冲 工具: hipcc(编译) + rocprofiler(分析) + roctracer(跟踪) 库: hipBLAS/MIOpen/RCCL → 90%场景不用手写kernel ``` ### 推荐学习路径 ``` 第1天: 装DTK → 跑向量加法demo → 改参数看性能变化 第2天: 写归约 → 理解共享内存和__syncthreads() 第3天: 写矩阵乘法V0→V2 → 感受共享内存tiling的威力 第4天: 学rocprofiler → 分析自己的代码瓶颈 第5天: 调库: hipBLAS做矩阵乘,MIOpen做卷积 第6天: 挑战: 矩阵乘法V3→V4 → 追求极致性能 第7天: 实战: 用RCCL写多卡程序 ``` --- 📖 **上篇回顾**:[DCU入门(上):硬件原理与内存体系](./DCU入门(上):硬件原理与内存体系.md)(DCU 硬件架构 · CU/Wavefront · 五级内存 · HBM2 · DMA) 如果觉得有帮助,欢迎 **点赞 + 收藏**,有问题评论区见。 下一篇打算写《DCU 共享内存与 Bank 冲突深度实战》,用真实的 rocprofiler 报告 + 分步优化记录,讲讲怎么把矩阵乘法从 750 GFLOPS 再提一档到 2000+。 **有什么问题欢迎评论区交流~**
全部评论(1)
只看楼主
暂无数据
--
楼主
Level.4
0 帖子
楼主热帖
暂无数据