coderouter-cli 2.9.4__py3-none-any.whl → 2.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coderouter/cli.py +125 -0
- coderouter/config/schemas.py +144 -10
- coderouter/ingress/launcher_routes.py +289 -22
- coderouter/launcher_devices.py +188 -4
- coderouter/launcher_speculative.py +5 -1
- coderouter/launcher_swap.py +11 -2
- coderouter/vscode_init.py +575 -0
- {coderouter_cli-2.9.4.dist-info → coderouter_cli-2.11.0.dist-info}/METADATA +5 -1
- {coderouter_cli-2.9.4.dist-info → coderouter_cli-2.11.0.dist-info}/RECORD +12 -11
- {coderouter_cli-2.9.4.dist-info → coderouter_cli-2.11.0.dist-info}/WHEEL +0 -0
- {coderouter_cli-2.9.4.dist-info → coderouter_cli-2.11.0.dist-info}/entry_points.txt +0 -0
- {coderouter_cli-2.9.4.dist-info → coderouter_cli-2.11.0.dist-info}/licenses/LICENSE +0 -0
coderouter/launcher_devices.py
CHANGED
|
@@ -23,13 +23,14 @@ import socket
|
|
|
23
23
|
import subprocess # controlled: fixed argv, no shell
|
|
24
24
|
import threading
|
|
25
25
|
import time
|
|
26
|
-
from collections.abc import Callable, Sequence
|
|
26
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
27
27
|
from dataclasses import dataclass, field
|
|
28
28
|
from enum import StrEnum
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
from typing import Any
|
|
31
31
|
|
|
32
32
|
__all__ = [
|
|
33
|
+
"KNOWN_BASE_BACKENDS",
|
|
33
34
|
"DeviceProbe",
|
|
34
35
|
"DeviceSelection",
|
|
35
36
|
"LlamaDevice",
|
|
@@ -37,21 +38,140 @@ __all__ = [
|
|
|
37
38
|
"SweepState",
|
|
38
39
|
"SweepStep",
|
|
39
40
|
"backend_of",
|
|
41
|
+
"base_backend",
|
|
40
42
|
"build_auto_sweep_configs",
|
|
43
|
+
"build_cross_variant_sweep_configs",
|
|
41
44
|
"build_sweep_steps",
|
|
42
45
|
"detect_llama_devices",
|
|
43
46
|
"group_by_backend",
|
|
44
47
|
"is_port_free",
|
|
48
|
+
"is_valid_backend_name",
|
|
49
|
+
"is_variant",
|
|
45
50
|
"load_latest_results",
|
|
46
51
|
"parse_list_devices",
|
|
47
52
|
"render_bench_command",
|
|
48
53
|
"reset_device_cache",
|
|
54
|
+
"resolve_option_profiles",
|
|
49
55
|
"selectable_devices",
|
|
50
56
|
"suggest_tensor_split",
|
|
51
57
|
"summarize_results",
|
|
58
|
+
"unknown_device_ids",
|
|
59
|
+
"variant_of",
|
|
52
60
|
]
|
|
53
61
|
|
|
54
62
|
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# 2.0 バックエンド名の正規化 (基底名 + バリアント)
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
# llama.cpp を CUDA / Vulkan / ROCm 向けに個別ビルドしている環境では、同じ
|
|
68
|
+
# "llama.cpp" でもビルドごとに実行ファイルも列挙されるデバイスも違う。これを
|
|
69
|
+
# 設定・UI・API の全面で扱うため、バックエンド名に "<基底名>-<バリアント>"
|
|
70
|
+
# 形式を認める:
|
|
71
|
+
#
|
|
72
|
+
# llama.cpp 基底名 (素のビルド / PATH の llama-server)
|
|
73
|
+
# llama.cpp-cuda build-cuda/bin/llama-server
|
|
74
|
+
# llama.cpp-vulkan build-vulkan/bin/llama-server
|
|
75
|
+
# llama.cpp-rocm build-rocm/bin/llama-server
|
|
76
|
+
#
|
|
77
|
+
# ★重要: バックエンド名は実行ファイルの選択だけでなく「どのフラグ体系か」
|
|
78
|
+
# 「readiness を /health で見るか」「MTP を使えるか」といった挙動の分岐にも
|
|
79
|
+
# 使われている。それらの分岐は必ず :func:`base_backend` を通して判定すること。
|
|
80
|
+
# 素の文字列比較のままバリアント名が届くと、モデル上書きガードが無効化され
|
|
81
|
+
# たり readiness が TCP connect に退行したりと、例外を出さずに壊れる
|
|
82
|
+
# (docs/designs/launcher-multi-build.md §4 に全分岐の一覧がある)。
|
|
83
|
+
KNOWN_BASE_BACKENDS: tuple[str, ...] = ("llama.cpp", "vllm", "mlx")
|
|
84
|
+
|
|
85
|
+
# バリアント部分に許す文字。プロバイダ名・プロファイル名・ログに混ざっても
|
|
86
|
+
# 壊れない集合に限定し、パス区切りやシェルメタ文字を通さない。
|
|
87
|
+
_VARIANT_RE = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def base_backend(name: str) -> str:
|
|
91
|
+
"""``"llama.cpp-cuda"`` → ``"llama.cpp"``。基底名そのものはそのまま返す。
|
|
92
|
+
|
|
93
|
+
既知基底名との**最長一致**で判定する。``name.split("-", 1)[0]`` にしない
|
|
94
|
+
のは意図的で、基底名自体がハイフンを含むようになった日に静かに誤判定する
|
|
95
|
+
のを防ぐため。既知基底名で始まらない名前は加工せずそのまま返し、呼び出し
|
|
96
|
+
側の既存 "Unknown backend" 経路に委ねる。
|
|
97
|
+
"""
|
|
98
|
+
best = ""
|
|
99
|
+
for base in KNOWN_BASE_BACKENDS:
|
|
100
|
+
if name == base:
|
|
101
|
+
return base
|
|
102
|
+
if name.startswith(base + "-") and len(base) > len(best):
|
|
103
|
+
best = base
|
|
104
|
+
return best or name
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def variant_of(name: str) -> str | None:
|
|
108
|
+
"""``"llama.cpp-cuda"`` → ``"cuda"``。基底名そのものなら ``None``。
|
|
109
|
+
|
|
110
|
+
``"llama.cpp-"`` のようにバリアント部が空の場合も ``None`` を返す
|
|
111
|
+
(妥当性判定は :func:`is_valid_backend_name` が担う)。
|
|
112
|
+
"""
|
|
113
|
+
base = base_backend(name)
|
|
114
|
+
if base == name or not name.startswith(base + "-"):
|
|
115
|
+
return None
|
|
116
|
+
return name[len(base) + 1 :] or None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def is_variant(name: str) -> bool:
|
|
120
|
+
"""``name`` が ``<既知基底名>-<バリアント>`` 形式かどうか。"""
|
|
121
|
+
return variant_of(name) is not None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def is_valid_backend_name(name: str) -> bool:
|
|
125
|
+
"""既知基底名そのもの、または既知基底名 + 妥当なバリアントかどうか。
|
|
126
|
+
|
|
127
|
+
``launcher.backends`` のキー検証に使う。``"llamacpp"`` (typo) や
|
|
128
|
+
``"llama.cpp-CUDA"`` (大文字) / ``"llama.cpp-"`` (空) は False。
|
|
129
|
+
"""
|
|
130
|
+
if name in KNOWN_BASE_BACKENDS:
|
|
131
|
+
return True
|
|
132
|
+
variant = variant_of(name)
|
|
133
|
+
return variant is not None and _VARIANT_RE.match(variant) is not None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def resolve_option_profiles[P](
|
|
137
|
+
profiles_by_backend: Mapping[str, Sequence[P]], backend: str
|
|
138
|
+
) -> list[P]:
|
|
139
|
+
"""バックエンド(バリアント可)に適用する option_profiles を解決する。
|
|
140
|
+
|
|
141
|
+
基底名のリスト(継承)を先に、バリアント固有のリストを後に連結する。
|
|
142
|
+
``name`` が衝突したらバリアント固有が**同じ位置で置き換える** —— 並び順を
|
|
143
|
+
安定させ、末尾に重複を作らないため。``backend`` が基底名そのものなら
|
|
144
|
+
``profiles_by_backend.get(backend, [])`` と完全に同一の結果になる
|
|
145
|
+
(後方互換)。
|
|
146
|
+
|
|
147
|
+
要素は ``.name`` 属性を持つ任意の型でよい。pydantic の
|
|
148
|
+
``LauncherOptionProfile`` と GUI の dataclass ``OptionProfile`` の双方から
|
|
149
|
+
使うため、この層は pydantic に依存しない (モジュール冒頭の方針どおり)。
|
|
150
|
+
"""
|
|
151
|
+
base = base_backend(backend)
|
|
152
|
+
inherited = list(profiles_by_backend.get(base, []))
|
|
153
|
+
if backend == base:
|
|
154
|
+
return inherited
|
|
155
|
+
own = list(profiles_by_backend.get(backend, []))
|
|
156
|
+
if not own:
|
|
157
|
+
return inherited
|
|
158
|
+
|
|
159
|
+
by_name = {p.name: p for p in own} # type: ignore[attr-defined]
|
|
160
|
+
merged: list[P] = []
|
|
161
|
+
replaced: set[str] = set()
|
|
162
|
+
for p in inherited:
|
|
163
|
+
name = p.name # type: ignore[attr-defined]
|
|
164
|
+
if name in by_name:
|
|
165
|
+
merged.append(by_name[name]) # 継承分と同じ位置で差し替え
|
|
166
|
+
replaced.add(name)
|
|
167
|
+
else:
|
|
168
|
+
merged.append(p)
|
|
169
|
+
merged.extend(
|
|
170
|
+
p for p in own if p.name not in replaced # type: ignore[attr-defined]
|
|
171
|
+
)
|
|
172
|
+
return merged
|
|
173
|
+
|
|
174
|
+
|
|
55
175
|
# ---------------------------------------------------------------------------
|
|
56
176
|
# 2.1 デバイス検出
|
|
57
177
|
# ---------------------------------------------------------------------------
|
|
@@ -249,6 +369,24 @@ class DeviceSelection:
|
|
|
249
369
|
return args
|
|
250
370
|
|
|
251
371
|
|
|
372
|
+
def unknown_device_ids(device_ids: Sequence[str], probe: DeviceProbe) -> list[str]:
|
|
373
|
+
"""``device_ids`` のうち ``probe`` に存在しないものを返す(空なら健全)。
|
|
374
|
+
|
|
375
|
+
バックエンドバリアントを切り替えるとデバイス ID の名前空間も変わる
|
|
376
|
+
(``CUDA0`` と ``Vulkan0`` は同じ GPU を指さない)。CUDA ビルドで
|
|
377
|
+
``CUDA0`` を選んだまま Vulkan ビルドで起動すると ``--device CUDA0`` が
|
|
378
|
+
Vulkan ビルドに渡って起動失敗するので、spawn 前に弾くための判定。
|
|
379
|
+
|
|
380
|
+
``probe.ok`` が False (``--list-devices`` 自体が失敗) のときは **常に空
|
|
381
|
+
リスト** を返す。デバイスを列挙できない環境で機能を殺さないための
|
|
382
|
+
best-effort 原則 (``hardware.py`` の 5-deps 不変則と同じ姿勢)。
|
|
383
|
+
"""
|
|
384
|
+
if not probe.ok:
|
|
385
|
+
return []
|
|
386
|
+
known = {d.id for d in probe.devices}
|
|
387
|
+
return [d for d in device_ids if d not in known]
|
|
388
|
+
|
|
389
|
+
|
|
252
390
|
# ---------------------------------------------------------------------------
|
|
253
391
|
# 2.3 tensor-split 自動提案
|
|
254
392
|
# ---------------------------------------------------------------------------
|
|
@@ -357,6 +495,36 @@ def build_auto_sweep_configs(
|
|
|
357
495
|
return configs
|
|
358
496
|
|
|
359
497
|
|
|
498
|
+
def build_cross_variant_sweep_configs(
|
|
499
|
+
probes: Sequence[tuple[str, Sequence[LlamaDevice]]],
|
|
500
|
+
*,
|
|
501
|
+
by: str = "total",
|
|
502
|
+
) -> list[tuple[str, str, DeviceSelection]]:
|
|
503
|
+
"""複数ビルド(バリアント)を横断するスイープ構成を生成する。
|
|
504
|
+
|
|
505
|
+
``probes`` は ``[(backend_name, devices), ...]`` —— 各バリアントを
|
|
506
|
+
``--list-devices`` でプローブした結果。返すのは
|
|
507
|
+
``[(label, backend_name, DeviceSelection), ...]`` で、ラベルには
|
|
508
|
+
バリアント名を前置する (``"cuda / CUDA0 単体"``)。``{config}`` プレース
|
|
509
|
+
ホルダ経由でベンチ結果 JSON もビルド別に見分けられる。
|
|
510
|
+
|
|
511
|
+
``build_auto_sweep_configs`` を各ビルドに対して呼ぶだけなので、
|
|
512
|
+
「バックエンド跨ぎの混成構成は作らない」という既存規則はビルド内でも
|
|
513
|
+
そのまま維持される。ビルド間の混成 (CUDA ビルドの CUDA0 と Vulkan
|
|
514
|
+
ビルドの Vulkan2 を同時に、など) は **原理的に不可能** ——1 プロセスは
|
|
515
|
+
1 つの実行ファイルでしか動かないので、ここでも作らない。
|
|
516
|
+
|
|
517
|
+
これで「同一モデルを CUDA ビルドと Vulkan ビルドで順に起動してベンチし、
|
|
518
|
+
どちらが速いか比較する」が 1 回のスイープで回る。
|
|
519
|
+
"""
|
|
520
|
+
out: list[tuple[str, str, DeviceSelection]] = []
|
|
521
|
+
for backend, devices in probes:
|
|
522
|
+
prefix = variant_of(backend) or backend
|
|
523
|
+
for label, selection in build_auto_sweep_configs(devices, by=by):
|
|
524
|
+
out.append((f"{prefix} / {label}", backend, selection))
|
|
525
|
+
return out
|
|
526
|
+
|
|
527
|
+
|
|
360
528
|
# ---------------------------------------------------------------------------
|
|
361
529
|
# 2.4 スイープ計画・状態
|
|
362
530
|
# ---------------------------------------------------------------------------
|
|
@@ -382,6 +550,10 @@ class SweepStep:
|
|
|
382
550
|
error: str | None = None
|
|
383
551
|
started_at: float = 0.0
|
|
384
552
|
ended_at: float = 0.0
|
|
553
|
+
# ステップ個別のバックエンド(バリアント横断スイープ用)。None なら
|
|
554
|
+
# ``SweepPlan.backend`` を使う=従来と完全に同じ argv。末尾の任意
|
|
555
|
+
# フィールドなので既存の位置指定コンストラクタ呼び出しは無影響。
|
|
556
|
+
backend: str | None = None
|
|
385
557
|
|
|
386
558
|
def as_dict(self) -> dict[str, Any]:
|
|
387
559
|
return {
|
|
@@ -393,6 +565,7 @@ class SweepStep:
|
|
|
393
565
|
"results_path": self.results_path,
|
|
394
566
|
"summary": self.summary,
|
|
395
567
|
"error": self.error,
|
|
568
|
+
"backend": self.backend,
|
|
396
569
|
}
|
|
397
570
|
|
|
398
571
|
|
|
@@ -400,7 +573,7 @@ class SweepStep:
|
|
|
400
573
|
class SweepPlan:
|
|
401
574
|
steps: list[SweepStep]
|
|
402
575
|
model_path: str
|
|
403
|
-
backend: str #
|
|
576
|
+
backend: str # プラン既定。step.backend が None のとき使われる
|
|
404
577
|
port: int
|
|
405
578
|
bench_cmd_template: str
|
|
406
579
|
results_dir: str | None = None
|
|
@@ -409,9 +582,20 @@ class SweepPlan:
|
|
|
409
582
|
|
|
410
583
|
|
|
411
584
|
def build_sweep_steps(
|
|
412
|
-
labeled: Sequence[tuple[str, DeviceSelection]]
|
|
585
|
+
labeled: Sequence[tuple[str, DeviceSelection]]
|
|
586
|
+
| Sequence[tuple[str, DeviceSelection, str | None]],
|
|
413
587
|
) -> list[SweepStep]:
|
|
414
|
-
|
|
588
|
+
"""``(label, selection)`` または ``(label, selection, backend)`` から生成。
|
|
589
|
+
|
|
590
|
+
3 要素形はバリアント横断スイープ用 (ステップごとに実行ファイルが違う)。
|
|
591
|
+
2 要素形は従来どおり ``backend=None`` になり、プラン既定で動く。
|
|
592
|
+
"""
|
|
593
|
+
steps: list[SweepStep] = []
|
|
594
|
+
for item in labeled:
|
|
595
|
+
lbl, sel = item[0], item[1]
|
|
596
|
+
backend = item[2] if len(item) == 3 else None
|
|
597
|
+
steps.append(SweepStep(label=lbl, selection=sel, backend=backend))
|
|
598
|
+
return steps
|
|
415
599
|
|
|
416
600
|
|
|
417
601
|
# ---------------------------------------------------------------------------
|
|
@@ -45,6 +45,7 @@ import re
|
|
|
45
45
|
from pathlib import Path
|
|
46
46
|
|
|
47
47
|
from coderouter.gguf_introspect import try_read_gguf_metadata
|
|
48
|
+
from coderouter.launcher_devices import base_backend
|
|
48
49
|
|
|
49
50
|
__all__ = ["find_draft_companion", "resolve_speculative"]
|
|
50
51
|
|
|
@@ -248,7 +249,10 @@ def resolve_speculative(
|
|
|
248
249
|
|
|
249
250
|
# 1. Non-llama.cpp backends do not support speculation. Reject explicit
|
|
250
251
|
# use; otherwise (auto default, no draft) stay a silent no-op.
|
|
251
|
-
|
|
252
|
+
# ``backend`` may be a variant (``llama.cpp-cuda``) — every llama.cpp
|
|
253
|
+
# build supports speculation, so compare the base name. A plain ``!=``
|
|
254
|
+
# here would silently drop the spec flags for variants.
|
|
255
|
+
if base_backend(backend) != _LLAMA_CPP:
|
|
252
256
|
if draft_model_path or mtp_mode == "off":
|
|
253
257
|
raise ValueError(
|
|
254
258
|
"draft_model_path / mtp_mode are only supported for llama.cpp"
|
coderouter/launcher_swap.py
CHANGED
|
@@ -50,6 +50,7 @@ from coderouter.ingress.launcher_routes import (
|
|
|
50
50
|
spawn_process,
|
|
51
51
|
stop_process,
|
|
52
52
|
)
|
|
53
|
+
from coderouter.launcher_devices import resolve_option_profiles
|
|
53
54
|
from coderouter.logging import get_logger
|
|
54
55
|
|
|
55
56
|
logger = get_logger(__name__)
|
|
@@ -353,10 +354,18 @@ class SwapManager:
|
|
|
353
354
|
reg.remove(proc_id)
|
|
354
355
|
|
|
355
356
|
def _resolve_options(self, spec: SwapModelSpec) -> dict[str, Any]:
|
|
356
|
-
"""Resolve ``spec.option_profile`` into the launcher options dict.
|
|
357
|
+
"""Resolve ``spec.option_profile`` into the launcher options dict.
|
|
358
|
+
|
|
359
|
+
``spec.backend`` may be a variant (``llama.cpp-cuda``), in which case
|
|
360
|
+
the base backend's presets are inherited — the same merge the manual
|
|
361
|
+
UI applies, so a swap entry can reference a shared preset without
|
|
362
|
+
duplicating it under every variant key.
|
|
363
|
+
"""
|
|
357
364
|
if not spec.option_profile or self._launcher_cfg is None:
|
|
358
365
|
return {}
|
|
359
|
-
profiles =
|
|
366
|
+
profiles = resolve_option_profiles(
|
|
367
|
+
self._launcher_cfg.option_profiles, spec.backend
|
|
368
|
+
)
|
|
360
369
|
for p in profiles:
|
|
361
370
|
if p.name == spec.option_profile:
|
|
362
371
|
return dict(p.args)
|