File size: 11,042 Bytes
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a95cd4f
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90f3d2f
 
 
 
 
3b1bffa
 
 
 
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a95cd4f
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
---
license: apache-2.0
library_name: kernels
tags:
  - kernel
  - neuron
  - nki
  - trainium
  - trn2
  - vision
  - vit
  - dinov3
  - vjepa2
  - rope
  - swiglu
---

# vit-encoder-neuron-kernels

NKI kernels that run the whole transformer stack of **RoPE vision-transformer encoders** on AWS
Trainium2 (trn2). Each block is 2-3 coarse kernels instead of dozens of small ops: attention plus
the residual add and LayerNorm, and the MLP / SwiGLU FFN plus the next block's LayerNorm. The
residual stream stays in fp32 between kernels.

Drop-in layers are included for two Hugging Face Transformers models:

| Layer | Replaces | Models |
|---|---|---|
| `NeuronDINOv3ViTModel` | `DINOv3ViTModel.forward` | DINOv3 ViT-S/16, S+/16, B/16, L/16, H+/16 |
| `NeuronVJEPA2Encoder` | `VJEPA2Encoder.forward` | V-JEPA2 ViT-L (video), up to 2048 tokens per clip |

The building blocks (`BlockWeights` / `pack_block` / `EncoderRunner`) are model-agnostic. Any
pre-norm ViT with the shape constraints below can use them, but only the two models above
have been validated.

## Performance (trn2.3xlarge, bf16, `torch.compile(backend="neuron")`)

Measured 2026-10 on PyTorch Native Beta 6 (torch 2.13.0, torch-neuronx 2.13.3, NKI 0.7.0b1).
Real Hugging Face weights. "Stock" is the same model compiled with
`torch.compile(backend="neuron")` without these kernels: DINOv3 cast to bf16, V-JEPA2 as its fp32
checkpoint (stock V-JEPA2 is the encoder as shipped, and its accuracy is in the table below).

**Per instance** (one process per logical core, all cores busy):

| Model | Resolution | Stock (img/s) | **These kernels (img/s)** | Speedup |
|---|---|---|---|---|
| DINOv3 ViT-B/16 | 224 | 2,901.5 | **4,105.2** | 1.41x |
| DINOv3 ViT-L/16 | 224 | 1,061.2 | **1,767.5** | 1.67x |
| DINOv3 ViT-L/16 | 448 | 201.1 | **491.9** | 2.45x |
| DINOv3 ViT-H+/16 | 224 | 400.0 | **626.6** | 1.57x |
| DINOv3 ViT-H+/16 | 448 | 106.8 | **200.3** | 1.88x |
| V-JEPA2 ViT-L, 16 frames x 256 px | - | 36.8 clips/s | **157.3 clips/s** | 4.27x |

Configuration: LNC=1 (8 logical cores) for every row except V-JEPA2 stock, whose best was LNC=2
(4 cores). DINOv3 kernels run at batch 8 (224 px) or batch 2 (448 px). DINOv3 stock runs at batch 4
(224 px) or batch 1 (448 px), its best measured. V-JEPA2 runs at batch 1 per core for both.

**Single core, latency-oriented** (LNC=2, batch 1):

| Model | Resolution | Stock | **These kernels** | Speedup |
|---|---|---|---|---|
| DINOv3 ViT-B/16 | 224 | 251.0 img/s | **510.7 img/s** | 2.03x |
| DINOv3 ViT-L/16 | 224 | 97.1 img/s | **220.3 img/s** (4.5 ms) | 2.27x |
| DINOv3 ViT-L/16 | 448 | 25.1 img/s | **83.0 img/s** | 3.31x |
| DINOv3 ViT-H+/16 | 224 | 21.4 img/s | **88.2 img/s** | 4.12x |
| DINOv3 ViT-H+/16 | 448 | 17.7 img/s | **32.4 img/s** | 1.83x |
| V-JEPA2 ViT-L, 16 frames x 256 px | - | 101.5 ms/clip | **25.8 ms/clip** | 3.93x |

The gain grows with resolution: attention cost grows with token count, and these kernels keep
attention and its surroundings on chip. At 224 px with large batches the gap narrows. For example,
stock ViT-L/16 at LNC=2 batch 4 is 252 img/s and the kernels give 321 img/s (1.27x).

## Accuracy

Every result above passes a per-image check against the fp32 CPU model (rel_l2 <= 5e-2).
On DINOv3 the kernels are **closer to fp32 than stock bf16**, because the residual stream and
LayerNorm statistics stay in fp32:

