Skip to content

[Bug] [Compiler] SIMTVF 循环内 if 分支导致 END 指令带谓词 #485

Description

@KurrinQu

问题概述

在 VPTO SIMTVF kernel 中,如果 scf.for 循环体内包含 scf.if 分支,编译后的 AICore 反汇编中可能出现带谓词的 END 指令,例如:

END @!P0

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 @!P0

END 被生成成了带谓词的指令,并且位于分支控制流相关路径中。

3. 问题说明

SIMTVF 中 END 应当是所有相关线程都能确定执行的终止点。

反汇编中 END 携带了 !P0

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 之前消除这类小常量循环和常量条件分支。

规避思路:

  1. 只针对 pto.simt_entry 函数;
  2. 对小 trip count 的常量 scf.for 做 full unroll;
  3. 运行 canonicalize
  4. 运行 sccp
  5. 再运行 canonicalizecse

对于如下模式:

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 的根因仍需要在编译器侧处理。

编译器仍需要分析:

  1. SIMTVF 中存在循环内分支时,END @!P0 的生成路径;
  2. END 被放入分支控制流相关路径的原因;
  3. SIMTVF 控制流收敛分析对该场景的覆盖情况;
  4. SIMTVF 的 END 应保持无谓词、无条件执行;

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions