jburtoft commited on
Commit
567f054
·
1 Parent(s): 1fc5661

v1.8: post-LayerNorm encoders (NeuronXLMRobertaModel; post-LN epilogue outputs + no-RoPE switch) and LNC=2 fix for odd head-group counts

Browse files
CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
  # Changelog
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ## v1.7 (2026-10)
4
 
5
  * **Fix: `res_layernorm_fold` failed to compile at large batch** (`NCC_IBIR229`, state buffer allocation failed). The
 
1
  # Changelog
2
 
3
+ ## v1.8 (2026-10)
4
+
5
+ * **New: post-LayerNorm encoders (BERT / RoBERTa / XLM-R).** `NeuronXLMRobertaModel` is a drop-in forward for
6
+ `XLMRobertaModel`, `RobertaModel` and `BertModel` (GELU, absolute positions, no RoPE). It runs on the same kernels via
7
+ two new options:
8
+ - `attention_block_l1_ln_post` / `mlp_res_ln_post`: the existing fused attention and MLP epilogues also emit the
9
+ post-LN residual `LN(x_new) * gamma + beta` in fp32, computed from the mean and rstd they already have.
10
+ - `norope=1` on the attention kernel skips the rotation (and its table loads) for models without RoPE.
11
+
12
+ XLM-RoBERTa-Large, LNC=1, 8 processes (one per logical core) on one trn2.3xlarge, fully padded batches (inferences/s):
13
+
14
+ | seq | stock `torch.compile` | v1.8 `NeuronXLMRobertaModel` (`attention_mask=None`) | speedup | rel_l2 vs fp32 (stock / kernels) |
15
+ |---|---|---|---|---|
16
+ | 128 | 2,092.7 | **3,144.8** | 1.50x | 0.031 / 0.0066 |
17
+ | 256 | 944.4 (BS 16) | **1,869.5** | 1.98x | 0.051 / 0.0147 |
18
+ | 512 | 397.1 (BS 8) | **935.1** | 2.35x | 0.018 / 0.0143 |
19
+
20
+ Batch per core: 16 / 8 / 4. Passing an all-ones `attention_mask` costs 3-4% (a host-side check that no position is
21
+ 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).
22
+
23
+ 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,
24
+ 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)
25
+ and no padding mask (all positions attended). Anything else falls back to the stock forward.
26
+ * **Fix: models with an odd number of head groups failed to compile at LNC=2 on neuronx-cc 2.28** (`NCC_EVRF066`: a
27
+ 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
28
+ an odd group count with one all-zero head group, so every attention launch uses a two-program grid. The padded heads have
29
+ zero V and out-projection weights and add exactly 0. Models with an even group count (ViT-B, ViT-L, V-JEPA2) are unchanged.
30
+ 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
31
+ 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
32
+ fallback on the same core.
33
+ * **No change for existing models at existing shapes:** in the 28-case regression (DINOv3 S/S+/B/L/H+ at 224/448 and
34
+ 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
35
+ (within ±1%). That is 22 cases. The other 6 are the odd-group LNC=2 cases above, which v1.7 cannot compile on this
36
+ toolchain. Verified on the SDK 2.33 DLAMI.
37
+
38
  ## v1.7 (2026-10)
39
 
40
  * **Fix: `res_layernorm_fold` failed to compile at large batch** (`NCC_IBIR229`, state buffer allocation failed). The
README.md CHANGED
@@ -13,6 +13,8 @@ tags:
13
  - vjepa2
14
  - rope
15
  - swiglu
 
 
16
  ---
17
 
18
  # vit-encoder-neuron-kernels
@@ -22,15 +24,16 @@ Trainium2 (trn2). Each block is 2-3 coarse kernels instead of dozens of small op
22
  the residual add and LayerNorm, and the MLP / SwiGLU FFN plus the next block's LayerNorm. The
23
  residual stream stays in fp32 between kernels.
24
 
25
- Drop-in layers are included for two Hugging Face Transformers models:
26
 
27
  | Layer | Replaces | Models |
28
  |---|---|---|
29
  | `NeuronDINOv3ViTModel` | `DINOv3ViTModel.forward` | DINOv3 ViT-S/16, S+/16, B/16, L/16, H+/16 |
30
  | `NeuronVJEPA2Encoder` | `VJEPA2Encoder.forward` | V-JEPA2 ViT-L (video), up to 2048 tokens per clip |
 
31
 
32
  The building blocks (`BlockWeights` / `pack_block` / `EncoderRunner`) are model-agnostic. Any
33
- pre-norm ViT with the shape constraints below can use them, but only the two models above
34
  have been validated.
35
 
36
  ## Performance (trn2.3xlarge, bf16, `torch.compile(backend="neuron")`)
@@ -141,6 +144,25 @@ activation scale with `calibrate_fp8(runner, gelu_amax)`, because trn2's e4m3 sa
141
  Which LNC to use: **LNC=1** (8 logical cores, one process per core) gives the most throughput per
142
  instance. **LNC=2** (4 larger cores) gives the lowest latency per request.
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  ## Kernels
145
 
146
  | Kernel | What it does |
@@ -173,13 +195,16 @@ RoPE pairings via `rope=`:
173
  * Inference only (no autograd). The layers refuse in training mode.
174
  * ViT-7B-sized models (hidden 4096) are not covered by the drop-in layer. They need tensor parallelism.
175
  The streaming SwiGLU kernel is slower than the compiler's FFN at that width.
176
- * Validated end to end on DINOv3 ViT-B/L/H+ and V-JEPA2 ViT-L only.
 
 
177
 
178
  ## Requirements
179
 
180
  * trn2 (verified on trn2.3xlarge, LNC=1 and LNC=2).
181
  * PyTorch Native (TorchNeuron) with NKI 0.7 and `nkilib`. Verified on Beta 6 (torch 2.13.0,
182
- torch-neuronx 2.13.3, NKI 0.7.0b1, neuronx-cc 2.0.404056).
 
183
  * Transformers with DINOv3 / V-JEPA2 (verified on 4.57.6); `kernels` for `get_kernel`.
184
 
185
  ## Repository layout
 
13
  - vjepa2
14
  - rope
15
  - swiglu
16
+ - bert
17
+ - xlm-roberta
18
  ---
19
 
20
  # vit-encoder-neuron-kernels
 
24
  the residual add and LayerNorm, and the MLP / SwiGLU FFN plus the next block's LayerNorm. The
25
  residual stream stays in fp32 between kernels.
26
 
27
+ Drop-in layers are included for these Hugging Face Transformers models:
28
 
29
  | Layer | Replaces | Models |
30
  |---|---|---|
31
  | `NeuronDINOv3ViTModel` | `DINOv3ViTModel.forward` | DINOv3 ViT-S/16, S+/16, B/16, L/16, H+/16 |
32
  | `NeuronVJEPA2Encoder` | `VJEPA2Encoder.forward` | V-JEPA2 ViT-L (video), up to 2048 tokens per clip |
33
+ | `NeuronXLMRobertaModel` (v1.8) | `XLMRobertaModel.forward` (also `RobertaModel` / `BertModel`) | XLM-RoBERTa-Large (validated); post-LN text encoders without RoPE |
34
 
35
  The building blocks (`BlockWeights` / `pack_block` / `EncoderRunner`) are model-agnostic. Any
36
+ pre-norm ViT with the shape constraints below can use them, but only the models above
37
  have been validated.
38
 
39
  ## Performance (trn2.3xlarge, bf16, `torch.compile(backend="neuron")`)
 
144
  Which LNC to use: **LNC=1** (8 logical cores, one process per core) gives the most throughput per
145
  instance. **LNC=2** (4 larger cores) gives the lowest latency per request.
146
 
147
+ ### XLM-RoBERTa / BERT-family text encoders (v1.8)
148
+
149
+ ```python
150
+ import os
151
+ os.environ.setdefault("NEURON_LOGICAL_NC_CONFIG", "1") # the post-LN path runs at LNC=1
152
+ import types, torch
153
+ from transformers import XLMRobertaModel
154
+ model = XLMRobertaModel.from_pretrained("FacebookAI/xlm-roberta-large", dtype=torch.float32,
155
+ add_pooling_layer=False).eval().to("neuron")
156
+ model.forward = types.MethodType(k.NeuronXLMRobertaModel.forward, model) # instance graft (k = get_kernel(...) as above)
157
+ model = torch.compile(model, backend="neuron")
158
+ ids = tokenizer(texts, padding="max_length", max_length=128, return_tensors="pt").input_ids # seq % 128 == 0
159
+ out = model(input_ids=ids.to("neuron")).last_hidden_state
160
+ ```
161
+
162
+ The kernel path needs LNC=1, a sequence length that is a multiple of 128 (up to 2048) and no padding mask. Pass
163
+ `attention_mask=None` or all ones; masked batches fall back to the stock forward. Per instance, XLM-RoBERTa-Large at
164
+ seq 128 / 256 / 512 runs at 3,145 / 1,870 / 935 inferences/s, against 2,093 / 944 / 397 stock (see CHANGELOG v1.8).
165
+
166
  ## Kernels
167
 
168
  | Kernel | What it does |
 
195
  * Inference only (no autograd). The layers refuse in training mode.
196
  * ViT-7B-sized models (hidden 4096) are not covered by the drop-in layer. They need tensor parallelism.
197
  The streaming SwiGLU kernel is slower than the compiler's FFN at that width.
198
+ * Validated end to end on DINOv3 ViT-B/L/H+, V-JEPA2 ViT-L and (v1.8) XLM-RoBERTa-Large.
199
+ * Post-LN text encoders (v1.8, `NeuronXLMRobertaModel`): LNC=1 only, no padding mask, no token-type / relative
200
+ position variants beyond the standard BERT embeddings, GELU FFN.
201
 
202
  ## Requirements
203
 
204
  * trn2 (verified on trn2.3xlarge, LNC=1 and LNC=2).
205
  * PyTorch Native (TorchNeuron) with NKI 0.7 and `nkilib`. Verified on Beta 6 (torch 2.13.0,
206
+ torch-neuronx 2.13.3, NKI 0.7.0b1, neuronx-cc 2.0.404056) and (v1.8) on the **SDK 2.33 DLAMI**
207
+ `torch_neuronx_2_14_0` venv (torch 2.14.0, torch-neuronx 2.14.3.0, NKI 0.7.0, neuronx-cc 2.28.7631).
208
  * Transformers with DINOv3 / V-JEPA2 (verified on 4.57.6); `kernels` for `get_kernel`.
209
 
210
  ## Repository layout
build/torch-neuron/__init__.py CHANGED
@@ -3,22 +3,25 @@
3
  Model layers (drop-in forwards for Hugging Face Transformers):
4
  NeuronDINOv3ViTModel -- DINOv3ViTModel (S/S+/B/L/H+)
5
  NeuronVJEPA2Encoder -- VJEPA2Encoder
 
6
 
7
  Model-agnostic building blocks:
8
  BlockWeights, pack_block, rope_tables, EncoderRunner -- pre-norm encoder loop on the kernels
9
- kernels: attention_block, attention_block_l1, attention_block_l1_ln, res_layernorm_fold,
10
- mlp, mlp_res_ln, swiglu_stream, swiglu_stream_ln
11
  """
12
 
13
  from .encoder import LNC, BlockWeights, EncoderRunner, calibrate_fp8, pack_block, rope_tables
14
- from .layers import NeuronDINOv3ViTModel, NeuronVJEPA2Encoder
15
  from .nki_kernels import (
16
  attention_block,
17
  attention_block_l1,
18
  attention_block_l1_ln,
 
19
  lane_permutation,
20
  mlp,
21
  mlp_res_ln,
 
22
  pack_out_weights,
23
  pack_tables,
24
  pack_v_weights,
@@ -29,9 +32,9 @@ from .nki_kernels import (
29
  )
30
 
31
  __all__ = [
32
- "NeuronDINOv3ViTModel", "NeuronVJEPA2Encoder",
33
  "BlockWeights", "pack_block", "rope_tables", "EncoderRunner", "LNC", "calibrate_fp8",
34
- "attention_block", "attention_block_l1", "attention_block_l1_ln", "res_layernorm_fold",
35
- "mlp", "mlp_res_ln", "swiglu_stream", "swiglu_stream_ln",
36
  "pack_weights", "pack_tables", "pack_v_weights", "pack_out_weights", "lane_permutation",
37
  ]
 
3
  Model layers (drop-in forwards for Hugging Face Transformers):
4
  NeuronDINOv3ViTModel -- DINOv3ViTModel (S/S+/B/L/H+)
5
  NeuronVJEPA2Encoder -- VJEPA2Encoder
6
+ NeuronXLMRobertaModel -- XLMRobertaModel / RobertaModel / BertModel (post-LN, no RoPE; v1.8)
7
 
8
  Model-agnostic building blocks:
9
  BlockWeights, pack_block, rope_tables, EncoderRunner -- pre-norm encoder loop on the kernels
10
+ kernels: attention_block, attention_block_l1, attention_block_l1_ln(_post), res_layernorm_fold,
11
+ mlp, mlp_res_ln(_post), swiglu_stream, swiglu_stream_ln
12
  """
13
 
14
  from .encoder import LNC, BlockWeights, EncoderRunner, calibrate_fp8, pack_block, rope_tables
15
+ from .layers import NeuronDINOv3ViTModel, NeuronVJEPA2Encoder, NeuronXLMRobertaModel
16
  from .nki_kernels import (
17
  attention_block,
18
  attention_block_l1,
19
  attention_block_l1_ln,
20
+ attention_block_l1_ln_post,
21
  lane_permutation,
22
  mlp,
23
  mlp_res_ln,
24
+ mlp_res_ln_post,
25
  pack_out_weights,
26
  pack_tables,
27
  pack_v_weights,
 
32
  )
33
 
34
  __all__ = [
35
+ "NeuronDINOv3ViTModel", "NeuronVJEPA2Encoder", "NeuronXLMRobertaModel",
36
  "BlockWeights", "pack_block", "rope_tables", "EncoderRunner", "LNC", "calibrate_fp8",
37
+ "attention_block", "attention_block_l1", "attention_block_l1_ln", "attention_block_l1_ln_post", "res_layernorm_fold",
38
+ "mlp", "mlp_res_ln", "mlp_res_ln_post", "swiglu_stream", "swiglu_stream_ln",
39
  "pack_weights", "pack_tables", "pack_v_weights", "pack_out_weights", "lane_permutation",
40
  ]
build/torch-neuron/encoder.py CHANGED
@@ -105,6 +105,11 @@ def pack_block(bw: BlockWeights, num_heads: int, rope: str, device) -> dict:
105
  D = F_ // num_heads
106
  gsz = 512 // D # heads per kernel program
107
  nh_p = ((num_heads + gsz - 1) // gsz) * gsz # padded head count
 
 
 
 
 
108
  Fp = nh_p * D
109
  ngrp = nh_p // gsz
110
  g1, t1, g2, t2 = _f(bw.ln1_w), _f(bw.ln1_b), _f(bw.ln2_w), _f(bw.ln2_b)
@@ -167,6 +172,8 @@ def rope_tables(cos: torch.Tensor, sin: torch.Tensor, n_prefix: int, n_pad: int,
167
  S[n_prefix:n_prefix + n_rot] = sin.float().cpu()
168
  gsz = 512 // D
169
  nh_p = ((num_heads + gsz - 1) // gsz) * gsz
 
 
170
  t = _ab.pack_tables(C, S, nh_p, D, rope)
171
  if _osr.environ.get("VIT_ENC_ROPE_BF16", "0") == "1": # EXPERIMENTAL bf16 RoPE (tables in bf16)
172
  t = t.to(torch.bfloat16)
 
105
  D = F_ // num_heads
106
  gsz = 512 // D # heads per kernel program
107
  nh_p = ((num_heads + gsz - 1) // gsz) * gsz # padded head count
108
+ if LNC == 2 and (nh_p // gsz) % 2 == 1:
109
+ # v1.8: at LNC=2 every attention launch must use a 2-program grid (newer neuronx-cc rejects a 1-program
110
+ # grid on an LNC=2 build: NCC_EVRF066). Pad an odd group count with one all-zero head group; its V and
111
+ # out-projection weights are zero, so it adds exactly 0 to the output.
112
+ nh_p += gsz
113
  Fp = nh_p * D
114
  ngrp = nh_p // gsz
115
  g1, t1, g2, t2 = _f(bw.ln1_w), _f(bw.ln1_b), _f(bw.ln2_w), _f(bw.ln2_b)
 
172
  S[n_prefix:n_prefix + n_rot] = sin.float().cpu()
173
  gsz = 512 // D
174
  nh_p = ((num_heads + gsz - 1) // gsz) * gsz
175
+ if LNC == 2 and (nh_p // gsz) % 2 == 1:
176
+ nh_p += gsz # same padding as pack_block (v1.8)
177
  t = _ab.pack_tables(C, S, nh_p, D, rope)
178
  if _osr.environ.get("VIT_ENC_ROPE_BF16", "0") == "1": # EXPERIMENTAL bf16 RoPE (tables in bf16)
179
  t = t.to(torch.bfloat16)
build/torch-neuron/layers.py CHANGED
@@ -218,3 +218,131 @@ class NeuronVJEPA2Encoder(torch.nn.Module):
218
  x = getattr(self, _PACK)(_stack(hs, N), tabs[N], n, B)
219
  out = self.layernorm(x.reshape(B, N, H)[:, :n])
220
  return BaseModelOutput(last_hidden_state=out)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  x = getattr(self, _PACK)(_stack(hs, N), tabs[N], n, B)
219
  out = self.layernorm(x.reshape(B, N, H)[:, :n])
220
  return BaseModelOutput(last_hidden_state=out)
221
+
222
+
223
+ # ------------------------------------------------------------------------------------------------
224
+ # XLM-RoBERTa / RoBERTa / BERT (post-LayerNorm encoders, no RoPE) -- v1.8
225
+ # ------------------------------------------------------------------------------------------------
226
+ _POST = "_vit_enc_neuron_postln"
227
+
228
+
229
+ def _bert_blocks(model):
230
+ """BlockWeights per layer for a post-LN BERT-family encoder. Block i's "ln1" is the LayerNorm that produced
231
+ its input (embeddings.LayerNorm for block 0, else layer i-1's output.LayerNorm) and "ln2" its attention-output
232
+ LayerNorm, so the kernels' affine-free LN + folded affine reproduce the post-LN math exactly."""
233
+ out, prev = [], model.embeddings.LayerNorm
234
+ for lyr in model.encoder.layer:
235
+ a = lyr.attention
236
+ out.append(BlockWeights(
237
+ ln1_w=prev.weight, ln1_b=prev.bias, ln1_eps=prev.eps,
238
+ wq=a.self.query.weight, bq=a.self.query.bias, wk=a.self.key.weight, bk=a.self.key.bias,
239
+ wv=a.self.value.weight, bv=a.self.value.bias, wo=a.output.dense.weight, bo=a.output.dense.bias,
240
+ ln2_w=a.output.LayerNorm.weight, ln2_b=a.output.LayerNorm.bias, ln2_eps=a.output.LayerNorm.eps,
241
+ fc1_w=lyr.intermediate.dense.weight, fc1_b=lyr.intermediate.dense.bias,
242
+ fc2_w=lyr.output.dense.weight, fc2_b=lyr.output.dense.bias))
243
+ prev = lyr.output.LayerNorm
244
+ return out
245
+
246
+
247
+ def _bert_ok(model, S):
248
+ c = model.config
249
+ return (_supported(c.hidden_size, c.num_attention_heads) and getattr(c, "hidden_act", "gelu") == "gelu"
250
+ and getattr(c, "position_embedding_type", "absolute") == "absolute" and S <= _MAX_TOKENS
251
+ and S % (128 * LNC) == 0 and LNC == 1)
252
+
253
+
254
+ @torch._dynamo.disable
255
+ def _bert_freeze(model):
256
+ if model.training:
257
+ raise RuntimeError("NeuronXLMRobertaModel: these kernels have no autograd rule; call model.eval() first.")
258
+ c = model.config
259
+ dev = model.embeddings.word_embeddings.weight.device
260
+ heads, H = c.num_attention_heads, c.hidden_size
261
+ D = H // heads
262
+ runner = EncoderRunner([pack_block(bw, heads, "half", dev) for bw in _bert_blocks(model)])
263
+ f32 = lambda t: t.detach().float().to(dev)
264
+ lns = [model.embeddings.LayerNorm] + [l.output.LayerNorm for l in model.encoder.layer]
265
+ st = {"runner": runner, "D": D, "heads": heads, "H": H, "tabs": {},
266
+ "ln_in": [(f32(m.weight), f32(m.bias), float(m.eps)) for m in lns],
267
+ "ln_attn": [(f32(l.attention.output.LayerNorm.weight), f32(l.attention.output.LayerNorm.bias),
268
+ float(l.attention.output.LayerNorm.eps)) for l in model.encoder.layer]}
269
+ setattr(model, _POST, st)
270
+
271
+
272
+ @torch._dynamo.disable
273
+ def _bert_tab(st, S, dev):
274
+ if S not in st["tabs"]:
275
+ D = st["D"] # no RoPE: identity tables (the kernel skips them, norope=1)
276
+ st["tabs"][S] = rope_tables(torch.ones(S, D), torch.zeros(S, D), 0, S, st["heads"], D, "half", dev)
277
+ return st["tabs"][S]
278
+
279
+
280
+ @torch._dynamo.disable
281
+ def _bert_masked(attention_mask) -> bool:
282
+ """True if any position is masked out. Runs outside the compiled graph (one small host check per call)."""
283
+ return not bool(torch.all(attention_mask == 1))
284
+
285
+
286
+ class NeuronXLMRobertaModel(torch.nn.Module):
287
+ """Drop-in forward for ``XLMRobertaModel`` / ``RobertaModel`` / ``BertModel`` (post-LN, GELU, absolute positions).
288
+
289
+ Graft on an instance (``model.forward = types.MethodType(NeuronXLMRobertaModel.forward, model)``); do not use it
290
+ as a class-level KernelConfig replacement, because the fallback calls ``type(model).forward``.
291
+
292
+ Returns ``last_hidden_state``. Kernel path: LNC=1, sequence length a multiple of 128 and <= 2048, all positions
293
+ attended (``attention_mask`` all ones / no padding). Anything else (padding mask, LNC=2, other lengths,
294
+ off-device, training) falls back to the stock forward.
295
+ """
296
+
297
+ can_torch_compile = True
298
+ has_backward = False
299
+
300
+ def forward(self, input_ids=None, attention_mask=None, token_type_ids=None, position_ids=None, **kwargs):
301
+ import torch.nn.functional as F
302
+ from transformers.modeling_outputs import BaseModelOutputWithPoolingAndCrossAttentions as Out
303
+ from .encoder import _ab1ln
304
+ from .nki_kernels.mlp import nki_mlp_k4_xp
305
+ from .nki_kernels.mlp_res_ln import nki_mlp_res_ln_post
306
+ from .nki_kernels.res_layernorm import nki_res_layernorm_fold
307
+ S = input_ids.shape[1] if input_ids is not None else -1
308
+ # The mask check runs outside the compiled graph (one host sync per call); pass attention_mask=None to skip it.
309
+ masked = attention_mask is not None and _bert_masked(attention_mask)
310
+ if (input_ids is None or input_ids.device.type != "neuron" or self.training or masked or position_ids is not None
311
+ or kwargs.get("inputs_embeds") is not None or not _bert_ok(self, S)):
312
+ # stock forward of the grafted instance's own class (XLMRobertaModel / RobertaModel / BertModel). The graft
313
+ # sets an INSTANCE attribute (types.MethodType), so the class attribute is still the stock forward.
314
+ return type(self).forward(self, input_ids=input_ids, attention_mask=attention_mask,
315
+ token_type_ids=token_type_ids, position_ids=position_ids, **kwargs)
316
+ if not hasattr(self, _POST):
317
+ _bert_freeze(self)
318
+ st = getattr(self, _POST)
319
+ R, H, D = st["runner"], st["H"], st["D"]
320
+ B = input_ids.shape[0]
321
+ # the kernels need the PRE-LayerNorm embedding sum (the embedding LN is block 0's "ln1"); same as embeddings()
322
+ e = self.embeddings
323
+ pos = e.position_ids[:, :S] if getattr(e, "padding_idx", None) is None else None
324
+ if pos is None:
325
+ m = input_ids.ne(e.padding_idx).int()
326
+ pos = (torch.cumsum(m, dim=1) * m).long() + e.padding_idx
327
+ tt = token_type_ids if token_type_ids is not None else torch.zeros_like(input_ids)
328
+ x = (e.word_embeddings(input_ids) + e.position_embeddings(pos) + e.token_type_embeddings(tt)).float()
329
+ x = x.reshape(B * S, H).contiguous()
330
+ tab = _bert_tab(st, S, x.device)
331
+ g0, b0, eps0 = st["ln_in"][0]
332
+ _, z1 = nki_res_layernorm_fold(x, x.new_zeros(1, x.shape[0], H, dtype=torch.bfloat16), eps0)
333
+ h = F.layer_norm(x, (H,), g0, b0, eps0)
334
+ nL = len(R.L)
335
+ from .encoder import _PACK2
336
+ pk2 = int(_PACK2 and B % 2 == 0 and 2 * S <= 512)
337
+ for li, p in enumerate(R.L):
338
+ ga, ba, _ = st["ln_attn"][li]
339
+ _, z2, h1 = _ab1ln.attention_block_l1_ln_post(
340
+ z1, p["wqk"], p["bqk"], tab, p["wvt"], p["bv"], p["wot"], p["bo_full"], h, p["scale"], D, S, p["eps2"],
341
+ ga, ba, B, R._res(_ab1ln, S), pk2, 1)
342
+ go, bo, eo = st["ln_in"][li + 1]
343
+ if li + 1 < nL:
344
+ _, z1, h = nki_mlp_res_ln_post(z2, p["w1t"], p["b1"], p["w2t"], p["b2"], h1, eo, go, bo)
345
+ else:
346
+ h = F.layer_norm(h1 + nki_mlp_k4_xp(z2, p["w1t"], p["b1"], p["w2t"], p["b2"]).float(), (H,), go, bo, eo)
347
+ out = h.reshape(B, S, H)
348
+ return Out(last_hidden_state=out, pooler_output=None)
build/torch-neuron/nki_kernels/__init__.py CHANGED
@@ -10,16 +10,17 @@ from .attention_block import (
10
  pack_weights,
11
  )
12
  from .attention_block_l1 import attention_block_l1
13
- from .attention_block_l1_ln import attention_block_l1_ln
14
  from .mlp import nki_mlp_k4_xp as mlp
15
  from .mlp import pack_b1
16
  from .mlp_res_ln import nki_mlp_res_ln as mlp_res_ln
 
17
  from .res_layernorm import nki_res_layernorm_fold as res_layernorm_fold
18
  from .swiglu_stream import nki_swiglu_stream as swiglu_stream
19
  from .swiglu_stream import nki_swiglu_stream_ln as swiglu_stream_ln
20
 
21
  __all__ = [
22
- "attention_block", "attention_block_l1", "attention_block_l1_ln", "res_layernorm_fold",
23
- "mlp", "mlp_res_ln", "pack_b1", "swiglu_stream", "swiglu_stream_ln",
24
  "pack_weights", "pack_qk_bias", "pack_tables", "pack_v_weights", "pack_out_weights", "lane_permutation",
25
  ]
 
10
  pack_weights,
11
  )
12
  from .attention_block_l1 import attention_block_l1
13
+ from .attention_block_l1_ln import attention_block_l1_ln, attention_block_l1_ln_post
14
  from .mlp import nki_mlp_k4_xp as mlp
15
  from .mlp import pack_b1
16
  from .mlp_res_ln import nki_mlp_res_ln as mlp_res_ln
17
+ from .mlp_res_ln import nki_mlp_res_ln_post as mlp_res_ln_post
18
  from .res_layernorm import nki_res_layernorm_fold as res_layernorm_fold
19
  from .swiglu_stream import nki_swiglu_stream as swiglu_stream
20
  from .swiglu_stream import nki_swiglu_stream_ln as swiglu_stream_ln
21
 
22
  __all__ = [
23
+ "attention_block", "attention_block_l1", "attention_block_l1_ln", "attention_block_l1_ln_post", "res_layernorm_fold",
24
+ "mlp", "mlp_res_ln", "mlp_res_ln_post", "pack_b1", "swiglu_stream", "swiglu_stream_ln",
25
  "pack_weights", "pack_qk_bias", "pack_tables", "pack_v_weights", "pack_out_weights", "lane_permutation",
26
  ]
build/torch-neuron/nki_kernels/attention_block_l1_ln.py CHANGED
@@ -87,7 +87,8 @@ def sbuf_estimate(H: int, Fp: int, N: int, D: int, resident: bool) -> int:
87
 
88
  @nki.jit
89
  def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_hbm,
90
- wot_hbm, bo_hbm, x_hbm, scale, head_dim, n_valid, eps, nimg=1, resident=0, pack2=0):
 
91
  NT, H = h_hbm.shape
92
  N = NT // nimg
93
  Fp = wqk_hbm.shape[2]
@@ -117,6 +118,9 @@ def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_
117
  _kernel_assert(NT == nimg * N, "rows must be nimg x N")
118
 
119
  _kernel_assert(x_hbm.shape == (NT, H), "x must be [nimg*N, H] fp32")
 
 
 
120
  xnew_hbm = nl.ndarray((NT, H), dtype=nl.float32, buffer=nl.shared_hbm)
121
  z_hbm = nl.ndarray((NT, H), dtype=nl.bfloat16, buffer=nl.shared_hbm)
122
  inv_h = 1.0 / float(H)
@@ -158,13 +162,15 @@ def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_
158
  r_wv.append(wv_sb_)
159
  r_b.append(b_sb_)
160
  r_bv.append(bv_bc_)
161
- r_tab = ra.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=_ROPE_DT,
162
- block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
163
- for s in range(n_tt):
164
- for t_i in range(4):
165
- _tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
166
- nisa.dma_copy(dst=r_tab[t_i][s][:, 0:_tw],
167
- src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
 
 
168
  r_wo = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
169
  block_dim=[G * n_ochunks], num_free_tiles=[G * n_ochunks])
170
  for oc in range(G * n_ochunks):
@@ -208,7 +214,9 @@ def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_
208
  )
209
 
210
  # rope tables: shared by every group.
211
- if resident and PK == 1:
 
 
212
  tab = r_tab
213
  elif PK == 2:
214
  # pack2: the same per-image tables, side by side (one 512-wide tile)
@@ -316,20 +324,26 @@ def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_
316
  nisa.nc_matmul(dst=ps[:, 0:tw],
317
  stationary=w_sb[qk][c][:, cols:cols + _P_MAX],
318
  moving=x_t[c][tt][:, 0:tw], accumulate=(c > 0))
319
- nisa.tensor_scalar(dst=proj[par][:, 0:tw], data=ps[:, 0:tw],
320
- op0=nl.add, operand0=b_sb[qk][:, gq:gq + 1])
321
- nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[0][:, 0:tw],
322
- data2=tab[0][tt][:, 0:tw], op=nl.multiply)
323
- nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[1][:, 0:tw],
324
- data2=tab[2][tt][:, 0:tw], op=nl.multiply)
325
- nisa.tensor_tensor(dst=roped[0][:, 0:tw], data1=acc[:, 0:tw],
326
- data2=tmp[:, 0:tw], op=nl.add)
327
- nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[1][:, 0:tw],
328
- data2=tab[1][tt][:, 0:tw], op=nl.multiply)
329
- nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[0][:, 0:tw],
330
- data2=tab[3][tt][:, 0:tw], op=nl.multiply)
331
- nisa.tensor_tensor(dst=roped[1][:, 0:tw], data1=acc[:, 0:tw],
332
- data2=tmp[:, 0:tw], op=nl.add)
 
 
 
 
 
 
333
  for b in range(quad):
334
  lh = j * quad + b
335
  r0 = b * half
@@ -413,6 +427,11 @@ def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_
413
  st.append(allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
414
  block_dim=[5], num_free_tiles=[5], align_to=4))
415
  sink = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
 
 
 
 
 
416
  n_ob = (H + _PSUM_FMAX - 1) // _PSUM_FMAX
417
  for tb in range(n_grp):
418
  k = tb % _RING
@@ -455,10 +474,21 @@ def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_
455
  nisa.tensor_scalar(dst=z_t[k], data=row[k], op0=nl.add, operand0=neg_mu,
456
  op1=nl.multiply, operand1=rstd)
457
  nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:H], src=z_t[k])
 
 
 
 
 
 
 
 
 
