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
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→ scalarRV_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*strideto a pointer thatis incremented by
strideonce per iteration (nor lower it to immediate-offsetaddressing). The only way we can get clean (zero-scalar,
RV_VLDI/RV_VSTIimmediate) 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:
fast_hadamard_a5(bisheng)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.forwhose UB base pointer is affine in theinduction variable. Lower with ptoas and inspect the RVEC op stream inside the loop
body:
Expected behavior
vpto strength-reduces the affine
base + iv*stride: the per-tile UB pointer iscarried as a loop-carried value incremented by
strideeach iteration (or loweredto immediate-offset addressing), so no scalar address arithmetic
(
RV_SADD/RV_SMOVK/…) is emitted inside the loop body — matching thezero-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 opserializes the
RV_VLDIthat 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