ryugyosoft commited on
Commit
ea217f6
·
verified ·
1 Parent(s): f4f4926

onw 3.2.0: Ternary Bonsai 2 4B / 9B 64-token graphs (one copy of the 2-bit weights), parallel compile in processes, faster kit compiler, paged graphs released under fused ones, vision encoder in the background

Browse files
CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@
5
  日付は日本時間です。各版は Hugging Face の `ryugyosoft/onw` にタグ(`v0.4.2` 以降)付きで残っています。
6
  onw ウィンドウは、新しい版があるとこのファイルから変更点を表示します(`## v…` の見出しで区切っています)。
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  ## v3.1.0 (2026-10-19)
9
  - **Qwen 系を速く(設定「Qwen 系を高速化する」、既定オン)**:Qwen3.5・Ornith など、これまで NPUW で動いていたモデルを、Lunar Lake 以降の PC(onw の NPU キット)では区間ごとの融合グラフと MTP 先読みで動かします。重みは 1 組のまま(キットの共有)で、同じ質問には 3.0.0 と同じ答えが返ります。
10
  - Panther Lake(Core Ultra X7 358H)の Qwen3.5-9B NF4、3.0.0 との比較:約 3.5K トークンのプロンプト 205 → 299 tok/s、約 900 トークン 242 → 280 tok/s、生成 17.9 → 19.3 tok/s。
 
5
  日付は日本時間です。各版は Hugging Face の `ryugyosoft/onw` にタグ(`v0.4.2` 以降)付きで残っています。
6
  onw ウィンドウは、新しい版があるとこのファイルから変更点を表示します(`## v…` の見出しで区切っています)。
7
 
8
+ ## v3.2.0 (2026-10-20)
9
+ - **Ternary Bonsai 2 4B/9B のプロンプト処理を 10 倍以上に(64 トークン用グラフ、2 ビットの重みは 1 組のまま)**:これまで 2 ビットの重みは 1 トークンずつしか正しく計算できず、プロンプトも 1 トークンずつでした。onw の NPU キットのコンパイラーで 64 トークンのブロックを正しく速く計算できるようにし、NPU 上の 2 ビットの重みを 1 トークン用と 64 トークン用のグラフで共有します(重みを 2 組持たない)。モデルの更新(64 トークン用のグラフ入り)が必要です(モデルの一覧に「更新」が出ます。新しいグラフは onw 3.2 以降だけが使い、3.1.0 以前はこれまでどおり 1 トークンずつ動きます)。Panther Lake(Core Ultra X7 358H)、3.1.0 との比較:
10
+ - Bonsai 2 4B:約 1,200 トークンのプロンプト 25 → 約 300 tok/s、生成 20〜21.6 → 約 22 tok/s、専用メモリ 2.96 → 約 2.1 GB、初回のロード 約 3.6 → 約 6 分(2 回目以降 約 20 秒)。
11
+ - Bonsai 2 9B:プロンプト 約 190 tok/s、生成 約 14.5 tok/s、専用メモリ 約 3.2 GB、初回のロード 約 12 分(2 回目以降 約 35 秒)。
12
+ - 画像の質問も、64 トークンのブロックで読み込みます。
13
+ - **正直に書くと、NPU のメモリは 2 ビットの重みよりずっと大きく、ここは失敗です**:重みとスケール 約 0.9 / 2.1 GB に対してNPU 約 2.6 / 4.5 GB(4B / 9B)。グラフごとに要る重みテーブルが、1 トークン用と 64 トークン用の 2 組で重みと同じくらいあるためです。Intel に改善を要望しています。
14
+ - **Ternary Bonsai 2 27B は 1 トークン用のグラフのままです(64 トークン用は試作したが失敗)**:作って動かすことはできました(プロンプト 約 60 tok/s)が、NPU のメモリを 13.8 GB 使い、16 GB の PC に載りません。2 ビットの重みを NPU で計算するには、グループ(128 個の重み)ごとに 16 バイトの「重みテーブル」がグラフごとに必要で、27B ではグラフ 1 組あたり 3.4 GB になるためです。初回のコンパイルも Panther Lake で 36 分かかりました。Intel に改善を要望しています。
15
+ - **初回のロードを速く:コンパイルを複数のプロセスで同時に**:NPU のコンパイラーはグラフ 1 つに CPU を約 1 コアしか使わないので、空きメモリとコア数に応じて最大 6 つの子プロセスで同時にコンパイルします(省メモリモードでは最大 2)。子プロセスはコンパイルして保存するだけで、NPU には読み込みません(重みが二重にならない)。Panther Lake で Bonsai 2 4B の 1 トークン用+64 トークン用のグラフ:819 → 約 180 秒。融合グラフの裏でのコンパイルも、既定で同時に行います。
16
+ - **NPU コンパイラーの高速化(キット)**:戦略探索で同じ計算を繰り返していた所を直し、2 ビットのモデルの 64 トークン用グラフ 1 つのコンパイルが 335 → 約 20 秒になりました(結果のバイナリーは同じ)。
17
+ - **融合グラフを使う大きさでは、従来のグラフをメモリから外す**:融合グラフが動く範囲では従来のグラフは使わないので、読み込んだあとで外します(重みテーブルの分だけ省メモリ。Bonsai 2 4B:NPU 3.9 → 2.6 GB)。画像の質問や、融合グラフより長い文脈のときだけ、その場で読み込み直します(数秒)。
18
+ - **画像エンコーダーを裏でコンパイル**:画像の付いたモデルは、初回のロードのあと裏で画像エンコーダーをコンパイルしておき、最初の画像の質問で数分待たないようにしました。
19
+ - **Linux 版のキットも更新**しました(上の修正を含む。Bonsai 2 の 64 トークン用グラフも、Ubuntu の Lunar Lake で確かめました)。
20
+ - 2 ビットのモデルでは、融合グラフの長さ違いの種類を全部は常駐させず、1 つずつ入れ替えます(種類ごとに重みテーブルが要るため)。
21
+ - 融合グラフでも、最後の正規化の fp16 のあふれで 0 になった行を、縮めて計算し直します(従来のグラフと同じ処理。Bonsai 2 9B の 64 トークンのブロックで、Lunar Lake の 256 行中 20 行が一様分布になっていた)。
22
+ - 2 ビットのモデルでは、先読み検証(PLD)を使いません(下書きの確認に 64 トークン用のグラフを使うこ��になり、Lunar Lake で生成が 23 → 13〜17 tok/s に落ちていた)。
23
+
24
  ## v3.1.0 (2026-10-19)
25
  - **Qwen 系を速く(設定「Qwen 系を高速化する」、既定オン)**:Qwen3.5・Ornith など、これまで NPUW で動いていたモデルを、Lunar Lake 以降の PC(onw の NPU キット)では区間ごとの融合グラフと MTP 先読みで動かします。重みは 1 組のまま(キットの共有)で、同じ質問には 3.0.0 と同じ答えが返ります。
26
  - Panther Lake(Core Ultra X7 358H)の Qwen3.5-9B NF4、3.0.0 との比較:約 3.5K トークンのプロンプト 205 → 299 tok/s、約 900 トークン 242 → 280 tok/s、生成 17.9 → 19.3 tok/s。
CHANGELOG_en.md CHANGED
@@ -4,6 +4,37 @@
4
 
