File size: 15,130 Bytes
c855122
 
 
 
 
 
 
 
 
cabaa8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c855122
 
cabaa8f
 
 
 
 
 
 
 
c855122
 
 
 
 
cabaa8f
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cabaa8f
c855122
 
cabaa8f
 
 
 
 
c855122
 
cabaa8f
 
 
 
 
 
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cabaa8f
 
 
 
 
 
 
 
 
c855122
 
 
 
 
 
 
 
 
 
cabaa8f
c855122
 
 
 
 
 
 
 
 
 
 
 
 
cabaa8f
 
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cabaa8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c855122
cabaa8f
c855122
 
 
 
 
 
 
 
cabaa8f
 
 
 
 
 
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cabaa8f
c855122
 
cabaa8f
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cabaa8f
 
 
 
 
 
 
 
 
 
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cabaa8f
 
 
c855122
 
 
 
 
 
cabaa8f
 
c855122
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
"""K4 -- fused MLP (fc1 + gelu + fc2), x-prestage ("XP") in prologue mode.

WHAT IT COMPUTES
----------------
``out = gelu(x @ w1.T + b1) @ w2.T + b2``, with the rows sharded across LNC cores and
the weights pre-transposed to bf16. Returns ``[R, K]`` bf16.

WHY IT IS SHAPED THIS WAY
-------------------------
This is the effective shipping kernel: the ``b1pre`` MLP (fc1 bias staged in the prologue
so every gelu's bias is resident from t=0) with the x-prestage applied. The schedule was
rebuilt from the device profile (external contribution by jlongen-aws, PR #2 on
``jburtoft/vjepa2-neuron-kernels``). Four design choices carry the performance; the
instruction order and numerics are unchanged, so the output is bit-identical to the
previous version:

* x BEFORE weights: row-block 0's ``x`` load + transpose is issued first, then W1, then the
  later row blocks' ``x``, then W2. The first matmul no longer waits on ~16 MB of weights it
  does not need yet (the first fc1 matmul used to retire at ~57 us).
* b1 in ONE DMA on the Scalar engine's HW-DGE queue: ``b1`` is host-packed to ``[128, H/128]``
  (:func:`pack_b1`) and lands in a single ~16 KB DMA instead of ``n_h`` ``[128, 1]`` fp32 DMAs
  that trickled in and stalled each gelu (and, through its PSUM bank, the fc1 matmuls behind
  it). Routing it on ``engine=scalar`` HW-DGE keeps gelu's wait off the W1/W2 traffic.
* row-block 0's ``x`` is transposed on the TENSOR engine (``nc_transpose``) in the prologue,
  while the Tensor engine is otherwise dark in the pre-K4 seam. Later row blocks keep
  ``dma_transpose`` (they stream under rb0's matmuls).
* x prestaged in the prologue: EVERY row block's ``x`` staging is issued before the ``rb``
  loop, in row-block order, so a later block's serialised DMA chain runs inside the pre-K4
  seam instead of in the idle window at the rb boundary. Appending later blocks after rb=0's
  stages means rb=0's readiness cannot regress -- the first matmul cannot be delayed.

The x staging is emitted through a module-level emitter (``_stage_xt``) so the ``x`` load,
the ``nc_transpose`` and the ``dma_transpose`` each keep exactly one source site.

PSUM: fc1 uses 1 bank, fc2 uses up to 4 banks; the ``y1_psum`` / ``out_psums`` tiles are
declared inside their loops so the ring never accumulates onto a stale PSUM value. The b2
bias broadcast is kept (removing it regressed the sibling kernel by 23-30%). Weight DMAs stay
one-per-tile: merging them into a single access-pattern DMA lost ~8 us (one completion signal
per 8 MB weight).

This kernel is LNC-parametric (``LNC`` from ``NEURON_LOGICAL_NC_CONFIG``) and keeps the
``min()`` remainder handling, so it is NOT restricted to exact-multiple shapes. INFERENCE
ONLY: no autograd rule.
"""

from __future__ import annotations

import math
import os as _os

import torch
from torch_neuronx import nki_op

import nki
import nki.isa as nisa
import nki.language as nl

#: PE array partition limit, and the moving-operand free-axis limit.
_P_MAX = 128
_F_MOV_MAX = 512

_BF16 = nl.bfloat16

#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))

_INV_SQRT2 = 1.0 / math.sqrt(2.0)
_INV_SQRT_2PI = 1.0 / math.sqrt(2.0 * math.pi)


def _div_ceil(n: int, d: int) -> int:
    """Ceiling division (never write (n + d - 1) // d inline)."""
    return (n + d - 1) // d


