launch: a model folder is found whatever its letter case, so a downloaded model is not offered again on a case-sensitive disk (#3996); a log path the person set in the config is kept (#3656); --dspark takes on|off|auto|none and the word reaches the engine (#3959)

This commit is contained in:
Niko1221 committed 2026-10-26 18:19:38 +01:00
1 parent a8755042bd
commit 68ba321acc
8 files changed
+193 -4

No files matched your search

+19
View File
@@ -397,6 +397,25 @@ class Catalog:
_m, families, _c, _s = self.tables()
return (families[family].get("tag", "") + model).lower()
def model_folder(self, base: Path, family: str, model: str) -> Path:
"""The folder a model's files are in under `base/models`. setup.py names it with the case kept (`models/IQ3_S`), the run-config tag is
lower case (`iq3_s`): on a case-sensitive disk the two are different folders, so the one that exists wins, and a folder that differs
only in case is found (#3996). When none exists yet it is setup's own name, because that is where the download will go."""
_m, families, _c, _s = self.tables()
models = Path(base) / "models"
own = models / (families[family].get("tag", "") + model)
lower = models / self.tag_of(family, model)
for candidate in (own, lower):
if candidate.is_dir():
return candidate
try:
for entry in models.iterdir():
if entry.name.lower() == lower.name and entry.is_dir():
return entry
except OSError:
pass
return own
def kv_gb(self, tokens: int, kv: str = "int8") -> float:
"""The conversation memory for this many tokens, in GB (setup.py's UD_FIT_KV_BYTES: 13 attention layers)."""
mod = self.strata.setup_module()
+5
View File
@@ -114,6 +114,9 @@ def add_parsers(sp: argparse._SubParsersAction) -> list[argparse.ArgumentParser]
"Main does the last steps of every answer; a helper holds part of the model")
g.add_argument("--tune", type=_choice("a tuning mode", S.TUNE_MODES), metavar="quick|thorough|skip", help="find the fastest settings first (default: skip here; the guided screens suggest quick the first time)")
g.add_argument("--tune-allow-small", action="store_true", help="let the tuner also try settings that can change answers very slightly")
g.add_argument("--dspark", type=_choice("a DSpark mode", ("on", "off", "auto", "none")), metavar="on|off|auto|none",
help="DeepSeek: the DSpark drafter that guesses ahead (auto loads it only where it helps; off still loads it on some cards). "
"The same as --set dspark=<word>")
g.add_argument("--set", action="append", default=[], metavar="KEY=VALUE",
help="an advanced engine setting or STRATA_* variable, e.g. prefill=65536 (repeat); unknown names get a warning, not a refusal")
g.add_argument("--pool-cores", metavar="LIST",
@@ -279,6 +282,8 @@ def settings_from_args(a: argparse.Namespace, catalog=None) -> tuple[S.LaunchSet
s.no_key = False
if a.set:
s.extra = {**s.extra, **S.settings_from_pairs(a.set)}
if a.dspark:
s.extra = {**s.extra, "--dspark": a.dspark}
if a.pool_cores:
text = a.pool_cores.replace(" ", "")
if not re.fullmatch(r"\d+(-\d+(:\d+)?)?(,\d+(-\d+(:\d+)?)?)*", text):
+1 -1
View File
@@ -156,7 +156,7 @@ def assess(catalog, family: str | None, model: str | None, *, vision: str = "off
# not installed: whatever of the weights is in the model's folder counts
if family and model:
base = data or Path(catalog.data_dir())
folder = base / "models" / catalog.tag_of(family, model)
folder = catalog.model_folder(base, family, model)
got = 0
try:
got = sum(f.stat().st_size for f in folder.glob("*.gguf") if not f.name.lower().startswith("mmproj"))
+14 -2
View File
@@ -489,6 +489,18 @@ def apply_experts(cfg: dict, experts: int | None) -> None:
cfg["args"] = out + ["--experts", str(int(experts))]
_SETUP_LOG_NAME = re.compile(r"^strata-[A-Za-z0-9_.-]+\.log$")
def own_log_path(base: dict) -> str | None:
"""The log path a person wrote into the model's config themselves (#3656): setup names its own `strata-<model>.log`, so any other
name is theirs and a launch keeps it instead of writing its per-profile log file."""
value = base.get("log")
if not isinstance(value, str) or not value.strip():
return None
return None if _SETUP_LOG_NAME.match(Path(value.replace("\\", "/")).name) else value
def derive_config(base: dict, s: S.LaunchSettings, *, bool_flags: frozenset[str], log: str | None = None, gpus: "list[int] | dict | None" = None,
experts: int | None = None) -> dict:
"""The private run config of this launch: the installed one plus the choices that are not setup's (host, port, key, cards, advanced)."""
@@ -508,7 +520,7 @@ def derive_config(base: dict, s: S.LaunchSettings, *, bool_flags: frozenset[str]
cfg["open_browser"] = False # strata launch never opens a browser by itself (only `--open` does, after the start)
if isinstance(cfg.get("args"), list):
cfg["args"] = [a for a in cfg["args"] if a != "--open"]
if log:
if log and not own_log_path(base):
cfg["log"] = log
apply_cards(cfg, gpus)
from strata_control import drafting as _drafting
@@ -1060,7 +1072,7 @@ class Runner:
s, cat = self.s, self.catalog
data = Path(s.data_dir) if s.data_dir else (self.existing_data_dir() or cat.data_dir())
models_dir = data / "models"
job = {"folder": str(models_dir / cat.tag_of(s.family, s.model)),
job = {"folder": str(cat.model_folder(data, s.family, s.model)),
"total_gb": float(next((r.download_gb for r in cat.rows() if r.family == s.family and r.model == s.model), 0) or 0)}
try:
setup_module = cat.strata.setup_module()
+12 -1
View File
@@ -277,15 +277,26 @@ def settings_from_pairs(pairs: list[str]) -> dict[str, str]:
return out
#: Options whose value is a WORD the engine reads (on, off, auto, none): the word goes through as typed. Making "on" a bare flag
#: started strata-dsv41 with a lone `--dspark`, which it refuses ("--dspark needs a value", #3959), and "off" removed the flag instead of
#: switching the drafter off.
WORD_VALUE_FLAGS = frozenset({"--dspark"})
def as_argv_changes(extra: dict[str, str], bool_flags: frozenset[str]) -> tuple[dict[str, Any], dict[str, str | None]]:
"""Turn the Advanced table's values into `tune_knobs.apply_to_argv` changes and environment changes.
A flag without a value (or on/true/yes) is "present"; off/false/no/none removes it; anything else is a value."""
A flag without a value (or on/true/yes) is "present"; off/false/no/none removes it; anything else is a value.
A word-valued option (WORD_VALUE_FLAGS) keeps its word."""
args: dict[str, Any] = {}
env: dict[str, str | None] = {}
for key, value in extra.items():
if key.startswith("--"):
low = value.lower()
if key in WORD_VALUE_FLAGS and key not in bool_flags:
if low:
args[key] = low
continue # an empty value is no change, as for every option that takes one (#3504)
if low == "" and key not in bool_flags:
continue # an empty value for an option that takes one is no change, never a bare flag (#3504)
if key in bool_flags or low in ("on", "true", "yes", ""):
+20
View File
@@ -832,6 +832,26 @@ class FlagsCommand(unittest.TestCase):
self.assertNotIn("--pool-cores", p["settings"]["extra"])
self.assertTrue(any("pool-cores" in w for w in p["warnings"]))
def test_dspark_takes_a_word_and_the_word_reaches_the_engine_line(self):
# --set dspark=on used to start strata-dsv41 with a lone "--dspark" ("--dspark needs a value", #3959)
from strata_cli.launch import settings as S
from strata_control import tune_knobs
base = ["--pack", "p", "--max-context", "262144"]
for word in ("on", "off", "auto", "none"):
a, _ = S.as_argv_changes({"--dspark": word}, tune_knobs.BOOL_FLAGS)
self.assertEqual(a, {"--dspark": word})
out = tune_knobs.apply_to_argv(base + ["--dspark", "auto"], a)
self.assertEqual(out[out.index("--dspark") + 1], word, word)
self.assertEqual(out.count("--dspark"), 1)
a, _ = S.as_argv_changes({"--dspark": "ON", "--dspark-conf": "0.7"}, tune_knobs.BOOL_FLAGS)
self.assertEqual(a, {"--dspark": "on", "--dspark-conf": "0.7"})
self.assertEqual(S.as_argv_changes({"--dspark": ""}, tune_knobs.BOOL_FLAGS)[0], {})
code, out, _err = run_cli(["launch", "--help"])
self.assertIn("--dspark", out)
p = self.plan("--model", "q2_0", "--dspark", "on")
self.assertEqual(p["settings"]["extra"], {"--dspark": "on"})
self.assertIn("--set dspark=on", p["flags"])
def test_api_key_flags(self):
p = self.plan("--model", "q2_0", "--access", "lan", "--api-key", "sk-mine")
self.assertEqual(p["settings"]["api_key"], "set")
+33
View File
@@ -0,0 +1,33 @@
"""A launch keeps the log path the person wrote into the model's config (#3656)."""
from __future__ import annotations
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from strata_cli.launch import runner, settings as S # noqa: E402
from strata_control import tune_knobs # noqa: E402
def derive(base):
return runner.derive_config(base, S.LaunchSettings(), bool_flags=tune_knobs.BOOL_FLAGS, log="C:/launch/q2_0.engine.log")
class OwnLog(unittest.TestCase):
def test_the_setup_log_name_is_replaced_by_the_launch_log(self):
for name in ("M:/Strata/strata-iq3_xxs.log", "/home/me/strata/strata-q2_0.log", "M:\Strata\strata-iq3_s.log"):
self.assertEqual(derive({"exe": "e", "args": [], "log": name})["log"], "C:/launch/q2_0.engine.log", name)
self.assertEqual(derive({"exe": "e", "args": []})["log"], "C:/launch/q2_0.engine.log")
def test_a_log_path_the_person_chose_is_kept(self):
for name in ("M:/logs/history.log", "/var/log/strata/engine.log", "D:\notes\iq3.txt"):
self.assertEqual(derive({"exe": "e", "args": [], "log": name})["log"], name, name)
self.assertEqual(runner.own_log_path({"log": "M:/Strata/strata-iq3_xxs.log"}), None)
self.assertEqual(runner.own_log_path({"log": ""}), None)
self.assertEqual(runner.own_log_path({"log": 5}), None)
if __name__ == "__main__":
unittest.main()
+89
View File
@@ -0,0 +1,89 @@
"""strata launch finds a model folder whatever its letter case (#3996).
setup.py writes `models/IQ3_S` (the model name's case kept); the run-config tag is lower case (`iq3_s`). On a case-sensitive disk the
launch lookup missed the downloaded folder and offered the whole download again. The tests make the disk case-sensitive on any system by
answering `Path.is_dir` from the names the folder really lists.
"""
from __future__ import annotations
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from strata_cli.launch import modelstate # noqa: E402
from strata_cli.launch.catalog import Catalog # noqa: E402
_real_is_dir = Path.is_dir
def _case_sensitive_is_dir(self):
try:
listed = {entry.name for entry in self.parent.iterdir()}
except OSError:
return False
return self.name in listed and _real_is_dir(self)
class StubCatalog:
"""Just what model_folder and assess use."""
tables = lambda self: ({}, {"qwen": {"tag": ""}}, None, None) # noqa: E731
tag_of = Catalog.tag_of
model_folder = Catalog.model_folder
def __init__(self, data: Path):
self._data = data
def data_dir(self):
return self._data
def rows(self):
return [mock.Mock(family="qwen", model="IQ3_S", download_gb=83.6, images=False)]
def installed_config(self, tag):
return None
class ModelFolderCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.data = Path(self.tmp.name)
patch = mock.patch.object(Path, "is_dir", _case_sensitive_is_dir)
patch.start()
self.addCleanup(patch.stop)
self.cat = StubCatalog(self.data)
def test_the_folder_setup_wrote_is_found(self):
folder = self.data / "models" / "IQ3_S"
folder.mkdir(parents=True)
self.assertEqual(self.cat.model_folder(self.data, "qwen", "IQ3_S").name, "IQ3_S")
def test_a_lower_case_folder_is_found_too(self):
(self.data / "models" / "iq3_s").mkdir(parents=True)
self.assertEqual(self.cat.model_folder(self.data, "qwen", "IQ3_S").name, "iq3_s")
def test_a_folder_that_differs_only_in_case_is_found(self):
(self.data / "models" / "Iq3_S").mkdir(parents=True)
self.assertEqual(self.cat.model_folder(self.data, "qwen", "IQ3_S").name, "Iq3_S")
def test_nothing_there_yet_is_the_name_setup_will_write(self):
(self.data / "models").mkdir(parents=True)
self.assertEqual(self.cat.model_folder(self.data, "qwen", "IQ3_S").name, "IQ3_S")
self.assertEqual(self.cat.model_folder(self.data / "missing", "qwen", "IQ3_S").name, "IQ3_S")
def test_a_downloaded_model_in_setups_folder_is_counted_by_the_launch_plan(self):
folder = self.data / "models" / "IQ3_S"
folder.mkdir(parents=True)
(folder / "m-00001-of-00002.gguf").write_bytes(b"x" * 1000)
state = modelstate.assess(self.cat, "qwen", "IQ3_S", data_dir=self.data)
self.assertAlmostEqual(state.on_disk_gb, 1000 / 1e9)
empty = modelstate.assess(self.cat, "qwen", "IQ3_S", data_dir=self.data / "other")
self.assertEqual(empty.on_disk_gb, 0.0)
if __name__ == "__main__":
unittest.main()