458
  return xnew_hbm, z_hbm
459
 
460
 
461
  _ATTN_L1_LN = wrap_nki_to_torch_hop(_attention_block_l1_ln_kernel[1])
 
 
462
 
463
 
464
  @nki_op("vit_enc::attention_block_l1_ln", mutates_args={})
@@ -489,3 +519,20 @@ def attention_block_l1_ln(
489
  if bqk.dim() == 3 and bqk.shape[1] != 128:
490
  bqk = _pack_qk_bias(bqk)
491
  return _ATTN_L1_LN(h, wqk, bqk, tab, wvt, bv, wot, bo, x, scale, head_dim, n_valid, eps, nimg, resident, pack2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  @nki.jit
89
  def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_hbm,
90
+ wot_hbm, bo_hbm, x_hbm, scale, head_dim, n_valid, eps, nimg=1, resident=0, pack2=0,
91
+ g_hbm=None, bt_hbm=None, norope=0):
92
  NT, H = h_hbm.shape
93
  N = NT // nimg
94
  Fp = wqk_hbm.shape[2]
 
118
  _kernel_assert(NT == nimg * N, "rows must be nimg x N")
119
 
120
  _kernel_assert(x_hbm.shape == (NT, H), "x must be [nimg*N, H] fp32")
121
+ post = g_hbm is not None # post-LN mode: also emit h = LN(x_new) * g + bt (fp32)
122
+ if post:
123
+ hout_hbm = nl.ndarray((NT, H), dtype=nl.float32, buffer=nl.shared_hbm)
124
  xnew_hbm = nl.ndarray((NT, H), dtype=nl.float32, buffer=nl.shared_hbm)
125
  z_hbm = nl.ndarray((NT, H), dtype=nl.bfloat16, buffer=nl.shared_hbm)
126
  inv_h = 1.0 / float(H)
 
162
  r_wv.append(wv_sb_)
163
  r_b.append(b_sb_)
164
  r_bv.append(bv_bc_)
165
+ r_tab = None
166
+ if not norope:
167
+ r_tab = ra.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=_ROPE_DT,
168
+ block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
169
+ for s in range(n_tt):
170
+ for t_i in range(4):
171
+ _tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
172
+ nisa.dma_copy(dst=r_tab[t_i][s][:, 0:_tw],
173
+ src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
174
  r_wo = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
175
  block_dim=[G * n_ochunks], num_free_tiles=[G * n_ochunks])
176
  for oc in range(G * n_ochunks):
 
214
  )
