onwx 0.6.1: faster reading of what agents add after a long context (Hermes run 989 -> 778 s), tool calls written inside the thinking, API max output tokens setting (default 8192), an anchor checkpoint at user messages, clearer XRT diagnosis on Linux
Browse files- README.md +3 -1
- README_en.md +3 -2
- onwx/__init__.py +1 -1
- onwx/app.py +2 -1
- onwx/chat.py +43 -10
- onwx/i18n.json +6 -0
- onwx/manager.py +6 -1
- onwx/npu_pfx.py +14 -5
- onwx/server.py +6 -1
- onwx/web/manage.html +5 -1
- onwx/xdna.py +86 -13
- pyproject.toml +1 -1
README.md
CHANGED
|
@@ -45,6 +45,7 @@ onwx の MTP(温度 0 のときの先読み)を使った値は、生成の
|
|
| 45 |
- 512 トークンの回答をプロンプトから書き終えるまでの実効速度(512 ÷ 合計時間)は、128〜32K のどの長さでも onwx が上です(MTP なしでも)。
|
| 46 |
- プロンプトの読み込みは 2K〜8K で onwx が速く(1K はほぼ同じ。4K で FLM 8.7 秒、onwx 7.7 秒)、256〜512 と 16K〜32K では FLM が少し速いです。CPU も使う設定では 16K まで onwx が速く、32K でほぼ同じです(FLM 74 秒、onwx 74 秒)。
|
| 47 |
- 生成はどの長さでも onwx が 3〜4 割速く、MTP を使うと 8K までの文脈で約 14〜18 tok/s です(24K を超えると MTP は自動で止まります)。
|
|
|
|
| 48 |
- Ryzen AI 9 HX 370 でも傾向は同じでした(onwx 0.5.0。32K のプロンプト:FLM 379 tok/s、onwx 339 tok/s、生成 FLM 6.0 tok/s、onwx 8.9 tok/s)。
|
| 49 |
|
| 50 |
## インストール(コマンドを 1 行貼るだけ)
|
|
@@ -77,7 +78,7 @@ curl -fsSL https://huggingface.co/ryugyosoft/onwx/resolve/main/install.sh | bash
|
|
| 77 |
Ryzen AI Max シリーズ(Strix Halo、例:Ryzen AI Max+ 395)
|
| 78 |
- **Windows 11** と AMD の NPU ドライバー(32.0.203 以降で確認。Windows Update か PC メーカーのドライバーで入ります)。
|
| 79 |
タスクマネージャーの「パフォーマンス」に NPU が出ていれば大丈夫です
|
| 80 |
-
- Ubuntu では NPU ドライバー(amdxdna)
|
| 81 |
- メモリ 16 GB 以上(Ternary Bonsai 2 27B で約 8.8 GB を使います)
|
| 82 |
- 空き容量:アプリ約 0.5 GB + モデル 7.6 GB
|
| 83 |
- インターネット接続(最初のダウンロードのとき)
|
|
@@ -122,6 +123,7 @@ onwx chat ryugyosoft/Ternary-Bonsai-2-27B-onwx # 端末でチャット
|
|
| 122 |
`/v1/chat/completions`(ストリーミング可)、`/v1/models`、`/v1/messages`(Anthropic 形式)、`/v1/responses` に対応しています。
|
| 123 |
思考モードは `"chat_template_kwargs": {"enable_thinking": true}` か `"reasoning_effort"` で切り替えます。
|
| 124 |
設定で LAN からの接続を許可すると、他の PC からも使えます(API キーの設定をおすすめします)。
|
|
|
|
| 125 |
|
| 126 |
## ライセンス
|
| 127 |
|
|
|
|
| 45 |
- 512 トークンの回答をプロンプトから書き終えるまでの実効速度(512 ÷ 合計時間)は、128〜32K のどの長さでも onwx が上です(MTP なしでも)。
|
| 46 |
- プロンプトの読み込みは 2K〜8K で onwx が速く(1K はほぼ同じ。4K で FLM 8.7 秒、onwx 7.7 秒)、256〜512 と 16K〜32K では FLM が少し速いです。CPU も使う設定では 16K まで onwx が速く、32K でほぼ同じです(FLM 74 秒、onwx 74 秒)。
|
| 47 |
- 生成はどの長さでも onwx が 3〜4 割速く、MTP を使うと 8K までの文脈で約 14〜18 tok/s です(24K を超えると MTP は自動で止まります)。
|
| 48 |
+
- **エージェントでも速い**:Hermes Agent のコーディング作業(バグ修正、29 回のやり取り、文脈 1.2 万〜2.9 万トークン)で記録した同じリクエストを流すと、全体で onwx 0.6.1 が 778 秒、FLM が 874 秒でした(0.6.0 は 989 秒。長い文脈の後ろに足す読み込みを 0.6.1 で速くしました)。
|
| 49 |
- Ryzen AI 9 HX 370 でも傾向は同じでした(onwx 0.5.0。32K のプロンプト:FLM 379 tok/s、onwx 339 tok/s、生成 FLM 6.0 tok/s、onwx 8.9 tok/s)。
|
| 50 |
|
| 51 |
## インストール(コマンドを 1 行貼るだけ)
|
|
|
|
| 78 |
Ryzen AI Max シリーズ(Strix Halo、例:Ryzen AI Max+ 395)
|
| 79 |
- **Windows 11** と AMD の NPU ドライバー(32.0.203 以降で確認。Windows Update か PC メーカーのドライバーで入ります)。
|
| 80 |
タスクマネージャーの「パフォーマンス」に NPU が出ていれば大丈夫です
|
| 81 |
+
- Ubuntu では NPU ドライバー(amdxdna)に加えて、AMD の NPU ランタイム XRT(Python バインディング pyxrt 付き)が必要です。カーネルのドライバーだけでは動きません。AMD の [xdna-driver](https://github.com/amd/xdna-driver) の手順で XRT と NPU プラグインを入れてください(`/opt/xilinx/xrt` に入ります)。足りないものがあると、onwx のウィンドウに理由が出ます
|
| 82 |
- メモリ 16 GB 以上(Ternary Bonsai 2 27B で約 8.8 GB を使います)
|
| 83 |
- 空き容量:アプリ約 0.5 GB + モデル 7.6 GB
|
| 84 |
- インターネット接続(最初のダウンロードのとき)
|
|
|
|
| 123 |
`/v1/chat/completions`(ストリーミング可)、`/v1/models`、`/v1/messages`(Anthropic 形式)、`/v1/responses` に対応しています。
|
| 124 |
思考モードは `"chat_template_kwargs": {"enable_thinking": true}` か `"reasoning_effort"` で切り替えます。
|
| 125 |
設定で LAN からの接続を許可すると、他の PC からも使えます(API キーの設定をおすすめします)。
|
| 126 |
+
アプリが `max_tokens` を送らないときの回答の長さの上限は、設定の「API の最大出力トークン」で変えられます(既定 8192)。
|
| 127 |
|
| 128 |
## ライセンス
|
| 129 |
|
README_en.md
CHANGED
|
@@ -33,6 +33,7 @@ match. onwx with its MTP drafts (temperature 0) is shown apart, as a dashed line
|
|
| 33 |
- A 512-token answer, start to finish (512 / total time): onwx is ahead at every length from 128 to 32K, even without MTP.
|
| 34 |
- Prompt reading: onwx is faster from 2K to 8K (1K about the same; 4K: FLM 8.7 s, onwx 7.7 s); FLM is a little faster at 256-512 and 16K-32K. With the CPU setting onwx is faster up to 16K and about the same at 32K (FLM 74 s, onwx 74 s).
|
| 35 |
- Generation: onwx is 30-40% faster at every length; with MTP about 14-18 tok/s up to 8K of context (past 24K MTP turns itself off).
|
|
|
|
| 36 |
- The Ryzen AI 9 HX 370 shows the same picture (onwx 0.5.0; a 32K prompt: FLM 379 tok/s, onwx 339 tok/s; generation FLM 6.0, onwx 8.9 tok/s).
|
| 37 |
|
| 38 |
## Install (paste one line)
|
|
@@ -61,7 +62,7 @@ curl -fsSL https://huggingface.co/ryugyosoft/onwx/resolve/main/install.sh | bash
|
|
| 61 |
series (Strix Halo, e.g. Ryzen AI Max+ 395)
|
| 62 |
- **Windows 11** with AMD's NPU driver (tested with 32.0.203; it comes with Windows Update or the PC maker's drivers) -
|
| 63 |
if Task Manager's Performance tab shows an NPU, you are set
|
| 64 |
-
- On Ubuntu: the NPU driver (amdxdna)
|
| 65 |
- 16 GB of RAM or more (Ternary Bonsai 2 27B takes about 8.8 GB)
|
| 66 |
- Disk: about 0.5 GB for the app + 7.6 GB for the model
|
| 67 |
|
|
@@ -104,7 +105,7 @@ onwx chat ryugyosoft/Ternary-Bonsai-2-27B-onwx # chat in the terminal
|
|
| 104 |
|
| 105 |
`/v1/chat/completions` (streaming too), `/v1/models`, `/v1/messages` (Anthropic format) and `/v1/responses`.
|
| 106 |
Thinking: `"chat_template_kwargs": {"enable_thinking": true}` or `"reasoning_effort"`. Allow LAN connections in the
|
| 107 |
-
settings to use it from other PCs (set an API key then).
|
| 108 |
|
| 109 |
## License
|
| 110 |
|
|
|
|
| 33 |
- A 512-token answer, start to finish (512 / total time): onwx is ahead at every length from 128 to 32K, even without MTP.
|
| 34 |
- Prompt reading: onwx is faster from 2K to 8K (1K about the same; 4K: FLM 8.7 s, onwx 7.7 s); FLM is a little faster at 256-512 and 16K-32K. With the CPU setting onwx is faster up to 16K and about the same at 32K (FLM 74 s, onwx 74 s).
|
| 35 |
- Generation: onwx is 30-40% faster at every length; with MTP about 14-18 tok/s up to 8K of context (past 24K MTP turns itself off).
|
| 36 |
+
- **Fast for agents too**: the same recorded requests of a Hermes Agent coding task (a bug fix, 29 requests, 12K-29K tokens of context) took 778 s on onwx 0.6.1 and 874 s on FLM (0.6.0: 989 s - 0.6.1 reads what is added after a long context faster).
|
| 37 |
- The Ryzen AI 9 HX 370 shows the same picture (onwx 0.5.0; a 32K prompt: FLM 379 tok/s, onwx 339 tok/s; generation FLM 6.0, onwx 8.9 tok/s).
|
| 38 |
|
| 39 |
## Install (paste one line)
|
|
|
|
| 62 |
series (Strix Halo, e.g. Ryzen AI Max+ 395)
|
| 63 |
- **Windows 11** with AMD's NPU driver (tested with 32.0.203; it comes with Windows Update or the PC maker's drivers) -
|
| 64 |
if Task Manager's Performance tab shows an NPU, you are set
|
| 65 |
+
- On Ubuntu: besides the NPU driver (amdxdna), AMD's NPU runtime XRT with its Python bindings (pyxrt) - the kernel driver alone is not enough. Install XRT and its NPU plugin as AMD's [xdna-driver](https://github.com/amd/xdna-driver) describes (it goes to `/opt/xilinx/xrt`); if something is missing, the onwx window says what
|
| 66 |
- 16 GB of RAM or more (Ternary Bonsai 2 27B takes about 8.8 GB)
|
| 67 |
- Disk: about 0.5 GB for the app + 7.6 GB for the model
|
| 68 |
|
|
|
|
| 105 |
|
| 106 |
`/v1/chat/completions` (streaming too), `/v1/models`, `/v1/messages` (Anthropic format) and `/v1/responses`.
|
| 107 |
Thinking: `"chat_template_kwargs": {"enable_thinking": true}` or `"reasoning_effort"`. Allow LAN connections in the
|
| 108 |
+
settings to use it from other PCs (set an API key then). The longest answer when an app sends no `max_tokens` is the setting "API max output tokens" (default 8192).
|
| 109 |
|
| 110 |
## License
|
| 111 |
|
onwx/__init__.py
CHANGED
|
@@ -2,6 +2,6 @@
|
|
| 2 |
|
| 3 |
(A working name: the product's name is set here and in the install scripts.)
|
| 4 |
"""
|
| 5 |
-
__version__ = "0.6.
|
| 6 |
NAME = "onwx"
|
| 7 |
TITLE = "onwx (AMD Ryzen AI NPU)"
|
|
|
|
| 2 |
|
| 3 |
(A working name: the product's name is set here and in the install scripts.)
|
| 4 |
"""
|
| 5 |
+
__version__ = "0.6.1"
|
| 6 |
NAME = "onwx"
|
| 7 |
TITLE = "onwx (AMD Ryzen AI NPU)"
|
onwx/app.py
CHANGED
|
@@ -40,7 +40,7 @@ MEMORY = {
|
|
| 40 |
STREAMABLE = {} # (no model streams weights from the SSD)
|
| 41 |
DEFAULTS = {"model": "", "port": 8100, "lan": False, "api_key": "", "context": 3840,
|
| 42 |
"autostart_server": True, "login_start": False, "models_dir": "", "model_folders": [], "update_check": True,
|
| 43 |
-
"think_default": False, "lang": "auto", "cpu_glue": False,
|
| 44 |
"api_sampling": "greedy", "api_temperature": 0.7, "api_top_p": 0.95, "api_top_k": 20, "api_presence": 0.0,
|
| 45 |
"model_prefs": {}}
|
| 46 |
CONTEXTS = ["1024", "2048", "3840", "8192", "16384", "32768", "65536", "131072", "262144"] # (models built before the
|
|
@@ -639,6 +639,7 @@ def _start_server(cfg):
|
|
| 639 |
env["ONWX_THINK_DEFAULT"] = "1"
|
| 640 |
if cfg.get("cpu_glue"): # [0.5] the prefill glue ops on the CPU (opt-in)
|
| 641 |
env["ONWX_CPU_GLUE"] = "1"
|
|
|
|
| 642 |
env["ONWX_MODEL_KEY"] = cfg["model"] # the server reads this model's defaults (model_prefs) itself
|
| 643 |
# API requests that send no sampling values (agent apps like OpenCode): greedy, the model's recommendation or set
|
| 644 |
if cfg.get("api_sampling") == "recommended":
|
|
|
|
| 40 |
STREAMABLE = {} # (no model streams weights from the SSD)
|
| 41 |
DEFAULTS = {"model": "", "port": 8100, "lan": False, "api_key": "", "context": 3840,
|
| 42 |
"autostart_server": True, "login_start": False, "models_dir": "", "model_folders": [], "update_check": True,
|
| 43 |
+
"think_default": False, "lang": "auto", "cpu_glue": False, "api_max_tokens": 8192,
|
| 44 |
"api_sampling": "greedy", "api_temperature": 0.7, "api_top_p": 0.95, "api_top_k": 20, "api_presence": 0.0,
|
| 45 |
"model_prefs": {}}
|
| 46 |
CONTEXTS = ["1024", "2048", "3840", "8192", "16384", "32768", "65536", "131072", "262144"] # (models built before the
|
|
|
|
| 639 |
env["ONWX_THINK_DEFAULT"] = "1"
|
| 640 |
if cfg.get("cpu_glue"): # [0.5] the prefill glue ops on the CPU (opt-in)
|
| 641 |
env["ONWX_CPU_GLUE"] = "1"
|
| 642 |
+
env["ONWX_MAX_TOKENS"] = str(int(cfg.get("api_max_tokens", 8192))) # [0.6.1] requests without max_tokens
|
| 643 |
env["ONWX_MODEL_KEY"] = cfg["model"] # the server reads this model's defaults (model_prefs) itself
|
| 644 |
# API requests that send no sampling values (agent apps like OpenCode): greedy, the model's recommendation or set
|
| 645 |
if cfg.get("api_sampling") == "recommended":
|
onwx/chat.py
CHANGED
|
@@ -97,6 +97,7 @@ class ChatEngine:
|
|
| 97 |
self.device = "NPU"
|
| 98 |
self.pld = False
|
| 99 |
self.checkpoint = None # (token ids, model snapshot) at the end of the last conversation
|
|
|
|
| 100 |
m = self.model # warm up (a request's first inference is slow)
|
| 101 |
for rep in range(2):
|
| 102 |
m.reset()
|
|
@@ -138,6 +139,10 @@ class ChatEngine:
|
|
| 138 |
if "<think>" in raw:
|
| 139 |
pre, _, rest = raw.partition("<think>")
|
| 140 |
reasoning, closed, post = rest.partition("</think>")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
if not closed: # still thinking (hold back a half-written tag)
|
| 142 |
for k in range(len("</think>") - 1, 0, -1):
|
| 143 |
if reasoning.endswith("</think>"[:k]):
|
|
@@ -428,14 +433,21 @@ class ChatEngine:
|
|
| 428 |
pos, delta = self._positions(ids)
|
| 429 |
key = lambda upto: (ids[:upto], [h for s0, e0, h in spans if s0 < upto])
|
| 430 |
# prefix reuse: state kept at the end of the previous conversation (before its generation prompt)
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
else:
|
| 436 |
self.model.reset()
|
| 437 |
-
self.checkpoint = None
|
| 438 |
-
|
| 439 |
t0 = time.time()
|
| 440 |
t_vis = 0.0
|
| 441 |
if images and any(e0 > reuse for s0, e0, _ in spans):
|
|
@@ -454,11 +466,32 @@ class ChatEngine:
|
|
| 454 |
self.model.feeding = True # ([MTP] the MTP layer's rows go along with the prompt)
|
| 455 |
w = int(os.environ.get("ONWX_MTP_ROWS", "128") or 0) # ([MTP] rows of the last w prompt tokens only (~3.5 ms a row); 0: all)
|
| 456 |
self.model.mtp_from = max(0, n - w) if w > 0 else 0
|
|
|
|
|
|
|
| 457 |
try:
|
| 458 |
-
if
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
self.model.delta = delta
|
| 463 |
except Cancelled:
|
| 464 |
yield {"finish_reason": "stop", "prompt_tokens": n, "cached_tokens": reuse, "completion_tokens": 0,
|
|
|
|
| 97 |
self.device = "NPU"
|
| 98 |
self.pld = False
|
| 99 |
self.checkpoint = None # (token ids, model snapshot) at the end of the last conversation
|
| 100 |
+
self.anchor = None # (the same at the end of its last user message)
|
| 101 |
m = self.model # warm up (a request's first inference is slow)
|
| 102 |
for rep in range(2):
|
| 103 |
m.reset()
|
|
|
|
| 139 |
if "<think>" in raw:
|
| 140 |
pre, _, rest = raw.partition("<think>")
|
| 141 |
reasoning, closed, post = rest.partition("</think>")
|
| 142 |
+
tc = reasoning.find("<tool_call>") if markup else -1
|
| 143 |
+
if tc >= 0: # a call written inside the thought (Qwen3.5 does it
|
| 144 |
+
reasoning, post, closed = reasoning[:tc], reasoning[tc:] + post, True # after tool results, never
|
| 145 |
+
# closing it): the thought ends where the call starts
|
| 146 |
if not closed: # still thinking (hold back a half-written tag)
|
| 147 |
for k in range(len("</think>") - 1, 0, -1):
|
| 148 |
if reasoning.endswith("</think>"[:k]):
|
|
|
|
| 433 |
pos, delta = self._positions(ids)
|
| 434 |
key = lambda upto: (ids[:upto], [h for s0, e0, h in spans if s0 < upto])
|
| 435 |
# prefix reuse: state kept at the end of the previous conversation (before its generation prompt)
|
| 436 |
+
# or, failing that, at the end of the conversation's last user message (the anchor): agents add a user message
|
| 437 |
+
# after tool turns (Hermes: "continue"), and Qwen's template then drops the thinking of the turns since the
|
| 438 |
+
# last user message - the prompt differs from there on, not from the start
|
| 439 |
+
reuse = 0
|
| 440 |
+
for ck in (self.checkpoint, self.anchor):
|
| 441 |
+
if ck is not None and len(ck[0][0]) <= B and key(len(ck[0][0])) == ck[0]:
|
| 442 |
+
reuse = len(ck[0][0])
|
| 443 |
+
self.model.restore(ck[1])
|
| 444 |
+
if ck is not self.checkpoint: # (the latest one's rows above the anchor get overwritten)
|
| 445 |
+
self.checkpoint = None
|
| 446 |
+
break
|
| 447 |
else:
|
| 448 |
self.model.reset()
|
| 449 |
+
self.checkpoint = self.anchor = None # (their KV rows are overwritten now: a prefill failing part-way
|
| 450 |
+
# would leave a checkpoint over another conversation's rows)
|
| 451 |
t0 = time.time()
|
| 452 |
t_vis = 0.0
|
| 453 |
if images and any(e0 > reuse for s0, e0, _ in spans):
|
|
|
|
| 466 |
self.model.feeding = True # ([MTP] the MTP layer's rows go along with the prompt)
|
| 467 |
w = int(os.environ.get("ONWX_MTP_ROWS", "128") or 0) # ([MTP] rows of the last w prompt tokens only (~3.5 ms a row); 0: all)
|
| 468 |
self.model.mtp_from = max(0, n - w) if w > 0 else 0
|
| 469 |
+
user = bool(messages) and messages[-1].get("role") == "user"
|
| 470 |
+
al = self.model.chunk_align() if hasattr(self.model, "chunk_align") and embeds is None and pos is None else 0
|
| 471 |
try:
|
| 472 |
+
if al:
|
| 473 |
+
# [0.6.1] checkpoints at multiples of the chunks' start unit (256): a chunk can only start there, and
|
| 474 |
+
# one that starts elsewhere has up to 255 tokens to read on the old programs first (seconds deep in a
|
| 475 |
+
# long cache). Each request reads from the checkpoint to its end in one go (re-reading what the last
|
| 476 |
+
# one added - a padded chunk costs the same); the checkpoint moves up once that passes 1024 tokens,
|
| 477 |
+
# or at a new user message (the anchor)
|
| 478 |
+
A = B // al * al
|
| 479 |
+
if A > reuse and (user or n - reuse > 1024):
|
| 480 |
+
self._prefill(ids, reuse, A, need_logits=False)
|
| 481 |
+
self.checkpoint = (key(A), self.model.snapshot())
|
| 482 |
+
if user:
|
| 483 |
+
self.anchor = self.checkpoint
|
| 484 |
+
reuse_to = A
|
| 485 |
+
else:
|
| 486 |
+
reuse_to = reuse
|
| 487 |
+
lg = self._prefill(ids, reuse_to, n)
|
| 488 |
+
else:
|
| 489 |
+
if B > reuse:
|
| 490 |
+
self._prefill(ids, reuse, B, need_logits=False, embeds=embeds, pos=pos)
|
| 491 |
+
self.checkpoint = (key(B), self.model.snapshot())
|
| 492 |
+
if user:
|
| 493 |
+
self.anchor = self.checkpoint # (a new user message: B is its end)
|
| 494 |
+
lg = self._prefill(ids, B, n, embeds=embeds, pos=pos)
|
| 495 |
self.model.delta = delta
|
| 496 |
except Cancelled:
|
| 497 |
yield {"finish_reason": "stop", "prompt_tokens": n, "cached_tokens": reuse, "completion_tokens": 0,
|
onwx/i18n.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
| 1 |
{
|
| 2 |
"en": {
|
|
|
|
|
|
|
|
|
|
| 3 |
"CPU も使ってプロンプトの読み込みを速くする(消費電力が増えます)": "Use the CPU too, to read prompts faster (uses more power)",
|
| 4 |
"長いプロンプトほど効きます。ノート PC ではファンの音が大きくなることがあります。": "Helps most with long prompts. On a laptop the fan may get louder.",
|
| 5 |
" + 画像 {0}秒": " + image {0} s",
|
|
@@ -363,6 +366,9 @@
|
|
| 363 |
"日本語に強い 8B を int4 の重みで、全部 NPU で動かします(国立情報学研究所の llm-jp-4.1-8b-thinking。考えてから答えるモデルです。生成は Ryzen AI 9 HX 370 で約 11 tok/s、1,000 トークン以上のプロンプトの読み込みは Ryzen AI Max+ 395 で約 300〜430 tok/s)。文脈は最大 65536 トークン": "An 8B strong in Japanese, with int4 weights, run entirely on the NPU (NII's llm-jp-4.1-8b-thinking, a model that thinks before it answers; generation about 11 tok/s on a Ryzen AI 9 HX 370, prompts of 1,000+ tokens read at about 300-430 tok/s on a Ryzen AI Max+ 395). Context up to 65536 tokens"
|
| 364 |
},
|
| 365 |
"zh": {
|
|
|
|
|
|
|
|
|
|
| 366 |
"CPU も使ってプロンプトの読み込みを速くする(消費電力が増えます)": "同时使用 CPU 加快读取提示词(耗电增加)",
|
| 367 |
"長いプロンプトほど効きます。ノート PC ではファンの音が大きくなることがあります。": "提示词越长效果越明显。笔记本电脑的风扇可能会更响。",
|
| 368 |
" + 画像 {0}秒": " + 图像 {0} 秒",
|
|
|
|
| 1 |
{
|
| 2 |
"en": {
|
| 3 |
+
"API の最大出力トークン": "API max output tokens",
|
| 4 |
+
"文脈の残りまで": "Up to the rest of the context",
|
| 5 |
+
"アプリが上限を指定しないときの、1 回の回答の長さの上限です(エージェントがファイルを書くときなどに効きます)。大きくしても使用メモリは増えません。": "The longest single answer when an app sets no limit (matters when agents write files). A larger value uses no more memory.",
|
| 6 |
"CPU も使ってプロンプトの読み込みを速くする(消費電力が増えます)": "Use the CPU too, to read prompts faster (uses more power)",
|
| 7 |
"長いプロンプトほど効きます。ノート PC ではファンの音が大きくなることがあります。": "Helps most with long prompts. On a laptop the fan may get louder.",
|
| 8 |
" + 画像 {0}秒": " + image {0} s",
|
|
|
|
| 366 |
"日本語に強い 8B を int4 の重みで、全部 NPU で動かします(国立情報学研究所の llm-jp-4.1-8b-thinking。考えてから答えるモデルです。生成は Ryzen AI 9 HX 370 で約 11 tok/s、1,000 トークン以上のプロンプトの読み込みは Ryzen AI Max+ 395 で約 300〜430 tok/s)。文脈は最大 65536 トークン": "An 8B strong in Japanese, with int4 weights, run entirely on the NPU (NII's llm-jp-4.1-8b-thinking, a model that thinks before it answers; generation about 11 tok/s on a Ryzen AI 9 HX 370, prompts of 1,000+ tokens read at about 300-430 tok/s on a Ryzen AI Max+ 395). Context up to 65536 tokens"
|
| 367 |
},
|
| 368 |
"zh": {
|
| 369 |
+
"API の最大出力トークン": "API 最大输出 token 数",
|
| 370 |
+
"文脈の残りまで": "直到上下文用完",
|
| 371 |
+
"アプリが上限を指定しないときの、1 回の回答の長さの上限です(エージェントがファイルを書くときなどに効きます)。大きくしても使用メモリは増えません。": "应用未指定上限时,单次回答长度的上限(智能体写文件时会用到)。调大不会增加内存占用。",
|
| 372 |
"CPU も使ってプロンプトの読み込みを速くする(消費電力が増えます)": "同时使用 CPU 加快读取提示词(耗电增加)",
|
| 373 |
"長いプロンプトほど効きます。ノート PC ではファンの音が大きくなることがあります。": "提示词越长效果越明显。笔记本电脑的风扇可能会更响。",
|
| 374 |
" + 画像 {0}秒": " + 图像 {0} 秒",
|
onwx/manager.py
CHANGED
|
@@ -457,10 +457,15 @@ class Manager:
|
|
| 457 |
cfg[k] = min(max(cast(float(new[k])), lo), hi)
|
| 458 |
except (TypeError, ValueError):
|
| 459 |
return {"error": T("生成設定の値が不正です")}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
if ctx != cfg["context"]:
|
| 461 |
cfg["context_chosen"] = True # (the old 1024 default moves up; a choice stays)
|
| 462 |
cfg["port"], cfg["context"] = port, ctx
|
| 463 |
-
server_keys = ("lan", "api_key", "port", "context", "think_default", "cpu_glue",
|
| 464 |
"api_sampling", "api_temperature", "api_top_p", "api_top_k", "api_presence")
|
| 465 |
app.save_config(cfg)
|
| 466 |
i18n.current(cfg) # messages switch language right away
|
|
|
|
| 457 |
cfg[k] = min(max(cast(float(new[k])), lo), hi)
|
| 458 |
except (TypeError, ValueError):
|
| 459 |
return {"error": T("生成設定の値が不正です")}
|
| 460 |
+
if "api_max_tokens" in new: # [0.6.1] 0: up to the end of the context
|
| 461 |
+
try:
|
| 462 |
+
cfg["api_max_tokens"] = max(0, int(new["api_max_tokens"]))
|
| 463 |
+
except (TypeError, ValueError):
|
| 464 |
+
return {"error": T("生成設定の値が不正です")}
|
| 465 |
if ctx != cfg["context"]:
|
| 466 |
cfg["context_chosen"] = True # (the old 1024 default moves up; a choice stays)
|
| 467 |
cfg["port"], cfg["context"] = port, ctx
|
| 468 |
+
server_keys = ("lan", "api_key", "port", "context", "think_default", "cpu_glue", "api_max_tokens",
|
| 469 |
"api_sampling", "api_temperature", "api_top_p", "api_top_k", "api_presence")
|
| 470 |
app.save_config(cfg)
|
| 471 |
i18n.current(cfg) # messages switch language right away
|
onwx/npu_pfx.py
CHANGED
|
@@ -279,8 +279,14 @@ class BigPrefill:
|
|
| 279 |
self.CAP = min(caps)
|
| 280 |
kr = max(int(json.load(open(os.path.join(self.attn_dirs(M)[-1], "case.json"))).get("kround", 8)) for M in self.MS)
|
| 281 |
self.ALIGN = self.RA = 4 * kr # (a chunk starts at a multiple of ALIGN; RA: the cache read past it)
|
|
|
|
|
|
|
|
|
|
| 282 |
self.cost = cfg.get("cost", {"1024": 2.5, "2048": 4.3})
|
| 283 |
self.old_tps = float(cfg.get("old_tps", 95.0))
|
|
|
|
|
|
|
|
|
|
| 284 |
self.D = D = dims_of(b)
|
| 285 |
self.H, self.ND, self.NO = D["H"], D["ND"], D.get("NO", 1)
|
| 286 |
self.bz = D.get("model") == "bonsai"
|
|
@@ -467,10 +473,13 @@ class BigPrefill:
|
|
| 467 |
self.dev.sync(q["aux"][0], True)
|
| 468 |
q["aux_T0"] = T0
|
| 469 |
|
| 470 |
-
def plan(self, n):
|
| 471 |
-
"""how many of n tokens the next chunk takes (0: the old programs are faster for all n):
|
| 472 |
-
the measured costs (xdna.json "pfx" "cost": s a chunk of M, "old_tps": the old programs'
|
| 473 |
-
|
|
|
|
|
|
|
|
|
|
| 474 |
memo = {}
|
| 475 |
|
| 476 |
def f(r):
|
|
@@ -478,7 +487,7 @@ class BigPrefill:
|
|
| 478 |
return 0.0, 0
|
| 479 |
if r in memo:
|
| 480 |
return memo[r]
|
| 481 |
-
c, take = r
|
| 482 |
for M in self.MS:
|
| 483 |
v = cost[M] + f(r - M)[0]
|
| 484 |
if v < c:
|
|
|
|
| 279 |
self.CAP = min(caps)
|
| 280 |
kr = max(int(json.load(open(os.path.join(self.attn_dirs(M)[-1], "case.json"))).get("kround", 8)) for M in self.MS)
|
| 281 |
self.ALIGN = self.RA = 4 * kr # (a chunk starts at a multiple of ALIGN; RA: the cache read past it)
|
| 282 |
+
# (pfa5 needs the 256: a chunk at a 128-offset start reads its first rows wrong - KL 0.01-0.2 against the
|
| 283 |
+
# aligned run, 2026-10-15)
|
| 284 |
+
self.tcost = len(self.attn_dirs(max(self.MS))) == 2 # [0.6.1] (pfa5: Qwen3.5 9B / 4B - costs measured)
|
| 285 |
self.cost = cfg.get("cost", {"1024": 2.5, "2048": 4.3})
|
| 286 |
self.old_tps = float(cfg.get("old_tps", 95.0))
|
| 287 |
+
# [0.6.1] the costs grow with the cache extent T: a chunk's attention reads the cache once for the chunk
|
| 288 |
+
# (Max+ 395: a 1024 chunk 2.1-2.5 + 0.029 s per 1K of T), the old programs once a block (Bonsai.old_cost)
|
| 289 |
+
self.chunk_k = float(cfg.get("chunk_k", 0.028)) if self.tcost else 0.0
|
| 290 |
self.D = D = dims_of(b)
|
| 291 |
self.H, self.ND, self.NO = D["H"], D["ND"], D.get("NO", 1)
|
| 292 |
self.bz = D.get("model") == "bonsai"
|
|
|
|
| 473 |
self.dev.sync(q["aux"][0], True)
|
| 474 |
q["aux_T0"] = T0
|
| 475 |
|
| 476 |
+
def plan(self, n, T=0):
|
| 477 |
+
"""how many of n tokens at cache position T the next chunk takes (0: the old programs are faster for all n):
|
| 478 |
+
the cheapest split by the measured costs (xdna.json "pfx" "cost": s a chunk of M, "old_tps": the old programs'
|
| 479 |
+
tokens / s; both grow with T: "old_k", "chunk_k")"""
|
| 480 |
+
kT = T / 1024.0
|
| 481 |
+
cost = {int(k): v + self.chunk_k * int(k) / 1024.0 * kT for k, v in self.cost.items()}
|
| 482 |
+
old = (lambda r: self.b.old_cost(r, T)) if self.tcost else (lambda r: r / self.old_tps)
|
| 483 |
memo = {}
|
| 484 |
|
| 485 |
def f(r):
|
|
|
|
| 487 |
return 0.0, 0
|
| 488 |
if r in memo:
|
| 489 |
return memo[r]
|
| 490 |
+
c, take = old(r), 0
|
| 491 |
for M in self.MS:
|
| 492 |
v = cost[M] + f(r - M)[0]
|
| 493 |
if v < c:
|
onwx/server.py
CHANGED
|
@@ -407,7 +407,12 @@ def gen_args(body):
|
|
| 407 |
raise HTTPException(400, "n > 1 is not supported (one NPU pipeline)")
|
| 408 |
stop = body.get("stop") or []
|
| 409 |
mx = body.get("max_completion_tokens", body.get("max_tokens"))
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
if mx < 1:
|
| 412 |
raise HTTPException(400, "max_tokens must be at least 1")
|
| 413 |
think = (body.get("chat_template_kwargs") or {}).get("enable_thinking")
|
|
|
|
| 407 |
raise HTTPException(400, "n > 1 is not supported (one NPU pipeline)")
|
| 408 |
stop = body.get("stop") or []
|
| 409 |
mx = body.get("max_completion_tokens", body.get("max_tokens"))
|
| 410 |
+
# [0.6.1] a request without max_tokens: the setting (ONWX_MAX_TOKENS; 0 = up to the end of the context - the
|
| 411 |
+
# engine stops there anyway); agents send none and write whole files in one answer
|
| 412 |
+
if mx is None:
|
| 413 |
+
mx = int(os.environ.get("ONWX_MAX_TOKENS", "8192") or 0) or (1 << 30)
|
| 414 |
+
else:
|
| 415 |
+
mx = _num({"max_tokens": mx}, "max_tokens", 512, int)
|
| 416 |
if mx < 1:
|
| 417 |
raise HTTPException(400, "max_tokens must be at least 1")
|
| 418 |
think = (body.get("chat_template_kwargs") or {}).get("enable_thinking")
|
onwx/web/manage.html
CHANGED
|
@@ -240,6 +240,8 @@
|
|
| 240 |
<div class="opt"><span class="k">API キー(任意)</span><input type="password" id="f-api_key" placeholder="未設定なら何を入れても通ります" style="width:260px"></div>
|
| 241 |
<div class="opt"><span class="k">コンテキスト長</span><select id="f-context" style="width:110px"><option>1024</option><option>2048</option><option>3840</option><option>8192</option><option>16384</option><option>32768</option><option>65536</option><option>131072</option><option>262144</option></select> <span id="ctxnote" class="small muted"></span>
|
| 242 |
<span class="desc">会話で使える最大の長さ(モデルの上限まで。長いほどメモリを使い、長い会話ほど遅くなります)。</span></div>
|
|
|
|
|
|
|
| 243 |
<label class="swrow"><span class="switch"><input type="checkbox" id="f-lan"><i></i></span><span>LAN からの接続を許可する<span class="desc">他の PC から API を使えるようにします。API キーの設定をおすすめします。</span></span></label>
|
| 244 |
<label class="swrow"><span class="switch"><input type="checkbox" id="f-cpu_glue"><i></i></span><span>CPU も使ってプロンプトの読み込みを速くする(消費電力が増えます)<span class="desc">長いプロンプトほど効きます。ノート PC ではファンの音が大きくなることがあります。</span></span></label>
|
| 245 |
</div>
|
|
@@ -423,6 +425,7 @@
|
|
| 423 |
$("f-lang").value = c.lang || "auto";
|
| 424 |
for (const k of ["autostart_server", "login_start", "lan", "update_check"]) $("f-" + k).checked = c[k] !== false && !!(c[k] ?? true);
|
| 425 |
$("f-cpu_glue").checked = !!c.cpu_glue; // (off unless chosen)
|
|
|
|
| 426 |
}
|
| 427 |
}
|
| 428 |
function noteOf(m) {
|
|
@@ -535,7 +538,8 @@
|
|
| 535 |
if (await ask(T("{0}\nを探す場所から外しますか?(フォルダやモデルは削除しません)", d))) post("folders", {remove: d});
|
| 536 |
}
|
| 537 |
async function saveSettings() {
|
| 538 |
-
const body = {port: $("f-port").value, context: $("f-context").value, api_key: $("f-api_key").value.trim(), lang: $("f-lang").value
|
|
|
|
| 539 |
for (const k of ["autostart_server", "login_start", "lan", "update_check", "cpu_glue"]) body[k] = $("f-" + k).checked;
|
| 540 |
const oldLang = S.config.lang || "auto";
|
| 541 |
const r = await post("settings", body);
|
|
|
|
| 240 |
<div class="opt"><span class="k">API キー(任意)</span><input type="password" id="f-api_key" placeholder="未設定なら何を入れても通ります" style="width:260px"></div>
|
| 241 |
<div class="opt"><span class="k">コンテキスト長</span><select id="f-context" style="width:110px"><option>1024</option><option>2048</option><option>3840</option><option>8192</option><option>16384</option><option>32768</option><option>65536</option><option>131072</option><option>262144</option></select> <span id="ctxnote" class="small muted"></span>
|
| 242 |
<span class="desc">会話で使える最大の長さ(モデルの上限まで。長いほどメモリを使い、長い会話ほど遅くなります)。</span></div>
|
| 243 |
+
<div class="opt"><span class="k">API の最大出力トークン</span><select id="f-api_max_tokens" style="width:150px"><option value="2048">2048</option><option value="4096">4096</option><option value="8192">8192</option><option value="16384">16384</option><option value="32768">32768</option><option value="0">文脈の残りまで</option></select>
|
| 244 |
+
<span class="desc">アプリが上限を指定しないときの、1 回の回答の長さの上限です(エージェントがファイルを書くときなどに効きます)。大きくしても使用メモリは増えません。</span></div>
|
| 245 |
<label class="swrow"><span class="switch"><input type="checkbox" id="f-lan"><i></i></span><span>LAN からの接続を許可する<span class="desc">他の PC から API を使えるようにします。API キーの設定をおすすめします。</span></span></label>
|
| 246 |
<label class="swrow"><span class="switch"><input type="checkbox" id="f-cpu_glue"><i></i></span><span>CPU も使ってプロンプトの読み込みを速くする(消費電力が増えます)<span class="desc">長いプロンプトほど効きます。ノート PC ではファンの音が大きくなることがあります。</span></span></label>
|
| 247 |
</div>
|
|
|
|
| 425 |
$("f-lang").value = c.lang || "auto";
|
| 426 |
for (const k of ["autostart_server", "login_start", "lan", "update_check"]) $("f-" + k).checked = c[k] !== false && !!(c[k] ?? true);
|
| 427 |
$("f-cpu_glue").checked = !!c.cpu_glue; // (off unless chosen)
|
| 428 |
+
$("f-api_max_tokens").value = String(c.api_max_tokens ?? 8192);
|
| 429 |
}
|
| 430 |
}
|
| 431 |
function noteOf(m) {
|
|
|
|
| 538 |
if (await ask(T("{0}\nを探す場所から外しますか?(フォルダやモデルは削除しません)", d))) post("folders", {remove: d});
|
| 539 |
}
|
| 540 |
async function saveSettings() {
|
| 541 |
+
const body = {port: $("f-port").value, context: $("f-context").value, api_key: $("f-api_key").value.trim(), lang: $("f-lang").value,
|
| 542 |
+
api_max_tokens: $("f-api_max_tokens").value};
|
| 543 |
for (const k of ["autostart_server", "login_start", "lan", "update_check", "cpu_glue"]) body[k] = $("f-" + k).checked;
|
| 544 |
const oldLang = S.config.lang || "auto";
|
| 545 |
const r = await post("settings", body);
|
onwx/xdna.py
CHANGED
|
@@ -52,17 +52,39 @@ def is_xdna_model(d):
|
|
| 52 |
|
| 53 |
# ---------------------------------------------------------------- devices
|
| 54 |
PYXRT_DIRS = ["{XILINX_XRT}/python", "/opt/xilinx/xrt/python", "/usr/lib/python3/dist-packages",
|
| 55 |
-
"/usr/local/lib/python3/dist-packages", "/usr/lib/x86_64-linux-gnu/xrt/python", "/usr/local/xrt/python"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
def pyxrt_dirs():
|
| 59 |
-
"""the folders that may hold XRT's Python bindings (its packages put them beside the library, off sys.path)
|
|
|
|
|
|
|
| 60 |
xrt = os.environ.get("XILINX_XRT", "/opt/xilinx/xrt")
|
|
|
|
|
|
|
| 61 |
out = []
|
| 62 |
-
for
|
| 63 |
-
d
|
| 64 |
-
|
| 65 |
-
|
| 66 |
return out
|
| 67 |
|
| 68 |
|
|
@@ -109,10 +131,18 @@ def import_pyxrt():
|
|
| 109 |
f"XRT's Python bindings (pyxrt) were found ({', '.join(found)}) but they are built for Python {want}, and "
|
| 110 |
f"onwx runs on Python {here} ({sys.executable}). Install XRT built for Python {here} (AMD xdna-driver: build "
|
| 111 |
f"XRT on this PC), or run onwx with Python {want}.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
raise RuntimeError(
|
| 113 |
-
"XRT
|
| 114 |
-
|
| 115 |
-
|
|
|
|
| 116 |
|
| 117 |
|
| 118 |
PF128_CAP = int(os.environ.get("ONWX_PF128_CAP", "62208")) # [0.5] the 128-token prompt programs' last working cache extent
|
|
@@ -438,9 +468,9 @@ class Bonsai:
|
|
| 438 |
res = []
|
| 439 |
keep = (lambda r: res.append(np.asarray(r, np.float32).reshape(-1, self.hid))) if rows else (lambda r: None)
|
| 440 |
bp = (self._big() if embeds is None and mpos is None and self.rdelta == 0 and not self._big_off else None)
|
| 441 |
-
while bp is not None and len(toks) - i >= bp.MIN:
|
| 442 |
-
a = (-self.pos) % bp.ALIGN #
|
| 443 |
-
m = bp.plan(len(toks) - i - a)
|
| 444 |
if m == 0 or self.pos + a + bp.chunk_for(m) + max(32, bp.RA) > self.tmax + self.kv_slack: # programs, by the measured costs)
|
| 445 |
break
|
| 446 |
if self.pos + a + bp.chunk_for(m) > bp.CAP: # (past the chunk programs' cache extent: the old programs)
|
|
@@ -476,7 +506,7 @@ class Bonsai:
|
|
| 476 |
None if mpos is None else mpos[:, i:i + k])
|
| 477 |
keep(p128.rows)
|
| 478 |
i += k
|
| 479 |
-
while self.pf is not None and len(toks) - i >= 2 and self.pos + self.pf.NT <= self.tmax:
|
| 480 |
if self.pair and self.pos % self.kq: # (pair-major: a batch starts at an even position; q8: at a quad)
|
| 481 |
self.step(toks[i], want_logits=False, emb=e(i), mpos=mp(i))
|
| 482 |
if rows:
|
|
@@ -502,6 +532,43 @@ class Bonsai:
|
|
| 502 |
return None
|
| 503 |
return self.head(h)
|
| 504 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
def _big(self):
|
| 506 |
"""[0.4] the chunked NPU prefill (None: not in this package / disabled / failed to start)"""
|
| 507 |
if self.bigpf is None and self._big_ok:
|
|
@@ -1120,6 +1187,12 @@ class XdnaModel:
|
|
| 1120 |
lg[self.banned] = -np.inf
|
| 1121 |
return lg[None]
|
| 1122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1123 |
def reset(self):
|
| 1124 |
self.rt.reset()
|
| 1125 |
self._mtp_reset()
|
|
|
|
| 52 |
|
| 53 |
# ---------------------------------------------------------------- devices
|
| 54 |
PYXRT_DIRS = ["{XILINX_XRT}/python", "/opt/xilinx/xrt/python", "/usr/lib/python3/dist-packages",
|
| 55 |
+
"/usr/local/lib/python3/dist-packages", "/usr/lib/x86_64-linux-gnu/xrt/python", "/usr/local/xrt/python",
|
| 56 |
+
# [0.6.1] versioned Python folders and other XRT roots (distribution packages differ)
|
| 57 |
+
"/usr/lib/python3*/dist-packages", "/usr/lib/python3*/site-packages", "/usr/local/lib/python3*/dist-packages",
|
| 58 |
+
"/usr/local/lib/python3*/site-packages", "/usr/lib64/python3*/site-packages", "/opt/xilinx/xrt/lib/python*",
|
| 59 |
+
"/usr/lib/xrt/python", "/usr/share/xrt/python", "/opt/amd/xrt/python"]
|
| 60 |
+
XRT_LIBS = ["{XILINX_XRT}/lib", "/opt/xilinx/xrt/lib", "/usr/lib/x86_64-linux-gnu", "/usr/lib64", "/usr/lib",
|
| 61 |
+
"/usr/local/lib", "/opt/amd/xrt/lib"]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def xrt_libs():
|
| 65 |
+
"""[0.6.1] where XRT's runtime library (libxrt_coreutil) is - XRT installed at all?"""
|
| 66 |
+
import glob
|
| 67 |
+
xrt = os.environ.get("XILINX_XRT", "/opt/xilinx/xrt")
|
| 68 |
+
out = []
|
| 69 |
+
for d in XRT_LIBS:
|
| 70 |
+
for f in sorted(glob.glob(os.path.join(d.format(XILINX_XRT=xrt), "libxrt_coreutil.so*"))):
|
| 71 |
+
if f not in out:
|
| 72 |
+
out.append(f)
|
| 73 |
+
return out
|
| 74 |
|
| 75 |
|
| 76 |
def pyxrt_dirs():
|
| 77 |
+
"""the folders that may hold XRT's Python bindings (its packages put them beside the library, off sys.path):
|
| 78 |
+
the known ones, and <root>/python beside every XRT library found"""
|
| 79 |
+
import glob
|
| 80 |
xrt = os.environ.get("XILINX_XRT", "/opt/xilinx/xrt")
|
| 81 |
+
cands = [d.format(XILINX_XRT=xrt) for d in PYXRT_DIRS]
|
| 82 |
+
cands += [os.path.join(os.path.dirname(os.path.dirname(f)), "python") for f in xrt_libs()]
|
| 83 |
out = []
|
| 84 |
+
for c in cands:
|
| 85 |
+
for d in (sorted(glob.glob(c)) if "*" in c else [c]):
|
| 86 |
+
if d not in out and os.path.isdir(d):
|
| 87 |
+
out.append(d)
|
| 88 |
return out
|
| 89 |
|
| 90 |
|
|
|
|
| 131 |
f"XRT's Python bindings (pyxrt) were found ({', '.join(found)}) but they are built for Python {want}, and "
|
| 132 |
f"onwx runs on Python {here} ({sys.executable}). Install XRT built for Python {here} (AMD xdna-driver: build "
|
| 133 |
f"XRT on this PC), or run onwx with Python {want}.")
|
| 134 |
+
libs = xrt_libs()
|
| 135 |
+
if not libs: # [0.6.1] say which of the two it is
|
| 136 |
+
raise RuntimeError(
|
| 137 |
+
"XRT (AMD's NPU runtime) is not installed - the amdxdna kernel driver alone is not enough. Install XRT and "
|
| 138 |
+
"its NPU plugin from AMD's xdna-driver (https://github.com/amd/xdna-driver); that XRT has the Python "
|
| 139 |
+
"bindings onwx needs (/opt/xilinx/xrt/python/pyxrt*.so). Searched for libxrt_coreutil.so in: "
|
| 140 |
+
f"{', '.join(d.format(XILINX_XRT=os.environ.get('XILINX_XRT', '/opt/xilinx/xrt')) for d in XRT_LIBS)}.")
|
| 141 |
raise RuntimeError(
|
| 142 |
+
f"XRT is installed ({', '.join(libs)}) but its Python bindings (pyxrt) were not found - distribution packages "
|
| 143 |
+
"may leave them out. Install XRT built from AMD's xdna-driver (https://github.com/amd/xdna-driver; it puts "
|
| 144 |
+
"them in /opt/xilinx/xrt/python), or set XILINX_XRT to an XRT folder with python/pyxrt*.so. Searched: "
|
| 145 |
+
f"{', '.join(pyxrt_dirs()) or ', '.join(PYXRT_DIRS)}.")
|
| 146 |
|
| 147 |
|
| 148 |
PF128_CAP = int(os.environ.get("ONWX_PF128_CAP", "62208")) # [0.5] the 128-token prompt programs' last working cache extent
|
|
|
|
| 468 |
res = []
|
| 469 |
keep = (lambda r: res.append(np.asarray(r, np.float32).reshape(-1, self.hid))) if rows else (lambda r: None)
|
| 470 |
bp = (self._big() if embeds is None and mpos is None and self.rdelta == 0 and not self._big_off else None)
|
| 471 |
+
while bp is not None and len(toks) - i >= (32 if bp.tcost else bp.MIN): # ([0.6.1] short ones too: deep
|
| 472 |
+
a = (-self.pos) % bp.ALIGN # in a long cache a padded chunk beats the old programs; plan decides)
|
| 473 |
+
m = bp.plan(len(toks) - i - a, self.pos + a) # (the cheapest split; a short tail: a padded chunk or the old
|
| 474 |
if m == 0 or self.pos + a + bp.chunk_for(m) + max(32, bp.RA) > self.tmax + self.kv_slack: # programs, by the measured costs)
|
| 475 |
break
|
| 476 |
if self.pos + a + bp.chunk_for(m) > bp.CAP: # (past the chunk programs' cache extent: the old programs)
|
|
|
|
| 506 |
None if mpos is None else mpos[:, i:i + k])
|
| 507 |
keep(p128.rows)
|
| 508 |
i += k
|
| 509 |
+
while self.pf is not None and len(toks) - i >= max(2, self._few()) and self.pos + self.pf.NT <= self.tmax:
|
| 510 |
if self.pair and self.pos % self.kq: # (pair-major: a batch starts at an even position; q8: at a quad)
|
| 511 |
self.step(toks[i], want_logits=False, emb=e(i), mpos=mp(i))
|
| 512 |
if rows:
|
|
|
|
| 532 |
return None
|
| 533 |
return self.head(h)
|
| 534 |
|
| 535 |
+
def _old_k(self):
|
| 536 |
+
"""[0.6.1] the old programs' costs at the cache position, s: (a 128-token block or None, a 32-token block, a
|
| 537 |
+
single step) - each reads the whole cache once (Max+ 395, s + s per 1K of cache: Qwen3.5 9B 0.85 + 0.125,
|
| 538 |
+
0.33 + 0.067, 0.083 + 0.00075; the 4B (no 128s) 0.25 + 0.067, 0.05 + 0.0008; xdna.json "old_cost" overrides);
|
| 539 |
+
None: not measured for this model (the old rules)"""
|
| 540 |
+
p128 = getattr(self, "pf128", None) is not None
|
| 541 |
+
c = getattr(self, "_oc", False)
|
| 542 |
+
if c is False: # (once)
|
| 543 |
+
c = self.man.get("old_cost")
|
| 544 |
+
if c is None and os.path.isdir(os.path.join(self.d, "plans", "pfx", "m1024", "pfa5r1")):
|
| 545 |
+
c = [0.85, 0.125, 0.33, 0.067, 0.083, 0.00075] if p128 else [None, None, 0.25, 0.067, 0.05, 0.0008]
|
| 546 |
+
self._oc = c
|
| 547 |
+
if c is None:
|
| 548 |
+
return None
|
| 549 |
+
kT = self.pos / 1024.0
|
| 550 |
+
return (c[0] + c[1] * kT if p128 and c[0] is not None else None, c[2] + c[3] * kT, c[4] + c[5] * kT)
|
| 551 |
+
|
| 552 |
+
def old_cost(self, r, T):
|
| 553 |
+
"""[0.6.1] s for r tokens on the old programs from cache position T (BigPrefill.plan)"""
|
| 554 |
+
pos, self.pos = self.pos, T
|
| 555 |
+
try:
|
| 556 |
+
k = self._old_k()
|
| 557 |
+
finally:
|
| 558 |
+
self.pos = pos
|
| 559 |
+
if k is None:
|
| 560 |
+
return r / 150.0
|
| 561 |
+
c128, c32, st = k
|
| 562 |
+
t = 0.0
|
| 563 |
+
if c128 is not None:
|
| 564 |
+
t, r = (r // 128) * c128, r % 128
|
| 565 |
+
return t + (r // 32) * c32 + min(c32, (r % 32) * st)
|
| 566 |
+
|
| 567 |
+
def _few(self):
|
| 568 |
+
"""[0.6.1] below how many tokens single steps beat one dispatch of the 32-token prompt program here"""
|
| 569 |
+
k = self._old_k()
|
| 570 |
+
return 2 if k is None else int(k[1] / k[2])
|
| 571 |
+
|
| 572 |
def _big(self):
|
| 573 |
"""[0.4] the chunked NPU prefill (None: not in this package / disabled / failed to start)"""
|
| 574 |
if self.bigpf is None and self._big_ok:
|
|
|
|
| 1187 |
lg[self.banned] = -np.inf
|
| 1188 |
return lg[None]
|
| 1189 |
|
| 1190 |
+
def chunk_align(self):
|
| 1191 |
+
"""[0.6.1] where chunked prompt reading can start (a multiple of this) when its costs are measured (Qwen3.5 9B /
|
| 1192 |
+
4B), else 0 (chat then keeps its checkpoints where the conversation ended)"""
|
| 1193 |
+
bp = self.rt._big()
|
| 1194 |
+
return bp.ALIGN if bp is not None and getattr(bp, "tcost", False) else 0
|
| 1195 |
+
|
| 1196 |
def reset(self):
|
| 1197 |
self.rt.reset()
|
| 1198 |
self._mtp_reset()
|
pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "onwx"
|
| 7 |
-
version = "0.6.
|
| 8 |
description = "LLMs entirely on the AMD Ryzen AI NPU (XDNA2)"
|
| 9 |
readme = "README.md"
|
| 10 |
license = {text = "Apache-2.0"}
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "onwx"
|
| 7 |
+
version = "0.6.1"
|
| 8 |
description = "LLMs entirely on the AMD Ryzen AI NPU (XDNA2)"
|
| 9 |
readme = "README.md"
|
| 10 |
license = {text = "Apache-2.0"}
|