File size: 14,025 Bytes
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90f3d2f
 
c855122
 
 
 
 
 
cabaa8f
c855122
3b1bffa
 
 
 
c855122
 
 
3b1bffa
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce0e45f
 
c855122
ce0e45f
 
c855122
 
 
 
 
 
cabaa8f
 
c855122
3b1bffa
 
 
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90f3d2f
 
 
 
c855122
 
3b1bffa
 
 
 
 
 
 
 
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a827ebd
 
c855122
 
 
 
 
 
 
 
 
 
 
ce0e45f
 
 
c855122
3b1bffa
 
c855122
 
 
 
 
 
 
 
3b1bffa
 
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90f3d2f
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
"""Model-agnostic ViT encoder loop on the NKI kernels.

A pre-norm transformer block

    x = x + ls1 * attn(LN1(x))          attn: q/k/v projections, RoPE on q/k, softmax, out proj
    x = x + ls2 * ffn(LN2(x))           ffn:  GELU MLP  or  SwiGLU

is described once per block as a :class:`BlockWeights` (plain fp32 tensors, any source), packed
once by :func:`pack_block` (LayerNorm affines and LayerScale folded into the weights), and run by
:class:`EncoderRunner` on a stacked ``[B*N, H]`` row buffer.

Kernel choice per block (all automatic):

* attention: LNC=1 -> one program over all head groups, with the residual add + LN2 in its
  epilogue; LNC=2 -> per-group programs whose partials the residual/LN kernel sums.
* FFN: GELU -> fused MLP; SwiGLU -> weight-streaming SwiGLU. Both also do the residual add and
  the NEXT block's LN1 in their epilogue, so the stand-alone residual/LN kernel runs only before
  block 0 (and after attention at LNC=2).

INFERENCE ONLY (no autograd). Constraints: hidden size % 128 == 0, head_dim in {64, 128},
tokens per image <= 2048 after padding to a multiple of 128 * LNC.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

import torch

import importlib as _il
import os as _osr
_PACK2 = _osr.environ.get("VIT_ENC_PACK2", "1") == "1"   # 2 images per producer pass (N <= 256, LNC=1)

_ab = _il.import_module(__package__ + ".nki_kernels.attention_block")
_ab1 = _il.import_module(__package__ + ".nki_kernels.attention_block_l1")
_ab1ln = _il.import_module(__package__ + ".nki_kernels.attention_block_l1_ln")
from .nki_kernels.attention_block import LNC
from .nki_kernels.mlp import nki_mlp_k4_xp
from .nki_kernels.mlp import pack_b1 as _mlp_pack_b1
from .nki_kernels.mlp_res_ln import nki_mlp_res_ln
import os as _osx
_MLP_FP8 = _osx.environ.get("VIT_ENC_MLP_FP8", "0") == "1"      # EXPERIMENTAL: FP8 e4m3 GELU MLP (see README)
if _MLP_FP8:
    _mf8 = _il.import_module(__package__ + ".nki_kernels.mlp_fp8")
from .nki_kernels.res_layernorm import nki_res_layernorm_fold
from .nki_kernels.swiglu_stream import nki_swiglu_stream, nki_swiglu_stream_ln

__all__ = ["BlockWeights", "pack_block", "rope_tables", "EncoderRunner", "LNC", "calibrate_fp8"]

_SBUF_BUDGET = 180 * 1024        # per-partition bytes the LNC=1 resident-weights decision allows
_SW_MIN_ROWS = 256               # below this many rows per core, SwiGLU runs in torch (measured faster)
_L1_MAX_N = 1280                 # LNC=1 all-group attention kernels verified up to this many tokens per
                                 # image; beyond it (e.g. 2048-token video clips) they exceed SBUF and
                                 # the per-group kernel + residual/LN kernel are used instead
_RESIDENT_MAX_N = 1280           # LNC=2 attention: weights staged once per launch only up to this many
                                 # tokens per image (2048 tokens + resident weights exceed SBUF)


@dataclass
class BlockWeights:
    """One transformer block, as plain fp32 CPU tensors in ``nn.Linear`` layout ([out, in])."""
    ln1_w: torch.Tensor
    ln1_b: torch.Tensor
    ln1_eps: float
    wq: torch.Tensor
    bq: Optional[torch.Tensor]
    wk: torch.Tensor
    bk: Optional[torch.Tensor]
    wv: torch.Tensor
    bv: Optional[torch.Tensor]
    wo: torch.Tensor
    bo: Optional[torch.Tensor]
    ln2_w: torch.Tensor
    ln2_b: torch.Tensor
    ln2_eps: float
    # GELU MLP: fc1 [I, H], fc2 [H, I]. SwiGLU: gate [I, H], up [I, H], down [H, I].
    fc1_w: Optional[torch.Tensor] = None
    fc1_b: Optional[torch.Tensor] = None
    fc2_w: Optional[torch.Tensor] = None
    fc2_b: Optional[torch.Tensor] = None
    gate_w: Optional[torch.Tensor] = None
    gate_b: Optional[torch.Tensor] = None
    up_w: Optional[torch.Tensor] = None
    up_b: Optional[torch.Tensor] = None
    down_w: Optional[torch.Tensor] = None
    down_b: Optional[torch.Tensor] = None
    ls1: Optional[torch.Tensor] = None       # LayerScale (None = 1)
    ls2: Optional[torch.Tensor] = None
    scale: Optional[float] = None            # attention scale (None = head_dim ** -0.5)


def _f(t):
    return None if t is None else t.detach().float().cpu()


def _z(t, n):
    return torch.zeros(n) if t is None else t


def pack_block(bw: BlockWeights, num_heads: int, rope: str, device) -> dict:
    """Fold and pack one block for the kernels. ``rope`` in {"half", "interleaved"}."""
    wq, wk, wv, wo = _f(bw.wq), _f(bw.wk), _f(bw.wv), _f(bw.wo)
    H = wq.shape[1]
    F_ = wq.shape[0]
    D = F_ // num_heads
    gsz = 512 // D                                   # heads per kernel program
    nh_p = ((num_heads + gsz - 1) // gsz) * gsz      # padded head count
    Fp = nh_p * D
    ngrp = nh_p // gsz
    g1, t1, g2, t2 = _f(bw.ln1_w), _f(bw.ln1_b), _f(bw.ln2_w), _f(bw.ln2_b)
    bq, bk, bv = _z(_f(bw.bq), F_), _z(_f(bw.bk), F_), _z(_f(bw.bv), F_)
    ls1 = _f(bw.ls1) if bw.ls1 is not None else torch.ones(H)
    ls2 = _f(bw.ls2) if bw.ls2 is not None else torch.ones(H)
    bo = _z(_f(bw.bo), H) * ls1
    wo = wo * ls1[:, None]
    padr = lambda w: torch.cat([w, w.new_zeros(Fp - w.shape[0], *w.shape[1:])], 0)
    p = {"D": D, "heads": num_heads, "nh_p": nh_p, "ngrp": ngrp, "H": H,
         "eps1": float(bw.ln1_eps), "eps2": float(bw.ln2_eps),
         "scale": float(bw.scale if bw.scale is not None else D ** -0.5)}
    p["wqk"], p["bqk"] = _ab.pack_weights(padr(wq * g1[None]), padr(wk * g1[None]),
                                          padr(bq + wq @ t1), padr(bk + wk @ t1), nh_p, D, rope)
    p["wvt"], p["bv"] = _ab.pack_v_weights(padr(wv * g1[None]), padr(bv + wv @ t1))
    p["wot"] = torch.cat([wo, wo.new_zeros(H, Fp - F_)], 1).t().contiguous().to(torch.bfloat16)
    p["bo"] = (bo / ngrp).reshape(1, H).contiguous().to(torch.bfloat16)      # per-group partials
    p["bo_full"] = bo.reshape(1, H).contiguous().to(torch.bfloat16)          # all-group kernel
    if bw.gate_w is not None:                                                # SwiGLU
        gw, uw, dw = _f(bw.gate_w), _f(bw.up_w), _f(bw.down_w)
        I = gw.shape[0]
        Ip = ((I + 127) // 128) * 128
        pi = lambda t: torch.cat([t, t.new_zeros(Ip - t.shape[0], *t.shape[1:])], 0)
        p["swiglu"] = True
        p["s_w1t"] = pi(gw * g2[None]).t().contiguous().to(torch.bfloat16)          # gate (silu)
        p["s_b1u"] = pi(_z(_f(bw.gate_b), I) + gw @ t2).contiguous()               # [Ip] for the torch fallback
        p["s_b1"] = _mlp_pack_b1(p["s_b1u"])                                       # [128, Ip/128], one-DMA
        p["s_wgt"] = pi(uw * g2[None]).t().contiguous().to(torch.bfloat16)          # up
        p["s_bgu"] = pi(_z(_f(bw.up_b), I) + uw @ t2).contiguous()                 # [Ip] for the torch fallback
        p["s_bg"] = _mlp_pack_b1(p["s_bgu"])                                       # [128, Ip/128], one-DMA
        p["s_w2t"] = pi((dw * ls2[:, None]).t().contiguous()).contiguous().to(torch.bfloat16)
        p["s_b2"] = (_z(_f(bw.down_b), H) * ls2).contiguous()
    else:                                                                     # GELU MLP
        w1, w2 = _f(bw.fc1_w), _f(bw.fc2_w)
        p["swiglu"] = False
        p["w1t"] = (w1 * g2[None]).t().contiguous().to(torch.bfloat16)
        # Pre-pack fc1 bias to [128, H_mlp/128] so K4/K4+LN load it in one DMA (see mlp.pack_b1).
        p["b1"] = _mlp_pack_b1((_z(_f(bw.fc1_b), w1.shape[0]) + w1 @ t2).contiguous())
        p["w2t"] = (w2 * ls2[:, None]).t().contiguous().to(torch.bfloat16)
        if _MLP_FP8:
            p["w1p8"], p["sw1"] = _mf8.pack_fp8_weight((w1 * g2[None]).t().contiguous())
            p["w2p8"], p["sw2"] = _mf8.pack_fp8_weight((w2 * ls2[:, None]).t().contiguous())
            p["g8"] = 1.0 / 32           # GELU-output scale; per-layer calibration recommended (calibrate_fp8)
        p["b2"] = (_z(_f(bw.fc2_b), H) * ls2).contiguous()
    return {k: (v.to(device) if torch.is_tensor(v) else v) for k, v in p.items()}


def rope_tables(cos: torch.Tensor, sin: torch.Tensor, n_prefix: int, n_pad: int, num_heads: int,
                D: int, rope: str, device) -> torch.Tensor:
    """Kernel RoPE tables for a ``[n_prefix + n_rot, ...]`` token layout padded to ``n_pad`` rows.

    ``cos``/``sin``: ``[n_rot, D]`` the model's own tables for the rotated tokens (prefix tokens
    such as CLS/registers and padding get cos=1, sin=0). For ``rope="interleaved"`` pass the
    SIGNED sin table (see ``pack_tables``).
    """
    C = torch.ones(n_pad, D)
    S = torch.zeros(n_pad, D)
    n_rot = cos.shape[0]
    C[n_prefix:n_prefix + n_rot] = cos.float().cpu()
    S[n_prefix:n_prefix + n_rot] = sin.float().cpu()
    gsz = 512 // D
    nh_p = ((num_heads + gsz - 1) // gsz) * gsz
    t = _ab.pack_tables(C, S, nh_p, D, rope)
    if _osr.environ.get("VIT_ENC_ROPE_BF16", "0") == "1":     # EXPERIMENTAL bf16 RoPE (tables in bf16)
        t = t.to(torch.bfloat16)
    return t.to(device)


def calibrate_fp8(runner, gelu_amax, headroom: float = 2.0):
    """EXPERIMENTAL (VIT_ENC_MLP_FP8=1). Set each layer's power-of-two GELU-output scale from the observed max |GELU
    output| per layer (list, one per block): scale = 2^floor(log2(128 / (amax * headroom))). trn2's e4m3 saturates at
    240; uncalibrated activations above that turn into inf/NaN."""
    import math
    for p, a in zip(runner.L, gelu_amax):
        p["g8"] = 2.0 ** math.floor(math.log2(128.0 / max(a * headroom, 1e-6)))


class EncoderRunner:
    """Runs packed blocks on a stacked ``[B*N, H]`` fp32 residual stream (rows of image b at b*N)."""

    def __init__(self, packed: list):
        self.L = packed
        p0 = packed[0]
        self.D, self.H, self.ngrp = p0["D"], p0["H"], p0["ngrp"]
        self.plan = []                                   # (group base, grid) per LNC=2 attention call
        g = 0
        while g < self.ngrp:
            gr = 2 if (LNC == 2 and self.ngrp - g >= 2) else 1
            self.plan.append((g, gr))
            g += gr

    def _res(self, mod, N):
        return int(mod.sbuf_estimate(self.H, self.ngrp * 512, N, self.D, True) <= _SBUF_BUDGET)

    def _all_group(self, N):
        return LNC == 1 and self.ngrp > 1 and N <= _L1_MAX_N

    def _attn_parts(self, z1, p, tab, n_valid, B):
        if self._all_group(z1.shape[0] // B):
            N = z1.shape[0] // B
            return _ab1.attention_block_l1(z1, p["wqk"], p["bqk"], tab, p["wvt"], p["bv"], p["wot"],
                                           p["bo_full"], p["scale"], self.D, n_valid, B, self._res(_ab1, N))
        outs = [_ab.attention_block(z1, p["wqk"], p["bqk"], tab, p["wvt"], p["bv"], p["wot"], p["bo"],
                                    p["scale"], self.D, n_valid, hb, gr, B,
                                    int(z1.shape[0] // B <= _RESIDENT_MAX_N),
                                    int(_PACK2 and B % 2 == 0 and 2 * (z1.shape[0] // B) <= 512))
                for hb, gr in self.plan]
        P = outs[0] if len(outs) == 1 else torch.cat(outs, 0)
        if P.shape[0] > 4:                                # residual/LN kernel takes <= 4 branches
            P = P.float().sum(0, keepdim=True).to(torch.bfloat16)
        return P

    def _ffn(self, z2, p):
        if p["swiglu"]:
            if z2.shape[0] // LNC >= _SW_MIN_ROWS:
                return nki_swiglu_stream(z2, p["s_w1t"], p["s_b1"], p["s_wgt"], p["s_bg"],
                                         p["s_w2t"], p["s_b2"]).unsqueeze(0)
            # torch fallback needs the UNPACKED [Ip] biases (v1.2 regression: it used the packed ones).
            g = torch.nn.functional.silu((z2 @ p["s_w1t"]).float() + p["s_b1u"])
            u = (z2 @ p["s_wgt"]).float() + p["s_bgu"]
            return (((g * u).to(torch.bfloat16) @ p["s_w2t"]).float() + p["s_b2"]).to(torch.bfloat16).unsqueeze(0)
        if _MLP_FP8:
            return _mf8.nki_mlp_fp8(z2, p["w1p8"], p["b1"], p["w2p8"], p["b2"], p["sw1"], p["sw2"], p["g8"]).unsqueeze(0)
        return nki_mlp_k4_xp(z2, p["w1t"], p["b1"], p["w2t"], p["b2"]).unsqueeze(0)

    def _ffn_fused(self, z2, p, x, eps_next):
        if p["swiglu"]:
            if z2.shape[0] // LNC < _SW_MIN_ROWS:
                return None
            return nki_swiglu_stream_ln(z2, p["s_w1t"], p["s_b1"], p["s_wgt"], p["s_bg"],
                                        p["s_w2t"], p["s_b2"], x, eps_next)
        if _MLP_FP8:
            return _mf8.nki_mlp_fp8_res_ln(z2, p["w1p8"], p["b1"], p["w2p8"], p["b2"], x, eps_next,
                                           p["sw1"], p["sw2"], p["g8"])
        return nki_mlp_res_ln(z2, p["w1t"], p["b1"], p["w2t"], p["b2"], x, eps_next)

    def __call__(self, x: torch.Tensor, tab: torch.Tensor, n_valid: int, B: int) -> torch.Tensor:
        """x: [B*N, H] fp32 (padded rows zero). Returns the pre-final-norm residual stream [B*N, H] fp32."""
        H = self.H
        branch = x.new_zeros(1, x.shape[0], H, dtype=torch.bfloat16)
        z1 = None
        nL = len(self.L)
        for li, p in enumerate(self.L):
            if z1 is None:
                x, z1 = nki_res_layernorm_fold(x, branch, p["eps1"])
            N = x.shape[0] // B
            if self._all_group(N):
                x, z2 = _ab1ln.attention_block_l1_ln(z1, p["wqk"], p["bqk"], tab, p["wvt"], p["bv"], p["wot"],
                                                     p["bo_full"], x, p["scale"], self.D, n_valid, p["eps2"], B,
                                                     self._res(_ab1ln, N), int(_PACK2 and B % 2 == 0 and 2 * N <= 512))
            else:
                parts = self._attn_parts(z1, p, tab, n_valid, B)
                x, z2 = nki_res_layernorm_fold(x, parts, p["eps2"])
            fused = self._ffn_fused(z2, p, x, self.L[li + 1]["eps1"]) if li + 1 < nL else None
            if fused is not None:
                x, z1 = fused
                branch = None
            else:
                branch = self._ffn(z2, p)
                z1 = None
        if branch is not None:
            x = x + branch[0].float()
        return x