v1: V-JEPA2 ViT-L encoder NKI kernels for Trainium2 (4.35x, 44% MFU)
Browse files- .gitattributes +4 -2
- CHANGELOG.md +26 -0
- LICENSE +202 -0
- NOTICE +8 -0
- README.md +143 -0
- build/torch-neuron/__init__.py +27 -0
- build/torch-neuron/layers.py +172 -0
- build/torch-neuron/metadata.json +1 -0
- build/torch-neuron/nki_kernels/__init__.py +19 -0
- build/torch-neuron/nki_kernels/attention_block.py +709 -0
- build/torch-neuron/nki_kernels/attention_inner.py +1214 -0
- build/torch-neuron/nki_kernels/mlp.py +294 -0
- build/torch-neuron/nki_kernels/res_layernorm.py +211 -0
- build/torch-neuron/nki_kernels/rope_tables.py +47 -0
.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)
|