mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 06:15:58 -10:00
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:
1 parent
a8755042bd
commit
68ba321acc
8 files changed
+193
-4
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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", ""):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user