onw 0.12.0: Ornith-1.5-9B, earlier turns' reasoning_content to the template, --deltanet-fmt q8g128
Browse files- CHANGELOG.md +8 -0
- CHANGELOG_en.md +9 -0
- README.md +1 -0
- README_en.md +1 -0
- TECHNICAL.md +8 -0
- TECHNICAL_en.md +8 -0
- onw/__init__.py +1 -1
- onw/app.py +3 -0
- onw/chat.py +5 -1
- onw/convert.py +7 -2
- onw/i18n.json +4 -0
- onw/ir.py +13 -1
- onw/models/qwen3_5_moe.py +11 -5
- onw/server.py +1 -1
- pyproject.toml +1 -1
CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@
|
|
| 5 |
日付は日本時間です。各版は Hugging Face の `ryugyosoft/onw` にタグ(`v0.4.2` 以降)付きで残っています。
|
| 6 |
onw ウィンドウは、新しい版があるとこのファイルから変更点を表示します(`## v…` の見出しで区切っています)。
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
## v0.11.3 (2026-09-30)
|
| 9 |
- Ubuntu:ランチャー(アプリ一覧の onw)から起動するとすぐ落ちる問題を修正。GNOME はランチャーの中身をログインし直すまで
|
| 10 |
使い回し、しかもホームフォルダで起動するため、`python -m onw` がインストール先の `~/onw` を空の `onw` パッケージとして
|
|
|
|
| 5 |
日付は日本時間です。各版は Hugging Face の `ryugyosoft/onw` にタグ(`v0.4.2` 以降)付きで残っています。
|
| 6 |
onw ウィンドウは、新しい版があるとこのファイルから変更点を表示します(`## v…` の見出しで区切っています)。
|
| 7 |
|
| 8 |
+
## v0.12.0 (2026-10-01)
|
| 9 |
+
- **Ornith-1.5-9B に対応**(ornith-ai、MIT)。Qwen3.5-9B をエージェント・ツール呼び出し向けに鍛えたモデルで、モデル一覧に追加しました。
|
| 10 |
+
構造は Qwen3.5 と同じで、テキストと画像に対応します。MoE の 35B-A3B は NPU での精度を調整中で、次の版で追加する予定です。
|
| 11 |
+
- `onw convert --deltanet-fmt q8g128`:Qwen3.5/3.6 系の DeltaNet の `in_proj_qkv`・`out_proj` を INT8 にする変換オプション
|
| 12 |
+
(量子化に敏感なモデル向け。INT4 と同じグループ 128 の形にしたので、チャネル単位 INT8 よりずっと速い)。
|
| 13 |
+
- 履歴の `reasoning_content`(前のターンの思考)をテンプレートに渡すように。Ornith のテンプレートは過去のターンの思考も残す作りで、
|
| 14 |
+
エージェント用途ではこれが効きます(Qwen は最新のターンだけ、gpt-oss は `thinking` として渡ります)。
|
| 15 |
+
|
| 16 |
## v0.11.3 (2026-09-30)
|
| 17 |
- Ubuntu:ランチャー(アプリ一覧の onw)から起動するとすぐ落ちる問題を修正。GNOME はランチャーの中身をログインし直すまで
|
| 18 |
使い回し、しかもホームフォルダで起動するため、`python -m onw` がインストール先の `~/onw` を空の `onw` パッケージとして
|
CHANGELOG_en.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
| 4 |
|
| 5 |
Dates are JST. Every version is on Hugging Face `ryugyosoft/onw`, tagged from `v0.4.2` on.
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
## v0.11.3 (2026-09-30)
|
| 8 |
- Ubuntu: starting onw from its launcher crashed at once. GNOME keeps a launcher's command until the next login and
|
| 9 |
starts it in the home folder, where `python -m onw` imported the install folder `~/onw` as an empty `onw` package that
|
|
|
|
| 4 |
|
| 5 |
Dates are JST. Every version is on Hugging Face `ryugyosoft/onw`, tagged from `v0.4.2` on.
|
| 6 |
|
| 7 |
+
## v0.12.0 (2026-10-01)
|
| 8 |
+
- **Ornith-1.5-9B** (ornith-ai, MIT): Qwen3.5-9B trained further for agents and tool calls, added to the model list.
|
| 9 |
+
Same structure as Qwen3.5, text + image. The 35B-A3B MoE is still being tuned for accuracy on the NPU and should come
|
| 10 |
+
in the next version.
|
| 11 |
+
- `onw convert --deltanet-fmt q8g128`: INT8 DeltaNet `in_proj_qkv` / `out_proj` for Qwen3.5 / 3.6-family models
|
| 12 |
+
(for quantization-sensitive ones; in INT4's group-128 layout, much faster than per-channel INT8).
|
| 13 |
+
- `reasoning_content` in the history (an earlier turn's thinking) reaches the template. Ornith's template keeps earlier
|
| 14 |
+
turns' thinking, which agents rely on (Qwen keeps the latest turn's; gpt-oss gets it as `thinking`).
|
| 15 |
+
|
| 16 |
## v0.11.3 (2026-09-30)
|
| 17 |
- Ubuntu: starting onw from its launcher crashed at once. GNOME keeps a launcher's command until the next login and
|
| 18 |
starts it in the home folder, where `python -m onw` imported the install folder `~/onw` as an empty `onw` package that
|
README.md
CHANGED
|
@@ -106,6 +106,7 @@ Python や git の知識は要りません。Python が入っていなければ
|
|
| 106 |
| [ryugyosoft/Qwen3.5-9B-onw](https://huggingface.co/ryugyosoft/Qwen3.5-9B-onw) | `qwen3_5`(Gated DeltaNet+ゲート付きアテンション、dense) | テキスト+画像 | 4.1 tok/s | 5.3 GB | 約 11 GB |
|
| 107 |
| [ryugyosoft/Qwen3.6-REAP-18B-A3B-onw](https://huggingface.co/ryugyosoft/Qwen3.6-REAP-18B-A3B-onw) | `qwen3_5_moe`、256 個中 128 個のエキスパートを残す(REAP) | テキスト+画像 | 6.8 tok/s | 9.6 GB | 約 12 GB |
|
| 108 |
| [ryugyosoft/Qwen3.6-35B-A3B-onw](https://huggingface.co/ryugyosoft/Qwen3.6-35B-A3B-onw) | `qwen3_5_moe`(Gated DeltaNet+ゲート付きアテンション+MoE 256 個から 8 個+共有エキスパート) | テキスト+画像 | 6.8 tok/s | 17 GB | 約 20 GB(SSD から読めば 8〜13 GB) |
|
|
|
|
| 109 |
| [ryugyosoft/gpt-oss-20b-onw](https://huggingface.co/ryugyosoft/gpt-oss-20b-onw) | `gpt_oss`(OpenAI gpt-oss-20b:MoE 32 個から 4 個、attention sink、YaRN、harmony 形式。エキスパートはチャネル単位 INT4) | テキスト | 8.5 tok/s | 11.4 GB | 約 12 GB |
|
| 110 |
| [ryugyosoft/gpt-oss-20b-int8-onw](https://huggingface.co/ryugyosoft/gpt-oss-20b-int8-onw) | 同上、エキスパート INT8(bf16 に近い精度) | テキスト | 6 tok/s | 21 GB | 約 21 GB(SSD から読めば 8〜13 GB) |
|
| 111 |
|
|
|
|
| 106 |
| [ryugyosoft/Qwen3.5-9B-onw](https://huggingface.co/ryugyosoft/Qwen3.5-9B-onw) | `qwen3_5`(Gated DeltaNet+ゲート付きアテンション、dense) | テキスト+画像 | 4.1 tok/s | 5.3 GB | 約 11 GB |
|
| 107 |
| [ryugyosoft/Qwen3.6-REAP-18B-A3B-onw](https://huggingface.co/ryugyosoft/Qwen3.6-REAP-18B-A3B-onw) | `qwen3_5_moe`、256 個中 128 個のエキスパートを残す(REAP) | テキスト+画像 | 6.8 tok/s | 9.6 GB | 約 12 GB |
|
| 108 |
| [ryugyosoft/Qwen3.6-35B-A3B-onw](https://huggingface.co/ryugyosoft/Qwen3.6-35B-A3B-onw) | `qwen3_5_moe`(Gated DeltaNet+ゲート付きアテンション+MoE 256 個から 8 個+共有エキスパート) | テキスト+画像 | 6.8 tok/s | 17 GB | 約 20 GB(SSD から読めば 8〜13 GB) |
|
| 109 |
+
| [ryugyosoft/Ornith-1.5-9B-onw](https://huggingface.co/ryugyosoft/Ornith-1.5-9B-onw) | `qwen3_5`(Qwen3.5-9B をエージェント・ツール呼び出し向けに鍛えた Ornith-1.5) | テキスト+画像 | 4.9 tok/s | 5.3 GB | 約 11 GB |
|
| 110 |
| [ryugyosoft/gpt-oss-20b-onw](https://huggingface.co/ryugyosoft/gpt-oss-20b-onw) | `gpt_oss`(OpenAI gpt-oss-20b:MoE 32 個から 4 個、attention sink、YaRN、harmony 形式。エキスパートはチャネル単位 INT4) | テキスト | 8.5 tok/s | 11.4 GB | 約 12 GB |
|
| 111 |
| [ryugyosoft/gpt-oss-20b-int8-onw](https://huggingface.co/ryugyosoft/gpt-oss-20b-int8-onw) | 同上、エキスパート INT8(bf16 に近い精度) | テキスト | 6 tok/s | 21 GB | 約 21 GB(SSD から読めば 8〜13 GB) |
|
| 112 |
|
README_en.md
CHANGED
|
@@ -93,6 +93,7 @@ window keeps the server running; "Unload" stops it, "Quit" ends onw.
|
|
| 93 |
| [ryugyosoft/Qwen3.5-9B-onw](https://huggingface.co/ryugyosoft/Qwen3.5-9B-onw) | `qwen3_5` (Gated DeltaNet + gated attention, dense) | text + image | 4.1 tok/s | 5.3 GB | ~11 GB |
|
| 94 |
| [ryugyosoft/Qwen3.6-REAP-18B-A3B-onw](https://huggingface.co/ryugyosoft/Qwen3.6-REAP-18B-A3B-onw) | `qwen3_5_moe`, 128 of 256 experts kept (REAP) | text + image | 6.8 tok/s | 9.6 GB | ~12 GB |
|
| 95 |
| [ryugyosoft/Qwen3.6-35B-A3B-onw](https://huggingface.co/ryugyosoft/Qwen3.6-35B-A3B-onw) | `qwen3_5_moe` (Gated DeltaNet + gated attention + MoE 256x top-8 + shared expert) | text + image | 6.8 tok/s | 17 GB | ~20 GB (8-13 GB with experts from the SSD) |
|
|
|
|
| 96 |
| [ryugyosoft/gpt-oss-20b-onw](https://huggingface.co/ryugyosoft/gpt-oss-20b-onw) | `gpt_oss` (OpenAI gpt-oss-20b: MoE 32x top-4, attention sinks, YaRN, harmony format; channel-wise INT4 experts) | text | 8.5 tok/s | 11.4 GB | ~12 GB |
|
| 97 |
| [ryugyosoft/gpt-oss-20b-int8-onw](https://huggingface.co/ryugyosoft/gpt-oss-20b-int8-onw) | same with INT8 experts (close to bf16) | text | 6 tok/s | 21 GB | ~21 GB (8-13 GB with experts from the SSD) |
|
| 98 |
|
|
|
|
| 93 |
| [ryugyosoft/Qwen3.5-9B-onw](https://huggingface.co/ryugyosoft/Qwen3.5-9B-onw) | `qwen3_5` (Gated DeltaNet + gated attention, dense) | text + image | 4.1 tok/s | 5.3 GB | ~11 GB |
|
| 94 |
| [ryugyosoft/Qwen3.6-REAP-18B-A3B-onw](https://huggingface.co/ryugyosoft/Qwen3.6-REAP-18B-A3B-onw) | `qwen3_5_moe`, 128 of 256 experts kept (REAP) | text + image | 6.8 tok/s | 9.6 GB | ~12 GB |
|
| 95 |
| [ryugyosoft/Qwen3.6-35B-A3B-onw](https://huggingface.co/ryugyosoft/Qwen3.6-35B-A3B-onw) | `qwen3_5_moe` (Gated DeltaNet + gated attention + MoE 256x top-8 + shared expert) | text + image | 6.8 tok/s | 17 GB | ~20 GB (8-13 GB with experts from the SSD) |
|
| 96 |
+
| [ryugyosoft/Ornith-1.5-9B-onw](https://huggingface.co/ryugyosoft/Ornith-1.5-9B-onw) | `qwen3_5` (Ornith-1.5: Qwen3.5-9B trained further for agents and tool calls) | text + image | 4.9 tok/s | 5.3 GB | ~11 GB |
|
| 97 |
| [ryugyosoft/gpt-oss-20b-onw](https://huggingface.co/ryugyosoft/gpt-oss-20b-onw) | `gpt_oss` (OpenAI gpt-oss-20b: MoE 32x top-4, attention sinks, YaRN, harmony format; channel-wise INT4 experts) | text | 8.5 tok/s | 11.4 GB | ~12 GB |
|
| 98 |
| [ryugyosoft/gpt-oss-20b-int8-onw](https://huggingface.co/ryugyosoft/gpt-oss-20b-int8-onw) | same with INT8 experts (close to bf16) | text | 6 tok/s | 21 GB | ~21 GB (8-13 GB with experts from the SSD) |
|
| 99 |
|
TECHNICAL.md
CHANGED
|
@@ -100,6 +100,14 @@ Qwen3.5/Qwen3.6 は、Gated DeltaNet(線形アテンションの一種で、
|
|
| 100 |
- **大きすぎる値の前処理**:1024 倍した出力を複数トークンのブロックで RMSNorm に入れると、今度は二乗和が FP16 で
|
| 101 |
溢れます。x/c を正規化し eps を c² で割る形に前処理しています。
|
| 102 |
- 実際の状態の大きさは CPU(FP32)で確かめています(畳み込みの状態の最大値は 14 で、FP16 の限界からは遠い)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
## 画像エンコーダーまで NPU に載せた経緯
|
| 105 |
|
|
|
|
| 100 |
- **大きすぎる値の前処理**:1024 倍した出力を複数トークンのブロックで RMSNorm に入れると、今度は二乗和が FP16 で
|
| 101 |
溢れます。x/c を正規化し eps を c² で割る形に前処理しています。
|
| 102 |
- 実際の状態の大きさは CPU(FP32)で確かめています(畳み込みの状態の最大値は 14 で、FP16 の限界からは遠い)。
|
| 103 |
+
- **量子化に敏感なモデル(onw 0.12、Ornith-1.5-35B-A3B)**:Qwen3.6-35B-A3B の追加学習版ですが、全重みグループ 128 の INT4
|
| 104 |
+
では最初のトークンを外しました(NPU でなく CPU の FP32 でも同じ)。bf16 のモデルと層ごとに比べると、層 0 の MoE 入力で
|
| 105 |
+
すでに 22% ずれていて、その大半が DeltaNet の `out_proj` と `in_proj_qkv` でした(グループ 32 の INT4 でも 19%)。この 2 つ
|
| 106 |
+
だけを INT8 にすると回答の終わりまで一致します。INT8 は INT4 と同じ **グループ 128 の形(q8g128)** にしています。同じ重みを
|
| 107 |
+
チャネル単位 INT8 にすると 1 トークンの区間が 157 → 364 ms と倍以上遅くなりましたが、q8g128 なら 195 ms で済みます
|
| 108 |
+
(`onw convert --deltanet-fmt q8g128`)。
|
| 109 |
+
35B-A3B は、これとは別に NPU だけで出るずれ(改行などのトークンで残差がとても小さく、RMSNorm の二乗が FP16 の
|
| 110 |
+
非正規化数に落ちる)を調査中のため、まだモデル一覧には載せていません。
|
| 111 |
|
| 112 |
## 画像エンコーダーまで NPU に載せた経緯
|
| 113 |
|
TECHNICAL_en.md
CHANGED
|
@@ -99,6 +99,14 @@ attention 3:1. The NPU computes in fp16, which needed:
|
|
| 99 |
- **Prescaling large values**: with that 1024x output, multi-token blocks overflow the RMSNorm's sum of squares in
|
| 100 |
fp16, so the norm is prescaled (normalize x / c with eps / c²).
|
| 101 |
- The real state magnitudes were checked on the CPU in fp32 (conv state max 14, far from fp16 limits).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
## How the vision encoders got onto the NPU
|
| 104 |
|
|
|
|
| 99 |
- **Prescaling large values**: with that 1024x output, multi-token blocks overflow the RMSNorm's sum of squares in
|
| 100 |
fp16, so the norm is prescaled (normalize x / c with eps / c²).
|
| 101 |
- The real state magnitudes were checked on the CPU in fp32 (conv state max 14, far from fp16 limits).
|
| 102 |
+
- **A quantization-sensitive model (onw 0.12, Ornith-1.5-35B-A3B)**: a Qwen3.6-35B-A3B fine-tune, but with every weight
|
| 103 |
+
in group-128 INT4 it missed the first token (on the CPU in fp32 too, not just the NPU). Compared layer by layer with
|
| 104 |
+
the bf16 model, layer 0's MoE input was already 22% off, most of it from the DeltaNet `out_proj` and `in_proj_qkv`
|
| 105 |
+
(group-32 INT4 still 19%). With just those two in INT8 it matches to the end of the answer. The INT8 uses INT4's
|
| 106 |
+
**group-128 layout (q8g128)**: the same weights in per-channel INT8 made the 1-token segments 157 -> 364 ms, q8g128
|
| 107 |
+
195 ms (`onw convert --deltanet-fmt q8g128`). The
|
| 108 |
+
35B-A3B is not in the model list yet: a separate NPU-only drift (line-break tokens have a tiny residual whose squares
|
| 109 |
+
in RMSNorm fall into fp16's subnormal range) is still being worked on.
|
| 110 |
|
| 111 |
## How the vision encoders got onto the NPU
|
| 112 |
|
onw/__init__.py
CHANGED
|
@@ -2,4 +2,4 @@
|
|
| 2 |
|
| 3 |
A small engine that runs LLMs entirely on the Intel NPU: static segment graphs + host-managed state.
|
| 4 |
"""
|
| 5 |
-
__version__ = "0.
|
|
|
|
| 2 |
|
| 3 |
A small engine that runs LLMs entirely on the Intel NPU: static segment graphs + host-managed state.
|
| 4 |
"""
|
| 5 |
+
__version__ = "0.12.0"
|
onw/app.py
CHANGED
|
@@ -16,6 +16,8 @@ CATALOG = [ # repo, title, takes images, download GB, decode speed (NPU 3720),
|
|
| 16 |
("ryugyosoft/Qwen3.6-REAP-18B-A3B-onw", "Qwen3.6-REAP-18B-A3B", True, 9.6, "6.8 tok/s", "35B MoE を半分に刈り込み"),
|
| 17 |
("ryugyosoft/Qwen3.6-35B-A3B-onw", "Qwen3.6-35B-A3B", True, 17.1, "6.8 tok/s",
|
| 18 |
"最大(メモリが足りない PC ではエキスパートを SSD から読みます)"),
|
|
|
|
|
|
|
| 19 |
("ryugyosoft/gpt-oss-20b-onw", "gpt-oss-20b", False, 11.4, "8.5 tok/s", "OpenAI の MoE。考えてから答えます(思考モードで思考を表示)"),
|
| 20 |
("ryugyosoft/gpt-oss-20b-int8-onw", "gpt-oss-20b (INT8)", False, 21.0, "6 tok/s",
|
| 21 |
"gpt-oss-20b の高精度版(エキスパート INT8。メモリ 24 GB 以上向け)"),
|
|
@@ -30,6 +32,7 @@ MEMORY = {
|
|
| 30 |
"ryugyosoft/Qwen3.5-9B-onw": (10.9, 0.0, 16),
|
| 31 |
"ryugyosoft/Qwen3.6-REAP-18B-A3B-onw": (12.3, 0.0, 10),
|
| 32 |
"ryugyosoft/Qwen3.6-35B-A3B-onw": (19.9, 0.0, 10),
|
|
|
|
| 33 |
"ryugyosoft/gpt-oss-20b-onw": (12.4, 0.0, 12),
|
| 34 |
"ryugyosoft/gpt-oss-20b-int8-onw": (20.6, 0.0, 12),
|
| 35 |
}
|
|
|
|
| 16 |
("ryugyosoft/Qwen3.6-REAP-18B-A3B-onw", "Qwen3.6-REAP-18B-A3B", True, 9.6, "6.8 tok/s", "35B MoE を半分に刈り込み"),
|
| 17 |
("ryugyosoft/Qwen3.6-35B-A3B-onw", "Qwen3.6-35B-A3B", True, 17.1, "6.8 tok/s",
|
| 18 |
"最大(メモリが足りない PC ではエキスパートを SSD から読みます)"),
|
| 19 |
+
("ryugyosoft/Ornith-1.5-9B-onw", "Ornith-1.5-9B", True, 5.3, "5 tok/s",
|
| 20 |
+
"エージェント・ツール呼び出し向け(Qwen3.5-9B ベース)"),
|
| 21 |
("ryugyosoft/gpt-oss-20b-onw", "gpt-oss-20b", False, 11.4, "8.5 tok/s", "OpenAI の MoE。考えてから答えます(思考モードで思考を表示)"),
|
| 22 |
("ryugyosoft/gpt-oss-20b-int8-onw", "gpt-oss-20b (INT8)", False, 21.0, "6 tok/s",
|
| 23 |
"gpt-oss-20b の高精度版(エキスパート INT8。メモリ 24 GB 以上向け)"),
|
|
|
|
| 32 |
"ryugyosoft/Qwen3.5-9B-onw": (10.9, 0.0, 16),
|
| 33 |
"ryugyosoft/Qwen3.6-REAP-18B-A3B-onw": (12.3, 0.0, 10),
|
| 34 |
"ryugyosoft/Qwen3.6-35B-A3B-onw": (19.9, 0.0, 10),
|
| 35 |
+
"ryugyosoft/Ornith-1.5-9B-onw": (10.9, 0.0, 16), # the shape of Qwen3.5-9B
|
| 36 |
"ryugyosoft/gpt-oss-20b-onw": (12.4, 0.0, 12),
|
| 37 |
"ryugyosoft/gpt-oss-20b-int8-onw": (20.6, 0.0, 12),
|
| 38 |
}
|
onw/chat.py
CHANGED
|
@@ -224,7 +224,11 @@ class ChatEngine:
|
|
| 224 |
"""-> token ids (image placeholders expanded), B = end of the conversation before the generation prompt,
|
| 225 |
images in order of appearance. Messages may carry tool_calls / tool_call_id / name (tool use)."""
|
| 226 |
images, tmpl = [], []
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
for m in messages:
|
| 229 |
c = m["content"]
|
| 230 |
if isinstance(c, str) or c is None:
|
|
|
|
| 224 |
"""-> token ids (image placeholders expanded), B = end of the conversation before the generation prompt,
|
| 225 |
images in order of appearance. Messages may carry tool_calls / tool_call_id / name (tool use)."""
|
| 226 |
images, tmpl = [], []
|
| 227 |
+
# an earlier turn's thinking goes back to the template (Qwen keeps the latest turn's, Ornith every turn's -
|
| 228 |
+
# agents rely on it); gpt-oss's harmony template calls it "thinking"
|
| 229 |
+
rc = "thinking" if self.think_style == "harmony" else "reasoning_content"
|
| 230 |
+
extra = lambda m: {**{k: m[k] for k in ("tool_calls", "tool_call_id", "name") if m.get(k) is not None},
|
| 231 |
+
**({rc: m["reasoning_content"]} if m.get("reasoning_content") else {})}
|
| 232 |
for m in messages:
|
| 233 |
c = m["content"]
|
| 234 |
if isinstance(c, str) or c is None:
|
onw/convert.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""Convert a Hugging Face checkpoint into an onw model directory.
|
| 2 |
-
usage: onw convert HF_DIR OUT_DIR [--sizes 1,16] [--T 1024] [--fmt q4g128|q8] [--expert-bits 4|8] [--graphs-only]
|
| 3 |
[--compact] [--prune SALIENCY.json --keep 0.5]"""
|
| 4 |
import argparse, importlib, inspect, json, os, shutil, time
|
| 5 |
|
|
@@ -17,6 +17,8 @@ def main(argv=None):
|
|
| 17 |
ap.add_argument("--sizes", default="1,16", help="compiled block sizes (1 = decoding, largest = prompt blocks)")
|
| 18 |
ap.add_argument("--T", type=int, default=1024, help="static context length")
|
| 19 |
ap.add_argument("--fmt", default=None, help="non-expert weights: q4g128 | q8 (default: the architecture's, q4g128 or q8)")
|
|
|
|
|
|
|
| 20 |
ap.add_argument("--graphs-only", action="store_true", help="rebuild the segment graphs, keep the weight files")
|
| 21 |
ap.add_argument("--expert-bits", type=int, choices=(4, 8), default=None,
|
| 22 |
help="MoE expert bank precision (gpt-oss: 4 = 9.6 GB, fits 16 GB machines; 8 = 19 GB, closer to bf16)")
|
|
@@ -40,11 +42,14 @@ def main(argv=None):
|
|
| 40 |
raise SystemExit(f"--prune is not supported for {arch}")
|
| 41 |
from onw import prune
|
| 42 |
prune.SETTING = (a.prune, a.keep)
|
|
|
|
|
|
|
| 43 |
if a.expert_bits and "bits" not in takes:
|
| 44 |
raise SystemExit(f"--expert-bits is not supported for {arch}")
|
| 45 |
t0 = time.time()
|
| 46 |
mod.export(a.src, a.out, sizes=tuple(int(x) for x in a.sizes.split(",")), T=a.T, graphs_only=a.graphs_only,
|
| 47 |
-
**({"fmt": a.fmt} if a.fmt else {}), **({"bits": a.expert_bits} if a.expert_bits else {})
|
|
|
|
| 48 |
for f in COPY:
|
| 49 |
if os.path.exists(os.path.join(a.src, f)):
|
| 50 |
shutil.copy(os.path.join(a.src, f), os.path.join(a.out, f))
|
|
|
|
| 1 |
"""Convert a Hugging Face checkpoint into an onw model directory.
|
| 2 |
+
usage: onw convert HF_DIR OUT_DIR [--sizes 1,16] [--T 1024] [--fmt q4g128|q8] [--deltanet-fmt q8g128] [--expert-bits 4|8] [--graphs-only]
|
| 3 |
[--compact] [--prune SALIENCY.json --keep 0.5]"""
|
| 4 |
import argparse, importlib, inspect, json, os, shutil, time
|
| 5 |
|
|
|
|
| 17 |
ap.add_argument("--sizes", default="1,16", help="compiled block sizes (1 = decoding, largest = prompt blocks)")
|
| 18 |
ap.add_argument("--T", type=int, default=1024, help="static context length")
|
| 19 |
ap.add_argument("--fmt", default=None, help="non-expert weights: q4g128 | q8 (default: the architecture's, q4g128 or q8)")
|
| 20 |
+
ap.add_argument("--deltanet-fmt", default=None,
|
| 21 |
+
help="Qwen3.5 / 3.6: DeltaNet in_proj_qkv / out_proj weights, e.g. q8g128 (Ornith-1.5-35B-A3B)")
|
| 22 |
ap.add_argument("--graphs-only", action="store_true", help="rebuild the segment graphs, keep the weight files")
|
| 23 |
ap.add_argument("--expert-bits", type=int, choices=(4, 8), default=None,
|
| 24 |
help="MoE expert bank precision (gpt-oss: 4 = 9.6 GB, fits 16 GB machines; 8 = 19 GB, closer to bf16)")
|
|
|
|
| 42 |
raise SystemExit(f"--prune is not supported for {arch}")
|
| 43 |
from onw import prune
|
| 44 |
prune.SETTING = (a.prune, a.keep)
|
| 45 |
+
if a.deltanet_fmt and "dn_fmt" not in takes:
|
| 46 |
+
raise SystemExit(f"--deltanet-fmt is not supported for {arch}")
|
| 47 |
if a.expert_bits and "bits" not in takes:
|
| 48 |
raise SystemExit(f"--expert-bits is not supported for {arch}")
|
| 49 |
t0 = time.time()
|
| 50 |
mod.export(a.src, a.out, sizes=tuple(int(x) for x in a.sizes.split(",")), T=a.T, graphs_only=a.graphs_only,
|
| 51 |
+
**({"fmt": a.fmt} if a.fmt else {}), **({"bits": a.expert_bits} if a.expert_bits else {}),
|
| 52 |
+
**({"dn_fmt": a.deltanet_fmt} if a.deltanet_fmt else {}))
|
| 53 |
for f in COPY:
|
| 54 |
if os.path.exists(os.path.join(a.src, f)):
|
| 55 |
shutil.copy(os.path.join(a.src, f), os.path.join(a.out, f))
|
onw/i18n.json
CHANGED
|
@@ -61,6 +61,8 @@
|
|
| 61 |
"アンロード(サーバー停止)": "Unload (stop the server)",
|
| 62 |
"エキスパートのメモリ": "Expert memory",
|
| 63 |
"エラー: {0}": "Error: {0}",
|
|
|
|
|
|
|
| 64 |
"キャッシュを削除({0} GB)": "Delete the compile cache ({0} GB)",
|
| 65 |
"キャンセル": "Cancel",
|
| 66 |
"コピー": "Copy",
|
|
@@ -290,6 +292,8 @@
|
|
| 290 |
"アンロード(サーバー停止)": "卸载(停止服务器)",
|
| 291 |
"エキスパートのメモリ": "专家内存",
|
| 292 |
"エラー: {0}": "错误:{0}",
|
|
|
|
|
|
|
| 293 |
"キャッシュを削除({0} GB)": "删除编译缓存({0} GB)",
|
| 294 |
"キャンセル": "取消",
|
| 295 |
"コピー": "复制",
|
|
|
|
| 61 |
"アンロード(サーバー停止)": "Unload (stop the server)",
|
| 62 |
"エキスパートのメモリ": "Expert memory",
|
| 63 |
"エラー: {0}": "Error: {0}",
|
| 64 |
+
"エージェント・ツール呼び出し向け": "For agents and tool calls",
|
| 65 |
+
"エージェント・ツール呼び出し向け(Qwen3.5-9B ベース)": "For agents and tool calls (based on Qwen3.5-9B)",
|
| 66 |
"キャッシュを削除({0} GB)": "Delete the compile cache ({0} GB)",
|
| 67 |
"キャンセル": "Cancel",
|
| 68 |
"コピー": "Copy",
|
|
|
|
| 292 |
"アンロード(サーバー停止)": "卸载(停止服务器)",
|
| 293 |
"エキスパートのメモリ": "专家内存",
|
| 294 |
"エラー: {0}": "错误:{0}",
|
| 295 |
+
"エージェント・ツール呼び出し向け": "面向智能体与工具调用",
|
| 296 |
+
"エージェント・ツール呼び出し向け(Qwen3.5-9B ベース)": "面向智能体与工具调用(基于 Qwen3.5-9B)",
|
| 297 |
"キャッシュを削除({0} GB)": "删除编译缓存({0} GB)",
|
| 298 |
"キャンセル": "取消",
|
| 299 |
"コピー": "复制",
|
onw/ir.py
CHANGED
|
@@ -66,7 +66,7 @@ def quant_i8(w):
|
|
| 66 |
|
| 67 |
def weight(w, fmt="q4g128"):
|
| 68 |
"""Constant weight subgraph for w [out, in] (float array) -> f32 node [out, in].
|
| 69 |
-
fmt: q4g<N> (INT4 sym, group N), q8 (INT8 per channel), f16."""
|
| 70 |
w = np.asarray(w, np.float32)
|
| 71 |
o, i = w.shape
|
| 72 |
if fmt == "f16":
|
|
@@ -83,6 +83,18 @@ def weight(w, fmt="q4g128"):
|
|
| 83 |
c = ops.constant(pack_i4(q.reshape(o, i // g, g)))
|
| 84 |
d = ops.multiply(ops.convert(c, ov.Type.f16), ops.constant(s[..., None]))
|
| 85 |
return ops.convert(ops.reshape(d, i64([o, i]), False), F32)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
raise ValueError(fmt)
|
| 87 |
|
| 88 |
|
|
|
|
| 66 |
|
| 67 |
def weight(w, fmt="q4g128"):
|
| 68 |
"""Constant weight subgraph for w [out, in] (float array) -> f32 node [out, in].
|
| 69 |
+
fmt: q4g<N> (INT4 sym, group N), q8g<N> (INT8 sym, group N), q8 (INT8 per channel), f16."""
|
| 70 |
w = np.asarray(w, np.float32)
|
| 71 |
o, i = w.shape
|
| 72 |
if fmt == "f16":
|
|
|
|
| 83 |
c = ops.constant(pack_i4(q.reshape(o, i // g, g)))
|
| 84 |
d = ops.multiply(ops.convert(c, ov.Type.f16), ops.constant(s[..., None]))
|
| 85 |
return ops.convert(ops.reshape(d, i64([o, i]), False), F32)
|
| 86 |
+
if fmt.startswith("q8g"):
|
| 87 |
+
# INT8 in q4g's grouped layout: on NPU 3720 a 1-token Qwen3.5 segment with its DeltaNet qkv / out weights in
|
| 88 |
+
# q8g128 took 195 ms against 157 ms all-q4g128, the same weights in per-channel q8 364 ms
|
| 89 |
+
g = int(fmt[3:])
|
| 90 |
+
while i % g:
|
| 91 |
+
g //= 2
|
| 92 |
+
x = w.reshape(o, i // g, g)
|
| 93 |
+
sc = np.abs(x).max(-1, keepdims=True) / 127.0
|
| 94 |
+
sc[sc == 0] = 1e-8
|
| 95 |
+
q = np.clip(np.round(x / sc), -127, 127).astype(np.int8)
|
| 96 |
+
d = ops.multiply(ops.convert(ops.constant(q), ov.Type.f16), ops.constant(sc.astype(np.float16)))
|
| 97 |
+
return ops.convert(ops.reshape(d, i64([o, i]), False), F32)
|
| 98 |
raise ValueError(fmt)
|
| 99 |
|
| 100 |
|
onw/models/qwen3_5_moe.py
CHANGED
|
@@ -37,7 +37,8 @@ def rotary(cfg):
|
|
| 37 |
return int(cfg["head_dim"] * frac), rp.get("rope_theta", cfg.get("rope_theta", 10000.0))
|
| 38 |
|
| 39 |
|
| 40 |
-
def build_segments(cfg, W, S, T, fmt="q4g128"):
|
|
|
|
| 41 |
H, E, K, I = cfg["hidden_size"], cfg.get("num_experts", 0), cfg.get("num_experts_per_tok", 0), cfg.get("moe_intermediate_size")
|
| 42 |
nh, nkv, D = cfg["num_attention_heads"], cfg["num_key_value_heads"], cfg["head_dim"]
|
| 43 |
rot = rotary(cfg)[0]
|
|
@@ -53,7 +54,7 @@ def build_segments(cfg, W, S, T, fmt="q4g128"):
|
|
| 53 |
|
| 54 |
def deltanet(l, x, params, outs, shared):
|
| 55 |
a = f"{P}layers.{l}.linear_attn."
|
| 56 |
-
qkv = ir.linear(x, W[a + "in_proj_qkv.weight"], fmt)
|
| 57 |
C = 2 * kh * dk + vh * dv
|
| 58 |
z = ops.reshape(ir.linear(x, W[a + "in_proj_z.weight"], fmt), i64([1, S, vh, dv]), False)
|
| 59 |
b = ir.linear(x, W[a + "in_proj_b.weight"], "f16") # [1,S,vh]
|
|
@@ -96,7 +97,7 @@ def build_segments(cfg, W, S, T, fmt="q4g128"):
|
|
| 96 |
outs[f"ssm_new.{l}"] = s1
|
| 97 |
o = ops.transpose(o, i64([0, 2, 1, 3])) # [1,S,vh,dv]
|
| 98 |
o = ops.multiply(ir.rmsnorm(o, W[a + "norm.weight"], eps * QSCALE ** 2, prescale=QSCALE / 16), ir.silu(z))
|
| 99 |
-
return ir.linear(ops.reshape(o, i64([1, S, vh * dv]), False), W[a + "out_proj.weight"], fmt)
|
| 100 |
|
| 101 |
def attention(l, x, params, outs, shared):
|
| 102 |
a = f"{P}layers.{l}.self_attn."
|
|
@@ -216,18 +217,23 @@ def export_weights(cfg, W, out):
|
|
| 216 |
common.export_embed_head(W, out, f"{P}embed_tokens.weight", "lm_head.weight")
|
| 217 |
|
| 218 |
|
| 219 |
-
def export(src, out, sizes=(1, 16), T=1024, fmt="q4g128", graphs_only=False):
|
|
|
|
|
|
|
|
|
|
| 220 |
os.makedirs(out, exist_ok=True)
|
| 221 |
from ..prune import maybe_prune
|
| 222 |
common.restore_prune(out, graphs_only)
|
| 223 |
W, cfg = maybe_prune(Checkpoint(src), text_config(src))
|
| 224 |
-
metas = common.save_segments(lambda S: build_segments(cfg, W, S, T, fmt), out, sizes)
|
| 225 |
rp = cfg.get("rope_parameters") or {}
|
| 226 |
rot, theta = rotary(cfg)
|
| 227 |
full = json.load(open(os.path.join(src, "config.json")))
|
| 228 |
meta = {"arch": full["model_type"], "T": T, "sizes": list(sizes), "config": cfg, "segments": metas,
|
| 229 |
"rope": {"dim": rot, "theta": theta,
|
| 230 |
"mrope_section": rp.get("mrope_section"), "mrope_interleaved": rp.get("mrope_interleaved", False)}}
|
|
|
|
|
|
|
| 231 |
if common.prune_meta():
|
| 232 |
meta["prune"] = common.prune_meta()
|
| 233 |
if "vision_config" in full and not graphs_only: # static vision tower, fixed 512x512
|
|
|
|
| 37 |
return int(cfg["head_dim"] * frac), rp.get("rope_theta", cfg.get("rope_theta", 10000.0))
|
| 38 |
|
| 39 |
|
| 40 |
+
def build_segments(cfg, W, S, T, fmt="q4g128", dn_fmt=None):
|
| 41 |
+
"""dn_fmt: format of the DeltaNet in_proj_qkv / out_proj weights (default fmt); see export()."""
|
| 42 |
H, E, K, I = cfg["hidden_size"], cfg.get("num_experts", 0), cfg.get("num_experts_per_tok", 0), cfg.get("moe_intermediate_size")
|
| 43 |
nh, nkv, D = cfg["num_attention_heads"], cfg["num_key_value_heads"], cfg["head_dim"]
|
| 44 |
rot = rotary(cfg)[0]
|
|
|
|
| 54 |
|
| 55 |
def deltanet(l, x, params, outs, shared):
|
| 56 |
a = f"{P}layers.{l}.linear_attn."
|
| 57 |
+
qkv = ir.linear(x, W[a + "in_proj_qkv.weight"], dn_fmt or fmt) # [1,S,2*kh*dk + vh*dv]
|
| 58 |
C = 2 * kh * dk + vh * dv
|
| 59 |
z = ops.reshape(ir.linear(x, W[a + "in_proj_z.weight"], fmt), i64([1, S, vh, dv]), False)
|
| 60 |
b = ir.linear(x, W[a + "in_proj_b.weight"], "f16") # [1,S,vh]
|
|
|
|
| 97 |
outs[f"ssm_new.{l}"] = s1
|
| 98 |
o = ops.transpose(o, i64([0, 2, 1, 3])) # [1,S,vh,dv]
|
| 99 |
o = ops.multiply(ir.rmsnorm(o, W[a + "norm.weight"], eps * QSCALE ** 2, prescale=QSCALE / 16), ir.silu(z))
|
| 100 |
+
return ir.linear(ops.reshape(o, i64([1, S, vh * dv]), False), W[a + "out_proj.weight"], dn_fmt or fmt)
|
| 101 |
|
| 102 |
def attention(l, x, params, outs, shared):
|
| 103 |
a = f"{P}layers.{l}.self_attn."
|
|
|
|
| 217 |
common.export_embed_head(W, out, f"{P}embed_tokens.weight", "lm_head.weight")
|
| 218 |
|
| 219 |
|
| 220 |
+
def export(src, out, sizes=(1, 16), T=1024, fmt="q4g128", graphs_only=False, dn_fmt=None):
|
| 221 |
+
"""dn_fmt="q8g128": Ornith-1.5-35B-A3B (a Qwen3.6-35B-A3B fine-tune) loses its first answer token with every
|
| 222 |
+
weight in q4g128 (the MoE input of layer 0 is 22% off the bf16 model's, the DeltaNet out_proj and in_proj_qkv
|
| 223 |
+
being most of it); with those two in INT8 it matches the bf16 model to the end of the answer."""
|
| 224 |
os.makedirs(out, exist_ok=True)
|
| 225 |
from ..prune import maybe_prune
|
| 226 |
common.restore_prune(out, graphs_only)
|
| 227 |
W, cfg = maybe_prune(Checkpoint(src), text_config(src))
|
| 228 |
+
metas = common.save_segments(lambda S: build_segments(cfg, W, S, T, fmt, dn_fmt), out, sizes)
|
| 229 |
rp = cfg.get("rope_parameters") or {}
|
| 230 |
rot, theta = rotary(cfg)
|
| 231 |
full = json.load(open(os.path.join(src, "config.json")))
|
| 232 |
meta = {"arch": full["model_type"], "T": T, "sizes": list(sizes), "config": cfg, "segments": metas,
|
| 233 |
"rope": {"dim": rot, "theta": theta,
|
| 234 |
"mrope_section": rp.get("mrope_section"), "mrope_interleaved": rp.get("mrope_interleaved", False)}}
|
| 235 |
+
if dn_fmt:
|
| 236 |
+
meta["deltanet_fmt"] = dn_fmt
|
| 237 |
if common.prune_meta():
|
| 238 |
meta["prune"] = common.prune_meta()
|
| 239 |
if "vision_config" in full and not graphs_only: # static vision tower, fixed 512x512
|
onw/server.py
CHANGED
|
@@ -136,7 +136,7 @@ def to_hf_messages(messages):
|
|
| 136 |
"""OpenAI chat messages -> HF chat format with PIL images."""
|
| 137 |
out = []
|
| 138 |
for m in tool_calls.history(messages):
|
| 139 |
-
keep = {k: m[k] for k in ("tool_calls", "tool_call_id", "name") if m.get(k) is not None}
|
| 140 |
c = m.get("content")
|
| 141 |
if c is None or isinstance(c, str):
|
| 142 |
out.append({"role": m["role"], "content": c or "", **keep})
|
|
|
|
| 136 |
"""OpenAI chat messages -> HF chat format with PIL images."""
|
| 137 |
out = []
|
| 138 |
for m in tool_calls.history(messages):
|
| 139 |
+
keep = {k: m[k] for k in ("tool_calls", "tool_call_id", "name", "reasoning_content") if m.get(k) is not None}
|
| 140 |
c = m.get("content")
|
| 141 |
if c is None or isinstance(c, str):
|
| 142 |
out.append({"role": m["role"], "content": c or "", **keep})
|
pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "onw"
|
| 7 |
-
version = "0.
|
| 8 |
description = "Ore no NPU ga konna ni ugoku wake nai - LLMs entirely on the Intel NPU"
|
| 9 |
readme = "README.md"
|
| 10 |
license = {text = "Apache-2.0"}
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "onw"
|
| 7 |
+
version = "0.12.0"
|
| 8 |
description = "Ore no NPU ga konna ni ugoku wake nai - LLMs entirely on the Intel NPU"
|
| 9 |
readme = "README.md"
|
| 10 |
license = {text = "Apache-2.0"}
|