onw 3.0.0: onw NPU kit (Windows and Linux) - weightless graphs, shared weights, compiler fixes; fused graphs on by default from Lunar Lake on, up to 32K positions
Browse files- .gitattributes +4 -0
- CHANGELOG.md +19 -0
- CHANGELOG_en.md +19 -0
- README.md +14 -23
- README_en.md +16 -25
- TECHNICAL.md +46 -0
- TECHNICAL_en.md +46 -0
- install.ps1 +1 -0
- install.sh +1 -0
- onw/__init__.py +1 -1
- onw/app.py +25 -5
- onw/chat.py +12 -5
- onw/fused.py +60 -16
- onw/manager.py +686 -685
- onw/models/gemma4.py +136 -135
- onw/npukit.py +122 -0
- onw/npukit/LICENSE +201 -0
- onw/npukit/NOTICE.txt +16 -0
- onw/npukit/kit.json +19 -0
- onw/npukit/linux/npukit-linux.tar.xz +3 -0
- onw/npukit/openvino_intel_npu_compiler.dll +3 -0
- onw/npukit/openvino_intel_npu_compiler_loader.dll +3 -0
- onw/npukit/openvino_intel_npu_plugin.dll +3 -0
- onw/npukit/openvino_intel_npu_vm_runtime.dll +3 -0
- onw/npukit/patches/npu_compiler-20e7912.patch +1455 -0
- onw/npukit/patches/openvino-6db412d-npu_plugin.patch +186 -0
- onw/npukit/patches/vpucostmodel.patch +17 -0
- onw/npukit/plugins.xml +6 -0
- onw/precompile.py +20 -19
- onw/probe.py +60 -59
- onw/runtime.py +158 -2
- onw/server.py +749 -748
- onw/update.py +26 -1
- pyproject.toml +4 -3
- requirements.txt +1 -1
.gitattributes
CHANGED
|
@@ -45,3 +45,7 @@ images/onw-models.png filter=lfs diff=lfs merge=lfs -text
|
|
| 45 |
images/onw-settings-speed-en.png filter=lfs diff=lfs merge=lfs -text
|
| 46 |
images/onw-settings-speed.png filter=lfs diff=lfs merge=lfs -text
|
| 47 |
images/onw-server.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
images/onw-settings-speed-en.png filter=lfs diff=lfs merge=lfs -text
|
| 46 |
images/onw-settings-speed.png filter=lfs diff=lfs merge=lfs -text
|
| 47 |
images/onw-server.png filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
onw/npukit/openvino_intel_npu_compiler.dll filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
onw/npukit/openvino_intel_npu_compiler_loader.dll filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
onw/npukit/openvino_intel_npu_plugin.dll filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
onw/npukit/openvino_intel_npu_vm_runtime.dll filter=lfs diff=lfs merge=lfs -text
|
CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@
|
|
| 5 |
日付は日本時間です。各版は Hugging Face の `ryugyosoft/onw` にタグ(`v0.4.2` 以降)付きで残っています。
|
| 6 |
onw ウィンドウは、新しい版があるとこのファイルから変更点を表示します(`## v…` の見出しで区切っています)。
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
## v2.1.0 (2026-10-18)
|
| 9 |
- **Lunar Lake 以降専用の NF4 版を 3 つ追加:Granite 4.2 3B/8B、llm-jp-4.1-8B-thinking**(ryugyosoft/granite-4.2-3b-nf4-onw、granite-4.2-8b-nf4-onw、llm-jp-4.1-8b-thinking-nf4-onw)。重みはチャネル単位の NF4 を基本に、誤差の出やすいアテンションの入力(q/k/v_proj)と down_proj だけをチャネル単位の INT8 にしました。
|
| 10 |
- Panther Lake(Core Ultra X7 358H):Granite 4.2 8B は生成 5.4 → 約 9 tok/s(INT4 版・2.0.3 との比較)、約 900 トークンのプロンプト約 160 tok/s。llm-jp-4.1-8B は生成約 9.9 tok/s・プロンプト約 170 tok/s、Granite 4.2 3B は生成約 14.3 tok/s・プロンプト約 235 tok/s。1 トークンあたりの電力量(パッケージ全体)は 8B で約 1.3 J、3B で約 0.7 J。
|
|
|
|
| 5 |
日付は日本時間です。各版は Hugging Face の `ryugyosoft/onw` にタグ(`v0.4.2` 以降)付きで残っています。
|
| 6 |
onw ウィンドウは、新しい版があるとこのファイルから変更点を表示します(`## v…` の見出しで区切っています)。
|
| 7 |
|
| 8 |
+
## v3.0.0 (2026-10-19)
|
| 9 |
+
- **onw 独自の NPU キット(Windows・Linux)を同梱:NPU のコンパイラーとプラグインを、Intel の公開ソースから onw 用に作り直したもの**(`onw/npukit`、Apache 2.0、変更点は `onw/npukit/patches`)。onw が入れる OpenVINO 2026.5.0 のときだけ使い、インストール済みの OpenVINO には手を加えません(`ONW_NPUKIT=0` で OpenVINO 自身のものに戻せます)。これにより次のことができるようになりました。
|
| 10 |
+
- **重みを持たないコンパイル済みグラフ**:キャッシュにはグラフだけを置き、重みはロードのたびにモデルのファイルから NPU 上で組み立てます。キャッシュがモデル本体の 1/10 以下になります(Granite 4.2 8B NF4:10.5 → 0.8 GB、llm-jp-4.1-8B NF4:9.2 → 0.65 GB、Granite 4.2 3B NF4:4.2 → 0.5 GB)。OpenVINO 2026.5 のコンパイラーでは NF4 の重みでこれができませんでした。
|
| 11 |
+
- **同じ重みを NPU 上で 1 つだけ持つ**:グラフの種類(ブロックサイズ、文脈の長さごとのグラフ)がいくつあっても、重みは 1 組です。
|
| 12 |
+
- コンパイラーの修正:Qwen3.5 系 NF4 の 4 トークンの検証グラフ(MTP 先読み)、2 ビットの重み(Bonsai)を 2 行以上で計算すると 0 になる不具合、コンパイルの高速化(Panther Lake で Ternary Bonsai 2 9B の初回のロード約 7.5 分、生成約 17 tok/s など)。
|
| 13 |
+
- **Lunar Lake 以降の PC(Windows・Ubuntu)で、融合グラフを既定で使うように**(2.1.0 では試験的・既定オフ)。区間ごとに NPU を 1 回呼ぶだけにして、生成もプロンプトも速くなります。重みを共有するので、メモリは増えず、むしろ減ります。Panther Lake(Core Ultra X7 358H)、2.1.0 の既定との比較:
|
| 14 |
+
- Granite 4.2 8B NF4:生成 約 9 → 約 13 tok/s、約 900 トークンのプロンプト 約 160 → 約 400 tok/s、専用メモリ 11.4 → 約 1.6 GB(ピーク 12.5 → 約 9.8 GB)、1 トークンあたりの電力量 約 1.3 → 約 0.9 J。
|
| 15 |
+
- llm-jp-4.1-8B NF4:生成 約 9.9 → 約 14 tok/s、プロンプト 約 170 → 約 440 tok/s、専用メモリ 11.0 → 約 2.4 GB。
|
| 16 |
+
- Lunar Lake(Ubuntu、16 GB)の Granite 4.2 8B NF4:生成 11〜13 tok/s、約 900 トークンのプロンプト約 385 tok/s、専用メモリ約 2 GB(ピーク 3.3 GB)、キャッシュ 1.8 GB、初回のロード 3.5 分(残りの融合グラフは裏で約 14 分)。
|
| 17 |
+
- Granite 4.2 3B NF4:生成 約 14.3 → 約 23 tok/s、プロンプト 約 235 → 約 730 tok/s、専用メモリ 5.1 → 約 1.4 GB。
|
| 18 |
+
- **長い会話も速く**:融合グラフは文脈の長さまで 8K・16K・32K 位置の種類も作ります(2.1.0 は 4K 位置まで、その先は従来の経路)。Granite 4.2 3B NF4、約 7.3K/15K/22K トークンのプロンプト(従来の経路との比較):生成 4.6 → 13.6/4.0 → 8.8/2.6 → 4.9 tok/s、プロンプト 94 → 170/62 → 146/43 → 74 tok/s。
|
| 19 |
+
- 文脈の長さごとのグラフ(1K〜32K 位置)をすべて読み込んだままにするので、会話の途中で止まらず、同じ質問には同じ答えが返ります。初回のロードは 2.1.0 とほぼ同じで(8B で約 10〜12 分)、残りのグラフは回答しながら裏でコンパイルします。
|
| 20 |
+
- 省メモリモードでは、今までどおり使う長さのグラフだけを読み込みます。
|
| 21 |
+
- Qwen3.5 系・Ornith・gemma-4(NPUW で動くモデル)もキャッシュが小さくなります(Qwen3.5-9B NF4:約 0.3 GB)。Qwen3.5-9B NF4 の生成は MTP 先読み付きで約 14〜19 tok/s。
|
| 22 |
+
- **Ubuntu でもキットを使います**(`onw/npukit/linux`、32 MB の圧縮ファイルを最初の起動で展開)。Lunar Lake(16 GB)の Qwen3.5-9B NF4:初回のロード約 2.2 分、生成 15.5〜20.6 tok/s、プロンプト約 240 tok/s、キャッシュ 0.26 GB、専用メモリ約 1.1 GB。Granite 4.2 3B INT4 は初回のロード 4.7 → 2.7 分(答え・速さは 2.1.0 と同じ)。
|
| 23 |
+
- INT4 版のモデル(Granite・llm-jp など区間に分けて動くもの)は、重みを持たないグラフにできない(重みが分離されない)ので、Lunar Lake 以降でも重みを持つグラフ・融合グラフなしの 2.1.0 と同じ動作です。Lunar Lake 以降では NF4 版を勧めます。
|
| 24 |
+
- Meteor Lake・Arrow Lake(NPU 3720)は、2.1.0 と同じ動作です(重みを持たないグラフに対応していないため)。
|
| 25 |
+
- OpenVINO は 2026.5.0b1 に固定しました(キットがこの版専用のため)��
|
| 26 |
+
|
| 27 |
## v2.1.0 (2026-10-18)
|
| 28 |
- **Lunar Lake 以降専用の NF4 版を 3 つ追加:Granite 4.2 3B/8B、llm-jp-4.1-8B-thinking**(ryugyosoft/granite-4.2-3b-nf4-onw、granite-4.2-8b-nf4-onw、llm-jp-4.1-8b-thinking-nf4-onw)。重みはチャネル単位の NF4 を基本に、誤差の出やすいアテンションの入力(q/k/v_proj)と down_proj だけをチャネル単位の INT8 にしました。
|
| 29 |
- Panther Lake(Core Ultra X7 358H):Granite 4.2 8B は生成 5.4 → 約 9 tok/s(INT4 版・2.0.3 との比較)、約 900 トークンのプロンプト約 160 tok/s。llm-jp-4.1-8B は生成約 9.9 tok/s・プロンプト約 170 tok/s、Granite 4.2 3B は生成約 14.3 tok/s・プロンプト約 235 tok/s。1 トークンあたりの電力量(パッケージ全体)は 8B で約 1.3 J、3B で約 0.7 J。
|
CHANGELOG_en.md
CHANGED
|
@@ -4,6 +4,25 @@
|
|
| 4 |
|
| 5 |
Dates are JST. Every version is on Hugging Face `ryugyosoft/onw`, tagged from `v0.4.2` on.
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
## v2.1.0 (2026-10-18)
|
| 8 |
- **Three NF4 builds for Lunar Lake and later: Granite 4.2 3B / 8B and llm-jp-4.1-8B-thinking** (ryugyosoft/granite-4.2-3b-nf4-onw, granite-4.2-8b-nf4-onw, llm-jp-4.1-8b-thinking-nf4-onw). The weights are channel-wise NF4, except the attention inputs (q/k/v_proj) and down_proj, which are channel-wise INT8 (where 4 bits cost the most accuracy).
|
| 9 |
- Panther Lake (Core Ultra X7 358H): Granite 4.2 8B decodes 5.4 -> ~9 tok/s (against the INT4 build on 2.0.3) and reads a ~900-token prompt at ~160 tok/s; llm-jp-4.1-8B ~9.9 tok/s / ~170 tok/s; Granite 4.2 3B ~14.3 tok/s / ~235 tok/s. Energy per token (whole package): ~1.3 J for the 8B, ~0.7 J for the 3B.
|
|
|
|
| 4 |
|
| 5 |
Dates are JST. Every version is on Hugging Face `ryugyosoft/onw`, tagged from `v0.4.2` on.
|
| 6 |
|
| 7 |
+
## v3.0.0 (2026-10-19)
|
| 8 |
+
- **onw's own NPU kit (Windows and Linux): the NPU compiler and the NPU plugin, rebuilt for onw from Intel's open sources** (`onw/npukit`, Apache 2.0, changes in `onw/npukit/patches`). Used only with the OpenVINO 2026.5.0 that onw installs; nothing of the installed OpenVINO is modified (`ONW_NPUKIT=0` goes back to OpenVINO's own). What it brings:
|
| 9 |
+
- **Compiled graphs without weights**: the cache holds the graphs only; the weights are rebuilt on the NPU from the model's own files at every load. The cache shrinks to a tenth of the model or less (Granite 4.2 8B NF4: 10.5 -> 0.8 GB, llm-jp-4.1-8B NF4: 9.2 -> 0.65 GB, Granite 4.2 3B NF4: 4.2 -> 0.5 GB). OpenVINO 2026.5's compiler cannot do this with NF4 weights.
|
| 10 |
+
- **One copy of identical weights on the NPU**: however many graphs (block sizes, context-length variants), the weights are held once.
|
| 11 |
+
- Compiler fixes: the 4-token verification graphs of the Qwen3.5 NF4 models (MTP drafts), 2-bit weights (Bonsai) computing zeros with 2+ rows, faster compiles (Panther Lake: Ternary Bonsai 2 9B loads the first time in ~7.5 min and answers at ~17 tok/s, ...).
|
| 12 |
+
- **Fused graphs on by default on PCs from Lunar Lake on (Windows and Ubuntu)** (experimental and off in 2.1.0): one NPU call per segment, faster answers and prompts. The weights are shared, so memory does not grow - it shrinks. Panther Lake (Core Ultra X7 358H), against 2.1.0's default:
|
| 13 |
+
- Granite 4.2 8B NF4: answers ~9 -> ~13 tok/s, a ~900-token prompt ~160 -> ~400 tok/s, private memory 11.4 -> ~1.6 GB (peak 12.5 -> ~9.8 GB), energy per token ~1.3 -> ~0.9 J.
|
| 14 |
+
- llm-jp-4.1-8B NF4: answers ~9.9 -> ~14 tok/s, prompt ~170 -> ~440 tok/s, private memory 11.0 -> ~2.4 GB.
|
| 15 |
+
- Lunar Lake (Ubuntu, 16 GB), Granite 4.2 8B NF4: answers 11-13 tok/s, a ~900-token prompt ~385 tok/s, private memory ~2 GB (peak 3.3 GB), cache 1.8 GB, first load 3.5 min (the remaining fused graphs ~14 min in the background).
|
| 16 |
+
- Granite 4.2 3B NF4: answers ~14.3 -> ~23 tok/s, prompt ~235 -> ~730 tok/s, private memory 5.1 -> ~1.4 GB.
|
| 17 |
+
- **Long chats are fast too**: the fused graphs go on up to the context, with 8K / 16K / 32K-position variants (2.1.0: up to 4K positions, the paged path beyond). Granite 4.2 3B NF4, prompts of ~7.3K / 15K / 22K tokens (against the paged path): answers 4.6 -> 13.6 / 4.0 -> 8.8 / 2.6 -> 4.9 tok/s, prompts 94 -> 170 / 62 -> 146 / 43 -> 74 tok/s.
|
| 18 |
+
- Every context-length variant (1K to 32K positions) stays loaded: no pauses mid-chat, and the same question gets the same answer. The first load takes about as long as with 2.1.0 (8B: ~10-12 min); the remaining variants compile in the background while you chat.
|
| 19 |
+
- The memory-saving mode keeps loading only the variant in use, as before.
|
| 20 |
+
- The Qwen3.5 family, Ornith and gemma-4 (models that run through NPUW) get small caches too (Qwen3.5-9B NF4: ~0.3 GB). Qwen3.5-9B NF4 answers at ~14-19 tok/s with MTP drafts.
|
| 21 |
+
- **The kit on Ubuntu too** (`onw/npukit/linux`, a 32 MB archive unpacked at the first start). Lunar Lake (16 GB), Qwen3.5-9B NF4: first load ~2.2 min, answers 15.5-20.6 tok/s, prompts ~240 tok/s, cache 0.26 GB, private memory ~1.1 GB. Granite 4.2 3B INT4 loads the first time in 2.7 instead of 4.7 min (same answers and speed as 2.1.0).
|
| 22 |
+
- The INT4 builds (Granite, llm-jp and the other models run in paged parts) cannot have graphs without weights (their weights are not separated), so from Lunar Lake on too they run as in 2.1.0: graphs with their weights, no fused graphs. From Lunar Lake on, use the NF4 builds.
|
| 23 |
+
- Meteor Lake / Arrow Lake (NPU 3720) behave as in 2.1.0 (no graphs without weights there).
|
| 24 |
+
- OpenVINO is pinned to 2026.5.0b1 (the kit is built for exactly this release).
|
| 25 |
+
|
| 26 |
## v2.1.0 (2026-10-18)
|
| 27 |
- **Three NF4 builds for Lunar Lake and later: Granite 4.2 3B / 8B and llm-jp-4.1-8B-thinking** (ryugyosoft/granite-4.2-3b-nf4-onw, granite-4.2-8b-nf4-onw, llm-jp-4.1-8b-thinking-nf4-onw). The weights are channel-wise NF4, except the attention inputs (q/k/v_proj) and down_proj, which are channel-wise INT8 (where 4 bits cost the most accuracy).
|
| 28 |
- Panther Lake (Core Ultra X7 358H): Granite 4.2 8B decodes 5.4 -> ~9 tok/s (against the INT4 build on 2.0.3) and reads a ~900-token prompt at ~160 tok/s; llm-jp-4.1-8B ~9.9 tok/s / ~170 tok/s; Granite 4.2 3B ~14.3 tok/s / ~235 tok/s. Energy per token (whole package): ~1.3 J for the 8B, ~0.7 J for the 3B.
|
README.md
CHANGED
|
@@ -102,10 +102,10 @@ Python や git の知識は要りません。Python が入っていなければ
|
|
| 102 |
| [ryugyosoft/NeoHorse-1-4B-onw](https://huggingface.co/ryugyosoft/NeoHorse-1-4B-onw) | `qwen3_5`(Qwen3.5-4B をエージェント・ツール呼び出し向けに追加学習した NeoHorse-1) | テキスト | 切替 | 6.4 tok/s | 2.4 GB | 約 6 GB |
|
| 103 |
| [ryugyosoft/granite-4.2-3b-onw](https://huggingface.co/ryugyosoft/granite-4.2-3b-onw) | `granite`(IBM Granite 4.2:思考とツール呼び出しを強化した 3B。Llama 型) | テキスト | 切替 | 6.7 tok/s(チャット約 7.7) | 2.1 GB | 約 6 GB |
|
| 104 |
| [ryugyosoft/granite-4.2-8b-onw](https://huggingface.co/ryugyosoft/granite-4.2-8b-onw) | `granite`(同上の 8B) | テキスト | 切替 | 3.6 tok/s(チャット約 3.9) | 5.0 GB | 約 13 GB |
|
| 105 |
-
| [ryugyosoft/granite-4.2-3b-nf4-onw](https://huggingface.co/ryugyosoft/granite-4.2-3b-nf4-onw) | 3B の NF4 版(重みはチャネル単位 NF4、q/k/v・down_proj は INT8。**Lunar Lake 以降専用**) | テキスト | 切替 | —(Panther Lake で約
|
| 106 |
-
| [ryugyosoft/granite-4.2-8b-nf4-onw](https://huggingface.co/ryugyosoft/granite-4.2-8b-nf4-onw) | 8B の NF4 版(同上。**Lunar Lake 以降専用**) | テキスト | 切替 | —(Panther Lake で約
|
| 107 |
| [ryugyosoft/llm-jp-4.1-8b-thinking-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-8b-thinking-onw) | `llama`(NII LLM-jp の日本語中心の思考型 8B、harmony 形式) | テキスト | **常に** | 4.2〜4.7 tok/s | 4.6 GB | 約 10 GB |
|
| 108 |
-
| [ryugyosoft/llm-jp-4.1-8b-thinking-nf4-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-8b-thinking-nf4-onw) | 同上、NF4 版(q/k/v・down_proj は INT8。**Lunar Lake 以降専用**) | テキスト | **常に** | —(Panther Lake で約
|
| 109 |
| [ryugyosoft/llm-jp-4.1-8b-thinking-int8-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-8b-thinking-int8-onw) | 同上、重みはチャネル単位 INT8(bf16 に近い精度) | テキスト | **常に** | 4.1 tok/s | 7.7 GB | 約 15 GB(節約モードで待機中 約 8 GB) |
|
| 110 |
| [ryugyosoft/llm-jp-4.1-32b-a3b-thinking-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-32b-a3b-thinking-onw) | `qwen3_moe`(llm-jp の MoE:128 個から 8 個、32B のうち 1 トークン約 3B、harmony 形式) | テキスト | **常に** | 5.8 tok/s | 16 GB | 約 17 GB(SSD から読めば 16 GB の PC でも。Lunar Lake 16 GB で 3.4 tok/s) |
|
| 111 |
| [ryugyosoft/llm-jp-4.1-32b-a3b-thinking-int8-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-32b-a3b-thinking-int8-onw) | 同上、エキスパート・アテンション INT8(bf16 に近い精度) | テキスト | **常に** | 4.6 tok/s | 30 GB | 約 32 GB |
|
|
@@ -200,7 +200,8 @@ onw list # 対応アーキ
|
|
| 200 |
| **コンテキスト長**(`--context`、既定 32K、モデルの最大まで) | サーバーのオプション/設定画面 | 不要(KV キャッシュの上限が変わるだけ) |
|
| 201 |
| 64 トークン処理(dense モデルのプロンプトを 64 トークンずつ。auto はオンで、16 トークン用のグラフの代わりに読み込む。off で 16 トークンずつ、`ONW_S64=1` で両方) | `ONW_S64=1/0`/設定画面 | 初回のみ |
|
| 202 |
| MTP 先読み(Qwen3.5 系 NF4 版。auto は Lunar Lake 以降でオン) | `ONW_MTP=1/0` | 初回のみ |
|
| 203 |
-
| 融合グラフ(
|
|
|
|
| 204 |
| エキスパートのメモリ(Qwen3.6 系。上限を超える分は SSD から読む) | `ONW_EXPERT_GB=5`/設定画面 | 不要 |
|
| 205 |
| NPUW 重み共有(試験的。auto は Lunar Lake 以降でオン、MoE モデル(LFM2・Qwen3.6)と区間の多いモデル(Granite・llm-jp)は対象外:重みを 1 コピーにしてメモリとコンパイル時間を削減) | `ONW_NPUW=1/0`/設定画面 | 初回のみ |
|
| 206 |
| メモリ節約モード(1 トークン用と 16 トークン用で重みを共有できない dense モデル:NPU 3720 ではすべて、Lunar Lake 以降は Granite・llm-jp と NPUW オフのとき。普段は 16 トークン用のグラフだけを持ち、回答を書く間だけ 1 トークン用と入れ替える。待機中のメモリ約 4〜6 割減、回答の出だし数秒は遅め、先読み検証なし) | `ONW_LAZY16=1`/設定画面(効くモデルがあるときだけ表示) | 初回のみ |
|
|
@@ -318,25 +319,15 @@ KV キャッシュは INT8 で、会話が実際に使った長さの分だけ
|
|
| 318 |
(Windows 11、ドライバー 32.0.100.5540、2026 年 10 月 6 日。1 パーツは 4 トークン用グラフの最初のパーツ。命令の大きさは
|
| 319 |
コンパイル結果の ELF セクションの大きさで、命令の数そのものではありません。)
|
| 320 |
|
| 321 |
-
### 既知の問題(onw
|
| 322 |
-
|
| 323 |
-
- **
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
- **
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
再起動したりしても、コンパイルの済んだ部分はキャッシュに残ります。
|
| 331 |
-
- 原因:OpenVINO 2026.5 に同梱の NPU コンパイラー(8.4。ナイトリー版の 8.5 も同じ)は、1 トークン用(生成用)のグラフの
|
| 332 |
-
コンパイル結果を書き出す段階が遅くなっています。1 トークン用のグラフでは、INT4 の行列積が量子化のグループごとの小さな
|
| 333 |
-
行列積に分解され(2 トークン以上なら分解されません)、その小さな定数を書き出す処理が 8.3 より約 2.7 倍遅くなっています。
|
| 334 |
-
Granite 4.2 8B の部品 1 つ(NPU 5010 向け、Core Ultra 9 285HX で計測)は、1 トークン用で 97 秒、2 トークン用で 4 秒でした。
|
| 335 |
-
- 2.0.3 の対処:NPU 4000 以降(Lunar Lake/Panther Lake。Arrow Lake などの NPU 3720 は変わりません)では、上の 3 モデルの生成に 2 トークン用のグラフを使います。
|
| 336 |
-
モデルの更新(数百バイトのファイル追加)が必要で、アプリが更新を案内します。生成は約 1 割遅くなります(Granite 4.2 8B で
|
| 337 |
-
6.0 → 5.4 tok/s)。環境変数 `ONW_DECODE2=0` で元の 1 トークン用に戻せます。
|
| 338 |
-
- 試験機能:環境変数 `ONW_NPU_COMPILER=driver` で、OpenVINO 同梱のコンパイラーではなくドライバー内のコンパイラーを使います。
|
| 339 |
-
Qwen3.5-9B(INT4)は正しく動きましたが、NF4 版の 4 トークン用グラフのコンパイルに失敗するので既定にはしていません。
|
| 340 |
- **Panther Lake の小型機では、MTP 先読みの生成が Lunar Lake より遅い**:Qwen3.5-9B(NF4)+ MTP の生成は、Panther Lake の
|
| 341 |
小型機で 13.6〜14.3 tok/s(Lunar Lake は約 19 tok/s)でした。MTP は CPU 側の処理が多く、電力の制限で定格まで落ちる小型機の
|
| 342 |
CPU が効いていると見て調査中です。
|
|
|
|
| 102 |
| [ryugyosoft/NeoHorse-1-4B-onw](https://huggingface.co/ryugyosoft/NeoHorse-1-4B-onw) | `qwen3_5`(Qwen3.5-4B をエージェント・ツール呼び出し向けに追加学習した NeoHorse-1) | テキスト | 切替 | 6.4 tok/s | 2.4 GB | 約 6 GB |
|
| 103 |
| [ryugyosoft/granite-4.2-3b-onw](https://huggingface.co/ryugyosoft/granite-4.2-3b-onw) | `granite`(IBM Granite 4.2:思考とツール呼び出しを強化した 3B。Llama 型) | テキスト | 切替 | 6.7 tok/s(チャット約 7.7) | 2.1 GB | 約 6 GB |
|
| 104 |
| [ryugyosoft/granite-4.2-8b-onw](https://huggingface.co/ryugyosoft/granite-4.2-8b-onw) | `granite`(同上の 8B) | テキスト | 切替 | 3.6 tok/s(チャット約 3.9) | 5.0 GB | 約 13 GB |
|
| 105 |
+
| [ryugyosoft/granite-4.2-3b-nf4-onw](https://huggingface.co/ryugyosoft/granite-4.2-3b-nf4-onw) | 3B の NF4 版(重みはチャネル単位 NF4、q/k/v・down_proj は INT8。**Lunar Lake 以降専用**) | テキスト | 切替 | —(Panther Lake で約 23 tok/s) | 2.6 GB | 約 4.8 GB |
|
| 106 |
+
| [ryugyosoft/granite-4.2-8b-nf4-onw](https://huggingface.co/ryugyosoft/granite-4.2-8b-nf4-onw) | 8B の NF4 版(同上。**Lunar Lake 以降専用**) | テキスト | 切替 | —(Panther Lake で約 13 tok/s) | 6.2 GB | 約 9.8 GB |
|
| 107 |
| [ryugyosoft/llm-jp-4.1-8b-thinking-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-8b-thinking-onw) | `llama`(NII LLM-jp の日本語中心の思考型 8B、harmony 形式) | テキスト | **常に** | 4.2〜4.7 tok/s | 4.6 GB | 約 10 GB |
|
| 108 |
+
| [ryugyosoft/llm-jp-4.1-8b-thinking-nf4-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-8b-thinking-nf4-onw) | 同上、NF4 版(q/k/v・down_proj は INT8。**Lunar Lake 以降専用**) | テキスト | **常に** | —(Panther Lake で約 14 tok/s) | 6.1 GB | 約 9.7 GB |
|
| 109 |
| [ryugyosoft/llm-jp-4.1-8b-thinking-int8-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-8b-thinking-int8-onw) | 同上、重みはチャネル単位 INT8(bf16 に近い精度) | テキスト | **常に** | 4.1 tok/s | 7.7 GB | 約 15 GB(節約モードで待機中 約 8 GB) |
|
| 110 |
| [ryugyosoft/llm-jp-4.1-32b-a3b-thinking-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-32b-a3b-thinking-onw) | `qwen3_moe`(llm-jp の MoE:128 個から 8 個、32B のうち 1 トークン約 3B、harmony 形式) | テキスト | **常に** | 5.8 tok/s | 16 GB | 約 17 GB(SSD から読めば 16 GB の PC でも。Lunar Lake 16 GB で 3.4 tok/s) |
|
| 111 |
| [ryugyosoft/llm-jp-4.1-32b-a3b-thinking-int8-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-32b-a3b-thinking-int8-onw) | 同上、エキスパート・アテンション INT8(bf16 に近い精度) | テキスト | **常に** | 4.6 tok/s | 30 GB | 約 32 GB |
|
|
|
|
| 200 |
| **コンテキスト長**(`--context`、既定 32K、モデルの最大まで) | サーバーのオプション/設定画面 | 不要(KV キャッシュの上限が変わるだけ) |
|
| 201 |
| 64 トークン処理(dense モデルのプロンプトを 64 トークンずつ。auto はオンで、16 トークン用のグラフの代わりに読み込む。off で 16 トークンずつ、`ONW_S64=1` で両方) | `ONW_S64=1/0`/設定画面 | 初回のみ |
|
| 202 |
| MTP 先読み(Qwen3.5 系 NF4 版。auto は Lunar Lake 以降でオン) | `ONW_MTP=1/0` | 初回のみ |
|
| 203 |
+
| 融合グラフ(dense モデルの区間を 1 回の呼び出しで。回答用と、Lunar Lake 以降はプロンプト用も。onw の NPU キットが使える Lunar Lake 以降の PC では既定でオン) | `ONW_FUSED=0`(無効)/`1`(キットなしでも有効。キャッシュがモデルの約 6〜7 倍になる)、`ONW_FUSED_PROMPT=0`(回答だけ) | 初回のみ |
|
| 204 |
+
| onw の NPU キット(Windows・Linux。重みを持たないグラフと重みの共有、コンパイラーの修正) | `ONW_NPUKIT=0`(OpenVINO 自身の NPU プラグインを使う) | 起動時 |
|
| 205 |
| エキスパートのメモリ(Qwen3.6 系。上限を超える分は SSD から読む) | `ONW_EXPERT_GB=5`/設定画面 | 不要 |
|
| 206 |
| NPUW 重み共有(試験的。auto は Lunar Lake 以降でオン、MoE モデル(LFM2・Qwen3.6)と区間の多いモデル(Granite・llm-jp)は対象外:重みを 1 コピーにしてメモリとコンパイル時間を削減) | `ONW_NPUW=1/0`/設定画面 | 初回のみ |
|
| 207 |
| メモリ節約モード(1 トークン用と 16 トークン用で重みを共有できない dense モデル:NPU 3720 ではすべて、Lunar Lake 以降は Granite・llm-jp と NPUW オフのとき。普段は 16 トークン用のグラフだけを持ち、回答を書く間だけ 1 トークン用と入れ替える。待機中のメモリ約 4〜6 割減、回答の出だし数秒は遅め、先読み検証なし) | `ONW_LAZY16=1`/設定画面(効くモデルがあるときだけ表示) | 初回のみ |
|
|
|
|
| 319 |
(Windows 11、ドライバー 32.0.100.5540、2026 年 10 月 6 日。1 パーツは 4 トークン用グラフの最初のパーツ。命令の大きさは
|
| 320 |
コンパイル結果の ELF セクションの大きさで、命令の数そのものではありません。)
|
| 321 |
|
| 322 |
+
### 既知の問題(onw 3.0.0、2026 年 10 月 19 日)
|
| 323 |
+
|
| 324 |
+
- **INT4 版のモデルと Meteor Lake・Arrow Lake(NPU 3720)は 2.1.0 と同じです**:INT4 の重みは重みを持たないグラフにできず、NPU 3720 は
|
| 325 |
+
それに対応していないので、キャッシュとメモリは 2.1.0 と同じで、融合グラフも使いません。Lunar Lake 以降では NF4 版を勧めます。
|
| 326 |
+
- **長い会話では生成が遅くなります**:文脈が長いほど毎回読む KV キャッシュが増えるためです(Panther Lake の Granite 4.2 3B NF4:
|
| 327 |
+
約 7.3K トークン�� 13.6 tok/s、約 15K で 8.8 tok/s、約 22K で 4.9 tok/s。従来の経路の約 2〜3 倍です)。
|
| 328 |
+
- **初回のロード(Panther Lake の小型機 Core Ultra X7 358H、3.0)**:Granite 4.2 8B NF4 約 12 分、llm-jp-4.1-8B NF4 約 10 分、
|
| 329 |
+
Granite 4.2 3B NF4 約 6.5 分、Qwen3.5-9B NF4 約 7.5 分。残りの融合グラフは、回答しながら裏でコンパイルします(4K 位置までで 7〜10 分、32K 位置まで全部で Granite 4.2 3B NF4 は約 28 分)。
|
| 330 |
+
2 回目以降はキャッシュから数十秒です。ロードを途中で止めたり PC が再起動したりしても、コンパイルの済んだ部分は残ります。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
- **Panther Lake の小型機では、MTP 先読みの生成が Lunar Lake より遅い**:Qwen3.5-9B(NF4)+ MTP の生成は、Panther Lake の
|
| 332 |
小型機で 13.6〜14.3 tok/s(Lunar Lake は約 19 tok/s)でした。MTP は CPU 側の処理が多く、電力の制限で定格まで落ちる小型機の
|
| 333 |
CPU が効いていると見て調査中です。
|
README_en.md
CHANGED
|
@@ -89,10 +89,10 @@ troubleshooting), is the **[user guide](MANUAL_en.md)**.
|
|
| 89 |
| [ryugyosoft/NeoHorse-1-4B-onw](https://huggingface.co/ryugyosoft/NeoHorse-1-4B-onw) | `qwen3_5` (NeoHorse-1: Qwen3.5-4B trained further for agents and tool calls) | text | switchable | 6.4 tok/s | 2.4 GB | ~6 GB |
|
| 90 |
| [ryugyosoft/granite-4.2-3b-onw](https://huggingface.co/ryugyosoft/granite-4.2-3b-onw) | `granite` (IBM Granite 4.2: a 3B with thinking and stronger tool calls; Llama-type) | text | switchable | 6.7 tok/s (~7.7 in chat) | 2.1 GB | ~6 GB |
|
| 91 |
| [ryugyosoft/granite-4.2-8b-onw](https://huggingface.co/ryugyosoft/granite-4.2-8b-onw) | `granite` (the same, 8B) | text | switchable | 3.6 tok/s (~3.9 in chat) | 5.0 GB | ~13 GB |
|
| 92 |
-
| [ryugyosoft/granite-4.2-3b-nf4-onw](https://huggingface.co/ryugyosoft/granite-4.2-3b-nf4-onw) | the 3B as NF4 (channel-wise NF4, q/k/v and down_proj INT8; **Lunar Lake and newer only**) | text | switchable | — (~
|
| 93 |
-
| [ryugyosoft/granite-4.2-8b-nf4-onw](https://huggingface.co/ryugyosoft/granite-4.2-8b-nf4-onw) | the 8B as NF4 (the same; **Lunar Lake and newer only**) | text | switchable | — (~
|
| 94 |
| [ryugyosoft/llm-jp-4.1-8b-thinking-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-8b-thinking-onw) | `llama` (NII LLM-jp's Japanese-centred 8B thinking model, harmony format) | text | **always** | 4.2-4.7 tok/s | 4.6 GB | ~10 GB |
|
| 95 |
-
| [ryugyosoft/llm-jp-4.1-8b-thinking-nf4-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-8b-thinking-nf4-onw) | the same as NF4 (q/k/v and down_proj INT8; **Lunar Lake and newer only**) | text | **always** | — (~
|
| 96 |
| [ryugyosoft/llm-jp-4.1-8b-thinking-int8-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-8b-thinking-int8-onw) | the same with channel-wise INT8 weights (close to bf16) | text | **always** | 4.1 tok/s | 7.7 GB | ~15 GB (~8 GB idle in memory-saving mode) |
|
| 97 |
| [ryugyosoft/llm-jp-4.1-32b-a3b-thinking-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-32b-a3b-thinking-onw) | `qwen3_moe` (llm-jp's MoE: 8 of 128 experts, ~3B of 32B per token, harmony format) | text | **always** | 5.8 tok/s | 16 GB | ~17 GB (16 GB PCs with the experts streamed from the SSD: 3.4 tok/s on a 16 GB Lunar Lake) |
|
| 98 |
| [ryugyosoft/llm-jp-4.1-32b-a3b-thinking-int8-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-32b-a3b-thinking-int8-onw) | the same, INT8 experts and attention (close to bf16) | text | **always** | 4.6 tok/s | 30 GB | ~32 GB |
|
|
@@ -189,7 +189,8 @@ What needs what:
|
|
| 189 |
| **context length** (`--context`, 32K by default, up to the model's maximum) | server option / settings | no (it only bounds the KV cache) |
|
| 190 |
| 64-token prompt blocks (dense models read prompts 64 tokens at a time; auto = on, loaded in place of the 16-token graphs; off = 16 at a time, `ONW_S64=1` = both) | `ONW_S64=1/0` / settings | once, the first time |
|
| 191 |
| MTP draft tokens (the NF4 Qwen3.5-family builds; auto = on from Lunar Lake) | `ONW_MTP=1/0` | once, the first time |
|
| 192 |
-
| fused graphs (
|
|
|
|
| 193 |
| expert memory (Qwen3.6 models; the rest streams from the SSD) | `ONW_EXPERT_GB=5` / settings | no |
|
| 194 |
| NPUW weight sharing (experimental; auto = on from Lunar Lake, not for the MoE models LFM2 / Qwen3.6 or models with many parts (Granite, llm-jp): one copy of the weights, less memory, much faster compile) | `ONW_NPUW=1/0` / settings | once, the first time |
|
| 195 |
| Memory-saving mode (dense models whose 1-token and 16-token graphs cannot share weights: all of them on NPU 3720; from Lunar Lake on Granite / llm-jp, and any with NPUW off. Only the 16-token graphs are kept, swapped for the 1-token ones while an answer is written; 40-60% less memory when idle, the first seconds of an answer slower, no prompt lookup) | `ONW_LAZY16=1` / settings (shown only when a model it helps is there) | once, the first time |
|
|
@@ -306,27 +307,17 @@ How it was measured, and caveats:
|
|
| 306 |
(Windows 11, driver 32.0.100.5540, 2026-10-06. One part = the first part of a 4-token graph. Code sizes are ELF section sizes of
|
| 307 |
the compiled file, not instruction counts.)
|
| 308 |
|
| 309 |
-
### Known issues (onw
|
| 310 |
-
|
| 311 |
-
- **The
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
- **
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
- Cause: the NPU compiler that comes with OpenVINO 2026.5 (8.4; the nightly 8.5 is the same) is slow at writing out the
|
| 321 |
-
compiled 1-token (decode) graphs. In a 1-token graph each INT4 matmul is split into one small matmul per quantization group
|
| 322 |
-
(not with 2 or more tokens), and writing out those small constants is ~2.7x slower than in 8.3. One Granite 4.2 8B part
|
| 323 |
-
(for the NPU 5010, timed on a Core Ultra 9 285HX) takes 97 s as a 1-token graph and 4 s as a 2-token graph.
|
| 324 |
-
- The 2.0.3 fix: on the NPU 4000 and later (Lunar Lake / Panther Lake; the NPU 3720 of Arrow Lake etc. is unchanged) these three models decode on 2-token
|
| 325 |
-
graphs. This needs a model update (a few hundred bytes of new files); the app offers it. Decoding gets ~10% slower
|
| 326 |
-
(Granite 4.2 8B 6.0 -> 5.4 tok/s). `ONW_DECODE2=0` goes back to the 1-token graphs.
|
| 327 |
-
- Experimental: `ONW_NPU_COMPILER=driver` uses the compiler inside the NPU driver instead of the one that comes with
|
| 328 |
-
OpenVINO. Qwen3.5-9B (INT4) runs correctly with it, but it fails to compile the 4-token graphs of the NF4 builds, so it is
|
| 329 |
-
not the default.
|
| 330 |
- **On small Panther Lake PCs MTP decoding is slower than on Lunar Lake**: Qwen3.5-9B (NF4) + MTP decodes at 13.6-14.3 tok/s
|
| 331 |
there (Lunar Lake ~19 tok/s). MTP does a lot of CPU-side work, and the small PC's CPU drops to its base clock under its power
|
| 332 |
limit; being looked into.
|
|
|
|
| 89 |
| [ryugyosoft/NeoHorse-1-4B-onw](https://huggingface.co/ryugyosoft/NeoHorse-1-4B-onw) | `qwen3_5` (NeoHorse-1: Qwen3.5-4B trained further for agents and tool calls) | text | switchable | 6.4 tok/s | 2.4 GB | ~6 GB |
|
| 90 |
| [ryugyosoft/granite-4.2-3b-onw](https://huggingface.co/ryugyosoft/granite-4.2-3b-onw) | `granite` (IBM Granite 4.2: a 3B with thinking and stronger tool calls; Llama-type) | text | switchable | 6.7 tok/s (~7.7 in chat) | 2.1 GB | ~6 GB |
|
| 91 |
| [ryugyosoft/granite-4.2-8b-onw](https://huggingface.co/ryugyosoft/granite-4.2-8b-onw) | `granite` (the same, 8B) | text | switchable | 3.6 tok/s (~3.9 in chat) | 5.0 GB | ~13 GB |
|
| 92 |
+
| [ryugyosoft/granite-4.2-3b-nf4-onw](https://huggingface.co/ryugyosoft/granite-4.2-3b-nf4-onw) | the 3B as NF4 (channel-wise NF4, q/k/v and down_proj INT8; **Lunar Lake and newer only**) | text | switchable | — (~23 tok/s on Panther Lake) | 2.6 GB | ~4.8 GB |
|
| 93 |
+
| [ryugyosoft/granite-4.2-8b-nf4-onw](https://huggingface.co/ryugyosoft/granite-4.2-8b-nf4-onw) | the 8B as NF4 (the same; **Lunar Lake and newer only**) | text | switchable | — (~13 tok/s on Panther Lake) | 6.2 GB | ~9.8 GB |
|
| 94 |
| [ryugyosoft/llm-jp-4.1-8b-thinking-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-8b-thinking-onw) | `llama` (NII LLM-jp's Japanese-centred 8B thinking model, harmony format) | text | **always** | 4.2-4.7 tok/s | 4.6 GB | ~10 GB |
|
| 95 |
+
| [ryugyosoft/llm-jp-4.1-8b-thinking-nf4-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-8b-thinking-nf4-onw) | the same as NF4 (q/k/v and down_proj INT8; **Lunar Lake and newer only**) | text | **always** | — (~14 tok/s on Panther Lake) | 6.1 GB | ~9.7 GB |
|
| 96 |
| [ryugyosoft/llm-jp-4.1-8b-thinking-int8-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-8b-thinking-int8-onw) | the same with channel-wise INT8 weights (close to bf16) | text | **always** | 4.1 tok/s | 7.7 GB | ~15 GB (~8 GB idle in memory-saving mode) |
|
| 97 |
| [ryugyosoft/llm-jp-4.1-32b-a3b-thinking-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-32b-a3b-thinking-onw) | `qwen3_moe` (llm-jp's MoE: 8 of 128 experts, ~3B of 32B per token, harmony format) | text | **always** | 5.8 tok/s | 16 GB | ~17 GB (16 GB PCs with the experts streamed from the SSD: 3.4 tok/s on a 16 GB Lunar Lake) |
|
| 98 |
| [ryugyosoft/llm-jp-4.1-32b-a3b-thinking-int8-onw](https://huggingface.co/ryugyosoft/llm-jp-4.1-32b-a3b-thinking-int8-onw) | the same, INT8 experts and attention (close to bf16) | text | **always** | 4.6 tok/s | 30 GB | ~32 GB |
|
|
|
|
| 189 |
| **context length** (`--context`, 32K by default, up to the model's maximum) | server option / settings | no (it only bounds the KV cache) |
|
| 190 |
| 64-token prompt blocks (dense models read prompts 64 tokens at a time; auto = on, loaded in place of the 16-token graphs; off = 16 at a time, `ONW_S64=1` = both) | `ONW_S64=1/0` / settings | once, the first time |
|
| 191 |
| MTP draft tokens (the NF4 Qwen3.5-family builds; auto = on from Lunar Lake) | `ONW_MTP=1/0` | once, the first time |
|
| 192 |
+
| fused graphs (a dense model's segment in one call; answers, and prompts from Lunar Lake on; on by default on PCs from Lunar Lake on with onw's NPU kit) | `ONW_FUSED=0` (off) / `1` (on without the kit too - the cache then grows to ~6-7x the model), `ONW_FUSED_PROMPT=0` (answers only) | once, the first time |
|
| 193 |
+
| onw's NPU kit (Windows and Linux: graphs without weights, shared weights, compiler fixes) | `ONW_NPUKIT=0` (use OpenVINO's own NPU plugin) | at start |
|
| 194 |
| expert memory (Qwen3.6 models; the rest streams from the SSD) | `ONW_EXPERT_GB=5` / settings | no |
|
| 195 |
| NPUW weight sharing (experimental; auto = on from Lunar Lake, not for the MoE models LFM2 / Qwen3.6 or models with many parts (Granite, llm-jp): one copy of the weights, less memory, much faster compile) | `ONW_NPUW=1/0` / settings | once, the first time |
|
| 196 |
| Memory-saving mode (dense models whose 1-token and 16-token graphs cannot share weights: all of them on NPU 3720; from Lunar Lake on Granite / llm-jp, and any with NPUW off. Only the 16-token graphs are kept, swapped for the 1-token ones while an answer is written; 40-60% less memory when idle, the first seconds of an answer slower, no prompt lookup) | `ONW_LAZY16=1` / settings (shown only when a model it helps is there) | once, the first time |
|
|
|
|
| 307 |
(Windows 11, driver 32.0.100.5540, 2026-10-06. One part = the first part of a 4-token graph. Code sizes are ELF section sizes of
|
| 308 |
the compiled file, not instruction counts.)
|
| 309 |
|
| 310 |
+
### Known issues (onw 3.0.0, 2026-10-19)
|
| 311 |
+
|
| 312 |
+
- **The INT4 builds and Meteor Lake / Arrow Lake (NPU 3720) are as in 2.1.0**: INT4 weights cannot go into graphs without weights,
|
| 313 |
+
and the NPU 3720 has no such graphs, so cache and memory are as in 2.1.0 and no fused graphs are used. From Lunar Lake on, use
|
| 314 |
+
the NF4 builds.
|
| 315 |
+
- **Answers slow down in long chats**: the longer the context, the more KV cache every step reads (Panther Lake, Granite 4.2 3B
|
| 316 |
+
NF4: 13.6 tok/s at ~7.3K tokens, 8.8 at ~15K, 4.9 at ~22K - about 2-3x the paged path).
|
| 317 |
+
- **First loads (a small Panther Lake PC, Core Ultra X7 358H, 3.0)**: Granite 4.2 8B NF4 ~12 min, llm-jp-4.1-8B NF4 ~10 min,
|
| 318 |
+
Granite 4.2 3B NF4 ~6.5 min, Qwen3.5-9B NF4 ~7.5 min. The remaining fused graphs compile in the background while you chat
|
| 319 |
+
(~7-10 min up to 4K positions; all of them up to 32K positions ~28 min for Granite 4.2 3B NF4). Later loads come from the cache in well under a minute; if a load is stopped or the PC restarts midway, the
|
| 320 |
+
parts already compiled stay.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
- **On small Panther Lake PCs MTP decoding is slower than on Lunar Lake**: Qwen3.5-9B (NF4) + MTP decodes at 13.6-14.3 tok/s
|
| 322 |
there (Lunar Lake ~19 tok/s). MTP does a lot of CPU-side work, and the small PC's CPU drops to its base clock under its power
|
| 323 |
limit; being looked into.
|
TECHNICAL.md
CHANGED
|
@@ -412,6 +412,52 @@ dense モデルのプロンプトは 16 トークンのブロックで処理し
|
|
| 412 |
- 種類の切り替え:会話が 1K・2K 位置を超えるとき、4K を超えて従来の経路に戻るとき、長い会話の後に短い会話に戻るとき、グラフを読み直します(Panther Lake の Granite 4.2 8B で 1 サイズ 6〜10 秒、NPU 3720 の 3B で約 3 秒)。この時間はプロンプトの処理時間に入ります。
|
| 413 |
- **NF4 版(形式 C)**:Lunar Lake 以降向けの Granite 4.2 3B/8B、llm-jp-4.1-8B は、重みをチャネル単位の NF4 にし、q/k/v_proj と down_proj だけチャネル単位の INT8 にしました。いくつかの組み合わせ(全部 NF4、q/k/v だけ INT8、グループ 256 の NF4)を教師強制の精度で比べ、精度と速さの釣り合いでこれを選びました(512 トークンで 1 位の一致率/KL:Granite 4.2 8B 88.5%/0.097、INT4 版 85.5%/0.132)。グラフは 2 トークン用と 64 トークン用だけです(NPU 4000 以降は 2 トークン用で回答します)。
|
| 414 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
## 1 ステップのホスト側の処理(onw 1.4)
|
| 416 |
|
| 417 |
MTP の 1 ブロックのうち、NPU 以外(Python・numpy)が約 47 ms ありました。生成結果を 1 ビットも変えずに、次のことで約 32 ms に
|
|
|
|
| 412 |
- 種類の切り替え:会話が 1K・2K 位置を超えるとき、4K を超えて従来の経路に戻るとき、長い会話の後に短い会話に戻るとき、グラフを読み直します(Panther Lake の Granite 4.2 8B で 1 サイズ 6〜10 秒、NPU 3720 の 3B で約 3 秒)。この時間はプロンプトの処理時間に入ります。
|
| 413 |
- **NF4 版(形式 C)**:Lunar Lake 以降向けの Granite 4.2 3B/8B、llm-jp-4.1-8B は、重みをチャネル単位の NF4 にし、q/k/v_proj と down_proj だけチャネル単位の INT8 にしました。いくつかの組み合わせ(全部 NF4、q/k/v だけ INT8、グループ 256 の NF4)を教師強制の精度で比べ、精度と速さの釣り合いでこれを選びました(512 トークンで 1 位の一致率/KL:Granite 4.2 8B 88.5%/0.097、INT4 版 85.5%/0.132)。グラフは 2 トークン用と 64 トークン用だけです(NPU 4000 以降は 2 トークン用で回答します)。
|
| 414 |
|
| 415 |
+
## onw の NPU キット:重みを持たないグラフと重みの共有(onw 3.0、Windows・Linux)
|
| 416 |
+
|
| 417 |
+
onw 3.0 は、Intel が公開している NPU コンパイラー(npu_compiler)と OpenVINO の NPU プラグインのソースに手を入れて作り直したものを
|
| 418 |
+
`onw/npukit` に同梱しています(Apache 2.0、変更点は `onw/npukit/patches`)。プラグインは onw が入れる OpenVINO 2026.5.0b1
|
| 419 |
+
(`2026.5.0-23160-6db412d0a27`)と同じコミットから作ってあり、onw は `ov.Core(onw/npukit/plugins.xml)` で「NPU」をこのプラグインに
|
| 420 |
+
向けます。インストール済みの OpenVINO のファイルは変えません。版が違うときは使わず、OpenVINO 自身の NPU プラグインに戻ります。
|
| 421 |
+
Linux 用(Ubuntu 24.04 で作った .so)は `onw/npukit/linux/npukit-linux.tar.xz`(32 MB)にまとめてあり、最初の起動で同じフォルダーに
|
| 422 |
+
展開します。Linux の OpenVINO にはプラグイン側のコンパイラーが入っていないので、キットのない Linux では重みを持たないグラフを作れません
|
| 423 |
+
(ドライバー内蔵のコンパイラーを使うため)。NPU 3720(Meteor Lake・Arrow Lake)では使いません(重みを持たないグラフに対応しておらず、
|
| 424 |
+
OpenVINO 自身のコンパイラーのほうが約 7% 速いグラフを作るため。`ONW_NPUKIT=1` で強制)。
|
| 425 |
+
|
| 426 |
+
- **重みを持たないグラフ**:`CACHE_MODE=OPTIMIZE_SIZE` と `ENABLE_WEIGHTLESS` でコンパイルすると、コンパイラーはグラフを
|
| 427 |
+
「重みを NPU 用の並びに組み立てる初期化スケジュール」と「本体」に分け、重みを含めずに書き出します。ロード時に同じ ov.Model を
|
| 428 |
+
`MODEL_PTR` で渡すと、モデルのファイル(.bin の写像)から重みを読み、NPU 上で組み立てて本体の入力につなぎます。組み立て済みの
|
| 429 |
+
重みを入力として渡しても、重みを含めたグラフと同じ速さで、計算結果もビット単位で同じでした。
|
| 430 |
+
- OpenVINO 2026.5 のコンパイラー(8.4)は NF4 の重みでこれに失敗します(「ui4 → quantile の変換は未対応」)。キットの
|
| 431 |
+
コンパイラーでは、NF4(quantile 型と、それを格納型にした量子化型)を格納型 ui4 のまま読み替えるようにし、4 ビット値の
|
| 432 |
+
クラスタ分割・並べ替えがバイト境界をまたがないようにしました(初期化スケジュールの中だけ)。
|
| 433 |
+
- 2 ビットの重み(Bonsai)は重みが分離されず(グラフの大きさがほぼ変わらない)、コンパイルが約 7 倍遅くなるので、使いません。
|
| 434 |
+
- INT4 の重みも分離されません(Granite 4.2 3B INT4 の 1 トークンの部品で 63.5 対 63.6 MB)。2 行以上のグラフでは、NPU 4000 以降の
|
| 435 |
+
コンパイラーが INT4 を表引き(4 ビットの値をそのまま表の番号にする)に変える処理と重ならず失敗し、直してもコンパイルが 20〜35 倍
|
| 436 |
+
遅くなるだけなので、INT4 のモデル(NPUW を使わない区間分割のもの)では使いません。融合グラフも、種類ごとに重みを持つことに
|
| 437 |
+
なるので既定でオフです(2.1.0 と同じ動作。Lunar Lake の 16 GB ではメモリーが足りなくなりました)。INT8 は分離されます
|
| 438 |
+
(llm-jp-4.1-8B INT8 の部品で 2.5 対 216 MB)。Lunar Lake 以降では NF4 版を勧めます。
|
| 439 |
+
- **重みの共有**:重みを持たないグラフでも、グラフごとに NPU 上へ重みを組み立てるので、融合グラフの長さ違い(1K〜32K 位置)や
|
| 440 |
+
ブロックサイズの数だけ重みが増えます(8B NF4 で 1 種類あたり約 4.8 GB)。キットのプラグインは、初期化スケジュールを走らせた
|
| 441 |
+
あと、出力した重みのバッファを大きさ・標本ハッシュ・全バイト比較で既存のものと照合し、すべて一致したら本体をそちらにつないで
|
| 442 |
+
新しいバッファを捨てます(`ONW_WS_SHARE=0` で無効)。区切り方の違うグラフどうし(区間を分けた従来の経路と融合グラフ)でも共有
|
| 443 |
+
できるよう、初期化スケジュールの出力を重みごとに分けます(コンパイラーの `ONW_WS_NO_CONCAT`。OpenVINO は既定で 1 本にまとめます)。
|
| 444 |
+
- Panther Lake の llm-jp-4.1-8B NF4:融合グラフ 6 種類を同時に読み込んで +5.7 GB(共有なしなら約 29 GB)。
|
| 445 |
+
- 共有したまま、ある種類のグラフを捨てて別の種類を読み込むと、NPU ドライバーの中で解放済みの領域への書き込みが起きることが
|
| 446 |
+
あるので、3.0 は融合グラフの全種類と従来の経路のグラフを読み込んだままにします(`ONW_FUSED_KEEP`。省メモリモードでは従来どおり
|
| 447 |
+
使う長さのグラフだけ)。全種類を持つので、融合グラフは文脈の長さまで 8K・16K・32K 位置の種類も作ります(2.1.0 は 4K まで)。
|
| 448 |
+
回答もプロンプトも、そのときの位置を覆う一番短い種類で計算し、位置が進むと次の種類に切り替えます(長い種類ほど毎回読む
|
| 449 |
+
KV キャッシュの範囲が広く、遅くなるため)。
|
| 450 |
+
- **コンパイラーのその他の修正**:
|
| 451 |
+
- Qwen3.5 系 NF4 の 4 トークンのグラフ(MTP 先読みの検証用)が `'async.await' has non 'return' consumer` で失敗していたのを修正
|
| 452 |
+
(関数の結果の手前に形だけ変える操作が残る場合)。これがないと MTP が使えません。
|
| 453 |
+
- 2 ビットのゼロ点付き重み(Bonsai)を 2 行以上で計算すると 0 になる不具合(2.x で報告済み)の修正。
|
| 454 |
+
- コンパイル時間:並列コンパイルが Windows のヒープのロックで直列になっていた(分散情報のマップが要素 1 つでも約 45 KB を確保)のを
|
| 455 |
+
直し、定数の畳み込みと縦方向の融合の探索の結果を使い回すようにしました。出力はビット単位で同じです(Qwen3.5-9B の 64 トークンの
|
| 456 |
+
部品 52.6 → 34 秒など)。
|
| 457 |
+
|
| 458 |
+
結果(Panther Lake、2.1.0 の既定との比較)は CHANGELOG の v3.0.0 を見てください。キャッシュはモデル本体の 1/10 以下、専用メモリは
|
| 459 |
+
数分の一になり、生成・プロンプトとも速くなります。
|
| 460 |
+
|
| 461 |
## 1 ステップのホスト側の処理(onw 1.4)
|
| 462 |
|
| 463 |
MTP の 1 ブロックのうち、NPU 以外(Python・numpy)が約 47 ms ありました。生成結果を 1 ビットも変えずに、次のことで約 32 ms に
|
TECHNICAL_en.md
CHANGED
|
@@ -435,6 +435,52 @@ The paged KV cache (onw 0.10) cuts each segment at its attention layers. A dense
|
|
| 435 |
- Switching variants: when a chat passes 1K / 2K positions, passes 4K (back to the previous path), or a short chat follows a long one, graphs are read again (Granite 4.2 8B on Panther Lake: 6-10 s per block size; Granite 4.2 3B on the NPU 3720 ~3 s). That time shows up in the prompt time.
|
| 436 |
- **The NF4 builds (format C)**: Granite 4.2 3B / 8B and llm-jp-4.1-8B for Lunar Lake and later have channel-wise NF4 weights with only q/k/v_proj and down_proj as channel-wise INT8. Several mixes (all NF4, only q/k/v INT8, group-256 NF4) were compared on teacher-forced accuracy, and this one was the best balance of accuracy and speed (top-1 agreement / KL on 512 tokens: Granite 4.2 8B 88.5% / 0.097, the INT4 build 85.5% / 0.132). They carry 2- and 64-token graphs only (NPU 4000+ answers on the 2-token ones).
|
| 437 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
## Host-side work per step (onw 1.4)
|
| 439 |
|
| 440 |
Of an MTP block, ~47 ms was Python / numpy. Without changing a single output bit, it is now ~32 ms (the same tokens as
|
|
|
|
| 435 |
- Switching variants: when a chat passes 1K / 2K positions, passes 4K (back to the previous path), or a short chat follows a long one, graphs are read again (Granite 4.2 8B on Panther Lake: 6-10 s per block size; Granite 4.2 3B on the NPU 3720 ~3 s). That time shows up in the prompt time.
|
| 436 |
- **The NF4 builds (format C)**: Granite 4.2 3B / 8B and llm-jp-4.1-8B for Lunar Lake and later have channel-wise NF4 weights with only q/k/v_proj and down_proj as channel-wise INT8. Several mixes (all NF4, only q/k/v INT8, group-256 NF4) were compared on teacher-forced accuracy, and this one was the best balance of accuracy and speed (top-1 agreement / KL on 512 tokens: Granite 4.2 8B 88.5% / 0.097, the INT4 build 85.5% / 0.132). They carry 2- and 64-token graphs only (NPU 4000+ answers on the 2-token ones).
|
| 437 |
|
| 438 |
+
## onw's NPU kit: graphs without weights and shared weights (onw 3.0, Windows and Linux)
|
| 439 |
+
|
| 440 |
+
onw 3.0 ships in `onw/npukit` an NPU compiler and an NPU plugin rebuilt from Intel's open sources (npu_compiler and OpenVINO's NPU
|
| 441 |
+
plugin; Apache 2.0, changes in `onw/npukit/patches`). The plugin is built from the very commit of the OpenVINO 2026.5.0b1 that onw
|
| 442 |
+
installs (`2026.5.0-23160-6db412d0a27`); onw points the "NPU" device at it with `ov.Core(onw/npukit/plugins.xml)`, so no file of the
|
| 443 |
+
installed OpenVINO changes. With another OpenVINO build it is not used and OpenVINO's own NPU plugin serves as before. The Linux
|
| 444 |
+
build (.so files made on Ubuntu 24.04) comes packed as `onw/npukit/linux/npukit-linux.tar.xz` (32 MB) and is unpacked next to it at
|
| 445 |
+
the first start; Linux OpenVINO has no plugin-side compiler, so without the kit no graphs without weights can be made there (the
|
| 446 |
+
driver's own compiler is used). Not used on the NPU 3720 (Meteor / Arrow Lake): no graphs without weights there, and OpenVINO's own
|
| 447 |
+
compiler makes ~7% faster graphs for it (`ONW_NPUKIT=1` forces it).
|
| 448 |
+
|
| 449 |
+
- **Graphs without weights**: compiled with `CACHE_MODE=OPTIMIZE_SIZE` and `ENABLE_WEIGHTLESS`, a graph is split into an init
|
| 450 |
+
schedule (which lays the weights out for the NPU) and the main one, and written without its weights. At load, the same ov.Model
|
| 451 |
+
given as `MODEL_PTR` supplies the weights from the model's files (the mapped .bin); the NPU rebuilds them and they become the main
|
| 452 |
+
schedule's inputs. Passed as inputs, the rebuilt weights run as fast as weights inside the graph, with bit-identical results.
|
| 453 |
+
- OpenVINO 2026.5's compiler (8.4) fails this with NF4 weights ("ui4 -> quantile convert not supported"). The kit's compiler
|
| 454 |
+
reinterprets NF4 (the quantile type and quantized types stored as quantile) through its ui4 storage, and keeps cluster splits and
|
| 455 |
+
permutes of 4-bit values on byte boundaries (inside init schedules only).
|
| 456 |
+
- 2-bit weights (Bonsai) are not separated (the graph keeps its size) and compile ~7x slower, so they are left as they are.
|
| 457 |
+
- Nor are INT4 weights (a Granite 4.2 3B INT4 1-token part: 63.5 vs 63.6 MB). With 2+ rows the NPU 4000+ compiler's rewrite of INT4
|
| 458 |
+
into a lookup table (the 4-bit value used as the table index) clashes with it and fails; fixed, it only compiles 20-35x slower -
|
| 459 |
+
so INT4 models (the paged ones, without NPUW) do not use it, and the fused graphs stay off by default for them (each variant
|
| 460 |
+
would hold the weights again; the 16 GB Lunar Lake ran out of memory): the same as 2.1.0. INT8 is separated (an llm-jp-4.1-8B
|
| 461 |
+
INT8 part: 2.5 vs 216 MB). From Lunar Lake on, the NF4 models are the ones to use.
|
| 462 |
+
- **Shared weights**: graphs without weights still rebuild their weights on the NPU each, so every fused-graph length (1K to 32K
|
| 463 |
+
positions) and block size adds a copy (~4.8 GB each for an 8B NF4). After an init schedule has run, the kit's plugin matches its
|
| 464 |
+
output buffers against the existing ones (size, a sampled hash, then all bytes); when all match, the main schedule is bound to
|
| 465 |
+
those and the new buffer is released (`ONW_WS_SHARE=0` turns it off). So that graphs cut differently (the paged parts and the fused
|
| 466 |
+
graphs) can share, the init outputs are kept per weight (the compiler's `ONW_WS_NO_CONCAT`; OpenVINO concatenates them into one).
|
| 467 |
+
- Panther Lake, llm-jp-4.1-8B NF4: six fused variants loaded at once take +5.7 GB (~29 GB without sharing).
|
| 468 |
+
- Dropping one variant and loading another while their weights are shared can make the NPU driver write into released memory, so
|
| 469 |
+
3.0 keeps every fused variant and the paged graphs loaded (`ONW_FUSED_KEEP`; the memory-saving mode keeps loading only the length
|
| 470 |
+
in use). With every variant kept, the fused graphs go on up to the context: 8K / 16K / 32K positions too (2.1.0: up to 4K). Answers
|
| 471 |
+
and prompts run in the shortest variant covering the current position and move to the next one as it grows (a longer variant
|
| 472 |
+
reads a longer stretch of the KV cache every step).
|
| 473 |
+
- **Other compiler fixes**:
|
| 474 |
+
- The Qwen3.5 NF4 4-token graphs (MTP verification) failed with `'async.await' has non 'return' consumer` (a shape-only op left in
|
| 475 |
+
front of the function result) - without them no MTP.
|
| 476 |
+
- 2-bit weights with zero points (Bonsai) computing zeros with 2+ rows.
|
| 477 |
+
- Compile time: parallel compiles serialised on the Windows heap lock (a distribution map allocated ~45 KB even for one entry);
|
| 478 |
+
constant folding and the vertical-fusion search now reuse their results. Bit-identical output (a Qwen3.5-9B 64-token part
|
| 479 |
+
52.6 -> 34 s, ...).
|
| 480 |
+
|
| 481 |
+
For the results (Panther Lake against 2.1.0's default) see v3.0.0 in the CHANGELOG: caches a tenth of the model or less, a fraction
|
| 482 |
+
of the private memory, faster answers and prompts.
|
| 483 |
+
|
| 484 |
## Host-side work per step (onw 1.4)
|
| 485 |
|
| 486 |
Of an MTP block, ~47 ms was Python / numpy. Without changing a single output bit, it is now ~32 ms (the same tokens as
|
install.ps1
CHANGED
|
@@ -25,6 +25,7 @@ $info = Get-WithRetry "https://huggingface.co/api/models/$repo" $null
|
|
| 25 |
foreach ($s in $info.siblings) {
|
| 26 |
$f = $s.rfilename
|
| 27 |
if ($f -eq ".gitattributes") { continue }
|
|
|
|
| 28 |
$out = Join-Path $dest ($f -replace "/", "\")
|
| 29 |
New-Item -ItemType Directory -Force -Path (Split-Path $out) | Out-Null
|
| 30 |
Get-WithRetry "https://huggingface.co/$repo/resolve/main/$f" $out
|
|
|
|
| 25 |
foreach ($s in $info.siblings) {
|
| 26 |
$f = $s.rfilename
|
| 27 |
if ($f -eq ".gitattributes") { continue }
|
| 28 |
+
if ($f.StartsWith("onw/npukit/linux/")) { continue } # (the NPU kit for Linux)
|
| 29 |
$out = Join-Path $dest ($f -replace "/", "\")
|
| 30 |
New-Item -ItemType Directory -Force -Path (Split-Path $out) | Out-Null
|
| 31 |
Get-WithRetry "https://huggingface.co/$repo/resolve/main/$f" $out
|
install.sh
CHANGED
|
@@ -28,6 +28,7 @@ def fetch(url, tries=4): # the hub redirects to a CDN; a re
|
|
| 28 |
|
| 29 |
info = json.loads(fetch(f"https://huggingface.co/api/models/{repo}"))
|
| 30 |
files = [s["rfilename"] for s in info["siblings"] if s["rfilename"] != ".gitattributes"]
|
|
|
|
| 31 |
for f in files:
|
| 32 |
out = os.path.join(dest, f)
|
| 33 |
os.makedirs(os.path.dirname(out), exist_ok=True)
|
|
|
|
| 28 |
|
| 29 |
info = json.loads(fetch(f"https://huggingface.co/api/models/{repo}"))
|
| 30 |
files = [s["rfilename"] for s in info["siblings"] if s["rfilename"] != ".gitattributes"]
|
| 31 |
+
files = [f for f in files if not (f.startswith("onw/npukit/") and f.endswith(".dll"))] # (the NPU kit for Windows)
|
| 32 |
for f in files:
|
| 33 |
out = os.path.join(dest, f)
|
| 34 |
os.makedirs(os.path.dirname(out), exist_ok=True)
|
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__ = "
|
|
|
|
| 2 |
|
| 3 |
A small engine that runs LLMs entirely on the Intel NPU: static segment graphs + host-managed state.
|
| 4 |
"""
|
| 5 |
+
__version__ = "3.0.0"
|
onw/app.py
CHANGED
|
@@ -32,15 +32,15 @@ CATALOG = [ # repo, title, takes images, download GB, decode speed (NPU 3720),
|
|
| 32 |
"エージェント・ツール呼び出し向けの軽量モデル(Qwen3.5-4B ベース)"),
|
| 33 |
("ryugyosoft/granite-4.2-3b-onw", "Granite 4.2 3B", False, 2.1, "7.7 tok/s",
|
| 34 |
"IBM の軽量モデル。思考とツール呼び出しに対応(日本語はやや弱め)"),
|
| 35 |
-
("ryugyosoft/granite-4.2-3b-nf4-onw", "Granite 4.2 3B (NF4)", False, 2.6, "
|
| 36 |
"Lunar Lake 以降専用の高速版(重み NF4、アテンションの入力と down_proj は INT8。INT4 版より高精度)"),
|
| 37 |
("ryugyosoft/granite-4.2-8b-onw", "Granite 4.2 8B", False, 5.0, "3.9 tok/s",
|
| 38 |
"IBM の 8B。思考とツール呼び出し・コーディング向け(メモリ 32 GB 以上推奨)"),
|
| 39 |
-
("ryugyosoft/granite-4.2-8b-nf4-onw", "Granite 4.2 8B (NF4)", False, 6.2, "
|
| 40 |
"Lunar Lake 以降専用の高速版(重み NF4、アテンションの入力と down_proj は INT8。INT4 版より高精度)"),
|
| 41 |
("ryugyosoft/llm-jp-4.1-8b-thinking-onw", "llm-jp-4.1-8B-thinking", False, 4.6, "4.5 tok/s",
|
| 42 |
"国立情報学研究所の日本語モデル。考えてから答えます(思考モードで思考を表示)"),
|
| 43 |
-
("ryugyosoft/llm-jp-4.1-8b-thinking-nf4-onw", "llm-jp-4.1-8B-thinking (NF4)", False, 6.1, "
|
| 44 |
"Lunar Lake 以降専用の高速版(重み NF4、アテンションの入力と down_proj は INT8。INT4 版より高精度)"),
|
| 45 |
("ryugyosoft/llm-jp-4.1-8b-thinking-int8-onw", "llm-jp-4.1-8B-thinking (INT8)", False, 7.7, "4.1 tok/s",
|
| 46 |
"llm-jp の高精度版(重み INT8。メモリ 24 GB 以上向け、16 GB の PC はメモリ節約モードで)"),
|
|
@@ -358,11 +358,30 @@ def context_of(repo, cfg):
|
|
| 358 |
return min(want, top) if top else want
|
| 359 |
|
| 360 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
def memory_full(repo, cfg, ram):
|
| 362 |
"""GB this model needs with the whole expert bank in memory (context length, 64-token blocks), or None."""
|
| 363 |
if repo not in MEMORY:
|
| 364 |
return None
|
| 365 |
-
base, s64, kv = MEMORY[repo]
|
| 366 |
return base + (s64 if big_blocks_on(cfg, ram) else 0.0) + kv * max(context_of(repo, cfg) - 1024, 0) / 1024 / 1024
|
| 367 |
|
| 368 |
|
|
@@ -423,8 +442,9 @@ def npu_arch():
|
|
| 423 |
if NPU_ARCH is None:
|
| 424 |
try:
|
| 425 |
import openvino as ov
|
|
|
|
| 426 |
from .runtime import npu_arch as arch
|
| 427 |
-
NPU_ARCH = arch(
|
| 428 |
except Exception:
|
| 429 |
NPU_ARCH = 0
|
| 430 |
return NPU_ARCH
|
|
|
|
| 32 |
"エージェント・ツール呼び出し向けの軽量モデル(Qwen3.5-4B ベース)"),
|
| 33 |
("ryugyosoft/granite-4.2-3b-onw", "Granite 4.2 3B", False, 2.1, "7.7 tok/s",
|
| 34 |
"IBM の軽量モデル。思考とツール呼び出しに対応(日本語はやや弱め)"),
|
| 35 |
+
("ryugyosoft/granite-4.2-3b-nf4-onw", "Granite 4.2 3B (NF4)", False, 2.6, "23 tok/s",
|
| 36 |
"Lunar Lake 以降専用の高速版(重み NF4、アテンションの入力と down_proj は INT8。INT4 版より高精度)"),
|
| 37 |
("ryugyosoft/granite-4.2-8b-onw", "Granite 4.2 8B", False, 5.0, "3.9 tok/s",
|
| 38 |
"IBM の 8B。思考とツール呼び出し・コーディング向け(メモリ 32 GB 以上推奨)"),
|
| 39 |
+
("ryugyosoft/granite-4.2-8b-nf4-onw", "Granite 4.2 8B (NF4)", False, 6.2, "13 tok/s",
|
| 40 |
"Lunar Lake 以降専用の高速版(重み NF4、アテンションの入力と down_proj は INT8。INT4 版より高精度)"),
|
| 41 |
("ryugyosoft/llm-jp-4.1-8b-thinking-onw", "llm-jp-4.1-8B-thinking", False, 4.6, "4.5 tok/s",
|
| 42 |
"国立情報学研究所の日本語モデル。考えてから答えます(思考モードで思考を表示)"),
|
| 43 |
+
("ryugyosoft/llm-jp-4.1-8b-thinking-nf4-onw", "llm-jp-4.1-8B-thinking (NF4)", False, 6.1, "14 tok/s",
|
| 44 |
"Lunar Lake 以降専用の高速版(重み NF4、アテンションの入力と down_proj は INT8。INT4 版より高精度)"),
|
| 45 |
("ryugyosoft/llm-jp-4.1-8b-thinking-int8-onw", "llm-jp-4.1-8B-thinking (INT8)", False, 7.7, "4.1 tok/s",
|
| 46 |
"llm-jp の高精度版(重み INT8。メモリ 24 GB 以上向け、16 GB の PC はメモリ節約モードで)"),
|
|
|
|
| 358 |
return min(want, top) if top else want
|
| 359 |
|
| 360 |
|
| 361 |
+
# [3.0] with onw's NPU kit (Windows and Linux, NPU 4000+): weightless graphs and one copy of the weights for every graph - peak
|
| 362 |
+
# working set measured on Panther Lake with every fused variant loaded (onw 3.0)
|
| 363 |
+
MEMORY_KIT = {
|
| 364 |
+
"ryugyosoft/granite-4.2-3b-nf4-onw": (4.8, 0.0, 80),
|
| 365 |
+
"ryugyosoft/granite-4.2-8b-nf4-onw": (9.8, 0.0, 80),
|
| 366 |
+
"ryugyosoft/llm-jp-4.1-8b-thinking-nf4-onw": (9.7, 0.0, 64),
|
| 367 |
+
"ryugyosoft/Qwen3.5-9B-nf4-onw": (6.0, 0.0, 16),
|
| 368 |
+
"ryugyosoft/Ornith-1.5-9B-nf4-onw": (6.0, 0.0, 16),
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def _kit():
|
| 373 |
+
try:
|
| 374 |
+
from . import npukit
|
| 375 |
+
return npukit.active() and npu_arch() >= 4000
|
| 376 |
+
except Exception:
|
| 377 |
+
return False
|
| 378 |
+
|
| 379 |
+
|
| 380 |
def memory_full(repo, cfg, ram):
|
| 381 |
"""GB this model needs with the whole expert bank in memory (context length, 64-token blocks), or None."""
|
| 382 |
if repo not in MEMORY:
|
| 383 |
return None
|
| 384 |
+
base, s64, kv = MEMORY_KIT[repo] if repo in MEMORY_KIT and _kit() else MEMORY[repo]
|
| 385 |
return base + (s64 if big_blocks_on(cfg, ram) else 0.0) + kv * max(context_of(repo, cfg) - 1024, 0) / 1024 / 1024
|
| 386 |
|
| 387 |
|
|
|
|
| 442 |
if NPU_ARCH is None:
|
| 443 |
try:
|
| 444 |
import openvino as ov
|
| 445 |
+
from . import npukit
|
| 446 |
from .runtime import npu_arch as arch
|
| 447 |
+
NPU_ARCH = arch(npukit.core())
|
| 448 |
except Exception:
|
| 449 |
NPU_ARCH = 0
|
| 450 |
return NPU_ARCH
|
onw/chat.py
CHANGED
|
@@ -5,6 +5,7 @@ import re
|
|
| 5 |
import hashlib, json, os, time
|
| 6 |
import numpy as np
|
| 7 |
import openvino as ov
|
|
|
|
| 8 |
from transformers import AutoTokenizer
|
| 9 |
|
| 10 |
from .runtime import Cancelled, SegmentedModel, all_sizes, grouped_wanted
|
|
@@ -112,7 +113,7 @@ class ChatEngine:
|
|
| 112 |
_cancel = None # should_stop of the request whose prompt is being fed
|
| 113 |
|
| 114 |
def __init__(self, engine_dir, device=None, pld=True, cache_dir=None, context=None):
|
| 115 |
-
core =
|
| 116 |
device = device or ("NPU" if npu_available(core) else "CPU")
|
| 117 |
cfg = {"CACHE_DIR": cache_dir or os.path.join(engine_dir, "npu_cache")} if device == "NPU" else \
|
| 118 |
{"INFERENCE_PRECISION_HINT": "f32"}
|
|
@@ -787,9 +788,14 @@ class ChatEngine:
|
|
| 787 |
# [2.0.4] the fused graphs long enough for this prompt and its answer, set before the prompt runs (its last
|
| 788 |
# block of 1-2 tokens goes to the answer graphs: a switch there would load them twice)
|
| 789 |
sizes = self.model._fused_sizes
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
if len(sizes) > 1 and n - reuse > 4:
|
| 791 |
-
self.model.fused_prepare(n, max(sizes))
|
| 792 |
-
|
|
|
|
| 793 |
try:
|
| 794 |
if B > reuse:
|
| 795 |
self._prefill(lids, reuse, B, embeds, pos, need_logits=False)
|
|
@@ -816,8 +822,9 @@ class ChatEngine:
|
|
| 816 |
self.model.swap_to(1, drop=16)
|
| 817 |
elif swap: # memory-saving mode: the 1-token graphs for the answer
|
| 818 |
self.model.start_size(1, need_gb=self.swap_gb + 1.0, then_drop=16)
|
| 819 |
-
if not swap:
|
| 820 |
-
self.model.
|
|
|
|
| 821 |
# exact only for plain greedy; not while block sizes are swapped (the verification blocks go away mid-answer)
|
| 822 |
use_pld = self.pld and plain_greedy and self.model.S > 1 and not swap and budget is None
|
| 823 |
# MTP drafts (Qwen3.5 dense): every sampling setting (exact), the thinking budget included
|
|
|
|
| 5 |
import hashlib, json, os, time
|
| 6 |
import numpy as np
|
| 7 |
import openvino as ov
|
| 8 |
+
from . import npukit
|
| 9 |
from transformers import AutoTokenizer
|
| 10 |
|
| 11 |
from .runtime import Cancelled, SegmentedModel, all_sizes, grouped_wanted
|
|
|
|
| 113 |
_cancel = None # should_stop of the request whose prompt is being fed
|
| 114 |
|
| 115 |
def __init__(self, engine_dir, device=None, pld=True, cache_dir=None, context=None):
|
| 116 |
+
core = npukit.core()
|
| 117 |
device = device or ("NPU" if npu_available(core) else "CPU")
|
| 118 |
cfg = {"CACHE_DIR": cache_dir or os.path.join(engine_dir, "npu_cache")} if device == "NPU" else \
|
| 119 |
{"INFERENCE_PRECISION_HINT": "f32"}
|
|
|
|
| 788 |
# [2.0.4] the fused graphs long enough for this prompt and its answer, set before the prompt runs (its last
|
| 789 |
# block of 1-2 tokens goes to the answer graphs: a switch there would load them twice)
|
| 790 |
sizes = self.model._fused_sizes
|
| 791 |
+
# ([3.0] every variant kept loaded: the shortest one for now - a longer one takes over at no cost once the
|
| 792 |
+
# prompt / the answer passes it (_step); a variant longer than needed reads its whole cache every block:
|
| 793 |
+
# a 21.8K-token prompt all in the 32K graphs ran at 30 tok/s)
|
| 794 |
+
keep = self.model.fused_keep_all()
|
| 795 |
if len(sizes) > 1 and n - reuse > 4:
|
| 796 |
+
self.model.fused_prepare(reuse + max(sizes) if keep else n, max(sizes))
|
| 797 |
+
ahead = 64 if keep else min(max_new_tokens, 1024)
|
| 798 |
+
self.model.fused_prepare(n + ahead, min(sizes))
|
| 799 |
try:
|
| 800 |
if B > reuse:
|
| 801 |
self._prefill(lids, reuse, B, embeds, pos, need_logits=False)
|
|
|
|
| 822 |
self.model.swap_to(1, drop=16)
|
| 823 |
elif swap: # memory-saving mode: the 1-token graphs for the answer
|
| 824 |
self.model.start_size(1, need_gb=self.swap_gb + 1.0, then_drop=16)
|
| 825 |
+
if not swap and getattr(self.model, "_fused_sizes", None): # [2.0.4] fused decode graphs for most answers
|
| 826 |
+
ahead = 64 if self.model.fused_keep_all() else min(max_new_tokens, 1024)
|
| 827 |
+
self.model.fused_prepare(self.model.cur + ahead)
|
| 828 |
# exact only for plain greedy; not while block sizes are swapped (the verification blocks go away mid-answer)
|
| 829 |
use_pld = self.pld and plain_greedy and self.model.S > 1 and not swap and budget is None
|
| 830 |
# MTP drafts (Qwen3.5 dense): every sampling setting (exact), the thinking budget included
|
onw/fused.py
CHANGED
|
@@ -196,7 +196,21 @@ def wanted(m):
|
|
| 196 |
[2.1.0] off by default - every variant's blob holds its own copy of the weights (Granite 4.2 8B NF4: ~42 GB of
|
| 197 |
cache against ~11 GB for the paged graphs); ONW_FUSED=1: on; ONW_FUSED_PROMPT=0: answers only)."""
|
| 198 |
import os
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
return None
|
| 201 |
if m.E or m.predict_k or m.window or m.ring or not m.segs:
|
| 202 |
return None
|
|
@@ -244,7 +258,8 @@ class FusedDecode:
|
|
| 244 |
cfg = {k: v for k, v in c["config"].items() if k != "CACHE_DIR"}
|
| 245 |
cache = c["config"].get("CACHE_DIR")
|
| 246 |
npuw = cfg.get("NPU_USE_NPUW") == "YES"
|
| 247 |
-
self.plan, self.common, producer, bins = [], {}, {}, {}
|
|
|
|
| 248 |
self.mask_kv_t = mk(ov.Type.f16, [1, self.Ct])
|
| 249 |
self.mask_self_t = mk(ov.Type.f16, [S, S])
|
| 250 |
t0, built = time.time(), 0
|
|
@@ -286,24 +301,30 @@ class FusedDecode:
|
|
| 286 |
log(f"[fused] {name} ({self.Ct} positions, NPUW): ready in {time.time() - t:.0f}s", flush=True)
|
| 287 |
built += 1
|
| 288 |
elif cache:
|
| 289 |
-
|
| 290 |
-
|
|
|
|
| 291 |
lk = _lock(bp) if bp and cm is None else None
|
| 292 |
if lk is not None:
|
| 293 |
lk.acquire() # (the background may be compiling this graph: wait for it)
|
| 294 |
try:
|
| 295 |
-
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
| 297 |
if not compile_only:
|
| 298 |
try:
|
| 299 |
-
cm = import_blob(core,
|
| 300 |
except (RuntimeError, OSError):
|
| 301 |
cm = None
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
|
|
|
| 305 |
if cm is None and not npuw:
|
| 306 |
-
cm = self._compile(core, model, device, cfg, bp, f"{name} ({self.Ct} positions)",
|
|
|
|
| 307 |
built += 1
|
| 308 |
finally:
|
| 309 |
if lk is not None:
|
|
@@ -352,16 +373,39 @@ class FusedDecode:
|
|
| 352 |
f"({built} compiled) in {time.time() - t0:.0f}s", flush=True)
|
| 353 |
self._mask_pos = None
|
| 354 |
|
|
|
|
|
|
|
| 355 |
@staticmethod
|
| 356 |
-
def _compile(core, model, device, cfg, bp, name, compile_only, log):
|
| 357 |
"""Compile one fused graph; with a blob path: written there, and (unless compile_only) imported back as the
|
| 358 |
driver's copy only. compile_only: no executor on the device - creating one next to running inference lost the
|
| 359 |
-
NPU on Panther Lake (ZE_RESULT_ERROR_DEVICE_LOST). Returns the compiled model, or None (compile_only).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
import os, io, time
|
| 361 |
from .runtime import import_blob, USED_BLOBS
|
|
|
|
|
|
|
| 362 |
t = time.time()
|
| 363 |
-
|
| 364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
if not bp:
|
| 366 |
return None if compile_only else cm
|
| 367 |
os.makedirs(os.path.dirname(bp), exist_ok=True)
|
|
@@ -372,7 +416,7 @@ class FusedDecode:
|
|
| 372 |
os.replace(f"{bp}.{os.getpid()}.tmp", bp)
|
| 373 |
del buf, cm
|
| 374 |
USED_BLOBS.add(bp)
|
| 375 |
-
return None if compile_only else import_blob(core, bp, device, cfg)
|
| 376 |
|
| 377 |
def step(self, ids, want_logits=True, embeds=None):
|
| 378 |
m, S, n = self.m, self.S, len(ids)
|
|
|
|
| 196 |
[2.1.0] off by default - every variant's blob holds its own copy of the weights (Granite 4.2 8B NF4: ~42 GB of
|
| 197 |
cache against ~11 GB for the paged graphs); ONW_FUSED=1: on; ONW_FUSED_PROMPT=0: answers only)."""
|
| 198 |
import os
|
| 199 |
+
# [3.0] on by default with onw's NPU kit (weightless blobs: the variants cost no disk, their weights are shared)
|
| 200 |
+
# (not on the NPU 3720: no weightless blobs there - every variant would hold its own weights)
|
| 201 |
+
from . import npukit
|
| 202 |
+
from .runtime import npu_arch
|
| 203 |
+
if m._ctx["device"] != "NPU" or not m.paged or m.kv is None:
|
| 204 |
+
return None
|
| 205 |
+
# (nor for 2-bit weights: they stay inside the blobs, so the fused graphs would hold a second copy - Ternary Bonsai 2
|
| 206 |
+
# 9B: 7.1 GB private instead of ~3.5)
|
| 207 |
+
# (nor when this model's graphs keep their weights - INT4 (runtime.int4_weights), or a part the compiler could not
|
| 208 |
+
# build without them: every variant would carry the weights again; Lunar Lake 16 GB, Granite 4.2 3B INT4 with the
|
| 209 |
+
# fused graphs ran out of memory)
|
| 210 |
+
from .runtime import _WEIGHTLESS
|
| 211 |
+
on = (npukit.active() and npu_arch(m._ctx["core"]) >= 4000 and m.meta.get("needs") != "u2"
|
| 212 |
+
and not _WEIGHTLESS.get("skip_model") and _WEIGHTLESS["ok"] is not False and not _WEIGHTLESS.get("full"))
|
| 213 |
+
if os.environ.get("ONW_FUSED", "1" if on else "0") != "1":
|
| 214 |
return None
|
| 215 |
if m.E or m.predict_k or m.window or m.ring or not m.segs:
|
| 216 |
return None
|
|
|
|
| 258 |
cfg = {k: v for k, v in c["config"].items() if k != "CACHE_DIR"}
|
| 259 |
cache = c["config"].get("CACHE_DIR")
|
| 260 |
npuw = cfg.get("NPU_USE_NPUW") == "YES"
|
| 261 |
+
self.plan, self.common, producer, bins = [], {}, {}, {}
|
| 262 |
+
bp = bpw = None # (bins: the mapped weight files, alive while building)
|
| 263 |
self.mask_kv_t = mk(ov.Type.f16, [1, self.Ct])
|
| 264 |
self.mask_self_t = mk(ov.Type.f16, [S, S])
|
| 265 |
t0, built = time.time(), 0
|
|
|
|
| 301 |
log(f"[fused] {name} ({self.Ct} positions, NPUW): ready in {time.time() - t:.0f}s", flush=True)
|
| 302 |
built += 1
|
| 303 |
elif cache:
|
| 304 |
+
key = [VERSION, c["tried"].key, _signature(model), sorted(cfg.items()), self.Cs]
|
| 305 |
+
bp = blob_path(cache, f"fused-{name[:-4]}-B{B}", key)
|
| 306 |
+
bpw = blob_path(cache, f"fused-{name[:-4]}-B{B}-wl", key)
|
| 307 |
lk = _lock(bp) if bp and cm is None else None
|
| 308 |
if lk is not None:
|
| 309 |
lk.acquire() # (the background may be compiling this graph: wait for it)
|
| 310 |
try:
|
| 311 |
+
# a weightless blob (the weights come from this model's constants at import) first, then a full one
|
| 312 |
+
for path, wl in ((bpw, True), (bp, False)) if bp else ():
|
| 313 |
+
if not os.path.exists(path):
|
| 314 |
+
continue
|
| 315 |
+
USED_BLOBS.add(path)
|
| 316 |
if not compile_only:
|
| 317 |
try:
|
| 318 |
+
cm = import_blob(core, path, device, {**cfg, "MODEL_PTR": model} if wl else cfg)
|
| 319 |
except (RuntimeError, OSError):
|
| 320 |
cm = None
|
| 321 |
+
break
|
| 322 |
+
if cm is None and compile_only and bp and (os.path.exists(bpw) or os.path.exists(bp)):
|
| 323 |
+
del model
|
| 324 |
+
continue
|
| 325 |
if cm is None and not npuw:
|
| 326 |
+
cm = self._compile(core, model, device, cfg, bp, bpw, f"{name} ({self.Ct} positions)",
|
| 327 |
+
compile_only, log)
|
| 328 |
built += 1
|
| 329 |
finally:
|
| 330 |
if lk is not None:
|
|
|
|
| 373 |
f"({built} compiled) in {time.time() - t0:.0f}s", flush=True)
|
| 374 |
self._mask_pos = None
|
| 375 |
|
| 376 |
+
_weightless = None # (False once a weightless compile failed in this process)
|
| 377 |
+
|
| 378 |
@staticmethod
|
| 379 |
+
def _compile(core, model, device, cfg, bp, bpw, name, compile_only, log):
|
| 380 |
"""Compile one fused graph; with a blob path: written there, and (unless compile_only) imported back as the
|
| 381 |
driver's copy only. compile_only: no executor on the device - creating one next to running inference lost the
|
| 382 |
+
NPU on Panther Lake (ZE_RESULT_ERROR_DEVICE_LOST). Returns the compiled model, or None (compile_only).
|
| 383 |
+
Weightless first (bpw): the blob keeps no weights (CACHE_MODE=OPTIMIZE_SIZE + ENABLE_WEIGHTLESS; the import
|
| 384 |
+
rebuilds them on the NPU from this model's constants, MODEL_PTR) - a fused graph holds a whole segment's
|
| 385 |
+
weights, ~1.2 GB each for an 8B model, so every variant would add a copy of the model to the cache otherwise
|
| 386 |
+
(~19 MB weightless). Needs a compiler that supports it for the weight format (onw's own build: NF4 too; pip
|
| 387 |
+
OpenVINO 2026.5: not NF4; NPU 3720: no) - else the full blob as before. ONW_FUSED_WEIGHTLESS=0: off."""
|
| 388 |
import os, io, time
|
| 389 |
from .runtime import import_blob, USED_BLOBS
|
| 390 |
+
base = {**cfg, "NPU_CREATE_EXECUTOR": "0"} if compile_only else cfg
|
| 391 |
+
cm, wl = None, False
|
| 392 |
t = time.time()
|
| 393 |
+
from .runtime import weightless_wanted
|
| 394 |
+
if bpw and FusedDecode._weightless is not False and os.environ.get("ONW_FUSED_WEIGHTLESS", "1") != "0" and weightless_wanted():
|
| 395 |
+
try:
|
| 396 |
+
cm = core.compile_model(model, device, {**base, "CACHE_MODE": "OPTIMIZE_SIZE",
|
| 397 |
+
"ENABLE_WEIGHTLESS": True, "NPU_COMPILER_TYPE": "PLUGIN"})
|
| 398 |
+
wl = FusedDecode._weightless = True
|
| 399 |
+
except RuntimeError as e:
|
| 400 |
+
FusedDecode._weightless = False
|
| 401 |
+
log(f"[fused] weightless blobs not supported here ({' '.join(str(e).split())[-120:]}): full blobs",
|
| 402 |
+
flush=True)
|
| 403 |
+
if cm is None:
|
| 404 |
+
cm = core.compile_model(model, device, base)
|
| 405 |
+
log(f"[fused] {name}: compiled{' weightless' if wl else ''} in {time.time() - t:.0f}s"
|
| 406 |
+
f"{' (background)' if compile_only else ''}", flush=True)
|
| 407 |
+
if wl:
|
| 408 |
+
bp = bpw
|
| 409 |
if not bp:
|
| 410 |
return None if compile_only else cm
|
| 411 |
os.makedirs(os.path.dirname(bp), exist_ok=True)
|
|
|
|
| 416 |
os.replace(f"{bp}.{os.getpid()}.tmp", bp)
|
| 417 |
del buf, cm
|
| 418 |
USED_BLOBS.add(bp)
|
| 419 |
+
return None if compile_only else import_blob(core, bp, device, {**cfg, "MODEL_PTR": model} if wl else cfg)
|
| 420 |
|
| 421 |
def step(self, ids, want_logits=True, embeds=None):
|
| 422 |
m, S, n = self.m, self.S, len(ids)
|
onw/manager.py
CHANGED
|
@@ -1,685 +1,686 @@
|
|
| 1 |
-
"""The onw manager: model list, downloads, server control and settings behind a small local HTTP API, used by the
|
| 2 |
-
window (onw/web/manage.html in a webview or a browser). It runs inside the tray process, so downloads keep going when
|
| 3 |
-
the window is closed; `onw window` without a tray runs its own copy.
|
| 4 |
-
|
| 5 |
-
GET /manage.html the page
|
| 6 |
-
GET /api/state everything the page shows (polled once a second)
|
| 7 |
-
POST /api/<action> load / unload / restart / download / cancel_download / delete / clear_cache /
|
| 8 |
-
folders / settings / open
|
| 9 |
-
|
| 10 |
-
Bound to 127.0.0.1 only; every /api call needs the token from <config>/manager.json (X-Onw-Token header or ?token=).
|
| 11 |
-
"""
|
| 12 |
-
import json, os, secrets, shutil, subprocess, sys, threading, time, webbrowser
|
| 13 |
-
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
| 14 |
-
from urllib.parse import parse_qs, urlparse
|
| 15 |
-
|
| 16 |
-
from . import app, cachever, __version__
|
| 17 |
-
from .cachever import rmtree
|
| 18 |
-
from . import i18n
|
| 19 |
-
from .i18n import T
|
| 20 |
-
|
| 21 |
-
WEB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web")
|
| 22 |
-
INFO = lambda: os.path.join(app.config_dir(), "manager.json")
|
| 23 |
-
SPEED = {c[0]: c[4] for c in app.CATALOG}
|
| 24 |
-
NOTE = {c[0]: c[5] for c in app.CATALOG}
|
| 25 |
-
LAST_SEEN = [time.time()] # when the page last asked for the state (a browser-only window keeps us alive)
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def _files(d):
|
| 29 |
-
"""{path: (size, mtime)} under a model folder, without the compile cache."""
|
| 30 |
-
out = {}
|
| 31 |
-
for root, dirs, files in os.walk(d):
|
| 32 |
-
dirs[:] = [x for x in dirs if x != "npu_cache"]
|
| 33 |
-
for f in files:
|
| 34 |
-
p = os.path.join(root, f)
|
| 35 |
-
try:
|
| 36 |
-
st = os.stat(p)
|
| 37 |
-
out[p] = (st.st_size, st.st_mtime_ns)
|
| 38 |
-
except OSError:
|
| 39 |
-
pass
|
| 40 |
-
return out
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def _to_fetch(siblings, dest):
|
| 44 |
-
"""Bytes a download into dest still has to fetch: files missing there, or older than the repo's (the hub's own
|
| 45 |
-
record of what it downloaded; without it, a size mismatch)."""
|
| 46 |
-
try:
|
| 47 |
-
from pathlib import Path
|
| 48 |
-
from huggingface_hub._local_folder import read_download_metadata
|
| 49 |
-
except ImportError:
|
| 50 |
-
read_download_metadata = None
|
| 51 |
-
n = 0
|
| 52 |
-
for s in siblings:
|
| 53 |
-
p = os.path.join(dest, s.rfilename)
|
| 54 |
-
if not os.path.exists(p):
|
| 55 |
-
n += s.size or 0
|
| 56 |
-
continue
|
| 57 |
-
want = s.lfs.sha256 if s.lfs else s.blob_id
|
| 58 |
-
try:
|
| 59 |
-
meta = read_download_metadata(Path(dest), s.rfilename) if read_download_metadata else None
|
| 60 |
-
except Exception:
|
| 61 |
-
meta = None
|
| 62 |
-
stale = (meta.etag != want) if meta is not None and want else os.path.getsize(p) != (s.size or 0)
|
| 63 |
-
n += (s.size or 0) if stale else 0
|
| 64 |
-
return n
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def _installed_version():
|
| 68 |
-
"""The version of the onw files on disk now (this tray process may still run an older one: files replaced by the
|
| 69 |
-
installer or by hand while it ran - the window warns, as settings it does not know would be dropped)."""
|
| 70 |
-
import re
|
| 71 |
-
try:
|
| 72 |
-
s = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "__init__.py"), encoding="utf8").read()
|
| 73 |
-
return re.search(r'__version__\s*=\s*"([^"]+)"', s).group(1)
|
| 74 |
-
except (OSError, AttributeError):
|
| 75 |
-
return __version__
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def find_manager():
|
| 79 |
-
"""{port, token} of a running manager (the tray), or None."""
|
| 80 |
-
try:
|
| 81 |
-
d = json.load(open(INFO()))
|
| 82 |
-
if app._alive(d["pid"], d.get("created")): # (a file left by a crash may name a reused pid)
|
| 83 |
-
return d
|
| 84 |
-
except (OSError, ValueError, KeyError):
|
| 85 |
-
pass
|
| 86 |
-
return None
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
class Manager:
|
| 90 |
-
def __init__(self):
|
| 91 |
-
self.lock = threading.Lock()
|
| 92 |
-
self.dl = None # {"repo", "proc", "dest", "total", "frac", "text", "error", "done", "t0"}
|
| 93 |
-
self.npu = None
|
| 94 |
-
self.ram = app.total_ram_gb()
|
| 95 |
-
self.busy = "" # a message about a long action (loading / deleting) for the page
|
| 96 |
-
self.busy_err = False
|
| 97 |
-
self.upd = {"latest": None, "tag": None, "notes": "", "checked": None, "state": "idle", "msg": "",
|
| 98 |
-
"checking": False, "error": ""}
|
| 99 |
-
self.on_updated = None # set by the tray: restart into the new version
|
| 100 |
-
self.on_update_found = None # set by the tray: notification
|
| 101 |
-
threading.Thread(target=self._update_loop, daemon=True).start()
|
| 102 |
-
threading.Thread(target=self._npu_info, daemon=True).start()
|
| 103 |
-
|
| 104 |
-
def _npu_info(self):
|
| 105 |
-
try:
|
| 106 |
-
import openvino as ov
|
| 107 |
-
from .
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
app.
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
"
|
| 133 |
-
"
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
m["
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
m["
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
"
|
| 158 |
-
"
|
| 159 |
-
"
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
"
|
| 171 |
-
"
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
"
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
"
|
| 178 |
-
|
| 179 |
-
"
|
| 180 |
-
"
|
| 181 |
-
"
|
| 182 |
-
"
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
"
|
| 186 |
-
"
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
"
|
| 191 |
-
"
|
| 192 |
-
"
|
| 193 |
-
"
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
dl["
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
allp
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
"
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
self.upd
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
self.upd
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
self.
|
| 588 |
-
self.send_header("Content-
|
| 589 |
-
self.send_header("
|
| 590 |
-
self.
|
| 591 |
-
self.
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
|
|
|
|
|
| 1 |
+
"""The onw manager: model list, downloads, server control and settings behind a small local HTTP API, used by the
|
| 2 |
+
window (onw/web/manage.html in a webview or a browser). It runs inside the tray process, so downloads keep going when
|
| 3 |
+
the window is closed; `onw window` without a tray runs its own copy.
|
| 4 |
+
|
| 5 |
+
GET /manage.html the page
|
| 6 |
+
GET /api/state everything the page shows (polled once a second)
|
| 7 |
+
POST /api/<action> load / unload / restart / download / cancel_download / delete / clear_cache /
|
| 8 |
+
folders / settings / open
|
| 9 |
+
|
| 10 |
+
Bound to 127.0.0.1 only; every /api call needs the token from <config>/manager.json (X-Onw-Token header or ?token=).
|
| 11 |
+
"""
|
| 12 |
+
import json, os, secrets, shutil, subprocess, sys, threading, time, webbrowser
|
| 13 |
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
| 14 |
+
from urllib.parse import parse_qs, urlparse
|
| 15 |
+
|
| 16 |
+
from . import app, cachever, __version__
|
| 17 |
+
from .cachever import rmtree
|
| 18 |
+
from . import i18n
|
| 19 |
+
from .i18n import T
|
| 20 |
+
|
| 21 |
+
WEB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web")
|
| 22 |
+
INFO = lambda: os.path.join(app.config_dir(), "manager.json")
|
| 23 |
+
SPEED = {c[0]: c[4] for c in app.CATALOG}
|
| 24 |
+
NOTE = {c[0]: c[5] for c in app.CATALOG}
|
| 25 |
+
LAST_SEEN = [time.time()] # when the page last asked for the state (a browser-only window keeps us alive)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _files(d):
|
| 29 |
+
"""{path: (size, mtime)} under a model folder, without the compile cache."""
|
| 30 |
+
out = {}
|
| 31 |
+
for root, dirs, files in os.walk(d):
|
| 32 |
+
dirs[:] = [x for x in dirs if x != "npu_cache"]
|
| 33 |
+
for f in files:
|
| 34 |
+
p = os.path.join(root, f)
|
| 35 |
+
try:
|
| 36 |
+
st = os.stat(p)
|
| 37 |
+
out[p] = (st.st_size, st.st_mtime_ns)
|
| 38 |
+
except OSError:
|
| 39 |
+
pass
|
| 40 |
+
return out
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _to_fetch(siblings, dest):
|
| 44 |
+
"""Bytes a download into dest still has to fetch: files missing there, or older than the repo's (the hub's own
|
| 45 |
+
record of what it downloaded; without it, a size mismatch)."""
|
| 46 |
+
try:
|
| 47 |
+
from pathlib import Path
|
| 48 |
+
from huggingface_hub._local_folder import read_download_metadata
|
| 49 |
+
except ImportError:
|
| 50 |
+
read_download_metadata = None
|
| 51 |
+
n = 0
|
| 52 |
+
for s in siblings:
|
| 53 |
+
p = os.path.join(dest, s.rfilename)
|
| 54 |
+
if not os.path.exists(p):
|
| 55 |
+
n += s.size or 0
|
| 56 |
+
continue
|
| 57 |
+
want = s.lfs.sha256 if s.lfs else s.blob_id
|
| 58 |
+
try:
|
| 59 |
+
meta = read_download_metadata(Path(dest), s.rfilename) if read_download_metadata else None
|
| 60 |
+
except Exception:
|
| 61 |
+
meta = None
|
| 62 |
+
stale = (meta.etag != want) if meta is not None and want else os.path.getsize(p) != (s.size or 0)
|
| 63 |
+
n += (s.size or 0) if stale else 0
|
| 64 |
+
return n
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _installed_version():
|
| 68 |
+
"""The version of the onw files on disk now (this tray process may still run an older one: files replaced by the
|
| 69 |
+
installer or by hand while it ran - the window warns, as settings it does not know would be dropped)."""
|
| 70 |
+
import re
|
| 71 |
+
try:
|
| 72 |
+
s = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "__init__.py"), encoding="utf8").read()
|
| 73 |
+
return re.search(r'__version__\s*=\s*"([^"]+)"', s).group(1)
|
| 74 |
+
except (OSError, AttributeError):
|
| 75 |
+
return __version__
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def find_manager():
|
| 79 |
+
"""{port, token} of a running manager (the tray), or None."""
|
| 80 |
+
try:
|
| 81 |
+
d = json.load(open(INFO()))
|
| 82 |
+
if app._alive(d["pid"], d.get("created")): # (a file left by a crash may name a reused pid)
|
| 83 |
+
return d
|
| 84 |
+
except (OSError, ValueError, KeyError):
|
| 85 |
+
pass
|
| 86 |
+
return None
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class Manager:
|
| 90 |
+
def __init__(self):
|
| 91 |
+
self.lock = threading.Lock()
|
| 92 |
+
self.dl = None # {"repo", "proc", "dest", "total", "frac", "text", "error", "done", "t0"}
|
| 93 |
+
self.npu = None
|
| 94 |
+
self.ram = app.total_ram_gb()
|
| 95 |
+
self.busy = "" # a message about a long action (loading / deleting) for the page
|
| 96 |
+
self.busy_err = False
|
| 97 |
+
self.upd = {"latest": None, "tag": None, "notes": "", "checked": None, "state": "idle", "msg": "",
|
| 98 |
+
"checking": False, "error": ""}
|
| 99 |
+
self.on_updated = None # set by the tray: restart into the new version
|
| 100 |
+
self.on_update_found = None # set by the tray: notification
|
| 101 |
+
threading.Thread(target=self._update_loop, daemon=True).start()
|
| 102 |
+
threading.Thread(target=self._npu_info, daemon=True).start()
|
| 103 |
+
|
| 104 |
+
def _npu_info(self):
|
| 105 |
+
try:
|
| 106 |
+
import openvino as ov
|
| 107 |
+
from . import npukit
|
| 108 |
+
from .runtime import npu_info, npu_arch
|
| 109 |
+
core = npukit.core()
|
| 110 |
+
app.NPU_ARCH = npu_arch(core)
|
| 111 |
+
self.npu = npu_info(core)
|
| 112 |
+
except Exception:
|
| 113 |
+
self.npu = "NPU: unknown"
|
| 114 |
+
|
| 115 |
+
# ---------------------------------------------------------------- state
|
| 116 |
+
def state(self):
|
| 117 |
+
LAST_SEEN[0] = time.time()
|
| 118 |
+
cfg = app.load_config()
|
| 119 |
+
inst = app.installed(cfg)
|
| 120 |
+
app.note_max_contexts(inst)
|
| 121 |
+
info = app.server_info()
|
| 122 |
+
status = app.server_status(cfg, info=info)
|
| 123 |
+
save = app.models_dir(cfg)
|
| 124 |
+
self._poll_download()
|
| 125 |
+
dl = self.dl
|
| 126 |
+
models = []
|
| 127 |
+
for repo, title, image, size, _, _ in app.catalog():
|
| 128 |
+
path = inst.get(repo)
|
| 129 |
+
mem = app.memory_estimate(repo, cfg, self.ram)
|
| 130 |
+
stream = app.expert_budget(repo, cfg, self.ram)
|
| 131 |
+
m = {"key": repo, "stream_gb": stream, "title": title, "catalog": True, "path": path, "image": image, "size_gb": size,
|
| 132 |
+
"mem_gb": mem, "fits": app.fits(mem, self.ram),
|
| 133 |
+
"speed": SPEED.get(repo, "") if stream is None else T("遅め(SSD)"),
|
| 134 |
+
"note": (T(NOTE.get(repo, "").split("(")[0]) + T("・エキスパートは SSD から読み込み(キャッシュ {0} GB)", f"{stream:g}"))
|
| 135 |
+
if stream else T(NOTE.get(repo, "")), "in_save_dir": bool(path) and os.path.dirname(path) == save,
|
| 136 |
+
"deletable": bool(path) and app.downloaded_by_onw(path), "cache_gb": self._cache_gb(path) if path else 0}
|
| 137 |
+
if app.mem_saver_on(repo, cfg):
|
| 138 |
+
m["note"] += T("・メモリ節約モード(回答の出だしは遅め)")
|
| 139 |
+
if path and cachever.stale(path):
|
| 140 |
+
m["note"] += T("・onw の更新で、次のロードでは NPU 向けの準備をやり直すことがあります(数分)")
|
| 141 |
+
m["think"] = app.think_kind(repo, path)
|
| 142 |
+
m["state"] = self._state_of(repo, path, info, status, dl)
|
| 143 |
+
if m["state"] == "ready" and app.update_interrupted(path):
|
| 144 |
+
m["state"], m["note"] = "outdated", T("更新が途中で止まっています(「更新」で残りを取り直します。それまでは正しく動かないことがあります)")
|
| 145 |
+
elif m["state"] == "ready" and app.outdated(repo, path):
|
| 146 |
+
m["state"], m["note"] = "outdated", T("新しい版があります(「更新」で変わったファイルだけ取り直します)")
|
| 147 |
+
if dl and dl["repo"] == repo and not dl.get("done"):
|
| 148 |
+
m["progress"] = dl["frac"]
|
| 149 |
+
m["note"] = T("ダウンロード中 {0}・ウィンドウを閉じても続きます", dl["text"])
|
| 150 |
+
elif dl and dl.get("done") == repo and dl.get("error"):
|
| 151 |
+
m["note"] = T("ダウンロードに失敗: {0}", dl["error"])
|
| 152 |
+
models.append(m)
|
| 153 |
+
for key, path in inst.items():
|
| 154 |
+
if key in app.CATALOG_REPOS:
|
| 155 |
+
continue
|
| 156 |
+
models.append({"key": key, "title": key, "catalog": False, "path": path, "image": None, "size_gb": None,
|
| 157 |
+
"mem_gb": None, "fits": True, "speed": "", "note": T("自分で置いたモデル・{0}", path),
|
| 158 |
+
"in_save_dir": os.path.dirname(path) == save, "deletable": app.downloaded_by_onw(path),
|
| 159 |
+
"cache_gb": self._cache_gb(path), "state": self._state_of(key, path, info, status, None),
|
| 160 |
+
"think": app.think_kind(key, path)})
|
| 161 |
+
if cachever.stale(path):
|
| 162 |
+
models[-1]["note"] += T("・onw の更新で、次のロードでは NPU 向けの準備をやり直すことがあります(数分)")
|
| 163 |
+
if app.update_interrupted(path): # (not in the catalog: no "update" to finish it with)
|
| 164 |
+
models[-1]["note"] += T("・ダウンロードが途中で止まっています(正しく動かないことがあります)")
|
| 165 |
+
downloading = dl["dest"] if dl and not dl.get("done") else None
|
| 166 |
+
for name, path in app.partial_downloads(cfg).items(): # stopped part-way: listed so they can be removed
|
| 167 |
+
if downloading and os.path.normcase(path) == os.path.normcase(downloading):
|
| 168 |
+
continue
|
| 169 |
+
models.append({"key": "partial:" + name, "title": name, "catalog": False, "path": path, "image": None,
|
| 170 |
+
"size_gb": None, "mem_gb": None, "fits": True, "speed": "", "partial": True,
|
| 171 |
+
"note": T("途中で止まったダウンロード・{0}", path), "in_save_dir": True, "deletable": True,
|
| 172 |
+
"cache_gb": 0, "state": "partial"})
|
| 173 |
+
return {"version": __version__, "installed_version": _installed_version(), "npu": self.npu or "", "ram_gb": self.ram,
|
| 174 |
+
"config": cfg, "api_url": app.api_url(cfg, info),
|
| 175 |
+
"models_dir_missing": app.models_dir_missing(cfg),
|
| 176 |
+
# the memory-saving switch is shown only when a model it applies to is in the folders
|
| 177 |
+
"saver_models": [app.model_title(k) for k, p in inst.items() if app.saver_applies(p, cfg)],
|
| 178 |
+
"server": {"status": status, "model": info and info.get("model"), "path": info and info.get("path"),
|
| 179 |
+
"progress": app.load_progress() if status == "loading" else None,
|
| 180 |
+
"recompile": app.recompile_reason() if status == "loading" else None,
|
| 181 |
+
"error": app.failure_reason() if status == "failed" else "",
|
| 182 |
+
"title": app.model_title(info["model"]) if info else None, "port": info and info.get("port"),
|
| 183 |
+
"speed": SPEED.get(info["model"], "") if info else "",
|
| 184 |
+
# (the setting at most the model's longest: the setting itself applies from the next start)
|
| 185 |
+
"context": app.context_of(info["model"], cfg) if info else None,
|
| 186 |
+
"max_context": app.MAX_CONTEXT.get(info["model"]) if info else None,
|
| 187 |
+
"mem_gb": app.memory_estimate(info["model"], cfg, self.ram) if info else None},
|
| 188 |
+
"save_dir": save, "folders": app.model_folders(cfg, existing=False)[1:], "models": models,
|
| 189 |
+
# (a start that failed from any path - this window, the tray, an update - until the next start / stop)
|
| 190 |
+
"busy": self.busy or (T("ロードできません: {0}", app.LAST_ERROR[0]) if app.LAST_ERROR[0] else ""),
|
| 191 |
+
"busy_err": self.busy_err or bool(not self.busy and app.LAST_ERROR[0]),
|
| 192 |
+
"log": os.path.join(app.config_dir(), "server.log"), "store_python": app.store_python(),
|
| 193 |
+
"deps_error": self._deps_error(), "usage": self._usage(info),
|
| 194 |
+
"update": {**self.upd, "can_update": self._can_update(), "current": __version__}}
|
| 195 |
+
|
| 196 |
+
@staticmethod
|
| 197 |
+
def _usage(info):
|
| 198 |
+
"""Memory (onw's server, the whole machine) and the NPU's load for the server tab (onw.usage)."""
|
| 199 |
+
try:
|
| 200 |
+
from . import usage
|
| 201 |
+
return usage.sample(info["pid"] if info else None)
|
| 202 |
+
except Exception:
|
| 203 |
+
return None
|
| 204 |
+
|
| 205 |
+
@staticmethod
|
| 206 |
+
def _deps_error():
|
| 207 |
+
"""The packages of this version could not be installed (pip failed in the update's restart): its error."""
|
| 208 |
+
from . import deps, update
|
| 209 |
+
d = update.install_dir()
|
| 210 |
+
return deps.failed(d) if d and not deps.current(d) else None
|
| 211 |
+
|
| 212 |
+
@staticmethod
|
| 213 |
+
def _state_of(key, path, info, status, dl):
|
| 214 |
+
if info and info.get("model") == key:
|
| 215 |
+
return "live" if status == "running" else ("loading" if status == "loading" else "failed")
|
| 216 |
+
if dl and dl["repo"] == key and not dl.get("done"): # (a finished download is just there: ready)
|
| 217 |
+
return "downloading"
|
| 218 |
+
if path:
|
| 219 |
+
return "ready"
|
| 220 |
+
return "missing"
|
| 221 |
+
|
| 222 |
+
@staticmethod
|
| 223 |
+
def _cache_gb(path):
|
| 224 |
+
c = os.path.join(path, "npu_cache")
|
| 225 |
+
if not os.path.isdir(c):
|
| 226 |
+
return 0
|
| 227 |
+
n = 0
|
| 228 |
+
for r, _, fs in os.walk(c):
|
| 229 |
+
for f in fs:
|
| 230 |
+
try:
|
| 231 |
+
n += os.path.getsize(os.path.join(r, f))
|
| 232 |
+
except OSError:
|
| 233 |
+
pass
|
| 234 |
+
return round(n / 2**30, 1)
|
| 235 |
+
|
| 236 |
+
# ---------------------------------------------------------------- downloads (a child process per download)
|
| 237 |
+
def download(self, repo):
|
| 238 |
+
if repo not in app.CATALOG_REPOS:
|
| 239 |
+
return {"error": T("そのモデルはありません")}
|
| 240 |
+
if repo in app.NF4_ONLY and app.npu_arch() < 4000: # (NF4 and 2-bit weights)
|
| 241 |
+
return {"error": T("このモデルは Lunar Lake 以降の NPU 専用です")}
|
| 242 |
+
info = app.server_info()
|
| 243 |
+
if info and info.get("model") == repo: # updating it: its files are open in the server
|
| 244 |
+
return {"error": T("このモデルはロード中です。先にアンロードしてください。")}
|
| 245 |
+
if self.upd["state"] == "updating":
|
| 246 |
+
return {"error": T("onw の更新中です。終わってからダウンロードしてください")}
|
| 247 |
+
with self.lock:
|
| 248 |
+
if self.dl and self.dl["proc"].poll() is None:
|
| 249 |
+
return {"error": T("別のダウンロードが進行中です")}
|
| 250 |
+
dest = os.path.join(app.models_dir(app.load_config()), repo.split("/")[-1])
|
| 251 |
+
try: # (the network call outside the lock: cancel stays instant)
|
| 252 |
+
from huggingface_hub import HfApi
|
| 253 |
+
total = _to_fetch(HfApi().model_info(repo, files_metadata=True).siblings, dest) or 1
|
| 254 |
+
except Exception as e:
|
| 255 |
+
return {"error": T("Hugging Face に接続できません: {0}", e)}
|
| 256 |
+
with self.lock:
|
| 257 |
+
if self.dl and self.dl["proc"].poll() is None:
|
| 258 |
+
return {"error": T("別のダウンロードが進行中です")}
|
| 259 |
+
log = os.path.join(app.config_dir(), "download.log")
|
| 260 |
+
before = _files(dest) # (before the child writes anything)
|
| 261 |
+
kw = {"creationflags": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {}
|
| 262 |
+
with open(log, "w", encoding="utf8") as out: # a file, not a pipe: a chatty download never blocks on it
|
| 263 |
+
proc = subprocess.Popen([sys.executable, "-m", "onw.dl", repo, dest], stdout=out, stderr=subprocess.STDOUT,
|
| 264 |
+
env={**os.environ, "PYTHONUTF8": "1"}, **kw)
|
| 265 |
+
self.dl = {"repo": repo, "proc": proc, "dest": dest, "total": total, "frac": 0.0, "text": T("準備中…"),
|
| 266 |
+
"error": None, "done": None, "t0": time.time(), "log": log, "before": before}
|
| 267 |
+
app.ACTIVE_DOWNLOAD[0] = dest # (an update: not loaded while its files are replaced)
|
| 268 |
+
return {}
|
| 269 |
+
|
| 270 |
+
def cancel_download(self):
|
| 271 |
+
with self.lock:
|
| 272 |
+
if self.dl and self.dl["proc"].poll() is None:
|
| 273 |
+
self.dl["proc"].terminate()
|
| 274 |
+
try:
|
| 275 |
+
self.dl["proc"].wait(10)
|
| 276 |
+
except subprocess.TimeoutExpired:
|
| 277 |
+
self.dl["proc"].kill()
|
| 278 |
+
try:
|
| 279 |
+
self.dl["proc"].wait(5)
|
| 280 |
+
except subprocess.TimeoutExpired:
|
| 281 |
+
pass
|
| 282 |
+
self._update_stopped(self.dl)
|
| 283 |
+
self.dl = None
|
| 284 |
+
app.ACTIVE_DOWNLOAD[0] = None
|
| 285 |
+
return {}
|
| 286 |
+
|
| 287 |
+
@staticmethod
|
| 288 |
+
def _update_stopped(dl):
|
| 289 |
+
"""An update of a complete model cancelled or failed: when it replaced no file yet, the model is just as it
|
| 290 |
+
was - the marker goes (the hub's partial files stay in .cache for the next try). Otherwise the folder stays
|
| 291 |
+
marked: listed as an update to finish (app.update_interrupted), never as a partial download to delete."""
|
| 292 |
+
d, before = dl["dest"], dl["before"]
|
| 293 |
+
mark = os.path.join(d, ".downloading")
|
| 294 |
+
if not app.downloaded_by_onw(d) or mark in before: # (a first download / one stopped before this try)
|
| 295 |
+
return
|
| 296 |
+
skip = (os.path.join(d, ".cache") + os.sep, mark)
|
| 297 |
+
mine = lambda files: {p: st for p, st in files.items() if not p.startswith(skip)}
|
| 298 |
+
if mine(_files(d)) == mine(before):
|
| 299 |
+
try:
|
| 300 |
+
os.remove(mark)
|
| 301 |
+
except OSError:
|
| 302 |
+
pass
|
| 303 |
+
|
| 304 |
+
def _poll_download(self):
|
| 305 |
+
dl = self.dl
|
| 306 |
+
if not dl or dl.get("done"):
|
| 307 |
+
return
|
| 308 |
+
before = dl["before"] # only what this download wrote (an update keeps the rest)
|
| 309 |
+
n = sum(st[0] for p, st in _files(dl["dest"]).items() if before.get(p) != st)
|
| 310 |
+
dl["frac"] = min(n / dl["total"], 0.99)
|
| 311 |
+
dl["text"] = f"{n / 2**30:.1f} / {dl['total'] / 2**30:.1f} GB ({dl['frac'] * 100:.0f}%)"
|
| 312 |
+
rc = dl["proc"].poll()
|
| 313 |
+
if rc is not None:
|
| 314 |
+
dl["done"] = dl["repo"]
|
| 315 |
+
app.ACTIVE_DOWNLOAD[0] = None
|
| 316 |
+
if rc != 0:
|
| 317 |
+
try:
|
| 318 |
+
out = open(dl["log"], encoding="utf8", errors="replace").read()[-4000:]
|
| 319 |
+
except OSError:
|
| 320 |
+
out = ""
|
| 321 |
+
dl["error"] = " ".join(out.split()[-30:]) or f"exit code {rc}"
|
| 322 |
+
self._update_stopped(dl)
|
| 323 |
+
else:
|
| 324 |
+
dl["frac"] = 1.0
|
| 325 |
+
with app._CFG_LOCK:
|
| 326 |
+
if not app.load_config().get("model"):
|
| 327 |
+
app.update_config(model=dl["repo"])
|
| 328 |
+
|
| 329 |
+
# ---------------------------------------------------------------- server
|
| 330 |
+
def load(self, key):
|
| 331 |
+
if key not in app.installed(app.load_config()):
|
| 332 |
+
return {"error": T("そのモデルはありません")}
|
| 333 |
+
with self.lock: # (a second click while the first load is still starting)
|
| 334 |
+
if self.busy and not self.busy_err:
|
| 335 |
+
return {"error": T("ロードの準備中です。少しお待ちください")}
|
| 336 |
+
self.busy, self.busy_err = T("ロードしています…"), False
|
| 337 |
+
try:
|
| 338 |
+
cfg = app.update_config(model=key)
|
| 339 |
+
except Exception:
|
| 340 |
+
self.busy = ""
|
| 341 |
+
raise
|
| 342 |
+
|
| 343 |
+
def work():
|
| 344 |
+
try:
|
| 345 |
+
app.start_server(cfg)
|
| 346 |
+
except Exception: # (the reason: app.LAST_ERROR, shown by state())
|
| 347 |
+
pass
|
| 348 |
+
self.busy, self.busy_err = "", False
|
| 349 |
+
threading.Thread(target=work, daemon=True).start()
|
| 350 |
+
return {}
|
| 351 |
+
|
| 352 |
+
def unload(self):
|
| 353 |
+
threading.Thread(target=app.stop_server, daemon=True).start()
|
| 354 |
+
return {}
|
| 355 |
+
|
| 356 |
+
def restart(self):
|
| 357 |
+
info = app.server_info()
|
| 358 |
+
return self.load(info["model"]) if info else {}
|
| 359 |
+
|
| 360 |
+
# ---------------------------------------------------------------- models
|
| 361 |
+
def delete(self, key):
|
| 362 |
+
cfg = app.load_config()
|
| 363 |
+
if key.startswith("partial:"): # a download that stopped part-way
|
| 364 |
+
p = app.partial_downloads(cfg).get(key[8:])
|
| 365 |
+
if not p or (self.dl and not self.dl.get("done") and os.path.normcase(self.dl["dest"]) == os.path.normcase(p)):
|
| 366 |
+
return {"error": T("そのモデルはありません")}
|
| 367 |
+
rmtree(p)
|
| 368 |
+
return {}
|
| 369 |
+
inst = app.installed(cfg)
|
| 370 |
+
info = app.server_info()
|
| 371 |
+
if key not in inst:
|
| 372 |
+
return {"error": T("そのモデルはありません")}
|
| 373 |
+
if info and info.get("model") == key:
|
| 374 |
+
return {"error": T("このモデルはロード中です。先にアンロードしてください。")}
|
| 375 |
+
if not app.downloaded_by_onw(inst[key]):
|
| 376 |
+
return {"error": T("onw がダウンロードしたモデルではないので、ここからは削除しません。")}
|
| 377 |
+
rmtree(inst[key]) # (the compile cache inside: read-only files)
|
| 378 |
+
return {}
|
| 379 |
+
|
| 380 |
+
def clear_cache(self, key):
|
| 381 |
+
cfg = app.load_config()
|
| 382 |
+
inst = app.installed(cfg)
|
| 383 |
+
info = app.server_info()
|
| 384 |
+
if key not in inst:
|
| 385 |
+
return {"error": T("そのモデルはありません")}
|
| 386 |
+
if info and info.get("model") == key:
|
| 387 |
+
return {"error": T("このモデルはロード中です。先にアンロードしてください。")}
|
| 388 |
+
rmtree(os.path.join(inst[key], "npu_cache"))
|
| 389 |
+
return {}
|
| 390 |
+
|
| 391 |
+
def folders(self, save_dir=None, add=None, remove=None):
|
| 392 |
+
with app._CFG_LOCK:
|
| 393 |
+
return self._folders(save_dir, add, remove)
|
| 394 |
+
|
| 395 |
+
def _folders(self, save_dir=None, add=None, remove=None):
|
| 396 |
+
cfg = app.load_config()
|
| 397 |
+
nc = os.path.normcase
|
| 398 |
+
if save_dir:
|
| 399 |
+
old, new = app.models_dir(cfg), os.path.abspath(save_dir)
|
| 400 |
+
try:
|
| 401 |
+
os.makedirs(new, exist_ok=True)
|
| 402 |
+
except OSError as e:
|
| 403 |
+
return {"error": T("そのフォルダは使えません: {0}", e)}
|
| 404 |
+
extra = [x for x in cfg.get("model_folders") or [] if nc(x) != nc(new)]
|
| 405 |
+
if nc(old) != nc(new) and app.installed({**cfg, "model_folders": []}):
|
| 406 |
+
extra.append(old) # keep finding the models already downloaded to the old place
|
| 407 |
+
cfg.update(models_dir=new, model_folders=extra)
|
| 408 |
+
if add:
|
| 409 |
+
d = os.path.abspath(add)
|
| 410 |
+
if not os.path.isdir(d):
|
| 411 |
+
return {"error": T("フォルダが見つかりません: {0}", d)}
|
| 412 |
+
if nc(d) not in map(nc, app.model_folders(cfg)):
|
| 413 |
+
cfg["model_folders"] = (cfg.get("model_folders") or []) + [d]
|
| 414 |
+
if remove:
|
| 415 |
+
cfg["model_folders"] = [x for x in cfg.get("model_folders") or [] if nc(x) != nc(os.path.abspath(remove))]
|
| 416 |
+
app.save_config(cfg)
|
| 417 |
+
return {}
|
| 418 |
+
|
| 419 |
+
def settings(self, new):
|
| 420 |
+
with app._CFG_LOCK:
|
| 421 |
+
return self._settings(new)
|
| 422 |
+
|
| 423 |
+
def model_prefs(self, key, prefs):
|
| 424 |
+
"""A model's defaults for the API and the chat page (the server reads them per request: no restart):
|
| 425 |
+
think off | low | medium | high, sampling greedy | recommended | custom and the custom values."""
|
| 426 |
+
if not key:
|
| 427 |
+
return {"error": T("モデルが選ばれていません")}
|
| 428 |
+
p = {}
|
| 429 |
+
if prefs.get("think") in ("off", "low", "medium", "high"):
|
| 430 |
+
p["think"] = prefs["think"]
|
| 431 |
+
if prefs.get("sampling") in ("greedy", "recommended", "custom"):
|
| 432 |
+
p["sampling"] = prefs["sampling"]
|
| 433 |
+
try:
|
| 434 |
+
for k, cast, lo, hi in (("temperature", float, 0, 2), ("top_p", float, 0.01, 1), ("top_k", int, 0, 1000),
|
| 435 |
+
("presence_penalty", float, 0, 2)):
|
| 436 |
+
if prefs.get(k) not in (None, ""):
|
| 437 |
+
p[k] = min(max(cast(float(prefs[k])), lo), hi)
|
| 438 |
+
except (TypeError, ValueError):
|
| 439 |
+
return {"error": T("生成設定の値が不正です")}
|
| 440 |
+
with app._CFG_LOCK:
|
| 441 |
+
cfg = app.load_config()
|
| 442 |
+
allp = dict(cfg.get("model_prefs") or {})
|
| 443 |
+
allp[key] = p
|
| 444 |
+
cfg["model_prefs"] = allp
|
| 445 |
+
app.save_config(cfg)
|
| 446 |
+
return {"ok": True}
|
| 447 |
+
|
| 448 |
+
def _settings(self, new):
|
| 449 |
+
cfg = app.load_config()
|
| 450 |
+
try:
|
| 451 |
+
port, ctx = int(new.get("port", cfg["port"])), int(new.get("context", cfg["context"]))
|
| 452 |
+
if not (1 <= port <= 65535) or ctx < 256:
|
| 453 |
+
raise ValueError
|
| 454 |
+
except (TypeError, ValueError):
|
| 455 |
+
return {"error": T("ポートかコンテキスト長の値が不正です")}
|
| 456 |
+
from .i18n import LANGS
|
| 457 |
+
if new.get("lang", "auto") not in ("auto", *LANGS):
|
| 458 |
+
new = {k: v for k, v in new.items() if k != "lang"}
|
| 459 |
+
old = dict(cfg)
|
| 460 |
+
for k in ("lan", "api_key", "device", "pld", "npu_turbo", "big_blocks", "npuw", "expert_gb", "autostart_server", "login_start",
|
| 461 |
+
"update_check", "lang", "npuw_moe", "npuw_async", "cache_size", "mem_saver", "mem_saver_strict", "think_default"):
|
| 462 |
+
if k in new:
|
| 463 |
+
cfg[k] = new[k]
|
| 464 |
+
if new.get("api_sampling") in ("greedy", "recommended", "custom"):
|
| 465 |
+
cfg["api_sampling"] = new["api_sampling"]
|
| 466 |
+
try:
|
| 467 |
+
for k, cast, lo, hi in (("api_temperature", float, 0, 2), ("api_top_p", float, 0.01, 1), ("api_top_k", int, 0, 1000),
|
| 468 |
+
("api_presence", float, 0, 2)):
|
| 469 |
+
if k in new:
|
| 470 |
+
cfg[k] = min(max(cast(float(new[k])), lo), hi)
|
| 471 |
+
except (TypeError, ValueError):
|
| 472 |
+
return {"error": T("生成設定の値が不正です")}
|
| 473 |
+
if "api_max_tokens" in new: # (0: up to the end of the context)
|
| 474 |
+
try:
|
| 475 |
+
cfg["api_max_tokens"] = max(0, int(new["api_max_tokens"]))
|
| 476 |
+
except (TypeError, ValueError):
|
| 477 |
+
return {"error": T("生成設定の値が不正です")}
|
| 478 |
+
if ctx != cfg["context"]:
|
| 479 |
+
cfg["context_chosen"] = True # (the old 1024 default moves up; a choice stays)
|
| 480 |
+
cfg["port"], cfg["context"] = port, ctx
|
| 481 |
+
server_keys = ("lan", "api_key", "device", "pld", "npu_turbo", "api_max_tokens", "big_blocks", "npuw", "expert_gb", "port", "context",
|
| 482 |
+
"npuw_moe", "npuw_async", "cache_size", "mem_saver", "mem_saver_strict", "think_default",
|
| 483 |
+
"api_sampling", "api_temperature", "api_top_p", "api_top_k", "api_presence")
|
| 484 |
+
app.save_config(cfg)
|
| 485 |
+
i18n.current(cfg) # messages switch language right away
|
| 486 |
+
try:
|
| 487 |
+
app.set_login_start(bool(cfg["login_start"]))
|
| 488 |
+
except OSError:
|
| 489 |
+
pass
|
| 490 |
+
changed = any(str(old.get(k)) != str(cfg.get(k)) for k in server_keys)
|
| 491 |
+
return {"restart_needed": changed and app.server_info() is not None}
|
| 492 |
+
|
| 493 |
+
# ---------------------------------------------------------------- updates of onw itself
|
| 494 |
+
@staticmethod
|
| 495 |
+
def _can_update():
|
| 496 |
+
from . import update
|
| 497 |
+
return update.can_update()
|
| 498 |
+
|
| 499 |
+
def _update_loop(self):
|
| 500 |
+
time.sleep(30) # let the tray and the server start first
|
| 501 |
+
while True:
|
| 502 |
+
if app.load_config().get("update_check", True):
|
| 503 |
+
self.check_update()
|
| 504 |
+
time.sleep(24 * 3600)
|
| 505 |
+
|
| 506 |
+
def start_check(self):
|
| 507 |
+
"""The window's "Check now": checks in the background and answers at once; the page follows
|
| 508 |
+
upd["checking"] / upd["error"] (a slow connection must not leave the button without an answer)."""
|
| 509 |
+
if not self.upd["checking"]:
|
| 510 |
+
threading.Thread(target=self.check_update, daemon=True).start()
|
| 511 |
+
return {"checking": True}
|
| 512 |
+
|
| 513 |
+
def check_update(self):
|
| 514 |
+
self.upd.update(checking=True, error="")
|
| 515 |
+
try:
|
| 516 |
+
r = self._check_update()
|
| 517 |
+
except Exception as e:
|
| 518 |
+
r = {"error": T("更新を確認できませんでした: {0}", e)}
|
| 519 |
+
self.upd.update(checking=False, error=r.get("error", ""))
|
| 520 |
+
return r
|
| 521 |
+
|
| 522 |
+
def _check_update(self):
|
| 523 |
+
from . import update
|
| 524 |
+
found = update.latest()
|
| 525 |
+
self.upd["checked"] = time.time()
|
| 526 |
+
if found is None:
|
| 527 |
+
return {"error": T("更新を確認できませんでした(オフライン?)")}
|
| 528 |
+
v, tag = found
|
| 529 |
+
if update.newer(v):
|
| 530 |
+
fresh = self.upd["latest"] != v
|
| 531 |
+
self.upd.update(latest=v, tag=tag, notes=update.notes(lang=i18n.current()))
|
| 532 |
+
if fresh and self.on_update_found:
|
| 533 |
+
try:
|
| 534 |
+
self.on_update_found(v)
|
| 535 |
+
except Exception:
|
| 536 |
+
pass
|
| 537 |
+
else:
|
| 538 |
+
self.upd.update(latest=None, tag=None, notes="")
|
| 539 |
+
return {"latest": self.upd["latest"]}
|
| 540 |
+
|
| 541 |
+
def run_update(self):
|
| 542 |
+
from . import update
|
| 543 |
+
if not self.upd["tag"]:
|
| 544 |
+
return {"error": T("新しい版はありません")}
|
| 545 |
+
if not update.can_update():
|
| 546 |
+
return {"error": T("このフォルダの onw は 1 行インストーラーで入れたものではないので、自動更新できません")}
|
| 547 |
+
if self.upd["state"] == "updating":
|
| 548 |
+
return {}
|
| 549 |
+
if self.dl and self.dl["proc"].poll() is None: # the restart would orphan the download
|
| 550 |
+
return {"error": T("ダウンロードが終わってから更新してください")}
|
| 551 |
+
self.upd.update(state="updating", msg=T("準備中…"))
|
| 552 |
+
|
| 553 |
+
def work():
|
| 554 |
+
try:
|
| 555 |
+
v = update.apply(self.upd["tag"], progress=lambda t: self.upd.update(msg=t))
|
| 556 |
+
self.upd.update(state="done", msg=T("v{0} に更新しました。再起動します…", v))
|
| 557 |
+
if self.on_updated:
|
| 558 |
+
self.on_updated()
|
| 559 |
+
else:
|
| 560 |
+
self.upd["msg"] = T("v{0} に更新しました。onw を起動し直してください", v)
|
| 561 |
+
except Exception as e:
|
| 562 |
+
self.upd.update(state="error", msg=T("更新できませんでした: {0}", e))
|
| 563 |
+
threading.Thread(target=work, daemon=True).start()
|
| 564 |
+
return {}
|
| 565 |
+
|
| 566 |
+
def open(self, what, key=None):
|
| 567 |
+
cfg = app.load_config()
|
| 568 |
+
if what == "log":
|
| 569 |
+
app.open_path(os.path.join(app.config_dir(), "server.log"))
|
| 570 |
+
elif what == "folder":
|
| 571 |
+
p = app.installed(cfg).get(key) if key else None
|
| 572 |
+
app.open_path(p or app.models_dir(cfg))
|
| 573 |
+
elif what in ("chat", "check"):
|
| 574 |
+
webbrowser.open(f"http://127.0.0.1:{app.server_port(cfg)}/{what}")
|
| 575 |
+
return {}
|
| 576 |
+
|
| 577 |
+
|
| 578 |
+
class _Handler(BaseHTTPRequestHandler):
|
| 579 |
+
manager: Manager = None
|
| 580 |
+
token: str = ""
|
| 581 |
+
|
| 582 |
+
def log_message(self, *a): # quiet
|
| 583 |
+
pass
|
| 584 |
+
|
| 585 |
+
def _send(self, code, body, ctype="application/json"):
|
| 586 |
+
data = body if isinstance(body, bytes) else json.dumps(body, ensure_ascii=False).encode("utf8")
|
| 587 |
+
self.send_response(code)
|
| 588 |
+
self.send_header("Content-Type", ctype + ("; charset=utf-8" if ctype.startswith("text") else ""))
|
| 589 |
+
self.send_header("Content-Length", str(len(data)))
|
| 590 |
+
self.send_header("Cache-Control", "no-store")
|
| 591 |
+
self.end_headers()
|
| 592 |
+
self.wfile.write(data)
|
| 593 |
+
|
| 594 |
+
def _authed(self, q):
|
| 595 |
+
t = self.headers.get("X-Onw-Token") or (q.get("token") or [""])[0]
|
| 596 |
+
return secrets.compare_digest(t, self.token)
|
| 597 |
+
|
| 598 |
+
def do_GET(self):
|
| 599 |
+
u = urlparse(self.path)
|
| 600 |
+
q = parse_qs(u.query)
|
| 601 |
+
if u.path in ("/", "/manage.html"):
|
| 602 |
+
return self._send(200, i18n.page(os.path.join(WEB, "manage.html")), "text/html")
|
| 603 |
+
if u.path == "/api/state":
|
| 604 |
+
if not self._authed(q):
|
| 605 |
+
return self._send(403, {"error": T("この画面は onw のウィンドウかトレイから開いてください")})
|
| 606 |
+
return self._send(200, self.manager.state())
|
| 607 |
+
self._send(404, {"error": "not found"})
|
| 608 |
+
|
| 609 |
+
def do_POST(self):
|
| 610 |
+
u = urlparse(self.path)
|
| 611 |
+
if not self._authed(parse_qs(u.query)):
|
| 612 |
+
return self._send(403, {"error": T("この画面は onw のウィンドウかトレイから開いてください")})
|
| 613 |
+
m = self.manager
|
| 614 |
+
act = u.path[len("/api/"):]
|
| 615 |
+
try:
|
| 616 |
+
n = int(self.headers.get("Content-Length") or 0)
|
| 617 |
+
body = json.loads(self.rfile.read(n) or b"{}")
|
| 618 |
+
if act == "load":
|
| 619 |
+
r = m.load(body["key"])
|
| 620 |
+
elif act == "unload":
|
| 621 |
+
r = m.unload()
|
| 622 |
+
elif act == "restart":
|
| 623 |
+
r = m.restart()
|
| 624 |
+
elif act == "download":
|
| 625 |
+
r = m.download(body["repo"])
|
| 626 |
+
elif act == "cancel_download":
|
| 627 |
+
r = m.cancel_download()
|
| 628 |
+
elif act == "delete":
|
| 629 |
+
r = m.delete(body["key"])
|
| 630 |
+
elif act == "clear_cache":
|
| 631 |
+
r = m.clear_cache(body["key"])
|
| 632 |
+
elif act == "folders":
|
| 633 |
+
r = m.folders(body.get("save_dir"), body.get("add"), body.get("remove"))
|
| 634 |
+
elif act == "settings":
|
| 635 |
+
r = m.settings(body)
|
| 636 |
+
elif act == "model_prefs":
|
| 637 |
+
r = m.model_prefs(body.get("key"), body.get("prefs") or {})
|
| 638 |
+
elif act == "update_check":
|
| 639 |
+
r = m.start_check()
|
| 640 |
+
elif act == "update":
|
| 641 |
+
r = m.run_update()
|
| 642 |
+
elif act == "open":
|
| 643 |
+
r = m.open(body.get("what"), body.get("key"))
|
| 644 |
+
else:
|
| 645 |
+
return self._send(404, {"error": "not found"})
|
| 646 |
+
except Exception as e:
|
| 647 |
+
r = {"error": T("エラー: {0}", e)}
|
| 648 |
+
self._send(200, r)
|
| 649 |
+
|
| 650 |
+
|
| 651 |
+
def _created():
|
| 652 |
+
try:
|
| 653 |
+
import psutil
|
| 654 |
+
return psutil.Process().create_time()
|
| 655 |
+
except Exception:
|
| 656 |
+
return None
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def forget(info):
|
| 660 |
+
"""Remove manager.json if it still describes this process's manager (at exit)."""
|
| 661 |
+
try:
|
| 662 |
+
if json.load(open(INFO())).get("pid") == info.get("pid"):
|
| 663 |
+
os.remove(INFO())
|
| 664 |
+
except (OSError, ValueError):
|
| 665 |
+
pass
|
| 666 |
+
|
| 667 |
+
|
| 668 |
+
def serve(manager=None):
|
| 669 |
+
"""Start the manager's HTTP server in a daemon thread; returns {port, token}. Writes <config>/manager.json."""
|
| 670 |
+
manager = manager or Manager()
|
| 671 |
+
handler = type("H", (_Handler,), {"manager": manager, "token": secrets.token_hex(16)})
|
| 672 |
+
srv = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
| 673 |
+
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
| 674 |
+
info = {"port": srv.server_address[1], "token": handler.token, "pid": os.getpid(), "created": _created()}
|
| 675 |
+
with open(INFO() + ".tmp", "w") as f:
|
| 676 |
+
json.dump(info, f)
|
| 677 |
+
try:
|
| 678 |
+
os.chmod(INFO() + ".tmp", 0o600) # (the token) before it is in place
|
| 679 |
+
except OSError:
|
| 680 |
+
pass
|
| 681 |
+
os.replace(INFO() + ".tmp", INFO())
|
| 682 |
+
return info
|
| 683 |
+
|
| 684 |
+
|
| 685 |
+
def page_url(info, tab="server"):
|
| 686 |
+
return f"http://127.0.0.1:{info['port']}/manage.html?token={info['token']}#{tab}"
|
onw/models/gemma4.py
CHANGED
|
@@ -1,135 +1,136 @@
|
|
| 1 |
-
"""Gemma 4 (google/gemma-4-E4B-it) from the proven NPU graphs of the ryugyosoft/gemma-4-E4B-it-npu repo.
|
| 2 |
-
|
| 3 |
-
Gemma 4 needs several NPU-specific graph rewrites (head_dim-512 attention split into 256-wide heads, host-built
|
| 4 |
-
sliding / full masks, static vision tower); those graphs already exist and are validated, so this builder imports
|
| 5 |
-
them instead of rebuilding from safetensors:
|
| 6 |
-
* decoder_S{1,16,64}_T1024.xml -> segment 0 of each block size (I/O renamed to the engine's conventions);
|
| 7 |
-
* a new segment 1 per block size: the LM head as a shared INT8 input (one copy for all sizes) + softcapping;
|
| 8 |
-
* the token embedding and the 2.7 GB per-layer embedding tables are pulled out of their OpenVINO IRs into
|
| 9 |
-
shared.bin and looked up on the host (they are pure table reads) - no NPU memory, no worker process;
|
| 10 |
-
* vision_split/vision_npu_280.xml is used as the vision tower.
|
| 11 |
-
usage: onw convert <folder of ryugyosoft/gemma-4-E4B-it-npu> OUT_DIR
|
| 12 |
-
"""
|
| 13 |
-
import json, os, shutil
|
| 14 |
-
import numpy as np
|
| 15 |
-
import openvino as ov
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
from ..
|
| 20 |
-
from . import
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
lg =
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
ov.save_model(
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
shutil.copy(vx
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
"
|
| 110 |
-
"
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
sw.add("
|
| 124 |
-
sw.
|
| 125 |
-
sw.
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
sw.add("
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
sw.
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
| 1 |
+
"""Gemma 4 (google/gemma-4-E4B-it) from the proven NPU graphs of the ryugyosoft/gemma-4-E4B-it-npu repo.
|
| 2 |
+
|
| 3 |
+
Gemma 4 needs several NPU-specific graph rewrites (head_dim-512 attention split into 256-wide heads, host-built
|
| 4 |
+
sliding / full masks, static vision tower); those graphs already exist and are validated, so this builder imports
|
| 5 |
+
them instead of rebuilding from safetensors:
|
| 6 |
+
* decoder_S{1,16,64}_T1024.xml -> segment 0 of each block size (I/O renamed to the engine's conventions);
|
| 7 |
+
* a new segment 1 per block size: the LM head as a shared INT8 input (one copy for all sizes) + softcapping;
|
| 8 |
+
* the token embedding and the 2.7 GB per-layer embedding tables are pulled out of their OpenVINO IRs into
|
| 9 |
+
shared.bin and looked up on the host (they are pure table reads) - no NPU memory, no worker process;
|
| 10 |
+
* vision_split/vision_npu_280.xml is used as the vision tower.
|
| 11 |
+
usage: onw convert <folder of ryugyosoft/gemma-4-E4B-it-npu> OUT_DIR
|
| 12 |
+
"""
|
| 13 |
+
import json, os, shutil
|
| 14 |
+
import numpy as np
|
| 15 |
+
import openvino as ov
|
| 16 |
+
from .. import npukit
|
| 17 |
+
import openvino.opset13 as ops
|
| 18 |
+
|
| 19 |
+
from .. import ir
|
| 20 |
+
from ..ir import i64, f32c
|
| 21 |
+
from . import common
|
| 22 |
+
|
| 23 |
+
EMB = "models/gemma-4-E4B-it-int4-ov"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _table(core, xml):
|
| 27 |
+
"""(int8 table, f16 per-row scale, output multiplier, gather-index subgraph output) from an embedding IR."""
|
| 28 |
+
m = core.read_model(xml)
|
| 29 |
+
tab = sc = mult = gather = None
|
| 30 |
+
for op in m.get_ordered_ops():
|
| 31 |
+
t = op.get_type_name()
|
| 32 |
+
if t == "Constant" and op.get_output_element_type(0) == ov.Type.i8:
|
| 33 |
+
tab = op.get_data().copy()
|
| 34 |
+
elif t == "Constant" and op.get_output_element_type(0) == ov.Type.f16 and len(op.get_output_shape(0)) == 2:
|
| 35 |
+
sc = op.get_data().reshape(-1).copy()
|
| 36 |
+
elif t == "Constant" and op.get_output_element_type(0) == ov.Type.f32 and list(op.get_output_shape(0)) == [1, 1, 1]:
|
| 37 |
+
mult = float(op.get_data().reshape(-1)[0])
|
| 38 |
+
elif t == "Gather":
|
| 39 |
+
gather = op
|
| 40 |
+
return m, tab, sc, mult, gather
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _id_map(core, m, gather, V):
|
| 44 |
+
"""The per-layer model remaps some token ids before its gather; evaluate that index for every id once."""
|
| 45 |
+
idx = gather.input(1).get_source_output()
|
| 46 |
+
sub = ov.Model([ops.result(idx)], m.get_parameters(), "idmap")
|
| 47 |
+
r = core.compile_model(sub, "CPU").create_infer_request()
|
| 48 |
+
return r.infer([np.arange(V, dtype=np.int64)[None]])[0].reshape(-1).astype(np.int32)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _rename(m):
|
| 52 |
+
for p in m.get_parameters():
|
| 53 |
+
n = p.get_output_tensor(0).get_any_name()
|
| 54 |
+
if n.startswith("past_key_values."):
|
| 55 |
+
_, l, kv = n.split(".")
|
| 56 |
+
p.get_output_tensor(0).set_names({f"past_{kv[0]}.{l}"})
|
| 57 |
+
for r in m.get_results(): # names live on the result's tensor and on its source
|
| 58 |
+
tensors = [r.output(0).get_tensor(), r.input(0).get_source_output().get_tensor()]
|
| 59 |
+
n = tensors[0].get_any_name()
|
| 60 |
+
new = None
|
| 61 |
+
if n.startswith("present."):
|
| 62 |
+
_, l, kv = n.split(".")
|
| 63 |
+
new = f"new_{kv[0]}.{l}"
|
| 64 |
+
elif n == "hidden":
|
| 65 |
+
new = "next.h"
|
| 66 |
+
if new:
|
| 67 |
+
for t in tensors:
|
| 68 |
+
t.set_names({new})
|
| 69 |
+
return m
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _head(S, V, H, cap):
|
| 73 |
+
"""LM head + final logit softcapping cap * tanh(logits / cap). The 1/cap is applied to the hidden state before
|
| 74 |
+
the matmul: with it after the matmul the NPU compiler miscomputes multi-row blocks (NPU 3720, S=16: 99% error)."""
|
| 75 |
+
h = ops.parameter([1, S, H], ov.Type.f32, name="carry.h")
|
| 76 |
+
lg, p = ir.shared_head(ops.multiply(h, f32c(1.0 / cap)), V, H)
|
| 77 |
+
lg = ops.multiply(ops.tanh(lg), f32c(cap))
|
| 78 |
+
return ir.finish({"logits": lg}, [h] + p, f"gemma4_head_S{S}")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def export(src, out, sizes=(1, 16, 64), T=1024, fmt=None, graphs_only=False):
|
| 82 |
+
from ..store import SharedWriter
|
| 83 |
+
os.makedirs(out, exist_ok=True)
|
| 84 |
+
core = npukit.core()
|
| 85 |
+
full = json.load(open(os.path.join(src, EMB, "config.json")))
|
| 86 |
+
tc = full["text_config"]
|
| 87 |
+
V, H = tc["vocab_size"], tc["hidden_size"]
|
| 88 |
+
metas = {}
|
| 89 |
+
for S in sizes:
|
| 90 |
+
xml = os.path.join(src, "models", "lm_static", f"decoder_S{S}_T{T}.xml")
|
| 91 |
+
if not os.path.exists(xml):
|
| 92 |
+
continue
|
| 93 |
+
ov.save_model(_rename(core.read_model(xml)), os.path.join(out, f"seg00_S{S}.xml"))
|
| 94 |
+
ov.save_model(_head(S, V, H, tc.get("final_logit_softcapping") or 30.0), os.path.join(out, f"seg01_S{S}.xml"))
|
| 95 |
+
for i in (0, 1):
|
| 96 |
+
metas[f"seg{i:02d}_S{S}.xml"] = {"index": i, "consume_moe": None, "router_layer": None, "S": S, "slots": 0,
|
| 97 |
+
"head": i == 1}
|
| 98 |
+
print(f"S={S}: segments saved", flush=True)
|
| 99 |
+
if not metas:
|
| 100 |
+
import glob
|
| 101 |
+
have = sorted(os.path.basename(p) for p in glob.glob(os.path.join(src, "models", "lm_static", "decoder_S*_T*.xml")))
|
| 102 |
+
raise SystemExit(f"no decoder graphs for T={T} and sizes {list(sizes)} in {src} (there are: {', '.join(have)}); "
|
| 103 |
+
"Gemma 4 is imported from those pre-built graphs")
|
| 104 |
+
vx = os.path.join(src, "models", "vision_split", "vision_npu_280.xml")
|
| 105 |
+
shutil.copy(vx, os.path.join(out, "vision.xml"))
|
| 106 |
+
shutil.copy(vx[:-4] + ".bin", os.path.join(out, "vision.bin"))
|
| 107 |
+
pc = json.load(open(os.path.join(src, EMB, "processor_config.json")))
|
| 108 |
+
meta = {"arch": "gemma4", "T": T, "sizes": [S for S in sizes if f"seg00_S{S}.xml" in metas], "config": tc,
|
| 109 |
+
"segments": metas, "masks": {"sliding_window": tc["sliding_window"]},
|
| 110 |
+
"host_inputs": {"per_layer_inputs": {"table": "ple", "shape": [tc["num_hidden_layers"], tc["hidden_size_per_layer_input"]]}},
|
| 111 |
+
"vision": {"type": "gemma", "soft_tokens": 280, "image_processor": pc["image_processor"],
|
| 112 |
+
"image_token_id": full["image_token_id"], "pad_token_id": tc.get("pad_token_id", 0)},
|
| 113 |
+
"npu_vision_fallback": {"NPU_USE_NPUW": "YES", "NPUW_DEVICES": "NPU", "NPUW_FOLD": "YES"}}
|
| 114 |
+
for f in ("tokenizer.json", "tokenizer_config.json", "chat_template.jinja", "config.json", "generation_config.json",
|
| 115 |
+
"processor_config.json", "preprocessor_config.json"):
|
| 116 |
+
if os.path.exists(os.path.join(src, EMB, f)):
|
| 117 |
+
shutil.copy(os.path.join(src, EMB, f), os.path.join(out, f))
|
| 118 |
+
if graphs_only:
|
| 119 |
+
common.write_engine(out, meta, graphs_only)
|
| 120 |
+
return
|
| 121 |
+
sw = SharedWriter(os.path.join(out, "shared.bin"))
|
| 122 |
+
m, tab, sc, mult, _ = _table(core, os.path.join(src, EMB, "openvino_text_embeddings_model.xml"))
|
| 123 |
+
sw.add("embed_q", tab, "i8", tab.shape)
|
| 124 |
+
sw.add("embed_s", (sc.astype(np.float32) * mult).astype(np.float16), "f16", sc.shape)
|
| 125 |
+
sw.alias("head_q", "embed_q") # tied: same int8 rows,
|
| 126 |
+
sw.add("head_s", sc.astype(np.float16), "f16", sc.shape) # without the embedding scale
|
| 127 |
+
del tab
|
| 128 |
+
ple_xml = os.path.join(src, EMB, "openvino_text_embeddings_per_layer_model.xml")
|
| 129 |
+
m, tab, sc, mult, gather = _table(core, ple_xml)
|
| 130 |
+
sw.add("ple_q", tab, "i8", tab.shape)
|
| 131 |
+
sw.add("ple_s", (sc.astype(np.float32) * mult).astype(np.float16), "f16", sc.shape)
|
| 132 |
+
idmap = _id_map(core, m, gather, tab.shape[0])
|
| 133 |
+
sw.add("ple_map", idmap, "i32", idmap.shape)
|
| 134 |
+
sw.close()
|
| 135 |
+
print("shared tables saved", flush=True)
|
| 136 |
+
common.write_engine(out, meta, graphs_only)
|
onw/npukit.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""[3.0] onw's own NPU kit: an NPU compiler and an NPU plugin built for onw from Intel's open sources, for exactly the
|
| 2 |
+
OpenVINO release onw installs (Windows and Linux). The kit is used through a plugins file given to ov.Core - the "NPU" device is
|
| 3 |
+
then served by the kit's plugin, which loads the kit's compiler next to it; nothing of the installed OpenVINO is
|
| 4 |
+
changed, and with any other OpenVINO build onw uses OpenVINO's own NPU plugin as before.
|
| 5 |
+
|
| 6 |
+
What the kit adds (see TECHNICAL.md):
|
| 7 |
+
- weightless blobs (CACHE_MODE=OPTIMIZE_SIZE + ENABLE_WEIGHTLESS): the compiled graphs keep no weights, the cache holds
|
| 8 |
+
the graphs only (NF4 too - OpenVINO 2026.5's compiler cannot); the weights are rebuilt on the NPU at load from the
|
| 9 |
+
model's own files;
|
| 10 |
+
- one copy of identical weights for every graph of a model (paged parts, fused graphs of every length);
|
| 11 |
+
- faster compiles (the strategy search no longer serialises on the Windows heap, constant folding memoised) and
|
| 12 |
+
compiler fixes: 2-bit weights on 2+ rows (Bonsai), 4-token MTP verification blocks (Qwen3.5 NF4).
|
| 13 |
+
ONW_NPUKIT=0 turns it off."""
|
| 14 |
+
import json
|
| 15 |
+
import os
|
| 16 |
+
import tarfile
|
| 17 |
+
|
| 18 |
+
import openvino as ov
|
| 19 |
+
|
| 20 |
+
KIT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "npukit")
|
| 21 |
+
# Linux: the kit comes packed (npukit/linux/npukit-linux.tar.xz, ~1/4 of its size - installs that update from an older
|
| 22 |
+
# onw download every file once) and is unpacked next to it at the first start
|
| 23 |
+
LINUX = os.path.join(KIT, "linux")
|
| 24 |
+
_STATE = {"ok": None, "why": ""}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _info():
|
| 28 |
+
try:
|
| 29 |
+
with open(os.path.join(KIT, "kit.json"), encoding="utf8") as f:
|
| 30 |
+
return json.load(f)
|
| 31 |
+
except (OSError, ValueError):
|
| 32 |
+
return None
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def active():
|
| 36 |
+
"""True when the kit serves the NPU in this process (checked once)."""
|
| 37 |
+
if _STATE["ok"] is None:
|
| 38 |
+
for n in os.listdir(KIT) if os.path.isdir(KIT) else []:
|
| 39 |
+
if ".dll.old-" in n: # (set aside by an update while a process had them loaded)
|
| 40 |
+
try:
|
| 41 |
+
os.remove(os.path.join(KIT, n))
|
| 42 |
+
except OSError:
|
| 43 |
+
pass
|
| 44 |
+
info = _info()
|
| 45 |
+
if os.environ.get("ONW_NPUKIT", "1") == "0":
|
| 46 |
+
_STATE.update(ok=False, why="off (ONW_NPUKIT=0)")
|
| 47 |
+
elif info is None:
|
| 48 |
+
_STATE.update(ok=False, why="not installed")
|
| 49 |
+
elif ov.__version__ != info.get("openvino"):
|
| 50 |
+
_STATE.update(ok=False, why=f"built for OpenVINO {info.get('openvino')}, not {ov.__version__}")
|
| 51 |
+
elif os.environ.get("ONW_NPUKIT") != "1" and _arch() < 4000:
|
| 52 |
+
# NPU 3720 (Meteor / Arrow Lake): nothing to gain - no graphs without weights there, and OpenVINO's own
|
| 53 |
+
# compiler builds ~7% faster graphs for it (Granite 4.2 3B INT4: 6.4 vs 6.0 tok/s, same answers)
|
| 54 |
+
_STATE.update(ok=False, why="not used on this NPU (ONW_NPUKIT=1 forces it)")
|
| 55 |
+
elif not _unpacked(info):
|
| 56 |
+
_STATE.update(ok=False, why="not installed")
|
| 57 |
+
else:
|
| 58 |
+
if os.name == "nt":
|
| 59 |
+
# the kit's plugin needs OpenVINO's own libraries (openvino.dll, TBB) from the installed package
|
| 60 |
+
os.add_dll_directory(os.path.join(os.path.dirname(ov.__file__), "libs"))
|
| 61 |
+
_STATE.update(ok=True, why=info.get("version", ""))
|
| 62 |
+
return _STATE["ok"]
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _dir():
|
| 66 |
+
return KIT if os.name == "nt" else LINUX
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _unpacked(info):
|
| 70 |
+
"""All of the kit's files are there (Linux: unpacked from its archive when needed)."""
|
| 71 |
+
files = info.get("files" if os.name == "nt" else "files_linux", [])
|
| 72 |
+
if not files:
|
| 73 |
+
return False
|
| 74 |
+
arc = os.path.join(LINUX, info.get("archive_linux", "-"))
|
| 75 |
+
stamp = os.path.join(LINUX, "unpacked.txt")
|
| 76 |
+
if os.name != "nt" and os.path.isfile(arc):
|
| 77 |
+
mark = f"{os.path.getsize(arc)} {info.get('version', '')}"
|
| 78 |
+
try:
|
| 79 |
+
with open(stamp, encoding="utf8") as f:
|
| 80 |
+
done = f.read().strip() == mark
|
| 81 |
+
except OSError:
|
| 82 |
+
done = False
|
| 83 |
+
if done and all(os.path.exists(os.path.join(LINUX, f)) for f in files):
|
| 84 |
+
return True
|
| 85 |
+
try:
|
| 86 |
+
with tarfile.open(arc) as t:
|
| 87 |
+
for m in t.getmembers():
|
| 88 |
+
if m.isfile() and os.path.basename(m.name) == m.name and m.name in files:
|
| 89 |
+
tmp = os.path.join(LINUX, m.name + ".part")
|
| 90 |
+
with t.extractfile(m) as src, open(tmp, "wb") as dst:
|
| 91 |
+
while True:
|
| 92 |
+
b = src.read(1 << 22)
|
| 93 |
+
if not b:
|
| 94 |
+
break
|
| 95 |
+
dst.write(b)
|
| 96 |
+
os.replace(tmp, os.path.join(LINUX, m.name))
|
| 97 |
+
with open(stamp, "w", encoding="utf8") as f:
|
| 98 |
+
f.write(mark + "\n")
|
| 99 |
+
except (OSError, tarfile.TarError):
|
| 100 |
+
return False
|
| 101 |
+
return all(os.path.exists(os.path.join(_dir(), f)) for f in files)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _arch():
|
| 105 |
+
"""NPU generation from OpenVINO's own plugin (3720, 4000, 5010, ...), 0 when there is no NPU."""
|
| 106 |
+
try:
|
| 107 |
+
a = str(ov.Core().get_property("NPU", "DEVICE_ARCHITECTURE"))
|
| 108 |
+
return int("".join(c for c in a if c.isdigit()) or 0)
|
| 109 |
+
except Exception:
|
| 110 |
+
return 0
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def why():
|
| 114 |
+
active()
|
| 115 |
+
return _STATE["why"]
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def core():
|
| 119 |
+
"""An ov.Core whose NPU is the kit's when the kit is usable (else OpenVINO's own)."""
|
| 120 |
+
if active():
|
| 121 |
+
return ov.Core(os.path.join(_dir(), "plugins.xml"))
|
| 122 |
+
return ov.Core()
|
onw/npukit/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
onw/npukit/NOTICE.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
onw NPU kit (onw 3.0)
|
| 2 |
+
|
| 3 |
+
This folder holds binaries built by the onw project from open-source code of Intel Corporation, licensed under the
|
| 4 |
+
Apache License 2.0 (LICENSE). They are modified versions; the modifications are in patches/.
|
| 5 |
+
|
| 6 |
+
- openvino_intel_npu_compiler.dll: Intel NPU compiler (https://github.com/openvinotoolkit/npu_compiler) at commit
|
| 7 |
+
20e7912fbc230725e1ca7394762348d4f480aa65 (submodule thirdparty/vpucostmodel at 4b5da8484d7a5c8cb097486c1248c4d2a753ae4b),
|
| 8 |
+
built as the compiler-in-driver library (BUILD_COMPILER_FOR_DRIVER=ON, Release) against OpenVINO
|
| 9 |
+
d8047fb380b27a9d5827cb3f22aec7781ae5ac82. Changes: patches/npu_compiler-20e7912.patch, patches/vpucostmodel.patch.
|
| 10 |
+
- openvino_intel_npu_plugin.dll: the NPU plugin of OpenVINO (https://github.com/openvinotoolkit/openvino) at commit
|
| 11 |
+
6db412d0a2766d88936477bb97bd37acf2d9d767 - the commit of the OpenVINO 2026.5.0 release onw installs.
|
| 12 |
+
Changes: patches/openvino-6db412d-npu_plugin.patch.
|
| 13 |
+
- openvino_intel_npu_compiler_loader.dll, openvino_intel_npu_vm_runtime.dll: unchanged copies from the OpenVINO 2026.5.0
|
| 14 |
+
wheel (https://pypi.org/project/openvino/), needed next to the plugin.
|
| 15 |
+
|
| 16 |
+
Copyright (C) 2018-2026 Intel Corporation (the original code). The binaries are provided as is, without warranty.
|
onw/npukit/kit.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "3.0.0 (npu_compiler 8.3 + onw fixes, NPU plugin 2026.5.0 6db412d + weight sharing)",
|
| 3 |
+
"openvino": "2026.5.0-23160-6db412d0a27",
|
| 4 |
+
"files": [
|
| 5 |
+
"openvino_intel_npu_plugin.dll",
|
| 6 |
+
"openvino_intel_npu_compiler.dll",
|
| 7 |
+
"openvino_intel_npu_compiler_loader.dll",
|
| 8 |
+
"openvino_intel_npu_vm_runtime.dll",
|
| 9 |
+
"plugins.xml"
|
| 10 |
+
],
|
| 11 |
+
"files_linux": [
|
| 12 |
+
"libopenvino_intel_npu_plugin.so",
|
| 13 |
+
"libopenvino_intel_npu_compiler.so",
|
| 14 |
+
"libopenvino_intel_npu_compiler_loader.so",
|
| 15 |
+
"libopenvino_intel_npu_vm_runtime.so",
|
| 16 |
+
"plugins.xml"
|
| 17 |
+
],
|
| 18 |
+
"archive_linux": "npukit-linux.tar.xz"
|
| 19 |
+
}
|
onw/npukit/linux/npukit-linux.tar.xz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:46fa6b160305b720e56d0d4a44d76a344b06cf7ca886b2a26a447bfb338ba857
|
| 3 |
+
size 32282368
|
onw/npukit/openvino_intel_npu_compiler.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ce1113abf245d6ada1fff920eb1373dc136e1190dc0558bc97d0b6c9ae27ed22
|
| 3 |
+
size 83966464
|
onw/npukit/openvino_intel_npu_compiler_loader.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:15494682f53e465e38c7d655a108f04681fb2af7a1dce1738d2e9212aafe5a14
|
| 3 |
+
size 529680
|
onw/npukit/openvino_intel_npu_plugin.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:91cf9ebe2bd9ef7a3e3e451b63e7497abe465bfa2396edb52b35e8eb1d4db2ce
|
| 3 |
+
size 7992832
|
onw/npukit/openvino_intel_npu_vm_runtime.dll
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:39ece78739633565efd60858f63f2a307397b6ef168019c8abe4a24f7b345c88
|
| 3 |
+
size 559376
|
onw/npukit/patches/npu_compiler-20e7912.patch
ADDED
|
@@ -0,0 +1,1455 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
diff --git a/src/vpux_compiler/include/vpux/compiler/core/dma_cost_model_utils.hpp b/src/vpux_compiler/include/vpux/compiler/core/dma_cost_model_utils.hpp
|
| 2 |
+
index 3bbd048..c7d8799 100644
|
| 3 |
+
--- a/src/vpux_compiler/include/vpux/compiler/core/dma_cost_model_utils.hpp
|
| 4 |
+
+++ b/src/vpux_compiler/include/vpux/compiler/core/dma_cost_model_utils.hpp
|
| 5 |
+
@@ -34,10 +34,10 @@ double getStrideDMACorrectionThresholdByArch(config::ArchKind arch);
|
| 6 |
+
bool applyStrideDMACorrectionForTile(vpux::NDTypeInterface tileType, bool isStridedDMA, uint32_t& cost,
|
| 7 |
+
config::ArchKind arch, bool isFullSearchVersion = false);
|
| 8 |
+
bool correctStrideDMACostOnAllTiles(
|
| 9 |
+
- ArrayRef<std::vector<std::pair<vpux::NDTypeInterface, llvm::DenseMap<mlir::Type, VPU::DistributionInfo>>>>
|
| 10 |
+
+ ArrayRef<std::vector<std::pair<vpux::NDTypeInterface, llvm::SmallDenseMap<mlir::Type, VPU::DistributionInfo, 4>>>>
|
| 11 |
+
tilesTypes,
|
| 12 |
+
const std::function<vpux::NDTypeInterface(
|
| 13 |
+
- ArrayRef<std::pair<vpux::NDTypeInterface, llvm::DenseMap<mlir::Type, VPU::DistributionInfo>>>)>&
|
| 14 |
+
+ ArrayRef<std::pair<vpux::NDTypeInterface, llvm::SmallDenseMap<mlir::Type, VPU::DistributionInfo, 4>>>)>&
|
| 15 |
+
tileTypeGetter,
|
| 16 |
+
SmallVector<uint32_t>& dmaCost, bool isStridedDMA, config::ArchKind arch);
|
| 17 |
+
|
| 18 |
+
diff --git a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/distributed_tensor_utils.hpp b/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/distributed_tensor_utils.hpp
|
| 19 |
+
index 3943fd7..c15b6aa 100644
|
| 20 |
+
--- a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/distributed_tensor_utils.hpp
|
| 21 |
+
+++ b/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/distributed_tensor_utils.hpp
|
| 22 |
+
@@ -46,7 +46,30 @@ const SmallVector<int64_t> DISTRIBUTED_N_ALIGNMENT = SmallVector<int64_t>{16, 1,
|
| 23 |
+
const SmallVector<int64_t> DISTRIBUTED_DW_ACT_C_ALIGNMENT = SmallVector<int64_t>{1, 32, 1, 1};
|
| 24 |
+
const SmallVector<int64_t> DISTRIBUTED_DW_WT_C_ALIGNMENT = SmallVector<int64_t>{32, 1, 1, 1};
|
| 25 |
+
|
| 26 |
+
-using TensorDistributionMap = llvm::DenseMap<mlir::Type, VPU::DistributionInfo>;
|
| 27 |
+
+// onw: a plain DenseMap allocates 64 buckets of DistributionInfo (~45 KB) even for one entry - above the Windows
|
| 28 |
+
+// low-fragmentation-heap limit, so every map went through the locked heap path and the parallel compile threads
|
| 29 |
+
+// serialised on it (strategy assignment ~3x slower than single-threaded). Up to 3 entries now live inline.
|
| 30 |
+
+using TensorDistributionMap = llvm::SmallDenseMap<mlir::Type, VPU::DistributionInfo, 4>;
|
| 31 |
+
+
|
| 32 |
+
+} // namespace VPU
|
| 33 |
+
+} // namespace vpux
|
| 34 |
+
+
|
| 35 |
+
+// onw: the inline buckets make these element types too big for SmallVector's default inline count; keep one inline.
|
| 36 |
+
+template <>
|
| 37 |
+
+struct llvm::CalculateSmallVectorDefaultInlinedElements<vpux::VPU::TensorDistributionMap> {
|
| 38 |
+
+ static constexpr size_t value = 1;
|
| 39 |
+
+};
|
| 40 |
+
+template <>
|
| 41 |
+
+struct llvm::CalculateSmallVectorDefaultInlinedElements<std::pair<vpux::NDTypeInterface, vpux::VPU::TensorDistributionMap>> {
|
| 42 |
+
+ static constexpr size_t value = 1;
|
| 43 |
+
+};
|
| 44 |
+
+template <>
|
| 45 |
+
+struct llvm::CalculateSmallVectorDefaultInlinedElements<std::pair<mlir::Type, vpux::VPU::TensorDistributionMap>> {
|
| 46 |
+
+ static constexpr size_t value = 1;
|
| 47 |
+
+};
|
| 48 |
+
+
|
| 49 |
+
+namespace vpux {
|
| 50 |
+
+namespace VPU {
|
| 51 |
+
|
| 52 |
+
VPU::DistributionInfoAttr updateSliceLikeOpsAlignment(mlir::MLIRContext* ctx, vpux::ShapeRef inShape,
|
| 53 |
+
vpux::ShapeRef sliceShape,
|
| 54 |
+
diff --git a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/multi_cluster_strategy_utils.hpp b/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/multi_cluster_strategy_utils.hpp
|
| 55 |
+
index 8895bf9..1e0b4d3 100644
|
| 56 |
+
--- a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/multi_cluster_strategy_utils.hpp
|
| 57 |
+
+++ b/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/multi_cluster_strategy_utils.hpp
|
| 58 |
+
@@ -148,6 +148,9 @@ private:
|
| 59 |
+
std::pair<std::pair<mlir::Type, TensorDistributionMap>, std::pair<mlir::Type, TensorDistributionMap>>
|
| 60 |
+
getDistributionsWithStrategy(VPU::ClusteredOpInterface parentOp, VPU::MultiClusterStrategy parentStrategy,
|
| 61 |
+
VPU::ClusteredOpInterface userOp, VPU::MultiClusterStrategy userStrategy) const;
|
| 62 |
+
+ std::pair<std::pair<mlir::Type, TensorDistributionMap>, std::pair<mlir::Type, TensorDistributionMap>>
|
| 63 |
+
+ getDistributionsWithStrategyImpl(VPU::ClusteredOpInterface parentOp, VPU::MultiClusterStrategy parentStrategy,
|
| 64 |
+
+ VPU::ClusteredOpInterface userOp, VPU::MultiClusterStrategy userStrategy) const;
|
| 65 |
+
|
| 66 |
+
/* DMA cost calculation helpers */
|
| 67 |
+
double getDMACostOfType(vpux::NDTypeInterface srcTensorType) const;
|
| 68 |
+
@@ -184,6 +187,8 @@ private:
|
| 69 |
+
SiblingOpsAnalysis& _siblingsOpsAnalysis;
|
| 70 |
+
bool _underSubgraphOpt = false;
|
| 71 |
+
bool _enableTilingFullSearchSpace = false;
|
| 72 |
+
+ // onw: memo of getDistributionsWithStrategy results (type-erased, see multi_cluster_strategy_utils.cpp)
|
| 73 |
+
+ mutable std::shared_ptr<void> _onwDistCache;
|
| 74 |
+
};
|
| 75 |
+
vpux::VPU::StrategyCost correctSwOpCost(VPU::SWOpInterface swOp, ArrayRef<vpux::NDTypeInterface> tiledInputTypes,
|
| 76 |
+
vpux::VPU::StrategyCost cost);
|
| 77 |
+
diff --git a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/vertical_fusion/v2/merge_vf_region_rewriter.hpp b/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/vertical_fusion/v2/merge_vf_region_rewriter.hpp
|
| 78 |
+
index fe48940..c819038 100644
|
| 79 |
+
--- a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/vertical_fusion/v2/merge_vf_region_rewriter.hpp
|
| 80 |
+
+++ b/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/vertical_fusion/v2/merge_vf_region_rewriter.hpp
|
| 81 |
+
@@ -40,6 +40,8 @@ protected:
|
| 82 |
+
std::shared_ptr<IVFScheduling<VFConfig>> detectScenario(VFConfig& vfConfig) const override;
|
| 83 |
+
std::optional<VFCase> findVFTiling(VPU::VerticalFusionOp mergedOp, VPU::VerticalFusionOp prevOp,
|
| 84 |
+
VPU::VerticalFusionOp currentOp, const MergeTilingPolicy& tilingPolicy) const;
|
| 85 |
+
+ std::optional<VFCase> findVFTilingImpl(VPU::VerticalFusionOp mergedOp, VPU::VerticalFusionOp prevOp,
|
| 86 |
+
+ VPU::VerticalFusionOp currentOp, const MergeTilingPolicy& tilingPolicy) const;
|
| 87 |
+
StrategyCost getOriginalVFCost(VPU::VerticalFusionOp vfOp) const;
|
| 88 |
+
|
| 89 |
+
VFCacheAnalysis& _cache;
|
| 90 |
+
diff --git a/src/vpux_compiler/src/compiler.cpp b/src/vpux_compiler/src/compiler.cpp
|
| 91 |
+
index 263a325..acf446c 100644
|
| 92 |
+
--- a/src/vpux_compiler/src/compiler.cpp
|
| 93 |
+
+++ b/src/vpux_compiler/src/compiler.cpp
|
| 94 |
+
@@ -1087,7 +1087,11 @@ void compileIEtoVPU(mlir::OwningOpRef<mlir::ModuleOp>& moduleOp,
|
| 95 |
+
wsExtractionMode.value());
|
| 96 |
+
ieToVPUpm.addPass(VPU::createConstructWsAnalysisPass(log));
|
| 97 |
+
ieToVPUpm.addPass(VPU::createIntroduceInitFunctionPass(wsExtractionMode.value(), initPart, memLimit, log));
|
| 98 |
+
- ieToVPUpm.addPass(VPU::createConcatInitResultsPass(wsExtractionMode.value(), initPart, memLimit, log));
|
| 99 |
+
+ // onw: ONW_WS_NO_CONCAT keeps one init output per weight (no concatenation into one obfuscated buffer), so the
|
| 100 |
+
+ // plugin can share identical weights between graphs of different structure
|
| 101 |
+
+ if (std::getenv("ONW_WS_NO_CONCAT") == nullptr) {
|
| 102 |
+
+ ieToVPUpm.addPass(VPU::createConcatInitResultsPass(wsExtractionMode.value(), initPart, memLimit, log));
|
| 103 |
+
+ }
|
| 104 |
+
ieToVPUpm.addPass(VPU::createDestructWsAnalysisPass(log));
|
| 105 |
+
|
| 106 |
+
const auto grc = getDefaultGreedyRewriteConfig();
|
| 107 |
+
@@ -1121,8 +1125,10 @@ void compileVPUIP(mlir::OwningOpRef<mlir::ModuleOp>& vpuModule,
|
| 108 |
+
vpuipPM.addPass(VPU::createConstructWsAnalysisPass(log));
|
| 109 |
+
vpuipPM.addPass(VPU::createIntroduceInitFunctionPass(wsExtractionMode.value(), /* initPart = */ std::nullopt,
|
| 110 |
+
memLimit, log));
|
| 111 |
+
- vpuipPM.addPass(VPU::createConcatInitResultsPass(wsExtractionMode.value(), /* initPart = */ std::nullopt,
|
| 112 |
+
- memLimit, log));
|
| 113 |
+
+ if (std::getenv("ONW_WS_NO_CONCAT") == nullptr) {
|
| 114 |
+
+ vpuipPM.addPass(VPU::createConcatInitResultsPass(wsExtractionMode.value(), /* initPart = */ std::nullopt,
|
| 115 |
+
+ memLimit, log));
|
| 116 |
+
+ }
|
| 117 |
+
vpuipPM.addPass(VPU::createDestructWsAnalysisPass(log));
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
diff --git a/src/vpux_compiler/src/conversion/passes/IE2VPU/convert_layers_to_VPU.cpp b/src/vpux_compiler/src/conversion/passes/IE2VPU/convert_layers_to_VPU.cpp
|
| 121 |
+
index f80f56e..3cfef97 100644
|
| 122 |
+
--- a/src/vpux_compiler/src/conversion/passes/IE2VPU/convert_layers_to_VPU.cpp
|
| 123 |
+
+++ b/src/vpux_compiler/src/conversion/passes/IE2VPU/convert_layers_to_VPU.cpp
|
| 124 |
+
@@ -10,6 +10,8 @@
|
| 125 |
+
#include <mlir/Support/LLVM.h>
|
| 126 |
+
#include "vpux/compiler/conversion.hpp"
|
| 127 |
+
#include "vpux/compiler/dialect/IE/IR/dialect.hpp"
|
| 128 |
+
+#include "vpux/compiler/dialect/IE/IR/ops/data_movement.hpp"
|
| 129 |
+
+#include "vpux/compiler/dialect/IE/IR/ops/data_type.hpp"
|
| 130 |
+
#include "vpux/compiler/dialect/IE/IR/ops/activation.hpp"
|
| 131 |
+
#include "vpux/compiler/dialect/IE/IR/ops/arithmetic.hpp"
|
| 132 |
+
#include "vpux/compiler/dialect/IE/IR/ops/bitwise.hpp"
|
| 133 |
+
@@ -820,9 +822,72 @@ private:
|
| 134 |
+
void safeRunOnFunc() final;
|
| 135 |
+
};
|
| 136 |
+
|
| 137 |
+
+// onw: the weights-separation @init schedule permutes sub-byte (2/4-bit) weights as data; a permute of 4-bit elements
|
| 138 |
+
+// becomes a PermuteDMA with 4-bit element strides, which the DMA cannot do ("Non byte aligned stride value"). In init
|
| 139 |
+
+// functions only, do such permutes in f16 (exact for these integers) between two Converts.
|
| 140 |
+
+static void onwWidenSubBytePermutes(mlir::func::FuncOp func) {
|
| 141 |
+
+ if (std::getenv("ONW_WS_NO_WIDEN") != nullptr || func.getName().find("init") == llvm::StringRef::npos ||
|
| 142 |
+
+ config::getArch(func) <= config::ArchKind::NPU37XX) {
|
| 143 |
+
+ return;
|
| 144 |
+
+ }
|
| 145 |
+
+ SmallVector<mlir::Operation*> todo;
|
| 146 |
+
+ func.walk([&](mlir::Operation* op) {
|
| 147 |
+
+ if (!mlir::isa<IE::MemPermuteOp, IE::ReorderOp, IE::TransposeOp>(op)) {
|
| 148 |
+
+ return;
|
| 149 |
+
+ }
|
| 150 |
+
+ auto elemType = mlir::cast<NDTypeInterface>(op->getOperand(0).getType()).getElementType();
|
| 151 |
+
+ mlir::Type storage = elemType;
|
| 152 |
+
+ if (auto q = mlir::dyn_cast<mlir::quant::QuantizedType>(elemType)) {
|
| 153 |
+
+ storage = q.getStorageType();
|
| 154 |
+
+ }
|
| 155 |
+
+ auto intType = mlir::dyn_cast<mlir::IntegerType>(storage);
|
| 156 |
+
+ if (intType != nullptr && intType.getWidth() < 8) {
|
| 157 |
+
+ todo.push_back(op);
|
| 158 |
+
+ }
|
| 159 |
+
+ });
|
| 160 |
+
+ for (auto* op : todo) {
|
| 161 |
+
+ mlir::OpBuilder builder(op);
|
| 162 |
+
+ const auto loc = op->getLoc();
|
| 163 |
+
+ const auto inType = mlir::cast<NDTypeInterface>(op->getOperand(0).getType());
|
| 164 |
+
+ const auto outType = mlir::cast<NDTypeInterface>(op->getResult(0).getType());
|
| 165 |
+
+ const auto inElem = inType.getElementType();
|
| 166 |
+
+ const auto outElem = outType.getElementType();
|
| 167 |
+
+ auto toStorage = [](mlir::Type t) -> mlir::Type {
|
| 168 |
+
+ if (auto q = mlir::dyn_cast<mlir::quant::QuantizedType>(t)) {
|
| 169 |
+
+ return q.getStorageType();
|
| 170 |
+
+ }
|
| 171 |
+
+ return t;
|
| 172 |
+
+ };
|
| 173 |
+
+ mlir::Value x = op->getOperand(0);
|
| 174 |
+
+ if (mlir::isa<mlir::quant::QuantizedType>(inElem)) {
|
| 175 |
+
+ x = builder.create<IE::QuantizeCastOp>(appendLoc(loc, "onw_storage_in"), x, toStorage(inElem));
|
| 176 |
+
+ }
|
| 177 |
+
+ auto f16 = mlir::Float16Type::get(op->getContext());
|
| 178 |
+
+ x = builder.create<IE::ConvertOp>(appendLoc(loc, "onw_to_f16"), x, f16);
|
| 179 |
+
+ auto* newOp = builder.clone(*op);
|
| 180 |
+
+ newOp->setOperand(0, x);
|
| 181 |
+
+ newOp->getResult(0).setType(outType.changeElemType(f16));
|
| 182 |
+
+ mlir::Value y = builder.create<IE::ConvertOp>(appendLoc(loc, "onw_from_f16"), newOp->getResult(0),
|
| 183 |
+
+ toStorage(outElem));
|
| 184 |
+
+ if (mlir::isa<mlir::quant::QuantizedType>(outElem)) {
|
| 185 |
+
+ y = builder.create<IE::QuantizeCastOp>(appendLoc(loc, "onw_storage_out"), y, outElem);
|
| 186 |
+
+ }
|
| 187 |
+
+ op->getResult(0).replaceAllUsesWith(y);
|
| 188 |
+
+ op->erase();
|
| 189 |
+
+ }
|
| 190 |
+
+}
|
| 191 |
+
+
|
| 192 |
+
void ConvertLayers2VPUPass::safeRunOnFunc() {
|
| 193 |
+
auto& ctx = getContext();
|
| 194 |
+
auto func = getOperation();
|
| 195 |
+
+ if (const char* onwDump = std::getenv("ONW_DUMP_IE")) {
|
| 196 |
+
+ std::error_code ec;
|
| 197 |
+
+ llvm::raw_fd_ostream fo((std::string(onwDump) + "_" + func.getName().str() + ".mlir"), ec);
|
| 198 |
+
+ if (!ec) {
|
| 199 |
+
+ func->print(fo, mlir::OpPrintingFlags().enableDebugInfo(true, false));
|
| 200 |
+
+ }
|
| 201 |
+
+ }
|
| 202 |
+
+ onwWidenSubBytePermutes(func);
|
| 203 |
+
|
| 204 |
+
mlir::ConversionTarget target(ctx);
|
| 205 |
+
target.addIllegalDialect<IE::IEDialect>();
|
| 206 |
+
diff --git a/src/vpux_compiler/src/dialect/IE/transforms/passes/swap_convert_with_reshape_kind_ops.cpp b/src/vpux_compiler/src/dialect/IE/transforms/passes/swap_convert_with_reshape_kind_ops.cpp
|
| 207 |
+
index d16690c..93f292f 100644
|
| 208 |
+
--- a/src/vpux_compiler/src/dialect/IE/transforms/passes/swap_convert_with_reshape_kind_ops.cpp
|
| 209 |
+
+++ b/src/vpux_compiler/src/dialect/IE/transforms/passes/swap_convert_with_reshape_kind_ops.cpp
|
| 210 |
+
@@ -3,6 +3,7 @@
|
| 211 |
+
// SPDX-License-Identifier: Apache-2.0
|
| 212 |
+
//
|
| 213 |
+
|
| 214 |
+
+#include "vpux/compiler/core/types/quantile_float/types.hpp"
|
| 215 |
+
#include "vpux/compiler/dialect/IE/IR/dialect.hpp"
|
| 216 |
+
#include "vpux/compiler/dialect/IE/IR/ops/data_movement.hpp"
|
| 217 |
+
#include "vpux/compiler/dialect/IE/IR/ops/data_type.hpp"
|
| 218 |
+
@@ -212,6 +213,27 @@ PropagationDecision SwapConvertWithReshapeKindOps::PropagateConvertToFuseConvert
|
| 219 |
+
if (!firstInputSize || !middleTypeSize || !lastOutputSize) {
|
| 220 |
+
return PropagationDecision::NoPropagate;
|
| 221 |
+
}
|
| 222 |
+
+ // onw: in the weights-separation @init schedule, Converts around a QuantizeCast to/from a quantile (NF4) or
|
| 223 |
+
+ // quantized type carry raw table indices; fusing them would create an unsupported "Convert to quantile".
|
| 224 |
+
+ {
|
| 225 |
+
+ auto isQuantLike = [](mlir::Type t) {
|
| 226 |
+
+ return mlir::isa<mlir::quant::QuantizedType, vpux::type::QuantileType>(t);
|
| 227 |
+
+ };
|
| 228 |
+
+ auto func = firstConvert->getParentOfType<mlir::func::FuncOp>();
|
| 229 |
+
+ if (func != nullptr && func.getName().find("init") != llvm::StringRef::npos &&
|
| 230 |
+
+ (isQuantLike(firstInputType) || isQuantLike(firstOutputType) || isQuantLike(lastOutputType))) {
|
| 231 |
+
+ return PropagationDecision::NoPropagate;
|
| 232 |
+
+ }
|
| 233 |
+
+ if (func != nullptr && func.getName().find("init") != llvm::StringRef::npos) {
|
| 234 |
+
+ for (mlir::Operation* op = firstConvert.getOperation(); op != nullptr && op != lastConvert.getOperation();
|
| 235 |
+
+ op = op->hasOneUse() ? *op->getUsers().begin() : nullptr) {
|
| 236 |
+
+ if (mlir::isa<IE::QuantizeCastOp>(op) ||
|
| 237 |
+
+ isQuantLike(mlir::cast<vpux::NDTypeInterface>(op->getResult(0).getType()).getElementType())) {
|
| 238 |
+
+ return PropagationDecision::NoPropagate;
|
| 239 |
+
+ }
|
| 240 |
+
+ }
|
| 241 |
+
+ }
|
| 242 |
+
+ }
|
| 243 |
+
|
| 244 |
+
int64_t minTypeSize = std::min({firstInputSize, middleTypeSize, lastOutputSize});
|
| 245 |
+
if (minTypeSize == lastOutputSize) {
|
| 246 |
+
diff --git a/src/vpux_compiler/src/dialect/IE/transforms/passes/weights_dequantize_to_dynamic_dequantize.cpp b/src/vpux_compiler/src/dialect/IE/transforms/passes/weights_dequantize_to_dynamic_dequantize.cpp
|
| 247 |
+
index ac46b4a..5ec32cc 100644
|
| 248 |
+
--- a/src/vpux_compiler/src/dialect/IE/transforms/passes/weights_dequantize_to_dynamic_dequantize.cpp
|
| 249 |
+
+++ b/src/vpux_compiler/src/dialect/IE/transforms/passes/weights_dequantize_to_dynamic_dequantize.cpp
|
| 250 |
+
@@ -113,7 +113,11 @@ mlir::LogicalResult finishDynamicDequantizeRewrite(mlir::PatternRewriter& rewrit
|
| 251 |
+
rewriter.create<IE::DynamicDequantizeOp>(appendLoc(loc, "ddq"), rawWeights, scaleValue, zpValue, dstType);
|
| 252 |
+
|
| 253 |
+
// Mark the op for the DynDeq -> FakeQuantize bridge pass.
|
| 254 |
+
- if (wdInfo.isKVcachedPattern() || !zpElemType.isInteger(2)) {
|
| 255 |
+
+ // [onw] 2-bit zero points too outside the KV-cached (one-row) pattern: unmarked, a multi-row FullyConnected
|
| 256 |
+
+ // dequantizes 2-bit weights with the DynamicDequantize SHAVE kernel, which returns zeros for them; marked, they go
|
| 257 |
+
+ // FakeQuantize -> group unroll -> palletized LUT on the DPU as the one-row graphs do (2-bit weights kept)
|
| 258 |
+
+ static const bool onwMarkAll = std::getenv("ONW_U2_MARK") == nullptr || std::string(std::getenv("ONW_U2_MARK")) != "0";
|
| 259 |
+
+ if (wdInfo.isKVcachedPattern() || !zpElemType.isInteger(2) || onwMarkAll) {
|
| 260 |
+
dynamicDequant->setAttr(IE::WEIGHTS_IMPORT_DYN_DEQUANT_ATTR, mlir::UnitAttr::get(rewriter.getContext()));
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
diff --git a/src/vpux_compiler/src/dialect/VPU/IR/ops/convert.cpp b/src/vpux_compiler/src/dialect/VPU/IR/ops/convert.cpp
|
| 264 |
+
index 7390492..4dd29e4 100644
|
| 265 |
+
--- a/src/vpux_compiler/src/dialect/VPU/IR/ops/convert.cpp
|
| 266 |
+
+++ b/src/vpux_compiler/src/dialect/VPU/IR/ops/convert.cpp
|
| 267 |
+
@@ -55,11 +55,37 @@ bool vpux::VPU::ConvertOp::areCastCompatible(mlir::TypeRange inputs, mlir::TypeR
|
| 268 |
+
return input && output && input.getShape() == output.getShape();
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
-bool vpux::VPU::ConvertOp::checkStrategyCompatibility(vpux::VPU::MultiClusterStrategy strategy, size_t) {
|
| 272 |
+
+bool vpux::VPU::ConvertOp::checkStrategyCompatibility(vpux::VPU::MultiClusterStrategy strategy, size_t numTiles) {
|
| 273 |
+
bool isStrategyCompatible = false;
|
| 274 |
+
constexpr int64_t MIN_DIM_SIZE_FOR_TILING = 4;
|
| 275 |
+
auto inputShape = getBoundedShape(getInput());
|
| 276 |
+
|
| 277 |
+
+ // onw: a sub-byte output split along its innermost memory dim must give byte-aligned cluster segments, otherwise
|
| 278 |
+
+ // the cluster DMAs start mid-byte ("Can't convert 172 Bit to Byte" in UnrollDistributedOps; seen in the init
|
| 279 |
+
+ // schedule of weightless blobs: f16 -> si4, 128 rows over 3 clusters = 43 x 4 bit).
|
| 280 |
+
+ const auto splitsMidByteFor = [&](mlir::Value v, Dim dim) {
|
| 281 |
+
+ const auto type = mlir::cast<NDTypeInterface>(v.getType());
|
| 282 |
+
+ const auto bits = type.getElemTypeSize().count();
|
| 283 |
+
+ if (bits >= 8 || numTiles == 0 || type.getRank() != 4 || type.getDimsOrder().dimAt(type.getRank() - 1) != dim) {
|
| 284 |
+
+ return false;
|
| 285 |
+
+ }
|
| 286 |
+
+ const auto shape = getBoundedShape(v);
|
| 287 |
+
+ const auto chunk = divUp(shape[dim], static_cast<int64_t>(numTiles));
|
| 288 |
+
+ return (chunk * bits) % 8 != 0;
|
| 289 |
+
+ };
|
| 290 |
+
+ const auto splitsMidByte = [&](Dim dim) {
|
| 291 |
+
+ static const bool onwOff = std::getenv("ONW_NO_SUBBYTE_SPLIT_FIX") != nullptr;
|
| 292 |
+
+ return !onwOff && (splitsMidByteFor(getInput(), dim) || splitsMidByteFor(getOutput(), dim));
|
| 293 |
+
+ };
|
| 294 |
+
+ if ((strategy == VPU::MultiClusterStrategy::SplitOverHeight ||
|
| 295 |
+
+ strategy == VPU::MultiClusterStrategy::SplitOverHeightOverlapped) &&
|
| 296 |
+
+ splitsMidByte(Dims4D::Act::H)) {
|
| 297 |
+
+ return false;
|
| 298 |
+
+ }
|
| 299 |
+
+ if (strategy == VPU::MultiClusterStrategy::SplitOverKernel && splitsMidByte(Dims4D::Act::C)) {
|
| 300 |
+
+ return false;
|
| 301 |
+
+ }
|
| 302 |
+
+
|
| 303 |
+
switch (strategy) {
|
| 304 |
+
case VPU::MultiClusterStrategy::Clustering:
|
| 305 |
+
isStrategyCompatible = true;
|
| 306 |
+
diff --git a/src/vpux_compiler/src/dialect/VPU/transforms/passes/multi_cluster_strategy_assignment.cpp b/src/vpux_compiler/src/dialect/VPU/transforms/passes/multi_cluster_strategy_assignment.cpp
|
| 307 |
+
index adaca74..fab49de 100644
|
| 308 |
+
--- a/src/vpux_compiler/src/dialect/VPU/transforms/passes/multi_cluster_strategy_assignment.cpp
|
| 309 |
+
+++ b/src/vpux_compiler/src/dialect/VPU/transforms/passes/multi_cluster_strategy_assignment.cpp
|
| 310 |
+
@@ -5,6 +5,10 @@
|
| 311 |
+
|
| 312 |
+
#include "vpux/compiler/dialect/VPU/IR/dialect.hpp"
|
| 313 |
+
#include "vpux/compiler/dialect/VPU/transforms/passes.hpp"
|
| 314 |
+
+
|
| 315 |
+
+#include <chrono>
|
| 316 |
+
+#include <cstdio>
|
| 317 |
+
+#include <cstdlib>
|
| 318 |
+
#include "vpux/compiler/dialect/VPU/utils/cost_model/layer_vpunn_cost.hpp"
|
| 319 |
+
#include "vpux/compiler/dialect/VPU/utils/manual_strategy_utils.hpp"
|
| 320 |
+
#include "vpux/compiler/dialect/VPU/utils/multi_cluster_strategy_utils.hpp"
|
| 321 |
+
@@ -121,10 +125,21 @@ void MultiClusterStrategyAssignmentPass::safeRunOnFunc() {
|
| 322 |
+
_log.debug("Greedy Strategy Assignment");
|
| 323 |
+
auto enableMultiClusterForSWLayer =
|
| 324 |
+
config::getAvailableExecutor(module, config::ExecutorKind::SHAVE_ACT) != nullptr;
|
| 325 |
+
+ const auto onwT0 = std::chrono::steady_clock::now();
|
| 326 |
+
strategyManager.assignMultiClusterStrategy(enableMultiClusterForSWLayer);
|
| 327 |
+
+ const auto onwT1 = std::chrono::steady_clock::now();
|
| 328 |
+
|
| 329 |
+
_log.debug("Execute Subgraph Optimization");
|
| 330 |
+
strategyManager.optimizeMulticlusterStrategy();
|
| 331 |
+
+ const auto onwT2 = std::chrono::steady_clock::now();
|
| 332 |
+
+ if (const char* onwP = std::getenv("ONW_PROF_FILE")) {
|
| 333 |
+
+ if (FILE* f = std::fopen(onwP, "a")) {
|
| 334 |
+
+ std::fprintf(f, "mcsa ops %lld assign %.2f s optimize %.2f s%c", (long long)clusteredOpCount,
|
| 335 |
+
+ std::chrono::duration<double>(onwT1 - onwT0).count(),
|
| 336 |
+
+ std::chrono::duration<double>(onwT2 - onwT1).count(), 10);
|
| 337 |
+
+ std::fclose(f);
|
| 338 |
+
+ }
|
| 339 |
+
+ }
|
| 340 |
+
|
| 341 |
+
_log.trace("Remove Temporary Strategy");
|
| 342 |
+
strategyManager.removeTemporaryMulticlusterStrategy();
|
| 343 |
+
diff --git a/src/vpux_compiler/src/dialect/VPU/utils/strategy_manager/multi_cluster_strategy_utils.cpp b/src/vpux_compiler/src/dialect/VPU/utils/strategy_manager/multi_cluster_strategy_utils.cpp
|
| 344 |
+
index 4bfec49..26e3194 100644
|
| 345 |
+
--- a/src/vpux_compiler/src/dialect/VPU/utils/strategy_manager/multi_cluster_strategy_utils.cpp
|
| 346 |
+
+++ b/src/vpux_compiler/src/dialect/VPU/utils/strategy_manager/multi_cluster_strategy_utils.cpp
|
| 347 |
+
@@ -29,6 +29,11 @@
|
| 348 |
+
#include "vpux/compiler/dialect/VPU/utils/op_tiling_cache.hpp"
|
| 349 |
+
#include "vpux/compiler/dialect/VPU/utils/scheduling/temporal_tiling_driver.hpp"
|
| 350 |
+
#include "vpux/compiler/dialect/VPU/utils/singleton_cache.hpp"
|
| 351 |
+
+#include <atomic>
|
| 352 |
+
+#include <cstdio>
|
| 353 |
+
+#include <cstdlib>
|
| 354 |
+
+#include <map>
|
| 355 |
+
+#include <tuple>
|
| 356 |
+
#include "vpux/compiler/dialect/VPU/utils/sparsity_utils.hpp"
|
| 357 |
+
#include "vpux/compiler/dialect/VPU/utils/tile_utils.hpp"
|
| 358 |
+
#include "vpux/compiler/dialect/VPUIP/utils/convert_to_dma_utils.hpp"
|
| 359 |
+
@@ -1081,8 +1086,90 @@ bool LayerCostModel::hasSpilling(VPU::ClusteredOpInterface op, vpux::NDTypeInter
|
| 360 |
+
return hasSpilling(op, srcTensorDistributionMap, dstTensorDistributionMap);
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
+namespace {
|
| 364 |
+
+using OnwDistPair = std::pair<std::pair<mlir::Type, TensorDistributionMap>, std::pair<mlir::Type, TensorDistributionMap>>;
|
| 365 |
+
+struct OnwDistCache {
|
| 366 |
+
+ std::map<std::tuple<void*, int, void*, int>, OnwDistPair> m;
|
| 367 |
+
+};
|
| 368 |
+
+std::atomic<long long> onwDistCalls{0}, onwDistHits{0}, onwDistBad{0};
|
| 369 |
+
+// ONW_DIST_CACHE: unset/0 = off, "check" = compute every time and compare with the memo, anything else = use the memo
|
| 370 |
+
+int onwDistMode() {
|
| 371 |
+
+ static const int mode = [] {
|
| 372 |
+
+ const char* e = std::getenv("ONW_DIST_CACHE");
|
| 373 |
+
+ if (e == nullptr || std::string(e) == "0" || std::string(e).empty()) {
|
| 374 |
+
+ return 0;
|
| 375 |
+
+ }
|
| 376 |
+
+ return std::string(e) == "check" ? 2 : 1;
|
| 377 |
+
+ }();
|
| 378 |
+
+ return mode;
|
| 379 |
+
+}
|
| 380 |
+
+bool onwSameMap(const TensorDistributionMap& a, const TensorDistributionMap& b) {
|
| 381 |
+
+ if (a.size() != b.size()) {
|
| 382 |
+
+ return false;
|
| 383 |
+
+ }
|
| 384 |
+
+ for (const auto& kv : a) {
|
| 385 |
+
+ auto it = b.find(kv.first);
|
| 386 |
+
+ if (it == b.end() || !(it->second == kv.second)) {
|
| 387 |
+
+ return false;
|
| 388 |
+
+ }
|
| 389 |
+
+ }
|
| 390 |
+
+ return true;
|
| 391 |
+
+}
|
| 392 |
+
+bool onwSame(const OnwDistPair& a, const OnwDistPair& b) {
|
| 393 |
+
+ return a.first.first == b.first.first && a.second.first == b.second.first &&
|
| 394 |
+
+ onwSameMap(a.first.second, b.first.second) && onwSameMap(a.second.second, b.second.second);
|
| 395 |
+
+}
|
| 396 |
+
+struct OnwDistReport {
|
| 397 |
+
+ ~OnwDistReport() {
|
| 398 |
+
+ if (onwDistCalls.load() == 0) {
|
| 399 |
+
+ return;
|
| 400 |
+
+ }
|
| 401 |
+
+ if (const char* p = std::getenv("ONW_PROF_FILE")) {
|
| 402 |
+
+ if (FILE* f = std::fopen(p, "a")) {
|
| 403 |
+
+ std::fprintf(f, "distcache calls %lld hits %lld mismatches %lld%c", onwDistCalls.load(),
|
| 404 |
+
+ onwDistHits.load(), onwDistBad.load(), 10);
|
| 405 |
+
+ std::fclose(f);
|
| 406 |
+
+ }
|
| 407 |
+
+ }
|
| 408 |
+
+ }
|
| 409 |
+
+} onwDistReport;
|
| 410 |
+
+} // namespace
|
| 411 |
+
+
|
| 412 |
+
+OnwDistPair LayerCostModel::getDistributionsWithStrategy(VPU::ClusteredOpInterface parentOp,
|
| 413 |
+
+ VPU::MultiClusterStrategy parentStrategy,
|
| 414 |
+
+ VPU::ClusteredOpInterface userOp,
|
| 415 |
+
+ VPU::MultiClusterStrategy userStrategy) const {
|
| 416 |
+
+ const int mode = onwDistMode();
|
| 417 |
+
+ if (mode == 0) {
|
| 418 |
+
+ return getDistributionsWithStrategyImpl(parentOp, parentStrategy, userOp, userStrategy);
|
| 419 |
+
+ }
|
| 420 |
+
+ if (_onwDistCache == nullptr) {
|
| 421 |
+
+ _onwDistCache = std::make_shared<OnwDistCache>();
|
| 422 |
+
+ }
|
| 423 |
+
+ auto& cache = static_cast<OnwDistCache*>(_onwDistCache.get())->m;
|
| 424 |
+
+ ++onwDistCalls;
|
| 425 |
+
+ const auto key = std::make_tuple(static_cast<void*>(parentOp.getOperation()), static_cast<int>(parentStrategy),
|
| 426 |
+
+ static_cast<void*>(userOp.getOperation()), static_cast<int>(userStrategy));
|
| 427 |
+
+ auto it = cache.find(key);
|
| 428 |
+
+ if (it != cache.end()) {
|
| 429 |
+
+ ++onwDistHits;
|
| 430 |
+
+ if (mode == 1) {
|
| 431 |
+
+ return it->second;
|
| 432 |
+
+ }
|
| 433 |
+
+ auto fresh = getDistributionsWithStrategyImpl(parentOp, parentStrategy, userOp, userStrategy);
|
| 434 |
+
+ if (!onwSame(fresh, it->second)) {
|
| 435 |
+
+ ++onwDistBad;
|
| 436 |
+
+ it->second = fresh;
|
| 437 |
+
+ }
|
| 438 |
+
+ return fresh;
|
| 439 |
+
+ }
|
| 440 |
+
+ auto fresh = getDistributionsWithStrategyImpl(parentOp, parentStrategy, userOp, userStrategy);
|
| 441 |
+
+ cache.emplace(key, fresh);
|
| 442 |
+
+ return fresh;
|
| 443 |
+
+}
|
| 444 |
+
+
|
| 445 |
+
std::pair<std::pair<mlir::Type, TensorDistributionMap>, std::pair<mlir::Type, TensorDistributionMap>>
|
| 446 |
+
-LayerCostModel::getDistributionsWithStrategy(VPU::ClusteredOpInterface parentOp,
|
| 447 |
+
+LayerCostModel::getDistributionsWithStrategyImpl(VPU::ClusteredOpInterface parentOp,
|
| 448 |
+
VPU::MultiClusterStrategy parentStrategy, VPU::ClusteredOpInterface userOp,
|
| 449 |
+
VPU::MultiClusterStrategy userStrategy) const {
|
| 450 |
+
// Set the custom strategy to the op to get the accurate distributed type
|
| 451 |
+
@@ -1090,14 +1177,26 @@ LayerCostModel::getDistributionsWithStrategy(VPU::ClusteredOpInterface parentOp,
|
| 452 |
+
// e.g., Conv (SOK) -> SW (SOK), the output of the Conv would be SEGMENTED
|
| 453 |
+
// Conv (SOK) -> SW (Clustering), the output of the Conv would be DUPLICATED|SEGMENTED
|
| 454 |
+
// The DistributedType is decided by the ops multiCluster strategy attributes
|
| 455 |
+
+ // onw: setting an op's strategy to the value it already has is a no-op (same uniqued attribute), so skip the
|
| 456 |
+
+ // temporary set/restore pair then - each one rebuilds the op's attribute dictionary in the shared context.
|
| 457 |
+
auto greedyStrategyParentOp = getMultiClusterStrategyValue(parentOp);
|
| 458 |
+
auto greedyStrategyUserOp = getMultiClusterStrategyValue(userOp);
|
| 459 |
+
- parentOp.setMultiClusterStrategy(parentStrategy);
|
| 460 |
+
- userOp.setMultiClusterStrategy(userStrategy);
|
| 461 |
+
+ const bool setParent = parentStrategy != greedyStrategyParentOp;
|
| 462 |
+
+ const bool setUser = userStrategy != greedyStrategyUserOp;
|
| 463 |
+
+ if (setParent) {
|
| 464 |
+
+ parentOp.setMultiClusterStrategy(parentStrategy);
|
| 465 |
+
+ }
|
| 466 |
+
+ if (setUser) {
|
| 467 |
+
+ userOp.setMultiClusterStrategy(userStrategy);
|
| 468 |
+
+ }
|
| 469 |
+
auto targetOutput = getOutputWithDistribution(parentOp, parentStrategy);
|
| 470 |
+
auto targetInput = getInputWithDistribution(userOp, parentOp, userStrategy);
|
| 471 |
+
- parentOp.setMultiClusterStrategy(greedyStrategyParentOp);
|
| 472 |
+
- userOp.setMultiClusterStrategy(greedyStrategyUserOp);
|
| 473 |
+
+ if (setParent) {
|
| 474 |
+
+ parentOp.setMultiClusterStrategy(greedyStrategyParentOp);
|
| 475 |
+
+ }
|
| 476 |
+
+ if (setUser) {
|
| 477 |
+
+ userOp.setMultiClusterStrategy(greedyStrategyUserOp);
|
| 478 |
+
+ }
|
| 479 |
+
|
| 480 |
+
VPU::DistributionInfo outputDistribution{};
|
| 481 |
+
auto outputType = mlir::isa<VPU::SparseTensorType>(targetOutput.first)
|
| 482 |
+
diff --git a/src/vpux_compiler/src/dialect/VPU/utils/strategy_manager/subgraph_optimizer.cpp b/src/vpux_compiler/src/dialect/VPU/utils/strategy_manager/subgraph_optimizer.cpp
|
| 483 |
+
index 610fe6c..8cac379 100644
|
| 484 |
+
--- a/src/vpux_compiler/src/dialect/VPU/utils/strategy_manager/subgraph_optimizer.cpp
|
| 485 |
+
+++ b/src/vpux_compiler/src/dialect/VPU/utils/strategy_manager/subgraph_optimizer.cpp
|
| 486 |
+
@@ -3,6 +3,10 @@
|
| 487 |
+
// SPDX-License-Identifier: Apache-2.0
|
| 488 |
+
//
|
| 489 |
+
|
| 490 |
+
+#include <atomic>
|
| 491 |
+
+#include <chrono>
|
| 492 |
+
+#include <cstdio>
|
| 493 |
+
+#include <cstdlib>
|
| 494 |
+
#include "vpux/compiler/dialect/VPU/utils/strategy_manager/subgraph_optimizer.hpp"
|
| 495 |
+
#include "vpux/compiler/dialect/VPU/IR/ops/data_movement.hpp"
|
| 496 |
+
#include "vpux/compiler/dialect/VPU/IR/ops/internal.hpp"
|
| 497 |
+
@@ -17,6 +21,19 @@
|
| 498 |
+
#include <llvm/ADT/STLExtras.h>
|
| 499 |
+
#include <llvm/ADT/TypeSwitch.h>
|
| 500 |
+
#include "mlir/IR/Iterators.h"
|
| 501 |
+
+#include <atomic>
|
| 502 |
+
+#include <chrono>
|
| 503 |
+
+#include <optional>
|
| 504 |
+
+
|
| 505 |
+
+namespace {
|
| 506 |
+
+std::atomic<long long> onwSoNs[16];
|
| 507 |
+
+std::atomic<long long> onwSoN[16];
|
| 508 |
+
+struct OnwSoT {
|
| 509 |
+
+ int i; std::chrono::steady_clock::time_point t0;
|
| 510 |
+
+ explicit OnwSoT(int ii): i(ii), t0(std::chrono::steady_clock::now()) {}
|
| 511 |
+
+ ~OnwSoT() { onwSoNs[i] += std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - t0).count(); ++onwSoN[i]; }
|
| 512 |
+
+};
|
| 513 |
+
+} // namespace
|
| 514 |
+
|
| 515 |
+
using namespace vpux;
|
| 516 |
+
using namespace VPU;
|
| 517 |
+
@@ -55,6 +72,7 @@ bool SubgraphOptimizer::isStrategySOHLike(VPU::ClusteredOpInterface op) {
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
bool SubgraphOptimizer::isValidStrategy(VPU::ClusteredOpInterface clusteredOp, VPU::MultiClusterStrategy strategy) {
|
| 521 |
+
+ OnwSoT onwT8(8);
|
| 522 |
+
auto moduleOp = clusteredOp->getParentOfType<mlir::ModuleOp>();
|
| 523 |
+
auto tileOp = config::getTileExecutor(moduleOp);
|
| 524 |
+
const auto numTiles = tileOp.getCount();
|
| 525 |
+
@@ -249,6 +267,7 @@ VPU::MultiClusterStrategy SubgraphOptimizer::getBestInSOHLikeStrategies(VPU::Clu
|
| 526 |
+
double SubgraphOptimizer::getInputSpillingCostToMultiClusterLayer(VPU::ClusteredOpInterface clusteredOp,
|
| 527 |
+
VPU::MultiClusterStrategy strategy,
|
| 528 |
+
const SubgraphOptConfig& config) {
|
| 529 |
+
+ OnwSoT onwT9(9);
|
| 530 |
+
return llvm::TypeSwitch<mlir::Operation*, double>(clusteredOp.getOperation())
|
| 531 |
+
.Case<NCEMaxPoolOp>([&](NCEMaxPoolOp origOp) {
|
| 532 |
+
return getInputSpillingCostToMultiClusterLayer(origOp, origOp.getInput(), strategy, config);
|
| 533 |
+
@@ -351,6 +370,7 @@ double SubgraphOptimizer::getInputSpillingCostToMultiClusterLayer(VPU::Clustered
|
| 534 |
+
double SubgraphOptimizer::getOutputSpillingCostToMultiClusterLayer(VPU::ClusteredOpInterface clusteredOp,
|
| 535 |
+
VPU::MultiClusterStrategy strategy,
|
| 536 |
+
const SubgraphOptConfig& config) {
|
| 537 |
+
+ OnwSoT onwT10(10);
|
| 538 |
+
bool hasCalculatedSpillingWriteCost = false;
|
| 539 |
+
double totalSpillingCost = 0.0;
|
| 540 |
+
// Iterate users in stable program order: the write cost is accumulated only for the first user
|
| 541 |
+
@@ -380,8 +400,10 @@ double SubgraphOptimizer::getOutputSpillingCostToMultiClusterLayer(VPU::Clustere
|
| 542 |
+
auto userStrategy = config.useRollbackStrategy
|
| 543 |
+
? getRollbackStrategy(userClusteredOp)
|
| 544 |
+
: _layerCostModel.getMultiClusterStrategyValue(userClusteredOp);
|
| 545 |
+
+ std::optional<OnwSoT> onwCs(std::in_place, 13);
|
| 546 |
+
auto spillingCost =
|
| 547 |
+
_layerCostModel.calculateSpillingCost(clusteredOp, userClusteredOp, strategy, userStrategy);
|
| 548 |
+
+ onwCs.reset();
|
| 549 |
+
if (!hasCalculatedSpillingWriteCost) {
|
| 550 |
+
totalSpillingCost += spillingCost.writeCost;
|
| 551 |
+
hasCalculatedSpillingWriteCost = true;
|
| 552 |
+
@@ -802,6 +824,7 @@ bool SubgraphOptimizer::hasInputSpillingToMultiClusterLayer(VPU::ClusteredOpInte
|
| 553 |
+
bool SubgraphOptimizer::hasInputSpillingToMultiClusterLayer(VPU::ClusteredOpInterface origClusteredOp,
|
| 554 |
+
mlir::Value input, VPU::MultiClusterStrategy origStrategy,
|
| 555 |
+
const SubgraphOptConfig& config) {
|
| 556 |
+
+ OnwSoT onwT12(12);
|
| 557 |
+
if (mlir::isa<mlir::BlockArgument>(input)) {
|
| 558 |
+
return false;
|
| 559 |
+
}
|
| 560 |
+
@@ -891,6 +914,7 @@ bool SubgraphOptimizer::hasOutputSpillingToMultiClusterLayer(VPU::ClusteredOpInt
|
| 561 |
+
mlir::Operation* userOp,
|
| 562 |
+
VPU::MultiClusterStrategy origStrategy,
|
| 563 |
+
const SubgraphOptConfig& config) {
|
| 564 |
+
+ OnwSoT onwT11(11);
|
| 565 |
+
auto propagateShapeCast = false;
|
| 566 |
+
|
| 567 |
+
if (mlir::isa<VPU::QuantizeCastOp, VPU::GroupSparseTensorOp>(userOp)) {
|
| 568 |
+
@@ -1010,6 +1034,8 @@ bool SubgraphOptimizer::hasOutputSpillingToMultiClusterLayer(VPU::ClusteredOpInt
|
| 569 |
+
/// 4. {SOK -> SOH -> SOK} convert to {SOH -> HK -> SOK}
|
| 570 |
+
/// @todo This algorithm is a local strategy optimization for subgraphs. The final strategy maybe not global optimized.
|
| 571 |
+
/// For example, {SOK -> SOK -> SOH} maybe better converting to {SOK -> SOK -> SOK}
|
| 572 |
+
+
|
| 573 |
+
+
|
| 574 |
+
void SubgraphOptimizer::optimizeStrategyAvoidSpillingOnSubgraph(VPU::ClusteredOpInterface clusteredOp) {
|
| 575 |
+
// #E112083 performance regression to be investigated in order to remove this filter
|
| 576 |
+
// TODO: [E-126102] Cost model for Grouped MatMul
|
| 577 |
+
@@ -1021,6 +1047,8 @@ void SubgraphOptimizer::optimizeStrategyAvoidSpillingOnSubgraph(VPU::ClusteredOp
|
| 578 |
+
// As a result we specifically handle it for Runtime Dequant cases. Details : E#162100
|
| 579 |
+
// Non-VF cost calculated here chooses misaligned strategy. This workaround can be removed when VF can
|
| 580 |
+
// align MC strategies itself. E#148750
|
| 581 |
+
+ OnwSoT onwAll(0);
|
| 582 |
+
+ std::optional<OnwSoT> onwPre(std::in_place, 1);
|
| 583 |
+
if (alignStrategyWithParentRuntimeDequant(clusteredOp, _layerCostModel)) {
|
| 584 |
+
_log.debug("[Subgraph Optimizer] Set SOK to Runtime Dequant Convolution to fix weights spilling");
|
| 585 |
+
}
|
| 586 |
+
@@ -1042,6 +1070,8 @@ void SubgraphOptimizer::optimizeStrategyAvoidSpillingOnSubgraph(VPU::ClusteredOp
|
| 587 |
+
return;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
+ onwPre.reset();
|
| 591 |
+
+ OnwSoT onwBfs(2);
|
| 592 |
+
_log.debug("[Subgraph Optimizer] Begin Op {0}", clusteredOp->getLoc());
|
| 593 |
+
|
| 594 |
+
layersNeedRollback.clear();
|
| 595 |
+
@@ -1089,8 +1119,12 @@ void SubgraphOptimizer::optimizeStrategyAvoidSpillingOnSubgraph(VPU::ClusteredOp
|
| 596 |
+
currentOp = layersNeedRollback.front();
|
| 597 |
+
auto clusteredCurrentOp = mlir::dyn_cast<ClusteredOpInterface>(currentOp.getOperation());
|
| 598 |
+
layersNeedRollback.pop_front();
|
| 599 |
+
- rollbackStrategy =
|
| 600 |
+
- rollbackToSOH ? getBestInSOHLikeStrategies(currentOp) : getBestInSOKLikeStrategies(currentOp);
|
| 601 |
+
+ ++onwSoN[3];
|
| 602 |
+
+ {
|
| 603 |
+
+ OnwSoT onwB(4);
|
| 604 |
+
+ rollbackStrategy =
|
| 605 |
+
+ rollbackToSOH ? getBestInSOHLikeStrategies(currentOp) : getBestInSOKLikeStrategies(currentOp);
|
| 606 |
+
+ }
|
| 607 |
+
// Sometimes a layer triggers rollback several times in a subgraph because we allows rollback strategy be
|
| 608 |
+
// changed during optimization. It makes sense when we find rollback strategy A first but later a better
|
| 609 |
+
// rollback strategy B is found. However if B is equal to A, it means the rollback strategy is converged. We
|
| 610 |
+
@@ -1109,6 +1143,7 @@ void SubgraphOptimizer::optimizeStrategyAvoidSpillingOnSubgraph(VPU::ClusteredOp
|
| 611 |
+
// Check if the child layer needs rollback strategy.
|
| 612 |
+
// Iterate users in stable program order so the rollback BFS exploration (and the resulting
|
| 613 |
+
// strategies) does not depend on use-list ordering.
|
| 614 |
+
+ std::optional<OnwSoT> onwCh(std::in_place, 6);
|
| 615 |
+
for (auto* directChild : usersInProgramOrder(currentOp.getOperation())) {
|
| 616 |
+
auto executeChild = directChild;
|
| 617 |
+
while (mlir::isa_and_nonnull<VPU::GroupSparseTensorOp>(executeChild) ||
|
| 618 |
+
@@ -1152,6 +1187,8 @@ void SubgraphOptimizer::optimizeStrategyAvoidSpillingOnSubgraph(VPU::ClusteredOp
|
| 619 |
+
}
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
+ onwCh.reset();
|
| 623 |
+
+ OnwSoT onwPa(7);
|
| 624 |
+
// Check if the parent layer needs rollback strategy
|
| 625 |
+
SmallVector<mlir::Value> layerInputs;
|
| 626 |
+
if (auto eltwiseOp = mlir::dyn_cast<VPU::NCEEltwiseOp>(currentOp.getOperation())) {
|
| 627 |
+
@@ -1210,6 +1247,7 @@ void SubgraphOptimizer::optimizeStrategyAvoidSpillingOnSubgraph(VPU::ClusteredOp
|
| 628 |
+
}
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
+ OnwSoT onwAfter(5);
|
| 632 |
+
// Calculate original cost and rollback cost
|
| 633 |
+
double originalCost = 0;
|
| 634 |
+
double rollbackCost = 0;
|
| 635 |
+
@@ -1447,7 +1485,9 @@ void SubgraphOptimizer::optimizeStrategyAvoidSpillingOnModel() {
|
| 636 |
+
// Assure Layer cost only include layer DPU cost under this phase. Exclude layer activation DMAs
|
| 637 |
+
_layerCostModel.setUnderSubgraphOpt(true);
|
| 638 |
+
// detect shortcuts in model like resnet
|
| 639 |
+
+ const auto onwS0 = std::chrono::steady_clock::now();
|
| 640 |
+
detectShortcuts();
|
| 641 |
+
+ const auto onwS1 = std::chrono::steady_clock::now();
|
| 642 |
+
|
| 643 |
+
const auto callback = [this](VPU::ClusteredOpInterface clusteredOp) {
|
| 644 |
+
if (auto swOp = mlir::dyn_cast_or_null<VPU::SWOpInterface>(clusteredOp.getOperation())) {
|
| 645 |
+
@@ -1465,6 +1505,21 @@ void SubgraphOptimizer::optimizeStrategyAvoidSpillingOnModel() {
|
| 646 |
+
|
| 647 |
+
/// @brief Traversing nodes with preOrder to execute subgraph optimization
|
| 648 |
+
_func.walk(callback);
|
| 649 |
+
+ const auto onwS2 = std::chrono::steady_clock::now();
|
| 650 |
+
+ if (const char* onwP = std::getenv("ONW_PROF_FILE")) {
|
| 651 |
+
+ if (FILE* f = std::fopen(onwP, "a")) {
|
| 652 |
+
+ std::fprintf(f, "subgraph detectShortcuts %.2f s avoidSpilling walk %.2f s | per-op all %lld %.2f s, pre-checks %lld %.2f s, rollback search %lld %.2f s, bfs visits %lld, best-strategy %lld %.2f s, after-bfs %lld %.2f s, children %.2f s, parents %.2f s, isValid %lld %.2f s, inSpill %lld %.2f s, outSpill %lld %.2f s, hasOut %lld %.2f s, hasIn %lld %.2f s, calcSpill(out) %lld %.2f s%c",
|
| 653 |
+
+ std::chrono::duration<double>(onwS1 - onwS0).count(),
|
| 654 |
+
+ std::chrono::duration<double>(onwS2 - onwS1).count(),
|
| 655 |
+
+ (long long)onwSoN[0].load(), onwSoNs[0].load() / 1e9, (long long)onwSoN[1].load(), onwSoNs[1].load() / 1e9,
|
| 656 |
+
+ (long long)onwSoN[2].load(), onwSoNs[2].load() / 1e9, (long long)onwSoN[3].load(),
|
| 657 |
+
+ (long long)onwSoN[4].load(), onwSoNs[4].load() / 1e9, (long long)onwSoN[5].load(), onwSoNs[5].load() / 1e9, onwSoNs[6].load() / 1e9, onwSoNs[7].load() / 1e9,
|
| 658 |
+
+ (long long)onwSoN[8].load(), onwSoNs[8].load() / 1e9, (long long)onwSoN[9].load(), onwSoNs[9].load() / 1e9,
|
| 659 |
+
+ (long long)onwSoN[10].load(), onwSoNs[10].load() / 1e9, (long long)onwSoN[11].load(), onwSoNs[11].load() / 1e9,
|
| 660 |
+
+ (long long)onwSoN[12].load(), onwSoNs[12].load() / 1e9, (long long)onwSoN[13].load(), onwSoNs[13].load() / 1e9, 10);
|
| 661 |
+
+ std::fclose(f);
|
| 662 |
+
+ }
|
| 663 |
+
+ }
|
| 664 |
+
|
| 665 |
+
const auto callbackForHK = [this](VPU::ClusteredOpInterface clusteredOp) {
|
| 666 |
+
auto currentStrategyAttr = clusteredOp.getMultiClusterStrategy();
|
| 667 |
+
diff --git a/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v1/merge_vf_region_rewriter.cpp b/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v1/merge_vf_region_rewriter.cpp
|
| 668 |
+
index 4207b33..7c9f3d1 100644
|
| 669 |
+
--- a/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v1/merge_vf_region_rewriter.cpp
|
| 670 |
+
+++ b/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v1/merge_vf_region_rewriter.cpp
|
| 671 |
+
@@ -17,6 +17,40 @@
|
| 672 |
+
|
| 673 |
+
#include <deque>
|
| 674 |
+
|
| 675 |
+
+#include <atomic>
|
| 676 |
+
+#include <chrono>
|
| 677 |
+
+#include <cstdio>
|
| 678 |
+
+#include <cstdlib>
|
| 679 |
+
+
|
| 680 |
+
+namespace {
|
| 681 |
+
+// [onw] profiling of the merge attempts (ONW_PROF_FILE=path: totals appended at exit)
|
| 682 |
+
+struct OnwVfProf {
|
| 683 |
+
+ std::atomic<long long> ns[5]{};
|
| 684 |
+
+ std::atomic<long long> n[5]{};
|
| 685 |
+
+ const char* tag;
|
| 686 |
+
+ explicit OnwVfProf(const char* t): tag(t) {}
|
| 687 |
+
+ ~OnwVfProf() {
|
| 688 |
+
+ const char* p = std::getenv("ONW_PROF_FILE");
|
| 689 |
+
+ if (p == nullptr) return;
|
| 690 |
+
+ if (FILE* f = std::fopen(p, "a")) {
|
| 691 |
+
+ const char* names[5] = {"match", "fuseOpsInBlock", "findVFCase", "checkVFCost", "fuseBlocks"};
|
| 692 |
+
+ for (int i = 0; i < 5; ++i)
|
| 693 |
+
+ std::fprintf(f, "%s %s: %lld calls %.2f s%c", tag, names[i], (long long)n[i].load(), ns[i].load() / 1e9, 10);
|
| 694 |
+
+ std::fclose(f);
|
| 695 |
+
+ }
|
| 696 |
+
+ }
|
| 697 |
+
+};
|
| 698 |
+
+struct OnwVfTimer {
|
| 699 |
+
+ OnwVfProf& p; int i; std::chrono::steady_clock::time_point t0;
|
| 700 |
+
+ OnwVfTimer(OnwVfProf& pp, int ii): p(pp), i(ii), t0(std::chrono::steady_clock::now()) {}
|
| 701 |
+
+ ~OnwVfTimer() {
|
| 702 |
+
+ p.ns[i] += std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - t0).count();
|
| 703 |
+
+ ++p.n[i];
|
| 704 |
+
+ }
|
| 705 |
+
+};
|
| 706 |
+
+OnwVfProf onwVfProf("v1");
|
| 707 |
+
+} // namespace
|
| 708 |
+
+
|
| 709 |
+
namespace vpux::VPU::VF::v1 {
|
| 710 |
+
|
| 711 |
+
namespace {
|
| 712 |
+
@@ -674,6 +708,7 @@ bool MergeVFRegionRewriter::checkVFCostFunction(VPU::VerticalFusionOp prevOp, VP
|
| 713 |
+
|
| 714 |
+
mlir::LogicalResult MergeVFRegionRewriter::matchAndRewrite(VPU::VerticalFusionOp vfOp,
|
| 715 |
+
mlir::PatternRewriter& rewriter) const {
|
| 716 |
+
+ OnwVfTimer onwT0(onwVfProf, 0);
|
| 717 |
+
_log.trace("Starting vertical fusion for region with VerticalFusionOp {0} at location {1}", vfOp, vfOp->getLoc());
|
| 718 |
+
if (vfOp.getIsManualConfigured()) {
|
| 719 |
+
_log.trace("Skipping vertical fusion for manually configured VerticalFusionOp at location {0}", vfOp->getLoc());
|
| 720 |
+
@@ -709,7 +744,10 @@ mlir::LogicalResult MergeVFRegionRewriter::matchAndRewrite(VPU::VerticalFusionOp
|
| 721 |
+
continue;
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
- vfBlock = fuseOpsInBlock(rewriter, vfOp, parentVFOp.getOperation());
|
| 725 |
+
+ {
|
| 726 |
+
+ OnwVfTimer onwT(onwVfProf, 1);
|
| 727 |
+
+ vfBlock = fuseOpsInBlock(rewriter, vfOp, parentVFOp.getOperation());
|
| 728 |
+
+ }
|
| 729 |
+
// `findVFCase` internally compares operations using IR order which is calculated "on the fly" in e.g.
|
| 730 |
+
// `isBeforeInBlock()`
|
| 731 |
+
// - order recalculation modifies the state and, due to multi-threaded environment, results in a data race
|
| 732 |
+
@@ -718,10 +756,25 @@ mlir::LogicalResult MergeVFRegionRewriter::matchAndRewrite(VPU::VerticalFusionOp
|
| 733 |
+
// As `findVFCase` is expected to not modify the IR, what can be done is an eager recalculation
|
| 734 |
+
// of the operation orders that would ensure later calls to e.g. `isBeforeInBlock()` become concurrent
|
| 735 |
+
// reads instead of concurrent writes (which is no longer a data race).
|
| 736 |
+
- vfBlock->getBlock()->recomputeOpOrder();
|
| 737 |
+
- vfBlock.getBody()->recomputeOpOrder();
|
| 738 |
+
- auto vfCase = findVFCase(parentVFOp, vfOp, vfBlock);
|
| 739 |
+
- if (!vfCase.has_value() || !checkVFCostFunction(parentVFOp, vfOp, vfCase.value())) {
|
| 740 |
+
+ // [onw] only when the order is invalid (see v2): renumbering the whole function block on every candidate made the
|
| 741 |
+
+ // pass quadratic in the op count
|
| 742 |
+
+ if (!vfBlock->getBlock()->isOpOrderValid()) {
|
| 743 |
+
+ vfBlock->getBlock()->recomputeOpOrder();
|
| 744 |
+
+ }
|
| 745 |
+
+ if (!vfBlock.getBody()->isOpOrderValid()) {
|
| 746 |
+
+ vfBlock.getBody()->recomputeOpOrder();
|
| 747 |
+
+ }
|
| 748 |
+
+ std::optional<VFCase> vfCase;
|
| 749 |
+
+ {
|
| 750 |
+
+ OnwVfTimer onwT(onwVfProf, 2);
|
| 751 |
+
+ vfCase = findVFCase(parentVFOp, vfOp, vfBlock);
|
| 752 |
+
+ }
|
| 753 |
+
+ bool onwCostOk = false;
|
| 754 |
+
+ if (vfCase.has_value()) {
|
| 755 |
+
+ OnwVfTimer onwT(onwVfProf, 3);
|
| 756 |
+
+ onwCostOk = checkVFCostFunction(parentVFOp, vfOp, vfCase.value());
|
| 757 |
+
+ }
|
| 758 |
+
+ if (!vfCase.has_value() || !onwCostOk) {
|
| 759 |
+
// Drop all references to vfBlock to avoid it being added back to the rewriter
|
| 760 |
+
// worklist.
|
| 761 |
+
vfBlock->dropAllReferences();
|
| 762 |
+
@@ -743,7 +796,10 @@ mlir::LogicalResult MergeVFRegionRewriter::matchAndRewrite(VPU::VerticalFusionOp
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
_log.trace("Merged subgraph {0}", vfBlock);
|
| 766 |
+
- fuseBlocks(rewriter, vfOp, vfBlock);
|
| 767 |
+
+ {
|
| 768 |
+
+ OnwVfTimer onwT(onwVfProf, 4);
|
| 769 |
+
+ fuseBlocks(rewriter, vfOp, vfBlock);
|
| 770 |
+
+ }
|
| 771 |
+
|
| 772 |
+
return mlir::success();
|
| 773 |
+
}
|
| 774 |
+
diff --git a/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v2/merge_vf_region_rewriter.cpp b/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v2/merge_vf_region_rewriter.cpp
|
| 775 |
+
index f3d8660..2e84289 100644
|
| 776 |
+
--- a/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v2/merge_vf_region_rewriter.cpp
|
| 777 |
+
+++ b/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v2/merge_vf_region_rewriter.cpp
|
| 778 |
+
@@ -28,6 +28,43 @@
|
| 779 |
+
#include <mlir/IR/IRMapping.h>
|
| 780 |
+
#include <mlir/IR/Value.h>
|
| 781 |
+
|
| 782 |
+
+#include <atomic>
|
| 783 |
+
+#include <string>
|
| 784 |
+
+#include <unordered_set>
|
| 785 |
+
+#include <mutex>
|
| 786 |
+
+#include <chrono>
|
| 787 |
+
+#include <cstdio>
|
| 788 |
+
+#include <cstdlib>
|
| 789 |
+
+
|
| 790 |
+
+namespace {
|
| 791 |
+
+// [onw] profiling of the merge attempts (ONW_PROF_FILE=path: totals appended at exit)
|
| 792 |
+
+struct OnwVfProf {
|
| 793 |
+
+ std::atomic<long long> ns[20]{};
|
| 794 |
+
+ std::atomic<long long> n[20]{};
|
| 795 |
+
+ const char* tag;
|
| 796 |
+
+ explicit OnwVfProf(const char* t): tag(t) {}
|
| 797 |
+
+ ~OnwVfProf() {
|
| 798 |
+
+ const char* p = std::getenv("ONW_PROF_FILE");
|
| 799 |
+
+ if (p == nullptr) return;
|
| 800 |
+
+ if (FILE* f = std::fopen(p, "a")) {
|
| 801 |
+
+ const char* names[20] = {"match", "fuseOpsInBlock", "findVFCase", "checkVFCost", "fuseBlocks", "isLegalFusion", "isMCAligned", "VFConfig x2", "policy.match", "adjustMC", "findVFTiling", "snapshot/restore", "tiling none fail", "tiling none ok", "tiling one fail", "tiling one ok", "tiling both fail", "tiling both ok", "skipMerge", "negCacheHit"};
|
| 802 |
+
+ for (int i = 0; i < 20; ++i)
|
| 803 |
+
+ std::fprintf(f, "%s %s: %lld calls %.2f s%c", tag, names[i], (long long)n[i].load(), ns[i].load() / 1e9, 10);
|
| 804 |
+
+ std::fclose(f);
|
| 805 |
+
+ }
|
| 806 |
+
+ }
|
| 807 |
+
+};
|
| 808 |
+
+struct OnwVfTimer {
|
| 809 |
+
+ OnwVfProf& p; int i; std::chrono::steady_clock::time_point t0;
|
| 810 |
+
+ OnwVfTimer(OnwVfProf& pp, int ii): p(pp), i(ii), t0(std::chrono::steady_clock::now()) {}
|
| 811 |
+
+ ~OnwVfTimer() {
|
| 812 |
+
+ p.ns[i] += std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - t0).count();
|
| 813 |
+
+ ++p.n[i];
|
| 814 |
+
+ }
|
| 815 |
+
+};
|
| 816 |
+
+OnwVfProf onwVfProf("v2");
|
| 817 |
+
+} // namespace
|
| 818 |
+
+
|
| 819 |
+
namespace vpux::VPU::VF::v2 {
|
| 820 |
+
|
| 821 |
+
namespace {
|
| 822 |
+
@@ -241,11 +278,20 @@ bool MergeVFRegionRewriter::isMCStrategyAligned(VPU::VerticalFusionOp currentOp,
|
| 823 |
+
|
| 824 |
+
std::optional<VFCase> MergeVFRegionRewriter::findVFCase(VPU::VerticalFusionOp prevOp, VPU::VerticalFusionOp currentOp,
|
| 825 |
+
VPU::VerticalFusionOp mergedVFOp) const {
|
| 826 |
+
- if (!isLegalFusion(currentOp, prevOp)) {
|
| 827 |
+
+ bool onwLegal;
|
| 828 |
+
+ {
|
| 829 |
+
+ OnwVfTimer onwT(onwVfProf, 5);
|
| 830 |
+
+ onwLegal = isLegalFusion(currentOp, prevOp);
|
| 831 |
+
+ }
|
| 832 |
+
+ if (!onwLegal) {
|
| 833 |
+
_log.trace("Illegal fusion between {0} and {1}", currentOp->getLoc(), prevOp->getLoc());
|
| 834 |
+
return std::nullopt;
|
| 835 |
+
}
|
| 836 |
+
- const auto isMCStrategyAlreadyAligned = isMCStrategyAligned(currentOp, prevOp);
|
| 837 |
+
+ bool isMCStrategyAlreadyAligned;
|
| 838 |
+
+ {
|
| 839 |
+
+ OnwVfTimer onwT(onwVfProf, 6);
|
| 840 |
+
+ isMCStrategyAlreadyAligned = isMCStrategyAligned(currentOp, prevOp);
|
| 841 |
+
+ }
|
| 842 |
+
const auto supportedPolicyTypes = getSupportedMergeTilingPolicyTypes();
|
| 843 |
+
|
| 844 |
+
for (const auto& tilingPolicy : _tilingPolicies) {
|
| 845 |
+
@@ -253,20 +299,47 @@ std::optional<VFCase> MergeVFRegionRewriter::findVFCase(VPU::VerticalFusionOp pr
|
| 846 |
+
continue;
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
- VFConfig prevConfig(prevOp, _cache, _enableVerticalFusionPipelining);
|
| 850 |
+
- VFConfig currentConfig(currentOp, _cache, _enableVerticalFusionPipelining);
|
| 851 |
+
- if (!tilingPolicy->match(prevConfig, currentConfig)) {
|
| 852 |
+
+ std::optional<VFConfig> prevConfigO, currentConfigO;
|
| 853 |
+
+ {
|
| 854 |
+
+ OnwVfTimer onwT(onwVfProf, 7);
|
| 855 |
+
+ prevConfigO.emplace(prevOp, _cache, _enableVerticalFusionPipelining);
|
| 856 |
+
+ currentConfigO.emplace(currentOp, _cache, _enableVerticalFusionPipelining);
|
| 857 |
+
+ }
|
| 858 |
+
+ auto& prevConfig = *prevConfigO;
|
| 859 |
+
+ auto& currentConfig = *currentConfigO;
|
| 860 |
+
+ bool onwMatch;
|
| 861 |
+
+ {
|
| 862 |
+
+ OnwVfTimer onwT(onwVfProf, 8);
|
| 863 |
+
+ onwMatch = tilingPolicy->match(prevConfig, currentConfig);
|
| 864 |
+
+ }
|
| 865 |
+
+ if (!onwMatch) {
|
| 866 |
+
continue;
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
- auto mcStrategyAttrSnapshot = snapshotMCStrategyAttrs(mergedVFOp);
|
| 870 |
+
- if (isMCStrategyAlreadyAligned || tilingPolicy->adjustMCStrategyInMergedVF(currentOp, prevOp, mergedVFOp)) {
|
| 871 |
+
- auto mergedCase = findVFTiling(mergedVFOp, prevOp, currentOp, *tilingPolicy);
|
| 872 |
+
+ std::optional<decltype(snapshotMCStrategyAttrs(mergedVFOp))> snapO;
|
| 873 |
+
+ {
|
| 874 |
+
+ OnwVfTimer onwT(onwVfProf, 11);
|
| 875 |
+
+ snapO.emplace(snapshotMCStrategyAttrs(mergedVFOp));
|
| 876 |
+
+ }
|
| 877 |
+
+ bool onwAdj = isMCStrategyAlreadyAligned;
|
| 878 |
+
+ if (!onwAdj) {
|
| 879 |
+
+ OnwVfTimer onwT(onwVfProf, 9);
|
| 880 |
+
+ onwAdj = tilingPolicy->adjustMCStrategyInMergedVF(currentOp, prevOp, mergedVFOp);
|
| 881 |
+
+ }
|
| 882 |
+
+ if (onwAdj) {
|
| 883 |
+
+ std::optional<VFCase> mergedCase;
|
| 884 |
+
+ {
|
| 885 |
+
+ OnwVfTimer onwT(onwVfProf, 10);
|
| 886 |
+
+ mergedCase = findVFTiling(mergedVFOp, prevOp, currentOp, *tilingPolicy);
|
| 887 |
+
+ }
|
| 888 |
+
if (mergedCase.has_value()) {
|
| 889 |
+
return mergedCase;
|
| 890 |
+
}
|
| 891 |
+
}
|
| 892 |
+
- restoreMCStrategyAttrs(mcStrategyAttrSnapshot);
|
| 893 |
+
+ {
|
| 894 |
+
+ OnwVfTimer onwT(onwVfProf, 11);
|
| 895 |
+
+ restoreMCStrategyAttrs(*snapO);
|
| 896 |
+
+ }
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
if (!isMCStrategyAlreadyAligned) {
|
| 900 |
+
@@ -325,6 +398,19 @@ MergeVFRegionRewriter::IVFSchedulingPtr MergeVFRegionRewriter::detectScenario(VF
|
| 901 |
+
std::optional<VFCase> MergeVFRegionRewriter::findVFTiling(VPU::VerticalFusionOp mergedOp, VPU::VerticalFusionOp prevOp,
|
| 902 |
+
VPU::VerticalFusionOp currentOp,
|
| 903 |
+
const MergeTilingPolicy& tilingPolicy) const {
|
| 904 |
+
+ const auto t0 = std::chrono::steady_clock::now();
|
| 905 |
+
+ const int cls = (hasTiling(parseIntArrayAttr<int64_t>(currentOp.getTilingStrategy())) ? 1 : 0) +
|
| 906 |
+
+ (hasTiling(parseIntArrayAttr<int64_t>(prevOp.getTilingStrategy())) ? 1 : 0);
|
| 907 |
+
+ auto r = findVFTilingImpl(mergedOp, prevOp, currentOp, tilingPolicy);
|
| 908 |
+
+ const int slot = 12 + cls * 2 + (r.has_value() ? 1 : 0);
|
| 909 |
+
+ onwVfProf.ns[slot] += std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - t0).count();
|
| 910 |
+
+ ++onwVfProf.n[slot];
|
| 911 |
+
+ return r;
|
| 912 |
+
+}
|
| 913 |
+
+
|
| 914 |
+
+std::optional<VFCase> MergeVFRegionRewriter::findVFTilingImpl(VPU::VerticalFusionOp mergedOp, VPU::VerticalFusionOp prevOp,
|
| 915 |
+
+ VPU::VerticalFusionOp currentOp,
|
| 916 |
+
+ const MergeTilingPolicy& tilingPolicy) const {
|
| 917 |
+
const auto currentTiling = parseIntArrayAttr<int64_t>(currentOp.getTilingStrategy());
|
| 918 |
+
const auto prevTiling = parseIntArrayAttr<int64_t>(prevOp.getTilingStrategy());
|
| 919 |
+
|
| 920 |
+
@@ -345,6 +431,7 @@ std::optional<VFCase> MergeVFRegionRewriter::findVFTiling(VPU::VerticalFusionOp
|
| 921 |
+
|
| 922 |
+
bool opsNeedTiling = prevHasTiling || curHasTiling;
|
| 923 |
+
if (canSkipMergeVF(vfConfig, opsNeedTiling)) {
|
| 924 |
+
+ ++onwVfProf.n[18];
|
| 925 |
+
return std::nullopt;
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
@@ -543,8 +630,61 @@ bool MergeVFRegionRewriter::checkVFCostFunction(VPU::VerticalFusionOp prevOp, VP
|
| 929 |
+
return true;
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
+
|
| 933 |
+
+namespace {
|
| 934 |
+
+// [onw] Structural signature of a VF candidate pair: the ops of both regions (names, attributes, operand / result
|
| 935 |
+
+// types with quantized element types reduced to their storage type) and the link between them. Group-unrolled
|
| 936 |
+
+// MatMuls (2-bit / 4-bit group-quantized weights) produce long runs of identically shaped VF regions; the same merge
|
| 937 |
+
+// was searched (tiling + VPUNN cost, ~15 ms) and rejected thousands of times. Rejections are remembered per
|
| 938 |
+
+// signature (ONW_VF_NEG_CACHE=0: off).
|
| 939 |
+
+llvm::hash_code onwHashType(mlir::Type t) {
|
| 940 |
+
+ if (auto nd = mlir::dyn_cast<vpux::NDTypeInterface>(t)) {
|
| 941 |
+
+ auto elem = nd.getElementType();
|
| 942 |
+
+ if (auto q = mlir::dyn_cast<mlir::quant::QuantizedType>(elem)) {
|
| 943 |
+
+ elem = q.getStorageType();
|
| 944 |
+
+ }
|
| 945 |
+
+ const auto shape = nd.getShape().raw();
|
| 946 |
+
+ return llvm::hash_combine(llvm::hash_combine_range(shape.begin(), shape.end()), elem.getAsOpaquePointer(),
|
| 947 |
+
+ nd.getDimsOrder().code(), nd.getMemSpace().getAsOpaquePointer(),
|
| 948 |
+
+ t.getTypeID().getAsOpaquePointer());
|
| 949 |
+
+ }
|
| 950 |
+
+ return llvm::hash_value(t.getAsOpaquePointer());
|
| 951 |
+
+}
|
| 952 |
+
+
|
| 953 |
+
+llvm::hash_code onwHashVF(VPU::VerticalFusionOp op) {
|
| 954 |
+
+ llvm::hash_code h = llvm::hash_value(op->getAttrDictionary().getAsOpaquePointer());
|
| 955 |
+
+ for (auto v : op->getOperands()) {
|
| 956 |
+
+ h = llvm::hash_combine(h, onwHashType(v.getType()));
|
| 957 |
+
+ }
|
| 958 |
+
+ op.getBody()->walk([&](mlir::Operation* o) {
|
| 959 |
+
+ h = llvm::hash_combine(h, o->getName().getAsOpaquePointer(), o->getAttrDictionary().getAsOpaquePointer());
|
| 960 |
+
+ for (auto v : o->getOperands()) {
|
| 961 |
+
+ h = llvm::hash_combine(h, onwHashType(v.getType()));
|
| 962 |
+
+ }
|
| 963 |
+
+ for (auto t : o->getResultTypes()) {
|
| 964 |
+
+ h = llvm::hash_combine(h, onwHashType(t));
|
| 965 |
+
+ }
|
| 966 |
+
+ });
|
| 967 |
+
+ return h;
|
| 968 |
+
+}
|
| 969 |
+
+
|
| 970 |
+
+struct OnwVfNegCache {
|
| 971 |
+
+ std::mutex mutex;
|
| 972 |
+
+ std::unordered_set<size_t> keys;
|
| 973 |
+
+};
|
| 974 |
+
+OnwVfNegCache& onwVfNegCache() {
|
| 975 |
+
+ static OnwVfNegCache c;
|
| 976 |
+
+ return c;
|
| 977 |
+
+}
|
| 978 |
+
+bool onwNegCacheOn() {
|
| 979 |
+
+ static const bool on = std::getenv("ONW_VF_NEG_CACHE") == nullptr || std::string(std::getenv("ONW_VF_NEG_CACHE")) != "0";
|
| 980 |
+
+ return on;
|
| 981 |
+
+}
|
| 982 |
+
+} // namespace
|
| 983 |
+
+
|
| 984 |
+
mlir::LogicalResult MergeVFRegionRewriter::matchAndRewrite(VPU::VerticalFusionOp vfOp,
|
| 985 |
+
mlir::PatternRewriter& rewriter) const {
|
| 986 |
+
+ OnwVfTimer onwT0(onwVfProf, 0);
|
| 987 |
+
_log.trace("Starting vertical fusion for region with VerticalFusionOp {0} at location {1}", vfOp, vfOp->getLoc());
|
| 988 |
+
if (vfOp.getIsManualConfigured()) {
|
| 989 |
+
_log.trace("Skipping vertical fusion for manually configured VerticalFusionOp at location {0}", vfOp->getLoc());
|
| 990 |
+
@@ -553,13 +693,27 @@ mlir::LogicalResult MergeVFRegionRewriter::matchAndRewrite(VPU::VerticalFusionOp
|
| 991 |
+
|
| 992 |
+
VPU::VerticalFusionOp vfBlock = nullptr;
|
| 993 |
+
VPU::VerticalFusionOp parentVFOp = nullptr;
|
| 994 |
+
+ unsigned onwIdx = 0;
|
| 995 |
+
for (auto operand : vfOp->getOperands()) {
|
| 996 |
+
+ const unsigned onwOperandIdx = onwIdx++;
|
| 997 |
+
parentVFOp = operand.getDefiningOp<VPU::VerticalFusionOp>();
|
| 998 |
+
vfBlock = nullptr;
|
| 999 |
+
|
| 1000 |
+
if (parentVFOp == nullptr || parentVFOp.getIsManualConfigured()) {
|
| 1001 |
+
continue;
|
| 1002 |
+
}
|
| 1003 |
+
+ size_t onwKey = 0;
|
| 1004 |
+
+ if (onwNegCacheOn()) {
|
| 1005 |
+
+ onwKey = llvm::hash_combine(onwHashVF(parentVFOp), onwHashVF(vfOp), onwOperandIdx);
|
| 1006 |
+
+ std::lock_guard<std::mutex> lock(onwVfNegCache().mutex);
|
| 1007 |
+
+ if (onwVfNegCache().keys.count(onwKey) != 0) {
|
| 1008 |
+
+ ++onwVfProf.n[19];
|
| 1009 |
+
+ if (checkOtherVFInput(vfOp, parentVFOp)) {
|
| 1010 |
+
+ continue;
|
| 1011 |
+
+ }
|
| 1012 |
+
+ return mlir::failure();
|
| 1013 |
+
+ }
|
| 1014 |
+
+ }
|
| 1015 |
+
|
| 1016 |
+
_log.trace("Analyzing vertical fusion region with parent VerticalFusionOp {0} at location {1}", parentVFOp,
|
| 1017 |
+
parentVFOp->getLoc());
|
| 1018 |
+
@@ -580,7 +734,10 @@ mlir::LogicalResult MergeVFRegionRewriter::matchAndRewrite(VPU::VerticalFusionOp
|
| 1019 |
+
continue;
|
| 1020 |
+
}
|
| 1021 |
+
|
| 1022 |
+
- vfBlock = fuseOpsInBlock(rewriter, vfOp, parentVFOp.getOperation());
|
| 1023 |
+
+ {
|
| 1024 |
+
+ OnwVfTimer onwT(onwVfProf, 1);
|
| 1025 |
+
+ vfBlock = fuseOpsInBlock(rewriter, vfOp, parentVFOp.getOperation());
|
| 1026 |
+
+ }
|
| 1027 |
+
_log.trace("Merged subgraph candidate {0}", vfBlock.getLoc());
|
| 1028 |
+
// `findVFCase` internally compares operations using IR order which is calculated "on the fly" in e.g.
|
| 1029 |
+
// `isBeforeInBlock()`
|
| 1030 |
+
@@ -590,15 +747,34 @@ mlir::LogicalResult MergeVFRegionRewriter::matchAndRewrite(VPU::VerticalFusionOp
|
| 1031 |
+
// As `findVFCase` is expected to not modify the IR, what can be done is an eager recalculation
|
| 1032 |
+
// of the operation orders that would ensure later calls to e.g. `isBeforeInBlock()` become concurrent
|
| 1033 |
+
// reads instead of concurrent writes (which is no longer a data race).
|
| 1034 |
+
- vfBlock->getBlock()->recomputeOpOrder();
|
| 1035 |
+
- vfBlock.getBody()->recomputeOpOrder();
|
| 1036 |
+
- auto vfCase = findVFCase(parentVFOp, vfOp, vfBlock);
|
| 1037 |
+
- if (!vfCase.has_value() || !checkVFCostFunction(parentVFOp, vfOp, vfCase.value())) {
|
| 1038 |
+
+ // [onw] only when the order is invalid: renumbering the whole function block on every candidate made the pass
|
| 1039 |
+
+ // quadratic in the op count (Bonsai 2-bit group-unrolled graphs: ~60% of the compile time)
|
| 1040 |
+
+ if (!vfBlock->getBlock()->isOpOrderValid()) {
|
| 1041 |
+
+ vfBlock->getBlock()->recomputeOpOrder();
|
| 1042 |
+
+ }
|
| 1043 |
+
+ if (!vfBlock.getBody()->isOpOrderValid()) {
|
| 1044 |
+
+ vfBlock.getBody()->recomputeOpOrder();
|
| 1045 |
+
+ }
|
| 1046 |
+
+ std::optional<VFCase> vfCase;
|
| 1047 |
+
+ {
|
| 1048 |
+
+ OnwVfTimer onwT(onwVfProf, 2);
|
| 1049 |
+
+ vfCase = findVFCase(parentVFOp, vfOp, vfBlock);
|
| 1050 |
+
+ }
|
| 1051 |
+
+ bool onwCostOk = false;
|
| 1052 |
+
+ if (vfCase.has_value()) {
|
| 1053 |
+
+ OnwVfTimer onwT(onwVfProf, 3);
|
| 1054 |
+
+ onwCostOk = checkVFCostFunction(parentVFOp, vfOp, vfCase.value());
|
| 1055 |
+
+ }
|
| 1056 |
+
+ if (!vfCase.has_value() || !onwCostOk) {
|
| 1057 |
+
// Drop all references to vfBlock to avoid it being added back to the rewriter
|
| 1058 |
+
// worklist.
|
| 1059 |
+
vfBlock->dropAllReferences();
|
| 1060 |
+
rewriter.eraseOp(vfBlock);
|
| 1061 |
+
vfBlock = nullptr;
|
| 1062 |
+
+ if (onwNegCacheOn()) {
|
| 1063 |
+
+ std::lock_guard<std::mutex> lock(onwVfNegCache().mutex);
|
| 1064 |
+
+ onwVfNegCache().keys.insert(onwKey);
|
| 1065 |
+
+ }
|
| 1066 |
+
if (checkOtherVFInput(vfOp, parentVFOp)) {
|
| 1067 |
+
continue;
|
| 1068 |
+
}
|
| 1069 |
+
@@ -614,7 +790,10 @@ mlir::LogicalResult MergeVFRegionRewriter::matchAndRewrite(VPU::VerticalFusionOp
|
| 1070 |
+
}
|
| 1071 |
+
|
| 1072 |
+
_log.trace("Merged subgraph {0}", vfBlock->getLoc());
|
| 1073 |
+
- fuseBlocks(rewriter, vfOp, vfBlock);
|
| 1074 |
+
+ {
|
| 1075 |
+
+ OnwVfTimer onwT(onwVfProf, 4);
|
| 1076 |
+
+ fuseBlocks(rewriter, vfOp, vfBlock);
|
| 1077 |
+
+ }
|
| 1078 |
+
|
| 1079 |
+
return mlir::success();
|
| 1080 |
+
}
|
| 1081 |
+
diff --git a/src/vpux_compiler/src/dialect/VPU/utils/weights_separation.cpp b/src/vpux_compiler/src/dialect/VPU/utils/weights_separation.cpp
|
| 1082 |
+
index 35a6c00..d48e08c 100644
|
| 1083 |
+
--- a/src/vpux_compiler/src/dialect/VPU/utils/weights_separation.cpp
|
| 1084 |
+
+++ b/src/vpux_compiler/src/dialect/VPU/utils/weights_separation.cpp
|
| 1085 |
+
@@ -4,6 +4,7 @@
|
| 1086 |
+
//
|
| 1087 |
+
|
| 1088 |
+
#include "vpux/compiler/dialect/VPU/utils/weights_separation.hpp"
|
| 1089 |
+
+#include "vpux/compiler/core/types/quantile_float/types.hpp"
|
| 1090 |
+
#include "vpux/compiler/core/force_link_macros.hpp"
|
| 1091 |
+
#include "vpux/compiler/dialect/IE/IR/ops/data_movement.hpp"
|
| 1092 |
+
#include "vpux/compiler/dialect/IE/IR/ops/data_type.hpp"
|
| 1093 |
+
@@ -345,6 +346,9 @@ mlir::Value createAvgPoolForInterQuantizedConvert(mlir::OpBuilder& builder, mlir
|
| 1094 |
+
/// Returns an IE operation for the given constant transformation.
|
| 1095 |
+
mlir::Value createMatchingIeOperation(mlir::OpBuilder& builder, mlir::Location loc, mlir::Value input,
|
| 1096 |
+
Const::TransformAttrInterface t) {
|
| 1097 |
+
+ if (std::getenv("ONW_DEBUG_WS") != nullptr) {
|
| 1098 |
+
+ llvm::errs() << "ONW ws transform: in " << input.getType() << " t " << t << char(10);
|
| 1099 |
+
+ }
|
| 1100 |
+
return llvm::TypeSwitch<Const::TransformAttrInterface, mlir::Value>(t)
|
| 1101 |
+
.Case<Const::AddAttr>([&](Const::AddAttr add) {
|
| 1102 |
+
const auto biasValue = checked_cast<float>(add.getBias().getValueAsDouble());
|
| 1103 |
+
@@ -398,15 +402,94 @@ mlir::Value createMatchingIeOperation(mlir::OpBuilder& builder, mlir::Location l
|
| 1104 |
+
const auto inElemType = mlir::cast<NDTypeInterface>(input.getType()).getElementType();
|
| 1105 |
+
const auto outElemType = cast.getElemType();
|
| 1106 |
+
|
| 1107 |
+
- const bool inputQuantized = mlir::isa<mlir::quant::QuantizedType>(inElemType);
|
| 1108 |
+
- const bool outputQuantized = mlir::isa<mlir::quant::QuantizedType>(outElemType);
|
| 1109 |
+
+ // onw: quantile (NF4) element types are a storage reinterpretation too, like quantized ones
|
| 1110 |
+
+ const bool inputQuantized = mlir::isa<mlir::quant::QuantizedType, vpux::type::QuantileType>(inElemType);
|
| 1111 |
+
+ const bool outputQuantized =
|
| 1112 |
+
+ mlir::isa<mlir::quant::QuantizedType, vpux::type::QuantileType>(outElemType);
|
| 1113 |
+
|
| 1114 |
+
+ const auto hasQuantileStorage = [](mlir::Type t) {
|
| 1115 |
+
+ if (auto q = mlir::dyn_cast<mlir::quant::QuantizedType>(t)) {
|
| 1116 |
+
+ t = q.getStorageType();
|
| 1117 |
+
+ }
|
| 1118 |
+
+ return mlir::isa<vpux::type::QuantileType>(t);
|
| 1119 |
+
+ };
|
| 1120 |
+
+ if (hasQuantileStorage(inElemType) || hasQuantileStorage(outElemType)) {
|
| 1121 |
+
+ // onw: CastElemType keeps the stored values (NF4 table indices); go through the storage type:
|
| 1122 |
+
+ // quantile -> storage int (reinterpret), int/float conversions of the index values (via f16,
|
| 1123 |
+
+ // exact for 0..15), storage int -> quantile (reinterpret)
|
| 1124 |
+
+ auto storageOf = [](mlir::Type t) -> mlir::Type {
|
| 1125 |
+
+ // quant.uniform<quantile<ui4>> -> quantile<ui4> -> ui4
|
| 1126 |
+
+ for (int k = 0; k < 2; ++k) {
|
| 1127 |
+
+ if (auto q = mlir::dyn_cast<vpux::type::QuantileType>(t)) {
|
| 1128 |
+
+ t = q.getStorageType();
|
| 1129 |
+
+ } else if (auto q = mlir::dyn_cast<mlir::quant::QuantizedType>(t)) {
|
| 1130 |
+
+ t = q.getStorageType();
|
| 1131 |
+
+ }
|
| 1132 |
+
+ }
|
| 1133 |
+
+ return t;
|
| 1134 |
+
+ };
|
| 1135 |
+
+ // Index values only pass through value-preserving casts here (integer widening, int <-> f16,
|
| 1136 |
+
+ // storage reinterpretation). Walk back to the earliest value of this chain whose type is the
|
| 1137 |
+
+ // target or its storage type and reinterpret that directly - the greedy canonicalizer would
|
| 1138 |
+
+ // otherwise fold a Convert+QuantizeCast pair into an unsupported "Convert to quantile".
|
| 1139 |
+
+ {
|
| 1140 |
+
+ const auto targetStorage = storageOf(outElemType);
|
| 1141 |
+
+ mlir::Value origin = nullptr;
|
| 1142 |
+
+ for (mlir::Value v = input; v != nullptr;) {
|
| 1143 |
+
+ const auto vt = mlir::cast<NDTypeInterface>(v.getType()).getElementType();
|
| 1144 |
+
+ if (vt == outElemType || vt == targetStorage) {
|
| 1145 |
+
+ origin = v;
|
| 1146 |
+
+ }
|
| 1147 |
+
+ auto* def = v.getDefiningOp();
|
| 1148 |
+
+ if (!mlir::isa_and_nonnull<IE::ConvertOp, IE::QuantizeCastOp>(def)) {
|
| 1149 |
+
+ break;
|
| 1150 |
+
+ }
|
| 1151 |
+
+ v = def->getOperand(0);
|
| 1152 |
+
+ }
|
| 1153 |
+
+ if (origin != nullptr && mlir::cast<NDTypeInterface>(origin.getType()).getShape() ==
|
| 1154 |
+
+ mlir::cast<NDTypeInterface>(input.getType()).getShape()) {
|
| 1155 |
+
+ const auto ot = mlir::cast<NDTypeInterface>(origin.getType()).getElementType();
|
| 1156 |
+
+ return ot == outElemType ? origin
|
| 1157 |
+
+ : builder.create<IE::QuantizeCastOp>(loc, origin, outElemType)
|
| 1158 |
+
+ .getOutput();
|
| 1159 |
+
+ }
|
| 1160 |
+
+ }
|
| 1161 |
+
+ const auto f16 = mlir::Float16Type::get(builder.getContext());
|
| 1162 |
+
+ mlir::Value x = input;
|
| 1163 |
+
+ mlir::Type cur = inElemType;
|
| 1164 |
+
+ if (storageOf(cur) != cur) {
|
| 1165 |
+
+ cur = storageOf(cur);
|
| 1166 |
+
+ x = builder.create<IE::QuantizeCastOp>(appendLoc(loc, "onw_q_in"), x, cur);
|
| 1167 |
+
+ }
|
| 1168 |
+
+ const auto target = storageOf(outElemType);
|
| 1169 |
+
+ if (cur != target) {
|
| 1170 |
+
+ if (cur != f16 && target != f16 && !mlir::isa<mlir::FloatType>(cur)) {
|
| 1171 |
+
+ x = builder.create<IE::ConvertOp>(appendLoc(loc, "onw_q_f16"), x, f16);
|
| 1172 |
+
+ }
|
| 1173 |
+
+ x = builder.create<IE::ConvertOp>(appendLoc(loc, "onw_q_cvt"), x, target);
|
| 1174 |
+
+ }
|
| 1175 |
+
+ if (target != outElemType) {
|
| 1176 |
+
+ x = builder.create<IE::QuantizeCastOp>(loc, x, outElemType);
|
| 1177 |
+
+ }
|
| 1178 |
+
+ return x;
|
| 1179 |
+
+ }
|
| 1180 |
+
if (inputQuantized || outputQuantized) {
|
| 1181 |
+
return createTypeCorrectedQuantizedCast(builder, input, loc, inElemType, outElemType);
|
| 1182 |
+
}
|
| 1183 |
+
return builder.create<IE::ConvertOp>(loc, input, outElemType);
|
| 1184 |
+
})
|
| 1185 |
+
.Case<Const::ConvertElemTypeAttr>([&](Const::ConvertElemTypeAttr convert) -> mlir::Value {
|
| 1186 |
+
+ {
|
| 1187 |
+
+ // onw: storage integer <-> quantile (NF4) of the same width is a reinterpretation
|
| 1188 |
+
+ const auto inT = mlir::cast<NDTypeInterface>(input.getType()).getElementType();
|
| 1189 |
+
+ const auto outT = convert.getElemType();
|
| 1190 |
+
+ const auto inQ = mlir::dyn_cast<vpux::type::QuantileType>(inT);
|
| 1191 |
+
+ const auto outQ = mlir::dyn_cast<vpux::type::QuantileType>(outT);
|
| 1192 |
+
+ if ((outQ != nullptr && outQ.getStorageType() == inT) ||
|
| 1193 |
+
+ (inQ != nullptr && inQ.getStorageType() == outT)) {
|
| 1194 |
+
+ return builder.create<IE::QuantizeCastOp>(loc, input, outT);
|
| 1195 |
+
+ }
|
| 1196 |
+
+ }
|
| 1197 |
+
const auto inElemType = mlir::dyn_cast_or_null<mlir::quant::QuantizedType>(
|
| 1198 |
+
mlir::cast<NDTypeInterface>(input.getType()).getElementType());
|
| 1199 |
+
const auto outElemType = mlir::dyn_cast_or_null<mlir::quant::QuantizedType>(convert.getElemType());
|
| 1200 |
+
diff --git a/src/vpux_compiler/src/dialect/VPUIP/transforms/passes/convert_async_ops_to_tasks.cpp b/src/vpux_compiler/src/dialect/VPUIP/transforms/passes/convert_async_ops_to_tasks.cpp
|
| 1201 |
+
index f637bad..6c3db25 100644
|
| 1202 |
+
--- a/src/vpux_compiler/src/dialect/VPUIP/transforms/passes/convert_async_ops_to_tasks.cpp
|
| 1203 |
+
+++ b/src/vpux_compiler/src/dialect/VPUIP/transforms/passes/convert_async_ops_to_tasks.cpp
|
| 1204 |
+
@@ -164,7 +164,40 @@ mlir::LogicalResult RemoveWait::matchAndRewrite(mlir::async::AwaitOp waitOp, OpA
|
| 1205 |
+
// If you faced with "'async.await' has more than one consumer" make sure you add your layer
|
| 1206 |
+
// into src/vpux_compiler/src/dialect/VPUIP/ops.cpp `redirectOpInterfacesForIE(...)`
|
| 1207 |
+
VPUX_THROW_UNLESS(waitOp.getResult().hasOneUse(), "'async.await' doesn't have only one consumer");
|
| 1208 |
+
- VPUX_THROW_UNLESS(mlir::isa<mlir::func::ReturnOp>(*waitOp.getResult().user_begin()),
|
| 1209 |
+
+ if (!mlir::isa<mlir::func::ReturnOp>(*waitOp.getResult().user_begin())) {
|
| 1210 |
+
+ if (const char* onwDump = std::getenv("ONW_DEBUG_ASYNC")) {
|
| 1211 |
+
+ std::error_code ec;
|
| 1212 |
+
+ llvm::raw_fd_ostream fo(onwDump, ec);
|
| 1213 |
+
+ if (!ec) {
|
| 1214 |
+
+ fo << "=== await user: " << **waitOp.getResult().user_begin() << char(10);
|
| 1215 |
+
+ fo << "=== await: " << waitOp << char(10);
|
| 1216 |
+
+ waitOp->getParentOfType<mlir::func::FuncOp>()->print(fo, mlir::OpPrintingFlags().enableDebugInfo(true, false));
|
| 1217 |
+
+ fo << char(10);
|
| 1218 |
+
+ }
|
| 1219 |
+
+ }
|
| 1220 |
+
+ }
|
| 1221 |
+
+ // onw: a pure view (ShapeCast, PermuteCast, ...: no data movement) between the await and the return is fine as
|
| 1222 |
+
+ // well - the 8.3 pipeline leaves such views of a function's result outside the async regions (Qwen3.5 4-token
|
| 1223 |
+
+ // blocks, Bonsai 4/8-token blocks: "'async.await' has non 'return' consumer")
|
| 1224 |
+
+ const auto onlyViewsToReturn = [](mlir::Value v) {
|
| 1225 |
+
+ while (true) {
|
| 1226 |
+
+ if (!v.hasOneUse()) {
|
| 1227 |
+
+ return false;
|
| 1228 |
+
+ }
|
| 1229 |
+
+ auto* user = *v.user_begin();
|
| 1230 |
+
+ if (mlir::isa<mlir::func::ReturnOp>(user)) {
|
| 1231 |
+
+ return true;
|
| 1232 |
+
+ }
|
| 1233 |
+
+ if (!mlir::isa<VPUIP::ShapeCastOp, VPUIP::PermuteCastOp, VPUIP::GenericReshapeOp, VPUIP::QuantizeCastOp,
|
| 1234 |
+
+ VPUIP::ViewOp>(user) ||
|
| 1235 |
+
+ user->getNumResults() != 1) {
|
| 1236 |
+
+ return false;
|
| 1237 |
+
+ }
|
| 1238 |
+
+ v = user->getResult(0);
|
| 1239 |
+
+ }
|
| 1240 |
+
+ };
|
| 1241 |
+
+ VPUX_THROW_UNLESS(mlir::isa<mlir::func::ReturnOp>(*waitOp.getResult().user_begin()) ||
|
| 1242 |
+
+ onlyViewsToReturn(waitOp.getResult()),
|
| 1243 |
+
"'async.await' has non 'return' consumer");
|
| 1244 |
+
|
| 1245 |
+
rewriter.replaceOp(waitOp, newArgs.getOperand());
|
| 1246 |
+
diff --git a/src/vpux_compiler/src/dialect/VPUIP/transforms/passes/unroll_distributed_ops.cpp b/src/vpux_compiler/src/dialect/VPUIP/transforms/passes/unroll_distributed_ops.cpp
|
| 1247 |
+
index 23fe4fc..b93a938 100644
|
| 1248 |
+
--- a/src/vpux_compiler/src/dialect/VPUIP/transforms/passes/unroll_distributed_ops.cpp
|
| 1249 |
+
+++ b/src/vpux_compiler/src/dialect/VPUIP/transforms/passes/unroll_distributed_ops.cpp
|
| 1250 |
+
@@ -1732,6 +1732,13 @@ void VPUIP::unrollDistributedOpsCommon40XXPlus(mlir::func::FuncOp func,
|
| 1251 |
+
const VPUIP::ClusterNCERewriter nceRewriter(ctx, log);
|
| 1252 |
+
const VPUIP::ClusterConvertDMARewriter convertDMARewriter(ctx, dmaPortCount, log);
|
| 1253 |
+
|
| 1254 |
+
+ if (const char* onwDump = std::getenv("ONW_DUMP_UNROLL")) {
|
| 1255 |
+
+ std::error_code ec;
|
| 1256 |
+
+ llvm::raw_fd_ostream fo((std::string(onwDump) + "_" + func.getName().str() + ".mlir"), ec);
|
| 1257 |
+
+ if (!ec) {
|
| 1258 |
+
+ func->print(fo, mlir::OpPrintingFlags().enableDebugInfo(true, false));
|
| 1259 |
+
+ }
|
| 1260 |
+
+ }
|
| 1261 |
+
uint64_t unrollIdx = 0;
|
| 1262 |
+
func.walk<mlir::WalkOrder::PostOrder>([&](VPURT::TaskOp vpurtTask) {
|
| 1263 |
+
auto op = vpurtTask.getInnerTaskOp();
|
| 1264 |
+
@@ -1740,6 +1747,7 @@ void VPUIP::unrollDistributedOpsCommon40XXPlus(mlir::func::FuncOp func,
|
| 1265 |
+
}
|
| 1266 |
+
|
| 1267 |
+
mlir::OpBuilder builder(op);
|
| 1268 |
+
+ try {
|
| 1269 |
+
if (auto nndmaOp = mlir::dyn_cast<VPUIP::NNDMAOp>(op)) {
|
| 1270 |
+
dmaRewriter.matchAndRewrite(nndmaOp, builder, unrollIdx, /*isDataOverlapped*/ true);
|
| 1271 |
+
unrollIdx++;
|
| 1272 |
+
@@ -1748,6 +1756,22 @@ void VPUIP::unrollDistributedOpsCommon40XXPlus(mlir::func::FuncOp func,
|
| 1273 |
+
} else if (auto dmaOp = mlir::dyn_cast<VPUIP::DMATypeOpInterface>(op)) {
|
| 1274 |
+
convertDMARewriter.matchAndRewrite(dmaOp, builder);
|
| 1275 |
+
}
|
| 1276 |
+
+ } catch (...) {
|
| 1277 |
+
+ if (std::getenv("ONW_DEBUG_UNROLL") != nullptr) {
|
| 1278 |
+
+ llvm::errs() << "ONW unroll failed on: " << *op << char(10);
|
| 1279 |
+
+ {
|
| 1280 |
+
+ std::error_code ec;
|
| 1281 |
+
+ llvm::raw_fd_ostream fo(std::getenv("ONW_DEBUG_UNROLL"), ec);
|
| 1282 |
+
+ if (!ec) {
|
| 1283 |
+
+ func->print(fo);
|
| 1284 |
+
+ }
|
| 1285 |
+
+ }
|
| 1286 |
+
+ for (auto operand : op->getOperands()) {
|
| 1287 |
+
+ llvm::errs() << " operand type: " << operand.getType() << char(10); if (auto d = operand.getDefiningOp()) { llvm::errs() << " def: " << *d << char(10); for (auto u : d->getOperands()) if (auto dd = u.getDefiningOp()) llvm::errs() << " def-def: " << *dd << char(10); }
|
| 1288 |
+
+ }
|
| 1289 |
+
+ }
|
| 1290 |
+
+ throw;
|
| 1291 |
+
+ }
|
| 1292 |
+
});
|
| 1293 |
+
}
|
| 1294 |
+
|
| 1295 |
+
diff --git a/src/vpux_compiler/src/dialect/const/attributes/content.cpp b/src/vpux_compiler/src/dialect/const/attributes/content.cpp
|
| 1296 |
+
index 3f4f862..f5d66fe 100644
|
| 1297 |
+
--- a/src/vpux_compiler/src/dialect/const/attributes/content.cpp
|
| 1298 |
+
+++ b/src/vpux_compiler/src/dialect/const/attributes/content.cpp
|
| 1299 |
+
@@ -7,6 +7,14 @@
|
| 1300 |
+
#include "vpux/compiler/dialect/const/attributes/stable_hash_storage.hpp"
|
| 1301 |
+
#include "vpux/compiler/dialect/const/constant_call_stack.hpp"
|
| 1302 |
+
#include "vpux/compiler/dialect/const/constant_transformations_control.hpp"
|
| 1303 |
+
+
|
| 1304 |
+
+#include <atomic>
|
| 1305 |
+
+#include <cstdio>
|
| 1306 |
+
+#include <cstdlib>
|
| 1307 |
+
+#include <list>
|
| 1308 |
+
+#include <memory>
|
| 1309 |
+
+#include <mutex>
|
| 1310 |
+
+#include <vector>
|
| 1311 |
+
#include "vpux/compiler/dialect/const/dialect.hpp"
|
| 1312 |
+
#include "vpux/compiler/dialect/const/utils/const_logger.hpp"
|
| 1313 |
+
#include "vpux/compiler/utils/stable_hash.hpp"
|
| 1314 |
+
@@ -468,11 +476,117 @@ mlir::DenseElementsAttr Const::detail::createConstContentWithConversion(mlir::Sh
|
| 1315 |
+
// ContentAttr::fold
|
| 1316 |
+
//
|
| 1317 |
+
|
| 1318 |
+
+namespace {
|
| 1319 |
+
+// [onw] Folding memo for "... Concat ... SubView ..." chains: a large weight is concatenated from many pieces (the
|
| 1320 |
+
+// unrolled groups of a group-quantized MatMul, Q/K/V merged), reshaped, then cut into DPU tiles with a SubView each.
|
| 1321 |
+
+// When the SubView cannot be moved in front of the reshape (it slices the inner factor of a flattened dim), every tile
|
| 1322 |
+
+// folds the whole concatenation again: O(tiles x weight) - most of the ELF export time of one-row (decode) graphs
|
| 1323 |
+
+// (Granite 4.2 8B part on NPU 5010: ~2000 tiles x 0.16 s). The result of the prefix before the SubView is the same for
|
| 1324 |
+
+// all tiles of a weight (attributes are uniqued: pointers identify it), so it is kept and copied for each tile.
|
| 1325 |
+
+struct FoldMemo {
|
| 1326 |
+
+ std::mutex mutex;
|
| 1327 |
+
+ struct Entry {
|
| 1328 |
+
+ std::vector<const void*> key;
|
| 1329 |
+
+ std::shared_ptr<std::vector<char>> data;
|
| 1330 |
+
+ vpux::NDTypeInterface type;
|
| 1331 |
+
+ mlir::Type storageElemType;
|
| 1332 |
+
+ bool isSplat;
|
| 1333 |
+
+ };
|
| 1334 |
+
+ std::list<Entry> entries; // most recent first
|
| 1335 |
+
+ size_t bytes = 0;
|
| 1336 |
+
+ static constexpr size_t LIMIT = size_t(512) << 20;
|
| 1337 |
+
+};
|
| 1338 |
+
+
|
| 1339 |
+
+FoldMemo& foldMemo() {
|
| 1340 |
+
+ static FoldMemo memo;
|
| 1341 |
+
+ return memo;
|
| 1342 |
+
+}
|
| 1343 |
+
+} // namespace
|
| 1344 |
+
+
|
| 1345 |
+
Const::Content vpux::Const::ContentAttr::fold() const {
|
| 1346 |
+
auto baseContent = getBaseContent();
|
| 1347 |
+
- auto res = wrapBaseContent(baseContent);
|
| 1348 |
+
- for (const auto& attr : getTransformations()) {
|
| 1349 |
+
- res = attr.transform(res);
|
| 1350 |
+
+ const auto transformations = getTransformations();
|
| 1351 |
+
+ // the last SubView that follows a Concat: the prefix before it is memoized
|
| 1352 |
+
+ int64_t cut = -1;
|
| 1353 |
+
+ bool sawConcat = false;
|
| 1354 |
+
+ for (size_t i = 0; i < transformations.size(); ++i) {
|
| 1355 |
+
+ if (mlir::isa<Const::ConcatAttr>(transformations[i])) {
|
| 1356 |
+
+ sawConcat = true;
|
| 1357 |
+
+ } else if (sawConcat && mlir::isa<Const::SubViewAttr>(transformations[i])) {
|
| 1358 |
+
+ cut = static_cast<int64_t>(i);
|
| 1359 |
+
+ }
|
| 1360 |
+
+ }
|
| 1361 |
+
+ static std::atomic<int64_t> nHit{0}, nMiss{0}, nPlain{0};
|
| 1362 |
+
+ if (cut < 0 || std::getenv("ONW_NO_FOLD_MEMO") != nullptr) {
|
| 1363 |
+
+ ++nPlain;
|
| 1364 |
+
+ auto res = wrapBaseContent(baseContent);
|
| 1365 |
+
+ for (const auto& attr : transformations) {
|
| 1366 |
+
+ res = attr.transform(res);
|
| 1367 |
+
+ }
|
| 1368 |
+
+ return res;
|
| 1369 |
+
+ }
|
| 1370 |
+
+ std::vector<const void*> key;
|
| 1371 |
+
+ key.reserve(cut + 1);
|
| 1372 |
+
+ key.push_back(baseContent.getAsOpaquePointer());
|
| 1373 |
+
+ for (int64_t i = 0; i < cut; ++i) {
|
| 1374 |
+
+ key.push_back(transformations[i].getAsOpaquePointer());
|
| 1375 |
+
+ }
|
| 1376 |
+
+ Const::Content res;
|
| 1377 |
+
+ bool hit = false;
|
| 1378 |
+
+ std::shared_ptr<std::vector<char>> held; // (alive while the SubView reads it, whatever the memo evicts)
|
| 1379 |
+
+ vpux::NDTypeInterface heldType;
|
| 1380 |
+
+ mlir::Type heldStorage;
|
| 1381 |
+
+ bool heldSplat = false;
|
| 1382 |
+
+ {
|
| 1383 |
+
+ auto& memo = foldMemo();
|
| 1384 |
+
+ std::lock_guard<std::mutex> lock(memo.mutex);
|
| 1385 |
+
+ for (auto it = memo.entries.begin(); it != memo.entries.end(); ++it) {
|
| 1386 |
+
+ if (it->key == key) {
|
| 1387 |
+
+ held = it->data;
|
| 1388 |
+
+ heldType = it->type;
|
| 1389 |
+
+ heldStorage = it->storageElemType;
|
| 1390 |
+
+ heldSplat = it->isSplat;
|
| 1391 |
+
+ memo.entries.splice(memo.entries.begin(), memo.entries, it);
|
| 1392 |
+
+ hit = true;
|
| 1393 |
+
+ ++nHit;
|
| 1394 |
+
+ break;
|
| 1395 |
+
+ }
|
| 1396 |
+
+ }
|
| 1397 |
+
+ }
|
| 1398 |
+
+ if (hit) {
|
| 1399 |
+
+ // no copy: the SubView only reads its input and writes a buffer of its own (SubViewAttr::transform)
|
| 1400 |
+
+ auto view = Const::Content::fromRawBuffer(heldType, ArrayRef<char>(held->data(), held->size()), heldStorage,
|
| 1401 |
+
+ heldSplat);
|
| 1402 |
+
+ res = transformations[cut].transform(view);
|
| 1403 |
+
+ for (size_t i = static_cast<size_t>(cut) + 1; i < transformations.size(); ++i) {
|
| 1404 |
+
+ res = transformations[i].transform(res);
|
| 1405 |
+
+ }
|
| 1406 |
+
+ return res;
|
| 1407 |
+
+ }
|
| 1408 |
+
+ if (!hit) {
|
| 1409 |
+
+ ++nMiss;
|
| 1410 |
+
+ res = wrapBaseContent(baseContent);
|
| 1411 |
+
+ for (int64_t i = 0; i < cut; ++i) {
|
| 1412 |
+
+ res = transformations[i].transform(res);
|
| 1413 |
+
+ }
|
| 1414 |
+
+ FoldMemo::Entry e;
|
| 1415 |
+
+ e.key = key;
|
| 1416 |
+
+ e.type = res.getType();
|
| 1417 |
+
+ e.storageElemType = res.getStorageElemType();
|
| 1418 |
+
+ e.isSplat = res.isSplat();
|
| 1419 |
+
+ const auto raw = res.getRawStorageBuf();
|
| 1420 |
+
+ e.data = std::make_shared<std::vector<char>>(raw.begin(), raw.end());
|
| 1421 |
+
+ auto& memo = foldMemo();
|
| 1422 |
+
+ std::lock_guard<std::mutex> lock(memo.mutex);
|
| 1423 |
+
+ memo.bytes += e.data->size();
|
| 1424 |
+
+ memo.entries.push_front(std::move(e));
|
| 1425 |
+
+ while (memo.bytes > FoldMemo::LIMIT && memo.entries.size() > 1) {
|
| 1426 |
+
+ memo.bytes -= memo.entries.back().data->size();
|
| 1427 |
+
+ memo.entries.pop_back();
|
| 1428 |
+
+ }
|
| 1429 |
+
+ }
|
| 1430 |
+
+ for (size_t i = static_cast<size_t>(cut); i < transformations.size(); ++i) {
|
| 1431 |
+
+ res = transformations[i].transform(res);
|
| 1432 |
+
}
|
| 1433 |
+
return res;
|
| 1434 |
+
}
|
| 1435 |
+
diff --git a/src/vpux_compiler/src/utils/dma_transaction_utils.cpp b/src/vpux_compiler/src/utils/dma_transaction_utils.cpp
|
| 1436 |
+
index 10cb1f4..45e02e8 100644
|
| 1437 |
+
--- a/src/vpux_compiler/src/utils/dma_transaction_utils.cpp
|
| 1438 |
+
+++ b/src/vpux_compiler/src/utils/dma_transaction_utils.cpp
|
| 1439 |
+
@@ -181,6 +181,16 @@ DMAPattern reduceDimsForDma(SmallVector<int64_t> memShape, SmallVector<vpux::Mem
|
| 1440 |
+
// Innermost dim is special
|
| 1441 |
+
reducedDims.front() = Bit(reducedDims.front() * elemSize).to<Byte>().count();
|
| 1442 |
+
|
| 1443 |
+
+ if (std::getenv("ONW_DEBUG_UNROLL") != nullptr &&
|
| 1444 |
+
+ std::any_of(std::begin(reducedBitStrides), std::prev(std::end(reducedBitStrides)), [](auto bitStride) {
|
| 1445 |
+
+ return bitStride.count() % CHAR_BIT != 0;
|
| 1446 |
+
+ })) {
|
| 1447 |
+
+ llvm::errs() << "ONW dma reduce: elem " << elemSize << " shape";
|
| 1448 |
+
+ for (auto v : memShape) llvm::errs() << " " << v;
|
| 1449 |
+
+ llvm::errs() << " strides";
|
| 1450 |
+
+ for (auto v : memStrides) llvm::errs() << " " << v.count();
|
| 1451 |
+
+ llvm::errs() << char(10);
|
| 1452 |
+
+ }
|
| 1453 |
+
VPUX_THROW_WHEN(std::any_of(std::begin(reducedBitStrides), std::prev(std::end(reducedBitStrides)),
|
| 1454 |
+
[](auto bitStride) {
|
| 1455 |
+
return bitStride.count() % CHAR_BIT != 0;
|
onw/npukit/patches/openvino-6db412d-npu_plugin.patch
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
diff --git a/src/plugins/intel_npu/src/compiler_adapter/include/weightless_graph.hpp b/src/plugins/intel_npu/src/compiler_adapter/include/weightless_graph.hpp
|
| 2 |
+
index c5f05a69..e976d53b 100644
|
| 3 |
+
--- a/src/plugins/intel_npu/src/compiler_adapter/include/weightless_graph.hpp
|
| 4 |
+
+++ b/src/plugins/intel_npu/src/compiler_adapter/include/weightless_graph.hpp
|
| 5 |
+
@@ -133,6 +133,9 @@ private:
|
| 6 |
+
* per init compiled model and not per init schedule output for performance reasons.
|
| 7 |
+
*/
|
| 8 |
+
mutable std::vector<std::shared_ptr<ZeroTensor>> _mainInputsAllocatedTensors;
|
| 9 |
+
+ // onw: init outputs found identical to an earlier graph's - released only after the init schedules' graphs and
|
| 10 |
+
+ // command lists are destroyed (freed earlier, the driver later released their reused address range)
|
| 11 |
+
+ mutable std::vector<std::shared_ptr<ZeroTensor>> _onwDeferredRelease;
|
| 12 |
+
/**
|
| 13 |
+
* @brief Tensors pointing towards the buffers found in "_mainInputsAllocatedTensors".
|
| 14 |
+
* @details Each map entry corresponds to one input of the main schedule.
|
| 15 |
+
diff --git a/src/plugins/intel_npu/src/compiler_adapter/src/weightless_graph.cpp b/src/plugins/intel_npu/src/compiler_adapter/src/weightless_graph.cpp
|
| 16 |
+
index b56d95b5..24790c3c 100644
|
| 17 |
+
--- a/src/plugins/intel_npu/src/compiler_adapter/src/weightless_graph.cpp
|
| 18 |
+
+++ b/src/plugins/intel_npu/src/compiler_adapter/src/weightless_graph.cpp
|
| 19 |
+
@@ -5,6 +5,11 @@
|
| 20 |
+
#include "weightless_graph.hpp"
|
| 21 |
+
|
| 22 |
+
#include <cinttypes>
|
| 23 |
+
+#include <unordered_map>
|
| 24 |
+
+#include <optional>
|
| 25 |
+
+#include <cstring>
|
| 26 |
+
+#include <cstdlib>
|
| 27 |
+
+#include <cstdio>
|
| 28 |
+
#include <condition_variable>
|
| 29 |
+
#include <iterator>
|
| 30 |
+
#include <mutex>
|
| 31 |
+
@@ -30,6 +35,102 @@ namespace intel_npu {
|
| 32 |
+
|
| 33 |
+
namespace {
|
| 34 |
+
|
| 35 |
+
+// onw: share identical initialized weights between weightless graphs. Every weightless graph runs its init schedules
|
| 36 |
+
+// into its own device buffer - a fused graph of an 8B model holds ~4.8 GB, so each extra graph variant (more positions,
|
| 37 |
+
+// another block size) of the same weights costs that much again. After an init schedule ran, its output buffers are
|
| 38 |
+
+// looked up by size + a sampled hash and confirmed byte by byte; when every output of the schedule already exists in
|
| 39 |
+
+// a live buffer of an earlier graph, the main schedule is bound to those and the new buffer is released.
|
| 40 |
+
+// ONW_WS_SHARE=0 turns it off; ONW_WS_SHARE_LOG=1 prints what was shared.
|
| 41 |
+
+struct OnwWsEntry {
|
| 42 |
+
+ std::weak_ptr<ZeroTensor> owner;
|
| 43 |
+
+ const unsigned char* data;
|
| 44 |
+
+ size_t size;
|
| 45 |
+
+};
|
| 46 |
+
+std::mutex onwWsMutex;
|
| 47 |
+
+std::unordered_multimap<uint64_t, OnwWsEntry> onwWsRegistry;
|
| 48 |
+
+
|
| 49 |
+
+uint64_t onwSampledHash(const unsigned char* p, size_t n) {
|
| 50 |
+
+ uint64_t h = 1469598103934665603ULL ^ n;
|
| 51 |
+
+ auto mix = [&](const unsigned char* q, size_t len) {
|
| 52 |
+
+ for (size_t i = 0; i < len; ++i) {
|
| 53 |
+
+ h = (h ^ q[i]) * 1099511628211ULL;
|
| 54 |
+
+ }
|
| 55 |
+
+ };
|
| 56 |
+
+ for (size_t off = 0; off < n; off += 4096) {
|
| 57 |
+
+ mix(p + off, std::min<size_t>(64, n - off));
|
| 58 |
+
+ }
|
| 59 |
+
+ if (n > 64) {
|
| 60 |
+
+ mix(p + n - 64, 64);
|
| 61 |
+
+ }
|
| 62 |
+
+ return h;
|
| 63 |
+
+}
|
| 64 |
+
+
|
| 65 |
+
+bool onwWsShareEnabled() {
|
| 66 |
+
+ const char* e = std::getenv("ONW_WS_SHARE");
|
| 67 |
+
+ return e == nullptr || std::string(e) != "0";
|
| 68 |
+
+}
|
| 69 |
+
+
|
| 70 |
+
+// returns the owners to keep alive when every output was found (and rewrites the map to point at them), else registers
|
| 71 |
+
+// this buffer's outputs and returns nothing
|
| 72 |
+
+std::optional<std::vector<std::shared_ptr<ZeroTensor>>> onwWsShare(
|
| 73 |
+
+ const std::shared_ptr<ZeroTensor>& hostTensor,
|
| 74 |
+
+ std::unordered_map<std::string, std::shared_ptr<ov::ITensor>>& tensorsMap) {
|
| 75 |
+
+ if (!onwWsShareEnabled() || hostTensor == nullptr) {
|
| 76 |
+
+ return std::nullopt;
|
| 77 |
+
+ }
|
| 78 |
+
+ std::lock_guard<std::mutex> lock(onwWsMutex);
|
| 79 |
+
+ std::vector<std::pair<std::string, std::pair<std::shared_ptr<ZeroTensor>, const unsigned char*>>> found;
|
| 80 |
+
+ size_t bytes = 0;
|
| 81 |
+
+ for (const auto& [name, t] : tensorsMap) {
|
| 82 |
+
+ const auto* p = static_cast<const unsigned char*>(t->data());
|
| 83 |
+
+ const size_t n = t->get_byte_size();
|
| 84 |
+
+ const uint64_t h = onwSampledHash(p, n);
|
| 85 |
+
+ bool hit = false;
|
| 86 |
+
+ auto range = onwWsRegistry.equal_range(h);
|
| 87 |
+
+ for (auto it = range.first; it != range.second; ++it) {
|
| 88 |
+
+ auto owner = it->second.owner.lock();
|
| 89 |
+
+ if (owner == nullptr || owner == hostTensor || it->second.size != n) {
|
| 90 |
+
+ continue;
|
| 91 |
+
+ }
|
| 92 |
+
+ if (std::memcmp(it->second.data, p, n) == 0) {
|
| 93 |
+
+ found.push_back({name, {owner, it->second.data}});
|
| 94 |
+
+ bytes += n;
|
| 95 |
+
+ hit = true;
|
| 96 |
+
+ break;
|
| 97 |
+
+ }
|
| 98 |
+
+ }
|
| 99 |
+
+ if (!hit) {
|
| 100 |
+
+ break;
|
| 101 |
+
+ }
|
| 102 |
+
+ }
|
| 103 |
+
+ const bool log = std::getenv("ONW_WS_SHARE_LOG") != nullptr;
|
| 104 |
+
+ if (found.size() != tensorsMap.size()) {
|
| 105 |
+
+ for (const auto& [name, t] : tensorsMap) {
|
| 106 |
+
+ const auto* p = static_cast<const unsigned char*>(t->data());
|
| 107 |
+
+ const size_t n = t->get_byte_size();
|
| 108 |
+
+ onwWsRegistry.emplace(onwSampledHash(p, n), OnwWsEntry{hostTensor, p, n});
|
| 109 |
+
+ }
|
| 110 |
+
+ if (log) {
|
| 111 |
+
+ std::fprintf(stderr, "[onw ws-share] new weights: %zu outputs, %.1f MB%c", tensorsMap.size(),
|
| 112 |
+
+ hostTensor->get_byte_size() / 1e6, 10);
|
| 113 |
+
+ }
|
| 114 |
+
+ return std::nullopt;
|
| 115 |
+
+ }
|
| 116 |
+
+ std::vector<std::shared_ptr<ZeroTensor>> owners;
|
| 117 |
+
+ for (auto& [name, src] : found) {
|
| 118 |
+
+ const auto& old = tensorsMap.at(name);
|
| 119 |
+
+ tensorsMap[name] = ov::make_tensor(old->get_element_type(), old->get_shape(),
|
| 120 |
+
+ const_cast<unsigned char*>(src.second));
|
| 121 |
+
+ owners.push_back(src.first);
|
| 122 |
+
+ }
|
| 123 |
+
+ if (log) {
|
| 124 |
+
+ std::fprintf(stderr, "[onw ws-share] shared: %zu outputs, %.1f MB released%c", found.size(),
|
| 125 |
+
+ hostTensor->get_byte_size() / 1e6, 10);
|
| 126 |
+
+ }
|
| 127 |
+
+ return owners;
|
| 128 |
+
+}
|
| 129 |
+
+
|
| 130 |
+
+
|
| 131 |
+
constexpr uint8_t MAIN_SCHEDULE_INDEX = 0;
|
| 132 |
+
constexpr std::string_view WEIGHTS_IR_EXTENSION = ".bin";
|
| 133 |
+
constexpr std::string_view WEIGHTS_ONNX_EXTENSION = ".data";
|
| 134 |
+
@@ -423,6 +524,7 @@ void WeightlessGraph::initialize_impl(const FilteredConfig& config) {
|
| 135 |
+
_initsFences.clear();
|
| 136 |
+
_initsMetadata.clear();
|
| 137 |
+
_initsCommandQueue.reset();
|
| 138 |
+
+ _onwDeferredRelease.clear();
|
| 139 |
+
|
| 140 |
+
// The main schedule is initialized after the weights initialization ones in order to save some memory
|
| 141 |
+
Graph::initialize_impl(config);
|
| 142 |
+
@@ -502,7 +604,13 @@ WeightlessGraph::InputData WeightlessGraph::allocate_inputs(
|
| 143 |
+
// model appears in exactly one schedule. Thus, one can delete
|
| 144 |
+
// the handle to the constant memory early.
|
| 145 |
+
if (auto it = constants.find(id); it != constants.end()) {
|
| 146 |
+
- ov::wsh::Extension::hint_evict(*it->second);
|
| 147 |
+
+ // onw: several graphs (paged parts, fused graphs, every block size) are built from the same mapped weight
|
| 148 |
+
+ // files and imported one after another; evicting a constant's source here left later imports reading
|
| 149 |
+
+ // released memory (access violation in this function). The mapping is file-backed, so the OS reclaims it
|
| 150 |
+
+ // anyway. ONW_WS_EVICT=1 restores the eviction.
|
| 151 |
+
+ if (std::getenv("ONW_WS_EVICT") != nullptr) {
|
| 152 |
+
+ ov::wsh::Extension::hint_evict(*it->second);
|
| 153 |
+
+ }
|
| 154 |
+
constants.erase(it);
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
@@ -557,6 +665,14 @@ void WeightlessGraph::run_init_single_threaded() {
|
| 158 |
+
create_pipeline(initIndex, initInputsViewTensors, initOutputsViewTensors);
|
| 159 |
+
run_pipeline(initIndex);
|
| 160 |
+
|
| 161 |
+
+ if (auto owners = onwWsShare(initOutputsAllocatedTensor, initOutputsViewTensorsMap)) {
|
| 162 |
+
+ merge_two_maps(_mainInputsViewTensors, initOutputsViewTensorsMap);
|
| 163 |
+
+ for (auto& o : *owners) {
|
| 164 |
+
+ _mainInputsAllocatedTensors.push_back(o);
|
| 165 |
+
+ }
|
| 166 |
+
+ _onwDeferredRelease.push_back(std::move(initOutputsAllocatedTensor));
|
| 167 |
+
+ continue;
|
| 168 |
+
+ }
|
| 169 |
+
merge_two_maps(_mainInputsViewTensors, initOutputsViewTensorsMap);
|
| 170 |
+
_mainInputsAllocatedTensors.push_back(std::move(initOutputsAllocatedTensor));
|
| 171 |
+
}
|
| 172 |
+
@@ -600,6 +716,14 @@ void WeightlessGraph::run_init_multi_threaded() {
|
| 173 |
+
run_pipeline(data.initIndex);
|
| 174 |
+
|
| 175 |
+
// TODO: pre-allocate those well in advance? (outside of this loop)
|
| 176 |
+
+ if (auto owners = onwWsShare(data.outputs.hostTensor, data.outputs.tensorsMap)) {
|
| 177 |
+
+ merge_two_maps(_mainInputsViewTensors, data.outputs.tensorsMap);
|
| 178 |
+
+ for (auto& o : *owners) {
|
| 179 |
+
+ _mainInputsAllocatedTensors.push_back(o);
|
| 180 |
+
+ }
|
| 181 |
+
+ _onwDeferredRelease.push_back(std::move(data.outputs.hostTensor));
|
| 182 |
+
+ return;
|
| 183 |
+
+ }
|
| 184 |
+
merge_two_maps(_mainInputsViewTensors, data.outputs.tensorsMap);
|
| 185 |
+
_mainInputsAllocatedTensors.push_back(data.outputs.hostTensor);
|
| 186 |
+
},
|
onw/npukit/patches/vpucostmodel.patch
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
diff --git a/include/core/cache.h b/include/core/cache.h
|
| 2 |
+
index 0340f76..20dca16 100644
|
| 3 |
+
--- a/include/core/cache.h
|
| 4 |
+
+++ b/include/core/cache.h
|
| 5 |
+
@@ -215,6 +215,12 @@ public:
|
| 6 |
+
if (map_it == m_table.cend()) {
|
| 7 |
+
return std::nullopt;
|
| 8 |
+
}
|
| 9 |
+
+ // onw: below the size limit nothing is ever evicted, so the recency order does not matter - return
|
| 10 |
+
+ // under the shared lock instead of taking the exclusive one (every hit serialised all compile threads)
|
| 11 |
+
+ if (m_table.size() < max_size) {
|
| 12 |
+
+ if (source) *source = "dyn_cache";
|
| 13 |
+
+ return (map_it->second->second);
|
| 14 |
+
+ }
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// If found, acquire a unique lock and do the mutation
|
onw/npukit/plugins.xml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<ie>
|
| 2 |
+
<plugins>
|
| 3 |
+
<plugin name="NPU" location="openvino_intel_npu_plugin.dll">
|
| 4 |
+
</plugin>
|
| 5 |
+
</plugins>
|
| 6 |
+
</ie>
|
onw/precompile.py
CHANGED
|
@@ -1,19 +1,20 @@
|
|
| 1 |
-
"""A first load's precompile child (runtime.compile_jobs): build the segments named in ONW_PRECOMPILE_ONLY exactly as
|
| 2 |
-
the parent would, so they land in the compile cache (marked as the parent's run, ONW_NPUW_SESSION), then exit.
|
| 3 |
-
usage (internal): python -m onw.precompile '[MODEL_DIR, DEVICE, CONFIG, SIZES, CONTEXT]'"""
|
| 4 |
-
import json, sys
|
| 5 |
-
import openvino as ov
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
| 1 |
+
"""A first load's precompile child (runtime.compile_jobs): build the segments named in ONW_PRECOMPILE_ONLY exactly as
|
| 2 |
+
the parent would, so they land in the compile cache (marked as the parent's run, ONW_NPUW_SESSION), then exit.
|
| 3 |
+
usage (internal): python -m onw.precompile '[MODEL_DIR, DEVICE, CONFIG, SIZES, CONTEXT]'"""
|
| 4 |
+
import json, sys
|
| 5 |
+
import openvino as ov
|
| 6 |
+
from . import npukit
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main():
|
| 10 |
+
d, device, config, sizes, context = json.loads(sys.argv[1])
|
| 11 |
+
from .runtime import Precompiled, SegmentedModel
|
| 12 |
+
try:
|
| 13 |
+
SegmentedModel(npukit.core(), d, device, config, sizes=sizes, context=context)
|
| 14 |
+
except Precompiled:
|
| 15 |
+
return
|
| 16 |
+
sys.exit(3) # (not reached: the model raises once compiled)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
if __name__ == "__main__":
|
| 20 |
+
main()
|
onw/probe.py
CHANGED
|
@@ -1,59 +1,60 @@
|
|
| 1 |
-
"""Compile one segment in a separate process, so a crash inside the NPU compiler (a driver bug: the process dies
|
| 2 |
-
with no Python exception) does not take the server down. With EXPORT_BLOB the compiled blob is written there for the
|
| 3 |
-
parent to import (onw's own blob store, see runtime.import_blob); otherwise the result lands in the OpenVINO cache dir
|
| 4 |
-
given in the config and the parent's compile of the same segment is a cache hit.
|
| 5 |
-
usage (internal): python -m onw.probe MODEL_DIR SEG_XML CONFIG_JSON NPUW(0/1) T [EXPORT_BLOB]
|
| 6 |
-
ONW_RING (env): the sliding-window ring length the parent reshaped the caches to (paged mode).
|
| 7 |
-
exit 0: compiled; 1: the compiler rejected it (reason on stdout); 2: something else went wrong (disk full, a file
|
| 8 |
-
missing - not the compiler's verdict, so the parent does not remember it); anything else: crashed"""
|
| 9 |
-
import io, json, os, sys
|
| 10 |
-
import openvino as ov
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
| 1 |
+
"""Compile one segment in a separate process, so a crash inside the NPU compiler (a driver bug: the process dies
|
| 2 |
+
with no Python exception) does not take the server down. With EXPORT_BLOB the compiled blob is written there for the
|
| 3 |
+
parent to import (onw's own blob store, see runtime.import_blob); otherwise the result lands in the OpenVINO cache dir
|
| 4 |
+
given in the config and the parent's compile of the same segment is a cache hit.
|
| 5 |
+
usage (internal): python -m onw.probe MODEL_DIR SEG_XML CONFIG_JSON NPUW(0/1) T [EXPORT_BLOB]
|
| 6 |
+
ONW_RING (env): the sliding-window ring length the parent reshaped the caches to (paged mode).
|
| 7 |
+
exit 0: compiled; 1: the compiler rejected it (reason on stdout); 2: something else went wrong (disk full, a file
|
| 8 |
+
missing - not the compiler's verdict, so the parent does not remember it); anything else: crashed"""
|
| 9 |
+
import io, json, os, sys
|
| 10 |
+
import openvino as ov
|
| 11 |
+
from . import npukit
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
d, name, cfg, npuw, T = sys.argv[1], sys.argv[2], json.loads(sys.argv[3]), sys.argv[4] == "1", int(sys.argv[5])
|
| 16 |
+
from .runtime import read_segment
|
| 17 |
+
core = npukit.core()
|
| 18 |
+
from .runtime import enable_compiler
|
| 19 |
+
enable_compiler(core, "NPU", {}, quiet=True) # [2.0.3] the same NPU compiler as the parent (see npu_compiler)
|
| 20 |
+
meta = json.load(open(f"{d}/engine.json"))
|
| 21 |
+
bins = {} # keeps the .bin buffer alive while compiling
|
| 22 |
+
try:
|
| 23 |
+
segs = {**meta["segments"], **(meta.get("mtp") or {}).get("segments", {})} # (the MTP graphs too)
|
| 24 |
+
model = read_segment(core, d, name, segs[name], npuw, bins, T, meta["T"])
|
| 25 |
+
if os.environ.get("ONW_KV", "paged") != "legacy": # the same graph the parent compiles (host-side rope)
|
| 26 |
+
from . import paged
|
| 27 |
+
paged.host_rope(model)
|
| 28 |
+
if os.environ.get("ONW_RING"): # paged mode: sliding-window caches live in a ring
|
| 29 |
+
from .runtime import ring_shapes
|
| 30 |
+
model.reshape(ring_shapes(model, int(os.environ["ONW_RING"]), segs[name]["S"]))
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print(f"{type(e).__name__}: {e}", flush=True)
|
| 33 |
+
sys.exit(2)
|
| 34 |
+
if os.environ.get("ONW_TEST_CRASH_KEY") in cfg: # test hook: behave like a compiler crash on this route
|
| 35 |
+
os.abort()
|
| 36 |
+
try:
|
| 37 |
+
compiled = core.compile_model(model, "NPU", cfg)
|
| 38 |
+
except MemoryError as e: # (out of memory: not the compiler's verdict)
|
| 39 |
+
print(f"MemoryError: {e}", flush=True)
|
| 40 |
+
sys.exit(2)
|
| 41 |
+
except RuntimeError as e:
|
| 42 |
+
why = str(e)
|
| 43 |
+
print(why[why.find("While validating"):] if "While validating" in why else why[-300:], flush=True)
|
| 44 |
+
sys.exit(1)
|
| 45 |
+
if len(sys.argv) > 6: # write the blob for the parent to import
|
| 46 |
+
out = sys.argv[6]
|
| 47 |
+
try:
|
| 48 |
+
os.makedirs(os.path.dirname(out), exist_ok=True)
|
| 49 |
+
buf = io.BytesIO()
|
| 50 |
+
compiled.export_model(buf)
|
| 51 |
+
with open(out + ".tmp", "wb") as f:
|
| 52 |
+
f.write(buf.getbuffer())
|
| 53 |
+
os.replace(out + ".tmp", out)
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"{type(e).__name__}: {e}", flush=True)
|
| 56 |
+
sys.exit(2)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
main()
|
onw/runtime.py
CHANGED
|
@@ -692,6 +692,46 @@ def import_blob(core, path, device, cfg):
|
|
| 692 |
return core.import_model(io.BytesIO(f.read()), device, cfg)
|
| 693 |
|
| 694 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
def _signature(model):
|
| 696 |
"""A cheap fingerprint of a graph's structure (op types, shapes, input / output names): a part's compiled blob is
|
| 697 |
only reused for exactly the same part, whatever changes in how segments are split."""
|
|
@@ -1041,6 +1081,18 @@ class SegmentedModel:
|
|
| 1041 |
# MoE models already hold their experts once (weights as inputs), so NPUW is off for them unless ONW_NPUW_MOE=1:
|
| 1042 |
# it stopped mid-answer on LFM2 (NPU 4000), but that was with a Linux driver < 1.38 (see the conv fix there)
|
| 1043 |
npuw = device == "NPU" and use_npuw(core) and (not self.bank or os.environ.get("ONW_NPUW_MOE") == "1")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1044 |
if npuw and self.meta.get("needs") == "u2" and os.environ.get("ONW_NPUW") != "1":
|
| 1045 |
# 2-bit weights stay 2-bit compiled plain only: NPUW expands them (Lunar Lake, Ternary Bonsai 2 27B: 12.6 GB
|
| 1046 |
# of NPU memory instead of ~7, the 16 GB machine out of memory, 0.04 tok/s against 4 tok/s plain)
|
|
@@ -1052,6 +1104,8 @@ class SegmentedModel:
|
|
| 1052 |
print(f"[npuw] off: ~{parts} compiled parts (each attention layer cuts a segment) - more than the NPU driver "
|
| 1053 |
f"holds with NPUW (~30); ONW_NPUW=1 forces it", flush=True)
|
| 1054 |
npuw = False
|
|
|
|
|
|
|
| 1055 |
if npuw:
|
| 1056 |
# experimental: NPUW pulls the weights out of every compiled graph into one weights bank; constants match
|
| 1057 |
# by data pointer, so every block size reads its IR against the same in-memory copy of the .bin
|
|
@@ -1340,6 +1394,12 @@ class SegmentedModel:
|
|
| 1340 |
# ---- [2.0.4] fused graphs (onw/fused.py): one NPU call per segment, for answers and for prompts ----
|
| 1341 |
FUSED_B = (1, 2, 3) # the variants: the first 1 / 2 / 3 paged blocks (1K / 2K / 4K)
|
| 1342 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1343 |
def fused_prepare(self, need, S=None):
|
| 1344 |
"""Make the fused S-token graphs (default: the answer size) covering `need` positions the ones in use for that
|
| 1345 |
size (loaded from onw's blob store, or compiled the first time); the other variants are compiled in the
|
|
@@ -1352,9 +1412,38 @@ class SegmentedModel:
|
|
| 1352 |
if S not in sizes:
|
| 1353 |
return None
|
| 1354 |
from . import fused as _f
|
| 1355 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1356 |
want = next((B for B, c in caps if c >= min(need, self.T)), None)
|
| 1357 |
F = self._fused.get(S)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1358 |
if want is None: # longer than the largest variant: the paged graphs
|
| 1359 |
if F is not None:
|
| 1360 |
F = None # [2.1.0] (no reference left: freed before the paged graphs load)
|
|
@@ -1387,6 +1476,10 @@ class SegmentedModel:
|
|
| 1387 |
self.swap_to(S)
|
| 1388 |
return None
|
| 1389 |
self.S = max([*self.segs, *self._fused]) # (prompts are still fed in blocks of the largest size)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1390 |
npuw_ = (self._ctx["config"] or {}).get("NPU_USE_NPUW") == "YES" # (NPUW: compiled when needed, no background)
|
| 1391 |
if os.environ.get("ONW_FUSED_BG", "1") != "0" and not self._fused_bg and not npuw_:
|
| 1392 |
# every variant of every fused size, in one background thread, the short ones first (compiled once; later
|
|
@@ -1396,7 +1489,6 @@ class SegmentedModel:
|
|
| 1396 |
jobs = [(x, B) for B, _ in caps for x in self._fused_sizes]
|
| 1397 |
self._fused_thread = threading.Thread(target=self._fused_precompile, args=(jobs,), daemon=True)
|
| 1398 |
self._fused_thread.start()
|
| 1399 |
-
return self._fused[S]
|
| 1400 |
|
| 1401 |
def _fused_precompile(self, jobs):
|
| 1402 |
from . import fused as _f
|
|
@@ -1842,10 +1934,33 @@ class SegmentedModel:
|
|
| 1842 |
os.remove(bp)
|
| 1843 |
except OSError:
|
| 1844 |
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1845 |
if own and os.path.exists(bp):
|
| 1846 |
try:
|
| 1847 |
req = import_blob(core, bp, device, nocache).create_infer_request()
|
| 1848 |
USED_BLOBS.add(bp)
|
|
|
|
|
|
|
| 1849 |
tried.set(gname, how)
|
| 1850 |
return req
|
| 1851 |
except (RuntimeError, OSError) as e: # (OSError: removed meanwhile by another process)
|
|
@@ -1858,6 +1973,47 @@ class SegmentedModel:
|
|
| 1858 |
except OSError:
|
| 1859 |
pass
|
| 1860 |
fresh = how != known and device == "NPU"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1861 |
try:
|
| 1862 |
if fresh:
|
| 1863 |
tried.trying(gname, how)
|
|
|
|
| 692 |
return core.import_model(io.BytesIO(f.read()), device, cfg)
|
| 693 |
|
| 694 |
|
| 695 |
+
# [3.0] Weightless blobs: the compiled blob keeps no weights (CACHE_MODE=OPTIMIZE_SIZE + ENABLE_WEIGHTLESS); importing
|
| 696 |
+
# it with the same ov.Model (MODEL_PTR) runs the blob's init schedules, which rebuild the weights on the NPU from the
|
| 697 |
+
# model's constants (the mapped .bin files). The disk cache then holds the graphs only, and onw's own NPU plugin shares
|
| 698 |
+
# identical rebuilt weights between graphs (paged parts, fused graphs, every block size: the model's weights once in
|
| 699 |
+
# memory) - with one init output per weight (ONW_WS_NO_CONCAT, read by onw's own compiler). Needs a compiler that
|
| 700 |
+
# supports it for the weight format (onw's own build; pip OpenVINO 2026.5: not NF4; NPU 3720: no): after the first
|
| 701 |
+
# failure in a process, full blobs as before. ONW_WEIGHTLESS=0 turns it off.
|
| 702 |
+
WEIGHTLESS_CFG = {"CACHE_MODE": "OPTIMIZE_SIZE", "ENABLE_WEIGHTLESS": True, "NPU_COMPILER_TYPE": "PLUGIN"}
|
| 703 |
+
_WEIGHTLESS = {"ok": None}
|
| 704 |
+
os.environ.setdefault("ONW_WS_NO_CONCAT", "1")
|
| 705 |
+
|
| 706 |
+
|
| 707 |
+
def weightless_wanted():
|
| 708 |
+
from . import npukit # (default: with onw's NPU kit only - OpenVINO's compiler fails NF4)
|
| 709 |
+
return (os.environ.get("ONW_WEIGHTLESS", "1" if npukit.active() else "0") != "0" and _WEIGHTLESS["ok"] is not False
|
| 710 |
+
and not _WEIGHTLESS.get("skip_model"))
|
| 711 |
+
|
| 712 |
+
|
| 713 |
+
def int4_weights(d, meta):
|
| 714 |
+
"""[3.0] The model's weights are INT4 (i4 / u4 constants, no NF4) - read from its first segment's IR."""
|
| 715 |
+
names = sorted(n for n in meta.get("segments", {}) if n.endswith(".xml"))
|
| 716 |
+
if not names:
|
| 717 |
+
return False
|
| 718 |
+
try:
|
| 719 |
+
with open(os.path.join(d, names[0]), encoding="utf8", errors="replace") as f:
|
| 720 |
+
xml = f.read()
|
| 721 |
+
except OSError:
|
| 722 |
+
return False
|
| 723 |
+
return ('element_type="nf4"' not in xml and
|
| 724 |
+
('element_type="i4"' in xml or 'element_type="u4"' in xml))
|
| 725 |
+
|
| 726 |
+
|
| 727 |
+
def weightless_failed(e, log=print):
|
| 728 |
+
if _WEIGHTLESS["ok"] is not False:
|
| 729 |
+
log(f"[compile] weightless blobs not supported here ({' '.join(str(e).split())[-140:]}): full blobs",
|
| 730 |
+
flush=True)
|
| 731 |
+
_WEIGHTLESS["ok"] = False
|
| 732 |
+
_WEIGHTLESS["full"] = True
|
| 733 |
+
|
| 734 |
+
|
| 735 |
def _signature(model):
|
| 736 |
"""A cheap fingerprint of a graph's structure (op types, shapes, input / output names): a part's compiled blob is
|
| 737 |
only reused for exactly the same part, whatever changes in how segments are split."""
|
|
|
|
| 1081 |
# MoE models already hold their experts once (weights as inputs), so NPUW is off for them unless ONW_NPUW_MOE=1:
|
| 1082 |
# it stopped mid-answer on LFM2 (NPU 4000), but that was with a Linux driver < 1.38 (see the conv fix there)
|
| 1083 |
npuw = device == "NPU" and use_npuw(core) and (not self.bank or os.environ.get("ONW_NPUW_MOE") == "1")
|
| 1084 |
+
# [3.0] 2-bit weights stay inside the blobs: the weights separation leaves them there anyway (Bonsai 2 9B part:
|
| 1085 |
+
# 332.6 -> 331.8 MB) and compiles 7x slower (19 -> 142 s)
|
| 1086 |
+
# INT4 weights of the plain paged models as well: never separated by the compiler (Granite 4.2 3B INT4 part:
|
| 1087 |
+
# 63.5 MB weightless against 63.6 MB, multi-row parts 20-35x slower to compile) - INT8 and NF4 are (llm-jp INT8
|
| 1088 |
+
# part: 2.5 against 216 MB). (NPUW and MoE models as they were checked: gemma-4, Qwen3.6)
|
| 1089 |
+
_WEIGHTLESS["skip_model"] = self.meta.get("needs") == "u2"
|
| 1090 |
+
_WEIGHTLESS["full"] = False # (this model: a part with its weights inside, see fused.wanted)
|
| 1091 |
+
if device == "NPU" and not _WEIGHTLESS.get("told"):
|
| 1092 |
+
from . import npukit
|
| 1093 |
+
_WEIGHTLESS["told"] = True
|
| 1094 |
+
print(f"[npukit] {'onw NPU kit ' + npukit.why() if npukit.active() else 'OpenVINO NPU plugin (onw kit: ' + npukit.why() + ')'}",
|
| 1095 |
+
flush=True)
|
| 1096 |
if npuw and self.meta.get("needs") == "u2" and os.environ.get("ONW_NPUW") != "1":
|
| 1097 |
# 2-bit weights stay 2-bit compiled plain only: NPUW expands them (Lunar Lake, Ternary Bonsai 2 27B: 12.6 GB
|
| 1098 |
# of NPU memory instead of ~7, the 16 GB machine out of memory, 0.04 tok/s against 4 tok/s plain)
|
|
|
|
| 1104 |
print(f"[npuw] off: ~{parts} compiled parts (each attention layer cuts a segment) - more than the NPU driver "
|
| 1105 |
f"holds with NPUW (~30); ONW_NPUW=1 forces it", flush=True)
|
| 1106 |
npuw = False
|
| 1107 |
+
if not npuw and not self.bank and int4_weights(d, self.meta):
|
| 1108 |
+
_WEIGHTLESS["skip_model"] = True # (decided here: NPUW may have been turned off just above)
|
| 1109 |
if npuw:
|
| 1110 |
# experimental: NPUW pulls the weights out of every compiled graph into one weights bank; constants match
|
| 1111 |
# by data pointer, so every block size reads its IR against the same in-memory copy of the .bin
|
|
|
|
| 1394 |
# ---- [2.0.4] fused graphs (onw/fused.py): one NPU call per segment, for answers and for prompts ----
|
| 1395 |
FUSED_B = (1, 2, 3) # the variants: the first 1 / 2 / 3 paged blocks (1K / 2K / 4K)
|
| 1396 |
|
| 1397 |
+
def fused_keep_all(self):
|
| 1398 |
+
"""[3.0] every fused variant stays loaded (onw's NPU kit, NPU 4000+, not the memory-saving mode)."""
|
| 1399 |
+
from . import npukit
|
| 1400 |
+
return os.environ.get("ONW_FUSED_KEEP", "1" if npukit.active() and npu_arch(self._ctx["core"]) >= 4000
|
| 1401 |
+
and not mem_saver() else "0") == "1"
|
| 1402 |
+
|
| 1403 |
def fused_prepare(self, need, S=None):
|
| 1404 |
"""Make the fused S-token graphs (default: the answer size) covering `need` positions the ones in use for that
|
| 1405 |
size (loaded from onw's blob store, or compiled the first time); the other variants are compiled in the
|
|
|
|
| 1412 |
if S not in sizes:
|
| 1413 |
return None
|
| 1414 |
from . import fused as _f
|
| 1415 |
+
keep_all = self.fused_keep_all()
|
| 1416 |
+
# [3.0] with every variant kept (shared weights) the variants go on up to the context: 8K / 16K / 32K positions
|
| 1417 |
+
# (the first 4 / 5 / 6 paged blocks) - past 4K the paged graphs answered at about half the speed
|
| 1418 |
+
nb = len(_f.blocks_for(self.kv.capacity, 99))
|
| 1419 |
+
bs = tuple(range(1, min(nb, int(os.environ.get("ONW_FUSED_BLOCKS", "6"))) + 1)) if keep_all else self.FUSED_B
|
| 1420 |
+
caps = [(B, sum(_f.blocks_for(self.kv.capacity, B))) for B in bs]
|
| 1421 |
want = next((B for B, c in caps if c >= min(need, self.T)), None)
|
| 1422 |
F = self._fused.get(S)
|
| 1423 |
+
if keep_all:
|
| 1424 |
+
# [3.0] every variant stays loaded next to the paged graphs (with onw's own NPU plugin they share one copy
|
| 1425 |
+
# of the weights): no graph is dropped or reloaded while chatting - no pause at 1K / 2K / 4K positions, and
|
| 1426 |
+
# the same graphs for the same positions whatever came before
|
| 1427 |
+
keep = self.__dict__.setdefault("_fused_keep", {})
|
| 1428 |
+
if want is None:
|
| 1429 |
+
self._fused.pop(S, None)
|
| 1430 |
+
if S not in self.segs:
|
| 1431 |
+
self.swap_to(S)
|
| 1432 |
+
self.S = max([*self.segs, *self._fused])
|
| 1433 |
+
return None
|
| 1434 |
+
F = next((keep[(S, B)] for B in bs if B >= want and (S, B) in keep), None)
|
| 1435 |
+
if F is None:
|
| 1436 |
+
with self._mutex:
|
| 1437 |
+
try:
|
| 1438 |
+
F = keep[(S, want)] = _f.FusedDecode(self, S, want)
|
| 1439 |
+
except Exception as e:
|
| 1440 |
+
print(f"[fused] {S}-token graphs off: {' '.join(str(e).split())[-400:]}", flush=True)
|
| 1441 |
+
self._fused_sizes = [x for x in sizes if x != S]
|
| 1442 |
+
return None
|
| 1443 |
+
self._fused[S] = F
|
| 1444 |
+
self.S = max([*self.segs, *self._fused])
|
| 1445 |
+
self._fused_background(caps)
|
| 1446 |
+
return F
|
| 1447 |
if want is None: # longer than the largest variant: the paged graphs
|
| 1448 |
if F is not None:
|
| 1449 |
F = None # [2.1.0] (no reference left: freed before the paged graphs load)
|
|
|
|
| 1476 |
self.swap_to(S)
|
| 1477 |
return None
|
| 1478 |
self.S = max([*self.segs, *self._fused]) # (prompts are still fed in blocks of the largest size)
|
| 1479 |
+
self._fused_background(caps)
|
| 1480 |
+
return self._fused[S]
|
| 1481 |
+
|
| 1482 |
+
def _fused_background(self, caps):
|
| 1483 |
npuw_ = (self._ctx["config"] or {}).get("NPU_USE_NPUW") == "YES" # (NPUW: compiled when needed, no background)
|
| 1484 |
if os.environ.get("ONW_FUSED_BG", "1") != "0" and not self._fused_bg and not npuw_:
|
| 1485 |
# every variant of every fused size, in one background thread, the short ones first (compiled once; later
|
|
|
|
| 1489 |
jobs = [(x, B) for B, _ in caps for x in self._fused_sizes]
|
| 1490 |
self._fused_thread = threading.Thread(target=self._fused_precompile, args=(jobs,), daemon=True)
|
| 1491 |
self._fused_thread.start()
|
|
|
|
| 1492 |
|
| 1493 |
def _fused_precompile(self, jobs):
|
| 1494 |
from . import fused as _f
|
|
|
|
| 1934 |
os.remove(bp)
|
| 1935 |
except OSError:
|
| 1936 |
pass
|
| 1937 |
+
bpw = blob_path(cache, gname, src + [key, "weightless"]) if own and device == "NPU" else None
|
| 1938 |
+
if bpw and os.path.exists(bpw) and stale(binf, bpw):
|
| 1939 |
+
try:
|
| 1940 |
+
os.remove(bpw)
|
| 1941 |
+
except OSError:
|
| 1942 |
+
pass
|
| 1943 |
+
if bpw and os.path.exists(bpw) and weightless_wanted():
|
| 1944 |
+
try:
|
| 1945 |
+
req = import_blob(core, bpw, device, {**nocache, "MODEL_PTR": model}).create_infer_request()
|
| 1946 |
+
USED_BLOBS.add(bpw)
|
| 1947 |
+
tried.set(gname, how)
|
| 1948 |
+
return req
|
| 1949 |
+
except (RuntimeError, OSError) as e:
|
| 1950 |
+
if _transient(str(e)):
|
| 1951 |
+
raise RuntimeError(TRANSIENT + str(e)) from e
|
| 1952 |
+
print(f"[compile] {gname}: stored weightless blob unusable ({' '.join(str(e).split())[-120:]})",
|
| 1953 |
+
flush=True)
|
| 1954 |
+
try:
|
| 1955 |
+
os.remove(bpw)
|
| 1956 |
+
except OSError:
|
| 1957 |
+
pass
|
| 1958 |
if own and os.path.exists(bp):
|
| 1959 |
try:
|
| 1960 |
req = import_blob(core, bp, device, nocache).create_infer_request()
|
| 1961 |
USED_BLOBS.add(bp)
|
| 1962 |
+
if bpw and weightless_wanted():
|
| 1963 |
+
_WEIGHTLESS["full"] = True # (compiled with its weights: weightless failed for it before)
|
| 1964 |
tried.set(gname, how)
|
| 1965 |
return req
|
| 1966 |
except (RuntimeError, OSError) as e: # (OSError: removed meanwhile by another process)
|
|
|
|
| 1973 |
except OSError:
|
| 1974 |
pass
|
| 1975 |
fresh = how != known and device == "NPU"
|
| 1976 |
+
if bpw and weightless_wanted():
|
| 1977 |
+
try:
|
| 1978 |
+
if fresh:
|
| 1979 |
+
tried.trying(gname, how)
|
| 1980 |
+
if part is None: # (whole segments: out of process, as below)
|
| 1981 |
+
r = _probe(d, name, {**nocache, **WEIGHTLESS_CFG}, npuw, T, export=bpw, ring=ring)
|
| 1982 |
+
if r is not True:
|
| 1983 |
+
raise RuntimeError(r if isinstance(r, str) else "rejected")
|
| 1984 |
+
else:
|
| 1985 |
+
cm = core.compile_model(model, device, {**nocache, **WEIGHTLESS_CFG})
|
| 1986 |
+
import io
|
| 1987 |
+
buf = io.BytesIO()
|
| 1988 |
+
cm.export_model(buf)
|
| 1989 |
+
del cm
|
| 1990 |
+
os.makedirs(os.path.dirname(bpw), exist_ok=True)
|
| 1991 |
+
with open(f"{bpw}.{os.getpid()}.tmp", "wb") as f:
|
| 1992 |
+
f.write(buf.getbuffer())
|
| 1993 |
+
del buf
|
| 1994 |
+
os.replace(f"{bpw}.{os.getpid()}.tmp", bpw)
|
| 1995 |
+
req = import_blob(core, bpw, device, {**nocache, "MODEL_PTR": model}).create_infer_request()
|
| 1996 |
+
USED_BLOBS.add(bpw)
|
| 1997 |
+
_WEIGHTLESS["ok"] = True
|
| 1998 |
+
if fresh:
|
| 1999 |
+
print(f"[compile] {gname}: {how} (weightless)", flush=True)
|
| 2000 |
+
if provisional:
|
| 2001 |
+
tried.untry(gname, how, known)
|
| 2002 |
+
else:
|
| 2003 |
+
tried.set(gname, how)
|
| 2004 |
+
tried.save()
|
| 2005 |
+
return req
|
| 2006 |
+
except RuntimeError as e:
|
| 2007 |
+
if _transient(str(e)):
|
| 2008 |
+
tried.untry(gname, how, known)
|
| 2009 |
+
raise RuntimeError(TRANSIENT + str(e)) from e
|
| 2010 |
+
weightless_failed(e)
|
| 2011 |
+
if fresh:
|
| 2012 |
+
tried.untry(gname, how, known)
|
| 2013 |
+
try:
|
| 2014 |
+
os.remove(bpw)
|
| 2015 |
+
except OSError:
|
| 2016 |
+
pass
|
| 2017 |
try:
|
| 2018 |
if fresh:
|
| 2019 |
tried.trying(gname, how)
|
onw/server.py
CHANGED
|
@@ -1,748 +1,749 @@
|
|
| 1 |
-
"""OpenAI-compatible API server for an onw model on the Intel NPU (point other apps at http://HOST:PORT/v1).
|
| 2 |
-
|
| 3 |
-
GET / what this server is: model, OpenAI base URL, endpoints (JSON)
|
| 4 |
-
GET /v1/models
|
| 5 |
-
GET /v1/models/{id}
|
| 6 |
-
POST /v1/chat/completions (stream true/false; text and image_url parts: data: URLs or http(s)); responses
|
| 7 |
-
also carry "onw_stats" (prefill / decode speed, reused tokens) - extra field
|
| 8 |
-
GET /health 200 when the model is loaded
|
| 9 |
-
GET /chat browser chat UI for trying the model
|
| 10 |
-
GET /check verification page: speed, image, multi-turn reuse
|
| 11 |
-
GET /api/info model and device details
|
| 12 |
-
|
| 13 |
-
usage: onw serve MODEL [--port 8000] [--host 0.0.0.0] [--api-key KEY] [--device NPU|CPU] [--open]
|
| 14 |
-
CORS is open, so browser-based clients work too; with --api-key every /v1 request needs "Authorization: Bearer KEY"
|
| 15 |
-
(except from this PC's own programs and pages: the chat and check pages send no key).
|
| 16 |
-
Thinking mode: "chat_template_kwargs": {"enable_thinking": true}, or "reasoning_effort": "low" | "medium" | "high"
|
| 17 |
-
(gpt-oss uses the level; "none" / "minimal" turn thinking off). Whatever the model thought comes back as
|
| 18 |
-
message.reasoning_content (delta.reasoning_content when streaming; also for models that think with thinking off, as vLLM's
|
| 19 |
-
reasoning parsers do), the answer as content, and the thinking's token count as OpenAI's
|
| 20 |
-
usage.completion_tokens_details.reasoning_tokens.
|
| 21 |
-
Tool calls: "tools" (OpenAI format) -> message.tool_calls / finish_reason "tool_calls"; results as role "tool".
|
| 22 |
-
tool_choice "none" hides the tools; a named function narrows them to that one ("required" cannot be enforced).
|
| 23 |
-
response_format json_object / json_schema is best effort (an instruction is added; the output is not constrained).
|
| 24 |
-
Not supported: n > 1 (400), logprobs (null).
|
| 25 |
-
One NPU pipeline serves requests one at a time (a lock serializes them).
|
| 26 |
-
"""
|
| 27 |
-
import argparse, asyncio, base64, io, ipaddress, json, os, re, socket, threading, time, uuid, urllib.parse, urllib.request
|
| 28 |
-
|
| 29 |
-
import uvicorn
|
| 30 |
-
from fastapi import FastAPI, HTTPException, Request
|
| 31 |
-
from fastapi.exceptions import RequestValidationError
|
| 32 |
-
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
| 33 |
-
from PIL import Image
|
| 34 |
-
from onw import __version__, i18n
|
| 35 |
-
from onw import tools as tool_calls
|
| 36 |
-
|
| 37 |
-
MODEL_ID = "npu-model" # set from the engine directory in main()
|
| 38 |
-
app = FastAPI(title="LLM on Intel NPU")
|
| 39 |
-
pipe = None # onw.chat.ChatEngine, created in main()
|
| 40 |
-
info = {}
|
| 41 |
-
lock = threading.Lock() # one generation at a time on the NPU
|
| 42 |
-
WEB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web")
|
| 43 |
-
API_KEY = None # --api-key: required as a Bearer token on /v1
|
| 44 |
-
API_SAMPLING = None # sampling for requests that send none (ONW_API_SAMPLING: "recommended" or a JSON of values)
|
| 45 |
-
TOO_LONG = re.compile(r"the prompt is (\d+) tokens; the context holds (\d+)") # (ChatEngine.stream_chat's ValueError)
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def oai_error(status, message, type_="invalid_request_error", code=None):
|
| 49 |
-
"""An error in OpenAI's shape: {"error": {"message", "type", "code"}} (a prompt over the context: code
|
| 50 |
-
"context_length_exceeded", as OpenAI's)."""
|
| 51 |
-
code = code or ("context_length_exceeded" if TOO_LONG.match(message) else None)
|
| 52 |
-
return JSONResponse({"error": {"message": message, "type": type_, "param": None, "code": code}}, status_code=status)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def anthropic_message(message):
|
| 56 |
-
"""Anthropic's wording for a prompt over the context ("prompt is too long: N tokens > M maximum" - Claude Code
|
| 57 |
-
compacts the conversation on it); other messages as they are."""
|
| 58 |
-
m = TOO_LONG.match(message)
|
| 59 |
-
return f"prompt is too long: {m[1]} tokens > {m[2]} maximum" if m else message
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def api_error(request, status, message, type_="invalid_request_error", code=None):
|
| 63 |
-
"""oai_error, or on the Anthropic Messages API (/v1/messages...) Anthropic's shape:
|
| 64 |
-
{"type": "error", "error": {"type", "message"}}."""
|
| 65 |
-
if request.url.path.startswith("/v1/messages"):
|
| 66 |
-
typ = {401: "authentication_error", 403: "permission_error", 404: "not_found_error",
|
| 67 |
-
413: "request_too_large"}.get(status, "api_error" if status >= 500 else "invalid_request_error")
|
| 68 |
-
return JSONResponse({"type": "error", "error": {"type": typ, "message": anthropic_message(message)}},
|
| 69 |
-
status_code=status)
|
| 70 |
-
return oai_error(status, message, type_, code)
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
@app.exception_handler(HTTPException)
|
| 74 |
-
async def _http_error(request, e):
|
| 75 |
-
return api_error(request, e.status_code, str(e.detail),
|
| 76 |
-
"invalid_request_error" if e.status_code < 500 else "server_error")
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
@app.exception_handler(RequestValidationError)
|
| 80 |
-
async def _validation_error(request, e):
|
| 81 |
-
return api_error(request, 400, str(e))
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
@app.exception_handler(Exception)
|
| 85 |
-
async def _server_error(request, e):
|
| 86 |
-
return api_error(request, 500, f"{type(e).__name__}: {e}", "server_error")
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
CORS = [(b"access-control-allow-origin", b"*"), (b"access-control-allow-headers", b"*"),
|
| 90 |
-
(b"access-control-allow-methods", b"GET, POST, OPTIONS")]
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
class CorsAndKey:
|
| 94 |
-
"""Open CORS (browser-based OpenAI clients) and the optional API key on /v1 (not asked of this PC's own programs and
|
| 95 |
-
pages - the chat and check pages send none; the key keeps the LAN out). A plain ASGI middleware, not
|
| 96 |
-
@app.middleware("http"): behind that one (Starlette's BaseHTTPMiddleware) request.is_disconnected() never turns
|
| 97 |
-
true, and a client that left a whole (non-streamed) answer kept the NPU busy to max_tokens."""
|
| 98 |
-
def __init__(self, inner):
|
| 99 |
-
self.inner = inner
|
| 100 |
-
|
| 101 |
-
async def __call__(self, scope, receive, send):
|
| 102 |
-
if scope["type"] != "http":
|
| 103 |
-
return await self.inner(scope, receive, send)
|
| 104 |
-
request = Request(scope) # (headers and client only: the body stays with the app)
|
| 105 |
-
path = scope["path"]
|
| 106 |
-
if path.startswith("/api/chats"): # saved conversations: this PC's own chat page only
|
| 107 |
-
why = _chats_refused(request)
|
| 108 |
-
if why:
|
| 109 |
-
return await JSONResponse({"error": why}, status_code=403)(scope, receive, send) # (no CORS)
|
| 110 |
-
return await self.inner(scope, receive, send)
|
| 111 |
-
|
| 112 |
-
async def with_cors(msg):
|
| 113 |
-
if msg["type"] == "http.response.start":
|
| 114 |
-
msg = {**msg, "headers": [h for h in msg.get("headers", []) if h[0].lower() not in dict(CORS)] + CORS}
|
| 115 |
-
await send(msg)
|
| 116 |
-
if scope["method"] == "OPTIONS":
|
| 117 |
-
return await JSONResponse({})(scope, receive, with_cors)
|
| 118 |
-
if API_KEY and path.startswith("/v1") and not _local(request) and \
|
| 119 |
-
request.headers.get("authorization", "") != f"Bearer {API_KEY}" and \
|
| 120 |
-
request.headers.get("x-api-key", "") != API_KEY: # (Anthropic clients send x-api-key)
|
| 121 |
-
return await api_error(request, 401, "invalid API key", code="invalid_api_key")(scope, receive, with_cors)
|
| 122 |
-
await self.inner(scope, receive, with_cors)
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
app.add_middleware(CorsAndKey)
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
# ---------------------------------------------------------------- saved conversations (the chat page's history)
|
| 129 |
-
LOCAL_HOSTS = ("localhost", "127.0.0.1", "[::1]", "::1")
|
| 130 |
-
CHAT_ID = __import__("re").compile(r"^[a-z0-9-]{8,48}$")
|
| 131 |
-
CHAT_MAX = 32 << 20 # bytes per conversation (images are kept small by the page)
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
def _chats_refused(request):
|
| 135 |
-
"""Why a request for the saved conversations is refused, or None: only from this PC (also when the server listens on
|
| 136 |
-
the LAN), only under a localhost name (a rebound DNS name of some web site would otherwise pass as same-origin) and
|
| 137 |
-
only from the chat page itself (no cross-site reads or writes: these responses carry no CORS headers)."""
|
| 138 |
-
client = request.client.host if request.client else ""
|
| 139 |
-
try:
|
| 140 |
-
if not ipaddress.ip_address(client).is_loopback:
|
| 141 |
-
return "only from this PC"
|
| 142 |
-
except ValueError:
|
| 143 |
-
return "only from this PC"
|
| 144 |
-
host = urllib.parse.urlsplit("//" + request.headers.get("host", "")).hostname or ""
|
| 145 |
-
if host not in LOCAL_HOSTS:
|
| 146 |
-
return "only under localhost"
|
| 147 |
-
origin = request.headers.get("origin")
|
| 148 |
-
if origin and (urllib.parse.urlsplit(origin).hostname or "") not in LOCAL_HOSTS:
|
| 149 |
-
return "not from another site"
|
| 150 |
-
return None
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
def _local(request):
|
| 154 |
-
"""True for a request from this PC itself as _chats_refused judges it (this PC's address, a localhost name, no other
|
| 155 |
-
site's page) and not one a proxy forwards (with X-Forwarded-For from a proxy on this PC, uvicorn already puts the
|
| 156 |
-
real client's address in request.client; a bare "Forwarded" it does not read)."""
|
| 157 |
-
return _chats_refused(request) is None and "forwarded" not in request.headers and \
|
| 158 |
-
"x-forwarded-for" not in request.headers
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
def chats_dir():
|
| 162 |
-
from onw import app as _app
|
| 163 |
-
d = os.path.join(_app.config_dir(), "chats")
|
| 164 |
-
os.makedirs(d, exist_ok=True)
|
| 165 |
-
return d
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
def _chat_path(cid):
|
| 169 |
-
if not CHAT_ID.match(cid or ""):
|
| 170 |
-
raise HTTPException(400, "bad conversation id")
|
| 171 |
-
return os.path.join(chats_dir(), cid + ".json")
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
@app.get("/api/chats")
|
| 175 |
-
def chats_list():
|
| 176 |
-
out = []
|
| 177 |
-
for f in os.listdir(chats_dir()):
|
| 178 |
-
if not f.endswith(".json"):
|
| 179 |
-
continue
|
| 180 |
-
try:
|
| 181 |
-
c = json.load(open(os.path.join(chats_dir(), f), encoding="utf8"))
|
| 182 |
-
out.append({k: c.get(k) for k in ("id", "title", "model", "created", "updated")} | {"n": len(c.get("messages", []))})
|
| 183 |
-
except (OSError, ValueError):
|
| 184 |
-
continue
|
| 185 |
-
return sorted(out, key=lambda c: c.get("updated") or 0, reverse=True)
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
@app.get("/api/chats/{cid}")
|
| 189 |
-
def chat_get(cid: str):
|
| 190 |
-
try:
|
| 191 |
-
return json.load(open(_chat_path(cid), encoding="utf8"))
|
| 192 |
-
except FileNotFoundError:
|
| 193 |
-
raise HTTPException(404, "no such conversation")
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
@app.put("/api/chats/{cid}")
|
| 197 |
-
async def chat_put(cid: str, request: Request):
|
| 198 |
-
p = _chat_path(cid)
|
| 199 |
-
raw = await request.body()
|
| 200 |
-
if len(raw) > CHAT_MAX:
|
| 201 |
-
raise HTTPException(413, "conversation too large")
|
| 202 |
-
try:
|
| 203 |
-
c = json.loads(raw)
|
| 204 |
-
assert isinstance(c, dict) and isinstance(c.get("messages"), list)
|
| 205 |
-
except (ValueError, AssertionError):
|
| 206 |
-
raise HTTPException(400, "not a conversation")
|
| 207 |
-
c["id"] = cid
|
| 208 |
-
with open(p + ".tmp", "w", encoding="utf8") as f:
|
| 209 |
-
json.dump(c, f, ensure_ascii=False)
|
| 210 |
-
os.replace(p + ".tmp", p)
|
| 211 |
-
return {"ok": True}
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
@app.delete("/api/chats/{cid}")
|
| 215 |
-
def chat_delete(cid: str):
|
| 216 |
-
try:
|
| 217 |
-
os.remove(_chat_path(cid))
|
| 218 |
-
except FileNotFoundError:
|
| 219 |
-
pass
|
| 220 |
-
return {"ok": True}
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
@app.delete("/api/chats")
|
| 224 |
-
def chats_delete_all():
|
| 225 |
-
n = 0
|
| 226 |
-
for f in os.listdir(chats_dir()):
|
| 227 |
-
if f.endswith(".json"):
|
| 228 |
-
try:
|
| 229 |
-
os.remove(os.path.join(chats_dir(), f))
|
| 230 |
-
n += 1
|
| 231 |
-
except OSError:
|
| 232 |
-
pass
|
| 233 |
-
return {"ok": True, "deleted": n}
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
def start_generation(msgs, kw):
|
| 237 |
-
"""Run stream_chat in a worker thread; returns (asyncio queue of deltas/stats/exceptions, None at the end; stop
|
| 238 |
-
event). Called on the event loop: the worker hands its items over to it, so no thread waits per queued request."""
|
| 239 |
-
loop, q, stop = asyncio.get_running_loop(), asyncio.Queue(), threading.Event()
|
| 240 |
-
|
| 241 |
-
def put(x):
|
| 242 |
-
try:
|
| 243 |
-
loop.call_soon_threadsafe(q.put_nowait, x)
|
| 244 |
-
except RuntimeError: # the event loop is gone (the server is stopping)
|
| 245 |
-
stop.set()
|
| 246 |
-
|
| 247 |
-
def work():
|
| 248 |
-
try:
|
| 249 |
-
with lock:
|
| 250 |
-
if stop.is_set(): # the client left while this request waited for the NPU
|
| 251 |
-
return
|
| 252 |
-
set_priority(True)
|
| 253 |
-
try:
|
| 254 |
-
for d in pipe.stream_chat(msgs, should_stop=stop.is_set, split_reasoning=True, **kw):
|
| 255 |
-
put(d)
|
| 256 |
-
finally:
|
| 257 |
-
set_priority(False)
|
| 258 |
-
except Exception as e: # surfaced to the client
|
| 259 |
-
put(e)
|
| 260 |
-
finally:
|
| 261 |
-
put(None)
|
| 262 |
-
threading.Thread(target=work, daemon=True).start()
|
| 263 |
-
return q, stop
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
def reader(q, stop, request=None):
|
| 267 |
-
"""-> async function returning start_generation's next item. With request (a whole response, not a stream): a
|
| 268 |
-
client that leaves meanwhile stops the generation (checked about once a second; a stream stops when it closes)."""
|
| 269 |
-
last = [time.monotonic()]
|
| 270 |
-
|
| 271 |
-
async def next_item():
|
| 272 |
-
while request is not None and not stop.is_set():
|
| 273 |
-
wait = last[0] + 1.0 - time.monotonic()
|
| 274 |
-
if wait <= 0:
|
| 275 |
-
last[0] = time.monotonic()
|
| 276 |
-
if await request.is_disconnected():
|
| 277 |
-
stop.set()
|
| 278 |
-
continue
|
| 279 |
-
try:
|
| 280 |
-
return await asyncio.wait_for(q.get(), wait)
|
| 281 |
-
except asyncio.TimeoutError:
|
| 282 |
-
pass
|
| 283 |
-
return await q.get()
|
| 284 |
-
return next_item
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
MAX_IMAGE = 20 << 20 # bytes
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
def _public_host(url):
|
| 291 |
-
"""True when every address the URL's host resolves to is a public one: the server does not fetch from the local
|
| 292 |
-
network on a client's behalf (ONW_ALLOW_LOCAL_IMAGES=1 allows it)."""
|
| 293 |
-
if os.environ.get("ONW_ALLOW_LOCAL_IMAGES") == "1":
|
| 294 |
-
return True
|
| 295 |
-
host = urllib.parse.urlsplit(url).hostname
|
| 296 |
-
try:
|
| 297 |
-
addrs = {a[4][0] for a in socket.getaddrinfo(host, None)}
|
| 298 |
-
except (socket.gaierror, UnicodeError, TypeError):
|
| 299 |
-
return False
|
| 300 |
-
return bool(addrs) and all(ipaddress.ip_address(a.split("%")[0]).is_global for a in addrs)
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
def load_image(url):
|
| 304 |
-
try:
|
| 305 |
-
if url.startswith("data:"):
|
| 306 |
-
data = base64.b64decode(url.split(",", 1)[1])
|
| 307 |
-
elif url.startswith(("http://", "https://")):
|
| 308 |
-
if not _public_host(url):
|
| 309 |
-
raise HTTPException(400, "image_url: only public http(s) addresses are fetched (send a data: URL instead)")
|
| 310 |
-
req = urllib.request.Request(url, headers={"User-Agent": f"onw/{__version__}"})
|
| 311 |
-
with urllib.request.urlopen(req, timeout=20) as r:
|
| 312 |
-
data = r.read(MAX_IMAGE + 1)
|
| 313 |
-
if len(data) > MAX_IMAGE:
|
| 314 |
-
raise HTTPException(400, "image_url: the image is larger than 20 MB")
|
| 315 |
-
else:
|
| 316 |
-
raise HTTPException(400, "image_url must be a data: URL or http(s) URL")
|
| 317 |
-
return Image.open(io.BytesIO(data)).convert("RGB")
|
| 318 |
-
except HTTPException:
|
| 319 |
-
raise
|
| 320 |
-
except Exception as e: # bad base64, not an image, unreachable URL
|
| 321 |
-
raise HTTPException(400, f"image_url: {type(e).__name__}: {e}")
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
def to_hf_messages(messages):
|
| 325 |
-
"""OpenAI chat messages -> HF chat format with PIL images."""
|
| 326 |
-
out = []
|
| 327 |
-
for m in tool_calls.history(messages):
|
| 328 |
-
keep = {k: m[k] for k in ("tool_calls", "tool_call_id", "name", "reasoning_content") if m.get(k) is not None}
|
| 329 |
-
c = m.get("content")
|
| 330 |
-
if c is None or isinstance(c, str):
|
| 331 |
-
out.append({"role": m["role"], "content": c or "", **keep})
|
| 332 |
-
continue
|
| 333 |
-
parts = []
|
| 334 |
-
for p in c:
|
| 335 |
-
if p.get("type") == "text":
|
| 336 |
-
parts.append({"type": "text", "text": p.get("text") or ""})
|
| 337 |
-
elif p.get("type") == "image_url":
|
| 338 |
-
iu = p.get("image_url") or {}
|
| 339 |
-
url = iu.get("url") if isinstance(iu, dict) else iu
|
| 340 |
-
if not isinstance(url, str):
|
| 341 |
-
raise HTTPException(400, "image_url part without a url")
|
| 342 |
-
parts.append({"type": "image", "image": load_image(url)})
|
| 343 |
-
else:
|
| 344 |
-
raise HTTPException(400, f"unsupported content part type: {p.get('type')}")
|
| 345 |
-
out.append({"role": m["role"], "content": parts, **keep})
|
| 346 |
-
return out
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
def _num(body, key, default, cast=float):
|
| 350 |
-
v = body.get(key)
|
| 351 |
-
if v is None:
|
| 352 |
-
return default
|
| 353 |
-
try:
|
| 354 |
-
return cast(v)
|
| 355 |
-
except (TypeError, ValueError):
|
| 356 |
-
raise HTTPException(400, f"{key} must be a number")
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
LEVELS = ("off", "low", "medium", "high") # the thinking levels of the window and the chat page (reasoning_effort)
|
| 360 |
-
_PREFS = [None, None] # (config mtime, this model's defaults)
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
def model_defaults():
|
| 364 |
-
"""This model's defaults from the onw window (config model_prefs, read again when the file changes - no restart):
|
| 365 |
-
{"think": level, "sampling": "greedy" | "recommended" | "custom", values...}; {} when none were saved. Settings
|
| 366 |
-
of earlier versions (ONW_THINK_DEFAULT, ONW_API_SAMPLING) apply where these say nothing."""
|
| 367 |
-
key = os.environ.get("ONW_MODEL_KEY")
|
| 368 |
-
if not key:
|
| 369 |
-
return {}
|
| 370 |
-
try:
|
| 371 |
-
from onw import app as _app
|
| 372 |
-
p = os.path.join(_app.config_dir(), "config.json")
|
| 373 |
-
mt = os.path.getmtime(p)
|
| 374 |
-
if _PREFS[0] != mt:
|
| 375 |
-
_PREFS[:] = [mt, (_app.load_config().get("model_prefs") or {}).get(key) or {}]
|
| 376 |
-
return _PREFS[1]
|
| 377 |
-
except (OSError, ValueError):
|
| 378 |
-
return {}
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
def default_think():
|
| 382 |
-
"""Thinking for a request that says nothing about it: the model's saved level, else the old switch."""
|
| 383 |
-
lvl = model_defaults().get("think")
|
| 384 |
-
if lvl == "off":
|
| 385 |
-
return False
|
| 386 |
-
if lvl in LEVELS:
|
| 387 |
-
return lvl
|
| 388 |
-
return True if os.environ.get("ONW_THINK_DEFAULT") == "1" else None
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
def default_sampling():
|
| 392 |
-
"""Sampling for a request that sends none: the model's saved choice, else the old setting (API_SAMPLING)."""
|
| 393 |
-
d = model_defaults()
|
| 394 |
-
mode = d.get("sampling")
|
| 395 |
-
if mode == "greedy":
|
| 396 |
-
return None
|
| 397 |
-
if mode == "recommended":
|
| 398 |
-
return {k: v for k, v in (info.get("sampling") or {}).items() if k != "source"} or None
|
| 399 |
-
if mode == "custom":
|
| 400 |
-
return {k: d[k] for k in ("temperature", "top_p", "top_k", "presence_penalty") if k in d}
|
| 401 |
-
return API_SAMPLING
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
def gen_args(body):
|
| 405 |
-
"""OpenAI request fields -> ChatEngine.stream_chat arguments."""
|
| 406 |
-
if _num(body, "n", 1, int) != 1:
|
| 407 |
-
raise HTTPException(400, "n > 1 is not supported (one NPU pipeline)")
|
| 408 |
-
stop = body.get("stop") or []
|
| 409 |
-
mx = body.get("max_completion_tokens", body.get("max_tokens"))
|
| 410 |
-
# a request without max_tokens: the setting (ONW_MAX_TOKENS; 0 = up to the end of the context - the engine stops
|
| 411 |
-
# there anyway); agents send none and write whole files in one answer
|
| 412 |
-
if mx is None:
|
| 413 |
-
mx = int(os.environ.get("ONW_MAX_TOKENS", "8192") or 0) or (1 << 30)
|
| 414 |
-
else:
|
| 415 |
-
mx = _num({"max_tokens": mx}, "max_tokens", 512, int)
|
| 416 |
-
if mx < 1:
|
| 417 |
-
raise HTTPException(400, "max_tokens must be at least 1")
|
| 418 |
-
think = (body.get("chat_template_kwargs") or {}).get("enable_thinking")
|
| 419 |
-
effort = body.get("reasoning_effort")
|
| 420 |
-
if isinstance(effort, str): # a level: low / medium / high (gpt-oss, llm-jp: trained;
|
| 421 |
-
think = False if effort in ("none", "minimal", "off") else effort # others: a thinking budget)
|
| 422 |
-
if think is None: # nothing said: the window's default for this model
|
| 423 |
-
think = default_think()
|
| 424 |
-
tools, choice = body.get("tools") or None, body.get("tool_choice")
|
| 425 |
-
if choice == "none":
|
| 426 |
-
tools = None
|
| 427 |
-
elif isinstance(choice, dict) and tools: # a named function: offer just that one
|
| 428 |
-
name = (choice.get("function") or {}).get("name")
|
| 429 |
-
tools = [t for t in tools if (t.get("function") or {}).get("name") == name] or tools
|
| 430 |
-
# what the request leaves out: greedy, or the settings' API defaults (API_SAMPLING; thinking has its own set)
|
| 431 |
-
dflt = dict(default_sampling() or {})
|
| 432 |
-
if think and isinstance(dflt.get("think"), dict):
|
| 433 |
-
dflt.update(dflt["think"])
|
| 434 |
-
return dict(max_new_tokens=mx, temperature=_num(body, "temperature", dflt.get("temperature", 0.0)),
|
| 435 |
-
top_p=_num(body, "top_p", dflt.get("top_p", 1.0)),
|
| 436 |
-
stop=[stop] if isinstance(stop, str) else list(stop), seed=body.get("seed"), think=think,
|
| 437 |
-
top_k=_num(body, "top_k", dflt.get("top_k", 0), int), repetition_penalty=_num(body, "repetition_penalty", 1.0),
|
| 438 |
-
presence_penalty=_num(body, "presence_penalty", dflt.get("presence_penalty", 0.0)),
|
| 439 |
-
frequency_penalty=_num(body, "frequency_penalty", 0.0), tools=tools)
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
def with_format(msgs, body):
|
| 443 |
-
"""response_format json_object / json_schema: best effort - an instruction as a system message (the output is not
|
| 444 |
-
constrained)."""
|
| 445 |
-
rf = body.get("response_format") or {}
|
| 446 |
-
kind = rf.get("type") if isinstance(rf, dict) else None
|
| 447 |
-
if kind not in ("json_object", "json_schema"):
|
| 448 |
-
return msgs
|
| 449 |
-
note = "Respond with a single JSON object only, no other text."
|
| 450 |
-
if kind == "json_schema":
|
| 451 |
-
note += " It must follow this JSON schema: " + json.dumps((rf.get("json_schema") or {}).get("schema") or {},
|
| 452 |
-
ensure_ascii=False)
|
| 453 |
-
if msgs and msgs[0]["role"] == "system" and isinstance(msgs[0]["content"], str):
|
| 454 |
-
return [dict(msgs[0], content=msgs[0]["content"] + "\n\n" + note)] + msgs[1:]
|
| 455 |
-
return [{"role": "system", "content": note}] + msgs
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
@app.get("/")
|
| 459 |
-
def index(request: Request):
|
| 460 |
-
base = str(request.base_url).rstrip("/")
|
| 461 |
-
return {"object": "onw.server", "name": "onw - 俺のNPUがこんなに動くわけない", "version": __version__, "model": MODEL_ID,
|
| 462 |
-
"openai_base_url": base + "/v1", "api_key_required": bool(API_KEY),
|
| 463 |
-
"endpoints": {"models": "/v1/models", "chat_completions": "/v1/chat/completions", "health": "/health",
|
| 464 |
-
"chat_ui": "/chat", "check_page": "/check", "info": "/api/info"},
|
| 465 |
-
"context": info.get("max_context"), "vision": info.get("vision")}
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
@app.get("/health")
|
| 469 |
-
def health():
|
| 470 |
-
return {"status": "ok", "model": MODEL_ID}
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
@app.get("/chat")
|
| 474 |
-
def chat_ui():
|
| 475 |
-
return HTMLResponse(i18n.page(os.path.join(WEB, "index.html")))
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
@app.get("/check")
|
| 479 |
-
def check_page():
|
| 480 |
-
return HTMLResponse(i18n.page(os.path.join(WEB, "check.html")))
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
@app.get("/api/info")
|
| 484 |
-
def api_info():
|
| 485 |
-
# the window's defaults for this model, read now (they change without a restart)
|
| 486 |
-
d, lvl = model_defaults(), default_think()
|
| 487 |
-
samp = default_sampling()
|
| 488 |
-
mode = d.get("sampling") or ("custom" if API_SAMPLING and os.environ.get("ONW_API_SAMPLING") != "recommended"
|
| 489 |
-
else "recommended" if API_SAMPLING else "greedy")
|
| 490 |
-
return {**info, "api_sampling": samp, "api_sampling_mode": mode,
|
| 491 |
-
"think_default": "off" if lvl is False else ("medium" if lvl is True else lvl)}
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
STARTED = int(time.time())
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
def _model_card():
|
| 498 |
-
return {"id": MODEL_ID, "object": "model", "created": STARTED, "owned_by": "local-npu"}
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
@app.get("/v1/models")
|
| 502 |
-
def models():
|
| 503 |
-
return {"object": "list", "data": [_model_card()]}
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
@app.get("/v1/models/{model_id:path}")
|
| 507 |
-
def model(model_id: str):
|
| 508 |
-
if model_id != MODEL_ID:
|
| 509 |
-
raise HTTPException(404, f"model {model_id!r} not found (this server runs {MODEL_ID!r})")
|
| 510 |
-
return _model_card()
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
def finalize(text, tools, finish):
|
| 514 |
-
"""The answer's end: tool calls parsed out of the text (OpenAI tool_calls), finish_reason "tool_calls" when there
|
| 515 |
-
are any. -> (text, calls, finish_reason)."""
|
| 516 |
-
if not tools:
|
| 517 |
-
return text, [], finish
|
| 518 |
-
text, calls = tool_calls.parse(text, tools)
|
| 519 |
-
return text, calls, ("tool_calls" if calls else finish)
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
def usage_of(st):
|
| 523 |
-
return {"prompt_tokens": st["prompt_tokens"], "completion_tokens": st["completion_tokens"],
|
| 524 |
-
"total_tokens": st["prompt_tokens"] + st["completion_tokens"],
|
| 525 |
-
"prompt_tokens_details": {"cached_tokens": st.get("cached_tokens", 0)},
|
| 526 |
-
# (OpenAI's field: how many of the completion tokens were thinking)
|
| 527 |
-
"completion_tokens_details": {"reasoning_tokens": st.get("reasoning_tokens", 0)}}
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
def stats_of(st):
|
| 531 |
-
return {k: (round(v, 2) if isinstance(v, float) else v) for k, v in st.items() if k != "finish_reason"}
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
def worker_error(e):
|
| 535 |
-
"""A generation error -> (HTTP status, OpenAI error type): the request's fault (a prompt too long, an image for a
|
| 536 |
-
text-only model) or the server's (the NPU, the driver)."""
|
| 537 |
-
return (400, "invalid_request_error") if isinstance(e, (ValueError, KeyError, TypeError)) else (500, "server_error")
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
@app.post("/v1/chat/completions")
|
| 541 |
-
async def chat(request: Request):
|
| 542 |
-
try:
|
| 543 |
-
body = await request.json()
|
| 544 |
-
except ValueError:
|
| 545 |
-
raise HTTPException(400, "the request body is not valid JSON")
|
| 546 |
-
if not isinstance(body, dict) or not isinstance(body.get("messages"), list):
|
| 547 |
-
raise HTTPException(400, "messages (a list) is required")
|
| 548 |
-
kw = gen_args(body)
|
| 549 |
-
loop = asyncio.get_running_loop()
|
| 550 |
-
try: # images may be downloaded: not on the event loop
|
| 551 |
-
msgs = await loop.run_in_executor(None, to_hf_messages, body["messages"])
|
| 552 |
-
except (KeyError, TypeError, AttributeError, ValueError) as e:
|
| 553 |
-
raise HTTPException(400, f"malformed messages: {e}")
|
| 554 |
-
msgs = with_format(msgs, body)
|
| 555 |
-
cid, created = f"chatcmpl-{uuid.uuid4().hex[:24]}", int(time.time())
|
| 556 |
-
tools = kw["tools"]
|
| 557 |
-
if body.get("stream"):
|
| 558 |
-
def chunk(delta, finish=None, **extra):
|
| 559 |
-
d = {"id": cid, "object": "chat.completion.chunk", "created": created, "model": MODEL_ID,
|
| 560 |
-
"choices": [{"index": 0, "delta": delta, "logprobs": None, "finish_reason": finish}], **extra}
|
| 561 |
-
return f"data: {json.dumps(d, ensure_ascii=False)}\n\n"
|
| 562 |
-
|
| 563 |
-
async def events():
|
| 564 |
-
full, sent = "", 0 # with tools: text is held back once a call may start
|
| 565 |
-
# (started here, not before the response: one whose stream never began would generate on to max_tokens)
|
| 566 |
-
q, stop = start_generation(msgs, kw)
|
| 567 |
-
next_item = reader(q, stop)
|
| 568 |
-
try:
|
| 569 |
-
yield chunk({"role": "assistant", "content": ""})
|
| 570 |
-
while True:
|
| 571 |
-
d = await next_item()
|
| 572 |
-
if d is None:
|
| 573 |
-
break
|
| 574 |
-
if isinstance(d, Exception):
|
| 575 |
-
status, typ = worker_error(d)
|
| 576 |
-
err = {"error": {"message": str(d), "type": typ, "param": None,
|
| 577 |
-
"code": "context_length_exceeded" if TOO_LONG.match(str(d)) else None}}
|
| 578 |
-
yield f"data: {json.dumps(err, ensure_ascii=False)}\n\n"
|
| 579 |
-
elif isinstance(d, dict) and "reasoning" in d:
|
| 580 |
-
yield chunk({"reasoning_content": d["reasoning"]})
|
| 581 |
-
elif isinstance(d, dict):
|
| 582 |
-
rest, calls, finish = finalize(full, tools, d["finish_reason"])
|
| 583 |
-
if tools and rest.startswith(full[:sent]) and rest[sent:].strip(): # text after the calls
|
| 584 |
-
yield chunk({"content": rest[sent:]})
|
| 585 |
-
for i, tc in enumerate(calls):
|
| 586 |
-
yield chunk({"tool_calls": [dict(tc, index=i)]})
|
| 587 |
-
yield chunk({}, finish, onw_stats=stats_of(d))
|
| 588 |
-
if (body.get("stream_options") or {}).get("include_usage"): # OpenAI: its own last chunk
|
| 589 |
-
yield f"data: {json.dumps({'id': cid, 'object': 'chat.completion.chunk', 'created': created, 'model': MODEL_ID, 'choices': [], 'usage': usage_of(d)})}\n\n"
|
| 590 |
-
elif tools:
|
| 591 |
-
full += d
|
| 592 |
-
upto = tool_calls.safe_upto(full)
|
| 593 |
-
if upto > sent:
|
| 594 |
-
yield chunk({"content": full[sent:upto]})
|
| 595 |
-
sent = upto
|
| 596 |
-
else:
|
| 597 |
-
yield chunk({"content": d})
|
| 598 |
-
yield "data: [DONE]\n\n"
|
| 599 |
-
finally: # client disconnected or finished: stop the NPU loop so the lock is released
|
| 600 |
-
stop.set()
|
| 601 |
-
return StreamingResponse(events(), media_type="text/event-stream",
|
| 602 |
-
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
| 603 |
-
|
| 604 |
-
parts, thought, st = [], [], None
|
| 605 |
-
q, stop = start_generation(msgs, kw)
|
| 606 |
-
next_item = reader(q, stop, request) # (a client that leaves stops the generation)
|
| 607 |
-
try:
|
| 608 |
-
while True:
|
| 609 |
-
d = await next_item()
|
| 610 |
-
if d is None:
|
| 611 |
-
break
|
| 612 |
-
if isinstance(d, Exception):
|
| 613 |
-
status, typ = worker_error(d)
|
| 614 |
-
return oai_error(status, str(d), typ)
|
| 615 |
-
if isinstance(d, dict) and "reasoning" in d:
|
| 616 |
-
thought.append(d["reasoning"])
|
| 617 |
-
elif isinstance(d, dict):
|
| 618 |
-
st = d
|
| 619 |
-
else:
|
| 620 |
-
parts.append(d)
|
| 621 |
-
finally:
|
| 622 |
-
stop.set()
|
| 623 |
-
if st is None:
|
| 624 |
-
return oai_error(500, "the generation ended without a result", "server_error")
|
| 625 |
-
text, calls, finish = finalize("".join(parts), tools, st["finish_reason"])
|
| 626 |
-
msg = {"role": "assistant", "content": (text.strip() or None) if tools else text}
|
| 627 |
-
if thought:
|
| 628 |
-
msg["reasoning_content"] = "".join(thought)
|
| 629 |
-
if calls:
|
| 630 |
-
msg["tool_calls"] = calls
|
| 631 |
-
return JSONResponse({
|
| 632 |
-
"id": cid, "object": "chat.completion", "created": created, "model": MODEL_ID,
|
| 633 |
-
"choices": [{"index": 0, "message": msg, "logprobs": None, "finish_reason": finish}],
|
| 634 |
-
"usage": usage_of(st),
|
| 635 |
-
"onw_stats": stats_of(st),
|
| 636 |
-
})
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
def recommended_sampling(arch, engine_dir=None):
|
| 640 |
-
"""The chat page's starting sampling values ({temperature, top_p, top_k, presence_penalty?, think?, source}) or
|
| 641 |
-
None (greedy): the model's own generation_config.json first (Gemma 4, Granite 4.2), else onw/sampling.json by
|
| 642 |
-
model_type prefix. The API itself stays greedy unless asked."""
|
| 643 |
-
keys = ("temperature", "top_p", "top_k")
|
| 644 |
-
try:
|
| 645 |
-
with open(os.path.join(engine_dir, "generation_config.json"), encoding="utf8") as f:
|
| 646 |
-
g = json.load(f)
|
| 647 |
-
if g.get("do_sample") and any(k in g for k in keys):
|
| 648 |
-
return {"source": "model", "temperature": g.get("temperature", 1.0), "top_p": g.get("top_p", 1.0),
|
| 649 |
-
"top_k": g.get("top_k", 0)}
|
| 650 |
-
except (OSError, ValueError, TypeError):
|
| 651 |
-
pass
|
| 652 |
-
try:
|
| 653 |
-
with open(os.path.join(WEB, "..", "sampling.json"), encoding="utf8") as f:
|
| 654 |
-
table = {k: v for k, v in json.load(f).items() if not k.startswith("_")}
|
| 655 |
-
except (OSError, ValueError):
|
| 656 |
-
return None
|
| 657 |
-
hit = max((k for k in table if arch.startswith(k)), key=len, default=None)
|
| 658 |
-
if hit is None:
|
| 659 |
-
return None
|
| 660 |
-
return {k: v for k, v in table[hit].items() if k != "note"}
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
from onw import compat # noqa: E402,F401 (/v1/messages and /v1/responses on this app)
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
def set_priority(busy):
|
| 667 |
-
"""Windows: the server's priority class - normal while loading and waiting, above normal while it generates
|
| 668 |
-
(busy). Windows puts a below-normal process on the low-power cores at low clocks (onw 1.4.5 and earlier started
|
| 669 |
-
the server below normal: Lunar Lake, Qwen3.5-9B NF4 with MTP ~12 tok/s, 16 above normal); generating takes one or two
|
| 670 |
-
cores, so the rest of the PC hardly notices. ONW_PRIORITY: the class while generating, normal | above (default) |
|
| 671 |
-
off (leave it as started)."""
|
| 672 |
-
want = os.environ.get("ONW_PRIORITY", "above")
|
| 673 |
-
if os.name != "nt" or want not in ("normal", "above"):
|
| 674 |
-
return
|
| 675 |
-
import ctypes
|
| 676 |
-
k32 = ctypes.WinDLL("kernel32")
|
| 677 |
-
k32.GetCurrentProcess.restype = ctypes.c_void_p
|
| 678 |
-
k32.SetPriorityClass.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
|
| 679 |
-
k32.SetPriorityClass(k32.GetCurrentProcess(), 0x8000 if busy and want == "above" else 0x20)
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
def main(argv=None):
|
| 683 |
-
global pipe, MODEL_ID, API_KEY, API_SAMPLING
|
| 684 |
-
ap = argparse.ArgumentParser(prog="onw serve", description="chat UI + OpenAI-compatible API on the Intel NPU")
|
| 685 |
-
ap.add_argument("model", help="onw model folder, or a Hugging Face repo id (downloaded into ./models)")
|
| 686 |
-
ap.add_argument("--host", default="127.0.0.1")
|
| 687 |
-
ap.add_argument("--port", type=int, default=8000)
|
| 688 |
-
ap.add_argument("--device", default="NPU", help="run every component on this device (CPU works without an NPU driver)")
|
| 689 |
-
ap.add_argument("--name", help="model id / title (default: engine directory name)")
|
| 690 |
-
ap.add_argument("--context", type=int, default=None,
|
| 691 |
-
help="context length in tokens (default 32768, at most the model's maximum); no recompile")
|
| 692 |
-
ap.add_argument("--open", action="store_true", help="open the chat UI in the default browser once loaded")
|
| 693 |
-
ap.add_argument("--no-pld", action="store_true", help="disable prompt lookup decoding")
|
| 694 |
-
ap.add_argument("--api-key", help="require this key as a Bearer token on /v1 from other machines "
|
| 695 |
-
"(e.g. with --host 0.0.0.0)")
|
| 696 |
-
a = ap.parse_args(argv)
|
| 697 |
-
API_KEY = a.api_key or os.environ.get("ONW_API_KEY") or None
|
| 698 |
-
from onw.hub import resolve
|
| 699 |
-
a.engine = resolve(a.model)
|
| 700 |
-
import openvino as ov
|
| 701 |
-
from
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
#
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
_main
|
|
|
|
|
|
| 1 |
+
"""OpenAI-compatible API server for an onw model on the Intel NPU (point other apps at http://HOST:PORT/v1).
|
| 2 |
+
|
| 3 |
+
GET / what this server is: model, OpenAI base URL, endpoints (JSON)
|
| 4 |
+
GET /v1/models
|
| 5 |
+
GET /v1/models/{id}
|
| 6 |
+
POST /v1/chat/completions (stream true/false; text and image_url parts: data: URLs or http(s)); responses
|
| 7 |
+
also carry "onw_stats" (prefill / decode speed, reused tokens) - extra field
|
| 8 |
+
GET /health 200 when the model is loaded
|
| 9 |
+
GET /chat browser chat UI for trying the model
|
| 10 |
+
GET /check verification page: speed, image, multi-turn reuse
|
| 11 |
+
GET /api/info model and device details
|
| 12 |
+
|
| 13 |
+
usage: onw serve MODEL [--port 8000] [--host 0.0.0.0] [--api-key KEY] [--device NPU|CPU] [--open]
|
| 14 |
+
CORS is open, so browser-based clients work too; with --api-key every /v1 request needs "Authorization: Bearer KEY"
|
| 15 |
+
(except from this PC's own programs and pages: the chat and check pages send no key).
|
| 16 |
+
Thinking mode: "chat_template_kwargs": {"enable_thinking": true}, or "reasoning_effort": "low" | "medium" | "high"
|
| 17 |
+
(gpt-oss uses the level; "none" / "minimal" turn thinking off). Whatever the model thought comes back as
|
| 18 |
+
message.reasoning_content (delta.reasoning_content when streaming; also for models that think with thinking off, as vLLM's
|
| 19 |
+
reasoning parsers do), the answer as content, and the thinking's token count as OpenAI's
|
| 20 |
+
usage.completion_tokens_details.reasoning_tokens.
|
| 21 |
+
Tool calls: "tools" (OpenAI format) -> message.tool_calls / finish_reason "tool_calls"; results as role "tool".
|
| 22 |
+
tool_choice "none" hides the tools; a named function narrows them to that one ("required" cannot be enforced).
|
| 23 |
+
response_format json_object / json_schema is best effort (an instruction is added; the output is not constrained).
|
| 24 |
+
Not supported: n > 1 (400), logprobs (null).
|
| 25 |
+
One NPU pipeline serves requests one at a time (a lock serializes them).
|
| 26 |
+
"""
|
| 27 |
+
import argparse, asyncio, base64, io, ipaddress, json, os, re, socket, threading, time, uuid, urllib.parse, urllib.request
|
| 28 |
+
|
| 29 |
+
import uvicorn
|
| 30 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 31 |
+
from fastapi.exceptions import RequestValidationError
|
| 32 |
+
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
| 33 |
+
from PIL import Image
|
| 34 |
+
from onw import __version__, i18n
|
| 35 |
+
from onw import tools as tool_calls
|
| 36 |
+
|
| 37 |
+
MODEL_ID = "npu-model" # set from the engine directory in main()
|
| 38 |
+
app = FastAPI(title="LLM on Intel NPU")
|
| 39 |
+
pipe = None # onw.chat.ChatEngine, created in main()
|
| 40 |
+
info = {}
|
| 41 |
+
lock = threading.Lock() # one generation at a time on the NPU
|
| 42 |
+
WEB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web")
|
| 43 |
+
API_KEY = None # --api-key: required as a Bearer token on /v1
|
| 44 |
+
API_SAMPLING = None # sampling for requests that send none (ONW_API_SAMPLING: "recommended" or a JSON of values)
|
| 45 |
+
TOO_LONG = re.compile(r"the prompt is (\d+) tokens; the context holds (\d+)") # (ChatEngine.stream_chat's ValueError)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def oai_error(status, message, type_="invalid_request_error", code=None):
|
| 49 |
+
"""An error in OpenAI's shape: {"error": {"message", "type", "code"}} (a prompt over the context: code
|
| 50 |
+
"context_length_exceeded", as OpenAI's)."""
|
| 51 |
+
code = code or ("context_length_exceeded" if TOO_LONG.match(message) else None)
|
| 52 |
+
return JSONResponse({"error": {"message": message, "type": type_, "param": None, "code": code}}, status_code=status)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def anthropic_message(message):
|
| 56 |
+
"""Anthropic's wording for a prompt over the context ("prompt is too long: N tokens > M maximum" - Claude Code
|
| 57 |
+
compacts the conversation on it); other messages as they are."""
|
| 58 |
+
m = TOO_LONG.match(message)
|
| 59 |
+
return f"prompt is too long: {m[1]} tokens > {m[2]} maximum" if m else message
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def api_error(request, status, message, type_="invalid_request_error", code=None):
|
| 63 |
+
"""oai_error, or on the Anthropic Messages API (/v1/messages...) Anthropic's shape:
|
| 64 |
+
{"type": "error", "error": {"type", "message"}}."""
|
| 65 |
+
if request.url.path.startswith("/v1/messages"):
|
| 66 |
+
typ = {401: "authentication_error", 403: "permission_error", 404: "not_found_error",
|
| 67 |
+
413: "request_too_large"}.get(status, "api_error" if status >= 500 else "invalid_request_error")
|
| 68 |
+
return JSONResponse({"type": "error", "error": {"type": typ, "message": anthropic_message(message)}},
|
| 69 |
+
status_code=status)
|
| 70 |
+
return oai_error(status, message, type_, code)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@app.exception_handler(HTTPException)
|
| 74 |
+
async def _http_error(request, e):
|
| 75 |
+
return api_error(request, e.status_code, str(e.detail),
|
| 76 |
+
"invalid_request_error" if e.status_code < 500 else "server_error")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@app.exception_handler(RequestValidationError)
|
| 80 |
+
async def _validation_error(request, e):
|
| 81 |
+
return api_error(request, 400, str(e))
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@app.exception_handler(Exception)
|
| 85 |
+
async def _server_error(request, e):
|
| 86 |
+
return api_error(request, 500, f"{type(e).__name__}: {e}", "server_error")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
CORS = [(b"access-control-allow-origin", b"*"), (b"access-control-allow-headers", b"*"),
|
| 90 |
+
(b"access-control-allow-methods", b"GET, POST, OPTIONS")]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class CorsAndKey:
|
| 94 |
+
"""Open CORS (browser-based OpenAI clients) and the optional API key on /v1 (not asked of this PC's own programs and
|
| 95 |
+
pages - the chat and check pages send none; the key keeps the LAN out). A plain ASGI middleware, not
|
| 96 |
+
@app.middleware("http"): behind that one (Starlette's BaseHTTPMiddleware) request.is_disconnected() never turns
|
| 97 |
+
true, and a client that left a whole (non-streamed) answer kept the NPU busy to max_tokens."""
|
| 98 |
+
def __init__(self, inner):
|
| 99 |
+
self.inner = inner
|
| 100 |
+
|
| 101 |
+
async def __call__(self, scope, receive, send):
|
| 102 |
+
if scope["type"] != "http":
|
| 103 |
+
return await self.inner(scope, receive, send)
|
| 104 |
+
request = Request(scope) # (headers and client only: the body stays with the app)
|
| 105 |
+
path = scope["path"]
|
| 106 |
+
if path.startswith("/api/chats"): # saved conversations: this PC's own chat page only
|
| 107 |
+
why = _chats_refused(request)
|
| 108 |
+
if why:
|
| 109 |
+
return await JSONResponse({"error": why}, status_code=403)(scope, receive, send) # (no CORS)
|
| 110 |
+
return await self.inner(scope, receive, send)
|
| 111 |
+
|
| 112 |
+
async def with_cors(msg):
|
| 113 |
+
if msg["type"] == "http.response.start":
|
| 114 |
+
msg = {**msg, "headers": [h for h in msg.get("headers", []) if h[0].lower() not in dict(CORS)] + CORS}
|
| 115 |
+
await send(msg)
|
| 116 |
+
if scope["method"] == "OPTIONS":
|
| 117 |
+
return await JSONResponse({})(scope, receive, with_cors)
|
| 118 |
+
if API_KEY and path.startswith("/v1") and not _local(request) and \
|
| 119 |
+
request.headers.get("authorization", "") != f"Bearer {API_KEY}" and \
|
| 120 |
+
request.headers.get("x-api-key", "") != API_KEY: # (Anthropic clients send x-api-key)
|
| 121 |
+
return await api_error(request, 401, "invalid API key", code="invalid_api_key")(scope, receive, with_cors)
|
| 122 |
+
await self.inner(scope, receive, with_cors)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
app.add_middleware(CorsAndKey)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# ---------------------------------------------------------------- saved conversations (the chat page's history)
|
| 129 |
+
LOCAL_HOSTS = ("localhost", "127.0.0.1", "[::1]", "::1")
|
| 130 |
+
CHAT_ID = __import__("re").compile(r"^[a-z0-9-]{8,48}$")
|
| 131 |
+
CHAT_MAX = 32 << 20 # bytes per conversation (images are kept small by the page)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _chats_refused(request):
|
| 135 |
+
"""Why a request for the saved conversations is refused, or None: only from this PC (also when the server listens on
|
| 136 |
+
the LAN), only under a localhost name (a rebound DNS name of some web site would otherwise pass as same-origin) and
|
| 137 |
+
only from the chat page itself (no cross-site reads or writes: these responses carry no CORS headers)."""
|
| 138 |
+
client = request.client.host if request.client else ""
|
| 139 |
+
try:
|
| 140 |
+
if not ipaddress.ip_address(client).is_loopback:
|
| 141 |
+
return "only from this PC"
|
| 142 |
+
except ValueError:
|
| 143 |
+
return "only from this PC"
|
| 144 |
+
host = urllib.parse.urlsplit("//" + request.headers.get("host", "")).hostname or ""
|
| 145 |
+
if host not in LOCAL_HOSTS:
|
| 146 |
+
return "only under localhost"
|
| 147 |
+
origin = request.headers.get("origin")
|
| 148 |
+
if origin and (urllib.parse.urlsplit(origin).hostname or "") not in LOCAL_HOSTS:
|
| 149 |
+
return "not from another site"
|
| 150 |
+
return None
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def _local(request):
|
| 154 |
+
"""True for a request from this PC itself as _chats_refused judges it (this PC's address, a localhost name, no other
|
| 155 |
+
site's page) and not one a proxy forwards (with X-Forwarded-For from a proxy on this PC, uvicorn already puts the
|
| 156 |
+
real client's address in request.client; a bare "Forwarded" it does not read)."""
|
| 157 |
+
return _chats_refused(request) is None and "forwarded" not in request.headers and \
|
| 158 |
+
"x-forwarded-for" not in request.headers
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def chats_dir():
|
| 162 |
+
from onw import app as _app
|
| 163 |
+
d = os.path.join(_app.config_dir(), "chats")
|
| 164 |
+
os.makedirs(d, exist_ok=True)
|
| 165 |
+
return d
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _chat_path(cid):
|
| 169 |
+
if not CHAT_ID.match(cid or ""):
|
| 170 |
+
raise HTTPException(400, "bad conversation id")
|
| 171 |
+
return os.path.join(chats_dir(), cid + ".json")
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
@app.get("/api/chats")
|
| 175 |
+
def chats_list():
|
| 176 |
+
out = []
|
| 177 |
+
for f in os.listdir(chats_dir()):
|
| 178 |
+
if not f.endswith(".json"):
|
| 179 |
+
continue
|
| 180 |
+
try:
|
| 181 |
+
c = json.load(open(os.path.join(chats_dir(), f), encoding="utf8"))
|
| 182 |
+
out.append({k: c.get(k) for k in ("id", "title", "model", "created", "updated")} | {"n": len(c.get("messages", []))})
|
| 183 |
+
except (OSError, ValueError):
|
| 184 |
+
continue
|
| 185 |
+
return sorted(out, key=lambda c: c.get("updated") or 0, reverse=True)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@app.get("/api/chats/{cid}")
|
| 189 |
+
def chat_get(cid: str):
|
| 190 |
+
try:
|
| 191 |
+
return json.load(open(_chat_path(cid), encoding="utf8"))
|
| 192 |
+
except FileNotFoundError:
|
| 193 |
+
raise HTTPException(404, "no such conversation")
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
@app.put("/api/chats/{cid}")
|
| 197 |
+
async def chat_put(cid: str, request: Request):
|
| 198 |
+
p = _chat_path(cid)
|
| 199 |
+
raw = await request.body()
|
| 200 |
+
if len(raw) > CHAT_MAX:
|
| 201 |
+
raise HTTPException(413, "conversation too large")
|
| 202 |
+
try:
|
| 203 |
+
c = json.loads(raw)
|
| 204 |
+
assert isinstance(c, dict) and isinstance(c.get("messages"), list)
|
| 205 |
+
except (ValueError, AssertionError):
|
| 206 |
+
raise HTTPException(400, "not a conversation")
|
| 207 |
+
c["id"] = cid
|
| 208 |
+
with open(p + ".tmp", "w", encoding="utf8") as f:
|
| 209 |
+
json.dump(c, f, ensure_ascii=False)
|
| 210 |
+
os.replace(p + ".tmp", p)
|
| 211 |
+
return {"ok": True}
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
@app.delete("/api/chats/{cid}")
|
| 215 |
+
def chat_delete(cid: str):
|
| 216 |
+
try:
|
| 217 |
+
os.remove(_chat_path(cid))
|
| 218 |
+
except FileNotFoundError:
|
| 219 |
+
pass
|
| 220 |
+
return {"ok": True}
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
@app.delete("/api/chats")
|
| 224 |
+
def chats_delete_all():
|
| 225 |
+
n = 0
|
| 226 |
+
for f in os.listdir(chats_dir()):
|
| 227 |
+
if f.endswith(".json"):
|
| 228 |
+
try:
|
| 229 |
+
os.remove(os.path.join(chats_dir(), f))
|
| 230 |
+
n += 1
|
| 231 |
+
except OSError:
|
| 232 |
+
pass
|
| 233 |
+
return {"ok": True, "deleted": n}
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def start_generation(msgs, kw):
|
| 237 |
+
"""Run stream_chat in a worker thread; returns (asyncio queue of deltas/stats/exceptions, None at the end; stop
|
| 238 |
+
event). Called on the event loop: the worker hands its items over to it, so no thread waits per queued request."""
|
| 239 |
+
loop, q, stop = asyncio.get_running_loop(), asyncio.Queue(), threading.Event()
|
| 240 |
+
|
| 241 |
+
def put(x):
|
| 242 |
+
try:
|
| 243 |
+
loop.call_soon_threadsafe(q.put_nowait, x)
|
| 244 |
+
except RuntimeError: # the event loop is gone (the server is stopping)
|
| 245 |
+
stop.set()
|
| 246 |
+
|
| 247 |
+
def work():
|
| 248 |
+
try:
|
| 249 |
+
with lock:
|
| 250 |
+
if stop.is_set(): # the client left while this request waited for the NPU
|
| 251 |
+
return
|
| 252 |
+
set_priority(True)
|
| 253 |
+
try:
|
| 254 |
+
for d in pipe.stream_chat(msgs, should_stop=stop.is_set, split_reasoning=True, **kw):
|
| 255 |
+
put(d)
|
| 256 |
+
finally:
|
| 257 |
+
set_priority(False)
|
| 258 |
+
except Exception as e: # surfaced to the client
|
| 259 |
+
put(e)
|
| 260 |
+
finally:
|
| 261 |
+
put(None)
|
| 262 |
+
threading.Thread(target=work, daemon=True).start()
|
| 263 |
+
return q, stop
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def reader(q, stop, request=None):
|
| 267 |
+
"""-> async function returning start_generation's next item. With request (a whole response, not a stream): a
|
| 268 |
+
client that leaves meanwhile stops the generation (checked about once a second; a stream stops when it closes)."""
|
| 269 |
+
last = [time.monotonic()]
|
| 270 |
+
|
| 271 |
+
async def next_item():
|
| 272 |
+
while request is not None and not stop.is_set():
|
| 273 |
+
wait = last[0] + 1.0 - time.monotonic()
|
| 274 |
+
if wait <= 0:
|
| 275 |
+
last[0] = time.monotonic()
|
| 276 |
+
if await request.is_disconnected():
|
| 277 |
+
stop.set()
|
| 278 |
+
continue
|
| 279 |
+
try:
|
| 280 |
+
return await asyncio.wait_for(q.get(), wait)
|
| 281 |
+
except asyncio.TimeoutError:
|
| 282 |
+
pass
|
| 283 |
+
return await q.get()
|
| 284 |
+
return next_item
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
MAX_IMAGE = 20 << 20 # bytes
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def _public_host(url):
|
| 291 |
+
"""True when every address the URL's host resolves to is a public one: the server does not fetch from the local
|
| 292 |
+
network on a client's behalf (ONW_ALLOW_LOCAL_IMAGES=1 allows it)."""
|
| 293 |
+
if os.environ.get("ONW_ALLOW_LOCAL_IMAGES") == "1":
|
| 294 |
+
return True
|
| 295 |
+
host = urllib.parse.urlsplit(url).hostname
|
| 296 |
+
try:
|
| 297 |
+
addrs = {a[4][0] for a in socket.getaddrinfo(host, None)}
|
| 298 |
+
except (socket.gaierror, UnicodeError, TypeError):
|
| 299 |
+
return False
|
| 300 |
+
return bool(addrs) and all(ipaddress.ip_address(a.split("%")[0]).is_global for a in addrs)
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def load_image(url):
|
| 304 |
+
try:
|
| 305 |
+
if url.startswith("data:"):
|
| 306 |
+
data = base64.b64decode(url.split(",", 1)[1])
|
| 307 |
+
elif url.startswith(("http://", "https://")):
|
| 308 |
+
if not _public_host(url):
|
| 309 |
+
raise HTTPException(400, "image_url: only public http(s) addresses are fetched (send a data: URL instead)")
|
| 310 |
+
req = urllib.request.Request(url, headers={"User-Agent": f"onw/{__version__}"})
|
| 311 |
+
with urllib.request.urlopen(req, timeout=20) as r:
|
| 312 |
+
data = r.read(MAX_IMAGE + 1)
|
| 313 |
+
if len(data) > MAX_IMAGE:
|
| 314 |
+
raise HTTPException(400, "image_url: the image is larger than 20 MB")
|
| 315 |
+
else:
|
| 316 |
+
raise HTTPException(400, "image_url must be a data: URL or http(s) URL")
|
| 317 |
+
return Image.open(io.BytesIO(data)).convert("RGB")
|
| 318 |
+
except HTTPException:
|
| 319 |
+
raise
|
| 320 |
+
except Exception as e: # bad base64, not an image, unreachable URL
|
| 321 |
+
raise HTTPException(400, f"image_url: {type(e).__name__}: {e}")
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def to_hf_messages(messages):
|
| 325 |
+
"""OpenAI chat messages -> HF chat format with PIL images."""
|
| 326 |
+
out = []
|
| 327 |
+
for m in tool_calls.history(messages):
|
| 328 |
+
keep = {k: m[k] for k in ("tool_calls", "tool_call_id", "name", "reasoning_content") if m.get(k) is not None}
|
| 329 |
+
c = m.get("content")
|
| 330 |
+
if c is None or isinstance(c, str):
|
| 331 |
+
out.append({"role": m["role"], "content": c or "", **keep})
|
| 332 |
+
continue
|
| 333 |
+
parts = []
|
| 334 |
+
for p in c:
|
| 335 |
+
if p.get("type") == "text":
|
| 336 |
+
parts.append({"type": "text", "text": p.get("text") or ""})
|
| 337 |
+
elif p.get("type") == "image_url":
|
| 338 |
+
iu = p.get("image_url") or {}
|
| 339 |
+
url = iu.get("url") if isinstance(iu, dict) else iu
|
| 340 |
+
if not isinstance(url, str):
|
| 341 |
+
raise HTTPException(400, "image_url part without a url")
|
| 342 |
+
parts.append({"type": "image", "image": load_image(url)})
|
| 343 |
+
else:
|
| 344 |
+
raise HTTPException(400, f"unsupported content part type: {p.get('type')}")
|
| 345 |
+
out.append({"role": m["role"], "content": parts, **keep})
|
| 346 |
+
return out
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def _num(body, key, default, cast=float):
|
| 350 |
+
v = body.get(key)
|
| 351 |
+
if v is None:
|
| 352 |
+
return default
|
| 353 |
+
try:
|
| 354 |
+
return cast(v)
|
| 355 |
+
except (TypeError, ValueError):
|
| 356 |
+
raise HTTPException(400, f"{key} must be a number")
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
LEVELS = ("off", "low", "medium", "high") # the thinking levels of the window and the chat page (reasoning_effort)
|
| 360 |
+
_PREFS = [None, None] # (config mtime, this model's defaults)
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def model_defaults():
|
| 364 |
+
"""This model's defaults from the onw window (config model_prefs, read again when the file changes - no restart):
|
| 365 |
+
{"think": level, "sampling": "greedy" | "recommended" | "custom", values...}; {} when none were saved. Settings
|
| 366 |
+
of earlier versions (ONW_THINK_DEFAULT, ONW_API_SAMPLING) apply where these say nothing."""
|
| 367 |
+
key = os.environ.get("ONW_MODEL_KEY")
|
| 368 |
+
if not key:
|
| 369 |
+
return {}
|
| 370 |
+
try:
|
| 371 |
+
from onw import app as _app
|
| 372 |
+
p = os.path.join(_app.config_dir(), "config.json")
|
| 373 |
+
mt = os.path.getmtime(p)
|
| 374 |
+
if _PREFS[0] != mt:
|
| 375 |
+
_PREFS[:] = [mt, (_app.load_config().get("model_prefs") or {}).get(key) or {}]
|
| 376 |
+
return _PREFS[1]
|
| 377 |
+
except (OSError, ValueError):
|
| 378 |
+
return {}
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def default_think():
|
| 382 |
+
"""Thinking for a request that says nothing about it: the model's saved level, else the old switch."""
|
| 383 |
+
lvl = model_defaults().get("think")
|
| 384 |
+
if lvl == "off":
|
| 385 |
+
return False
|
| 386 |
+
if lvl in LEVELS:
|
| 387 |
+
return lvl
|
| 388 |
+
return True if os.environ.get("ONW_THINK_DEFAULT") == "1" else None
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
def default_sampling():
|
| 392 |
+
"""Sampling for a request that sends none: the model's saved choice, else the old setting (API_SAMPLING)."""
|
| 393 |
+
d = model_defaults()
|
| 394 |
+
mode = d.get("sampling")
|
| 395 |
+
if mode == "greedy":
|
| 396 |
+
return None
|
| 397 |
+
if mode == "recommended":
|
| 398 |
+
return {k: v for k, v in (info.get("sampling") or {}).items() if k != "source"} or None
|
| 399 |
+
if mode == "custom":
|
| 400 |
+
return {k: d[k] for k in ("temperature", "top_p", "top_k", "presence_penalty") if k in d}
|
| 401 |
+
return API_SAMPLING
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def gen_args(body):
|
| 405 |
+
"""OpenAI request fields -> ChatEngine.stream_chat arguments."""
|
| 406 |
+
if _num(body, "n", 1, int) != 1:
|
| 407 |
+
raise HTTPException(400, "n > 1 is not supported (one NPU pipeline)")
|
| 408 |
+
stop = body.get("stop") or []
|
| 409 |
+
mx = body.get("max_completion_tokens", body.get("max_tokens"))
|
| 410 |
+
# a request without max_tokens: the setting (ONW_MAX_TOKENS; 0 = up to the end of the context - the engine stops
|
| 411 |
+
# there anyway); agents send none and write whole files in one answer
|
| 412 |
+
if mx is None:
|
| 413 |
+
mx = int(os.environ.get("ONW_MAX_TOKENS", "8192") or 0) or (1 << 30)
|
| 414 |
+
else:
|
| 415 |
+
mx = _num({"max_tokens": mx}, "max_tokens", 512, int)
|
| 416 |
+
if mx < 1:
|
| 417 |
+
raise HTTPException(400, "max_tokens must be at least 1")
|
| 418 |
+
think = (body.get("chat_template_kwargs") or {}).get("enable_thinking")
|
| 419 |
+
effort = body.get("reasoning_effort")
|
| 420 |
+
if isinstance(effort, str): # a level: low / medium / high (gpt-oss, llm-jp: trained;
|
| 421 |
+
think = False if effort in ("none", "minimal", "off") else effort # others: a thinking budget)
|
| 422 |
+
if think is None: # nothing said: the window's default for this model
|
| 423 |
+
think = default_think()
|
| 424 |
+
tools, choice = body.get("tools") or None, body.get("tool_choice")
|
| 425 |
+
if choice == "none":
|
| 426 |
+
tools = None
|
| 427 |
+
elif isinstance(choice, dict) and tools: # a named function: offer just that one
|
| 428 |
+
name = (choice.get("function") or {}).get("name")
|
| 429 |
+
tools = [t for t in tools if (t.get("function") or {}).get("name") == name] or tools
|
| 430 |
+
# what the request leaves out: greedy, or the settings' API defaults (API_SAMPLING; thinking has its own set)
|
| 431 |
+
dflt = dict(default_sampling() or {})
|
| 432 |
+
if think and isinstance(dflt.get("think"), dict):
|
| 433 |
+
dflt.update(dflt["think"])
|
| 434 |
+
return dict(max_new_tokens=mx, temperature=_num(body, "temperature", dflt.get("temperature", 0.0)),
|
| 435 |
+
top_p=_num(body, "top_p", dflt.get("top_p", 1.0)),
|
| 436 |
+
stop=[stop] if isinstance(stop, str) else list(stop), seed=body.get("seed"), think=think,
|
| 437 |
+
top_k=_num(body, "top_k", dflt.get("top_k", 0), int), repetition_penalty=_num(body, "repetition_penalty", 1.0),
|
| 438 |
+
presence_penalty=_num(body, "presence_penalty", dflt.get("presence_penalty", 0.0)),
|
| 439 |
+
frequency_penalty=_num(body, "frequency_penalty", 0.0), tools=tools)
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
def with_format(msgs, body):
|
| 443 |
+
"""response_format json_object / json_schema: best effort - an instruction as a system message (the output is not
|
| 444 |
+
constrained)."""
|
| 445 |
+
rf = body.get("response_format") or {}
|
| 446 |
+
kind = rf.get("type") if isinstance(rf, dict) else None
|
| 447 |
+
if kind not in ("json_object", "json_schema"):
|
| 448 |
+
return msgs
|
| 449 |
+
note = "Respond with a single JSON object only, no other text."
|
| 450 |
+
if kind == "json_schema":
|
| 451 |
+
note += " It must follow this JSON schema: " + json.dumps((rf.get("json_schema") or {}).get("schema") or {},
|
| 452 |
+
ensure_ascii=False)
|
| 453 |
+
if msgs and msgs[0]["role"] == "system" and isinstance(msgs[0]["content"], str):
|
| 454 |
+
return [dict(msgs[0], content=msgs[0]["content"] + "\n\n" + note)] + msgs[1:]
|
| 455 |
+
return [{"role": "system", "content": note}] + msgs
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
@app.get("/")
|
| 459 |
+
def index(request: Request):
|
| 460 |
+
base = str(request.base_url).rstrip("/")
|
| 461 |
+
return {"object": "onw.server", "name": "onw - 俺のNPUがこんなに動くわけない", "version": __version__, "model": MODEL_ID,
|
| 462 |
+
"openai_base_url": base + "/v1", "api_key_required": bool(API_KEY),
|
| 463 |
+
"endpoints": {"models": "/v1/models", "chat_completions": "/v1/chat/completions", "health": "/health",
|
| 464 |
+
"chat_ui": "/chat", "check_page": "/check", "info": "/api/info"},
|
| 465 |
+
"context": info.get("max_context"), "vision": info.get("vision")}
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
@app.get("/health")
|
| 469 |
+
def health():
|
| 470 |
+
return {"status": "ok", "model": MODEL_ID}
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
@app.get("/chat")
|
| 474 |
+
def chat_ui():
|
| 475 |
+
return HTMLResponse(i18n.page(os.path.join(WEB, "index.html")))
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
@app.get("/check")
|
| 479 |
+
def check_page():
|
| 480 |
+
return HTMLResponse(i18n.page(os.path.join(WEB, "check.html")))
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
@app.get("/api/info")
|
| 484 |
+
def api_info():
|
| 485 |
+
# the window's defaults for this model, read now (they change without a restart)
|
| 486 |
+
d, lvl = model_defaults(), default_think()
|
| 487 |
+
samp = default_sampling()
|
| 488 |
+
mode = d.get("sampling") or ("custom" if API_SAMPLING and os.environ.get("ONW_API_SAMPLING") != "recommended"
|
| 489 |
+
else "recommended" if API_SAMPLING else "greedy")
|
| 490 |
+
return {**info, "api_sampling": samp, "api_sampling_mode": mode,
|
| 491 |
+
"think_default": "off" if lvl is False else ("medium" if lvl is True else lvl)}
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
STARTED = int(time.time())
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
def _model_card():
|
| 498 |
+
return {"id": MODEL_ID, "object": "model", "created": STARTED, "owned_by": "local-npu"}
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
@app.get("/v1/models")
|
| 502 |
+
def models():
|
| 503 |
+
return {"object": "list", "data": [_model_card()]}
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
@app.get("/v1/models/{model_id:path}")
|
| 507 |
+
def model(model_id: str):
|
| 508 |
+
if model_id != MODEL_ID:
|
| 509 |
+
raise HTTPException(404, f"model {model_id!r} not found (this server runs {MODEL_ID!r})")
|
| 510 |
+
return _model_card()
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
def finalize(text, tools, finish):
|
| 514 |
+
"""The answer's end: tool calls parsed out of the text (OpenAI tool_calls), finish_reason "tool_calls" when there
|
| 515 |
+
are any. -> (text, calls, finish_reason)."""
|
| 516 |
+
if not tools:
|
| 517 |
+
return text, [], finish
|
| 518 |
+
text, calls = tool_calls.parse(text, tools)
|
| 519 |
+
return text, calls, ("tool_calls" if calls else finish)
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def usage_of(st):
|
| 523 |
+
return {"prompt_tokens": st["prompt_tokens"], "completion_tokens": st["completion_tokens"],
|
| 524 |
+
"total_tokens": st["prompt_tokens"] + st["completion_tokens"],
|
| 525 |
+
"prompt_tokens_details": {"cached_tokens": st.get("cached_tokens", 0)},
|
| 526 |
+
# (OpenAI's field: how many of the completion tokens were thinking)
|
| 527 |
+
"completion_tokens_details": {"reasoning_tokens": st.get("reasoning_tokens", 0)}}
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
def stats_of(st):
|
| 531 |
+
return {k: (round(v, 2) if isinstance(v, float) else v) for k, v in st.items() if k != "finish_reason"}
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
def worker_error(e):
|
| 535 |
+
"""A generation error -> (HTTP status, OpenAI error type): the request's fault (a prompt too long, an image for a
|
| 536 |
+
text-only model) or the server's (the NPU, the driver)."""
|
| 537 |
+
return (400, "invalid_request_error") if isinstance(e, (ValueError, KeyError, TypeError)) else (500, "server_error")
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
@app.post("/v1/chat/completions")
|
| 541 |
+
async def chat(request: Request):
|
| 542 |
+
try:
|
| 543 |
+
body = await request.json()
|
| 544 |
+
except ValueError:
|
| 545 |
+
raise HTTPException(400, "the request body is not valid JSON")
|
| 546 |
+
if not isinstance(body, dict) or not isinstance(body.get("messages"), list):
|
| 547 |
+
raise HTTPException(400, "messages (a list) is required")
|
| 548 |
+
kw = gen_args(body)
|
| 549 |
+
loop = asyncio.get_running_loop()
|
| 550 |
+
try: # images may be downloaded: not on the event loop
|
| 551 |
+
msgs = await loop.run_in_executor(None, to_hf_messages, body["messages"])
|
| 552 |
+
except (KeyError, TypeError, AttributeError, ValueError) as e:
|
| 553 |
+
raise HTTPException(400, f"malformed messages: {e}")
|
| 554 |
+
msgs = with_format(msgs, body)
|
| 555 |
+
cid, created = f"chatcmpl-{uuid.uuid4().hex[:24]}", int(time.time())
|
| 556 |
+
tools = kw["tools"]
|
| 557 |
+
if body.get("stream"):
|
| 558 |
+
def chunk(delta, finish=None, **extra):
|
| 559 |
+
d = {"id": cid, "object": "chat.completion.chunk", "created": created, "model": MODEL_ID,
|
| 560 |
+
"choices": [{"index": 0, "delta": delta, "logprobs": None, "finish_reason": finish}], **extra}
|
| 561 |
+
return f"data: {json.dumps(d, ensure_ascii=False)}\n\n"
|
| 562 |
+
|
| 563 |
+
async def events():
|
| 564 |
+
full, sent = "", 0 # with tools: text is held back once a call may start
|
| 565 |
+
# (started here, not before the response: one whose stream never began would generate on to max_tokens)
|
| 566 |
+
q, stop = start_generation(msgs, kw)
|
| 567 |
+
next_item = reader(q, stop)
|
| 568 |
+
try:
|
| 569 |
+
yield chunk({"role": "assistant", "content": ""})
|
| 570 |
+
while True:
|
| 571 |
+
d = await next_item()
|
| 572 |
+
if d is None:
|
| 573 |
+
break
|
| 574 |
+
if isinstance(d, Exception):
|
| 575 |
+
status, typ = worker_error(d)
|
| 576 |
+
err = {"error": {"message": str(d), "type": typ, "param": None,
|
| 577 |
+
"code": "context_length_exceeded" if TOO_LONG.match(str(d)) else None}}
|
| 578 |
+
yield f"data: {json.dumps(err, ensure_ascii=False)}\n\n"
|
| 579 |
+
elif isinstance(d, dict) and "reasoning" in d:
|
| 580 |
+
yield chunk({"reasoning_content": d["reasoning"]})
|
| 581 |
+
elif isinstance(d, dict):
|
| 582 |
+
rest, calls, finish = finalize(full, tools, d["finish_reason"])
|
| 583 |
+
if tools and rest.startswith(full[:sent]) and rest[sent:].strip(): # text after the calls
|
| 584 |
+
yield chunk({"content": rest[sent:]})
|
| 585 |
+
for i, tc in enumerate(calls):
|
| 586 |
+
yield chunk({"tool_calls": [dict(tc, index=i)]})
|
| 587 |
+
yield chunk({}, finish, onw_stats=stats_of(d))
|
| 588 |
+
if (body.get("stream_options") or {}).get("include_usage"): # OpenAI: its own last chunk
|
| 589 |
+
yield f"data: {json.dumps({'id': cid, 'object': 'chat.completion.chunk', 'created': created, 'model': MODEL_ID, 'choices': [], 'usage': usage_of(d)})}\n\n"
|
| 590 |
+
elif tools:
|
| 591 |
+
full += d
|
| 592 |
+
upto = tool_calls.safe_upto(full)
|
| 593 |
+
if upto > sent:
|
| 594 |
+
yield chunk({"content": full[sent:upto]})
|
| 595 |
+
sent = upto
|
| 596 |
+
else:
|
| 597 |
+
yield chunk({"content": d})
|
| 598 |
+
yield "data: [DONE]\n\n"
|
| 599 |
+
finally: # client disconnected or finished: stop the NPU loop so the lock is released
|
| 600 |
+
stop.set()
|
| 601 |
+
return StreamingResponse(events(), media_type="text/event-stream",
|
| 602 |
+
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
| 603 |
+
|
| 604 |
+
parts, thought, st = [], [], None
|
| 605 |
+
q, stop = start_generation(msgs, kw)
|
| 606 |
+
next_item = reader(q, stop, request) # (a client that leaves stops the generation)
|
| 607 |
+
try:
|
| 608 |
+
while True:
|
| 609 |
+
d = await next_item()
|
| 610 |
+
if d is None:
|
| 611 |
+
break
|
| 612 |
+
if isinstance(d, Exception):
|
| 613 |
+
status, typ = worker_error(d)
|
| 614 |
+
return oai_error(status, str(d), typ)
|
| 615 |
+
if isinstance(d, dict) and "reasoning" in d:
|
| 616 |
+
thought.append(d["reasoning"])
|
| 617 |
+
elif isinstance(d, dict):
|
| 618 |
+
st = d
|
| 619 |
+
else:
|
| 620 |
+
parts.append(d)
|
| 621 |
+
finally:
|
| 622 |
+
stop.set()
|
| 623 |
+
if st is None:
|
| 624 |
+
return oai_error(500, "the generation ended without a result", "server_error")
|
| 625 |
+
text, calls, finish = finalize("".join(parts), tools, st["finish_reason"])
|
| 626 |
+
msg = {"role": "assistant", "content": (text.strip() or None) if tools else text}
|
| 627 |
+
if thought:
|
| 628 |
+
msg["reasoning_content"] = "".join(thought)
|
| 629 |
+
if calls:
|
| 630 |
+
msg["tool_calls"] = calls
|
| 631 |
+
return JSONResponse({
|
| 632 |
+
"id": cid, "object": "chat.completion", "created": created, "model": MODEL_ID,
|
| 633 |
+
"choices": [{"index": 0, "message": msg, "logprobs": None, "finish_reason": finish}],
|
| 634 |
+
"usage": usage_of(st),
|
| 635 |
+
"onw_stats": stats_of(st),
|
| 636 |
+
})
|
| 637 |
+
|
| 638 |
+
|
| 639 |
+
def recommended_sampling(arch, engine_dir=None):
|
| 640 |
+
"""The chat page's starting sampling values ({temperature, top_p, top_k, presence_penalty?, think?, source}) or
|
| 641 |
+
None (greedy): the model's own generation_config.json first (Gemma 4, Granite 4.2), else onw/sampling.json by
|
| 642 |
+
model_type prefix. The API itself stays greedy unless asked."""
|
| 643 |
+
keys = ("temperature", "top_p", "top_k")
|
| 644 |
+
try:
|
| 645 |
+
with open(os.path.join(engine_dir, "generation_config.json"), encoding="utf8") as f:
|
| 646 |
+
g = json.load(f)
|
| 647 |
+
if g.get("do_sample") and any(k in g for k in keys):
|
| 648 |
+
return {"source": "model", "temperature": g.get("temperature", 1.0), "top_p": g.get("top_p", 1.0),
|
| 649 |
+
"top_k": g.get("top_k", 0)}
|
| 650 |
+
except (OSError, ValueError, TypeError):
|
| 651 |
+
pass
|
| 652 |
+
try:
|
| 653 |
+
with open(os.path.join(WEB, "..", "sampling.json"), encoding="utf8") as f:
|
| 654 |
+
table = {k: v for k, v in json.load(f).items() if not k.startswith("_")}
|
| 655 |
+
except (OSError, ValueError):
|
| 656 |
+
return None
|
| 657 |
+
hit = max((k for k in table if arch.startswith(k)), key=len, default=None)
|
| 658 |
+
if hit is None:
|
| 659 |
+
return None
|
| 660 |
+
return {k: v for k, v in table[hit].items() if k != "note"}
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
from onw import compat # noqa: E402,F401 (/v1/messages and /v1/responses on this app)
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
def set_priority(busy):
|
| 667 |
+
"""Windows: the server's priority class - normal while loading and waiting, above normal while it generates
|
| 668 |
+
(busy). Windows puts a below-normal process on the low-power cores at low clocks (onw 1.4.5 and earlier started
|
| 669 |
+
the server below normal: Lunar Lake, Qwen3.5-9B NF4 with MTP ~12 tok/s, 16 above normal); generating takes one or two
|
| 670 |
+
cores, so the rest of the PC hardly notices. ONW_PRIORITY: the class while generating, normal | above (default) |
|
| 671 |
+
off (leave it as started)."""
|
| 672 |
+
want = os.environ.get("ONW_PRIORITY", "above")
|
| 673 |
+
if os.name != "nt" or want not in ("normal", "above"):
|
| 674 |
+
return
|
| 675 |
+
import ctypes
|
| 676 |
+
k32 = ctypes.WinDLL("kernel32")
|
| 677 |
+
k32.GetCurrentProcess.restype = ctypes.c_void_p
|
| 678 |
+
k32.SetPriorityClass.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
|
| 679 |
+
k32.SetPriorityClass(k32.GetCurrentProcess(), 0x8000 if busy and want == "above" else 0x20)
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
def main(argv=None):
|
| 683 |
+
global pipe, MODEL_ID, API_KEY, API_SAMPLING
|
| 684 |
+
ap = argparse.ArgumentParser(prog="onw serve", description="chat UI + OpenAI-compatible API on the Intel NPU")
|
| 685 |
+
ap.add_argument("model", help="onw model folder, or a Hugging Face repo id (downloaded into ./models)")
|
| 686 |
+
ap.add_argument("--host", default="127.0.0.1")
|
| 687 |
+
ap.add_argument("--port", type=int, default=8000)
|
| 688 |
+
ap.add_argument("--device", default="NPU", help="run every component on this device (CPU works without an NPU driver)")
|
| 689 |
+
ap.add_argument("--name", help="model id / title (default: engine directory name)")
|
| 690 |
+
ap.add_argument("--context", type=int, default=None,
|
| 691 |
+
help="context length in tokens (default 32768, at most the model's maximum); no recompile")
|
| 692 |
+
ap.add_argument("--open", action="store_true", help="open the chat UI in the default browser once loaded")
|
| 693 |
+
ap.add_argument("--no-pld", action="store_true", help="disable prompt lookup decoding")
|
| 694 |
+
ap.add_argument("--api-key", help="require this key as a Bearer token on /v1 from other machines "
|
| 695 |
+
"(e.g. with --host 0.0.0.0)")
|
| 696 |
+
a = ap.parse_args(argv)
|
| 697 |
+
API_KEY = a.api_key or os.environ.get("ONW_API_KEY") or None
|
| 698 |
+
from onw.hub import resolve
|
| 699 |
+
a.engine = resolve(a.model)
|
| 700 |
+
import openvino as ov
|
| 701 |
+
from . import npukit
|
| 702 |
+
from onw.runtime import npu_info
|
| 703 |
+
print(f"[onw] {__version__} | OpenVINO {ov.__version__} | {npu_info(npukit.core())}", flush=True)
|
| 704 |
+
if a.device == "NPU" and "NPU" not in npukit.core().available_devices:
|
| 705 |
+
print("WARNING: no NPU found (is the NPU driver installed?) - running everything on the CPU instead.", flush=True)
|
| 706 |
+
a.device = "CPU"
|
| 707 |
+
from onw.chat import ChatEngine
|
| 708 |
+
pipe = ChatEngine(a.engine, a.device, context=a.context, pld=not a.no_pld)
|
| 709 |
+
set_priority(False)
|
| 710 |
+
MODEL_ID = a.name or os.path.basename(os.path.normpath(a.engine))
|
| 711 |
+
v = pipe.vision
|
| 712 |
+
info.update(model=MODEL_ID, title=MODEL_ID, devices={"lm": a.device}, fast=False, vision=bool(v),
|
| 713 |
+
image_px=v.get("image_px") if v else None,
|
| 714 |
+
think=pipe.think_style is not None, max_context=pipe.max_context,
|
| 715 |
+
always_thinks=pipe.think_style == "harmony", # (gpt-oss, llm-jp: the thinking counts against max_tokens)
|
| 716 |
+
note=f"文脈は約{pipe.max_context}トークン", context=pipe.max_context, pld=pipe.pld,
|
| 717 |
+
mtp=getattr(pipe.model, "mtp", None) is not None)
|
| 718 |
+
try:
|
| 719 |
+
with open(os.path.join(a.engine, "config.json"), encoding="utf8") as f:
|
| 720 |
+
arch = json.load(f).get("model_type") or ""
|
| 721 |
+
except (OSError, ValueError):
|
| 722 |
+
arch = ""
|
| 723 |
+
# greedy Gemma on the NPU can fall into repeating one word (fp16 on the NPU tips a near tie; the same happens with
|
| 724 |
+
# any onw version): the pages suggest a repetition penalty, the API default stays 1.0 (OpenAI behaviour)
|
| 725 |
+
info.update(arch=arch, rep_hint=1.2 if arch.startswith("gemma") else None, sampling=recommended_sampling(arch, a.engine))
|
| 726 |
+
want = os.environ.get("ONW_API_SAMPLING")
|
| 727 |
+
if want == "recommended":
|
| 728 |
+
API_SAMPLING = {k: v for k, v in (info["sampling"] or {}).items() if k != "source"} or None
|
| 729 |
+
elif want:
|
| 730 |
+
try:
|
| 731 |
+
API_SAMPLING = json.loads(want)
|
| 732 |
+
except ValueError:
|
| 733 |
+
API_SAMPLING = None
|
| 734 |
+
info.update(api_sampling=API_SAMPLING, api_sampling_mode="recommended" if want == "recommended" else
|
| 735 |
+
"custom" if API_SAMPLING else "greedy")
|
| 736 |
+
if API_SAMPLING:
|
| 737 |
+
print(f"[api] requests without sampling values use {API_SAMPLING}", flush=True)
|
| 738 |
+
if a.open:
|
| 739 |
+
import webbrowser
|
| 740 |
+
threading.Timer(1.5, webbrowser.open, [f"http://{'localhost' if a.host in ('0.0.0.0', '127.0.0.1') else a.host}:{a.port}/chat"]).start()
|
| 741 |
+
print(f"\n OpenAI API: http://localhost:{a.port}/v1 (model id: {MODEL_ID})"
|
| 742 |
+
f"\n Chat UI: http://localhost:{a.port}/chat\n Check page: http://localhost:{a.port}/check\n", flush=True)
|
| 743 |
+
uvicorn.run(app, host=a.host, port=a.port, log_level="warning")
|
| 744 |
+
|
| 745 |
+
|
| 746 |
+
if __name__ == "__main__":
|
| 747 |
+
# (python -m onw.server: run the module that onw.compat put its routes on - this one is a second copy of it)
|
| 748 |
+
from onw.server import main as _main
|
| 749 |
+
_main()
|
onw/update.py
CHANGED
|
@@ -86,8 +86,27 @@ def apply(tag, progress=lambda text: None):
|
|
| 86 |
d = install_dir()
|
| 87 |
if d is None:
|
| 88 |
raise RuntimeError("this onw is not an installer-made install (update it with git / hf instead)")
|
| 89 |
-
info = json.loads(_get(f"https://huggingface.co/api/models/{REPO}/revision/{tag}"))
|
| 90 |
files = [s["rfilename"] for s in info["siblings"] if s["rfilename"] != ".gitattributes"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
stage = os.path.join(d, ".update")
|
| 92 |
shutil.rmtree(stage, ignore_errors=True)
|
| 93 |
|
|
@@ -118,6 +137,12 @@ def apply(tag, progress=lambda text: None):
|
|
| 118 |
break
|
| 119 |
except PermissionError:
|
| 120 |
if i == 9:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
raise
|
| 122 |
time.sleep(0.5)
|
| 123 |
# modules the new version no longer has: stale code must not be imported by accident
|
|
|
|
| 86 |
d = install_dir()
|
| 87 |
if d is None:
|
| 88 |
raise RuntimeError("this onw is not an installer-made install (update it with git / hf instead)")
|
| 89 |
+
info = json.loads(_get(f"https://huggingface.co/api/models/{REPO}/revision/{tag}?blobs=true"))
|
| 90 |
files = [s["rfilename"] for s in info["siblings"] if s["rfilename"] != ".gitattributes"]
|
| 91 |
+
# [3.0] the NPU kit of the other system is not downloaded (Windows: the DLLs, Linux: npukit/linux)
|
| 92 |
+
if os.name == "nt":
|
| 93 |
+
files = [f for f in files if not f.startswith("onw/npukit/linux/")]
|
| 94 |
+
else:
|
| 95 |
+
files = [f for f in files if not (f.startswith("onw/npukit/") and f.endswith(".dll"))]
|
| 96 |
+
# [3.0] large (LFS) files that are already here unchanged - the NPU kit's ~92 MB - are not downloaded again
|
| 97 |
+
lfs = {s["rfilename"]: s["lfs"] for s in info["siblings"] if s.get("lfs")}
|
| 98 |
+
|
| 99 |
+
def same(f):
|
| 100 |
+
meta, dst = lfs.get(f), os.path.join(d, f)
|
| 101 |
+
if not meta or not os.path.exists(dst) or os.path.getsize(dst) != meta.get("size"):
|
| 102 |
+
return False
|
| 103 |
+
import hashlib
|
| 104 |
+
h = hashlib.sha256()
|
| 105 |
+
with open(dst, "rb") as fh:
|
| 106 |
+
for chunk in iter(lambda: fh.read(1 << 22), b""):
|
| 107 |
+
h.update(chunk)
|
| 108 |
+
return h.hexdigest() == meta.get("sha256")
|
| 109 |
+
files = [f for f in files if not same(f)]
|
| 110 |
stage = os.path.join(d, ".update")
|
| 111 |
shutil.rmtree(stage, ignore_errors=True)
|
| 112 |
|
|
|
|
| 137 |
break
|
| 138 |
except PermissionError:
|
| 139 |
if i == 9:
|
| 140 |
+
if f.lower().endswith(".dll") and os.path.exists(dst):
|
| 141 |
+
# a DLL loaded by a running onw process (the NPU kit): it can be renamed, not replaced -
|
| 142 |
+
# moved aside (removed at the next start, see npukit) and the new one put in its place
|
| 143 |
+
os.replace(dst, f"{dst}.old-{os.getpid()}-{int(time.time())}")
|
| 144 |
+
os.replace(os.path.join(stage, f), dst)
|
| 145 |
+
break
|
| 146 |
raise
|
| 147 |
time.sleep(0.5)
|
| 148 |
# modules the new version no longer has: stale code must not be imported by accident
|
pyproject.toml
CHANGED
|
@@ -4,13 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "onw"
|
| 7 |
-
version = "
|
| 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"}
|
| 11 |
requires-python = ">=3.10"
|
| 12 |
# openvino: from https://storage.openvinotoolkit.org/simple/wheels/pre-release until 2026.5 is on PyPI
|
| 13 |
-
|
|
|
|
| 14 |
"huggingface_hub>=1.0,<2", "jinja2"]
|
| 15 |
|
| 16 |
[project.optional-dependencies]
|
|
@@ -24,4 +25,4 @@ onw = "onw.__main__:main"
|
|
| 24 |
include = ["onw*"]
|
| 25 |
|
| 26 |
[tool.setuptools.package-data]
|
| 27 |
-
onw = ["web/*"]
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "onw"
|
| 7 |
+
version = "3.0.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"}
|
| 11 |
requires-python = ">=3.10"
|
| 12 |
# openvino: from https://storage.openvinotoolkit.org/simple/wheels/pre-release until 2026.5 is on PyPI
|
| 13 |
+
# [3.0] exactly the OpenVINO build onw's NPU kit (onw/npukit) is made for (2026.5.0b1 = 2026.5.0-23160-6db412d0a27)
|
| 14 |
+
dependencies = ["openvino==2026.5.0b1", "transformers>=5.5.4,<6", "numpy>=1.26", "pillow", "fastapi", "uvicorn",
|
| 15 |
"huggingface_hub>=1.0,<2", "jinja2"]
|
| 16 |
|
| 17 |
[project.optional-dependencies]
|
|
|
|
| 25 |
include = ["onw*"]
|
| 26 |
|
| 27 |
[tool.setuptools.package-data]
|
| 28 |
+
onw = ["web/*", "npukit/*", "npukit/patches/*", "npukit/linux/*"]
|
requirements.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# Same dependencies as pyproject.toml (pip install --pre -e . is what start.bat / start.sh do).
|
| 2 |
# OpenVINO is a pre-release build (2026.5.0b1, from OpenVINO's pre-release index):
|
| 3 |
# pip install -r requirements.txt --extra-index-url https://storage.openvinotoolkit.org/simple/wheels/pre-release
|
| 4 |
-
openvino
|
| 5 |
transformers>=5.5.4,<6
|
| 6 |
numpy>=1.26
|
| 7 |
pillow
|
|
|
|
| 1 |
# Same dependencies as pyproject.toml (pip install --pre -e . is what start.bat / start.sh do).
|
| 2 |
# OpenVINO is a pre-release build (2026.5.0b1, from OpenVINO's pre-release index):
|
| 3 |
# pip install -r requirements.txt --extra-index-url https://storage.openvinotoolkit.org/simple/wheels/pre-release
|
| 4 |
+
openvino==2026.5.0b1
|
| 5 |
transformers>=5.5.4,<6
|
| 6 |
numpy>=1.26
|
| 7 |
pillow
|