ryugyosoft commited on
Commit
27a5a32
·
verified ·
1 Parent(s): c739c9e

onw 3.3.3: background compiles end with onw

Browse files
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"
 
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
- first = core.compile_model(os.path.join(d, "vision.xml"), device, c)
 
 
 
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
- d, device, config, sizes, context = json.loads(sys.argv[1])
11
- from .runtime import Precompiled, SegmentedModel
12
- try:
13
- SegmentedModel(npukit.core(), d, device, config, sizes=sizes, context=context)
14
- except Precompiled:
15
- return
16
- sys.exit(3) # (not reached: the model raises once compiled)
17
-
18
-
19
- if __name__ == "__main__":
20
- main()
 
 
 
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
- p = subprocess.Popen([sys.executable, "-m", "onw.fusedjob", arg], env={**env, "ONW_FUSED_SEG": seg},
 
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.2"
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"}