def _kernel_assert(condition: bool, error_text: str):
    """Assert with an NKI-formatted error message."""
    assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"


def _stage_xt(x_hbm, r_abs, pr, n_k, K, rb_size, tensor_transpose):
    """Emit one row block's x staging and return its ``n_k`` transposed bf16 tiles.

    ``n_rs`` loads of ``[<=128, K]`` bf16, each followed by ``n_k`` transposes into the row
    block's ``xt_tiles``. The transpose runs on the Tensor engine (``nc_transpose``, via a PSUM
    tile) when ``tensor_transpose`` is set -- used for row block 0 while the Tensor engine is
    dark in the pre-K4 seam -- and on the DMA engine (``dma_transpose``) otherwise. Kept as a
    module-level emitter so the ``x`` load and each transpose keep a single source site.

    Args:
        x_hbm:            [R, K] @ HBM bf16, the MLP input.
        r_abs:            absolute starting row of this row block.
        pr:               number of rows in this row block (<= rb_size).
        n_k:              number of K tiles (fc1 contraction dimension tiles).
        K:                hidden-in dimension.
        rb_size:          row-block width (free axis of the transposed tiles).
        tensor_transpose: transpose on the Tensor engine (True) or the DMA engine (False).

    Returns:
        list of ``n_k`` transposed ``[128, rb_size]`` bf16 SBUF tiles.
    """
    n_rs = _div_ceil(pr, _P_MAX)
    xt_tiles = []
    for _ in range(n_k):
        xt_tiles.append(nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf))
    for rs in range(n_rs):
        rr0 = rs * _P_MAX
        prs = min(_P_MAX, pr - rr0)
        x_b16 = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
        nisa.dma_copy(
            dst=x_b16[0:prs, 0:K],
            src=x_hbm[r_abs + rr0:r_abs + rr0 + prs, 0:K],
        )
        for kt in range(n_k):
            k0 = kt * _P_MAX
            pk = min(_P_MAX, K - k0)
            if tensor_transpose:
                # Transpose on the (idle) Tensor engine via PSUM, then evict to the xt tile.
                tp = nl.ndarray((_P_MAX, _P_MAX), dtype=_BF16, buffer=nl.psum)
                nisa.nc_transpose(dst=tp[0:pk, 0:prs], data=x_b16[0:prs, k0:k0 + pk],
                                  engine=nisa.engine.tensor)
                nisa.tensor_copy(dst=xt_tiles[kt][0:pk, rr0:rr0 + prs], src=tp[0:pk, 0:prs])
            else:
                nisa.dma_transpose(dst=xt_tiles[kt][0:pk, rr0:rr0 + prs],
                                   src=x_b16[0:prs, k0:k0 + pk])
    return xt_tiles


