onw 3.3.3: background compiles end with onw
Browse files- CHANGELOG.md +5 -0
- CHANGELOG_en.md +5 -0
- onw/__init__.py +1 -1
- onw/chat.py +11 -3
- onw/childguard.py +41 -0
- onw/fusedjob.py +2 -0
- onw/precompile.py +22 -20
- onw/runtime.py +4 -2
- onw/visionjob.py +2 -0
- pyproject.toml +1 -1
CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@
|
|
| 5 |
日付は日本時間です。各版は Hugging Face の `ryugyosoft/onw` にタグ(`v0.4.2` 以降)付きで残っています。
|
| 6 |
onw ウィンドウは、新しい版があるとこのファイルから変更点を表示します(`## v…` の見出しで区切っています)。
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
## v3.3.2 (2026-10-25)
|
| 9 |
- **不具合の修正(モデルの更新は不要)**
|
| 10 |
- この PC で変換するモデル(LFM2.5 24B-A2B)の「ダウンロードして変換」を途中で止めても、変換の処理が裏で動き続けていました(メモリを使ったまま)。止めると変換も終わるようにしました。
|
|
|
|
| 5 |
日付は日本時間です。各版は Hugging Face の `ryugyosoft/onw` にタグ(`v0.4.2` 以降)付きで残っています。
|
| 6 |
onw ウィンドウは、新しい版があるとこのファイルから変更点を表示します(`## v…` の見出しで区切っています)。
|
| 7 |
|
| 8 |
+
## v3.3.3 (2026-10-26)
|
| 9 |
+
- **裏でのコンパイルが onw を閉じた後も続いていたのを修正(モデルの更新は不要)**:画像エンコーダー・融合グラフ・初回ロードの区間を裏でコンパイルする子プロセスが、onw のサーバーを止めたり終了したりした後も動き続けていました(画像エンコーダーは 10 分以上 CPU を使い続け、onw の更新でファイルを置き換えられないこともあった)。起動した onw が終わると、子プロセスも終わるようにしました。途中までの分は、次のロードで続きからコンパイルします。
|
| 10 |
+
- 画像エンコーダーの裏でのコンパイルは、ほかの子プロセスと同じくコンパイルだけを行います(NPU に実行の準備を作らない。同じものを OpenVINO のキャッシュにも書いていたのをやめました)。
|
| 11 |
+
- NPU が受け付けなかった画像エンコーダーのコンパイル結果は、キャッシュから消します(Gemma 4 で約 230 MB)。
|
| 12 |
+
|
| 13 |
## v3.3.2 (2026-10-25)
|
| 14 |
- **不具合の修正(モデルの更新は不要)**
|
| 15 |
- この PC で変換するモデル(LFM2.5 24B-A2B)の「ダウンロードして変換」を途中で止めても、変換の処理が裏で動き続けていました(メモリを使ったまま)。止めると変換も終わるようにしました。
|
CHANGELOG_en.md
CHANGED
|
@@ -4,6 +4,11 @@
|
|
| 4 |
|
| 5 |
Dates are JST. Every version is on Hugging Face `ryugyosoft/onw`, tagged from `v0.4.2` on.
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
## v3.3.2 (2026-10-25)
|
| 8 |
- **Fixes (no model update needed)**
|
| 9 |
- Stopping "Download and convert" (models converted on this PC, LFM2.5 24B-A2B) left the conversion running in the background (holding its memory). Stopping now ends the conversion too.
|
|
|
|
| 4 |
|
| 5 |
Dates are JST. Every version is on Hugging Face `ryugyosoft/onw`, tagged from `v0.4.2` on.
|
| 6 |
|
| 7 |
+
## v3.3.3 (2026-10-26)
|
| 8 |
+
- **Fix: background compiles went on after onw was closed (no model update needed)**: the child processes that compile the vision encoder, the fused graphs and a first load's segments in the background kept running after the onw server was stopped or exited (the vision encoder for 10+ minutes on the CPU, sometimes keeping an onw update from replacing its files). They now end with the onw that started them; the next load compiles what is missing.
|
| 9 |
+
- The vision encoder's background compile only compiles now, like the other children (no executor on the NPU, and no second copy in OpenVINO's cache).
|
| 10 |
+
- A vision encoder blob the NPU refused is removed from the cache (~230 MB for Gemma 4).
|
| 11 |
+
|
| 12 |
## v3.3.2 (2026-10-25)
|
| 13 |
- **Fixes (no model update needed)**
|
| 14 |
- Stopping "Download and convert" (models converted on this PC, LFM2.5 24B-A2B) left the conversion running in the background (holding its memory). Stopping now ends the conversion too.
|
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.3.
|
|
|
|
| 2 |
|
| 3 |
A small engine that runs LLMs entirely on the Intel NPU: static segment graphs + host-managed state.
|
| 4 |
"""
|
| 5 |
+
__version__ = "3.3.3"
|
onw/chat.py
CHANGED
|
@@ -180,7 +180,10 @@ def export_vision_blob(core, d, device, c):
|
|
| 180 |
bp = vision_blob(d, c)
|
| 181 |
if bp is None or os.path.exists(bp) or os.path.exists(bp + ".bad"):
|
| 182 |
return
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
| 184 |
buf = io.BytesIO()
|
| 185 |
first.export_model(buf)
|
| 186 |
os.makedirs(os.path.dirname(bp), exist_ok=True)
|
|
@@ -335,12 +338,13 @@ class ChatEngine:
|
|
| 335 |
vb = vision_blob(engine_dir, self._venc_args[3])
|
| 336 |
if vb is not None and not os.path.exists(vb) and not os.path.exists(vb + ".bad"):
|
| 337 |
import subprocess, sys
|
|
|
|
| 338 |
flags = 0x00004000 if os.name == "nt" else 0 # (BELOW_NORMAL_PRIORITY_CLASS)
|
| 339 |
self._venc_job = subprocess.Popen(
|
| 340 |
[sys.executable, "-m", "onw.visionjob", json.dumps([engine_dir, device, self._venc_args[3]],
|
| 341 |
default=str)],
|
| 342 |
-
env={**os.environ, "PYTHONUTF8": "1"}, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
| 343 |
-
creationflags=flags)
|
| 344 |
print("[vision] encoder compiling in the background", flush=True)
|
| 345 |
pld_state = "off (memory-saving mode)" if self.swap_gb is not None else "on" if self.pld else "off"
|
| 346 |
print(f"[load] {time.time()-t0:.1f}s on {device}; verify block = {self.block_cost or 0:.1f} single steps, "
|
|
@@ -420,6 +424,10 @@ class ChatEngine:
|
|
| 420 |
err = e
|
| 421 |
if bp:
|
| 422 |
open(bp + ".bad", "w").write(str(e)[-500:])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
raise err
|
| 424 |
|
| 425 |
# ---- prompt ----
|
|
|
|
| 180 |
bp = vision_blob(d, c)
|
| 181 |
if bp is None or os.path.exists(bp) or os.path.exists(bp + ".bad"):
|
| 182 |
return
|
| 183 |
+
# [3.3.3] compile only, as the fused graphs' children do: no NPU executor next to the parent's inference (one created
|
| 184 |
+
# there lost the NPU on Panther Lake) and no CACHE_DIR (OpenVINO's own cache file was a second copy of this blob)
|
| 185 |
+
nc = {**{k: v for k, v in c.items() if k != "CACHE_DIR"}, **({"NPU_CREATE_EXECUTOR": "0"} if device == "NPU" else {})}
|
| 186 |
+
first = core.compile_model(os.path.join(d, "vision.xml"), device, nc)
|
| 187 |
buf = io.BytesIO()
|
| 188 |
first.export_model(buf)
|
| 189 |
os.makedirs(os.path.dirname(bp), exist_ok=True)
|
|
|
|
| 338 |
vb = vision_blob(engine_dir, self._venc_args[3])
|
| 339 |
if vb is not None and not os.path.exists(vb) and not os.path.exists(vb + ".bad"):
|
| 340 |
import subprocess, sys
|
| 341 |
+
from .childguard import child_env
|
| 342 |
flags = 0x00004000 if os.name == "nt" else 0 # (BELOW_NORMAL_PRIORITY_CLASS)
|
| 343 |
self._venc_job = subprocess.Popen(
|
| 344 |
[sys.executable, "-m", "onw.visionjob", json.dumps([engine_dir, device, self._venc_args[3]],
|
| 345 |
default=str)],
|
| 346 |
+
env=child_env({**os.environ, "PYTHONUTF8": "1"}), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
| 347 |
+
creationflags=flags) # [3.3.3] (child_env: it ends with this process)
|
| 348 |
print("[vision] encoder compiling in the background", flush=True)
|
| 349 |
pld_state = "off (memory-saving mode)" if self.swap_gb is not None else "on" if self.pld else "off"
|
| 350 |
print(f"[load] {time.time()-t0:.1f}s on {device}; verify block = {self.block_cost or 0:.1f} single steps, "
|
|
|
|
| 424 |
err = e
|
| 425 |
if bp:
|
| 426 |
open(bp + ".bad", "w").write(str(e)[-500:])
|
| 427 |
+
try: # [3.3.3] the refused blob is never used again (Gemma 4 on
|
| 428 |
+
os.remove(bp) # Panther Lake: 237 MB left in the cache)
|
| 429 |
+
except OSError:
|
| 430 |
+
pass
|
| 431 |
raise err
|
| 432 |
|
| 433 |
# ---- prompt ----
|
onw/childguard.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""[3.3.3] The background compile children (onw.visionjob, onw.fusedjob, onw.precompile) end with the onw process that
|
| 2 |
+
started them: they only fill onw's blob store, and the next load starts what is missing again. Before, a vision encoder
|
| 3 |
+
child kept compiling for 10+ minutes after its server had exited (Panther Lake, Gemma 4), holding CPU and the kit's DLLs.
|
| 4 |
+
The parent passes its pid (child_env); the child watches it (watch_parent) - not its own parent process, which on Windows
|
| 5 |
+
is the venv's launcher and outlives onw."""
|
| 6 |
+
import os
|
| 7 |
+
import threading
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
ENV = "ONW_PARENT_PID"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def child_env(env=None):
|
| 14 |
+
"""The environment for a compile child: env (default os.environ) plus this process's pid."""
|
| 15 |
+
return {**(os.environ if env is None else env), ENV: str(os.getpid())}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def watch_parent(every=2.0):
|
| 19 |
+
"""In a child: end this process (exit code 4) once the onw process named in ONW_PARENT_PID is gone."""
|
| 20 |
+
pid = os.environ.get(ENV)
|
| 21 |
+
if not pid:
|
| 22 |
+
return
|
| 23 |
+
|
| 24 |
+
def run():
|
| 25 |
+
try:
|
| 26 |
+
import psutil
|
| 27 |
+
except ImportError: # (nothing to watch with: run to the end as before)
|
| 28 |
+
return
|
| 29 |
+
try:
|
| 30 |
+
p = psutil.Process(int(pid))
|
| 31 |
+
except (psutil.Error, ValueError): # (the parent is gone already)
|
| 32 |
+
os._exit(4)
|
| 33 |
+
while True:
|
| 34 |
+
time.sleep(every)
|
| 35 |
+
try:
|
| 36 |
+
if not p.is_running() or p.status() == psutil.STATUS_ZOMBIE: # (is_running checks the start time:
|
| 37 |
+
os._exit(4) # a reused pid is not the parent)
|
| 38 |
+
except psutil.Error:
|
| 39 |
+
os._exit(4)
|
| 40 |
+
|
| 41 |
+
threading.Thread(target=run, daemon=True, name="onw-parent-watch").start()
|
onw/fusedjob.py
CHANGED
|
@@ -32,6 +32,8 @@ class _Model:
|
|
| 32 |
|
| 33 |
|
| 34 |
def main():
|
|
|
|
|
|
|
| 35 |
d, device, config, tried_key, built_T, capacity, kt, S, B = json.loads(sys.argv[1])
|
| 36 |
from .fused import FusedDecode
|
| 37 |
core = npukit.core()
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
def main():
|
| 35 |
+
from .childguard import watch_parent
|
| 36 |
+
watch_parent() # [3.3.3] (ends with the onw process that started it)
|
| 37 |
d, device, config, tried_key, built_T, capacity, kt, S, B = json.loads(sys.argv[1])
|
| 38 |
from .fused import FusedDecode
|
| 39 |
core = npukit.core()
|
onw/precompile.py
CHANGED
|
@@ -1,20 +1,22 @@
|
|
| 1 |
-
"""A first load's precompile child (runtime.compile_jobs): build the segments named in ONW_PRECOMPILE_ONLY exactly as
|
| 2 |
-
the parent would, so they land in the compile cache (marked as the parent's run, ONW_NPUW_SESSION), then exit.
|
| 3 |
-
usage (internal): python -m onw.precompile '[MODEL_DIR, DEVICE, CONFIG, SIZES, CONTEXT]'"""
|
| 4 |
-
import json, sys
|
| 5 |
-
import openvino as ov
|
| 6 |
-
from . import npukit
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
def main():
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
"""A first load's precompile child (runtime.compile_jobs): build the segments named in ONW_PRECOMPILE_ONLY exactly as
|
| 2 |
+
the parent would, so they land in the compile cache (marked as the parent's run, ONW_NPUW_SESSION), then exit.
|
| 3 |
+
usage (internal): python -m onw.precompile '[MODEL_DIR, DEVICE, CONFIG, SIZES, CONTEXT]'"""
|
| 4 |
+
import json, sys
|
| 5 |
+
import openvino as ov
|
| 6 |
+
from . import npukit
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main():
|
| 10 |
+
from .childguard import watch_parent
|
| 11 |
+
watch_parent() # [3.3.3] (ends with the onw process that started it)
|
| 12 |
+
d, device, config, sizes, context = json.loads(sys.argv[1])
|
| 13 |
+
from .runtime import Precompiled, SegmentedModel
|
| 14 |
+
try:
|
| 15 |
+
SegmentedModel(npukit.core(), d, device, config, sizes=sizes, context=context)
|
| 16 |
+
except Precompiled:
|
| 17 |
+
return
|
| 18 |
+
sys.exit(3) # (not reached: the model raises once compiled)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if __name__ == "__main__":
|
| 22 |
+
main()
|
onw/runtime.py
CHANGED
|
@@ -1869,7 +1869,8 @@ class SegmentedModel:
|
|
| 1869 |
arg = json.dumps([c["d"], c["device"], cfg, c["tried"].key, c["built_T"], self.kv.capacity, kt, S, B],
|
| 1870 |
default=str)
|
| 1871 |
flags = 0x00004000 if os.name == "nt" else 0 # (BELOW_NORMAL_PRIORITY_CLASS: answers go first)
|
| 1872 |
-
|
|
|
|
| 1873 |
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=flags)
|
| 1874 |
running.append(p)
|
| 1875 |
with _FUSED_KIDS_LOCK:
|
|
@@ -2063,8 +2064,9 @@ class SegmentedModel:
|
|
| 2063 |
while waiting or running:
|
| 2064 |
while waiting and len(running) < jobs:
|
| 2065 |
n = waiting.pop(0)
|
|
|
|
| 2066 |
running.append((n, subprocess.Popen([sys.executable, "-m", "onw.precompile", args],
|
| 2067 |
-
env={**env, "ONW_PRECOMPILE_ONLY": n}, stdout=subprocess.DEVNULL,
|
| 2068 |
stderr=subprocess.DEVNULL, **kw)))
|
| 2069 |
time.sleep(0.5)
|
| 2070 |
for n, p in [x for x in running if x[1].poll() is not None]:
|
|
|
|
| 1869 |
arg = json.dumps([c["d"], c["device"], cfg, c["tried"].key, c["built_T"], self.kv.capacity, kt, S, B],
|
| 1870 |
default=str)
|
| 1871 |
flags = 0x00004000 if os.name == "nt" else 0 # (BELOW_NORMAL_PRIORITY_CLASS: answers go first)
|
| 1872 |
+
from .childguard import child_env # [3.3.3] (ends with this process)
|
| 1873 |
+
p = subprocess.Popen([sys.executable, "-m", "onw.fusedjob", arg], env=child_env({**env, "ONW_FUSED_SEG": seg}),
|
| 1874 |
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=flags)
|
| 1875 |
running.append(p)
|
| 1876 |
with _FUSED_KIDS_LOCK:
|
|
|
|
| 2064 |
while waiting or running:
|
| 2065 |
while waiting and len(running) < jobs:
|
| 2066 |
n = waiting.pop(0)
|
| 2067 |
+
from .childguard import child_env # [3.3.3] (ends with this process)
|
| 2068 |
running.append((n, subprocess.Popen([sys.executable, "-m", "onw.precompile", args],
|
| 2069 |
+
env=child_env({**env, "ONW_PRECOMPILE_ONLY": n}), stdout=subprocess.DEVNULL,
|
| 2070 |
stderr=subprocess.DEVNULL, **kw)))
|
| 2071 |
time.sleep(0.5)
|
| 2072 |
for n, p in [x for x in running if x[1].poll() is not None]:
|
onw/visionjob.py
CHANGED
|
@@ -7,6 +7,8 @@ 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
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
def main():
|
| 10 |
+
from .childguard import watch_parent
|
| 11 |
+
watch_parent() # [3.3.3] (ends with the onw process that started it)
|
| 12 |
d, device, cfg = json.loads(sys.argv[1])
|
| 13 |
from .chat import export_vision_blob
|
| 14 |
from .runtime import enable_compiler
|
pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "onw"
|
| 7 |
-
version = "3.3.
|
| 8 |
description = "Ore no NPU ga konna ni ugoku wake nai - LLMs entirely on the Intel NPU"
|
| 9 |
readme = "README.md"
|
| 10 |
license = {text = "Apache-2.0"}
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "onw"
|
| 7 |
+
version = "3.3.3"
|
| 8 |
description = "Ore no NPU ga konna ni ugoku wake nai - LLMs entirely on the Intel NPU"
|
| 9 |
readme = "README.md"
|
| 10 |
license = {text = "Apache-2.0"}
|