K4 (`mlp`) and K1/K3 (`res_layernorm_fold`): -4.8% end-to-end encoder, bitwise identical
#2
by jlongen-aws - opened
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
| 1 |
# Changelog
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
## v1.1 (2026-10)
|
| 4 |
|
| 5 |
Documentation only: README points to
|
|
|
|
| 1 |
# Changelog
|
| 2 |
|
| 3 |
+
## v1.2 (2026-10)
|
| 4 |
+
|
| 5 |
+
K4 (`mlp`) and K1/K3 (`res_layernorm_fold`) reworked: **-4.8% end-to-end encoder**, output
|
| 6 |
+
bitwise identical to v1.1.
|
| 7 |
+
|
| 8 |
+
* Full encoder (`torch.compile(backend="neuron")`, 24 layers, one 16-frame clip, trn2.3xlarge):
|
| 9 |
+
26.635 -> 25.353 ms/clip wall (3 rounds each), 26.83 -> 25.50 ms device time,
|
| 10 |
+
`mfu_estimated_percent` 39.5% -> 41.7%. Encoder output `torch.equal` to v1.1.
|
| 11 |
+
* Standalone at the encoder shape (2 rounds each, all bitwise identical):
|
| 12 |
+
* K4: 295.3 -> 248.6 us (-15.8%), MFU 70.3% -> 82.8%.
|
| 13 |
+
* K1 (1 branch): 48.0 -> 41.9 us (-12.7%), HBM bandwidth 73% -> 84% of the 716 GB/s ceiling.
|
| 14 |
+
* K3 (2 branches): 57.7 -> 48.2 us (-16.3%), HBM bandwidth 71% -> 85%.
|
| 15 |
+
* K4 schedule: x for row block 0 is staged before the weights (was queued behind 16 MB of
|
| 16 |
+
W1/W2); fc1 bias loads in one 16 KB DMA from a host-packed [128, H/128] layout (was 32 x 512 B
|
| 17 |
+
DMAs that stalled each gelu, and through PSUM the fc1 matmuls); row block 0's x is transposed on
|
| 18 |
+
the idle Tensor engine; b1 rides the Scalar engine's HW-DGE queue. Code: 3D SBUF tiles,
|
| 19 |
+
`nl.tile_size` constants, exact-multiple shapes asserted.
|
| 20 |
+
* K1/K3: every DMA uses `dge_mode.none` (descriptors generated by the runtime before execution).
|
| 21 |
+
Before, each of the ~40 DMAs was issued by GpSimd's software DGE at ~0.75 us apiece, one after
|
| 22 |
+
another, and the first add waited ~8 us for them. Hardware DGE on the Sync queue was as fast
|
| 23 |
+
standalone but slower in the encoder, so it is not used. All token tiles are also resident in SBUF
|
| 24 |
+
and loaded before any compute. Same instructions in the same order, so results are unchanged.
|
| 25 |
+
* `pack_b1` is exported; `mlp` accepts b1 either as [H] (packed on the fly) or pre-packed.
|
| 26 |
+
* K4 now requires K % 512 == 0, H % 128 == 0, R % 1024 == 0 (the encoder shape satisfies all three).
|
| 27 |
+
* Toolchain: everything above was measured on torch-neuronx 2.14.3 with nki 0.7.0b1. Standalone K4
|
| 28 |
+
and K1/K3 were also verified on the latest public NKI release (nki 0.6.0, neuronx-cc 2.27.5334):
|
| 29 |
+
K4 248.6 / 247.7 us vs 291.6 / 294.0 us for v1.1, K1 42.6 us, K3 48.1 us, all bitwise identical.
|
| 30 |
+
The full encoder could not be run on nki 0.6.0 with torch-neuronx 2.14.3 (v1.1 fails there too).
|
| 31 |
+
|
| 32 |
## v1.1 (2026-10)
|
| 33 |
|
| 34 |
Documentation only: README points to
|
build/torch-neuron/layers.py
CHANGED
|
@@ -40,6 +40,7 @@ from .nki_kernels import (
|
|
| 40 |
attention_block,
|
| 41 |
build_rope_tables,
|
| 42 |
mlp,
|
|
|
|
| 43 |
pack_out_weights,
|
| 44 |
pack_tables,
|
| 45 |
pack_v_weights,
|
|
@@ -85,7 +86,7 @@ def _pack_layer(lyr, tab):
|
|
| 85 |
"wqk_f": wqk_f.to(dev), "bqk_f": bqk_f.to(dev), "tab": tab,
|
| 86 |
"wvt_f": wvt_f.to(dev), "bv_f": bv_f.to(dev), "wot": wot.to(dev), "bo": bo.to(dev),
|
| 87 |
"w1t_f": (w1 * g2[None, :]).t().contiguous().to(torch.bfloat16).to(dev),
|
| 88 |
-
"b1_f": (b1 + w1 @ t2).
|
| 89 |
"w2t": lyr.mlp.fc2.weight.detach().t().contiguous().to(torch.bfloat16).to(dev),
|
| 90 |
"b2": lyr.mlp.fc2.bias.detach().float().contiguous().to(dev),
|
| 91 |
}
|
|
|
|
| 40 |
attention_block,
|
| 41 |
build_rope_tables,
|
| 42 |
mlp,
|
| 43 |
+
pack_b1,
|
| 44 |
pack_out_weights,
|
| 45 |
pack_tables,
|
| 46 |
pack_v_weights,
|
|
|
|
| 86 |
"wqk_f": wqk_f.to(dev), "bqk_f": bqk_f.to(dev), "tab": tab,
|
| 87 |
"wvt_f": wvt_f.to(dev), "bv_f": bv_f.to(dev), "wot": wot.to(dev), "bo": bo.to(dev),
|
| 88 |
"w1t_f": (w1 * g2[None, :]).t().contiguous().to(torch.bfloat16).to(dev),
|
| 89 |
+
"b1_f": pack_b1(b1 + w1 @ t2).to(dev), # [128, H/128], one 16 KB DMA in K4
|
| 90 |
"w2t": lyr.mlp.fc2.weight.detach().t().contiguous().to(torch.bfloat16).to(dev),
|
| 91 |
"b2": lyr.mlp.fc2.bias.detach().float().contiguous().to(dev),
|
| 92 |
}
|
build/torch-neuron/nki_kernels/__init__.py
CHANGED
|
@@ -8,12 +8,12 @@ from .attention_block import (
|
|
| 8 |
pack_v_weights,
|
| 9 |
pack_weights,
|
| 10 |
)
|
| 11 |
-
from .mlp import mlp
|
| 12 |
from .res_layernorm import res_layernorm_fold
|
| 13 |
from .rope_tables import build_rope_tables
|
| 14 |
|
| 15 |
__all__ = [
|
| 16 |
-
"res_layernorm_fold", "attention_block", "mlp",
|
| 17 |
"pack_weights", "pack_tables", "pack_v_weights", "pack_out_weights",
|
| 18 |
"lane_permutation", "build_rope_tables",
|
| 19 |
]
|
|
|
|
| 8 |
pack_v_weights,
|
| 9 |
pack_weights,
|
| 10 |
)
|
| 11 |
+
from .mlp import mlp, pack_b1
|
| 12 |
from .res_layernorm import res_layernorm_fold
|
| 13 |
from .rope_tables import build_rope_tables
|
| 14 |
|
| 15 |
__all__ = [
|
| 16 |
+
"res_layernorm_fold", "attention_block", "mlp", "pack_b1",
|
| 17 |
"pack_weights", "pack_tables", "pack_v_weights", "pack_out_weights",
|
| 18 |
"lane_permutation", "build_rope_tables",
|
| 19 |
]
|
build/torch-neuron/nki_kernels/mlp.py
CHANGED
|
@@ -1,42 +1,21 @@
|
|
| 1 |
-
"""K4 -- fused MLP (fc1 + gelu + fc2)
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
--
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
staging, so they land within ~1 us of kernel start and no gelu ever waits on its bias.
|
| 16 |
-
* x prestaged in the prologue: BOTH row blocks' ``x`` load + dma_transpose staging is
|
| 17 |
-
issued before the ``rb`` loop, in row-block order. rb=0's staging already hid in the
|
| 18 |
-
pre-K4 seam (where the Tensor engine is dark for ~148 us because K1/K3 emit no Tensor
|
| 19 |
-
instructions); prestaging rb=1 gives it the same cover, filling the ~26.6 us Tensor
|
| 20 |
-
idle window that used to sit at the rb0->rb1 boundary. Appending rb=1's stages after
|
| 21 |
-
rb=0's, in order, means rb=0's readiness cannot regress -- the first matmul cannot be
|
| 22 |
-
delayed.
|
| 23 |
-
|
| 24 |
-
The x staging is emitted through a single module-level function (``_stage_xt``) rather
|
| 25 |
-
than inlined per row block, so the ``x`` load and the dma_transpose each keep exactly one
|
| 26 |
-
source site. The two-row-block staging is preserved exactly: ``xt_sets`` holds one
|
| 27 |
-
transposed-tile set per row block, and the loop consumes ``xt_sets[rb]``.
|
| 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 |
-
b2 bias broadcast is kept (removing it regressed the sibling kernel by 23-30%).
|
| 32 |
-
|
| 33 |
-
There are no ``engine=`` pins anywhere in this kernel. INFERENCE ONLY: no autograd rule.
|
| 34 |
"""
|
| 35 |
|
| 36 |
from __future__ import annotations
|
| 37 |
|
| 38 |
-
import math
|
| 39 |
-
|
| 40 |
import torch
|
| 41 |
from torch_neuronx import nki_op
|
| 42 |
|
|
@@ -44,251 +23,134 @@ import nki
|
|
| 44 |
import nki.isa as nisa
|
| 45 |
import nki.language as nl
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
_F_MOV_MAX = 512
|
| 50 |
-
|
| 51 |
_BF16 = nl.bfloat16
|
| 52 |
|
| 53 |
#: Logical-to-physical NeuronCore degree. Must match the shipping shard's.
|
| 54 |
LNC = 2
|
| 55 |
|
| 56 |
-
_INV_SQRT2 = 1.0 / math.sqrt(2.0)
|
| 57 |
-
_INV_SQRT_2PI = 1.0 / math.sqrt(2.0 * math.pi)
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def _div_ceil(n: int, d: int) -> int:
|
| 61 |
-
"""Ceiling division (never write (n + d - 1) // d inline)."""
|
| 62 |
-
return (n + d - 1) // d
|
| 63 |
-
|
| 64 |
|
| 65 |
def _kernel_assert(condition: bool, error_text: str):
|
| 66 |
-
"""Assert with an NKI-formatted error message."""
|
| 67 |
assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
|
| 68 |
|
| 69 |
|
| 70 |
-
def
|
| 71 |
-
"""
|
| 72 |
-
|
| 73 |
-
``n_rs`` loads of ``[<=128, K]`` bf16, each followed by ``n_k`` DMA transposes into
|
| 74 |
-
the row block's ``xt_tiles``. Kept as a module-level emitter so the ``x`` load and
|
| 75 |
-
the dma_transpose each map to a single source site regardless of where it is called.
|
| 76 |
-
|
| 77 |
-
Args:
|
| 78 |
-
x_hbm: [R, K] @ HBM bf16, the MLP input.
|
| 79 |
-
r_abs: absolute starting row of this row block.
|
| 80 |
-
pr: number of rows in this row block (<= rb_size).
|
| 81 |
-
n_k: number of K tiles (fc1 contraction dimension tiles).
|
| 82 |
-
K: hidden-in dimension.
|
| 83 |
-
rb_size: row-block width (free axis of the transposed tiles).
|
| 84 |
-
|
| 85 |
-
Returns:
|
| 86 |
-
list of ``n_k`` transposed ``[128, rb_size]`` bf16 SBUF tiles.
|
| 87 |
-
"""
|
| 88 |
-
n_rs = _div_ceil(pr, _P_MAX)
|
| 89 |
-
xt_tiles = []
|
| 90 |
-
for _ in range(n_k):
|
| 91 |
-
xt_tiles.append(nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf))
|
| 92 |
for rs in range(n_rs):
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
x_b16 = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
|
| 96 |
-
nisa.dma_copy(
|
| 97 |
-
dst=x_b16[0:prs, 0:K],
|
| 98 |
-
src=x_hbm[r_abs + rr0:r_abs + rr0 + prs, 0:K],
|
| 99 |
-
)
|
| 100 |
-
for kt in range(n_k):
|
| 101 |
-
k0 = kt * _P_MAX
|
| 102 |
-
pk = min(_P_MAX, K - k0)
|
| 103 |
-
nisa.dma_transpose(dst=xt_tiles[kt][0:pk, rr0:rr0 + prs],
|
| 104 |
-
src=x_b16[0:prs, k0:k0 + pk])
|
| 105 |
-
return xt_tiles
|
| 106 |
|
| 107 |
|
| 108 |
@nki.jit
|
| 109 |
def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
|
| 110 |
-
"""
|
| 111 |
-
|
| 112 |
-
Args:
|
| 113 |
-
x_hbm (nl.ndarray): [R, K] @ HBM, **bfloat16** (kernel 1 emits bf16).
|
| 114 |
-
w1t_hbm (nl.ndarray): [K, H] @ HBM, **bf16**, == fc1.weight.T.
|
| 115 |
-
b1_hbm (nl.ndarray): [H] @ HBM, float32.
|
| 116 |
-
w2t_hbm (nl.ndarray): [H, K] @ HBM, **bf16**, == fc2.weight.T.
|
| 117 |
-
b2_hbm (nl.ndarray): [K] @ HBM, float32.
|
| 118 |
-
|
| 119 |
-
Returns:
|
| 120 |
-
nl.ndarray: [R, K] @ HBM, **bfloat16**.
|
| 121 |
-
"""
|
| 122 |
R, K = x_hbm.shape
|
| 123 |
-
|
| 124 |
-
H_w2t, K_w2t = w2t_hbm.shape
|
| 125 |
-
_kernel_assert(R >= 1 and K >= 1 and H >= 1, f"R({R}) K({K}) H({H}) must be >= 1")
|
| 126 |
-
_kernel_assert(K_w1t == K, f"w1t in-dim {K_w1t} != K {K}; expected w1t = w1.T of shape [K, H]")
|
| 127 |
-
_kernel_assert(K_w2t == K, f"w2t out-dim {K_w2t} != K {K}; expected w2t = w2.T of shape [H, K]")
|
| 128 |
-
_kernel_assert(H_w2t == H, f"w2t in-dim {H_w2t} != H {H}")
|
| 129 |
-
_kernel_assert(
|
| 130 |
-
R % LNC == 0,
|
| 131 |
-
f"the {LNC}-core kernel needs R divisible by {LNC}, got R={R}. "
|
| 132 |
-
f"program_id is a symbolic value, so a partial final row block cannot be "
|
| 133 |
-
f"clamped; use the one-core pre-transposed kernel for uneven R",
|
| 134 |
-
)
|
| 135 |
-
|
| 136 |
rows_per = R // LNC
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
out_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
|
| 141 |
|
| 142 |
-
|
| 143 |
-
|
|
|
|
| 144 |
|
| 145 |
-
#
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
ph = min(_P_MAX, H - h0)
|
| 151 |
-
t = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 152 |
-
nisa.dma_copy(dst=t[0:ph, 0:1], src=b1_hbm[h0:h0 + ph])
|
| 153 |
-
b1_tiles.append(t)
|
| 154 |
-
|
| 155 |
-
# Weight staging (pre-transposed bf16, one DMA per tile).
|
| 156 |
-
w1t_tiles = []
|
| 157 |
for kt in range(n_k):
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
for ht in range(n_h):
|
| 166 |
-
|
| 167 |
-
ph = min(_P_MAX, H - h0)
|
| 168 |
-
t = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
|
| 169 |
-
nisa.dma_copy(dst=t[0:ph, :], src=w2t_hbm[h0:h0 + ph, :])
|
| 170 |
-
w2t_tiles.append(t)
|
| 171 |
-
|
| 172 |
-
# Main fused loop.
|
| 173 |
-
rb_size = _F_MOV_MAX
|
| 174 |
-
n_rb = _div_ceil(rows_per, rb_size)
|
| 175 |
-
n_ko = _div_ceil(K, _F_MOV_MAX)
|
| 176 |
-
|
| 177 |
-
# THE X-PRESTAGE: stage EVERY row block's x here, in rb order, so rb=1's serialised
|
| 178 |
-
# DMA chain runs inside the pre-K4 seam (Tensor dark) instead of inside the idle
|
| 179 |
-
# window at the rb0->rb1 boundary. Appending rb=1's stages after rb=0's leaves rb=0's
|
| 180 |
-
# stages exactly where they are on the Sync engine's sub-stream.
|
| 181 |
-
xt_sets = []
|
| 182 |
-
for rb_pre in range(n_rb):
|
| 183 |
-
xt_sets.append(_stage_xt(
|
| 184 |
-
x_hbm, r_base + rb_pre * rb_size,
|
| 185 |
-
min(rb_size, rows_per - rb_pre * rb_size), n_k, K, rb_size))
|
| 186 |
|
| 187 |
for rb in range(n_rb):
|
| 188 |
-
|
| 189 |
-
pr = min(rb_size, rows_per - r_local)
|
| 190 |
-
n_rs = _div_ceil(pr, _P_MAX)
|
| 191 |
-
r_abs = r_base + r_local
|
| 192 |
|
| 193 |
-
#
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
# ---- fc1: gt_tiles[ht] = gelu(w1 @ x.T + b1), bf16, SBUF-resident ----
|
| 197 |
-
gt_tiles = []
|
| 198 |
for ht in range(n_h):
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
# Declared INSIDE the ht loop, so the ring never reuses a written PSUM tile.
|
| 202 |
-
# nc_matmul's default accumulate=None auto-detects "previously written" and
|
| 203 |
-
# would accumulate onto a stale value.
|
| 204 |
-
y1_psum = nl.ndarray((_P_MAX, rb_size), dtype=nl.float32, buffer=nl.psum)
|
| 205 |
for kt in nl.affine_range(n_k):
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
stationary=w1t_tiles[kt][0:pk, h0:h0 + ph],
|
| 211 |
-
moving=xt_tiles[kt][0:pk, 0:pr],
|
| 212 |
-
)
|
| 213 |
-
# b1 comes from the prologue-resident tile.
|
| 214 |
-
gt = nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf)
|
| 215 |
-
nisa.activation(dst=gt[0:ph, 0:pr], op=nl.gelu,
|
| 216 |
-
data=y1_psum[0:ph, 0:pr], bias=b1_tiles[ht][0:ph, 0:1])
|
| 217 |
-
gt_tiles.append(gt)
|
| 218 |
-
|
| 219 |
-
# ---- fc2: out[r,k] = sum_h g[r,h] * w2[k,h] + b2[k] ----
|
| 220 |
for ko in range(n_ko):
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
out_psums = []
|
| 224 |
for _ in range(n_rs):
|
| 225 |
-
|
| 226 |
-
nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
|
| 227 |
-
)
|
| 228 |
for ht in nl.affine_range(n_h):
|
| 229 |
-
h0 = ht * _P_MAX
|
| 230 |
-
ph = min(_P_MAX, H - h0)
|
| 231 |
for rs in range(n_rs):
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
)
|
| 239 |
-
# b2 broadcast, hoisted out of the rs loop (removing the broadcast itself
|
| 240 |
-
# cost 23% and 30% in two separate attempts).
|
| 241 |
-
b2_bc = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.sbuf)
|
| 242 |
-
nisa.dma_copy(dst=b2_bc[0:_P_MAX, 0:nk],
|
| 243 |
-
src=b2_hbm.ap(pattern=[[0, _P_MAX], [1, nk]], offset=ko0))
|
| 244 |
for rs in range(n_rs):
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
out_sb = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=_BF16, buffer=nl.sbuf)
|
| 248 |
-
nisa.tensor_tensor(dst=out_sb[0:prs, 0:nk],
|
| 249 |
-
data1=out_psums[rs][0:prs, 0:nk],
|
| 250 |
-
data2=b2_bc[0:prs, 0:nk],
|
| 251 |
-
op=nl.add)
|
| 252 |
-
nisa.dma_copy(
|
| 253 |
-
dst=out_hbm[r_abs + rr0:r_abs + rr0 + prs, ko0:ko0 + nk],
|
| 254 |
-
src=out_sb[0:prs, 0:nk],
|
| 255 |
-
)
|
| 256 |
|
| 257 |
return out_hbm
|
| 258 |
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
def _no_grad(*tensors) -> None:
|
| 261 |
"""Refuse gradient-tracking inputs -- this op registers no autograd rule."""
|
| 262 |
for t in tensors:
|
| 263 |
if t is not None and t.requires_grad:
|
| 264 |
-
raise RuntimeError(
|
| 265 |
-
|
| 266 |
-
"Use under torch.no_grad()/eval()."
|
| 267 |
-
)
|
| 268 |
|
| 269 |
|
| 270 |
@nki_op("vjepa2::nki_mlp_k4_xp", mutates_args={})
|
| 271 |
-
def nki_mlp_k4_xp(
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
w2t: torch.Tensor,
|
| 276 |
-
b2: torch.Tensor,
|
| 277 |
-
) -> torch.Tensor:
|
| 278 |
-
"""Fused MLP on LNC=2, pre-transposed bf16 weights, x prestaged in the prologue.
|
| 279 |
-
|
| 280 |
-
Args:
|
| 281 |
-
x : [R, K] **bf16** on 'neuron'. ``R`` must be divisible by :data:`LNC`.
|
| 282 |
-
w1t : [K, H] bf16 == fc1.weight.T. b1 : [H] float32.
|
| 283 |
-
w2t : [H, K] bf16 == fc2.weight.T. b2 : [K] float32.
|
| 284 |
-
|
| 285 |
-
Returns:
|
| 286 |
-
[R, K] bf16.
|
| 287 |
-
"""
|
| 288 |
_no_grad(x, w1t, b1, w2t, b2)
|
|
|
|
|
|
|
| 289 |
return _mlp_k4_xp_kernel[LNC](x, w1t, b1, w2t, b2)
|
| 290 |
|
| 291 |
|
| 292 |
-
#: Export alias
|
| 293 |
-
#: op-name string ("vjepa2::nki_mlp_k4_xp") is unchanged whichever name is used.
|
| 294 |
mlp = nki_mlp_k4_xp
|
|
|
|
| 1 |
+
"""K4 -- fused MLP (fc1 + gelu + fc2).
|
| 2 |
+
|
| 3 |
+
``out = gelu(x @ w1.T + b1) @ w2.T + b2``, rows sharded across LNC cores, bf16 matmuls with
|
| 4 |
+
fp32 PSUM accumulation. Same instruction order as v10 (bit-exact with mlp.py), with:
|
| 5 |
+
|
| 6 |
+
* 3D SBUF tiles instead of lists of tiles: W1 [P, n_k, H], W2 [P, n_h, K], x per row block
|
| 7 |
+
[P, n_rs, K], fc1 output [P, n_h, FMOV]. DMAs stay per tile (v14 merged them into single
|
| 8 |
+
access-pattern DMAs and lost ~8 us: one completion signal per 8 MB weight), b2 is one
|
| 9 |
+
partition-broadcast access-pattern load [P, K];
|
| 10 |
+
* hardware sizes from ``nl.tile_size`` instead of literals;
|
| 11 |
+
* exact-multiple shapes asserted up front instead of ``min()`` remainder handling.
|
| 12 |
+
|
| 13 |
+
Prologue order (v3/v8/v10): b1 (Scalar HW-DGE queue) -> x(rb0) + Tensor-engine transposes
|
| 14 |
+
-> W1 -> x(rb1..) + DMA transposes -> W2. INFERENCE ONLY: no autograd rule.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
"""
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
|
|
|
|
|
|
| 19 |
import torch
|
| 20 |
from torch_neuronx import nki_op
|
| 21 |
|
|
|
|
| 23 |
import nki.isa as nisa
|
| 24 |
import nki.language as nl
|
| 25 |
|
| 26 |
+
P = nl.tile_size.pmax # partitions / stationary width (128)
|
| 27 |
+
FMOV = nl.tile_size.gemm_moving_fmax # moving free-axis limit = one PSUM bank of fp32 (512)
|
|
|
|
|
|
|
| 28 |
_BF16 = nl.bfloat16
|
| 29 |
|
| 30 |
#: Logical-to-physical NeuronCore degree. Must match the shipping shard's.
|
| 31 |
LNC = 2
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
def _kernel_assert(condition: bool, error_text: str):
|
|
|
|
| 35 |
assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
|
| 36 |
|
| 37 |
|
| 38 |
+
def _load_rows(x_hbm, r0, n_rs, K):
|
| 39 |
+
"""x[r0 : r0 + n_rs*P, :] into [P, n_rs, K], one DMA per row chunk so consumers start early."""
|
| 40 |
+
x_sb = nl.ndarray((P, n_rs, K), dtype=_BF16, buffer=nl.sbuf)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
for rs in range(n_rs):
|
| 42 |
+
nisa.dma_copy(dst=x_sb[:, rs, :], src=x_hbm[r0 + rs * P:r0 + (rs + 1) * P, :])
|
| 43 |
+
return x_sb
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
@nki.jit
|
| 47 |
def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
|
| 48 |
+
"""Args: x [R, K] bf16, w1t [K, H] bf16 (= fc1.weight.T), b1 [P, H/P] fp32 (pack_b1),
|
| 49 |
+
w2t [H, K] bf16 (= fc2.weight.T), b2 [K] fp32. Returns [R, K] bf16."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
R, K = x_hbm.shape
|
| 51 |
+
H = w1t_hbm.shape[1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
rows_per = R // LNC
|
| 53 |
+
_kernel_assert(tuple(w1t_hbm.shape) == (K, H) and tuple(w2t_hbm.shape) == (H, K),
|
| 54 |
+
f"expected w1t [K, H] and w2t [H, K], got {w1t_hbm.shape}, {w2t_hbm.shape}")
|
| 55 |
+
_kernel_assert(tuple(b1_hbm.shape) == (P, H // P), f"b1 must be pack_b1(b1): [{P}, {H // P}]")
|
| 56 |
+
_kernel_assert(K % FMOV == 0 and H % P == 0 and R % (LNC * FMOV) == 0,
|
| 57 |
+
f"needs K % {FMOV} == 0, H % {P} == 0, R % {LNC * FMOV} == 0; got R={R} K={K} H={H}")
|
| 58 |
+
n_k, n_h, n_ko, n_rs, n_rb = K // P, H // P, K // FMOV, FMOV // P, rows_per // FMOV
|
| 59 |
+
|
| 60 |
+
r_base = nl.program_id(0) * rows_per
|
| 61 |
out_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
|
| 62 |
|
| 63 |
+
# b1 on the Scalar engine's HW-DGE queue, so gelu's wait doesn't count W1/W2 traffic.
|
| 64 |
+
b1 = nl.ndarray((P, n_h), dtype=nl.float32, buffer=nl.sbuf)
|
| 65 |
+
nisa.dma_copy(dst=b1, src=b1_hbm, dge_mode=nisa.dge_mode.hwdge, engine=nisa.engine.scalar)
|
| 66 |
|
| 67 |
+
# x for rb0, transposed on the (idle) Tensor engine: xt[rb][:, kt, :] is x.T's K tile kt.
|
| 68 |
+
xt = []
|
| 69 |
+
for _ in range(n_rb):
|
| 70 |
+
xt.append(nl.ndarray((P, n_k, FMOV), dtype=_BF16, buffer=nl.sbuf))
|
| 71 |
+
x0 = _load_rows(x_hbm, r_base, n_rs, K)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
for kt in range(n_k):
|
| 73 |
+
tp = nl.ndarray((P, FMOV), dtype=_BF16, buffer=nl.psum)
|
| 74 |
+
for rs in range(n_rs):
|
| 75 |
+
nisa.nc_transpose(dst=tp[:, rs * P:(rs + 1) * P], data=x0[:, rs, kt * P:(kt + 1) * P],
|
| 76 |
+
engine=nisa.engine.tensor)
|
| 77 |
+
nisa.tensor_copy(dst=xt[0][:, kt, :], src=tp)
|
| 78 |
+
|
| 79 |
+
# W1 as [P, n_k, H]: w1[p, kt, h] = w1t[kt*P + p, h]. One DMA per K tile.
|
| 80 |
+
w1 = nl.ndarray((P, n_k, H), dtype=_BF16, buffer=nl.sbuf)
|
| 81 |
+
for kt in range(n_k):
|
| 82 |
+
nisa.dma_copy(dst=w1[:, kt, :], src=w1t_hbm[kt * P:(kt + 1) * P, :])
|
| 83 |
+
|
| 84 |
+
# Later row blocks' x via DMA transposes, streaming under rb0's matmuls.
|
| 85 |
+
for rb in range(1, n_rb):
|
| 86 |
+
xr = _load_rows(x_hbm, r_base + rb * FMOV, n_rs, K)
|
| 87 |
+
for rs in range(n_rs):
|
| 88 |
+
for kt in range(n_k):
|
| 89 |
+
nisa.dma_transpose(dst=xt[rb][:, kt, rs * P:(rs + 1) * P],
|
| 90 |
+
src=xr[:, rs, kt * P:(kt + 1) * P])
|
| 91 |
+
|
| 92 |
+
# W2 as [P, n_h, K]: w2[p, ht, k] = w2t[ht*P + p, k]. One DMA per H tile, so fc2 can
|
| 93 |
+
# start on the first strips (a single 8 MB DMA signals completion only at the end).
|
| 94 |
+
w2 = nl.ndarray((P, n_h, K), dtype=_BF16, buffer=nl.sbuf)
|
| 95 |
for ht in range(n_h):
|
| 96 |
+
nisa.dma_copy(dst=w2[:, ht, :], src=w2t_hbm[ht * P:(ht + 1) * P, :])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
for rb in range(n_rb):
|
| 99 |
+
r_abs = r_base + rb * FMOV
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
+
# fc1 + gelu: g[:, ht, :] = gelu(w1[:, :, ht].T @ x.T + b1[:, ht]), kept in SBUF.
|
| 102 |
+
g = nl.ndarray((P, n_h, FMOV), dtype=_BF16, buffer=nl.sbuf)
|
|
|
|
|
|
|
|
|
|
| 103 |
for ht in range(n_h):
|
| 104 |
+
# Declared per ht so the PSUM ring never accumulates onto a stale value.
|
| 105 |
+
y1 = nl.ndarray((P, FMOV), dtype=nl.float32, buffer=nl.psum)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
for kt in nl.affine_range(n_k):
|
| 107 |
+
nisa.nc_matmul(dst=y1, stationary=w1[:, kt, ht * P:(ht + 1) * P], moving=xt[rb][:, kt, :])
|
| 108 |
+
nisa.activation(dst=g[:, ht, :], op=nl.gelu, data=y1, bias=b1[:, ht:ht + 1])
|
| 109 |
+
|
| 110 |
+
# fc2 + b2: out[rows, ko chunk] = sum_ht g[:, ht, rows].T @ w2[:, ht, chunk] + b2.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
for ko in range(n_ko):
|
| 112 |
+
k0 = ko * FMOV
|
| 113 |
+
acc = []
|
|
|
|
| 114 |
for _ in range(n_rs):
|
| 115 |
+
acc.append(nl.ndarray((P, FMOV), dtype=nl.float32, buffer=nl.psum))
|
|
|
|
|
|
|
| 116 |
for ht in nl.affine_range(n_h):
|
|
|
|
|
|
|
| 117 |
for rs in range(n_rs):
|
| 118 |
+
nisa.nc_matmul(dst=acc[rs], stationary=g[:, ht, rs * P:(rs + 1) * P],
|
| 119 |
+
moving=w2[:, ht, k0:k0 + FMOV])
|
| 120 |
+
# b2 chunk broadcast across partitions (stride 0), loaded per (rb, ko) as in v10.
|
| 121 |
+
b2 = nl.ndarray((P, FMOV), dtype=nl.float32, buffer=nl.sbuf)
|
| 122 |
+
nisa.dma_copy(dst=b2, src=b2_hbm.ap(pattern=[[0, P], [1, FMOV]], offset=k0))
|
| 123 |
+
o = nl.ndarray((P, n_rs, FMOV), dtype=_BF16, buffer=nl.sbuf)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
for rs in range(n_rs):
|
| 125 |
+
nisa.tensor_tensor(dst=o[:, rs, :], data1=acc[rs], data2=b2, op=nl.add)
|
| 126 |
+
nisa.dma_copy(dst=out_hbm[r_abs + rs * P:r_abs + (rs + 1) * P, k0:k0 + FMOV], src=o[:, rs, :])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
return out_hbm
|
| 129 |
|
| 130 |
|
| 131 |
+
def pack_b1(b1: torch.Tensor) -> torch.Tensor:
|
| 132 |
+
"""Pack fc1.bias [H] fp32 into [P, H/P] so b1p[p, ht] == b1[ht*P + p]."""
|
| 133 |
+
return b1.reshape(-1, P).T.contiguous()
|
| 134 |
+
|
| 135 |
+
|
| 136 |
def _no_grad(*tensors) -> None:
|
| 137 |
"""Refuse gradient-tracking inputs -- this op registers no autograd rule."""
|
| 138 |
for t in tensors:
|
| 139 |
if t is not None and t.requires_grad:
|
| 140 |
+
raise RuntimeError("nki_mlp_k4_xp registers no autograd rule (INFERENCE ONLY). "
|
| 141 |
+
"Use under torch.no_grad()/eval().")
|
|
|
|
|
|
|
| 142 |
|
| 143 |
|
| 144 |
@nki_op("vjepa2::nki_mlp_k4_xp", mutates_args={})
|
| 145 |
+
def nki_mlp_k4_xp(x: torch.Tensor, w1t: torch.Tensor, b1: torch.Tensor,
|
| 146 |
+
w2t: torch.Tensor, b2: torch.Tensor) -> torch.Tensor:
|
| 147 |
+
"""Fused MLP on LNC=2. x [R, K] bf16, w1t [K, H] bf16, b1 [H] or pack_b1(b1) [P, H/P] fp32,
|
| 148 |
+
w2t [H, K] bf16, b2 [K] fp32. Returns [R, K] bf16."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
_no_grad(x, w1t, b1, w2t, b2)
|
| 150 |
+
if b1.dim() == 1: # accept the pre-v1.2 [H] bias; NeuronVJEPA2Encoder passes it pre-packed
|
| 151 |
+
b1 = pack_b1(b1)
|
| 152 |
return _mlp_k4_xp_kernel[LNC](x, w1t, b1, w2t, b2)
|
| 153 |
|
| 154 |
|
| 155 |
+
#: Export alias, so callers using ``mlp`` (e.g. layers.py) pick up this op unchanged.
|
|
|
|
| 156 |
mlp = nki_mlp_k4_xp
|
build/torch-neuron/nki_kernels/res_layernorm.py
CHANGED
|
@@ -19,9 +19,15 @@ 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
|
|
@@ -54,9 +60,6 @@ _BF16 = nl.bfloat16
|
|
| 54 |
#: Logical-to-physical NeuronCore degree. Rows are split evenly across LNC cores.
|
| 55 |
LNC = 2
|
| 56 |
|
| 57 |
-
#: Ring depth: number of rotating SBUF tile sets that overlap load with compute.
|
| 58 |
-
_RING = 4
|
| 59 |
-
|
| 60 |
|
| 61 |
def _kernel_assert(condition: bool, error_text: str):
|
| 62 |
"""Assert with an NKI-formatted error message."""
|
|
@@ -68,8 +71,8 @@ def _res_layernorm_fold_kernel(a_hbm, b_hbm, eps):
|
|
| 68 |
"""Emit the fold kernel body: ``(a + sum(b), LN_no_affine(a + sum(b)))``, token-major.
|
| 69 |
|
| 70 |
gamma/beta live in the consumers' packed weights, so this kernel takes no affine
|
| 71 |
-
parameters. Rows are sharded across ``LNC`` cores;
|
| 72 |
-
|
| 73 |
normalized result is cast to bf16 on write.
|
| 74 |
|
| 75 |
Args:
|
|
@@ -97,8 +100,7 @@ def _res_layernorm_fold_kernel(a_hbm, b_hbm, eps):
|
|
| 97 |
|
| 98 |
rows_per = R // LNC
|
| 99 |
n_t = rows_per // _P_MAX
|
| 100 |
-
|
| 101 |
-
r_base = pid * rows_per
|
| 102 |
|
| 103 |
x_new_hbm = nl.ndarray((R, H), dtype=nl.float32, buffer=nl.shared_hbm)
|
| 104 |
z_hbm = nl.ndarray((R, H), dtype=_BF16, buffer=nl.shared_hbm)
|
|
@@ -106,69 +108,62 @@ def _res_layernorm_fold_kernel(a_hbm, b_hbm, eps):
|
|
| 106 |
inv_h = 1.0 / float(H)
|
| 107 |
neg_inv_h = -inv_h
|
| 108 |
|
| 109 |
-
#
|
| 110 |
-
a_sb =
|
| 111 |
-
b_sb =
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
s_sb.append(nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf))
|
| 121 |
-
z_sb.append(nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf))
|
| 122 |
|
| 123 |
# Scratch destination for the reduce activations (its full tile is discarded; only
|
| 124 |
# the reduce result is consumed).
|
| 125 |
sink = nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf)
|
| 126 |
|
| 127 |
-
# Per-lane
|
| 128 |
-
acc = nl.ndarray((_P_MAX,
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
|
|
|
| 133 |
|
| 134 |
for t in range(n_t):
|
| 135 |
-
k = t % _RING
|
| 136 |
r0 = r_base + t * _P_MAX
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
nisa.dma_copy(dst=a_sb[k], src=a_hbm[r0:r0 + _P_MAX, 0:H])
|
| 140 |
-
for j in range(n_b):
|
| 141 |
-
nisa.dma_copy(dst=b_sb[k][j], src=b_hbm[j, r0:r0 + _P_MAX, 0:H])
|
| 142 |
|
| 143 |
# ---- x_new = a + sum_j b[j] in fp32, then write the residual stream out ----
|
| 144 |
-
nisa.tensor_tensor(dst=
|
| 145 |
for j in range(1, n_b):
|
| 146 |
-
nisa.tensor_tensor(dst=
|
| 147 |
|
| 148 |
-
nisa.dma_copy(dst=x_new_hbm[r0:r0 + _P_MAX, 0:H], src=
|
| 149 |
|
| 150 |
# ---- thinned stat chain: mean via shift-and-reduce, variance, rstd ----
|
| 151 |
-
nisa.tensor_scalar(dst=neg_shift, data=s_sb[
|
| 152 |
op0=nl.multiply, operand0=-1.0)
|
| 153 |
-
nisa.activation_reduce(dst=sink, op=nl.copy, data=
|
| 154 |
-
reduce_op=nl.add, reduce_res=acc,
|
| 155 |
-
bias=neg_shift, scale=1.0)
|
| 156 |
-
nisa.tensor_scalar(dst=neg_mu, data=acc, op0=nl.multiply, operand0=neg_inv_h,
|
| 157 |
-
op1=nl.add, operand1=neg_shift)
|
| 158 |
-
nisa.activation_reduce(dst=sink, op=nl.square, data=
|
| 159 |
-
reduce_op=nl.add, reduce_res=
|
| 160 |
-
nisa.tensor_scalar(dst=var, data=
|
| 161 |
op1=nl.add, operand1=eps)
|
| 162 |
-
nisa.activation(dst=rstd, op=nl.rsqrt, data=var)
|
| 163 |
|
| 164 |
# ---- one-op affine-free apply: z = (x_new + neg_mu) * rstd, bf16 cast on write.
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
op0=nl.add, operand0=neg_mu,
|
| 169 |
-
op1=nl.multiply, operand1=rstd)
|
| 170 |
|
| 171 |
-
nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:H], src=z_sb[
|
| 172 |
|
| 173 |
return x_new_hbm, z_hbm
|
| 174 |
|
|
|
|
| 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]; ~116 KB/partition at H=1024, B=2)
|
| 23 |
+
and all loads are issued before any compute, so tile ``t+1``'s loads never wait behind
|
| 24 |
+
tile ``t``'s stores. One DMA per tile and operand: a single whole-shard DMA would signal
|
| 25 |
+
completion only at the end.
|
| 26 |
+
* ``dge_mode.none`` on every DMA: the runtime generates the descriptors before execution,
|
| 27 |
+
so no engine spends time issuing them. The default (software DGE) issued each of the ~40
|
| 28 |
+
DMAs on GpSimd at ~0.75 us apiece, one after another. Hardware DGE on the Sync queue was
|
| 29 |
+
as fast standalone, but inside the encoder it delayed K2/K4's loads on that queue by
|
| 30 |
+
about as much as it saved.
|
| 31 |
* fp32 residual / bf16 branch: ``a`` (the residual stream) stays fp32 so ``x_new`` is
|
| 32 |
bitwise-invariant math and can be carried forward losslessly; the branch inputs and
|
| 33 |
the normalized output ``z`` are bf16, which is the precision boundary the downstream
|
|
|
|
| 60 |
#: Logical-to-physical NeuronCore degree. Rows are split evenly across LNC cores.
|
| 61 |
LNC = 2
|
| 62 |
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
def _kernel_assert(condition: bool, error_text: str):
|
| 65 |
"""Assert with an NKI-formatted error message."""
|
|
|
|
| 71 |
"""Emit the fold kernel body: ``(a + sum(b), LN_no_affine(a + sum(b)))``, token-major.
|
| 72 |
|
| 73 |
gamma/beta live in the consumers' packed weights, so this kernel takes no affine
|
| 74 |
+
parameters. Rows are sharded across ``LNC`` cores; every token tile is loaded up
|
| 75 |
+
front into its own SBUF slot, summed in fp32, normalized with the thinned stat chain, and the
|
| 76 |
normalized result is cast to bf16 on write.
|
| 77 |
|
| 78 |
Args:
|
|
|
|
| 100 |
|
| 101 |
rows_per = R // LNC
|
| 102 |
n_t = rows_per // _P_MAX
|
| 103 |
+
r_base = nl.program_id(0) * rows_per
|
|
|
|
| 104 |
|
| 105 |
x_new_hbm = nl.ndarray((R, H), dtype=nl.float32, buffer=nl.shared_hbm)
|
| 106 |
z_hbm = nl.ndarray((R, H), dtype=_BF16, buffer=nl.shared_hbm)
|
|
|
|
| 108 |
inv_h = 1.0 / float(H)
|
| 109 |
neg_inv_h = -inv_h
|
| 110 |
|
| 111 |
+
# One SBUF slot per tile; all loads issued before any compute or store (see module doc).
|
| 112 |
+
a_sb = nl.ndarray((_P_MAX, n_t, H), dtype=nl.float32, buffer=nl.sbuf)
|
| 113 |
+
b_sb = nl.ndarray((_P_MAX, n_b, n_t, H), dtype=_BF16, buffer=nl.sbuf)
|
| 114 |
+
for t in range(n_t):
|
| 115 |
+
r0 = r_base + t * _P_MAX
|
| 116 |
+
nisa.dma_copy(dst=a_sb[:, t, :], src=a_hbm[r0:r0 + _P_MAX, 0:H], dge_mode=nisa.dge_mode.none)
|
| 117 |
+
for j in range(n_b):
|
| 118 |
+
nisa.dma_copy(dst=b_sb[:, j, t, :], src=b_hbm[j, r0:r0 + _P_MAX, 0:H], dge_mode=nisa.dge_mode.none)
|
| 119 |
+
|
| 120 |
+
s_sb = nl.ndarray((_P_MAX, n_t, H), dtype=nl.float32, buffer=nl.sbuf)
|
| 121 |
+
z_sb = nl.ndarray((_P_MAX, n_t, H), dtype=_BF16, buffer=nl.sbuf)
|
|
|
|
|
|
|
| 122 |
|
| 123 |
# Scratch destination for the reduce activations (its full tile is discarded; only
|
| 124 |
# the reduce result is consumed).
|
| 125 |
sink = nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf)
|
| 126 |
|
| 127 |
+
# Per-lane statistics, one column per tile so tile t+1's chain doesn't wait on tile t's.
|
| 128 |
+
acc = nl.ndarray((_P_MAX, n_t), dtype=nl.float32, buffer=nl.sbuf)
|
| 129 |
+
acc2 = nl.ndarray((_P_MAX, n_t), dtype=nl.float32, buffer=nl.sbuf)
|
| 130 |
+
neg_shift = nl.ndarray((_P_MAX, n_t), dtype=nl.float32, buffer=nl.sbuf)
|
| 131 |
+
neg_mu = nl.ndarray((_P_MAX, n_t), dtype=nl.float32, buffer=nl.sbuf)
|
| 132 |
+
var = nl.ndarray((_P_MAX, n_t), dtype=nl.float32, buffer=nl.sbuf)
|
| 133 |
+
rstd = nl.ndarray((_P_MAX, n_t), dtype=nl.float32, buffer=nl.sbuf)
|
| 134 |
|
| 135 |
for t in range(n_t):
|
|
|
|
| 136 |
r0 = r_base + t * _P_MAX
|
| 137 |
+
s = s_sb[:, t, :]
|
| 138 |
+
c = slice(t, t + 1)
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
# ---- x_new = a + sum_j b[j] in fp32, then write the residual stream out ----
|
| 141 |
+
nisa.tensor_tensor(dst=s, data1=a_sb[:, t, :], data2=b_sb[:, 0, t, :], op=nl.add)
|
| 142 |
for j in range(1, n_b):
|
| 143 |
+
nisa.tensor_tensor(dst=s, data1=s, data2=b_sb[:, j, t, :], op=nl.add)
|
| 144 |
|
| 145 |
+
nisa.dma_copy(dst=x_new_hbm[r0:r0 + _P_MAX, 0:H], src=s, dge_mode=nisa.dge_mode.none)
|
| 146 |
|
| 147 |
# ---- thinned stat chain: mean via shift-and-reduce, variance, rstd ----
|
| 148 |
+
nisa.tensor_scalar(dst=neg_shift[:, c], data=s_sb[:, t, 0:1],
|
| 149 |
op0=nl.multiply, operand0=-1.0)
|
| 150 |
+
nisa.activation_reduce(dst=sink, op=nl.copy, data=s,
|
| 151 |
+
reduce_op=nl.add, reduce_res=acc[:, c],
|
| 152 |
+
bias=neg_shift[:, c], scale=1.0)
|
| 153 |
+
nisa.tensor_scalar(dst=neg_mu[:, c], data=acc[:, c], op0=nl.multiply, operand0=neg_inv_h,
|
| 154 |
+
op1=nl.add, operand1=neg_shift[:, c])
|
| 155 |
+
nisa.activation_reduce(dst=sink, op=nl.square, data=s,
|
| 156 |
+
reduce_op=nl.add, reduce_res=acc2[:, c], bias=neg_mu[:, c], scale=1.0)
|
| 157 |
+
nisa.tensor_scalar(dst=var[:, c], data=acc2[:, c], op0=nl.multiply, operand0=inv_h,
|
| 158 |
op1=nl.add, operand1=eps)
|
| 159 |
+
nisa.activation(dst=rstd[:, c], op=nl.rsqrt, data=var[:, c])
|
| 160 |
|
| 161 |
# ---- one-op affine-free apply: z = (x_new + neg_mu) * rstd, bf16 cast on write.
|
| 162 |
+
nisa.tensor_scalar(dst=z_sb[:, t, :], data=s,
|
| 163 |
+
op0=nl.add, operand0=neg_mu[:, c],
|
| 164 |
+
op1=nl.multiply, operand1=rstd[:, c])
|
|
|
|
|
|
|
| 165 |
|
| 166 |
+
nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:H], src=z_sb[:, t, :], dge_mode=nisa.dge_mode.none)
|
| 167 |
|
| 168 |
return x_new_hbm, z_hbm
|
| 169 |
|