GGUF
English
Japanese
misaka-palw
misaka
palw
proof-of-compute
deterministic-inference
integer-quantization
conversational
Instructions to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M # Run inference directly in the terminal: llama cli -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M # Run inference directly in the terminal: llama cli -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M # Run inference directly in the terminal: ./llama-cli -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M # Run inference directly in the terminal: ./build/bin/llama-cli -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Use Docker
docker model run hf.co/Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
- LM Studio
- Jan
- Ollama
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with Ollama:
ollama run hf.co/Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
- Unsloth Desktop
- Pi
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Docker Model Runner
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with Docker Model Runner:
docker model run hf.co/Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
- Lemonade
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Run and chat with the model
lemonade run user.Qwen3.6-35B-A3B-PALW-runtime-Q4_K_M
List all available models
lemonade list
- Hermes Agent
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Update PALW runtime to Qwen source 3866a25
Browse filesTracked 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
- .gitattributes +2 -0
- config/qi35-calibration-prompts-v1.json +66 -0
- docs/adr-receipt-v3-node-ownership.md +109 -0
- docs/canonical-integer-compute-design.md +188 -0
- docs/canonical-integer-inc4b-fullmodel-design.md +114 -0
- docs/canonical-integer-qwen36-design.md +181 -0
- docs/evidence/canon_attn.cu +136 -0
- docs/evidence/canon_attn.py +188 -0
- docs/evidence/canon_attn.rs +187 -0
- docs/evidence/canon_attn_int.py +182 -0
- docs/evidence/canon_attn_norm.rs +233 -0
- docs/evidence/canon_attn_norm.swift +334 -0
- docs/evidence/canon_attn_sq.py +133 -0
- docs/evidence/canon_cuda.cu +75 -0
- docs/evidence/canon_dump.py +12 -0
- docs/evidence/canon_extract.py +73 -0
- docs/evidence/canon_extract_full.py +55 -0
- docs/evidence/canon_int.rs +126 -0
- docs/evidence/canon_layer.rs +83 -0
- docs/evidence/canon_layer_cuda.cu +89 -0
- docs/evidence/canon_layer_int.py +158 -0
- docs/evidence/canon_layer_metal.swift +94 -0
- docs/evidence/canon_metal.swift +93 -0
- docs/evidence/canon_model.cu +149 -0
- docs/evidence/canon_model.rs +188 -0
- docs/evidence/canon_model_hifi.py +175 -0
- docs/evidence/canon_model_int.py +169 -0
- docs/evidence/canon_ops.rs +97 -0
- docs/evidence/canon_ops_cuda.cu +93 -0
- docs/evidence/canon_ops_metal.swift +110 -0
- docs/evidence/canon_quant.rs +139 -0
- docs/evidence/canon_quant_full.py +115 -0
- docs/evidence/canonical-int-attention-4b2a-2026-07-19.md +77 -0
- docs/evidence/canonical-int-attention-4b2b-2026-07-19.md +53 -0
- docs/evidence/canonical-int-cross-isa-2026-07-19.md +61 -0
- docs/evidence/canonical-int-fulllayer-4b3-2026-07-20.md +52 -0
- docs/evidence/canonical-int-fullmodel-4b4-4b5-2026-07-20.md +56 -0
- docs/evidence/canonical-int-gpu-3backend-2026-07-19.md +58 -0
- docs/evidence/canonical-int-hifi-fullmodel-2026-07-20.md +58 -0
- docs/evidence/canonical-int-layer-4backend-2026-07-19.md +59 -0
- docs/evidence/canonical-int-metal-norm-2026-07-20.md +35 -0
- docs/evidence/canonical-int-ops-4backend-2026-07-19.md +62 -0
- docs/evidence/canonical-int-quantizer-2026-07-19.md +68 -0
- docs/evidence/canonical-int-qwen36-bench-2026-07-20.md +34 -0
- docs/evidence/canonical-int-qwen36-chat-receipt-2026-07-20.md +76 -0
- docs/evidence/canonical-int-qwen36-fullmodel-g3-2026-07-20.md +42 -0
- docs/evidence/canonical-int-qwen36-layer-g2-2026-07-20.md +64 -0
- docs/evidence/canonical-int-qwen36-metal-v1-2026-07-20.md +192 -0
- docs/evidence/canonical-int-qwen36-v3-pair-2026-07-20.md +76 -0
- 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}")
|