5
  Dates are JST. Every version is on Hugging Face `ryugyosoft/onw`, tagged from `v0.4.2` on.
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  ## v3.1.0 (2026-10-19)
8
  - **The Qwen family faster (setting "Speed up the Qwen family", on by default)**: Qwen3.5, Ornith and the other models that ran through NPUW run fused segment graphs with MTP draft tokens on PCs from Lunar Lake on (onw's NPU kit). One copy of the weights as before (the kit shares them), and the same question gets the same answer as with 3.0.0.
9
  - Panther Lake (Core Ultra X7 358H), Qwen3.5-9B NF4 against 3.0.0: a ~3.5K-token prompt 205 -> 299 tok/s, ~900 tokens 242 -> 280 tok/s, answers 17.9 -> 19.3 tok/s.
 
4
 
5
  Dates are JST. Every version is on Hugging Face `ryugyosoft/onw`, tagged from `v0.4.2` on.
6
 
7
+ ## v3.2.0 (2026-10-20)
8
+ - **Ternary Bonsai 2 4B / 9B: prompts more than 10x faster (64-token graphs, the 2-bit weights still held once)**:
9
+ until now 2-bit weights were only computed right one token at a time, so prompts went one token at a time too. The
10
+ compiler of onw's NPU kit now computes 64-token blocks right and fast, and the 1-token and the 64-token graphs share
11
+ the 2-bit weights on the NPU (no second copy). Needs the updated models (with 64-token graphs; the model list offers the update. Only onw 3.2 and later use the new graphs - 3.1.0 and earlier keep going token by token). Panther Lake
12
+ (Core Ultra X7 358H), against 3.1.0:
13
+ - Bonsai 2 4B: ~1,200-token prompt 25 -> ~300 tok/s, answers 20-21.6 -> ~22 tok/s, private memory 2.96 -> ~2.1 GB,
14
+ first load ~3.6 -> ~6 min (later loads ~20 s).
15
+ - Bonsai 2 9B: prompts ~190 tok/s, answers ~14.5 tok/s, private memory ~3.2 GB, first load ~12 min (later ~35 s).
16
+ - Image questions are read in 64-token blocks too.
17
+ - **To be frank, the NPU memory is far above the 2-bit weights - a failure on that count**: weights + scales ~0.9 / 2.1 GB, NPU ~2.6 / 4.5 GB (4B / 9B). The weight tables every graph needs are, for the 1-token and the 64-token set, about as large as the weights. Requested from Intel.
18
+ - **Ternary Bonsai 2 27B keeps its 1-token graphs (64-token graphs prototyped - and failed)**: they built and ran
19
+ (prompts ~60 tok/s), but used 13.8 GB of NPU memory - no 16 GB PC holds that. Computing 2-bit weights on the NPU needs
20
+ a 16-byte "weight table" entry per group of 128 weights in every graph, 3.4 GB per set of graphs for the 27B. The first
21
+ compile also took 36 minutes on Panther Lake. Reported to Intel with a request.
22
+ - **Faster first loads: graphs compile in parallel processes**: the NPU compiler uses about one CPU core per graph, so up
23
+ to 6 child processes compile at the same time, as the free memory and the cores allow (2 at most in the memory-saving
24
+ mode). The children only compile and save; nothing is loaded on the NPU twice. Panther Lake, Bonsai 2 4B 1-token +
25
+ 64-token graphs: 819 -> ~180 s. The fused graphs' background compile runs in parallel by default as well.
26
+ - **Faster NPU compiler (kit)**: the strategy search no longer repeats identical work; one 64-token graph of a 2-bit model
27
+ compiles in ~20 instead of 335 s (same binary).
28
+ - **The paged graphs leave memory where the fused graphs run**: they are not used for the positions the fused graphs
29
+ cover, so they are released after loading (their weight tables: Bonsai 2 4B NPU memory 3.9 -> 2.6 GB). Image questions
30
+ and contexts longer than the fused graphs load them again on the spot (a few seconds).
31
+ - **The vision encoder compiles in the background**: models that read images compile their encoder right after the
32
+ first load, so the first image question does not wait minutes.
33
+ - **The Linux kit is updated** too (with the fixes above; Bonsai 2's 64-token graphs checked on Lunar Lake under Ubuntu too).
34
+ - 2-bit models keep one fused variant at a time instead of all lengths (each variant needs its own weight tables).
35
+ - The fused graphs recompute rows the final norm's fp16 overflow zeroed, scaled down (as the paged graphs do; Bonsai 2 9B 64-token blocks on Lunar Lake: 20 of 256 rows came out uniform).
36
+ - 2-bit models do not use prompt-lookup decoding (a draft check would run the 64-token graphs: answers fell from 23 to 13-17 tok/s on Lunar Lake).
37
+
38
  ## v3.1.0 (2026-10-19)
39
  - **The Qwen family faster (setting "Speed up the Qwen family", on by default)**: Qwen3.5, Ornith and the other models that ran through NPUW run fused segment graphs with MTP draft tokens on PCs from Lunar Lake on (onw's NPU kit). One copy of the weights as before (the kit shares them), and the same question gets the same answer as with 3.0.0.
40
  - Panther Lake (Core Ultra X7 358H), Qwen3.5-9B NF4 against 3.0.0: a ~3.5K-token prompt 205 -> 299 tok/s, ~900 tokens 242 -> 280 tok/s, answers 17.9 -> 19.3 tok/s.
README.md CHANGED
@@ -105,8 +105,8 @@ Python や git の知識は要りません。Python が入っていなければ
105
  | [ryugyosoft/Ornith-1.5-9B-onw](https://huggingface.co/ryugyosoft/Ornith-1.5-9B-onw) | `qwen3_5`(Qwen3.5-9B をエージェント・ツール呼び出し向けに鍛えた Ornith-1.5) | テキスト+画像 | 切替 | 4.9 tok/s | 5.3 GB | 約 11 GB |
106
  | [ryugyosoft/Ornith-1.5-9B-nf4-onw](https://huggingface.co/ryugyosoft/Ornith-1.5-9B-nf4-onw) | 同上、重みはチャネル単位 NF4、MTP 先読み付き(**Lunar Lake 以降専用**) | テキスト+画像 | 切替 | —(Lunar Lake で約 15 tok/s、通常版の約 2 倍) | 5.6 GB | 通常版の約 85% |
107
  | [ryugyosoft/Ternary-Bonsai-2-27B-onw](https://huggingface.co/ryugyosoft/Ternary-Bonsai-2-27B-onw) | `qwen3_5`(Qwen3.8-27B を三値の重みで学習し直した Ternary Bonsai 2。重みは 2 ビットのまま。**Lunar Lake 以降専用**) | テキスト | 切替(既定オフ) | —(Lunar Lake で約 4 tok/s。プロンプトは 1 トークンずつ) | 7.6 GB | 約 10 GB |
108
- | [ryugyosoft/Ternary-Bonsai-2-9B-onw](https://huggingface.co/ryugyosoft/Ternary-Bonsai-2-9B-onw) | `qwen3_5`(Qwen3.5-9B を三値の重みで学習し直した Ternary Bonsai 2。重みは 2 ビットのまま。**Lunar Lake 以降専用**) | テキスト+画像 | 切替 | —(Lunar Lake で約 13 tok/s。プロンプトは 1 トークンずつ) | 3.0 GB | 約 3.5 GB |
109
- | [ryugyosoft/Ternary-Bonsai-2-4B-onw](https://huggingface.co/ryugyosoft/Ternary-Bonsai-2-4B-onw) | `qwen3_5`(同じく Qwen3.5-4B の三値版。**Lunar Lake 以降専用**) | テキスト+画像 | 切替 | —(Lunar Lake で約 21 tok/s。プロンプトは 1 トークンずつ) | 1.8 GB | 約 2 GB |
110
  | [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 |
111
  | [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 |
112
  | [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 |
@@ -128,7 +128,7 @@ down_proj だけチャネル単位 INT8。INT4 版より元のモデルに近く
128
 
129
  **2 ビット版(onw 1.4.9、Lunar Lake 以降専用)**:Lunar Lake 以降の NPU は 2 ビットの重みも計算できます。Ternary Bonsai 2 27B は三値(−1・0・+1)で
130
  学習されたモデルで、重みを 2 ビットのまま NPU に載せると 27B がメモリ約 10 GB で動きます。ただし今の NPU コンパイラーは 2 ビットの重みを複数トークンの
131
- ブロックで計算すると間違えるため、プロンプトは 1 トークンずつ(約 4 tok/s)処理します。実用というより「NPU で 27B が動く」ロマン向けです(Intel に報告済み)。onw 1.4.13 では同じ三値の Ternary Bonsai 2 9B / 4B も加えました。こちらは画像も読め、メモリ約 3.5 GB / 約 2 GB と軽く、Lunar Lake で生成約 13 / 21 tok/s です(プロンプトは同じく 1 トークンずつ)。
132
 
133
  **MTP 先読み(onw 1.4.0、Lunar Lake 以降)**:Qwen3.5 系のモデルには、次の次のト��クンを予想する小さな層(MTP)が付いています。
134
  onw はこれで 2 トークン先まで下書きし、本体の 4 トークン用のグラフで一度に確かめます(外れた下書きの分だけ正確に巻き戻します)。
@@ -328,7 +328,10 @@ KV キャッシュは INT8 で、会話が実際に使った長さの分だけ
328
  (Windows 11、ドライバー 32.0.100.5540、2026 年 10 月 6 日。1 パーツは 4 トークン用グラフの最初のパーツ。命令の大きさは
329
  コンパイル結果の ELF セクションの大きさで、命令の数そのものではありません。)
330
 
331
- ### 既知の問題(onw 3.1.0、2026 年 10 月 19 日)
 
 
 
332
 
333
  - **Qwen 系の高速化をオンにした最初のロードは長め**:融合グラフのコンパイルが加わります(Panther Lake の小型機の Qwen3.5-9B NF4:使い始めるまで約 13 分、残りの種類は回答しながら裏で約 25 分)。2 回目以降はキャッシュから数十秒です。
334
  - **INT4 版のモデルと Meteor Lake・Arrow Lake(NPU 3720)は 2.1.0 と同じです**:INT4 の重みは重みを持たないグラフにできず、NPU 3720 は
 
105
  | [ryugyosoft/Ornith-1.5-9B-onw](https://huggingface.co/ryugyosoft/Ornith-1.5-9B-onw) | `qwen3_5`(Qwen3.5-9B をエージェント・ツール呼び出し向けに鍛えた Ornith-1.5) | テキスト+画像 | 切替 | 4.9 tok/s | 5.3 GB | 約 11 GB |
106
  | [ryugyosoft/Ornith-1.5-9B-nf4-onw](https://huggingface.co/ryugyosoft/Ornith-1.5-9B-nf4-onw) | 同上、重みはチャネル単位 NF4、MTP 先読み付き(**Lunar Lake 以降専用**) | テキスト+画像 | 切替 | —(Lunar Lake で約 15 tok/s、通常版の約 2 倍) | 5.6 GB | 通常版の約 85% |
107
  | [ryugyosoft/Ternary-Bonsai-2-27B-onw](https://huggingface.co/ryugyosoft/Ternary-Bonsai-2-27B-onw) | `qwen3_5`(Qwen3.8-27B を三値の重みで学習し直した Ternary Bonsai 2。重みは 2 ビットのまま。**Lunar Lake 以降専用**) | テキスト | 切替(既定オフ) | —(Lunar Lake で約 4 tok/s。プロンプトは 1 トークンずつ) | 7.6 GB | 約 10 GB |
108
+ | [ryugyosoft/Ternary-Bonsai-2-9B-onw](https://huggingface.co/ryugyosoft/Ternary-Bonsai-2-9B-onw) | `qwen3_5`(Qwen3.5-9B を三値の重みで学習し直した Ternary Bonsai 2。重みは 2 ビットのまま。**Lunar Lake 以降専用**) | テキスト+画像 | 切替 | —(Lunar Lake で約 13 tok/s。3.2.0 からプロンプトは 64 トークンずつ) | 3.0 GB | 約 3.5 GB |
109
+ | [ryugyosoft/Ternary-Bonsai-2-4B-onw](https://huggingface.co/ryugyosoft/Ternary-Bonsai-2-4B-onw) | `qwen3_5`(同じく Qwen3.5-4B の三値版。**Lunar Lake 以降専用**) | テキスト+画像 | 切替 | —(Lunar Lake で約 21 tok/s。3.2.0 からプロンプトは 64 トークンずつ) | 1.8 GB | 約 2 GB |
110
  | [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 |
111
  | [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 |
112
  | [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 |
 
128
 
129
  **2 ビット版(onw 1.4.9、Lunar Lake 以降専用)**:Lunar Lake 以降の NPU は 2 ビットの重みも計算できます。Ternary Bonsai 2 27B は三値(−1・0・+1)で
130
  学習されたモデルで、重みを 2 ビットのまま NPU に載せると 27B がメモリ約 10 GB で動きます。ただし今の NPU コンパイラーは 2 ビットの重みを複数トークンの
131
+ ブロックで計算すると間違えるため、プロンプトは 1 トークンずつ(約 4 tok/s)処理します。実用というより「NPU で 27B が動く」ロマン向けです(Intel に報告済み)。onw 1.4.13 では同じ三値の Ternary Bonsai 2 9B / 4B も加えました。こちらは画像も読め、メモリ約 3.5 GB / 約 2 GB と軽く、Lunar Lake で生成約 13 / 21 tok/s です。onw 3.2.0 では、NPU キットのコンパイラーでこの不具合を直し、9B / 4B に 64 トークン用のグラフを加えました(2 ビットの重みは 1 組のまま共有。Panther Lake でプロンプト約 190 / 300 tok/s)。27B も試作しましたが、NPU のメモリを 13.8 GB 使うため出していません(1 トークンずつのまま)。
132
 
133
  **MTP 先読み(onw 1.4.0、Lunar Lake 以降)**:Qwen3.5 系のモデルには、次の次のト��クンを予想する小さな層(MTP)が付いています。
134
  onw はこれで 2 トークン先まで下書きし、本体の 4 トークン用のグラフで一度に確かめます(外れた下書きの分だけ正確に巻き戻します)。
 
328
  (Windows 11、ドライバー 32.0.100.5540、2026 年 10 月 6 日。1 パーツは 4 トークン用グラフの最初のパーツ。命令の大きさは
329
  コンパイル結果の ELF セクションの大きさで、命令の数そのものではありません。)
330
 
331
+ ### 既知の問題(onw 3.2.0、2026 年 10 月 20 日)
332
+
333
+ - **Ternary Bonsai 2 27B の 64 トークン用グラフは試作したが失敗**:NPU のメモリを 13.8 GB 使い(グラフごとに要る 2 ビットの重みテーブルが 1 組 3.4 GB)、16 GB の PC に載らないため出していません。27B のプロンプトは 1 トークンずつのままです。Intel に要望しています。
334
+ - **Ternary Bonsai 2 の 64 トークン版の初回のロードは長め**:Panther Lake で 4B 約 6 分、9B 約 12 分(2 回目以降は 20〜35 秒)。
335
 
336
  - **Qwen 系の高速化をオンにした最初のロードは長め**:融合グラフのコンパイルが加わります(Panther Lake の小型機の Qwen3.5-9B NF4:使い始めるまで約 13 分、残りの種類は回答しながら裏で約 25 分)。2 回目以降はキャッシュから数十秒です。
337
  - **INT4 版のモデルと Meteor Lake・Arrow Lake(NPU 3720)は 2.1.0 と同じです**:INT4 の重みは重みを持たないグラフにできず、NPU 3720 は
README_en.md CHANGED
@@ -92,8 +92,8 @@ troubleshooting), is the **[user guide](MANUAL_en.md)**.
92
  | [ryugyosoft/Ornith-1.5-9B-onw](https://huggingface.co/ryugyosoft/Ornith-1.5-9B-onw) | `qwen3_5` (Ornith-1.5: Qwen3.5-9B trained further for agents and tool calls) | text + image | switchable | 4.9 tok/s | 5.3 GB | ~11 GB |
93
  | [ryugyosoft/Ornith-1.5-9B-nf4-onw](https://huggingface.co/ryugyosoft/Ornith-1.5-9B-nf4-onw) | the same, channel-wise NF4 weights, MTP draft tokens (**Lunar Lake and newer only**) | text + image | switchable | - (~15 tok/s on Lunar Lake, ~2x the regular build) | 5.6 GB | ~85% of the regular build |
94
  | [ryugyosoft/Ternary-Bonsai-2-27B-onw](https://huggingface.co/ryugyosoft/Ternary-Bonsai-2-27B-onw) | `qwen3_5` (Ternary Bonsai 2: Qwen3.8-27B retrained with ternary weights, kept 2-bit; **Lunar Lake and newer only**) | text | switchable (off by default) | - (~4 tok/s on Lunar Lake; prompts token by token) | 7.6 GB | ~10 GB |
95
- | [ryugyosoft/Ternary-Bonsai-2-9B-onw](https://huggingface.co/ryugyosoft/Ternary-Bonsai-2-9B-onw) | `qwen3_5` (Ternary Bonsai 2: Qwen3.5-9B retrained with ternary weights, kept 2-bit; **Lunar Lake and newer only**) | text + image | switchable | - (~13 tok/s on Lunar Lake; prompts token by token) | 3.0 GB | ~3.5 GB |
96
- | [ryugyosoft/Ternary-Bonsai-2-4B-onw](https://huggingface.co/ryugyosoft/Ternary-Bonsai-2-4B-onw) | `qwen3_5` (the same for Qwen3.5-4B; **Lunar Lake and newer only**) | text + image | switchable | - (~21 tok/s on Lunar Lake; prompts token by token) | 1.8 GB | ~2 GB |
97
  | [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 |
98
  | [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 |
99
  | [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 |
@@ -117,7 +117,7 @@ original than the INT4 builds; on Panther Lake Granite 4.2 8B decodes ~1.7x as f
117
  **2-bit build (onw 1.4.9, Lunar Lake and newer only)**: NPUs from Lunar Lake on compute 2-bit weights too. Ternary Bonsai 2 27B was
118
  trained with ternary weights (-1, 0, +1); kept 2-bit on the NPU, the 27B runs in about 10 GB of memory. Today's NPU compilers compute
119
  2-bit weights wrong in blocks of more than one token, so prompts are processed token by token (~4 tok/s): a "27B on the NPU" showpiece
120
- rather than a daily driver (reported to Intel). onw 1.4.13 adds the ternary Ternary Bonsai 2 9B / 4B as well: they read images, need only ~3.5 / ~2 GB of memory and generate ~13 / ~21 tok/s on Lunar Lake (prompts token by token too).
121
 
122
  **MTP draft tokens (onw 1.4.0, Lunar Lake and newer)**: Qwen3.5-family models carry a small layer (MTP) that predicts the
123
  token after next. onw uses it to draft two tokens ahead and checks them in one pass on the model's 4-token graphs
@@ -316,7 +316,10 @@ How it was measured, and caveats:
316
  (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
317
  the compiled file, not instruction counts.)
318
 
319
- ### Known issues (onw 3.1.0, 2026-10-19)
 
 
 
320
 
321
  - **The first load with the Qwen family sped up takes longer**: the fused graphs compile as well (a small Panther Lake PC, Qwen3.5-9B NF4: ~13 min until usable, the other variants ~25 min in the background while you chat). Later loads come from the cache in well under a minute.
322
  - **The INT4 builds and Meteor Lake / Arrow Lake (NPU 3720) are as in 2.1.0**: INT4 weights cannot go into graphs without weights,
 
92
  | [ryugyosoft/Ornith-1.5-9B-onw](https://huggingface.co/ryugyosoft/Ornith-1.5-9B-onw) | `qwen3_5` (Ornith-1.5: Qwen3.5-9B trained further for agents and tool calls) | text + image | switchable | 4.9 tok/s | 5.3 GB | ~11 GB |
93
  | [ryugyosoft/Ornith-1.5-9B-nf4-onw](https://huggingface.co/ryugyosoft/Ornith-1.5-9B-nf4-onw) | the same, channel-wise NF4 weights, MTP draft tokens (**Lunar Lake and newer only**) | text + image | switchable | - (~15 tok/s on Lunar Lake, ~2x the regular build) | 5.6 GB | ~85% of the regular build |
94
  | [ryugyosoft/Ternary-Bonsai-2-27B-onw](https://huggingface.co/ryugyosoft/Ternary-Bonsai-2-27B-onw) | `qwen3_5` (Ternary Bonsai 2: Qwen3.8-27B retrained with ternary weights, kept 2-bit; **Lunar Lake and newer only**) | text | switchable (off by default) | - (~4 tok/s on Lunar Lake; prompts token by token) | 7.6 GB | ~10 GB |
95
+ | [ryugyosoft/Ternary-Bonsai-2-9B-onw](https://huggingface.co/ryugyosoft/Ternary-Bonsai-2-9B-onw) | `qwen3_5` (Ternary Bonsai 2: Qwen3.5-9B retrained with ternary weights, kept 2-bit; **Lunar Lake and newer only**) | text + image | switchable | - (~13 tok/s on Lunar Lake; prompts 64 tokens at a time from 3.2.0) | 3.0 GB | ~3.5 GB |
96
+ | [ryugyosoft/Ternary-Bonsai-2-4B-onw](https://huggingface.co/ryugyosoft/Ternary-Bonsai-2-4B-onw) | `qwen3_5` (the same for Qwen3.5-4B; **Lunar Lake and newer only**) | text + image | switchable | - (~21 tok/s on Lunar Lake; prompts 64 tokens at a time from 3.2.0) | 1.8 GB | ~2 GB |
97
  | [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 |
98
  | [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 |
99
  | [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 |
 
117
  **2-bit build (onw 1.4.9, Lunar Lake and newer only)**: NPUs from Lunar Lake on compute 2-bit weights too. Ternary Bonsai 2 27B was
118
  trained with ternary weights (-1, 0, +1); kept 2-bit on the NPU, the 27B runs in about 10 GB of memory. Today's NPU compilers compute
119
  2-bit weights wrong in blocks of more than one token, so prompts are processed token by token (~4 tok/s): a "27B on the NPU" showpiece
120
+ rather than a daily driver (reported to Intel). onw 1.4.13 adds the ternary Ternary Bonsai 2 9B / 4B as well: they read images, need only ~3.5 / ~2 GB of memory and generate ~13 / ~21 tok/s on Lunar Lake. onw 3.2.0 fixes the compiler bug in the NPU kit and adds 64-token graphs to the 9B / 4B (sharing one copy of the 2-bit weights; prompts ~190 / ~300 tok/s on Panther Lake). We prototyped them for the 27B too, but it took 13.8 GB of NPU memory, so the 27B stays token by token.
121
 
122
  **MTP draft tokens (onw 1.4.0, Lunar Lake and newer)**: Qwen3.5-family models carry a small layer (MTP) that predicts the
123
  token after next. onw uses it to draft two tokens ahead and checks them in one pass on the model's 4-token graphs
 
316
  (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
317
  the compiled file, not instruction counts.)
318
 
319
+ ### Known issues (onw 3.2.0, 2026-10-20)
320
+
321
+ - **64-token graphs for Ternary Bonsai 2 27B: prototyped, and failed**: they took 13.8 GB of NPU memory (the 2-bit weight tables every graph needs are 3.4 GB per set), which no 16 GB PC holds, so they are not shipped. The 27B still reads prompts token by token. Requested from Intel.
322
+ - **The first load of the 64-token Ternary Bonsai 2 builds takes a while**: on Panther Lake ~6 min for the 4B, ~12 min for the 9B (later loads 20-35 s).
323
 
324
  - **The first load with the Qwen family sped up takes longer**: the fused graphs compile as well (a small Panther Lake PC, Qwen3.5-9B NF4: ~13 min until usable, the other variants ~25 min in the background while you chat). Later loads come from the cache in well under a minute.
325
  - **The INT4 builds and Meteor Lake / Arrow Lake (NPU 3720) are as in 2.1.0**: INT4 weights cannot go into graphs without weights,
TECHNICAL.md CHANGED
@@ -59,7 +59,7 @@ OpenVINO 2026.5 のコンパイラーは NF4 の重みでこれに失敗し、
59
  63.5 対 63.6 MB)。INT4 は 2 行以上のグラフで、NPU 4000 以降のコンパイラーが INT4 を表引き(4 ビットの値をそのまま表の番号に
60
  する)に変える処理と重なって失敗し、直してもコンパイルが 20〜35 倍遅くなるだけでした。このため 2 ビットと INT4 のモデル
61
  (NPUW を使わないもの)では使わず、融合グラフも既定でオフです(2.1.0 と同じ動作)。INT8 は分離されます
62
- (llm-jp-4.1-8B INT8 の部品で 2.5 対 216 MB)。Lunar Lake 以降では NF4 版を勧めます。
63
 
64
  ### コンパイラーの修正(npu_compiler 8.3 をもとに)
65
 
@@ -139,6 +139,40 @@ OpenVINO の仕組み)で動かしていました。キットで重みを共
139
 
140
  結果は上のグラフと CHANGELOG の v3.0.0・v3.1.0 を見てください。
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  ## NPU で初めて動かした 4 つ
143
 
144
  **私たちの知る限り、次の 4 つを NPU で動かして公開している実装は onw が初めてです**(2026 年 9 月、Fable 5.1 調べ)。
 
59
  63.5 対 63.6 MB)。INT4 は 2 行以上のグラフで、NPU 4000 以降のコンパイラーが INT4 を表引き(4 ビットの値をそのまま表の番号に
60
  する)に変える処理と重なって失敗し、直してもコンパイルが 20〜35 倍遅くなるだけでした。このため 2 ビットと INT4 のモデル
61
  (NPUW を使わないもの)では使わず、融合グラフも既定でオフです(2.1.0 と同じ動作)。INT8 は分離されます
62
+ (llm-jp-4.1-8B INT8 の部品で 2.5 対 216 MB)。Lunar Lake 以降では NF4 版を勧めます。2 ビットは 3.2 で分離できるようにしました(下の「onw 3.2」)。
63
 
64
  ### コンパイラーの修正(npu_compiler 8.3 をもとに)
65
 
 
139
 
140
  結果は上のグラフと CHANGELOG の v3.0.0・v3.1.0 を見てください。
141
 
142
+ ### onw 3.2:2 ビット(Bonsai)を 64 トークンずつ、重みは 1 組のまま
143
+
144
+ - **重みを持たないグラフを 2 ビットでも**:Bonsai の 2 ビットの重みは 128 個ずつのグループに分けて、グループごとのスケールと
145
+ ゼロ点で量子化されています。コンパイラーは隣り合うグループの切り出しを「仮の土台の上の Concat」にまとめてしまい、重みを
146
+ 分離できませんでした。キットのコンパイラーは、同じ OV 定数の連続したグループの切り出しを「先頭+k グループの切り出し+残り」
147
+ に書き直し、[k, N, K] の切り出しはグループごとの Slice → Reshape → Concat で組み立てます(ui2 の Transpose は初期化スケジュール
148
+ の中では使えないため)。`MergeFullyConnected` のまとめはそのまま使えます。
149
+ - **重みの部分一致の共有(プラグイン)**:1 トークン用と 64 トークン用のグラフは、重みの区切り方が違います(片方の 1 本が、もう
150
+ 片方の数本をつないだもの)。キットのプラグインは、64 KB 以上の出力を登録しておき、32 KB 以上の新しい出力を既存のバッファの
151
+ 中の同じ大きさの倍数の位置と照合して、一致すればその一部を指すようにします(`ONW_WS_SUBRANGE=0` で無効)。2 ビットの重みは
152
+ NPU 上に 1 組だけです。
153
+ - **重みテーブルは共有できない**:NPU(5010 で確認)は、2 ビットのグループのスケールを、出力チャネルごと・グループごとに 16 バイト
154
+ の重みテーブルの行から読みます。テーブルはグラフの中で並びが決まるので、グラフごとに別に要り、重み 1 個あたり 1 ビット
155
+ (2 ビットの重みの半分)になります。新しい形式のテーブル(`useNewWeightTableFormat`)は 5010 の PPE では使えませんでした。
156
+ 27B では 1 組 3.4 GB になり、1 トークン用+64 トークン用で NPU 13.8 GB を使ったので、27B には入れていません。
157
+ - **コンパイル時間**:戦略探索(MCSA・兄弟の操作の解析)が、同じ重なり方の分散情報や兄弟のリストを何度も作り直していたのを
158
+ 覚えておくようにし、定数の畳み込みのメモを転置・並べ替えにも広げました。定数を左から掛ける行列積がバッチごとに展開される
159
+ 所も、右から掛ける形に直します(`ONW_LEFTMM=0` で無効)。2 ビットの 64 トークンの部品 1 つが 335 → 約 20 秒(バイナリーは同じ)。
160
+ - **プロセスでの並列コンパイル**:OpenVINO は 1 プロセスの中の `compile_model` を同時に呼んでも順番に処理し、コンパイラーも
161
+ グラフ 1 つにほぼ 1 コアしか使いません。初回のロードでは、コンパイルして `export_model` で保存するだけの子プロセスを、空き
162
+ メモリ(1 つあたり 2.5 GB)とコア数(半分)に応じて最大 6 つ走らせ、親はできたファイルを読み込みます。子は NPU に重みを
163
+ 載せないので、重みは二重になりません(Bonsai 2 4B:819 → 約 180 秒)。子は MoE のエキスパートの束も共有の重みも NPU から
164
+ 見えるメモリに読み込みません(最初は読み込んでいて、Qwen3.6-35B-A3B の子 6 つが Panther Lake の NPU ドライバーの中で止まり、
165
+ 再起動が要りました。直したあとは 84 部品を 185 秒、初回のロード 231 秒)。
166
+ - **従来のグラフを外す**:融合グラフが覆う長さでは、従来のグラフ(とその重みテーブル)を読み込んだあとで外します。画像の質問や、
167
+ 融合グラフより長い文脈になったときだけ読み込み直します(`ONW_FUSED_DROP_PAGED=0` で外さない)。外したグラフの状態のテンソルは
168
+ onw 側で持ちます。
169
+ - **融合グラフの 0 の行**:複数トークンのブロックでは、最後の RMSNorm の二乗和が fp16 であふれて行全体が 0 になることがあります
170
+ (NPU 4000 と 3720 で見ていた現象)。従来のグラフは頭の部分だけ縮めた入力で計算し直していましたが、融合グラフにはそれが
171
+ なく、Bonsai 2 9B の 64 トークンのブロックで Lunar Lake の 256 行中 20 行が一様分布になっていました(KL 0.86)。同じ処理を
172
+ 入れて KL 0.02 です。
173
+ - **2 ビットのモデルは先読み検証(PLD)を使いません**:下書きの確認が融合 64 トークンのグラフになり、Lunar Lake で生成が
174
+ 23 → 13〜17 tok/s に落ちていました(確認の重さは従来のグラフで測っていたので、オンと判断していました)。
175
+
176
  ## NPU で初めて動かした 4 つ
177
 
178
  **私たちの知る限り、次の 4 つを NPU で動かして公開している実装は onw が初めてです**(2026 年 9 月、Fable 5.1 調べ)。
TECHNICAL_en.md CHANGED
@@ -58,7 +58,7 @@ and compiles were slow. Instead of rewriting the graphs, we fixed the compiler's
58
  With 2+ rows, INT4 clashes with the NPU 4000+ compiler's rewrite of INT4 into a lookup table (the 4-bit value used as the table
59
  index) and fails; fixed, it only compiled 20-35x slower. So 2-bit and INT4 models (those without NPUW) do not use it, and their
60
  fused graphs stay off by default (as in 2.1.0). INT8 is separated (an llm-jp-4.1-8B INT8 part: 2.5 vs 216 MB). From Lunar Lake
61
- on, use the NF4 builds.
62
 
63
  ### Compiler fixes (based on npu_compiler 8.3)
64
 
@@ -142,6 +142,40 @@ one copy of the weights). With the kit sharing the weights, 3.1 turns NPUW off f
142
 
143
  For the results see the chart above and v3.0.0 / v3.1.0 in the CHANGELOG.
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  ## Four firsts on the NPU
146
 
147
  **To our knowledge onw is the first public implementation of these four on the NPU** (survey by Fable 5.1,
 
58
  With 2+ rows, INT4 clashes with the NPU 4000+ compiler's rewrite of INT4 into a lookup table (the 4-bit value used as the table
59
  index) and fails; fixed, it only compiled 20-35x slower. So 2-bit and INT4 models (those without NPUW) do not use it, and their
60
  fused graphs stay off by default (as in 2.1.0). INT8 is separated (an llm-jp-4.1-8B INT8 part: 2.5 vs 216 MB). From Lunar Lake
61
+ on, use the NF4 builds. 3.2 separates 2-bit weights (see "onw 3.2" below).
62
 
63
  ### Compiler fixes (based on npu_compiler 8.3)
64
 
 
142
 
143
  For the results see the chart above and v3.0.0 / v3.1.0 in the CHANGELOG.
144
 
145
+ ### onw 3.2: 2-bit (Bonsai) 64 tokens at a time, one copy of the weights
146
+
147
+ - **Graphs without weights for 2-bit too**: Bonsai's 2-bit weights are quantized in groups of 128 with a scale and a zero
148
+ point per group. The compiler folded slices of neighbouring groups into a Concat on a dummy base, which cannot be
149
+ separated. The kit's compiler rewrites consecutive group slices of one OV constant into "prefix + k-group slice + rest",
150
+ and builds a [k, N, K] slice from per-group Slice -> Reshape -> Concat (a ui2 Transpose is not available in the init
151
+ schedule). `MergeFullyConnected` still merges.
152
+ - **Sharing parts of weight buffers (plugin)**: the 1-token and the 64-token graphs cut their weights differently (one
153
+ output of one is several outputs of the other joined). The kit's plugin registers outputs of 64 KB and more and matches a
154
+ new output of 32 KB or more against same-size-aligned offsets inside them; a match points into the existing buffer
155
+ (`ONW_WS_SUBRANGE=0` turns it off). The NPU holds the 2-bit weights once.
156
+ - **Weight tables cannot be shared**: the NPU (seen on 5010) reads a 2-bit group's scale from a 16-byte weight-table row
157
+ per output channel and group. The table layout is fixed per graph, so every graph needs its own: 1 bit per weight, half of
158
+ the 2-bit weights. The newer table format (`useNewWeightTableFormat`) did not work on the 5010's PPE. For the 27B that is
159
+ 3.4 GB per set; the 1-token + 64-token sets took 13.8 GB of NPU memory, so the 27B does not get them.
160
+ - **Compile time**: the strategy search (MCSA, sibling-op analysis) rebuilt the same overlapped distributions and sibling
161
+ lists again and again; they are remembered now, and the constant-folding memo covers transposes / permutes too. MatMuls
162
+ with a constant on the left that were unrolled per batch are rewritten with the constant on the right (`ONW_LEFTMM=0`
163
+ turns it off). One 64-token part of a 2-bit model: 335 -> ~20 s (same binary).
164
+ - **Parallel compile in processes**: OpenVINO serializes concurrent `compile_model` calls in one process, and the compiler
165
+ uses about one core per graph. At the first load, up to 6 child processes (2.5 GB of free memory and half a core count
166
+ each) only compile and `export_model`; the parent imports the files. The children never put weights on the NPU, so
167
+ nothing is held twice (Bonsai 2 4B: 819 -> ~180 s). The children load neither a MoE's expert bank nor the shared
168
+ weights into NPU-visible memory (at first they did: six children of Qwen3.6-35B-A3B hung inside Panther Lake's NPU
169
+ driver, a reboot was needed; fixed, 84 parts in 185 s, the first load 231 s).
170
+ - **Dropping the paged graphs**: at lengths the fused graphs cover, the paged graphs (and their weight tables) are released
171
+ after loading; image questions and contexts longer than the fused graphs load them again (`ONW_FUSED_DROP_PAGED=0` keeps
172
+ them). onw keeps the state tensors of the dropped graphs itself.
173
+ - **Zero rows in the fused graphs**: in multi-token blocks the final RMSNorm's sum of squares can overflow fp16 and zero a
174
+ whole row (seen on NPU 4000 and 3720). The paged graphs ran their head again on scaled inputs; the fused ones did not,
175
+ and Bonsai 2 9B's 64-token blocks came out uniform on 20 of 256 rows on Lunar Lake (KL 0.86). With the same fix: 0.02.
176
+ - **2-bit models do not use prompt-lookup decoding**: a draft check would run the fused 64-token graphs (answers 23 ->
177
+ 13-17 tok/s on Lunar Lake; the check's cost had been timed on the paged graphs, so it was switched on).
178
+
179
  ## Four firsts on the NPU
180
 
181
  **To our knowledge onw is the first public implementation of these four on the NPU** (survey by Fable 5.1,
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__ = "3.1.0"
 
2
 
3
  A small engine that runs LLMs entirely on the Intel NPU: static segment graphs + host-managed state.
4
  """
5
+ __version__ = "3.2.0"
onw/app.py CHANGED
@@ -25,9 +25,9 @@ CATALOG = [ # repo, title, takes images, download GB, decode speed (NPU 3720),
25
  ("ryugyosoft/Ternary-Bonsai-2-27B-onw", "Ternary Bonsai 2 27B (2-bit)", False, 7.6, "4 tok/s",
26
  "27B を三値(2 ビット)の重みで(Qwen3.8-27B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 10 GB で 27B が動きますが、プロンプトの処理は 1 トークンずつで遅い(約 4 tok/s)ので、短い質問向け"),
27
  ("ryugyosoft/Ternary-Bonsai-2-9B-onw", "Ternary Bonsai 2 9B (2-bit)", True, 3.0, "13 tok/s",
28
- "三値(2 ビット)の 9B(Qwen3.5-9B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 3.5 GB と軽く、画像も読めます。プロンプトの処理は 1 トークンずつ(約 13 tok/s)なので、短めの質問向け"),
29
  ("ryugyosoft/Ternary-Bonsai-2-4B-onw", "Ternary Bonsai 2 4B (2-bit)", True, 1.8, "21 tok/s",
30
- "三値(2 ビット)の軽い 4B(Qwen3.5-4B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 2 GB で約 21 tok/s、画像も読めます。プロンプトの処理は 1 トークンずつ(約 21 tok/s)なので、短めの質問向け"),
31
  ("ryugyosoft/NeoHorse-1-4B-onw", "NeoHorse-1-4B", False, 2.4, "6.4 tok/s",
32
  "エージェント・ツール呼び出し向けの軽量モデル(Qwen3.5-4B ベース)"),
33
  ("ryugyosoft/granite-4.2-3b-onw", "Granite 4.2 3B", False, 2.1, "7.7 tok/s",
@@ -75,6 +75,9 @@ DENSE_PROMPT = {"ryugyosoft/Qwen3.5-9B-onw", "ryugyosoft/Ornith-1.5-9B-onw", "ry
75
  "ryugyosoft/llm-jp-4.1-8b-thinking-onw", "ryugyosoft/llm-jp-4.1-8b-thinking-int8-onw"}
76
  # [2.0.3] republished with 2-token graphs: NPU 4000+ answers on them instead of the 1-token ones (chat.block_sizes), whose
77
  # compile takes minutes a part there (Granite 4.2 8B first load on Panther Lake 2 h 46 min -> 23 min)
 
 
 
78
  DECODE2 = {"ryugyosoft/granite-4.2-3b-onw", "ryugyosoft/granite-4.2-8b-onw", "ryugyosoft/llm-jp-4.1-8b-thinking-onw"}
79
 
80
 
@@ -619,8 +622,8 @@ def outdated(repo, p):
619
  the INT8 one is noticeably more accurate; the NF4 Qwen3.5 models published before the MTP layer and the 64-token
620
  blocks (onw 1.4.0: ~1.3x the answer and 2x the prompt speed on Lunar Lake)."""
621
  gpt, nf4, mix = repo.startswith("ryugyosoft/gpt-oss-20b"), repo in NF4_MTP, repo in MOE_MIXER
622
- dense = repo in DENSE_PROMPT
623
- if not (gpt or nf4 or mix or dense) or not downloaded_by_onw(p):
624
  return False
625
  try:
626
  meta = json.load(open(os.path.join(p, "engine.json")))
@@ -628,6 +631,8 @@ def outdated(repo, p):
628
  return False
629
  if mix:
630
  return "mixer" not in meta
 
 
631
  if dense:
632
  if repo in DECODE2 and npu_arch() >= 4000 and 2 not in (meta.get("prompt_sizes") or []):
633
  return True
 
25
  ("ryugyosoft/Ternary-Bonsai-2-27B-onw", "Ternary Bonsai 2 27B (2-bit)", False, 7.6, "4 tok/s",
26
  "27B を三値(2 ビット)の重みで(Qwen3.8-27B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 10 GB で 27B が動きますが、プロンプトの処理は 1 トークンずつで遅い(約 4 tok/s)ので、短い質問向け"),
27
  ("ryugyosoft/Ternary-Bonsai-2-9B-onw", "Ternary Bonsai 2 9B (2-bit)", True, 3.0, "13 tok/s",
28
+ "三値(2 ビット)の 9B(Qwen3.5-9B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 3.5 GB と軽く、画像も読めます。onw 3.2 からプロンプトは 64 トークンずつ(Panther Lake で約 190 tok/s)"),
29
  ("ryugyosoft/Ternary-Bonsai-2-4B-onw", "Ternary Bonsai 2 4B (2-bit)", True, 1.8, "21 tok/s",
30
+ "三値(2 ビット)の軽い 4B(Qwen3.5-4B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 2 GB で約 21 tok/s、画像も読めます。onw 3.2 からプロンプトは 64 トークンずつ(Panther Lake で約 300 tok/s)"),
31
  ("ryugyosoft/NeoHorse-1-4B-onw", "NeoHorse-1-4B", False, 2.4, "6.4 tok/s",
32
  "エージェント・ツール呼び出し向けの軽量モデル(Qwen3.5-4B ベース)"),
33
  ("ryugyosoft/granite-4.2-3b-onw", "Granite 4.2 3B", False, 2.1, "7.7 tok/s",
 
75
  "ryugyosoft/llm-jp-4.1-8b-thinking-onw", "ryugyosoft/llm-jp-4.1-8b-thinking-int8-onw"}
76
  # [2.0.3] republished with 2-token graphs: NPU 4000+ answers on them instead of the 1-token ones (chat.block_sizes), whose
77
  # compile takes minutes a part there (Granite 4.2 8B first load on Panther Lake 2 h 46 min -> 23 min)
78
+ # [3.2] republished with 64-token graphs for onw's kit (engine.json "kit_sizes"): a copy without them is an old download
79
+ # where the kit serves a 2-bit-capable NPU
80
+ U2_PROMPT = {"ryugyosoft/Ternary-Bonsai-2-4B-onw", "ryugyosoft/Ternary-Bonsai-2-9B-onw"}
81
  DECODE2 = {"ryugyosoft/granite-4.2-3b-onw", "ryugyosoft/granite-4.2-8b-onw", "ryugyosoft/llm-jp-4.1-8b-thinking-onw"}
82
 
83
 
 
622
  the INT8 one is noticeably more accurate; the NF4 Qwen3.5 models published before the MTP layer and the 64-token
623
  blocks (onw 1.4.0: ~1.3x the answer and 2x the prompt speed on Lunar Lake)."""
624
  gpt, nf4, mix = repo.startswith("ryugyosoft/gpt-oss-20b"), repo in NF4_MTP, repo in MOE_MIXER
625
+ dense, u2 = repo in DENSE_PROMPT, repo in U2_PROMPT
626
+ if not (gpt or nf4 or mix or dense or u2) or not downloaded_by_onw(p):
627
  return False
628
  try:
629
  meta = json.load(open(os.path.join(p, "engine.json")))
 
631
  return False
632
  if mix:
633
  return "mixer" not in meta
634
+ if u2:
635
+ return "kit_sizes" not in meta and _kit() # (chat.u2_blocks)
636
  if dense:
637
  if repo in DECODE2 and npu_arch() >= 4000 and 2 not in (meta.get("prompt_sizes") or []):
638
  return True
onw/chat.py CHANGED
@@ -49,9 +49,9 @@ def kit_shares(core, device, engine_dir, meta):
49
  (2-bit, INT4: runtime.int4_weights) nor MoE models (experts.bin: their own path)."""
50
  from .runtime import npu_arch, int4_weights
51
  return (device == "NPU" and npukit.active() and npu_arch(core) >= 4000
52
- and (meta.get("needs") != "u2" or os.environ.get("ONW_U2_WEIGHTLESS") == "1")
53
  and not os.path.exists(os.path.join(engine_dir, "experts.bin"))
54
- and (not int4_weights(engine_dir, meta) or os.environ.get("ONW_U2_WEIGHTLESS") == "1")
55
  and os.environ.get("ONW_WEIGHTLESS", "1") != "0")
56
 
57
 
@@ -63,6 +63,13 @@ def use_mtp(meta, shared):
63
  return bool(meta.get("mtp")) and (env == "1" if env in ("0", "1") else shared)
64
 
65
 
 
 
 
 
 
 
 
66
  def block_sizes(meta, shared=False, mtp=False, arch=None):
67
  """Block sizes to load: the 1-token graphs for answers and ONE prompt size, the largest the model has ([2.0]: in
68
  the 16-token graphs' place - with the weights shared between the sizes (NPUW) every size still adds its NPU
@@ -71,6 +78,10 @@ def block_sizes(meta, shared=False, mtp=False, arch=None):
71
  1-token graphs. ONW_S64=0: the 16-token graphs as before; 1: every size. arch: the NPU's (runtime.npu_arch) -
72
  NPU 3720 takes blocks up to 64 tokens (128 are slower there: its INT4 matmuls stop scaling); ONW_PROMPT_MAX: a cap."""
73
  sizes = all_sizes(meta)
 
 
 
 
74
  only = os.environ.get("ONW_SIZES") # e.g. "16": decode with the 16-token graphs only
75
  if not only and 1 in sizes and grouped_wanted(meta):
76
  # [2.0] a MoE model with mixer graphs: every prompt goes the grouped-MoE path (onw/grouped.py), so only the
@@ -115,6 +126,34 @@ def block_sizes(meta, shared=False, mtp=False, arch=None):
115
  THINK_BUDGET = {"low": 512, "medium": 2048, "high": None}
116
 
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  class ChatEngine:
119
  _think_marks = None # the thought markers when they are not <think> ... </think> (Gemma 4)
120
  swap_gb = None # memory-saving mode: GB of the 1-token graphs swapped in and out
@@ -248,8 +287,26 @@ class ChatEngine:
248
  # [2.1.0] the answer graphs as stream_chat sets them for an answer of the usual length (n + up to 1024 new
249
  # tokens: the 2K variant), so the first question does not switch variants (Panther Lake, Granite 4.2 8B
250
  # NF4: 6 s added to the first prompt)
251
- for S in sorted(m._fused_sizes, reverse=True):
 
 
 
 
 
 
252
  m.fused_prepare(min(1024 if S > min(m._fused_sizes) else 2048, m.T), S)
 
 
 
 
 
 
 
 
 
 
 
 
253
  pld_state = "off (memory-saving mode)" if self.swap_gb is not None else "on" if self.pld else "off"
254
  print(f"[load] {time.time()-t0:.1f}s on {device}; verify block = {self.block_cost or 0:.1f} single steps, "
255
  f"prompt lookup decoding {pld_state}", flush=True)
@@ -258,6 +315,10 @@ class ChatEngine:
258
  def venc(self):
259
  if self._venc is None:
260
  t0 = time.time()
 
 
 
 
261
  self._venc = self._compile_vision(*self._venc_args)
262
  print(f"[vision] encoder loaded in {time.time() - t0:.1f}s", flush=True)
263
  return self._venc
@@ -854,6 +915,12 @@ class ChatEngine:
854
  self.model.fused_prepare(self.model.cur + ahead)
855
  # exact only for plain greedy; not while block sizes are swapped (the verification blocks go away mid-answer)
856
  use_pld = self.pld and plain_greedy and self.model.S > 1 and not swap and budget is None
 
 
 
 
 
 
857
  # MTP drafts (Qwen3.5 dense): every sampling setting (exact), the thinking budget included
858
  mtp = getattr(self.model, "mtp", None) is not None and not swap and any(S >= 4 for S in [*self.model.segs, *(getattr(self.model, "_fused", None) or {})])
859
  use_pld = use_pld and not mtp
 
49
  (2-bit, INT4: runtime.int4_weights) nor MoE models (experts.bin: their own path)."""
50
  from .runtime import npu_arch, int4_weights
51
  return (device == "NPU" and npukit.active() and npu_arch(core) >= 4000
52
+ and (meta.get("needs") != "u2" or os.environ.get("ONW_U2_WEIGHTLESS", "1") != "0") # [3.2] on
53
  and not os.path.exists(os.path.join(engine_dir, "experts.bin"))
54
+ and (not int4_weights(engine_dir, meta) or os.environ.get("ONW_INT4_WEIGHTLESS") == "1")
55
  and os.environ.get("ONW_WEIGHTLESS", "1") != "0")
56
 
57
 
 
63
  return bool(meta.get("mtp")) and (env == "1" if env in ("0", "1") else shared)
64
 
65
 
66
+ def u2_blocks():
67
+ """[3.2] 2-bit (Bonsai) blocks of 2+ tokens: with onw's kit (Windows and Linux: Bonsai 2 4B / 9B checked on Panther
68
+ Lake and on Lunar Lake under Ubuntu). ONW_U2_BLOCKS=1 / 0 forces them on / off."""
69
+ env = os.environ.get("ONW_U2_BLOCKS")
70
+ return env == "1" if env in ("0", "1") else npukit.active()
71
+
72
+
73
  def block_sizes(meta, shared=False, mtp=False, arch=None):
74
  """Block sizes to load: the 1-token graphs for answers and ONE prompt size, the largest the model has ([2.0]: in
75
  the 16-token graphs' place - with the weights shared between the sizes (NPUW) every size still adds its NPU
 
78
  1-token graphs. ONW_S64=0: the 16-token graphs as before; 1: every size. arch: the NPU's (runtime.npu_arch) -
79
  NPU 3720 takes blocks up to 64 tokens (128 are slower there: its INT4 matmuls stop scaling); ONW_PROMPT_MAX: a cap."""
80
  sizes = all_sizes(meta)
81
+ if meta.get("needs") == "u2" and not u2_blocks():
82
+ # [3.2] 2-bit weights in blocks of 2+ rows need onw's kit (its compiler has the fix of npu_compiler #355: the
83
+ # stock compilers return zeros for them) - the 1-token graphs only without it
84
+ sizes = [S for S in sizes if S == 1] or sizes
85
  only = os.environ.get("ONW_SIZES") # e.g. "16": decode with the 16-token graphs only
86
  if not only and 1 in sizes and grouped_wanted(meta):
87
  # [2.0] a MoE model with mixer graphs: every prompt goes the grouped-MoE path (onw/grouped.py), so only the
 
126
  THINK_BUDGET = {"low": 512, "medium": 2048, "high": None}
127
 
128
 
129
+ def vision_blob(d, c):
130
+ """[3.2] onw's blob store path of the vision encoder for config c (the plain route), or None."""
131
+ from .runtime import blob_path, NO_KEY
132
+ if not c.get("CACHE_DIR") or any(k.startswith("NPU_USE_NPUW") for k in c) or os.environ.get("ONW_OWN_BLOBS") == "0":
133
+ return None
134
+ xml = os.path.join(d, "vision.xml")
135
+ nocache = {k: v for k, v in c.items() if k != "CACHE_DIR"}
136
+ return blob_path(c["CACHE_DIR"], "vision.xml", [open(xml, "rb").read(), os.path.getsize(xml[:-4] + ".bin"),
137
+ ov.__version__, json.dumps({k: v for k, v in nocache.items()
138
+ if k not in NO_KEY}, sort_keys=True)])
139
+
140
+
141
+ def export_vision_blob(core, d, device, c):
142
+ """[3.2] Compile the vision encoder and write its blob (a background child at a first load: the first image
143
+ then only imports it - Panther Lake, Bonsai 2 4B: 144 s at the first image before)."""
144
+ import io
145
+ bp = vision_blob(d, c)
146
+ if bp is None or os.path.exists(bp) or os.path.exists(bp + ".bad"):
147
+ return
148
+ first = core.compile_model(os.path.join(d, "vision.xml"), device, c)
149
+ buf = io.BytesIO()
150
+ first.export_model(buf)
151
+ os.makedirs(os.path.dirname(bp), exist_ok=True)
152
+ with open(f"{bp}.{os.getpid()}.tmp", "wb") as f:
153
+ f.write(buf.getbuffer())
154
+ os.replace(f"{bp}.{os.getpid()}.tmp", bp)
155
+
156
+
157
  class ChatEngine:
158
  _think_marks = None # the thought markers when they are not <think> ... </think> (Gemma 4)
159
  swap_gb = None # memory-saving mode: GB of the 1-token graphs swapped in and out
 
287
  # [2.1.0] the answer graphs as stream_chat sets them for an answer of the usual length (n + up to 1024 new
288
  # tokens: the 2K variant), so the first question does not switch variants (Panther Lake, Granite 4.2 8B
289
  # NF4: 6 s added to the first prompt)
290
+ # [3.2] memory first: the paged graphs of those sizes go before the fused ones load, and the 1-token ones
291
+ # load first - their merged 2-bit group weights are the buffers the 64-token graphs then share (a graph
292
+ # must not share a buffer of a graph that is dropped later: Bonsai 2 9B crashed in the NPU driver)
293
+ for S in sorted(m._fused_sizes):
294
+ if hasattr(m, "_drop_paged_for_fused"):
295
+ m._drop_paged_for_fused(S, before=True)
296
+ for S in sorted(m._fused_sizes):
297
  m.fused_prepare(min(1024 if S > min(m._fused_sizes) else 2048, m.T), S)
298
+ self._venc_job = None
299
+ if self.vision and device == "NPU" and os.environ.get("ONW_VISION_PRECOMPILE", "1") != "0":
300
+ vb = vision_blob(engine_dir, self._venc_args[3])
301
+ if vb is not None and not os.path.exists(vb) and not os.path.exists(vb + ".bad"):
302
+ import subprocess, sys
303
+ flags = 0x00004000 if os.name == "nt" else 0 # (BELOW_NORMAL_PRIORITY_CLASS)
304
+ self._venc_job = subprocess.Popen(
305
+ [sys.executable, "-m", "onw.visionjob", json.dumps([engine_dir, device, self._venc_args[3]],
306
+ default=str)],
307
+ env={**os.environ, "PYTHONUTF8": "1"}, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
308
+ creationflags=flags)
309
+ print("[vision] encoder compiling in the background", flush=True)
310
  pld_state = "off (memory-saving mode)" if self.swap_gb is not None else "on" if self.pld else "off"
311
  print(f"[load] {time.time()-t0:.1f}s on {device}; verify block = {self.block_cost or 0:.1f} single steps, "
312
  f"prompt lookup decoding {pld_state}", flush=True)
 
315
  def venc(self):
316
  if self._venc is None:
317
  t0 = time.time()
318
+ job = getattr(self, "_venc_job", None)
319
+ if job is not None: # [3.2] (being compiled in the background: wait for its blob)
320
+ job.wait()
321
+ self._venc_job = None
322
  self._venc = self._compile_vision(*self._venc_args)
323
  print(f"[vision] encoder loaded in {time.time() - t0:.1f}s", flush=True)
324
  return self._venc
 
915
  self.model.fused_prepare(self.model.cur + ahead)
916
  # exact only for plain greedy; not while block sizes are swapped (the verification blocks go away mid-answer)
917
  use_pld = self.pld and plain_greedy and self.model.S > 1 and not swap and budget is None
918
+ fz = sorted(getattr(self.model, "_fused", None) or [])
919
+ if use_pld and fz[:1] == [1] and not any(1 < S <= 16 for S in fz) and os.environ.get("ONW_PLD_BIG") != "1":
920
+ # [3.2] answers on the fused 1-token graphs with no small block beside them (2-bit models: [1, 64]): a draft
921
+ # check would take the fused 64-token graphs, many fused steps (block_cost was timed on the paged graphs,
922
+ # whose single steps are slower - Bonsai 2 4B on Lunar Lake: answers 23 -> 13-17 tok/s with PLD)
923
+ use_pld = False
924
  # MTP drafts (Qwen3.5 dense): every sampling setting (exact), the thinking budget included
925
  mtp = getattr(self.model, "mtp", None) is not None and not swap and any(S >= 4 for S in [*self.model.segs, *(getattr(self.model, "_fused", None) or {})])
926
  use_pld = use_pld and not mtp
onw/fused.py CHANGED
@@ -209,7 +209,7 @@ def wanted(m):
209
  # fused graphs ran out of memory)
210
  from .runtime import _WEIGHTLESS
211
  on = (npukit.active() and npu_arch(m._ctx["core"]) >= 4000
212
- and (m.meta.get("needs") != "u2" or os.environ.get("ONW_U2_WEIGHTLESS") == "1")
213
  and not _WEIGHTLESS.get("skip_model") and _WEIGHTLESS["ok"] is not False and not _WEIGHTLESS.get("full"))
214
  if os.environ.get("ONW_FUSED", "1" if on else "0") != "1":
215
  return None
@@ -264,8 +264,9 @@ class FusedDecode:
264
  self.mask_kv_t = mk(ov.Type.f16, [1, self.Ct])
265
  self.mask_self_t = mk(ov.Type.f16, [S, S])
266
  t0, built = time.time(), 0
 
267
  for name, sm in sorted(m.meta["segments"].items()):
268
- if sm["S"] != S:
269
  continue
270
  # (NPUW: the model's shared mapping of the weight files and NPUW's reading, so its weights bank sees the
271
  # same constants as for the other block size - one copy of the weights)
@@ -441,7 +442,12 @@ class FusedDecode:
441
  "prev": {k: v.copy() for k, v in m.state.items() if k not in m._pp} if n > 1 else None,
442
  "pp": {k: e[1] for k, e in m._pp.items()} if n > 1 else None}
443
  logits, pend = None, None
444
- for st in self.plan:
 
 
 
 
 
445
  req = st["req"]
446
  if st["head"] and m.mtp is not None and st["h_in"] is not None:
447
  m.last_h = np.asarray(st["h_in"].data[0, :n], np.float32) # (what the MTP layer drafts from)
@@ -451,11 +457,15 @@ class FusedDecode:
451
  bufs, c = m._pp[k]
452
  req.set_tensor(k, bufs[c])
453
  req.set_tensor("ssm_new." + k[4:], bufs[1 - c])
 
 
454
  req.start_async()
455
  if pend is not None: # the previous segment's new rows while this one runs
456
  pend()
457
  pend = None
458
  req.wait()
 
 
459
 
460
  if n > 1:
461
  for o in st["rows"]: # (the request's own buffer: read by truncate() before the
@@ -479,9 +489,31 @@ class FusedDecode:
479
  pend = write
480
  if st["head"]:
481
  logits = np.array(req.get_tensor("logits").data[0, :n])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  if pend is not None:
483
  pend()
484
  m.cur += n
 
 
 
 
 
 
 
485
  if logits is not None:
486
  if not np.isfinite(logits).all():
487
  raise FloatingPointError(f"the fused {S}-token graphs returned NaN/inf logits")
 
209
  # fused graphs ran out of memory)
210
  from .runtime import _WEIGHTLESS
211
  on = (npukit.active() and npu_arch(m._ctx["core"]) >= 4000
212
+ and (m.meta.get("needs") != "u2" or os.environ.get("ONW_U2_WEIGHTLESS", "1") != "0") # [3.2] on
213
  and not _WEIGHTLESS.get("skip_model") and _WEIGHTLESS["ok"] is not False and not _WEIGHTLESS.get("full"))
214
  if os.environ.get("ONW_FUSED", "1" if on else "0") != "1":
215
  return None
 
264
  self.mask_kv_t = mk(ov.Type.f16, [1, self.Ct])
265
  self.mask_self_t = mk(ov.Type.f16, [S, S])
266
  t0, built = time.time(), 0
267
+ only = os.environ.get("ONW_FUSED_SEG") if compile_only else None # [3.2] (a child: this segment only)
268
  for name, sm in sorted(m.meta["segments"].items()):
269
+ if sm["S"] != S or (only and name != only):
270
  continue
271
  # (NPUW: the model's shared mapping of the weight files and NPUW's reading, so its weights bank sees the
272
  # same constants as for the other block size - one copy of the weights)
 
442
  "prev": {k: v.copy() for k, v in m.state.items() if k not in m._pp} if n > 1 else None,
443
  "pp": {k: e[1] for k, e in m._pp.items()} if n > 1 else None}
444
  logits, pend = None, None
445
+ import os, time
446
+ prof = None
447
+ if os.environ.get("ONW_PROFILE") == "1": # [3.2] per-segment NPU times, every 64 steps
448
+ prof = self.__dict__.setdefault("_prof", {"n": 0, "seg": [0.0] * len(self.plan), "step": 0.0})
449
+ t_step = time.perf_counter()
450
+ for si, st in enumerate(self.plan):
451
  req = st["req"]
452
  if st["head"] and m.mtp is not None and st["h_in"] is not None:
453
  m.last_h = np.asarray(st["h_in"].data[0, :n], np.float32) # (what the MTP layer drafts from)
 
457
  bufs, c = m._pp[k]
458
  req.set_tensor(k, bufs[c])
459
  req.set_tensor("ssm_new." + k[4:], bufs[1 - c])
460
+ if prof is not None:
461
+ t0 = time.perf_counter()
462
  req.start_async()
463
  if pend is not None: # the previous segment's new rows while this one runs
464
  pend()
465
  pend = None
466
  req.wait()
467
+ if prof is not None:
468
+ prof["seg"][si] += time.perf_counter() - t0
469
 
470
  if n > 1:
471
  for o in st["rows"]: # (the request's own buffer: read by truncate() before the
 
489
  pend = write
490
  if st["head"]:
491
  logits = np.array(req.get_tensor("logits").data[0, :n])
492
+ dead = ~logits.any(axis=1)
493
+ if dead.any() and st["h_in"] is not None:
494
+ # [3.2] rows of all zeros: the final RMSNorm's sum of squares overflowed fp16 (large activations in
495
+ # multi-token blocks - Bonsai 2 9B 64-token blocks on Lunar Lake, 20 of 256 rows), as
496
+ # SegmentedModel._head_again: the norm does not care about scale, so the head runs again on the
497
+ # hidden states divided by their max |h| (written in place: the next step overwrites them anyway)
498
+ h = st["h_in"].data
499
+ x = np.asarray(h[0, :n], np.float32)
500
+ h[0, :n] = (x / np.maximum(np.abs(x).max(-1, keepdims=True), 1e-2)).astype(h.dtype)
501
+ req.infer()
502
+ logits[dead] = np.array(req.get_tensor("logits").data[0, :n])[dead]
503
+ if not getattr(m, "_dead_note", False):
504
+ print("[head] logit rows of all zeros (fp16 overflow in the final norm) computed again, scaled",
505
+ flush=True)
506
+ m._dead_note = True
507
  if pend is not None:
508
  pend()
509
  m.cur += n
510
+ if prof is not None:
511
+ prof["step"] += time.perf_counter() - t_step
512
+ prof["n"] += 1
513
+ if prof["n"] % 64 == 0:
514
+ k = prof["n"]
515
+ print(f"[profile] fused {S}-token, {k} steps: {prof['step'] / k * 1e3:.2f} ms a step, segments "
516
+ + " ".join(f"{v / k * 1e3:.2f}" for v in prof["seg"]), flush=True)
517
  if logits is not None:
518
  if not np.isfinite(logits).all():
519
  raise FloatingPointError(f"the fused {S}-token graphs returned NaN/inf logits")
onw/i18n.json CHANGED
@@ -355,8 +355,8 @@
355
  "27B を三値(2 ビット)の重みで(Qwen3.8-27B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 10 GB で 27B が動きますが、プロンプトの処理は 1 トークンずつで遅い(約 4 tok/s)ので、短い質問向け": "27B with ternary (2-bit) weights (Ternary Bonsai 2, from Qwen3.8-27B; Lunar Lake and newer only). A 27B in about 10 GB of memory - but prompts are processed token by token, slowly (~4 tok/s): for short questions",
356
  "NPU を性能優先で動かす(ターボ)": "Run the NPU for speed (turbo)",
357
  "NPU のクロックを上げて、回答とプロンプトの読み込みを速くします(Lunar Lake で 2〜4 割、Panther Lake で 2 割ほど)。そのぶん消費電力が増えます(Lunar Lake で約 6 W)。電池で長く使いたいときはオフに。": "Raises the NPU's clocks: answers and prompt reading get faster (20-40% on Lunar Lake, ~20% on Panther Lake), at the cost of more power (~6 W on Lunar Lake). Turn it off for long battery use.",
358
- "三値(2 ビット)の 9B(Qwen3.5-9B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 3.5 GB と軽く、画像も読めます。プロンプトの処理は 1 トークンずつ(約 13 tok/s)なので、短めの質問向け": "Ternary (2-bit) 9B (Ternary Bonsai 2, based on Qwen3.5-9B; Lunar Lake and later only). Light at about 3.5 GB of memory and reads images. Prompts are processed one token at a time (about 13 tok/s): best for shorter questions",
359
- "三値(2 ビット)の軽い 4B(Qwen3.5-4B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 2 GB で約 21 tok/s、画像も読めます。プロンプトの処理は 1 トークンずつ(約 21 tok/s)なので、短めの質問向け": "A light ternary (2-bit) 4B (Ternary Bonsai 2, based on Qwen3.5-4B; Lunar Lake and later only). About 2 GB of memory, about 21 tok/s, reads images. Prompts are processed one token at a time (about 21 tok/s): best for shorter questions",
360
  "Qwen 系を高速化する": "Speed up the Qwen family",
361
  "Qwen3.5・Ornith などで、区間ごとの融合グラフと MTP 先読みを使います。プロンプトの処理が 2〜4 割、回答が 1 割ほど速くなり、メモリを約 1 GB 多く使います。Lunar Lake 以降で、メモリ節約モードがオフのときに働きます。": "Qwen3.5, Ornith and similar models run fused segment graphs with MTP draft tokens: prompts 20-40% and answers about 10% faster, about 1 GB more memory. Works from Lunar Lake on, while the memory-saving mode is off.",
362
  "モデルを検索": "Search models",
@@ -726,8 +726,8 @@
726
  "27B を三値(2 ビット)の重みで(Qwen3.8-27B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 10 GB で 27B が動きますが、プロンプトの処理は 1 トークンずつで遅い(約 4 tok/s)ので、短い質問向け": "三值(2 位)权重的 27B(基于 Qwen3.8-27B 的 Ternary Bonsai 2,仅限 Lunar Lake 及更新的 NPU)。约 10 GB 内存即可运行 27B,但提示词逐个 token 处理、较慢(约 4 tok/s),适合简短提问",
727
  "NPU を性能優先で動かす(ターボ)": "NPU 性能优先运行(加速模式)",
728
  "NPU のクロックを上げて、回答とプロンプトの読み込みを速くします(Lunar Lake で 2〜4 割、Panther Lake で 2 割ほど)。そのぶん消費電力が増えます(Lunar Lake で約 6 W)。電池で長く使いたいときはオフに。": "提高 NPU 频率,回答和提示词读取更快(Lunar Lake 约快 20~40%,Panther Lake 约 20%),但功耗增加(Lunar Lake 约 6 W)。需要长时间使用电池时请关闭。",
729
- "三値(2 ビット)の 9B(Qwen3.5-9B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 3.5 GB と軽く、画像も読めます。プロンプトの処理は 1 トークンずつ(約 13 tok/s)なので、短めの質問向け": "三值(2 位)9B(基于 Qwen3.5-9B 的 Ternary Bonsai 2,仅限 Lunar Lake 及以后)。内存约 3.5 GB,很轻,也能读图。提示词逐个词元处理(约 13 tok/s),适合较短的问题",
730
- "三値(2 ビット)の軽い 4B(Qwen3.5-4B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 2 GB で約 21 tok/s、画像も読めます。プロンプトの処理は 1 トークンずつ(約 21 tok/s)なので、短めの質問向け": "轻量的三值(2 位)4B(基于 Qwen3.5-4B 的 Ternary Bonsai 2,仅限 Lunar Lake 及以后)。内存约 2 GB,约 21 tok/s,也能读图。提示词逐个词元处理(约 21 tok/s),适合较短的问题",
731
  "Qwen 系を高速化する": "加速 Qwen 系列",
732
  "Qwen3.5・Ornith などで、区間ごとの融合グラフと MTP 先読みを使います。プロンプトの処理が 2〜4 割、回答が 1 割ほど速くなり、メモリを約 1 GB 多く使います。Lunar Lake 以降で、メモリ節約モードがオフのときに働きます。": "Qwen3.5、Ornith 等模型使用按区段融合的图和 MTP 预测:提示处理快 2〜4 成,回答快约 1 成,内存多用约 1 GB。适用于 Lunar Lake 及之后的机型,且内存节省模式关闭时。",
733
  "モデルを検索": "搜索模型",
 
355
  "27B を三値(2 ビット)の重みで(Qwen3.8-27B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 10 GB で 27B が動きますが、プロンプトの処理は 1 トークンずつで遅い(約 4 tok/s)ので、短い質問向け": "27B with ternary (2-bit) weights (Ternary Bonsai 2, from Qwen3.8-27B; Lunar Lake and newer only). A 27B in about 10 GB of memory - but prompts are processed token by token, slowly (~4 tok/s): for short questions",
356
  "NPU を性能優先で動かす(ターボ)": "Run the NPU for speed (turbo)",
357
  "NPU のクロックを上げて、回答とプロンプトの読み込みを速くします(Lunar Lake で 2〜4 割、Panther Lake で 2 割ほど)。そのぶん消費電力が増えます(Lunar Lake で約 6 W)。電池で長く使いたいときはオフに。": "Raises the NPU's clocks: answers and prompt reading get faster (20-40% on Lunar Lake, ~20% on Panther Lake), at the cost of more power (~6 W on Lunar Lake). Turn it off for long battery use.",
358
+ "三値(2 ビット)の 9B(Qwen3.5-9B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 3.5 GB と軽く、画像も読めます。onw 3.2 からプロンプトは 64 トークンずつ(Panther Lake で約 190 tok/s)": "Ternary (2-bit) 9B (Ternary Bonsai 2, based on Qwen3.5-9B; Lunar Lake and later only). Light at about 3.5 GB of memory and reads images. From onw 3.2 prompts go 64 tokens at a time (~190 tok/s on Panther Lake)",
359
+ "三値(2 ビット)の軽い 4B(Qwen3.5-4B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 2 GB で約 21 tok/s、画像も読めます。onw 3.2 からプロンプトは 64 トークンずつ(Panther Lake で約 300 tok/s)": "A light ternary (2-bit) 4B (Ternary Bonsai 2, based on Qwen3.5-4B; Lunar Lake and later only). About 2 GB of memory, about 21 tok/s, reads images. From onw 3.2 prompts go 64 tokens at a time (~300 tok/s on Panther Lake)",
360
  "Qwen 系を高速化する": "Speed up the Qwen family",
361
  "Qwen3.5・Ornith などで、区間ごとの融合グラフと MTP 先読みを使います。プロンプトの処理が 2〜4 割、回答が 1 割ほど速くなり、メモリを約 1 GB 多く使います。Lunar Lake 以降で、メモリ節約モードがオフのときに働きます。": "Qwen3.5, Ornith and similar models run fused segment graphs with MTP draft tokens: prompts 20-40% and answers about 10% faster, about 1 GB more memory. Works from Lunar Lake on, while the memory-saving mode is off.",
362
  "モデルを検索": "Search models",
 
726
  "27B を三値(2 ビット)の重みで(Qwen3.8-27B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 10 GB で 27B が動きますが、プロンプトの処理は 1 トークンずつで遅い(約 4 tok/s)ので、短い質問向け": "三值(2 位)权重的 27B(基于 Qwen3.8-27B 的 Ternary Bonsai 2,仅限 Lunar Lake 及更新的 NPU)。约 10 GB 内存即可运行 27B,但提示词逐个 token 处理、较慢(约 4 tok/s),适合简短提问",
727
  "NPU を性能優先で動かす(ターボ)": "NPU 性能优先运行(加速模式)",
728
  "NPU のクロックを上げて、回答とプロンプトの読み込みを速くします(Lunar Lake で 2〜4 割、Panther Lake で 2 割ほど)。そのぶん消費電力が増えます(Lunar Lake で約 6 W)。電池で長く使いたいときはオフに。": "提高 NPU 频率,回答和提示词读取更快(Lunar Lake 约快 20~40%,Panther Lake 约 20%),但功耗增加(Lunar Lake 约 6 W)。需要长时间使用电池时请关闭。",
729
+ "三値(2 ビット)の 9B(Qwen3.5-9B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 3.5 GB と軽く、画像も読めます。onw 3.2 からプロンプトは 64 トークンずつ(Panther Lake で約 190 tok/s)": "三值(2 位)9B(基于 Qwen3.5-9B 的 Ternary Bonsai 2,仅限 Lunar Lake 及以后)。内存约 3.5 GB,很轻,也能读图。自 onw 3.2 起提示词每次处理 64 个词元(Panther Lake 上约 190 tok/s)",
730
+ "三値(2 ビット)の軽い 4B(Qwen3.5-4B ベースの Ternary Bonsai 2、Lunar Lake 以降専用)。メモリ約 2 GB で約 21 tok/s、画像も読めます。onw 3.2 からプロンプトは 64 トークンずつ(Panther Lake で約 300 tok/s)": "轻量的三值(2 位)4B(基于 Qwen3.5-4B 的 Ternary Bonsai 2,仅限 Lunar Lake 及以后)。内存约 2 GB,约 21 tok/s,也能读图。自 onw 3.2 起提示词每次处理 64 个词元(Panther Lake 上约 300 tok/s)",
731
  "Qwen 系を高速化する": "加速 Qwen 系列",
732
  "Qwen3.5・Ornith などで、区間ごとの融合グラフと MTP 先読みを使います。プロンプトの処理が 2〜4 割、回答が 1 割ほど速くなり、メモリを約 1 GB 多く使います。Lunar Lake 以降で、メモリ節約モードがオフのときに働きます。": "Qwen3.5、Ornith 等模型使用按区段融合的图和 MTP 预测:提示处理快 2〜4 成,回答快约 1 成,内存多用约 1 GB。适用于 Lunar Lake 及之后的机型,且内存节省模式关闭时。",
733
  "モデルを検索": "搜索模型",
onw/ir.py CHANGED
@@ -9,6 +9,8 @@ Conventions shared by every model builder:
9
  the output side. Those are the weights-as-inputs forms the NPU runs at full speed (group-wise scales on
10
  runtime weights turn into slow element-wise work), and they let the host bind just the selected experts.
11
  """
 
 
12
  import numpy as np
13
  import openvino as ov
14
  import openvino.opset13 as ops
@@ -197,6 +199,12 @@ def ternary_weight(w):
197
  G = i // w.group
198
  t = ov.Tensor(ov.Type.u2, [o, G, w.group])
199
  np.copyto(t.data, np.asarray(w.packed).view(np.uint8).reshape(t.data.shape))
 
 
 
 
 
 
200
  d = ops.multiply(ops.subtract(ops.convert(ops.constant(t), ov.Type.f16), ops.constant(np.array(1.0, np.float16))),
201
  ops.constant(np.asarray(w.scales, np.float16).reshape(o, G, 1)))
202
  return ops.convert(ops.reshape(d, i64([o, i]), False), F32)
 
9
  the output side. Those are the weights-as-inputs forms the NPU runs at full speed (group-wise scales on
10
  runtime weights turn into slow element-wise work), and they let the host bind just the selected experts.
11
  """
12
+ import os
13
+
14
  import numpy as np
15
  import openvino as ov
16
  import openvino.opset13 as ops
 
199
  G = i // w.group
200
  t = ov.Tensor(ov.Type.u2, [o, G, w.group])
201
  np.copyto(t.data, np.asarray(w.packed).view(np.uint8).reshape(t.data.shape))
202
+ if os.environ.get("ONW_TERNARY_F32") == "1":
203
+ # (test) the chain in the f32 form NPUW's asymmetric-weight pattern (npuw/partitioning/patterns/dcoff.cpp,
204
+ # AsymmZP::CWAI) recognizes: only the 2-bit codes go to its shared weights bank, zero point and scale stay
205
+ d = ops.multiply(ops.subtract(ops.convert(ops.constant(t), F32), ops.constant(np.array(1.0, np.float32))),
206
+ ops.constant(np.asarray(w.scales, np.float32).reshape(o, G, 1)))
207
+ return ops.reshape(d, i64([o, i]), False)
208
  d = ops.multiply(ops.subtract(ops.convert(ops.constant(t), ov.Type.f16), ops.constant(np.array(1.0, np.float16))),
209
  ops.constant(np.asarray(w.scales, np.float16).reshape(o, G, 1)))
210
  return ops.convert(ops.reshape(d, i64([o, i]), False), F32)
onw/npukit/NOTICE.txt CHANGED
@@ -1,4 +1,4 @@
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/.
 
1
+ onw NPU kit (onw 3.2)
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/.
onw/npukit/kit.json CHANGED
@@ -1,5 +1,5 @@
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",
 
1
  {
2
+ "version": "3.2.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",
onw/npukit/linux/npukit-linux.tar.xz CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:46fa6b160305b720e56d0d4a44d76a344b06cf7ca886b2a26a447bfb338ba857
3
- size 32282368
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2eeec38edc79a59b39e52f4f65b3bdb1b6cba5f944f189aa2a2343057ef5efa8
3
+ size 33102668
onw/npukit/openvino_intel_npu_compiler.dll CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:ce1113abf245d6ada1fff920eb1373dc136e1190dc0558bc97d0b6c9ae27ed22
3
- size 83966464
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:609f80dad80b510eab914352b02dedf61c8744e0b5fc6da59c6816ba7ae2f105
3
+ size 84021760
onw/npukit/openvino_intel_npu_plugin.dll CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:91cf9ebe2bd9ef7a3e3e451b63e7497abe465bfa2396edb52b35e8eb1d4db2ce
3
- size 7992832
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fe7fd8fa7f0a5a69b7ad627a9d5568d5a0206cdedc0fd01d92fd5e590732a39e
3
+ size 8001024
onw/npukit/patches/npu_compiler-20e7912.patch CHANGED
@@ -51,6 +51,44 @@ index 3943fd7..c15b6aa 100644
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
@@ -74,6 +112,67 @@ index 8895bf9..1e0b4d3 100644
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
@@ -87,8 +186,32 @@ index fe48940..c819038 100644
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,
@@ -117,6 +240,22 @@ index 263a325..acf446c 100644
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
@@ -203,6 +342,76 @@ index f80f56e..3cfef97 100644
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
@@ -304,7 +513,7 @@ index 7390492..4dd29e4 100644
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 @@
@@ -318,7 +527,14 @@ index adaca74..fab49de 100644
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;
@@ -340,6 +556,331 @@ index adaca74..fab49de 100644
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
@@ -772,7 +1313,7 @@ index 4207b33..7c9f3d1 100644
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 @@
@@ -925,7 +1466,7 @@ index f3d8660..2e84289 100644
925
  return std::nullopt;
926
  }
927
 
928
- @@ -543,8 +630,61 @@ bool MergeVFRegionRewriter::checkVFCostFunction(VPU::VerticalFusionOp prevOp, VP
929
  return true;
930
  }
931
 
@@ -953,12 +1494,12 @@ index f3d8660..2e84289 100644
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));
@@ -987,7 +1528,26 @@ index f3d8660..2e84289 100644
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;
@@ -1015,7 +1575,7 @@ index f3d8660..2e84289 100644
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
 
@@ -1027,7 +1587,7 @@ index f3d8660..2e84289 100644
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).
@@ -1066,7 +1626,7 @@ index f3d8660..2e84289 100644
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());
@@ -1078,8 +1638,47 @@ index f3d8660..2e84289 100644
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 @@
@@ -1090,7 +1689,50 @@ index 35a6c00..d48e08c 100644
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) {
@@ -1100,7 +1742,7 @@ index 35a6c00..d48e08c 100644
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
 
@@ -1197,6 +1839,518 @@ index 35a6c00..d48e08c 100644
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
@@ -1293,7 +2447,7 @@ index 23fe4fc..b93a938 100644
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 @@
@@ -1311,7 +2465,7 @@ index 3f4f862..f5d66fe 100644
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
 
@@ -1348,18 +2502,24 @@ index 3f4f862..f5d66fe 100644
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) {
@@ -1403,7 +2563,10 @@ index 3f4f862..f5d66fe 100644
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;
@@ -1432,6 +2595,105 @@ index 3f4f862..f5d66fe 100644
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
@@ -1453,3 +2715,9 @@ index 10cb1f4..45e02e8 100644
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;
 
 
 
 
 
 
 
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/mpe_engine_utils.hpp b/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/mpe_engine_utils.hpp
55
+ index b058405..e8d6cb5 100644
56
+ --- a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/mpe_engine_utils.hpp
57
+ +++ b/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/mpe_engine_utils.hpp
58
+ @@ -5,7 +5,10 @@
59
+
60
+ #pragma once
61
+
62
+ +#include <cstdlib>
63
+ +
64
+ #include "vpux/compiler/core/types/quantile_float/types.hpp"
65
+ +#include "vpux/compiler/dialect/IE/IR/ops/convolution.hpp"
66
+ #include "vpux/compiler/dialect/IE/IR/ops/eltwise.hpp"
67
+ #include "vpux/compiler/dialect/IE/IR/ops/pooling.hpp"
68
+ #include "vpux/compiler/dialect/IE/utils/matmul.hpp"
69
+ @@ -23,8 +26,20 @@ namespace vpux::VPU {
70
+ */
71
+ class MPEEngineConfig {
72
+ public:
73
+ - static bool useNewWeightTableFormat(mlir::Operation*, bool) {
74
+ - return false;
75
+ + // onw: ONW_NEW_WT=1 - the split weight table (data pointers / scale / bias as separate operands) on NPU 50XX: the
76
+ + // per-channel scales then come from the model's constants (weightless: computed by the init schedule) instead of
77
+ + // being baked into the table rows, so graphs that differ only in weights compile to the same blob (test).
78
+ + static bool useNewWeightTableFormat(mlir::Operation* op, bool isCompressConv) {
79
+ + static const bool on = std::getenv("ONW_NEW_WT") != nullptr;
80
+ + if (!on || op == nullptr || isCompressConv) {
81
+ + return false;
82
+ + }
83
+ + // only plain convolutions: depthwise / pooling / eltwise NCE ops do not take the split table in this build
84
+ + // (NCE.DepthConvolution got null scale / bias operands)
85
+ + if (!mlir::isa<IE::ConvolutionOp, VPU::NCEConvolutionOp>(op)) {
86
+ + return false;
87
+ + }
88
+ + return config::getArch(op) == config::ArchKind::NPU50XX;
89
+ }
90
+
91
+ static bool isNewWeightTableFormatSupportedWithDwOps([[maybe_unused]] config::ArchKind arch) {
92
  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
93
  index 8895bf9..1e0b4d3 100644
94
  --- a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/multi_cluster_strategy_utils.hpp
 
112
  };
113
  vpux::VPU::StrategyCost correctSwOpCost(VPU::SWOpInterface swOp, ArrayRef<vpux::NDTypeInterface> tiledInputTypes,
114
  vpux::VPU::StrategyCost cost);
115
+ diff --git a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/sibling_ops_analysis.hpp b/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/sibling_ops_analysis.hpp
116
+ index 2eaaece..513fb6e 100644
117
+ --- a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/sibling_ops_analysis.hpp
118
+ +++ b/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/sibling_ops_analysis.hpp
119
+ @@ -10,6 +10,7 @@
120
+ #include <mlir/Dialect/Func/IR/FuncOps.h>
121
+ #include <mlir/IR/BuiltinTypes.h>
122
+
123
+ +#include <deque>
124
+ #include <set>
125
+ #include <vector>
126
+
127
+ @@ -33,13 +34,48 @@ public:
128
+ // ClusteredOp(producer) -> View-like op -> ClusteredOp(consumer)
129
+ virtual std::set<ClusteredOpInterface> getConsumers(ClusteredOpInterface);
130
+
131
+ + // onw: the same groups as getSiblings / getConsumers (same order), as views of the stored group - those return a
132
+ + // copy of the std::set, which was 21% of the strategy search on 2-bit (Bonsai) graphs. The views stay valid while
133
+ + // the analysis lives. Eager analyses (no lazy fill) answer through the copying calls.
134
+ + ArrayRef<ClusteredOpInterface> onwSiblingsView(ClusteredOpInterface);
135
+ + ArrayRef<ClusteredOpInterface> onwConsumersView(ClusteredOpInterface);
136
+ +
137
+ protected:
138
+ + bool _onwLazy = true;
139
+ + std::deque<SmallVector<ClusteredOpInterface>> _onwGroupVecs{};
140
+ + llvm::DenseMap<mlir::Operation*, size_t> _onwConsumerGroup{};
141
+ + SmallVector<ClusteredOpInterface> _onwScratch{};
142
+ std::set<ClusteredOpInterface> lookupOpSiblings(mlir::Operation* op);
143
+ mlir::Operation* getConsumerOp(ClusteredOpInterface);
144
+ + // onw: adds a group and indexes it by its members and by the op it was computed for
145
+ + void addGroup(mlir::Operation* queried, std::set<ClusteredOpInterface> group);
146
+ std::vector<std::set<ClusteredOpInterface>> _siblingGroups{};
147
+ + // onw: op -> index of the first group that holds it (or was computed for it); lookups were a linear scan of every
148
+ + // group, and an op found in none recomputed its siblings (and appended another group) on every call - the strategy
149
+ + // search of 2-bit (Bonsai) graphs, whose weights split into 20 per-group convolutions on one input, spent 56% there
150
+ + llvm::DenseMap<mlir::Operation*, size_t> _onwGroupOf{};
151
+ llvm::DenseMap<ClusteredOpInterface, std::set<ClusteredOpInterface>> _consumers{};
152
+ };
153
+
154
+ +// onw: the analysis of the pass running on this thread (MultiClusterStrategyAssignment publishes its own) - the
155
+ +// overload of getOutputOverlappedParams without an analysis (the split-over-height checks) built a fresh one on every
156
+ +// call, so the sibling search ran again for every op and strategy (2-bit Bonsai parts: 89% of a 6-minute compile).
157
+ +// ONW_NO_SIBLING_CACHE=1: as before.
158
+ +extern thread_local SiblingOpsAnalysis* onwCurrentSiblingAnalysis;
159
+ +// onw: drops this thread's overlapped-params cache (overlap_distribution_utils.cpp)
160
+ +void onwClearOverlapCache();
161
+ +struct OnwSiblingAnalysisScope {
162
+ + SiblingOpsAnalysis* prev;
163
+ + explicit OnwSiblingAnalysisScope(SiblingOpsAnalysis* a): prev(onwCurrentSiblingAnalysis) {
164
+ + onwCurrentSiblingAnalysis = a;
165
+ + onwClearOverlapCache();
166
+ + }
167
+ + ~OnwSiblingAnalysisScope() {
168
+ + onwCurrentSiblingAnalysis = prev;
169
+ + onwClearOverlapCache();
170
+ + }
171
+ +};
172
+ +
173
+ // Eager version of SiblingOpsAnalysis which doesn't modify internal state
174
+ // on calls to getConsumers/getSiblings. Suitable for usage with multithreading.
175
+ class EagerSiblingOpsAnalysis : public SiblingOpsAnalysis {
176
  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
177
  index fe48940..c819038 100644
178
  --- a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/vertical_fusion/v2/merge_vf_region_rewriter.hpp
 
186
  StrategyCost getOriginalVFCost(VPU::VerticalFusionOp vfOp) const;
187
 
188
  VFCacheAnalysis& _cache;
189
+ diff --git a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/weights_separation.hpp b/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/weights_separation.hpp
190
+ index 0beeab6..3d54599 100644
191
+ --- a/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/weights_separation.hpp
192
+ +++ b/src/vpux_compiler/include/vpux/compiler/dialect/VPU/utils/weights_separation.hpp
193
+ @@ -10,6 +10,9 @@
194
+ #include "vpux/compiler/utils/logging.hpp"
195
+
196
+ #include "mlir/Dialect/Func/IR/FuncOps.h"
197
+ +
198
+ +#include <map>
199
+ +#include <vector>
200
+ #include "mlir/Pass/AnalysisManager.h"
201
+
202
+ namespace vpux::VPU {
203
+ @@ -234,6 +237,9 @@ class ConstOpConverter {
204
+ // Note: operation cache is pimpl-ed to not expose it as a public class.
205
+ class OperationCache;
206
+ std::unique_ptr<OperationCache> _operationCache;
207
+ + // onw: results of folded sub-byte view chains (foldSubByteView) by input value and chain - identical chains (the
208
+ + // tiles of one weight) must give one value, i.e. one Init result, as the per-transformation cache does
209
+ + std::map<std::vector<const void*>, mlir::Value> _onwViewCache;
210
+
211
+ // IR building function with internal caching semantics.
212
+ std::tuple<ArrayRef<Const::TransformAttrInterface>, mlir::Value> createMatchingOperation(
213
  diff --git a/src/vpux_compiler/src/compiler.cpp b/src/vpux_compiler/src/compiler.cpp
214
+ index 263a325..419221a 100644
215
  --- a/src/vpux_compiler/src/compiler.cpp
216
  +++ b/src/vpux_compiler/src/compiler.cpp
217
  @@ -1087,7 +1087,11 @@ void compileIEtoVPU(mlir::OwningOpRef<mlir::ModuleOp>& moduleOp,
 
240
  vpuipPM.addPass(VPU::createDestructWsAnalysisPass(log));
241
  }
242
 
243
+ @@ -1154,6 +1160,7 @@ std::vector<CompilationResult> compileImplWsOneShot(
244
+ mlir::OwningOpRef<mlir::ModuleOp> moduleMain =
245
+ importNetwork(ctx, model, originalParameters, originalResults, config, devConf, rootTiming, log,
246
+ /*enableWeightsSeparationPath=*/true);
247
+ + moduleMain.get()->setAttr("VPU.OnwWeightless", mlir::UnitAttr::get(ctx)); // onw: see MergeParallelFullyConnected
248
+
249
+ OV_ITT_TASK_NEXT(COMPILER_IMPLEMENTATION, "compile");
250
+ auto factoryMethod = createDialectPipelineStrategyFn(config);
251
+ @@ -1276,6 +1283,7 @@ std::tuple<mlir::OwningOpRef<mlir::ModuleOp>, bool> compileImplWsIterative(
252
+ OV_ITT_TASK_NEXT(COMPILER_IMPLEMENTATION, "importNetwork");
253
+ auto moduleOp = importNetwork(ctx, model, originalParameters, originalResults, config, devConf, rootTiming, log,
254
+ /*enableWeightsSeparationPath=*/true);
255
+ + moduleOp.get()->setAttr("VPU.OnwWeightless", mlir::UnitAttr::get(ctx)); // onw: see MergeParallelFullyConnected
256
+
257
+ auto factoryMethod = createDialectPipelineStrategyFn(config);
258
+ auto mainPipelineStrategy = factoryMethod(config::CompilationMode::WSMain);
259
  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
260
  index f80f56e..3cfef97 100644
261
  --- a/src/vpux_compiler/src/conversion/passes/IE2VPU/convert_layers_to_VPU.cpp
 
342
 
343
  mlir::ConversionTarget target(ctx);
344
  target.addIllegalDialect<IE::IEDialect>();
345
+ diff --git a/src/vpux_compiler/src/dialect/IE/transforms/passes/forbid_four_bit_outputs.cpp b/src/vpux_compiler/src/dialect/IE/transforms/passes/forbid_four_bit_outputs.cpp
346
+ index bc08bb0..d67d826 100644
347
+ --- a/src/vpux_compiler/src/dialect/IE/transforms/passes/forbid_four_bit_outputs.cpp
348
+ +++ b/src/vpux_compiler/src/dialect/IE/transforms/passes/forbid_four_bit_outputs.cpp
349
+ @@ -23,6 +23,8 @@ namespace vpux::IE {
350
+ #include "vpux/compiler/dialect/IE/passes.hpp.inc"
351
+ } // namespace vpux::IE
352
+
353
+ +#include <cstdlib>
354
+ +
355
+ using namespace vpux;
356
+
357
+ namespace {
358
+ @@ -41,6 +43,11 @@ private:
359
+ if (archKind != config::ArchKind::NPU37XX) {
360
+ return;
361
+ }
362
+ + // onw: graphs without weights - the init schedule hands its 4-bit weights straight to the main schedule
363
+ + // (slices of the model's packed codes, see weights_separation foldSubByteView); 4-bit inputs already work
364
+ + if (moduleOp->hasAttr("VPU.OnwWeightless") && std::getenv("ONW_FORBID_4BIT_OUT") == nullptr) {
365
+ + return;
366
+ + }
367
+ auto netInfo = net::getNetworkInfo(moduleOp);
368
+ auto outputsInfo = netInfo.getOutputsDataInfo();
369
+ for (auto outputInfo : outputsInfo) {
370
+ diff --git a/src/vpux_compiler/src/dialect/IE/transforms/passes/merge_fully_connected.cpp b/src/vpux_compiler/src/dialect/IE/transforms/passes/merge_fully_connected.cpp
371
+ index baa9fec..029e104 100644
372
+ --- a/src/vpux_compiler/src/dialect/IE/transforms/passes/merge_fully_connected.cpp
373
+ +++ b/src/vpux_compiler/src/dialect/IE/transforms/passes/merge_fully_connected.cpp
374
+ @@ -3,6 +3,7 @@
375
+ // SPDX-License-Identifier: Apache-2.0
376
+ //
377
+
378
+ +#include <cstdlib>
379
+ #include "vpux/compiler/dialect/IE/IR/dialect.hpp"
380
+ #include "vpux/compiler/dialect/IE/IR/ops/convolution.hpp"
381
+ #include "vpux/compiler/dialect/IE/IR/ops/data_movement.hpp"
382
+ @@ -1516,6 +1517,13 @@ mlir::LogicalResult MergeFullyConnectedPass::initialize(mlir::MLIRContext* ctx)
383
+ }
384
+
385
+ void MergeFullyConnectedPass::safeRunOnFunc() {
386
+ + // onw: ONW_WS_NO_MERGE_FC=1 - weightless compiles keep the unrolled group matmuls as they are. (Merged, the
387
+ + // weight of k groups is a Concat of k group constants: VPU::WeightsSeparationInfo rewrites it into one chain on the
388
+ + // OV constant again, so it goes weightless - before, it stayed in the blob as a second copy of the codes.)
389
+ + if (auto mod = getOperation()->getParentOfType<mlir::ModuleOp>();
390
+ + mod != nullptr && mod->hasAttr("VPU.OnwWeightless") && std::getenv("ONW_WS_NO_MERGE_FC") != nullptr) {
391
+ + return;
392
+ + }
393
+ auto& ctx = getContext();
394
+ auto func = getOperation();
395
+
396
+ diff --git a/src/vpux_compiler/src/dialect/IE/transforms/passes/merge_parallel_fully_connected.cpp b/src/vpux_compiler/src/dialect/IE/transforms/passes/merge_parallel_fully_connected.cpp
397
+ index c2c08f6..a3edc00 100644
398
+ --- a/src/vpux_compiler/src/dialect/IE/transforms/passes/merge_parallel_fully_connected.cpp
399
+ +++ b/src/vpux_compiler/src/dialect/IE/transforms/passes/merge_parallel_fully_connected.cpp
400
+ @@ -838,6 +838,14 @@ private:
401
+ //
402
+
403
+ void MergeParallelFullyConnectedPass::safeRunOnFunc() {
404
+ + // onw: not when compiling without weights - the merged weights become one Const.Concat of several model constants,
405
+ + // which the weights separation cannot trace back to the model's files (every merged weight stays in the graph:
406
+ + // Ternary Bonsai 2 9B 1-token parts 331.8 MB weightless against 332.6 MB). ONW_WS_MERGE_FC=1 keeps the merge.
407
+ + if (auto mod = getOperation()->getParentOfType<mlir::ModuleOp>();
408
+ + mod != nullptr && mod->hasAttr("VPU.OnwWeightless") && std::getenv("ONW_WS_MERGE_FC") == nullptr) {
409
+ + return;
410
+ + }
411
+ +
412
+ auto& ctx = getContext();
413
+ auto func = getOperation();
414
+
415
  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
416
  index d16690c..93f292f 100644
417
  --- a/src/vpux_compiler/src/dialect/IE/transforms/passes/swap_convert_with_reshape_kind_ops.cpp
 
513
  case VPU::MultiClusterStrategy::Clustering:
514
  isStrategyCompatible = true;
515
  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
516
+ index adaca74..c5d7f27 100644
517
  --- a/src/vpux_compiler/src/dialect/VPU/transforms/passes/multi_cluster_strategy_assignment.cpp
518
  +++ b/src/vpux_compiler/src/dialect/VPU/transforms/passes/multi_cluster_strategy_assignment.cpp
519
  @@ -5,6 +5,10 @@
 
527
  #include "vpux/compiler/dialect/VPU/utils/cost_model/layer_vpunn_cost.hpp"
528
  #include "vpux/compiler/dialect/VPU/utils/manual_strategy_utils.hpp"
529
  #include "vpux/compiler/dialect/VPU/utils/multi_cluster_strategy_utils.hpp"
530
+ @@ -115,16 +119,28 @@ void MultiClusterStrategyAssignmentPass::safeRunOnFunc() {
531
+ cache.enableIfNecessary(clusteredOpCount > _clusteredOpThreshold);
532
+
533
+ auto& siblingAnalysis = getAnalysis<SiblingOpsAnalysis>();
534
+ + VPU::OnwSiblingAnalysisScope onwSiblingScope(&siblingAnalysis); // (onw: see sibling_ops_analysis.hpp)
535
+ StrategyManager strategyManager(func, tileOp.getCount(), _enablePrefetchTiling, _mcOptimizationScope,
536
+ siblingAnalysis, std::move(layerCostModel), _enableTilingFullSearchSpace,
537
+ _log.nest());
538
  _log.debug("Greedy Strategy Assignment");
539
  auto enableMultiClusterForSWLayer =
540
  config::getAvailableExecutor(module, config::ExecutorKind::SHAVE_ACT) != nullptr;
 
556
 
557
  _log.trace("Remove Temporary Strategy");
558
  strategyManager.removeTemporaryMulticlusterStrategy();
559
+ diff --git a/src/vpux_compiler/src/dialect/VPU/utils/overlap_distribution_utils.cpp b/src/vpux_compiler/src/dialect/VPU/utils/overlap_distribution_utils.cpp
560
+ index 62d2c5b..94ebf19 100644
561
+ --- a/src/vpux_compiler/src/dialect/VPU/utils/overlap_distribution_utils.cpp
562
+ +++ b/src/vpux_compiler/src/dialect/VPU/utils/overlap_distribution_utils.cpp
563
+ @@ -4,6 +4,7 @@
564
+ //
565
+
566
+ #include "vpux/compiler/dialect/VPU/utils/overlap_distribution_utils.hpp"
567
+ +#include <cstdlib>
568
+ #include "vpux/compiler/core/attributes/shape.hpp"
569
+ #include "vpux/compiler/core/tiling.hpp"
570
+ #include "vpux/compiler/dialect/VPU/IR/native_attributes/distribution_info.hpp"
571
+ @@ -18,6 +19,8 @@
572
+ #include <mlir/Dialect/Func/IR/FuncOps.h>
573
+
574
+ #include <stack>
575
+ +#include <unordered_map>
576
+ +#include <vector>
577
+
578
+ using namespace vpux;
579
+ using namespace VPU;
580
+ @@ -323,11 +326,106 @@ std::pair<Shape, Shape> getActualDataMemOffsetsAndShapes(VPU::SparseTensorType s
581
+ }
582
+
583
+ // Version with extension of halo
584
+ +static OverlapDistributionParams onwOverlapCompute(
585
+ + NDTypeInterface tensorType, ArrayRef<VPU::ClusteredOpInterface> consumerSubgraph, const int64_t numClusters,
586
+ + ArrayRef<int64_t> numTiles, const bool uniformDistributedSegments, const vpux::TileInfo& tileInfo,
587
+ + const std::optional<ArrayRef<int64_t>> outputTensorMemoryNumTiles,
588
+ + const std::optional<ArrayRef<int64_t>> alignment);
589
+ +
590
+ +namespace {
591
+ +// onw: the strategy search asks for the same overlapped params once per sibling and strategy check - a 2-bit Bonsai
592
+ +// matmul is split into one conv per weight group, so ~20 siblings each recomputed the whole group (94% of the strategy
593
+ +// pass). The result depends only on the values in the key, so it is cached while a pass publishes its sibling analysis
594
+ +// (OnwSiblingAnalysisScope clears it on entry and exit). ONW_NO_OVERLAP_CACHE=1: as before.
595
+ +struct OnwKeyHash {
596
+ + size_t operator()(const std::vector<int64_t>& k) const {
597
+ + return llvm::hash_combine_range(k.begin(), k.end());
598
+ + }
599
+ +};
600
+ +thread_local std::unordered_map<std::vector<int64_t>, OverlapDistributionParams, OnwKeyHash> onwOverlapCache;
601
+ +// kernel / strides / pads per NCE op: getKernelSizeVal builds the filter shape from attributes each time (more than
602
+ +// half of the key cost); the strategy pass only sets strategies, so they stay fixed while the scope is active
603
+ +thread_local llvm::DenseMap<mlir::Operation*, SmallVector<int64_t, 12>> onwNceGeometry;
604
+ +const bool onwOverlapCacheOff = std::getenv("ONW_NO_OVERLAP_CACHE") != nullptr;
605
+ +// ONW_NO_SIBLING_VIEW=1: siblings / consumers copied out of the analysis as before
606
+ +const bool onwSiblingViewOn = std::getenv("ONW_NO_SIBLING_VIEW") == nullptr;
607
+ +} // namespace
608
+ +
609
+ +void vpux::VPU::onwClearOverlapCache() {
610
+ + onwOverlapCache.clear();
611
+ + onwNceGeometry.clear();
612
+ +}
613
+ +
614
+ OverlapDistributionParams vpux::VPU::getOverlappedDistributionParameters(
615
+ NDTypeInterface tensorType, ArrayRef<VPU::ClusteredOpInterface> consumerSubgraph, const int64_t numClusters,
616
+ ArrayRef<int64_t> numTiles, const bool uniformDistributedSegments, const vpux::TileInfo& tileInfo,
617
+ const std::optional<ArrayRef<int64_t>> outputTensorMemoryNumTiles,
618
+ const std::optional<ArrayRef<int64_t>> alignment) {
619
+ + if (onwCurrentSiblingAnalysis == nullptr || onwOverlapCacheOff || tensorType == nullptr) {
620
+ + return onwOverlapCompute(tensorType, consumerSubgraph, numClusters, numTiles, uniformDistributedSegments,
621
+ + tileInfo, outputTensorMemoryNumTiles, alignment);
622
+ + }
623
+ + std::vector<int64_t> key;
624
+ + key.reserve(16 + 12 * consumerSubgraph.size());
625
+ + key.push_back(reinterpret_cast<int64_t>(tensorType.getAsOpaquePointer()));
626
+ + key.push_back(numClusters);
627
+ + key.push_back(uniformDistributedSegments ? 1 : 0);
628
+ + key.push_back(static_cast<int64_t>(numTiles.size()));
629
+ + key.insert(key.end(), numTiles.begin(), numTiles.end());
630
+ + key.push_back(outputTensorMemoryNumTiles.has_value() ? static_cast<int64_t>(outputTensorMemoryNumTiles->size())
631
+ + : -1);
632
+ + if (outputTensorMemoryNumTiles.has_value()) {
633
+ + key.insert(key.end(), outputTensorMemoryNumTiles->begin(), outputTensorMemoryNumTiles->end());
634
+ + }
635
+ + key.push_back(alignment.has_value() ? static_cast<int64_t>(alignment->size()) : -1);
636
+ + if (alignment.has_value()) {
637
+ + key.insert(key.end(), alignment->begin(), alignment->end());
638
+ + }
639
+ + bool anyNce = false;
640
+ + for (auto clusteredOp : consumerSubgraph) {
641
+ + auto nceOp = mlir::dyn_cast<VPU::NCEOpInterface>(clusteredOp.getOperation());
642
+ + if (nceOp == nullptr) {
643
+ + continue;
644
+ + }
645
+ + anyNce = true;
646
+ + auto [geo, fresh] = onwNceGeometry.try_emplace(nceOp.getOperation());
647
+ + if (fresh) {
648
+ + const auto kernel = nceOp.getKernelSizeVal();
649
+ + const auto strides = nceOp.getStridesVal();
650
+ + const auto pads = VPU::Padding::getClassFromAttr(nceOp.getPad());
651
+ + auto& g = geo->second;
652
+ + g.push_back(-7);
653
+ + g.push_back(static_cast<int64_t>(kernel.size()));
654
+ + g.append(kernel.begin(), kernel.end());
655
+ + g.push_back(static_cast<int64_t>(strides.size()));
656
+ + g.append(strides.begin(), strides.end());
657
+ + g.push_back(pads.getLeftPad());
658
+ + g.push_back(pads.getRightPad());
659
+ + g.push_back(pads.getTopPad());
660
+ + g.push_back(pads.getBottomPad());
661
+ + }
662
+ + key.insert(key.end(), geo->second.begin(), geo->second.end());
663
+ + key.push_back(reinterpret_cast<int64_t>(nceOp->getOperand(0).getType().getAsOpaquePointer()));
664
+ + }
665
+ + if (anyNce) {
666
+ + const auto clusteringAxis = VPU::getDistributedTilingAxis(numTiles);
667
+ + const auto tileOffset = !tileInfo.offsets.empty() ? tileInfo.offsets[Dim(clusteringAxis)] : 0;
668
+ + key.push_back(tileOffset != 0 ? 1 : 0);
669
+ + }
670
+ + if (auto it = onwOverlapCache.find(key); it != onwOverlapCache.end()) {
671
+ + return it->second;
672
+ + }
673
+ + auto result = onwOverlapCompute(tensorType, consumerSubgraph, numClusters, numTiles, uniformDistributedSegments,
674
+ + tileInfo, outputTensorMemoryNumTiles, alignment);
675
+ + onwOverlapCache.emplace(std::move(key), result);
676
+ + return result;
677
+ +}
678
+ +
679
+ +static OverlapDistributionParams onwOverlapCompute(
680
+ + NDTypeInterface tensorType, ArrayRef<VPU::ClusteredOpInterface> consumerSubgraph, const int64_t numClusters,
681
+ + ArrayRef<int64_t> numTiles, const bool uniformDistributedSegments, const vpux::TileInfo& tileInfo,
682
+ + const std::optional<ArrayRef<int64_t>> outputTensorMemoryNumTiles,
683
+ + const std::optional<ArrayRef<int64_t>> alignment) {
684
+ VPUX_THROW_WHEN(tensorType == nullptr, "getOverlappedDistributionParameters: tensorType cannot be nullptr");
685
+
686
+ auto neutralKernel = SmallVector<int64_t>{1, 1};
687
+ @@ -701,18 +799,23 @@ OverlapDistributionParams vpux::VPU::getActivationOverlappedParams(VPU::Clustere
688
+ auto sparseType = mlir::dyn_cast<vpux::VPU::SparseTensorType>(clusteredOp->getOperand(0).getType());
689
+ const bool isSparseTypeInputWithSeTable = (sparseType != nullptr) && (sparseType.getSeAttr() != nullptr);
690
+ SmallVector<VPU::ClusteredOpInterface> siblingSubgraph{};
691
+ + ArrayRef<VPU::ClusteredOpInterface> siblingView;
692
+ if (isSparseTypeInputWithSeTable) {
693
+ siblingSubgraph.push_back(clusteredOp);
694
+ + siblingView = siblingSubgraph;
695
+ + } else if (onwSiblingViewOn) {
696
+ + siblingView = siblingsAnalysis.onwSiblingsView(clusteredOp);
697
+ } else {
698
+ auto siblings = siblingsAnalysis.getSiblings(clusteredOp);
699
+ siblingSubgraph.append(siblings.begin(), siblings.end());
700
+ + siblingView = siblingSubgraph;
701
+ }
702
+
703
+ const auto clusteringAxis = VPU::getDistributedTilingAxis(activationTensorNumTiles);
704
+ return getOverlappedDistributionParameters(
705
+ (inputType != nullptr) ? inputType
706
+ : mlir::cast<vpux::NDTypeInterface>(clusteredOp->getOperand(0).getType()),
707
+ - siblingSubgraph, activationTensorNumTiles[clusteringAxis], activationTensorNumTiles,
708
+ + siblingView, activationTensorNumTiles[clusteringAxis], activationTensorNumTiles,
709
+ uniformDistributedSegments, tileInfo);
710
+ }
711
+
712
+ @@ -920,12 +1023,20 @@ OverlapDistributionParams vpux::VPU::getOutputOverlappedParams(
713
+ return getOutputOverlappedParamsNoHalo(clusteredOp, outputTensorNumTiles);
714
+ }
715
+
716
+ - auto consumers = siblingsAnalysis.getConsumers(clusteredOp);
717
+ + SmallVector<VPU::ClusteredOpInterface> consumerCopy;
718
+ + ArrayRef<VPU::ClusteredOpInterface> consumerView;
719
+ + if (onwSiblingViewOn) {
720
+ + consumerView = siblingsAnalysis.onwConsumersView(clusteredOp);
721
+ + } else {
722
+ + auto consumers = siblingsAnalysis.getConsumers(clusteredOp);
723
+ + consumerCopy.assign(consumers.begin(), consumers.end());
724
+ + consumerView = consumerCopy;
725
+ + }
726
+ const auto clusteringAxis = VPU::getDistributedTilingAxis(outputTensorNumTiles);
727
+ return getOverlappedDistributionParameters(
728
+ (outputType != nullptr) ? outputType
729
+ : mlir::cast<vpux::NDTypeInterface>(clusteredOp->getResult(0).getType()),
730
+ - SmallVector<VPU::ClusteredOpInterface>(consumers.begin(), consumers.end()),
731
+ + consumerView,
732
+ outputTensorNumTiles[clusteringAxis], outputTensorNumTiles, uniformDistributedSegments, tileInfo,
733
+ outputTensorMemoryNumTiles, alignment);
734
+ }
735
+ @@ -941,6 +1052,10 @@ OverlapDistributionParams vpux::VPU::getOutputOverlappedParams(
736
+ return getOutputOverlappedParamsNoHalo(clusteredOp, outputTensorNumTiles);
737
+ }
738
+
739
+ + if (auto* shared = onwCurrentSiblingAnalysis; shared != nullptr && std::getenv("ONW_NO_SIBLING_CACHE") == nullptr) {
740
+ + return getOutputOverlappedParams(clusteredOp, outputTensorNumTiles, uniformDistributedSegments, outputType,
741
+ + tileInfo, *shared, outputTensorMemoryNumTiles, alignment);
742
+ + }
743
+ auto siblingsAnalysis = SiblingOpsAnalysis(clusteredOp);
744
+ return getOutputOverlappedParams(clusteredOp, outputTensorNumTiles, uniformDistributedSegments, outputType,
745
+ tileInfo, siblingsAnalysis, outputTensorMemoryNumTiles, alignment);
746
+ diff --git a/src/vpux_compiler/src/dialect/VPU/utils/sibling_ops_analysis.cpp b/src/vpux_compiler/src/dialect/VPU/utils/sibling_ops_analysis.cpp
747
+ index c5f0be3..c28612d 100644
748
+ --- a/src/vpux_compiler/src/dialect/VPU/utils/sibling_ops_analysis.cpp
749
+ +++ b/src/vpux_compiler/src/dialect/VPU/utils/sibling_ops_analysis.cpp
750
+ @@ -8,6 +8,8 @@
751
+
752
+ namespace vpux::VPU {
753
+
754
+ +thread_local SiblingOpsAnalysis* onwCurrentSiblingAnalysis = nullptr;
755
+ +
756
+ // Empty constructor to satisfy getAnalysis API requirements.
757
+ SiblingOpsAnalysis::SiblingOpsAnalysis(mlir::Operation*) {
758
+ }
759
+ @@ -16,17 +18,74 @@ std::set<ClusteredOpInterface> SiblingOpsAnalysis::lookupOpSiblings(mlir::Operat
760
+ if (op == nullptr) {
761
+ return {};
762
+ }
763
+ - if (auto clusteredOp = mlir::dyn_cast<VPU::ClusteredOpInterface>(op)) {
764
+ - for (const auto& siblingGroup : _siblingGroups) {
765
+ - if (siblingGroup.find(clusteredOp) != siblingGroup.end()) {
766
+ - return siblingGroup;
767
+ - }
768
+ - }
769
+ + if (auto it = _onwGroupOf.find(op); it != _onwGroupOf.end()) {
770
+ + return _siblingGroups[it->second];
771
+ }
772
+ -
773
+ return {};
774
+ }
775
+
776
+ +void SiblingOpsAnalysis::addGroup(mlir::Operation* queried, std::set<ClusteredOpInterface> group) {
777
+ + const size_t idx = _siblingGroups.size();
778
+ + for (auto member : group) { // (by value: getOperation is not const)
779
+ + _onwGroupOf.try_emplace(member.getOperation(), idx);
780
+ + }
781
+ + if (queried != nullptr) {
782
+ + _onwGroupOf.try_emplace(queried, idx);
783
+ + }
784
+ + _onwGroupVecs.emplace_back(group.begin(), group.end());
785
+ + _siblingGroups.emplace_back(std::move(group));
786
+ +}
787
+ +
788
+ +// (eager analyses are read from many threads at once - TilingStrategyAssignment's workers: their copies go into a
789
+ +// buffer of the calling thread; a shared member buffer was overwritten by other threads -> access violations)
790
+ +static thread_local SmallVector<ClusteredOpInterface> onwThreadScratch;
791
+ +
792
+ +ArrayRef<ClusteredOpInterface> SiblingOpsAnalysis::onwSiblingsView(ClusteredOpInterface clusteredOp) {
793
+ + if (!_onwLazy) {
794
+ + auto s = getSiblings(clusteredOp);
795
+ + onwThreadScratch.assign(s.begin(), s.end());
796
+ + return onwThreadScratch;
797
+ + }
798
+ + auto it = _onwGroupOf.find(clusteredOp.getOperation());
799
+ + if (it == _onwGroupOf.end() || _siblingGroups[it->second].empty()) {
800
+ + // as getSiblings: compute (an empty group is computed again, which gives an empty group again)
801
+ + addGroup(clusteredOp.getOperation(), getSiblingOps(clusteredOp));
802
+ + return _onwGroupVecs.back();
803
+ + }
804
+ + return _onwGroupVecs[it->second];
805
+ +}
806
+ +
807
+ +ArrayRef<ClusteredOpInterface> SiblingOpsAnalysis::onwConsumersView(ClusteredOpInterface clusteredOp) {
808
+ + if (!_onwLazy) {
809
+ + auto s = getConsumers(clusteredOp);
810
+ + onwThreadScratch.assign(s.begin(), s.end());
811
+ + return onwThreadScratch;
812
+ + }
813
+ + if (auto it = _onwConsumerGroup.find(clusteredOp.getOperation()); it != _onwConsumerGroup.end()) {
814
+ + return _onwGroupVecs[it->second];
815
+ + }
816
+ + if (_consumers.contains(clusteredOp)) {
817
+ + // filled by getConsumers before: answer with that copy
818
+ + const auto& s = _consumers.at(clusteredOp);
819
+ + onwThreadScratch.assign(s.begin(), s.end());
820
+ + return onwThreadScratch;
821
+ + }
822
+ + auto consumerOp = getConsumerOp(clusteredOp);
823
+ + if (consumerOp == nullptr) {
824
+ + return {};
825
+ + }
826
+ + size_t idx;
827
+ + auto it = _onwGroupOf.find(consumerOp);
828
+ + if (it != _onwGroupOf.end() && !_siblingGroups[it->second].empty()) {
829
+ + idx = it->second;
830
+ + } else {
831
+ + addGroup(consumerOp, getSiblingOps(consumerOp));
832
+ + idx = _siblingGroups.size() - 1;
833
+ + }
834
+ + _onwConsumerGroup.try_emplace(clusteredOp.getOperation(), idx);
835
+ + return _onwGroupVecs[idx];
836
+ +}
837
+ +
838
+ mlir::Operation* SiblingOpsAnalysis::getConsumerOp(ClusteredOpInterface clusteredOp) {
839
+ if (isPassthroughOp(clusteredOp.getOperation())) {
840
+ // For passthrough ops, ensure input and output tensors use the same pool of ops to
841
+ @@ -47,7 +106,7 @@ std::set<ClusteredOpInterface> SiblingOpsAnalysis::getSiblings(ClusteredOpInterf
842
+ auto siblings = lookupOpSiblings(clusteredOp);
843
+ if (siblings.empty()) {
844
+ siblings = getSiblingOps(clusteredOp);
845
+ - _siblingGroups.emplace_back(siblings);
846
+ + addGroup(clusteredOp.getOperation(), siblings);
847
+ }
848
+ return siblings;
849
+ }
850
+ @@ -63,13 +122,14 @@ std::set<ClusteredOpInterface> SiblingOpsAnalysis::getConsumers(ClusteredOpInter
851
+ auto consumers = lookupOpSiblings(consumerOp);
852
+ if (consumers.empty()) {
853
+ consumers = getSiblingOps(consumerOp);
854
+ - _siblingGroups.emplace_back(consumers);
855
+ + addGroup(consumerOp, consumers);
856
+ }
857
+ _consumers.insert(std::make_pair(clusteredOp, consumers));
858
+ return consumers;
859
+ }
860
+
861
+ EagerSiblingOpsAnalysis::EagerSiblingOpsAnalysis(mlir::func::FuncOp func): SiblingOpsAnalysis(func) {
862
+ + _onwLazy = false;
863
+ // Skip siblings for architectures that do not support halo overlap to save some compile time
864
+ if (!outputOverlappedParamsIsHaloSupported(func)) {
865
+ return;
866
+ @@ -77,7 +137,7 @@ EagerSiblingOpsAnalysis::EagerSiblingOpsAnalysis(mlir::func::FuncOp func): Sibli
867
+ func->walk([&](VPU::ClusteredOpInterface clusteredOp) {
868
+ if (lookupOpSiblings(clusteredOp.getOperation()).empty()) {
869
+ auto opSiblings = getSiblingOps(clusteredOp.getOperation());
870
+ - _siblingGroups.emplace_back(opSiblings);
871
+ + addGroup(clusteredOp.getOperation(), opSiblings);
872
+ }
873
+ auto consumerOp = getConsumerOp(clusteredOp);
874
+ if (consumerOp == nullptr) {
875
+ @@ -86,7 +146,7 @@ EagerSiblingOpsAnalysis::EagerSiblingOpsAnalysis(mlir::func::FuncOp func): Sibli
876
+ auto consumerSiblings = lookupOpSiblings(consumerOp);
877
+ if (consumerSiblings.empty()) {
878
+ consumerSiblings = getSiblingOps(consumerOp);
879
+ - _siblingGroups.emplace_back(consumerSiblings);
880
+ + addGroup(consumerOp, consumerSiblings);
881
+ }
882
+ _consumers.insert(std::make_pair(clusteredOp, consumerSiblings));
883
+ });
884
  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
885
  index 4bfec49..26e3194 100644
886
  --- a/src/vpux_compiler/src/dialect/VPU/utils/strategy_manager/multi_cluster_strategy_utils.cpp
 
1313
  return mlir::success();
1314
  }
1315
  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
1316
+ index f3d8660..2d4e4fb 100644
1317
  --- a/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v2/merge_vf_region_rewriter.cpp
1318
  +++ b/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v2/merge_vf_region_rewriter.cpp
1319
  @@ -28,6 +28,43 @@
 
1466
  return std::nullopt;
1467
  }
1468
 
1469
+ @@ -543,23 +630,108 @@ bool MergeVFRegionRewriter::checkVFCostFunction(VPU::VerticalFusionOp prevOp, VP
1470
  return true;
1471
  }
1472
 
 
1494
  +llvm::hash_code onwHashVF(VPU::VerticalFusionOp op) {
1495
  + llvm::hash_code h = llvm::hash_value(op->getAttrDictionary().getAsOpaquePointer());
1496
  + for (auto v : op->getOperands()) {
1497
+ + h = llvm::hash_combine(h, v ? onwHashType(v.getType()) : llvm::hash_code(0));
1498
  + }
1499
  + op.getBody()->walk([&](mlir::Operation* o) {
1500
  + h = llvm::hash_combine(h, o->getName().getAsOpaquePointer(), o->getAttrDictionary().getAsOpaquePointer());
1501
+ + for (auto v : o->getOperands()) { // (a null operand: an op being rewritten)
1502
+ + h = llvm::hash_combine(h, v ? onwHashType(v.getType()) : llvm::hash_code(0));
1503
  + }
1504
  + for (auto t : o->getResultTypes()) {
1505
  + h = llvm::hash_combine(h, onwHashType(t));
 
1528
  _log.trace("Starting vertical fusion for region with VerticalFusionOp {0} at location {1}", vfOp, vfOp->getLoc());
1529
  if (vfOp.getIsManualConfigured()) {
1530
  _log.trace("Skipping vertical fusion for manually configured VerticalFusionOp at location {0}", vfOp->getLoc());
1531
+ return mlir::failure();
1532
+ }
1533
+ + // onw: regions with split-weight-table convolutions (ONW_NEW_WT) are not merged - this build's merge leaves their
1534
+ + // scale / table operands unset (null) in the candidate regions
1535
+ + {
1536
+ + bool splitTable = false;
1537
+ + vfOp.getBody()->walk([&](VPU::NCEConvolutionOp conv) {
1538
+ + splitTable |= conv.getWeightTableScale() != nullptr;
1539
+ + });
1540
+ + for (auto operand : vfOp->getOperands()) {
1541
+ + if (auto prev = operand.getDefiningOp<VPU::VerticalFusionOp>()) {
1542
+ + prev.getBody()->walk([&](VPU::NCEConvolutionOp conv) {
1543
+ + splitTable |= conv.getWeightTableScale() != nullptr;
1544
+ + });
1545
+ + }
1546
+ + }
1547
+ + if (splitTable) {
1548
+ + return mlir::failure();
1549
+ + }
1550
+ + }
1551
 
1552
  VPU::VerticalFusionOp vfBlock = nullptr;
1553
  VPU::VerticalFusionOp parentVFOp = nullptr;
 
1575
 
1576
  _log.trace("Analyzing vertical fusion region with parent VerticalFusionOp {0} at location {1}", parentVFOp,
1577
  parentVFOp->getLoc());
1578
+ @@ -580,7 +752,10 @@ mlir::LogicalResult MergeVFRegionRewriter::matchAndRewrite(VPU::VerticalFusionOp
1579
  continue;
1580
  }
1581
 
 
1587
  _log.trace("Merged subgraph candidate {0}", vfBlock.getLoc());
1588
  // `findVFCase` internally compares operations using IR order which is calculated "on the fly" in e.g.
1589
  // `isBeforeInBlock()`
1590
+ @@ -590,15 +765,34 @@ mlir::LogicalResult MergeVFRegionRewriter::matchAndRewrite(VPU::VerticalFusionOp
1591
  // As `findVFCase` is expected to not modify the IR, what can be done is an eager recalculation
1592
  // of the operation orders that would ensure later calls to e.g. `isBeforeInBlock()` become concurrent
1593
  // reads instead of concurrent writes (which is no longer a data race).
 
1626
  if (checkOtherVFInput(vfOp, parentVFOp)) {
1627
  continue;
1628
  }
1629
+ @@ -614,7 +808,10 @@ mlir::LogicalResult MergeVFRegionRewriter::matchAndRewrite(VPU::VerticalFusionOp
1630
  }
1631
 
1632
  _log.trace("Merged subgraph {0}", vfBlock->getLoc());
 
1638
 
1639
  return mlir::success();
1640
  }
1641
+ diff --git a/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v2/vertical_fusion_config.cpp b/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v2/vertical_fusion_config.cpp
1642
+ index d25b349..e5fef7c 100644
1643
+ --- a/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v2/vertical_fusion_config.cpp
1644
+ +++ b/src/vpux_compiler/src/dialect/VPU/utils/vertical_fusion/v2/vertical_fusion_config.cpp
1645
+ @@ -3,6 +3,7 @@
1646
+ // SPDX-License-Identifier: Apache-2.0
1647
+ //
1648
+
1649
+ +#include <cstdlib>
1650
+ #include "vpux/compiler/dialect/VPU/utils/vertical_fusion/v2/vertical_fusion_config.hpp"
1651
+ #include "vpux/compiler/dialect/VPU/utils/distributed_tensor_utils.hpp"
1652
+ #include "vpux/compiler/dialect/VPU/utils/hash_utils.hpp"
1653
+ @@ -167,6 +168,16 @@ void VFConfig::init() {
1654
+
1655
+ if (_inputOps.empty()) {
1656
+ auto operations = getVFOperations();
1657
+ + if (std::getenv("ONW_DEBUG_NULLOP") != nullptr) { // onw: report ops with a null operand
1658
+ + for (auto* operation : operations) {
1659
+ + for (unsigned i = 0; i < operation->getNumOperands(); ++i) {
1660
+ + if (!operation->getOperand(i)) {
1661
+ + llvm::errs() << "ONW null operand " << i << " of " << operation->getName() << " at "
1662
+ + << operation->getLoc() << "\n";
1663
+ + }
1664
+ + }
1665
+ + }
1666
+ + }
1667
+ const auto allOperandsInputs = [&](auto* current) -> bool {
1668
+ return llvm::all_of(current->getOperands(), [&](mlir::Value operand) {
1669
+ return mlir::dyn_cast<mlir::BlockArgument>(operand) != nullptr ||
1670
+ @@ -197,7 +208,8 @@ void VFConfig::init() {
1671
+ break;
1672
+ }
1673
+
1674
+ - parent = parent->getOperand(0).getDefiningOp();
1675
+ + // onw: an op without operands (a constant feeding the split weight table's scale)
1676
+ + parent = parent->getNumOperands() > 0 ? parent->getOperand(0).getDefiningOp() : nullptr;
1677
+ }
1678
+ }
1679
+ }
1680
  diff --git a/src/vpux_compiler/src/dialect/VPU/utils/weights_separation.cpp b/src/vpux_compiler/src/dialect/VPU/utils/weights_separation.cpp
1681
+ index 35a6c00..cb47654 100644
1682
  --- a/src/vpux_compiler/src/dialect/VPU/utils/weights_separation.cpp
1683
  +++ b/src/vpux_compiler/src/dialect/VPU/utils/weights_separation.cpp
1684
  @@ -4,6 +4,7 @@
 
1689
  #include "vpux/compiler/core/force_link_macros.hpp"
1690
  #include "vpux/compiler/dialect/IE/IR/ops/data_movement.hpp"
1691
  #include "vpux/compiler/dialect/IE/IR/ops/data_type.hpp"
1692
+ @@ -128,10 +129,24 @@ bool isTrivialForWeightsSeparation(Const::DeclareOp constOp, bool skipViewLikeOn
1693
+ bool isSuitableForWeightlessCompilation(Const::DeclareOp constOp, bool skipViewLikeOnly) {
1694
+ // ignore all non-OV constants
1695
+ if (!Const::isOpenVINOConstant(constOp)) {
1696
+ + if (std::getenv("ONW_DEBUG_WS") != nullptr) {
1697
+ + const auto bt = mlir::cast<NDTypeInterface>(constOp.getContentAttr().getBaseContent().getType());
1698
+ + if (bt.getTotalAllocSize().count() >= (1 << 14)) {
1699
+ + llvm::errs() << "ONW ws non-OV constant: " << bt << " " << constOp.getLoc() << char(10);
1700
+ + }
1701
+ + }
1702
+ return false;
1703
+ }
1704
+
1705
+ if (isTrivialForWeightsSeparation(constOp, skipViewLikeOnly)) {
1706
+ + if (std::getenv("ONW_DEBUG_WS") != nullptr) {
1707
+ + const auto bt = mlir::cast<NDTypeInterface>(constOp.getContentAttr().getBaseContent().getType());
1708
+ + if (bt.getTotalAllocSize().count() >= (1 << 14)) {
1709
+ + llvm::errs() << "ONW ws trivial: " << bt << " splat=" << constOp.getContentAttr().isSplat()
1710
+ + << " skipViewLikeOnly=" << skipViewLikeOnly << " n="
1711
+ + << constOp.getContentAttr().getTransformations().size() << char(10);
1712
+ + }
1713
+ + }
1714
+ return false;
1715
+ }
1716
+
1717
+ @@ -160,7 +175,16 @@ bool isSuitableForWeightlessCompilation(Const::DeclareOp constOp, bool skipViewL
1718
+ return conversions::isSupportedTransformation(baseType, transformations.back());
1719
+ };
1720
+
1721
+ - return hasOnlySupportedTransformations(constOp);
1722
+ + const bool supported = hasOnlySupportedTransformations(constOp);
1723
+ + if (!supported && std::getenv("ONW_DEBUG_WS") != nullptr) {
1724
+ + llvm::errs() << "ONW ws rejected: " << constOp.getContentAttr().getBaseContent().getType() << " "
1725
+ + << constOp.getContentAttr().getTransformations().size() << " transformations:";
1726
+ + for (auto t : constOp.getContentAttr().getTransformations()) {
1727
+ + llvm::errs() << " " << t;
1728
+ + }
1729
+ + llvm::errs() << char(10);
1730
+ + }
1731
+ + return supported;
1732
+ }
1733
+
1734
+ // Choose only constants that are used in multiple kernel functions
1735
+ @@ -345,6 +369,9 @@ mlir::Value createAvgPoolForInterQuantizedConvert(mlir::OpBuilder& builder, mlir
1736
  /// Returns an IE operation for the given constant transformation.
1737
  mlir::Value createMatchingIeOperation(mlir::OpBuilder& builder, mlir::Location loc, mlir::Value input,
1738
  Const::TransformAttrInterface t) {
 
1742
  return llvm::TypeSwitch<Const::TransformAttrInterface, mlir::Value>(t)
1743
  .Case<Const::AddAttr>([&](Const::AddAttr add) {
1744
  const auto biasValue = checked_cast<float>(add.getBias().getValueAsDouble());
1745
+ @@ -398,15 +425,94 @@ mlir::Value createMatchingIeOperation(mlir::OpBuilder& builder, mlir::Location l
1746
  const auto inElemType = mlir::cast<NDTypeInterface>(input.getType()).getElementType();
1747
  const auto outElemType = cast.getElemType();
1748
 
 
1839
  const auto inElemType = mlir::dyn_cast_or_null<mlir::quant::QuantizedType>(
1840
  mlir::cast<NDTypeInterface>(input.getType()).getElementType());
1841
  const auto outElemType = mlir::dyn_cast_or_null<mlir::quant::QuantizedType>(convert.getElemType());
1842
+ @@ -480,15 +586,25 @@ mlir::Value createMatchingIeOperation(mlir::OpBuilder& builder, mlir::Location l
1843
+ })
1844
+ .Case<Const::RescaleAttr>([&](Const::RescaleAttr rescale) {
1845
+ auto foldedAttr = rescale.getScale().fold();
1846
+ - const auto scaleValue = foldedAttr.getSplatValue<float>();
1847
+ -
1848
+ const auto scaleLoc = appendLoc(loc, "scale");
1849
+ - SmallVector<int64_t> shapeRank = {1};
1850
+ - auto scaleType = mlir::RankedTensorType::get({shapeRank}, mlir::Float32Type::get(builder.getContext()));
1851
+ auto transform = [&](Const::ContentSetup& setup) -> Const::ContentSetup {
1852
+ return setup.castElemType(mlir::cast<NDTypeInterface>(input.getType()).getElementType());
1853
+ };
1854
+ - auto scale = Const::createConst<float>(builder, scaleLoc, scaleType, {scaleValue}, transform);
1855
+ + mlir::Value scale;
1856
+ + if (foldedAttr.isSplat()) {
1857
+ + const auto scaleValue = foldedAttr.getSplatValue<float>();
1858
+ + SmallVector<int64_t> shapeRank = {1};
1859
+ + auto scaleType =
1860
+ + mlir::RankedTensorType::get({shapeRank}, mlir::Float32Type::get(builder.getContext()));
1861
+ + scale = Const::createConst<float>(builder, scaleLoc, scaleType, {scaleValue}, transform);
1862
+ + } else {
1863
+ + // onw: per-channel scales (2-bit weights): a constant of the scale's own shape, broadcast by the
1864
+ + // Multiply (numpy rules, as Const::RescaleAttr::transform applies it)
1865
+ + const auto values = to_small_vector(foldedAttr.getValues<float>());
1866
+ + const auto shape = to_small_vector(rescale.getScale().getType().getShape().raw());
1867
+ + auto scaleType = mlir::RankedTensorType::get(shape, mlir::Float32Type::get(builder.getContext()));
1868
+ + scale = Const::createConst<float>(builder, scaleLoc, scaleType, values, transform);
1869
+ + }
1870
+
1871
+ return builder.create<IE::MultiplyOp>(loc, input, scale, IE::AutoBroadcastType::NUMPY,
1872
+ /*postOp=*/nullptr, /*clamp=*/nullptr,
1873
+ @@ -1071,6 +1187,330 @@ std::tuple<ArrayRef<Const::TransformAttrInterface>, mlir::Value> ConstOpConverte
1874
+ return {transformations.drop_front(), outputValue};
1875
+ }
1876
+
1877
+ +namespace {
1878
+ +// onw: sub-byte weights (2-bit Bonsai, 4-bit) in init schedules. Their constant chains unpack the codes to f16 only to
1879
+ +// move them around (transpose, subview, reshape) and cast them back to a sub-byte type of the same storage - which the
1880
+ +// NPU 40XX Convert kernels cannot write for 2 bits ("f16 -> ui2 not supported"). When the whole chain only moves the
1881
+ +// values (value-preserving casts and view ops), its result is a box of the original tensor in its own order, reshaped:
1882
+ +// that is a Slice + Reshape on the packed codes plus a reinterpretation of the storage - no unpacking at all.
1883
+ +
1884
+ +mlir::Type subByteStorage(mlir::Type t) {
1885
+ + for (int k = 0; k < 2; ++k) {
1886
+ + if (auto q = mlir::dyn_cast<mlir::quant::QuantizedType>(t)) {
1887
+ + t = q.getStorageType();
1888
+ + } else if (auto q = mlir::dyn_cast<vpux::type::QuantileType>(t)) {
1889
+ + t = q.getStorageType();
1890
+ + }
1891
+ + }
1892
+ + return t;
1893
+ +}
1894
+ +
1895
+ +// a cast that keeps the code values of `storage` (unsigned / signed integers of width w): integer widening of the same
1896
+ +// signedness, integer -> float, float -> float of >= 16 bits, float / integer -> a type stored as `storage`, and
1897
+ +// quantized / quantile reinterpretations of the same storage
1898
+ +bool keepsCodes(mlir::Type in, mlir::Type out, mlir::IntegerType storage) {
1899
+ + const auto inS = subByteStorage(in), outS = subByteStorage(out);
1900
+ + auto isInt = [](mlir::Type t) {
1901
+ + return mlir::isa<mlir::IntegerType>(t);
1902
+ + };
1903
+ + auto isFloat16Plus = [](mlir::Type t) {
1904
+ + return mlir::isa<mlir::FloatType>(t) && mlir::cast<mlir::FloatType>(t).getWidth() >= 16;
1905
+ + };
1906
+ + if (outS == storage) {
1907
+ + return inS == storage || isFloat16Plus(inS) ||
1908
+ + (isInt(inS) && mlir::cast<mlir::IntegerType>(inS).isSigned() == storage.isSigned());
1909
+ + }
1910
+ + // the lookup table of NPU 40XX+ (convert_to_palletization_lut_strategy): signed codes become unsigned table
1911
+ + // indices with the same bits (-7 = 4'b1001 is index 9) - a reinterpretation of storage, as the compiler meant it
1912
+ + if (inS == storage && isInt(outS) && mlir::cast<mlir::IntegerType>(outS).getWidth() == storage.getWidth() &&
1913
+ + mlir::isa<mlir::quant::QuantizedType>(out) &&
1914
+ + mlir::isa<vpux::type::QuantileType>(mlir::cast<mlir::quant::QuantizedType>(out).getStorageType())) {
1915
+ + return true;
1916
+ + }
1917
+ + if (isInt(inS) && isInt(outS)) {
1918
+ + const auto a = mlir::cast<mlir::IntegerType>(inS), b = mlir::cast<mlir::IntegerType>(outS);
1919
+ + return b.getWidth() >= a.getWidth() && a.isSigned() == b.isSigned() && a.isSigned() == storage.isSigned();
1920
+ + }
1921
+ + if (isInt(inS) && isFloat16Plus(outS)) {
1922
+ + return true;
1923
+ + }
1924
+ + return isFloat16Plus(inS) && isFloat16Plus(outS);
1925
+ +}
1926
+ +
1927
+ +struct View {
1928
+ + SmallVector<int64_t> shape, strides;
1929
+ + int64_t offset = 0;
1930
+ +};
1931
+ +
1932
+ +// strides of `newShape` over the same elements as `v` (a reshape of a view), or false when not expressible
1933
+ +bool reshapeView(View& v, ArrayRef<int64_t> newShape) {
1934
+ + // drop size-1 dims, then match products of old and new dims chunk by chunk
1935
+ + SmallVector<int64_t> os, ost;
1936
+ + for (size_t i = 0; i < v.shape.size(); ++i) {
1937
+ + if (v.shape[i] != 1) {
1938
+ + os.push_back(v.shape[i]);
1939
+ + ost.push_back(v.strides[i]);
1940
+ + }
1941
+ + }
1942
+ + SmallVector<int64_t> nst(newShape.size(), 0);
1943
+ + size_t oi = 0;
1944
+ + size_t ni = 0;
1945
+ + while (ni < newShape.size()) {
1946
+ + if (newShape[ni] == 1) {
1947
+ + nst[ni] = 1;
1948
+ + ++ni;
1949
+ + continue;
1950
+ + }
1951
+ + if (oi >= os.size()) {
1952
+ + return false;
1953
+ + }
1954
+ + // a chunk: old dims oi..oj (contiguous in the view) against new dims ni..nj with the same product
1955
+ + int64_t op = os[oi], np = newShape[ni];
1956
+ + size_t oj = oi, nj = ni;
1957
+ + while (op != np) {
1958
+ + if (op < np) {
1959
+ + if (++oj >= os.size() || ost[oj - 1] != ost[oj] * os[oj]) {
1960
+ + return false;
1961
+ + }
1962
+ + op *= os[oj];
1963
+ + } else {
1964
+ + if (++nj >= newShape.size()) {
1965
+ + return false;
1966
+ + }
1967
+ + np *= newShape[nj];
1968
+ + }
1969
+ + }
1970
+ + for (size_t k = oi; k < oj; ++k) {
1971
+ + if (ost[k] != ost[k + 1] * os[k + 1]) {
1972
+ + return false;
1973
+ + }
1974
+ + }
1975
+ + int64_t st = ost[oj];
1976
+ + for (size_t k = nj + 1; k-- > ni;) {
1977
+ + nst[k] = st;
1978
+ + st *= newShape[k];
1979
+ + }
1980
+ + oi = oj + 1;
1981
+ + ni = nj + 1;
1982
+ + }
1983
+ + if (oi != os.size()) {
1984
+ + return false;
1985
+ + }
1986
+ + v.shape.assign(newShape.begin(), newShape.end());
1987
+ + v.strides = nst;
1988
+ + return true;
1989
+ +}
1990
+ +
1991
+ +// The chain `ts` applied to `input` (sub-byte codes) as Slice + Reshape + QuantizeCast, or nullptr.
1992
+ +// [onw] Group rows: a view that takes k consecutive groups of a weight [N, G, K] as [k, N, K] (the merged group
1993
+ +// matmuls of a one-token graph: groups and rows swapped) is no box of the weight; it is built as a Concat of the k
1994
+ +// group slices [N, 1, K] (byte moves of the packed codes), then sliced / reshaped like a box of that [k, N, K] stack.
1995
+ +mlir::Value foldSubByteView(mlir::OpBuilder& builder, mlir::Location loc, mlir::Value input,
1996
+ + ArrayRef<Const::TransformAttrInterface> ts) {
1997
+ + if (std::getenv("ONW_WS_VIEWFOLD") != nullptr && std::string(std::getenv("ONW_WS_VIEWFOLD")) == "0") {
1998
+ + return nullptr;
1999
+ + }
2000
+ + auto inType = mlir::cast<NDTypeInterface>(input.getType());
2001
+ + auto storage = mlir::dyn_cast<mlir::IntegerType>(subByteStorage(inType.getElementType()));
2002
+ + if (storage == nullptr || storage.getWidth() >= 8 || ts.empty() || !inType.getDimsOrder().isIdentity()) {
2003
+ + return nullptr;
2004
+ + }
2005
+ + const auto origShape = to_small_vector(inType.getShape().raw());
2006
+ + SmallVector<int64_t> srcShape = origShape; // what the view addresses: the weight, or the group stack
2007
+ + std::optional<std::pair<int64_t, int64_t>> groupRows; // (first group, groups) once rebased onto the stack
2008
+ + View v;
2009
+ + v.shape = origShape;
2010
+ + v.strides.assign(origShape.size(), 1);
2011
+ + for (size_t i = origShape.size() - 1; i-- > 0;) {
2012
+ + v.strides[i] = v.strides[i + 1] * origShape[i + 1];
2013
+ + }
2014
+ + // the view as k groups x N rows x K of a [N, G, K] weight (all rows, all of K) -> the same view on the stack
2015
+ + const auto rebase = [&]() -> bool {
2016
+ + if (groupRows.has_value() || origShape.size() != 3 || std::getenv("ONW_WS_NO_GROUP_ROWS") != nullptr) {
2017
+ + return false;
2018
+ + }
2019
+ + const int64_t N = origShape[0], G = origShape[1], K = origShape[2];
2020
+ + SmallVector<size_t> dims;
2021
+ + for (size_t i = 0; i < v.shape.size(); ++i) {
2022
+ + if (v.shape[i] != 1) {
2023
+ + dims.push_back(i);
2024
+ + }
2025
+ + }
2026
+ + if (dims.size() != 3 || v.strides[dims[0]] != K || v.shape[dims[1]] != N || v.strides[dims[1]] != G * K ||
2027
+ + v.shape[dims[2]] != K || v.strides[dims[2]] != 1 || v.offset % K != 0) {
2028
+ + return false;
2029
+ + }
2030
+ + const int64_t g0 = v.offset / K, k = v.shape[dims[0]];
2031
+ + if (g0 < 0 || g0 + k > G || k < 2) {
2032
+ + return false;
2033
+ + }
2034
+ + groupRows = std::make_pair(g0, k);
2035
+ + srcShape = {k, N, K};
2036
+ + v.strides[dims[0]] = N * K;
2037
+ + v.strides[dims[1]] = K;
2038
+ + v.strides[dims[2]] = 1;
2039
+ + v.offset = 0;
2040
+ + return true;
2041
+ + };
2042
+ + NDTypeInterface cur = inType;
2043
+ + for (auto t : ts) {
2044
+ + const auto next = t.inferOutputType(cur);
2045
+ + const auto reshapeTo = [&](ArrayRef<int64_t> shape) {
2046
+ + View keep = v;
2047
+ + if (reshapeView(v, shape)) {
2048
+ + return true;
2049
+ + }
2050
+ + v = keep;
2051
+ + return rebase() && reshapeView(v, shape);
2052
+ + };
2053
+ + const bool ok = llvm::TypeSwitch<Const::TransformAttrInterface, bool>(t)
2054
+ + .Case<Const::CastElemTypeAttr, Const::ConvertElemTypeAttr>([&](auto) {
2055
+ + return keepsCodes(cur.getElementType(), next.getElementType(), storage);
2056
+ + })
2057
+ + .Case<Const::TransposeAttr>([&](Const::TransposeAttr tr) {
2058
+ + const auto map = tr.getOrder().getValue();
2059
+ + View w;
2060
+ + for (unsigned i = 0; i < map.getNumResults(); ++i) {
2061
+ + const auto d = map.getDimPosition(i);
2062
+ + w.shape.push_back(v.shape[d]);
2063
+ + w.strides.push_back(v.strides[d]);
2064
+ + }
2065
+ + w.offset = v.offset;
2066
+ + v = w;
2067
+ + return true;
2068
+ + })
2069
+ + .Case<Const::SubViewAttr>([&](Const::SubViewAttr sv) {
2070
+ + const auto off = parseIntArrayAttr<int64_t>(sv.getOffset());
2071
+ + const auto shp = parseIntArrayAttr<int64_t>(sv.getShape());
2072
+ + for (size_t i = 0; i < off.size(); ++i) {
2073
+ + v.offset += off[i] * v.strides[i];
2074
+ + }
2075
+ + v.shape.assign(shp.begin(), shp.end());
2076
+ + return true;
2077
+ + })
2078
+ + .Case<Const::ReshapeAttr>([&](Const::ReshapeAttr r) {
2079
+ + return reshapeTo(parseIntArrayAttr<int64_t>(r.getShape()));
2080
+ + })
2081
+ + .Case<Const::AffineReshapeAttr>([&](Const::AffineReshapeAttr r) {
2082
+ + return reshapeTo(parseIntArrayAttr<int64_t>(r.getShapeValue()));
2083
+ + })
2084
+ + .Default([](Const::TransformAttrInterface) {
2085
+ + return false;
2086
+ + });
2087
+ + if (!ok || !next.getDimsOrder().isIdentity()) {
2088
+ + return nullptr;
2089
+ + }
2090
+ + cur = next;
2091
+ + }
2092
+ + {
2093
+ + auto outStorage = mlir::dyn_cast<mlir::IntegerType>(subByteStorage(cur.getElementType()));
2094
+ + if (outStorage == nullptr || outStorage.getWidth() != storage.getWidth()) {
2095
+ + return nullptr;
2096
+ + }
2097
+ + }
2098
+ + // the result as a box of the source (the weight or the group stack) in its own row-major order: walk the non-1
2099
+ + // result dims from the innermost out, each covering one source dim (or several whole inner ones)
2100
+ + SmallVector<int64_t> boxOff, boxSize;
2101
+ + const auto findBox = [&]() -> bool {
2102
+ + boxOff.assign(srcShape.size(), 0);
2103
+ + boxSize.assign(srcShape.size(), 1);
2104
+ + int64_t rem = v.offset;
2105
+ + SmallVector<int64_t> ost(srcShape.size(), 1);
2106
+ + for (size_t i = srcShape.size() - 1; i-- > 0;) {
2107
+ + ost[i] = ost[i + 1] * srcShape[i + 1];
2108
+ + }
2109
+ + for (size_t i = 0; i < srcShape.size(); ++i) {
2110
+ + boxOff[i] = rem / ost[i];
2111
+ + rem %= ost[i];
2112
+ + }
2113
+ + int d = static_cast<int>(srcShape.size()) - 1;
2114
+ + for (size_t k = v.shape.size(); k-- > 0;) {
2115
+ + int64_t n = v.shape[k];
2116
+ + if (n == 1) {
2117
+ + continue;
2118
+ + }
2119
+ + const int64_t s = v.strides[k];
2120
+ + while (d >= 0 && (ost[d] != s || srcShape[d] == 1)) {
2121
+ + if (ost[d] < s) {
2122
+ + --d; // a source dim skipped: the box takes one element of it (its offset)
2123
+ + continue;
2124
+ + }
2125
+ + return false; // a stride that is no source dim's: the values are reordered
2126
+ + }
2127
+ + if (d < 0) {
2128
+ + return false;
2129
+ + }
2130
+ + // n elements from dim d up (the dims above d must then be taken whole)
2131
+ + while (n > 1) {
2132
+ + if (d < 0) {
2133
+ + return false;
2134
+ + }
2135
+ + if (n <= srcShape[d] - boxOff[d] && (n == 1 || boxSize[d] == 1)) {
2136
+ + boxSize[d] = n;
2137
+ + n = 1;
2138
+ + } else if (boxOff[d] == 0 && n % srcShape[d] == 0 && boxSize[d] == 1) {
2139
+ + boxSize[d] = srcShape[d];
2140
+ + n /= srcShape[d];
2141
+ + } else {
2142
+ + return false;
2143
+ + }
2144
+ + --d;
2145
+ + }
2146
+ + }
2147
+ + return true;
2148
+ + };
2149
+ + if (!findBox()) {
2150
+ + if (!rebase() || !findBox()) {
2151
+ + return nullptr;
2152
+ + }
2153
+ + }
2154
+ + // the box in row-major order must be exactly the result's element order: the result's dims walked above came in
2155
+ + // decreasing source dims, so it is; check the element count and the byte alignment of the innermost box
2156
+ + int64_t total = 1, boxTotal = 1;
2157
+ + for (auto n : v.shape) {
2158
+ + total *= n;
2159
+ + }
2160
+ + for (auto n : boxSize) {
2161
+ + boxTotal *= n;
2162
+ + }
2163
+ + const auto bits = storage.getWidth();
2164
+ + const auto inner = srcShape.size() - 1;
2165
+ + if (total != boxTotal || (boxOff[inner] * bits) % 8 != 0 || (boxSize[inner] * bits) % 8 != 0) {
2166
+ + return nullptr;
2167
+ + }
2168
+ + auto ctx = builder.getContext();
2169
+ + mlir::Value x = input;
2170
+ + if (inType.getElementType() != storage) { // (quantized input: slice the raw codes, not the axes)
2171
+ + x = builder.create<IE::QuantizeCastOp>(appendLoc(loc, "onw_view_raw"), x, storage);
2172
+ + }
2173
+ + if (groupRows.has_value()) {
2174
+ + const auto [g0, k] = groupRows.value();
2175
+ + SmallVector<mlir::Value> groups;
2176
+ + for (int64_t g = g0; g < g0 + k; ++g) {
2177
+ + auto sl = builder.create<IE::SliceOp>(appendLoc(loc, "onw_group_{0}", g), x,
2178
+ + getIntArrayAttr(ctx, SmallVector<int64_t>{0, g, 0}),
2179
+ + getIntArrayAttr(ctx, SmallVector<int64_t>{origShape[0], 1, origShape[2]}));
2180
+ + groups.push_back(builder.create<IE::ReshapeOp>(appendLoc(loc, "onw_group_row_{0}", g), sl,
2181
+ + getIntArrayAttr(ctx, SmallVector<int64_t>{1, origShape[0],
2182
+ + origShape[2]})));
2183
+ + }
2184
+ + x = builder.create<IE::ConcatOp>(appendLoc(loc, "onw_group_rows"), groups, Dim(0));
2185
+ + }
2186
+ + if (boxSize != srcShape) {
2187
+ + x = builder.create<IE::SliceOp>(appendLoc(loc, "onw_view_slice"), x, getIntArrayAttr(ctx, boxOff),
2188
+ + getIntArrayAttr(ctx, boxSize));
2189
+ + }
2190
+ + const auto outShape = to_small_vector(cur.getShape().raw());
2191
+ + if (to_small_vector(mlir::cast<NDTypeInterface>(x.getType()).getShape().raw()) != outShape) {
2192
+ + x = builder.create<IE::ReshapeOp>(appendLoc(loc, "onw_view_reshape"), x, getIntArrayAttr(ctx, outShape));
2193
+ + }
2194
+ + if (mlir::cast<NDTypeInterface>(x.getType()).getElementType() != cur.getElementType()) {
2195
+ + x = builder.create<IE::QuantizeCastOp>(appendLoc(loc, "onw_view_cast"), x, cur.getElementType());
2196
+ + }
2197
+ + return x;
2198
+ +}
2199
+ +} // namespace
2200
+ +
2201
+ mlir::Value ConstOpConverter::convertToIrForm(mlir::Location loc, const VPU::TransformationsSplit::Projection& split,
2202
+ mlir::BlockArgument argValue, const IoBoundaryAdapter& ioAdaptor,
2203
+ WeightsSeparationSchedule scheduleKind) {
2204
+ @@ -1091,6 +1531,29 @@ mlir::Value ConstOpConverter::convertToIrForm(mlir::Location loc, const VPU::Tra
2205
+ _log.trace("Creating matching operations for '{0}'", contentAttr);
2206
+ _log.trace(" These transformations are converted into IR '{0}'", transformations);
2207
+
2208
+ + if (scheduleKind == WeightsSeparationSchedule::Init && !transformations.empty()) {
2209
+ + std::vector<const void*> viewKey{value.getAsOpaquePointer()};
2210
+ + for (auto t : transformations) {
2211
+ + viewKey.push_back(t.getAsOpaquePointer());
2212
+ + }
2213
+ + mlir::Value folded;
2214
+ + if (auto it = _onwViewCache.find(viewKey); it != _onwViewCache.end()) {
2215
+ + folded = it->second;
2216
+ + } else {
2217
+ + folded = foldSubByteView(_opBuilder, appendLoc(loc, "onw_view"), value, transformations);
2218
+ + if (folded) {
2219
+ + _onwViewCache.emplace(std::move(viewKey), folded);
2220
+ + }
2221
+ + }
2222
+ + if (folded) {
2223
+ + if (std::getenv("ONW_DEBUG_WS") != nullptr) {
2224
+ + llvm::errs() << "ONW ws viewfold: " << value.getType() << " -> " << folded.getType() << char(10);
2225
+ + }
2226
+ + value = folded;
2227
+ + transformations = {};
2228
+ + }
2229
+ + }
2230
+ +
2231
+ // convert transformation chain to operations
2232
+ size_t count = 0;
2233
+ while (!transformations.empty()) {
2234
+ @@ -1152,8 +1615,119 @@ void removeRepeatedConstantsForHostPipeline(std::vector<Const::DeclareOp>& const
2235
+ }
2236
+ } // namespace
2237
+
2238
+ +namespace {
2239
+ +// onw: MergeFullyConnected joins the unrolled group matmuls of a 2-bit weight [N, G, K] (one-token graphs): the weight
2240
+ +// of k groups becomes a Concat of k constants, each the same chain on the same OV constant ending with a one-group
2241
+ +// SubView, on a dummy base - not an OV constant, so it stayed in the blob (a second copy of the codes) and the merged
2242
+ +// matmul could not go weightless. When the parts are consecutive groups of one constant the Concat is that chain with
2243
+ +// a k-group SubView: rewritten so, the weight is an OV constant again (its init builds it, see foldSubByteView).
2244
+ +// ONW_WS_NO_CODE_CONCAT=1: as before.
2245
+ +void onwCanonicalizeCodeConcats(mlir::ModuleOp moduleOp) {
2246
+ + if (!moduleOp->hasAttr("VPU.OnwWeightless") || std::getenv("ONW_WS_NO_CODE_CONCAT") != nullptr) {
2247
+ + return;
2248
+ + }
2249
+ + SmallVector<Const::DeclareOp> ops;
2250
+ + moduleOp.walk([&](Const::DeclareOp op) {
2251
+ + ops.push_back(op);
2252
+ + });
2253
+ + size_t rewritten = 0;
2254
+ + for (auto op : ops) {
2255
+ + const auto content = op.getContentAttr();
2256
+ + const auto ts = content.getTransformations();
2257
+ + if (ts.empty() || !Const::isDummyBaseContent(content.getBaseContent())) {
2258
+ + continue;
2259
+ + }
2260
+ + auto concat = mlir::dyn_cast<Const::ConcatAttr>(ts.front());
2261
+ + if (concat == nullptr || concat.getAxis().getInt() != 0) {
2262
+ + continue;
2263
+ + }
2264
+ + const auto constants = concat.getConstants().getValue();
2265
+ + const auto offsets = parseIntArrayOfArrayAttr<int64_t>(concat.getStaticOffsets());
2266
+ + if (constants.size() < 2 || offsets.size() != constants.size()) {
2267
+ + continue;
2268
+ + }
2269
+ + const auto first = constants[0];
2270
+ + const auto base = first.getBaseContent();
2271
+ + const auto baseType = mlir::cast<NDTypeInterface>(base.getType());
2272
+ + auto storage = mlir::dyn_cast<mlir::IntegerType>(baseType.getElementType());
2273
+ + if (!getResourceName(base).starts_with(Const::IMPORTED_WEIGHT_PREFIX) || storage == nullptr || storage.getWidth() >= 8 ||
2274
+ + baseType.getRank() != 3) {
2275
+ + continue;
2276
+ + }
2277
+ + const auto firstTs = first.getTransformations();
2278
+ + if (firstTs.empty() || !mlir::isa<Const::SubViewAttr>(firstTs.back())) {
2279
+ + continue;
2280
+ + }
2281
+ + const auto prefix = firstTs.drop_back(1);
2282
+ + // the SubView's input: [G', ...] with the groups on dim 0 (the parts' chains transpose them there)
2283
+ + const auto prefixType = Const::inferFinalType(baseType, prefix);
2284
+ + bool ok = true;
2285
+ + int64_t g0 = -1;
2286
+ + SmallVector<int64_t> partShape;
2287
+ + for (size_t i = 0; i < constants.size() && ok; ++i) {
2288
+ + const auto c = constants[i];
2289
+ + const auto cts = c.getTransformations();
2290
+ + ok = c.getBaseContent() == base && cts.size() == firstTs.size();
2291
+ + for (size_t j = 0; ok && j + 1 < cts.size(); ++j) {
2292
+ + ok = cts[j] == firstTs[j];
2293
+ + }
2294
+ + if (!ok) {
2295
+ + break;
2296
+ + }
2297
+ + auto sv = mlir::dyn_cast<Const::SubViewAttr>(cts.back());
2298
+ + ok = sv != nullptr;
2299
+ + if (!ok) {
2300
+ + break;
2301
+ + }
2302
+ + const auto off = parseIntArrayAttr<int64_t>(sv.getOffset());
2303
+ + const auto shp = parseIntArrayAttr<int64_t>(sv.getShape());
2304
+ + const auto full = to_small_vector(prefixType.getShape().raw());
2305
+ + ok = off.size() == full.size() && shp.size() == full.size() && shp[0] == 1;
2306
+ + for (size_t d = 1; ok && d < full.size(); ++d) {
2307
+ + ok = off[d] == 0 && shp[d] == full[d];
2308
+ + }
2309
+ + if (!ok) {
2310
+ + break;
2311
+ + }
2312
+ + if (i == 0) {
2313
+ + g0 = off[0];
2314
+ + partShape = shp;
2315
+ + }
2316
+ + ok = off[0] == g0 + static_cast<int64_t>(i) && offsets[i].size() == shp.size() &&
2317
+ + offsets[i][0] == static_cast<int64_t>(i);
2318
+ + for (size_t d = 1; ok && d < offsets[i].size(); ++d) {
2319
+ + ok = offsets[i][d] == 0;
2320
+ + }
2321
+ + }
2322
+ + if (!ok || g0 < 0) {
2323
+ + continue;
2324
+ + }
2325
+ + SmallVector<int64_t> newOff(partShape.size(), 0), newShape(partShape.begin(), partShape.end());
2326
+ + newOff[0] = g0;
2327
+ + newShape[0] = static_cast<int64_t>(constants.size());
2328
+ + SmallVector<Const::TransformAttrInterface> newTs(prefix.begin(), prefix.end());
2329
+ + newTs.push_back(Const::SubViewAttr::get(getIntArrayAttr(op.getContext(), newOff),
2330
+ + getIntArrayAttr(op.getContext(), newShape)));
2331
+ + newTs.append(ts.begin() + 1, ts.end());
2332
+ + const auto newContent = Const::ContentAttr::get(base, newTs);
2333
+ + if (newContent.getType() != op.getType()) {
2334
+ + continue;
2335
+ + }
2336
+ + mlir::OpBuilder builder(op);
2337
+ + auto newOp = builder.create<Const::DeclareOp>(op.getLoc(), op.getType(), newContent);
2338
+ + op.replaceAllUsesWith(newOp.getOutput());
2339
+ + op.erase();
2340
+ + ++rewritten;
2341
+ + }
2342
+ + if (rewritten != 0 && std::getenv("ONW_DEBUG_WS") != nullptr) {
2343
+ + llvm::errs() << "ONW ws code concats rewritten: " << rewritten << char(10);
2344
+ + }
2345
+ +}
2346
+ +} // namespace
2347
+ +
2348
+ WeightsSeparationInfo::WeightsSeparationInfo(mlir::ModuleOp moduleOp) {
2349
+ auto log = Logger::global().nest("weights-separation-info", 0);
2350
+ + onwCanonicalizeCodeConcats(moduleOp);
2351
+ auto mainFuncOp = net::getMainFunc(moduleOp);
2352
+ auto tree = VPU::getOutliningRepresentation(mainFuncOp);
2353
+
2354
  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
2355
  index f637bad..6c3db25 100644
2356
  --- a/src/vpux_compiler/src/dialect/VPUIP/transforms/passes/convert_async_ops_to_tasks.cpp
 
2447
  }
2448
 
2449
  diff --git a/src/vpux_compiler/src/dialect/const/attributes/content.cpp b/src/vpux_compiler/src/dialect/const/attributes/content.cpp
2450
+ index 3f4f862..69d23e8 100644
2451
  --- a/src/vpux_compiler/src/dialect/const/attributes/content.cpp
2452
  +++ b/src/vpux_compiler/src/dialect/const/attributes/content.cpp
2453
  @@ -7,6 +7,14 @@
 
2465
  #include "vpux/compiler/dialect/const/dialect.hpp"
2466
  #include "vpux/compiler/dialect/const/utils/const_logger.hpp"
2467
  #include "vpux/compiler/utils/stable_hash.hpp"
2468
+ @@ -468,11 +476,126 @@ mlir::DenseElementsAttr Const::detail::createConstContentWithConversion(mlir::Sh
2469
  // ContentAttr::fold
2470
  //
2471
 
 
2502
  - for (const auto& attr : getTransformations()) {
2503
  - res = attr.transform(res);
2504
  + const auto transformations = getTransformations();
2505
+ + // the last SubView that follows a Concat or a permutation: the prefix before it is memoized. [onw] Weightless
2506
+ + // folding keeps SubViews last (ws::getWsOptimizations: moveSubViewAfter, so the init schedule transforms a weight
2507
+ + // once), so every group slice of a 2-bit weight [N, G, 128] (and of its scales) transposed the whole tensor again
2508
+ + // when folded on the host - SplitFakeQuant and the ELF export of 2-bit Bonsai parts (~10 s of a 16 s compile).
2509
  + int64_t cut = -1;
2510
  + bool sawConcat = false;
2511
+ + static const bool permuteMemo = std::getenv("ONW_NO_PERMUTE_FOLD_MEMO") == nullptr;
2512
  + for (size_t i = 0; i < transformations.size(); ++i) {
2513
+ + if (mlir::isa<Const::ConcatAttr>(transformations[i]) ||
2514
+ + (permuteMemo && mlir::isa<Const::TransposeAttr, Const::MemPermuteAttr>(transformations[i]))) {
2515
  + sawConcat = true;
2516
  + } else if (sawConcat && mlir::isa<Const::SubViewAttr>(transformations[i])) {
2517
  + cut = static_cast<int64_t>(i);
2518
  + }
2519
  + }
2520
  + static std::atomic<int64_t> nHit{0}, nMiss{0}, nPlain{0};
2521
+ + static const bool noFoldMemo = std::getenv("ONW_NO_FOLD_MEMO") != nullptr;
2522
+ + if (cut < 0 || noFoldMemo) {
2523
  + ++nPlain;
2524
  + auto res = wrapBaseContent(baseContent);
2525
  + for (const auto& attr : transformations) {
 
2563
  + for (size_t i = static_cast<size_t>(cut) + 1; i < transformations.size(); ++i) {
2564
  + res = transformations[i].transform(res);
2565
  + }
2566
+ + // a result that still points into the memo's buffer (a SubView / reshape that moved its input instead of
2567
+ + // copying) must own its data: another thread can evict the entry and free the buffer once `held` is gone
2568
+ + // (multi-threaded compiles of 2-bit Bonsai 9B parts crashed with an access violation, now and then)
2569
+ + return Const::Content::copyUnownedBuffer(std::move(res));
2570
  + }
2571
  + if (!hit) {
2572
  + ++nMiss;
 
2595
  }
2596
  return res;
2597
  }
2598
+ @@ -747,8 +870,18 @@ llvm::hash_code StableHashStorage<QuantizeAttrStorage>::calculateStableHash() co
2599
+ template <>
2600
+ llvm::hash_code StableHashStorage<RescaleAttrStorage>::calculateStableHash() const {
2601
+ if (!this->scale.isSplat()) {
2602
+ - // Note: non-splat rescale is currently not supported
2603
+ - return {};
2604
+ + // onw: non-splat rescale (per-channel scales, e.g. 2-bit weights) - its shape and values, so that the weights
2605
+ + // separation can name the transformation chain (upstream: "not supported", QueryWSInfo threw)
2606
+ + const auto content = this->scale.fold();
2607
+ + SmallVector<uint32_t> bits;
2608
+ + for (const auto v : content.getValues<float>()) {
2609
+ + uint32_t b;
2610
+ + std::memcpy(&b, &v, sizeof(b));
2611
+ + bits.push_back(b);
2612
+ + }
2613
+ + const auto shape = this->scale.getType().getShape().raw();
2614
+ + return llvm::hash_combine(RescaleAttr::getMnemonic(), llvm::hash_combine_range(shape.begin(), shape.end()),
2615
+ + llvm::hash_combine_range(bits.begin(), bits.end()));
2616
+ }
2617
+
2618
+ const auto scale = this->scale.fold().getSplatValue<double>();
2619
+ @@ -868,7 +1001,6 @@ llvm::hash_code StableHashStorage<CumSumAttrStorage>::calculateStableHash() cons
2620
+ } // namespace details
2621
+
2622
+ llvm::hash_code RescaleAttr::getStableHashValue() const {
2623
+ - VPUX_THROW_UNLESS(getScale().isSplat(), "RescaleAttr scale must be splat");
2624
+ return static_cast<details::StableHashStorage<details::RescaleAttrStorage>*>(getImpl())->stableHash;
2625
+ }
2626
+
2627
+ diff --git a/src/vpux_compiler/src/pipelines/developer_config.cpp b/src/vpux_compiler/src/pipelines/developer_config.cpp
2628
+ index 175c65d..5bde0f7 100644
2629
+ --- a/src/vpux_compiler/src/pipelines/developer_config.cpp
2630
+ +++ b/src/vpux_compiler/src/pipelines/developer_config.cpp
2631
+ @@ -19,6 +19,7 @@
2632
+ #include "vpux/utils/core/developer_path_utils.hpp"
2633
+ #endif
2634
+
2635
+ +#include <cstdlib>
2636
+ #include <mlir/Pass/Pass.h>
2637
+ #include <mlir/Support/Timing.h>
2638
+
2639
+ @@ -30,6 +31,18 @@
2640
+ namespace vpux {
2641
+
2642
+ DeveloperConfig::DeveloperConfig(Logger log, mlir::MLIRContext* ctx, const vpux::OV::Config& config): _log(log) {
2643
+ +#if !defined(VPUX_DEVELOPER_BUILD) && defined(NDEBUG)
2644
+ + // onw: IR dumps in release builds too (ONW_IR_PRINTING_FILTER / _LOCATION / _ORDER)
2645
+ + if (const char* v = std::getenv("ONW_IR_PRINTING_FILTER")) {
2646
+ + _irPrintingFilter = v;
2647
+ + }
2648
+ + if (const char* v = std::getenv("ONW_IR_PRINTING_LOCATION")) {
2649
+ + _irPrintingLocation = v;
2650
+ + }
2651
+ + if (const char* v = std::getenv("ONW_IR_PRINTING_ORDER")) {
2652
+ + _irPrintingOrderStr = v;
2653
+ + }
2654
+ +#endif
2655
+ #if defined(VPUX_DEVELOPER_BUILD) || !defined(NDEBUG)
2656
+ parseEnv("IE_NPU_CRASH_REPRODUCER_FILE", _crashReproducerFile);
2657
+ parseEnv("IE_NPU_GEN_LOCAL_REPRODUCER", _localReproducer);
2658
+ @@ -218,8 +231,38 @@ private:
2659
+ Logger _log;
2660
+ };
2661
+
2662
+ +// onw: ONW_FIND_NULLOP=1 - after every pass, report the first op that has a null operand (debugging passes that
2663
+ +// leave half-built ops behind)
2664
+ +class OnwNullOperandCheck final : public mlir::PassInstrumentation {
2665
+ +public:
2666
+ + void runAfterPass(mlir::Pass* pass, mlir::Operation* op) final {
2667
+ + if (_reported) {
2668
+ + return;
2669
+ + }
2670
+ + op->walk([&](mlir::Operation* o) {
2671
+ + if (_reported) {
2672
+ + return;
2673
+ + }
2674
+ + for (unsigned i = 0; i < o->getNumOperands(); ++i) {
2675
+ + if (!o->getOperand(i)) {
2676
+ + llvm::errs() << "ONW null operand " << i << " of " << o->getName() << " after pass "
2677
+ + << pass->getName() << " at " << o->getLoc() << "\n";
2678
+ + _reported = true;
2679
+ + return;
2680
+ + }
2681
+ + }
2682
+ + });
2683
+ + }
2684
+ +
2685
+ +private:
2686
+ + bool _reported = false;
2687
+ +};
2688
+ +
2689
+ void addLogging(mlir::PassManager& pm, Logger log) {
2690
+ pm.addInstrumentation(std::make_unique<PassLogging>(log));
2691
+ + if (std::getenv("ONW_FIND_NULLOP") != nullptr) {
2692
+ + pm.addInstrumentation(std::make_unique<OnwNullOperandCheck>());
2693
+ + }
2694
+ }
2695
+
2696
+ void DeveloperConfig::setup(mlir::PassManager& pm, const vpux::OV::Config& config, bool isSubPipeline) const {
2697
  diff --git a/src/vpux_compiler/src/utils/dma_transaction_utils.cpp b/src/vpux_compiler/src/utils/dma_transaction_utils.cpp
2698
  index 10cb1f4..45e02e8 100644
2699
  --- a/src/vpux_compiler/src/utils/dma_transaction_utils.cpp
 
2715
  VPUX_THROW_WHEN(std::any_of(std::begin(reducedBitStrides), std::prev(std::end(reducedBitStrides)),
2716
  [](auto bitStride) {
2717
  return bitStride.count() % CHAR_BIT != 0;
2718
+ diff --git a/thirdparty/vpucostmodel b/thirdparty/vpucostmodel
2719
+ --- a/thirdparty/vpucostmodel
2720
+ +++ b/thirdparty/vpucostmodel
2721
+ @@ -1 +1 @@
2722
+ -Subproject commit 4b5da8484d7a5c8cb097486c1248c4d2a753ae4b
2723
+ +Subproject commit 4b5da8484d7a5c8cb097486c1248c4d2a753ae4b-dirty
onw/npukit/patches/openvino-6db412d-npu_plugin.patch CHANGED
@@ -13,10 +13,10 @@ index c5f05a69..e976d53b 100644
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>
@@ -27,8 +27,19 @@ index b56d95b5..24790c3c 100644
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
 
@@ -45,6 +56,10 @@ index b56d95b5..24790c3c 100644
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;
@@ -67,46 +82,98 @@ index b56d95b5..24790c3c 100644
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);
@@ -120,6 +187,40 @@ index b56d95b5..24790c3c 100644
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);
@@ -131,7 +232,7 @@ index b56d95b5..24790c3c 100644
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();
@@ -139,7 +240,7 @@ index b56d95b5..24790c3c 100644
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()) {
@@ -154,11 +255,40 @@ index b56d95b5..24790c3c 100644
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);
@@ -169,11 +299,11 @@ index b56d95b5..24790c3c 100644
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);
 
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..2d43d8fe 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,8 +5,14 @@
20
  #include "weightless_graph.hpp"
21
 
22
  #include <cinttypes>
 
27
  +#include <cstdio>
28
  #include <condition_variable>
29
  #include <iterator>
30
+ +#include <map>
31
  #include <mutex>
32
+ #include <queue>
33
+
34
+ @@ -24,12 +30,200 @@
35
+ #include "openvino/util/common_util.hpp"
36
+ #include "openvino/util/mmap_object.hpp"
37
+
38
+ +#include <algorithm>
39
+ +
40
+ #define USE_SINGLE_THREADED_RUN_INIT 1
41
+
42
+ namespace intel_npu {
43
 
44
  namespace {
45
 
 
56
  +};
57
  +std::mutex onwWsMutex;
58
  +std::unordered_multimap<uint64_t, OnwWsEntry> onwWsRegistry;
59
+ +// outputs of 64 KB and more, for outputs that are a part of one (onw 3.2): a 64-token graph keeps a 2-bit weight per
60
+ +// group, the one-token graph the same groups merged k at a time - group g of a merged weight is its bytes at
61
+ +// (g - g0) x the group's size. ONW_WS_SUBRANGE=0: whole outputs only.
62
+ +std::vector<OnwWsEntry> onwWsBig;
63
  +
64
  +uint64_t onwSampledHash(const unsigned char* p, size_t n) {
65
  + uint64_t h = 1469598103934665603ULL ^ n;
 
82
  + return e == nullptr || std::string(e) != "0";
83
  +}
84
  +
85
+ +// Per output (onw 3.2): every output of the schedule is looked up on its own. Found ones are bound to the existing
86
+ +// buffer; when all were found the new buffer is released as before; when only some were, the others are copied into a
87
+ +// compact buffer of their own (registered for later graphs) and the big one is released too - so graphs that hold the
88
+ +// same weights in a different set (the 1-token and the 64-token graphs of a 2-bit model: one output per weight group,
89
+ +// split differently over their init schedules) share every tensor they have in common. Returns the buffers to keep
90
+ +// alive when the new buffer can go, else registers its outputs and returns nothing (ONW_WS_SHARE_PARTIAL=0: all or
91
+ +// nothing, as before).
92
  +std::optional<std::vector<std::shared_ptr<ZeroTensor>>> onwWsShare(
93
+ + const std::shared_ptr<ZeroInitStructsHolder>& initStruct,
94
  + const std::shared_ptr<ZeroTensor>& hostTensor,
95
  + std::unordered_map<std::string, std::shared_ptr<ov::ITensor>>& tensorsMap) {
96
  + if (!onwWsShareEnabled() || hostTensor == nullptr) {
97
  + return std::nullopt;
98
  + }
99
  + std::lock_guard<std::mutex> lock(onwWsMutex);
100
+ + const bool log = std::getenv("ONW_WS_SHARE_LOG") != nullptr;
101
+ + const char* pe = std::getenv("ONW_WS_SHARE_PARTIAL");
102
+ + const bool partial = pe == nullptr || std::string(pe) != "0";
103
+ + // found[name] = (owner, data) of an identical live buffer
104
+ + std::unordered_map<std::string, std::pair<std::shared_ptr<ZeroTensor>, const unsigned char*>> found;
105
+ + size_t foundBytes = 0, restBytes = 0;
106
  + for (const auto& [name, t] : tensorsMap) {
107
  + const auto* p = static_cast<const unsigned char*>(t->data());
108
  + const size_t n = t->get_byte_size();
 
109
  + bool hit = false;
110
+ + auto range = onwWsRegistry.equal_range(onwSampledHash(p, n));
111
  + for (auto it = range.first; it != range.second; ++it) {
112
  + auto owner = it->second.owner.lock();
113
  + if (owner == nullptr || owner == hostTensor || it->second.size != n) {
114
  + continue;
115
  + }
116
  + if (std::memcmp(it->second.data, p, n) == 0) {
117
+ + found.emplace(name, std::make_pair(owner, it->second.data));
118
+ + foundBytes += n;
119
  + hit = true;
120
  + break;
121
  + }
122
  + }
123
+ + static const bool subrange = std::getenv("ONW_WS_SUBRANGE") == nullptr ||
124
+ + std::string(std::getenv("ONW_WS_SUBRANGE")) != "0";
125
+ + if (!hit && subrange && partial && n >= (size_t(1) << 15)) {
126
+ + for (const auto& big : onwWsBig) {
127
+ + if (big.size <= n || big.size % n != 0) {
128
+ + continue;
129
+ + }
130
+ + auto owner = big.owner.lock();
131
+ + if (owner == nullptr || owner == hostTensor) {
132
+ + continue;
133
+ + }
134
+ + for (size_t off = 0; off + n <= big.size; off += n) {
135
+ + if (std::memcmp(big.data + off, p, 64) == 0 && std::memcmp(big.data + off, p, n) == 0) {
136
+ + found.emplace(name, std::make_pair(owner, big.data + off));
137
+ + foundBytes += n;
138
+ + hit = true;
139
+ + break;
140
+ + }
141
+ + }
142
+ + if (hit) {
143
+ + break;
144
+ + }
145
+ + }
146
+ + }
147
  + if (!hit) {
148
+ + restBytes += n;
149
+ + if (!partial) {
150
+ + break;
151
+ + }
152
  + }
153
  + }
154
+ + auto registerAll = [&](const std::shared_ptr<ZeroTensor>& owner,
155
+ + const std::unordered_map<std::string, std::shared_ptr<ov::ITensor>>& m,
156
+ + const std::unordered_map<std::string, bool>* only) {
157
+ + for (const auto& [name, t] : m) {
158
+ + if (only != nullptr && only->count(name) == 0) {
159
+ + continue;
160
+ + }
161
  + const auto* p = static_cast<const unsigned char*>(t->data());
162
  + const size_t n = t->get_byte_size();
163
+ + onwWsRegistry.emplace(onwSampledHash(p, n), OnwWsEntry{owner, p, n});
164
+ + if (n >= (size_t(1) << 16)) {
165
+ + onwWsBig.push_back(OnwWsEntry{owner, p, n});
166
+ + }
167
  + }
168
+ + // (entries of released buffers go)
169
+ + onwWsBig.erase(std::remove_if(onwWsBig.begin(), onwWsBig.end(),
170
+ + [](const OnwWsEntry& e) {
171
+ + return e.owner.expired();
172
+ + }),
173
+ + onwWsBig.end());
174
+ + };
175
+ + if (found.empty() || (!partial && found.size() != tensorsMap.size())) {
176
+ + registerAll(hostTensor, tensorsMap, nullptr);
177
  + if (log) {
178
  + std::fprintf(stderr, "[onw ws-share] new weights: %zu outputs, %.1f MB%c", tensorsMap.size(),
179
  + hostTensor->get_byte_size() / 1e6, 10);
 
187
  + const_cast<unsigned char*>(src.second));
188
  + owners.push_back(src.first);
189
  + }
190
+ + if (found.size() != tensorsMap.size()) {
191
+ + // the outputs not found: copied into a compact buffer (each at a 64-byte aligned offset)
192
+ + auto align = [](size_t v) {
193
+ + return (v + 63) & ~size_t(63);
194
+ + };
195
+ + size_t total = 0;
196
+ + for (const auto& [name, t] : tensorsMap) {
197
+ + if (found.count(name) == 0) {
198
+ + total += align(t->get_byte_size());
199
+ + }
200
+ + }
201
+ + auto compact = std::make_shared<ZeroTensor>(initStruct, ov::element::Type_t::u8, ov::Shape({total}), false);
202
+ + auto* base = static_cast<unsigned char*>(const_cast<void*>(compact->data(ov::element::Type_t::u8)));
203
+ + size_t off = 0;
204
+ + std::unordered_map<std::string, bool> moved;
205
+ + for (auto& [name, t] : tensorsMap) {
206
+ + if (found.count(name) != 0) {
207
+ + continue;
208
+ + }
209
+ + const size_t n = t->get_byte_size();
210
+ + std::memcpy(base + off, t->data(), n);
211
+ + t = ov::make_tensor(t->get_element_type(), t->get_shape(), base + off);
212
+ + moved.emplace(name, true);
213
+ + off += align(n);
214
+ + }
215
+ + registerAll(compact, tensorsMap, &moved);
216
+ + owners.push_back(compact);
217
+ + if (log) {
218
+ + std::fprintf(stderr, "[onw ws-share] partly shared: %zu of %zu outputs, %.1f MB shared, %.1f MB kept "
219
+ + "(of %.1f MB)%c", found.size(), tensorsMap.size(), foundBytes / 1e6, total / 1e6,
220
+ + hostTensor->get_byte_size() / 1e6, 10);
221
+ + }
222
+ + return owners;
223
+ + }
224
  + if (log) {
225
  + std::fprintf(stderr, "[onw ws-share] shared: %zu outputs, %.1f MB released%c", found.size(),
226
  + hostTensor->get_byte_size() / 1e6, 10);
 
232
  constexpr uint8_t MAIN_SCHEDULE_INDEX = 0;
233
  constexpr std::string_view WEIGHTS_IR_EXTENSION = ".bin";
234
  constexpr std::string_view WEIGHTS_ONNX_EXTENSION = ".data";
235
+ @@ -423,6 +617,7 @@ void WeightlessGraph::initialize_impl(const FilteredConfig& config) {
236
  _initsFences.clear();
237
  _initsMetadata.clear();
238
  _initsCommandQueue.reset();
 
240
 
241
  // The main schedule is initialized after the weights initialization ones in order to save some memory
242
  Graph::initialize_impl(config);
243
+ @@ -502,7 +697,13 @@ WeightlessGraph::InputData WeightlessGraph::allocate_inputs(
244
  // model appears in exactly one schedule. Thus, one can delete
245
  // the handle to the constant memory early.
246
  if (auto it = constants.find(id); it != constants.end()) {
 
255
  constants.erase(it);
256
  }
257
  }
258
+ @@ -525,6 +726,28 @@ WeightlessGraph::OutputData WeightlessGraph::allocate_outputs(const size_t initI
259
+ initOutputsByteSize += size.value();
260
+ }
261
+
262
+ + if (std::getenv("ONW_WS_SHARE_LOG") != nullptr && initOutputsByteSize >= (size_t(1) << 30)) {
263
+ + std::map<std::string, std::pair<size_t, size_t>> byType; // type -> (count, bytes)
264
+ + for (const IODescriptor& descriptor : _initsMetadata.at(initIndex).outputs) {
265
+ + auto& e = byType[descriptor.precision.to_string()];
266
+ + e.first += 1;
267
+ + e.second += ov::util::get_memory_size(descriptor.precision, shape_size(descriptor.shapeFromCompiler.to_shape()));
268
+ + }
269
+ + std::fprintf(stderr, "[onw ws-share] init %zu outputs: %zu, %.1f MB%c", initIndex,
270
+ + _initsMetadata.at(initIndex).outputs.size(), initOutputsByteSize / 1e6, 10);
271
+ + for (const auto& [t, e] : byType) {
272
+ + std::fprintf(stderr, "[onw ws-share] type %s: %zu outputs, %.1f MB%c", t.c_str(), e.first, e.second / 1e6,
273
+ + 10);
274
+ + }
275
+ + size_t k = 0;
276
+ + for (const IODescriptor& descriptor : _initsMetadata.at(initIndex).outputs) {
277
+ + if (k++ < 4) {
278
+ + std::fprintf(stderr, "[onw ws-share] e.g. %s %s %s%c", descriptor.nameFromCompiler.c_str(),
279
+ + descriptor.precision.to_string().c_str(),
280
+ + descriptor.shapeFromCompiler.to_string().c_str(), 10);
281
+ + }
282
+ + }
283
+ + }
284
+ const std::shared_ptr<ZeroTensor> initOutputsAllocatedTensor =
285
+ std::make_shared<ZeroTensor>(_zeroInitStruct, ov::element::Type_t::u8, ov::Shape({initOutputsByteSize}), false);
286
+
287
+ @@ -557,6 +780,14 @@ void WeightlessGraph::run_init_single_threaded() {
288
  create_pipeline(initIndex, initInputsViewTensors, initOutputsViewTensors);
289
  run_pipeline(initIndex);
290
 
291
+ + if (auto owners = onwWsShare(_zeroInitStruct, initOutputsAllocatedTensor, initOutputsViewTensorsMap)) {
292
  + merge_two_maps(_mainInputsViewTensors, initOutputsViewTensorsMap);
293
  + for (auto& o : *owners) {
294
  + _mainInputsAllocatedTensors.push_back(o);
 
299
  merge_two_maps(_mainInputsViewTensors, initOutputsViewTensorsMap);
300
  _mainInputsAllocatedTensors.push_back(std::move(initOutputsAllocatedTensor));
301
  }
302
+ @@ -600,6 +831,14 @@ void WeightlessGraph::run_init_multi_threaded() {
303
  run_pipeline(data.initIndex);
304
 
305
  // TODO: pre-allocate those well in advance? (outside of this loop)
306
+ + if (auto owners = onwWsShare(_zeroInitStruct, data.outputs.hostTensor, data.outputs.tensorsMap)) {
307
  + merge_two_maps(_mainInputsViewTensors, data.outputs.tensorsMap);
308
  + for (auto& o : *owners) {
309
  + _mainInputsAllocatedTensors.push_back(o);
onw/runtime.py CHANGED
@@ -311,7 +311,9 @@ def all_sizes(meta):
311
  """[2.0] Every block size the model folder has graphs for: "sizes" (what onw before 2.0 reads) + "prompt_sizes"
312
  (larger prompt blocks added to a published model later, see compact.graft_sizes - out of older onw's sight, whose
313
  loader cannot read their seg<i>x.bin)."""
314
- return sorted(set(meta["sizes"]) | set(meta.get("prompt_sizes") or []))
 
 
315
 
316
 
317
  _CONST_RE = re.compile(rb'<layer id="\d+" name="([^"]+)" type="Const"([^>]*)>(\s*)<data ([^>]*?)offset="(\d+)" size="(\d+)"([^>]*)/>')
@@ -348,6 +350,14 @@ def _xbin_model(core, d, name, m, weights):
348
  return model
349
 
350
 
 
 
 
 
 
 
 
 
351
  def read_segment(core, d, name, m, npuw, bins, T, built_T):
352
  """The segment's ov.Model, reshaped to context T. With NPUW every segment of a .bin reads against the same
353
  in-memory copy (bins cache), so the weights bank sees one set of constants."""
@@ -358,23 +368,65 @@ def read_segment(core, d, name, m, npuw, bins, T, built_T):
358
  model = _xbin_model(core, d, name, m, bins[b])
359
  if npuw:
360
  model = _no_bool_constants(model)
361
- elif npuw:
362
  b = m.get("bin") or name[:-4] + ".bin"
363
  if b not in bins:
364
  # a file-backed mapping (OpenVINO shares it, see mapped_bin): NPUW's weights bank keeps reading the
365
  # constants after compiling, so the buffer lives as long as the model (SegmentedModel._bins), and as
366
  # mapped pages it costs no private memory
367
  bins[b] = ov.Tensor(mapped_bin(os.path.join(d, b)), shared_memory=True)
368
- model = _no_bool_constants(core.read_model(open(os.path.join(d, name), encoding="utf8").read(), bins[b]))
 
 
369
  else:
370
  model = core.read_model(os.path.join(d, name), os.path.join(d, m["bin"]) if "bin" in m else None)
371
  if T != built_T:
372
  model.reshape(reshaped_inputs(model, m["S"], built_T, T))
373
  if os.environ.get("ONW_CODES_SCALE") == "const" and codes_const_scale(model): # (opt-in, see there; marked
374
  model.set_rt_info("1", "onw_codes_const") # for the compile keys)
 
 
375
  return model
376
 
377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  GROUPED_MIN = int(os.environ.get("ONW_GROUPED_MIN", "2")) # [2.0] prompts from this long go the grouped-MoE path
379
  GROUPED_RAM_GB = 24 # [2.0] RAM the grouped-MoE prompt path needs
380
 
@@ -944,6 +996,13 @@ class _Tried:
944
  pass
945
 
946
 
 
 
 
 
 
 
 
947
  class Precompiled(Exception):
948
  """Raised by a precompile child (ONW_PRECOMPILE_ONLY) once its segments are in the compile cache."""
949
 
@@ -962,9 +1021,34 @@ def compile_jobs(core, device, npuw):
962
  (Panther Lake, Ornith-1.5-9B NF4: 8.4 instead of 5.2 GB of NPU memory, onw 1.4.2 / 1.4.3 did this by default).
963
  Each child holds about 3.5 GB at its peak."""
964
  env = os.environ.get("ONW_COMPILE_JOBS")
965
- if not env or device != "NPU" or not npuw or mem_saver():
966
  return 1
967
- return max(1, int(env))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
968
 
969
 
970
  def setup_mark(cache, config, tried_key):
@@ -1059,12 +1143,19 @@ class SegmentedModel:
1059
  config = enable_turbo(core, device, config) # (see turbo(): on unless ONW_TURBO=0)
1060
  config = enable_compiler(core, device, config) # [2.0.3] (see npu_compiler())
1061
  t0 = time.time()
1062
- if os.path.exists(os.path.join(d, "experts.bin")):
 
 
 
 
 
 
1063
  self.bank, self.E, self.K = self._expert_bank(core, d, device, sizes)
1064
  else: # dense model
1065
  self.bank, self.E, self.K, self.streaming = {}, 0, 0, False
1066
  sp = os.path.join(d, "shared.bin")
1067
- self.shared = load_shared(core, sp, device, self._shared_inputs(d)) if os.path.exists(sp) else {}
 
1068
  self.embed_rows = self._table("embed")
1069
  self.window = self.meta.get("masks", {}).get("sliding_window")
1070
  self.host_inputs = {}
@@ -1089,7 +1180,8 @@ class SegmentedModel:
1089
  # INT4 weights of the plain paged models as well: never separated by the compiler (Granite 4.2 3B INT4 part:
1090
  # 63.5 MB weightless against 63.6 MB, multi-row parts 20-35x slower to compile) - INT8 and NF4 are (llm-jp INT8
1091
  # part: 2.5 against 216 MB). (NPUW and MoE models as they were checked: gemma-4, Qwen3.6)
1092
- _WEIGHTLESS["skip_model"] = self.meta.get("needs") == "u2" and os.environ.get("ONW_U2_WEIGHTLESS") != "1"
 
1093
  _WEIGHTLESS["full"] = False # (this model: a part with its weights inside, see fused.wanted)
1094
  if device == "NPU" and not _WEIGHTLESS.get("told"):
1095
  from . import npukit
@@ -1107,7 +1199,7 @@ class SegmentedModel:
1107
  print(f"[npuw] off: ~{parts} compiled parts (each attention layer cuts a segment) - more than the NPU driver "
1108
  f"holds with NPUW (~30); ONW_NPUW=1 forces it", flush=True)
1109
  npuw = False
1110
- if not npuw and not self.bank and int4_weights(d, self.meta) and os.environ.get("ONW_U2_WEIGHTLESS") != "1":
1111
  _WEIGHTLESS["skip_model"] = True # (decided here: NPUW may have been turned off just above)
1112
  if npuw:
1113
  # experimental: NPUW pulls the weights out of every compiled graph into one weights bank; constants match
@@ -1198,9 +1290,16 @@ class SegmentedModel:
1198
  and os.path.exists(os.path.join(d, m.get("bin") or n[:-4] + ".bin"))] or [0.0])
1199
  npuw_check_runs(cache, graphs, covered, newest)
1200
  jobs = compile_jobs(core, device, npuw) if cache and own and not (only or precompile) else 1
 
 
 
1201
  if jobs > 1 and mark and not covered:
1202
  self._precompile(jobs, sorted(n for n in self.meta["segments"] if n[:-4] in graphs), todo)
1203
  tried = self._ctx["tried"] = _Tried(config, core, device, npuw) # (what the children recorded)
 
 
 
 
1204
  for attempt in range(2):
1205
  k, tc = 0, time.time()
1206
  for name, m in sorted(self.meta["segments"].items()):
@@ -1275,6 +1374,8 @@ class SegmentedModel:
1275
  runs = set(NPUW_SEEN.values())
1276
  if mark and not (NPUW_SEEN and (len(runs) > 1 or None in runs)): # (every graph is in the cache, one run)
1277
  write_mark(mark, graphs)
 
 
1278
  # the MTP layer (draft tokens, Qwen3.5 dense): compiled without NPUW, before the paged KV cache is set up
1279
  # (its attention layer gets a cache of its own); mtp: wanted (chat.use_mtp)
1280
  self.mtp, self.last_h = None, None
@@ -1330,7 +1431,12 @@ class SegmentedModel:
1330
  # memory-saving mode (ONW_LAZY16=1): block sizes come and go, so the state lives in tensors of its own (else in
1331
  # the smallest size's requests)
1332
  self.swapping = bool(deferred)
1333
- mk = host_tensor(core, device) if self.swapping else None
 
 
 
 
 
1334
  for req, m, names in self.segs[min(self.segs)]:
1335
  for n in names:
1336
  if n.startswith(("past_k.", "past_v.", "conv.", "ssm.")):
@@ -1400,8 +1506,10 @@ class SegmentedModel:
1400
  def fused_keep_all(self):
1401
  """[3.0] every fused variant stays loaded (onw's NPU kit, NPU 4000+, not the memory-saving mode)."""
1402
  from . import npukit
 
 
1403
  return os.environ.get("ONW_FUSED_KEEP", "1" if npukit.active() and npu_arch(self._ctx["core"]) >= 4000
1404
- and not mem_saver() else "0") == "1"
1405
 
1406
  def fused_prepare(self, need, S=None):
1407
  """Make the fused S-token graphs (default: the answer size) covering `need` positions the ones in use for that
@@ -1444,6 +1552,9 @@ class SegmentedModel:
1444
  return None
1445
  F = next((keep[(S, B)] for B in bs if B >= want and (S, B) in keep), None)
1446
  if F is None:
 
 
 
1447
  with self._mutex:
1448
  try:
1449
  F = keep[(S, want)] = _f.FusedDecode(self, S, want)
@@ -1452,6 +1563,7 @@ class SegmentedModel:
1452
  self._fused_sizes = [x for x in sizes if x != S]
1453
  return None
1454
  self._fused[S] = F
 
1455
  self.S = max([*self.segs, *self._fused])
1456
  self._fused_background(caps)
1457
  return F
@@ -1467,6 +1579,9 @@ class SegmentedModel:
1467
  return None
1468
  if F is not None and F.B >= want:
1469
  return F
 
 
 
1470
  F = None # [2.1.0] (this reference kept the old variant alive while the
1471
  with self._mutex: # next one loaded: peak working set +3.3 GB, NPU 3720 Granite 3B)
1472
  self._fused.pop(S, None) # (out first: never two copies of the weights)
@@ -1490,6 +1605,44 @@ class SegmentedModel:
1490
  self._fused_background(caps)
1491
  return self._fused[S]
1492
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1493
  def _fused_background(self, caps):
1494
  npuw_ = (self._ctx["config"] or {}).get("NPU_USE_NPUW") == "YES" # (NPUW: compiled when needed, no background)
1495
  if os.environ.get("ONW_FUSED_BG", "1") != "0" and not self._fused_bg and not npuw_:
@@ -1509,14 +1662,17 @@ class SegmentedModel:
1509
  env = os.environ.get("ONW_FUSED_JOBS")
1510
  if env:
1511
  return max(1, int(env))
1512
- if mem_saver() or self._ctx["device"] != "NPU" or os.environ.get("ONW_FUSED_JOBS_AUTO") != "1":
1513
- return 1 # (3.1.0: opt-in - ONW_FUSED_JOBS=n or ONW_FUSED_JOBS_AUTO=1)
 
 
1514
  try:
1515
  import psutil
1516
  free = psutil.virtual_memory().available / 2**30
1517
  except Exception:
1518
  return 1
1519
- return max(1, min(int((free - 4) / 3), (os.cpu_count() or 2) // 2, 6))
 
1520
 
1521
  def _fused_precompile(self, jobs):
1522
  from . import fused as _f
@@ -1546,7 +1702,10 @@ class SegmentedModel:
1546
  cfg = {k: v for k, v in (c["config"] or {}).items()}
1547
  kt = self.kv.kt.get_type_name()
1548
  env = dict(os.environ, ONW_WS_NO_CONCAT=os.environ.get("ONW_WS_NO_CONCAT", "1"))
1549
- todo = sorted(jobs, key=lambda j: (j[1], j[0]))
 
 
 
1550
  running, ok = [], True
1551
  while todo or running:
1552
  for p in [p for p in running if p.poll() is not None]:
@@ -1555,11 +1714,11 @@ class SegmentedModel:
1555
  if not ok:
1556
  break
1557
  if todo and len(running) < n and (not running or self._fused_jobs_parallel() > len(running)):
1558
- S, B = todo.pop(0)
1559
  arg = json.dumps([c["d"], c["device"], cfg, c["tried"].key, c["built_T"], self.kv.capacity, kt, S, B],
1560
  default=str)
1561
  flags = 0x00004000 if os.name == "nt" else 0 # (BELOW_NORMAL_PRIORITY_CLASS: answers go first)
1562
- running.append(subprocess.Popen([sys.executable, "-m", "onw.fusedjob", arg], env=env,
1563
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
1564
  creationflags=flags))
1565
  continue
@@ -1568,7 +1727,7 @@ class SegmentedModel:
1568
  p.wait()
1569
  if not ok:
1570
  print("[fused] a background compile process failed: the rest one after another here", flush=True)
1571
- for S, B in todo:
1572
  try:
1573
  from . import fused as _f
1574
  _f.FusedDecode(self, S, B, compile_only=True, log=lambda *a, **k: None)
@@ -1733,12 +1892,17 @@ class SegmentedModel:
1733
  lg[np.isin(self.mtp["vocab"], self.banned)] = -np.inf
1734
  return int(self.mtp["vocab"][int(lg.argmax())])
1735
 
1736
- def _precompile(self, jobs, names, todo):
1737
  """Compile these segments into the cache in child processes, `jobs` at a time (see compile_jobs); the load
1738
- then imports them. A child that fails leaves its segment to the load (compiled there, as without this)."""
 
1739
  import subprocess, sys
1740
  kw = {"creationflags": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {}
1741
  env = {**os.environ, "PYTHONUTF8": "1", "ONW_NPUW_SESSION": NPUW_SESSION}
 
 
 
 
1742
  args = json.dumps(self._args)
1743
  print(f"[compile] {len(names)} segments, {jobs} processes at a time", flush=True)
1744
  t0, waiting, running, done = time.time(), list(names), [], 0
@@ -1812,7 +1976,7 @@ class SegmentedModel:
1812
  the expert bank; note which carried values each segment's successors still read."""
1813
  for req, m, names in lst:
1814
  for n in names:
1815
- if n in self._owner and (self.swapping or S != min(self.segs, default=S)):
1816
  req.set_tensor(n, self._owner[n])
1817
  elif n.startswith("shared.") and n[7:] in self.shared:
1818
  req.set_tensor(n, self.shared[n[7:]])
@@ -2013,6 +2177,8 @@ class SegmentedModel:
2013
  [xml, size, S, "paged-1", part, ring, _signature(model), tried.key]
2014
  if part is None and model.has_rt_info("onw_codes_const"): # (a part's signature tells the forms apart)
2015
  src.append("codes-const")
 
 
2016
  # DeltaNet parts of the 4- and 64-token blocks (Qwen3.5): REP folds the repeated rows / chunks inside a layer
2017
  # into graphs of undecided shape, which no NPU compiler takes (2-6 s lost per part, an empty cache folder
2018
  # left); written so that REP takes them, they ran 6-12% slower than as one graph (Panther Lake) - so NONE first
@@ -2050,6 +2216,8 @@ class SegmentedModel:
2050
  except OSError:
2051
  pass
2052
  if bpw and os.path.exists(bpw) and weightless_wanted():
 
 
2053
  try:
2054
  req = import_blob(core, bpw, device, {**nocache, "MODEL_PTR": model}).create_infer_request()
2055
  USED_BLOBS.add(bpw)
@@ -2065,6 +2233,8 @@ class SegmentedModel:
2065
  except OSError:
2066
  pass
2067
  if own and os.path.exists(bp):
 
 
2068
  try:
2069
  req = import_blob(core, bp, device, nocache).create_infer_request()
2070
  USED_BLOBS.add(bp)
@@ -2101,6 +2271,12 @@ class SegmentedModel:
2101
  f.write(buf.getbuffer())
2102
  del buf
2103
  os.replace(f"{bpw}.{os.getpid()}.tmp", bpw)
 
 
 
 
 
 
2104
  req = import_blob(core, bpw, device, {**nocache, "MODEL_PTR": model}).create_infer_request()
2105
  USED_BLOBS.add(bpw)
2106
  _WEIGHTLESS["ok"] = True
@@ -2131,6 +2307,10 @@ class SegmentedModel:
2131
  # child compiles without CACHE_DIR (no second copy in OpenVINO's cache), otherwise it keeps
2132
  # CACHE_DIR and the compile below is a cache hit
2133
  r = _probe(d, name, nocache if (own or npuw_dir) else cfg, npuw, T, export=bp, ring=ring)
 
 
 
 
2134
  if r is True and own and os.path.exists(bp):
2135
  try:
2136
  req = import_blob(core, bp, device, nocache).create_infer_request()
@@ -2175,6 +2355,10 @@ class SegmentedModel:
2175
  # import what was just written and drop the compiled model: the first load then holds no more
2176
  # memory than later ones (a compiled model keeps its blob next to the driver's copy)
2177
  del cm
 
 
 
 
2178
  req = import_blob(core, bp, device, nocache).create_infer_request()
2179
  else:
2180
  req = cm.create_infer_request()
@@ -2559,6 +2743,7 @@ class SegmentedModel:
2559
  lg = F.step([int(t) for t in ids], want_logits, embeds)
2560
  if lg is not False:
2561
  return lg
 
2562
  w = self._wired
2563
  if self._wire_for is not (self.segs.get(min(self.segs)) if self.segs else None): # (a size dropped / loaded since)
2564
  w = self._wire(quiet=True)
@@ -2587,6 +2772,10 @@ class SegmentedModel:
2587
  return logits
2588
 
2589
  def _prefill_layers(self, ids, embeds=None, pos=None, want_logits=True, stop=None):
 
 
 
 
2590
  """A whole prompt, layer by layer: every block goes through segment 0, then every block through segment 1,
2591
  ... Each layer's state only depends on that layer's earlier blocks, so this is exact, and with experts
2592
  streamed from the SSD each layer's experts are read once for the whole prompt instead of once per block.
 
311
  """[2.0] Every block size the model folder has graphs for: "sizes" (what onw before 2.0 reads) + "prompt_sizes"
312
  (larger prompt blocks added to a published model later, see compact.graft_sizes - out of older onw's sight, whose
313
  loader cannot read their seg<i>x.bin)."""
314
+ # [3.2] "kit_sizes": the 2-bit (Bonsai) 64-token graphs - out of the sight of onw before 3.2, which would compute
315
+ # them wrong without the kit (npu_compiler #355) and hold the 2-bit weights twice with it
316
+ return sorted(set(meta["sizes"]) | set(meta.get("prompt_sizes") or []) | set(meta.get("kit_sizes") or []))
317
 
318
 
319
  _CONST_RE = re.compile(rb'<layer id="\d+" name="([^"]+)" type="Const"([^>]*)>(\s*)<data ([^>]*?)offset="(\d+)" size="(\d+)"([^>]*)/>')
 
350
  return model
351
 
352
 
353
+ def _own_mapping():
354
+ """[3.2] Weightless blobs on Windows: the weights are read through our own file mapping (as with NPUW), so that
355
+ release_pages can drop their pages from the working set once the init schedules put them on the NPU - read by
356
+ OpenVINO's own mapping they stayed (Panther Lake, Bonsai 4B: working set 0.9 GB above the private memory).
357
+ ONW_MAP_BINS=0: OpenVINO's mapping as before."""
358
+ return os.name == "nt" and os.environ.get("ONW_MAP_BINS") != "0" and weightless_wanted()
359
+
360
+
361
  def read_segment(core, d, name, m, npuw, bins, T, built_T):
362
  """The segment's ov.Model, reshaped to context T. With NPUW every segment of a .bin reads against the same
363
  in-memory copy (bins cache), so the weights bank sees one set of constants."""
 
368
  model = _xbin_model(core, d, name, m, bins[b])
369
  if npuw:
370
  model = _no_bool_constants(model)
371
+ elif npuw or _own_mapping():
372
  b = m.get("bin") or name[:-4] + ".bin"
373
  if b not in bins:
374
  # a file-backed mapping (OpenVINO shares it, see mapped_bin): NPUW's weights bank keeps reading the
375
  # constants after compiling, so the buffer lives as long as the model (SegmentedModel._bins), and as
376
  # mapped pages it costs no private memory
377
  bins[b] = ov.Tensor(mapped_bin(os.path.join(d, b)), shared_memory=True)
378
+ model = core.read_model(open(os.path.join(d, name), encoding="utf8").read(), bins[b])
379
+ if npuw:
380
+ model = _no_bool_constants(model)
381
  else:
382
  model = core.read_model(os.path.join(d, name), os.path.join(d, m["bin"]) if "bin" in m else None)
383
  if T != built_T:
384
  model.reshape(reshaped_inputs(model, m["S"], built_T, T))
385
  if os.environ.get("ONW_CODES_SCALE") == "const" and codes_const_scale(model): # (opt-in, see there; marked
386
  model.set_rt_info("1", "onw_codes_const") # for the compile keys)
387
+ if os.environ.get("ONW_LEFTMM") != "0" and right_const_matmuls(model): # (marked for the compile keys)
388
+ model.set_rt_info("1", "onw_leftmm")
389
  return model
390
 
391
 
392
+ def right_const_matmuls(model):
393
+ """[3.2] MatMul(C, X) - a constant 2-D C times a batched X [..., a, b] (Bonsai's Hadamard rotation, ir.hadamard:
394
+ H_a X H_b) - becomes Transpose(MatMul(Transpose(X), C^T)): the NPU compiler unrolls a constant on the left into one
395
+ tiny matmul per batch (512 per 64-token block of Bonsai 4B, 93% of a part's matmuls), which made its compile 3-4x
396
+ slower; on the right it is one matmul. The same values (CPU: equal outputs). Returns how many were rewritten."""
397
+ import openvino.opset13 as ov_ops
398
+
399
+ def const2d(v):
400
+ n, conv = v.get_node(), None
401
+ if n.get_type_name() == "Convert":
402
+ conv, n = n, n.input_value(0).get_node()
403
+ if n.get_type_name() != "Constant" or len(n.get_output_partial_shape(0)) != 2:
404
+ return None
405
+ return n, conv
406
+ k = 0
407
+ for op in list(model.get_ordered_ops()):
408
+ if op.get_type_name() != "MatMul":
409
+ continue
410
+ at = op.get_attributes()
411
+ x = op.input_value(1)
412
+ r = len(x.get_partial_shape())
413
+ got = const2d(op.input_value(0))
414
+ if at.get("transpose_a") or at.get("transpose_b") or got is None or r < 3 or const2d(x) is not None:
415
+ continue
416
+ cn, conv = got
417
+ ct = ov_ops.constant(np.ascontiguousarray(cn.get_data().T))
418
+ if conv is not None:
419
+ ct = ov_ops.convert(ct, conv.get_destination_type())
420
+ perm = ov_ops.constant(np.array(list(range(r - 2)) + [r - 1, r - 2], np.int64))
421
+ y = ov_ops.transpose(ov_ops.matmul(ov_ops.transpose(x, perm), ct, False, False), perm)
422
+ for t in op.output(0).get_target_inputs():
423
+ t.replace_source_output(y.output(0))
424
+ k += 1
425
+ if k:
426
+ model.validate_nodes_and_infer_types()
427
+ return k
428
+
429
+
430
  GROUPED_MIN = int(os.environ.get("ONW_GROUPED_MIN", "2")) # [2.0] prompts from this long go the grouped-MoE path
431
  GROUPED_RAM_GB = 24 # [2.0] RAM the grouped-MoE prompt path needs
432
 
 
996
  pass
997
 
998
 
999
+ class _NoBank(dict):
1000
+ """[3.2] A precompile child's stand-in for the expert bank: truthy like a loaded one (the compile routes test it),
1001
+ holding nothing."""
1002
+ def __bool__(self):
1003
+ return True
1004
+
1005
+
1006
  class Precompiled(Exception):
1007
  """Raised by a precompile child (ONW_PRECOMPILE_ONLY) once its segments are in the compile cache."""
1008
 
 
1021
  (Panther Lake, Ornith-1.5-9B NF4: 8.4 instead of 5.2 GB of NPU memory, onw 1.4.2 / 1.4.3 did this by default).
1022
  Each child holds about 3.5 GB at its peak."""
1023
  env = os.environ.get("ONW_COMPILE_JOBS")
1024
+ if device != "NPU":
1025
  return 1
1026
+ if npuw:
1027
+ return max(1, int(env)) if env and not mem_saver() else 1
1028
+ if env:
1029
+ return max(1, int(env))
1030
+ # [3.2] onw's own blobs (no NPUW): the children only compile and write the blobs (export_only, no import - no
1031
+ # weights on the NPU, no copies), so as many as the free memory holds at ~2.5 GB a child (a 2-bit Bonsai part
1032
+ # peaks at ~1.1 GB), at most half the cores, 6; 2 in the memory-saving mode. Panther Lake compiled Bonsai 4B's 26
1033
+ # parts one after the other for 19 minutes on one of its 16 cores. ONW_COMPILE_JOBS=1: one at a time.
1034
+ try:
1035
+ import psutil
1036
+ free = psutil.virtual_memory().available / 2**30
1037
+ except Exception:
1038
+ return 1
1039
+ n = max(1, min(int((free - 3) / 2.5), (os.cpu_count() or 2) // 2, 6))
1040
+ return min(n, 2) if mem_saver() else n
1041
+
1042
+
1043
+ def export_only():
1044
+ """[3.2] A precompile child of the own-blob route: compile and write the blobs, import nothing (the parent does)."""
1045
+ return bool(os.environ.get("ONW_PRECOMPILE_ONLY")) and os.environ.get("ONW_PRECOMPILE_EXPORT") == "1"
1046
+
1047
+
1048
+ class _Exported:
1049
+ """What _compile returns in an export-only child: the graph's inputs / outputs, nothing to run."""
1050
+ def __init__(self, model):
1051
+ self.model_inputs, self.model_outputs = model.inputs, model.outputs
1052
 
1053
 
1054
  def setup_mark(cache, config, tried_key):
 
1143
  config = enable_turbo(core, device, config) # (see turbo(): on unless ONW_TURBO=0)
1144
  config = enable_compiler(core, device, config) # [2.0.3] (see npu_compiler())
1145
  t0 = time.time()
1146
+ child = bool(os.environ.get("ONW_PRECOMPILE_ONLY"))
1147
+ if os.path.exists(os.path.join(d, "experts.bin")) and child:
1148
+ # [3.2] a precompile child only compiles (it stops before binding anything): no copy of the expert bank in
1149
+ # NPU-visible memory - six children holding Qwen3.6-35B-A3B's bank each hung the NPU driver (Panther Lake)
1150
+ idx = json.load(open(os.path.join(d, "experts.bin.json")))
1151
+ self.bank, self.E, self.K, self.streaming = _NoBank(), idx["E"], idx["K"], False
1152
+ elif os.path.exists(os.path.join(d, "experts.bin")):
1153
  self.bank, self.E, self.K = self._expert_bank(core, d, device, sizes)
1154
  else: # dense model
1155
  self.bank, self.E, self.K, self.streaming = {}, 0, 0, False
1156
  sp = os.path.join(d, "shared.bin")
1157
+ # (a precompile child: every shared tensor stays mapped from the file, none copied into NPU-visible memory)
1158
+ self.shared = load_shared(core, sp, device, set() if child else self._shared_inputs(d)) if os.path.exists(sp) else {}
1159
  self.embed_rows = self._table("embed")
1160
  self.window = self.meta.get("masks", {}).get("sliding_window")
1161
  self.host_inputs = {}
 
1180
  # INT4 weights of the plain paged models as well: never separated by the compiler (Granite 4.2 3B INT4 part:
1181
  # 63.5 MB weightless against 63.6 MB, multi-row parts 20-35x slower to compile) - INT8 and NF4 are (llm-jp INT8
1182
  # part: 2.5 against 216 MB). (NPUW and MoE models as they were checked: gemma-4, Qwen3.6)
1183
+ # [3.2] 2-bit weights weightless by default (onw's kit: compiler fixes; ONW_U2_WEIGHTLESS=0: full blobs)
1184
+ _WEIGHTLESS["skip_model"] = self.meta.get("needs") == "u2" and os.environ.get("ONW_U2_WEIGHTLESS", "1") == "0"
1185
  _WEIGHTLESS["full"] = False # (this model: a part with its weights inside, see fused.wanted)
1186
  if device == "NPU" and not _WEIGHTLESS.get("told"):
1187
  from . import npukit
 
1199
  print(f"[npuw] off: ~{parts} compiled parts (each attention layer cuts a segment) - more than the NPU driver "
1200
  f"holds with NPUW (~30); ONW_NPUW=1 forces it", flush=True)
1201
  npuw = False
1202
+ if not npuw and not self.bank and int4_weights(d, self.meta) and os.environ.get("ONW_INT4_WEIGHTLESS") != "1":
1203
  _WEIGHTLESS["skip_model"] = True # (decided here: NPUW may have been turned off just above)
1204
  if npuw:
1205
  # experimental: NPUW pulls the weights out of every compiled graph into one weights bank; constants match
 
1290
  and os.path.exists(os.path.join(d, m.get("bin") or n[:-4] + ".bin"))] or [0.0])
1291
  npuw_check_runs(cache, graphs, covered, newest)
1292
  jobs = compile_jobs(core, device, npuw) if cache and own and not (only or precompile) else 1
1293
+ # [3.2] the own-blob route: its own mark (a load with these graphs went through: all blobs are there)
1294
+ own_mark = loaded_mark(cache, config, tried.key, graphs) if cache and own and not npuw and device == "NPU" \
1295
+ else None
1296
  if jobs > 1 and mark and not covered:
1297
  self._precompile(jobs, sorted(n for n in self.meta["segments"] if n[:-4] in graphs), todo)
1298
  tried = self._ctx["tried"] = _Tried(config, core, device, npuw) # (what the children recorded)
1299
+ elif jobs > 1 and own_mark and not os.path.exists(own_mark):
1300
+ self._precompile(jobs, sorted(n for n in self.meta["segments"] if n[:-4] in graphs), todo,
1301
+ export=True)
1302
+ tried = self._ctx["tried"] = _Tried(config, core, device, npuw)
1303
  for attempt in range(2):
1304
  k, tc = 0, time.time()
1305
  for name, m in sorted(self.meta["segments"].items()):
 
1374
  runs = set(NPUW_SEEN.values())
1375
  if mark and not (NPUW_SEEN and (len(runs) > 1 or None in runs)): # (every graph is in the cache, one run)
1376
  write_mark(mark, graphs)
1377
+ if own_mark and not dropped: # (every blob is in the store: no precompile next time)
1378
+ write_mark(own_mark, graphs)
1379
  # the MTP layer (draft tokens, Qwen3.5 dense): compiled without NPUW, before the paged KV cache is set up
1380
  # (its attention layer gets a cache of its own); mtp: wanted (chat.use_mtp)
1381
  self.mtp, self.last_h = None, None
 
1431
  # memory-saving mode (ONW_LAZY16=1): block sizes come and go, so the state lives in tensors of its own (else in
1432
  # the smallest size's requests)
1433
  self.swapping = bool(deferred)
1434
+ # [3.2] the state in tensors of its own as well when the paged graphs may go for the fused ones
1435
+ # (_drop_paged_for_fused): in the dropped requests' buffers the fused graphs wrote into freed memory (Panther
1436
+ # Lake, Bonsai 2 9B: an access violation in the NPU driver at the first prompt block)
1437
+ self._own_state = self.swapping or (self.paged and device == "NPU"
1438
+ and os.environ.get("ONW_FUSED_DROP_PAGED", "1") == "1")
1439
+ mk = host_tensor(core, device) if self._own_state else None
1440
  for req, m, names in self.segs[min(self.segs)]:
1441
  for n in names:
1442
  if n.startswith(("past_k.", "past_v.", "conv.", "ssm.")):
 
1506
  def fused_keep_all(self):
1507
  """[3.0] every fused variant stays loaded (onw's NPU kit, NPU 4000+, not the memory-saving mode)."""
1508
  from . import npukit
1509
+ # [3.2] not for 2-bit weights: every variant has its own weight tables (1 bit per weight: Bonsai 2 27B +3.4 GB
1510
+ # a variant) - one variant at a time, swapped as the chat grows
1511
  return os.environ.get("ONW_FUSED_KEEP", "1" if npukit.active() and npu_arch(self._ctx["core"]) >= 4000
1512
+ and not mem_saver() and self.meta.get("needs") != "u2" else "0") == "1"
1513
 
1514
  def fused_prepare(self, need, S=None):
1515
  """Make the fused S-token graphs (default: the answer size) covering `need` positions the ones in use for that
 
1552
  return None
1553
  F = next((keep[(S, B)] for B in bs if B >= want and (S, B) in keep), None)
1554
  if F is None:
1555
+ n = self._fused_jobs_parallel()
1556
+ if n > 1 and not (self._ctx["config"] or {}).get("NPU_USE_NPUW") == "YES":
1557
+ self._fused_children([(S, want)], n) # [3.2] (its graphs at the same time; the load imports them)
1558
  with self._mutex:
1559
  try:
1560
  F = keep[(S, want)] = _f.FusedDecode(self, S, want)
 
1563
  self._fused_sizes = [x for x in sizes if x != S]
1564
  return None
1565
  self._fused[S] = F
1566
+ self._drop_paged_for_fused(S)
1567
  self.S = max([*self.segs, *self._fused])
1568
  self._fused_background(caps)
1569
  return F
 
1579
  return None
1580
  if F is not None and F.B >= want:
1581
  return F
1582
+ n = self._fused_jobs_parallel() # [3.2] (its graphs compiled at the same time first)
1583
+ if n > 1 and not (self._ctx["config"] or {}).get("NPU_USE_NPUW") == "YES":
1584
+ self._fused_children([(S, want)], n)
1585
  F = None # [2.1.0] (this reference kept the old variant alive while the
1586
  with self._mutex: # next one loaded: peak working set +3.3 GB, NPU 3720 Granite 3B)
1587
  self._fused.pop(S, None) # (out first: never two copies of the weights)
 
1605
  self._fused_background(caps)
1606
  return self._fused[S]
1607
 
1608
+ def _drop_paged_for_fused(self, S, before=False):
1609
+ """[3.2] Memory first: the fused S-token graphs run this size for every position they cover, so the paged
1610
+ S-token graphs go (their weight tables are as large as the fused ones': Bonsai 4B +1.3 GB on the NPU with both;
1611
+ the weights themselves are shared). They come back when a block needs them - images (explicit positions),
1612
+ past the fused variants (_paged_back). Not with MTP (its verification blocks use them). ONW_FUSED_DROP_PAGED=0:
1613
+ kept as before."""
1614
+ if (os.environ.get("ONW_FUSED_DROP_PAGED", "1") != "1" or S not in self.segs or self.mtp is not None
1615
+ or not self._own_state):
1616
+ return
1617
+ only = os.environ.get("ONW_FUSED_DROP_SIZES") # (test: only these sizes, e.g. "64")
1618
+ if only and str(S) not in only.split(","):
1619
+ return
1620
+ if before and not self._fused_ready(S): # (no fused blobs yet: dropped once they are prepared)
1621
+ return
1622
+ with self._mutex:
1623
+ self.drop_size(S, force=True)
1624
+ self._wired = self._wire_for = None
1625
+ self.__dict__.setdefault("_paged_dropped", set()).add(S)
1626
+ self._trim()
1627
+ print(f"[fused] the paged {S}-token graphs out (the fused ones run this size; back when a block needs them)",
1628
+ flush=True)
1629
+
1630
+ def _fused_ready(self, S):
1631
+ """[3.2] Whether the paged S-token graphs may go before the fused ones are prepared: always - the fused graphs
1632
+ compile in child processes (or here, from the files) without them, and if they fail the paged ones come back
1633
+ when a block needs them (_paged_back)."""
1634
+ return S in (self._fused_sizes or [])
1635
+
1636
+ def _paged_back(self):
1637
+ """[3.2] The paged graphs dropped for the fused ones, loaded again (a block the fused graphs cannot run)."""
1638
+ dropped = self.__dict__.get("_paged_dropped")
1639
+ if not dropped:
1640
+ return
1641
+ for S in sorted(dropped):
1642
+ self.swap_to(S)
1643
+ dropped.clear()
1644
+ self._wired = self._wire_for = None
1645
+
1646
  def _fused_background(self, caps):
1647
  npuw_ = (self._ctx["config"] or {}).get("NPU_USE_NPUW") == "YES" # (NPUW: compiled when needed, no background)
1648
  if os.environ.get("ONW_FUSED_BG", "1") != "0" and not self._fused_bg and not npuw_:
 
1662
  env = os.environ.get("ONW_FUSED_JOBS")
1663
  if env:
1664
  return max(1, int(env))
1665
+ # [3.2] on by default (the children only compile and write blobs; ONW_FUSED_JOBS_AUTO=0: one at a time here);
1666
+ # 2 at most in the memory-saving mode
1667
+ if self._ctx["device"] != "NPU" or os.environ.get("ONW_FUSED_JOBS_AUTO") == "0":
1668
+ return 1
1669
  try:
1670
  import psutil
1671
  free = psutil.virtual_memory().available / 2**30
1672
  except Exception:
1673
  return 1
1674
+ n = max(1, min(int((free - 4) / 3), (os.cpu_count() or 2) // 2, 6))
1675
+ return min(n, 2) if mem_saver() else n
1676
 
1677
  def _fused_precompile(self, jobs):
1678
  from . import fused as _f
 
1702
  cfg = {k: v for k, v in (c["config"] or {}).items()}
1703
  kt = self.kv.kt.get_type_name()
1704
  env = dict(os.environ, ONW_WS_NO_CONCAT=os.environ.get("ONW_WS_NO_CONCAT", "1"))
1705
+ # [3.2] one child per graph (segment) of a variant, the longest first: a variant's graphs compile at the same
1706
+ # time (Panther Lake, Bonsai 4B: the 64-token 4K variant's 5 graphs took 590 s one after another)
1707
+ segs = {S_: [n_ for n_, sm in sorted(self.meta["segments"].items()) if sm["S"] == S_] for S_ in {j[0] for j in jobs}}
1708
+ todo = [(S, B, n_) for S, B in sorted(jobs, key=lambda j: (j[1], j[0])) for n_ in segs[S]]
1709
  running, ok = [], True
1710
  while todo or running:
1711
  for p in [p for p in running if p.poll() is not None]:
 
1714
  if not ok:
1715
  break
1716
  if todo and len(running) < n and (not running or self._fused_jobs_parallel() > len(running)):
1717
+ S, B, seg = todo.pop(0)
1718
  arg = json.dumps([c["d"], c["device"], cfg, c["tried"].key, c["built_T"], self.kv.capacity, kt, S, B],
1719
  default=str)
1720
  flags = 0x00004000 if os.name == "nt" else 0 # (BELOW_NORMAL_PRIORITY_CLASS: answers go first)
1721
+ running.append(subprocess.Popen([sys.executable, "-m", "onw.fusedjob", arg], env={**env, "ONW_FUSED_SEG": seg},
1722
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
1723
  creationflags=flags))
1724
  continue
 
1727
  p.wait()
1728
  if not ok:
1729
  print("[fused] a background compile process failed: the rest one after another here", flush=True)
1730
+ for S, B in sorted({(j[0], j[1]) for j in todo}):
1731
  try:
1732
  from . import fused as _f
1733
  _f.FusedDecode(self, S, B, compile_only=True, log=lambda *a, **k: None)
 
1892
  lg[np.isin(self.mtp["vocab"], self.banned)] = -np.inf
1893
  return int(self.mtp["vocab"][int(lg.argmax())])
1894
 
1895
+ def _precompile(self, jobs, names, todo, export=False):
1896
  """Compile these segments into the cache in child processes, `jobs` at a time (see compile_jobs); the load
1897
+ then imports them. A child that fails leaves its segment to the load (compiled there, as without this).
1898
+ export: the children only write onw's blobs (export_only), the biggest segments first."""
1899
  import subprocess, sys
1900
  kw = {"creationflags": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {}
1901
  env = {**os.environ, "PYTHONUTF8": "1", "ONW_NPUW_SESSION": NPUW_SESSION}
1902
+ if export:
1903
+ env["ONW_PRECOMPILE_EXPORT"] = "1"
1904
+ d = self._ctx["d"] # (the longest compiles start first: less waiting at the end)
1905
+ names = sorted(names, key=lambda n: -self.meta["segments"][n]["S"] * os.path.getsize(os.path.join(d, n)))
1906
  args = json.dumps(self._args)
1907
  print(f"[compile] {len(names)} segments, {jobs} processes at a time", flush=True)
1908
  t0, waiting, running, done = time.time(), list(names), [], 0
 
1976
  the expert bank; note which carried values each segment's successors still read."""
1977
  for req, m, names in lst:
1978
  for n in names:
1979
+ if n in self._owner and (self._own_state or S != min(self.segs, default=S)):
1980
  req.set_tensor(n, self._owner[n])
1981
  elif n.startswith("shared.") and n[7:] in self.shared:
1982
  req.set_tensor(n, self.shared[n[7:]])
 
2177
  [xml, size, S, "paged-1", part, ring, _signature(model), tried.key]
2178
  if part is None and model.has_rt_info("onw_codes_const"): # (a part's signature tells the forms apart)
2179
  src.append("codes-const")
2180
+ if part is None and model.has_rt_info("onw_leftmm"):
2181
+ src.append("leftmm")
2182
  # DeltaNet parts of the 4- and 64-token blocks (Qwen3.5): REP folds the repeated rows / chunks inside a layer
2183
  # into graphs of undecided shape, which no NPU compiler takes (2-6 s lost per part, an empty cache folder
2184
  # left); written so that REP takes them, they ran 6-12% slower than as one graph (Panther Lake) - so NONE first
 
2216
  except OSError:
2217
  pass
2218
  if bpw and os.path.exists(bpw) and weightless_wanted():
2219
+ if export_only(): # [3.2] (a precompile child: already there)
2220
+ return _Exported(model)
2221
  try:
2222
  req = import_blob(core, bpw, device, {**nocache, "MODEL_PTR": model}).create_infer_request()
2223
  USED_BLOBS.add(bpw)
 
2233
  except OSError:
2234
  pass
2235
  if own and os.path.exists(bp):
2236
+ if export_only():
2237
+ return _Exported(model)
2238
  try:
2239
  req = import_blob(core, bp, device, nocache).create_infer_request()
2240
  USED_BLOBS.add(bp)
 
2271
  f.write(buf.getbuffer())
2272
  del buf
2273
  os.replace(f"{bpw}.{os.getpid()}.tmp", bpw)
2274
+ if export_only(): # [3.2] the parent imports it
2275
+ _WEIGHTLESS["ok"] = True
2276
+ tried.set(gname, how)
2277
+ tried.save()
2278
+ print(f"[compile] {gname}: {how} (weightless, exported)", flush=True)
2279
+ return _Exported(model)
2280
  req = import_blob(core, bpw, device, {**nocache, "MODEL_PTR": model}).create_infer_request()
2281
  USED_BLOBS.add(bpw)
2282
  _WEIGHTLESS["ok"] = True
 
2307
  # child compiles without CACHE_DIR (no second copy in OpenVINO's cache), otherwise it keeps
2308
  # CACHE_DIR and the compile below is a cache hit
2309
  r = _probe(d, name, nocache if (own or npuw_dir) else cfg, npuw, T, export=bp, ring=ring)
2310
+ if r is True and own and os.path.exists(bp) and export_only():
2311
+ tried.set(gname, how)
2312
+ tried.save()
2313
+ return _Exported(model)
2314
  if r is True and own and os.path.exists(bp):
2315
  try:
2316
  req = import_blob(core, bp, device, nocache).create_infer_request()
 
2355
  # import what was just written and drop the compiled model: the first load then holds no more
2356
  # memory than later ones (a compiled model keeps its blob next to the driver's copy)
2357
  del cm
2358
+ if export_only(): # [3.2] the parent imports it
2359
+ tried.set(gname, how)
2360
+ tried.save()
2361
+ return _Exported(model)
2362
  req = import_blob(core, bp, device, nocache).create_infer_request()
2363
  else:
2364
  req = cm.create_infer_request()
 
2743
  lg = F.step([int(t) for t in ids], want_logits, embeds)
2744
  if lg is not False:
2745
  return lg
2746
+ self._paged_back() # [3.2] (dropped for the fused graphs: needed here)
2747
  w = self._wired
2748
  if self._wire_for is not (self.segs.get(min(self.segs)) if self.segs else None): # (a size dropped / loaded since)
2749
  w = self._wire(quiet=True)
 
2772
  return logits
2773
 
2774
  def _prefill_layers(self, ids, embeds=None, pos=None, want_logits=True, stop=None):
2775
+ self._paged_back() # [3.2] (dropped for the fused graphs: needed here)
2776
+ return self._prefill_layers2(ids, embeds, pos, want_logits, stop)
2777
+
2778
+ def _prefill_layers2(self, ids, embeds=None, pos=None, want_logits=True, stop=None):
2779
  """A whole prompt, layer by layer: every block goes through segment 0, then every block through segment 1,
2780
  ... Each layer's state only depends on that layer's earlier blocks, so this is exact, and with experts
2781
  streamed from the SSD each layer's experts are read once for the whole prompt instead of once per block.
onw/visionjob.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """[3.2] A background child that compiles the vision encoder into onw's blob store at a first load (chat.export_vision_blob),
2
+ so the first image only imports it. usage (internal): python -m onw.visionjob '[MODEL_DIR, DEVICE, CONFIG]'"""
3
+ import json
4
+ import sys
5
+
6
+ from . import npukit
7
+
8
+
9
+ def main():
10
+ d, device, cfg = json.loads(sys.argv[1])
11
+ from .chat import export_vision_blob
12
+ from .runtime import enable_compiler
13
+ core = npukit.core()
14
+ enable_compiler(core, device, {}, quiet=True)
15
+ export_vision_blob(core, d, device, cfg)
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
onw/wired.py CHANGED
@@ -7,10 +7,12 @@ SegmentedModel._step (NPU 3720, Qwen3.6-35B-A3B: 146.5 -> 139.5 ms a token). ONW
7
  [2.0.4] Dense models too (paged attention), on their decode size: the 1-token graphs, or the 2-token ones that answer on
8
  NPU 4000+ since 2.0.3 (one real row, the second masked by token_valid). Granite 4.2 8B on Panther Lake: the stock path
9
  spent ~40 of 167 ms a token on the host."""
10
- import os
11
  import numpy as np
12
  from .store import host_tensor
13
 
 
 
14
  COMMON = ("inputs_embeds", "rope_cos", "rope_sin", "position_ids", "token_valid", "mask")
15
 
16
 
@@ -80,6 +82,11 @@ class Wired:
80
  m._tok += 1
81
  m._last = {"start": m.cur, "cx": {}, "rows": {}, "prev": {} if n > 1 else None, "pp": None}
82
  cur, later, logits = m.cur, None, None
 
 
 
 
 
83
  for pi, st in enumerate(self.plan):
84
  req, meta = st["req"], st["meta"]
85
  if st["head"] and not want_logits:
@@ -101,11 +108,16 @@ class Wired:
101
  bufs, c = m._pp[k]
102
  req.set_tensor(k, bufs[c])
103
  req.set_tensor("ssm_new." + k[4:], bufs[1 - c])
 
 
104
  req.start_async()
105
  if later is not None:
106
  later()
107
  later = None
108
  req.wait()
 
 
 
109
  L = st["kv"]
110
  if L is not None: # paged attention: rows into the cache, attention on the NPU
111
  if st["kvw"]: # (a layer sharing another's cache only reads it)
@@ -113,6 +125,8 @@ class Wired:
113
  q = req.get_tensor(f"q.{L}").data
114
  self.parts[pi + 1][0].get_tensor(f"carry.attn.{L}").data[:] = m.kv.attend(
115
  L, q[0], cur, S, n, sink=m._sink.get(L), rows_out=True)
 
 
116
 
117
  def book(st=st, req=req):
118
  for o in st["cx"]:
@@ -135,6 +149,16 @@ class Wired:
135
  if later is not None:
136
  later()
137
  m.cur += n
 
 
 
 
 
 
 
 
 
 
138
  if logits is not None:
139
  if not np.isfinite(logits).all():
140
  raise FloatingPointError(f"the {S}-token graph returned NaN/inf logits")
 
7
  [2.0.4] Dense models too (paged attention), on their decode size: the 1-token graphs, or the 2-token ones that answer on
8
  NPU 4000+ since 2.0.3 (one real row, the second masked by token_valid). Granite 4.2 8B on Panther Lake: the stock path
9
  spent ~40 of 167 ms a token on the host."""
10
+ import os, time
11
  import numpy as np
12
  from .store import host_tensor
13
 
14
+ PROFILE = os.environ.get("ONW_PROFILE") == "1" # [3.2] per-part NPU / attention / host times, printed every 64 tokens
15
+
16
  COMMON = ("inputs_embeds", "rope_cos", "rope_sin", "position_ids", "token_valid", "mask")
17
 
18
 
 
82
  m._tok += 1
83
  m._last = {"start": m.cur, "cx": {}, "rows": {}, "prev": {} if n > 1 else None, "pp": None}
84
  cur, later, logits = m.cur, None, None
85
+ if PROFILE:
86
+ t_step = time.perf_counter()
87
+ prof = getattr(self, "_prof", None)
88
+ if prof is None:
89
+ prof = self._prof = {"n": 0, "npu": [0.0] * len(self.plan), "attn": 0.0, "step": 0.0}
90
  for pi, st in enumerate(self.plan):
91
  req, meta = st["req"], st["meta"]
92
  if st["head"] and not want_logits:
 
108
  bufs, c = m._pp[k]
109
  req.set_tensor(k, bufs[c])
110
  req.set_tensor("ssm_new." + k[4:], bufs[1 - c])
111
+ if PROFILE:
112
+ t0 = time.perf_counter()
113
  req.start_async()
114
  if later is not None:
115
  later()
116
  later = None
117
  req.wait()
118
+ if PROFILE:
119
+ t1 = time.perf_counter()
120
+ prof["npu"][pi] += t1 - t0
121
  L = st["kv"]
122
  if L is not None: # paged attention: rows into the cache, attention on the NPU
123
  if st["kvw"]: # (a layer sharing another's cache only reads it)
 
125
  q = req.get_tensor(f"q.{L}").data
126
  self.parts[pi + 1][0].get_tensor(f"carry.attn.{L}").data[:] = m.kv.attend(
127
  L, q[0], cur, S, n, sink=m._sink.get(L), rows_out=True)
128
+ if PROFILE:
129
+ prof["attn"] += time.perf_counter() - t1
130
 
131
  def book(st=st, req=req):
132
  for o in st["cx"]:
 
149
  if later is not None:
150
  later()
151
  m.cur += n
152
+ if PROFILE:
153
+ prof["step"] += time.perf_counter() - t_step
154
+ prof["n"] += 1
155
+ if prof["n"] % 64 == 0:
156
+ k = prof["n"]
157
+ npu = sum(prof["npu"])
158
+ print(f"[profile] {k} tokens: {prof['step'] / k * 1e3:.2f} ms a token = parts on the NPU "
159
+ f"{npu / k * 1e3:.2f} + attention {prof['attn'] / k * 1e3:.2f} + host "
160
+ f"{(prof['step'] - npu - prof['attn']) / k * 1e3:.2f}; parts: "
161
+ + " ".join(f"{v / k * 1e3:.2f}" for v in prof["npu"]), flush=True)
162
  if logits is not None:
163
  if not np.isfinite(logits).all():
164
  raise FloatingPointError(f"the {S}-token graph returned NaN/inf logits")
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
  name = "onw"
7
- version = "3.1.0"
8
  description = "Ore no NPU ga konna ni ugoku wake nai - LLMs entirely on the Intel NPU"
9
  readme = "README.md"
10
  license = {text = "Apache-2.0"}
 
4
 
5
  [project]
6
  name = "onw"
7
+ version = "3.2.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"}