v1: NKI kernels for RoPE ViT encoders (DINOv3, V-JEPA2) on Trainium2
Browse files- .gitattributes +4 -2
- CHANGELOG.md +20 -0
- LICENSE +202 -0
- NOTICE +13 -0
- README.md +206 -0
- build/torch-neuron/__init__.py +37 -0
- build/torch-neuron/encoder.py +237 -0
- build/torch-neuron/layers.py +220 -0
- build/torch-neuron/metadata.json +1 -0
- build/torch-neuron/nki_kernels/__init__.py +23 -0
- build/torch-neuron/nki_kernels/attention_block.py +801 -0
- build/torch-neuron/nki_kernels/attention_block_l1.py +402 -0
- build/torch-neuron/nki_kernels/attention_block_l1_ln.py +451 -0
- build/torch-neuron/nki_kernels/attention_inner.py +1220 -0
- build/torch-neuron/nki_kernels/mlp.py +296 -0
- build/torch-neuron/nki_kernels/mlp_res_ln.py +308 -0
- build/torch-neuron/nki_kernels/res_layernorm.py +213 -0
- build/torch-neuron/nki_kernels/swiglu_stream.py +291 -0
- examples/dinov3_features.py +22 -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,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Changelog
|
| 2 |
+
|
| 3 |
+
## v1 (2026-10)
|
| 4 |
+
|
| 5 |
+
First release.
|
| 6 |
+
|
| 7 |
+
* Drop-in layers for Hugging Face Transformers: `NeuronDINOv3ViTModel` (DINOv3 ViT-S/16, S+/16, B/16,
|
| 8 |
+
L/16, H+/16) and `NeuronVJEPA2Encoder` (V-JEPA2 ViT-L, any batch size, up to 2048 tokens per clip).
|
| 9 |
+
* Model-agnostic encoder loop: `BlockWeights`, `pack_block`, `rope_tables`, `EncoderRunner`.
|
| 10 |
+
* Kernels: `attention_block` (per head group; batched; padded keys skipped; head_dim 64 or 128;
|
| 11 |
+
any head count), `attention_block_l1` and `attention_block_l1_ln` (LNC=1, all head groups, optional
|
| 12 |
+
residual + LayerNorm epilogue), `res_layernorm_fold`, `mlp` and `mlp_res_ln` (GELU MLP, optional
|
| 13 |
+
residual + next LayerNorm epilogue), `swiglu_stream` and `swiglu_stream_ln` (weight-streaming
|
| 14 |
+
SwiGLU FFN).
|
| 15 |
+
* RoPE pairing selectable at packing time: `rope="half"` (rotate-half) or `rope="interleaved"`.
|
| 16 |
+
* Verified on trn2.3xlarge, PyTorch Native Beta 6 (torch 2.13.0, torch-neuronx 2.13.3, NKI 0.7.0b1),
|
| 17 |
+
Transformers 4.57.6, LNC=1 and LNC=2. Every measured case passes rel_l2 <= 5e-2 vs the fp32 CPU model.
|
| 18 |
+
|
| 19 |
+
Known limitations: encoder (bidirectional) attention with RoPE only; LayerNorm (not RMSNorm);
|
| 20 |
+
<= 2048 tokens per image; inference only; hidden size 4096 (ViT-7B) not covered by the drop-in layer.
|
LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
NOTICE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
vit-encoder-neuron-kernels -- NKI kernels for RoPE ViT encoders on AWS Trainium2
|
| 2 |
+
Licensed under the Apache License, Version 2.0 (see LICENSE).
|
| 3 |
+
|
| 4 |
+
nki_kernels/attention_block.py, res_layernorm.py and mlp.py are modified versions of the V-JEPA2
|
| 5 |
+
encoder kernels published at huggingface.co/kernels/jburtoft/vjepa2-neuron-kernels (Apache-2.0),
|
| 6 |
+
developed by Antonio Mena. Modifications: selectable RoPE pairing, any head count, head_dim 128,
|
| 7 |
+
ragged token tiles and padded-key skipping, multi-image launches, resident weights, LNC=1 grid plan.
|
| 8 |
+
nki_kernels/attention_block_l1.py, attention_block_l1_ln.py and mlp_res_ln.py are derived from them.
|
| 9 |
+
nki_kernels/swiglu_stream.py, encoder.py and layers.py are new.
|
| 10 |
+
|
| 11 |
+
nki_kernels/attention_inner.py is a modified copy of the AWS Neuron SDK's nkilib
|
| 12 |
+
core/attention/attention_cte.py, Copyright Amazon.com, Inc. or its affiliates, licensed under the
|
| 13 |
+
Apache License, Version 2.0. See the file header for the list of modifications.
|
README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: kernels
|
| 4 |
+
tags:
|
| 5 |
+
- kernel
|
| 6 |
+
- neuron
|
| 7 |
+
- nki
|
| 8 |
+
- trainium
|
| 9 |
+
- trn2
|
| 10 |
+
- vision
|
| 11 |
+
- vit
|
| 12 |
+
- dinov3
|
| 13 |
+
- vjepa2
|
| 14 |
+
- rope
|
| 15 |
+
- swiglu
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
# vit-encoder-neuron-kernels
|
| 19 |
+
|
| 20 |
+
NKI kernels that run the whole transformer stack of **RoPE vision-transformer encoders** on AWS
|
| 21 |
+
Trainium2 (trn2). Each block is 2-3 coarse kernels instead of dozens of small ops: attention plus
|
| 22 |
+
the residual add and LayerNorm, and the MLP / SwiGLU FFN plus the next block's LayerNorm. The
|
| 23 |
+
residual stream stays in fp32 between kernels.
|
| 24 |
+
|
| 25 |
+
Drop-in layers are included for two Hugging Face Transformers models:
|
| 26 |
+
|
| 27 |
+
| Layer | Replaces | Models |
|
| 28 |
+
|---|---|---|
|
| 29 |
+
| `NeuronDINOv3ViTModel` | `DINOv3ViTModel.forward` | DINOv3 ViT-S/16, S+/16, B/16, L/16, H+/16 |
|
| 30 |
+
| `NeuronVJEPA2Encoder` | `VJEPA2Encoder.forward` | V-JEPA2 ViT-L (video), up to 2048 tokens per clip |
|
| 31 |
+
|
| 32 |
+
The building blocks (`BlockWeights` / `pack_block` / `EncoderRunner`) are model-agnostic. Any
|
| 33 |
+
pre-norm ViT with the shape constraints below can use them, but only the two models above
|
| 34 |
+
have been validated.
|
| 35 |
+
|
| 36 |
+
## Performance (trn2.3xlarge, bf16, `torch.compile(backend="neuron")`)
|
| 37 |
+
|
| 38 |
+
Measured 2026-10 on PyTorch Native Beta 6 (torch 2.13.0, torch-neuronx 2.13.3, NKI 0.7.0b1).
|
| 39 |
+
Real Hugging Face weights. "Stock" is the same model compiled with
|
| 40 |
+
`torch.compile(backend="neuron")` without these kernels: DINOv3 cast to bf16, V-JEPA2 as its fp32
|
| 41 |
+
checkpoint (stock V-JEPA2 is the encoder as shipped, and its accuracy is in the table below).
|
| 42 |
+
|
| 43 |
+
**Per instance** (one process per logical core, all cores busy):
|
| 44 |
+
|
| 45 |
+
| Model | Resolution | Stock (img/s) | **These kernels (img/s)** | Speedup |
|
| 46 |
+
|---|---|---|---|---|
|
| 47 |
+
| DINOv3 ViT-B/16 | 224 | 2,901.5 | **4,105.2** | 1.41x |
|
| 48 |
+
| DINOv3 ViT-L/16 | 224 | 1,061.2 | **1,767.5** | 1.67x |
|
| 49 |
+
| DINOv3 ViT-L/16 | 448 | 201.1 | **491.9** | 2.45x |
|
| 50 |
+
| DINOv3 ViT-H+/16 | 224 | 400.0 | **626.6** | 1.57x |
|
| 51 |
+
| DINOv3 ViT-H+/16 | 448 | 106.8 | **200.3** | 1.88x |
|
| 52 |
+
| V-JEPA2 ViT-L, 16 frames x 256 px | - | 36.8 clips/s | **157.3 clips/s** | 4.27x |
|
| 53 |
+
|
| 54 |
+
Configuration: LNC=1 (8 logical cores) for every row except V-JEPA2 stock, whose best was LNC=2
|
| 55 |
+
(4 cores). DINOv3 kernels run at batch 8 (224 px) or batch 2 (448 px). DINOv3 stock runs at batch 4
|
| 56 |
+
(224 px) or batch 1 (448 px), its best measured. V-JEPA2 runs at batch 1 per core for both.
|
| 57 |
+
|
| 58 |
+
**Single core, latency-oriented** (LNC=2, batch 1):
|
| 59 |
+
|
| 60 |
+
| Model | Resolution | Stock | **These kernels** | Speedup |
|
| 61 |
+
|---|---|---|---|---|
|
| 62 |
+
| DINOv3 ViT-B/16 | 224 | 251.0 img/s | **510.7 img/s** | 2.03x |
|
| 63 |
+
| DINOv3 ViT-L/16 | 224 | 97.1 img/s | **220.3 img/s** (4.5 ms) | 2.27x |
|
| 64 |
+
| DINOv3 ViT-L/16 | 448 | 25.1 img/s | **83.0 img/s** | 3.31x |
|
| 65 |
+
| DINOv3 ViT-H+/16 | 224 | 21.4 img/s | **88.2 img/s** | 4.12x |
|
| 66 |
+
| DINOv3 ViT-H+/16 | 448 | 17.7 img/s | **32.4 img/s** | 1.83x |
|
| 67 |
+
| V-JEPA2 ViT-L, 16 frames x 256 px | - | 101.5 ms/clip | **25.8 ms/clip** | 3.93x |
|
| 68 |
+
|
| 69 |
+
The gain grows with resolution: attention cost grows with token count, and these kernels keep
|
| 70 |
+
attention and its surroundings on chip. At 224 px with large batches the gap narrows. For example,
|
| 71 |
+
stock ViT-L/16 at LNC=2 batch 4 is 252 img/s and the kernels give 321 img/s (1.27x).
|
| 72 |
+
|
| 73 |
+
## Accuracy
|
| 74 |
+
|
| 75 |
+
Every result above passes a per-image check against the fp32 CPU model (rel_l2 <= 5e-2).
|
| 76 |
+
On DINOv3 the kernels are **closer to fp32 than stock bf16**, because the residual stream and
|
| 77 |
+
LayerNorm statistics stay in fp32:
|
| 78 |
+
|
| 79 |
+
| Model | Stock bf16 rel_l2 vs fp32 | These kernels |
|
| 80 |
+
|---|---|---|
|
| 81 |
+
| DINOv3 ViT-B/16 | 1.4-1.8e-2 | 0.9-1.7e-2 |
|
| 82 |
+
| DINOv3 ViT-L/16 | 1.3-1.6e-2 | 0.8-1.0e-2 |
|
| 83 |
+
| DINOv3 ViT-H+/16 | 1.0-1.7e-2 | 0.6-1.0e-2 |
|
| 84 |
+
| V-JEPA2 ViT-L | 1.4e-2 (fp32 model, compiler-chosen precision) | 3.9e-2 (bf16 matmuls) |
|
| 85 |
+
|
| 86 |
+
For V-JEPA2 the stock path compiles the fp32 checkpoint and keeps more of it in higher precision,
|
| 87 |
+
so it is closer to fp32 than the kernels (which run every matmul in bf16).
|
| 88 |
+
|
| 89 |
+
## Quick start
|
| 90 |
+
|
| 91 |
+
```python
|
| 92 |
+
import types, torch
|
| 93 |
+
from kernels import get_kernel
|
| 94 |
+
from transformers import AutoModel
|
| 95 |
+
|
| 96 |
+
k = get_kernel("jburtoft/vit-encoder-neuron-kernels", version=1, trust_remote_code=True)
|
| 97 |
+
|
| 98 |
+
model = AutoModel.from_pretrained("facebook/dinov3-vitl16-pretrain-lvd1689m",
|
| 99 |
+
dtype=torch.float32).eval().to("neuron")
|
| 100 |
+
model.forward = types.MethodType(k.NeuronDINOv3ViTModel.forward, model)
|
| 101 |
+
model = torch.compile(model, backend="neuron")
|
| 102 |
+
|
| 103 |
+
with torch.no_grad():
|
| 104 |
+
out = model(pixel_values.to("neuron")) # [B, 3, H, W] fp32, any batch size
|
| 105 |
+
feats = out.last_hidden_state # [B, 1 + 4 registers + patches, 1024]
|
| 106 |
+
cls = out.pooler_output
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
V-JEPA2:
|
| 110 |
+
|
| 111 |
+
```python
|
| 112 |
+
model = AutoModel.from_pretrained("facebook/vjepa2-vitl-fpc64-256", dtype=torch.float32).eval().to("neuron")
|
| 113 |
+
model.encoder.forward = types.MethodType(k.NeuronVJEPA2Encoder.forward, model.encoder)
|
| 114 |
+
enc = torch.compile(model.encoder, backend="neuron")
|
| 115 |
+
emb = enc(clip.to("neuron")).last_hidden_state # clip [B, 16, 3, 256, 256] -> [B, 2048, 1024]
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
Notes:
|
| 119 |
+
* Load the model in **fp32**. The layer folds LayerNorm affines and LayerScale into the weights
|
| 120 |
+
once, on the first device forward, and casts the weights to bf16 itself.
|
| 121 |
+
* **`torch.compile(backend="neuron")` is part of the configuration.** The layers set
|
| 122 |
+
`can_torch_compile = True`.
|
| 123 |
+
* **Set `NEURON_LOGICAL_NC_CONFIG` before importing**, and to the same value the process runs
|
| 124 |
+
with: `2` (default) or `1`. The kernels are built for that core configuration. For LNC=1 also pass
|
| 125 |
+
`NEURON_CC_FLAGS=--lnc=1`.
|
| 126 |
+
* `NKI_ENABLE_TRACE_CACHE=0` is recommended when you edit kernels. The cross-process trace cache
|
| 127 |
+
can otherwise reuse a stale NEFF.
|
| 128 |
+
* On a vLLM-Neuron venv (CUDA-tagged torch), `import libtorch_neuronx_lite` before `get_kernel`
|
| 129 |
+
so the `torch-neuron` variant is selected. On PyTorch Native this is not needed.
|
| 130 |
+
|
| 131 |
+
Which LNC to use: **LNC=1** (8 logical cores, one process per core) gives the most throughput per
|
| 132 |
+
instance. **LNC=2** (4 larger cores) gives the lowest latency per request.
|
| 133 |
+
|
| 134 |
+
## Kernels
|
| 135 |
+
|
| 136 |
+
| Kernel | What it does |
|
| 137 |
+
|---|---|
|
| 138 |
+
| `attention_block` | LN'd hidden -> Q/K/V projection + RoPE + attention + output projection, one 8-head (D=64) or 4-head (D=128) group per program; any number of images per launch; padded keys skipped |
|
| 139 |
+
| `attention_block_l1` | LNC=1: one program over **all** head groups, one output (no per-group partials) |
|
| 140 |
+
| `attention_block_l1_ln` | `attention_block_l1` plus residual add and LayerNorm in the epilogue |
|
| 141 |
+
| `res_layernorm_fold` | residual add (sum of up to 4 branches) + affine-free LayerNorm |
|
| 142 |
+
| `mlp` | GELU MLP (fc1 -> GELU -> fc2) |
|
| 143 |
+
| `mlp_res_ln` | `mlp` plus residual add and the next block's LayerNorm in the epilogue |
|
| 144 |
+
| `swiglu_stream` | SwiGLU FFN that streams the weights in chunks of the intermediate dimension (so large FFNs fit on chip) |
|
| 145 |
+
| `swiglu_stream_ln` | `swiglu_stream` plus residual add and the next block's LayerNorm |
|
| 146 |
+
|
| 147 |
+
Host-side packing (`pack_weights`, `pack_tables`, `lane_permutation`, `pack_block`) supports both
|
| 148 |
+
RoPE pairings via `rope=`:
|
| 149 |
+
* `"half"`: pairs lane `j` with `j + D/2` (rotate-half; DINOv3, EVA-02, HF Llama style).
|
| 150 |
+
* `"interleaved"`: pairs lane `2j` with `2j + 1` (V-JEPA2 style).
|
| 151 |
+
|
| 152 |
+
## Supported shapes and limits
|
| 153 |
+
|
| 154 |
+
* **Bidirectional (encoder) self-attention with RoPE.** No causal masks, KV cache, cross-attention,
|
| 155 |
+
or learned / absolute position embeddings. A model without RoPE could pass identity tables (cos=1,
|
| 156 |
+
sin=0), but that is untested.
|
| 157 |
+
* **Pre-norm blocks with standard LayerNorm** (not RMSNorm), optional LayerScale.
|
| 158 |
+
* hidden size a multiple of 128; head_dim 64 or 128; any head count (padded to a multiple of 8
|
| 159 |
+
at D=64 or 4 at D=128).
|
| 160 |
+
* **<= 2048 tokens per image** after padding to a multiple of 128 x LNC (e.g. 720 x 720 px at
|
| 161 |
+
patch 16). Longer inputs fall back to the stock forward.
|
| 162 |
+
* FFN: GELU MLP (`nl.gelu`, the erf form) or SwiGLU (SiLU gate).
|
| 163 |
+
* Inference only (no autograd). The layers refuse in training mode.
|
| 164 |
+
* ViT-7B-sized models (hidden 4096) are not covered by the drop-in layer. They need tensor parallelism.
|
| 165 |
+
The streaming SwiGLU kernel is slower than the compiler's FFN at that width.
|
| 166 |
+
* Validated end to end on DINOv3 ViT-B/L/H+ and V-JEPA2 ViT-L only.
|
| 167 |
+
|
| 168 |
+
## Requirements
|
| 169 |
+
|
| 170 |
+
* trn2 (verified on trn2.3xlarge, LNC=1 and LNC=2).
|
| 171 |
+
* PyTorch Native (TorchNeuron) with NKI 0.7 and `nkilib`. Verified on Beta 6 (torch 2.13.0,
|
| 172 |
+
torch-neuronx 2.13.3, NKI 0.7.0b1, neuronx-cc 2.0.404056).
|
| 173 |
+
* Transformers with DINOv3 / V-JEPA2 (verified on 4.57.6); `kernels` for `get_kernel`.
|
| 174 |
+
|
| 175 |
+
## Repository layout
|
| 176 |
+
|
| 177 |
+
```
|
| 178 |
+
build/torch-neuron/
|
| 179 |
+
├── __init__.py # public API
|
| 180 |
+
├── metadata.json
|
| 181 |
+
├── layers.py # NeuronDINOv3ViTModel, NeuronVJEPA2Encoder
|
| 182 |
+
├── encoder.py # BlockWeights, pack_block, rope_tables, EncoderRunner
|
| 183 |
+
└── nki_kernels/
|
| 184 |
+
├── attention_block.py # attention (per head group) + host-side packing
|
| 185 |
+
├── attention_block_l1.py # attention, all head groups (LNC=1)
|
| 186 |
+
├── attention_block_l1_ln.py # ... + residual + LayerNorm epilogue
|
| 187 |
+
├── attention_inner.py # modified copy of nkilib attention_cte (vendor attention core)
|
| 188 |
+
├── res_layernorm.py # residual + affine-free LayerNorm
|
| 189 |
+
├── mlp.py # GELU MLP
|
| 190 |
+
├── mlp_res_ln.py # GELU MLP + residual + next LayerNorm
|
| 191 |
+
└── swiglu_stream.py # weight-streaming SwiGLU (+ residual/LayerNorm variant)
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
## Attribution and license
|
| 195 |
+
|
| 196 |
+
Apache-2.0 (see `LICENSE`, `NOTICE`). The attention, MLP and residual/LayerNorm kernels derive from
|
| 197 |
+
the V-JEPA2 encoder kernels in
|
| 198 |
+
[`jburtoft/vjepa2-neuron-kernels`](https://huggingface.co/kernels/jburtoft/vjepa2-neuron-kernels),
|
| 199 |
+
developed by Antonio Mena ([@menaman123](https://huggingface.co/menaman123)). This repo generalizes
|
| 200 |
+
them: both RoPE pairings, any head count, head_dim 128, batching, padded-key skipping, an LNC=1
|
| 201 |
+
all-group variant, and the fused residual/LayerNorm epilogues. `swiglu_stream.py` is new.
|
| 202 |
+
`nki_kernels/attention_inner.py` is a modified copy of the AWS Neuron SDK's `nkilib`
|
| 203 |
+
`core/attention/attention_cte.py` (Copyright Amazon.com, Inc. or its affiliates, Apache-2.0). The
|
| 204 |
+
file header lists the modifications.
|
| 205 |
+
|
| 206 |
+
DINOv3 and V-JEPA2 model weights are distributed under their own licenses.
|
build/torch-neuron/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""NKI kernels for RoPE ViT encoders on AWS Trainium2 (trn2).
|
| 2 |
+
|
| 3 |
+
Model layers (drop-in forwards for Hugging Face Transformers):
|
| 4 |
+
NeuronDINOv3ViTModel -- DINOv3ViTModel (S/S+/B/L/H+)
|
| 5 |
+
NeuronVJEPA2Encoder -- VJEPA2Encoder
|
| 6 |
+
|
| 7 |
+
Model-agnostic building blocks:
|
| 8 |
+
BlockWeights, pack_block, rope_tables, EncoderRunner -- pre-norm encoder loop on the kernels
|
| 9 |
+
kernels: attention_block, attention_block_l1, attention_block_l1_ln, res_layernorm_fold,
|
| 10 |
+
mlp, mlp_res_ln, swiglu_stream, swiglu_stream_ln
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from .encoder import LNC, BlockWeights, EncoderRunner, pack_block, rope_tables
|
| 14 |
+
from .layers import NeuronDINOv3ViTModel, NeuronVJEPA2Encoder
|
| 15 |
+
from .nki_kernels import (
|
| 16 |
+
attention_block,
|
| 17 |
+
attention_block_l1,
|
| 18 |
+
attention_block_l1_ln,
|
| 19 |
+
lane_permutation,
|
| 20 |
+
mlp,
|
| 21 |
+
mlp_res_ln,
|
| 22 |
+
pack_out_weights,
|
| 23 |
+
pack_tables,
|
| 24 |
+
pack_v_weights,
|
| 25 |
+
pack_weights,
|
| 26 |
+
res_layernorm_fold,
|
| 27 |
+
swiglu_stream,
|
| 28 |
+
swiglu_stream_ln,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
__all__ = [
|
| 32 |
+
"NeuronDINOv3ViTModel", "NeuronVJEPA2Encoder",
|
| 33 |
+
"BlockWeights", "pack_block", "rope_tables", "EncoderRunner", "LNC",
|
| 34 |
+
"attention_block", "attention_block_l1", "attention_block_l1_ln", "res_layernorm_fold",
|
| 35 |
+
"mlp", "mlp_res_ln", "swiglu_stream", "swiglu_stream_ln",
|
| 36 |
+
"pack_weights", "pack_tables", "pack_v_weights", "pack_out_weights", "lane_permutation",
|
| 37 |
+
]
|
build/torch-neuron/encoder.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Model-agnostic ViT encoder loop on the NKI kernels.
|
| 2 |
+
|
| 3 |
+
A pre-norm transformer block
|
| 4 |
+
|
| 5 |
+
x = x + ls1 * attn(LN1(x)) attn: q/k/v projections, RoPE on q/k, softmax, out proj
|
| 6 |
+
x = x + ls2 * ffn(LN2(x)) ffn: GELU MLP or SwiGLU
|
| 7 |
+
|
| 8 |
+
is described once per block as a :class:`BlockWeights` (plain fp32 tensors, any source), packed
|
| 9 |
+
once by :func:`pack_block` (LayerNorm affines and LayerScale folded into the weights), and run by
|
| 10 |
+
:class:`EncoderRunner` on a stacked ``[B*N, H]`` row buffer.
|
| 11 |
+
|
| 12 |
+
Kernel choice per block (all automatic):
|
| 13 |
+
|
| 14 |
+
* attention: LNC=1 -> one program over all head groups, with the residual add + LN2 in its
|
| 15 |
+
epilogue; LNC=2 -> per-group programs whose partials the residual/LN kernel sums.
|
| 16 |
+
* FFN: GELU -> fused MLP; SwiGLU -> weight-streaming SwiGLU. Both also do the residual add and
|
| 17 |
+
the NEXT block's LN1 in their epilogue, so the stand-alone residual/LN kernel runs only before
|
| 18 |
+
block 0 (and after attention at LNC=2).
|
| 19 |
+
|
| 20 |
+
INFERENCE ONLY (no autograd). Constraints: hidden size % 128 == 0, head_dim in {64, 128},
|
| 21 |
+
tokens per image <= 2048 after padding to a multiple of 128 * LNC.
|
| 22 |
+
"""
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
from dataclasses import dataclass
|
| 26 |
+
from typing import Optional
|
| 27 |
+
|
| 28 |
+
import torch
|
| 29 |
+
|
| 30 |
+
import importlib as _il
|
| 31 |
+
|
| 32 |
+
_ab = _il.import_module(__package__ + ".nki_kernels.attention_block")
|
| 33 |
+
_ab1 = _il.import_module(__package__ + ".nki_kernels.attention_block_l1")
|
| 34 |
+
_ab1ln = _il.import_module(__package__ + ".nki_kernels.attention_block_l1_ln")
|
| 35 |
+
from .nki_kernels.attention_block import LNC
|
| 36 |
+
from .nki_kernels.mlp import nki_mlp_k4_xp
|
| 37 |
+
from .nki_kernels.mlp_res_ln import nki_mlp_res_ln
|
| 38 |
+
from .nki_kernels.res_layernorm import nki_res_layernorm_fold
|
| 39 |
+
from .nki_kernels.swiglu_stream import nki_swiglu_stream, nki_swiglu_stream_ln
|
| 40 |
+
|
| 41 |
+
__all__ = ["BlockWeights", "pack_block", "rope_tables", "EncoderRunner", "LNC"]
|
| 42 |
+
|
| 43 |
+
_SBUF_BUDGET = 180 * 1024 # per-partition bytes the LNC=1 resident-weights decision allows
|
| 44 |
+
_SW_MIN_ROWS = 256 # below this many rows per core, SwiGLU runs in torch (measured faster)
|
| 45 |
+
_L1_MAX_N = 1280 # LNC=1 all-group attention kernels verified up to this many tokens per
|
| 46 |
+
# image; beyond it (e.g. 2048-token video clips) they exceed SBUF and
|
| 47 |
+
# the per-group kernel + residual/LN kernel are used instead
|
| 48 |
+
_RESIDENT_MAX_N = 1280 # LNC=2 attention: weights staged once per launch only up to this many
|
| 49 |
+
# tokens per image (2048 tokens + resident weights exceed SBUF)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@dataclass
|
| 53 |
+
class BlockWeights:
|
| 54 |
+
"""One transformer block, as plain fp32 CPU tensors in ``nn.Linear`` layout ([out, in])."""
|
| 55 |
+
ln1_w: torch.Tensor
|
| 56 |
+
ln1_b: torch.Tensor
|
| 57 |
+
ln1_eps: float
|
| 58 |
+
wq: torch.Tensor
|
| 59 |
+
bq: Optional[torch.Tensor]
|
| 60 |
+
wk: torch.Tensor
|
| 61 |
+
bk: Optional[torch.Tensor]
|
| 62 |
+
wv: torch.Tensor
|
| 63 |
+
bv: Optional[torch.Tensor]
|
| 64 |
+
wo: torch.Tensor
|
| 65 |
+
bo: Optional[torch.Tensor]
|
| 66 |
+
ln2_w: torch.Tensor
|
| 67 |
+
ln2_b: torch.Tensor
|
| 68 |
+
ln2_eps: float
|
| 69 |
+
# GELU MLP: fc1 [I, H], fc2 [H, I]. SwiGLU: gate [I, H], up [I, H], down [H, I].
|
| 70 |
+
fc1_w: Optional[torch.Tensor] = None
|
| 71 |
+
fc1_b: Optional[torch.Tensor] = None
|
| 72 |
+
fc2_w: Optional[torch.Tensor] = None
|
| 73 |
+
fc2_b: Optional[torch.Tensor] = None
|
| 74 |
+
gate_w: Optional[torch.Tensor] = None
|
| 75 |
+
gate_b: Optional[torch.Tensor] = None
|
| 76 |
+
up_w: Optional[torch.Tensor] = None
|
| 77 |
+
up_b: Optional[torch.Tensor] = None
|
| 78 |
+
down_w: Optional[torch.Tensor] = None
|
| 79 |
+
down_b: Optional[torch.Tensor] = None
|
| 80 |
+
ls1: Optional[torch.Tensor] = None # LayerScale (None = 1)
|
| 81 |
+
ls2: Optional[torch.Tensor] = None
|
| 82 |
+
scale: Optional[float] = None # attention scale (None = head_dim ** -0.5)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _f(t):
|
| 86 |
+
return None if t is None else t.detach().float().cpu()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _z(t, n):
|
| 90 |
+
return torch.zeros(n) if t is None else t
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def pack_block(bw: BlockWeights, num_heads: int, rope: str, device) -> dict:
|
| 94 |
+
"""Fold and pack one block for the kernels. ``rope`` in {"half", "interleaved"}."""
|
| 95 |
+
wq, wk, wv, wo = _f(bw.wq), _f(bw.wk), _f(bw.wv), _f(bw.wo)
|
| 96 |
+
H = wq.shape[1]
|
| 97 |
+
F_ = wq.shape[0]
|
| 98 |
+
D = F_ // num_heads
|
| 99 |
+
gsz = 512 // D # heads per kernel program
|
| 100 |
+
nh_p = ((num_heads + gsz - 1) // gsz) * gsz # padded head count
|
| 101 |
+
Fp = nh_p * D
|
| 102 |
+
ngrp = nh_p // gsz
|
| 103 |
+
g1, t1, g2, t2 = _f(bw.ln1_w), _f(bw.ln1_b), _f(bw.ln2_w), _f(bw.ln2_b)
|
| 104 |
+
bq, bk, bv = _z(_f(bw.bq), F_), _z(_f(bw.bk), F_), _z(_f(bw.bv), F_)
|
| 105 |
+
ls1 = _f(bw.ls1) if bw.ls1 is not None else torch.ones(H)
|
| 106 |
+
ls2 = _f(bw.ls2) if bw.ls2 is not None else torch.ones(H)
|
| 107 |
+
bo = _z(_f(bw.bo), H) * ls1
|
| 108 |
+
wo = wo * ls1[:, None]
|
| 109 |
+
padr = lambda w: torch.cat([w, w.new_zeros(Fp - w.shape[0], *w.shape[1:])], 0)
|
| 110 |
+
p = {"D": D, "heads": num_heads, "nh_p": nh_p, "ngrp": ngrp, "H": H,
|
| 111 |
+
"eps1": float(bw.ln1_eps), "eps2": float(bw.ln2_eps),
|
| 112 |
+
"scale": float(bw.scale if bw.scale is not None else D ** -0.5)}
|
| 113 |
+
p["wqk"], p["bqk"] = _ab.pack_weights(padr(wq * g1[None]), padr(wk * g1[None]),
|
| 114 |
+
padr(bq + wq @ t1), padr(bk + wk @ t1), nh_p, D, rope)
|
| 115 |
+
p["wvt"], p["bv"] = _ab.pack_v_weights(padr(wv * g1[None]), padr(bv + wv @ t1))
|
| 116 |
+
p["wot"] = torch.cat([wo, wo.new_zeros(H, Fp - F_)], 1).t().contiguous().to(torch.bfloat16)
|
| 117 |
+
p["bo"] = (bo / ngrp).reshape(1, H).contiguous().to(torch.bfloat16) # per-group partials
|
| 118 |
+
p["bo_full"] = bo.reshape(1, H).contiguous().to(torch.bfloat16) # all-group kernel
|
| 119 |
+
if bw.gate_w is not None: # SwiGLU
|
| 120 |
+
gw, uw, dw = _f(bw.gate_w), _f(bw.up_w), _f(bw.down_w)
|
| 121 |
+
I = gw.shape[0]
|
| 122 |
+
Ip = ((I + 127) // 128) * 128
|
| 123 |
+
pi = lambda t: torch.cat([t, t.new_zeros(Ip - t.shape[0], *t.shape[1:])], 0)
|
| 124 |
+
p["swiglu"] = True
|
| 125 |
+
p["s_w1t"] = pi(gw * g2[None]).t().contiguous().to(torch.bfloat16) # gate (silu)
|
| 126 |
+
p["s_b1"] = pi(_z(_f(bw.gate_b), I) + gw @ t2).contiguous()
|
| 127 |
+
p["s_wgt"] = pi(uw * g2[None]).t().contiguous().to(torch.bfloat16) # up
|
| 128 |
+
p["s_bg"] = pi(_z(_f(bw.up_b), I) + uw @ t2).contiguous()
|
| 129 |
+
p["s_w2t"] = pi((dw * ls2[:, None]).t().contiguous()).contiguous().to(torch.bfloat16)
|
| 130 |
+
p["s_b2"] = (_z(_f(bw.down_b), H) * ls2).contiguous()
|
| 131 |
+
else: # GELU MLP
|
| 132 |
+
w1, w2 = _f(bw.fc1_w), _f(bw.fc2_w)
|
| 133 |
+
p["swiglu"] = False
|
| 134 |
+
p["w1t"] = (w1 * g2[None]).t().contiguous().to(torch.bfloat16)
|
| 135 |
+
p["b1"] = (_z(_f(bw.fc1_b), w1.shape[0]) + w1 @ t2).contiguous()
|
| 136 |
+
p["w2t"] = (w2 * ls2[:, None]).t().contiguous().to(torch.bfloat16)
|
| 137 |
+
p["b2"] = (_z(_f(bw.fc2_b), H) * ls2).contiguous()
|
| 138 |
+
return {k: (v.to(device) if torch.is_tensor(v) else v) for k, v in p.items()}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def rope_tables(cos: torch.Tensor, sin: torch.Tensor, n_prefix: int, n_pad: int, num_heads: int,
|
| 142 |
+
D: int, rope: str, device) -> torch.Tensor:
|
| 143 |
+
"""Kernel RoPE tables for a ``[n_prefix + n_rot, ...]`` token layout padded to ``n_pad`` rows.
|
| 144 |
+
|
| 145 |
+
``cos``/``sin``: ``[n_rot, D]`` the model's own tables for the rotated tokens (prefix tokens
|
| 146 |
+
such as CLS/registers and padding get cos=1, sin=0). For ``rope="interleaved"`` pass the
|
| 147 |
+
SIGNED sin table (see ``pack_tables``).
|
| 148 |
+
"""
|
| 149 |
+
C = torch.ones(n_pad, D)
|
| 150 |
+
S = torch.zeros(n_pad, D)
|
| 151 |
+
n_rot = cos.shape[0]
|
| 152 |
+
C[n_prefix:n_prefix + n_rot] = cos.float().cpu()
|
| 153 |
+
S[n_prefix:n_prefix + n_rot] = sin.float().cpu()
|
| 154 |
+
gsz = 512 // D
|
| 155 |
+
nh_p = ((num_heads + gsz - 1) // gsz) * gsz
|
| 156 |
+
return _ab.pack_tables(C, S, nh_p, D, rope).to(device)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class EncoderRunner:
|
| 160 |
+
"""Runs packed blocks on a stacked ``[B*N, H]`` fp32 residual stream (rows of image b at b*N)."""
|
| 161 |
+
|
| 162 |
+
def __init__(self, packed: list):
|
| 163 |
+
self.L = packed
|
| 164 |
+
p0 = packed[0]
|
| 165 |
+
self.D, self.H, self.ngrp = p0["D"], p0["H"], p0["ngrp"]
|
| 166 |
+
self.plan = [] # (group base, grid) per LNC=2 attention call
|
| 167 |
+
g = 0
|
| 168 |
+
while g < self.ngrp:
|
| 169 |
+
gr = 2 if (LNC == 2 and self.ngrp - g >= 2) else 1
|
| 170 |
+
self.plan.append((g, gr))
|
| 171 |
+
g += gr
|
| 172 |
+
|
| 173 |
+
def _res(self, mod, N):
|
| 174 |
+
return int(mod.sbuf_estimate(self.H, self.ngrp * 512, N, self.D, True) <= _SBUF_BUDGET)
|
| 175 |
+
|
| 176 |
+
def _all_group(self, N):
|
| 177 |
+
return LNC == 1 and self.ngrp > 1 and N <= _L1_MAX_N
|
| 178 |
+
|
| 179 |
+
def _attn_parts(self, z1, p, tab, n_valid, B):
|
| 180 |
+
if self._all_group(z1.shape[0] // B):
|
| 181 |
+
N = z1.shape[0] // B
|
| 182 |
+
return _ab1.attention_block_l1(z1, p["wqk"], p["bqk"], tab, p["wvt"], p["bv"], p["wot"],
|
| 183 |
+
p["bo_full"], p["scale"], self.D, n_valid, B, self._res(_ab1, N))
|
| 184 |
+
outs = [_ab.attention_block(z1, p["wqk"], p["bqk"], tab, p["wvt"], p["bv"], p["wot"], p["bo"],
|
| 185 |
+
p["scale"], self.D, n_valid, hb, gr, B,
|
| 186 |
+
int(z1.shape[0] // B <= _RESIDENT_MAX_N))
|
| 187 |
+
for hb, gr in self.plan]
|
| 188 |
+
P = outs[0] if len(outs) == 1 else torch.cat(outs, 0)
|
| 189 |
+
if P.shape[0] > 4: # residual/LN kernel takes <= 4 branches
|
| 190 |
+
P = P.float().sum(0, keepdim=True).to(torch.bfloat16)
|
| 191 |
+
return P
|
| 192 |
+
|
| 193 |
+
def _ffn(self, z2, p):
|
| 194 |
+
if p["swiglu"]:
|
| 195 |
+
if z2.shape[0] // LNC >= _SW_MIN_ROWS:
|
| 196 |
+
return nki_swiglu_stream(z2, p["s_w1t"], p["s_b1"], p["s_wgt"], p["s_bg"],
|
| 197 |
+
p["s_w2t"], p["s_b2"]).unsqueeze(0)
|
| 198 |
+
g = torch.nn.functional.silu((z2 @ p["s_w1t"]).float() + p["s_b1"])
|
| 199 |
+
u = (z2 @ p["s_wgt"]).float() + p["s_bg"]
|
| 200 |
+
return (((g * u).to(torch.bfloat16) @ p["s_w2t"]).float() + p["s_b2"]).to(torch.bfloat16).unsqueeze(0)
|
| 201 |
+
return nki_mlp_k4_xp(z2, p["w1t"], p["b1"], p["w2t"], p["b2"]).unsqueeze(0)
|
| 202 |
+
|
| 203 |
+
def _ffn_fused(self, z2, p, x, eps_next):
|
| 204 |
+
if p["swiglu"]:
|
| 205 |
+
if z2.shape[0] // LNC < _SW_MIN_ROWS:
|
| 206 |
+
return None
|
| 207 |
+
return nki_swiglu_stream_ln(z2, p["s_w1t"], p["s_b1"], p["s_wgt"], p["s_bg"],
|
| 208 |
+
p["s_w2t"], p["s_b2"], x, eps_next)
|
| 209 |
+
return nki_mlp_res_ln(z2, p["w1t"], p["b1"], p["w2t"], p["b2"], x, eps_next)
|
| 210 |
+
|
| 211 |
+
def __call__(self, x: torch.Tensor, tab: torch.Tensor, n_valid: int, B: int) -> torch.Tensor:
|
| 212 |
+
"""x: [B*N, H] fp32 (padded rows zero). Returns the pre-final-norm residual stream [B*N, H] fp32."""
|
| 213 |
+
H = self.H
|
| 214 |
+
branch = x.new_zeros(1, x.shape[0], H, dtype=torch.bfloat16)
|
| 215 |
+
z1 = None
|
| 216 |
+
nL = len(self.L)
|
| 217 |
+
for li, p in enumerate(self.L):
|
| 218 |
+
if z1 is None:
|
| 219 |
+
x, z1 = nki_res_layernorm_fold(x, branch, p["eps1"])
|
| 220 |
+
N = x.shape[0] // B
|
| 221 |
+
if self._all_group(N):
|
| 222 |
+
x, z2 = _ab1ln.attention_block_l1_ln(z1, p["wqk"], p["bqk"], tab, p["wvt"], p["bv"], p["wot"],
|
| 223 |
+
p["bo_full"], x, p["scale"], self.D, n_valid, p["eps2"], B,
|
| 224 |
+
self._res(_ab1ln, N))
|
| 225 |
+
else:
|
| 226 |
+
parts = self._attn_parts(z1, p, tab, n_valid, B)
|
| 227 |
+
x, z2 = nki_res_layernorm_fold(x, parts, p["eps2"])
|
| 228 |
+
fused = self._ffn_fused(z2, p, x, self.L[li + 1]["eps1"]) if li + 1 < nL else None
|
| 229 |
+
if fused is not None:
|
| 230 |
+
x, z1 = fused
|
| 231 |
+
branch = None
|
| 232 |
+
else:
|
| 233 |
+
branch = self._ffn(z2, p)
|
| 234 |
+
z1 = None
|
| 235 |
+
if branch is not None:
|
| 236 |
+
x = x + branch[0].float()
|
| 237 |
+
return x
|
build/torch-neuron/layers.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""KernelConfig-mappable drop-in forwards for Hugging Face Transformers ViT encoders.
|
| 2 |
+
|
| 3 |
+
``NeuronDINOv3ViTModel`` -> ``DINOv3ViTModel`` (all DINOv3 ViT sizes: S/S+/B/L/H+; 7B needs TP)
|
| 4 |
+
``NeuronVJEPA2Encoder`` -> ``VJEPA2Encoder`` (V-JEPA2 video encoder, up to 2048 tokens / clip)
|
| 5 |
+
|
| 6 |
+
Both run the whole transformer stack on the NKI kernels (``encoder.EncoderRunner``) and keep the
|
| 7 |
+
model's own patch embedding, RoPE position computation and final LayerNorm. Weights are folded
|
| 8 |
+
once, on the first device forward, from the stock module's parameters (checkpoint loading is
|
| 9 |
+
untouched) and cached on the module. Anything the kernels do not support (off-device input,
|
| 10 |
+
training mode, > 2048 tokens, unsupported head size) falls back to the stock forward.
|
| 11 |
+
|
| 12 |
+
Usage (graft, works on any Transformers version)::
|
| 13 |
+
|
| 14 |
+
import types
|
| 15 |
+
model.forward = types.MethodType(kernels.NeuronDINOv3ViTModel.forward, model)
|
| 16 |
+
model = torch.compile(model, backend="neuron")
|
| 17 |
+
|
| 18 |
+
``torch.compile(backend="neuron")`` is part of the intended configuration; the classes set
|
| 19 |
+
``can_torch_compile = True`` so the ``kernels`` library keeps them under ``Mode.TORCH_COMPILE``.
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
|
| 25 |
+
from .encoder import LNC, BlockWeights, EncoderRunner, pack_block, rope_tables
|
| 26 |
+
|
| 27 |
+
_PACK = "_vit_enc_neuron_packed"
|
| 28 |
+
_TABS = "_vit_enc_neuron_tabs"
|
| 29 |
+
_MAX_TOKENS = 2048
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _rup(n: int, m: int) -> int:
|
| 33 |
+
return ((n + m - 1) // m) * m
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _supported(H: int, heads: int) -> bool:
|
| 37 |
+
D = H // heads
|
| 38 |
+
return H % 128 == 0 and D in (64, 128) and H == heads * D
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _stack(hs: torch.Tensor, N: int):
|
| 42 |
+
B, n, H = hs.shape
|
| 43 |
+
x = hs.float()
|
| 44 |
+
if N > n:
|
| 45 |
+
x = torch.cat([x, x.new_zeros(B, N - n, H)], 1)
|
| 46 |
+
return x.reshape(B * N, H).contiguous()
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# ------------------------------------------------------------------------------------------------
|
| 50 |
+
# DINOv3
|
| 51 |
+
# ------------------------------------------------------------------------------------------------
|
| 52 |
+
def _dinov3_layers(model):
|
| 53 |
+
enc = getattr(model, "model", None) # newer Transformers: model.model.layer
|
| 54 |
+
return enc.layer if enc is not None and hasattr(enc, "layer") else model.layer
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _dinov3_block(lyr) -> BlockWeights:
|
| 58 |
+
a = lyr.attention
|
| 59 |
+
bw = BlockWeights(
|
| 60 |
+
ln1_w=lyr.norm1.weight, ln1_b=lyr.norm1.bias, ln1_eps=lyr.norm1.eps,
|
| 61 |
+
wq=a.q_proj.weight, bq=a.q_proj.bias, wk=a.k_proj.weight, bk=a.k_proj.bias,
|
| 62 |
+
wv=a.v_proj.weight, bv=a.v_proj.bias, wo=a.o_proj.weight, bo=a.o_proj.bias,
|
| 63 |
+
ln2_w=lyr.norm2.weight, ln2_b=lyr.norm2.bias, ln2_eps=lyr.norm2.eps,
|
| 64 |
+
ls1=lyr.layer_scale1.lambda1, ls2=lyr.layer_scale2.lambda1, scale=float(a.scaling))
|
| 65 |
+
m = lyr.mlp
|
| 66 |
+
if hasattr(m, "gate_proj"):
|
| 67 |
+
bw.gate_w, bw.gate_b = m.gate_proj.weight, m.gate_proj.bias
|
| 68 |
+
bw.up_w, bw.up_b = m.up_proj.weight, m.up_proj.bias
|
| 69 |
+
bw.down_w, bw.down_b = m.down_proj.weight, m.down_proj.bias
|
| 70 |
+
else:
|
| 71 |
+
bw.fc1_w, bw.fc1_b = m.up_proj.weight, m.up_proj.bias
|
| 72 |
+
bw.fc2_w, bw.fc2_b = m.down_proj.weight, m.down_proj.bias
|
| 73 |
+
return bw
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _dinov3_ok(model) -> bool:
|
| 77 |
+
c = model.config
|
| 78 |
+
if not _supported(c.hidden_size, c.num_attention_heads):
|
| 79 |
+
return False
|
| 80 |
+
if getattr(c, "use_gated_mlp", False):
|
| 81 |
+
return getattr(c, "hidden_act", "silu") == "silu"
|
| 82 |
+
return getattr(c, "hidden_act", "gelu") == "gelu"
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@torch._dynamo.disable
|
| 86 |
+
def _dinov3_freeze(model):
|
| 87 |
+
if model.training:
|
| 88 |
+
raise RuntimeError("NeuronDINOv3ViTModel: these kernels have no autograd rule; call model.eval() first.")
|
| 89 |
+
dev = model.norm.weight.device
|
| 90 |
+
heads = model.config.num_attention_heads
|
| 91 |
+
packed = [pack_block(_dinov3_block(l), heads, "half", dev) for l in _dinov3_layers(model)]
|
| 92 |
+
setattr(model, _PACK, EncoderRunner(packed))
|
| 93 |
+
setattr(model, _TABS, {})
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@torch._dynamo.disable
|
| 97 |
+
def _dinov3_tab(model, pixel_values, n_prefix, N):
|
| 98 |
+
key = (tuple(pixel_values.shape[-2:]), N)
|
| 99 |
+
tabs = getattr(model, _TABS)
|
| 100 |
+
if key not in tabs:
|
| 101 |
+
cos, sin = model.rope_embeddings(pixel_values[:1].float()) # [n_patches, D] each
|
| 102 |
+
c = model.config
|
| 103 |
+
tabs[key] = rope_tables(cos, sin, n_prefix, N, c.num_attention_heads,
|
| 104 |
+
c.hidden_size // c.num_attention_heads, "half", model.norm.weight.device)
|
| 105 |
+
return tabs[key]
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class NeuronDINOv3ViTModel(torch.nn.Module):
|
| 109 |
+
"""Drop-in forward for ``transformers.DINOv3ViTModel`` (bf16 NKI encoder, fp32 residual stream).
|
| 110 |
+
|
| 111 |
+
Returns ``last_hidden_state`` (final-normed, ``[B, 1 + registers + patches, H]``) and
|
| 112 |
+
``pooler_output`` (CLS), like the stock model. Any batch size; images up to 2048 tokens
|
| 113 |
+
(e.g. 720x720 at patch 16).
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
can_torch_compile = True
|
| 117 |
+
has_backward = False
|
| 118 |
+
|
| 119 |
+
def forward(self, pixel_values, bool_masked_pos=None, head_mask=None, **kwargs):
|
| 120 |
+
from transformers.modeling_outputs import BaseModelOutputWithPooling
|
| 121 |
+
pv = pixel_values.to(self.embeddings.patch_embeddings.weight.dtype)
|
| 122 |
+
hs = self.embeddings(pv, bool_masked_pos=bool_masked_pos) # [B, n, H]
|
| 123 |
+
B, n, H = hs.shape
|
| 124 |
+
N = _rup(n, 128 * LNC)
|
| 125 |
+
if (hs.device.type != "neuron" or self.training or head_mask is not None or N > _MAX_TOKENS
|
| 126 |
+
or not _dinov3_ok(self)):
|
| 127 |
+
import transformers.models.dinov3_vit.modeling_dinov3_vit as mv
|
| 128 |
+
if head_mask is not None:
|
| 129 |
+
kwargs["head_mask"] = head_mask
|
| 130 |
+
return mv.DINOv3ViTModel.forward(self, pixel_values, bool_masked_pos=bool_masked_pos, **kwargs)
|
| 131 |
+
if not hasattr(self, _PACK):
|
| 132 |
+
_dinov3_freeze(self)
|
| 133 |
+
n_prefix = 1 + self.config.num_register_tokens
|
| 134 |
+
tab = _dinov3_tab(self, pixel_values, n_prefix, N)
|
| 135 |
+
x = getattr(self, _PACK)(_stack(hs, N), tab, n, B)
|
| 136 |
+
x = x.reshape(B, N, H)[:, :n]
|
| 137 |
+
out = self.norm(x.to(self.norm.weight.dtype))
|
| 138 |
+
return BaseModelOutputWithPooling(last_hidden_state=out, pooler_output=out[:, 0, :])
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# ------------------------------------------------------------------------------------------------
|
| 142 |
+
# V-JEPA2
|
| 143 |
+
# ------------------------------------------------------------------------------------------------
|
| 144 |
+
def _vjepa2_block(lyr) -> BlockWeights:
|
| 145 |
+
a = lyr.attention
|
| 146 |
+
return BlockWeights(
|
| 147 |
+
ln1_w=lyr.norm1.weight, ln1_b=lyr.norm1.bias, ln1_eps=lyr.norm1.eps,
|
| 148 |
+
wq=a.query.weight, bq=a.query.bias, wk=a.key.weight, bk=a.key.bias,
|
| 149 |
+
wv=a.value.weight, bv=a.value.bias, wo=a.proj.weight, bo=a.proj.bias,
|
| 150 |
+
ln2_w=lyr.norm2.weight, ln2_b=lyr.norm2.bias, ln2_eps=lyr.norm2.eps,
|
| 151 |
+
fc1_w=lyr.mlp.fc1.weight, fc1_b=lyr.mlp.fc1.bias, fc2_w=lyr.mlp.fc2.weight, fc2_b=lyr.mlp.fc2.bias,
|
| 152 |
+
scale=float(a.scaling))
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
@torch._dynamo.disable
|
| 156 |
+
def _vjepa2_tab(encoder, N, n):
|
| 157 |
+
"""Signed-sin 3D RoPE tables from the model's own position ids (interleaved pairing)."""
|
| 158 |
+
a = encoder.layer[0].attention
|
| 159 |
+
H, D = a.all_head_size, a.attention_head_size
|
| 160 |
+
dev = a.query.weight.device
|
| 161 |
+
pos = a.get_position_ids(torch.zeros(1, n, H, device=dev), masks=None)
|
| 162 |
+
to1d = lambda t: (t if t.dim() == 1 else t.reshape(-1, n)[0]).float().cpu()
|
| 163 |
+
cos = torch.ones(n, D)
|
| 164 |
+
sin = torch.zeros(n, D)
|
| 165 |
+
s = 0
|
| 166 |
+
for sub, p in zip((a.d_dim, a.h_dim, a.w_dim), (to1d(pos[0]), to1d(pos[1]), to1d(pos[2]))):
|
| 167 |
+
half = sub // 2
|
| 168 |
+
omega = 1.0 / (10000.0 ** (torch.arange(half, dtype=torch.float32) / float(half)))
|
| 169 |
+
f = p[:, None] * omega
|
| 170 |
+
c, sn = f.cos(), f.sin()
|
| 171 |
+
cos[:, s:s + half] = c; cos[:, s + half:s + sub] = c
|
| 172 |
+
sin[:, s:s + half] = sn; sin[:, s + half:s + sub] = sn
|
| 173 |
+
s += sub
|
| 174 |
+
g = torch.ones(D); g[0::2] = -1.0
|
| 175 |
+
return rope_tables(cos, sin * g, 0, N, H // D, D, "interleaved", dev)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
@torch._dynamo.disable
|
| 179 |
+
def _vjepa2_freeze(encoder):
|
| 180 |
+
if encoder.training:
|
| 181 |
+
raise RuntimeError("NeuronVJEPA2Encoder: these kernels have no autograd rule; call model.eval() first.")
|
| 182 |
+
a = encoder.layer[0].attention
|
| 183 |
+
dev = a.query.weight.device
|
| 184 |
+
heads = a.all_head_size // a.attention_head_size
|
| 185 |
+
setattr(encoder, _PACK, EncoderRunner([pack_block(_vjepa2_block(l), heads, "interleaved", dev)
|
| 186 |
+
for l in encoder.layer]))
|
| 187 |
+
setattr(encoder, _TABS, {})
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class NeuronVJEPA2Encoder(torch.nn.Module):
|
| 191 |
+
"""Drop-in forward for ``transformers.VJEPA2Encoder``.
|
| 192 |
+
|
| 193 |
+
Any batch size; clips up to 2048 tokens (e.g. 16 frames at 256 px, tubelet 2, patch 16).
|
| 194 |
+
"""
|
| 195 |
+
|
| 196 |
+
can_torch_compile = True
|
| 197 |
+
has_backward = False
|
| 198 |
+
|
| 199 |
+
def forward(self, pixel_values_videos=None, head_mask=None, output_attentions=False,
|
| 200 |
+
output_hidden_states=False, **kwargs):
|
| 201 |
+
from transformers.modeling_outputs import BaseModelOutput
|
| 202 |
+
hs = self.embeddings(pixel_values_videos) # [B, n, H]
|
| 203 |
+
B, n, H = hs.shape
|
| 204 |
+
N = _rup(n, 128 * LNC)
|
| 205 |
+
a = self.layer[0].attention
|
| 206 |
+
if (hs.device.type != "neuron" or self.training or head_mask is not None or output_attentions
|
| 207 |
+
or output_hidden_states or N > _MAX_TOKENS
|
| 208 |
+
or not _supported(H, a.all_head_size // a.attention_head_size)):
|
| 209 |
+
import transformers.models.vjepa2.modeling_vjepa2 as mv
|
| 210 |
+
return mv.VJEPA2Encoder.forward(self, pixel_values_videos, head_mask=head_mask,
|
| 211 |
+
output_attentions=output_attentions,
|
| 212 |
+
output_hidden_states=output_hidden_states, **kwargs)
|
| 213 |
+
if not hasattr(self, _PACK):
|
| 214 |
+
_vjepa2_freeze(self)
|
| 215 |
+
tabs = getattr(self, _TABS)
|
| 216 |
+
if N not in tabs:
|
| 217 |
+
tabs[N] = _vjepa2_tab(self, N, n)
|
| 218 |
+
x = getattr(self, _PACK)(_stack(hs, N), tabs[N], n, B)
|
| 219 |
+
out = self.layernorm(x.reshape(B, N, H)[:, :n])
|
| 220 |
+
return BaseModelOutput(last_hidden_state=out)
|
build/torch-neuron/metadata.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{ "name": "vit-encoder-neuron-kernels", "id": "vit_encoder_neuron_kernels", "version": 1, "license": "Apache-2.0", "backend": {"type": "neuron", "archs": []}, "python-depends": [] }
|
build/torch-neuron/nki_kernels/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The NKI kernels and their host-side weight packing."""
|
| 2 |
+
|
| 3 |
+
from .attention_block import (
|
| 4 |
+
attention_block,
|
| 5 |
+
lane_permutation,
|
| 6 |
+
pack_out_weights,
|
| 7 |
+
pack_tables,
|
| 8 |
+
pack_v_weights,
|
| 9 |
+
pack_weights,
|
| 10 |
+
)
|
| 11 |
+
from .attention_block_l1 import attention_block_l1
|
| 12 |
+
from .attention_block_l1_ln import attention_block_l1_ln
|
| 13 |
+
from .mlp import nki_mlp_k4_xp as mlp
|
| 14 |
+
from .mlp_res_ln import nki_mlp_res_ln as mlp_res_ln
|
| 15 |
+
from .res_layernorm import nki_res_layernorm_fold as res_layernorm_fold
|
| 16 |
+
from .swiglu_stream import nki_swiglu_stream as swiglu_stream
|
| 17 |
+
from .swiglu_stream import nki_swiglu_stream_ln as swiglu_stream_ln
|
| 18 |
+
|
| 19 |
+
__all__ = [
|
| 20 |
+
"attention_block", "attention_block_l1", "attention_block_l1_ln", "res_layernorm_fold",
|
| 21 |
+
"mlp", "mlp_res_ln", "swiglu_stream", "swiglu_stream_ln",
|
| 22 |
+
"pack_weights", "pack_tables", "pack_v_weights", "pack_out_weights", "lane_permutation",
|
| 23 |
+
]
|
build/torch-neuron/nki_kernels/attention_block.py
ADDED
|
@@ -0,0 +1,801 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""K2 outer: fused QKV projection + 3D RoPE + vendor attention + fused out-projection, in ONE
|
| 2 |
+
top-level NKI kernel, FLATTENED from ``nki_attn_block_k2v_h10`` (which forks
|
| 3 |
+
``nki_attn_block_k2v_dp4``). Self-contained: imports only ``nki``, ``nkilib``, ``torch``, and
|
| 4 |
+
``_attention_cte`` from the sibling ``attention_inner`` -- NO imports from ``experiment/`` or
|
| 5 |
+
the repo-root incumbent modules.
|
| 6 |
+
|
| 7 |
+
WHAT THIS COMPUTES (per core; LNC=2, this core owns 8 of the 16 heads)
|
| 8 |
+
======================================================================
|
| 9 |
+
``result[LNC, N, H] bf16`` = out-projection PARTIALS of
|
| 10 |
+
``attention(rope(h@Wq), rope(h@Wk), h@Wv)``. Each core computes a partial sum over its 8 heads'
|
| 11 |
+
512 out-projection contraction rows; K3's residual add sums the two cores' partials.
|
| 12 |
+
|
| 13 |
+
Phase 1 (producer): dma_transpose h -> x_t; project Q/K (lane-permuted) and V; apply the six
|
| 14 |
+
rope ``tensor_tensor`` ops; scatter K into ``k_tiles`` (SBUF), Q into ``q_priv`` (private_hbm),
|
| 15 |
+
V into ``v_tiles`` (SBUF).
|
| 16 |
+
Phase 2 (attention + out-proj): reset the allocator to reclaim the dead producer buffers, call
|
| 17 |
+
``_attention_cte`` per local head into feature-major SBUF O slices, then contract the fused
|
| 18 |
+
out-projection (tokens on partitions) and DMA token-major to ``result``.
|
| 19 |
+
|
| 20 |
+
PSUM BANK PLAN (producer): V uses bank 7 (``(0, 7*_PSUM_BANK)``); Q/K parities use banks 5/6
|
| 21 |
+
(``(5+par)*_PSUM_BANK``); the fused out-projection uses bank 0 (``(0, 0)``). The vendor
|
| 22 |
+
attention (``attention_inner``) pins its own low banks {0-3} for MM1 and {4-7} for MM2 during
|
| 23 |
+
its phase, which runs after the producer's high-water mark, so there is no live overlap.
|
| 24 |
+
|
| 25 |
+
THE TWO ENGINE PINS (both to Scalar), and WHY (from PERF_INVENTORY)
|
| 26 |
+
-------------------------------------------------------------------
|
| 27 |
+
* ``:276`` V-scatter ``tensor_copy`` -> SCALAR (H9's inherited mechanism): eight copies read
|
| 28 |
+
one ``vout`` before the next vsub overwrites it (a WAR chain of 8); Scalar (240 ns) beats
|
| 29 |
+
GpSimd (375 ns) for that chain and sheds the copy off Vector, K2's binding engine.
|
| 30 |
+
* ``:458`` QK bias-add-and-evict ``tensor_scalar`` -> SCALAR (H10's mechanism): the largest
|
| 31 |
+
remaining Vector-resident relocatable site (768/core); ``data=ps`` is PSUM so GpSimd is
|
| 32 |
+
illegal, and Scalar's 612 ns fee is faster than the 744 ns Vector cell it replaces, so the
|
| 33 |
+
zero-lead rope chain that consumes ``proj`` cannot lengthen.
|
| 34 |
+
The ``:224`` bias-cast ``tensor_copy`` and ``:483`` K-scatter ``tensor_copy`` stay UNPINNED
|
| 35 |
+
(compiler default) -- pinning them was dropped (:224 is 0.09% of the window; :483 has no
|
| 36 |
+
affordable landing engine), and they MUST stay unpinned to match the shipping stream.
|
| 37 |
+
|
| 38 |
+
HOST-SIDE FOLD ALGEBRA (packing helpers below, copied flat)
|
| 39 |
+
-----------------------------------------------------------
|
| 40 |
+
Affine-free LN1 folds gamma1/beta1 into the K2 weights at freeze time:
|
| 41 |
+
``pack_weights(wq*g1, wk*g1, bq+wq@t1, bk+wk@t1, ...)`` and ``pack_v_weights(wv*g1, bv+wv@t1)``.
|
| 42 |
+
The lane permutation puts each quad-of-heads' even/odd RoPE lanes in their own 128-partition
|
| 43 |
+
group so RoPE is six full-width ``tensor_tensor`` ops with no transposes; it cancels in
|
| 44 |
+
attention's Q/K contraction (a dot product is invariant under a shared permutation of the
|
| 45 |
+
contraction axis), so V is natural and nothing downstream un-permutes.
|
| 46 |
+
LNC2 BIAS HALVING: ``pack_out_weights`` halves the out-projection bias (``bo*0.5``) so the two
|
| 47 |
+
cores' partials sum to exactly one bias -- halving is bitwise exact in bf16 (it only decrements
|
| 48 |
+
the exponent, and summing two identical bf16 values is exact).
|
| 49 |
+
|
| 50 |
+
INFERENCE ONLY: no autograd; the packed weights are detached caches.
|
| 51 |
+
|
| 52 |
+
SBUF BUDGET (bytes/lane; budget = 192 KiB = 196608 B/lane), TWO GEOMETRIES
|
| 53 |
+
==========================================================================
|
| 54 |
+
N=2048 (16-frame clips) is the SHIPPING path: nothing below is gated on, so buffer shapes,
|
| 55 |
+
byte layout, DMA target addresses and fill VALUES are BIT-FOR-BIT the incumbent (verified:
|
| 56 |
+
torch.equal(perf_out.pt) == True, max abs diff 0.0). N=8192 (64-frame clips) is a SEPARATE,
|
| 57 |
+
gated path (``if N > 2048``) that the N<=2048 forward never enters.
|
| 58 |
+
|
| 59 |
+
| N=2048 (16f) | N=8192 (64f, gated)
|
| 60 |
+
------------------------------ + -------------- + --------------------------------------
|
| 61 |
+
k_tiles (8 heads resident) | 32768 | 0 (streamed to private_hbm)
|
| 62 |
+
v_tiles (8 heads resident) | 16384 | 0 (streamed to private_hbm)
|
| 63 |
+
k_stage / v_stage (1 head) | - | 16384 + 8192 = 24576 (one head at a time)
|
| 64 |
+
x_t (hidden^T ring) | 32768 | 32768 (depth-4 token-block ring, was 131072)
|
| 65 |
+
tab (rope tables ring) | 32768 | 32768 (depth-4 token-block ring, was 131072)
|
| 66 |
+
producer working set | ~25000 | ~25000
|
| 67 |
+
------------------------------ + -------------- + --------------------------------------
|
| 68 |
+
PHASE-1 PEAK | ~124 KiB | ~106 KiB (FITS)
|
| 69 |
+
------------------------------ + -------------- + --------------------------------------
|
| 70 |
+
phase2_base (persistent K/V) | 49152 | 24576 (only k_stage/v_stage persist)
|
| 71 |
+
wo_sb + bo_bc + o_feat + out | 29696 | 78848
|
| 72 |
+
vendor inner (per head) | ~45 KiB | ~251 KiB (single 8192 section, n_large=4:
|
| 73 |
+
| | exp_tp_sb 131072 + mm1_masked 65536 +
|
| 74 |
+
| | exp_sb 32768 balloon -- see below)
|
| 75 |
+
------------------------------ + -------------- + --------------------------------------
|
| 76 |
+
PHASE-2 PEAK | ~125 KiB | ~354912 B/lane (DOES NOT FIT -- measured
|
| 77 |
+
| | on device [NCC_INLA001] 128x354912, 1.81x
|
| 78 |
+
| | the 196608 budget => N>2048 is REFUSED)
|
| 79 |
+
|
| 80 |
+
WHY N>2048 IS REFUSED. At N=8192 the shipping design has TWO independent SBUF overflows
|
| 81 |
+
(both confirmed on device as ``[NCC_INLA001] ... out of bound 128x526944``):
|
| 82 |
+
(1) PHASE 1: the four N-scaled residents k_tiles(131072)+v_tiles(65536)+x_t(131072)+
|
| 83 |
+
tab(131072) = 458752 B/lane coexist -- 2.3x the whole budget.
|
| 84 |
+
(2) PHASE 2: eight heads' K/V resident (196608 B/lane) is the ENTIRE budget.
|
| 85 |
+
The phase-1 fixes work (x_t/tab depth-4 token-block rings, bit-for-bit at N<=2048, saving
|
| 86 |
+
196608 B/lane; K/V streamed through private_hbm one head at a time) => phase-1 peak ~106 KiB.
|
| 87 |
+
But PHASE 2 does NOT fit: handing one head all 8192 keys makes the single-section flatten run
|
| 88 |
+
with num_large_tiles_per_section=ceil(8192/2048)=4, ballooning three inner buffers (exp_tp_sb
|
| 89 |
+
131072 + mm1_masked 65536 + exp_sb 32768 = 229376 B/lane) -> phase-2 peak measured 354912 B/lane,
|
| 90 |
+
1.81x the budget. The shrinkable phase-2 region (o_feat/out/wo/bo = 78848) is smaller than the
|
| 91 |
+
158304 overflow, so streaming O or shrinking o_feat cannot close the gap; re-enabling flash
|
| 92 |
+
sectioning fits but is numerically wrong (rel_l2 0.90, the V-JEPA2 kernels' verification notes), and
|
| 93 |
+
editing the inner would perturb the bit-for-bit 16-frame path. So the host wrapper
|
| 94 |
+
``attention_block`` raises ``NotImplementedError`` for N>2048 rather than emit a non-fitting or
|
| 95 |
+
wrong kernel (see the V-JEPA2 kernels' verification notes). The N<=2048 path is bit-for-bit unchanged.
|
| 96 |
+
"""
|
| 97 |
+
from __future__ import annotations
|
| 98 |
+
|
| 99 |
+
import torch
|
| 100 |
+
from nki.framework.kernel import wrap_nki_to_torch_hop
|
| 101 |
+
from torch_neuronx import nki_op
|
| 102 |
+
|
| 103 |
+
import nki
|
| 104 |
+
import nki.isa as nisa
|
| 105 |
+
import nki.language as nl
|
| 106 |
+
from nki.isa import engine
|
| 107 |
+
|
| 108 |
+
from nkilib.core.utils.modular_allocator import ModularAllocator
|
| 109 |
+
|
| 110 |
+
# The vendor attention, flattened to this model's reached path with the h8/h10 pins inlined.
|
| 111 |
+
# The N<=2048 path calls THIS single-section flatten; the N>2048 path is REFUSED (see below).
|
| 112 |
+
# seqlen_k=8192 < the vendor's _FLASH_ATTENTION_THRESHOLD (10240), so num_sections==1 -- the
|
| 113 |
+
# SAME single-section, non-flash code path the verified 16-frame configuration uses (no
|
| 114 |
+
# multi-section online-softmax combine, which is numerically wrong here: the V-JEPA2 kernels' verification notes).
|
| 115 |
+
# BUT single-section at 8192 does NOT fit: the inner's mm1_masked / exp_sb / exp_tp_sb are
|
| 116 |
+
# allocated with num_free_tiles carrying num_large_tiles_per_section = ceil(8192/2048) = 4
|
| 117 |
+
# (attention_inner.py:658-701), so those three buffers BALLOON with total_seqlen_k -- they are
|
| 118 |
+
# NOT reused across the large-tile loop. exp_tp_sb 131072 + mm1_masked 65536 + exp_sb 32768 =
|
| 119 |
+
# 229376 B/lane, driving phase-2 peak to ~354912 B/lane (1.81x the 196608 budget; measured on
|
| 120 |
+
# device [NCC_INLA001] 128x354912). See the SBUF table and refusal rationale below.
|
| 121 |
+
from .attention_inner import _attention_cte
|
| 122 |
+
|
| 123 |
+
import os as _os
|
| 124 |
+
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 125 |
+
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
| 126 |
+
_SPLIT = 2 # heads are always split in two halves (8 heads per program)
|
| 127 |
+
_P_MAX = 128
|
| 128 |
+
_PSUM_FMAX = 512
|
| 129 |
+
_PSUM_BANK = 2048 # bytes per partition per bank
|
| 130 |
+
_K_TILE = 512 # vendor k_cache_sbuf tiles are (d, 512)
|
| 131 |
+
_V_TILE = 128 # vendor v_cache_sbuf tiles are (128, d)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _kernel_assert(cond: bool, msg: str):
|
| 135 |
+
assert cond, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {msg}"
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
# ==========================================================================================
|
| 139 |
+
# Host-side packing helpers (copied flat from nki_qk_fused_2core / nki_attn_block_k2v_dp4).
|
| 140 |
+
# All are frozen-weight work, cached once; INFERENCE ONLY.
|
| 141 |
+
# ==========================================================================================
|
| 142 |
+
def lane_permutation(heads: int, D: int, rope: str = "half") -> torch.Tensor:
|
| 143 |
+
"""Column order that puts each quad-of-heads' RoPE pair halves in their own 128-row group.
|
| 144 |
+
|
| 145 |
+
RoPE rotates pairs of lanes ``(a, b)`` of each head. ``rope`` picks the pairing:
|
| 146 |
+
* ``"half"`` -- (j, j + D/2), the rotate-half convention (DINOv3, EVA-02, HF Llama style).
|
| 147 |
+
* ``"interleaved"`` -- (2j, 2j + 1), adjacent lanes (V-JEPA2 / GPT-NeoX-interleaved style).
|
| 148 |
+
The projection's output columns are reordered so group ``2a`` holds the FIRST lane of every
|
| 149 |
+
pair for heads ``4a..4a+3`` (at D=64; 2 groups per 4 heads at D=128) and group ``2a+1`` the
|
| 150 |
+
SECOND lane, so RoPE is six full-width elementwise ops with no transposes. The permutation
|
| 151 |
+
cancels in the Q/K contraction, so nothing downstream un-permutes.
|
| 152 |
+
|
| 153 |
+
Returns ``perm`` of length ``heads*D`` s.t. ``w.t()[:, perm]`` is the reordered weight.
|
| 154 |
+
"""
|
| 155 |
+
_kernel_assert(rope in ("half", "interleaved"), f"rope must be 'half' or 'interleaved', got {rope!r}")
|
| 156 |
+
half = D // 2
|
| 157 |
+
quad = _P_MAX // half
|
| 158 |
+
_kernel_assert(quad * half == _P_MAX, f"D={D} gives half={half}, needs quad*half=128")
|
| 159 |
+
_kernel_assert(heads % quad == 0, f"heads={heads} must be divisible by quad={quad}")
|
| 160 |
+
perm = torch.empty(heads * D, dtype=torch.long)
|
| 161 |
+
for hd in range(heads):
|
| 162 |
+
a, b = hd // quad, hd % quad
|
| 163 |
+
for j in range(half):
|
| 164 |
+
lo, hi = (j, half + j) if rope == "half" else (2 * j, 2 * j + 1)
|
| 165 |
+
perm[(2 * a) * _P_MAX + b * half + j] = hd * D + lo
|
| 166 |
+
perm[(2 * a + 1) * _P_MAX + b * half + j] = hd * D + hi
|
| 167 |
+
return perm
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def pack_weights(wq: torch.Tensor, wk: torch.Tensor,
|
| 171 |
+
bq: torch.Tensor | None, bk: torch.Tensor | None,
|
| 172 |
+
heads: int, D: int, rope: str = "half"):
|
| 173 |
+
"""``(w_packed [2,H,H] bf16, b_packed [2,H,1] bf16)`` -- transposed and column-permuted.
|
| 174 |
+
|
| 175 |
+
FOLD: in the affine-free-LN1 freeze this is called on gamma-folded inputs
|
| 176 |
+
``pack_weights(wq*g1, wk*g1, bq+wq@t1, bk+wk@t1, heads, D)``. ``wq``/``wk`` are torch
|
| 177 |
+
``Linear.weight`` ``[H_out, H_in]``; the kernel wants ``[H_in, H_out]`` with ``H_out``
|
| 178 |
+
permuted, i.e. ``w.t()[:, perm]``. The bias is shaped ``[2,H,1]`` (not ``[2,H]``) so the
|
| 179 |
+
kernel can slice a ``[128,1]`` tile straight out of HBM.
|
| 180 |
+
"""
|
| 181 |
+
H = wq.shape[0]
|
| 182 |
+
perm = lane_permutation(heads, D, rope).to(wq.device)
|
| 183 |
+
w = torch.stack([wq.detach().t()[:, perm].contiguous(),
|
| 184 |
+
wk.detach().t()[:, perm].contiguous()], 0)
|
| 185 |
+
zero = torch.zeros(H, dtype=wq.dtype, device=wq.device)
|
| 186 |
+
b = torch.stack([(zero if bq is None else bq.detach())[perm].contiguous(),
|
| 187 |
+
(zero if bk is None else bk.detach())[perm].contiguous()], 0)
|
| 188 |
+
b = b.unsqueeze(-1)
|
| 189 |
+
return w.to(torch.bfloat16).contiguous(), b.to(torch.bfloat16).contiguous()
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def pack_tables(cos: torch.Tensor, sin: torch.Tensor, heads: int, D: int, rope: str = "half"):
|
| 193 |
+
"""``tab [4, 128, N] fp32`` RoPE tables, quad-tiled to match ``lane_permutation(..., rope)``.
|
| 194 |
+
|
| 195 |
+
Inputs are the model's own ``[N, D]`` cos / sin tables (shared by every head; prefix tokens that
|
| 196 |
+
take no rotation: cos=1, sin=0). With ``(a, b)`` a lane pair the kernel computes
|
| 197 |
+
``a' = a*tab0 + b*tab2`` and ``b' = b*tab1 + a*tab3``, so:
|
| 198 |
+
* ``"half"``: ``sin`` is the PLAIN sin table (rotate-half: ``x*cos + rotate_half(x)*sin``);
|
| 199 |
+
tab = (cos_lo, cos_hi, -sin_lo, +sin_hi).
|
| 200 |
+
* ``"interleaved"``: ``sin`` is the SIGNED sin table (``-sin`` on even lanes, ``+sin`` on odd);
|
| 201 |
+
tab = (cos_even, cos_odd, sinsigned_even, sinsigned_odd).
|
| 202 |
+
"""
|
| 203 |
+
_kernel_assert(rope in ("half", "interleaved"), f"rope must be 'half' or 'interleaved', got {rope!r}")
|
| 204 |
+
half = D // 2
|
| 205 |
+
quad = _P_MAX // half
|
| 206 |
+
if rope == "half":
|
| 207 |
+
parts = [cos[:, :half], cos[:, half:], -sin[:, :half], sin[:, half:]]
|
| 208 |
+
else:
|
| 209 |
+
parts = [cos[:, 0::2], cos[:, 1::2], sin[:, 0::2], sin[:, 1::2]]
|
| 210 |
+
tab = torch.stack([p.t().contiguous().repeat(quad, 1) for p in parts], 0)
|
| 211 |
+
return tab.to(torch.float32).contiguous()
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def pack_v_weights(wv: torch.Tensor, bv: torch.Tensor | None):
|
| 215 |
+
"""``(wv_t [H, H] bf16, bv [1, H] bf16)``. V is NOT lane-permuted (natural order).
|
| 216 |
+
|
| 217 |
+
FOLD: in the affine-free-LN1 freeze this is called on gamma-folded inputs
|
| 218 |
+
``pack_v_weights(wv*g1, bv+wv@t1)``.
|
| 219 |
+
"""
|
| 220 |
+
H = wv.shape[0]
|
| 221 |
+
zero = torch.zeros(H, dtype=wv.dtype, device=wv.device)
|
| 222 |
+
return (wv.detach().t().contiguous().to(torch.bfloat16),
|
| 223 |
+
(bv.detach() if bv is not None else zero).reshape(1, H).contiguous().to(torch.bfloat16))
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def pack_out_weights(wo, bo):
|
| 227 |
+
"""``(wo_t [H, H] bf16, bo/2 [1, H] bf16)`` for the fused out projection.
|
| 228 |
+
|
| 229 |
+
``wo`` is ``attn.proj.weight`` [H, H] (attention features -> hidden). The kernel contracts
|
| 230 |
+
over attention features, so it wants those on partitions: ``wo.T`` = [features, hidden].
|
| 231 |
+
|
| 232 |
+
LNC2 BIAS HALVING. Both cores add ``bo/2`` and K3's partial sum restores ``bo``. Adding the
|
| 233 |
+
full bias on both would double it; adding it on one core only would need ``program_id`` as an
|
| 234 |
+
arithmetic operand (it is symbolic -- neither a compile-time constant nor a [P,1] tile).
|
| 235 |
+
Halving is bitwise exact in bf16: it only decrements the exponent, and summing two identical
|
| 236 |
+
bf16 values is exact.
|
| 237 |
+
"""
|
| 238 |
+
import torch as _t
|
| 239 |
+
Hh = wo.shape[0]
|
| 240 |
+
zero = _t.zeros(Hh, dtype=wo.dtype, device=wo.device)
|
| 241 |
+
half = (bo.detach() if bo is not None else zero) * 0.5
|
| 242 |
+
return (wo.detach().t().contiguous().to(_t.bfloat16),
|
| 243 |
+
half.reshape(1, Hh).contiguous().to(_t.bfloat16))
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
# ==========================================================================================
|
| 247 |
+
# THE KERNEL. Flattened from _attn_block_k2v_h10_dp_kernel (verbatim dp4 body with the two
|
| 248 |
+
# engine= keywords hardcoded to engine.scalar at :276 and :458; :224 and :483 left unpinned).
|
| 249 |
+
# ==========================================================================================
|
| 250 |
+
@nki.jit
|
| 251 |
+
def _attention_block_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_hbm,
|
| 252 |
+
wot_hbm, bo_hbm, scale, head_dim, n_valid, hb, grid, nimg=1, resident=0):
|
| 253 |
+
"""out[16, N, D] bf16 = attention(rope(h@Wq), rope(h@Wk), h@Wv), one kernel (per-core partials).
|
| 254 |
+
|
| 255 |
+
BATCHED (nimg>1): h_hbm is the STACKED [nimg*N, H] buffer (images back to back, N rows each).
|
| 256 |
+
The per-image body below runs once per image inside this ONE launch, reading image b's rows at
|
| 257 |
+
row offset b*N and writing partials to result[slot, b*N:(b+1)*N]. ONE launch, no host-side
|
| 258 |
+
slice/cat glue.
|
| 259 |
+
resident=0: the loop wraps the whole body, so weights/biases/rope tables are re-DMA'd per image.
|
| 260 |
+
resident=1 (N<=2048): Q/K/V/O weights, biases and rope tables are staged ONCE at the bottom of
|
| 261 |
+
SBUF before the image loop (~38 KiB/lane ViT-L + tables); each image's buffers are allocated
|
| 262 |
+
above them. Same math, bit-identical outputs."""
|
| 263 |
+
NT, H = h_hbm.shape
|
| 264 |
+
N = NT // nimg
|
| 265 |
+
Fp = wqk_hbm.shape[2] # padded attention features = 512 * n_groups
|
| 266 |
+
D = head_dim
|
| 267 |
+
half = D // 2
|
| 268 |
+
quad = _P_MAX // half # heads per 128-partition parity group = 4
|
| 269 |
+
heads = Fp // D # padded head count (multiple of 8)
|
| 270 |
+
local_heads = _PSUM_FMAX // D # heads per program: 8 at D=64, 4 at D=128
|
| 271 |
+
local_quads = local_heads // quad # 2
|
| 272 |
+
n_chunks = H // _P_MAX # 8 contraction chunks
|
| 273 |
+
n_tt = (N + _PSUM_FMAX - 1) // _PSUM_FMAX # token tiles of 512 (last may be ragged)
|
| 274 |
+
n_ktiles = (N + _K_TILE - 1) // _K_TILE # K tiles / head (last may be partially filled)
|
| 275 |
+
n_vtiles = N // _V_TILE # 16 V tiles / head
|
| 276 |
+
n_ochunks = (local_heads * D) // _P_MAX # feature chunks of the out projection
|
| 277 |
+
v_per_tt = _PSUM_FMAX // _V_TILE # 4 V tiles per 512-token tile
|
| 278 |
+
|
| 279 |
+
# N>2048 gate (see the phase-1/phase-2 note below). Computed early so the K/V residents can
|
| 280 |
+
# be allocated as HBM (streamed) for the big path.
|
| 281 |
+
_big = N > _PSUM_FMAX * 4
|
| 282 |
+
|
| 283 |
+
# N>2048 IS NOT SUPPORTED (guarded at the host wrapper `attention_block` below, which raises
|
| 284 |
+
# NotImplementedError before this kernel is ever traced -- NKI kernels cannot `raise`). The
|
| 285 |
+
# phase-1 memory fixes (x_t/tab depth-4 rings, K/V streamed through private_hbm one head at a
|
| 286 |
+
# time) DO fit -- phase-1 peak ~106 KiB/lane. But phase 2 does NOT: handing one head all 8192
|
| 287 |
+
# keys makes the flattened single-section _attention_cte run with num_large_tiles_per_section
|
| 288 |
+
# =ceil(8192/2048)=4, which balloons three N-scaled inner buffers -- exp_tp_sb (8*4*4*512*2 =
|
| 289 |
+
# 131072), mm1_masked (2*4*2048*4 = 65536), exp_sb (2*4*2048*2 = 32768) = 229376 B/lane for
|
| 290 |
+
# three buffers alone. Measured on device (2026-09-25, neuronx-cc 2.25): [NCC_INLA001] out of
|
| 291 |
+
# bound 128x354912 -> phase-2 peak 354912 B/lane, 1.81x the 196608 budget. The shrinkable
|
| 292 |
+
# phase-2 region (o_feat 65536 + out 1024 + wo/bo 12288 = 78848) is smaller than the 158304
|
| 293 |
+
# overflow, so streaming O or shrinking o_feat CANNOT close the gap -- the inner's N-scaled
|
| 294 |
+
# buffers are the wall. Re-enabling flash sectioning (2048-key sections) shrinks the inner but
|
| 295 |
+
# activates the vendor's multi-section online-softmax combine, a path the shipping config never
|
| 296 |
+
# exercises and which produced rel_l2 0.90 (wrong). See the V-JEPA2 kernels' verification notes.
|
| 297 |
+
|
| 298 |
+
_kernel_assert(wqk_hbm.shape == (2, H, Fp), f"wqk must be [2,{H},{Fp}]")
|
| 299 |
+
_kernel_assert(bqk_hbm.shape == (2, Fp, 1), f"bqk must be [2,{Fp},1]")
|
| 300 |
+
_kernel_assert(tab_hbm.shape == (4, _P_MAX, N), f"tab must be [4,{_P_MAX},{N}]")
|
| 301 |
+
_kernel_assert(wvt_hbm.shape == (H, Fp) and bv_hbm.shape == (1, Fp), "bad V weights")
|
| 302 |
+
_kernel_assert(wot_hbm.shape == (Fp, H) and bo_hbm.shape == (1, H), "bad O weights")
|
| 303 |
+
_kernel_assert(H % _P_MAX == 0, f"H={H} must be a multiple of 128")
|
| 304 |
+
_kernel_assert(quad * half == _P_MAX, f"D={D} must give 4 heads per group")
|
| 305 |
+
_kernel_assert(Fp % _PSUM_FMAX == 0, "padded features must be a multiple of 512")
|
| 306 |
+
_kernel_assert(local_heads * D == _PSUM_FMAX,
|
| 307 |
+
"inline V projection wants all local heads in one 512-wide stream")
|
| 308 |
+
_kernel_assert(N % _P_MAX == 0, f"N={N} must be a multiple of 128")
|
| 309 |
+
_kernel_assert(NT == nimg * N, f"rows {NT} must be nimg({nimg}) x N")
|
| 310 |
+
_kernel_assert(str(h_hbm.dtype) == str(nl.bfloat16), "h must be bf16")
|
| 311 |
+
|
| 312 |
+
# [LNC, N, H] PARTIALS. The out projection contracts over all H features == all 16 heads,
|
| 313 |
+
# and this core owns 8, so what it computes is a partial sum; K3 adds the two.
|
| 314 |
+
result = nl.ndarray((grid, NT, H), dtype=nl.bfloat16, buffer=nl.shared_hbm)
|
| 315 |
+
|
| 316 |
+
# Each program handles one 8-head group: group = hb + program_id. grid==1 -> single program.
|
| 317 |
+
if grid == 1:
|
| 318 |
+
pid = hb
|
| 319 |
+
out_slot = 0
|
| 320 |
+
else:
|
| 321 |
+
out_slot = nl.program_id(0) # SYMBOLIC: index arithmetic only
|
| 322 |
+
pid = hb + out_slot
|
| 323 |
+
head0 = pid * local_heads
|
| 324 |
+
col0 = pid * (local_heads * D) # first permuted qk weight column
|
| 325 |
+
vcol0 = pid * (local_heads * D) # first natural v weight column (same offset)
|
| 326 |
+
|
| 327 |
+
# ---- RESIDENT MODE (resident=1, N<=2048 only): stage the per-launch-invariant inputs ONCE,
|
| 328 |
+
# at the BOTTOM of SBUF, before the image loop. Everything per image is then allocated above
|
| 329 |
+
# `res_top`. Invariant: Q/K/V/O weights, their biases, and the rope tables (same for every image).
|
| 330 |
+
# Cost ~38 KiB/lane (ViT-L) + tables; per-image phase peaks were <=106 KiB, budget 192 KiB.
|
| 331 |
+
_res = bool(resident) and not (N > _PSUM_FMAX * 4)
|
| 332 |
+
res_top = 0
|
| 333 |
+
if _res:
|
| 334 |
+
ra = ModularAllocator(initial_address=0)
|
| 335 |
+
r_w_sb = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2 * _P_MAX), dtype=nl.bfloat16,
|
| 336 |
+
block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
|
| 337 |
+
for qk in range(2):
|
| 338 |
+
for c in range(n_chunks):
|
| 339 |
+
nisa.dma_copy(dst=r_w_sb[qk][c],
|
| 340 |
+
src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX, col0:col0 + local_quads * 2 * _P_MAX])
|
| 341 |
+
r_wv_sb = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
|
| 342 |
+
block_dim=[n_chunks], num_free_tiles=[n_chunks])
|
| 343 |
+
for c in range(n_chunks):
|
| 344 |
+
nisa.dma_copy(dst=r_wv_sb[c], src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX, vcol0:vcol0 + local_heads * D])
|
| 345 |
+
r_b_sb = ra.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
|
| 346 |
+
block_dim=[2, local_quads * 2], num_free_tiles=[2, local_quads * 2], align_to=4)
|
| 347 |
+
r_b_stage = ra.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
|
| 348 |
+
for qk in range(2):
|
| 349 |
+
for g in range(local_quads * 2):
|
| 350 |
+
nisa.dma_copy(dst=r_b_stage, src=bqk_hbm[qk, col0 + g * _P_MAX:col0 + (g + 1) * _P_MAX, 0:1])
|
| 351 |
+
nisa.tensor_copy(dst=r_b_sb[qk][g], src=r_b_stage)
|
| 352 |
+
r_bv_bc = ra.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
|
| 353 |
+
nisa.dma_copy(dst=r_bv_bc, src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]], offset=vcol0))
|
| 354 |
+
r_tab = ra.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
|
| 355 |
+
block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
|
| 356 |
+
for s in range(n_tt):
|
| 357 |
+
for t_i in range(4):
|
| 358 |
+
_tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
|
| 359 |
+
nisa.dma_copy(dst=r_tab[t_i][s][:, 0:_tw], src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
|
| 360 |
+
r_wo_sb = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
|
| 361 |
+
block_dim=[n_ochunks], num_free_tiles=[n_ochunks])
|
| 362 |
+
for oc in range(n_ochunks):
|
| 363 |
+
nisa.dma_copy(dst=r_wo_sb[oc], src=wot_hbm[col0 + oc * _P_MAX:col0 + (oc + 1) * _P_MAX, 0:H])
|
| 364 |
+
r_bo_bc = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
|
| 365 |
+
nisa.dma_copy(dst=r_bo_bc, src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
|
| 366 |
+
res_top = ra.get_current_address()
|
| 367 |
+
|
| 368 |
+
for img in range(nimg): # unrolled at trace time (python int)
|
| 369 |
+
rofs = img * N * H # element offset of image img's rows in h / result
|
| 370 |
+
allocator = ModularAllocator(initial_address=res_top)
|
| 371 |
+
|
| 372 |
+
# ---- persistent consumer tiles (written by producer, read by attention) ---------
|
| 373 |
+
# N<=2048: all 8 heads' K/V resident in SBUF (bit-for-bit shipping path). N>2048: K/V for
|
| 374 |
+
# all 8 heads (196608 B/lane) is the ENTIRE 192 KiB budget, so they cannot stay resident
|
| 375 |
+
# alongside x_t/tab/working set. STREAM them: the producer scatters each head's K/V to
|
| 376 |
+
# private_hbm, and phase-2 loads ONE head's K/V into small resident staging rings just
|
| 377 |
+
# before its attention call (attention runs per head and writes O to HBM, so only one
|
| 378 |
+
# head's K/V need be live at a time).
|
| 379 |
+
k_tiles = []
|
| 380 |
+
v_tiles = []
|
| 381 |
+
k_hbm = []
|
| 382 |
+
v_hbm = []
|
| 383 |
+
if _big:
|
| 384 |
+
for _lh in range(local_heads):
|
| 385 |
+
k_hbm.append(nl.ndarray((n_ktiles, D, _K_TILE), dtype=nl.bfloat16,
|
| 386 |
+
buffer=nl.private_hbm))
|
| 387 |
+
v_hbm.append(nl.ndarray((n_vtiles, _V_TILE, D), dtype=nl.bfloat16,
|
| 388 |
+
buffer=nl.private_hbm))
|
| 389 |
+
# ONE head's resident staging rings (reused across heads in phase 2).
|
| 390 |
+
k_stage = allocator.alloc_sbuf_tensor(
|
| 391 |
+
shape=(D, _K_TILE), dtype=nl.bfloat16,
|
| 392 |
+
block_dim=[n_ktiles], num_free_tiles=[n_ktiles], align_to=32)
|
| 393 |
+
v_stage = allocator.alloc_sbuf_tensor(
|
| 394 |
+
shape=(_V_TILE, D), dtype=nl.bfloat16,
|
| 395 |
+
block_dim=[n_vtiles], num_free_tiles=[n_vtiles])
|
| 396 |
+
else:
|
| 397 |
+
for _lh in range(local_heads):
|
| 398 |
+
kt_ = allocator.alloc_sbuf_tensor(
|
| 399 |
+
shape=(D, _K_TILE), dtype=nl.bfloat16,
|
| 400 |
+
block_dim=[n_ktiles], num_free_tiles=[n_ktiles], align_to=32)
|
| 401 |
+
k_tiles.append(kt_)
|
| 402 |
+
vt_ = allocator.alloc_sbuf_tensor(
|
| 403 |
+
shape=(_V_TILE, D), dtype=nl.bfloat16,
|
| 404 |
+
block_dim=[n_vtiles], num_free_tiles=[n_vtiles])
|
| 405 |
+
v_tiles.append(vt_)
|
| 406 |
+
# HIGH WATER MARK for phase 2: k_tiles/v_tiles are the only phase-1 products attention
|
| 407 |
+
# needs, so every buffer above this address is dead once the producer loop finishes.
|
| 408 |
+
phase2_base = allocator.get_current_address()
|
| 409 |
+
|
| 410 |
+
# ------------------------------------------------------------------------------------
|
| 411 |
+
# N>2048 GATE (64-frame clips, N=8192). At N<=2048 every ring below keeps its full-N
|
| 412 |
+
# depth, so the buffer shape, byte layout, DMA target addresses and fill VALUES are
|
| 413 |
+
# bit-for-bit identical to the shipping 16-frame path (see the SBUF budget table in the
|
| 414 |
+
# module docstring), and the attention call goes to the pinned single-section flatten
|
| 415 |
+
# ``_attention_cte`` (all engine pins intact). At N>2048 two independent overflows appear:
|
| 416 |
+
# (1) PHASE 1: the four N-scaled residents (k_tiles+v_tiles+x_t+tab = 458752 B/lane)
|
| 417 |
+
# coexist. FIX: x_t and tab become depth-XT_RING token-block rings (a rolling 4-tile
|
| 418 |
+
# window filled inside the per-token-tile loop; a pure copy, identical values).
|
| 419 |
+
# (2) PHASE 2: eight heads' K/V resident (196608 B/lane) is the whole budget. FIX: stream
|
| 420 |
+
# K/V through private_hbm and stage ONE head at a time into k_stage/v_stage. The
|
| 421 |
+
# attention call is the SAME single-section flatten ``_attention_cte`` the N<=2048 path
|
| 422 |
+
# uses (seqlen_k=8192 < 10240 threshold => num_sections==1, no multi-section flash
|
| 423 |
+
# combine); its large-tile loop iterates 4x over _LARGE_TILE_SZ-sized buffers.
|
| 424 |
+
# The N<=2048 path enters NEITHER branch and is bit-for-bit unchanged.
|
| 425 |
+
_big = N > _PSUM_FMAX * 4 # N>2048 -> the 64-frame geometry
|
| 426 |
+
XT_RING = min(n_tt, 4) if _big else n_tt # token-block ring depth for x_t / tab
|
| 427 |
+
|
| 428 |
+
# ---- resident producer inputs ----------------------------------------------------
|
| 429 |
+
# x^T: hidden on partitions, tokens on the free axis, as a per-512-token-block ring.
|
| 430 |
+
# At N<=2048 XT_RING==n_tt so the ring holds every block (== the old (128,N) buffer,
|
| 431 |
+
# same bytes, same addresses); at N=8192 it holds a rolling window of 4 blocks. The fill
|
| 432 |
+
# is a pure dma_transpose (no arithmetic), so any fill ORDER yields identical values.
|
| 433 |
+
x_t = allocator.alloc_sbuf_tensor(
|
| 434 |
+
shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
|
| 435 |
+
block_dim=[n_chunks, n_tt], num_free_tiles=[n_chunks, XT_RING], align_to=32)
|
| 436 |
+
|
| 437 |
+
if not _big:
|
| 438 |
+
# N<=2048: fill the whole buffer up front, exactly as the shipping path did (pure
|
| 439 |
+
# dma_transpose; block-indexed AP lands at the same addresses as the old (128,N) buffer).
|
| 440 |
+
for s in range(n_tt):
|
| 441 |
+
for c in range(n_chunks):
|
| 442 |
+
nisa.dma_transpose(
|
| 443 |
+
dst=x_t[c][s].ap([[_PSUM_FMAX, _P_MAX], [1, 1], [1, 1],
|
| 444 |
+
[1, min(_PSUM_FMAX, N - s * _PSUM_FMAX)]], offset=0),
|
| 445 |
+
src=h_hbm.ap([[H, min(_PSUM_FMAX, N - s * _PSUM_FMAX)], [1, 1], [1, 1], [1, _P_MAX]],
|
| 446 |
+
offset=rofs + (s * _PSUM_FMAX) * H + c * _P_MAX),
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
if _res:
|
| 450 |
+
w_sb, wv_sb, b_sb, bv_bc = r_w_sb, r_wv_sb, r_b_sb, r_bv_bc
|
| 451 |
+
else:
|
| 452 |
+
# this core's Q/K weight columns (lane-permuted) and V columns (natural)
|
| 453 |
+
w_sb = allocator.alloc_sbuf_tensor(
|
| 454 |
+
shape=(_P_MAX, local_quads * 2 * _P_MAX), dtype=nl.bfloat16,
|
| 455 |
+
block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
|
| 456 |
+
for qk in range(2):
|
| 457 |
+
for c in range(n_chunks):
|
| 458 |
+
nisa.dma_copy(dst=w_sb[qk][c],
|
| 459 |
+
src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX,
|
| 460 |
+
col0:col0 + local_quads * 2 * _P_MAX])
|
| 461 |
+
wv_sb = allocator.alloc_sbuf_tensor(
|
| 462 |
+
shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
|
| 463 |
+
block_dim=[n_chunks], num_free_tiles=[n_chunks])
|
| 464 |
+
for c in range(n_chunks):
|
| 465 |
+
nisa.dma_copy(dst=wv_sb[c],
|
| 466 |
+
src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX,
|
| 467 |
+
vcol0:vcol0 + local_heads * D])
|
| 468 |
+
|
| 469 |
+
# biases: qk as (128,1) per group; v broadcast to (128, 8*64) once
|
| 470 |
+
b_sb = allocator.alloc_sbuf_tensor(
|
| 471 |
+
shape=(_P_MAX, 1), dtype=nl.float32,
|
| 472 |
+
block_dim=[2, local_quads * 2], num_free_tiles=[2, local_quads * 2], align_to=4)
|
| 473 |
+
b_stage = allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
|
| 474 |
+
for qk in range(2):
|
| 475 |
+
for g in range(local_quads * 2):
|
| 476 |
+
nisa.dma_copy(dst=b_stage,
|
| 477 |
+
src=bqk_hbm[qk, col0 + g * _P_MAX:col0 + (g + 1) * _P_MAX, 0:1])
|
| 478 |
+
# SITE :224 -- bias CAST, left UNPINNED on purpose (0.09% of the window, already at
|
| 479 |
+
# the Vector fixed fee; nothing to move).
|
| 480 |
+
nisa.tensor_copy(dst=b_sb[qk][g], src=b_stage)
|
| 481 |
+
bv_bc = allocator.alloc_sbuf_tensor(
|
| 482 |
+
shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
|
| 483 |
+
nisa.dma_copy(dst=bv_bc,
|
| 484 |
+
src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]],
|
| 485 |
+
offset=vcol0))
|
| 486 |
+
|
| 487 |
+
# rope tables, resident (quad-tiled on the host: identical rows for every quad). Token-block
|
| 488 |
+
# ring, same gating as x_t: at N<=2048 TAB_RING==n_tt holds every block (== old (128,N)
|
| 489 |
+
# buffer, bit-for-bit); at N=8192 a rolling 4-block window filled inside the tt loop. Pure
|
| 490 |
+
# dma_copy -> identical values regardless of fill order.
|
| 491 |
+
if _res:
|
| 492 |
+
tab = r_tab
|
| 493 |
+
else:
|
| 494 |
+
tab = allocator.alloc_sbuf_tensor(
|
| 495 |
+
shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
|
| 496 |
+
block_dim=[4, n_tt], num_free_tiles=[4, XT_RING], align_to=4)
|
| 497 |
+
|
| 498 |
+
if not _big:
|
| 499 |
+
for s in range(n_tt):
|
| 500 |
+
for t_i in range(4):
|
| 501 |
+
_tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
|
| 502 |
+
nisa.dma_copy(dst=tab[t_i][s][:, 0:_tw],
|
| 503 |
+
src=tab_hbm[t_i, 0:_P_MAX,
|
| 504 |
+
s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
|
| 505 |
+
|
| 506 |
+
# ---- per-tile working set (reused across iterations; same tensor objects) --------
|
| 507 |
+
proj = allocator.alloc_sbuf_tensor(
|
| 508 |
+
shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16, block_dim=[2],
|
| 509 |
+
num_free_tiles=[2])
|
| 510 |
+
acc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
|
| 511 |
+
align_to=4)
|
| 512 |
+
tmp = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
|
| 513 |
+
align_to=4)
|
| 514 |
+
roped = allocator.alloc_sbuf_tensor(
|
| 515 |
+
shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16, block_dim=[2],
|
| 516 |
+
num_free_tiles=[2])
|
| 517 |
+
vout = allocator.alloc_sbuf_tensor(
|
| 518 |
+
shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
|
| 519 |
+
|
| 520 |
+
# Q staging via private_hbm, one per LOCAL head (vendor segmented-kernel pattern)
|
| 521 |
+
q_priv = []
|
| 522 |
+
for _lh in range(local_heads):
|
| 523 |
+
q_priv.append(nl.ndarray((1, D, N), dtype=nl.bfloat16, buffer=nl.private_hbm))
|
| 524 |
+
|
| 525 |
+
# ---- producer: per 512-token tile, straight through the pipeline ----------------
|
| 526 |
+
for tt in range(n_tt):
|
| 527 |
+
t0 = tt * _PSUM_FMAX
|
| 528 |
+
tw = min(_PSUM_FMAX, N - t0) # ragged last token tile (N % 512 != 0)
|
| 529 |
+
|
| 530 |
+
# N>2048: fill this token-block's x_t / tab slice NOW (rolling ring window). At
|
| 531 |
+
# N<=2048 the whole buffer was already filled up front (bit-for-bit), so skip.
|
| 532 |
+
if _big:
|
| 533 |
+
for c in range(n_chunks):
|
| 534 |
+
nisa.dma_transpose(
|
| 535 |
+
dst=x_t[c][tt].ap([[_PSUM_FMAX, _P_MAX], [1, 1], [1, 1], [1, _PSUM_FMAX]],
|
| 536 |
+
offset=0),
|
| 537 |
+
src=h_hbm.ap([[H, _PSUM_FMAX], [1, 1], [1, 1], [1, _P_MAX]],
|
| 538 |
+
offset=rofs + t0 * H + c * _P_MAX),
|
| 539 |
+
)
|
| 540 |
+
for t_i in range(4):
|
| 541 |
+
nisa.dma_copy(dst=tab[t_i][tt],
|
| 542 |
+
src=tab_hbm[t_i, 0:_P_MAX, t0:t0 + _PSUM_FMAX])
|
| 543 |
+
|
| 544 |
+
# V for all 8 local heads of this token tile.
|
| 545 |
+
for vsub in range(tw // _V_TILE):
|
| 546 |
+
ps_v = nl.ndarray((_V_TILE, local_heads * D), dtype=nl.float32,
|
| 547 |
+
buffer=nl.psum, address=(0, 7 * _PSUM_BANK))
|
| 548 |
+
for c in range(n_chunks):
|
| 549 |
+
nisa.nc_matmul(
|
| 550 |
+
dst=ps_v,
|
| 551 |
+
stationary=x_t[c][tt][:, vsub * _V_TILE:(vsub + 1) * _V_TILE],
|
| 552 |
+
moving=wv_sb[c],
|
| 553 |
+
accumulate=(c > 0),
|
| 554 |
+
)
|
| 555 |
+
nisa.tensor_tensor(dst=vout, data1=ps_v, data2=bv_bc, op=nl.add)
|
| 556 |
+
for lh in range(local_heads):
|
| 557 |
+
if _big:
|
| 558 |
+
# N>2048: stream V to HBM (SBUF->HBM dma from vout directly). The pinned
|
| 559 |
+
# :276 tensor_copy is the N<=2048 shipping site and is left untouched below.
|
| 560 |
+
nisa.dma_copy(
|
| 561 |
+
dst=v_hbm[lh][tt * v_per_tt + vsub],
|
| 562 |
+
src=vout[:, lh * D:(lh + 1) * D])
|
| 563 |
+
continue
|
| 564 |
+
# ======== SITE :276 -- pinned to Scalar. WAR chain of 8 copies read one vout
|
| 565 |
+
# before the next vsub overwrites it; Scalar (240 ns) beats GpSimd (375 ns) and
|
| 566 |
+
# sheds the copy off Vector, the binding engine. ========
|
| 567 |
+
nisa.tensor_copy(
|
| 568 |
+
dst=v_tiles[lh][tt * v_per_tt + vsub],
|
| 569 |
+
src=vout[:, lh * D:(lh + 1) * D],
|
| 570 |
+
engine=engine.scalar)
|
| 571 |
+
|
| 572 |
+
# Q and K for both local quads, both parities, this token tile.
|
| 573 |
+
for qk in range(2):
|
| 574 |
+
for j in range(local_quads):
|
| 575 |
+
for par in range(2):
|
| 576 |
+
g = j * 2 + par
|
| 577 |
+
cols = g * _P_MAX
|
| 578 |
+
ps = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32,
|
| 579 |
+
buffer=nl.psum,
|
| 580 |
+
address=(0, (5 + par) * _PSUM_BANK))
|
| 581 |
+
for c in range(n_chunks):
|
| 582 |
+
nisa.nc_matmul(
|
| 583 |
+
dst=ps[:, 0:tw],
|
| 584 |
+
stationary=w_sb[qk][c][:, cols:cols + _P_MAX],
|
| 585 |
+
moving=x_t[c][tt][:, 0:tw],
|
| 586 |
+
accumulate=(c > 0),
|
| 587 |
+
)
|
| 588 |
+
# ======== SITE :458 -- pinned to Scalar. Largest remaining Vector-resident
|
| 589 |
+
# relocatable site (768/core); data=ps is PSUM so GpSimd is illegal, and
|
| 590 |
+
# Scalar's 612 ns fee is faster than the 744 ns Vector cell, so the
|
| 591 |
+
# zero-lead rope chain consuming proj cannot lengthen. ========
|
| 592 |
+
# NKI 0.7: a Scalar-engine pin here fails ISA opcode_engine_check on newer compilers
|
| 593 |
+
# TOT compiler -> unpinned (compiler picks the engine).
|
| 594 |
+
nisa.tensor_scalar(dst=proj[par][:, 0:tw], data=ps[:, 0:tw],
|
| 595 |
+
op0=nl.add, operand0=b_sb[qk][g])
|
| 596 |
+
|
| 597 |
+
# rope: the six ops of nki_rope, same order, tables sliced per tile
|
| 598 |
+
nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[0][:, 0:tw],
|
| 599 |
+
data2=tab[0][tt][:, 0:tw], op=nl.multiply)
|
| 600 |
+
nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[1][:, 0:tw],
|
| 601 |
+
data2=tab[2][tt][:, 0:tw], op=nl.multiply)
|
| 602 |
+
nisa.tensor_tensor(dst=roped[0][:, 0:tw], data1=acc[:, 0:tw], data2=tmp[:, 0:tw], op=nl.add)
|
| 603 |
+
nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[1][:, 0:tw],
|
| 604 |
+
data2=tab[1][tt][:, 0:tw], op=nl.multiply)
|
| 605 |
+
nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[0][:, 0:tw],
|
| 606 |
+
data2=tab[3][tt][:, 0:tw], op=nl.multiply)
|
| 607 |
+
nisa.tensor_tensor(dst=roped[1][:, 0:tw], data1=acc[:, 0:tw], data2=tmp[:, 0:tw], op=nl.add)
|
| 608 |
+
|
| 609 |
+
# scatter: head b of the quad owns rows [b*32,(b+1)*32)
|
| 610 |
+
for b in range(quad):
|
| 611 |
+
lh = j * quad + b
|
| 612 |
+
r0 = b * half
|
| 613 |
+
if qk == 1:
|
| 614 |
+
for par2 in range(2):
|
| 615 |
+
if _big:
|
| 616 |
+
# N>2048: stream K to HBM (SBUF->HBM dma). The :483 tensor_copy
|
| 617 |
+
# below is the N<=2048 shipping site, left untouched.
|
| 618 |
+
nisa.dma_copy(
|
| 619 |
+
dst=k_hbm[lh][tt][par2 * half:(par2 + 1) * half,
|
| 620 |
+
0:_K_TILE],
|
| 621 |
+
src=roped[par2][r0:r0 + half, 0:_PSUM_FMAX])
|
| 622 |
+
continue
|
| 623 |
+
# SITE :483 -- K-scatter COPY, left UNPINNED (no affordable landing
|
| 624 |
+
# engine: it is a WAR chain against the next iteration's rope write).
|
| 625 |
+
nisa.tensor_copy(
|
| 626 |
+
dst=k_tiles[lh][tt][par2 * half:(par2 + 1) * half,
|
| 627 |
+
0:tw],
|
| 628 |
+
src=roped[par2][r0:r0 + half, 0:tw])
|
| 629 |
+
else:
|
| 630 |
+
for par2 in range(2):
|
| 631 |
+
nisa.dma_copy(
|
| 632 |
+
dst=q_priv[lh][0, par2 * half:(par2 + 1) * half,
|
| 633 |
+
t0:t0 + tw],
|
| 634 |
+
src=roped[par2][r0:r0 + half, 0:tw])
|
| 635 |
+
|
| 636 |
+
# ---- PHASE 2: reclaim the dead producer buffers. NO ALIASING. -------------------
|
| 637 |
+
allocator.set_current_address(phase2_base)
|
| 638 |
+
|
| 639 |
+
if _res:
|
| 640 |
+
wo_sb, bo_bc = r_wo_sb, r_bo_bc
|
| 641 |
+
else:
|
| 642 |
+
# ---- out-projection weights: THIS core's feature rows, all H output columns -------
|
| 643 |
+
wo_sb = allocator.alloc_sbuf_tensor(
|
| 644 |
+
shape=(_P_MAX, H), dtype=nl.bfloat16,
|
| 645 |
+
block_dim=[n_ochunks], num_free_tiles=[n_ochunks])
|
| 646 |
+
for oc in range(n_ochunks):
|
| 647 |
+
nisa.dma_copy(dst=wo_sb[oc],
|
| 648 |
+
src=wot_hbm[col0 + oc * _P_MAX:col0 + (oc + 1) * _P_MAX, 0:H])
|
| 649 |
+
# bias already HALVED on the host (pack_out_weights) so the two cores' partials sum to
|
| 650 |
+
# exactly one bias. Broadcast across partitions (output columns are on the free axis).
|
| 651 |
+
bo_bc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
|
| 652 |
+
nisa.dma_copy(dst=bo_bc,
|
| 653 |
+
src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
|
| 654 |
+
|
| 655 |
+
# ---- O STAYS IN SBUF, ALLOCATED EXACTLY ONCE. tp_out=True gives [D, 128 tokens] per Q
|
| 656 |
+
# group (feature-major), which the projection contracts over, so O never transposes.
|
| 657 |
+
# The list handed to the vendor is SLICES into o_feat, so _write_back_o_impl copies
|
| 658 |
+
# straight into final position. ------------------------------------------------------
|
| 659 |
+
n_grp = N // _P_MAX
|
| 660 |
+
o_feat = allocator.alloc_sbuf_tensor(
|
| 661 |
+
shape=(_P_MAX, N), dtype=nl.bfloat16,
|
| 662 |
+
block_dim=[n_ochunks], num_free_tiles=[n_ochunks], align_to=32)
|
| 663 |
+
o_sb = []
|
| 664 |
+
for _lh in range(local_heads):
|
| 665 |
+
oc = (_lh * D) // _P_MAX
|
| 666 |
+
rr = (_lh * D) % _P_MAX
|
| 667 |
+
# explicit loop, NOT a comprehension: the NKI tracer rejects comprehensions in a kernel.
|
| 668 |
+
grp = []
|
| 669 |
+
for _g in range(n_grp):
|
| 670 |
+
grp.append(o_feat[oc][rr:rr + D, _g * _P_MAX:(_g + 1) * _P_MAX])
|
| 671 |
+
o_sb.append(grp)
|
| 672 |
+
|
| 673 |
+
attn_base = allocator.get_current_address()
|
| 674 |
+
for lh in range(local_heads):
|
| 675 |
+
if _big:
|
| 676 |
+
# N>2048: load THIS head's K/V from HBM into the resident staging rings (only one
|
| 677 |
+
# head live at a time -> K/V cost is one head's 24576 B/lane, not eight heads'
|
| 678 |
+
# 196608). Then run the SAME single-section flatten the N<=2048 path uses: seqlen_k
|
| 679 |
+
# =8192 < 10240 threshold => num_sections==1, no multi-section flash combine. The
|
| 680 |
+
# inner's large-tile loop iterates 4x over _LARGE_TILE_SZ-sized buffers; O is written
|
| 681 |
+
# straight into this head's o_feat SBUF slices (o_sb[lh]), exactly as N<=2048.
|
| 682 |
+
for _i in range(n_ktiles):
|
| 683 |
+
nisa.dma_copy(dst=k_stage[_i], src=k_hbm[lh][_i])
|
| 684 |
+
for _i in range(n_vtiles):
|
| 685 |
+
nisa.dma_copy(dst=v_stage[_i], src=v_hbm[lh][_i])
|
| 686 |
+
# 1D python-list views [seq_tile] (the flatten indexes k_sb[k_tile_idx_in_section]
|
| 687 |
+
# and v_sb[v_tile_idx] as flat lists -- same convention as k_tiles[lh]/v_tiles[lh]).
|
| 688 |
+
k1d = []
|
| 689 |
+
for _i in range(n_ktiles):
|
| 690 |
+
k1d.append(k_stage[_i])
|
| 691 |
+
v1d = []
|
| 692 |
+
for _i in range(n_vtiles):
|
| 693 |
+
v1d.append(v_stage[_i])
|
| 694 |
+
_attention_cte(
|
| 695 |
+
q_priv[lh], None, None,
|
| 696 |
+
scale=scale, causal_mask=False,
|
| 697 |
+
tp_q=False, tp_k=False, tp_out=True,
|
| 698 |
+
cache_softmax=False,
|
| 699 |
+
k_cache_sbuf=k1d,
|
| 700 |
+
v_cache_sbuf=v1d,
|
| 701 |
+
out_o_hbm=o_sb[lh],
|
| 702 |
+
init_sbuf_addr=attn_base,
|
| 703 |
+
seqlen_k_valid=n_valid,
|
| 704 |
+
)
|
| 705 |
+
else:
|
| 706 |
+
# N<=2048: the pinned single-section flatten (h8/h10 engine pins intact, bit-for-bit).
|
| 707 |
+
_attention_cte(
|
| 708 |
+
q_priv[lh], None, None,
|
| 709 |
+
scale=scale, causal_mask=False,
|
| 710 |
+
tp_q=False, tp_k=False, tp_out=True,
|
| 711 |
+
cache_softmax=False,
|
| 712 |
+
k_cache_sbuf=k_tiles[lh],
|
| 713 |
+
v_cache_sbuf=v_tiles[lh],
|
| 714 |
+
out_o_hbm=o_sb[lh],
|
| 715 |
+
init_sbuf_addr=attn_base,
|
| 716 |
+
seqlen_k_valid=n_valid,
|
| 717 |
+
)
|
| 718 |
+
allocator.set_current_address(attn_base)
|
| 719 |
+
|
| 720 |
+
# ---- PARTIAL out projection: tokens on partitions, output columns on the free axis
|
| 721 |
+
# (chosen by the HBM write: result is token-major, so a row of consecutive output columns
|
| 722 |
+
# is a 1024-byte contiguous run). 128 calls/layer/core (16 token blocks x 2 col blocks x
|
| 723 |
+
# 4 chunks). --------------------------------------------------------------------------
|
| 724 |
+
out_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX),
|
| 725 |
+
dtype=nl.bfloat16, align_to=32)
|
| 726 |
+
n_ob = (H + _PSUM_FMAX - 1) // _PSUM_FMAX
|
| 727 |
+
for tb in range(N // _P_MAX):
|
| 728 |
+
for ob in range(n_ob):
|
| 729 |
+
ow = min(_PSUM_FMAX, H - ob * _PSUM_FMAX) # ragged last column block (H%512!=0)
|
| 730 |
+
ps_out = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32, buffer=nl.psum,
|
| 731 |
+
address=(0, 0))
|
| 732 |
+
for oc in range(n_ochunks):
|
| 733 |
+
nisa.nc_matmul(
|
| 734 |
+
dst=ps_out[:, 0:ow],
|
| 735 |
+
stationary=o_feat[oc][:, tb * _P_MAX:(tb + 1) * _P_MAX],
|
| 736 |
+
moving=wo_sb[oc][:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow],
|
| 737 |
+
accumulate=(oc > 0),
|
| 738 |
+
)
|
| 739 |
+
nisa.tensor_tensor(dst=out_sb[:, 0:ow], data1=ps_out[:, 0:ow],
|
| 740 |
+
data2=bo_bc[:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow],
|
| 741 |
+
op=nl.add)
|
| 742 |
+
nisa.dma_copy(
|
| 743 |
+
dst=result.ap(pattern=[[H, _P_MAX], [1, ow]],
|
| 744 |
+
offset=out_slot * NT * H + rofs + tb * _P_MAX * H + ob * _PSUM_FMAX),
|
| 745 |
+
src=out_sb[:, 0:ow])
|
| 746 |
+
return result
|
| 747 |
+
|
| 748 |
+
|
| 749 |
+
_ATTN = {g: wrap_nki_to_torch_hop(_attention_block_kernel[g]) for g in sorted({1, LNC})}
|
| 750 |
+
|
| 751 |
+
|
| 752 |
+
def _no_grad(*tensors) -> None:
|
| 753 |
+
for t in tensors:
|
| 754 |
+
if t is not None and t.requires_grad:
|
| 755 |
+
raise RuntimeError(
|
| 756 |
+
"attention_block registers no autograd rule and its packed weights are "
|
| 757 |
+
"detached caches. Use under torch.no_grad()/eval().")
|
| 758 |
+
|
| 759 |
+
|
| 760 |
+
@nki_op("vit_enc::attention_block", mutates_args={})
|
| 761 |
+
def attention_block(
|
| 762 |
+
h: torch.Tensor,
|
| 763 |
+
wqk: torch.Tensor,
|
| 764 |
+
bqk: torch.Tensor,
|
| 765 |
+
tab: torch.Tensor,
|
| 766 |
+
wvt: torch.Tensor,
|
| 767 |
+
bv: torch.Tensor,
|
| 768 |
+
wot: torch.Tensor,
|
| 769 |
+
bo: torch.Tensor,
|
| 770 |
+
scale: float,
|
| 771 |
+
head_dim: int,
|
| 772 |
+
n_valid: int,
|
| 773 |
+
hb: int = 0,
|
| 774 |
+
grid: int = LNC,
|
| 775 |
+
nimg: int = 1,
|
| 776 |
+
resident: int = 0,
|
| 777 |
+
) -> torch.Tensor:
|
| 778 |
+
"""Fused norm1'd-hidden -> QKV+rope -> attention -> fused out-projection PARTIALS.
|
| 779 |
+
|
| 780 |
+
Returns ``[LNC=2, N, H]`` bf16 per-core out-projection partials; K3's residual add sums the
|
| 781 |
+
two cores. The two Scalar engine pins (V-scatter :276, QK bias-evict :458) are hardcoded.
|
| 782 |
+
"""
|
| 783 |
+
_no_grad(h, wqk, bqk, tab, wvt, bv, wot, bo)
|
| 784 |
+
# N>2048 (e.g. 64-frame clips) is NOT supported. The phase-1 memory fixes fit (~106 KiB/lane),
|
| 785 |
+
# but the single-section vendor inner overflows phase 2 (measured 354912 B/lane vs the 196608
|
| 786 |
+
# budget: exp_tp_sb/mm1_masked/exp_sb scale with num_large_tiles_per_section=ceil(N/2048)=4).
|
| 787 |
+
# The shrinkable phase-2 region (78848 B/lane) is smaller than the 158304 overflow, so
|
| 788 |
+
# streaming O / shrinking o_feat cannot close the gap; re-enabling flash sectioning fits but
|
| 789 |
+
# is numerically wrong (rel_l2 0.90). Refuse here rather than emit a non-fitting or wrong
|
| 790 |
+
# kernel. See the V-JEPA2 kernels' verification notes. NKI kernels cannot `raise`, so the guard lives here.
|
| 791 |
+
N = h.shape[0] // nimg
|
| 792 |
+
if N > 2048:
|
| 793 |
+
raise NotImplementedError(
|
| 794 |
+
f"attention_block N={N} (>2048, e.g. 64-frame clips) is not supported: the "
|
| 795 |
+
f"single-section vendor inner does not fit SBUF (phase-2 peak measured 354912 B/lane "
|
| 796 |
+
f"vs the 196608 budget). Streaming O or shrinking o_feat cannot close the 158304 "
|
| 797 |
+
f"B/lane gap, and flash sectioning is numerically wrong (rel_l2 0.90). See "
|
| 798 |
+
f"the V-JEPA2 kernels' verification notes. Use N<=2048 (up to 16-frame clips) -- the bit-for-bit "
|
| 799 |
+
f"shipping path."
|
| 800 |
+
)
|
| 801 |
+
return _ATTN[grid](h, wqk, bqk, tab, wvt, bv, wot, bo, scale, head_dim, n_valid, hb, grid, nimg, resident)
|
build/torch-neuron/nki_kernels/attention_block_l1.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Attention for LNC=1: ONE program over ALL head groups.
|
| 2 |
+
|
| 3 |
+
The 2-core kernel in ``attention_block.py`` owns one 8-head group per program. At LNC=1 that
|
| 4 |
+
means one launch per group per layer (2 for ViT-L, 3 for ViT-H+), each re-transposing x, each
|
| 5 |
+
re-loading the rope tables, each writing an [N, H] bf16 out-projection PARTIAL to HBM that the
|
| 6 |
+
LN-fold kernel then sums.
|
| 7 |
+
|
| 8 |
+
This kernel, per image (``nimg`` images stacked on the row axis, one launch):
|
| 9 |
+
* transposes x once and loads the rope tables once (shared by every group: tables are
|
| 10 |
+
quad-tiled per head, identical for all heads);
|
| 11 |
+
* for each group: projects Q/K/V + RoPE (same producer as attention_block), runs the vendor
|
| 12 |
+
attention per head, and writes O feature-major into a PERSISTENT per-group SBUF slab;
|
| 13 |
+
* after all groups: ONE out-projection contracting over ALL features (G*4 chunks accumulate
|
| 14 |
+
in one fp32 PSUM tile), adds the FULL bias once, writes ONE [N, H] result.
|
| 15 |
+
No partials, no host sum; the LN-fold kernel receives a single branch.
|
| 16 |
+
|
| 17 |
+
``resident=1``: Q/K/V/O weights, biases and rope tables are staged ONCE per launch (bottom of
|
| 18 |
+
SBUF) and reused by every image. Host decides by an SBUF estimate (does not fit at N>=1024 for
|
| 19 |
+
ViT-L); ``resident=0`` restages each group's weights per image.
|
| 20 |
+
|
| 21 |
+
N<=2048 only (the shipping single-section vendor path). Numerics: identical producer and vendor
|
| 22 |
+
attention; the out-projection now accumulates all groups in fp32 PSUM instead of summing bf16
|
| 23 |
+
per-group partials, so results differ from attention_block by bf16 rounding of the partials.
|
| 24 |
+
"""
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
from nki.framework.kernel import wrap_nki_to_torch_hop
|
| 29 |
+
from torch_neuronx import nki_op
|
| 30 |
+
|
| 31 |
+
import nki
|
| 32 |
+
import nki.isa as nisa
|
| 33 |
+
import nki.language as nl
|
| 34 |
+
from nki.isa import engine
|
| 35 |
+
|
| 36 |
+
from nkilib.core.utils.modular_allocator import ModularAllocator
|
| 37 |
+
|
| 38 |
+
from .attention_inner import _attention_cte
|
| 39 |
+
|
| 40 |
+
_P_MAX = 128
|
| 41 |
+
_PSUM_FMAX = 512
|
| 42 |
+
_PSUM_BANK = 2048
|
| 43 |
+
_K_TILE = 512
|
| 44 |
+
_V_TILE = 128
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _kernel_assert(cond: bool, msg: str):
|
| 48 |
+
assert cond, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {msg}"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def sbuf_estimate(H: int, Fp: int, N: int, D: int, resident: bool) -> int:
|
| 52 |
+
"""Conservative bytes/lane at the kernel's peak (host-side decision helper, not exact)."""
|
| 53 |
+
G = Fp // _PSUM_FMAX
|
| 54 |
+
n_chunks = H // _P_MAX
|
| 55 |
+
n_tt = (N + _PSUM_FMAX - 1) // _PSUM_FMAX
|
| 56 |
+
n_ktiles = (N + _K_TILE - 1) // _K_TILE
|
| 57 |
+
lh = _PSUM_FMAX // D
|
| 58 |
+
n_och = (lh * D) // _P_MAX
|
| 59 |
+
w_grp = 3 * n_chunks * _PSUM_FMAX * 2 + _PSUM_FMAX * 4 + 64 # wqk + wv + bv_bc + biases
|
| 60 |
+
w_out = G * n_och * H * 2 + H * 4 # wo + bo_bc
|
| 61 |
+
tab = 4 * n_tt * _PSUM_FMAX * 4
|
| 62 |
+
o_feat = G * n_och * N * 2
|
| 63 |
+
x_t = n_chunks * n_tt * _PSUM_FMAX * 2
|
| 64 |
+
kv = lh * (n_ktiles * _K_TILE * 2 + (N // _V_TILE) * D * 2)
|
| 65 |
+
work = 2 * _PSUM_FMAX * 2 * 2 + 2 * _PSUM_FMAX * 4 + _PSUM_FMAX * 4
|
| 66 |
+
inner = 48 * 1024
|
| 67 |
+
resid = (G * w_grp + w_out + tab) if resident else 0
|
| 68 |
+
base = resid + o_feat + x_t + (0 if resident else tab)
|
| 69 |
+
p1 = kv + (0 if resident else w_grp) + work
|
| 70 |
+
p2 = kv + inner
|
| 71 |
+
p3 = (0 if resident else w_out) + 2048
|
| 72 |
+
return base + max(p1, p2, p3)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@nki.jit
|
| 76 |
+
def _attention_block_l1_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_hbm,
|
| 77 |
+
wot_hbm, bo_hbm, scale, head_dim, n_valid, nimg=1, resident=0):
|
| 78 |
+
NT, H = h_hbm.shape
|
| 79 |
+
N = NT // nimg
|
| 80 |
+
Fp = wqk_hbm.shape[2]
|
| 81 |
+
D = head_dim
|
| 82 |
+
half = D // 2
|
| 83 |
+
quad = _P_MAX // half
|
| 84 |
+
local_heads = _PSUM_FMAX // D
|
| 85 |
+
local_quads = local_heads // quad
|
| 86 |
+
G = Fp // _PSUM_FMAX # head groups, ALL handled by this one program
|
| 87 |
+
n_chunks = H // _P_MAX
|
| 88 |
+
n_tt = (N + _PSUM_FMAX - 1) // _PSUM_FMAX
|
| 89 |
+
n_ktiles = (N + _K_TILE - 1) // _K_TILE
|
| 90 |
+
n_vtiles = N // _V_TILE
|
| 91 |
+
n_ochunks = (local_heads * D) // _P_MAX # feature chunks per group
|
| 92 |
+
v_per_tt = _PSUM_FMAX // _V_TILE
|
| 93 |
+
|
| 94 |
+
_kernel_assert(wqk_hbm.shape == (2, H, Fp), "bad wqk")
|
| 95 |
+
_kernel_assert(bqk_hbm.shape == (2, Fp, 1), "bad bqk")
|
| 96 |
+
_kernel_assert(tab_hbm.shape == (4, _P_MAX, N), "bad tab")
|
| 97 |
+
_kernel_assert(wvt_hbm.shape == (H, Fp) and bv_hbm.shape == (1, Fp), "bad V weights")
|
| 98 |
+
_kernel_assert(wot_hbm.shape == (Fp, H) and bo_hbm.shape == (1, H), "bad O weights")
|
| 99 |
+
_kernel_assert(H % _P_MAX == 0, "H must be a multiple of 128")
|
| 100 |
+
_kernel_assert(quad * half == _P_MAX, "D must give quad*half=128")
|
| 101 |
+
_kernel_assert(Fp % _PSUM_FMAX == 0, "padded features must be a multiple of 512")
|
| 102 |
+
_kernel_assert(N % _P_MAX == 0, "N must be a multiple of 128")
|
| 103 |
+
_kernel_assert(N <= _PSUM_FMAX * 4, "N>2048 not supported")
|
| 104 |
+
_kernel_assert(NT == nimg * N, "rows must be nimg x N")
|
| 105 |
+
|
| 106 |
+
result = nl.ndarray((1, NT, H), dtype=nl.bfloat16, buffer=nl.shared_hbm)
|
| 107 |
+
|
| 108 |
+
# ---------------- resident (per-launch invariant) staging ----------------
|
| 109 |
+
res_top = 0
|
| 110 |
+
r_w = []
|
| 111 |
+
r_wv = []
|
| 112 |
+
r_b = []
|
| 113 |
+
r_bv = []
|
| 114 |
+
if resident:
|
| 115 |
+
ra = ModularAllocator(initial_address=0)
|
| 116 |
+
for g in range(G):
|
| 117 |
+
col0 = g * _PSUM_FMAX
|
| 118 |
+
w_sb_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2 * _P_MAX), dtype=nl.bfloat16,
|
| 119 |
+
block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
|
| 120 |
+
for qk in range(2):
|
| 121 |
+
for c in range(n_chunks):
|
| 122 |
+
nisa.dma_copy(dst=w_sb_[qk][c],
|
| 123 |
+
src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX,
|
| 124 |
+
col0:col0 + local_quads * 2 * _P_MAX])
|
| 125 |
+
wv_sb_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
|
| 126 |
+
block_dim=[n_chunks], num_free_tiles=[n_chunks])
|
| 127 |
+
for c in range(n_chunks):
|
| 128 |
+
nisa.dma_copy(dst=wv_sb_[c],
|
| 129 |
+
src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX, col0:col0 + local_heads * D])
|
| 130 |
+
b_sb_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
|
| 131 |
+
block_dim=[2, local_quads * 2],
|
| 132 |
+
num_free_tiles=[2, local_quads * 2], align_to=4)
|
| 133 |
+
b_stage_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
|
| 134 |
+
for qk in range(2):
|
| 135 |
+
for gg in range(local_quads * 2):
|
| 136 |
+
nisa.dma_copy(dst=b_stage_,
|
| 137 |
+
src=bqk_hbm[qk, col0 + gg * _P_MAX:col0 + (gg + 1) * _P_MAX, 0:1])
|
| 138 |
+
nisa.tensor_copy(dst=b_sb_[qk][gg], src=b_stage_)
|
| 139 |
+
bv_bc_ = ra.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
|
| 140 |
+
nisa.dma_copy(dst=bv_bc_,
|
| 141 |
+
src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]], offset=col0))
|
| 142 |
+
r_w.append(w_sb_)
|
| 143 |
+
r_wv.append(wv_sb_)
|
| 144 |
+
r_b.append(b_sb_)
|
| 145 |
+
r_bv.append(bv_bc_)
|
| 146 |
+
r_tab = ra.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
|
| 147 |
+
block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
|
| 148 |
+
for s in range(n_tt):
|
| 149 |
+
for t_i in range(4):
|
| 150 |
+
_tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
|
| 151 |
+
nisa.dma_copy(dst=r_tab[t_i][s][:, 0:_tw],
|
| 152 |
+
src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
|
| 153 |
+
r_wo = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
|
| 154 |
+
block_dim=[G * n_ochunks], num_free_tiles=[G * n_ochunks])
|
| 155 |
+
for oc in range(G * n_ochunks):
|
| 156 |
+
nisa.dma_copy(dst=r_wo[oc], src=wot_hbm[oc * _P_MAX:(oc + 1) * _P_MAX, 0:H])
|
| 157 |
+
r_bo = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
|
| 158 |
+
nisa.dma_copy(dst=r_bo, src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
|
| 159 |
+
res_top = ra.get_current_address()
|
| 160 |
+
|
| 161 |
+
n_grp = N // _P_MAX
|
| 162 |
+
for img in range(nimg):
|
| 163 |
+
rofs = img * N * H
|
| 164 |
+
allocator = ModularAllocator(initial_address=res_top)
|
| 165 |
+
|
| 166 |
+
# O for ALL groups stays in SBUF until the single out-projection.
|
| 167 |
+
o_feat = []
|
| 168 |
+
for g in range(G):
|
| 169 |
+
of_ = allocator.alloc_sbuf_tensor(shape=(_P_MAX, N), dtype=nl.bfloat16,
|
| 170 |
+
block_dim=[n_ochunks], num_free_tiles=[n_ochunks],
|
| 171 |
+
align_to=32)
|
| 172 |
+
o_feat.append(of_)
|
| 173 |
+
|
| 174 |
+
# x^T: shared by every group (transposed ONCE per image).
|
| 175 |
+
x_t = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
|
| 176 |
+
block_dim=[n_chunks, n_tt], num_free_tiles=[n_chunks, n_tt],
|
| 177 |
+
align_to=32)
|
| 178 |
+
for s in range(n_tt):
|
| 179 |
+
for c in range(n_chunks):
|
| 180 |
+
nisa.dma_transpose(
|
| 181 |
+
dst=x_t[c][s].ap([[_PSUM_FMAX, _P_MAX], [1, 1], [1, 1],
|
| 182 |
+
[1, min(_PSUM_FMAX, N - s * _PSUM_FMAX)]], offset=0),
|
| 183 |
+
src=h_hbm.ap([[H, min(_PSUM_FMAX, N - s * _PSUM_FMAX)], [1, 1], [1, 1], [1, _P_MAX]],
|
| 184 |
+
offset=rofs + (s * _PSUM_FMAX) * H + c * _P_MAX),
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
# rope tables: shared by every group.
|
| 188 |
+
if resident:
|
| 189 |
+
tab = r_tab
|
| 190 |
+
else:
|
| 191 |
+
tab = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
|
| 192 |
+
block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
|
| 193 |
+
for s in range(n_tt):
|
| 194 |
+
for t_i in range(4):
|
| 195 |
+
_tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
|
| 196 |
+
nisa.dma_copy(dst=tab[t_i][s][:, 0:_tw],
|
| 197 |
+
src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
|
| 198 |
+
|
| 199 |
+
img_base = allocator.get_current_address()
|
| 200 |
+
|
| 201 |
+
for g in range(G):
|
| 202 |
+
allocator.set_current_address(img_base)
|
| 203 |
+
col0 = g * _PSUM_FMAX
|
| 204 |
+
|
| 205 |
+
k_tiles = []
|
| 206 |
+
v_tiles = []
|
| 207 |
+
for _lh in range(local_heads):
|
| 208 |
+
kt_ = allocator.alloc_sbuf_tensor(shape=(D, _K_TILE), dtype=nl.bfloat16,
|
| 209 |
+
block_dim=[n_ktiles], num_free_tiles=[n_ktiles],
|
| 210 |
+
align_to=32)
|
| 211 |
+
k_tiles.append(kt_)
|
| 212 |
+
vt_ = allocator.alloc_sbuf_tensor(shape=(_V_TILE, D), dtype=nl.bfloat16,
|
| 213 |
+
block_dim=[n_vtiles], num_free_tiles=[n_vtiles])
|
| 214 |
+
v_tiles.append(vt_)
|
| 215 |
+
phase2_base = allocator.get_current_address()
|
| 216 |
+
|
| 217 |
+
if resident:
|
| 218 |
+
w_sb = r_w[g]
|
| 219 |
+
wv_sb = r_wv[g]
|
| 220 |
+
b_sb = r_b[g]
|
| 221 |
+
bv_bc = r_bv[g]
|
| 222 |
+
else:
|
| 223 |
+
w_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2 * _P_MAX),
|
| 224 |
+
dtype=nl.bfloat16,
|
| 225 |
+
block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
|
| 226 |
+
for qk in range(2):
|
| 227 |
+
for c in range(n_chunks):
|
| 228 |
+
nisa.dma_copy(dst=w_sb[qk][c],
|
| 229 |
+
src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX,
|
| 230 |
+
col0:col0 + local_quads * 2 * _P_MAX])
|
| 231 |
+
wv_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
|
| 232 |
+
block_dim=[n_chunks], num_free_tiles=[n_chunks])
|
| 233 |
+
for c in range(n_chunks):
|
| 234 |
+
nisa.dma_copy(dst=wv_sb[c],
|
| 235 |
+
src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX, col0:col0 + local_heads * D])
|
| 236 |
+
b_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
|
| 237 |
+
block_dim=[2, local_quads * 2],
|
| 238 |
+
num_free_tiles=[2, local_quads * 2], align_to=4)
|
| 239 |
+
b_stage = allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
|
| 240 |
+
for qk in range(2):
|
| 241 |
+
for gg in range(local_quads * 2):
|
| 242 |
+
nisa.dma_copy(dst=b_stage,
|
| 243 |
+
src=bqk_hbm[qk, col0 + gg * _P_MAX:col0 + (gg + 1) * _P_MAX, 0:1])
|
| 244 |
+
nisa.tensor_copy(dst=b_sb[qk][gg], src=b_stage)
|
| 245 |
+
bv_bc = allocator.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32,
|
| 246 |
+
align_to=4)
|
| 247 |
+
nisa.dma_copy(dst=bv_bc,
|
| 248 |
+
src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]], offset=col0))
|
| 249 |
+
|
| 250 |
+
proj = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
|
| 251 |
+
block_dim=[2], num_free_tiles=[2])
|
| 252 |
+
acc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32, align_to=4)
|
| 253 |
+
tmp = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32, align_to=4)
|
| 254 |
+
roped = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
|
| 255 |
+
block_dim=[2], num_free_tiles=[2])
|
| 256 |
+
vout = allocator.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32,
|
| 257 |
+
align_to=4)
|
| 258 |
+
q_priv = []
|
| 259 |
+
for _lh in range(local_heads):
|
| 260 |
+
q_priv.append(nl.ndarray((1, D, N), dtype=nl.bfloat16, buffer=nl.private_hbm))
|
| 261 |
+
|
| 262 |
+
# ---- producer (identical to attention_block's N<=2048 path) ----
|
| 263 |
+
for tt in range(n_tt):
|
| 264 |
+
t0 = tt * _PSUM_FMAX
|
| 265 |
+
tw = min(_PSUM_FMAX, N - t0)
|
| 266 |
+
for vsub in range(tw // _V_TILE):
|
| 267 |
+
ps_v = nl.ndarray((_V_TILE, local_heads * D), dtype=nl.float32,
|
| 268 |
+
buffer=nl.psum, address=(0, 7 * _PSUM_BANK))
|
| 269 |
+
for c in range(n_chunks):
|
| 270 |
+
nisa.nc_matmul(dst=ps_v,
|
| 271 |
+
stationary=x_t[c][tt][:, vsub * _V_TILE:(vsub + 1) * _V_TILE],
|
| 272 |
+
moving=wv_sb[c], accumulate=(c > 0))
|
| 273 |
+
nisa.tensor_tensor(dst=vout, data1=ps_v, data2=bv_bc, op=nl.add)
|
| 274 |
+
for lh in range(local_heads):
|
| 275 |
+
nisa.tensor_copy(dst=v_tiles[lh][tt * v_per_tt + vsub],
|
| 276 |
+
src=vout[:, lh * D:(lh + 1) * D], engine=engine.scalar)
|
| 277 |
+
for qk in range(2):
|
| 278 |
+
for j in range(local_quads):
|
| 279 |
+
for par in range(2):
|
| 280 |
+
gq = j * 2 + par
|
| 281 |
+
cols = gq * _P_MAX
|
| 282 |
+
ps = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32, buffer=nl.psum,
|
| 283 |
+
address=(0, (5 + par) * _PSUM_BANK))
|
| 284 |
+
for c in range(n_chunks):
|
| 285 |
+
nisa.nc_matmul(dst=ps[:, 0:tw],
|
| 286 |
+
stationary=w_sb[qk][c][:, cols:cols + _P_MAX],
|
| 287 |
+
moving=x_t[c][tt][:, 0:tw], accumulate=(c > 0))
|
| 288 |
+
nisa.tensor_scalar(dst=proj[par][:, 0:tw], data=ps[:, 0:tw],
|
| 289 |
+
op0=nl.add, operand0=b_sb[qk][gq])
|
| 290 |
+
nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[0][:, 0:tw],
|
| 291 |
+
data2=tab[0][tt][:, 0:tw], op=nl.multiply)
|
| 292 |
+
nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[1][:, 0:tw],
|
| 293 |
+
data2=tab[2][tt][:, 0:tw], op=nl.multiply)
|
| 294 |
+
nisa.tensor_tensor(dst=roped[0][:, 0:tw], data1=acc[:, 0:tw],
|
| 295 |
+
data2=tmp[:, 0:tw], op=nl.add)
|
| 296 |
+
nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[1][:, 0:tw],
|
| 297 |
+
data2=tab[1][tt][:, 0:tw], op=nl.multiply)
|
| 298 |
+
nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[0][:, 0:tw],
|
| 299 |
+
data2=tab[3][tt][:, 0:tw], op=nl.multiply)
|
| 300 |
+
nisa.tensor_tensor(dst=roped[1][:, 0:tw], data1=acc[:, 0:tw],
|
| 301 |
+
data2=tmp[:, 0:tw], op=nl.add)
|
| 302 |
+
for b in range(quad):
|
| 303 |
+
lh = j * quad + b
|
| 304 |
+
r0 = b * half
|
| 305 |
+
if qk == 1:
|
| 306 |
+
for par2 in range(2):
|
| 307 |
+
nisa.tensor_copy(
|
| 308 |
+
dst=k_tiles[lh][tt][par2 * half:(par2 + 1) * half, 0:tw],
|
| 309 |
+
src=roped[par2][r0:r0 + half, 0:tw])
|
| 310 |
+
else:
|
| 311 |
+
for par2 in range(2):
|
| 312 |
+
nisa.dma_copy(
|
| 313 |
+
dst=q_priv[lh][0, par2 * half:(par2 + 1) * half, t0:t0 + tw],
|
| 314 |
+
src=roped[par2][r0:r0 + half, 0:tw])
|
| 315 |
+
|
| 316 |
+
# ---- attention for this group's heads -> o_feat[g] (SBUF, feature-major) ----
|
| 317 |
+
allocator.set_current_address(phase2_base)
|
| 318 |
+
o_sb = []
|
| 319 |
+
for _lh in range(local_heads):
|
| 320 |
+
oc = (_lh * D) // _P_MAX
|
| 321 |
+
rr = (_lh * D) % _P_MAX
|
| 322 |
+
grp = []
|
| 323 |
+
for _g in range(n_grp):
|
| 324 |
+
grp.append(o_feat[g][oc][rr:rr + D, _g * _P_MAX:(_g + 1) * _P_MAX])
|
| 325 |
+
o_sb.append(grp)
|
| 326 |
+
attn_base = allocator.get_current_address()
|
| 327 |
+
for lh in range(local_heads):
|
| 328 |
+
_attention_cte(
|
| 329 |
+
q_priv[lh], None, None,
|
| 330 |
+
scale=scale, causal_mask=False,
|
| 331 |
+
tp_q=False, tp_k=False, tp_out=True,
|
| 332 |
+
cache_softmax=False,
|
| 333 |
+
k_cache_sbuf=k_tiles[lh],
|
| 334 |
+
v_cache_sbuf=v_tiles[lh],
|
| 335 |
+
out_o_hbm=o_sb[lh],
|
| 336 |
+
init_sbuf_addr=attn_base,
|
| 337 |
+
seqlen_k_valid=n_valid,
|
| 338 |
+
)
|
| 339 |
+
allocator.set_current_address(attn_base)
|
| 340 |
+
|
| 341 |
+
# ---- ONE out-projection over ALL G*n_ochunks feature chunks, full bias once ----
|
| 342 |
+
allocator.set_current_address(img_base)
|
| 343 |
+
if resident:
|
| 344 |
+
wo_sb = r_wo
|
| 345 |
+
bo_bc = r_bo
|
| 346 |
+
else:
|
| 347 |
+
wo_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
|
| 348 |
+
block_dim=[G * n_ochunks], num_free_tiles=[G * n_ochunks])
|
| 349 |
+
for oc in range(G * n_ochunks):
|
| 350 |
+
nisa.dma_copy(dst=wo_sb[oc], src=wot_hbm[oc * _P_MAX:(oc + 1) * _P_MAX, 0:H])
|
| 351 |
+
bo_bc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
|
| 352 |
+
nisa.dma_copy(dst=bo_bc, src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
|
| 353 |
+
out_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16, align_to=32)
|
| 354 |
+
n_ob = (H + _PSUM_FMAX - 1) // _PSUM_FMAX
|
| 355 |
+
for tb in range(n_grp):
|
| 356 |
+
for ob in range(n_ob):
|
| 357 |
+
ow = min(_PSUM_FMAX, H - ob * _PSUM_FMAX)
|
| 358 |
+
ps_out = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32, buffer=nl.psum,
|
| 359 |
+
address=(0, 0))
|
| 360 |
+
for g in range(G):
|
| 361 |
+
for oc in range(n_ochunks):
|
| 362 |
+
nisa.nc_matmul(
|
| 363 |
+
dst=ps_out[:, 0:ow],
|
| 364 |
+
stationary=o_feat[g][oc][:, tb * _P_MAX:(tb + 1) * _P_MAX],
|
| 365 |
+
moving=wo_sb[g * n_ochunks + oc][:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow],
|
| 366 |
+
accumulate=((g > 0) or (oc > 0)),
|
| 367 |
+
)
|
| 368 |
+
nisa.tensor_tensor(dst=out_sb[:, 0:ow], data1=ps_out[:, 0:ow],
|
| 369 |
+
data2=bo_bc[:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow], op=nl.add)
|
| 370 |
+
nisa.dma_copy(
|
| 371 |
+
dst=result.ap(pattern=[[H, _P_MAX], [1, ow]],
|
| 372 |
+
offset=rofs + tb * _P_MAX * H + ob * _PSUM_FMAX),
|
| 373 |
+
src=out_sb[:, 0:ow])
|
| 374 |
+
return result
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
_ATTN_L1 = wrap_nki_to_torch_hop(_attention_block_l1_kernel[1])
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
@nki_op("vit_enc::attention_block_l1", mutates_args={})
|
| 381 |
+
def attention_block_l1(
|
| 382 |
+
h: torch.Tensor,
|
| 383 |
+
wqk: torch.Tensor,
|
| 384 |
+
bqk: torch.Tensor,
|
| 385 |
+
tab: torch.Tensor,
|
| 386 |
+
wvt: torch.Tensor,
|
| 387 |
+
bv: torch.Tensor,
|
| 388 |
+
wot: torch.Tensor,
|
| 389 |
+
bo: torch.Tensor,
|
| 390 |
+
scale: float,
|
| 391 |
+
head_dim: int,
|
| 392 |
+
n_valid: int,
|
| 393 |
+
nimg: int = 1,
|
| 394 |
+
resident: int = 0,
|
| 395 |
+
) -> torch.Tensor:
|
| 396 |
+
"""All head groups in one program. Returns ONE [1, nimg*N, H] bf16 result (full bias)."""
|
| 397 |
+
for t in (h, wqk, bqk, tab, wvt, bv, wot, bo):
|
| 398 |
+
if t is not None and t.requires_grad:
|
| 399 |
+
raise RuntimeError("attention_block_l1 is inference-only; use torch.no_grad().")
|
| 400 |
+
if h.shape[0] // nimg > 2048:
|
| 401 |
+
raise NotImplementedError("attention_block_l1 supports N<=2048 only")
|
| 402 |
+
return _ATTN_L1(h, wqk, bqk, tab, wvt, bv, wot, bo, scale, head_dim, n_valid, nimg, resident)
|
build/torch-neuron/nki_kernels/attention_block_l1_ln.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Attention for LNC=1, all head groups, with the residual add + LN2 fused into the epilogue.
|
| 2 |
+
|
| 3 |
+
Same body as ``attention_block_l1``. The epilogue differs: per 128-token block the
|
| 4 |
+
out-projection for ALL H columns lands in an fp32 SBUF row tile, then
|
| 5 |
+
x_new = x + (attn @ Wo + bo) (fp32; attention output never rounded to bf16)
|
| 6 |
+
z2 = LN_no_affine(x_new) (bf16; same stat chain as res_layernorm.py)
|
| 7 |
+
and both are written to HBM. This replaces K3 (one launch + a bf16 [N,H] HBM write/read per layer).
|
| 8 |
+
|
| 9 |
+
--- attention_block_l1 notes ---
|
| 10 |
+
Attention for LNC=1: ONE program over ALL head groups.
|
| 11 |
+
|
| 12 |
+
The 2-core kernel in ``attention_block.py`` owns one 8-head group per program. At LNC=1 that
|
| 13 |
+
means one launch per group per layer (2 for ViT-L, 3 for ViT-H+), each re-transposing x, each
|
| 14 |
+
re-loading the rope tables, each writing an [N, H] bf16 out-projection PARTIAL to HBM that the
|
| 15 |
+
LN-fold kernel then sums.
|
| 16 |
+
|
| 17 |
+
This kernel, per image (``nimg`` images stacked on the row axis, one launch):
|
| 18 |
+
* transposes x once and loads the rope tables once (shared by every group: tables are
|
| 19 |
+
quad-tiled per head, identical for all heads);
|
| 20 |
+
* for each group: projects Q/K/V + RoPE (same producer as attention_block), runs the vendor
|
| 21 |
+
attention per head, and writes O feature-major into a PERSISTENT per-group SBUF slab;
|
| 22 |
+
* after all groups: ONE out-projection contracting over ALL features (G*4 chunks accumulate
|
| 23 |
+
in one fp32 PSUM tile), adds the FULL bias once, writes ONE [N, H] result.
|
| 24 |
+
No partials, no host sum; the LN-fold kernel receives a single branch.
|
| 25 |
+
|
| 26 |
+
``resident=1``: Q/K/V/O weights, biases and rope tables are staged ONCE per launch (bottom of
|
| 27 |
+
SBUF) and reused by every image. Host decides by an SBUF estimate (does not fit at N>=1024 for
|
| 28 |
+
ViT-L); ``resident=0`` restages each group's weights per image.
|
| 29 |
+
|
| 30 |
+
N<=2048 only (the shipping single-section vendor path). Numerics: identical producer and vendor
|
| 31 |
+
attention; the out-projection now accumulates all groups in fp32 PSUM instead of summing bf16
|
| 32 |
+
per-group partials, so results differ from attention_block by bf16 rounding of the partials.
|
| 33 |
+
"""
|
| 34 |
+
from __future__ import annotations
|
| 35 |
+
|
| 36 |
+
import torch
|
| 37 |
+
from nki.framework.kernel import wrap_nki_to_torch_hop
|
| 38 |
+
from torch_neuronx import nki_op
|
| 39 |
+
|
| 40 |
+
import nki
|
| 41 |
+
import nki.isa as nisa
|
| 42 |
+
import nki.language as nl
|
| 43 |
+
from nki.isa import engine
|
| 44 |
+
|
| 45 |
+
from nkilib.core.utils.modular_allocator import ModularAllocator
|
| 46 |
+
|
| 47 |
+
from .attention_inner import _attention_cte
|
| 48 |
+
|
| 49 |
+
_P_MAX = 128
|
| 50 |
+
_PSUM_FMAX = 512
|
| 51 |
+
_PSUM_BANK = 2048
|
| 52 |
+
_K_TILE = 512
|
| 53 |
+
_V_TILE = 128
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _kernel_assert(cond: bool, msg: str):
|
| 57 |
+
assert cond, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {msg}"
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def sbuf_estimate(H: int, Fp: int, N: int, D: int, resident: bool) -> int:
|
| 61 |
+
"""Conservative bytes/lane at the kernel's peak (host-side decision helper, not exact)."""
|
| 62 |
+
G = Fp // _PSUM_FMAX
|
| 63 |
+
n_chunks = H // _P_MAX
|
| 64 |
+
n_tt = (N + _PSUM_FMAX - 1) // _PSUM_FMAX
|
| 65 |
+
n_ktiles = (N + _K_TILE - 1) // _K_TILE
|
| 66 |
+
lh = _PSUM_FMAX // D
|
| 67 |
+
n_och = (lh * D) // _P_MAX
|
| 68 |
+
w_grp = 3 * n_chunks * _PSUM_FMAX * 2 + _PSUM_FMAX * 4 + 64 # wqk + wv + bv_bc + biases
|
| 69 |
+
w_out = G * n_och * H * 2 + H * 4 # wo + bo_bc
|
| 70 |
+
tab = 4 * n_tt * _PSUM_FMAX * 4
|
| 71 |
+
o_feat = G * n_och * N * 2
|
| 72 |
+
x_t = n_chunks * n_tt * _PSUM_FMAX * 2
|
| 73 |
+
kv = lh * (n_ktiles * _K_TILE * 2 + (N // _V_TILE) * D * 2)
|
| 74 |
+
work = 2 * _PSUM_FMAX * 2 * 2 + 2 * _PSUM_FMAX * 4 + _PSUM_FMAX * 4
|
| 75 |
+
inner = 48 * 1024
|
| 76 |
+
resid = (G * w_grp + w_out + tab) if resident else 0
|
| 77 |
+
base = resid + o_feat + x_t + (0 if resident else tab)
|
| 78 |
+
p1 = kv + (0 if resident else w_grp) + work
|
| 79 |
+
p2 = kv + inner
|
| 80 |
+
p3 = (0 if resident else w_out) + 2 * (H * 4 * 2 + H * 2 + 32) + H * 4
|
| 81 |
+
return base + max(p1, p2, p3)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@nki.jit
|
| 85 |
+
def _attention_block_l1_ln_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_hbm,
|
| 86 |
+
wot_hbm, bo_hbm, x_hbm, scale, head_dim, n_valid, eps, nimg=1, resident=0):
|
| 87 |
+
NT, H = h_hbm.shape
|
| 88 |
+
N = NT // nimg
|
| 89 |
+
Fp = wqk_hbm.shape[2]
|
| 90 |
+
D = head_dim
|
| 91 |
+
half = D // 2
|
| 92 |
+
quad = _P_MAX // half
|
| 93 |
+
local_heads = _PSUM_FMAX // D
|
| 94 |
+
local_quads = local_heads // quad
|
| 95 |
+
G = Fp // _PSUM_FMAX # head groups, ALL handled by this one program
|
| 96 |
+
n_chunks = H // _P_MAX
|
| 97 |
+
n_tt = (N + _PSUM_FMAX - 1) // _PSUM_FMAX
|
| 98 |
+
n_ktiles = (N + _K_TILE - 1) // _K_TILE
|
| 99 |
+
n_vtiles = N // _V_TILE
|
| 100 |
+
n_ochunks = (local_heads * D) // _P_MAX # feature chunks per group
|
| 101 |
+
v_per_tt = _PSUM_FMAX // _V_TILE
|
| 102 |
+
|
| 103 |
+
_kernel_assert(wqk_hbm.shape == (2, H, Fp), "bad wqk")
|
| 104 |
+
_kernel_assert(bqk_hbm.shape == (2, Fp, 1), "bad bqk")
|
| 105 |
+
_kernel_assert(tab_hbm.shape == (4, _P_MAX, N), "bad tab")
|
| 106 |
+
_kernel_assert(wvt_hbm.shape == (H, Fp) and bv_hbm.shape == (1, Fp), "bad V weights")
|
| 107 |
+
_kernel_assert(wot_hbm.shape == (Fp, H) and bo_hbm.shape == (1, H), "bad O weights")
|
| 108 |
+
_kernel_assert(H % _P_MAX == 0, "H must be a multiple of 128")
|
| 109 |
+
_kernel_assert(quad * half == _P_MAX, "D must give quad*half=128")
|
| 110 |
+
_kernel_assert(Fp % _PSUM_FMAX == 0, "padded features must be a multiple of 512")
|
| 111 |
+
_kernel_assert(N % _P_MAX == 0, "N must be a multiple of 128")
|
| 112 |
+
_kernel_assert(N <= _PSUM_FMAX * 4, "N>2048 not supported")
|
| 113 |
+
_kernel_assert(NT == nimg * N, "rows must be nimg x N")
|
| 114 |
+
|
| 115 |
+
_kernel_assert(x_hbm.shape == (NT, H), "x must be [nimg*N, H] fp32")
|
| 116 |
+
xnew_hbm = nl.ndarray((NT, H), dtype=nl.float32, buffer=nl.shared_hbm)
|
| 117 |
+
z_hbm = nl.ndarray((NT, H), dtype=nl.bfloat16, buffer=nl.shared_hbm)
|
| 118 |
+
inv_h = 1.0 / float(H)
|
| 119 |
+
neg_inv_h = -inv_h
|
| 120 |
+
|
| 121 |
+
# ---------------- resident (per-launch invariant) staging ----------------
|
| 122 |
+
res_top = 0
|
| 123 |
+
r_w = []
|
| 124 |
+
r_wv = []
|
| 125 |
+
r_b = []
|
| 126 |
+
r_bv = []
|
| 127 |
+
if resident:
|
| 128 |
+
ra = ModularAllocator(initial_address=0)
|
| 129 |
+
for g in range(G):
|
| 130 |
+
col0 = g * _PSUM_FMAX
|
| 131 |
+
w_sb_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2 * _P_MAX), dtype=nl.bfloat16,
|
| 132 |
+
block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
|
| 133 |
+
for qk in range(2):
|
| 134 |
+
for c in range(n_chunks):
|
| 135 |
+
nisa.dma_copy(dst=w_sb_[qk][c],
|
| 136 |
+
src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX,
|
| 137 |
+
col0:col0 + local_quads * 2 * _P_MAX])
|
| 138 |
+
wv_sb_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
|
| 139 |
+
block_dim=[n_chunks], num_free_tiles=[n_chunks])
|
| 140 |
+
for c in range(n_chunks):
|
| 141 |
+
nisa.dma_copy(dst=wv_sb_[c],
|
| 142 |
+
src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX, col0:col0 + local_heads * D])
|
| 143 |
+
b_sb_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
|
| 144 |
+
block_dim=[2, local_quads * 2],
|
| 145 |
+
num_free_tiles=[2, local_quads * 2], align_to=4)
|
| 146 |
+
b_stage_ = ra.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
|
| 147 |
+
for qk in range(2):
|
| 148 |
+
for gg in range(local_quads * 2):
|
| 149 |
+
nisa.dma_copy(dst=b_stage_,
|
| 150 |
+
src=bqk_hbm[qk, col0 + gg * _P_MAX:col0 + (gg + 1) * _P_MAX, 0:1])
|
| 151 |
+
nisa.tensor_copy(dst=b_sb_[qk][gg], src=b_stage_)
|
| 152 |
+
bv_bc_ = ra.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
|
| 153 |
+
nisa.dma_copy(dst=bv_bc_,
|
| 154 |
+
src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]], offset=col0))
|
| 155 |
+
r_w.append(w_sb_)
|
| 156 |
+
r_wv.append(wv_sb_)
|
| 157 |
+
r_b.append(b_sb_)
|
| 158 |
+
r_bv.append(bv_bc_)
|
| 159 |
+
r_tab = ra.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
|
| 160 |
+
block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
|
| 161 |
+
for s in range(n_tt):
|
| 162 |
+
for t_i in range(4):
|
| 163 |
+
_tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
|
| 164 |
+
nisa.dma_copy(dst=r_tab[t_i][s][:, 0:_tw],
|
| 165 |
+
src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
|
| 166 |
+
r_wo = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
|
| 167 |
+
block_dim=[G * n_ochunks], num_free_tiles=[G * n_ochunks])
|
| 168 |
+
for oc in range(G * n_ochunks):
|
| 169 |
+
nisa.dma_copy(dst=r_wo[oc], src=wot_hbm[oc * _P_MAX:(oc + 1) * _P_MAX, 0:H])
|
| 170 |
+
r_bo = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
|
| 171 |
+
nisa.dma_copy(dst=r_bo, src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
|
| 172 |
+
res_top = ra.get_current_address()
|
| 173 |
+
|
| 174 |
+
n_grp = N // _P_MAX
|
| 175 |
+
for img in range(nimg):
|
| 176 |
+
rofs = img * N * H
|
| 177 |
+
allocator = ModularAllocator(initial_address=res_top)
|
| 178 |
+
|
| 179 |
+
# O for ALL groups stays in SBUF until the single out-projection.
|
| 180 |
+
o_feat = []
|
| 181 |
+
for g in range(G):
|
| 182 |
+
of_ = allocator.alloc_sbuf_tensor(shape=(_P_MAX, N), dtype=nl.bfloat16,
|
| 183 |
+
block_dim=[n_ochunks], num_free_tiles=[n_ochunks],
|
| 184 |
+
align_to=32)
|
| 185 |
+
o_feat.append(of_)
|
| 186 |
+
|
| 187 |
+
# x^T: shared by every group (transposed ONCE per image).
|
| 188 |
+
x_t = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
|
| 189 |
+
block_dim=[n_chunks, n_tt], num_free_tiles=[n_chunks, n_tt],
|
| 190 |
+
align_to=32)
|
| 191 |
+
for s in range(n_tt):
|
| 192 |
+
for c in range(n_chunks):
|
| 193 |
+
nisa.dma_transpose(
|
| 194 |
+
dst=x_t[c][s].ap([[_PSUM_FMAX, _P_MAX], [1, 1], [1, 1],
|
| 195 |
+
[1, min(_PSUM_FMAX, N - s * _PSUM_FMAX)]], offset=0),
|
| 196 |
+
src=h_hbm.ap([[H, min(_PSUM_FMAX, N - s * _PSUM_FMAX)], [1, 1], [1, 1], [1, _P_MAX]],
|
| 197 |
+
offset=rofs + (s * _PSUM_FMAX) * H + c * _P_MAX),
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
# rope tables: shared by every group.
|
| 201 |
+
if resident:
|
| 202 |
+
tab = r_tab
|
| 203 |
+
else:
|
| 204 |
+
tab = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32,
|
| 205 |
+
block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
|
| 206 |
+
for s in range(n_tt):
|
| 207 |
+
for t_i in range(4):
|
| 208 |
+
_tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
|
| 209 |
+
nisa.dma_copy(dst=tab[t_i][s][:, 0:_tw],
|
| 210 |
+
src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
|
| 211 |
+
|
| 212 |
+
img_base = allocator.get_current_address()
|
| 213 |
+
|
| 214 |
+
for g in range(G):
|
| 215 |
+
allocator.set_current_address(img_base)
|
| 216 |
+
col0 = g * _PSUM_FMAX
|
| 217 |
+
|
| 218 |
+
k_tiles = []
|
| 219 |
+
v_tiles = []
|
| 220 |
+
for _lh in range(local_heads):
|
| 221 |
+
kt_ = allocator.alloc_sbuf_tensor(shape=(D, _K_TILE), dtype=nl.bfloat16,
|
| 222 |
+
block_dim=[n_ktiles], num_free_tiles=[n_ktiles],
|
| 223 |
+
align_to=32)
|
| 224 |
+
k_tiles.append(kt_)
|
| 225 |
+
vt_ = allocator.alloc_sbuf_tensor(shape=(_V_TILE, D), dtype=nl.bfloat16,
|
| 226 |
+
block_dim=[n_vtiles], num_free_tiles=[n_vtiles])
|
| 227 |
+
v_tiles.append(vt_)
|
| 228 |
+
phase2_base = allocator.get_current_address()
|
| 229 |
+
|
| 230 |
+
if resident:
|
| 231 |
+
w_sb = r_w[g]
|
| 232 |
+
wv_sb = r_wv[g]
|
| 233 |
+
b_sb = r_b[g]
|
| 234 |
+
bv_bc = r_bv[g]
|
| 235 |
+
else:
|
| 236 |
+
w_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2 * _P_MAX),
|
| 237 |
+
dtype=nl.bfloat16,
|
| 238 |
+
block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
|
| 239 |
+
for qk in range(2):
|
| 240 |
+
for c in range(n_chunks):
|
| 241 |
+
nisa.dma_copy(dst=w_sb[qk][c],
|
| 242 |
+
src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX,
|
| 243 |
+
col0:col0 + local_quads * 2 * _P_MAX])
|
| 244 |
+
wv_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
|
| 245 |
+
block_dim=[n_chunks], num_free_tiles=[n_chunks])
|
| 246 |
+
for c in range(n_chunks):
|
| 247 |
+
nisa.dma_copy(dst=wv_sb[c],
|
| 248 |
+
src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX, col0:col0 + local_heads * D])
|
| 249 |
+
b_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
|
| 250 |
+
block_dim=[2, local_quads * 2],
|
| 251 |
+
num_free_tiles=[2, local_quads * 2], align_to=4)
|
| 252 |
+
b_stage = allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.bfloat16)
|
| 253 |
+
for qk in range(2):
|
| 254 |
+
for gg in range(local_quads * 2):
|
| 255 |
+
nisa.dma_copy(dst=b_stage,
|
| 256 |
+
src=bqk_hbm[qk, col0 + gg * _P_MAX:col0 + (gg + 1) * _P_MAX, 0:1])
|
| 257 |
+
nisa.tensor_copy(dst=b_sb[qk][gg], src=b_stage)
|
| 258 |
+
bv_bc = allocator.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32,
|
| 259 |
+
align_to=4)
|
| 260 |
+
nisa.dma_copy(dst=bv_bc,
|
| 261 |
+
src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]], offset=col0))
|
| 262 |
+
|
| 263 |
+
proj = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
|
| 264 |
+
block_dim=[2], num_free_tiles=[2])
|
| 265 |
+
acc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32, align_to=4)
|
| 266 |
+
tmp = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.float32, align_to=4)
|
| 267 |
+
roped = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
|
| 268 |
+
block_dim=[2], num_free_tiles=[2])
|
| 269 |
+
vout = allocator.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32,
|
| 270 |
+
align_to=4)
|
| 271 |
+
q_priv = []
|
| 272 |
+
for _lh in range(local_heads):
|
| 273 |
+
q_priv.append(nl.ndarray((1, D, N), dtype=nl.bfloat16, buffer=nl.private_hbm))
|
| 274 |
+
|
| 275 |
+
# ---- producer (identical to attention_block's N<=2048 path) ----
|
| 276 |
+
for tt in range(n_tt):
|
| 277 |
+
t0 = tt * _PSUM_FMAX
|
| 278 |
+
tw = min(_PSUM_FMAX, N - t0)
|
| 279 |
+
for vsub in range(tw // _V_TILE):
|
| 280 |
+
ps_v = nl.ndarray((_V_TILE, local_heads * D), dtype=nl.float32,
|
| 281 |
+
buffer=nl.psum, address=(0, 7 * _PSUM_BANK))
|
| 282 |
+
for c in range(n_chunks):
|
| 283 |
+
nisa.nc_matmul(dst=ps_v,
|
| 284 |
+
stationary=x_t[c][tt][:, vsub * _V_TILE:(vsub + 1) * _V_TILE],
|
| 285 |
+
moving=wv_sb[c], accumulate=(c > 0))
|
| 286 |
+
nisa.tensor_tensor(dst=vout, data1=ps_v, data2=bv_bc, op=nl.add)
|
| 287 |
+
for lh in range(local_heads):
|
| 288 |
+
nisa.tensor_copy(dst=v_tiles[lh][tt * v_per_tt + vsub],
|
| 289 |
+
src=vout[:, lh * D:(lh + 1) * D], engine=engine.scalar)
|
| 290 |
+
for qk in range(2):
|
| 291 |
+
for j in range(local_quads):
|
| 292 |
+
for par in range(2):
|
| 293 |
+
gq = j * 2 + par
|
| 294 |
+
cols = gq * _P_MAX
|
| 295 |
+
ps = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32, buffer=nl.psum,
|
| 296 |
+
address=(0, (5 + par) * _PSUM_BANK))
|
| 297 |
+
for c in range(n_chunks):
|
| 298 |
+
nisa.nc_matmul(dst=ps[:, 0:tw],
|
| 299 |
+
stationary=w_sb[qk][c][:, cols:cols + _P_MAX],
|
| 300 |
+
moving=x_t[c][tt][:, 0:tw], accumulate=(c > 0))
|
| 301 |
+
nisa.tensor_scalar(dst=proj[par][:, 0:tw], data=ps[:, 0:tw],
|
| 302 |
+
op0=nl.add, operand0=b_sb[qk][gq])
|
| 303 |
+
nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[0][:, 0:tw],
|
| 304 |
+
data2=tab[0][tt][:, 0:tw], op=nl.multiply)
|
| 305 |
+
nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[1][:, 0:tw],
|
| 306 |
+
data2=tab[2][tt][:, 0:tw], op=nl.multiply)
|
| 307 |
+
nisa.tensor_tensor(dst=roped[0][:, 0:tw], data1=acc[:, 0:tw],
|
| 308 |
+
data2=tmp[:, 0:tw], op=nl.add)
|
| 309 |
+
nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[1][:, 0:tw],
|
| 310 |
+
data2=tab[1][tt][:, 0:tw], op=nl.multiply)
|
| 311 |
+
nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[0][:, 0:tw],
|
| 312 |
+
data2=tab[3][tt][:, 0:tw], op=nl.multiply)
|
| 313 |
+
nisa.tensor_tensor(dst=roped[1][:, 0:tw], data1=acc[:, 0:tw],
|
| 314 |
+
data2=tmp[:, 0:tw], op=nl.add)
|
| 315 |
+
for b in range(quad):
|
| 316 |
+
lh = j * quad + b
|
| 317 |
+
r0 = b * half
|
| 318 |
+
if qk == 1:
|
| 319 |
+
for par2 in range(2):
|
| 320 |
+
nisa.tensor_copy(
|
| 321 |
+
dst=k_tiles[lh][tt][par2 * half:(par2 + 1) * half, 0:tw],
|
| 322 |
+
src=roped[par2][r0:r0 + half, 0:tw])
|
| 323 |
+
else:
|
| 324 |
+
for par2 in range(2):
|
| 325 |
+
nisa.dma_copy(
|
| 326 |
+
dst=q_priv[lh][0, par2 * half:(par2 + 1) * half, t0:t0 + tw],
|
| 327 |
+
src=roped[par2][r0:r0 + half, 0:tw])
|
| 328 |
+
|
| 329 |
+
# ---- attention for this group's heads -> o_feat[g] (SBUF, feature-major) ----
|
| 330 |
+
allocator.set_current_address(phase2_base)
|
| 331 |
+
o_sb = []
|
| 332 |
+
for _lh in range(local_heads):
|
| 333 |
+
oc = (_lh * D) // _P_MAX
|
| 334 |
+
rr = (_lh * D) % _P_MAX
|
| 335 |
+
grp = []
|
| 336 |
+
for _g in range(n_grp):
|
| 337 |
+
grp.append(o_feat[g][oc][rr:rr + D, _g * _P_MAX:(_g + 1) * _P_MAX])
|
| 338 |
+
o_sb.append(grp)
|
| 339 |
+
attn_base = allocator.get_current_address()
|
| 340 |
+
for lh in range(local_heads):
|
| 341 |
+
_attention_cte(
|
| 342 |
+
q_priv[lh], None, None,
|
| 343 |
+
scale=scale, causal_mask=False,
|
| 344 |
+
tp_q=False, tp_k=False, tp_out=True,
|
| 345 |
+
cache_softmax=False,
|
| 346 |
+
k_cache_sbuf=k_tiles[lh],
|
| 347 |
+
v_cache_sbuf=v_tiles[lh],
|
| 348 |
+
out_o_hbm=o_sb[lh],
|
| 349 |
+
init_sbuf_addr=attn_base,
|
| 350 |
+
seqlen_k_valid=n_valid,
|
| 351 |
+
)
|
| 352 |
+
allocator.set_current_address(attn_base)
|
| 353 |
+
|
| 354 |
+
# ---- ONE out-projection over ALL G*n_ochunks feature chunks, full bias once ----
|
| 355 |
+
allocator.set_current_address(img_base)
|
| 356 |
+
if resident:
|
| 357 |
+
wo_sb = r_wo
|
| 358 |
+
bo_bc = r_bo
|
| 359 |
+
else:
|
| 360 |
+
wo_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
|
| 361 |
+
block_dim=[G * n_ochunks], num_free_tiles=[G * n_ochunks])
|
| 362 |
+
for oc in range(G * n_ochunks):
|
| 363 |
+
nisa.dma_copy(dst=wo_sb[oc], src=wot_hbm[oc * _P_MAX:(oc + 1) * _P_MAX, 0:H])
|
| 364 |
+
bo_bc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
|
| 365 |
+
nisa.dma_copy(dst=bo_bc, src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
|
| 366 |
+
# ---- epilogue: out-proj (all groups, fp32) + bias + residual -> x_new ; LN -> z2 ----
|
| 367 |
+
_RING = 2
|
| 368 |
+
row = []
|
| 369 |
+
a_t = []
|
| 370 |
+
z_t = []
|
| 371 |
+
st = []
|
| 372 |
+
for _i in range(_RING):
|
| 373 |
+
row.append(allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4))
|
| 374 |
+
a_t.append(allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4))
|
| 375 |
+
z_t.append(allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16, align_to=32))
|
| 376 |
+
st.append(allocator.alloc_sbuf_tensor(shape=(_P_MAX, 1), dtype=nl.float32,
|
| 377 |
+
block_dim=[5], num_free_tiles=[5], align_to=4))
|
| 378 |
+
sink = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
|
| 379 |
+
n_ob = (H + _PSUM_FMAX - 1) // _PSUM_FMAX
|
| 380 |
+
for tb in range(n_grp):
|
| 381 |
+
k = tb % _RING
|
| 382 |
+
r0 = img * N + tb * _P_MAX
|
| 383 |
+
nisa.dma_copy(dst=a_t[k], src=x_hbm[r0:r0 + _P_MAX, 0:H])
|
| 384 |
+
for ob in range(n_ob):
|
| 385 |
+
ow = min(_PSUM_FMAX, H - ob * _PSUM_FMAX)
|
| 386 |
+
ps_out = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32, buffer=nl.psum,
|
| 387 |
+
address=(0, ((tb * n_ob + ob) % 4) * _PSUM_BANK))
|
| 388 |
+
for g in range(G):
|
| 389 |
+
for oc in range(n_ochunks):
|
| 390 |
+
nisa.nc_matmul(
|
| 391 |
+
dst=ps_out[:, 0:ow],
|
| 392 |
+
stationary=o_feat[g][oc][:, tb * _P_MAX:(tb + 1) * _P_MAX],
|
| 393 |
+
moving=wo_sb[g * n_ochunks + oc][:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow],
|
| 394 |
+
accumulate=((g > 0) or (oc > 0)),
|
| 395 |
+
)
|
| 396 |
+
nisa.tensor_tensor(dst=row[k][:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow],
|
| 397 |
+
data1=ps_out[:, 0:ow],
|
| 398 |
+
data2=bo_bc[:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow], op=nl.add)
|
| 399 |
+
nisa.tensor_tensor(dst=row[k], data1=row[k], data2=a_t[k], op=nl.add)
|
| 400 |
+
nisa.dma_copy(dst=xnew_hbm[r0:r0 + _P_MAX, 0:H], src=row[k])
|
| 401 |
+
# stat chain (identical ops/order to res_layernorm.py)
|
| 402 |
+
neg_shift = st[k][0]
|
| 403 |
+
acc_s = st[k][1]
|
| 404 |
+
neg_mu = st[k][2]
|
| 405 |
+
var = st[k][3]
|
| 406 |
+
rstd = st[k][4]
|
| 407 |
+
nisa.tensor_scalar(dst=neg_shift, data=row[k][0:_P_MAX, 0:1],
|
| 408 |
+
op0=nl.multiply, operand0=-1.0)
|
| 409 |
+
nisa.activation_reduce(dst=sink, op=nl.copy, data=row[k],
|
| 410 |
+
reduce_op=nl.add, reduce_res=acc_s, bias=neg_shift, scale=1.0)
|
| 411 |
+
nisa.tensor_scalar(dst=neg_mu, data=acc_s, op0=nl.multiply, operand0=neg_inv_h,
|
| 412 |
+
op1=nl.add, operand1=neg_shift)
|
| 413 |
+
nisa.activation_reduce(dst=sink, op=nl.square, data=row[k],
|
| 414 |
+
reduce_op=nl.add, reduce_res=acc_s, bias=neg_mu, scale=1.0)
|
| 415 |
+
nisa.tensor_scalar(dst=var, data=acc_s, op0=nl.multiply, operand0=inv_h,
|
| 416 |
+
op1=nl.add, operand1=eps)
|
| 417 |
+
nisa.activation(dst=rstd, op=nl.rsqrt, data=var)
|
| 418 |
+
nisa.tensor_scalar(dst=z_t[k], data=row[k], op0=nl.add, operand0=neg_mu,
|
| 419 |
+
op1=nl.multiply, operand1=rstd)
|
| 420 |
+
nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:H], src=z_t[k])
|
| 421 |
+
return xnew_hbm, z_hbm
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
_ATTN_L1_LN = wrap_nki_to_torch_hop(_attention_block_l1_ln_kernel[1])
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
@nki_op("vit_enc::attention_block_l1_ln", mutates_args={})
|
| 428 |
+
def attention_block_l1_ln(
|
| 429 |
+
h: torch.Tensor,
|
| 430 |
+
wqk: torch.Tensor,
|
| 431 |
+
bqk: torch.Tensor,
|
| 432 |
+
tab: torch.Tensor,
|
| 433 |
+
wvt: torch.Tensor,
|
| 434 |
+
bv: torch.Tensor,
|
| 435 |
+
wot: torch.Tensor,
|
| 436 |
+
bo: torch.Tensor,
|
| 437 |
+
x: torch.Tensor,
|
| 438 |
+
scale: float,
|
| 439 |
+
head_dim: int,
|
| 440 |
+
n_valid: int,
|
| 441 |
+
eps: float,
|
| 442 |
+
nimg: int = 1,
|
| 443 |
+
resident: int = 0,
|
| 444 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 445 |
+
"""All-group attention + residual + affine-free LN2. Returns (x_new [R,H] fp32, z2 [R,H] bf16)."""
|
| 446 |
+
for t in (h, wqk, bqk, tab, wvt, bv, wot, bo, x):
|
| 447 |
+
if t is not None and t.requires_grad:
|
| 448 |
+
raise RuntimeError("attention_block_l1_ln is inference-only; use torch.no_grad().")
|
| 449 |
+
if h.shape[0] // nimg > 2048:
|
| 450 |
+
raise NotImplementedError("attention_block_l1_ln supports N<=2048 only")
|
| 451 |
+
return _ATTN_L1_LN(h, wqk, bqk, tab, wvt, bv, wot, bo, x, scale, head_dim, n_valid, eps, nimg, resident)
|
build/torch-neuron/nki_kernels/attention_inner.py
ADDED
|
@@ -0,0 +1,1220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
| 4 |
+
# You may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
#
|
| 13 |
+
# MODIFIED from the AWS Neuron SDK's nkilib ``core/attention/attention_cte.py``
|
| 14 |
+
# (Apache-2.0, Copyright Amazon.com, Inc. or its affiliates). Modifications:
|
| 15 |
+
# 1. Four engine-placement edits (two memsets -> GpSimd, the tp_out eviction
|
| 16 |
+
# tensor_copy -> Scalar, the list-branch write-back tensor_copy -> GpSimd),
|
| 17 |
+
# measured worth ~1.19x end-to-end in the V-JEPA2 encoder.
|
| 18 |
+
# 2. Code paths unreachable for this model's configuration removed (causal/SWA
|
| 19 |
+
# masks, prefix caching, flash sectioning at seq<=10240, sinks, fp8,
|
| 20 |
+
# sequence packing, CP) -- the executed instruction stream is unchanged.
|
| 21 |
+
# The original supports the general case; use nkilib for anything but this
|
| 22 |
+
# non-causal, single-section, SBUF-KV configuration.
|
| 23 |
+
"""K2 inner attention: the vendor ``_attention_cte`` context-encoding kernel, FLATTENED to
|
| 24 |
+
exactly the branches the 4-kernel V-JEPA2 encoder reaches, with the four h8/h10 engine pins
|
| 25 |
+
applied in place. Self-contained: imports only ``nki``, ``nkilib``, ``torch`` (none here) --
|
| 26 |
+
NO imports from ``experiment/`` or the repo-root incumbent modules.
|
| 27 |
+
|
| 28 |
+
WHAT THIS COMPUTES
|
| 29 |
+
==================
|
| 30 |
+
``Output = softmax(scale * Q @ K^T) @ V`` for ONE head, called 8x per core by the K2 outer
|
| 31 |
+
kernel (``attention_block.py``). The pipeline, in order, per 128-token Q group:
|
| 32 |
+
|
| 33 |
+
load_q -> QK^T (MM1) + row-max -> update running max -> exp (softmax numerator)
|
| 34 |
+
-> P@V (MM2) -> write-back (reciprocal-normalise, evict to the caller's SBUF O).
|
| 35 |
+
|
| 36 |
+
Group software-pipelining overlaps three groups at a time (grp_i does PV+writeback, grp_i+1
|
| 37 |
+
does exp, grp_i+2 does load-Q + QK/max), which is the vendor's Level-4 loop structure.
|
| 38 |
+
|
| 39 |
+
PSUM BANK PLAN (this config, tp_out=True, single 2048 section)
|
| 40 |
+
--------------------------------------------------------------
|
| 41 |
+
* MM1 (QK^T) tiles: banks {0,1,2,3} (``address=(0, (k_tile_idx % 4) * PSUM_BANK_SIZE)``).
|
| 42 |
+
* MM2 (P@V) tiles: banks {4,5,6,7} (``address=(0, (4 + large_tile_idx % 4) * ...)``).
|
| 43 |
+
* tp reciprocal / correction transposes: bank {3 or 7} (``((grp%2)*4 + 3) * ...``).
|
| 44 |
+
The producer (K2 outer) owns banks 5/6/7 during its phase; the attention region runs after the
|
| 45 |
+
producer's high-water mark, so there is no live overlap.
|
| 46 |
+
|
| 47 |
+
RING / MULTI-BUFFER DEPTHS (from the dp/dp2/dp4 fork lineage; kept verbatim)
|
| 48 |
+
---------------------------------------------------------------------------
|
| 49 |
+
* ``exp_sb`` group-axis depth 2 (dp2): exp(g+1) can run while transpose(g) still drains.
|
| 50 |
+
* ``exp_tp_sb`` group-axis depth 8 (dp4): P^T transposes issue as soon as their exp output
|
| 51 |
+
exists, spreading DMA traffic out of the P.V collision window.
|
| 52 |
+
|
| 53 |
+
THE FOUR ENGINE PINS (h8/h10), and WHY each exists (one line each, from PERF_INVENTORY)
|
| 54 |
+
---------------------------------------------------------------------------------------
|
| 55 |
+
1. ``_scale_reciprocal_write_back_impl`` tp_out PSUM->SBUF evict ``tensor_copy`` -> SCALAR
|
| 56 |
+
(h8 site 1): the un-pinned copy defaults to Vector, K2's binding engine; Scalar/Vector are
|
| 57 |
+
the only legal engines (PSUM source excludes GpSimd) and Scalar is the exercised majority.
|
| 58 |
+
2. ``_write_back_o_impl`` tp_out list-branch ``tensor_copy`` -> GPSIMD (h8 site 3): SBUF->SBUF
|
| 59 |
+
fp32->bf16 CAST, all three engines legal, GpSimd is the idle engine so the copy sheds off
|
| 60 |
+
Vector.
|
| 61 |
+
3. ``_qk_and_max_impl`` ``memset`` -> GPSIMD (h10 :2876): initialises mm1_partial_max, first
|
| 62 |
+
consumer is several ops downstream, so a slower idle engine is safe and sheds Vector.
|
| 63 |
+
4. ``_fused_qkmax_and_pv_impl`` ``memset`` -> GPSIMD (h10 :3294): same rationale, 2688/core.
|
| 64 |
+
The vendor ``_exp_impl`` memset (dp4 :2993) stays UNPINNED (engine.unknown / Vector) on purpose
|
| 65 |
+
-- pinning it would fork ``_exp_impl`` and re-home the softmax-exp source strings; it MUST stay
|
| 66 |
+
unpinned to reproduce the shipping instruction stream.
|
| 67 |
+
|
| 68 |
+
DEAD PATHS REMOVED (§4 of PERF_INVENTORY) -- guard that made each unreachable for this call:
|
| 69 |
+
``causal_mask=False, use_sbuf_kv=True, seqlen_k=2048<10240, bs=1, tp_out=True, fuse_v_proj=False,
|
| 70 |
+
no prior/APC/SWA/CP/seqpack/sink, cache_softmax=False``. See per-function docstrings.
|
| 71 |
+
The allocator usage (``nkilib`` ``ModularAllocator``) is kept EXACTLY as the vendor uses it.
|
| 72 |
+
"""
|
| 73 |
+
from __future__ import annotations
|
| 74 |
+
|
| 75 |
+
from dataclasses import dataclass
|
| 76 |
+
from typing import Any, List, Optional
|
| 77 |
+
|
| 78 |
+
import nki
|
| 79 |
+
import nki.isa as nisa
|
| 80 |
+
import nki.language as nl
|
| 81 |
+
from nki.isa import engine, reduce_cmd
|
| 82 |
+
from nki.language.opcode import maximum as _maximum
|
| 83 |
+
|
| 84 |
+
from nkilib.core.utils.allocator import align_to
|
| 85 |
+
from nkilib.core.utils.kernel_assert import assert_shape, kernel_assert
|
| 86 |
+
from nkilib.core.utils.kernel_helpers import (
|
| 87 |
+
PSUM_BANK_SIZE,
|
| 88 |
+
div_ceil,
|
| 89 |
+
get_verified_program_sharding_info,
|
| 90 |
+
is_sbuf_tensor,
|
| 91 |
+
)
|
| 92 |
+
from nkilib.core.utils.logging import get_logger
|
| 93 |
+
from nkilib.core.utils.modular_allocator import ModularAllocator
|
| 94 |
+
from nkilib.core.utils.stream_shuffle_broadcast import stream_shuffle_broadcast
|
| 95 |
+
|
| 96 |
+
logger = get_logger("attention_inner")
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
_FLOAT32_MIN = -3.4028235e38 # used for initialization and masking
|
| 100 |
+
|
| 101 |
+
# Tested-range constraints (checked in _attention_cte).
|
| 102 |
+
_MAX_BS = 512
|
| 103 |
+
_MAX_SEQLEN = 131072
|
| 104 |
+
_MAX_BS_TIMES_SEQLEN_QK = 32.0 * 36864 * 36864
|
| 105 |
+
_MAX_HEAD_DIM = 128
|
| 106 |
+
_MIN_GLOBAL_CP_DEGREE = 1
|
| 107 |
+
_MAX_GLOBAL_CP_DEGREE = 32
|
| 108 |
+
|
| 109 |
+
# Tile size / threshold constants used by the LIVE path.
|
| 110 |
+
_Q_GRP_SZ = 128
|
| 111 |
+
_V_TILE_SZ = 128 # V tile size for loading and MM2 operations
|
| 112 |
+
_K_TILE_SZ = 512 # K tile size for loading and MM1+masking operations
|
| 113 |
+
_EXP_TILE_SZ = 512 # Tile size for exp instructions (must equal _K_TILE_SZ)
|
| 114 |
+
_LARGE_TILE_SZ = 2048 # Larger tile size for allocations/pipelining (4 x 512 tiles)
|
| 115 |
+
_FLASH_ATTENTION_THRESHOLD = 10 * 1024 # Use flash attention above this K/V length
|
| 116 |
+
_FLASH_ATTENTION_SECTION_LENGTH = 8 * 1024 # Section size when using flash attention
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# ==========================================================================================
|
| 120 |
+
# Buffer / config dataclasses (kept verbatim -- the four required by the reached path).
|
| 121 |
+
# ==========================================================================================
|
| 122 |
+
@dataclass
|
| 123 |
+
class AttnConfig(nl.NKIObject):
|
| 124 |
+
"""High-level attention configuration set at kernel entry point."""
|
| 125 |
+
|
| 126 |
+
seqlen_q: int = None
|
| 127 |
+
seqlen_k_active: int = None
|
| 128 |
+
seqlen_k_prior: int = None
|
| 129 |
+
d: int = None
|
| 130 |
+
|
| 131 |
+
tp_q: bool = None
|
| 132 |
+
tp_k: bool = None
|
| 133 |
+
tp_out: bool = None
|
| 134 |
+
|
| 135 |
+
is_prefix_caching: bool = None
|
| 136 |
+
causal_mask: bool = None
|
| 137 |
+
|
| 138 |
+
use_swa: bool = None
|
| 139 |
+
sliding_window: int = None
|
| 140 |
+
|
| 141 |
+
use_cp: bool = None
|
| 142 |
+
global_cp_deg: int = None
|
| 143 |
+
cp_strided_q_slicing: bool = None
|
| 144 |
+
cp_striped_input: bool = None
|
| 145 |
+
|
| 146 |
+
scale: float = None
|
| 147 |
+
cache_softmax: bool = None
|
| 148 |
+
skip_output_normalization: bool = None
|
| 149 |
+
dtype: Any = None
|
| 150 |
+
softmax_dtype: Any = None
|
| 151 |
+
mm_out_dtype: Any = None
|
| 152 |
+
|
| 153 |
+
is_sequence_packed: bool = None
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
@dataclass
|
| 157 |
+
class AttnInternalBuffers(nl.NKIObject):
|
| 158 |
+
"""Container for all SBUF and PSUM tensor buffers used in attention computation."""
|
| 159 |
+
|
| 160 |
+
q_sb = None
|
| 161 |
+
k_sb = None
|
| 162 |
+
v_sb = None
|
| 163 |
+
k_sb_prior = None
|
| 164 |
+
v_sb_prior = None
|
| 165 |
+
|
| 166 |
+
k_scale_sb = None
|
| 167 |
+
|
| 168 |
+
mm1_psum = None
|
| 169 |
+
mm1_copy_sb = None
|
| 170 |
+
mm1_affine_select_output = None
|
| 171 |
+
mm1_masked = None
|
| 172 |
+
mm1_partial_max = None
|
| 173 |
+
mm1_section_max = None
|
| 174 |
+
mm1_running_max = None
|
| 175 |
+
prev_mm1_running_max = None
|
| 176 |
+
flash_attn_correction_factor = None
|
| 177 |
+
|
| 178 |
+
exp_sb = None
|
| 179 |
+
exp_partial_sum = None
|
| 180 |
+
exp_section_sum = None
|
| 181 |
+
exp_tp_sb = None
|
| 182 |
+
exp_running_sum = None
|
| 183 |
+
prev_exp_running_sum = None
|
| 184 |
+
exp_sum_reciprocal = None
|
| 185 |
+
|
| 186 |
+
mm2_psum = None
|
| 187 |
+
mm2_sb = None
|
| 188 |
+
mm2_prev_output = None
|
| 189 |
+
mm2_accum_flash_attn = None
|
| 190 |
+
mm2_final = None
|
| 191 |
+
|
| 192 |
+
tp_flash_attn_correction_factor_psum = None
|
| 193 |
+
tp_flash_attn_correction_factor_sb = None
|
| 194 |
+
tp_exp_sum_reciprocal_psum = None
|
| 195 |
+
tp_exp_sum_reciprocal_sb = None
|
| 196 |
+
mm2_prev_output_scaled = None
|
| 197 |
+
|
| 198 |
+
zero_bias_tensor = None
|
| 199 |
+
sink_sb = None
|
| 200 |
+
range_sel_lbs = None
|
| 201 |
+
range_sel_ubs = None
|
| 202 |
+
range_sel_lbs_prior = None
|
| 203 |
+
range_sel_ubs_prior = None
|
| 204 |
+
k_offset_sb_u32 = None
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
@dataclass
|
| 208 |
+
class SectionParams(nl.NKIObject):
|
| 209 |
+
section_idx = None
|
| 210 |
+
section_offset = None
|
| 211 |
+
section_offset_active = None
|
| 212 |
+
next_section_offset_active = None
|
| 213 |
+
section_contains_prefix = None
|
| 214 |
+
next_section_contains_prefix = None
|
| 215 |
+
kv_section_idx = None
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
@dataclass
|
| 219 |
+
class AttnTileParams(nl.NKIObject):
|
| 220 |
+
"""Tile and buffer sizing parameters computed during implementation."""
|
| 221 |
+
|
| 222 |
+
seqlen_k_active_updated: int = None
|
| 223 |
+
seqlen_k_prior_padded: int = None
|
| 224 |
+
is_causal: bool = None
|
| 225 |
+
|
| 226 |
+
sb_p: int = None
|
| 227 |
+
|
| 228 |
+
num_grps: int = None
|
| 229 |
+
num_q_grps_per_load: int = None
|
| 230 |
+
can_pack_q_load: bool = None
|
| 231 |
+
|
| 232 |
+
num_large_tiles_per_section: int = None
|
| 233 |
+
num_k_tiles_per_section: int = None
|
| 234 |
+
num_v_tiles_per_section: int = None
|
| 235 |
+
|
| 236 |
+
exp_inst_elems: int = None
|
| 237 |
+
num_exp_insts_per_large_tile: int = None
|
| 238 |
+
|
| 239 |
+
num_tps_in_mm2_grp: int = None
|
| 240 |
+
mm2_grp_sz: int = None
|
| 241 |
+
|
| 242 |
+
use_swa_optimized_allocation: bool = None
|
| 243 |
+
|
| 244 |
+
dynamic_sel_mask: bool = None
|
| 245 |
+
|
| 246 |
+
section_len: int = None
|
| 247 |
+
num_sections: int = None
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
# ==========================================================================================
|
| 251 |
+
# Entry: _attention_cte. Dead paths removed: prefix caching / APC / sink / seqpack / CP / SWA /
|
| 252 |
+
# fused-v-proj / seqlen-q sharding remainder / cache_softmax. This call ALWAYS takes
|
| 253 |
+
# use_sbuf_kv=True, causal_mask=False, tp_out=True, single-shard (num_shard=1), bs=1.
|
| 254 |
+
# ==========================================================================================
|
| 255 |
+
def _attention_cte(
|
| 256 |
+
q: nl.ndarray,
|
| 257 |
+
k: Optional[nl.ndarray] = None,
|
| 258 |
+
v: Optional[nl.ndarray] = None,
|
| 259 |
+
scale: float = 1.0,
|
| 260 |
+
causal_mask: bool = True,
|
| 261 |
+
tp_q: bool = True,
|
| 262 |
+
tp_k: bool = False,
|
| 263 |
+
tp_out: bool = False,
|
| 264 |
+
cache_softmax: bool = False,
|
| 265 |
+
softmax_dtype=nl.float32,
|
| 266 |
+
mm_out_dtype=nl.float32,
|
| 267 |
+
k_cache_sbuf: Optional[List[nl.ndarray]] = None,
|
| 268 |
+
v_cache_sbuf: Optional[List[nl.ndarray]] = None,
|
| 269 |
+
out_o_hbm=None,
|
| 270 |
+
init_sbuf_addr: int = 0,
|
| 271 |
+
seqlen_k_valid: int = None,
|
| 272 |
+
):
|
| 273 |
+
"""One head of context-encoding attention (SBUF-KV path).
|
| 274 |
+
EDIT: ``seqlen_k_valid`` overrides the active key length (default len(k_cache)*512)
|
| 275 |
+
so keys at positions >= seqlen_k_valid (token padding) are skipped by every loop bound.
|
| 276 |
+
|
| 277 |
+
LIVE contract for the K2 encoder: ``k=v=None`` (K/V handed in via ``k_cache_sbuf`` /
|
| 278 |
+
``v_cache_sbuf``), ``causal_mask=False``, ``tp_q=False, tp_k=False, tp_out=True``,
|
| 279 |
+
``cache_softmax=False``, ``out_o_hbm`` is a LIST of SBUF slices into the caller's O tile.
|
| 280 |
+
|
| 281 |
+
DEAD (removed): prefix caching (k_prior/v_prior/prior_used_len), sink, sliding_window, CP
|
| 282 |
+
(cp_offset/global_cp_deg/striped/strided), sequence packing (bound_min/bound_max),
|
| 283 |
+
skip_output_normalization, fp8 k_scale, and the fused v-projection (h_hbm/wv_t/bv).
|
| 284 |
+
"""
|
| 285 |
+
is_prefix_caching = False
|
| 286 |
+
|
| 287 |
+
use_sbuf_kv = (k_cache_sbuf is not None and len(k_cache_sbuf) > 0) or (
|
| 288 |
+
v_cache_sbuf is not None and len(v_cache_sbuf) > 0
|
| 289 |
+
)
|
| 290 |
+
kernel_assert(
|
| 291 |
+
k_cache_sbuf is not None and len(k_cache_sbuf) > 0, "k_cache_sbuf must be provided when using sbuf kv"
|
| 292 |
+
)
|
| 293 |
+
kernel_assert(
|
| 294 |
+
v_cache_sbuf is not None and len(v_cache_sbuf) > 0, "v_cache_sbuf must be provided when using sbuf kv"
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
seqlen_q, seqlen_k_active, seqlen_k_prior, d, out_shape, softmax_shape = _check_input_and_return_shape(
|
| 298 |
+
q,
|
| 299 |
+
k,
|
| 300 |
+
v,
|
| 301 |
+
is_prefix_caching,
|
| 302 |
+
None,
|
| 303 |
+
None,
|
| 304 |
+
None,
|
| 305 |
+
tp_q,
|
| 306 |
+
tp_k,
|
| 307 |
+
tp_out,
|
| 308 |
+
cache_softmax,
|
| 309 |
+
k_cache_sbuf,
|
| 310 |
+
v_cache_sbuf,
|
| 311 |
+
None,
|
| 312 |
+
None,
|
| 313 |
+
skip_v_check=False,
|
| 314 |
+
)
|
| 315 |
+
if seqlen_k_valid is not None:
|
| 316 |
+
kernel_assert(seqlen_k_valid <= seqlen_k_active, "seqlen_k_valid exceeds K cache length")
|
| 317 |
+
seqlen_k_active = seqlen_k_valid
|
| 318 |
+
|
| 319 |
+
# Use out_o_hbm if provided (LIVE: it is a list of SBUF slices).
|
| 320 |
+
if out_o_hbm is not None:
|
| 321 |
+
result = out_o_hbm
|
| 322 |
+
else:
|
| 323 |
+
result = nl.ndarray(shape=out_shape, dtype=q.dtype, buffer=nl.shared_hbm)
|
| 324 |
+
|
| 325 |
+
bs = q.shape[0]
|
| 326 |
+
bs_kv = 1 # sbuf-kv: batch_size_kv is per-batch tiles
|
| 327 |
+
|
| 328 |
+
kernel_assert(bs > 0, f"Batch size must be positive, got {bs}")
|
| 329 |
+
kernel_assert(bs <= _MAX_BS, f"attention_cte kernel is not tested for batch size above {_MAX_BS}, got {bs}.")
|
| 330 |
+
kernel_assert(bs_kv > 0, f"Batch size must be positive, got {bs_kv}")
|
| 331 |
+
kernel_assert(bs % bs_kv == 0, f"Q batch size must be a multiple of KV batch size, got {bs=}, {bs_kv=}")
|
| 332 |
+
|
| 333 |
+
seqlen_k_total = seqlen_k_active
|
| 334 |
+
kernel_assert(seqlen_q <= _MAX_SEQLEN, f"seqlen above {_MAX_SEQLEN}, got {seqlen_q=}.")
|
| 335 |
+
kernel_assert(seqlen_k_total <= _MAX_SEQLEN, f"seqlen above {_MAX_SEQLEN}, got {seqlen_k_total=}.")
|
| 336 |
+
bs_seqlen_qk_product = float(bs * seqlen_q) * seqlen_k_total
|
| 337 |
+
if bs_seqlen_qk_product > _MAX_BS_TIMES_SEQLEN_QK:
|
| 338 |
+
logger.warn(
|
| 339 |
+
f"attention_cte kernel is not tested for batch size x seqlen_q x seqlen_k above "
|
| 340 |
+
f"{_MAX_BS_TIMES_SEQLEN_QK}, got {bs_seqlen_qk_product=}.",
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
kernel_assert(d > 0, f"d must be > 0, got {d=}.")
|
| 344 |
+
kernel_assert(d <= _MAX_HEAD_DIM, f"we do not support head_dim > {_MAX_HEAD_DIM}, got head dim {d}")
|
| 345 |
+
|
| 346 |
+
kernel_assert(
|
| 347 |
+
(str(mm_out_dtype) == str(nl.float32))
|
| 348 |
+
or (str(mm_out_dtype) == str(nl.bfloat16) and (nisa.get_nc_version() >= nisa.nc_version.gen4)),
|
| 349 |
+
f"mm_out_dtype (psum) should be in [float32, bfloat16], got {mm_out_dtype} in {nisa.get_nc_version()}.",
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
ac = AttnConfig(
|
| 353 |
+
seqlen_q=seqlen_q,
|
| 354 |
+
seqlen_k_active=seqlen_k_active,
|
| 355 |
+
seqlen_k_prior=seqlen_k_prior,
|
| 356 |
+
d=d,
|
| 357 |
+
tp_q=tp_q,
|
| 358 |
+
tp_k=tp_k,
|
| 359 |
+
tp_out=tp_out,
|
| 360 |
+
is_prefix_caching=is_prefix_caching,
|
| 361 |
+
causal_mask=causal_mask,
|
| 362 |
+
use_swa=False,
|
| 363 |
+
sliding_window=0,
|
| 364 |
+
use_cp=False,
|
| 365 |
+
global_cp_deg=None,
|
| 366 |
+
cp_strided_q_slicing=False,
|
| 367 |
+
cp_striped_input=False,
|
| 368 |
+
scale=scale,
|
| 369 |
+
cache_softmax=cache_softmax,
|
| 370 |
+
skip_output_normalization=False,
|
| 371 |
+
dtype=q.dtype,
|
| 372 |
+
softmax_dtype=softmax_dtype,
|
| 373 |
+
mm_out_dtype=mm_out_dtype,
|
| 374 |
+
is_sequence_packed=False,
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
# Sharding info. use_sbuf_kv disables internal sharding (caller already sharded), so
|
| 378 |
+
# num_shard=1, shard_id=0, num_bs_per_shard=bs, has_remainder=False.
|
| 379 |
+
get_verified_program_sharding_info("attention_cte", max_sharding=2)
|
| 380 |
+
num_shard = 1
|
| 381 |
+
shard_id = 0
|
| 382 |
+
num_bs_per_shard = bs // num_shard
|
| 383 |
+
bs_offset = shard_id * num_bs_per_shard
|
| 384 |
+
|
| 385 |
+
for batch_idx in range(num_bs_per_shard):
|
| 386 |
+
# use_sbuf_kv: bs is LOCAL batch size and b is the LOCAL index.
|
| 387 |
+
batch_id = batch_idx
|
| 388 |
+
kv_batch_id = _q_to_kv_batch_id(batch_id, bs, bs_kv)
|
| 389 |
+
_attention_cte_impl(
|
| 390 |
+
q,
|
| 391 |
+
k,
|
| 392 |
+
v,
|
| 393 |
+
None,
|
| 394 |
+
None,
|
| 395 |
+
None,
|
| 396 |
+
result,
|
| 397 |
+
batch_id,
|
| 398 |
+
kv_batch_id,
|
| 399 |
+
ac,
|
| 400 |
+
k_cache_sbuf=k_cache_sbuf,
|
| 401 |
+
v_cache_sbuf=v_cache_sbuf,
|
| 402 |
+
init_sbuf_addr=init_sbuf_addr,
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
# has_remainder is False (num_shard==1); the seqlen-q sharding path is DEAD.
|
| 406 |
+
return result
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def _attention_cte_impl(
|
| 410 |
+
q,
|
| 411 |
+
k_active,
|
| 412 |
+
v_active,
|
| 413 |
+
k_prior,
|
| 414 |
+
v_prior,
|
| 415 |
+
prior_used_len,
|
| 416 |
+
o,
|
| 417 |
+
batch_id,
|
| 418 |
+
batch_id_kv,
|
| 419 |
+
ac: AttnConfig,
|
| 420 |
+
k_cache_sbuf: Optional[List[nl.ndarray]] = None,
|
| 421 |
+
v_cache_sbuf: Optional[List[nl.ndarray]] = None,
|
| 422 |
+
init_sbuf_addr: int = 0,
|
| 423 |
+
):
|
| 424 |
+
"""Single-batch attention body. LIVE path only: single section (num_sections==1), pipelined
|
| 425 |
+
group loop (num_grps=16 > 1), SBUF-KV (no K/V HBM load), tp_out=True.
|
| 426 |
+
|
| 427 |
+
DEAD (removed): seqlen-q sharding, sink, SWA memset, sections>1 flash accumulation, K/V HBM
|
| 428 |
+
load (_load_k_tile / _load_v_tile), SBUF-prior storage, cache_softmax intermediate writes,
|
| 429 |
+
the non-pipelined (shard_seqlen_q_length<=1) branch.
|
| 430 |
+
"""
|
| 431 |
+
# Not seqlen-sharded: start at group 0, cover all groups.
|
| 432 |
+
atp = _compute_tile_parameters(ac, is_seqlen_sharded=False)
|
| 433 |
+
shard_seqlen_q_start = 0
|
| 434 |
+
shard_seqlen_q_length = atp.num_grps
|
| 435 |
+
|
| 436 |
+
allocator = ModularAllocator(initial_address=init_sbuf_addr)
|
| 437 |
+
bufs = AttnInternalBuffers()
|
| 438 |
+
bufs.k_scale_sb = None
|
| 439 |
+
|
| 440 |
+
bufs.zero_bias_tensor = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, 1), dtype=nl.float32)
|
| 441 |
+
nisa.memset(bufs.zero_bias_tensor, 0.0)
|
| 442 |
+
|
| 443 |
+
# _setup_range_select_bounds is a NO-OP here: dynamic_sel_mask=False and not prefix caching.
|
| 444 |
+
_setup_range_select_bounds(ac, atp, bufs, allocator, None, None, None, None, batch_id=batch_id)
|
| 445 |
+
|
| 446 |
+
bufs.mm1_running_max = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, atp.num_grps), dtype=nl.float32)
|
| 447 |
+
bufs.exp_running_sum = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, atp.num_grps), dtype=nl.float32)
|
| 448 |
+
bufs.exp_sum_reciprocal = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, atp.num_grps), dtype=nl.float32)
|
| 449 |
+
|
| 450 |
+
sbuf_addr_outer = allocator.get_current_address()
|
| 451 |
+
|
| 452 |
+
# Single section (num_sections == 1).
|
| 453 |
+
for section_idx in range(atp.num_sections):
|
| 454 |
+
section_offset = atp.section_len * section_idx
|
| 455 |
+
section_offset_active, section_contains_prefix = _compute_section_offset_active(
|
| 456 |
+
section_offset, ac.is_prefix_caching, atp.seqlen_k_prior_padded
|
| 457 |
+
)
|
| 458 |
+
next_section_offset = atp.section_len * (section_idx + 1)
|
| 459 |
+
next_section_offset_active, next_section_contains_prefix = _compute_section_offset_active(
|
| 460 |
+
next_section_offset, ac.is_prefix_caching, atp.seqlen_k_prior_padded
|
| 461 |
+
)
|
| 462 |
+
sp = SectionParams(
|
| 463 |
+
section_idx=section_idx,
|
| 464 |
+
section_offset=section_offset,
|
| 465 |
+
section_offset_active=section_offset_active,
|
| 466 |
+
next_section_offset_active=next_section_offset_active,
|
| 467 |
+
section_contains_prefix=section_contains_prefix,
|
| 468 |
+
next_section_contains_prefix=next_section_contains_prefix,
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
allocator.set_current_address(sbuf_addr_outer)
|
| 472 |
+
_allocate_attention_buffers(allocator, ac, atp, bufs, sink=None,
|
| 473 |
+
k_cache_sbuf=k_cache_sbuf, v_cache_sbuf=v_cache_sbuf)
|
| 474 |
+
sbuf_addr = allocator.get_current_address()
|
| 475 |
+
|
| 476 |
+
# SBUF-KV: K and V are already resident, so the HBM K/V load block is skipped entirely.
|
| 477 |
+
|
| 478 |
+
# Software pipelining: num_grps=16 > 1, so the pipelined branch is the only reached one.
|
| 479 |
+
_load_q_impl(shard_seqlen_q_start, ac, atp, sp, bufs, q, batch_id, sbuf_addr)
|
| 480 |
+
_qk_and_max_impl(shard_seqlen_q_start, ac, atp, sp, bufs, batch_id)
|
| 481 |
+
_update_max_impl(shard_seqlen_q_start, ac, atp, sp, bufs, None)
|
| 482 |
+
_exp_impl(shard_seqlen_q_start, ac, atp, sp, bufs, None)
|
| 483 |
+
|
| 484 |
+
_load_q_impl(shard_seqlen_q_start + 1, ac, atp, sp, bufs, q, batch_id, sbuf_addr)
|
| 485 |
+
_qk_and_max_impl(shard_seqlen_q_start + 1, ac, atp, sp, bufs, batch_id)
|
| 486 |
+
_update_max_impl(shard_seqlen_q_start + 1, ac, atp, sp, bufs, None)
|
| 487 |
+
shard_seqlen_q_end = shard_seqlen_q_start + shard_seqlen_q_length
|
| 488 |
+
|
| 489 |
+
for grp_i in range(shard_seqlen_q_start, shard_seqlen_q_end - 2):
|
| 490 |
+
# grp_i: PV, write_back ; grp_i+1: EXP ; grp_i+2: load Q, QK+Max.
|
| 491 |
+
_load_q_impl(grp_i + 2, ac, atp, sp, bufs, q, batch_id, sbuf_addr)
|
| 492 |
+
_exp_impl(grp_i + 1, ac, atp, sp, bufs, None)
|
| 493 |
+
_fused_qkmax_and_pv_impl(grp_i, ac, atp, sp, bufs, batch_id)
|
| 494 |
+
_write_back_impl(grp_i, ac, atp, sp, bufs, o, batch_id)
|
| 495 |
+
_update_max_impl(grp_i + 2, ac, atp, sp, bufs, None)
|
| 496 |
+
|
| 497 |
+
_pv_impl(shard_seqlen_q_end - 2, ac, atp, sp, bufs)
|
| 498 |
+
_write_back_impl(shard_seqlen_q_end - 2, ac, atp, sp, bufs, o, batch_id)
|
| 499 |
+
_exp_impl(shard_seqlen_q_end - 1, ac, atp, sp, bufs, None)
|
| 500 |
+
_pv_impl(shard_seqlen_q_end - 1, ac, atp, sp, bufs)
|
| 501 |
+
_write_back_impl(shard_seqlen_q_end - 1, ac, atp, sp, bufs, o, batch_id)
|
| 502 |
+
|
| 503 |
+
# cache_softmax intermediate writes (out_neg_max / out_sum_recip) are DEAD (cache_softmax=False).
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
def _compute_tile_parameters(ac: AttnConfig, is_seqlen_sharded: bool) -> AttnTileParams:
|
| 507 |
+
"""Tile parameters for the LIVE config. Dead: SWA/prefix/CP scale checks and seqlen updates,
|
| 508 |
+
flash sectioning (total_seqlen_k=2048 <= 10240 -> single section)."""
|
| 509 |
+
atp = AttnTileParams()
|
| 510 |
+
|
| 511 |
+
# LIVE: causal_mask=False, no CP/SWA/seqpack -> is_causal=False, dynamic_sel_mask=False.
|
| 512 |
+
atp.is_causal = ac.causal_mask
|
| 513 |
+
atp.dynamic_sel_mask = False
|
| 514 |
+
atp.seqlen_k_active_updated = ac.seqlen_k_active
|
| 515 |
+
atp.use_swa_optimized_allocation = False
|
| 516 |
+
|
| 517 |
+
atp.sb_p = nl.tile_size.pmax
|
| 518 |
+
kernel_assert(_Q_GRP_SZ == atp.sb_p, f"expect Q group size == pmax, got {_Q_GRP_SZ=}, {atp.sb_p=}")
|
| 519 |
+
kernel_assert(_V_TILE_SZ == atp.sb_p, f"expect V tile size == pmax, got {_V_TILE_SZ=}, {atp.sb_p=}")
|
| 520 |
+
|
| 521 |
+
atp.num_grps = div_ceil(ac.seqlen_q, atp.sb_p)
|
| 522 |
+
atp.can_pack_q_load = not is_seqlen_sharded
|
| 523 |
+
|
| 524 |
+
num_q_grps_per_load_dtype = 4 if ac.dtype == nl.float32 else 8
|
| 525 |
+
atp.num_q_grps_per_load = min(num_q_grps_per_load_dtype if atp.can_pack_q_load else 1, atp.num_grps)
|
| 526 |
+
kernel_assert(atp.num_q_grps_per_load > 0, f"num_q_grps_per_load must be positive, got {atp.num_q_grps_per_load}.")
|
| 527 |
+
|
| 528 |
+
atp.seqlen_k_prior_padded = None # not prefix caching
|
| 529 |
+
total_seqlen_k = atp.seqlen_k_active_updated
|
| 530 |
+
|
| 531 |
+
# total_seqlen_k = 2048 <= 10240 -> no flash attention, single section.
|
| 532 |
+
use_flash_attn = total_seqlen_k > _FLASH_ATTENTION_THRESHOLD
|
| 533 |
+
if use_flash_attn:
|
| 534 |
+
atp.section_len = min(total_seqlen_k, _FLASH_ATTENTION_SECTION_LENGTH)
|
| 535 |
+
else:
|
| 536 |
+
atp.section_len = total_seqlen_k
|
| 537 |
+
|
| 538 |
+
kernel_assert(atp.section_len > 0, f"section_len must be positive, got {atp.section_len}")
|
| 539 |
+
atp.num_sections = div_ceil(total_seqlen_k, atp.section_len)
|
| 540 |
+
if not use_flash_attn:
|
| 541 |
+
kernel_assert(atp.num_sections == 1, "Logic fault, must only have 1 section if not using flash_attn")
|
| 542 |
+
|
| 543 |
+
atp.num_large_tiles_per_section = div_ceil(atp.section_len, _LARGE_TILE_SZ)
|
| 544 |
+
atp.num_k_tiles_per_section = div_ceil(atp.section_len, _K_TILE_SZ)
|
| 545 |
+
atp.num_v_tiles_per_section = div_ceil(atp.section_len, _V_TILE_SZ)
|
| 546 |
+
|
| 547 |
+
atp.exp_inst_elems = _EXP_TILE_SZ
|
| 548 |
+
atp.num_exp_insts_per_large_tile = _LARGE_TILE_SZ // atp.exp_inst_elems
|
| 549 |
+
atp.num_tps_in_mm2_grp = _K_TILE_SZ // atp.sb_p
|
| 550 |
+
atp.mm2_grp_sz = _K_TILE_SZ
|
| 551 |
+
|
| 552 |
+
return atp
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
def _setup_range_select_bounds(
|
| 556 |
+
ac: AttnConfig,
|
| 557 |
+
atp: AttnTileParams,
|
| 558 |
+
bufs: AttnInternalBuffers,
|
| 559 |
+
allocator: ModularAllocator,
|
| 560 |
+
cp_offset: Any,
|
| 561 |
+
prior_used_len: Any,
|
| 562 |
+
bound_min: Any,
|
| 563 |
+
bound_max: Any,
|
| 564 |
+
batch_id: int = 0,
|
| 565 |
+
) -> tuple:
|
| 566 |
+
"""NO-OP for this config: only CP / SWA / prefix-caching / sequence-packing populate range
|
| 567 |
+
bounds, and all are off (``dynamic_sel_mask=False``, ``is_prefix_caching=False``). The entire
|
| 568 |
+
vendor body was gated by ``if atp.dynamic_sel_mask:`` / ``if ac.is_prefix_caching:`` / SWA,
|
| 569 |
+
so it emits no instructions here and is removed."""
|
| 570 |
+
return
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
def _allocate_attention_buffers(
|
| 574 |
+
allocator: ModularAllocator,
|
| 575 |
+
ac: AttnConfig,
|
| 576 |
+
atp: AttnTileParams,
|
| 577 |
+
bufs: AttnInternalBuffers,
|
| 578 |
+
sink: Any,
|
| 579 |
+
k_cache_sbuf: Optional[List[nl.ndarray]] = None,
|
| 580 |
+
v_cache_sbuf: Optional[List[nl.ndarray]] = None,
|
| 581 |
+
):
|
| 582 |
+
"""Allocate SBUF/PSUM buffers. LIVE: tp_out=True (so all tp_out branches taken), K/V come
|
| 583 |
+
from k_cache_sbuf/v_cache_sbuf, sink=None, dynamic_sel_mask=False (so the static-mask
|
| 584 |
+
mm1_copy_sb / mm1_affine_select_output buffers ARE allocated -- kept). The tp_out=False
|
| 585 |
+
branches and the fused-v-proj branch are DEAD and removed."""
|
| 586 |
+
mm2_p, mm2_n = atp.sb_p, ac.d
|
| 587 |
+
|
| 588 |
+
# K/V provided in SBUF by the caller.
|
| 589 |
+
bufs.k_sb = k_cache_sbuf
|
| 590 |
+
bufs.v_sb = v_cache_sbuf
|
| 591 |
+
|
| 592 |
+
bufs.q_sb = allocator.alloc_sbuf_tensor(
|
| 593 |
+
shape=(ac.d, atp.sb_p * atp.num_q_grps_per_load),
|
| 594 |
+
dtype=nl.bfloat16,
|
| 595 |
+
block_dim=[div_ceil(atp.num_grps, atp.num_q_grps_per_load)],
|
| 596 |
+
num_free_tiles=[2],
|
| 597 |
+
align_to=32,
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
bufs.flash_attn_correction_factor = allocator.alloc_sbuf_tensor(
|
| 601 |
+
shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
mm1_partial_max_n_elts = atp.num_k_tiles_per_section # sink is None
|
| 605 |
+
bufs.mm1_partial_max = allocator.alloc_sbuf_tensor(
|
| 606 |
+
shape=(atp.sb_p, mm1_partial_max_n_elts), dtype=nl.float32,
|
| 607 |
+
block_dim=[atp.num_grps], num_free_tiles=[2], align_to=4,
|
| 608 |
+
)
|
| 609 |
+
|
| 610 |
+
bufs.mm1_section_max = allocator.alloc_sbuf_tensor(
|
| 611 |
+
shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
n_final_reduce_sum_elts = div_ceil(atp.section_len, atp.exp_inst_elems) # sink is None
|
| 615 |
+
bufs.exp_partial_sum = allocator.alloc_sbuf_tensor(
|
| 616 |
+
shape=(atp.sb_p, n_final_reduce_sum_elts), dtype=nl.float32,
|
| 617 |
+
block_dim=[atp.num_grps], num_free_tiles=[2],
|
| 618 |
+
)
|
| 619 |
+
|
| 620 |
+
bufs.exp_section_sum = allocator.alloc_sbuf_tensor(
|
| 621 |
+
shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
|
| 622 |
+
)
|
| 623 |
+
|
| 624 |
+
bufs.prev_mm1_running_max = allocator.alloc_sbuf_tensor(
|
| 625 |
+
shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
|
| 626 |
+
)
|
| 627 |
+
|
| 628 |
+
bufs.prev_exp_running_sum = allocator.alloc_sbuf_tensor(
|
| 629 |
+
shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
# tp_out=True everywhere below.
|
| 633 |
+
bufs.mm2_prev_output = allocator.alloc_sbuf_tensor(
|
| 634 |
+
shape=(ac.d, atp.sb_p), dtype=ac.mm_out_dtype, block_dim=[atp.num_grps], num_free_tiles=[2],
|
| 635 |
+
)
|
| 636 |
+
|
| 637 |
+
bufs.mm2_accum_flash_attn = allocator.alloc_sbuf_tensor(
|
| 638 |
+
shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
bufs.mm2_prev_output_scaled = allocator.alloc_sbuf_tensor(
|
| 642 |
+
shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2],
|
| 643 |
+
)
|
| 644 |
+
|
| 645 |
+
bufs.tp_flash_attn_correction_factor_psum = []
|
| 646 |
+
bufs.tp_exp_sum_reciprocal_psum = []
|
| 647 |
+
for grp_idx in range(atp.num_grps):
|
| 648 |
+
tp_flash_attn_correction_factor_psum_tile = nl.ndarray(
|
| 649 |
+
(ac.d, atp.sb_p), dtype=nl.float32, buffer=nl.psum,
|
| 650 |
+
address=(0, ((grp_idx % 2) * 4 + 3) * PSUM_BANK_SIZE),
|
| 651 |
+
)
|
| 652 |
+
bufs.tp_flash_attn_correction_factor_psum.append(tp_flash_attn_correction_factor_psum_tile)
|
| 653 |
+
tp_exp_sum_reciprocal_psum_tile = nl.ndarray(
|
| 654 |
+
(ac.d, atp.sb_p), dtype=nl.float32, buffer=nl.psum,
|
| 655 |
+
address=(0, ((grp_idx % 2) * 4 + 3) * PSUM_BANK_SIZE),
|
| 656 |
+
)
|
| 657 |
+
bufs.tp_exp_sum_reciprocal_psum.append(tp_exp_sum_reciprocal_psum_tile)
|
| 658 |
+
|
| 659 |
+
bufs.tp_flash_attn_correction_factor_sb = allocator.alloc_sbuf_tensor(
|
| 660 |
+
shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[4],
|
| 661 |
+
)
|
| 662 |
+
|
| 663 |
+
bufs.tp_exp_sum_reciprocal_sb = allocator.alloc_sbuf_tensor(
|
| 664 |
+
shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[4],
|
| 665 |
+
)
|
| 666 |
+
|
| 667 |
+
bufs.mm2_final = allocator.alloc_sbuf_tensor(
|
| 668 |
+
shape=(ac.d, atp.sb_p), dtype=ac.mm_out_dtype, block_dim=[atp.num_grps], num_free_tiles=[2],
|
| 669 |
+
)
|
| 670 |
+
|
| 671 |
+
bufs.mm2_sb = allocator.alloc_sbuf_tensor(
|
| 672 |
+
shape=(mm2_n, mm2_p), dtype=ac.mm_out_dtype, block_dim=[atp.num_grps], num_free_tiles=[2],
|
| 673 |
+
)
|
| 674 |
+
|
| 675 |
+
bufs.mm1_masked = allocator.alloc_sbuf_tensor(
|
| 676 |
+
shape=(atp.sb_p, _LARGE_TILE_SZ), dtype=nl.float32,
|
| 677 |
+
block_dim=[atp.num_grps, atp.num_large_tiles_per_section],
|
| 678 |
+
num_free_tiles=[2, atp.num_large_tiles_per_section],
|
| 679 |
+
)
|
| 680 |
+
|
| 681 |
+
bufs.exp_sb = allocator.alloc_sbuf_tensor(
|
| 682 |
+
shape=(atp.sb_p, _LARGE_TILE_SZ), dtype=nl.bfloat16,
|
| 683 |
+
block_dim=[atp.num_grps, atp.num_large_tiles_per_section],
|
| 684 |
+
# DP2: group-axis depth 2 so exp(g+1) runs while transpose(g) still drains exp_sb(g).
|
| 685 |
+
num_free_tiles=[2, atp.num_large_tiles_per_section],
|
| 686 |
+
)
|
| 687 |
+
|
| 688 |
+
# MM1 PSUM tiles: banks {0,1,2,3}.
|
| 689 |
+
bufs.mm1_psum = []
|
| 690 |
+
for grp_idx in range(atp.num_grps):
|
| 691 |
+
grp_row = []
|
| 692 |
+
for large_tile_idx in range(atp.num_large_tiles_per_section):
|
| 693 |
+
tile_row = []
|
| 694 |
+
for k_tile_idx in range(4):
|
| 695 |
+
mm1_psum_tile = nl.ndarray(
|
| 696 |
+
(atp.sb_p, nl.tile_size.psum_fmax), dtype=ac.mm_out_dtype, buffer=nl.psum,
|
| 697 |
+
address=(0, (k_tile_idx % 4) * PSUM_BANK_SIZE),
|
| 698 |
+
)
|
| 699 |
+
tile_row.append(mm1_psum_tile)
|
| 700 |
+
grp_row.append(tile_row)
|
| 701 |
+
bufs.mm1_psum.append(grp_row)
|
| 702 |
+
|
| 703 |
+
# dynamic_sel_mask=False -> the static-mask staging buffers are allocated (no-mask branch
|
| 704 |
+
# does not read them, but the buffers exist in the vendor's non-dynamic allocation).
|
| 705 |
+
bufs.mm1_copy_sb = allocator.alloc_sbuf_tensor(
|
| 706 |
+
shape=(atp.sb_p, nl.tile_size.psum_fmax), dtype=ac.mm_out_dtype,
|
| 707 |
+
block_dim=[atp.num_grps, atp.num_large_tiles_per_section, 4], num_free_tiles=[1, 1, 2],
|
| 708 |
+
)
|
| 709 |
+
bufs.mm1_affine_select_output = allocator.alloc_sbuf_tensor(
|
| 710 |
+
shape=(atp.sb_p, nl.tile_size.psum_fmax), dtype=ac.mm_out_dtype,
|
| 711 |
+
block_dim=[atp.num_grps, atp.num_large_tiles_per_section, 4], num_free_tiles=[1, 1, 2],
|
| 712 |
+
)
|
| 713 |
+
|
| 714 |
+
bufs.exp_tp_sb = allocator.alloc_sbuf_tensor(
|
| 715 |
+
shape=(atp.sb_p, atp.mm2_grp_sz), dtype=nl.bfloat16,
|
| 716 |
+
block_dim=[atp.num_grps, atp.num_large_tiles_per_section, atp.num_tps_in_mm2_grp],
|
| 717 |
+
# DP4: 8 group-blocks resident so P^T transposes issue as soon as their exp exists.
|
| 718 |
+
num_free_tiles=[8, atp.num_large_tiles_per_section, atp.num_tps_in_mm2_grp],
|
| 719 |
+
align_to=32,
|
| 720 |
+
)
|
| 721 |
+
|
| 722 |
+
# MM2 PSUM tiles: banks {4,5,6,7}. tp_out=True shape (mm2_n, mm2_p).
|
| 723 |
+
bufs.mm2_psum = []
|
| 724 |
+
for grp_idx in range(atp.num_grps):
|
| 725 |
+
grp_row = []
|
| 726 |
+
for large_tile_idx in range(atp.num_large_tiles_per_section):
|
| 727 |
+
mm2_psum_tile = nl.ndarray(
|
| 728 |
+
(mm2_n, mm2_p), dtype=ac.mm_out_dtype, buffer=nl.psum,
|
| 729 |
+
address=(0, ((4 + (large_tile_idx % 4)) * PSUM_BANK_SIZE)),
|
| 730 |
+
)
|
| 731 |
+
grp_row.append(mm2_psum_tile)
|
| 732 |
+
bufs.mm2_psum.append(grp_row)
|
| 733 |
+
|
| 734 |
+
|
| 735 |
+
def _q_to_kv_batch_id(batch_id: int, bs: int, bs_kv: int) -> int:
|
| 736 |
+
"""Map Q batch id to KV batch id for native GQA support. bs_kv=1 here -> always 0."""
|
| 737 |
+
return batch_id // (bs // bs_kv)
|
| 738 |
+
|
| 739 |
+
|
| 740 |
+
def _check_input_and_return_shape(
|
| 741 |
+
q,
|
| 742 |
+
k,
|
| 743 |
+
v,
|
| 744 |
+
is_prefix_caching,
|
| 745 |
+
k_prior,
|
| 746 |
+
v_prior,
|
| 747 |
+
prior_used_len,
|
| 748 |
+
tp_q,
|
| 749 |
+
tp_k,
|
| 750 |
+
tp_out,
|
| 751 |
+
cache_softmax,
|
| 752 |
+
k_cache_sbuf=None,
|
| 753 |
+
v_cache_sbuf=None,
|
| 754 |
+
k_prior_sbuf=None,
|
| 755 |
+
v_prior_sbuf=None,
|
| 756 |
+
skip_v_check=False,
|
| 757 |
+
) -> tuple:
|
| 758 |
+
"""Validate shapes and return (seqlen_q, seqlen_k, seqlen_k_prior, d, out_shape,
|
| 759 |
+
cache_softmax_shape). LIVE: use_sbuf_kv=True, tp_q=False, tp_out=True, not prefix caching,
|
| 760 |
+
cache_softmax=False."""
|
| 761 |
+
# SBUF-KV path.
|
| 762 |
+
kernel_assert(k_cache_sbuf is not None and len(k_cache_sbuf) > 0, "k_cache_sbuf must be provided")
|
| 763 |
+
kernel_assert(v_cache_sbuf is not None and len(v_cache_sbuf) > 0, "v_cache_sbuf must be provided")
|
| 764 |
+
|
| 765 |
+
if tp_q:
|
| 766 |
+
batch_size, seqlen_q, d = q.shape
|
| 767 |
+
else:
|
| 768 |
+
batch_size, d, seqlen_q = q.shape
|
| 769 |
+
|
| 770 |
+
d_from_k = k_cache_sbuf[0].shape[0]
|
| 771 |
+
kernel_assert(d == d_from_k, f"Head dimension mismatch: q has d={d}, but k_cache_sbuf has d={d_from_k}")
|
| 772 |
+
seqlen_k = len(k_cache_sbuf) * _K_TILE_SZ
|
| 773 |
+
batch_size_kv = 1
|
| 774 |
+
seqlen_k_prior = None # not prefix caching
|
| 775 |
+
|
| 776 |
+
out_seqlen = seqlen_q
|
| 777 |
+
if tp_out:
|
| 778 |
+
out_shape = (batch_size, d, out_seqlen)
|
| 779 |
+
else:
|
| 780 |
+
out_shape = (batch_size, out_seqlen, d)
|
| 781 |
+
|
| 782 |
+
cache_softmax_tile_size = 128
|
| 783 |
+
padded_seq_grps = div_ceil(out_seqlen, cache_softmax_tile_size)
|
| 784 |
+
cache_softmax_shape = [batch_size, cache_softmax_tile_size, padded_seq_grps]
|
| 785 |
+
|
| 786 |
+
return (seqlen_q, seqlen_k, seqlen_k_prior, d, out_shape, cache_softmax_shape)
|
| 787 |
+
|
| 788 |
+
|
| 789 |
+
def _load_q_tile(q, out, tp_q, batch_id, grp_i, seqlen_offset, load_dtype, sbuf_addr, grps_per_load=1) -> int:
|
| 790 |
+
"""Load a Q tile HBM->SBUF. LIVE: tp_q=False (q is [bs, d, seqlen]), no transpose; the whole
|
| 791 |
+
``if tp_q:`` transpose sub-tree (dma_transpose / nc_transpose paths) is DEAD and removed."""
|
| 792 |
+
kernel_assert(str(load_dtype) == str(out[0].dtype), "Conflicting dtype")
|
| 793 |
+
_, d, seqlen = q.shape
|
| 794 |
+
|
| 795 |
+
num_f = min(seqlen - seqlen_offset, _Q_GRP_SZ * grps_per_load)
|
| 796 |
+
out_dst_pat = out[grp_i // grps_per_load].ap(pattern=[[_Q_GRP_SZ * grps_per_load, d], [1, num_f]], offset=0)
|
| 797 |
+
q_src_pat = q.ap(pattern=[[seqlen, d], [1, num_f]], offset=batch_id * d * seqlen + seqlen_offset)
|
| 798 |
+
nisa.dma_copy(dst=out_dst_pat, src=q_src_pat)
|
| 799 |
+
|
| 800 |
+
|
| 801 |
+
def _get_kv_tile_apc(
|
| 802 |
+
is_prefix_caching,
|
| 803 |
+
k_active,
|
| 804 |
+
k_prior,
|
| 805 |
+
seqlen_active,
|
| 806 |
+
seqlen_prior,
|
| 807 |
+
seqlen_offset,
|
| 808 |
+
load_offset_active,
|
| 809 |
+
) -> tuple:
|
| 810 |
+
"""Determine which KV tensor (active or prior) to use. LIVE: is_prefix_caching=False, so this
|
| 811 |
+
returns the trivial active-tile mapping; the prefix-caching branch is DEAD and removed."""
|
| 812 |
+
return k_active, seqlen_active, seqlen_offset, load_offset_active
|
| 813 |
+
|
| 814 |
+
|
| 815 |
+
def _compute_section_offset_active(k_section_offset, is_prefix_caching, seqlen_k_prior_padded):
|
| 816 |
+
"""Active-K offset for a section. LIVE: is_prefix_caching=False -> pass-through."""
|
| 817 |
+
section_contains_prefix = False
|
| 818 |
+
k_section_offset_active = k_section_offset
|
| 819 |
+
return k_section_offset_active, section_contains_prefix
|
| 820 |
+
|
| 821 |
+
|
| 822 |
+
def _load_q_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
|
| 823 |
+
bufs: AttnInternalBuffers, q, batch_id, sbuf_addr):
|
| 824 |
+
"""Load a Q group if compute is needed. LIVE: is_causal=False -> has_any_compute is always
|
| 825 |
+
True; k_scale_sb is None (fp8 dequant path DEAD)."""
|
| 826 |
+
if grp_i % atp.num_q_grps_per_load == 0:
|
| 827 |
+
# is_causal=False -> has_any_compute_pred is True.
|
| 828 |
+
q_seqlen_offset = grp_i * _Q_GRP_SZ
|
| 829 |
+
_load_q_tile(
|
| 830 |
+
q, bufs.q_sb, ac.tp_q, batch_id, grp_i, q_seqlen_offset,
|
| 831 |
+
bufs.q_sb[0].dtype, sbuf_addr, grps_per_load=atp.num_q_grps_per_load,
|
| 832 |
+
)
|
| 833 |
+
# k_scale_sb is None -> the delayed-fp8 Q scaling is DEAD.
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
def _qk_and_max_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
|
| 837 |
+
bufs: AttnInternalBuffers, batch_id: int = 0):
|
| 838 |
+
"""MM1 (QK^T) + row-max for a Q group. LIVE: is_causal=False -> always compute.
|
| 839 |
+
|
| 840 |
+
h10 EDIT (site :2876): the ``mm1_partial_max`` init memset is pinned to GpSimd -- it sheds
|
| 841 |
+
Vector (K2's binding engine) and its first consumer is several ops downstream, so a slower
|
| 842 |
+
idle engine is safe."""
|
| 843 |
+
# is_causal=False -> has_any_compute_pred is True.
|
| 844 |
+
nisa.memset(bufs.mm1_partial_max[grp_i], value=_FLOAT32_MIN, engine=engine.gpsimd)
|
| 845 |
+
|
| 846 |
+
for large_tile_idx in range(atp.num_large_tiles_per_section):
|
| 847 |
+
_qk_and_max_large_tile_impl(grp_i, large_tile_idx, ac, atp, sp, bufs, batch_id)
|
| 848 |
+
|
| 849 |
+
|
| 850 |
+
def _update_max_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
|
| 851 |
+
bufs: AttnInternalBuffers, sink):
|
| 852 |
+
"""Update running max. LIVE: is_causal=False (always compute), sink=None, num_sections==1 so
|
| 853 |
+
the reduce writes straight into mm1_running_max (max-direct promotion) and the flash
|
| 854 |
+
multi-section correction is DEAD."""
|
| 855 |
+
# is_causal=False -> has_any_compute_pred is True.
|
| 856 |
+
|
| 857 |
+
# num_sections==1: the reduce writes straight into mm1_running_max (max-direct); the [128,1]
|
| 858 |
+
# copy the vendor once had here is gone (3,072 Vector instructions removed upstream).
|
| 859 |
+
_max_dst = bufs.mm1_running_max[:, grp_i]
|
| 860 |
+
nisa.tensor_reduce(_max_dst, nl.maximum, bufs.mm1_partial_max[grp_i], 1, negate=True)
|
| 861 |
+
# num_sections==1: no running-max copy / flash correction factor. sink block DEAD (sink=None).
|
| 862 |
+
|
| 863 |
+
|
| 864 |
+
def _exp_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
|
| 865 |
+
bufs: AttnInternalBuffers, sink):
|
| 866 |
+
"""Softmax exp (Scalar ACTIVATE) + P^T dma_transposes. LIVE: is_causal=False, use_swa=False,
|
| 867 |
+
not prefix caching -> every k-tile is emitted (emit_idxs is full), wide_exp=False, sink=None.
|
| 868 |
+
|
| 869 |
+
The ``memset`` on ``exp_partial_sum`` stays UNPINNED (engine.unknown / Vector) ON PURPOSE
|
| 870 |
+
(vendor :2993): pinning it would fork this 280-line function and re-home the softmax-exp and
|
| 871 |
+
dma_transpose source strings, which MUST keep their ``attention_inner`` sites stable and
|
| 872 |
+
match the shipping stream. The ACCUM FORK (reset on first tile, accumulate, drain once) and
|
| 873 |
+
the reduce_cmd discipline are preserved verbatim."""
|
| 874 |
+
# is_causal=False -> has_any_compute_pred is True.
|
| 875 |
+
q_seqlen_offset = grp_i * atp.sb_p
|
| 876 |
+
nisa.memset(bufs.exp_partial_sum[grp_i][...], value=0.0) # UNPINNED on purpose (:2993).
|
| 877 |
+
|
| 878 |
+
for large_tile_idx in range(atp.num_large_tiles_per_section):
|
| 879 |
+
kernel_assert(atp.exp_inst_elems == 512, "Internal validation failed.")
|
| 880 |
+
|
| 881 |
+
# ACCUM FORK pre-scan: is_causal=False and not prefix caching -> every tile emits, so
|
| 882 |
+
# emit_idxs is [0, 1, 2, 3]. reset on the first, accumulate, drain once.
|
| 883 |
+
emit_idxs = []
|
| 884 |
+
for scan_idx in range(atp.num_exp_insts_per_large_tile):
|
| 885 |
+
scan_prior, scan_seqlen_k, scan_k_start, _ = _get_kv_tile_apc(
|
| 886 |
+
ac.is_prefix_caching,
|
| 887 |
+
False,
|
| 888 |
+
True,
|
| 889 |
+
atp.seqlen_k_active_updated,
|
| 890 |
+
ac.seqlen_k_prior,
|
| 891 |
+
sp.section_offset + large_tile_idx * _LARGE_TILE_SZ + scan_idx * atp.exp_inst_elems,
|
| 892 |
+
None,
|
| 893 |
+
)
|
| 894 |
+
scan_mask = True # is_causal=False, use_swa=False
|
| 895 |
+
if scan_mask and scan_seqlen_k > scan_k_start:
|
| 896 |
+
emit_idxs.append(scan_idx)
|
| 897 |
+
first_emit = emit_idxs[0] if emit_idxs else -1
|
| 898 |
+
last_emit = emit_idxs[-1] if emit_idxs else -1
|
| 899 |
+
|
| 900 |
+
for exp_tile_idx in range(atp.num_exp_insts_per_large_tile):
|
| 901 |
+
is_prior_tile, seqlen_k, k_start_pos, _ = _get_kv_tile_apc(
|
| 902 |
+
ac.is_prefix_caching,
|
| 903 |
+
False,
|
| 904 |
+
True,
|
| 905 |
+
atp.seqlen_k_active_updated,
|
| 906 |
+
ac.seqlen_k_prior,
|
| 907 |
+
sp.section_offset + large_tile_idx * _LARGE_TILE_SZ + exp_tile_idx * atp.exp_inst_elems,
|
| 908 |
+
None,
|
| 909 |
+
)
|
| 910 |
+
num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
|
| 911 |
+
num_f = min(seqlen_k - k_start_pos, atp.exp_inst_elems)
|
| 912 |
+
|
| 913 |
+
exp_sel_mask = True # is_causal=False, use_swa=False
|
| 914 |
+
|
| 915 |
+
if exp_sel_mask and seqlen_k > k_start_pos:
|
| 916 |
+
# Step 1: exp. reduce_cmd is ALWAYS explicit (see the vendor note: an idle cmd
|
| 917 |
+
# with reduce_op/reduce_res would write a stale register).
|
| 918 |
+
if exp_tile_idx == first_emit:
|
| 919 |
+
exp_reduce_cmd = nisa.reduce_cmd.reset_reduce
|
| 920 |
+
else:
|
| 921 |
+
exp_reduce_cmd = nisa.reduce_cmd.reduce
|
| 922 |
+
if exp_tile_idx == last_emit:
|
| 923 |
+
exp_reduce_res = bufs.exp_partial_sum[grp_i][
|
| 924 |
+
:num_p,
|
| 925 |
+
large_tile_idx * atp.num_exp_insts_per_large_tile + exp_tile_idx,
|
| 926 |
+
]
|
| 927 |
+
else:
|
| 928 |
+
exp_reduce_res = None
|
| 929 |
+
nisa.activation(
|
| 930 |
+
bufs.exp_sb[grp_i][large_tile_idx][
|
| 931 |
+
:num_p, nl.ds(exp_tile_idx * atp.exp_inst_elems, num_f)
|
| 932 |
+
],
|
| 933 |
+
op=nl.exp,
|
| 934 |
+
data=bufs.mm1_masked[grp_i][large_tile_idx][
|
| 935 |
+
:num_p, nl.ds(exp_tile_idx * atp.exp_inst_elems, num_f)
|
| 936 |
+
],
|
| 937 |
+
reduce_op=nl.add,
|
| 938 |
+
reduce_cmd=exp_reduce_cmd,
|
| 939 |
+
reduce_res=exp_reduce_res,
|
| 940 |
+
bias=bufs.mm1_running_max[:num_p, grp_i],
|
| 941 |
+
)
|
| 942 |
+
|
| 943 |
+
# Step 2: P^T dma_transpose into exp_tp_sb ([128, 4, 128]).
|
| 944 |
+
num_f_outer = num_f // atp.sb_p
|
| 945 |
+
num_f_inner = num_f % atp.sb_p
|
| 946 |
+
|
| 947 |
+
if num_f_outer >= 1:
|
| 948 |
+
nisa.dma_transpose(
|
| 949 |
+
dst=bufs.exp_tp_sb[grp_i][large_tile_idx][exp_tile_idx].ap(
|
| 950 |
+
[
|
| 951 |
+
[atp.mm2_grp_sz, atp.sb_p],
|
| 952 |
+
[1, 1],
|
| 953 |
+
[atp.sb_p, num_f_outer],
|
| 954 |
+
[1, num_p],
|
| 955 |
+
]
|
| 956 |
+
),
|
| 957 |
+
src=bufs.exp_sb[grp_i][large_tile_idx].ap(
|
| 958 |
+
[
|
| 959 |
+
[_LARGE_TILE_SZ, num_p],
|
| 960 |
+
[1, 1],
|
| 961 |
+
[atp.sb_p, num_f_outer],
|
| 962 |
+
[1, atp.sb_p],
|
| 963 |
+
],
|
| 964 |
+
offset=exp_tile_idx * atp.mm2_grp_sz,
|
| 965 |
+
),
|
| 966 |
+
)
|
| 967 |
+
|
| 968 |
+
if num_f_inner > 0:
|
| 969 |
+
nisa.dma_transpose(
|
| 970 |
+
dst=bufs.exp_tp_sb[grp_i][large_tile_idx][exp_tile_idx].ap(
|
| 971 |
+
[
|
| 972 |
+
[atp.mm2_grp_sz, num_f_inner],
|
| 973 |
+
[1, 1],
|
| 974 |
+
[atp.sb_p, 1],
|
| 975 |
+
[1, num_p],
|
| 976 |
+
],
|
| 977 |
+
offset=num_f_outer * atp.sb_p,
|
| 978 |
+
),
|
| 979 |
+
src=bufs.exp_sb[grp_i][large_tile_idx].ap(
|
| 980 |
+
[
|
| 981 |
+
[_LARGE_TILE_SZ, num_p],
|
| 982 |
+
[1, 1],
|
| 983 |
+
[atp.sb_p, 1],
|
| 984 |
+
[1, num_f_inner],
|
| 985 |
+
],
|
| 986 |
+
offset=exp_tile_idx * atp.mm2_grp_sz + num_f_outer * atp.sb_p,
|
| 987 |
+
),
|
| 988 |
+
)
|
| 989 |
+
|
| 990 |
+
# sink block DEAD (sink=None).
|
| 991 |
+
|
| 992 |
+
|
| 993 |
+
def _pv_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams, bufs: AttnInternalBuffers):
|
| 994 |
+
"""MM2 (P@V) for a Q group. LIVE: is_causal=False -> always compute."""
|
| 995 |
+
# is_causal=False -> has_any_compute_pred is True.
|
| 996 |
+
nisa.memset(bufs.mm2_sb[grp_i][...], value=0.0)
|
| 997 |
+
|
| 998 |
+
for large_tile_idx in range(atp.num_large_tiles_per_section):
|
| 999 |
+
_pv_large_tile_impl(grp_i, large_tile_idx, ac, atp, sp, bufs)
|
| 1000 |
+
|
| 1001 |
+
|
| 1002 |
+
def _fused_qkmax_and_pv_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
|
| 1003 |
+
bufs: AttnInternalBuffers, batch_id: int = 0):
|
| 1004 |
+
"""Pipelined MM2 for grp_i while doing QK+max for grp_i+2. LIVE: is_causal=False -> both
|
| 1005 |
+
predicates True.
|
| 1006 |
+
|
| 1007 |
+
h10 EDIT (site :3294): the ``mm1_partial_max[qkmax_grp]`` init memset is pinned to GpSimd
|
| 1008 |
+
(same rationale as :2876; 2,688/core, the larger of the two)."""
|
| 1009 |
+
qkmax_grp = grp_i + 2
|
| 1010 |
+
# is_causal=False -> both has_any_compute predicates are True.
|
| 1011 |
+
nisa.memset(bufs.mm1_partial_max[qkmax_grp][...], value=_FLOAT32_MIN, engine=engine.gpsimd)
|
| 1012 |
+
|
| 1013 |
+
for large_tile_idx in range(atp.num_large_tiles_per_section):
|
| 1014 |
+
_pv_large_tile_impl(grp_i, large_tile_idx, ac, atp, sp, bufs)
|
| 1015 |
+
_qk_and_max_large_tile_impl(qkmax_grp, large_tile_idx, ac, atp, sp, bufs, batch_id)
|
| 1016 |
+
|
| 1017 |
+
|
| 1018 |
+
def _write_back_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams,
|
| 1019 |
+
bufs: AttnInternalBuffers, o, batch_id):
|
| 1020 |
+
"""Finalise output. LIVE: is_causal=False (always compute), num_sections==1,
|
| 1021 |
+
skip_output_normalization=False -> single-section reciprocal + scale_reciprocal_write_back.
|
| 1022 |
+
The flash multi-section accumulation and the skip-normalization branches are DEAD."""
|
| 1023 |
+
# is_causal=False -> has_any_compute_pred is True.
|
| 1024 |
+
q_seqlen_offset = grp_i * atp.sb_p
|
| 1025 |
+
nisa.tensor_reduce(bufs.exp_section_sum[grp_i][...], nl.add, bufs.exp_partial_sum[grp_i], axis=1)
|
| 1026 |
+
# num_sections==1, not skip_output_normalization.
|
| 1027 |
+
nisa.reciprocal(bufs.exp_sum_reciprocal[:, grp_i], bufs.exp_section_sum[grp_i])
|
| 1028 |
+
|
| 1029 |
+
num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
|
| 1030 |
+
num_f = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
|
| 1031 |
+
|
| 1032 |
+
# Single section: scale by reciprocal and write current output.
|
| 1033 |
+
_scale_reciprocal_write_back_impl(bufs.mm2_sb[grp_i], grp_i, ac, atp, bufs, o, batch_id, num_p, num_f)
|
| 1034 |
+
|
| 1035 |
+
|
| 1036 |
+
def _scale_reciprocal_write_back_impl(src_buf, grp_i, ac: AttnConfig, atp: AttnTileParams,
|
| 1037 |
+
bufs: AttnInternalBuffers, o, batch_id, num_p, num_f):
|
| 1038 |
+
"""Reciprocal-normalise and write back (final section). LIVE: tp_out=True.
|
| 1039 |
+
|
| 1040 |
+
h8 EDIT (site 1): the tp_out PSUM->SBUF evict ``tensor_copy`` is pinned to Scalar -- the
|
| 1041 |
+
un-pinned copy defaults to Vector (K2's binding engine); GpSimd is illegal (PSUM source), so
|
| 1042 |
+
Scalar is the only shedding target and is the exercised majority path. The tp_out=False
|
| 1043 |
+
``tensor_scalar(..., engine=engine.vector)`` branch is DEAD (tp_out always True) and removed."""
|
| 1044 |
+
# tp_out=True.
|
| 1045 |
+
nisa.nc_transpose(
|
| 1046 |
+
bufs.tp_exp_sum_reciprocal_psum[grp_i].ap(pattern=[[atp.sb_p, ac.d], [1, atp.sb_p]], offset=0),
|
| 1047 |
+
bufs.exp_sum_reciprocal.ap(pattern=[[atp.num_grps, atp.sb_p], [0, ac.d]], offset=grp_i),
|
| 1048 |
+
)
|
| 1049 |
+
# ---- h8 SITE 1: PSUM->SBUF evict pinned to Scalar. ----
|
| 1050 |
+
nisa.tensor_copy(
|
| 1051 |
+
bufs.tp_exp_sum_reciprocal_sb[grp_i][: ac.d, :num_f],
|
| 1052 |
+
bufs.tp_exp_sum_reciprocal_psum[grp_i][: ac.d, :num_f],
|
| 1053 |
+
engine=engine.scalar,
|
| 1054 |
+
)
|
| 1055 |
+
nisa.tensor_tensor(
|
| 1056 |
+
bufs.mm2_final[grp_i][: ac.d, :num_f],
|
| 1057 |
+
src_buf[: ac.d, :num_f],
|
| 1058 |
+
bufs.tp_exp_sum_reciprocal_sb[grp_i][: ac.d, :num_f],
|
| 1059 |
+
nl.multiply,
|
| 1060 |
+
)
|
| 1061 |
+
|
| 1062 |
+
_write_back_o_impl(bufs.mm2_final[grp_i], grp_i, ac, atp, o, batch_id, num_p, num_f)
|
| 1063 |
+
|
| 1064 |
+
|
| 1065 |
+
def _write_back_o_impl(src_buf, grp_i, ac: AttnConfig, atp: AttnTileParams, o, batch_id, num_p, num_f):
|
| 1066 |
+
"""Evict finished O. LIVE: ``o`` is a LIST of caller-owned SBUF slices, tp_out=True, so the
|
| 1067 |
+
output is copied into the caller's O tile (kept on-chip for the fused out-projection) rather
|
| 1068 |
+
than DMA'd to HBM. The HBM-sink branch is DEAD (o is always a list) and removed.
|
| 1069 |
+
|
| 1070 |
+
h8 EDIT (site 3): the tp_out list-branch ``tensor_copy`` (SBUF->SBUF fp32->bf16 CAST) is
|
| 1071 |
+
pinned to GpSimd -- all three engines are legal and GpSimd is the idle engine, so it sheds
|
| 1072 |
+
off Vector."""
|
| 1073 |
+
# o is a list (caller-owned SBUF slices), tp_out=True.
|
| 1074 |
+
# ---- h8 SITE 3: SBUF->SBUF CAST pinned to GpSimd. ----
|
| 1075 |
+
nisa.tensor_copy(dst=o[grp_i][: ac.d, :num_f], src=src_buf[: ac.d, :num_f], engine=engine.gpsimd)
|
| 1076 |
+
|
| 1077 |
+
|
| 1078 |
+
def _qk_and_max_large_tile_impl(qkmax_grp, large_tile_idx, ac: AttnConfig, atp: AttnTileParams,
|
| 1079 |
+
sp: SectionParams, bufs: AttnInternalBuffers, batch_id: int = 0):
|
| 1080 |
+
"""MM1 (QK^T) + masking for a Q group and a 2048 K tile. LIVE: is_causal=False,
|
| 1081 |
+
dynamic_sel_mask=False, is_prior_tile=False -> the FINAL ``else: # no masking``
|
| 1082 |
+
``tensor_scalar_reduce`` branch. The diagonal (affine_select) and range_select branches are
|
| 1083 |
+
DEAD and removed; SBUF-prefix-caching K-tile selection is DEAD (k_sb_prior is None)."""
|
| 1084 |
+
q_seqlen_offset = qkmax_grp * atp.sb_p
|
| 1085 |
+
|
| 1086 |
+
num_k_tiles_in_large_tile = _LARGE_TILE_SZ // _K_TILE_SZ
|
| 1087 |
+
for k_tile_idx in range(num_k_tiles_in_large_tile):
|
| 1088 |
+
mm1_psum_tile = bufs.mm1_psum[qkmax_grp][large_tile_idx][k_tile_idx]
|
| 1089 |
+
# dynamic_sel_mask=False, but the no-mask branch below reads neither mm1_copy_sb nor
|
| 1090 |
+
# mm1_affine_select_output, so their per-tile locals are not needed.
|
| 1091 |
+
mm1_masked_tile = bufs.mm1_masked[qkmax_grp][large_tile_idx]
|
| 1092 |
+
mm1_partial_max_tile = bufs.mm1_partial_max[qkmax_grp]
|
| 1093 |
+
|
| 1094 |
+
k_tile_idx_in_section = large_tile_idx * num_k_tiles_in_large_tile + k_tile_idx
|
| 1095 |
+
_kv_sec_idx = sp.kv_section_idx if sp.kv_section_idx is not None else sp.section_idx
|
| 1096 |
+
k_tile_idx_global = atp.num_k_tiles_per_section * _kv_sec_idx + k_tile_idx_in_section
|
| 1097 |
+
is_prior_tile, seqlen_k, k_start_pos, _ = _get_kv_tile_apc(
|
| 1098 |
+
ac.is_prefix_caching,
|
| 1099 |
+
False,
|
| 1100 |
+
True,
|
| 1101 |
+
atp.seqlen_k_active_updated,
|
| 1102 |
+
ac.seqlen_k_prior,
|
| 1103 |
+
k_tile_idx_global * _K_TILE_SZ,
|
| 1104 |
+
None,
|
| 1105 |
+
)
|
| 1106 |
+
|
| 1107 |
+
matmul_selection = True # is_causal=False
|
| 1108 |
+
|
| 1109 |
+
if q_seqlen_offset >= ac.seqlen_q or k_start_pos >= seqlen_k:
|
| 1110 |
+
matmul_selection = False
|
| 1111 |
+
|
| 1112 |
+
if matmul_selection and k_tile_idx_in_section < atp.num_k_tiles_per_section:
|
| 1113 |
+
num_f = min(seqlen_k - k_start_pos, _K_TILE_SZ)
|
| 1114 |
+
num_q_free = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
|
| 1115 |
+
|
| 1116 |
+
# Step 1: MM1 matmul. Non-APC path -> use k_tile_idx_in_section.
|
| 1117 |
+
k_tile_to_use = bufs.k_sb[k_tile_idx_in_section]
|
| 1118 |
+
|
| 1119 |
+
nisa.nc_matmul(
|
| 1120 |
+
mm1_psum_tile[:num_q_free, :num_f],
|
| 1121 |
+
bufs.q_sb[qkmax_grp // atp.num_q_grps_per_load][
|
| 1122 |
+
: ac.d,
|
| 1123 |
+
nl.ds((qkmax_grp % atp.num_q_grps_per_load) * _Q_GRP_SZ, num_q_free),
|
| 1124 |
+
],
|
| 1125 |
+
k_tile_to_use[:, :num_f],
|
| 1126 |
+
)
|
| 1127 |
+
|
| 1128 |
+
# Step 2: no masking (is_causal=False, dynamic_sel_mask=False).
|
| 1129 |
+
num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
|
| 1130 |
+
num_f = min(seqlen_k - k_start_pos, _K_TILE_SZ)
|
| 1131 |
+
|
| 1132 |
+
nisa.tensor_scalar_reduce(
|
| 1133 |
+
mm1_masked_tile[:num_p, nl.ds(k_tile_idx * _K_TILE_SZ, num_f)],
|
| 1134 |
+
data=mm1_psum_tile[:num_p, :num_f],
|
| 1135 |
+
op0=nl.multiply,
|
| 1136 |
+
operand0=ac.scale,
|
| 1137 |
+
reduce_op=nl.maximum,
|
| 1138 |
+
reduce_res=mm1_partial_max_tile[:num_p, k_tile_idx_in_section],
|
| 1139 |
+
)
|
| 1140 |
+
|
| 1141 |
+
|
| 1142 |
+
def _pv_large_tile_impl(pv_grp, large_tile_idx, ac: AttnConfig, atp: AttnTileParams,
|
| 1143 |
+
sp: SectionParams, bufs: AttnInternalBuffers):
|
| 1144 |
+
"""MM2 (P@V) for a Q group and 2048 V tile. LIVE: is_causal=False (all tiles selected),
|
| 1145 |
+
tp_out=True, single large tile so ``large_tile_idx==0`` -> the ``tensor_copy`` PSUM->SBUF
|
| 1146 |
+
reduction (never the accumulate ``tensor_tensor`` branch). SBUF-prefix V-tile selection is
|
| 1147 |
+
DEAD (v_sb_prior is None); the tp_out=False MM2 orientation is DEAD."""
|
| 1148 |
+
q_seqlen_offset = pv_grp * atp.sb_p
|
| 1149 |
+
num_mm2_grps_in_large_tile = _LARGE_TILE_SZ // atp.mm2_grp_sz
|
| 1150 |
+
mm2_psum_set = False
|
| 1151 |
+
|
| 1152 |
+
mm2_psum_tile = bufs.mm2_psum[pv_grp][large_tile_idx]
|
| 1153 |
+
|
| 1154 |
+
# Step 1: matmul + accumulate in PSUM per group.
|
| 1155 |
+
for mm2_grp_i in range(num_mm2_grps_in_large_tile):
|
| 1156 |
+
num_mm2_per_grp = atp.mm2_grp_sz // _V_TILE_SZ
|
| 1157 |
+
num_mm2_per_large_tile = num_mm2_per_grp * num_mm2_grps_in_large_tile
|
| 1158 |
+
|
| 1159 |
+
exp_tp_sb_tile = bufs.exp_tp_sb[pv_grp][large_tile_idx][mm2_grp_i]
|
| 1160 |
+
|
| 1161 |
+
is_prior_tile, seqlen_k, k_start_pos_512_tile, _ = _get_kv_tile_apc(
|
| 1162 |
+
ac.is_prefix_caching,
|
| 1163 |
+
False,
|
| 1164 |
+
True,
|
| 1165 |
+
atp.seqlen_k_active_updated,
|
| 1166 |
+
ac.seqlen_k_prior,
|
| 1167 |
+
sp.section_offset + large_tile_idx * _LARGE_TILE_SZ + mm2_grp_i * atp.mm2_grp_sz,
|
| 1168 |
+
None,
|
| 1169 |
+
)
|
| 1170 |
+
|
| 1171 |
+
for mm2_i in range(num_mm2_per_grp):
|
| 1172 |
+
v_tile_idx = large_tile_idx * num_mm2_per_large_tile + mm2_grp_i * num_mm2_per_grp + mm2_i
|
| 1173 |
+
k_start_pos = k_start_pos_512_tile + mm2_i * _V_TILE_SZ
|
| 1174 |
+
|
| 1175 |
+
num_p = min(seqlen_k - k_start_pos, _V_TILE_SZ)
|
| 1176 |
+
num_f = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
|
| 1177 |
+
|
| 1178 |
+
mm2_sel_mask = True # is_causal=False
|
| 1179 |
+
|
| 1180 |
+
if mm2_sel_mask and v_tile_idx < atp.num_v_tiles_per_section and num_p > 0 and num_f > 0:
|
| 1181 |
+
mm2_psum_set = True
|
| 1182 |
+
|
| 1183 |
+
# Non-APC path -> use v_tile_idx.
|
| 1184 |
+
v_tile_to_use = bufs.v_sb[v_tile_idx]
|
| 1185 |
+
|
| 1186 |
+
# tp_out=True orientation.
|
| 1187 |
+
nisa.nc_matmul(
|
| 1188 |
+
mm2_psum_tile[: ac.d, :num_f],
|
| 1189 |
+
v_tile_to_use[:num_p, : ac.d],
|
| 1190 |
+
exp_tp_sb_tile[:num_p, nl.ds(mm2_i * _V_TILE_SZ, num_f)],
|
| 1191 |
+
)
|
| 1192 |
+
|
| 1193 |
+
# Step 2: reduce MM2 groups into the large tile. is_prior_tile=False, is_causal=False ->
|
| 1194 |
+
# loop_reduce_sel True; single large tile -> large_tile_idx==0 -> tensor_copy (never the
|
| 1195 |
+
# accumulate tensor_tensor branch).
|
| 1196 |
+
is_prior_tile, seqlen_k, k_start_pos, _ = _get_kv_tile_apc(
|
| 1197 |
+
ac.is_prefix_caching,
|
| 1198 |
+
False,
|
| 1199 |
+
True,
|
| 1200 |
+
atp.seqlen_k_active_updated,
|
| 1201 |
+
ac.seqlen_k_prior,
|
| 1202 |
+
sp.section_offset + large_tile_idx * _LARGE_TILE_SZ,
|
| 1203 |
+
None,
|
| 1204 |
+
)
|
| 1205 |
+
k_seqlen_mask = k_start_pos < seqlen_k
|
| 1206 |
+
|
| 1207 |
+
loop_reduce_sel = True # is_causal=False
|
| 1208 |
+
|
| 1209 |
+
if loop_reduce_sel and k_seqlen_mask and mm2_psum_set:
|
| 1210 |
+
# tp_out=True.
|
| 1211 |
+
num_f = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ)
|
| 1212 |
+
if large_tile_idx == 0:
|
| 1213 |
+
nisa.tensor_copy(bufs.mm2_sb[pv_grp][:, :num_f], mm2_psum_tile[:, :num_f])
|
| 1214 |
+
else:
|
| 1215 |
+
nisa.tensor_tensor(
|
| 1216 |
+
bufs.mm2_sb[pv_grp][:, :num_f],
|
| 1217 |
+
bufs.mm2_sb[pv_grp][:, :num_f],
|
| 1218 |
+
mm2_psum_tile[:, :num_f],
|
| 1219 |
+
nl.add,
|
| 1220 |
+
)
|
build/torch-neuron/nki_kernels/mlp.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""K4 -- fused MLP (fc1 + gelu + fc2), x-prestage ("XP") in prologue mode.
|
| 2 |
+
|
| 3 |
+
WHAT IT COMPUTES
|
| 4 |
+
----------------
|
| 5 |
+
``out = gelu(x @ w1.T + b1) @ w2.T + b2``, with the rows sharded across LNC cores and
|
| 6 |
+
the weights pre-transposed to bf16. Returns ``[R, K]`` bf16.
|
| 7 |
+
|
| 8 |
+
WHY IT IS SHAPED THIS WAY
|
| 9 |
+
-------------------------
|
| 10 |
+
This is the effective shipping kernel: it is the ``b1pre`` MLP (fc1 bias staged in the
|
| 11 |
+
prologue so every gelu's bias is resident from t=0) with the x-prestage applied. Two
|
| 12 |
+
design choices carry the performance:
|
| 13 |
+
|
| 14 |
+
* b1 first: the 32 per-block ``b1`` bias tiles are DMA'd before the 16 MB of weight
|
| 15 |
+
staging, so they land within ~1 us of kernel start and no gelu ever waits on its bias.
|
| 16 |
+
* x prestaged in the prologue: BOTH row blocks' ``x`` load + dma_transpose staging is
|
| 17 |
+
issued before the ``rb`` loop, in row-block order. rb=0's staging already hid in the
|
| 18 |
+
pre-K4 seam (where the Tensor engine is dark for ~148 us because K1/K3 emit no Tensor
|
| 19 |
+
instructions); prestaging rb=1 gives it the same cover, filling the ~26.6 us Tensor
|
| 20 |
+
idle window that used to sit at the rb0->rb1 boundary. Appending rb=1's stages after
|
| 21 |
+
rb=0's, in order, means rb=0's readiness cannot regress -- the first matmul cannot be
|
| 22 |
+
delayed.
|
| 23 |
+
|
| 24 |
+
The x staging is emitted through a single module-level function (``_stage_xt``) rather
|
| 25 |
+
than inlined per row block, so the ``x`` load and the dma_transpose each keep exactly one
|
| 26 |
+
source site. The two-row-block staging is preserved exactly: ``xt_sets`` holds one
|
| 27 |
+
transposed-tile set per row block, and the loop consumes ``xt_sets[rb]``.
|
| 28 |
+
|
| 29 |
+
PSUM: fc1 uses 1 bank, fc2 uses up to 4 banks; the ``y1_psum`` / ``out_psums`` tiles are
|
| 30 |
+
declared inside their loops so the ring never accumulates onto a stale PSUM value. The
|
| 31 |
+
b2 bias broadcast is kept (removing it regressed the sibling kernel by 23-30%).
|
| 32 |
+
|
| 33 |
+
There are no ``engine=`` pins anywhere in this kernel. INFERENCE ONLY: no autograd rule.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
from __future__ import annotations
|
| 37 |
+
|
| 38 |
+
import math
|
| 39 |
+
|
| 40 |
+
import torch
|
| 41 |
+
from torch_neuronx import nki_op
|
| 42 |
+
|
| 43 |
+
import nki
|
| 44 |
+
import nki.isa as nisa
|
| 45 |
+
import nki.language as nl
|
| 46 |
+
|
| 47 |
+
#: PE array partition limit, and the moving-operand free-axis limit.
|
| 48 |
+
_P_MAX = 128
|
| 49 |
+
_F_MOV_MAX = 512
|
| 50 |
+
|
| 51 |
+
_BF16 = nl.bfloat16
|
| 52 |
+
|
| 53 |
+
#: Logical-to-physical NeuronCore degree. Must match the shipping shard's.
|
| 54 |
+
import os as _os
|
| 55 |
+
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 56 |
+
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
| 57 |
+
|
| 58 |
+
_INV_SQRT2 = 1.0 / math.sqrt(2.0)
|
| 59 |
+
_INV_SQRT_2PI = 1.0 / math.sqrt(2.0 * math.pi)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _div_ceil(n: int, d: int) -> int:
|
| 63 |
+
"""Ceiling division (never write (n + d - 1) // d inline)."""
|
| 64 |
+
return (n + d - 1) // d
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _kernel_assert(condition: bool, error_text: str):
|
| 68 |
+
"""Assert with an NKI-formatted error message."""
|
| 69 |
+
assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _stage_xt(x_hbm, r_abs, pr, n_k, K, rb_size):
|
| 73 |
+
"""Emit one row block's x staging and return its ``n_k`` transposed bf16 tiles.
|
| 74 |
+
|
| 75 |
+
``n_rs`` loads of ``[<=128, K]`` bf16, each followed by ``n_k`` DMA transposes into
|
| 76 |
+
the row block's ``xt_tiles``. Kept as a module-level emitter so the ``x`` load and
|
| 77 |
+
the dma_transpose each map to a single source site regardless of where it is called.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
x_hbm: [R, K] @ HBM bf16, the MLP input.
|
| 81 |
+
r_abs: absolute starting row of this row block.
|
| 82 |
+
pr: number of rows in this row block (<= rb_size).
|
| 83 |
+
n_k: number of K tiles (fc1 contraction dimension tiles).
|
| 84 |
+
K: hidden-in dimension.
|
| 85 |
+
rb_size: row-block width (free axis of the transposed tiles).
|
| 86 |
+
|
| 87 |
+
Returns:
|
| 88 |
+
list of ``n_k`` transposed ``[128, rb_size]`` bf16 SBUF tiles.
|
| 89 |
+
"""
|
| 90 |
+
n_rs = _div_ceil(pr, _P_MAX)
|
| 91 |
+
xt_tiles = []
|
| 92 |
+
for _ in range(n_k):
|
| 93 |
+
xt_tiles.append(nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf))
|
| 94 |
+
for rs in range(n_rs):
|
| 95 |
+
rr0 = rs * _P_MAX
|
| 96 |
+
prs = min(_P_MAX, pr - rr0)
|
| 97 |
+
x_b16 = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
|
| 98 |
+
nisa.dma_copy(
|
| 99 |
+
dst=x_b16[0:prs, 0:K],
|
| 100 |
+
src=x_hbm[r_abs + rr0:r_abs + rr0 + prs, 0:K],
|
| 101 |
+
)
|
| 102 |
+
for kt in range(n_k):
|
| 103 |
+
k0 = kt * _P_MAX
|
| 104 |
+
pk = min(_P_MAX, K - k0)
|
| 105 |
+
nisa.dma_transpose(dst=xt_tiles[kt][0:pk, rr0:rr0 + prs],
|
| 106 |
+
src=x_b16[0:prs, k0:k0 + pk])
|
| 107 |
+
return xt_tiles
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
@nki.jit
|
| 111 |
+
def _mlp_k4_xp_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm):
|
| 112 |
+
"""gelu(x @ w1.T + b1) @ w2.T + b2, rows sharded, b1 AND both x blocks resident early.
|
| 113 |
+
|
| 114 |
+
Args:
|
| 115 |
+
x_hbm (nl.ndarray): [R, K] @ HBM, **bfloat16** (kernel 1 emits bf16).
|
| 116 |
+
w1t_hbm (nl.ndarray): [K, H] @ HBM, **bf16**, == fc1.weight.T.
|
| 117 |
+
b1_hbm (nl.ndarray): [H] @ HBM, float32.
|
| 118 |
+
w2t_hbm (nl.ndarray): [H, K] @ HBM, **bf16**, == fc2.weight.T.
|
| 119 |
+
b2_hbm (nl.ndarray): [K] @ HBM, float32.
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
nl.ndarray: [R, K] @ HBM, **bfloat16**.
|
| 123 |
+
"""
|
| 124 |
+
R, K = x_hbm.shape
|
| 125 |
+
K_w1t, H = w1t_hbm.shape
|
| 126 |
+
H_w2t, K_w2t = w2t_hbm.shape
|
| 127 |
+
_kernel_assert(R >= 1 and K >= 1 and H >= 1, f"R({R}) K({K}) H({H}) must be >= 1")
|
| 128 |
+
_kernel_assert(K_w1t == K, f"w1t in-dim {K_w1t} != K {K}; expected w1t = w1.T of shape [K, H]")
|
| 129 |
+
_kernel_assert(K_w2t == K, f"w2t out-dim {K_w2t} != K {K}; expected w2t = w2.T of shape [H, K]")
|
| 130 |
+
_kernel_assert(H_w2t == H, f"w2t in-dim {H_w2t} != H {H}")
|
| 131 |
+
_kernel_assert(
|
| 132 |
+
R % LNC == 0,
|
| 133 |
+
f"the {LNC}-core kernel needs R divisible by {LNC}, got R={R}. "
|
| 134 |
+
f"program_id is a symbolic value, so a partial final row block cannot be "
|
| 135 |
+
f"clamped; use the one-core pre-transposed kernel for uneven R",
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
rows_per = R // LNC
|
| 139 |
+
pid = nl.program_id(0)
|
| 140 |
+
r_base = pid * rows_per
|
| 141 |
+
|
| 142 |
+
out_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
|
| 143 |
+
|
| 144 |
+
n_k = _div_ceil(K, _P_MAX) # K tiles (fc1 contraction)
|
| 145 |
+
n_h = _div_ceil(H, _P_MAX) # H tiles (fc1 out-rows / fc2 contraction)
|
| 146 |
+
|
| 147 |
+
# b1 FIRST, before the 16 MB of weight DMA: 32 [128,1] tiles issue at t=0 and land in
|
| 148 |
+
# ~1 us, so every gelu's bias is resident long before the first matmul retires.
|
| 149 |
+
b1_tiles = []
|
| 150 |
+
for ht in range(n_h):
|
| 151 |
+
h0 = ht * _P_MAX
|
| 152 |
+
ph = min(_P_MAX, H - h0)
|
| 153 |
+
t = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 154 |
+
nisa.dma_copy(dst=t[0:ph, 0:1], src=b1_hbm[h0:h0 + ph])
|
| 155 |
+
b1_tiles.append(t)
|
| 156 |
+
|
| 157 |
+
# Weight staging (pre-transposed bf16, one DMA per tile).
|
| 158 |
+
w1t_tiles = []
|
| 159 |
+
for kt in range(n_k):
|
| 160 |
+
k0 = kt * _P_MAX
|
| 161 |
+
pk = min(_P_MAX, K - k0)
|
| 162 |
+
t = nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf)
|
| 163 |
+
nisa.dma_copy(dst=t[0:pk, :], src=w1t_hbm[k0:k0 + pk, :])
|
| 164 |
+
w1t_tiles.append(t)
|
| 165 |
+
|
| 166 |
+
w2t_tiles = []
|
| 167 |
+
for ht in range(n_h):
|
| 168 |
+
h0 = ht * _P_MAX
|
| 169 |
+
ph = min(_P_MAX, H - h0)
|
| 170 |
+
t = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
|
| 171 |
+
nisa.dma_copy(dst=t[0:ph, :], src=w2t_hbm[h0:h0 + ph, :])
|
| 172 |
+
w2t_tiles.append(t)
|
| 173 |
+
|
| 174 |
+
# Main fused loop.
|
| 175 |
+
rb_size = _F_MOV_MAX
|
| 176 |
+
n_rb = _div_ceil(rows_per, rb_size)
|
| 177 |
+
n_ko = _div_ceil(K, _F_MOV_MAX)
|
| 178 |
+
|
| 179 |
+
# THE X-PRESTAGE: stage EVERY row block's x here, in rb order, so rb=1's serialised
|
| 180 |
+
# DMA chain runs inside the pre-K4 seam (Tensor dark) instead of inside the idle
|
| 181 |
+
# window at the rb0->rb1 boundary. Appending rb=1's stages after rb=0's leaves rb=0's
|
| 182 |
+
# stages exactly where they are on the Sync engine's sub-stream.
|
| 183 |
+
xt_sets = []
|
| 184 |
+
for rb_pre in range(n_rb):
|
| 185 |
+
xt_sets.append(_stage_xt(
|
| 186 |
+
x_hbm, r_base + rb_pre * rb_size,
|
| 187 |
+
min(rb_size, rows_per - rb_pre * rb_size), n_k, K, rb_size))
|
| 188 |
+
|
| 189 |
+
for rb in range(n_rb):
|
| 190 |
+
r_local = rb * rb_size
|
| 191 |
+
pr = min(rb_size, rows_per - r_local)
|
| 192 |
+
n_rs = _div_ceil(pr, _P_MAX)
|
| 193 |
+
r_abs = r_base + r_local
|
| 194 |
+
|
| 195 |
+
# x_T bf16 tiles: consume the prologue-staged set for this row block.
|
| 196 |
+
xt_tiles = xt_sets[rb]
|
| 197 |
+
|
| 198 |
+
# ---- fc1: gt_tiles[ht] = gelu(w1 @ x.T + b1), bf16, SBUF-resident ----
|
| 199 |
+
gt_tiles = []
|
| 200 |
+
for ht in range(n_h):
|
| 201 |
+
h0 = ht * _P_MAX
|
| 202 |
+
ph = min(_P_MAX, H - h0)
|
| 203 |
+
# Declared INSIDE the ht loop, so the ring never reuses a written PSUM tile.
|
| 204 |
+
# nc_matmul's default accumulate=None auto-detects "previously written" and
|
| 205 |
+
# would accumulate onto a stale value.
|
| 206 |
+
y1_psum = nl.ndarray((_P_MAX, rb_size), dtype=nl.float32, buffer=nl.psum)
|
| 207 |
+
for kt in nl.affine_range(n_k):
|
| 208 |
+
k0 = kt * _P_MAX
|
| 209 |
+
pk = min(_P_MAX, K - k0)
|
| 210 |
+
nisa.nc_matmul(
|
| 211 |
+
dst=y1_psum[0:ph, 0:pr],
|
| 212 |
+
stationary=w1t_tiles[kt][0:pk, h0:h0 + ph],
|
| 213 |
+
moving=xt_tiles[kt][0:pk, 0:pr],
|
| 214 |
+
)
|
| 215 |
+
# b1 comes from the prologue-resident tile.
|
| 216 |
+
gt = nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf)
|
| 217 |
+
nisa.activation(dst=gt[0:ph, 0:pr], op=nl.gelu,
|
| 218 |
+
data=y1_psum[0:ph, 0:pr], bias=b1_tiles[ht][0:ph, 0:1])
|
| 219 |
+
gt_tiles.append(gt)
|
| 220 |
+
|
| 221 |
+
# ---- fc2: out[r,k] = sum_h g[r,h] * w2[k,h] + b2[k] ----
|
| 222 |
+
for ko in range(n_ko):
|
| 223 |
+
ko0 = ko * _F_MOV_MAX
|
| 224 |
+
nk = min(_F_MOV_MAX, K - ko0)
|
| 225 |
+
out_psums = []
|
| 226 |
+
for _ in range(n_rs):
|
| 227 |
+
out_psums.append(
|
| 228 |
+
nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
|
| 229 |
+
)
|
| 230 |
+
for ht in nl.affine_range(n_h):
|
| 231 |
+
h0 = ht * _P_MAX
|
| 232 |
+
ph = min(_P_MAX, H - h0)
|
| 233 |
+
for rs in range(n_rs):
|
| 234 |
+
rr0 = rs * _P_MAX
|
| 235 |
+
prs = min(_P_MAX, pr - rr0)
|
| 236 |
+
nisa.nc_matmul(
|
| 237 |
+
dst=out_psums[rs][0:prs, 0:nk],
|
| 238 |
+
stationary=gt_tiles[ht][0:ph, rr0:rr0 + prs],
|
| 239 |
+
moving=w2t_tiles[ht][0:ph, ko0:ko0 + nk],
|
| 240 |
+
)
|
| 241 |
+
# b2 broadcast, hoisted out of the rs loop (removing the broadcast itself
|
| 242 |
+
# cost 23% and 30% in two separate attempts).
|
| 243 |
+
b2_bc = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.sbuf)
|
| 244 |
+
nisa.dma_copy(dst=b2_bc[0:_P_MAX, 0:nk],
|
| 245 |
+
src=b2_hbm.ap(pattern=[[0, _P_MAX], [1, nk]], offset=ko0))
|
| 246 |
+
for rs in range(n_rs):
|
| 247 |
+
rr0 = rs * _P_MAX
|
| 248 |
+
prs = min(_P_MAX, pr - rr0)
|
| 249 |
+
out_sb = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=_BF16, buffer=nl.sbuf)
|
| 250 |
+
nisa.tensor_tensor(dst=out_sb[0:prs, 0:nk],
|
| 251 |
+
data1=out_psums[rs][0:prs, 0:nk],
|
| 252 |
+
data2=b2_bc[0:prs, 0:nk],
|
| 253 |
+
op=nl.add)
|
| 254 |
+
nisa.dma_copy(
|
| 255 |
+
dst=out_hbm[r_abs + rr0:r_abs + rr0 + prs, ko0:ko0 + nk],
|
| 256 |
+
src=out_sb[0:prs, 0:nk],
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
return out_hbm
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def _no_grad(*tensors) -> None:
|
| 263 |
+
"""Refuse gradient-tracking inputs -- this op registers no autograd rule."""
|
| 264 |
+
for t in tensors:
|
| 265 |
+
if t is not None and t.requires_grad:
|
| 266 |
+
raise RuntimeError(
|
| 267 |
+
"nki_mlp_k4_xp registers no autograd rule (INFERENCE ONLY). "
|
| 268 |
+
"Use under torch.no_grad()/eval()."
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
@nki_op("vit_enc::nki_mlp_k4_xp", mutates_args={})
|
| 273 |
+
def nki_mlp_k4_xp(
|
| 274 |
+
x: torch.Tensor,
|
| 275 |
+
w1t: torch.Tensor,
|
| 276 |
+
b1: torch.Tensor,
|
| 277 |
+
w2t: torch.Tensor,
|
| 278 |
+
b2: torch.Tensor,
|
| 279 |
+
) -> torch.Tensor:
|
| 280 |
+
"""Fused MLP on LNC=2, pre-transposed bf16 weights, x prestaged in the prologue.
|
| 281 |
+
|
| 282 |
+
Args:
|
| 283 |
+
x : [R, K] **bf16** on 'neuron'. ``R`` must be divisible by :data:`LNC`.
|
| 284 |
+
w1t : [K, H] bf16 == fc1.weight.T. b1 : [H] float32.
|
| 285 |
+
w2t : [H, K] bf16 == fc2.weight.T. b2 : [K] float32.
|
| 286 |
+
|
| 287 |
+
Returns:
|
| 288 |
+
[R, K] bf16.
|
| 289 |
+
"""
|
| 290 |
+
_no_grad(x, w1t, b1, w2t, b2)
|
| 291 |
+
return _mlp_k4_xp_kernel[LNC](x, w1t, b1, w2t, b2)
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
#: Requested export alias. Same object as the incumbent-named nki_op, so the runtime
|
| 295 |
+
#: op-name string ("vit_enc::nki_mlp_k4_xp") is unchanged whichever name is used.
|
| 296 |
+
mlp = nki_mlp_k4_xp
|
build/torch-neuron/nki_kernels/mlp_res_ln.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""GELU MLP with the residual add + NEXT layer's affine-free LN1 in the epilogue.
|
| 2 |
+
|
| 3 |
+
Same fc1/gelu as ``mlp.py``. fc2 is reordered to (row subtile rs, out block ko) so each 128-row tile's
|
| 4 |
+
full [128, K] fc2 output lands in one fp32 SBUF row, then:
|
| 5 |
+
x_new = x + (g @ w2.T + b2) fp32 (MLP output never rounded to bf16)
|
| 6 |
+
z1 = LN_no_affine(x_new; eps) bf16 (same stat chain as res_layernorm.py)
|
| 7 |
+
Returns (x_new [R,K] fp32, z1 [R,K] bf16). Replaces K4 + the next layer's K1.
|
| 8 |
+
|
| 9 |
+
--- original mlp.py notes ---
|
| 10 |
+
K4 -- fused MLP (fc1 + gelu + fc2), x-prestage ("XP") in prologue mode.
|
| 11 |
+
|
| 12 |
+
WHAT IT COMPUTES
|
| 13 |
+
----------------
|
| 14 |
+
``out = gelu(x @ w1.T + b1) @ w2.T + b2``, with the rows sharded across LNC cores and
|
| 15 |
+
the weights pre-transposed to bf16. Returns ``[R, K]`` bf16.
|
| 16 |
+
|
| 17 |
+
WHY IT IS SHAPED THIS WAY
|
| 18 |
+
-------------------------
|
| 19 |
+
This is the effective shipping kernel: it is the ``b1pre`` MLP (fc1 bias staged in the
|
| 20 |
+
prologue so every gelu's bias is resident from t=0) with the x-prestage applied. Two
|
| 21 |
+
design choices carry the performance:
|
| 22 |
+
|
| 23 |
+
* b1 first: the 32 per-block ``b1`` bias tiles are DMA'd before the 16 MB of weight
|
| 24 |
+
staging, so they land within ~1 us of kernel start and no gelu ever waits on its bias.
|
| 25 |
+
* x prestaged in the prologue: BOTH row blocks' ``x`` load + dma_transpose staging is
|
| 26 |
+
issued before the ``rb`` loop, in row-block order. rb=0's staging already hid in the
|
| 27 |
+
pre-K4 seam (where the Tensor engine is dark for ~148 us because K1/K3 emit no Tensor
|
| 28 |
+
instructions); prestaging rb=1 gives it the same cover, filling the ~26.6 us Tensor
|
| 29 |
+
idle window that used to sit at the rb0->rb1 boundary. Appending rb=1's stages after
|
| 30 |
+
rb=0's, in order, means rb=0's readiness cannot regress -- the first matmul cannot be
|
| 31 |
+
delayed.
|
| 32 |
+
|
| 33 |
+
The x staging is emitted through a single module-level function (``_stage_xt``) rather
|
| 34 |
+
than inlined per row block, so the ``x`` load and the dma_transpose each keep exactly one
|
| 35 |
+
source site. The two-row-block staging is preserved exactly: ``xt_sets`` holds one
|
| 36 |
+
transposed-tile set per row block, and the loop consumes ``xt_sets[rb]``.
|
| 37 |
+
|
| 38 |
+
PSUM: fc1 uses 1 bank, fc2 uses up to 4 banks; the ``y1_psum`` / ``out_psums`` tiles are
|
| 39 |
+
declared inside their loops so the ring never accumulates onto a stale PSUM value. The
|
| 40 |
+
b2 bias broadcast is kept (removing it regressed the sibling kernel by 23-30%).
|
| 41 |
+
|
| 42 |
+
There are no ``engine=`` pins anywhere in this kernel. INFERENCE ONLY: no autograd rule.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
from __future__ import annotations
|
| 46 |
+
|
| 47 |
+
import math
|
| 48 |
+
|
| 49 |
+
import torch
|
| 50 |
+
from torch_neuronx import nki_op
|
| 51 |
+
|
| 52 |
+
import nki
|
| 53 |
+
import nki.isa as nisa
|
| 54 |
+
import nki.language as nl
|
| 55 |
+
|
| 56 |
+
#: PE array partition limit, and the moving-operand free-axis limit.
|
| 57 |
+
_P_MAX = 128
|
| 58 |
+
_F_MOV_MAX = 512
|
| 59 |
+
|
| 60 |
+
_BF16 = nl.bfloat16
|
| 61 |
+
|
| 62 |
+
#: Logical-to-physical NeuronCore degree. Must match the shipping shard's.
|
| 63 |
+
import os as _os
|
| 64 |
+
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 65 |
+
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
| 66 |
+
|
| 67 |
+
_INV_SQRT2 = 1.0 / math.sqrt(2.0)
|
| 68 |
+
_INV_SQRT_2PI = 1.0 / math.sqrt(2.0 * math.pi)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _div_ceil(n: int, d: int) -> int:
|
| 72 |
+
"""Ceiling division (never write (n + d - 1) // d inline)."""
|
| 73 |
+
return (n + d - 1) // d
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _kernel_assert(condition: bool, error_text: str):
|
| 77 |
+
"""Assert with an NKI-formatted error message."""
|
| 78 |
+
assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _stage_xt(x_hbm, r_abs, pr, n_k, K, rb_size):
|
| 82 |
+
"""Emit one row block's x staging and return its ``n_k`` transposed bf16 tiles.
|
| 83 |
+
|
| 84 |
+
``n_rs`` loads of ``[<=128, K]`` bf16, each followed by ``n_k`` DMA transposes into
|
| 85 |
+
the row block's ``xt_tiles``. Kept as a module-level emitter so the ``x`` load and
|
| 86 |
+
the dma_transpose each map to a single source site regardless of where it is called.
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
x_hbm: [R, K] @ HBM bf16, the MLP input.
|
| 90 |
+
r_abs: absolute starting row of this row block.
|
| 91 |
+
pr: number of rows in this row block (<= rb_size).
|
| 92 |
+
n_k: number of K tiles (fc1 contraction dimension tiles).
|
| 93 |
+
K: hidden-in dimension.
|
| 94 |
+
rb_size: row-block width (free axis of the transposed tiles).
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
list of ``n_k`` transposed ``[128, rb_size]`` bf16 SBUF tiles.
|
| 98 |
+
"""
|
| 99 |
+
n_rs = _div_ceil(pr, _P_MAX)
|
| 100 |
+
xt_tiles = []
|
| 101 |
+
for _ in range(n_k):
|
| 102 |
+
xt_tiles.append(nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf))
|
| 103 |
+
for rs in range(n_rs):
|
| 104 |
+
rr0 = rs * _P_MAX
|
| 105 |
+
prs = min(_P_MAX, pr - rr0)
|
| 106 |
+
x_b16 = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
|
| 107 |
+
nisa.dma_copy(
|
| 108 |
+
dst=x_b16[0:prs, 0:K],
|
| 109 |
+
src=x_hbm[r_abs + rr0:r_abs + rr0 + prs, 0:K],
|
| 110 |
+
)
|
| 111 |
+
for kt in range(n_k):
|
| 112 |
+
k0 = kt * _P_MAX
|
| 113 |
+
pk = min(_P_MAX, K - k0)
|
| 114 |
+
nisa.dma_transpose(dst=xt_tiles[kt][0:pk, rr0:rr0 + prs],
|
| 115 |
+
src=x_b16[0:prs, k0:k0 + pk])
|
| 116 |
+
return xt_tiles
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@nki.jit
|
| 120 |
+
def _mlp_res_ln_kernel(x_hbm, w1t_hbm, b1_hbm, w2t_hbm, b2_hbm, a_hbm, eps):
|
| 121 |
+
"""gelu(x @ w1.T + b1) @ w2.T + b2, rows sharded, b1 AND both x blocks resident early.
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
x_hbm (nl.ndarray): [R, K] @ HBM, **bfloat16** (kernel 1 emits bf16).
|
| 125 |
+
w1t_hbm (nl.ndarray): [K, H] @ HBM, **bf16**, == fc1.weight.T.
|
| 126 |
+
b1_hbm (nl.ndarray): [H] @ HBM, float32.
|
| 127 |
+
w2t_hbm (nl.ndarray): [H, K] @ HBM, **bf16**, == fc2.weight.T.
|
| 128 |
+
b2_hbm (nl.ndarray): [K] @ HBM, float32.
|
| 129 |
+
|
| 130 |
+
Returns:
|
| 131 |
+
nl.ndarray: [R, K] @ HBM, **bfloat16**.
|
| 132 |
+
"""
|
| 133 |
+
R, K = x_hbm.shape
|
| 134 |
+
K_w1t, H = w1t_hbm.shape
|
| 135 |
+
H_w2t, K_w2t = w2t_hbm.shape
|
| 136 |
+
_kernel_assert(R >= 1 and K >= 1 and H >= 1, f"R({R}) K({K}) H({H}) must be >= 1")
|
| 137 |
+
_kernel_assert(K_w1t == K, f"w1t in-dim {K_w1t} != K {K}; expected w1t = w1.T of shape [K, H]")
|
| 138 |
+
_kernel_assert(K_w2t == K, f"w2t out-dim {K_w2t} != K {K}; expected w2t = w2.T of shape [H, K]")
|
| 139 |
+
_kernel_assert(H_w2t == H, f"w2t in-dim {H_w2t} != H {H}")
|
| 140 |
+
_kernel_assert(
|
| 141 |
+
R % LNC == 0,
|
| 142 |
+
f"the {LNC}-core kernel needs R divisible by {LNC}, got R={R}. "
|
| 143 |
+
f"program_id is a symbolic value, so a partial final row block cannot be "
|
| 144 |
+
f"clamped; use the one-core pre-transposed kernel for uneven R",
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
rows_per = R // LNC
|
| 148 |
+
pid = nl.program_id(0)
|
| 149 |
+
r_base = pid * rows_per
|
| 150 |
+
|
| 151 |
+
_kernel_assert(tuple(a_hbm.shape) == (R, K), "residual a must be [R, K] fp32")
|
| 152 |
+
xnew_hbm = nl.ndarray((R, K), dtype=nl.float32, buffer=nl.shared_hbm)
|
| 153 |
+
z_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
|
| 154 |
+
inv_k = 1.0 / float(K)
|
| 155 |
+
neg_inv_k = -inv_k
|
| 156 |
+
|
| 157 |
+
n_k = _div_ceil(K, _P_MAX) # K tiles (fc1 contraction)
|
| 158 |
+
n_h = _div_ceil(H, _P_MAX) # H tiles (fc1 out-rows / fc2 contraction)
|
| 159 |
+
|
| 160 |
+
# b1 FIRST, before the 16 MB of weight DMA: 32 [128,1] tiles issue at t=0 and land in
|
| 161 |
+
# ~1 us, so every gelu's bias is resident long before the first matmul retires.
|
| 162 |
+
b1_tiles = []
|
| 163 |
+
for ht in range(n_h):
|
| 164 |
+
h0 = ht * _P_MAX
|
| 165 |
+
ph = min(_P_MAX, H - h0)
|
| 166 |
+
t = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 167 |
+
nisa.dma_copy(dst=t[0:ph, 0:1], src=b1_hbm[h0:h0 + ph])
|
| 168 |
+
b1_tiles.append(t)
|
| 169 |
+
|
| 170 |
+
# Weight staging (pre-transposed bf16, one DMA per tile).
|
| 171 |
+
w1t_tiles = []
|
| 172 |
+
for kt in range(n_k):
|
| 173 |
+
k0 = kt * _P_MAX
|
| 174 |
+
pk = min(_P_MAX, K - k0)
|
| 175 |
+
t = nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf)
|
| 176 |
+
nisa.dma_copy(dst=t[0:pk, :], src=w1t_hbm[k0:k0 + pk, :])
|
| 177 |
+
w1t_tiles.append(t)
|
| 178 |
+
|
| 179 |
+
w2t_tiles = []
|
| 180 |
+
for ht in range(n_h):
|
| 181 |
+
h0 = ht * _P_MAX
|
| 182 |
+
ph = min(_P_MAX, H - h0)
|
| 183 |
+
t = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
|
| 184 |
+
nisa.dma_copy(dst=t[0:ph, :], src=w2t_hbm[h0:h0 + ph, :])
|
| 185 |
+
w2t_tiles.append(t)
|
| 186 |
+
|
| 187 |
+
# b2 broadcast ONCE over all K columns (fp32), plus LN per-lane scratch.
|
| 188 |
+
b2_bc = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf)
|
| 189 |
+
nisa.dma_copy(dst=b2_bc[0:_P_MAX, 0:K], src=b2_hbm.ap(pattern=[[0, _P_MAX], [1, K]], offset=0))
|
| 190 |
+
acc_s = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 191 |
+
neg_shift = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 192 |
+
neg_mu = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 193 |
+
var = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 194 |
+
rstd = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 195 |
+
|
| 196 |
+
# Main fused loop.
|
| 197 |
+
rb_size = _F_MOV_MAX
|
| 198 |
+
n_rb = _div_ceil(rows_per, rb_size)
|
| 199 |
+
n_ko = _div_ceil(K, _F_MOV_MAX)
|
| 200 |
+
|
| 201 |
+
# THE X-PRESTAGE: stage EVERY row block's x here, in rb order, so rb=1's serialised
|
| 202 |
+
# DMA chain runs inside the pre-K4 seam (Tensor dark) instead of inside the idle
|
| 203 |
+
# window at the rb0->rb1 boundary. Appending rb=1's stages after rb=0's leaves rb=0's
|
| 204 |
+
# stages exactly where they are on the Sync engine's sub-stream.
|
| 205 |
+
xt_sets = []
|
| 206 |
+
for rb_pre in range(n_rb):
|
| 207 |
+
xt_sets.append(_stage_xt(
|
| 208 |
+
x_hbm, r_base + rb_pre * rb_size,
|
| 209 |
+
min(rb_size, rows_per - rb_pre * rb_size), n_k, K, rb_size))
|
| 210 |
+
|
| 211 |
+
for rb in range(n_rb):
|
| 212 |
+
r_local = rb * rb_size
|
| 213 |
+
pr = min(rb_size, rows_per - r_local)
|
| 214 |
+
n_rs = _div_ceil(pr, _P_MAX)
|
| 215 |
+
r_abs = r_base + r_local
|
| 216 |
+
|
| 217 |
+
# x_T bf16 tiles: consume the prologue-staged set for this row block.
|
| 218 |
+
xt_tiles = xt_sets[rb]
|
| 219 |
+
|
| 220 |
+
# ---- fc1: gt_tiles[ht] = gelu(w1 @ x.T + b1), bf16, SBUF-resident ----
|
| 221 |
+
gt_tiles = []
|
| 222 |
+
for ht in range(n_h):
|
| 223 |
+
h0 = ht * _P_MAX
|
| 224 |
+
ph = min(_P_MAX, H - h0)
|
| 225 |
+
# Declared INSIDE the ht loop, so the ring never reuses a written PSUM tile.
|
| 226 |
+
# nc_matmul's default accumulate=None auto-detects "previously written" and
|
| 227 |
+
# would accumulate onto a stale value.
|
| 228 |
+
y1_psum = nl.ndarray((_P_MAX, rb_size), dtype=nl.float32, buffer=nl.psum)
|
| 229 |
+
for kt in nl.affine_range(n_k):
|
| 230 |
+
k0 = kt * _P_MAX
|
| 231 |
+
pk = min(_P_MAX, K - k0)
|
| 232 |
+
nisa.nc_matmul(
|
| 233 |
+
dst=y1_psum[0:ph, 0:pr],
|
| 234 |
+
stationary=w1t_tiles[kt][0:pk, h0:h0 + ph],
|
| 235 |
+
moving=xt_tiles[kt][0:pk, 0:pr],
|
| 236 |
+
)
|
| 237 |
+
# b1 comes from the prologue-resident tile.
|
| 238 |
+
gt = nl.ndarray((_P_MAX, rb_size), dtype=_BF16, buffer=nl.sbuf)
|
| 239 |
+
nisa.activation(dst=gt[0:ph, 0:pr], op=nl.gelu,
|
| 240 |
+
data=y1_psum[0:ph, 0:pr], bias=b1_tiles[ht][0:ph, 0:1])
|
| 241 |
+
gt_tiles.append(gt)
|
| 242 |
+
|
| 243 |
+
# ---- fc2 + b2 + residual -> x_new (fp32), LN -> z1 (bf16), per 128-row subtile ----
|
| 244 |
+
for rs in range(n_rs):
|
| 245 |
+
rr0 = rs * _P_MAX
|
| 246 |
+
prs = min(_P_MAX, pr - rr0)
|
| 247 |
+
r0 = r_abs + rr0
|
| 248 |
+
row = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf) # residual, accumulated in place
|
| 249 |
+
z_t = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
|
| 250 |
+
nisa.dma_copy(dst=row[0:prs, 0:K], src=a_hbm[r0:r0 + prs, 0:K])
|
| 251 |
+
for ko in range(n_ko):
|
| 252 |
+
ko0 = ko * _F_MOV_MAX
|
| 253 |
+
nk = min(_F_MOV_MAX, K - ko0)
|
| 254 |
+
ps = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
|
| 255 |
+
for ht in nl.affine_range(n_h):
|
| 256 |
+
h0 = ht * _P_MAX
|
| 257 |
+
ph = min(_P_MAX, H - h0)
|
| 258 |
+
nisa.nc_matmul(
|
| 259 |
+
dst=ps[0:prs, 0:nk],
|
| 260 |
+
stationary=gt_tiles[ht][0:ph, rr0:rr0 + prs],
|
| 261 |
+
moving=w2t_tiles[ht][0:ph, ko0:ko0 + nk],
|
| 262 |
+
)
|
| 263 |
+
nisa.tensor_tensor(dst=row[0:prs, ko0:ko0 + nk], data1=row[0:prs, ko0:ko0 + nk],
|
| 264 |
+
data2=ps[0:prs, 0:nk], op=nl.add)
|
| 265 |
+
nisa.tensor_tensor(dst=row[0:prs, 0:K], data1=row[0:prs, 0:K], data2=b2_bc[0:prs, 0:K], op=nl.add)
|
| 266 |
+
nisa.dma_copy(dst=xnew_hbm[r0:r0 + prs, 0:K], src=row[0:prs, 0:K])
|
| 267 |
+
# stat chain (identical ops/order to res_layernorm.py)
|
| 268 |
+
nisa.tensor_scalar(dst=neg_shift[0:prs, 0:1], data=row[0:prs, 0:1],
|
| 269 |
+
op0=nl.multiply, operand0=-1.0)
|
| 270 |
+
nisa.activation_reduce(dst=z_t[0:prs, 0:K], op=nl.copy, data=row[0:prs, 0:K],
|
| 271 |
+
reduce_op=nl.add, reduce_res=acc_s[0:prs, 0:1],
|
| 272 |
+
bias=neg_shift[0:prs, 0:1], scale=1.0)
|
| 273 |
+
nisa.tensor_scalar(dst=neg_mu[0:prs, 0:1], data=acc_s[0:prs, 0:1], op0=nl.multiply,
|
| 274 |
+
operand0=neg_inv_k, op1=nl.add, operand1=neg_shift[0:prs, 0:1])
|
| 275 |
+
nisa.activation_reduce(dst=z_t[0:prs, 0:K], op=nl.square, data=row[0:prs, 0:K],
|
| 276 |
+
reduce_op=nl.add, reduce_res=acc_s[0:prs, 0:1],
|
| 277 |
+
bias=neg_mu[0:prs, 0:1], scale=1.0)
|
| 278 |
+
nisa.tensor_scalar(dst=var[0:prs, 0:1], data=acc_s[0:prs, 0:1], op0=nl.multiply,
|
| 279 |
+
operand0=inv_k, op1=nl.add, operand1=eps)
|
| 280 |
+
nisa.activation(dst=rstd[0:prs, 0:1], op=nl.rsqrt, data=var[0:prs, 0:1])
|
| 281 |
+
nisa.tensor_scalar(dst=z_t[0:prs, 0:K], data=row[0:prs, 0:K],
|
| 282 |
+
op0=nl.add, operand0=neg_mu[0:prs, 0:1],
|
| 283 |
+
op1=nl.multiply, operand1=rstd[0:prs, 0:1])
|
| 284 |
+
nisa.dma_copy(dst=z_hbm[r0:r0 + prs, 0:K], src=z_t[0:prs, 0:K])
|
| 285 |
+
|
| 286 |
+
return xnew_hbm, z_hbm
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def _no_grad(*tensors) -> None:
|
| 290 |
+
for t in tensors:
|
| 291 |
+
if t is not None and t.requires_grad:
|
| 292 |
+
raise RuntimeError("nki_mlp_res_ln is inference-only; use torch.no_grad().")
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
@nki_op("vit_enc::nki_mlp_res_ln", mutates_args={})
|
| 296 |
+
def nki_mlp_res_ln(
|
| 297 |
+
x: torch.Tensor,
|
| 298 |
+
w1t: torch.Tensor,
|
| 299 |
+
b1: torch.Tensor,
|
| 300 |
+
w2t: torch.Tensor,
|
| 301 |
+
b2: torch.Tensor,
|
| 302 |
+
a: torch.Tensor,
|
| 303 |
+
eps: float,
|
| 304 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 305 |
+
"""MLP(x) + residual a, then affine-free LN. x [R,K] bf16, a [R,K] fp32.
|
| 306 |
+
Returns (x_new [R,K] fp32, z [R,K] bf16)."""
|
| 307 |
+
_no_grad(x, w1t, b1, w2t, b2, a)
|
| 308 |
+
return _mlp_res_ln_kernel[LNC](x, w1t, b1, w2t, b2, a, eps)
|
build/torch-neuron/nki_kernels/res_layernorm.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""K1/K3 -- fused residual add + affine-free LayerNorm (the "fold" kernel).
|
| 2 |
+
|
| 3 |
+
WHAT IT COMPUTES
|
| 4 |
+
----------------
|
| 5 |
+
Given a residual stream ``a`` [R, H] fp32 and a stack of 1..4 branch tensors
|
| 6 |
+
``b`` [B, R, H] bf16, per token row this kernel computes::
|
| 7 |
+
|
| 8 |
+
x_new = a + sum_j b[j] # fp32, the untouched residual stream
|
| 9 |
+
z = (x_new - mean(x_new)) * rstd # bf16, LayerNorm WITHOUT affine
|
| 10 |
+
|
| 11 |
+
and returns ``(x_new, z)``. gamma/beta are NOT applied here -- they have been folded
|
| 12 |
+
into the downstream consumers' packed weights at freeze time (K1's z feeds K2's Q/K/V
|
| 13 |
+
projections; K3's z feeds K4's fc1). The affine fold is exact:
|
| 14 |
+
``W @ (z*g + bt) + c == (W * g) @ z + (W @ bt + c)``, so scaling the consumer's input
|
| 15 |
+
columns by gamma and adding ``W @ bt`` to its bias reproduces the same math with the
|
| 16 |
+
affine removed from the norm kernel.
|
| 17 |
+
|
| 18 |
+
WHY IT IS SHAPED THIS WAY
|
| 19 |
+
-------------------------
|
| 20 |
+
* LNC = 2 shard: the kernel is launched on both NeuronCores; each owns ``R // 2`` rows
|
| 21 |
+
selected by ``program_id(0)``, so the two cores never touch the same tokens.
|
| 22 |
+
* Ring of depth 4 over the token tiles: four rotating sets of SBUF tiles let the DMA
|
| 23 |
+
loads of tile ``t+1`` overlap the compute of tile ``t`` without a write-after-read
|
| 24 |
+
hazard on the buffers.
|
| 25 |
+
* fp32 residual / bf16 branch: ``a`` (the residual stream) stays fp32 so ``x_new`` is
|
| 26 |
+
bitwise-invariant math and can be carried forward losslessly; the branch inputs and
|
| 27 |
+
the normalized output ``z`` are bf16, which is the precision boundary the downstream
|
| 28 |
+
kernels consume.
|
| 29 |
+
* Thinned stat chain: mean and variance are computed with a shift-and-reduce chain
|
| 30 |
+
(neg_shift -> copy-reduce -> neg_mu -> square-reduce -> var -> rsqrt) that keeps the
|
| 31 |
+
per-lane statistics in fp32; the final normalize is a single fused ``tensor_scalar``
|
| 32 |
+
(``z = (x_new + neg_mu) * rstd``) whose two per-lane operands cost the same as one op
|
| 33 |
+
and whose bf16 cast rides the write.
|
| 34 |
+
|
| 35 |
+
There are no ``engine=`` pins anywhere in this kernel; nothing engine-related to
|
| 36 |
+
preserve. INFERENCE ONLY: no autograd rule is registered.
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
from __future__ import annotations
|
| 40 |
+
|
| 41 |
+
import torch
|
| 42 |
+
from torch_neuronx import nki_op
|
| 43 |
+
|
| 44 |
+
import nki
|
| 45 |
+
import nki.isa as nisa
|
| 46 |
+
import nki.language as nl
|
| 47 |
+
|
| 48 |
+
#: SBUF partition count, and the token-tile height (rows processed per instruction).
|
| 49 |
+
_P_MAX = 128
|
| 50 |
+
|
| 51 |
+
#: The bf16 dtype used for branch inputs and the normalized output.
|
| 52 |
+
_BF16 = nl.bfloat16
|
| 53 |
+
|
| 54 |
+
#: Logical-to-physical NeuronCore degree. Rows are split evenly across LNC cores.
|
| 55 |
+
import os as _os
|
| 56 |
+
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 57 |
+
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
| 58 |
+
|
| 59 |
+
#: Ring depth: number of rotating SBUF tile sets that overlap load with compute.
|
| 60 |
+
_RING = 4
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _kernel_assert(condition: bool, error_text: str):
|
| 64 |
+
"""Assert with an NKI-formatted error message."""
|
| 65 |
+
assert condition, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {error_text}"
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@nki.jit
|
| 69 |
+
def _res_layernorm_fold_kernel(a_hbm, b_hbm, eps):
|
| 70 |
+
"""Emit the fold kernel body: ``(a + sum(b), LN_no_affine(a + sum(b)))``, token-major.
|
| 71 |
+
|
| 72 |
+
gamma/beta live in the consumers' packed weights, so this kernel takes no affine
|
| 73 |
+
parameters. Rows are sharded across ``LNC`` cores; each token tile is loaded through
|
| 74 |
+
a depth-4 ring, summed in fp32, normalized with the thinned stat chain, and the
|
| 75 |
+
normalized result is cast to bf16 on write.
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
a_hbm (nl.ndarray): [R, H] @ HBM float32 -- the residual stream.
|
| 79 |
+
b_hbm (nl.ndarray): [B, R, H] @ HBM bf16 -- B branch tensors (1 <= B <= 4), summed in.
|
| 80 |
+
eps (float): LayerNorm epsilon.
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
tuple: ``(x_new [R, H] float32, z [R, H] bf16)`` both in shared HBM.
|
| 84 |
+
"""
|
| 85 |
+
R, H = a_hbm.shape
|
| 86 |
+
n_b, Rb, Hb = b_hbm.shape
|
| 87 |
+
|
| 88 |
+
_kernel_assert(R >= 1 and H >= 1, f"R({R}) H({H}) must be >= 1")
|
| 89 |
+
_kernel_assert(Rb == R and Hb == H, f"b {(Rb, Hb)} must match a {(R, H)}")
|
| 90 |
+
_kernel_assert(1 <= n_b <= 4, f"b must stack 1..4 branches, got {n_b}")
|
| 91 |
+
_kernel_assert(
|
| 92 |
+
H <= 16384,
|
| 93 |
+
f"H={H} is one instruction's free axis here; no chunking is implemented.",
|
| 94 |
+
)
|
| 95 |
+
_kernel_assert(
|
| 96 |
+
R % (LNC * _P_MAX) == 0,
|
| 97 |
+
f"need R divisible by LNC*{_P_MAX} = {LNC * _P_MAX}, got R={R}",
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
rows_per = R // LNC
|
| 101 |
+
n_t = rows_per // _P_MAX
|
| 102 |
+
pid = nl.program_id(0)
|
| 103 |
+
r_base = pid * rows_per
|
| 104 |
+
|
| 105 |
+
x_new_hbm = nl.ndarray((R, H), dtype=nl.float32, buffer=nl.shared_hbm)
|
| 106 |
+
z_hbm = nl.ndarray((R, H), dtype=_BF16, buffer=nl.shared_hbm)
|
| 107 |
+
|
| 108 |
+
inv_h = 1.0 / float(H)
|
| 109 |
+
neg_inv_h = -inv_h
|
| 110 |
+
|
| 111 |
+
# Ring buffers: _RING rotating sets of (a tile, B branch tiles, sum tile, z tile).
|
| 112 |
+
a_sb = []
|
| 113 |
+
b_sb = []
|
| 114 |
+
s_sb = []
|
| 115 |
+
z_sb = []
|
| 116 |
+
for _i in range(_RING):
|
| 117 |
+
a_sb.append(nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf))
|
| 118 |
+
_slot = []
|
| 119 |
+
for _j in range(n_b):
|
| 120 |
+
_slot.append(nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf))
|
| 121 |
+
b_sb.append(_slot)
|
| 122 |
+
s_sb.append(nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf))
|
| 123 |
+
z_sb.append(nl.ndarray((_P_MAX, H), dtype=_BF16, buffer=nl.sbuf))
|
| 124 |
+
|
| 125 |
+
# Scratch destination for the reduce activations (its full tile is discarded; only
|
| 126 |
+
# the reduce result is consumed).
|
| 127 |
+
sink = nl.ndarray((_P_MAX, H), dtype=nl.float32, buffer=nl.sbuf)
|
| 128 |
+
|
| 129 |
+
# Per-lane [128, 1] statistics.
|
| 130 |
+
acc = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 131 |
+
neg_shift = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 132 |
+
neg_mu = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 133 |
+
var = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 134 |
+
rstd = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 135 |
+
|
| 136 |
+
for t in range(n_t):
|
| 137 |
+
k = t % _RING
|
| 138 |
+
r0 = r_base + t * _P_MAX
|
| 139 |
+
|
| 140 |
+
# ---- load residual + branches for this tile ----
|
| 141 |
+
nisa.dma_copy(dst=a_sb[k], src=a_hbm[r0:r0 + _P_MAX, 0:H])
|
| 142 |
+
for j in range(n_b):
|
| 143 |
+
nisa.dma_copy(dst=b_sb[k][j], src=b_hbm[j, r0:r0 + _P_MAX, 0:H])
|
| 144 |
+
|
| 145 |
+
# ---- x_new = a + sum_j b[j] in fp32, then write the residual stream out ----
|
| 146 |
+
nisa.tensor_tensor(dst=s_sb[k], data1=a_sb[k], data2=b_sb[k][0], op=nl.add)
|
| 147 |
+
for j in range(1, n_b):
|
| 148 |
+
nisa.tensor_tensor(dst=s_sb[k], data1=s_sb[k], data2=b_sb[k][j], op=nl.add)
|
| 149 |
+
|
| 150 |
+
nisa.dma_copy(dst=x_new_hbm[r0:r0 + _P_MAX, 0:H], src=s_sb[k])
|
| 151 |
+
|
| 152 |
+
# ---- thinned stat chain: mean via shift-and-reduce, variance, rstd ----
|
| 153 |
+
nisa.tensor_scalar(dst=neg_shift, data=s_sb[k][0:_P_MAX, 0:1],
|
| 154 |
+
op0=nl.multiply, operand0=-1.0)
|
| 155 |
+
nisa.activation_reduce(dst=sink, op=nl.copy, data=s_sb[k],
|
| 156 |
+
reduce_op=nl.add, reduce_res=acc,
|
| 157 |
+
bias=neg_shift, scale=1.0)
|
| 158 |
+
nisa.tensor_scalar(dst=neg_mu, data=acc, op0=nl.multiply, operand0=neg_inv_h,
|
| 159 |
+
op1=nl.add, operand1=neg_shift)
|
| 160 |
+
nisa.activation_reduce(dst=sink, op=nl.square, data=s_sb[k],
|
| 161 |
+
reduce_op=nl.add, reduce_res=acc, bias=neg_mu, scale=1.0)
|
| 162 |
+
nisa.tensor_scalar(dst=var, data=acc, op0=nl.multiply, operand0=inv_h,
|
| 163 |
+
op1=nl.add, operand1=eps)
|
| 164 |
+
nisa.activation(dst=rstd, op=nl.rsqrt, data=var)
|
| 165 |
+
|
| 166 |
+
# ---- one-op affine-free apply: z = (x_new + neg_mu) * rstd, bf16 cast on write.
|
| 167 |
+
# Both operands are per-lane [128, 1] vectors; the fused two-op tensor_scalar
|
| 168 |
+
# costs the same as one op.
|
| 169 |
+
nisa.tensor_scalar(dst=z_sb[k], data=s_sb[k],
|
| 170 |
+
op0=nl.add, operand0=neg_mu,
|
| 171 |
+
op1=nl.multiply, operand1=rstd)
|
| 172 |
+
|
| 173 |
+
nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:H], src=z_sb[k])
|
| 174 |
+
|
| 175 |
+
return x_new_hbm, z_hbm
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def _no_grad(*tensors: torch.Tensor) -> None:
|
| 179 |
+
"""Refuse gradient-tracking inputs -- this op registers no autograd rule."""
|
| 180 |
+
for t in tensors:
|
| 181 |
+
if t.requires_grad:
|
| 182 |
+
raise RuntimeError(
|
| 183 |
+
"nki_res_layernorm_fold registers no autograd rule. Use under "
|
| 184 |
+
"torch.no_grad()/eval()."
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@nki_op("vit_enc::nki_res_layernorm_fold", mutates_args={})
|
| 189 |
+
def nki_res_layernorm_fold(
|
| 190 |
+
a: torch.Tensor,
|
| 191 |
+
b: torch.Tensor,
|
| 192 |
+
eps: float,
|
| 193 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 194 |
+
"""Fused residual add + affine-free LayerNorm on LNC=2 (gamma/beta folded away).
|
| 195 |
+
|
| 196 |
+
The consumers MUST have been packed with folded weights; feeding ``z`` to unfolded
|
| 197 |
+
weights silently drops the affine.
|
| 198 |
+
|
| 199 |
+
Args:
|
| 200 |
+
a: [R, H] float32 on 'neuron'. ``R`` divisible by ``LNC * 128``.
|
| 201 |
+
b: [B, R, H] bf16 on 'neuron'. B branch tensors (1 <= B <= 4), summed in.
|
| 202 |
+
eps: LayerNorm epsilon.
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
``(x_new [R, H] float32, z [R, H] bf16)``.
|
| 206 |
+
"""
|
| 207 |
+
_no_grad(a, b)
|
| 208 |
+
return _res_layernorm_fold_kernel[LNC](a, b, eps)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
#: Requested export alias. Same object as the incumbent-named nki_op, so the runtime
|
| 212 |
+
#: op-name string ("vit_enc::nki_res_layernorm_fold") is unchanged whichever name is used.
|
| 213 |
+
res_layernorm_fold = nki_res_layernorm_fold
|
build/torch-neuron/nki_kernels/swiglu_stream.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Weight-streaming SwiGLU FFN, with an optional fused residual + next-layer LN epilogue.
|
| 2 |
+
|
| 3 |
+
out = (silu(x @ w1.T + b1) * (x @ wg.T + bg)) @ w2.T + b2 (SwiGLU FFN)
|
| 4 |
+
|
| 5 |
+
WHY STREAMING. ``swiglu.py`` keeps gate+up+down weights SBUF-resident (3 x K x I bf16). At ViT-H+
|
| 6 |
+
(K=1280, I=3416) that is ~205 KiB/partition, over the ~192 KiB budget, so the compiler spills.
|
| 7 |
+
Here the intermediate dim I is processed in chunks of CI columns: only one chunk's gate/up columns
|
| 8 |
+
and down rows are live (double-buffered by the scheduler: chunk c+1's DMA has no dependency on
|
| 9 |
+
chunk c's matmuls), and the down-projection is accumulated across chunks in an fp32 SBUF
|
| 10 |
+
accumulator with rows on partitions.
|
| 11 |
+
|
| 12 |
+
LOOP ORDER. Rows (per core) are processed in super-blocks of S rows (host-chosen, <=1024). Within
|
| 13 |
+
a super-block the chunk loop is OUTER, so each weight chunk is DMA'd once per super-block and
|
| 14 |
+
reused by every 512-row slice in it -- weight traffic per row falls as S grows.
|
| 15 |
+
|
| 16 |
+
FUSED EPILOGUE (``fuse=1``). The accumulator is initialised from the fp32 residual ``a``; after the
|
| 17 |
+
last chunk: x_new = acc + b2 (fp32), z = LN_no_affine(x_new; eps) (bf16, same stat chain as
|
| 18 |
+
res_layernorm.py). Returns (x_new, z). ``fuse=0`` returns bf16 ``acc + b2`` like ``swiglu.py``.
|
| 19 |
+
|
| 20 |
+
Rows are sharded across LNC cores (R % (LNC*128) == 0). INFERENCE ONLY.
|
| 21 |
+
"""
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import torch
|
| 25 |
+
from torch_neuronx import nki_op
|
| 26 |
+
|
| 27 |
+
import nki
|
| 28 |
+
import nki.isa as nisa
|
| 29 |
+
import nki.language as nl
|
| 30 |
+
|
| 31 |
+
import os as _os
|
| 32 |
+
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
|
| 33 |
+
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
|
| 34 |
+
|
| 35 |
+
_P_MAX = 128
|
| 36 |
+
_F_MOV_MAX = 512
|
| 37 |
+
_BF16 = nl.bfloat16
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _div_ceil(n: int, d: int) -> int:
|
| 41 |
+
return (n + d - 1) // d
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _kernel_assert(cond: bool, msg: str):
|
| 45 |
+
assert cond, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {msg}"
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def plan(K: int, I: int, rows_per_core: int, budget: int = 176 * 1024):
|
| 49 |
+
"""Host-side choice of (S, CI) from a per-partition SBUF estimate (bytes)."""
|
| 50 |
+
n_k = _div_ceil(K, _P_MAX)
|
| 51 |
+
best = None
|
| 52 |
+
for CI in (1024, 512, 256, 128):
|
| 53 |
+
for S in (1024, 512, 256, 128):
|
| 54 |
+
if S > _div_ceil(rows_per_core, _P_MAX) * _P_MAX and S != 128:
|
| 55 |
+
continue
|
| 56 |
+
nh = CI // _P_MAX
|
| 57 |
+
w = 2 * (2 * n_k * CI * 2 + nh * K * 2) # 2 chunks in flight
|
| 58 |
+
acc = (S // _P_MAX) * K * 4 # fp32 accumulator rows
|
| 59 |
+
xt = n_k * S * 2 + (S // _P_MAX) * K * 2 # x^T + staging rows
|
| 60 |
+
g = nh * _F_MOV_MAX * (2 + 4) # g bf16 + silu fp32 per tile
|
| 61 |
+
ep = 2 * K * 2 + K * 4 # z tiles + b2 broadcast
|
| 62 |
+
tot = w + acc + xt + g + ep + _div_ceil(I, _P_MAX) * 8 + 4096
|
| 63 |
+
if tot <= budget:
|
| 64 |
+
n_sb = _div_ceil(rows_per_core, S)
|
| 65 |
+
key = (-n_sb, CI, -S) # fewest weight re-streams, then big CI, then tight S
|
| 66 |
+
if best is None or key > best[0]:
|
| 67 |
+
best = (key, S, CI, tot)
|
| 68 |
+
_kernel_assert(best is not None, f"no SBUF plan for K={K} I={I} rows={rows_per_core}")
|
| 69 |
+
return best[1], best[2], best[3]
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _stage_xt(x_hbm, r_abs, S, n_k, K):
|
| 73 |
+
xt = []
|
| 74 |
+
for _ in range(n_k):
|
| 75 |
+
xt.append(nl.ndarray((_P_MAX, S), dtype=_BF16, buffer=nl.sbuf))
|
| 76 |
+
for rs in range(S // _P_MAX):
|
| 77 |
+
rr0 = rs * _P_MAX
|
| 78 |
+
xb = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
|
| 79 |
+
nisa.dma_copy(dst=xb[0:_P_MAX, 0:K], src=x_hbm[r_abs + rr0:r_abs + rr0 + _P_MAX, 0:K])
|
| 80 |
+
for kt in range(n_k):
|
| 81 |
+
k0 = kt * _P_MAX
|
| 82 |
+
pk = min(_P_MAX, K - k0)
|
| 83 |
+
nisa.dma_transpose(dst=xt[kt][0:pk, rr0:rr0 + _P_MAX], src=xb[0:_P_MAX, k0:k0 + pk])
|
| 84 |
+
return xt
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _body(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, a_hbm, eps, S, CI, fuse):
|
| 88 |
+
R, K = x_hbm.shape
|
| 89 |
+
K1, I = w1t_hbm.shape
|
| 90 |
+
_kernel_assert(K1 == K and tuple(wgt_hbm.shape) == (K, I), "w1t/wgt must be [K, I]")
|
| 91 |
+
_kernel_assert(tuple(w2t_hbm.shape) == (I, K), "w2t must be [I, K]")
|
| 92 |
+
_kernel_assert(R % (LNC * _P_MAX) == 0, f"R={R} must be a multiple of LNC*128")
|
| 93 |
+
rows_per = R // LNC
|
| 94 |
+
_kernel_assert(S % _P_MAX == 0, f"S={S} must be a multiple of 128")
|
| 95 |
+
_kernel_assert(CI % _P_MAX == 0, "CI must be a multiple of 128")
|
| 96 |
+
|
| 97 |
+
pid = nl.program_id(0)
|
| 98 |
+
r_base = pid * rows_per
|
| 99 |
+
n_k = _div_ceil(K, _P_MAX)
|
| 100 |
+
n_h = _div_ceil(I, _P_MAX)
|
| 101 |
+
n_c = _div_ceil(I, CI)
|
| 102 |
+
n_ko = _div_ceil(K, _F_MOV_MAX)
|
| 103 |
+
n_sb = _div_ceil(rows_per, S)
|
| 104 |
+
|
| 105 |
+
if fuse:
|
| 106 |
+
xnew_hbm = nl.ndarray((R, K), dtype=nl.float32, buffer=nl.shared_hbm)
|
| 107 |
+
z_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
|
| 108 |
+
else:
|
| 109 |
+
out_hbm = nl.ndarray((R, K), dtype=_BF16, buffer=nl.shared_hbm)
|
| 110 |
+
|
| 111 |
+
# biases (tiny, resident): b1/bg per 128-row tile of I, b2 broadcast over partitions.
|
| 112 |
+
b1_t = []
|
| 113 |
+
bg_t = []
|
| 114 |
+
for ht in range(n_h):
|
| 115 |
+
h0 = ht * _P_MAX
|
| 116 |
+
ph = min(_P_MAX, I - h0)
|
| 117 |
+
t1 = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 118 |
+
nisa.dma_copy(dst=t1[0:ph, 0:1], src=b1_hbm[h0:h0 + ph])
|
| 119 |
+
b1_t.append(t1)
|
| 120 |
+
tg = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 121 |
+
nisa.dma_copy(dst=tg[0:ph, 0:1], src=bg_hbm[h0:h0 + ph])
|
| 122 |
+
bg_t.append(tg)
|
| 123 |
+
b2_bc = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf)
|
| 124 |
+
nisa.dma_copy(dst=b2_bc[0:_P_MAX, 0:K], src=b2_hbm.ap(pattern=[[0, _P_MAX], [1, K]], offset=0))
|
| 125 |
+
|
| 126 |
+
inv_k = 1.0 / float(K)
|
| 127 |
+
neg_inv_k = -inv_k
|
| 128 |
+
|
| 129 |
+
for sb in range(n_sb):
|
| 130 |
+
r_sb = r_base + sb * S
|
| 131 |
+
Ssb = min(S, rows_per - sb * S) # ragged last super-block (multiple of 128)
|
| 132 |
+
n_rs = Ssb // _P_MAX
|
| 133 |
+
xt = _stage_xt(x_hbm, r_sb, Ssb, n_k, K)
|
| 134 |
+
|
| 135 |
+
acc = []
|
| 136 |
+
for rs in range(n_rs):
|
| 137 |
+
t = nl.ndarray((_P_MAX, K), dtype=nl.float32, buffer=nl.sbuf)
|
| 138 |
+
if fuse:
|
| 139 |
+
nisa.dma_copy(dst=t[0:_P_MAX, 0:K],
|
| 140 |
+
src=a_hbm[r_sb + rs * _P_MAX:r_sb + (rs + 1) * _P_MAX, 0:K])
|
| 141 |
+
acc.append(t)
|
| 142 |
+
|
| 143 |
+
for c in range(n_c):
|
| 144 |
+
i0 = c * CI
|
| 145 |
+
ci = min(CI, I - i0)
|
| 146 |
+
nh_c = _div_ceil(ci, _P_MAX)
|
| 147 |
+
# ---- this chunk's weights (gate/up columns i0:i0+ci, down rows i0:i0+ci) ----
|
| 148 |
+
w1c = []
|
| 149 |
+
wgc = []
|
| 150 |
+
for kt in range(n_k):
|
| 151 |
+
k0 = kt * _P_MAX
|
| 152 |
+
pk = min(_P_MAX, K - k0)
|
| 153 |
+
t1 = nl.ndarray((_P_MAX, CI), dtype=_BF16, buffer=nl.sbuf)
|
| 154 |
+
nisa.dma_copy(dst=t1[0:pk, 0:ci], src=w1t_hbm[k0:k0 + pk, i0:i0 + ci])
|
| 155 |
+
w1c.append(t1)
|
| 156 |
+
tg = nl.ndarray((_P_MAX, CI), dtype=_BF16, buffer=nl.sbuf)
|
| 157 |
+
nisa.dma_copy(dst=tg[0:pk, 0:ci], src=wgt_hbm[k0:k0 + pk, i0:i0 + ci])
|
| 158 |
+
wgc.append(tg)
|
| 159 |
+
w2c = []
|
| 160 |
+
for hl in range(nh_c):
|
| 161 |
+
h0 = i0 + hl * _P_MAX
|
| 162 |
+
ph = min(_P_MAX, I - h0)
|
| 163 |
+
t2 = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
|
| 164 |
+
nisa.dma_copy(dst=t2[0:ph, 0:K], src=w2t_hbm[h0:h0 + ph, 0:K])
|
| 165 |
+
w2c.append(t2)
|
| 166 |
+
|
| 167 |
+
for rb in range(_div_ceil(Ssb, _F_MOV_MAX)):
|
| 168 |
+
q0 = rb * _F_MOV_MAX
|
| 169 |
+
rw = min(_F_MOV_MAX, Ssb - q0)
|
| 170 |
+
# ---- gate/up for this chunk x this 512-row slice -> g [ci, rw] bf16 ----
|
| 171 |
+
g_t = []
|
| 172 |
+
for hl in range(nh_c):
|
| 173 |
+
hg = (i0 // _P_MAX) + hl
|
| 174 |
+
ph = min(_P_MAX, I - (i0 + hl * _P_MAX))
|
| 175 |
+
c0 = hl * _P_MAX
|
| 176 |
+
y1 = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
|
| 177 |
+
for kt in nl.affine_range(n_k):
|
| 178 |
+
pk = min(_P_MAX, K - kt * _P_MAX)
|
| 179 |
+
nisa.nc_matmul(dst=y1[0:ph, 0:rw],
|
| 180 |
+
stationary=w1c[kt][0:pk, c0:c0 + ph],
|
| 181 |
+
moving=xt[kt][0:pk, q0:q0 + rw])
|
| 182 |
+
yg = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
|
| 183 |
+
for kt in nl.affine_range(n_k):
|
| 184 |
+
pk = min(_P_MAX, K - kt * _P_MAX)
|
| 185 |
+
nisa.nc_matmul(dst=yg[0:ph, 0:rw],
|
| 186 |
+
stationary=wgc[kt][0:pk, c0:c0 + ph],
|
| 187 |
+
moving=xt[kt][0:pk, q0:q0 + rw])
|
| 188 |
+
sa = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.sbuf)
|
| 189 |
+
nisa.activation(dst=sa[0:ph, 0:rw], op=nl.silu, data=y1[0:ph, 0:rw],
|
| 190 |
+
bias=b1_t[hg][0:ph, 0:1])
|
| 191 |
+
g = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=_BF16, buffer=nl.sbuf)
|
| 192 |
+
# g = (yg + bg) * silu(y1 + b1) -- one fused op, PSUM operand
|
| 193 |
+
nisa.scalar_tensor_tensor(dst=g[0:ph, 0:rw], data=yg[0:ph, 0:rw],
|
| 194 |
+
op0=nl.add, operand0=bg_t[hg][0:ph, 0:1],
|
| 195 |
+
op1=nl.multiply, operand1=sa[0:ph, 0:rw])
|
| 196 |
+
g_t.append(g)
|
| 197 |
+
# ---- down-projection partial for this chunk, accumulated into acc (fp32 SBUF) ----
|
| 198 |
+
for rs_l in range(rw // _P_MAX):
|
| 199 |
+
rs = (q0 // _P_MAX) + rs_l
|
| 200 |
+
for ko in range(n_ko):
|
| 201 |
+
ko0 = ko * _F_MOV_MAX
|
| 202 |
+
nk = min(_F_MOV_MAX, K - ko0)
|
| 203 |
+
ps = nl.ndarray((_P_MAX, _F_MOV_MAX), dtype=nl.float32, buffer=nl.psum)
|
| 204 |
+
for hl in nl.affine_range(nh_c):
|
| 205 |
+
ph = min(_P_MAX, I - (i0 + hl * _P_MAX))
|
| 206 |
+
nisa.nc_matmul(dst=ps[0:_P_MAX, 0:nk],
|
| 207 |
+
stationary=g_t[hl][0:ph, rs_l * _P_MAX:(rs_l + 1) * _P_MAX],
|
| 208 |
+
moving=w2c[hl][0:ph, ko0:ko0 + nk])
|
| 209 |
+
if c == 0 and not fuse:
|
| 210 |
+
nisa.tensor_copy(dst=acc[rs][0:_P_MAX, ko0:ko0 + nk], src=ps[0:_P_MAX, 0:nk])
|
| 211 |
+
else:
|
| 212 |
+
nisa.tensor_tensor(dst=acc[rs][0:_P_MAX, ko0:ko0 + nk],
|
| 213 |
+
data1=acc[rs][0:_P_MAX, ko0:ko0 + nk],
|
| 214 |
+
data2=ps[0:_P_MAX, 0:nk], op=nl.add)
|
| 215 |
+
|
| 216 |
+
# ---- epilogue per 128-row subtile ----
|
| 217 |
+
for rs in range(n_rs):
|
| 218 |
+
r0 = r_sb + rs * _P_MAX
|
| 219 |
+
row = acc[rs]
|
| 220 |
+
nisa.tensor_tensor(dst=row[0:_P_MAX, 0:K], data1=row[0:_P_MAX, 0:K],
|
| 221 |
+
data2=b2_bc[0:_P_MAX, 0:K], op=nl.add)
|
| 222 |
+
zt = nl.ndarray((_P_MAX, K), dtype=_BF16, buffer=nl.sbuf)
|
| 223 |
+
if fuse:
|
| 224 |
+
nisa.dma_copy(dst=xnew_hbm[r0:r0 + _P_MAX, 0:K], src=row[0:_P_MAX, 0:K])
|
| 225 |
+
neg_shift = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 226 |
+
acc_s = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 227 |
+
neg_mu = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 228 |
+
var = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 229 |
+
rstd = nl.ndarray((_P_MAX, 1), dtype=nl.float32, buffer=nl.sbuf)
|
| 230 |
+
nisa.tensor_scalar(dst=neg_shift, data=row[0:_P_MAX, 0:1], op0=nl.multiply, operand0=-1.0)
|
| 231 |
+
nisa.activation_reduce(dst=zt, op=nl.copy, data=row[0:_P_MAX, 0:K],
|
| 232 |
+
reduce_op=nl.add, reduce_res=acc_s, bias=neg_shift, scale=1.0)
|
| 233 |
+
nisa.tensor_scalar(dst=neg_mu, data=acc_s, op0=nl.multiply, operand0=neg_inv_k,
|
| 234 |
+
op1=nl.add, operand1=neg_shift)
|
| 235 |
+
nisa.activation_reduce(dst=zt, op=nl.square, data=row[0:_P_MAX, 0:K],
|
| 236 |
+
reduce_op=nl.add, reduce_res=acc_s, bias=neg_mu, scale=1.0)
|
| 237 |
+
nisa.tensor_scalar(dst=var, data=acc_s, op0=nl.multiply, operand0=inv_k,
|
| 238 |
+
op1=nl.add, operand1=eps)
|
| 239 |
+
nisa.activation(dst=rstd, op=nl.rsqrt, data=var)
|
| 240 |
+
nisa.tensor_scalar(dst=zt, data=row[0:_P_MAX, 0:K], op0=nl.add, operand0=neg_mu,
|
| 241 |
+
op1=nl.multiply, operand1=rstd)
|
| 242 |
+
nisa.dma_copy(dst=z_hbm[r0:r0 + _P_MAX, 0:K], src=zt)
|
| 243 |
+
else:
|
| 244 |
+
nisa.tensor_copy(dst=zt, src=row[0:_P_MAX, 0:K])
|
| 245 |
+
nisa.dma_copy(dst=out_hbm[r0:r0 + _P_MAX, 0:K], src=zt)
|
| 246 |
+
|
| 247 |
+
if fuse:
|
| 248 |
+
return xnew_hbm, z_hbm
|
| 249 |
+
return out_hbm
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
@nki.jit
|
| 253 |
+
def _swiglu_stream_kernel(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, S, CI):
|
| 254 |
+
return _body(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, None, 0.0, S, CI, 0)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
@nki.jit
|
| 258 |
+
def _swiglu_stream_ln_kernel(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, a_hbm, eps, S, CI):
|
| 259 |
+
return _body(x_hbm, w1t_hbm, b1_hbm, wgt_hbm, bg_hbm, w2t_hbm, b2_hbm, a_hbm, eps, S, CI, 1)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def _no_grad(*ts):
|
| 263 |
+
for t in ts:
|
| 264 |
+
if t is not None and t.requires_grad:
|
| 265 |
+
raise RuntimeError("swiglu_stream is inference-only; use torch.no_grad().")
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def _plan_for(x, w1t):
|
| 269 |
+
S, CI, _ = plan(x.shape[1], w1t.shape[1], x.shape[0] // LNC)
|
| 270 |
+
S = int(_os.environ.get("VIT_ENC_SW_S", S))
|
| 271 |
+
CI = int(_os.environ.get("VIT_ENC_SW_CI", CI))
|
| 272 |
+
return S, CI
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
@nki_op("vit_enc::nki_swiglu_stream", mutates_args={})
|
| 276 |
+
def nki_swiglu_stream(x: torch.Tensor, w1t: torch.Tensor, b1: torch.Tensor, wgt: torch.Tensor,
|
| 277 |
+
bg: torch.Tensor, w2t: torch.Tensor, b2: torch.Tensor) -> torch.Tensor:
|
| 278 |
+
"""x [R,K] bf16; w1t/wgt [K,I] bf16; b1/bg [I] f32; w2t [I,K] bf16; b2 [K] f32 -> [R,K] bf16."""
|
| 279 |
+
_no_grad(x, w1t, b1, wgt, bg, w2t, b2)
|
| 280 |
+
S, CI = _plan_for(x, w1t)
|
| 281 |
+
return _swiglu_stream_kernel[LNC](x, w1t, b1, wgt, bg, w2t, b2, S, CI)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
@nki_op("vit_enc::nki_swiglu_stream_ln", mutates_args={})
|
| 285 |
+
def nki_swiglu_stream_ln(x: torch.Tensor, w1t: torch.Tensor, b1: torch.Tensor, wgt: torch.Tensor,
|
| 286 |
+
bg: torch.Tensor, w2t: torch.Tensor, b2: torch.Tensor, a: torch.Tensor,
|
| 287 |
+
eps: float) -> tuple[torch.Tensor, torch.Tensor]:
|
| 288 |
+
"""SwiGLU(x) + residual a, then affine-free LN. Returns (x_new [R,K] fp32, z [R,K] bf16)."""
|
| 289 |
+
_no_grad(x, w1t, b1, wgt, bg, w2t, b2, a)
|
| 290 |
+
S, CI = _plan_for(x, w1t)
|
| 291 |
+
return _swiglu_stream_ln_kernel[LNC](x, w1t, b1, wgt, bg, w2t, b2, a, eps, S, CI)
|
examples/dinov3_features.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DINOv3 ViT-L/16 features on trn2 with vit-encoder-neuron-kernels.
|
| 2 |
+
|
| 3 |
+
NEURON_LOGICAL_NC_CONFIG=2 python dinov3_features.py
|
| 4 |
+
"""
|
| 5 |
+
import types
|
| 6 |
+
import torch
|
| 7 |
+
from kernels import get_kernel
|
| 8 |
+
from transformers import AutoModel
|
| 9 |
+
|
| 10 |
+
k = get_kernel("jburtoft/vit-encoder-neuron-kernels", version=1, trust_remote_code=True)
|
| 11 |
+
model = AutoModel.from_pretrained("facebook/dinov3-vitl16-pretrain-lvd1689m", dtype=torch.float32).eval()
|
| 12 |
+
ref_model = AutoModel.from_pretrained("facebook/dinov3-vitl16-pretrain-lvd1689m", dtype=torch.float32).eval()
|
| 13 |
+
model = model.to("neuron")
|
| 14 |
+
model.forward = types.MethodType(k.NeuronDINOv3ViTModel.forward, model)
|
| 15 |
+
model = torch.compile(model, backend="neuron")
|
| 16 |
+
|
| 17 |
+
torch.manual_seed(0)
|
| 18 |
+
pixel_values = torch.randn(4, 3, 224, 224)
|
| 19 |
+
with torch.no_grad():
|
| 20 |
+
out = model(pixel_values.to("neuron")).last_hidden_state.cpu()
|
| 21 |
+
ref = ref_model(pixel_values).last_hidden_state
|
| 22 |
+
print("features", tuple(out.shape), "rel_l2 vs fp32 CPU:", ((out - ref).norm() / ref.norm()).item())
|