jburtoft commited on
Commit
c855122
·
1 Parent(s): b821eb9

v1: NKI kernels for RoPE ViT encoders (DINOv3, V-JEPA2) on Trainium2

Browse files
.gitattributes CHANGED
@@ -1,3 +1,5 @@
1
- *.so filter=lfs diff=lfs merge=lfs -text
2
- *.pyd filter=lfs diff=lfs merge=lfs -text
3
  *.dylib filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
1
  *.dylib filter=lfs diff=lfs merge=lfs -text
2
+ *.neff filter=lfs diff=lfs merge=lfs -text
3
+ *.pt filter=lfs diff=lfs merge=lfs -text
4
+ *.pyd filter=lfs diff=lfs merge=lfs -text
5
+ *.so filter=lfs diff=lfs merge=lfs -text
CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## v1 (2026-10)
4
+
5
+ First release.
6
+
7
+ * Drop-in layers for Hugging Face Transformers: `NeuronDINOv3ViTModel` (DINOv3 ViT-S/16, S+/16, B/16,
8
+ L/16, H+/16) and `NeuronVJEPA2Encoder` (V-JEPA2 ViT-L, any batch size, up to 2048 tokens per clip).
9
+ * Model-agnostic encoder loop: `BlockWeights`, `pack_block`, `rope_tables`, `EncoderRunner`.
10
+ * Kernels: `attention_block` (per head group; batched; padded keys skipped; head_dim 64 or 128;
11
+ any head count), `attention_block_l1` and `attention_block_l1_ln` (LNC=1, all head groups, optional
12
+ residual + LayerNorm epilogue), `res_layernorm_fold`, `mlp` and `mlp_res_ln` (GELU MLP, optional
13
+ residual + next LayerNorm epilogue), `swiglu_stream` and `swiglu_stream_ln` (weight-streaming
14
+ SwiGLU FFN).
15
+ * RoPE pairing selectable at packing time: `rope="half"` (rotate-half) or `rope="interleaved"`.
16
+ * Verified on trn2.3xlarge, PyTorch Native Beta 6 (torch 2.13.0, torch-neuronx 2.13.3, NKI 0.7.0b1),
17
+ Transformers 4.57.6, LNC=1 and LNC=2. Every measured case passes rel_l2 <= 5e-2 vs the fp32 CPU model.
18
+
19
+ Known limitations: encoder (bidirectional) attention with RoPE only; LayerNorm (not RMSNorm);
20
+ <= 2048 tokens per image; inference only; hidden size 4096 (ViT-7B) not covered by the drop-in layer.
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
NOTICE ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ vit-encoder-neuron-kernels -- NKI kernels for RoPE ViT encoders on AWS Trainium2
2
+ Licensed under the Apache License, Version 2.0 (see LICENSE).
3
+
4
+ nki_kernels/attention_block.py, res_layernorm.py and mlp.py are modified versions of the V-JEPA2
5
+ encoder kernels published at huggingface.co/kernels/jburtoft/vjepa2-neuron-kernels (Apache-2.0),
6
+ developed by Antonio Mena. Modifications: selectable RoPE pairing, any head count, head_dim 128,
7
+ ragged token tiles and padded-key skipping, multi-image launches, resident weights, LNC=1 grid plan.
8
+ nki_kernels/attention_block_l1.py, attention_block_l1_ln.py and mlp_res_ln.py are derived from them.
9
+ nki_kernels/swiglu_stream.py, encoder.py and layers.py are new.
10
+
11
+ nki_kernels/attention_inner.py is a modified copy of the AWS Neuron SDK's nkilib
12
+ core/attention/attention_cte.py, Copyright Amazon.com, Inc. or its affiliates, licensed under the
13
+ Apache License, Version 2.0. See the file header for the list of modifications.
README.md ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ library_name: kernels
4
+ tags:
5
+ - kernel
6
+ - neuron
7
+ - nki
8
+ - trainium
9
+ - trn2
10
+ - vision
11
+ - vit
12
+ - dinov3
13
+ - vjepa2
14
+ - rope
15
+ - swiglu
16
+ ---
17
+
18
+ # vit-encoder-neuron-kernels
19
+
20
+ NKI kernels that run the whole transformer stack of **RoPE vision-transformer encoders** on AWS
21
+ Trainium2 (trn2). Each block is 2-3 coarse kernels instead of dozens of small ops: attention plus
22
+ the residual add and LayerNorm, and the MLP / SwiGLU FFN plus the next block's LayerNorm. The
23
+ residual stream stays in fp32 between kernels.
24
+
25
+ Drop-in layers are included for two Hugging Face Transformers models:
26
+
27
+ | Layer | Replaces | Models |
28
+ |---|---|---|
29
+ | `NeuronDINOv3ViTModel` | `DINOv3ViTModel.forward` | DINOv3 ViT-S/16, S+/16, B/16, L/16, H+/16 |
30
+ | `NeuronVJEPA2Encoder` | `VJEPA2Encoder.forward` | V-JEPA2 ViT-L (video), up to 2048 tokens per clip |
31
+
32
+ The building blocks (`BlockWeights` / `pack_block` / `EncoderRunner`) are model-agnostic. Any
33
+ pre-norm ViT with the shape constraints below can use them, but only the two models above
34
+ have been validated.
35
+
36
+ ## Performance (trn2.3xlarge, bf16, `torch.compile(backend="neuron")`)
37
+
38
+ Measured 2026-10 on PyTorch Native Beta 6 (torch 2.13.0, torch-neuronx 2.13.3, NKI 0.7.0b1).
39
+ Real Hugging Face weights. "Stock" is the same model compiled with
40
+ `torch.compile(backend="neuron")` without these kernels: DINOv3 cast to bf16, V-JEPA2 as its fp32
41
+ checkpoint (stock V-JEPA2 is the encoder as shipped, and its accuracy is in the table below).
42
+
43
+ **Per instance** (one process per logical core, all cores busy):
44
+
45
+ | Model | Resolution | Stock (img/s) | **These kernels (img/s)** | Speedup |
46
+ |---|---|---|---|---|
47
+ | DINOv3 ViT-B/16 | 224 | 2,901.5 | **4,105.2** | 1.41x |
48
+ | DINOv3 ViT-L/16 | 224 | 1,061.2 | **1,767.5** | 1.67x |
49
+ | DINOv3 ViT-L/16 | 448 | 201.1 | **491.9** | 2.45x |
50
+ | DINOv3 ViT-H+/16 | 224 | 400.0 | **626.6** | 1.57x |
51
+ | DINOv3 ViT-H+/16 | 448 | 106.8 | **200.3** | 1.88x |
52
+ | V-JEPA2 ViT-L, 16 frames x 256 px | - | 36.8 clips/s | **157.3 clips/s** | 4.27x |
53
+
54
+ Configuration: LNC=1 (8 logical cores) for every row except V-JEPA2 stock, whose best was LNC=2
55
+ (4 cores). DINOv3 kernels run at batch 8 (224 px) or batch 2 (448 px). DINOv3 stock runs at batch 4
56
+ (224 px) or batch 1 (448 px), its best measured. V-JEPA2 runs at batch 1 per core for both.
57
+
58
+ **Single core, latency-oriented** (LNC=2, batch 1):
59
+
60
+ | Model | Resolution | Stock | **These kernels** | Speedup |
61
+ |---|---|---|---|---|
62
+ | DINOv3 ViT-B/16 | 224 | 251.0 img/s | **510.7 img/s** | 2.03x |
63
+ | DINOv3 ViT-L/16 | 224 | 97.1 img/s | **220.3 img/s** (4.5 ms) | 2.27x |
64
+ | DINOv3 ViT-L/16 | 448 | 25.1 img/s | **83.0 img/s** | 3.31x |
65
+ | DINOv3 ViT-H+/16 | 224 | 21.4 img/s | **88.2 img/s** | 4.12x |
66
+ | DINOv3 ViT-H+/16 | 448 | 17.7 img/s | **32.4 img/s** | 1.83x |
67
+ | V-JEPA2 ViT-L, 16 frames x 256 px | - | 101.5 ms/clip | **25.8 ms/clip** | 3.93x |
68
+
69
+ The gain grows with resolution: attention cost grows with token count, and these kernels keep
70
+ attention and its surroundings on chip. At 224 px with large batches the gap narrows. For example,
71
+ stock ViT-L/16 at LNC=2 batch 4 is 252 img/s and the kernels give 321 img/s (1.27x).
72
+
73
+ ## Accuracy
74
+
75
+ Every result above passes a per-image check against the fp32 CPU model (rel_l2 <= 5e-2).
76
+ On DINOv3 the kernels are **closer to fp32 than stock bf16**, because the residual stream and
77
+ LayerNorm statistics stay in fp32:
78
+
79
+ | Model | Stock bf16 rel_l2 vs fp32 | These kernels |
80
+ |---|---|---|
81
+ | DINOv3 ViT-B/16 | 1.4-1.8e-2 | 0.9-1.7e-2 |
82
+ | DINOv3 ViT-L/16 | 1.3-1.6e-2 | 0.8-1.0e-2 |
83
+ | DINOv3 ViT-H+/16 | 1.0-1.7e-2 | 0.6-1.0e-2 |
84
+ | V-JEPA2 ViT-L | 1.4e-2 (fp32 model, compiler-chosen precision) | 3.9e-2 (bf16 matmuls) |
85
+
86
+ For V-JEPA2 the stock path compiles the fp32 checkpoint and keeps more of it in higher precision,
87
+ so it is closer to fp32 than the kernels (which run every matmul in bf16).
88
+
89
+ ## Quick start
90
+
91
+ ```python
92
+ import types, torch
93
+ from kernels import get_kernel
94
+ from transformers import AutoModel
95
+
96
+ k = get_kernel("jburtoft/vit-encoder-neuron-kernels", version=1, trust_remote_code=True)
97
+
98
+ model = AutoModel.from_pretrained("facebook/dinov3-vitl16-pretrain-lvd1689m",
99
+ dtype=torch.float32).eval().to("neuron")
100
+ model.forward = types.MethodType(k.NeuronDINOv3ViTModel.forward, model)
101
+ model = torch.compile(model, backend="neuron")
102
+
103
+ with torch.no_grad():
104
+ out = model(pixel_values.to("neuron")) # [B, 3, H, W] fp32, any batch size
105
+ feats = out.last_hidden_state # [B, 1 + 4 registers + patches, 1024]
106
+ cls = out.pooler_output
107
+ ```
108
+
109
+ V-JEPA2:
110
+
111
+ ```python
112
+ model = AutoModel.from_pretrained("facebook/vjepa2-vitl-fpc64-256", dtype=torch.float32).eval().to("neuron")
113
+ model.encoder.forward = types.MethodType(k.NeuronVJEPA2Encoder.forward, model.encoder)
114
+ enc = torch.compile(model.encoder, backend="neuron")
115
+ emb = enc(clip.to("neuron")).last_hidden_state # clip [B, 16, 3, 256, 256] -> [B, 2048, 1024]
116
+ ```
117
+
118
+ Notes:
119
+ * Load the model in **fp32**. The layer folds LayerNorm affines and LayerScale into the weights
120
+ once, on the first device forward, and casts the weights to bf16 itself.
121
+ * **`torch.compile(backend="neuron")` is part of the configuration.** The layers set
122
+ `can_torch_compile = True`.
123
+ * **Set `NEURON_LOGICAL_NC_CONFIG` before importing**, and to the same value the process runs
124
+ with: `2` (default) or `1`. The kernels are built for that core configuration. For LNC=1 also pass
125
+ `NEURON_CC_FLAGS=--lnc=1`.
126
+ * `NKI_ENABLE_TRACE_CACHE=0` is recommended when you edit kernels. The cross-process trace cache
127
+ can otherwise reuse a stale NEFF.
128
+ * On a vLLM-Neuron venv (CUDA-tagged torch), `import libtorch_neuronx_lite` before `get_kernel`
129
+ so the `torch-neuron` variant is selected. On PyTorch Native this is not needed.
130
+
131
+ Which LNC to use: **LNC=1** (8 logical cores, one process per core) gives the most throughput per
132
+ instance. **LNC=2** (4 larger cores) gives the lowest latency per request.
133
+
134
+ ## Kernels
135
+
136
+ | Kernel | What it does |
137
+ |---|---|
138
+ | `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 |
139
+ | `attention_block_l1` | LNC=1: one program over **all** head groups, one output (no per-group partials) |
140
+ | `attention_block_l1_ln` | `attention_block_l1` plus residual add and LayerNorm in the epilogue |
141
+ | `res_layernorm_fold` | residual add (sum of up to 4 branches) + affine-free LayerNorm |
142
+ | `mlp` | GELU MLP (fc1 -> GELU -> fc2) |
143
+ | `mlp_res_ln` | `mlp` plus residual add and the next block's LayerNorm in the epilogue |
144
+ | `swiglu_stream` | SwiGLU FFN that streams the weights in chunks of the intermediate dimension (so large FFNs fit on chip) |
145
+ | `swiglu_stream_ln` | `swiglu_stream` plus residual add and the next block's LayerNorm |
146
+
147
+ Host-side packing (`pack_weights`, `pack_tables`, `lane_permutation`, `pack_block`) supports both
148
+ RoPE pairings via `rope=`:
149
+ * `"half"`: pairs lane `j` with `j + D/2` (rotate-half; DINOv3, EVA-02, HF Llama style).
150
+ * `"interleaved"`: pairs lane `2j` with `2j + 1` (V-JEPA2 style).
151
+
152
+ ## Supported shapes and limits
153
+
154
+ * **Bidirectional (encoder) self-attention with RoPE.** No causal masks, KV cache, cross-attention,
155
+ or learned / absolute position embeddings. A model without RoPE could pass identity tables (cos=1,
156
+ sin=0), but that is untested.
157
+ * **Pre-norm blocks with standard LayerNorm** (not RMSNorm), optional LayerScale.
158
+ * hidden size a multiple of 128; head_dim 64 or 128; any head count (padded to a multiple of 8
159
+ at D=64 or 4 at D=128).
160
+ * **<= 2048 tokens per image** after padding to a multiple of 128 x LNC (e.g. 720 x 720 px at
161
+ patch 16). Longer inputs fall back to the stock forward.
162
+ * FFN: GELU MLP (`nl.gelu`, the erf form) or SwiGLU (SiLU gate).
163
+ * Inference only (no autograd). The layers refuse in training mode.
164
+ * ViT-7B-sized models (hidden 4096) are not covered by the drop-in layer. They need tensor parallelism.
165
+ The streaming SwiGLU kernel is slower than the compiler's FFN at that width.
166
+ * Validated end to end on DINOv3 ViT-B/L/H+ and V-JEPA2 ViT-L only.
167
+
168
+ ## Requirements
169
+
170
+ * trn2 (verified on trn2.3xlarge, LNC=1 and LNC=2).
171
+ * PyTorch Native (TorchNeuron) with NKI 0.7 and `nkilib`. Verified on Beta 6 (torch 2.13.0,
172
+ torch-neuronx 2.13.3, NKI 0.7.0b1, neuronx-cc 2.0.404056).
173
+ * Transformers with DINOv3 / V-JEPA2 (verified on 4.57.6); `kernels` for `get_kernel`.
174
+
175
+ ## Repository layout
176
+
177
+ ```
178
+ build/torch-neuron/
179
+ ├── __init__.py # public API
180
+ ├── metadata.json
181
+ ├── layers.py # NeuronDINOv3ViTModel, NeuronVJEPA2Encoder
182
+ ├── encoder.py # BlockWeights, pack_block, rope_tables, EncoderRunner
183
+ └── nki_kernels/
184
+ ├── attention_block.py # attention (per head group) + host-side packing
185
+ ├── attention_block_l1.py # attention, all head groups (LNC=1)
186
+ ├── attention_block_l1_ln.py # ... + residual + LayerNorm epilogue
187
+ ├── attention_inner.py # modified copy of nkilib attention_cte (vendor attention core)
188
+ ├── res_layernorm.py # residual + affine-free LayerNorm
189
+ ├── mlp.py # GELU MLP
190
+ ├── mlp_res_ln.py # GELU MLP + residual + next LayerNorm
191
+ └── swiglu_stream.py # weight-streaming SwiGLU (+ residual/LayerNorm variant)
192
+ ```
193
+
194
+ ## Attribution and license
195
+
196
+ Apache-2.0 (see `LICENSE`, `NOTICE`). The attention, MLP and residual/LayerNorm kernels derive from
197
+ the V-JEPA2 encoder kernels in
198
+ [`jburtoft/vjepa2-neuron-kernels`](https://huggingface.co/kernels/jburtoft/vjepa2-neuron-kernels),
199
+ developed by Antonio Mena ([@menaman123](https://huggingface.co/menaman123)). This repo generalizes
200
+ them: both RoPE pairings, any head count, head_dim 128, batching, padded-key skipping, an LNC=1
201
+ all-group variant, and the fused residual/LayerNorm epilogues. `swiglu_stream.py` is new.
202
+ `nki_kernels/attention_inner.py` is a modified copy of the AWS Neuron SDK's `nkilib`
203
+ `core/attention/attention_cte.py` (Copyright Amazon.com, Inc. or its affiliates, Apache-2.0). The
204
+ file header lists the modifications.
205
+
206
+ DINOv3 and V-JEPA2 model weights are distributed under their own licenses.
build/torch-neuron/__init__.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """NKI kernels for RoPE ViT encoders on AWS Trainium2 (trn2).
2
+
3
+ Model layers (drop-in forwards for Hugging Face Transformers):
4
+ NeuronDINOv3ViTModel -- DINOv3ViTModel (S/S+/B/L/H+)
5
+ NeuronVJEPA2Encoder -- VJEPA2Encoder
6
+
7
+ Model-agnostic building blocks:
8
+ BlockWeights, pack_block, rope_tables, EncoderRunner -- pre-norm encoder loop on the kernels
9
+ kernels: attention_block, attention_block_l1, attention_block_l1_ln, res_layernorm_fold,
10
+ mlp, mlp_res_ln, swiglu_stream, swiglu_stream_ln
11
+ """
12
+
13
+ from .encoder import LNC, BlockWeights, EncoderRunner, pack_block, rope_tables
14
+ from .layers import NeuronDINOv3ViTModel, NeuronVJEPA2Encoder
15
+ from .nki_kernels import (
16
+ attention_block,
17
+ attention_block_l1,
18
+ attention_block_l1_ln,
19
+ lane_permutation,
20
+ mlp,
21
+ mlp_res_ln,
22
+ pack_out_weights,
23
+ pack_tables,
24
+ pack_v_weights,
25
+ pack_weights,
26
+ res_layernorm_fold,
27
+ swiglu_stream,
28
+ swiglu_stream_ln,
29
+ )
30
+
31
+ __all__ = [
32
+ "NeuronDINOv3ViTModel", "NeuronVJEPA2Encoder",
33
+ "BlockWeights", "pack_block", "rope_tables", "EncoderRunner", "LNC",
34
+ "attention_block", "attention_block_l1", "attention_block_l1_ln", "res_layernorm_fold",
35
+ "mlp", "mlp_res_ln", "swiglu_stream", "swiglu_stream_ln",
36
+ "pack_weights", "pack_tables", "pack_v_weights", "pack_out_weights", "lane_permutation",
37
+ ]
build/torch-neuron/encoder.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model-agnostic ViT encoder loop on the NKI kernels.
2
+
3
+ A pre-norm transformer block
4
+
5
+ x = x + ls1 * attn(LN1(x)) attn: q/k/v projections, RoPE on q/k, softmax, out proj
6
+ x = x + ls2 * ffn(LN2(x)) ffn: GELU MLP or SwiGLU
7
+
8
+ is described once per block as a :class:`BlockWeights` (plain fp32 tensors, any source), packed
9
+ once by :func:`pack_block` (LayerNorm affines and LayerScale folded into the weights), and run by
10
+ :class:`EncoderRunner` on a stacked ``[B*N, H]`` row buffer.
11
+
12
+ Kernel choice per block (all automatic):
13
+
14
+ * attention: LNC=1 -> one program over all head groups, with the residual add + LN2 in its
15
+ epilogue; LNC=2 -> per-group programs whose partials the residual/LN kernel sums.
16
+ * FFN: GELU -> fused MLP; SwiGLU -> weight-streaming SwiGLU. Both also do the residual add and
17
+ the NEXT block's LN1 in their epilogue, so the stand-alone residual/LN kernel runs only before
18
+ block 0 (and after attention at LNC=2).
19
+
20
+ INFERENCE ONLY (no autograd). Constraints: hidden size % 128 == 0, head_dim in {64, 128},
21
+ tokens per image <= 2048 after padding to a multiple of 128 * LNC.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ from dataclasses import dataclass
26
+ from typing import Optional
27
+
28
+ import torch
29
+
30
+ import importlib as _il
31
+
32
+ _ab = _il.import_module(__package__ + ".nki_kernels.attention_block")
33
+ _ab1 = _il.import_module(__package__ + ".nki_kernels.attention_block_l1")
34
+ _ab1ln = _il.import_module(__package__ + ".nki_kernels.attention_block_l1_ln")
35
+ from .nki_kernels.attention_block import LNC
36
+ from .nki_kernels.mlp import nki_mlp_k4_xp
37
+ from .nki_kernels.mlp_res_ln import nki_mlp_res_ln
38
+ from .nki_kernels.res_layernorm import nki_res_layernorm_fold
39
+ from .nki_kernels.swiglu_stream import nki_swiglu_stream, nki_swiglu_stream_ln
40
+
41
+ __all__ = ["BlockWeights", "pack_block", "rope_tables", "EncoderRunner", "LNC"]
42
+
43
+ _SBUF_BUDGET = 180 * 1024 # per-partition bytes the LNC=1 resident-weights decision allows
44
+ _SW_MIN_ROWS = 256 # below this many rows per core, SwiGLU runs in torch (measured faster)
45
+ _L1_MAX_N = 1280 # LNC=1 all-group attention kernels verified up to this many tokens per
46
+ # image; beyond it (e.g. 2048-token video clips) they exceed SBUF and
47
+ # the per-group kernel + residual/LN kernel are used instead
48
+ _RESIDENT_MAX_N = 1280 # LNC=2 attention: weights staged once per launch only up to this many
49
+ # tokens per image (2048 tokens + resident weights exceed SBUF)
50
+
51
+
52
+ @dataclass
53
+ class BlockWeights:
54
+ """One transformer block, as plain fp32 CPU tensors in ``nn.Linear`` layout ([out, in])."""
55
+ ln1_w: torch.Tensor
56
+ ln1_b: torch.Tensor
57
+ ln1_eps: float
58
+ wq: torch.Tensor
59
+ bq: Optional[torch.Tensor]
60
+ wk: torch.Tensor
61
+ bk: Optional[torch.Tensor]
62
+ wv: torch.Tensor
63
+ bv: Optional[torch.Tensor]
64
+ wo: torch.Tensor
65
+ bo: Optional[torch.Tensor]
66
+ ln2_w: torch.Tensor
67
+ ln2_b: torch.Tensor
68
+ ln2_eps: float
69
+ # GELU MLP: fc1 [I, H], fc2 [H, I]. SwiGLU: gate [I, H], up [I, H], down [H, I].
70
+ fc1_w: Optional[torch.Tensor] = None
71
+ fc1_b: Optional[torch.Tensor] = None
72
+ fc2_w: Optional[torch.Tensor] = None
73
+ fc2_b: Optional[torch.Tensor] = None
74
+ gate_w: Optional[torch.Tensor] = None
75
+ gate_b: Optional[torch.Tensor] = None
76
+ up_w: Optional[torch.Tensor] = None
77
+ up_b: Optional[torch.Tensor] = None
78
+ down_w: Optional[torch.Tensor] = None
79
+ down_b: Optional[torch.Tensor] = None
80
+ ls1: Optional[torch.Tensor] = None # LayerScale (None = 1)
81
+ ls2: Optional[torch.Tensor] = None
82
+ scale: Optional[float] = None # attention scale (None = head_dim ** -0.5)
83
+
84
+
85
+ def _f(t):
86
+ return None if t is None else t.detach().float().cpu()
87
+
88
+
89
+ def _z(t, n):
90
+ return torch.zeros(n) if t is None else t
91
+
92
+
93
+ def pack_block(bw: BlockWeights, num_heads: int, rope: str, device) -> dict:
94
+ """Fold and pack one block for the kernels. ``rope`` in {"half", "interleaved"}."""
95
+ wq, wk, wv, wo = _f(bw.wq), _f(bw.wk), _f(bw.wv), _f(bw.wo)
96
+ H = wq.shape[1]
97
+ F_ = wq.shape[0]
98
+ D = F_ // num_heads
99
+ gsz = 512 // D # heads per kernel program
100
+ nh_p = ((num_heads + gsz - 1) // gsz) * gsz # padded head count
101
+ Fp = nh_p * D
102
+ ngrp = nh_p // gsz
103
+ g1, t1, g2, t2 = _f(bw.ln1_w), _f(bw.ln1_b), _f(bw.ln2_w), _f(bw.ln2_b)
104
+ bq, bk, bv = _z(_f(bw.bq), F_), _z(_f(bw.bk), F_), _z(_f(bw.bv), F_)
105
+ ls1 = _f(bw.ls1) if bw.ls1 is not None else torch.ones(H)
106
+ ls2 = _f(bw.ls2) if bw.ls2 is not None else torch.ones(H)
107
+ bo = _z(_f(bw.bo), H) * ls1
108
+ wo = wo * ls1[:, None]
109
+ padr = lambda w: torch.cat([w, w.new_zeros(Fp - w.shape[0], *w.shape[1:])], 0)
110
+ p = {"D": D, "heads": num_heads, "nh_p": nh_p, "ngrp": ngrp, "H": H,
111
+ "eps1": float(bw.ln1_eps), "eps2": float(bw.ln2_eps),
112
+ "scale": float(bw.scale if bw.scale is not None else D ** -0.5)}
113
+ p["wqk"], p["bqk"] = _ab.pack_weights(padr(wq * g1[None]), padr(wk * g1[None]),
114
+ padr(bq + wq @ t1), padr(bk + wk @ t1), nh_p, D, rope)
115
+ p["wvt"], p["bv"] = _ab.pack_v_weights(padr(wv * g1[None]), padr(bv + wv @ t1))
116
+ p["wot"] = torch.cat([wo, wo.new_zeros(H, Fp - F_)], 1).t().contiguous().to(torch.bfloat16)
117
+ p["bo"] = (bo / ngrp).reshape(1, H).contiguous().to(torch.bfloat16) # per-group partials
118
+ p["bo_full"] = bo.reshape(1, H).contiguous().to(torch.bfloat16) # all-group kernel
119
+ if bw.gate_w is not None: # SwiGLU
120
+ gw, uw, dw = _f(bw.gate_w), _f(bw.up_w), _f(bw.down_w)
121
+ I = gw.shape[0]
122
+ Ip = ((I + 127) // 128) * 128
123
+ pi = lambda t: torch.cat([t, t.new_zeros(Ip - t.shape[0], *t.shape[1:])], 0)
124
+ p["swiglu"] = True
125
+ p["s_w1t"] = pi(gw * g2[None]).t().contiguous().to(torch.bfloat16) # gate (silu)
126
+ p["s_b1"] = pi(_z(_f(bw.gate_b), I) + gw @ t2).contiguous()
127
+ p["s_wgt"] = pi(uw * g2[None]).t().contiguous().to(torch.bfloat16) # up
128
+ p["s_bg"] = pi(_z(_f(bw.up_b), I) + uw @ t2).contiguous()
129
+ p["s_w2t"] = pi((dw * ls2[:, None]).t().contiguous()).contiguous().to(torch.bfloat16)
130
+ p["s_b2"] = (_z(_f(bw.down_b), H) * ls2).contiguous()
131
+ else: # GELU MLP
132
+ w1, w2 = _f(bw.fc1_w), _f(bw.fc2_w)
133
+ p["swiglu"] = False
134
+ p["w1t"] = (w1 * g2[None]).t().contiguous().to(torch.bfloat16)
135
+ p["b1"] = (_z(_f(bw.fc1_b), w1.shape[0]) + w1 @ t2).contiguous()
136
+ p["w2t"] = (w2 * ls2[:, None]).t().contiguous().to(torch.bfloat16)
137
+ p["b2"] = (_z(_f(bw.fc2_b), H) * ls2).contiguous()
138
+ return {k: (v.to(device) if torch.is_tensor(v) else v) for k, v in p.items()}
139
+
140
+
141
+ def rope_tables(cos: torch.Tensor, sin: torch.Tensor, n_prefix: int, n_pad: int, num_heads: int,
142
+ D: int, rope: str, device) -> torch.Tensor:
143
+ """Kernel RoPE tables for a ``[n_prefix + n_rot, ...]`` token layout padded to ``n_pad`` rows.
144
+
145
+ ``cos``/``sin``: ``[n_rot, D]`` the model's own tables for the rotated tokens (prefix tokens
146
+ such as CLS/registers and padding get cos=1, sin=0). For ``rope="interleaved"`` pass the
147
+ SIGNED sin table (see ``pack_tables``).
148
+ """
149
+ C = torch.ones(n_pad, D)
150
+ S = torch.zeros(n_pad, D)
151
+ n_rot = cos.shape[0]
152
+ C[n_prefix:n_prefix + n_rot] = cos.float().cpu()
153
+ S[n_prefix:n_prefix + n_rot] = sin.float().cpu()
154
+ gsz = 512 // D
155
+ nh_p = ((num_heads + gsz - 1) // gsz) * gsz
156
+ return _ab.pack_tables(C, S, nh_p, D, rope).to(device)
157
+
158
+
159
+ class EncoderRunner:
160
+ """Runs packed blocks on a stacked ``[B*N, H]`` fp32 residual stream (rows of image b at b*N)."""
161
+
162
+ def __init__(self, packed: list):
163
+ self.L = packed
164
+ p0 = packed[0]
165
+ self.D, self.H, self.ngrp = p0["D"], p0["H"], p0["ngrp"]
166
+ self.plan = [] # (group base, grid) per LNC=2 attention call
167
+ g = 0
168
+ while g < self.ngrp:
169
+ gr = 2 if (LNC == 2 and self.ngrp - g >= 2) else 1
170
+ self.plan.append((g, gr))
171
+ g += gr
172
+
173
+ def _res(self, mod, N):
174
+ return int(mod.sbuf_estimate(self.H, self.ngrp * 512, N, self.D, True) <= _SBUF_BUDGET)
175
+
176
+ def _all_group(self, N):
177
+ return LNC == 1 and self.ngrp > 1 and N <= _L1_MAX_N
178
+
179
+ def _attn_parts(self, z1, p, tab, n_valid, B):
180
+ if self._all_group(z1.shape[0] // B):
181
+ N = z1.shape[0] // B
182
+ return _ab1.attention_block_l1(z1, p["wqk"], p["bqk"], tab, p["wvt"], p["bv"], p["wot"],
183
+ p["bo_full"], p["scale"], self.D, n_valid, B, self._res(_ab1, N))
184
+ outs = [_ab.attention_block(z1, p["wqk"], p["bqk"], tab, p["wvt"], p["bv"], p["wot"], p["bo"],
185
+ p["scale"], self.D, n_valid, hb, gr, B,
186
+ int(z1.shape[0] // B <= _RESIDENT_MAX_N))
187
+ for hb, gr in self.plan]
188
+ P = outs[0] if len(outs) == 1 else torch.cat(outs, 0)
189
+ if P.shape[0] > 4: # residual/LN kernel takes <= 4 branches
190
+ P = P.float().sum(0, keepdim=True).to(torch.bfloat16)
191
+ return P
192
+
193
+ def _ffn(self, z2, p):
194
+ if p["swiglu"]:
195
+ if z2.shape[0] // LNC >= _SW_MIN_ROWS:
196
+ return nki_swiglu_stream(z2, p["s_w1t"], p["s_b1"], p["s_wgt"], p["s_bg"],
197
+ p["s_w2t"], p["s_b2"]).unsqueeze(0)
198
+ g = torch.nn.functional.silu((z2 @ p["s_w1t"]).float() + p["s_b1"])
199
+ u = (z2 @ p["s_wgt"]).float() + p["s_bg"]
200
+ return (((g * u).to(torch.bfloat16) @ p["s_w2t"]).float() + p["s_b2"]).to(torch.bfloat16).unsqueeze(0)
201
+ return nki_mlp_k4_xp(z2, p["w1t"], p["b1"], p["w2t"], p["b2"]).unsqueeze(0)
202
+
203
+ def _ffn_fused(self, z2, p, x, eps_next):
204
+ if p["swiglu"]:
205
+ if z2.shape[0] // LNC < _SW_MIN_ROWS:
206
+ return None
207
+ return nki_swiglu_stream_ln(z2, p["s_w1t"], p["s_b1"], p["s_wgt"], p["s_bg"],
208
+ p["s_w2t"], p["s_b2"], x, eps_next)
209
+ return nki_mlp_res_ln(z2, p["w1t"], p["b1"], p["w2t"], p["b2"], x, eps_next)
210
+
211
+ def __call__(self, x: torch.Tensor, tab: torch.Tensor, n_valid: int, B: int) -> torch.Tensor:
212
+ """x: [B*N, H] fp32 (padded rows zero). Returns the pre-final-norm residual stream [B*N, H] fp32."""
213
+ H = self.H
214
+ branch = x.new_zeros(1, x.shape[0], H, dtype=torch.bfloat16)
215
+ z1 = None
216
+ nL = len(self.L)
217
+ for li, p in enumerate(self.L):
218
+ if z1 is None:
219
+ x, z1 = nki_res_layernorm_fold(x, branch, p["eps1"])
220
+ N = x.shape[0] // B
221
+ if self._all_group(N):
222
+ x, z2 = _ab1ln.attention_block_l1_ln(z1, p["wqk"], p["bqk"], tab, p["wvt"], p["bv"], p["wot"],
223
+ p["bo_full"], x, p["scale"], self.D, n_valid, p["eps2"], B,
224
+ self._res(_ab1ln, N))
225
+ else:
226
+ parts = self._attn_parts(z1, p, tab, n_valid, B)
227
+ x, z2 = nki_res_layernorm_fold(x, parts, p["eps2"])
228
+ fused = self._ffn_fused(z2, p, x, self.L[li + 1]["eps1"]) if li + 1 < nL else None
229
+ if fused is not None:
230
+ x, z1 = fused
231
+ branch = None
232
+ else:
233
+ branch = self._ffn(z2, p)
234
+ z1 = None
235
+ if branch is not None:
236
+ x = x + branch[0].float()
237
+ return x
build/torch-neuron/layers.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """KernelConfig-mappable drop-in forwards for Hugging Face Transformers ViT encoders.
2
+
3
+ ``NeuronDINOv3ViTModel`` -> ``DINOv3ViTModel`` (all DINOv3 ViT sizes: S/S+/B/L/H+; 7B needs TP)
4
+ ``NeuronVJEPA2Encoder`` -> ``VJEPA2Encoder`` (V-JEPA2 video encoder, up to 2048 tokens / clip)
5
+
6
+ Both run the whole transformer stack on the NKI kernels (``encoder.EncoderRunner``) and keep the
7
+ model's own patch embedding, RoPE position computation and final LayerNorm. Weights are folded
8
+ once, on the first device forward, from the stock module's parameters (checkpoint loading is
9
+ untouched) and cached on the module. Anything the kernels do not support (off-device input,
10
+ training mode, > 2048 tokens, unsupported head size) falls back to the stock forward.
11
+
12
+ Usage (graft, works on any Transformers version)::
13
+
14
+ import types
15
+ model.forward = types.MethodType(kernels.NeuronDINOv3ViTModel.forward, model)
16
+ model = torch.compile(model, backend="neuron")
17
+
18
+ ``torch.compile(backend="neuron")`` is part of the intended configuration; the classes set
19
+ ``can_torch_compile = True`` so the ``kernels`` library keeps them under ``Mode.TORCH_COMPILE``.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import torch
24
+
25
+ from .encoder import LNC, BlockWeights, EncoderRunner, pack_block, rope_tables
26
+
27
+ _PACK = "_vit_enc_neuron_packed"
28
+ _TABS = "_vit_enc_neuron_tabs"
29
+ _MAX_TOKENS = 2048
30
+
31
+
32
+ def _rup(n: int, m: int) -> int:
33
+ return ((n + m - 1) // m) * m
34
+
35
+
36
+ def _supported(H: int, heads: int) -> bool:
37
+ D = H // heads
38
+ return H % 128 == 0 and D in (64, 128) and H == heads * D
39
+
40
+
41
+ def _stack(hs: torch.Tensor, N: int):
42
+ B, n, H = hs.shape
43
+ x = hs.float()
44
+ if N > n:
45
+ x = torch.cat([x, x.new_zeros(B, N - n, H)], 1)
46
+ return x.reshape(B * N, H).contiguous()
47
+
48
+
49
+ # ------------------------------------------------------------------------------------------------
50
+ # DINOv3
51
+ # ------------------------------------------------------------------------------------------------
52
+ def _dinov3_layers(model):
53
+ enc = getattr(model, "model", None) # newer Transformers: model.model.layer
54
+ return enc.layer if enc is not None and hasattr(enc, "layer") else model.layer
55
+
56
+
57
+ def _dinov3_block(lyr) -> BlockWeights:
58
+ a = lyr.attention
59
+ bw = BlockWeights(
60
+ ln1_w=lyr.norm1.weight, ln1_b=lyr.norm1.bias, ln1_eps=lyr.norm1.eps,
61
+ wq=a.q_proj.weight, bq=a.q_proj.bias, wk=a.k_proj.weight, bk=a.k_proj.bias,
62
+ wv=a.v_proj.weight, bv=a.v_proj.bias, wo=a.o_proj.weight, bo=a.o_proj.bias,
63
+ ln2_w=lyr.norm2.weight, ln2_b=lyr.norm2.bias, ln2_eps=lyr.norm2.eps,
64
+ ls1=lyr.layer_scale1.lambda1, ls2=lyr.layer_scale2.lambda1, scale=float(a.scaling))
65
+ m = lyr.mlp
66
+ if hasattr(m, "gate_proj"):
67
+ bw.gate_w, bw.gate_b = m.gate_proj.weight, m.gate_proj.bias
68
+ bw.up_w, bw.up_b = m.up_proj.weight, m.up_proj.bias
69
+ bw.down_w, bw.down_b = m.down_proj.weight, m.down_proj.bias
70
+ else:
71
+ bw.fc1_w, bw.fc1_b = m.up_proj.weight, m.up_proj.bias
72
+ bw.fc2_w, bw.fc2_b = m.down_proj.weight, m.down_proj.bias
73
+ return bw
74
+
75
+
76
+ def _dinov3_ok(model) -> bool:
77
+ c = model.config
78
+ if not _supported(c.hidden_size, c.num_attention_heads):
79
+ return False
80
+ if getattr(c, "use_gated_mlp", False):
81
+ return getattr(c, "hidden_act", "silu") == "silu"
82
+ return getattr(c, "hidden_act", "gelu") == "gelu"
83
+
84
+
85
+ @torch._dynamo.disable
86
+ def _dinov3_freeze(model):
87
+ if model.training:
88
+ raise RuntimeError("NeuronDINOv3ViTModel: these kernels have no autograd rule; call model.eval() first.")
89
+ dev = model.norm.weight.device
90
+ heads = model.config.num_attention_heads
91
+ packed = [pack_block(_dinov3_block(l), heads, "half", dev) for l in _dinov3_layers(model)]
92
+ setattr(model, _PACK, EncoderRunner(packed))
93
+ setattr(model, _TABS, {})
94
+
95
+
96
+ @torch._dynamo.disable
97
+ def _dinov3_tab(model, pixel_values, n_prefix, N):
98
+ key = (tuple(pixel_values.shape[-2:]), N)
99
+ tabs = getattr(model, _TABS)
100
+ if key not in tabs:
101
+ cos, sin = model.rope_embeddings(pixel_values[:1].float()) # [n_patches, D] each
102
+ c = model.config
103
+ tabs[key] = rope_tables(cos, sin, n_prefix, N, c.num_attention_heads,
104
+ c.hidden_size // c.num_attention_heads, "half", model.norm.weight.device)
105
+ return tabs[key]
106
+
107
+
108
+ class NeuronDINOv3ViTModel(torch.nn.Module):
109
+ """Drop-in forward for ``transformers.DINOv3ViTModel`` (bf16 NKI encoder, fp32 residual stream).
110
+
111
+ Returns ``last_hidden_state`` (final-normed, ``[B, 1 + registers + patches, H]``) and
112
+ ``pooler_output`` (CLS), like the stock model. Any batch size; images up to 2048 tokens
113
+ (e.g. 720x720 at patch 16).
114
+ """
115
+
116
+ can_torch_compile = True
117
+ has_backward = False
118
+
119
+ def forward(self, pixel_values, bool_masked_pos=None, head_mask=None, **kwargs):
120
+ from transformers.modeling_outputs import BaseModelOutputWithPooling
121
+ pv = pixel_values.to(self.embeddings.patch_embeddings.weight.dtype)
122
+ hs = self.embeddings(pv, bool_masked_pos=bool_masked_pos) # [B, n, H]
123
+ B, n, H = hs.shape
124
+ N = _rup(n, 128 * LNC)
125
+ if (hs.device.type != "neuron" or self.training or head_mask is not None or N > _MAX_TOKENS
126
+ or not _dinov3_ok(self)):
127
+ import transformers.models.dinov3_vit.modeling_dinov3_vit as mv
128
+ if head_mask is not None:
129
+ kwargs["head_mask"] = head_mask
130
+ return mv.DINOv3ViTModel.forward(self, pixel_values, bool_masked_pos=bool_masked_pos, **kwargs)
131
+ if not hasattr(self, _PACK):
132
+ _dinov3_freeze(self)
133
+ n_prefix = 1 + self.config.num_register_tokens
134
+ tab = _dinov3_tab(self, pixel_values, n_prefix, N)
135
+ x = getattr(self, _PACK)(_stack(hs, N), tab, n, B)
136
+ x = x.reshape(B, N, H)[:, :n]
137
+ out = self.norm(x.to(self.norm.weight.dtype))
138
+ return BaseModelOutputWithPooling(last_hidden_state=out, pooler_output=out[:, 0, :])
139
+
140
+
141
+ # ------------------------------------------------------------------------------------------------
142
+ # V-JEPA2
143
+ # ------------------------------------------------------------------------------------------------
144
+ def _vjepa2_block(lyr) -> BlockWeights:
145
+ a = lyr.attention
146
+ return BlockWeights(
147
+ ln1_w=lyr.norm1.weight, ln1_b=lyr.norm1.bias, ln1_eps=lyr.norm1.eps,
148
+ wq=a.query.weight, bq=a.query.bias, wk=a.key.weight, bk=a.key.bias,
149
+ wv=a.value.weight, bv=a.value.bias, wo=a.proj.weight, bo=a.proj.bias,
150
+ ln2_w=lyr.norm2.weight, ln2_b=lyr.norm2.bias, ln2_eps=lyr.norm2.eps,
151
+ fc1_w=lyr.mlp.fc1.weight, fc1_b=lyr.mlp.fc1.bias, fc2_w=lyr.mlp.fc2.weight, fc2_b=lyr.mlp.fc2.bias,
152
+ scale=float(a.scaling))
153
+
154
+
155
+ @torch._dynamo.disable
156
+ def _vjepa2_tab(encoder, N, n):
157
+ """Signed-sin 3D RoPE tables from the model's own position ids (interleaved pairing)."""
158
+ a = encoder.layer[0].attention
159
+ H, D = a.all_head_size, a.attention_head_size
160
+ dev = a.query.weight.device
161
+ pos = a.get_position_ids(torch.zeros(1, n, H, device=dev), masks=None)
162
+ to1d = lambda t: (t if t.dim() == 1 else t.reshape(-1, n)[0]).float().cpu()
163
+ cos = torch.ones(n, D)
164
+ sin = torch.zeros(n, D)
165
+ s = 0
166
+ for sub, p in zip((a.d_dim, a.h_dim, a.w_dim), (to1d(pos[0]), to1d(pos[1]), to1d(pos[2]))):
167
+ half = sub // 2
168
+ omega = 1.0 / (10000.0 ** (torch.arange(half, dtype=torch.float32) / float(half)))
169
+ f = p[:, None] * omega
170
+ c, sn = f.cos(), f.sin()
171
+ cos[:, s:s + half] = c; cos[:, s + half:s + sub] = c
172
+ sin[:, s:s + half] = sn; sin[:, s + half:s + sub] = sn
173
+ s += sub
174
+ g = torch.ones(D); g[0::2] = -1.0
175
+ return rope_tables(cos, sin * g, 0, N, H // D, D, "interleaved", dev)
176
+
177
+
178
+ @torch._dynamo.disable
179
+ def _vjepa2_freeze(encoder):
180
+ if encoder.training:
181
+ raise RuntimeError("NeuronVJEPA2Encoder: these kernels have no autograd rule; call model.eval() first.")
182
+ a = encoder.layer[0].attention
183
+ dev = a.query.weight.device
184
+ heads = a.all_head_size // a.attention_head_size
185
+ setattr(encoder, _PACK, EncoderRunner([pack_block(_vjepa2_block(l), heads, "interleaved", dev)
186
+ for l in encoder.layer]))
187
+ setattr(encoder, _TABS, {})
188
+
189
+
190
+ class NeuronVJEPA2Encoder(torch.nn.Module):
191
+ """Drop-in forward for ``transformers.VJEPA2Encoder``.
192
+
193
+ Any batch size; clips up to 2048 tokens (e.g. 16 frames at 256 px, tubelet 2, patch 16).
194
+ """
195
+
196
+ can_torch_compile = True
197
+ has_backward = False
198
+
199
+ def forward(self, pixel_values_videos=None, head_mask=None, output_attentions=False,
200
+ output_hidden_states=False, **kwargs):
201
+ from transformers.modeling_outputs import BaseModelOutput
202
+ hs = self.embeddings(pixel_values_videos) # [B, n, H]
203
+ B, n, H = hs.shape
204
+ N = _rup(n, 128 * LNC)
205
+ a = self.layer[0].attention
206
+ if (hs.device.type != "neuron" or self.training or head_mask is not None or output_attentions
207
+ or output_hidden_states or N > _MAX_TOKENS
208
+ or not _supported(H, a.all_head_size // a.attention_head_size)):
209
+ import transformers.models.vjepa2.modeling_vjepa2 as mv
210
+ return mv.VJEPA2Encoder.forward(self, pixel_values_videos, head_mask=head_mask,
211
+ output_attentions=output_attentions,
212
+ output_hidden_states=output_hidden_states, **kwargs)
213
+ if not hasattr(self, _PACK):
214
+ _vjepa2_freeze(self)
215
+ tabs = getattr(self, _TABS)
216
+ if N not in tabs:
217
+ tabs[N] = _vjepa2_tab(self, N, n)
218
+ x = getattr(self, _PACK)(_stack(hs, N), tabs[N], n, B)
219
+ out = self.layernorm(x.reshape(B, N, H)[:, :n])
220
+ return BaseModelOutput(last_hidden_state=out)
build/torch-neuron/metadata.json ADDED
@@ -0,0 +1 @@
 
 
1
+ { "name": "vit-encoder-neuron-kernels", "id": "vit_encoder_neuron_kernels", "version": 1, "license": "Apache-2.0", "backend": {"type": "neuron", "archs": []}, "python-depends": [] }
build/torch-neuron/nki_kernels/__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The NKI kernels and their host-side weight packing."""
2
+
3
+ from .attention_block import (
4
+ attention_block,
5
+ lane_permutation,
6
+ pack_out_weights,
7
+ pack_tables,
8
+ pack_v_weights,
9
+ pack_weights,
10
+ )
11
+ from .attention_block_l1 import attention_block_l1
12
+ from .attention_block_l1_ln import attention_block_l1_ln
13
+ from .mlp import nki_mlp_k4_xp as mlp
14
+ from .mlp_res_ln import nki_mlp_res_ln as mlp_res_ln
15
+ from .res_layernorm import nki_res_layernorm_fold as res_layernorm_fold
16
+ from .swiglu_stream import nki_swiglu_stream as swiglu_stream
17
+ from .swiglu_stream import nki_swiglu_stream_ln as swiglu_stream_ln
18
+
19
+ __all__ = [
20
+ "attention_block", "attention_block_l1", "attention_block_l1_ln", "res_layernorm_fold",
21
+ "mlp", "mlp_res_ln", "swiglu_stream", "swiglu_stream_ln",
22
+ "pack_weights", "pack_tables", "pack_v_weights", "pack_out_weights", "lane_permutation",
23
+ ]
build/torch-neuron/nki_kernels/attention_block.py ADDED
@@ -0,0 +1,801 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """K2 outer: fused QKV projection + 3D RoPE + vendor attention + fused out-projection, in ONE
2
+ top-level NKI kernel, FLATTENED from ``nki_attn_block_k2v_h10`` (which forks
3
+ ``nki_attn_block_k2v_dp4``). Self-contained: imports only ``nki``, ``nkilib``, ``torch``, and
4
+ ``_attention_cte`` from the sibling ``attention_inner`` -- NO imports from ``experiment/`` or
5
+ the repo-root incumbent modules.
6
+
7
+ WHAT THIS COMPUTES (per core; LNC=2, this core owns 8 of the 16 heads)
8
+ ======================================================================
9
+ ``result[LNC, N, H] bf16`` = out-projection PARTIALS of
10
+ ``attention(rope(h@Wq), rope(h@Wk), h@Wv)``. Each core computes a partial sum over its 8 heads'
11
+ 512 out-projection contraction rows; K3's residual add sums the two cores' partials.
12
+
13
+ Phase 1 (producer): dma_transpose h -> x_t; project Q/K (lane-permuted) and V; apply the six
14
+ rope ``tensor_tensor`` ops; scatter K into ``k_tiles`` (SBUF), Q into ``q_priv`` (private_hbm),
15
+ V into ``v_tiles`` (SBUF).
16
+ Phase 2 (attention + out-proj): reset the allocator to reclaim the dead producer buffers, call
17
+ ``_attention_cte`` per local head into feature-major SBUF O slices, then contract the fused
18
+ out-projection (tokens on partitions) and DMA token-major to ``result``.
19
+
20
+ PSUM BANK PLAN (producer): V uses bank 7 (``(0, 7*_PSUM_BANK)``); Q/K parities use banks 5/6
21
+ (``(5+par)*_PSUM_BANK``); the fused out-projection uses bank 0 (``(0, 0)``). The vendor
22
+ attention (``attention_inner``) pins its own low banks {0-3} for MM1 and {4-7} for MM2 during
23
+ its phase, which runs after the producer's high-water mark, so there is no live overlap.
24
+
25
+ THE TWO ENGINE PINS (both to Scalar), and WHY (from PERF_INVENTORY)
26
+ -------------------------------------------------------------------
27
+ * ``:276`` V-scatter ``tensor_copy`` -> SCALAR (H9's inherited mechanism): eight copies read
28
+ one ``vout`` before the next vsub overwrites it (a WAR chain of 8); Scalar (240 ns) beats
29
+ GpSimd (375 ns) for that chain and sheds the copy off Vector, K2's binding engine.
30
+ * ``:458`` QK bias-add-and-evict ``tensor_scalar`` -> SCALAR (H10's mechanism): the largest
31
+ remaining Vector-resident relocatable site (768/core); ``data=ps`` is PSUM so GpSimd is
32
+ illegal, and Scalar's 612 ns fee is faster than the 744 ns Vector cell it replaces, so the
33
+ zero-lead rope chain that consumes ``proj`` cannot lengthen.
34
+ The ``:224`` bias-cast ``tensor_copy`` and ``:483`` K-scatter ``tensor_copy`` stay UNPINNED
35
+ (compiler default) -- pinning them was dropped (:224 is 0.09% of the window; :483 has no
36
+ affordable landing engine), and they MUST stay unpinned to match the shipping stream.
37
+
38
+ HOST-SIDE FOLD ALGEBRA (packing helpers below, copied flat)
39
+ -----------------------------------------------------------
40
+ Affine-free LN1 folds gamma1/beta1 into the K2 weights at freeze time:
41
+ ``pack_weights(wq*g1, wk*g1, bq+wq@t1, bk+wk@t1, ...)`` and ``pack_v_weights(wv*g1, bv+wv@t1)``.
42
+ The lane permutation puts each quad-of-heads' even/odd RoPE lanes in their own 128-partition
43
+ group so RoPE is six full-width ``tensor_tensor`` ops with no transposes; it cancels in
44
+ attention's Q/K contraction (a dot product is invariant under a shared permutation of the
45
+ contraction axis), so V is natural and nothing downstream un-permutes.
46
+ LNC2 BIAS HALVING: ``pack_out_weights`` halves the out-projection bias (``bo*0.5``) so the two
47
+ cores' partials sum to exactly one bias -- halving is bitwise exact in bf16 (it only decrements
48
+ the exponent, and summing two identical bf16 values is exact).
49
+
50
+ INFERENCE ONLY: no autograd; the packed weights are detached caches.
51
+
52
+ SBUF BUDGET (bytes/lane; budget = 192 KiB = 196608 B/lane), TWO GEOMETRIES
53
+ ==========================================================================
54
+ N=2048 (16-frame clips) is the SHIPPING path: nothing below is gated on, so buffer shapes,
55
+ byte layout, DMA target addresses and fill VALUES are BIT-FOR-BIT the incumbent (verified:
56
+ torch.equal(perf_out.pt) == True, max abs diff 0.0). N=8192 (64-frame clips) is a SEPARATE,
57
+ gated path (``if N > 2048``) that the N<=2048 forward never enters.
58
+
59
+ | N=2048 (16f) | N=8192 (64f, gated)
60
+ ------------------------------ + -------------- + --------------------------------------
61
+ k_tiles (8 heads resident) | 32768 | 0 (streamed to private_hbm)
62
+ v_tiles (8 heads resident) | 16384 | 0 (streamed to private_hbm)
63
+ k_stage / v_stage (1 head) | - | 16384 + 8192 = 24576 (one head at a time)
64
+ x_t (hidden^T ring) | 32768 | 32768 (depth-4 token-block ring, was 131072)
65
+ tab (rope tables ring) | 32768 | 32768 (depth-4 token-block ring, was 131072)
66
+ producer working set | ~25000 | ~25000
67
+ ------------------------------ + -------------- + --------------------------------------
68
+ PHASE-1 PEAK | ~124 KiB | ~106 KiB (FITS)
69
+ ------------------------------ + -------------- + --------------------------------------
70
+ phase2_base (persistent K/V) | 49152 | 24576 (only k_stage/v_stage persist)
71
+ wo_sb + bo_bc + o_feat + out | 29696 | 78848
72
+ vendor inner (per head) | ~45 KiB | ~251 KiB (single 8192 section, n_large=4:
73
+ | | exp_tp_sb 131072 + mm1_masked 65536 +
74
+ | | exp_sb 32768 balloon -- see below)
75
+ ------------------------------ + -------------- + --------------------------------------
76
+ PHASE-2 PEAK | ~125 KiB | ~354912 B/lane (DOES NOT FIT -- measured
77
+ | | on device [NCC_INLA001] 128x354912, 1.81x
78
+ | | the 196608 budget => N>2048 is REFUSED)
79
+
80
+ WHY N>2048 IS REFUSED. At N=8192 the shipping design has TWO independent SBUF overflows
81
+ (both confirmed on device as ``[NCC_INLA001] ... out of bound 128x526944``):
82
+ (1) PHASE 1: the four N-scaled residents k_tiles(131072)+v_tiles(65536)+x_t(131072)+
83
+ tab(131072) = 458752 B/lane coexist -- 2.3x the whole budget.
84
+ (2) PHASE 2: eight heads' K/V resident (196608 B/lane) is the ENTIRE budget.
85
+ The phase-1 fixes work (x_t/tab depth-4 token-block rings, bit-for-bit at N<=2048, saving
86
+ 196608 B/lane; K/V streamed through private_hbm one head at a time) => phase-1 peak ~106 KiB.
87
+ But PHASE 2 does NOT fit: handing one head all 8192 keys makes the single-section flatten run
88
+ with num_large_tiles_per_section=ceil(8192/2048)=4, ballooning three inner buffers (exp_tp_sb
89
+ 131072 + mm1_masked 65536 + exp_sb 32768 = 229376 B/lane) -> phase-2 peak measured 354912 B/lane,
90
+ 1.81x the budget. The shrinkable phase-2 region (o_feat/out/wo/bo = 78848) is smaller than the
91
+ 158304 overflow, so streaming O or shrinking o_feat cannot close the gap; re-enabling flash
92
+ sectioning fits but is numerically wrong (rel_l2 0.90, the V-JEPA2 kernels' verification notes), and
93
+ editing the inner would perturb the bit-for-bit 16-frame path. So the host wrapper
94
+ ``attention_block`` raises ``NotImplementedError`` for N>2048 rather than emit a non-fitting or
95
+ wrong kernel (see the V-JEPA2 kernels' verification notes). The N<=2048 path is bit-for-bit unchanged.
96
+ """
97
+ from __future__ import annotations
98
+
99
+ import torch
100
+ from nki.framework.kernel import wrap_nki_to_torch_hop
101
+ from torch_neuronx import nki_op
102
+
103
+ import nki
104
+ import nki.isa as nisa
105
+ import nki.language as nl
106
+ from nki.isa import engine
107
+
108
+ from nkilib.core.utils.modular_allocator import ModularAllocator
109
+
110
+ # The vendor attention, flattened to this model's reached path with the h8/h10 pins inlined.
111
+ # The N<=2048 path calls THIS single-section flatten; the N>2048 path is REFUSED (see below).
112
+ # seqlen_k=8192 < the vendor's _FLASH_ATTENTION_THRESHOLD (10240), so num_sections==1 -- the
113
+ # SAME single-section, non-flash code path the verified 16-frame configuration uses (no
114
+ # multi-section online-softmax combine, which is numerically wrong here: the V-JEPA2 kernels' verification notes).
115
+ # BUT single-section at 8192 does NOT fit: the inner's mm1_masked / exp_sb / exp_tp_sb are
116
+ # allocated with num_free_tiles carrying num_large_tiles_per_section = ceil(8192/2048) = 4
117
+ # (attention_inner.py:658-701), so those three buffers BALLOON with total_seqlen_k -- they are
118
+ # NOT reused across the large-tile loop. exp_tp_sb 131072 + mm1_masked 65536 + exp_sb 32768 =
119
+ # 229376 B/lane, driving phase-2 peak to ~354912 B/lane (1.81x the 196608 budget; measured on
120
+ # device [NCC_INLA001] 128x354912). See the SBUF table and refusal rationale below.
121
+ from .attention_inner import _attention_cte
122
+
123
+ import os as _os
124
+ #: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
125
+ LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
126
+ _SPLIT = 2 # heads are always split in two halves (8 heads per program)
127
+ _P_MAX = 128
128
+ _PSUM_FMAX = 512
129
+ _PSUM_BANK = 2048 # bytes per partition per bank
130
+ _K_TILE = 512 # vendor k_cache_sbuf tiles are (d, 512)
131
+ _V_TILE = 128 # vendor v_cache_sbuf tiles are (128, d)
132
+
133
+
134
+ def _kernel_assert(cond: bool, msg: str):
135
+ assert cond, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {msg}"
136
+
137
+
138
+ # ==========================================================================================
139
+ # Host-side packing helpers (copied flat from nki_qk_fused_2core / nki_attn_block_k2v_dp4).
140
+ # All are frozen-weight work, cached once; INFERENCE ONLY.
141
+ # ==========================================================================================
142
+ def lane_permutation(heads: int, D: int, rope: str = "half") -> torch.Tensor:
143
+ """Column order that puts each quad-of-heads' RoPE pair halves in their own 128-row group.
144
+
145
+ RoPE rotates pairs of lanes ``(a, b)`` of each head. ``rope`` picks the pairing:
146
+ * ``"half"`` -- (j, j + D/2), the rotate-half convention (DINOv3, EVA-02, HF Llama style).
147
+ * ``"interleaved"`` -- (2j, 2j + 1), adjacent lanes (V-JEPA2 / GPT-NeoX-interleaved style).
148
+ The projection's output columns are reordered so group ``2a`` holds the FIRST lane of every
149
+ pair for heads ``4a..4a+3`` (at D=64; 2 groups per 4 heads at D=128) and group ``2a+1`` the
150
+ SECOND lane, so RoPE is six full-width elementwise ops with no transposes. The permutation
151
+ cancels in the Q/K contraction, so nothing downstream un-permutes.
152
+
153
+ Returns ``perm`` of length ``heads*D`` s.t. ``w.t()[:, perm]`` is the reordered weight.
154
+ """
155
+ _kernel_assert(rope in ("half", "interleaved"), f"rope must be 'half' or 'interleaved', got {rope!r}")
156
+ half = D // 2
157
+ quad = _P_MAX // half
158
+ _kernel_assert(quad * half == _P_MAX, f"D={D} gives half={half}, needs quad*half=128")
159
+ _kernel_assert(heads % quad == 0, f"heads={heads} must be divisible by quad={quad}")
160
+ perm = torch.empty(heads * D, dtype=torch.long)
161
+ for hd in range(heads):
162
+ a, b = hd // quad, hd % quad
163
+ for j in range(half):
164
+ lo, hi = (j, half + j) if rope == "half" else (2 * j, 2 * j + 1)
165
+ perm[(2 * a) * _P_MAX + b * half + j] = hd * D + lo
166
+ perm[(2 * a + 1) * _P_MAX + b * half + j] = hd * D + hi
167
+ return perm
168
+
169
+
170
+ def pack_weights(wq: torch.Tensor, wk: torch.Tensor,
171
+ bq: torch.Tensor | None, bk: torch.Tensor | None,
172
+ heads: int, D: int, rope: str = "half"):
173
+ """``(w_packed [2,H,H] bf16, b_packed [2,H,1] bf16)`` -- transposed and column-permuted.
174
+
175
+ FOLD: in the affine-free-LN1 freeze this is called on gamma-folded inputs
176
+ ``pack_weights(wq*g1, wk*g1, bq+wq@t1, bk+wk@t1, heads, D)``. ``wq``/``wk`` are torch
177
+ ``Linear.weight`` ``[H_out, H_in]``; the kernel wants ``[H_in, H_out]`` with ``H_out``
178
+ permuted, i.e. ``w.t()[:, perm]``. The bias is shaped ``[2,H,1]`` (not ``[2,H]``) so the
179
+ kernel can slice a ``[128,1]`` tile straight out of HBM.
180
+ """
181
+ H = wq.shape[0]
182
+ perm = lane_permutation(heads, D, rope).to(wq.device)
183
+ w = torch.stack([wq.detach().t()[:, perm].contiguous(),
184
+ wk.detach().t()[:, perm].contiguous()], 0)
185
+ zero = torch.zeros(H, dtype=wq.dtype, device=wq.device)
186
+ b = torch.stack([(zero if bq is None else bq.detach())[perm].contiguous(),
187
+ (zero if bk is None else bk.detach())[perm].contiguous()], 0)
188
+ b = b.unsqueeze(-1)
189
+ return w.to(torch.bfloat16).contiguous(), b.to(torch.bfloat16).contiguous()
190
+
191
+
192
+ def pack_tables(cos: torch.Tensor, sin: torch.Tensor, heads: int, D: int, rope: str = "half"):
193
+ """``tab [4, 128, N] fp32`` RoPE tables, quad-tiled to match ``lane_permutation(..., rope)``.
194
+
195
+ Inputs are the model's own ``[N, D]`` cos / sin tables (shared by every head; prefix tokens that
196
+ take no rotation: cos=1, sin=0). With ``(a, b)`` a lane pair the kernel computes
197
+ ``a' = a*tab0 + b*tab2`` and ``b' = b*tab1 + a*tab3``, so:
198
+ * ``"half"``: ``sin`` is the PLAIN sin table (rotate-half: ``x*cos + rotate_half(x)*sin``);
199
+ tab = (cos_lo, cos_hi, -sin_lo, +sin_hi).
200
+ * ``"interleaved"``: ``sin`` is the SIGNED sin table (``-sin`` on even lanes, ``+sin`` on odd);
201
+ tab = (cos_even, cos_odd, sinsigned_even, sinsigned_odd).
202
+ """
203
+ _kernel_assert(rope in ("half", "interleaved"), f"rope must be 'half' or 'interleaved', got {rope!r}")
204
+ half = D // 2
205
+ quad = _P_MAX // half
206
+ if rope == "half":
207
+ parts = [cos[:, :half], cos[:, half:], -sin[:, :half], sin[:, half:]]
208
+ else:
209
+ parts = [cos[:, 0::2], cos[:, 1::2], sin[:, 0::2], sin[:, 1::2]]
210
+ tab = torch.stack([p.t().contiguous().repeat(quad, 1) for p in parts], 0)
211
+ return tab.to(torch.float32).contiguous()
212
+
213
+
214
+ def pack_v_weights(wv: torch.Tensor, bv: torch.Tensor | None):
215
+ """``(wv_t [H, H] bf16, bv [1, H] bf16)``. V is NOT lane-permuted (natural order).
216
+
217
+ FOLD: in the affine-free-LN1 freeze this is called on gamma-folded inputs
218
+ ``pack_v_weights(wv*g1, bv+wv@t1)``.
219
+ """
220
+ H = wv.shape[0]
221
+ zero = torch.zeros(H, dtype=wv.dtype, device=wv.device)
222
+ return (wv.detach().t().contiguous().to(torch.bfloat16),
223
+ (bv.detach() if bv is not None else zero).reshape(1, H).contiguous().to(torch.bfloat16))
224
+
225
+
226
+ def pack_out_weights(wo, bo):
227
+ """``(wo_t [H, H] bf16, bo/2 [1, H] bf16)`` for the fused out projection.
228
+
229
+ ``wo`` is ``attn.proj.weight`` [H, H] (attention features -> hidden). The kernel contracts
230
+ over attention features, so it wants those on partitions: ``wo.T`` = [features, hidden].
231
+
232
+ LNC2 BIAS HALVING. Both cores add ``bo/2`` and K3's partial sum restores ``bo``. Adding the
233
+ full bias on both would double it; adding it on one core only would need ``program_id`` as an
234
+ arithmetic operand (it is symbolic -- neither a compile-time constant nor a [P,1] tile).
235
+ Halving is bitwise exact in bf16: it only decrements the exponent, and summing two identical
236
+ bf16 values is exact.
237
+ """
238
+ import torch as _t
239
+ Hh = wo.shape[0]
240
+ zero = _t.zeros(Hh, dtype=wo.dtype, device=wo.device)
241
+ half = (bo.detach() if bo is not None else zero) * 0.5
242
+ return (wo.detach().t().contiguous().to(_t.bfloat16),
243
+ half.reshape(1, Hh).contiguous().to(_t.bfloat16))
244
+
245
+
246
+ # ==========================================================================================
247
+ # THE KERNEL. Flattened from _attn_block_k2v_h10_dp_kernel (verbatim dp4 body with the two
248
+ # engine= keywords hardcoded to engine.scalar at :276 and :458; :224 and :483 left unpinned).
249
+ # ==========================================================================================
250
+ @nki.jit
251
+ def _attention_block_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_hbm,
252
+ wot_hbm, bo_hbm, scale, head_dim, n_valid, hb, grid, nimg=1, resident=0):
253
+ """out[16, N, D] bf16 = attention(rope(h@Wq), rope(h@Wk), h@Wv), one kernel (per-core partials).
254
+
255
+ BATCHED (nimg>1): h_hbm is the STACKED [nimg*N, H] buffer (images back to back, N rows each).
256
+ The per-image body below runs once per image inside this ONE launch, reading image b's rows at
257
+ row offset b*N and writing partials to result[slot, b*N:(b+1)*N]. ONE launch, no host-side
258
+ slice/cat glue.
259
+ resident=0: the loop wraps the whole body, so weights/biases/rope tables are re-DMA'd per image.
260
+ resident=1 (N<=2048): Q/K/V/O weights, biases and rope tables are staged ONCE at the bottom of
261
+ SBUF before the image loop (~38 KiB/lane ViT-L + tables); each image's buffers are allocated
262
+ above them. Same math, bit-identical outputs."""
263
+ NT, H = h_hbm.shape
264
+ N = NT // nimg
265
+ Fp = wqk_hbm.shape[2] # padded attention features = 512 * n_groups
266
+ D = head_dim
267
+ half = D // 2
268
+ quad = _P_MAX // half # heads per 128-partition parity group = 4
269
+ heads = Fp // D # padded head count (multiple of 8)
270
+ local_heads = _PSUM_FMAX // D # heads per program: 8 at D=64, 4 at D=128
271
+ local_quads = local_heads // quad # 2
272
+ n_chunks = H // _P_MAX # 8 contraction chunks
273
+ n_tt = (N + _PSUM_FMAX - 1) // _PSUM_FMAX # token tiles of 512 (last may be ragged)
274
+ n_ktiles = (N + _K_TILE - 1) // _K_TILE # K tiles / head (last may be partially filled)
275
+ n_vtiles = N // _V_TILE # 16 V tiles / head
276
+ n_ochunks = (local_heads * D) // _P_MAX # feature chunks of the out projection
277
+ v_per_tt = _PSUM_FMAX // _V_TILE # 4 V tiles per 512-token tile
278
+
279
+ # N>2048 gate (see the phase-1/phase-2 note below). Computed early so the K/V residents can
280
+ # be allocated as HBM (streamed) for the big path.
281
+ _big = N > _PSUM_FMAX * 4
282
+
283
+ # N>2048 IS NOT SUPPORTED (guarded at the host wrapper `attention_block` below, which raises
284
+ # NotImplementedError before this kernel is ever traced -- NKI kernels cannot `raise`). The
285
+ # phase-1 memory fixes (x_t/tab depth-4 rings, K/V streamed through private_hbm one head at a
286
+ # time) DO fit -- phase-1 peak ~106 KiB/lane. But phase 2 does NOT: handing one head all 8192
287
+ # keys makes the flattened single-section _attention_cte run with num_large_tiles_per_section
288
+ # =ceil(8192/2048)=4, which balloons three N-scaled inner buffers -- exp_tp_sb (8*4*4*512*2 =
289
+ # 131072), mm1_masked (2*4*2048*4 = 65536), exp_sb (2*4*2048*2 = 32768) = 229376 B/lane for
290
+ # three buffers alone. Measured on device (2026-09-25, neuronx-cc 2.25): [NCC_INLA001] out of
291
+ # bound 128x354912 -> phase-2 peak 354912 B/lane, 1.81x the 196608 budget. The shrinkable
292
+ # phase-2 region (o_feat 65536 + out 1024 + wo/bo 12288 = 78848) is smaller than the 158304
293
+ # overflow, so streaming O or shrinking o_feat CANNOT close the gap -- the inner's N-scaled
294
+ # buffers are the wall. Re-enabling flash sectioning (2048-key sections) shrinks the inner but
295
+ # activates the vendor's multi-section online-softmax combine, a path the shipping config never
296
+ # exercises and which produced rel_l2 0.90 (wrong). See the V-JEPA2 kernels' verification notes.
297
+
298
+ _kernel_assert(wqk_hbm.shape == (2, H, Fp), f"wqk must be [2,{H},{Fp}]")
299
+ _kernel_assert(bqk_hbm.shape == (2, Fp, 1), f"bqk must be [2,{Fp},1]")
300
+ _kernel_assert(tab_hbm.shape == (4, _P_MAX, N), f"tab must be [4,{_P_MAX},{N}]")
301
+ _kernel_assert(wvt_hbm.shape == (H, Fp) and bv_hbm.shape == (1, Fp), "bad V weights")
302
+ _kernel_assert(wot_hbm.shape == (Fp, H) and bo_hbm.shape == (1, H), "bad O weights")
303
+ _kernel_assert(H % _P_MAX == 0, f"H={H} must be a multiple of 128")
304
+ _kernel_assert(quad * half == _P_MAX, f"D={D} must give 4 heads per group")
305
+ _kernel_assert(Fp % _PSUM_FMAX == 0, "padded features must be a multiple of 512")
306
+ _kernel_assert(local_heads * D == _PSUM_FMAX,
307
+ "inline V projection wants all local heads in one 512-wide stream")
308
+ _kernel_assert(N % _P_MAX == 0, f"N={N} must be a multiple of 128")
309
+ _kernel_assert(NT == nimg * N, f"rows {NT} must be nimg({nimg}) x N")
310
+ _kernel_assert(str(h_hbm.dtype) == str(nl.bfloat16), "h must be bf16")
311
+
312
+ # [LNC, N, H] PARTIALS. The out projection contracts over all H features == all 16 heads,
313
+ # and this core owns 8, so what it computes is a partial sum; K3 adds the two.
314
+ result = nl.ndarray((grid, NT, H), dtype=nl.bfloat16, buffer=nl.shared_hbm)
315
+
316
+ # Each program handles one 8-head group: group = hb + program_id. grid==1 -> single program.
317
+ if grid == 1:
318
+ pid = hb
319
+ out_slot = 0
320
+ else:
321
+ out_slot = nl.program_id(0) # SYMBOLIC: index arithmetic only
322
+ pid = hb + out_slot
323
+ head0 = pid * local_heads
324
+ col0 = pid * (local_heads * D) # first permuted qk weight column
325
+ vcol0 = pid * (local_heads * D) # first natural v weight column (same offset)
326
+
327
+ # ---- RESIDENT MODE (resident=1, N<=2048 only): stage the per-launch-invariant inputs ONCE,
328
+ # at the BOTTOM of SBUF, before the image loop. Everything per image is then allocated above
329
+ # `res_top`. Invariant: Q/K/V/O weights, their biases, and the rope tables (same for every image).
330
+ # Cost ~38 KiB/lane (ViT-L) + tables; per-image phase peaks were <=106 KiB, budget 192 KiB.
331
+ _res = bool(resident) and not (N > _PSUM_FMAX * 4)
332
+ res_top = 0
333
+ if _res:
334
+ ra = ModularAllocator(initial_address=0)
335
+ r_w_sb = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2 * _P_MAX), dtype=nl.bfloat16,
336
+ block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
337
+ for qk in range(2):
338
+ for c in range(n_chunks):
339
+ nisa.dma_copy(dst=r_w_sb[qk][c],
340
+ src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX, col0:col0 + local_quads * 2 * _P_MAX])
341
+ r_wv_sb = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
342
+ block_dim=[n_chunks], num_free_tiles=[n_chunks])
343
+ for c in range(n_chunks):
344
+ nisa.dma_copy(dst=r_wv_sb[c], src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX, vcol0:vcol0 + local_heads * D])
345
+ r_b_sb = ra.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
346
+ block_dim=[2, local_quads * 2], num_free_tiles=[2, local_quads * 2], align_to=4)
347
+ r_b_stage = ra.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
348
+ for qk in range(2):
349
+ for g in range(local_quads * 2):
350
+ nisa.dma_copy(dst=r_b_stage, src=bqk_hbm[qk, col0 + g * _P_MAX:col0 + (g + 1) * _P_MAX, 0:1])
351
+ nisa.tensor_copy(dst=r_b_sb[qk][g], src=r_b_stage)
352
+ r_bv_bc = ra.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
353
+ nisa.dma_copy(dst=r_bv_bc, src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]], offset=vcol0))
354
+ r_tab = ra.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
355
+ block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
356
+ for s in range(n_tt):
357
+ for t_i in range(4):
358
+ _tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
359
+ nisa.dma_copy(dst=r_tab[t_i][s][:, 0:_tw], src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
360
+ r_wo_sb = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
361
+ block_dim=[n_ochunks], num_free_tiles=[n_ochunks])
362
+ for oc in range(n_ochunks):
363
+ nisa.dma_copy(dst=r_wo_sb[oc], src=wot_hbm[col0 + oc * _P_MAX:col0 + (oc + 1) * _P_MAX, 0:H])
364
+ r_bo_bc = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
365
+ nisa.dma_copy(dst=r_bo_bc, src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
366
+ res_top = ra.get_current_address()
367
+
368
+ for img in range(nimg): # unrolled at trace time (python int)
369
+ rofs = img * N * H # element offset of image img's rows in h / result
370
+ allocator = ModularAllocator(initial_address=res_top)
371
+
372
+ # ---- persistent consumer tiles (written by producer, read by attention) ---------
373
+ # N<=2048: all 8 heads' K/V resident in SBUF (bit-for-bit shipping path). N>2048: K/V for
374
+ # all 8 heads (196608 B/lane) is the ENTIRE 192 KiB budget, so they cannot stay resident
375
+ # alongside x_t/tab/working set. STREAM them: the producer scatters each head's K/V to
376
+ # private_hbm, and phase-2 loads ONE head's K/V into small resident staging rings just
377
+ # before its attention call (attention runs per head and writes O to HBM, so only one
378
+ # head's K/V need be live at a time).
379
+ k_tiles = []
380
+ v_tiles = []
381
+ k_hbm = []
382
+ v_hbm = []
383
+ if _big:
384
+ for _lh in range(local_heads):
385
+ k_hbm.append(nl.ndarray((n_ktiles, D, _K_TILE), dtype=nl.bfloat16,
386
+ buffer=nl.private_hbm))
387
+ v_hbm.append(nl.ndarray((n_vtiles, _V_TILE, D), dtype=nl.bfloat16,
388
+ buffer=nl.private_hbm))
389
+ # ONE head's resident staging rings (reused across heads in phase 2).
390
+ k_stage = allocator.alloc_sbuf_tensor(
391
+ shape=(D, _K_TILE), dtype=nl.bfloat16,
392
+ block_dim=[n_ktiles], num_free_tiles=[n_ktiles], align_to=32)
393
+ v_stage = allocator.alloc_sbuf_tensor(
394
+ shape=(_V_TILE, D), dtype=nl.bfloat16,
395
+ block_dim=[n_vtiles], num_free_tiles=[n_vtiles])
396
+ else:
397
+ for _lh in range(local_heads):
398
+ kt_ = allocator.alloc_sbuf_tensor(
399
+ shape=(D, _K_TILE), dtype=nl.bfloat16,
400
+ block_dim=[n_ktiles], num_free_tiles=[n_ktiles], align_to=32)
401
+ k_tiles.append(kt_)
402
+ vt_ = allocator.alloc_sbuf_tensor(
403
+ shape=(_V_TILE, D), dtype=nl.bfloat16,
404
+ block_dim=[n_vtiles], num_free_tiles=[n_vtiles])
405
+ v_tiles.append(vt_)
406
+ # HIGH WATER MARK for phase 2: k_tiles/v_tiles are the only phase-1 products attention
407
+ # needs, so every buffer above this address is dead once the producer loop finishes.
408
+ phase2_base = allocator.get_current_address()
409
+
410
+ # ------------------------------------------------------------------------------------
411
+ # N>2048 GATE (64-frame clips, N=8192). At N<=2048 every ring below keeps its full-N
412
+ # depth, so the buffer shape, byte layout, DMA target addresses and fill VALUES are
413
+ # bit-for-bit identical to the shipping 16-frame path (see the SBUF budget table in the
414
+ # module docstring), and the attention call goes to the pinned single-section flatten
415
+ # ``_attention_cte`` (all engine pins intact). At N>2048 two independent overflows appear:
416
+ # (1) PHASE 1: the four N-scaled residents (k_tiles+v_tiles+x_t+tab = 458752 B/lane)
417
+ # coexist. FIX: x_t and tab become depth-XT_RING token-block rings (a rolling 4-tile
418
+ # window filled inside the per-token-tile loop; a pure copy, identical values).
419
+ # (2) PHASE 2: eight heads' K/V resident (196608 B/lane) is the whole budget. FIX: stream
420
+ # K/V through private_hbm and stage ONE head at a time into k_stage/v_stage. The
421
+ # attention call is the SAME single-section flatten ``_attention_cte`` the N<=2048 path
422
+ # uses (seqlen_k=8192 < 10240 threshold => num_sections==1, no multi-section flash
423
+ # combine); its large-tile loop iterates 4x over _LARGE_TILE_SZ-sized buffers.
424
+ # The N<=2048 path enters NEITHER branch and is bit-for-bit unchanged.
425
+ _big = N > _PSUM_FMAX * 4 # N>2048 -> the 64-frame geometry
426
+ XT_RING = min(n_tt, 4) if _big else n_tt # token-block ring depth for x_t / tab
427
+
428
+ # ---- resident producer inputs ----------------------------------------------------
429
+ # x^T: hidden on partitions, tokens on the free axis, as a per-512-token-block ring.
430
+ # At N<=2048 XT_RING==n_tt so the ring holds every block (== the old (128,N) buffer,
431
+ # same bytes, same addresses); at N=8192 it holds a rolling window of 4 blocks. The fill
432
+ # is a pure dma_transpose (no arithmetic), so any fill ORDER yields identical values.
433
+ x_t = allocator.alloc_sbuf_tensor(
434
+ shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
435
+ block_dim=[n_chunks, n_tt], num_free_tiles=[n_chunks, XT_RING], align_to=32)
436
+
437
+ if not _big:
438
+ # N<=2048: fill the whole buffer up front, exactly as the shipping path did (pure
439
+ # dma_transpose; block-indexed AP lands at the same addresses as the old (128,N) buffer).
440
+ for s in range(n_tt):
441
+ for c in range(n_chunks):
442
+ nisa.dma_transpose(
443
+ dst=x_t[c][s].ap([[_PSUM_FMAX, _P_MAX], [1, 1], [1, 1],
444
+ [1, min(_PSUM_FMAX, N - s * _PSUM_FMAX)]], offset=0),
445
+ src=h_hbm.ap([[H, min(_PSUM_FMAX, N - s * _PSUM_FMAX)], [1, 1], [1, 1], [1, _P_MAX]],
446
+ offset=rofs + (s * _PSUM_FMAX) * H + c * _P_MAX),
447
+ )
448
+
449
+ if _res:
450
+ w_sb, wv_sb, b_sb, bv_bc = r_w_sb, r_wv_sb, r_b_sb, r_bv_bc
451
+ else:
452
+ # this core's Q/K weight columns (lane-permuted) and V columns (natural)
453
+ w_sb = allocator.alloc_sbuf_tensor(
454
+ shape=(_P_MAX, local_quads * 2 * _P_MAX), dtype=nl.bfloat16,
455
+ block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
456
+ for qk in range(2):
457
+ for c in range(n_chunks):
458
+ nisa.dma_copy(dst=w_sb[qk][c],
459
+ src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX,
460
+ col0:col0 + local_quads * 2 * _P_MAX])
461
+ wv_sb = allocator.alloc_sbuf_tensor(
462
+ shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
463
+ block_dim=[n_chunks], num_free_tiles=[n_chunks])
464
+ for c in range(n_chunks):
465
+ nisa.dma_copy(dst=wv_sb[c],
466
+ src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX,
467
+ vcol0:vcol0 + local_heads * D])
468
+
469
+ # biases: qk as (128,1) per group; v broadcast to (128, 8*64) once
470
+ b_sb = allocator.alloc_sbuf_tensor(
471
+ shape=(_P_MAX, 1), dtype=nl.float32,
472
+ block_dim=[2, local_quads * 2], num_free_tiles=[2, local_quads * 2], align_to=4)
473
+ b_stage = allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
474
+ for qk in range(2):
475
+ for g in range(local_quads * 2):
476
+ nisa.dma_copy(dst=b_stage,
477
+ src=bqk_hbm[qk, col0 + g * _P_MAX:col0 + (g + 1) * _P_MAX, 0:1])
478
+ # SITE :224 -- bias CAST, left UNPINNED on purpose (0.09% of the window, already at
479
+ # the Vector fixed fee; nothing to move).
480
+ nisa.tensor_copy(dst=b_sb[qk][g], src=b_stage)
481
+ bv_bc = allocator.alloc_sbuf_tensor(
482
+ shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
483
+ nisa.dma_copy(dst=bv_bc,
484
+ src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]],
485
+ offset=vcol0))
486
+
487
+ # rope tables, resident (quad-tiled on the host: identical rows for every quad). Token-block
488
+ # ring, same gating as x_t: at N<=2048 TAB_RING==n_tt holds every block (== old (128,N)
489
+ # buffer, bit-for-bit); at N=8192 a rolling 4-block window filled inside the tt loop. Pure
490
+ # dma_copy -> identical values regardless of fill order.
491
+ if _res:
492
+ tab = r_tab
493
+ else:
494
+ tab = allocator.alloc_sbuf_tensor(
495
+ shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
496
+ block_dim=[4, n_tt], num_free_tiles=[4, XT_RING], align_to=4)
497
+
498
+ if not _big:
499
+ for s in range(n_tt):
500
+ for t_i in range(4):
501
+ _tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
502
+ nisa.dma_copy(dst=tab[t_i][s][:, 0:_tw],
503
+ src=tab_hbm[t_i, 0:_P_MAX,
504
+ s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
505
+
506
+ # ---- per-tile working set (reused across iterations; same tensor objects) --------
507
+ proj = allocator.alloc_sbuf_tensor(
508
+ shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16, block_dim=[2],
509
+ num_free_tiles=[2])
510
+ acc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
511
+ align_to=4)
512
+ tmp = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
513
+ align_to=4)
514
+ roped = allocator.alloc_sbuf_tensor(
515
+ shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16, block_dim=[2],
516
+ num_free_tiles=[2])
517
+ vout = allocator.alloc_sbuf_tensor(
518
+ shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
519
+
520
+ # Q staging via private_hbm, one per LOCAL head (vendor segmented-kernel pattern)
521
+ q_priv = []
522
+ for _lh in range(local_heads):
523
+ q_priv.append(nl.ndarray((1, D, N), dtype=nl.bfloat16, buffer=nl.private_hbm))
524
+
525
+ # ---- producer: per 512-token tile, straight through the pipeline ----------------
526
+ for tt in range(n_tt):
527
+ t0 = tt * _PSUM_FMAX
528
+ tw = min(_PSUM_FMAX, N - t0) # ragged last token tile (N % 512 != 0)
529
+
530
+ # N>2048: fill this token-block's x_t / tab slice NOW (rolling ring window). At
531
+ # N<=2048 the whole buffer was already filled up front (bit-for-bit), so skip.
532
+ if _big:
533
+ for c in range(n_chunks):
534
+ nisa.dma_transpose(
535
+ dst=x_t[c][tt].ap([[_PSUM_FMAX, _P_MAX], [1, 1], [1, 1], [1, _PSUM_FMAX]],
536
+ offset=0),
537
+ src=h_hbm.ap([[H, _PSUM_FMAX], [1, 1], [1, 1], [1, _P_MAX]],
538
+ offset=rofs + t0 * H + c * _P_MAX),
539
+ )
540
+ for t_i in range(4):
541
+ nisa.dma_copy(dst=tab[t_i][tt],
542
+ src=tab_hbm[t_i, 0:_P_MAX, t0:t0 + _PSUM_FMAX])
543
+
544
+ # V for all 8 local heads of this token tile.
545
+ for vsub in range(tw // _V_TILE):
546
+ ps_v = nl.ndarray((_V_TILE, local_heads * D), dtype=nl.float32,
547
+ buffer=nl.psum, address=(0, 7 * _PSUM_BANK))
548
+ for c in range(n_chunks):
549
+ nisa.nc_matmul(
550
+ dst=ps_v,
551
+ stationary=x_t[c][tt][:, vsub * _V_TILE:(vsub + 1) * _V_TILE],
552
+ moving=wv_sb[c],
553
+ accumulate=(c > 0),
554
+ )
555
+ nisa.tensor_tensor(dst=vout, data1=ps_v, data2=bv_bc, op=nl.add)
556
+ for lh in range(local_heads):
557
+ if _big:
558
+ # N>2048: stream V to HBM (SBUF->HBM dma from vout directly). The pinned
559
+ # :276 tensor_copy is the N<=2048 shipping site and is left untouched below.
560
+ nisa.dma_copy(
561
+ dst=v_hbm[lh][tt * v_per_tt + vsub],
562
+ src=vout[:, lh * D:(lh + 1) * D])
563
+ continue
564
+ # ======== SITE :276 -- pinned to Scalar. WAR chain of 8 copies read one vout
565
+ # before the next vsub overwrites it; Scalar (240 ns) beats GpSimd (375 ns) and
566
+ # sheds the copy off Vector, the binding engine. ========
567
+ nisa.tensor_copy(
568
+ dst=v_tiles[lh][tt * v_per_tt + vsub],
569
+ src=vout[:, lh * D:(lh + 1) * D],
570
+ engine=engine.scalar)
571
+
572
+ # Q and K for both local quads, both parities, this token tile.
573
+ for qk in range(2):
574
+ for j in range(local_quads):
575
+ for par in range(2):
576
+ g = j * 2 + par
577
+ cols = g * _P_MAX
578
+ ps = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32,
579
+ buffer=nl.psum,
580
+ address=(0, (5 + par) * _PSUM_BANK))
581
+ for c in range(n_chunks):
582
+ nisa.nc_matmul(
583
+ dst=ps[:, 0:tw],
584
+ stationary=w_sb[qk][c][:, cols:cols + _P_MAX],
585
+ moving=x_t[c][tt][:, 0:tw],
586
+ accumulate=(c > 0),
587
+ )
588
+ # ======== SITE :458 -- pinned to Scalar. Largest remaining Vector-resident
589
+ # relocatable site (768/core); data=ps is PSUM so GpSimd is illegal, and
590
+ # Scalar's 612 ns fee is faster than the 744 ns Vector cell, so the
591
+ # zero-lead rope chain consuming proj cannot lengthen. ========
592
+ # NKI 0.7: a Scalar-engine pin here fails ISA opcode_engine_check on newer compilers
593
+ # TOT compiler -> unpinned (compiler picks the engine).
594
+ nisa.tensor_scalar(dst=proj[par][:, 0:tw], data=ps[:, 0:tw],
595
+ op0=nl.add, operand0=b_sb[qk][g])
596
+
597
+ # rope: the six ops of nki_rope, same order, tables sliced per tile
598
+ nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[0][:, 0:tw],
599
+ data2=tab[0][tt][:, 0:tw], op=nl.multiply)
600
+ nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[1][:, 0:tw],
601
+ data2=tab[2][tt][:, 0:tw], op=nl.multiply)
602
+ nisa.tensor_tensor(dst=roped[0][:, 0:tw], data1=acc[:, 0:tw], data2=tmp[:, 0:tw], op=nl.add)
603
+ nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[1][:, 0:tw],
604
+ data2=tab[1][tt][:, 0:tw], op=nl.multiply)
605
+ nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[0][:, 0:tw],
606
+ data2=tab[3][tt][:, 0:tw], op=nl.multiply)
607
+ nisa.tensor_tensor(dst=roped[1][:, 0:tw], data1=acc[:, 0:tw], data2=tmp[:, 0:tw], op=nl.add)
608
+
609
+ # scatter: head b of the quad owns rows [b*32,(b+1)*32)
610
+ for b in range(quad):
611
+ lh = j * quad + b
612
+ r0 = b * half
613
+ if qk == 1:
614
+ for par2 in range(2):
615
+ if _big:
616
+ # N>2048: stream K to HBM (SBUF->HBM dma). The :483 tensor_copy
617
+ # below is the N<=2048 shipping site, left untouched.
618
+ nisa.dma_copy(
619
+ dst=k_hbm[lh][tt][par2 * half:(par2 + 1) * half,
620
+ 0:_K_TILE],
621
+ src=roped[par2][r0:r0 + half, 0:_PSUM_FMAX])
622
+ continue
623
+ # SITE :483 -- K-scatter COPY, left UNPINNED (no affordable landing
624
+ # engine: it is a WAR chain against the next iteration's rope write).
625
+ nisa.tensor_copy(
626
+ dst=k_tiles[lh][tt][par2 * half:(par2 + 1) * half,
627
+ 0:tw],
628
+ src=roped[par2][r0:r0 + half, 0:tw])
629
+ else:
630
+ for par2 in range(2):
631
+ nisa.dma_copy(
632
+ dst=q_priv[lh][0, par2 * half:(par2 + 1) * half,
633
+ t0:t0 + tw],
634
+ src=roped[par2][r0:r0 + half, 0:tw])
635
+
636
+ # ---- PHASE 2: reclaim the dead producer buffers. NO ALIASING. -------------------
637
+ allocator.set_current_address(phase2_base)
638
+
639
+ if _res:
640
+ wo_sb, bo_bc = r_wo_sb, r_bo_bc
641
+ else:
642
+ # ---- out-projection weights: THIS core's feature rows, all H output columns -------
643
+ wo_sb = allocator.alloc_sbuf_tensor(
644
+ shape=(_P_MAX, H), dtype=nl.bfloat16,
645
+ block_dim=[n_ochunks], num_free_tiles=[n_ochunks])
646
+ for oc in range(n_ochunks):
647
+ nisa.dma_copy(dst=wo_sb[oc],
648
+ src=wot_hbm[col0 + oc * _P_MAX:col0 + (oc + 1) * _P_MAX, 0:H])
649
+ # bias already HALVED on the host (pack_out_weights) so the two cores' partials sum to
650
+ # exactly one bias. Broadcast across partitions (output columns are on the free axis).
651
+ bo_bc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
652
+ nisa.dma_copy(dst=bo_bc,
653
+ src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
654
+
655
+ # ---- O STAYS IN SBUF, ALLOCATED EXACTLY ONCE. tp_out=True gives [D, 128 tokens] per Q
656
+ # group (feature-major), which the projection contracts over, so O never transposes.
657
+ # The list handed to the vendor is SLICES into o_feat, so _write_back_o_impl copies
658
+ # straight into final position. ------------------------------------------------------
659
+ n_grp = N // _P_MAX
660
+ o_feat = allocator.alloc_sbuf_tensor(
661
+ shape=(_P_MAX, N), dtype=nl.bfloat16,
662
+ block_dim=[n_ochunks], num_free_tiles=[n_ochunks], align_to=32)
663
+ o_sb = []
664
+ for _lh in range(local_heads):
665
+ oc = (_lh * D) // _P_MAX
666
+ rr = (_lh * D) % _P_MAX
667
+ # explicit loop, NOT a comprehension: the NKI tracer rejects comprehensions in a kernel.
668
+ grp = []
669
+ for _g in range(n_grp):
670
+ grp.append(o_feat[oc][rr:rr + D, _g * _P_MAX:(_g + 1) * _P_MAX])
671
+ o_sb.append(grp)
672
+
673
+ attn_base = allocator.get_current_address()
674
+ for lh in range(local_heads):
675
+ if _big:
676
+ # N>2048: load THIS head's K/V from HBM into the resident staging rings (only one
677
+ # head live at a time -> K/V cost is one head's 24576 B/lane, not eight heads'
678
+ # 196608). Then run the SAME single-section flatten the N<=2048 path uses: seqlen_k
679
+ # =8192 < 10240 threshold => num_sections==1, no multi-section flash combine. The
680
+ # inner's large-tile loop iterates 4x over _LARGE_TILE_SZ-sized buffers; O is written
681
+ # straight into this head's o_feat SBUF slices (o_sb[lh]), exactly as N<=2048.
682
+ for _i in range(n_ktiles):
683
+ nisa.dma_copy(dst=k_stage[_i], src=k_hbm[lh][_i])
684
+ for _i in range(n_vtiles):
685
+ nisa.dma_copy(dst=v_stage[_i], src=v_hbm[lh][_i])
686
+ # 1D python-list views [seq_tile] (the flatten indexes k_sb[k_tile_idx_in_section]
687
+ # and v_sb[v_tile_idx] as flat lists -- same convention as k_tiles[lh]/v_tiles[lh]).
688
+ k1d = []
689
+ for _i in range(n_ktiles):
690
+ k1d.append(k_stage[_i])
691
+ v1d = []
692
+ for _i in range(n_vtiles):
693
+ v1d.append(v_stage[_i])
694
+ _attention_cte(
695
+ q_priv[lh], None, None,
696
+ scale=scale, causal_mask=False,
697
+ tp_q=False, tp_k=False, tp_out=True,
698
+ cache_softmax=False,
699
+ k_cache_sbuf=k1d,
700
+ v_cache_sbuf=v1d,
701
+ out_o_hbm=o_sb[lh],
702
+ init_sbuf_addr=attn_base,
703
+ seqlen_k_valid=n_valid,
704
+ )
705
+ else:
706
+ # N<=2048: the pinned single-section flatten (h8/h10 engine pins intact, bit-for-bit).
707
+ _attention_cte(
708
+ q_priv[lh], None, None,
709
+ scale=scale, causal_mask=False,
710
+ tp_q=False, tp_k=False, tp_out=True,
711
+ cache_softmax=False,
712
+ k_cache_sbuf=k_tiles[lh],
713
+ v_cache_sbuf=v_tiles[lh],
714
+ out_o_hbm=o_sb[lh],
715
+ init_sbuf_addr=attn_base,
716
+ seqlen_k_valid=n_valid,
717
+ )
718
+ allocator.set_current_address(attn_base)
719
+
720
+ # ---- PARTIAL out projection: tokens on partitions, output columns on the free axis
721
+ # (chosen by the HBM write: result is token-major, so a row of consecutive output columns
722
+ # is a 1024-byte contiguous run). 128 calls/layer/core (16 token blocks x 2 col blocks x
723
+ # 4 chunks). --------------------------------------------------------------------------
724
+ out_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX),
725
+ dtype=nl.bfloat16, align_to=32)
726
+ n_ob = (H + _PSUM_FMAX - 1) // _PSUM_FMAX
727
+ for tb in range(N // _P_MAX):
728
+ for ob in range(n_ob):
729
+ ow = min(_PSUM_FMAX, H - ob * _PSUM_FMAX) # ragged last column block (H%512!=0)
730
+ ps_out = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32, buffer=nl.psum,
731
+ address=(0, 0))
732
+ for oc in range(n_ochunks):
733
+ nisa.nc_matmul(
734
+ dst=ps_out[:, 0:ow],
735
+ stationary=o_feat[oc][:, tb * _P_MAX:(tb + 1) * _P_MAX],
736
+ moving=wo_sb[oc][:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow],
737
+ accumulate=(oc > 0),
738
+ )
739
+ nisa.tensor_tensor(dst=out_sb[:, 0:ow], data1=ps_out[:, 0:ow],
740
+ data2=bo_bc[:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow],
741
+ op=nl.add)
742
+ nisa.dma_copy(
743
+ dst=result.ap(pattern=[[H, _P_MAX], [1, ow]],
744
+ offset=out_slot * NT * H + rofs + tb * _P_MAX * H + ob * _PSUM_FMAX),
745
+ src=out_sb[:, 0:ow])
746
+ return result
747
+
748
+
749
+ _ATTN = {g: wrap_nki_to_torch_hop(_attention_block_kernel[g]) for g in sorted({1, LNC})}
750
+
751
+
752
+ def _no_grad(*tensors) -> None:
753
+ for t in tensors:
754
+ if t is not None and t.requires_grad:
755
+ raise RuntimeError(
756
+ "attention_block registers no autograd rule and its packed weights are "
757
+ "detached caches. Use under torch.no_grad()/eval().")
758
+
759
+
760
+ @nki_op("vit_enc::attention_block", mutates_args={})
761
+ def attention_block(
762
+ h: torch.Tensor,
763
+ wqk: torch.Tensor,
764
+ bqk: torch.Tensor,
765
+ tab: torch.Tensor,
766
+ wvt: torch.Tensor,
767
+ bv: torch.Tensor,
768
+ wot: torch.Tensor,
769
+ bo: torch.Tensor,
770
+ scale: float,
771
+ head_dim: int,
772
+ n_valid: int,
773
+ hb: int = 0,
774
+ grid: int = LNC,
775
+ nimg: int = 1,
776
+ resident: int = 0,
777
+ ) -> torch.Tensor:
778
+ """Fused norm1'd-hidden -> QKV+rope -> attention -> fused out-projection PARTIALS.
779
+
780
+ Returns ``[LNC=2, N, H]`` bf16 per-core out-projection partials; K3's residual add sums the
781
+ two cores. The two Scalar engine pins (V-scatter :276, QK bias-evict :458) are hardcoded.
782
+ """
783
+ _no_grad(h, wqk, bqk, tab, wvt, bv, wot, bo)
784
+ # N>2048 (e.g. 64-frame clips) is NOT supported. The phase-1 memory fixes fit (~106 KiB/lane),
785
+ # but the single-section vendor inner overflows phase 2 (measured 354912 B/lane vs the 196608
786
+ # budget: exp_tp_sb/mm1_masked/exp_sb scale with num_large_tiles_per_section=ceil(N/2048)=4).
787
+ # The shrinkable phase-2 region (78848 B/lane) is smaller than the 158304 overflow, so
788
+ # streaming O / shrinking o_feat cannot close the gap; re-enabling flash sectioning fits but
789
+ # is numerically wrong (rel_l2 0.90). Refuse here rather than emit a non-fitting or wrong
790
+ # kernel. See the V-JEPA2 kernels' verification notes. NKI kernels cannot `raise`, so the guard lives here.
791
+ N = h.shape[0] // nimg
792
+ if N > 2048:
793
+ raise NotImplementedError(
794
+ f"attention_block N={N} (>2048, e.g. 64-frame clips) is not supported: the "
795
+ f"single-section vendor inner does not fit SBUF (phase-2 peak measured 354912 B/lane "
796
+ f"vs the 196608 budget). Streaming O or shrinking o_feat cannot close the 158304 "
797
+ f"B/lane gap, and flash sectioning is numerically wrong (rel_l2 0.90). See "
798
+ f"the V-JEPA2 kernels' verification notes. Use N<=2048 (up to 16-frame clips) -- the bit-for-bit "
799
+ f"shipping path."
800
+ )
801
+ return _ATTN[grid](h, wqk, bqk, tab, wvt, bv, wot, bo, scale, head_dim, n_valid, hb, grid, nimg, resident)
build/torch-neuron/nki_kernels/attention_block_l1.py ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Attention for LNC=1: ONE program over ALL head groups.
2
+
3
+ The 2-core kernel in ``attention_block.py`` owns one 8-head group per program. At LNC=1 that
4
+ means one launch per group per layer (2 for ViT-L, 3 for ViT-H+), each re-transposing x, each
5
+ re-loading the rope tables, each writing an [N, H] bf16 out-projection PARTIAL to HBM that the
6
+ LN-fold kernel then sums.
7
+
8
+ This kernel, per image (``nimg`` images stacked on the row axis, one launch):
9
+ * transposes x once and loads the rope tables once (shared by every group: tables are
10
+ quad-tiled per head, identical for all heads);
11
+ * for each group: projects Q/K/V + RoPE (same producer as attention_block), runs the vendor
12
+ attention per head, and writes O feature-major into a PERSISTENT per-group SBUF slab;
13
+ * after all groups: ONE out-projection contracting over ALL features (G*4 chunks accumulate
14
+ in one fp32 PSUM tile), adds the FULL bias once, writes ONE [N, H] result.
15
+ No partials, no host sum; the LN-fold kernel receives a single branch.
16
+
17
+ ``resident=1``: Q/K/V/O weights, biases and rope tables are staged ONCE per launch (bottom of
18
+ SBUF) and reused by every image. Host decides by an SBUF estimate (does not fit at N>=1024 for
19
+ ViT-L); ``resident=0`` restages each group's weights per image.
20
+
21
+ N<=2048 only (the shipping single-section vendor path). Numerics: identical producer and vendor
22
+ attention; the out-projection now accumulates all groups in fp32 PSUM instead of summing bf16
23
+ per-group partials, so results differ from attention_block by bf16 rounding of the partials.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import torch
28
+ from nki.framework.kernel import wrap_nki_to_torch_hop
29
+ from torch_neuronx import nki_op
30
+
31
+ import nki
32
+ import nki.isa as nisa
33
+ import nki.language as nl
34
+ from nki.isa import engine
35
+
36
+ from nkilib.core.utils.modular_allocator import ModularAllocator
37
+
38
+ from .attention_inner import _attention_cte
39
+
40
+ _P_MAX = 128
41
+ _PSUM_FMAX = 512
42
+ _PSUM_BANK = 2048
43
+ _K_TILE = 512
44
+ _V_TILE = 128
45
+
46
+
47
+ def _kernel_assert(cond: bool, msg: str):
48
+ assert cond, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {msg}"
49
+
50
+
51
+ def sbuf_estimate(H: int, Fp: int, N: int, D: int, resident: bool) -> int:
52
+ """Conservative bytes/lane at the kernel's peak (host-side decision helper, not exact)."""
53
+ G = Fp // _PSUM_FMAX
54
+ n_chunks = H // _P_MAX
55
+ n_tt = (N + _PSUM_FMAX - 1) // _PSUM_FMAX
56
+ n_ktiles = (N + _K_TILE - 1) // _K_TILE
57
+ lh = _PSUM_FMAX // D
58
+ n_och = (lh * D) // _P_MAX
59
+ w_grp = 3 * n_chunks * _PSUM_FMAX * 2 + _PSUM_FMAX * 4 + 64 # wqk + wv + bv_bc + biases
60
+ w_out = G * n_och * H * 2 + H * 4 # wo + bo_bc
61
+ tab = 4 * n_tt * _PSUM_FMAX * 4
62
+ o_feat = G * n_och * N * 2
63
+ x_t = n_chunks * n_tt * _PSUM_FMAX * 2
64
+ kv = lh * (n_ktiles * _K_TILE * 2 + (N // _V_TILE) * D * 2)
65
+ work = 2 * _PSUM_FMAX * 2 * 2 + 2 * _PSUM_FMAX * 4 + _PSUM_FMAX * 4
66
+ inner = 48 * 1024
67
+ resid = (G * w_grp + w_out + tab) if resident else 0
68
+ base = resid + o_feat + x_t + (0 if resident else tab)
69
+ p1 = kv + (0 if resident else w_grp) + work
70
+ p2 = kv + inner
71
+ p3 = (0 if resident else w_out) + 2048
72
+ return base + max(p1, p2, p3)
73
+
74
+
75
+ @nki.jit
76
+ def _attention_block_l1_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_hbm,
77
+ wot_hbm, bo_hbm, scale, head_dim, n_valid, nimg=1, resident=0):
78
+ NT, H = h_hbm.shape
79
+ N = NT // nimg
80
+ Fp = wqk_hbm.shape[2]
81
+ D = head_dim
82
+ half = D // 2
83
+ quad = _P_MAX // half
84
+ local_heads = _PSUM_FMAX // D
85
+ local_quads = local_heads // quad
86
+ G = Fp // _PSUM_FMAX # head groups, ALL handled by this one program
87
+ n_chunks = H // _P_MAX
88
+ n_tt = (N + _PSUM_FMAX - 1) // _PSUM_FMAX
89
+ n_ktiles = (N + _K_TILE - 1) // _K_TILE
90
+ n_vtiles = N // _V_TILE
91
+ n_ochunks = (local_heads * D) // _P_MAX # feature chunks per group
92
+ v_per_tt = _PSUM_FMAX // _V_TILE
93
+
94
+ _kernel_assert(wqk_hbm.shape == (2, H, Fp), "bad wqk")
95
+ _kernel_assert(bqk_hbm.shape == (2, Fp, 1), "bad bqk")
96
+ _kernel_assert(tab_hbm.shape == (4, _P_MAX, N), "bad tab")
97
+ _kernel_assert(wvt_hbm.shape == (H, Fp) and bv_hbm.shape == (1, Fp), "bad V weights")
98
+ _kernel_assert(wot_hbm.shape == (Fp, H) and bo_hbm.shape == (1, H), "bad O weights")
99
+ _kernel_assert(H % _P_MAX == 0, "H must be a multiple of 128")
100
+ _kernel_assert(quad * half == _P_MAX, "D must give quad*half=128")
101
+ _kernel_assert(Fp % _PSUM_FMAX == 0, "padded features must be a multiple of 512")
102
+ _kernel_assert(N % _P_MAX == 0, "N must be a multiple of 128")
103
+ _kernel_assert(N <= _PSUM_FMAX * 4, "N>2048 not supported")
104
+ _kernel_assert(NT == nimg * N, "rows must be nimg x N")
105
+
106
+ result = nl.ndarray((1, NT, H), dtype=nl.bfloat16, buffer=nl.shared_hbm)
107
+
108
+ # ---------------- resident (per-launch invariant) staging ----------------
109
+ res_top = 0
110
+ r_w = []
111
+ r_wv = []
112
+ r_b = []
113
+ r_bv = []
114
+ if resident:
115
+ ra = ModularAllocator(initial_address=0)
116
+ for g in range(G):
117
+ col0 = g * _PSUM_FMAX
118
+ w_sb_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2 * _P_MAX), dtype=nl.bfloat16,
119
+ block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
120
+ for qk in range(2):
121
+ for c in range(n_chunks):
122
+ nisa.dma_copy(dst=w_sb_[qk][c],
123
+ src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX,
124
+ col0:col0 + local_quads * 2 * _P_MAX])
125
+ wv_sb_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
126
+ block_dim=[n_chunks], num_free_tiles=[n_chunks])
127
+ for c in range(n_chunks):
128
+ nisa.dma_copy(dst=wv_sb_[c],
129
+ src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX, col0:col0 + local_heads * D])
130
+ b_sb_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
131
+ block_dim=[2, local_quads * 2],
132
+ num_free_tiles=[2, local_quads * 2], align_to=4)
133
+ b_stage_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
134
+ for qk in range(2):
135
+ for gg in range(local_quads * 2):
136
+ nisa.dma_copy(dst=b_stage_,
137
+ src=bqk_hbm[qk, col0 + gg * _P_MAX:col0 + (gg + 1) * _P_MAX, 0:1])
138
+ nisa.tensor_copy(dst=b_sb_[qk][gg], src=b_stage_)
139
+ bv_bc_ = ra.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
140
+ nisa.dma_copy(dst=bv_bc_,
141
+ src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]], offset=col0))
142
+ r_w.append(w_sb_)
143
+ r_wv.append(wv_sb_)
144
+ r_b.append(b_sb_)
145
+ r_bv.append(bv_bc_)
146
+ r_tab = ra.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
147
+ block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
148
+ for s in range(n_tt):
149
+ for t_i in range(4):
150
+ _tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
151
+ nisa.dma_copy(dst=r_tab[t_i][s][:, 0:_tw],
152
+ src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
153
+ r_wo = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
154
+ block_dim=[G * n_ochunks], num_free_tiles=[G * n_ochunks])
155
+ for oc in range(G * n_ochunks):
156
+ nisa.dma_copy(dst=r_wo[oc], src=wot_hbm[oc * _P_MAX:(oc + 1) * _P_MAX, 0:H])
157
+ r_bo = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
158
+ nisa.dma_copy(dst=r_bo, src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
159
+ res_top = ra.get_current_address()
160
+
161
+ n_grp = N // _P_MAX
162
+ for img in range(nimg):
163
+ rofs = img * N * H
164
+ allocator = ModularAllocator(initial_address=res_top)
165
+
166
+ # O for ALL groups stays in SBUF until the single out-projection.
167
+ o_feat = []
168
+ for g in range(G):
169
+ of_ = allocator.alloc_sbuf_tensor(shape=(_P_MAX, N), dtype=nl.bfloat16,
170
+ block_dim=[n_ochunks], num_free_tiles=[n_ochunks],
171
+ align_to=32)
172
+ o_feat.append(of_)
173
+
174
+ # x^T: shared by every group (transposed ONCE per image).
175
+ x_t = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
176
+ block_dim=[n_chunks, n_tt], num_free_tiles=[n_chunks, n_tt],
177
+ align_to=32)
178
+ for s in range(n_tt):
179
+ for c in range(n_chunks):
180
+ nisa.dma_transpose(
181
+ dst=x_t[c][s].ap([[_PSUM_FMAX, _P_MAX], [1, 1], [1, 1],
182
+ [1, min(_PSUM_FMAX, N - s * _PSUM_FMAX)]], offset=0),
183
+ src=h_hbm.ap([[H, min(_PSUM_FMAX, N - s * _PSUM_FMAX)], [1, 1], [1, 1], [1, _P_MAX]],
184
+ offset=rofs + (s * _PSUM_FMAX) * H + c * _P_MAX),
185
+ )
186
+
187
+ # rope tables: shared by every group.
188
+ if resident:
189
+ tab = r_tab
190
+ else:
191
+ tab = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
192
+ block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
193
+ for s in range(n_tt):
194
+ for t_i in range(4):
195
+ _tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
196
+ nisa.dma_copy(dst=tab[t_i][s][:, 0:_tw],
197
+ src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
198
+
199
+ img_base = allocator.get_current_address()
200
+
201
+ for g in range(G):
202
+ allocator.set_current_address(img_base)
203
+ col0 = g * _PSUM_FMAX
204
+
205
+ k_tiles = []
206
+ v_tiles = []
207
+ for _lh in range(local_heads):
208
+ kt_ = allocator.alloc_sbuf_tensor(shape=(D, _K_TILE), dtype=nl.bfloat16,
209
+ block_dim=[n_ktiles], num_free_tiles=[n_ktiles],
210
+ align_to=32)
211
+ k_tiles.append(kt_)
212
+ vt_ = allocator.alloc_sbuf_tensor(shape=(_V_TILE, D), dtype=nl.bfloat16,
213
+ block_dim=[n_vtiles], num_free_tiles=[n_vtiles])
214
+ v_tiles.append(vt_)
215
+ phase2_base = allocator.get_current_address()
216
+
217
+ if resident:
218
+ w_sb = r_w[g]
219
+ wv_sb = r_wv[g]
220
+ b_sb = r_b[g]
221
+ bv_bc = r_bv[g]
222
+ else:
223
+ w_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2 * _P_MAX),
224
+ dtype=nl.bfloat16,
225
+ block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
226
+ for qk in range(2):
227
+ for c in range(n_chunks):
228
+ nisa.dma_copy(dst=w_sb[qk][c],
229
+ src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX,
230
+ col0:col0 + local_quads * 2 * _P_MAX])
231
+ wv_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
232
+ block_dim=[n_chunks], num_free_tiles=[n_chunks])
233
+ for c in range(n_chunks):
234
+ nisa.dma_copy(dst=wv_sb[c],
235
+ src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX, col0:col0 + local_heads * D])
236
+ b_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
237
+ block_dim=[2, local_quads * 2],
238
+ num_free_tiles=[2, local_quads * 2], align_to=4)
239
+ b_stage = allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
240
+ for qk in range(2):
241
+ for gg in range(local_quads * 2):
242
+ nisa.dma_copy(dst=b_stage,
243
+ src=bqk_hbm[qk, col0 + gg * _P_MAX:col0 + (gg + 1) * _P_MAX, 0:1])
244
+ nisa.tensor_copy(dst=b_sb[qk][gg], src=b_stage)
245
+ bv_bc = allocator.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32,
246
+ align_to=4)
247
+ nisa.dma_copy(dst=bv_bc,
248
+ src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]], offset=col0))
249
+
250
+ proj = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
251
+ block_dim=[2], num_free_tiles=[2])
252
+ acc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32, align_to=4)
253
+ tmp = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32, align_to=4)
254
+ roped = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
255
+ block_dim=[2], num_free_tiles=[2])
256
+ vout = allocator.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32,
257
+ align_to=4)
258
+ q_priv = []
259
+ for _lh in range(local_heads):
260
+ q_priv.append(nl.ndarray((1, D, N), dtype=nl.bfloat16, buffer=nl.private_hbm))
261
+
262
+ # ---- producer (identical to attention_block's N<=2048 path) ----
263
+ for tt in range(n_tt):
264
+ t0 = tt * _PSUM_FMAX
265
+ tw = min(_PSUM_FMAX, N - t0)
266
+ for vsub in range(tw // _V_TILE):
267
+ ps_v = nl.ndarray((_V_TILE, local_heads * D), dtype=nl.float32,
268
+ buffer=nl.psum, address=(0, 7 * _PSUM_BANK))
269
+ for c in range(n_chunks):
270
+ nisa.nc_matmul(dst=ps_v,
271
+ stationary=x_t[c][tt][:, vsub * _V_TILE:(vsub + 1) * _V_TILE],
272
+ moving=wv_sb[c], accumulate=(c > 0))
273
+ nisa.tensor_tensor(dst=vout, data1=ps_v, data2=bv_bc, op=nl.add)
274
+ for lh in range(local_heads):
275
+ nisa.tensor_copy(dst=v_tiles[lh][tt * v_per_tt + vsub],
276
+ src=vout[:, lh * D:(lh + 1) * D], engine=engine.scalar)
277
+ for qk in range(2):
278
+ for j in range(local_quads):
279
+ for par in range(2):
280
+ gq = j * 2 + par
281
+ cols = gq * _P_MAX
282
+ ps = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32, buffer=nl.psum,
283
+ address=(0, (5 + par) * _PSUM_BANK))
284
+ for c in range(n_chunks):
285
+ nisa.nc_matmul(dst=ps[:, 0:tw],
286
+ stationary=w_sb[qk][c][:, cols:cols + _P_MAX],
287
+ moving=x_t[c][tt][:, 0:tw], accumulate=(c > 0))
288
+ nisa.tensor_scalar(dst=proj[par][:, 0:tw], data=ps[:, 0:tw],
289
+ op0=nl.add, operand0=b_sb[qk][gq])
290
+ nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[0][:, 0:tw],
291
+ data2=tab[0][tt][:, 0:tw], op=nl.multiply)
292
+ nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[1][:, 0:tw],
293
+ data2=tab[2][tt][:, 0:tw], op=nl.multiply)
294
+ nisa.tensor_tensor(dst=roped[0][:, 0:tw], data1=acc[:, 0:tw],
295
+ data2=tmp[:, 0:tw], op=nl.add)
296
+ nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[1][:, 0:tw],
297
+ data2=tab[1][tt][:, 0:tw], op=nl.multiply)
298
+ nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[0][:, 0:tw],
299
+ data2=tab[3][tt][:, 0:tw], op=nl.multiply)
300
+ nisa.tensor_tensor(dst=roped[1][:, 0:tw], data1=acc[:, 0:tw],
301
+ data2=tmp[:, 0:tw], op=nl.add)
302
+ for b in range(quad):
303
+ lh = j * quad + b
304
+ r0 = b * half
305
+ if qk == 1:
306
+ for par2 in range(2):
307
+ nisa.tensor_copy(
308
+ dst=k_tiles[lh][tt][par2 * half:(par2 + 1) * half, 0:tw],
309
+ src=roped[par2][r0:r0 + half, 0:tw])
310
+ else:
311
+ for par2 in range(2):
312
+ nisa.dma_copy(
313
+ dst=q_priv[lh][0, par2 * half:(par2 + 1) * half, t0:t0 + tw],
314
+ src=roped[par2][r0:r0 + half, 0:tw])
315
+
316
+ # ---- attention for this group's heads -> o_feat[g] (SBUF, feature-major) ----
317
+ allocator.set_current_address(phase2_base)
318
+ o_sb = []
319
+ for _lh in range(local_heads):
320
+ oc = (_lh * D) // _P_MAX
321
+ rr = (_lh * D) % _P_MAX
322
+ grp = []
323
+ for _g in range(n_grp):
324
+ grp.append(o_feat[g][oc][rr:rr + D, _g * _P_MAX:(_g + 1) * _P_MAX])
325
+ o_sb.append(grp)
326
+ attn_base = allocator.get_current_address()
327
+ for lh in range(local_heads):
328
+ _attention_cte(
329
+ q_priv[lh], None, None,
330
+ scale=scale, causal_mask=False,
331
+ tp_q=False, tp_k=False, tp_out=True,
332
+ cache_softmax=False,
333
+ k_cache_sbuf=k_tiles[lh],
334
+ v_cache_sbuf=v_tiles[lh],
335
+ out_o_hbm=o_sb[lh],
336
+ init_sbuf_addr=attn_base,
337
+ seqlen_k_valid=n_valid,
338
+ )
339
+ allocator.set_current_address(attn_base)
340
+
341
+ # ---- ONE out-projection over ALL G*n_ochunks feature chunks, full bias once ----
342
+ allocator.set_current_address(img_base)
343
+ if resident:
344
+ wo_sb = r_wo
345
+ bo_bc = r_bo
346
+ else:
347
+ wo_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
348
+ block_dim=[G * n_ochunks], num_free_tiles=[G * n_ochunks])
349
+ for oc in range(G * n_ochunks):
350
+ nisa.dma_copy(dst=wo_sb[oc], src=wot_hbm[oc * _P_MAX:(oc + 1) * _P_MAX, 0:H])
351
+ bo_bc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
352
+ nisa.dma_copy(dst=bo_bc, src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
353
+ out_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16, align_to=32)
354
+ n_ob = (H + _PSUM_FMAX - 1) // _PSUM_FMAX
355
+ for tb in range(n_grp):
356
+ for ob in range(n_ob):
357
+ ow = min(_PSUM_FMAX, H - ob * _PSUM_FMAX)
358
+ ps_out = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32, buffer=nl.psum,
359
+ address=(0, 0))
360
+ for g in range(G):
361
+ for oc in range(n_ochunks):
362
+ nisa.nc_matmul(
363
+ dst=ps_out[:, 0:ow],
364
+ stationary=o_feat[g][oc][:, tb * _P_MAX:(tb + 1) * _P_MAX],
365
+ moving=wo_sb[g * n_ochunks + oc][:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow],
366
+ accumulate=((g > 0) or (oc > 0)),
367
+ )
368
+ nisa.tensor_tensor(dst=out_sb[:, 0:ow], data1=ps_out[:, 0:ow],
369
+ data2=bo_bc[:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow], op=nl.add)
370
+ nisa.dma_copy(
371
+ dst=result.ap(pattern=[[H, _P_MAX], [1, ow]],
372
+ offset=rofs + tb * _P_MAX * H + ob * _PSUM_FMAX),
373
+ src=out_sb[:, 0:ow])
374
+ return result
375
+
376
+
377
+ _ATTN_L1 = wrap_nki_to_torch_hop(_attention_block_l1_kernel[1])
378
+
379
+
380
+ @nki_op("vit_enc::attention_block_l1", mutates_args={})
381
+ def attention_block_l1(
382
+ h: torch.Tensor,
383
+ wqk: torch.Tensor,
384
+ bqk: torch.Tensor,
385
+ tab: torch.Tensor,
386
+ wvt: torch.Tensor,
387
+ bv: torch.Tensor,
388
+ wot: torch.Tensor,
389
+ bo: torch.Tensor,
390
+ scale: float,
391
+ head_dim: int,
392
+ n_valid: int,
393
+ nimg: int = 1,
394
+ resident: int = 0,
395
+ ) -> torch.Tensor:
396
+ """All head groups in one program. Returns ONE [1, nimg*N, H] bf16 result (full bias)."""
397
+ for t in (h, wqk, bqk, tab, wvt, bv, wot, bo):
398
+ if t is not None and t.requires_grad:
399
+ raise RuntimeError("attention_block_l1 is inference-only; use torch.no_grad().")
400
+ if h.shape[0] // nimg > 2048:
401
+ raise NotImplementedError("attention_block_l1 supports N<=2048 only")
402
+ return _ATTN_L1(h, wqk, bqk, tab, wvt, bv, wot, bo, scale, head_dim, n_valid, nimg, resident)
build/torch-neuron/nki_kernels/attention_block_l1_ln.py ADDED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Attention for LNC=1, all head groups, with the residual add + LN2 fused into the epilogue.
2
+
3
+ Same body as ``attention_block_l1``. The epilogue differs: per 128-token block the
4
+ out-projection for ALL H columns lands in an fp32 SBUF row tile, then
5
+ x_new = x + (attn @ Wo + bo) (fp32; attention output never rounded to bf16)
6
+ z2 = LN_no_affine(x_new) (bf16; same stat chain as res_layernorm.py)
7
+ and both are written to HBM. This replaces K3 (one launch + a bf16 [N,H] HBM write/read per layer).
8
+
9
+ --- attention_block_l1 notes ---
10
+ Attention for LNC=1: ONE program over ALL head groups.
11
+
12
+ The 2-core kernel in ``attention_block.py`` owns one 8-head group per program. At LNC=1 that
13
+ means one launch per group per layer (2 for ViT-L, 3 for ViT-H+), each re-transposing x, each
14
+ re-loading the rope tables, each writing an [N, H] bf16 out-projection PARTIAL to HBM that the
15
+ LN-fold kernel then sums.
16
+
17
+ This kernel, per image (``nimg`` images stacked on the row axis, one launch):
18
+ * transposes x once and loads the rope tables once (shared by every group: tables are
19
+ quad-tiled per head, identical for all heads);
20
+ * for each group: projects Q/K/V + RoPE (same producer as attention_block), runs the vendor
21
+ attention per head, and writes O feature-major into a PERSISTENT per-group SBUF slab;
22
+ * after all groups: ONE out-projection contracting over ALL features (G*4 chunks accumulate
23
+ in one fp32 PSUM tile), adds the FULL bias once, writes ONE [N, H] result.
24
+ No partials, no host sum; the LN-fold kernel receives a single branch.
25
+
26
+ ``resident=1``: Q/K/V/O weights, biases and rope tables are staged ONCE per launch (bottom of
27
+ SBUF) and reused by every image. Host decides by an SBUF estimate (does not fit at N>=1024 for
28
+ ViT-L); ``resident=0`` restages each group's weights per image.
29
+
30
+ N<=2048 only (the shipping single-section vendor path). Numerics: identical producer and vendor
31
+ attention; the out-projection now accumulates all groups in fp32 PSUM instead of summing bf16
32
+ per-group partials, so results differ from attention_block by bf16 rounding of the partials.
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import torch
37
+ from nki.framework.kernel import wrap_nki_to_torch_hop
38
+ from torch_neuronx import nki_op
39
+
40
+ import nki
41
+ import nki.isa as nisa
42
+ import nki.language as nl
43
+ from nki.isa import engine
44
+
45
+ from nkilib.core.utils.modular_allocator import ModularAllocator
46
+
47
+ from .attention_inner import _attention_cte
48
+
49
+ _P_MAX = 128
50
+ _PSUM_FMAX = 512
51
+ _PSUM_BANK = 2048
52
+ _K_TILE = 512
53
+ _V_TILE = 128
54
+
55
+
56
+ def _kernel_assert(cond: bool, msg: str):
57
+ assert cond, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {msg}"
58
+
59
+
60
+ def sbuf_estimate(H: int, Fp: int, N: int, D: int, resident: bool) -> int:
61
+ """Conservative bytes/lane at the kernel's peak (host-side decision helper, not exact)."""
62
+ G = Fp // _PSUM_FMAX
63
+ n_chunks = H // _P_MAX
64
+ n_tt = (N + _PSUM_FMAX - 1) // _PSUM_FMAX
65
+ n_ktiles = (N + _K_TILE - 1) // _K_TILE
66
+ lh = _PSUM_FMAX // D
67
+ n_och = (lh * D) // _P_MAX
68
+ w_grp = 3 * n_chunks * _PSUM_FMAX * 2 + _PSUM_FMAX * 4 + 64 # wqk + wv + bv_bc + biases
69
+ w_out = G * n_och * H * 2 + H * 4 # wo + bo_bc
70
+ tab = 4 * n_tt * _PSUM_FMAX * 4
71
+ o_feat = G * n_och * N * 2
72
+ x_t = n_chunks * n_tt * _PSUM_FMAX * 2
73
+ kv = lh * (n_ktiles * _K_TILE * 2 + (N // _V_TILE) * D * 2)
74
+ work = 2 * _PSUM_FMAX * 2 * 2 + 2 * _PSUM_FMAX * 4 + _PSUM_FMAX * 4
75
+ inner = 48 * 1024
76
+ resid = (G * w_grp + w_out + tab) if resident else 0
77
+ base = resid + o_feat + x_t + (0 if resident else tab)
78
+ p1 = kv + (0 if resident else w_grp) + work
79
+ p2 = kv + inner
80
+ p3 = (0 if resident else w_out) + 2 * (H * 4 * 2 + H * 2 + 32) + H * 4
81
+ return base + max(p1, p2, p3)
82
+
83
+
84
+ @nki.jit
85
+ def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_hbm,
86
+ wot_hbm, bo_hbm, x_hbm, scale, head_dim, n_valid, eps, nimg=1, resident=0):
87
+ NT, H = h_hbm.shape
88
+ N = NT // nimg
89
+ Fp = wqk_hbm.shape[2]
90
+ D = head_dim
91
+ half = D // 2
92
+ quad = _P_MAX // half
93
+ local_heads = _PSUM_FMAX // D
94
+ local_quads = local_heads // quad
95
+ G = Fp // _PSUM_FMAX # head groups, ALL handled by this one program
96
+ n_chunks = H // _P_MAX
97
+ n_tt = (N + _PSUM_FMAX - 1) // _PSUM_FMAX
98
+ n_ktiles = (N + _K_TILE - 1) // _K_TILE
99
+ n_vtiles = N // _V_TILE
100
+ n_ochunks = (local_heads * D) // _P_MAX # feature chunks per group
101
+ v_per_tt = _PSUM_FMAX // _V_TILE
102
+
103
+ _kernel_assert(wqk_hbm.shape == (2, H, Fp), "bad wqk")
104
+ _kernel_assert(bqk_hbm.shape == (2, Fp, 1), "bad bqk")
105
+ _kernel_assert(tab_hbm.shape == (4, _P_MAX, N), "bad tab")
106
+ _kernel_assert(wvt_hbm.shape == (H, Fp) and bv_hbm.shape == (1, Fp), "bad V weights")
107
+ _kernel_assert(wot_hbm.shape == (Fp, H) and bo_hbm.shape == (1, H), "bad O weights")
108
+ _kernel_assert(H % _P_MAX == 0, "H must be a multiple of 128")
109
+ _kernel_assert(quad * half == _P_MAX, "D must give quad*half=128")
110
+ _kernel_assert(Fp % _PSUM_FMAX == 0, "padded features must be a multiple of 512")
111
+ _kernel_assert(N % _P_MAX == 0, "N must be a multiple of 128")
112
+ _kernel_assert(N <= _PSUM_FMAX * 4, "N>2048 not supported")
113
+ _kernel_assert(NT == nimg * N, "rows must be nimg x N")
114
+
115
+ _kernel_assert(x_hbm.shape == (NT, H), "x must be [nimg*N, H] fp32")
116
+ xnew_hbm = nl.ndarray((NT, H), dtype=nl.float32, buffer=nl.shared_hbm)
117
+ z_hbm = nl.ndarray((NT, H), dtype=nl.bfloat16, buffer=nl.shared_hbm)
118
+ inv_h = 1.0 / float(H)
119
+ neg_inv_h = -inv_h
120
+
121
+ # ---------------- resident (per-launch invariant) staging ----------------
122
+ res_top = 0
123
+ r_w = []
124
+ r_wv = []
125
+ r_b = []
126
+ r_bv = []
127
+ if resident:
128
+ ra = ModularAllocator(initial_address=0)
129
+ for g in range(G):
130
+ col0 = g * _PSUM_FMAX
131
+ w_sb_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2 * _P_MAX), dtype=nl.bfloat16,
132
+ block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
133
+ for qk in range(2):
134
+ for c in range(n_chunks):
135
+ nisa.dma_copy(dst=w_sb_[qk][c],
136
+ src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX,
137
+ col0:col0 + local_quads * 2 * _P_MAX])
138
+ wv_sb_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
139
+ block_dim=[n_chunks], num_free_tiles=[n_chunks])
140
+ for c in range(n_chunks):
141
+ nisa.dma_copy(dst=wv_sb_[c],
142
+ src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX, col0:col0 + local_heads * D])
143
+ b_sb_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
144
+ block_dim=[2, local_quads * 2],
145
+ num_free_tiles=[2, local_quads * 2], align_to=4)
146
+ b_stage_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
147
+ for qk in range(2):
148
+ for gg in range(local_quads * 2):
149
+ nisa.dma_copy(dst=b_stage_,
150
+ src=bqk_hbm[qk, col0 + gg * _P_MAX:col0 + (gg + 1) * _P_MAX, 0:1])
151
+ nisa.tensor_copy(dst=b_sb_[qk][gg], src=b_stage_)
152
+ bv_bc_ = ra.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
153
+ nisa.dma_copy(dst=bv_bc_,
154
+ src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]], offset=col0))
155
+ r_w.append(w_sb_)
156
+ r_wv.append(wv_sb_)
157
+ r_b.append(b_sb_)
158
+ r_bv.append(bv_bc_)
159
+ r_tab = ra.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
160
+ block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
161
+ for s in range(n_tt):
162
+ for t_i in range(4):
163
+ _tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
164
+ nisa.dma_copy(dst=r_tab[t_i][s][:, 0:_tw],
165
+ src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
166
+ r_wo = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
167
+ block_dim=[G * n_ochunks], num_free_tiles=[G * n_ochunks])
168
+ for oc in range(G * n_ochunks):
169
+ nisa.dma_copy(dst=r_wo[oc], src=wot_hbm[oc * _P_MAX:(oc + 1) * _P_MAX, 0:H])
170
+ r_bo = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
171
+ nisa.dma_copy(dst=r_bo, src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
172
+ res_top = ra.get_current_address()
173
+
174
+ n_grp = N // _P_MAX
175
+ for img in range(nimg):
176
+ rofs = img * N * H
177
+ allocator = ModularAllocator(initial_address=res_top)
178
+
179
+ # O for ALL groups stays in SBUF until the single out-projection.
180
+ o_feat = []
181
+ for g in range(G):
182
+ of_ = allocator.alloc_sbuf_tensor(shape=(_P_MAX, N), dtype=nl.bfloat16,
183
+ block_dim=[n_ochunks], num_free_tiles=[n_ochunks],
184
+ align_to=32)
185
+ o_feat.append(of_)
186
+
187
+ # x^T: shared by every group (transposed ONCE per image).
188
+ x_t = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
189
+ block_dim=[n_chunks, n_tt], num_free_tiles=[n_chunks, n_tt],
190
+ align_to=32)
191
+ for s in range(n_tt):
192
+ for c in range(n_chunks):
193
+ nisa.dma_transpose(
194
+ dst=x_t[c][s].ap([[_PSUM_FMAX, _P_MAX], [1, 1], [1, 1],
195
+ [1, min(_PSUM_FMAX, N - s * _PSUM_FMAX)]], offset=0),
196
+ src=h_hbm.ap([[H, min(_PSUM_FMAX, N - s * _PSUM_FMAX)], [1, 1], [1, 1], [1, _P_MAX]],
197
+ offset=rofs + (s * _PSUM_FMAX) * H + c * _P_MAX),
198
+ )
199
+
200
+ # rope tables: shared by every group.
201
+ if resident:
202
+ tab = r_tab
203
+ else:
204
+ tab = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
205
+ block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
206
+ for s in range(n_tt):
207
+ for t_i in range(4):
208
+ _tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
209
+ nisa.dma_copy(dst=tab[t_i][s][:, 0:_tw],
210
+ src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
211
+
212
+ img_base = allocator.get_current_address()
213
+
214
+ for g in range(G):
215
+ allocator.set_current_address(img_base)
216
+ col0 = g * _PSUM_FMAX
217
+
218
+ k_tiles = []
219
+ v_tiles = []
220
+ for _lh in range(local_heads):
221
+ kt_ = allocator.alloc_sbuf_tensor(shape=(D, _K_TILE), dtype=nl.bfloat16,
222
+ block_dim=[n_ktiles], num_free_tiles=[n_ktiles],
223
+ align_to=32)
224
+ k_tiles.append(kt_)
225
+ vt_ = allocator.alloc_sbuf_tensor(shape=(_V_TILE, D), dtype=nl.bfloat16,
226
+ block_dim=[n_vtiles], num_free_tiles=[n_vtiles])
227
+ v_tiles.append(vt_)
228
+ phase2_base = allocator.get_current_address()
229
+
230
+ if resident:
231
+ w_sb = r_w[g]
232
+ wv_sb = r_wv[g]
233
+ b_sb = r_b[g]
234
+ bv_bc = r_bv[g]
235
+ else:
236
+ w_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2 * _P_MAX),
237
+ dtype=nl.bfloat16,
238
+ block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
239
+ for qk in range(2):
240
+ for c in range(n_chunks):
241
+ nisa.dma_copy(dst=w_sb[qk][c],
242
+ src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX,
243
+ col0:col0 + local_quads * 2 * _P_MAX])
244
+ wv_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
245
+ block_dim=[n_chunks], num_free_tiles=[n_chunks])
246
+ for c in range(n_chunks):
247
+ nisa.dma_copy(dst=wv_sb[c],
248
+ src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX, col0:col0 + local_heads * D])
249
+ b_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
250
+ block_dim=[2, local_quads * 2],
251
+ num_free_tiles=[2, local_quads * 2], align_to=4)
252
+ b_stage = allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
253
+ for qk in range(2):
254
+ for gg in range(local_quads * 2):
255
+ nisa.dma_copy(dst=b_stage,
256
+ src=bqk_hbm[qk, col0 + gg * _P_MAX:col0 + (gg + 1) * _P_MAX, 0:1])
257
+ nisa.tensor_copy(dst=b_sb[qk][gg], src=b_stage)
258
+ bv_bc = allocator.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32,
259
+ align_to=4)
260
+ nisa.dma_copy(dst=bv_bc,
261
+ src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]], offset=col0))
262
+
263
+ proj = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
264
+ block_dim=[2], num_free_tiles=[2])
265
+ acc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32, align_to=4)
266
+ tmp = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32, align_to=4)
267
+ roped = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
268
+ block_dim=[2], num_free_tiles=[2])
269
+ vout = allocator.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32,
270
+ align_to=4)
271
+ q_priv = []
272
+ for _lh in range(local_heads):
273
+ q_priv.append(nl.ndarray((1, D, N), dtype=nl.bfloat16, buffer=nl.private_hbm))
274
+
275
+ # ---- producer (identical to attention_block's N<=2048 path) ----
276
+ for tt in range(n_tt):
277
+ t0 = tt * _PSUM_FMAX
278
+ tw = min(_PSUM_FMAX, N - t0)
279
+ for vsub in range(tw // _V_TILE):
280
+ ps_v = nl.ndarray((_V_TILE, local_heads * D), dtype=nl.float32,
281
+ buffer=nl.psum, address=(0, 7 * _PSUM_BANK))
282
+ for c in range(n_chunks):
283
+ nisa.nc_matmul(dst=ps_v,
284
+ stationary=x_t[c][tt][:, vsub * _V_TILE:(vsub + 1) * _V_TILE],
285
+ moving=wv_sb[c], accumulate=(c > 0))
286
+ nisa.tensor_tensor(dst=vout, data1=ps_v, data2=bv_bc, op=nl.add)
287
+ for lh in range(local_heads):
288
+ nisa.tensor_copy(dst=v_tiles[lh][tt * v_per_tt + vsub],
289
+ src=vout[:, lh * D:(lh + 1) * D], engine=engine.scalar)
290
+ for qk in range(2):
291
+ for j in range(local_quads):
292
+ for par in range(2):
293
+ gq = j * 2 + par
294
+ cols = gq * _P_MAX
295
+ ps = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32, buffer=nl.psum,
296
+ address=(0, (5 + par) * _PSUM_BANK))
297
+ for c in range(n_chunks):
298
+ nisa.nc_matmul(dst=ps[:, 0:tw],
299
+ stationary=w_sb[qk][c][:, cols:cols + _P_MAX],
300
+ moving=x_t[c][tt][:, 0:tw], accumulate=(c > 0))
301
+ nisa.tensor_scalar(dst=proj[par][:, 0:tw], data=ps[:, 0:tw],
302
+ op0=nl.add, operand0=b_sb[qk][gq])
303
+ nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[0][:, 0:tw],
304
+ data2=tab[0][tt][:, 0:tw], op=nl.multiply)
305
+ nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[1][:, 0:tw],
306
+ data2=tab[2][tt][:, 0:tw], op=nl.multiply)
307
+ nisa.tensor_tensor(dst=roped[0][:, 0:tw], data1=acc[:, 0:tw],
308
+ data2=tmp[:, 0:tw], op=nl.add)
309
+ nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[1][:, 0:tw],
310
+ data2=tab[1][tt][:, 0:tw], op=nl.multiply)
311
+ nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[0][:, 0:tw],
312
+ data2=tab[3][tt][:, 0:tw], op=nl.multiply)
313
+ nisa.tensor_tensor(dst=roped[1][:, 0:tw], data1=acc[:, 0:tw],
314
+ data2=tmp[:, 0:tw], op=nl.add)
315
+ for b in range(quad):
316
+ lh = j * quad + b
317
+ r0 = b * half
318
+ if qk == 1:
319
+ for par2 in range(2):
320
+ nisa.tensor_copy(
321
+ dst=k_tiles[lh][tt][par2 * half:(par2 + 1) * half, 0:tw],
322
+ src=roped[par2][r0:r0 + half, 0:tw])
323
+ else:
324
+ for par2 in range(2):
325
+ nisa.dma_copy(
326
+ dst=q_priv[lh][0, par2 * half:(par2 + 1) * half, t0:t0 + tw],
327
+ src=roped[par2][r0:r0 + half, 0:tw])
328
+
329
+ # ---- attention for this group's heads -> o_feat[g] (SBUF, feature-major) ----
330
+ allocator.set_current_address(phase2_base)
331
+ o_sb = []
332
+ for _lh in range(local_heads):
333
+ oc = (_lh * D) // _P_MAX
334
+ rr = (_lh * D) % _P_MAX
335
+ grp = []
336
+ for _g in range(n_grp):
337
+ grp.append(o_feat[g][oc][rr:rr + D, _g * _P_MAX:(_g + 1) * _P_MAX])
338
+ o_sb.append(grp)
339
+ attn_base = allocator.get_current_address()
340
+ for lh in range(local_heads):
341
+ _attention_cte(
342
+ q_priv[lh], None, None,
343
+ scale=scale, causal_mask=False,
344
+ tp_q=False, tp_k=False, tp_out=True,
345
+ cache_softmax=False,
346
+ k_cache_sbuf=k_tiles[lh],
347
+ v_cache_sbuf=v_tiles[lh],
348
+ out_o_hbm=o_sb[lh],
349
+ init_sbuf_addr=attn_base,
350
+ seqlen_k_valid=n_valid,
351
+ )
352
+ allocator.set_current_address(attn_base)
353
+
354
+ # ---- ONE out-projection over ALL G*n_ochunks feature chunks, full bias once ----
355
+ allocator.set_current_address(img_base)
356
+ if resident:
357
+ wo_sb = r_wo
358
+ bo_bc = r_bo
359
+ else:
360
+ wo_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
361
+ block_dim=[G * n_ochunks], num_free_tiles=[G * n_ochunks])
362
+ for oc in range(G * n_ochunks):
363
+ nisa.dma_copy(dst=wo_sb[oc], src=wot_hbm[oc * _P_MAX:(oc + 1) * _P_MAX, 0:H])
364
+ bo_bc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
365
+ nisa.dma_copy(dst=bo_bc, src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
366
+ # ---- epilogue: out-proj (all groups, fp32) + bias + residual -> x_new ; LN -> z2 ----
367
+ _RING = 2
368
+ row = []
369
+ a_t = []
370
+ z_t = []
371
+ st = []
372
+ for _i in range(_RING):
373
+ row.append(allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4))
374
+ a_t.append(allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4))
375
+ z_t.append(allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16, align_to=32))
376
+ st.append(allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
377
+ block_dim=[5], num_free_tiles=[5], align_to=4))
378
+ sink = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
379
+ n_ob = (H + _PSUM_FMAX - 1) // _PSUM_FMAX
380
+ for tb in range(n_grp):
381
+ k = tb % _RING
382
+ r0 = img * N + tb * _P_MAX
383
+ nisa.dma_copy(dst=a_t[k], src=x_hbm[r0:r0 + _P_MAX, 0:H])
384
+ for ob in range(n_ob):
385
+ ow = min(_PSUM_FMAX, H - ob * _PSUM_FMAX)
386
+ ps_out = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32, buffer=nl.psum,
387
+ address=(0, ((tb * n_ob + ob) % 4) * _PSUM_BANK))
388
+ for g in range(G):
389
+ for oc in range(n_ochunks):
390
+ nisa.nc_matmul(
391
+ dst=ps_out[:, 0:ow],
392
+ stationary=o_feat[g][oc][:, tb * _P_MAX:(tb + 1) * _P_MAX],
393
+ moving=wo_sb[g * n_ochunks + oc][:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow],
394
+ accumulate=((g > 0) or (oc > 0)),
395
+ )
396
+ nisa.tensor_tensor(dst=row[k][:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow],
397
+ data1=ps_out[:, 0:ow],
398
+ data2=bo_bc[:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow], op=nl.add)
399
+ nisa.tensor_tensor(dst=row[k], data1=row[k], data2=a_t[k], op=nl.add)
400
+ nisa.dma_copy(dst=xnew_hbm[r0:r0 + _P_MAX, 0:H], src=row[k])
401
+ # stat chain (identical ops/order to res_layernorm.py)
402
+ neg_shift = st[k][0]
403
+ acc_s = st[k][1]
404
+ neg_mu = st[k][2]
405
+ var = st[k][3]
406
+ rstd = st[k][4]
407
+ nisa.tensor_scalar(dst=neg_shift, data=row[k][0:_P_MAX, 0:1],
408
+ op0=nl.multiply, operand0=-1.0)
409
+ nisa.activation_reduce(dst=sink, op=nl.copy, data=row[k],
410
+ reduce_op=nl.add, reduce_res=acc_s, bias=neg_shift, scale=1.0)
411
+ nisa.tensor_scalar(dst=neg_mu, data=acc_s, op0=nl.multiply, operand0=neg_inv_h,
412
+ op1=nl.add, operand1=neg_shift)
413
+ nisa.activation_reduce(dst=sink, op=nl.square, data=row[k],
414
+ reduce_op=nl.add, reduce_res=acc_s, bias=neg_mu, scale=1.0)
415
+ nisa.tensor_scalar(dst=var, data=acc_s, op0=nl.multiply, operand0=inv_h,
416
+ op1=nl.add, operand1=eps)
417
+ nisa.activation(dst=rstd, op=nl.rsqrt, data=var)
418
+ nisa.tensor_scalar(dst=z_t[k], data=row[k], op0=nl.add, operand0=neg_mu,
419
+ op1=nl.multiply, operand1=rstd)
420
+ nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:H], src=z_t[k])
421
+ return xnew_hbm, z_hbm
422
+
423
+
424
+ _ATTN_L1_LN = wrap_nki_to_torch_hop(_attention_block_l1_ln_kernel[1])
425
+
426
+
427
+ @nki_op("vit_enc::attention_block_l1_ln", mutates_args={})
428
+ def attention_block_l1_ln(
429
+ h: torch.Tensor,
430
+ wqk: torch.Tensor,
431
+ bqk: torch.Tensor,
432
+ tab: torch.Tensor,
433
+ wvt: torch.Tensor,
434
+ bv: torch.Tensor,
435
+ wot: torch.Tensor,
436
+ bo: torch.Tensor,
437
+ x: torch.Tensor,
438
+ scale: float,
439
+ head_dim: int,
440
+ n_valid: int,
441
+ eps: float,
442
+ nimg: int = 1,
443
+ resident: int = 0,
444
+ ) -> tuple[torch.Tensor, torch.Tensor]:
445
+ """All-group attention + residual + affine-free LN2. Returns (x_new [R,H] fp32, z2 [R,H] bf16)."""
446
+ for t in (h, wqk, bqk, tab, wvt, bv, wot, bo, x):
447
+ if t is not None and t.requires_grad:
448
+ raise RuntimeError("attention_block_l1_ln is inference-only; use torch.no_grad().")
449
+ if h.shape[0] // nimg > 2048:
450
+ raise NotImplementedError("attention_block_l1_ln supports N<=2048 only")
451
+ return _ATTN_L1_LN(h, wqk, bqk, tab, wvt, bv, wot, bo, x, scale, head_dim, n_valid, eps, nimg, resident)
build/torch-neuron/nki_kernels/attention_inner.py ADDED
@@ -0,0 +1,1220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License").
4
+ # You may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ #
13
+ # MODIFIED from the AWS Neuron SDK's nkilib ``core/attention/attention_cte.py``
14
+ # (Apache-2.0, Copyright Amazon.com, Inc. or its affiliates). Modifications:
15
+ # 1. Four engine-placement edits (two memsets -> GpSimd, the tp_out eviction
16
+ # tensor_copy -> Scalar, the list-branch write-back tensor_copy -> GpSimd),
17
+ # measured worth ~1.19x end-to-end in the V-JEPA2 encoder.
18
+ # 2. Code paths unreachable for this model's configuration removed (causal/SWA
19
+ # masks, prefix caching, flash sectioning at seq<=10240, sinks, fp8,
20
+ # sequence packing, CP) -- the executed instruction stream is unchanged.
21
+ # The original supports the general case; use nkilib for anything but this
22
+ # non-causal, single-section, SBUF-KV configuration.
23
+ """K2 inner attention: the vendor ``_attention_cte`` context-encoding kernel, FLATTENED to
24
+ exactly the branches the 4-kernel V-JEPA2 encoder reaches, with the four h8/h10 engine pins
25
+ applied in place. Self-contained: imports only ``nki``, ``nkilib``, ``torch`` (none here) --
26
+ NO imports from ``experiment/`` or the repo-root incumbent modules.
27
+
28
+ WHAT THIS COMPUTES
29
+ ==================
30
+ ``Output = softmax(scale * Q @ K^T) @ V`` for ONE head, called 8x per core by the K2 outer
31
+ kernel (``attention_block.py``). The pipeline, in order, per 128-token Q group:
32
+
33
+ load_q -> QK^T (MM1) + row-max -> update running max -> exp (softmax numerator)
34
+ -> P@V (MM2) -> write-back (reciprocal-normalise, evict to the caller's SBUF O).
35
+
36
+ Group software-pipelining overlaps three groups at a time (grp_i does PV+writeback, grp_i+1
37
+ does exp, grp_i+2 does load-Q + QK/max), which is the vendor's Level-4 loop structure.
38
+
39
+ PSUM BANK PLAN (this config, tp_out=True, single 2048 section)
40
+ --------------------------------------------------------------
41
+ * MM1 (QK^T) tiles: banks {0,1,2,3} (``address=(0, (k_tile_idx % 4) * PSUM_BANK_SIZE)``).
42
+ * MM2 (P@V) tiles: banks {4,5,6,7} (``address=(0, (4 + large_tile_idx % 4) * ...)``).
43
+ * tp reciprocal / correction transposes: bank {3 or 7} (``((grp%2)*4 + 3) * ...``).
44
+ The producer (K2 outer) owns banks 5/6/7 during its phase; the attention region runs after the
45
+ producer's high-water mark, so there is no live overlap.
46
+
47
+ RING / MULTI-BUFFER DEPTHS (from the dp/dp2/dp4 fork lineage; kept verbatim)
48
+ ---------------------------------------------------------------------------
49
+ * ``exp_sb`` group-axis depth 2 (dp2): exp(g+1) can run while transpose(g) still drains.
50
+ * ``exp_tp_sb`` group-axis depth 8 (dp4): P^T transposes issue as soon as their exp output
51
+ exists, spreading DMA traffic out of the P.V collision window.
52
+
53
+ THE FOUR ENGINE PINS (h8/h10), and WHY each exists (one line each, from PERF_INVENTORY)
54
+ ---------------------------------------------------------------------------------------
55
+ 1. ``_scale_reciprocal_write_back_impl`` tp_out PSUM->SBUF evict ``tensor_copy`` -> SCALAR
56
+ (h8 site 1): the un-pinned copy defaults to Vector, K2's binding engine; Scalar/Vector are
57
+ the only legal engines (PSUM source excludes GpSimd) and Scalar is the exercised majority.
58
+ 2. ``_write_back_o_impl`` tp_out list-branch ``tensor_copy`` -> GPSIMD (h8 site 3): SBUF->SBUF
59
+ fp32->bf16 CAST, all three engines legal, GpSimd is the idle engine so the copy sheds off
60
+ Vector.
61
+ 3. ``_qk_and_max_impl`` ``memset`` -> GPSIMD (h10 :2876): initialises mm1_partial_max, first
62
+ consumer is several ops downstream, so a slower idle engine is safe and sheds Vector.
63
+ 4. ``_fused_qkmax_and_pv_impl`` ``memset`` -> GPSIMD (h10 :3294): same rationale, 2688/core.
64
+ The vendor ``_exp_impl`` memset (dp4 :2993) stays UNPINNED (engine.unknown / Vector) on purpose
65
+ -- pinning it would fork ``_exp_impl`` and re-home the softmax-exp source strings; it MUST stay
66
+ unpinned to reproduce the shipping instruction stream.
67
+
68
+ DEAD PATHS REMOVED (§4 of PERF_INVENTORY) -- guard that made each unreachable for this call:
69
+ ``causal_mask=False, use_sbuf_kv=True, seqlen_k=2048<10240, bs=1, tp_out=True, fuse_v_proj=False,
70
+ no prior/APC/SWA/CP/seqpack/sink, cache_softmax=False``. See per-function docstrings.
71
+ The allocator usage (``nkilib`` ``ModularAllocator``) is kept EXACTLY as the vendor uses it.
72
+ """
73
+ from __future__ import annotations
74
+
75
+ from dataclasses import dataclass
76
+ from typing import Any, List, Optional
77
+
78
+ import nki
79
+ import nki.isa as nisa
80
+ import nki.language as nl
81
+ from nki.isa import engine, reduce_cmd
82
+ from nki.language.opcode import maximum as _maximum
83
+
84
+ from nkilib.core.utils.allocator import align_to
85
+ from nkilib.core.utils.kernel_assert import assert_shape, kernel_assert
86
+ from nkilib.core.utils.kernel_helpers import (
87
+ PSUM_BANK_SIZE,
88
+ div_ceil,
89
+ get_verified_program_sharding_info,
90
+ is_sbuf_tensor,
91
+ )
92
+ from nkilib.core.utils.logging import get_logger
93
+ from nkilib.core.utils.modular_allocator import ModularAllocator
94
+ from nkilib.core.utils.stream_shuffle_broadcast import stream_shuffle_broadcast
95
+
96
+ logger = get_logger("attention_inner")
97
+
98
+
99
+ _FLOAT32_MIN = -3.4028235e38 # used for initialization and masking
100
+
101
+ # Tested-range constraints (checked in _attention_cte).
102
+ _MAX_BS = 512
103
+ _MAX_SEQLEN = 131072
104
+ _MAX_BS_TIMES_SEQLEN_QK = 32.0 * 36864 * 36864
105
+ _MAX_HEAD_DIM = 128
106
+ _MIN_GLOBAL_CP_DEGREE = 1
107
+ _MAX_GLOBAL_CP_DEGREE = 32
108
+
109
+ # Tile size / threshold constants used by the LIVE path.
110
+ _Q_GRP_SZ = 128
111
+ _V_TILE_SZ = 128 # V tile size for loading and MM2 operations
112
+ _K_TILE_SZ = 512 # K tile size for loading and MM1+masking operations
113
+ _EXP_TILE_SZ = 512 # Tile size for exp instructions (must equal _K_TILE_SZ)
114
+ _LARGE_TILE_SZ = 2048 # Larger tile size for allocations/pipelining (4 x 512 tiles)
115
+ _FLASH_ATTENTION_THRESHOLD = 10 * 1024 # Use flash attention above this K/V length
116
+ _FLASH_ATTENTION_SECTION_LENGTH = 8 * 1024 # Section size when using flash attention
117
+
118
+
119
+ # ==========================================================================================
120
+ # Buffer / config dataclasses (kept verbatim -- the four required by the reached path).
121
+ # ==========================================================================================
122
+ @dataclass
123
+ class AttnConfig(nl.NKIObject):
124
+ """High-level attention configuration set at kernel entry point."""
125
+
126
+ seqlen_q: int = None
127
+ seqlen_k_active: int = None
128
+ seqlen_k_prior: int = None
129
+ d: int = None
130
+
131
+ tp_q: bool = None
132
+ tp_k: bool = None
133
+ tp_out: bool = None
134
+
135
+ is_prefix_caching: bool = None
136
+ causal_mask: bool = None
137
+
138
+ use_swa: bool = None
139
+ sliding_window: int = None
140
+
141
+ use_cp: bool = None
142
+ global_cp_deg: int = None
143
+ cp_strided_q_slicing: bool = None
144
+ cp_striped_input: bool = None
145
+
146
+ scale: float = None
147
+ cache_softmax: bool = None
148
+ skip_output_normalization: bool = None
149
+ dtype: Any = None
150
+ softmax_dtype: Any = None
151
+ mm_out_dtype: Any = None
152
+
153
+ is_sequence_packed: bool = None
154
+
155
+
156
+ @dataclass
157
+ class AttnInternalBuffers(nl.NKIObject):
158
+ """Container for all SBUF and PSUM tensor buffers used in attention computation."""
159
+
160
+ q_sb = None
161
+ k_sb = None
162
+ v_sb = None
163
+ k_sb_prior = None
164
+ v_sb_prior = None
165
+
166
+ k_scale_sb = None
167
+
168
+ mm1_psum = None
169
+ mm1_copy_sb = None
170
+ mm1_affine_select_output = None
171
+ mm1_masked = None
172
+ mm1_partial_max = None
173
+ mm1_section_max = None
174
+ mm1_running_max = None
175
+ prev_mm1_running_max = None
176
+ flash_attn_correction_factor = None
177
+
178
+ exp_sb = None
179
+ exp_partial_sum = None
180
+ exp_section_sum = None
181
+ exp_tp_sb = None
182
+ exp_running_sum = None
183
+ prev_exp_running_sum = None
184
+ exp_sum_reciprocal = None
185
+
186
+ mm2_psum = None
187
+ mm2_sb = None
188
+ mm2_prev_output = None
189
+ mm2_accum_flash_attn = None
190
+ mm2_final = None
191
+
192
+ tp_flash_attn_correction_factor_psum = None
193
+ tp_flash_attn_correction_factor_sb = None
194
+ tp_exp_sum_reciprocal_psum = None
195
+ tp_exp_sum_reciprocal_sb = None
196
+ mm2_prev_output_scaled = None
197
+
198
+ zero_bias_tensor = None
199
+ sink_sb = None
200
+ range_sel_lbs = None
201
+ range_sel_ubs = None
202
+ range_sel_lbs_prior = None
203
+ range_sel_ubs_prior = None
204
+ k_offset_sb_u32 = None
205
+
206
+
207
+ @dataclass
208
+ class SectionParams(nl.NKIObject):
209
+ section_idx = None
210
+ section_offset = None
211
+ section_offset_active = None
212
+ next_section_offset_active = None
213
+ section_contains_prefix = None
214
+ next_section_contains_prefix = None
215
+ kv_section_idx = None
216
+
217
+
218
+ @dataclass
219
+ class AttnTileParams(nl.NKIObject):
220
+ """Tile and buffer sizing parameters computed during implementation."""
221
+
222
+ seqlen_k_active_updated: int = None
223
+ seqlen_k_prior_padded: int = None
224
+ is_causal: bool = None
225
+
226
+ sb_p: int = None
227
+
228
+ num_grps: int = None
229
+ num_q_grps_per_load: int = None
230
+ can_pack_q_load: bool = None
231
+
232
+ num_large_tiles_per_section: int = None
233
+ num_k_tiles_per_section: int = None
234
+ num_v_tiles_per_section: int = None
235
+
236
+ exp_inst_elems: int = None
237
+ num_exp_insts_per_large_tile: int = None
238
+
239
+ num_tps_in_mm2_grp: int = None
240
+ mm2_grp_sz: int = None
241
+
242
+ use_swa_optimized_allocation: bool = None
243
+
244
+ dynamic_sel_mask: bool = None
245
+
246
+ section_len: int = None
247
+ num_sections: int = None
248
+
249
+
250
+ # ==========================================================================================
251
+ # Entry: _attention_cte. Dead paths removed: prefix caching / APC / sink / seqpack / CP / SWA /
252
+ # fused-v-proj / seqlen-q sharding remainder / cache_softmax. This call ALWAYS takes
253
+ # use_sbuf_kv=True, causal_mask=False, tp_out=True, single-shard (num_shard=1), bs=1.
254
+ # ==========================================================================================
255
+ def _attention_cte(
256
+ q: nl.ndarray,
257
+ k: Optional[nl.ndarray] = None,
258
+ v: Optional[nl.ndarray] = None,
259
+ scale: float = 1.0,
260
+ causal_mask: bool = True,
261
+ tp_q: bool = True,
262
+ tp_k: bool = False,
263
+ tp_out: bool = False,
264
+ cache_softmax: bool = False,
265
+ softmax_dtype=nl.float32,
266
+ mm_out_dtype=nl.float32,
267
+ k_cache_sbuf: Optional[List[nl.ndarray]] = None,
268
+ v_cache_sbuf: Optional[List[nl.ndarray]] = None,
269
+ out_o_hbm=None,
270
+ init_sbuf_addr: int = 0,
271
+ seqlen_k_valid: int = None,
272
+ ):
273
+ """One head of context-encoding attention (SBUF-KV path).
274
+ EDIT: ``seqlen_k_valid`` overrides the active key length (default len(k_cache)*512)
275
+ so keys at positions >= seqlen_k_valid (token padding) are skipped by every loop bound.
276
+
277
+ LIVE contract for the K2 encoder: ``k=v=None`` (K/V handed in via ``k_cache_sbuf`` /
278
+ ``v_cache_sbuf``), ``causal_mask=False``, ``tp_q=False, tp_k=False, tp_out=True``,
279
+ ``cache_softmax=False``, ``out_o_hbm`` is a LIST of SBUF slices into the caller's O tile.
280
+
281
+ DEAD (removed): prefix caching (k_prior/v_prior/prior_used_len), sink, sliding_window, CP
282
+ (cp_offset/global_cp_deg/striped/strided), sequence packing (bound_min/bound_max),
283
+ skip_output_normalization, fp8 k_scale, and the fused v-projection (h_hbm/wv_t/bv).
284
+ """
285
+ is_prefix_caching = False
286
+
287
+ use_sbuf_kv = (k_cache_sbuf is not None and len(k_cache_sbuf) > 0) or (
288
+ v_cache_sbuf is not None and len(v_cache_sbuf) > 0
289
+ )
290
+ kernel_assert(
291
+ k_cache_sbuf is not None and len(k_cache_sbuf) > 0, "k_cache_sbuf must be provided when using sbuf kv"
292
+ )
293
+ kernel_assert(
294
+ v_cache_sbuf is not None and len(v_cache_sbuf) > 0, "v_cache_sbuf must be provided when using sbuf kv"
295
+ )
296
+
297
+ seqlen_q, seqlen_k_active, seqlen_k_prior, d, out_shape, softmax_shape = _check_input_and_return_shape(
298
+ q,
299
+ k,
300
+ v,
301
+ is_prefix_caching,
302
+ None,
303
+ None,
304
+ None,
305
+ tp_q,
306
+ tp_k,
307
+ tp_out,
308
+ cache_softmax,
309
+ k_cache_sbuf,
310
+ v_cache_sbuf,
311
+ None,
312
+ None,
313
+ skip_v_check=False,
314
+ )
315
+ if seqlen_k_valid is not None:
316
+ kernel_assert(seqlen_k_valid <= seqlen_k_active, "seqlen_k_valid exceeds K cache length")
317
+ seqlen_k_active = seqlen_k_valid
318
+
319
+ # Use out_o_hbm if provided (LIVE: it is a list of SBUF slices).
320
+ if out_o_hbm is not None:
321
+ result = out_o_hbm
322
+ else:
323
+ result = nl.ndarray(shape=out_shape, dtype=q.dtype, buffer=nl.shared_hbm)
324
+
325
+ bs = q.shape[0]
326
+ bs_kv = 1 # sbuf-kv: batch_size_kv is per-batch tiles
327
+
328
+ kernel_assert(bs > 0, f"Batch size must be positive, got {bs}")
329
+ kernel_assert(bs <= _MAX_BS, f"attention_cte kernel is not tested for batch size above {_MAX_BS}, got {bs}.")
330
+ kernel_assert(bs_kv > 0, f"Batch size must be positive, got {bs_kv}")
331
+ kernel_assert(bs % bs_kv == 0, f"Q batch size must be a multiple of KV batch size, got {bs=}, {bs_kv=}")
332
+
333
+ seqlen_k_total = seqlen_k_active
334
+ kernel_assert(seqlen_q <= _MAX_SEQLEN, f"seqlen above {_MAX_SEQLEN}, got {seqlen_q=}.")
335
+ kernel_assert(seqlen_k_total <= _MAX_SEQLEN, f"seqlen above {_MAX_SEQLEN}, got {seqlen_k_total=}.")
336
+ bs_seqlen_qk_product = float(bs * seqlen_q) * seqlen_k_total
337
+ if bs_seqlen_qk_product > _MAX_BS_TIMES_SEQLEN_QK:
338
+ logger.warn(
339
+ f"attention_cte kernel is not tested for batch size x seqlen_q x seqlen_k above "
340
+ f"{_MAX_BS_TIMES_SEQLEN_QK}, got {bs_seqlen_qk_product=}.",
341
+ )
342
+
343
+ kernel_assert(d > 0, f"d must be > 0, got {d=}.")
344
+ kernel_assert(d <= _MAX_HEAD_DIM, f"we do not support head_dim > {_MAX_HEAD_DIM}, got head dim {d}")
345
+
346
+ kernel_assert(
347
+ (str(mm_out_dtype) == str(nl.float32))
348
+ or (str(mm_out_dtype) == str(nl.bfloat16) and (nisa.get_nc_version() >= nisa.nc_version.gen4)),
349
+ f"mm_out_dtype (psum) should be in [float32, bfloat16], got {mm_out_dtype} in {nisa.get_nc_version()}.",
350
+ )
351
+
352
+ ac = AttnConfig(
353
+ seqlen_q=seqlen_q,
354
+ seqlen_k_active=seqlen_k_active,
355
+ seqlen_k_prior=seqlen_k_prior,
356
+ d=d,
357
+ tp_q=tp_q,
358
+ tp_k=tp_k,
359
+ tp_out=tp_out,
360
+ is_prefix_caching=is_prefix_caching,
361
+ causal_mask=causal_mask,
362
+ use_swa=False,
363
+ sliding_window=0,
364
+ use_cp=False,
365
+ global_cp_deg=None,
366
+ cp_strided_q_slicing=False,
367
+ cp_striped_input=False,
368
+ scale=scale,
369
+ cache_softmax=cache_softmax,
370
+ skip_output_normalization=False,
371
+ dtype=q.dtype,
372
+ softmax_dtype=softmax_dtype,
373
+ mm_out_dtype=mm_out_dtype,
374
+ is_sequence_packed=False,
375
+ )
376
+
377
+ # Sharding info. use_sbuf_kv disables internal sharding (caller already sharded), so
378
+ # num_shard=1, shard_id=0, num_bs_per_shard=bs, has_remainder=False.
379
+ get_verified_program_sharding_info("attention_cte", max_sharding=2)
380
+ num_shard = 1
381
+ shard_id = 0
382
+ num_bs_per_shard = bs // num_shard
383
+ bs_offset = shard_id * num_bs_per_shard
384
+
385
+ for batch_idx in range(num_bs_per_shard):
386
+ # use_sbuf_kv: bs is LOCAL batch size and b is the LOCAL index.
387
+ batch_id = batch_idx
388
+ kv_batch_id = _q_to_kv_batch_id(batch_id, bs, bs_kv)
389
+ _attention_cte_impl(
390
+ q,
391
+ k,
392
+ v,
393
+ None,
394
+ None,
395
+ None,
396
+ result,
397
+ batch_id,
398
+ kv_batch_id,
399
+ ac,
400
+ k_cache_sbuf=k_cache_sbuf,
401
+ v_cache_sbuf=v_cache_sbuf,
402
+ init_sbuf_addr=init_sbuf_addr,
403
+ )
404
+
405
+ # has_remainder is False (num_shard==1); the seqlen-q sharding path is DEAD.
406
+ return result
407
+
408
+
409
+ def _attention_cte_impl(
410
+ q,
411
+ k_active,
412
+ v_active,
413
+ k_prior,
414
+ v_prior,
415
+ prior_used_len,
416
+ o,
417
+ batch_id,
418
+ batch_id_kv,
419
+ ac: AttnConfig,
420
+ k_cache_sbuf: Optional[List[nl.ndarray]] = None,
421
+ v_cache_sbuf: Optional[List[nl.ndarray]] = None,
422
+ init_sbuf_addr: int = 0,
423
+ ):
424
+ """Single-batch attention body. LIVE path only: single section (num_sections==1), pipelined
425
+ group loop (num_grps=16 > 1), SBUF-KV (no K/V HBM load), tp_out=True.
426
+
427
+ DEAD (removed): seqlen-q sharding, sink, SWA memset, sections>1 flash accumulation, K/V HBM
428
+ load (_load_k_tile / _load_v_tile), SBUF-prior storage, cache_softmax intermediate writes,
429
+ the non-pipelined (shard_seqlen_q_length<=1) branch.
430
+ """
431
+ # Not seqlen-sharded: start at group 0, cover all groups.
432
+ atp = _compute_tile_parameters(ac, is_seqlen_sharded=False)
433
+ shard_seqlen_q_start = 0
434
+ shard_seqlen_q_length = atp.num_grps
435
+
436
+ allocator = ModularAllocator(initial_address=init_sbuf_addr)
437
+ bufs = AttnInternalBuffers()
438
+ bufs.k_scale_sb = None
439
+
440
+ bufs.zero_bias_tensor = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, 1), dtype=nl.float32)
441
+ nisa.memset(bufs.zero_bias_tensor, 0.0)
442
+
443
+ # _setup_range_select_bounds is a NO-OP here: dynamic_sel_mask=False and not prefix caching.
444
+ _setup_range_select_bounds(ac, atp, bufs, allocator, None, None, None, None, batch_id=batch_id)
445
+
446
+ bufs.mm1_running_max = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, atp.num_grps), dtype=nl.float32)
447
+ bufs.exp_running_sum = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, atp.num_grps), dtype=nl.float32)
448
+ bufs.exp_sum_reciprocal = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, atp.num_grps), dtype=nl.float32)
449
+
450
+ sbuf_addr_outer = allocator.get_current_address()
451
+
452
+ # Single section (num_sections == 1).
453
+ for section_idx in range(atp.num_sections):
454
+ section_offset = atp.section_len * section_idx
455
+ section_offset_active, section_contains_prefix = _compute_section_offset_active(
456
+ section_offset, ac.is_prefix_caching, atp.seqlen_k_prior_padded
457
+ )
458
+ next_section_offset = atp.section_len * (section_idx + 1)
459
+ next_section_offset_active, next_section_contains_prefix = _compute_section_offset_active(
460
+ next_section_offset, ac.is_prefix_caching, atp.seqlen_k_prior_padded
461
+ )
462
+ sp = SectionParams(
463
+ section_idx=section_idx,
464
+ section_offset=section_offset,
465
+ section_offset_active=section_offset_active,
466
+ next_section_offset_active=next_section_offset_active,
467
+ section_contains_prefix=section_contains_prefix,
468
+ next_section_contains_prefix=next_section_contains_prefix,
469
+ )
470
+
471
+ allocator.set_current_address(sbuf_addr_outer)
472
+ _allocate_attention_buffers(allocator, ac, atp, bufs, sink=None,
473
+ k_cache_sbuf=k_cache_sbuf, v_cache_sbuf=v_cache_sbuf)
474
+ sbuf_addr = allocator.get_current_address()
475
+
476
+ # SBUF-KV: K and V are already resident, so the HBM K/V load block is skipped entirely.
477
+
478
+ # Software pipelining: num_grps=16 > 1, so the pipelined branch is the only reached one.
479
+ _load_q_impl(shard_seqlen_q_start, ac, atp, sp, bufs, q, batch_id, sbuf_addr)
480
+ _qk_and_max_impl(shard_seqlen_q_start, ac, atp, sp, bufs, batch_id)
481
+ _update_max_impl(shard_seqlen_q_start, ac, atp, sp, bufs, None)
482
+ _exp_impl(shard_seqlen_q_start, ac, atp, sp, bufs, None)
483
+
484
+ _load_q_impl(shard_seqlen_q_start + 1, ac, atp, sp, bufs, q, batch_id, sbuf_addr)
485
+ _qk_and_max_impl(shard_seqlen_q_start + 1, ac, atp, sp, bufs, batch_id)
486
+ _update_max_impl(shard_seqlen_q_start + 1, ac, atp, sp, bufs, None)
487
+ shard_seqlen_q_end = shard_seqlen_q_start + shard_seqlen_q_length
488
+
489
+ for grp_i in range(shard_seqlen_q_start, shard_seqlen_q_end - 2):
490
+ # grp_i: PV, write_back ; grp_i+1: EXP ; grp_i+2: load Q, QK+Max.
491
+ _load_q_impl(grp_i + 2, ac, atp, sp, bufs, q, batch_id, sbuf_addr)
492
+ _exp_impl(grp_i + 1, ac, atp, sp, bufs, None)
493
+ _fused_qkmax_and_pv_impl(grp_i, ac, atp, sp, bufs, batch_id)
494
+ _write_back_impl(grp_i, ac, atp, sp, bufs, o, batch_id)
495
+ _update_max_impl(grp_i + 2, ac, atp, sp, bufs, None)
496
+
497
+ _pv_impl(shard_seqlen_q_end - 2, ac, atp, sp, bufs)
498
+ _write_back_impl(shard_seqlen_q_end - 2, ac, atp, sp, bufs, o, batch_id)
499
+ _exp_impl(shard_seqlen_q_end - 1, ac, atp, sp, bufs, None)
500
+ _pv_impl(shard_seqlen_q_end - 1, ac, atp, sp, bufs)
501
+ _write_back_impl(shard_seqlen_q_end - 1, ac, atp, sp, bufs, o, batch_id)
502
+
503
+ # cache_softmax intermediate writes (out_neg_max / out_sum_recip) are DEAD (cache_softmax=False).
504
+
505
+
506
+ def _compute_tile_parameters(ac: AttnConfig, is_seqlen_sharded: bool) -> AttnTileParams:
507
+ """Tile parameters for the LIVE config. Dead: SWA/prefix/CP scale checks and seqlen updates,
508
+ flash sectioning (total_seqlen_k=2048 <= 10240 -> single section)."""
509
+ atp = AttnTileParams()
510
+
511
+ # LIVE: causal_mask=False, no CP/SWA/seqpack -> is_causal=False, dynamic_sel_mask=False.
512
+ atp.is_causal = ac.causal_mask
513
+ atp.dynamic_sel_mask = False
514
+ atp.seqlen_k_active_updated = ac.seqlen_k_active
515
+ atp.use_swa_optimized_allocation = False
516
+
517
+ atp.sb_p = nl.tile_size.pmax
518
+ kernel_assert(_Q_GRP_SZ == atp.sb_p, f"expect Q group size == pmax, got {_Q_GRP_SZ=}, {atp.sb_p=}")
519
+ kernel_assert(_V_TILE_SZ == atp.sb_p, f"expect V tile size == pmax, got {_V_TILE_SZ=}, {atp.sb_p=}")
520
+
521
+ atp.num_grps = div_ceil(ac.seqlen_q, atp.sb_p)
522
+ atp.can_pack_q_load = not is_seqlen_sharded
523
+
524
+ num_q_grps_per_load_dtype = 4 if ac.dtype == nl.float32 else 8
525
+ atp.num_q_grps_per_load = min(num_q_grps_per_load_dtype if atp.can_pack_q_load else 1, atp.num_grps)
526
+ kernel_assert(atp.num_q_grps_per_load > 0, f"num_q_grps_per_load must be positive, got {atp.num_q_grps_per_load}.")
527
+
528
+ atp.seqlen_k_prior_padded = None # not prefix caching
529
+ total_seqlen_k = atp.seqlen_k_active_updated
530
+
531
+ # total_seqlen_k = 2048 <= 10240 -> no flash attention, single section.
532
+ use_flash_attn = total_seqlen_k > _FLASH_ATTENTION_THRESHOLD
533
+ if use_flash_attn:
534
+ atp.section_len = min(total_seqlen_k, _FLASH_ATTENTION_SECTION_LENGTH)
535
+ else:
536
+ atp.section_len = total_seqlen_k
537
+
538
+ kernel_assert(atp.section_len > 0, f"section_len must be positive, got {atp.section_len}")
539
+ atp.num_sections = div_ceil(total_seqlen_k, atp.section_len)
540
+ if not use_flash_attn:
541
+ kernel_assert(atp.num_sections == 1, "Logic fault, must only have 1 section if not using flash_attn")
542
+
543
+ atp.num_large_tiles_per_section = div_ceil(atp.section_len, _LARGE_TILE_SZ)
544
+ atp.num_k_tiles_per_section = div_ceil(atp.section_len, _K_TILE_SZ)
545
+ atp.num_v_tiles_per_section = div_ceil(atp.section_len, _V_TILE_SZ)
546
+
547
+ atp.exp_inst_elems = _EXP_TILE_SZ
548
+ atp.num_exp_insts_per_large_tile = _LARGE_TILE_SZ // atp.exp_inst_elems
549
+ atp.num_tps_in_mm2_grp = _K_TILE_SZ // atp.sb_p
550
+ atp.mm2_grp_sz = _K_TILE_SZ
551
+
552
+ return atp
553
+
554
+
555
+ def _setup_range_select_bounds(
556
+ ac: AttnConfig,
557
+ atp: AttnTileParams,
558
+ bufs: AttnInternalBuffers,
559
+ allocator: ModularAllocator,
560
+ cp_offset: Any,
561
+ prior_used_len: Any,
562
+ bound_min: Any,
563
+ bound_max: Any,
564
+ batch_id: int = 0,
565
+ ) -> tuple:
566
+ """NO-OP for this config: only CP / SWA / prefix-caching / sequence-packing populate range
567
+ bounds, and all are off (``dynamic_sel_mask=False``, ``is_prefix_caching=False``). The entire
568
+ vendor body was gated by ``if atp.dynamic_sel_mask:`` / ``if ac.is_prefix_caching:`` / SWA,
569
+ so it emits no instructions here and is removed."""
570
+ return
571
+
572
+
573
+ def _allocate_attention_buffers(
574
+ allocator: ModularAllocator,
575
+ ac: AttnConfig,
576
+ atp: AttnTileParams,
577
+ bufs: AttnInternalBuffers,
578
+ sink: Any,
579
+ k_cache_sbuf: Optional[List[nl.ndarray]] = None,
580
+ v_cache_sbuf: Optional[List[nl.ndarray]] = None,
581
+ ):
582
+ """Allocate SBUF/PSUM buffers. LIVE: tp_out=True (so all tp_out branches taken), K/V come
583
+ from k_cache_sbuf/v_cache_sbuf, sink=None, dynamic_sel_mask=False (so the static-mask
584
+ mm1_copy_sb / mm1_affine_select_output buffers ARE allocated -- kept). The tp_out=False
585
+ branches and the fused-v-proj branch are DEAD and removed."""
586
+ mm2_p, mm2_n = atp.sb_p, ac.d
587
+
588
+ # K/V provided in SBUF by the caller.
589
+ bufs.k_sb = k_cache_sbuf
590
+ bufs.v_sb = v_cache_sbuf
591
+
592
+ bufs.q_sb = allocator.alloc_sbuf_tensor(
593
+ shape=(ac.d, atp.sb_p * atp.num_q_grps_per_load),
594
+ dtype=nl.bfloat16,
595
+ block_dim=[div_ceil(atp.num_grps, atp.num_q_grps_per_load)],
596
+ num_free_tiles=[2],
597
+ align_to=32,
598
+ )
599
+
600
+ bufs.flash_attn_correction_factor = allocator.alloc_sbuf_tensor(
601
+ shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
602
+ )
603
+
604
+ mm1_partial_max_n_elts = atp.num_k_tiles_per_section # sink is None
605
+ bufs.mm1_partial_max = allocator.alloc_sbuf_tensor(
606
+ shape=(atp.sb_p, mm1_partial_max_n_elts), dtype=nl.float32,
607
+ block_dim=[atp.num_grps], num_free_tiles=[2], align_to=4,
608
+ )
609
+
610
+ bufs.mm1_section_max = allocator.alloc_sbuf_tensor(
611
+ shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
612
+ )
613
+
614
+ n_final_reduce_sum_elts = div_ceil(atp.section_len, atp.exp_inst_elems) # sink is None
615
+ bufs.exp_partial_sum = allocator.alloc_sbuf_tensor(
616
+ shape=(atp.sb_p, n_final_reduce_sum_elts), dtype=nl.float32,
617
+ block_dim=[atp.num_grps], num_free_tiles=[2],
618
+ )
619
+
620
+ bufs.exp_section_sum = allocator.alloc_sbuf_tensor(
621
+ shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
622
+ )
623
+
624
+ bufs.prev_mm1_running_max = allocator.alloc_sbuf_tensor(
625
+ shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
626
+ )
627
+
628
+ bufs.prev_exp_running_sum = allocator.alloc_sbuf_tensor(
629
+ shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
630
+ )
631
+
632
+ # tp_out=True everywhere below.
633
+ bufs.mm2_prev_output = allocator.alloc_sbuf_tensor(
634
+ shape=(ac.d, atp.sb_p), dtype=ac.mm_out_dtype, block_dim=[atp.num_grps], num_free_tiles=[2],
635
+ )
636
+
637
+ bufs.mm2_accum_flash_attn = allocator.alloc_sbuf_tensor(
638
+ shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
639
+ )
640
+
641
+ bufs.mm2_prev_output_scaled = allocator.alloc_sbuf_tensor(
642
+ shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
643
+ )
644
+
645
+ bufs.tp_flash_attn_correction_factor_psum = []
646
+ bufs.tp_exp_sum_reciprocal_psum = []
647
+ for grp_idx in range(atp.num_grps):
648
+ tp_flash_attn_correction_factor_psum_tile = nl.ndarray(
649
+ (ac.d, atp.sb_p), dtype=nl.float32, buffer=nl.psum,
650
+ address=(0, ((grp_idx % 2) * 4 + 3) * PSUM_BANK_SIZE),
651
+ )
652
+ bufs.tp_flash_attn_correction_factor_psum.append(tp_flash_attn_correction_factor_psum_tile)
653
+ tp_exp_sum_reciprocal_psum_tile = nl.ndarray(
654
+ (ac.d, atp.sb_p), dtype=nl.float32, buffer=nl.psum,
655
+ address=(0, ((grp_idx % 2) * 4 + 3) * PSUM_BANK_SIZE),
656
+ )
657
+ bufs.tp_exp_sum_reciprocal_psum.append(tp_exp_sum_reciprocal_psum_tile)
658
+
659
+ bufs.tp_flash_attn_correction_factor_sb = allocator.alloc_sbuf_tensor(
660
+ shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[4],
661
+ )
662
+
663
+ bufs.tp_exp_sum_reciprocal_sb = allocator.alloc_sbuf_tensor(
664
+ shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[4],
665
+ )
666
+
667
+ bufs.mm2_final = allocator.alloc_sbuf_tensor(
668
+ shape=(ac.d, atp.sb_p), dtype=ac.mm_out_dtype, block_dim=[atp.num_grps], num_free_tiles=[2],
669
+ )
670
+
671
+ bufs.mm2_sb = allocator.alloc_sbuf_tensor(
672
+ shape=(mm2_n, mm2_p), dtype=ac.mm_out_dtype, block_dim=[atp.num_grps], num_free_tiles=[2],
673
+ )
674
+
675
+ bufs.mm1_masked = allocator.alloc_sbuf_tensor(
676
+ shape=(atp.sb_p, _LARGE_TILE_SZ), dtype=nl.float32,
677
+ block_dim=[atp.num_grps, atp.num_large_tiles_per_section],
678
+ num_free_tiles=[2, atp.num_large_tiles_per_section],
679
+ )
680
+
681
+ bufs.exp_sb = allocator.alloc_sbuf_tensor(
682
+ shape=(atp.sb_p, _LARGE_TILE_SZ), dtype=nl.bfloat16,
683
+ block_dim=[atp.num_grps, atp.num_large_tiles_per_section],
684
+ # DP2: group-axis depth 2 so exp(g+1) runs while transpose(g) still drains exp_sb(g).
685
+ num_free_tiles=[2, atp.num_large_tiles_per_section],
686
+ )
687
+
688
+ # MM1 PSUM tiles: banks {0,1,2,3}.
689
+ bufs.mm1_psum = []
690
+ for grp_idx in range(atp.num_grps):
691
+ grp_row = []
692
+ for large_tile_idx in range(atp.num_large_tiles_per_section):
693
+ tile_row = []
694
+ for k_tile_idx in range(4):
695
+ mm1_psum_tile = nl.ndarray(
696
+ (atp.sb_p, nl.tile_size.psum_fmax), dtype=ac.mm_out_dtype, buffer=nl.psum,
697
+ address=(0, (k_tile_idx % 4) * PSUM_BANK_SIZE),
698
+ )
699
+ tile_row.append(mm1_psum_tile)
700
+ grp_row.append(tile_row)
701
+ bufs.mm1_psum.append(grp_row)
702
+
703
+ # dynamic_sel_mask=False -> the static-mask staging buffers are allocated (no-mask branch
704
+ # does not read them, but the buffers exist in the vendor's non-dynamic allocation).
705
+ bufs.mm1_copy_sb = allocator.alloc_sbuf_tensor(
706
+ shape=(atp.sb_p, nl.tile_size.psum_fmax), dtype=ac.mm_out_dtype,
707
+ block_dim=[atp.num_grps, atp.num_large_tiles_per_section, 4], num_free_tiles=[1, 1, 2],
708
+ )
709
+ bufs.mm1_affine_select_output = allocator.alloc_sbuf_tensor(
710
+ shape=(atp.sb_p, nl.tile_size.psum_fmax), dtype=ac.mm_out_dtype,
711
+ block_dim=[atp.num_grps, atp.num_large_tiles_per_section, 4], num_free_tiles=[1, 1, 2],
712
+ )
713
+
714
+ bufs.exp_tp_sb = allocator.alloc_sbuf_tensor(
715
+ shape=(atp.sb_p, atp.mm2_grp_sz), dtype=nl.bfloat16,
716
+ block_dim=[atp.num_grps, atp.num_large_tiles_per_section, atp.num_tps_in_mm2_grp],
717
+ # DP4: 8 group-blocks resident so P^T transposes issue as soon as their exp exists.
718
+ num_free_tiles=[8, atp.num_large_tiles_per_section, atp.num_tps_in_mm2_grp],
719
+ align_to=32,
720
+ )
721
+
722
+ # MM2 PSUM tiles: banks {4,5,6,7}. tp_out=True shape (mm2_n, mm2_p).
723
+ bufs.mm2_psum = []
724
+ for grp_idx in range(atp.num_grps):
725
+ grp_row = []
726
+ for large_tile_idx in range(atp.num_large_tiles_per_section):
727
+ mm2_psum_tile = nl.ndarray(
728
+ (mm2_n, mm2_p), dtype=ac.mm_out_dtype, buffer=nl.psum,
729
+ address=(0, ((4 + (large_tile_idx % 4)) * PSUM_BANK_SIZE)),
730
+ )
731
+ grp_row.append(mm2_psum_tile)
732
+ bufs.mm2_psum.append(grp_row)
733
+
734
+
735
+ def _q_to_kv_batch_id(batch_id: int, bs: int, bs_kv: int) -> int:
736
+ """Map Q batch id to KV batch id for native GQA support. bs_kv=1 here -> always 0."""
737
+ return batch_id // (bs // bs_kv)
738
+
739
+
740
+ def _check_input_and_return_shape(
741
+ q,
742
+ k,
743
+ v,
744
+ is_prefix_caching,
745
+ k_prior,
746
+ v_prior,
747
+ prior_used_len,
748
+ tp_q,
749
+ tp_k,
750
+ tp_out,
751
+ cache_softmax,
752
+ k_cache_sbuf=None,
753
+ v_cache_sbuf=None,
754
+ k_prior_sbuf=None,
755
+ v_prior_sbuf=None,
756
+ skip_v_check=False,
757
+ ) -> tuple:
758
+ """Validate shapes and return (seqlen_q, seqlen_k, seqlen_k_prior, d, out_shape,
759
+ cache_softmax_shape). LIVE: use_sbuf_kv=True, tp_q=False, tp_out=True, not prefix caching,
760
+ cache_softmax=False."""
761
+ # SBUF-KV path.
762
+ kernel_assert(k_cache_sbuf is not None and len(k_cache_sbuf) > 0, "k_cache_sbuf must be provided")
763
+ kernel_assert(v_cache_sbuf is not None and len(v_cache_sbuf) > 0, "v_cache_sbuf must be provided")
764
+
765
+ if tp_q:
766
+ batch_size, seqlen_q, d = q.shape
767
+ else:
768
+ batch_size, d, seqlen_q = q.shape
769
+
770
+ d_from_k = k_cache_sbuf[0].shape[0]
771
+ kernel_assert(d == d_from_k, f"Head dimension mismatch: q has d={d}, but k_cache_sbuf has d={d_from_k}")
772
+ seqlen_k = len(k_cache_sbuf) * _K_TILE_SZ
773
+ batch_size_kv = 1
774
+ seqlen_k_prior = None # not prefix caching
775
+
776
+ out_seqlen = seqlen_q
777
+ if tp_out:
778
+ out_shape = (batch_size, d, out_seqlen)
779
+ else:
780
+ out_shape = (batch_size, out_seqlen, d)
781
+
782
+ cache_softmax_tile_size = 128
783
+ padded_seq_grps = div_ceil(out_seqlen, cache_softmax_tile_size)
784
+ cache_softmax_shape = [batch_size, cache_softmax_tile_size, padded_seq_grps]
785
+
786
+ return (seqlen_q, seqlen_k, seqlen_k_prior, d, out_shape, cache_softmax_shape)
787
+
788
+
789
+ def _load_q_tile(q, out, tp_q, batch_id, grp_i, seqlen_offset, load_dtype, sbuf_addr, grps_per_load=1) -> int:
790
+ """Load a Q tile HBM->SBUF. LIVE: tp_q=False (q is [bs, d, seqlen]), no transpose; the whole
791
+ ``if tp_q:`` transpose sub-tree (dma_transpose / nc_transpose paths) is DEAD and removed."""
792
+ kernel_assert(str(load_dtype) == str(out[0].dtype), "Conflicting dtype")
793
+ _, d, seqlen = q.shape
794
+
795
+ num_f = min(seqlen - seqlen_offset, _Q_GRP_SZ * grps_per_load)
796
+ out_dst_pat = out[grp_i // grps_per_load].ap(pattern=[[_Q_GRP_SZ * grps_per_load, d], [1, num_f]], offset=0)
797
+ q_src_pat = q.ap(pattern=[[seqlen, d], [1, num_f]], offset=batch_id * d * seqlen + seqlen_offset)
798
+ nisa.dma_copy(dst=out_dst_pat, src=q_src_pat)
799
+
800
+
801
+ def _get_kv_tile_apc(
802
+ is_prefix_caching,
803
+ k_active,
804
+ k_prior,
805
+ seqlen_active,
806
+ seqlen_prior,
807
+ seqlen_offset,
808
+ load_offset_active,
809
+ ) -> tuple:
810
+ """Determine which KV tensor (active or prior) to use. LIVE: is_prefix_caching=False, so this
811
+ returns the trivial active-tile mapping; the prefix-caching branch is DEAD and removed."""
812
+ return k_active, seqlen_active, seqlen_offset, load_offset_active
813
+
814
+
815
+ def _compute_section_offset_active(k_section_offset, is_prefix_caching, seqlen_k_prior_padded):
816
+ """Active-K offset for a section. LIVE: is_prefix_caching=False -> pass-through."""
817
+ section_contains_prefix = False
818
+ k_section_offset_active = k_section_offset
819
+ return k_section_offset_active, section_contains_prefix
820
+
821
+
822
+ def _load_q_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
823
+ bufs: AttnInternalBuffers, q, batch_id, sbuf_addr):
824
+ """Load a Q group if compute is needed. LIVE: is_causal=False -> has_any_compute is always
825
+ True; k_scale_sb is None (fp8 dequant path DEAD)."""
826
+ if grp_i % atp.num_q_grps_per_load == 0:
827
+ # is_causal=False -> has_any_compute_pred is True.
828
+ q_seqlen_offset = grp_i * _Q_GRP_SZ
829
+ _load_q_tile(
830
+ q, bufs.q_sb, ac.tp_q, batch_id, grp_i, q_seqlen_offset,
831
+ bufs.q_sb[0].dtype, sbuf_addr, grps_per_load=atp.num_q_grps_per_load,
832
+ )
833
+ # k_scale_sb is None -> the delayed-fp8 Q scaling is DEAD.
834
+
835
+
836
+ def _qk_and_max_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
837
+ bufs: AttnInternalBuffers, batch_id: int = 0):
838
+ """MM1 (QK^T) + row-max for a Q group. LIVE: is_causal=False -> always compute.
839
+
840
+ h10 EDIT (site :2876): the ``mm1_partial_max`` init memset is pinned to GpSimd -- it sheds
841
+ Vector (K2's binding engine) and its first consumer is several ops downstream, so a slower
842
+ idle engine is safe."""
843
+ # is_causal=False -> has_any_compute_pred is True.
844
+ nisa.memset(bufs.mm1_partial_max[grp_i], value=_FLOAT32_MIN, engine=engine.gpsimd)
845
+
846
+ for large_tile_idx in range(atp.num_large_tiles_per_section):
847
+ _qk_and_max_large_tile_impl(grp_i, large_tile_idx, ac, atp, sp, bufs, batch_id)
848
+
849
+
850
+ def _update_max_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
851
+ bufs: AttnInternalBuffers, sink):
852
+ """Update running max. LIVE: is_causal=False (always compute), sink=None, num_sections==1 so
853
+ the reduce writes straight into mm1_running_max (max-direct promotion) and the flash
854
+ multi-section correction is DEAD."""
855
+ # is_causal=False -> has_any_compute_pred is True.
856
+
857
+ # num_sections==1: the reduce writes straight into mm1_running_max (max-direct); the [128,1]
858
+ # copy the vendor once had here is gone (3,072 Vector instructions removed upstream).
859
+ _max_dst = bufs.mm1_running_max[:, grp_i]
860
+ nisa.tensor_reduce(_max_dst, nl.maximum, bufs.mm1_partial_max[grp_i], 1, negate=True)
861
+ # num_sections==1: no running-max copy / flash correction factor. sink block DEAD (sink=None).
862
+
863
+
864
+ def _exp_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
865
+ bufs: AttnInternalBuffers, sink):
866
+ """Softmax exp (Scalar ACTIVATE) + P^T dma_transposes. LIVE: is_causal=False, use_swa=False,
867
+ not prefix caching -> every k-tile is emitted (emit_idxs is full), wide_exp=False, sink=None.
868
+
869
+ The ``memset`` on ``exp_partial_sum`` stays UNPINNED (engine.unknown / Vector) ON PURPOSE
870
+ (vendor :2993): pinning it would fork this 280-line function and re-home the softmax-exp and
871
+ dma_transpose source strings, which MUST keep their ``attention_inner`` sites stable and
872
+ match the shipping stream. The ACCUM FORK (reset on first tile, accumulate, drain once) and
873
+ the reduce_cmd discipline are preserved verbatim."""
874
+ # is_causal=False -> has_any_compute_pred is True.
875
+ q_seqlen_offset = grp_i * atp.sb_p
876
+ nisa.memset(bufs.exp_partial_sum[grp_i][...], value=0.0) # UNPINNED on purpose (:2993).
877
+
878
+ for large_tile_idx in range(atp.num_large_tiles_per_section):
879
+ kernel_assert(atp.exp_inst_elems == 512, "Internal validation failed.")
880
+
881
+ # ACCUM FORK pre-scan: is_causal=False and not prefix caching -> every tile emits, so
882
+ # emit_idxs is [0, 1, 2, 3]. reset on the first, accumulate, drain once.
883
+ emit_idxs = []
884
+ for scan_idx in range(atp.num_exp_insts_per_large_tile):
885
+ scan_prior, scan_seqlen_k, scan_k_start, _ = _get_kv_tile_apc(
886
+ ac.is_prefix_caching,
887
+ False,
888
+ True,
889
+ atp.seqlen_k_active_updated,
890
+ ac.seqlen_k_prior,
891
+ sp.section_offset + large_tile_idx * _LARGE_TILE_SZ + scan_idx * atp.exp_inst_elems,
892
+ None,
893
+ )
894
+ scan_mask = True # is_causal=False, use_swa=False
895
+ if scan_mask and scan_seqlen_k > scan_k_start:
896
+ emit_idxs.append(scan_idx)
897
+ first_emit = emit_idxs[0] if emit_idxs else -1
898
+ last_emit = emit_idxs[-1] if emit_idxs else -1
899
+
900
+ for exp_tile_idx in range(atp.num_exp_insts_per_large_tile):
901
+ is_prior_tile, seqlen_k, k_start_pos, _ = _get_kv_tile_apc(
902
+ ac.is_prefix_caching,
903
+ False,
904
+ True,
905
+ atp.seqlen_k_active_updated,
906
+ ac.seqlen_k_prior,
907
+ sp.section_offset + large_tile_idx * _LARGE_TILE_SZ + exp_tile_idx * atp.exp_inst_elems,
908
+ None,
909
+ )
910
+ num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
911
+ num_f = min(seqlen_k - k_start_pos, atp.exp_inst_elems)
912
+
913
+ exp_sel_mask = True # is_causal=False, use_swa=False
914
+
915
+ if exp_sel_mask and seqlen_k > k_start_pos:
916
+ # Step 1: exp. reduce_cmd is ALWAYS explicit (see the vendor note: an idle cmd
917
+ # with reduce_op/reduce_res would write a stale register).
918
+ if exp_tile_idx == first_emit:
919
+ exp_reduce_cmd = nisa.reduce_cmd.reset_reduce
920
+ else:
921
+ exp_reduce_cmd = nisa.reduce_cmd.reduce
922
+ if exp_tile_idx == last_emit:
923
+ exp_reduce_res = bufs.exp_partial_sum[grp_i][
924
+ :num_p,
925
+ large_tile_idx * atp.num_exp_insts_per_large_tile + exp_tile_idx,
926
+ ]
927
+ else:
928
+ exp_reduce_res = None
929
+ nisa.activation(
930
+ bufs.exp_sb[grp_i][large_tile_idx][
931
+ :num_p, nl.ds(exp_tile_idx * atp.exp_inst_elems, num_f)
932
+ ],
933
+ op=nl.exp,
934
+ data=bufs.mm1_masked[grp_i][large_tile_idx][
935
+ :num_p, nl.ds(exp_tile_idx * atp.exp_inst_elems, num_f)
936
+ ],
937
+ reduce_op=nl.add,
938
+ reduce_cmd=exp_reduce_cmd,
939
+ reduce_res=exp_reduce_res,
940
+ bias=bufs.mm1_running_max[:num_p, grp_i],
941
+ )
942
+
943
+ # Step 2: P^T dma_transpose into exp_tp_sb ([128, 4, 128]).
944
+ num_f_outer = num_f // atp.sb_p
945
+ num_f_inner = num_f % atp.sb_p
946
+
947
+ if num_f_outer >= 1:
948
+ nisa.dma_transpose(
949
+ dst=bufs.exp_tp_sb[grp_i][large_tile_idx][exp_tile_idx].ap(
950
+ [
951
+ [atp.mm2_grp_sz, atp.sb_p],
952
+ [1, 1],
953
+ [atp.sb_p, num_f_outer],
954
+ [1, num_p],
955
+ ]
956
+ ),
957
+ src=bufs.exp_sb[grp_i][large_tile_idx].ap(
958
+ [
959
+ [_LARGE_TILE_SZ, num_p],
960
+ [1, 1],
961
+ [atp.sb_p, num_f_outer],
962
+ [1, atp.sb_p],
963
+ ],
964
+ offset=exp_tile_idx * atp.mm2_grp_sz,
965
+ ),
966
+ )
967
+
968
+ if num_f_inner > 0:
969
+ nisa.dma_transpose(
970
+ dst=bufs.exp_tp_sb[grp_i][large_tile_idx][exp_tile_idx].ap(
971
+ [
972
+ [atp.mm2_grp_sz, num_f_inner],
973
+ [1, 1],
974
+ [atp.sb_p, 1],
975
+ [1, num_p],
976
+ ],
977
+ offset=num_f_outer * atp.sb_p,
978
+ ),
979
+ src=bufs.exp_sb[grp_i][large_tile_idx].ap(
980
+ [
981
+ [_LARGE_TILE_SZ, num_p],
982
+ [1, 1],
983
+ [atp.sb_p, 1],
984
+ [1, num_f_inner],
985
+ ],
986
+ offset=exp_tile_idx * atp.mm2_grp_sz + num_f_outer * atp.sb_p,
987
+ ),
988
+ )
989
+
990
+ # sink block DEAD (sink=None).
991
+
992
+
993
+ def _pv_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams, bufs: AttnInternalBuffers):
994
+ """MM2 (P@V) for a Q group. LIVE: is_causal=False -> always compute."""
995
+ # is_causal=False -> has_any_compute_pred is True.
996
+ nisa.memset(bufs.mm2_sb[grp_i][...], value=0.0)
997
+
998
+ for large_tile_idx in range(atp.num_large_tiles_per_section):
999
+ _pv_large_tile_impl(grp_i, large_tile_idx, ac, atp, sp, bufs)
1000
+
1001
+
1002
+ def _fused_qkmax_and_pv_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
1003
+ bufs: AttnInternalBuffers, batch_id: int = 0):
1004
+ """Pipelined MM2 for grp_i while doing QK+max for grp_i+2. LIVE: is_causal=False -> both
1005
+ predicates True.
1006
+
1007
+ h10 EDIT (site :3294): the ``mm1_partial_max[qkmax_grp]`` init memset is pinned to GpSimd
1008
+ (same rationale as :2876; 2,688/core, the larger of the two)."""
1009
+ qkmax_grp = grp_i + 2
1010
+ # is_causal=False -> both has_any_compute predicates are True.
1011
+ nisa.memset(bufs.mm1_partial_max[qkmax_grp][...], value=_FLOAT32_MIN, engine=engine.gpsimd)
1012
+
1013
+ for large_tile_idx in range(atp.num_large_tiles_per_section):
1014
+ _pv_large_tile_impl(grp_i, large_tile_idx, ac, atp, sp, bufs)
1015
+ _qk_and_max_large_tile_impl(qkmax_grp, large_tile_idx, ac, atp, sp, bufs, batch_id)
1016
+
1017
+
1018
+ def _write_back_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
1019
+ bufs: AttnInternalBuffers, o, batch_id):
1020
+ """Finalise output. LIVE: is_causal=False (always compute), num_sections==1,
1021
+ skip_output_normalization=False -> single-section reciprocal + scale_reciprocal_write_back.
1022
+ The flash multi-section accumulation and the skip-normalization branches are DEAD."""
1023
+ # is_causal=False -> has_any_compute_pred is True.
1024
+ q_seqlen_offset = grp_i * atp.sb_p
1025
+ nisa.tensor_reduce(bufs.exp_section_sum[grp_i][...], nl.add, bufs.exp_partial_sum[grp_i], axis=1)
1026
+ # num_sections==1, not skip_output_normalization.
1027
+ nisa.reciprocal(bufs.exp_sum_reciprocal[:, grp_i], bufs.exp_section_sum[grp_i])
1028
+
1029
+ num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1030
+ num_f = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1031
+
1032
+ # Single section: scale by reciprocal and write current output.
1033
+ _scale_reciprocal_write_back_impl(bufs.mm2_sb[grp_i], grp_i, ac, atp, bufs, o, batch_id, num_p, num_f)
1034
+
1035
+
1036
+ def _scale_reciprocal_write_back_impl(src_buf, grp_i, ac: AttnConfig, atp: AttnTileParams,
1037
+ bufs: AttnInternalBuffers, o, batch_id, num_p, num_f):
1038
+ """Reciprocal-normalise and write back (final section). LIVE: tp_out=True.
1039
+
1040
+ h8 EDIT (site 1): the tp_out PSUM->SBUF evict ``tensor_copy`` is pinned to Scalar -- the
1041
+ un-pinned copy defaults to Vector (K2's binding engine); GpSimd is illegal (PSUM source), so
1042
+ Scalar is the only shedding target and is the exercised majority path. The tp_out=False
1043
+ ``tensor_scalar(..., engine=engine.vector)`` branch is DEAD (tp_out always True) and removed."""
1044
+ # tp_out=True.
1045
+ nisa.nc_transpose(
1046
+ bufs.tp_exp_sum_reciprocal_psum[grp_i].ap(pattern=[[atp.sb_p, ac.d], [1, atp.sb_p]], offset=0),
1047
+ bufs.exp_sum_reciprocal.ap(pattern=[[atp.num_grps, atp.sb_p], [0, ac.d]], offset=grp_i),
1048
+ )
1049
+ # ---- h8 SITE 1: PSUM->SBUF evict pinned to Scalar. ----
1050
+ nisa.tensor_copy(
1051
+ bufs.tp_exp_sum_reciprocal_sb[grp_i][: ac.d, :num_f],
1052
+ bufs.tp_exp_sum_reciprocal_psum[grp_i][: ac.d, :num_f],
1053
+ engine=engine.scalar,
1054
+ )
1055
+ nisa.tensor_tensor(
1056
+ bufs.mm2_final[grp_i][: ac.d, :num_f],
1057
+ src_buf[: ac.d, :num_f],
1058
+ bufs.tp_exp_sum_reciprocal_sb[grp_i][: ac.d, :num_f],
1059
+ nl.multiply,
1060
+ )
1061
+
1062
+ _write_back_o_impl(bufs.mm2_final[grp_i], grp_i, ac, atp, o, batch_id, num_p, num_f)
1063
+
1064
+
1065
+ def _write_back_o_impl(src_buf, grp_i, ac: AttnConfig, atp: AttnTileParams, o, batch_id, num_p, num_f):
1066
+ """Evict finished O. LIVE: ``o`` is a LIST of caller-owned SBUF slices, tp_out=True, so the
1067
+ output is copied into the caller's O tile (kept on-chip for the fused out-projection) rather
1068
+ than DMA'd to HBM. The HBM-sink branch is DEAD (o is always a list) and removed.
1069
+
1070
+ h8 EDIT (site 3): the tp_out list-branch ``tensor_copy`` (SBUF->SBUF fp32->bf16 CAST) is
1071
+ pinned to GpSimd -- all three engines are legal and GpSimd is the idle engine, so it sheds
1072
+ off Vector."""
1073
+ # o is a list (caller-owned SBUF slices), tp_out=True.
1074
+ # ---- h8 SITE 3: SBUF->SBUF CAST pinned to GpSimd. ----
1075
+ nisa.tensor_copy(dst=o[grp_i][: ac.d, :num_f], src=src_buf[: ac.d, :num_f], engine=engine.gpsimd)
1076
+
1077
+
1078
+ def _qk_and_max_large_tile_impl(qkmax_grp, large_tile_idx, ac: AttnConfig, atp: AttnTileParams,
1079
+ sp: SectionParams, bufs: AttnInternalBuffers, batch_id: int = 0):
1080
+ """MM1 (QK^T) + masking for a Q group and a 2048 K tile. LIVE: is_causal=False,
1081
+ dynamic_sel_mask=False, is_prior_tile=False -> the FINAL ``else: # no masking``
1082
+ ``tensor_scalar_reduce`` branch. The diagonal (affine_select) and range_select branches are
1083
+ DEAD and removed; SBUF-prefix-caching K-tile selection is DEAD (k_sb_prior is None)."""
1084
+ q_seqlen_offset = qkmax_grp * atp.sb_p
1085
+
1086
+ num_k_tiles_in_large_tile = _LARGE_TILE_SZ // _K_TILE_SZ
1087
+ for k_tile_idx in range(num_k_tiles_in_large_tile):
1088
+ mm1_psum_tile = bufs.mm1_psum[qkmax_grp][large_tile_idx][k_tile_idx]
1089
+ # dynamic_sel_mask=False, but the no-mask branch below reads neither mm1_copy_sb nor
1090
+ # mm1_affine_select_output, so their per-tile locals are not needed.
1091
+ mm1_masked_tile = bufs.mm1_masked[qkmax_grp][large_tile_idx]
1092
+ mm1_partial_max_tile = bufs.mm1_partial_max[qkmax_grp]
1093
+
1094
+ k_tile_idx_in_section = large_tile_idx * num_k_tiles_in_large_tile + k_tile_idx
1095
+ _kv_sec_idx = sp.kv_section_idx if sp.kv_section_idx is not None else sp.section_idx
1096
+ k_tile_idx_global = atp.num_k_tiles_per_section * _kv_sec_idx + k_tile_idx_in_section
1097
+ is_prior_tile, seqlen_k, k_start_pos, _ = _get_kv_tile_apc(
1098
+ ac.is_prefix_caching,
1099
+ False,
1100
+ True,
1101
+ atp.seqlen_k_active_updated,
1102
+ ac.seqlen_k_prior,
1103
+ k_tile_idx_global * _K_TILE_SZ,
1104
+ None,
1105
+ )
1106
+
1107
+ matmul_selection = True # is_causal=False
1108
+
1109
+ if q_seqlen_offset >= ac.seqlen_q or k_start_pos >= seqlen_k:
1110
+ matmul_selection = False
1111
+
1112
+ if matmul_selection and k_tile_idx_in_section < atp.num_k_tiles_per_section:
1113
+ num_f = min(seqlen_k - k_start_pos, _K_TILE_SZ)
1114
+ num_q_free = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1115
+
1116
+ # Step 1: MM1 matmul. Non-APC path -> use k_tile_idx_in_section.
1117
+ k_tile_to_use = bufs.k_sb[k_tile_idx_in_section]
1118
+
1119
+ nisa.nc_matmul(
1120
+ mm1_psum_tile[:num_q_free, :num_f],
1121
+ bufs.q_sb[qkmax_grp // atp.num_q_grps_per_load][
1122
+ : ac.d,
1123
+ nl.ds((qkmax_grp % atp.num_q_grps_per_load) * _Q_GRP_SZ, num_q_free),
1124
+ ],
1125
+ k_tile_to_use[:, :num_f],
1126
+ )
1127
+
1128
+ # Step 2: no masking (is_causal=False, dynamic_sel_mask=False).
1129
+ num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1130
+ num_f = min(seqlen_k - k_start_pos, _K_TILE_SZ)
1131
+
1132
+ nisa.tensor_scalar_reduce(
1133
+ mm1_masked_tile[:num_p, nl.ds(k_tile_idx * _K_TILE_SZ, num_f)],
1134
+ data=mm1_psum_tile[:num_p, :num_f],
1135
+ op0=nl.multiply,
1136
+ operand0=ac.scale,
1137
+ reduce_op=nl.maximum,
1138
+ reduce_res=mm1_partial_max_tile[:num_p, k_tile_idx_in_section],
1139
+ )
1140
+
1141
+
1142
+ def _pv_large_tile_impl(pv_grp, large_tile_idx, ac: AttnConfig, atp: AttnTileParams,
1143
+ sp: SectionParams, bufs: AttnInternalBuffers):
1144
+ """MM2 (P@V) for a Q group and 2048 V tile. LIVE: is_causal=False (all tiles selected),
1145
+ tp_out=True, single large tile so ``large_tile_idx==0`` -> the ``tensor_copy`` PSUM->SBUF
1146
+ reduction (never the accumulate ``tensor_tensor`` branch). SBUF-prefix V-tile selection is
1147
+ DEAD (v_sb_prior is None); the tp_out=False MM2 orientation is DEAD."""
1148
+ q_seqlen_offset = pv_grp * atp.sb_p
1149
+ num_mm2_grps_in_large_tile = _LARGE_TILE_SZ // atp.mm2_grp_sz
1150
+ mm2_psum_set = False
1151
+
1152
+ mm2_psum_tile = bufs.mm2_psum[pv_grp][large_tile_idx]
1153
+
1154
+ # Step 1: matmul + accumulate in PSUM per group.
1155
+ for mm2_grp_i in range(num_mm2_grps_in_large_tile):
1156
+ num_mm2_per_grp = atp.mm2_grp_sz // _V_TILE_SZ
1157
+ num_mm2_per_large_tile = num_mm2_per_grp * num_mm2_grps_in_large_tile
1158
+
1159
+ exp_tp_sb_tile = bufs.exp_tp_sb[pv_grp][large_tile_idx][mm2_grp_i]
1160
+
1161
+ is_prior_tile, seqlen_k, k_start_pos_512_tile, _ = _get_kv_tile_apc(
1162
+ ac.is_prefix_caching,
1163
+ False,
1164
+ True,
1165
+ atp.seqlen_k_active_updated,
1166
+ ac.seqlen_k_prior,
1167
+ sp.section_offset + large_tile_idx * _LARGE_TILE_SZ + mm2_grp_i * atp.mm2_grp_sz,
1168
+ None,
1169
+ )
1170
+
1171
+ for mm2_i in range(num_mm2_per_grp):
1172
+ v_tile_idx = large_tile_idx * num_mm2_per_large_tile + mm2_grp_i * num_mm2_per_grp + mm2_i
1173
+ k_start_pos = k_start_pos_512_tile + mm2_i * _V_TILE_SZ
1174
+
1175
+ num_p = min(seqlen_k - k_start_pos, _V_TILE_SZ)
1176
+ num_f = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1177
+
1178
+ mm2_sel_mask = True # is_causal=False
1179
+
1180
+ if mm2_sel_mask and v_tile_idx < atp.num_v_tiles_per_section and num_p > 0 and num_f > 0:
1181
+ mm2_psum_set = True
1182
+
1183
+ # Non-APC path -> use v_tile_idx.
1184
+ v_tile_to_use = bufs.v_sb[v_tile_idx]
1185
+
1186
+ # tp_out=True orientation.
1187
+ nisa.nc_matmul(
1188
+ mm2_psum_tile[: ac.d, :num_f],
1189
+ v_tile_to_use[:num_p, : ac.d],
1190
+ exp_tp_sb_tile[:num_p, nl.ds(mm2_i * _V_TILE_SZ, num_f)],
1191
+ )
1192
+
1193
+ # Step 2: reduce MM2 groups into the large tile. is_prior_tile=False, is_causal=False ->
1194
+ # loop_reduce_sel True; single large tile -> large_tile_idx==0 -> tensor_copy (never the
1195
+ # accumulate tensor_tensor branch).
1196
+ is_prior_tile, seqlen_k, k_start_pos, _ = _get_kv_tile_apc(
1197
+ ac.is_prefix_caching,
1198
+ False,
1199
+ True,
1200
+ atp.seqlen_k_active_updated,
1201
+ ac.seqlen_k_prior,
1202
+ sp.section_offset + large_tile_idx * _LARGE_TILE_SZ,
1203
+ None,
1204
+ )
1205
+ k_seqlen_mask = k_start_pos < seqlen_k
1206
+
1207
+ loop_reduce_sel = True # is_causal=False
1208
+
1209
+ if loop_reduce_sel and k_seqlen_mask and mm2_psum_set:
1210
+ # tp_out=True.
1211
+ num_f = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1212
+ if large_tile_idx == 0:
1213
+ nisa.tensor_copy(bufs.mm2_sb[pv_grp][:, :num_f], mm2_psum_tile[:, :num_f])
1214
+ else:
1215
+ nisa.tensor_tensor(
1216
+ bufs.mm2_sb[pv_grp][:, :num_f],
1217
+ bufs.mm2_sb[pv_grp][:, :num_f],
1218
+ mm2_psum_tile[:, :num_f],
1219
+ nl.add,
1220
+ )
build/torch-neuron/nki_kernels/mlp.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """K4 -- fused MLP (fc1 + gelu + fc2), x-prestage ("XP") in prologue mode.
2
+
3
+ WHAT IT COMPUTES
4
+ ----------------
5
+ ``out = gelu(x @ w1.T + b1) @ w2.T + b2``, with the rows sharded across LNC cores and
6
+ the weights pre-transposed to bf16. Returns ``[R, K]`` bf16.
7
+
8
+ WHY IT IS SHAPED THIS WAY
9
+ -------------------------
10
+ This is the effective shipping kernel: it is the ``b1pre`` MLP (fc1 bias staged in the
11
+ prologue so every gelu's bias is resident from t=0) with the x-prestage applied. Two
12
+ design choices carry the performance:
13
+
14
+ * b1 first: the 32 per-block ``b1`` bias tiles are DMA'd before the 16 MB of weight
15
+ staging, so they land within ~1 us of kernel start and no gelu ever waits on its bias.
16
+ * x prestaged in the prologue: BOTH row blocks' ``x`` load + dma_transpose staging is
17
+ issued before the ``rb`` loop, in row-block order. rb=0's staging already hid in the
18
+ pre-K4 seam (where the Tensor engine is dark for ~148 us because K1/K3 emit no Tensor
19
+ instructions); prestaging rb=1 gives it the same cover, filling the ~26.6 us Tensor
20
+ idle window that used to sit at the rb0->rb1 boundary. Appending rb=1's stages after
21
+ rb=0's, in order, means rb=0's readiness cannot regress -- the first matmul cannot be
22
+ delayed.
23
+
24
+ The x staging is emitted through a single module-level function (``_stage_xt``) rather
25
+ than inlined per row block, so the ``x`` load and the dma_transpose each keep exactly one
26
+ source site. The two-row-block staging is preserved exactly: ``xt_sets`` holds one
27
+ transposed-tile set per row block, and the loop consumes ``xt_sets[rb]``.
28
+
29
+ PSUM: fc1 uses 1 bank, fc2 uses up to 4 banks; the ``y1_psum`` / ``out_psums`` tiles are
30
+ declared inside their loops so the ring never accumulates onto a stale PSUM value. The
31
+ b2 bias broadcast is kept (removing it regressed the sibling kernel by 23-30%).
32
+
33
+ There are no ``engine=`` pins anywhere in this kernel. INFERENCE ONLY: no autograd rule.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import math
39
+
40
+ import torch
41
+ from torch_neuronx import nki_op
42
+
43
+ import nki
44
+ import nki.isa as nisa
45
+ import nki.language as nl
46
+
47
+ #: PE array partition limit, and the moving-operand free-axis limit.
48
+ _P_MAX = 128
49
+ _F_MOV_MAX = 512
50
+
51
+ _BF16 = nl.bfloat16
52
+
53
+ #: Logical-to-physical NeuronCore degree. Must match the shipping shard's.
54
+ import os as _os
55
+ #: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
56
+ LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
57
+
58
+ _INV_SQRT2 = 1.0 / math.sqrt(2.0)
59
+ _INV_SQRT_2PI = 1.0 / math.sqrt(2.0 * math.pi)
60
+
61
+
62
+ def _div_ceil(n: int, d: int) -> int:
63
+ """Ceiling division (never write (n + d - 1) // d inline)."""
64
+ return (n + d - 1) // d
65
+
66
+
67
+ def _kernel_assert(condition: bool, error_text: str):
68
+ """Assert with an NKI-formatted error message."""
69
+ assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
70
+
71
+
72
+ def _stage_xt(x_hbm, r_abs, pr, n_k, K, rb_size):
73
+ """Emit one row block's x staging and return its ``n_k`` transposed bf16 tiles.
74
+
75
+ ``n_rs`` loads of ``[<=128, K]`` bf16, each followed by ``n_k`` DMA transposes into
76
+ the row block's ``xt_tiles``. Kept as a module-level emitter so the ``x`` load and
77
+ the dma_transpose each map to a single source site regardless of where it is called.
78
+
79
+ Args:
80
+ x_hbm: [R, K] @ HBM bf16, the MLP input.
81
+ r_abs: absolute starting row of this row block.
82
+ pr: number of rows in this row block (<= rb_size).
83
+ n_k: number of K tiles (fc1 contraction dimension tiles).
84
+ K: hidden-in dimension.
85
+ rb_size: row-block width (free axis of the transposed tiles).
86
+
87
+ Returns:
88
+ list of ``n_k`` transposed ``[128, rb_size]`` bf16 SBUF tiles.
89
+ """
90
+ n_rs = _div_ceil(pr, _P_MAX)
91
+ xt_tiles = []
92
+ for _ in range(n_k):
93
+ xt_tiles.append(nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf))
94
+ for rs in range(n_rs):
95
+ rr0 = rs * _P_MAX
96
+ prs = min(_P_MAX, pr - rr0)
97
+ x_b16 = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
98
+ nisa.dma_copy(
99
+ dst=x_b16[0:prs, 0:K],
100
+ src=x_hbm[r_abs + rr0:r_abs + rr0 + prs, 0:K],
101
+ )
102
+ for kt in range(n_k):
103
+ k0 = kt * _P_MAX
104
+ pk = min(_P_MAX, K - k0)
105
+ nisa.dma_transpose(dst=xt_tiles[kt][0:pk, rr0:rr0 + prs],
106
+ src=x_b16[0:prs, k0:k0 + pk])
107
+ return xt_tiles
108
+
109
+
110
+ @nki.jit
111
+ def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
112
+ """gelu(x @ w1.T + b1) @ w2.T + b2, rows sharded, b1 AND both x blocks resident early.
113
+
114
+ Args:
115
+ x_hbm (nl.ndarray): [R, K] @ HBM, **bfloat16** (kernel 1 emits bf16).
116
+ w1t_hbm (nl.ndarray): [K, H] @ HBM, **bf16**, == fc1.weight.T.
117
+ b1_hbm (nl.ndarray): [H] @ HBM, float32.
118
+ w2t_hbm (nl.ndarray): [H, K] @ HBM, **bf16**, == fc2.weight.T.
119
+ b2_hbm (nl.ndarray): [K] @ HBM, float32.
120
+
121
+ Returns:
122
+ nl.ndarray: [R, K] @ HBM, **bfloat16**.
123
+ """
124
+ R, K = x_hbm.shape
125
+ K_w1t, H = w1t_hbm.shape
126
+ H_w2t, K_w2t = w2t_hbm.shape
127
+ _kernel_assert(R >= 1 and K >= 1 and H >= 1, f"R({R}) K({K}) H({H}) must be >= 1")
128
+ _kernel_assert(K_w1t == K, f"w1t in-dim {K_w1t} != K {K}; expected w1t = w1.T of shape [K, H]")
129
+ _kernel_assert(K_w2t == K, f"w2t out-dim {K_w2t} != K {K}; expected w2t = w2.T of shape [H, K]")
130
+ _kernel_assert(H_w2t == H, f"w2t in-dim {H_w2t} != H {H}")
131
+ _kernel_assert(
132
+ R % LNC == 0,
133
+ f"the {LNC}-core kernel needs R divisible by {LNC}, got R={R}. "
134
+ f"program_id is a symbolic value, so a partial final row block cannot be "
135
+ f"clamped; use the one-core pre-transposed kernel for uneven R",
136
+ )
137
+
138
+ rows_per = R // LNC
139
+ pid = nl.program_id(0)
140
+ r_base = pid * rows_per
141
+
142
+ out_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
143
+
144
+ n_k = _div_ceil(K, _P_MAX) # K tiles (fc1 contraction)
145
+ n_h = _div_ceil(H, _P_MAX) # H tiles (fc1 out-rows / fc2 contraction)
146
+
147
+ # b1 FIRST, before the 16 MB of weight DMA: 32 [128,1] tiles issue at t=0 and land in
148
+ # ~1 us, so every gelu's bias is resident long before the first matmul retires.
149
+ b1_tiles = []
150
+ for ht in range(n_h):
151
+ h0 = ht * _P_MAX
152
+ ph = min(_P_MAX, H - h0)
153
+ t = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
154
+ nisa.dma_copy(dst=t[0:ph, 0:1], src=b1_hbm[h0:h0 + ph])
155
+ b1_tiles.append(t)
156
+
157
+ # Weight staging (pre-transposed bf16, one DMA per tile).
158
+ w1t_tiles = []
159
+ for kt in range(n_k):
160
+ k0 = kt * _P_MAX
161
+ pk = min(_P_MAX, K - k0)
162
+ t = nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf)
163
+ nisa.dma_copy(dst=t[0:pk, :], src=w1t_hbm[k0:k0 + pk, :])
164
+ w1t_tiles.append(t)
165
+
166
+ w2t_tiles = []
167
+ for ht in range(n_h):
168
+ h0 = ht * _P_MAX
169
+ ph = min(_P_MAX, H - h0)
170
+ t = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
171
+ nisa.dma_copy(dst=t[0:ph, :], src=w2t_hbm[h0:h0 + ph, :])
172
+ w2t_tiles.append(t)
173
+
174
+ # Main fused loop.
175
+ rb_size = _F_MOV_MAX
176
+ n_rb = _div_ceil(rows_per, rb_size)
177
+ n_ko = _div_ceil(K, _F_MOV_MAX)
178
+
179
+ # THE X-PRESTAGE: stage EVERY row block's x here, in rb order, so rb=1's serialised
180
+ # DMA chain runs inside the pre-K4 seam (Tensor dark) instead of inside the idle
181
+ # window at the rb0->rb1 boundary. Appending rb=1's stages after rb=0's leaves rb=0's
182
+ # stages exactly where they are on the Sync engine's sub-stream.
183
+ xt_sets = []
184
+ for rb_pre in range(n_rb):
185
+ xt_sets.append(_stage_xt(
186
+ x_hbm, r_base + rb_pre * rb_size,
187
+ min(rb_size, rows_per - rb_pre * rb_size), n_k, K, rb_size))
188
+
189
+ for rb in range(n_rb):
190
+ r_local = rb * rb_size
191
+ pr = min(rb_size, rows_per - r_local)
192
+ n_rs = _div_ceil(pr, _P_MAX)
193
+ r_abs = r_base + r_local
194
+
195
+ # x_T bf16 tiles: consume the prologue-staged set for this row block.
196
+ xt_tiles = xt_sets[rb]
197
+
198
+ # ---- fc1: gt_tiles[ht] = gelu(w1 @ x.T + b1), bf16, SBUF-resident ----
199
+ gt_tiles = []
200
+ for ht in range(n_h):
201
+ h0 = ht * _P_MAX
202
+ ph = min(_P_MAX, H - h0)
203
+ # Declared INSIDE the ht loop, so the ring never reuses a written PSUM tile.
204
+ # nc_matmul's default accumulate=None auto-detects "previously written" and
205
+ # would accumulate onto a stale value.
206
+ y1_psum = nl.ndarray((_P_MAX, rb_size), dtype=nl.float32, buffer=nl.psum)
207
+ for kt in nl.affine_range(n_k):
208
+ k0 = kt * _P_MAX
209
+ pk = min(_P_MAX, K - k0)
210
+ nisa.nc_matmul(
211
+ dst=y1_psum[0:ph, 0:pr],
212
+ stationary=w1t_tiles[kt][0:pk, h0:h0 + ph],
213
+ moving=xt_tiles[kt][0:pk, 0:pr],
214
+ )
215
+ # b1 comes from the prologue-resident tile.
216
+ gt = nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf)
217
+ nisa.activation(dst=gt[0:ph, 0:pr], op=nl.gelu,
218
+ data=y1_psum[0:ph, 0:pr], bias=b1_tiles[ht][0:ph, 0:1])
219
+ gt_tiles.append(gt)
220
+
221
+ # ---- fc2: out[r,k] = sum_h g[r,h] * w2[k,h] + b2[k] ----
222
+ for ko in range(n_ko):
223
+ ko0 = ko * _F_MOV_MAX
224
+ nk = min(_F_MOV_MAX, K - ko0)
225
+ out_psums = []
226
+ for _ in range(n_rs):
227
+ out_psums.append(
228
+ nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
229
+ )
230
+ for ht in nl.affine_range(n_h):
231
+ h0 = ht * _P_MAX
232
+ ph = min(_P_MAX, H - h0)
233
+ for rs in range(n_rs):
234
+ rr0 = rs * _P_MAX
235
+ prs = min(_P_MAX, pr - rr0)
236
+ nisa.nc_matmul(
237
+ dst=out_psums[rs][0:prs, 0:nk],
238
+ stationary=gt_tiles[ht][0:ph, rr0:rr0 + prs],
239
+ moving=w2t_tiles[ht][0:ph, ko0:ko0 + nk],
240
+ )
241
+ # b2 broadcast, hoisted out of the rs loop (removing the broadcast itself
242
+ # cost 23% and 30% in two separate attempts).
243
+ b2_bc = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.sbuf)
244
+ nisa.dma_copy(dst=b2_bc[0:_P_MAX, 0:nk],
245
+ src=b2_hbm.ap(pattern=[[0, _P_MAX], [1, nk]], offset=ko0))
246
+ for rs in range(n_rs):
247
+ rr0 = rs * _P_MAX
248
+ prs = min(_P_MAX, pr - rr0)
249
+ out_sb = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=_BF16, buffer=nl.sbuf)
250
+ nisa.tensor_tensor(dst=out_sb[0:prs, 0:nk],
251
+ data1=out_psums[rs][0:prs, 0:nk],
252
+ data2=b2_bc[0:prs, 0:nk],
253
+ op=nl.add)
254
+ nisa.dma_copy(
255
+ dst=out_hbm[r_abs + rr0:r_abs + rr0 + prs, ko0:ko0 + nk],
256
+ src=out_sb[0:prs, 0:nk],
257
+ )
258
+
259
+ return out_hbm
260
+
261
+
262
+ def _no_grad(*tensors) -> None:
263
+ """Refuse gradient-tracking inputs -- this op registers no autograd rule."""
264
+ for t in tensors:
265
+ if t is not None and t.requires_grad:
266
+ raise RuntimeError(
267
+ "nki_mlp_k4_xp registers no autograd rule (INFERENCE ONLY). "
268
+ "Use under torch.no_grad()/eval()."
269
+ )
270
+
271
+
272
+ @nki_op("vit_enc::nki_mlp_k4_xp", mutates_args={})
273
+ def nki_mlp_k4_xp(
274
+ x: torch.Tensor,
275
+ w1t: torch.Tensor,
276
+ b1: torch.Tensor,
277
+ w2t: torch.Tensor,
278
+ b2: torch.Tensor,
279
+ ) -> torch.Tensor:
280
+ """Fused MLP on LNC=2, pre-transposed bf16 weights, x prestaged in the prologue.
281
+
282
+ Args:
283
+ x : [R, K] **bf16** on 'neuron'. ``R`` must be divisible by :data:`LNC`.
284
+ w1t : [K, H] bf16 == fc1.weight.T. b1 : [H] float32.
285
+ w2t : [H, K] bf16 == fc2.weight.T. b2 : [K] float32.
286
+
287
+ Returns:
288
+ [R, K] bf16.
289
+ """
290
+ _no_grad(x, w1t, b1, w2t, b2)
291
+ return _mlp_k4_xp_kernel[LNC](x, w1t, b1, w2t, b2)
292
+
293
+
294
+ #: Requested export alias. Same object as the incumbent-named nki_op, so the runtime
295
+ #: op-name string ("vit_enc::nki_mlp_k4_xp") is unchanged whichever name is used.
296
+ mlp = nki_mlp_k4_xp
build/torch-neuron/nki_kernels/mlp_res_ln.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GELU MLP with the residual add + NEXT layer's affine-free LN1 in the epilogue.
2
+
3
+ Same fc1/gelu as ``mlp.py``. fc2 is reordered to (row subtile rs, out block ko) so each 128-row tile's
4
+ full [128, K] fc2 output lands in one fp32 SBUF row, then:
5
+ x_new = x + (g @ w2.T + b2) fp32 (MLP output never rounded to bf16)
6
+ z1 = LN_no_affine(x_new; eps) bf16 (same stat chain as res_layernorm.py)
7
+ Returns (x_new [R,K] fp32, z1 [R,K] bf16). Replaces K4 + the next layer's K1.
8
+
9
+ --- original mlp.py notes ---
10
+ K4 -- fused MLP (fc1 + gelu + fc2), x-prestage ("XP") in prologue mode.
11
+
12
+ WHAT IT COMPUTES
13
+ ----------------
14
+ ``out = gelu(x @ w1.T + b1) @ w2.T + b2``, with the rows sharded across LNC cores and
15
+ the weights pre-transposed to bf16. Returns ``[R, K]`` bf16.
16
+
17
+ WHY IT IS SHAPED THIS WAY
18
+ -------------------------
19
+ This is the effective shipping kernel: it is the ``b1pre`` MLP (fc1 bias staged in the
20
+ prologue so every gelu's bias is resident from t=0) with the x-prestage applied. Two
21
+ design choices carry the performance:
22
+
23
+ * b1 first: the 32 per-block ``b1`` bias tiles are DMA'd before the 16 MB of weight
24
+ staging, so they land within ~1 us of kernel start and no gelu ever waits on its bias.
25
+ * x prestaged in the prologue: BOTH row blocks' ``x`` load + dma_transpose staging is
26
+ issued before the ``rb`` loop, in row-block order. rb=0's staging already hid in the
27
+ pre-K4 seam (where the Tensor engine is dark for ~148 us because K1/K3 emit no Tensor
28
+ instructions); prestaging rb=1 gives it the same cover, filling the ~26.6 us Tensor
29
+ idle window that used to sit at the rb0->rb1 boundary. Appending rb=1's stages after
30
+ rb=0's, in order, means rb=0's readiness cannot regress -- the first matmul cannot be
31
+ delayed.
32
+
33
+ The x staging is emitted through a single module-level function (``_stage_xt``) rather
34
+ than inlined per row block, so the ``x`` load and the dma_transpose each keep exactly one
35
+ source site. The two-row-block staging is preserved exactly: ``xt_sets`` holds one
36
+ transposed-tile set per row block, and the loop consumes ``xt_sets[rb]``.
37
+
38
+ PSUM: fc1 uses 1 bank, fc2 uses up to 4 banks; the ``y1_psum`` / ``out_psums`` tiles are
39
+ declared inside their loops so the ring never accumulates onto a stale PSUM value. The
40
+ b2 bias broadcast is kept (removing it regressed the sibling kernel by 23-30%).
41
+
42
+ There are no ``engine=`` pins anywhere in this kernel. INFERENCE ONLY: no autograd rule.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import math
48
+
49
+ import torch
50
+ from torch_neuronx import nki_op
51
+
52
+ import nki
53
+ import nki.isa as nisa
54
+ import nki.language as nl
55
+
56
+ #: PE array partition limit, and the moving-operand free-axis limit.
57
+ _P_MAX = 128
58
+ _F_MOV_MAX = 512
59
+
60
+ _BF16 = nl.bfloat16
61
+
62
+ #: Logical-to-physical NeuronCore degree. Must match the shipping shard's.
63
+ import os as _os
64
+ #: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
65
+ LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
66
+
67
+ _INV_SQRT2 = 1.0 / math.sqrt(2.0)
68
+ _INV_SQRT_2PI = 1.0 / math.sqrt(2.0 * math.pi)
69
+
70
+
71
+ def _div_ceil(n: int, d: int) -> int:
72
+ """Ceiling division (never write (n + d - 1) // d inline)."""
73
+ return (n + d - 1) // d
74
+
75
+
76
+ def _kernel_assert(condition: bool, error_text: str):
77
+ """Assert with an NKI-formatted error message."""
78
+ assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
79
+
80
+
81
+ def _stage_xt(x_hbm, r_abs, pr, n_k, K, rb_size):
82
+ """Emit one row block's x staging and return its ``n_k`` transposed bf16 tiles.
83
+
84
+ ``n_rs`` loads of ``[<=128, K]`` bf16, each followed by ``n_k`` DMA transposes into
85
+ the row block's ``xt_tiles``. Kept as a module-level emitter so the ``x`` load and
86
+ the dma_transpose each map to a single source site regardless of where it is called.
87
+
88
+ Args:
89
+ x_hbm: [R, K] @ HBM bf16, the MLP input.
90
+ r_abs: absolute starting row of this row block.
91
+ pr: number of rows in this row block (<= rb_size).
92
+ n_k: number of K tiles (fc1 contraction dimension tiles).
93
+ K: hidden-in dimension.
94
+ rb_size: row-block width (free axis of the transposed tiles).
95
+
96
+ Returns:
97
+ list of ``n_k`` transposed ``[128, rb_size]`` bf16 SBUF tiles.
98
+ """
99
+ n_rs = _div_ceil(pr, _P_MAX)
100
+ xt_tiles = []
101
+ for _ in range(n_k):
102
+ xt_tiles.append(nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf))
103
+ for rs in range(n_rs):
104
+ rr0 = rs * _P_MAX
105
+ prs = min(_P_MAX, pr - rr0)
106
+ x_b16 = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
107
+ nisa.dma_copy(
108
+ dst=x_b16[0:prs, 0:K],
109
+ src=x_hbm[r_abs + rr0:r_abs + rr0 + prs, 0:K],
110
+ )
111
+ for kt in range(n_k):
112
+ k0 = kt * _P_MAX
113
+ pk = min(_P_MAX, K - k0)
114
+ nisa.dma_transpose(dst=xt_tiles[kt][0:pk, rr0:rr0 + prs],
115
+ src=x_b16[0:prs, k0:k0 + pk])
116
+ return xt_tiles
117
+
118
+
119
+ @nki.jit
120
+ def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
121
+ """gelu(x @ w1.T + b1) @ w2.T + b2, rows sharded, b1 AND both x blocks resident early.
122
+
123
+ Args:
124
+ x_hbm (nl.ndarray): [R, K] @ HBM, **bfloat16** (kernel 1 emits bf16).
125
+ w1t_hbm (nl.ndarray): [K, H] @ HBM, **bf16**, == fc1.weight.T.
126
+ b1_hbm (nl.ndarray): [H] @ HBM, float32.
127
+ w2t_hbm (nl.ndarray): [H, K] @ HBM, **bf16**, == fc2.weight.T.
128
+ b2_hbm (nl.ndarray): [K] @ HBM, float32.
129
+
130
+ Returns:
131
+ nl.ndarray: [R, K] @ HBM, **bfloat16**.
132
+ """
133
+ R, K = x_hbm.shape
134
+ K_w1t, H = w1t_hbm.shape
135
+ H_w2t, K_w2t = w2t_hbm.shape
136
+ _kernel_assert(R >= 1 and K >= 1 and H >= 1, f"R({R}) K({K}) H({H}) must be >= 1")
137
+ _kernel_assert(K_w1t == K, f"w1t in-dim {K_w1t} != K {K}; expected w1t = w1.T of shape [K, H]")
138
+ _kernel_assert(K_w2t == K, f"w2t out-dim {K_w2t} != K {K}; expected w2t = w2.T of shape [H, K]")
139
+ _kernel_assert(H_w2t == H, f"w2t in-dim {H_w2t} != H {H}")
140
+ _kernel_assert(
141
+ R % LNC == 0,
142
+ f"the {LNC}-core kernel needs R divisible by {LNC}, got R={R}. "
143
+ f"program_id is a symbolic value, so a partial final row block cannot be "
144
+ f"clamped; use the one-core pre-transposed kernel for uneven R",
145
+ )
146
+
147
+ rows_per = R // LNC
148
+ pid = nl.program_id(0)
149
+ r_base = pid * rows_per
150
+
151
+ _kernel_assert(tuple(a_hbm.shape) == (R, K), "residual a must be [R, K] fp32")
152
+ xnew_hbm = nl.ndarray((R, K), dtype=nl.float32, buffer=nl.shared_hbm)
153
+ z_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
154
+ inv_k = 1.0 / float(K)
155
+ neg_inv_k = -inv_k
156
+
157
+ n_k = _div_ceil(K, _P_MAX) # K tiles (fc1 contraction)
158
+ n_h = _div_ceil(H, _P_MAX) # H tiles (fc1 out-rows / fc2 contraction)
159
+
160
+ # b1 FIRST, before the 16 MB of weight DMA: 32 [128,1] tiles issue at t=0 and land in
161
+ # ~1 us, so every gelu's bias is resident long before the first matmul retires.
162
+ b1_tiles = []
163
+ for ht in range(n_h):
164
+ h0 = ht * _P_MAX
165
+ ph = min(_P_MAX, H - h0)
166
+ t = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
167
+ nisa.dma_copy(dst=t[0:ph, 0:1], src=b1_hbm[h0:h0 + ph])
168
+ b1_tiles.append(t)
169
+
170
+ # Weight staging (pre-transposed bf16, one DMA per tile).
171
+ w1t_tiles = []
172
+ for kt in range(n_k):
173
+ k0 = kt * _P_MAX
174
+ pk = min(_P_MAX, K - k0)
175
+ t = nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf)
176
+ nisa.dma_copy(dst=t[0:pk, :], src=w1t_hbm[k0:k0 + pk, :])
177
+ w1t_tiles.append(t)
178
+
179
+ w2t_tiles = []
180
+ for ht in range(n_h):
181
+ h0 = ht * _P_MAX
182
+ ph = min(_P_MAX, H - h0)
183
+ t = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
184
+ nisa.dma_copy(dst=t[0:ph, :], src=w2t_hbm[h0:h0 + ph, :])
185
+ w2t_tiles.append(t)
186
+
187
+ # b2 broadcast ONCE over all K columns (fp32), plus LN per-lane scratch.
188
+ b2_bc = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf)
189
+ nisa.dma_copy(dst=b2_bc[0:_P_MAX, 0:K], src=b2_hbm.ap(pattern=[[0, _P_MAX], [1, K]], offset=0))
190
+ acc_s = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
191
+ neg_shift = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
192
+ neg_mu = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
193
+ var = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
194
+ rstd = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
195
+
196
+ # Main fused loop.
197
+ rb_size = _F_MOV_MAX
198
+ n_rb = _div_ceil(rows_per, rb_size)
199
+ n_ko = _div_ceil(K, _F_MOV_MAX)
200
+
201
+ # THE X-PRESTAGE: stage EVERY row block's x here, in rb order, so rb=1's serialised
202
+ # DMA chain runs inside the pre-K4 seam (Tensor dark) instead of inside the idle
203
+ # window at the rb0->rb1 boundary. Appending rb=1's stages after rb=0's leaves rb=0's
204
+ # stages exactly where they are on the Sync engine's sub-stream.
205
+ xt_sets = []
206
+ for rb_pre in range(n_rb):
207
+ xt_sets.append(_stage_xt(
208
+ x_hbm, r_base + rb_pre * rb_size,
209
+ min(rb_size, rows_per - rb_pre * rb_size), n_k, K, rb_size))
210
+
211
+ for rb in range(n_rb):
212
+ r_local = rb * rb_size
213
+ pr = min(rb_size, rows_per - r_local)
214
+ n_rs = _div_ceil(pr, _P_MAX)
215
+ r_abs = r_base + r_local
216
+
217
+ # x_T bf16 tiles: consume the prologue-staged set for this row block.
218
+ xt_tiles = xt_sets[rb]
219
+
220
+ # ---- fc1: gt_tiles[ht] = gelu(w1 @ x.T + b1), bf16, SBUF-resident ----
221
+ gt_tiles = []
222
+ for ht in range(n_h):
223
+ h0 = ht * _P_MAX
224
+ ph = min(_P_MAX, H - h0)
225
+ # Declared INSIDE the ht loop, so the ring never reuses a written PSUM tile.
226
+ # nc_matmul's default accumulate=None auto-detects "previously written" and
227
+ # would accumulate onto a stale value.
228
+ y1_psum = nl.ndarray((_P_MAX, rb_size), dtype=nl.float32, buffer=nl.psum)
229
+ for kt in nl.affine_range(n_k):
230
+ k0 = kt * _P_MAX
231
+ pk = min(_P_MAX, K - k0)
232
+ nisa.nc_matmul(
233
+ dst=y1_psum[0:ph, 0:pr],
234
+ stationary=w1t_tiles[kt][0:pk, h0:h0 + ph],
235
+ moving=xt_tiles[kt][0:pk, 0:pr],
236
+ )
237
+ # b1 comes from the prologue-resident tile.
238
+ gt = nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf)
239
+ nisa.activation(dst=gt[0:ph, 0:pr], op=nl.gelu,
240
+ data=y1_psum[0:ph, 0:pr], bias=b1_tiles[ht][0:ph, 0:1])
241
+ gt_tiles.append(gt)
242
+
243
+ # ---- fc2 + b2 + residual -> x_new (fp32), LN -> z1 (bf16), per 128-row subtile ----
244
+ for rs in range(n_rs):
245
+ rr0 = rs * _P_MAX
246
+ prs = min(_P_MAX, pr - rr0)
247
+ r0 = r_abs + rr0
248
+ row = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf) # residual, accumulated in place
249
+ z_t = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
250
+ nisa.dma_copy(dst=row[0:prs, 0:K], src=a_hbm[r0:r0 + prs, 0:K])
251
+ for ko in range(n_ko):
252
+ ko0 = ko * _F_MOV_MAX
253
+ nk = min(_F_MOV_MAX, K - ko0)
254
+ ps = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
255
+ for ht in nl.affine_range(n_h):
256
+ h0 = ht * _P_MAX
257
+ ph = min(_P_MAX, H - h0)
258
+ nisa.nc_matmul(
259
+ dst=ps[0:prs, 0:nk],
260
+ stationary=gt_tiles[ht][0:ph, rr0:rr0 + prs],
261
+ moving=w2t_tiles[ht][0:ph, ko0:ko0 + nk],
262
+ )
263
+ nisa.tensor_tensor(dst=row[0:prs, ko0:ko0 + nk], data1=row[0:prs, ko0:ko0 + nk],
264
+ data2=ps[0:prs, 0:nk], op=nl.add)
265
+ nisa.tensor_tensor(dst=row[0:prs, 0:K], data1=row[0:prs, 0:K], data2=b2_bc[0:prs, 0:K], op=nl.add)
266
+ nisa.dma_copy(dst=xnew_hbm[r0:r0 + prs, 0:K], src=row[0:prs, 0:K])
267
+ # stat chain (identical ops/order to res_layernorm.py)
268
+ nisa.tensor_scalar(dst=neg_shift[0:prs, 0:1], data=row[0:prs, 0:1],
269
+ op0=nl.multiply, operand0=-1.0)
270
+ nisa.activation_reduce(dst=z_t[0:prs, 0:K], op=nl.copy, data=row[0:prs, 0:K],
271
+ reduce_op=nl.add, reduce_res=acc_s[0:prs, 0:1],
272
+ bias=neg_shift[0:prs, 0:1], scale=1.0)
273
+ nisa.tensor_scalar(dst=neg_mu[0:prs, 0:1], data=acc_s[0:prs, 0:1], op0=nl.multiply,
274
+ operand0=neg_inv_k, op1=nl.add, operand1=neg_shift[0:prs, 0:1])
275
+ nisa.activation_reduce(dst=z_t[0:prs, 0:K], op=nl.square, data=row[0:prs, 0:K],
276
+ reduce_op=nl.add, reduce_res=acc_s[0:prs, 0:1],
277
+ bias=neg_mu[0:prs, 0:1], scale=1.0)
278
+ nisa.tensor_scalar(dst=var[0:prs, 0:1], data=acc_s[0:prs, 0:1], op0=nl.multiply,
279
+ operand0=inv_k, op1=nl.add, operand1=eps)
280
+ nisa.activation(dst=rstd[0:prs, 0:1], op=nl.rsqrt, data=var[0:prs, 0:1])
281
+ nisa.tensor_scalar(dst=z_t[0:prs, 0:K], data=row[0:prs, 0:K],
282
+ op0=nl.add, operand0=neg_mu[0:prs, 0:1],
283
+ op1=nl.multiply, operand1=rstd[0:prs, 0:1])
284
+ nisa.dma_copy(dst=z_hbm[r0:r0 + prs, 0:K], src=z_t[0:prs, 0:K])
285
+
286
+ return xnew_hbm, z_hbm
287
+
288
+
289
+ def _no_grad(*tensors) -> None:
290
+ for t in tensors:
291
+ if t is not None and t.requires_grad:
292
+ raise RuntimeError("nki_mlp_res_ln is inference-only; use torch.no_grad().")
293
+
294
+
295
+ @nki_op("vit_enc::nki_mlp_res_ln", mutates_args={})
296
+ def nki_mlp_res_ln(
297
+ x: torch.Tensor,
298
+ w1t: torch.Tensor,
299
+ b1: torch.Tensor,
300
+ w2t: torch.Tensor,
301
+ b2: torch.Tensor,
302
+ a: torch.Tensor,
303
+ eps: float,
304
+ ) -> tuple[torch.Tensor, torch.Tensor]:
305
+ """MLP(x) + residual a, then affine-free LN. x [R,K] bf16, a [R,K] fp32.
306
+ Returns (x_new [R,K] fp32, z [R,K] bf16)."""
307
+ _no_grad(x, w1t, b1, w2t, b2, a)
308
+ return _mlp_res_ln_kernel[LNC](x, w1t, b1, w2t, b2, a, eps)
build/torch-neuron/nki_kernels/res_layernorm.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """K1/K3 -- fused residual add + affine-free LayerNorm (the "fold" kernel).
2
+
3
+ WHAT IT COMPUTES
4
+ ----------------
5
+ Given a residual stream ``a`` [R, H] fp32 and a stack of 1..4 branch tensors
6
+ ``b`` [B, R, H] bf16, per token row this kernel computes::
7
+
8
+ x_new = a + sum_j b[j] # fp32, the untouched residual stream
9
+ z = (x_new - mean(x_new)) * rstd # bf16, LayerNorm WITHOUT affine
10
+
11
+ and returns ``(x_new, z)``. gamma/beta are NOT applied here -- they have been folded
12
+ into the downstream consumers' packed weights at freeze time (K1's z feeds K2's Q/K/V
13
+ projections; K3's z feeds K4's fc1). The affine fold is exact:
14
+ ``W @ (z*g + bt) + c == (W * g) @ z + (W @ bt + c)``, so scaling the consumer's input
15
+ columns by gamma and adding ``W @ bt`` to its bias reproduces the same math with the
16
+ affine removed from the norm kernel.
17
+
18
+ WHY IT IS SHAPED THIS WAY
19
+ -------------------------
20
+ * LNC = 2 shard: the kernel is launched on both NeuronCores; each owns ``R // 2`` rows
21
+ selected by ``program_id(0)``, so the two cores never touch the same tokens.
22
+ * Ring of depth 4 over the token tiles: four rotating sets of SBUF tiles let the DMA
23
+ loads of tile ``t+1`` overlap the compute of tile ``t`` without a write-after-read
24
+ hazard on the buffers.
25
+ * fp32 residual / bf16 branch: ``a`` (the residual stream) stays fp32 so ``x_new`` is
26
+ bitwise-invariant math and can be carried forward losslessly; the branch inputs and
27
+ the normalized output ``z`` are bf16, which is the precision boundary the downstream
28
+ kernels consume.
29
+ * Thinned stat chain: mean and variance are computed with a shift-and-reduce chain
30
+ (neg_shift -> copy-reduce -> neg_mu -> square-reduce -> var -> rsqrt) that keeps the
31
+ per-lane statistics in fp32; the final normalize is a single fused ``tensor_scalar``
32
+ (``z = (x_new + neg_mu) * rstd``) whose two per-lane operands cost the same as one op
33
+ and whose bf16 cast rides the write.
34
+
35
+ There are no ``engine=`` pins anywhere in this kernel; nothing engine-related to
36
+ preserve. INFERENCE ONLY: no autograd rule is registered.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import torch
42
+ from torch_neuronx import nki_op
43
+
44
+ import nki
45
+ import nki.isa as nisa
46
+ import nki.language as nl
47
+
48
+ #: SBUF partition count, and the token-tile height (rows processed per instruction).
49
+ _P_MAX = 128
50
+
51
+ #: The bf16 dtype used for branch inputs and the normalized output.
52
+ _BF16 = nl.bfloat16
53
+
54
+ #: Logical-to-physical NeuronCore degree. Rows are split evenly across LNC cores.
55
+ import os as _os
56
+ #: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
57
+ LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
58
+
59
+ #: Ring depth: number of rotating SBUF tile sets that overlap load with compute.
60
+ _RING = 4
61
+
62
+
63
+ def _kernel_assert(condition: bool, error_text: str):
64
+ """Assert with an NKI-formatted error message."""
65
+ assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
66
+
67
+
68
+ @nki.jit
69
+ def _res_layernorm_fold_kernel(a_hbm, b_hbm, eps):
70
+ """Emit the fold kernel body: ``(a + sum(b), LN_no_affine(a + sum(b)))``, token-major.
71
+
72
+ gamma/beta live in the consumers' packed weights, so this kernel takes no affine
73
+ parameters. Rows are sharded across ``LNC`` cores; each token tile is loaded through
74
+ a depth-4 ring, summed in fp32, normalized with the thinned stat chain, and the
75
+ normalized result is cast to bf16 on write.
76
+
77
+ Args:
78
+ a_hbm (nl.ndarray): [R, H] @ HBM float32 -- the residual stream.
79
+ b_hbm (nl.ndarray): [B, R, H] @ HBM bf16 -- B branch tensors (1 <= B <= 4), summed in.
80
+ eps (float): LayerNorm epsilon.
81
+
82
+ Returns:
83
+ tuple: ``(x_new [R, H] float32, z [R, H] bf16)`` both in shared HBM.
84
+ """
85
+ R, H = a_hbm.shape
86
+ n_b, Rb, Hb = b_hbm.shape
87
+
88
+ _kernel_assert(R >= 1 and H >= 1, f"R({R}) H({H}) must be >= 1")
89
+ _kernel_assert(Rb == R and Hb == H, f"b {(Rb, Hb)} must match a {(R, H)}")
90
+ _kernel_assert(1 <= n_b <= 4, f"b must stack 1..4 branches, got {n_b}")
91
+ _kernel_assert(
92
+ H <= 16384,
93
+ f"H={H} is one instruction's free axis here; no chunking is implemented.",
94
+ )
95
+ _kernel_assert(
96
+ R % (LNC * _P_MAX) == 0,
97
+ f"need R divisible by LNC*{_P_MAX} = {LNC * _P_MAX}, got R={R}",
98
+ )
99
+
100
+ rows_per = R // LNC
101
+ n_t = rows_per // _P_MAX
102
+ pid = nl.program_id(0)
103
+ r_base = pid * rows_per
104
+
105
+ x_new_hbm = nl.ndarray((R, H), dtype=nl.float32, buffer=nl.shared_hbm)
106
+ z_hbm = nl.ndarray((R, H), dtype=_BF16, buffer=nl.shared_hbm)
107
+
108
+ inv_h = 1.0 / float(H)
109
+ neg_inv_h = -inv_h
110
+
111
+ # Ring buffers: _RING rotating sets of (a tile, B branch tiles, sum tile, z tile).
112
+ a_sb = []
113
+ b_sb = []
114
+ s_sb = []
115
+ z_sb = []
116
+ for _i in range(_RING):
117
+ a_sb.append(nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf))
118
+ _slot = []
119
+ for _j in range(n_b):
120
+ _slot.append(nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf))
121
+ b_sb.append(_slot)
122
+ s_sb.append(nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf))
123
+ z_sb.append(nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf))
124
+
125
+ # Scratch destination for the reduce activations (its full tile is discarded; only
126
+ # the reduce result is consumed).
127
+ sink = nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf)
128
+
129
+ # Per-lane [128, 1] statistics.
130
+ acc = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
131
+ neg_shift = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
132
+ neg_mu = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
133
+ var = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
134
+ rstd = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
135
+
136
+ for t in range(n_t):
137
+ k = t % _RING
138
+ r0 = r_base + t * _P_MAX
139
+
140
+ # ---- load residual + branches for this tile ----
141
+ nisa.dma_copy(dst=a_sb[k], src=a_hbm[r0:r0 + _P_MAX, 0:H])
142
+ for j in range(n_b):
143
+ nisa.dma_copy(dst=b_sb[k][j], src=b_hbm[j, r0:r0 + _P_MAX, 0:H])
144
+
145
+ # ---- x_new = a + sum_j b[j] in fp32, then write the residual stream out ----
146
+ nisa.tensor_tensor(dst=s_sb[k], data1=a_sb[k], data2=b_sb[k][0], op=nl.add)
147
+ for j in range(1, n_b):
148
+ nisa.tensor_tensor(dst=s_sb[k], data1=s_sb[k], data2=b_sb[k][j], op=nl.add)
149
+
150
+ nisa.dma_copy(dst=x_new_hbm[r0:r0 + _P_MAX, 0:H], src=s_sb[k])
151
+
152
+ # ---- thinned stat chain: mean via shift-and-reduce, variance, rstd ----
153
+ nisa.tensor_scalar(dst=neg_shift, data=s_sb[k][0:_P_MAX, 0:1],
154
+ op0=nl.multiply, operand0=-1.0)
155
+ nisa.activation_reduce(dst=sink, op=nl.copy, data=s_sb[k],
156
+ reduce_op=nl.add, reduce_res=acc,
157
+ bias=neg_shift, scale=1.0)
158
+ nisa.tensor_scalar(dst=neg_mu, data=acc, op0=nl.multiply, operand0=neg_inv_h,
159
+ op1=nl.add, operand1=neg_shift)
160
+ nisa.activation_reduce(dst=sink, op=nl.square, data=s_sb[k],
161
+ reduce_op=nl.add, reduce_res=acc, bias=neg_mu, scale=1.0)
162
+ nisa.tensor_scalar(dst=var, data=acc, op0=nl.multiply, operand0=inv_h,
163
+ op1=nl.add, operand1=eps)
164
+ nisa.activation(dst=rstd, op=nl.rsqrt, data=var)
165
+
166
+ # ---- one-op affine-free apply: z = (x_new + neg_mu) * rstd, bf16 cast on write.
167
+ # Both operands are per-lane [128, 1] vectors; the fused two-op tensor_scalar
168
+ # costs the same as one op.
169
+ nisa.tensor_scalar(dst=z_sb[k], data=s_sb[k],
170
+ op0=nl.add, operand0=neg_mu,
171
+ op1=nl.multiply, operand1=rstd)
172
+
173
+ nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:H], src=z_sb[k])
174
+
175
+ return x_new_hbm, z_hbm
176
+
177
+
178
+ def _no_grad(*tensors: torch.Tensor) -> None:
179
+ """Refuse gradient-tracking inputs -- this op registers no autograd rule."""
180
+ for t in tensors:
181
+ if t.requires_grad:
182
+ raise RuntimeError(
183
+ "nki_res_layernorm_fold registers no autograd rule. Use under "
184
+ "torch.no_grad()/eval()."
185
+ )
186
+
187
+
188
+ @nki_op("vit_enc::nki_res_layernorm_fold", mutates_args={})
189
+ def nki_res_layernorm_fold(
190
+ a: torch.Tensor,
191
+ b: torch.Tensor,
192
+ eps: float,
193
+ ) -> tuple[torch.Tensor, torch.Tensor]:
194
+ """Fused residual add + affine-free LayerNorm on LNC=2 (gamma/beta folded away).
195
+
196
+ The consumers MUST have been packed with folded weights; feeding ``z`` to unfolded
197
+ weights silently drops the affine.
198
+
199
+ Args:
200
+ a: [R, H] float32 on 'neuron'. ``R`` divisible by ``LNC * 128``.
201
+ b: [B, R, H] bf16 on 'neuron'. B branch tensors (1 <= B <= 4), summed in.
202
+ eps: LayerNorm epsilon.
203
+
204
+ Returns:
205
+ ``(x_new [R, H] float32, z [R, H] bf16)``.
206
+ """
207
+ _no_grad(a, b)
208
+ return _res_layernorm_fold_kernel[LNC](a, b, eps)
209
+
210
+
211
+ #: Requested export alias. Same object as the incumbent-named nki_op, so the runtime
212
+ #: op-name string ("vit_enc::nki_res_layernorm_fold") is unchanged whichever name is used.
213
+ res_layernorm_fold = nki_res_layernorm_fold
build/torch-neuron/nki_kernels/swiglu_stream.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Weight-streaming SwiGLU FFN, with an optional fused residual + next-layer LN epilogue.
2
+
3
+ out = (silu(x @ w1.T + b1) * (x @ wg.T + bg)) @ w2.T + b2 (SwiGLU FFN)
4
+
5
+ WHY STREAMING. ``swiglu.py`` keeps gate+up+down weights SBUF-resident (3 x K x I bf16). At ViT-H+
6
+ (K=1280, I=3416) that is ~205 KiB/partition, over the ~192 KiB budget, so the compiler spills.
7
+ Here the intermediate dim I is processed in chunks of CI columns: only one chunk's gate/up columns
8
+ and down rows are live (double-buffered by the scheduler: chunk c+1's DMA has no dependency on
9
+ chunk c's matmuls), and the down-projection is accumulated across chunks in an fp32 SBUF
10
+ accumulator with rows on partitions.
11
+
12
+ LOOP ORDER. Rows (per core) are processed in super-blocks of S rows (host-chosen, <=1024). Within
13
+ a super-block the chunk loop is OUTER, so each weight chunk is DMA'd once per super-block and
14
+ reused by every 512-row slice in it -- weight traffic per row falls as S grows.
15
+
16
+ FUSED EPILOGUE (``fuse=1``). The accumulator is initialised from the fp32 residual ``a``; after the
17
+ last chunk: x_new = acc + b2 (fp32), z = LN_no_affine(x_new; eps) (bf16, same stat chain as
18
+ res_layernorm.py). Returns (x_new, z). ``fuse=0`` returns bf16 ``acc + b2`` like ``swiglu.py``.
19
+
20
+ Rows are sharded across LNC cores (R % (LNC*128) == 0). INFERENCE ONLY.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import torch
25
+ from torch_neuronx import nki_op
26
+
27
+ 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"))
34
+
35
+ _P_MAX = 128
36
+ _F_MOV_MAX = 512
37
+ _BF16 = nl.bfloat16
38
+
39
+
40
+ def _div_ceil(n: int, d: int) -> int:
41
+ return (n + d - 1) // d
42
+
43
+
44
+ def _kernel_assert(cond: bool, msg: str):
45
+ assert cond, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {msg}"
46
+
47
+
48
+ def plan(K: int, I: int, rows_per_core: int, budget: int = 176 * 1024):
49
+ """Host-side choice of (S, CI) from a per-partition SBUF estimate (bytes)."""
50
+ n_k = _div_ceil(K, _P_MAX)
51
+ best = None
52
+ for CI in (1024, 512, 256, 128):
53
+ for S in (1024, 512, 256, 128):
54
+ if S > _div_ceil(rows_per_core, _P_MAX) * _P_MAX and S != 128:
55
+ continue
56
+ nh = CI // _P_MAX
57
+ w = 2 * (2 * n_k * CI * 2 + nh * K * 2) # 2 chunks in flight
58
+ acc = (S // _P_MAX) * K * 4 # fp32 accumulator rows
59
+ xt = n_k * S * 2 + (S // _P_MAX) * K * 2 # x^T + staging rows
60
+ g = nh * _F_MOV_MAX * (2 + 4) # g bf16 + silu fp32 per tile
61
+ ep = 2 * K * 2 + K * 4 # z tiles + b2 broadcast
62
+ tot = w + acc + xt + g + ep + _div_ceil(I, _P_MAX) * 8 + 4096
63
+ if tot <= budget:
64
+ n_sb = _div_ceil(rows_per_core, S)
65
+ key = (-n_sb, CI, -S) # fewest weight re-streams, then big CI, then tight S
66
+ if best is None or key > best[0]:
67
+ best = (key, S, CI, tot)
68
+ _kernel_assert(best is not None, f"no SBUF plan for K={K} I={I} rows={rows_per_core}")
69
+ return best[1], best[2], best[3]
70
+
71
+
72
+ def _stage_xt(x_hbm, r_abs, S, n_k, K):
73
+ xt = []
74
+ for _ in range(n_k):
75
+ xt.append(nl.ndarray((_P_MAX, S), dtype=_BF16, buffer=nl.sbuf))
76
+ for rs in range(S // _P_MAX):
77
+ rr0 = rs * _P_MAX
78
+ xb = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
79
+ nisa.dma_copy(dst=xb[0:_P_MAX, 0:K], src=x_hbm[r_abs + rr0:r_abs + rr0 + _P_MAX, 0:K])
80
+ for kt in range(n_k):
81
+ k0 = kt * _P_MAX
82
+ pk = min(_P_MAX, K - k0)
83
+ nisa.dma_transpose(dst=xt[kt][0:pk, rr0:rr0 + _P_MAX], src=xb[0:_P_MAX, k0:k0 + pk])
84
+ return xt
85
+
86
+
87
+ def _body(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, a_hbm, eps, S, CI, fuse):
88
+ R, K = x_hbm.shape
89
+ K1, I = w1t_hbm.shape
90
+ _kernel_assert(K1 == K and tuple(wgt_hbm.shape) == (K, I), "w1t/wgt must be [K, I]")
91
+ _kernel_assert(tuple(w2t_hbm.shape) == (I, K), "w2t must be [I, K]")
92
+ _kernel_assert(R % (LNC * _P_MAX) == 0, f"R={R} must be a multiple of LNC*128")
93
+ rows_per = R // LNC
94
+ _kernel_assert(S % _P_MAX == 0, f"S={S} must be a multiple of 128")
95
+ _kernel_assert(CI % _P_MAX == 0, "CI must be a multiple of 128")
96
+
97
+ pid = nl.program_id(0)
98
+ r_base = pid * rows_per
99
+ n_k = _div_ceil(K, _P_MAX)
100
+ n_h = _div_ceil(I, _P_MAX)
101
+ n_c = _div_ceil(I, CI)
102
+ n_ko = _div_ceil(K, _F_MOV_MAX)
103
+ n_sb = _div_ceil(rows_per, S)
104
+
105
+ if fuse:
106
+ xnew_hbm = nl.ndarray((R, K), dtype=nl.float32, buffer=nl.shared_hbm)
107
+ z_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
108
+ else:
109
+ out_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
110
+
111
+ # biases (tiny, resident): b1/bg per 128-row tile of I, b2 broadcast over partitions.
112
+ b1_t = []
113
+ bg_t = []
114
+ for ht in range(n_h):
115
+ h0 = ht * _P_MAX
116
+ ph = min(_P_MAX, I - h0)
117
+ t1 = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
118
+ nisa.dma_copy(dst=t1[0:ph, 0:1], src=b1_hbm[h0:h0 + ph])
119
+ b1_t.append(t1)
120
+ tg = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
121
+ nisa.dma_copy(dst=tg[0:ph, 0:1], src=bg_hbm[h0:h0 + ph])
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
+
126
+ inv_k = 1.0 / float(K)
127
+ neg_inv_k = -inv_k
128
+
129
+ for sb in range(n_sb):
130
+ r_sb = r_base + sb * S
131
+ Ssb = min(S, rows_per - sb * S) # ragged last super-block (multiple of 128)
132
+ n_rs = Ssb // _P_MAX
133
+ xt = _stage_xt(x_hbm, r_sb, Ssb, n_k, K)
134
+
135
+ acc = []
136
+ for rs in range(n_rs):
137
+ t = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf)
138
+ if fuse:
139
+ nisa.dma_copy(dst=t[0:_P_MAX, 0:K],
140
+ src=a_hbm[r_sb + rs * _P_MAX:r_sb + (rs + 1) * _P_MAX, 0:K])
141
+ acc.append(t)
142
+
143
+ for c in range(n_c):
144
+ i0 = c * CI
145
+ ci = min(CI, I - i0)
146
+ nh_c = _div_ceil(ci, _P_MAX)
147
+ # ---- this chunk's weights (gate/up columns i0:i0+ci, down rows i0:i0+ci) ----
148
+ w1c = []
149
+ wgc = []
150
+ for kt in range(n_k):
151
+ k0 = kt * _P_MAX
152
+ pk = min(_P_MAX, K - k0)
153
+ t1 = nl.ndarray((_P_MAX, CI), dtype=_BF16, buffer=nl.sbuf)
154
+ nisa.dma_copy(dst=t1[0:pk, 0:ci], src=w1t_hbm[k0:k0 + pk, i0:i0 + ci])
155
+ w1c.append(t1)
156
+ tg = nl.ndarray((_P_MAX, CI), dtype=_BF16, buffer=nl.sbuf)
157
+ nisa.dma_copy(dst=tg[0:pk, 0:ci], src=wgt_hbm[k0:k0 + pk, i0:i0 + ci])
158
+ wgc.append(tg)
159
+ w2c = []
160
+ for hl in range(nh_c):
161
+ h0 = i0 + hl * _P_MAX
162
+ ph = min(_P_MAX, I - h0)
163
+ t2 = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
164
+ nisa.dma_copy(dst=t2[0:ph, 0:K], src=w2t_hbm[h0:h0 + ph, 0:K])
165
+ w2c.append(t2)
166
+
167
+ for rb in range(_div_ceil(Ssb, _F_MOV_MAX)):
168
+ q0 = rb * _F_MOV_MAX
169
+ rw = min(_F_MOV_MAX, Ssb - q0)
170
+ # ---- gate/up for this chunk x this 512-row slice -> g [ci, rw] bf16 ----
171
+ g_t = []
172
+ for hl in range(nh_c):
173
+ hg = (i0 // _P_MAX) + hl
174
+ ph = min(_P_MAX, I - (i0 + hl * _P_MAX))
175
+ c0 = hl * _P_MAX
176
+ y1 = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
177
+ for kt in nl.affine_range(n_k):
178
+ pk = min(_P_MAX, K - kt * _P_MAX)
179
+ nisa.nc_matmul(dst=y1[0:ph, 0:rw],
180
+ stationary=w1c[kt][0:pk, c0:c0 + ph],
181
+ moving=xt[kt][0:pk, q0:q0 + rw])
182
+ yg = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
183
+ for kt in nl.affine_range(n_k):
184
+ pk = min(_P_MAX, K - kt * _P_MAX)
185
+ nisa.nc_matmul(dst=yg[0:ph, 0:rw],
186
+ stationary=wgc[kt][0:pk, c0:c0 + ph],
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=b1_t[hg][0:ph, 0:1])
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=bg_t[hg][0:ph, 0:1],
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) ----
198
+ for rs_l in range(rw // _P_MAX):
199
+ rs = (q0 // _P_MAX) + rs_l
200
+ for ko in range(n_ko):
201
+ ko0 = ko * _F_MOV_MAX
202
+ nk = min(_F_MOV_MAX, K - ko0)
203
+ ps = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
204
+ for hl in nl.affine_range(nh_c):
205
+ ph = min(_P_MAX, I - (i0 + hl * _P_MAX))
206
+ nisa.nc_matmul(dst=ps[0:_P_MAX, 0:nk],
207
+ stationary=g_t[hl][0:ph, rs_l * _P_MAX:(rs_l + 1) * _P_MAX],
208
+ moving=w2c[hl][0:ph, ko0:ko0 + nk])
209
+ if c == 0 and not fuse:
210
+ nisa.tensor_copy(dst=acc[rs][0:_P_MAX, ko0:ko0 + nk], src=ps[0:_P_MAX, 0:nk])
211
+ else:
212
+ nisa.tensor_tensor(dst=acc[rs][0:_P_MAX, ko0:ko0 + nk],
213
+ data1=acc[rs][0:_P_MAX, ko0:ko0 + nk],
214
+ data2=ps[0:_P_MAX, 0:nk], op=nl.add)
215
+
216
+ # ---- epilogue per 128-row subtile ----
217
+ for rs in range(n_rs):
218
+ r0 = r_sb + rs * _P_MAX
219
+ row = acc[rs]
220
+ nisa.tensor_tensor(dst=row[0:_P_MAX, 0:K], data1=row[0:_P_MAX, 0:K],
221
+ data2=b2_bc[0:_P_MAX, 0:K], op=nl.add)
222
+ zt = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
223
+ if fuse:
224
+ nisa.dma_copy(dst=xnew_hbm[r0:r0 + _P_MAX, 0:K], src=row[0:_P_MAX, 0:K])
225
+ neg_shift = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
226
+ acc_s = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
227
+ neg_mu = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
228
+ var = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
229
+ rstd = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
230
+ nisa.tensor_scalar(dst=neg_shift, data=row[0:_P_MAX, 0:1], op0=nl.multiply, operand0=-1.0)
231
+ nisa.activation_reduce(dst=zt, op=nl.copy, data=row[0:_P_MAX, 0:K],
232
+ reduce_op=nl.add, reduce_res=acc_s, bias=neg_shift, scale=1.0)
233
+ nisa.tensor_scalar(dst=neg_mu, data=acc_s, op0=nl.multiply, operand0=neg_inv_k,
234
+ op1=nl.add, operand1=neg_shift)
235
+ nisa.activation_reduce(dst=zt, op=nl.square, data=row[0:_P_MAX, 0:K],
236
+ reduce_op=nl.add, reduce_res=acc_s, bias=neg_mu, scale=1.0)
237
+ nisa.tensor_scalar(dst=var, data=acc_s, op0=nl.multiply, operand0=inv_k,
238
+ op1=nl.add, operand1=eps)
239
+ nisa.activation(dst=rstd, op=nl.rsqrt, data=var)
240
+ nisa.tensor_scalar(dst=zt, data=row[0:_P_MAX, 0:K], op0=nl.add, operand0=neg_mu,
241
+ op1=nl.multiply, operand1=rstd)
242
+ nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:K], src=zt)
243
+ else:
244
+ nisa.tensor_copy(dst=zt, src=row[0:_P_MAX, 0:K])
245
+ nisa.dma_copy(dst=out_hbm[r0:r0 + _P_MAX, 0:K], src=zt)
246
+
247
+ if fuse:
248
+ return xnew_hbm, z_hbm
249
+ return out_hbm
250
+
251
+
252
+ @nki.jit
253
+ def _swiglu_stream_kernel(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, S, CI):
254
+ return _body(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, None, 0.0, S, CI, 0)
255
+
256
+
257
+ @nki.jit
258
+ def _swiglu_stream_ln_kernel(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, a_hbm, eps, S, CI):
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:
265
+ raise RuntimeError("swiglu_stream is inference-only; use torch.no_grad().")
266
+
267
+
268
+ def _plan_for(x, w1t):
269
+ S, CI, _ = plan(x.shape[1], w1t.shape[1], x.shape[0] // LNC)
270
+ S = int(_os.environ.get("VIT_ENC_SW_S", S))
271
+ CI = int(_os.environ.get("VIT_ENC_SW_CI", CI))
272
+ return S, CI
273
+
274
+
275
+ @nki_op("vit_enc::nki_swiglu_stream", mutates_args={})
276
+ def nki_swiglu_stream(x: torch.Tensor, w1t: torch.Tensor, b1: torch.Tensor, wgt: torch.Tensor,
277
+ bg: torch.Tensor, w2t: torch.Tensor, b2: torch.Tensor) -> torch.Tensor:
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={})
285
+ def nki_swiglu_stream_ln(x: torch.Tensor, w1t: torch.Tensor, b1: torch.Tensor, wgt: torch.Tensor,
286
+ bg: torch.Tensor, w2t: torch.Tensor, b2: torch.Tensor, a: torch.Tensor,
287
+ eps: float) -> tuple[torch.Tensor, torch.Tensor]:
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)
examples/dinov3_features.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DINOv3 ViT-L/16 features on trn2 with vit-encoder-neuron-kernels.
2
+
3
+ NEURON_LOGICAL_NC_CONFIG=2 python dinov3_features.py
4
+ """
5
+ import types
6
+ import torch
7
+ from kernels import get_kernel
8
+ from transformers import AutoModel
9
+
10
+ k = get_kernel("jburtoft/vit-encoder-neuron-kernels", version=1, trust_remote_code=True)
11
+ model = AutoModel.from_pretrained("facebook/dinov3-vitl16-pretrain-lvd1689m", dtype=torch.float32).eval()
12
+ ref_model = AutoModel.from_pretrained("facebook/dinov3-vitl16-pretrain-lvd1689m", dtype=torch.float32).eval()
13
+ model = model.to("neuron")
14
+ model.forward = types.MethodType(k.NeuronDINOv3ViTModel.forward, model)
15
+ model = torch.compile(model, backend="neuron")
16
+
17
+ torch.manual_seed(0)
18
+ pixel_values = torch.randn(4, 3, 224, 224)
19
+ with torch.no_grad():
20
+ out = model(pixel_values.to("neuron")).last_hidden_state.cpu()
21
+ ref = ref_model(pixel_values).last_hidden_state
22
+ print("features", tuple(out.shape), "rel_l2 vs fp32 CPU:", ((out - ref).norm() / ref.norm()).item())