@nki.jit
def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
    """gelu(x @ w1.T + b1) @ w2.T + b2, rows sharded, b1 AND both x blocks resident early.

    Args:
        x_hbm   (nl.ndarray): [R, K] @ HBM, **bfloat16** (kernel 1 emits bf16).
        w1t_hbm (nl.ndarray): [K, H] @ HBM, **bf16**, == fc1.weight.T.
        b1_hbm  (nl.ndarray): [128, H/128] @ HBM, float32, == pack_b1(fc1 bias).
        w2t_hbm (nl.ndarray): [H, K] @ HBM, **bf16**, == fc2.weight.T.
        b2_hbm  (nl.ndarray): [K]    @ HBM, float32.

    Returns:
        nl.ndarray: [R, K] @ HBM, **bfloat16**.
    """
    R, K = x_hbm.shape
    K_w1t, H = w1t_hbm.shape
    H_w2t, K_w2t = w2t_hbm.shape
    _kernel_assert(R >= 1 and K >= 1 and H >= 1, f"R({R}) K({K}) H({H}) must be >= 1")
    _kernel_assert(K_w1t == K, f"w1t in-dim {K_w1t} != K {K}; expected w1t = w1.T of shape [K, H]")
    _kernel_assert(K_w2t == K, f"w2t out-dim {K_w2t} != K {K}; expected w2t = w2.T of shape [H, K]")
    _kernel_assert(H_w2t == H, f"w2t in-dim {H_w2t} != H {H}")
    _kernel_assert(H % _P_MAX == 0, f"H={H} must be a multiple of {_P_MAX} for the packed b1 layout")
    _kernel_assert(tuple(b1_hbm.shape) == (_P_MAX, H // _P_MAX),
                   f"b1 must be pack_b1(fc1 bias): [{_P_MAX}, {H // _P_MAX}], got {tuple(b1_hbm.shape)}")
    _kernel_assert(
        R % LNC == 0,
        f"the {LNC}-core kernel needs R divisible by {LNC}, got R={R}. "
        f"program_id is a symbolic value, so a partial final row block cannot be "
        f"clamped; use the one-core pre-transposed kernel for uneven R",
    )

    rows_per = R // LNC
    pid = nl.program_id(0)
    r_base = pid * rows_per

    out_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)

    n_k = _div_ceil(K, _P_MAX)   # K tiles (fc1 contraction)
    n_h = _div_ceil(H, _P_MAX)   # H tiles (fc1 out-rows / fc2 contraction)

    # b1 FIRST, in ONE DMA on the Scalar engine's HW-DGE queue: the packed [128, H/128] bias
    # lands in a single ~16 KB transfer so every gelu's bias is resident long before the first
    # matmul retires, and gelu's wait no longer counts the W1/W2 traffic. b1_sb[:, ht] is the
    # bias column for H tile ht (b1_sb[p, ht] == fc1_bias[ht*128 + p]).
    b1_sb = nl.ndarray((_P_MAX, n_h), dtype=nl.float32, buffer=nl.sbuf)
    nisa.dma_copy(dst=b1_sb, src=b1_hbm, dge_mode=nisa.dge_mode.hwdge, engine=nisa.engine.scalar)

    # Main fused loop geometry (needed to size the x prestage).
    rb_size = _F_MOV_MAX
    n_rb = _div_ceil(rows_per, rb_size)
    n_ko = _div_ceil(K, _F_MOV_MAX)

    # x BEFORE weights: stage row block 0's x first (Tensor-engine transpose, filling the dark
    # pre-K4 seam), then W1, then later row blocks' x (DMA transpose, streaming under rb0's
    # matmuls), then W2. Appending later blocks after rb=0's stages leaves rb=0's readiness fixed.
    xt_sets = [None] * n_rb
    xt_sets[0] = _stage_xt(x_hbm, r_base, min(rb_size, rows_per), n_k, K, rb_size,
                           tensor_transpose=True)

    # W1 staging (pre-transposed bf16, one DMA per K tile).
    w1t_tiles = []
    for kt in range(n_k):
        k0 = kt * _P_MAX
        pk = min(_P_MAX, K - k0)
        t = nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf)
        nisa.dma_copy(dst=t[0:pk, :], src=w1t_hbm[k0:k0 + pk, :])
        w1t_tiles.append(t)

    # Later row blocks' x via DMA transpose, under rb0's matmuls.
    for rb_pre in range(1, n_rb):
        r_pre = rb_pre * rb_size
        xt_sets[rb_pre] = _stage_xt(x_hbm, r_base + r_pre, min(rb_size, rows_per - r_pre),
                                    n_k, K, rb_size, tensor_transpose=False)

    # W2 staging (one DMA per H tile, so fc2 can start on the first strips).
    w2t_tiles = []
    for ht in range(n_h):
        h0 = ht * _P_MAX
        ph = min(_P_MAX, H - h0)
        t = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
        nisa.dma_copy(dst=t[0:ph, :], src=w2t_hbm[h0:h0 + ph, :])
        w2t_tiles.append(t)

    for rb in range(n_rb):
        r_local = rb * rb_size
        pr = min(rb_size, rows_per - r_local)
        n_rs = _div_ceil(pr, _P_MAX)
        r_abs = r_base + r_local

        # x_T bf16 tiles: consume the prologue-staged set for this row block.
        xt_tiles = xt_sets[rb]

        # ---- fc1: gt_tiles[ht] = gelu(w1 @ x.T + b1), bf16, SBUF-resident ----
        gt_tiles = []
        for ht in range(n_h):
            h0 = ht * _P_MAX
            ph = min(_P_MAX, H - h0)
            # Declared INSIDE the ht loop, so the ring never reuses a written PSUM tile.
            # nc_matmul's default accumulate=None auto-detects "previously written" and
            # would accumulate onto a stale value.
            y1_psum = nl.ndarray((_P_MAX, rb_size), dtype=nl.float32, buffer=nl.psum)
            for kt in nl.affine_range(n_k):
                k0 = kt * _P_MAX
                pk = min(_P_MAX, K - k0)
                nisa.nc_matmul(
                    dst=y1_psum[0:ph, 0:pr],
                    stationary=w1t_tiles[kt][0:pk, h0:h0 + ph],
                    moving=xt_tiles[kt][0:pk, 0:pr],
                )
            # b1 comes from the prologue-resident packed tile, column ht.
            gt = nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf)
            nisa.activation(dst=gt[0:ph, 0:pr], op=nl.gelu,
                            data=y1_psum[0:ph, 0:pr], bias=b1_sb[0:ph, ht:ht + 1])
            gt_tiles.append(gt)

        # ---- fc2: out[r,k] = sum_h g[r,h] * w2[k,h] + b2[k] ----
        for ko in range(n_ko):
            ko0 = ko * _F_MOV_MAX
            nk = min(_F_MOV_MAX, K - ko0)
            out_psums = []
            for _ in range(n_rs):
                out_psums.append(
                    nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
                )
            for ht in nl.affine_range(n_h):
                h0 = ht * _P_MAX
                ph = min(_P_MAX, H - h0)
                for rs in range(n_rs):
                    rr0 = rs * _P_MAX
                    prs = min(_P_MAX, pr - rr0)
                    nisa.nc_matmul(
                        dst=out_psums[rs][0:prs, 0:nk],
                        stationary=gt_tiles[ht][0:ph, rr0:rr0 + prs],
                        moving=w2t_tiles[ht][0:ph, ko0:ko0 + nk],
                    )
            # b2 broadcast, hoisted out of the rs loop (removing the broadcast itself
            # cost 23% and 30% in two separate attempts).
            b2_bc = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.sbuf)
            nisa.dma_copy(dst=b2_bc[0:_P_MAX, 0:nk],
                          src=b2_hbm.ap(pattern=[[0, _P_MAX], [1, nk]], offset=ko0))
            for rs in range(n_rs):
                rr0 = rs * _P_MAX
                prs = min(_P_MAX, pr - rr0)
                out_sb = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=_BF16, buffer=nl.sbuf)
                nisa.tensor_tensor(dst=out_sb[0:prs, 0:nk],
                                   data1=out_psums[rs][0:prs, 0:nk],
                                   data2=b2_bc[0:prs, 0:nk],
                                   op=nl.add)
                nisa.dma_copy(
                    dst=out_hbm[r_abs + rr0:r_abs + rr0 + prs, ko0:ko0 + nk],
                    src=out_sb[0:prs, 0:nk],
                )

    return out_hbm


