File size: 17,401 Bytes
c855122 954a874 567f054 1fc5661 a827ebd 90f3d2f 6da9fa8 90f3d2f 3b1bffa ce0e45f 7f9eb4c cabaa8f c855122 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | # Changelog
## v1.9 (2026-10)
* **New: padding masks on the kernel path for `NeuronXLMRobertaModel`.** v1.8 sent any batch with a padding
`attention_mask` to the stock forward. The attention kernel now takes an optional per-image additive key bias
(`attention_block_l1_ln_post_kb`: 0 for attended keys, -1e9 for masked keys), and the layer turns the mask into that
bias, so variable-length batches stay on the kernels. `VIT_ENC_MASK_KERNEL=0` restores the v1.8 fallback.
XLM-RoBERTa-Large, LNC=1, 8 processes on one trn2 device, a log-normal length mix padded to the sequence length
(mean valid length 53 / 89 / 191), inferences/s:
| seq | stock with mask (v1.8 fallback) | v1.9 kernels with mask | speedup | v1.9 kernels, no mask |
|---|---|---|---|---|
| 128 | 956.2 | **2,923.0** | 3.06x | 3,177.0 |
| 256 | 464.5 | **1,899.2** | 4.09x | 1,922.7 |
| 512 | 213.0 | **894.4** | 4.20x | 962.9 |
Accuracy vs the fp32 CPU model on the same mask (valid positions): rel_l2 0.005-0.055 for right padding, half
padding, length-1 sequences and interior holes (the unmasked v1.8 path gives 0.007-0.051 at the same shapes).
Masked positions are computed like the stock model does (their keys still attend to valid keys). Every query row
is still computed, so padding is not skipped: throughput follows the padded length.
* **No change for existing models:** 28/28 regression cases (DINOv3 S/S+/B/L/H+ at 224/448, V-JEPA2, LNC=1 and LNC=2)
give output bitwise identical to v1.8 at the same speed (within ±0.15%). Without the new argument the kernels emit
the v1.8 instruction stream. Verified on the public SDK 2.33 container
(`public.ecr.aws/neuron/torch-neuronx:2.14.0-neuronx-py314-sdk2.33.0-ubuntu26.04`: torch 2.14.0, torch-neuronx 2.14.3,
neuronx-cc 2.28.7631, NKI 0.7.0) with transformers 4.57.6.
## v1.8 (2026-10)
* **New: post-LayerNorm encoders (BERT / RoBERTa / XLM-R).** `NeuronXLMRobertaModel` is a drop-in forward for
`XLMRobertaModel`, `RobertaModel` and `BertModel` (GELU, absolute positions, no RoPE). It runs on the same kernels via
two new options:
- `attention_block_l1_ln_post` / `mlp_res_ln_post`: the existing fused attention and MLP epilogues also emit the
post-LN residual `LN(x_new) * gamma + beta` in fp32, computed from the mean and rstd they already have.
- `norope=1` on the attention kernel skips the rotation (and its table loads) for models without RoPE.
XLM-RoBERTa-Large, LNC=1, 8 processes (one per logical core) on one trn2.3xlarge, fully padded batches (inferences/s):
| seq | stock `torch.compile` | v1.8 `NeuronXLMRobertaModel` (`attention_mask=None`) | speedup | rel_l2 vs fp32 (stock / kernels) |
|---|---|---|---|---|
| 128 | 2,092.7 | **3,144.8** | 1.50x | 0.031 / 0.0066 |
| 256 | 944.4 (BS 16) | **1,869.5** | 1.98x | 0.051 / 0.0147 |
| 512 | 397.1 (BS 8) | **935.1** | 2.35x | 0.018 / 0.0143 |
Batch per core: 16 / 8 / 4. Passing an all-ones `attention_mask` costs 3-4% (a host-side check that no position is
masked: 3,022.7 / 1,789.7 / 902.8). A padded mask falls back to the stock forward (checked: rel_l2 vs fp32 1.2e-6).
Measured on the SDK 2.33 DLAMI (`torch_neuronx_2_14_0` venv: torch 2.14.0, torch-neuronx 2.14.3.0, neuronx-cc 2.28.7631,
NKI 0.7.0) with transformers 4.57.6. The kernel path needs LNC=1, a sequence length that is a multiple of 128 (up to 2048)
and no padding mask (all positions attended). Anything else falls back to the stock forward.
* **Fix: models with an odd number of head groups failed to compile at LNC=2 on neuronx-cc 2.28** (`NCC_EVRF066`: a
one-program attention grid on an LNC=2 build). That covers ViT-S/16, ViT-S+/16 and ViT-H+/16. At LNC=2 the host packing now pads
an odd group count with one all-zero head group, so every attention launch uses a two-program grid. The padded heads have
zero V and out-projection weights and add exactly 0. Models with an even group count (ViT-B, ViT-L, V-JEPA2) are unchanged.
Single core, LNC=2: ViT-S/16 224 BS4 1,148.8 img/s (rel_l2 0.0109), ViT-S+/16 1,097.9 (0.0148), ViT-H+/16 224 BS1/BS4
87.8 / 130.9, 448 BS1/BS2 32.6 / 33.9 (rel_l2 0.0059-0.0080). The ViT-H+ LNC=2 path is 1.18-1.34x faster than its LNC=1
fallback on the same core.
* **No change for existing models at existing shapes:** in the 28-case regression (DINOv3 S/S+/B/L/H+ at 224/448 and
V-JEPA2 at batch 1/2, LNC=1 and LNC=2), every case v1.7 runs gives output bitwise identical to v1.7, at the same speed
(within ±1%). That is 22 cases. The other 6 are the odd-group LNC=2 cases above, which v1.7 cannot compile on this
toolchain. Verified on the SDK 2.33 DLAMI.
## v1.7 (2026-10)
* **Fix: `res_layernorm_fold` failed to compile at large batch** (`NCC_IBIR229`, state buffer allocation failed). The
kernel staged every row tile of a core in SBUF at once. Above about 200 KiB per partition that overflowed:
- ViT-S/16 at LNC=1, batch 32 at 224 px or batch 8 at 448 px;
- ViT-B/16 at LNC=1, batch 16 at 224 px;
- ViT-L/16 at LNC=1, batch 16 at 224 px;
- ViT-H+/16 at LNC=1, batch 16 at 224 px.
Each failure killed the worker process. Tiles are now processed in groups that fit, with the same per-tile
instructions in the same order. Every shape that compiled before keeps its schedule and is **bit-identical** to v1.6
(checked at 9 shapes, LNC=1 and LNC=2). The newly compiling shapes match a float64 reference
(z rel_l2 1.66e-3, the same as before).
Per instance (LNC=1, 8 cores) at the newly reachable batch sizes:
| model | batch | img/s | best before |
|---|---|---|---|
| ViT-S/16 224 px | 32 / 64 | 8,205.4 / 8,009.5 | 9,422.6 (batch 16) |
| ViT-S/16 448 px | 8 / 16 | 2,176.4 / 2,192.3 | 2,486.1 (batch 4) |
| ViT-B/16 224 px | 16 / 32 | 4,775.4 / 4,679.0 | 4,737.8 (batch 8) |
| ViT-L/16 224 px | 16 / 32 | 1,892.3 / 1,866.8 | 1,905.8 (batch 8) |
| ViT-H+/16 224 px | 16 | 737.2 | 742.7 (batch 8) |
None of these larger batch sizes beat the best smaller one (ViT-B 16 is +0.8%, within noise), so the recommended
batch sizes are unchanged. The fix means a user-chosen batch no longer crashes.
**No cost at the recommended batch sizes:** grouping turns on only when the tiles do not fit. v1.6 vs v1.7 per instance,
same instance, 2 runs each: ViT-S 224 BS 16 -0.02%, ViT-B 224 BS 8 -0.12%, ViT-L 224 BS 8 +0.04%, ViT-H+ 224 BS 8 +0.01%,
ViT-L 448 BS 2 -0.03% (LNC=1); ViT-L 224 BS 8 LNC=2 +0.12%.
## v1.6 (2026-10)
* **Two images per producer pass in `attention_block` too** (new `pack2` argument; the encoder enables it for even
batches up to 256 tokens per image, e.g. 224 px). This kernel serves every model at LNC=2 and single-head-group models
such as ViT-S/16 at LNC=1. v1.5 had packing only in the LNC=1 all-group kernel. Q/K/V projections and RoPE run once
over 2N columns; attention stays per image. Output bit-identical to v1.5 (same rel_l2 vs fp32 on all 20 cases).
Kernel device cycles, ViT-L 224 px LNC=2: batch 4 -8.4% (resident weights) / -12.0% (restaged), batch 2 -8.4%.
Per instance at 224 px, batch 8:
| model | config | v1.5 img/s | v1.6 img/s | change |
|---|---|---|---|---|
| ViT-S/16 | LNC=1 DP=8 | 8,592.8 | **9,365.6** | +9.0% |
| ViT-S/16 | LNC=2 DP=4 | 4,667.6 | 5,061.6 | +8.4% |
| ViT-B/16 | LNC=2 DP=4 | 3,578.3 | 3,856.8 | +7.8% |
| ViT-L/16 | LNC=2 DP=4 | 1,514.8 | 1,589.7 | +4.9% |
| ViT-H+/16 | LNC=2 DP=4 | 507.3 | 554.0 | +9.2% |
Single core, batch 4: +5.9% to +10.8%. Batch 1 and 448 px unchanged (packing off). `VIT_ENC_PACK2=0` disables it.
## v1.5 (2026-10)
* **Two images per producer pass** (LNC=1 all-group attention, `attention_block_l1_ln`, new `pack2` argument, on by
default from the encoder when the batch is even and two images fit one 512-token tile, i.e. up to 256 tokens such as
224 px). The two images' Q/K/V projections and RoPE run as one pass over 2N columns; attention stays per image.
Kernel device cycles, ViT-L 224 px, batch 4: -6.2% (resident weights) / -14.8% (restaged), batch 2: -8.5%.
End to end (real HF weights, LNC=1): per instance (8 cores, batch 8) ViT-L/16 **1,838 -> 1,906 img/s (+3.7%)**,
ViT-H+/16 **672 -> 743 (+10.6%)**, ViT-B/16 **4,390 -> 4,738 (+7.9%)**. Single core batch 4: ViT-L +4.6%, ViT-H+
+12.6%, ViT-B +4.1%. Output bit-identical to v1.4 (same rel_l2 vs fp32 on all 12 cases). `VIT_ENC_PACK2=0` disables it.
A fused-attention-block technique (two images per producer pass).
* **Fix: inputs of 128 tokens or fewer** failed to compile in every attention kernel (`index 1 out of bounds`). They
now use a non-pipelined single-Q-group path (rel_l2 vs a float64 reference 1.9-2.5e-3). Longer inputs are
unchanged (bit-identical).
* **EXPERIMENTAL bf16 RoPE** (`VIT_ENC_ROPE_BF16=1`, off by default): RoPE tables and products in bf16. +0.6% to +3.3%
end to end on DINOv3; rel_l2 vs fp32 grows by up to 1.16x (V-JEPA2: 0.039 -> 0.041).
* **EXPERIMENTAL fused attention write-back** (`VIT_ENC_WB_FUSED=1`, off by default): one Vector multiply normalises and
casts O. -3.4% at 256 tokens / LNC=2, but +6.6% at 2048 tokens (Vector is the attention bottleneck), so off.
## v1.4 (2026-10)
* **QK^T PSUM banks:** for 1024 or fewer tokens per image (two key tiles or fewer), the QK^T matmul now rotates over
five PSUM banks across Q groups instead of reusing four, so it no longer waits for the Vector row-max evict of the
tile four back. Attention kernel device cycles vs v1.3: -2.1% (N=256 and N=1024, LNC=2), -1.1% (N=1024, LNC=1),
-4.7% (all-group kernel, N=256, LNC=1). At 2048 tokens the old layout is kept (the rotation collided with other banks
and measured +7%). `VIT_ENC_MM1_WIDE=0` restores the v1.3 layout.
End to end (single core, real HF weights) vs v1.3: DINOv3 ViT-L/16 -0.5% to +3.7%, ViT-H+/16 0.0% to +2.6%.
V-JEPA2 (2048 tokens): 23.80 ms/clip vs 23.81, output identical to v1.3. All 24 DINOv3 cases pass (rel_l2 vs fp32
0.56-1.75e-2).
* **EXPERIMENTAL FP8 MLP** (`VIT_ENC_MLP_FP8=1`, GELU-MLP models only, off by default): fc1 and fc2 run as e4m3
`double_row` matmuls, with power-of-two weight scaling and a per-layer GELU-output scale (`calibrate_fp8`). Measured
on DINOv3 ViT-L/16, single core: +7.3% (224 px, LNC=2) to +10.5% (448 px, LNC=1) end to end. On imagenette
(real weights), k-NN and linear-probe predictions were identical to fp32. But the CLS feature error rises to
rel_l2 5.8e-2 (bf16 path: 0.84e-2). **Calibrate before use**: trn2's e4m3 saturates at 240, and DINOv3's first
block produces GELU outputs near 2,000. Without calibration the default scale (1/32) is used, which is safe for
DINOv3 but coarse.
* Outputs are deterministic per compiled graph. Across separate compilations they can differ at bf16-ulp level
(rel ~1e-5), as in v1.3.
## v1.3 (2026-10)
Attention-path schedule improvements, plus a SwiGLU fallback fix. Output of every attention kernel is
**bitwise identical** to v1.2. Verified on trn2.3xlarge at LNC=1 and LNC=2, PyTorch Native Beta 6.
Device-cycle A/B, v1.2 vs v1.3 (median of 3 captures, same instance, container and inputs):
| Kernel / shape | LNC | change |
|---|---|---|
| `attention_block`, N=1024, B=2 | 2 | -1.9% |
| `attention_block`, N=2048, B=1 (V-JEPA2 clip) | 2 | **-5.2%** |
| `attention_block`, N=256, B=8 | 2 | -2.0% |
| `attention_block`, N=1024, B=2 | 1 | -3.3% |
| `attention_block_l1`, N=256, B=4 | 1 | **-11.1%** |
| `attention_block_l1_ln`, N=256, B=4 | 1 | **-10.2%** |
| `attention_block_l1_ln`, N=1024, B=2 | 1 | -4.5% |
End to end (real Hugging Face weights, single core, vs v1.0 on the same harness): DINOv3 ViT-L/16 +1.0% to
+12.7%, ViT-H+/16 +0.6% to +17.3%. Largest gains are at 224 px and batch 1, where attention's fixed costs
dominate. V-JEPA2 (16 frames, LNC=2): 24.78 -> 24.48 ms/clip vs v1.2, output identical to v1.2.
All 16 DINOv3 cases pass rel_l2 <= 5e-2 vs fp32 (0.56-0.98e-2).
Changes:
* **Q/K bias in one DMA.** `pack_weights` now returns the Q/K bias packed as `[2, 128, Fp/128]` fp32
(`pack_qk_bias`, exported). Each attention program loads its groups with one DMA per Q/K on the Scalar
HW-DGE queue, replacing one `[128, 1]` DMA plus one cast per 128-row group. The values are rounded to bf16
first, which keeps the output bit-identical. The kernels still accept the legacy `[2, Fp, 1]` layout.
* **Dead work removed in the attention core** (`attention_inner.py`, single-section path, N <= 2048):
the `mm2_sb` and `exp_partial_sum` zero-fills (never observed), and the `tensor_reduce` over the chained
exp-sum columns (the sum is already in one column; it is now read directly).
* **Chained row-max reduce**: the QK^T row-max accumulates across the 512-key tiles of a large tile and is
read out once, instead of once per tile followed by a `tensor_reduce`. Pattern from the upstream nkilib
`attention_cte` row-max chaining, re-implemented for this fork. It adds -3.5% at N=2048 and is neutral at
N <= 1024 (fewer key tiles to chain).
* **Fix (v1.2 regression):** the SwiGLU torch fallback (taken below 256 rows per core, e.g. ViT-H+/16 at
LNC=2, batch 1, 224 px) used the packed biases and failed to compile. It now uses unpacked copies. The
kernel path was unaffected.
## v1.2 (2026-10)
Applied the v1.1 packed-bias one-DMA technique to the SwiGLU FFN kernels (`swiglu_stream`,
`swiglu_stream_ln`). The silu bias `b1` and the up-projection bias `bg` are now host-packed to
`[128, I/128]` (`pack_b1`) and each loaded in a single DMA on the Scalar engine's HW-DGE queue,
replacing `I/128` `[128, 1]` column DMAs apiece. Output is **bitwise identical** to v1.1 at LNC=1 and
LNC=2 (`torch.equal`, verified on trn2.3xlarge). Device cycles (same stack, A/B): `swiglu_stream`
-2.5% (reproducible); `swiglu_stream_ln` within measurement noise (the LN epilogue dominates and the
bias DMAs already overlap compute). The wrappers still accept an unpacked `[I]` bias (packed on the
fly). Affects only the gated-MLP (SwiGLU) models; the GELU-MLP path is unchanged from v1.1.
## v1.1 (2026-10)
Reworked DMA/engine schedule for `mlp` (K4), `mlp_res_ln` (K4 + next LN) and `res_layernorm_fold`
(K1/K3). Output is **bitwise identical** to v1 at both LNC=1 and LNC=2. Based on an external
contribution by [jlongen-aws](https://huggingface.co/jlongen-aws) (PR #2 on
`jburtoft/vjepa2-neuron-kernels`), re-implemented here with the LNC-parametric path and the `min()`
remainder handling preserved.
Verified on trn2.3xlarge (A/B on one consistent stack: old vs new compiled and run in the same
container + inputs, device cycles from `neuron-explorer show-session`). LNC=2, V-JEPA2 ViT-L shape
(R=2048, H=1024, MLP hidden 4096):
* K4 `mlp`: -7.5% device cycles (reproducible, -7.8% +/- ~1% over 3 captures).
* K4+LN `mlp_res_ln`: -6.8%.
* K1 `res_layernorm_fold` (1 branch): -16.2%; K3 (2 branches): -18.3%.
* Full 24-layer encoder (synthetic ViT-L weights, includes the unchanged attention): -3.7% device
cycles, output `torch.equal` to v1.
* Real-weight V-JEPA2 (`facebook/vjepa2-vitl-fpc64-256`, 16 frames x 256 px, LNC=2): output bitwise
identical to v1 (`new_vs_v1_rel` 0.0), rel_l2 0.0389 vs fp32 CPU (unchanged from v1), -4.0% ms/clip
(speedup vs stock fp32 3.19x -> 3.33x).
* Per-kernel output is `torch.equal` to v1 at LNC=1 and LNC=2.
Changes:
* `mlp` / `mlp_res_ln`: fc1 bias is host-packed to `[128, H/128]` (`pack_b1`, now exported) and
loaded in one DMA on the Scalar engine's HW-DGE queue instead of `H/128` column DMAs; row block 0's
`x` is staged and transposed on the (idle) Tensor engine before the weights, later row blocks stream
under row block 0's matmuls. `mlp` still accepts an unpacked `[H]` bias (packed on the fly).
* `res_layernorm_fold`: every DMA uses `dge_mode.none` (the runtime pre-generates descriptors) and all
token tiles are made SBUF-resident before any compute, replacing the depth-4 ring. Per-tile statistic
columns (`[128, n_t]`) so one tile's stat chain does not wait on the previous tile's.
* `pack_block` now stores the GELU fc1 bias pre-packed; `pack_b1` is exported from `nki_kernels`.
The compute instructions and their order are unchanged, so results are bit-identical to v1.
## v1 (2026-10)
First release.
* Drop-in layers for Hugging Face Transformers: `NeuronDINOv3ViTModel` (DINOv3 ViT-S/16, S+/16, B/16,
L/16, H+/16) and `NeuronVJEPA2Encoder` (V-JEPA2 ViT-L, any batch size, up to 2048 tokens per clip).
* Model-agnostic encoder loop: `BlockWeights`, `pack_block`, `rope_tables`, `EncoderRunner`.
* Kernels: `attention_block` (per head group; batched; padded keys skipped; head_dim 64 or 128;
any head count), `attention_block_l1` and `attention_block_l1_ln` (LNC=1, all head groups, optional
residual + LayerNorm epilogue), `res_layernorm_fold`, `mlp` and `mlp_res_ln` (GELU MLP, optional
residual + next LayerNorm epilogue), `swiglu_stream` and `swiglu_stream_ln` (weight-streaming
SwiGLU FFN).
* RoPE pairing selectable at packing time: `rope="half"` (rotate-half) or `rope="interleaved"`.
* Verified on trn2.3xlarge, PyTorch Native Beta 6 (torch 2.13.0, torch-neuronx 2.13.3, NKI 0.7.0b1),
Transformers 4.57.6, LNC=1 and LNC=2. Every measured case passes rel_l2 <= 5e-2 vs the fp32 CPU model.
Known limitations: encoder (bidirectional) attention with RoPE only; LayerNorm (not RMSNorm);
<= 2048 tokens per image; inference only; hidden size 4096 (ViT-7B) not covered by the drop-in layer.
|