FermionResearch commited on
Commit
9c71059
·
0 Parent(s):

Super-squash branch 'main' using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +34 -0
  2. LICENSE +202 -0
  3. MANIFEST.json +257 -0
  4. NOTICE +43 -0
  5. README.md +496 -0
  6. bin/MANIFEST.json +25 -0
  7. bin/README.md +216 -0
  8. bin/fermion-run-linux-x64 +3 -0
  9. bin/fermion-run-linux-x64.sha256 +1 -0
  10. bin/fermion-run-macos-arm64 +3 -0
  11. bin/fermion-run-macos-arm64.sha256 +1 -0
  12. config.json +26 -0
  13. generation_config.json +10 -0
  14. gguf/Modelfile +6 -0
  15. gguf/README.md +151 -0
  16. gguf/SHA256SUMS +6 -0
  17. gguf/fv5.patch +952 -0
  18. gguf/neutrino-0.6b-base-fv5.gguf +3 -0
  19. gguf/receipts/bench_0p6b.json +112 -0
  20. gguf/receipts/convert_0p6b.json +44 -0
  21. gguf/receipts/gv_0p6b_f32.json +17 -0
  22. gguf/receipts/gv_0p6b_fv5.json +17 -0
  23. gguf/receipts/ref_0p6b.json +26 -0
  24. merges.txt +0 -0
  25. mlx/MANIFEST.json +122 -0
  26. mlx/README.md +116 -0
  27. mlx/fermion_mlx/__init__.py +130 -0
  28. mlx/fermion_mlx/__main__.py +177 -0
  29. mlx/fermion_mlx/arch3.py +183 -0
  30. mlx/fermion_mlx/container.py +143 -0
  31. mlx/fermion_mlx/gates.py +164 -0
  32. mlx/fermion_mlx/mbatch.py +164 -0
  33. mlx/fermion_mlx/model.py +281 -0
  34. mlx/fermion_mlx/spec.py +256 -0
  35. mlx/fermion_mlx/trit_kernels.py +1053 -0
  36. mlx/generation_config.json +10 -0
  37. mlx/merges.txt +0 -0
  38. mlx/receipts/bench_0p6b_m5.json +51 -0
  39. mlx/receipts/cachegate_0p6b.json +61 -0
  40. mlx/receipts/kernel_version_sweep_0p6b.json +11 -0
  41. mlx/receipts/pathgate_0p6b.json +75 -0
  42. mlx/receipts/refgate_0p6b.json +133 -0
  43. mlx/requirements.txt +6 -0
  44. mlx/tokenizer.json +3 -0
  45. mlx/tokenizer_config.json +239 -0
  46. mlx/vocab.json +0 -0
  47. neutrino-0.6b_v4.bin +3 -0
  48. neutrino-0.6b_v4.tv4z +3 -0
  49. receipts/hf_greedy_cache_86565bf6.json +433 -0
  50. receipts/m5_bitgate_analysis.json +18 -0
.gitattributes ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Fermion Research — Hugging Face LFS patterns.
2
+ # Order matters: later matching rules override earlier ones.
3
+
4
+ # ---------------------------------------------------------------
5
+ # Weights, transports and other large binaries -> Git LFS.
6
+ # These MUST be LFS. The 8B container is 3.9 GB and its GGUF is
7
+ # 4.1 GB; committed as plain blobs the push fails outright.
8
+ # ---------------------------------------------------------------
9
+ *.bin filter=lfs diff=lfs merge=lfs -text
10
+ *.tv4z filter=lfs diff=lfs merge=lfs -text
11
+ *.gguf filter=lfs diff=lfs merge=lfs -text
12
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
13
+ *.pt filter=lfs diff=lfs merge=lfs -text
14
+ *.pth filter=lfs diff=lfs merge=lfs -text
15
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
16
+ *.npz filter=lfs diff=lfs merge=lfs -text
17
+ *.onnx filter=lfs diff=lfs merge=lfs -text
18
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
19
+ *.tar filter=lfs diff=lfs merge=lfs -text
20
+ *.tar.gz filter=lfs diff=lfs merge=lfs -text
21
+ *.zip filter=lfs diff=lfs merge=lfs -text
22
+
23
+ # Prebuilt native runtimes shipped in bin/ (multi-MB executables).
24
+ # Listed explicitly rather than as bin/fermion-run-*: the Hub resolves LFS by
25
+ # "does any filter=lfs pattern match" and does NOT honour a later `-filter`
26
+ # unset, so a glob here would drag the tiny .sha256 sidecars into LFS too and
27
+ # they would render as pointer files instead of the sha line.
28
+ bin/fermion-run-linux-x64 filter=lfs diff=lfs merge=lfs -text
29
+ bin/fermion-run-macos-arm64 filter=lfs diff=lfs merge=lfs -text
30
+
31
+ # Tokenizer vocab: 11.4 MB, over the Hub's 10 MB non-LFS limit.
32
+ # (Must come AFTER any *.json rule to win. There is none, by design:
33
+ # every other .json here is a few KB and belongs in plain git.)
34
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "{}"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Fermion Research
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+
MANIFEST.json ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "note": "provenance: HF-tree filename -> source + sha256; weights entry is declarative until materialized",
3
+ "weights_container": {
4
+ "file": "neutrino-0.6b_v4.bin",
5
+ "status": "FINAL \u2014 0.6B base ship container (card + gguf + mlx pack gated on these bytes)",
6
+ "bytes": 327719836,
7
+ "sha256": "91c0b26d105710e755ef1ec1b5607c00da5d5cbd048fd5584135fb6fc1e19399",
8
+ "source": "Fermion Research release archive (public copy: huggingface.co/fermionresearch/Neutrino-0.6B)"
9
+ },
10
+ "files": {
11
+ ".gitattributes": {
12
+ "bytes": 1829,
13
+ "sha256": "83f9874d64fc325a932d03f7e71d75895113b7faf57cfe78d759fd8ac4f514c5",
14
+ "source": "banked lane artifact"
15
+ },
16
+ "LICENSE": {
17
+ "bytes": 11347,
18
+ "sha256": "2f978a54ef89f3e4ee2d413c347e4b03500c95f29342277a287acc2ae27ae280",
19
+ "source": "Apache-2.0 text (same file across all release trees)"
20
+ },
21
+ "NOTICE": {
22
+ "bytes": 1561,
23
+ "sha256": "6b7f7ed0cbf486ac68240e223d551eabdeb1d6dba59ed614f04316780437e643",
24
+ "source": "banked lane artifact"
25
+ },
26
+ "bin/README.md": {
27
+ "bytes": 11489,
28
+ "sha256": "d0e8d9e0e647013f79fd9ecb28a9ac0dac00e418d28e51e2a6f4610716be27bb",
29
+ "source": "bin/ staging doc: which fermion-run binaries land at upload + sidecar convention"
30
+ },
31
+ "bin/fermion-run-linux-x64": {
32
+ "bytes": 1317208,
33
+ "sha256": "d3e1c3d7a7fe43e9dd4d6e16e75c0cd234bfc9866964284101d200f3da05310b",
34
+ "source": "RE-BLESSED release binary (Linux x86-64 AVX2 baseline, fully static) \u2014 0.1.10 fp16-KV + YaRN rebuild 2026-07-28"
35
+ },
36
+ "bin/fermion-run-linux-x64.sha256": {
37
+ "bytes": 88,
38
+ "sha256": "44b26f8fd77232280a3a9ac7576487ceaeeaf4170f89f9f6334a623ee88acfa0",
39
+ "source": "sha256 sidecar (shasum line) for fermion-run-linux-x64"
40
+ },
41
+ "bin/fermion-run-macos-arm64": {
42
+ "bytes": 671656,
43
+ "sha256": "2b9902fc36bf59c50a67ee0c79ae38a259d066e6ebff764abbf05ff72017351c",
44
+ "source": "RE-BLESSED release binary (macOS arm64 CPU/NEON, static libomp) \u2014 0.1.10 fp16-KV + YaRN rebuild 2026-07-28"
45
+ },
46
+ "bin/fermion-run-macos-arm64.sha256": {
47
+ "bytes": 90,
48
+ "sha256": "8bc33f96ac8c95ee842c741aa91ac1a4aef051deae4ba55ed3a3d246d0af9743",
49
+ "source": "sha256 sidecar (shasum line) for fermion-run-macos-arm64"
50
+ },
51
+ "config.json": {
52
+ "bytes": 503,
53
+ "sha256": "67cae94190633a2ea52435c0b159202e9c16f8196e110c7a2639cb28736ae972",
54
+ "source": "written for the trtc_v4 loader (hf_ternary registration)"
55
+ },
56
+ "generation_config.json": {
57
+ "bytes": 181,
58
+ "sha256": "2537909d4312b7155b60fe0aee96d342de1a00b7841ed3e7eba4c3835d16d8ae",
59
+ "source": "transformers generation defaults for this SKU"
60
+ },
61
+ "gguf/Modelfile": {
62
+ "bytes": 275,
63
+ "sha256": "af5261e7e6f6f0e2f52895f2f1c165f6d7c058b93628fa54e4d8fb35c82aad2a",
64
+ "source": "banked lane artifact"
65
+ },
66
+ "gguf/README.md": {
67
+ "bytes": 6650,
68
+ "sha256": "16657bac673140b0cef77afb68af869f2f1dc39887d36bfc77eb7d94176bd95c",
69
+ "source": "banked lane artifact"
70
+ },
71
+ "gguf/SHA256SUMS": {
72
+ "bytes": 501,
73
+ "sha256": "96d97263ff5e276b7852a08aa52eca9e49418fbdc8671f60e5d1c6d73a18779d",
74
+ "source": "banked lane artifact"
75
+ },
76
+ "gguf/receipts/bench_0p6b.json": {
77
+ "bytes": 2667,
78
+ "sha256": "1e22cc409b85626efa44b8d26fa5cc2cb93124f841aff76eb5df8d7347933d40",
79
+ "source": "banked lane artifact"
80
+ },
81
+ "gguf/receipts/convert_0p6b.json": {
82
+ "bytes": 949,
83
+ "sha256": "176ff8191f00257a913595e20da2b4bdbdbc994f48364c67b049b7a753d23d0b",
84
+ "source": "banked lane artifact"
85
+ },
86
+ "gguf/receipts/gv_0p6b_f32.json": {
87
+ "bytes": 461,
88
+ "sha256": "d2ddf55b1cbfc24125292683f3eb08cc2de37c3a8f4147f638f135277fd7d187",
89
+ "source": "banked lane artifact"
90
+ },
91
+ "gguf/receipts/gv_0p6b_fv5.json": {
92
+ "bytes": 460,
93
+ "sha256": "b87c0411e2644fb03b4f00d187751757b3eb4960238df55fbf10c348154881cc",
94
+ "source": "banked lane artifact"
95
+ },
96
+ "gguf/receipts/ref_0p6b.json": {
97
+ "bytes": 471,
98
+ "sha256": "5b294b8fe3653662e6baec494fcc821097bca9ff2a4318552b9583fbc1fd42bd",
99
+ "source": "banked lane artifact"
100
+ },
101
+ "merges.txt": {
102
+ "bytes": 1671853,
103
+ "sha256": "8831e4f1a044471340f7c0a83d7bd71306a5b867e95fd870f74d0c5308a904d5",
104
+ "source": "Qwen/Qwen3-8B tokenizer file (Apache-2.0 upstream)"
105
+ },
106
+ "mlx/MANIFEST.json": {
107
+ "bytes": 5342,
108
+ "sha256": "3ba9ba4932cc98c91a634bd96e43c6305d644dc73e9aabf886b9130a5b2014bb",
109
+ "source": "banked lane artifact"
110
+ },
111
+ "mlx/README.md": {
112
+ "bytes": 5392,
113
+ "sha256": "c32d9fa4f7dbbc53ecdad2131c1e33596e57f6045acc8f0790a9d2431f014b11",
114
+ "source": "banked lane artifact"
115
+ },
116
+ "mlx/fermion_mlx/__init__.py": {
117
+ "bytes": 5063,
118
+ "sha256": "a547844d587067681639df546e6d010dab80bbd4f0ee55c6e2a16fba97a1dd40",
119
+ "source": "banked lane artifact"
120
+ },
121
+ "mlx/fermion_mlx/__main__.py": {
122
+ "bytes": 8069,
123
+ "sha256": "71e8c7fea446184bd471af2c37e51b04b9fa8d0f5d35e0a76c31f6fd19af957d",
124
+ "source": "banked lane artifact"
125
+ },
126
+ "mlx/fermion_mlx/arch3.py": {
127
+ "bytes": 7541,
128
+ "sha256": "a831e8a24b2b9a986b393d1c316aac66a3399a082a57176254b2feeae032f8cd",
129
+ "source": "banked lane artifact"
130
+ },
131
+ "mlx/fermion_mlx/container.py": {
132
+ "bytes": 6059,
133
+ "sha256": "c5b352184ba9ddf9a3e424731226cb7dcb9901d3a1516c67d4557953b0101f2f",
134
+ "source": "banked lane artifact"
135
+ },
136
+ "mlx/fermion_mlx/gates.py": {
137
+ "bytes": 7394,
138
+ "sha256": "64d07d9ba1a783a3f56ba978da7885e9fbe693495b3a4b7b8e176607d59de52c",
139
+ "source": "banked lane artifact"
140
+ },
141
+ "mlx/fermion_mlx/mbatch.py": {
142
+ "bytes": 6492,
143
+ "sha256": "c9ec8d37f1cdc0554cda01cefbc435e67525779c79e0dac7fd760e7919e6fdd2",
144
+ "source": "banked lane artifact"
145
+ },
146
+ "mlx/fermion_mlx/model.py": {
147
+ "bytes": 12748,
148
+ "sha256": "9488f8194ce8be006ce05dfaf6118353de44005ce54f38f73a0908ccac2749da",
149
+ "source": "banked lane artifact"
150
+ },
151
+ "mlx/fermion_mlx/spec.py": {
152
+ "bytes": 11207,
153
+ "sha256": "969e946d5d2eeb63d44afa4c62dd2453d87fa14e83b936bc673482b59ee3b745",
154
+ "source": "banked lane artifact"
155
+ },
156
+ "mlx/fermion_mlx/trit_kernels.py": {
157
+ "bytes": 44135,
158
+ "sha256": "aeb8bd4b03419763c1543893f19be3b4029611b2df883096990c4beebade384b",
159
+ "source": "banked lane artifact"
160
+ },
161
+ "mlx/generation_config.json": {
162
+ "bytes": 181,
163
+ "sha256": "6ca52b0bcb818c9e52db8ea18413494110c5f2def581b769f621dda951a7863b",
164
+ "source": "banked lane artifact"
165
+ },
166
+ "mlx/merges.txt": {
167
+ "bytes": 1671853,
168
+ "sha256": "8831e4f1a044471340f7c0a83d7bd71306a5b867e95fd870f74d0c5308a904d5",
169
+ "source": "banked lane artifact"
170
+ },
171
+ "mlx/receipts/bench_0p6b_m5.json": {
172
+ "bytes": 812,
173
+ "sha256": "543a9e41d8374a8cd066643a4e7cfa925359a8efc9c3182cbebb366bb0afbe89",
174
+ "source": "banked lane artifact"
175
+ },
176
+ "mlx/receipts/cachegate_0p6b.json": {
177
+ "bytes": 775,
178
+ "sha256": "b6acd64a31f4529ea01b96103a44b5bc54da283a7c3250cd564b5510ebaabc8c",
179
+ "source": "banked lane artifact"
180
+ },
181
+ "mlx/receipts/kernel_version_sweep_0p6b.json": {
182
+ "bytes": 355,
183
+ "sha256": "6b0df2026ded00c50d6b34004bf2dc4aabf097d21decfad7a354de5be594fe87",
184
+ "source": "banked lane artifact"
185
+ },
186
+ "mlx/receipts/pathgate_0p6b.json": {
187
+ "bytes": 623,
188
+ "sha256": "2453e15509a012616a100e3767a373af20a763dff89104e3c4dfbeb24085d072",
189
+ "source": "banked lane artifact"
190
+ },
191
+ "mlx/receipts/refgate_0p6b.json": {
192
+ "bytes": 1907,
193
+ "sha256": "ee4b3949bad85a522f1d1b65344a230a40f9fe9dc5e5dfdca736c28751eb4d85",
194
+ "source": "banked lane artifact"
195
+ },
196
+ "mlx/requirements.txt": {
197
+ "bytes": 191,
198
+ "sha256": "1cf4d1fa581aaa5deae0568f88388c51166bbd69e7a89080b5a53488a7ef39b6",
199
+ "source": "banked lane artifact"
200
+ },
201
+ "mlx/tokenizer.json": {
202
+ "bytes": 11422654,
203
+ "sha256": "aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4",
204
+ "source": "banked lane artifact"
205
+ },
206
+ "mlx/tokenizer_config.json": {
207
+ "bytes": 9732,
208
+ "sha256": "d5d09f07b48c3086c508b30d1c9114bd1189145b74e982a265350c923acd8101",
209
+ "source": "banked lane artifact"
210
+ },
211
+ "mlx/vocab.json": {
212
+ "bytes": 2776833,
213
+ "sha256": "ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910",
214
+ "source": "banked lane artifact"
215
+ },
216
+ "receipts/hf_greedy_cache_86565bf6.json": {
217
+ "bytes": 3578,
218
+ "sha256": "f8e81dd6b582c4097cf49b2abbc10b9e233331e60df7ebc4e03bcc587ec8f7e3",
219
+ "source": "banked lane artifact"
220
+ },
221
+ "receipts/m5_bitgate_analysis.json": {
222
+ "bytes": 1751,
223
+ "sha256": "26ef236f04d2bf0957ae9206f660898e3a73947bda272085e07e5523c4ac2979",
224
+ "source": "banked lane artifact"
225
+ },
226
+ "receipts/m5_demo_rereceipt_p3.json": {
227
+ "bytes": 1740,
228
+ "sha256": "aff7397eec5146a0418523140fa7531fc748b6c5229bb34c8b76f0eb8f73776b",
229
+ "source": "banked lane artifact"
230
+ },
231
+ "receipts/receipt_base.json": {
232
+ "bytes": 890,
233
+ "sha256": "2ae4accabe018f8efb1e0434ef63c8347e9cd096de74456f9887cfe5c874b117",
234
+ "source": "banked lane artifact"
235
+ },
236
+ "receipts/ship_checkpoint_numbers.json": {
237
+ "bytes": 1635,
238
+ "sha256": "95e757373209d99fdf1a3381d6f4a124e588a90ad01b2ae21b105db83349c29d",
239
+ "source": "banked lane artifact"
240
+ },
241
+ "tokenizer.json": {
242
+ "bytes": 11422654,
243
+ "sha256": "aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4",
244
+ "source": "Qwen/Qwen3-8B tokenizer file (Apache-2.0 upstream)"
245
+ },
246
+ "tokenizer_config.json": {
247
+ "bytes": 9979,
248
+ "sha256": "02e7044c846290e910d9c9deca9812315d64be56a6aeee3cfbedc99d177644c4",
249
+ "source": "Qwen/Qwen3-8B tokenizer file (Apache-2.0 upstream)"
250
+ },
251
+ "vocab.json": {
252
+ "bytes": 2776833,
253
+ "sha256": "ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910",
254
+ "source": "Qwen/Qwen3-8B tokenizer file (Apache-2.0 upstream)"
255
+ }
256
+ }
257
+ }
NOTICE ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Neutrino — Fermion Research
2
+ Copyright 2026 Fermion Research
3
+
4
+ This product includes software and model artifacts developed by
5
+ Fermion Research, licensed under the Apache License, Version 2.0
6
+ (see LICENSE).
7
+
8
+ --------------------------------------------------------------------
9
+ THIRD-PARTY NOTICES
10
+ --------------------------------------------------------------------
11
+
12
+ Qwen3 (Alibaba Cloud / Qwen Team)
13
+ Copyright (c) Alibaba Cloud
14
+ Licensed under the Apache License, Version 2.0
15
+ https://huggingface.co/Qwen/Qwen3-0.6B
16
+
17
+ This model is a derivative of Qwen/Qwen3-0.6B. In particular, the following
18
+ files in this repository are redistributed VERBATIM from the upstream
19
+ Qwen3 release and are covered by Alibaba Cloud's copyright and the
20
+ Apache License 2.0:
21
+
22
+ tokenizer.json
23
+ tokenizer_config.json
24
+ vocab.json
25
+ merges.txt
26
+
27
+ The model architecture is stock Qwen3 geometry. The ternary
28
+ quantization-aware training, the TRTC v4 container format, the
29
+ post-training, the runtimes and the evaluation receipts are Fermion
30
+ Research's own work.
31
+
32
+ You may obtain a copy of the Apache License 2.0 at:
33
+ http://www.apache.org/licenses/LICENSE-2.0
34
+
35
+ --------------------------------------------------------------------
36
+
37
+ llama.cpp (The ggml authors) — MIT License
38
+
39
+ The GGUF pack in gguf/ is produced for, and loaded by, Fermion
40
+ Research's fork of ggml-org/llama.cpp. That fork is distributed
41
+ separately under upstream's MIT license, NOT under this repository's
42
+ Apache-2.0 grant. Any patch or source in gguf/ that derives from
43
+ llama.cpp is MIT-licensed; see the fork's own LICENSE.
README.md ADDED
@@ -0,0 +1,496 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: Qwen/Qwen3-0.6B
4
+ pipeline_tag: text-generation
5
+ library_name: transformers
6
+ tags:
7
+ - ternary
8
+ - quantization
9
+ - trtc
10
+ - small-model
11
+ - speculative-decoding
12
+ ---
13
+
14
+ # Neutrino-0.6B (certified speculative-decoding draft for Neutrino-8B)
15
+
16
+ > [!IMPORTANT]
17
+ > **This model exists for one purpose: it is the certified
18
+ > speculative-decoding DRAFT for
19
+ > [Neutrino-8B](https://huggingface.co/fermionresearch/Neutrino-8B).**
20
+ > Paired with the 8B it delivers certified-exact speculative decoding
21
+ > (27,648 spec tokens compared on the ship brain, zero mismatches; 41,472
22
+ > across the whole lane, zero mismatches anywhere). It is **not an
23
+ > assistant**: no chat tuning, no instruction following, no tool use, and
24
+ > its standalone knowledge is deliberately not a claim. If you want a small
25
+ > model to talk to, use
26
+ > [Neutrino-0.6B-Chat](https://huggingface.co/fermionresearch/Neutrino-0.6B-Chat).
27
+
28
+ A 596M-parameter base language model whose every transformer linear is
29
+ stored in a proprietary ternary-family weight format eight times smaller
30
+ than fp16, in a single 327,719,836-byte container (237,703,560 bytes on the
31
+ wire), decoding at **225-236 tok/s on the CPU cores of a 16 GB Apple M5**
32
+ with the same runtime binaries that execute Neutrino-8B. That last property
33
+ is what makes it a draft model rather than a separate deployment: the
34
+ verifier and the draft are the same format, read by the same code, in one
35
+ process.
36
+
37
+ This is the small end of the Neutrino family, shipped as the 8B's speed
38
+ companion. The honest capability numbers are in the battery below; read
39
+ them before using it for anything but its intended lane (drafting,
40
+ edge/embedded token generation, and as a reference artifact for
41
+ ternary-family QAT at tiny scale).
42
+
43
+ - Container: `neutrino-0.6b_v4.bin` - TRTC v4, arch-3, 28 layers, hidden
44
+ 1024, vocab 151936. 327,719,836 bytes, sha256
45
+ `91c0b26d105710e755ef1ec1b5607c00da5d5cbd048fd5584135fb6fc1e19399`
46
+ (working name in receipts: `neutrino-0p6b_v4_base.bin`; the same bytes
47
+ appear as `neutrino_v4.bin` in the speculative-decoding lane).
48
+ - Coded transport: `neutrino-0.6b_v4.tv4z` - 237,703,560 bytes, lossless,
49
+ byte-exact round-trip receipt banked; sha256
50
+ `132d9ab758b0b1a5b9b6638e55647462dbdb4ec8345720cc98c3d4cf669e780c`.
51
+ - Inside: 196 packed ternary linears + one int8 embedding lane (**tied**:
52
+ the input embedding and lm_head share one table; container walk
53
+ cross-checked in the internal anatomy audit, and the GGUF
54
+ conversion records `tied_output_omitted: true`). Nothing in the decode
55
+ path is fp16/fp32 weights.
56
+ - Base: [Qwen/Qwen3-0.6B](https://huggingface.co/Qwen/Qwen3-0.6B)
57
+ (Apache-2.0), ternary QAT by
58
+ [Fermion Research](https://huggingface.co/fermionresearch).
59
+ Tokenizer shipped in this repo; vocabulary identical to the 8B's,
60
+ identity receipt banked, which is a precondition for drafting.
61
+
62
+ ## Architecture
63
+
64
+ Geometry as read from the shipped container's header (bit-checked during
65
+ the GGUF conversion, `gguf/receipts/convert_0p6b.json`; parameter split and
66
+ byte budget from the provenance-gated container walk):
67
+
68
+ | field | value |
69
+ |---|---|
70
+ | Parameters | 596,049,920 (440,401,920 ternary projection + 155,582,464 int8 embedding + 65,536 fp32 norm) |
71
+ | Decoder layers | 28 |
72
+ | Hidden width | 1,024 |
73
+ | Feed-forward width | 3,072, gated (SwiGLU) |
74
+ | Attention | grouped-query 2:1 - 16 query heads, 8 KV heads, head_dim 128 |
75
+ | Rotary embedding | full head width (rotary_dims 128), theta 1,000,000 |
76
+ | Normalization | RMSNorm, eps 1e-6, plus per-head Q/K RMSNorm in attention |
77
+ | Context length | 40,960 tokens (`max_pos`) |
78
+ | Vocabulary | 151,936 |
79
+ | Embeddings | **tied** - one int8 table serves embed-in and lm_head (`eok=1`) |
80
+ | KV cache | 114,688 B/token fp16 (112 KiB): 0.47 GB @ 4k, 3.76 GB @ 32k |
81
+ | Coded lane | 196 ternary linears (7 per layer x 28); 113 fp32 norm tensors stay full precision |
82
+
83
+ Byte budget of the 327,719,836-byte container, by tensor class:
84
+
85
+ | lane | bytes | share |
86
+ |---|---:|---:|
87
+ | ternary weight lane (q/k/v/o/gate/up/down x 28 layers = 196 linears) | 165,150,720 | 50.39% |
88
+ | token embeddings, int8 (one tied table) | 155,582,464 | 47.47% |
89
+ | per-row metadata (dims, scales, row sums) | 5,508,960 | 1.68% |
90
+ | embedding row scales | 1,215,488 | 0.37% |
91
+ | norm vectors, fp32 (113 tensors) | 262,144 | 0.08% |
92
+ | container header | 60 | - |
93
+
94
+ Every layer costs exactly 5,898,240 weight-lane bytes (40.0% attention /
95
+ 60.0% MLP; the 8B's split is 21.7% / 78.3%). State occupancy across the
96
+ 440,401,920 ternary weights: 62.263% zero / 18.873% plus / 18.864% minus
97
+ (per-layer arrays in the internal anatomy audit).
98
+
99
+ **The scale story in one row.** The tied embedding table is 26.1% of the
100
+ parameters but 47.5% of the container's bytes, because the format only
101
+ touches the transformer linears and the int8 vocabulary is left alone. On
102
+ the 8B the same lane is 32.1% of the file. At this size the vocabulary,
103
+ not the network, is the largest single object you download.
104
+
105
+ ## Formats and artifacts
106
+
107
+ | artifact | bytes | sha256 |
108
+ |---|---:|---|
109
+ | `neutrino-0.6b_v4.bin` (TRTC v4 container, the file every runtime executes) | 327,719,836 | `91c0b26d105710e755ef1ec1b5607c00da5d5cbd048fd5584135fb6fc1e19399` |
110
+ | `neutrino-0.6b_v4.tv4z` (lossless coded transport, 72.5% of raw) | 237,703,560 | `132d9ab758b0b1a5b9b6638e55647462dbdb4ec8345720cc98c3d4cf669e780c` |
111
+ | `gguf/neutrino-0.6b-base-fv5.gguf` (llama.cpp-fork pack, 310/310 tensors bit-checked vs the container) | 343,140,320 | `dd9354c4b3cd2b34195b0270540e4fa5db3811e42b6e4bdbd57958e0027e9272` |
112
+ | `gguf/fv5.patch` (the llama.cpp patch that builds the fork) | - | `81bf7be5eef18eb98ab4823596374c8dd6615151cf5615425973cb65942c0f73` |
113
+
114
+ Container md5 for cross-checking older receipts:
115
+ `86565bf6183e8031943aac9fd9771385`.
116
+
117
+ ## Quickstart
118
+
119
+ Five surfaces read those artifacts. All five execute the same container.
120
+
121
+ **1. pip engine** (the one-command door; pulls the container and the
122
+ platform-matching native runtime):
123
+
124
+ ```bash
125
+ pip install fermion-research
126
+ fermion generate --model fermionresearch/Neutrino-0.6B \
127
+ --raw --max-new 64 "2, 3, 5, 7, 11," # base model: --raw skips the chat template; the prompt is positional
128
+ ```
129
+
130
+ The first run downloads the ~0.3 GB container with no progress display in
131
+ fermion 0.1.5; from 0.1.6 the download shows a progress bar. Later runs
132
+ load from the local cache.
133
+
134
+ **2. Reference torch path** (bit-packed weights at rest, registers as a
135
+ native `transformers` model). Download the repo first — the loader
136
+ resolves the container relative to a local path only, so passing the hub
137
+ id straight to `from_pretrained` raises `FileNotFoundError`:
138
+
139
+ ```bash
140
+ hf download fermionresearch/Neutrino-0.6B --local-dir Neutrino-0.6B \
141
+ --exclude "gguf/*" --exclude "*.tv4z" # skip the packs other runtimes use
142
+ ```
143
+
144
+ ```python
145
+ import fermion # registers the trtc_v4 model type
146
+ from transformers import AutoModelForCausalLM, AutoTokenizer
147
+
148
+ model = AutoModelForCausalLM.from_pretrained("Neutrino-0.6B") # the downloaded directory
149
+ tokenizer = AutoTokenizer.from_pretrained("Neutrino-0.6B")
150
+ ids = tokenizer("2, 3, 5, 7, 11,", return_tensors="pt").input_ids
151
+ print(tokenizer.decode(model.generate(ids, max_new_tokens=64)[0]))
152
+ ```
153
+
154
+ This is a BASE model: prompt it as a completion engine. Greedy decoding
155
+ produces repetitive text on open-ended prompts (typical for 0.6B base
156
+ models); use sampling for anything user-facing.
157
+
158
+ `import fermion` must come first: it registers the `trtc_v4` model type.
159
+ Without it Transformers raises `model type 'trtc_v4' ... not recognize this
160
+ architecture` and advises upgrading Transformers or installing it from
161
+ source. Both are dead ends — the type is registered at import time by the
162
+ `fermion-research` package, and `trust_remote_code=True` will not help
163
+ because this repo carries no `auto_map`. Add the import.
164
+
165
+ **3. Native binary** (the fast path; this repo ships the `fermion-run-*`
166
+ binaries under `bin/` — they execute TRTC v4 arch-3 containers
167
+ generically, so the same binaries also run the 8B with no rebuild). From
168
+ the downloaded repo directory:
169
+
170
+ ```bash
171
+ (cd bin && shasum -a 256 -c fermion-run-macos-arm64.sha256) # sidecar holds a bare filename
172
+ chmod +x bin/fermion-run-macos-arm64 # hf download writes 0644
173
+ xattr -d com.apple.quarantine bin/fermion-run-macos-arm64 2>/dev/null || true # macOS only
174
+ ./bin/fermion-run-macos-arm64 neutrino-0.6b_v4.bin "1,791,1283" 128 9 # ids, n_new, threads
175
+ ```
176
+
177
+ On Linux use `bin/fermion-run-linux-x64` and skip the `xattr` line.
178
+
179
+ **4. GGUF pack + our llama.cpp fork** (also the draft-model file for
180
+ llama.cpp's native speculative decoding against the 8B pack). The fork is
181
+ public at [fermionresearch/llama.cpp](https://github.com/fermionresearch/llama.cpp),
182
+ branch `fermion-fv5` (= upstream `ggml-org/llama.cpp` @ `d67c0b41` + the
183
+ FV5 patch; `gguf/fv5.patch` in this repo is the same change as a mailbox
184
+ patch):
185
+
186
+ ```bash
187
+ hf download fermionresearch/Neutrino-0.6B gguf/neutrino-0.6b-base-fv5.gguf --local-dir .
188
+ git clone https://github.com/fermionresearch/llama.cpp && cd llama.cpp
189
+ git checkout fermion-fv5
190
+ cmake -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=OFF -DGGML_METAL=OFF # no FV5 Metal kernel yet: CPU path
191
+ cmake --build build -j --target llama-completion
192
+ ./build/bin/llama-completion -m ../gguf/neutrino-0.6b-base-fv5.gguf \
193
+ -p "2, 3, 5, 7, 11," -n 128 --temp 0 -no-cnv
194
+ ```
195
+
196
+ **5. MLX pack** (Apple-silicon runtime with custom Metal kernels). Run it
197
+ from the `mlx/` folder of the downloaded repo — the `fermion_mlx` package
198
+ and its tokenizer files ship there, and `requirements.txt` installs `mlx`
199
+ itself:
200
+
201
+ ```bash
202
+ cd Neutrino-0.6B/mlx # the directory downloaded in step 2
203
+ pip install -r requirements.txt
204
+ python -m fermion_mlx --model ../neutrino-0.6b_v4.bin --mode gen \
205
+ --tokenizer . --prompt "2, 3, 5, 7, 11,"
206
+ ```
207
+
208
+ ## Recommended settings, by use case
209
+
210
+ | use case | settings | why |
211
+ |---|---|---|
212
+ | **draft model for Neutrino-8B** | **inherit the verifier — set nothing** | The draft's sampler cannot change *what* is emitted. Under greedy the verifier's own argmax decides acceptance; under HF-assisted sampling the correction re-weights by `p_i/q_i`. Tuning the draft is a throughput knob, never a quality knob. |
213
+ | **benchmark / completion reproduction** | greedy, no penalty, no top-k — this repo's shipped `generation_config.json` | Everything in the Evaluation section was measured this way, so a bare `model.generate()`, an `lm-eval` run and `fermion generate` all reproduce each other. |
214
+ | **user-facing chat** | use [Neutrino-0.6B-Chat](https://huggingface.co/fermionresearch/Neutrino-0.6B-Chat) instead | This SKU has no chat post-training; every graded conversational number in the family belongs to the -Chat container. |
215
+
216
+ `generation_config.json` here is `do_sample: false`, no `temperature`, no
217
+ `top_k`, no `top_p`, no `repetition_penalty` — deliberately empty of sampler
218
+ opinion, so nothing silently overrides a harness. Top-k is not exposed on any
219
+ runtime we ship; note that llama.cpp and ollama default to top-k 40, which is
220
+ why the `Modelfile` in `gguf/` pins it to 0.
221
+
222
+ ## Speed
223
+
224
+ | Surface | Machine | Rate | Memory / protocol |
225
+ |---|---|---:|---|
226
+ | native `fermion-run`, CPU only | Apple M5 | **225-236 tok/s** | 9 threads, 128-token greedy, mmap; per-prompt 232.89 / 224.52 / 233.56, demo median 225.37 (`receipts/m5_demo_rereceipt_p3.json`) |
227
+ | MLX 0.31.2 + Metal | Apple M5, 16 GB | **201.16 tok/s** | median of 5 (197.19-202.03), peak 0.526 GiB, 512-token greedy, 9-token prompt (`mlx/receipts/bench_0p6b_m5.json`) |
228
+ | GGUF pack via `fermion-fv5` | CPU, 16 threads | **55.38 tok/s** tg128 (+/-0.30) | pp512 132.18, build e07b3ec (`gguf/receipts/bench_0p6b.json`) |
229
+ | C-served draft walker inside the 8B's process | H100 | **1,177 tok/s** | 0.85 ms/token standalone, graph == eager on a 24-token identity |
230
+
231
+ ## Evaluation battery (this exact container, `91c0b26d...`)
232
+
233
+ Measured on the shipped checkpoint with frozen,
234
+ receipted meters. Stack pins: lm-eval **0.4.8**, datasets 3.6.0,
235
+ transformers 4.53.2, grade_probe `b279c53e`; generative rows evalscope
236
+ **1.4.2**; BFCL wheel **bfcl-eval 2025.10.27.1**. Canonical roll-up:
237
+ `receipts/ship_checkpoint_numbers.json`.
238
+
239
+ A 596M model at this storage depth reads LOW on knowledge-adjacent meters.
240
+ That is expected, disclosed, and the reason this SKU's story is size,
241
+ speed, and drafting rather than capability.
242
+
243
+ ### Knowledge and likelihood
244
+
245
+ | Meter | Score | Protocol |
246
+ |---|---:|---|
247
+ | ARC-easy | **53.45** | lm-eval suite leg, 0-shot |
248
+ | Winogrande | 53.43 | same |
249
+ | PIQA | 62.79 | same |
250
+ | BoolQ | 50.52 | same |
251
+ | HellaSwag | 31.64 | same |
252
+ | ARC-challenge | 24.06 | same |
253
+ | OpenBookQA | 19.60 | same |
254
+ | **Suite / commonsense average (7 tasks)** | **42.21** | mean of the seven rows above, 0-shot |
255
+ | MMLU full-57 | 26.66 | likelihood, 14,042 items (near chance; the sub-7B knowledge-conversion floor is a banked house finding) |
256
+ | MMLU subset-14 (frozen gate probe) | 27.50 | kprobe, `mmlu_subset 0.25`, seed 0, 14 named subtasks |
257
+ | SciQ | 80.40 | 0-shot acc, lm-eval 0.4.8 (chance 25.0) |
258
+ | COPA | 63.00 | 0-shot acc, lm-eval 0.4.8, n=100 (chance 50.0) |
259
+ | MNLI, house meter | 35.45 | `e4_mnli_bitnet_parity`, label-token likelihood, validation_matched, full 9,815 (chance 33.3) |
260
+ | MNLI, lm-eval stock | 34.18 | 0-shot acc, lm-eval 0.4.8. Not comparable to the house-meter row above: the two formulations differ, and both readings sit within 2 pp of the 3-class floor |
261
+ | LAMBADA (openai) | 25.79 | 0-shot acc, lm-eval 0.4.8. Long-range last-word prediction is the capability that thins first at this scale, and it is the mechanical explanation for the GSM8K and termination cells below |
262
+
263
+ fp32 reference constants for context, not re-run here: MMLU 46.9, suite
264
+ 53.5.
265
+
266
+ ### Generation, perplexity, behaviour
267
+
268
+ | Meter | Score | Protocol |
269
+ |---|---:|---|
270
+ | C4 perplexity | **46.25** | 500,000 tokens, C4-val, fp32-bake meter, wall 1,329.9 s |
271
+ | GSM8K flexible-extract | 1.67 | lm-eval `gsm8k`, 0-shot generative, first-N fixed subset limit=300, batch 8 |
272
+ | GSM8K strict-match | 0.00 | same run |
273
+ | GSM8K stated-format (house grader) | 1.67 | second greedy pass, `max_gen_toks=1024`, n=300 |
274
+ | Termination rate | 0.23 | fraction of generations ending on EOS/stop within budget, GSM8K stated pass, cap 1024, n=300 |
275
+ | IFEval prompt-strict | 14.88 | house IFEval leg, evalscope 1.4.2 (base model, no instruction tuning) |
276
+ | BFCL v3 macro-13 | 15.38 | bfcl-eval 2025.10.27.1; degenerate never-calls profile (irrelevance 100.0 + live_irrelevance 100.0, all 10 calling subsets 0.0). **Do NOT use this model for function calling.** |
277
+ | genfmt QA (house) | match 0.62 / well-formed 0.80 | 50-row house QA probe, `genfmt_max_new: 32` |
278
+
279
+ Against its full-precision reference at the identical parameter count
280
+ (596,049,920 on both sides), every cell graded by us on one harness on one
281
+ day at zero shots: **81.6% of the reference's ability across the seven
282
+ common tasks, at 15.8% of the bytes** (238 MB against 1,503 MB), and
283
+ **78.0% averaged across all eleven tasks measured**. Best-retaining axes:
284
+ Winogrande 91.5%, PIQA 90.4%. Rig, pins, revision gates, and the disclosed
285
+ deviations (A10G rather than A100, per-row batch size) are receipted in
286
+ the eval-receipts repo. This supersedes the earlier single-task ARC-easy read, which was one
287
+ cell against a differently-sourced reference number.
288
+
289
+ The SciQ, COPA, LAMBADA, and stock-MNLI cells were measured on this
290
+ container on 2026-07-27 (A100-80GB, lm-eval 0.4.8, fp32-baked TritLinear
291
+ path, same pins and bake as the suite above; run receipt
292
+ in the eval-receipts repo, `grade_probe.py` sha `b279c53e...`,
293
+ state sha gate PASS). Read them as absolute capability
294
+ evidence and as a same-family comparison against our own 8B on the identical
295
+ tasks (95.90 / 84.00 / 56.65 / 54.57). They are **not** competitive claims:
296
+ a same-harness sub-1B cohort grid run on 2026-07-27
297
+ (7 rival checkpoints x 11 tasks,
298
+ 77 cells) measures this model at or near the bottom of its cohort on most of
299
+ those tasks. That grid is the calibrated read; use it rather than inferring
300
+ a ranking from the absolute cells above.
301
+
302
+ Not measured at this scale: MMLU-Redux (an 8B-line generative meter),
303
+ wikitext perplexity (the house perplexity meter is C4-val only), and
304
+ IFEval's loose/instruction variants (only prompt-strict was promoted from
305
+ the raw report).
306
+
307
+ ### Correctness gates
308
+
309
+ - MLX refgate vs `neutrino_ref.npz`: teacher-forced rel err max 1.74e-06,
310
+ argmax 9/9, greedy 24/24, **0 flips**; cachegate 0/256
311
+ (`mlx/receipts/refgate_0p6b.json`, `cachegate_0p6b.json`).
312
+ - GGUF convert bit-compare: **310/310 CONVERT_PASS**
313
+ (`gguf/receipts/convert_0p6b.json`).
314
+ - C-runtime bit gate: 221/384 raw positions as measured, diagnosed as
315
+ exactly 2 flat-margin argmax flips then greedy cascade (top-2 margins
316
+ 0.02-0.20 logits); kernel output is deterministic across runs and
317
+ threads (`receipts/m5_bitgate_analysis.json`).
318
+ - Export: double-export byte-determinism PASS, tv4z byte round-trip PASS,
319
+ fp32 HF expansion self-check PASS (rc=0) (`receipts/receipt_base.json`).
320
+
321
+ ### Reproducing the battery
322
+
323
+ The model registers as a native `transformers` model (`import fermion`),
324
+ so the public harnesses run directly.
325
+
326
+ ```python
327
+ # first: hf download fermionresearch/Neutrino-0.6B --local-dir Neutrino-0.6B (Quickstart 2)
328
+ # lm-eval 0.4.12 needs accelerate for HFLM: pip install lm-eval accelerate
329
+ import fermion, lm_eval
330
+ from lm_eval.models.huggingface import HFLM
331
+ from transformers import AutoModelForCausalLM, AutoTokenizer
332
+
333
+ model = AutoModelForCausalLM.from_pretrained("Neutrino-0.6B") # the downloaded directory
334
+ tok = AutoTokenizer.from_pretrained("Neutrino-0.6B")
335
+ model.config.tie_word_embeddings = False # tied inside the packed container; skip HF's re-tie
336
+ lm = HFLM(pretrained=model, tokenizer=tok)
337
+ lm_eval.simple_evaluate(model=lm, num_fewshot=0, tasks=[
338
+ "arc_easy", "arc_challenge", "boolq", "hellaswag",
339
+ "openbookqa", "piqa", "winogrande"]) # the suite-average row
340
+ lm_eval.simple_evaluate(model=lm, tasks=["mmlu"], num_fewshot=5)
341
+ lm_eval.simple_evaluate(model=lm, tasks=["gsm8k"], num_fewshot=0, limit=300)
342
+ ```
343
+
344
+ IFEval and BFCL used the same frozen house meters as the 8B card, against
345
+ a vLLM OpenAI-compatible endpoint:
346
+
347
+ ```bash
348
+ pip install evalscope==1.4.2 bfcl-eval==2025.10.27.1
349
+ evalscope eval --model Neutrino-0.6B --api-url http://127.0.0.1:8000/v1 \
350
+ --datasets ifeval bfcl_v3
351
+ ```
352
+
353
+ Speed is re-measured with the shipped binary and the MLX pack:
354
+
355
+ ```bash
356
+ ./bin/fermion-run-macos-arm64 neutrino-0.6b_v4.bin "1,791,1283" 128 9 # from the repo dir (Quickstart 3)
357
+ python -m fermion_mlx --model ../neutrino-0.6b_v4.bin --mode bench \
358
+ --tokenizer . --gen-tokens 512 --runs 5 # from mlx/ (Quickstart 5)
359
+ ```
360
+
361
+ Per-cell venue, version, and date receipts are in the eval-receipts repo.
362
+
363
+ ## As the 8B's draft (the reason this SKU exists)
364
+
365
+ A 0.6B draft proposes k tokens; the 8B verifies all k in one batched
366
+ forward pass and keeps the accepted prefix plus one token of its own. A
367
+ proposal is accepted only when it equals the 8B's own argmax, so the
368
+ output stream is the plain greedy stream: **speed changes, the text does
369
+ not.**
370
+
371
+ ### The exactness certificate
372
+
373
+ | Suite | Arms | Tokens compared | Result |
374
+ |---|---|---:|---|
375
+ | Ship-brain cert (8B `016c6f36...5fa0`) | dynamic draft length, draft = Neutrino-0.6B + confidence head | **27,648** | **0 mismatches** (re-run 2026-07-27 against the shipping 8B container) |
376
+ | dynamic-k cert (earlier internal 8B baseline verifier) | dynamic + k in {2..6} x 9 prompts x 512 | 27,648 | PASS, all IDENTICAL |
377
+ | **Story leg: this base container as the draft, no distillation, no confidence head** | k in {3,4,5} x 9 x 512 | **13,824** | **PASS, all IDENTICAL** |
378
+ | forced-reject floors (both drafts) | k=1 dynamic / k in {3,5} fixed | 256 each | output EXACT |
379
+ | **Lane total** | - | **41,472** | **zero mismatches anywhere** |
380
+
381
+ Venue: Modal H100, 2026-07-26. The certification receipt ships in the
382
+ Neutrino-8B repo (`receipts/`).
383
+
384
+ ### Speedup by prompt class (ship brain, H100, 3-round median, baseline 395.9 tok/s)
385
+
386
+ | Class | Speedup | tok/s | Acceptance behaviour |
387
+ |---|---:|---:|---|
388
+ | counting | **x1.93** | 762.6 | 100% of drafted tokens accepted (k=6 probe: all 6 survive every verify pass, 74 passes per 512 tokens, ~7 tokens emitted per 8B forward) |
389
+ | facts | x1.55 | 613.0 | 96.5% survive (5.8 of 6 per pass) |
390
+ | prose | x1.34 | 531.9 | free-form narrative |
391
+ | chat explanation | x1.13 | 446.6 | the controller often disables drafting and pays a small resync cost |
392
+ | code | x1.07 | 426.1 | hardest class for the draft |
393
+
394
+ Acceptance decays with draft length everywhere except facts (97-99% flat),
395
+ which is why the controller rides long drafts on facts and explanation
396
+ (histogram mass at k=7), drops to 1-2-token drafts on code and counting,
397
+ and turns itself off on chat explanation.
398
+
399
+ ### Out of the box, without distillation
400
+
401
+ This shipped base container was never trained for drafting and carries no
402
+ confidence head, and it still certifies: 13,824 tokens, all identical,
403
+ run as a fixed-k draft. Bench medians on H100: k=4 facts **610.8 (x1.61)**,
404
+ k=5 facts **648.9 (x1.71)**, k=5 explanation/mmlu x1.54, prose k=4-5
405
+ x1.29-1.40. Fixed-k has no disable machinery, so its floor classes drop
406
+ below 1.0 (x0.46 at k=3, x0.34 at k=5); the dynamic controller with the
407
+ distilled draft floors at x0.75. The distillation then buys +10 to +25
408
+ percentage points of acceptance (prose 93.6 against ~70-77, code 85
409
+ against ~50) plus the confidence head and dynamic-k layer.
410
+
411
+ ### On Apple silicon
412
+
413
+ The pair runs in one process on a 16 GB M5 under a 6 GiB MLX cap (peak 4.3
414
+ GiB; the draft alone peaks at 0.526 GiB). Exactness holds: the gate returns
415
+ **6/6 prompts token-identical with and without `--spec`**
416
+ (`mlx/receipts/specgate_8b_0p6b.json` in the Neutrino-8B repo). Throughput on Apple
417
+ is class-dependent: **facts 22.00 -> 25.71 tok/s (x1.168)** at 0.7442
418
+ acceptance, while **chat (x0.837) and prose (x0.680) regress** because the
419
+ batched verify is compute-bound at ~2.8x a single step, so break-even
420
+ needs about 2.9 accepted tokens per round and the base draft's acceptance
421
+ on those classes is 0.30-0.44. A code class was never measured on MLX. Use `--spec` on
422
+ Apple for factual workloads; an FV5 sgemm path is the wired follow-up.
423
+
424
+ ### Draft-model artifact pins
425
+
426
+ - `neutrino_v4.bin` (this shipping base container, under its
427
+ speculative-decoding working name): sha `91c0b26d...19399`, 327,719,836 B
428
+ - the distilled dspec draft and its confidence head are separate
429
+ artifacts, not published here
430
+
431
+ ## Weights: get them and verify them
432
+
433
+ The container ships in this repository. Fetch it and check the digests —
434
+ `MANIFEST.json` carries the same sha256, and `fermion info` verifies it for
435
+ you automatically:
436
+
437
+ ```bash
438
+ hf download fermionresearch/Neutrino-0.6B neutrino-0.6b_v4.bin --local-dir .
439
+ shasum -a 256 neutrino-0.6b_v4.bin # must print 91c0b26d...19399
440
+ md5 neutrino-0.6b_v4.bin # macOS — must print 86565bf6183e8031943aac9fd9771385
441
+ md5sum neutrino-0.6b_v4.bin # Linux — same digest
442
+ ```
443
+
444
+ Export provenance: exported on a Modal CPU container (cpu=8, 24 GiB) from
445
+ the archived QAT state with the banked exporter (`export_trit_c_model.py`
446
+ sha `78bd8b8b...`, pinned command and flags recorded in the export receipt;
447
+ quantizer parameters are not published). Gates: double-export
448
+ byte-determinism PASS, tv4z byte round-trip PASS, HF expansion self-check
449
+ PASS. Full receipt: `receipts/receipt_base.json`; greedy-cache trajectory
450
+ `receipts/hf_greedy_cache_86565bf6.json`.
451
+
452
+ ## Limitations
453
+
454
+ - BASE model: no chat template behavior, no instruction-following
455
+ guarantees, no tool use (see the battery above).
456
+ - Knowledge-adjacent meters are at or near floor by construction (596M in
457
+ a ternary-family format; the sub-7B knowledge-conversion floor is a
458
+ banked house finding). Do not quote the MMLU or GSM8K cells as
459
+ competitive benchmarks.
460
+ - BFCL v3 measures a degenerate never-calls profile. This model has no
461
+ function-calling capability and none is claimed.
462
+ - Greedy decode parity vs the fp reference implementation is 382/384
463
+ positions on the banked gate prompts: the int8-activation runtime and
464
+ the fp expansion disagree at 2 near-tie argmax positions (top-2 margins
465
+ 0.02-0.20 logits; full analysis in `receipts/m5_bitgate_analysis.json`).
466
+ Runtime output is deterministic.
467
+ - On Apple silicon, speculative decoding against the 8B is a win on
468
+ factual prompts and a regression on chat and prose (see the section
469
+ above). The H100 path is a win on all five measured classes.
470
+ - The GGUF pack loads only through our llama.cpp fork until upstreaming
471
+ lands; stock llama.cpp, ollama, and LM Studio binaries do not know its
472
+ tensor types.
473
+ - Knowledge cutoff and multilingual behavior inherit from the base model
474
+ plus the disclosed feed diet (see eval-receipts).
475
+
476
+ ## Model lineage and methodology
477
+
478
+ Lineage in one line: base model (see frontmatter) -> ternary QAT on the
479
+ streamed small-model diet corpus -> exported with the banked
480
+ exporter, gates in the export receipt above. The QAT recipe, the diet
481
+ disclosure, and every graded cell
482
+ (venue, version, date) live in the eval-receipts repo; the format and
483
+ engine write-ups are at fermionresearch.com/research/.
484
+
485
+ ## License and attribution
486
+
487
+ Weights: Apache-2.0. Derivative of Qwen/Qwen3-0.6B (Apache-2.0, Alibaba
488
+ Cloud), see `LICENSE`. Training data: the small-model diet corpus
489
+ (streamed realization) disclosed in the eval-receipts repo.
490
+
491
+ `bin/` binaries: shipped in this repo under `bin/` with sha256 sidecars —
492
+ the same generic TRTC v4 runtimes bundled with Neutrino-8B (same EULA:
493
+ free to use with these weights, no redistribution, no reverse
494
+ engineering).
495
+
496
+ Contact: [email protected]
bin/MANIFEST.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "note": "bin/ staging manifest: blessed fermion-run release binaries. Entries mirror the .sha256 sidecars. The binaries are container-agnostic (any TRTC v4 arch-3 file).",
3
+ "binaries": {
4
+ "fermion-run-macos-arm64": {
5
+ "status": "RE-BLESSED 2026-07-28 (0.1.10 fp16-KV + runtime YaRN): macOS arm64 CPU runtime (NEON dotprod + persistent thread pool; libomp statically linked). Supersedes the 0.1.8 build: adds --kv-dtype fp16 (KV bytes halved, fp32 compute over fp16 storage, opt-in because near-tie tokens can flip) and --yarn-factor (YaRN rope tables, long-context extension; factor 1.0 gated byte-identical to flag-off). With both flags absent, output gated BYTE-IDENTICAL to the superseded binary (ids + final logits).",
6
+ "bytes": 671656,
7
+ "sha256": "2b9902fc36bf59c50a67ee0c79ae38a259d066e6ebff764abbf05ff72017351c",
8
+ "sidecar": "fermion-run-macos-arm64.sha256",
9
+ "build_receipt": "internal reference"
10
+ },
11
+ "fermion-run-linux-x64": {
12
+ "status": "RE-BLESSED 2026-07-28 (0.1.10 fp16-KV + runtime YaRN): Linux x86-64 CPU runtime, AVX2 baseline (-march=x86-64-v3), fully static ELF. Supersedes the 0.1.8 build: adds --kv-dtype fp16 (KV bytes halved, fp32 compute over fp16 storage, opt-in because near-tie tokens can flip) and --yarn-factor (YaRN rope tables, long-context extension; factor 1.0 gated byte-identical to flag-off). With both flags absent, output gated BYTE-IDENTICAL to the superseded binary (ids + final logits) on a 24-core venue.",
13
+ "bytes": 1317208,
14
+ "sha256": "d3e1c3d7a7fe43e9dd4d6e16e75c0cd234bfc9866964284101d200f3da05310b",
15
+ "sidecar": "fermion-run-linux-x64.sha256",
16
+ "build_receipt": "internal reference"
17
+ },
18
+ "fermion-run-linux-cuda": {
19
+ "status": "COMING-SOON (NOT STAGED, no fake artifact): no standalone arch-3 (Qwen3) CUDA runtime binary exists yet. Working fallback documented in bin/README.md: pip fermion torch path with --device cuda.",
20
+ "bytes": null,
21
+ "sha256": null,
22
+ "sidecar": null
23
+ }
24
+ }
25
+ }
bin/README.md ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # bin/ — prebuilt `fermion-run` native runtimes
2
+
3
+ > MIRRORED COPY (2026-07-27): byte-identical
4
+ > binaries and sidecars to `fermionresearch/Neutrino-8B/bin/`. The runtime is
5
+ > container-agnostic (any TRTC v4 arch-3 file), so the same two binaries serve
6
+ > this repo's container; the examples below name **this** repo's container.
7
+
8
+ STATUS: **BLESSED BINARIES STAGED 2026-07-25**.
9
+ Binaries are prebuilt and closed-source (kernel source does not ship);
10
+ every binary has a `.sha256` sidecar. EULA line lives on the model card.
11
+ Functional gates ran on the interim 8B v4 cut; the runtime is
12
+ container-agnostic (any TRTC v4 arch-3).
13
+
14
+ ## What is here
15
+
16
+ | File | Platform / backend | State |
17
+ |---|---|---|
18
+ | `fermion-run-macos-arm64` | macOS arm64 (M1+), CPU — NEON dotprod + persistent thread pool, OpenMP statically linked, zero dylib deps beyond libSystem | BLESSED (functional-gated on the 8B v4 container) |
19
+ | `fermion-run-linux-x64` | Linux x86-64, CPU — AVX2 baseline (`-march=x86-64-v3`), fully static ELF, runs on any 2015+ x86-64 distro | BLESSED (functional-gated on the 8B v4 container) |
20
+ | `fermion-run-linux-cuda` | Linux x86-64, NVIDIA CUDA | **COMING SOON** — not staged; see fallback below |
21
+
22
+ Honesty notes, so nobody is surprised:
23
+
24
+ - The macOS binary is the **CPU** runtime. The Metal-optimized runtime for
25
+ this model's container format is a week-one follow-up (the existing Metal
26
+ dev builds target a different container lineage and cannot load this
27
+ model); when it lands it replaces this entry with its own receipts.
28
+ - The linux-x64 binary is built for the AVX2 **compatibility baseline**, not
29
+ the AVX-512-VNNI fast path the banked x86 speed receipts were measured on.
30
+ It is correct (bit-exact GEMV unit gate at build time) and self-contained;
31
+ a VNNI-optimized variant is a week-one candidate.
32
+ - CUDA fallback that works today: `pip install fermion-research` then
33
+ `fermion chat --model fermionresearch/Neutrino-8B --device cuda`
34
+ (reference torch path — correctness-first, not the speed path).
35
+
36
+ ## CLI (what the binaries actually take)
37
+
38
+ The binaries take **pre-tokenized ids**, not prompt strings — tokenization
39
+ lives in the `fermion` CLI (`pip install fermion-research`, the one door), which
40
+ downloads the model + the platform-matching binary from this repo and shells
41
+ out for the fast path. Manual use:
42
+
43
+ ```
44
+ ./fermion-run-<platform> <container.bin> "<id0,id1,...>" <n_new_tokens> <n_threads> \
45
+ [logits_out.bin] [--temp T] [--top-p P] [--rep-pen R] [--seed S] \
46
+ [--stop-id I0[,I1...]] [--min-p F] [--presence-pen F] [--freq-pen F] \
47
+ [--dry MULT [--dry-base B --dry-allow N]] [--pen-window N] \
48
+ [--eos-exempt-minp] [--eos-boost B:N] [--ppl]
49
+ ```
50
+
51
+ The container, the id list, the token count and the **thread count are
52
+ positionals, in that order**. There is no `--chat` and no `--threads`.
53
+
54
+ > [!IMPORTANT]
55
+ > **Sampler flags require `--temp > 0` to engage.** At `--temp 0`
56
+ > (pure greedy) the binary takes a fast argmax path and silently
57
+ > ignores `--rep-pen`, `--top-p`, and all other penalty flags. If
58
+ > you want penalties with near-deterministic output, use
59
+ > `--temp 0.01 --top-p 1.0 --seed 42`. (Found 2026-07-26; a
60
+ > runtime patch making this explicit is a week-one item.)
61
+ >
62
+ > **`--pen-window` is inert on the legacy sampled path.** With `--temp > 0`
63
+ > and none of `--min-p` / `--presence-pen` / `--freq-pen` / `--dry` present,
64
+ > the repetition-penalty window is clamped to the last **64** tokens
65
+ > regardless of what `--pen-window` says (`trit_run.c`: `rwin = g_ext ?
66
+ > max(g_penwin, 64) : 64`). Adding any one of those four flags switches on
67
+ > the modern penalty stack and makes `--pen-window` literal. Every graded
68
+ > C receipt was taken on the legacy path, so the banked C numbers are
69
+ > **64-window** numbers even where the command line reads
70
+ > `--pen-window 256`; the torch and MLX paths do apply 256. Documented
71
+ > 2026-07-27; making the flag
72
+ > literal on the legacy path is a week-one runtime patch.
73
+
74
+ Defaults: greedy decode; without `--stop-id` exactly `n_new_tokens` are
75
+ generated (EOS ignored); with `--stop-id` generation stops at the stop token
76
+ (coherence mode). Output: line 1 = generated ids (comma-separated), line 2 =
77
+ `tok/s: ...`. `n_new_tokens 0` = prefill-only logits dump.
78
+
79
+ Per-platform one-liners (chat-format ids for a user prompt, then generate up
80
+ to 64 tokens, stop at `<|im_end|>` = 151645):
81
+
82
+ ```bash
83
+ # ids for 'In one sentence, why is the sky blue?' via this repo's tokenizer:
84
+ IDS=$(python3 -c "
85
+ from tokenizers import Tokenizer
86
+ t = Tokenizer.from_file('tokenizer.json')
87
+ p = '<|im_start|>user\nIn one sentence, why is the sky blue?<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n'
88
+ print(','.join(map(str, t.encode(p).ids)))")
89
+
90
+ # macOS arm64:
91
+ ./bin/fermion-run-macos-arm64 neutrino-0.6b_v4.bin "$IDS" 64 8 --stop-id 151645
92
+ # Linux x86-64:
93
+ ./bin/fermion-run-linux-x64 neutrino-0.6b_v4.bin "$IDS" 64 8 --stop-id 151645
94
+ ```
95
+
96
+ macOS quarantine note: after download run
97
+ `xattr -d com.apple.quarantine bin/fermion-run-macos-arm64` once.
98
+
99
+ ## sha256 sidecar convention
100
+
101
+ Every binary `X` ships with `X.sha256` (standard `shasum` line):
102
+
103
+ ```bash
104
+ # producer (at staging time, from this directory):
105
+ shasum -a 256 fermion-run-macos-arm64 > fermion-run-macos-arm64.sha256
106
+ # consumer:
107
+ shasum -a 256 -c fermion-run-macos-arm64.sha256
108
+ ```
109
+
110
+ `MANIFEST.json` here mirrors the sidecars (file -> bytes + sha256 + build
111
+ provenance) and is regenerated
112
+ whenever binaries are added or replaced.
113
+
114
+ ## thread count: the runtime is safe above 16 threads again
115
+
116
+ > [!IMPORTANT]
117
+ > **Rebuilt 2026-07-27 (thread-safety fix).** The 2026-07-25 binaries
118
+ > corrupted memory above **16 threads**: the per-thread activation-quant
119
+ > scratch buffer inside the runtime was allocated for 16 workers while the
120
+ > OpenMP path indexed it by thread id with no clamp. On Linux x86-64 that was
121
+ > a SIGSEGV on the 8B and — worse — **exit code 0 with silently different,
122
+ > wrong tokens** on the 0.6B, on any box with 18 or more logical cores (the
123
+ > `fermion` CLI asked for `cpu_count - 1`). macOS arm64 never reached it
124
+ > because Apple silicon tops out at 16 logical cores.
125
+ >
126
+ > These binaries size that buffer from the live thread count, clamp the
127
+ > requested count at a hard ceiling, and are gated by an output-stability
128
+ > thread sweep that asserts
129
+ > **byte-identical generated ids** at 1/4/8/15/16/17/20/23/max threads rather
130
+ > than merely a zero exit code. The `fermion` package independently caps the
131
+ > count it will request at 16 (`fermion.native.THREAD_CAP`).
132
+
133
+ ## prefill: prompts are now processed as a batch
134
+
135
+ > [!NOTE]
136
+ > **Rebuilt 2026-07-27 (batched prefill, 0.1.6).** Earlier binaries ran the
137
+ > PROMPT token-by-token — one full GEMV pass over the weight stack per prompt
138
+ > token — so a long chat history prefilled at a fraction of decode speed and
139
+ > the stall grew with every turn. These binaries push prompt positions through
140
+ > each layer in chunks (weights are decoded once per chunk and applied to all
141
+ > positions from cache), measured ~4x faster prefill on Apple M-series and
142
+ > ~10x on Linux AVX2 at 300 prompt tokens, with **byte-identical output**:
143
+ > generated ids and final logits are gated bit-for-bit against the previous
144
+ > binaries across a prompt/(n_new) matrix, a chunk-size sweep and the full
145
+ > thread ladder.
146
+ > `TRIT_PREFILL_BATCH=0` in the environment restores the sequential prefill.
147
+
148
+ ## session mode: a resident runner with KV-prefix reuse
149
+
150
+ > [!NOTE]
151
+ > **Rebuilt 2026-07-28 (session mode, 0.1.7).** The runner now has a
152
+ > persistent mode: `<binary> model.bin "0" 0 <threads> --session [--ctx N]`
153
+ > keeps ONE process resident holding the mmap'd weights and a KV cache, takes
154
+ > requests on stdin (full prompt ids per request) and prefills only the part
155
+ > of the prompt it has not seen before (longest-common-prefix against the
156
+ > cached token history). `fermion serve` and `fermion chat` (pip 0.1.7) drive
157
+ > this automatically; an agent step over a fixed ~2k-token preamble drops from
158
+ > ~94 s to ~1.3 s on the 8B (24-core Linux) because the preamble prefill and
159
+ > the per-request process start are both paid once. Output is gated
160
+ > **byte-identical** to a one-shot invocation of the same full prompt — greedy
161
+ > and fixed-seed sampled, including reuse after a divergent history and across
162
+ > the full thread ladder to 40 threads. One-shot invocations are gated
163
+ > byte-identical (ids + final logits) to the previous 0.1.6 binaries. A
164
+ > mid-decode `C\n` on stdin cancels the current request; `R\n` resets the
165
+ > cache; the KV capacity is `--ctx` positions (RAM scales linearly with it).
166
+
167
+ ## 0.1.8: multi-slot positional KV reuse + tool-name aliasing
168
+
169
+ > [!NOTE]
170
+ > **Rebuilt 2026-07-28 (0.1.8).** `--session` now
171
+ > tracks KV validity per (position, content) across up to 4 logical
172
+ > sequences instead of one longest-common-prefix history. An interleaved
173
+ > divergent request (agent frameworks fire short utility prompts — titles,
174
+ > status — into the same server between steps) no longer destroys the warm
175
+ > conversation cache: only the positions it physically overwrites are
176
+ > re-prefilled when the conversation returns (measured on the Hermes shape:
177
+ > re-prefill 4,122 -> 177 tokens, turn wall 50 s -> 2.5 s on an M5).
178
+ > Reuse stays EXACT: a cached position is reused only when it was computed
179
+ > under a prefix identical to the new prompt — a mutated head therefore
180
+ > still (correctly) invalidates everything after it, which no exact cache
181
+ > can avoid. Output is gated byte-identical to one-shot decodes across
182
+ > fresh/extended/mutated-head/mutated-middle/eviction/interleave shapes,
183
+ > chunk sweeps, the sequential path, and the N4 thread ladder, on both
184
+ > models and both platforms; one-shot output is gated byte-identical (ids +
185
+ > final logits) to the 0.1.7 binaries. The `ACK` line gains a `segs=<n>`
186
+ > field (number of prefill runs); the protocol is otherwise unchanged and
187
+ > 0.1.7 clients work as-is.
188
+
189
+ ## 0.1.10: fp16 KV cache (--kv-dtype) + runtime YaRN (--yarn-factor)
190
+
191
+ > [!NOTE]
192
+ > **Rebuilt 2026-07-28 (0.1.10).** Two additive
193
+ > runtime flags; with both flags absent the binary is gated BYTE-IDENTICAL
194
+ > (ids + final logits) to the 0.1.8/0.1.9 pair on both platforms.
195
+ >
196
+ > `--kv-dtype fp16` stores the session/decode KV cache in IEEE fp16 and
197
+ > computes attention in fp32 over it: HALF the KV memory (288 -> 144
198
+ > KiB/token on the 8B; an 8192-position session drops ~2.3 -> ~1.15 GiB).
199
+ > Cached k/v values are rounded once (RNE) at the position they are
200
+ > produced, identically on every code path and platform, so the mode is
201
+ > deterministic and thread-count-stable -- but near-tie tokens CAN resolve
202
+ > differently than fp32 (a small measured flip rate). fp16 is
203
+ > therefore OPT-IN; the default stays fp32 and byte-exact.
204
+ >
205
+ > `--yarn-factor F` builds YaRN rope tables at load (the transformers
206
+ > `_compute_yarn_parameters` twin, double precision; derived defaults
207
+ > orig_max = model max_pos, attn_factor = 0.1*ln(F)+1, beta 32/1, all
208
+ > overridable), extending usable context toward F x 40,960 positions.
209
+ > `--yarn-factor 1.0` is the identity (byte-identical to flag-off).
210
+ > Teacher-forced logits beyond position 40,960 are gated against
211
+ > transformers' own YaRN on the same checkpoint; long-context retrieval
212
+ > quality of the ternary brains is EXPERIMENTAL -- validate retrieval
213
+ > quality for your workload before advertising a
214
+ > context length. KV memory grows linearly with context (a 61,440-token
215
+ > fp16 session on the 8B is ~8.4 GiB): pair long contexts with
216
+ > `--kv-dtype fp16` and mind the printed KV arithmetic.
bin/fermion-run-linux-x64 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d3e1c3d7a7fe43e9dd4d6e16e75c0cd234bfc9866964284101d200f3da05310b
3
+ size 1317208
bin/fermion-run-linux-x64.sha256 ADDED
@@ -0,0 +1 @@
 
 
1
+ d3e1c3d7a7fe43e9dd4d6e16e75c0cd234bfc9866964284101d200f3da05310b fermion-run-linux-x64
bin/fermion-run-macos-arm64 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2b9902fc36bf59c50a67ee0c79ae38a259d066e6ebff764abbf05ff72017351c
3
+ size 671656
bin/fermion-run-macos-arm64.sha256 ADDED
@@ -0,0 +1 @@
 
 
1
+ 2b9902fc36bf59c50a67ee0c79ae38a259d066e6ebff764abbf05ff72017351c fermion-run-macos-arm64
config.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "transformers_version": "5.14.1",
3
+ "architectures": [
4
+ "TrtcV4ForCausalLM"
5
+ ],
6
+ "output_hidden_states": false,
7
+ "return_dict": true,
8
+ "dtype": "auto",
9
+ "chunk_size_feed_forward": 0,
10
+ "is_encoder_decoder": false,
11
+ "id2label": {
12
+ "0": "LABEL_0",
13
+ "1": "LABEL_1"
14
+ },
15
+ "label2id": {
16
+ "LABEL_0": 0,
17
+ "LABEL_1": 1
18
+ },
19
+ "problem_type": null,
20
+ "_name_or_path": "",
21
+ "container": "neutrino-0.6b_v4.bin",
22
+ "row_chunk": 512,
23
+ "int8_row_chunk": 4096,
24
+ "model_type": "trtc_v4",
25
+ "output_attentions": false
26
+ }
generation_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token_id": 151643,
3
+ "do_sample": false,
4
+ "eos_token_id": [
5
+ 151645,
6
+ 151643
7
+ ],
8
+ "pad_token_id": 151643,
9
+ "transformers_version": "4.53.2"
10
+ }
gguf/Modelfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Neutrino-0.6B base — ollama Modelfile (requires ollama built against the
2
+ # fermion-fv5 llama.cpp fork; stock ollama cannot load the FV5 type)
3
+ FROM ./neutrino-0.6b-base-fv5.gguf
4
+ PARAMETER temperature 0.7
5
+ PARAMETER top_p 0.95
6
+ # base model: raw completion, no chat template
gguf/README.md ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Neutrino-0.6B — GGUF pack (llama.cpp fork `fermion-fv5`)
2
+
3
+ > [!IMPORTANT]
4
+ > **Neutrino-0.6B exists as the speculative-decoding DRAFT for
5
+ > Neutrino-8B** — that pairing is its supported purpose. It is a BASE
6
+ > model: not an assistant, no chat/instruction/tool claims. For a small
7
+ > conversational model use Neutrino-0.6B-Chat.
8
+
9
+ Run the [Neutrino-0.6B](../README.md) five-value container through the
10
+ **llama.cpp toolchain** (llama-cli, llama-server, llama-bench, and anything
11
+ built on top of them, including ollama built from source against the fork).
12
+
13
+ **Read this first — which door do you want?**
14
+
15
+ - **This GGUF pack is the compatibility door.** It exists so the
16
+ llama.cpp/ollama/LM Studio world can load and run our format with the
17
+ ecosystem's own tooling. It is CPU-backend, correctness-certified
18
+ (token-identical greedy vs the container's fp32 expansion), and *not*
19
+ tuned for speed.
20
+ - **The native binaries are the fast door.** `fermion-run` (shipped with
21
+ the Neutrino-8B repo) decodes the same container at ~225–236 tok/s on an
22
+ Apple M5 CPU. If you want speed, use the native runtime; if you want
23
+ llama.cpp-ecosystem integration, use this pack.
24
+
25
+ ## What's in the file
26
+
27
+ `neutrino-0.6b-base-fv5.gguf` stores the model in two Fermion GGML types
28
+ added by the fork (they do **not** exist in stock llama.cpp):
29
+
30
+ | type | used for | layout per 256-element block | bits/weight |
31
+ |---|---|---|---|
32
+ | `FV5` (43) | all 196 transformer linears | f32 `s_lo`, f32 `s_hi`, 3×32B bit-planes (`bp`,`bn`,`br`) | 3.25 |
33
+ | `FV5B` (44) | int8 embedding / lm_head rows | f32 row scale, 256×int8 | 8.125 |
34
+
35
+ Weights are `w = (bp − bn) · (br ? s_hi : s_lo)` — the exact five-value
36
+ {0, ±s_lo, ±s_hi} function of the TRTC v4 container. The converter copies
37
+ the container's bit-planes and f32 per-row dual scales verbatim (no
38
+ re-quantization anywhere), and bit-compares every tensor's dequantization
39
+ against the container's f32 expansion before writing. Norms stay f32.
40
+ Activations are never quantized at runtime (`vec_dot_type = F32`), so the
41
+ only numerical difference vs the fp32 expansion is float summation order.
42
+
43
+ ## Files
44
+
45
+ See `SHA256SUMS` in this folder for the authoritative digests of:
46
+
47
+ - `neutrino-0.6b-base-fv5.gguf` — the model (FV5/FV5B, ~490 MiB)
48
+ - `fv5.patch` / `fermion-fv5.bundle` — the fork as a single patch/bundle
49
+ on top of upstream `ggml-org/llama.cpp` @ `d67c0b41`, branch
50
+ `fermion-fv5`, commit `e07b3ec1`, docs in `FORK_NOTES.md`
51
+
52
+ ## Quickstart (build the fork, run the model)
53
+
54
+ ```bash
55
+ git clone https://github.com/fermionresearch/llama.cpp && cd llama.cpp
56
+ git checkout fermion-fv5
57
+ # or, from upstream + our patch:
58
+ # git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
59
+ # git checkout d67c0b4107112e4790774c3a8169e2e3eb24643b && git am fv5.patch
60
+ cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_NATIVE=ON -DLLAMA_CURL=OFF -DGGML_METAL=OFF
61
+ cmake --build build -j --target llama-completion
62
+
63
+ ./build/bin/llama-completion -m neutrino-0.6b-base-fv5.gguf \
64
+ -p "The key idea behind ternary neural networks is" -n 128 --temp 0 -no-cnv
65
+ ```
66
+
67
+ Build notes: the command above targets the `fermion-fv5` branch, which is
68
+ CPU/CUDA only — hence `-DGGML_METAL=OFF`. In this tree `llama-cli` is the
69
+ chat-first surface; `llama-completion` is the completion tool these
70
+ instructions are verified with. The 8B pack's README documents the full
71
+ backend matrix.
72
+
73
+ ### Apple GPU (Metal)
74
+
75
+ FV5 Metal kernels are on the `fermion-fv5-metal` branch. Build **without**
76
+ `-DGGML_METAL=OFF` and run with `-ngl 99`:
77
+
78
+ ```bash
79
+ git checkout fermion-fv5-metal
80
+ cmake -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=OFF
81
+ cmake --build build -j --target llama-completion llama-bench
82
+ ```
83
+
84
+ Measured on an Apple M5 (16 GB, machine idle, `llama-bench -r 3`, build
85
+ `181e4d8`), model resident 321.57 MiB:
86
+
87
+ | test | `-ngl 99` (Metal) | `-ngl 0` (same binary, CPU) | speedup |
88
+ |---|---|---|---|
89
+ | pp512 | **6776.42 ± 206.99 tok/s** | 845.46 ± 68.75 tok/s | ×8.0 |
90
+ | tg128 | **187.19 ± 1.89 tok/s** | 3.73 ± 0.01 tok/s | ×50.2 |
91
+
92
+ This is a **base** model (no chat template, no instruction tuning) — use
93
+ completion-style prompts and expect base-model behavior.
94
+
95
+ ### ollama (built against the fork)
96
+
97
+ Stock ollama binaries do not know the FV5 type. Build ollama from source
98
+ with its vendored llama.cpp replaced by the fork checkout, then:
99
+
100
+ ```
101
+ # Modelfile -- deterministic, matching this repo's generation_config.json
102
+ # and the `--temp 0` llama-cli line above. llama.cpp defaults would
103
+ # otherwise give you temperature 0.8 and top-k 40.
104
+ FROM ./neutrino-0.6b-base-fv5.gguf
105
+ PARAMETER temperature 0
106
+ PARAMETER top_k 0
107
+ PARAMETER repeat_penalty 1.0
108
+ # base model: no template, raw completion
109
+ ```
110
+
111
+ ```bash
112
+ ollama create neutrino-0.6b -f Modelfile
113
+ ollama run neutrino-0.6b "The key idea behind ternary neural networks is"
114
+ ```
115
+
116
+ LM Studio: point it at a llama.cpp server built from the fork
117
+ (`llama-server -m neutrino-0.6b-base-fv5.gguf`) and connect via the
118
+ OpenAI-compatible endpoint; the bundled engine cannot load FV5 directly.
119
+
120
+ ## As a speculative-decoding draft for Neutrino-8B (EXPERIMENTAL)
121
+
122
+ This 0.6B fv5 pack can draft for the Neutrino-8B fv5 pack via llama.cpp's
123
+ own built-in speculative decoding (`llama-speculative -m 8b.gguf -md
124
+ neutrino-0.6b-base-fv5.gguf --spec-draft-n-max 4 --temp 0`). Honesty note:
125
+ that acceptance rule is llama.cpp's own sampling-based speculation, not
126
+ our bit-gated certification — and on CPU it is currently a measured
127
+ honest null (slower than plain decode; the FV5 type has no batched-matmul
128
+ path yet, so verify doesn't amortize). Numbers, receipts, and the full
129
+ note live in the Neutrino-8B GGUF README. Certified-exact speculation
130
+ lives in the native runtime and the MLX pack's `--spec` mode.
131
+
132
+ ## Correctness & speed
133
+
134
+ - Gate: greedy streams (8 prompts × 128 tokens) from the fork's CPU
135
+ backend, checked token-by-token against the container's fp32 expansion
136
+ reference (free-running + teacher-forced). Verdict and any near-tie
137
+ argmax notes: `receipts/` in this folder.
138
+ - Speed (record only, not a goal): see `receipts/bench_*.json` for CPU
139
+ tok/s on the recorded container class. The native runtime is the fast
140
+ door; upstreaming + SIMD-fused `vec_dot` (NEON) are wired follow-ups.
141
+
142
+ ## License
143
+
144
+ **This folder is mixed-licence.**
145
+
146
+ - The **model pack** (`*.gguf`) is Apache-2.0, same as the parent model card
147
+ (`../LICENSE`; `../NOTICE` carries the Qwen3 attribution).
148
+ - Any **`fv5.patch` / fork source** is a derivative of ggml-org/llama.cpp and
149
+ is therefore **MIT**, upstream's licence — *not* Apache-2.0, and not
150
+ covered by the parent card's grant. See the fork repository's own
151
+ `LICENSE`.
gguf/SHA256SUMS ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Neutrino-0.6B GGUF pack — authoritative digests
2
+ # verify after download with `shasum -a 256 -c`.
3
+ dd9354c4b3cd2b34195b0270540e4fa5db3811e42b6e4bdbd57958e0027e9272 neutrino-0.6b-base-fv5.gguf
4
+ 81bf7be5eef18eb98ab4823596374c8dd6615151cf5615425973cb65942c0f73 fv5.patch
5
+ # source container (parent card): 91c0b26d105710e755ef1ec1b5607c00da5d5cbd048fd5584135fb6fc1e19399 neutrino-0p6b_v4_base.bin
6
+ # fork: ggml-org/llama.cpp @ d67c0b41 + fv5.patch = branch fermion-fv5, commit e07b3ec1 (tree f3067d90)
gguf/fv5.patch ADDED
@@ -0,0 +1,952 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From e07b3ec135c1f90c64584e0ea2dc983e47094ab9 Mon Sep 17 00:00:00 2001
2
+ From: Fermion Research <[email protected]>
3
+ Date: Sat, 25 Jul 2026 14:59:41 +0530
4
+ Subject: [PATCH] ggml: add Fermion FV5/FV5B five-value ternary types (CPU) +
5
+ fermion-greedy gate tool
6
+
7
+ FV5: 256-block, f32 dual per-row scales + 3 bit-planes (3.25 bpw), the GGUF
8
+ carrier for TRTC v4 trit records {0, +/-s_lo, +/-s_hi}. FV5B: f32 row scale +
9
+ int8 (8.125 bpw) for the container's embed/lm_head records. vec_dot against
10
+ raw F32 activations (no runtime activation quantization) so CPU numerics stay
11
+ in the same class as the f32 container expansion; blocks are produced offline
12
+ by the TRTC v4 -> GGUF converter (no quantize path). tools/fermion-greedy
13
+ certifies greedy token-identity vs the reference cache with teacher-forced
14
+ margin forensics.
15
+ ---
16
+ FORK_NOTES.md | 89 +++++++
17
+ ggml/include/ggml.h | 4 +-
18
+ ggml/src/ggml-common.h | 24 ++
19
+ ggml/src/ggml-cpu/ggml-cpu.c | 14 ++
20
+ ggml/src/ggml-cpu/ops.cpp | 2 +
21
+ ggml/src/ggml-cpu/quants.c | 133 +++++++++++
22
+ ggml/src/ggml-cpu/quants.h | 5 +
23
+ ggml/src/ggml-quants.c | 65 ++++++
24
+ ggml/src/ggml-quants.h | 3 +
25
+ ggml/src/ggml.c | 16 ++
26
+ gguf-py/gguf/constants.py | 5 +
27
+ gguf-py/gguf/quants.py | 44 ++++
28
+ include/llama.h | 1 +
29
+ src/llama-model-loader.cpp | 2 +
30
+ tools/CMakeLists.txt | 1 +
31
+ tools/fermion-greedy/CMakeLists.txt | 5 +
32
+ tools/fermion-greedy/fermion-greedy.cpp | 299 ++++++++++++++++++++++++
33
+ 17 files changed, 711 insertions(+), 1 deletion(-)
34
+ create mode 100644 FORK_NOTES.md
35
+ create mode 100644 tools/fermion-greedy/CMakeLists.txt
36
+ create mode 100644 tools/fermion-greedy/fermion-greedy.cpp
37
+
38
+ diff --git a/FORK_NOTES.md b/FORK_NOTES.md
39
+ new file mode 100644
40
+ index 0000000..0d4cfe2
41
+ --- /dev/null
42
+ +++ b/FORK_NOTES.md
43
+ @@ -0,0 +1,89 @@
44
+ +# Fermion `fermion-fv5` fork notes
45
+ +
46
+ +Branch `fermion-fv5` on top of upstream `ggml-org/llama.cpp` commit
47
+ +`d67c0b4107112e4790774c3a8169e2e3eb24643b` (2026-07-25). This fork adds two
48
+ +ggml weight types so llama.cpp can LOAD AND RUN Fermion Research's TRTC v4
49
+ +five-value ternary containers (Neutrino-0.6B, Neutrino-8B) converted to GGUF.
50
+ +CPU backend only in this cut. The graph is untouched: both models are stock
51
+ +Qwen3 geometry (`qwen3` arch, per-head Q/K RMSNorm, biasless qkv), which
52
+ +mainline already implements.
53
+ +
54
+ +GGUF is the *compatibility* door. The Fermion native runtime and its fused
55
+ +kernels remain the *fast* door; this fork optimizes for correctness parity
56
+ +with the container's deployed function, not for speed.
57
+ +
58
+ +## New types
59
+ +
60
+ +| ggml type | id | block | bytes/block | bpw | used for |
61
+ +|---|---|---|---|---|---|
62
+ +| `GGML_TYPE_FV5` | 43 | 256 | 104 (f32 `s_lo`, f32 `s_hi`, `bp[32]`, `bn[32]`, `br[32]`) | 3.25 | all attention/MLP linears |
63
+ +| `GGML_TYPE_FV5B` | 44 | 256 | 260 (f32 `s`, `int8 qs[256]`) | 8.125 | token_embd / output (TRTC int8 records) |
64
+ +
65
+ +Reconstruction semantics (identical to the container spec in
66
+ +`scripts/expand_trtc_v4_to_hf.py` of the research repo):
67
+ +
68
+ +- FV5: `w[j] = (bp[j] - bn[j]) * (br[j] ? s_hi : s_lo)` — five values per row
69
+ + `{0, ±s_lo, ±s_hi}`; the per-row dual scales are stored as exact f32 copies
70
+ + replicated into each 256-block; bit-planes keep the container's little bit
71
+ + order, so conversion is a plane slice, not a re-encode.
72
+ +- FV5B: `w[j] = s * qs[j]` with the exact f32 per-row scale.
73
+ +
74
+ +This is NOT BitNet TQ1_0/TQ2_0 (ternary, single fp16 block scale): five
75
+ +values + dual per-row scales do not map onto those types.
76
+ +
77
+ +## Numerics policy
78
+ +
79
+ +`vec_dot_type = GGML_TYPE_F32` for both types: activations are consumed RAW
80
+ +(no runtime Q8 quantization), and scales are f32, so every weight value seen
81
+ +by the CPU backend is bit-identical to the f32 expansion of the container.
82
+ +The only difference vs an f32 reference forward is summation order. The
83
+ +`llama-fermion-greedy` tool (below) exists to certify exactly that: greedy
84
+ +token streams vs the reference cache, with teacher-forced margin forensics
85
+ +for any argmax flip. KV cache is set to F32 in the gate tool.
86
+ +
87
+ +## Changed files
88
+ +
89
+ +- `ggml/include/ggml.h` — enum entries FV5=43, FV5B=44 (appended; no renumbering).
90
+ +- `ggml/src/ggml-common.h` — `block_fv5`, `block_fv5b` + `QK_FV5`.
91
+ +- `ggml/src/ggml.c` — type traits (`to_float`; no `from_float_ref`: blocks are
92
+ + produced offline by the TRTC v4 → GGUF converter, `llama-quantize` is
93
+ + intentionally NOT wired).
94
+ +- `ggml/src/ggml-quants.{c,h}` — reference `dequantize_row_fv5{,b}` +
95
+ + `ggml_validate_row_data` cases (validates scale finiteness + the fv5
96
+ + invariants `bp&bn==0`, `br⊆bp|bn`).
97
+ +- `ggml/src/ggml-cpu/quants.{c,h}` — `ggml_vec_dot_fv5_f32`,
98
+ + `ggml_vec_dot_fv5b_f32`: portable scalar with an AVX2 fast path inside
99
+ + (masked activation sums mirroring the research repo's NEON decode
100
+ + structure); no `arch/*/quants.c` or `arch-fallback.h` churn.
101
+ +- `ggml/src/ggml-cpu/ggml-cpu.c` — CPU traits (`vec_dot_type = F32`).
102
+ +- `ggml/src/ggml-cpu/ops.cpp` — `get_rows` cases (embedding lookup for FV5B).
103
+ +- `include/llama.h`, `src/llama-model-loader.cpp` — `LLAMA_FTYPE_MOSTLY_FV5`
104
+ + (=42) name/mapping for clean loader printouts.
105
+ +- `gguf-py/gguf/constants.py`, `gguf-py/gguf/quants.py` — python-side type
106
+ + registration + numpy dequantize (used by the converter's bitwise
107
+ + cross-check); float→FV5 quantize deliberately raises.
108
+ +- `tools/fermion-greedy/` — the correctness-gate tool (exact token-id
109
+ + prompts, free + teacher-forced greedy, per-flip logit margins, F32 KV).
110
+ +
111
+ +## Deliberate non-goals of this cut
112
+ +
113
+ +- No `llama-quantize` path (conversion is offline from TRTC v4 containers).
114
+ +- No CUDA/Metal/Vulkan kernels: those backends report the types unsupported
115
+ + and fall back to CPU. (Native GPU serving lives in the Fermion runtime.)
116
+ +- No repack/IMatrix/LoRA integration; training-path ops (`add`, `acc`,
117
+ + `out_prod`) on FV5 tensors abort as unsupported — inference never hits them.
118
+ +
119
+ +## Build
120
+ +
121
+ + cmake -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=OFF
122
+ + cmake --build build -j --target llama-cli llama-bench llama-fermion-greedy
123
+ +
124
+ +## Gate usage
125
+ +
126
+ + llama-fermion-greedy -m neutrino-0p6b.fv5.gguf \
127
+ + --prompts prompts_ids.txt --ref ref_ids.txt --steps 128 -t 16 \
128
+ + --out gate_receipt.json
129
+ +
130
+ +`forced_mismatch_total == 0` and `all_free_match == true` is the shipping
131
+ +gate; any nonzero flip must be margin-classified (near-tie class) in the
132
+ +release receipts.
133
+ diff --git a/ggml/include/ggml.h b/ggml/include/ggml.h
134
+ index 35f0c44..3958bca 100644
135
+ --- a/ggml/include/ggml.h
136
+ +++ b/ggml/include/ggml.h
137
+ @@ -430,7 +430,9 @@ extern "C" {
138
+ GGML_TYPE_NVFP4 = 40, // NVFP4 (4 blocks, E4M3 scale)
139
+ GGML_TYPE_Q1_0 = 41,
140
+ GGML_TYPE_Q2_0 = 42,
141
+ - GGML_TYPE_COUNT = 43,
142
+ + GGML_TYPE_FV5 = 43, // Fermion five-value ternary {0, +/-s_lo, +/-s_hi}: dual f32 scales + 3 bit-planes per 256-block
143
+ + GGML_TYPE_FV5B = 44, // Fermion int8 companion: f32 row scale + 256 int8 per block (TRTC v4 embed/lm_head records)
144
+ + GGML_TYPE_COUNT = 45,
145
+ };
146
+
147
+ // precision
148
+ diff --git a/ggml/src/ggml-common.h b/ggml/src/ggml-common.h
149
+ index 83f9118..5d3eb07 100644
150
+ --- a/ggml/src/ggml-common.h
151
+ +++ b/ggml/src/ggml-common.h
152
+ @@ -287,6 +287,30 @@ typedef struct {
153
+ } block_tq2_0;
154
+ static_assert(sizeof(block_tq2_0) == sizeof(ggml_half) + QK_K / 4, "wrong tq2_0 block size/padding");
155
+
156
+ +// Fermion five-value ternary (TRTC v4 trit records): w = (bp - bn) * (br ? s_hi : s_lo)
157
+ +// Scales are exact f32 copies of the container's per-row dual scales,
158
+ +// replicated into every block of the row. Bit-planes keep the container's
159
+ +// little bit order (bit i of byte j selects element 8*j + i).
160
+ +// 3.25 bpw
161
+ +#define QK_FV5 256
162
+ +typedef struct {
163
+ + float s_lo; // low-magnitude scale
164
+ + float s_hi; // high-magnitude scale
165
+ + uint8_t bp[QK_FV5/8]; // +1 plane
166
+ + uint8_t bn[QK_FV5/8]; // -1 plane (disjoint from bp)
167
+ + uint8_t br[QK_FV5/8]; // hi-magnitude selector (subset of bp|bn)
168
+ +} block_fv5;
169
+ +static_assert(sizeof(block_fv5) == 2 * sizeof(float) + 3 * (QK_FV5/8), "wrong fv5 block size/padding");
170
+ +
171
+ +// Fermion int8 rows (TRTC v4 embed/lm_head records): w = s * q
172
+ +// s is the exact f32 per-row scale, replicated into every block of the row.
173
+ +// 8.125 bpw
174
+ +typedef struct {
175
+ + float s; // row scale
176
+ + int8_t qs[QK_FV5];
177
+ +} block_fv5b;
178
+ +static_assert(sizeof(block_fv5b) == sizeof(float) + QK_FV5, "wrong fv5b block size/padding");
179
+ +
180
+ //
181
+ // Super-block quantization structures
182
+ //
183
+ diff --git a/ggml/src/ggml-cpu/ggml-cpu.c b/ggml/src/ggml-cpu/ggml-cpu.c
184
+ index 491316f..db1679e 100644
185
+ --- a/ggml/src/ggml-cpu/ggml-cpu.c
186
+ +++ b/ggml/src/ggml-cpu/ggml-cpu.c
187
+ @@ -409,6 +409,20 @@ static const struct ggml_type_traits_cpu type_traits_cpu[GGML_TYPE_COUNT] = {
188
+ .vec_dot_type = GGML_TYPE_Q8_K,
189
+ .nrows = 1,
190
+ },
191
+ + [GGML_TYPE_FV5] = {
192
+ + // no from_float: FV5 tensors are produced offline (TRTC v4 -> GGUF)
193
+ + // vec_dot_type F32: activations are used raw (no runtime quantization),
194
+ + // keeping llama.cpp numerics in the same class as the f32 expansion
195
+ + // reference the Fermion correctness gate compares against.
196
+ + .vec_dot = ggml_vec_dot_fv5_f32,
197
+ + .vec_dot_type = GGML_TYPE_F32,
198
+ + .nrows = 1,
199
+ + },
200
+ + [GGML_TYPE_FV5B] = {
201
+ + .vec_dot = ggml_vec_dot_fv5b_f32,
202
+ + .vec_dot_type = GGML_TYPE_F32,
203
+ + .nrows = 1,
204
+ + },
205
+ [GGML_TYPE_I32] = {
206
+ .from_float = (ggml_from_float_t) ggml_cpu_fp32_to_i32,
207
+ },
208
+ diff --git a/ggml/src/ggml-cpu/ops.cpp b/ggml/src/ggml-cpu/ops.cpp
209
+ index 42ec809..4d4ab0c 100644
210
+ --- a/ggml/src/ggml-cpu/ops.cpp
211
+ +++ b/ggml/src/ggml-cpu/ops.cpp
212
+ @@ -5038,6 +5038,8 @@ void ggml_compute_forward_get_rows(
213
+ case GGML_TYPE_Q6_K:
214
+ case GGML_TYPE_TQ1_0:
215
+ case GGML_TYPE_TQ2_0:
216
+ + case GGML_TYPE_FV5:
217
+ + case GGML_TYPE_FV5B:
218
+ case GGML_TYPE_IQ2_XXS:
219
+ case GGML_TYPE_IQ2_XS:
220
+ case GGML_TYPE_IQ3_XXS:
221
+ diff --git a/ggml/src/ggml-cpu/quants.c b/ggml/src/ggml-cpu/quants.c
222
+ index 5e36459..731c01f 100644
223
+ --- a/ggml/src/ggml-cpu/quants.c
224
+ +++ b/ggml/src/ggml-cpu/quants.c
225
+ @@ -562,6 +562,139 @@ void ggml_vec_dot_tq2_0_q8_K_generic(int n, float * GGML_RESTRICT s, size_t bs,
226
+ *s = sumf;
227
+ }
228
+
229
+ +// ====================== Fermion five-value ternary (FV5 / FV5B)
230
+ +//
231
+ +// vec_dot against RAW F32 activations (vec_dot_type == GGML_TYPE_F32): the
232
+ +// activations are never quantized, so the only difference vs a full f32
233
+ +// matmul with the expanded weights is summation order. Weights per block:
234
+ +// w[j] = (bp[j] - bn[j]) * (br[j] ? s_hi : s_lo)
235
+ +// so the block dot factors into four masked activation sums:
236
+ +// dot = s_lo * (sum_lo_p - sum_lo_n) + s_hi * (sum_hi_p - sum_hi_n)
237
+ +// This mirrors the sign/masked-accumulate structure of the reference NEON
238
+ +// decode (kernels/neon_war5_lab.c) in portable form.
239
+ +
240
+ +#if defined(__AVX2__)
241
+ +static inline float ggml_fv5_hsum_ps(__m256 v) {
242
+ + __m128 lo = _mm256_castps256_ps128(v);
243
+ + __m128 hi = _mm256_extractf128_ps(v, 1);
244
+ + lo = _mm_add_ps(lo, hi);
245
+ + lo = _mm_add_ps(lo, _mm_movehl_ps(lo, lo));
246
+ + lo = _mm_add_ss(lo, _mm_shuffle_ps(lo, lo, 0x55));
247
+ + return _mm_cvtss_f32(lo);
248
+ +}
249
+ +#endif
250
+ +
251
+ +void ggml_vec_dot_fv5_f32(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) {
252
+ + assert(nrc == 1);
253
+ + assert(n % QK_FV5 == 0);
254
+ + UNUSED(nrc);
255
+ + UNUSED(bx);
256
+ + UNUSED(by);
257
+ + UNUSED(bs);
258
+ +
259
+ + const block_fv5 * GGML_RESTRICT x = vx;
260
+ + const float * GGML_RESTRICT y = vy;
261
+ +
262
+ + const int nb = n / QK_FV5;
263
+ + float sumf = 0.0f;
264
+ +
265
+ + for (int i = 0; i < nb; ++i) {
266
+ + const float s_lo = x[i].s_lo;
267
+ + const float s_hi = x[i].s_hi;
268
+ + const float * GGML_RESTRICT xf = y + i*QK_FV5;
269
+ +
270
+ +#if defined(__AVX2__)
271
+ + const __m256i bitpos = _mm256_setr_epi32(1, 2, 4, 8, 16, 32, 64, 128);
272
+ + __m256 acc_lo_p = _mm256_setzero_ps();
273
+ + __m256 acc_lo_n = _mm256_setzero_ps();
274
+ + __m256 acc_hi_p = _mm256_setzero_ps();
275
+ + __m256 acc_hi_n = _mm256_setzero_ps();
276
+ +
277
+ + for (int j = 0; j < QK_FV5/8; ++j) {
278
+ + const uint8_t bpj = x[i].bp[j];
279
+ + const uint8_t bnj = x[i].bn[j];
280
+ + if (!(bpj | bnj)) {
281
+ + continue;
282
+ + }
283
+ + const __m256i m_p = _mm256_cmpeq_epi32(_mm256_and_si256(_mm256_set1_epi32(bpj), bitpos), bitpos);
284
+ + const __m256i m_n = _mm256_cmpeq_epi32(_mm256_and_si256(_mm256_set1_epi32(bnj), bitpos), bitpos);
285
+ + const __m256i m_r = _mm256_cmpeq_epi32(_mm256_and_si256(_mm256_set1_epi32(x[i].br[j]), bitpos), bitpos);
286
+ +
287
+ + const __m256 x8 = _mm256_loadu_ps(xf + 8*j);
288
+ +
289
+ + acc_lo_p = _mm256_add_ps(acc_lo_p, _mm256_and_ps(x8, _mm256_castsi256_ps(_mm256_andnot_si256(m_r, m_p))));
290
+ + acc_lo_n = _mm256_add_ps(acc_lo_n, _mm256_and_ps(x8, _mm256_castsi256_ps(_mm256_andnot_si256(m_r, m_n))));
291
+ + acc_hi_p = _mm256_add_ps(acc_hi_p, _mm256_and_ps(x8, _mm256_castsi256_ps(_mm256_and_si256(m_p, m_r))));
292
+ + acc_hi_n = _mm256_add_ps(acc_hi_n, _mm256_and_ps(x8, _mm256_castsi256_ps(_mm256_and_si256(m_n, m_r))));
293
+ + }
294
+ +
295
+ + sumf += s_lo * (ggml_fv5_hsum_ps(acc_lo_p) - ggml_fv5_hsum_ps(acc_lo_n))
296
+ + + s_hi * (ggml_fv5_hsum_ps(acc_hi_p) - ggml_fv5_hsum_ps(acc_hi_n));
297
+ +#else
298
+ + float sum_lo_p = 0.0f, sum_lo_n = 0.0f, sum_hi_p = 0.0f, sum_hi_n = 0.0f;
299
+ +
300
+ + for (int j = 0; j < QK_FV5/8; ++j) {
301
+ + const uint8_t bpj = x[i].bp[j];
302
+ + const uint8_t bnj = x[i].bn[j];
303
+ + if (!(bpj | bnj)) {
304
+ + continue;
305
+ + }
306
+ + const uint8_t brj = x[i].br[j];
307
+ + const float * GGML_RESTRICT xj = xf + 8*j;
308
+ + for (int b = 0; b < 8; ++b) {
309
+ + const uint8_t bit = 1u << b;
310
+ + if (bpj & bit) {
311
+ + if (brj & bit) { sum_hi_p += xj[b]; } else { sum_lo_p += xj[b]; }
312
+ + } else if (bnj & bit) {
313
+ + if (brj & bit) { sum_hi_n += xj[b]; } else { sum_lo_n += xj[b]; }
314
+ + }
315
+ + }
316
+ + }
317
+ +
318
+ + sumf += s_lo * (sum_lo_p - sum_lo_n) + s_hi * (sum_hi_p - sum_hi_n);
319
+ +#endif
320
+ + }
321
+ +
322
+ + *s = sumf;
323
+ +}
324
+ +
325
+ +void ggml_vec_dot_fv5b_f32(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) {
326
+ + assert(nrc == 1);
327
+ + assert(n % QK_FV5 == 0);
328
+ + UNUSED(nrc);
329
+ + UNUSED(bx);
330
+ + UNUSED(by);
331
+ + UNUSED(bs);
332
+ +
333
+ + const block_fv5b * GGML_RESTRICT x = vx;
334
+ + const float * GGML_RESTRICT y = vy;
335
+ +
336
+ + const int nb = n / QK_FV5;
337
+ + float sumf = 0.0f;
338
+ +
339
+ + for (int i = 0; i < nb; ++i) {
340
+ + const float * GGML_RESTRICT xf = y + i*QK_FV5;
341
+ +
342
+ +#if defined(__AVX2__)
343
+ + __m256 acc = _mm256_setzero_ps();
344
+ + for (int j = 0; j < QK_FV5/8; ++j) {
345
+ + const __m128i q8 = _mm_loadl_epi64((const __m128i *)(x[i].qs + 8*j));
346
+ + const __m256 qf = _mm256_cvtepi32_ps(_mm256_cvtepi8_epi32(q8));
347
+ + acc = _mm256_fmadd_ps(qf, _mm256_loadu_ps(xf + 8*j), acc);
348
+ + }
349
+ + sumf += x[i].s * ggml_fv5_hsum_ps(acc);
350
+ +#else
351
+ + float sumq = 0.0f;
352
+ + for (int j = 0; j < QK_FV5; ++j) {
353
+ + sumq += (float) x[i].qs[j] * xf[j];
354
+ + }
355
+ + sumf += x[i].s * sumq;
356
+ +#endif
357
+ + }
358
+ +
359
+ + *s = sumf;
360
+ +}
361
+ +
362
+ void ggml_vec_dot_q2_K_q8_K_generic(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc) {
363
+ assert(nrc == 1);
364
+ UNUSED(nrc);
365
+ diff --git a/ggml/src/ggml-cpu/quants.h b/ggml/src/ggml-cpu/quants.h
366
+ index 93ea7ee..541f292 100644
367
+ --- a/ggml/src/ggml-cpu/quants.h
368
+ +++ b/ggml/src/ggml-cpu/quants.h
369
+ @@ -58,6 +58,11 @@ void ggml_vec_dot_q6_K_q8_K(int n, float * GGML_RESTRICT s, size_t bs, const voi
370
+ void ggml_vec_dot_tq1_0_q8_K(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc);
371
+ void ggml_vec_dot_tq2_0_q8_K(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc);
372
+
373
+ +// Fermion FV5/FV5B: single portable implementation (scalar with an AVX2 fast
374
+ +// path inside), dotted against raw F32 activations — no _generic/arch split.
375
+ +void ggml_vec_dot_fv5_f32 (int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc);
376
+ +void ggml_vec_dot_fv5b_f32(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc);
377
+ +
378
+ void ggml_vec_dot_iq2_xxs_q8_K(int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc);
379
+ void ggml_vec_dot_iq2_xs_q8_K (int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc);
380
+ void ggml_vec_dot_iq2_s_q8_K (int n, float * GGML_RESTRICT s, size_t bs, const void * GGML_RESTRICT vx, size_t bx, const void * GGML_RESTRICT vy, size_t by, int nrc);
381
+ diff --git a/ggml/src/ggml-quants.c b/ggml/src/ggml-quants.c
382
+ index 1ebc50a..39ea3cf 100644
383
+ --- a/ggml/src/ggml-quants.c
384
+ +++ b/ggml/src/ggml-quants.c
385
+ @@ -2483,6 +2483,43 @@ void dequantize_row_tq2_0(const block_tq2_0 * GGML_RESTRICT x, float * GGML_REST
386
+ }
387
+ }
388
+
389
+ +// ====================== Fermion five-value ternary (FV5 / FV5B)
390
+ +//
391
+ +// FV5 blocks are produced offline by the TRTC v4 -> GGUF converter; there is
392
+ +// no float -> FV5 quantizer here on purpose (the container is the source of
393
+ +// truth). Reconstruction semantics match the container exactly:
394
+ +// w[j] = (bp[j] - bn[j]) * (br[j] ? s_hi : s_lo) (all f32)
395
+ +
396
+ +void dequantize_row_fv5(const block_fv5 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) {
397
+ + assert(k % QK_FV5 == 0);
398
+ + const int64_t nb = k / QK_FV5;
399
+ +
400
+ + for (int64_t i = 0; i < nb; ++i) {
401
+ + const float s_lo = x[i].s_lo;
402
+ + const float s_hi = x[i].s_hi;
403
+ +
404
+ + for (int j = 0; j < QK_FV5; ++j) {
405
+ + const int byte = j >> 3;
406
+ + const uint8_t bit = 1u << (j & 7);
407
+ + const int sign = ((x[i].bp[byte] & bit) ? 1 : 0) - ((x[i].bn[byte] & bit) ? 1 : 0);
408
+ + const float mag = (x[i].br[byte] & bit) ? s_hi : s_lo;
409
+ + y[i*QK_FV5 + j] = (float) sign * mag;
410
+ + }
411
+ + }
412
+ +}
413
+ +
414
+ +void dequantize_row_fv5b(const block_fv5b * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) {
415
+ + assert(k % QK_FV5 == 0);
416
+ + const int64_t nb = k / QK_FV5;
417
+ +
418
+ + for (int64_t i = 0; i < nb; ++i) {
419
+ + const float s = x[i].s;
420
+ + for (int j = 0; j < QK_FV5; ++j) {
421
+ + y[i*QK_FV5 + j] = s * (float) x[i].qs[j];
422
+ + }
423
+ + }
424
+ +}
425
+ +
426
+ // ====================== "True" 2-bit (de)-quantization
427
+
428
+ void dequantize_row_iq2_xxs(const block_iq2_xxs * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k) {
429
+ @@ -5604,6 +5641,34 @@ bool ggml_validate_row_data(enum ggml_type type, const void * data, size_t nbyte
430
+ {
431
+ VALIDATE_ROW_DATA_D_F16_IMPL(block_tq2_0, data, nb);
432
+ } break;
433
+ + case GGML_TYPE_FV5:
434
+ + {
435
+ + const block_fv5 * q = (const block_fv5 *) data;
436
+ + for (size_t i = 0; i < nb; ++i) {
437
+ + if (!validate_float(q[i].s_lo, i) || !validate_float(q[i].s_hi, i)) {
438
+ + return false;
439
+ + }
440
+ + for (int j = 0; j < QK_FV5/8; ++j) {
441
+ + if (q[i].bp[j] & q[i].bn[j]) {
442
+ + fprintf(stderr, "%s: fv5 invariant bp&bn != 0 at block %zu byte %d\n", __func__, i, j);
443
+ + return false;
444
+ + }
445
+ + if (q[i].br[j] & ~(q[i].bp[j] | q[i].bn[j])) {
446
+ + fprintf(stderr, "%s: fv5 invariant br outside bp|bn at block %zu byte %d\n", __func__, i, j);
447
+ + return false;
448
+ + }
449
+ + }
450
+ + }
451
+ + } break;
452
+ + case GGML_TYPE_FV5B:
453
+ + {
454
+ + const block_fv5b * q = (const block_fv5b *) data;
455
+ + for (size_t i = 0; i < nb; ++i) {
456
+ + if (!validate_float(q[i].s, i)) {
457
+ + return false;
458
+ + }
459
+ + }
460
+ + } break;
461
+ case GGML_TYPE_IQ1_S:
462
+ {
463
+ VALIDATE_ROW_DATA_D_F16_IMPL(block_iq1_s, data, nb);
464
+ diff --git a/ggml/src/ggml-quants.h b/ggml/src/ggml-quants.h
465
+ index 75188f1..f71e078 100644
466
+ --- a/ggml/src/ggml-quants.h
467
+ +++ b/ggml/src/ggml-quants.h
468
+ @@ -65,6 +65,9 @@ GGML_API void dequantize_row_q8_K(const block_q8_K * GGML_RESTRICT x, float * GG
469
+ GGML_API void dequantize_row_tq1_0(const block_tq1_0 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k);
470
+ GGML_API void dequantize_row_tq2_0(const block_tq2_0 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k);
471
+
472
+ +GGML_API void dequantize_row_fv5 (const block_fv5 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k);
473
+ +GGML_API void dequantize_row_fv5b(const block_fv5b * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k);
474
+ +
475
+ GGML_API void dequantize_row_iq2_xxs(const block_iq2_xxs * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k);
476
+ GGML_API void dequantize_row_iq2_xs (const block_iq2_xs * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k);
477
+ GGML_API void dequantize_row_iq2_s (const block_iq2_s * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k);
478
+ diff --git a/ggml/src/ggml.c b/ggml/src/ggml.c
479
+ index a7d1fe7..7de0dda 100644
480
+ --- a/ggml/src/ggml.c
481
+ +++ b/ggml/src/ggml.c
482
+ @@ -942,6 +942,22 @@ static const struct ggml_type_traits type_traits[GGML_TYPE_COUNT] = {
483
+ .type_size = 0,
484
+ .is_quantized = false,
485
+ },
486
+ + [GGML_TYPE_FV5] = {
487
+ + .type_name = "fv5",
488
+ + .blck_size = QK_FV5,
489
+ + .type_size = sizeof(block_fv5),
490
+ + .is_quantized = true,
491
+ + .to_float = (ggml_to_float_t) dequantize_row_fv5,
492
+ + .from_float_ref = NULL, // produced offline by the TRTC v4 -> GGUF converter
493
+ + },
494
+ + [GGML_TYPE_FV5B] = {
495
+ + .type_name = "fv5b",
496
+ + .blck_size = QK_FV5,
497
+ + .type_size = sizeof(block_fv5b),
498
+ + .is_quantized = true,
499
+ + .to_float = (ggml_to_float_t) dequantize_row_fv5b,
500
+ + .from_float_ref = NULL, // produced offline by the TRTC v4 -> GGUF converter
501
+ + },
502
+ };
503
+
504
+ const struct ggml_type_traits * ggml_get_type_traits(enum ggml_type type) {
505
+ diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py
506
+ index d55253e..05076c9 100644
507
+ --- a/gguf-py/gguf/constants.py
508
+ +++ b/gguf-py/gguf/constants.py
509
+ @@ -4595,6 +4595,8 @@ class GGMLQuantizationType(IntEnum):
510
+ NVFP4 = 40
511
+ Q1_0 = 41
512
+ Q2_0 = 42
513
+ + FV5 = 43
514
+ + FV5B = 44
515
+
516
+
517
+ class ExpertGatingFuncType(IntEnum):
518
+ @@ -4651,6 +4653,7 @@ class LlamaFileType(IntEnum):
519
+ MOSTLY_NVFP4 = 39 # except 1d tensors
520
+ MOSTLY_Q1_0 = 40 # except 1d tensors
521
+ MOSTLY_Q2_0 = 41 # except 1d tensors
522
+ + MOSTLY_FV5 = 42 # Fermion five-value ternary; int8 embed/lm_head (FV5B), norms F32
523
+
524
+ GUESSED = 1024 # not specified in the model file
525
+
526
+ @@ -4777,6 +4780,8 @@ GGML_QUANT_SIZES: dict[GGMLQuantizationType, tuple[int, int]] = {
527
+ GGMLQuantizationType.NVFP4: (64, 4 + 32),
528
+ GGMLQuantizationType.Q1_0: (128, 2 + 16),
529
+ GGMLQuantizationType.Q2_0: (64, 2 + 16),
530
+ + GGMLQuantizationType.FV5: (256, 4 + 4 + 3 * 32),
531
+ + GGMLQuantizationType.FV5B: (256, 4 + 256),
532
+ }
533
+
534
+
535
+ diff --git a/gguf-py/gguf/quants.py b/gguf-py/gguf/quants.py
536
+ index 80966b6..c7122e5 100644
537
+ --- a/gguf-py/gguf/quants.py
538
+ +++ b/gguf-py/gguf/quants.py
539
+ @@ -654,6 +654,50 @@ class TQ2_0(__Quant, qtype=GGMLQuantizationType.TQ2_0):
540
+ return (d * qs.astype(np.float32))
541
+
542
+
543
+ +class FV5(__Quant, qtype=GGMLQuantizationType.FV5):
544
+ + # Fermion five-value ternary. Blocks are produced offline by the
545
+ + # TRTC v4 -> GGUF converter; float -> FV5 quantization is intentionally
546
+ + # not implemented (per-row dual scales live in the container).
547
+ + # Layout: f32 s_lo, f32 s_hi, bp[32], bn[32], br[32] (little bit order).
548
+ + @classmethod
549
+ + def quantize_blocks(cls, blocks: np.ndarray) -> np.ndarray:
550
+ + raise NotImplementedError("FV5 is produced offline from TRTC v4 containers")
551
+ +
552
+ + @classmethod
553
+ + def dequantize_blocks(cls, blocks: np.ndarray) -> np.ndarray:
554
+ + n_blocks = blocks.shape[0]
555
+ +
556
+ + s_lo, s_hi, bp, bn, br = np.hsplit(blocks, [4, 8, 40, 72])
557
+ +
558
+ + s_lo = s_lo.view(np.float32) # (n, 1)
559
+ + s_hi = s_hi.view(np.float32)
560
+ +
561
+ + bp = np.unpackbits(bp, axis=-1, bitorder="little") # (n, 256) in {0,1}
562
+ + bn = np.unpackbits(bn, axis=-1, bitorder="little")
563
+ + br = np.unpackbits(br, axis=-1, bitorder="little")
564
+ +
565
+ + sign = bp.astype(np.float32) - bn.astype(np.float32)
566
+ + mag = np.where(br != 0, s_hi, s_lo).astype(np.float32)
567
+ + return sign * mag
568
+ +
569
+ +
570
+ +class FV5B(__Quant, qtype=GGMLQuantizationType.FV5B):
571
+ + # Fermion int8 rows (TRTC v4 embed/lm_head records): w = f32(s) * int8 q.
572
+ + # Layout: f32 s, int8 qs[256].
573
+ + @classmethod
574
+ + def quantize_blocks(cls, blocks: np.ndarray) -> np.ndarray:
575
+ + raise NotImplementedError("FV5B is produced offline from TRTC v4 containers")
576
+ +
577
+ + @classmethod
578
+ + def dequantize_blocks(cls, blocks: np.ndarray) -> np.ndarray:
579
+ + s, qs = np.hsplit(blocks, [4])
580
+ +
581
+ + s = s.view(np.float32) # (n, 1)
582
+ + qs = qs.view(np.int8) # (n, 256)
583
+ +
584
+ + return s * qs.astype(np.float32)
585
+ +
586
+ +
587
+ class MXFP4(__Quant, qtype=GGMLQuantizationType.MXFP4):
588
+ # e2m1 values (doubled)
589
+ # ref: https://www.opencompute.org/documents/ocp-microscaling-formats-mx-v1-0-spec-final-pdf
590
+ diff --git a/include/llama.h b/include/llama.h
591
+ index 9fab693..584717c 100644
592
+ --- a/include/llama.h
593
+ +++ b/include/llama.h
594
+ @@ -156,6 +156,7 @@ extern "C" {
595
+ LLAMA_FTYPE_MOSTLY_NVFP4 = 39, // except 1d tensors
596
+ LLAMA_FTYPE_MOSTLY_Q1_0 = 40, // except 1d tensors
597
+ LLAMA_FTYPE_MOSTLY_Q2_0 = 41, // except 1d tensors
598
+ + LLAMA_FTYPE_MOSTLY_FV5 = 42, // Fermion five-value ternary; int8 embed/lm_head (FV5B), norms F32
599
+
600
+ LLAMA_FTYPE_GUESSED = 1024, // not specified in the model file
601
+ };
602
+ diff --git a/src/llama-model-loader.cpp b/src/llama-model-loader.cpp
603
+ index 43447f5..6fb50f8 100644
604
+ --- a/src/llama-model-loader.cpp
605
+ +++ b/src/llama-model-loader.cpp
606
+ @@ -58,6 +58,7 @@ const char * llama_ftype_name(llama_ftype ftype) {
607
+ case LLAMA_FTYPE_MOSTLY_Q6_K: name = LLAMA_FTYPE_PREFIX "Q6_K"; break;
608
+ case LLAMA_FTYPE_MOSTLY_TQ1_0: name = LLAMA_FTYPE_PREFIX "TQ1_0 - 1.69 bpw ternary"; break;
609
+ case LLAMA_FTYPE_MOSTLY_TQ2_0: name = LLAMA_FTYPE_PREFIX "TQ2_0 - 2.06 bpw ternary"; break;
610
+ + case LLAMA_FTYPE_MOSTLY_FV5: name = LLAMA_FTYPE_PREFIX "FV5 - 3.25 bpw Fermion five-value ternary"; break;
611
+ case LLAMA_FTYPE_MOSTLY_IQ2_XXS: name = LLAMA_FTYPE_PREFIX "IQ2_XXS - 2.0625 bpw"; break;
612
+ case LLAMA_FTYPE_MOSTLY_IQ2_XS: name = LLAMA_FTYPE_PREFIX "IQ2_XS - 2.3125 bpw"; break;
613
+ case LLAMA_FTYPE_MOSTLY_IQ2_S: name = LLAMA_FTYPE_PREFIX "IQ2_S - 2.5 bpw"; break;
614
+ @@ -746,6 +747,7 @@ llama_model_loader::llama_model_loader(
615
+ case GGML_TYPE_Q6_K: ftype = LLAMA_FTYPE_MOSTLY_Q6_K; break;
616
+ case GGML_TYPE_TQ1_0: ftype = LLAMA_FTYPE_MOSTLY_TQ1_0; break;
617
+ case GGML_TYPE_TQ2_0: ftype = LLAMA_FTYPE_MOSTLY_TQ2_0; break;
618
+ + case GGML_TYPE_FV5: ftype = LLAMA_FTYPE_MOSTLY_FV5; break;
619
+ case GGML_TYPE_IQ2_XXS: ftype = LLAMA_FTYPE_MOSTLY_IQ2_XXS; break;
620
+ case GGML_TYPE_IQ2_XS: ftype = LLAMA_FTYPE_MOSTLY_IQ2_XS; break;
621
+ case GGML_TYPE_IQ2_S: ftype = LLAMA_FTYPE_MOSTLY_IQ2_S; break;
622
+ diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
623
+ index 780df32..e39837d 100644
624
+ --- a/tools/CMakeLists.txt
625
+ +++ b/tools/CMakeLists.txt
626
+ @@ -15,6 +15,7 @@ llama_add_compile_flags()
627
+ if (EMSCRIPTEN)
628
+ else()
629
+ add_subdirectory(batched-bench)
630
+ + add_subdirectory(fermion-greedy)
631
+ add_subdirectory(gguf-split)
632
+ add_subdirectory(imatrix)
633
+ add_subdirectory(llama-bench)
634
+ diff --git a/tools/fermion-greedy/CMakeLists.txt b/tools/fermion-greedy/CMakeLists.txt
635
+ new file mode 100644
636
+ index 0000000..021440a
637
+ --- /dev/null
638
+ +++ b/tools/fermion-greedy/CMakeLists.txt
639
+ @@ -0,0 +1,5 @@
640
+ +set(TARGET llama-fermion-greedy)
641
+ +add_executable(${TARGET} fermion-greedy.cpp)
642
+ +install(TARGETS ${TARGET} RUNTIME)
643
+ +target_link_libraries(${TARGET} PRIVATE llama ${CMAKE_THREAD_LIBS_INIT})
644
+ +target_compile_features(${TARGET} PRIVATE cxx_std_17)
645
+ diff --git a/tools/fermion-greedy/fermion-greedy.cpp b/tools/fermion-greedy/fermion-greedy.cpp
646
+ new file mode 100644
647
+ index 0000000..6f7fde2
648
+ --- /dev/null
649
+ +++ b/tools/fermion-greedy/fermion-greedy.cpp
650
+ @@ -0,0 +1,299 @@
651
+ +// fermion-greedy — deterministic greedy runner for the Fermion FV5 correctness gate.
652
+ +//
653
+ +// Reads prompts as EXACT token ids (no tokenizer in the loop), runs pure
654
+ +// argmax greedy generation, and — when a reference stream is given — also a
655
+ +// teacher-forced pass that feeds the reference token at every step while
656
+ +// recording where our argmax disagrees and by what logit margin. Root-cause
657
+ +// flips (teacher-forced mismatches) are the gate currency: free-run streams
658
+ +// diverge wholesale after one flip, forced mode localizes every disagreement.
659
+ +//
660
+ +// The KV cache is kept in F32 so the only numerical difference vs the f32
661
+ +// expansion reference is operation order, not storage precision.
662
+ +//
663
+ +// usage:
664
+ +// llama-fermion-greedy -m model.gguf --prompts prompts.txt [--ref ref.txt]
665
+ +// [--steps 128] [-t nthreads] [--out receipt.json]
666
+ +//
667
+ +// prompts.txt: one prompt per line, comma-separated token ids
668
+ +// ref.txt: one line per prompt, comma-separated reference continuation ids
669
+ +// output: JSON receipt on stdout (or --out file)
670
+ +
671
+ +#include "llama.h"
672
+ +
673
+ +#include <chrono>
674
+ +#include <cinttypes>
675
+ +#include <cstdio>
676
+ +#include <cstdlib>
677
+ +#include <cstring>
678
+ +#include <fstream>
679
+ +#include <sstream>
680
+ +#include <string>
681
+ +#include <vector>
682
+ +
683
+ +static double now_s() {
684
+ + using clk = std::chrono::steady_clock;
685
+ + return std::chrono::duration<double>(clk::now().time_since_epoch()).count();
686
+ +}
687
+ +
688
+ +static std::vector<std::vector<llama_token>> read_id_lines(const std::string & path) {
689
+ + std::vector<std::vector<llama_token>> out;
690
+ + std::ifstream f(path);
691
+ + if (!f) {
692
+ + fprintf(stderr, "error: cannot open %s\n", path.c_str());
693
+ + exit(1);
694
+ + }
695
+ + std::string line;
696
+ + while (std::getline(f, line)) {
697
+ + if (line.empty()) {
698
+ + continue;
699
+ + }
700
+ + std::vector<llama_token> ids;
701
+ + std::stringstream ss(line);
702
+ + std::string tok;
703
+ + while (std::getline(ss, tok, ',')) {
704
+ + if (!tok.empty()) {
705
+ + ids.push_back((llama_token) strtol(tok.c_str(), nullptr, 10));
706
+ + }
707
+ + }
708
+ + if (!ids.empty()) {
709
+ + out.push_back(std::move(ids));
710
+ + }
711
+ + }
712
+ + return out;
713
+ +}
714
+ +
715
+ +struct step_info {
716
+ + llama_token top1;
717
+ + float logit_top1;
718
+ + llama_token top2;
719
+ + float logit_top2;
720
+ +};
721
+ +
722
+ +// argmax + runner-up over the last decoded token's logits
723
+ +static step_info argmax2(const float * logits, int n_vocab) {
724
+ + step_info si = { 0, -1e30f, 0, -1e30f };
725
+ + for (int i = 0; i < n_vocab; ++i) {
726
+ + const float v = logits[i];
727
+ + if (v > si.logit_top1) {
728
+ + si.top2 = si.top1; si.logit_top2 = si.logit_top1;
729
+ + si.top1 = i; si.logit_top1 = v;
730
+ + } else if (v > si.logit_top2) {
731
+ + si.top2 = i; si.logit_top2 = v;
732
+ + }
733
+ + }
734
+ + return si;
735
+ +}
736
+ +
737
+ +int main(int argc, char ** argv) {
738
+ + std::string model_path;
739
+ + std::string prompts_path;
740
+ + std::string ref_path;
741
+ + std::string out_path;
742
+ + int steps = 128;
743
+ + int n_threads = 8;
744
+ +
745
+ + for (int i = 1; i < argc; ++i) {
746
+ + auto need = [&](const char * flag) -> const char * {
747
+ + if (i + 1 >= argc) { fprintf(stderr, "error: %s needs a value\n", flag); exit(1); }
748
+ + return argv[++i];
749
+ + };
750
+ + if (!strcmp(argv[i], "-m")) { model_path = need("-m"); }
751
+ + else if (!strcmp(argv[i], "--prompts")) { prompts_path = need("--prompts"); }
752
+ + else if (!strcmp(argv[i], "--ref")) { ref_path = need("--ref"); }
753
+ + else if (!strcmp(argv[i], "--out")) { out_path = need("--out"); }
754
+ + else if (!strcmp(argv[i], "--steps")) { steps = atoi(need("--steps")); }
755
+ + else if (!strcmp(argv[i], "-t")) { n_threads = atoi(need("-t")); }
756
+ + else { fprintf(stderr, "error: unknown arg %s\n", argv[i]); return 1; }
757
+ + }
758
+ + if (model_path.empty() || prompts_path.empty()) {
759
+ + fprintf(stderr, "usage: %s -m model.gguf --prompts ids.txt [--ref ids.txt] [--steps N] [-t N] [--out f.json]\n", argv[0]);
760
+ + return 1;
761
+ + }
762
+ +
763
+ + const auto prompts = read_id_lines(prompts_path);
764
+ + std::vector<std::vector<llama_token>> refs;
765
+ + if (!ref_path.empty()) {
766
+ + refs = read_id_lines(ref_path);
767
+ + if (refs.size() != prompts.size()) {
768
+ + fprintf(stderr, "error: %zu prompts but %zu reference lines\n", prompts.size(), refs.size());
769
+ + return 1;
770
+ + }
771
+ + }
772
+ +
773
+ + size_t max_prompt = 0;
774
+ + for (const auto & p : prompts) {
775
+ + max_prompt = p.size() > max_prompt ? p.size() : max_prompt;
776
+ + }
777
+ +
778
+ + ggml_backend_load_all();
779
+ +
780
+ + llama_model_params mparams = llama_model_default_params();
781
+ + mparams.n_gpu_layers = 0;
782
+ + const double t_load0 = now_s();
783
+ + llama_model * model = llama_model_load_from_file(model_path.c_str(), mparams);
784
+ + if (model == nullptr) {
785
+ + fprintf(stderr, "error: failed to load %s\n", model_path.c_str());
786
+ + return 1;
787
+ + }
788
+ + const double t_load = now_s() - t_load0;
789
+ +
790
+ + const llama_vocab * vocab = llama_model_get_vocab(model);
791
+ + const int n_vocab = llama_vocab_n_tokens(vocab);
792
+ +
793
+ + llama_context_params cparams = llama_context_default_params();
794
+ + cparams.n_ctx = (uint32_t) (max_prompt + steps + 8);
795
+ + cparams.n_batch = (uint32_t) (max_prompt + 8);
796
+ + cparams.n_threads = n_threads;
797
+ + cparams.n_threads_batch = n_threads;
798
+ + cparams.type_k = GGML_TYPE_F32; // keep KV in f32: gate compares against an f32 reference
799
+ + cparams.type_v = GGML_TYPE_F32;
800
+ + cparams.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_DISABLED; // conservative numerics for the gate
801
+ + cparams.no_perf = true;
802
+ +
803
+ + llama_context * ctx = llama_init_from_model(model, cparams);
804
+ + if (ctx == nullptr) {
805
+ + fprintf(stderr, "error: failed to create context\n");
806
+ + return 1;
807
+ + }
808
+ +
809
+ + FILE * out = stdout;
810
+ + if (!out_path.empty()) {
811
+ + out = fopen(out_path.c_str(), "w");
812
+ + if (!out) {
813
+ + fprintf(stderr, "error: cannot write %s\n", out_path.c_str());
814
+ + return 1;
815
+ + }
816
+ + }
817
+ +
818
+ + fprintf(out, "{\n \"model\": \"%s\",\n \"steps\": %d,\n \"n_threads\": %d,\n \"load_seconds\": %.2f,\n \"kv_type\": \"f32\",\n \"prompts\": [\n",
819
+ + model_path.c_str(), steps, n_threads, t_load);
820
+ +
821
+ + double total_gen_s = 0.0;
822
+ + double total_prompt_s = 0.0;
823
+ + int64_t total_gen_tok = 0;
824
+ + int64_t total_prompt_tok = 0;
825
+ + int total_forced_mismatch = 0;
826
+ + bool all_free_match = true;
827
+ +
828
+ + for (size_t pi = 0; pi < prompts.size(); ++pi) {
829
+ + std::vector<llama_token> prompt = prompts[pi];
830
+ +
831
+ + // ---------------- free-running greedy pass ----------------
832
+ + llama_memory_clear(llama_get_memory(ctx), true);
833
+ +
834
+ + double t0 = now_s();
835
+ + llama_batch batch = llama_batch_get_one(prompt.data(), (int32_t) prompt.size());
836
+ + if (llama_decode(ctx, batch) != 0) {
837
+ + fprintf(stderr, "error: prompt decode failed (prompt %zu)\n", pi);
838
+ + return 1;
839
+ + }
840
+ + const double prompt_s = now_s() - t0;
841
+ +
842
+ + std::vector<llama_token> free_stream;
843
+ + float min_margin_free = 1e30f;
844
+ + t0 = now_s();
845
+ + for (int t = 0; t < steps; ++t) {
846
+ + const float * logits = llama_get_logits_ith(ctx, -1);
847
+ + step_info si = argmax2(logits, n_vocab);
848
+ + const float margin = si.logit_top1 - si.logit_top2;
849
+ + if (margin < min_margin_free) {
850
+ + min_margin_free = margin;
851
+ + }
852
+ + free_stream.push_back(si.top1);
853
+ + if (t + 1 < steps) {
854
+ + llama_token next = si.top1;
855
+ + batch = llama_batch_get_one(&next, 1);
856
+ + if (llama_decode(ctx, batch) != 0) {
857
+ + fprintf(stderr, "error: decode failed (prompt %zu step %d)\n", pi, t);
858
+ + return 1;
859
+ + }
860
+ + }
861
+ + }
862
+ + const double gen_s = now_s() - t0;
863
+ +
864
+ + total_prompt_s += prompt_s;
865
+ + total_gen_s += gen_s;
866
+ + total_prompt_tok += (int64_t) prompt.size();
867
+ + total_gen_tok += steps;
868
+ +
869
+ + fprintf(out, " {\n \"prompt_ids\": [");
870
+ + for (size_t i = 0; i < prompt.size(); ++i) {
871
+ + fprintf(out, "%s%d", i ? "," : "", prompt[i]);
872
+ + }
873
+ + fprintf(out, "],\n \"free_ids\": [");
874
+ + for (size_t i = 0; i < free_stream.size(); ++i) {
875
+ + fprintf(out, "%s%d", i ? "," : "", free_stream[i]);
876
+ + }
877
+ + fprintf(out, "],\n \"free_min_top2_margin\": %.6f,\n", min_margin_free);
878
+ + fprintf(out, " \"prompt_seconds\": %.3f,\n \"gen_seconds\": %.3f", prompt_s, gen_s);
879
+ +
880
+ + if (!refs.empty()) {
881
+ + const std::vector<llama_token> & ref = refs[pi];
882
+ + const int n_ref = (int) ref.size() < steps ? (int) ref.size() : steps;
883
+ +
884
+ + // free-run vs reference (first divergence)
885
+ + int first_div = -1;
886
+ + for (int t = 0; t < n_ref; ++t) {
887
+ + if (free_stream[t] != ref[t]) { first_div = t; break; }
888
+ + }
889
+ + const bool free_match = first_div < 0;
890
+ + all_free_match = all_free_match && free_match;
891
+ +
892
+ + // ---------------- teacher-forced pass ----------------
893
+ + llama_memory_clear(llama_get_memory(ctx), true);
894
+ + batch = llama_batch_get_one(prompt.data(), (int32_t) prompt.size());
895
+ + if (llama_decode(ctx, batch) != 0) {
896
+ + fprintf(stderr, "error: forced prompt decode failed (prompt %zu)\n", pi);
897
+ + return 1;
898
+ + }
899
+ +
900
+ + int n_mismatch = 0;
901
+ + std::string mm_json;
902
+ + char buf[256];
903
+ + for (int t = 0; t < n_ref; ++t) {
904
+ + const float * logits = llama_get_logits_ith(ctx, -1);
905
+ + step_info si = argmax2(logits, n_vocab);
906
+ + if (si.top1 != ref[t]) {
907
+ + const float logit_ref = logits[ref[t]];
908
+ + snprintf(buf, sizeof(buf),
909
+ + "%s\n {\"step\": %d, \"got\": %d, \"want\": %d, \"logit_got\": %.6f, \"logit_want\": %.6f, \"margin\": %.6f}",
910
+ + n_mismatch ? "," : "", t, si.top1, ref[t], si.logit_top1, logit_ref, si.logit_top1 - logit_ref);
911
+ + mm_json += buf;
912
+ + n_mismatch++;
913
+ + }
914
+ + llama_token next = ref[t]; // teacher-force the reference stream
915
+ + if (t + 1 < n_ref) {
916
+ + batch = llama_batch_get_one(&next, 1);
917
+ + if (llama_decode(ctx, batch) != 0) {
918
+ + fprintf(stderr, "error: forced decode failed (prompt %zu step %d)\n", pi, t);
919
+ + return 1;
920
+ + }
921
+ + }
922
+ + }
923
+ + total_forced_mismatch += n_mismatch;
924
+ +
925
+ + fprintf(out, ",\n \"free_match\": %s,\n \"first_divergence\": %d,\n \"forced_mismatch_count\": %d,\n \"forced_mismatches\": [%s%s]",
926
+ + free_match ? "true" : "false", first_div, n_mismatch,
927
+ + mm_json.c_str(), n_mismatch ? "\n " : "");
928
+ + }
929
+ +
930
+ + fprintf(out, "\n }%s\n", pi + 1 < prompts.size() ? "," : "");
931
+ + fflush(out);
932
+ + fprintf(stderr, "prompt %zu/%zu done (%.1f tok/s gen)\n", pi + 1, prompts.size(), steps / gen_s);
933
+ + }
934
+ +
935
+ + fprintf(out, " ],\n \"totals\": {\n");
936
+ + fprintf(out, " \"prompt_tokens\": %" PRId64 ",\n \"gen_tokens\": %" PRId64 ",\n", total_prompt_tok, total_gen_tok);
937
+ + fprintf(out, " \"prompt_tok_per_s\": %.2f,\n \"gen_tok_per_s\": %.2f,\n",
938
+ + total_prompt_tok / (total_prompt_s > 0 ? total_prompt_s : 1e-9),
939
+ + total_gen_tok / (total_gen_s > 0 ? total_gen_s : 1e-9));
940
+ + fprintf(out, " \"forced_mismatch_total\": %d,\n \"all_free_match\": %s\n }\n}\n",
941
+ + total_forced_mismatch, (refs.empty() || !all_free_match) ? (refs.empty() ? "null" : "false") : "true");
942
+ +
943
+ + if (out != stdout) {
944
+ + fclose(out);
945
+ + }
946
+ + llama_free(ctx);
947
+ + llama_model_free(model);
948
+ + return 0;
949
+ +}
950
+ --
951
+ 2.50.1 (Apple Git-155)
952
+
gguf/neutrino-0.6b-base-fv5.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dd9354c4b3cd2b34195b0270540e4fa5db3811e42b6e4bdbd57958e0027e9272
3
+ size 343140320
gguf/receipts/bench_0p6b.json ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "0p6b",
3
+ "bench": [
4
+ {
5
+ "build_commit": "e07b3ec",
6
+ "build_number": 2,
7
+ "cpu_info": "unknown",
8
+ "gpu_info": "",
9
+ "backends": "CPU",
10
+ "model_filename": "/vol/gguf/neutrino-0.6b-base-fv5.gguf",
11
+ "model_type": "qwen3 0.6B FV5 - 3.25 bpw Fermion five-value ternary",
12
+ "model_size": 337188864,
13
+ "model_n_params": 596049920,
14
+ "n_batch": 2048,
15
+ "n_ubatch": 512,
16
+ "n_threads": 16,
17
+ "cpu_mask": "0x0",
18
+ "cpu_strict": false,
19
+ "poll": 50,
20
+ "type_k": "f16",
21
+ "type_v": "f16",
22
+ "n_gpu_layers": -1,
23
+ "n_cpu_moe": 0,
24
+ "split_mode": "layer",
25
+ "main_gpu": 0,
26
+ "no_kv_offload": false,
27
+ "flash_attn": -1,
28
+ "devices": "auto",
29
+ "tensor_split": "0.00",
30
+ "tensor_buft_overrides": "none",
31
+ "load_mode": "mmap",
32
+ "embeddings": false,
33
+ "no_op_offload": 0,
34
+ "no_host": false,
35
+ "fit_target": 0,
36
+ "fit_min_ctx": 0,
37
+ "n_prompt": 512,
38
+ "n_gen": 0,
39
+ "n_depth": 0,
40
+ "test_time": "2026-07-25T10:23:15Z",
41
+ "avg_ns": 3873491459,
42
+ "stddev_ns": 672837,
43
+ "avg_ts": 132.180492,
44
+ "stddev_ts": 0.02296,
45
+ "samples_ns": [
46
+ 3873015691,
47
+ 3873967227
48
+ ],
49
+ "samples_ts": [
50
+ 132.197,
51
+ 132.164
52
+ ]
53
+ },
54
+ {
55
+ "build_commit": "e07b3ec",
56
+ "build_number": 2,
57
+ "cpu_info": "unknown",
58
+ "gpu_info": "",
59
+ "backends": "CPU",
60
+ "model_filename": "/vol/gguf/neutrino-0.6b-base-fv5.gguf",
61
+ "model_type": "qwen3 0.6B FV5 - 3.25 bpw Fermion five-value ternary",
62
+ "model_size": 337188864,
63
+ "model_n_params": 596049920,
64
+ "n_batch": 2048,
65
+ "n_ubatch": 512,
66
+ "n_threads": 16,
67
+ "cpu_mask": "0x0",
68
+ "cpu_strict": false,
69
+ "poll": 50,
70
+ "type_k": "f16",
71
+ "type_v": "f16",
72
+ "n_gpu_layers": -1,
73
+ "n_cpu_moe": 0,
74
+ "split_mode": "layer",
75
+ "main_gpu": 0,
76
+ "no_kv_offload": false,
77
+ "flash_attn": -1,
78
+ "devices": "auto",
79
+ "tensor_split": "0.00",
80
+ "tensor_buft_overrides": "none",
81
+ "load_mode": "mmap",
82
+ "embeddings": false,
83
+ "no_op_offload": 0,
84
+ "no_host": false,
85
+ "fit_target": 0,
86
+ "fit_min_ctx": 0,
87
+ "n_prompt": 0,
88
+ "n_gen": 128,
89
+ "n_depth": 0,
90
+ "test_time": "2026-07-25T10:23:27Z",
91
+ "avg_ns": 2311377861,
92
+ "stddev_ns": 12562356,
93
+ "avg_ts": 55.379041,
94
+ "stddev_ts": 0.300981,
95
+ "samples_ns": [
96
+ 2320260659,
97
+ 2302495064
98
+ ],
99
+ "samples_ts": [
100
+ 55.1662,
101
+ 55.5919
102
+ ]
103
+ }
104
+ ],
105
+ "smoke_rc": 0,
106
+ "smoke_text": "located in the capital of the country. The capital of France is located in the capital of the country. The capital of France is located in the capital of the country. The capital of France is located in the capital of the country. The",
107
+ "threads": 16,
108
+ "cpu": "unknown",
109
+ "nproc": 32,
110
+ "wall_s": 20.4,
111
+ "ts": "2026-07-25T10:23:34Z"
112
+ }
gguf/receipts/convert_0p6b.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "status": "CONVERT_PASS",
3
+ "container": {
4
+ "name": "neutrino-0p6b_v4_base.bin",
5
+ "sha256": "91c0b26d105710e755ef1ec1b5607c00da5d5cbd048fd5584135fb6fc1e19399"
6
+ },
7
+ "f32_gguf": {
8
+ "name": "neutrino-0.6b-base-f32.gguf",
9
+ "bytes": 3012480800
10
+ },
11
+ "out": {
12
+ "name": "neutrino-0.6b-base-fv5.gguf",
13
+ "bytes": 343140320,
14
+ "sha256": "dd9354c4b3cd2b34195b0270540e4fa5db3811e42b6e4bdbd57958e0027e9272"
15
+ },
16
+ "tensors": {
17
+ "fv5": 196,
18
+ "fv5b": 1,
19
+ "f32": 113,
20
+ "total": 310,
21
+ "bitwise_checked": 310
22
+ },
23
+ "tied_output_omitted": true,
24
+ "quantized_params": 595984384,
25
+ "effective_bpw_quantized": 4.606,
26
+ "header": {
27
+ "magic": 1129599572,
28
+ "version": 4,
29
+ "arch": 3,
30
+ "n_layers": 28,
31
+ "hidden": 1024,
32
+ "n_heads": 16,
33
+ "n_kv_heads": 8,
34
+ "head_dim": 128,
35
+ "rotary_dims": 128,
36
+ "intermediate": 3072,
37
+ "vocab": 151936,
38
+ "max_pos": 40960,
39
+ "rms_norm_eps": 9.999999974752427e-07,
40
+ "rope_theta": 1000000.0,
41
+ "eok": 1
42
+ },
43
+ "wall_seconds": 14.6
44
+ }
gguf/receipts/gv_0p6b_f32.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "0p6b",
3
+ "variant": "f32",
4
+ "verdict": "PASS",
5
+ "all_free_match": true,
6
+ "forced_mismatch_total": 0,
7
+ "gen_tok_per_s": 35.04,
8
+ "prompt_tok_per_s": 140.06,
9
+ "flips": [],
10
+ "unexplained": 0,
11
+ "gguf_sha256": "3f3e8864c18f450240bab0498fac871a64dcc3eed72bfa97f35cbf33881db3e5",
12
+ "ref_trajectory_sha256": "685bd94b1f77b041ce7836bb2d45042b142799b824837bcc420271494a4fabdd",
13
+ "threads": 16,
14
+ "cpu": "unknown",
15
+ "wall_s": 63.0,
16
+ "ts": "2026-07-25T10:23:13Z"
17
+ }
gguf/receipts/gv_0p6b_fv5.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "0p6b",
3
+ "variant": "fv5",
4
+ "verdict": "PASS",
5
+ "all_free_match": true,
6
+ "forced_mismatch_total": 0,
7
+ "gen_tok_per_s": 51.63,
8
+ "prompt_tok_per_s": 92.96,
9
+ "flips": [],
10
+ "unexplained": 0,
11
+ "gguf_sha256": "dd9354c4b3cd2b34195b0270540e4fa5db3811e42b6e4bdbd57958e0027e9272",
12
+ "ref_trajectory_sha256": "685bd94b1f77b041ce7836bb2d45042b142799b824837bcc420271494a4fabdd",
13
+ "threads": 16,
14
+ "cpu": "unknown",
15
+ "wall_s": 43.7,
16
+ "ts": "2026-07-25T10:22:09Z"
17
+ }
gguf/receipts/ref_0p6b.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "0p6b",
3
+ "dtype": "float32",
4
+ "steps": 128,
5
+ "trajectory_sha256": "685bd94b1f77b041ce7836bb2d45042b142799b824837bcc420271494a4fabdd",
6
+ "continuity_vs_banked_fp16": [
7
+ {
8
+ "prompt": 0,
9
+ "fp16_match": true,
10
+ "first_divergence": -1
11
+ },
12
+ {
13
+ "prompt": 1,
14
+ "fp16_match": true,
15
+ "first_divergence": -1
16
+ },
17
+ {
18
+ "prompt": 2,
19
+ "fp16_match": true,
20
+ "first_divergence": -1
21
+ }
22
+ ],
23
+ "wall_s": 212.4,
24
+ "cpu": "unknown",
25
+ "ts": "2026-07-25T10:16:30Z"
26
+ }
merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
mlx/MANIFEST.json ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "note": "MLX pack provenance: file -> bytes/sha256/source. Weights entry is declarative (not duplicated in this subfolder).",
3
+ "generated_utc": "2026-07-25T16:52:04Z",
4
+ "weights_container": {
5
+ "file": "neutrino-0.6b_v4.bin",
6
+ "status": "NOT-IN-SUBFOLDER; parent card materialization applies",
7
+ "bytes": 327719836,
8
+ "sha256": "91c0b26d105710e755ef1ec1b5607c00da5d5cbd048fd5584135fb6fc1e19399",
9
+ "source": "Fermion Research release archive",
10
+ "gated_on": "this exact container (V1 refgate + cachegate + pathgate receipts in receipts/)"
11
+ },
12
+ "files": {
13
+ "README.md": {
14
+ "bytes": 5392,
15
+ "sha256": "c32d9fa4f7dbbc53ecdad2131c1e33596e57f6045acc8f0790a9d2431f014b11",
16
+ "source": "pack file"
17
+ },
18
+ "fermion_mlx/__init__.py": {
19
+ "bytes": 5063,
20
+ "sha256": "a547844d587067681639df546e6d010dab80bbd4f0ee55c6e2a16fba97a1dd40",
21
+ "source": "NEW: load()/generate()/chat() API"
22
+ },
23
+ "fermion_mlx/__main__.py": {
24
+ "bytes": 8069,
25
+ "sha256": "71e8c7fea446184bd471af2c37e51b04b9fa8d0f5d35e0a76c31f6fd19af957d",
26
+ "source": "NEW: CLI (gen/chat/gate/cachegate/pathgate/bench) + spec/specgate/specbench modes"
27
+ },
28
+ "fermion_mlx/arch3.py": {
29
+ "bytes": 7541,
30
+ "sha256": "a831e8a24b2b9a986b393d1c316aac66a3399a082a57176254b2feeae032f8cd",
31
+ "source": "NEW (2026-07-25): fused per-head QK-RMSNorm+rope+KV-append kernel + s8 v3 x-staged int8 unembed"
32
+ },
33
+ "fermion_mlx/container.py": {
34
+ "bytes": 6059,
35
+ "sha256": "c5b352184ba9ddf9a3e424731226cb7dcb9901d3a1516c67d4557953b0101f2f",
36
+ "source": "NEW: TRTC v4 arch-3 reader"
37
+ },
38
+ "fermion_mlx/gates.py": {
39
+ "bytes": 7394,
40
+ "sha256": "64d07d9ba1a783a3f56ba978da7885e9fbe693495b3a4b7b8e176607d59de52c",
41
+ "source": "NEW: house V1 refgate / cachegate / pathgate"
42
+ },
43
+ "fermion_mlx/mbatch.py": {
44
+ "bytes": 6492,
45
+ "sha256": "c9ec8d37f1cdc0554cda01cefbc435e67525779c79e0dac7fd760e7919e6fdd2",
46
+ "source": "NEW (2026-07-25): M-token batched trit/s8 GEMMs for speculative verify (one weight read per k+1 positions)"
47
+ },
48
+ "fermion_mlx/model.py": {
49
+ "bytes": 12748,
50
+ "sha256": "9488f8194ce8be006ce05dfaf6118353de44005ce54f38f73a0908ccac2749da",
51
+ "source": "NEW: MetalTritQwen3 walker (fused primary path + mx.fast-ops reference twin)"
52
+ },
53
+ "fermion_mlx/spec.py": {
54
+ "bytes": 11207,
55
+ "sha256": "969e946d5d2eeb63d44afa4c62dd2453d87fa14e83b936bc673482b59ee3b745",
56
+ "source": "NEW (2026-07-25): EXPERIMENTAL greedy-exact speculative decoding (0.6B drafts, 8B verifies), exactness-gated"
57
+ },
58
+ "fermion_mlx/trit_kernels.py": {
59
+ "bytes": 44135,
60
+ "sha256": "aeb8bd4b03419763c1543893f19be3b4029611b2df883096990c4beebade384b",
61
+ "source": "the banked five-value Metal GEMV stack"
62
+ },
63
+ "generation_config.json": {
64
+ "bytes": 181,
65
+ "sha256": "6ca52b0bcb818c9e52db8ea18413494110c5f2def581b769f621dda951a7863b",
66
+ "source": "donor Qwen/Qwen3-0.6B @ c1899de289a04d12100db370d81485cdf75e47ca (pinned revision from export receipts / greedy cache)"
67
+ },
68
+ "merges.txt": {
69
+ "bytes": 1671853,
70
+ "sha256": "8831e4f1a044471340f7c0a83d7bd71306a5b867e95fd870f74d0c5308a904d5",
71
+ "source": "donor Qwen/Qwen3-0.6B @ c1899de289a04d12100db370d81485cdf75e47ca (pinned revision from export receipts / greedy cache)"
72
+ },
73
+ "receipts/bench_0p6b_m5.json": {
74
+ "bytes": 812,
75
+ "sha256": "543a9e41d8374a8cd066643a4e7cfa925359a8efc9c3182cbebb366bb0afbe89",
76
+ "source": "gate/bench receipt (2026-07-25)"
77
+ },
78
+ "receipts/cachegate_0p6b.json": {
79
+ "bytes": 775,
80
+ "sha256": "b6acd64a31f4529ea01b96103a44b5bc54da283a7c3250cd564b5510ebaabc8c",
81
+ "source": "gate/bench receipt (2026-07-25)"
82
+ },
83
+ "receipts/kernel_version_sweep_0p6b.json": {
84
+ "bytes": 355,
85
+ "sha256": "6b0df2026ded00c50d6b34004bf2dc4aabf097d21decfad7a354de5be594fe87",
86
+ "source": "gate/bench receipt (2026-07-25)"
87
+ },
88
+ "receipts/pathgate_0p6b.json": {
89
+ "bytes": 623,
90
+ "sha256": "2453e15509a012616a100e3767a373af20a763dff89104e3c4dfbeb24085d072",
91
+ "source": "gate/bench receipt (2026-07-25)"
92
+ },
93
+ "receipts/refgate_0p6b.json": {
94
+ "bytes": 1907,
95
+ "sha256": "ee4b3949bad85a522f1d1b65344a230a40f9fe9dc5e5dfdca736c28751eb4d85",
96
+ "source": "gate/bench receipt (2026-07-25)"
97
+ },
98
+ "requirements.txt": {
99
+ "bytes": 191,
100
+ "sha256": "1cf4d1fa581aaa5deae0568f88388c51166bbd69e7a89080b5a53488a7ef39b6",
101
+ "source": "pack file"
102
+ },
103
+ "tokenizer.json": {
104
+ "bytes": 11422654,
105
+ "sha256": "aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4",
106
+ "source": "donor Qwen/Qwen3-0.6B @ c1899de289a04d12100db370d81485cdf75e47ca (pinned revision from export receipts / greedy cache)"
107
+ },
108
+ "tokenizer_config.json": {
109
+ "bytes": 9732,
110
+ "sha256": "d5d09f07b48c3086c508b30d1c9114bd1189145b74e982a265350c923acd8101",
111
+ "source": "donor Qwen/Qwen3-0.6B @ c1899de289a04d12100db370d81485cdf75e47ca (pinned revision from export receipts / greedy cache)"
112
+ },
113
+ "vocab.json": {
114
+ "bytes": 2776833,
115
+ "sha256": "ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910",
116
+ "source": "donor Qwen/Qwen3-0.6B @ c1899de289a04d12100db370d81485cdf75e47ca (pinned revision from export receipts / greedy cache)"
117
+ }
118
+ },
119
+ "addenda": [
120
+ "2026-07-25: EXPERIMENTAL --spec speculative decoding added (new modules mbatch.py/spec.py + CLI modes + README section). Exactness-gated: specgate 6/6 token-identical vs plain (receipts). Existing gate battery re-verified on the updated package."
121
+ ]
122
+ }
mlx/README.md ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Neutrino-0.6B — MLX pack (Apple silicon)
2
+
3
+ > [!IMPORTANT]
4
+ > **Neutrino-0.6B exists as the speculative-decoding DRAFT for
5
+ > Neutrino-8B** — that pairing is its supported purpose. It is a BASE
6
+ > model: not an assistant, no chat/instruction/tool claims. For a small
7
+ > conversational model use Neutrino-0.6B-Chat.
8
+
9
+ Run the [Neutrino-0.6B](../README.md) five-value (sub-2-bit) container
10
+ natively from Python on Apple silicon: MLX + our custom Metal kernels. The
11
+ weights stay bit-packed at rest — the five-value {0, ±s_lo, ±s_hi} planes
12
+ are decoded inside the GEMV kernels; nothing in the decode path is
13
+ fp16/fp32 weight material.
14
+
15
+ This pack is the **Python/MLX lane**. The fastest way to run this model on
16
+ a Mac remains the `fermion-run` CPU binary shipped in the Neutrino-8B
17
+ repo's `bin/` (225–236 tok/s on M5, 9 threads); this pack is for MLX users
18
+ who want the model inside a Python process (notebooks, pipelines, mlx
19
+ ecosystems) with certified-faithful outputs.
20
+
21
+ ## Install & run
22
+
23
+ ```bash
24
+ pip install -r requirements.txt # mlx, numpy, transformers
25
+ # fetch neutrino-0.6b_v4.bin per the parent card (sha256 must match)
26
+ python -m fermion_mlx --model neutrino-0.6b_v4.bin --mode gen \
27
+ --tokenizer . --prompt "The key idea behind ternary neural networks is"
28
+ ```
29
+
30
+ Python API:
31
+
32
+ ```python
33
+ import fermion_mlx
34
+ from transformers import AutoTokenizer
35
+
36
+ model = fermion_mlx.load("neutrino-0.6b_v4.bin") # memory-maps the container
37
+ tok = AutoTokenizer.from_pretrained(".") # tokenizer files ship here
38
+ text, stats = fermion_mlx.generate(model, tok, "2, 3, 5, 7, 11,",
39
+ max_tokens=64)
40
+ print(text, stats) # {'tok_s': ..., ...}
41
+ ```
42
+
43
+ This is a BASE model: prompt it as a completion engine (see the parent
44
+ card's honest-numbers table before using it for anything capability-heavy).
45
+
46
+ ## What runs on the GPU
47
+
48
+ - 3-bit-planar five-value GEMV (`trit_kernels.py`, kernel v7: threadgroup
49
+ LUT decode, interleaved 8-row plane layout) for qkv / o / gate+up / down,
50
+ with RMSNorm, SwiGLU and residual adds fused into the GEMV epilogues.
51
+ - The arch-3 (Qwen3) delta — **per-head Q/K-RMSNorm + NeoX rope + KV-cache
52
+ append** — fused into one dispatch (`arch3.py`).
53
+ - int8 embedding/unembedding lanes with threadgroup x-staging (`arch3.py`,
54
+ s8 v3); embeddings dequantize one row per token (int8 × fp32 row scale —
55
+ exact).
56
+ - MLX supplies SDPA (fp32 KV, block-grown windows), argmax, and the lazy
57
+ graph; the decode step is `mx.compile`d.
58
+
59
+ ## Correctness receipts (this exact container, sha `91c0b26d…`)
60
+
61
+ House rule: greedy streams must be token-identical to the container's own
62
+ fp expansion reference; near-tie argmax flips at tiny logit margins are the
63
+ only tolerated class, and every flip must be documented with its margin.
64
+ Receipts in `receipts/`:
65
+
66
+ | gate | reference | result |
67
+ |---|---|---|
68
+ | V1 refgate | fp32 HF expansion of this container (eager attn), teacher-forced 9 pos + 24-token greedy | **PASS — TF rel err 0.000% (max), cos 1.000000, argmax 9/9, greedy 24/24, 0 flips** |
69
+ | cachegate | banked HF fp16 greedy cache `86565bf6` (3 prompts × 128 steps, Modal CPU) | **PASS — 0 flips / 384 tokens** |
70
+ | pathgate | fused-kernel path vs pure `mx.fast`-ops twin | 32-token greedy IDENTICAL |
71
+
72
+ Note: the int8-activation CPU engine documents 2 benign near-tie flips
73
+ (top-2 margins 0.02–0.20 logits) against the same fp16 cache on this
74
+ checkpoint's flat logit landscape. The MLX path (fp32 activations) resolves
75
+ all 384 positions identically to the HF reference — 0 flips, no near-tie
76
+ class needed.
77
+
78
+ ## Speed (honest numbers, with venue)
79
+
80
+ | path | venue | 512-token greedy decode |
81
+ |---|---|---|
82
+ | this MLX pack (kernel v7) | Apple M5 MacBook (16 GB), macOS, MLX 0.31.2, median of 5 runs, 2026-07-25 | **201 tok/s** (197–202 across runs, peak MLX mem 0.53 GiB) |
83
+ | `fermion-run` CPU binary (same container) | same M5, 9 threads, banked re-receipt | 225–236 tok/s |
84
+
85
+ The CPU binary remains this SKU's speed crown; the MLX lane buys native
86
+ Python integration with certified outputs. Kernel version selectable via
87
+ `TRIT_METAL_V` (7 = default/fastest; the sweep receipt is in
88
+ `receipts/`).
89
+
90
+ ## EXPERIMENTAL: this pack drafts for Neutrino-8B
91
+
92
+ The same package powers `--spec` speculative decoding where this 0.6B
93
+ drafts and the Neutrino-8B pack verifies under the greedy-exact acceptance
94
+ rule (output stream exactness-gated, token-identical to plain 8B decode).
95
+ See the Neutrino-8B MLX pack README for the mode, receipts, and honest
96
+ per-text-type numbers (real speedup on high-acceptance text only).
97
+
98
+ ## Files
99
+
100
+ - `fermion_mlx/` — the loader/runtime package (self-contained, including
101
+ the Metal GEMV kernels).
102
+ - `tokenizer.json`, `tokenizer_config.json`, `vocab.json`, `merges.txt`,
103
+ `generation_config.json` — donor tokenizer (Qwen/Qwen3-0.6B @
104
+ `c1899de289a04d12100db370d81485cdf75e47ca`, the pinned revision from the
105
+ export receipts).
106
+ - `receipts/` — gate + bench receipts (JSON), kernel-source SHA sidecar.
107
+ - `MANIFEST.json` — file-by-file provenance + shas.
108
+
109
+ Weights: not duplicated in this subfolder — use the parent card's
110
+ materialization instructions (`neutrino-0.6b_v4.bin`, sha256
111
+ `91c0b26d105710e755ef1ec1b5607c00da5d5cbd048fd5584135fb6fc1e19399`).
112
+
113
+ ## License
114
+
115
+ Apache-2.0 (weights and this pack's code). Derivative of Qwen/Qwen3-0.6B —
116
+ see the parent `LICENSE`.
mlx/fermion_mlx/__init__.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """fermion_mlx — run Fermion Research TRTC v4 arch-3 (Qwen3-family) ternary
2
+ containers on Apple silicon via MLX + custom Metal kernels.
3
+
4
+ import fermion_mlx
5
+ model = fermion_mlx.load("neutrino-0p6b_v4_base.bin")
6
+ text = fermion_mlx.generate(model, tokenizer, "The capital of France is",
7
+ max_tokens=64)
8
+
9
+ The five-value {0, ±s_lo, ±s_hi} weights stay bit-packed at rest and are
10
+ decoded inside the GEMV kernels; nothing is expanded to fp16/fp32 weights.
11
+ Kernel version: env TRIT_METAL_V (default 7 = the banked LUT-decode bodies).
12
+ """
13
+ import os
14
+
15
+ # choose the crown kernel bodies before the kernel module is imported
16
+ # (v9 = single-barrier double-buffered LUT, gated bit-identical to v7;
17
+ # set TRIT_METAL_V=7 to fall back to the previous default)
18
+ os.environ.setdefault("TRIT_METAL_V", "9")
19
+
20
+ from .model import MetalTritQwen3 # noqa: E402
21
+
22
+
23
+ def load(container_path, maxseq=1024, verbose=True):
24
+ """Memory-map a TRTC v4 arch-3 container and build the Metal model."""
25
+ return MetalTritQwen3(container_path, maxseq=maxseq, verbose=verbose)
26
+
27
+
28
+ DEFAULT_EOS = (151645, 151643) # <|im_end|>, <|endoftext|>
29
+
30
+
31
+ def _as_ids(x):
32
+ """Normalise whatever a tokenizer handed back into a flat list of ids.
33
+
34
+ transformers >= 4.56 flipped `apply_chat_template(tokenize=True)` to
35
+ return a BatchEncoding instead of a list of ids (return_dict defaults to
36
+ True). requirements.txt says `transformers>=4.53`, so a fresh install gets
37
+ the new behaviour and the old code fed a BatchEncoding straight into
38
+ `model.prefill`, which died with
39
+ `int() argument must be ... not 'tokenizers.Encoding'`.
40
+ """
41
+ if hasattr(x, "input_ids"):
42
+ x = x.input_ids
43
+ elif isinstance(x, dict) and "input_ids" in x:
44
+ x = x["input_ids"]
45
+ if len(x) and isinstance(x[0], (list, tuple)): # batched -> first row
46
+ x = x[0]
47
+ return [int(t) for t in x]
48
+
49
+
50
+ def _rp_greedy(model, ids, n, rp, eos=None):
51
+ """Greedy decode with the CTRL/HF multiplicative repetition penalty.
52
+
53
+ Byte-for-byte the rule the graded MLX battery used
54
+ (`rp_greedy`): the penalty applies to every id SEEN SO FAR in the whole
55
+ context, not to a 256-token window — that is the MLX-side convention and
56
+ it is what the banked MLX numbers on the model card were produced at.
57
+
58
+ This path materialises the logits every step, so it forfeits the fused
59
+ fast path's one-step async lookahead. It is only taken when rp != 1.0;
60
+ `model.generate` (rp = 1.0) is untouched, so the banked tok/s receipts
61
+ still describe the default.
62
+ """
63
+ import time
64
+
65
+ import mlx.core as mx
66
+ import numpy as np
67
+
68
+ eos = set(eos or ())
69
+ mask = model.new_mask()
70
+ x, pos0 = model.prefill(ids, mask)
71
+ seen = set(int(t) for t in ids)
72
+ toks = []
73
+ t0 = time.perf_counter()
74
+ for i in range(n):
75
+ pb = mx.array([pos0 + i], dtype=mx.int32)
76
+ W = model._window(pos0 + i)
77
+ nt, xn, lg = model.step_compiled(x, pb, mask, W)
78
+ lgn = np.array(lg, copy=True)
79
+ idxs = np.fromiter(seen, dtype=np.int64)
80
+ vals = lgn[idxs]
81
+ lgn[idxs] = np.where(vals > 0, vals / rp, vals * rp)
82
+ tsel = int(lgn.argmax())
83
+ toks.append(tsel)
84
+ if tsel in eos:
85
+ break
86
+ x = model.embed(tsel)
87
+ seen.add(tsel)
88
+ return toks, time.perf_counter() - t0
89
+
90
+
91
+ def _decode(model, tokenizer, ids, max_tokens, eos, rep_penalty):
92
+ if rep_penalty and float(rep_penalty) != 1.0:
93
+ toks, dt = _rp_greedy(model, ids, max_tokens, float(rep_penalty),
94
+ eos=eos)
95
+ else:
96
+ toks, dt = model.generate(ids, max_tokens, eos=eos)
97
+ if eos:
98
+ cut = next((i for i, t in enumerate(toks) if t in eos), len(toks))
99
+ toks = toks[:cut]
100
+ text = tokenizer.decode(toks)
101
+ rate = (len(toks) or 1) / dt if dt > 0 else 0.0
102
+ return text, {"tokens": len(toks), "seconds": dt, "tok_s": rate}
103
+
104
+
105
+ def generate(model, tokenizer, prompt, max_tokens=256, stop_on_eos=True,
106
+ rep_penalty=1.0):
107
+ """Greedy completion of a plain-text prompt. Returns (text, stats).
108
+
109
+ `rep_penalty` > 1.0 enables the CTRL-style repetition penalty the graded
110
+ MLX battery ran at (1.05 on the chat SKU); 1.0, the default, is the
111
+ untouched pure-argmax fast path.
112
+ """
113
+ ids = _as_ids(tokenizer(prompt))
114
+ eos = set(DEFAULT_EOS) if stop_on_eos else None
115
+ return _decode(model, tokenizer, ids, max_tokens, eos, rep_penalty)
116
+
117
+
118
+ def chat(model, tokenizer, messages, max_tokens=256, rep_penalty=1.0):
119
+ """Chat completion via the donor chat template (thinking off).
120
+
121
+ `rep_penalty` as in `generate`; the chat SKU's card documents 1.05.
122
+ """
123
+ try:
124
+ raw = tokenizer.apply_chat_template(messages, add_generation_prompt=True,
125
+ enable_thinking=False)
126
+ except TypeError:
127
+ raw = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
128
+ ids = _as_ids(raw)
129
+ return _decode(model, tokenizer, ids, max_tokens, set(DEFAULT_EOS),
130
+ rep_penalty)
mlx/fermion_mlx/__main__.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """fermion_mlx CLI.
3
+
4
+ python -m fermion_mlx --model x.bin --mode gen --prompt "..."
5
+ python -m fermion_mlx --model x.bin --mode chat --prompt "..." --tokenizer DIR
6
+ python -m fermion_mlx --model x.bin --mode chat --prompt "..." --tokenizer DIR \
7
+ --rep-penalty 1.05 # the graded chat-SKU config
8
+ python -m fermion_mlx --model x.bin --mode gate --ref neutrino_ref.npz
9
+ python -m fermion_mlx --model x.bin --mode cachegate --cache hf_greedy_cache.json
10
+ python -m fermion_mlx --model x.bin --mode pathgate
11
+ python -m fermion_mlx --model x.bin --mode bench --runs 5 --gen-tokens 512
12
+
13
+ EXPERIMENTAL speculative decoding (0.6B drafts, exactness-gated):
14
+ python -m fermion_mlx --model 8b.bin --draft-model 0.6b.bin --mode spec \
15
+ --prompt "..." --tokenizer DIR --mem-limit-gb 6
16
+ python -m fermion_mlx --model 8b.bin --draft-model 0.6b.bin --mode specgate \
17
+ --tokenizer DIR --mem-limit-gb 6 --receipt specgate.json
18
+ python -m fermion_mlx --model 8b.bin --draft-model 0.6b.bin --mode specbench \
19
+ --tokenizer DIR --mem-limit-gb 6 --receipt specbench.json
20
+
21
+ Gate receipts are written next to --receipt (JSON) when given.
22
+ """
23
+ import argparse
24
+ import json
25
+ import sys
26
+ import time
27
+
28
+ import mlx.core as mx
29
+
30
+ import fermion_mlx
31
+ from fermion_mlx.gates import refgate, cachegate, pathgate
32
+
33
+ HOUSE_PROMPT = "The key idea behind ternary neural networks is"
34
+ # tokenizer-free fallback: Qwen3-tokenized HOUSE_PROMPT (donor-verified ids)
35
+ HOUSE_IDS = [785, 1376, 4522, 4815, 71617, 658, 29728, 14155, 374]
36
+
37
+
38
+ def _tok(args):
39
+ from transformers import AutoTokenizer
40
+ return AutoTokenizer.from_pretrained(args.tokenizer)
41
+
42
+
43
+ def main():
44
+ ap = argparse.ArgumentParser()
45
+ ap.add_argument("--model", required=True)
46
+ ap.add_argument("--mode", default="gen",
47
+ choices=["gen", "chat", "gate", "cachegate", "pathgate",
48
+ "bench", "spec", "specgate", "specbench"])
49
+ ap.add_argument("--draft-model", default="",
50
+ help="draft container for --mode spec* (e.g. the 0.6B pack)")
51
+ ap.add_argument("--spec-k", type=int, default=4,
52
+ help="draft tokens per speculative round")
53
+ ap.add_argument("--prompt", default=HOUSE_PROMPT)
54
+ ap.add_argument("--tokenizer", default="Qwen/Qwen3-0.6B")
55
+ ap.add_argument("--ref", default="")
56
+ ap.add_argument("--cache", default="")
57
+ ap.add_argument("--receipt", default="")
58
+ ap.add_argument("--tie-tol", type=float, default=0.04, dest="tie_tol")
59
+ ap.add_argument("--max-flips", type=int, default=8, dest="max_flips")
60
+ ap.add_argument("--gen-tokens", type=int, default=512)
61
+ ap.add_argument("--runs", type=int, default=5)
62
+ ap.add_argument("--maxseq", type=int, default=1024)
63
+ ap.add_argument("--no-tokenizer", action="store_true",
64
+ help="use the pinned house prompt ids (no transformers)")
65
+ ap.add_argument("--mem-limit-gb", type=float, default=0.0,
66
+ help="hard MLX memory limit; the process fails fast "
67
+ "instead of pressuring the machine (house OOM rule)")
68
+ ap.add_argument("--rep-penalty", type=float, default=1.0,
69
+ dest="rep_penalty",
70
+ help="CTRL-style repetition penalty over the whole "
71
+ "context, the rule the graded MLX battery ran at "
72
+ "(1.05 on the chat SKU). 1.0 (default) = the "
73
+ "untouched pure-argmax fast path.")
74
+ args = ap.parse_args()
75
+
76
+ if args.mem_limit_gb > 0:
77
+ mx.set_memory_limit(int(args.mem_limit_gb * 2**30))
78
+ print(f"[mem] MLX memory limit set to {args.mem_limit_gb:.1f} GiB")
79
+
80
+ model = fermion_mlx.load(args.model, maxseq=args.maxseq)
81
+
82
+ def save(rec):
83
+ if args.receipt:
84
+ json.dump(rec, open(args.receipt, "w"), indent=1)
85
+ print(f"receipt -> {args.receipt}")
86
+
87
+ if args.mode in ("spec", "specgate", "specbench"):
88
+ from fermion_mlx.spec import SpecDecoder, specgate, specbench
89
+ assert args.draft_model, "--mode spec* needs --draft-model"
90
+ draft = fermion_mlx.load(args.draft_model, maxseq=args.maxseq)
91
+ tok = _tok(args)
92
+ if args.mode == "specgate":
93
+ rec = specgate(model, draft, tok, k=args.spec_k,
94
+ receipt=args.receipt or None)
95
+ print(f"[specgate] {rec['verdict']}")
96
+ sys.exit(0 if rec["verdict"] == "PASS" else 1)
97
+ if args.mode == "specbench":
98
+ rec = specbench(model, draft, tok, k=args.spec_k,
99
+ n=args.gen_tokens, runs=args.runs,
100
+ receipt=args.receipt or None)
101
+ for t, v in rec["types"].items():
102
+ print(f"[specbench {t}] plain {v['plain_median_tok_s']} -> "
103
+ f"spec {v['spec_median_tok_s']} tok/s "
104
+ f"({v['speedup']}x, acc {v['acceptance_median']})")
105
+ return
106
+ sd = SpecDecoder(model, draft, k=args.spec_k)
107
+ ids = tok(args.prompt, add_special_tokens=False).input_ids
108
+ toks, dt, st = sd.generate(ids, args.gen_tokens)
109
+ print(tok.decode(toks))
110
+ print(f"[spec: {len(toks)} tok in {dt*1e3:.0f} ms -> "
111
+ f"{len(toks)/dt:.1f} tok/s | acceptance {st['acceptance']:.3f} "
112
+ f"| {st['tokens_per_round']} tok/round]")
113
+ return
114
+
115
+ if args.mode == "gate":
116
+ ok, rec = refgate(model, args.ref, args.tie_tol, args.max_flips)
117
+ rec["mode"], rec["model"] = "refgate", args.model
118
+ save(rec)
119
+ sys.exit(0 if ok else 1)
120
+
121
+ if args.mode == "cachegate":
122
+ ok, rec = cachegate(model, args.model, args.cache, args.tie_tol,
123
+ args.max_flips)
124
+ rec["mode"], rec["model"] = "cachegate", args.model
125
+ save(rec)
126
+ sys.exit(0 if ok else 1)
127
+
128
+ if args.mode == "pathgate":
129
+ ids = (HOUSE_IDS if args.no_tokenizer else
130
+ fermion_mlx._as_ids(_tok(args)(args.prompt)))
131
+ ok, rec = pathgate(model, ids, n=32)
132
+ rec["mode"], rec["model"] = "pathgate", args.model
133
+ save(rec)
134
+ sys.exit(0 if ok else 1)
135
+
136
+ if args.mode == "bench":
137
+ ids = (HOUSE_IDS if args.no_tokenizer else
138
+ fermion_mlx._as_ids(_tok(args)(args.prompt)))
139
+ n = args.gen_tokens
140
+ model.generate(ids, 16) # warmup + kernel JIT
141
+ rates, runs = [], []
142
+ for r in range(args.runs):
143
+ toks, dt = model.generate(ids, n) # no EOS stop: full budget
144
+ rates.append(n / dt)
145
+ runs.append({"run": r, "tokens": n, "seconds": dt, "tok_s": n / dt})
146
+ print(f" run {r}: {n} tok in {dt*1e3:.0f} ms -> {rates[-1]:.1f} tok/s")
147
+ rates.sort()
148
+ med = rates[len(rates) // 2]
149
+ peak = mx.get_peak_memory() / 2**30
150
+ print(f"[bench] median {med:.1f} tok/s over {args.runs} runs "
151
+ f"({n}-token greedy), peak mem {peak:.2f} GiB")
152
+ save({"mode": "bench", "model": args.model, "prompt_ids": ids,
153
+ "gen_tokens": n, "runs": runs, "median_tok_s": med,
154
+ "peak_mem_gib": peak,
155
+ "utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())})
156
+ return
157
+
158
+ tok = _tok(args)
159
+ if args.mode == "gen":
160
+ text, st = fermion_mlx.generate(model, tok, args.prompt,
161
+ max_tokens=args.gen_tokens,
162
+ rep_penalty=args.rep_penalty)
163
+ print(text)
164
+ print(f"[{st['tokens']} tokens in {st['seconds']*1e3:.0f} ms -> "
165
+ f"{st['tok_s']:.1f} tok/s]")
166
+ else:
167
+ text, st = fermion_mlx.chat(model, tok,
168
+ [{"role": "user", "content": args.prompt}],
169
+ max_tokens=args.gen_tokens,
170
+ rep_penalty=args.rep_penalty)
171
+ print(text)
172
+ print(f"[{st['tokens']} tokens in {st['seconds']*1e3:.0f} ms -> "
173
+ f"{st['tok_s']:.1f} tok/s]")
174
+
175
+
176
+ if __name__ == "__main__":
177
+ main()
mlx/fermion_mlx/arch3.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Arch-3 (Qwen3) Metal kernel: fused per-head Q/K-RMSNorm + NeoX rope +
3
+ KV-cache append, one dispatch per decode step.
4
+
5
+ The Qwen3 arch delta vs arch-1 is the per-head RMSNorm applied to the qkv
6
+ GEMV output BEFORE rope (HF order: q_norm/k_norm over head_dim, weights
7
+ shared across heads). This kernel is the arch-3 twin of
8
+ trit_kernels.rope_append: one simdgroup per head; q/k heads compute the
9
+ head's rms via simd_sum then rope the normed values; v heads copy raw
10
+ values into the cache. kc/vc/mask are written in place at position pos
11
+ (stream-serial ordering with the consumer SDPA — the banked rope_append
12
+ pattern; correctness enforced by the e2e gates).
13
+ """
14
+ import mlx.core as mx
15
+
16
+ _CACHE = {}
17
+ SG_PER_TG = 8
18
+
19
+
20
+ def _s8_source_v3(rows, cols, rs, sg_per_tg, fuse_norm, eps, rpsg=8):
21
+ """arch-3 lane addition: int8 GEMV with threadgroup x staging.
22
+
23
+ The vendored s8 v2 kernel re-reads the full x vector from device memory
24
+ once per simdgroup (rows/rpsg simdgroups x cols floats — for the 151936-
25
+ row Qwen3 unembed that is +50% of the weight bytes). v3 stages the
26
+ effective x (rmsnorm applied) into threadgroup memory once per
27
+ THREADGROUP, tile by tile; every simdgroup then streams only its int8
28
+ rows. Requires rows % (rpsg * sg_per_tg) == 0 (true for vocab 151936)."""
29
+ assert rs % 4 == 0 and cols % 4 == 0 and rows % (rpsg * sg_per_tg) == 0
30
+ tile = min(cols, 1024)
31
+ assert cols % tile == 0
32
+ ntiles = cols // tile
33
+ nthreads = sg_per_tg * 32
34
+ if fuse_norm:
35
+ norm_pre = f"""
36
+ threadgroup float ssq_sh[{sg_per_tg}];
37
+ {{
38
+ float ssq = 0.0f;
39
+ for (uint c = tid; c < {cols}u; c += {nthreads}u) {{
40
+ float v = x[c]; ssq = metal::fma(v, v, ssq);
41
+ }}
42
+ ssq = metal::simd_sum(ssq);
43
+ if (lane == 0u) ssq_sh[sg] = ssq;
44
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
45
+ float tt = 0.0f;
46
+ for (uint i = 0u; i < {sg_per_tg}u; ++i) tt += ssq_sh[i];
47
+ rms = metal::rsqrt(tt / {float(cols)}f + {eps:.9g}f);
48
+ }}
49
+ """
50
+ stage = f"xb[cl] = x[c] * nw[c] * rms;"
51
+ else:
52
+ norm_pre = ""
53
+ stage = "xb[cl] = x[c];"
54
+ return f"""
55
+ uint tid = thread_position_in_threadgroup.x;
56
+ uint lane = tid & 31u;
57
+ uint sg = tid >> 5u;
58
+ uint row0 = (threadgroup_position_in_grid.x * {sg_per_tg}u + sg) * {rpsg}u;
59
+ float rms = 1.0f;
60
+ {norm_pre}
61
+ threadgroup float xb[{tile}];
62
+ float acc[{rpsg}];
63
+ for (uint r = 0u; r < {rpsg}u; ++r) acc[r] = 0.0f;
64
+ const float4 one4 = float4(1.0f);
65
+ for (uint t = 0u; t < {ntiles}u; ++t) {{
66
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
67
+ for (uint cl = tid; cl < {tile}u; cl += {nthreads}u) {{
68
+ uint c = t * {tile}u + cl;
69
+ {stage}
70
+ }}
71
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
72
+ for (uint i = lane * 4u; i < {tile}u; i += 128u) {{
73
+ float4 xv = *(const threadgroup float4*)(xb + i);
74
+ for (uint r = 0u; r < {rpsg}u; ++r) {{
75
+ char4 wv = *(const device char4*)((const device char*)W
76
+ + (ulong)(row0 + r) * {rs}u + t * {tile}u + i);
77
+ acc[r] += metal::dot(float4(wv.x, wv.y, wv.z, wv.w) * xv, one4);
78
+ }}
79
+ }}
80
+ }}
81
+ for (uint r = 0u; r < {rpsg}u; ++r) {{
82
+ float a = metal::simd_sum(acc[r]);
83
+ if (lane == 0u) out[row0 + r] = scale[row0 + r] * a;
84
+ }}
85
+ """
86
+
87
+
88
+ def get_s8_gemv_v3(rows, cols, rs, fuse_norm=False, eps=1e-6):
89
+ key = ("s8v3", rows, cols, rs, fuse_norm, round(eps, 12))
90
+ if key in _CACHE:
91
+ return _CACHE[key]
92
+ inputs = ["W", "scale", "x"]
93
+ if fuse_norm:
94
+ inputs.append("nw")
95
+ src = _s8_source_v3(rows, cols, rs, SG_PER_TG, fuse_norm, eps)
96
+ k = mx.fast.metal_kernel(name="s8gemv3_r%d_c%d_n%d" % (rows, cols, fuse_norm),
97
+ input_names=inputs, output_names=["out"], source=src)
98
+ _CACHE[key] = (k, inputs)
99
+ return _CACHE[key]
100
+
101
+
102
+ def s8_gemv_v3(W, scale, x, norm_w=None, eps=1e-6, stream=None):
103
+ """drop-in for trit_kernels.s8_gemv where rows % 64 == 0."""
104
+ rows, rs = W.shape
105
+ cols = x.shape[0]
106
+ k, names = get_s8_gemv_v3(rows, cols, rs, norm_w is not None, eps)
107
+ vals = {"W": W, "scale": scale, "x": x, "nw": norm_w}
108
+ nsg = rows // 8
109
+ ntg = (nsg + SG_PER_TG - 1) // SG_PER_TG
110
+ return k(inputs=[vals[n] for n in names],
111
+ grid=(ntg * SG_PER_TG * 32, 1, 1), threadgroup=(SG_PER_TG * 32, 1, 1),
112
+ output_shapes=[(rows,)], output_dtypes=[mx.float32], stream=stream)[0]
113
+
114
+
115
+ def get_qknorm_rope_append(n_heads, n_kv, hd, maxseq, eps):
116
+ key = ("qkra", n_heads, n_kv, hd, maxseq, round(eps, 12))
117
+ if key in _CACHE:
118
+ return _CACHE[key]
119
+ h2 = hd // 2
120
+ nheads_total = n_heads + 2 * n_kv
121
+ src = f"""
122
+ uint tid = thread_position_in_threadgroup.x;
123
+ uint lane = tid & 31u;
124
+ uint sg = tid >> 5u;
125
+ uint head = threadgroup_position_in_grid.x * {SG_PER_TG}u + sg;
126
+ if (head >= {nheads_total}u) return;
127
+ uint pos = (uint)posbuf[0];
128
+ if (head == 0u && lane == 0u) {{ ((device float*)mask)[pos] = 0.0f; }}
129
+ device float* kc_w = (device float*)kc;
130
+ device float* vc_w = (device float*)vc;
131
+ if (head < {n_heads + n_kv}u) {{
132
+ uint base = head * {hd}u;
133
+ const device float* g = (head < {n_heads}u) ? qn : kn;
134
+ float ssq = 0.0f;
135
+ for (uint d = lane; d < {hd}u; d += 32u) {{
136
+ float v = qkv[base + d];
137
+ ssq = metal::fma(v, v, ssq);
138
+ }}
139
+ ssq = metal::simd_sum(ssq);
140
+ float rms = metal::rsqrt(ssq / {float(hd)}f + {eps:.9g}f);
141
+ for (uint d = lane; d < {hd}u; d += 32u) {{
142
+ float c = ct[pos * {h2}u + (d % {h2}u)];
143
+ float sn = st[pos * {h2}u + (d % {h2}u)];
144
+ float v = qkv[base + d] * rms * g[d];
145
+ float vr = (d < {h2}u)
146
+ ? -(qkv[base + d + {h2}u] * rms * g[d + {h2}u])
147
+ : (qkv[base + d - {h2}u] * rms * g[d - {h2}u]);
148
+ float r = metal::fma(v, c, vr * sn);
149
+ if (head < {n_heads}u) {{
150
+ qout[base + d] = r;
151
+ }} else {{
152
+ uint kh = head - {n_heads}u;
153
+ kc_w[(ulong)(kh * {maxseq}u + pos) * {hd}u + d] = r;
154
+ }}
155
+ }}
156
+ }} else {{
157
+ uint vh = head - {n_heads + n_kv}u;
158
+ for (uint d = lane; d < {hd}u; d += 32u) {{
159
+ vc_w[(ulong)(vh * {maxseq}u + pos) * {hd}u + d] =
160
+ qkv[{(n_heads + n_kv) * hd}u + vh * {hd}u + d];
161
+ }}
162
+ }}
163
+ """
164
+ k = mx.fast.metal_kernel(
165
+ name="qknorm_rope_append_%d_%d_%d" % (n_heads, n_kv, hd),
166
+ input_names=["qkv", "qn", "kn", "kc", "vc", "ct", "st", "posbuf", "mask"],
167
+ output_names=["qout"], source=src)
168
+ _CACHE[key] = k
169
+ return k
170
+
171
+
172
+ def qknorm_rope_append(qkv, qn, kn, kc, vc, ct, st, posbuf, mask,
173
+ n_heads, n_kv, hd, maxseq, eps, stream=None):
174
+ """qkv fp32 [(n_heads+2*n_kv)*hd] -> roped+normed q fp32 [n_heads*hd];
175
+ writes normed+roped k and raw v into kc/vc at position posbuf[0] and
176
+ zeroes mask[pos]."""
177
+ k = get_qknorm_rope_append(n_heads, n_kv, hd, maxseq, eps)
178
+ nheads_total = n_heads + 2 * n_kv
179
+ ntg = (nheads_total + SG_PER_TG - 1) // SG_PER_TG
180
+ return k(inputs=[qkv, qn, kn, kc, vc, ct, st, posbuf, mask],
181
+ grid=(ntg * SG_PER_TG * 32, 1, 1), threadgroup=(SG_PER_TG * 32, 1, 1),
182
+ output_shapes=[(n_heads * hd,)], output_dtypes=[mx.float32],
183
+ stream=stream)[0]
mlx/fermion_mlx/container.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """TRTC v4 arch-3 (Qwen3) container reader — numpy memmap views, zero-copy
3
+ until the caller materializes.
4
+
5
+ Layout (ground truth: scripts/export_trit_c_model.py; cross-checks:
6
+ scripts/expand_trtc_v4_to_hf.py and kernels/gpu_e2e/runtime8b.py):
7
+ 60 B header <Iii9i2fi>: magic 'TRTC', version=4, arch=3, n_layers, hidden,
8
+ n_heads, n_kv_heads, head_dim, rotary_dims, intermediate, vocab, max_pos,
9
+ f32 rms_norm_eps, f32 rope_theta, i32 embed_out_kind (eok)
10
+ eok=2 (untied, 8B): int8 embed_in record right after the header
11
+ per layer: input_layernorm f32[H], post_attention_layernorm f32[H],
12
+ q_norm f32[head_dim], k_norm f32[head_dim],
13
+ then 7 trit records (q,k,v,o,gate,up,down) — arch-3 QKV are bias-less
14
+ final norm f32[H]
15
+ int8 embed_out (lm_head) record; eok=1 (tied, 0.6B) derives embed_in
16
+ from this record.
17
+ trit record: i32 rows, cols, cols_pad, row_stride(bytes);
18
+ u8 bp/bn/br [rows*row_stride] LSB-first; f32 s_lo[rows], s_hi[rows];
19
+ i32 rs_lo[rows], rs_hi[rows] (skipped); i32 has_bias (+ f32 bias[rows])
20
+ int8 record: i32 rows, cols, cols_pad, row_stride (== cols_pad);
21
+ s8 W[rows*row_stride]; f32 scale[rows]; i32 wsum[rows] (skipped)
22
+
23
+ Weight semantics: w[r,c] = (bp-bn) * (s_hi[r] if br else s_lo[r]);
24
+ int8 embeds: w[r,c] = scale[r] * Q[r,c].
25
+ """
26
+ import struct
27
+
28
+ import numpy as np
29
+
30
+ MAGIC = 0x43545254
31
+ HDR_FMT = "<Iii9i2fi"
32
+ HDR_BYTES = struct.calcsize(HDR_FMT) # 60
33
+ PROJS = ("q", "k", "v", "o", "gate", "up", "down")
34
+
35
+
36
+ class TritRecord:
37
+ __slots__ = ("rows", "cols", "cols_pad", "row_stride", "bp", "bn", "br",
38
+ "slo", "shi", "bias")
39
+
40
+
41
+ class Int8Record:
42
+ __slots__ = ("rows", "cols", "cols_pad", "row_stride", "W", "scale")
43
+
44
+
45
+ class Container:
46
+ pass
47
+
48
+
49
+ def _trit_record(mm, off):
50
+ r = TritRecord()
51
+ r.rows, r.cols, r.cols_pad, r.row_stride = (
52
+ int(v) for v in np.frombuffer(mm, np.int32, 4, off))
53
+ off += 16
54
+ n = r.rows * r.row_stride
55
+ r.bp = np.frombuffer(mm, np.uint8, n, off).reshape(r.rows, r.row_stride); off += n
56
+ r.bn = np.frombuffer(mm, np.uint8, n, off).reshape(r.rows, r.row_stride); off += n
57
+ r.br = np.frombuffer(mm, np.uint8, n, off).reshape(r.rows, r.row_stride); off += n
58
+ r.slo = np.frombuffer(mm, np.float32, r.rows, off); off += 4 * r.rows
59
+ r.shi = np.frombuffer(mm, np.float32, r.rows, off); off += 4 * r.rows
60
+ off += 8 * r.rows # rs_lo, rs_hi trit-sum sidecars (integrity data, unused here)
61
+ has_bias = int(np.frombuffer(mm, np.int32, 1, off)[0]); off += 4
62
+ r.bias = None
63
+ if has_bias:
64
+ r.bias = np.frombuffer(mm, np.float32, r.rows, off); off += 4 * r.rows
65
+ return r, off
66
+
67
+
68
+ def _int8_record(mm, off):
69
+ r = Int8Record()
70
+ r.rows, r.cols, r.cols_pad, r.row_stride = (
71
+ int(v) for v in np.frombuffer(mm, np.int32, 4, off))
72
+ assert r.row_stride == r.cols_pad, "int8 record: row_stride must equal cols_pad"
73
+ off += 16
74
+ n = r.rows * r.row_stride
75
+ r.W = np.frombuffer(mm, np.int8, n, off).reshape(r.rows, r.row_stride); off += n
76
+ r.scale = np.frombuffer(mm, np.float32, r.rows, off); off += 4 * r.rows
77
+ off += 4 * r.rows # wsum
78
+ return r, off
79
+
80
+
81
+ def read_container(path):
82
+ """Memory-map a TRTC v4 arch-3 container; all arrays are views into the map."""
83
+ mm = np.memmap(path, dtype=np.uint8, mode="r")
84
+ h = struct.unpack(HDR_FMT, bytes(mm[:HDR_BYTES]))
85
+ c = Container()
86
+ (magic, c.version, c.arch, c.n_layers, c.hidden, c.n_heads, c.n_kv,
87
+ c.head_dim, c.rot, c.inter, c.vocab, c.maxpos, c.eps, c.theta, c.eok) = h
88
+ assert magic == MAGIC, f"bad magic {magic:#x}"
89
+ assert c.version == 4 and c.arch == 3, \
90
+ f"need TRTC v4 arch-3 (got v={c.version} arch={c.arch})"
91
+ assert c.eok in (1, 2), f"bad embed_out_kind {c.eok}"
92
+ off = HDR_BYTES
93
+ c.embed_in = None
94
+ if c.eok == 2: # untied: int8 embed_in follows the header
95
+ c.embed_in, off = _int8_record(mm, off)
96
+ assert (c.embed_in.rows, c.embed_in.cols) == (c.vocab, c.hidden)
97
+ H, hd = c.hidden, c.head_dim
98
+ q_dim, kv_dim = c.n_heads * hd, c.n_kv * hd
99
+ shapes = {"q": (q_dim, H), "k": (kv_dim, H), "v": (kv_dim, H),
100
+ "o": (H, q_dim), "gate": (c.inter, H), "up": (c.inter, H),
101
+ "down": (H, c.inter)}
102
+ c.layers = []
103
+ for li in range(c.n_layers):
104
+ L = {}
105
+ L["norm1"] = np.frombuffer(mm, np.float32, H, off); off += 4 * H
106
+ L["norm2"] = np.frombuffer(mm, np.float32, H, off); off += 4 * H
107
+ L["qnorm"] = np.frombuffer(mm, np.float32, hd, off); off += 4 * hd
108
+ L["knorm"] = np.frombuffer(mm, np.float32, hd, off); off += 4 * hd
109
+ for nm in PROJS:
110
+ L[nm], off = _trit_record(mm, off)
111
+ assert (L[nm].rows, L[nm].cols) == shapes[nm], \
112
+ f"L{li}.{nm}: {(L[nm].rows, L[nm].cols)} != {shapes[nm]}"
113
+ assert L[nm].bias is None, f"L{li}.{nm}: arch-3 records must be bias-less"
114
+ c.layers.append(L)
115
+ c.final_norm = np.frombuffer(mm, np.float32, H, off); off += 4 * H
116
+ c.embed_out, off = _int8_record(mm, off)
117
+ assert (c.embed_out.rows, c.embed_out.cols) == (c.vocab, c.hidden)
118
+ assert off == mm.size, f"reader ended at {off}, file is {mm.size}"
119
+ if c.embed_in is None: # tied: same int8 record serves both
120
+ c.embed_in = c.embed_out
121
+ c.mm = mm # keep the map alive
122
+ return c
123
+
124
+
125
+ # ---------------- fp32 reference math (gate-side, numpy) ----------------
126
+
127
+ def unpack_plane(plane, cols):
128
+ return np.unpackbits(plane, axis=1, bitorder="little")[:, :cols]
129
+
130
+
131
+ def dense_weights(rec, dtype=np.float32):
132
+ p = unpack_plane(rec.bp, rec.cols).astype(np.int8)
133
+ n = unpack_plane(rec.bn, rec.cols).astype(np.int8)
134
+ h = unpack_plane(rec.br, rec.cols)
135
+ s = (p - n).astype(dtype)
136
+ mag = np.where(h.astype(bool), rec.shi[:, None].astype(dtype),
137
+ rec.slo[:, None].astype(dtype))
138
+ return s * mag
139
+
140
+
141
+ def ref_rmsnorm(x, g, eps, dtype=np.float32):
142
+ x = x.astype(dtype)
143
+ return (x * (1.0 / np.sqrt(np.mean(x * x) + eps))) * g.astype(dtype)
mlx/fermion_mlx/gates.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """House correctness gates for the MLX arch-3 path.
3
+
4
+ refgate : V1 gate — teacher-forced logits +
5
+ greedy-with-resync vs the container's own fp expansion reference
6
+ npz {prompt_ids, tf_logits, greedy, meta}. Every argmax
7
+ divergence must be a near-tie <= tie_tol of the logit scale;
8
+ greedy flips resynced and bounded.
9
+ cachegate : greedy-with-resync vs a banked hf_greedy_cache.json
10
+ (keyed to container md5).
11
+ pathgate : path-consistency — the fused-kernel primary path and the pure
12
+ mx.fast-ops reference twin must produce the same greedy stream.
13
+
14
+ All gates return (ok, receipt_dict); receipts carry per-flip absolute and
15
+ relative margins so near-tie flips are documented, never waved through.
16
+ """
17
+ import hashlib
18
+ import json
19
+
20
+ import mlx.core as mx
21
+ import numpy as np
22
+
23
+
24
+ def _md5(path):
25
+ h = hashlib.md5()
26
+ with open(path, "rb") as f:
27
+ while chunk := f.read(1 << 24):
28
+ h.update(chunk)
29
+ return h.hexdigest()
30
+
31
+
32
+ def refgate(model, ref_path, tie_tol=0.04, max_flips=8):
33
+ ref = np.load(ref_path, allow_pickle=True)
34
+ ids = [int(t) for t in ref["prompt_ids"].tolist()]
35
+ tf = ref["tf_logits"]
36
+ greedy_ref = [int(t) for t in ref["greedy"].tolist()]
37
+ meta = json.loads(str(ref["meta"])) if "meta" in ref else {}
38
+ print(f"[refgate] prompt ids ({len(ids)}): {ids}")
39
+ print(f"[refgate] ref meta: {meta}")
40
+ rec = {"ref": str(ref_path), "meta": meta, "tf": [], "greedy_flips": [],
41
+ "tie_tol": tie_tol, "max_flips": max_flips}
42
+ # stage A: teacher-forced logits
43
+ mask = model.new_mask()
44
+ rels, ams, near, worst_gap = [], 0, 0, 0.0
45
+ for i, t in enumerate(ids):
46
+ _, _, lg = model.step(model.embed(t), i, mask)
47
+ g = np.array(lg, np.float64)
48
+ r = tf[i].astype(np.float64)
49
+ scale = float(np.abs(r).max())
50
+ rel = float(np.abs(g - r).max() / scale)
51
+ cos = float((g * r).sum() / (np.linalg.norm(g) * np.linalg.norm(r)))
52
+ gam, ram = int(g.argmax()), int(r.argmax())
53
+ am = gam == ram
54
+ ams += am
55
+ rels.append(rel)
56
+ extra = ""
57
+ if not am:
58
+ gap = float(g[gam] - g[ram]) / scale
59
+ worst_gap = max(worst_gap, gap)
60
+ near += gap <= tie_tol
61
+ extra = f" gap {100*gap:.3f}% ({'near-tie' if gap <= tie_tol else 'REAL'})"
62
+ rec["tf"].append({"pos": i, "rel": rel, "cos": cos, "argmax_match": am})
63
+ print(f" pos {i:2d}: rel {100*rel:6.3f}% | cos {cos:.6f} | argmax "
64
+ f"{'match' if am else 'MISMATCH'} (mlx {gam} ref {ram}){extra}")
65
+ print(f"[refgate] stage A: TF rel max {100*max(rels):.3f}% / mean "
66
+ f"{100*float(np.mean(rels)):.3f}%, argmax {ams}/{len(ids)} "
67
+ f"(+{near} near-tie), worst gap {100*worst_gap:.3f}%")
68
+ rec["tf_summary"] = {"rel_max": max(rels), "rel_mean": float(np.mean(rels)),
69
+ "argmax": f"{ams}/{len(ids)}", "near_tie": near}
70
+ # stage B: greedy with resync
71
+ mask = model.new_mask()
72
+ x, pos0 = model.prefill(ids, mask, compiled=False)
73
+ flips, g_toks, ok = 0, [], True
74
+ for s in range(len(greedy_ref)):
75
+ _, _, lg = model.step(x, pos0 + s, mask)
76
+ l = np.array(lg, np.float64)
77
+ g_tok = int(l.argmax())
78
+ g_toks.append(g_tok)
79
+ tok = g_tok
80
+ if g_tok != greedy_ref[s]:
81
+ gap_abs = float(l[g_tok] - l[greedy_ref[s]])
82
+ scale = float(np.abs(l).max())
83
+ gap = gap_abs / scale
84
+ flips += 1
85
+ worst_gap = max(worst_gap, gap)
86
+ rec["greedy_flips"].append({"step": s, "mlx": g_tok,
87
+ "ref": greedy_ref[s],
88
+ "margin_abs_logits": gap_abs,
89
+ "margin_rel": gap})
90
+ print(f" step {s}: mlx {g_tok} vs ref {greedy_ref[s]}, gap "
91
+ f"{gap_abs:.4f} logits = {100*gap:.3f}% of scale -> resync")
92
+ if flips > max_flips:
93
+ ok = False
94
+ break
95
+ tok = greedy_ref[s]
96
+ x = model.embed(tok)
97
+ mx.eval(x)
98
+ print(f"[refgate] ref greedy: {greedy_ref}")
99
+ print(f"[refgate] mlx greedy: {g_toks}")
100
+ ok = ok and worst_gap <= tie_tol and flips <= max_flips
101
+ rec.update(flips=flips, worst_gap=worst_gap, ok=bool(ok),
102
+ mlx_greedy=g_toks, ref_greedy=greedy_ref)
103
+ print(f"[refgate] {'PASS' if ok else 'FAIL'} (worst gap {100*worst_gap:.3f}% "
104
+ f"tol {100*tie_tol:.1f}%; {flips} flips, tol {max_flips})")
105
+ return ok, rec
106
+
107
+
108
+ def cachegate(model, container_path, cache_path, tie_tol=0.04, max_flips=8):
109
+ cache = json.load(open(cache_path))
110
+ md5 = _md5(container_path)
111
+ print(f"[cachegate] container md5 {md5} vs cache key {cache['container_md5']}")
112
+ assert md5 == cache["container_md5"], "cache is for a different container"
113
+ rec = {"cache": str(cache_path), "container_md5": md5, "prompts": [],
114
+ "tie_tol": tie_tol, "max_flips_per_prompt": max_flips}
115
+ ok = True
116
+ tot_flips, worst = 0, 0.0
117
+ for pi, (prompt, ref) in enumerate(zip(cache["prompts"], cache["tokens"])):
118
+ mask = model.new_mask()
119
+ x, pos0 = model.prefill([int(t) for t in prompt], mask)
120
+ flips, fliprec = 0, []
121
+ for s, rt in enumerate(ref):
122
+ _, _, lg = model.step(x, pos0 + s, mask)
123
+ l = np.array(lg, np.float64)
124
+ g_tok = int(l.argmax())
125
+ tok = g_tok
126
+ if g_tok != int(rt):
127
+ gap_abs = float(l[g_tok] - l[int(rt)])
128
+ gap = gap_abs / float(np.abs(l).max())
129
+ worst = max(worst, gap)
130
+ flips += 1
131
+ fliprec.append({"step": s, "mlx": g_tok, "ref": int(rt),
132
+ "margin_abs_logits": gap_abs, "margin_rel": gap})
133
+ print(f" p{pi} step {s}: mlx {g_tok} vs hf {rt}, gap "
134
+ f"{gap_abs:.4f} logits = {100*gap:.3f}% -> resync")
135
+ tok = int(rt)
136
+ x = model.embed(tok)
137
+ mx.eval(x)
138
+ tot_flips += flips
139
+ good = flips <= max_flips
140
+ ok = ok and good
141
+ rec["prompts"].append({"prompt": prompt, "steps": len(ref),
142
+ "flips": flips, "flip_detail": fliprec,
143
+ "pass": bool(good)})
144
+ print(f" prompt {pi}: {len(ref)} steps, {flips} resynced flips -> "
145
+ f"{'PASS' if good else 'FAIL'}")
146
+ ok = ok and worst <= tie_tol
147
+ rec.update(total_flips=tot_flips, worst_gap=worst, ok=bool(ok))
148
+ print(f"[cachegate] {'PASS' if ok else 'FAIL'} ({tot_flips} flips total, "
149
+ f"worst gap {100*worst:.3f}% tol {100*tie_tol:.1f}%)")
150
+ return ok, rec
151
+
152
+
153
+ def pathgate(model, ids, n=32):
154
+ """fused-kernel path vs pure-mx.fast-ops twin: greedy streams must be
155
+ IDENTICAL (same weights, same GEMV kernels; isolates the new kernel)."""
156
+ fused, _ = model.generate(ids, n, compiled=True)
157
+ ref = model.generate_ref(ids, n)
158
+ same = fused == ref
159
+ d = next((i for i in range(min(len(fused), len(ref)))
160
+ if fused[i] != ref[i]), None)
161
+ print(f"[pathgate] fused-vs-mxops {n}-token greedy: "
162
+ f"{'IDENTICAL' if same else f'FIRST DIVERGENCE at {d}'}")
163
+ return same, {"n": n, "identical": bool(same), "first_divergence": d,
164
+ "fused": fused, "mxops": ref}
mlx/fermion_mlx/mbatch.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Small-batch (M-token) GEMM kernels for speculative verify — EXPERIMENTAL.
3
+
4
+ Why these exist: the shipped decode path is M=1 GEMV. Speculative decoding
5
+ only wins if the verifier scores k+1 draft positions while streaming the
6
+ weight planes ONCE; re-running the GEMV k+1 times re-reads the weights k+1
7
+ times and cannot beat plain decode. These kernels keep the v5/v7 interleaved
8
+ plane layout + one-weight-read structure and add an M-wide accumulator:
9
+
10
+ trit_gemm_m : five-value planes X[M, cols] -> out[M, rows]
11
+ v7 lane mapping (lane = row(8) x word-slot(4), planes read
12
+ once, fully coalesced) + v6 mask-convert-FMA decode (float4
13
+ (uint4(a) & m4) == 2^bit when set; x pre-scaled by 2^-bit,
14
+ exact in fp32). No LUT: a per-token LUT tile for M=5 exceeds
15
+ the 32KB threadgroup budget at inter=12288.
16
+ s8_gemm_m : int8 rows (lm_head / untied embeds), one simdgroup per row,
17
+ weight char4 loaded once and dotted against all M x-vectors.
18
+
19
+ No norm/GLU/residual template flags here: the spec verify path applies
20
+ RMSNorm / SwiGLU / residual adds as plain MLX elementwise ops on [M, ...]
21
+ tensors (cheap at M<=8), keeping these kernels pure GEMMs.
22
+
23
+ Additive module — nothing in trit_kernels.py / arch3.py / model.py changes.
24
+ """
25
+ import mlx.core as mx
26
+
27
+ SG_PER_TG = 8
28
+ _CACHE = {}
29
+
30
+
31
+ def _trit_gemm_m_source(M, rows, cols, rs):
32
+ assert rs % 4 == 0 and rs * 8 == cols and cols % 32 == 0 and rows % 8 == 0
33
+ W = rs // 4
34
+ kbody = []
35
+ for k in range(8):
36
+ m4 = [1 << (4 * k + i) for i in range(4)]
37
+ ps = ", ".join("%.9ef" % (2.0 ** -(4 * k + i)) for i in range(4))
38
+ loads = "\n".join(
39
+ f""" {{
40
+ float4 xs = x4[{m}u * {cols // 4}u + wl * 8u + {k}u] * ps{k};
41
+ accLo[{m}] += metal::dot(sa, xs);
42
+ accHi[{m}] += metal::dot(sb, xs);
43
+ }}""" for m in range(M))
44
+ kbody.append(f""" {{
45
+ const uint4 m4 = uint4({m4[0]}u, {m4[1]}u, {m4[2]}u, {m4[3]}u);
46
+ const float4 ps{k} = float4({ps});
47
+ float4 sa = float4(uint4(a) & m4) - float4(uint4(b) & m4);
48
+ float4 sb = float4(uint4(cc) & m4) - float4(uint4(d) & m4);
49
+ {loads}
50
+ }}""")
51
+ kbody = "\n".join(kbody)
52
+ outs = "\n".join(
53
+ f""" accLo[{m}] += metal::simd_shuffle_down(accLo[{m}], 16u);
54
+ accLo[{m}] += metal::simd_shuffle_down(accLo[{m}], 8u);
55
+ accHi[{m}] += metal::simd_shuffle_down(accHi[{m}], 16u);
56
+ accHi[{m}] += metal::simd_shuffle_down(accHi[{m}], 8u);"""
57
+ for m in range(M))
58
+ writes = "\n".join(
59
+ f" out[{m}u * {rows}u + row] = "
60
+ f"metal::fma(slo[row], accLo[{m}], shi[row] * accHi[{m}]);"
61
+ for m in range(M))
62
+ return f"""
63
+ uint tid = thread_position_in_threadgroup.x;
64
+ uint lane = tid & 31u;
65
+ uint sg = tid >> 5u;
66
+ uint blk = threadgroup_position_in_grid.x * {SG_PER_TG}u + sg;
67
+ uint row0 = blk * 8u;
68
+ if (row0 >= {rows}u) return;
69
+ uint r = lane & 7u;
70
+ uint wsl = lane >> 3u;
71
+ const device uint* p32 = (const device uint*)bp;
72
+ const device uint* n32 = (const device uint*)bn;
73
+ const device uint* h32 = (const device uint*)br;
74
+ const device float4* x4 = (const device float4*)x;
75
+ float accLo[{M}], accHi[{M}];
76
+ for (uint m = 0u; m < {M}u; ++m) {{ accLo[m] = 0.0f; accHi[m] = 0.0f; }}
77
+ for (uint wl = wsl; wl < {W}u; wl += 4u) {{
78
+ ulong base = ((ulong)blk * {W}u + wl) * 8u + r;
79
+ uint pw = p32[base], nw2 = n32[base], hw = h32[base];
80
+ uint a = pw & ~hw, b = nw2 & ~hw, cc = pw & hw, d = nw2 & hw;
81
+ {kbody}
82
+ }}
83
+ {outs}
84
+ if (lane < 8u) {{
85
+ uint row = row0 + lane;
86
+ {writes}
87
+ }}
88
+ """
89
+
90
+
91
+ def _s8_gemm_m_source(M, rows, cols, rs):
92
+ assert rs % 4 == 0 and cols % 4 == 0
93
+ accs = "\n".join(
94
+ f" acc[{m}] += metal::dot(wf, x4[{m}u * {cols // 4}u + i]);"
95
+ for m in range(M))
96
+ reduces = "\n".join(
97
+ f""" {{
98
+ float a = metal::simd_sum(acc[{m}]);
99
+ if (lane == 0u) out[{m}u * {rows}u + row] = scale[row] * a;
100
+ }}""" for m in range(M))
101
+ return f"""
102
+ uint tid = thread_position_in_threadgroup.x;
103
+ uint lane = tid & 31u;
104
+ uint sg = tid >> 5u;
105
+ uint row = threadgroup_position_in_grid.x * {SG_PER_TG}u + sg;
106
+ if (row >= {rows}u) return;
107
+ const device char* w = (const device char*)W + (ulong)row * {rs}u;
108
+ const device float4* x4 = (const device float4*)x;
109
+ float acc[{M}];
110
+ for (uint m = 0u; m < {M}u; ++m) acc[m] = 0.0f;
111
+ for (uint i = lane; i < {cols // 4}u; i += 32u) {{
112
+ char4 wv = *(const device char4*)(w + i * 4u);
113
+ float4 wf = float4(wv.x, wv.y, wv.z, wv.w);
114
+ {accs}
115
+ }}
116
+ {reduces}
117
+ """
118
+
119
+
120
+ def _get(kind, M, rows, cols, rs):
121
+ key = (kind, M, rows, cols, rs)
122
+ if key in _CACHE:
123
+ return _CACHE[key]
124
+ if kind == "trit":
125
+ src = _trit_gemm_m_source(M, rows, cols, rs)
126
+ k = mx.fast.metal_kernel(
127
+ name=f"tritgemm_m{M}_r{rows}_c{cols}",
128
+ input_names=["bp", "bn", "br", "slo", "shi", "x"],
129
+ output_names=["out"], source=src)
130
+ else:
131
+ src = _s8_gemm_m_source(M, rows, cols, rs)
132
+ k = mx.fast.metal_kernel(
133
+ name=f"s8gemm_m{M}_r{rows}_c{cols}",
134
+ input_names=["W", "scale", "x"],
135
+ output_names=["out"], source=src)
136
+ _CACHE[key] = k
137
+ return k
138
+
139
+
140
+ def trit_gemm_m(planes, slo, shi, X, stream=None):
141
+ """planes v5-interleaved (KERNEL_VERSION>=5 prep). X fp32 [M, cols]
142
+ (already normed / activated). Returns fp32 [M, rows]."""
143
+ bp, bn, br = planes
144
+ rows, rs = bp.shape
145
+ M, cols = X.shape
146
+ k = _get("trit", M, rows, cols, rs)
147
+ nsg = rows // 8
148
+ ntg = (nsg + SG_PER_TG - 1) // SG_PER_TG
149
+ return k(inputs=[bp, bn, br, slo, shi, X],
150
+ grid=(ntg * SG_PER_TG * 32, 1, 1), threadgroup=(SG_PER_TG * 32, 1, 1),
151
+ output_shapes=[(M, rows)], output_dtypes=[mx.float32],
152
+ stream=stream)[0]
153
+
154
+
155
+ def s8_gemm_m(W, scale, X, stream=None):
156
+ """int8 W [rows, rs], X fp32 [M, cols] (already normed). -> [M, rows]."""
157
+ rows, rs = W.shape
158
+ M, cols = X.shape
159
+ k = _get("s8", M, rows, cols, rs)
160
+ ntg = (rows + SG_PER_TG - 1) // SG_PER_TG
161
+ return k(inputs=[W, scale, X],
162
+ grid=(ntg * SG_PER_TG * 32, 1, 1), threadgroup=(SG_PER_TG * 32, 1, 1),
163
+ output_shapes=[(M, rows)], output_dtypes=[mx.float32],
164
+ stream=stream)[0]
mlx/fermion_mlx/model.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """End-to-end MLX/Metal decode for TRTC v4 **arch-3 (Qwen3)** five-value
3
+ containers — Neutrino-0.6B (tied int8 embeds) and Neutrino-8B (untied).
4
+
5
+ Every transformer linear runs on the banked five-value {0,±s_lo,±s_hi}
6
+ 3-bit-planar Metal GEMV kernels (vendored trit_kernels.py, byte-identical
7
+ to kernels/metal/trit_kernels.py): qkv / o / gate+up / down fused per layer
8
+ + the int8 lm_head GEMV. The arch-3 delta — per-head Q/K-RMSNorm before
9
+ rope, biasless qkv, int8 embed_in — is handled by the new fused
10
+ qknorm_rope_append kernel (arch3.py) and an int8 embedding dequant.
11
+
12
+ Fusions per decode token: rmsnorm folded into qkv/gu/lm_head GEMVs, SwiGLU
13
+ folded into the down GEMV, residual adds folded into o/down epilogues,
14
+ QK-norm+rope+KV-append in one dispatch. MLX supplies SDPA, argmax and the
15
+ lazy-graph dispatch batching.
16
+
17
+ Two forward implementations:
18
+ _step_fn : primary path (fused kernel, preallocated KV + additive mask,
19
+ mx.compile-able) — the shipped/gated/benched path.
20
+ step_ref : pure-MLX-ops reference twin for the NEW kernel (mx.fast
21
+ rms_norm/rope, concat KV) — used by the path-consistency gate.
22
+ """
23
+ import time
24
+
25
+ import mlx.core as mx
26
+ import numpy as np
27
+
28
+ from .container import read_container
29
+ from .trit_kernels import trit_gemv, s8_gemv, prep_planes
30
+ from . import trit_kernels
31
+ from .arch3 import qknorm_rope_append, s8_gemv_v3
32
+
33
+ NEG_INF = -1e9
34
+
35
+
36
+ class _Layer:
37
+ pass
38
+
39
+
40
+ def _cat(arrs):
41
+ return np.ascontiguousarray(np.concatenate(arrs, axis=0))
42
+
43
+
44
+ class MetalTritQwen3:
45
+ """TRTC v4 arch-3 walker (0.6B or 8B, dims from the container header)."""
46
+
47
+ def __init__(self, path, maxseq=1024, verbose=True):
48
+ t0 = time.time()
49
+ c = read_container(path)
50
+ self.meta = c
51
+ self.eps, self.theta = float(c.eps), float(c.theta)
52
+ self.n_layers, self.hidden = c.n_layers, c.hidden
53
+ self.n_heads, self.n_kv, self.hd = c.n_heads, c.n_kv, c.head_dim
54
+ self.inter, self.vocab, self.eok = c.inter, c.vocab, c.eok
55
+ assert c.rot == self.hd, "kernels assume full-head_dim rope"
56
+ self.scale = self.hd ** -0.5
57
+ self.qd = self.n_heads * self.hd
58
+ self.kd = self.n_kv * self.hd
59
+ self.maxseq = maxseq
60
+
61
+ # int8 lm_head (+ tied/untied int8 embed_in)
62
+ eo = c.embed_out
63
+ self.eoW = mx.array(np.ascontiguousarray(eo.W)) # int8 [V, H]
64
+ self.eo_scale = mx.array(np.ascontiguousarray(eo.scale))
65
+ mx.eval(self.eoW, self.eo_scale)
66
+ if c.eok == 2:
67
+ ei = c.embed_in
68
+ self.embW = mx.array(np.ascontiguousarray(ei.W)) # int8 [V, H]
69
+ self.embS = mx.array(np.ascontiguousarray(ei.scale))
70
+ mx.eval(self.embW, self.embS)
71
+ else:
72
+ self.embW, self.embS = self.eoW, self.eo_scale
73
+
74
+ self.layers = []
75
+ for L in c.layers:
76
+ q, k, v, o, gate, up, down = (L[n] for n in
77
+ ("q", "k", "v", "o", "gate", "up", "down"))
78
+ y = _Layer()
79
+ y.norm1 = mx.array(np.ascontiguousarray(L["norm1"]))
80
+ y.norm2 = mx.array(np.ascontiguousarray(L["norm2"]))
81
+ y.qnorm = mx.array(np.ascontiguousarray(L["qnorm"]))
82
+ y.knorm = mx.array(np.ascontiguousarray(L["knorm"]))
83
+ y.qkv_planes = prep_planes(tuple(
84
+ mx.array(_cat([getattr(r, pl) for r in (q, k, v)]))
85
+ for pl in ("bp", "bn", "br")), q.cols)
86
+ y.qkv_slo = mx.array(_cat([q.slo, k.slo, v.slo]))
87
+ y.qkv_shi = mx.array(_cat([q.shi, k.shi, v.shi]))
88
+ y.o_planes = prep_planes(tuple(
89
+ mx.array(np.ascontiguousarray(getattr(o, pl)))
90
+ for pl in ("bp", "bn", "br")), o.cols)
91
+ y.o_slo = mx.array(np.ascontiguousarray(o.slo))
92
+ y.o_shi = mx.array(np.ascontiguousarray(o.shi))
93
+ y.gu_planes = prep_planes(tuple(
94
+ mx.array(_cat([getattr(gate, pl), getattr(up, pl)]))
95
+ for pl in ("bp", "bn", "br")), gate.cols)
96
+ y.gu_slo = mx.array(_cat([gate.slo, up.slo]))
97
+ y.gu_shi = mx.array(_cat([gate.shi, up.shi]))
98
+ y.dn_planes = prep_planes(tuple(
99
+ mx.array(np.ascontiguousarray(getattr(down, pl)))
100
+ for pl in ("bp", "bn", "br")), down.cols)
101
+ y.dn_slo = mx.array(np.ascontiguousarray(down.slo))
102
+ y.dn_shi = mx.array(np.ascontiguousarray(down.shi))
103
+ mx.eval(*(y.qkv_planes + y.o_planes + y.gu_planes + y.dn_planes),
104
+ y.qkv_slo, y.qkv_shi, y.o_slo, y.o_shi,
105
+ y.gu_slo, y.gu_shi, y.dn_slo, y.dn_shi,
106
+ y.norm1, y.norm2, y.qnorm, y.knorm)
107
+ self.layers.append(y)
108
+ self.final_norm = mx.array(np.ascontiguousarray(c.final_norm))
109
+
110
+ # rope tables + preallocated KV + mask
111
+ inv = self.theta ** -(np.arange(0, self.hd, 2, dtype=np.float64) / self.hd)
112
+ ang = np.outer(np.arange(maxseq, dtype=np.float64), inv)
113
+ self.ct = mx.array(np.cos(ang).astype(np.float32).ravel())
114
+ self.st = mx.array(np.sin(ang).astype(np.float32).ravel())
115
+ self.kc = [mx.zeros((self.n_kv, maxseq, self.hd), dtype=mx.float32)
116
+ for _ in self.layers]
117
+ self.vc = [mx.zeros((self.n_kv, maxseq, self.hd), dtype=mx.float32)
118
+ for _ in self.layers]
119
+ mx.eval(self.ct, self.st, self.final_norm, *self.kc, *self.vc)
120
+ self._stepc = None
121
+ # x-restaged int8 unembed when the row count allows it
122
+ self._s8 = s8_gemv_v3 if self.vocab % 64 == 0 else s8_gemv
123
+ if verbose:
124
+ gb = mx.get_active_memory() / 2**30
125
+ print(f"loaded {path} in {time.time()-t0:.1f}s (arch-3, "
126
+ f"{self.n_layers}L/{self.hidden}H/{self.n_heads}q/{self.n_kv}kv/"
127
+ f"{self.inter}i/{self.vocab}V eok={self.eok}, kernel v"
128
+ f"{trit_kernels.KERNEL_VERSION}, maxseq {maxseq}, "
129
+ f"active mem {gb:.2f} GiB)")
130
+
131
+ # ---------------- primary path ----------------
132
+
133
+ def new_mask(self):
134
+ m = mx.full((self.maxseq,), NEG_INF, dtype=mx.float32)
135
+ mx.eval(m)
136
+ return m
137
+
138
+ def embed(self, tok):
139
+ """int8 embedding dequant: fp32 row = scale[tok] * W8[tok]."""
140
+ return self.embW[tok].astype(mx.float32) * self.embS[tok]
141
+
142
+ def _window(self, pos):
143
+ """KV window for position pos: grown in 256-slot blocks. Slicing off
144
+ the -1e9-masked tail is numerically exact (exp underflows to 0.0 in
145
+ fp32), so shrinking the SDPA read never changes the math — it only
146
+ stops streaming megabytes of masked cache per step."""
147
+ return min(self.maxseq, ((pos + 1 + 255) // 256) * 256)
148
+
149
+ def _step_fn(self, x, posbuf, mask, W):
150
+ """x fp32 [H] at position posbuf[0] -> (next_tok, x_next, logits).
151
+ Fixed shapes per W: SDPA over the first W cache slots with additive
152
+ mask (stale slots -1e9; qknorm_rope_append maintains mask[pos]=0)."""
153
+ eps = self.eps
154
+ m4 = mask[:W].reshape(1, 1, 1, W)
155
+ for li, L in enumerate(self.layers):
156
+ qkv = trit_gemv(L.qkv_planes, L.qkv_slo, L.qkv_shi, x,
157
+ norm_w=L.norm1, eps=eps)
158
+ q = qknorm_rope_append(qkv, L.qnorm, L.knorm, self.kc[li], self.vc[li],
159
+ self.ct, self.st, posbuf, mask,
160
+ self.n_heads, self.n_kv, self.hd,
161
+ self.maxseq, eps)
162
+ q = q.reshape(1, self.n_heads, 1, self.hd)
163
+ a = mx.fast.scaled_dot_product_attention(
164
+ q, self.kc[li][:, :W][None], self.vc[li][:, :W][None],
165
+ scale=self.scale, mask=m4)
166
+ a = a.reshape(self.qd)
167
+ x = trit_gemv(L.o_planes, L.o_slo, L.o_shi, a, resid=x, eps=eps)
168
+ gu = trit_gemv(L.gu_planes, L.gu_slo, L.gu_shi, x,
169
+ norm_w=L.norm2, eps=eps)
170
+ x = trit_gemv(L.dn_planes, L.dn_slo, L.dn_shi, gu, glu=True,
171
+ resid=x, eps=eps)
172
+ logits = self._s8(self.eoW, self.eo_scale, x, norm_w=self.final_norm, eps=eps)
173
+ nt = mx.argmax(logits)
174
+ return nt, self.embed(nt), logits
175
+
176
+ def step(self, x, pos, mask):
177
+ """uncompiled single step (gates)."""
178
+ return self._step_fn(x, mx.array([pos], dtype=mx.int32), mask,
179
+ self._window(pos))
180
+
181
+ def step_compiled(self, x, posbuf, mask, W):
182
+ if self._stepc is None:
183
+ self._stepc = {}
184
+ if W not in self._stepc:
185
+ self._stepc[W] = mx.compile(
186
+ lambda xx, pb, mk, _W=W: self._step_fn(xx, pb, mk, _W))
187
+ return self._stepc[W](x, posbuf, mask)
188
+
189
+ def prefill(self, ids, mask, compiled=True):
190
+ """feed ids[:-1]; returns (x armed with ids[-1], pos of ids[-1])."""
191
+ for i, t in enumerate(ids[:-1]):
192
+ pb = mx.array([i], dtype=mx.int32)
193
+ W = self._window(i)
194
+ if compiled:
195
+ nt, xn, lg = self.step_compiled(self.embed(int(t)), pb, mask, W)
196
+ else:
197
+ nt, xn, lg = self._step_fn(self.embed(int(t)), pb, mask, W)
198
+ mx.eval(lg)
199
+ x = self.embed(int(ids[-1]))
200
+ mx.eval(x)
201
+ return x, len(ids) - 1
202
+
203
+ def generate(self, ids, n, compiled=True, eos=None):
204
+ """greedy decode n tokens after prompt ids; returns (tokens,
205
+ decode_seconds). eos: optional set of stop ids (stop AFTER emitting)."""
206
+ mask = self.new_mask()
207
+ x, pos0 = self.prefill(ids, mask, compiled=compiled)
208
+ toks = []
209
+ prev = None
210
+ t0 = time.perf_counter()
211
+ for i in range(n):
212
+ pos = pos0 + i
213
+ pb = mx.array([pos], dtype=mx.int32)
214
+ W = self._window(pos)
215
+ if compiled:
216
+ nt, x, lg = self.step_compiled(x, pb, mask, W)
217
+ else:
218
+ nt, x, lg = self._step_fn(x, pb, mask, W)
219
+ mx.async_eval(nt, x)
220
+ if prev is not None:
221
+ toks.append(int(prev))
222
+ if eos and toks[-1] in eos:
223
+ prev = None
224
+ break
225
+ prev = nt
226
+ if prev is not None:
227
+ toks.append(int(prev))
228
+ mx.eval(x)
229
+ dt = time.perf_counter() - t0
230
+ return toks, dt
231
+
232
+ # ---------------- pure-MLX-ops reference twin ----------------
233
+
234
+ def step_ref(self, x, kv, pos):
235
+ """same GEMV kernels, but QK-norm/rope/cache via mx.fast ops +
236
+ concat KV — the cross-check for the NEW fused kernel."""
237
+ eps = self.eps
238
+ for li, L in enumerate(self.layers):
239
+ qkv = trit_gemv(L.qkv_planes, L.qkv_slo, L.qkv_shi, x,
240
+ norm_w=L.norm1, eps=eps)
241
+ q = mx.fast.rms_norm(qkv[:self.qd].reshape(self.n_heads, self.hd),
242
+ L.qnorm, eps)
243
+ k = mx.fast.rms_norm(qkv[self.qd:self.qd + self.kd]
244
+ .reshape(self.n_kv, self.hd), L.knorm, eps)
245
+ qk = mx.concatenate([q, k], axis=0).reshape(
246
+ 1, self.n_heads + self.n_kv, 1, self.hd)
247
+ qk = mx.fast.rope(qk, self.hd, traditional=False, base=self.theta,
248
+ scale=1.0, offset=pos)
249
+ qh = qk[:, :self.n_heads]
250
+ kh = qk[:, self.n_heads:]
251
+ vh = qkv[self.qd + self.kd:].reshape(1, self.n_kv, 1, self.hd)
252
+ if kv[li] is None:
253
+ kv[li] = (kh, vh)
254
+ else:
255
+ kv[li] = (mx.concatenate([kv[li][0], kh], axis=2),
256
+ mx.concatenate([kv[li][1], vh], axis=2))
257
+ keys, values = kv[li]
258
+ a = mx.fast.scaled_dot_product_attention(qh, keys, values,
259
+ scale=self.scale)
260
+ a = a.reshape(self.qd)
261
+ x = trit_gemv(L.o_planes, L.o_slo, L.o_shi, a, resid=x, eps=eps)
262
+ gu = trit_gemv(L.gu_planes, L.gu_slo, L.gu_shi, x,
263
+ norm_w=L.norm2, eps=eps)
264
+ x = trit_gemv(L.dn_planes, L.dn_slo, L.dn_shi, gu, glu=True,
265
+ resid=x, eps=eps)
266
+ return self._s8(self.eoW, self.eo_scale, x, norm_w=self.final_norm, eps=eps)
267
+
268
+ def generate_ref(self, ids, n):
269
+ kv = [None] * self.n_layers
270
+ for i, t in enumerate(ids[:-1]):
271
+ lg = self.step_ref(self.embed(int(t)), kv, i)
272
+ mx.eval(lg)
273
+ x = self.embed(int(ids[-1]))
274
+ toks = []
275
+ for i in range(n):
276
+ lg = self.step_ref(x, kv, len(ids) - 1 + i)
277
+ nt = mx.argmax(lg)
278
+ x = self.embed(nt)
279
+ mx.eval(x)
280
+ toks.append(int(nt))
281
+ return toks
mlx/fermion_mlx/spec.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """EXPERIMENTAL speculative decoding: 0.6B pack drafts, 8B pack verifies.
3
+
4
+ House exactness rule (greedy-exact acceptance): a draft token is accepted
5
+ iff it equals the verifier's argmax at that position. The emitted stream is
6
+ therefore exactly the verifier's own greedy stream *as computed by the
7
+ batched verify path* — the ship gate (specgate) checks that stream is
8
+ token-identical to the shipped single-token path at 32 and 256 tokens.
9
+
10
+ Mechanics per round (k=4 draft tokens, M=k+1 verify batch):
11
+ 1. draft consumes its pending tokens, then chains k greedy proposals
12
+ d_1..d_k on its own single-token path;
13
+ 2. verifier scores [pending[-1], d_1..d_k] in ONE batched pass
14
+ (mbatch.trit_gemm_m / s8_gemm_m — weight planes stream once, which is
15
+ the entire speedup mechanism on a bandwidth-bound GPU);
16
+ 3. accept the longest prefix with d_i == argmax_{i-1}; emit accepted
17
+ tokens + the verifier's own next token (correction or bonus);
18
+ 4. KV rewind: positions are the source of truth. The verifier's stale
19
+ rows get overwritten by the next round's appends before any read (the
20
+ verify mask only allows slots <= row position). The draft re-masks its
21
+ stale slots (additive mask back to -1e9) and rolls its position back.
22
+
23
+ Both models live in one process; run under --mem-limit-gb 6 (house OOM
24
+ rule). Additive module: model.py / gates.py / existing CLI modes untouched.
25
+ """
26
+ import json
27
+ import time
28
+
29
+ import mlx.core as mx
30
+
31
+ from .mbatch import trit_gemm_m, s8_gemm_m
32
+
33
+ NEG_INF = -1e9
34
+
35
+
36
+ def _silu(g):
37
+ return g * mx.sigmoid(g)
38
+
39
+
40
+ class SpecDecoder:
41
+ def __init__(self, verifier, draft, k=4):
42
+ assert verifier.vocab == draft.vocab, "draft/verifier vocab mismatch"
43
+ self.V, self.D, self.k = verifier, draft, k
44
+ self._vc = {} # compiled verify fns keyed (M, W)
45
+ self._specmask = verifier.new_mask() # kernel side-channel, never read
46
+ self.stats = None
47
+
48
+ # ---------------- batched verify ----------------
49
+
50
+ def _verify(self, toks, pos0):
51
+ from .arch3 import qknorm_rope_append
52
+ V = self.V
53
+ M = len(toks)
54
+ W = V._window(pos0 + M - 1)
55
+ key = (M, W)
56
+ if key not in self._vc:
57
+ # build an uncompiled closure once; mx.compile wrapper around it
58
+ eps = V.eps
59
+
60
+ def fn(X, pbs, amask, _M=M, _W=W):
61
+ m4 = amask.reshape(1, 1, _M, _W)
62
+ for li, L in enumerate(V.layers):
63
+ qkv = trit_gemm_m(L.qkv_planes, L.qkv_slo, L.qkv_shi,
64
+ mx.fast.rms_norm(X, L.norm1, eps))
65
+ qrows = [qknorm_rope_append(
66
+ qkv[m], L.qnorm, L.knorm, V.kc[li], V.vc[li],
67
+ V.ct, V.st, pbs[m:m + 1], self._specmask,
68
+ V.n_heads, V.n_kv, V.hd, V.maxseq, eps)
69
+ for m in range(_M)]
70
+ q = mx.stack(qrows).reshape(_M, V.n_heads, V.hd)
71
+ q = q.transpose(1, 0, 2)[None]
72
+ a = mx.fast.scaled_dot_product_attention(
73
+ q, V.kc[li][:, :_W][None], V.vc[li][:, :_W][None],
74
+ scale=V.scale, mask=m4)
75
+ a = a[0].transpose(1, 0, 2).reshape(_M, V.qd)
76
+ X = X + trit_gemm_m(L.o_planes, L.o_slo, L.o_shi, a)
77
+ gu = trit_gemm_m(L.gu_planes, L.gu_slo, L.gu_shi,
78
+ mx.fast.rms_norm(X, L.norm2, eps))
79
+ h = _silu(gu[:, :V.inter]) * gu[:, V.inter:]
80
+ X = X + trit_gemm_m(L.dn_planes, L.dn_slo, L.dn_shi, h)
81
+ logits = s8_gemm_m(V.eoW, V.eo_scale,
82
+ mx.fast.rms_norm(X, V.final_norm, eps))
83
+ return mx.argmax(logits, axis=1)
84
+
85
+ self._vc[key] = mx.compile(fn)
86
+ tarr = mx.array(toks, dtype=mx.int32)
87
+ X = V.embW[tarr].astype(mx.float32) * V.embS[tarr][:, None]
88
+ pbs = mx.array([pos0 + m for m in range(M)], dtype=mx.int32)
89
+ idx = mx.arange(W)[None, :]
90
+ rowp = (pos0 + mx.arange(M))[:, None]
91
+ amask = mx.where(idx <= rowp, mx.array(0.0, dtype=mx.float32),
92
+ mx.array(NEG_INF, dtype=mx.float32))
93
+ am = self._vc[(M, W)](X, pbs, amask)
94
+ mx.eval(am)
95
+ return [int(t) for t in am]
96
+
97
+ # ---------------- draft helpers (single-token path) ----------------
98
+
99
+ def _draft_chain(self, pending, dmask, dpos):
100
+ """consume pending tokens, then chain k greedy proposals.
101
+ Returns (proposals, new_dpos). Draft KV/mask advance by
102
+ len(pending) + k - 1 consumed inputs."""
103
+ # NOTE: each step is dispatched with async_eval before the next is
104
+ # built — the KV/mask writes are input mutations invisible to the
105
+ # lazy graph, and batching several steps into one evaluation makes
106
+ # MLX copy-on-write the caches and silently drop intermediate
107
+ # appends (found the hard way; the shipped generate() uses the same
108
+ # per-step dispatch discipline).
109
+ D, k = self.D, self.k
110
+ pos = dpos
111
+ nt = x = None
112
+ for t in pending:
113
+ pb = mx.array([pos], dtype=mx.int32)
114
+ nt, x, _ = D.step_compiled(D.embed(int(t)), pb, dmask,
115
+ D._window(pos))
116
+ mx.async_eval(nt, x)
117
+ pos += 1
118
+ props = [nt] # d_1
119
+ for _ in range(k - 1):
120
+ pb = mx.array([pos], dtype=mx.int32)
121
+ nt, x, _ = D.step_compiled(x, pb, dmask, D._window(pos))
122
+ mx.async_eval(nt, x)
123
+ props.append(nt)
124
+ pos += 1
125
+ mx.eval(*props)
126
+ return [int(p) for p in props], pos
127
+
128
+ # ---------------- main loop ----------------
129
+
130
+ def generate(self, ids, n):
131
+ """greedy spec decode: n tokens after prompt ids.
132
+ Returns (tokens, decode_seconds, stats)."""
133
+ V, D, k = self.V, self.D, self.k
134
+ vmask = V.new_mask()
135
+ dmask = D.new_mask()
136
+ _, p0 = V.prefill(ids, vmask)
137
+ _, _ = D.prefill(ids, dmask)
138
+ # both consumed ids[:-1]; next input ids[-1] at position p0
139
+ pending = [int(ids[-1])]
140
+ vpos = dpos = p0
141
+ toks = []
142
+ rounds = accepted = proposed = 0
143
+ hist = [0] * (k + 1)
144
+ t0 = time.perf_counter()
145
+ while len(toks) < n:
146
+ # invariant: pending[-1] sits at position vpos for the verifier
147
+ # and at dpos + len(pending) - 1 for the draft — same slot.
148
+ assert dpos + len(pending) - 1 == vpos, (dpos, len(pending), vpos)
149
+ props, dpos_after = self._draft_chain(pending, dmask, dpos)
150
+ am = self._verify([pending[-1]] + props, vpos)
151
+ nacc = 0
152
+ for i in range(k):
153
+ if props[i] == am[i]:
154
+ nacc += 1
155
+ else:
156
+ break
157
+ rounds += 1
158
+ proposed += k
159
+ accepted += nacc
160
+ hist[nacc] += 1
161
+ base = vpos # position of pending[-1]
162
+ if nacc == k:
163
+ emitted = props + [am[k]]
164
+ pending = [props[k - 1], am[k]]
165
+ # draft consumed through d_{k-1} at base+k-1: no rewind
166
+ dpos = dpos_after
167
+ else:
168
+ emitted = props[:nacc] + [am[nacc]]
169
+ pending = [am[nacc]]
170
+ # draft rewind: valid through input d_nacc at base+nacc
171
+ lo, hi = base + nacc + 1, dpos_after
172
+ if hi > lo:
173
+ dmask[lo:hi] = NEG_INF
174
+ dpos = lo
175
+ vpos = base + nacc + 1 # verifier valid: <= base+nacc
176
+ toks.extend(emitted)
177
+ dt = time.perf_counter() - t0
178
+ toks = toks[:n]
179
+ self.stats = {
180
+ "k": k, "rounds": rounds, "proposed": proposed,
181
+ "accepted": accepted,
182
+ "acceptance": round(accepted / max(proposed, 1), 4),
183
+ "tokens_per_round": round(len(toks) / max(rounds, 1), 3),
184
+ "accept_hist": hist,
185
+ }
186
+ return toks, dt, self.stats
187
+
188
+
189
+ # ---------------- gate + bench drivers ----------------
190
+
191
+ GATE_PROMPTS = {
192
+ "facts": "The three laws of thermodynamics state that",
193
+ "prose": "Once upon a time, in a quiet village by the sea,",
194
+ "chat": "User: How do I reverse a list in Python?\nAssistant:",
195
+ }
196
+
197
+
198
+ def specgate(verifier, draft, tok, k=4, lens=(32, 256), receipt=None):
199
+ """[G1 frozen] WITH --spec vs WITHOUT: token-identical at each length,
200
+ every prompt. Any mismatch = flag does not ship."""
201
+ sd = SpecDecoder(verifier, draft, k=k)
202
+ out = {"gate": "specgate", "k": k, "lens": list(lens), "prompts": {},
203
+ "verdict": "PASS"}
204
+ for name, text in GATE_PROMPTS.items():
205
+ ids = tok(text, add_special_tokens=False).input_ids if tok else None
206
+ assert ids, "specgate needs a tokenizer"
207
+ res = {}
208
+ for n in lens:
209
+ plain, _ = verifier.generate(ids, n)
210
+ spec, _, st = sd.generate(ids, n)
211
+ same = plain == spec
212
+ first = next((i for i, (a, b) in enumerate(zip(plain, spec))
213
+ if a != b), -1)
214
+ res[str(n)] = {"identical": same, "first_divergence": first,
215
+ "acceptance": st["acceptance"]}
216
+ if not same:
217
+ out["verdict"] = "FAIL"
218
+ print(f" [{name} n={n}] identical={same} "
219
+ f"acc={st['acceptance']:.3f}", flush=True)
220
+ out["prompts"][name] = res
221
+ if receipt:
222
+ json.dump(out, open(receipt, "w"), indent=1)
223
+ print(f"receipt -> {receipt}")
224
+ return out
225
+
226
+
227
+ def specbench(verifier, draft, tok, k=4, n=512, runs=3, receipt=None):
228
+ """[G2 frozen] plain-8B vs 8B+draft medians, 3 text types, same process."""
229
+ sd = SpecDecoder(verifier, draft, k=k)
230
+ out = {"bench": "specbench", "k": k, "n": n, "runs": runs, "types": {}}
231
+ for name, text in GATE_PROMPTS.items():
232
+ ids = tok(text, add_special_tokens=False).input_ids
233
+ plain_r, spec_r, accs = [], [], []
234
+ for r in range(runs):
235
+ t, dt = verifier.generate(ids, n)
236
+ plain_r.append(n / dt)
237
+ st_toks, sdt, st = sd.generate(ids, n)
238
+ spec_r.append(n / sdt)
239
+ accs.append(st["acceptance"])
240
+ print(f" [{name} run{r}] plain {plain_r[-1]:.1f} tok/s | "
241
+ f"spec {spec_r[-1]:.1f} tok/s | acc {accs[-1]:.3f} | "
242
+ f"tok/round {st['tokens_per_round']}", flush=True)
243
+ med = lambda v: sorted(v)[len(v) // 2]
244
+ out["types"][name] = {
245
+ "plain_median_tok_s": round(med(plain_r), 2),
246
+ "spec_median_tok_s": round(med(spec_r), 2),
247
+ "speedup": round(med(spec_r) / med(plain_r), 3),
248
+ "acceptance_median": round(med(accs), 4),
249
+ "plain_runs": [round(v, 2) for v in plain_r],
250
+ "spec_runs": [round(v, 2) for v in spec_r],
251
+ }
252
+ out["peak_mem_gib"] = round(mx.get_peak_memory() / 2**30, 2)
253
+ if receipt:
254
+ json.dump(out, open(receipt, "w"), indent=1)
255
+ print(f"receipt -> {receipt}")
256
+ return out
mlx/fermion_mlx/trit_kernels.py ADDED
@@ -0,0 +1,1053 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Five-value {0, +-s_lo, +-s_hi} 3-bit-planar GEMV as mx.fast.metal_kernel.
3
+
4
+ Decode: per output row, three bit planes (bp=positive, bn=negative, br=high-mag),
5
+ LSB-first. sign = p - n, magnitude selects per-row s_lo / s_hi. The kernel keeps
6
+ two fp32 accumulators (lo/hi) so the per-row scales multiply once at the end —
7
+ the inner loop is pure bit-extract + FMA (the Metal twin of the NEON tbl /
8
+ CUDA PRMT decode; MSL has no byte shuffle, ALU decode is the standard move).
9
+
10
+ Layout: one simdgroup per output row, SG_PER_TG simdgroups per threadgroup.
11
+ Lanes stride uchar4 loads across the row's plane bytes; simd_sum reduction.
12
+
13
+ Template flags (baked into per-shape kernel source, cached):
14
+ HAS_BIAS : add fp32 bias[row]
15
+ FUSE_NORM : x is pre-norm; kernel computes rmsnorm(x)*g inline (g = extra input)
16
+ ADD_RESID : out[row] = resid[row] + y
17
+ GLU : down-proj mode; x has 2*INTER entries [gate;up], col value is
18
+ silu(gate[c]) * up[c] (fuses the SwiGLU elementwise pass into GEMV)
19
+ """
20
+ import mlx.core as mx
21
+
22
+ _HEADER = """
23
+ static inline float trit_silu(float v) { return v / (1.0f + metal::exp(-v)); }
24
+ """
25
+
26
+ _CACHE = {}
27
+
28
+
29
+ def _norm_pre(cols, eps):
30
+ return f"""
31
+ float ssq = 0.0f;
32
+ for (uint c = lane; c < {cols}u; c += 32u) {{ float v = x[c]; ssq = metal::fma(v, v, ssq); }}
33
+ ssq = metal::simd_sum(ssq);
34
+ float rms = metal::rsqrt(ssq / {float(cols)}f + {eps:.9g}f);
35
+ """
36
+
37
+
38
+ def _gemv_source(rows, cols, rs, sg_per_tg, has_bias, fuse_norm, add_resid, glu, eps):
39
+ """v1: per-bit scalar decode with sparsity branches."""
40
+ assert rs % 4 == 0, "row_stride must be multiple of 4 for uchar4 loads"
41
+ tail = rs * 8 != cols
42
+ # x effective value macro
43
+ if glu:
44
+ xv = "trit_silu(x[c]) * x[c + %d]" % cols
45
+ norm_pre = ""
46
+ elif fuse_norm:
47
+ xv = "x[c] * nw[c] * rms"
48
+ norm_pre = _norm_pre(cols, eps)
49
+ else:
50
+ xv = "x[c]"
51
+ norm_pre = ""
52
+ guard = f"if (c >= {cols}u) break;" if tail else ""
53
+ return f"""
54
+ uint tid = thread_position_in_threadgroup.x;
55
+ uint lane = tid & 31u;
56
+ uint sg = tid >> 5u;
57
+ uint row = threadgroup_position_in_grid.x * {sg_per_tg}u + sg;
58
+ if (row >= {rows}u) return;
59
+ {norm_pre}
60
+ const device uchar4* p4 = (const device uchar4*)bp + row * {rs // 4}u;
61
+ const device uchar4* n4 = (const device uchar4*)bn + row * {rs // 4}u;
62
+ const device uchar4* h4 = (const device uchar4*)br + row * {rs // 4}u;
63
+ float accLo = 0.0f, accHi = 0.0f;
64
+ for (uint i = lane; i < {rs // 4}u; i += 32u) {{
65
+ uchar4 pb = p4[i], nb = n4[i], hb = h4[i];
66
+ uint cb = i * 32u;
67
+ for (uint k = 0u; k < 4u; ++k) {{
68
+ uint pw = pb[k], nw2 = nb[k], hw = hb[k];
69
+ if ((pw | nw2) == 0u) continue;
70
+ for (uint j = 0u; j < 8u; ++j) {{
71
+ uint c = cb + k * 8u + j;
72
+ {guard}
73
+ float s = (float)((pw >> j) & 1u) - (float)((nw2 >> j) & 1u);
74
+ if (s == 0.0f) continue;
75
+ float v = s * ({xv});
76
+ if ((hw >> j) & 1u) accHi += v; else accLo += v;
77
+ }}
78
+ }}
79
+ }}
80
+ accLo = metal::simd_sum(accLo);
81
+ accHi = metal::simd_sum(accHi);
82
+ if (lane == 0u) {{
83
+ float y = metal::fma(slo[row], accLo, shi[row] * accHi);
84
+ {"y += bias[row];" if has_bias else ""}
85
+ {"y += resid[row];" if add_resid else ""}
86
+ out[row] = y;
87
+ }}
88
+ """
89
+
90
+
91
+ def _gemv_source_v2(rows, cols, rs, sg_per_tg, has_bias, fuse_norm, add_resid, glu, eps):
92
+ """v2: branchless decode. Planes loaded as uint32 (32 weights per load),
93
+ per-4-bit float4 select against float4 x loads. Requires cols % 32 == 0
94
+ and rs % 4 == 0 with rs*8 == cols (true for all Qwen shapes)."""
95
+ assert rs % 4 == 0 and rs * 8 == cols and cols % 32 == 0
96
+ if glu:
97
+ xload = f"""float4 g = xg4[w * 8u + k], u = xu4[w * 8u + k];
98
+ float4 xv = g / (1.0f + metal::exp(-g)) * u;"""
99
+ norm_pre = ""
100
+ xpre = f"""
101
+ const device float4* xg4 = (const device float4*)x;
102
+ const device float4* xu4 = (const device float4*)(x + {cols}u);"""
103
+ elif fuse_norm:
104
+ xload = """float4 xv = x4[w * 8u + k] * nw4[w * 8u + k] * rms;"""
105
+ norm_pre = _norm_pre(cols, eps)
106
+ xpre = """
107
+ const device float4* x4 = (const device float4*)x;
108
+ const device float4* nw4 = (const device float4*)nw;"""
109
+ else:
110
+ xload = """float4 xv = x4[w * 8u + k];"""
111
+ norm_pre = ""
112
+ xpre = """
113
+ const device float4* x4 = (const device float4*)x;"""
114
+ return f"""
115
+ uint tid = thread_position_in_threadgroup.x;
116
+ uint lane = tid & 31u;
117
+ uint sg = tid >> 5u;
118
+ uint row = threadgroup_position_in_grid.x * {sg_per_tg}u + sg;
119
+ if (row >= {rows}u) return;
120
+ {norm_pre}{xpre}
121
+ const device uint* p32 = (const device uint*)bp + row * {rs // 4}u;
122
+ const device uint* n32 = (const device uint*)bn + row * {rs // 4}u;
123
+ const device uint* h32 = (const device uint*)br + row * {rs // 4}u;
124
+ float4 accLo4 = 0.0f, accHi4 = 0.0f;
125
+ for (uint w = lane; w < {rs // 4}u; w += 32u) {{
126
+ uint pw = p32[w], nw2 = n32[w], hw = h32[w];
127
+ for (uint k = 0u; k < 8u; ++k) {{
128
+ {xload}
129
+ uint sh = k * 4u;
130
+ float4 s = float4(float((pw >> sh) & 1u), float((pw >> (sh+1u)) & 1u),
131
+ float((pw >> (sh+2u)) & 1u), float((pw >> (sh+3u)) & 1u))
132
+ - float4(float((nw2 >> sh) & 1u), float((nw2 >> (sh+1u)) & 1u),
133
+ float((nw2 >> (sh+2u)) & 1u), float((nw2 >> (sh+3u)) & 1u));
134
+ float4 v = s * xv;
135
+ bool4 hi = bool4((hw >> sh) & 1u, (hw >> (sh+1u)) & 1u,
136
+ (hw >> (sh+2u)) & 1u, (hw >> (sh+3u)) & 1u);
137
+ accHi4 += metal::select(float4(0.0f), v, hi);
138
+ accLo4 += metal::select(v, float4(0.0f), hi);
139
+ }}
140
+ }}
141
+ float accLo = metal::simd_sum(accLo4.x + accLo4.y + accLo4.z + accLo4.w);
142
+ float accHi = metal::simd_sum(accHi4.x + accHi4.y + accHi4.z + accHi4.w);
143
+ if (lane == 0u) {{
144
+ float y = metal::fma(slo[row], accLo, shi[row] * accHi);
145
+ {"y += bias[row];" if has_bias else ""}
146
+ {"y += resid[row];" if add_resid else ""}
147
+ out[row] = y;
148
+ }}
149
+ """
150
+
151
+
152
+ def _s8_source(rows, cols, rs, sg_per_tg, fuse_norm, eps):
153
+ assert rs % 4 == 0
154
+ if fuse_norm:
155
+ xv = lambda c: f"x[{c}] * nw[{c}] * rms"
156
+ norm_pre = f"""
157
+ float ssq = 0.0f;
158
+ for (uint c = lane; c < {cols}u; c += 32u) {{ float v = x[c]; ssq = metal::fma(v, v, ssq); }}
159
+ ssq = metal::simd_sum(ssq);
160
+ float rms = metal::rsqrt(ssq / {float(cols)}f + {eps:.9g}f);
161
+ """
162
+ else:
163
+ xv = lambda c: f"x[{c}]"
164
+ norm_pre = ""
165
+ assert rs * 8 >= cols and rs >= cols, "int8 record: expect row_stride >= cols"
166
+ nvec = cols // 4
167
+ return f"""
168
+ uint tid = thread_position_in_threadgroup.x;
169
+ uint lane = tid & 31u;
170
+ uint sg = tid >> 5u;
171
+ uint row = threadgroup_position_in_grid.x * {sg_per_tg}u + sg;
172
+ if (row >= {rows}u) return;
173
+ {norm_pre}
174
+ const device char* w = (const device char*)W + (ulong)row * {rs}u;
175
+ float acc = 0.0f;
176
+ for (uint i = lane * 4u; i + 3u < {cols}u; i += 128u) {{
177
+ char4 wv = *(const device char4*)(w + i);
178
+ uint c = i;
179
+ acc = metal::fma((float)wv.x, {xv('c')}, acc);
180
+ acc = metal::fma((float)wv.y, {xv('c + 1u')}, acc);
181
+ acc = metal::fma((float)wv.z, {xv('c + 2u')}, acc);
182
+ acc = metal::fma((float)wv.w, {xv('c + 3u')}, acc);
183
+ }}
184
+ acc = metal::simd_sum(acc);
185
+ if (lane == 0u) out[row] = scale[row] * acc;
186
+ """ if nvec * 4 == cols else None
187
+
188
+
189
+ SG_PER_TG = int(__import__("os").environ.get("TRIT_METAL_SGPTG", "8"))
190
+
191
+
192
+ import os
193
+ KERNEL_VERSION = int(os.environ.get("TRIT_METAL_V", "1"))
194
+ RPSG = int(os.environ.get("TRIT_METAL_RPSG", "8"))
195
+
196
+
197
+ def _gemv_source_v3(rows, cols, rs, sg_per_tg, has_bias, fuse_norm, add_resid, glu, eps,
198
+ rpsg=RPSG):
199
+ """v3: rpsg rows per simdgroup; x (and silu/norm work) loaded once per
200
+ column window and reused across rows in registers — kills the per-row
201
+ x-reload that bound v1/v2 at ~20 GB/s. 4-wide horizontal accumulate."""
202
+ assert rs % 4 == 0 and rs * 8 == cols and cols % 32 == 0 and rows % rpsg == 0
203
+ if glu:
204
+ xload = f"""float4 g = xg4[w * 8u + k], u = xu4[w * 8u + k];
205
+ float4 xv = g / (1.0f + metal::exp(-g)) * u;"""
206
+ norm_pre = ""
207
+ xpre = f"""
208
+ const device float4* xg4 = (const device float4*)x;
209
+ const device float4* xu4 = (const device float4*)(x + {cols}u);"""
210
+ elif fuse_norm:
211
+ xload = "float4 xv = x4[w * 8u + k] * nw4[w * 8u + k] * rms;"
212
+ norm_pre = _norm_pre(cols, eps)
213
+ xpre = """
214
+ const device float4* x4 = (const device float4*)x;
215
+ const device float4* nw4 = (const device float4*)nw;"""
216
+ else:
217
+ xload = "float4 xv = x4[w * 8u + k];"
218
+ norm_pre = ""
219
+ xpre = """
220
+ const device float4* x4 = (const device float4*)x;"""
221
+ return f"""
222
+ uint tid = thread_position_in_threadgroup.x;
223
+ uint lane = tid & 31u;
224
+ uint sg = tid >> 5u;
225
+ uint row0 = (threadgroup_position_in_grid.x * {sg_per_tg}u + sg) * {rpsg}u;
226
+ if (row0 >= {rows}u) return;
227
+ {norm_pre}{xpre}
228
+ const device uint* p32 = (const device uint*)bp;
229
+ const device uint* n32 = (const device uint*)bn;
230
+ const device uint* h32 = (const device uint*)br;
231
+ float accLo[{rpsg}], accHi[{rpsg}];
232
+ for (uint r = 0u; r < {rpsg}u; ++r) {{ accLo[r] = 0.0f; accHi[r] = 0.0f; }}
233
+ const float4 one4 = float4(1.0f);
234
+ for (uint w = lane; w < {rs // 4}u; w += 32u) {{
235
+ uint pw[{rpsg}], nw2[{rpsg}], hw[{rpsg}];
236
+ for (uint r = 0u; r < {rpsg}u; ++r) {{
237
+ ulong off = (ulong)(row0 + r) * {rs // 4}u + w;
238
+ pw[r] = p32[off]; nw2[r] = n32[off]; hw[r] = h32[off];
239
+ }}
240
+ for (uint k = 0u; k < 8u; ++k) {{
241
+ {xload}
242
+ uint sh = k * 4u;
243
+ for (uint r = 0u; r < {rpsg}u; ++r) {{
244
+ float4 s = float4(float((pw[r] >> sh) & 1u), float((pw[r] >> (sh+1u)) & 1u),
245
+ float((pw[r] >> (sh+2u)) & 1u), float((pw[r] >> (sh+3u)) & 1u))
246
+ - float4(float((nw2[r] >> sh) & 1u), float((nw2[r] >> (sh+1u)) & 1u),
247
+ float((nw2[r] >> (sh+2u)) & 1u), float((nw2[r] >> (sh+3u)) & 1u));
248
+ float4 v = s * xv;
249
+ bool4 hi = bool4((hw[r] >> sh) & 1u, (hw[r] >> (sh+1u)) & 1u,
250
+ (hw[r] >> (sh+2u)) & 1u, (hw[r] >> (sh+3u)) & 1u);
251
+ accHi[r] += metal::dot(metal::select(float4(0.0f), v, hi), one4);
252
+ accLo[r] += metal::dot(metal::select(v, float4(0.0f), hi), one4);
253
+ }}
254
+ }}
255
+ }}
256
+ for (uint r = 0u; r < {rpsg}u; ++r) {{
257
+ float lo = metal::simd_sum(accLo[r]);
258
+ float hi = metal::simd_sum(accHi[r]);
259
+ if (lane == 0u) {{
260
+ uint row = row0 + r;
261
+ float y = metal::fma(slo[row], lo, shi[row] * hi);
262
+ {"y += bias[row];" if has_bias else ""}
263
+ {"y += resid[row];" if add_resid else ""}
264
+ out[row] = y;
265
+ }}
266
+ }}
267
+ """
268
+
269
+
270
+ def _gemv_source_v4(rows, cols, rs, sg_per_tg, has_bias, fuse_norm, add_resid, glu, eps,
271
+ rpsg=RPSG):
272
+ """v4: threadgroup-memory LUT decode (Metal twin of NEON vqtbl1q).
273
+ Per 4-column group g, lut[g][m] = sum of xv[4g+i] over set bits of m
274
+ (16 subset sums). Split planes on the fly into lo/hi pos/neg nibbles
275
+ (a=p&~h, b=n&~h, c=p&h, d=n&h): 4 weights cost 4 LUT loads + 4 adds.
276
+ LUT stride 17 floats kills the 16-float bank-conflict pattern. Tiled over
277
+ columns when the LUT would exceed threadgroup memory (down proj)."""
278
+ assert rs % 4 == 0 and rs * 8 == cols and cols % 32 == 0 and rows % rpsg == 0
279
+ # column tiling: LUT floats = tile/4*17, xbuf floats = tile -> keep <= ~26KB
280
+ ntiles = 1
281
+ while (cols // ntiles) % 32 != 0 or (cols // ntiles) * (17 / 4 + 1) * 4 > 26624:
282
+ ntiles += 1
283
+ assert ntiles <= 16
284
+ tile = cols // ntiles
285
+ gpt = tile // 4 # groups per tile
286
+ w0 = tile // 32 # 32-weight words per tile (per row)
287
+ nthreads = sg_per_tg * 32
288
+ if glu:
289
+ xv_expr = "{ float gg = x[c]; xb[cl] = gg / (1.0f + metal::exp(-gg)) * x[c + %du]; }" % cols
290
+ elif fuse_norm:
291
+ xv_expr = "xb[cl] = x[c] * nw[c] * rms;"
292
+ else:
293
+ xv_expr = "xb[cl] = x[c];"
294
+ if fuse_norm:
295
+ norm_pre = f"""
296
+ threadgroup float ssq_sh[{sg_per_tg}];
297
+ {{
298
+ float ssq = 0.0f;
299
+ for (uint c = tid; c < {cols}u; c += {nthreads}u) {{ float v = x[c]; ssq = metal::fma(v, v, ssq); }}
300
+ ssq = metal::simd_sum(ssq);
301
+ if (lane == 0u) ssq_sh[sg] = ssq;
302
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
303
+ float t = 0.0f;
304
+ for (uint i = 0u; i < {sg_per_tg}u; ++i) t += ssq_sh[i];
305
+ rms = metal::rsqrt(t / {float(cols)}f + {eps:.9g}f);
306
+ }}
307
+ """
308
+ else:
309
+ norm_pre = ""
310
+ return f"""
311
+ uint tid = thread_position_in_threadgroup.x;
312
+ uint lane = tid & 31u;
313
+ uint sg = tid >> 5u;
314
+ uint row0 = (threadgroup_position_in_grid.x * {sg_per_tg}u + sg) * {rpsg}u;
315
+ float rms = 1.0f;
316
+ {norm_pre}
317
+ threadgroup float xb[{tile}];
318
+ threadgroup float lut[{gpt} * 17];
319
+ const device uint* p32 = (const device uint*)bp;
320
+ const device uint* n32 = (const device uint*)bn;
321
+ const device uint* h32 = (const device uint*)br;
322
+ float accLo[{rpsg}], accHi[{rpsg}];
323
+ for (uint r = 0u; r < {rpsg}u; ++r) {{ accLo[r] = 0.0f; accHi[r] = 0.0f; }}
324
+ for (uint t = 0u; t < {ntiles}u; ++t) {{
325
+ // stage effective x for this tile
326
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
327
+ for (uint cl = tid; cl < {tile}u; cl += {nthreads}u) {{
328
+ uint c = t * {tile}u + cl;
329
+ {xv_expr}
330
+ }}
331
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
332
+ // build 16 subset sums per 4-col group
333
+ for (uint e = tid; e < {gpt}u * 16u; e += {nthreads}u) {{
334
+ uint g = e >> 4u, m = e & 15u;
335
+ const threadgroup float* xg = xb + g * 4u;
336
+ float v = 0.0f;
337
+ if (m & 1u) v += xg[0];
338
+ if (m & 2u) v += xg[1];
339
+ if (m & 4u) v += xg[2];
340
+ if (m & 8u) v += xg[3];
341
+ lut[g * 17u + m] = v;
342
+ }}
343
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
344
+ if (row0 < {rows}u) {{
345
+ for (uint wl = lane; wl < {w0}u; wl += 32u) {{
346
+ uint w = t * {w0}u + wl;
347
+ for (uint r = 0u; r < {rpsg}u; ++r) {{
348
+ ulong off = (ulong)(row0 + r) * {rs // 4}u + w;
349
+ uint pw = p32[off], nw2 = n32[off], hw = h32[off];
350
+ uint a = pw & ~hw, b = nw2 & ~hw, cc = pw & hw, d = nw2 & hw;
351
+ uint gbase = wl * 8u;
352
+ float lo = 0.0f, hi = 0.0f;
353
+ for (uint k = 0u; k < 8u; ++k) {{
354
+ const threadgroup float* lg = lut + (gbase + k) * 17u;
355
+ uint sh = k * 4u;
356
+ lo += lg[(a >> sh) & 15u] - lg[(b >> sh) & 15u];
357
+ hi += lg[(cc >> sh) & 15u] - lg[(d >> sh) & 15u];
358
+ }}
359
+ accLo[r] += lo; accHi[r] += hi;
360
+ }}
361
+ }}
362
+ }}
363
+ }}
364
+ if (row0 >= {rows}u) return;
365
+ for (uint r = 0u; r < {rpsg}u; ++r) {{
366
+ float lo = metal::simd_sum(accLo[r]);
367
+ float hi = metal::simd_sum(accHi[r]);
368
+ if (lane == 0u) {{
369
+ uint row = row0 + r;
370
+ float y = metal::fma(slo[row], lo, shi[row] * hi);
371
+ {"y += bias[row];" if has_bias else ""}
372
+ {"y += resid[row];" if add_resid else ""}
373
+ out[row] = y;
374
+ }}
375
+ }}
376
+ """
377
+
378
+
379
+ import os as _os
380
+ _TILE_MAX = int(_os.environ.get("TRIT_METAL_TILE", "448"))
381
+
382
+
383
+ def _v5_tile(cols):
384
+ """largest tile <= TILE_MAX with tile % 32 == 0 and cols % tile == 0."""
385
+ for t in range(_TILE_MAX, 31, -32):
386
+ if cols % t == 0 and t % 32 == 0:
387
+ return t
388
+ raise ValueError(cols)
389
+
390
+
391
+ def _gemv_source_v5(rows, cols, rs, sg_per_tg, has_bias, fuse_norm, add_resid, glu, eps,
392
+ rpsg=8):
393
+ """v5 = v4 LUT decode with the two occupancy/bandwidth fixes:
394
+ (1) planes pre-interleaved on load: uint32 words stored [row_block, word,
395
+ row_in_block] so a lane's 8-row load is 32 contiguous bytes (streams
396
+ at DRAM bandwidth; the v3/v4 scattered pattern ran at 59% of it);
397
+ (2) small LUT tiles (<= ~7.6KB threadgroup memory) so several threadgroups
398
+ fit per GPU core (v4's 19-26KB allowed only one, exposing latency).
399
+ Requires planes packed with pack_planes_v5. rpsg fixed at 8 by layout."""
400
+ assert rs % 4 == 0 and rs * 8 == cols and cols % 32 == 0 and rows % 8 == 0
401
+ tile = _v5_tile(cols)
402
+ ntiles = cols // tile
403
+ gpt = tile // 4
404
+ w0 = tile // 32
405
+ W = rs // 4
406
+ nthreads = sg_per_tg * 32
407
+ if glu:
408
+ stage = f"""
409
+ for (uint cl = tid; cl < {tile}u; cl += {nthreads}u) {{
410
+ uint c = t * {tile}u + cl;
411
+ float gg = x[c];
412
+ xb[cl] = gg / (1.0f + metal::exp(-gg)) * x[c + {cols}u];
413
+ }}
414
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);"""
415
+ xval = "xb[gl * 4u + i]"
416
+ xb_decl = f"threadgroup float xb[{tile}];"
417
+ elif fuse_norm:
418
+ stage = ""
419
+ xval = "x[t * %du + gl * 4u + i] * nw[t * %du + gl * 4u + i] * rms" % (tile, tile)
420
+ xb_decl = ""
421
+ else:
422
+ stage = ""
423
+ xval = "x[t * %du + gl * 4u + i]" % tile
424
+ xb_decl = ""
425
+ if fuse_norm:
426
+ norm_pre = f"""
427
+ threadgroup float ssq_sh[{sg_per_tg}];
428
+ {{
429
+ float ssq = 0.0f;
430
+ for (uint c = tid; c < {cols}u; c += {nthreads}u) {{ float v = x[c]; ssq = metal::fma(v, v, ssq); }}
431
+ ssq = metal::simd_sum(ssq);
432
+ if (lane == 0u) ssq_sh[sg] = ssq;
433
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
434
+ float tt = 0.0f;
435
+ for (uint i = 0u; i < {sg_per_tg}u; ++i) tt += ssq_sh[i];
436
+ rms = metal::rsqrt(tt / {float(cols)}f + {eps:.9g}f);
437
+ }}
438
+ """
439
+ else:
440
+ norm_pre = ""
441
+ return f"""
442
+ uint tid = thread_position_in_threadgroup.x;
443
+ uint lane = tid & 31u;
444
+ uint sg = tid >> 5u;
445
+ uint blk = threadgroup_position_in_grid.x * {sg_per_tg}u + sg; // 8-row block
446
+ uint row0 = blk * 8u;
447
+ float rms = 1.0f;
448
+ {norm_pre}
449
+ {xb_decl}
450
+ threadgroup float lut[{gpt} * 17];
451
+ const device uint* p32 = (const device uint*)bp;
452
+ const device uint* n32 = (const device uint*)bn;
453
+ const device uint* h32 = (const device uint*)br;
454
+ float accLo[8], accHi[8];
455
+ for (uint r = 0u; r < 8u; ++r) {{ accLo[r] = 0.0f; accHi[r] = 0.0f; }}
456
+ for (uint t = 0u; t < {ntiles}u; ++t) {{
457
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
458
+ {stage}
459
+ for (uint e = tid; e < {gpt}u * 16u; e += {nthreads}u) {{
460
+ uint gl = e >> 4u, m = e & 15u;
461
+ float v = 0.0f;
462
+ for (uint i = 0u; i < 4u; ++i)
463
+ if ((m >> i) & 1u) v += {xval};
464
+ lut[gl * 17u + m] = v;
465
+ }}
466
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
467
+ if (row0 < {rows}u) {{
468
+ for (uint wl = lane; wl < {w0}u; wl += 32u) {{
469
+ uint w = t * {w0}u + wl;
470
+ ulong base = ((ulong)blk * {W}u + w) * 8u; // 8 rows contiguous
471
+ uint gbase = wl * 8u;
472
+ for (uint r = 0u; r < 8u; ++r) {{
473
+ uint pw = p32[base + r], nw2 = n32[base + r], hw = h32[base + r];
474
+ uint a = pw & ~hw, b = nw2 & ~hw, cc = pw & hw, d = nw2 & hw;
475
+ float lo = 0.0f, hi = 0.0f;
476
+ for (uint k = 0u; k < 8u; ++k) {{
477
+ const threadgroup float* lg = lut + (gbase + k) * 17u;
478
+ uint sh = k * 4u;
479
+ lo += lg[(a >> sh) & 15u] - lg[(b >> sh) & 15u];
480
+ hi += lg[(cc >> sh) & 15u] - lg[(d >> sh) & 15u];
481
+ }}
482
+ accLo[r] += lo; accHi[r] += hi;
483
+ }}
484
+ }}
485
+ }}
486
+ }}
487
+ if (row0 >= {rows}u) return;
488
+ for (uint r = 0u; r < 8u; ++r) {{
489
+ float lo = metal::simd_sum(accLo[r]);
490
+ float hi = metal::simd_sum(accHi[r]);
491
+ if (lane == 0u) {{
492
+ uint row = row0 + r;
493
+ float y = metal::fma(slo[row], lo, shi[row] * hi);
494
+ {"y += bias[row];" if has_bias else ""}
495
+ {"y += resid[row];" if add_resid else ""}
496
+ out[row] = y;
497
+ }}
498
+ }}
499
+ """
500
+
501
+
502
+ def _gemv_source_v6(rows, cols, rs, sg_per_tg, has_bias, fuse_norm, add_resid, glu, eps):
503
+ """v6: interleaved 8-row plane layout (pack_planes_v5) + mask-convert-fma
504
+ decode. For bit j of word a: (float)(a & (1<<j)) == 2^j when set, so
505
+ multiplying by x pre-scaled with 2^-j turns decode into AND + convert +
506
+ FMA (no shifts) — 4 masks (lo/hi x pos/neg) ~3.5 instr/weight, exact in
507
+ fp32 (power-of-two scales). x reused across the 8 rows in registers."""
508
+ assert rs % 4 == 0 and rs * 8 == cols and cols % 32 == 0 and rows % 8 == 0
509
+ W = rs // 4
510
+ if glu:
511
+ xload = f"""float4 g = xg4[w * 8u + k], u = xu4[w * 8u + k];
512
+ float4 xv = g / (1.0f + metal::exp(-g)) * u;"""
513
+ norm_pre = ""
514
+ xpre = f"""
515
+ const device float4* xg4 = (const device float4*)x;
516
+ const device float4* xu4 = (const device float4*)(x + {cols}u);"""
517
+ elif fuse_norm:
518
+ xload = "float4 xv = x4[w * 8u + k] * nw4[w * 8u + k] * rms;"
519
+ norm_pre = _norm_pre(cols, eps)
520
+ xpre = """
521
+ const device float4* x4 = (const device float4*)x;
522
+ const device float4* nw4 = (const device float4*)nw;"""
523
+ else:
524
+ xload = "float4 xv = x4[w * 8u + k];"
525
+ norm_pre = ""
526
+ xpre = """
527
+ const device float4* x4 = (const device float4*)x;"""
528
+ # unrolled k-body with literal masks / prescales
529
+ kbody = []
530
+ for k in range(8):
531
+ m = [1 << (4 * k + i) for i in range(4)]
532
+ ps = ", ".join("%.9ef" % (2.0 ** -(4 * k + i)) for i in range(4))
533
+ kbody.append(f""" {{
534
+ uint k = {k}u; {xload}
535
+ float4 xs = xv * float4({ps});
536
+ const uint4 m4 = uint4({m[0]}u, {m[1]}u, {m[2]}u, {m[3]}u);
537
+ for (uint r = 0u; r < 8u; ++r) {{
538
+ uint pw = pl[r], nw2 = nl[r], hw = hl[r];
539
+ uint a = pw & ~hw, b = nw2 & ~hw, cc = pw & hw, d = nw2 & hw;
540
+ accLo[r] += metal::dot(float4(uint4(a) & m4) - float4(uint4(b) & m4), xs);
541
+ accHi[r] += metal::dot(float4(uint4(cc) & m4) - float4(uint4(d) & m4), xs);
542
+ }}
543
+ }}""")
544
+ kbody = "\n".join(kbody)
545
+ return f"""
546
+ uint tid = thread_position_in_threadgroup.x;
547
+ uint lane = tid & 31u;
548
+ uint sg = tid >> 5u;
549
+ uint blk = threadgroup_position_in_grid.x * {sg_per_tg}u + sg;
550
+ uint row0 = blk * 8u;
551
+ if (row0 >= {rows}u) return;
552
+ {norm_pre}{xpre}
553
+ const device uint* p32 = (const device uint*)bp;
554
+ const device uint* n32 = (const device uint*)bn;
555
+ const device uint* h32 = (const device uint*)br;
556
+ float accLo[8], accHi[8];
557
+ for (uint r = 0u; r < 8u; ++r) {{ accLo[r] = 0.0f; accHi[r] = 0.0f; }}
558
+ for (uint w = lane; w < {W}u; w += 32u) {{
559
+ ulong base = ((ulong)blk * {W}u + w) * 8u;
560
+ uint pl[8], nl[8], hl[8];
561
+ for (uint r = 0u; r < 8u; ++r) {{ pl[r] = p32[base+r]; nl[r] = n32[base+r]; hl[r] = h32[base+r]; }}
562
+ {kbody}
563
+ }}
564
+ for (uint r = 0u; r < 8u; ++r) {{
565
+ float lo = metal::simd_sum(accLo[r]);
566
+ float hi = metal::simd_sum(accHi[r]);
567
+ if (lane == 0u) {{
568
+ uint row = row0 + r;
569
+ float y = metal::fma(slo[row], lo, shi[row] * hi);
570
+ {"y += bias[row];" if has_bias else ""}
571
+ {"y += resid[row];" if add_resid else ""}
572
+ out[row] = y;
573
+ }}
574
+ }}
575
+ """
576
+
577
+
578
+ def _gemv_source_v7(rows, cols, rs, sg_per_tg, has_bias, fuse_norm, add_resid, glu, eps):
579
+ """v7: v5 interleaved layout + LUT decode with full lane utilization.
580
+ Lane = (row r = lane&7, word-slot = lane>>3): each lane accumulates ONE
581
+ row over words wslot, wslot+4, ... — all 32 lanes busy for any w0 >= 4.
582
+ Row sums gathered with two simd_shuffle_down folds. LUT tile 448 cols
583
+ (7.6KB threadgroup mem) so multiple threadgroups stay resident per core."""
584
+ assert rs % 4 == 0 and rs * 8 == cols and cols % 32 == 0 and rows % 8 == 0
585
+ tile = _v5_tile(cols)
586
+ ntiles = cols // tile
587
+ gpt = tile // 4
588
+ w0 = tile // 32
589
+ W = rs // 4
590
+ nthreads = sg_per_tg * 32
591
+ if glu:
592
+ stage = f"""
593
+ for (uint cl = tid; cl < {tile}u; cl += {nthreads}u) {{
594
+ uint c = t * {tile}u + cl;
595
+ float gg = x[c];
596
+ xb[cl] = gg / (1.0f + metal::exp(-gg)) * x[c + {cols}u];
597
+ }}
598
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);"""
599
+ xval = "xb[gl * 4u + i]"
600
+ xb_decl = f"threadgroup float xb[{tile}];"
601
+ elif fuse_norm:
602
+ stage = ""
603
+ xval = "x[t * %du + gl * 4u + i] * nw[t * %du + gl * 4u + i] * rms" % (tile, tile)
604
+ xb_decl = ""
605
+ else:
606
+ stage = ""
607
+ xval = "x[t * %du + gl * 4u + i]" % tile
608
+ xb_decl = ""
609
+ if fuse_norm:
610
+ norm_pre = f"""
611
+ threadgroup float ssq_sh[{sg_per_tg}];
612
+ {{
613
+ float ssq = 0.0f;
614
+ for (uint c = tid; c < {cols}u; c += {nthreads}u) {{ float v = x[c]; ssq = metal::fma(v, v, ssq); }}
615
+ ssq = metal::simd_sum(ssq);
616
+ if (lane == 0u) ssq_sh[sg] = ssq;
617
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
618
+ float tt = 0.0f;
619
+ for (uint i = 0u; i < {sg_per_tg}u; ++i) tt += ssq_sh[i];
620
+ rms = metal::rsqrt(tt / {float(cols)}f + {eps:.9g}f);
621
+ }}
622
+ """
623
+ else:
624
+ norm_pre = ""
625
+ return f"""
626
+ uint tid = thread_position_in_threadgroup.x;
627
+ uint lane = tid & 31u;
628
+ uint sg = tid >> 5u;
629
+ uint blk = threadgroup_position_in_grid.x * {sg_per_tg}u + sg;
630
+ uint row0 = blk * 8u;
631
+ uint r = lane & 7u;
632
+ uint wsl = lane >> 3u;
633
+ float rms = 1.0f;
634
+ {norm_pre}
635
+ {xb_decl}
636
+ threadgroup float lut[{gpt} * 17];
637
+ const device uint* p32 = (const device uint*)bp;
638
+ const device uint* n32 = (const device uint*)bn;
639
+ const device uint* h32 = (const device uint*)br;
640
+ float accLo = 0.0f, accHi = 0.0f;
641
+ for (uint t = 0u; t < {ntiles}u; ++t) {{
642
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
643
+ {stage}
644
+ for (uint e = tid; e < {gpt}u * 16u; e += {nthreads}u) {{
645
+ uint gl = e >> 4u, m = e & 15u;
646
+ float v = 0.0f;
647
+ for (uint i = 0u; i < 4u; ++i)
648
+ if ((m >> i) & 1u) v += {xval};
649
+ lut[gl * 17u + m] = v;
650
+ }}
651
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
652
+ if (row0 < {rows}u) {{
653
+ for (uint wl = wsl; wl < {w0}u; wl += 4u) {{
654
+ ulong base = ((ulong)blk * {W}u + t * {w0}u + wl) * 8u + r;
655
+ uint pw = p32[base], nw2 = n32[base], hw = h32[base];
656
+ uint a = pw & ~hw, b = nw2 & ~hw, cc = pw & hw, d = nw2 & hw;
657
+ uint gbase = wl * 8u;
658
+ for (uint k = 0u; k < 8u; ++k) {{
659
+ const threadgroup float* lg = lut + (gbase + k) * 17u;
660
+ uint sh = k * 4u;
661
+ accLo += lg[(a >> sh) & 15u] - lg[(b >> sh) & 15u];
662
+ accHi += lg[(cc >> sh) & 15u] - lg[(d >> sh) & 15u];
663
+ }}
664
+ }}
665
+ }}
666
+ }}
667
+ if (row0 >= {rows}u) return;
668
+ accLo += metal::simd_shuffle_down(accLo, 16u);
669
+ accLo += metal::simd_shuffle_down(accLo, 8u);
670
+ accHi += metal::simd_shuffle_down(accHi, 16u);
671
+ accHi += metal::simd_shuffle_down(accHi, 8u);
672
+ if (lane < 8u) {{
673
+ uint row = row0 + lane;
674
+ float y = metal::fma(slo[row], accLo, shi[row] * accHi);
675
+ {"y += bias[row];" if has_bias else ""}
676
+ {"y += resid[row];" if add_resid else ""}
677
+ out[row] = y;
678
+ }}
679
+ """
680
+
681
+
682
+ def _gemv_source_v8(rows, cols, rs, sg_per_tg, has_bias, fuse_norm, add_resid,
683
+ glu, eps):
684
+ """v8: v7 LUT decode + DOUBLE-BUFFERED LUT — build tile t+1's LUT into
685
+ the alternate threadgroup buffer, then decode tile t: one barrier per
686
+ tile (plain/norm; two for glu) instead of v7's two/three. Entry build
687
+ order identical to v7 -> outputs BIT-IDENTICAL (gated 2026-07-28)."""
688
+ assert rs % 4 == 0 and rs * 8 == cols and cols % 32 == 0 and rows % 8 == 0
689
+ tile = _v5_tile(cols)
690
+ ntiles = cols // tile
691
+ gpt = tile // 4
692
+ w0 = tile // 32
693
+ W = rs // 4
694
+ nthreads = sg_per_tg * 32
695
+ if glu:
696
+ xval = "xb[bb * %du + gl * 4u + i]" % tile
697
+ xb_decl = f"threadgroup float xb[2 * {tile}];"
698
+ stage = f"""
699
+ for (uint cl = tid; cl < {tile}u; cl += {nthreads}u) {{
700
+ uint c = tt * {tile}u + cl;
701
+ float gg = x[c];
702
+ xb[bb * {tile}u + cl] = gg / (1.0f + metal::exp(-gg)) * x[c + {cols}u];
703
+ }}
704
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);"""
705
+ elif fuse_norm:
706
+ xval = "x[tt * %du + gl * 4u + i] * nw[tt * %du + gl * 4u + i] * rms" % (tile, tile)
707
+ xb_decl = ""
708
+ stage = ""
709
+ else:
710
+ xval = "x[tt * %du + gl * 4u + i]" % tile
711
+ xb_decl = ""
712
+ stage = ""
713
+ build = f"""{stage}
714
+ for (uint e = tid; e < {gpt}u * 16u; e += {nthreads}u) {{
715
+ uint gl = e >> 4u, m = e & 15u;
716
+ float v = 0.0f;
717
+ for (uint i = 0u; i < 4u; ++i)
718
+ if ((m >> i) & 1u) v += {xval};
719
+ lut[bb * {gpt}u * 17u + gl * 17u + m] = v;
720
+ }}"""
721
+ return _v89_frame(rows, cols, rs, sg_per_tg, has_bias, fuse_norm,
722
+ add_resid, eps, tile, ntiles, gpt, w0, W, xb_decl, build)
723
+
724
+
725
+ def _gemv_source_v9(rows, cols, rs, sg_per_tg, has_bias, fuse_norm, add_resid,
726
+ glu, eps):
727
+ """v9: double-buffered LUT + thread-per-GROUP incremental build. Each
728
+ builder thread computes its group's 4 effective x values ONCE in
729
+ registers (silu/norm included — no xb stage) and writes the 16 subset
730
+ sums by ascending-bit doubling; the norm path applies rms via explicit
731
+ fma to match v7's contraction. ONE barrier per tile for every flag
732
+ combo. BIT-IDENTICAL to v7 (gated: TF argmax 9/9, 128-tok greedy
733
+ identical)."""
734
+ assert rs % 4 == 0 and rs * 8 == cols and cols % 32 == 0 and rows % 8 == 0
735
+ tile = _v5_tile(cols)
736
+ ntiles = cols // tile
737
+ gpt = tile // 4
738
+ w0 = tile // 32
739
+ W = rs // 4
740
+ nthreads = sg_per_tg * 32
741
+ if glu:
742
+ xv = f"""float gg = x[c + i];
743
+ xv[i] = gg / (1.0f + metal::exp(-gg)) * x[c + i + {cols}u];"""
744
+ elif fuse_norm:
745
+ xv = "xv[i] = x[c + i] * nw[c + i];"
746
+ else:
747
+ xv = "xv[i] = x[c + i];"
748
+ if fuse_norm and not glu:
749
+ sums = """lg[0] = 0.0f;
750
+ lg[1] = metal::fma(xv[0], rms, 0.0f);
751
+ lg[2] = metal::fma(xv[1], rms, 0.0f);
752
+ lg[3] = metal::fma(xv[1], rms, lg[1]);
753
+ for (uint m = 0u; m < 4u; ++m) lg[4u + m] = metal::fma(xv[2], rms, lg[m]);
754
+ for (uint m = 0u; m < 8u; ++m) lg[8u + m] = metal::fma(xv[3], rms, lg[m]);"""
755
+ else:
756
+ sums = """lg[0] = 0.0f;
757
+ lg[1] = xv[0];
758
+ lg[2] = xv[1];
759
+ lg[3] = xv[0] + xv[1];
760
+ for (uint m = 0u; m < 4u; ++m) lg[4u + m] = lg[m] + xv[2];
761
+ for (uint m = 0u; m < 8u; ++m) lg[8u + m] = lg[m] + xv[3];"""
762
+ build = f"""
763
+ for (uint gl = tid; gl < {gpt}u; gl += {nthreads}u) {{
764
+ uint c = tt * {tile}u + gl * 4u;
765
+ float xv[4];
766
+ for (uint i = 0u; i < 4u; ++i) {{
767
+ {xv}
768
+ }}
769
+ threadgroup float* lg = lut + bb * {gpt}u * 17u + gl * 17u;
770
+ {sums}
771
+ }}"""
772
+ return _v89_frame(rows, cols, rs, sg_per_tg, has_bias, fuse_norm,
773
+ add_resid, eps, tile, ntiles, gpt, w0, W, "", build)
774
+
775
+
776
+ def _v89_frame(rows, cols, rs, sg_per_tg, has_bias, fuse_norm, add_resid,
777
+ eps, tile, ntiles, gpt, w0, W, xb_decl, build):
778
+ """shared v8/v9 skeleton: v7's lane mapping + decode, double-buffered
779
+ LUT loop (prologue build, then build-next/decode-current/one-barrier)."""
780
+ nthreads = sg_per_tg * 32
781
+ if fuse_norm:
782
+ norm_pre = f"""
783
+ threadgroup float ssq_sh[{sg_per_tg}];
784
+ {{
785
+ float ssq = 0.0f;
786
+ for (uint c = tid; c < {cols}u; c += {nthreads}u) {{ float v = x[c]; ssq = metal::fma(v, v, ssq); }}
787
+ ssq = metal::simd_sum(ssq);
788
+ if (lane == 0u) ssq_sh[sg] = ssq;
789
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
790
+ float tt = 0.0f;
791
+ for (uint i = 0u; i < {sg_per_tg}u; ++i) tt += ssq_sh[i];
792
+ rms = metal::rsqrt(tt / {float(cols)}f + {eps:.9g}f);
793
+ }}
794
+ """
795
+ else:
796
+ norm_pre = ""
797
+ return f"""
798
+ uint tid = thread_position_in_threadgroup.x;
799
+ uint lane = tid & 31u;
800
+ uint sg = tid >> 5u;
801
+ uint blk = threadgroup_position_in_grid.x * {sg_per_tg}u + sg;
802
+ uint row0 = blk * 8u;
803
+ uint r = lane & 7u;
804
+ uint wsl = lane >> 3u;
805
+ float rms = 1.0f;
806
+ {norm_pre}
807
+ {xb_decl}
808
+ threadgroup float lut[2 * {gpt} * 17];
809
+ const device uint* p32 = (const device uint*)bp;
810
+ const device uint* n32 = (const device uint*)bn;
811
+ const device uint* h32 = (const device uint*)br;
812
+ float accLo = 0.0f, accHi = 0.0f;
813
+ {{
814
+ uint tt = 0u, bb = 0u;
815
+ {build}
816
+ }}
817
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
818
+ for (uint t = 0u; t < {ntiles}u; ++t) {{
819
+ if (t + 1u < {ntiles}u) {{
820
+ uint tt = t + 1u, bb = (t + 1u) & 1u;
821
+ {build}
822
+ }}
823
+ {{
824
+ const threadgroup float* lutc = lut + (t & 1u) * {gpt}u * 17u;
825
+ if (row0 < {rows}u) {{
826
+ for (uint wl = wsl; wl < {w0}u; wl += 4u) {{
827
+ ulong base = ((ulong)blk * {W}u + t * {w0}u + wl) * 8u + r;
828
+ uint pw = p32[base], nw2 = n32[base], hw = h32[base];
829
+ uint a = pw & ~hw, b = nw2 & ~hw, cc = pw & hw, d = nw2 & hw;
830
+ uint gbase = wl * 8u;
831
+ for (uint k = 0u; k < 8u; ++k) {{
832
+ const threadgroup float* lg = lutc + (gbase + k) * 17u;
833
+ uint sh = k * 4u;
834
+ accLo += lg[(a >> sh) & 15u] - lg[(b >> sh) & 15u];
835
+ accHi += lg[(cc >> sh) & 15u] - lg[(d >> sh) & 15u];
836
+ }}
837
+ }}
838
+ }}
839
+ }}
840
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
841
+ }}
842
+ if (row0 >= {rows}u) return;
843
+ accLo += metal::simd_shuffle_down(accLo, 16u);
844
+ accLo += metal::simd_shuffle_down(accLo, 8u);
845
+ accHi += metal::simd_shuffle_down(accHi, 16u);
846
+ accHi += metal::simd_shuffle_down(accHi, 8u);
847
+ if (lane < 8u) {{
848
+ uint row = row0 + lane;
849
+ float y = metal::fma(slo[row], accLo, shi[row] * accHi);
850
+ {"y += bias[row];" if has_bias else ""}
851
+ {"y += resid[row];" if add_resid else ""}
852
+ out[row] = y;
853
+ }}
854
+ """
855
+
856
+
857
+ def prep_planes(planes, cols=None):
858
+ """Trim row padding (row_stride*8 > cols in real exports; pad bits are
859
+ zero) so rs*8 == cols, then apply the layout transform required by the
860
+ active kernel version. planes = (bp, bn, br) mx.uint8 [rows, rs]."""
861
+ if cols is not None and cols % 8 == 0 and planes[0].shape[1] > cols // 8:
862
+ planes = tuple(mx.contiguous(p[:, :cols // 8]) for p in planes)
863
+ if KERNEL_VERSION >= 5: # v5/v6 interleaved layout
864
+ return tuple(pack_planes_v5(p) for p in planes)
865
+ return planes
866
+
867
+
868
+ def pack_planes_v5(plane_mx):
869
+ """[rows, rs] uint8 -> v5 interleaved layout (same byte count):
870
+ uint32 words reordered to [row_block(8), word, row_in_block]."""
871
+ rows, rs = plane_mx.shape
872
+ assert rows % 8 == 0 and rs % 4 == 0
873
+ w = plane_mx.view(mx.uint32).reshape(rows // 8, 8, rs // 4)
874
+ return mx.contiguous(w.transpose(0, 2, 1)).view(mx.uint8).reshape(rows, rs)
875
+
876
+
877
+ def _rows_per_sg(rows, cols, rs, version, glu):
878
+ if version == 3 and rs % 4 == 0 and rs * 8 == cols and cols % 32 == 0 and rows % RPSG == 0:
879
+ return RPSG
880
+ return 1
881
+
882
+
883
+ def get_trit_gemv(rows, cols, rs, has_bias=False, fuse_norm=False, add_resid=False,
884
+ glu=False, eps=1e-6, version=None):
885
+ version = KERNEL_VERSION if version is None else version
886
+ if version >= 2 and not (rs % 4 == 0 and rs * 8 == cols and cols % 32 == 0):
887
+ version = 1 # v2+ layout preconditions not met, fall back
888
+ if version >= 3 and rows % (8 if version >= 5 else RPSG) != 0:
889
+ version = 2
890
+ key = ("trit", rows, cols, rs, has_bias, fuse_norm, add_resid, glu, round(eps, 12), version)
891
+ if key in _CACHE:
892
+ return _CACHE[key]
893
+ inputs = ["bp", "bn", "br", "slo", "shi", "x"]
894
+ if has_bias:
895
+ inputs.append("bias")
896
+ if fuse_norm and not glu:
897
+ inputs.append("nw")
898
+ if add_resid:
899
+ inputs.append("resid")
900
+ gen = {1: _gemv_source, 2: _gemv_source_v2, 3: _gemv_source_v3,
901
+ 4: _gemv_source_v4, 5: _gemv_source_v5, 6: _gemv_source_v6,
902
+ 7: _gemv_source_v7, 8: _gemv_source_v8, 9: _gemv_source_v9}[version]
903
+ src = gen(rows, cols, rs, SG_PER_TG, has_bias, fuse_norm, add_resid, glu, eps)
904
+ name = "tritgemv%d_r%d_c%d_b%d_n%d_a%d_g%d" % (version, rows, cols, has_bias, fuse_norm,
905
+ add_resid, glu)
906
+ k = mx.fast.metal_kernel(name=name, input_names=inputs, output_names=["out"],
907
+ source=src, header=_HEADER)
908
+ _CACHE[key] = (k, inputs, version)
909
+ return _CACHE[key]
910
+
911
+
912
+ def _s8_source_v2(rows, cols, rs, sg_per_tg, fuse_norm, eps, rpsg=RPSG):
913
+ """v2 int8 GEMV: rpsg rows per simdgroup, float4 x reuse across rows."""
914
+ assert rs % 4 == 0 and cols % 4 == 0 and rows % rpsg == 0
915
+ if fuse_norm:
916
+ xload = "float4 xv = x4[i] * nw4[i] * rms;"
917
+ norm_pre = _norm_pre(cols, eps)
918
+ xpre = """
919
+ const device float4* x4 = (const device float4*)x;
920
+ const device float4* nw4 = (const device float4*)nw;"""
921
+ else:
922
+ xload = "float4 xv = x4[i];"
923
+ norm_pre = ""
924
+ xpre = """
925
+ const device float4* x4 = (const device float4*)x;"""
926
+ return f"""
927
+ uint tid = thread_position_in_threadgroup.x;
928
+ uint lane = tid & 31u;
929
+ uint sg = tid >> 5u;
930
+ uint row0 = (threadgroup_position_in_grid.x * {sg_per_tg}u + sg) * {rpsg}u;
931
+ if (row0 >= {rows}u) return;
932
+ {norm_pre}{xpre}
933
+ float acc[{rpsg}];
934
+ for (uint r = 0u; r < {rpsg}u; ++r) acc[r] = 0.0f;
935
+ const float4 one4 = float4(1.0f);
936
+ for (uint i = lane; i < {cols // 4}u; i += 32u) {{
937
+ {xload}
938
+ for (uint r = 0u; r < {rpsg}u; ++r) {{
939
+ char4 wv = *(const device char4*)((const device char*)W
940
+ + (ulong)(row0 + r) * {rs}u + i * 4u);
941
+ acc[r] += metal::dot(float4(wv.x, wv.y, wv.z, wv.w) * xv, one4);
942
+ }}
943
+ }}
944
+ for (uint r = 0u; r < {rpsg}u; ++r) {{
945
+ float a = metal::simd_sum(acc[r]);
946
+ if (lane == 0u) out[row0 + r] = scale[row0 + r] * a;
947
+ }}
948
+ """
949
+
950
+
951
+ def get_s8_gemv(rows, cols, rs, fuse_norm=False, eps=1e-6, version=None):
952
+ version = KERNEL_VERSION if version is None else version
953
+ v = 2 if (version >= 2 and rs % 4 == 0 and cols % 4 == 0 and rows % RPSG == 0) else 1
954
+ key = ("s8", rows, cols, rs, fuse_norm, round(eps, 12), v)
955
+ if key in _CACHE:
956
+ return _CACHE[key]
957
+ inputs = ["W", "scale", "x"]
958
+ if fuse_norm:
959
+ inputs.append("nw")
960
+ src = (_s8_source_v2 if v == 2 else _s8_source)(rows, cols, rs, SG_PER_TG, fuse_norm, eps)
961
+ assert src is not None, "cols must be multiple of 4"
962
+ name = "s8gemv%d_r%d_c%d_n%d" % (v, rows, cols, fuse_norm)
963
+ k = mx.fast.metal_kernel(name=name, input_names=inputs, output_names=["out"],
964
+ source=src, header=_HEADER)
965
+ _CACHE[key] = (k, inputs, v)
966
+ return _CACHE[key]
967
+
968
+
969
+ def _grid_for(rows, rpsg=1):
970
+ nsg = (rows + rpsg - 1) // rpsg # one simdgroup per rpsg rows
971
+ ntg = (nsg + SG_PER_TG - 1) // SG_PER_TG
972
+ return (ntg * SG_PER_TG * 32, 1, 1), (SG_PER_TG * 32, 1, 1)
973
+
974
+
975
+ def trit_gemv(planes, slo, shi, x, bias=None, norm_w=None, resid=None, glu=False,
976
+ out_rows=None, eps=1e-6, stream=None):
977
+ """planes = (bp, bn, br) mx.uint8 [rows, rs]. x fp32. Returns fp32 [out_rows]."""
978
+ bp, bn, br = planes
979
+ rows, rs = bp.shape
980
+ out_rows = out_rows if out_rows is not None else rows
981
+ cols = x.shape[0] if not glu else x.shape[0] // 2
982
+ k, names, version = get_trit_gemv(rows, cols, rs, bias is not None, norm_w is not None,
983
+ resid is not None, glu, eps)
984
+ vals = {"bp": bp, "bn": bn, "br": br, "slo": slo, "shi": shi, "x": x,
985
+ "bias": bias, "nw": norm_w, "resid": resid}
986
+ grid, tg = _grid_for(out_rows, 8 if version >= 5 else (RPSG if version >= 3 else 1))
987
+ return k(inputs=[vals[n] for n in names], grid=grid, threadgroup=tg,
988
+ output_shapes=[(out_rows,)], output_dtypes=[mx.float32], stream=stream)[0]
989
+
990
+
991
+ def s8_gemv(W, scale, x, norm_w=None, eps=1e-6, stream=None):
992
+ rows, rs = W.shape
993
+ cols = x.shape[0]
994
+ k, names, v = get_s8_gemv(rows, cols, rs, norm_w is not None, eps)
995
+ vals = {"W": W, "scale": scale, "x": x, "nw": norm_w}
996
+ grid, tg = _grid_for(rows, RPSG if v == 2 else 1)
997
+ return k(inputs=[vals[n] for n in names], grid=grid, threadgroup=tg,
998
+ output_shapes=[(rows,)], output_dtypes=[mx.float32], stream=stream)[0]
999
+
1000
+
1001
+ # ---------------- fused rope + KV cache append ----------------
1002
+
1003
+ def get_rope_append(n_heads, n_kv, hd, maxseq):
1004
+ """One dispatch: rope q (n_heads*hd) and k (n_kv*hd) from the fused qkv
1005
+ vector, write roped k and raw v in-place into the preallocated caches at
1006
+ position pos (device scalar), output roped q. NeoX rotate-half, full hd.
1007
+ kc/vc passed as inputs and written in place (stream-serial ordering with
1008
+ the consumer sdpa; correctness enforced by the e2e gate)."""
1009
+ key = ("rope_append", n_heads, n_kv, hd, maxseq)
1010
+ if key in _CACHE:
1011
+ return _CACHE[key]
1012
+ qd, kd = n_heads * hd, n_kv * hd
1013
+ h2 = hd // 2
1014
+ src = f"""
1015
+ uint i = thread_position_in_grid.x; // one thread per q/k element
1016
+ uint pos = (uint)posbuf[0];
1017
+ device float* kc_w = (device float*)kc;
1018
+ device float* vc_w = (device float*)vc;
1019
+ if (i == 0u) {{ ((device float*)mask)[pos] = 0.0f; }}
1020
+ if (i < {qd}u + {kd}u) {{
1021
+ uint hidx = i / {hd}u, d = i % {hd}u;
1022
+ float c = ct[pos * {h2}u + (d % {h2}u)];
1023
+ float sn = st[pos * {h2}u + (d % {h2}u)];
1024
+ float v = qkv[i];
1025
+ float vrot = (d < {h2}u) ? -qkv[hidx * {hd}u + d + {h2}u]
1026
+ : qkv[hidx * {hd}u + d - {h2}u];
1027
+ float r = metal::fma(v, c, vrot * sn);
1028
+ if (i < {qd}u) {{
1029
+ qout[i] = r;
1030
+ }} else {{
1031
+ uint kh = (i - {qd}u) / {hd}u;
1032
+ kc_w[(ulong)(kh * {maxseq}u + pos) * {hd}u + d] = r;
1033
+ }}
1034
+ }} else if (i < {qd}u + 2u * {kd}u) {{
1035
+ uint j = i - {qd}u - {kd}u;
1036
+ uint vh = j / {hd}u, d = j % {hd}u;
1037
+ vc_w[(ulong)(vh * {maxseq}u + pos) * {hd}u + d] = qkv[{qd}u + {kd}u + j];
1038
+ }}
1039
+ """
1040
+ k = mx.fast.metal_kernel(name="rope_append_%d_%d_%d" % (n_heads, n_kv, hd),
1041
+ input_names=["qkv", "kc", "vc", "ct", "st", "posbuf", "mask"],
1042
+ output_names=["qout"], source=src)
1043
+ _CACHE[key] = k
1044
+ return k
1045
+
1046
+
1047
+ def rope_append(qkv, kc, vc, ct, st, posbuf, mask, n_heads, n_kv, hd, maxseq, stream=None):
1048
+ k = get_rope_append(n_heads, n_kv, hd, maxseq)
1049
+ n = (n_heads + 2 * n_kv) * hd
1050
+ nthr = ((n + 255) // 256) * 256
1051
+ return k(inputs=[qkv, kc, vc, ct, st, posbuf, mask], grid=(nthr, 1, 1),
1052
+ threadgroup=(256, 1, 1),
1053
+ output_shapes=[(n_heads * hd,)], output_dtypes=[mx.float32], stream=stream)[0]
mlx/generation_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token_id": 151643,
3
+ "do_sample": false,
4
+ "eos_token_id": [
5
+ 151645,
6
+ 151643
7
+ ],
8
+ "pad_token_id": 151643,
9
+ "transformers_version": "4.51.0"
10
+ }
mlx/merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
mlx/receipts/bench_0p6b_m5.json ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mode": "bench",
3
+ "model": "neutrino-0p6b_v4_base.bin",
4
+ "prompt_ids": [
5
+ 785,
6
+ 1376,
7
+ 4522,
8
+ 4815,
9
+ 71617,
10
+ 658,
11
+ 29728,
12
+ 14155,
13
+ 374
14
+ ],
15
+ "gen_tokens": 512,
16
+ "runs": [
17
+ {
18
+ "run": 0,
19
+ "tokens": 512,
20
+ "seconds": 2.536120083008427,
21
+ "tok_s": 201.88318503934923
22
+ },
23
+ {
24
+ "run": 1,
25
+ "tokens": 512,
26
+ "seconds": 2.596512083997368,
27
+ "tok_s": 197.1876053092611
28
+ },
29
+ {
30
+ "run": 2,
31
+ "tokens": 512,
32
+ "seconds": 2.534324457999901,
33
+ "tok_s": 202.02622374724365
34
+ },
35
+ {
36
+ "run": 3,
37
+ "tokens": 512,
38
+ "seconds": 2.5451969589921646,
39
+ "tok_s": 201.16321379023626
40
+ },
41
+ {
42
+ "run": 4,
43
+ "tokens": 512,
44
+ "seconds": 2.5724296660046093,
45
+ "tok_s": 199.03362442372122
46
+ }
47
+ ],
48
+ "median_tok_s": 201.16321379023626,
49
+ "peak_mem_gib": 0.5260553508996964,
50
+ "utc": "2026-07-25T09:39:14Z"
51
+ }
mlx/receipts/cachegate_0p6b.json ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cache": "hf_greedy_cache_86565bf6.json",
3
+ "container_md5": "86565bf6183e8031943aac9fd9771385",
4
+ "prompts": [
5
+ {
6
+ "prompt": [
7
+ 1,
8
+ 791,
9
+ 1283,
10
+ 315,
11
+ 220,
12
+ 17,
13
+ 42,
14
+ 99
15
+ ],
16
+ "steps": 128,
17
+ "flips": 0,
18
+ "flip_detail": [],
19
+ "pass": true
20
+ },
21
+ {
22
+ "prompt": [
23
+ 1,
24
+ 9707,
25
+ 374,
26
+ 264,
27
+ 1296,
28
+ 13,
29
+ 28,
30
+ 7
31
+ ],
32
+ "steps": 128,
33
+ "flips": 0,
34
+ "flip_detail": [],
35
+ "pass": true
36
+ },
37
+ {
38
+ "prompt": [
39
+ 1,
40
+ 2610,
41
+ 525,
42
+ 498,
43
+ 1091,
44
+ 11,
45
+ 16,
46
+ 3
47
+ ],
48
+ "steps": 128,
49
+ "flips": 0,
50
+ "flip_detail": [],
51
+ "pass": true
52
+ }
53
+ ],
54
+ "tie_tol": 0.04,
55
+ "max_flips_per_prompt": 8,
56
+ "total_flips": 0,
57
+ "worst_gap": 0.0,
58
+ "ok": true,
59
+ "mode": "cachegate",
60
+ "model": "neutrino-0p6b_v4_base.bin"
61
+ }
mlx/receipts/kernel_version_sweep_0p6b.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "receipt": "TRIT_METAL_V kernel-version sweep, 0.6B container 91c0b26d, 512-token greedy x3, M5 MacBook 16GB (concurrent 2.5MiB/s download noted)",
3
+ "utc": "2026-07-25T09:52:00Z",
4
+ "medians_tok_s": {
5
+ "v7": 185.6,
6
+ "v6": 120.4,
7
+ "v5": 99.8,
8
+ "v2": 105.2
9
+ },
10
+ "note": "v7 = default; official quiet-machine v7 receipt is bench_0p6b_m5.json (201.2)"
11
+ }
mlx/receipts/pathgate_0p6b.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "n": 32,
3
+ "identical": true,
4
+ "first_divergence": null,
5
+ "fused": [
6
+ 429,
7
+ 807,
8
+ 646,
9
+ 387,
10
+ 1483,
11
+ 311,
12
+ 1614,
13
+ 6351,
14
+ 5942,
15
+ 11,
16
+ 1741,
17
+ 438,
18
+ 279,
19
+ 3738,
20
+ 8109,
21
+ 11,
22
+ 323,
23
+ 807,
24
+ 646,
25
+ 387,
26
+ 1483,
27
+ 311,
28
+ 11625,
29
+ 5322,
30
+ 304,
31
+ 5257,
32
+ 5043,
33
+ 13,
34
+ 576,
35
+ 1376,
36
+ 4522,
37
+ 4815
38
+ ],
39
+ "mxops": [
40
+ 429,
41
+ 807,
42
+ 646,
43
+ 387,
44
+ 1483,
45
+ 311,
46
+ 1614,
47
+ 6351,
48
+ 5942,
49
+ 11,
50
+ 1741,
51
+ 438,
52
+ 279,
53
+ 3738,
54
+ 8109,
55
+ 11,
56
+ 323,
57
+ 807,
58
+ 646,
59
+ 387,
60
+ 1483,
61
+ 311,
62
+ 11625,
63
+ 5322,
64
+ 304,
65
+ 5257,
66
+ 5043,
67
+ 13,
68
+ 576,
69
+ 1376,
70
+ 4522,
71
+ 4815
72
+ ],
73
+ "mode": "pathgate",
74
+ "model": "neutrino-0p6b_v4_base.bin"
75
+ }
mlx/receipts/refgate_0p6b.json ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "ref": "neutrino_ref.npz",
3
+ "meta": {
4
+ "tag": "neutrino",
5
+ "donor": "Qwen/Qwen3-0.6B",
6
+ "dtype": "float32",
7
+ "attn": "eager",
8
+ "prompt": "The key idea behind ternary neural networks is",
9
+ "made": "2026-07-25T05:45:54Z"
10
+ },
11
+ "tf": [
12
+ {
13
+ "pos": 0,
14
+ "rel": 1.420795504981901e-06,
15
+ "cos": 0.9999999999996864,
16
+ "argmax_match": true
17
+ },
18
+ {
19
+ "pos": 1,
20
+ "rel": 1.4544835094150464e-06,
21
+ "cos": 0.9999999999997129,
22
+ "argmax_match": true
23
+ },
24
+ {
25
+ "pos": 2,
26
+ "rel": 1.245608417973328e-06,
27
+ "cos": 0.9999999999997389,
28
+ "argmax_match": true
29
+ },
30
+ {
31
+ "pos": 3,
32
+ "rel": 1.2583815360193984e-06,
33
+ "cos": 0.999999999999599,
34
+ "argmax_match": true
35
+ },
36
+ {
37
+ "pos": 4,
38
+ "rel": 8.871204307383681e-07,
39
+ "cos": 0.9999999999997197,
40
+ "argmax_match": true
41
+ },
42
+ {
43
+ "pos": 5,
44
+ "rel": 1.742981119610197e-06,
45
+ "cos": 0.999999999999722,
46
+ "argmax_match": true
47
+ },
48
+ {
49
+ "pos": 6,
50
+ "rel": 1.5824395401174085e-06,
51
+ "cos": 0.999999999999796,
52
+ "argmax_match": true
53
+ },
54
+ {
55
+ "pos": 7,
56
+ "rel": 1.167621618981324e-06,
57
+ "cos": 0.9999999999997883,
58
+ "argmax_match": true
59
+ },
60
+ {
61
+ "pos": 8,
62
+ "rel": 1.386377853532605e-06,
63
+ "cos": 0.99999999999965,
64
+ "argmax_match": true
65
+ }
66
+ ],
67
+ "greedy_flips": [],
68
+ "tie_tol": 0.04,
69
+ "max_flips": 8,
70
+ "tf_summary": {
71
+ "rel_max": 1.742981119610197e-06,
72
+ "rel_mean": 1.3495343923743974e-06,
73
+ "argmax": "9/9",
74
+ "near_tie": 0
75
+ },
76
+ "flips": 0,
77
+ "worst_gap": 0.0,
78
+ "ok": true,
79
+ "mlx_greedy": [
80
+ 429,
81
+ 807,
82
+ 646,
83
+ 387,
84
+ 1483,
85
+ 311,
86
+ 1614,
87
+ 6351,
88
+ 5942,
89
+ 11,
90
+ 1741,
91
+ 438,
92
+ 279,
93
+ 3738,
94
+ 8109,
95
+ 11,
96
+ 323,
97
+ 807,
98
+ 646,
99
+ 387,
100
+ 1483,
101
+ 311,
102
+ 11625,
103
+ 5322
104
+ ],
105
+ "ref_greedy": [
106
+ 429,
107
+ 807,
108
+ 646,
109
+ 387,
110
+ 1483,
111
+ 311,
112
+ 1614,
113
+ 6351,
114
+ 5942,
115
+ 11,
116
+ 1741,
117
+ 438,
118
+ 279,
119
+ 3738,
120
+ 8109,
121
+ 11,
122
+ 323,
123
+ 807,
124
+ 646,
125
+ 387,
126
+ 1483,
127
+ 311,
128
+ 11625,
129
+ 5322
130
+ ],
131
+ "mode": "refgate",
132
+ "model": "neutrino-0p6b_v4_base.bin"
133
+ }
mlx/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # fermion_mlx runtime (Apple silicon, macOS 14+)
2
+ mlx>=0.31.2
3
+ numpy>=1.26
4
+ # tokenizer + chat template (generation CLI modes only; gates/bench can run
5
+ # with --no-tokenizer)
6
+ transformers>=4.53
mlx/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4
3
+ size 11422654
mlx/tokenizer_config.json ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_bos_token": false,
3
+ "add_prefix_space": false,
4
+ "added_tokens_decoder": {
5
+ "151643": {
6
+ "content": "<|endoftext|>",
7
+ "lstrip": false,
8
+ "normalized": false,
9
+ "rstrip": false,
10
+ "single_word": false,
11
+ "special": true
12
+ },
13
+ "151644": {
14
+ "content": "<|im_start|>",
15
+ "lstrip": false,
16
+ "normalized": false,
17
+ "rstrip": false,
18
+ "single_word": false,
19
+ "special": true
20
+ },
21
+ "151645": {
22
+ "content": "<|im_end|>",
23
+ "lstrip": false,
24
+ "normalized": false,
25
+ "rstrip": false,
26
+ "single_word": false,
27
+ "special": true
28
+ },
29
+ "151646": {
30
+ "content": "<|object_ref_start|>",
31
+ "lstrip": false,
32
+ "normalized": false,
33
+ "rstrip": false,
34
+ "single_word": false,
35
+ "special": true
36
+ },
37
+ "151647": {
38
+ "content": "<|object_ref_end|>",
39
+ "lstrip": false,
40
+ "normalized": false,
41
+ "rstrip": false,
42
+ "single_word": false,
43
+ "special": true
44
+ },
45
+ "151648": {
46
+ "content": "<|box_start|>",
47
+ "lstrip": false,
48
+ "normalized": false,
49
+ "rstrip": false,
50
+ "single_word": false,
51
+ "special": true
52
+ },
53
+ "151649": {
54
+ "content": "<|box_end|>",
55
+ "lstrip": false,
56
+ "normalized": false,
57
+ "rstrip": false,
58
+ "single_word": false,
59
+ "special": true
60
+ },
61
+ "151650": {
62
+ "content": "<|quad_start|>",
63
+ "lstrip": false,
64
+ "normalized": false,
65
+ "rstrip": false,
66
+ "single_word": false,
67
+ "special": true
68
+ },
69
+ "151651": {
70
+ "content": "<|quad_end|>",
71
+ "lstrip": false,
72
+ "normalized": false,
73
+ "rstrip": false,
74
+ "single_word": false,
75
+ "special": true
76
+ },
77
+ "151652": {
78
+ "content": "<|vision_start|>",
79
+ "lstrip": false,
80
+ "normalized": false,
81
+ "rstrip": false,
82
+ "single_word": false,
83
+ "special": true
84
+ },
85
+ "151653": {
86
+ "content": "<|vision_end|>",
87
+ "lstrip": false,
88
+ "normalized": false,
89
+ "rstrip": false,
90
+ "single_word": false,
91
+ "special": true
92
+ },
93
+ "151654": {
94
+ "content": "<|vision_pad|>",
95
+ "lstrip": false,
96
+ "normalized": false,
97
+ "rstrip": false,
98
+ "single_word": false,
99
+ "special": true
100
+ },
101
+ "151655": {
102
+ "content": "<|image_pad|>",
103
+ "lstrip": false,
104
+ "normalized": false,
105
+ "rstrip": false,
106
+ "single_word": false,
107
+ "special": true
108
+ },
109
+ "151656": {
110
+ "content": "<|video_pad|>",
111
+ "lstrip": false,
112
+ "normalized": false,
113
+ "rstrip": false,
114
+ "single_word": false,
115
+ "special": true
116
+ },
117
+ "151657": {
118
+ "content": "<tool_call>",
119
+ "lstrip": false,
120
+ "normalized": false,
121
+ "rstrip": false,
122
+ "single_word": false,
123
+ "special": false
124
+ },
125
+ "151658": {
126
+ "content": "</tool_call>",
127
+ "lstrip": false,
128
+ "normalized": false,
129
+ "rstrip": false,
130
+ "single_word": false,
131
+ "special": false
132
+ },
133
+ "151659": {
134
+ "content": "<|fim_prefix|>",
135
+ "lstrip": false,
136
+ "normalized": false,
137
+ "rstrip": false,
138
+ "single_word": false,
139
+ "special": false
140
+ },
141
+ "151660": {
142
+ "content": "<|fim_middle|>",
143
+ "lstrip": false,
144
+ "normalized": false,
145
+ "rstrip": false,
146
+ "single_word": false,
147
+ "special": false
148
+ },
149
+ "151661": {
150
+ "content": "<|fim_suffix|>",
151
+ "lstrip": false,
152
+ "normalized": false,
153
+ "rstrip": false,
154
+ "single_word": false,
155
+ "special": false
156
+ },
157
+ "151662": {
158
+ "content": "<|fim_pad|>",
159
+ "lstrip": false,
160
+ "normalized": false,
161
+ "rstrip": false,
162
+ "single_word": false,
163
+ "special": false
164
+ },
165
+ "151663": {
166
+ "content": "<|repo_name|>",
167
+ "lstrip": false,
168
+ "normalized": false,
169
+ "rstrip": false,
170
+ "single_word": false,
171
+ "special": false
172
+ },
173
+ "151664": {
174
+ "content": "<|file_sep|>",
175
+ "lstrip": false,
176
+ "normalized": false,
177
+ "rstrip": false,
178
+ "single_word": false,
179
+ "special": false
180
+ },
181
+ "151665": {
182
+ "content": "<tool_response>",
183
+ "lstrip": false,
184
+ "normalized": false,
185
+ "rstrip": false,
186
+ "single_word": false,
187
+ "special": false
188
+ },
189
+ "151666": {
190
+ "content": "</tool_response>",
191
+ "lstrip": false,
192
+ "normalized": false,
193
+ "rstrip": false,
194
+ "single_word": false,
195
+ "special": false
196
+ },
197
+ "151667": {
198
+ "content": "<think>",
199
+ "lstrip": false,
200
+ "normalized": false,
201
+ "rstrip": false,
202
+ "single_word": false,
203
+ "special": false
204
+ },
205
+ "151668": {
206
+ "content": "</think>",
207
+ "lstrip": false,
208
+ "normalized": false,
209
+ "rstrip": false,
210
+ "single_word": false,
211
+ "special": false
212
+ }
213
+ },
214
+ "additional_special_tokens": [
215
+ "<|im_start|>",
216
+ "<|im_end|>",
217
+ "<|object_ref_start|>",
218
+ "<|object_ref_end|>",
219
+ "<|box_start|>",
220
+ "<|box_end|>",
221
+ "<|quad_start|>",
222
+ "<|quad_end|>",
223
+ "<|vision_start|>",
224
+ "<|vision_end|>",
225
+ "<|vision_pad|>",
226
+ "<|image_pad|>",
227
+ "<|video_pad|>"
228
+ ],
229
+ "bos_token": null,
230
+ "chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].role == 'system' %}\n {{- messages[0].content + '\\n\\n' }}\n {%- endif %}\n {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}\n {%- set index = (messages|length - 1) - loop.index0 %}\n {%- if ns.multi_step_tool and message.role == \"user\" and message.content is string and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}\n {%- set ns.multi_step_tool = false %}\n {%- set ns.last_query_index = index %}\n {%- endif %}\n{%- endfor %}\n{%- for message in messages %}\n {%- if message.content is string %}\n {%- set content = message.content %}\n {%- else %}\n {%- set content = '' %}\n {%- endif %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n {{- '<|im_start|>' + message.role + '\\n' + content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set reasoning_content = '' %}\n {%- if message.reasoning_content is string %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if '</think>' in content %}\n {%- set reasoning_content = content.split('</think>')[0].rstrip('\\n').split('<think>')[-1].lstrip('\\n') %}\n {%- set content = content.split('</think>')[-1].lstrip('\\n') %}\n {%- endif %}\n {%- endif %}\n {%- if loop.index0 > ns.last_query_index %}\n {%- if loop.last or (not loop.last and reasoning_content) %}\n {{- '<|im_start|>' + message.role + '\\n<think>\\n' + reasoning_content.strip('\\n') + '\\n</think>\\n\\n' + content.lstrip('\\n') }}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- if message.tool_calls %}\n {%- for tool_call in message.tool_calls %}\n {%- if (loop.first and content) or (not loop.first) %}\n {{- '\\n' }}\n {%- endif %}\n {%- if tool_call.function %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {%- if tool_call.arguments is string %}\n {{- tool_call.arguments }}\n {%- else %}\n {{- tool_call.arguments | tojson }}\n {%- endif %}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n {%- if enable_thinking is defined and enable_thinking is false %}\n {{- '<think>\\n\\n</think>\\n\\n' }}\n {%- endif %}\n{%- endif %}",
231
+ "clean_up_tokenization_spaces": false,
232
+ "eos_token": "<|im_end|>",
233
+ "errors": "replace",
234
+ "model_max_length": 131072,
235
+ "pad_token": "<|endoftext|>",
236
+ "split_special_tokens": false,
237
+ "tokenizer_class": "Qwen2Tokenizer",
238
+ "unk_token": null
239
+ }
mlx/vocab.json ADDED
The diff for this file is too large to render. See raw diff
 
neutrino-0.6b_v4.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:91c0b26d105710e755ef1ec1b5607c00da5d5cbd048fd5584135fb6fc1e19399
3
+ size 327719836
neutrino-0.6b_v4.tv4z ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:132d9ab758b0b1a5b9b6638e55647462dbdb4ec8345720cc98c3d4cf669e780c
3
+ size 237703560
receipts/hf_greedy_cache_86565bf6.json ADDED
@@ -0,0 +1,433 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "container_md5": "86565bf6183e8031943aac9fd9771385",
3
+ "donor": "Qwen/Qwen3-0.6B@c1899de289a04d12100db370d81485cdf75e47ca",
4
+ "dtype": "float16",
5
+ "prompts": [
6
+ [
7
+ 1,
8
+ 791,
9
+ 1283,
10
+ 315,
11
+ 220,
12
+ 17,
13
+ 42,
14
+ 99
15
+ ],
16
+ [
17
+ 1,
18
+ 9707,
19
+ 374,
20
+ 264,
21
+ 1296,
22
+ 13,
23
+ 28,
24
+ 7
25
+ ],
26
+ [
27
+ 1,
28
+ 2610,
29
+ 525,
30
+ 498,
31
+ 1091,
32
+ 11,
33
+ 16,
34
+ 3
35
+ ]
36
+ ],
37
+ "steps": 128,
38
+ "tokens": [
39
+ [
40
+ 1,
41
+ 374,
42
+ 264,
43
+ 4185,
44
+ 17133,
45
+ 1483,
46
+ 304,
47
+ 279,
48
+ 2266,
49
+ 315,
50
+ 279,
51
+ 1809,
52
+ 315,
53
+ 32719,
54
+ 13,
55
+ 1084,
56
+ 19257,
57
+ 311,
58
+ 279,
59
+ 2309,
60
+ 315,
61
+ 279,
62
+ 2781,
63
+ 879,
64
+ 374,
65
+ 304,
66
+ 279,
67
+ 1156,
68
+ 2309,
69
+ 11,
70
+ 323,
71
+ 279,
72
+ 2309,
73
+ 315,
74
+ 279,
75
+ 14704,
76
+ 879,
77
+ 374,
78
+ 304,
79
+ 279,
80
+ 2086,
81
+ 2309,
82
+ 13,
83
+ 576,
84
+ 1809,
85
+ 374,
86
+ 6342,
87
+ 553,
88
+ 24047,
89
+ 9666,
90
+ 389,
91
+ 264,
92
+ 4479,
93
+ 11,
94
+ 323,
95
+ 279,
96
+ 2781,
97
+ 879,
98
+ 374,
99
+ 304,
100
+ 279,
101
+ 1156,
102
+ 2309,
103
+ 374,
104
+ 279,
105
+ 13632,
106
+ 13,
107
+ 576,
108
+ 1809,
109
+ 374,
110
+ 6342,
111
+ 553,
112
+ 24047,
113
+ 9666,
114
+ 389,
115
+ 264,
116
+ 4479,
117
+ 11,
118
+ 323,
119
+ 279,
120
+ 2781,
121
+ 879,
122
+ 374,
123
+ 304,
124
+ 279,
125
+ 2086,
126
+ 2309,
127
+ 374,
128
+ 279,
129
+ 58891,
130
+ 13,
131
+ 576,
132
+ 1809,
133
+ 374,
134
+ 6342,
135
+ 553,
136
+ 24047,
137
+ 9666,
138
+ 389,
139
+ 264,
140
+ 4479,
141
+ 11,
142
+ 323,
143
+ 279,
144
+ 2781,
145
+ 879,
146
+ 374,
147
+ 304,
148
+ 279,
149
+ 1156,
150
+ 2309,
151
+ 374,
152
+ 279,
153
+ 13632,
154
+ 13,
155
+ 576,
156
+ 1809,
157
+ 374,
158
+ 6342,
159
+ 553,
160
+ 24047,
161
+ 9666,
162
+ 389,
163
+ 264,
164
+ 4479,
165
+ 11,
166
+ 323,
167
+ 279
168
+ ],
169
+ [
170
+ 16,
171
+ 11,
172
+ 17,
173
+ 11,
174
+ 18,
175
+ 11,
176
+ 19,
177
+ 11,
178
+ 20,
179
+ 11,
180
+ 21,
181
+ 11,
182
+ 22,
183
+ 11,
184
+ 23,
185
+ 11,
186
+ 24,
187
+ 11,
188
+ 16,
189
+ 15,
190
+ 11,
191
+ 16,
192
+ 16,
193
+ 11,
194
+ 16,
195
+ 17,
196
+ 11,
197
+ 16,
198
+ 18,
199
+ 11,
200
+ 16,
201
+ 19,
202
+ 11,
203
+ 16,
204
+ 20,
205
+ 11,
206
+ 16,
207
+ 21,
208
+ 11,
209
+ 16,
210
+ 22,
211
+ 11,
212
+ 16,
213
+ 23,
214
+ 11,
215
+ 16,
216
+ 24,
217
+ 11,
218
+ 17,
219
+ 15,
220
+ 11,
221
+ 17,
222
+ 16,
223
+ 11,
224
+ 17,
225
+ 17,
226
+ 11,
227
+ 17,
228
+ 18,
229
+ 11,
230
+ 17,
231
+ 19,
232
+ 11,
233
+ 17,
234
+ 20,
235
+ 11,
236
+ 17,
237
+ 21,
238
+ 11,
239
+ 17,
240
+ 22,
241
+ 11,
242
+ 17,
243
+ 23,
244
+ 11,
245
+ 17,
246
+ 24,
247
+ 11,
248
+ 18,
249
+ 15,
250
+ 11,
251
+ 18,
252
+ 16,
253
+ 11,
254
+ 18,
255
+ 17,
256
+ 11,
257
+ 18,
258
+ 18,
259
+ 11,
260
+ 18,
261
+ 19,
262
+ 11,
263
+ 18,
264
+ 20,
265
+ 11,
266
+ 18,
267
+ 21,
268
+ 11,
269
+ 18,
270
+ 22,
271
+ 11,
272
+ 18,
273
+ 23,
274
+ 11,
275
+ 18,
276
+ 24,
277
+ 11,
278
+ 19,
279
+ 15,
280
+ 11,
281
+ 19,
282
+ 16,
283
+ 11,
284
+ 19,
285
+ 17,
286
+ 11,
287
+ 19,
288
+ 18,
289
+ 11,
290
+ 19,
291
+ 19,
292
+ 11,
293
+ 19,
294
+ 20,
295
+ 11,
296
+ 19,
297
+ 21
298
+ ],
299
+ [
300
+ 323,
301
+ 279,
302
+ 1008,
303
+ 374,
304
+ 279,
305
+ 1852,
306
+ 438,
307
+ 279,
308
+ 1008,
309
+ 11,
310
+ 17,
311
+ 3,
312
+ 323,
313
+ 279,
314
+ 1008,
315
+ 374,
316
+ 279,
317
+ 1852,
318
+ 438,
319
+ 279,
320
+ 1008,
321
+ 11,
322
+ 18,
323
+ 3,
324
+ 323,
325
+ 279,
326
+ 1008,
327
+ 374,
328
+ 279,
329
+ 1852,
330
+ 438,
331
+ 279,
332
+ 1008,
333
+ 11,
334
+ 19,
335
+ 3,
336
+ 323,
337
+ 279,
338
+ 1008,
339
+ 374,
340
+ 279,
341
+ 1852,
342
+ 438,
343
+ 279,
344
+ 1008,
345
+ 11,
346
+ 20,
347
+ 3,
348
+ 323,
349
+ 279,
350
+ 1008,
351
+ 374,
352
+ 279,
353
+ 1852,
354
+ 438,
355
+ 279,
356
+ 1008,
357
+ 11,
358
+ 21,
359
+ 3,
360
+ 323,
361
+ 279,
362
+ 1008,
363
+ 374,
364
+ 279,
365
+ 1852,
366
+ 438,
367
+ 279,
368
+ 1008,
369
+ 11,
370
+ 22,
371
+ 3,
372
+ 323,
373
+ 279,
374
+ 1008,
375
+ 374,
376
+ 279,
377
+ 1852,
378
+ 438,
379
+ 279,
380
+ 1008,
381
+ 11,
382
+ 23,
383
+ 3,
384
+ 323,
385
+ 279,
386
+ 1008,
387
+ 374,
388
+ 279,
389
+ 1852,
390
+ 438,
391
+ 279,
392
+ 1008,
393
+ 11,
394
+ 24,
395
+ 3,
396
+ 323,
397
+ 279,
398
+ 1008,
399
+ 374,
400
+ 279,
401
+ 1852,
402
+ 438,
403
+ 279,
404
+ 1008,
405
+ 11,
406
+ 16,
407
+ 15,
408
+ 3,
409
+ 323,
410
+ 279,
411
+ 1008,
412
+ 374,
413
+ 279,
414
+ 1852,
415
+ 438,
416
+ 279,
417
+ 1008,
418
+ 11,
419
+ 16,
420
+ 16,
421
+ 3,
422
+ 323,
423
+ 279,
424
+ 1008,
425
+ 374,
426
+ 279,
427
+ 1852
428
+ ]
429
+ ],
430
+ "trajectory_sha256": "a5c3a24c7918558239d0ef958adb335d7a67df630b084b492735e6bc00c6c74b",
431
+ "generation_seconds": 22.1,
432
+ "venue": "modal cpu=8 (fp16 weights, greedy argmax)"
433
+ }
receipts/m5_bitgate_analysis.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "receipt": "M5 bit-gate divergence analysis (banked with the FAIL-as-measured re-receipt)",
3
+ "utc": "2026-07-25T06:38:06Z",
4
+ "container": {
5
+ "name": "neutrino-0p6b_v4_base.bin",
6
+ "sha256": "91c0b26d105710e755ef1ec1b5607c00da5d5cbd048fd5584135fb6fc1e19399",
7
+ "md5": "86565bf6183e8031943aac9fd9771385"
8
+ },
9
+ "raw_bit_gate": "221/384 token mismatches vs fresh HF-fp16 greedy cache (per m5_demo_rereceipt_p3.json)",
10
+ "structure": "ONLY 2 first-divergence argmax flips (prompt1@idx13, prompt3@idx1); all other mismatches are the greedy cascade after those flips; prompt2 exact 128/128",
11
+ "evidence": {
12
+ "hf_reference": "three variants agree (expand fp16/load fp16, expand fp16/load fp32, expand fp32/load fp32): prefers 32719 by +0.197..+0.203 (prompt1) and 279 by +0.020..+0.023 (prompt3)",
13
+ "kernel_exact": "teacher-forced exact-int8 logit dump: argmax 5994 by -0.456 (prompt1) and 400 by -0.197 (prompt3); decode picks == exact-kernel argmax, so NOT a nomination artifact",
14
+ "determinism": "mismatch pattern identical across runs and across threads 1/4/9",
15
+ "integrity": "double-export byte-determinism PASS, tv4z byte round-trip PASS, HF expand --self-check PASS (receipt_base.json)"
16
+ },
17
+ "conclusion": "trit_run is an int8-activation engine; the HF fp expansion is a different numeric implementation of the same weights. They disagree on ranking only where |top-2 margin| <~ 0.5 logits. The shipped state has a flat (high-entropy) logit landscape, so 2 such positions occur in 384 greedy steps; other banked states gated under the same protocol presented none (their gates passed 0/384). Kernel output is deterministic and self-consistent; container integrity is fully receipted. The bit gate FAIL is banked as measured with this analysis."
18
+ }