Skip to content

[VMI] 256-lane E2B group broadcast: layout assignment downgrades the deinterleaved direct row to contiguous, landing on vsldb + 2x vselr (device 507035) instead of one E2B vlds #1549

Description

@liuzidi

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:

vsldb + 2x vselr

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_broadcastgs(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:

  1. 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.
  2. 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.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions