问题概述
在 VPTO SIMTVF kernel 中,如果 scf.for 循环体内包含 scf.if 分支,编译后的 AICore 反汇编中可能出现带谓词的 END 指令,例如:
SIMTVF 的限制是:END 指令不应出现在分支路径中,也不应通过分支跳转后条件执行。否则未执行 END 的线程可能继续向后执行,硬件行为不可预期。
该现象发生在编译器处理 SIMTVF 控制流之后,生成代码不符合上述约束。
1. 最小复现用例
失败场景是:SIMTVF 内循环 16 次,但只有前 10 次写 UB。
完整 PTO 复现如下:
module attributes {pto.target_arch = "a5", pto.kernel_kind = #pto.kernel_kind<vector>} {
func.func @simt_vf_0(%arg0: !pto.ptr<i8, ub>, %arg1: f32, %arg2: i32)
attributes {pto.simt_entry, pto.simt_max_regs = 64 : i32} {
%tid = pto.get_tid_x : i32
%c0 = arith.constant 0 : index
%c16 = arith.constant 16 : index
%c1 = arith.constant 1 : index
%c0_i32 = arith.constant 0 : i32
%c1_i32 = arith.constant 1 : i32
%c2_i32 = arith.constant 2 : i32
%c10_i32 = arith.constant 10 : i32
%c512_i32 = arith.constant 512 : i32
%c8192_i32 = arith.constant 8192 : i32
%c21520_i32 = arith.constant 21520 : i32
%c4_i64 = arith.constant 4 : i64
%fpconst = arith.constant 1.250000e+00 : f32
%undef = llvm.mlir.undef : vector<2xf32>
%ins0 = "llvm.insertelement"(%undef, %fpconst, %c0_i32)
: (vector<2xf32>, f32, i32) -> vector<2xf32>
%val = "llvm.insertelement"(%ins0, %fpconst, %c1_i32)
: (vector<2xf32>, f32, i32) -> vector<2xf32>
%ub_int = "pto.castptr"(%arg0) : (!pto.ptr<i8, ub>) -> i64
%ub_llvm = "llvm.inttoptr"(%ub_int) : (i64) -> !llvm.ptr<6>
scf.for %i = %c0 to %c16 step %c1 {
%i32 = arith.index_cast %i : index to i32
%active = arith.cmpi slt, %i32, %c10_i32 : i32
scf.if %active {
%eid = arith.andi %arg2, %c1_i32 : i32
%ping = arith.muli %eid, %c8192_i32 : i32
%chunk = arith.muli %i32, %c512_i32 : i32
%base0 = arith.addi %ping, %chunk : i32
%tid2 = arith.muli %tid, %c2_i32 : i32
%base1 = arith.addi %base0, %tid2 : i32
%idx = arith.addi %base1, %c21520_i32 : i32
%idx_index = arith.index_cast %idx : i32 to index
%idx_i64 = arith.index_cast %idx_index : index to i64
%byte_off = arith.muli %idx_i64, %c4_i64 : i64
%ptr = "llvm.getelementptr"(%ub_llvm, %byte_off)
{elem_type = i8, rawConstantIndices = array<i32: -2147483648>}
: (!llvm.ptr<6>, i64) -> !llvm.ptr<6>
"llvm.store"(%val, %ptr) : (vector<2xf32>, !llvm.ptr<6>) -> ()
}
}
pto.syncthreads
return
}
func.func @main_kernel_kernel(%arg0: !pto.ptr<f32, gm>, %arg1: !pto.ptr<f32, gm>,
%arg2: !pto.ptr<f32, gm>, %arg3: !pto.ptr<f32, gm>,
%arg4: f32)
attributes {dyn_shared_memory_buf = 152640 : i64, pto.aicore} {
%c0_i64 = arith.constant 0 : i64
%ub_base = pto.castptr %c0_i64 : i64 -> !pto.ptr<i8, ub>
%bx = "pto.get_block_idx"() : () -> i64
%core = arith.trunci %bx : i64 to i32
%c0_i32 = arith.constant 0 : i32
%c1_i32 = arith.constant 1 : i32
%c64 = arith.constant 64 : index
%c256_i32 = arith.constant 256 : i32
%c5120_i32 = arith.constant 5120 : i32
%c8192_i32 = arith.constant 8192 : i32
%c21520_i32 = arith.constant 21520 : i32
%c327680_i32 = arith.constant 327680 : i32
%c4 = arith.constant 4 : index
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%c1_i64 = arith.constant 1 : i64
%c20480_i64 = arith.constant 20480 : i64
"pto.set_flag"() {src_pipe = #pto.pipe<PIPE_MTE3>,
dst_pipe = #pto.pipe<PIPE_V>,
event_id = #pto.event<EVENT_ID0>} : () -> ()
"pto.set_flag"() {src_pipe = #pto.pipe<PIPE_MTE3>,
dst_pipe = #pto.pipe<PIPE_V>,
event_id = #pto.event<EVENT_ID1>} : () -> ()
scf.for %t = %c0 to %c64 step %c1 {
%t_i32 = arith.index_cast %t : index to i32
%eid = arith.andi %t_i32, %c1_i32 : i32
%eid_index = arith.index_cast %eid : i32 to index
"pto.wait_flag_dyn"(%eid_index) {src_pipe = #pto.pipe<PIPE_MTE3>,
dst_pipe = #pto.pipe<PIPE_V>} : (index) -> ()
pto.simt_launch @simt_vf_0<<<%c256_i32, %c1_i32, %c1_i32>>>
(%ub_base, %arg4, %t_i32) : (!pto.ptr<i8, ub>, f32, i32) -> ()
"pto.set_flag_dyn"(%eid_index) {src_pipe = #pto.pipe<PIPE_V>,
dst_pipe = #pto.pipe<PIPE_MTE3>} : (index) -> ()
"pto.wait_flag_dyn"(%eid_index) {src_pipe = #pto.pipe<PIPE_V>,
dst_pipe = #pto.pipe<PIPE_MTE3>} : (index) -> ()
%t_stride = arith.muli %t_i32, %c327680_i32 : i32
%core_stride = arith.muli %core, %c5120_i32 : i32
%gm_idx_i32 = arith.addi %t_stride, %core_stride : i32
%gm_idx = arith.index_cast %gm_idx_i32 : i32 to index
%gm_y = "pto.addptr"(%arg3, %gm_idx)
: (!pto.ptr<f32, gm>, index) -> !pto.ptr<f32, gm>
%ping = arith.muli %eid, %c8192_i32 : i32
%ub_idx_i32 = arith.addi %ping, %c21520_i32 : i32
%ub_idx = arith.index_cast %ub_idx_i32 : i32 to index
%byte_off = arith.muli %ub_idx, %c4 : index
%ub_elem = "pto.addptr"(%ub_base, %byte_off)
: (!pto.ptr<i8, ub>, index) -> !pto.ptr<i8, ub>
%ub_y = "pto.castptr"(%ub_elem) : (!pto.ptr<i8, ub>) -> !pto.ptr<f32, ub>
pto.mte_ub_gm %ub_y, %gm_y, %c20480_i64
nburst(%c1_i64, %c20480_i64, %c20480_i64)
: !pto.ptr<f32, ub>, !pto.ptr<f32, gm>, i64, i64, i64, i64
"pto.set_flag_dyn"(%eid_index) {src_pipe = #pto.pipe<PIPE_MTE3>,
dst_pipe = #pto.pipe<PIPE_V>} : (index) -> ()
}
"pto.wait_flag"() {src_pipe = #pto.pipe<PIPE_MTE3>,
dst_pipe = #pto.pipe<PIPE_V>,
event_id = #pto.event<EVENT_ID0>} : () -> ()
"pto.wait_flag"() {src_pipe = #pto.pipe<PIPE_MTE3>,
dst_pipe = #pto.pipe<PIPE_V>,
event_id = #pto.event<EVENT_ID1>} : () -> ()
return
}
}
该用例的语义很简单:每个 SIMTVF lane 按 tid 计算地址,前 10 个 chunk 写 UB,后 6 个 chunk 不写。
2. 问题反汇编示例
问题版本的反汇编中,SIMTVF 入口里存在类似如下控制流:
0000000000000000 <simt_vf_0_simt_entry>:
...
70: BRANCH 88
78: ISETP.NE.AND.s32 P1, PT, S8, #0, PT
88: BRANCH 416, !P1
...
c0: END @!P0
...
1b8: BRANCH 0x78, P1
...
220: BRANCH 0x78
关键点是:
END 被生成成了带谓词的指令,并且位于分支控制流相关路径中。
3. 问题说明
SIMTVF 中 END 应当是所有相关线程都能确定执行的终止点。
反汇编中 END 携带了 !P0:
这意味着:
!P0 为真的线程会执行 END;
P0 为真的线程不会执行 END;
- 未执行
END 的线程可能继续跑到后续指令;
- 这违反 SIMTVF 对
END 的约束,硬件行为不可预期。
PTO 源码中没有条件终止 SIMTVF 的语义。问题出现在带分支的 SIMTVF 控制流降低之后,END 没有保持在无条件收敛路径上。
4. 可以通过的场景
如果把逻辑改成循环 10 次,并去掉 if 分支,则可以通过。
等价通过版本如下:
module attributes {pto.target_arch = "a5", pto.kernel_kind = #pto.kernel_kind<vector>} {
func.func @simt_vf_0(%arg0: !pto.ptr<i8, ub>, %arg1: f32, %arg2: i32)
attributes {pto.simt_entry, pto.simt_max_regs = 64 : i32} {
%tid = pto.get_tid_x : i32
%c0 = arith.constant 0 : index
%c10 = arith.constant 10 : index
%c1 = arith.constant 1 : index
%c0_i32 = arith.constant 0 : i32
%c1_i32 = arith.constant 1 : i32
%c2_i32 = arith.constant 2 : i32
%c512_i32 = arith.constant 512 : i32
%c8192_i32 = arith.constant 8192 : i32
%c21520_i32 = arith.constant 21520 : i32
%c4_i64 = arith.constant 4 : i64
%fpconst = arith.constant 1.250000e+00 : f32
%undef = llvm.mlir.undef : vector<2xf32>
%ins0 = "llvm.insertelement"(%undef, %fpconst, %c0_i32)
: (vector<2xf32>, f32, i32) -> vector<2xf32>
%val = "llvm.insertelement"(%ins0, %fpconst, %c1_i32)
: (vector<2xf32>, f32, i32) -> vector<2xf32>
%ub_int = "pto.castptr"(%arg0) : (!pto.ptr<i8, ub>) -> i64
%ub_llvm = "llvm.inttoptr"(%ub_int) : (i64) -> !llvm.ptr<6>
scf.for %i = %c0 to %c10 step %c1 {
%i32 = arith.index_cast %i : index to i32
%eid = arith.andi %arg2, %c1_i32 : i32
%ping = arith.muli %eid, %c8192_i32 : i32
%chunk = arith.muli %i32, %c512_i32 : i32
%base0 = arith.addi %ping, %chunk : i32
%tid2 = arith.muli %tid, %c2_i32 : i32
%base1 = arith.addi %base0, %tid2 : i32
%idx = arith.addi %base1, %c21520_i32 : i32
%idx_index = arith.index_cast %idx : i32 to index
%idx_i64 = arith.index_cast %idx_index : index to i64
%byte_off = arith.muli %idx_i64, %c4_i64 : i64
%ptr = "llvm.getelementptr"(%ub_llvm, %byte_off)
{elem_type = i8, rawConstantIndices = array<i32: -2147483648>}
: (!llvm.ptr<6>, i64) -> !llvm.ptr<6>
"llvm.store"(%val, %ptr) : (vector<2xf32>, !llvm.ptr<6>) -> ()
}
return
}
func.func @main_kernel_kernel(%arg0: !pto.ptr<f32, gm>, %arg1: !pto.ptr<f32, gm>,
%arg2: !pto.ptr<f32, gm>, %arg3: !pto.ptr<f32, gm>,
%arg4: f32)
attributes {dyn_shared_memory_buf = 152640 : i64, pto.aicore} {
%c0_i64 = arith.constant 0 : i64
%ub_base = pto.castptr %c0_i64 : i64 -> !pto.ptr<i8, ub>
%bx = "pto.get_block_idx"() : () -> i64
%core = arith.trunci %bx : i64 to i32
%c0_i32 = arith.constant 0 : i32
%c1_i32 = arith.constant 1 : i32
%c64 = arith.constant 64 : index
%c256_i32 = arith.constant 256 : i32
%c5120_i32 = arith.constant 5120 : i32
%c8192_i32 = arith.constant 8192 : i32
%c21520_i32 = arith.constant 21520 : i32
%c327680_i32 = arith.constant 327680 : i32
%c4 = arith.constant 4 : index
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%c1_i64 = arith.constant 1 : i64
%c20480_i64 = arith.constant 20480 : i64
"pto.set_flag"() {src_pipe = #pto.pipe<PIPE_MTE3>,
dst_pipe = #pto.pipe<PIPE_V>,
event_id = #pto.event<EVENT_ID0>} : () -> ()
"pto.set_flag"() {src_pipe = #pto.pipe<PIPE_MTE3>,
dst_pipe = #pto.pipe<PIPE_V>,
event_id = #pto.event<EVENT_ID1>} : () -> ()
scf.for %t = %c0 to %c64 step %c1 {
%t_i32 = arith.index_cast %t : index to i32
%eid = arith.andi %t_i32, %c1_i32 : i32
%eid_index = arith.index_cast %eid : i32 to index
"pto.wait_flag_dyn"(%eid_index) {src_pipe = #pto.pipe<PIPE_MTE3>,
dst_pipe = #pto.pipe<PIPE_V>} : (index) -> ()
pto.simt_launch @simt_vf_0<<<%c256_i32, %c1_i32, %c1_i32>>>
(%ub_base, %arg4, %t_i32) : (!pto.ptr<i8, ub>, f32, i32) -> ()
"pto.set_flag_dyn"(%eid_index) {src_pipe = #pto.pipe<PIPE_V>,
dst_pipe = #pto.pipe<PIPE_MTE3>} : (index) -> ()
"pto.wait_flag_dyn"(%eid_index) {src_pipe = #pto.pipe<PIPE_V>,
dst_pipe = #pto.pipe<PIPE_MTE3>} : (index) -> ()
%t_stride = arith.muli %t_i32, %c327680_i32 : i32
%core_stride = arith.muli %core, %c5120_i32 : i32
%gm_idx_i32 = arith.addi %t_stride, %core_stride : i32
%gm_idx = arith.index_cast %gm_idx_i32 : i32 to index
%gm_y = "pto.addptr"(%arg3, %gm_idx)
: (!pto.ptr<f32, gm>, index) -> !pto.ptr<f32, gm>
%ping = arith.muli %eid, %c8192_i32 : i32
%ub_idx_i32 = arith.addi %ping, %c21520_i32 : i32
%ub_idx = arith.index_cast %ub_idx_i32 : i32 to index
%byte_off = arith.muli %ub_idx, %c4 : index
%ub_elem = "pto.addptr"(%ub_base, %byte_off)
: (!pto.ptr<i8, ub>, index) -> !pto.ptr<i8, ub>
%ub_y = "pto.castptr"(%ub_elem) : (!pto.ptr<i8, ub>) -> !pto.ptr<f32, ub>
pto.mte_ub_gm %ub_y, %gm_y, %c20480_i64
nburst(%c1_i64, %c20480_i64, %c20480_i64)
: !pto.ptr<f32, ub>, !pto.ptr<f32, gm>, i64, i64, i64, i64
"pto.set_flag_dyn"(%eid_index) {src_pipe = #pto.pipe<PIPE_MTE3>,
dst_pipe = #pto.pipe<PIPE_V>} : (index) -> ()
}
"pto.wait_flag"() {src_pipe = #pto.pipe<PIPE_MTE3>,
dst_pipe = #pto.pipe<PIPE_V>,
event_id = #pto.event<EVENT_ID0>} : () -> ()
"pto.wait_flag"() {src_pipe = #pto.pipe<PIPE_MTE3>,
dst_pipe = #pto.pipe<PIPE_V>,
event_id = #pto.event<EVENT_ID1>} : () -> ()
return
}
}
这个版本没有循环内 if,只做 10 次 UB 写入,不会生成上述带谓词的 END 问题。
5. 规避方法
可以在 PTOAS 的 VPTO pipeline 中引入 MLIR 公共优化,在进入最终 VPTO/LLVM emission 之前消除这类小常量循环和常量条件分支。
规避思路:
- 只针对
pto.simt_entry 函数;
- 对小 trip count 的常量
scf.for 做 full unroll;
- 运行
canonicalize;
- 运行
sccp;
- 再运行
canonicalize 和 cse。
对于如下模式:
for i in 0..16:
if i < 10:
store
展开后每个 i 都变成常量:
i = 0..9 时,i < 10 折叠为 true;
i = 10..15 时,i < 10 折叠为 false;
canonicalize/sccp 删除恒 false 分支;
- 最终只剩 10 段直线
store。
本地验证结果:
- 最小复现的同步版本上板通过;
- 真实 RMSNorm 5120 VPTO 用例上板通过;
- 对应 SIMTVF 中原始
for 0..16 + if i < 10 已经被消除。
6. 需要编译器侧分析
PTOAS 可以通过循环展开和 if 消除规避该问题,但生成 END @!P0 的根因仍需要在编译器侧处理。
编译器仍需要分析:
- SIMTVF 中存在循环内分支时,
END @!P0 的生成路径;
END 被放入分支控制流相关路径的原因;
- SIMTVF 控制流收敛分析对该场景的覆盖情况;
- SIMTVF 的
END 应保持无谓词、无条件执行;
问题概述
在 VPTO SIMTVF kernel 中,如果
scf.for循环体内包含scf.if分支,编译后的 AICore 反汇编中可能出现带谓词的END指令,例如:END @!P0SIMTVF 的限制是:
END指令不应出现在分支路径中,也不应通过分支跳转后条件执行。否则未执行END的线程可能继续向后执行,硬件行为不可预期。该现象发生在编译器处理 SIMTVF 控制流之后,生成代码不符合上述约束。
1. 最小复现用例
失败场景是:SIMTVF 内循环 16 次,但只有前 10 次写 UB。
完整 PTO 复现如下:
该用例的语义很简单:每个 SIMTVF lane 按
tid计算地址,前 10 个 chunk 写 UB,后 6 个 chunk 不写。2. 问题反汇编示例
问题版本的反汇编中,SIMTVF 入口里存在类似如下控制流:
关键点是:
END @!P0END被生成成了带谓词的指令,并且位于分支控制流相关路径中。3. 问题说明
SIMTVF 中
END应当是所有相关线程都能确定执行的终止点。反汇编中
END携带了!P0:END @!P0这意味着:
!P0为真的线程会执行END;P0为真的线程不会执行END;END的线程可能继续跑到后续指令;END的约束,硬件行为不可预期。PTO 源码中没有条件终止 SIMTVF 的语义。问题出现在带分支的 SIMTVF 控制流降低之后,
END没有保持在无条件收敛路径上。4. 可以通过的场景
如果把逻辑改成循环 10 次,并去掉
if分支,则可以通过。等价通过版本如下:
这个版本没有循环内
if,只做 10 次 UB 写入,不会生成上述带谓词的END问题。5. 规避方法
可以在 PTOAS 的 VPTO pipeline 中引入 MLIR 公共优化,在进入最终 VPTO/LLVM emission 之前消除这类小常量循环和常量条件分支。
规避思路:
pto.simt_entry函数;scf.for做 full unroll;canonicalize;sccp;canonicalize和cse。对于如下模式:
展开后每个
i都变成常量:i = 0..9时,i < 10折叠为 true;i = 10..15时,i < 10折叠为 false;canonicalize/sccp删除恒 false 分支;store。本地验证结果:
for 0..16 + if i < 10已经被消除。6. 需要编译器侧分析
PTOAS 可以通过循环展开和 if 消除规避该问题,但生成
END @!P0的根因仍需要在编译器侧处理。编译器仍需要分析:
END @!P0的生成路径;END被放入分支控制流相关路径的原因;END应保持无谓词、无条件执行;