1
DCU技术达人
从CUDA迁移到DTK:一个后端开发踩过的坑和真相
2026-07-07 10:44:33
86
# 从CUDA迁移到DTK:一个后端开发踩过的坑和真相 > 花了三周把3000行CUDA代码移植到DTK上,GPU利用率从92%掉到47%。排查了一个通宵才发现,问题不在代码逻辑——在线程束大小和同步模型上,这俩家伙根本不是一回事。 --- ## 一、先搞明白硬件:DCU到底长什么样 DCU(Deep Computing Unit)采用两级缓存的SIMT架构,核心计算单元叫Compute Unit(CU),每个CU包含4个SIMD。这和NVIDIA GPU的SM(Streaming Multiprocessor)在大框架上类似,但有个关键差异: **Warp Size不同。** - NVIDIA GPU:Warp Size = 32 - DCU:Warp Size = 64 意味着什么?线程束内的线程同时执行同一条指令,你的代码如果按32对齐优化,在DCU上可能只跑了一半的效率。 每个SIMD内部结构:64线程组成一个wavefront在1个SIMD上执行;64线程 × 256 × 32bit寄存器(VGPR),每个线程最多256个寄存器;资源充足时每个SIMD最多支持10个wavefront并发。 ```mermaid graph TB subgraph DCU["DCU 芯片"] subgraph CU0["Compute Unit 0"] SIMD0["SIMD 0
64 Threads / Wavefront"] SIMD1["SIMD 1
64 Threads / Wavefront"] SIMD2["SIMD 2
64 Threads / Wavefront"] SIMD3["SIMD 3
64 Threads / Wavefront"] VGPR0["VGPR
256×32bit ×64"] SGPR0["SGPR
800×32bit"] SALU0["SALU"] LDS0["LDS 64KB"] L1_Cache0["Texture R/W Cache L1"] end CU1["Compute Unit 1
(结构同CU0)"] CU_N["... 更多 Compute Unit"] GDS["GDS 64KB
所有CU共享"] L2["L2 Read/Write Cache
Per Memory Channel"] CrossBar["CrossBar 互联"] end CU0 --> CrossBar CU1 --> CrossBar CU_N --> CrossBar CrossBar --> L2 CrossBar --> GDS style DCU fill:#f5f5f5,stroke:#1565c0,stroke-width:2px style CU0 fill:#e3f2fd,stroke:#1976d2 style GDS fill:#fff3e0,stroke:#ef6c00 style L2 fill:#e8f5e9,stroke:#2e7d32 ``` 除了CU和SIMD,DCU还包含: - **LDS**(Local Data Share):每个CU 64KB,类似NVIDIA的shared memory - **GDS**(Global Data Share):64KB,所有CU共享 - **L1/L2 Cache**:每CU有Texture R/W Cache L1,每Memory Channel有L2 R/W Cache > 说白了:DCU在硬件架构上和NVIDIA GPGPU基本同源,但线程束大小翻倍是迁移时第一个要盯住的变量。 DCU系列经历了多代产品迭代,从初代产品打通GPGPU生产研发流程,到后续逐步扩充计算单元、显存参数,再到新增TensorCore功能提升单精度和半精度计算速率,以及增加节点内数据高速互联、视频编解码器等。整个软件生态目前已支持主流AI框架和HPC应用。 ```mermaid timeline title DCU系列产品迭代路线 section 早期产品 初代 : 实现国产化GPGPU
生产研发流程打通 section 中期演进 第二代 : 扩充计算单元与显存参数 第三代 : 新增数学计算指令
计算资源翻倍 section 近期发展 AI增强代 : 新增TensorCore功能
提高单精/半精计算速率 最新代 : 节点内高速互联
视频编解码器
进一步扩充计算单元 ``` --- ## 二、软件栈对比:DTK到底兼容到什么程度 DTK(DCU Toolkit)是DCU硬件平台的开发工具包。官方说法是同时支持HIP和CUDA两种编程模型,能快速将CUDA和ROCm生态中的应用部署到DCU上。**说白了就是:你写HIP能跑,写CUDA也能跑(通过GPUfusion兼容层)。** ### 2.1 软件栈全景 DTK各组件对ROCm生态原生兼容,与CUDA生态高度兼容。 ```mermaid graph TB subgraph APP["HPC & AI 广泛应用"] AI["AI框架"] HPC["HPC应用"] SCI["科学计算"] end subgraph MATH["数学库层"] DTK_MATH["DTK数学库
rocblas/rocsparse/
rocsolver/rocrand/rocfft"] HIP_MATH["HIP数学库"] CUDA_MATH["CUDA兼容数学库
cublas/cusparse/
cusolver/curand/cufft"] end subgraph COMPILER["编译器层"] DCC["dcc/clang
(DTK编译器)"] HIPCC["hipcc"] NVCC["nvcc/clang
(CUDA兼容)"] end subgraph RUNTIME["运行时 & 库"] LIBS_DTK["librocsm_smi64.so
libhiprtc.so
等动态库"] LIBS_CUDA["libnvidia-ml.so
libnvrtc.so
libcudart.so
等动态库"] HEADERS_DTK["HIP头文件"] HEADERS_CUDA["CUDA头文件"] end subgraph OS["多操作系统支持"] CENTOS["CentOS"] UBUNTU["Ubuntu"] KYLIN["Kylin"] UOS["UOS"] NFS["NFS"] end subgraph HW["硬件平台"] CPU["CPU
Intel / AMD / Hygon / ARM"] DCU_HW["DCU 加速器"] end APP --> MATH MATH --> COMPILER COMPILER --> RUNTIME RUNTIME --> OS OS --> HW style APP fill:#e8eaf6,stroke:#283593 style MATH fill:#e3f2fd,stroke:#1565c0 style COMPILER fill:#e8f5e9,stroke:#2e7d32 style RUNTIME fill:#fff3e0,stroke:#ef6c00 style OS fill:#fce4ec,stroke:#c62828 style HW fill:#f3e5f5,stroke:#6a1b9a ``` 核心模块对照表: | 模块 | CUDA | ROCm | DTK | DTK-CUDA兼容 | | -------------- | ------------------------------- | ----------------------------------- | ----------------------------------- | ------------------------------- | | Driver API | cuda | hsa-runtime | hsa-runtime | cuda | | 运行时系统 | cudart | amdhip64 | galaxy | cudart | | 数学库 | cublas/cusparse/cusolver/curand | rocblas/rocsparse/rocsolver/rocrand | rocblas/rocsparse/rocsolver/rocrand | cublas/cusparse/cusolver/curand | | FFT | cufft | rocfft | rocfft | cufft | | AI算子库 | cuDNN | miopen | miopen | cuDNN | | 设备管理 | nvml | rocm-smi | rocm-smi | nvml | | 头文件库 | Thrust/cub | Thrust/rocprim | Thrust/rocprim | Thrust/cub | | 编译器 | nvcc | hipcc/clang | dcc/clang | nvcc/clang | | 性能调优 | nvprof | rocprofiler | Hipprof | hipprof | | 调试器 | cuda-gdb | rocgdb | hipgdb | hipgdb | | 内存检查 | cuda-memcheck | radeon_memory_visualizer | Hipprof --leakcheck | hipprof | | OpenACC/OpenMP | — | 仅OpenMP | 支持 | 支持 | ### 2.2 运行时功能差异 | 功能 | CUDA | ROCm | DTK | | -------- | ---- | ---------------- | ------------------------------- | | 纹理对象 | 支持 | 支持(仅单进程) | 支持(通用计算单元实现) | | 协作组 | 支持 | 部分支持 | 部分支持(cluster等硬件不支持) | | 动态并行 | 支持 | 不支持 | 不支持(研发中) | ### 2.3 API命名规范 | API类型 | CUDA | ROCm/DTK | | ----------- | ---------- | --------- | | Driver API | `cu`开头 | `hip`开头 | | Runtime API | `cuda`开头 | `hip`开头 | > **坑就在这里**:如果你原来的CUDA代码大量用了动态并行(Dynamic Parallelism),目前DTK不支持,需要重构这部分逻辑。 --- ## 三、HIP编程模型:C++扩展的三板斧 HIP并行编程模型是对C/C++的扩展,为加速器提供可移植的异构并行编程环境。核心就三件事:**核函数、线程结构、存储结构**。 ### 3.1 核函数 Kernel 把一段要在设备端并行执行的代码用 `__global__` 标记,这就是核函数。 ```cpp // 设备端核函数 __global__ void myKernel(int N, double *d_a) { int i = threadIdx.x + blockIdx.x * blockDim.x; if (i < N) { d_a[i] *= 2.0; } } // 主机端调用 myKernel<<>>(N, d_a); ``` 核函数的规矩: - 返回值**必须是 void** - 参数可以是设备端指针、立即数、结构体或引用 - 以线程为基本单位执行,线程之间总是"并发"的 - 每个线程有独立编号 HIP核函数启动语法有三种写法(本质上等价): | 方式 | 语法 | | --------------- | ------------------------------------------------------------ | | HIP宏 | `hipLaunchKernelGGL(kernel, numBlocks, dimBlocks, sharedMemBytes, stream, args...)` | | HIP Kernel语法 | `kernel<<>>(args...)` | | CUDA Kernel语法 | `kernel<<>>(args...)` | CUDA与HIP核函数基本完全兼容——除了目前DTK中Dynamic Parallelism还在研发中。 ### 3.2 线程结构:理解了Grid/Block/Wavefront才算入门 线程层次结构是异构编程的灵魂。HIP和CUDA在线程组织上的对应关系: | NVIDIA GPU | HIP | 描述 | | ----------------------------- | ----------------- | ------------------------------------------------------------ | | Streaming Multiprocessor (SM) | Compute Unit (CU) | 多组并行计算单元组成的计算结构,一个block内所有thread分配到同一个CU | | Kernel | Kernel | 核函数,可被多个CU并发执行 | | Warp | Wavefront | 线程束/波前,硬件执行基本单元。DTK线程束大小=64 | | Thread | Thread | 执行核函数的基本单元 | | Block | Block | 线程块,由一个CU执行。块内线程可直接通信和同步 | ```mermaid graph TB subgraph GRID["Grid(线程网格)"] subgraph BLOCK0["Block 0"] TH0["Thread
threadIdx=0"] TH1["Thread
threadIdx=1"] TH2["Thread
..."] THN["Thread
threadIdx=N"] end subgraph BLOCK1["Block 1"] TH10["Thread"] TH11["Thread"] TH12["..."] end BLOCK_M["Block M"] end CU0["Compute Unit 0
(执行Block 0)"] CU1["Compute Unit 1
(执行Block 1)"] BLOCK0 --> CU0 BLOCK1 --> CU1 BLOCK_M --> CU1 style GRID fill:#e3f2fd,stroke:#1565c0,stroke-width:2px style BLOCK0 fill:#bbdefb,stroke:#1976d2 style BLOCK1 fill:#bbdefb,stroke:#1976d2 style CU0 fill:#e8f5e9,stroke:#2e7d32 style CU1 fill:#e8f5e9,stroke:#2e7d32 ``` 核函数中通过内置变量获取线程在3D-Grid中的位置: | 含义 | HIP变量 | CUDA变量 | | ----------------- | -------------------- | ----------------- | | block内thread位置 | `hipThreadIdx_x/y/z` | `threadIdx.x/y/z` | | grid内block位置 | `hipBlockIdx_x/y/z` | `blockIdx.x/y/z` | | block维度 | `hipBlockDim_x/y/z` | `blockDim.x/y/z` | | grid维度 | `hipGridDim_x/y/z` | `gridDim.x/y/z` | **1D线程网格**——计算全局索引: ```cpp int idx = blockDim.x * blockIdx.x + threadIdx.x; ``` **2D线程网格**——需要同时计算X和Y: ```cpp int idx_x = blockDim.x * blockIdx.x + threadIdx.x; int idx_y = blockDim.y * blockIdx.y + threadIdx.y; ``` 搞懂了1D和2D,3D加个Z轴同理。并行算法的核心就是把计算任务映射到这个3D-Grid上——这个映射关系选对了,性能提升往往是数量级的。 ```mermaid graph LR subgraph GRID2D["2D Grid"] subgraph B00["Block(0,0)"] T00["T00"] --- T01["T01"] --- T02["T02"] end subgraph B01["Block(0,1)"] T10["T10"] --- T11["T11"] --- T12["T12"] end subgraph B10["Block(1,0)"] T20["T20"] --- T21["T21"] --- T22["T22"] end subgraph B11["Block(1,1)"] T30["T30"] --- T31["T31"] --- T32["T32"] end end style GRID2D fill:#e8eaf6,stroke:#283593,stroke-width:2px style B00 fill:#c5cae9,stroke:#3949ab style B01 fill:#c5cae9,stroke:#3949ab style B10 fill:#c5cae9,stroke:#3949ab style B11 fill:#c5cae9,stroke:#3949ab ``` ### 3.3 存储结构:三层内存模型 HIP存储结构是层次化的,直接对应硬件存储层次。**搞懂了这个,一大半的性能问题都能自己排查了。** ```cpp __device__ int array_on_global[10]; // 全局内存,所有线程可访问 __global__ void MyKernel(int *array, int arrayCount) { int idx = blockDim.x * blockIdx.x + threadIdx.x; __shared__ int array_on_lds[10]; // 共享内存/LDS,block内可见 array_on_global[0] = idx; } ``` ```mermaid graph TB subgraph GRID_MEM["Grid 级别"] GM["Global Memory(全局内存 HBM)
__device__ 修饰
所有线程可访问 | 应用生命周期"] CM["Constant Memory(常量内存)
__constant__ 修饰
所有线程可读 | 应用生命周期"] end subgraph BLOCK_MEM["Block 级别"] LDS["LDS / Shared Memory(共享内存)
__shared__ 修饰
Block内可见 | Block生命周期"] end subgraph THREAD_MEM["Thread 级别"] REG["寄存器 Register
float var(标量)
仅本线程 | 线程生命周期"] LM["本地内存 Local Memory
float var[100](数组溢出)
仅本线程 | 线程生命周期"] end THREAD_MEM --> BLOCK_MEM --> GRID_MEM style GM fill:#ffcdd2,stroke:#c62828 style CM fill:#ffcdd2,stroke:#c62828 style LDS fill:#fff9c4,stroke:#f9a825 style REG fill:#c8e6c9,stroke:#2e7d32 style LM fill:#c8e6c9,stroke:#2e7d32 ``` 存储层次速查表: | 修饰符 | 存储器 | 作用域 | 生命周期 | | ---------------- | ------------ | ------ | -------- | | (无修饰符)标量 | 寄存器 | 线程 | 线程 | | (无修饰符)数组 | 本地内存 | 线程 | 线程 | | `__shared__` | LDS/共享内存 | Block | Block | | `__device__` | 全局内存 | 全局 | 应用程序 | | `__constant__` | 常量内存 | 全局 | 应用程序 | 函数修饰符: | 限定符 | 执行位置 | 调用来源 | | ------------ | -------- | ------------------------ | | `__global__` | 设备端 | 主机端调用(设备端也可) | | `__device__` | 设备端 | 仅设备端 | | `__host__` | 主机端 | 仅主机端 | `__device__` 和 `__host__` 可以一起用,函数会在主机和设备端各编译一份。 ### 3.4 内置函数:同步与原子操作 **原子操作**:支持 `atomicAdd/Sub/Exch/Min/Max/CAS/And/Or/Xor` 等,覆盖 `int/unsigned int/long long/float/double`。`atomicInc`/`atomicDec` 目前用 `atomicCAS` 替代实现,性能会差一些。 Scope语义: | CUDA scope | HIP宏 | 含义 | | --------------------- | ------------------------------ | ----------- | | `thread_scope_system` | `__HIP_MEMORY_SCOPE_SYSTEM` | 系统级可见 | | `thread_scope_device` | `__HIP_MEMORY_SCOPE_AGENT` | 设备级可见 | | `thread_scope_block` | `__HIP_MEMORY_SCOPE_WORKGROUP` | Block内可见 | **同步函数**——这里有个重要差异: | 函数 | DTK | CUDA | 原因 | | ------------------------------ | ------ | ---- | --------------------------------- | | `__syncwarp()` | 不支持 | 支持 | DCU硬件一个线程束共用一个PC寄存器 | | `__syncthreads()` | 支持 | 支持 | — | | `__shfl_down/up/sync` 系列 | 支持 | 支持 | 调对应shfl实现 | | `__ballot_sync` 系列 | 支持 | 支持 | — | | `__reduce_add/min/max_sync` 等 | 不支持 | 支持 | — | > **NVIDIA Volta后每个线程有独立PC寄存器,能做独立线程调度。DCU目前一个线程束才一个PC,所以 `__syncwarp` 这套玩不转。** 另外DCU不支持异步操作(Asynchronous),需要异步的场景目前用同步替代。 --- ## 四、并行编程接口:Stream、Event、Memory ### 4.1 阻塞与非阻塞 ```cpp // 阻塞拷贝——完成前主机端暂停 hipMemcpy(d_a, h_a, Nbytes, hipMemcpyHostToDevice); // 非阻塞拷贝——立即返回,需配合stream hipMemcpyAsync(d_a, h_a, Nbytes, hipMemcpyHostToDevice, stream); // 核函数启动——非阻塞,立即返回 hipLaunchKernelGGL(myKernel, grid, block, 0, stream, N, d_a); ``` ### 4.2 Stream Stream是一种逻辑队列。任何对设备的操作(kernel、memcpy)都通过队列执行。同一队列顺序执行,不同队列共享硬件但互不影响。 ```cpp hipStream_t stream; hipStreamCreate(&stream); // ... 提交任务到stream ... hipStreamDestroy(stream); ``` **注意**:使用NULL或0表示默认流。对默认流的操作会导致强制同步,影响其他流中的任务。 ### 4.3 Pinned Memory 主机端默认分配的是Pageable内存。Pinned Memory(Page-Lock Memory)让DMA直接访问,大幅提升Host↔Device传输带宽。 ```cpp double *h_a = NULL; hipHostMalloc(&h_a, Nbytes); // 分配pinned内存 // ... 使用 ... hipHostFree(h_a); // 释放 ``` 频繁传输的数据优先用Pinned Memory。带宽差距非常显著,这个优化是性价比最高的。 ### 4.4 同步 ```cpp hipDeviceSynchronize(); // 重型同步:阻塞主机直到所有设备流完成 hipStreamSynchronize(stream); // 轻量同步:仅等待指定流 ``` ### 4.5 Event ```cpp hipEvent_t startEvent, endEvent; hipEventCreate(&startEvent); hipEventCreate(&endEvent); hipEventRecord(startEvent, stream); // ... 在stream中执行任务 ... hipEventRecord(endEvent, stream); hipEventSynchronize(endEvent); float time_ms; hipEventElapsedTime(&time_ms, startEvent, endEvent); // 精确计时 // 流间依赖 hipStreamWaitEvent(stream2, endEvent, 0); // stream2等endEvent完成后再执行 ``` ### 4.6 多设备编程 ```cpp int canAccessPeer; hipDeviceCanAccessPeer(&canAccessPeer, device0, device1); if (canAccessPeer) { hipDeviceEnablePeerAccess(device1, 0); // ... P2P内存访问 ... hipDeviceDisablePeerAccess(device1); } ``` 每个设备最多支持8个对等连接。P2P访问可以通过 `hipMemcpyPeerAsync` 实现设备间直接拷贝。 --- ## 五、Runtime日志与异常调试 ### 5.1 Runtime日志 出问题时开启Runtime日志做初步分析: ```bash # 方式一:环境变量 export HIP_LOG_LEVEL=4 # 1:error 2:warning 3:info 4:debug export HIP_MODULE_MASK=0x7fffffff export HIP_ENABLE_LIST=hip export HIP_LOG_OUTPUT_PATH=/home/ # 不设置默认在可执行程序当前路径 # 方式二:命令行直接设置 HIP_LOG_LEVEL=4 HIP_MODULE_MASK=0x7fffffff HIP_ENABLE_LIST=hip ./your_program # 解析日志 hiplogdump sort *.nano > log.txt ``` ### 5.2 VMFault异常分析 Kernel非法地址访问是最常见的坑。错误信息类似: ``` Invalid address access: 0x2ad4e9000000, Error code: 3. ``` VMFault信息里几个关键字段: | 字段 | 含义 | | ------------------------- | ---------------------------------------------- | | Invalid address access | 非法访问的具体地址 | | Error code: 3 | 非法内存地址访问 | | DUMP KERNEL AQL PACKET | workgroup和grid参数,grid = blockDim × gridDim | | FIND MATCH KERNEL COMMAND | 被C++修饰过的kernel名,用`c++filt`命令还原 | | DUMP KERNEL ARGS | kernel参数内容(size + 16进制堆参数) | > 调试建议:设备端函数用hipgdb,core文件用hipprof工具。 --- ## 六、GDS:GPU直通存储 GDS(GPU Direct Storage)让DCU直接读写存储设备,绕过CPU内存中转。 ```mermaid graph TB subgraph APP_GDS["应用程序"] HYFILES["hyfiles.so
文件操作库"] end subgraph KERNEL["内核层"] HYFS["hyfs.ko
内核驱动模块"] end subgraph STORAGE["存储层"] EXT4["EXT4"] XFS["XFS"] SAMBA["SAMBA"] end HYFILES -->|"会话管理
文件句柄注册/注销
设备Buffer管理
直通读写(cuFileRead/Write)"| HYFS HYFS -->|"直通内存管理(锁页/map/unmap)
直通IO管理
地址转换(影子页→设备地址)"| STORAGE style APP_GDS fill:#e3f2fd,stroke:#1565c0 style KERNEL fill:#fff3e0,stroke:#ef6c00 style STORAGE fill:#e8f5e9,stroke:#2e7d32 ``` DTK GDS核心组件:**hyfiles.so**(文件操作库)负责会话管理、文件句柄注册、设备Buffer管理、直通读写;**hyfs.ko**(内核模块)负责直通内存管理、IO管理、地址转换。 与CUDA GDS对比:基础读写(`cuFileRead`/`cuFileWrite`)、文件句柄注册/注销、设备内存注册/注销均已实现。异步读写、批量IO、Stream注册等高级功能暂未实现。生态方面DTK GDS目前支持EXT4/XFS/SAMBA文件系统。 --- ## 七、设备监控:hy-smi 对标 `nvidia-smi`,DTK提供 `hy-smi`: ```bash hy-smi # 显示卡信息(等同于rocm-smi) hy-smi --showhw # 硬件详细信息 hy-smi --showuse # DCU使用率 hy-smi --showtemp # 温度 hy-smi --showpower # 平均功耗 hy-smi --showmemuse # 显存使用率 hy-smi --showclocks # 当前时钟频率 hy-smi --showperflevel # 性能等级 hy-smi --showserial # 序列号 hy-smi --forchip # 简洁格式显示重要信息 ``` 常用的就上面这些,更多参数(如风扇、电压、PCIe带宽、ECC信息等)可以通过 `hy-smi --help` 查看。 --- ## 八、HMM:异构内存管理 HMM(Heterogeneous Memory Management)管理CPU和GPU之间的内存互访和迁移。 当前DTK HMM支持情况: - **高版本内核**:完整支持 - **低版本内核**:仅支持 `hipMallocManaged` 申请的内存 > 如果你的生产环境内核版本不够,HMM功能是受限的,很多高级特性用不了。升级内核是迁移DCU的前置动作,别等代码写完了才发现。 --- ## 九、CUDA程序迁移:两条路 DTK环境下迁移CUDA程序有两种方式: | 方式 | 说明 | 推荐度 | | -------------------------- | ------------------------------------------------------------ | ---------------- | | **hipify**(自动移植工具) | 将CUDA代码自动转成HIP代码。未来将停止维护 | 不推荐新项目使用 | | **GPUfusion**(兼容层) | 无需修改程序,加载环境变量 `source dtk/cuda/env.sh` 即可直接跑CUDA程序 | 主推荐 | GPUfusion是目前的主推方案,不改一行代码就能在DCU上跑CUDA程序。但性能需要实测——别指望零成本迁移还能跑出一样的利用率。 --- ## 写在最后 从CUDA迁移到DTK,搞定了这几个点就能少踩80%的坑: 1. **Warp Size从32变64**——线程块大小、共享内存bank冲突策略全部要重新评估 2. **动态并行不支持**——kernel嵌套调用的逻辑需要展平,别硬搬 3. **`__syncwarp`不支持**——线程束级细粒度同步得重新设计 4. **异步操作不支持**——异步拷贝等场景用同步替代,IO模式需要调整 5. **Pinned Memory一定要用**——Host↔Device带宽差距巨大,这步不做纯属浪费硬件 > 迁移不是简单的API替换。把硬件差异吃透了,才能在DCU上写出真正跑得快的代码。
全部评论(0)
只看楼主
暂无数据
--
楼主
Level.2
0 帖子
楼主热帖
暂无数据