215
 
216
  # rope tables: shared by every group.
217
+ if norope:
218
+ tab = None
219
+ elif resident and PK == 1:
220
  tab = r_tab
221
  elif PK == 2:
222
  # pack2: the same per-image tables, side by side (one 512-wide tile)
 
324
  nisa.nc_matmul(dst=ps[:, 0:tw],
325
  stationary=w_sb[qk][c][:, cols:cols + _P_MAX],
326
  moving=x_t[c][tt][:, 0:tw], accumulate=(c > 0))
327
+ if norope:
328
+ # no positional rotation (e.g. BERT/XLM-R): bias-add straight into the roped buffers
329
+ nisa.tensor_scalar(dst=roped[par][:, 0:tw], data=ps[:, 0:tw],
330
+ op0=nl.add, operand0=b_sb[qk][:, gq:gq + 1])
331
+ else:
332
+ nisa.tensor_scalar(dst=proj[par][:, 0:tw], data=ps[:, 0:tw],
333
+ op0=nl.add, operand0=b_sb[qk][:, gq:gq + 1])
334
+ if not norope:
335
+ nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[0][:, 0:tw],
336
+ data2=tab[0][tt][:, 0:tw], op=nl.multiply)
337
+ nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[1][:, 0:tw],
338
+ data2=tab[2][tt][:, 0:tw], op=nl.multiply)
339
+ nisa.tensor_tensor(dst=roped[0][:, 0:tw], data1=acc[:, 0:tw],
340
+ data2=tmp[:, 0:tw], op=nl.add)
341
+ nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[1][:, 0:tw],
342
+ data2=tab[1][tt][:, 0:tw], op=nl.multiply)
343
+ nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[0][:, 0:tw],
344
+ data2=tab[3][tt][:, 0:tw], op=nl.multiply)
345
+ nisa.tensor_tensor(dst=roped[1][:, 0:tw], data1=acc[:, 0:tw],
346
+ data2=tmp[:, 0:tw], op=nl.add)
347
  for b in range(quad):
