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 +26 -0
- README.md +10 -8
- build/torch-neuron/layers.py +23 -8
- build/torch-neuron/nki_kernels/attention_block_l1_ln.py +33 -1
- build/torch-neuron/nki_kernels/attention_inner.py +32 -1
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 |
-
|
| 159 |
-
out = model(input_ids=
|
| 160 |
```
|
| 161 |
|
| 162 |
-
The kernel path needs LNC=1
|
| 163 |
-
`attention_mask
|
| 164 |
-
|
|
|
|
|
|
|
| 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
|
| 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
|
| 293 |
-
|
| 294 |
-
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 340 |
-
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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],
|