ryugyosoft commited on
Commit
f270ec5
·
verified ·
1 Parent(s): 88bad6e

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 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)と XRT(Python バインディング pyxrt 付き)
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) and XRT with its Python bindings (pyxrt)
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.0"
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
- ck, reuse = self.checkpoint, 0
432
- if ck is not None and len(ck[0][0]) <= B and key(len(ck[0][0])) == ck[0]:
433
- reuse = len(ck[0][0])
434
- self.model.restore(ck[1])
 
 
 
 
 
 
 
435
  else:
436
  self.model.reset()
437
- self.checkpoint = None # (its KV rows are overwritten now: a prefill failing part-way would
438
- # leave a checkpoint over another conversation's rows)
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 B > reuse:
459
- self._prefill(ids, reuse, B, need_logits=False, embeds=embeds, pos=pos)
460
- self.checkpoint = (key(B), self.model.snapshot())
461
- lg = self._prefill(ids, B, n, embeds=embeds, pos=pos)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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): the cheapest split by
472
- the measured costs (xdna.json "pfx" "cost": s a chunk of M, "old_tps": the old programs' tokens / s)"""
473
- cost = {int(k): v for k, v in self.cost.items()}
 
 
 
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 / self.old_tps, 0
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
- mx = 512 if mx is None else _num({"max_tokens": mx}, "max_tokens", 512, int)
 
 
 
 
 
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 d in PYXRT_DIRS:
63
- d = d.format(XILINX_XRT=xrt)
64
- if d not in out and os.path.isdir(d):
65
- out.append(d)
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's Python bindings (pyxrt) are missing: install the AMD NPU driver (amdxdna) and XRT with its Python "
114
- f"bindings (usually /opt/xilinx/xrt/python), or set XILINX_XRT to the XRT folder. Searched: "
115
- f"{', '.join(PYXRT_DIRS).format(XILINX_XRT=os.environ.get('XILINX_XRT', '/opt/xilinx/xrt'))}.")
 
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 # (a chunk starts at a multiple of ALIGN (32 / 256): the rest first)
443
- m = bp.plan(len(toks) - i - a) # (the cheapest split; a short tail: a padded chunk or the old
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.0"
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"}