348
  lh = j * quad + b
349
  r0 = b * half
 
427
  st.append(allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
428
  block_dim=[5], num_free_tiles=[5], align_to=4))
429
  sink = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
430
+ if post:
431
+ g_bc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
432
+ bt_bc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
433
+ nisa.dma_copy(dst=g_bc, src=g_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
434
+ nisa.dma_copy(dst=bt_bc, src=bt_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
435
  n_ob = (H + _PSUM_FMAX - 1) // _PSUM_FMAX
436
  for tb in range(n_grp):
437
  k = tb % _RING
 
474
  nisa.tensor_scalar(dst=z_t[k], data=row[k], op0=nl.add, operand0=neg_mu,
475
  op1=nl.multiply, operand1=rstd)
476
  nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:H], src=z_t[k])
477
+ if post:
478
+ # h = (x_new - mu) * rstd * g + bt, fp32; reuse `sink` (its reduce output is dead here)
479
+ nisa.tensor_scalar(dst=sink, data=row[k], op0=nl.add, operand0=neg_mu,
480
+ op1=nl.multiply, operand1=rstd)
481
+ nisa.tensor_tensor(dst=sink, data1=sink, data2=g_bc, op=nl.multiply)
482
+ nisa.tensor_tensor(dst=sink, data1=sink, data2=bt_bc, op=nl.add)
483
+ nisa.dma_copy(dst=hout_hbm[r0:r0 + _P_MAX, 0:H], src=sink)
484
+ if post:
485
+ return xnew_hbm, z_hbm, hout_hbm
486
  return xnew_hbm, z_hbm
