jburtoft commited on
Commit
eadc767
·
1 Parent(s): 8caafae

v1: V-JEPA2 ViT-L encoder NKI kernels for Trainium2 (4.35x, 44% MFU)

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,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## v1 (2026-09-25)
4
+
5
+ First release. Four NKI kernels replacing the per-layer torch ops of the V-JEPA2 ViT-L video
6
+ encoder on Trainium2, exposed as `NeuronVJEPA2Encoder` for `transformers.KernelConfig`.
7
+
8
+ * **4.35x over the stock torch encoder** (104.18 -> 23.94 ms/clip, `torch.compile(backend="neuron")`),
9
+ **MFU 10.1% -> 44.08%** (neuron-explorer `mfu_estimated_percent`, device window).
10
+ * End-to-end serving 42.8% MFU (24.59 ms/clip) with preprocessing folded into the compiled graph
11
+ and double-buffered uint8 host-to-device copies.
12
+ * Output **bitwise identical** to the verified reference implementation (`torch.equal`), including
13
+ through the KernelConfig forward graft under `torch.compile`; rel_l2 4.2e-2 vs the fp32 torch
14
+ encoder (same as stock -- both run bf16 matmuls).
15
+ * `can_torch_compile = True` set, so the kernels library does not silently fall back to the stock
16
+ layer under `Mode.TORCH_COMPILE`.
17
+ * The one-time weight fold is `@torch._dynamo.disable`d: compiled folding rounds differently and
18
+ breaks bitwise parity (observed output norm 4487.45 vs the correct 4486.19).
19
+ * Verified on trn2.3xlarge: neuronx-cc 2.27.2878.0, torch-neuronx 2.11.3, nki 0.6.0,
20
+ transformers 5.13.1, neuron-explorer 2.32.0.
21
+
22
+ Known limitation: 16 frames / 2048 tokens / batch 1 only. 64-frame clips (8192 tokens) are not
23
+ supported -- three of the vendor attention kernel's SBUF buffers scale with keys-per-section
24
+ (347 KiB/lane needed vs a 192 KiB budget, device-confirmed), so 8192 keys require the
25
+ flash-sectioning path whose cross-section combine this integration does not yet reproduce
26
+ correctly. Off-geometry input falls back to the stock encoder.
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,8 @@
 
 
 
 
 
 
 
 
 
