Skip to content

[Bug] [ptoas/vpto a5] No strength-reduction of an affine base pointer castptr(base + iv*stride) inside a runtime scf.for — leaks per-iteration scalar address ops onto the vector pipe #591

Description

@MirkoDeVita98

Component

EmitC / Codegen (lib/PTO/Transforms/PTOToEmitC.cpp)

Description

When a vector kernel indexes UB through a base pointer computed per loop iteration as an affine function of the induction variable — castptr(base + iv * stride) — vpto re-materializes the full address arithmetic inside every iteration (index_cast + muli + addi → scalar RV_SADD/RV_SMOVK/RV_SZEROEXT/RV_SMOV). Those scalar ops issue on the vector pipe and serialize the loads that consume the address, so a rolled tile loop becomes load-subpipe-bound.

vpto does not strength-reduce the affine base + iv*stride to a pointer that
is incremented by stride once per iteration (nor lower it to immediate-offset
addressing). The only way we can get clean (zero-scalar, RV_VLDI/RV_VSTI
immediate) addressing today is to fully unroll the tile loop at generate-time
so every offset is a compile-time constant — which then costs code size and, for
kernels whose schedule needs a rolled loop to software-pipeline load/store
overlap, blocks that schedule entirely.

This is the single blocker preventing our fast Walsh–Hadamard (WHT) kernel from
reaching 1:1 cycle parity with the bisheng (CCE) reference at N > 256. bisheng
strength-reduces the same affine base pointer and software-pipelines the rolled
loop simultaneously; vpto forces us to choose one, and neither choice reaches
parity:

N=512 emission (matched, single core) RVEC cycles vs CCE why
CCE fast_hadamard_a5 (bisheng) 28903 1.00× rolled + overlapped + strength-reduced
vpto rolled loop 70694 2.45× 1640 leaked scalar addr ops → load-subpipe pinned ~98.5%
vpto fully unrolled (shipped) 49276 1.71× 0 scalar (immediate), but no cross-tile overlap → store-subpipe 40.5% idle

The rolled form is what should match CCE; it is 2.45× only because of the leaked
scalar addressing. Fixing this strength-reduction is the prime lever to close 1.71×
→ ~1.0×. (Op counts VST/VLD/VADD/VSUB and RVEC event counts are identical
vpto==CCE at every N — the gap is purely schedule, IPC 0.94 vs 1.60.)

Reproduction (minimal)

Minimal pattern — a runtime scf.for whose UB base pointer is affine in the
induction variable. Lower with ptoas and inspect the RVEC op stream inside the loop
body:

module attributes {
  pto.target_arch = "a5",
  pto.kernel_kind = #pto.kernel_kind<vector>
} {
  // Streams `ntile` UB tiles; tile t lives at UB byte offset t*STRIDE.
  func.func @affine_base_in_scf_for(%x: !pto.ptr<f16, gm>, %ntile: i32)
      attributes {pto.kernel} {
    %c0 = arith.constant 0 : index
    %c1 = arith.constant 1 : index
    %e0 = arith.constant 0 : index
    %stride = arith.constant 4096 : i64          // per-tile UB byte stride
    %nt = arith.index_cast %ntile : i32 to index
    scf.for %t = %c0 to %nt step %c1 {
      %ti  = arith.index_cast %t : index to i64
      %off = arith.muli %ti, %stride : i64        // <-- affine: base(0) + t*stride
      %ub  = pto.castptr %off : i64 -> !pto.ptr<f16, ub>
      %lo, %hi = pto.vldsx2 %ub[%e0], "DINTLV_B16"
          : !pto.ptr<f16, ub>, index -> !pto.vreg<128xf16>, !pto.vreg<128xf16>
      // ... any vadd/vsub + vsts back through %ub ...
    }
    return
  }
}
ptoas --target a5 --backend vpto affine_base_in_scf_for.pto -o out    # inspect out for RV_SADD/SMOVK in the loop

Expected behavior

vpto strength-reduces the affine base + iv*stride: the per-tile UB pointer is
carried as a loop-carried value incremented by stride each iteration (or lowered
to immediate-offset addressing), so no scalar address arithmetic
(RV_SADD/RV_SMOVK/…) is emitted inside the loop body
— matching the
zero-scalar addressing that the fully-unrolled form already achieves, and matching
what bisheng does for the CCE kernel. The rolled loop is then free to
software-pipeline load/store overlap and reach CCE cycle parity.

Actual behavior / error logs

vpto re-computes the address every iteration. In the full N=512 kernel the rolled
form emits 1640 scalar address ops in the tile loop (RV_SADD, RV_SMOVK,
RV_SZEROEXT, RV_SMOV) where the unrolled form emits 0. Each scalar op
serializes the RV_VLDI that consumes its address, pinning the load subpipe at
~98.5% and inflating RVEC span to 70694 (IPC 0.94) vs CCE 28903 (IPC 1.60)
— a 2.45× regression that is entirely the leaked addressing. Falling back to
full unroll removes the scalar ops (→ 1.71×, store-bound) but forfeits the rolled
schedule, so parity is unreachable either way with the current lowering.

Git commit

4870cd3

Host platform

Linux (aarch64)

Target Ascend arch (if relevant)

a5

PTOAS build level (if relevant)

None

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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