487
 
488
 
489
  _ATTN_L1_LN = wrap_nki_to_torch_hop(_attention_block_l1_ln_kernel[1])
490
+ _ATTN_L1_LN_POST = wrap_nki_to_torch_hop(_attention_block_l1_ln_kernel[1])
491
+ _ATTN_L1_LN_POST_NR = _ATTN_L1_LN_POST
492
 
493
 
494
  @nki_op("vit_enc::attention_block_l1_ln", mutates_args={})
 
519
  if bqk.dim() == 3 and bqk.shape[1] != 128:
520
  bqk = _pack_qk_bias(bqk)
521
  return _ATTN_L1_LN(h, wqk, bqk, tab, wvt, bv, wot, bo, x, scale, head_dim, n_valid, eps, nimg, resident, pack2)
522
+
523
+
524
+ @nki_op("vit_enc::attention_block_l1_ln_post", mutates_args={})
525
+ def attention_block_l1_ln_post(
526
+ h: torch.Tensor, wqk: torch.Tensor, bqk: torch.Tensor, tab: torch.Tensor, wvt: torch.Tensor, bv: torch.Tensor,
527
+ wot: torch.Tensor, bo: torch.Tensor, x: torch.Tensor, scale: float, head_dim: int, n_valid: int, eps: float,
528
+ g: torch.Tensor, bt: torch.Tensor, nimg: int = 1, resident: int = 0, pack2: int = 0, norope: int = 0,
529
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
530
+ """Post-LN variant: (x_new fp32, z2 bf16 = LN0(x_new), h fp32 = LN(x_new) * g + bt). g/bt: [H] fp32."""
531
+ for t in (h, wqk, bqk, tab, wvt, bv, wot, bo, x, g, bt):
532
+ if t is not None and t.requires_grad:
533
+ raise RuntimeError("attention_block_l1_ln_post is inference-only; use torch.no_grad().")
534
+ if h.shape[0] // nimg > 2048:
535
+ raise NotImplementedError("attention_block_l1_ln_post supports N<=2048 only")
536
+ if bqk.dim() == 3 and bqk.shape[1] != 128:
537
+ bqk = _pack_qk_bias(bqk)
538
+ return _ATTN_L1_LN_POST(h, wqk, bqk, tab, wvt, bv, wot, bo, x, scale, head_dim, n_valid, eps, nimg, resident, pack2, g, bt, norope)
build/torch-neuron/nki_kernels/mlp_res_ln.py CHANGED
@@ -54,7 +54,7 @@ def _kernel_assert(condition: bool, error_text: str):
54
 
55
 
56
  @nki.jit
57
- def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
58
  """gelu(x @ w1.T + b1) @ w2.T + b2, rows sharded, b1 AND both x blocks resident early.
59
 
60
  Args:
@@ -91,6 +91,9 @@ def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
91
  _kernel_assert(tuple(a_hbm.shape) == (R, K), "residual a must be [R, K] fp32")
92
  xnew_hbm = nl.ndarray((R, K), dtype=nl.float32, buffer=nl.shared_hbm)
93
  z_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
 
 
 
94
  inv_k = 1.0 / float(K)
95
  neg_inv_k = -inv_k
96
 
@@ -141,6 +144,11 @@ def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
141
  # b2 broadcast ONCE over all K columns (fp32), plus LN per-lane scratch.
142
  b2_bc = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf)
143
  nisa.dma_copy(dst=b2_bc[0:_P_MAX, 0:K], src=b2_hbm.ap(pattern=[[0, _P_MAX], [1, K]], offset=0))
 
 
 
 
 
144
  acc_s = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
145
  neg_shift = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
146
  neg_mu = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
@@ -221,7 +229,18 @@ def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
221
  op0=nl.add, operand0=neg_mu[0:prs, 0:1],
222
  op1=nl.multiply, operand1=rstd[0:prs, 0:1])
223
  nisa.dma_copy(dst=z_hbm[r0:r0 + prs, 0:K], src=z_t[0:prs, 0:K])
224
-
 
 
 
 
 
 
 
 
 
 
 
225
  return xnew_hbm, z_hbm
226
 
227
 
@@ -248,3 +267,15 @@ def nki_mlp_res_ln(
248
  if b1.dim() == 1:
249
  b1 = pack_b1(b1)
250
  return _mlp_res_ln_kernel[LNC](x, w1t, b1, w2t, b2, a, eps)
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
 
56
  @nki.jit
57
+ def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps, g_hbm=None, bt_hbm=None):
58
  """gelu(x @ w1.T + b1) @ w2.T + b2, rows sharded, b1 AND both x blocks resident early.
59
 
60
  Args:
 
91
  _kernel_assert(tuple(a_hbm.shape) == (R, K), "residual a must be [R, K] fp32")
92
  xnew_hbm = nl.ndarray((R, K), dtype=nl.float32, buffer=nl.shared_hbm)
93
  z_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
94
+ post = g_hbm is not None # post-LN mode: also emit h = LN(x_new) * g + bt (fp32)
95
+ if post:
96
+ h_hbm = nl.ndarray((R, K), dtype=nl.float32, buffer=nl.shared_hbm)
97
  inv_k = 1.0 / float(K)
98
  neg_inv_k = -inv_k
99
 
 
144
  # b2 broadcast ONCE over all K columns (fp32), plus LN per-lane scratch.
145
  b2_bc = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf)
