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

v1.9: padding masks on the kernel path for NeuronXLMRobertaModel (per-image additive key bias in attention_block_l1_ln_post_kb); existing models bitwise identical to v1.8

Browse files
CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
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
 
1
  # Changelog
2
 
3
+ ## v1.9 (2026-10)
4
+
5
+ * **New: padding masks on the kernel path for `NeuronXLMRobertaModel`.** v1.8 sent any batch with a padding
6
+ `attention_mask` to the stock forward. The attention kernel now takes an optional per-image additive key bias
7
+ (`attention_block_l1_ln_post_kb`: 0 for attended keys, -1e9 for masked keys), and the layer turns the mask into that
8
+ bias, so variable-length batches stay on the kernels. `VIT_ENC_MASK_KERNEL=0` restores the v1.8 fallback.
9
+
10
+ XLM-RoBERTa-Large, LNC=1, 8 processes on one trn2 device, a log-normal length mix padded to the sequence length
11
+ (mean valid length 53 / 89 / 191), inferences/s:
12
+
13
+ | seq | stock with mask (v1.8 fallback) | v1.9 kernels with mask | speedup | v1.9 kernels, no mask |
14
+ |---|---|---|---|---|
15
+ | 128 | 956.2 | **2,923.0** | 3.06x | 3,177.0 |
16
+ | 256 | 464.5 | **1,899.2** | 4.09x | 1,922.7 |
17
+ | 512 | 213.0 | **894.4** | 4.20x | 962.9 |
18
+
19
+ Accuracy vs the fp32 CPU model on the same mask (valid positions): rel_l2 0.005-0.055 for right padding, half
20
+ padding, length-1 sequences and interior holes (the unmasked v1.8 path gives 0.007-0.051 at the same shapes).
21
+ Masked positions are computed like the stock model does (their keys still attend to valid keys). Every query row
22
+ is still computed, so padding is not skipped: throughput follows the padded length.
23
+ * **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)
24
+ give output bitwise identical to v1.8 at the same speed (within ±0.15%). Without the new argument the kernels emit
25
+ the v1.8 instruction stream. Verified on the public SDK 2.33 container
26
+ (`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,
27
+ neuronx-cc 2.28.7631, NKI 0.7.0) with transformers 4.57.6.
28
+
29
  ## v1.8 (2026-10)
30
 
31
  * **New: post-LayerNorm encoders (BERT / RoBERTa / XLM-R).** `NeuronXLMRobertaModel` is a drop-in forward for
README.md CHANGED
@@ -144,7 +144,7 @@ activation scale with `calibrate_fp8(runner, gelu_amax)`, because trn2's e4m3 sa
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
@@ -155,13 +155,15 @@ model = XLMRobertaModel.from_pretrained("FacebookAI/xlm-roberta-large", dtype=to
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
 
@@ -169,7 +171,7 @@ seq 128 / 256 / 512 runs at 3,145 / 1,870 / 935 inferences/s, against 2,093 / 94
169
  |---|---|
170
  | `attention_block` | LN'd hidden -> Q/K/V projection + RoPE + attention + output projection, one 8-head (D=64) or 4-head (D=128) group per program; any number of images per launch; padded keys skipped |
171
  | `attention_block_l1` | LNC=1: one program over **all** head groups, one output (no per-group partials) |
172
- | `attention_block_l1_ln` | `attention_block_l1` plus residual add and LayerNorm in the epilogue |
173
  | `res_layernorm_fold` | residual add (sum of up to 4 branches) + affine-free LayerNorm |
174
  | `mlp` | GELU MLP (fc1 -> GELU -> fc2) |
175
  | `mlp_res_ln` | `mlp` plus residual add and the next block's LayerNorm in the epilogue |
@@ -196,7 +198,7 @@ RoPE pairings via `rope=`:
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
 
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; padding masks v1.9)
148
 
149
  ```python
150
  import os
 
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
+ enc = tokenizer(texts, padding="max_length", max_length=128, return_tensors="pt") # seq % 128 == 0
159
+ out = model(input_ids=enc.input_ids.to("neuron"), attention_mask=enc.attention_mask.to("neuron")).last_hidden_state
160
  ```
161
 
162
+ The kernel path needs LNC=1 and a sequence length that is a multiple of 128 (up to 2048). Since v1.9 a padding
163
+ `attention_mask` stays on the kernel path (an additive key bias in the attention kernel); `attention_mask=None` skips
164
+ the mask entirely and is slightly faster on fully padded batches. Per instance, XLM-RoBERTa-Large at seq 128 / 256 / 512
165
+ runs at 3,145 / 1,870 / 935 inferences/s without a mask (2,093 / 944 / 397 stock), and at 2,923 / 1,899 / 894 with a
166
+ real padding mask (956 / 465 / 213 stock with the same mask). See CHANGELOG v1.8 and v1.9.
167
 
168
  ## Kernels
169
 
 
171
  |---|---|
172
  | `attention_block` | LN'd hidden -> Q/K/V projection + RoPE + attention + output projection, one 8-head (D=64) or 4-head (D=128) group per program; any number of images per launch; padded keys skipped |
173
  | `attention_block_l1` | LNC=1: one program over **all** head groups, one output (no per-group partials) |
174
+ | `attention_block_l1_ln` | `attention_block_l1` plus residual add and LayerNorm in the epilogue (`_post`: post-LN output; `_post_kb`, v1.9: per-image additive key bias for padding masks) |
175
  | `res_layernorm_fold` | residual add (sum of up to 4 branches) + affine-free LayerNorm |
176
  | `mlp` | GELU MLP (fc1 -> GELU -> fc2) |
177
  | `mlp_res_ln` | `mlp` plus residual add and the next block's LayerNorm in the epilogue |
 
198
  * ViT-7B-sized models (hidden 4096) are not covered by the drop-in layer. They need tensor parallelism.
199
  The streaming SwiGLU kernel is slower than the compiler's FFN at that width.
200
  * Validated end to end on DINOv3 ViT-B/L/H+, V-JEPA2 ViT-L and (v1.8) XLM-RoBERTa-Large.
201
+ * Post-LN text encoders (v1.8, `NeuronXLMRobertaModel`): LNC=1 only (padding masks supported since v1.9), no token-type / relative
202
  position variants beyond the standard BERT embeddings, GELU FFN.
203
 
204
  ## Requirements
build/torch-neuron/layers.py CHANGED
@@ -20,6 +20,7 @@ Usage (graft, works on any Transformers version)::
20
  """
21
  from __future__ import annotations
22
 
 
23
  import torch
24
 
25
  from .encoder import LNC, BlockWeights, EncoderRunner, pack_block, rope_tables
@@ -289,9 +290,10 @@ class NeuronXLMRobertaModel(torch.nn.Module):
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
@@ -307,7 +309,12 @@ class NeuronXLMRobertaModel(torch.nn.Module):
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.
@@ -333,12 +340,20 @@ class NeuronXLMRobertaModel(torch.nn.Module):
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)
 
20
  """
21
  from __future__ import annotations
22
 
23
+ import os as _osm
24
  import torch
25
 
26
  from .encoder import LNC, BlockWeights, EncoderRunner, pack_block, rope_tables
 
290
  Graft on an instance (``model.forward = types.MethodType(NeuronXLMRobertaModel.forward, model)``); do not use it
291
  as a class-level KernelConfig replacement, because the fallback calls ``type(model).forward``.
292
 
293
+ Returns ``last_hidden_state``. Kernel path: LNC=1, sequence length a multiple of 128 and <= 2048. v1.9: a
294
+ padding ``attention_mask`` (any 0/1 pattern) stays on the kernel path as an additive key bias
295
+ (``VIT_ENC_MASK_KERNEL=0`` restores the v1.8 fallback). Anything else (LNC=2, other lengths, off-device,
296
+ training) falls back to the stock forward.
297
  """
298
 
299
  can_torch_compile = True
 
309
  S = input_ids.shape[1] if input_ids is not None else -1
310
  # The mask check runs outside the compiled graph (one host sync per call); pass attention_mask=None to skip it.
311
  masked = attention_mask is not None and _bert_masked(attention_mask)
312
+ if masked and _osm.environ.get("VIT_ENC_MASK_KERNEL", "1") == "0":
313
+ kmask = None
314
+ else:
315
+ kmask = attention_mask if masked else None
316
+ if (input_ids is None or input_ids.device.type != "neuron" or self.training or (masked and kmask is None)
317
+ or position_ids is not None
318
  or kwargs.get("inputs_embeds") is not None or not _bert_ok(self, S)):
319
  # stock forward of the grafted instance's own class (XLMRobertaModel / RobertaModel / BertModel). The graft
320
  # sets an INSTANCE attribute (types.MethodType), so the class attribute is still the stock forward.
 
340
  h = F.layer_norm(x, (H,), g0, b0, eps0)
341
  nL = len(R.L)
342
  from .encoder import _PACK2
343
+ pk2 = int(_PACK2 and B % 2 == 0 and 2 * S <= 512 and kmask is None)
344
+ # v1.9: padding mask -> additive key bias [B, S] (0 attended, -1e9 masked), same softmax as the stock
345
+ # extended mask; every query row (padding rows included) is computed, as in the stock forward.
346
+ kb = None if kmask is None else ((1.0 - kmask.to(torch.float32)) * -1e9).contiguous()
347
  for li, p in enumerate(R.L):
348
  ga, ba, _ = st["ln_attn"][li]
349
+ if kb is None:
350
+ _, z2, h1 = _ab1ln.attention_block_l1_ln_post(
351
+ z1, p["wqk"], p["bqk"], tab, p["wvt"], p["bv"], p["wot"], p["bo_full"], h, p["scale"], D, S,
352
+ p["eps2"], ga, ba, B, R._res(_ab1ln, S), pk2, 1)
353
+ else:
354
+ _, z2, h1 = _ab1ln.attention_block_l1_ln_post_kb(
355
+ z1, p["wqk"], p["bqk"], tab, p["wvt"], p["bv"], p["wot"], p["bo_full"], h, p["scale"], D, S,
356
+ p["eps2"], ga, ba, kb, B, R._res(_ab1ln, S), 1)
357
  go, bo, eo = st["ln_in"][li + 1]
358
  if li + 1 < nL:
359
  _, z1, h = nki_mlp_res_ln_post(z2, p["w1t"], p["b1"], p["w2t"], p["b2"], h1, eo, go, bo)
build/torch-neuron/nki_kernels/attention_block_l1_ln.py CHANGED
@@ -88,7 +88,7 @@ def sbuf_estimate(H: int, Fp: int, N: int, D: int, resident: bool) -> int:
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,6 +118,11 @@ def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_
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)
@@ -234,6 +239,12 @@ def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_
234
  nisa.dma_copy(dst=tab[t_i][s][:, 0:_tw],
235
  src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
236
 
 
 
 
 
 
 
237
  img_base = allocator.get_current_address()
238
 
239
  for g in range(G):
@@ -399,6 +410,7 @@ def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_
399
  out_o_hbm=o_l,
400
  init_sbuf_addr=attn_base,
401
  seqlen_k_valid=n_valid,
 
402
  )
403
  allocator.set_current_address(attn_base)
404
 
@@ -489,6 +501,7 @@ def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_
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={})
@@ -536,3 +549,22 @@ def attention_block_l1_ln_post(
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, kbias_hbm=None):
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
+ # v1.9: optional per-image additive key bias [nimg, N] fp32 (0 attended, large negative for padding keys).
122
+ kb = kbias_hbm is not None
123
+ if kb:
124
+ _kernel_assert(kbias_hbm.shape == (nimg, N), "kbias must be [nimg, N] fp32")
125
+ _kernel_assert(not pack2, "kbias is not supported with pack2")
126
  post = g_hbm is not None # post-LN mode: also emit h = LN(x_new) * g + bt (fp32)
127
  if post:
128
  hout_hbm = nl.ndarray((NT, H), dtype=nl.float32, buffer=nl.shared_hbm)
 
239
  nisa.dma_copy(dst=tab[t_i][s][:, 0:_tw],
240
  src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
241
 
242
+ # v1.9: this image's key bias, broadcast to every partition (one stride-0 DMA)
243
+ kb_sb = None
244
+ if kb:
245
+ kb_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, N), dtype=nl.float32, align_to=4)
246
+ nisa.dma_copy(dst=kb_sb, src=kbias_hbm.ap(pattern=[[0, _P_MAX], [1, N]], offset=img * N))
247
+
248
  img_base = allocator.get_current_address()
249
 
250
  for g in range(G):
 
410
  out_o_hbm=o_l,
411
  init_sbuf_addr=attn_base,
412
  seqlen_k_valid=n_valid,
413
+ kbias_sbuf=kb_sb,
414
  )
415
  allocator.set_current_address(attn_base)
416
 
 
501
  _ATTN_L1_LN = wrap_nki_to_torch_hop(_attention_block_l1_ln_kernel[1])
502
  _ATTN_L1_LN_POST = wrap_nki_to_torch_hop(_attention_block_l1_ln_kernel[1])
503
  _ATTN_L1_LN_POST_NR = _ATTN_L1_LN_POST
504
+ _ATTN_L1_LN_POST_KB = wrap_nki_to_torch_hop(_attention_block_l1_ln_kernel[1])
505
 
506
 
507
  @nki_op("vit_enc::attention_block_l1_ln", mutates_args={})
 
549
  if bqk.dim() == 3 and bqk.shape[1] != 128:
550
  bqk = _pack_qk_bias(bqk)
551
  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)
552
+
553
+
554
+ @nki_op("vit_enc::attention_block_l1_ln_post_kb", mutates_args={})
555
+ def attention_block_l1_ln_post_kb(
556
+ h: torch.Tensor, wqk: torch.Tensor, bqk: torch.Tensor, tab: torch.Tensor, wvt: torch.Tensor, bv: torch.Tensor,
557
+ wot: torch.Tensor, bo: torch.Tensor, x: torch.Tensor, scale: float, head_dim: int, n_valid: int, eps: float,
558
+ g: torch.Tensor, bt: torch.Tensor, kbias: torch.Tensor, nimg: int = 1, resident: int = 0, norope: int = 0,
559
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
560
+ """v1.9: ``attention_block_l1_ln_post`` with a per-image additive key bias ``kbias`` [nimg, N] fp32 (0 for
561
+ attended keys, a large negative value such as -1e9 for padding keys), the BERT-style padding mask. pack2 is off."""
562
+ for t in (h, wqk, bqk, tab, wvt, bv, wot, bo, x, g, bt, kbias):
563
+ if t is not None and t.requires_grad:
564
+ raise RuntimeError("attention_block_l1_ln_post_kb is inference-only; use torch.no_grad().")
565
+ if h.shape[0] // nimg > 2048:
566
+ raise NotImplementedError("attention_block_l1_ln_post_kb supports N<=2048 only")
567
+ if bqk.dim() == 3 and bqk.shape[1] != 128:
568
+ bqk = _pack_qk_bias(bqk)
569
+ return _ATTN_L1_LN_POST_KB(h, wqk, bqk, tab, wvt, bv, wot, bo, x, scale, head_dim, n_valid, eps, nimg, resident, 0,
570
+ g, bt, norope, kbias)
build/torch-neuron/nki_kernels/attention_inner.py CHANGED
@@ -208,6 +208,7 @@ class AttnInternalBuffers(nl.NKIObject):
208
  range_sel_lbs_prior = None
209
  range_sel_ubs_prior = None
210
  k_offset_sb_u32 = None
 
211
 
212
 
213
  @dataclass
@@ -275,8 +276,12 @@ def _attention_cte(
275
  out_o_hbm=None,
276
  init_sbuf_addr: int = 0,
277
  seqlen_k_valid: int = None,
 
278
  ):
279
  """One head of context-encoding attention (SBUF-KV path).
 
 
 
280
  EDIT: ``seqlen_k_valid`` overrides the active key length (default len(k_cache)*512)
281
  so keys at positions >= seqlen_k_valid (token padding) are skipped by every loop bound.
282
 
@@ -406,6 +411,7 @@ def _attention_cte(
406
  k_cache_sbuf=k_cache_sbuf,
407
  v_cache_sbuf=v_cache_sbuf,
408
  init_sbuf_addr=init_sbuf_addr,
 
409
  )
410
 
411
  # has_remainder is False (num_shard==1); the seqlen-q sharding path is DEAD.
@@ -426,6 +432,7 @@ def _attention_cte_impl(
426
  k_cache_sbuf: Optional[List[nl.ndarray]] = None,
427
  v_cache_sbuf: Optional[List[nl.ndarray]] = None,
428
  init_sbuf_addr: int = 0,
 
429
  ):
430
  """Single-batch attention body. LIVE path only: single section (num_sections==1), pipelined
431
  group loop (num_grps=16 > 1), SBUF-KV (no K/V HBM load), tp_out=True.
@@ -442,6 +449,7 @@ def _attention_cte_impl(
442
  allocator = ModularAllocator(initial_address=init_sbuf_addr)
443
  bufs = AttnInternalBuffers()
444
  bufs.k_scale_sb = None
 
445
 
446
  bufs.zero_bias_tensor = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, 1), dtype=nl.float32)
447
  nisa.memset(bufs.zero_bias_tensor, 0.0)
@@ -1196,7 +1204,30 @@ def _qk_and_max_large_tile_impl(qkmax_grp, large_tile_idx, ac: AttnConfig, atp:
1196
  num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1197
  num_f = min(seqlen_k - k_start_pos, _K_TILE_SZ)
1198
 
1199
- if _chain:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1200
  nisa.tensor_scalar_reduce(
1201
  mm1_masked_tile[:num_p, nl.ds(k_tile_idx * _K_TILE_SZ, num_f)],
1202
  data=mm1_psum_tile[:num_p, :num_f],
 
208
  range_sel_lbs_prior = None
209
  range_sel_ubs_prior = None
210
  k_offset_sb_u32 = None
211
+ kbias_sb = None
212
 
213
 
214
  @dataclass
 
276
  out_o_hbm=None,
277
  init_sbuf_addr: int = 0,
278
  seqlen_k_valid: int = None,
279
+ kbias_sbuf=None,
280
  ):
281
  """One head of context-encoding attention (SBUF-KV path).
282
+ v1.9: ``kbias_sbuf`` (optional, [128, seqlen_k] fp32 SBUF, every partition row identical) is added to the scaled
283
+ scores before softmax: 0 for attended keys, a large negative value for masked (padding) keys. None keeps the
284
+ v1.8 instruction stream exactly.
285
  EDIT: ``seqlen_k_valid`` overrides the active key length (default len(k_cache)*512)
286
  so keys at positions >= seqlen_k_valid (token padding) are skipped by every loop bound.
287
 
 
411
  k_cache_sbuf=k_cache_sbuf,
412
  v_cache_sbuf=v_cache_sbuf,
413
  init_sbuf_addr=init_sbuf_addr,
414
+ kbias_sbuf=kbias_sbuf,
415
  )
416
 
417
  # has_remainder is False (num_shard==1); the seqlen-q sharding path is DEAD.
 
432
  k_cache_sbuf: Optional[List[nl.ndarray]] = None,
433
  v_cache_sbuf: Optional[List[nl.ndarray]] = None,
434
  init_sbuf_addr: int = 0,
435
+ kbias_sbuf=None,
436
  ):
437
  """Single-batch attention body. LIVE path only: single section (num_sections==1), pipelined
438
  group loop (num_grps=16 > 1), SBUF-KV (no K/V HBM load), tp_out=True.
 
449
  allocator = ModularAllocator(initial_address=init_sbuf_addr)
450
  bufs = AttnInternalBuffers()
451
  bufs.k_scale_sb = None
452
+ bufs.kbias_sb = kbias_sbuf
453
 
454
  bufs.zero_bias_tensor = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, 1), dtype=nl.float32)
455
  nisa.memset(bufs.zero_bias_tensor, 0.0)
 
1204
  num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1205
  num_f = min(seqlen_k - k_start_pos, _K_TILE_SZ)
1206
 
1207
+ if bufs.kbias_sb is not None:
1208
+ # v1.9 key-padding bias: s = psum * scale + kbias[k], then the row-max of this k-tile.
1209
+ _ko = k_start_pos
1210
+ nisa.scalar_tensor_tensor(
1211
+ dst=mm1_masked_tile[:num_p, nl.ds(k_tile_idx * _K_TILE_SZ, num_f)],
1212
+ data=mm1_psum_tile[:num_p, :num_f],
1213
+ op0=nl.multiply,
1214
+ operand0=ac.scale,
1215
+ op1=nl.add,
1216
+ operand1=bufs.kbias_sb[:num_p, nl.ds(_ko, num_f)],
1217
+ )
1218
+ if _chain:
1219
+ # keep the chained-max contract: the running max over executed k-tiles ends in the last
1220
+ # executed tile's column (read by _update_max_impl).
1221
+ _col = mm1_partial_max_tile[:num_p, k_tile_idx_in_section]
1222
+ nisa.tensor_reduce(_col, nl.maximum,
1223
+ mm1_masked_tile[:num_p, nl.ds(k_tile_idx * _K_TILE_SZ, num_f)], 1)
1224
+ if k_tile_idx > _first_kt:
1225
+ _prev = mm1_partial_max_tile[:num_p, k_tile_idx_in_section - 1]
1226
+ nisa.tensor_tensor(dst=_col, data1=_col, data2=_prev, op=nl.maximum)
1227
+ else:
1228
+ nisa.tensor_reduce(mm1_partial_max_tile[:num_p, k_tile_idx_in_section], nl.maximum,
1229
+ mm1_masked_tile[:num_p, nl.ds(k_tile_idx * _K_TILE_SZ, num_f)], 1)
1230
+ elif _chain:
1231
  nisa.tensor_scalar_reduce(
1232
  mm1_masked_tile[:num_p, nl.ds(k_tile_idx * _K_TILE_SZ, num_f)],
1233
  data=mm1_psum_tile[:num_p, :num_f],