jburtoft commited on
Commit
cabaa8f
·
1 Parent(s): c855122

v1.1: K4 (mlp/mlp_res_ln) + K1/K3 (res_layernorm) DMA/engine-schedule rework

Browse files

Bitwise-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
- p["b1"] = (_z(_f(bw.fc1_b), w1.shape[0]) + w1 @ t2).contiguous()
 
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: it is the ``b1pre`` MLP (fc1 bias staged in the
11
- prologue so every gelu's bias is resident from t=0) with the x-prestage applied. Two
12
- design choices carry the performance:
13
-
14
- * b1 first: the 32 per-block ``b1`` bias tiles are DMA'd before the 16 MB of weight
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
@@ -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`` DMA transposes into
76
- the row block's ``xt_tiles``. Kept as a module-level emitter so the ``x`` load and
77
- the dma_transpose each map to a single source site regardless of where it is called.
 
 
78
 
79
  Args:
80
- x_hbm: [R, K] @ HBM bf16, the MLP input.
81
- r_abs: absolute starting row of this row block.
82
- pr: number of rows in this row block (<= rb_size).
83
- n_k: number of K tiles (fc1 contraction dimension tiles).
84
- K: hidden-in dimension.
85
- rb_size: row-block width (free axis of the transposed tiles).
 
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
- nisa.dma_transpose(dst=xt_tiles[kt][0:pk, rr0:rr0 + prs],
106
- src=x_b16[0:prs, k0:k0 + pk])
 
 
 
 
 
 
 
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] @ HBM, float32.
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, before the 16 MB of weight DMA: 32 [128,1] tiles issue at t=0 and land in
148
- # ~1 us, so every gelu's bias is resident long before the first matmul retires.
149
- b1_tiles = []
150
- for ht in range(n_h):
151
- h0 = ht * _P_MAX
152
- ph = min(_P_MAX, H - h0)
153
- t = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
154
- nisa.dma_copy(dst=t[0:ph, 0:1], src=b1_hbm[h0:h0 + ph])
155
- b1_tiles.append(t)
 
 
 
 
 
 
 
 
 
156
 
157
- # Weight staging (pre-transposed bf16, one DMA per tile).
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=b1_tiles[ht][0:ph, 0:1])
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. b1 : [H] float32.
 
 
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``. fc2 is reordered to (row subtile rs, out block ko) so each 128-row tile's
4
- full [128, K] fc2 output lands in one fp32 SBUF row, then:
 
 
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
- --- original mlp.py notes ---
10
- K4 -- fused MLP (fc1 + gelu + fc2), x-prestage ("XP") in prologue mode.
11
-
12
- WHAT IT COMPUTES
13
- ----------------
14
- ``out = gelu(x @ w1.T + b1) @ w2.T + b2``, with the rows sharded across LNC cores and
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] @ HBM, float32.
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
- # b1 FIRST, before the 16 MB of weight DMA: 32 [128,1] tiles issue at t=0 and land in
161
- # ~1 us, so every gelu's bias is resident long before the first matmul retires.
162
- b1_tiles = []
163
- for ht in range(n_h):
164
- h0 = ht * _P_MAX
165
- ph = min(_P_MAX, H - h0)
166
- t = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
167
- nisa.dma_copy(dst=t[0:ph, 0:1], src=b1_hbm[h0:h0 + ph])
168
- b1_tiles.append(t)
 
 
 
 
 
 
 
 
169
 
170
- # Weight staging (pre-transposed bf16, one DMA per tile).
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=b1_tiles[ht][0:ph, 0:1])
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
- * Ring of depth 4 over the token tiles: four rotating sets of SBUF tiles let the DMA
23
- loads of tile ``t+1`` overlap the compute of tile ``t`` without a write-after-read
24
- hazard on the buffers.
 
 
 
 
 
 
 
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
- There are no ``engine=`` pins anywhere in this kernel; nothing engine-related to
36
- preserve. INFERENCE ONLY: no autograd rule is registered.
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; each token tile is loaded through
74
- a depth-4 ring, summed in fp32, normalized with the thinned stat chain, and the
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
- # Ring buffers: _RING rotating sets of (a tile, B branch tiles, sum tile, z tile).
112
- a_sb = []
113
- b_sb = []
114
- s_sb = []
115
- z_sb = []
116
- for _i in range(_RING):
117
- a_sb.append(nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf))
118
- _slot = []
119
- for _j in range(n_b):
120
- _slot.append(nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf))
121
- b_sb.append(_slot)
122
- s_sb.append(nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf))
123
- z_sb.append(nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf))
 
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 [128, 1] statistics.
130
- acc = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
131
- neg_shift = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
132
- neg_mu = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
133
- var = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
134
- rstd = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
 
135
 
136
  for t in range(n_t):
137
- k = t % _RING
138
  r0 = r_base + t * _P_MAX
139
-
140
- # ---- load residual + branches for this tile ----
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=s_sb[k], data1=a_sb[k], data2=b_sb[k][0], op=nl.add)
147
  for j in range(1, n_b):
148
- nisa.tensor_tensor(dst=s_sb[k], data1=s_sb[k], data2=b_sb[k][j], op=nl.add)
149
 
150
- nisa.dma_copy(dst=x_new_hbm[r0:r0 + _P_MAX, 0:H], src=s_sb[k])
151
 
152
  # ---- thinned stat chain: mean via shift-and-reduce, variance, rstd ----
153
- nisa.tensor_scalar(dst=neg_shift, data=s_sb[k][0:_P_MAX, 0:1],
154
  op0=nl.multiply, operand0=-1.0)
155
- nisa.activation_reduce(dst=sink, op=nl.copy, data=s_sb[k],
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=s_sb[k],
161
- reduce_op=nl.add, reduce_res=acc, bias=neg_mu, scale=1.0)
162
- nisa.tensor_scalar(dst=var, data=acc, op0=nl.multiply, operand0=inv_h,
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 [128, 1] vectors; the fused two-op tensor_scalar
168
- # costs the same as one op.
169
- nisa.tensor_scalar(dst=z_sb[k], data=s_sb[k],
170
- op0=nl.add, operand0=neg_mu,
171
- op1=nl.multiply, operand1=rstd)
172
 
173
- nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:H], src=z_sb[k])
 
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