1
+ V-JEPA2 encoder NKI kernels for AWS Trainium2
2
+ Copyright 2026 (kernels: res_layernorm.py, attention_block.py, mlp.py, rope_tables.py,
3
+ model glue and tooling -- original work, Apache-2.0)
4
+
5
+ kernels/attention_inner.py is a modified copy of the AWS Neuron SDK's nkilib
6
+ core/attention/attention_cte.py, Copyright Amazon.com, Inc. or its affiliates,
7
+ licensed under the Apache License, Version 2.0. See the file header for the
8
+ list of modifications.
README.md ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ library_name: kernels
4
+ tags:
5
+ - kernel
6
+ - neuron
7
+ - nki
8
+ - trainium
9
+ - trn2
10
+ - vjepa2
11
+ - video
12
+ - vision
13
+ - attention
14
+ - encoder
15
+ ---
16
+
17
+ # V-JEPA2 ViT-L Encoder NKI Kernels for AWS Trainium2
18
+
19
+ **Version 1 (2026-09-25).** Four NKI kernels replacing the per-layer torch ops of the
20
+ [V-JEPA2](https://huggingface.co/facebook/vjepa2-vitl-fpc64-256) ViT-L video encoder on trn2:
21
+ **4.35x over the stock torch path (104.2 → 23.9 ms/clip), MFU 10.1% → 44.1%** (neuron-explorer
22
+ `mfu_estimated_percent`, device window). Output is **bitwise identical** to the reference
23
+ implementation this was verified against, and rel_l2 4.2e-2 vs the fp32 torch encoder (the
24
+ model's own bf16-matmul sensitivity).
25
+
26
+ ## What ships
27
+
28
+ | Component | Purpose |
29
+ |---|---|
30
+ | `NeuronVJEPA2Encoder` (layer) | KernelConfig-mappable drop-in for `VJEPA2Encoder`: the 4-kernel layer loop, lazy one-time weight fold, stock fallback off-device/off-geometry |
31
+ | `res_layernorm_fold` | K1/K3: residual add + affine-free LayerNorm (fp32 stream in, bf16 normalized out); the LN affines are folded into the neighboring matmuls |
32
+ | `attention_block` | K2: fused QKV projection (LN-gain folded, lane-permuted) + 3D RoPE + flash-attention inner + fused out-projection → [2, N, H] per-core partials (LNC=2) |
33
+ | `mlp` | K4: fc1 + GELU + fc2, bf16 matmuls, fp32 boundary, prologue-prestaged row blocks |
34
+ | `pack_*`, `build_rope_tables` | host-side weight folding (used internally by the layer; exported for direct kernel callers) |
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ import torch
40
+ from transformers import AutoModel, KernelConfig
41
+
42
+ kernel_config = KernelConfig({
43
+ "VJEPA2Encoder": "jburtoft/vjepa2-neuron-kernels:NeuronVJEPA2Encoder",
44
+ })
45
+ model = AutoModel.from_pretrained(
46
+ "facebook/vjepa2-vitl-fpc64-256",
47
+ dtype=torch.float32,
48
+ kernel_config=kernel_config,
49
+ device_map="neuron",
50
+ ).eval()
51
+
52
+ enc = torch.compile(model.encoder, backend="neuron") # the published numbers are of this graph
53
+ video = your_clip # [1, 16, 3, 256, 256] fp32
54
+ with torch.no_grad():
55
+ embeddings = enc(video).last_hidden_state # [1, 2048, 1024]
56
+ ```
57
+
58
+ > **`torch.compile(backend="neuron")` is part of the configuration.** Eager dispatch adds
59
+ > ~5.5 ms/forward. The layer sets `can_torch_compile`, so the kernels library will not silently
60
+ > swap it out under compile.
61
+
62
+ > **Resolving via `get_kernel` / `kernels`:** on a vLLM (0.24) venv where `torch` is a
63
+ > CUDA-tagged build, `import libtorch_neuronx_lite` (or `torch_xla`) **before** `get_kernel(...)`
64
+ > so `torch.neuron` is registered and the `torch-neuron` variant is selected. On a PyTorch Native
65
+ > container (`torch.neuron` already registered) this import is unnecessary.
66
+
67
+ ## Performance (trn2.3xlarge, verified 2026-09-25)
68
+
69
+ | | stock torch encoder | these kernels |
70
+ |---|---|---|
71
+ | throughput, compiled graph | 104.18 ms/clip | **23.94 ms/clip (4.35x)** |
72
+ | device MFU (`mfu_estimated_percent`) | 10.1% | **44.08%** |
73
+ | end-to-end serving, preprocess on device + double-buffered uint8 H2D | — | 24.59 ms/clip = **42.8%** |
74
+ | end-to-end single stream, nothing overlapped | — | 29.86 ms = 35.7% |
75
+
76
+ MFU is neuron-explorer's own field: model FLOPs (1.6557e12, equal to the closed-form
77
+ QK+PV+projections+MLP+patch count to 0.000%) over 157.3 TFLOP/s (one logical NeuronCore =
78
+ 2 physical cores, bf16) times the window. One logical core of four: a single stream uses ~11%
79
+ of the chip; four concurrent streams reach ~44% chip-wide.
80
+
81
+ Where the remaining 2.3x to physics goes (measured, not estimated): the softmax exp runs on the
82
+ Scalar engine at 1 element/lane/cycle (+2.6 ms over physics — unreachable without different
83
+ hardware), instruction issue overhead (~3.8 ms), and the queueing of three engines co-bound
84
+ within 0.5% (~3.7 ms). The exact-math kernel-level candidate classes are enumerated to
85
+ exhaustion; details in the source repository's cycle records.
86
+
87
+ ## Correctness
88
+
89
+ * Full-forward output **bitwise identical** (`torch.equal`) to the verified reference
90
+ implementation, including through this repo's KernelConfig-style forward graft under
91
+ `torch.compile`.
92
+ * rel_l2 vs the fp32 torch encoder: 4.2028e-2 — identical to the stock-vs-fp32 difference
93
+ (both paths run bf16 matmuls; neither is "more approximate" than the other).
94
+ * K1/K3's GpSimd `rsqrt` is measurably *closer* to fp64 than the torch path's (6.9e-8 vs
95
+ 3.9e-5 relative).
96
+ * The vendor attention inner is run-to-run nondeterministic on some layers at a few bf16
97
+ positions (fp64 agreement unchanged) — an upstream kernel property, reported to the Neuron
98
+ team. Bitwise comparisons must use a fixed captured sample.
99
+
100
+ ## Requirements
101
+
102
+ * **Hardware**: trn2 (verified on trn2.3xlarge, LNC=2).
103
+ * **SDK**: neuronx-cc 2.27.2878.0; torch-neuronx 2.11.3 (PyTorch Native); nki 0.6.0;
104
+ nkilib (bundled with the SDK — `ModularAllocator` is imported from it).
105
+ * **Transformers** with `KernelConfig` support; `kernels` library.
106
+ * `NKI_ENABLE_TRACE_CACHE=0` in the environment (the runtime defaults the cross-process trace
107
+ cache on; a stale NEFF silently captures a different instruction stream).
108
+
109
+ ## Repository layout
110
+
111
+ ```
112
+ build/torch-neuron/
113
+ ├── __init__.py # public API re-exports
114
+ ├── metadata.json # kernels-library metadata (backend: neuron)
115
+ ├── layers.py # NeuronVJEPA2Encoder (KernelConfig target)
116
+ └── nki_kernels/
117
+ ├── res_layernorm.py # K1/K3
118
+ ├── attention_block.py # K2 outer (QKV + RoPE + out-proj) and weight packing
119
+ ├── attention_inner.py # K2 inner: MODIFIED copy of the Neuron SDK's nkilib attention_cte
120
+ ├── mlp.py # K4
121
+ └── rope_tables.py # host-side 3D RoPE tables
122
+ ```
123
+
124
+ ## Known limitations
125
+
126
+ * **16 frames / 2048 tokens / batch 1 / 256x256 only.** The model's native 64-frame clip
127
+ (8192 tokens) is NOT supported: three of the vendor attention kernel's SBUF buffers scale
128
+ with keys-per-section (347 KiB/lane needed vs the 192 KiB budget, device-confirmed), so 8192
129
+ keys require the flash-sectioning path, whose cross-section combine this integration does not
130
+ yet reproduce correctly. Off-geometry inputs fall back to the stock implementation.
131
+ * **Inference only** — no autograd rule; the layer refuses in training mode.
132
+ * The first device forward performs the one-time weight fold eagerly (~seconds); it is
133
+ deliberately kept outside `torch.compile` (compiled folding rounds differently and breaks
134
+ bitwise parity with the verified reference).
135
+
136
+
137
+ ## Attribution and license
138
+
139
+ Apache-2.0 (see `LICENSE`, `NOTICE`). `nki_kernels/attention_inner.py` is a **modified copy of
140
+ the AWS Neuron SDK's `nkilib` `core/attention/attention_cte.py`** (Copyright Amazon.com, Inc.
141
+ or its affiliates, Apache-2.0); the file header lists the modifications (four engine-placement
142
+ pins worth ~1.19x end-to-end, and removal of code paths unreachable for this configuration).
143
+ The other three kernels, the layer glue and the packing are original work.
build/torch-neuron/__init__.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """V-JEPA2 ViT-L encoder NKI kernels for AWS Trainium2 (trn2).
2
+
3
+ Public API:
4
+ NeuronVJEPA2Encoder -- KernelConfig-mappable layer ("VJEPA2Encoder" -> this)
5
+ res_layernorm_fold, attention_block, mlp -- the three kernel entry points
6
+ pack_* / build_rope_tables / lane_permutation -- host-side weight packing
7
+ """
8
+
9
+ from .layers import NeuronVJEPA2Encoder
10
+ from .nki_kernels import (
11
+ attention_block,
12
+ build_rope_tables,
13
+ lane_permutation,
14
+ mlp,
15
+ pack_out_weights,
16
+ pack_tables,
17
+ pack_v_weights,
18
+ pack_weights,
19
+ res_layernorm_fold,
20
+ )
21
+
22
+ __all__ = [
23
+ "NeuronVJEPA2Encoder",
24
+ "res_layernorm_fold", "attention_block", "mlp",
25
+ "pack_weights", "pack_tables", "pack_v_weights", "pack_out_weights",
26
+ "lane_permutation", "build_rope_tables",
27
+ ]
build/torch-neuron/layers.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """``NeuronVJEPA2Encoder`` -- the KernelConfig-mappable layer for the V-JEPA2 encoder.
2
+
3
+ USAGE (HF Transformers ``KernelConfig``)::
4
+
5
+ from transformers import AutoModel, KernelConfig
6
+ kernel_config = KernelConfig({
7
+ "VJEPA2Encoder": "OWNER/vjepa2-neuron-kernels:NeuronVJEPA2Encoder", # <- set to the published repo id
8
+ })
9
+ model = AutoModel.from_pretrained("facebook/vjepa2-vitl-fpc64-256",
10
+ dtype=torch.float32,
11
+ kernel_config=kernel_config,
12
+ device_map="neuron")
13
+ model.encoder.forward = model.encoder.forward # (kernelize does the graft)
14
+
15
+ WHY THE ENCODER CLASS AND NOT A PER-LAYER MAPPING. The K1/K3 kernel fuses the *previous*
16
+ sublayer's residual add with the *next* sublayer's LayerNorm, so the fusion spans transformer-
17
+ layer boundaries: the state carried between layers is the pair (fp32 residual stream, bf16
18
+ branch partials), not a single summed hidden_states. Replacing the whole encoder forward keeps
19
+ that chain -- and the instruction stream it was verified bitwise against -- intact. A per-layer
20
+ mapping would force a residual re-add at every boundary and change the stream.
21
+
22
+ MECHANICS. The kernels library grafts this class's ``forward`` onto the existing
23
+ ``VJEPA2Encoder`` instance, so ``self`` inside ``forward`` is the *stock* encoder with its
24
+ stock parameters (checkpoint loading is untouched). On the first device forward, the per-layer
25
+ packed weights (LayerNorm affines folded into QKV/V and MLP-fc1, out-proj bias halved for the
26
+ LNC=2 partials, rope tables) are computed once from those stock parameters and cached on the
27
+ layer objects. Off-device or non-fp32 inputs fall back to the stock implementation.
28
+
29
+ ``can_torch_compile = True`` is REQUIRED here: the published performance (23.94 ms/forward,
30
+ 44% MFU) is of the ``torch.compile(backend="neuron")`` graph, and without this flag the kernels
31
+ library silently swaps the layer back to stock under Mode.TORCH_COMPILE (observed on other
32
+ Neuron kernel repos as a 25-127x silent regression).
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import torch
38
+
39
+ from .nki_kernels import (
40
+ attention_block,
41
+ build_rope_tables,
42
+ mlp,
43
+ pack_out_weights,
44
+ pack_tables,
45
+ pack_v_weights,
46
+ pack_weights,
47
+ res_layernorm_fold,
48
+ )
49
+
50
+ _PACKED_ATTR = "_vjepa2_nki_packed"
51
+ _TAB_ATTR = "_vjepa2_nki_tab"
52
+
53
+
54
+ def _pack_layer(lyr, tab):
55
+ """Fold one transformer block's LayerNorm affines into the kernel weight packings.
56
+
57
+ Exact fp32 fold on CPU (the algebra of the verified reference implementation):
58
+ K1 gamma1/beta1 -> QKV and V packs; out-proj bias HALVED (two cores' partials sum);
59
+ K3 gamma2/beta2 -> MLP fc1; fc2 unfolded.
60
+ """
61
+ attn = lyr.attention
62
+ dev = attn.query.weight.device
63
+ H = attn.all_head_size
64
+ D = attn.attention_head_size
65
+ heads = H // D
66
+
67
+ g1 = lyr.norm1.weight.detach().float().cpu()
68
+ t1 = lyr.norm1.bias.detach().float().cpu()
69
+ g2 = lyr.norm2.weight.detach().float().cpu()
70
+ t2 = lyr.norm2.bias.detach().float().cpu()
71
+ wq = attn.query.weight.detach().float().cpu(); bq = attn.query.bias.detach().float().cpu()
72
+ wk = attn.key.weight.detach().float().cpu(); bk = attn.key.bias.detach().float().cpu()
73
+ wv = attn.value.weight.detach().float().cpu(); bv = attn.value.bias.detach().float().cpu()
74
+
75
+ wqk_f, bqk_f = pack_weights(wq * g1[None, :], wk * g1[None, :],
76
+ bq + wq @ t1, bk + wk @ t1, heads, D)
77
+ wvt_f, bv_f = pack_v_weights(wv * g1[None, :], bv + wv @ t1)
78
+ wot, bo = pack_out_weights(attn.proj.weight, attn.proj.bias)
79
+
80
+ w1 = lyr.mlp.fc1.weight.detach().float().cpu(); b1 = lyr.mlp.fc1.bias.detach().float().cpu()
81
+ packed = {
82
+ "D": D, "heads": heads,
83
+ "k1_eps": float(lyr.norm1.eps), "k3_eps": float(lyr.norm2.eps),
84
+ "scale": float(attn.scaling),
85
+ "wqk_f": wqk_f.to(dev), "bqk_f": bqk_f.to(dev), "tab": tab,
86
+ "wvt_f": wvt_f.to(dev), "bv_f": bv_f.to(dev), "wot": wot.to(dev), "bo": bo.to(dev),
87
+ "w1t_f": (w1 * g2[None, :]).t().contiguous().to(torch.bfloat16).to(dev),
88
+ "b1_f": (b1 + w1 @ t2).contiguous().to(dev),
89
+ "w2t": lyr.mlp.fc2.weight.detach().t().contiguous().to(torch.bfloat16).to(dev),
90
+ "b2": lyr.mlp.fc2.bias.detach().float().contiguous().to(dev),
91
+ }
92
+ return packed
93
+
94
+
95
+ def _rope_tables(encoder, tokens):
96
+ attn0 = encoder.layer[0].attention
97
+ H = attn0.all_head_size
98
+ D = attn0.attention_head_size
99
+ dev = attn0.query.weight.device
100
+ pos = attn0.get_position_ids(torch.zeros(1, tokens, H, device=dev), masks=None)
101
+
102
+ def to1d(t):
103
+ return t if t.dim() == 1 else t.reshape(-1, tokens)[0]
104
+
105
+ cos, sinsigned = build_rope_tables(
106
+ (attn0.d_dim, attn0.h_dim, attn0.w_dim),
107
+ (to1d(pos[0]), to1d(pos[1]), to1d(pos[2])),
108
+ D, torch.float32, dev)
109
+ return pack_tables(cos, sinsigned, H // D, D)
110
+
111
+
112
+ @torch._dynamo.disable
113
+ def _freeze(encoder, tokens):
114
+ """Pack every layer once; cached on the layer objects. Refuses in training mode.
115
+
116
+ ``@torch._dynamo.disable`` is load-bearing: without it, a user who wraps the model in
117
+ ``torch.compile`` before the first forward gets the weight-fold arithmetic traced/executed
118
+ by the compiler instead of eagerly, and the packed weights round DIFFERENTLY (measured:
119
+ output norm 4487.45 vs the verified 4486.19). The fold must run eagerly, once.
120
+ """
121
+ if encoder.training:
122
+ raise RuntimeError(
123
+ "NeuronVJEPA2Encoder: encoder.training is True; these kernels have no autograd "
124
+ "rule. Call model.eval() first.")
125
+ tab = _rope_tables(encoder, tokens)
126
+ setattr(encoder, _TAB_ATTR, tab)
127
+ for lyr in encoder.layer:
128
+ setattr(lyr, _PACKED_ATTR, _pack_layer(lyr, tab))
129
+
130
+
131
+ class NeuronVJEPA2Encoder(torch.nn.Module):
132
+ """Drop-in forward for ``VJEPA2Encoder``: the 4-kernel NKI layer loop.
133
+
134
+ Supported: trn2, 16 frames / 2048 tokens / batch 1, fp32 model. Anything else falls back
135
+ to the stock implementation (off-device, wrong geometry) or raises (training mode during
136
+ the first-freeze).
137
+ """
138
+
139
+ can_torch_compile = True
140
+ has_backward = False
141
+
142
+ def forward(self, pixel_values_videos=None, **kwargs):
143
+ hs = self.embeddings(pixel_values_videos)
144
+ N = hs.shape[1]
145
+ # Fallback: off-device, or an unsupported geometry (only N=2048 is verified; the vendor
146
+ # attention kernel cannot hold 8192 keys in SBUF -- see the card's Known limitations).
147
+ if hs.device.type != "neuron" or N != 2048:
148
+ import transformers.models.vjepa2.modeling_vjepa2 as mv
149
+ return mv.VJEPA2Encoder.forward(self, pixel_values_videos, **kwargs)
150
+
151
+ if not hasattr(self.layer[0], _PACKED_ATTR):
152
+ _freeze(self, N)
153
+
154
+ x = hs[0] # [N, H] fp32 residual stream
155
+ H = x.shape[1]
156
+ branch = torch.zeros(1, N, H, dtype=torch.bfloat16, device=x.device)
157
+
158
+ for lyr in self.layer:
159
+ p = getattr(lyr, _PACKED_ATTR)
160
+ x, z1 = res_layernorm_fold(x, branch, p["k1_eps"])
161
+ attn_partials = attention_block(
162
+ z1, p["wqk_f"], p["bqk_f"], p["tab"], p["wvt_f"], p["bv_f"],
163
+ p["wot"], p["bo"], p["scale"], p["D"])
164
+ x, z2 = res_layernorm_fold(x, attn_partials, p["k3_eps"])
165
+ branch = mlp(z2, p["w1t_f"], p["b1_f"], p["w2t"], p["b2"]).unsqueeze(0)
166
+
167
+ x = x + branch[0].to(torch.float32)
168
+ out = self.layernorm(x.unsqueeze(0))
169
+ import transformers.models.vjepa2.modeling_vjepa2 as mv
170
+ return mv.BaseModelOutput(last_hidden_state=out)
171
+
172
+
build/torch-neuron/metadata.json ADDED
@@ -0,0 +1 @@
 
 
1
+ { "name": "vjepa2-neuron-kernels", "id": "vjepa2_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,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The four NKI kernels of the V-JEPA2 encoder plus 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 .mlp import mlp
12
+ from .res_layernorm import res_layernorm_fold
13
+ from .rope_tables import build_rope_tables
14
+
15
+ __all__ = [
16
+ "res_layernorm_fold", "attention_block", "mlp",
17
+ "pack_weights", "pack_tables", "pack_v_weights", "pack_out_weights",
18
+ "lane_permutation", "build_rope_tables",
19
+ ]
build/torch-neuron/nki_kernels/attention_block.py ADDED
@@ -0,0 +1,709 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ any project-local 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 reference version (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, ``perf/SBUF_8192_STOPPED.md``), 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 ``perf/SBUF_8192_STOPPED2.md``). 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: SBUF_8192_STOPPED.md).
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
+ LNC = 2
124
+ _P_MAX = 128
125
+ _PSUM_FMAX = 512
126
+ _PSUM_BANK = 2048 # bytes per partition per bank
127
+ _K_TILE = 512 # vendor k_cache_sbuf tiles are (d, 512)
128
+ _V_TILE = 128 # vendor v_cache_sbuf tiles are (128, d)
129
+
130
+
131
+ def _kernel_assert(cond: bool, msg: str):
132
+ assert cond, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {msg}"
133
+
134
+
135
+ # ==========================================================================================
136
+ # Host-side packing helpers (copied flat from nki_qk_fused_2core / nki_attn_block_k2v_dp4).
137
+ # All are frozen-weight work, cached once; INFERENCE ONLY.
138
+ # ==========================================================================================
139
+ def lane_permutation(heads: int, D: int) -> torch.Tensor:
140
+ """Column order that puts each quad-of-heads' evens and odds in their own 128 group.
141
+
142
+ FOLD ALGEBRA: RoPE pairs adjacent lanes ``out[2j]=x[2j]*cos+x[2j+1]*sinsigned`` etc. A
143
+ projection produces lanes on the partition axis, so a pair is a stride-2 partition access.
144
+ Reorder the projection's output columns so group ``2a`` holds the EVEN lanes of heads
145
+ ``4a..4a+3`` and group ``2a+1`` their ODD lanes (``quad*half = 4*(D//2) = 128`` rows when
146
+ D==64). Then one matmul yields a full [128,512] all-evens tile and another all-odds, and
147
+ RoPE is six full-width ``tensor_tensor`` ops -- no transposes, no partition waste. The
148
+ permutation cancels in attention's Q/K contraction, so nothing downstream un-permutes.
149
+
150
+ Returns ``perm`` of length ``heads*D`` s.t. ``w.t()[:, perm]`` is the reordered weight:
151
+ ``perm[p]`` is the natural column that lands at permuted row ``p % 128`` of group ``p // 128``.
152
+ """
153
+ half = D // 2
154
+ quad = _P_MAX // half
155
+ _kernel_assert(quad * half == _P_MAX, f"D={D} gives half={half}, needs quad*half=128")
156
+ _kernel_assert(heads % quad == 0, f"heads={heads} must be divisible by quad={quad}")
157
+ perm = torch.empty(heads * D, dtype=torch.long)
158
+ for hd in range(heads):
159
+ a, b = hd // quad, hd % quad
160
+ for j in range(half):
161
+ perm[(2 * a) * _P_MAX + b * half + j] = hd * D + 2 * j # even lane
162
+ perm[(2 * a + 1) * _P_MAX + b * half + j] = hd * D + 2 * j + 1 # odd lane
163
+ return perm
164
+
165
+
166
+ def pack_weights(wq: torch.Tensor, wk: torch.Tensor,
167
+ bq: torch.Tensor | None, bk: torch.Tensor | None,
168
+ heads: int, D: int):
169
+ """``(w_packed [2,H,H] bf16, b_packed [2,H,1] bf16)`` -- transposed and column-permuted.
170
+
171
+ FOLD: in the affine-free-LN1 freeze this is called on gamma-folded inputs
172
+ ``pack_weights(wq*g1, wk*g1, bq+wq@t1, bk+wk@t1, heads, D)``. ``wq``/``wk`` are torch
173
+ ``Linear.weight`` ``[H_out, H_in]``; the kernel wants ``[H_in, H_out]`` with ``H_out``
174
+ permuted, i.e. ``w.t()[:, perm]``. The bias is shaped ``[2,H,1]`` (not ``[2,H]``) so the
175
+ kernel can slice a ``[128,1]`` tile straight out of HBM.
176
+ """
177
+ H = wq.shape[0]
178
+ perm = lane_permutation(heads, D).to(wq.device)
179
+ w = torch.stack([wq.detach().t()[:, perm].contiguous(),
180
+ wk.detach().t()[:, perm].contiguous()], 0)
181
+ zero = torch.zeros(H, dtype=wq.dtype, device=wq.device)
182
+ b = torch.stack([(zero if bq is None else bq.detach())[perm].contiguous(),
183
+ (zero if bk is None else bk.detach())[perm].contiguous()], 0)
184
+ b = b.unsqueeze(-1)
185
+ return w.to(torch.bfloat16).contiguous(), b.to(torch.bfloat16).contiguous()
186
+
187
+
188
+ def pack_tables(cos: torch.Tensor, sinsigned: torch.Tensor, heads: int, D: int):
189
+ """``tab [4, 128, N] fp32`` = (cos_even, cos_odd, sin_even, sin_odd), quad-tiled.
190
+
191
+ FOLD ALGEBRA: ``cos``/``sinsigned`` are ``[N, D]`` shared by every head. A group's 128 rows
192
+ are one head's 32 even (or odd) lanes tiled ``quad`` (=4) times to match the lane_permutation
193
+ layout, so each RoPE operand is a full 128-partition tile with a matching partition base.
194
+ Built once via the rope-table build and shared across all layers.
195
+ """
196
+ half = D // 2
197
+ quad = _P_MAX // half
198
+ parts = [cos[:, 0::2], cos[:, 1::2], sinsigned[:, 0::2], sinsigned[:, 1::2]]
199
+ tab = torch.stack([p.t().contiguous().repeat(quad, 1) for p in parts], 0)
200
+ return tab.to(torch.float32).contiguous()
201
+
202
+
203
+ def pack_v_weights(wv: torch.Tensor, bv: torch.Tensor | None):
204
+ """``(wv_t [H, H] bf16, bv [1, H] bf16)``. V is NOT lane-permuted (natural order).
205
+
206
+ FOLD: in the affine-free-LN1 freeze this is called on gamma-folded inputs
207
+ ``pack_v_weights(wv*g1, bv+wv@t1)``.
208
+ """
209
+ H = wv.shape[0]
210
+ zero = torch.zeros(H, dtype=wv.dtype, device=wv.device)
211
+ return (wv.detach().t().contiguous().to(torch.bfloat16),
212
+ (bv.detach() if bv is not None else zero).reshape(1, H).contiguous().to(torch.bfloat16))
213
+
214
+
215
+ def pack_out_weights(wo, bo):
216
+ """``(wo_t [H, H] bf16, bo/2 [1, H] bf16)`` for the fused out projection.
217
+
218
+ ``wo`` is ``attn.proj.weight`` [H, H] (attention features -> hidden). The kernel contracts
219
+ over attention features, so it wants those on partitions: ``wo.T`` = [features, hidden].
220
+
221
+ LNC2 BIAS HALVING. Both cores add ``bo/2`` and K3's partial sum restores ``bo``. Adding the
222
+ full bias on both would double it; adding it on one core only would need ``program_id`` as an
223
+ arithmetic operand (it is symbolic -- neither a compile-time constant nor a [P,1] tile).
224
+ Halving is bitwise exact in bf16: it only decrements the exponent, and summing two identical
225
+ bf16 values is exact.
226
+ """
227
+ import torch as _t
228
+ Hh = wo.shape[0]
229
+ zero = _t.zeros(Hh, dtype=wo.dtype, device=wo.device)
230
+ half = (bo.detach() if bo is not None else zero) * 0.5
231
+ return (wo.detach().t().contiguous().to(_t.bfloat16),
232
+ half.reshape(1, Hh).contiguous().to(_t.bfloat16))
233
+
234
+
235
+ # ==========================================================================================
236
+ # THE KERNEL. Flattened from _attn_block_k2v_h10_dp_kernel (verbatim dp4 body with the two
237
+ # engine= keywords hardcoded to engine.scalar at :276 and :458; :224 and :483 left unpinned).
238
+ # ==========================================================================================
239
+ @nki.jit
240
+ def _attention_block_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_hbm,
241
+ wot_hbm, bo_hbm, scale, head_dim):
242
+ """out[16, N, D] bf16 = attention(rope(h@Wq), rope(h@Wk), h@Wv), one kernel (per-core partials)."""
243
+ N, H = h_hbm.shape
244
+ D = head_dim
245
+ half = D // 2
246
+ quad = _P_MAX // half # heads per 128-partition parity group = 4
247
+ heads = H // D # 16
248
+ local_heads = heads // LNC # 8
249
+ local_quads = local_heads // quad # 2
250
+ n_chunks = H // _P_MAX # 8 contraction chunks
251
+ n_tt = N // _PSUM_FMAX # 4 token tiles of 512
252
+ n_ktiles = N // _K_TILE # 4 K tiles / head
253
+ n_vtiles = N // _V_TILE # 16 V tiles / head
254
+ n_ochunks = (local_heads * D) // _P_MAX # feature chunks of the out projection
255
+ v_per_tt = _PSUM_FMAX // _V_TILE # 4 V tiles per 512-token tile
256
+
257
+ # N>2048 gate (see the phase-1/phase-2 note below). Computed early so the K/V residents can
258
+ # be allocated as HBM (streamed) for the big path.
259
+ _big = N > _PSUM_FMAX * 4
260
+
261
+ # N>2048 IS NOT SUPPORTED (guarded at the host wrapper `attention_block` below, which raises
262
+ # NotImplementedError before this kernel is ever traced -- NKI kernels cannot `raise`). The
263
+ # phase-1 memory fixes (x_t/tab depth-4 rings, K/V streamed through private_hbm one head at a
264
+ # time) DO fit -- phase-1 peak ~106 KiB/lane. But phase 2 does NOT: handing one head all 8192
265
+ # keys makes the flattened single-section _attention_cte run with num_large_tiles_per_section
266
+ # =ceil(8192/2048)=4, which balloons three N-scaled inner buffers -- exp_tp_sb (8*4*4*512*2 =
267
+ # 131072), mm1_masked (2*4*2048*4 = 65536), exp_sb (2*4*2048*2 = 32768) = 229376 B/lane for
268
+ # three buffers alone. Measured on device (2026-09-25, neuronx-cc 2.25): [NCC_INLA001] out of
269
+ # bound 128x354912 -> phase-2 peak 354912 B/lane, 1.81x the 196608 budget. The shrinkable
270
+ # phase-2 region (o_feat 65536 + out 1024 + wo/bo 12288 = 78848) is smaller than the 158304
271
+ # overflow, so streaming O or shrinking o_feat CANNOT close the gap -- the inner's N-scaled
272
+ # buffers are the wall. Re-enabling flash sectioning (2048-key sections) shrinks the inner but
273
+ # activates the vendor's multi-section online-softmax combine, a path the shipping config never
274
+ # exercises and which produced rel_l2 0.90 (wrong). See perf/SBUF_8192_STOPPED2.md.
275
+
276
+ _kernel_assert(wqk_hbm.shape == (2, H, H), f"wqk must be [2,{H},{H}]")
277
+ _kernel_assert(bqk_hbm.shape == (2, H, 1), f"bqk must be [2,{H},1]")
278
+ _kernel_assert(tab_hbm.shape == (4, _P_MAX, N), f"tab must be [4,{_P_MAX},{N}]")
279
+ _kernel_assert(wvt_hbm.shape == (H, H) and bv_hbm.shape == (1, H), "bad V weights")
280
+ _kernel_assert(quad * half == _P_MAX, f"D={D} must give 4 heads per group")
281
+ _kernel_assert(heads % (quad * LNC) == 0, "heads must split into per-core quads")
282
+ _kernel_assert(local_heads * D == _PSUM_FMAX,
283
+ "inline V projection wants all local heads in one 512-wide stream")
284
+ _kernel_assert(N % _PSUM_FMAX == 0, f"N={N} must tile by 512")
285
+ _kernel_assert(str(h_hbm.dtype) == str(nl.bfloat16), "h must be bf16")
286
+
287
+ # [LNC, N, H] PARTIALS. The out projection contracts over all H features == all 16 heads,
288
+ # and this core owns 8, so what it computes is a partial sum; K3 adds the two.
289
+ result = nl.ndarray((LNC, N, H), dtype=nl.bfloat16, buffer=nl.shared_hbm)
290
+
291
+ pid = nl.program_id(0) # SYMBOLIC: index arithmetic only
292
+ head0 = pid * local_heads
293
+ col0 = pid * (local_heads * D) # first permuted qk weight column
294
+ vcol0 = pid * (local_heads * D) # first natural v weight column (same offset)
295
+
296
+ allocator = ModularAllocator(initial_address=0)
297
+
298
+ # ---- persistent consumer tiles (written by producer, read by attention) ---------
299
+ # N<=2048: all 8 heads' K/V resident in SBUF (bit-for-bit shipping path). N>2048: K/V for
300
+ # all 8 heads (196608 B/lane) is the ENTIRE 192 KiB budget, so they cannot stay resident
301
+ # alongside x_t/tab/working set. STREAM them: the producer scatters each head's K/V to
302
+ # private_hbm, and phase-2 loads ONE head's K/V into small resident staging rings just
303
+ # before its attention call (attention runs per head and writes O to HBM, so only one
304
+ # head's K/V need be live at a time).
305
+ k_tiles = []
306
+ v_tiles = []
307
+ k_hbm = []
308
+ v_hbm = []
309
+ if _big:
310
+ for _lh in range(local_heads):
311
+ k_hbm.append(nl.ndarray((n_ktiles, D, _K_TILE), dtype=nl.bfloat16,
312
+ buffer=nl.private_hbm))
313
+ v_hbm.append(nl.ndarray((n_vtiles, _V_TILE, D), dtype=nl.bfloat16,
314
+ buffer=nl.private_hbm))
315
+ # ONE head's resident staging rings (reused across heads in phase 2).
316
+ k_stage = allocator.alloc_sbuf_tensor(
317
+ shape=(D, _K_TILE), dtype=nl.bfloat16,
318
+ block_dim=[n_ktiles], num_free_tiles=[n_ktiles], align_to=32)
319
+ v_stage = allocator.alloc_sbuf_tensor(
320
+ shape=(_V_TILE, D), dtype=nl.bfloat16,
321
+ block_dim=[n_vtiles], num_free_tiles=[n_vtiles])
322
+ else:
323
+ for _lh in range(local_heads):
324
+ kt_ = allocator.alloc_sbuf_tensor(
325
+ shape=(D, _K_TILE), dtype=nl.bfloat16,
326
+ block_dim=[n_ktiles], num_free_tiles=[n_ktiles], align_to=32)
327
+ k_tiles.append(kt_)
328
+ vt_ = allocator.alloc_sbuf_tensor(
329
+ shape=(_V_TILE, D), dtype=nl.bfloat16,
330
+ block_dim=[n_vtiles], num_free_tiles=[n_vtiles])
331
+ v_tiles.append(vt_)
332
+ # HIGH WATER MARK for phase 2: k_tiles/v_tiles are the only phase-1 products attention
333
+ # needs, so every buffer above this address is dead once the producer loop finishes.
334
+ phase2_base = allocator.get_current_address()
335
+
336
+ # ------------------------------------------------------------------------------------
337
+ # N>2048 GATE (64-frame clips, N=8192). At N<=2048 every ring below keeps its full-N
338
+ # depth, so the buffer shape, byte layout, DMA target addresses and fill VALUES are
339
+ # bit-for-bit identical to the shipping 16-frame path (see the SBUF budget table in the
340
+ # module docstring), and the attention call goes to the pinned single-section flatten
341
+ # ``_attention_cte`` (all engine pins intact). At N>2048 two independent overflows appear:
342
+ # (1) PHASE 1: the four N-scaled residents (k_tiles+v_tiles+x_t+tab = 458752 B/lane)
343
+ # coexist. FIX: x_t and tab become depth-XT_RING token-block rings (a rolling 4-tile
344
+ # window filled inside the per-token-tile loop; a pure copy, identical values).
345
+ # (2) PHASE 2: eight heads' K/V resident (196608 B/lane) is the whole budget. FIX: stream
346
+ # K/V through private_hbm and stage ONE head at a time into k_stage/v_stage. The
347
+ # attention call is the SAME single-section flatten ``_attention_cte`` the N<=2048 path
348
+ # uses (seqlen_k=8192 < 10240 threshold => num_sections==1, no multi-section flash
349
+ # combine); its large-tile loop iterates 4x over _LARGE_TILE_SZ-sized buffers.
350
+ # The N<=2048 path enters NEITHER branch and is bit-for-bit unchanged.
351
+ _big = N > _PSUM_FMAX * 4 # N>2048 -> the 64-frame geometry
352
+ XT_RING = min(n_tt, 4) if _big else n_tt # token-block ring depth for x_t / tab
353
+
354
+ # ---- resident producer inputs ----------------------------------------------------
355
+ # x^T: hidden on partitions, tokens on the free axis, as a per-512-token-block ring.
356
+ # At N<=2048 XT_RING==n_tt so the ring holds every block (== the old (128,N) buffer,
357
+ # same bytes, same addresses); at N=8192 it holds a rolling window of 4 blocks. The fill
358
+ # is a pure dma_transpose (no arithmetic), so any fill ORDER yields identical values.
359
+ x_t = allocator.alloc_sbuf_tensor(
360
+ shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
361
+ block_dim=[n_chunks, n_tt], num_free_tiles=[n_chunks, XT_RING], align_to=32)
362
+
363
+ if not _big:
364
+ # N<=2048: fill the whole buffer up front, exactly as the shipping path did (pure
365
+ # dma_transpose; block-indexed AP lands at the same addresses as the old (128,N) buffer).
366
+ for s in range(n_tt):
367
+ for c in range(n_chunks):
368
+ nisa.dma_transpose(
369
+ dst=x_t[c][s].ap([[_PSUM_FMAX, _P_MAX], [1, 1], [1, 1], [1, _PSUM_FMAX]],
370
+ offset=0),
371
+ src=h_hbm.ap([[H, _PSUM_FMAX], [1, 1], [1, 1], [1, _P_MAX]],
372
+ offset=(s * _PSUM_FMAX) * H + c * _P_MAX),
373
+ )
374
+
375
+ # this core's Q/K weight columns (lane-permuted) and V columns (natural)
376
+ w_sb = allocator.alloc_sbuf_tensor(
377
+ shape=(_P_MAX, local_quads * 2 * _P_MAX), dtype=nl.bfloat16,
378
+ block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
379
+ for qk in range(2):
380
+ for c in range(n_chunks):
381
+ nisa.dma_copy(dst=w_sb[qk][c],
382
+ src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX,
383
+ col0:col0 + local_quads * 2 * _P_MAX])
384
+ wv_sb = allocator.alloc_sbuf_tensor(
385
+ shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
386
+ block_dim=[n_chunks], num_free_tiles=[n_chunks])
387
+ for c in range(n_chunks):
388
+ nisa.dma_copy(dst=wv_sb[c],
389
+ src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX,
390
+ vcol0:vcol0 + local_heads * D])
391
+
392
+ # biases: qk as (128,1) per group; v broadcast to (128, 8*64) once
393
+ b_sb = allocator.alloc_sbuf_tensor(
394
+ shape=(_P_MAX, 1), dtype=nl.float32,
395
+ block_dim=[2, local_quads * 2], num_free_tiles=[2, local_quads * 2], align_to=4)
396
+ b_stage = allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
397
+ for qk in range(2):
398
+ for g in range(local_quads * 2):
399
+ nisa.dma_copy(dst=b_stage,
400
+ src=bqk_hbm[qk, col0 + g * _P_MAX:col0 + (g + 1) * _P_MAX, 0:1])
401
+ # SITE :224 -- bias CAST, left UNPINNED on purpose (0.09% of the window, already at
402
+ # the Vector fixed fee; nothing to move).
403
+ nisa.tensor_copy(dst=b_sb[qk][g], src=b_stage)
404
+ bv_bc = allocator.alloc_sbuf_tensor(
405
+ shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
406
+ nisa.dma_copy(dst=bv_bc,
407
+ src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]],
408
+ offset=vcol0))
409
+
410
+ # rope tables, resident (quad-tiled on the host: identical rows for every quad). Token-block
411
+ # ring, same gating as x_t: at N<=2048 TAB_RING==n_tt holds every block (== old (128,N)
412
+ # buffer, bit-for-bit); at N=8192 a rolling 4-block window filled inside the tt loop. Pure
413
+ # dma_copy -> identical values regardless of fill order.
414
+ tab = allocator.alloc_sbuf_tensor(
415
+ shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
416
+ block_dim=[4, n_tt], num_free_tiles=[4, XT_RING], align_to=4)
417
+
418
+ if not _big:
419
+ for s in range(n_tt):
420
+ for t_i in range(4):
421
+ nisa.dma_copy(dst=tab[t_i][s],
422
+ src=tab_hbm[t_i, 0:_P_MAX,
423
+ s * _PSUM_FMAX:(s + 1) * _PSUM_FMAX])
424
+
425
+ # ---- per-tile working set (reused across iterations; same tensor objects) --------
426
+ proj = allocator.alloc_sbuf_tensor(
427
+ shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16, block_dim=[2],
428
+ num_free_tiles=[2])
429
+ acc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
430
+ align_to=4)
431
+ tmp = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
432
+ align_to=4)
433
+ roped = allocator.alloc_sbuf_tensor(
434
+ shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16, block_dim=[2],
435
+ num_free_tiles=[2])
436
+ vout = allocator.alloc_sbuf_tensor(
437
+ shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
438
+
439
+ # Q staging via private_hbm, one per LOCAL head (vendor segmented-kernel pattern)
440
+ q_priv = []
441
+ for _lh in range(local_heads):
442
+ q_priv.append(nl.ndarray((1, D, N), dtype=nl.bfloat16, buffer=nl.private_hbm))
443
+
444
+ # ---- producer: per 512-token tile, straight through the pipeline ----------------
445
+ for tt in range(n_tt):
446
+ t0 = tt * _PSUM_FMAX
447
+
448
+ # N>2048: fill this token-block's x_t / tab slice NOW (rolling ring window). At
449
+ # N<=2048 the whole buffer was already filled up front (bit-for-bit), so skip.
450
+ if _big:
451
+ for c in range(n_chunks):
452
+ nisa.dma_transpose(
453
+ dst=x_t[c][tt].ap([[_PSUM_FMAX, _P_MAX], [1, 1], [1, 1], [1, _PSUM_FMAX]],
454
+ offset=0),
455
+ src=h_hbm.ap([[H, _PSUM_FMAX], [1, 1], [1, 1], [1, _P_MAX]],
456
+ offset=t0 * H + c * _P_MAX),
457
+ )
458
+ for t_i in range(4):
459
+ nisa.dma_copy(dst=tab[t_i][tt],
460
+ src=tab_hbm[t_i, 0:_P_MAX, t0:t0 + _PSUM_FMAX])
461
+
462
+ # V for all 8 local heads of this token tile.
463
+ for vsub in range(v_per_tt):
464
+ ps_v = nl.ndarray((_V_TILE, local_heads * D), dtype=nl.float32,
465
+ buffer=nl.psum, address=(0, 7 * _PSUM_BANK))
466
+ for c in range(n_chunks):
467
+ nisa.nc_matmul(
468
+ dst=ps_v,
469
+ stationary=x_t[c][tt][:, vsub * _V_TILE:(vsub + 1) * _V_TILE],
470
+ moving=wv_sb[c],
471
+ accumulate=(c > 0),
472
+ )
473
+ nisa.tensor_tensor(dst=vout, data1=ps_v, data2=bv_bc, op=nl.add)
474
+ for lh in range(local_heads):
475
+ if _big:
476
+ # N>2048: stream V to HBM (SBUF->HBM dma from vout directly). The pinned
477
+ # :276 tensor_copy is the N<=2048 shipping site and is left untouched below.
478
+ nisa.dma_copy(
479
+ dst=v_hbm[lh][tt * v_per_tt + vsub],
480
+ src=vout[:, lh * D:(lh + 1) * D])
481
+ continue
482
+ # ======== SITE :276 -- pinned to Scalar. WAR chain of 8 copies read one vout
483
+ # before the next vsub overwrites it; Scalar (240 ns) beats GpSimd (375 ns) and
484
+ # sheds the copy off Vector, the binding engine. ========
485
+ nisa.tensor_copy(
486
+ dst=v_tiles[lh][tt * v_per_tt + vsub],
487
+ src=vout[:, lh * D:(lh + 1) * D],
488
+ engine=engine.scalar)
489
+
490
+ # Q and K for both local quads, both parities, this token tile.
491
+ for qk in range(2):
492
+ for j in range(local_quads):
493
+ for par in range(2):
494
+ g = j * 2 + par
495
+ cols = g * _P_MAX
496
+ ps = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32,
497
+ buffer=nl.psum,
498
+ address=(0, (5 + par) * _PSUM_BANK))
499
+ for c in range(n_chunks):
500
+ nisa.nc_matmul(
501
+ dst=ps,
502
+ stationary=w_sb[qk][c][:, cols:cols + _P_MAX],
503
+ moving=x_t[c][tt][:, 0:_PSUM_FMAX],
504
+ accumulate=(c > 0),
505
+ )
506
+ # ======== SITE :458 -- pinned to Scalar. Largest remaining Vector-resident
507
+ # relocatable site (768/core); data=ps is PSUM so GpSimd is illegal, and
508
+ # Scalar's 612 ns fee is faster than the 744 ns Vector cell, so the
509
+ # zero-lead rope chain consuming proj cannot lengthen. ========
510
+ nisa.tensor_scalar(dst=proj[par], data=ps,
511
+ op0=nl.add, operand0=b_sb[qk][g],
512
+ engine=engine.scalar)
513
+
514
+ # rope: the six ops of nki_rope, same order, tables sliced per tile
515
+ nisa.tensor_tensor(dst=acc, data1=proj[0],
516
+ data2=tab[0][tt][:, 0:_PSUM_FMAX], op=nl.multiply)
517
+ nisa.tensor_tensor(dst=tmp, data1=proj[1],
518
+ data2=tab[2][tt][:, 0:_PSUM_FMAX], op=nl.multiply)
519
+ nisa.tensor_tensor(dst=roped[0], data1=acc, data2=tmp, op=nl.add)
520
+ nisa.tensor_tensor(dst=acc, data1=proj[1],
521
+ data2=tab[1][tt][:, 0:_PSUM_FMAX], op=nl.multiply)
522
+ nisa.tensor_tensor(dst=tmp, data1=proj[0],
523
+ data2=tab[3][tt][:, 0:_PSUM_FMAX], op=nl.multiply)
524
+ nisa.tensor_tensor(dst=roped[1], data1=acc, data2=tmp, op=nl.add)
525
+
526
+ # scatter: head b of the quad owns rows [b*32,(b+1)*32)
527
+ for b in range(quad):
528
+ lh = j * quad + b
529
+ r0 = b * half
530
+ if qk == 1:
531
+ for par2 in range(2):
532
+ if _big:
533
+ # N>2048: stream K to HBM (SBUF->HBM dma). The :483 tensor_copy
534
+ # below is the N<=2048 shipping site, left untouched.
535
+ nisa.dma_copy(
536
+ dst=k_hbm[lh][tt][par2 * half:(par2 + 1) * half,
537
+ 0:_K_TILE],
538
+ src=roped[par2][r0:r0 + half, 0:_PSUM_FMAX])
539
+ continue
540
+ # SITE :483 -- K-scatter COPY, left UNPINNED (no affordable landing
541
+ # engine: it is a WAR chain against the next iteration's rope write).
542
+ nisa.tensor_copy(
543
+ dst=k_tiles[lh][tt][par2 * half:(par2 + 1) * half,
544
+ 0:_K_TILE],
545
+ src=roped[par2][r0:r0 + half, 0:_PSUM_FMAX])
546
+ else:
547
+ for par2 in range(2):
548
+ nisa.dma_copy(
549
+ dst=q_priv[lh][0, par2 * half:(par2 + 1) * half,
550
+ t0:t0 + _PSUM_FMAX],
551
+ src=roped[par2][r0:r0 + half, 0:_PSUM_FMAX])
552
+
553
+ # ---- PHASE 2: reclaim the dead producer buffers. NO ALIASING. -------------------
554
+ allocator.set_current_address(phase2_base)
555
+
556
+ # ---- out-projection weights: THIS core's feature rows, all H output columns -------
557
+ wo_sb = allocator.alloc_sbuf_tensor(
558
+ shape=(_P_MAX, H), dtype=nl.bfloat16,
559
+ block_dim=[n_ochunks], num_free_tiles=[n_ochunks])
560
+ for oc in range(n_ochunks):
561
+ nisa.dma_copy(dst=wo_sb[oc],
562
+ src=wot_hbm[col0 + oc * _P_MAX:col0 + (oc + 1) * _P_MAX, 0:H])
563
+ # bias already HALVED on the host (pack_out_weights) so the two cores' partials sum to
564
+ # exactly one bias. Broadcast across partitions (output columns are on the free axis).
565
+ bo_bc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
566
+ nisa.dma_copy(dst=bo_bc,
567
+ src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
568
+
569
+ # ---- O STAYS IN SBUF, ALLOCATED EXACTLY ONCE. tp_out=True gives [D, 128 tokens] per Q
570
+ # group (feature-major), which the projection contracts over, so O never transposes.
571
+ # The list handed to the vendor is SLICES into o_feat, so _write_back_o_impl copies
572
+ # straight into final position. ------------------------------------------------------
573
+ n_grp = N // _P_MAX
574
+ o_feat = allocator.alloc_sbuf_tensor(
575
+ shape=(_P_MAX, N), dtype=nl.bfloat16,
576
+ block_dim=[n_ochunks], num_free_tiles=[n_ochunks], align_to=32)
577
+ o_sb = []
578
+ for _lh in range(local_heads):
579
+ oc = (_lh * D) // _P_MAX
580
+ rr = (_lh * D) % _P_MAX
581
+ # explicit loop, NOT a comprehension: the NKI tracer rejects comprehensions in a kernel.
582
+ grp = []
583
+ for _g in range(n_grp):
584
+ grp.append(o_feat[oc][rr:rr + D, _g * _P_MAX:(_g + 1) * _P_MAX])
585
+ o_sb.append(grp)
586
+
587
+ attn_base = allocator.get_current_address()
588
+ for lh in range(local_heads):
589
+ if _big:
590
+ # N>2048: load THIS head's K/V from HBM into the resident staging rings (only one
591
+ # head live at a time -> K/V cost is one head's 24576 B/lane, not eight heads'
592
+ # 196608). Then run the SAME single-section flatten the N<=2048 path uses: seqlen_k
593
+ # =8192 < 10240 threshold => num_sections==1, no multi-section flash combine. The
594
+ # inner's large-tile loop iterates 4x over _LARGE_TILE_SZ-sized buffers; O is written
595
+ # straight into this head's o_feat SBUF slices (o_sb[lh]), exactly as N<=2048.
596
+ for _i in range(n_ktiles):
597
+ nisa.dma_copy(dst=k_stage[_i], src=k_hbm[lh][_i])
598
+ for _i in range(n_vtiles):
599
+ nisa.dma_copy(dst=v_stage[_i], src=v_hbm[lh][_i])
600
+ # 1D python-list views [seq_tile] (the flatten indexes k_sb[k_tile_idx_in_section]
601
+ # and v_sb[v_tile_idx] as flat lists -- same convention as k_tiles[lh]/v_tiles[lh]).
602
+ k1d = []
603
+ for _i in range(n_ktiles):
604
+ k1d.append(k_stage[_i])
605
+ v1d = []
606
+ for _i in range(n_vtiles):
607
+ v1d.append(v_stage[_i])
608
+ _attention_cte(
609
+ q_priv[lh], None, None,
610
+ scale=scale, causal_mask=False,
611
+ tp_q=False, tp_k=False, tp_out=True,
612
+ cache_softmax=False,
613
+ k_cache_sbuf=k1d,
614
+ v_cache_sbuf=v1d,
615
+ out_o_hbm=o_sb[lh],
616
+ init_sbuf_addr=attn_base,
617
+ )
618
+ else:
619
+ # N<=2048: the pinned single-section flatten (h8/h10 engine pins intact, bit-for-bit).
620
+ _attention_cte(
621
+ q_priv[lh], None, None,
622
+ scale=scale, causal_mask=False,
623
+ tp_q=False, tp_k=False, tp_out=True,
624
+ cache_softmax=False,
625
+ k_cache_sbuf=k_tiles[lh],
626
+ v_cache_sbuf=v_tiles[lh],
627
+ out_o_hbm=o_sb[lh],
628
+ init_sbuf_addr=attn_base,
629
+ )
630
+ allocator.set_current_address(attn_base)
631
+
632
+ # ---- PARTIAL out projection: tokens on partitions, output columns on the free axis
633
+ # (chosen by the HBM write: result is token-major, so a row of consecutive output columns
634
+ # is a 1024-byte contiguous run). 128 calls/layer/core (16 token blocks x 2 col blocks x
635
+ # 4 chunks). --------------------------------------------------------------------------
636
+ out_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX),
637
+ dtype=nl.bfloat16, align_to=32)
638
+ for tb in range(N // _P_MAX):
639
+ for ob in range(H // _PSUM_FMAX):
640
+ ps_out = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32, buffer=nl.psum,
641
+ address=(0, 0))
642
+ for oc in range(n_ochunks):
643
+ nisa.nc_matmul(
644
+ dst=ps_out,
645
+ stationary=o_feat[oc][:, tb * _P_MAX:(tb + 1) * _P_MAX],
646
+ moving=wo_sb[oc][:, ob * _PSUM_FMAX:(ob + 1) * _PSUM_FMAX],
647
+ accumulate=(oc > 0),
648
+ )
649
+ # bias is per output column (free axis) -> tensor_tensor against the pre-broadcast
650
+ # tile, not tensor_scalar. Same single eviction pass.
651
+ nisa.tensor_tensor(dst=out_sb, data1=ps_out,
652
+ data2=bo_bc[:, ob * _PSUM_FMAX:(ob + 1) * _PSUM_FMAX],
653
+ op=nl.add)
654
+ # [128 tokens, 512 out cols] -> token-major HBM: contiguous along the free axis.
655
+ nisa.dma_copy(
656
+ dst=result.ap(pattern=[[H, _P_MAX], [1, _PSUM_FMAX]],
657
+ offset=pid * N * H + tb * _P_MAX * H + ob * _PSUM_FMAX),
658
+ src=out_sb)
659
+ return result
660
+
661
+
662
+ _ATTN_BLOCK = wrap_nki_to_torch_hop(_attention_block_kernel[LNC])
663
+
664
+
665
+ def _no_grad(*tensors) -> None:
666
+ for t in tensors:
667
+ if t is not None and t.requires_grad:
668
+ raise RuntimeError(
669
+ "attention_block registers no autograd rule and its packed weights are "
670
+ "detached caches. Use under torch.no_grad()/eval().")
671
+
672
+
673
+ @nki_op("vjepa2::attention_block", mutates_args={})
674
+ def attention_block(
675
+ h: torch.Tensor,
676
+ wqk: torch.Tensor,
677
+ bqk: torch.Tensor,
678
+ tab: torch.Tensor,
679
+ wvt: torch.Tensor,
680
+ bv: torch.Tensor,
681
+ wot: torch.Tensor,
682
+ bo: torch.Tensor,
683
+ scale: float,
684
+ head_dim: int,
685
+ ) -> torch.Tensor:
686
+ """Fused norm1'd-hidden -> QKV+rope -> attention -> fused out-projection PARTIALS.
687
+
688
+ Returns ``[LNC=2, N, H]`` bf16 per-core out-projection partials; K3's residual add sums the
689
+ two cores. The two Scalar engine pins (V-scatter :276, QK bias-evict :458) are hardcoded.
690
+ """
691
+ _no_grad(h, wqk, bqk, tab, wvt, bv, wot, bo)
692
+ # N>2048 (e.g. 64-frame clips) is NOT supported. The phase-1 memory fixes fit (~106 KiB/lane),
693
+ # but the single-section vendor inner overflows phase 2 (measured 354912 B/lane vs the 196608
694
+ # budget: exp_tp_sb/mm1_masked/exp_sb scale with num_large_tiles_per_section=ceil(N/2048)=4).
695
+ # The shrinkable phase-2 region (78848 B/lane) is smaller than the 158304 overflow, so
696
+ # streaming O / shrinking o_feat cannot close the gap; re-enabling flash sectioning fits but
697
+ # is numerically wrong (rel_l2 0.90). Refuse here rather than emit a non-fitting or wrong
698
+ # kernel. See perf/SBUF_8192_STOPPED2.md. NKI kernels cannot `raise`, so the guard lives here.
699
+ N = h.shape[0]
700
+ if N > 2048:
701
+ raise NotImplementedError(
702
+ f"attention_block N={N} (>2048, e.g. 64-frame clips) is not supported: the "
703
+ f"single-section vendor inner does not fit SBUF (phase-2 peak measured 354912 B/lane "
704
+ f"vs the 196608 budget). Streaming O or shrinking o_feat cannot close the 158304 "
705
+ f"B/lane gap, and flash sectioning is numerically wrong (rel_l2 0.90). See "
706
+ f"perf/SBUF_8192_STOPPED2.md. Use N<=2048 (up to 16-frame clips) -- the bit-for-bit "
707
+ f"shipping path."
708
+ )
709
+ return _ATTN_BLOCK(h, wqk, bqk, tab, wvt, bv, wot, bo, scale, head_dim)
build/torch-neuron/nki_kernels/attention_inner.py ADDED
@@ -0,0 +1,1214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 vjepa2's 4-kernel encoder reaches, with the four h8/h10 engine pins
25
+ applied in place. Self-contained: imports only ``nki``, ``nkilib``, ``torch`` (none here) --
26
+ Self-contained: depends only on nki, nkilib and torch.
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
+ ):
272
+ """One head of context-encoding attention (SBUF-KV path).
273
+
274
+ LIVE contract for the K2 encoder: ``k=v=None`` (K/V handed in via ``k_cache_sbuf`` /
275
+ ``v_cache_sbuf``), ``causal_mask=False``, ``tp_q=False, tp_k=False, tp_out=True``,
276
+ ``cache_softmax=False``, ``out_o_hbm`` is a LIST of SBUF slices into the caller's O tile.
277
+
278
+ DEAD (removed): prefix caching (k_prior/v_prior/prior_used_len), sink, sliding_window, CP
279
+ (cp_offset/global_cp_deg/striped/strided), sequence packing (bound_min/bound_max),
280
+ skip_output_normalization, fp8 k_scale, and the fused v-projection (h_hbm/wv_t/bv).
281
+ """
282
+ is_prefix_caching = False
283
+
284
+ use_sbuf_kv = (k_cache_sbuf is not None and len(k_cache_sbuf) > 0) or (
285
+ v_cache_sbuf is not None and len(v_cache_sbuf) > 0
286
+ )
287
+ kernel_assert(
288
+ k_cache_sbuf is not None and len(k_cache_sbuf) > 0, "k_cache_sbuf must be provided when using sbuf kv"
289
+ )
290
+ kernel_assert(
291
+ v_cache_sbuf is not None and len(v_cache_sbuf) > 0, "v_cache_sbuf must be provided when using sbuf kv"
292
+ )
293
+
294
+ seqlen_q, seqlen_k_active, seqlen_k_prior, d, out_shape, softmax_shape = _check_input_and_return_shape(
295
+ q,
296
+ k,
297
+ v,
298
+ is_prefix_caching,
299
+ None,
300
+ None,
301
+ None,
302
+ tp_q,
303
+ tp_k,
304
+ tp_out,
305
+ cache_softmax,
306
+ k_cache_sbuf,
307
+ v_cache_sbuf,
308
+ None,
309
+ None,
310
+ skip_v_check=False,
311
+ )
312
+
313
+ # Use out_o_hbm if provided (LIVE: it is a list of SBUF slices).
314
+ if out_o_hbm is not None:
315
+ result = out_o_hbm
316
+ else:
317
+ result = nl.ndarray(shape=out_shape, dtype=q.dtype, buffer=nl.shared_hbm)
318
+
319
+ bs = q.shape[0]
320
+ bs_kv = 1 # sbuf-kv: batch_size_kv is per-batch tiles
321
+
322
+ kernel_assert(bs > 0, f"Batch size must be positive, got {bs}")
323
+ kernel_assert(bs <= _MAX_BS, f"attention_cte kernel is not tested for batch size above {_MAX_BS}, got {bs}.")
324
+ kernel_assert(bs_kv > 0, f"Batch size must be positive, got {bs_kv}")
325
+ kernel_assert(bs % bs_kv == 0, f"Q batch size must be a multiple of KV batch size, got {bs=}, {bs_kv=}")
326
+
327
+ seqlen_k_total = seqlen_k_active
328
+ kernel_assert(seqlen_q <= _MAX_SEQLEN, f"seqlen above {_MAX_SEQLEN}, got {seqlen_q=}.")
329
+ kernel_assert(seqlen_k_total <= _MAX_SEQLEN, f"seqlen above {_MAX_SEQLEN}, got {seqlen_k_total=}.")
330
+ bs_seqlen_qk_product = float(bs * seqlen_q) * seqlen_k_total
331
+ if bs_seqlen_qk_product > _MAX_BS_TIMES_SEQLEN_QK:
332
+ logger.warn(
333
+ f"attention_cte kernel is not tested for batch size x seqlen_q x seqlen_k above "
334
+ f"{_MAX_BS_TIMES_SEQLEN_QK}, got {bs_seqlen_qk_product=}.",
335
+ )
336
+
337
+ kernel_assert(d > 0, f"d must be > 0, got {d=}.")
338
+ kernel_assert(d <= _MAX_HEAD_DIM, f"we do not support head_dim > {_MAX_HEAD_DIM}, got head dim {d}")
339
+
340
+ kernel_assert(
341
+ (str(mm_out_dtype) == str(nl.float32))
342
+ or (str(mm_out_dtype) == str(nl.bfloat16) and (nisa.get_nc_version() >= nisa.nc_version.gen4)),
343
+ f"mm_out_dtype (psum) should be in [float32, bfloat16], got {mm_out_dtype} in {nisa.get_nc_version()}.",
344
+ )
345
+
346
+ ac = AttnConfig(
347
+ seqlen_q=seqlen_q,
348
+ seqlen_k_active=seqlen_k_active,
349
+ seqlen_k_prior=seqlen_k_prior,
350
+ d=d,
351
+ tp_q=tp_q,
352
+ tp_k=tp_k,
353
+ tp_out=tp_out,
354
+ is_prefix_caching=is_prefix_caching,
355
+ causal_mask=causal_mask,
356
+ use_swa=False,
357
+ sliding_window=0,
358
+ use_cp=False,
359
+ global_cp_deg=None,
360
+ cp_strided_q_slicing=False,
361
+ cp_striped_input=False,
362
+ scale=scale,
363
+ cache_softmax=cache_softmax,
364
+ skip_output_normalization=False,
365
+ dtype=q.dtype,
366
+ softmax_dtype=softmax_dtype,
367
+ mm_out_dtype=mm_out_dtype,
368
+ is_sequence_packed=False,
369
+ )
370
+
371
+ # Sharding info. use_sbuf_kv disables internal sharding (caller already sharded), so
372
+ # num_shard=1, shard_id=0, num_bs_per_shard=bs, has_remainder=False.
373
+ get_verified_program_sharding_info("attention_cte", max_sharding=2)
374
+ num_shard = 1
375
+ shard_id = 0
376
+ num_bs_per_shard = bs // num_shard
377
+ bs_offset = shard_id * num_bs_per_shard
378
+
379
+ for batch_idx in range(num_bs_per_shard):
380
+ # use_sbuf_kv: bs is LOCAL batch size and b is the LOCAL index.
381
+ batch_id = batch_idx
382
+ kv_batch_id = _q_to_kv_batch_id(batch_id, bs, bs_kv)
383
+ _attention_cte_impl(
384
+ q,
385
+ k,
386
+ v,
387
+ None,
388
+ None,
389
+ None,
390
+ result,
391
+ batch_id,
392
+ kv_batch_id,
393
+ ac,
394
+ k_cache_sbuf=k_cache_sbuf,
395
+ v_cache_sbuf=v_cache_sbuf,
396
+ init_sbuf_addr=init_sbuf_addr,
397
+ )
398
+
399
+ # has_remainder is False (num_shard==1); the seqlen-q sharding path is DEAD.
400
+ return result
401
+
402
+
403
+ def _attention_cte_impl(
404
+ q,
405
+ k_active,
406
+ v_active,
407
+ k_prior,
408
+ v_prior,
409
+ prior_used_len,
410
+ o,
411
+ batch_id,
412
+ batch_id_kv,
413
+ ac: AttnConfig,
414
+ k_cache_sbuf: Optional[List[nl.ndarray]] = None,
415
+ v_cache_sbuf: Optional[List[nl.ndarray]] = None,
416
+ init_sbuf_addr: int = 0,
417
+ ):
418
+ """Single-batch attention body. LIVE path only: single section (num_sections==1), pipelined
419
+ group loop (num_grps=16 > 1), SBUF-KV (no K/V HBM load), tp_out=True.
420
+
421
+ DEAD (removed): seqlen-q sharding, sink, SWA memset, sections>1 flash accumulation, K/V HBM
422
+ load (_load_k_tile / _load_v_tile), SBUF-prior storage, cache_softmax intermediate writes,
423
+ the non-pipelined (shard_seqlen_q_length<=1) branch.
424
+ """
425
+ # Not seqlen-sharded: start at group 0, cover all groups.
426
+ atp = _compute_tile_parameters(ac, is_seqlen_sharded=False)
427
+ shard_seqlen_q_start = 0
428
+ shard_seqlen_q_length = atp.num_grps
429
+
430
+ allocator = ModularAllocator(initial_address=init_sbuf_addr)
431
+ bufs = AttnInternalBuffers()
432
+ bufs.k_scale_sb = None
433
+
434
+ bufs.zero_bias_tensor = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, 1), dtype=nl.float32)
435
+ nisa.memset(bufs.zero_bias_tensor, 0.0)
436
+
437
+ # _setup_range_select_bounds is a NO-OP here: dynamic_sel_mask=False and not prefix caching.
438
+ _setup_range_select_bounds(ac, atp, bufs, allocator, None, None, None, None, batch_id=batch_id)
439
+
440
+ bufs.mm1_running_max = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, atp.num_grps), dtype=nl.float32)
441
+ bufs.exp_running_sum = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, atp.num_grps), dtype=nl.float32)
442
+ bufs.exp_sum_reciprocal = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, atp.num_grps), dtype=nl.float32)
443
+
444
+ sbuf_addr_outer = allocator.get_current_address()
445
+
446
+ # Single section (num_sections == 1).
447
+ for section_idx in range(atp.num_sections):
448
+ section_offset = atp.section_len * section_idx
449
+ section_offset_active, section_contains_prefix = _compute_section_offset_active(
450
+ section_offset, ac.is_prefix_caching, atp.seqlen_k_prior_padded
451
+ )
452
+ next_section_offset = atp.section_len * (section_idx + 1)
453
+ next_section_offset_active, next_section_contains_prefix = _compute_section_offset_active(
454
+ next_section_offset, ac.is_prefix_caching, atp.seqlen_k_prior_padded
455
+ )
456
+ sp = SectionParams(
457
+ section_idx=section_idx,
458
+ section_offset=section_offset,
459
+ section_offset_active=section_offset_active,
460
+ next_section_offset_active=next_section_offset_active,
461
+ section_contains_prefix=section_contains_prefix,
462
+ next_section_contains_prefix=next_section_contains_prefix,
463
+ )
464
+
465
+ allocator.set_current_address(sbuf_addr_outer)
466
+ _allocate_attention_buffers(allocator, ac, atp, bufs, sink=None,
467
+ k_cache_sbuf=k_cache_sbuf, v_cache_sbuf=v_cache_sbuf)
468
+ sbuf_addr = allocator.get_current_address()
469
+
470
+ # SBUF-KV: K and V are already resident, so the HBM K/V load block is skipped entirely.
471
+
472
+ # Software pipelining: num_grps=16 > 1, so the pipelined branch is the only reached one.
473
+ _load_q_impl(shard_seqlen_q_start, ac, atp, sp, bufs, q, batch_id, sbuf_addr)
474
+ _qk_and_max_impl(shard_seqlen_q_start, ac, atp, sp, bufs, batch_id)
475
+ _update_max_impl(shard_seqlen_q_start, ac, atp, sp, bufs, None)
476
+ _exp_impl(shard_seqlen_q_start, ac, atp, sp, bufs, None)
477
+
478
+ _load_q_impl(shard_seqlen_q_start + 1, ac, atp, sp, bufs, q, batch_id, sbuf_addr)
479
+ _qk_and_max_impl(shard_seqlen_q_start + 1, ac, atp, sp, bufs, batch_id)
480
+ _update_max_impl(shard_seqlen_q_start + 1, ac, atp, sp, bufs, None)
481
+ shard_seqlen_q_end = shard_seqlen_q_start + shard_seqlen_q_length
482
+
483
+ for grp_i in range(shard_seqlen_q_start, shard_seqlen_q_end - 2):
484
+ # grp_i: PV, write_back ; grp_i+1: EXP ; grp_i+2: load Q, QK+Max.
485
+ _load_q_impl(grp_i + 2, ac, atp, sp, bufs, q, batch_id, sbuf_addr)
486
+ _exp_impl(grp_i + 1, ac, atp, sp, bufs, None)
487
+ _fused_qkmax_and_pv_impl(grp_i, ac, atp, sp, bufs, batch_id)
488
+ _write_back_impl(grp_i, ac, atp, sp, bufs, o, batch_id)
489
+ _update_max_impl(grp_i + 2, ac, atp, sp, bufs, None)
490
+
491
+ _pv_impl(shard_seqlen_q_end - 2, ac, atp, sp, bufs)
492
+ _write_back_impl(shard_seqlen_q_end - 2, ac, atp, sp, bufs, o, batch_id)
493
+ _exp_impl(shard_seqlen_q_end - 1, ac, atp, sp, bufs, None)
494
+ _pv_impl(shard_seqlen_q_end - 1, ac, atp, sp, bufs)
495
+ _write_back_impl(shard_seqlen_q_end - 1, ac, atp, sp, bufs, o, batch_id)
496
+
497
+ # cache_softmax intermediate writes (out_neg_max / out_sum_recip) are DEAD (cache_softmax=False).
498
+
499
+
500
+ def _compute_tile_parameters(ac: AttnConfig, is_seqlen_sharded: bool) -> AttnTileParams:
501
+ """Tile parameters for the LIVE config. Dead: SWA/prefix/CP scale checks and seqlen updates,
502
+ flash sectioning (total_seqlen_k=2048 <= 10240 -> single section)."""
503
+ atp = AttnTileParams()
504
+
505
+ # LIVE: causal_mask=False, no CP/SWA/seqpack -> is_causal=False, dynamic_sel_mask=False.
506
+ atp.is_causal = ac.causal_mask
507
+ atp.dynamic_sel_mask = False
508
+ atp.seqlen_k_active_updated = ac.seqlen_k_active
509
+ atp.use_swa_optimized_allocation = False
510
+
511
+ atp.sb_p = nl.tile_size.pmax
512
+ kernel_assert(_Q_GRP_SZ == atp.sb_p, f"expect Q group size == pmax, got {_Q_GRP_SZ=}, {atp.sb_p=}")
513
+ kernel_assert(_V_TILE_SZ == atp.sb_p, f"expect V tile size == pmax, got {_V_TILE_SZ=}, {atp.sb_p=}")
514
+
515
+ atp.num_grps = div_ceil(ac.seqlen_q, atp.sb_p)
516
+ atp.can_pack_q_load = not is_seqlen_sharded
517
+
518
+ num_q_grps_per_load_dtype = 4 if ac.dtype == nl.float32 else 8
519
+ atp.num_q_grps_per_load = min(num_q_grps_per_load_dtype if atp.can_pack_q_load else 1, atp.num_grps)
520
+ 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}.")
521
+
522
+ atp.seqlen_k_prior_padded = None # not prefix caching
523
+ total_seqlen_k = atp.seqlen_k_active_updated
524
+
525
+ # total_seqlen_k = 2048 <= 10240 -> no flash attention, single section.
526
+ use_flash_attn = total_seqlen_k > _FLASH_ATTENTION_THRESHOLD
527
+ if use_flash_attn:
528
+ atp.section_len = min(total_seqlen_k, _FLASH_ATTENTION_SECTION_LENGTH)
529
+ else:
530
+ atp.section_len = total_seqlen_k
531
+
532
+ kernel_assert(atp.section_len > 0, f"section_len must be positive, got {atp.section_len}")
533
+ atp.num_sections = div_ceil(total_seqlen_k, atp.section_len)
534
+ if not use_flash_attn:
535
+ kernel_assert(atp.num_sections == 1, "Logic fault, must only have 1 section if not using flash_attn")
536
+
537
+ atp.num_large_tiles_per_section = div_ceil(atp.section_len, _LARGE_TILE_SZ)
538
+ atp.num_k_tiles_per_section = div_ceil(atp.section_len, _K_TILE_SZ)
539
+ atp.num_v_tiles_per_section = div_ceil(atp.section_len, _V_TILE_SZ)
540
+
541
+ atp.exp_inst_elems = _EXP_TILE_SZ
542
+ atp.num_exp_insts_per_large_tile = _LARGE_TILE_SZ // atp.exp_inst_elems
543
+ atp.num_tps_in_mm2_grp = _K_TILE_SZ // atp.sb_p
544
+ atp.mm2_grp_sz = _K_TILE_SZ
545
+
546
+ return atp
547
+
548
+
549
+ def _setup_range_select_bounds(
550
+ ac: AttnConfig,
551
+ atp: AttnTileParams,
552
+ bufs: AttnInternalBuffers,
553
+ allocator: ModularAllocator,
554
+ cp_offset: Any,
555
+ prior_used_len: Any,
556
+ bound_min: Any,
557
+ bound_max: Any,
558
+ batch_id: int = 0,
559
+ ) -> tuple:
560
+ """NO-OP for this config: only CP / SWA / prefix-caching / sequence-packing populate range
561
+ bounds, and all are off (``dynamic_sel_mask=False``, ``is_prefix_caching=False``). The entire
562
+ vendor body was gated by ``if atp.dynamic_sel_mask:`` / ``if ac.is_prefix_caching:`` / SWA,
563
+ so it emits no instructions here and is removed."""
564
+ return
565
+
566
+
567
+ def _allocate_attention_buffers(
568
+ allocator: ModularAllocator,
569
+ ac: AttnConfig,
570
+ atp: AttnTileParams,
571
+ bufs: AttnInternalBuffers,
572
+ sink: Any,
573
+ k_cache_sbuf: Optional[List[nl.ndarray]] = None,
574
+ v_cache_sbuf: Optional[List[nl.ndarray]] = None,
575
+ ):
576
+ """Allocate SBUF/PSUM buffers. LIVE: tp_out=True (so all tp_out branches taken), K/V come
577
+ from k_cache_sbuf/v_cache_sbuf, sink=None, dynamic_sel_mask=False (so the static-mask
578
+ mm1_copy_sb / mm1_affine_select_output buffers ARE allocated -- kept). The tp_out=False
579
+ branches and the fused-v-proj branch are DEAD and removed."""
580
+ mm2_p, mm2_n = atp.sb_p, ac.d
581
+
582
+ # K/V provided in SBUF by the caller.
583
+ bufs.k_sb = k_cache_sbuf
584
+ bufs.v_sb = v_cache_sbuf
585
+
586
+ bufs.q_sb = allocator.alloc_sbuf_tensor(
587
+ shape=(ac.d, atp.sb_p * atp.num_q_grps_per_load),
588
+ dtype=nl.bfloat16,
589
+ block_dim=[div_ceil(atp.num_grps, atp.num_q_grps_per_load)],
590
+ num_free_tiles=[2],
591
+ align_to=32,
592
+ )
593
+
594
+ bufs.flash_attn_correction_factor = allocator.alloc_sbuf_tensor(
595
+ shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
596
+ )
597
+
598
+ mm1_partial_max_n_elts = atp.num_k_tiles_per_section # sink is None
599
+ bufs.mm1_partial_max = allocator.alloc_sbuf_tensor(
600
+ shape=(atp.sb_p, mm1_partial_max_n_elts), dtype=nl.float32,
601
+ block_dim=[atp.num_grps], num_free_tiles=[2], align_to=4,
602
+ )
603
+
604
+ bufs.mm1_section_max = allocator.alloc_sbuf_tensor(
605
+ shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
606
+ )
607
+
608
+ n_final_reduce_sum_elts = div_ceil(atp.section_len, atp.exp_inst_elems) # sink is None
609
+ bufs.exp_partial_sum = allocator.alloc_sbuf_tensor(
610
+ shape=(atp.sb_p, n_final_reduce_sum_elts), dtype=nl.float32,
611
+ block_dim=[atp.num_grps], num_free_tiles=[2],
612
+ )
613
+
614
+ bufs.exp_section_sum = allocator.alloc_sbuf_tensor(
615
+ shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
616
+ )
617
+
618
+ bufs.prev_mm1_running_max = allocator.alloc_sbuf_tensor(
619
+ shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
620
+ )
621
+
622
+ bufs.prev_exp_running_sum = allocator.alloc_sbuf_tensor(
623
+ shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
624
+ )
625
+
626
+ # tp_out=True everywhere below.
627
+ bufs.mm2_prev_output = allocator.alloc_sbuf_tensor(
628
+ shape=(ac.d, atp.sb_p), dtype=ac.mm_out_dtype, block_dim=[atp.num_grps], num_free_tiles=[2],
629
+ )
630
+
631
+ bufs.mm2_accum_flash_attn = allocator.alloc_sbuf_tensor(
632
+ shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
633
+ )
634
+
635
+ bufs.mm2_prev_output_scaled = allocator.alloc_sbuf_tensor(
636
+ shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
637
+ )
638
+
639
+ bufs.tp_flash_attn_correction_factor_psum = []
640
+ bufs.tp_exp_sum_reciprocal_psum = []
641
+ for grp_idx in range(atp.num_grps):
642
+ tp_flash_attn_correction_factor_psum_tile = nl.ndarray(
643
+ (ac.d, atp.sb_p), dtype=nl.float32, buffer=nl.psum,
644
+ address=(0, ((grp_idx % 2) * 4 + 3) * PSUM_BANK_SIZE),
645
+ )
646
+ bufs.tp_flash_attn_correction_factor_psum.append(tp_flash_attn_correction_factor_psum_tile)
647
+ tp_exp_sum_reciprocal_psum_tile = nl.ndarray(
648
+ (ac.d, atp.sb_p), dtype=nl.float32, buffer=nl.psum,
649
+ address=(0, ((grp_idx % 2) * 4 + 3) * PSUM_BANK_SIZE),
650
+ )
651
+ bufs.tp_exp_sum_reciprocal_psum.append(tp_exp_sum_reciprocal_psum_tile)
652
+
653
+ bufs.tp_flash_attn_correction_factor_sb = allocator.alloc_sbuf_tensor(
654
+ shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[4],
655
+ )
656
+
657
+ bufs.tp_exp_sum_reciprocal_sb = allocator.alloc_sbuf_tensor(
658
+ shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[4],
659
+ )
660
+
661
+ bufs.mm2_final = allocator.alloc_sbuf_tensor(
662
+ shape=(ac.d, atp.sb_p), dtype=ac.mm_out_dtype, block_dim=[atp.num_grps], num_free_tiles=[2],
663
+ )
664
+
665
+ bufs.mm2_sb = allocator.alloc_sbuf_tensor(
666
+ shape=(mm2_n, mm2_p), dtype=ac.mm_out_dtype, block_dim=[atp.num_grps], num_free_tiles=[2],
667
+ )
668
+
669
+ bufs.mm1_masked = allocator.alloc_sbuf_tensor(
670
+ shape=(atp.sb_p, _LARGE_TILE_SZ), dtype=nl.float32,
671
+ block_dim=[atp.num_grps, atp.num_large_tiles_per_section],
672
+ num_free_tiles=[2, atp.num_large_tiles_per_section],
673
+ )
674
+
675
+ bufs.exp_sb = allocator.alloc_sbuf_tensor(
676
+ shape=(atp.sb_p, _LARGE_TILE_SZ), dtype=nl.bfloat16,
677
+ block_dim=[atp.num_grps, atp.num_large_tiles_per_section],
678
+ # DP2: group-axis depth 2 so exp(g+1) runs while transpose(g) still drains exp_sb(g).
679
+ num_free_tiles=[2, atp.num_large_tiles_per_section],
680
+ )
681
+
682
+ # MM1 PSUM tiles: banks {0,1,2,3}.
683
+ bufs.mm1_psum = []
684
+ for grp_idx in range(atp.num_grps):
685
+ grp_row = []
686
+ for large_tile_idx in range(atp.num_large_tiles_per_section):
687
+ tile_row = []
688
+ for k_tile_idx in range(4):
689
+ mm1_psum_tile = nl.ndarray(
690
+ (atp.sb_p, nl.tile_size.psum_fmax), dtype=ac.mm_out_dtype, buffer=nl.psum,
691
+ address=(0, (k_tile_idx % 4) * PSUM_BANK_SIZE),
692
+ )
693
+ tile_row.append(mm1_psum_tile)
694
+ grp_row.append(tile_row)
695
+ bufs.mm1_psum.append(grp_row)
696
+
697
+ # dynamic_sel_mask=False -> the static-mask staging buffers are allocated (no-mask branch
698
+ # does not read them, but the buffers exist in the vendor's non-dynamic allocation).
699
+ bufs.mm1_copy_sb = allocator.alloc_sbuf_tensor(
700
+ shape=(atp.sb_p, nl.tile_size.psum_fmax), dtype=ac.mm_out_dtype,
701
+ block_dim=[atp.num_grps, atp.num_large_tiles_per_section, 4], num_free_tiles=[1, 1, 2],
702
+ )
703
+ bufs.mm1_affine_select_output = allocator.alloc_sbuf_tensor(
704
+ shape=(atp.sb_p, nl.tile_size.psum_fmax), dtype=ac.mm_out_dtype,
705
+ block_dim=[atp.num_grps, atp.num_large_tiles_per_section, 4], num_free_tiles=[1, 1, 2],
706
+ )
707
+
708
+ bufs.exp_tp_sb = allocator.alloc_sbuf_tensor(
709
+ shape=(atp.sb_p, atp.mm2_grp_sz), dtype=nl.bfloat16,
710
+ block_dim=[atp.num_grps, atp.num_large_tiles_per_section, atp.num_tps_in_mm2_grp],
711
+ # DP4: 8 group-blocks resident so P^T transposes issue as soon as their exp exists.
712
+ num_free_tiles=[8, atp.num_large_tiles_per_section, atp.num_tps_in_mm2_grp],
713
+ align_to=32,
714
+ )
715
+
716
+ # MM2 PSUM tiles: banks {4,5,6,7}. tp_out=True shape (mm2_n, mm2_p).
717
+ bufs.mm2_psum = []
718
+ for grp_idx in range(atp.num_grps):
719
+ grp_row = []
720
+ for large_tile_idx in range(atp.num_large_tiles_per_section):
721
+ mm2_psum_tile = nl.ndarray(
722
+ (mm2_n, mm2_p), dtype=ac.mm_out_dtype, buffer=nl.psum,
723
+ address=(0, ((4 + (large_tile_idx % 4)) * PSUM_BANK_SIZE)),
724
+ )
725
+ grp_row.append(mm2_psum_tile)
726
+ bufs.mm2_psum.append(grp_row)
727
+
728
+
729
+ def _q_to_kv_batch_id(batch_id: int, bs: int, bs_kv: int) -> int:
730
+ """Map Q batch id to KV batch id for native GQA support. bs_kv=1 here -> always 0."""
731
+ return batch_id // (bs // bs_kv)
732
+
733
+
734
+ def _check_input_and_return_shape(
735
+ q,
736
+ k,
737
+ v,
738
+ is_prefix_caching,
739
+ k_prior,
740
+ v_prior,
741
+ prior_used_len,
742
+ tp_q,
743
+ tp_k,
744
+ tp_out,
745
+ cache_softmax,
746
+ k_cache_sbuf=None,
747
+ v_cache_sbuf=None,
748
+ k_prior_sbuf=None,
749
+ v_prior_sbuf=None,
750
+ skip_v_check=False,
751
+ ) -> tuple:
752
+ """Validate shapes and return (seqlen_q, seqlen_k, seqlen_k_prior, d, out_shape,
753
+ cache_softmax_shape). LIVE: use_sbuf_kv=True, tp_q=False, tp_out=True, not prefix caching,
754
+ cache_softmax=False."""
755
+ # SBUF-KV path.
756
+ kernel_assert(k_cache_sbuf is not None and len(k_cache_sbuf) > 0, "k_cache_sbuf must be provided")
757
+ kernel_assert(v_cache_sbuf is not None and len(v_cache_sbuf) > 0, "v_cache_sbuf must be provided")
758
+
759
+ if tp_q:
760
+ batch_size, seqlen_q, d = q.shape
761
+ else:
762
+ batch_size, d, seqlen_q = q.shape
763
+
764
+ d_from_k = k_cache_sbuf[0].shape[0]
765
+ kernel_assert(d == d_from_k, f"Head dimension mismatch: q has d={d}, but k_cache_sbuf has d={d_from_k}")
766
+ seqlen_k = len(k_cache_sbuf) * _K_TILE_SZ
767
+ batch_size_kv = 1
768
+ seqlen_k_prior = None # not prefix caching
769
+
770
+ out_seqlen = seqlen_q
771
+ if tp_out:
772
+ out_shape = (batch_size, d, out_seqlen)
773
+ else:
774
+ out_shape = (batch_size, out_seqlen, d)
775
+
776
+ cache_softmax_tile_size = 128
777
+ padded_seq_grps = div_ceil(out_seqlen, cache_softmax_tile_size)
778
+ cache_softmax_shape = [batch_size, cache_softmax_tile_size, padded_seq_grps]
779
+
780
+ return (seqlen_q, seqlen_k, seqlen_k_prior, d, out_shape, cache_softmax_shape)
781
+
782
+
783
+ def _load_q_tile(q, out, tp_q, batch_id, grp_i, seqlen_offset, load_dtype, sbuf_addr, grps_per_load=1) -> int:
784
+ """Load a Q tile HBM->SBUF. LIVE: tp_q=False (q is [bs, d, seqlen]), no transpose; the whole
785
+ ``if tp_q:`` transpose sub-tree (dma_transpose / nc_transpose paths) is DEAD and removed."""
786
+ kernel_assert(str(load_dtype) == str(out[0].dtype), "Conflicting dtype")
787
+ _, d, seqlen = q.shape
788
+
789
+ num_f = min(seqlen - seqlen_offset, _Q_GRP_SZ * grps_per_load)
790
+ out_dst_pat = out[grp_i // grps_per_load].ap(pattern=[[_Q_GRP_SZ * grps_per_load, d], [1, num_f]], offset=0)
791
+ q_src_pat = q.ap(pattern=[[seqlen, d], [1, num_f]], offset=batch_id * d * seqlen + seqlen_offset)
792
+ nisa.dma_copy(dst=out_dst_pat, src=q_src_pat)
793
+
794
+
795
+ def _get_kv_tile_apc(
796
+ is_prefix_caching,
797
+ k_active,
798
+ k_prior,
799
+ seqlen_active,
800
+ seqlen_prior,
801
+ seqlen_offset,
802
+ load_offset_active,
803
+ ) -> tuple:
804
+ """Determine which KV tensor (active or prior) to use. LIVE: is_prefix_caching=False, so this
805
+ returns the trivial active-tile mapping; the prefix-caching branch is DEAD and removed."""
806
+ return k_active, seqlen_active, seqlen_offset, load_offset_active
807
+
808
+
809
+ def _compute_section_offset_active(k_section_offset, is_prefix_caching, seqlen_k_prior_padded):
810
+ """Active-K offset for a section. LIVE: is_prefix_caching=False -> pass-through."""
811
+ section_contains_prefix = False
812
+ k_section_offset_active = k_section_offset
813
+ return k_section_offset_active, section_contains_prefix
814
+
815
+
816
+ def _load_q_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
817
+ bufs: AttnInternalBuffers, q, batch_id, sbuf_addr):
818
+ """Load a Q group if compute is needed. LIVE: is_causal=False -> has_any_compute is always
819
+ True; k_scale_sb is None (fp8 dequant path DEAD)."""
820
+ if grp_i % atp.num_q_grps_per_load == 0:
821
+ # is_causal=False -> has_any_compute_pred is True.
822
+ q_seqlen_offset = grp_i * _Q_GRP_SZ
823
+ _load_q_tile(
824
+ q, bufs.q_sb, ac.tp_q, batch_id, grp_i, q_seqlen_offset,
825
+ bufs.q_sb[0].dtype, sbuf_addr, grps_per_load=atp.num_q_grps_per_load,
826
+ )
827
+ # k_scale_sb is None -> the delayed-fp8 Q scaling is DEAD.
828
+
829
+
830
+ def _qk_and_max_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
831
+ bufs: AttnInternalBuffers, batch_id: int = 0):
832
+ """MM1 (QK^T) + row-max for a Q group. LIVE: is_causal=False -> always compute.
833
+
834
+ h10 EDIT (site :2876): the ``mm1_partial_max`` init memset is pinned to GpSimd -- it sheds
835
+ Vector (K2's binding engine) and its first consumer is several ops downstream, so a slower
836
+ idle engine is safe."""
837
+ # is_causal=False -> has_any_compute_pred is True.
838
+ nisa.memset(bufs.mm1_partial_max[grp_i], value=_FLOAT32_MIN, engine=engine.gpsimd)
839
+
840
+ for large_tile_idx in range(atp.num_large_tiles_per_section):
841
+ _qk_and_max_large_tile_impl(grp_i, large_tile_idx, ac, atp, sp, bufs, batch_id)
842
+
843
+
844
+ def _update_max_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
845
+ bufs: AttnInternalBuffers, sink):
846
+ """Update running max. LIVE: is_causal=False (always compute), sink=None, num_sections==1 so
847
+ the reduce writes straight into mm1_running_max (max-direct promotion) and the flash
848
+ multi-section correction is DEAD."""
849
+ # is_causal=False -> has_any_compute_pred is True.
850
+
851
+ # num_sections==1: the reduce writes straight into mm1_running_max (max-direct); the [128,1]
852
+ # copy the vendor once had here is gone (3,072 Vector instructions removed upstream).
853
+ _max_dst = bufs.mm1_running_max[:, grp_i]
854
+ nisa.tensor_reduce(_max_dst, nl.maximum, bufs.mm1_partial_max[grp_i], 1, negate=True)
855
+ # num_sections==1: no running-max copy / flash correction factor. sink block DEAD (sink=None).
856
+
857
+
858
+ def _exp_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
859
+ bufs: AttnInternalBuffers, sink):
860
+ """Softmax exp (Scalar ACTIVATE) + P^T dma_transposes. LIVE: is_causal=False, use_swa=False,
861
+ not prefix caching -> every k-tile is emitted (emit_idxs is full), wide_exp=False, sink=None.
862
+
863
+ The ``memset`` on ``exp_partial_sum`` stays UNPINNED (engine.unknown / Vector) ON PURPOSE
864
+ (vendor :2993): pinning it would fork this 280-line function and re-home the softmax-exp and
865
+ dma_transpose source strings, which MUST keep their ``attention_inner`` sites stable and
866
+ match the shipping stream. The ACCUM FORK (reset on first tile, accumulate, drain once) and
867
+ the reduce_cmd discipline are preserved verbatim."""
868
+ # is_causal=False -> has_any_compute_pred is True.
869
+ q_seqlen_offset = grp_i * atp.sb_p
870
+ nisa.memset(bufs.exp_partial_sum[grp_i][...], value=0.0) # UNPINNED on purpose (:2993).
871
+
872
+ for large_tile_idx in range(atp.num_large_tiles_per_section):
873
+ kernel_assert(atp.exp_inst_elems == 512, "Internal validation failed.")
874
+
875
+ # ACCUM FORK pre-scan: is_causal=False and not prefix caching -> every tile emits, so
876
+ # emit_idxs is [0, 1, 2, 3]. reset on the first, accumulate, drain once.
877
+ emit_idxs = []
878
+ for scan_idx in range(atp.num_exp_insts_per_large_tile):
879
+ scan_prior, scan_seqlen_k, scan_k_start, _ = _get_kv_tile_apc(
880
+ ac.is_prefix_caching,
881
+ False,
882
+ True,
883
+ atp.seqlen_k_active_updated,
884
+ ac.seqlen_k_prior,
885
+ sp.section_offset + large_tile_idx * _LARGE_TILE_SZ + scan_idx * atp.exp_inst_elems,
886
+ None,
887
+ )
888
+ scan_mask = True # is_causal=False, use_swa=False
889
+ if scan_mask and scan_seqlen_k > scan_k_start:
890
+ emit_idxs.append(scan_idx)
891
+ first_emit = emit_idxs[0] if emit_idxs else -1
892
+ last_emit = emit_idxs[-1] if emit_idxs else -1
893
+
894
+ for exp_tile_idx in range(atp.num_exp_insts_per_large_tile):
895
+ is_prior_tile, seqlen_k, k_start_pos, _ = _get_kv_tile_apc(
896
+ ac.is_prefix_caching,
897
+ False,
898
+ True,
899
+ atp.seqlen_k_active_updated,
900
+ ac.seqlen_k_prior,
901
+ sp.section_offset + large_tile_idx * _LARGE_TILE_SZ + exp_tile_idx * atp.exp_inst_elems,
902
+ None,
903
+ )
904
+ num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
905
+ num_f = min(seqlen_k - k_start_pos, atp.exp_inst_elems)
906
+
907
+ exp_sel_mask = True # is_causal=False, use_swa=False
908
+
909
+ if exp_sel_mask and seqlen_k > k_start_pos:
910
+ # Step 1: exp. reduce_cmd is ALWAYS explicit (see the vendor note: an idle cmd
911
+ # with reduce_op/reduce_res would write a stale register).
912
+ if exp_tile_idx == first_emit:
913
+ exp_reduce_cmd = nisa.reduce_cmd.reset_reduce
914
+ else:
915
+ exp_reduce_cmd = nisa.reduce_cmd.reduce
916
+ if exp_tile_idx == last_emit:
917
+ exp_reduce_res = bufs.exp_partial_sum[grp_i][
918
+ :num_p,
919
+ large_tile_idx * atp.num_exp_insts_per_large_tile + exp_tile_idx,
920
+ ]
921
+ else:
922
+ exp_reduce_res = None
923
+ nisa.activation(
924
+ bufs.exp_sb[grp_i][large_tile_idx][
925
+ :num_p, nl.ds(exp_tile_idx * atp.exp_inst_elems, num_f)
926
+ ],
927
+ op=nl.exp,
928
+ data=bufs.mm1_masked[grp_i][large_tile_idx][
929
+ :num_p, nl.ds(exp_tile_idx * atp.exp_inst_elems, num_f)
930
+ ],
931
+ reduce_op=nl.add,
932
+ reduce_cmd=exp_reduce_cmd,
933
+ reduce_res=exp_reduce_res,
934
+ bias=bufs.mm1_running_max[:num_p, grp_i],
935
+ )
936
+
937
+ # Step 2: P^T dma_transpose into exp_tp_sb ([128, 4, 128]).
938
+ num_f_outer = num_f // atp.sb_p
939
+ num_f_inner = num_f % atp.sb_p
940
+
941
+ if num_f_outer >= 1:
942
+ nisa.dma_transpose(
943
+ dst=bufs.exp_tp_sb[grp_i][large_tile_idx][exp_tile_idx].ap(
944
+ [
945
+ [atp.mm2_grp_sz, atp.sb_p],
946
+ [1, 1],
947
+ [atp.sb_p, num_f_outer],
948
+ [1, num_p],
949
+ ]
950
+ ),
951
+ src=bufs.exp_sb[grp_i][large_tile_idx].ap(
952
+ [
953
+ [_LARGE_TILE_SZ, num_p],
954
+ [1, 1],
955
+ [atp.sb_p, num_f_outer],
956
+ [1, atp.sb_p],
957
+ ],
958
+ offset=exp_tile_idx * atp.mm2_grp_sz,
959
+ ),
960
+ )
961
+
962
+ if num_f_inner > 0:
963
+ nisa.dma_transpose(
964
+ dst=bufs.exp_tp_sb[grp_i][large_tile_idx][exp_tile_idx].ap(
965
+ [
966
+ [atp.mm2_grp_sz, num_f_inner],
967
+ [1, 1],
968
+ [atp.sb_p, 1],
969
+ [1, num_p],
970
+ ],
971
+ offset=num_f_outer * atp.sb_p,
972
+ ),
973
+ src=bufs.exp_sb[grp_i][large_tile_idx].ap(
974
+ [
975
+ [_LARGE_TILE_SZ, num_p],
976
+ [1, 1],
977
+ [atp.sb_p, 1],
978
+ [1, num_f_inner],
979
+ ],
980
+ offset=exp_tile_idx * atp.mm2_grp_sz + num_f_outer * atp.sb_p,
981
+ ),
982
+ )
983
+
984
+ # sink block DEAD (sink=None).
985
+
986
+
987
+ def _pv_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams, bufs: AttnInternalBuffers):
988
+ """MM2 (P@V) for a Q group. LIVE: is_causal=False -> always compute."""
989
+ # is_causal=False -> has_any_compute_pred is True.
990
+ nisa.memset(bufs.mm2_sb[grp_i][...], value=0.0)
991
+
992
+ for large_tile_idx in range(atp.num_large_tiles_per_section):
993
+ _pv_large_tile_impl(grp_i, large_tile_idx, ac, atp, sp, bufs)
994
+
995
+
996
+ def _fused_qkmax_and_pv_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
997
+ bufs: AttnInternalBuffers, batch_id: int = 0):
998
+ """Pipelined MM2 for grp_i while doing QK+max for grp_i+2. LIVE: is_causal=False -> both
999
+ predicates True.
1000
+
1001
+ h10 EDIT (site :3294): the ``mm1_partial_max[qkmax_grp]`` init memset is pinned to GpSimd
1002
+ (same rationale as :2876; 2,688/core, the larger of the two)."""
1003
+ qkmax_grp = grp_i + 2
1004
+ # is_causal=False -> both has_any_compute predicates are True.
1005
+ nisa.memset(bufs.mm1_partial_max[qkmax_grp][...], value=_FLOAT32_MIN, engine=engine.gpsimd)
1006
+
1007
+ for large_tile_idx in range(atp.num_large_tiles_per_section):
1008
+ _pv_large_tile_impl(grp_i, large_tile_idx, ac, atp, sp, bufs)
1009
+ _qk_and_max_large_tile_impl(qkmax_grp, large_tile_idx, ac, atp, sp, bufs, batch_id)
1010
+
1011
+
1012
+ def _write_back_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
1013
+ bufs: AttnInternalBuffers, o, batch_id):
1014
+ """Finalise output. LIVE: is_causal=False (always compute), num_sections==1,
1015
+ skip_output_normalization=False -> single-section reciprocal + scale_reciprocal_write_back.
1016
+ The flash multi-section accumulation and the skip-normalization branches are DEAD."""
1017
+ # is_causal=False -> has_any_compute_pred is True.
1018
+ q_seqlen_offset = grp_i * atp.sb_p
1019
+ nisa.tensor_reduce(bufs.exp_section_sum[grp_i][...], nl.add, bufs.exp_partial_sum[grp_i], axis=1)
1020
+ # num_sections==1, not skip_output_normalization.
1021
+ nisa.reciprocal(bufs.exp_sum_reciprocal[:, grp_i], bufs.exp_section_sum[grp_i])
1022
+
1023
+ num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1024
+ num_f = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1025
+
1026
+ # Single section: scale by reciprocal and write current output.
1027
+ _scale_reciprocal_write_back_impl(bufs.mm2_sb[grp_i], grp_i, ac, atp, bufs, o, batch_id, num_p, num_f)
1028
+
1029
+
1030
+ def _scale_reciprocal_write_back_impl(src_buf, grp_i, ac: AttnConfig, atp: AttnTileParams,
1031
+ bufs: AttnInternalBuffers, o, batch_id, num_p, num_f):
1032
+ """Reciprocal-normalise and write back (final section). LIVE: tp_out=True.
1033
+
1034
+ h8 EDIT (site 1): the tp_out PSUM->SBUF evict ``tensor_copy`` is pinned to Scalar -- the
1035
+ un-pinned copy defaults to Vector (K2's binding engine); GpSimd is illegal (PSUM source), so
1036
+ Scalar is the only shedding target and is the exercised majority path. The tp_out=False
1037
+ ``tensor_scalar(..., engine=engine.vector)`` branch is DEAD (tp_out always True) and removed."""
1038
+ # tp_out=True.
1039
+ nisa.nc_transpose(
1040
+ bufs.tp_exp_sum_reciprocal_psum[grp_i].ap(pattern=[[atp.sb_p, ac.d], [1, atp.sb_p]], offset=0),
1041
+ bufs.exp_sum_reciprocal.ap(pattern=[[atp.num_grps, atp.sb_p], [0, ac.d]], offset=grp_i),
1042
+ )
1043
+ # ---- h8 SITE 1: PSUM->SBUF evict pinned to Scalar. ----
1044
+ nisa.tensor_copy(
1045
+ bufs.tp_exp_sum_reciprocal_sb[grp_i][: ac.d, :num_f],
1046
+ bufs.tp_exp_sum_reciprocal_psum[grp_i][: ac.d, :num_f],
1047
+ engine=engine.scalar,
1048
+ )
1049
+ nisa.tensor_tensor(
1050
+ bufs.mm2_final[grp_i][: ac.d, :num_f],
1051
+ src_buf[: ac.d, :num_f],
1052
+ bufs.tp_exp_sum_reciprocal_sb[grp_i][: ac.d, :num_f],
1053
+ nl.multiply,
1054
+ )
1055
+
1056
+ _write_back_o_impl(bufs.mm2_final[grp_i], grp_i, ac, atp, o, batch_id, num_p, num_f)
1057
+
1058
+
1059
+ def _write_back_o_impl(src_buf, grp_i, ac: AttnConfig, atp: AttnTileParams, o, batch_id, num_p, num_f):
1060
+ """Evict finished O. LIVE: ``o`` is a LIST of caller-owned SBUF slices, tp_out=True, so the
1061
+ output is copied into the caller's O tile (kept on-chip for the fused out-projection) rather
1062
+ than DMA'd to HBM. The HBM-sink branch is DEAD (o is always a list) and removed.
1063
+
1064
+ h8 EDIT (site 3): the tp_out list-branch ``tensor_copy`` (SBUF->SBUF fp32->bf16 CAST) is
1065
+ pinned to GpSimd -- all three engines are legal and GpSimd is the idle engine, so it sheds
1066
+ off Vector."""
1067
+ # o is a list (caller-owned SBUF slices), tp_out=True.
1068
+ # ---- h8 SITE 3: SBUF->SBUF CAST pinned to GpSimd. ----
1069
+ nisa.tensor_copy(dst=o[grp_i][: ac.d, :num_f], src=src_buf[: ac.d, :num_f], engine=engine.gpsimd)
1070
+
1071
+
1072
+ def _qk_and_max_large_tile_impl(qkmax_grp, large_tile_idx, ac: AttnConfig, atp: AttnTileParams,
1073
+ sp: SectionParams, bufs: AttnInternalBuffers, batch_id: int = 0):
1074
+ """MM1 (QK^T) + masking for a Q group and a 2048 K tile. LIVE: is_causal=False,
1075
+ dynamic_sel_mask=False, is_prior_tile=False -> the FINAL ``else: # no masking``
1076
+ ``tensor_scalar_reduce`` branch. The diagonal (affine_select) and range_select branches are
1077
+ DEAD and removed; SBUF-prefix-caching K-tile selection is DEAD (k_sb_prior is None)."""
1078
+ q_seqlen_offset = qkmax_grp * atp.sb_p
1079
+
1080
+ num_k_tiles_in_large_tile = _LARGE_TILE_SZ // _K_TILE_SZ
1081
+ for k_tile_idx in range(num_k_tiles_in_large_tile):
1082
+ mm1_psum_tile = bufs.mm1_psum[qkmax_grp][large_tile_idx][k_tile_idx]
1083
+ # dynamic_sel_mask=False, but the no-mask branch below reads neither mm1_copy_sb nor
1084
+ # mm1_affine_select_output, so their per-tile locals are not needed.
1085
+ mm1_masked_tile = bufs.mm1_masked[qkmax_grp][large_tile_idx]
1086
+ mm1_partial_max_tile = bufs.mm1_partial_max[qkmax_grp]
1087
+
1088
+ k_tile_idx_in_section = large_tile_idx * num_k_tiles_in_large_tile + k_tile_idx
1089
+ _kv_sec_idx = sp.kv_section_idx if sp.kv_section_idx is not None else sp.section_idx
1090
+ k_tile_idx_global = atp.num_k_tiles_per_section * _kv_sec_idx + k_tile_idx_in_section
1091
+ is_prior_tile, seqlen_k, k_start_pos, _ = _get_kv_tile_apc(
1092
+ ac.is_prefix_caching,
1093
+ False,
1094
+ True,
1095
+ atp.seqlen_k_active_updated,
1096
+ ac.seqlen_k_prior,
1097
+ k_tile_idx_global * _K_TILE_SZ,
1098
+ None,
1099
+ )
1100
+
1101
+ matmul_selection = True # is_causal=False
1102
+
1103
+ if q_seqlen_offset >= ac.seqlen_q or k_start_pos >= seqlen_k:
1104
+ matmul_selection = False
1105
+
1106
+ if matmul_selection and k_tile_idx_in_section < atp.num_k_tiles_per_section:
1107
+ num_f = min(seqlen_k - k_start_pos, _K_TILE_SZ)
1108
+ num_q_free = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1109
+
1110
+ # Step 1: MM1 matmul. Non-APC path -> use k_tile_idx_in_section.
1111
+ k_tile_to_use = bufs.k_sb[k_tile_idx_in_section]
1112
+
1113
+ nisa.nc_matmul(
1114
+ mm1_psum_tile[:num_q_free, :num_f],
1115
+ bufs.q_sb[qkmax_grp // atp.num_q_grps_per_load][
1116
+ : ac.d,
1117
+ nl.ds((qkmax_grp % atp.num_q_grps_per_load) * _Q_GRP_SZ, num_q_free),
1118
+ ],
1119
+ k_tile_to_use[:, :num_f],
1120
+ )
1121
+
1122
+ # Step 2: no masking (is_causal=False, dynamic_sel_mask=False).
1123
+ num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1124
+ num_f = min(seqlen_k - k_start_pos, _K_TILE_SZ)
1125
+
1126
+ nisa.tensor_scalar_reduce(
1127
+ mm1_masked_tile[:num_p, nl.ds(k_tile_idx * _K_TILE_SZ, num_f)],
1128
+ data=mm1_psum_tile[:num_p, :num_f],
1129
+ op0=nl.multiply,
1130
+ operand0=ac.scale,
1131
+ reduce_op=nl.maximum,
1132
+ reduce_res=mm1_partial_max_tile[:num_p, k_tile_idx_in_section],
1133
+ )
1134
+
1135
+
1136
+ def _pv_large_tile_impl(pv_grp, large_tile_idx, ac: AttnConfig, atp: AttnTileParams,
1137
+ sp: SectionParams, bufs: AttnInternalBuffers):
1138
+ """MM2 (P@V) for a Q group and 2048 V tile. LIVE: is_causal=False (all tiles selected),
1139
+ tp_out=True, single large tile so ``large_tile_idx==0`` -> the ``tensor_copy`` PSUM->SBUF
1140
+ reduction (never the accumulate ``tensor_tensor`` branch). SBUF-prefix V-tile selection is
1141
+ DEAD (v_sb_prior is None); the tp_out=False MM2 orientation is DEAD."""
1142
+ q_seqlen_offset = pv_grp * atp.sb_p
1143
+ num_mm2_grps_in_large_tile = _LARGE_TILE_SZ // atp.mm2_grp_sz
1144
+ mm2_psum_set = False
1145
+
1146
+ mm2_psum_tile = bufs.mm2_psum[pv_grp][large_tile_idx]
1147
+
1148
+ # Step 1: matmul + accumulate in PSUM per group.
1149
+ for mm2_grp_i in range(num_mm2_grps_in_large_tile):
1150
+ num_mm2_per_grp = atp.mm2_grp_sz // _V_TILE_SZ
1151
+ num_mm2_per_large_tile = num_mm2_per_grp * num_mm2_grps_in_large_tile
1152
+
1153
+ exp_tp_sb_tile = bufs.exp_tp_sb[pv_grp][large_tile_idx][mm2_grp_i]
1154
+
1155
+ is_prior_tile, seqlen_k, k_start_pos_512_tile, _ = _get_kv_tile_apc(
1156
+ ac.is_prefix_caching,
1157
+ False,
1158
+ True,
1159
+ atp.seqlen_k_active_updated,
1160
+ ac.seqlen_k_prior,
1161
+ sp.section_offset + large_tile_idx * _LARGE_TILE_SZ + mm2_grp_i * atp.mm2_grp_sz,
1162
+ None,
1163
+ )
1164
+
1165
+ for mm2_i in range(num_mm2_per_grp):
1166
+ v_tile_idx = large_tile_idx * num_mm2_per_large_tile + mm2_grp_i * num_mm2_per_grp + mm2_i
1167
+ k_start_pos = k_start_pos_512_tile + mm2_i * _V_TILE_SZ
1168
+
1169
+ num_p = min(seqlen_k - k_start_pos, _V_TILE_SZ)
1170
+ num_f = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1171
+
1172
+ mm2_sel_mask = True # is_causal=False
1173
+
1174
+ if mm2_sel_mask and v_tile_idx < atp.num_v_tiles_per_section and num_p > 0 and num_f > 0:
1175
+ mm2_psum_set = True
1176
+
1177
+ # Non-APC path -> use v_tile_idx.
1178
+ v_tile_to_use = bufs.v_sb[v_tile_idx]
1179
+
1180
+ # tp_out=True orientation.
1181
+ nisa.nc_matmul(
1182
+ mm2_psum_tile[: ac.d, :num_f],
1183
+ v_tile_to_use[:num_p, : ac.d],
1184
+ exp_tp_sb_tile[:num_p, nl.ds(mm2_i * _V_TILE_SZ, num_f)],
1185
+ )
1186
+
1187
+ # Step 2: reduce MM2 groups into the large tile. is_prior_tile=False, is_causal=False ->
1188
+ # loop_reduce_sel True; single large tile -> large_tile_idx==0 -> tensor_copy (never the
1189
+ # accumulate tensor_tensor branch).
1190
+ is_prior_tile, seqlen_k, k_start_pos, _ = _get_kv_tile_apc(
1191
+ ac.is_prefix_caching,
1192
+ False,
1193
+ True,
1194
+ atp.seqlen_k_active_updated,
1195
+ ac.seqlen_k_prior,
1196
+ sp.section_offset + large_tile_idx * _LARGE_TILE_SZ,
1197
+ None,
1198
+ )
1199
+ k_seqlen_mask = k_start_pos < seqlen_k
1200
+
1201
+ loop_reduce_sel = True # is_causal=False
1202
+
1203
+ if loop_reduce_sel and k_seqlen_mask and mm2_psum_set:
1204
+ # tp_out=True.
1205
+ num_f = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
1206
+ if large_tile_idx == 0:
1207
+ nisa.tensor_copy(bufs.mm2_sb[pv_grp][:, :num_f], mm2_psum_tile[:, :num_f])
1208
+ else:
1209
+ nisa.tensor_tensor(
1210
+ bufs.mm2_sb[pv_grp][:, :num_f],
1211
+ bufs.mm2_sb[pv_grp][:, :num_f],
1212
+ mm2_psum_tile[:, :num_f],
1213
+ nl.add,
1214
+ )
build/torch-neuron/nki_kernels/mlp.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ LNC = 2
55
+
56
+ _INV_SQRT2 = 1.0 / math.sqrt(2.0)
57
+ _INV_SQRT_2PI = 1.0 / math.sqrt(2.0 * math.pi)
58
+
59
+
60
+ def _div_ceil(n: int, d: int) -> int:
61
+ """Ceiling division (never write (n + d - 1) // d inline)."""
62
+ return (n + d - 1) // d
63
+
64
+
65
+ def _kernel_assert(condition: bool, error_text: str):
66
+ """Assert with an NKI-formatted error message."""
67
+ assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
68
+
69
+
70
+ def _stage_xt(x_hbm, r_abs, pr, n_k, K, rb_size):
71
+ """Emit one row block's x staging and return its ``n_k`` transposed bf16 tiles.
72
+
73
+ ``n_rs`` loads of ``[<=128, K]`` bf16, each followed by ``n_k`` DMA transposes into
74
+ the row block's ``xt_tiles``. Kept as a module-level emitter so the ``x`` load and
75
+ the dma_transpose each map to a single source site regardless of where it is called.
76
+
77
+ Args:
78
+ x_hbm: [R, K] @ HBM bf16, the MLP input.
79
+ r_abs: absolute starting row of this row block.
80
+ pr: number of rows in this row block (<= rb_size).
81
+ n_k: number of K tiles (fc1 contraction dimension tiles).
82
+ K: hidden-in dimension.
83
+ rb_size: row-block width (free axis of the transposed tiles).
84
+
85
+ Returns:
86
+ list of ``n_k`` transposed ``[128, rb_size]`` bf16 SBUF tiles.
87
+ """
88
+ n_rs = _div_ceil(pr, _P_MAX)
89
+ xt_tiles = []
90
+ for _ in range(n_k):
91
+ xt_tiles.append(nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf))
92
+ for rs in range(n_rs):
93
+ rr0 = rs * _P_MAX
94
+ prs = min(_P_MAX, pr - rr0)
95
+ x_b16 = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
96
+ nisa.dma_copy(
97
+ dst=x_b16[0:prs, 0:K],
98
+ src=x_hbm[r_abs + rr0:r_abs + rr0 + prs, 0:K],
99
+ )
100
+ for kt in range(n_k):
101
+ k0 = kt * _P_MAX
102
+ pk = min(_P_MAX, K - k0)
103
+ nisa.dma_transpose(dst=xt_tiles[kt][0:pk, rr0:rr0 + prs],
104
+ src=x_b16[0:prs, k0:k0 + pk])
105
+ return xt_tiles
106
+
107
+
108
+ @nki.jit
109
+ def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
110
+ """gelu(x @ w1.T + b1) @ w2.T + b2, rows sharded, b1 AND both x blocks resident early.
111
+
112
+ Args:
113
+ x_hbm (nl.ndarray): [R, K] @ HBM, **bfloat16** (kernel 1 emits bf16).
114
+ w1t_hbm (nl.ndarray): [K, H] @ HBM, **bf16**, == fc1.weight.T.
115
+ b1_hbm (nl.ndarray): [H] @ HBM, float32.
116
+ w2t_hbm (nl.ndarray): [H, K] @ HBM, **bf16**, == fc2.weight.T.
117
+ b2_hbm (nl.ndarray): [K] @ HBM, float32.
118
+
119
+ Returns:
120
+ nl.ndarray: [R, K] @ HBM, **bfloat16**.
121
+ """
122
+ R, K = x_hbm.shape
123
+ K_w1t, H = w1t_hbm.shape
124
+ H_w2t, K_w2t = w2t_hbm.shape
125
+ _kernel_assert(R >= 1 and K >= 1 and H >= 1, f"R({R}) K({K}) H({H}) must be >= 1")
126
+ _kernel_assert(K_w1t == K, f"w1t in-dim {K_w1t} != K {K}; expected w1t = w1.T of shape [K, H]")
127
+ _kernel_assert(K_w2t == K, f"w2t out-dim {K_w2t} != K {K}; expected w2t = w2.T of shape [H, K]")
128
+ _kernel_assert(H_w2t == H, f"w2t in-dim {H_w2t} != H {H}")
129
+ _kernel_assert(
130
+ R % LNC == 0,
131
+ f"the {LNC}-core kernel needs R divisible by {LNC}, got R={R}. "
132
+ f"program_id is a symbolic value, so a partial final row block cannot be "
133
+ f"clamped; use the one-core pre-transposed kernel for uneven R",
134
+ )
135
+
136
+ rows_per = R // LNC
137
+ pid = nl.program_id(0)
138
+ r_base = pid * rows_per
139
+
140
+ out_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
141
+
142
+ n_k = _div_ceil(K, _P_MAX) # K tiles (fc1 contraction)
143
+ n_h = _div_ceil(H, _P_MAX) # H tiles (fc1 out-rows / fc2 contraction)
144
+
145
+ # b1 FIRST, before the 16 MB of weight DMA: 32 [128,1] tiles issue at t=0 and land in
146
+ # ~1 us, so every gelu's bias is resident long before the first matmul retires.
147
+ b1_tiles = []
148
+ for ht in range(n_h):
149
+ h0 = ht * _P_MAX
150
+ ph = min(_P_MAX, H - h0)
151
+ t = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
152
+ nisa.dma_copy(dst=t[0:ph, 0:1], src=b1_hbm[h0:h0 + ph])
153
+ b1_tiles.append(t)
154
+
155
+ # Weight staging (pre-transposed bf16, one DMA per tile).
156
+ w1t_tiles = []
157
+ for kt in range(n_k):
158
+ k0 = kt * _P_MAX
159
+ pk = min(_P_MAX, K - k0)
160
+ t = nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf)
161
+ nisa.dma_copy(dst=t[0:pk, :], src=w1t_hbm[k0:k0 + pk, :])
162
+ w1t_tiles.append(t)
163
+
164
+ w2t_tiles = []
165
+ for ht in range(n_h):
166
+ h0 = ht * _P_MAX
167
+ ph = min(_P_MAX, H - h0)
168
+ t = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
169
+ nisa.dma_copy(dst=t[0:ph, :], src=w2t_hbm[h0:h0 + ph, :])
170
+ w2t_tiles.append(t)
171
+
172
+ # Main fused loop.
173
+ rb_size = _F_MOV_MAX
174
+ n_rb = _div_ceil(rows_per, rb_size)
175
+ n_ko = _div_ceil(K, _F_MOV_MAX)
176
+
177
+ # THE X-PRESTAGE: stage EVERY row block's x here, in rb order, so rb=1's serialised
178
+ # DMA chain runs inside the pre-K4 seam (Tensor dark) instead of inside the idle
179
+ # window at the rb0->rb1 boundary. Appending rb=1's stages after rb=0's leaves rb=0's
180
+ # stages exactly where they are on the Sync engine's sub-stream.
181
+ xt_sets = []
182
+ for rb_pre in range(n_rb):
183
+ xt_sets.append(_stage_xt(
184
+ x_hbm, r_base + rb_pre * rb_size,
185
+ min(rb_size, rows_per - rb_pre * rb_size), n_k, K, rb_size))
186
+
187
+ for rb in range(n_rb):
188
+ r_local = rb * rb_size
189
+ pr = min(rb_size, rows_per - r_local)
190
+ n_rs = _div_ceil(pr, _P_MAX)
191
+ r_abs = r_base + r_local
192
+
193
+ # x_T bf16 tiles: consume the prologue-staged set for this row block.
194
+ xt_tiles = xt_sets[rb]
195
+
196
+ # ---- fc1: gt_tiles[ht] = gelu(w1 @ x.T + b1), bf16, SBUF-resident ----
197
+ gt_tiles = []
198
+ for ht in range(n_h):
199
+ h0 = ht * _P_MAX
200
+ ph = min(_P_MAX, H - h0)
201
+ # Declared INSIDE the ht loop, so the ring never reuses a written PSUM tile.
202
+ # nc_matmul's default accumulate=None auto-detects "previously written" and
203
+ # would accumulate onto a stale value.
204
+ y1_psum = nl.ndarray((_P_MAX, rb_size), dtype=nl.float32, buffer=nl.psum)
205
+ for kt in nl.affine_range(n_k):
206
+ k0 = kt * _P_MAX
207
+ pk = min(_P_MAX, K - k0)
208
+ nisa.nc_matmul(
209
+ dst=y1_psum[0:ph, 0:pr],
210
+ stationary=w1t_tiles[kt][0:pk, h0:h0 + ph],
211
+ moving=xt_tiles[kt][0:pk, 0:pr],
212
+ )
213
+ # b1 comes from the prologue-resident tile.
214
+ gt = nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf)
215
+ nisa.activation(dst=gt[0:ph, 0:pr], op=nl.gelu,
216
+ data=y1_psum[0:ph, 0:pr], bias=b1_tiles[ht][0:ph, 0:1])
217
+ gt_tiles.append(gt)
218
+
219
+ # ---- fc2: out[r,k] = sum_h g[r,h] * w2[k,h] + b2[k] ----
220
+ for ko in range(n_ko):
221
+ ko0 = ko * _F_MOV_MAX
222
+ nk = min(_F_MOV_MAX, K - ko0)
223
+ out_psums = []
224
+ for _ in range(n_rs):
225
+ out_psums.append(
226
+ nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
227
+ )
228
+ for ht in nl.affine_range(n_h):
229
+ h0 = ht * _P_MAX
230
+ ph = min(_P_MAX, H - h0)
231
+ for rs in range(n_rs):
232
+ rr0 = rs * _P_MAX
233
+ prs = min(_P_MAX, pr - rr0)
234
+ nisa.nc_matmul(
235
+ dst=out_psums[rs][0:prs, 0:nk],
236
+ stationary=gt_tiles[ht][0:ph, rr0:rr0 + prs],
237
+ moving=w2t_tiles[ht][0:ph, ko0:ko0 + nk],
238
+ )
239
+ # b2 broadcast, hoisted out of the rs loop (removing the broadcast itself
240
+ # cost 23% and 30% in two separate attempts).
241
+ b2_bc = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.sbuf)
242
+ nisa.dma_copy(dst=b2_bc[0:_P_MAX, 0:nk],
243
+ src=b2_hbm.ap(pattern=[[0, _P_MAX], [1, nk]], offset=ko0))
244
+ for rs in range(n_rs):
245
+ rr0 = rs * _P_MAX
246
+ prs = min(_P_MAX, pr - rr0)
247
+ out_sb = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=_BF16, buffer=nl.sbuf)
248
+ nisa.tensor_tensor(dst=out_sb[0:prs, 0:nk],
249
+ data1=out_psums[rs][0:prs, 0:nk],
250
+ data2=b2_bc[0:prs, 0:nk],
251
+ op=nl.add)
252
+ nisa.dma_copy(
253
+ dst=out_hbm[r_abs + rr0:r_abs + rr0 + prs, ko0:ko0 + nk],
254
+ src=out_sb[0:prs, 0:nk],
255
+ )
256
+
257
+ return out_hbm
258
+
259
+
260
+ def _no_grad(*tensors) -> None:
261
+ """Refuse gradient-tracking inputs -- this op registers no autograd rule."""
262
+ for t in tensors:
263
+ if t is not None and t.requires_grad:
264
+ raise RuntimeError(
265
+ "nki_mlp_k4_xp registers no autograd rule (INFERENCE ONLY). "
266
+ "Use under torch.no_grad()/eval()."
267
+ )
268
+
269
+
270
+ @nki_op("vjepa2::nki_mlp_k4_xp", mutates_args={})
271
+ def nki_mlp_k4_xp(
272
+ x: torch.Tensor,
273
+ w1t: torch.Tensor,
274
+ b1: torch.Tensor,
275
+ w2t: torch.Tensor,
276
+ b2: torch.Tensor,
277
+ ) -> torch.Tensor:
278
+ """Fused MLP on LNC=2, pre-transposed bf16 weights, x prestaged in the prologue.
279
+
280
+ Args:
281
+ x : [R, K] **bf16** on 'neuron'. ``R`` must be divisible by :data:`LNC`.
282
+ w1t : [K, H] bf16 == fc1.weight.T. b1 : [H] float32.
283
+ w2t : [H, K] bf16 == fc2.weight.T. b2 : [K] float32.
284
+
285
+ Returns:
286
+ [R, K] bf16.
287
+ """
288
+ _no_grad(x, w1t, b1, w2t, b2)
289
+ return _mlp_k4_xp_kernel[LNC](x, w1t, b1, w2t, b2)
290
+
291
+
292
+ #: Export alias. Same object as the nki_op above, so the runtime
293
+ #: op-name string ("vjepa2::nki_mlp_k4_xp") is unchanged whichever name is used.
294
+ mlp = nki_mlp_k4_xp
build/torch-neuron/nki_kernels/res_layernorm.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ LNC = 2
56
+
57
+ #: Ring depth: number of rotating SBUF tile sets that overlap load with compute.
58
+ _RING = 4
59
+
60
+
61
+ def _kernel_assert(condition: bool, error_text: str):
62
+ """Assert with an NKI-formatted error message."""
63
+ assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
64
+
65
+
66
+ @nki.jit
67
+ def _res_layernorm_fold_kernel(a_hbm, b_hbm, eps):
68
+ """Emit the fold kernel body: ``(a + sum(b), LN_no_affine(a + sum(b)))``, token-major.
69
+
70
+ gamma/beta live in the consumers' packed weights, so this kernel takes no affine
71
+ parameters. Rows are sharded across ``LNC`` cores; each token tile is loaded through
72
+ a depth-4 ring, summed in fp32, normalized with the thinned stat chain, and the
73
+ normalized result is cast to bf16 on write.
74
+
75
+ Args:
76
+ a_hbm (nl.ndarray): [R, H] @ HBM float32 -- the residual stream.
77
+ b_hbm (nl.ndarray): [B, R, H] @ HBM bf16 -- B branch tensors (1 <= B <= 4), summed in.
78
+ eps (float): LayerNorm epsilon.
79
+
80
+ Returns:
81
+ tuple: ``(x_new [R, H] float32, z [R, H] bf16)`` both in shared HBM.
82
+ """
83
+ R, H = a_hbm.shape
84
+ n_b, Rb, Hb = b_hbm.shape
85
+
86
+ _kernel_assert(R >= 1 and H >= 1, f"R({R}) H({H}) must be >= 1")
87
+ _kernel_assert(Rb == R and Hb == H, f"b {(Rb, Hb)} must match a {(R, H)}")
88
+ _kernel_assert(1 <= n_b <= 4, f"b must stack 1..4 branches, got {n_b}")
89
+ _kernel_assert(
90
+ H <= 16384,
91
+ f"H={H} is one instruction's free axis here; no chunking is implemented.",
92
+ )
93
+ _kernel_assert(
94
+ R % (LNC * _P_MAX) == 0,
95
+ f"need R divisible by LNC*{_P_MAX} = {LNC * _P_MAX}, got R={R}",
96
+ )
97
+
98
+ rows_per = R // LNC
99
+ n_t = rows_per // _P_MAX
100
+ pid = nl.program_id(0)
101
+ r_base = pid * rows_per
102
+
103
+ x_new_hbm = nl.ndarray((R, H), dtype=nl.float32, buffer=nl.shared_hbm)
104
+ z_hbm = nl.ndarray((R, H), dtype=_BF16, buffer=nl.shared_hbm)
105
+
106
+ inv_h = 1.0 / float(H)
107
+ neg_inv_h = -inv_h
108
+
109
+ # Ring buffers: _RING rotating sets of (a tile, B branch tiles, sum tile, z tile).
110
+ a_sb = []
111
+ b_sb = []
112
+ s_sb = []
113
+ z_sb = []
114
+ for _i in range(_RING):
115
+ a_sb.append(nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf))
116
+ _slot = []
117
+ for _j in range(n_b):
118
+ _slot.append(nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf))
119
+ b_sb.append(_slot)
120
+ s_sb.append(nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf))
121
+ z_sb.append(nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf))
122
+
123
+ # Scratch destination for the reduce activations (its full tile is discarded; only
124
+ # the reduce result is consumed).
125
+ sink = nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf)
126
+
127
+ # Per-lane [128, 1] statistics.
128
+ acc = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
129
+ neg_shift = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
130
+ neg_mu = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
131
+ var = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
132
+ rstd = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
133
+
134
+ for t in range(n_t):
135
+ k = t % _RING
136
+ r0 = r_base + t * _P_MAX
137
+
138
+ # ---- load residual + branches for this tile ----
139
+ nisa.dma_copy(dst=a_sb[k], src=a_hbm[r0:r0 + _P_MAX, 0:H])
140
+ for j in range(n_b):
141
+ nisa.dma_copy(dst=b_sb[k][j], src=b_hbm[j, r0:r0 + _P_MAX, 0:H])
142
+
143
+ # ---- x_new = a + sum_j b[j] in fp32, then write the residual stream out ----
144
+ nisa.tensor_tensor(dst=s_sb[k], data1=a_sb[k], data2=b_sb[k][0], op=nl.add)
145
+ for j in range(1, n_b):
146
+ nisa.tensor_tensor(dst=s_sb[k], data1=s_sb[k], data2=b_sb[k][j], op=nl.add)
147
+
148
+ nisa.dma_copy(dst=x_new_hbm[r0:r0 + _P_MAX, 0:H], src=s_sb[k])
149
+
150
+ # ---- thinned stat chain: mean via shift-and-reduce, variance, rstd ----
151
+ nisa.tensor_scalar(dst=neg_shift, data=s_sb[k][0:_P_MAX, 0:1],
152
+ op0=nl.multiply, operand0=-1.0)
153
+ nisa.activation_reduce(dst=sink, op=nl.copy, data=s_sb[k],
154
+ reduce_op=nl.add, reduce_res=acc,
155
+ bias=neg_shift, scale=1.0)
156
+ nisa.tensor_scalar(dst=neg_mu, data=acc, op0=nl.multiply, operand0=neg_inv_h,
157
+ op1=nl.add, operand1=neg_shift)
158
+ nisa.activation_reduce(dst=sink, op=nl.square, data=s_sb[k],
159
+ reduce_op=nl.add, reduce_res=acc, bias=neg_mu, scale=1.0)
160
+ nisa.tensor_scalar(dst=var, data=acc, op0=nl.multiply, operand0=inv_h,
161
+ op1=nl.add, operand1=eps)
162
+ nisa.activation(dst=rstd, op=nl.rsqrt, data=var)
163
+
164
+ # ---- one-op affine-free apply: z = (x_new + neg_mu) * rstd, bf16 cast on write.
165
+ # Both operands are per-lane [128, 1] vectors; the fused two-op tensor_scalar
166
+ # costs the same as one op.
167
+ nisa.tensor_scalar(dst=z_sb[k], data=s_sb[k],
168
+ op0=nl.add, operand0=neg_mu,
169
+ op1=nl.multiply, operand1=rstd)
170
+
171
+ nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:H], src=z_sb[k])
172
+
173
+ return x_new_hbm, z_hbm
174
+
175
+
176
+ def _no_grad(*tensors: torch.Tensor) -> None:
177
+ """Refuse gradient-tracking inputs -- this op registers no autograd rule."""
178
+ for t in tensors:
179
+ if t.requires_grad:
180
+ raise RuntimeError(
181
+ "nki_res_layernorm_fold registers no autograd rule. Use under "
182
+ "torch.no_grad()/eval()."
183
+ )
184
+
185
+
186
+ @nki_op("vjepa2::nki_res_layernorm_fold", mutates_args={})
187
+ def nki_res_layernorm_fold(
188
+ a: torch.Tensor,
189
+ b: torch.Tensor,
190
+ eps: float,
191
+ ) -> tuple[torch.Tensor, torch.Tensor]:
192
+ """Fused residual add + affine-free LayerNorm on LNC=2 (gamma/beta folded away).
193
+
194
+ The consumers MUST have been packed with folded weights; feeding ``z`` to unfolded
195
+ weights silently drops the affine.
196
+
197
+ Args:
198
+ a: [R, H] float32 on 'neuron'. ``R`` divisible by ``LNC * 128``.
199
+ b: [B, R, H] bf16 on 'neuron'. B branch tensors (1 <= B <= 4), summed in.
200
+ eps: LayerNorm epsilon.
201
+
202
+ Returns:
203
+ ``(x_new [R, H] float32, z [R, H] bf16)``.
204
+ """
205
+ _no_grad(a, b)
206
+ return _res_layernorm_fold_kernel[LNC](a, b, eps)
207
+
208
+
209
+ #: Export alias. Same object as the nki_op above, so the runtime
210
+ #: op-name string ("vjepa2::nki_res_layernorm_fold") is unchanged whichever name is used.
211
+ res_layernorm_fold = nki_res_layernorm_fold
build/torch-neuron/nki_kernels/rope_tables.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Host-side 3D RoPE cos / signed-sin tables for the attention block.
2
+
3
+ Pure torch, computed once at install time per model (positions depend only on the token grid).
4
+ The attention block consumes these through ``pack_tables`` (see attention_block.py).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import torch
10
+
11
+ __all__ = ["build_rope_tables"]
12
+
13
+
14
+ def build_rope_tables(sub_dims, positions, head_dim, dtype, device):
15
+ """Build faithful cos + signed-sin tables [N, head_dim] for 3D RoPE.
16
+
17
+ Args:
18
+ sub_dims : (d_dim, h_dim, w_dim) sub-block widths (each even).
19
+ positions : (pos_d, pos_h, pos_w) each [N] int/long token positions.
20
+ head_dim : full head dim D (>= sum(sub_dims)); remainder is passthrough.
21
+ dtype, device: output dtype/device.
22
+
23
+ Returns:
24
+ (cos, sinsigned): each [N, head_dim] float32:
25
+ cos[n,k]=cos(pos_axis*omega[k%half]) for rotated dims, else 1.
26
+ sinsigned[n,k]=g[k]*sin(...) for rotated dims, else 0; g=[-1,+1,...].
27
+ """
28
+ (pos_d, pos_h, pos_w) = positions
29
+ N = pos_d.shape[0]
30
+ cos = torch.ones(N, head_dim, dtype=torch.float32, device=device)
31
+ sin = torch.zeros(N, head_dim, dtype=torch.float32, device=device)
32
+ s = 0
33
+ for sub, pos in zip(sub_dims, (pos_d, pos_h, pos_w)):
34
+ half = sub // 2
35
+ omega = torch.arange(half, dtype=torch.float32, device=device) / float(half)
36
+ omega = 1.0 / (10000.0 ** omega) # [half]
37
+ freq = pos.to(device=device, dtype=torch.float32).unsqueeze(-1) * omega # [N, half]
38
+ c, sn = freq.cos(), freq.sin()
39
+ cos[:, s:s + half] = c
40
+ cos[:, s + half:s + sub] = c
41
+ sin[:, s:s + half] = sn
42
+ sin[:, s + half:s + sub] = sn
43
+ s += sub
44
+ g = torch.ones(head_dim, dtype=torch.float32, device=device)
45
+ g[0::2] = -1.0
46
+ sinsigned = sin * g
47
+ return cos.to(dtype), sinsigned.to(dtype)