# 调试模块DFX
## 硬件背景
**device_print**:
device_print是Triton框架在昇腾NPU上提供的一个设备端调试工具,允许开发者在算子内核执行过程中直接打印标量/矢量信息,核心流程如下:
```mermaid
flowchart LR
subgraph Host[Host 侧流程]
A[Host Launcher] -->|1. 传递打印缓冲区| B[Kernel 执行]
B -->|2. Kernel 返回| C[读取缓冲区]
C -->|3. 解析并打印| D[终端输出]
end
subgraph Code[代码实现]
E[BiSheng 头文件
内置打印逻辑] -->|自动抽取| F[triton-ascend
集成]
F -->|调用| A
end
```
**关键硬件资源约束**:
- UB打印缓冲区:每个aicore固定分配16 KB空间用于数据暂存,且同一个aicore内的所有打印操作共享这16 KB缓冲区,写满后新数据提示warning,大小超过缓冲区最大值后丢弃。
- 多核并发:每个aicore独立执行内核代码,最终Host侧呈现每个核的打印结果。
## 算法原理
实现原理涉及Triton Ascend、AscendNPU IR、毕昇编译器三部分配合,实现该功能主要以AscendNPU IR为重点展开说明。
### Triton Ascend
生成初始`.ttadapter` IR过程中会将triton侧的`tl.device_print`转换成`func.call @triton_print_*`接口。
### AscendNPU IR
接收到`.ttadapter` IR之后在AscendNPU IR阶段主要会经历如下变换:
#### AdaptTritonKernel
将`func.call @triton_print_*`接口转换成`hfusion.print`接口。
```mlir
// Before AdaptTritonKernel
%reinterpret_cast = memref.reinterpret_cast %arg2 to offset: [0], sizes: [8], strides: [1] : memref to memref<8xi64, strided<[1]>>
%alloc = memref.alloc() : memref<8xi64>
memref.copy %reinterpret_cast, %alloc : memref<8xi64, strided<[1]>> to memref<8xi64>
%0 = bufferization.to_tensor %alloc restrict writable : memref<8xi64>
call @triton_print_0(%0) : (tensor<8xi64>) -> ()
// After AdaptTritonKernel
%reinterpret_cast = memref.reinterpret_cast %arg2 to offset: [0], sizes: [8], strides: [1] : memref to memref<8xi64, strided<[1]>>
%alloc = memref.alloc() : memref<8xi64>
memref.copy %reinterpret_cast, %alloc : memref<8xi64, strided<[1]>> to memref<8xi64>
%0 = bufferization.to_tensor %alloc restrict writable : memref<8xi64>
hfusion.print " x: " {hex = false} %0 : tensor<8xi64>
```
#### HFusionToHIVM
将`hfusion.print`接口转换成`hivm.hir.debug`接口。
```mlir
// Before ConvertHFusionToHIVM
%reinterpret_cast = memref.reinterpret_cast %arg3 to offset: [0], sizes: [8], strides: [1] : memref to memref<8xi64, strided<[1]>>
%alloc = memref.alloc() : memref<8xi64>
memref.copy %reinterpret_cast, %alloc : memref<8xi64, strided<[1]>> to memref<8xi64>
%0 = bufferization.to_tensor %alloc restrict writable : memref<8xi64>
hfusion.print " x: " {hex = false} %0 : tensor<8xi64>
// After ConvertHFusionToHIVM
%reinterpret_cast = memref.reinterpret_cast %arg3 to offset: [0], sizes: [8], strides: [1] : memref to memref<8xi64, strided<[1]>>
%alloc = memref.alloc() : memref<8xi64>
memref.copy %reinterpret_cast, %alloc : memref<8xi64, strided<[1]>> to memref<8xi64>
%0 = bufferization.to_tensor %alloc restrict writable : memref<8xi64>
hivm.hir.debug {debugtype = "print", hex = false, prefix = " x: ", tcoretype = #hivm.tcore_type} %0 : tensor<8xi64>
```
#### InlineFixpipe
插入fixpipe以用于`hivm.print`,该`hivm.print`会打印`mmad`结果,而`mmad`结果是`scf.for`中的`yield`。
```mlir
// Before InlineFixpipe
%init = tensor.empty()
%res = scf.for iter_arg(%arg = %init) {
%t = hivm.mmadL1 ins() outs(%arg)
hivm.print %t
scf.yield %t
}
// After InlineFixpipe
%init = tensor.empty()
%res = scf.for iter_arg(%arg = %init) {
%t = hivm.mmadL1 ins() outs(%arg)
%fixpipe = hivm.fixpipe int(%t)
hivm.print %fixpipe
scf.yield %t
}
```
#### InsertNZ2NDForDebug
`device_print`仅支持UB/GM上的数据打印,因此当打印L1的数据时,需要将数据先从L1搬至GM。该Pass的作用就是:当识别到`hivm::MmadL1Op`时,检查该op的输入;若输入被`hivm::DebugOp`用到,则需要申请一块`workspace`的大小,然后插入`NZ2ND`的op,确保搬至GM打印。
```mlir
// Before InsertNZ2NDForDebug
%12 = bufferization.to_tensor %alloc restrict writable : memref<1x4xf32>
%13 = arith.index_cast %arg8 : i32 to index
%14 = arith.index_cast %5 : i32 to index
%reinterpret_cast_0 = memref.reinterpret_cast %arg4 to offset: [%14], sizes: [4, 1], strides: [%13, 1] : memref to memref<4x1xf32, strided<[?, 1], offset: ?>>
%alloc_1 = memref.alloc() : memref<4x1xf32>
hivm.hir.load ins(%reinterpret_cast_0 : memref<4x1xf32, strided<[?, 1], offset: ?>>) outs(%alloc_1 : memref<4x1xf32>) init_out_buffer = false may_implicit_transpose_with_last_axis = false
%15 = bufferization.to_tensor %alloc_1 restrict writable : memref<4x1xf32>
%16 = arith.muli %8, %arg8 : i32
%17 = arith.index_cast %16 : i32 to index
%18 = arith.addi %17, %14 : index
%19 = tensor.empty() : tensor<1x1xf32>
%c1 = arith.constant 1 : index
%c4 = arith.constant 4 : index
%c1_2 = arith.constant 1 : index
%20 = hivm.hir.mmadL1 {fixpipe_already_inserted = true} ins(%12, %15, %true, %c1, %c4, %c1_2 : tensor<1x4xf32>, tensor<4x1xf32>, i1, index, index, index) outs(%19 : tensor<1x1xf32>) -> tensor<1x1xf32>
hivm.hir.debug {debugtype = "print", hex = false, prefix = " a_vals: ", tcoretype = #hivm.tcore_type} %12 : tensor<1x4xf32>
// After InsertNZ2NDForDebug
%12 = bufferization.to_tensor %alloc restrict writable : memref<1x4xf32>
%13 = memref_ext.alloc_workspace() : memref<1x4xf32>
%14 = bufferization.to_tensor %13 restrict writable : memref<1x4xf32>
%15 = hivm.hir.nz2nd ins(%12 : tensor<1x4xf32>) outs(%14 : tensor<1x4xf32>) -> tensor<1x4xf32>
%16 = arith.index_cast %arg8 : i32 to index
%17 = arith.index_cast %5 : i32 to index
%reinterpret_cast_0 = memref.reinterpret_cast %arg4 to offset: [%17], sizes: [4, 1], strides: [%16, 1] : memref to memref<4x1xf32, strided<[?, 1], offset: ?>>
%alloc_1 = memref.alloc() : memref<4x1xf32>
hivm.hir.load ins(%reinterpret_cast_0 : memref<4x1xf32, strided<[?, 1], offset: ?>>) outs(%alloc_1 : memref<4x1xf32>) init_out_buffer = false may_implicit_transpose_with_last_axis = false
%18 = bufferization.to_tensor %alloc_1 restrict writable : memref<4x1xf32>
%19 = arith.muli %8, %arg8 : i32
%20 = arith.index_cast %19 : i32 to index
%21 = arith.addi %20, %17 : index
%22 = tensor.empty() : tensor<1x1xf32>
%23 = hivm.hir.mmadL1 {fixpipe_already_inserted = true} ins(%12, %18, %true, %c1, %c4, %c1 : tensor<1x4xf32>, tensor<4x1xf32>, i1, index, index, index) outs(%22 : tensor<1x1xf32>) -> tensor<1x1xf32>
hivm.hir.debug {debugtype = "print", hex = false, prefix = " a_vals: ", tcoretype = #hivm.tcore_type} %15 : tensor<1x4xf32>
```
#### SplitMixKernel
`mix`类用例`Debug` op会在该Pass内先进行`InferCoreType`推断出精确的`coretype`(VECTOR/CUBE),默认是CUBE_OR_VECTOR,然后对`mix`函数进行拆分后生成纯`cube`函数和纯`vector`函数,这将决定`Debug` op最终在`cube`核上运行还是`vector`核上运行。
#### InsertInitAndFinishForDebug
若存在`Debug` op则将`hivm.hir.init_print`调用添加到每个函数开头,将`hivm.hir.finish_print`添加到每个`hivm.hir.print`之后。`hivm.hir.init_print`用于打印之前的准备工作,`hivm.hir.finish_print`用于打印之后的工作,目前没有特别具体作用,为将来扩展`device_print`预留了接口。
```mlir
// Before InsertInitAndFinishForDebug
hivm.hir.mmadL1 {fixpipe_already_inserted = true} ins(%cast, %cast_1, %true, %c1, %c4, %c1 : memref>, memref>, i1, index, index, index) outs(%cast_2 : memref>) sync_related_args(%c1_i64, %c0_i64, %c-1_i64, %c-1_i64, %c-1_i64, %c-1_i64, %c-1_i64 : i64, i64, i64, i64, i64, i64, i64)
hivm.hir.set_flag[, , ]
%16 = arith.index_cast %2 : i64 to index
%17 = affine.apply affine_map<()[s0] -> (s0 * 4)>()[%16]
%view = memref.view %arg2[%17][] : memref> to memref<1x1xf32, #hivm.address_space>
hivm.hir.wait_flag[, , ]
hivm.hir.fixpipe {enable_nz2nd} ins(%cast_2 : memref>) outs(%view : memref<1x1xf32, #hivm.address_space>)
hivm.hir.pipe_barrier[]
hivm.hir.sync_block_set[, , ] flag = 0 ffts_base_addr = %arg0
hivm.hir.debug {debugtype = "print", hex = false, prefix = " acc_11: ", tcoretype = #hivm.tcore_type} %view : memref<1x1xf32, #hivm.address_space>
// After InsertInitAndFinishForDebug
hivm.hir.init_debug
hivm.hir.mmadL1 {fixpipe_already_inserted = true} ins(%cast, %cast_1, %true, %c1, %c4, %c1 : memref>, memref>, i1, index, index, index) outs(%cast_2 : memref>) sync_related_args(%c1_i64, %c0_i64, %c-1_i64, %c-1_i64, %c-1_i64, %c-1_i64, %c-1_i64 : i64, i64, i64, i64, i64, i64, i64)
hivm.hir.set_flag[, , ]
%14 = arith.index_cast %0 : i64 to index
%15 = affine.apply affine_map<()[s0] -> (s0 * 4)>()[%14]
%view = memref.view %arg2[%15][] : memref> to memref<1x1xf32, #hivm.address_space>
hivm.hir.wait_flag[, , ]
hivm.hir.fixpipe {enable_nz2nd} ins(%cast_2 : memref>) outs(%view : memref<1x1xf32, #hivm.address_space>)
hivm.hir.pipe_barrier[]
hivm.hir.sync_block_set[, , ] flag = 0 ffts_base_addr = %arg0
hivm.hir.debug {debugtype = "print", finishInserted = 0 : i32, hex = false, prefix = " acc_11: ", tcoretype = #hivm.tcore_type} %view : memref<1x1xf32, #hivm.address_space>
hivm.hir.finish_debug
```
#### ConvertHIVMToStandard
将`hivm.hir.init_print`/`hivm.hir.print`/`hivm.hir.finish_print`转换为库函数调用。
#### ConvertHIVMToLLVM
`ConvertHIVMToLLVM`引入真正的库函数,并设置print相关函数的链接为`ExternWeak`(允许在多个llvm modules中重复定义)。
#### Debug op库实现
`op`库当前实现是通过scalar打印来实现的,通过`for`循环外抛的方式调用毕昇编译器提供的`cce::printf`接口进行scalar打印。
### 毕昇编译器
triton-ascend产生的Host侧launcher调用bisheng编译器编好的kernel并将打印缓冲区传给kernel,待kernel返回后在Host Launcher侧读取缓冲区并进行真正的打印。此部分代码在bisheng编译器自带的头文件中实现,并由triton-ascend自动从bisheng编译器的路径中抽取。
## 接口说明
通过设置环境变量`TRITON_DEVICE_PRINT=1`来开启该功能。开启后,triton-ascend侧会设置相关宏信息`__CCE_ENABLE_PRINT__`,该宏信息在毕昇编译器侧会影响是否开启打印。此外,编译`meta op`库的时候需开启`--cce-enable-print`(当前默认一直开启),以确保开启打印。
```mlir
// hfusion op接口
// dtype - 待打印tensor/scalar对应的数据类型
hfusion.print " prefix = xxx " {hex = xxx} %args : dtype
// hivm op接口
// tcoretype - 指明是在core核上运行还是vector核上运行(默认初始值为: CUBE_OR_VECTOR)
hivm.hir.debug {debugtype = "print", hex = xxx, prefix = " xxx: ", tcoretype = #hivm.tcore_type} %args : dtype
```
## 使用约束
| 适用硬件 | 使用约束 |
|--------|--------|
| - Ascend 950PR/Ascend 950DT
- Atlas A3训练系列产品/Atlas A3推理系列产品
- Atlas A2训练系列产品/Atlas A2推理系列产品
| 1. 打印对象仅支持张量、标量。
2. `device_print`打印缓冲区固定为16KB。
3. Triton内存检测工具sanitizer与`device_print`互斥,不可同时启用。
4. 编码规范:单个张量单独打印,打印指令紧跟目标张量,防止张量生命周期变动引发运行异常。
5. 内核限制:不允许待打印算子仅作为`device_print`唯一输入。
6. 循环限制:`while`循环内禁止打印循环体外定义的操作数。
7. 超时限制:打印等待内核完成超时时间10分钟,长耗时用例开启打印会触发超时失败。 |
| - Atlas A3训练系列产品/Atlas A3推理系列产品
- Atlas A2训练系列产品/Atlas A2推理系列产品
| 支持打印数据类型:`bool`、`int8`、`uint8`、`int16`、`uint16`、`int32`、`uint32`、`int64`、`bfloat16`、`half`、`float32`。 |
| - Ascend 950PR/Ascend 950DT
| 1. 数据类型兼容:兼容Atlas A3训练系列产品/Atlas A3推理系列产品全部类型,额外支持`fp8`。
2. 融合调度约束:插入`device_print`破坏VF融合边界情况下可能引发UB溢出,需减小tiling分块。
3. 缓存资源约束:打印`fp8`张量、L1张量边界情况下可能会引发UB溢出,需减小tiling分块规避缓存溢出。 |