K4 (`mlp`) and K1/K3 (`res_layernorm_fold`): -4.8% end-to-end encoder, bitwise identical
#2
by jlongen-aws - opened
Reworks the fused MLP kernel and the residual + LayerNorm kernel. K2, the weight folds and the
layer are unchanged apart from one line in layers.py that packs the folded fc1 bias.
Results (trn2.3xlarge, LNC=2, all outputs torch.equal to v1.1)
Full encoder (torch.compile(model.encoder, backend="neuron"), 24 layers, one 16-frame clip):
| v1.1 | this PR | change | |
|---|---|---|---|
| Wall time, ms/clip (3 rounds, 20 iters each) | 26.632 / 26.635 / 26.639 | 25.356 / 25.357 / 25.347 | -1.28 ms (-4.8%) |
| Device time (one profiled forward) | 26.83 ms | 25.50 ms | -5.0% |
mfu_estimated_percent |
39.5% | 41.7% | +2.2 points |
Standalone at the encoder shape (2 interleaved rounds each):
| Kernel | v1.1 | this PR | change | efficiency |
|---|---|---|---|---|
K4 mlp ([2048, 1024] -> 4096 -> 1024) |
295.3 us | 248.6 us | -15.8% | MFU 70.3% -> 82.8% |
K1 res_layernorm_fold, 1 branch |
48.0 us | 41.9 us | -12.7% | HBM 73% -> 84% of 716 GB/s |
K3 res_layernorm_fold, 2 branches |
57.7 us | 48.2 us | -16.3% | HBM 71% -> 85% of 716 GB/s |
K1/K3 do no matmuls, so the profiler reports MFU 0 for them. Achieved HBM bandwidth
(bytes moved / device time) is their roofline metric.
Absolute times are on torch-neuronx 2.14.3 + nki 0.7.0b1. The same v1.1 code measures 26.63 ms/clip
there, against 23.94 in the README, so compare the relative change.
What changed in K4 (found from the device profile)
- x before weights. v1.1 issued b1 -> all of W1 -> all of W2 -> x, so the first matmul waited
for ~16 MB of weights it didn't need yet (first matmul at ~57 us). Order is now
b1 -> x(rb0) -> W1 -> x(rb1) -> W2. - b1 in one DMA. v1.1 loaded fc1 bias as 32 DMAs of [128, 1] fp32 (4 B per partition), which
trickled in until ~69 us. Each gelu waited on its bias tile, and through its PSUM bank the fc1
matmuls behind it. b1 is now host-packed to [128, H/128] (pack_b1) and lands in one 16 KB DMA on
the Scalar engine's HW-DGE queue, so gelu's wait no longer counts W1/W2 traffic. - Row block 0's x transposed on the Tensor engine, which is idle in the prologue.
- Rewrite: 3D SBUF tiles instead of tile lists,
nl.tile_size.pmax/gemm_moving_fmax
instead of literals, and exact-multiple shapes asserted instead ofmin()remainder handling.
b2 stays a per-(row block, output chunk) partition-broadcast load; hoisting it into one load was
neutral standalone but cost ~0.7 ms/clip in the encoder.
What changed in K1/K3
dge_mode.noneon every DMA. In v1.1 each of the ~40 DMAs had its descriptors generated by
GpSimd (software DGE) at ~0.75 us apiece, one after another; the first add waited ~8 us for them.
Withdge_mode.nonethe runtime generates descriptors before execution. Hardware DGE on the Sync
queue (dge_mode.hwdge, engine=sync) was as fast standalone but 0.1 ms/clip slower in the
encoder, so it is not used.- All token tiles resident in SBUF, every load issued before any compute, so a tile's loads
never wait behind the previous tile's stores.
The compute instructions and their order are unchanged, so results are bitwise identical.
Compatibility
mlpstill accepts b1 as[H](packed on the fly) as well as the pre-packed[128, H/128];pack_b1is exported. Op names unchanged.- New K4 requirement: K % 512 == 0, H % 128 == 0, R % 1024 == 0. The encoder shape (R=2048, K=1024,
H=4096) satisfies these. - K1/K3 SBUF use is ~116 KB/partition at H=1024 with 2 branches (all tiles resident).
- Toolchain: all numbers above are torch-neuronx 2.14.3 + nki 0.7.0b1. Standalone K4 and K1/K3
were also verified on the latest public NKI release, nki 0.6.0 + neuronx-cc 2.27.5334: K4 248.6 /
247.7 us vs 291.6 / 294.0 us for v1.1, K1 42.6 us, K3 48.1 us, all bitwise identical. The full
encoder could not be run on nki 0.6.0 with torch-neuronx 2.14.3: v1.1 itself fails there
(CPU fallback failed for 'model_default'). nki 0.6.0 rejects list comprehensions inside kernels,
so the kernels use explicit loops.
