Files

2478 lines
139 KiB
Python

"""What `strata launch` does after the choices are made: get the engine and the model, write the run config, tune, start, open access.
Every step reports plain-word events through one callback (`emit`), so the TUI (progress screen), `--no-tui` (printed lines) and the
tests see the same thing. Nothing here prints or reads the keyboard. The steps reuse what is already in the Terminal:
* the engine comes from the signed feed (`strata update`, the one explicit step that downloads);
* the model comes from setup.py in the app's own installer mode (STRATA_CONTROL_INSTALL), the same job the desktop app runs;
* the run config the engine uses is a private copy of the installed one with this launch's choices applied; the profile
`launch-<name>` (the shared profiles store) points at it, and carries host, port and parallel;
* tuning is `strata tune auto`, its result lives in the shared presets store and is applied at every start;
* start and stop are `strata runtime start|stop` (a background service: it keeps serving after this terminal closes).
"""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
import tempfile
import threading
import time
from strata_control import atomic as _atomic_mod
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable
from strata_cli.launch import access as acc
from strata_cli.launch import modelstate
from strata_cli.launch import settings as S
from strata_cli.launch.catalog import Catalog, CatalogUnavailable
ROOT = Path(__file__).resolve().parents[2]
WINDOWS = os.name == "nt"
def install_isolation_words(catalog, data_dir: str | os.PathLike | None = None) -> list[str]:
"""Round 8, N5: a clean test (a scratch data folder) silently ran an OLDER install found elsewhere (its run config and engine 0.1.41).
When a data folder was chosen (--data-dir, or STRATA_DATA_DIR from the installer) and the install in use is outside it, say which one is used
and why. Nothing is moved or switched."""
chosen = data_dir or os.environ.get("STRATA_DATA_DIR")
places = getattr(catalog, "places", None) or []
if not chosen or not places or places[0].source == "explicit":
return []
main = Path(places[0].path)
try:
inside = os.path.commonpath([os.path.abspath(main), os.path.abspath(chosen)]) == os.path.abspath(chosen)
except ValueError:
inside = False
if inside:
return []
return [f"Strata is using the earlier install in {main} ({places[0].configs} saved model setting(s) and its engine settings), not a clean one in "
f"{chosen}: the data folder only says where model files are kept. To test from scratch, use a new user account or remove the earlier install first."]
#: the first engine whose Metal build reads Gyro files (round 8, M: 0.1.45.3 was reused and failed on missing TQK6/TQK7 kernels)
GYRO_METAL_ENGINE = (0, 1, 47)
def version_tuple(version: str | None) -> tuple[int, ...]:
"""'0.1.45.3' -> (0, 1, 45, 3); () when it is not a dotted run of numbers (a source build says so itself)."""
m = re.match(r"\d+(?:\.\d+)*", str(version or "").strip())
return tuple(int(x) for x in m.group(0).split(".")) if m else ()
def python_failure(lines: list[str]) -> str | None:
"""The last line of a Python traceback in setup.py's output (ModuleNotFoundError: No module named 'calibrate'), or None when the
output holds no traceback. Frames are skipped; the exception line is the first unindented line after the last 'Traceback'."""
for i in range(len(lines) - 1, -1, -1):
if lines[i].startswith("Traceback (most recent call last)"):
for line in lines[i + 1:]:
if line.strip() and not line[:1].isspace():
return line.strip()[:300]
return None
return None
DEEPSEEK_ENGINE_LINE = re.compile(r"(A newer DeepSeek engine (?:is available|may be available)[^\r\n]*|DeepSeek engine updated from [^\r\n]*)")
def deepseek_engine_note(line: str) -> tuple[str, str] | None:
"""setup's line about the DeepSeek engine upgrade ("A newer DeepSeek engine is available but could not be installed: ...; still using X",
or "DeepSeek engine updated from X to Y"): ("warn" | "info", the sentence), None for any other line. The failure line must reach the
person: the model starts with the engine they have, and they should know there is a newer one."""
found = DEEPSEEK_ENGINE_LINE.search(line)
if not found:
return None
text = found.group(1).strip()
return ("info" if text.startswith("DeepSeek engine updated") else "warn", text)
def argparse_failure(lines: list[str]) -> str:
"""setup.py's own refusal of an option ("setup.py: error: argument --kv: invalid choice: ..."), which is neither an [X] line nor a
traceback (#3785): the words after "error:" are the cause."""
for line in reversed(lines):
found = re.match(r"\s*(?:setup|strata)(?:\.py)?: error: (.+)", line)
if found:
return found.group(1).strip()[:300]
return ""
def missing_file(failure: str) -> bool:
"""An import or file error: the install itself is short of a file, which an update of Strata brings back."""
return failure.startswith(("ModuleNotFoundError", "ImportError", "FileNotFoundError"))
def capital(text: str) -> str:
"""A reason that was written to stand alone ("the update feed could not be read: ...") starts the sentence it is joined to with a capital."""
return text[:1].upper() + text[1:]
class LaunchError(RuntimeError):
"""A step could not be done. `text` says what it means; `next` says what to do."""
def __init__(self, text: str, next: str = "", code: int = 6):
super().__init__((text + " " + next).strip())
self.text, self.next, self.code = text, next, code
def setup_failure(lines: list[str], err: str, hint: str, details: str | None, own_files: bool) -> LaunchError:
"""The error for a setup.py run that exited non-zero. setup.py's own `[X]` line (err) is the message when there is one; when it
stopped with a Python error instead, that error's last line is (#3613, #3467: a file missing from the Terminal's tools/ ended every
launch after a tune with only "The model could not be installed"). `details` is the file that holds the rest of its output."""
err = err or argparse_failure(lines)
crashed = python_failure(lines) if not err else None
if crashed:
where = f" The whole setup output is in {details}." if details else ""
if missing_file(crashed):
return LaunchError(f"The model could not be installed. A file that setup needs is missing from this Strata install ({crashed}).",
"Update Strata (`strata update`) and run it again." + where, 6)
if crashed.startswith("PermissionError"):
folder = re.findall(r"'([^']+)'", crashed)
shown = folder[-1] if folder else None
try: # the folder Strata was told to use, not a file inside it
shown = str(Path(shown).parent) if shown and Path(shown).name in ("models", "packs", "mtp") else shown
except (OSError, ValueError):
pass
return LaunchError("The model could not be installed. " + (f"Windows would not let Strata write to {shown}" if shown else
"Windows would not let Strata write to a folder") + " (access denied).",
"Pick a folder you own for the model files (the Folder step, or --data-dir D:\\Strata-models) and start again. "
"Nothing was downloaded." + where, 6)
return LaunchError(f"The model could not be installed. Setup stopped with an error ({crashed}).", "Run it again." + where, 6)
return LaunchError("The model could not be installed." + (f" {capital(err.rstrip('.'))}." if err else ""),
(hint or ("Run it again: finished steps are skipped." if own_files else
"Run it again: finished steps are skipped and a download continues where it stopped."))
+ (f" Details: {details}" if details else ""), 6)
class Cancelled(LaunchError):
def __init__(self):
super().__init__("Stopped. Nothing was changed on your computer beyond files already downloaded.", "", 130)
@dataclass
class Event:
stage: str # engine | model | config | tune | start | access | done
text: str
pct: float | None = None
level: str = "info" # info | warn | error
data: dict = field(default_factory=dict)
@dataclass
class Result:
profile: str
urls: list[str]
key: str | None
tune_word: str | None
notices: list[str]
config: str
tunnel: str | None = None
serving: bool = True # False: the model is ready but nothing was started (no engine yet)
engine_problem: str | None = None # the plain reason there is no engine, when serving is False
final_text: str = "" # what to tell the person when serving is False
reused: bool = False # the same choices were already serving: nothing was stopped or started
NO_ENGINE_NEXT = ("Update Strata (`strata update`) and run `strata launch` again. Nothing was downloaded, and any model files you already have are kept.")
STAGES = ("engine", "model", "config", "tune", "start", "access")
NO_ENGINE_STEP = ("run `strata update` to get the newest engines and start again; if this stays, your graphics card is not one Strata supports yet, "
"and the model files stay on this computer for when it is")
#: no graphics card Strata can use at all (an Intel UHD/Iris only, an AMD APU-only laptop, a virtual machine, Windows on ARM)
NO_GPU_PREFIX = "no graphics card that Strata can use was found"
NO_GPU_STEP = ("Strata needs an NVIDIA or AMD graphics card, an Intel Arc card or an Apple Silicon Mac, and there is no CPU-only engine for this "
"computer. Run Strata on a computer that has one; the model files stay here")
def no_gpu_words(names: list[str] | None = None) -> str:
"""The plain reason when nothing Strata can run on is in this computer, naming what is there when it is known."""
names = [n for n in (names or []) if n]
if names:
return f"{NO_GPU_PREFIX}; this computer has {', '.join(names[:3])}"
return NO_GPU_PREFIX
def _step_for(reason: str) -> str:
return NO_GPU_STEP if reason.startswith(NO_GPU_PREFIX) else NO_ENGINE_STEP
def no_engine_warning(reason: str) -> str:
"""The plain warning shown before a model is downloaded on a computer with no engine to run it."""
reason = reason.strip().rstrip(".")
if reason.startswith(NO_GPU_PREFIX):
return (f"Strata can't run on this computer: {reason}. The model will download, but it will not run here "
f"(next step: {NO_GPU_STEP}).")
return (f"Strata couldn't find an engine for this computer ({reason}). The model will download, but it may not run until you install "
f"the right engine (next step: {NO_ENGINE_STEP}).")
def no_engine_final(reason: str, name: str = "") -> str:
reason = reason.strip().rstrip(".")
if reason.startswith(NO_GPU_PREFIX):
return f"The model is ready, but Strata can't start it on this computer: {reason}. Next step: {NO_GPU_STEP}."
return (f"The model is ready, but Strata can't start it yet: it couldn't find an engine for this computer ({reason}). "
f"Next step: {NO_ENGINE_STEP}. The model files stay on this computer.")
def tune_words(result: dict | None, code: int | None = None) -> tuple[str, str]:
"""(short word, plain sentence) for a tuner result: faster / already fastest / busy PC / failed / stopped."""
if not result:
return "failed", "The tuner did not finish, so the standard settings run. Nothing was changed."
out = result.get("outcome")
if out == "improved":
gain = result.get("gain_pct")
return "faster", (f"Done: answers are about {gain:.0f}% faster with the tuned settings." if isinstance(gain, (int, float))
else "Done: the tuned settings are faster.")
if out == "default_confirmed":
return "already fastest", "Your computer already runs at its best with the standard settings. Nothing needed changing."
if out == "inconclusive":
return "busy PC", ("The computer was too busy to tell a real gain from noise, so the standard settings run. "
"Close other programs and tune again later (strata tune auto).")
if out == "cancelled":
return "stopped", "Tuning was stopped. Your settings were not changed."
return "failed", (result.get("message") or "Tuning could not run here.") + " The standard settings run."
# ----------------------------------------------------------------------------------------------- run config
def runtime_max_wait() -> int:
from strata_control import runtime
return int(runtime.MAX_START_WAIT)
def read_config(path: Path) -> dict:
return json.loads(Path(path).read_text(encoding="utf-8-sig"))
def desc_matches(desc: dict, s: S.LaunchSettings) -> bool:
"""Does the installed model's config already carry the context, memory format and picture choice of these settings?
(The graphics cards are not part of it: they go into each launch's own config, see apply_cards.)"""
if s.context and desc.get("context") != s.context:
return False
want_kv = s.kv or ("int8" if (s.context or 32768) > 8192 else "fp16")
if desc.get("kv") not in (None, want_kv) and (s.context or 32768) > 8192:
return False
images = desc.get("images") or "off"
if s.vision is None:
return True # no preference yet: the run settles it before it compares
if (s.vision == "off") != (images == "off"):
return False
if s.vision in ("gpu", "cpu") and images != s.vision:
return False
return True
def _inside(path: Path, folder: Path) -> bool:
a, b = os.path.normcase(str(path)), os.path.normcase(str(folder))
return a == b or a.startswith(b.rstrip("\\/") + os.sep)
def files_present(cfg: dict) -> list[str]:
"""The model files a run config names that are missing (empty list: all there)."""
missing = []
for tok in cfg.get("args") or []:
if isinstance(tok, str) and tok.lower().endswith(".gguf") and not Path(tok).exists():
missing.append(tok)
for flag in ("--pack", "--mtp"):
args = [str(a) for a in cfg.get("args") or []]
if flag == "--mtp" and flag in args and args.index(flag) + 1 < len(args) and args[args.index(flag) + 1].isdigit():
continue # Qwen3.6: a number is the draft depth, not a folder (#3298)
if flag in args and args.index(flag) + 1 < len(args) and not Path(args[args.index(flag) + 1]).exists():
missing.append(args[args.index(flag) + 1])
vision = cfg.get("vision")
if isinstance(vision, dict) and vision.get("mmproj") and not Path(str(vision["mmproj"])).exists():
missing.append(str(vision["mmproj"])) # the picture reader's file: setup fetches it again
return missing
def apply_choices(cfg: dict, s: S.LaunchSettings) -> dict:
"""A copy of the installed run config with this launch's context, memory format and picture choice on its engine command, so the command
the Advanced step shows is the one these choices run (the installed config only holds the choices it was installed with). Only the command
preview and the comparison use this; the start itself goes through setup, which writes the same values."""
import copy
cfg = copy.deepcopy(cfg)
args = [str(a) for a in cfg.get("args") or []]
def put(flag: str, value: str) -> None:
if flag in args:
i = args.index(flag)
if i + 1 < len(args) and not args[i + 1].startswith("--"):
args[i + 1] = value
return
args.insert(i + 1, value)
else:
args.extend([flag, value])
if s.context:
put("--max-context", str(int(s.context)))
kv = s.kv or ("int8" if (s.context or 32768) > 8192 else "fp16")
if "--kv" in args or s.kv:
put("--kv", kv)
if cfg.get("arch") == "deepseek41": # the engine's own tower: --vision on / off is its whole picture setting
if s.vision:
put("--vision", "off" if s.vision == "off" else "on")
elif s.vision == "off":
args = [a for a in args if a != "--vision"]
cfg.pop("vision", None) # the picture reader's own settings: the server adds --vision for them
elif s.vision and "--vision" not in args:
args.append("--vision")
cfg["args"] = args
return cfg
def apply_cards(cfg: dict, gpus: "list[int] | dict | None") -> None:
"""The graphics cards of THIS launch go into its run config, whatever card the model was installed with (no reinstall): one card is a number,
several are a list with the automatic layer split. None: the installed config's own choice stays. A dict is the roles of every card
(strata_control/gpu_roles.mirror: "gpu_roles" plus the legacy "gpu" with the Main card last)."""
if isinstance(gpus, dict):
for k in ("gpu", "gpu_order", "gpu_none", "gpu_roles", "gpu_cards"):
cfg.pop(k, None)
cfg.update(gpus)
cfg["gpus_asked"] = True
cfg.pop("hip_ordinal", None)
on = gpus.get("gpu")
if isinstance(on, list) and len(on) > 1:
cfg["layer_split"] = cfg.get("layer_split") or "auto"
else:
cfg.pop("layer_split", None)
return
if not gpus:
return
for k in ("gpu_roles", "gpu_cards", "gpu_none"): # a plain card list replaces an earlier roles choice
cfg.pop(k, None)
gpus = [int(g) for g in gpus]
cfg["gpu"] = gpus[0] if len(gpus) == 1 else gpus
cfg["gpus_asked"] = True
cfg.pop("hip_ordinal", None) # an old AMD config's ordinal of ITS card would win over the card named now
if len(gpus) > 1:
cfg["layer_split"] = cfg.get("layer_split") or "auto"
else:
cfg.pop("layer_split", None)
def installed_cards(base: dict | None, usable: list[dict] | None = None) -> list[int] | None:
"""The cards the installed model's own config names after a choice (setup --gpu / --gpus, the app's card pick: "gpus_asked"), in its order;
None when it names none or was never asked, or when setup picked them itself (strata_control/gpu_pick.py: a DeepSeek config of setup 0.2.8
and earlier marks setup's one-card pick as asked; `usable`: the cards that count on this PC, needed to tell the two apart)."""
if not isinstance(base, dict) or not (base.get("gpus_asked") or base.get("gpu_roles")):
return None
from strata_control import gpu_pick
if not gpu_pick.chosen_by_person(base, usable):
return None
g = base.get("gpu")
if g is None or g == "":
return None
try:
cards = [int(str(x).strip()) for x in (g if isinstance(g, (list, tuple)) else str(g).split(",")) if str(x).strip() != ""]
except ValueError:
return None
return cards or None
def cards_for_launch(s: S.LaunchSettings, catalog, ram_gb: float | None = None, base: dict | None = None) -> "list[int] | dict | None":
"""The cards this launch uses: the ones named (--gpu / --gpus, the Cards step), else the cards the installed config was set to (a person's
earlier choice, "gpu": 1 or [1, 0] - never replaced by the automatic pick, which would put the first of equal cards back), else the
recommended ones by the shared fit rule (strata_control/gpu_fit.py) when the computer has more than one card that counts, else None."""
if s.gpu_roles:
try:
from strata_control import gpu_roles
comp = catalog.computer()
cards = gpu_roles.rows(comp.gpus, comp.engine_vendor)
named = {int(k): v for k, v in s.gpu_roles.items()}
roles = gpu_roles.normalize({int(c.get("index", 0)): named.get(int(c.get("index", 0)), "off") for c in cards}, cards)
return gpu_roles.mirror(roles, cards)
except Exception: # noqa: BLE001 - never block a launch over the choice of cards
return None
if s.gpus:
return [int(g) for g in s.gpus]
try:
usable_now = catalog.computer().usable_gpus
except Exception: # noqa: BLE001 - without the card list a saved card is kept, as before
usable_now = None
if installed_cards(base, usable_now) or (isinstance(base, dict) and base.get("gpu_roles")):
return None
try:
from strata_control import gpu_roles
comp = catalog.computer()
if len(comp.usable_gpus) < 2:
return None
cards = gpu_roles.rows(comp.gpus, comp.engine_vendor)
if comp.engine_vendor == "amd" and ((isinstance(base, dict) and base.get("arch") == "deepseek41")
or getattr(s, "family", None) == "deepseek41"):
# DeepSeek on AMD: the cards setup and `--plan` use (strata_control/dsv41_fit.run_cards): one card on Windows, only cards the engine
# package runs, no integrated chip beside a card with memory of its own
from strata_control import dsv41_fit
ids = [int(c["index"]) for c in dsv41_fit.run_cards(comp.usable_gpus, comp.engine_vendor)[0]]
if len(ids) < 2:
return ids or None # pinned to that card, exactly as setup pins it
cards = [c for c in cards if int(c.get("index", 0)) in ids]
rec = gpu_roles.recommend(cards, comp.engine_vendor)["roles"]
return gpu_roles.order(rec, cards) or None
except Exception: # noqa: BLE001 - never block a launch over the choice of cards
return None
EXPERTS_MIN_ENGINE = (0, 1, 50) # the first DeepSeek engine that accepts --experts (0.1.49.2, the one shipped before, does not)
def dsv41_engine_version(exe) -> tuple:
"""The version of a DeepSeek engine: from its package folder's name ("0.1.50-windows-x86_64-cuda-cuda13"), else what `--version` prints;
() when neither says."""
if not exe:
return ()
from strata_control import dsv41_engine
folder = Path(str(exe)).parent.name
version = dsv41_engine.version_key(folder) if folder[:1].isdigit() else ()
if version:
return version
try:
done = subprocess.run([str(exe), "--version"], capture_output=True, text=True, errors="replace", timeout=10, stdin=subprocess.DEVNULL)
found = re.search(r"(\d+\.\d+\.\d+(?:\.\d+)?)", (done.stdout or "") + (done.stderr or ""))
return tuple(int(x) for x in found.group(1).split(".")) if found else ()
except (OSError, subprocess.SubprocessError, ValueError):
return ()
def engine_takes_experts(exe) -> bool:
"""Does this DeepSeek engine accept --experts (0.1.50 or newer)? An engine whose version cannot be told counts as not: nothing is passed,
nothing is shown, no error."""
return dsv41_engine_version(exe) >= EXPERTS_MIN_ENGINE
def used_cards(computer, s: "S.LaunchSettings") -> list:
"""The cards this launch uses: the ones named (--gpu, the Cards step; a role other than off), else every card that can be used."""
cards = list(computer.usable_gpus)
chosen = None
if s.gpu_roles:
chosen = {int(k) for k, v in s.gpu_roles.items() if str(v) != "off"}
elif s.gpus:
chosen = {int(g) for g in s.gpus}
if chosen is not None:
cards = [c for c in cards if int(c.get("index", 0)) in chosen]
return cards
def used_vram_gb(computer, s: "S.LaunchSettings") -> float:
"""Graphics memory (GiB) of the cards this launch uses, summed as the engine sums it."""
return sum(float(c.get("vram_gb") or 0) for c in used_cards(computer, s))
def uses_integrated_gpu(computer, s: "S.LaunchSettings") -> bool:
"""Does the launch run on an integrated / unified-memory GPU (Strix Halo, gfx1151, Radeon 8060S: the hardware probe marks it uma, integrated or
unified_memory)? A Mac is left out: DeepSeek is not on the Mac."""
for c in used_cards(computer, s):
if str(c.get("vendor")) == "apple" or c.get("unified_gb"):
continue
if c.get("integrated") or c.get("uma") or c.get("unified_memory"):
return True
return False
def faster_by_default(computer, s: "S.LaunchSettings") -> bool:
"""The engine's `--experts auto` rule: Faster (5 experts) on an integrated GPU, or when the cards used total 16 GiB or less."""
return uses_integrated_gpu(computer, s) or S.faster_default(used_vram_gb(computer, s))
EXPERTS_LINE = re.compile(r"experts per word: (\d+)")
def experts_from_log(path) -> int | None:
"""The engine's own answer, read back from its startup line "experts per word: N" (the last one in the log), None when it is not there."""
try:
data = Path(path).read_bytes()[-262144:].decode("utf-8", "replace")
except (OSError, TypeError, ValueError):
return None
found = EXPERTS_LINE.findall(data)
return int(found[-1]) if found else None
def apply_experts(cfg: dict, experts: int | None) -> None:
"""DeepSeek's speed choice as the engine flag: --experts 5 (Faster) or --experts 6 (Full quality), replacing one already in the saved args."""
if experts is None or not isinstance(cfg.get("args"), list):
return
args, out, i = [str(x) for x in cfg["args"]], [], 0
while i < len(args):
if args[i] == "--experts":
i += 2
continue
if args[i].startswith("--experts="):
i += 1
continue
out.append(args[i])
i += 1
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)."""
from strata_control import tune_knobs
cfg = json.loads(json.dumps(base))
cfg["host"] = acc.host_for(s.access)
cfg["port"] = int(s.port)
if s.api_key:
cfg["api_key"] = s.api_key
elif s.no_key and s.access != "local":
cfg["api_key"] = "none" # served without a key on purpose (the server warns once at start)
else:
cfg.pop("api_key", None)
if s.drafting: # strata_control/drafting.py: serve/server.py applies it at every start
cfg["drafting"] = s.drafting
cfg.setdefault("exe", "component:strata-engine")
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 and not own_log_path(base):
cfg["log"] = log
apply_cards(cfg, gpus)
from strata_control import drafting as _drafting
drafting_off = _drafting.normalize(s.drafting if s.drafting is not None else cfg.get("drafting")) == "off"
if s.mtp == "off" or drafting_off: # no draft layer for this launch (serve/server.py applies "mtp": false at every start)
from strata_control import draft_layer # --drafting off: the written file carries no --mtp either (it was left in before)
cfg["mtp"] = False
draft_layer.apply(cfg)
elif s.mtp == "on":
cfg["mtp"] = True
if cfg.get("backend") == "sycl": # exactly two Arc B-series cards: --spec 2 (strata_control/intel_spec.py); the person's
from strata_control import intel_spec # own choices (user_args, an extra option below) come after it and win
intel_spec.apply_for_launch(cfg)
if s.extra:
a_changes, e_changes = S.as_argv_changes(s.extra, bool_flags)
cfg["args"] = tune_knobs.apply_to_argv([str(x) for x in cfg.get("args") or []], a_changes)
if e_changes:
cfg["env"] = tune_knobs.apply_to_env(dict(cfg.get("env") or {}), e_changes)
from strata_control import draft_layer
draft_layer.apply(cfg) # "mtp": false (from the installed config too) never sits beside --mtp in the saved args (B10)
apply_experts(cfg, experts)
return cfg
def write_private(path: Path, data: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(prefix=".launch-cfg-", suffix=".tmp", dir=path.parent)
try:
with os.fdopen(fd, "wb") as fh:
fh.write((json.dumps(data, indent=1, ensure_ascii=False) + "\n").encode("utf-8"))
try:
os.chmod(tmp, 0o600)
except OSError:
pass
_atomic_mod.replace(tmp, path)
finally:
if os.path.exists(tmp):
os.unlink(tmp)
if WINDOWS:
try:
from strata_control import private_files
private_files.restrict_to_current_user(path, directory=False)
except Exception: # noqa: BLE001
pass
def launch_dir() -> Path:
from strata_control import paths
d = paths.state_base() / "launch"
d.mkdir(parents=True, exist_ok=True)
return d
# ----------------------------------------------------------------------------------------------- the runner
def download_text(dl: dict, already: bool, label: str | None = None) -> str:
"""The step line for a download or a check. `label` is what is being fetched, in words (strata_control/progress_words.py). A model whose
files were all on disk before this run says so instead of "Downloading 58.5 of 58.5 GB"."""
if already:
return f"The model files are already on this computer ({dl['total_gb']:.1f} GB); checking them"
return f"{label}: {dl['done_gb']:.1f} of {dl['total_gb']:.1f} GB" if label else f"Downloading {dl['done_gb']:.1f} of {dl['total_gb']:.1f} GB"
_END = object()
def _elapsed(seconds: float) -> str:
"""'40 s', '3 min' (plain, rounded down)."""
seconds = int(seconds)
return f"{seconds} s" if seconds < 90 else f"{seconds // 60} min"
class Runner:
def __init__(self, s: S.LaunchSettings, *, catalog: Catalog | None = None, emit: Callable[[Event], None] | None = None,
cancel: threading.Event | None = None, feed: str | None = None, python: str | None = None,
components_dir: str | None = None, hardware: dict | None = None,
confirm: Callable[[str, str], bool] | None = None, assume_yes: bool = False, move_data: bool = False,
retune: bool = False, restart: bool = False, confirm_restart: Callable[[str, str], bool] | None = None,
after_check: Callable[[], None] | None = None):
self.s = s
self.after_check = after_check # called once when nothing blocks the launch (no server, the same one, or "Restart" was answered): the screens save the preset then
self.restart = restart # --restart: a server that serves other choices (or holds this port) is stopped and replaced without asking
self.confirm_restart = confirm_restart # the screens: asked (question, detail) once; True = restart. None (no screens) and no --restart: refuse in one line
self._stop_first: list[str] = [] # other launch profiles that hold this port; stopped before the measuring and the start
self.retune = retune # --retune: measure again even when a tuning result for this model on this computer is stored
self.move_data = move_data # --move-data / the answer "move": the model files already here go to the new --data-dir
self.confirm = confirm # asked (title, text) before a model is downloaded with no engine; True = go on
self.assume_yes = assume_yes # --yes: no questions
self.engine_problem: str | None = None
self._catalog = catalog
self.emit_cb = emit or (lambda e: None)
self.cancel = cancel or threading.Event()
self.feed = feed
self.python = python or sys.executable
self.components_dir = components_dir
self.hardware_override = hardware
self.children: list[subprocess.Popen] = []
self.notices: list[str] = []
self._engine_notes: set[str] = set()
self.identity_notes: list[str] = []
self.tune_result: dict | None = None
self.config_path: Path | None = None
self._own_checked = False
# ---- small helpers
def emit(self, stage: str, text: str, pct: float | None = None, level: str = "info", **data: Any) -> None:
self.emit_cb(Event(stage, text, pct, level, data))
def check_cancel(self) -> None:
if self.cancel.is_set():
raise Cancelled()
@property
def catalog(self) -> Catalog:
if self._catalog is None:
try:
self._catalog = Catalog(hardware=self.hardware_override)
except CatalogUnavailable as e:
raise LaunchError(str(e), "", 5) from None
return self._catalog
@property
def name(self) -> str:
return S.slug(self.s.name or (self.s.model or "model") + "-" + S.context_label(self.s.context).lower())
@property
def profile(self) -> str:
return "launch-" + self.name
def _env(self) -> dict[str, str]:
env = dict(os.environ, PYTHONUNBUFFERED="1", PYTHONIOENCODING="utf-8")
env.pop("STRATA_NO_ACQUIRE", None) # launch is the explicit step that may download
if self.components_dir:
env["STRATA_COMPONENTS_DIR"] = self.components_dir
return env
def _spawn(self, argv: list[str], *, env: dict | None = None, cwd: str | None = None) -> subprocess.Popen:
kw = {"creationflags": subprocess.CREATE_NO_WINDOW} if WINDOWS else {}
p = subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
encoding="utf-8", errors="replace", env=env or self._env(), cwd=cwd, bufsize=1, **kw)
self.children.append(p)
return p
def _stop_children(self) -> None:
"""End only the processes this runner started (by the handles it holds)."""
for p in self.children:
if p.poll() is None:
try:
p.terminate()
p.wait(timeout=15)
except Exception: # noqa: BLE001
try:
p.kill()
except Exception: # noqa: BLE001
pass
def _cli(self, *args: str) -> list[str]:
return [self.python, str(ROOT / "strata.py"), *args]
def warn_wsl_amd(self) -> None:
"""Under WSL the Linux AMD engines hang (no /dev/kfd, librocdxg never completes a GPU command): say so plainly with one next step,
and go on (a warning, never a refusal)."""
try:
from strata_control import engine_component as ec
note = ec.wsl_amd_note("hip") and ec.wsl_amd_note(ec.detect_backend()[0]) # the cheap file test first: no GPU tool is run on other systems
except Exception: # noqa: BLE001 - a nicety, never a reason to stop
return
if note:
self.emit("engine", note, level="warn")
if note not in self.notices:
self.notices.append(note)
# ---- 1. the engine
def ensure_engine(self) -> None:
self.warn_wsl_amd()
if self.multi_file(): # DeepSeek: its own engine package (setup fetches it); the Strata engine is not used
self.emit("engine", "DeepSeek runs on its own engine package, so the Strata engine is not downloaded for it.", 100)
return
self.emit("engine", "Checking the engine for this computer…")
from strata_control import engine_component as ec
root = Path(self.components_dir) if self.components_dir else None
report = ec.status(root)
stale = None
if report.get("usable"):
eng = report["engine"]
stale = self.stale_engine(eng.get("version"))
if not stale:
self.emit("engine", f"Engine {eng['version']} ({eng['backend']}) is installed.", 100)
return
feed = self.feed or os.environ.get("STRATA_UPDATE_FEED")
if stale:
self.emit("engine", stale + " Looking for a newer one…", 0, level="warn")
else:
self.emit("engine", "No engine is installed yet. Getting the one for your graphics card…", 0)
argv = self._cli("update", "--no-control", "--output", "jsonl")
if feed:
argv += ["--feed", feed]
if self.components_dir:
argv += ["--components-dir", self.components_dir]
env = self._env()
for key, value in self.s.extra.items(): # the engine's own environment settings (device selectors, ...) apply to its test start too
if not key.startswith("--") and value.lower() not in ("off", "unset", "none"):
env[key] = value
p = self._spawn(argv, env=env)
last = None
tail: list[str] = []
for line in p.stdout: # type: ignore[union-attr]
self.check_cancel()
line = line.strip()
if not line:
continue
try:
ev = json.loads(line)
except ValueError:
tail.append(line)
continue
if ev.get("event") == "progress":
self.emit("engine", f"Downloading the engine… {ev.get('percent', 0)}%", float(ev.get("percent", 0)))
elif ev.get("event") in ("phase", "waiting"):
if ev.get("phase") == "warn":
self.emit("engine", str(ev.get("message") or ""), level="warn")
else:
self.emit("engine", str(ev.get("message") or ""))
last = ev
code = p.wait()
if stale:
# an engine is there and was too old: warn, never refuse (round 8, M: 0.1.45.3 was reused although 0.1.47 existed, and Gyro-S failed
# on kernels only the newer engine has). Whatever the update did, say where it stands.
after = ec.status(root)
now = (after.get("engine") or {}).get("version") if after.get("usable") else None
still = self.stale_engine(now) if now else stale
if still:
self.notices.append(still + " Run `strata update`, then start it again.")
self.emit("engine", still + (" The update did not bring a newer one." if code == 0 else " The update did not finish.") +
" Run `strata update`, then start it again.", 100, level="warn")
else:
self.emit("engine", f"Engine {now} is installed.", 100)
return
if code == 0:
# `strata update` exits 0 when the feed simply has no engine for this PC (or only one for another card family). That is not an
# engine: refuse here, before the model (tens of GB) is touched.
after = ec.status(root)
if not after.get("usable"):
self.no_engine(self._no_engine_words(last, after))
return
if code != 0:
why = ""
if isinstance(last, dict):
why = str((last.get("error") or {}).get("message") or last.get("message") or "")
if not why:
for res in last.get("results") or []:
if isinstance(res, dict) and res.get("status") == "failed" and res.get("error"):
why = str(res["error"])
break
why = why or (tail[-1] if tail else "")
why = capital(why) # (joined after a sentence: "the update feed could not be read ..." starts a new one)
raise LaunchError("The engine could not be installed." + (f" {why}" if why else ""),
"Check your internet connection, or point Strata at an update source with --feed, then try again.", 6)
self.emit("engine", "The engine is installed.", 100)
def needed_engine(self) -> tuple[int, ...] | None:
"""The oldest engine this model runs on, from setup.py's own tables (MIN_ENGINE for everything, GYRO_ENGINE for Gyro), or None."""
try:
mod = self.catalog.strata.setup_module()
need = tuple(getattr(mod, "MIN_ENGINE", ()) or ())
if self.s.family == "gyro":
need = max(need, tuple(getattr(mod, "GYRO_ENGINE", ()) or ()))
if sys.platform == "darwin":
need = max(need, GYRO_METAL_ENGINE) # the Metal Gyro kernels (TQK6/TQK7/TQ2_T) first shipped in 0.1.47; CUDA, HIP, SYCL had them earlier
return need or None
except Exception: # noqa: BLE001 - no table, no check
return None
def stale_engine(self, version: str | None) -> str | None:
"""A plain sentence when the installed engine is older than the model needs, else None."""
have = version_tuple(version)
need = self.needed_engine()
if not have or not need or have >= need:
return None
return f"The engine installed here ({version}) is older than this model needs ({'.'.join(map(str, need))} or newer)."
def no_engine(self, reason: str) -> None:
"""There is no engine for this computer and none to get. This is a warning, not a refusal: the model may still be downloaded, because
the engine can be installed afterwards. Ask first (the answer is yes unless the person says no; --yes skips the question) when a
download is about to happen; otherwise just remember the problem for the final message."""
self.engine_problem = reason
warning = no_engine_warning(reason)
self.emit("engine", warning, 100, level="warn")
if self.assume_yes or self.confirm is None:
return
try:
gb = self.download_gb()
except Exception: # noqa: BLE001
gb = 0.0
if gb < 0.05:
return # nothing would be downloaded: no question, only the final message
if not self.confirm("No engine for this computer yet", warning + f" Download the model now (about {gb:.0f} GB)?"):
raise LaunchError("Stopped. Nothing was downloaded.", "Install the engine first, or run this again and answer yes.", 130)
@staticmethod
def _no_engine_words(last: object, report: dict) -> str:
"""The one plain sentence for "no usable engine for this PC": the update's own note (the wrong-card-family line, or "the feed has no
engine package for this PC"), else the engine report's plain notice. Never the raw list of reasons."""
note = ""
if isinstance(last, dict):
for res in last.get("results") or []:
if isinstance(res, dict) and res.get("component") == "engine":
note = str(res.get("note") or "").strip()
if not note:
note = str(report.get("notice") or "").strip()
if "backend" in report and report["backend"] is None: # not even a graphics card Strata knows: say that, not "the feed has nothing"
names = []
try:
from strata_control import engine_component as ec
names = [a.get("name", "") for a in ec.windows_display_adapters()] if os.name == "nt" else []
except Exception: # noqa: BLE001 - the names are a nicety
pass
return no_gpu_words(names)
if not note or note.startswith("No engine for this PC was found"):
note = "There is no engine for this computer's graphics card in the update source."
return note
# ---- 2. the model
def resolved_vision(self) -> str:
"""The picture setting of this launch: the chosen one, or (nothing chosen) the default for this model and engine (catalog.picture_default)."""
s = self.s
if s.vision is None:
row = next((r for r in self.catalog.rows() if (r.family, r.model) == (s.family, s.model)), None)
s.vision = self.catalog.picture_default(bool(row.images) if row is not None else False)
return s.vision
def vision_word(self) -> str:
"""What setup is told for the picture choice. "on" is setup's "yes": setup (Control mode) picks the CPU encoder on a card under 24 GB
and the GPU encoder from 24 GB (any make of card), and downloads the pinned picture-reader file (mmproj) with the model."""
s = self.s
self.resolved_vision()
if s.vision == "off":
return "no"
return "yes" if s.vision == "on" else s.vision
def check_pictures(self, path: Path) -> None:
"""Pictures were asked for: say so in plain words when the installed settings still have them off."""
s = self.s
if s.vision == "off":
return
try:
desc = self.catalog.strata.describe_config(path)
except Exception: # noqa: BLE001
return
if (desc or {}).get("images") in (None, "off"):
note = ("Pictures could not be switched on: this engine does not carry the picture reader, or this model cannot read pictures, "
"so it will answer text only.")
self.notices.append(note)
self.emit("model", note, 100, level="warn")
def setup_args(self) -> list[str]:
cat, s = self.catalog, self.s
fam, model = s.family, s.model
try: # a saved preset or choices file keeps the lower-case tag ("iq3_xxs", #4209), while setup's
fam, model = cat.resolve(str(model), None if fam is None else str(fam)) # --model takes the table's own key
except Exception: # noqa: BLE001 a name the list does not hold, or a list that cannot be read here, is setup's
pass # to report: passing the values on unchanged keeps the launch behaving as it did before
args = ["--yes", "--no-start", "--family", str(fam), "--model", str(model)]
ctx = s.context
if ctx:
args += ["--context", str(ctx)]
args += ["--vision", self.vision_word()]
if s.kv:
args += ["--kv", s.kv]
if fam in ("qwen", "coder"):
args += ["--experimental-speed-projection", "off"]
mtp_word = modelstate.mtp_choice(s.mtp, s.drafting)
if mtp_word in ("on", "off") and fam in modelstate.HELPER_FAMILIES:
args += ["--mtp", mtp_word] # round 8, N3: "--mtp off" never reached setup, which then fetched the 5.2 GB draft layer
data = s.data_dir or self.existing_data_dir()
if data:
args += ["--data-dir", str(data)]
if self.move_data and s.data_dir: # only when the person said so: setup moves nothing otherwise
args.append("--move-data")
if s.data_dir: # a folder the person chose is remembered for the next start and for the app
args.append("--set-default-data-dir")
if s.model_path:
p = Path(s.model_path)
args += ["--gguf-dir", str(p if p.is_dir() else p.parent)]
if s.dsv41_package and s.family == "deepseek41":
args += ["--dsv41-package", s.dsv41_package]
if self.feed: # --feed / STRATA_UPDATE_FEED: setup fetches the DeepSeek engine from it too
args += ["--feed", str(self.feed)]
if s.gpu_roles:
args += ["--gpu-roles", ",".join(f"{k}={v}" for k, v in sorted(s.gpu_roles.items(), key=lambda kv: int(kv[0])))]
elif s.gpus:
args += ["--gpus", ",".join(str(g) for g in s.gpus)] if len(s.gpus) > 1 else ["--gpu", str(s.gpus[0])]
else:
args += self.installed_card_args()
if sys.platform == "darwin":
# a Mac's setup (Control mode, strata_control/mac_fit.py) takes fewer options (no --kv, --gpu, speed projection)
keep = {"--yes", "--no-start", "--family", "--model", "--context", "--vision", "--data-dir", "--gguf-dir", "--move-data",
"--set-default-data-dir", "--feed", "--dsv41-package"} # (the last two: the DeepSeek engine package, 0.2.7.1)
flags = ("--yes", "--no-start", "--move-data", "--set-default-data-dir")
out, i = [], 0
while i < len(args):
if args[i] in keep:
out.append(args[i])
if args[i] not in flags and i + 1 < len(args):
out.append(args[i + 1])
i += 2 if args[i] not in flags else 1
else:
i += 2 if i + 1 < len(args) and not args[i + 1].startswith("--") else 1
args = out
return args
def installed_card_args(self) -> list[str]:
"""--gpu-roles / --gpus of the installed config when the launch names no cards itself. A change of context or pictures runs setup again;
without these it picked the cards and the Main/Helper roles afresh and reset an explicit choice (round 8, F3: 0=main,1=helper
came back as 0=helper,1=main). Only a choice that was made ("gpus_asked" / roles) is forwarded, never the automatic pick."""
try:
path, _desc = self.installed_state()
base = read_config(path) if path is not None else None
if not isinstance(base, dict):
return []
roles = base.get("gpu_roles")
if isinstance(roles, dict) and roles:
return ["--gpu-roles", ",".join(f"{k}={v}" for k, v in sorted(roles.items(), key=lambda kv: int(kv[0])))]
try:
usable_now = self.catalog.computer().usable_gpus
except Exception: # noqa: BLE001
usable_now = None
cards = installed_cards(base, usable_now)
if cards:
return ["--gpus", ",".join(str(g) for g in cards)] if len(cards) > 1 else ["--gpu", str(cards[0])]
except Exception: # noqa: BLE001 - never block a launch over remembering the cards
pass
return []
def installed_state(self) -> tuple[Path | None, dict | None]:
tag = self.catalog.tag_of(self.s.family, self.s.model)
path = self.catalog.installed_config(tag)
if path is None:
return None, None
desc = self.catalog.strata.describe_config(path)
return path, desc
def installed_serves_request(self, path: Path) -> bool:
"""C08: the installed copy is what this launch asked for. A --model-path (or the Folder step's own files) and a --data-dir always win:
when the installed config names files outside them, it is not reused (that served another copy without a word) and setup runs with
the given folders, which rewrites the config so the profile follows."""
s = self.s
if not (s.model_path or s.data_dir):
return True
try:
cfg = read_config(path)
args = [str(a) for a in cfg.get("args") or []]
if s.model_path:
want = Path(s.model_path).expanduser().resolve()
folder = want if want.is_dir() else want.parent
ggufs = [Path(a) for a in args if a.lower().endswith(".gguf")]
if not ggufs or not all(_inside(g.resolve(), folder) for g in ggufs):
return False
if s.data_dir:
packs = [Path(args[i + 1]) for i, a in enumerate(args[:-1]) if a == "--pack"]
return all(_inside(pk.resolve(), Path(s.data_dir).expanduser().resolve()) for pk in packs)
return True
from strata_control import discovery
found = discovery.footprint(cfg, path).data_dir
return found is not None and os.path.normcase(str(Path(found).resolve())) == os.path.normcase(str(Path(s.data_dir).expanduser().resolve()))
except Exception: # noqa: BLE001 - when in doubt do what was asked: run setup with the given folders
return False
def existing_data_dir(self) -> Path | None:
"""The data folder to give setup when the person chose none: the folder the model already lives in (its run config names its files),
else the folder the desktop app uses, so a helper file that is missing is fetched next to the rest, never into a second folder.
None: setup's own default is right."""
s = self.s
try:
if s.model_path:
from strata_control import discovery
first = next(iter(sorted(Path(s.model_path).glob("*.gguf"))), None) if Path(s.model_path).is_dir() else Path(s.model_path)
found = discovery.prepared_beside(first)["data_dir"] if first else None
return Path(found) if found else None
if s.family and s.model:
path = self.catalog.installed_config(self.catalog.tag_of(s.family, s.model))
if path is not None:
from strata_control import discovery
found = discovery.footprint(read_config(path), path).data_dir
if found is not None:
return found
found, source = self.catalog.data_dir_choice()
return found if source != "default" else None
except Exception: # noqa: BLE001
return None
def model_state(self):
"""What this launch still needs for the model, in numbers and words (strata_cli/launch/modelstate.py)."""
from strata_cli.launch import modelstate
s = self.s
self.resolved_vision()
path, desc = (None, None)
if s.family and s.model and not s.model_path:
path, desc = self.installed_state()
return modelstate.assess(self.catalog, s.family, s.model, vision=s.vision, model_path=s.model_path, data_dir=s.data_dir,
config=path, desc=desc, mtp=s.mtp, drafting=s.drafting)
def download_gb(self) -> float:
"""How much would be downloaded for this launch right now: 0 when everything is on disk (the model is installed with these choices
or is the person's own files). A model whose helper files are missing counts only those files, never the whole model."""
s = self.s
if not (s.family and s.model) and not s.model_path:
return 0.0
try:
return float(self.model_state().remaining_gb)
except Exception: # noqa: BLE001
return 0.0
def start_wait(self) -> int:
"""How long the start waits for the model to be ready (#3949 #3459): 10 minutes, more for a big model, which a slow disk needs
longer to read (about 8 more seconds per GB above 40 GB, at most 30 minutes). Automatic: nobody sets a variable for it."""
gb = 0.0
try:
row = next((r for r in self.catalog.rows() if r.family == self.s.family and r.model == self.s.model), None)
gb = float(getattr(row, "download_gb", 0) or 0)
except Exception: # noqa: BLE001
gb = 0.0
return int(min(runtime_max_wait(), max(600, 600 + (gb - 40.0) * 8.0)))
def download_question(self) -> tuple[str, str]:
"""(title, text) of the question before a download starts, naming exactly what is fetched."""
return self.model_state().question()
#: seconds of silence from setup before a "still working" line (packing a 40 GB model prints nothing for minutes)
HEARTBEAT_S = 30.0
def _read_lines(self, p: subprocess.Popen):
"""The lines setup prints, and None whenever a second passes with none, so a silent step can say it is still going and a cancel is
seen at once."""
import queue
q: queue.Queue = queue.Queue()
def pump() -> None:
try:
for ln in p.stdout: # type: ignore[union-attr]
q.put(ln)
finally:
q.put(_END)
threading.Thread(target=pump, daemon=True).start()
while True:
try:
ln = q.get(timeout=min(1.0, max(0.05, self.HEARTBEAT_S / 2)))
except queue.Empty:
yield None
continue
if ln is _END:
return
yield ln
def progress_job(self) -> dict:
"""What the download figure is counted over: the folder setup writes the model into (the data folder it is GIVEN, which may be the
person's --data-dir and not the default one) and every file of the install with the catalog's total, so a model in several shards
shows 'of 66.4 GB' from the start and not the size of the file being fetched."""
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(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()
if setup_module is not None and not s.model_path:
job["files"] = cat.mcp.expected_files(setup_module, str(s.family), str(s.model), models_dir, self.vision_word() != "no")
first = next((f for f in job["files"] if f.get("role") == "model"), None)
if first: # the folder setup names it (Q2_0), not the lower-case run-config tag (q2_0)
job["folder"] = str(Path(first["path"]).parent)
except Exception: # noqa: BLE001 - progress is a convenience; the folder scan still counts
pass
return job
def ensure_model(self) -> Path:
s, cat = self.s, self.catalog
self.check_own_files()
self.resolved_vision()
if not (s.family and s.model):
raise LaunchError("No model was chosen.", "Choose a model, or use --model NAME (strata launch --help lists them).", 2)
path, desc = self.installed_state()
if path is not None and desc is not None and desc_matches(desc, s) and not files_present(read_config(path)) \
and not self.dsv41_engine_missing(read_config(path)):
if self.installed_serves_request(path):
self.emit("model", f"{s.family} {s.model} is already installed with these choices.", 100)
return path
self.emit("model", "The model already installed here is somewhere else than the folder you gave, so Strata uses the folder you gave "
"and points this launch at it.", 0)
title = f"{s.family} {s.model}"
if sys.platform == "darwin" and not (cat.src / "strata_control" / "mac_fit.py").is_file():
# a Terminal from before the Mac rules moved into strata_control/mac_fit.py: setup.py cannot install a Mac model in its Control
# mode (no source build, no setup_mac.py), so use model files prepared on this Mac. Every other Terminal installs through setup.py.
adopted = self.adopt_prepared_files("no Mac model installer in this Terminal")
if adopted is not None:
return adopted
self.emit("model", f"Preparing {title} from your model files…" if s.model_path else
f"Getting {title} ready (a download may be large; it continues where it stopped if interrupted)…", 0)
argv = [self.python, str(cat.src / "setup.py"), *self.setup_args()]
env = self._env()
env.update(STRATA_CONTROL_INSTALL="1", STRATA_INSTALL_DIR=str(cat.root), PYTHONNOUSERSITE="1")
env.pop("STRATA_NEED_ENGINE", None)
if self.engine_problem:
env["STRATA_ALLOW_NO_ENGINE"] = "1" # the person said go on: setup warns about the engine instead of stopping
elif not self.multi_file(): # (DeepSeek does not use the Strata engine: nothing to require)
env["STRATA_NEED_ENGINE"] = "1"
for k in ("PYTHONHOME", "PYTHONPATH", "VIRTUAL_ENV"):
env.pop(k, None)
env["PYTHONPATH"] = str(cat.src)
p = self._spawn(argv, env=env, cwd=str(cat.src))
lines: list[str] = []
already: bool | None = None # all the model's files were on disk before this run wrote anything
job = self.progress_job()
step_seen, step_since, quiet_since, said = None, time.monotonic(), time.monotonic(), None
for line in self._read_lines(p):
if self.cancel.is_set():
self._stop_children()
raise Cancelled()
if line is None: # nothing printed for a while: the long steps (packing, converting) are silent
if said and said[1] != "download" and time.monotonic() - quiet_since >= self.HEARTBEAT_S:
quiet_since = time.monotonic()
self.emit("model", f"{said[0]} (still working, {_elapsed(time.monotonic() - step_since)} so far)", None, step=step_seen)
continue
quiet_since = time.monotonic()
line = line.rstrip()
if not line:
continue
lines.append(line)
lines[:] = lines[-1500:]
note = deepseek_engine_note(line)
if note and note[1] not in self._engine_notes: # setup's DeepSeek engine lines are shown, not condensed away (0.2.5 list)
self._engine_notes.add(note[1])
if note[0] == "warn":
self.notices.append(note[1])
self.emit("model", note[1], None, level=note[0], step=step_seen)
prog = cat.mcp.install_progress(lines)
prog = cat.mcp.combined_progress(prog, job)
dl, phase, label = prog.get("download"), prog.get("phase"), prog.get("label")
if prog.get("step") != step_seen:
step_seen, step_since = prog.get("step"), time.monotonic()
if label and not dl:
said = (label, phase)
else:
said = None
if dl and dl.get("total_gb", 0) >= 0.05 and not (s.model_path and phase == "download"):
if phase == "download" and already is None:
already = float(dl["percent"]) >= 99.5
text = download_text(dl, bool(already) and phase == "download", label)
self.emit("model", text, float(dl["percent"]), step=prog.get("step"))
elif label:
self.emit("model", label, None, step=prog.get("step"))
elif prog.get("step"):
self.emit("model", f"Setup step {prog['step']}", None, step=prog.get("step"))
code = p.wait()
if code != 0:
err = ""
for line in reversed(lines):
if line.strip().startswith("[X]"):
err = line.strip()[3:].strip()
break
hint = ""
if err:
for i, line in enumerate(lines):
if err in line and i + 1 < len(lines) and lines[i + 1].startswith(" "):
hint = lines[i + 1].strip()
adopted = self.adopt_prepared_files(err)
if adopted is not None:
return adopted
raise setup_failure(lines, err, hint, self.keep_setup_output(lines), bool(s.model_path))
path, _desc = self.installed_state()
if path is None:
raise LaunchError("Setup finished but no run config was written for this model.",
"Run `strata launch` again; if it repeats, the setup output above says why.", 6)
self.emit("model", f"{title} is ready.", 100)
self.say_reused_data(path)
self.check_pictures(path)
return path
def say_reused_data(self, path: Path) -> None:
"""C08 note: your own model files are used where they are, and when the prepared pack or the draft layer are read from another data folder
than the one given, one line says where from."""
s = self.s
if not (s.model_path and s.data_dir):
return
try:
args = [str(a) for a in read_config(path).get("args") or []]
want = Path(s.data_dir).expanduser().resolve()
elsewhere = []
for flag, label in (("--pack", "the prepared pack"), ("--mtp", "the draft layer")):
if flag in args and args.index(flag) + 1 < len(args):
value = args[args.index(flag) + 1]
if value.isdigit():
continue
place = Path(value).expanduser().resolve()
if not _inside(place, want):
elsewhere.append((label, place))
if elsewhere:
folders = sorted({str(pl.parent.parent if pl.parent.name in ("packs", "mtp") else pl.parent) for _l, pl in elsewhere})
text = (f"{' and '.join(l for l, _p in elsewhere).capitalize()} {'is' if len(elsewhere) == 1 else 'are'} read from "
f"{', '.join(folders)}, not from {want}; nothing was copied.")
self.notices.append(text)
self.emit("model", text, 100)
except (OSError, ValueError):
pass
def keep_setup_output(self, lines: list[str]) -> str | None:
"""The last lines setup.py printed, kept next to the launch's other logs so a failed install can be read afterwards (the message
shows one line; a traceback needs the rest). Best effort: None when it cannot be written."""
try:
folder = launch_dir()
folder.mkdir(parents=True, exist_ok=True)
path = folder / f"{self.name}.setup.log"
path.write_text("\n".join(lines[-400:]) + "\n", encoding="utf-8", errors="replace")
return str(path)
except (OSError, ValueError):
return None
def adopt_prepared_files(self, setup_error: str) -> Path | None:
"""The installer cannot set this graphics card up (Intel Arc, for one): if the model's files and prepared pack are already on disk,
write the run config here (strata_cli/launch/adopt.py); else say what is missing."""
from strata_cli.launch import adopt
from strata_control import engine_component
s, cat = self.s, self.catalog
if not adopt.setup_cannot_handle_this_card(setup_error):
return None
_models, families, _ctx, _src = cat.tables()
fam = families.get(s.family or "", {})
tag = cat.tag_of(s.family, s.model)
data_dir = Path(s.data_dir) if s.data_dir else cat.data_dir()
model_dir = Path(s.model_path) if s.model_path else data_dir / "models" / (fam.get("tag", "") + s.model)
if model_dir.is_file():
model_dir = model_dir.parent
try:
backend = engine_component.detect_backend()[0]
except Exception: # noqa: BLE001
backend = None
cfg, problems = adopt.plan(family=s.family, model=s.model, model_dir=model_dir, data_dir=data_dir, src=cat.src,
context=s.context or 32768, kv=s.kv, model_name=f"{fam.get('name', s.family)}-{s.model.lower()}", family_def=fam,
log=cat.root / f"strata-{tag}.log", backend=backend,
vram_gb=float(cat.computer().main_vram_gb or 0), notes=self.notices, pack_name=tag.lower())
if cfg is None:
raise LaunchError("The installer cannot set up this graphics card yet, and the files Strata would need to write the settings itself are not all there: "
+ "; ".join(problems) + ".",
"Use model files that were prepared on this computer already (folder + pack), or install on a computer with an NVIDIA or AMD card.", 5)
path = cat.root / f"strata-{tag}.json"
adopt.write(path, cfg)
self.notices.append("The downloader built into this Terminal sets up NVIDIA and AMD cards only (Intel Arc cards have a separate installer), so Strata wrote the "
f"run settings itself from the prepared model files already on this computer ({path.name}). Nothing was downloaded.")
self.emit("model", f"Used your prepared model files and wrote the run settings ({path.name}); this card is not one the built-in downloader sets up.", 100, level="warn")
return path
@staticmethod
def deepseek_set_in(mod, model_path, prefer: str | None = None):
"""(set name, its catalog entry, the folder) when `model_path` (a folder, the folder above the set's own folder, or one shard) holds shards
of an official DeepSeek-V4.1-Flash set - recognised by the pinned shard names (the first release's names too), never by the generic GGUF
fingerprint table, which does not know DeepSeek (its shard 8 is a table, not a layer: "could not be read", #3914) - else None."""
fam = getattr(mod, "MULTI_FILE_FAMILIES", {}).get("deepseek41")
if fam is None or not model_path:
return None
p = Path(model_path)
legacy = getattr(mod, "DSV41_LEGACY_NAMES", {})
order = ([prefer] if prefer in fam["models"] else []) + [m for m in fam["models"] if m != prefer]
for m in order:
d = fam["models"][m]
subs = [d.get("subdir"), d.get("legacy_subdir"), m]
folders = [p, *[p / x for x in subs if x]] if p.is_dir() else [p.parent]
names = [n for s in d["shards"] for n in (s, legacy.get(s)) if n] + list(d.get("shards6") or []) # (+ the six-shard layout of 2026-10-27)
folder = next((f for f in folders if any((f / n).exists() for n in names)), None)
if folder is not None:
return m, d, folder
return None
def deepseek_files(self) -> bool:
"""--model-path names DeepSeek-V4.1-Flash's shards (a folder, the repository's folder with the set's own folder inside - UD-Q8_K_XL,
2.00bpw, 2.25bpw - or one shard): they are that model, whatever the fingerprint table of the Flash-Next files says. True when it did
(s.family / s.model / s.model_path set); a shard missing from the folder is said now, in plain words, before an engine or a download
is touched."""
s = self.s
try:
mod = self.catalog.strata.setup_module()
fam = getattr(mod, "MULTI_FILE_FAMILIES", {}).get("deepseek41")
if fam is None or not s.model_path:
return False
found = self.deepseek_set_in(mod, s.model_path, s.model)
except Exception: # noqa: BLE001 - not DeepSeek's: the usual check goes on
return False
if found is None:
return False
model, d, folder = found
from strata_control import dsv41_fit
if dsv41_fit.not_here(model): # Mac: no Metal IQ1_S kernel yet; DGX Spark: never run; said before an engine or a download is touched
raise LaunchError(dsv41_fit.not_here(model),
"Use the 2.00 or 2.25 files (--model ist-das-2.00bpw or --model ist-das-2.25bpw, or --model-path to those files). "
"Nothing was downloaded or changed.", 2)
if s.family and s.family != "deepseek41":
raise LaunchError(f"These are DeepSeek-V4.1-Flash's files, not {s.family} {s.model}'s.",
"Leave --model out, or use --model " + model + ".", 2)
_layout, others, missing = mod.dsv41_parts(d, lambda n: (folder / n).exists()) # the pinned four-shard layout, or ISTA's six-shard one (0.2.8.1)
if missing:
parts = {4: "four", 5: "five", 9: "nine"}.get(len(others), str(len(others)))
raise LaunchError(f"DeepSeek-V4.1-Flash {d.get('label', model)} is in {parts} parts and {len(missing)} of them are not in {folder} "
f"(first missing: {missing[0]}).",
"Put every part in the folder (or drop --model-path and let Strata download them). "
"Nothing was downloaded or changed.", 5)
s.family, s.model, s.model_path = "deepseek41", model, str(folder)
self.emit("model", f"These files are DeepSeek-V4.1-Flash {model}; they are used where they are.", None)
return True
def check_own_files(self) -> None:
"""The person's own model files, looked at once: a path that is not there or holds no model is said in plain words first, and the
files are matched to the catalog model (resolve_own_files). Nothing else is done with a path that was already checked."""
if self.s.model_path and not self._own_checked:
self.resolve_own_files()
self._own_checked = True
def resolve_own_files(self) -> None:
""""Use model files I already have": the fingerprint table says which catalog model the files are, and which folder holds them (the
person may have named the folder above it). When the person also named the model (--model), the files are never refused here: setup
takes the files of that model out of the folder by name, as it always did (0.1.45), and says what is wrong when it cannot."""
from strata_control import models
s = self.s
problem = models.own_path_problem(s.model_path)
if problem: # before the engine or setup are touched, with the path as typed
raise LaunchError(problem[0], problem[1], 2)
if self.deepseek_files():
return
given = bool(s.family and s.model)
try:
plan = models.plan_import(models.absolute(s.model_path))
except Exception as e: # noqa: BLE001
if given:
return
raise LaunchError(f"These model files cannot be used: {e}.", "Pick the folder with every shard of one model, or a .gguf file of it.", 5) from None
if given:
sel = plan.get("candidate_selection") or {}
if sel.get("family") and sel.get("model") and (sel["family"], sel["model"]) != (s.family, s.model):
# round 8, N8: --family swift was accepted for an IQ3_XXS file. The files are not refused (setup takes the named model's files
# out of the folder by name), but the person is told what the file header says.
words = (f"These files look like {sel['family']} {sel['model']} (by the file headers), not {s.family} {s.model}. "
f"Setup will look for {s.family} {s.model} files in that folder and may stop; to use these files, leave out --family and --model.")
self.identity_notes.append(words)
self.notices.append(words)
hold = [h for h in plan.get("folder_holds") or [] if (h.get("family"), h.get("model")) == (s.family, s.model)]
if len(hold) == 1 and hold[0].get("folder"):
s.model_path = hold[0]["folder"]
elif not plan.get("issues"):
s.model_path = models.import_target(s.model_path, plan)
return
sel = plan.get("candidate_selection") or {}
fam, model = sel.get("family"), sel.get("model")
if not (fam and model) or plan.get("issues"):
issues = models.problem_words(plan.get("issues") or [], missing=plan.get("missing_shards")) or "they are not a model Strata knows"
more = " " + next((m for m in plan.get("messages") or [] if "holds" in m), "")
raise LaunchError(f"These files cannot be used as they are ({issues}).{more.rstrip()}",
"Choose the matching model in the list, or check that every shard is in the folder.", 5)
s.family, s.model = fam, model
s.model_path = models.import_target(s.model_path, plan)
self.emit("model", f"These files are {fam} {model}.", None)
# ---- 3. config and profile
def drafting_inputs(self, cfg: dict) -> dict:
"""What strata_control/drafting.py needs to decide for this launch: the choice, the card class, the model and the data folder."""
from strata_control import drafting, engine_component
try:
archs = list(engine_component.cuda_archs_present()) + list(engine_component.hip_archs_present())
except Exception: # noqa: BLE001 - an unknown card is the "other" class
archs = []
mode = self.s.drafting if self.s.drafting is not None else cfg.get("drafting")
return {"mode": mode, "klass": drafting.card_class(archs), "model_name": cfg.get("model_name"), "family": self.s.family,
"data_dir": drafting.data_dir_of(cfg.get("args") or []) or (Path(self.s.data_dir) if self.s.data_dir else None)}
def ensure_drafter(self, base_path: Path) -> None:
"""Fetch the DFlash2 drafter when this launch will use it and it is not here yet. Never stops the launch: if it cannot be had, the model
starts with the MTP and the person is told (recommend, never force)."""
from strata_control import drafting, drafting_fetch
try:
want = self.drafting_inputs(read_config(base_path))
if want["data_dir"] is None or not drafting.needs_pack(**want):
return
self.emit("model", "Downloading the DFlash2 drafter (1.2 GB, once)…", 0)
def show(step: str, done: int, total: int) -> None:
self.check_cancel()
self.emit("model", f"{step} {done / 1e9:.2f} of {total / 1e9:.2f} GB" if total > 1 and done < total else step,
100.0 * done / total if total > 0 else None)
drafting_fetch.ensure(want["data_dir"], progress=show, cancel=self.cancel)
except Cancelled:
raise
except Exception as e: # noqa: BLE001 - the MTP still runs
text = f"{e} The model starts with the MTP instead." if isinstance(e, drafting_fetch.DraftingError) else \
"The DFlash2 drafter could not be downloaded. The model starts with the MTP instead."
self.notices.append(text)
self.emit("model", text, None, level="warn")
def build_config(self, base_path: Path) -> Path:
from strata_control import tune_knobs
base = read_config(base_path)
cfg = derive_config(base, self.s, bool_flags=tune_knobs.BOOL_FLAGS,
log=str(launch_dir() / f"{self.name}.engine.log"), gpus=cards_for_launch(self.s, self.catalog, base=base))
if self.s.gpus and not self.s.gpu_roles: # a DeepSeek model on AMD keeps the card the person names from now on (strata_control/gpu_pick.py)
from strata_control import gpu_pick
gpu_pick.remember(base_path, [int(g) for g in self.s.gpus])
self.apply_four_card(cfg)
self.use_own_dsv41_engine(cfg)
apply_experts(cfg, self.experts_flag(cfg.get("exe"))) # after the engine is settled: only a 0.1.50+ engine is given --experts
wait = self.start_wait()
if wait > 600 and not cfg.get("engine_ready_s"): # the server's own wait for the engine (900 s) must not end before the start's
cfg["engine_ready_s"] = wait + 60
out = launch_dir() / f"{self.name}.json"
write_private(out, cfg)
self.config_path = out
return out
def apply_four_card(self, cfg: dict) -> None:
"""Four Radeon AI PRO R9700 cards and Qwen3.8-Flash-Next IQ3_S: the measured serving profile (strata_control/amd_four_card.py), filled
in where the person chose nothing. Never stops a launch."""
self.four_card_parallel = None
try:
from strata_control import amd_four_card
picked = cfg.get("gpu")
if not (isinstance(picked, list) and len(picked) == amd_four_card.CARDS):
return
want = {int(i) for i in picked}
cards = [c for c in self.catalog.computer().usable_gpus if int(c.get("index", -1)) in want]
before = cfg.get("parallel")
notes = amd_four_card.apply(cards, cfg, parallel_given=bool(self.s.parallel))
if notes:
self.four_card_parallel = cfg.get("parallel") if before is None and cfg.get("parallel") else None # (only a number the profile set)
self.emit("model", "Four " + str(cards[0].get("name", "AMD")).replace("AMD Radeon ", "") + " cards: " + "; ".join(notes) + ".", None)
except Exception: # noqa: BLE001 - a profile is never a reason to stop a launch
self.four_card_parallel = None
def ensure_profile(self, config: Path) -> None:
from strata_control import profiles
name = self.profile
parallel = self.s.parallel if self.s.parallel else (getattr(self, "four_card_parallel", None) or self.catalog_parallel())
sets = [f"host={acc.host_for(self.s.access)}", f"port={int(self.s.port)}"] + ([f"parallel={parallel}"] if parallel else [])
try:
current, _rev = profiles.load(name)
except profiles.ProfileError:
profiles.create(name, str(config))
else:
if current.get("source_config") != str(config.resolve()):
proposed, rev = profiles.propose_source(name, str(config))
profiles.save(name, proposed, expected_revision=rev)
# the overrides are replaced as a set: a launch's host/port/parallel are exactly these
cur, rev = profiles.load(name)
want = {k: v for k, v in (profiles.parse_assignment(x) for x in sets)}
if cur.get("overrides") != want:
profiles.save(name, {**cur, "overrides": want}, expected_revision=rev)
def multi_file(self) -> bool:
"""The chosen model is DeepSeek-V4.1-Flash (setup.py MULTI_FILE_FAMILIES): nine shards, its own engine; no tuning and no parallel slots."""
try:
return bool(self.s.family and self.catalog.strata.multi_family(self.s.family))
except Exception: # noqa: BLE001
return False
def catalog_parallel(self) -> int | None:
if self.multi_file(): # the DeepSeek engine answers one request at a time: no --batch to pass
return None
try:
comp = self.catalog.computer()
return S.fit_parallel(comp.main_vram_gb) if comp.usable_gpus else None
except Exception: # noqa: BLE001
return None
# ---- 4. tuning
def tune_deepseek(self) -> None:
"""DeepSeek-V4.1-Flash has its own short tuner (tools/dsv41/tune.py: the drafter on or off, the CPU threads; the RAM store sizes itself):
a few test runs of the engine, a result written into this launch's config, and the standard settings on any failure."""
s = self.s
cfg = getattr(self, "config_path", None)
if cfg is None:
self.emit("tune", "There are no DeepSeek settings to tune yet, so it starts with the standard ones.", 100, level="warn")
return
mode = "thorough" if s.tune == "thorough" else "quick"
argv = [sys.executable, str(Path(__file__).resolve().parents[2] / "tools" / "dsv41" / "tune.py"), "--config", str(cfg), "--mode", mode,
"--output", "json"]
self.emit("tune", "Tuning DeepSeek: it times a short question with your standard settings, then with the DSpark drafter on (and, if that helps, a shorter "
"draft block) and the CPU threads set to your physical cores; each run loads the model once. It keeps only a clear gain. Close other heavy programs.", 0)
try:
p = self._spawn(argv)
except Exception as e: # noqa: BLE001 - a tune never stops a launch
self.emit("tune", f"The DeepSeek tuner could not start ({type(e).__name__}), so the standard settings run.", 100, level="warn")
return
lines: list[str] = []
reader = threading.Thread(target=lambda: lines.extend(p.stdout.readlines()), daemon=True) # type: ignore[union-attr]
reader.start()
while p.poll() is None:
if self.cancel.is_set():
self._stop_children()
raise Cancelled()
time.sleep(1.0)
reader.join(timeout=3)
result = None
for line in reversed(lines):
try:
result = json.loads(line).get("result")
break
except (ValueError, AttributeError):
continue
if result is None:
self.tune_word = "failed"
self.emit("tune", "The DeepSeek tuner did not finish, so the standard settings run. Nothing was changed.", 100, level="warn")
return
self.tune_result = result
self.tune_word = "faster" if result.get("changed") else "already fastest"
s.tuned_at = time.strftime("%Y-%m-%d")
self.emit("tune", " ".join(result.get("lines") or ["Tuning finished."]), 100, level="info", word=self.tune_word)
def tune(self) -> None:
s = self.s
if s.tune == "skip":
return
if self.multi_file():
self.tune_deepseek()
return
from strata_control import presets
if not self.retune:
if s.tuned_preset:
try:
presets.find(s.tuned_preset)
self.emit("tune", "This preset is already tuned for this computer. Add --retune to measure again.", 100)
return
except presets.PresetError:
pass
elif s.tuned_at: # measured before: the standard settings were already the fastest, nothing was saved
self.emit("tune", f"Already tuned on {s.tuned_at}: this computer runs best with the standard settings. Nothing to do; add --retune to measure again.", 100)
return
stored = self.stored_tuning()
if stored: # the same model and choices were tuned on this computer before (a run by flags, no preset)
s.tuned_preset, s.tuned_at = stored["id"], stored["date"]
self.emit("tune", f"This model was already tuned for this computer on {stored['date']} ('{stored['name']}'), so that result is used "
"and nothing is measured again. Add --retune to measure again.", 100)
return
mode = "thorough" if s.tune == "thorough" else "quick"
status_file = launch_dir() / f"{self.name}.tune.json"
try:
status_file.unlink()
except OSError:
pass
argv = self._cli("tune", "auto", self.profile, "--mode", mode, "--output", "json", "--status-file", str(status_file),
"--name", (s.name or self.name))
if s.tune_allow_small:
argv.append("--allow-small-differences")
self.stop_own_server_for_tuning()
self.emit("tune", "Tuning for the best speed. Close other heavy programs while it measures.", 0)
p = self._spawn(argv)
out_lines: list[str] = []
reader = threading.Thread(target=lambda: out_lines.extend(p.stdout.readlines()), daemon=True) # type: ignore[union-attr]
reader.start()
while p.poll() is None:
if self.cancel.is_set():
self._stop_children()
raise Cancelled()
st = _read_json(status_file)
if st:
done, planned = st.get("runs_done") or 0, st.get("runs_planned") or 0
pct = (100.0 * done / planned) if planned else None
left = st.get("minutes_left")
self.emit("tune", st.get("message") or "Measuring…", pct, speeds=st.get("speeds") or [],
round=(done, planned), minutes_left=left)
time.sleep(1.0)
reader.join(timeout=3)
result = None
for line in reversed(out_lines):
try:
result = json.loads(line).get("result")
break
except (ValueError, AttributeError):
continue
self.tune_result = result
word, sentence = tune_words(result, p.returncode)
self.tune_word = word
preset = (result or {}).get("preset") or {}
if preset.get("id"):
s.tuned_preset, s.tuned_at = preset["id"], time.strftime("%Y-%m-%d")
elif result and result.get("outcome") == "default_confirmed":
s.tuned_at = time.strftime("%Y-%m-%d")
self.emit("tune", f"Tuning result: {word}. {sentence}", 100, level="info" if word in ("faster", "already fastest") else "warn", word=word)
restart_after = True # run(start=False) measures only: nothing starts the model again
def stop_own_server_for_tuning(self) -> None:
"""The measuring engine needs the card, so this launch's own server (the same profile) is stopped first and `start()` brings it back with
the result (E07). Other engines are never touched here; the tuner says plainly when another program holds the card."""
from strata_control import runtime
from strata_control.profiles import ProfileError
self.stop_others()
try:
serving = runtime.status(self.profile).get("state") in ("ready", "starting")
except Exception: # noqa: BLE001 - no state to read: nothing of ours is running
return
if not serving:
return
self.emit("tune", "Stopping the model to measure; it starts again afterwards." if self.restart_after
else "Stopping the model to measure.", 0)
try:
runtime.stop(self.profile, timeout=120)
except ProfileError as e:
raise LaunchError(f"The running model could not be stopped to measure: {e}.", "Run `strata launch --stop` first, then measure again.", 6) from None
def stored_tuning(self) -> dict | None:
"""The tuning result stored for this launch's model, choices and computer (the active preset of its slot in the shared presets store,
which the start applies), or None. A result whose computer changed in a way that matters (a different card, processor or memory,
another model file) is not reused: it is measured again."""
if self.config_path is None:
return None
from strata_control import presets
try:
_cfg, info = presets.overlay(read_config(self.config_path))
if not info.get("applied") or not info.get("preset"):
return None
made = str(presets.load(info["preset"]["id"]).get("created") or "")[:10]
return {"id": info["preset"]["id"], "name": info["preset"]["name"], "date": made or time.strftime("%Y-%m-%d")}
except Exception: # noqa: BLE001 - an unreadable store means: measure
return None
tune_word: str | None = None
# ---- 0. a server is already running
CHOICE_SKIP = ("schema", "name", "tuned_preset", "tuned_at", "last_used", "created", "api_key")
def choices(self) -> dict:
"""The choices that make one server different from another (not the bookkeeping, and the key only as a fingerprint)."""
import hashlib
d = {k: v for k, v in self.s.to_dict(with_key=False).items() if k not in self.CHOICE_SKIP}
d["key"] = hashlib.sha256(("none" if self.s.no_key and not self.s.api_key else self.s.api_key or "").encode("utf-8")).hexdigest()[:16]
return json.loads(json.dumps(d, sort_keys=True))
def _choices_file(self, profile: str | None = None) -> Path:
return launch_dir() / f"{(profile or self.profile)[7:]}.choices.json"
def save_choices(self) -> None:
try:
write_private(self._choices_file(), self.choices())
except OSError:
pass
def _saved_choices(self, profile: str) -> dict | None:
try:
return json.loads(self._choices_file(profile).read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
def serving_launches(self) -> list[dict]:
"""Launch servers that are up now (this one included): [{profile, name, port, choices}]."""
from strata_control import profiles, runtime
out = []
try:
listing = profiles.list_profiles()
except Exception: # noqa: BLE001
return out
for row in listing:
pid = row["id"]
if not pid.startswith("launch-"):
continue
try:
if runtime.status(pid).get("state") not in ("ready", "starting"):
continue
prof, _rev = profiles.load(pid)
except Exception: # noqa: BLE001
continue
port = int((prof.get("overrides") or {}).get("port", S.DEFAULT_PORT))
out.append({"profile": pid, "name": pid[7:], "port": port, "choices": self._saved_choices(pid)})
return out
def running_phrase(self, row: dict) -> str:
"""`Gyro-S is running on port 8080 with 32K` for the restart question."""
c = row.get("choices") or {}
title = None
try:
title = next((r.title for r in self.catalog.rows() if r.family == c.get("family") and r.model == c.get("model")), None)
except Exception: # noqa: BLE001
title = None
title = title or ("Your model files" if c.get("model_path") else row["name"])
return f"{title} is running on port {row['port']}" + (f" with {S.context_label(c['context'])}" if c.get("context") else "")
@staticmethod
def describe_running(row: dict) -> str:
c = row.get("choices") or {}
bits = []
if c.get("context"):
bits.append(f"context {S.context_label(c['context'])}")
if c.get("vision"):
bits.append(f"pictures {c['vision']}")
if c.get("access"):
bits.append(f"access {S.ACCESS_WORDS.get(c['access'], c['access'])}")
return f"{row['name']} is serving on port {row['port']}" + (f" ({', '.join(bits)})" if bits else "")
def check_serving(self) -> "Result | None":
"""G04/E01. The same choices already serving: return that server (nothing stops or starts). Other choices on this profile, or another
launch on this port: ask once on the screens, go on with --restart, else refuse in one line and change nothing. A launch on another port
runs beside the others (the person asked for two), with a warning about graphics memory."""
running = self.serving_launches()
if not running:
self.checked()
return None
own = next((r for r in running if r["profile"] == self.profile), None)
same_port = [r for r in running if r["profile"] != self.profile and r["port"] == int(self.s.port)]
beside = [r for r in running if r["profile"] != self.profile and r["port"] != int(self.s.port)]
if own and not self.retune and not same_port and own["choices"] == self.choices() and own["port"] == int(self.s.port):
self.checked()
self.config_path = launch_dir() / f"{self.name}.json"
urls = acc.urls_for(self.s, tunnel_url=(acc.tunnel_info(self.profile) or {}).get("url"))
self.emit("done", "Already serving these choices; nothing was restarted.", 100, urls=urls)
return Result(self.profile, urls, self.s.api_key, None, self.notices, str(self.config_path), reused=True)
for r in beside:
self.notices.append(f"{r['name']} is already loaded on port {r['port']}. Two models at once may not fit in the graphics memory; "
"the new one can fail to start. Stop the first with `strata launch --stop` if it does.")
blocking = ([own] if own and not self.retune else []) + same_port
if not blocking:
self.checked()
return None
what = "; ".join(self.describe_running(r) for r in blocking)
if not self.restart:
if self.confirm_restart is not None:
if not self.confirm_restart(f"{self.running_phrase(blocking[0])}. Restart it with these settings?", "Answers in progress stop."):
raise LaunchError("Left as it is: the running model keeps serving.", "", 130)
else:
raise LaunchError(f"A model is already running: {what}. These choices are different.",
"Add --restart to stop it and start these, or run `strata launch --stop` first.", 6)
self._stop_first = [r["profile"] for r in same_port]
self.checked()
return None
def checked(self) -> None:
cb, self.after_check = self.after_check, None
if cb is not None:
cb()
def stop_others(self) -> None:
"""Stop the other launch servers that hold this port, so the new one gets it (no "port already in use")."""
from strata_control import runtime
from strata_control.profiles import ProfileError
for pid in self._stop_first:
self.emit("start", f"Stopping {pid[7:]} so the new choices can use port {int(self.s.port)}…", None)
try:
runtime.stop(pid, timeout=120)
acc.stop_tunnel(pid)
except ProfileError as e:
raise LaunchError(f"The running server {pid[7:]} could not be stopped: {e}.", "Run `strata launch --stop` first.", 6) from None
self._stop_first = []
# ---- 5. start
def start(self) -> None:
from strata_control import runtime
from strata_control.profiles import ProfileError
name = self.profile
self.stop_others()
st = runtime.status(name)
if st.get("state") in ("ready", "starting"):
self.emit("start", "A server for this preset is already running; restarting it with these choices…")
try:
runtime.stop(name, timeout=120)
except ProfileError as e:
raise LaunchError(f"The running server could not be stopped: {e}.", "Run `strata launch --stop` first.", 6) from None
self.check_port_free()
self._marks = self.log_marks() # what the logs already held: only this attempt's lines are ever quoted
self.emit("start", "Starting the engine and loading the model. The first start can take a few minutes.", None)
done = threading.Event()
box: dict[str, Any] = {}
def go() -> None:
try:
box["r"] = runtime.start(name, wait=self.start_wait())
except Exception as e: # noqa: BLE001
box["e"] = e
finally:
done.set()
threading.Thread(target=go, daemon=True).start()
t0 = time.monotonic()
while not done.wait(1.0):
if self.cancel.is_set():
try:
runtime.stop(name, timeout=60)
except Exception: # noqa: BLE001
pass
raise Cancelled()
self.emit("start", f"Loading the model… {int(time.monotonic() - t0)} s", None)
if "e" in box:
self.end_failed_start()
raise LaunchError(f"The server could not start: {box['e']}", _log_hint(self.config_path, box["e"]), 6)
r = box["r"]
if not r.get("ok"):
text, nxt = explain_engine_exit(r, self.config_path, marks=self._marks, server_log=self.server_log_path())
self.end_failed_start()
raise LaunchError(text, nxt, 6)
self.emit("start", "The model is loaded and answering.", 100)
self.report_drafting()
_marks: dict | None = None
def server_log_path(self) -> Path | None:
try:
from strata_control import runtime
return runtime.runtime_dir() / f"{self.profile}.server.log"
except Exception: # noqa: BLE001
return None
def log_marks(self) -> dict:
"""{log path: size now} for the engine log and the runtime's server log, so a failed start quotes only what this attempt wrote."""
marks = {}
paths = [self.server_log_path()]
try:
paths.append(Path(read_config(self.config_path).get("log"))) if self.config_path else None
except (OSError, ValueError, TypeError):
pass
for path in paths:
try:
marks[str(path)] = Path(path).stat().st_size if path else 0
except OSError:
if path:
marks[str(path)] = 0
return marks
def check_port_free(self) -> None:
"""Before the host starts: a port another program holds is said at once, in words (X01), and nothing is left running."""
import socket
port = int(self.s.port)
busy = False
for host in ("127.0.0.1",):
try:
with socket.create_connection((host, port), timeout=1.0):
busy = True
except OSError:
pass
if not busy:
return
raise LaunchError(f"Port {port} is already in use by another program.",
f"Close it, or start with a different port (for example --port {port + 1}).", 6)
def end_failed_start(self) -> None:
"""A start that failed leaves nothing alive: the runtime host of this attempt is stopped, or ended by its recorded identity."""
from strata_control import runtime
try:
runtime.stop(self.profile, timeout=30)
except Exception: # noqa: BLE001
pass
try:
runtime.cleanup(self.profile)
except Exception: # noqa: BLE001
pass
def report_drafting(self) -> None:
"""Say which drafter runs when the engine fell back from the one that was asked for ("Drafting: MTP (DFlash2 didn't fit)"). Best effort."""
try:
import urllib.request
headers = {"Authorization": f"Bearer {self.s.api_key}"} if self.s.api_key else {}
with urllib.request.urlopen(urllib.request.Request(f"http://127.0.0.1:{int(self.s.port)}/metrics", headers=headers), timeout=4) as resp:
info = (json.loads(resp.read().decode("utf-8", "replace")).get("engine") or {})
except Exception: # noqa: BLE001 - the line is a convenience
return
note = str(info.get("drafting_note") or "")
if note:
self.notices.append(note)
self.emit("start", note, None)
# ---- 6. access
def open_access(self) -> str | None:
s = self.s
missing = acc.missing_helper(s.access)
tunnel = None
if s.access == "cloudflare":
if missing:
self.notices.append(missing + " The server is running on this computer meanwhile.")
self.emit("access", missing, level="warn")
else:
self.emit("access", "Opening a Cloudflare quick tunnel…")
tunnel, msg = acc.start_tunnel(self.profile, int(s.port))
if tunnel is None:
self.notices.append(msg)
self.emit("access", msg, level="info" if tunnel else "warn", url=tunnel)
elif missing:
self.notices.append(missing)
self.emit("access", missing, level="warn")
else:
self.emit("access", "Ready: " + ", ".join(acc.urls_for(s)), 100)
return tunnel
# ---- all of it
def row(self):
"""The catalog row of the chosen model (None for a model the list does not carry)."""
s = self.s
return next((r for r in self.catalog.rows() if (r.family, r.model) == (s.family, s.model)), None)
def download_plan(self) -> dict:
"""What this launch would download, in numbers and words (nothing is changed): the model's missing files, and for a model that is
not on this computer yet also the draft layer and the picture reader that setup fetches after it. 0 when everything is here."""
from strata_control import engine_component
from strata_control.discovery import MMPROJ_GB, MTP_FETCH_GB
s = self.s
st = self.model_state()
parts = [{"kind": p.kind, "label": p.label, "gb": round(p.gb, 1)} for p in st.parts if p.gb >= 0.05] # a part already on disk is not a download
row = self.row()
helpers = s.family in modelstate.HELPER_FAMILIES
wants_pictures = s.vision not in (None, "off") and row is not None and bool(row.images)
if helpers and (st.own_files or not st.installed):
# a model that is not set up yet (or the person's own files) comes without the draft layer and the picture reader: setup fetches them
# next to the prepared data, unless they are there already
have = {p["kind"] for p in parts}
try:
data = Path(st.folder) if st.folder else (Path(s.data_dir) if s.data_dir else (self.existing_data_dir() or self.catalog.data_dir()))
if modelstate.mtp_choice(s.mtp, s.drafting) != "off" and "mtp" not in have and not (data / "mtp" / "rt" / "experts.bin").is_file():
parts.append({"kind": "mtp", "label": "the draft layer", "gb": MTP_FETCH_GB})
places = [data / "models", data / "Models"]
if s.model_path: # setup uses a reader that sits with the person's own files (setup.py --gguf-dir)
own = Path(s.model_path)
places += [own if own.is_dir() else own.parent, (own if own.is_dir() else own.parent).parent]
has_reader = any(list(d.glob("mmproj*.gguf")) for d in places if d.is_dir())
if wants_pictures and "mmproj" not in have and not has_reader:
parts.append({"kind": "mmproj", "label": "the picture reader", "gb": MMPROJ_GB})
except OSError:
pass
try:
from strata_control import drafting
_b, _cfg = self.installed_state()
want = self.drafting_inputs(_cfg or {})
want["data_dir"] = want["data_dir"] or self.existing_data_dir() or self.catalog.data_dir()
if drafting.needs_pack(**want): # chosen DFlash2 (or Auto picking it) and the drafter is not here: it is part of the download
parts.append({"kind": "dflash2", "label": "the DFlash2 drafter", "gb": drafting.DOWNLOAD_GB})
except Exception: # noqa: BLE001 - the plan still shows the rest
pass
gap = self.ready_pack_gap() if st.own_files else None
if gap:
# a ready-made pack (NVFP4) is taken from setup's own models folder, not from where the person's files are (round 8, N2: the plan
# said "nothing to download" and setup then asked for 129 GB): count what setup will fetch
parts = [p for p in parts if p["kind"] != "weights"] + [{"kind": "weights", "label": "the ready-made pack", "gb": gap["gb"]}]
gb = round(sum(p["gb"] for p in parts), 1)
engine, here = True, []
if self.multi_file(): # DeepSeek does not use the Strata engine: nothing about it in the plan (its own package is in the placement)
return {"gb": gb, "parts": parts, "engine_missing": False, "engine_usable": True, "own_files": st.own_files,
"text": ("Nothing to download: your model files are used where they are." if gb < 0.05 and st.own_files else
"Nothing to download: the model is already on this computer." if gb < 0.05 else
f"About {gb:.1f} GB to download.")}
try:
report = engine_component.status(Path(self.components_dir) if self.components_dir else None)
engine = bool(report.get("usable"))
here = [str(i.get("version")) for i in report.get("installed") or [] if i.get("version")]
except Exception: # noqa: BLE001 - the plan must not fail on this
pass
if gb < 0.05:
text = ("Nothing to download: your model files are used where they are." if st.own_files
else "Nothing to download: the model is already on this computer.")
else:
names = [f"{p['label']} ({p['gb']:.1f} GB)" for p in parts]
what = names[0] if len(names) == 1 else ", ".join(names[:-1]) + " and " + names[-1]
text = f"About {gb:.1f} GB to download: {what}."
if st.on_disk_gb >= 1:
text += f" {st.on_disk_gb:.0f} GB of the model is already in its folder."
if st.own_files:
text += " Your model files are used where they are."
if gap:
text += (f" This is a ready-made pack, and setup takes it from its own folder ({gap['folder']}), not from where your files are. "
"Copy or link your pack there to use it as it is" +
(f"; only {gap['free_gb']:.0f} GB are free on that drive, so choose a bigger drive with --data-dir" if gap.get("free_gb") is not None and gap["free_gb"] < gap["gb"] else "") + ".")
if any(p["kind"] == "mmproj" for p in parts):
text += " To skip the picture reader (the model then answers text only), add --vision off; or put its mmproj file next to your model files."
if any(p["kind"] == "mtp" for p in parts):
text += " To skip the draft layer (a little slower, less memory), add --mtp off."
if any(p["kind"] == "dflash2" for p in parts):
text += " To skip the DFlash2 drafter, add --drafting mtp."
if not engine and here:
# B03: an engine IS installed, but this plan cannot use it (its signature cannot be checked here, or it is for another card): not "missing"
text += (f" An engine ({', '.join(here[:3])}) is installed, but it did not pass Strata's check on this computer, so Strata looks for a working one "
"first. `strata components engine` says why.")
elif not engine:
text += " The engine for your graphics card is downloaded first (from the update source; its size is shown when it starts)."
return {"gb": gb, "parts": parts, "engine_missing": not engine and not here, "engine_usable": engine, "own_files": st.own_files, "text": text}
def ready_pack_gap(self) -> dict | None:
"""For a ready-made pack family (NVFP4): what setup would still fetch into its models folder for these choices, {gb, folder, free_gb}, or None."""
s = self.s
try:
mod = self.catalog.strata.setup_module()
_m, families, _c, _src = self.catalog.tables()
if mod is None or not (families.get(s.family or "") or {}).get("ready") or not hasattr(mod, "ready_needed"):
return None
data = Path(s.data_dir) if s.data_dir else (self.existing_data_dir() or self.catalog.data_dir())
models_dir = data / "models"
need = 0
for f in mod.ready_needed(s.model, models_dir):
if not mod.ready_verified(mod.ready_where(models_dir, f), f[5]):
need += int(f[4])
if need <= 0:
return None
probe = next((q for q in (models_dir, *models_dir.parents) if q.exists()), None)
free = self.catalog.disk_free(probe) if probe is not None else None
return {"gb": round(need / 1e9, 1), "folder": str(models_dir), "free_gb": free}
except Exception: # noqa: BLE001 - a plan that cannot work this out still shows the rest
return None
def preflight_words(self) -> list[str]:
"""Plain warnings for `--plan` that the run itself would meet: an engine older than the model needs, an earlier install taking the
place of a clean one, files that are not the model that was named. Nothing is changed."""
out: list[str] = list(self.identity_notes)
try:
from strata_control import engine_component as ec
report = ec.status(Path(self.components_dir) if self.components_dir else None)
if report.get("usable"):
stale = self.stale_engine(report["engine"].get("version"))
if stale:
out.append(stale + " Starting will look for a newer one first; `strata update` does the same.")
except Exception: # noqa: BLE001
pass
try:
out += install_isolation_words(self.catalog, self.s.data_dir)
except Exception: # noqa: BLE001
pass
return out
def plan(self) -> dict:
s = self.s
self.check_own_files()
warns = [n.line() for n in (*S.check_access(s),)]
warns += self.preflight_words()
row = self.row()
try:
self.resolved_vision()
except Exception: # noqa: BLE001
pass
try:
download = self.download_plan()
except Exception: # noqa: BLE001 - a plan that cannot count the download still shows the rest
download = {"gb": None, "parts": [], "engine_missing": None, "own_files": bool(s.model_path),
"text": "How much would be downloaded could not be worked out here."}
title = row.title if row is not None else (f"{s.family} {s.model}" if s.model else None)
model = {"title": title, "family": s.family, "model": s.model, "model_gb": row.download_gb if row is not None else None,
"own_files": bool(s.model_path), "folder": s.model_path}
out = {"profile": self.profile, "settings": s.to_dict(with_key=False), "warnings": warns,
"flags": S.flags_text(s), "steps": list(STAGES), "model": model, "download": download, "drafting": self.drafting_plan_line(),
"speed": self.speed_line()}
if self.multi_file():
out["placement"] = self.placement()
return out
def resolved_speed(self) -> tuple[str, bool] | None:
"""DeepSeek's speed: ("faster" | "full", whether it is the default for this PC); None for any other model. The default is the engine's own
rule (`--experts auto`: 5 when the cards used total 16 GiB or less); only an explicit choice is written as --experts."""
if not self.multi_file() or not self.speed_engine_ok():
return None
try:
default = faster_by_default(self.catalog.computer(), self.s)
except Exception: # noqa: BLE001 - when the cards cannot be read, the engine's own default stands
default = False
if self.s.speed in S.SPEED_MODES:
return self.s.speed, (self.s.speed == "faster") == default
return ("faster" if default else "full"), True
def speed_engine_ok(self, exe=None) -> bool:
"""The DeepSeek engine this launch will run takes --experts (0.1.50 or newer; the self-upgrade normally brings it first, but nothing relies
on that). `exe`: the engine a config names; else the one launch finds. No engine yet, or an older one: False, and the Speed choice stays out."""
if exe is None:
try:
exe = self.dsv41_engine_found()[0]
except Exception: # noqa: BLE001
exe = None
return engine_takes_experts(exe)
def experts_flag(self, exe=None) -> int | None:
"""The engine's --experts for this launch: 5 or 6 only when the person chose and the engine takes it; None (nothing is passed, the engine's
`--experts auto` decides) when they did not, for an engine that is too old, and for a model that has no such choice."""
if not self.multi_file() or self.s.speed not in S.SPEED_MODES or not self.speed_engine_ok(exe):
return None
return 5 if self.s.speed == "faster" else 6
def speed_line(self, engine_experts: int | None = None) -> str:
"""The speed line of --plan and of the summary, in plain words, with the quality note when it is Faster. `engine_experts`: what the
running engine said at its start ("experts per word: N"); it wins over the rule above, so the words are always the engine's."""
speed = self.resolved_speed()
if speed is None:
return ""
if engine_experts is not None and not self.s.speed:
speed = ("faster" if engine_experts < 6 else "full", True)
text = "Speed: " + S.speed_words(speed[0], speed[1])
return text + (". " + S.FASTER_NOTE if speed[0] == "faster" else "")
def drafting_plan_line(self) -> str:
"""The drafting line of --plan: what drafts, or why not what was asked for. Nothing is changed or downloaded."""
from strata_control import drafting
if self.multi_file(): # DeepSeek drafts with its own DSpark drafter (the --dspark auto the config writes), not MTP
if sys.platform == "darwin":
return "Drafting: off (DeepSeek's drafter was not faster on a Mac)"
return "Drafting: DSpark (used when it speeds up answers on this computer)"
try:
base, cfg = self.installed_state()
want = self.drafting_inputs(cfg or {})
return drafting.summary_line(drafting.decide(want["mode"], klass=want["klass"], model_name=want["model_name"], family=want["family"],
data_dir=want["data_dir"]))
except Exception: # noqa: BLE001
return ""
def dsv41_engine_missing(self, cfg: dict) -> bool:
"""An installed DeepSeek model whose config names an engine that is gone (an older package folder an update removed, or another install's) while
this install has no DeepSeek engine package either: not 'already installed' - setup runs, which fetches the package through the feed or says in
one plain line what to do (--dsv41-package, or the engine-dsv41 folder), instead of the start failing on a missing file."""
if cfg.get("arch") != "deepseek41":
return False
exe = str(cfg.get("exe") or "")
if exe and Path(exe).is_file():
return False
return self.dsv41_engine_found()[0] is None
def use_own_dsv41_engine(self, cfg: dict) -> None:
"""DeepSeek: the run config's engine is the DeepSeek engine package of THIS install (engine-dsv41/ here), not the path the installed model's
config was written with: that config can come from another Strata install on this computer (the model is shared between installs) or name
an older package folder that an update has replaced. Only when this install has no package does the config's own path stay."""
if cfg.get("arch") != "deepseek41":
return
found, _known = self.dsv41_engine_found()
if found is None:
return
found = self.upgrade_dsv41_engine(found)
old = str(cfg.get("exe") or "")
choice = getattr(self, "dsv41_choice", None)
if choice and choice[0] == str(found):
self.emit("engine", f"DeepSeek engine: {found} ({choice[1]}).", None)
if os.path.normcase(os.path.normpath(old)) != os.path.normcase(os.path.normpath(str(found))):
cfg["exe"] = str(found)
if old:
self.notices.append("DeepSeek runs on this install's own engine package, not the one the model's earlier settings named.")
libs = [d for d in (cfg.get("lib_dirs") or []) if "engine-dsv41" not in str(d)]
from strata_control import dsv41_engine
cfg["lib_dirs"] = [*libs, *dsv41_engine.lib_dirs(found)] # its own folder, and rocm/bin for a Windows HIP package
def upgrade_dsv41_engine(self, found: Path) -> Path:
"""The installed DeepSeek engine package is older than the signed feed's: fetch, check and start the newer one next to it and switch to it
(strata_control.dsv41_engine.upgrade). Not for a --dsv41-package or a STRATA_DSV41_EXE (the person chose the engine), not for an engine
outside an engine-dsv41 folder, and silent when the feed cannot be reached (a 10 s check; the launch never waits on the network)."""
from strata_control import dsv41_engine
if (os.environ.get("STRATA_DSV41_EXE") or "").strip() or getattr(self.s, "dsv41_package", None):
return found
root = Path(found).parent.parent
if root.name != dsv41_engine.DIR:
return found
try:
from strata_cli import update_cmd
source = update_cmd._feed_source(argparse.Namespace(feed=self.feed), root.parent)
if not dsv41_engine.source_reachable(source):
return found
new = dsv41_engine.upgrade(root, source, installed=found,
emit=lambda m: self.emit("engine", m, None, level="warn" if "could not" in m else "info"))
except Exception: # noqa: BLE001 - never stop a launch for this
return found
return new or found
def dsv41_engine_found(self) -> tuple[Path | None, bool | None]:
"""(strata-dsv41 already installed, or None; whether the engine can be told at all). Looks where setup puts DeepSeek's own engine package
(engine-dsv41/ beside the engine, in Strata's own folder, and in the data folder), and at STRATA_DSV41_EXE (a build of your own)."""
from strata_control import dsv41_engine
own = (os.environ.get("STRATA_DSV41_EXE") or "").strip()
if own:
return (Path(own) if Path(own).is_file() else None), True
# The install this Terminal RUNS from (STRATA_INSTALL_DIR, else the folder its strata_cli comes from) and the install the model's settings live in
# (catalog.root: the first remembered install, which after an update or on a computer with several installs can be an older one) can both hold a
# DeepSeek engine package: the NEWEST version wins, the running Terminal's own on a tie, and the answer is logged.
roots: list[Path] = []
for r in (os.environ.get("STRATA_INSTALL_DIR"), self.catalog.src, self.catalog.root):
if r and not any(os.path.normcase(os.path.normpath(str(r))) == os.path.normcase(os.path.normpath(str(x))) for x in roots):
roots.append(Path(r))
picks = []
for i, r in enumerate(roots):
exe = dsv41_engine.find(r / dsv41_engine.DIR)
if exe is not None:
picks.append((dsv41_engine.version_key(exe.parent.name), -i, exe, r))
if picks:
best = max(picks, key=lambda t: (t[0], t[1]))
why = ("this Terminal's own folder" if best[3] == roots[0] or best[3] == Path(self.catalog.src) else "the install the model's settings came from")
if len(picks) > 1:
why += f", the newest of {len(picks)} packages"
self.dsv41_choice = (str(best[2]), why)
return best[2], True
folders: list[Path] = [Path(self.catalog.root) / dsv41_engine.DIR] # (nothing there: the older places, as before)
try:
from strata_control import engine_component as ec
rep = ec.status()
exe = (rep.get("engine") or {}).get("exe") if rep.get("usable") else None
if exe:
folder = Path(str(exe)).parent
folders += [folder.parent / dsv41_engine.DIR]
try:
folders.append(self.catalog.strata.setup_module().dsv41_engine_dir(folder))
except Exception: # noqa: BLE001
pass
known = True
else:
known = None
except Exception: # noqa: BLE001
known = None
try: # where the setup this Terminal runs puts it (its own engine folder, or Strata's folder)
mod = self.catalog.strata.setup_module()
folders += [Path(self.catalog.root) / dsv41_engine.DIR, # setup runs here in the app's installer mode: its home() is this folder
mod.dsv41_engine_dir(Path(mod.ROOT) / "engine"), Path(mod.home()) / dsv41_engine.DIR]
except Exception: # noqa: BLE001
pass
try:
folders.append((Path(self.s.data_dir) if self.s.data_dir else self.catalog.data_dir()) / dsv41_engine.DIR)
except Exception: # noqa: BLE001
pass
for f in folders:
found = dsv41_engine.find(f)
if found is not None:
return found, True
for f in folders[:1]: # the engine's own folder: an all-in-one package carries it
for n in ("strata-dsv41", "strata-dsv41.exe"):
if (f.parent / n).is_file():
return f.parent / n, True
return None, known
def dsv41_engine(self) -> bool | None:
"""Whether the installed engine carries strata-dsv41 (True / False); None when no engine is installed yet or it cannot be told."""
found, known = self.dsv41_engine_found()
return True if found is not None else (False if known else None)
def placement(self) -> dict:
"""Where DeepSeek-V4.1-Flash would go on this computer's cards (strata_control/dsv41_fit.py): the cards this launch names, else every
card that counts; the real numbers in plain words, with the n-gram table's SSD note and the RAM / disk warnings (never a refusal)."""
s = self.s
hw = self.catalog.hardware()
try:
data = Path(s.data_dir) if s.data_dir else self.catalog.data_dir()
free = None if (s.model_path or self.model_state().remaining_gb < 1) else self.catalog.disk_free(data)
except Exception: # noqa: BLE001 - a plan that cannot read the disk still shows the rest
free = None
p = self.catalog.strata.dsv41_plan(hw, free, s.gpus, model=s.model)
warnings = list(p["warnings"])
found, known = self.dsv41_engine_found()
engine = True if found is not None else (False if known else None)
if found is not None:
ver = next((part for part in (found.parent.name.split("-")[0], ) if part[:1].isdigit()), "")
p["lines"].append("DeepSeek engine: installed" + (f", {ver}" if ver else "") + ".")
if engine is False:
warnings.append("DeepSeek runs on its own engine package (about 70 MB, separate from the Strata engine): setup downloads it first. "
"If it cannot be fetched, setup stops with a message before the model is downloaded.")
return {"verdict": p["verdict"], "lines": p["lines"], "warnings": warnings, "cards": p["cards"], "args": p["args"],
"engine_has_dsv41": engine,
"experts_in_vram": p["experts_in_vram"], "experts_total": p["experts_total"], "vram_experts_gb": p["vram_experts_gb"],
"cpu_experts_gb": p["cpu_experts_gb"], "ram_needed_gb": p.get("ram_needed_gb"), "engram_gb": p["engram_gb"],
"download_gb": p["download_gb"], "measured": p["measured"]}
def run(self, *, start: bool = True) -> Result:
self.restart_after = start
try:
self.check_own_files() # a wrong --model-path is said before the engine is downloaded
if start:
reused = self.check_serving() # before anything is downloaded or stopped
if reused is not None:
return reused
self.ensure_engine()
self.check_cancel()
base = self.ensure_model()
self.check_cancel()
self.ensure_drafter(base)
self.check_cancel()
self.emit("config", "Writing this launch's settings…")
cfg = self.build_config(base)
self.ensure_profile(cfg)
self.emit("config", "Settings are saved.", 100)
if self.engine_problem:
text = no_engine_final(self.engine_problem, self.s.name or self.name)
self.emit("done", text, 100, level="warn")
return Result(self.profile, [], self.s.api_key, None, self.notices + [text], str(cfg), serving=False,
engine_problem=self.engine_problem, final_text=text)
self.tune()
self.check_cancel()
if not start:
return Result(self.profile, [], self.s.api_key, self.tune_word, self.notices, str(cfg))
self.start()
self.save_choices()
tunnel = self.open_access()
urls = acc.urls_for(self.s, tunnel_url=tunnel)
res = Result(self.profile, urls, self.s.api_key, self.tune_word, self.notices, str(cfg), tunnel)
self.emit("done", "Strata is serving.", 100, urls=urls)
return res
except Cancelled:
self._stop_children()
raise
except LaunchError:
self._stop_children()
raise
def stop(self) -> dict:
from strata_control import runtime
out = runtime.stop(self.profile, timeout=180)
acc.stop_tunnel(self.profile)
return out
from strata_control.models import ISSUE_WORDS, plain_issue # noqa: E402,F401 (one wording for the app, the Terminal and `models import`)
def _read_json(path: Path) -> dict | None:
try:
return json.loads(Path(path).read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
#: lines of an engine log that say why it stopped (the last one wins), and a plain next step for the causes we know. Recommend, never force.
CAUSE_LINE = re.compile(r"error|exception|failed|cannot|can't|could not|no device|not found|out of memory|unsupported|abort|invalid|missing|"
r"no kernel|illegal|denied|refus", re.I)
# The GPU runtime (ROCm) failed while the card was being set up, often right after another engine exited: the runtime's own text
# ("terminate called after throwing an instance of 'rocr::AMD::hsa_exception'") stays in the log, not in the sentence shown.
HSA_START = re.compile(r"hsa_exception|HSA_STATUS_ERROR", re.I)
HSA_START_NEXT = ("The graphics card was not ready yet; wait a few seconds and run it again. If it keeps happening, restart the PC "
"and report the log.")
KNOWN_CAUSES = (
(re.compile(r"cannot run the expert kernel|needs AVX-?512", re.I),
"This model folder holds the pack for CPUs with AVX-512, and this PC's CPU does not have it. Run `strata setup` again: on a CPU "
"without AVX-512 it builds the pack that runs on AVX2 CPUs (Ryzen 3000-5000, Intel Core 12th-14th generation)."),
(re.compile(r"no device of requested type|device not found|no (gpu|sycl|level.zero) device|permission denied.*(dri|render)", re.I),
"The engine could not open the graphics card. On Linux the account must be allowed to use it: add it to the render group "
"(`sudo usermod -aG render $USER`), log in again and start it again."),
(re.compile(r"out of memory|not enough (gpu |video |free )?memory|cudaerrormemory|allocation failed", re.I),
"The card or memory ran out. Close other programs that use the card, or choose a smaller context (`--context 16k`) and start again."),
(re.compile(r"tqk\d|no kernel|unsupported (quant|type|model)|unknown (quant|tensor) type", re.I),
"This engine is too old for this model. Update it with `strata update` and start again."),
(re.compile(r"dll|libcu|cannot open shared object|0xc0000135", re.I),
"A library the engine needs is missing. Update the engine (`strata update`) or install the graphics driver again."),
)
def _lines_since(path: str | os.PathLike | None, mark: int | None, limit: int = 65536) -> list[str]:
"""Lines written to a log after `mark` bytes (the whole tail when there is no mark); a log that got shorter was started over."""
if not path:
return []
try:
with open(path, "rb") as stream:
stream.seek(0, os.SEEK_END)
size = stream.tell()
start = 0 if (mark is None or mark > size) else mark
stream.seek(max(start, size - limit))
data = stream.read().decode("utf-8", "replace")
except OSError:
return []
return [x.rstrip() for x in data.splitlines() if x.strip()]
def engine_log_tail(config: Path | None, limit: int = 65536, since: dict | None = None) -> tuple[list[str], str | None]:
"""(last lines of the engine log, its path); ([], path-or-None) when it cannot be read. With `since` (log sizes before the attempt)
only this attempt's lines are returned."""
log = None
if config:
try:
log = read_config(config).get("log")
except (OSError, ValueError):
log = None
if not log:
return [], None
if since is not None:
return _lines_since(log, since.get(str(log)), limit), str(log)
try:
with open(log, "rb") as stream:
stream.seek(0, os.SEEK_END)
stream.seek(max(0, stream.tell() - limit))
data = stream.read().decode("utf-8", "replace")
except OSError:
return [], str(log)
return [x.rstrip() for x in data.splitlines() if x.strip()], str(log)
def explain_engine_exit(result: dict, config: Path | None, marks: dict | None = None, server_log: Path | None = None) -> tuple[str, str]:
"""(text, next) for a start that did not reach ready: the stage, the cause line from the engine log, the next step and the log path
(round 8, L3: an Arc engine died with 'No device of requested type available' and the person saw only 'server_exited')."""
server_lines = _lines_since(server_log, (marks or {}).get(str(server_log))) if (server_log and marks is not None) else []
for x in server_lines: # the server's own refusal (server.py: error: ...) is the cause when there is one
busy = re.search(r"port (\d+) is already in use", x)
if busy:
return (f"Port {busy.group(1)} is already in use by another program.",
f"Close it, or start with a different port (for example --port {int(busy.group(1)) + 1}). The server's log: {server_log}")
lines, log = engine_log_tail(config, since=marks)
if marks is not None and not lines:
own = [x for x in server_lines if re.search(r"server\.py: error:|error:", x, re.I)]
if own:
lines, log = own, str(server_log)
lines = lines or [x.strip() for x in str(result.get("log_tail") or "").split(" | ") if x.strip()]
cause = next((x for x in reversed(lines) if CAUSE_LINE.search(x) and not x.lstrip().startswith(("strata generate: ", "strata serve: expert"))), None)
cause = (cause or (lines[-1] if lines else "")).strip()
cause = re.sub(r"^(\[strata\]\s*)?(\d{4}-\d\d-\d\d[ T][\d:.]+\s*)?", "", cause)[:240]
state, why = result.get("state"), result.get("error") or result.get("message")
if why == "startup_wait_expired":
stage = "The model did not finish loading in time"
elif why == "server_exited" or result.get("engine_exit_code") is not None:
code = result.get("engine_exit_code")
stage = "The engine stopped while it was loading the model" + (f" (exit code {code})" if code not in (None, 0) else "")
else:
stage = f"The server did not become ready ({state}: {why or 'see the engine log'})"
hsa = HSA_START.search(" ".join(lines + [str(why or "")])) # the runtime's own text is kept out of the sentence (it is in the log)
if hsa:
# a real cause in the same log (out of memory, no permission on the card ...) wins: its line is the cause shown
real = next((x for x in reversed(lines) if any(p.search(x) for p, _ in KNOWN_CAUSES)), None)
if real:
hsa = None
cause = re.sub(r"^(\[strata\]\s*)?(\d{4}-\d\d-\d\d[ T][\d:.]+\s*)?", "", real.strip())[:240]
else:
cause = ""
text = stage + (f": {cause}" if cause else ".")
if not text.endswith((".", "!")):
text += "."
step = HSA_START_NEXT if hsa else next((words for pattern, words in KNOWN_CAUSES if pattern.search(" ".join([cause, str(why or "")]))), "Run it again; if it repeats, report the log.")
return text, step + _log_sentence(log)
def _log_sentence(log: str | None) -> str:
"""The sentence that points at the engine's log. A log that does not exist is not pointed at: a start that stopped before the engine
existed (a profile whose runtime is still live, a missing engine package) never wrote one (#3714)."""
if log and os.path.isfile(log):
return f" The engine's log: {log}"
if log:
return " The engine did not get as far as writing its log, so the reason is in the lines above (`strata logs` has the server's own)."
return " The engine's log has the reason (strata logs, or the log named in the run config)."
#: what the runtime says when another host still owns the profile: the start never began, so there is no engine log to read.
PROFILE_BUSY = re.compile(r"already has a live managed runtime", re.I)
def _log_hint(config: Path | None, error: object = None) -> str:
if PROFILE_BUSY.search(str(error or "")):
return ("Another Strata server for this preset is still running or starting, so this start never began and there is no engine log for it. "
"Run the command again; if it repeats, `strata launch --stop` ends the other server first.")
log = None
if config:
try:
log = read_config(config).get("log")
except (OSError, ValueError):
log = None
if log and os.path.isfile(log):
return f"The engine's log has the reason: {log}"
if log:
return "The engine did not get as far as writing its log, so the reason is the message above (`strata logs` has the server's own)."
return "The engine's log has the reason (strata logs, or the log named in the run config)."