ryugyosoft commited on
Commit
94fb10d
·
verified ·
1 Parent(s): 9d28207

onw 0.12.0: Ornith-1.5-9B, earlier turns' reasoning_content to the template, --deltanet-fmt q8g128

Browse files
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.11.3"
 
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
- extra = lambda m: {k: m[k] for k in ("tool_calls", "tool_call_id", "name") if m.get(k) is not None}
 
 
 
 
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) # [1,S,2*kh*dk + vh*dv]
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.11.3"
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"}