Misakachain commited on
Commit
bff7dc8
·
verified ·
1 Parent(s): 0985c38

Update PALW runtime to Qwen source 3866a25

Browse files

Tracked runtime code, documentation, and evidence from qwen36-migration commit 3866a25e82ef40adc5109e7949754e69eb083c9c. Local untracked model data is intentionally excluded.

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. config/qi35-calibration-prompts-v1.json +66 -0
  3. docs/adr-receipt-v3-node-ownership.md +109 -0
  4. docs/canonical-integer-compute-design.md +188 -0
  5. docs/canonical-integer-inc4b-fullmodel-design.md +114 -0
  6. docs/canonical-integer-qwen36-design.md +181 -0
  7. docs/evidence/canon_attn.cu +136 -0
  8. docs/evidence/canon_attn.py +188 -0
  9. docs/evidence/canon_attn.rs +187 -0
  10. docs/evidence/canon_attn_int.py +182 -0
  11. docs/evidence/canon_attn_norm.rs +233 -0
  12. docs/evidence/canon_attn_norm.swift +334 -0
  13. docs/evidence/canon_attn_sq.py +133 -0
  14. docs/evidence/canon_cuda.cu +75 -0
  15. docs/evidence/canon_dump.py +12 -0
  16. docs/evidence/canon_extract.py +73 -0
  17. docs/evidence/canon_extract_full.py +55 -0
  18. docs/evidence/canon_int.rs +126 -0
  19. docs/evidence/canon_layer.rs +83 -0
  20. docs/evidence/canon_layer_cuda.cu +89 -0
  21. docs/evidence/canon_layer_int.py +158 -0
  22. docs/evidence/canon_layer_metal.swift +94 -0
  23. docs/evidence/canon_metal.swift +93 -0
  24. docs/evidence/canon_model.cu +149 -0
  25. docs/evidence/canon_model.rs +188 -0
  26. docs/evidence/canon_model_hifi.py +175 -0
  27. docs/evidence/canon_model_int.py +169 -0
  28. docs/evidence/canon_ops.rs +97 -0
  29. docs/evidence/canon_ops_cuda.cu +93 -0
  30. docs/evidence/canon_ops_metal.swift +110 -0
  31. docs/evidence/canon_quant.rs +139 -0
  32. docs/evidence/canon_quant_full.py +115 -0
  33. docs/evidence/canonical-int-attention-4b2a-2026-07-19.md +77 -0
  34. docs/evidence/canonical-int-attention-4b2b-2026-07-19.md +53 -0
  35. docs/evidence/canonical-int-cross-isa-2026-07-19.md +61 -0
  36. docs/evidence/canonical-int-fulllayer-4b3-2026-07-20.md +52 -0
  37. docs/evidence/canonical-int-fullmodel-4b4-4b5-2026-07-20.md +56 -0
  38. docs/evidence/canonical-int-gpu-3backend-2026-07-19.md +58 -0
  39. docs/evidence/canonical-int-hifi-fullmodel-2026-07-20.md +58 -0
  40. docs/evidence/canonical-int-layer-4backend-2026-07-19.md +59 -0
  41. docs/evidence/canonical-int-metal-norm-2026-07-20.md +35 -0
  42. docs/evidence/canonical-int-ops-4backend-2026-07-19.md +62 -0
  43. docs/evidence/canonical-int-quantizer-2026-07-19.md +68 -0
  44. docs/evidence/canonical-int-qwen36-bench-2026-07-20.md +34 -0
  45. docs/evidence/canonical-int-qwen36-chat-receipt-2026-07-20.md +76 -0
  46. docs/evidence/canonical-int-qwen36-fullmodel-g3-2026-07-20.md +42 -0
  47. docs/evidence/canonical-int-qwen36-layer-g2-2026-07-20.md +64 -0
  48. docs/evidence/canonical-int-qwen36-metal-v1-2026-07-20.md +192 -0
  49. docs/evidence/canonical-int-qwen36-v3-pair-2026-07-20.md +76 -0
  50. docs/evidence/gguf_full_dump.py +69 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ docs/evidence/libqi35metal.dylib filter=lfs diff=lfs merge=lfs -text
37
+ docs/evidence/qi35_model filter=lfs diff=lfs merge=lfs -text
config/qi35-calibration-prompts-v1.json ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema": "misaka.qi35.calibration-corpus.v1",
3
+ "tokenizer_sha256": "87a7830d63fcf43bf241c3c5242e96e62dd3fdc29224ca26fed8ea333db72de4",
4
+ "prompts": [
5
+ {
6
+ "name": "hello_world",
7
+ "text": "Hello, world",
8
+ "ids": [9419, 11, 1814]
9
+ },
10
+ {
11
+ "name": "paris",
12
+ "text": "The capital of France is",
13
+ "ids": [760, 6511, 314, 9338, 369]
14
+ },
15
+ {
16
+ "name": "arithmetic",
17
+ "text": "2 + 2 =",
18
+ "ids": [17, 478, 220, 17, 283]
19
+ },
20
+ {
21
+ "name": "freezing",
22
+ "text": "Water freezes at",
23
+ "ids": [27336, 90162, 506]
24
+ },
25
+ {
26
+ "name": "python_comprehension",
27
+ "text": "In Python, a list comprehension",
28
+ "ids": [623, 12654, 11, 264, 1103, 59036]
29
+ },
30
+ {
31
+ "name": "japanese_capital",
32
+ "text": "\u65e5\u672c\u306e\u9996\u90fd\u306f",
33
+ "ids": [161607, 110161, 14876]
34
+ },
35
+ {
36
+ "name": "quick_fox",
37
+ "text": "The quick brown fox",
38
+ "ids": [760, 3841, 13477, 37550]
39
+ },
40
+ {
41
+ "name": "gravity",
42
+ "text": "Explain gravity in one sentence:",
43
+ "ids": [814, 20139, 22525, 303, 799, 11316, 25]
44
+ },
45
+ {
46
+ "name": "rust_ownership",
47
+ "text": "Rust ownership prevents",
48
+ "ids": [49, 575, 14834, 27028]
49
+ },
50
+ {
51
+ "name": "logic_cat",
52
+ "text": "If all cats are mammals and Luna is a cat, then Luna is",
53
+ "ids": [2592, 660, 18833, 513, 53702, 321, 48130, 369, 264, 7993, 11, 1179, 48130, 369]
54
+ },
55
+ {
56
+ "name": "fibonacci",
57
+ "text": "def fibonacci(n):",
58
+ "ids": [727, 73111, 1393, 1590]
59
+ },
60
+ {
61
+ "name": "french_translation",
62
+ "text": "Translate good morning to French:",
63
+ "ids": [26583, 1603, 6353, 310, 8323, 25]
64
+ }
65
+ ]
66
+ }
docs/adr-receipt-v3-node-ownership.md ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ADR — Receipt v3 統合: 規約所有権の凍結と一括破壊変更(node ↔ runtime)
2
+
3
+ **日付**: 2026-07-20 **状態**: Accepted(node 側設計者と合意) **適用**: runtime-palw の次期一括変更
4
+ **背景**: 最近の設計決定(PQ-only・Hash64・単一プール・supersession)は全て **node 側**に住んでおり、
5
+ runtime の現行レシート(final-v7 系)は「Q4_K_M 較正期の使い捨て」と割り切り済み。互換 shim なしの
6
+ **クリーンブレイク**で node 規約へ寄せる。「写した瞬間に不整合が固定される」を防ぐため、**写す前に**本
7
+ 所有権表を凍結する。
8
+
9
+ ---
10
+
11
+ ## 0. 所有権表(凍結)
12
+
13
+ | 規約 | 原本 | 帰結 |
14
+ |---|---|---|
15
+ | 署名方式・鍵体系 | **node**(ML-DSA-87 のみ) | runtime の Ed25519 経路は排除 |
16
+ | ハッシュ関数・幅・keying | **node**(keyed BLAKE2b-512 / Hash64) | 合意が再計算する全値が対象 |
17
+ | 射影・match 述語 | **node**(§8 exact-match 集合) | `runtime_class_id` は非合意 telemetry |
18
+ | 署名コンテキスト台帳 | **node** の `signature_domains.rs` | Receipt v3 の active context を core const と alignment test で固定 |
19
+ | Receipt v3 hash-domain / wire / 正準直列化 | **node** の `mil/palw/src/receipt_v3.rs` | fixed-width・宣言順・LE。runtime は golden vector へ byte 一致させる |
20
+ | 実行内容(trace 語彙・各 root の生成規則) | **runtime** | node 所有の Receipt v3 field へ格納し、署名対象 wire は変更しない |
21
+
22
+ **所有権の一本化が肝** — 署名コンテキスト registry と Receipt v3 の byte-hash domain 定数は用途が
23
+ 異なるためファイルは分かれるが、どちらも node が原本であり runtime は独自 wire 規約を持たない。
24
+ Receipt v3 context `misaka-palw-v3/receipt/mldsa87` は既に active registry 行であり、PENDING ではない。
25
+ runtime-local の trace/root domain は実行内容の内部規則として残るが、最終値は node-owned field へ入る。
26
+
27
+ ## 1. 衝突 1(署名)— ML-DSA-87、鍵は credential、Ed25519 は「証明可能に不在」へ
28
+
29
+ - consensus 経路(mint に効く receipt / leaf / vote)の署名は **すべて ML-DSA-87**。
30
+ - **署名鍵 = 登録 credential 鍵** を v1 とする。delegation(credential が署名したセッション鍵)は運用
31
+ 要求が出てから。導入する場合もセッション鍵は 87 のまま(パラメタセットを 2 つにしない)。
32
+ - runtime の Ed25519 は EVM の secp256k1 と同じ手筋: **feature-gate で「provably-Ed25519-free な
33
+ mint 経路バイナリ」**にする(悪い状態を表現不能に)。
34
+ - 既存 final-v7 系レシートは**移行せず破棄**(較正期の使い捨て、既定方針どおり)。
35
+
36
+ ## 2. 衝突 2(ハッシュ)— Hash64 keyed BLAKE2b-512 を全面採用
37
+
38
+ - 判定基準: 「**合意側の誰かが再計算する値か**」。該当: `output_commitment`、checkpoint/execution root、
39
+ route root、state root、schedule commitment — 端から端まで同一関数でなければ再計算照合が成立しない。
40
+ - Receipt v3 の projection/body/id/nullifier/output/pair は、node が定義する 6 個の byte-domain と
41
+ fixed-width・宣言順・LE preimage を使う。可変長 field の汎用 framing を Receipt v3 wire へ流用しない。
42
+ - runtime-local root の内部 domain は別管理とし、その出力を node-owned Receipt v3 field へ格納する。
43
+ - runtime 現行の Hash32 / SHA3 系列 domain-hash はすべて対象(receipt フィールド型は Hash32→**Hash64**)。
44
+ - BLAKE3 は **S0 実測で BLAKE2b が受領税目標(<1%)を割ると判明した場合にのみ**、宣言行付きの第二ハッシュ
45
+ として検討。既定は一本。
46
+
47
+ ## 3. 衝突 3(射影)— MatchProjectionV2 として作り直す
48
+
49
+ - V1 の `runtime_class_id` 一致要件をコピーすると「**異機種が bit 一致しても永久に mint されない**」=
50
+ 単一プール化(canonical-integer トラックの成果)の否定になる。
51
+ - 対応は原子性規則の runtime 側適用: **射影の再定義と diversity 述語の削除を同一統合で**。
52
+ - V2 の中身 = §8 exact-match 集合:
53
+ `compute_set_id / job_challenge / output_commitment / schedule root / execution root / route root /
54
+ state root / CU / token count / stop reason`
55
+ - `runtime_class_id` は **ImplementationTelemetry**(非合意)へ移動。
56
+ - **V1 の名前を再利用しない**(「名前だけ同じで中身をすり替えると未来の実装者が泣く」)。V2 として切る。
57
+
58
+ > 整合メモ: これは本リポジトリの canonical-integer 実証(実 attention/層が CPU×2 ISA + CUDA でバイト一致、
59
+ > [4b-2b](evidence/canonical-int-attention-4b2b-2026-07-19.md) / [4b-3](evidence/canonical-int-fulllayer-4b3-2026-07-20.md))
60
+ > と噛み合う: 異ベンダ bit 一致を mint 可能にするのは V2 射影があってこそ。
61
+
62
+ ## 4. Receipt v3 — 一括破壊変更(破壊は 1 回)
63
+
64
+ 上 3 ���は個別に入れず、予約済みの一括バンドルに吸収:
65
+ **ML-DSA envelope / Hash64 / leaf フィールド(compute_set_id・job_challenge・checkpoint root・state root・
66
+ stop reason 等)/ challenge 束縛 / checkpoint root**。
67
+
68
+ **runtime 側の実施順(完了済み)**:
69
+ 1. node Receipt v3 の署名 context と 6 byte-hash domain を原本として固定
70
+ 2. canonical body/projection を fixed-width・宣言順・LE、Hash64 keyed BLAKE2b-512 へ移行
71
+ 3. ML-DSA-87 化(Ed25519 を v3 経路で表現不能にする)
72
+ 4. MatchProjectionV2(+ `runtime_class_id` → ImplementationTelemetry)
73
+ 5. production encoder/worker/verifier を node golden と byte 一致させ、admission expectations を必須化
74
+
75
+ ## 5. 経済側の付記(node 側で対応、runtime は観測のみ)
76
+
77
+ - `pump_ev_negative`: esc(k) は credential 単位のため**回転 sybil カルテル**(min-bond credential 多数で
78
+ no-show を分散、全イベント k=0)が esc をほぼ回避 → コストは平坦モデルに退化し pump が黒字化し得る
79
+ (140k < 167k)。よって真の目標状態は「**平坦モデル単独で EV 負**」、esc は defense-in-depth に降格。
80
+ P₀ 再較正(1.3–1.5×+余裕)で到達可能。薄市場は N_STAT hold が π ごと凍結するので pump 不能。
81
+ - テスト pin は再較正完了と同時に `flat_cost < gain` → `flat_cost > gain` へ反転。
82
+ - `NoShowPenaltyDestination` に `ToRequester` を**設けない**型表現は維持(悪い状態を表現不能に)。
83
+
84
+ ## 帰結(runtime-palw への具体的影響リスト)
85
+
86
+ - `receipt.rs` / `protocol_v2.rs`: ComputeReceipt **v3**(Hash64 フィールド、新 leaf フィールド)。
87
+ Ed25519 envelope は feature `legacy-ed25519`(既定 OFF)。v1/v2 デコードは検証ツール専用に隔離。
88
+ - 全 domain-hash site(schedule chain / trace roots / nullifiers / commitments): keyed BLAKE2b-512 へ。
89
+ タグは node 台帳の行として登録し、runtime 内のタグ定数は台帳から生成 or 参照に一本化。
90
+ - `matcher.rs`: `MatchProjectionV2`(§8 集合)。`runtime_class_id` は `ImplementationTelemetry` 構造体へ。
91
+ - `mint.rs`: 署名方式ゲート(ML-DSA でないものは mint 経路で不適格)を追加。
92
+ - receipts/final-v7 は破棄対象(アーカイブとしてのみ保持、照合対象にしない)。
93
+ - [palw-node-integration-spec.md](palw-node-integration-spec.md) は本 ADR を上位とし、v1/Ed25519/V1 射影の
94
+ 記述は「現行(較正期)実装の記録」に降格(同文書冒頭に supersession banner)。
95
+
96
+ ## 実装状況(2026-07-20 更新)
97
+
98
+ - **node 原本実装済み**: `MISAKA.../mil/palw/src/receipt_v3.rs` が `ComputeReceiptV3` / projection /
99
+ output/nullifier/pair の fixed-width LE bytes と byte-domain、selected-chain expectations 付き検証を所有する。
100
+ fixture は `mil/palw/test-data/receipt_v3_golden_v1.json`。
101
+ - **runtime production 経路を整合済み**: `runtime-palw/src/receipt_v3.rs` と integer/testnet worker CLI は
102
+ node と同じ canonical bytes、Hash64、credential binding、execution nullifier、output commitment、
103
+ ML-DSA-87 context `misaka-palw-v3/receipt/mldsa87`(vk 2592B / sig 4627B)を使用する。node fixture の
104
+ body/digest/pair-id golden と一致し、selected-chain admission API は network/compute-set/challenge/slot/
105
+ epoch/registered credential の `ReceiptV3Expectations` を必須とする。self-consistency と low-level matcher は
106
+ admission verdict ではない。
107
+ - **残り(ネットワーク統合)**: submission transport/scheduler、node genesis network identity と job challenge、
108
+ provider credential registry 接続、global nullifier/lifecycle、receipt DA と監査・決済。暗号 backend の
109
+ 「次段配線」ではなく、これら外部ライフサイクルが公開・有価値 network の blocker である。
docs/canonical-integer-compute-design.md ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Canonical Integer Compute — 決定性クラス統合の設計(RTX20/40 を 1 クラスに)
2
+
3
+ **日付**: 2026-07-19 **目的**: 異なる GPU 世代(RTX 20 `sm_75` / RTX 40 `sm_89`)・異なるベンダを、**1 つの
4
+ determinism / pool クラス**に統合する。**手段**: 浮動小数点をやめ、**整数(固定小数点)で計算**し、`int32`
5
+ 累積の結合律を使って hardware 非依存のビット同一性を「祈り」ではなく **設計上の性質**として保証する。
6
+
7
+ > 本設計は [spec-snapshot §6.1(QW36-int)](spec-snapshot-qwen36-2026-07-19.md) の「整数意味論」章を、
8
+ > クラス統合という目的から具体化したもの。**現状は未実装**(設計 + 基礎 primitive の実機実証まで)。
9
+
10
+ ---
11
+
12
+ ## 0. 結論(先に)
13
+
14
+ - fp のままでは RTX20/40 を同一クラスにできる保証はない。実測でも candle 0.9.2→0.11 の bump だけで
15
+ **Ada の token commitment が flip**(`f151cd16…`→`60a662e2…`)した — 低マージンの決定手が存在し、kernel
16
+ 差で argmax が動く([COMMITMENTS 参照](../README.md))。Metal は偶然保持しただけ。
17
+ - **整数化すれば、順序・kernel・世代・ベンダに依らずビット同一**になる。`int32` 加算は 2³² を法とする
18
+ 可換環で、wrapping 込みで結合律・交換律が厳密に成立するため、reduction 順序を固定する必要すらない。
19
+ - 本セッションで基礎 primitive を実装し、**Apple aarch64 と NVIDIA host x86_64 で canonical 整数出力が
20
+ バイト同一**(commitment `743652581b0b9725`)、fp は順序で割れることを実機確認した(§4)。
21
+ - 追い風: **int8 tensor core は Turing(`sm_75`)/Ampere/Ada/Hopper に共通**(bf16 と違い Turing にもある)
22
+ ので、整数パスなら RTX20〜40+ を 1 クラスに束ねられる筋がある。
23
+
24
+ ---
25
+
26
+ ## 1. なぜ fp では世代を跨げないか
27
+
28
+ - **根本原因**: IEEE-754 加算は**非結合** `(a+b)+c ≠ a+(b+c)`。GPU の matmul は tensor core の MMA、warp
29
+ reduction、タイル分割で**加算順序がカーネル/世代ごとに異なる** → 丸めが変わる → ビットが変わる。
30
+ - **実測の裏付け(本リポジトリ)**: candle を 0.9.2→0.11 に上げただけで、**同じ Ada カード**上で CUDA の
31
+ token `vector_commitment` が変化した。これは job set 内に「top-1 マージンが極小の決定手」が存在する
32
+ 直接証拠。世代違い(Turing vs Ada、tensor core 世代が違う)ではさらに割れやすい。
33
+ - argmax 自体は頑健(Apple M1 Max と M4 Pro は logits が違っても token 一致)。しかし**低マージン手が
34
+ 1 つでもあれば**、その手で世代差が argmax を反転させ、token 列全体のバイト一致が壊れる。
35
+
36
+ ---
37
+
38
+ ## 2. 核心定理 — 整数累積は hardware 非依存
39
+
40
+ **定理.** `int32` 上の加算は、二の補数 wrapping を含めて結合的かつ可換(2³² を法とする可換環)。
41
+ したがって `int8 × int8 → int32` GEMM の各出力要素は、**k 方向の reduction 順序に依らず同一ビット**。
42
+
43
+ **系.** 同じ量子化入力を与えれば、逐次和・ペアワイズ木・SIMD レーン・GPU warp-shuffle・マルチタイル、
44
+ どの実装でも、CPU / Apple GPU / NVIDIA(Turing/Ampere/Ada/Hopper) のどれでも**同一ビット**を出す。
45
+ fp と違い、順序を仕様で固定する必要すらない(整数であること自体が不変性を与える)。
46
+
47
+ ---
48
+
49
+ ## 3. Canonical 整数計算パスの仕様(QW36-int)
50
+
51
+ 肝は「**どこにも fp を混入させない**」こと。各段を整数関数として一意に定義する。
52
+
53
+ ### 3.1 量子化
54
+ - **重み**: W4(or W8)対称量子化。スケールを `(multiplier: i32, shift: u32)` の固定小数点で pin。
55
+ 丸めは round-half-to-even 固定。決定論的で 1 回だけ(GGUF に相当)。
56
+ - **活性**: A8 対称。per-tensor(or per-token)スケールを固定小数点で。**量子化の除算も固定小数点**
57
+ (`x * inv_scale_M0 >> shift`)で行い fp を使わない。
58
+
59
+ ### 3.2 GEMM
60
+ - `int8 × int8 → int32` 累積。tensor core 使用可(§5 caveat)。順序自由(結合律)。
61
+
62
+ ### 3.3 再量子化 `int32 → int8`(**最重要・fp 再混入の最大リスク**)
63
+ - gemmlowp / TFLite 形式: `q = clamp((acc * M0 + (1<<(shift-1))) >> shift, -127, 127)`。
64
+ `M0: i32`, `shift: u32` を層ごとに pin。**整数のみ・丸め点固定**。ここで fp を使うと台無しになる。
65
+
66
+ ### 3.4 非線形
67
+ - softmax / SiLU / sigmoid / exp / GELU は**固定小数点 or LUT**(量子化ドメイン上のテーブル)で定義し、
68
+ 入力→出力を整数関数として一意化。
69
+ - RMSNorm / LayerNorm は平方根を含む → 固定小数点 `rsqrt`(Newton 反復回数を固定)or LUT。
70
+
71
+ ### 3.5 routing / argmax / sampler
72
+ - MoE router logits は整数 → **top-k は整数比較**、同点は低 index 優先で決定的。
73
+ - 最終 argmax も整数比較 + 決定的 tie-break。greedy は完全決定的で、これが token `vector_commitment`
74
+ を backend 非依存にする。
75
+
76
+ ### 3.6 DeltaNet(hybrid model のみ)
77
+ - 状態更新を**トークン単位で再量子化**([spec-snapshot §6.1(2)](spec-snapshot-qwen36-2026-07-19.md))。
78
+ 丸め点を固定し、逐次デコードとチャンク並列 prefill が同一の丸め列を辿るようにする。
79
+
80
+ ---
81
+
82
+ ## 4. 実証(本セッション・実機)
83
+
84
+ - prototype: `docs/evidence/canon_int.rs`(依存なし、`rustc -O canon_int.rs && ./canon_int`)。
85
+ - `int8×int8→int32` GEMM を **逐次和と木和の 2 つの reduction 順序**で計算し比較、加えて同じソースを
86
+ 2 ISA で実行。結果([evidence md](evidence/canonical-int-cross-isa-2026-07-19.md)):
87
+
88
+ | machine | ISA | INT 順序不変 (seq vs tree) | fp32 順序不変 | canonical commitment |
89
+ |---|---|---|---|---|
90
+ | Mac (Apple) | aarch64 | **MATCH** | DIFFER | `743652581b0b9725` |
91
+ | RTX box (NVIDIA host) | x86_64 | **MATCH** | DIFFER | `743652581b0b9725` ✅ 同一 |
92
+
93
+ - 読み取り: **整数 → 順序にも ISA にも非依存**(同一ビット)。**fp → 順序で割れる**(`9019ee04…` vs
94
+ `e68f80da…`)= GPU の kernel 差がそのままビット差になる、という世代跨ぎ不能の正体。
95
+
96
+ **increment 2(GPU、完了)**: 同じ int8 GEMM を **custom GPU kernel** で実装し実 GPU 2 ベンダで実行
97
+ ([GPU evidence](evidence/canonical-int-gpu-3backend-2026-07-19.md)、`canon_metal.swift` / `canon_cuda.cu`):
98
+
99
+ | backend | hardware | ISA / arch | INT fnv |
100
+ |---|---|---|---|
101
+ | CPU | Apple M1 Max | aarch64 | `4b138ed70d137781` |
102
+ | CPU | NVIDIA/WSL2 host | x86_64 | `4b138ed70d137781` |
103
+ | **Apple Metal GPU** | Apple M1 Max | Apple GPU | `4b138ed70d137781` |
104
+ | **NVIDIA CUDA GPU** | RTX 4060 Ti | Ada `sm_89` | `4b138ed70d137781` |
105
+
106
+ → **Apple GPU ≡ NVIDIA Ada ≡ x86 CPU ≡ ARM CPU** が同一ビット。canonical 整数 GEMM は vendor・microarch・
107
+ ISA 非依存であることを実 GPU 上で実証。fp では candle bump だけで Ada の commitment が動いたのと対照的。
108
+ 残る Turing(`sm_75`)実機確認は増分 6(HW 未所持)。
109
+
110
+ ---
111
+
112
+ ## 5. これで RTX20/40 が 1 クラスになる理由 と caveat
113
+
114
+ **なる理由**
115
+ - int8 tensor core は **Turing(`sm_75`)/Ampere/Ada/Hopper 共通**。bf16 が Turing に無い問題(
116
+ [docs/cuda-turing の背景](../README.md))を回避し、全世代で**同じ整数演算**を実行できる。
117
+ - MMA int8 命令の内部累積は決定的。タイル間の `int32` 累積は結合律で順序不問 → **kernel/世代差を吸収**。
118
+
119
+ **caveat(正直に)**
120
+ 1. 量子化・再量子化・非線形を**全 backend で同一定義**にすること。ここに fp が 1 箇所でも混ざると不変性は
121
+ 壊れる(§3.3 が最大リスク)。
122
+ 2. MMA の入力レイアウト・飽和/オーバーフロー挙動が仕様通りか、**各 backend で実測検証**が要る。
123
+ 3. 前提は「同じ量子化モデル」。量子化そのものが決定論的でなければ出発点で割れる。
124
+ 4. **Turing 実機は未所持** → 最終的な RTX20 でのビット一致確認はユーザ側 HW ステップ。
125
+
126
+ ---
127
+
128
+ ## 6. 実装ロードマップ(increment)
129
+
130
+ - **increment 1(完了)**: 定理 + prototype + cross-ISA CPU 実証(§4)。
131
+ - **increment 2(完了)**: GPU int8 canonical GEMM kernel(Metal shader + CUDA `.cu`)を実装し、CPU / Apple
132
+ Metal / NVIDIA CUDA(Ada) の 4 経路でビット一致(`4b138ed70d137781`)を実機実証(§4)。※ candle 0.11 の
133
+ 公開 Tensor API は int8×int8→int32 の GPU matmul を持たないため custom kernel を書いた。次の硬化は
134
+ tensor-core / split-K 版で GPU 並列 reduction を直接検証すること。
135
+ - **increment 3(完了)**: 1 layer(int8 linear + int32 bias + 固定小数点 requant + 整数 LUT 非線形)を
136
+ canonical 化し、CPU aarch64 / CPU x86_64 / Apple Metal / NVIDIA CUDA(Ada) の 4 経路で LAYER fnv
137
+ `440bd1b93d991fe0` に一致([evidence](evidence/canonical-int-layer-4backend-2026-07-19.md)、
138
+ `canon_layer.{rs,swift,cu}`)。requant は整数 mul+shift、非線形は pinned 整数 LUT lookup — **fp 再混入の
139
+ 2 大リスク(requant の fp 除算・on-device transcendental)を両方排除**。q は `[-127,127]` 全域に分布
140
+ (非退化)。
141
+ - **increment 4a(完了)**: 残る op 型を canonical 整数化 — softmax(固定小数点 exp、整数 Horner 生成の
142
+ pinned テーブル)/ RMSNorm(整数 rsqrt = 整数 Newton の isqrt)/ argmax — を CPU aarch64 / CPU x86_64 /
143
+ Apple Metal / NVIDIA CUDA(Ada) で COMBINED fnv `4bf581ead4d57139` に一致
144
+ ([evidence](evidence/canonical-int-ops-4backend-2026-07-19.md)、`canon_ops.{rs,swift,cu}`)。
145
+ **inc2(GEMM)+inc3(bias/requant/LUT)+inc4a(softmax/RMSNorm/argmax)= dense transformer forward に要る
146
+ 全 op 型が backend 非依存**。
147
+ - **increment 4b**: 上記 primitive を dense Qwen2.5-0.5B の **実重み forward**(24 層 / RoPE / GQA /
148
+ SwiGLU / tied LM head)に組み上げ、verifier に `canonical` クラスを追加し、token `vector_commitment` が
149
+ CPU/Metal/CUDA で同一になることを実証。量子化の品質保持も含む大きな統合。
150
+ - **increment 5**: 本番 hybrid Qwen3.6(MoE + DeltaNet)へ。[spec-snapshot §6.1](spec-snapshot-qwen36-2026-07-19.md)
151
+ の整数意味論を実装。
152
+ - **increment 6**: 実機 Turing(RTX 2060 等)で increment 4 の値一致を確認 → **RTX20/16 系を Ada と同一
153
+ クラスに登録**。
154
+
155
+ ---
156
+
157
+ ## 7. 正直な限界
158
+
159
+ - 本セッションは **CPU 整数の cross-ISA 実証**まで。GPU int8 kernel のビット一致は未実証(increment 2)。
160
+ - Turing 実機未所持 → 最終確認はユーザ HW ステップ。
161
+ - candle デモ verifier は fp で動くので、canonical class は**別実装(runtime 側)**になる。
162
+ - 性能/精度トレードオフ: 整数パスは精度検証が要る。int8 tensor core は速いが、量子化誤差が argmax を
163
+ 変えないか(perplexity・token 一致率)を実測で確認する必要がある。
164
+
165
+ ---
166
+
167
+ ## 8. Metal 移植のための i128→i64 refactor 設計(2026-07-20 追記)
168
+
169
+ MSL には 128-bit 整数が無い(`long`=i64 まで)。hifi 構成は Q60 スケールで i128 積(最大 ~2^102)を
170
+ 使うため、そのままでは Metal に載らない。設計:
171
+
172
+ 1. **スケールを正規化 (m0, e) 表現に**: `scale = m0 × 2^e`、`m0 ∈ [2^30, 2^31)`(31-bit 正規化)。
173
+ すべてのスケール演算後に renormalize(shift+指数調整)。
174
+ - `mul(s1, s2)`: `m0 = (m0a×m0b) >> 31`(積は < 2^62 → **i64 に収まる**)、`e = ea+eb+31`、renorm。
175
+ - `ratio = s1/s2`: `m0 = (m0a << 31) / m0b`(分子 < 2^62)、`e = ea−eb−31`、renorm。
176
+ 2. **apply(value, scale)** = `value × m0 × 2^e`: `value`(≤2^42)× `m0`(2^31)= ~2^73 — i64 超。
177
+ → **mulhi ベースの 64×64→128 エミュレーション**: MSL の `metal::mulhi(long, long)` + `x*y`(下位)で
178
+ 128-bit 積を 2 word で構成し、`>> (−e)` を 2-word shift で実施(丸めは下位 word から桁上げ)。
179
+ 固定パターンなので ~15 行のヘルパで済む。
180
+ 3. **残る 128/64 除算は 2 箇所だけ**: (a) A·V の `acc·2^SC / den` — `acc ≤ 2^42`、`den ≤ 2^20` 程度なので
181
+ 先に `acc / den`(i64 除算)と剰余補正で分解可能(丸め規約を参照実装と厳密一致させること)。
182
+ (b) 残差の `(colsq << SC)/xsq` — 正規化表現では ratio 演算(上記 1)に置換され消える。
183
+ 4. **検証手順**: まず Rust で「正規化 (m0,e) + mulhi エミュ」版を書き、既存 i128 版と**同一 fingerprint**
184
+ (`906729150a5a926a` / full-model FNV `65d2e3adaf2dce9e`)を出すことを確認 → その Rust 版を MSL に
185
+ 直訳(意味論が i64+mulhi に閉じているので直訳可能)→ M1 Max で layer → full-model の順に一致確認。
186
+
187
+ 状態: **設計確定・実装は未着手**(このセッションでは CPU×2 ISA + CUDA の 4 実装一致を優先)。
188
+ Apple ハードは CPU(aarch64)で full-model 一致済みのため、残るのは Apple **GPU** 固有分のみ。
docs/canonical-integer-inc4b-fullmodel-design.md ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # inc4b 設計 — dense Qwen2.5-0.5B の canonical 整数 forward
2
+
3
+ **日付**: 2026-07-19 **前提**: [canonical-integer-compute-design.md](canonical-integer-compute-design.md)
4
+ の increment 4b。inc2–4a で **全 op 型の backend 非依存を実機実証済み**(GEMM / bias / requant / LUT /
5
+ softmax / RMSNorm / argmax)。本書はそれらを **実重みの Qwen2.5-0.5B forward** に組み上げ、verifier で
6
+ token `vector_commitment` を CPU/Metal/CUDA 同一にするための設計。
7
+
8
+ ## 0. 目的
9
+
10
+ - 実モデルの forward 全体を **fp を一切使わず整数**で通し、**backend 非依存の token commitment** を出す。
11
+ - これが完成すると Apple/Metal・NVIDIA/CUDA・AMD/ROCm・CPU が **1 pool** で相互検証でき、「Mac の job を
12
+ CUDA で丸ごと検証」がモデル全体で成立する。
13
+
14
+ ## 1. 対象アーキ(`/root/models/qwen0.5b-q4.gguf` GGUF 実測)
15
+
16
+ | 項目 | 値 |
17
+ |---|---|
18
+ | architecture | `qwen2`(Qwen2.5-0.5B-Instruct) |
19
+ | block_count(層) | 24 |
20
+ | embedding_length(hidden d) | 896 |
21
+ | feed_forward_length(FFN) | 4864(SwiGLU: gate/up 896→4864, down 4864→896) |
22
+ | head_count / head_count_kv | 14 / 2(GQA, group=7), head_dim=64 |
23
+ | rope.freq_base | 1000000.0 |
24
+ | rms_norm_eps | ~1e-6 |
25
+ | file_type | 15(Q4_K_M) |
26
+ | vocab | 151936(tied: `output` = `token_embd`)※ token_embd tensor shape で確認 |
27
+ | bias | Qwen2 は q/k/v proj に **bias 有**、o_proj は bias 無 ※ tensor 名で確認 |
28
+
29
+ ## 2. 量子化方式の決定 — **W8A16**(4b-2a 実測後の改訂)
30
+
31
+ > **改訂([4b-2a evidence](evidence/canonical-int-attention-4b2a-2026-07-19.md))**: 実測で W8A8(int8 活性)は
32
+ > attention の**活性外れ値**で誤差 ~21% と大きく、素の SmoothQuant(α=0.5)では改善せず(要 calibration 調整)。
33
+ > **W8A16(int8 重み × int16 活性 → int64 累積)**なら外れ値を潰さず、RMSNorm を高精度化(下記)した上で
34
+ > **attention 誤差 2.6%** に収まる。int8×int16 は tensor core 非対応(通常 ALU、決定性は不変)なので、
35
+ > tensor core を使う W8A8+調整済み SmoothQuant は後段の性能最適化に回す。**まず W8A16 で正確な canonical を作る。**
36
+ > また RMSNorm の整数実装は「正規化値を高精度固定小数点(例 Q20)で保持」すること必須(`>>` で潰すと精度崩壊。
37
+ > これが 4b-2a で 36% 誤差の真因だった)。
38
+
39
+ ### 旧 §2(W8A8 前提、tensor core 最優先版 — 参考)
40
+
41
+ - **重み int8・活性 int8・累積 int32**。理由: (1) int8 tensor core は Turing〜Hopper 共通で世代統合に最適、
42
+ (2) 0.5B が W8A8 で ~0.5GB と軽い、(3) 4bit より精度余裕があり最初の正しさ確認向き。W4A8(~0.25GB)は
43
+ サイズ最適化として後続。
44
+ - **重み量子化はオフラインで 1 回**(bf16 原重み → per-output-channel 対称 int8、scale は
45
+ `(multiplier:i32, shift:u32)` の固定小数点で pin)。**fp はこのオフライン工程のみで、runtime には一切
46
+ 入らない**。scale は成果物(`.canon` ファイル)に焼き込む。Q4_K_M の fp16 block scale は使わず、bf16
47
+ 原重みから再量子化する(Q4_K_M は既に lossy かつ fp scale なので canonical には不適)。
48
+ - **活性量子化**: per-token 動的対称 int8。scale 計算は「max(|x|) を整数で求め、`127/max` を固定小数点
49
+ reciprocal」で行い fp を使わない(= inc3 の requant 機構の一般化)。または静的 calibration scale を pin。
50
+ - **residual stream は int32(固定小数点)で保持**し、各 matmul の入口でだけ int8 に量子化する(層をまたぐ
51
+ ドリフトを防ぐ)。
52
+
53
+ ## 3. 各 op の整数仕様(すべて実証済み primitive の合成)
54
+
55
+ | stage | 整数仕様 | 依拠 primitive |
56
+ |---|---|---|
57
+ | token embedding | int8 埋め込み表を gather(tied) | 決定的 gather |
58
+ | RMSNorm | `Σx²`(int64)→ 整数 rsqrt(isqrt)→ γ(pin, 固定小数点) | **inc4a** |
59
+ | Q/K/V proj (+bias) | int8 GEMM → +int32 bias → 固定小数点 requant | **inc2+inc3** |
60
+ | RoPE | cos/sin を **位置×dim/2 の固定小数点 pinned テーブル**化(on-device で fp 三角関数を計算しない)→ 整数 mul-add 回転 | 新(LUT と同型、pin) |
61
+ | QK^T | int8 GEMM(head 毎)→ int32 scores | **inc2** |
62
+ | scale 1/√dₕ | 固定小数点定数の mul-shift | inc3 |
63
+ | causal mask | masked 位置に大負整数を加算 | 整数 |
64
+ | softmax | max 減算 → exp は pinned 固定小数点表 → int64 和 → 整数除算 | **inc4a** |
65
+ | A·V | probs(Q16) × V(int8) 累積(int64) → requant | inc2+inc3 変種 |
66
+ | o_proj + residual | int8 GEMM → int32 residual 加算 | inc2 |
67
+ | SwiGLU FFN | gate/up = int8GEMM+requant;act = SiLU LUT(gate);h = act·up → requant;down = int8GEMM+requant | **inc2+inc3** |
68
+ | final RMSNorm | 同上 | inc4a |
69
+ | LM head(tied) | int8 GEMM x × embedddingᵀ → int32 logits | inc2 |
70
+ | argmax(greedy) | 整数比較・決定的 tie-break | **inc4a** |
71
+
72
+ **RoPE テーブル**: 32768 pos × 32 dim-pair × (cos,sin) を固定小数点(例 Q15)で pin ≈ 8MB。必要位置のみ
73
+ 生成でも可だが、生成式は整数固定小数点で決定的にすること(fp `sin/cos` を on-device で使わない)。
74
+
75
+ ## 4. 決定論の担保
76
+
77
+ 全 stage が「inc2–4a で backend 一致を実証済みの整数 op」+「pin された固定小数点テーブル(RoPE/LUT/scale)」
78
+ +「決定的 gather」の合成。**backend 一致な op の合成は backend 一致**(fp が 1 箇所も無い限り)。よって
79
+ full forward は **構成上** backend 非依存。残るのは「実装で fp を混入させないこと」の徹底のみ。
80
+
81
+ ## 5. 精度検証計画(別軸: 決定論 ≠ 精度)
82
+
83
+ - canonical 整数 forward と **fp 参照(candle)**を同一プロンプトで greedy 実行し、**token 一致率**と
84
+ **perplexity** を測る。W8A8 は多くの token で fp に一致するはずだが、低マージン token は分岐し得る
85
+ (それは fp とは別の、より低精度だが**決定的**なモデル、という理解)。
86
+ - **重要**: canonical class の golden token commitment は fp class の値とは**別物**(新しいクラス)。価値は
87
+ 「全 backend で同一」であること。品質が不足なら per-group scale / W8A16 活性へ。
88
+
89
+ ## 6. verifier 統合
90
+
91
+ - `misaka-verify` に `--canonical`(整数 forward)モードを追加。`published.json` に **`canonical` クラス**
92
+ を 1 つ追加し、その token `vector_commitment` は **backend 非依存の単一値**(Apple+NVIDIA+AMD+CPU 共通)。
93
+ - fp の per-vendor クラス(metal/cuda)はそのまま残し、canonical を並置(移行期)。
94
+
95
+ ## 7. 実装 sub-increments
96
+
97
+ - **4b-1**: オフライン量子化器 — bf16 原重み → W8A8 `.canon` 成果物(pin: 重み int8 + per-channel 固定小数点
98
+ scale + RoPE/LUT テーブル + config)。
99
+ - **4b-2**: attention ブロック 1 個(RoPE + GQA QK^T + scale + causal + softmax + A·V + o_proj)を整数実装、
100
+ 4-backend 一致(inc4a の延長)。
101
+ - **4b-3**: full transformer layer 1 個(attn + SwiGLU + 2×RMSNorm + residual)を整数実装、4-backend 一致。
102
+ - **4b-4**: 24 層 + LM head + argmax の full forward、実プロンプトで token 列を出し 4-backend 一致。
103
+ - **4b-5**: fp 参照との token 一致率/perplexity 測定、verifier に `canonical` クラス統合・golden 記録。
104
+
105
+ ## 8. リスクと正直な限界
106
+
107
+ - **精度**: W8A8 が 0.5B の出力品質を落とす可能性 → 4b-5 で定量化。不足なら per-group/W8A16 に段階調整。
108
+ - **residual 精度**: int32 固定小数点の bit 幅設計を誤るとドリフト → 4b-3 で検証。
109
+ - **工数**: これは実質「整数推論エンジン 1 本」。単発カーネルの数倍で、複数セッション想定。
110
+ - **Turing 実機**: 依然未所持。4b-4 の値は定理上 Turing でも一致するが確認は HW ステップ(inc6)。
111
+ - **本番 hybrid(Qwen3.6 MoE+DeltaNet, inc5)**は 4b の後。MoE 整数 top-k・DeltaNet per-token 再量子化は
112
+ [spec-snapshot §6.1](spec-snapshot-qwen36-2026-07-19.md) 参照。
113
+ - 本書の scale/表は demonstrator。**本番は正確な scale・表・反復回数を pin し、モデル成果物と consensus に
114
+ 含める**必要がある。
docs/canonical-integer-qwen36-design.md ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # QI35 — Qwen3.6-35B-A3B canonical 整数 forward 設計(inc5 具体化)
2
+
3
+ **日付**: 2026-07-20 **対象モデル**: `models/Qwen3.6-abliterated-35b-Claude-4.7/Qwen3.6-abliterated-35b-Claude-4.7-Q4_K_M.gguf`
4
+ (23,938,321,728 bytes — 受領/receipt トラックが commit している本番 blob そのもの)
5
+ **前提**: [canonical-integer-compute-design.md](canonical-integer-compute-design.md)(inc1–4b、0.5B hifi で
6
+ fp 12/12 追跡 + 4 実装ビット一致を実機実証済み)。本書はロードマップ **increment 5**(本番 hybrid)の設計。
7
+ **方針転換**(ユーザ指示 2026-07-20): 以後 Qwen3.6-35B のみを対象とし、0.5B デモは基盤実証としてのみ参照。
8
+
9
+ ---
10
+
11
+ ## 0. 結論(先に)
12
+
13
+ 1. **重み源は Q4_K_M blob そのもの**。bf16 再量子化はしない。Q4_K/Q6_K の fp16 スケール
14
+ (d, dmin)は**ビット操作だけで exact な整数×2^e に分解できる**ため、GEMM を「4/6-bit 整数 ×
15
+ A24 整数活性 × 整数スケール代数」として**一切の丸め追加なしに**定義できる(§4)。
16
+ → 変換成果物ゼロ、receipt が commit する blob hash がそのまま canonical 整数モデルの重み hash。
17
+ 2. **整数化により spec-snapshot §6.3 の最難関(DeltaNet の per-token 丸め列を chunked prefill が
18
+ 再現する条件)は構造的に消滅**する。整数加算は結合的なので、GEMM をバッチしつつ状態再帰を
19
+ トークン順に回せば、逐次 decode と prefill は**自動的にビット一致**する(§7)。
20
+ 3. Mac Studio M1 Max 32GB は本モデルにとって**構造的に最良のローカル機**: 23.9GB blob が unified
21
+ memory に丸ごと載る(RTX 4060 Ti 16GB VRAM には載らない)。Metal offload(QI35-5)の速度目標は
22
+ fp Metal llama.cpp と同オーダーの tok/s。
23
+
24
+ ---
25
+
26
+ ## 1. アーキテクチャ ground truth(GGUF + vendored llama.cpp 実測)
27
+
28
+ メタデータ(`docs/evidence/` の gguf_full_dump 実行結果、2026-07-20):
29
+
30
+ | 項目 | 値 |
31
+ |---|---|
32
+ | arch / params | `qwen35moe` / 35,951,822,704 |
33
+ | 層 | 40(`full_attention_interval=4`: **(i+1)%4==0 の層 3,7,…,39 が full attention(10 層)、他 30 層が gated delta net**。`qwen35moe.cpp:39` `is_recr_impl[i] = (i+1)%4 != 0`) |
34
+ | d_model / vocab | 2048 / 248,320(eos 248046) |
35
+ | attention | 16 head × head_dim 256、GQA kv 2 head(`head_count_kv` 配列)、q/k per-head RMSNorm、**Q 射影は query+gate 融合出力 8192**(interleave: head 毎 [q256, gate256])、kq_scale = 1/√256 = **1/16(有理数)**、出力は sigmoid(gate) で elementwise gating |
36
+ | RoPE | IMROPE だが**テキスト入力では素の NEOX RoPE に退化**(3 位置成分が同値、`llama-graph.cpp:128-139`)。回転は先頭 64/256 次元のみ、ペア (i, i+32) i∈0..31、θ_k = pos·(10^7)^(−2k/64)。**YaRN 無効**(GGUF に rope.scaling 無し → ext_factor=0, freq_scale=1, attn_factor=1) |
37
+ | DeltaNet | conv kernel 4 / k-head 16×128 / v-head 32×128(d_inner 4096)/ state 128×128×32head / `ssm_dt`[32] bias / `ssm_a`[32](= −exp(A_log) 変換済み F32)/ β,α 射影 [2048,32] / 出力 gate z [2048,4096] / per-head RMSNorm(ssm_norm[128])× SiLU(z) / ssm_out [4096,2048] |
38
+ | MoE(全 40 層) | router `ffn_gate_inp` [2048,256] **F32**、**softmax→top-8**(argsort desc)、選択重みを和で正規化(fp は F16 min でクランプ)、expert_weights_scale **無し**(0.0 → スキップ確認)、expert FFN 512(gate/up Q4_K・down Q6_K、fused gate_up 無し)、shared expert 512 + sigmoid(gate_inp_shexp[2048,1] F16) |
39
+ | head | `output.weight` Q6_K [2048,248320](**非 tied**)、output_norm RMSNorm |
40
+ | 量子化型 | Q4_K 349 / Q6_K 94 / F32 544 / F16 207 tensor |
41
+ | 同梱・非実行 | vision tower `v.*` 441 tensor、MTP `mtp.*` 20 tensor(nextn KV 無し → n_layer_nextn=0、テキスト経路は 40 trunk 層のみ。ローダは個数整合のみ処理 `qwen35moe.cpp:184-192`) |
42
+ | その他 | `ssm.v_head_reordered=True` は**ランタイム未参照**(変換時に並べ替え済み、実測 grep 0 件) |
43
+
44
+ fp 意味論の出典(全て vendored `llama.cpp`、行番号 2026-07-20 時点):
45
+ - 層構造 / attention / MoE 呼び出し: `src/models/qwen35moe.cpp:202-599`
46
+ - DeltaNet per-token 再帰(canonical の基準): fused op CPU 実装 `ggml/src/ggml-cpu/ops.cpp:10768-10809`
47
+ および非融合 autoregressive `src/models/delta-net-base.cpp:289-371` — 数式は同一:
48
+ `S ← S·exp(g)`(head 毎スカラー減衰)→ `Δ[j] = (v[j] − Σ_i S[i][j]k[i])·β` → `S[i][j] += k[i]Δ[j]`
49
+ → `o[j] = (Σ_i S[i][j]q[i])·(1/√128)`
50
+ - MoE: `src/llama-graph.cpp:1799-2148`(softmax 先行 → argsort top-8 → 和正規化 → silu(gate)·up →
51
+ down → ×weight → 8 和; tie-break は fp 実装では**未定義**(std::sort 不安定))
52
+ - 単項 op: silu/sigmoid/softplus(x>20→x)/l2_norm(x/max(√Σx²,eps))/rms_norm(1/√(mean+eps))/
53
+ ssm_conv(窓 4 前方 dot)/soft_max: `ggml-cpu/vec.h:936-1048, unary-ops.cpp:80, ops.cpp:3795-5502,9494,10671`
54
+ - RoPE: `ggml-cpu/ops.cpp:5766-6031`(theta_scale = freq_base^(−2/64)、NEOX ペア、64 以降素通し)
55
+
56
+ ---
57
+
58
+ ## 2. Canonical class の定義(何を不変量とするか)
59
+
60
+ - **重み**: blob のバイト列(既存 receipt の model hash と同一物)。
61
+ - **整数意味論**: 本書 §4–§9 の固定小数点演算列。丸め点は全て pin(RNE または明示 floor)。
62
+ - **決定性**: 全段整数 → reduction 順序・カーネル・ベンダ・ISA 非依存(inc1–4a で実証済みの性質)。
63
+ - **出力**: greedy argmax token 列(低 index tie-break)と、その上の commitment。
64
+ - fp 版とは**別クラス**(fp 品質をどこまで追跡するかは G3 で実測報告 — 捏造しない)。
65
+
66
+ ## 3. スケール表現
67
+
68
+ - 0.5B hifi の **Q60 固定小数点(SC=60, HALF, RNE)+ i128 ホスト演算**をそのまま採用(実証済み)。
69
+ - fp16 → exact 整数: 値 = mant·2^(e−25)(normal: mant = 1024+frac10, e=exp5; subnormal mant=frac10,
70
+ e=0 扱い)。fp32 → mant·2^(e−150)。**ビット操作のみ、fp 演算ゼロ**。
71
+ - Metal オンデバイスでスケール代数が要る箇所は設計済みの正規化 (m0,e)+mulhi(親設計 §8)を使う。
72
+ ただし QI35-5 の第一段は CUDA-mirror 構成(GEMM=GPU 整数、スケール代数=host i128)なので不要。
73
+
74
+ ## 4. GEMM — Q4_K / Q6_K を**直接**整数消費する
75
+
76
+ **Q4_K**(superblock 256 = 32×8 sub-block; 16B ヘッダ + 128B nibble):
77
+ `w[i] = d·sc_j·q[i] − dmin·m_j`(q∈[0,15], sc_j,m_j∈[0,63] 6-bit)。行方向(k 方向)dot は
78
+
79
+ ```
80
+ dot_j = Σ_{i∈sub_j} x_i·q_i (x: A24 → |x|≤2^23; |dot_j| ≤ 2^32.3 → i64)
81
+ sum_j = Σ_{i∈sub_j} x_i (|sum_j| ≤ 2^28)
82
+ acc = Σ_j [ (d_mant·sc_j)·dot_j·2^{e_d} − (dmin_mant·m_j)·sum_j·2^{e_dm} ]
83
+ ```
84
+
85
+ d_mant·sc_j < 2^17、積 ≤ 2^49.3 → **i64 に収まり、指数アライン(行内最小指数へシフト)後の総和は
86
+ CPU=i128 / GPU=2-word (hi,lo) i64 で exact 累積**。丸めは発生しない(有理数の exact 和)。
87
+ **Q6_K**(256 = 16×16; sc int8 符号付き, w = d·sc_j·(q−32)): 同型で dmin 項なし。
88
+ 行スケールは Q60 に正規化した `colsq[o]`(= 2^{e_row}·…)として吐き、以降は 0.5B hifi の
89
+ `proj()`(colsq/cmax → ratio → apply → aq)と同一代数。**bias は本モデルに存在しない**(全 GEMM bias 無し)。
90
+
91
+ F32/F16 重み(router / norm γ / ssm_a / ssm_dt / conv1d / shexp gate)は**ロード時に exact 分解 →
92
+ per-tensor Q 固定小数点へ pinned RNE 変換**(決定的、blob バイトのみに依存)。
93
+
94
+ ## 5. 活性量子化 — **per-token A24**
95
+
96
+ - aq() は hifi と同じ(ALVL=2^23−1、符号付き丸め、scale=amax·cur/ALVL)だが、**必ず token 行単位**。
97
+ 理由: prefill(複数 token 同時)と decode(1 token)で量子化が変わらないこと = KV cache /
98
+ 逐次一致の前提。0.5B 参照(テンソル一括 amax)からの意図的変更点として pin する。
99
+ - residual stream は i64 値 + per-token スケール(hifi と同じ持ち方)。
100
+
101
+ ## 6. 非線形 — 全て pinned 整数 LUT / 整数反復
102
+
103
+ | op | 実装(**G2 で較正・確定** — [evidence](evidence/canonical-int-qwen36-layer-g2-2026-07-20.md)) | 状態 |
104
+ |---|---|---|
105
+ | softmax exp | **[0,8) 1/2048 刻み 16384 entry Q20**(0.5B の [0,4)×1/512 から拡張 — 35B router 差は ~7.2)、範囲外→0 | 確定 |
106
+ | sigmoid / SiLU / β / attention gate / shexp gate | 16384×(1/512) [−16,16) Q16、SiLU = x·σ表[x]、域外 clamp | 確定 |
107
+ | softplus(α+dt) | 16384×(1/512) [−16,16) 出力 Q16、x>20→x(ggml 準拠) | 確定 |
108
+ | decay γ=exp(g), g≤0 | **[−16,0] 1/2048 刻み 32769 entry Q30**、g<−16→0(実測 g absmean 8.8) | 確定 |
109
+ | RMSNorm(attn_norm/post_norm/q,k_norm/ssm_norm) | Σx² → 整数 isqrt。**eps=1e-6 を整数で pin**: `EPS_Q40=1,099,512` / `EPS_Q48=281,474,977`、可変スケール入力は per-token `eps_int=EPS_Q40·U²>>40`(U=2^60/scale)。**+1 近似は不可** — 減衰 head は eps-floor 支配(G2 で 29× ずれる実測) | 確定 |
110
+ | L2 norm(DeltaNet q,k) | x·2^20 / max(isqrt(Σx²),1)(ggml の分母 floor 意味論)→ 単位 Q20 | 確定 |
111
+ | RoPE cos/sin | Q30、32 ペア × SMAX pos、テーブルのバイト列を成果物として pin | 確定 |
112
+ | 1/√128(DeltaNet 読み出し) | Q30 定数 RNE pin | 確定 |
113
+ | 1/16(attention scale) | 右シフト 4 の exact 有理数 | — |
114
+
115
+ ## 7. DeltaNet 整数意味論(per-token が定義、prefill は同ビット)
116
+
117
+ 状態 S: **Q24 固定小数点・i64 演算で閉じる**(G2 実測 |S|≤10 → 積和 ≤2^55。GPU に i128 不要)
118
+ 128×128×32head×30 層 ≈ 63MB(i32 格納可)。conv 窓と v は**絶対 Q16**(実測 ±35)、
119
+ q/k は L2 norm 後 **Q20 単位ベクトル**、β/softplus Q16、γ Q30。v-head h ↔ k-head **h%16**(タイル)。
120
+ 1 token の列(丸め点 = ★のみ、他は exact):
121
+
122
+ 1. qkv_mixed = W_qkv GEMM(§4)→ per-token aq ★
123
+ 2. conv 窓 [state(3), 今 token] の 4-tap 整数 dot(conv 重みは pinned 整数)→ SiLU LUT ★ → conv state 更新(整数値そのまま保存)
124
+ 3. q,k 分割 → L2 norm ★(head 128 毎)、k-head 16 → v-head 32 へ複製
125
+ 4. β = σ表[β_proj] ★、g = ssm_a ⊙ softplus表[α_proj + dt] ★、γ = exp表[g] ★
126
+ 5. S ← (S·γ) ★RNE→QS; Δ[j] = (v[j] − Σ_i S[i][j]k[i]·2^{−…}) ★·β ★; S[i][j] += k[i]Δ[j] ★RNE→QS
127
+ 6. o[j] = Σ_i S[i][j]q[i] ★ × (1/√128)Q30 ★
128
+ 7. o を per-head RMSNorm(ssm_norm)★ × SiLU(z)★ → aq ★ → W_out GEMM(§4)
129
+
130
+ **prefill**: 射影 GEMM(1,2 の GEMM 部と 7)は全 token バッチで GPU 実行可(token 独立)。
131
+ 状態再帰(5,6)は token 順に実行(≈0.5M 積和/token で軽い)。整数なので**逐次 decode と bit 一致**。
132
+ 状態 checkpoint(spec §7 の N=64 token 毎 hash)は S+conv state+KV の整数バイト列に対して自然に定義できる。
133
+
134
+ ## 8. Full attention 層整数意味論
135
+
136
+ 1. QG = W_q GEMM → interleave 分割(q: head h の [512h, 512h+256)、gate: [512h+256, 512h+512))
137
+ 2. q,k per-head RMSNorm ★(γ=attn_q_norm/attn_k_norm)→ RoPE(Q30 表、64 次元、ペア (i,i+32))★
138
+ 3. KV cache: k,v を**絶対 Q16 固定小数点**で保存(per-token スケールだと A·V の異 token 混合で
139
+ 共通化が要る — 絶対 Q16 なら逐次 decode ≡ prefill が自明。範囲は q/k norm 後 ±10 で安全)
140
+ 4. scores = q·k dot(i128)× 1/16 → hifi 方式 max 減算 + exp 表 ★ → A·V 整数除算 ★(fdiv+HALF pin)
141
+ 5. × σ表[gate] ★ → aq ★ → W_o GEMM
142
+ GQA: kv head = h/8。mask: 因果のみ(pre-softmax の別 bias 無し、sinks 無し)。
143
+
144
+ ## 9. MoE 整数意味論
145
+
146
+ 1. logits = router GEMM(F32→pinned Q24 重み、i128 acc)
147
+ 2. softmax: max 減算 → exp 表 ★ → Σ → 各 top-8 重み w_i = fdiv(e_i·2^30 + Σ/2, Σ) ★
148
+ 3. **top-8 = 整数 desc 比較、同点は低 index**(fp の std::sort 不安定性を canonical 側で確定)
149
+ 4. 分母 Σw_top8 で再正規化 ★(fp の F16-min クランプは整数では Σ≥1 floor として pin)
150
+ 5. 各 expert: gate/up GEMM(Q4_K §4)→ SiLU 表 ★ × up → aq ★ → down GEMM(Q6_K)→ ×w_i ★ → 8 本 exact 和
151
+ 6. shared expert: 同型 + σ表[shexp gate] ★、routed 和に exact 加算
152
+ expert GEMM は「選ばれた 8 expert の行だけ」を実行(offset 計算は tensor 3 次元目 stride)。
153
+
154
+ ## 10. メモリ予算(M1 Max 32GB)
155
+
156
+ | 項目 | 量 |
157
+ |---|---|
158
+ | blob mmap(重み直読) | 23.9GB(クリーン evict 可) |
159
+ | DeltaNet S + conv state | ~66MB |
160
+ | KV cache(4K ctx, 10 層) | ~35MB(整数) |
161
+ | LUT/RoPE 表/スケール表 | <50MB |
162
+ | 活性ワーク | <200MB |
163
+ | 合計 | ~24.3GB — 32GB 内、余裕 ~7GB |
164
+
165
+ ## 11. 実装ゲート(hardware-validated、捏造なし)
166
+
167
+ - **G1(op)**: Q4_K/Q6_K 直接整数 GEMM を Python bigint exact 参照と突合、Rust x86/aarch64 でビット一致
168
+ - **G2(layer)【完了 2026-07-20】**: blk.0(DeltaNet)+ blk.3(attn)+ MoE を llama.cpp CPU fp32 の
169
+ 実活性と全 77 チェックポイント照合 — **layer 出力 ≤3%・状態 rmse ~1e-4・MoE top-8 全一致**
170
+ ([evidence](evidence/canonical-int-qwen36-layer-g2-2026-07-20.md)、`qi35_int_ref.py`)
171
+ - **G3(model)**: 40 層 greedy を fp(CPU/Metal)と複数 prompt で token 一致率測定(結果は実測値のみ記載)
172
+ - **G4(Metal)**: 整数 Metal ≡ 整数 CPU のビット一致(fingerprint)
173
+ - **G5(CUDA)**: RTX box 復帰時に同 fingerprint(モデルは VRAM 16GB に載らないため CPU offload 構成)
174
+
175
+ ## 12. 正直な限界
176
+
177
+ - fp 品質追跡(G3 の一致率)は**未測定**。0.5B hifi は 12/12 だったが、35B/Q4_K/長系列で同等とは限らない。
178
+ - 本書の丸め点(★)のうち QS 等の定数は QI35-3 の実データ較正後に pin(現時点は仕様スロット)。
179
+ - 長文(YaRN 域)・vision・MTP・非 greedy sampler は対象外(テキスト greedy のみ)。
180
+ - fp 実装の MoE tie-break 未定義により、logit 完全同点の手では fp と本 class は原理的に分岐し得る。
181
+ - 32GB 機で fp llama.cpp と整数版を**同時**駐在させると swap する(逐次実行で運用)。
docs/evidence/canon_attn.cu ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // inc4b-2b: CUDA (NVIDIA GPU) port of the fully-integer attention block. Host parses layer0.bin and uploads
2
+ // clean device arrays; a single-thread kernel runs the block with __int128 scales — same integer semantics
3
+ // as the Rust/Python reference. Must reproduce fingerprint dea9a75ba85b1352.
4
+ // Build/run: nvcc -O3 -arch=sm_89 canon_attn.cu -o canon_attn_cu && ./canon_attn_cu layer0.bin
5
+ #include <cstdio>
6
+ #include <cstdint>
7
+ #include <vector>
8
+ #include <cstring>
9
+ #include <cuda_runtime.h>
10
+ typedef long long i64; typedef __int128 i128;
11
+ static const int SC=30, D=896, NH=14, NKV=2, HD=64, S=4, FF=4864;
12
+ static const unsigned long long TARGET=0x906729150a5a926aULL; // full-layer (4b-3)
13
+
14
+ // ---------- host parse ----------
15
+ struct Buf{ std::vector<unsigned char> b; long p=0;
16
+ unsigned u32(){unsigned v;memcpy(&v,&b[p],4);p+=4;return v;} int i32(){int v;memcpy(&v,&b[p],4);p+=4;return v;}
17
+ i64 i64_(){i64 v;memcpy(&v,&b[p],8);p+=8;return v;} unsigned char u8(){return b[p++];}
18
+ };
19
+ __host__ __device__ static i128 qm(i64 m0,i64 sh){ i64 e=sh-1; return e>=0?((i128)m0<<e):(((i128)m0+((i128)1<<(-e-1)))>>(-e)); }
20
+
21
+ // device scratch (single thread, no races)
22
+ __device__ i64 dyg[S*D], dxnq[S*D], dqq[S*NH*HD], dkq[S*NKV*HD], dvq[S*NKV*HD];
23
+ __device__ i64 dscores[NH*S*S], dpn[NH*S*S], dps[NH*S], dattn[S*D], daqa[S*D], doacc[S*D], dacc[S*FF];
24
+ __device__ i128 dcolsq[FF];
25
+ __device__ i64 dhout[S*D], dxn2[S*D], dgate[S*FF], dup[S*FF], dact[S*FF], dhmul[S*FF]; // 4b-3 MLP scratch
26
+
27
+ __device__ static i128 HALFd=(i128)1<<29, ALVLd=32767;
28
+ __device__ i128 ap(i128 a,i128 s){return (a*s+HALFd)>>SC;}
29
+ __device__ i128 mul128(i128 a,i128 b){return (a*b+HALFd)>>SC;}
30
+ __device__ i128 sfrom(i128 amax,i128 cur){return (amax*cur)/ALVLd;}
31
+ __device__ i128 fdiv(i128 a,i128 b){i128 q=a/b,r=a%b; if(r!=0&&((r<0)!=(b<0)))return q-1; return q;}
32
+ __device__ i128 isqrtd(i128 n){if(n<=0)return 0; i128 x=n,y=(x+1)/2; while(y<x){x=y;y=(x+n/x)/2;} return x;}
33
+ __device__ i128 aqd(i64* v,int n,i128 cur){ i64 am=1; for(int i=0;i<n;i++){i64 a=v[i]<0?-v[i]:v[i]; if(a>am)am=a;} i128 amc=am;
34
+ for(int i=0;i<n;i++){i128 xi=v[i],sg=xi>0?amc:(xi<0?-amc:0); v[i]=(i64)fdiv(xi*ALVLd*2+sg,2*amc);} return sfrom(amc,cur);}
35
+
36
+ // proj: acc into dacc/out, returns scale via out param
37
+ __device__ i128 projd(const signed char* w,int oc,int inn,const i128* wsq,const i64* bv,i128 bsq,
38
+ const i64* xnq,i128 xnsq,i64* out){
39
+ for(int s=0;s<S;s++)for(int o=0;o<oc;o++){i64 a=0;for(int d=0;d<inn;d++)a+=xnq[s*inn+d]*(i64)w[o*inn+d]; dacc[s*oc+o]=a;}
40
+ for(int o=0;o<oc;o++) dcolsq[o]=mul128(xnsq,wsq[o]);
41
+ i128 cmax=dcolsq[0]; for(int o=1;o<oc;o++) if(dcolsq[o]>cmax)cmax=dcolsq[o];
42
+ for(int o=0;o<oc;o++){ i128 cs=dcolsq[o]; i64 b= (bv&&cs)? (i64)(fdiv((i128)bv[o]*bsq*((i128)1<<SC)+(cs>>1),cs)>>SC):0;
43
+ i128 r=(cs<<SC)/cmax; for(int s=0;s<S;s++){ i128 ab=(i128)dacc[s*oc+o]+b; out[s*oc+o]=(i64)ap(ab,r);} }
44
+ return aqd(out,S*oc,cmax);
45
+ }
46
+
47
+ __global__ void run(const signed char* qw,const i128* qsc,const i64* qb,i128 qbsc,
48
+ const signed char* kw,const i128* ksc,const i64* kb,i128 kbsc,
49
+ const signed char* vw,const i128* vsc,const i64* vb,i128 vbsc,
50
+ const signed char* ow,const i128* osc,
51
+ const signed char* xv8,const i128* xsc,const i64* gv,i128 gamsc,
52
+ const i64* pav,i128 pasc,const signed char* gw,const i128* gsc,
53
+ const signed char* uw,const i128* usc,const signed char* dw,const i128* dsc,
54
+ const i64* cosq,const i64* sinq,const i64* expl,const i64* sig,i64* hout){
55
+ if(blockIdx.x*blockDim.x+threadIdx.x!=0) return;
56
+ // RMSNorm
57
+ for(int s=0;s<S;s++){ i128 ss=0; for(int d=0;d<D;d++){i128 x=xv8[s*D+d]; ss+=x*x;} i128 mean=ss/D+1;
58
+ i64 inv=(i64)isqrtd(((i128)1<<40)/mean); for(int d=0;d<D;d++) dyg[s*D+d]=(i64)xv8[s*D+d]*inv*gv[d]; }
59
+ i128 xnsq0=aqd(dyg,S*D,(i128)1<<SC); for(int i=0;i<S*D;i++) dxnq[i]=dyg[i];
60
+ i128 xnsq=mul128(mul128(xnsq0,qm(1,11)),gamsc);
61
+ i128 qsq=projd(qw,NH*HD,D,qsc,qb,qbsc,dxnq,xnsq,dqq);
62
+ i128 ksq=projd(kw,NKV*HD,D,ksc,kb,kbsc,dxnq,xnsq,dkq);
63
+ i128 vsq=projd(vw,NKV*HD,D,vsc,vb,vbsc,dxnq,xnsq,dvq);
64
+ // rope (in place) using src copies
65
+ for(int nh=0; nh<2; nh++){ i64* x=nh?dkq:dqq; int nn=nh?NKV:NH; i64 src[S*NH*HD];
66
+ for(int i=0;i<S*nn*HD;i++) src[i]=x[i];
67
+ for(int s=0;s<S;s++)for(int h=0;h<nn;h++)for(int d=0;d<HD;d++){int base=(s*nn+h)*HD;
68
+ i64 rh=d<HD/2?-src[base+d+HD/2]:src[base+d-HD/2]; x[base+d]=(src[base+d]*cosq[s*HD+d]+rh*sinq[s*HD+d]+16384)>>15;} }
69
+ int gg=NH/NKV;
70
+ for(int h=0;h<NH;h++){int kvh=h/gg;for(int s=0;s<S;s++)for(int t=0;t<S;t++){i64 a=0;
71
+ for(int d=0;d<HD;d++)a+=dqq[(s*NH+h)*HD+d]*dkq[(t*NKV+kvh)*HD+d]; dscores[(h*S+s)*S+t]=a;}}
72
+ i128 c8=mul128(qsq,ksq)*8;
73
+ for(int h=0;h<NH;h++)for(int s=0;s<S;s++){ i64 m=dscores[(h*S+s)*S+0]; for(int t=1;t<=s;t++){i64 v=dscores[(h*S+s)*S+t]; if(v>m)m=v;}
74
+ i64 sm=0; for(int t=0;t<=s;t++){ i128 dd=ap((i128)(m-dscores[(h*S+s)*S+t]),c8); if(dd<0)dd=0; if(dd>255)dd=255;
75
+ i64 e=expl[(int)dd]; dpn[(h*S+s)*S+t]=e; sm+=e;} dps[h*S+s]=sm; }
76
+ for(int h=0;h<NH;h++){int kvh=h/gg;for(int s=0;s<S;s++){ i128 den=dps[h*S+s];
77
+ for(int d=0;d<HD;d++){ i128 acc=0; for(int t=0;t<=s;t++) acc+=(i128)dpn[(h*S+s)*S+t]*(i128)dvq[(t*NKV+kvh)*HD+d];
78
+ dattn[s*D+h*HD+d]=(i64)(fdiv(acc*((i128)1<<SC)+(den>>1),den)>>SC);}}}
79
+ for(int i=0;i<S*D;i++) daqa[i]=dattn[i]; i128 aqsq=aqd(daqa,S*D,vsq);
80
+ for(int s=0;s<S;s++)for(int d2=0;d2<D;d2++){i64 a=0;for(int d=0;d<D;d++)a+=daqa[s*D+d]*(i64)ow[d2*D+d]; doacc[s*D+d2]=a;}
81
+ for(int s=0;s<S;s++){ i128 xs=xsc[s]; for(int d2=0;d2<D;d2++){ i128 oc=mul128(aqsq,osc[d2]); i128 r=(oc<<SC)/xs;
82
+ dhout[s*D+d2]=(i64)xv8[s*D+d2]+(i64)ap((i128)doacc[s*D+d2],r);}}
83
+ // ---- SwiGLU MLP (4b-3) ----
84
+ for(int s=0;s<S;s++){ i128 ss=0; for(int d=0;d<D;d++){i128 x=dhout[s*D+d]; ss+=x*x;} i128 mean=ss/D+1;
85
+ i64 inv=(i64)isqrtd(((i128)1<<40)/mean); for(int d=0;d<D;d++) dyg[s*D+d]=dhout[s*D+d]*inv*pav[d]; }
86
+ i128 s0=aqd(dyg,S*D,(i128)1<<SC); for(int i=0;i<S*D;i++) dxn2[i]=dyg[i];
87
+ i128 xn2sq=mul128(mul128(s0,qm(1,11)),pasc);
88
+ i128 gsq=projd(gw,FF,D,gsc,(const i64*)0,0,dxn2,xn2sq,dgate);
89
+ i128 usq=projd(uw,FF,D,usc,(const i64*)0,0,dxn2,xn2sq,dup);
90
+ for(int i=0;i<S*FF;i++){ i128 idx=ap((i128)dgate[i],gsq<<7)+2048; if(idx<0)idx=0; if(idx>4095)idx=4095; dact[i]=dgate[i]*sig[(int)idx]; }
91
+ i128 spre=aqd(dact,S*FF,gsq); i128 asq=spre>>16; // dact now int16
92
+ for(int i=0;i<S*FF;i++) dhmul[i]=dact[i]*dup[i];
93
+ i128 hsq=mul128(asq,usq); i128 hqsq=aqd(dhmul,S*FF,hsq); // dhmul now int16
94
+ i128 msq=projd(dw,D,FF,dsc,(const i64*)0,0,dhmul,hqsq,dacc); // mlp out into dacc [S*D]
95
+ for(int s=0;s<S;s++){ i128 xs=xsc[s]; i128 r=(msq<<SC)/xs; for(int d2=0;d2<D;d2++) hout[s*D+d2]=dhout[s*D+d2]+(i64)ap((i128)dacc[s*D+d2],r); }
96
+ }
97
+
98
+ // host parse helpers
99
+ static void rd_w2d(Buf& b, std::vector<signed char>& w, std::vector<i128>& sq){
100
+ b.u8(); int r=b.u32(), co=b.u32(); w.resize((long)r*co); for(long i=0;i<(long)r*co;i++) w[i]=(signed char)b.u8();
101
+ int ns=b.u32(); sq.resize(ns); for(int i=0;i<ns;i++){i64 m=b.i32(),s=b.i32(); sq[i]=qm(m,s);} }
102
+ static void rd_v1d(Buf& b, std::vector<i64>& v, i128& sq){
103
+ b.u8(); int n=b.u32(); b.u32(); v.resize(n); for(int i=0;i<n;i++) v[i]=b.i32(); b.u32(); i64 m=b.i32(),s=b.i32(); sq=qm(m,s); }
104
+
105
+ int main(int argc,char** argv){
106
+ const char* path=argc>1?argv[1]:"layer0.bin"; FILE* f=fopen(path,"rb"); fseek(f,0,SEEK_END); long n=ftell(f); fseek(f,0,SEEK_SET);
107
+ Buf b; b.b.resize(n); fread(b.b.data(),1,n,f); fclose(f); b.p=8;
108
+ std::vector<i64> gv,qb,kb,vb,pav; i128 gamsc,qbsc,kbsc,vbsc,pasc;
109
+ std::vector<signed char> qw,kw,vw,ow,xv,gw_,uw_,dw_; std::vector<i128> qsc,ksc,vsc,osc,xsc,gsc,usc,dsc;
110
+ rd_v1d(b,gv,gamsc); rd_w2d(b,qw,qsc); rd_v1d(b,qb,qbsc); rd_w2d(b,kw,ksc); rd_v1d(b,kb,kbsc);
111
+ rd_w2d(b,vw,vsc); rd_v1d(b,vb,vbsc); rd_w2d(b,ow,osc); rd_w2d(b,xv,xsc);
112
+ rd_v1d(b,pav,pasc); rd_w2d(b,gw_,gsc); rd_w2d(b,uw_,usc); rd_w2d(b,dw_,dsc);
113
+ std::vector<i64> cosq(S*HD),sinq(S*HD),expl(256),sig(4096);
114
+ for(auto* a:{&cosq,&sinq}) for(auto& x:*a) x=b.i64_(); for(auto& x:expl) x=b.i64_(); for(auto& x:sig) x=b.i64_();
115
+ // upload
116
+ auto up=[&](auto& v){ using T=typename std::decay<decltype(v)>::type::value_type; T* d; cudaMalloc(&d,v.size()*sizeof(T)); cudaMemcpy(d,v.data(),v.size()*sizeof(T),cudaMemcpyHostToDevice); return d; };
117
+ auto *dqw=up(qw),*dkw=up(kw),*dvw=up(vw),*dow=up(ow),*dxv=up(xv);
118
+ auto *dqsc=up(qsc),*dksc=up(ksc),*dvsc=up(vsc),*dosc=up(osc),*dxsc=up(xsc);
119
+ auto *dgv=up(gv),*dqb=up(qb),*dkb=up(kb),*dvb=up(vb),*dcos=up(cosq),*dsin=up(sinq),*dexp=up(expl);
120
+ auto *dpav=up(pav),*dsig=up(sig); // i64*
121
+ auto *dgw=up(gw_),*duw=up(uw_),*ddw=up(dw_); // signed char*
122
+ auto *dgsc=up(gsc),*dusc=up(usc),*ddsc=up(dsc); // i128*
123
+ i64* dhout; cudaMalloc(&dhout,S*D*sizeof(i64));
124
+ run<<<1,1>>>(dqw,dqsc,dqb,qbsc, dkw,dksc,dkb,kbsc, dvw,dvsc,dvb,vbsc, dow,dosc,
125
+ dxv,dxsc,dgv,gamsc, dpav,pasc,dgw,dgsc, duw,dusc,ddw,ddsc, dcos,dsin,dexp,dsig, dhout);
126
+ cudaError_t e=cudaDeviceSynchronize(); if(e){printf("cuda err %s\n",cudaGetErrorString(e));return 1;}
127
+ std::vector<i64> hout(S*D); cudaMemcpy(hout.data(),dhout,S*D*sizeof(i64),cudaMemcpyDeviceToHost);
128
+ unsigned long long h=0xcbf29ce484222325ULL; for(i64 v:hout){ unsigned char* p=(unsigned char*)&v; for(int k=0;k<8;k++){h^=p[k]; h*=0x100000001b3ULL;} }
129
+ cudaDeviceProp pr; cudaGetDeviceProperties(&pr,0);
130
+ printf("backend = NVIDIA CUDA GPU (%s sm_%d%d)\n",pr.name,pr.major,pr.minor);
131
+ printf("block-out FNV= %016llx\n",h);
132
+ printf("TARGET = %016llx\n",TARGET);
133
+ printf("MATCH = %s\n", h==TARGET?"YES (byte-identical to Rust/Python reference)":"NO");
134
+ printf("hout[0..4] = %lld %lld %lld %lld\n",(long long)hout[0],(long long)hout[1],(long long)hout[2],(long long)hout[3]);
135
+ return 0;
136
+ }
docs/evidence/canon_attn.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # inc4b-2a: integer attention block (Qwen2.5-0.5B layer 0) consuming the real `.canon` W8A8 artifact.
3
+ # Validates CORRECTNESS: the integer block (int8 matmuls + integer RMSNorm/RoPE/softmax) matches a full-fp
4
+ # reference computed from the SAME dequantized weights. Scalar scales are carried as fp here (they become
5
+ # fixed-point in the 4b-2b cross-backend port). Heavy tensor compute is all integer.
6
+ import sys, json, struct, numpy as np
7
+
8
+ CANON = sys.argv[1] if len(sys.argv) > 1 else "/root/models/qwen05.canon"
9
+ D, NH, NKV, HD, THETA = 896, 14, 2, 64, 1000000.0
10
+ S = 4 # prefill length
11
+
12
+ def read_canon(path):
13
+ with open(path, "rb") as f:
14
+ assert f.read(8) == b"CANONv1\n"
15
+ n = struct.unpack("<Q", f.read(8))[0]
16
+ hdr = json.loads(f.read(n))
17
+ body = f.read()
18
+ out = {}
19
+ for t in hdr["tensors"]:
20
+ nm, k, shp = t["name"], t["kind"], t["shape"]
21
+ if k == "w2d_int8_pcs":
22
+ o, i = shp
23
+ w = np.frombuffer(body[t["w_off"]:t["w_off"] + o * i], dtype=np.int8).reshape(o, i)
24
+ sc = np.frombuffer(body[t["s_off"]:t["s_off"] + o * 8], dtype=np.int32).reshape(o, 2)
25
+ scale = sc[:, 0].astype(np.float64) * 2.0 ** (sc[:, 1].astype(np.float64) - 31)
26
+ out[nm] = ("w", w, scale)
27
+ else: # v1d_int32_fp
28
+ v = np.frombuffer(body[t["w_off"]:t["w_off"] + shp[0] * 4], dtype=np.int32)
29
+ m0, sh = struct.unpack("<ii", body[t["s_off"]:t["s_off"] + 8])
30
+ out[nm] = ("v", v, m0 * 2.0 ** (sh - 31))
31
+ return out
32
+
33
+ T = read_canon(CANON)
34
+ def wdeq(n): _, w, s = T[n]; return w.astype(np.float64) * s[:, None] # [out,in] fp
35
+ def vdeq(n): _, v, s = T[n]; return v.astype(np.float64) * s # [n] fp
36
+ L = "model.layers.0."
37
+ Wq, Wk, Wv, Wo = wdeq(L+"self_attn.q_proj.weight"), wdeq(L+"self_attn.k_proj.weight"), wdeq(L+"self_attn.v_proj.weight"), wdeq(L+"self_attn.o_proj.weight")
38
+ bq, bk, bv = vdeq(L+"self_attn.q_proj.bias"), vdeq(L+"self_attn.k_proj.bias"), vdeq(L+"self_attn.v_proj.bias")
39
+ gin = vdeq(L+"input_layernorm.weight")
40
+ # input hidden states = real embedding rows for a few token ids
41
+ Wemb, semb = T["model.embed_tokens.weight"][1], T["model.embed_tokens.weight"][2]
42
+ ids = [9707, 11, 1879, 30][:S]
43
+ X = np.stack([Wemb[i].astype(np.float64) * semb[i] for i in ids]) # [S, D]
44
+
45
+ # ---------- RoPE tables (offline fp -> pinned fixed-point Q15) ----------
46
+ inv_freq = 1.0 / (THETA ** (np.arange(0, HD, 2) / HD)) # [HD/2]
47
+ pos = np.arange(S)
48
+ ang = np.outer(pos, inv_freq) # [S, HD/2]
49
+ cos_h, sin_h = np.cos(ang), np.sin(ang)
50
+ cos_f = np.concatenate([cos_h, cos_h], axis=1) # [S, HD]
51
+ sin_f = np.concatenate([sin_h, sin_h], axis=1)
52
+ cosQ = np.clip(np.rint(cos_f * 32768), -32768, 32767).astype(np.int64)
53
+ sinQ = np.clip(np.rint(sin_f * 32768), -32768, 32767).astype(np.int64)
54
+ def rot_half_fp(x): return np.concatenate([-x[..., HD//2:], x[..., :HD//2]], axis=-1)
55
+ def rope_fp(x): # x [S,H,HD]
56
+ return x * cos_f[:, None, :] + rot_half_fp(x) * sin_f[:, None, :]
57
+ def rope_int(xq): # xq int8 [S,H,HD] -> int8, scale preserved
58
+ rh = np.concatenate([-xq[..., HD//2:].astype(np.int64), xq[..., :HD//2].astype(np.int64)], axis=-1)
59
+ v = (xq.astype(np.int64) * cosQ[:, None, :] + rh * sinQ[:, None, :] + 16384) >> 15
60
+ return np.clip(v, -127, 127).astype(np.int8)
61
+
62
+ def qdyn(Tf): # dynamic per-tensor int8 quant of fp tensor -> (int8, scale)
63
+ a = float(np.max(np.abs(Tf))) or 1.0
64
+ s = a / 127.0
65
+ return np.clip(np.rint(Tf / s), -127, 127).astype(np.int8), s
66
+ def imatmul(aq, wq): return aq.astype(np.int64) @ wq.astype(np.int64).T # [S,in]x[out,in]->[S,out]
67
+ def rms_int(xq): # int8 [S,D] -> normalized int8 [S,D] + scale (gamma folded), scale-invariant
68
+ ss = np.sum(xq.astype(np.int64)**2, axis=1) # [S]
69
+ mean = ss // D + 1
70
+ inv = np.floor(np.sqrt((1 << 32) / mean)).astype(np.int64) # ~2^16/sqrt(mean) per token
71
+ y = (xq.astype(np.int64) * inv[:, None]) >> 16 # x/rms in ~int scale
72
+ yg = y * np.rint(gin * (1 << 12)).astype(np.int64) # * gamma(Q12)
73
+ outs = 1.0 / (1 << 12)
74
+ yq, s2 = qdyn(yg.astype(np.float64))
75
+ return yq, s2 * outs # scale bookkeeping (fp scalar; fixed-point in 4b-2b)
76
+
77
+ # ---------- integer block ----------
78
+ Xq, Xs = qdyn(X)
79
+ xn_q, xn_s = rms_int(Xq)
80
+ # QKV: acc = xn_q @ Wq_int^T ; dequant-ish scale = xn_s * wq_scale[o]; + bias ; then reshape
81
+ def proj_int(xn_q, xn_s, wname, bfp):
82
+ _, wq, wsc = T[wname]
83
+ acc = imatmul(xn_q, wq) # [S, out] int64
84
+ outf = acc.astype(np.float64) * (xn_s * wsc)[None, :] + bfp[None, :] # to fp (scalar scales fp here)
85
+ return outf
86
+ qf = proj_int(xn_q, xn_s, L+"self_attn.q_proj.weight", bq).reshape(S, NH, HD)
87
+ kf = proj_int(xn_q, xn_s, L+"self_attn.k_proj.weight", bk).reshape(S, NKV, HD)
88
+ vf = proj_int(xn_q, xn_s, L+"self_attn.v_proj.weight", bv).reshape(S, NKV, HD)
89
+ # quantize q,k,v to int8 for integer RoPE + attention
90
+ qq, qs = qdyn(qf); kq, ks = qdyn(kf); vq, vs = qdyn(vf)
91
+ qq, kq = rope_int(qq), rope_int(kq)
92
+ # GQA expand kv -> NH heads
93
+ kq14 = np.repeat(kq, NH // NKV, axis=1); vq14 = np.repeat(vq, NH // NKV, axis=1)
94
+ # scores[h,s,t] = sum_d qq[s,h,d]*kq14[t,h,d]
95
+ scores = np.einsum('shd,thd->hst', qq.astype(np.int64), kq14.astype(np.int64)) # int
96
+ # integer softmax over t with causal mask; temperature C = qs*ks/sqrt(HD)
97
+ C = qs * ks / np.sqrt(HD)
98
+ # exp LUT (integer Horner Q16), same as inc4a
99
+ def build_exp_lut(nterms=28):
100
+ ONE = 1 << 16; t = np.zeros(256, np.int64)
101
+ for d in range(256):
102
+ x = -d * (ONE // 64); e = ONE; k = nterms
103
+ while k >= 1: e = ONE + ((x * e) // ONE) // k; k -= 1
104
+ t[d] = max(0, e)
105
+ return t
106
+ EXP = build_exp_lut()
107
+ # accuracy check of the integer exp-LUT vs true exp
108
+ _dd = np.arange(256); _true = np.exp(-_dd / 64) * (1 << 16)
109
+ print(f"exp-LUT max rel err vs true exp = {np.max(np.abs(EXP - _true) / np.maximum(_true,1)):.4f}")
110
+ probs = np.zeros((NH, S, S), np.float64)
111
+ for h in range(NH):
112
+ for s in range(S):
113
+ row = scores[h, s, :s+1]
114
+ m = row.max()
115
+ d = np.clip(((m - row).astype(np.float64) * C * 64).round().astype(int), 0, 255) # C*gap in exp-arg units
116
+ e = EXP[d]; sm = int(e.sum())
117
+ p = (e.astype(np.int64) << 16) // sm
118
+ probs[h, s, :s+1] = p / (1 << 16)
119
+ # DEBUG bisect
120
+ def _rel(a, b): return float(np.sqrt(np.sum((a-b)**2) / (np.sum(b*b)+1e-12)))
121
+ _fp_probs = np.zeros_like(probs)
122
+ for h in range(NH):
123
+ for s in range(S):
124
+ r = scores[h, s, :s+1].astype(float) * C
125
+ p = np.exp(r - r.max()); p /= p.sum(); _fp_probs[h, s, :s+1] = p
126
+ print(f" dbg int-softmax vs fp-softmax(same scores) = {_rel(probs, _fp_probs):.4f}")
127
+ print(f" dbg q post-rope int-deq vs fp-rope(same qf)= {_rel(qq.astype(float)*qs, rope_fp(qf)):.4f}")
128
+ print(f" dbg C={C:.3e} scores range=[{scores.min()},{scores.max()}] probs[0,3]={np.round(probs[0,3],3)}")
129
+ # A.V : out[s,h,d] = sum_t probs[h,s,t]*vf[t,h,d] (fp probs * fp v carry the value scale)
130
+ vf14 = np.repeat(vf.reshape(S, NKV, HD), NH // NKV, axis=1) # [S, NH, HD]
131
+ attn = np.einsum('hst,thd->shd', probs, vf14).reshape(S, D)
132
+ # o_proj (integer matmul)
133
+ aq, as_ = qdyn(attn)
134
+ _, wo, wosc = T[L+"self_attn.o_proj.weight"]
135
+ o = (imatmul(aq, wo).astype(np.float64) * (as_ * wosc)[None, :])
136
+ h_out_int = X + o
137
+
138
+ # ---------- full-fp reference (same dequantized weights) ----------
139
+ def rms_fp(h, g): return h / np.sqrt(np.mean(h*h, axis=1, keepdims=True) + 1e-6) * g
140
+ xn = rms_fp(X, gin)
141
+ q = (xn @ Wq.T + bq).reshape(S, NH, HD); k = (xn @ Wk.T + bk).reshape(S, NKV, HD); v = (xn @ Wv.T + bv).reshape(S, NKV, HD)
142
+ q, k = rope_fp(q), rope_fp(k)
143
+ k14 = np.repeat(k, NH//NKV, axis=1); v14 = np.repeat(v, NH//NKV, axis=1)
144
+ sc = np.einsum('shd,thd->hst', q, k14) / np.sqrt(HD)
145
+ for h in range(NH):
146
+ for s in range(S):
147
+ sc[h, s, s+1:] = -1e30
148
+ attn_fp = np.zeros((S, NH, HD))
149
+ for h in range(NH):
150
+ p = np.exp(sc[h] - sc[h].max(axis=1, keepdims=True)); p /= p.sum(axis=1, keepdims=True)
151
+ attn_fp[:, h, :] = p @ v14[:, h, :]
152
+ o_fp = attn_fp.reshape(S, D) @ Wo.T
153
+ h_out_fp = X + o_fp
154
+
155
+ # o_semi: int-PROJECTED q/k/v (fp values) run through FP attention -> isolates projection-quant amplification
156
+ qsr, ksr = rope_fp(qf), rope_fp(kf)
157
+ k14_ = np.repeat(ksr, NH//NKV, 1); v14_ = np.repeat(vf, NH//NKV, 1)
158
+ sc_ = np.einsum('shd,thd->hst', qsr, k14_) / np.sqrt(HD)
159
+ for h in range(NH):
160
+ for s in range(S): sc_[h, s, s+1:] = -1e30
161
+ ao = np.zeros((S, NH, HD))
162
+ for h in range(NH):
163
+ p = np.exp(sc_[h] - sc_[h].max(1, keepdims=True)); p /= p.sum(1, keepdims=True); ao[:, h, :] = p @ v14_[:, h, :]
164
+ o_semi = ao.reshape(S, D) @ Wo.T
165
+ # weight-only: fp activations xn @ int8-weight(dequant) -> isolates WEIGHT quant from ACTIVATION quant
166
+ def proj_wonly(xn_fp, wname, bfp):
167
+ _, wq, wsc = T[wname]; return xn_fp @ (wq.astype(np.float64) * wsc[:, None]).T + bfp
168
+ qw = proj_wonly(xn, L+"self_attn.q_proj.weight", bq).reshape(S, NH, HD)
169
+ kw = proj_wonly(xn, L+"self_attn.k_proj.weight", bk).reshape(S, NKV, HD)
170
+ vw = proj_wonly(xn, L+"self_attn.v_proj.weight", bv).reshape(S, NKV, HD)
171
+ qwr, kwr = rope_fp(qw), rope_fp(kw); k14w = np.repeat(kwr, NH//NKV, 1); v14w = np.repeat(vw, NH//NKV, 1)
172
+ scw = np.einsum('shd,thd->hst', qwr, k14w) / np.sqrt(HD)
173
+ for h in range(NH):
174
+ for s in range(S): scw[h, s, s+1:] = -1e30
175
+ aw = np.zeros((S, NH, HD))
176
+ for h in range(NH):
177
+ p = np.exp(scw[h] - scw[h].max(1, keepdims=True)); p /= p.sum(1, keepdims=True); aw[:, h, :] = p @ v14w[:, h, :]
178
+ o_wonly = aw.reshape(S, D) @ Wo.T
179
+ # q per-tensor quant error (dequant vs qf)
180
+ print(f" dbg o_semi(W8A8-proj, fp-attn) vs o_fp = {_rel(o_semi, o_fp):.4f} o_wonly(W8 only, fp-act) vs o_fp = {_rel(o_wonly, o_fp):.4f}")
181
+
182
+ def rel(a, b): return float(np.sqrt(np.sum((a-b)**2) / (np.sum(b*b)+1e-12)))
183
+ print(f"tokens={ids} S={S}")
184
+ print(f"rel err qkv(q) = {rel((imatmul(xn_q,T[L+'self_attn.q_proj.weight'][1]).astype(float)*(xn_s*T[L+'self_attn.q_proj.weight'][2])+bq), (xn@Wq.T+bq)):.4f}")
185
+ print(f"rel err o (attn) = {rel(o, o_fp):.4f} (attention output only) ||o||/||X||={np.linalg.norm(o)/np.linalg.norm(X):.3f}")
186
+ print(f"rel err block out = {rel(h_out_int, h_out_fp):.4f} (int block vs full-fp, same quantized weights)")
187
+ print(f"h_out_int[0,:4] = {np.round(h_out_int[0,:4],4)}")
188
+ print(f"h_out_fp [0,:4] = {np.round(h_out_fp [0,:4],4)}")
docs/evidence/canon_attn.rs ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // inc4b-2b: Rust (CPU) port of the fully-integer attention block. Reads layer0.bin (flat, no JSON), runs
2
+ // the block with the EXACT integer semantics of canon_attn_int.py (i128 scales, i64 values, floor division
3
+ // where Python uses //, arithmetic >>). Must reproduce the reference fingerprint dea9a75ba85b1352.
4
+ //
5
+ // Build/run: rustc -O canon_attn.rs -o canon_attn && ./canon_attn layer0.bin
6
+
7
+ use std::io::Read;
8
+
9
+ const SC: u32 = 30;
10
+ const HALF: i128 = 1 << 29;
11
+ const ALVL: i128 = 32767;
12
+ const D: usize = 896;
13
+ const NH: usize = 14;
14
+ const NKV: usize = 2;
15
+ const HD: usize = 64;
16
+ const S: usize = 4;
17
+ const TARGET: u64 = 0x906729150a5a926a; // full-layer (4b-3)
18
+
19
+ fn qm(m0: i64, shift: i64) -> i128 { let e = shift - 1; if e >= 0 { (m0 as i128) << e } else { ((m0 as i128) + (1i128 << (-e - 1))) >> (-e) } }
20
+ fn apply(acc: i128, sq: i128) -> i128 { (acc * sq + HALF) >> SC }
21
+ fn mul(a: i128, b: i128) -> i128 { (a * b + HALF) >> SC }
22
+ fn scale_from(amax: i128, cur: i128) -> i128 { (amax * cur) / ALVL }
23
+ fn fdiv(a: i128, b: i128) -> i128 { let q = a / b; let r = a % b; if r != 0 && (r < 0) != (b < 0) { q - 1 } else { q } } // floor
24
+ fn isqrt(n: i128) -> i128 { if n <= 0 { return 0; } let mut x = n; let mut y = (x + 1) / 2; while y < x { x = y; y = (x + n / x) / 2; } x }
25
+ fn aq(v: &[i64], cur: i128) -> (Vec<i64>, i128) {
26
+ let am = v.iter().map(|x| x.abs()).max().unwrap().max(1) as i128;
27
+ let q = v.iter().map(|&x| { let xi = x as i128; let sign = if xi > 0 { am } else if xi < 0 { -am } else { 0 }; fdiv(xi * ALVL * 2 + sign, 2 * am) as i64 }).collect();
28
+ (q, scale_from(am, cur))
29
+ }
30
+ fn fnv(bytes: &[u8]) -> u64 { let mut h = 0xcbf29ce484222325u64; for &b in bytes { h ^= b as u64; h = h.wrapping_mul(0x100000001b3); } h }
31
+
32
+ struct Cur<'a> { b: &'a [u8], p: usize }
33
+ impl<'a> Cur<'a> {
34
+ fn u8(&mut self) -> u8 { let v = self.b[self.p]; self.p += 1; v }
35
+ fn u32(&mut self) -> u32 { let v = u32::from_le_bytes(self.b[self.p..self.p + 4].try_into().unwrap()); self.p += 4; v }
36
+ fn i32(&mut self) -> i32 { let v = i32::from_le_bytes(self.b[self.p..self.p + 4].try_into().unwrap()); self.p += 4; v }
37
+ fn i64(&mut self) -> i64 { let v = i64::from_le_bytes(self.b[self.p..self.p + 8].try_into().unwrap()); self.p += 8; v }
38
+ }
39
+ // W2d: int8 weights [rows*cols] + per-row Q30 scale. V1d: int32 [n] + single Q30 scale.
40
+ struct W2d { rows: usize, cols: usize, w: Vec<i8>, sq: Vec<i128> }
41
+ struct V1d { v: Vec<i64>, sq: i128 }
42
+ fn rd_w2d(c: &mut Cur) -> W2d {
43
+ assert_eq!(c.u8(), 0); let rows = c.u32() as usize; let cols = c.u32() as usize;
44
+ let w: Vec<i8> = (0..rows * cols).map(|_| c.u8() as i8).collect();
45
+ let ns = c.u32() as usize; let sq = (0..ns).map(|_| { let m0 = c.i32() as i64; let sh = c.i32() as i64; qm(m0, sh) }).collect();
46
+ W2d { rows, cols, w, sq }
47
+ }
48
+ fn rd_v1d(c: &mut Cur) -> V1d {
49
+ assert_eq!(c.u8(), 1); let n = c.u32() as usize; let _cols = c.u32();
50
+ let v: Vec<i64> = (0..n).map(|_| c.i32() as i64).collect();
51
+ let _ns = c.u32(); let m0 = c.i32() as i64; let sh = c.i32() as i64;
52
+ V1d { v, sq: qm(m0, sh) }
53
+ }
54
+
55
+ fn proj(w: &W2d, b: &V1d, xn_q: &[i64], xn_sq: i128) -> (Vec<i64>, i128) {
56
+ let (oc, inn) = (w.rows, w.cols);
57
+ let mut acc = vec![0i64; S * oc];
58
+ for s in 0..S { for o in 0..oc { let mut a = 0i64; for d in 0..inn { a += xn_q[s * inn + d] * (w.w[o * inn + d] as i64); } acc[s * oc + o] = a; } }
59
+ let colsq: Vec<i128> = (0..oc).map(|o| mul(xn_sq, w.sq[o])).collect();
60
+ let bacc: Vec<i64> = (0..oc).map(|o| { let cs = colsq[o]; if cs == 0 { 0 } else { let num = (b.v[o] as i128) * b.sq * (1i128 << SC) + (cs >> 1); (fdiv(num, cs) >> SC) as i64 } }).collect();
61
+ let cmax = *colsq.iter().max().unwrap();
62
+ let ratio: Vec<i128> = (0..oc).map(|o| (colsq[o] << SC) / cmax).collect();
63
+ let mut resc = vec![0i64; S * oc];
64
+ for s in 0..S { for o in 0..oc { let ab = (acc[s * oc + o] as i128) + (bacc[o] as i128); resc[s * oc + o] = apply(ab, ratio[o]) as i64; } }
65
+ aq(&resc, cmax)
66
+ }
67
+ fn rmsnorm(hv: &[i64], gam: &V1d) -> (Vec<i64>, i128) {
68
+ let mut yg = vec![0i64; S * D];
69
+ for s in 0..S {
70
+ let ss: i128 = (0..D).map(|d| { let x = hv[s * D + d] as i128; x * x }).sum();
71
+ let mean = ss / (D as i128) + 1;
72
+ let inv = isqrt((1i128 << 40) / mean) as i64;
73
+ for d in 0..D { yg[s * D + d] = hv[s * D + d] * inv * gam.v[d]; }
74
+ }
75
+ let (q, s0) = aq(&yg, 1i128 << SC);
76
+ (q, mul(mul(s0, qm(1, 11)), gam.sq))
77
+ }
78
+ fn proj_nb(w: &W2d, xn_q: &[i64], xn_sq: i128) -> (Vec<i64>, i128) { // W8A16 GEMM, no bias
79
+ let (oc, inn) = (w.rows, w.cols);
80
+ let mut acc = vec![0i64; S * oc];
81
+ for s in 0..S { for o in 0..oc { let mut a = 0i64; for d in 0..inn { a += xn_q[s * inn + d] * (w.w[o * inn + d] as i64); } acc[s * oc + o] = a; } }
82
+ let colsq: Vec<i128> = (0..oc).map(|o| mul(xn_sq, w.sq[o])).collect();
83
+ let cmax = *colsq.iter().max().unwrap();
84
+ let ratio: Vec<i128> = (0..oc).map(|o| (colsq[o] << SC) / cmax).collect();
85
+ let mut resc = vec![0i64; S * oc];
86
+ for s in 0..S { for o in 0..oc { resc[s * oc + o] = apply(acc[s * oc + o] as i128, ratio[o]) as i64; } }
87
+ aq(&resc, cmax)
88
+ }
89
+ fn rope(x: &mut [i64], nh: usize, cosq: &[i64], sinq: &[i64]) {
90
+ let src = x.to_vec();
91
+ for s in 0..S { for h in 0..nh { for d in 0..HD {
92
+ let base = (s * nh + h) * HD;
93
+ let rh = if d < HD / 2 { -src[base + d + HD / 2] } else { src[base + d - HD / 2] };
94
+ x[base + d] = (src[base + d] * cosq[s * HD + d] + rh * sinq[s * HD + d] + 16384) >> 15;
95
+ } } }
96
+ }
97
+
98
+ fn main() {
99
+ let path = std::env::args().nth(1).unwrap_or("layer0.bin".into());
100
+ let mut buf = Vec::new(); std::fs::File::open(&path).unwrap().read_to_end(&mut buf).unwrap();
101
+ assert_eq!(&buf[0..8], b"L0BIN\0\0\0");
102
+ let mut c = Cur { b: &buf, p: 8 };
103
+ let gam = rd_v1d(&mut c);
104
+ let qw = rd_w2d(&mut c); let qb = rd_v1d(&mut c);
105
+ let kw = rd_w2d(&mut c); let kb = rd_v1d(&mut c);
106
+ let vw = rd_w2d(&mut c); let vb = rd_v1d(&mut c);
107
+ let ow = rd_w2d(&mut c);
108
+ let xt = rd_w2d(&mut c); // X: [S, D] int8 + per-row scale
109
+ let pan = rd_v1d(&mut c); // post_attention_layernorm
110
+ let gw = rd_w2d(&mut c); let uw = rd_w2d(&mut c); let dw = rd_w2d(&mut c); // mlp gate/up/down
111
+ let cosq: Vec<i64> = (0..S * HD).map(|_| c.i64()).collect();
112
+ let sinq: Vec<i64> = (0..S * HD).map(|_| c.i64()).collect();
113
+ let exp: Vec<i64> = (0..256).map(|_| c.i64()).collect();
114
+ let sig: Vec<i64> = (0..4096).map(|_| c.i64()).collect();
115
+
116
+ let xv: Vec<i64> = xt.w.iter().map(|&x| x as i64).collect(); // [S*D]
117
+ let xsq: Vec<i128> = xt.sq.clone(); // [S] per-token
118
+
119
+ let (xn_q, xn_sq) = rmsnorm(&xv, &gam);
120
+
121
+ let (mut qq, qsq) = proj(&qw, &qb, &xn_q, xn_sq); // [S*NH*HD]
122
+ let (mut kq, ksq) = proj(&kw, &kb, &xn_q, xn_sq); // [S*NKV*HD]
123
+ let (vq, vsq) = proj(&vw, &vb, &xn_q, xn_sq);
124
+ rope(&mut qq, NH, &cosq, &sinq);
125
+ rope(&mut kq, NKV, &cosq, &sinq);
126
+
127
+ // scores[h][s][t] = sum_d qq[s,h,d]*kq[t,kvh,d]
128
+ let g = NH / NKV;
129
+ let mut scores = vec![0i64; NH * S * S];
130
+ for h in 0..NH { let kvh = h / g; for s in 0..S { for t in 0..S {
131
+ let mut a = 0i64; for d in 0..HD { a += qq[(s * NH + h) * HD + d] * kq[(t * NKV + kvh) * HD + d]; }
132
+ scores[(h * S + s) * S + t] = a;
133
+ } } }
134
+ let c8 = mul(qsq, ksq) * 8;
135
+ let mut pn = vec![0i64; NH * S * S]; let mut ps = vec![0i64; NH * S];
136
+ for h in 0..NH { for s in 0..S {
137
+ let mut m = i64::MIN; for t in 0..=s { m = m.max(scores[(h * S + s) * S + t]); }
138
+ let mut sm = 0i64;
139
+ for t in 0..=s {
140
+ let mut dd = apply((m - scores[(h * S + s) * S + t]) as i128, c8);
141
+ if dd < 0 { dd = 0; } if dd > 255 { dd = 255; }
142
+ let e = exp[dd as usize]; pn[(h * S + s) * S + t] = e; sm += e;
143
+ }
144
+ ps[h * S + s] = sm;
145
+ } }
146
+ // A.V -> attn[s][h*HD+d]
147
+ let mut attn = vec![0i64; S * D];
148
+ for h in 0..NH { let kvh = h / g; for s in 0..S {
149
+ let den = ps[h * S + s] as i128;
150
+ for d in 0..HD {
151
+ let mut acc = 0i128; for t in 0..=s { acc += (pn[(h * S + s) * S + t] as i128) * (vq[(t * NKV + kvh) * HD + d] as i128); }
152
+ attn[s * D + h * HD + d] = (fdiv(acc * (1i128 << SC) + (den >> 1), den) >> SC) as i64;
153
+ }
154
+ } }
155
+ let (aq_attn, aq_attn_sq) = aq(&attn, vsq);
156
+ // o_proj + residual
157
+ let mut oacc = vec![0i64; S * D];
158
+ for s in 0..S { for d2 in 0..D { let mut a = 0i64; for d in 0..D { a += aq_attn[s * D + d] * (ow.w[d2 * D + d] as i64); } oacc[s * D + d2] = a; } }
159
+ let ocsq: Vec<i128> = (0..D).map(|d2| mul(aq_attn_sq, ow.sq[d2])).collect();
160
+ let mut hout = vec![0i64; S * D];
161
+ for s in 0..S { let xs = xsq[s]; for d2 in 0..D { let ratio = (ocsq[d2] << SC) / xs; hout[s * D + d2] = xv[s * D + d2] + apply(oacc[s * D + d2] as i128, ratio) as i64; } }
162
+
163
+ // ---- SwiGLU MLP sub-block (4b-3) ----
164
+ let (xn2_q, xn2_sq) = rmsnorm(&hout, &pan);
165
+ let (gate_q, gate_sq) = proj_nb(&gw, &xn2_q, xn2_sq); // [S*FF]
166
+ let (up_q, up_sq) = proj_nb(&uw, &xn2_q, xn2_sq);
167
+ let ff = gw.rows;
168
+ let mut act_int = vec![0i64; S * ff];
169
+ for i in 0..S * ff { let mut idx = apply(gate_q[i] as i128, gate_sq << 7) + 2048; if idx < 0 { idx = 0; } if idx > 4095 { idx = 4095; } act_int[i] = gate_q[i] * sig[idx as usize]; }
170
+ let (act_q, spre) = aq(&act_int, gate_sq); let act_sq = spre >> 16;
171
+ let mut hmul = vec![0i64; S * ff];
172
+ for i in 0..S * ff { hmul[i] = act_q[i] * up_q[i]; }
173
+ let hmul_sq = mul(act_sq, up_sq);
174
+ let (hmul_q, hmul_q_sq) = aq(&hmul, hmul_sq);
175
+ let (mlp_q, mlp_sq) = proj_nb(&dw, &hmul_q, hmul_q_sq); // [S*D]
176
+ let mut layer = vec![0i64; S * D];
177
+ for s in 0..S { let ratio = (mlp_sq << SC) / xsq[s]; for d2 in 0..D { layer[s * D + d2] = hout[s * D + d2] + apply(mlp_q[s * D + d2] as i128, ratio) as i64; } }
178
+
179
+ let bytes: Vec<u8> = layer.iter().flat_map(|v| v.to_le_bytes()).collect();
180
+ let fp = fnv(&bytes);
181
+ println!("backend = Rust CPU ({})", std::env::consts::ARCH);
182
+ println!("layer-out FNV= {:016x}", fp);
183
+ println!("TARGET = {:016x}", TARGET);
184
+ println!("MATCH = {}", if fp == TARGET { "YES (byte-identical to Python integer reference)" } else { "NO" });
185
+ println!("layer[0..4] = {:?}", &layer[0..4]);
186
+ let _ = hmul_q_sq;
187
+ }
docs/evidence/canon_attn_int.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # inc4b-2b reference: FULLY-INTEGER attention block (Qwen2.5-0.5B layer 0). No fp on the compute path —
3
+ # all scales are Q30 fixed-point integers. This is the exact spec + target fingerprint the Rust/Metal/CUDA
4
+ # ports must reproduce byte-for-byte. Also checks accuracy is preserved (~2.6% vs fp) and prints an FNV over
5
+ # the integer block output.
6
+ import sys, json, struct, numpy as np
7
+ CANON = sys.argv[1] if len(sys.argv) > 1 else "/root/models/qwen05.canon"
8
+ D, NH, NKV, HD, THETA, S = 896, 14, 2, 64, 1000000.0, 4
9
+ ALVL = 32767 # int16 activations (W8A16)
10
+ SC = 30 # scale fixed-point bits (Q30)
11
+ HALF = 1 << (SC - 1)
12
+
13
+ def read_canon(path):
14
+ f = open(path, "rb"); assert f.read(8) == b"CANONv1\n"
15
+ n = struct.unpack("<Q", f.read(8))[0]; hdr = json.loads(f.read(n)); body = f.read(); out = {}
16
+ for t in hdr["tensors"]:
17
+ nm, k, shp = t["name"], t["kind"], t["shape"]
18
+ if k == "w2d_int8_pcs":
19
+ o, i = shp
20
+ w = np.frombuffer(body[t["w_off"]:t["w_off"]+o*i], np.int8).reshape(o, i).astype(np.int64)
21
+ sc = np.frombuffer(body[t["s_off"]:t["s_off"]+o*8], np.int32).reshape(o, 2)
22
+ sq = np.array([qm_from_fixed(int(a), int(b)) for a, b in sc], np.int64) # per-out-ch scale in Q30
23
+ out[nm] = ("w", w, sq)
24
+ else:
25
+ v = np.frombuffer(body[t["w_off"]:t["w_off"]+shp[0]*4], np.int32).astype(np.int64)
26
+ m0, sh = struct.unpack("<ii", body[t["s_off"]:t["s_off"]+8])
27
+ out[nm] = ("v", v, qm_from_fixed(m0, sh))
28
+ return out
29
+
30
+ def qm_from_fixed(m0, shift): # gemmlowp (m0, shift) -> Q30 integer scale = round(m0*2^(shift-31) * 2^30)
31
+ e = shift - 1 # scale = m0 * 2^(shift-31); *2^30 => m0 * 2^(shift-1)
32
+ if e >= 0: return (m0 << e)
33
+ return (m0 + (1 << (-e - 1))) >> (-e) # round
34
+ def apply(acc, sq): return (acc * sq + HALF) >> SC # round(acc * scale)
35
+ def mul(a, b): return (a * b + HALF) >> SC # Q30 * Q30 -> Q30
36
+ def scale_from(amax, cur_sq): return (amax * cur_sq) // ALVL # new abs scale after requant to ALVL levels
37
+ def aq(v, cur_sq): # v: int64 array, cur_sq: Q30 abs scale -> (q int64 in +-ALVL, new_sq Q30)
38
+ amax = int(np.max(np.abs(v))) or 1
39
+ q = (v * ALVL * 2 + np.sign(v) * amax) // (2 * amax)
40
+ return q.astype(np.int64), scale_from(amax, cur_sq)
41
+
42
+ # RoPE Q15 tables (offline)
43
+ inv_freq = 1.0/(THETA**(np.arange(0, HD, 2)/HD)); ang = np.outer(np.arange(S), inv_freq)
44
+ cosF = np.concatenate([np.cos(ang)]*2, 1); sinF = np.concatenate([np.sin(ang)]*2, 1)
45
+ cosQ = np.clip(np.rint(cosF*32768), -32768, 32767).astype(np.int64); sinQ = np.clip(np.rint(sinF*32768), -32768, 32767).astype(np.int64)
46
+ def rope_int(xq):
47
+ rh = np.concatenate([-xq[..., HD//2:], xq[..., :HD//2]], -1)
48
+ return (xq*cosQ[:, None, :] + rh*sinQ[:, None, :] + 16384) >> 15 # scale preserved
49
+ def build_exp(n=28):
50
+ ONE = 1 << 16; t = np.zeros(256, np.int64)
51
+ for d in range(256):
52
+ x = -d*(ONE//64); e = ONE; k = n
53
+ while k >= 1: e = ONE+((x*e)//ONE)//k; k -= 1
54
+ t[d] = max(0, e)
55
+ return t
56
+ EXP = build_exp()
57
+ def fnv(b):
58
+ h = 0xcbf29ce484222325
59
+ for x in b: h = ((h ^ (x & 0xff)) * 0x100000001b3) & 0xFFFFFFFFFFFFFFFF
60
+ return h
61
+
62
+ T = read_canon(CANON)
63
+ L = "model.layers.0."
64
+ Wemb, Wemb_sq = T["model.embed_tokens.weight"][1], T["model.embed_tokens.weight"][2]
65
+ ids = [9707, 11, 1879, 30][:S]
66
+ # input residual stream: int8 embed values + per-token Q30 scale
67
+ Xv = np.stack([Wemb[i] for i in ids]).astype(np.int64) # [S,D] int8 values
68
+ Xsq = np.array([Wemb_sq[i] for i in ids], np.int64) # [S] Q30
69
+
70
+ def imm(a, w): return a @ w.T # int64 matmul
71
+
72
+ # ---- fully-integer block ----
73
+ # RMSNorm on Xv (scale-invariant): normalize then *gamma; keep Q20 precision
74
+ gW = T[L+"input_layernorm.weight"] # ("v", vint, sq)
75
+ gam_v, gam_sq = gW[1], gW[2]
76
+ def isqrt(n): # integer floor sqrt (Newton) — fp-free
77
+ if n <= 0: return 0
78
+ x = int(n); y = (x + 1) // 2
79
+ while y < x: x = y; y = (x + int(n) // x) // 2
80
+ return x
81
+ ss = np.sum(Xv**2, 1); mean = ss//D + 1
82
+ inv = np.array([isqrt((1 << 40)//int(m)) for m in mean], np.int64) # ~2^20/sqrt(mean), integer
83
+ ynorm = Xv * inv[:, None] # normalize(Xv) in Q20
84
+ yg = ynorm * gam_v[None, :] # * gamma_int (gamma true = gam_v*gam_sq)
85
+ # yg true scale = 2^-20 (from ynorm Q20) * gam_sq ; quantize yg to int16
86
+ xn_q, xn_sq0 = aq(yg, 1 << SC) # treat yg as raw ints, scale rel to yg units
87
+ # fold in the true unit scale of yg: true_rms_out = yg * 2^-20 * gam_sq -> multiply xn scale
88
+ xn_sq = mul(mul(xn_sq0, qm_from_fixed(1, 31 - 20)), gam_sq) # * 2^-20 * gam_sq (2^-20 as (m0=1<<30,shift=31-20))
89
+
90
+ def proj(wname, bname):
91
+ _, wv, wsq = T[wname]; acc = imm(xn_q, wv) # [S,out] int
92
+ bv, bsq = T[bname][1], T[bname][2]
93
+ out = np.zeros_like(acc)
94
+ for o in range(acc.shape[1]):
95
+ os = mul(xn_sq, int(wsq[o])) # acc scale = xn_sq*wsq[o]
96
+ # bias true = bv[o]*bsq ; in acc-scale units: bacc = round(bv[o]*bsq / os)
97
+ bacc = (int(bv[o]) * bsq * (1 << SC) + (os >> 1)) // os >> SC if os else 0
98
+ out[:, o] = acc[:, o] + bacc
99
+ out_sq_o = os
100
+ # per-channel scale differs; requant each column to a common int16 rep with per-tensor scale
101
+ # true[s,o] = out[s,o]*os[o]; dequant to compare -> but for downstream keep as (values, per-col scale)
102
+ return acc, wsq, np.array([mul(xn_sq, int(wsq[o])) for o in range(acc.shape[1])], np.int64), bv, bsq
103
+
104
+ # NOTE: projection produces per-output-channel scales; fold into a single int16 tensor for attention below.
105
+ def proj_i16(wname, bname, nheads):
106
+ _, wv, wsq = T[wname]; acc = imm(xn_q, wv) # [S,out] int64
107
+ bv, bsq = T[bname][1], int(T[bname][2])
108
+ OUT = acc.shape[1]
109
+ colsq = [mul(int(xn_sq), int(wsq[o])) for o in range(OUT)] # per-col Q30 (python ints)
110
+ bacc = [(((int(bv[o])*bsq*(1 << SC) + (cs >> 1)) // cs) >> SC) if (cs := colsq[o]) else 0
111
+ for o in range(OUT)]
112
+ accb = acc + np.array(bacc, np.int64)[None, :]
113
+ cmax = max(colsq)
114
+ rescaled = np.empty_like(acc)
115
+ for o in range(OUT):
116
+ rescaled[:, o] = apply(accb[:, o], (colsq[o] << SC) // cmax) # -> common scale cmax
117
+ q, sq = aq(rescaled.reshape(-1), cmax); q = q.reshape(acc.shape)
118
+ return q.reshape(S, nheads, HD), sq
119
+
120
+ qq, qsq = proj_i16(L+"self_attn.q_proj.weight", L+"self_attn.q_proj.bias", NH)
121
+ kq, ksq = proj_i16(L+"self_attn.k_proj.weight", L+"self_attn.k_proj.bias", NKV)
122
+ vq, vsq = proj_i16(L+"self_attn.v_proj.weight", L+"self_attn.v_proj.bias", NKV)
123
+ qq, kq = rope_int(qq), rope_int(kq)
124
+ kq14 = np.repeat(kq, NH//NKV, 1); vq14 = np.repeat(vq, NH//NKV, 1)
125
+ scores = np.einsum('shd,thd->hst', qq, kq14) # int
126
+ Csq = mul(qsq, ksq) # q*k scale; /sqrt(HD)=/8 -> >>3 on the value
127
+ # softmax: d = round((m-row) * Csq/8 * 64) = apply((m-row), Csq)*8 (Csq*64/8=Csq*8)
128
+ C8 = Csq * 8
129
+ probs_num = np.zeros((NH, S, S), np.int64); psum = np.zeros((NH, S), np.int64)
130
+ for h in range(NH):
131
+ for s in range(S):
132
+ row = scores[h, s, :s+1]; m = int(row.max())
133
+ d = np.clip([apply(m - int(r), C8) for r in row], 0, 255)
134
+ e = EXP[np.array(d)]; probs_num[h, s, :s+1] = e; psum[h, s] = int(e.sum())
135
+ # A.V in integer: out[s,h,dd] = sum_t probs[h,s,t]*v[t,h,dd] ; probs = num/psum (Q16-ish via e)
136
+ attn = np.zeros((S, NH, HD), np.int64)
137
+ for h in range(NH):
138
+ for s in range(S):
139
+ num = probs_num[h, s, :s+1]; den = int(psum[h, s])
140
+ acc = np.sum(num[:, None] * vq14[:s+1, h, :], 0) # int
141
+ attn[s, h, :] = (acc * (1 << SC) + (den >> 1)) // den >> SC # /den, keep vq scale
142
+ attn = attn.reshape(S, D); attn_sq = vsq
143
+ aq_attn, aq_attn_sq = aq(attn.reshape(-1), attn_sq); aq_attn = aq_attn.reshape(S, D)
144
+ _, wo, wosq = T[L+"self_attn.o_proj.weight"]
145
+ oacc = imm(aq_attn, wo) # [S,D]
146
+ o_colsq = [mul(int(aq_attn_sq), int(wosq[o])) for o in range(D)] # per-col Q30, python ints
147
+ # residual h_out[s,d] true = Xv[s,d]*Xsq[s] + oacc[s,d]*o_colsq[d] ; bring o into the X (per-token) scale
148
+ hout = np.zeros((S, D), np.int64)
149
+ for s in range(D and S):
150
+ xs = int(Xsq[s])
151
+ ratio = [(o_colsq[d] << SC) // xs for d in range(D)] # o_colsq[d]/Xsq[s] in Q30
152
+ o_in_x = np.array([apply(int(oacc[s, d]), ratio[d]) for d in range(D)], np.int64)
153
+ hout[s] = Xv[s] + o_in_x
154
+ # fingerprint over the integer block output
155
+ b = hout.astype(np.int64).tobytes()
156
+ fp = fnv(b)
157
+
158
+ # accuracy vs fp reference (dequant hout to fp using Xsq)
159
+ hout_fp_est = hout.astype(np.float64) * (Xsq[:, None].astype(np.float64) / (1 << SC))
160
+ # full fp reference
161
+ wd = lambda n: T[n][1].astype(np.float64) * np.array([T[n][2][o]/(1 << SC) for o in range(T[n][1].shape[0])])[:, None]
162
+ vd = lambda n: T[n][1].astype(np.float64) * (T[n][2]/(1 << SC))
163
+ Wq, Wk, Wv, Wo = wd(L+"self_attn.q_proj.weight"), wd(L+"self_attn.k_proj.weight"), wd(L+"self_attn.v_proj.weight"), wd(L+"self_attn.o_proj.weight")
164
+ bq, bk, bv = vd(L+"self_attn.q_proj.bias"), vd(L+"self_attn.k_proj.bias"), vd(L+"self_attn.v_proj.bias")
165
+ gin = vd(L+"input_layernorm.weight")
166
+ Xfp = Xv.astype(np.float64) * (Xsq[:, None].astype(np.float64)/(1 << SC))
167
+ xn = Xfp/np.sqrt(np.mean(Xfp**2, 1, keepdims=True)+1e-6)*gin
168
+ q = (xn@Wq.T+bq).reshape(S, NH, HD); k = (xn@Wk.T+bk).reshape(S, NKV, HD); v = (xn@Wv.T+bv).reshape(S, NKV, HD)
169
+ rhf = lambda x: np.concatenate([-x[..., HD//2:], x[..., :HD//2]], -1)
170
+ q = q*cosF[:, None, :]+rhf(q)*sinF[:, None, :]; k = k*cosF[:, None, :]+rhf(k)*sinF[:, None, :]
171
+ k14 = np.repeat(k, NH//NKV, 1); v14 = np.repeat(v, NH//NKV, 1)
172
+ scf = np.einsum('shd,thd->hst', q, k14)/np.sqrt(HD)
173
+ for h in range(NH):
174
+ for s in range(S): scf[h, s, s+1:] = -1e30
175
+ af = np.zeros((S, NH, HD))
176
+ for h in range(NH):
177
+ p = np.exp(scf[h]-scf[h].max(1, keepdims=True)); p /= p.sum(1, keepdims=True); af[:, h, :] = p@v14[:, h, :]
178
+ o_fp = af.reshape(S, D)@Wo.T; hfp = Xfp + o_fp
179
+ rel = float(np.sqrt(np.sum((hout_fp_est-hfp)**2)/np.sum(hfp**2)))
180
+ print(f"fully-integer block: accuracy vs fp = {rel:.4f}")
181
+ print(f"block-output FNV = {fp:016x} <- Rust/Metal/CUDA ports must reproduce")
182
+ print(f"hout[0,:4] int = {hout[0,:4]}")
docs/evidence/canon_attn_norm.rs ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Metal-profile integer layer (design §8): normalized (m0,e) scales, ALL arithmetic i64 + explicit
2
+ // two-word (hi,lo) 64x64 multiply + shift — every op maps 1:1 onto MSL (metal::mulhi(long)).
3
+ // This profile has its OWN reference fingerprint (rounding positions differ from the Q30/Q60 profiles);
4
+ // the claim is cross-backend identity: Rust-x86 == Rust-aarch64 == Metal-GPU.
5
+ // Build/run: rustc -O --edition 2021 canon_attn_norm.rs -o canon_attn_norm && ./canon_attn_norm layer0.bin
6
+
7
+ use std::io::Read;
8
+
9
+ const ALVL: i64 = 32767; // A16 for the layer profile (matches layer0.bin block config)
10
+ const D: usize = 896;
11
+ const NH: usize = 14;
12
+ const NKV: usize = 2;
13
+ const HD: usize = 64;
14
+ const S: usize = 4;
15
+
16
+ // ---- normalized scale: value = m0 * 2^e, m0 in [2^30, 2^31) (or m0=0) ----
17
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
18
+ struct Sc { m0: i64, e: i32 }
19
+
20
+ fn norm(mut m0: i64, mut e: i32) -> Sc {
21
+ if m0 <= 0 { return Sc { m0: 0, e: 0 }; }
22
+ while m0 >= 1 << 31 { m0 = (m0 + 1) >> 1; e += 1; }
23
+ while m0 < 1 << 30 { m0 <<= 1; e -= 1; }
24
+ Sc { m0, e }
25
+ }
26
+ fn sc_from_int(x: i64) -> Sc { norm(x, 0) } // integer as scale
27
+ fn sc_from_fixed(m0: i64, sh: i64) -> Sc { norm(m0, (sh - 31) as i32) } // gemmlowp (m0,shift)
28
+ fn sc_mul(a: Sc, b: Sc) -> Sc {
29
+ if a.m0 == 0 || b.m0 == 0 { return Sc { m0: 0, e: 0 }; }
30
+ let p = a.m0 * b.m0; // < 2^62, fits i64
31
+ norm((p + (1 << 30)) >> 31, a.e + b.e + 31)
32
+ }
33
+ fn sc_ratio(a: Sc, b: Sc) -> Sc {
34
+ if a.m0 == 0 { return Sc { m0: 0, e: 0 }; }
35
+ norm((a.m0 << 31) / b.m0, a.e - b.e - 31) // a.m0<<31 < 2^62
36
+ }
37
+ fn sc_cmp(a: Sc, b: Sc) -> std::cmp::Ordering { // compare a vs b (values)
38
+ if a.m0 == 0 || b.m0 == 0 { return a.m0.cmp(&b.m0); }
39
+ if a.e != b.e { return a.e.cmp(&b.e); }
40
+ a.m0.cmp(&b.m0)
41
+ }
42
+
43
+ // ---- two-word 64x64 multiply + shift (the exact ops MSL provides: mulhi + mullo) ----
44
+ fn mul64_wide(a: u64, b: u64) -> (u64, u64) {
45
+ // (hi, lo) of a*b — expressed via 32-bit limbs so the MSL translation is verbatim
46
+ let (a0, a1) = (a & 0xFFFF_FFFF, a >> 32);
47
+ let (b0, b1) = (b & 0xFFFF_FFFF, b >> 32);
48
+ let p00 = a0 * b0;
49
+ let p01 = a0 * b1;
50
+ let p10 = a1 * b0;
51
+ let p11 = a1 * b1;
52
+ let mid = (p00 >> 32) + (p01 & 0xFFFF_FFFF) + (p10 & 0xFFFF_FFFF);
53
+ let lo = (p00 & 0xFFFF_FFFF) | (mid << 32);
54
+ let hi = p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32);
55
+ (hi, lo)
56
+ }
57
+ /// round(v * m0 * 2^e), round-half-away-from-zero. |v| < 2^62; result must fit i64.
58
+ fn apply_sc(v: i64, s: Sc) -> i64 {
59
+ if s.m0 == 0 || v == 0 { return 0; }
60
+ let neg = v < 0;
61
+ let av = v.unsigned_abs();
62
+ let (hi, lo) = mul64_wide(av, s.m0 as u64);
63
+ let k = -(s.e); // shift right by k (e<0 in practice)
64
+ let mag: u64 = if s.e >= 0 {
65
+ assert!(hi == 0 && lo.leading_zeros() as i32 > s.e, "apply_sc overflow");
66
+ lo << s.e
67
+ } else if k >= 128 {
68
+ 0
69
+ } else if k >= 64 {
70
+ let kk = k - 64;
71
+ let main = if kk == 0 { hi } else { hi >> kk };
72
+ let rbit = if kk == 0 { (lo >> 63) & 1 } else { (hi >> (kk - 1)) & 1 };
73
+ main + rbit
74
+ } else {
75
+ let main = (hi << (64 - k)) | (lo >> k);
76
+ let rbit = (lo >> (k - 1)) & 1;
77
+ main + rbit
78
+ };
79
+ let m = mag as i64;
80
+ if neg { -m } else { m }
81
+ }
82
+ /// materialize a scale product as an integer: round(m0 * 2^e)
83
+ fn sc_to_int(s: Sc) -> i64 { apply_sc(1, Sc { m0: s.m0, e: s.e }) }
84
+
85
+ fn fnv(bytes: &[u8]) -> u64 { let mut h = 0xcbf29ce484222325u64; for &b in bytes { h ^= b as u64; h = h.wrapping_mul(0x100000001b3); } h }
86
+ fn isqrt64(n: i64) -> i64 { if n <= 0 { return 0; } let mut x = n; let mut y = (x + 1) / 2; while y < x { x = y; y = (x + n / x) / 2; } x }
87
+
88
+ // ---- layer0.bin records (same format as canon_attn.rs) ----
89
+ struct Cur<'a> { b: &'a [u8], p: usize }
90
+ impl<'a> Cur<'a> {
91
+ fn u8(&mut self) -> u8 { let v = self.b[self.p]; self.p += 1; v }
92
+ fn u32(&mut self) -> u32 { let v = u32::from_le_bytes(self.b[self.p..self.p + 4].try_into().unwrap()); self.p += 4; v }
93
+ fn i32(&mut self) -> i32 { let v = i32::from_le_bytes(self.b[self.p..self.p + 4].try_into().unwrap()); self.p += 4; v }
94
+ fn i64v(&mut self) -> i64 { let v = i64::from_le_bytes(self.b[self.p..self.p + 8].try_into().unwrap()); self.p += 8; v }
95
+ }
96
+ struct W2d { rows: usize, cols: usize, w: Vec<i8>, sq: Vec<Sc> }
97
+ struct V1d { v: Vec<i64>, sq: Sc }
98
+ fn rd_w2d(c: &mut Cur) -> W2d {
99
+ assert_eq!(c.u8(), 0); let rows = c.u32() as usize; let cols = c.u32() as usize;
100
+ let w: Vec<i8> = (0..rows * cols).map(|_| c.u8() as i8).collect();
101
+ let ns = c.u32() as usize;
102
+ let sq = (0..ns).map(|_| { let m0 = c.i32() as i64; let sh = c.i32() as i64; sc_from_fixed(m0, sh) }).collect();
103
+ W2d { rows, cols, w, sq }
104
+ }
105
+ fn rd_v1d(c: &mut Cur) -> V1d {
106
+ assert_eq!(c.u8(), 1); let n = c.u32() as usize; let _ = c.u32();
107
+ let v: Vec<i64> = (0..n).map(|_| c.i32() as i64).collect();
108
+ let _ = c.u32(); let m0 = c.i32() as i64; let sh = c.i32() as i64;
109
+ V1d { v, sq: sc_from_fixed(m0, sh) }
110
+ }
111
+
112
+ fn aq(v: &[i64], cur: Sc) -> (Vec<i64>, Sc) {
113
+ let am = v.iter().map(|x| x.abs()).max().unwrap().max(1);
114
+ let q = v.iter().map(|&x| {
115
+ let neg = x < 0; let ax = x.abs();
116
+ // round(ax*ALVL/am), half away from zero: (2*ax*ALVL + am) / (2*am) — ax<2^42*? keep safe:
117
+ // ax can reach ~2^42 (yg); ax*ALVL*2 ~ 2^58 fits i64.
118
+ let q = (2 * ax * ALVL + am) / (2 * am);
119
+ if neg { -q } else { q }
120
+ }).collect();
121
+ (q, sc_mul(sc_from_int(am), sc_ratio(cur, sc_from_int(ALVL))))
122
+ }
123
+
124
+ fn rmsnorm(hv: &[i64], gam: &V1d) -> (Vec<i64>, Sc) {
125
+ let mut yg = vec![0i64; S * D];
126
+ for s in 0..S {
127
+ let ss: i64 = (0..D).map(|d| { let x = hv[s * D + d]; x * x }).sum(); // |hv|<2^12 → ss<2^34
128
+ let mean = ss / (D as i64) + 1;
129
+ let inv = isqrt64((1i64 << 40) / mean);
130
+ for d in 0..D { yg[s * D + d] = hv[s * D + d] * inv * gam.v[d]; }
131
+ }
132
+ let (q, s0) = aq(&yg, Sc { m0: 1 << 30, e: -30 }); // cur = 1.0
133
+ // * 2^-20 (Q20 norm) * gamma scale
134
+ (q, sc_mul(sc_mul(s0, Sc { m0: 1 << 30, e: -50 }), gam.sq))
135
+ }
136
+ fn proj(w: &W2d, b: Option<&V1d>, xn_q: &[i64], xn_sq: Sc) -> (Vec<i64>, Sc) {
137
+ let (oc, inn) = (w.rows, w.cols);
138
+ let mut acc = vec![0i64; S * oc];
139
+ for s in 0..S { for o in 0..oc { let mut a = 0i64; for d in 0..inn { a += xn_q[s * inn + d] * (w.w[o * inn + d] as i64); } acc[s * oc + o] = a; } }
140
+ let colsq: Vec<Sc> = (0..oc).map(|o| sc_mul(xn_sq, w.sq[o])).collect();
141
+ let cmax = *colsq.iter().max_by(|a, b| sc_cmp(**a, **b)).unwrap();
142
+ let mut resc = vec![0i64; S * oc];
143
+ for o in 0..oc {
144
+ let bacc: i64 = match b {
145
+ Some(bv) if colsq[o].m0 != 0 => sc_to_int(sc_ratio(sc_mul(sc_from_int(bv.v[o]), bv.sq), colsq[o])),
146
+ _ => 0,
147
+ };
148
+ let ratio = sc_ratio(colsq[o], cmax);
149
+ for s in 0..S { resc[s * oc + o] = apply_sc(acc[s * oc + o] + bacc, ratio); }
150
+ }
151
+ aq(&resc, cmax)
152
+ }
153
+ fn rope(x: &mut [i64], nh: usize, cosq: &[i64], sinq: &[i64]) {
154
+ let src = x.to_vec();
155
+ for s in 0..S { for h in 0..nh { for d in 0..HD {
156
+ let base = (s * nh + h) * HD;
157
+ let rh = if d < HD / 2 { -src[base + d + HD / 2] } else { src[base + d - HD / 2] };
158
+ x[base + d] = (src[base + d] * cosq[s * HD + d] + rh * sinq[s * HD + d] + 16384) >> 15;
159
+ } } }
160
+ }
161
+
162
+ fn main() {
163
+ let path = std::env::args().nth(1).unwrap_or("layer0.bin".into());
164
+ let mut buf = Vec::new(); std::fs::File::open(&path).unwrap().read_to_end(&mut buf).unwrap();
165
+ assert_eq!(&buf[0..8], b"L0BIN\0\0\0");
166
+ let mut c = Cur { b: &buf, p: 8 };
167
+ let gam = rd_v1d(&mut c);
168
+ let qw = rd_w2d(&mut c); let qb = rd_v1d(&mut c);
169
+ let kw = rd_w2d(&mut c); let kb = rd_v1d(&mut c);
170
+ let vw = rd_w2d(&mut c); let vb = rd_v1d(&mut c);
171
+ let ow = rd_w2d(&mut c);
172
+ let xt = rd_w2d(&mut c);
173
+ let pan = rd_v1d(&mut c);
174
+ let gw = rd_w2d(&mut c); let uw = rd_w2d(&mut c); let dw = rd_w2d(&mut c);
175
+ let cosq: Vec<i64> = (0..S * HD).map(|_| c.i64v()).collect();
176
+ let sinq: Vec<i64> = (0..S * HD).map(|_| c.i64v()).collect();
177
+ let exp: Vec<i64> = (0..256).map(|_| c.i64v()).collect();
178
+ let sig: Vec<i64> = (0..4096).map(|_| c.i64v()).collect();
179
+
180
+ let xv: Vec<i64> = xt.w.iter().map(|&x| x as i64).collect();
181
+ let xsq: Vec<Sc> = xt.sq.clone();
182
+
183
+ let (xn_q, xn_sq) = rmsnorm(&xv, &gam);
184
+ let (mut qq, qsq) = proj(&qw, Some(&qb), &xn_q, xn_sq);
185
+ let (mut kq, ksq) = proj(&kw, Some(&kb), &xn_q, xn_sq);
186
+ let (vq, vsq) = proj(&vw, Some(&vb), &xn_q, xn_sq);
187
+ rope(&mut qq, NH, &cosq, &sinq);
188
+ rope(&mut kq, NKV, &cosq, &sinq);
189
+ let g = NH / NKV;
190
+ // C64 = qsq*ksq*8 (exp-arg 1/64 units, incl 1/sqrt(64)=1/8): e' = e+3
191
+ let cq = sc_mul(qsq, ksq); let c64 = Sc { m0: cq.m0, e: cq.e + 3 };
192
+ let mut attn = vec![0i64; S * D];
193
+ for h in 0..NH { let kvh = h / g; for s in 0..S {
194
+ let mut scores = vec![0i64; s + 1];
195
+ for t in 0..=s { let mut a = 0i64; for d in 0..HD { a += qq[(s * NH + h) * HD + d] * kq[(t * NKV + kvh) * HD + d]; } scores[t] = a; }
196
+ let m = *scores.iter().max().unwrap();
197
+ let mut e = vec![0i64; s + 1]; let mut den = 0i64;
198
+ for t in 0..=s { let mut dd = apply_sc(m - scores[t], c64); if dd < 0 { dd = 0; } if dd > 255 { dd = 255; } e[t] = exp[dd as usize]; den += e[t]; }
199
+ for d in 0..HD {
200
+ let mut acc = 0i64; for t in 0..=s { acc += e[t] * vq[(t * NKV + kvh) * HD + d]; } // <2^16*2^15*4=2^33
201
+ attn[s * D + h * HD + d] = { let neg = acc < 0; let a = acc.abs(); let r = (2 * a + den) / (2 * den); if neg { -r } else { r } };
202
+ }
203
+ } }
204
+ let (aqa, aqa_sq) = aq(&attn, vsq);
205
+ let mut hout = vec![0i64; S * D];
206
+ for s in 0..S { for d2 in 0..D {
207
+ let mut a = 0i64; for d in 0..D { a += aqa[s * D + d] * (ow.w[d2 * D + d] as i64); }
208
+ let ocsq = sc_mul(aqa_sq, ow.sq[d2]);
209
+ hout[s * D + d2] = xv[s * D + d2] + apply_sc(a, sc_ratio(ocsq, xsq[s]));
210
+ } }
211
+ let (xn2_q, xn2_sq) = rmsnorm(&hout, &pan);
212
+ let (gate_q, gate_sq) = proj(&gw, None, &xn2_q, xn2_sq);
213
+ let (up_q, up_sq) = proj(&uw, None, &xn2_q, xn2_sq);
214
+ let ff = gw.rows;
215
+ let gs9 = Sc { m0: gate_sq.m0, e: gate_sq.e + 9 }; // *512 (idx step 1/... layer profile uses 1/128<<2? keep <<9/8192 domain of 4096-sig? sig table is 4096 @1/128: idx=round(g*128)+2048 → e+7)
216
+ let gs7 = Sc { m0: gate_sq.m0, e: gate_sq.e + 7 };
217
+ let _ = gs9;
218
+ let mut act_int = vec![0i64; S * ff];
219
+ for i in 0..S * ff { let mut idx = apply_sc(gate_q[i], gs7) + 2048; if idx < 0 { idx = 0; } if idx > 4095 { idx = 4095; } act_int[i] = gate_q[i] * sig[idx as usize]; }
220
+ let (act_q, spre) = aq(&act_int, gate_sq);
221
+ let act_sq = Sc { m0: spre.m0, e: spre.e - 16 }; // /2^16 (Q16 sigmoid)
222
+ let mut hmul = vec![0i64; S * ff];
223
+ for i in 0..S * ff { hmul[i] = act_q[i] * up_q[i]; }
224
+ let (hmul_q, hmul_q_sq) = aq(&hmul, sc_mul(act_sq, up_sq));
225
+ let (mlp_q, mlp_sq) = proj(&dw, None, &hmul_q, hmul_q_sq);
226
+ let mut layer = vec![0i64; S * D];
227
+ for s in 0..S { let ratio = sc_ratio(mlp_sq, xsq[s]); for d2 in 0..D { layer[s * D + d2] = hout[s * D + d2] + apply_sc(mlp_q[s * D + d2], ratio); } }
228
+
229
+ let bytes: Vec<u8> = layer.iter().flat_map(|v| v.to_le_bytes()).collect();
230
+ println!("backend = Rust CPU ({}) [norm (m0,e) profile]", std::env::consts::ARCH);
231
+ println!("layer FNV = {:016x}", fnv(&bytes));
232
+ println!("layer[0..4] = {:?} (Q30 profile was [202, 187, 603, -177])", &layer[0..4]);
233
+ }
docs/evidence/canon_attn_norm.swift ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Metal GPU port of the norm-(m0,e) integer layer (design §8). Host parses layer0.bin, converts all
2
+ // stored gemmlowp scales to normalized (m0,e), uploads flat buffers; a single-thread MSL kernel runs the
3
+ // whole layer with ONLY long (i64) ops + a 32-bit-limb two-word multiply — the exact Rust semantics.
4
+ // Must reproduce: layer FNV 50a8caf4107f43f4, layer[0..4] = [265, 210, 581, -123].
5
+ // Build/run: swiftc -O canon_attn_norm.swift -o canon_attn_norm_metal && ./canon_attn_norm_metal layer0.bin
6
+
7
+ import Foundation
8
+ import Metal
9
+
10
+ // ---- host-side scale normalization (same as Rust norm/sc_from_fixed) ----
11
+ func normSc(_ m0in: Int64, _ ein: Int32) -> (Int64, Int32) {
12
+ var m0 = m0in, e = ein
13
+ if m0 <= 0 { return (0, 0) }
14
+ while m0 >= (1 << 31) { m0 = (m0 + 1) >> 1; e += 1 }
15
+ while m0 < (1 << 30) { m0 <<= 1; e -= 1 }
16
+ return (m0, e)
17
+ }
18
+
19
+ struct Rd { var d: Data; var p = 0
20
+ mutating func u8() -> UInt8 { defer { p += 1 }; return d[p] }
21
+ mutating func u32() -> UInt32 { defer { p += 4 }; return d.subdata(in: p..<p+4).withUnsafeBytes { $0.load(as: UInt32.self) } }
22
+ mutating func i32() -> Int32 { defer { p += 4 }; return d.subdata(in: p..<p+4).withUnsafeBytes { $0.load(as: Int32.self) } }
23
+ mutating func i64() -> Int64 { defer { p += 8 }; return d.subdata(in: p..<p+8).withUnsafeBytes { $0.load(as: Int64.self) } }
24
+ }
25
+ struct W2 { var rows = 0, cols = 0; var w: [Int8] = []; var m0: [Int64] = []; var e: [Int32] = [] }
26
+ struct V1 { var v: [Int64] = []; var m0: Int64 = 0; var e: Int32 = 0 }
27
+ func rdW2(_ r: inout Rd) -> W2 {
28
+ precondition(r.u8() == 0); var t = W2(); t.rows = Int(r.u32()); t.cols = Int(r.u32())
29
+ t.w = (0..<t.rows*t.cols).map { _ in Int8(bitPattern: r.u8()) }
30
+ let ns = Int(r.u32())
31
+ for _ in 0..<ns { let m = Int64(r.i32()); let s = r.i32(); let (m0, e) = normSc(m, s - 31); t.m0.append(m0); t.e.append(e) }
32
+ return t
33
+ }
34
+ func rdV1(_ r: inout Rd) -> V1 {
35
+ precondition(r.u8() == 1); var t = V1(); let n = Int(r.u32()); _ = r.u32()
36
+ t.v = (0..<n).map { _ in Int64(r.i32()) }
37
+ _ = r.u32(); let m = Int64(r.i32()); let s = r.i32(); (t.m0, t.e) = normSc(m, s - 31)
38
+ return t
39
+ }
40
+
41
+ let SHADER = """
42
+ #include <metal_stdlib>
43
+ using namespace metal;
44
+ constant int D = 896, NH = 14, NKV = 2, HD = 64, S = 4, FF = 4864;
45
+ constant long ALVL = 32767;
46
+
47
+ struct Sc { long m0; int e; };
48
+ static Sc norm_sc(long m0, int e) {
49
+ Sc s; if (m0 <= 0) { s.m0 = 0; s.e = 0; return s; }
50
+ while (m0 >= (1L << 31)) { m0 = (m0 + 1) >> 1; e += 1; }
51
+ while (m0 < (1L << 30)) { m0 <<= 1; e -= 1; }
52
+ s.m0 = m0; s.e = e; return s;
53
+ }
54
+ static Sc sc_from_int(long x) { return norm_sc(x, 0); }
55
+ static Sc sc_mul(Sc a, Sc b) {
56
+ if (a.m0 == 0 || b.m0 == 0) { Sc z; z.m0 = 0; z.e = 0; return z; }
57
+ long p = a.m0 * b.m0;
58
+ return norm_sc((p + (1L << 30)) >> 31, a.e + b.e + 31);
59
+ }
60
+ static Sc sc_ratio(Sc a, Sc b) {
61
+ if (a.m0 == 0) { Sc z; z.m0 = 0; z.e = 0; return z; }
62
+ return norm_sc((a.m0 << 31) / b.m0, a.e - b.e - 31);
63
+ }
64
+ static bool sc_gt(Sc a, Sc b) { // a > b
65
+ if (a.m0 == 0 || b.m0 == 0) return a.m0 > b.m0;
66
+ if (a.e != b.e) return a.e > b.e;
67
+ return a.m0 > b.m0;
68
+ }
69
+ static void mul64_wide(ulong a, ulong b, thread ulong &hi, thread ulong &lo) {
70
+ ulong a0 = a & 0xFFFFFFFFUL, a1 = a >> 32;
71
+ ulong b0 = b & 0xFFFFFFFFUL, b1 = b >> 32;
72
+ ulong p00 = a0 * b0, p01 = a0 * b1, p10 = a1 * b0, p11 = a1 * b1;
73
+ ulong mid = (p00 >> 32) + (p01 & 0xFFFFFFFFUL) + (p10 & 0xFFFFFFFFUL);
74
+ lo = (p00 & 0xFFFFFFFFUL) | (mid << 32);
75
+ hi = p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32);
76
+ }
77
+ static long apply_sc(long v, Sc s) {
78
+ if (s.m0 == 0 || v == 0) return 0;
79
+ bool neg = v < 0;
80
+ ulong av = ulong(neg ? -v : v);
81
+ ulong hi, lo; mul64_wide(av, ulong(s.m0), hi, lo);
82
+ int k = -s.e;
83
+ ulong mag;
84
+ if (s.e >= 0) { mag = lo << s.e; }
85
+ else if (k >= 128) { mag = 0; }
86
+ else if (k >= 64) {
87
+ int kk = k - 64;
88
+ ulong main_ = (kk == 0) ? hi : (hi >> kk);
89
+ ulong rbit = (kk == 0) ? ((lo >> 63) & 1) : ((hi >> (kk - 1)) & 1);
90
+ mag = main_ + rbit;
91
+ } else {
92
+ ulong main_ = (hi << (64 - k)) | (lo >> k);
93
+ ulong rbit = (lo >> (k - 1)) & 1;
94
+ mag = main_ + rbit;
95
+ }
96
+ long m = long(mag);
97
+ return neg ? -m : m;
98
+ }
99
+ static long sc_to_int(Sc s) { return apply_sc(1, s); }
100
+ static long isqrt64(long n) {
101
+ if (n <= 0) return 0;
102
+ long x = n, y = (x + 1) / 2;
103
+ while (y < x) { x = y; y = (x + n / x) / 2; }
104
+ return x;
105
+ }
106
+
107
+ // aq over a span in `vals[off..off+n]`, quantizes in place, returns new scale
108
+ static Sc aq_span(device long* vals, int off, int n, Sc cur) {
109
+ long am = 1;
110
+ for (int i = 0; i < n; i++) { long a = vals[off + i]; if (a < 0) a = -a; if (a > am) am = a; }
111
+ for (int i = 0; i < n; i++) {
112
+ long x = vals[off + i]; bool neg = x < 0; long ax = neg ? -x : x;
113
+ long q = (2 * ax * ALVL + am) / (2 * am);
114
+ vals[off + i] = neg ? -q : q;
115
+ }
116
+ return sc_mul(sc_from_int(am), sc_ratio(cur, sc_from_int(ALVL)));
117
+ }
118
+
119
+ kernel void layer_norm_profile(
120
+ device const char* qw [[buffer(0)]], device const long* qws [[buffer(1)]], // scales as (m0,e) pairs: 2*i64 each (e stored as i64)
121
+ device const char* kw [[buffer(2)]], device const long* kws [[buffer(3)]],
122
+ device const char* vw [[buffer(4)]], device const long* vws [[buffer(5)]],
123
+ device const char* ow [[buffer(6)]], device const long* ows [[buffer(7)]],
124
+ device const char* gw [[buffer(8)]], device const long* gws [[buffer(9)]],
125
+ device const char* uw [[buffer(10)]], device const long* uws [[buffer(11)]],
126
+ device const char* dw [[buffer(12)]], device const long* dws [[buffer(13)]],
127
+ device const long* qb [[buffer(14)]], device const long* kb [[buffer(15)]], device const long* vb [[buffer(16)]],
128
+ device const long* gamv [[buffer(17)]], device const long* panv [[buffer(18)]],
129
+ device const char* xv8 [[buffer(19)]], device const long* xscp [[buffer(20)]],
130
+ device const long* cosq [[buffer(21)]], device const long* sinq [[buffer(22)]],
131
+ device const long* expl [[buffer(23)]], device const long* sigl [[buffer(24)]],
132
+ device const long* meta [[buffer(25)]], // [qb_m0,qb_e, kb_m0,kb_e, vb_m0,vb_e, gam_m0,gam_e, pan_m0,pan_e]
133
+ device long* scratch [[buffer(26)]], // big scratch: layout below
134
+ device long* outbuf [[buffer(27)]],
135
+ uint gid [[thread_position_in_grid]]) {
136
+ if (gid != 0) return;
137
+ // scratch layout (i64 slots):
138
+ device long* xn = scratch; // S*D
139
+ device long* qq = xn + S*D; // S*NH*HD
140
+ device long* kq = qq + S*NH*HD; // S*NKV*HD
141
+ device long* vq = kq + S*NKV*HD; // S*NKV*HD
142
+ device long* attn= vq + S*NKV*HD; // S*D
143
+ device long* hout= attn + S*D; // S*D
144
+ device long* xn2 = hout + S*D; // S*D
145
+ device long* gate= xn2 + S*D; // S*FF
146
+ device long* up = gate + S*FF; // S*FF
147
+ device long* act = up + S*FF; // S*FF
148
+ device long* acc = act + S*FF; // S*FF (proj scratch)
149
+ device long* src = acc + S*FF; // S*NH*HD (rope scratch)
150
+
151
+ Sc gamsq; gamsq.m0 = meta[6]; gamsq.e = int(meta[7]);
152
+ Sc pansq; pansq.m0 = meta[8]; pansq.e = int(meta[9]);
153
+ Sc qbsq; qbsq.m0 = meta[0]; qbsq.e = int(meta[1]);
154
+ Sc kbsq; kbsq.m0 = meta[2]; kbsq.e = int(meta[3]);
155
+ Sc vbsq; vbsq.m0 = meta[4]; vbsq.e = int(meta[5]);
156
+
157
+ // ---- rmsnorm(Xv, gamma) -> xn (int16 span) + xnsq ----
158
+ for (int s = 0; s < S; s++) {
159
+ long ss = 0;
160
+ for (int d = 0; d < D; d++) { long x = xv8[s*D + d]; ss += x * x; }
161
+ long mean = ss / D + 1;
162
+ long inv = isqrt64((1L << 40) / mean);
163
+ for (int d = 0; d < D; d++) xn[s*D + d] = long(xv8[s*D + d]) * inv * gamv[d];
164
+ }
165
+ Sc one; one.m0 = 1L << 30; one.e = -30;
166
+ Sc s0 = aq_span(xn, 0, S*D, one);
167
+ Sc q20; q20.m0 = 1L << 30; q20.e = -50;
168
+ Sc xnsq = sc_mul(sc_mul(s0, q20), gamsq);
169
+
170
+ // ---- proj helper as macro-ish inline (q/k/v with bias, gate/up/down without) ----
171
+ // computed inline per projection to avoid function-pointer complexity.
172
+
173
+ #define PROJ(W, WS, OC, BV, BSQ, HAS_B, OUT, OUTSQ, XNBUF, XNSQ) \
174
+ { \
175
+ for (int s = 0; s < S; s++) for (int o = 0; o < (OC); o++) { \
176
+ long a = 0; for (int d = 0; d < D; d++) a += XNBUF[s*D + d] * long(W[o*D + d]); \
177
+ acc[s*(OC) + o] = a; } \
178
+ Sc cmax; cmax.m0 = 0; cmax.e = 0; \
179
+ for (int o = 0; o < (OC); o++) { Sc cs = sc_mul(XNSQ, norm_sc(WS[2*o], int(WS[2*o+1]))); if (sc_gt(cs, cmax)) cmax = cs; } \
180
+ for (int o = 0; o < (OC); o++) { \
181
+ Sc cs = sc_mul(XNSQ, norm_sc(WS[2*o], int(WS[2*o+1]))); \
182
+ long bacc = 0; \
183
+ if (HAS_B && cs.m0 != 0) bacc = sc_to_int(sc_ratio(sc_mul(sc_from_int(BV[o]), BSQ), cs)); \
184
+ Sc ratio = sc_ratio(cs, cmax); \
185
+ for (int s = 0; s < S; s++) OUT[s*(OC) + o] = apply_sc(acc[s*(OC) + o] + bacc, ratio); \
186
+ } \
187
+ OUTSQ = aq_span(OUT, 0, S*(OC), cmax); \
188
+ }
189
+
190
+ Sc qsq, ksq, vsq;
191
+ PROJ(qw, qws, NH*HD, qb, qbsq, true, qq, qsq, xn, xnsq)
192
+ PROJ(kw, kws, NKV*HD, kb, kbsq, true, kq, ksq, xn, xnsq)
193
+ PROJ(vw, vws, NKV*HD, vb, vbsq, true, vq, vsq, xn, xnsq)
194
+
195
+ // rope on qq (NH) and kq (NKV)
196
+ for (int pass = 0; pass < 2; pass++) {
197
+ device long* x = pass == 0 ? qq : kq;
198
+ int nh = pass == 0 ? NH : NKV;
199
+ for (int i = 0; i < S*nh*HD; i++) src[i] = x[i];
200
+ for (int s = 0; s < S; s++) for (int h = 0; h < nh; h++) for (int d = 0; d < HD; d++) {
201
+ int base = (s*nh + h)*HD;
202
+ long rh = d < HD/2 ? -src[base + d + HD/2] : src[base + d - HD/2];
203
+ x[base + d] = (src[base + d] * cosq[s*HD + d] + rh * sinq[s*HD + d] + 16384) >> 15;
204
+ }
205
+ }
206
+ int g = NH / NKV;
207
+ Sc cq = sc_mul(qsq, ksq); Sc c64; c64.m0 = cq.m0; c64.e = cq.e + 3;
208
+ for (int h = 0; h < NH; h++) { int kvh = h / g;
209
+ for (int s = 0; s < S; s++) {
210
+ long scores[4]; long m = -0x7FFFFFFFFFFFFFFFL;
211
+ for (int t = 0; t <= s; t++) { long a = 0; for (int d = 0; d < HD; d++) a += qq[(s*NH + h)*HD + d] * kq[(t*NKV + kvh)*HD + d]; scores[t] = a; if (a > m) m = a; }
212
+ long e[4]; long den = 0;
213
+ for (int t = 0; t <= s; t++) { long dd = apply_sc(m - scores[t], c64); if (dd < 0) dd = 0; if (dd > 255) dd = 255; e[t] = expl[dd]; den += e[t]; }
214
+ for (int d = 0; d < HD; d++) {
215
+ long a = 0; for (int t = 0; t <= s; t++) a += e[t] * vq[(t*NKV + kvh)*HD + d];
216
+ bool neg = a < 0; long aa = neg ? -a : a; long r = (2*aa + den) / (2*den);
217
+ attn[s*D + h*HD + d] = neg ? -r : r;
218
+ }
219
+ }
220
+ }
221
+ Sc aqsq = aq_span(attn, 0, S*D, vsq);
222
+ for (int s = 0; s < S; s++) for (int d2 = 0; d2 < D; d2++) {
223
+ long a = 0; for (int d = 0; d < D; d++) a += attn[s*D + d] * long(ow[d2*D + d]);
224
+ Sc ocs = sc_mul(aqsq, norm_sc(ows[2*d2], int(ows[2*d2+1])));
225
+ Sc xs; xs.m0 = xscp[2*s]; xs.e = int(xscp[2*s+1]);
226
+ hout[s*D + d2] = long(xv8[s*D + d2]) + apply_sc(a, sc_ratio(ocs, xs));
227
+ }
228
+ // ---- MLP ----
229
+ for (int s = 0; s < S; s++) {
230
+ long ss = 0;
231
+ for (int d = 0; d < D; d++) { long x = hout[s*D + d]; ss += x * x; }
232
+ long mean = ss / D + 1;
233
+ long inv = isqrt64((1L << 40) / mean);
234
+ for (int d = 0; d < D; d++) xn2[s*D + d] = hout[s*D + d] * inv * panv[d];
235
+ }
236
+ Sc s02 = aq_span(xn2, 0, S*D, one);
237
+ Sc xn2sq = sc_mul(sc_mul(s02, q20), pansq);
238
+
239
+ Sc gsq, usq;
240
+ // gate/up proj: OC=FF, weights [FF, D]
241
+ PROJ(gw, gws, FF, qb, qbsq, false, gate, gsq, xn2, xn2sq)
242
+ PROJ(uw, uws, FF, qb, qbsq, false, up, usq, xn2, xn2sq)
243
+
244
+ Sc gs7; gs7.m0 = gsq.m0; gs7.e = gsq.e + 7;
245
+ for (int i = 0; i < S*FF; i++) {
246
+ long idx = apply_sc(gate[i], gs7) + 2048;
247
+ if (idx < 0) idx = 0; if (idx > 4095) idx = 4095;
248
+ act[i] = gate[i] * sigl[idx];
249
+ }
250
+ Sc spre = aq_span(act, 0, S*FF, gsq);
251
+ Sc actsq; actsq.m0 = spre.m0; actsq.e = spre.e - 16;
252
+ for (int i = 0; i < S*FF; i++) act[i] = act[i] * up[i];
253
+ Sc hmsq = aq_span(act, 0, S*FF, sc_mul(actsq, usq));
254
+
255
+ // down proj: input act [S,FF], weights dw [D, FF]
256
+ Sc msq;
257
+ {
258
+ for (int s = 0; s < S; s++) for (int o = 0; o < D; o++) {
259
+ long a = 0; for (int d = 0; d < FF; d++) a += act[s*FF + d] * long(dw[o*FF + d]);
260
+ acc[s*D + o] = a; }
261
+ Sc cmax; cmax.m0 = 0; cmax.e = 0;
262
+ for (int o = 0; o < D; o++) { Sc cs = sc_mul(hmsq, norm_sc(dws[2*o], int(dws[2*o+1]))); if (sc_gt(cs, cmax)) cmax = cs; }
263
+ for (int o = 0; o < D; o++) {
264
+ Sc cs = sc_mul(hmsq, norm_sc(dws[2*o], int(dws[2*o+1])));
265
+ Sc ratio = sc_ratio(cs, cmax);
266
+ for (int s = 0; s < S; s++) xn[s*D + o] = apply_sc(acc[s*D + o], ratio);
267
+ }
268
+ msq = aq_span(xn, 0, S*D, cmax);
269
+ }
270
+ for (int s = 0; s < S; s++) {
271
+ Sc xs; xs.m0 = xscp[2*s]; xs.e = int(xscp[2*s+1]);
272
+ Sc ratio = sc_ratio(msq, xs);
273
+ for (int d2 = 0; d2 < D; d2++) outbuf[s*D + d2] = hout[s*D + d2] + apply_sc(xn[s*D + d2], ratio);
274
+ }
275
+ }
276
+ """
277
+
278
+ // ---- host main ----
279
+ let path = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "layer0.bin"
280
+ var rd = Rd(d: try! Data(contentsOf: URL(fileURLWithPath: path)))
281
+ precondition(rd.d.prefix(8) == Data([0x4C, 0x30, 0x42, 0x49, 0x4E, 0, 0, 0]), "bad magic") // "L0BIN\0\0\0"
282
+ rd.p = 8
283
+ let gam = rdV1(&rd)
284
+ let qw = rdW2(&rd); let qb = rdV1(&rd)
285
+ let kw = rdW2(&rd); let kb = rdV1(&rd)
286
+ let vw = rdW2(&rd); let vb = rdV1(&rd)
287
+ let ow = rdW2(&rd)
288
+ let xt = rdW2(&rd)
289
+ let pan = rdV1(&rd)
290
+ let gw = rdW2(&rd); let uw = rdW2(&rd); let dw = rdW2(&rd)
291
+ let S = 4, HD = 64, D = 896, FF = 4864, NH = 14, NKV = 2
292
+ let cosq = (0..<S*HD).map { _ in rd.i64() }
293
+ let sinq = (0..<S*HD).map { _ in rd.i64() }
294
+ let expl = (0..<256).map { _ in rd.i64() }
295
+ let sigl = (0..<4096).map { _ in rd.i64() }
296
+
297
+ guard let dev = MTLCreateSystemDefaultDevice() else { fatalError("no Metal") }
298
+ let lib = try! dev.makeLibrary(source: SHADER, options: nil)
299
+ let pipe = try! dev.makeComputePipelineState(function: lib.makeFunction(name: "layer_norm_profile")!)
300
+ let q = dev.makeCommandQueue()!
301
+ func bufI8(_ a: [Int8]) -> MTLBuffer { dev.makeBuffer(bytes: a, length: a.count, options: .storageModeShared)! }
302
+ func bufI64(_ a: [Int64]) -> MTLBuffer { dev.makeBuffer(bytes: a, length: a.count * 8, options: .storageModeShared)! }
303
+ func scPairs(_ w: W2) -> [Int64] { var out: [Int64] = []; for i in 0..<w.m0.count { out.append(w.m0[i]); out.append(Int64(w.e[i])) }; return out }
304
+
305
+ let meta: [Int64] = [qb.m0, Int64(qb.e), kb.m0, Int64(kb.e), vb.m0, Int64(vb.e), gam.m0, Int64(gam.e), pan.m0, Int64(pan.e)]
306
+ let scratchLen = S*D + S*NH*HD + 2*(S*NKV*HD) + 3*(S*D) + 4*(S*FF) + S*NH*HD
307
+ let bScr = dev.makeBuffer(length: scratchLen * 8, options: .storageModeShared)!
308
+ let bOut = dev.makeBuffer(length: S*D*8, options: .storageModeShared)!
309
+
310
+ let bufs: [MTLBuffer] = [
311
+ bufI8(qw.w), bufI64(scPairs(qw)), bufI8(kw.w), bufI64(scPairs(kw)), bufI8(vw.w), bufI64(scPairs(vw)),
312
+ bufI8(ow.w), bufI64(scPairs(ow)), bufI8(gw.w), bufI64(scPairs(gw)), bufI8(uw.w), bufI64(scPairs(uw)),
313
+ bufI8(dw.w), bufI64(scPairs(dw)),
314
+ bufI64(qb.v), bufI64(kb.v), bufI64(vb.v), bufI64(gam.v), bufI64(pan.v),
315
+ bufI8(xt.w), bufI64(scPairs(xt)),
316
+ bufI64(cosq), bufI64(sinq), bufI64(expl), bufI64(sigl), bufI64(meta), bScr, bOut,
317
+ ]
318
+ let cmd = q.makeCommandBuffer()!
319
+ let enc = cmd.makeComputeCommandEncoder()!
320
+ enc.setComputePipelineState(pipe)
321
+ for (i, b) in bufs.enumerated() { enc.setBuffer(b, offset: 0, index: i) }
322
+ enc.dispatchThreads(MTLSize(width: 1, height: 1, depth: 1), threadsPerThreadgroup: MTLSize(width: 1, height: 1, depth: 1))
323
+ enc.endEncoding(); cmd.commit(); cmd.waitUntilCompleted()
324
+
325
+ let optr = bOut.contents().bindMemory(to: Int64.self, capacity: S*D)
326
+ var h: UInt64 = 0xcbf29ce484222325
327
+ for i in 0..<(S*D) {
328
+ var v = optr[i].littleEndian
329
+ withUnsafeBytes(of: &v) { for b in $0 { h ^= UInt64(b); h = h &* 0x100000001b3 } }
330
+ }
331
+ print("backend = Apple Metal GPU (\(dev.name)) [norm (m0,e) profile]")
332
+ print(String(format: "layer FNV = %016llx (target 50a8caf4107f43f4)", h))
333
+ print("layer[0..4] = [\(optr[0]), \(optr[1]), \(optr[2]), \(optr[3])] (target [265, 210, 581, -123])")
334
+ print("MATCH = \(String(format: "%016llx", h) == "50a8caf4107f43f4" ? "YES — Apple GPU joins the integer pool" : "NO")")
docs/evidence/canon_attn_sq.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # inc4b-2a accuracy fix: SmoothQuant on the QKV path. Migrate per-input-channel activation outlier scale
3
+ # into gamma (RMSNorm) and the q/k/v weights, so per-tensor int8 activation quant becomes accurate while
4
+ # the int8 GEMM stays unchanged. Compares W8A8 attention output to fp before/after SmoothQuant.
5
+ import sys, json, struct, numpy as np
6
+ CANON = sys.argv[1] if len(sys.argv) > 1 else "/root/models/qwen05.canon"
7
+ D, NH, NKV, HD, THETA, S = 896, 14, 2, 64, 1000000.0, 4
8
+
9
+ def read_canon(path):
10
+ f = open(path, "rb"); assert f.read(8) == b"CANONv1\n"
11
+ n = struct.unpack("<Q", f.read(8))[0]; hdr = json.loads(f.read(n)); body = f.read()
12
+ out = {}
13
+ for t in hdr["tensors"]:
14
+ nm, k, shp = t["name"], t["kind"], t["shape"]
15
+ if k == "w2d_int8_pcs":
16
+ o, i = shp
17
+ w = np.frombuffer(body[t["w_off"]:t["w_off"]+o*i], dtype=np.int8).reshape(o, i)
18
+ sc = np.frombuffer(body[t["s_off"]:t["s_off"]+o*8], dtype=np.int32).reshape(o, 2)
19
+ out[nm] = ("w", w, sc[:, 0].astype(np.float64) * 2.0**(sc[:, 1].astype(np.float64)-31))
20
+ else:
21
+ v = np.frombuffer(body[t["w_off"]:t["w_off"]+shp[0]*4], dtype=np.int32)
22
+ m0, sh = struct.unpack("<ii", body[t["s_off"]:t["s_off"]+8])
23
+ out[nm] = ("v", v, m0 * 2.0**(sh-31))
24
+ return out
25
+ T = read_canon(CANON)
26
+ wd = lambda n: T[n][1].astype(np.float64) * T[n][2][:, None]
27
+ vd = lambda n: T[n][1].astype(np.float64) * T[n][2]
28
+ L = "model.layers.0."
29
+ Wq, Wk, Wv, Wo = wd(L+"self_attn.q_proj.weight"), wd(L+"self_attn.k_proj.weight"), wd(L+"self_attn.v_proj.weight"), wd(L+"self_attn.o_proj.weight")
30
+ bq, bk, bv = vd(L+"self_attn.q_proj.bias"), vd(L+"self_attn.k_proj.bias"), vd(L+"self_attn.v_proj.bias")
31
+ gin = vd(L+"input_layernorm.weight")
32
+ Wemb, semb = T["model.embed_tokens.weight"][1], T["model.embed_tokens.weight"][2]
33
+ emb = lambda ids: np.stack([Wemb[i].astype(np.float64)*semb[i] for i in ids])
34
+ X = emb([9707, 11, 1879, 30][:S])
35
+
36
+ inv_freq = 1.0/(THETA**(np.arange(0, HD, 2)/HD)); ang = np.outer(np.arange(S), inv_freq)
37
+ cos_f = np.concatenate([np.cos(ang)]*2, 1); sin_f = np.concatenate([np.sin(ang)]*2, 1)
38
+ cosQ = np.clip(np.rint(cos_f*32768), -32768, 32767).astype(np.int64); sinQ = np.clip(np.rint(sin_f*32768), -32768, 32767).astype(np.int64)
39
+ rhf = lambda x: np.concatenate([-x[..., HD//2:], x[..., :HD//2]], -1)
40
+ rope_fp = lambda x: x*cos_f[:, None, :] + rhf(x)*sin_f[:, None, :]
41
+ def rope_int(xq, clip=127):
42
+ rh = np.concatenate([-xq[..., HD//2:].astype(np.int64), xq[..., :HD//2].astype(np.int64)], -1)
43
+ return np.clip((xq.astype(np.int64)*cosQ[:, None, :] + rh*sinQ[:, None, :] + 16384) >> 15, -clip, clip)
44
+ def qdyn(Tf): a = float(np.max(np.abs(Tf))) or 1.0; return np.clip(np.rint(Tf/(a/127)), -127, 127).astype(np.int8), a/127
45
+ def qpc(Wf): amax = np.max(np.abs(Wf), 1); inv = np.where(amax > 0, 127/amax, 0); return np.clip(np.rint(Wf*inv[:, None]), -127, 127).astype(np.int8), np.where(amax > 0, amax/127, 1.0)
46
+ imm = lambda aq, wq: aq.astype(np.int64) @ wq.astype(np.int64).T
47
+ rms_fp = lambda h, g: h/np.sqrt(np.mean(h*h, 1, keepdims=True)+1e-6)*g
48
+ def build_exp(n=28):
49
+ ONE = 1 << 16; t = np.zeros(256, np.int64)
50
+ for d in range(256):
51
+ x = -d*(ONE//64); e = ONE; k = n
52
+ while k >= 1: e = ONE+((x*e)//ONE)//k; k -= 1
53
+ t[d] = max(0, e)
54
+ return t
55
+ EXP = build_exp()
56
+
57
+ def block_int(gam, wq_i, wqs, wk_i, wks, wv_i, wvs, alvl=127, fpsm=False):
58
+ def aq(Tf): # dynamic per-tensor activation quant to +-alvl levels
59
+ a = float(np.max(np.abs(Tf))) or 1.0; return np.clip(np.rint(Tf/(a/alvl)), -alvl, alvl).astype(np.int64), a/alvl
60
+ Xq, Xs = aq(X)
61
+ ss = np.sum(Xq**2, 1); mean = ss//D+1
62
+ inv = np.array([int(np.floor(np.sqrt((1 << 40)/m))) for m in mean], np.int64) # ~2^20/sqrt(mean) (Q20)
63
+ y = Xq*inv[:, None] # normalize(Xq) in Q20 (full precision kept)
64
+ yg = y*np.rint(gam*(1 << 12)).astype(np.int64) # = rmsnorm_out * 2^32 (integer)
65
+ xn_q, xn_s_raw = aq(yg.astype(np.float64)) # xn_q*xn_s_raw ≈ yg
66
+ xn_s = xn_s_raw / (1 << 32) # so xn_q*xn_s ≈ rmsnorm_out
67
+ def proj(wi, ws): return imm(xn_q, wi).astype(np.float64) * (xn_s*ws)[None, :]
68
+ qf = proj(wq_i, wqs).reshape(S, NH, HD) + bq.reshape(NH, HD)[None]
69
+ kf = proj(wk_i, wks).reshape(S, NKV, HD) + bk.reshape(NKV, HD)[None]
70
+ vf = proj(wv_i, wvs).reshape(S, NKV, HD) + bv.reshape(NKV, HD)[None]
71
+ qq, qs = aq(qf); kq, ks = aq(kf); vf2 = vf
72
+ qq, kq = rope_int(qq, alvl), rope_int(kq, alvl)
73
+ kq14 = np.repeat(kq, NH//NKV, 1)
74
+ scores = np.einsum('shd,thd->hst', qq.astype(np.int64), kq14.astype(np.int64))
75
+ C = qs*ks/np.sqrt(HD)
76
+ probs = np.zeros((NH, S, S))
77
+ for h in range(NH):
78
+ for s in range(S):
79
+ row = scores[h, s, :s+1]; m = row.max()
80
+ if fpsm:
81
+ e = np.exp((row - m).astype(np.float64) * C); probs[h, s, :s+1] = e / e.sum()
82
+ else:
83
+ d = np.clip(((m-row).astype(np.float64)*C*64).round().astype(int), 0, 255)
84
+ e = EXP[d]; probs[h, s, :s+1] = (e.astype(np.int64) << 16)//int(e.sum())/(1 << 16)
85
+ vf14 = np.repeat(vf2.reshape(S, NKV, HD), NH//NKV, 1)
86
+ attn = np.einsum('hst,thd->shd', probs, vf14).reshape(S, D)
87
+ aq, as_ = qdyn(attn); _, wo, wosc = T[L+"self_attn.o_proj.weight"]
88
+ o = imm(aq, wo).astype(np.float64)*(as_*wosc)[None, :]
89
+ return o
90
+
91
+ # NOTE: proj() folds only weight scale; the RMSNorm output scale is absorbed by re-quant. The bias is added
92
+ # post-hoc in fp here (scale bookkeeping is fp in this correctness cut).
93
+
94
+ # fp reference (original weights)
95
+ xn = rms_fp(X, gin)
96
+ q = (xn@Wq.T+bq).reshape(S, NH, HD); k = (xn@Wk.T+bk).reshape(S, NKV, HD); v = (xn@Wv.T+bv).reshape(S, NKV, HD)
97
+ q, k = rope_fp(q), rope_fp(k); k14 = np.repeat(k, NH//NKV, 1); v14 = np.repeat(v, NH//NKV, 1)
98
+ sc = np.einsum('shd,thd->hst', q, k14)/np.sqrt(HD)
99
+ for h in range(NH):
100
+ for s in range(S): sc[h, s, s+1:] = -1e30
101
+ af = np.zeros((S, NH, HD))
102
+ for h in range(NH):
103
+ p = np.exp(sc[h]-sc[h].max(1, keepdims=True)); p /= p.sum(1, keepdims=True); af[:, h, :] = p@v14[:, h, :]
104
+ o_fp = af.reshape(S, D)@Wo.T
105
+ rel = lambda a, b: float(np.sqrt(np.sum((a-b)**2)/(np.sum(b*b)+1e-12)))
106
+
107
+ # baseline: per-tensor, no smoothing
108
+ wq_i, wqs = qpc(Wq); wk_i, wks = qpc(Wk); wv_i, wvs = qpc(Wv)
109
+ o_base = block_int(gin, wq_i, wqs, wk_i, wks, wv_i, wvs)
110
+
111
+ # SmoothQuant: calibration per-channel activation max, fold s into gamma + qkv weights
112
+ calib = rms_fp(emb(list(range(1, 129))), gin)
113
+ act_max = np.max(np.abs(calib), 0) # [D]
114
+ w_max = np.max(np.abs(np.concatenate([Wq, Wk, Wv], 0)), 0) # [D]
115
+ alpha = 0.5
116
+ s = np.clip(np.power(act_max, alpha)/np.power(np.maximum(w_max, 1e-9), 1-alpha), 1e-2, 1e2)
117
+ gin_s = gin/s
118
+ Wq_s, Wk_s, Wv_s = Wq*s[None, :], Wk*s[None, :], Wv*s[None, :]
119
+ wq_i2, wqs2 = qpc(Wq_s); wk_i2, wks2 = qpc(Wk_s); wv_i2, wvs2 = qpc(Wv_s)
120
+ o_sq = block_int(gin_s, wq_i2, wqs2, wk_i2, wks2, wv_i2, wvs2)
121
+
122
+ o_a16 = block_int(gin, wq_i, wqs, wk_i, wks, wv_i, wvs, alvl=32767) # W8A16, no smoothing
123
+ o_sq_a16 = block_int(gin_s, wq_i2, wqs2, wk_i2, wks2, wv_i2, wvs2, alvl=32767) # W8A16 + SmoothQuant
124
+ print(f"attention out rel err (int vs fp):")
125
+ print(f" W8A8 per-tensor = {rel(o_base, o_fp):.4f}")
126
+ print(f" W8A8 SmoothQuant a=0.5 = {rel(o_sq, o_fp):.4f}")
127
+ print(f" W8A16 per-tensor = {rel(o_a16, o_fp):.4f}")
128
+ print(f" W8A16 + SmoothQuant = {rel(o_sq_a16, o_fp):.4f}")
129
+ o_a16_fpsm = block_int(gin, wq_i, wqs, wk_i, wks, wv_i, wvs, alvl=32767, fpsm=True)
130
+ o_a8_fpsm = block_int(gin, wq_i, wqs, wk_i, wks, wv_i, wvs, alvl=127, fpsm=True)
131
+ print(f" W8A16 + fp-softmax = {rel(o_a16_fpsm, o_fp):.4f} <- isolates integer softmax")
132
+ print(f" W8A8 + fp-softmax = {rel(o_a8_fpsm, o_fp):.4f}")
133
+ print(f" act outlier ratio max/median = {act_max.max()/np.median(act_max):.1f}x")
docs/evidence/canon_cuda.cu ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Canonical int8 GEMM on NVIDIA CUDA GPU (Ada sm_89) — must reproduce the CPU/Metal fingerprint.
2
+ // Same splitmix64 inputs + same int32 GEMM as canon_int.rs / canon_metal.swift.
3
+ // Target INT fnv: 4b138ed70d137781
4
+ //
5
+ // Build/run: nvcc -O3 -arch=sm_89 canon_cuda.cu -o canon_cuda && ./canon_cuda
6
+
7
+ #include <cstdio>
8
+ #include <cstdint>
9
+ #include <vector>
10
+ #include <cuda_runtime.h>
11
+
12
+ static inline uint64_t splitmix64(uint64_t &s) {
13
+ s += 0x9E3779B97F4A7C15ULL;
14
+ uint64_t z = s;
15
+ z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
16
+ z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
17
+ return z ^ (z >> 31);
18
+ }
19
+ static std::vector<int8_t> genI8(size_t n, uint64_t seed) {
20
+ uint64_t s = seed; std::vector<int8_t> v; v.reserve(n);
21
+ for (size_t i = 0; i < n; i++) v.push_back((int8_t)(splitmix64(s) % 128));
22
+ return v;
23
+ }
24
+ static uint64_t fnv(const uint8_t *p, size_t n) {
25
+ uint64_t h = 0xcbf29ce484222325ULL;
26
+ for (size_t i = 0; i < n; i++) { h ^= (uint64_t)p[i]; h *= 0x100000001b3ULL; }
27
+ return h;
28
+ }
29
+
30
+ __global__ void gemm_i8(const int8_t *A, const int8_t *B, int32_t *C,
31
+ unsigned M, unsigned K, unsigned N) {
32
+ unsigned gid = blockIdx.x * blockDim.x + threadIdx.x;
33
+ if (gid >= M * N) return;
34
+ unsigned i = gid / N, j = gid % N;
35
+ int acc = 0; // int32 accumulation
36
+ for (unsigned t = 0; t < K; t++) {
37
+ acc += (int)A[i * K + t] * (int)B[t * N + j];
38
+ }
39
+ C[gid] = acc;
40
+ }
41
+
42
+ int main() {
43
+ const unsigned M = 32, K = 8192, N = 32;
44
+ std::vector<int8_t> a = genI8((size_t)M * K, 0xA11CE);
45
+ std::vector<int8_t> b = genI8((size_t)K * N, 0xB0B);
46
+ std::vector<int32_t> c((size_t)M * N, 0);
47
+
48
+ int8_t *dA, *dB; int32_t *dC;
49
+ cudaMalloc(&dA, a.size()); cudaMalloc(&dB, b.size());
50
+ cudaMalloc(&dC, c.size() * sizeof(int32_t));
51
+ cudaMemcpy(dA, a.data(), a.size(), cudaMemcpyHostToDevice);
52
+ cudaMemcpy(dB, b.data(), b.size(), cudaMemcpyHostToDevice);
53
+ unsigned total = M * N, tpb = 64, blocks = (total + tpb - 1) / tpb;
54
+ gemm_i8<<<blocks, tpb>>>(dA, dB, dC, M, K, N);
55
+ cudaError_t err = cudaDeviceSynchronize();
56
+ if (err != cudaSuccess) { printf("cuda error: %s\n", cudaGetErrorString(err)); return 1; }
57
+ cudaMemcpy(c.data(), dC, c.size() * sizeof(int32_t), cudaMemcpyDeviceToHost);
58
+
59
+ // serialize int32 little-endian, FNV
60
+ std::vector<uint8_t> bytes; bytes.reserve(c.size() * 4);
61
+ for (int32_t v32 : c) {
62
+ uint32_t v = (uint32_t)v32;
63
+ bytes.push_back(v & 0xff); bytes.push_back((v >> 8) & 0xff);
64
+ bytes.push_back((v >> 16) & 0xff); bytes.push_back((v >> 24) & 0xff);
65
+ }
66
+ uint64_t h = fnv(bytes.data(), bytes.size());
67
+ cudaDeviceProp prop; cudaGetDeviceProperties(&prop, 0);
68
+ printf("backend = NVIDIA CUDA GPU (%s, sm_%d%d)\n", prop.name, prop.major, prop.minor);
69
+ printf("shape M,K,N = %u,%u,%u\n", M, K, N);
70
+ printf("INT gpu fnv = %016llx\n", (unsigned long long)h);
71
+ printf("TARGET (CPU/Metal) = 4b138ed70d137781\n");
72
+ printf("MATCH = %s\n", h == 0x4b138ed70d137781ULL ? "OK byte-identical" : "MISMATCH");
73
+ cudaFree(dA); cudaFree(dB); cudaFree(dC);
74
+ return 0;
75
+ }
docs/evidence/canon_dump.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import sys, json, struct
3
+ p = sys.argv[1]
4
+ with open(p, "rb") as f:
5
+ assert f.read(8) == b"CANONv1\n"
6
+ n = struct.unpack("<Q", f.read(8))[0]
7
+ hdr = json.loads(f.read(n))
8
+ print("format", hdr["format"], "model", hdr["model"], "rounding", hdr["rounding"])
9
+ for t in hdr["tensors"]:
10
+ nm = t["name"]
11
+ if nm.startswith("model.layers.0.") or "layers." not in nm:
12
+ print(f"{t['kind']:16s} {str(t['shape']):16s} {nm}")
docs/evidence/canon_extract.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Extract the layer-0 attention tensors (+ embedding rows for the test tokens) from qwen05.canon into a
3
+ # flat little-endian binary that standalone rustc can read with no JSON parser. Raw (m0,shift) scales are
4
+ # passed through verbatim; the Rust port replicates qm_from_fixed.
5
+ import sys, json, struct
6
+ CANON, OUT = sys.argv[1], sys.argv[2]
7
+ IDS = [9707, 11, 1879, 30]
8
+ f = open(CANON, "rb"); assert f.read(8) == b"CANONv1\n"
9
+ n = struct.unpack("<Q", f.read(8))[0]; hdr = json.loads(f.read(n)); body = f.read()
10
+ dir = {t["name"]: t for t in hdr["tensors"]}
11
+
12
+ def w2d(name): # -> (out, in, int8 bytes, [(m0,shift)]*out)
13
+ t = dir[name]; o, i = t["shape"]
14
+ w = body[t["w_off"]:t["w_off"]+o*i]
15
+ sc = [struct.unpack("<ii", body[t["s_off"]+8*c:t["s_off"]+8*c+8]) for c in range(o)]
16
+ return o, i, w, sc
17
+ def v1d(name): # -> (n, int32 bytes, (m0,shift))
18
+ t = dir[name]; nn = t["shape"][0]
19
+ v = body[t["w_off"]:t["w_off"]+nn*4]
20
+ m0, sh = struct.unpack("<ii", body[t["s_off"]:t["s_off"]+8])
21
+ return nn, v, (m0, sh)
22
+
23
+ out = bytearray(b"L0BIN\0\0\0")
24
+ def put_u32(x): out.extend(struct.pack("<I", x))
25
+ def put_scales(scs):
26
+ put_u32(len(scs))
27
+ for m0, sh in scs: out.extend(struct.pack("<ii", m0, sh))
28
+ def put_w2d(name):
29
+ o, i, w, sc = w2d(name); out.append(0); put_u32(o); put_u32(i); out.extend(w); put_scales(sc)
30
+ def put_v1d(name):
31
+ nn, v, s = v1d(name); out.append(1); put_u32(nn); put_u32(0); out.extend(v); put_scales([s])
32
+
33
+ L = "model.layers.0."
34
+ put_v1d(L+"input_layernorm.weight")
35
+ put_w2d(L+"self_attn.q_proj.weight"); put_v1d(L+"self_attn.q_proj.bias")
36
+ put_w2d(L+"self_attn.k_proj.weight"); put_v1d(L+"self_attn.k_proj.bias")
37
+ put_w2d(L+"self_attn.v_proj.weight"); put_v1d(L+"self_attn.v_proj.bias")
38
+ put_w2d(L+"self_attn.o_proj.weight")
39
+ # X: 4 embedding rows (as a w2d [4, 896] int8 + 4 scales)
40
+ et = dir["model.embed_tokens.weight"]; D = et["shape"][1]
41
+ out.append(0); put_u32(len(IDS)); put_u32(D)
42
+ scs = []
43
+ for tid in IDS:
44
+ out.extend(body[et["w_off"]+tid*D: et["w_off"]+(tid+1)*D])
45
+ scs.append(struct.unpack("<ii", body[et["s_off"]+8*tid: et["s_off"]+8*tid+8]))
46
+ put_scales(scs)
47
+ # MLP tensors (4b-3): post_attention_layernorm + gate/up/down (no bias)
48
+ put_v1d(L+"post_attention_layernorm.weight")
49
+ put_w2d(L+"mlp.gate_proj.weight")
50
+ put_w2d(L+"mlp.up_proj.weight")
51
+ put_w2d(L+"mlp.down_proj.weight")
52
+
53
+ # RoPE Q15 tables + integer EXP LUT (appended as i64 LE, fixed sizes: S*HD, S*HD, 256)
54
+ import numpy as np
55
+ HD = 64; THETA = 1000000.0; S = len(IDS)
56
+ inv_freq = 1.0/(THETA**(np.arange(0, HD, 2)/HD)); ang = np.outer(np.arange(S), inv_freq)
57
+ cosF = np.concatenate([np.cos(ang)]*2, 1); sinF = np.concatenate([np.sin(ang)]*2, 1)
58
+ cosQ = np.clip(np.rint(cosF*32768), -32768, 32767).astype(np.int64).flatten()
59
+ sinQ = np.clip(np.rint(sinF*32768), -32768, 32767).astype(np.int64).flatten()
60
+ def build_exp(nt=28):
61
+ ONE = 1 << 16; t = []
62
+ for d in range(256):
63
+ x = -d*(ONE//64); e = ONE; k = nt
64
+ while k >= 1: e = ONE+((x*e)//ONE)//k; k -= 1
65
+ t.append(max(0, e))
66
+ return t
67
+ EXP = build_exp()
68
+ SIG = np.clip(np.rint(1.0/(1.0+np.exp(-(np.arange(4096)-2048)/128.0))*(1 << 16)), 0, 1 << 16).astype(np.int64)
69
+ for arr in [cosQ, sinQ, np.array(EXP, np.int64), SIG]:
70
+ for x in arr: out.extend(struct.pack("<q", int(x)))
71
+
72
+ open(OUT, "wb").write(out)
73
+ print(f"wrote {OUT}: {len(out)} bytes, tokens={IDS}, +rope/exp/sig tables +mlp")
docs/evidence/canon_extract_full.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Full-model extractor: qwen05.canon -> model.bin (flat LE, no JSON) for the Rust/CUDA full-model ports.
3
+ # Layout: "QCM0\0\0\0\0" | u32 NL | u32 SMAX | embed(w2d) | NL x [ln1(v1d) qw qb kw kb vw vb ow(w2d/v1d)
4
+ # ln2(v1d) gate up down(w2d)] | final_norm(v1d) | cos[SMAX*HD] sin[SMAX*HD] EXP[2048] SIG[16384] (i64 LE)
5
+ import sys, json, struct
6
+ import numpy as np
7
+ CANON, OUT = sys.argv[1], sys.argv[2]
8
+ NL, SMAX, HD, THETA = 24, 32, 64, 1000000.0
9
+ f = open(CANON, "rb"); assert f.read(8) == b"CANONv1\n"
10
+ n = struct.unpack("<Q", f.read(8))[0]; hdr = json.loads(f.read(n)); body = f.read()
11
+ dir = {t["name"]: t for t in hdr["tensors"]}
12
+ out = bytearray(b"QCM0\0\0\0\0")
13
+ out.extend(struct.pack("<II", NL, SMAX))
14
+ def put_scales(scs):
15
+ out.extend(struct.pack("<I", len(scs)))
16
+ for m0, sh in scs: out.extend(struct.pack("<ii", m0, sh))
17
+ def put_w2d(name):
18
+ t = dir[name]; o, i = t["shape"]
19
+ out.append(0); out.extend(struct.pack("<II", o, i))
20
+ out.extend(body[t["w_off"]:t["w_off"]+o*i])
21
+ put_scales([struct.unpack("<ii", body[t["s_off"]+8*c:t["s_off"]+8*c+8]) for c in range(o)])
22
+ def put_v1d(name):
23
+ t = dir[name]; nn = t["shape"][0]
24
+ out.append(1); out.extend(struct.pack("<II", nn, 0))
25
+ out.extend(body[t["w_off"]:t["w_off"]+nn*4])
26
+ put_scales([struct.unpack("<ii", body[t["s_off"]:t["s_off"]+8])])
27
+ put_w2d("model.embed_tokens.weight")
28
+ for l in range(NL):
29
+ L = f"model.layers.{l}."
30
+ put_v1d(L+"input_layernorm.weight")
31
+ put_w2d(L+"self_attn.q_proj.weight"); put_v1d(L+"self_attn.q_proj.bias")
32
+ put_w2d(L+"self_attn.k_proj.weight"); put_v1d(L+"self_attn.k_proj.bias")
33
+ put_w2d(L+"self_attn.v_proj.weight"); put_v1d(L+"self_attn.v_proj.bias")
34
+ put_w2d(L+"self_attn.o_proj.weight")
35
+ put_v1d(L+"post_attention_layernorm.weight")
36
+ put_w2d(L+"mlp.gate_proj.weight"); put_w2d(L+"mlp.up_proj.weight"); put_w2d(L+"mlp.down_proj.weight")
37
+ put_v1d("model.norm.weight")
38
+ inv_freq = 1.0/(THETA**(np.arange(0, HD, 2)/HD)); ang = np.outer(np.arange(SMAX), inv_freq)
39
+ cosF = np.concatenate([np.cos(ang)]*2, 1); sinF = np.concatenate([np.sin(ang)]*2, 1)
40
+ cosQ = np.clip(np.rint(cosF*32768), -32768, 32767).astype(np.int64).flatten()
41
+ sinQ = np.clip(np.rint(sinF*32768), -32768, 32767).astype(np.int64).flatten()
42
+ def build_exp(nt=28):
43
+ ONE = 1 << 16; t = []
44
+ for d in range(2048):
45
+ x = -d*(ONE//512); e = ONE; k = nt
46
+ while k >= 1: e = ONE+((x*e)//ONE)//k; k -= 1
47
+ t.append(max(0, e))
48
+ return t
49
+ EXP = build_exp()
50
+ SIG = np.clip(np.rint(1.0/(1.0+np.exp(-(np.arange(16384)-8192)/512.0))*(1 << 16)), 0, 1 << 16).astype(np.int64)
51
+ for arr in [cosQ, sinQ, np.array(EXP, np.int64), SIG]:
52
+ for x in arr: out.extend(struct.pack("<q", int(x)))
53
+ open(OUT, "wb").write(out)
54
+ import hashlib
55
+ print(f"wrote {OUT}: {len(out):,} bytes blake2b16={hashlib.blake2b(bytes(out), digest_size=16).hexdigest()}")
docs/evidence/canon_int.rs ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Canonical integer compute — cross-hardware bit-identity demonstration.
2
+ //
3
+ // CORE THEOREM. int32 addition is a commutative ring even under two's-complement wrapping:
4
+ // (a + b) + c == a + (b + c) and a + b == b + a (mod 2^32), exactly, always.
5
+ // Therefore an int8×int8→int32 GEMM yields byte-identical results REGARDLESS of the reduction
6
+ // order — sequential, pairwise tree, SIMD-lane, GPU warp-shuffle, multi-tile — i.e. regardless of
7
+ // microarchitecture (RTX 20 sm_75 vs RTX 40 sm_89, Apple, CPU, …).
8
+ // IEEE-754 float addition is NOT associative, so a float GEMM's result depends on the order and
9
+ // hence on the hardware. This program demonstrates both facts on real hardware.
10
+ //
11
+ // Build/run (no deps): rustc -O canon_int.rs -o canon_int && ./canon_int
12
+
13
+ // ---- deterministic PRNG (splitmix64) so both machines generate identical inputs ----
14
+ fn splitmix64(s: &mut u64) -> u64 {
15
+ *s = s.wrapping_add(0x9E3779B97F4A7C15);
16
+ let mut z = *s;
17
+ z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
18
+ z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
19
+ z ^ (z >> 31)
20
+ }
21
+ // Values in [0,127] (valid int8 magnitudes) so the int32 accumulator grows past 2^24 — the regime
22
+ // where fp32 can no longer hold the running sum exactly and its result becomes order-dependent.
23
+ fn gen_i8(n: usize, seed: u64) -> Vec<i8> {
24
+ let mut s = seed;
25
+ (0..n).map(|_| (splitmix64(&mut s) % 128) as i8).collect()
26
+ }
27
+
28
+ // ---- int8×int8 -> int32 GEMM, two DIFFERENT reduction orders over k ----
29
+ fn dot_i32_seq(a: &[i8], b: &[i8]) -> i32 {
30
+ let mut acc: i32 = 0;
31
+ for i in 0..a.len() {
32
+ acc = acc.wrapping_add(a[i] as i32 * b[i] as i32);
33
+ }
34
+ acc
35
+ }
36
+ // pairwise / tree reduction — a genuinely different associativity grouping
37
+ fn dot_i32_tree(a: &[i8], b: &[i8]) -> i32 {
38
+ let mut prod: Vec<i32> = (0..a.len()).map(|i| a[i] as i32 * b[i] as i32).collect();
39
+ while prod.len() > 1 {
40
+ let mut next = Vec::with_capacity(prod.len().div_ceil(2));
41
+ let mut i = 0;
42
+ while i + 1 < prod.len() {
43
+ next.push(prod[i].wrapping_add(prod[i + 1]));
44
+ i += 2;
45
+ }
46
+ if i < prod.len() { next.push(prod[i]); }
47
+ prod = next;
48
+ }
49
+ prod[0]
50
+ }
51
+ // ---- same two orders in f32 (cast int8 to f32) ----
52
+ fn dot_f32_seq(a: &[i8], b: &[i8]) -> f32 {
53
+ let mut acc = 0f32;
54
+ for i in 0..a.len() { acc += a[i] as f32 * b[i] as f32; }
55
+ acc
56
+ }
57
+ fn dot_f32_tree(a: &[i8], b: &[i8]) -> f32 {
58
+ let mut prod: Vec<f32> = (0..a.len()).map(|i| a[i] as f32 * b[i] as f32).collect();
59
+ while prod.len() > 1 {
60
+ let mut next = Vec::with_capacity(prod.len().div_ceil(2));
61
+ let mut i = 0;
62
+ while i + 1 < prod.len() { next.push(prod[i] + prod[i + 1]); i += 2; }
63
+ if i < prod.len() { next.push(prod[i]); }
64
+ prod = next;
65
+ }
66
+ prod[0]
67
+ }
68
+
69
+ // ---- fixed-point requantization int32 -> int8 (integer-only, gemmlowp/TFLite style) ----
70
+ // q = clamp( (acc * mult + round) >> shift , -127, 127 ), round = 1<<(shift-1). Deterministic, no fp.
71
+ fn requant_i8(acc: i32, mult: i32, shift: u32) -> i8 {
72
+ let round: i64 = 1i64 << (shift - 1);
73
+ let v = ((acc as i64 * mult as i64) + round) >> shift;
74
+ v.clamp(-127, 127) as i8
75
+ }
76
+
77
+ // ---- FNV-1a 64 over bytes: identical bytes -> identical fingerprint ----
78
+ fn fnv(bytes: &[u8]) -> u64 {
79
+ let mut h: u64 = 0xcbf29ce484222325;
80
+ for &b in bytes { h ^= b as u64; h = h.wrapping_mul(0x100000001b3); }
81
+ h
82
+ }
83
+
84
+ fn main() {
85
+ let (m, k, n) = (32usize, 8192usize, 32usize); // K large so f32 loses precision across orders
86
+ let a = gen_i8(m * k, 0xA11CE); // weights
87
+ let b = gen_i8(k * n, 0xB0B); // activations
88
+ let arow = |i: usize| &a[i * k..(i + 1) * k];
89
+ // B is K×N; take column j as a strided gather into a contiguous vec
90
+ let bcol = |j: usize| -> Vec<i8> { (0..k).map(|t| b[t * n + j]).collect() };
91
+
92
+ let mut int_seq = vec![0i32; m * n];
93
+ let mut int_tree = vec![0i32; m * n];
94
+ let mut fp_seq = vec![0f32; m * n];
95
+ let mut fp_tree = vec![0f32; m * n];
96
+ for i in 0..m {
97
+ for j in 0..n {
98
+ let bc = bcol(j);
99
+ int_seq[i * n + j] = dot_i32_seq(arow(i), &bc);
100
+ int_tree[i * n + j] = dot_i32_tree(arow(i), &bc);
101
+ fp_seq[i * n + j] = dot_f32_seq(arow(i), &bc);
102
+ fp_tree[i * n + j] = dot_f32_tree(arow(i), &bc);
103
+ }
104
+ }
105
+ let bytes_i32 = |v: &[i32]| -> Vec<u8> { v.iter().flat_map(|x| x.to_le_bytes()).collect() };
106
+ let bytes_f32 = |v: &[f32]| -> Vec<u8> { v.iter().flat_map(|x| x.to_bits().to_le_bytes()).collect() };
107
+
108
+ let int_seq_h = fnv(&bytes_i32(&int_seq));
109
+ let int_tree_h = fnv(&bytes_i32(&int_tree));
110
+ let fp_seq_h = fnv(&bytes_f32(&fp_seq));
111
+ let fp_tree_h = fnv(&bytes_f32(&fp_tree));
112
+
113
+ // canonical commitment = requantized int output (what a next layer would consume)
114
+ let q: Vec<i8> = int_seq.iter().map(|&x| requant_i8(x, 1_073_741_824, 38)).collect();
115
+ let canon = fnv(&q.iter().map(|&x| x as u8).collect::<Vec<u8>>());
116
+
117
+ println!("arch = {}", std::env::consts::ARCH);
118
+ println!("shape M,K,N = {},{},{}", m, k, n);
119
+ println!("INT seq fnv = {:016x}", int_seq_h);
120
+ println!("INT tree fnv = {:016x}", int_tree_h);
121
+ println!("INT order-invariant= {}", if int_seq_h == int_tree_h { "MATCH ✅ (hardware-independent)" } else { "DIFFER ❌" });
122
+ println!("FP32 seq fnv = {:016x}", fp_seq_h);
123
+ println!("FP32 tree fnv = {:016x}", fp_tree_h);
124
+ println!("FP32 order-invariant= {}", if fp_seq_h == fp_tree_h { "MATCH (coincidence at this K)" } else { "DIFFER ⚠️ (order-dependent → hardware-dependent)" });
125
+ println!("CANONICAL commitment= {:016x} <-- must be identical on every machine/backend", canon);
126
+ }
docs/evidence/canon_layer.rs ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Canonical integer LAYER (increment 3): int8 GEMM -> +int32 bias -> fixed-point requant -> integer LUT.
2
+ // NO floating point anywhere on the compute path. Must be byte-identical across CPU / Metal / CUDA.
3
+ // This CPU build is the reference; it prints the target fingerprint the GPU builds must reproduce.
4
+ //
5
+ // Build/run: rustc -O canon_layer.rs -o canon_layer && ./canon_layer
6
+
7
+ fn splitmix64(s: &mut u64) -> u64 {
8
+ *s = s.wrapping_add(0x9E3779B97F4A7C15);
9
+ let mut z = *s;
10
+ z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
11
+ z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
12
+ z ^ (z >> 31)
13
+ }
14
+ fn gen_i8(n: usize, seed: u64) -> Vec<i8> {
15
+ let mut s = seed;
16
+ (0..n).map(|_| (splitmix64(&mut s) % 128) as i8).collect()
17
+ }
18
+ fn fnv(bytes: &[u8]) -> u64 {
19
+ let mut h: u64 = 0xcbf29ce484222325;
20
+ for &b in bytes { h ^= b as u64; h = h.wrapping_mul(0x100000001b3); }
21
+ h
22
+ }
23
+
24
+ // --- pinned integer nonlinearity (fast-sigmoid SiLU), fully integer -> a 256-entry int8 LUT ---
25
+ // silu(x) = x*sigmoid(x); sigmoid via fast-sigmoid x/(1+|x|). Fixed-point scale S, truncating int div
26
+ // (toward zero — identical semantics in Rust / CUDA / Metal). No fp.
27
+ fn silu_int(x: i32) -> i8 {
28
+ const S: i32 = 4096;
29
+ let ax = x.abs();
30
+ let fs = (x * S) / (1 + ax); // ~[-S, S]
31
+ let sig = (fs + S) / 2; // ~[0, S]
32
+ let silu = (x * sig) / S; // ~ x*sigmoid(x)
33
+ silu.clamp(-127, 127) as i8
34
+ }
35
+ fn build_lut() -> [i8; 256] {
36
+ let mut t = [0i8; 256];
37
+ for i in 0..256 { t[i] = silu_int(i as i32 - 128); } // index = q + 128, q in [-128,127]
38
+ t
39
+ }
40
+
41
+ // --- fixed-point requantization int32 -> int8 (gemmlowp/TFLite): integer-only, defined rounding ---
42
+ fn requant(pre: i32, m0: i32, shift: u32) -> i8 {
43
+ let round: i64 = 1i64 << (shift - 1);
44
+ let v = ((pre as i64) * (m0 as i64) + round) >> shift;
45
+ v.clamp(-127, 127) as i8
46
+ }
47
+
48
+ fn main() {
49
+ let (m, k, n) = (16usize, 2048usize, 16usize);
50
+ let x = gen_i8(m * k, 0x51); // activations, row-major M×K
51
+ let w = gen_i8(k * n, 0x52); // weights, row-major K×N
52
+ // per-output-channel bias (i32) and requant multiplier (i32); shift fixed. Deterministic.
53
+ let mut sb = 0x53u64;
54
+ let bias: Vec<i32> = (0..n).map(|_| -(8_300_000i32) + (splitmix64(&mut sb) % 400_000) as i32).collect();
55
+ let mut sm = 0x54u64;
56
+ let m0: Vec<i32> = (0..n).map(|_| 700 + (splitmix64(&mut sm) % 200) as i32).collect();
57
+ let shift: u32 = 20;
58
+ let lut = build_lut();
59
+
60
+ let mut out = vec![0i8; m * n];
61
+ let mut qmin = i32::MAX; let mut qmax = i32::MIN; let mut qsum = 0i64;
62
+ for i in 0..m {
63
+ for j in 0..n {
64
+ let mut acc: i32 = 0;
65
+ for t in 0..k { acc = acc.wrapping_add(x[i * k + t] as i32 * w[t * n + j] as i32); }
66
+ let pre = acc.wrapping_add(bias[j]);
67
+ let q = requant(pre, m0[j], shift);
68
+ qmin = qmin.min(q as i32); qmax = qmax.max(q as i32); qsum += q as i64;
69
+ out[i * n + j] = lut[(q as i32 + 128) as usize];
70
+ }
71
+ }
72
+ let bytes: Vec<u8> = out.iter().map(|&x| x as u8).collect();
73
+ let h = fnv(&bytes);
74
+ // LUT fingerprint too (so GPU builds can confirm identical pinned table)
75
+ let lut_h = fnv(&lut.iter().map(|&x| x as u8).collect::<Vec<u8>>());
76
+
77
+ println!("backend = CPU ({})", std::env::consts::ARCH);
78
+ println!("shape M,K,N = {},{},{}", m, k, n);
79
+ println!("q range (post-requant) = [{}, {}] mean~{}", qmin, qmax, qsum / (m * n) as i64);
80
+ println!("out[0..8] = {:?}", &out[0..8]);
81
+ println!("LUT fnv = {:016x}", lut_h);
82
+ println!("LAYER fnv = {:016x} <-- GPU builds must reproduce this", h);
83
+ }
docs/evidence/canon_layer_cuda.cu ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Canonical integer LAYER on NVIDIA CUDA GPU (increment 3). Must reproduce the CPU/Metal fingerprints:
2
+ // LAYER fnv = 440bd1b93d991fe0 , LUT fnv = 8ab2052fcf2b005a
3
+ // Build/run: nvcc -O3 -arch=sm_89 canon_layer_cuda.cu -o canon_layer_cuda && ./canon_layer_cuda
4
+
5
+ #include <cstdio>
6
+ #include <cstdint>
7
+ #include <vector>
8
+ #include <cuda_runtime.h>
9
+
10
+ static inline uint64_t splitmix64(uint64_t &s) {
11
+ s += 0x9E3779B97F4A7C15ULL;
12
+ uint64_t z = s;
13
+ z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
14
+ z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
15
+ return z ^ (z >> 31);
16
+ }
17
+ static std::vector<int8_t> genI8(size_t n, uint64_t seed) {
18
+ uint64_t s = seed; std::vector<int8_t> v; v.reserve(n);
19
+ for (size_t i = 0; i < n; i++) v.push_back((int8_t)(splitmix64(s) % 128));
20
+ return v;
21
+ }
22
+ static uint64_t fnv(const uint8_t *p, size_t n) {
23
+ uint64_t h = 0xcbf29ce484222325ULL;
24
+ for (size_t i = 0; i < n; i++) { h ^= (uint64_t)p[i]; h *= 0x100000001b3ULL; }
25
+ return h;
26
+ }
27
+ static int8_t silu_int(int32_t x) {
28
+ const int32_t S = 4096;
29
+ int32_t ax = x < 0 ? -x : x;
30
+ int32_t fs = (x * S) / (1 + ax);
31
+ int32_t sig = (fs + S) / 2;
32
+ int32_t silu = (x * sig) / S;
33
+ if (silu < -127) silu = -127; if (silu > 127) silu = 127;
34
+ return (int8_t)silu;
35
+ }
36
+
37
+ __global__ void layer(const int8_t *X, const int8_t *W, const int32_t *BIAS,
38
+ const int32_t *M0, const int8_t *LUT, int8_t *OUT,
39
+ unsigned M, unsigned K, unsigned N, unsigned shift) {
40
+ unsigned gid = blockIdx.x * blockDim.x + threadIdx.x;
41
+ if (gid >= M * N) return;
42
+ unsigned i = gid / N, j = gid % N;
43
+ int acc = 0;
44
+ for (unsigned t = 0; t < K; t++) acc += (int)X[i * K + t] * (int)W[t * N + j];
45
+ int pre = acc + BIAS[j];
46
+ long long round = (long long)1 << (shift - 1);
47
+ long long v = ((long long)pre * (long long)M0[j] + round) >> shift;
48
+ int q = v < -127 ? -127 : (v > 127 ? 127 : (int)v);
49
+ OUT[gid] = LUT[q + 128];
50
+ }
51
+
52
+ int main() {
53
+ const unsigned M = 16, K = 2048, N = 16, shift = 20;
54
+ std::vector<int8_t> x = genI8((size_t)M * K, 0x51);
55
+ std::vector<int8_t> w = genI8((size_t)K * N, 0x52);
56
+ std::vector<int32_t> bias(N), m0(N);
57
+ uint64_t sb = 0x53; for (unsigned j = 0; j < N; j++) bias[j] = -8300000 + (int32_t)(splitmix64(sb) % 400000);
58
+ uint64_t sm = 0x54; for (unsigned j = 0; j < N; j++) m0[j] = 700 + (int32_t)(splitmix64(sm) % 200);
59
+ std::vector<int8_t> lut(256);
60
+ for (int i = 0; i < 256; i++) lut[i] = silu_int(i - 128);
61
+ std::vector<int8_t> out((size_t)M * N, 0);
62
+
63
+ int8_t *dX, *dW, *dL, *dO; int32_t *dB, *dM;
64
+ cudaMalloc(&dX, x.size()); cudaMalloc(&dW, w.size()); cudaMalloc(&dL, lut.size());
65
+ cudaMalloc(&dO, out.size()); cudaMalloc(&dB, N * sizeof(int32_t)); cudaMalloc(&dM, N * sizeof(int32_t));
66
+ cudaMemcpy(dX, x.data(), x.size(), cudaMemcpyHostToDevice);
67
+ cudaMemcpy(dW, w.data(), w.size(), cudaMemcpyHostToDevice);
68
+ cudaMemcpy(dL, lut.data(), lut.size(), cudaMemcpyHostToDevice);
69
+ cudaMemcpy(dB, bias.data(), N * sizeof(int32_t), cudaMemcpyHostToDevice);
70
+ cudaMemcpy(dM, m0.data(), N * sizeof(int32_t), cudaMemcpyHostToDevice);
71
+ unsigned total = M * N, tpb = 64, blocks = (total + tpb - 1) / tpb;
72
+ layer<<<blocks, tpb>>>(dX, dW, dB, dM, dL, dO, M, K, N, shift);
73
+ cudaError_t err = cudaDeviceSynchronize();
74
+ if (err != cudaSuccess) { printf("cuda error: %s\n", cudaGetErrorString(err)); return 1; }
75
+ cudaMemcpy(out.data(), dO, out.size(), cudaMemcpyDeviceToHost);
76
+
77
+ std::vector<uint8_t> ob(out.size());
78
+ for (size_t i = 0; i < out.size(); i++) ob[i] = (uint8_t)out[i];
79
+ std::vector<uint8_t> lb(lut.size());
80
+ for (size_t i = 0; i < lut.size(); i++) lb[i] = (uint8_t)lut[i];
81
+ uint64_t h = fnv(ob.data(), ob.size());
82
+ uint64_t lh = fnv(lb.data(), lb.size());
83
+ cudaDeviceProp prop; cudaGetDeviceProperties(&prop, 0);
84
+ printf("backend = NVIDIA CUDA GPU (%s, sm_%d%d)\n", prop.name, prop.major, prop.minor);
85
+ printf("LUT fnv = %016llx (target 8ab2052fcf2b005a)\n", (unsigned long long)lh);
86
+ printf("LAYER fnv = %016llx (target 440bd1b93d991fe0)\n", (unsigned long long)h);
87
+ printf("MATCH = %s\n", h == 0x440bd1b93d991fe0ULL ? "OK byte-identical" : "MISMATCH");
88
+ return 0;
89
+ }
docs/evidence/canon_layer_int.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # inc4b-3 reference: FULLY-INTEGER full transformer layer (Qwen2.5-0.5B layer 0) = attention (4b-2b) +
3
+ # SwiGLU MLP + 2nd RMSNorm + residuals. No fp on the compute path. Prints accuracy vs fp and the layer
4
+ # fingerprint that the Rust/CUDA ports must reproduce.
5
+ import sys, json, struct, numpy as np
6
+ CANON = sys.argv[1] if len(sys.argv) > 1 else "/root/models/qwen05.canon"
7
+ D, NH, NKV, HD, THETA, S, FF = 896, 14, 2, 64, 1000000.0, 4, 4864
8
+ SC = 30; HALF = 1 << (SC-1); ALVL = 32767
9
+
10
+ def qm_from_fixed(m0, shift):
11
+ e = shift - 1
12
+ return (m0 << e) if e >= 0 else ((m0 + (1 << (-e-1))) >> (-e))
13
+ def apply(acc, sq): return (acc*sq + HALF) >> SC
14
+ def mul(a, b): return (a*b + HALF) >> SC
15
+ def scale_from(amax, cur): return (amax*cur)//ALVL
16
+ def isqrt(n):
17
+ if n <= 0: return 0
18
+ x = int(n); y = (x+1)//2
19
+ while y < x: x = y; y = (x + int(n)//x)//2
20
+ return x
21
+ def aq(v, cur):
22
+ amax = int(np.max(np.abs(v))) or 1
23
+ q = (v*ALVL*2 + np.sign(v)*amax)//(2*amax)
24
+ return q.astype(np.int64), scale_from(amax, cur)
25
+ def read_canon(path):
26
+ f = open(path, "rb"); assert f.read(8) == b"CANONv1\n"
27
+ n = struct.unpack("<Q", f.read(8))[0]; hdr = json.loads(f.read(n)); body = f.read(); out = {}
28
+ for t in hdr["tensors"]:
29
+ nm, k, shp = t["name"], t["kind"], t["shape"]
30
+ if k == "w2d_int8_pcs":
31
+ o, i = shp
32
+ w = np.frombuffer(body[t["w_off"]:t["w_off"]+o*i], np.int8).reshape(o, i).astype(np.int64)
33
+ sc = np.frombuffer(body[t["s_off"]:t["s_off"]+o*8], np.int32).reshape(o, 2)
34
+ sq = np.array([qm_from_fixed(int(a), int(b)) for a, b in sc], object)
35
+ out[nm] = ("w", w, sq)
36
+ else:
37
+ v = np.frombuffer(body[t["w_off"]:t["w_off"]+shp[0]*4], np.int32).astype(np.int64)
38
+ m0, sh = struct.unpack("<ii", body[t["s_off"]:t["s_off"]+8]); out[nm] = ("v", v, qm_from_fixed(m0, sh))
39
+ return out
40
+ T = read_canon(CANON); L = "model.layers.0."
41
+ def imm(a, w): return a @ w.T
42
+ inv_freq = 1.0/(THETA**(np.arange(0, HD, 2)/HD)); ang = np.outer(np.arange(S), inv_freq)
43
+ cosF = np.concatenate([np.cos(ang)]*2, 1); sinF = np.concatenate([np.sin(ang)]*2, 1)
44
+ cosQ = np.clip(np.rint(cosF*32768), -32768, 32767).astype(np.int64); sinQ = np.clip(np.rint(sinF*32768), -32768, 32767).astype(np.int64)
45
+ def rope_int(xq):
46
+ rh = np.concatenate([-xq[..., HD//2:], xq[..., :HD//2]], -1)
47
+ return (xq*cosQ[:, None, :] + rh*sinQ[:, None, :] + 16384) >> 15
48
+ def build_exp(nt=28):
49
+ ONE = 1 << 16; t = np.zeros(256, np.int64)
50
+ for d in range(256):
51
+ x = -d*(ONE//64); e = ONE; k = nt
52
+ while k >= 1: e = ONE+((x*e)//ONE)//k; k -= 1
53
+ t[d] = max(0, e)
54
+ return t
55
+ EXP = build_exp()
56
+ # sigmoid LUT: idx 0..4095 -> g=(idx-2048)/128, sig=sigmoid(g) in Q16 (pinned; offline fp table)
57
+ SIG = np.clip(np.rint(1.0/(1.0+np.exp(-(np.arange(4096)-2048)/128.0))*(1 << 16)), 0, 1 << 16).astype(np.int64)
58
+
59
+ Wemb, Wemb_sq = T["model.embed_tokens.weight"][1], T["model.embed_tokens.weight"][2]
60
+ ids = [9707, 11, 1879, 30][:S]
61
+ Xv = np.stack([Wemb[i] for i in ids]).astype(np.int64); Xsq = [int(Wemb_sq[i]) for i in ids]
62
+
63
+ def rmsnorm_q(hv, gname): # hv int [S,D], scale-invariant -> xn_q int16 [S,D], xn_sq
64
+ gv, gsq = T[gname][1], T[gname][2]
65
+ ss = np.sum(hv**2, 1); mean = ss//D + 1
66
+ inv = np.array([isqrt((1 << 40)//int(m)) for m in mean], np.int64)
67
+ yg = (hv*inv[:, None])*gv[None, :]
68
+ xn_q, s0 = aq(yg, 1 << SC)
69
+ return xn_q, mul(mul(s0, qm_from_fixed(1, 11)), int(gsq))
70
+ def proj(wname, bname, xn_q, xn_sq): # W8A16 int8 GEMM (+optional bias) -> int16 + scale
71
+ _, wv, wsq = T[wname]; acc = imm(xn_q, wv); OUT = acc.shape[1]
72
+ colsq = [mul(int(xn_sq), int(wsq[o])) for o in range(OUT)]
73
+ if bname:
74
+ bv, bsq = T[bname][1], int(T[bname][2])
75
+ bacc = np.array([(((int(bv[o])*bsq*(1 << SC)+(cs >> 1))//cs) >> SC) if (cs := colsq[o]) else 0 for o in range(OUT)], np.int64)
76
+ else:
77
+ bacc = np.zeros(OUT, np.int64)
78
+ accb = acc + bacc[None, :]; cmax = max(colsq)
79
+ resc = np.empty_like(acc)
80
+ for o in range(OUT): resc[:, o] = apply(accb[:, o], (colsq[o] << SC)//cmax)
81
+ q, sq = aq(resc.reshape(-1), cmax); return q.reshape(acc.shape), sq
82
+
83
+ # ---- attention sub-block ----
84
+ xn_q, xn_sq = rmsnorm_q(Xv, L+"input_layernorm.weight")
85
+ qf, qsq = proj(L+"self_attn.q_proj.weight", L+"self_attn.q_proj.bias", xn_q, xn_sq)
86
+ kf, ksq = proj(L+"self_attn.k_proj.weight", L+"self_attn.k_proj.bias", xn_q, xn_sq)
87
+ vf, vsq = proj(L+"self_attn.v_proj.weight", L+"self_attn.v_proj.bias", xn_q, xn_sq)
88
+ qq = rope_int(qf.reshape(S, NH, HD)); kq = rope_int(kf.reshape(S, NKV, HD)); vq = vf.reshape(S, NKV, HD)
89
+ kq14 = np.repeat(kq, NH//NKV, 1); vq14 = np.repeat(vq, NH//NKV, 1)
90
+ scores = np.einsum('shd,thd->hst', qq, kq14); C8 = mul(qsq, ksq)*8
91
+ attn = np.zeros((S, D), np.int64)
92
+ for h in range(NH):
93
+ for s in range(S):
94
+ row = scores[h, s, :s+1]; m = int(row.max())
95
+ d = np.clip([apply(m-int(r), C8) for r in row], 0, 255); e = EXP[np.array(d)]; den = int(e.sum())
96
+ for dd in range(HD):
97
+ acc = int(np.sum(e*vq14[:s+1, h, dd]))
98
+ attn[s, h*HD+dd] = ((acc*(1 << SC)+(den >> 1))//den) >> SC
99
+ aqa, aqa_sq = aq(attn.reshape(-1), vsq); aqa = aqa.reshape(S, D)
100
+ _, wo, wosq = T[L+"self_attn.o_proj.weight"]; oacc = imm(aqa, wo)
101
+ ocsq = [mul(int(aqa_sq), int(wosq[dd])) for dd in range(D)]
102
+ hout = np.zeros((S, D), np.int64)
103
+ for s in range(S):
104
+ for dd in range(D): hout[s, dd] = Xv[s, dd] + apply(int(oacc[s, dd]), (ocsq[dd] << SC)//Xsq[s])
105
+
106
+ # ---- SwiGLU MLP sub-block ----
107
+ xn2_q, xn2_sq = rmsnorm_q(hout, L+"post_attention_layernorm.weight")
108
+ gate_q, gate_sq = proj(L+"mlp.gate_proj.weight", None, xn2_q, xn2_sq) # [S,FF]
109
+ up_q, up_sq = proj(L+"mlp.up_proj.weight", None, xn2_q, xn2_sq)
110
+ # SiLU: act = gate * sigmoid(gate). idx = round(gate_true*128)+2048
111
+ idx = np.clip(apply(gate_q, int(gate_sq) << 7) + 2048, 0, 4095)
112
+ sig = SIG[idx] # Q16
113
+ act_int = gate_q*sig # int (large ~2^31)
114
+ # requant act to int16; scale = scale_from(amax, gate_sq) >> 16 (>>16 for the Q16 sigmoid; large amax first)
115
+ act_q, spre = aq(act_int.reshape(-1), int(gate_sq)); act_q = act_q.reshape(S, FF)
116
+ act_sq = spre >> 16
117
+ hmul = act_q*up_q # int
118
+ hmul_sq = mul(int(act_sq), int(up_sq))
119
+ hmul_q, hmul_q_sq = aq(hmul.reshape(-1), hmul_sq); hmul_q = hmul_q.reshape(S, FF)
120
+ mlp_q, mlp_sq = proj(L+"mlp.down_proj.weight", None, hmul_q, hmul_q_sq) # [S,D]
121
+ layer = np.zeros((S, D), np.int64) # mlp residual: bring mlp (scale mlp_sq) into hout's per-token Xsq scale
122
+ for s in range(S):
123
+ ratio = (int(mlp_sq) << SC)//Xsq[s]
124
+ for dd in range(D): layer[s, dd] = hout[s, dd] + apply(int(mlp_q[s, dd]), ratio)
125
+
126
+ def fnv(b):
127
+ h = 0xcbf29ce484222325
128
+ for x in b: h = ((h ^ (x & 0xff))*0x100000001b3) & 0xFFFFFFFFFFFFFFFF
129
+ return h
130
+ fp = fnv(layer.astype(np.int64).tobytes())
131
+
132
+ # ---- fp reference (full layer) ----
133
+ def wd(n): _, w, s = T[n]; return w.astype(np.float64) * np.array([int(s[o])/(1 << SC) for o in range(w.shape[0])])[:, None]
134
+ def vd(n): _, v, s = T[n]; return v.astype(np.float64) * (int(s)/(1 << SC))
135
+ Xfp = Xv.astype(np.float64)*np.array(Xsq)[:, None]/(1 << SC)
136
+ def rms_fp(h, g): return h/np.sqrt(np.mean(h*h, 1, keepdims=True)+1e-6)*g
137
+ xn = rms_fp(Xfp, vd(L+"input_layernorm.weight"))
138
+ Wq, Wk, Wv, Wo = wd(L+"self_attn.q_proj.weight"), wd(L+"self_attn.k_proj.weight"), wd(L+"self_attn.v_proj.weight"), wd(L+"self_attn.o_proj.weight")
139
+ bq, bk, bv = vd(L+"self_attn.q_proj.bias"), vd(L+"self_attn.k_proj.bias"), vd(L+"self_attn.v_proj.bias")
140
+ q = (xn@Wq.T+bq).reshape(S, NH, HD); k = (xn@Wk.T+bk).reshape(S, NKV, HD); v = (xn@Wv.T+bv).reshape(S, NKV, HD)
141
+ rhf = lambda x: np.concatenate([-x[..., HD//2:], x[..., :HD//2]], -1)
142
+ q = q*cosF[:, None, :]+rhf(q)*sinF[:, None, :]; k = k*cosF[:, None, :]+rhf(k)*sinF[:, None, :]
143
+ k14 = np.repeat(k, NH//NKV, 1); v14 = np.repeat(v, NH//NKV, 1); sc = np.einsum('shd,thd->hst', q, k14)/np.sqrt(HD)
144
+ for h in range(NH):
145
+ for s in range(S): sc[h, s, s+1:] = -1e30
146
+ af = np.zeros((S, NH, HD))
147
+ for h in range(NH):
148
+ p = np.exp(sc[h]-sc[h].max(1, keepdims=True)); p /= p.sum(1, keepdims=True); af[:, h, :] = p@v14[:, h, :]
149
+ hfp = Xfp + af.reshape(S, D)@Wo.T
150
+ xn2 = rms_fp(hfp, vd(L+"post_attention_layernorm.weight"))
151
+ g = xn2@wd(L+"mlp.gate_proj.weight").T; u = xn2@wd(L+"mlp.up_proj.weight").T
152
+ silu = g/(1.0+np.exp(-g)); mlp = (silu*u)@wd(L+"mlp.down_proj.weight").T
153
+ layer_fp = hfp + mlp
154
+ layer_int_fp = layer.astype(np.float64)*np.array(Xsq)[:, None]/(1 << SC)
155
+ rel = float(np.sqrt(np.sum((layer_int_fp-layer_fp)**2)/np.sum(layer_fp**2)))
156
+ print(f"FULL LAYER: accuracy vs fp = {rel:.4f}")
157
+ print(f"layer-out FNV = {fp:016x} <- Rust/CUDA ports must reproduce")
158
+ print(f"layer[0,:4] int = {layer[0,:4]}")
docs/evidence/canon_layer_metal.swift ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Canonical integer LAYER on Apple Metal GPU (increment 3). Must reproduce the CPU fingerprints:
2
+ // LAYER fnv = 440bd1b93d991fe0 , LUT fnv = 8ab2052fcf2b005a
3
+ // Build/run: swiftc -O canon_layer_metal.swift -o canon_layer_metal && ./canon_layer_metal
4
+
5
+ import Foundation
6
+ import Metal
7
+
8
+ func splitmix64(_ s: inout UInt64) -> UInt64 {
9
+ s = s &+ 0x9E3779B97F4A7C15
10
+ var z = s
11
+ z = (z ^ (z >> 30)) &* 0xBF58476D1CE4E5B9
12
+ z = (z ^ (z >> 27)) &* 0x94D049BB133111EB
13
+ return z ^ (z >> 31)
14
+ }
15
+ func genI8(_ n: Int, _ seed: UInt64) -> [Int8] {
16
+ var s = seed; var v = [Int8](); v.reserveCapacity(n)
17
+ for _ in 0..<n { v.append(Int8(splitmix64(&s) % 128)) }
18
+ return v
19
+ }
20
+ func fnv(_ bytes: [UInt8]) -> UInt64 {
21
+ var h: UInt64 = 0xcbf29ce484222325
22
+ for b in bytes { h ^= UInt64(b); h = h &* 0x100000001b3 }
23
+ return h
24
+ }
25
+ func siluInt(_ x: Int32) -> Int8 {
26
+ let S: Int32 = 4096
27
+ let ax = abs(x)
28
+ let fs = (x &* S) / (1 + ax)
29
+ let sig = (fs + S) / 2
30
+ let silu = (x &* sig) / S
31
+ return Int8(max(-127, min(127, silu)))
32
+ }
33
+
34
+ let M = 16, K = 2048, N = 16
35
+ let shift: UInt32 = 20
36
+ let x = genI8(M * K, 0x51)
37
+ let w = genI8(K * N, 0x52)
38
+ var sb: UInt64 = 0x53
39
+ let bias: [Int32] = (0..<N).map { _ in Int32(-8_300_000) &+ Int32(splitmix64(&sb) % 400_000) }
40
+ var sm: UInt64 = 0x54
41
+ let m0: [Int32] = (0..<N).map { _ in Int32(700) &+ Int32(splitmix64(&sm) % 200) }
42
+ var lut = [Int8](repeating: 0, count: 256)
43
+ for i in 0..<256 { lut[i] = siluInt(Int32(i) - 128) }
44
+
45
+ let shader = """
46
+ #include <metal_stdlib>
47
+ using namespace metal;
48
+ kernel void layer(device const char* X [[buffer(0)]],
49
+ device const char* W [[buffer(1)]],
50
+ device const int* BIAS [[buffer(2)]],
51
+ device const int* M0 [[buffer(3)]],
52
+ device const char* LUT [[buffer(4)]],
53
+ device char* OUT [[buffer(5)]],
54
+ constant uint4& dims [[buffer(6)]],
55
+ uint gid [[thread_position_in_grid]]) {
56
+ uint Mv=dims.x, Kv=dims.y, Nv=dims.z, sh=dims.w;
57
+ if (gid >= Mv*Nv) return;
58
+ uint i = gid / Nv, j = gid % Nv;
59
+ int acc = 0;
60
+ for (uint t = 0; t < Kv; t++) acc += int(X[i*Kv+t]) * int(W[t*Nv+j]);
61
+ int pre = acc + BIAS[j];
62
+ long round = (long)1 << (sh - 1);
63
+ long v = ((long)pre * (long)M0[j] + round) >> sh;
64
+ int q = v < -127 ? -127 : (v > 127 ? 127 : (int)v);
65
+ OUT[gid] = LUT[q + 128];
66
+ }
67
+ """
68
+
69
+ guard let dev = MTLCreateSystemDefaultDevice() else { fatalError("no Metal device") }
70
+ let lib = try! dev.makeLibrary(source: shader, options: nil)
71
+ let pipe = try! dev.makeComputePipelineState(function: lib.makeFunction(name: "layer")!)
72
+ let q = dev.makeCommandQueue()!
73
+ func buf<T>(_ arr: [T]) -> MTLBuffer { dev.makeBuffer(bytes: arr, length: MemoryLayout<T>.stride * arr.count, options: .storageModeShared)! }
74
+ let bX = buf(x), bW = buf(w), bB = buf(bias), bM = buf(m0), bL = buf(lut)
75
+ let bOut = dev.makeBuffer(length: M * N, options: .storageModeShared)!
76
+ var dims = SIMD4<UInt32>(UInt32(M), UInt32(K), UInt32(N), shift)
77
+ let bD = dev.makeBuffer(bytes: &dims, length: MemoryLayout<SIMD4<UInt32>>.size, options: .storageModeShared)!
78
+
79
+ let cmd = q.makeCommandBuffer()!
80
+ let enc = cmd.makeComputeCommandEncoder()!
81
+ enc.setComputePipelineState(pipe)
82
+ for (idx, b) in [bX, bW, bB, bM, bL, bOut, bD].enumerated() { enc.setBuffer(b, offset: 0, index: idx) }
83
+ enc.dispatchThreads(MTLSize(width: M * N, height: 1, depth: 1), threadsPerThreadgroup: MTLSize(width: 64, height: 1, depth: 1))
84
+ enc.endEncoding(); cmd.commit(); cmd.waitUntilCompleted()
85
+
86
+ let optr = bOut.contents().bindMemory(to: Int8.self, capacity: M * N)
87
+ var outBytes = [UInt8](); outBytes.reserveCapacity(M * N)
88
+ for i in 0..<(M * N) { outBytes.append(UInt8(bitPattern: optr[i])) }
89
+ let h = fnv(outBytes)
90
+ let lutH = fnv(lut.map { UInt8(bitPattern: $0) })
91
+ print("backend = Apple Metal GPU (\(dev.name))")
92
+ print(String(format: "LUT fnv = %016llx (target 8ab2052fcf2b005a)", lutH))
93
+ print(String(format: "LAYER fnv = %016llx (target 440bd1b93d991fe0)", h))
94
+ print("MATCH = \(String(format: "%016llx", h) == "440bd1b93d991fe0" ? "✅ byte-identical to CPU" : "❌ MISMATCH")")
docs/evidence/canon_metal.swift ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Canonical int8 GEMM on Apple Metal GPU — must reproduce the CPU/x86 canonical fingerprint.
2
+ // Same splitmix64 inputs + same int32 GEMM as canon_int.rs (M,K,N = 32,8192,32, values 0..127).
3
+ // Target INT fnv (from CPU aarch64 & x86_64): 4b138ed70d137781
4
+ //
5
+ // Build/run: swiftc -O canon_metal.swift -o canon_metal && ./canon_metal
6
+
7
+ import Foundation
8
+ import Metal
9
+
10
+ // ---- identical deterministic input generation ----
11
+ func splitmix64(_ s: inout UInt64) -> UInt64 {
12
+ s = s &+ 0x9E3779B97F4A7C15
13
+ var z = s
14
+ z = (z ^ (z >> 30)) &* 0xBF58476D1CE4E5B9
15
+ z = (z ^ (z >> 27)) &* 0x94D049BB133111EB
16
+ return z ^ (z >> 31)
17
+ }
18
+ func genI8(_ n: Int, _ seed: UInt64) -> [Int8] {
19
+ var s = seed
20
+ var v = [Int8](); v.reserveCapacity(n)
21
+ for _ in 0..<n { v.append(Int8(splitmix64(&s) % 128)) }
22
+ return v
23
+ }
24
+ func fnv(_ bytes: [UInt8]) -> UInt64 {
25
+ var h: UInt64 = 0xcbf29ce484222325
26
+ for b in bytes { h ^= UInt64(b); h = h &* 0x100000001b3 }
27
+ return h
28
+ }
29
+
30
+ let M = 32, K = 8192, N = 32
31
+ let a = genI8(M * K, 0xA11CE) // weights, row-major M×K
32
+ let b = genI8(K * N, 0xB0B) // activations, row-major K×N
33
+
34
+ let shader = """
35
+ #include <metal_stdlib>
36
+ using namespace metal;
37
+ kernel void gemm_i8(device const char* A [[buffer(0)]],
38
+ device const char* B [[buffer(1)]],
39
+ device int* C [[buffer(2)]],
40
+ constant uint3& dims [[buffer(3)]],
41
+ uint gid [[thread_position_in_grid]]) {
42
+ uint Mv = dims.x, Kv = dims.y, Nv = dims.z;
43
+ if (gid >= Mv * Nv) return;
44
+ uint i = gid / Nv, j = gid % Nv;
45
+ int acc = 0; // int32 accumulation
46
+ for (uint t = 0; t < Kv; t++) {
47
+ acc += int(A[i * Kv + t]) * int(B[t * Nv + j]);
48
+ }
49
+ C[gid] = acc;
50
+ }
51
+ """
52
+
53
+ guard let dev = MTLCreateSystemDefaultDevice() else { fatalError("no Metal device") }
54
+ let lib = try! dev.makeLibrary(source: shader, options: nil)
55
+ let fn = lib.makeFunction(name: "gemm_i8")!
56
+ let pipe = try! dev.makeComputePipelineState(function: fn)
57
+ let q = dev.makeCommandQueue()!
58
+
59
+ let bufA = dev.makeBuffer(bytes: a, length: a.count, options: .storageModeShared)!
60
+ let bufB = dev.makeBuffer(bytes: b, length: b.count, options: .storageModeShared)!
61
+ let bufC = dev.makeBuffer(length: M * N * MemoryLayout<Int32>.size, options: .storageModeShared)!
62
+ var dims = SIMD3<UInt32>(UInt32(M), UInt32(K), UInt32(N))
63
+ let bufD = dev.makeBuffer(bytes: &dims, length: MemoryLayout<SIMD3<UInt32>>.size, options: .storageModeShared)!
64
+
65
+ let cmd = q.makeCommandBuffer()!
66
+ let enc = cmd.makeComputeCommandEncoder()!
67
+ enc.setComputePipelineState(pipe)
68
+ enc.setBuffer(bufA, offset: 0, index: 0)
69
+ enc.setBuffer(bufB, offset: 0, index: 1)
70
+ enc.setBuffer(bufC, offset: 0, index: 2)
71
+ enc.setBuffer(bufD, offset: 0, index: 3)
72
+ let total = M * N
73
+ let tpg = 64
74
+ enc.dispatchThreads(MTLSize(width: total, height: 1, depth: 1),
75
+ threadsPerThreadgroup: MTLSize(width: tpg, height: 1, depth: 1))
76
+ enc.endEncoding()
77
+ cmd.commit()
78
+ cmd.waitUntilCompleted()
79
+
80
+ // read back C as int32, serialize little-endian, FNV
81
+ let cptr = bufC.contents().bindMemory(to: Int32.self, capacity: total)
82
+ var bytes = [UInt8](); bytes.reserveCapacity(total * 4)
83
+ for idx in 0..<total {
84
+ let v = UInt32(bitPattern: cptr[idx])
85
+ bytes.append(UInt8(v & 0xff)); bytes.append(UInt8((v >> 8) & 0xff))
86
+ bytes.append(UInt8((v >> 16) & 0xff)); bytes.append(UInt8((v >> 24) & 0xff))
87
+ }
88
+ let h = fnv(bytes)
89
+ print("backend = Apple Metal GPU (\(dev.name))")
90
+ print("shape M,K,N = \(M),\(K),\(N)")
91
+ print(String(format: "INT gpu fnv = %016llx", h))
92
+ print("TARGET (CPU) = 4b138ed70d137781")
93
+ print("MATCH = \(String(format: "%016llx", h) == "4b138ed70d137781" ? "✅ byte-identical to CPU" : "❌ MISMATCH")")
docs/evidence/canon_model.cu ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Full-model canonical integer forward (hifi: W8A24, Q60, 1/512 LUTs) — CUDA run.
2
+ // ALL GEMMs (q/k/v/o/gate/up/down per layer + tied LM head — the compute-dominant ops) execute ON THE GPU
3
+ // (one thread per output element, sequential k-loop → bit-exact). Integer glue (scales/aq/softmax/rope/
4
+ // residuals) runs on host with __int128 — every step integer, so the whole forward is deterministic.
5
+ // Must reproduce: [3555, 374, 279, 7290, 315, 279].
6
+ // Build: nvcc -O3 -arch=sm_89 canon_model.cu -o canon_model_cu && ./canon_model_cu model.bin 6
7
+ #include <cstdio>
8
+ #include <cstdint>
9
+ #include <vector>
10
+ #include <cstring>
11
+ #include <cuda_runtime.h>
12
+ typedef long long i64; typedef __int128 i128;
13
+ static const unsigned SC=60; static const i128 HALF=(i128)1<<59, ALVL=((i128)1<<23)-1;
14
+ static const int D=896, NH=14, NKV=2, HD=64, FF=4864, NL=24, VOCAB=151936;
15
+
16
+ static i128 qm(i64 m0,i64 sh){ i64 e=sh-31+(i64)SC; return e>=0?((i128)m0<<e):(((i128)m0+((i128)1<<(-e-1)))>>(-e)); }
17
+ static i128 ap(i128 a,i128 s){ return (a*s+HALF)>>SC; }
18
+ static i128 mul(i128 a,i128 b){ return (a*b+HALF)>>SC; }
19
+ static i128 fdiv(i128 a,i128 b){ i128 q=a/b,r=a%b; if(r!=0&&((r<0)!=(b<0)))return q-1; return q; }
20
+ static i128 isqrt(i128 n){ if(n<=0)return 0; i128 x=n,y=(x+1)/2; while(y<x){x=y;y=(x+n/x)/2;} return x; }
21
+ static i128 aq(std::vector<i64>& v,i128 cur){ i64 am=1; for(i64 x:v){i64 a=x<0?-x:x; if(a>am)am=a;} i128 amc=am;
22
+ for(auto& x:v){ i128 xi=x, sg=xi>0?amc:(xi<0?-amc:0); x=(i64)fdiv(xi*ALVL*2+sg,2*amc);} return (amc*cur)/ALVL; }
23
+
24
+ struct Buf{ std::vector<unsigned char> b; long p=0;
25
+ unsigned u32(){unsigned v;memcpy(&v,&b[p],4);p+=4;return v;} int i32(){int v;memcpy(&v,&b[p],4);p+=4;return v;}
26
+ i64 i64_(){i64 v;memcpy(&v,&b[p],8);p+=8;return v;} unsigned char u8(){return b[p++];} };
27
+ struct W2d{ int rows,cols; signed char* dw; std::vector<i128> sq; }; // weights live on DEVICE
28
+ struct V1d{ std::vector<i64> v; i128 sq; };
29
+ static W2d rd_w2d(Buf& b){ b.u8(); int r=b.u32(),c=b.u32(); signed char* d; cudaMalloc(&d,(size_t)r*c);
30
+ cudaMemcpy(d,&b.b[b.p],(size_t)r*c,cudaMemcpyHostToDevice); b.p+=(long)r*c;
31
+ int ns=b.u32(); std::vector<i128> sq(ns); for(int i=0;i<ns;i++){i64 m=b.i32(),s=b.i32(); sq[i]=qm(m,s);} return W2d{r,c,d,sq}; }
32
+ static V1d rd_v1d(Buf& b){ b.u8(); int n=b.u32(); b.u32(); std::vector<i64> v(n); for(int i=0;i<n;i++)v[i]=b.i32();
33
+ b.u32(); i64 m=b.i32(),s=b.i32(); return V1d{v,qm(m,s)}; }
34
+
35
+ __global__ void k_gemm(const i64* x,const signed char* w,i64* out,int S,int OC,int K){
36
+ int idx=blockIdx.x*blockDim.x+threadIdx.x; if(idx>=S*OC) return;
37
+ int s=idx/OC,o=idx%OC; i64 a=0; const signed char* wr=w+(size_t)o*K; const i64* xr=x+(size_t)s*K;
38
+ for(int d=0;d<K;d++) a+=xr[d]*(i64)wr[d]; out[idx]=a;
39
+ }
40
+ static i64 *g_dx,*g_dout; static size_t g_cx=0,g_co=0;
41
+ static void gemm(const std::vector<i64>& x,const W2d& w,int S,std::vector<i64>& out){
42
+ size_t nx=(size_t)S*w.cols, no=(size_t)S*w.rows;
43
+ if(nx>g_cx){ if(g_dx)cudaFree(g_dx); cudaMalloc(&g_dx,nx*8); g_cx=nx; }
44
+ if(no>g_co){ if(g_dout)cudaFree(g_dout); cudaMalloc(&g_dout,no*8); g_co=no; }
45
+ cudaMemcpy(g_dx,x.data(),nx*8,cudaMemcpyHostToDevice);
46
+ int tpb=256, blocks=(int)((no+tpb-1)/tpb);
47
+ k_gemm<<<blocks,tpb>>>(g_dx,w.dw,g_dout,S,w.rows,w.cols);
48
+ out.resize(no); cudaMemcpy(out.data(),g_dout,no*8,cudaMemcpyDeviceToHost);
49
+ }
50
+
51
+ struct Layer{ V1d ln1; W2d qw; V1d qb; W2d kw; V1d kb; W2d vw; V1d vb; W2d ow; V1d ln2; W2d gw,uw,dw; };
52
+ static std::vector<i64> COSQ,SINQ,EXPL,SIGL;
53
+
54
+ static void rmsnorm(const std::vector<i64>& hv,int S,const V1d& g,std::vector<i64>& q,i128& sq){
55
+ std::vector<i64> yg((size_t)S*D);
56
+ for(int s=0;s<S;s++){ i128 ss=0; for(int d=0;d<D;d++){i128 x=hv[(size_t)s*D+d]; ss+=x*x;}
57
+ i64 inv=(i64)isqrt(((i128)1<<40)/(ss/D+1));
58
+ for(int d=0;d<D;d++) yg[(size_t)s*D+d]=hv[(size_t)s*D+d]*inv*g.v[d]; }
59
+ i128 s0=aq(yg,(i128)1<<SC); q=yg; sq=mul(mul(s0,qm(1,11)),g.sq);
60
+ }
61
+ static void proj(const W2d& w,const V1d* b,int S,const std::vector<i64>& xnq,i128 xnsq,std::vector<i64>& out,i128& osq){
62
+ std::vector<i64> acc; gemm(xnq,w,S,acc);
63
+ int oc=w.rows; std::vector<i128> colsq(oc); i128 cmax=0;
64
+ for(int o=0;o<oc;o++){ colsq[o]=mul(xnsq,w.sq[o]); if(colsq[o]>cmax)cmax=colsq[o]; }
65
+ out.assign((size_t)S*oc,0);
66
+ for(int o=0;o<oc;o++){ i64 bacc=0; if(b){ i128 cs=colsq[o]; if(cs) bacc=(i64)(fdiv((i128)b->v[o]*b->sq*((i128)1<<SC)+(cs>>1),cs)>>SC); }
67
+ i128 r=(colsq[o]<<SC)/cmax;
68
+ for(int s=0;s<S;s++) out[(size_t)s*oc+o]=(i64)ap((i128)acc[(size_t)s*oc+o]+bacc,r); }
69
+ osq=aq(out,cmax);
70
+ }
71
+ static void rope(std::vector<i64>& x,int S,int nh){
72
+ std::vector<i64> src=x;
73
+ for(int s=0;s<S;s++)for(int h=0;h<nh;h++)for(int d=0;d<HD;d++){ size_t base=((size_t)s*nh+h)*HD;
74
+ i64 rh=d<HD/2?-src[base+d+HD/2]:src[base+d-HD/2];
75
+ x[base+d]=(src[base+d]*COSQ[(size_t)s*HD+d]+rh*SINQ[(size_t)s*HD+d]+16384)>>15; }
76
+ }
77
+
78
+ int main(int argc,char** argv){
79
+ const char* path=argc>1?argv[1]:"model.bin"; int ndecode=argc>2?atoi(argv[2]):6;
80
+ FILE* f=fopen(path,"rb"); fseek(f,0,SEEK_END); long n=ftell(f); fseek(f,0,SEEK_SET);
81
+ Buf b; b.b.resize(n); size_t rd=fread(b.b.data(),1,n,f); (void)rd; fclose(f);
82
+ if(memcmp(b.b.data(),"QCM0\0\0\0\0",8)){printf("bad magic\n");return 1;} b.p=8;
83
+ int nl=b.u32(); int smax=b.u32(); (void)nl;
84
+ W2d emb=rd_w2d(b);
85
+ std::vector<Layer> L; L.reserve(NL);
86
+ for(int l=0;l<NL;l++){ Layer y{rd_v1d(b),rd_w2d(b),rd_v1d(b),rd_w2d(b),rd_v1d(b),rd_w2d(b),rd_v1d(b),rd_w2d(b),rd_v1d(b),rd_w2d(b),rd_w2d(b),rd_w2d(b)}; L.push_back(std::move(y)); }
87
+ V1d fnorm=rd_v1d(b);
88
+ COSQ.resize((size_t)smax*HD); SINQ.resize((size_t)smax*HD); EXPL.resize(2048); SIGL.resize(16384);
89
+ for(auto* a:{&COSQ,&SINQ}) for(auto& x:*a) x=b.i64_();
90
+ for(auto& x:EXPL) x=b.i64_(); for(auto& x:SIGL) x=b.i64_();
91
+ // keep host copy of embedding int8 for gather (device copy used for LM head gemm)
92
+ // re-read embed weights from file region: simpler — gather via device? do a host copy path:
93
+ // (emb weights already uploaded; for the gather we re-read from the file buffer)
94
+ // embed record starts at offset 8+8: kind(1)+rows(4)+cols(4)=9 bytes header → weights at 25
95
+ const signed char* embh=(const signed char*)&b.b[8+8+9];
96
+
97
+ std::vector<int> prompt={9707,11,1879,30}, toks=prompt, gen;
98
+ for(int step=0; step<ndecode; step++){
99
+ int S=(int)toks.size();
100
+ std::vector<i64> hv((size_t)S*D); std::vector<i128> xsq(S);
101
+ for(int s=0;s<S;s++){ int t=toks[s]; for(int d=0;d<D;d++) hv[(size_t)s*D+d]=embh[(size_t)t*D+d]; xsq[s]=emb.sq[t]; }
102
+ for(auto& l:L){
103
+ std::vector<i64> xnq; i128 xnsq; rmsnorm(hv,S,l.ln1,xnq,xnsq);
104
+ std::vector<i64> qq,kq,vq; i128 qsq,ksq,vsq;
105
+ proj(l.qw,&l.qb,S,xnq,xnsq,qq,qsq); proj(l.kw,&l.kb,S,xnq,xnsq,kq,ksq); proj(l.vw,&l.vb,S,xnq,xnsq,vq,vsq);
106
+ rope(qq,S,NH); rope(kq,S,NKV);
107
+ int g=NH/NKV; i128 c512=mul(qsq,ksq)*64;
108
+ std::vector<i64> attn((size_t)S*D,0);
109
+ for(int h=0;h<NH;h++){ int kvh=h/g; for(int s=0;s<S;s++){
110
+ std::vector<i64> sc(s+1); i64 m=INT64_MIN;
111
+ for(int t=0;t<=s;t++){ i64 a=0; for(int d=0;d<HD;d++) a+=qq[((size_t)s*NH+h)*HD+d]*kq[((size_t)t*NKV+kvh)*HD+d]; sc[t]=a; if(a>m)m=a; }
112
+ std::vector<i64> e(s+1); i128 den=0;
113
+ for(int t=0;t<=s;t++){ i128 dd=ap((i128)(m-sc[t]),c512); if(dd<0)dd=0; if(dd>2047)dd=2047; e[t]=EXPL[(int)dd]; den+=e[t]; }
114
+ for(int d=0;d<HD;d++){ i128 acc=0; for(int t=0;t<=s;t++) acc+=(i128)e[t]*vq[((size_t)t*NKV+kvh)*HD+d];
115
+ attn[(size_t)s*D+h*HD+d]=(i64)(fdiv(acc*((i128)1<<SC)+(den>>1),den)>>SC); } } }
116
+ i128 aqsq=aq(attn,vsq);
117
+ std::vector<i64> oacc; gemm(attn,l.ow,S,oacc);
118
+ std::vector<i64> hout((size_t)S*D);
119
+ for(int s=0;s<S;s++)for(int d2=0;d2<D;d2++){ i128 oc=mul(aqsq,l.ow.sq[d2]);
120
+ hout[(size_t)s*D+d2]=hv[(size_t)s*D+d2]+(i64)ap((i128)oacc[(size_t)s*D+d2],(oc<<SC)/xsq[s]); }
121
+ std::vector<i64> xn2q; i128 xn2sq; rmsnorm(hout,S,l.ln2,xn2q,xn2sq);
122
+ std::vector<i64> gate,up; i128 gsq,usq;
123
+ proj(l.gw,nullptr,S,xn2q,xn2sq,gate,gsq); proj(l.uw,nullptr,S,xn2q,xn2sq,up,usq);
124
+ i128 gs9=gsq<<9;
125
+ std::vector<i64> act((size_t)S*FF);
126
+ for(size_t i=0;i<(size_t)S*FF;i++){ i128 idx=((i128)gate[i]*gs9+HALF)>>SC; idx+=8192; if(idx<0)idx=0; if(idx>16383)idx=16383; act[i]=gate[i]*SIGL[(int)idx]; }
127
+ i128 spre=aq(act,gsq); i128 asq=spre>>16;
128
+ std::vector<i64> hm((size_t)S*FF);
129
+ for(size_t i=0;i<(size_t)S*FF;i++) hm[i]=act[i]*up[i];
130
+ i128 hsq=aq(hm,mul(asq,usq));
131
+ std::vector<i64> mlp; i128 msq; proj(l.dw,nullptr,S,hm,hsq,mlp,msq);
132
+ for(int s=0;s<S;s++){ i128 r=(msq<<SC)/xsq[s]; for(int d2=0;d2<D;d2++) hv[(size_t)s*D+d2]=hout[(size_t)s*D+d2]+(i64)ap((i128)mlp[(size_t)s*D+d2],r); }
133
+ }
134
+ std::vector<i64> xnq; i128 xnsq; rmsnorm(hv,S,fnorm,xnq,xnsq); (void)xnsq;
135
+ std::vector<i64> last(xnq.end()-D,xnq.end());
136
+ std::vector<i64> raw; gemm(last,emb,1,raw); // LM head on GPU: [1,VOCAB]
137
+ int best=0; i128 bv=-(((i128)1)<<126);
138
+ for(int v=0;v<VOCAB;v++){ i128 sc=((i128)raw[v]*emb.sq[v]+HALF)>>SC; if(sc>bv){bv=sc;best=v;} }
139
+ gen.push_back(best); toks.push_back(best);
140
+ }
141
+ cudaDeviceProp pr; cudaGetDeviceProperties(&pr,0);
142
+ printf("backend = CUDA GEMMs on %s (sm_%d%d) + host int128 glue\n",pr.name,pr.major,pr.minor);
143
+ printf("generated = ["); for(size_t i=0;i<gen.size();i++) printf("%d%s",gen[i],i+1<gen.size()?", ":""); printf("]\n");
144
+ unsigned long long h=0xcbf29ce484222325ULL;
145
+ for(int t:gen){ i64 v=t; unsigned char* p=(unsigned char*)&v; for(int k=0;k<8;k++){h^=p[k];h*=0x100000001b3ULL;} }
146
+ printf("ids FNV = %016llx\n",h);
147
+ printf("EXPECTED = [3555, 374, 279, 7290, 315, 279] / FNV 65d2e3adaf2dce9e\n");
148
+ return 0;
149
+ }
docs/evidence/canon_model.rs ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Full-model canonical integer forward (hifi: W8A24, Q60 scales, 1/512 LUTs) — Rust port.
2
+ // Reads model.bin, greedy-decodes NDECODE tokens, prints token ids + FNV over ids.
3
+ // Must reproduce the Python reference: [3555, 374, 279, 7290, 315, 279] for prompt [9707,11,1879,30].
4
+ // Build/run: rustc -O --edition 2021 canon_model.rs -o canon_model && ./canon_model model.bin 6
5
+
6
+ use std::io::Read;
7
+
8
+ const SC: u32 = 60;
9
+ const HALF: i128 = 1 << 59;
10
+ const ALVL: i128 = (1 << 23) - 1;
11
+ const D: usize = 896;
12
+ const NH: usize = 14;
13
+ const NKV: usize = 2;
14
+ const HD: usize = 64;
15
+ const FF: usize = 4864;
16
+ const NL: usize = 24;
17
+ const VOCAB: usize = 151936;
18
+
19
+ fn qm(m0: i64, sh: i64) -> i128 { let e = sh - 31 + SC as i64; if e >= 0 { (m0 as i128) << e } else { ((m0 as i128) + (1i128 << (-e - 1))) >> (-e) } }
20
+ fn apply(a: i128, s: i128) -> i128 { (a * s + HALF) >> SC }
21
+ fn mul(a: i128, b: i128) -> i128 { (a * b + HALF) >> SC }
22
+ fn sfrom(amax: i128, cur: i128) -> i128 { (amax * cur) / ALVL }
23
+ fn fdiv(a: i128, b: i128) -> i128 { let q = a / b; let r = a % b; if r != 0 && (r < 0) != (b < 0) { q - 1 } else { q } }
24
+ fn isqrt(n: i128) -> i128 { if n <= 0 { return 0; } let mut x = n; let mut y = (x + 1) / 2; while y < x { x = y; y = (x + n / x) / 2; } x }
25
+ fn aq(v: &[i64], cur: i128) -> (Vec<i64>, i128) {
26
+ let am = v.iter().map(|x| x.abs()).max().unwrap().max(1) as i128;
27
+ let q = v.iter().map(|&x| { let xi = x as i128; let sg = if xi > 0 { am } else if xi < 0 { -am } else { 0 }; fdiv(xi * ALVL * 2 + sg, 2 * am) as i64 }).collect();
28
+ (q, sfrom(am, cur))
29
+ }
30
+ fn fnv(bytes: &[u8]) -> u64 { let mut h = 0xcbf29ce484222325u64; for &b in bytes { h ^= b as u64; h = h.wrapping_mul(0x100000001b3); } h }
31
+
32
+ struct Cur<'a> { b: &'a [u8], p: usize }
33
+ impl<'a> Cur<'a> {
34
+ fn u8(&mut self) -> u8 { let v = self.b[self.p]; self.p += 1; v }
35
+ fn u32(&mut self) -> u32 { let v = u32::from_le_bytes(self.b[self.p..self.p + 4].try_into().unwrap()); self.p += 4; v }
36
+ fn i32(&mut self) -> i32 { let v = i32::from_le_bytes(self.b[self.p..self.p + 4].try_into().unwrap()); self.p += 4; v }
37
+ fn i64(&mut self) -> i64 { let v = i64::from_le_bytes(self.b[self.p..self.p + 8].try_into().unwrap()); self.p += 8; v }
38
+ }
39
+ struct W2d { rows: usize, cols: usize, w: Vec<i8>, sq: Vec<i128> }
40
+ struct V1d { v: Vec<i64>, sq: i128 }
41
+ fn rd_w2d(c: &mut Cur) -> W2d {
42
+ assert_eq!(c.u8(), 0); let rows = c.u32() as usize; let cols = c.u32() as usize;
43
+ let w: Vec<i8> = (0..rows * cols).map(|_| c.u8() as i8).collect();
44
+ let ns = c.u32() as usize; let sq = (0..ns).map(|_| { let m0 = c.i32() as i64; let sh = c.i32() as i64; qm(m0, sh) }).collect();
45
+ W2d { rows, cols, w, sq }
46
+ }
47
+ fn rd_v1d(c: &mut Cur) -> V1d {
48
+ assert_eq!(c.u8(), 1); let n = c.u32() as usize; let _ = c.u32();
49
+ let v: Vec<i64> = (0..n).map(|_| c.i32() as i64).collect();
50
+ let _ = c.u32(); let m0 = c.i32() as i64; let sh = c.i32() as i64;
51
+ V1d { v, sq: qm(m0, sh) }
52
+ }
53
+ struct Layer { ln1: V1d, qw: W2d, qb: V1d, kw: W2d, kb: V1d, vw: W2d, vb: V1d, ow: W2d, ln2: V1d, gw: W2d, uw: W2d, dw: W2d }
54
+
55
+ fn rmsnorm(hv: &[i64], s: usize, gam: &V1d) -> (Vec<i64>, i128) {
56
+ let mut yg = vec![0i64; s * D];
57
+ for si in 0..s {
58
+ let ss: i128 = (0..D).map(|d| { let x = hv[si * D + d] as i128; x * x }).sum();
59
+ let mean = ss / (D as i128) + 1;
60
+ let inv = isqrt((1i128 << 40) / mean) as i64;
61
+ for d in 0..D { yg[si * D + d] = hv[si * D + d] * inv * gam.v[d]; }
62
+ }
63
+ let (q, s0) = aq(&yg, 1i128 << SC);
64
+ (q, mul(mul(s0, qm(1, 11)), gam.sq))
65
+ }
66
+ fn proj(w: &W2d, b: Option<&V1d>, s: usize, xn_q: &[i64], xn_sq: i128) -> (Vec<i64>, i128) {
67
+ let (oc, inn) = (w.rows, w.cols);
68
+ let mut acc = vec![0i64; s * oc];
69
+ for si in 0..s { for o in 0..oc { let mut a = 0i64; let base = o * inn; for d in 0..inn { a += xn_q[si * inn + d] * (w.w[base + d] as i64); } acc[si * oc + o] = a; } }
70
+ let colsq: Vec<i128> = (0..oc).map(|o| mul(xn_sq, w.sq[o])).collect();
71
+ let cmax = *colsq.iter().max().unwrap();
72
+ let mut resc = vec![0i64; s * oc];
73
+ for o in 0..oc {
74
+ let bacc: i64 = match b { Some(bv) => { let cs = colsq[o]; if cs == 0 { 0 } else { (fdiv((bv.v[o] as i128) * bv.sq * (1i128 << SC) + (cs >> 1), cs) >> SC) as i64 } }, None => 0 };
75
+ let ratio = (colsq[o] << SC) / cmax;
76
+ for si in 0..s { resc[si * oc + o] = apply((acc[si * oc + o] + bacc) as i128, ratio) as i64; }
77
+ }
78
+ aq(&resc, cmax)
79
+ }
80
+ fn rope(x: &mut [i64], s: usize, nh: usize, cosq: &[i64], sinq: &[i64]) {
81
+ let src = x.to_vec();
82
+ for si in 0..s { for h in 0..nh { for d in 0..HD {
83
+ let base = (si * nh + h) * HD;
84
+ let rh = if d < HD / 2 { -src[base + d + HD / 2] } else { src[base + d - HD / 2] };
85
+ x[base + d] = (src[base + d] * cosq[si * HD + d] + rh * sinq[si * HD + d] + 16384) >> 15;
86
+ } } }
87
+ }
88
+
89
+ fn main() {
90
+ let args: Vec<String> = std::env::args().collect();
91
+ let path = args.get(1).cloned().unwrap_or("model.bin".into());
92
+ let ndecode: usize = args.get(2).and_then(|x| x.parse().ok()).unwrap_or(6);
93
+ let mut buf = Vec::new(); std::fs::File::open(&path).unwrap().read_to_end(&mut buf).unwrap();
94
+ assert_eq!(&buf[0..8], b"QCM0\0\0\0\0");
95
+ let mut c = Cur { b: &buf, p: 8 };
96
+ let nl = c.u32() as usize; let smax = c.u32() as usize;
97
+ assert_eq!(nl, NL);
98
+ let emb = rd_w2d(&mut c);
99
+ assert_eq!(emb.rows, VOCAB); assert_eq!(emb.cols, D);
100
+ let layers: Vec<Layer> = (0..NL).map(|_| Layer {
101
+ ln1: rd_v1d(&mut c), qw: rd_w2d(&mut c), qb: rd_v1d(&mut c), kw: rd_w2d(&mut c), kb: rd_v1d(&mut c),
102
+ vw: rd_w2d(&mut c), vb: rd_v1d(&mut c), ow: rd_w2d(&mut c), ln2: rd_v1d(&mut c),
103
+ gw: rd_w2d(&mut c), uw: rd_w2d(&mut c), dw: rd_w2d(&mut c),
104
+ }).collect();
105
+ let fnorm = rd_v1d(&mut c);
106
+ let cosq: Vec<i64> = (0..smax * HD).map(|_| c.i64()).collect();
107
+ let sinq: Vec<i64> = (0..smax * HD).map(|_| c.i64()).collect();
108
+ let exp: Vec<i64> = (0..2048).map(|_| c.i64()).collect();
109
+ let sig: Vec<i64> = (0..16384).map(|_| c.i64()).collect();
110
+
111
+ let forward = |tokens: &[usize]| -> usize {
112
+ let s = tokens.len();
113
+ let mut hv = vec![0i64; s * D];
114
+ let mut xsq = vec![0i128; s];
115
+ for (si, &t) in tokens.iter().enumerate() {
116
+ for d in 0..D { hv[si * D + d] = emb.w[t * D + d] as i64; }
117
+ xsq[si] = emb.sq[t];
118
+ }
119
+ for l in &layers {
120
+ let (xn_q, xn_sq) = rmsnorm(&hv, s, &l.ln1);
121
+ let (mut qq, qsq) = proj(&l.qw, Some(&l.qb), s, &xn_q, xn_sq);
122
+ let (mut kq, ksq) = proj(&l.kw, Some(&l.kb), s, &xn_q, xn_sq);
123
+ let (vq, vsq) = proj(&l.vw, Some(&l.vb), s, &xn_q, xn_sq);
124
+ rope(&mut qq, s, NH, &cosq, &sinq);
125
+ rope(&mut kq, s, NKV, &cosq, &sinq);
126
+ let g = NH / NKV;
127
+ let c512 = mul(qsq, ksq) * 64;
128
+ let mut attn = vec![0i64; s * D];
129
+ for h in 0..NH { let kvh = h / g; for si in 0..s {
130
+ let mut scores = vec![0i64; si + 1];
131
+ for t in 0..=si { let mut a = 0i64; for d in 0..HD { a += qq[(si * NH + h) * HD + d] * kq[(t * NKV + kvh) * HD + d]; } scores[t] = a; }
132
+ let m = *scores.iter().max().unwrap();
133
+ let mut e = vec![0i64; si + 1]; let mut den: i128 = 0;
134
+ for t in 0..=si { let mut dd = apply((m - scores[t]) as i128, c512); if dd < 0 { dd = 0; } if dd > 2047 { dd = 2047; } e[t] = exp[dd as usize]; den += e[t] as i128; }
135
+ for d in 0..HD {
136
+ let mut acc: i128 = 0; for t in 0..=si { acc += (e[t] as i128) * (vq[(t * NKV + kvh) * HD + d] as i128); }
137
+ attn[si * D + h * HD + d] = (fdiv(acc * (1i128 << SC) + (den >> 1), den) >> SC) as i64;
138
+ }
139
+ } }
140
+ let (aqa, aqa_sq) = aq(&attn, vsq);
141
+ let mut hout = vec![0i64; s * D];
142
+ for si in 0..s { for d2 in 0..D {
143
+ let mut a = 0i64; for d in 0..D { a += aqa[si * D + d] * (l.ow.w[d2 * D + d] as i64); }
144
+ let ocsq = mul(aqa_sq, l.ow.sq[d2]);
145
+ hout[si * D + d2] = hv[si * D + d2] + apply(a as i128, (ocsq << SC) / xsq[si]) as i64;
146
+ } }
147
+ // MLP
148
+ let (xn2_q, xn2_sq) = rmsnorm(&hout, s, &l.ln2);
149
+ let (gate_q, gate_sq) = proj(&l.gw, None, s, &xn2_q, xn2_sq);
150
+ let (up_q, _up_used) = proj(&l.uw, None, s, &xn2_q, xn2_sq);
151
+ let up_sq = _up_used;
152
+ let gs9 = gate_sq << 9;
153
+ let mut act_int = vec![0i64; s * FF];
154
+ for i in 0..s * FF {
155
+ let mut idx = ((gate_q[i] as i128 * gs9 + HALF) >> SC) + 8192;
156
+ if idx < 0 { idx = 0; } if idx > 16383 { idx = 16383; }
157
+ act_int[i] = gate_q[i] * sig[idx as usize];
158
+ }
159
+ let (act_q, spre) = aq(&act_int, gate_sq); let act_sq = spre >> 16;
160
+ let mut hmul = vec![0i64; s * FF];
161
+ for i in 0..s * FF { hmul[i] = act_q[i] * up_q[i]; }
162
+ let (hmul_q, hmul_q_sq) = aq(&hmul, mul(act_sq, up_sq));
163
+ let (mlp_q, mlp_sq) = proj(&l.dw, None, s, &hmul_q, hmul_q_sq);
164
+ for si in 0..s { let ratio = (mlp_sq << SC) / xsq[si]; for d2 in 0..D { hv[si * D + d2] = hout[si * D + d2] + apply(mlp_q[si * D + d2] as i128, ratio) as i64; } }
165
+ }
166
+ let (xn_q, _) = rmsnorm(&hv, s, &fnorm);
167
+ // tied LM head: score[v] = (raw[v] * emb_scale[v] + HALF) >> SC, integer argmax (first max)
168
+ let last = &xn_q[(s - 1) * D..s * D];
169
+ let mut best_v = 0usize; let mut best: i128 = i128::MIN;
170
+ for v in 0..VOCAB {
171
+ let mut raw = 0i64; let base = v * D;
172
+ for d in 0..D { raw += last[d] * (emb.w[base + d] as i64); }
173
+ let sc = (raw as i128 * emb.sq[v] + HALF) >> SC;
174
+ if sc > best { best = sc; best_v = v; }
175
+ }
176
+ best_v
177
+ };
178
+
179
+ let prompt = [9707usize, 11, 1879, 30];
180
+ let mut toks: Vec<usize> = prompt.to_vec();
181
+ let mut gen: Vec<usize> = Vec::new();
182
+ for _ in 0..ndecode { let nt = forward(&toks); gen.push(nt); toks.push(nt); }
183
+ let bytes: Vec<u8> = gen.iter().flat_map(|&t| (t as i64).to_le_bytes()).collect();
184
+ println!("backend = Rust CPU ({})", std::env::consts::ARCH);
185
+ println!("generated = {:?}", gen);
186
+ println!("ids FNV = {:016x}", fnv(&bytes));
187
+ println!("EXPECTED = [3555, 374, 279, 7290, 315, 279] (Python reference, 6/6 vs fp)");
188
+ }
docs/evidence/canon_model_hifi.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # inc4b-4 reference: FULLY-INTEGER full Qwen2.5-0.5B forward = 24 layers + final RMSNorm + tied LM head +
3
+ # integer argmax, with greedy decode. No fp on the compute path. Produces a backend-independent TOKEN
4
+ # commitment (blake2b over the greedy token ids) and checks token-agreement vs the fp reference.
5
+ import sys, json, struct, hashlib, numpy as np
6
+ CANON = sys.argv[1] if len(sys.argv) > 1 else "/root/models/qwen05.canon"
7
+ NDECODE = int(sys.argv[2]) if len(sys.argv) > 2 else 8
8
+ D, NH, NKV, HD, THETA, FF, NL = 896, 14, 2, 64, 1000000.0, 4864, 24
9
+ VOCAB = 151936; SC = 60; HALF = 1 << (SC-1); ALVL = (1 << 23) - 1 # A24 activations, Q60 scales
10
+
11
+ def qmf(m0, sh): e = sh - 31 + SC; return (m0 << e) if e >= 0 else ((m0 + (1 << (-e-1))) >> (-e))
12
+ def apply(a, s): return (a*s + HALF) >> SC
13
+ def mul(a, b): return (a*b + HALF) >> SC
14
+ def sfrom(amax, cur): return (amax*cur)//ALVL
15
+ def isqrt(n):
16
+ if n <= 0: return 0
17
+ x = int(n); y = (x+1)//2
18
+ while y < x: x = y; y = (x + int(n)//x)//2
19
+ return x
20
+ def aq(v, cur):
21
+ amax = int(np.max(np.abs(v))) or 1
22
+ vo = v.astype(object)
23
+ q = ((vo*ALVL*2 + np.sign(vo)*amax)//(2*amax)).astype(np.int64)
24
+ return q, sfrom(amax, cur)
25
+ def read_canon(path):
26
+ f = open(path, "rb"); assert f.read(8) == b"CANONv1\n"
27
+ n = struct.unpack("<Q", f.read(8))[0]; hdr = json.loads(f.read(n)); body = f.read(); out = {}
28
+ for t in hdr["tensors"]:
29
+ nm, k, shp = t["name"], t["kind"], t["shape"]
30
+ if k == "w2d_int8_pcs":
31
+ o, i = shp
32
+ w = np.frombuffer(body[t["w_off"]:t["w_off"]+o*i], np.int8).reshape(o, i).astype(np.int64)
33
+ sc = np.frombuffer(body[t["s_off"]:t["s_off"]+o*8], np.int32).reshape(o, 2)
34
+ out[nm] = ("w", w, np.array([qmf(int(a), int(b)) for a, b in sc], object))
35
+ else:
36
+ v = np.frombuffer(body[t["w_off"]:t["w_off"]+shp[0]*4], np.int32).astype(np.int64)
37
+ m0, sh = struct.unpack("<ii", body[t["s_off"]:t["s_off"]+8]); out[nm] = ("v", v, qmf(m0, sh))
38
+ return out
39
+ T = read_canon(CANON)
40
+ def imm(a, w): return a @ w.T
41
+ def build_exp(nt=28):
42
+ ONE = 1 << 16; t = np.zeros(2048, np.int64)
43
+ for d in range(2048):
44
+ x = -d*(ONE//512); e = ONE; k = nt
45
+ while k >= 1: e = ONE+((x*e)//ONE)//k; k -= 1
46
+ t[d] = max(0, e)
47
+ return t
48
+ EXP = build_exp()
49
+ SIG = np.clip(np.rint(1.0/(1.0+np.exp(-(np.arange(16384)-8192)/512.0))*(1 << 16)), 0, 1 << 16).astype(np.int64)
50
+
51
+ def rms_q(hv, gname, S):
52
+ gv, gsq = T[gname][1], T[gname][2]
53
+ ss = np.sum(hv**2, 1); mean = ss//D + 1
54
+ inv = np.array([isqrt((1 << 40)//int(m)) for m in mean], np.int64)
55
+ yg = (hv*inv[:, None])*gv[None, :]; q, s0 = aq(yg, 1 << SC)
56
+ return q, mul(mul(s0, qmf(1, 11)), int(gsq))
57
+ def proj(wname, bname, xn_q, xn_sq, S):
58
+ _, wv, wsq = T[wname]; acc = imm(xn_q, wv); OUT = acc.shape[1]
59
+ colsq = [mul(int(xn_sq), int(wsq[o])) for o in range(OUT)]
60
+ if bname:
61
+ bv, bsq = T[bname][1], int(T[bname][2])
62
+ bacc = np.array([(((int(bv[o])*bsq*(1 << SC)+(cs >> 1))//cs) >> SC) if (cs := colsq[o]) else 0 for o in range(OUT)], np.int64)
63
+ else:
64
+ bacc = np.zeros(OUT, np.int64)
65
+ accb = acc + bacc[None, :]; cmax = max(colsq); resc = np.empty_like(acc)
66
+ for o in range(OUT): # exact requant (A24 products exceed int64)
67
+ r = (colsq[o] << SC)//cmax
68
+ resc[:, o] = np.array([ (int(x)*r + HALF) >> SC for x in accb[:, o] ], dtype=np.int64)
69
+ q, sq = aq(resc.reshape(-1), cmax); return q.reshape(acc.shape), sq
70
+
71
+ def rope_tables(S):
72
+ inv_freq = 1.0/(THETA**(np.arange(0, HD, 2)/HD)); ang = np.outer(np.arange(S), inv_freq)
73
+ cF = np.concatenate([np.cos(ang)]*2, 1); sF = np.concatenate([np.sin(ang)]*2, 1)
74
+ return (np.clip(np.rint(cF*32768), -32768, 32767).astype(np.int64),
75
+ np.clip(np.rint(sF*32768), -32768, 32767).astype(np.int64))
76
+ def rope_int(xq, cosQ, sinQ):
77
+ rh = np.concatenate([-xq[..., HD//2:], xq[..., :HD//2]], -1)
78
+ return (xq*cosQ[:, None, :] + rh*sinQ[:, None, :] + 16384) >> 15
79
+
80
+ def layer(hv, l, Xsq, cosQ, sinQ):
81
+ S = hv.shape[0]; L = f"model.layers.{l}."
82
+ xn_q, xn_sq = rms_q(hv, L+"input_layernorm.weight", S)
83
+ qf, qsq = proj(L+"self_attn.q_proj.weight", L+"self_attn.q_proj.bias", xn_q, xn_sq, S)
84
+ kf, ksq = proj(L+"self_attn.k_proj.weight", L+"self_attn.k_proj.bias", xn_q, xn_sq, S)
85
+ vf, vsq = proj(L+"self_attn.v_proj.weight", L+"self_attn.v_proj.bias", xn_q, xn_sq, S)
86
+ qq = rope_int(qf.reshape(S, NH, HD), cosQ, sinQ); kq = rope_int(kf.reshape(S, NKV, HD), cosQ, sinQ); vq = vf.reshape(S, NKV, HD)
87
+ kq14 = np.repeat(kq, NH//NKV, 1); vq14 = np.repeat(vq, NH//NKV, 1)
88
+ scores = np.einsum('shd,thd->hst', qq, kq14); C8 = mul(qsq, ksq)*64 # 1/512 exp units
89
+ attn = np.zeros((S, D), np.int64)
90
+ for h in range(NH):
91
+ for s in range(S):
92
+ row = scores[h, s, :s+1]; m = int(row.max())
93
+ d = np.clip([apply(m-int(r), C8) for r in row], 0, 2047); e = EXP[np.array(d)]; den = int(e.sum())
94
+ accv = e @ vq14[:s+1, h, :] # [HD] int64 (fits), exact per-lane division below
95
+ for dd in range(HD):
96
+ attn[s, h*HD+dd] = ((int(accv[dd])*(1 << SC)+(den >> 1))//den) >> SC
97
+ aqa, aqa_sq = aq(attn.reshape(-1), vsq); aqa = aqa.reshape(S, D)
98
+ _, wo, wosq = T[L+"self_attn.o_proj.weight"]; oacc = imm(aqa, wo)
99
+ ocsq = [mul(int(aqa_sq), int(wosq[dd])) for dd in range(D)]
100
+ hout = np.zeros((S, D), np.int64)
101
+ for s in range(S):
102
+ for dd in range(D): hout[s, dd] = hv[s, dd] + apply(int(oacc[s, dd]), (ocsq[dd] << SC)//Xsq[s])
103
+ # MLP
104
+ xn2_q, xn2_sq = rms_q(hout, L+"post_attention_layernorm.weight", S)
105
+ gate_q, gate_sq = proj(L+"mlp.gate_proj.weight", None, xn2_q, xn2_sq, S)
106
+ up_q, up_sq = proj(L+"mlp.up_proj.weight", None, xn2_q, xn2_sq, S)
107
+ gs9 = int(gate_sq) << 9
108
+ idx = np.clip(np.array([[((int(x)*gs9 + HALF) >> SC) + 8192 for x in row] for row in gate_q], dtype=np.int64), 0, 16383)
109
+ sig = SIG[idx]
110
+ act_int = gate_q*sig; act_q, spre = aq(act_int.reshape(-1), int(gate_sq)); act_q = act_q.reshape(S, FF); act_sq = spre >> 16
111
+ hmul = act_q*up_q; hmul_sq = mul(int(act_sq), int(up_sq))
112
+ hmul_q, hmul_q_sq = aq(hmul.reshape(-1), hmul_sq); hmul_q = hmul_q.reshape(S, FF)
113
+ mlp_q, mlp_sq = proj(L+"mlp.down_proj.weight", None, hmul_q, hmul_q_sq, S)
114
+ out = np.zeros((S, D), np.int64)
115
+ for s in range(S):
116
+ r = (int(mlp_sq) << SC)//Xsq[s]
117
+ for dd in range(D): out[s, dd] = hout[s, dd] + apply(int(mlp_q[s, dd]), r)
118
+ return out
119
+
120
+ Wemb, Wemb_sq = T["model.embed_tokens.weight"][1], T["model.embed_tokens.weight"][2]
121
+ def forward_last_logits_argmax(tokens):
122
+ S = len(tokens)
123
+ hv = np.stack([Wemb[t] for t in tokens]).astype(np.int64)
124
+ Xsq = [int(Wemb_sq[t]) for t in tokens]
125
+ cosQ, sinQ = rope_tables(S)
126
+ for l in range(NL): hv = layer(hv, l, Xsq, cosQ, sinQ)
127
+ xn_q, xn_sq = rms_q(hv, "model.norm.weight", S) # final norm
128
+ last = xn_q[S-1] # [D] int16
129
+ raw = last.astype(np.int64) @ Wemb.T.astype(np.int64) # [VOCAB] (tied LM head)
130
+ escale = np.array([int(Wemb_sq[v]) for v in range(VOCAB)], object)
131
+ score = (raw.astype(object)*escale + HALF) >> SC # exact: Q60 scale x 2^40 raw exceeds int64
132
+ return int(np.argmax(score)) # first-max tie-break (numpy argmax)
133
+
134
+ # ---- fp reference forward (dequantized weights) for token-agreement (4b-5) ----
135
+ def wd(n): _, w, s = T[n]; return w.astype(np.float64) * np.array([int(s[o])/(1 << SC) for o in range(w.shape[0])])[:, None]
136
+ def vd(n): _, v, s = T[n]; return v.astype(np.float64) * (int(s)/(1 << SC))
137
+ Wemb_fp = Wemb.astype(np.float64) * np.array([int(Wemb_sq[v])/(1 << SC) for v in range(VOCAB)])[:, None]
138
+ def rmsfp(h, g): return h/np.sqrt(np.mean(h*h, 1, keepdims=True)+1e-6)*g
139
+ def fp_argmax(tokens):
140
+ S = len(tokens); h = Wemb_fp[tokens].copy()
141
+ invf = 1.0/(THETA**(np.arange(0, HD, 2)/HD)); ang = np.outer(np.arange(S), invf)
142
+ cF = np.concatenate([np.cos(ang)]*2, 1); sF = np.concatenate([np.sin(ang)]*2, 1)
143
+ rhf = lambda x: np.concatenate([-x[..., HD//2:], x[..., :HD//2]], -1)
144
+ for l in range(NL):
145
+ L = f"model.layers.{l}."
146
+ x = rmsfp(h, vd(L+"input_layernorm.weight"))
147
+ q = (x@wd(L+"self_attn.q_proj.weight").T+vd(L+"self_attn.q_proj.bias")).reshape(S, NH, HD)
148
+ k = (x@wd(L+"self_attn.k_proj.weight").T+vd(L+"self_attn.k_proj.bias")).reshape(S, NKV, HD)
149
+ v = (x@wd(L+"self_attn.v_proj.weight").T+vd(L+"self_attn.v_proj.bias")).reshape(S, NKV, HD)
150
+ q = q*cF[:, None, :]+rhf(q)*sF[:, None, :]; k = k*cF[:, None, :]+rhf(k)*sF[:, None, :]
151
+ k14 = np.repeat(k, NH//NKV, 1); v14 = np.repeat(v, NH//NKV, 1); sc = np.einsum('shd,thd->hst', q, k14)/np.sqrt(HD)
152
+ for hh in range(NH):
153
+ for s in range(S): sc[hh, s, s+1:] = -1e30
154
+ af = np.zeros((S, NH, HD))
155
+ for hh in range(NH):
156
+ p = np.exp(sc[hh]-sc[hh].max(1, keepdims=True)); p /= p.sum(1, keepdims=True); af[:, hh, :] = p@v14[:, hh, :]
157
+ h = h + af.reshape(S, D)@wd(L+"self_attn.o_proj.weight").T
158
+ x2 = rmsfp(h, vd(L+"post_attention_layernorm.weight"))
159
+ g = x2@wd(L+"mlp.gate_proj.weight").T; u = x2@wd(L+"mlp.up_proj.weight").T
160
+ h = h + ((g/(1.0+np.exp(-g)))*u)@wd(L+"mlp.down_proj.weight").T
161
+ h = rmsfp(h, vd("model.norm.weight"))
162
+ return int(np.argmax(h[S-1]@Wemb_fp.T))
163
+
164
+ prompt = [9707, 11, 1879, 30]
165
+ toks = list(prompt); gen = []; toks_fp = list(prompt); gen_fp = []; match = 0
166
+ for _ in range(NDECODE):
167
+ nt = forward_last_logits_argmax(toks); gen.append(nt); toks.append(nt)
168
+ nf = fp_argmax(toks_fp); gen_fp.append(nf); toks_fp.append(nf)
169
+ if nt == nf: match += 1
170
+ commit = hashlib.blake2b(bytes(np.array(gen, np.int64).tobytes()), digest_size=16).hexdigest()
171
+ print(f"prompt={prompt}")
172
+ print(f"integer generated = {gen}")
173
+ print(f"fp generated = {gen_fp}")
174
+ print(f"token agreement (int vs fp) = {match}/{NDECODE}")
175
+ print(f"TOKEN COMMITMENT (blake2b16) = {commit} <- backend-independent (integer canonical class)")
docs/evidence/canon_model_int.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # inc4b-4 reference: FULLY-INTEGER full Qwen2.5-0.5B forward = 24 layers + final RMSNorm + tied LM head +
3
+ # integer argmax, with greedy decode. No fp on the compute path. Produces a backend-independent TOKEN
4
+ # commitment (blake2b over the greedy token ids) and checks token-agreement vs the fp reference.
5
+ import sys, json, struct, hashlib, numpy as np
6
+ CANON = sys.argv[1] if len(sys.argv) > 1 else "/root/models/qwen05.canon"
7
+ NDECODE = int(sys.argv[2]) if len(sys.argv) > 2 else 8
8
+ D, NH, NKV, HD, THETA, FF, NL = 896, 14, 2, 64, 1000000.0, 4864, 24
9
+ VOCAB = 151936; SC = 30; HALF = 1 << (SC-1); ALVL = 32767
10
+
11
+ def qmf(m0, sh): e = sh-1; return (m0 << e) if e >= 0 else ((m0 + (1 << (-e-1))) >> (-e))
12
+ def apply(a, s): return (a*s + HALF) >> SC
13
+ def mul(a, b): return (a*b + HALF) >> SC
14
+ def sfrom(amax, cur): return (amax*cur)//ALVL
15
+ def isqrt(n):
16
+ if n <= 0: return 0
17
+ x = int(n); y = (x+1)//2
18
+ while y < x: x = y; y = (x + int(n)//x)//2
19
+ return x
20
+ def aq(v, cur):
21
+ amax = int(np.max(np.abs(v))) or 1
22
+ return ((v*ALVL*2 + np.sign(v)*amax)//(2*amax)).astype(np.int64), sfrom(amax, cur)
23
+ def read_canon(path):
24
+ f = open(path, "rb"); assert f.read(8) == b"CANONv1\n"
25
+ n = struct.unpack("<Q", f.read(8))[0]; hdr = json.loads(f.read(n)); body = f.read(); out = {}
26
+ for t in hdr["tensors"]:
27
+ nm, k, shp = t["name"], t["kind"], t["shape"]
28
+ if k == "w2d_int8_pcs":
29
+ o, i = shp
30
+ w = np.frombuffer(body[t["w_off"]:t["w_off"]+o*i], np.int8).reshape(o, i).astype(np.int64)
31
+ sc = np.frombuffer(body[t["s_off"]:t["s_off"]+o*8], np.int32).reshape(o, 2)
32
+ out[nm] = ("w", w, np.array([qmf(int(a), int(b)) for a, b in sc], object))
33
+ else:
34
+ v = np.frombuffer(body[t["w_off"]:t["w_off"]+shp[0]*4], np.int32).astype(np.int64)
35
+ m0, sh = struct.unpack("<ii", body[t["s_off"]:t["s_off"]+8]); out[nm] = ("v", v, qmf(m0, sh))
36
+ return out
37
+ T = read_canon(CANON)
38
+ def imm(a, w): return a @ w.T
39
+ def build_exp(nt=28):
40
+ ONE = 1 << 16; t = np.zeros(256, np.int64)
41
+ for d in range(256):
42
+ x = -d*(ONE//64); e = ONE; k = nt
43
+ while k >= 1: e = ONE+((x*e)//ONE)//k; k -= 1
44
+ t[d] = max(0, e)
45
+ return t
46
+ EXP = build_exp()
47
+ SIG = np.clip(np.rint(1.0/(1.0+np.exp(-(np.arange(4096)-2048)/128.0))*(1 << 16)), 0, 1 << 16).astype(np.int64)
48
+
49
+ def rms_q(hv, gname, S):
50
+ gv, gsq = T[gname][1], T[gname][2]
51
+ ss = np.sum(hv**2, 1); mean = ss//D + 1
52
+ inv = np.array([isqrt((1 << 40)//int(m)) for m in mean], np.int64)
53
+ yg = (hv*inv[:, None])*gv[None, :]; q, s0 = aq(yg, 1 << SC)
54
+ return q, mul(mul(s0, qmf(1, 11)), int(gsq))
55
+ def proj(wname, bname, xn_q, xn_sq, S):
56
+ _, wv, wsq = T[wname]; acc = imm(xn_q, wv); OUT = acc.shape[1]
57
+ colsq = [mul(int(xn_sq), int(wsq[o])) for o in range(OUT)]
58
+ if bname:
59
+ bv, bsq = T[bname][1], int(T[bname][2])
60
+ bacc = np.array([(((int(bv[o])*bsq*(1 << SC)+(cs >> 1))//cs) >> SC) if (cs := colsq[o]) else 0 for o in range(OUT)], np.int64)
61
+ else:
62
+ bacc = np.zeros(OUT, np.int64)
63
+ accb = acc + bacc[None, :]; cmax = max(colsq); resc = np.empty_like(acc)
64
+ for o in range(OUT): resc[:, o] = apply(accb[:, o], (colsq[o] << SC)//cmax)
65
+ q, sq = aq(resc.reshape(-1), cmax); return q.reshape(acc.shape), sq
66
+
67
+ def rope_tables(S):
68
+ inv_freq = 1.0/(THETA**(np.arange(0, HD, 2)/HD)); ang = np.outer(np.arange(S), inv_freq)
69
+ cF = np.concatenate([np.cos(ang)]*2, 1); sF = np.concatenate([np.sin(ang)]*2, 1)
70
+ return (np.clip(np.rint(cF*32768), -32768, 32767).astype(np.int64),
71
+ np.clip(np.rint(sF*32768), -32768, 32767).astype(np.int64))
72
+ def rope_int(xq, cosQ, sinQ):
73
+ rh = np.concatenate([-xq[..., HD//2:], xq[..., :HD//2]], -1)
74
+ return (xq*cosQ[:, None, :] + rh*sinQ[:, None, :] + 16384) >> 15
75
+
76
+ def layer(hv, l, Xsq, cosQ, sinQ):
77
+ S = hv.shape[0]; L = f"model.layers.{l}."
78
+ xn_q, xn_sq = rms_q(hv, L+"input_layernorm.weight", S)
79
+ qf, qsq = proj(L+"self_attn.q_proj.weight", L+"self_attn.q_proj.bias", xn_q, xn_sq, S)
80
+ kf, ksq = proj(L+"self_attn.k_proj.weight", L+"self_attn.k_proj.bias", xn_q, xn_sq, S)
81
+ vf, vsq = proj(L+"self_attn.v_proj.weight", L+"self_attn.v_proj.bias", xn_q, xn_sq, S)
82
+ qq = rope_int(qf.reshape(S, NH, HD), cosQ, sinQ); kq = rope_int(kf.reshape(S, NKV, HD), cosQ, sinQ); vq = vf.reshape(S, NKV, HD)
83
+ kq14 = np.repeat(kq, NH//NKV, 1); vq14 = np.repeat(vq, NH//NKV, 1)
84
+ scores = np.einsum('shd,thd->hst', qq, kq14); C8 = mul(qsq, ksq)*8
85
+ attn = np.zeros((S, D), np.int64)
86
+ for h in range(NH):
87
+ for s in range(S):
88
+ row = scores[h, s, :s+1]; m = int(row.max())
89
+ d = np.clip([apply(m-int(r), C8) for r in row], 0, 255); e = EXP[np.array(d)]; den = int(e.sum())
90
+ accv = e @ vq14[:s+1, h, :] # [HD] int64 (fits), exact per-lane division below
91
+ for dd in range(HD):
92
+ attn[s, h*HD+dd] = ((int(accv[dd])*(1 << SC)+(den >> 1))//den) >> SC
93
+ aqa, aqa_sq = aq(attn.reshape(-1), vsq); aqa = aqa.reshape(S, D)
94
+ _, wo, wosq = T[L+"self_attn.o_proj.weight"]; oacc = imm(aqa, wo)
95
+ ocsq = [mul(int(aqa_sq), int(wosq[dd])) for dd in range(D)]
96
+ hout = np.zeros((S, D), np.int64)
97
+ for s in range(S):
98
+ for dd in range(D): hout[s, dd] = hv[s, dd] + apply(int(oacc[s, dd]), (ocsq[dd] << SC)//Xsq[s])
99
+ # MLP
100
+ xn2_q, xn2_sq = rms_q(hout, L+"post_attention_layernorm.weight", S)
101
+ gate_q, gate_sq = proj(L+"mlp.gate_proj.weight", None, xn2_q, xn2_sq, S)
102
+ up_q, up_sq = proj(L+"mlp.up_proj.weight", None, xn2_q, xn2_sq, S)
103
+ idx = np.clip(apply(gate_q, int(gate_sq) << 7) + 2048, 0, 4095); sig = SIG[idx]
104
+ act_int = gate_q*sig; act_q, spre = aq(act_int.reshape(-1), int(gate_sq)); act_q = act_q.reshape(S, FF); act_sq = spre >> 16
105
+ hmul = act_q*up_q; hmul_sq = mul(int(act_sq), int(up_sq))
106
+ hmul_q, hmul_q_sq = aq(hmul.reshape(-1), hmul_sq); hmul_q = hmul_q.reshape(S, FF)
107
+ mlp_q, mlp_sq = proj(L+"mlp.down_proj.weight", None, hmul_q, hmul_q_sq, S)
108
+ out = np.zeros((S, D), np.int64)
109
+ for s in range(S):
110
+ r = (int(mlp_sq) << SC)//Xsq[s]
111
+ for dd in range(D): out[s, dd] = hout[s, dd] + apply(int(mlp_q[s, dd]), r)
112
+ return out
113
+
114
+ Wemb, Wemb_sq = T["model.embed_tokens.weight"][1], T["model.embed_tokens.weight"][2]
115
+ def forward_last_logits_argmax(tokens):
116
+ S = len(tokens)
117
+ hv = np.stack([Wemb[t] for t in tokens]).astype(np.int64)
118
+ Xsq = [int(Wemb_sq[t]) for t in tokens]
119
+ cosQ, sinQ = rope_tables(S)
120
+ for l in range(NL): hv = layer(hv, l, Xsq, cosQ, sinQ)
121
+ xn_q, xn_sq = rms_q(hv, "model.norm.weight", S) # final norm
122
+ last = xn_q[S-1] # [D] int16
123
+ raw = last.astype(np.int64) @ Wemb.T.astype(np.int64) # [VOCAB] (tied LM head)
124
+ escale = np.array([int(Wemb_sq[v]) for v in range(VOCAB)], np.int64)
125
+ score = (raw*escale + HALF) >> SC # weight each vocab by its scale, argmax-equivalent
126
+ return int(np.argmax(score)) # first-max tie-break (numpy argmax)
127
+
128
+ # ---- fp reference forward (dequantized weights) for token-agreement (4b-5) ----
129
+ def wd(n): _, w, s = T[n]; return w.astype(np.float64) * np.array([int(s[o])/(1 << SC) for o in range(w.shape[0])])[:, None]
130
+ def vd(n): _, v, s = T[n]; return v.astype(np.float64) * (int(s)/(1 << SC))
131
+ Wemb_fp = Wemb.astype(np.float64) * np.array([int(Wemb_sq[v])/(1 << SC) for v in range(VOCAB)])[:, None]
132
+ def rmsfp(h, g): return h/np.sqrt(np.mean(h*h, 1, keepdims=True)+1e-6)*g
133
+ def fp_argmax(tokens):
134
+ S = len(tokens); h = Wemb_fp[tokens].copy()
135
+ invf = 1.0/(THETA**(np.arange(0, HD, 2)/HD)); ang = np.outer(np.arange(S), invf)
136
+ cF = np.concatenate([np.cos(ang)]*2, 1); sF = np.concatenate([np.sin(ang)]*2, 1)
137
+ rhf = lambda x: np.concatenate([-x[..., HD//2:], x[..., :HD//2]], -1)
138
+ for l in range(NL):
139
+ L = f"model.layers.{l}."
140
+ x = rmsfp(h, vd(L+"input_layernorm.weight"))
141
+ q = (x@wd(L+"self_attn.q_proj.weight").T+vd(L+"self_attn.q_proj.bias")).reshape(S, NH, HD)
142
+ k = (x@wd(L+"self_attn.k_proj.weight").T+vd(L+"self_attn.k_proj.bias")).reshape(S, NKV, HD)
143
+ v = (x@wd(L+"self_attn.v_proj.weight").T+vd(L+"self_attn.v_proj.bias")).reshape(S, NKV, HD)
144
+ q = q*cF[:, None, :]+rhf(q)*sF[:, None, :]; k = k*cF[:, None, :]+rhf(k)*sF[:, None, :]
145
+ k14 = np.repeat(k, NH//NKV, 1); v14 = np.repeat(v, NH//NKV, 1); sc = np.einsum('shd,thd->hst', q, k14)/np.sqrt(HD)
146
+ for hh in range(NH):
147
+ for s in range(S): sc[hh, s, s+1:] = -1e30
148
+ af = np.zeros((S, NH, HD))
149
+ for hh in range(NH):
150
+ p = np.exp(sc[hh]-sc[hh].max(1, keepdims=True)); p /= p.sum(1, keepdims=True); af[:, hh, :] = p@v14[:, hh, :]
151
+ h = h + af.reshape(S, D)@wd(L+"self_attn.o_proj.weight").T
152
+ x2 = rmsfp(h, vd(L+"post_attention_layernorm.weight"))
153
+ g = x2@wd(L+"mlp.gate_proj.weight").T; u = x2@wd(L+"mlp.up_proj.weight").T
154
+ h = h + ((g/(1.0+np.exp(-g)))*u)@wd(L+"mlp.down_proj.weight").T
155
+ h = rmsfp(h, vd("model.norm.weight"))
156
+ return int(np.argmax(h[S-1]@Wemb_fp.T))
157
+
158
+ prompt = [9707, 11, 1879, 30]
159
+ toks = list(prompt); gen = []; toks_fp = list(prompt); gen_fp = []; match = 0
160
+ for _ in range(NDECODE):
161
+ nt = forward_last_logits_argmax(toks); gen.append(nt); toks.append(nt)
162
+ nf = fp_argmax(toks_fp); gen_fp.append(nf); toks_fp.append(nf)
163
+ if nt == nf: match += 1
164
+ commit = hashlib.blake2b(bytes(np.array(gen, np.int64).tobytes()), digest_size=16).hexdigest()
165
+ print(f"prompt={prompt}")
166
+ print(f"integer generated = {gen}")
167
+ print(f"fp generated = {gen_fp}")
168
+ print(f"token agreement (int vs fp) = {match}/{NDECODE}")
169
+ print(f"TOKEN COMMITMENT (blake2b16) = {commit} <- backend-independent (integer canonical class)")
docs/evidence/canon_ops.rs ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Canonical integer OPS (increment 4a): softmax (fixed-point exp) + RMSNorm (integer rsqrt) + argmax.
2
+ // The remaining transcendental-bearing transformer ops, all integer, NO floating point. CPU reference.
3
+ // Build/run: rustc -O canon_ops.rs -o canon_ops && ./canon_ops
4
+
5
+ fn splitmix64(s: &mut u64) -> u64 {
6
+ *s = s.wrapping_add(0x9E3779B97F4A7C15);
7
+ let mut z = *s;
8
+ z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
9
+ z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
10
+ z ^ (z >> 31)
11
+ }
12
+ fn fnv(bytes: &[u8]) -> u64 {
13
+ let mut h: u64 = 0xcbf29ce484222325;
14
+ for &b in bytes { h ^= b as u64; h = h.wrapping_mul(0x100000001b3); }
15
+ h
16
+ }
17
+
18
+ // --- fixed-point exp table: exp(-d/64) in Q16, integer Horner (12 terms). Deterministic, no fp. ---
19
+ const ONE_Q16: i64 = 1 << 16;
20
+ fn exp_neg_q16(d: i32) -> u32 {
21
+ // arg = -d/64 in Q16
22
+ let x: i64 = -(d as i64) * (ONE_Q16 / 64); // = -d*1024
23
+ let mut e: i64 = ONE_Q16;
24
+ let mut k: i64 = 12;
25
+ while k >= 1 {
26
+ // e = 1 + (x/k)*e (Q16): (x*e)>>16 gives Q16 of x*e, then /k
27
+ e = ONE_Q16 + ((x * e) / ONE_Q16) / k;
28
+ k -= 1;
29
+ }
30
+ if e < 0 { 0 } else { e as u32 }
31
+ }
32
+ fn build_exp_lut() -> [u32; 256] {
33
+ let mut t = [0u32; 256];
34
+ for d in 0..256 { t[d] = exp_neg_q16(d as i32); }
35
+ t
36
+ }
37
+
38
+ // --- integer isqrt (floor) via Newton; deterministic. ---
39
+ fn isqrt(n: u64) -> u64 {
40
+ if n == 0 { return 0; }
41
+ let mut x = n;
42
+ let mut y = (x + 1) / 2;
43
+ while y < x { x = y; y = (x + n / x) / 2; }
44
+ x
45
+ }
46
+
47
+ fn main() {
48
+ // ---------- softmax over L int32 scores ----------
49
+ let l = 1024usize;
50
+ let mut ss = 0x71u64;
51
+ let scores: Vec<i32> = (0..l).map(|_| (splitmix64(&mut ss) % 20000) as i32 - 10000).collect();
52
+ let exp_lut = build_exp_lut();
53
+ const DSCALE: i32 = 78; // maps score gap -> exp table index d in [0,255]
54
+ let m = *scores.iter().max().unwrap();
55
+ let mut e: Vec<u32> = Vec::with_capacity(l);
56
+ let mut sum: u64 = 0;
57
+ for &s in &scores {
58
+ let mut d = (m - s) / DSCALE;
59
+ if d < 0 { d = 0; } if d > 255 { d = 255; }
60
+ let ev = exp_lut[d as usize];
61
+ e.push(ev);
62
+ sum += ev as u64;
63
+ }
64
+ // probabilities in Q16: p_i = (e_i << 16) / sum (integer division, deterministic)
65
+ let probs: Vec<u32> = e.iter().map(|&ev| (((ev as u64) << 16) / sum) as u32).collect();
66
+ let softmax_h = fnv(&probs.iter().flat_map(|x| x.to_le_bytes()).collect::<Vec<u8>>());
67
+ let exp_lut_h = fnv(&exp_lut.iter().flat_map(|x| x.to_le_bytes()).collect::<Vec<u8>>());
68
+
69
+ // ---------- RMSNorm over H int32, integer rsqrt ----------
70
+ let h = 896usize;
71
+ let mut sx = 0x72u64;
72
+ let x: Vec<i32> = (0..h).map(|_| (splitmix64(&mut sx) % 2001) as i32 - 1000).collect();
73
+ let mut sg = 0x73u64;
74
+ let gamma: Vec<i32> = (0..h).map(|_| 128 + (splitmix64(&mut sg) % 256) as i32).collect(); // ~Q8 gains
75
+ let ss2: i64 = x.iter().map(|&v| (v as i64) * (v as i64)).sum();
76
+ let mean: u64 = (ss2 as u64) / (h as u64) + 1; // +eps
77
+ let inv_q16: u64 = isqrt((1u64 << 32) / mean); // ~ 2^16 / sqrt(mean)
78
+ let y: Vec<i32> = x.iter().zip(&gamma).map(|(&v, &g)| {
79
+ let norm = ((v as i64) * (inv_q16 as i64)) >> 16; // x / rms (approx, scaled)
80
+ let sc = (norm * (g as i64)) >> 8; // apply gamma (Q8)
81
+ sc.clamp(-32768, 32767) as i32
82
+ }).collect();
83
+ let rms_h = fnv(&y.iter().flat_map(|v| v.to_le_bytes()).collect::<Vec<u8>>());
84
+
85
+ // ---------- argmax (deterministic: first max) ----------
86
+ let mut ai = 0usize; let mut av = scores[0];
87
+ for (i, &s) in scores.iter().enumerate() { if s > av { av = s; ai = i; } }
88
+
89
+ let combined = fnv(&[softmax_h.to_le_bytes(), rms_h.to_le_bytes(), (ai as u64).to_le_bytes()].concat());
90
+
91
+ println!("backend = CPU ({})", std::env::consts::ARCH);
92
+ println!("EXP_LUT fnv = {:016x}", exp_lut_h);
93
+ println!("softmax fnv = {:016x} (probs[0..3]={:?})", softmax_h, &probs[0..3]);
94
+ println!("rmsnorm fnv = {:016x} (inv_q16={}, y[0..3]={:?})", rms_h, inv_q16, &y[0..3]);
95
+ println!("argmax index = {}", ai);
96
+ println!("COMBINED fnv = {:016x} <-- GPU builds must reproduce this", combined);
97
+ }
docs/evidence/canon_ops_cuda.cu ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Canonical integer OPS on NVIDIA CUDA GPU (increment 4a): softmax + RMSNorm on device.
2
+ // Must reproduce CPU: COMBINED 4bf581ead4d57139 (EXP_LUT c4ddbbc186fe4e82).
3
+ // Build/run: nvcc -O3 -arch=sm_89 canon_ops_cuda.cu -o canon_ops_cuda && ./canon_ops_cuda
4
+
5
+ #include <cstdio>
6
+ #include <cstdint>
7
+ #include <vector>
8
+ #include <cuda_runtime.h>
9
+
10
+ static inline uint64_t splitmix64(uint64_t &s) {
11
+ s += 0x9E3779B97F4A7C15ULL;
12
+ uint64_t z = s;
13
+ z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
14
+ z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
15
+ return z ^ (z >> 31);
16
+ }
17
+ static uint64_t fnv(const uint8_t *p, size_t n) {
18
+ uint64_t h = 0xcbf29ce484222325ULL;
19
+ for (size_t i = 0; i < n; i++) { h ^= (uint64_t)p[i]; h *= 0x100000001b3ULL; }
20
+ return h;
21
+ }
22
+ static uint32_t exp_neg_q16(int32_t d) {
23
+ const int64_t ONE = 1 << 16;
24
+ int64_t x = -(int64_t)d * (ONE / 64);
25
+ int64_t e = ONE;
26
+ for (int64_t k = 12; k >= 1; k--) e = ONE + ((x * e) / ONE) / k;
27
+ return e < 0 ? 0u : (uint32_t)e;
28
+ }
29
+ static void put_le32(std::vector<uint8_t> &b, uint32_t v) { b.push_back(v & 0xff); b.push_back((v >> 8) & 0xff); b.push_back((v >> 16) & 0xff); b.push_back((v >> 24) & 0xff); }
30
+ static void put_le64(std::vector<uint8_t> &b, uint64_t v) { for (int i = 0; i < 8; i++) b.push_back((v >> (8 * i)) & 0xff); }
31
+
32
+ __global__ void softmax(const int *scores, const unsigned *elut, unsigned *probs, unsigned L, int DS) {
33
+ if (blockIdx.x * blockDim.x + threadIdx.x != 0) return;
34
+ int m = scores[0];
35
+ for (unsigned i = 1; i < L; i++) if (scores[i] > m) m = scores[i];
36
+ unsigned long long sum = 0;
37
+ for (unsigned i = 0; i < L; i++) { int d = (m - scores[i]) / DS; if (d < 0) d = 0; if (d > 255) d = 255; unsigned ev = elut[d]; probs[i] = ev; sum += ev; }
38
+ for (unsigned i = 0; i < L; i++) probs[i] = (unsigned)(((unsigned long long)probs[i] << 16) / sum);
39
+ }
40
+ __global__ void rmsnorm(const int *x, const int *g, int *y, unsigned H) {
41
+ if (blockIdx.x * blockDim.x + threadIdx.x != 0) return;
42
+ long long ssq = 0; for (unsigned i = 0; i < H; i++) { long long v = x[i]; ssq += v * v; }
43
+ unsigned long long mean = (unsigned long long)ssq / (unsigned long long)H + 1;
44
+ unsigned long long n = ((unsigned long long)1 << 32) / mean;
45
+ unsigned long long s = n; if (n > 0) { unsigned long long yy = (s + 1) / 2; while (yy < s) { s = yy; yy = (s + n / s) / 2; } } else s = 0;
46
+ for (unsigned i = 0; i < H; i++) { long long norm = ((long long)x[i] * (long long)s) >> 16; long long sc = (norm * (long long)g[i]) >> 8; if (sc < -32768) sc = -32768; if (sc > 32767) sc = 32767; y[i] = (int)sc; }
47
+ }
48
+
49
+ int main() {
50
+ const unsigned L = 1024, H = 896; const int DSCALE = 78;
51
+ uint64_t ss = 0x71; std::vector<int> scores(L);
52
+ for (unsigned i = 0; i < L; i++) scores[i] = (int)(splitmix64(ss) % 20000) - 10000;
53
+ uint64_t sx = 0x72; std::vector<int> x(H);
54
+ for (unsigned i = 0; i < H; i++) x[i] = (int)(splitmix64(sx) % 2001) - 1000;
55
+ uint64_t sg = 0x73; std::vector<int> gamma(H);
56
+ for (unsigned i = 0; i < H; i++) gamma[i] = 128 + (int)(splitmix64(sg) % 256);
57
+ std::vector<unsigned> elut(256);
58
+ for (int d = 0; d < 256; d++) elut[d] = exp_neg_q16(d);
59
+ std::vector<unsigned> probs(L); std::vector<int> y(H);
60
+
61
+ int *dS, *dX, *dG, *dY; unsigned *dE, *dP;
62
+ cudaMalloc(&dS, L * 4); cudaMalloc(&dE, 256 * 4); cudaMalloc(&dP, L * 4);
63
+ cudaMalloc(&dX, H * 4); cudaMalloc(&dG, H * 4); cudaMalloc(&dY, H * 4);
64
+ cudaMemcpy(dS, scores.data(), L * 4, cudaMemcpyHostToDevice);
65
+ cudaMemcpy(dE, elut.data(), 256 * 4, cudaMemcpyHostToDevice);
66
+ cudaMemcpy(dX, x.data(), H * 4, cudaMemcpyHostToDevice);
67
+ cudaMemcpy(dG, gamma.data(), H * 4, cudaMemcpyHostToDevice);
68
+ softmax<<<1, 1>>>(dS, dE, dP, L, DSCALE);
69
+ rmsnorm<<<1, 1>>>(dX, dG, dY, H);
70
+ cudaError_t err = cudaDeviceSynchronize();
71
+ if (err != cudaSuccess) { printf("cuda error: %s\n", cudaGetErrorString(err)); return 1; }
72
+ cudaMemcpy(probs.data(), dP, L * 4, cudaMemcpyDeviceToHost);
73
+ cudaMemcpy(y.data(), dY, H * 4, cudaMemcpyDeviceToHost);
74
+
75
+ std::vector<uint8_t> pb; for (unsigned v : probs) put_le32(pb, v);
76
+ uint64_t softmaxH = fnv(pb.data(), pb.size());
77
+ std::vector<uint8_t> yb; for (int v : y) put_le32(yb, (uint32_t)v);
78
+ uint64_t rmsH = fnv(yb.data(), yb.size());
79
+ int ai = 0, av = scores[0]; for (unsigned i = 0; i < L; i++) if (scores[i] > av) { av = scores[i]; ai = (int)i; }
80
+ std::vector<uint8_t> eb; for (unsigned v : elut) put_le32(eb, v);
81
+ uint64_t expLutH = fnv(eb.data(), eb.size());
82
+ std::vector<uint8_t> cb; put_le64(cb, softmaxH); put_le64(cb, rmsH); put_le64(cb, (uint64_t)ai);
83
+ uint64_t combined = fnv(cb.data(), cb.size());
84
+ cudaDeviceProp prop; cudaGetDeviceProperties(&prop, 0);
85
+ printf("backend = NVIDIA CUDA GPU (%s, sm_%d%d)\n", prop.name, prop.major, prop.minor);
86
+ printf("EXP_LUT fnv = %016llx (target c4ddbbc186fe4e82)\n", (unsigned long long)expLutH);
87
+ printf("softmax fnv = %016llx\n", (unsigned long long)softmaxH);
88
+ printf("rmsnorm fnv = %016llx\n", (unsigned long long)rmsH);
89
+ printf("argmax index = %d\n", ai);
90
+ printf("COMBINED fnv = %016llx (target 4bf581ead4d57139)\n", (unsigned long long)combined);
91
+ printf("MATCH = %s\n", combined == 0x4bf581ead4d57139ULL ? "OK byte-identical" : "MISMATCH");
92
+ return 0;
93
+ }
docs/evidence/canon_ops_metal.swift ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Canonical integer OPS on Apple Metal GPU (increment 4a): softmax + RMSNorm on device.
2
+ // Must reproduce CPU: COMBINED 4bf581ead4d57139 (EXP_LUT c4ddbbc186fe4e82).
3
+ // Build/run: swiftc -O canon_ops_metal.swift -o canon_ops_metal && ./canon_ops_metal
4
+
5
+ import Foundation
6
+ import Metal
7
+
8
+ func splitmix64(_ s: inout UInt64) -> UInt64 {
9
+ s = s &+ 0x9E3779B97F4A7C15
10
+ var z = s
11
+ z = (z ^ (z >> 30)) &* 0xBF58476D1CE4E5B9
12
+ z = (z ^ (z >> 27)) &* 0x94D049BB133111EB
13
+ return z ^ (z >> 31)
14
+ }
15
+ func fnv(_ bytes: [UInt8]) -> UInt64 {
16
+ var h: UInt64 = 0xcbf29ce484222325
17
+ for b in bytes { h ^= UInt64(b); h = h &* 0x100000001b3 }
18
+ return h
19
+ }
20
+ func expNegQ16(_ d: Int32) -> UInt32 {
21
+ let ONE: Int64 = 1 << 16
22
+ let x: Int64 = -Int64(d) * (ONE / 64)
23
+ var e: Int64 = ONE
24
+ var k: Int64 = 12
25
+ while k >= 1 { e = ONE + ((x &* e) / ONE) / k; k -= 1 }
26
+ return e < 0 ? 0 : UInt32(truncatingIfNeeded: e)
27
+ }
28
+ func le32u(_ v: UInt32) -> [UInt8] { [UInt8(v & 0xff), UInt8((v >> 8) & 0xff), UInt8((v >> 16) & 0xff), UInt8((v >> 24) & 0xff)] }
29
+ func le32i(_ v: Int32) -> [UInt8] { le32u(UInt32(bitPattern: v)) }
30
+ func le64(_ v: UInt64) -> [UInt8] { (0..<8).map { UInt8((v >> (8 * $0)) & 0xff) } }
31
+
32
+ let L = 1024, H = 896, DSCALE: UInt32 = 78
33
+ var ss: UInt64 = 0x71
34
+ let scores: [Int32] = (0..<L).map { _ in Int32(truncatingIfNeeded: splitmix64(&ss) % 20000) - 10000 }
35
+ var sx: UInt64 = 0x72
36
+ let x: [Int32] = (0..<H).map { _ in Int32(truncatingIfNeeded: splitmix64(&sx) % 2001) - 1000 }
37
+ var sg: UInt64 = 0x73
38
+ let gamma: [Int32] = (0..<H).map { _ in Int32(128) + Int32(truncatingIfNeeded: splitmix64(&sg) % 256) }
39
+ var expLut = [UInt32](repeating: 0, count: 256)
40
+ for d in 0..<256 { expLut[d] = expNegQ16(Int32(d)) }
41
+
42
+ let shader = """
43
+ #include <metal_stdlib>
44
+ using namespace metal;
45
+ kernel void softmax(device const int* scores [[buffer(0)]], device const uint* elut [[buffer(1)]],
46
+ device uint* probs [[buffer(2)]], constant uint2& p [[buffer(3)]], uint gid [[thread_position_in_grid]]) {
47
+ if (gid != 0) return;
48
+ uint Lv = p.x; int DS = int(p.y);
49
+ int m = scores[0];
50
+ for (uint i = 1; i < Lv; i++) if (scores[i] > m) m = scores[i];
51
+ ulong sum = 0;
52
+ for (uint i = 0; i < Lv; i++) { int d = (m - scores[i]) / DS; if (d < 0) d = 0; if (d > 255) d = 255; uint ev = elut[d]; probs[i] = ev; sum += ev; }
53
+ for (uint i = 0; i < Lv; i++) probs[i] = (uint)(((ulong)probs[i] << 16) / sum);
54
+ }
55
+ kernel void rmsnorm(device const int* x [[buffer(0)]], device const int* g [[buffer(1)]],
56
+ device int* y [[buffer(2)]], constant uint2& p [[buffer(3)]], uint gid [[thread_position_in_grid]]) {
57
+ if (gid != 0) return;
58
+ uint Hv = p.x;
59
+ long ssq = 0; for (uint i = 0; i < Hv; i++) { long v = x[i]; ssq += v * v; }
60
+ ulong mean = (ulong)ssq / (ulong)Hv + 1;
61
+ ulong n = ((ulong)1 << 32) / mean;
62
+ ulong s = n; if (n > 0) { ulong yy = (s + 1) / 2; while (yy < s) { s = yy; yy = (s + n / s) / 2; } } else s = 0;
63
+ for (uint i = 0; i < Hv; i++) { long norm = ((long)x[i] * (long)s) >> 16; long sc = (norm * (long)g[i]) >> 8; if (sc < -32768) sc = -32768; if (sc > 32767) sc = 32767; y[i] = (int)sc; }
64
+ }
65
+ """
66
+
67
+ guard let dev = MTLCreateSystemDefaultDevice() else { fatalError("no Metal") }
68
+ let lib = try! dev.makeLibrary(source: shader, options: nil)
69
+ let pSoft = try! dev.makeComputePipelineState(function: lib.makeFunction(name: "softmax")!)
70
+ let pRms = try! dev.makeComputePipelineState(function: lib.makeFunction(name: "rmsnorm")!)
71
+ let q = dev.makeCommandQueue()!
72
+ func buf<T>(_ a: [T]) -> MTLBuffer { dev.makeBuffer(bytes: a, length: MemoryLayout<T>.stride * a.count, options: .storageModeShared)! }
73
+
74
+ let bScores = buf(scores), bElut = buf(expLut), bProbs = dev.makeBuffer(length: L * 4, options: .storageModeShared)!
75
+ var pSoftDims = SIMD2<UInt32>(UInt32(L), DSCALE)
76
+ let bSD = dev.makeBuffer(bytes: &pSoftDims, length: 8, options: .storageModeShared)!
77
+ let bX = buf(x), bG = buf(gamma), bY = dev.makeBuffer(length: H * 4, options: .storageModeShared)!
78
+ var pRmsDims = SIMD2<UInt32>(UInt32(H), 0)
79
+ let bRD = dev.makeBuffer(bytes: &pRmsDims, length: 8, options: .storageModeShared)!
80
+
81
+ let cmd = q.makeCommandBuffer()!
82
+ let e1 = cmd.makeComputeCommandEncoder()!
83
+ e1.setComputePipelineState(pSoft)
84
+ for (i, b) in [bScores, bElut, bProbs, bSD].enumerated() { e1.setBuffer(b, offset: 0, index: i) }
85
+ e1.dispatchThreads(MTLSize(width: 1, height: 1, depth: 1), threadsPerThreadgroup: MTLSize(width: 1, height: 1, depth: 1))
86
+ e1.endEncoding()
87
+ let e2 = cmd.makeComputeCommandEncoder()!
88
+ e2.setComputePipelineState(pRms)
89
+ for (i, b) in [bX, bG, bY, bRD].enumerated() { e2.setBuffer(b, offset: 0, index: i) }
90
+ e2.dispatchThreads(MTLSize(width: 1, height: 1, depth: 1), threadsPerThreadgroup: MTLSize(width: 1, height: 1, depth: 1))
91
+ e2.endEncoding()
92
+ cmd.commit(); cmd.waitUntilCompleted()
93
+
94
+ let pptr = bProbs.contents().bindMemory(to: UInt32.self, capacity: L)
95
+ var pbytes = [UInt8](); for i in 0..<L { pbytes += le32u(pptr[i]) }
96
+ let softmaxH = fnv(pbytes)
97
+ let yptr = bY.contents().bindMemory(to: Int32.self, capacity: H)
98
+ var ybytes = [UInt8](); for i in 0..<H { ybytes += le32i(yptr[i]) }
99
+ let rmsH = fnv(ybytes)
100
+ var ai = 0; var av = scores[0]
101
+ for i in 0..<L { if scores[i] > av { av = scores[i]; ai = i } }
102
+ let expLutH = fnv(expLut.flatMap { le32u($0) })
103
+ let combined = fnv(le64(softmaxH) + le64(rmsH) + le64(UInt64(ai)))
104
+ print("backend = Apple Metal GPU (\(dev.name))")
105
+ print(String(format: "EXP_LUT fnv = %016llx (target c4ddbbc186fe4e82)", expLutH))
106
+ print(String(format: "softmax fnv = %016llx", softmaxH))
107
+ print(String(format: "rmsnorm fnv = %016llx", rmsH))
108
+ print("argmax index = \(ai)")
109
+ print(String(format: "COMBINED fnv = %016llx (target 4bf581ead4d57139)", combined))
110
+ print("MATCH = \(String(format: "%016llx", combined) == "4bf581ead4d57139" ? "✅ byte-identical to CPU" : "❌ MISMATCH")")
docs/evidence/canon_quant.rs ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // inc4b-1: offline canonical quantizer core — fp32 weights -> per-output-channel symmetric int8 with a
2
+ // PINNED fixed-point scale (multiplier M0:i32, shift:i32), plus a minimal `.canon` container.
3
+ // Floating point is used OFFLINE ONLY (this tool); the produced artifact is deterministic bytes that every
4
+ // runtime backend consumes identically. This build runs a SELF-TEST on synthetic weights (no download):
5
+ // it proves (a) determinism, (b) bounded round-trip error, (c) the fixed-point scale reconstructs the true
6
+ // scale, (d) `.canon` write/read round-trips.
7
+ //
8
+ // Build/run: rustc -O canon_quant.rs -o canon_quant && ./canon_quant
9
+
10
+ use std::convert::TryInto;
11
+
12
+ fn splitmix64(s: &mut u64) -> u64 {
13
+ *s = s.wrapping_add(0x9E3779B97F4A7C15);
14
+ let mut z = *s;
15
+ z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
16
+ z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
17
+ z ^ (z >> 31)
18
+ }
19
+ fn fnv(bytes: &[u8]) -> u64 {
20
+ let mut h: u64 = 0xcbf29ce484222325;
21
+ for &b in bytes { h ^= b as u64; h = h.wrapping_mul(0x100000001b3); }
22
+ h
23
+ }
24
+
25
+ // gemmlowp-style: real d>0 -> (M0 in [2^30,2^31), shift) with d ≈ M0 * 2^(shift-31).
26
+ fn quantize_multiplier(d: f64) -> (i32, i32) {
27
+ if d <= 0.0 { return (0, 0); }
28
+ let mut shift = 0i32;
29
+ let mut q = d;
30
+ while q >= 1.0 { q *= 0.5; shift += 1; }
31
+ while q < 0.5 { q *= 2.0; shift -= 1; }
32
+ let mut m0 = (q * (1i64 << 31) as f64).round() as i64; // q in [0.5,1) -> M0 in [2^30, 2^31]
33
+ if m0 == (1i64 << 31) { m0 >>= 1; shift += 1; }
34
+ (m0 as i32, shift)
35
+ }
36
+ fn scale_from_fixed(m0: i32, shift: i32) -> f64 { (m0 as f64) * 2f64.powi(shift - 31) }
37
+
38
+ struct QTensor { out_ch: usize, in_ch: usize, w: Vec<i8>, m0: Vec<i32>, shift: Vec<i32> }
39
+
40
+ // per-output-channel symmetric int8 quantization (round-half-away-from-zero, deterministic).
41
+ fn quantize_perchannel(w: &[f32], out_ch: usize, in_ch: usize) -> QTensor {
42
+ let mut qw = vec![0i8; out_ch * in_ch];
43
+ let mut m0 = vec![0i32; out_ch];
44
+ let mut shift = vec![0i32; out_ch];
45
+ for c in 0..out_ch {
46
+ let row = &w[c * in_ch..(c + 1) * in_ch];
47
+ let amax = row.iter().fold(0f32, |a, &x| a.max(x.abs()));
48
+ let scale = if amax > 0.0 { (amax as f64) / 127.0 } else { 1.0 }; // dequant scale
49
+ let inv = if amax > 0.0 { 127.0 / (amax as f64) } else { 0.0 };
50
+ for k in 0..in_ch {
51
+ let v = (row[k] as f64) * inv;
52
+ let r = if v >= 0.0 { (v + 0.5).floor() } else { (v - 0.5).ceil() }; // half away from zero
53
+ qw[c * in_ch + k] = r.clamp(-127.0, 127.0) as i8;
54
+ }
55
+ let (mm, sh) = quantize_multiplier(scale);
56
+ m0[c] = mm; shift[c] = sh;
57
+ }
58
+ QTensor { out_ch, in_ch, w: qw, m0, shift }
59
+ }
60
+
61
+ // minimal `.canon` v0 container: magic + one tensor (out,in) + i8 weights + per-channel (M0,shift).
62
+ fn write_canon(t: &QTensor) -> Vec<u8> {
63
+ let mut b = Vec::new();
64
+ b.extend_from_slice(b"CANONv0\n");
65
+ b.extend_from_slice(&(t.out_ch as u32).to_le_bytes());
66
+ b.extend_from_slice(&(t.in_ch as u32).to_le_bytes());
67
+ b.extend_from_slice(&t.w.iter().map(|&x| x as u8).collect::<Vec<u8>>());
68
+ for c in 0..t.out_ch { b.extend_from_slice(&t.m0[c].to_le_bytes()); b.extend_from_slice(&t.shift[c].to_le_bytes()); }
69
+ b
70
+ }
71
+ fn read_canon(b: &[u8]) -> QTensor {
72
+ assert_eq!(&b[0..8], b"CANONv0\n");
73
+ let out_ch = u32::from_le_bytes(b[8..12].try_into().unwrap()) as usize;
74
+ let in_ch = u32::from_le_bytes(b[12..16].try_into().unwrap()) as usize;
75
+ let wend = 16 + out_ch * in_ch;
76
+ let w: Vec<i8> = b[16..wend].iter().map(|&x| x as i8).collect();
77
+ let mut m0 = vec![0i32; out_ch]; let mut shift = vec![0i32; out_ch];
78
+ let mut p = wend;
79
+ for c in 0..out_ch {
80
+ m0[c] = i32::from_le_bytes(b[p..p + 4].try_into().unwrap()); p += 4;
81
+ shift[c] = i32::from_le_bytes(b[p..p + 4].try_into().unwrap()); p += 4;
82
+ }
83
+ QTensor { out_ch, in_ch, w, m0, shift }
84
+ }
85
+
86
+ fn main() {
87
+ // synthetic fp32 weight matrix [out=896, in=896] (a Qwen 0.5B proj-sized tensor), deterministic.
88
+ let (out_ch, in_ch) = (896usize, 896usize);
89
+ let mut s: u64 = 0x0000_0000_0051_4E54; // "QNT" seed
90
+ let w: Vec<f32> = (0..out_ch * in_ch).map(|_| {
91
+ let u = splitmix64(&mut s);
92
+ // map to ~N(0,0.05)-ish via two uniforms folded — offline fp is fine
93
+ let a = ((u & 0xFFFFFFFF) as f64) / (u32::MAX as f64) - 0.5;
94
+ let b = ((u >> 32) as f64) / (u32::MAX as f64) - 0.5;
95
+ ((a + b) * 0.1) as f32
96
+ }).collect();
97
+
98
+ let qt = quantize_perchannel(&w, out_ch, in_ch);
99
+
100
+ // (b) round-trip error: dequant and compare to original
101
+ let mut max_abs = 0f64; let mut sse = 0f64; let mut sxx = 0f64;
102
+ for c in 0..out_ch {
103
+ let sc = scale_from_fixed(qt.m0[c], qt.shift[c]);
104
+ for k in 0..in_ch {
105
+ let deq = qt.w[c * in_ch + k] as f64 * sc;
106
+ let orig = w[c * in_ch + k] as f64;
107
+ let e = (deq - orig).abs();
108
+ if e > max_abs { max_abs = e; }
109
+ sse += e * e; sxx += orig * orig;
110
+ }
111
+ }
112
+ let rms_rel = (sse / sxx).sqrt();
113
+
114
+ // (c) fixed-point scale reconstruction error (vs the true amax/127 per channel)
115
+ let mut max_scale_rel = 0f64;
116
+ for c in 0..out_ch {
117
+ let row = &w[c * in_ch..(c + 1) * in_ch];
118
+ let amax = row.iter().fold(0f32, |a, &x| a.max(x.abs())) as f64;
119
+ if amax > 0.0 {
120
+ let true_s = amax / 127.0;
121
+ let rec = scale_from_fixed(qt.m0[c], qt.shift[c]);
122
+ max_scale_rel = max_scale_rel.max(((rec - true_s) / true_s).abs());
123
+ }
124
+ }
125
+
126
+ // (a) determinism + (d) `.canon` round-trip
127
+ let bytes = write_canon(&qt);
128
+ let file_fnv = fnv(&bytes);
129
+ let qt2 = read_canon(&bytes);
130
+ let rt_ok = qt2.w == qt.w && qt2.m0 == qt.m0 && qt2.shift == qt.shift;
131
+
132
+ println!("tensor = [{}, {}] int8, per-channel fixed-point scale", out_ch, in_ch);
133
+ println!("round-trip (dequant): max_abs_err={:.3e} rms_rel_err={:.4}", max_abs, rms_rel);
134
+ println!("fixed-point scale rel err (max) = {:.2e} (M0 is 31-bit -> ~ns precision)", max_scale_rel);
135
+ println!(".canon bytes = {} fnv={:016x}", bytes.len(), file_fnv);
136
+ println!(".canon round-trip = {}", if rt_ok { "OK (weights+scales identical)" } else { "FAIL" });
137
+ println!("sample ch0: M0={} shift={} scale={:.6e} w[0..6]={:?}",
138
+ qt.m0[0], qt.shift[0], scale_from_fixed(qt.m0[0], qt.shift[0]), &qt.w[0..6]);
139
+ }
docs/evidence/canon_quant_full.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # inc4b-1 full-model offline quantizer: safetensors (bf16) -> per-output-channel symmetric int8 with a
3
+ # PINNED fixed-point scale (M0:i32, shift:i32), written into a `.canon` v1 container. Floating point is
4
+ # used OFFLINE ONLY here; the emitted artifact is deterministic bytes consumed identically on every backend.
5
+ # Canonical rounding: round-half-to-even (numpy default / IEEE). 2D tensors -> int8 per out-channel.
6
+ # 1D tensors (norms/biases) -> stored as fixed-point int32 with one (M0,shift) (kept for the runtime).
7
+ #
8
+ # Usage: python3 canon_quant_full.py model.safetensors out.canon
9
+
10
+ import sys, json, struct, numpy as np
11
+
12
+ def load_safetensors(path):
13
+ with open(path, "rb") as f:
14
+ n = struct.unpack("<Q", f.read(8))[0]
15
+ header = json.loads(f.read(n).decode("utf-8"))
16
+ data = f.read()
17
+ tensors = {}
18
+ for name, meta in header.items():
19
+ if name == "__metadata__":
20
+ continue
21
+ dt = meta["dtype"]; shape = meta["shape"]; s, e = meta["data_offsets"]
22
+ buf = data[s:e]
23
+ if dt == "BF16":
24
+ u16 = np.frombuffer(buf, dtype=np.uint16).astype(np.uint32)
25
+ arr = (u16 << 16).view(np.float32)
26
+ elif dt == "F16":
27
+ arr = np.frombuffer(buf, dtype=np.float16).astype(np.float32)
28
+ elif dt == "F32":
29
+ arr = np.frombuffer(buf, dtype=np.float32).copy()
30
+ else:
31
+ raise ValueError(f"dtype {dt} for {name}")
32
+ tensors[name] = (arr.reshape(shape), dt, shape)
33
+ return tensors
34
+
35
+ def quantize_multiplier(scale):
36
+ # scale (float64 array > 0) -> (M0 int32, shift int32) with scale = M0 * 2^(shift-31)
37
+ m, e = np.frexp(scale) # scale = m * 2^e, m in [0.5,1)
38
+ M0 = np.round(m * (1 << 31)).astype(np.int64)
39
+ over = M0 == (1 << 31)
40
+ M0 = np.where(over, M0 >> 1, M0)
41
+ e = np.where(over, e + 1, e)
42
+ M0 = np.where(scale > 0, M0, 0).astype(np.int32)
43
+ shift = np.where(scale > 0, e, 0).astype(np.int32)
44
+ return M0, shift
45
+
46
+ def quant_perchannel(W):
47
+ # W: [out, in] float32 -> int8 [out,in], (M0,shift)[out]
48
+ amax = np.max(np.abs(W), axis=1)
49
+ inv = np.where(amax > 0, 127.0 / amax, 0.0)
50
+ q = np.rint(W * inv[:, None]) # round-half-to-even
51
+ q = np.clip(q, -127, 127).astype(np.int8)
52
+ scale = np.where(amax > 0, amax / 127.0, 1.0).astype(np.float64)
53
+ M0, shift = quantize_multiplier(scale)
54
+ return q, M0, shift
55
+
56
+ def main():
57
+ src, dst = sys.argv[1], sys.argv[2]
58
+ tensors = load_safetensors(src)
59
+ print(f"tensors: {len(tensors)}")
60
+ directory = [] # (name, kind, shape, w_off, w_len, s_off)
61
+ body = bytearray()
62
+ total_elems = 0
63
+ worst = []
64
+ for name in sorted(tensors.keys()):
65
+ arr, dt, shape = tensors[name]
66
+ total_elems += arr.size
67
+ if arr.ndim == 2:
68
+ q, M0, shift = quant_perchannel(arr.astype(np.float32))
69
+ # round-trip rms for reporting
70
+ sc = M0.astype(np.float64) * (2.0 ** (shift.astype(np.float64) - 31))
71
+ deq = q.astype(np.float64) * sc[:, None]
72
+ num = np.sum((deq - arr.astype(np.float64)) ** 2)
73
+ den = np.sum(arr.astype(np.float64) ** 2) + 1e-12
74
+ rms = float(np.sqrt(num / den))
75
+ worst.append((rms, name, list(shape)))
76
+ w_off = len(body); wbytes = q.tobytes(); body += wbytes
77
+ s_off = len(body)
78
+ for c in range(q.shape[0]):
79
+ body += struct.pack("<ii", int(M0[c]), int(shift[c]))
80
+ directory.append({"name": name, "kind": "w2d_int8_pcs", "shape": list(shape),
81
+ "w_off": w_off, "w_len": len(wbytes), "s_off": s_off})
82
+ else:
83
+ # 1D (norm gamma / bias): fixed-point int32 with a single (M0,shift)
84
+ v = arr.astype(np.float64)
85
+ amax = float(np.max(np.abs(v))) if v.size else 0.0
86
+ scale = amax / (1 << 20) if amax > 0 else 1.0 # Q ~ 20-bit headroom
87
+ qi = np.rint(v / scale).astype(np.int32)
88
+ M0, shift = quantize_multiplier(np.array([scale]))
89
+ w_off = len(body); qb = qi.tobytes(); body += qb
90
+ s_off = len(body); body += struct.pack("<ii", int(M0[0]), int(shift[0]))
91
+ directory.append({"name": name, "kind": "v1d_int32_fp", "shape": list(shape),
92
+ "w_off": w_off, "w_len": len(qb), "s_off": s_off})
93
+
94
+ header = {"format": "canon.v1", "model": "qwen2.5-0.5b", "rounding": "half-to-even",
95
+ "tensors": directory}
96
+ hb = json.dumps(header, sort_keys=True, separators=(",", ":")).encode("utf-8")
97
+ with open(dst, "wb") as f:
98
+ f.write(b"CANONv1\n")
99
+ f.write(struct.pack("<Q", len(hb)))
100
+ f.write(hb)
101
+ f.write(body)
102
+
103
+ import hashlib
104
+ fb = open(dst, "rb").read()
105
+ h = hashlib.blake2b(fb, digest_size=16).hexdigest()
106
+ worst.sort(reverse=True)
107
+ print(f"params: {total_elems:,}")
108
+ print(f".canon: {len(fb):,} bytes blake2b16={h}")
109
+ print("worst per-tensor rms (2D):")
110
+ for rms, name, shp in worst[:6]:
111
+ print(f" {rms:.4f} {name} {shp}")
112
+ print("best/median count:", len(worst), "2D tensors quantized")
113
+
114
+ if __name__ == "__main__":
115
+ main()
docs/evidence/canonical-int-attention-4b2a-2026-07-19.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — inc4b-2a integer attention block on real Qwen2.5-0.5B weights (2026-07-19)
2
+
3
+ First integer-runtime piece for [inc4b](../canonical-integer-inc4b-fullmodel-design.md): a full attention
4
+ block (layer 0) that consumes the real `.canon` W8A8 artifact and runs the whole path in integer —
5
+ RMSNorm(isqrt) → Q/K/V int8 GEMM (+bias) → RoPE (fixed-point Q15 tables) → QK^T → integer softmax (exp-LUT)
6
+ → A·V → o_proj → residual. Validated against a full-fp reference computed from the **same dequantized
7
+ weights**, to check the arithmetic is correct **before** the 3-backend byte-identity port (4b-2b).
8
+
9
+ Script: `docs/evidence/canon_attn.py` (reads `docs/evidence/canon_dump.py`-inspectable `.canon`). Inputs are
10
+ the real embedding rows for tokens `[9707, 11, 1879, 30]`, S=4 prefill.
11
+
12
+ ## Per-stage agreement (integer vs fp)
13
+
14
+ | stage | int-vs-fp rel err | verdict |
15
+ |---|---|---|
16
+ | exp-LUT vs true `exp` (after bumping Horner 12→28 terms) | 0.0054 | accurate |
17
+ | integer softmax vs fp softmax (same scores) | 0.020 | correct |
18
+ | RoPE (int-dequant vs fp-rope, same q) | 0.024 | correct |
19
+ | Q/K/V projection (int8 GEMM) vs fp projection | 0.023 | correct |
20
+ | **attention out, W8 weights only, fp activations** | **0.0000** | **dataflow exactly correct** |
21
+ | attention out, full W8A8 (per-tensor activation quant) | 0.361 | ← accuracy problem |
22
+
23
+ ## Findings
24
+
25
+ 1. **The integer attention block is wired exactly right.** With int8 **weights** (per-output-channel) but
26
+ fp activations, the attention output matches fp to **0.0000** — GEMM / GQA head mapping / RoPE / causal
27
+ mask / softmax / o_proj / residual are all correct. Per-channel int8 weights are ~lossless here.
28
+ 2. **Per-tensor int8 *activation* quantization is the whole error** (36%). The RMSNorm output (attention
29
+ input) has **outlier channels**; a single per-tensor scale is set by the outliers and crushes the normal
30
+ channels to near-zero → large relative error → amplified ~15× through the peaked softmax. This is the
31
+ well-known transformer activation-outlier problem, surfaced early.
32
+ 3. The exp-LUT from inc4a was *deterministic but inaccurate* at 12 Horner terms (poor near arg −4); 28 terms
33
+ fixes it (0.5%). Determinism was never in question — accuracy was.
34
+
35
+ ## Implications
36
+
37
+ - **Determinism (the objective) is unaffected**: the block is all-integer and will be byte-identical across
38
+ backends (4b-2b) regardless of the quant scheme's *accuracy*.
39
+ - **Usability needs a better activation quant**: per-tensor A8 is inadequate. Fix options: **SmoothQuant**
40
+ (migrate per-channel activation scale into the weights, keeping the int8 GEMM clean), per-channel/per-group
41
+ handling, or W8A16 activations. Per-token alone won't fix per-channel outliers.
42
+ - The offline quantizer (4b-1) is fine as-is for weights; the activation-scale strategy is a runtime +
43
+ calibration decision to settle before the full 24-layer forward (else the model outputs garbage).
44
+
45
+ ## Accuracy fix (`docs/evidence/canon_attn_sq.py`)
46
+
47
+ Chasing the 36% turned up the actual root cause — and it was **not** activation quant or softmax:
48
+
49
+ 1. **The integer RMSNorm had a precision bug.** `y = (Xq·inv) >> 16` right-shifted away all the fractional
50
+ bits, crushing the normalized activations to tiny integers (0,1,2). Keeping the normalized value in
51
+ high-precision fixed point (Q20: `y = Xq·isqrt(2^40/mean)`) fixes it. This single fix dropped W8A16 error
52
+ 36% → 2.6%.
53
+ 2. **int16 activations (W8A16) resolve the outlier problem** without SmoothQuant tuning:
54
+
55
+ | config | attention out rel err |
56
+ |---|---|
57
+ | W8A8 per-tensor | 0.215 |
58
+ | W8A8 + SmoothQuant (α=0.5, untuned) | 0.211 |
59
+ | **W8A16 per-tensor** | **0.026** |
60
+ | W8A16 + fp-softmax | 0.017 (integer softmax adds ~1%) |
61
+
62
+ 3. Naive SmoothQuant (α=0.5, tiny calibration) barely helped here — it needs real calibration/tuning. W8A16
63
+ is the simpler, robust choice: int8 weight × int16 activation → int64 accumulate stays fully integer
64
+ (deterministic), and 16-bit range holds the outlier channels without crushing the rest.
65
+
66
+ **Conclusion**: with the RMSNorm-precision fix + **W8A16**, the integer attention block is accurate (2.6%)
67
+ AND deterministic. W8A8 (int8 activations, tensor-core-friendly) would need tuned SmoothQuant — a later perf
68
+ optimization. Determinism was never in question; the work here was accuracy.
69
+
70
+ ## Honest scope
71
+
72
+ - Scalar activation scales are carried as fp here (correctness cut); the 4b-2b port makes them fixed-point
73
+ and proves byte-identity on CPU/Metal/CUDA.
74
+ - Single (S=4) prompt, layer 0 only. 2.6%/block is usable; the compounded token-match vs fp over 24 layers
75
+ is measured in 4b-4/4b-5.
76
+ - int8×int16 is not a standard tensor-core op, so W8A16 runs on the regular integer ALU (determinism intact,
77
+ lower throughput). W8A8 + tuned SmoothQuant recovers tensor cores later if needed.
docs/evidence/canonical-int-attention-4b2b-2026-07-19.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — inc4b-2b: integer attention block byte-identical across CPU (×2 ISA) + NVIDIA CUDA GPU (2026-07-19)
2
+
3
+ The fully-integer attention block (Qwen2.5-0.5B layer 0), consuming the **real** `.canon` W8A16 artifact, is
4
+ **byte-identical across four implementations on real hardware** — the first end-to-end "same integer
5
+ computation, same bits, different vendor" demonstration on a real transformer block.
6
+
7
+ ## What runs
8
+
9
+ All scales are **Q30 fixed-point integers**; there is **no floating point on the compute path** (RMSNorm
10
+ uses integer `isqrt`, RoPE uses pinned Q15 tables, softmax uses an integer exp-LUT). The block:
11
+ `RMSNorm → Q/K/V int8 GEMM (+bias, W8A16) → RoPE → QK^T → integer softmax → A·V → o_proj → residual`.
12
+
13
+ - **Reference** `docs/evidence/canon_attn_int.py` (Python, arbitrary-precision ints) — defines the spec and
14
+ the target fingerprint (FNV over the int64 block output). Accuracy vs fp = **3.78%** (usable).
15
+ - **Extractor** `docs/evidence/canon_extract.py` — pulls the 9 layer-0 tensors + the test-token embedding
16
+ rows + RoPE/EXP tables from `qwen05.canon` into a flat `layer0.bin` (so standalone builds need no JSON).
17
+ - **Rust** `docs/evidence/canon_attn.rs` — `i128` scales, `i64` values, explicit floor-division to match
18
+ Python's `//`. This is the CPU runtime.
19
+ - **CUDA** `docs/evidence/canon_attn.cu` — host parses, a single-thread `__global__` kernel runs the block
20
+ on the GPU with `__int128` scales.
21
+
22
+ ## Results — one fingerprint, four implementations
23
+
24
+ | implementation | hardware | ISA / arch | block-out FNV |
25
+ |---|---|---|---|
26
+ | Python reference | RTX box | x86_64 | `dea9a75ba85b1352` |
27
+ | Rust CPU | RTX box | x86_64 | `dea9a75ba85b1352` |
28
+ | Rust CPU | Mac | **aarch64** | `dea9a75ba85b1352` |
29
+ | **CUDA GPU** | RTX 4060 Ti | **Ada sm_89** | `dea9a75ba85b1352` |
30
+
31
+ Block output `hout[0..4] = [-81, 33, -21, 30]` on all four.
32
+
33
+ ## Reading
34
+
35
+ - A **real** transformer attention block (real Qwen2.5-0.5B weights, not a synthetic primitive) produces
36
+ **byte-identical output on Apple aarch64 CPU, x86 CPU, and NVIDIA Ada GPU**. This is "Mac attention verified
37
+ on CUDA" — the cross-vendor determinism the canonical-integer path exists to deliver, now shown end-to-end
38
+ on a whole block, not just per-op.
39
+ - The i128 scale arithmetic is deterministic across Rust (`i128`) and CUDA (`__int128`); the integer floor
40
+ division + arithmetic shifts match exactly.
41
+
42
+ ## Honest scope / what remains
43
+
44
+ - **Metal GPU port is not done.** Metal Shading Language has no 128-bit integer (`long` = i64 max). The
45
+ current formulation lets Q30 scales grow to ~2^49, so products need i128 — this must be **refactored to a
46
+ bounded (m0, shift) representation** so all arithmetic fits i64 before Metal can join. The Apple **CPU**
47
+ (aarch64) already matches here, so Apple *hardware* is covered; the Apple **GPU** specifically needs that
48
+ refactor.
49
+ - Single-thread CUDA kernel (S=4 is tiny); a parallel-reduction/tensor-core version is a perf follow-up
50
+ (determinism unaffected — integer associativity).
51
+ - Layer 0, one prompt. Next: **4b-3** (full transformer layer = this attention block + SwiGLU MLP + 2nd
52
+ RMSNorm), **4b-4** (all 24 layers + LM head + argmax → token commitment), **4b-5** (token-match vs fp +
53
+ wire a `canonical` class into the verifier).
docs/evidence/canonical-int-cross-isa-2026-07-19.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — Canonical integer compute is order- and ISA-independent (2026-07-19)
2
+
3
+ First-party run record backing [`docs/canonical-integer-compute-design.md`](../canonical-integer-compute-design.md)
4
+ §4. Demonstrates the core theorem on **two real, different CPU ISAs**: an `int8×int8→int32` GEMM is
5
+ byte-identical across reduction orders and across `aarch64` (Apple) vs `x86_64` (the NVIDIA/WSL2 host),
6
+ while the same computation in fp32 diverges across reduction orders.
7
+
8
+ ## Artifact
9
+
10
+ `docs/evidence/canon_int.rs` — dependency-free Rust. Generates identical `int8` inputs on both machines via
11
+ a fixed splitmix64 seed, computes a `32×8192×32` GEMM two ways (sequential vs pairwise-tree reduction) in
12
+ both `int32` and `f32`, applies a fixed-point requantization to `int8`, and prints an FNV-1a fingerprint of
13
+ each. Build/run:
14
+
15
+ ```sh
16
+ rustc -O canon_int.rs -o canon_int && ./canon_int
17
+ ```
18
+
19
+ ## Results
20
+
21
+ **Mac (Apple, aarch64):**
22
+ ```
23
+ arch = aarch64
24
+ shape M,K,N = 32,8192,32
25
+ INT seq fnv = 4b138ed70d137781
26
+ INT tree fnv = 4b138ed70d137781
27
+ INT order-invariant= MATCH ✅ (hardware-independent)
28
+ FP32 seq fnv = 9019ee04ee549e3e
29
+ FP32 tree fnv = e68f80da326d710d
30
+ FP32 order-invariant= DIFFER ⚠️ (order-dependent → hardware-dependent)
31
+ CANONICAL commitment= 743652581b0b9725
32
+ ```
33
+
34
+ **RTX box (NVIDIA host, x86_64, WSL2):**
35
+ ```
36
+ arch = x86_64
37
+ shape M,K,N = 32,8192,32
38
+ INT seq fnv = 4b138ed70d137781
39
+ INT tree fnv = 4b138ed70d137781
40
+ INT order-invariant= MATCH ✅ (hardware-independent)
41
+ FP32 seq fnv = 9019ee04ee549e3e
42
+ FP32 tree fnv = e68f80da326d710d
43
+ FP32 order-invariant= DIFFER ⚠️ (order-dependent → hardware-dependent)
44
+ CANONICAL commitment= 743652581b0b9725
45
+ ```
46
+
47
+ ## Reading
48
+
49
+ - **INT canonical commitment `743652581b0b9725` is byte-identical on aarch64 and x86_64** — and identical
50
+ under two different reduction orders. This is the `int32`-ring associativity guarantee, on real hardware.
51
+ - **fp32 diverges across reduction orders** (`9019ee04…` seq vs `e68f80da…` tree). Different GPU kernels /
52
+ tensor-core generations = different reduction orders = different bits. This is exactly why RTX 20 (`sm_75`)
53
+ and RTX 40 (`sm_89`) cannot be assumed to share an fp class.
54
+
55
+ ## Honest scope
56
+
57
+ - This is **CPU** integer arithmetic across two ISAs. It proves the math (order/ISA independence) but does
58
+ **not** yet exercise a **GPU int8 kernel** — that is increment 2 in the design doc (custom CUDA + Metal
59
+ int8 GEMM, then bit-identity across CPU / Metal / CUDA-Ada). By the theorem, GPU parallel reductions fall
60
+ under the same invariance, but that must still be demonstrated on device.
61
+ - No Turing (`sm_75`) hardware was available here; the final RTX 20 bit-identity check is a user HW step.
docs/evidence/canonical-int-fulllayer-4b3-2026-07-20.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — inc4b-3: fully-integer full transformer layer (reference) (2026-07-20)
2
+
3
+ Extends the attention block (4b-2b) to a **complete Qwen2.5-0.5B transformer layer**, fully integer, no fp on
4
+ the compute path: `RMSNorm → attention → residual → RMSNorm → SwiGLU MLP → residual`.
5
+
6
+ Reference: `docs/evidence/canon_layer_int.py` (Python, arbitrary-precision int; the spec + target fingerprint).
7
+
8
+ ## New this increment (the MLP sub-block)
9
+
10
+ - **2nd RMSNorm** (`post_attention_layernorm`) — same integer isqrt RMSNorm as attention (scale-invariant).
11
+ - **gate / up projections** — int8 GEMM, W8A16, per-channel scale, requant (no bias, as Qwen2 SwiGLU).
12
+ - **integer SiLU** `act = gate · sigmoid(gate)` — via a **pinned sigmoid LUT** (4096 entries, Q16; sigmoid is
13
+ bounded so a LUT works, unlike unbounded SiLU). Index `= round(gate_true·128)+2048` computed in fixed point.
14
+ - **`hmul = act · up`** — elementwise integer multiply + requant.
15
+ - **down projection** — int8 GEMM → residual add into the attention output.
16
+
17
+ ### Bug found + fixed: scale underflow in the SiLU chain
18
+ `act = gate·sig` makes the values large (~2³¹) and the per-unit scale tiny (`gate_sq · 2⁻¹⁶`), which
19
+ **underflows to 0** in Q30 when `gate_sq < 2¹⁶`, collapsing all downstream scales (`cmax=0`, div-by-zero).
20
+ Fix: **requant `act` to int16 immediately**, computing its scale as `scale_from(amax, gate_sq) >> 16` — the
21
+ large `amax` is multiplied in *before* the `>>16`, so the scale stays healthy (aq recovers a ~2⁻¹⁵ scale). The
22
+ same "requant to keep values in int16 range and let aq recompute the scale" discipline is what keeps every
23
+ stage's Q30 scale representable.
24
+
25
+ ## Result
26
+
27
+ ```
28
+ FULL LAYER: accuracy vs fp = 0.0372 # 3.72%, ~unchanged from attention-only 3.78% (MLP adds ~0)
29
+ layer-out FNV = 906729150a5a926a # Rust/CUDA ports must reproduce
30
+ layer[0,:4] int = [202, 187, 603, -177]
31
+ ```
32
+
33
+ ## Ports — DONE (4-way byte-identical)
34
+
35
+ The full transformer layer is byte-identical `906729150a5a926a` across four implementations (`layer[0..4]=[202,187,603,-177]`):
36
+
37
+ | implementation | hardware | ISA / arch | layer-out FNV |
38
+ |---|---|---|---|
39
+ | Python reference | RTX box | x86_64 | `906729150a5a926a` |
40
+ | Rust CPU | RTX box | x86_64 | `906729150a5a926a` |
41
+ | Rust CPU | Mac | aarch64 | `906729150a5a926a` |
42
+ | CUDA GPU | RTX 4060 Ti | Ada sm_89 | `906729150a5a926a` |
43
+
44
+ Files: `docs/evidence/canon_extract.py` (now emits `post_attention_layernorm` + `mlp.gate/up/down` + sigmoid
45
+ LUT), `canon_attn.rs` / `canon_attn.cu` (added `rmsnorm`/`proj_nb` + the MLP block). CUDA note: `projd` takes a
46
+ nullable bias pointer; the `up()` upload lambda must be split by pointer type (i64* / signed char* / i128*).
47
+
48
+ ## Honest scope / what remains
49
+ - Metal still needs the i128→i64 scale refactor (unchanged from 4b-2b).
50
+ - After the ports: **4b-4** — stack all 24 layers (loop this layer, threading the residual stream) + final
51
+ RMSNorm + tied LM head + argmax → the backend-independent **token** commitment; then **4b-5** (token-match
52
+ vs fp + wire a `canonical` class into the verifier).
docs/evidence/canonical-int-fullmodel-4b4-4b5-2026-07-20.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — inc4b-4 / 4b-5: full 24-layer integer forward + token commitment + fp agreement (2026-07-20)
2
+
3
+ Extends the full layer (4b-3) to the **whole Qwen2.5-0.5B**: 24 layers + final RMSNorm + tied LM head +
4
+ integer argmax, with greedy decode. Fully integer, no fp on the compute path (`docs/evidence/canon_model_int.py`).
5
+
6
+ ## 4b-4 — backend-independent token commitment
7
+
8
+ ```
9
+ prompt = [9707, 11, 1879, 30]
10
+ integer generated = [3555, 1558, 432, 3076, 30, 1084] # greedy, 6 tokens
11
+ TOKEN COMMITMENT (blake2b16, over the generated ids) = 92a5380b0d1be4b126a2464d915243c8 (NDECODE=6)
12
+ b4a092dad65655c7def87f11a6ed0df7 (NDECODE=4)
13
+ ```
14
+
15
+ - **Backend independence** holds **by composition**: the full model is 24 stacked copies of the 4b-3 layer
16
+ (proven byte-identical across Python-x86 / Rust-x86 / Rust-aarch64 / CUDA-Ada) + final RMSNorm + an int8
17
+ LM-head GEMM + integer argmax (all proven byte-identical). Given identical pinned tables (RoPE per position,
18
+ sigmoid/exp LUTs), the whole forward — and thus the token commitment — is identical on every backend.
19
+ - LM-head argmax weights each vocab logit by its per-row int8 scale (fixed-point) before the integer argmax,
20
+ so it is exact and deterministic.
21
+ - A concrete full-model **Rust/CUDA run** would need all 24 layers' weights + the 151936×896 embedding
22
+ (~634 MB flat) extracted and uploaded; deferred. The layer-level 4-way byte-identity (4b-3) already
23
+ establishes the composition.
24
+
25
+ ## 4b-5 — fidelity to fp (token agreement): the honest result
26
+
27
+ ```
28
+ integer generated = [3555, 1558, 432, 3076, 30, 1084]
29
+ fp generated = [3555, 374, 279, 7290, 315, 279]
30
+ token agreement (int vs fp) = 1/6
31
+ ```
32
+
33
+ - **Only the first token matches.** The single-layer W8A16 error (~3.7%, from 4b-2a/4b-3) **compounds across
34
+ 24 layers** and flips the greedy argmax from token 2 onward. So the canonical integer model is a
35
+ **distinct, lower-fidelity model** than the fp reference — deterministic and vendor-independent, but not
36
+ tracking fp's output.
37
+ - This is **not a determinism failure** (the commitment reproduces across backends). It is a **quality**
38
+ finding: per-tensor W8A16 is too coarse for a 24-layer greedy decode. To make the canonical class track fp
39
+ / produce high-quality text, higher-fidelity quantization is needed — per-channel activation handling
40
+ (SmoothQuant), W16A16, or better calibration. This is the standard "naive PTQ degrades deep models" result.
41
+
42
+ ## Status of the canonical class
43
+
44
+ - **Determinism / backend-independence: achieved** (per-op → per-layer → full-model, all byte-identical).
45
+ This is the property the canonical-integer path exists to deliver, and it is demonstrated end-to-end.
46
+ - **Fidelity / usable quality: not yet** — needs a better quantization scheme (above). The class is a
47
+ correct proof-of-concept; its *golden tokens* are its own, distinct from any fp class, and currently
48
+ low-fidelity vs fp.
49
+
50
+ ## Honest scope / what remains
51
+
52
+ - Improve quantization fidelity (SmoothQuant / W16A16 / calibration) so the 24-layer greedy decode tracks fp,
53
+ then re-measure token agreement.
54
+ - Full-model Rust/CUDA run (concrete end-to-end cross-backend token-commitment match) — deferred on weight
55
+ size; backend-independence already established by the layer-level byte-identity.
56
+ - Metal port still needs the i128→i64 scale refactor.
docs/evidence/canonical-int-gpu-3backend-2026-07-19.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — Canonical int8 GEMM is byte-identical across CPU / Apple Metal / NVIDIA CUDA (2026-07-19)
2
+
3
+ Increment 2 of [`docs/canonical-integer-compute-design.md`](../canonical-integer-compute-design.md). Extends
4
+ the CPU-only cross-ISA result ([canonical-int-cross-isa](canonical-int-cross-isa-2026-07-19.md)) onto **real
5
+ GPUs of two different vendors**. The same `int8×int8→int32` GEMM (same splitmix64 inputs, `M,K,N=32,8192,32`,
6
+ values `0..127`) is computed on each backend and fingerprinted identically (int32 little-endian → FNV-1a 64).
7
+
8
+ ## Artifacts (all in `docs/evidence/`)
9
+
10
+ | backend | source | build |
11
+ |---|---|---|
12
+ | CPU (reference) | `canon_int.rs` | `rustc -O canon_int.rs` |
13
+ | Apple Metal GPU | `canon_metal.swift` | `swiftc -O canon_metal.swift` |
14
+ | NVIDIA CUDA GPU | `canon_cuda.cu` | `nvcc -O3 -arch=sm_89 canon_cuda.cu` |
15
+
16
+ ## Results — one fingerprint, four backends
17
+
18
+ | backend | hardware | ISA / arch | INT fnv |
19
+ |---|---|---|---|
20
+ | CPU | Apple M1 Max | aarch64 | `4b138ed70d137781` |
21
+ | CPU | NVIDIA/WSL2 host | x86_64 | `4b138ed70d137781` |
22
+ | Apple Metal GPU | Apple M1 Max | Apple GPU | `4b138ed70d137781` |
23
+ | NVIDIA CUDA GPU | RTX 4060 Ti | Ada `sm_89` | `4b138ed70d137781` |
24
+
25
+ Raw GPU runs:
26
+
27
+ ```
28
+ # Apple Metal (Mac)
29
+ backend = Apple Metal GPU (Apple M1 Max)
30
+ INT gpu fnv = 4b138ed70d137781
31
+ MATCH = ✅ byte-identical to CPU
32
+
33
+ # NVIDIA CUDA (RTX box, WSL2, nvcc 12.6, -arch=sm_89)
34
+ backend = NVIDIA CUDA GPU (NVIDIA GeForce RTX 4060 Ti, sm_89)
35
+ INT gpu fnv = 4b138ed70d137781
36
+ MATCH = OK byte-identical
37
+ ```
38
+
39
+ ## Reading
40
+
41
+ - The canonical integer GEMM is byte-identical across **two CPU ISAs and two GPU vendors/microarchitectures**
42
+ — Apple GPU ≡ NVIDIA Ada ≡ x86 CPU ≡ ARM CPU. This is the cross-vendor invariance that the token
43
+ `vector_commitment` needs in order for different hardware to share a determinism/pool class.
44
+ - Contrast with the fp reality measured on the same hardware: the fp32 verifier's token commitment CHANGED
45
+ merely from a candle 0.9.2→0.11 bump on CUDA (Ada), and Apple vs NVIDIA already diverge. Integer removes
46
+ that divergence by construction.
47
+
48
+ ## Honest scope / what remains
49
+
50
+ - These kernels use one thread per output with a sequential `k`-loop; order-independence of parallel
51
+ reductions is proven separately on CPU (seq vs tree, [cross-isa evidence](canonical-int-cross-isa-2026-07-19.md))
52
+ and follows from `int32`-ring associativity. A tensor-core / split-K variant would exercise parallel GPU
53
+ reduction directly — a good hardening follow-up.
54
+ - **Turing (`sm_75`, RTX 20/16) is still unrun here** (no Turing GPU). By the theorem it must also produce
55
+ `4b138ed70d137781`; confirming it on a real RTX 2060 is the user HW step that lets RTX 20/16 join the Ada
56
+ class (design doc increment 6).
57
+ - This is the GEMM **primitive**. A full model still needs the canonical requant / LUT nonlinearities /
58
+ softmax / argmax of design §3 (increments 3–5) before a whole-model token commitment is backend-independent.
docs/evidence/canonical-int-hifi-fullmodel-2026-07-20.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — hifi canonical integer model: fp と 12/12 一致 + full-model 4 実装一致 (2026-07-20)
2
+
3
+ 前回の正直な課題(4b-5: token 一致 1/6)を**解決**し、full-model の Rust/CUDA 具現ランまで完了した記録。
4
+
5
+ ## 1. 忠実度修正 — 1/6 → **12/12**
6
+
7
+ 原因は量子化方式ではなく**粒度と分解能**だった。hifi 構成(`docs/evidence/canon_model_hifi.py`):
8
+
9
+ | 変更 | before | after |
10
+ |---|---|---|
11
+ | 活性ビット幅 | A16(ALVL=32767) | **A24**(ALVL=2²³−1) |
12
+ | スケール固定小数点 | Q30(小スケール積が underflow) | **Q60**(underflow 消滅) |
13
+ | exp LUT | 256 エントリ・1/64 刻み | **2048・1/512 刻み** |
14
+ | sigmoid LUT | 4096・1/128 刻み | **16384・1/512 刻み** |
15
+
16
+ 結果(実 bf16→W8 量子化重み、prompt `[9707,11,1879,30]`、greedy):
17
+
18
+ ```
19
+ integer = [3555, 374, 279, 7290, 315, 279, 17133, 330, 9707, 11, 1879, 7521]
20
+ fp = [3555, 374, 279, 7290, 315, 279, 17133, 330, 9707, 11, 1879, 7521]
21
+ token agreement = 12/12
22
+ TOKEN COMMITMENT (blake2b16, 12 tok) = 2e911f7973bbaedfb4ff653899f673f0 (6 tok: 7f58ee97d9f6fdea0bb2e73c874a30c8)
23
+ ```
24
+
25
+ **per-tensor W8 のままで fp を追跡できた** — SmoothQuant/W16 は不要だった(この prompt 長では)。
26
+ 教訓 2 件: (a) Q30 は小スケール積で 0 に潰れる(4b-2a の RMSNorm バグと同族の**表現力問題**)— Q60 か
27
+ 正規化 (m0,shift) が必須。(b) numpy int64 は A24 の中間積で**サイレント wrap**する — 参照は object/int
28
+ の exact 経路に(ports は i128 で元々安全)。
29
+
30
+ ## 2. full-model 具現ラン — 4 実装・実ハードで同一 token
31
+
32
+ 抽出器 `canon_extract_full.py` が qwen05.canon → `model.bin`(498,080,914 bytes、blake2b16
33
+ `ead451ef79e88a2db5518f3d371722e9`、24 層 + embed + final norm + RoPE/EXP/SIG テーブル)を生成。
34
+ Rust port `canon_model.rs`(i128 スケール)と CUDA run `canon_model.cu`(**全 GEMM = 7×24 + LM head を
35
+ GPU がオンデバイス実行**、整数グルーは host __int128 — 全段整数なので決定性は同一)を実行:
36
+
37
+ | 実装 | hardware | generated (6 tok) | ids FNV | time |
38
+ |---|---|---|---|---|
39
+ | Python 参照 | x86_64 | `[3555, 374, 279, 7290, 315, 279]` | — (commitment 7f58ee97…) | ~45s |
40
+ | Rust CPU | x86_64 | 同上 | `65d2e3adaf2dce9e` | 9.5s |
41
+ | Rust CPU | **aarch64 (Mac)** | 同上 | `65d2e3adaf2dce9e` | 7.0s |
42
+ | **CUDA** | **RTX 4060 Ti (Ada)** | 同上 | `65d2e3adaf2dce9e` | **1.4s** |
43
+
44
+ - model.bin は Mac へバイト検証付き転送(blake2b16 一致)— 全実装が**同一成果物**を消費。
45
+ - 生成 token 列は **fp と 6/6 一致する系列そのもの** — つまり canonical integer class は「決定的・
46
+ vendor 非依存・かつ fp 品質を追跡」の 3 点を同時に満たした。
47
+
48
+ ## 3. 正直な限界
49
+
50
+ - token 一致は 1 prompt × 12 token(+6 token 構成でも一致)での確認。golden 化前に複数 prompt・長系列
51
+ での一致率測定(canary 的サンプリング)を推奨。低マージン手ではいずれ分岐し得る(それは class の
52
+ 定義上は問題ではないが、fp 品質の同等性主張には影響)。
53
+ - CUDA ランは「GPU が全 GEMM を実行 + host が整数グルー」構成。全 stage を単一 GPU カーネル化する
54
+ full-device 版は未実施(決定性の主張には影響なし — 全段整数)。
55
+ - Apple **GPU**(Metal)は未実施のまま(i128→i64 正規化 refactor が前提、設計は design doc §7 参照)。
56
+ Apple **CPU**(aarch64)は本記録で full-model 一致済み。
57
+ - Python 参照の RoPE/SIG テーブル生成はオフライン fp(テーブル自体は pin されるので runtime 決定性に
58
+ 影響なし — 生成一致は extractor が単一ソースであることで担保)。
docs/evidence/canonical-int-layer-4backend-2026-07-19.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — Canonical integer LAYER is byte-identical across CPU / Metal / CUDA (2026-07-19)
2
+
3
+ Increment 3 of [`docs/canonical-integer-compute-design.md`](../canonical-integer-compute-design.md). Extends
4
+ the GEMM primitive ([increment 2](canonical-int-gpu-3backend-2026-07-19.md)) to a **full quantized layer**
5
+ that includes the two stages where floating point normally sneaks back in — **requantization** and the
6
+ **nonlinearity** — and shows the whole layer is still byte-identical across four backends.
7
+
8
+ ## The layer (no fp on the compute path)
9
+
10
+ For each output `(i,j)`:
11
+ 1. `acc = Σ_t X[i,t]·W[t,j]` — int8×int8 → **int32** GEMM.
12
+ 2. `pre = acc + bias[j]` — int32 bias (per output channel).
13
+ 3. **fixed-point requant** `q = clamp((pre·M0[j] + 2^(shift-1)) >> shift, -127, 127)` — integer mul (int64)
14
+ + shift, gemmlowp/TFLite style. **No fp divide.** (`M0[j]` per-channel multiplier, `shift=20`.)
15
+ 4. **pinned integer LUT** `out = LUT[q + 128]` — a 256-entry int8→int8 table for a fast-sigmoid SiLU,
16
+ generated by integer-only arithmetic (truncating division, identical in Rust/CUDA/Metal). **No on-device
17
+ `expf`/`sigmoid` fp.**
18
+
19
+ `M,K,N = 16,2048,16`; inputs and per-channel params from fixed splitmix64 seeds. Fingerprint = FNV-1a 64 over
20
+ the int8 output (and separately over the LUT, to confirm the pinned table is identical everywhere).
21
+
22
+ ## Artifacts (`docs/evidence/`)
23
+
24
+ | backend | source | build |
25
+ |---|---|---|
26
+ | CPU (reference) | `canon_layer.rs` | `rustc -O canon_layer.rs` |
27
+ | Apple Metal GPU | `canon_layer_metal.swift` | `swiftc -O canon_layer_metal.swift` |
28
+ | NVIDIA CUDA GPU | `canon_layer_cuda.cu` | `nvcc -O3 -arch=sm_89 canon_layer_cuda.cu` |
29
+
30
+ ## Results
31
+
32
+ | backend | hardware | ISA / arch | LUT fnv | LAYER fnv |
33
+ |---|---|---|---|---|
34
+ | CPU | Mac | aarch64 | `8ab2052fcf2b005a` | `440bd1b93d991fe0` |
35
+ | CPU | RTX host (WSL2) | x86_64 | `8ab2052fcf2b005a` | `440bd1b93d991fe0` |
36
+ | Apple Metal GPU | M1 Max | Apple GPU | `8ab2052fcf2b005a` | `440bd1b93d991fe0` |
37
+ | NVIDIA CUDA GPU | RTX 4060 Ti | Ada `sm_89` | `8ab2052fcf2b005a` | `440bd1b93d991fe0` |
38
+
39
+ Post-requant `q` spans the full `[-127, 127]` (mean ~85) — the output is non-degenerate, so the LUT and
40
+ requant are genuinely exercised, not collapsed to a constant.
41
+
42
+ ## Reading
43
+
44
+ - A quantized layer **including requant and a nonlinearity** is byte-identical across two CPU ISAs and two
45
+ GPU vendors. The two classic fp-reinjection points are closed: requant is integer mul+shift (not fp
46
+ divide), and the nonlinearity is a pinned integer table lookup (not an on-device transcendental).
47
+ - Combined with increment 2 (GEMM), this is every op type a transformer FFN block needs, all backend-
48
+ independent. The remaining model-level work (increment 4) is composing these into the full dense
49
+ Qwen2.5-0.5B forward (attention softmax, RMSNorm rsqrt, embedding) under the same integer discipline.
50
+
51
+ ## Honest scope
52
+
53
+ - Kernels are one-thread-per-output with a sequential `k`-loop; parallel-reduction order-independence is
54
+ covered by the CPU seq-vs-tree proof + int32 associativity (increment 1). A tensor-core / split-K variant
55
+ is a hardening follow-up.
56
+ - The LUT here is a demonstrator fast-sigmoid SiLU, defined by integer arithmetic so the table is identical
57
+ on every host without baking bytes. A production spec would pin the exact table (and the exact requant
58
+ multipliers/shifts) as part of the model artifact.
59
+ - No Turing (`sm_75`) run here; by the theorem it must also yield `440bd1b93d991fe0` (user HW step).
docs/evidence/canonical-int-metal-norm-2026-07-20.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — Metal GPU 参加: (m0,e)+mulhi norm プロファイルで 3 実装ビット一致 (2026-07-20)
2
+
3
+ design doc §8 の i128→i64 refactor を実装・実証した記録。MSL には 128-bit 整数が無いため、
4
+ **全スケールを正規化 (m0∈[2^30,2^31), e) 表現**にし、値×スケールは **32-bit limb の 2-word 乗算 +
5
+ 2-word シフト**(= MSL がネイティブに書ける演算のみ)で行う「norm プロファイル」を定義した。
6
+
7
+ ## 実装
8
+
9
+ - `canon_attn_norm.rs` — Rust 参照(i64 演算 + limb 乗算のみ。i128 不使用 — MSL へ**逐語訳可能**)。
10
+ sc_mul: `m0a·m0b < 2^62`(i64 内)→ `>>31`; sc_ratio: `(m0a<<31)/m0b`; apply_sc: mul64_wide + 2-word
11
+ shift、round-half-away; aq: `(2·ax·ALVL+am)/(2am)`。
12
+ - `canon_attn_norm.swift` — Swift host(layer0.bin パース + gemmlowp→(m0,e) 変換)+ 単一スレッド MSL
13
+ カーネル(全層: RMSNorm→QKV+bias→RoPE→scores→整数 softmax→A·V→o_proj→residual→RMSNorm→SwiGLU→residual)。
14
+
15
+ 丸め位置が Q30/Q60 プロファイルと異なるため、このプロファイルは**独自のリファレンス値**を持つ
16
+ (クレームはクロスバックエンド同一性)。
17
+
18
+ ## 結果 — 3 実装ビット一致
19
+
20
+ | 実装 | hardware | layer FNV | layer[0..4] |
21
+ |---|---|---|---|
22
+ | Rust CPU | x86_64 (RTX box) | `50a8caf4107f43f4` | [265, 210, 581, -123] |
23
+ | Rust CPU | aarch64 (Mac) | `50a8caf4107f43f4` | 同一 |
24
+ | **Apple Metal GPU** | **M1 Max** | `50a8caf4107f43f4` | 同一 |
25
+
26
+ → **Apple GPU が整数 canonical プールに参加**(初回コンパイルで一致)。既プロファイルと合わせ、
27
+ 実重み transformer 層のビット一致実証は CPU×2 ISA + NVIDIA GPU(Q30/Q60)+ **Apple GPU(norm)** に到達。
28
+
29
+ ## 正直な範囲
30
+
31
+ - norm プロファイルは Q30 プロファイル(`906729150a5a926a`)と**値が異なる**(丸め差、出力は近傍)。
32
+ プール統合には全 backend を norm プロファイルに揃えるのが本線(CUDA の norm 移植は limb コードの
33
+ 逐語コピーで機械的 — 未実施)。
34
+ - 単一スレッドカーネル(S=4)。並列化は integer 結合律で安全(inc1 実証)だが perf 作業は別。
35
+ - full-model の norm 版・A24/Q60 相当の hifi 化(sc の 2^e 精度は同等以上なので適用可能)は次段。
docs/evidence/canonical-int-ops-4backend-2026-07-19.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — Canonical integer softmax / RMSNorm / argmax across CPU / Metal / CUDA (2026-07-19)
2
+
3
+ Increment 4a of [`docs/canonical-integer-compute-design.md`](../canonical-integer-compute-design.md). Covers
4
+ the **remaining transcendental-bearing transformer ops** — attention **softmax** (needs `exp`) and
5
+ **RMSNorm** (needs `rsqrt`) — plus **argmax**, all in integer arithmetic with **no floating point**, and
6
+ shows them byte-identical across four backends. With increments 2 (GEMM) and 3 (bias/requant/LUT), this
7
+ completes backend-independence for **every op type a dense transformer forward uses**.
8
+
9
+ ## The ops (no fp on the compute path)
10
+
11
+ - **softmax** over int32 scores: subtract max (int); `exp(-d)` via a **pinned fixed-point `exp` table**
12
+ (Q16, generated by integer Horner — 12 terms — so the table is identical on every host, no libm `exp`);
13
+ sum in `uint64`; normalize `p_i = (e_i << 16) / sum` by integer division. No fp.
14
+ - **RMSNorm** over int32: `Σx²` in `int64`; `mean`; **integer `rsqrt`** via `isqrt((1<<32)/mean)` where
15
+ `isqrt` is integer Newton (no fp, no fast-inverse-sqrt bit trick); scale by per-channel γ with a fixed
16
+ shift. No fp.
17
+ - **argmax**: integer compare, deterministic first-max tie-break.
18
+
19
+ `L=1024` (softmax/argmax), `H=896` (RMSNorm, Qwen-0.5B hidden). Inputs / γ from fixed splitmix64 seeds.
20
+ Fingerprints = FNV-1a 64 over the Q16 probabilities, the int32 RMSNorm output, the `exp` table, and a
21
+ combined digest of (softmax ‖ rmsnorm ‖ argmax).
22
+
23
+ ## Artifacts (`docs/evidence/`)
24
+
25
+ `canon_ops.rs` (CPU ref), `canon_ops_metal.swift` (Metal shaders), `canon_ops_cuda.cu` (CUDA). GPU builds
26
+ run softmax and RMSNorm on-device (single-thread kernels using `ulong`/`long` for the reductions and
27
+ `isqrt`); the host computes argmax and the FNV folds.
28
+
29
+ ## Results
30
+
31
+ | backend | hardware | ISA / arch | EXP_LUT fnv | COMBINED fnv |
32
+ |---|---|---|---|---|
33
+ | CPU | Mac | aarch64 | `c4ddbbc186fe4e82` | `4bf581ead4d57139` |
34
+ | CPU | RTX host (WSL2) | x86_64 | `c4ddbbc186fe4e82` | `4bf581ead4d57139` |
35
+ | Apple Metal GPU | M1 Max | Apple GPU | `c4ddbbc186fe4e82` | `4bf581ead4d57139` |
36
+ | NVIDIA CUDA GPU | RTX 4060 Ti | Ada `sm_89` | `c4ddbbc186fe4e82` | `4bf581ead4d57139` |
37
+
38
+ Per-op (identical on all four): softmax `4344a6ed2626237e`, rmsnorm `4465b751f2412255`, argmax index `495`.
39
+ RMSNorm `inv_q16 = 113` (correctly ≈ `2^16/√mean`); softmax probabilities are non-uniform — the ops are
40
+ genuinely exercised.
41
+
42
+ ## Reading
43
+
44
+ - The two ops that would normally re-introduce fp via a transcendental (`exp` in softmax, `rsqrt` in
45
+ RMSNorm) are backend-independent when done with a **pinned integer table** (`exp`) and **integer Newton**
46
+ (`rsqrt`). Metal's `long`/`ulong` and CUDA's `long long` both give the identical integer result as the CPU.
47
+ - Op coverage is now complete: GEMM (inc2) + bias/requant/LUT (inc3) + softmax/RMSNorm/argmax (inc4a) =
48
+ every primitive a dense-transformer forward needs, each proven byte-identical on CPU×2 ISAs + 2 GPU
49
+ vendors.
50
+
51
+ ## Honest scope / what remains (increment 4b)
52
+
53
+ - These are the **op primitives**, not a whole model. Increment 4b composes them into the full dense
54
+ Qwen2.5-0.5B forward with the **real quantized weights** (24 layers, RoPE, GQA attention, SwiGLU, tied
55
+ LM head) and validates that (a) it produces sensible tokens and (b) the token `vector_commitment` is
56
+ identical across CPU/Metal/CUDA. That integration — plus choosing a quantization that preserves output
57
+ quality — is a substantially larger effort than these primitives.
58
+ - Single-thread reductions here; parallel-reduction/tensor-core variants are a hardening follow-up (integer
59
+ associativity already covers them — increment 1).
60
+ - The `exp` table and `rsqrt` here are demonstrator fixed-point approximations; a production spec pins the
61
+ exact tables / iteration counts as part of the model artifact.
62
+ - No Turing (`sm_75`) run; by the theorem it must also yield `4bf581ead4d57139` (user HW step).
docs/evidence/canonical-int-quantizer-2026-07-19.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — inc4b-1 offline canonical quantizer core (2026-07-19)
2
+
3
+ First cut of the offline quantizer for [inc4b](../canonical-integer-inc4b-fullmodel-design.md) §7 (4b-1).
4
+ Produces the per-tensor int8 weights + **pinned fixed-point per-output-channel scale** that the integer
5
+ runtime consumes. Floating point is used **offline only** (this tool); the emitted `.canon` bytes are the
6
+ canonical artifact every backend consumes identically.
7
+
8
+ ## What it does (`docs/evidence/canon_quant.rs`)
9
+
10
+ - **Per-output-channel symmetric int8 quantization**, round-half-away-from-zero (deterministic).
11
+ - Each channel's dequant scale `amax/127` is pinned as a **gemmlowp-style fixed point** `(M0: i32, shift: i32)`
12
+ with `scale ≈ M0·2^(shift-31)`, `M0 ∈ [2^30, 2^31)`.
13
+ - Minimal `.canon` v0 container (`magic ‖ out_ch ‖ in_ch ‖ int8 weights ‖ per-channel (M0,shift)`), with a
14
+ write/read round-trip.
15
+ - Self-test on a synthetic `[896, 896]` (Qwen-0.5B proj-sized) weight matrix — no model download needed.
16
+
17
+ ## Results (identical on aarch64 Mac and x86_64 RTX host)
18
+
19
+ ```
20
+ tensor = [896, 896] int8, per-channel fixed-point scale
21
+ round-trip (dequant): max_abs_err=3.933e-4 rms_rel_err=0.0054
22
+ fixed-point scale rel err (max) = 3.06e-10
23
+ .canon bytes = 810000 fnv=a9be59709b4253fe
24
+ .canon round-trip = OK (weights+scales identical)
25
+ sample ch0: M0=1686124657 shift=-10 scale=7.667607e-4
26
+ ```
27
+
28
+ ## Reading
29
+
30
+ - int8 per-channel quantization round-trips at **0.54% RMS** (normal for symmetric int8) — a quality figure,
31
+ separate from determinism.
32
+ - The **fixed-point scale reconstructs the true `amax/127` to 3e-10 relative** — the 31-bit `M0` pins the
33
+ scale essentially exactly, so no fp scale is needed at runtime.
34
+ - The `.canon` artifact is **byte-identical across aarch64 and x86_64** (`a9be59709b4253fe`) — even the
35
+ offline fp quantization reproduced across ISAs here, so artifact builds are reproducible (the artifact is
36
+ still meant to be published once and hash-pinned regardless).
37
+
38
+ ## Real-model run (full quantizer, `docs/evidence/canon_quant_full.py`)
39
+
40
+ With user permission, the **bf16 original** `model.safetensors` was downloaded from
41
+ `huggingface.co/Qwen/Qwen2.5-0.5B-Instruct` (988,097,824 bytes, sha256 `fdf756fa…288fb7fe`) onto the RTX
42
+ box, and the full quantizer (safetensors parser + per-channel int8 + fixed-point scale, numpy) produced the
43
+ complete `.canon` v1 artifact:
44
+
45
+ ```
46
+ tensors: 290 # token_embd + 24×(2 norms + q/k/v/o + q/k/v bias + gate/up/down) + final norm; LM head tied
47
+ params: 494,032,768
48
+ .canon: 497,937,154 bytes blake2b16=bdb64817060deaf1898ef8918a4e181a
49
+ 169 2D tensors int8 per-channel; 121 1D tensors (norms/biases) as fixed-point int32
50
+ worst per-tensor rms (2D): 0.0149 model.layers.16.mlp.down_proj (down_proj / v_proj are the known-hard ones)
51
+ ```
52
+
53
+ - **Artifact is deterministic**: a byte-identical re-run reproduces `blake2b16=bdb64817…`.
54
+ - Tensor accounting matches the confirmed arch exactly (290 = 1 + 24·12 + 1), so the parse is complete.
55
+ - Worst per-tensor quantization RMS ~1.5% (down_proj/v_proj); most tensors far lower. That is a **quality**
56
+ figure (per-tensor symmetric int8); if 4b-5 shows token-quality loss, per-group scales fix these outliers.
57
+ - The 498 MB `.canon` lives on the RTX box (`/root/models/qwen05.canon`); it is a build output, hash-pinned
58
+ by `bdb64817…`, not committed to the repo.
59
+
60
+ ## Honest scope / what remains
61
+
62
+ - inc4b-1 is **done**: a deterministic W8A8 `.canon` artifact from the real bf16 weights. Next (4b-2) the
63
+ integer runtime consumes this artifact for an attention block; RoPE fixed-point cos/sin tables + SiLU/exp
64
+ LUTs (integer generators proven in inc3/inc4a) are generated there (they are model-independent constants).
65
+ - Activation quantization (dynamic per-token int8 with fixed-point scale) and the exact bias/norm consumption
66
+ are runtime concerns (4b-2+), not part of this offline tool.
67
+ - The offline fp quantizer reproduced its artifact across aarch64/x86_64 on the *synthetic* self-test; the
68
+ real-model artifact hash `bdb64817…` is the pinned identity regardless (publish-once).
docs/evidence/canonical-int-qwen36-bench-2026-07-20.md ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — QI35-9: 性能測定 — 純粋 Qwen3.6 vs receipt システム (2026-07-20)
2
+
3
+ 同一 Mac Studio(M1 Max 32GB)・同一本番 blob(23.9GB Q4_K_M)・同一 prompt
4
+ "The capital of France is"(5 tokens)+ 32 token 生成で、fp/整数 × receipt 有無の 4 構成を実測。
5
+
6
+ | # | 構成 | 生成速度 | 応答全体の wall | 内訳 |
7
+ |---|---|---|---|---|
8
+ | 1 | **fp Metal 素**(llama-bench, build-palw)| **decode 52.0 ± 0.5 tok/s**(tg32)、prefill 105.6 tok/s(pp5)| —(生成のみ)| GPU 実行 |
9
+ | 2 | **fp Metal + receipt**(palw-metal-receipt, n=32)| 生成自体は #1 相当 | **242s** | manifest(SHA-256 24GB ×前後 2 回)+ observer(Metal kernel trace)+ 署名/bundle/state-db が支配 |
10
+ | 3 | **整数 CPU 素**(qi35_model, 8 threads)| **1.66 tok/s**(37 forward / 22.4s ≈ 0.61s/token)| 22.4s | CPU のみ(Metal 版は未実装 = QI35-5)|
11
+ | 4 | **整数 CPU + receipt**(palw-integer-receipt, n=32)| エンジン 24.3s | **50s** | + blake2b256 24GB ハッシュ ~26s、ML-DSA-87 署名+bundle+state-db は合計 <1s |
12
+
13
+ ## 読み取り(正直に)
14
+
15
+ - **生成エンジン単体**: fp Metal が整数 CPU の **~31×** 速い。整数側は GPU 未移植(QI35-5)のためで、
16
+ 整数の原理的限界ではない(演算量は同じ A3B)。
17
+ - **receipt システムのオーバーヘッド**:
18
+ - fp 版: +~240s/発行。SHA-256 二重ハッシュ(実行前後の manifest 一致確認)と kernel trace が本体。
19
+ - 整数版: +~28s/発行で、**ほぼ全てが blake2b 24GB ハッシュ**。署名(ML-DSA-87)・XChaCha bundle・
20
+ sqlite 書き込みはミリ秒〜1s 未満。ハッシュはファイル不変なら**キャッシュ可能**(mtime+size キーで
21
+ ~2s 台まで短縮できる — 未実装の最適化)。
22
+ - **発行込みの応答時間**では、整数フロー(50s)が fp フロー(242s)の **~4.8× 速い**。整数 class の
23
+ evidence が「再実行で完全再現」という性質そのもので、実行時の trace 計装が不要なため。
24
+ - チャット常駐(qi35_chat.sh)ではモデルハッシュはセッション 1 回なので、2 問目以降の receipt 付き
25
+ 応答は「エンジン時間 + <1s」になる。
26
+
27
+ ## 測定条件の注記
28
+
29
+ - fp 素は llama-bench(r=2)。llama-cli(build-palw)は `-no-cnv` 指定でも対話ループに入る挙動があり
30
+ 計測に不適(2 回ログ暴走)— ベンチは llama-bench を正とする。
31
+ - #2/#4 の wall はコマンド起動〜終了(モデルロード込み)。page cache は直前ランで温まった状態。
32
+ - RTX box(WSL2 x86_64, RAM 21GB)の整数エンジンは同一ジョブ 8 token で 24.6s(Mac 10.0s)—
33
+ 24GB blob が RAM を超え mmap ページングになるため。機能・commitment は完全一致
34
+ ([v3-pair evidence](canonical-int-qwen36-v3-pair-2026-07-20.md))。
docs/evidence/canonical-int-qwen36-chat-receipt-2026-07-20.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — QI35-6/7: 整数エンジンの対話化 + canonical 整数 receipt 自家発行 (2026-07-20)
2
+
3
+ 本番 Qwen3.6-35B の **canonical 整数エンジン**を(1)Claude 風の対話 UI にし、(2)各応答で
4
+ **backend 非依存の canonical_integer_v0 receipt を自家発行**、(3)独立再実行で検証できるようにした記録。
5
+
6
+ ## 構成
7
+
8
+ - **エンジン** `qi35_model`(docs/evidence/qi35_model.rs、依存ゼロ Rust): `--serve` 常駐モードを追加。
9
+ stdin から prompt token id 列を受け、状態(KV/DeltaNet/conv)をリセット→整数 greedy 生成→
10
+ `GEN <ids>` / `RCPT <json>` / `DONE` を stdout に出す。EOS(`<|im_end|>`=248046)で停止。
11
+ 純 Rust **BLAKE2b**(keyed/unkeyed)を実装 — Python `hashlib.blake2b` と**バイト一致を検証**
12
+ (unkeyed `8ae72774…`、keyed も一致)。
13
+ - **チャット** `qi35_chat.py` + `qi35_chat.sh`: tokenizer.json(base-meta 同梱)+ Qwen3.6 chat
14
+ テンプレート(`<|im_start|>role\n…<|im_end|>`)で文↔token 変換、履歴付き REPL。エンジンを
15
+ serve 子プロセスとして起動し、応答ごとに receipt + opening を保存。
16
+ - **検証** `qi35_verify.py`: receipt + opening(prompt/output ids)を受け、MAC・prompt_commitment・
17
+ output_commitment を再計算し、`--rerun` で**整数エンジンを別途再実行して output_commitment を
18
+ 独立再現**。
19
+
20
+ ## receipt スキーマ(`palw.canonical-integer-receipt/v0`)
21
+
22
+ keyed-BLAKE2b-256 MAC(監査鍵)で以下を束縛。**秘密(鍵・生 prompt)は receipt に一切含めない**:
23
+ - `network_id`(= blake2b(dom‖label))、`timestamp_millis`、`backend`(rust-cpu-aarch64 等)
24
+ - `model_blake2b256`(本番 GGUF 全体 = `fabdc6dc…5326c`)/ `engine_blake2b256` / `tables_blake2b256`
25
+ - `ruleset_id=qi35-int-v1`、`prompt_tokens`/`output_tokens`/`eos_reached`
26
+ - `prompt_commitment` = blake2b(dom‖network‖prompt_ids)
27
+ - **`output_commitment` = blake2b(dom‖network‖prompt_commitment‖output_ids)** ← **backend 非依存の核心**
28
+ - `signer_key_id` = keyed_blake2b(key, dom)(鍵を露出せず署名主体を識別)、`mac`
29
+
30
+ ## 実機ラン(Mac Studio M1 Max、CPU)
31
+
32
+ 一問一答(`qi35_chat.sh "What is the capital of France? Answer in one sentence."`):
33
+
34
+ ```
35
+ [engine] hashing model (blake2b, ~once) model_blake2b256=fabdc6dc…5326c (29.2s)
36
+ Q: What is the capital of France? Answer in one sentence.
37
+ A: <think> The user is asking for the capital of France, which is Paris… </think>
38
+ The capital of France is Paris.
39
+ [receipt canonical_integer_v0 out_commit=328fe5c4d123f558… MAC=OK 46.92s 35tok]
40
+ ```
41
+
42
+ - **正答・流暢**。Qwen3.6 の `<think>` 推論ブロックも整数経路で正しく生成(必要なら system prompt で抑制可)。
43
+ - receipt 自家発行 + MAC 検証 OK。model ハッシュはセッション 1 回(29s、以後は常駐で無料)。
44
+ - 応答内で **integer engine が生成 = fp Metal observer は不使用**。fp receipt(trace_evidence=metal_kernel)
45
+ との本質的な違い: fp receipt は Metal カーネル launch 列に束縛され他 HW で再現不能。本 receipt の
46
+ `output_commitment` は**整数決定性により任意 HW で再現可能** → `qi35_verify.py --rerun` が独立に一致確認できる。
47
+
48
+ ## 検証の実機確認 — 独立再実行で output_commitment 再現
49
+
50
+ 同一 (model, prompt=`[760,6511,314,9338,369]`) を**独立に 2 回**、しかも**スレッド数を変えて**
51
+ (run A: 8 threads / run B: 4 threads)整数エンジンで走らせ、receipt の `output_commitment` を比較:
52
+
53
+ ```
54
+ A(8t) = d11d99254cf145b1a6f1a00faef4c09989772ef9240ee601584348754a65d0e6
55
+ B(4t) = d11d99254cf145b1a6f1a00faef4c09989772ef9240ee601584348754a65d0e6
56
+ COMMITMENT_MATCH ✅
57
+ ```
58
+
59
+ 並列度に依らず commitment がビット一致 = **検証者が別ラン(別 HW でも)で receipt を独立再現できる**
60
+ ことの直接証拠。`qi35_verify.py --rerun` はこの再現一致 + MAC + prompt/output commitment を自動判定する。
61
+ (prompt_commitment/output_commitment/MAC は Python `hashlib.blake2b` 側の独立実装でも一致を確認。)
62
+
63
+ ## backend 非依存性の担保(既存 evidence の再掲)
64
+
65
+ 整数 forward は結合律で reduction 順序・並列度・ISA 非依存(G2/G3 で実証: threads=4/8 で FNV 一致、
66
+ CPU x86/aarch64 一致)。したがって `output_commitment` は「同じ (model, prompt) を同じ canonical
67
+ エンジンで走らせれば、Mac/x86/CUDA のどれでも同一」。これが fp receipt に対する本 class の優位点。
68
+
69
+ ## 正直な限界
70
+
71
+ - 本 receipt は**独立した canonical class**であり、既存 `palw-metal-receipt` の ML-DSA-87 署名 +
72
+ 暗号化 bundle + state-db + network anchor には**まだ統合していない**(MAC は監査鍵ベースの
73
+ keyed-BLAKE2b で、ネットワーク束縛や worker credential bond はない)。mint 適格性は fp 版同様 false。
74
+ - 会話は毎ターン全文脈を prefill し直す(状態リセット)方式。context が伸びると 1 ��ーンが伸びる
75
+ (CPU で ~0.85s/token)。KV 持ち越し高速化と Metal offload(QI35-5)は未実施。
76
+ - `<think>` により token 数が増える。concise 運用は system prompt(`/no_think` 等)で調整。
docs/evidence/canonical-int-qwen36-fullmodel-g3-2026-07-20.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — QI35 G3(部分): Qwen3.6-35B full-model 整数 forward 実機ラン (2026-07-20)
2
+
3
+ [QI35 設計](../canonical-integer-qwen36-design.md) ゲート G3 の初回実測。エンジンは
4
+ `qi35_model.rs`(本ディレクトリ、依存ゼロ: `rustc -O --edition 2021`、mmap 直読 + std::thread 並列)。
5
+ 重みは**本番 blob そのもの**(変換なし)、LUT/RoPE は pinned 成果物
6
+ `qi35_tables.bin`(4,849,688 bytes、blake2b16 `b0a8e021b35aaceec3b08eeb31064cd2`、
7
+ 生成器 `qi35_export_tables.py`)。
8
+
9
+ ## 実行(Mac Studio M1 Max 32GB、CPU のみ)
10
+
11
+ 40 層(DeltaNet 30 + attention 10)+ MoE top-8 + shared + LM head(Q6_K 248,320 行)+
12
+ 整数 argmax。KV cache / DeltaNet 状態 / conv 状態を持ち越す逐次 decode。
13
+
14
+ | prompt (tokens) | fp CPU greedy (llama.cpp) | 整数 canonical | 一致 |
15
+ |---|---|---|---|
16
+ | "Hello, world" `[9419,11,1814]` | `[0,198,40,2688,264,491,1156,314]`("!\nI'm a new user of")| `[0,198,40,2688,264,3061,22839,13]`("!\nI'm a software engineer.")| **先頭 5/8**、6 個目("a␣new" vs "a␣software")の低マージン手で分岐 |
17
+ | "The capital of France is" `[760,6511,314,9338,369]` | `[11751,11,264,3177,34756,364,1141,25438]` | 同一列(**" Paris, a city renowned for its iconic"**)| **8/8 完全一致** |
18
+
19
+ 合計(初回測定): **13/16 一致**、分岐は 1 箇所の低マージン手のみ。
20
+
21
+ - **決定性**: 同 prompt を threads=4 / threads=8 で実行 → **ids FNV 完全一致**(`ff03bf7c9a5d7593`)。
22
+ 整数結合律により並列度がビットに影響しない(設計どおり)。
23
+ - **速度**: prefill+decode 11 forward ≈ 9.4–10.0s → **~0.85s/token(CPU 8 threads)**。
24
+ 0.5B 時代の「1 問数分」から、35B で桁違いに実用的。GPU 化(QI35-5)前の CPU 値。
25
+
26
+ ## 実装で確定した追加意味論(設計書反映)
27
+
28
+ - **正規化シフト(pinned)**: norm 系の積(h·inv·γ など)は **RNE >>30 で ≤2^52 に正規化してから
29
+ aq** する(スケール補正 74→44 等)。理由: 生の積は 2^76–2^92 に達し、`amax·2^60` が **i128 を
30
+ 超えて Rust release で無音 wrap** する(Python bigint 参照では顕在化しない罠)。G2 全チェックを
31
+ この意味論で再実行し、**誤差プロファイルは変化なし**(worst 一覧が同一)を確認済み。
32
+ - `sfrom` はオーバーフロー安全形 `(a/L)·c + ((a%L)·c)/L`(exact 恒等)で実装。
33
+ - LM head argmax は (acc, e) ペアの**厳密比較**(bitlen+指数、同桁は整列比較)で fp 経由なし。
34
+
35
+ ## 正直な限界
36
+
37
+ - G3 は 2 prompt × 8 token の初回測定。**分岐後の比較は無意味**(文脈が変わる)なので、指標は
38
+ 「最初の分岐までの一致長」。統計的な一致率測定(多 prompt)は未実施。
39
+ - fp 側との分岐そのものは canonical class の設計上の性質(W4+A24 の別モデル)。品質は生成文の
40
+ 流暢さ・事実性で判断する段階で、perplexity 定量は未実施。
41
+ - 現状 CPU のみ。Metal offload(QI35-5)・CUDA 一致(G5)は未実施。
42
+ - chat template / tokenizer 統合(実際の Q&A UI)は未接続(llama-tokenize で手動 encode)。
docs/evidence/canonical-int-qwen36-layer-g2-2026-07-20.md ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — QI35 G2: Qwen3.6-35B 実重み layer 単位の整数/fp 照合 (2026-07-20)
2
+
3
+ [QI35 設計](../canonical-integer-qwen36-design.md) のゲート **G2** を実施した記録。
4
+ 対象は**本番 blob そのもの**(`Qwen3.6-abliterated-35b-Claude-4.7-Q4_K_M.gguf`、23,938,321,728 bytes)。
5
+ 0.5B デモは以後使用しない(ユーザ指示)。
6
+
7
+ ## 手順(再現可能)
8
+
9
+ 1. `palw_fp_dump.cpp`(本ディレクトリ)を vendored llama.cpp(`build-cpu`, Metal OFF)に対して
10
+ コンパイルし、prompt `"Hello, world"`(tokens `[9419, 11, 1814]`)で prefill(s0)+ greedy
11
+ 1 token(s1: token 0)を実行。layer 0(DeltaNet)/ layer 3(attention)/ MoE / l_out-2 の
12
+ 全中間テンソルを**全要素** fp32 でダンプ(202 ファイル、15MB)。
13
+ 2. `qi35_int_ref.py`(本ディレクトリ)= exact 整数リファレンス。GGUF を直接 mmap し、
14
+ Q4_K/Q6_K を **fp16 スケールの exact (mant,e) 分解**で整数消費(再量子化なし・丸め追加なし)。
15
+ 設計書の整数意味論(Q60 スケール、per-token A24、DeltaNet 内部 Q16/Q20/Q24、LUT 群)で
16
+ layer 0(prefill 3 token + 状態持ち越し decode 1 token)と layer 3(fp l_out-2 入力の単層テスト
17
+ + KV cache 持ち越し decode)を実行し、fp32 ダンプと全チェックポイント照合。
18
+
19
+ ## 結果(relmax = max|Δ|/max|ref|、全 77 チェックポイント)
20
+
21
+ **layer 出力・状態(判定対象)— 全て 5% 以内で PASS**:
22
+
23
+ | checkpoint | prefill (s0) | decode (s1) |
24
+ |---|---|---|
25
+ | DeltaNet state(128×128×32)| relmax 1.45e-2 / rmse **1.1e-4** | 1.51e-2 / **8.9e-5** |
26
+ | final_output(gated norm 後)| 6.7e-3 | **8.9e-4** |
27
+ | linear_attn_out | 1.7e-2 | 1.5e-2 |
28
+ | l_out_0(層出力)| **1.1e-2** | — |
29
+ | MoE top-8 選択(layer 0/3, s0/s1)| **全 4 ケース exact 一致** | 同左 |
30
+ | ffn_out_0 / ffn_out_3 | 1.9e-2 / 5.0e-3 | 3.0e-2 / 1.9e-2 |
31
+ | attention 経路(Qcur/Kcur roped, pregate, gated, output)| ≤4.6e-2 | ≤2.4e-2 |
32
+ | l_out_3(層出力)| **3.5e-3** | **1.6e-3** |
33
+
34
+ 全数値は `qi35_int_ref.py` 実行ログ参照。embedding は exact 分解のため fp32 丸め誤差のみ(2e-7)。
35
+
36
+ **例外(記録)**: `s0_moe_probs_0` relmax 5.8e-2(rmse 6.7e-3)のみ 5% 超。原因は上流(layer-0
37
+ チェーン)の A24 量子化ノイズが logit 差 → exp で増幅されるため(**単層テストの layer-3 router は
38
+ 1.6e-2 で通過**、logits は 0.9e-2)。top-8 選択・正規化重み・ffn_out には影響していない。
39
+
40
+ ## 実装中に確定した設計定数(設計書に反映済み)
41
+
42
+ - **eps 整数化**: rms_norm_eps 1e-6 → `EPS_Q40=1,099,512`(2^40)/ `EPS_Q48=281,474,977`(2^48)。
43
+ スケール可変テンソルは per-token に `eps_int = EPS_Q40·U²>>40`(U=2^60/scale)。
44
+ ※ 初期実装で 1000× 小さい定数を置くバグがあり、減衰 head(o≈2e-5)が eps-floor 支配で
45
+ 29× ずれた — **eps は fidelity の一級市民**(4b-2a の RMSNorm 教訓の再演)。
46
+ - **γ(decay)LUT**: exp(g), g∈[−16,0]、**1/2048 刻み・32769 entry・Q30**(実測 g は absmean 8.8)。
47
+ - **exp LUT**: [0,8) **1/2048 刻み・16384 entry・Q20**(0.5B の 2048×1/512 から拡張 — 35B の
48
+ router logit 差は最大 ~7.2 で旧定義域 [0,4) を超える)。
49
+ - **softplus LUT**: [−16,16) 1/512・Q16、x>20 は恒等(ggml 準拠)。
50
+ - **KV cache / DeltaNet conv・v**: per-token スケールではなく**絶対固定小数点 Q16**で保存
51
+ (逐次 decode ≡ prefill のビット一致が自明になる。範囲は実測 ±35)。
52
+ - **状態 S**: Q24 / i64(実測 |S|≤10 → 積和が i64 に収まる。GPU 化に i128 不要)。
53
+ - q/k(L2 norm 後)Q20、β/softplus Q16、1/√128 は Q30 定数、attention scale 1/16 は exact。
54
+ - fused GDN の head 対応: v-head h → k-head **h % 16**(タイル型、`ops.cpp:10749-10750`)。
55
+ - fp グラフの cb 名 `Kcur` は射影直後と RoPE 後で重複 — ダンプ照合は 2 個目(`__1`)を使うこと。
56
+
57
+ ## 正直な限界
58
+
59
+ - 照合は 1 prompt × (3+1) token × layer 0/3 のみ。全 40 層 greedy token 一致率は G3(未実施)。
60
+ - 単層誤差 1–3% は W4(Q4_K 固有)+ A24 + LUT の合成ノイズで、これが canonical class の
61
+ fp 追跡限界。低マージン手での token 分岐は G3 で定量化する。
62
+ - Python リファレンスは遅い(全照合 ~3 分)。性能は QI35-4(Rust)/QI35-5(Metal)の担当。
63
+ - LUT 生成はオフライン fp(pin 後は決定的)。Rust/Metal 移植時は**テーブルのバイト列を成果物として
64
+ 固定**し、生成方法ではなくテーブル自体を配布する。
docs/evidence/canonical-int-qwen36-metal-v1-2026-07-20.md ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — QI35-5 v1: 整数エンジン Metal offload(bit-exact)+ 秒単位 receipt (2026-07-20)
2
+
3
+ ## 構成(CUDA-mirror、設計どおり)
4
+
5
+ - `qi35_metal_shim.m`(本ディレクトリ): MSL カーネル `gemm_q4k`/`gemm_q6k` — Q4_K/Q6_K ブロックを
6
+ **カーネル内で整数 fp16 ビット分解**し、superblock 部分和を**指数整列つき exact 128-bit (lo,hi)
7
+ 加算**で畳む(整数結合律 → スレッド順序がビットに影響しない)。重みは **mmap 領域への
8
+ per-tensor no-copy MTLBuffer**(24GB 単一バッファは maxBufferLength=20.1GB 超のため、ページ境界
9
+ 切り下げ+バッファ内オフセット+キャッシュ)。x/out はバッファプール再利用。
10
+ - Rust 側 `gemm_batch`: 依存のない GEMM 群(qkv+gate / q+k+v / expert gate+up×8+shared /
11
+ down×8+shared)を**1 command buffer に一括エンコード**して commit。rows<256 は CPU のまま。
12
+ ホストの整数グルー(aq/norm/再帰/softmax)は CPU — 意味論は完全不変。
13
+ - 追加高速化: prefill 中の LM head(508M MAC)スキップ(最終位置のみ計算)、MoE ルーター/shexp
14
+ ゲート/最終 norm 重みの起動時キャッシュ(毎 forward の 52 万要素 F32 変換を排除)、DeltaNet
15
+ 再帰の head 並列化。
16
+
17
+ ## bit-exactness(canonical class の生命線)
18
+
19
+ | 検証 | 結果 |
20
+ |---|---|
21
+ | カーネル selftest(Q4_K/Q6_K×5 tensor、16-GEMM バッチ、row0≠0、k=512/2048/4096)| **全行一致** |
22
+ | full 8-token: tokens / FNV / route_root / kv_root / state_root | **CPU と全一致** |
23
+ | full 32-token: FNV `8cd22345b5a14c5a` | **CPU と一致** |
24
+ | Metal エンジンで発行した v3 receipt の output_commitment | 既存 cross-vendor 値 `e0697cf9…e104` と**同一** |
25
+
26
+ 途中で踏んだバグ 2 件(記録): (a) MSL 予約語 `half` を変数名に使用 → コンパイル失敗、
27
+ (b) スレッドローカル配列 `A[8]` が k=4096(`ssm_out`/`attn_output`、nsb=16)で**配列外書き込み**
28
+ → 出力破壊。selftest が k=2048 のみだったため見逃し — 「全 k 形状を selftest に含める」を教訓化。
29
+
30
+ ## 速度(Mac Studio M1 Max、5 prompt + 32 decode)
31
+
32
+ | 構成 | 時間 | tok/s |
33
+ |---|---|---|
34
+ | 整数 CPU(改善込み)| 0.61s/token | 1.66 |
35
+ | **整数 Metal v1** | **0.276s/token** | **3.6**(2.2×)|
36
+ | fp Metal(参考)| — | 52.0 |
37
+
38
+ プロファイル実測: **2609 commit/13fwd(~200/token)× ~1ms = GPU 往復が 63%**。GPU 演算自体は
39
+ 数 ms/token。試して**棄却**した策: commit の busy-wait(コア競合で悪化)、proj_finish/aq の
40
+ マイクロ並列化(thread spawn コスト > 利得)。
41
+
42
+ ## 秒単位 receipt(達成)
43
+
44
+ - `palw-integer-receipt` に **mtime+size キーのモデルハッシュキャッシュ**(/tmp/qi35-hash-cache.txt)
45
+ を追加、`QI35_ENGINE_ARGS="--metal"` でエンジン引数を配線(verify --rerun 側も同様)。
46
+ - 実測: 初回発行 29s(cold hash)→ **2 回目以降 6s**(Metal 生成 3.4s + 署名/bundle/db + tokenize)。
47
+ 検証 --rerun も全 PASS。チャット常駐(`QI35_METAL=1 qi35_chat.sh`)は生成時間+1s 未満で receipt 付与。
48
+
49
+ ## 52 tok/s 級への残り(v2 ロードマップ、正直に)
50
+
51
+ 現況 3.6 tok/s の支配項は **dispatch 往復 ~200 回/token**。52 tok/s(19ms/token)には:
52
+ 1. **層単位の単一 command buffer 化** — グルー(aq/rms/conv/silu/再帰/softmax)を Metal カーネル化し、
53
+ 1 token = 数 commit に削減(親設計 §8 の full-device 路線。LUT/isqrt は整数なので移植は素直)。
54
+ 2. 期待値: GPU 演算 5-10ms + commit ~5 回 ×1ms + 残 CPU(router/argmax)→ **20-40 tok/s 圏**。
55
+ fp 並(52)には attention/KV も on-device 化が必要。
56
+ 3. 併走オプション: RTX 側 CUDA カーネル(同じ 128-bit 整列加算、i128 → 2-word)— cross-vendor の
57
+ まま両バックエンド高速化。
58
+
59
+ ## 再現
60
+
61
+ ```
62
+ clang -O2 -fobjc-arc -shared qi35_metal_shim.m qi35_metal_moe.m qi35_metal_dn.m \
63
+ -framework Metal -framework Foundation \
64
+ -install_name @rpath/libqi35metal.dylib -o libqi35metal.dylib
65
+ rustc -O --edition 2021 qi35_model.rs -L <dir> -l qi35metal -C link-arg=-Wl,-rpath,<dir> -o qi35_model
66
+ ./qi35_model <gguf> qi35_tables.bin --ids ... --metal # bit-exact GPU 実行 (v1)
67
+ ./qi35_model <gguf> qi35_tables.bin --ids ... --metal-v3 # MoE+DeltaNet on-device (Phase A+B)
68
+ QI35_SHADOW=1 ... --metal-v3 # 層単位 CPU/GPU 突合(pipe に head 禁止)
69
+ QI35_SELFTEST=1 ./qi35_model <gguf> qi35_tables.bin # カーネル CPU/GPU 突合
70
+ QI35_ENGINE_ARGS="--metal" palw-integer-receipt ... # 秒単位 receipt
71
+ ```
72
+
73
+ ## 追記 — v2 試行の実測結果 (同日深夜)
74
+
75
+ **常駐スレッドプール**(spawn レス並列)を実装し、グルー(proj_finish/aq/変換)の並列化を試行:
76
+ - 細粒度プール化(~300 呼び出し/token × 7 wake)は **Metal 完了待ちとスケジューラ競合**し
77
+ GPU wall を 2.53s→11.2s に悪化させた(M1 の P コア争���)。小 n 直列ガードでも 4.55s止まり。
78
+ - **確定構成**: プールは粗粒度サイトのみ(DeltaNet 再帰 head 並列 / CPU GEMM / router)、
79
+ 細粒度グルーは直列。最終 **0.268s/token = 3.7 tok/s**(bit-exact 維持、FNV `8cd22345…`)。
80
+
81
+ **結論(実測に基づく)**: 現アーキテクチャ(GEMM=GPU/グルー=CPU)は **~200 commit/token ×
82
+ ~0.8-1ms の dispatch 往復が構造的下限**で、頭打ちは ~4 tok/s。20-40 tok/s には
83
+ **グルーの完全 on-device 化**(aq/rms の reduction カーネル + Q60 スケール代数の (m0,e) 正規化
84
+ 2-word 実装 + 層単位 1-2 commit)が必須 — 親設計 §8 の full-device 路線そのもので、
85
+ 次セッション規模の独立した実装作業。CPU 側マイクロ最適化はこの SoC では逆効果と実証済み。
86
+
87
+ ## 追記 2 — v3 Phase A(MoE full-device)実装状況 (同日)
88
+
89
+ - `qi35_metal_moe.m`: **MoE 全段を 1 command buffer に on-device 化**(I128 2-word 演算・binary
90
+ 128-bit floor 除算・Q60 スケール代数・router I128 dot・決定的 top-8・silu·up・加重和、~20 dispatch)。
91
+ `--metal-v3` + `QI35_SHADOW=1` で **層単位 CPU/GPU 突合パス実装済み**、単発入力で全層一致を確認
92
+ (発見済みバグ 2 件修正: k_shgate の 128-bit 積引数順、shared expert の us スロット)。
93
+ - **未解決 2 件(v3 続行の起点)**: (a) 実チェーン実行で tokens/FNV は一致するが route/kv/state root
94
+ が CPU 真値と不一致 — 未検出の入力依存分岐あり。`QI35_SHADOW=1` を実プロンプトで回し最初の
95
+ 分岐層を特定するのが次の一手。**production は --metal(v1)のまま**。(b) moe_gpu_wall
96
+ 292ms/token — `k_topk` の thread-local 256 要素配列がデバイスメモリにスピル。threadgroup
97
+ memory 化(es[256] を shared に、max/選択は barrier 後 thread0)で解消見込み。
98
+ - 教訓: GPU 単一スレッドカーネル + thread-local 大配列は数 ms 級の罠;shadow 比較は**実プロンプト
99
+ の実チェーンで**回すこと(単発入力の全層 OK は十分条件でない)。
100
+
101
+ ## 追記 3 — v3 Phase A 完成: MoE full-device が bit-exact 成立 (2026-07-20 深夜 2)
102
+
103
+ - **バグ確定と修正**: 前回の「実チェーンで roots 不一致」の真因は 2 つ:
104
+ (1) **検証ハーネス側** — shadow 実行を `| head -N` に繋いだため SIGPIPE で layer 30 前に
105
+ エンジンが死んでおり「全層 OK」は偽陽性だった(教訓: 検証パイプに head を挟まない)。
106
+ (2) **本物のバグ** — k_topk の重み正規化を 2 段(w30→wsum 再正規化)で実装していたが、
107
+ CPU 意味論は **1 段**(`wn = fdiv(es·2^30 + dsel/2, dsel)`)。half 境界でのみ 1 ULP ずれ、
108
+ layer 30 の wn[0] が 1 差 → smax 経由で全 ratio 汚染 → tot 1 要素 1 差。段階別ダンプ
109
+ (dq/ds/wn/rat/smax レジスタ読出し)で機械的に特定。
110
+ - **修正後**: 実プロンプト 8-token の shadow **520/520 全通過**、FNV・route/kv/state root
111
+ すべて CPU 真値と一致。**MoE 全段 on-device が正式に bit-exact**。
112
+ - **速度改善**: k_topk/k_shgate の threadgroup 化 + **proj_finish チェーンの単一カーネル融合**
113
+ (colsq→cmax→rescale→amax→aq を threadgroup barrier で 1 dispatch 化、k_finish/k_finraw)
114
+ + concurrent エンコーダ + memoryBarrier フェーズ化(~26→~17 dispatch、~10 フェーズ)。
115
+ moe_gpu 292→**151ms/token**、8-token total 4.9→**4.0s**。
116
+ - **計測結論(Phase D の根拠)**: per-CB commit+wait ≈ 1.5-2.5ms(高分散)が現在の床。
117
+ fp llama.cpp が同 GPU で単一 CB/token(~2500 op エンコード)により 52 tok/s を出している事実
118
+ から、CB 内エンコード op のコストは ~10µs 級 → **全 token 単一 CB(Phase B/C/D: DeltaNet・
119
+ attention・残差/rms を on-device 化し hv を GPU 常駐)で 30-40 tok/s は到達可能**。
120
+ 必要部品(I128 演算・fused finish・reduction・LUT)は Phase A で全て実証済み。
121
+
122
+ ## 追記 4 — v3 Phase B 完成: DeltaNet full-device が bit-exact 成立 (2026-07-20 深夜 3)
123
+
124
+ - `qi35_metal_dn.m`(新規): **DeltaNet 全段を 1 command buffer / 13 dispatch に on-device 化**。
125
+ 4 GEMM(qkv/gate/beta/alpha)→ fused finish×4 → per-head scalars(sig/softplus/gamma LUT)
126
+ + conv(to_fix→4-tap Q30→silu→ring shift、channel-local)→ L2 norm(u64 Newton isqrt +
127
+ i64 floor 除算)→ 再帰(1 threadgroup/head・1 thread/列、列局所なので barrier 不要、
128
+ I128 積で i64 wrap を構造的に排除)→ gated norm(EPS_Q48、2^88/mean、I128 積)→
129
+ **k_finq128(I128 ベクトル用 fused finish: amax→aq→ys=rne(sfrom(amax,2^60),60))** →
130
+ ssm_out GEMM → fused finish。GEMM/k_finish は Phase A 検証済みパイプラインを再利用。
131
+ - **状態の GPU 常駐化(Phase D の下準備)**: DState(S 4MB + conv ring 192KB / 層)を Rust 側
132
+ page-aligned mmap(16K 境界)に置き換え、`newBufferWithBytesNoCopy` で **GPU/CPU が同一
133
+ ページを共有**(コピーゼロ、CPU fallback・shadow・state_root は同じバイト列を読む)。
134
+ GPU カーネルが in-place 更新した状態がそのまま state_root へ。
135
+ - **検証(初回ビルドで全通過)**: 単発 smoke(dn 120/120 + moe 160/160)→ 実プロンプト
136
+ `--ids 760,6511,314,9338,369 --ndecode 8` shadow **dn 390/390 + moe 520/520 全通過**、
137
+ FNV `57f71e98c68ce514`・route/kv/state root すべて CPU 真値と一致(state_root 一致 =
138
+ GPU 常駐状態の bit-exact 更新の直接証明)。32-token FNV `8cd22345b5a14c5a` 一致。
139
+ v1(--metal)回帰・カーネル selftest も全パス。shadow は pre-state snapshot → CPU 再実行 →
140
+ 出力+事後状態(S/ring)完全比較 + 段階レジスタダンプ(DNDBG)を実装。
141
+ ランタイム前に反証レビュー(カーネル別 7 視点 + 独立検証)をワークフローで並列実施 —
142
+ **7/7 全員 refuted=false**(bit-exactness への反証ゼロ。指摘は到達不能な headroom メモのみ。
143
+ 唯一の防御的指摘「未知 quant type が Q6_K カーネルへ黙って fallthrough」は Rust 側 assert で
144
+ CPU と同じ失敗様式に修正済み)。
145
+ - **速度**: 8-token 定常 **3.9s(Phase A 4.0s と同等)**、32-token 11.5s(3.2 tok/s)。
146
+ PROF 定常: moe 1.94s / dn 1.30-1.35s / gemm_batch 0.30s(269 commits)。
147
+ dn CB 内訳(stage 打ち切り QI35_DN_STAGE): S1(GEMM+finish)0.82s / +conv+scalars ±0 /
148
+ +L2+再帰 +0.08s / +gnorm+finq +0.26s / +out GEMM+finish +0.19s → **commit 床 0.82s(61%)、
149
+ カーネル実行 ~0.5s**。30 CB/token × ~2.1ms が支配項のまま = Phase D(単一 CB 化)の根拠を再確認。
150
+ finq の単一 tg 128-bit 長除算 4096 回(+0.65ms/層)は emin 別 ratio メモ化で削減可能(未実施)。
151
+ - 意味論の要点(バグゼロで通った理由): CPU が i128 を使う箇所は全て I128 2-word で複製
152
+ (s*g・kv*dlt・raw*inv30・o24²・v*inv*gm・normed*silu は i64 を溢れ得る)、silu の >>16 は
153
+ rne でなく算術シフト、L2 の fdiv は負分子の floor 補正付き i64 除算、isqrt は Rust と同一
154
+ Newton 列(入力 <2^63 で u64 化)、再帰は列局所ゆえ decay→sk→dlt→update→o を同一 thread で。
155
+
156
+ ## 追記 5 — v3 Phase C 完成: full attention on-device + KV cache GPU 常駐が bit-exact 成立 (2026-07-20 夜)
157
+
158
+ - `qi35_metal_at.m`(新規): **full-attention 10 層の全段を 1 command buffer / ~12 dispatch に
159
+ on-device 化**。3 GEMM(attn_q 8192 / attn_k 512 / attn_v 512)→ fused finish×3 →
160
+ `k_at_qknorm`(per-head RMS、per-token eps `u=min(2^60/s,2^40)`、**i128 Newton isqrt** —
161
+ head セグメントがほぼ零だと 2^88/mean が u64 を超えるため dn の isqrt64 は不可、Rust と
162
+ 同一反復列を I128 divmag で完全複製)→ `k_finq128`×2(**shift 引数化**: dn 60 / qn·kn 44 /
163
+ at 32 — dn 側は 60 を渡し bit 挙動不変)→ `k_at_rope`(A24 int 上の半回転、Q30 cos/sin
164
+ テーブル GPU 常駐)→ `k_at_kv`(tofix16 流用で k16/v16 を row=pos に書込 + cq)→
165
+ `k_at_core`(1 tg/head: I128 スコア内積 → 符号付き max → expn LUT + den → floor 除算 +
166
+ sigmoid gate)→ finq(32)→ attn_output GEMM → fused finish。GEMM/finish/finq は
167
+ Phase A/B 検証済みパイプラインを再利用。
168
+ - **KV cache の GPU 常駐(事前確保)**: KvState を Vec→page-aligned mmap(smax=8192 行 ×
169
+ NKV·HD)へ。`newBufferWithBytesNoCopy` で GPU が in-place append、CPU fallback・shadow・
170
+ kv_root は同じバイト列を読む。kv_root は len プレフィクスのみハッシュ = 旧 Vec 実装と
171
+ バイト同一(8tok/32tok の root 完全一致で確認)。
172
+ - **検証(初回ビルドで全通過)**: 単一 token shadow 全層 OK → Hello 8tok(FNV
173
+ `ff03bf7c9a5d7593` = G3 CPU 真値、生成列一致)→ Paris 8tok(FNV `57f71e98c68ce514` =
174
+ Phase B 真値)→ **rebind 強制(QI35_KVBIND0=8)shadow 全層 OK** → 32-token FNV
175
+ `8cd22345b5a14c5a` 一致 + route/kv/state root 一致。
176
+ - **敵対的レビュー(6 視点 + 独立検証)**: confirmed ゼロ(verdict 全 real=false、到達不能
177
+ 領域のみ)。防御修正 2 件を適用: (a) expn LUT の `ix∈[2^63,2^64)` 折返し領域を CPU と同じ
178
+ 0 に固定(device OOB read も排除)、(b) attention_gpu にテンソル幾何 assert(rows/nsb
179
+ ハードコードの黙殺防止、dn の同型指摘と同じ対応)。適用後 shadow 再 PASS。
180
+ - **perf 退行事件と修正(Phase D への重要な教訓)**: 初版は 64MB×20 の KV バッファを
181
+ hazard-tracked のまま bind → **バインドしていない moe/dn の CB まで commit が 10× 遅化**
182
+ (moe 151ms→1.5s/token、v1 は正常 = Phase C 由来と切り分け)。KVMAX=512 で完全回復 =
183
+ **コストは bind 済みサイズに比例**(per-commit residency/hazard 走査)。untracked 化で
184
+ 部分回復(8192 行: moe 5.8s)。**最終修正 = grow-rebind**: 初期 bind 1024 行/層
185
+ (`QI35_KVBIND0`)、pos が超えたら同一ページを ×4 で包み��し(データ無傷、〜2 回/8k)、
186
+ untracked 併用(CB 内は明示 barrier、CB 間は waitUntilCompleted で同期済みのため安全)。
187
+ → moe 1.93s / dn 1.29s(8tok)= Phase B と同水準。**教訓: 巨大 tracked no-copy バッファは
188
+ プロセスの全 CB commit に課税する。Phase D の hv 常駐も bind 最小化 + untracked を既定に。**
189
+ - **速度**: 8-token 4.2s(安定、×2 再現)、32-token **10.9s(3.4 tok/s)**、PROF 定常:
190
+ moe 4.77s / dn 3.63s / at 1.47s / gemm_batch 0.53s(33 commits = LM head のみ)。
191
+ attention の CB 4ms/層(床 2ms + カーネル 2ms)。トークンあたり CB 80 本(moe40+dn30+at10)
192
+ が残る床 = Phase D(単一 CB 化)で 20-40 tok/s を狙う構造が完成。
docs/evidence/canonical-int-qwen36-v3-pair-2026-07-20.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evidence — QI35-8/10: 整数 receipt の palw 統合(ML-DSA/bundle/state-db)+ 実 2 台 cross-vendor k=2 ペア (2026-07-20)
2
+
3
+ 整数エンジンの receipt を **既存 palw フローと同じ土俵**(ML-DSA-87 署名・暗号化 bundle・durable
4
+ state-db)に載せ、**Mac(Apple aarch64)と RTX box(WSL2 x86_64)の実 2 台**で同一ジョブの receipt を
5
+ 独立発行 → **receipt-v3 の `match_pair_v3` で正式ペア成立**まで実機で通した記録。
6
+
7
+ ## 実装(runtime-palw、`--features ml-dsa`)
8
+
9
+ - **`palw-integer-receipt`**(src/bin/): qi35 整数エンジンをサブプロセス実行し、
10
+ `receipt_v3::ComputeReceiptV3` を構築して **ML-DSA-87 署名**(`sign_receipt_v3`、seed は監査鍵+
11
+ worker label から導出)。consensus projection(`MatchProjectionV2`)は全て実データ:
12
+ - `output_commitment` = domain_hash64(network ‖ prompt_commit ‖ output ids)
13
+ - `route_root` / `execution_root` / `state_root` = エンジンが生成後に出力する **bit-exact digest**
14
+ (MoE expert route 列 / KV cache 整数列 / DeltaNet 状態+conv 整数列の blake2b256)を domain wrap
15
+ - `canonical_compute_units` = 既存 `qwen36_semantic_compute_units`(token 数のみの意味論的 CU — 整数
16
+ エンジンにそのまま適用可能)
17
+ - `compute_set_id` = model+tables blake2b256 束縛。engine ハッシュは **telemetry**(ADR §3: 実装差は
18
+ consensus 外)
19
+ - bundle: **XChaCha20Poly1305**(既存フローと同じ AEAD)で opening(prompt/output ids + digest)を
20
+ 封緘、AAD = receipt_id。state-db: sqlite `integer_receipts`(execution_nullifier UNIQUE = 二重計上防止)
21
+ - **`palw-verify-integer`**: ML-DSA 検証 / credential 束縛(vk→credential 再導出)/ bundle 復号と
22
+ projection 再計算 / state-db 照合 / `--rerun`(エンジン独立再実行で全 root 再現)/
23
+ `--pair`(receipt-v3 `match_pair_v3` による k=2 判定)。
24
+ - 新規 domain tag は [receipt-v3-ledger.md](../receipt-v3-ledger.md) §4 に昇格申請行として記録。
25
+
26
+ ## 実機ラン 1 — Mac 発行 + フル検証 15/15 PASS
27
+
28
+ prompt "The capital of France is"(ids `[760,6511,314,9338,369]`)、n-predict 8:
29
+
30
+ ```
31
+ receipt_id=e16e6181…3be318 signature=ml-dsa-87 (receipt-v3 envelope)
32
+ output_commitment=e0697cf9…e104 canonical_compute_units=77434 durable_receipt_count=1
33
+ 検証: ml_dsa_signature / receipt_id / credential_binding / bundle_receipt_binding /
34
+ output_commitment / route_root / execution_root / state_root / token_count /
35
+ canonical_compute_units / state_db_row / rerun_output_ids / rerun_route_root /
36
+ rerun_kv_root / rerun_state_root — 全 PASS
37
+ ```
38
+
39
+ (検証側の初回バグ: stop_reason=length の再実行に max_new=len+1 を渡し 1 token 余分に生成 → 全 root
40
+ 不一致。stop_reason で分岐して修正 — 「再実行は停止条件まで含めて再現せよ」という教訓ごと記録。)
41
+
42
+ ## 実機ラン 2 — RTX box(WSL2 x86_64)で独立発行 → commitment 完全一致
43
+
44
+ - blob 23,938,321,728 bytes を SSH バイナリパイプで転送、blake2b256 **一致**(`fabdc6dc…5326c`)。
45
+ tables も一致(`8ae72774…f9a4`)。エンジンは WSL 上で rustc 1.96 により**独立ビルド**。
46
+ - 同一 prompt ids / n-predict 8 を worker label `rtx-wsl-x86` で発行:
47
+
48
+ ```
49
+ RTX: output_commitment=e0697cf997e01899f525a4cac6f70a0f7b1f2c91d3ae9c0fc2154cccf324a66c…e104
50
+ Mac: output_commitment=e0697cf997e01899f525a4cac6f70a0f7b1f2c91d3ae9c0fc2154cccf324a66c…e104 ✅ 一致
51
+ ```
52
+
53
+ Apple aarch64 ↔ AMD64/WSL2、別 OS・別ビルド・別スレッド数で **projection が byte 一致**。
54
+ (RAM 21GB の WSL では 24GB blob が mmap ページングになり 8 token 24.6s(Mac 10.0s)— 機能は完全。)
55
+
56
+ ## 実機ラン 3 — k=2 cross-vendor ペア成立
57
+
58
+ ```
59
+ palw-verify-integer --receipt <mac>.integer.json --pair rtx-receipt.json
60
+ [PASS] ml_dsa_signature [PASS] pair_ml_dsa_signature [PASS] k2_pair_match
61
+ pair_id=ce81efa651669a7dd73eac8cfa40c54f117ee0ca3c2bcedea93084c56809038e…c2bf
62
+ ```
63
+
64
+ `match_pair_v3` の要件(同一 network / 独立 worker credential / 独立 nullifier / **projection 全
65
+ フィールド byte-equal**)を実 2 台で満たした。凍結済み ADR §3「cross-vendor ペアは有効、
66
+ runtime_class_id は telemetry」の**最初の実機実証**。
67
+
68
+ ## 正直な限界
69
+
70
+ - `job_challenge` は自己導出(epoch beacon 未接続)。credential は監査鍵+label 由来(ネットワーク
71
+ 登録 credential ではない)。state-db は専用テーブル(v3 単一台帳 store への統合は v3 本体作業)。
72
+ - mint 適格性は引き続き false(network anchor / bond が無いため)。ただし fp 版の非適格理由のうち
73
+ 「trace がカーネル launch スケッチで accumulator 証明でない」は、整数 class では**再実行で完全再現
74
+ 可能**という強い性質に置き換わっている(qi35-int の route/KV/state root は独立再実行で一致)。
75
+ - WSL 側 CUDA は未使用(CPU x86 で十分に cross-vendor を示せるため。CUDA offload は QI35-5 と同系の
76
+ 高速化作業)。
docs/evidence/gguf_full_dump.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Full GGUF dump: ALL kv (including array contents for small arrays) + ALL tensor names/shapes/types.
3
+ import struct, sys, json, collections
4
+
5
+ path = sys.argv[1]
6
+ f = open(path, "rb")
7
+ def rd(fmt):
8
+ n = struct.calcsize(fmt)
9
+ return struct.unpack("<" + fmt, f.read(n))[0]
10
+ def rd_str():
11
+ n = rd("Q")
12
+ return f.read(n).decode("utf-8", "replace")
13
+
14
+ magic = f.read(4)
15
+ assert magic == b"GGUF", magic
16
+ ver = rd("I")
17
+ tensor_count = rd("Q")
18
+ kv_count = rd("Q")
19
+ print(f"gguf_version={ver} tensor_count={tensor_count} kv_count={kv_count}")
20
+
21
+ def read_value(vt, keep_arrays=False):
22
+ if vt == 8: return rd_str()
23
+ if vt == 6: return rd("f")
24
+ if vt == 12: return rd("d")
25
+ if vt == 7: return bool(rd("B"))
26
+ if vt == 0: return rd("B")
27
+ if vt == 1: return rd("b")
28
+ if vt == 2: return rd("H")
29
+ if vt == 3: return rd("h")
30
+ if vt == 4: return rd("I")
31
+ if vt == 5: return rd("i")
32
+ if vt == 10: return rd("Q")
33
+ if vt == 11: return rd("q")
34
+ if vt == 9:
35
+ et = rd("I"); cnt = rd("Q")
36
+ vals = []
37
+ for _ in range(cnt):
38
+ v = rd_str() if et == 8 else read_value(et)
39
+ if cnt <= 64 or keep_arrays:
40
+ vals.append(v)
41
+ if cnt <= 64:
42
+ return vals
43
+ return f"<array type{et} x{cnt}>"
44
+ raise ValueError(f"unknown vt {vt}")
45
+
46
+ for _ in range(kv_count):
47
+ key = rd_str()
48
+ vt = rd("I")
49
+ val = read_value(vt)
50
+ print(f"KV {key} = {val}")
51
+
52
+ GGML_TYPES = {0:"F32",1:"F16",2:"Q4_0",3:"Q4_1",6:"Q5_0",7:"Q5_1",8:"Q8_0",9:"Q8_1",
53
+ 10:"Q2_K",11:"Q3_K",12:"Q4_K",13:"Q5_K",14:"Q6_K",15:"Q8_K",
54
+ 16:"IQ2_XXS",17:"IQ2_XS",18:"IQ3_XXS",19:"IQ1_S",20:"IQ4_NL",21:"IQ3_S",
55
+ 22:"IQ2_S",23:"IQ4_XS",24:"I8",25:"I16",26:"I32",27:"I64",28:"F64",29:"IQ1_M",30:"BF16"}
56
+ print("\n=== TENSORS ===")
57
+ by_type = collections.Counter()
58
+ for _ in range(tensor_count):
59
+ name = rd_str()
60
+ nd = rd("I")
61
+ shape = [rd("Q") for _ in range(nd)]
62
+ ttype = rd("I")
63
+ off = rd("Q")
64
+ tn = GGML_TYPES.get(ttype, str(ttype))
65
+ by_type[tn] += 1
66
+ print(f"T {name} shape={shape} type={tn} off={off}")
67
+ print("\n=== TYPE SUMMARY ===")
68
+ for t, c in by_type.most_common():
69
+ print(f"{t}: {c}")