| Model | Stock bf16 rel_l2 vs fp32 | These kernels |
|---|---|---|
| DINOv3 ViT-B/16 | 1.4-1.8e-2 | 0.9-1.7e-2 |
| DINOv3 ViT-L/16 | 1.3-1.6e-2 | 0.8-1.0e-2 |
| DINOv3 ViT-H+/16 | 1.0-1.7e-2 | 0.6-1.0e-2 |
| V-JEPA2 ViT-L | 1.4e-2 (fp32 model, compiler-chosen precision) | 3.9e-2 (bf16 matmuls) |

For V-JEPA2 the stock path compiles the fp32 checkpoint and keeps more of it in higher precision,
so it is closer to fp32 than the kernels (which run every matmul in bf16).

## Quick start

```python
import types, torch
from kernels import get_kernel
from transformers import AutoModel

k = get_kernel("aws-neuron/vit-encoder-neuron-kernels", version=1, trust_remote_code=True)

model = AutoModel.from_pretrained("facebook/dinov3-vitl16-pretrain-lvd1689m",
                                  dtype=torch.float32).eval().to("neuron")
model.forward = types.MethodType(k.NeuronDINOv3ViTModel.forward, model)
model = torch.compile(model, backend="neuron")

with torch.no_grad():
    out = model(pixel_values.to("neuron"))   # [B, 3, H, W] fp32, any batch size
feats = out.last_hidden_state                 # [B, 1 + 4 registers + patches, 1024]
cls = out.pooler_output
```

V-JEPA2:

```python
model = AutoModel.from_pretrained("facebook/vjepa2-vitl-fpc64-256", dtype=torch.float32).eval().to("neuron")
model.encoder.forward = types.MethodType(k.NeuronVJEPA2Encoder.forward, model.encoder)
enc = torch.compile(model.encoder, backend="neuron")
emb = enc(clip.to("neuron")).last_hidden_state   # clip [B, 16, 3, 256, 256] -> [B, 2048, 1024]
```

Notes:
* Load the model in **fp32**. The layer folds LayerNorm affines and LayerScale into the weights
  once, on the first device forward, and casts the weights to bf16 itself.
* **`torch.compile(backend="neuron")` is part of the configuration.** The layers set
  `can_torch_compile = True`.
* **Set `NEURON_LOGICAL_NC_CONFIG` before importing**, and to the same value the process runs
  with: `2` (default) or `1`. The kernels are built for that core configuration. For LNC=1 also pass
  `NEURON_CC_FLAGS=--lnc=1`.
* `NKI_ENABLE_TRACE_CACHE=0` is recommended when you edit kernels. The cross-process trace cache
  can otherwise reuse a stale NEFF.
* On a vLLM-Neuron venv (CUDA-tagged torch), `import libtorch_neuronx_lite` before `get_kernel`
  so the `torch-neuron` variant is selected. On PyTorch Native this is not needed.

**Environment switches** (defaults are the measured best): `VIT_ENC_PACK2=1` packs two images per attention producer
pass at LNC=1 when the batch is even and the images are <= 256 tokens (224 px); `VIT_ENC_ROPE_BF16=1` (experimental)
runs RoPE in bf16; `VIT_ENC_WB_FUSED=1` (experimental) fuses the attention normalisation; `VIT_ENC_MM1_WIDE=0`
restores the v1.3 QK^T PSUM layout.

**Experimental FP8 MLP** (v1.4, GELU-MLP models only): set `VIT_ENC_MLP_FP8=1` before importing to run the MLP
matmuls in FP8 e4m3 (`double_row`). That is +7-10% end to end on DINOv3 ViT-L/16, at ~7x the feature error of the
default bf16 path (CLS rel_l2 5.8e-2 vs 0.84e-2; imagenette k-NN predictions unchanged). Calibrate the per-layer
activation scale with `calibrate_fp8(runner, gelu_amax)`, because trn2's e4m3 saturates at 240. Off by default.

Which LNC to use: **LNC=1** (8 logical cores, one process per core) gives the most throughput per
instance. **LNC=2** (4 larger cores) gives the lowest latency per request.

## Kernels

