Summary
A 256-lane bf16 grouped broadcast (dist_mode="brc", group = 8, i.e. period-32
broadcast over 32 elements/group → groupSize = 256/8 = 32 elements = gb(2))
has a native E2B direct row in the layout table, but layout assignment
deliberately downgrades it, so the load ends up on the group_slot fallback:
instead of the one instruction the hardware has:
vlds {dist = "E2B_B16"} // one packet, reused by both physical parts
vsldb requires a 32B-aligned group base and reads 256B (16x the semantic
16B footprint). With the natural packed scale layout (8 contiguous bf16 scales
per chunk, so odd rows start 16B off a 32B boundary) the emitted code passes
lowering and then traps on device with 507035.
The only thing separating the two lowerings is a single if in
getPreferredGroupBroadcastLoadLayout().
Environment
Ascend950DT_9592 (A5), CANN 9.2.0, dav-c310-vec, 72 vector cores.
PTOAS 9b0b7697f; the same behaviour reproduces on 566d6af8c (= vmi-v0.1.8)
and on MR152 (cbb478460) — the two lowerings are identical there.
Minimal repro
pto-test-opt repro.pto -vmi-lower-unified-to-legacy -vmi-mask-granularity-assignment -vmi-layout-assignment -vmi-to-vpto
// A. contiguous result declared -> no E2B direct row for that layout
func.func @e2b_256_bf16_contiguous(
%src: !pto.ptr<bf16, ub>, %off: index) -> !pto.vmi.vreg<256xbf16, #pto.vmi.layout<contiguous>> {
%c1 = arith.constant 1 : index
%c8 = arith.constant 8 : index
%aligned_off = arith.muli %off, %c8 : index
%out = pto.vmi.vload %src[%aligned_off], %c1 {dist_mode = "brc", group = 8}
: !pto.ptr<bf16, ub> -> !pto.vmi.vreg<256xbf16, #pto.vmi.layout<contiguous>>
return %out : !pto.vmi.vreg<256xbf16, #pto.vmi.layout<contiguous>>
}
// B. the deinterleaved result the direct row asks for
func.func @e2b_256_bf16_deint2(
%src: !pto.ptr<bf16, ub>, %off: index) -> !pto.vmi.vreg<256xbf16, #pto.vmi.layout<deinterleaved = 2>> {
%c1 = arith.constant 1 : index
%c8 = arith.constant 8 : index
%aligned_off = arith.muli %off, %c8 : index
%out = pto.vmi.vload %src[%aligned_off], %c1 {dist_mode = "brc", group = 8}
: !pto.ptr<bf16, ub> -> !pto.vmi.vreg<256xbf16, #pto.vmi.layout<deinterleaved = 2>>
return %out : !pto.vmi.vreg<256xbf16, #pto.vmi.layout<deinterleaved = 2>>
}
// C. no layout declared: broadcast feeding an elementwise vmul + store
func.func @e2b_256_bf16_solver_choice(
%src: !pto.ptr<bf16, ub>, %x: !pto.ptr<bf16, ub>, %off: index, %xoff: index,
%dst: !pto.ptr<bf16, ub>, %doff: index, %mask: !pto.vmi.mask<256xb16>) {
%c1 = arith.constant 1 : index
%c8 = arith.constant 8 : index
%aligned_off = arith.muli %off, %c8 : index
%scale = pto.vmi.vload %src[%aligned_off], %c1 {dist_mode = "brc", group = 8}
: !pto.ptr<bf16, ub> -> !pto.vmi.vreg<256xbf16>
%aligned_xoff = arith.muli %xoff, %c8 : index
%value = pto.vmi.vload %x[%aligned_xoff]
: !pto.ptr<bf16, ub> -> !pto.vmi.vreg<256xbf16>
%prod = pto.vmi.vmul %value, %scale, %mask
: !pto.vmi.vreg<256xbf16>, !pto.vmi.vreg<256xbf16>, !pto.vmi.mask<256xb16> -> !pto.vmi.vreg<256xbf16>
%aligned_doff = arith.muli %doff, %c8 : index
pto.vmi.vstore %prod, %dst[%aligned_doff]
: !pto.vmi.vreg<256xbf16>, !pto.ptr<bf16, ub>
return
}
Observed lowering
A — contiguous (as declared, and as the solver picks for C):
%result = pto.vsldb %2, %c0_i16, %c0_i16, %1 : !pto.ptr<bf16, ub>, i16, i16, !pto.mask<b16> -> !pto.vreg<128xbf16>
%6 = pto.vselr %result, %5 : !pto.vreg<128xbf16>, !pto.vreg<128xui16> -> !pto.vreg<128xbf16>
%8 = pto.vselr %result, %7 : !pto.vreg<128xbf16>, !pto.vreg<128xui16> -> !pto.vreg<128xbf16>
return %6, %8 : !pto.vreg<128xbf16>, !pto.vreg<128xbf16>
B — deinterleaved = 2 declared (this is the whole difference):
%result = pto.vlds %arg0[%0] {dist = "E2B_B16"} : !pto.ptr<bf16, ub> -> !pto.vreg<128xbf16>
return %result, %result : !pto.vreg<128xbf16>, !pto.vreg<128xbf16> // one packet, both parts
C — no layout declared: the solver repeats A, then pays for the mismatch at the
store (note there is no d(2) in the consumer chain, so the assigned broadcast
result is contiguous):
%result = pto.vsldb %2, %c0_i16, %c0_i16, %1 : !pto.ptr<bf16, ub>, i16, i16, !pto.mask<b16> -> !pto.vreg<128xbf16>
%6 = pto.vselr %result, %5 : ...
%8 = pto.vselr %result, %7 : ...
%13 = pto.vmul %result_0, %6, %12 : ...
%15 = pto.vmul %result_1, %8, %14 : ...
pto.vstus ... %13 ... // + pto.vstus ... %15 ...
For reference, the AscendC form of the same operation is exactly one E2B packet
plus the broadcast split, with no slot load and no register gather:
1x vlds_brc_elem2datablock (E2B_B16) // 16B group base alignment is enough
1x vintlv
2x vlds UNPK4
2x vmul
2x vsts NORM_B16
Root cause
kGroupBroadcastLoadDirectPatterns (lib/PTO/Transforms/VMILayoutSupportTables.inc:510)
already carries the row for this shape — groupSize = 32 elements is gb(2):
{VMIGroupBroadcastLoadDirectKind::E2B, G<8>(), gb(2), bits<16, 32>(),
memContiguous(), d(2)}, // line 513
But getPreferredGroupBroadcastLoadLayout()
(lib/PTO/Transforms/VMILayoutAssignment.cpp:403, introduced by
0d7da8454 "vmi: narrow FP4 layout and physical part lowering", 2026-09-07)
throws that layout away whenever the result is unassigned:
VMILayoutAttr directLayout = fact->layout.resultLayout;
// A direct E2B packet fills exactly one physical part. When the
// contiguous form of this broadcast spans multiple physical chunks, the
// direct table can only offer a deinterleaved split layout. Prefer the
// generic contiguous lowering (group_slots -> contiguous) so consumers
// such as plain elementwise vmul can stay contiguous and avoid
// vldsx2/vintlv-style deinterleave materialization.
if (fact->kind == VMIGroupBroadcastLoadDirectKind::E2B &&
directLayout.isDeinterleaved()) { // line 424/425
...
return contiguous; // no E2B direct row exists for c()
}
return directLayout;
c() has no E2B row (only gb(1)→c(), gb(2)→d(2), gb(4)→d(4) exist), so the
value falls through to the generic group_broadcast → gs(8) layout table, and
the lowering takes lowerGroupSlotFallback
(VMIToVPTOPatternInternals3.cpp:1680) instead of lowerDirectE2B (:1545).
The comment states the trade-off honestly (keep vmul contiguous), but the
comparison is one-sided: it measures the cost of the d(2) path
(vdintlv/vintlv materialization) and ignores the cost of the contiguous path
(vsldb + vselr, plus a 32B base-alignment requirement and a 256B read
footprint for a 16B semantic load). On this shape the contiguous path is the
more expensive one, on every axis.
Proposal
The d(2) row is not the problem — the downgrade is. Two possible directions:
- Make the choice cost-aware instead of unconditional: keep
directLayout
when the E2B row exists and the consumer chain can absorb d(2), and only
fall back to contiguous when the cost actually favors it. At minimum, the
vsldb path's 32B alignment requirement (and its 256B read envelope) should
count against it — a slot path that is known-misaligned today is a device
trap, not a slower-but-valid alternative.
- Let
d(2) reach the store. With directLayout kept, what remains is a
vdintlv/vintlv pair around the elementwise consumer (see the local
experiment below). If elementwise ops could stay in d(2) end-to-end and
only the store did the interleave, the 256-lane form would generate exactly
the AscendC instruction sequence. This looks like the missing piece — the
lowering exists, the layout negotiation policy is what stops it.
A local 5-line experiment (gate the downgrade behind an env var, nothing else
changed) confirms the direction. Same .pto input:
|
generated broadcast load |
on-device |
| default |
vsldb + 2x vselr |
507035 (traps) |
| downgrade gated off |
1x vlds {dist="E2B_B16"} + vdintlv + 2x vmul + vintlv |
maxdiff 0 |
if (fact->kind == VMIGroupBroadcastLoadDirectKind::E2B &&
- directLayout.isDeinterleaved()) {
+ directLayout.isDeinterleaved() &&
+ std::getenv("PTOAS_KEEP_E2B_D2") == nullptr) {
Measured on a real kernel (fp4 e2m1 → bf16 dequant, m=512 h=512 bk=512, 72
cores, msprof warmup=0 rep=20 x 3 rounds, medians of two devices):
| variant |
device 0 |
device 1 |
vs ASC |
| AscendC reference |
2.5256 us |
2.5521 us |
1.000 |
| 128-lane E2B form (two carriers) |
2.5474 us |
2.5338 us |
+0.9% / +0.7% |
256-lane ideal form + PTOAS_KEEP_E2B_D2=1 |
2.6927 us |
2.6737 us |
+6.6% / +4.8% |
(I am not proposing this env-var gate as a patch — global removal of the
downgrade would slow down kernels whose consumers genuinely want contiguous,
which is what the comment is protecting. It is reported as an experiment to
characterize the gap. The remaining +5–7% is exactly the vdintlv/vintlv pair
that a "keep d(2) through the consumer" policy would remove.)
Note: the default path's trap is also reachable without any d(2)
consideration — a group-slot slots=8 load with a 16B-only-aligned base emits
vsldb and traps. I filed a related guard for that case previously (see
#1502 for a sibling problem in the same lowering family); the request here is
about the policy that sends this shape there in the first place.
Happy to contribute the repro as a lit test or to try the cost-aware variant if
that direction is acceptable.
Summary
A 256-lane bf16 grouped broadcast (
dist_mode="brc", group = 8, i.e. period-32broadcast over 32 elements/group →
groupSize = 256/8 = 32elements =gb(2))has a native E2B direct row in the layout table, but layout assignment
deliberately downgrades it, so the load ends up on the
group_slotfallback:instead of the one instruction the hardware has:
vsldbrequires a 32B-aligned group base and reads 256B (16x the semantic16B footprint). With the natural packed scale layout (8 contiguous bf16 scales
per chunk, so odd rows start 16B off a 32B boundary) the emitted code passes
lowering and then traps on device with 507035.
The only thing separating the two lowerings is a single
ifingetPreferredGroupBroadcastLoadLayout().Environment
Ascend950DT_9592 (A5), CANN 9.2.0,
dav-c310-vec, 72 vector cores.PTOAS
9b0b7697f; the same behaviour reproduces on566d6af8c(=vmi-v0.1.8)and on MR152 (
cbb478460) — the two lowerings are identical there.Minimal repro
pto-test-opt repro.pto -vmi-lower-unified-to-legacy -vmi-mask-granularity-assignment -vmi-layout-assignment -vmi-to-vptoObserved lowering
A —
contiguous(as declared, and as the solver picks for C):B —
deinterleaved = 2declared (this is the whole difference):C — no layout declared: the solver repeats A, then pays for the mismatch at the
store (note there is no
d(2)in the consumer chain, so the assigned broadcastresult is
contiguous):For reference, the AscendC form of the same operation is exactly one E2B packet
plus the broadcast split, with no slot load and no register gather:
Root cause
kGroupBroadcastLoadDirectPatterns(lib/PTO/Transforms/VMILayoutSupportTables.inc:510)already carries the row for this shape —
groupSize = 32elements isgb(2):{VMIGroupBroadcastLoadDirectKind::E2B, G<8>(), gb(2), bits<16, 32>(), memContiguous(), d(2)}, // line 513But
getPreferredGroupBroadcastLoadLayout()(
lib/PTO/Transforms/VMILayoutAssignment.cpp:403, introduced by0d7da8454"vmi: narrow FP4 layout and physical part lowering", 2026-09-07)throws that layout away whenever the result is unassigned:
c()has no E2B row (onlygb(1)→c(),gb(2)→d(2),gb(4)→d(4)exist), so thevalue falls through to the generic
group_broadcast→gs(8)layout table, andthe lowering takes
lowerGroupSlotFallback(
VMIToVPTOPatternInternals3.cpp:1680) instead oflowerDirectE2B(:1545).The comment states the trade-off honestly (keep
vmulcontiguous), but thecomparison is one-sided: it measures the cost of the
d(2)path(
vdintlv/vintlvmaterialization) and ignores the cost of the contiguous path(
vsldb+vselr, plus a 32B base-alignment requirement and a 256B readfootprint for a 16B semantic load). On this shape the contiguous path is the
more expensive one, on every axis.
Proposal
The
d(2)row is not the problem — the downgrade is. Two possible directions:directLayoutwhen the E2B row exists and the consumer chain can absorb
d(2), and onlyfall back to
contiguouswhen the cost actually favors it. At minimum, thevsldbpath's 32B alignment requirement (and its 256B read envelope) shouldcount against it — a slot path that is known-misaligned today is a device
trap, not a slower-but-valid alternative.
d(2)reach the store. WithdirectLayoutkept, what remains is avdintlv/vintlvpair around the elementwise consumer (see the localexperiment below). If elementwise ops could stay in
d(2)end-to-end andonly the store did the interleave, the 256-lane form would generate exactly
the AscendC instruction sequence. This looks like the missing piece — the
lowering exists, the layout negotiation policy is what stops it.
A local 5-line experiment (gate the downgrade behind an env var, nothing else
changed) confirms the direction. Same
.ptoinput:vsldb+ 2xvselrvlds {dist="E2B_B16"}+vdintlv+ 2xvmul+vintlvMeasured on a real kernel (fp4 e2m1 → bf16 dequant, m=512 h=512 bk=512, 72
cores,
msprof warmup=0 rep=20x 3 rounds, medians of two devices):PTOAS_KEEP_E2B_D2=1(I am not proposing this env-var gate as a patch — global removal of the
downgrade would slow down kernels whose consumers genuinely want
contiguous,which is what the comment is protecting. It is reported as an experiment to
characterize the gap. The remaining +5–7% is exactly the
vdintlv/vintlvpairthat a "keep
d(2)through the consumer" policy would remove.)Note: the default path's trap is also reachable without any
d(2)consideration — a group-slot
slots=8load with a 16B-only-aligned base emitsvsldband traps. I filed a related guard for that case previously (see#1502 for a sibling problem in the same lowering family); the request here is
about the policy that sends this shape there in the first place.
Happy to contribute the repro as a lit test or to try the cost-aware variant if
that direction is acceptable.