v1.2: packed-bias one-DMA for SwiGLU (swiglu_stream/_ln)
Browse filesSilu bias b1 and up bias bg host-packed to [128, I/128] and loaded in one DMA each on the
Scalar HW-DGE queue (same technique as v1.1 K4). Bitwise-identical to v1.1 at LNC=1 and LNC=2;
swiglu_stream -2.5% device cycles, swiglu_stream_ln within noise. GELU path unchanged.
- CHANGELOG.md +11 -0
- build/torch-neuron/encoder.py +2 -2
- build/torch-neuron/nki_kernels/swiglu_stream.py +25 -16
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
| 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`
|
|
|
|
| 1 |
# Changelog
|
| 2 |
|
| 3 |
+
## v1.2 (2026-10)
|
| 4 |
+
|
| 5 |
+
Applied the v1.1 packed-bias one-DMA technique to the SwiGLU FFN kernels (`swiglu_stream`,
|
| 6 |
+
`swiglu_stream_ln`). The silu bias `b1` and the up-projection bias `bg` are now host-packed to
|
| 7 |
+
`[128, I/128]` (`pack_b1`) and each loaded in a single DMA on the Scalar engine's HW-DGE queue,
|
| 8 |
+
replacing `I/128` `[128, 1]` column DMAs apiece. Output is **bitwise identical** to v1.1 at LNC=1 and
|
| 9 |
+
LNC=2 (`torch.equal`, verified on trn2.3xlarge). Device cycles (same stack, A/B): `swiglu_stream`
|
| 10 |
+
-2.5% (reproducible); `swiglu_stream_ln` within measurement noise (the LN epilogue dominates and the
|
| 11 |
+
bias DMAs already overlap compute). The wrappers still accept an unpacked `[I]` bias (packed on the
|
| 12 |
+
fly). Affects only the gated-MLP (SwiGLU) models; the GELU-MLP path is unchanged from v1.1.
|
| 13 |
+
|
| 14 |
## v1.1 (2026-10)
|
| 15 |
|
| 16 |
Reworked DMA/engine schedule for `mlp` (K4), `mlp_res_ln` (K4 + next LN) and `res_layernorm_fold`
|
build/torch-neuron/encoder.py
CHANGED
|
@@ -124,9 +124,9 @@ def pack_block(bw: BlockWeights, num_heads: int, rope: str, device) -> dict:
|
|
| 124 |
pi = lambda t: torch.cat([t, t.new_zeros(Ip - t.shape[0], *t.shape[1:])], 0)
|
| 125 |
p["swiglu"] = True
|
| 126 |
p["s_w1t"] = pi(gw * g2[None]).t().contiguous().to(torch.bfloat16) # gate (silu)
|
| 127 |
-
p["s_b1"] = pi(_z(_f(bw.gate_b), I) + gw @ t2).contiguous()
|
| 128 |
p["s_wgt"] = pi(uw * g2[None]).t().contiguous().to(torch.bfloat16) # up
|
| 129 |
-
p["s_bg"] = pi(_z(_f(bw.up_b), I) + uw @ t2).contiguous()
|
| 130 |
p["s_w2t"] = pi((dw * ls2[:, None]).t().contiguous()).contiguous().to(torch.bfloat16)
|
| 131 |
p["s_b2"] = (_z(_f(bw.down_b), H) * ls2).contiguous()
|
| 132 |
else: # GELU MLP
|
|
|
|
| 124 |
pi = lambda t: torch.cat([t, t.new_zeros(Ip - t.shape[0], *t.shape[1:])], 0)
|
| 125 |
p["swiglu"] = True
|
| 126 |
p["s_w1t"] = pi(gw * g2[None]).t().contiguous().to(torch.bfloat16) # gate (silu)
|
| 127 |
+
p["s_b1"] = _mlp_pack_b1(pi(_z(_f(bw.gate_b), I) + gw @ t2).contiguous()) # [128, Ip/128], one-DMA
|
| 128 |
p["s_wgt"] = pi(uw * g2[None]).t().contiguous().to(torch.bfloat16) # up
|
| 129 |
+
p["s_bg"] = _mlp_pack_b1(pi(_z(_f(bw.up_b), I) + uw @ t2).contiguous()) # [128, Ip/128], one-DMA
|
| 130 |
p["s_w2t"] = pi((dw * ls2[:, None]).t().contiguous()).contiguous().to(torch.bfloat16)
|
| 131 |
p["s_b2"] = (_z(_f(bw.down_b), H) * ls2).contiguous()
|
| 132 |
else: # GELU MLP
|
build/torch-neuron/nki_kernels/swiglu_stream.py
CHANGED
|
@@ -28,6 +28,8 @@ import nki
|
|
| 28 |
import nki.isa as nisa
|
| 29 |
import nki.language as nl
|
| 30 |
|
|
|
|
|
|
|
| 31 |
import os as _os
|
| 32 |
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 33 |
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
|
@@ -108,18 +110,17 @@ def _body(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, a_hbm, eps,
|
|
| 108 |
else:
|
| 109 |
out_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
|
| 110 |
|
| 111 |
-
# biases (tiny, resident)
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
bg_t.append(tg)
|
| 123 |
b2_bc = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf)
|
| 124 |
nisa.dma_copy(dst=b2_bc[0:_P_MAX, 0:K], src=b2_hbm.ap(pattern=[[0, _P_MAX], [1, K]], offset=0))
|
| 125 |
|
|
@@ -187,11 +188,11 @@ def _body(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, a_hbm, eps,
|
|
| 187 |
moving=xt[kt][0:pk, q0:q0 + rw])
|
| 188 |
sa = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.sbuf)
|
| 189 |
nisa.activation(dst=sa[0:ph, 0:rw], op=nl.silu, data=y1[0:ph, 0:rw],
|
| 190 |
-
bias=
|
| 191 |
g = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=_BF16, buffer=nl.sbuf)
|
| 192 |
# g = (yg + bg) * silu(y1 + b1) -- one fused op, PSUM operand
|
| 193 |
nisa.scalar_tensor_tensor(dst=g[0:ph, 0:rw], data=yg[0:ph, 0:rw],
|
| 194 |
-
op0=nl.add, operand0=
|
| 195 |
op1=nl.multiply, operand1=sa[0:ph, 0:rw])
|
| 196 |
g_t.append(g)
|
| 197 |
# ---- down-projection partial for this chunk, accumulated into acc (fp32 SBUF) ----
|
|
@@ -259,6 +260,14 @@ def _swiglu_stream_ln_kernel(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b
|
|
| 259 |
return _body(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, a_hbm, eps, S, CI, 1)
|
| 260 |
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
def _no_grad(*ts):
|
| 263 |
for t in ts:
|
| 264 |
if t is not None and t.requires_grad:
|
|
@@ -278,7 +287,7 @@ def nki_swiglu_stream(x: torch.Tensor, w1t: torch.Tensor, b1: torch.Tensor, wgt:
|
|
| 278 |
"""x [R,K] bf16; w1t/wgt [K,I] bf16; b1/bg [I] f32; w2t [I,K] bf16; b2 [K] f32 -> [R,K] bf16."""
|
| 279 |
_no_grad(x, w1t, b1, wgt, bg, w2t, b2)
|
| 280 |
S, CI = _plan_for(x, w1t)
|
| 281 |
-
return _swiglu_stream_kernel[LNC](x, w1t, b1, wgt, bg, w2t, b2, S, CI)
|
| 282 |
|
| 283 |
|
| 284 |
@nki_op("vit_enc::nki_swiglu_stream_ln", mutates_args={})
|
|
@@ -288,4 +297,4 @@ def nki_swiglu_stream_ln(x: torch.Tensor, w1t: torch.Tensor, b1: torch.Tensor, w
|
|
| 288 |
"""SwiGLU(x) + residual a, then affine-free LN. Returns (x_new [R,K] fp32, z [R,K] bf16)."""
|
| 289 |
_no_grad(x, w1t, b1, wgt, bg, w2t, b2, a)
|
| 290 |
S, CI = _plan_for(x, w1t)
|
| 291 |
-
return _swiglu_stream_ln_kernel[LNC](x, w1t, b1, wgt, bg, w2t, b2, a, eps, S, CI)
|
|
|
|
| 28 |
import nki.isa as nisa
|
| 29 |
import nki.language as nl
|
| 30 |
|
| 31 |
+
from .mlp import pack_b1
|
| 32 |
+
|
| 33 |
import os as _os
|
| 34 |
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 35 |
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
|
|
|
| 110 |
else:
|
| 111 |
out_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
|
| 112 |
|
| 113 |
+
# biases (tiny, resident). b1/bg are host-packed to [128, I/128] (pack_bias; b[p, ht] ==
|
| 114 |
+
# bias[ht*128 + p]) and each lands in ONE DMA on the Scalar HW-DGE queue -- replaces 2 * I/128
|
| 115 |
+
# [128, 1] DMAs (~54 per core at ViT-H+) that stalled the first silu/gate ops. b2 broadcast over
|
| 116 |
+
# partitions.
|
| 117 |
+
_kernel_assert(I % _P_MAX == 0, "I must be padded to a multiple of 128 (pack_bias)")
|
| 118 |
+
_kernel_assert(tuple(b1_hbm.shape) == (_P_MAX, n_h) and tuple(bg_hbm.shape) == (_P_MAX, n_h),
|
| 119 |
+
f"b1/bg must be pack_bias(...): [{_P_MAX}, {n_h}]")
|
| 120 |
+
b1_sb = nl.ndarray((_P_MAX, n_h), dtype=nl.float32, buffer=nl.sbuf)
|
| 121 |
+
nisa.dma_copy(dst=b1_sb, src=b1_hbm, dge_mode=nisa.dge_mode.hwdge, engine=nisa.engine.scalar)
|
| 122 |
+
bg_sb = nl.ndarray((_P_MAX, n_h), dtype=nl.float32, buffer=nl.sbuf)
|
| 123 |
+
nisa.dma_copy(dst=bg_sb, src=bg_hbm, dge_mode=nisa.dge_mode.hwdge, engine=nisa.engine.scalar)
|
|
|
|
| 124 |
b2_bc = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf)
|
| 125 |
nisa.dma_copy(dst=b2_bc[0:_P_MAX, 0:K], src=b2_hbm.ap(pattern=[[0, _P_MAX], [1, K]], offset=0))
|
| 126 |
|
|
|
|
| 188 |
moving=xt[kt][0:pk, q0:q0 + rw])
|
| 189 |
sa = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.sbuf)
|
| 190 |
nisa.activation(dst=sa[0:ph, 0:rw], op=nl.silu, data=y1[0:ph, 0:rw],
|
| 191 |
+
bias=b1_sb[0:ph, hg:hg + 1])
|
| 192 |
g = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=_BF16, buffer=nl.sbuf)
|
| 193 |
# g = (yg + bg) * silu(y1 + b1) -- one fused op, PSUM operand
|
| 194 |
nisa.scalar_tensor_tensor(dst=g[0:ph, 0:rw], data=yg[0:ph, 0:rw],
|
| 195 |
+
op0=nl.add, operand0=bg_sb[0:ph, hg:hg + 1],
|
| 196 |
op1=nl.multiply, operand1=sa[0:ph, 0:rw])
|
| 197 |
g_t.append(g)
|
| 198 |
# ---- down-projection partial for this chunk, accumulated into acc (fp32 SBUF) ----
|
|
|
|
| 260 |
return _body(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, a_hbm, eps, S, CI, 1)
|
| 261 |
|
| 262 |
|
| 263 |
+
def pack_bias(b: torch.Tensor) -> torch.Tensor:
|
| 264 |
+
"""Pack an fp32 bias ``[I]`` (I % 128 == 0) to ``[128, I/128]`` so ``b_p[p, t] == b[t*128 + p]``."""
|
| 265 |
+
if b.dim() == 2:
|
| 266 |
+
return b
|
| 267 |
+
assert b.shape[0] % _P_MAX == 0, f"pack_bias needs I % {_P_MAX} == 0, got {b.shape[0]}"
|
| 268 |
+
return b.reshape(-1, _P_MAX).t().contiguous()
|
| 269 |
+
|
| 270 |
+
|
| 271 |
def _no_grad(*ts):
|
| 272 |
for t in ts:
|
| 273 |
if t is not None and t.requires_grad:
|
|
|
|
| 287 |
"""x [R,K] bf16; w1t/wgt [K,I] bf16; b1/bg [I] f32; w2t [I,K] bf16; b2 [K] f32 -> [R,K] bf16."""
|
| 288 |
_no_grad(x, w1t, b1, wgt, bg, w2t, b2)
|
| 289 |
S, CI = _plan_for(x, w1t)
|
| 290 |
+
return _swiglu_stream_kernel[LNC](x, w1t, pack_bias(b1), wgt, pack_bias(bg), w2t, b2, S, CI)
|
| 291 |
|
| 292 |
|
| 293 |
@nki_op("vit_enc::nki_swiglu_stream_ln", mutates_args={})
|
|
|
|
| 297 |
"""SwiGLU(x) + residual a, then affine-free LN. Returns (x_new [R,K] fp32, z [R,K] bf16)."""
|
| 298 |
_no_grad(x, w1t, b1, wgt, bg, w2t, b2, a)
|
| 299 |
S, CI = _plan_for(x, w1t)
|
| 300 |
+
return _swiglu_stream_ln_kernel[LNC](x, w1t, pack_bias(b1), wgt, pack_bias(bg), w2t, b2, a, eps, S, CI)
|