| Kernel | What it does |
|---|---|
| `attention_block` | LN'd hidden -> Q/K/V projection + RoPE + attention + output projection, one 8-head (D=64) or 4-head (D=128) group per program; any number of images per launch; padded keys skipped |
| `attention_block_l1` | LNC=1: one program over **all** head groups, one output (no per-group partials) |
| `attention_block_l1_ln` | `attention_block_l1` plus residual add and LayerNorm in the epilogue |
| `res_layernorm_fold` | residual add (sum of up to 4 branches) + affine-free LayerNorm |
| `mlp` | GELU MLP (fc1 -> GELU -> fc2) |
| `mlp_res_ln` | `mlp` plus residual add and the next block's LayerNorm in the epilogue |
| `swiglu_stream` | SwiGLU FFN that streams the weights in chunks of the intermediate dimension (so large FFNs fit on chip) |
| `swiglu_stream_ln` | `swiglu_stream` plus residual add and the next block's LayerNorm |

Host-side packing (`pack_weights`, `pack_tables`, `lane_permutation`, `pack_block`) supports both
RoPE pairings via `rope=`:
* `"half"`: pairs lane `j` with `j + D/2` (rotate-half; DINOv3, EVA-02, HF Llama style).
* `"interleaved"`: pairs lane `2j` with `2j + 1` (V-JEPA2 style).

## Supported shapes and limits

* **Bidirectional (encoder) self-attention with RoPE.** No causal masks, KV cache, cross-attention,
  or learned / absolute position embeddings. A model without RoPE could pass identity tables (cos=1,
  sin=0), but that is untested.
* **Pre-norm blocks with standard LayerNorm** (not RMSNorm), optional LayerScale.
* hidden size a multiple of 128; head_dim 64 or 128; any head count (padded to a multiple of 8
  at D=64 or 4 at D=128).
* **<= 2048 tokens per image** after padding to a multiple of 128 x LNC (e.g. 720 x 720 px at
  patch 16). Longer inputs fall back to the stock forward.
* FFN: GELU MLP (`nl.gelu`, the erf form) or SwiGLU (SiLU gate).
* Inference only (no autograd). The layers refuse in training mode.
* ViT-7B-sized models (hidden 4096) are not covered by the drop-in layer. They need tensor parallelism.
  The streaming SwiGLU kernel is slower than the compiler's FFN at that width.
* Validated end to end on DINOv3 ViT-B/L/H+ and V-JEPA2 ViT-L only.

## Requirements

* trn2 (verified on trn2.3xlarge, LNC=1 and LNC=2).
* PyTorch Native (TorchNeuron) with NKI 0.7 and `nkilib`. Verified on Beta 6 (torch 2.13.0,
  torch-neuronx 2.13.3, NKI 0.7.0b1, neuronx-cc 2.0.404056).
* Transformers with DINOv3 / V-JEPA2 (verified on 4.57.6); `kernels` for `get_kernel`.

## Repository layout

```
build/torch-neuron/
├── __init__.py              # public API
├── metadata.json
├── layers.py                # NeuronDINOv3ViTModel, NeuronVJEPA2Encoder
├── encoder.py               # BlockWeights, pack_block, rope_tables, EncoderRunner
└── nki_kernels/
    ├── attention_block.py       # attention (per head group) + host-side packing
    ├── attention_block_l1.py    # attention, all head groups (LNC=1)
    ├── attention_block_l1_ln.py # ... + residual + LayerNorm epilogue
    ├── attention_inner.py       # modified copy of nkilib attention_cte (vendor attention core)
    ├── res_layernorm.py         # residual + affine-free LayerNorm
    ├── mlp.py                   # GELU MLP
    ├── mlp_res_ln.py            # GELU MLP + residual + next LayerNorm
    └── swiglu_stream.py         # weight-streaming SwiGLU (+ residual/LayerNorm variant)
```

## Attribution and license

Apache-2.0 (see `LICENSE`, `NOTICE`). The attention, MLP and residual/LayerNorm kernels derive from
the V-JEPA2 encoder kernels in
[`aws-neuron/vjepa2-neuron-kernels`](https://huggingface.co/kernels/aws-neuron/vjepa2-neuron-kernels),
developed by Antonio Mena ([@menaman123](https://huggingface.co/menaman123)). This repo generalizes
them: both RoPE pairings, any head count, head_dim 128, batching, padded-key skipping, an LNC=1
all-group variant, and the fused residual/LayerNorm epilogues. `swiglu_stream.py` is new.
`nki_kernels/attention_inner.py` is a modified copy of the AWS Neuron SDK's `nkilib`
`core/attention/attention_cte.py` (Copyright Amazon.com, Inc. or its affiliates, Apache-2.0). The
file header lists the modifications.

DINOv3 and V-JEPA2 model weights are distributed under their own licenses.