v1.1: K4 (mlp/mlp_res_ln) + K1/K3 (res_layernorm) DMA/engine-schedule rework
Browse filesBitwise-identical to v1 at LNC=1 and LNC=2. Packed fc1 bias loaded in one DMA on the
Scalar HW-DGE queue; row-block-0 x transposed on the Tensor engine before the weights;
res_layernorm uses dge_mode.none with all token tiles SBUF-resident. Based on an external
contribution by jlongen-aws (PR #2 on jburtoft/vjepa2-neuron-kernels), re-implemented with
the LNC-parametric path preserved. Verified on trn2.3xlarge: real-weight V-JEPA2 output
bitwise identical to v1, -4.0% ms/clip.
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
| 1 |
# Changelog
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
## v1 (2026-10)
|
| 4 |
|
| 5 |
First release.
|
|
|
|
| 1 |
# Changelog
|
| 2 |
|
| 3 |
+
## v1.1 (2026-10)
|
| 4 |
+
|
| 5 |
+
Reworked DMA/engine schedule for `mlp` (K4), `mlp_res_ln` (K4 + next LN) and `res_layernorm_fold`
|
| 6 |
+
(K1/K3). Output is **bitwise identical** to v1 at both LNC=1 and LNC=2. Based on an external
|
| 7 |
+
contribution by [jlongen-aws](https://huggingface.co/jlongen-aws) (PR #2 on
|
| 8 |
+
`jburtoft/vjepa2-neuron-kernels`), re-implemented here with the LNC-parametric path and the `min()`
|
| 9 |
+
remainder handling preserved.
|
| 10 |
+
|
| 11 |
+
Verified on trn2.3xlarge (A/B on one consistent stack: old vs new compiled and run in the same
|
| 12 |
+
container + inputs, device cycles from `neuron-explorer show-session`). LNC=2, V-JEPA2 ViT-L shape
|
| 13 |
+
(R=2048, H=1024, MLP hidden 4096):
|
| 14 |
+
|
| 15 |
+
* K4 `mlp`: -7.5% device cycles (reproducible, -7.8% +/- ~1% over 3 captures).
|
| 16 |
+
* K4+LN `mlp_res_ln`: -6.8%.
|
| 17 |
+
* K1 `res_layernorm_fold` (1 branch): -16.2%; K3 (2 branches): -18.3%.
|
| 18 |
+
* Full 24-layer encoder (synthetic ViT-L weights, includes the unchanged attention): -3.7% device
|
| 19 |
+
cycles, output `torch.equal` to v1.
|
| 20 |
+
* Real-weight V-JEPA2 (`facebook/vjepa2-vitl-fpc64-256`, 16 frames x 256 px, LNC=2): output bitwise
|
| 21 |
+
identical to v1 (`new_vs_v1_rel` 0.0), rel_l2 0.0389 vs fp32 CPU (unchanged from v1), -4.0% ms/clip
|
| 22 |
+
(speedup vs stock fp32 3.19x -> 3.33x).
|
| 23 |
+
* Per-kernel output is `torch.equal` to v1 at LNC=1 and LNC=2.
|
| 24 |
+
|
| 25 |
+
Changes:
|
| 26 |
+
|
| 27 |
+
* `mlp` / `mlp_res_ln`: fc1 bias is host-packed to `[128, H/128]` (`pack_b1`, now exported) and
|
| 28 |
+
loaded in one DMA on the Scalar engine's HW-DGE queue instead of `H/128` column DMAs; row block 0's
|
| 29 |
+
`x` is staged and transposed on the (idle) Tensor engine before the weights, later row blocks stream
|
| 30 |
+
under row block 0's matmuls. `mlp` still accepts an unpacked `[H]` bias (packed on the fly).
|
| 31 |
+
* `res_layernorm_fold`: every DMA uses `dge_mode.none` (the runtime pre-generates descriptors) and all
|
| 32 |
+
token tiles are made SBUF-resident before any compute, replacing the depth-4 ring. Per-tile statistic
|
| 33 |
+
columns (`[128, n_t]`) so one tile's stat chain does not wait on the previous tile's.
|
| 34 |
+
* `pack_block` now stores the GELU fc1 bias pre-packed; `pack_b1` is exported from `nki_kernels`.
|
| 35 |
+
|
| 36 |
+
The compute instructions and their order are unchanged, so results are bit-identical to v1.
|
| 37 |
+
|
| 38 |
## v1 (2026-10)
|
| 39 |
|
| 40 |
First release.
|
build/torch-neuron/encoder.py
CHANGED
|
@@ -34,6 +34,7 @@ _ab1 = _il.import_module(__package__ + ".nki_kernels.attention_block_l1")
|
|
| 34 |
_ab1ln = _il.import_module(__package__ + ".nki_kernels.attention_block_l1_ln")
|
| 35 |
from .nki_kernels.attention_block import LNC
|
| 36 |
from .nki_kernels.mlp import nki_mlp_k4_xp
|
|
|
|
| 37 |
from .nki_kernels.mlp_res_ln import nki_mlp_res_ln
|
| 38 |
from .nki_kernels.res_layernorm import nki_res_layernorm_fold
|
| 39 |
from .nki_kernels.swiglu_stream import nki_swiglu_stream, nki_swiglu_stream_ln
|
|
@@ -132,7 +133,8 @@ def pack_block(bw: BlockWeights, num_heads: int, rope: str, device) -> dict:
|
|
| 132 |
w1, w2 = _f(bw.fc1_w), _f(bw.fc2_w)
|
| 133 |
p["swiglu"] = False
|
| 134 |
p["w1t"] = (w1 * g2[None]).t().contiguous().to(torch.bfloat16)
|
| 135 |
-
|
|
|
|
| 136 |
p["w2t"] = (w2 * ls2[:, None]).t().contiguous().to(torch.bfloat16)
|
| 137 |
p["b2"] = (_z(_f(bw.fc2_b), H) * ls2).contiguous()
|
| 138 |
return {k: (v.to(device) if torch.is_tensor(v) else v) for k, v in p.items()}
|
|
|
|
| 34 |
_ab1ln = _il.import_module(__package__ + ".nki_kernels.attention_block_l1_ln")
|
| 35 |
from .nki_kernels.attention_block import LNC
|
| 36 |
from .nki_kernels.mlp import nki_mlp_k4_xp
|
| 37 |
+
from .nki_kernels.mlp import pack_b1 as _mlp_pack_b1
|
| 38 |
from .nki_kernels.mlp_res_ln import nki_mlp_res_ln
|
| 39 |
from .nki_kernels.res_layernorm import nki_res_layernorm_fold
|
| 40 |
from .nki_kernels.swiglu_stream import nki_swiglu_stream, nki_swiglu_stream_ln
|
|
|
|
| 133 |
w1, w2 = _f(bw.fc1_w), _f(bw.fc2_w)
|
| 134 |
p["swiglu"] = False
|
| 135 |
p["w1t"] = (w1 * g2[None]).t().contiguous().to(torch.bfloat16)
|
| 136 |
+
# Pre-pack fc1 bias to [128, H_mlp/128] so K4/K4+LN load it in one DMA (see mlp.pack_b1).
|
| 137 |
+
p["b1"] = _mlp_pack_b1((_z(_f(bw.fc1_b), w1.shape[0]) + w1 @ t2).contiguous())
|
| 138 |
p["w2t"] = (w2 * ls2[:, None]).t().contiguous().to(torch.bfloat16)
|
| 139 |
p["b2"] = (_z(_f(bw.fc2_b), H) * ls2).contiguous()
|
| 140 |
return {k: (v.to(device) if torch.is_tensor(v) else v) for k, v in p.items()}
|
build/torch-neuron/nki_kernels/__init__.py
CHANGED
|
@@ -11,6 +11,7 @@ from .attention_block import (
|
|
| 11 |
from .attention_block_l1 import attention_block_l1
|
| 12 |
from .attention_block_l1_ln import attention_block_l1_ln
|
| 13 |
from .mlp import nki_mlp_k4_xp as mlp
|
|
|
|
| 14 |
from .mlp_res_ln import nki_mlp_res_ln as mlp_res_ln
|
| 15 |
from .res_layernorm import nki_res_layernorm_fold as res_layernorm_fold
|
| 16 |
from .swiglu_stream import nki_swiglu_stream as swiglu_stream
|
|
@@ -18,6 +19,6 @@ from .swiglu_stream import nki_swiglu_stream_ln as swiglu_stream_ln
|
|
| 18 |
|
| 19 |
__all__ = [
|
| 20 |
"attention_block", "attention_block_l1", "attention_block_l1_ln", "res_layernorm_fold",
|
| 21 |
-
"mlp", "mlp_res_ln", "swiglu_stream", "swiglu_stream_ln",
|
| 22 |
"pack_weights", "pack_tables", "pack_v_weights", "pack_out_weights", "lane_permutation",
|
| 23 |
]
|
|
|
|
| 11 |
from .attention_block_l1 import attention_block_l1
|
| 12 |
from .attention_block_l1_ln import attention_block_l1_ln
|
| 13 |
from .mlp import nki_mlp_k4_xp as mlp
|
| 14 |
+
from .mlp import pack_b1
|
| 15 |
from .mlp_res_ln import nki_mlp_res_ln as mlp_res_ln
|
| 16 |
from .res_layernorm import nki_res_layernorm_fold as res_layernorm_fold
|
| 17 |
from .swiglu_stream import nki_swiglu_stream as swiglu_stream
|
|
|
|
| 19 |
|
| 20 |
__all__ = [
|
| 21 |
"attention_block", "attention_block_l1", "attention_block_l1_ln", "res_layernorm_fold",
|
| 22 |
+
"mlp", "mlp_res_ln", "pack_b1", "swiglu_stream", "swiglu_stream_ln",
|
| 23 |
"pack_weights", "pack_tables", "pack_v_weights", "pack_out_weights", "lane_permutation",
|
| 24 |
]
|
build/torch-neuron/nki_kernels/mlp.py
CHANGED
|
@@ -7,35 +7,46 @@ the weights pre-transposed to bf16. Returns ``[R, K]`` bf16.
|
|
| 7 |
|
| 8 |
WHY IT IS SHAPED THIS WAY
|
| 9 |
-------------------------
|
| 10 |
-
This is the effective shipping kernel:
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
PSUM: fc1 uses 1 bank, fc2 uses up to 4 banks; the ``y1_psum`` / ``out_psums`` tiles are
|
| 30 |
-
declared inside their loops so the ring never accumulates onto a stale PSUM value. The
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
"""
|
| 35 |
|
| 36 |
from __future__ import annotations
|
| 37 |
|
| 38 |
import math
|
|
|
|
| 39 |
|
| 40 |
import torch
|
| 41 |
from torch_neuronx import nki_op
|
|
@@ -50,8 +61,6 @@ _F_MOV_MAX = 512
|
|
| 50 |
|
| 51 |
_BF16 = nl.bfloat16
|
| 52 |
|
| 53 |
-
#: Logical-to-physical NeuronCore degree. Must match the shipping shard's.
|
| 54 |
-
import os as _os
|
| 55 |
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 56 |
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
| 57 |
|
|
@@ -69,20 +78,23 @@ def _kernel_assert(condition: bool, error_text: str):
|
|
| 69 |
assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
|
| 70 |
|
| 71 |
|
| 72 |
-
def _stage_xt(x_hbm, r_abs, pr, n_k, K, rb_size):
|
| 73 |
"""Emit one row block's x staging and return its ``n_k`` transposed bf16 tiles.
|
| 74 |
|
| 75 |
-
``n_rs`` loads of ``[<=128, K]`` bf16, each followed by ``n_k``
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
| 78 |
|
| 79 |
Args:
|
| 80 |
-
x_hbm:
|
| 81 |
-
r_abs:
|
| 82 |
-
pr:
|
| 83 |
-
n_k:
|
| 84 |
-
K:
|
| 85 |
-
rb_size:
|
|
|
|
| 86 |
|
| 87 |
Returns:
|
| 88 |
list of ``n_k`` transposed ``[128, rb_size]`` bf16 SBUF tiles.
|
|
@@ -102,8 +114,15 @@ def _stage_xt(x_hbm, r_abs, pr, n_k, K, rb_size):
|
|
| 102 |
for kt in range(n_k):
|
| 103 |
k0 = kt * _P_MAX
|
| 104 |
pk = min(_P_MAX, K - k0)
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
return xt_tiles
|
| 108 |
|
| 109 |
|
|
@@ -114,7 +133,7 @@ def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
|
|
| 114 |
Args:
|
| 115 |
x_hbm (nl.ndarray): [R, K] @ HBM, **bfloat16** (kernel 1 emits bf16).
|
| 116 |
w1t_hbm (nl.ndarray): [K, H] @ HBM, **bf16**, == fc1.weight.T.
|
| 117 |
-
b1_hbm (nl.ndarray): [H]
|
| 118 |
w2t_hbm (nl.ndarray): [H, K] @ HBM, **bf16**, == fc2.weight.T.
|
| 119 |
b2_hbm (nl.ndarray): [K] @ HBM, float32.
|
| 120 |
|
|
@@ -128,6 +147,9 @@ def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
|
|
| 128 |
_kernel_assert(K_w1t == K, f"w1t in-dim {K_w1t} != K {K}; expected w1t = w1.T of shape [K, H]")
|
| 129 |
_kernel_assert(K_w2t == K, f"w2t out-dim {K_w2t} != K {K}; expected w2t = w2.T of shape [H, K]")
|
| 130 |
_kernel_assert(H_w2t == H, f"w2t in-dim {H_w2t} != H {H}")
|
|
|
|
|
|
|
|
|
|
| 131 |
_kernel_assert(
|
| 132 |
R % LNC == 0,
|
| 133 |
f"the {LNC}-core kernel needs R divisible by {LNC}, got R={R}. "
|
|
@@ -144,17 +166,26 @@ def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
|
|
| 144 |
n_k = _div_ceil(K, _P_MAX) # K tiles (fc1 contraction)
|
| 145 |
n_h = _div_ceil(H, _P_MAX) # H tiles (fc1 out-rows / fc2 contraction)
|
| 146 |
|
| 147 |
-
# b1 FIRST,
|
| 148 |
-
# ~
|
| 149 |
-
|
| 150 |
-
for
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
-
#
|
| 158 |
w1t_tiles = []
|
| 159 |
for kt in range(n_k):
|
| 160 |
k0 = kt * _P_MAX
|
|
@@ -163,6 +194,13 @@ def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
|
|
| 163 |
nisa.dma_copy(dst=t[0:pk, :], src=w1t_hbm[k0:k0 + pk, :])
|
| 164 |
w1t_tiles.append(t)
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
w2t_tiles = []
|
| 167 |
for ht in range(n_h):
|
| 168 |
h0 = ht * _P_MAX
|
|
@@ -171,21 +209,6 @@ def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
|
|
| 171 |
nisa.dma_copy(dst=t[0:ph, :], src=w2t_hbm[h0:h0 + ph, :])
|
| 172 |
w2t_tiles.append(t)
|
| 173 |
|
| 174 |
-
# Main fused loop.
|
| 175 |
-
rb_size = _F_MOV_MAX
|
| 176 |
-
n_rb = _div_ceil(rows_per, rb_size)
|
| 177 |
-
n_ko = _div_ceil(K, _F_MOV_MAX)
|
| 178 |
-
|
| 179 |
-
# THE X-PRESTAGE: stage EVERY row block's x here, in rb order, so rb=1's serialised
|
| 180 |
-
# DMA chain runs inside the pre-K4 seam (Tensor dark) instead of inside the idle
|
| 181 |
-
# window at the rb0->rb1 boundary. Appending rb=1's stages after rb=0's leaves rb=0's
|
| 182 |
-
# stages exactly where they are on the Sync engine's sub-stream.
|
| 183 |
-
xt_sets = []
|
| 184 |
-
for rb_pre in range(n_rb):
|
| 185 |
-
xt_sets.append(_stage_xt(
|
| 186 |
-
x_hbm, r_base + rb_pre * rb_size,
|
| 187 |
-
min(rb_size, rows_per - rb_pre * rb_size), n_k, K, rb_size))
|
| 188 |
-
|
| 189 |
for rb in range(n_rb):
|
| 190 |
r_local = rb * rb_size
|
| 191 |
pr = min(rb_size, rows_per - r_local)
|
|
@@ -212,10 +235,10 @@ def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
|
|
| 212 |
stationary=w1t_tiles[kt][0:pk, h0:h0 + ph],
|
| 213 |
moving=xt_tiles[kt][0:pk, 0:pr],
|
| 214 |
)
|
| 215 |
-
# b1 comes from the prologue-resident tile.
|
| 216 |
gt = nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf)
|
| 217 |
nisa.activation(dst=gt[0:ph, 0:pr], op=nl.gelu,
|
| 218 |
-
data=y1_psum[0:ph, 0:pr], bias=
|
| 219 |
gt_tiles.append(gt)
|
| 220 |
|
| 221 |
# ---- fc2: out[r,k] = sum_h g[r,h] * w2[k,h] + b2[k] ----
|
|
@@ -259,6 +282,17 @@ def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
|
|
| 259 |
return out_hbm
|
| 260 |
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
def _no_grad(*tensors) -> None:
|
| 263 |
"""Refuse gradient-tracking inputs -- this op registers no autograd rule."""
|
| 264 |
for t in tensors:
|
|
@@ -281,13 +315,17 @@ def nki_mlp_k4_xp(
|
|
| 281 |
|
| 282 |
Args:
|
| 283 |
x : [R, K] **bf16** on 'neuron'. ``R`` must be divisible by :data:`LNC`.
|
| 284 |
-
w1t : [K, H] bf16 == fc1.weight.T.
|
|
|
|
|
|
|
| 285 |
w2t : [H, K] bf16 == fc2.weight.T. b2 : [K] float32.
|
| 286 |
|
| 287 |
Returns:
|
| 288 |
[R, K] bf16.
|
| 289 |
"""
|
| 290 |
_no_grad(x, w1t, b1, w2t, b2)
|
|
|
|
|
|
|
| 291 |
return _mlp_k4_xp_kernel[LNC](x, w1t, b1, w2t, b2)
|
| 292 |
|
| 293 |
|
|
|
|
| 7 |
|
| 8 |
WHY IT IS SHAPED THIS WAY
|
| 9 |
-------------------------
|
| 10 |
+
This is the effective shipping kernel: the ``b1pre`` MLP (fc1 bias staged in the prologue
|
| 11 |
+
so every gelu's bias is resident from t=0) with the x-prestage applied. The schedule was
|
| 12 |
+
rebuilt from the device profile (external contribution by jlongen-aws, PR #2 on
|
| 13 |
+
``jburtoft/vjepa2-neuron-kernels``). Four design choices carry the performance; the
|
| 14 |
+
instruction order and numerics are unchanged, so the output is bit-identical to the
|
| 15 |
+
previous version:
|
| 16 |
+
|
| 17 |
+
* x BEFORE weights: row-block 0's ``x`` load + transpose is issued first, then W1, then the
|
| 18 |
+
later row blocks' ``x``, then W2. The first matmul no longer waits on ~16 MB of weights it
|
| 19 |
+
does not need yet (the first fc1 matmul used to retire at ~57 us).
|
| 20 |
+
* b1 in ONE DMA on the Scalar engine's HW-DGE queue: ``b1`` is host-packed to ``[128, H/128]``
|
| 21 |
+
(:func:`pack_b1`) and lands in a single ~16 KB DMA instead of ``n_h`` ``[128, 1]`` fp32 DMAs
|
| 22 |
+
that trickled in and stalled each gelu (and, through its PSUM bank, the fc1 matmuls behind
|
| 23 |
+
it). Routing it on ``engine=scalar`` HW-DGE keeps gelu's wait off the W1/W2 traffic.
|
| 24 |
+
* row-block 0's ``x`` is transposed on the TENSOR engine (``nc_transpose``) in the prologue,
|
| 25 |
+
while the Tensor engine is otherwise dark in the pre-K4 seam. Later row blocks keep
|
| 26 |
+
``dma_transpose`` (they stream under rb0's matmuls).
|
| 27 |
+
* x prestaged in the prologue: EVERY row block's ``x`` staging is issued before the ``rb``
|
| 28 |
+
loop, in row-block order, so a later block's serialised DMA chain runs inside the pre-K4
|
| 29 |
+
seam instead of in the idle window at the rb boundary. Appending later blocks after rb=0's
|
| 30 |
+
stages means rb=0's readiness cannot regress -- the first matmul cannot be delayed.
|
| 31 |
+
|
| 32 |
+
The x staging is emitted through a module-level emitter (``_stage_xt``) so the ``x`` load,
|
| 33 |
+
the ``nc_transpose`` and the ``dma_transpose`` each keep exactly one source site.
|
| 34 |
|
| 35 |
PSUM: fc1 uses 1 bank, fc2 uses up to 4 banks; the ``y1_psum`` / ``out_psums`` tiles are
|
| 36 |
+
declared inside their loops so the ring never accumulates onto a stale PSUM value. The b2
|
| 37 |
+
bias broadcast is kept (removing it regressed the sibling kernel by 23-30%). Weight DMAs stay
|
| 38 |
+
one-per-tile: merging them into a single access-pattern DMA lost ~8 us (one completion signal
|
| 39 |
+
per 8 MB weight).
|
| 40 |
+
|
| 41 |
+
This kernel is LNC-parametric (``LNC`` from ``NEURON_LOGICAL_NC_CONFIG``) and keeps the
|
| 42 |
+
``min()`` remainder handling, so it is NOT restricted to exact-multiple shapes. INFERENCE
|
| 43 |
+
ONLY: no autograd rule.
|
| 44 |
"""
|
| 45 |
|
| 46 |
from __future__ import annotations
|
| 47 |
|
| 48 |
import math
|
| 49 |
+
import os as _os
|
| 50 |
|
| 51 |
import torch
|
| 52 |
from torch_neuronx import nki_op
|
|
|
|
| 61 |
|
| 62 |
_BF16 = nl.bfloat16
|
| 63 |
|
|
|
|
|
|
|
| 64 |
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 65 |
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
| 66 |
|
|
|
|
| 78 |
assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
|
| 79 |
|
| 80 |
|
| 81 |
+
def _stage_xt(x_hbm, r_abs, pr, n_k, K, rb_size, tensor_transpose):
|
| 82 |
"""Emit one row block's x staging and return its ``n_k`` transposed bf16 tiles.
|
| 83 |
|
| 84 |
+
``n_rs`` loads of ``[<=128, K]`` bf16, each followed by ``n_k`` transposes into the row
|
| 85 |
+
block's ``xt_tiles``. The transpose runs on the Tensor engine (``nc_transpose``, via a PSUM
|
| 86 |
+
tile) when ``tensor_transpose`` is set -- used for row block 0 while the Tensor engine is
|
| 87 |
+
dark in the pre-K4 seam -- and on the DMA engine (``dma_transpose``) otherwise. Kept as a
|
| 88 |
+
module-level emitter so the ``x`` load and each transpose keep a single source site.
|
| 89 |
|
| 90 |
Args:
|
| 91 |
+
x_hbm: [R, K] @ HBM bf16, the MLP input.
|
| 92 |
+
r_abs: absolute starting row of this row block.
|
| 93 |
+
pr: number of rows in this row block (<= rb_size).
|
| 94 |
+
n_k: number of K tiles (fc1 contraction dimension tiles).
|
| 95 |
+
K: hidden-in dimension.
|
| 96 |
+
rb_size: row-block width (free axis of the transposed tiles).
|
| 97 |
+
tensor_transpose: transpose on the Tensor engine (True) or the DMA engine (False).
|
| 98 |
|
| 99 |
Returns:
|
| 100 |
list of ``n_k`` transposed ``[128, rb_size]`` bf16 SBUF tiles.
|
|
|
|
| 114 |
for kt in range(n_k):
|
| 115 |
k0 = kt * _P_MAX
|
| 116 |
pk = min(_P_MAX, K - k0)
|
| 117 |
+
if tensor_transpose:
|
| 118 |
+
# Transpose on the (idle) Tensor engine via PSUM, then evict to the xt tile.
|
| 119 |
+
tp = nl.ndarray((_P_MAX, _P_MAX), dtype=_BF16, buffer=nl.psum)
|
| 120 |
+
nisa.nc_transpose(dst=tp[0:pk, 0:prs], data=x_b16[0:prs, k0:k0 + pk],
|
| 121 |
+
engine=nisa.engine.tensor)
|
| 122 |
+
nisa.tensor_copy(dst=xt_tiles[kt][0:pk, rr0:rr0 + prs], src=tp[0:pk, 0:prs])
|
| 123 |
+
else:
|
| 124 |
+
nisa.dma_transpose(dst=xt_tiles[kt][0:pk, rr0:rr0 + prs],
|
| 125 |
+
src=x_b16[0:prs, k0:k0 + pk])
|
| 126 |
return xt_tiles
|
| 127 |
|
| 128 |
|
|
|
|
| 133 |
Args:
|
| 134 |
x_hbm (nl.ndarray): [R, K] @ HBM, **bfloat16** (kernel 1 emits bf16).
|
| 135 |
w1t_hbm (nl.ndarray): [K, H] @ HBM, **bf16**, == fc1.weight.T.
|
| 136 |
+
b1_hbm (nl.ndarray): [128, H/128] @ HBM, float32, == pack_b1(fc1 bias).
|
| 137 |
w2t_hbm (nl.ndarray): [H, K] @ HBM, **bf16**, == fc2.weight.T.
|
| 138 |
b2_hbm (nl.ndarray): [K] @ HBM, float32.
|
| 139 |
|
|
|
|
| 147 |
_kernel_assert(K_w1t == K, f"w1t in-dim {K_w1t} != K {K}; expected w1t = w1.T of shape [K, H]")
|
| 148 |
_kernel_assert(K_w2t == K, f"w2t out-dim {K_w2t} != K {K}; expected w2t = w2.T of shape [H, K]")
|
| 149 |
_kernel_assert(H_w2t == H, f"w2t in-dim {H_w2t} != H {H}")
|
| 150 |
+
_kernel_assert(H % _P_MAX == 0, f"H={H} must be a multiple of {_P_MAX} for the packed b1 layout")
|
| 151 |
+
_kernel_assert(tuple(b1_hbm.shape) == (_P_MAX, H // _P_MAX),
|
| 152 |
+
f"b1 must be pack_b1(fc1 bias): [{_P_MAX}, {H // _P_MAX}], got {tuple(b1_hbm.shape)}")
|
| 153 |
_kernel_assert(
|
| 154 |
R % LNC == 0,
|
| 155 |
f"the {LNC}-core kernel needs R divisible by {LNC}, got R={R}. "
|
|
|
|
| 166 |
n_k = _div_ceil(K, _P_MAX) # K tiles (fc1 contraction)
|
| 167 |
n_h = _div_ceil(H, _P_MAX) # H tiles (fc1 out-rows / fc2 contraction)
|
| 168 |
|
| 169 |
+
# b1 FIRST, in ONE DMA on the Scalar engine's HW-DGE queue: the packed [128, H/128] bias
|
| 170 |
+
# lands in a single ~16 KB transfer so every gelu's bias is resident long before the first
|
| 171 |
+
# matmul retires, and gelu's wait no longer counts the W1/W2 traffic. b1_sb[:, ht] is the
|
| 172 |
+
# bias column for H tile ht (b1_sb[p, ht] == fc1_bias[ht*128 + p]).
|
| 173 |
+
b1_sb = nl.ndarray((_P_MAX, n_h), dtype=nl.float32, buffer=nl.sbuf)
|
| 174 |
+
nisa.dma_copy(dst=b1_sb, src=b1_hbm, dge_mode=nisa.dge_mode.hwdge, engine=nisa.engine.scalar)
|
| 175 |
+
|
| 176 |
+
# Main fused loop geometry (needed to size the x prestage).
|
| 177 |
+
rb_size = _F_MOV_MAX
|
| 178 |
+
n_rb = _div_ceil(rows_per, rb_size)
|
| 179 |
+
n_ko = _div_ceil(K, _F_MOV_MAX)
|
| 180 |
+
|
| 181 |
+
# x BEFORE weights: stage row block 0's x first (Tensor-engine transpose, filling the dark
|
| 182 |
+
# pre-K4 seam), then W1, then later row blocks' x (DMA transpose, streaming under rb0's
|
| 183 |
+
# matmuls), then W2. Appending later blocks after rb=0's stages leaves rb=0's readiness fixed.
|
| 184 |
+
xt_sets = [None] * n_rb
|
| 185 |
+
xt_sets[0] = _stage_xt(x_hbm, r_base, min(rb_size, rows_per), n_k, K, rb_size,
|
| 186 |
+
tensor_transpose=True)
|
| 187 |
|
| 188 |
+
# W1 staging (pre-transposed bf16, one DMA per K tile).
|
| 189 |
w1t_tiles = []
|
| 190 |
for kt in range(n_k):
|
| 191 |
k0 = kt * _P_MAX
|
|
|
|
| 194 |
nisa.dma_copy(dst=t[0:pk, :], src=w1t_hbm[k0:k0 + pk, :])
|
| 195 |
w1t_tiles.append(t)
|
| 196 |
|
| 197 |
+
# Later row blocks' x via DMA transpose, under rb0's matmuls.
|
| 198 |
+
for rb_pre in range(1, n_rb):
|
| 199 |
+
r_pre = rb_pre * rb_size
|
| 200 |
+
xt_sets[rb_pre] = _stage_xt(x_hbm, r_base + r_pre, min(rb_size, rows_per - r_pre),
|
| 201 |
+
n_k, K, rb_size, tensor_transpose=False)
|
| 202 |
+
|
| 203 |
+
# W2 staging (one DMA per H tile, so fc2 can start on the first strips).
|
| 204 |
w2t_tiles = []
|
| 205 |
for ht in range(n_h):
|
| 206 |
h0 = ht * _P_MAX
|
|
|
|
| 209 |
nisa.dma_copy(dst=t[0:ph, :], src=w2t_hbm[h0:h0 + ph, :])
|
| 210 |
w2t_tiles.append(t)
|
| 211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
for rb in range(n_rb):
|
| 213 |
r_local = rb * rb_size
|
| 214 |
pr = min(rb_size, rows_per - r_local)
|
|
|
|
| 235 |
stationary=w1t_tiles[kt][0:pk, h0:h0 + ph],
|
| 236 |
moving=xt_tiles[kt][0:pk, 0:pr],
|
| 237 |
)
|
| 238 |
+
# b1 comes from the prologue-resident packed tile, column ht.
|
| 239 |
gt = nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf)
|
| 240 |
nisa.activation(dst=gt[0:ph, 0:pr], op=nl.gelu,
|
| 241 |
+
data=y1_psum[0:ph, 0:pr], bias=b1_sb[0:ph, ht:ht + 1])
|
| 242 |
gt_tiles.append(gt)
|
| 243 |
|
| 244 |
# ---- fc2: out[r,k] = sum_h g[r,h] * w2[k,h] + b2[k] ----
|
|
|
|
| 282 |
return out_hbm
|
| 283 |
|
| 284 |
|
| 285 |
+
def pack_b1(b1: torch.Tensor) -> torch.Tensor:
|
| 286 |
+
"""Pack fc1 bias ``[H]`` fp32 into ``[128, H/128]`` so ``b1p[p, ht] == b1[ht*128 + p]``.
|
| 287 |
+
|
| 288 |
+
Lets the kernel load the whole bias in one DMA instead of ``H/128`` column DMAs. ``H`` must
|
| 289 |
+
be a multiple of 128.
|
| 290 |
+
"""
|
| 291 |
+
H = b1.shape[0]
|
| 292 |
+
assert H % _P_MAX == 0, f"pack_b1 needs H % {_P_MAX} == 0, got H={H}"
|
| 293 |
+
return b1.reshape(-1, _P_MAX).t().contiguous()
|
| 294 |
+
|
| 295 |
+
|
| 296 |
def _no_grad(*tensors) -> None:
|
| 297 |
"""Refuse gradient-tracking inputs -- this op registers no autograd rule."""
|
| 298 |
for t in tensors:
|
|
|
|
| 315 |
|
| 316 |
Args:
|
| 317 |
x : [R, K] **bf16** on 'neuron'. ``R`` must be divisible by :data:`LNC`.
|
| 318 |
+
w1t : [K, H] bf16 == fc1.weight.T.
|
| 319 |
+
b1 : fc1 bias, either ``[H]`` float32 (packed on the fly) or pre-packed
|
| 320 |
+
``[128, H/128]`` from :func:`pack_b1`.
|
| 321 |
w2t : [H, K] bf16 == fc2.weight.T. b2 : [K] float32.
|
| 322 |
|
| 323 |
Returns:
|
| 324 |
[R, K] bf16.
|
| 325 |
"""
|
| 326 |
_no_grad(x, w1t, b1, w2t, b2)
|
| 327 |
+
if b1.dim() == 1: # accept the pre-pack [H] bias; pack it on the fly
|
| 328 |
+
b1 = pack_b1(b1)
|
| 329 |
return _mlp_k4_xp_kernel[LNC](x, w1t, b1, w2t, b2)
|
| 330 |
|
| 331 |
|
build/torch-neuron/nki_kernels/mlp_res_ln.py
CHANGED
|
@@ -1,50 +1,25 @@
|
|
| 1 |
"""GELU MLP with the residual add + NEXT layer's affine-free LN1 in the epilogue.
|
| 2 |
|
| 3 |
-
Same fc1/gelu as ``mlp.py``
|
| 4 |
-
|
|
|
|
|
|
|
| 5 |
x_new = x + (g @ w2.T + b2) fp32 (MLP output never rounded to bf16)
|
| 6 |
z1 = LN_no_affine(x_new; eps) bf16 (same stat chain as res_layernorm.py)
|
| 7 |
Returns (x_new [R,K] fp32, z1 [R,K] bf16). Replaces K4 + the next layer's K1.
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
the weights pre-transposed to bf16. Returns ``[R, K]`` bf16.
|
| 16 |
-
|
| 17 |
-
WHY IT IS SHAPED THIS WAY
|
| 18 |
-
-------------------------
|
| 19 |
-
This is the effective shipping kernel: it is the ``b1pre`` MLP (fc1 bias staged in the
|
| 20 |
-
prologue so every gelu's bias is resident from t=0) with the x-prestage applied. Two
|
| 21 |
-
design choices carry the performance:
|
| 22 |
-
|
| 23 |
-
* b1 first: the 32 per-block ``b1`` bias tiles are DMA'd before the 16 MB of weight
|
| 24 |
-
staging, so they land within ~1 us of kernel start and no gelu ever waits on its bias.
|
| 25 |
-
* x prestaged in the prologue: BOTH row blocks' ``x`` load + dma_transpose staging is
|
| 26 |
-
issued before the ``rb`` loop, in row-block order. rb=0's staging already hid in the
|
| 27 |
-
pre-K4 seam (where the Tensor engine is dark for ~148 us because K1/K3 emit no Tensor
|
| 28 |
-
instructions); prestaging rb=1 gives it the same cover, filling the ~26.6 us Tensor
|
| 29 |
-
idle window that used to sit at the rb0->rb1 boundary. Appending rb=1's stages after
|
| 30 |
-
rb=0's, in order, means rb=0's readiness cannot regress -- the first matmul cannot be
|
| 31 |
-
delayed.
|
| 32 |
-
|
| 33 |
-
The x staging is emitted through a single module-level function (``_stage_xt``) rather
|
| 34 |
-
than inlined per row block, so the ``x`` load and the dma_transpose each keep exactly one
|
| 35 |
-
source site. The two-row-block staging is preserved exactly: ``xt_sets`` holds one
|
| 36 |
-
transposed-tile set per row block, and the loop consumes ``xt_sets[rb]``.
|
| 37 |
-
|
| 38 |
-
PSUM: fc1 uses 1 bank, fc2 uses up to 4 banks; the ``y1_psum`` / ``out_psums`` tiles are
|
| 39 |
-
declared inside their loops so the ring never accumulates onto a stale PSUM value. The
|
| 40 |
-
b2 bias broadcast is kept (removing it regressed the sibling kernel by 23-30%).
|
| 41 |
-
|
| 42 |
-
There are no ``engine=`` pins anywhere in this kernel. INFERENCE ONLY: no autograd rule.
|
| 43 |
"""
|
| 44 |
|
| 45 |
from __future__ import annotations
|
| 46 |
|
| 47 |
import math
|
|
|
|
| 48 |
|
| 49 |
import torch
|
| 50 |
from torch_neuronx import nki_op
|
|
@@ -53,14 +28,14 @@ import nki
|
|
| 53 |
import nki.isa as nisa
|
| 54 |
import nki.language as nl
|
| 55 |
|
|
|
|
|
|
|
| 56 |
#: PE array partition limit, and the moving-operand free-axis limit.
|
| 57 |
_P_MAX = 128
|
| 58 |
_F_MOV_MAX = 512
|
| 59 |
|
| 60 |
_BF16 = nl.bfloat16
|
| 61 |
|
| 62 |
-
#: Logical-to-physical NeuronCore degree. Must match the shipping shard's.
|
| 63 |
-
import os as _os
|
| 64 |
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 65 |
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
| 66 |
|
|
@@ -78,44 +53,6 @@ def _kernel_assert(condition: bool, error_text: str):
|
|
| 78 |
assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
|
| 79 |
|
| 80 |
|
| 81 |
-
def _stage_xt(x_hbm, r_abs, pr, n_k, K, rb_size):
|
| 82 |
-
"""Emit one row block's x staging and return its ``n_k`` transposed bf16 tiles.
|
| 83 |
-
|
| 84 |
-
``n_rs`` loads of ``[<=128, K]`` bf16, each followed by ``n_k`` DMA transposes into
|
| 85 |
-
the row block's ``xt_tiles``. Kept as a module-level emitter so the ``x`` load and
|
| 86 |
-
the dma_transpose each map to a single source site regardless of where it is called.
|
| 87 |
-
|
| 88 |
-
Args:
|
| 89 |
-
x_hbm: [R, K] @ HBM bf16, the MLP input.
|
| 90 |
-
r_abs: absolute starting row of this row block.
|
| 91 |
-
pr: number of rows in this row block (<= rb_size).
|
| 92 |
-
n_k: number of K tiles (fc1 contraction dimension tiles).
|
| 93 |
-
K: hidden-in dimension.
|
| 94 |
-
rb_size: row-block width (free axis of the transposed tiles).
|
| 95 |
-
|
| 96 |
-
Returns:
|
| 97 |
-
list of ``n_k`` transposed ``[128, rb_size]`` bf16 SBUF tiles.
|
| 98 |
-
"""
|
| 99 |
-
n_rs = _div_ceil(pr, _P_MAX)
|
| 100 |
-
xt_tiles = []
|
| 101 |
-
for _ in range(n_k):
|
| 102 |
-
xt_tiles.append(nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf))
|
| 103 |
-
for rs in range(n_rs):
|
| 104 |
-
rr0 = rs * _P_MAX
|
| 105 |
-
prs = min(_P_MAX, pr - rr0)
|
| 106 |
-
x_b16 = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
|
| 107 |
-
nisa.dma_copy(
|
| 108 |
-
dst=x_b16[0:prs, 0:K],
|
| 109 |
-
src=x_hbm[r_abs + rr0:r_abs + rr0 + prs, 0:K],
|
| 110 |
-
)
|
| 111 |
-
for kt in range(n_k):
|
| 112 |
-
k0 = kt * _P_MAX
|
| 113 |
-
pk = min(_P_MAX, K - k0)
|
| 114 |
-
nisa.dma_transpose(dst=xt_tiles[kt][0:pk, rr0:rr0 + prs],
|
| 115 |
-
src=x_b16[0:prs, k0:k0 + pk])
|
| 116 |
-
return xt_tiles
|
| 117 |
-
|
| 118 |
-
|
| 119 |
@nki.jit
|
| 120 |
def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
|
| 121 |
"""gelu(x @ w1.T + b1) @ w2.T + b2, rows sharded, b1 AND both x blocks resident early.
|
|
@@ -123,7 +60,7 @@ def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
|
|
| 123 |
Args:
|
| 124 |
x_hbm (nl.ndarray): [R, K] @ HBM, **bfloat16** (kernel 1 emits bf16).
|
| 125 |
w1t_hbm (nl.ndarray): [K, H] @ HBM, **bf16**, == fc1.weight.T.
|
| 126 |
-
b1_hbm (nl.ndarray): [H]
|
| 127 |
w2t_hbm (nl.ndarray): [H, K] @ HBM, **bf16**, == fc2.weight.T.
|
| 128 |
b2_hbm (nl.ndarray): [K] @ HBM, float32.
|
| 129 |
|
|
@@ -137,6 +74,9 @@ def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
|
|
| 137 |
_kernel_assert(K_w1t == K, f"w1t in-dim {K_w1t} != K {K}; expected w1t = w1.T of shape [K, H]")
|
| 138 |
_kernel_assert(K_w2t == K, f"w2t out-dim {K_w2t} != K {K}; expected w2t = w2.T of shape [H, K]")
|
| 139 |
_kernel_assert(H_w2t == H, f"w2t in-dim {H_w2t} != H {H}")
|
|
|
|
|
|
|
|
|
|
| 140 |
_kernel_assert(
|
| 141 |
R % LNC == 0,
|
| 142 |
f"the {LNC}-core kernel needs R divisible by {LNC}, got R={R}. "
|
|
@@ -157,17 +97,25 @@ def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
|
|
| 157 |
n_k = _div_ceil(K, _P_MAX) # K tiles (fc1 contraction)
|
| 158 |
n_h = _div_ceil(H, _P_MAX) # H tiles (fc1 out-rows / fc2 contraction)
|
| 159 |
|
| 160 |
-
#
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
-
#
|
| 171 |
w1t_tiles = []
|
| 172 |
for kt in range(n_k):
|
| 173 |
k0 = kt * _P_MAX
|
|
@@ -176,6 +124,12 @@ def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
|
|
| 176 |
nisa.dma_copy(dst=t[0:pk, :], src=w1t_hbm[k0:k0 + pk, :])
|
| 177 |
w1t_tiles.append(t)
|
| 178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
w2t_tiles = []
|
| 180 |
for ht in range(n_h):
|
| 181 |
h0 = ht * _P_MAX
|
|
@@ -193,21 +147,6 @@ def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
|
|
| 193 |
var = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 194 |
rstd = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 195 |
|
| 196 |
-
# Main fused loop.
|
| 197 |
-
rb_size = _F_MOV_MAX
|
| 198 |
-
n_rb = _div_ceil(rows_per, rb_size)
|
| 199 |
-
n_ko = _div_ceil(K, _F_MOV_MAX)
|
| 200 |
-
|
| 201 |
-
# THE X-PRESTAGE: stage EVERY row block's x here, in rb order, so rb=1's serialised
|
| 202 |
-
# DMA chain runs inside the pre-K4 seam (Tensor dark) instead of inside the idle
|
| 203 |
-
# window at the rb0->rb1 boundary. Appending rb=1's stages after rb=0's leaves rb=0's
|
| 204 |
-
# stages exactly where they are on the Sync engine's sub-stream.
|
| 205 |
-
xt_sets = []
|
| 206 |
-
for rb_pre in range(n_rb):
|
| 207 |
-
xt_sets.append(_stage_xt(
|
| 208 |
-
x_hbm, r_base + rb_pre * rb_size,
|
| 209 |
-
min(rb_size, rows_per - rb_pre * rb_size), n_k, K, rb_size))
|
| 210 |
-
|
| 211 |
for rb in range(n_rb):
|
| 212 |
r_local = rb * rb_size
|
| 213 |
pr = min(rb_size, rows_per - r_local)
|
|
@@ -234,10 +173,10 @@ def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
|
|
| 234 |
stationary=w1t_tiles[kt][0:pk, h0:h0 + ph],
|
| 235 |
moving=xt_tiles[kt][0:pk, 0:pr],
|
| 236 |
)
|
| 237 |
-
# b1 comes from the prologue-resident tile.
|
| 238 |
gt = nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf)
|
| 239 |
nisa.activation(dst=gt[0:ph, 0:pr], op=nl.gelu,
|
| 240 |
-
data=y1_psum[0:ph, 0:pr], bias=
|
| 241 |
gt_tiles.append(gt)
|
| 242 |
|
| 243 |
# ---- fc2 + b2 + residual -> x_new (fp32), LN -> z1 (bf16), per 128-row subtile ----
|
|
@@ -303,6 +242,9 @@ def nki_mlp_res_ln(
|
|
| 303 |
eps: float,
|
| 304 |
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 305 |
"""MLP(x) + residual a, then affine-free LN. x [R,K] bf16, a [R,K] fp32.
|
|
|
|
| 306 |
Returns (x_new [R,K] fp32, z [R,K] bf16)."""
|
| 307 |
_no_grad(x, w1t, b1, w2t, b2, a)
|
|
|
|
|
|
|
| 308 |
return _mlp_res_ln_kernel[LNC](x, w1t, b1, w2t, b2, a, eps)
|
|
|
|
| 1 |
"""GELU MLP with the residual add + NEXT layer's affine-free LN1 in the epilogue.
|
| 2 |
|
| 3 |
+
Same fc1/gelu as ``mlp.py`` (including its reworked prologue, external contribution by
|
| 4 |
+
jlongen-aws, PR #2 on ``jburtoft/vjepa2-neuron-kernels``). fc2 is reordered to (row subtile
|
| 5 |
+
rs, out block ko) so each 128-row tile's full [128, K] fc2 output lands in one fp32 SBUF row,
|
| 6 |
+
then:
|
| 7 |
x_new = x + (g @ w2.T + b2) fp32 (MLP output never rounded to bf16)
|
| 8 |
z1 = LN_no_affine(x_new; eps) bf16 (same stat chain as res_layernorm.py)
|
| 9 |
Returns (x_new [R,K] fp32, z1 [R,K] bf16). Replaces K4 + the next layer's K1.
|
| 10 |
|
| 11 |
+
Prologue (shared with mlp.py): b1 packed to [128, H/128] and loaded in one DMA on the Scalar
|
| 12 |
+
engine's HW-DGE queue; row block 0's x transposed on the (idle) Tensor engine, later blocks on
|
| 13 |
+
the DMA engine, all x staged before the weights so the first fc1 matmul does not wait on W1/W2.
|
| 14 |
+
Instruction order and numerics are unchanged from the previous version.
|
| 15 |
+
|
| 16 |
+
There are no ``engine=`` pins in the matmul/epilogue. INFERENCE ONLY: no autograd rule.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
"""
|
| 18 |
|
| 19 |
from __future__ import annotations
|
| 20 |
|
| 21 |
import math
|
| 22 |
+
import os as _os
|
| 23 |
|
| 24 |
import torch
|
| 25 |
from torch_neuronx import nki_op
|
|
|
|
| 28 |
import nki.isa as nisa
|
| 29 |
import nki.language as nl
|
| 30 |
|
| 31 |
+
from .mlp import _stage_xt, pack_b1
|
| 32 |
+
|
| 33 |
#: PE array partition limit, and the moving-operand free-axis limit.
|
| 34 |
_P_MAX = 128
|
| 35 |
_F_MOV_MAX = 512
|
| 36 |
|
| 37 |
_BF16 = nl.bfloat16
|
| 38 |
|
|
|
|
|
|
|
| 39 |
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 40 |
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
| 41 |
|
|
|
|
| 53 |
assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
|
| 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.
|
|
|
|
| 60 |
Args:
|
| 61 |
x_hbm (nl.ndarray): [R, K] @ HBM, **bfloat16** (kernel 1 emits bf16).
|
| 62 |
w1t_hbm (nl.ndarray): [K, H] @ HBM, **bf16**, == fc1.weight.T.
|
| 63 |
+
b1_hbm (nl.ndarray): [128, H/128] @ HBM, float32, == pack_b1(fc1 bias).
|
| 64 |
w2t_hbm (nl.ndarray): [H, K] @ HBM, **bf16**, == fc2.weight.T.
|
| 65 |
b2_hbm (nl.ndarray): [K] @ HBM, float32.
|
| 66 |
|
|
|
|
| 74 |
_kernel_assert(K_w1t == K, f"w1t in-dim {K_w1t} != K {K}; expected w1t = w1.T of shape [K, H]")
|
| 75 |
_kernel_assert(K_w2t == K, f"w2t out-dim {K_w2t} != K {K}; expected w2t = w2.T of shape [H, K]")
|
| 76 |
_kernel_assert(H_w2t == H, f"w2t in-dim {H_w2t} != H {H}")
|
| 77 |
+
_kernel_assert(H % _P_MAX == 0, f"H={H} must be a multiple of {_P_MAX} for the packed b1 layout")
|
| 78 |
+
_kernel_assert(tuple(b1_hbm.shape) == (_P_MAX, H // _P_MAX),
|
| 79 |
+
f"b1 must be pack_b1(fc1 bias): [{_P_MAX}, {H // _P_MAX}], got {tuple(b1_hbm.shape)}")
|
| 80 |
_kernel_assert(
|
| 81 |
R % LNC == 0,
|
| 82 |
f"the {LNC}-core kernel needs R divisible by {LNC}, got R={R}. "
|
|
|
|
| 97 |
n_k = _div_ceil(K, _P_MAX) # K tiles (fc1 contraction)
|
| 98 |
n_h = _div_ceil(H, _P_MAX) # H tiles (fc1 out-rows / fc2 contraction)
|
| 99 |
|
| 100 |
+
# Main fused loop geometry (needed to size the x prestage).
|
| 101 |
+
rb_size = _F_MOV_MAX
|
| 102 |
+
n_rb = _div_ceil(rows_per, rb_size)
|
| 103 |
+
n_ko = _div_ceil(K, _F_MOV_MAX)
|
| 104 |
+
|
| 105 |
+
# b1 FIRST, in ONE DMA on the Scalar engine's HW-DGE queue (packed [128, H/128]): the bias
|
| 106 |
+
# is resident long before the first matmul retires and gelu's wait no longer counts W1/W2
|
| 107 |
+
# traffic. b1_sb[:, ht] is the bias column for H tile ht.
|
| 108 |
+
b1_sb = nl.ndarray((_P_MAX, n_h), dtype=nl.float32, buffer=nl.sbuf)
|
| 109 |
+
nisa.dma_copy(dst=b1_sb, src=b1_hbm, dge_mode=nisa.dge_mode.hwdge, engine=nisa.engine.scalar)
|
| 110 |
+
|
| 111 |
+
# x BEFORE weights: rb0's x first (Tensor-engine transpose, filling the dark pre-K4 seam),
|
| 112 |
+
# then W1, then later row blocks' x (DMA transpose), then W2. Appending later blocks after
|
| 113 |
+
# rb0 leaves rb0's readiness fixed, so the first matmul cannot be delayed.
|
| 114 |
+
xt_sets = [None] * n_rb
|
| 115 |
+
xt_sets[0] = _stage_xt(x_hbm, r_base, min(rb_size, rows_per), n_k, K, rb_size,
|
| 116 |
+
tensor_transpose=True)
|
| 117 |
|
| 118 |
+
# W1 staging (pre-transposed bf16, one DMA per K tile).
|
| 119 |
w1t_tiles = []
|
| 120 |
for kt in range(n_k):
|
| 121 |
k0 = kt * _P_MAX
|
|
|
|
| 124 |
nisa.dma_copy(dst=t[0:pk, :], src=w1t_hbm[k0:k0 + pk, :])
|
| 125 |
w1t_tiles.append(t)
|
| 126 |
|
| 127 |
+
# Later row blocks' x via DMA transpose, under rb0's matmuls.
|
| 128 |
+
for rb_pre in range(1, n_rb):
|
| 129 |
+
r_pre = rb_pre * rb_size
|
| 130 |
+
xt_sets[rb_pre] = _stage_xt(x_hbm, r_base + r_pre, min(rb_size, rows_per - r_pre),
|
| 131 |
+
n_k, K, rb_size, tensor_transpose=False)
|
| 132 |
+
|
| 133 |
w2t_tiles = []
|
| 134 |
for ht in range(n_h):
|
| 135 |
h0 = ht * _P_MAX
|
|
|
|
| 147 |
var = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 148 |
rstd = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
for rb in range(n_rb):
|
| 151 |
r_local = rb * rb_size
|
| 152 |
pr = min(rb_size, rows_per - r_local)
|
|
|
|
| 173 |
stationary=w1t_tiles[kt][0:pk, h0:h0 + ph],
|
| 174 |
moving=xt_tiles[kt][0:pk, 0:pr],
|
| 175 |
)
|
| 176 |
+
# b1 comes from the prologue-resident packed tile, column ht.
|
| 177 |
gt = nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf)
|
| 178 |
nisa.activation(dst=gt[0:ph, 0:pr], op=nl.gelu,
|
| 179 |
+
data=y1_psum[0:ph, 0:pr], bias=b1_sb[0:ph, ht:ht + 1])
|
| 180 |
gt_tiles.append(gt)
|
| 181 |
|
| 182 |
# ---- fc2 + b2 + residual -> x_new (fp32), LN -> z1 (bf16), per 128-row subtile ----
|
|
|
|
| 242 |
eps: float,
|
| 243 |
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 244 |
"""MLP(x) + residual a, then affine-free LN. x [R,K] bf16, a [R,K] fp32.
|
| 245 |
+
b1 is fc1 bias, either [H] float32 (packed on the fly) or pre-packed [128, H/128].
|
| 246 |
Returns (x_new [R,K] fp32, z [R,K] bf16)."""
|
| 247 |
_no_grad(x, w1t, b1, w2t, b2, a)
|
| 248 |
+
if b1.dim() == 1:
|
| 249 |
+
b1 = pack_b1(b1)
|
| 250 |
return _mlp_res_ln_kernel[LNC](x, w1t, b1, w2t, b2, a, eps)
|
build/torch-neuron/nki_kernels/res_layernorm.py
CHANGED
|
@@ -19,9 +19,16 @@ WHY IT IS SHAPED THIS WAY
|
|
| 19 |
-------------------------
|
| 20 |
* LNC = 2 shard: the kernel is launched on both NeuronCores; each owns ``R // 2`` rows
|
| 21 |
selected by ``program_id(0)``, so the two cores never touch the same tokens.
|
| 22 |
-
*
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
* fp32 residual / bf16 branch: ``a`` (the residual stream) stays fp32 so ``x_new`` is
|
| 26 |
bitwise-invariant math and can be carried forward losslessly; the branch inputs and
|
| 27 |
the normalized output ``z`` are bf16, which is the precision boundary the downstream
|
|
@@ -30,10 +37,11 @@ WHY IT IS SHAPED THIS WAY
|
|
| 30 |
(neg_shift -> copy-reduce -> neg_mu -> square-reduce -> var -> rsqrt) that keeps the
|
| 31 |
per-lane statistics in fp32; the final normalize is a single fused ``tensor_scalar``
|
| 32 |
(``z = (x_new + neg_mu) * rstd``) whose two per-lane operands cost the same as one op
|
| 33 |
-
and whose bf16 cast rides the write.
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
"""
|
| 38 |
|
| 39 |
from __future__ import annotations
|
|
@@ -56,9 +64,6 @@ import os as _os
|
|
| 56 |
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 57 |
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
| 58 |
|
| 59 |
-
#: Ring depth: number of rotating SBUF tile sets that overlap load with compute.
|
| 60 |
-
_RING = 4
|
| 61 |
-
|
| 62 |
|
| 63 |
def _kernel_assert(condition: bool, error_text: str):
|
| 64 |
"""Assert with an NKI-formatted error message."""
|
|
@@ -70,9 +75,10 @@ def _res_layernorm_fold_kernel(a_hbm, b_hbm, eps):
|
|
| 70 |
"""Emit the fold kernel body: ``(a + sum(b), LN_no_affine(a + sum(b)))``, token-major.
|
| 71 |
|
| 72 |
gamma/beta live in the consumers' packed weights, so this kernel takes no affine
|
| 73 |
-
parameters. Rows are sharded across ``LNC`` cores;
|
| 74 |
-
|
| 75 |
-
normalized result is cast to bf16 on write.
|
|
|
|
| 76 |
|
| 77 |
Args:
|
| 78 |
a_hbm (nl.ndarray): [R, H] @ HBM float32 -- the residual stream.
|
|
@@ -108,69 +114,67 @@ def _res_layernorm_fold_kernel(a_hbm, b_hbm, eps):
|
|
| 108 |
inv_h = 1.0 / float(H)
|
| 109 |
neg_inv_h = -inv_h
|
| 110 |
|
| 111 |
-
#
|
| 112 |
-
a_sb
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
for
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
| 124 |
|
| 125 |
# Scratch destination for the reduce activations (its full tile is discarded; only
|
| 126 |
# the reduce result is consumed).
|
| 127 |
sink = nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf)
|
| 128 |
|
| 129 |
-
# Per-lane
|
| 130 |
-
acc = nl.ndarray((_P_MAX,
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
|
|
|
| 135 |
|
| 136 |
for t in range(n_t):
|
| 137 |
-
k = t % _RING
|
| 138 |
r0 = r_base + t * _P_MAX
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
nisa.dma_copy(dst=a_sb[k], src=a_hbm[r0:r0 + _P_MAX, 0:H])
|
| 142 |
-
for j in range(n_b):
|
| 143 |
-
nisa.dma_copy(dst=b_sb[k][j], src=b_hbm[j, r0:r0 + _P_MAX, 0:H])
|
| 144 |
|
| 145 |
# ---- x_new = a + sum_j b[j] in fp32, then write the residual stream out ----
|
| 146 |
-
nisa.tensor_tensor(dst=
|
| 147 |
for j in range(1, n_b):
|
| 148 |
-
nisa.tensor_tensor(dst=
|
| 149 |
|
| 150 |
-
nisa.dma_copy(dst=x_new_hbm[r0:r0 + _P_MAX, 0:H], src=
|
| 151 |
|
| 152 |
# ---- thinned stat chain: mean via shift-and-reduce, variance, rstd ----
|
| 153 |
-
nisa.tensor_scalar(dst=neg_shift, data=s_sb[
|
| 154 |
op0=nl.multiply, operand0=-1.0)
|
| 155 |
-
nisa.activation_reduce(dst=sink, op=nl.copy, data=
|
| 156 |
-
reduce_op=nl.add, reduce_res=acc,
|
| 157 |
-
bias=neg_shift, scale=1.0)
|
| 158 |
-
nisa.tensor_scalar(dst=neg_mu, data=acc, op0=nl.multiply, operand0=neg_inv_h,
|
| 159 |
-
op1=nl.add, operand1=neg_shift)
|
| 160 |
-
nisa.activation_reduce(dst=sink, op=nl.square, data=
|
| 161 |
-
reduce_op=nl.add, reduce_res=
|
| 162 |
-
nisa.tensor_scalar(dst=var, data=
|
| 163 |
op1=nl.add, operand1=eps)
|
| 164 |
-
nisa.activation(dst=rstd, op=nl.rsqrt, data=var)
|
| 165 |
|
| 166 |
# ---- one-op affine-free apply: z = (x_new + neg_mu) * rstd, bf16 cast on write.
|
| 167 |
-
# Both operands are per-lane
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
op1=nl.multiply, operand1=rstd)
|
| 172 |
|
| 173 |
-
nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:H], src=z_sb[
|
|
|
|
| 174 |
|
| 175 |
return x_new_hbm, z_hbm
|
| 176 |
|
|
|
|
| 19 |
-------------------------
|
| 20 |
* LNC = 2 shard: the kernel is launched on both NeuronCores; each owns ``R // 2`` rows
|
| 21 |
selected by ``program_id(0)``, so the two cores never touch the same tokens.
|
| 22 |
+
* Every token tile has its own SBUF slot ([P, n_t, H]) and all loads are issued before any
|
| 23 |
+
compute, so tile ``t+1``'s loads never wait behind tile ``t``'s stores. One DMA per tile and
|
| 24 |
+
operand: a single whole-shard DMA would signal completion only at the end. (External
|
| 25 |
+
contribution by jlongen-aws, PR #2 on ``jburtoft/vjepa2-neuron-kernels``; replaces the
|
| 26 |
+
depth-4 ring, same instructions in the same order so results are unchanged.)
|
| 27 |
+
* ``dge_mode.none`` on every DMA: the runtime generates the descriptors before execution,
|
| 28 |
+
so no engine spends time issuing them. The default (software DGE) issued each of the ~40
|
| 29 |
+
DMAs on GpSimd at ~0.75 us apiece, one after another, and the first add waited ~8 us for
|
| 30 |
+
them. Hardware DGE on the Sync queue was as fast standalone, but inside the encoder it
|
| 31 |
+
delayed K2/K4's loads on that queue by about as much as it saved, so it is not used.
|
| 32 |
* fp32 residual / bf16 branch: ``a`` (the residual stream) stays fp32 so ``x_new`` is
|
| 33 |
bitwise-invariant math and can be carried forward losslessly; the branch inputs and
|
| 34 |
the normalized output ``z`` are bf16, which is the precision boundary the downstream
|
|
|
|
| 37 |
(neg_shift -> copy-reduce -> neg_mu -> square-reduce -> var -> rsqrt) that keeps the
|
| 38 |
per-lane statistics in fp32; the final normalize is a single fused ``tensor_scalar``
|
| 39 |
(``z = (x_new + neg_mu) * rstd``) whose two per-lane operands cost the same as one op
|
| 40 |
+
and whose bf16 cast rides the write. Each per-lane statistic has its own column per tile
|
| 41 |
+
(``[128, n_t]``) so tile ``t+1``'s chain does not wait on tile ``t``'s.
|
| 42 |
|
| 43 |
+
This kernel is LNC-parametric (``LNC`` from ``NEURON_LOGICAL_NC_CONFIG``). There are no
|
| 44 |
+
``engine=`` pins. INFERENCE ONLY: no autograd rule is registered.
|
| 45 |
"""
|
| 46 |
|
| 47 |
from __future__ import annotations
|
|
|
|
| 64 |
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 65 |
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
| 66 |
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
def _kernel_assert(condition: bool, error_text: str):
|
| 69 |
"""Assert with an NKI-formatted error message."""
|
|
|
|
| 75 |
"""Emit the fold kernel body: ``(a + sum(b), LN_no_affine(a + sum(b)))``, token-major.
|
| 76 |
|
| 77 |
gamma/beta live in the consumers' packed weights, so this kernel takes no affine
|
| 78 |
+
parameters. Rows are sharded across ``LNC`` cores; every token tile is loaded up front
|
| 79 |
+
into its own SBUF slot (all loads issued before any compute), summed in fp32, normalized
|
| 80 |
+
with the thinned stat chain, and the normalized result is cast to bf16 on write. Every DMA
|
| 81 |
+
uses ``dge_mode.none`` so the runtime pre-generates the descriptors.
|
| 82 |
|
| 83 |
Args:
|
| 84 |
a_hbm (nl.ndarray): [R, H] @ HBM float32 -- the residual stream.
|
|
|
|
| 114 |
inv_h = 1.0 / float(H)
|
| 115 |
neg_inv_h = -inv_h
|
| 116 |
|
| 117 |
+
# One SBUF slot per tile; all loads issued before any compute or store (see module doc).
|
| 118 |
+
# a_sb[:, t, :] is tile t's residual, b_sb[:, j, t, :] is branch j of tile t.
|
| 119 |
+
a_sb = nl.ndarray((_P_MAX, n_t, H), dtype=nl.float32, buffer=nl.sbuf)
|
| 120 |
+
b_sb = nl.ndarray((_P_MAX, n_b, n_t, H), dtype=_BF16, buffer=nl.sbuf)
|
| 121 |
+
for t in range(n_t):
|
| 122 |
+
r0 = r_base + t * _P_MAX
|
| 123 |
+
nisa.dma_copy(dst=a_sb[:, t, :], src=a_hbm[r0:r0 + _P_MAX, 0:H],
|
| 124 |
+
dge_mode=nisa.dge_mode.none)
|
| 125 |
+
for j in range(n_b):
|
| 126 |
+
nisa.dma_copy(dst=b_sb[:, j, t, :], src=b_hbm[j, r0:r0 + _P_MAX, 0:H],
|
| 127 |
+
dge_mode=nisa.dge_mode.none)
|
| 128 |
+
|
| 129 |
+
s_sb = nl.ndarray((_P_MAX, n_t, H), dtype=nl.float32, buffer=nl.sbuf)
|
| 130 |
+
z_sb = nl.ndarray((_P_MAX, n_t, H), dtype=_BF16, buffer=nl.sbuf)
|
| 131 |
|
| 132 |
# Scratch destination for the reduce activations (its full tile is discarded; only
|
| 133 |
# the reduce result is consumed).
|
| 134 |
sink = nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf)
|
| 135 |
|
| 136 |
+
# Per-lane statistics, one column per tile so tile t+1's chain doesn't wait on tile t's.
|
| 137 |
+
acc = nl.ndarray((_P_MAX, n_t), dtype=nl.float32, buffer=nl.sbuf)
|
| 138 |
+
acc2 = nl.ndarray((_P_MAX, n_t), dtype=nl.float32, buffer=nl.sbuf)
|
| 139 |
+
neg_shift = nl.ndarray((_P_MAX, n_t), dtype=nl.float32, buffer=nl.sbuf)
|
| 140 |
+
neg_mu = nl.ndarray((_P_MAX, n_t), dtype=nl.float32, buffer=nl.sbuf)
|
| 141 |
+
var = nl.ndarray((_P_MAX, n_t), dtype=nl.float32, buffer=nl.sbuf)
|
| 142 |
+
rstd = nl.ndarray((_P_MAX, n_t), dtype=nl.float32, buffer=nl.sbuf)
|
| 143 |
|
| 144 |
for t in range(n_t):
|
|
|
|
| 145 |
r0 = r_base + t * _P_MAX
|
| 146 |
+
s = s_sb[:, t, :]
|
| 147 |
+
c = slice(t, t + 1)
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
# ---- x_new = a + sum_j b[j] in fp32, then write the residual stream out ----
|
| 150 |
+
nisa.tensor_tensor(dst=s, data1=a_sb[:, t, :], data2=b_sb[:, 0, t, :], op=nl.add)
|
| 151 |
for j in range(1, n_b):
|
| 152 |
+
nisa.tensor_tensor(dst=s, data1=s, data2=b_sb[:, j, t, :], op=nl.add)
|
| 153 |
|
| 154 |
+
nisa.dma_copy(dst=x_new_hbm[r0:r0 + _P_MAX, 0:H], src=s, dge_mode=nisa.dge_mode.none)
|
| 155 |
|
| 156 |
# ---- thinned stat chain: mean via shift-and-reduce, variance, rstd ----
|
| 157 |
+
nisa.tensor_scalar(dst=neg_shift[:, c], data=s_sb[:, t, 0:1],
|
| 158 |
op0=nl.multiply, operand0=-1.0)
|
| 159 |
+
nisa.activation_reduce(dst=sink, op=nl.copy, data=s,
|
| 160 |
+
reduce_op=nl.add, reduce_res=acc[:, c],
|
| 161 |
+
bias=neg_shift[:, c], scale=1.0)
|
| 162 |
+
nisa.tensor_scalar(dst=neg_mu[:, c], data=acc[:, c], op0=nl.multiply, operand0=neg_inv_h,
|
| 163 |
+
op1=nl.add, operand1=neg_shift[:, c])
|
| 164 |
+
nisa.activation_reduce(dst=sink, op=nl.square, data=s,
|
| 165 |
+
reduce_op=nl.add, reduce_res=acc2[:, c], bias=neg_mu[:, c], scale=1.0)
|
| 166 |
+
nisa.tensor_scalar(dst=var[:, c], data=acc2[:, c], op0=nl.multiply, operand0=inv_h,
|
| 167 |
op1=nl.add, operand1=eps)
|
| 168 |
+
nisa.activation(dst=rstd[:, c], op=nl.rsqrt, data=var[:, c])
|
| 169 |
|
| 170 |
# ---- one-op affine-free apply: z = (x_new + neg_mu) * rstd, bf16 cast on write.
|
| 171 |
+
# Both operands are per-lane vectors; the fused two-op tensor_scalar costs one op.
|
| 172 |
+
nisa.tensor_scalar(dst=z_sb[:, t, :], data=s,
|
| 173 |
+
op0=nl.add, operand0=neg_mu[:, c],
|
| 174 |
+
op1=nl.multiply, operand1=rstd[:, c])
|
|
|
|
| 175 |
|
| 176 |
+
nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:H], src=z_sb[:, t, :],
|
| 177 |
+
dge_mode=nisa.dge_mode.none)
|
| 178 |
|
| 179 |
return x_new_hbm, z_hbm
|
| 180 |
|