146
  nisa.dma_copy(dst=b2_bc[0:_P_MAX, 0:K], src=b2_hbm.ap(pattern=[[0, _P_MAX], [1, K]], offset=0))
147
+ if post:
148
+ g_bc = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf)
149
+ bt_bc = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf)
150
+ nisa.dma_copy(dst=g_bc[0:_P_MAX, 0:K], src=g_hbm.ap(pattern=[[0, _P_MAX], [1, K]], offset=0))
151
+ nisa.dma_copy(dst=bt_bc[0:_P_MAX, 0:K], src=bt_hbm.ap(pattern=[[0, _P_MAX], [1, K]], offset=0))
152
  acc_s = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
153
  neg_shift = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
154
  neg_mu = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
 
229
  op0=nl.add, operand0=neg_mu[0:prs, 0:1],
230
  op1=nl.multiply, operand1=rstd[0:prs, 0:1])
231
  nisa.dma_copy(dst=z_hbm[r0:r0 + prs, 0:K], src=z_t[0:prs, 0:K])
232
+ if post:
233
+ # h = (x_new - mu) * rstd * g + bt in fp32 (the post-LN residual for the next sublayer)
234
+ h_t = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf)
235
+ nisa.tensor_scalar(dst=h_t[0:prs, 0:K], data=row[0:prs, 0:K],
236
+ op0=nl.add, operand0=neg_mu[0:prs, 0:1],
237
+ op1=nl.multiply, operand1=rstd[0:prs, 0:1])
238
+ nisa.tensor_tensor(dst=h_t[0:prs, 0:K], data1=h_t[0:prs, 0:K], data2=g_bc[0:prs, 0:K], op=nl.multiply)
239
+ nisa.tensor_tensor(dst=h_t[0:prs, 0:K], data1=h_t[0:prs, 0:K], data2=bt_bc[0:prs, 0:K], op=nl.add)
240
+ nisa.dma_copy(dst=h_hbm[r0:r0 + prs, 0:K], src=h_t[0:prs, 0:K])
241
+
242
+ if post:
243
+ return xnew_hbm, z_hbm, h_hbm
244
  return xnew_hbm, z_hbm
245
 
246
 
 
267
  if b1.dim() == 1:
268
  b1 = pack_b1(b1)
269
  return _mlp_res_ln_kernel[LNC](x, w1t, b1, w2t, b2, a, eps)
270
+
271
+
272
+ @nki_op("vit_enc::nki_mlp_res_ln_post", mutates_args={})
273
+ def nki_mlp_res_ln_post(
274
+ x: torch.Tensor, w1t: torch.Tensor, b1: torch.Tensor, w2t: torch.Tensor, b2: torch.Tensor,
275
+ a: torch.Tensor, eps: float, g: torch.Tensor, bt: torch.Tensor,
276
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
277
+ """Post-LN variant: (x_new fp32, z bf16 = LN0(x_new), h fp32 = LN(x_new) * g + bt). g/bt: [K] fp32."""
278
+ _no_grad(x, w1t, b1, w2t, b2, a, g, bt)
279
+ if b1.dim() == 1:
280
+ b1 = pack_b1(b1)
281
+ return _mlp_res_ln_kernel[LNC](x, w1t, b1, w2t, b2, a, eps, g, bt)