File size: 7,662 Bytes
eadc767 | 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 | """``NeuronVJEPA2Encoder`` -- the KernelConfig-mappable layer for the V-JEPA2 encoder.
USAGE (HF Transformers ``KernelConfig``)::
from transformers import AutoModel, KernelConfig
kernel_config = KernelConfig({
"VJEPA2Encoder": "OWNER/vjepa2-neuron-kernels:NeuronVJEPA2Encoder", # <- set to the published repo id
})
model = AutoModel.from_pretrained("facebook/vjepa2-vitl-fpc64-256",
dtype=torch.float32,
kernel_config=kernel_config,
device_map="neuron")
model.encoder.forward = model.encoder.forward # (kernelize does the graft)
WHY THE ENCODER CLASS AND NOT A PER-LAYER MAPPING. The K1/K3 kernel fuses the *previous*
sublayer's residual add with the *next* sublayer's LayerNorm, so the fusion spans transformer-
layer boundaries: the state carried between layers is the pair (fp32 residual stream, bf16
branch partials), not a single summed hidden_states. Replacing the whole encoder forward keeps
that chain -- and the instruction stream it was verified bitwise against -- intact. A per-layer
mapping would force a residual re-add at every boundary and change the stream.
MECHANICS. The kernels library grafts this class's ``forward`` onto the existing
``VJEPA2Encoder`` instance, so ``self`` inside ``forward`` is the *stock* encoder with its
stock parameters (checkpoint loading is untouched). On the first device forward, the per-layer
packed weights (LayerNorm affines folded into QKV/V and MLP-fc1, out-proj bias halved for the
LNC=2 partials, rope tables) are computed once from those stock parameters and cached on the
layer objects. Off-device or non-fp32 inputs fall back to the stock implementation.
``can_torch_compile = True`` is REQUIRED here: the published performance (23.94 ms/forward,
44% MFU) is of the ``torch.compile(backend="neuron")`` graph, and without this flag the kernels
library silently swaps the layer back to stock under Mode.TORCH_COMPILE (observed on other
Neuron kernel repos as a 25-127x silent regression).
"""
from __future__ import annotations
import torch
from .nki_kernels import (
attention_block,
build_rope_tables,
mlp,
pack_out_weights,
pack_tables,
pack_v_weights,
pack_weights,
res_layernorm_fold,
)
_PACKED_ATTR = "_vjepa2_nki_packed"
_TAB_ATTR = "_vjepa2_nki_tab"
def _pack_layer(lyr, tab):
"""Fold one transformer block's LayerNorm affines into the kernel weight packings.
Exact fp32 fold on CPU (the algebra of the verified reference implementation):
K1 gamma1/beta1 -> QKV and V packs; out-proj bias HALVED (two cores' partials sum);
K3 gamma2/beta2 -> MLP fc1; fc2 unfolded.
"""
attn = lyr.attention
dev = attn.query.weight.device
H = attn.all_head_size
D = attn.attention_head_size
heads = H // D
g1 = lyr.norm1.weight.detach().float().cpu()
t1 = lyr.norm1.bias.detach().float().cpu()
g2 = lyr.norm2.weight.detach().float().cpu()
t2 = lyr.norm2.bias.detach().float().cpu()
wq = attn.query.weight.detach().float().cpu(); bq = attn.query.bias.detach().float().cpu()
wk = attn.key.weight.detach().float().cpu(); bk = attn.key.bias.detach().float().cpu()
wv = attn.value.weight.detach().float().cpu(); bv = attn.value.bias.detach().float().cpu()
wqk_f, bqk_f = pack_weights(wq * g1[None, :], wk * g1[None, :],
bq + wq @ t1, bk + wk @ t1, heads, D)
wvt_f, bv_f = pack_v_weights(wv * g1[None, :], bv + wv @ t1)
wot, bo = pack_out_weights(attn.proj.weight, attn.proj.bias)
w1 = lyr.mlp.fc1.weight.detach().float().cpu(); b1 = lyr.mlp.fc1.bias.detach().float().cpu()
packed = {
"D": D, "heads": heads,
"k1_eps": float(lyr.norm1.eps), "k3_eps": float(lyr.norm2.eps),
"scale": float(attn.scaling),
"wqk_f": wqk_f.to(dev), "bqk_f": bqk_f.to(dev), "tab": tab,
"wvt_f": wvt_f.to(dev), "bv_f": bv_f.to(dev), "wot": wot.to(dev), "bo": bo.to(dev),
"w1t_f": (w1 * g2[None, :]).t().contiguous().to(torch.bfloat16).to(dev),
"b1_f": (b1 + w1 @ t2).contiguous().to(dev),
"w2t": lyr.mlp.fc2.weight.detach().t().contiguous().to(torch.bfloat16).to(dev),
"b2": lyr.mlp.fc2.bias.detach().float().contiguous().to(dev),
}
return packed
def _rope_tables(encoder, tokens):
attn0 = encoder.layer[0].attention
H = attn0.all_head_size
D = attn0.attention_head_size
dev = attn0.query.weight.device
pos = attn0.get_position_ids(torch.zeros(1, tokens, H, device=dev), masks=None)
def to1d(t):
return t if t.dim() == 1 else t.reshape(-1, tokens)[0]
cos, sinsigned = build_rope_tables(
(attn0.d_dim, attn0.h_dim, attn0.w_dim),
(to1d(pos[0]), to1d(pos[1]), to1d(pos[2])),
D, torch.float32, dev)
return pack_tables(cos, sinsigned, H // D, D)
@torch._dynamo.disable
def _freeze(encoder, tokens):
"""Pack every layer once; cached on the layer objects. Refuses in training mode.
``@torch._dynamo.disable`` is load-bearing: without it, a user who wraps the model in
``torch.compile`` before the first forward gets the weight-fold arithmetic traced/executed
by the compiler instead of eagerly, and the packed weights round DIFFERENTLY (measured:
output norm 4487.45 vs the verified 4486.19). The fold must run eagerly, once.
"""
if encoder.training:
raise RuntimeError(
"NeuronVJEPA2Encoder: encoder.training is True; these kernels have no autograd "
"rule. Call model.eval() first.")
tab = _rope_tables(encoder, tokens)
setattr(encoder, _TAB_ATTR, tab)
for lyr in encoder.layer:
setattr(lyr, _PACKED_ATTR, _pack_layer(lyr, tab))
class NeuronVJEPA2Encoder(torch.nn.Module):
"""Drop-in forward for ``VJEPA2Encoder``: the 4-kernel NKI layer loop.
Supported: trn2, 16 frames / 2048 tokens / batch 1, fp32 model. Anything else falls back
to the stock implementation (off-device, wrong geometry) or raises (training mode during
the first-freeze).
"""
can_torch_compile = True
has_backward = False
def forward(self, pixel_values_videos=None, **kwargs):
hs = self.embeddings(pixel_values_videos)
N = hs.shape[1]
# Fallback: off-device, or an unsupported geometry (only N=2048 is verified; the vendor
# attention kernel cannot hold 8192 keys in SBUF -- see the card's Known limitations).
if hs.device.type != "neuron" or N != 2048:
import transformers.models.vjepa2.modeling_vjepa2 as mv
return mv.VJEPA2Encoder.forward(self, pixel_values_videos, **kwargs)
if not hasattr(self.layer[0], _PACKED_ATTR):
_freeze(self, N)
x = hs[0] # [N, H] fp32 residual stream
H = x.shape[1]
branch = torch.zeros(1, N, H, dtype=torch.bfloat16, device=x.device)
for lyr in self.layer:
p = getattr(lyr, _PACKED_ATTR)
x, z1 = res_layernorm_fold(x, branch, p["k1_eps"])
attn_partials = attention_block(
z1, p["wqk_f"], p["bqk_f"], p["tab"], p["wvt_f"], p["bv_f"],
p["wot"], p["bo"], p["scale"], p["D"])
x, z2 = res_layernorm_fold(x, attn_partials, p["k3_eps"])
branch = mlp(z2, p["w1t_f"], p["b1_f"], p["w2t"], p["b2"]).unsqueeze(0)
x = x + branch[0].to(torch.float32)
out = self.layernorm(x.unsqueeze(0))
import transformers.models.vjepa2.modeling_vjepa2 as mv
return mv.BaseModelOutput(last_hidden_state=out)
|