def pack_b1(b1: torch.Tensor) -> torch.Tensor:
    """Pack fc1 bias ``[H]`` fp32 into ``[128, H/128]`` so ``b1p[p, ht] == b1[ht*128 + p]``.

    Lets the kernel load the whole bias in one DMA instead of ``H/128`` column DMAs. ``H`` must
    be a multiple of 128.
    """
    H = b1.shape[0]
    assert H % _P_MAX == 0, f"pack_b1 needs H % {_P_MAX} == 0, got H={H}"
    return b1.reshape(-1, _P_MAX).t().contiguous()


def _no_grad(*tensors) -> None:
    """Refuse gradient-tracking inputs -- this op registers no autograd rule."""
    for t in tensors:
        if t is not None and t.requires_grad:
            raise RuntimeError(
                "nki_mlp_k4_xp registers no autograd rule (INFERENCE ONLY). "
                "Use under torch.no_grad()/eval()."
            )


@nki_op("vit_enc::nki_mlp_k4_xp", mutates_args={})
def nki_mlp_k4_xp(
    x: torch.Tensor,
    w1t: torch.Tensor,
    b1: torch.Tensor,
    w2t: torch.Tensor,
    b2: torch.Tensor,
) -> torch.Tensor:
    """Fused MLP on LNC=2, pre-transposed bf16 weights, x prestaged in the prologue.

    Args:
        x   : [R, K] **bf16** on 'neuron'. ``R`` must be divisible by :data:`LNC`.
        w1t : [K, H] bf16 == fc1.weight.T.
        b1  : fc1 bias, either ``[H]`` float32 (packed on the fly) or pre-packed
              ``[128, H/128]`` from :func:`pack_b1`.
        w2t : [H, K] bf16 == fc2.weight.T.   b2 : [K] float32.

    Returns:
        [R, K] bf16.
    """
    _no_grad(x, w1t, b1, w2t, b2)
    if b1.dim() == 1:                       # accept the pre-pack [H] bias; pack it on the fly
        b1 = pack_b1(b1)
    return _mlp_k4_xp_kernel[LNC](x, w1t, b1, w2t, b2)


#: Requested export alias. Same object as the incumbent-named nki_op, so the runtime
#: op-name string ("vit_enc::nki_mlp_k4_xp") is unchanged whichever name is used.
mlp = nki_mlp_k4_xp