After a model is stopped the update waits for its engine to let go; the start-again question follows what was changed

This commit is contained in:
Niko1221 committed 2026-11-01 17:15:20 +01:00
1 parent f7ada7119a
commit 3165c2e8a5
3 files changed
+61 -7

No files matched your search

+19 -6
View File
@@ -295,10 +295,10 @@ class ModelGate:
line; ``emit`` gets one event per step (the app's host reads them from ``strata update --output jsonl``)."""
def __init__(self, *, ask: Callable[[str, bool], bool] | None = None, say: Callable[[str], None] = lambda text: None,
emit: Callable[[dict], None] = lambda event: None, allow_stop: bool = False, start_again: bool = False,
emit: Callable[[dict], None] = lambda event: None, allow_stop: bool = False, start_again: bool = False, settle_s: float = 120.0,
stop: Callable[[RunningModel], None] | None = None, start: Callable[[RunningModel], tuple[bool, str]] | None = None):
self.ask, self.say, self.emit = ask, say, emit
self.allow_stop, self.start_again = allow_stop, start_again
self.allow_stop, self.start_again, self.settle_s = allow_stop, start_again, settle_s
self._stop, self._start = stop or stop_model, start or start_model # (looked up now, so a test or a host can replace them)
self.consented: dict[str, RunningModel] = {}
self.stopped: list[RunningModel] = []
@@ -360,13 +360,25 @@ class ModelGate:
if m.profile not in {s.profile for s in self.stopped}:
self.stopped.append(m)
self.emit({"event": "models", "phase": "stopped", "models": [m.to_dict()], "message": f"{m.label} is stopped"})
self._settle(serving_now)
def _settle(self, serving_now: Callable[[], list[dict]]) -> None:
"""A stopped engine lets go of the card and of the shared lease a moment later (a big DeepSeek model takes a while to unload): wait for that,
up to ``settle_s``, before the update goes on. Found by the real DeepSeek flow, where the very next check still saw "a running Strata server"."""
import time
deadline = time.monotonic() + self.settle_s
while serving_now():
if time.monotonic() >= deadline:
raise updater.UpdateError("The model was stopped but its engine has not let go of the graphics card yet, so nothing more was changed. "
f"Wait a minute and run {NEXT_STEP} again.", EXIT_BUSY)
time.sleep(min(1.0, self.settle_s / 10 or 0.1))
# -- after the update, whatever happened
def finish(self, update_ok: bool) -> bool:
def finish(self, update_ok: bool, changed: bool | None = None) -> bool:
"""Start the stopped models again when the person says so. True when every model that was meant to start did."""
everyone = True
for m in self.stopped:
if update_ok:
if update_ok or changed:
question = f"Start {m.label} again with the same settings?"
else:
question = f"The old engine is unchanged. Start {m.label} again on it?"
@@ -402,15 +414,16 @@ def update_with_models(ctx: "updater.Context", gate: ModelGate, show: Callable[[
after a model had been stopped). Returns run_update's answer plus ``models`` and, if a model did not start again, exit code 8.
Raises what run_update raises (Busy, Declined, a refused feed) after the models have had their chance to start again."""
ctx.gate = gate
ok, out, error = False, None, None
ok, out, error, changed = False, None, None, False
try:
out = updater.run_update(ctx, **kwargs)
ok = out["exit_code"] == 0
changed = any(r.get("changed") for r in out["results"])
if show is not None:
show(out) # what the update did comes before the questions about the models
except Exception as exc: # noqa: BLE001 - restarted below, then re-raised
error = exc
restarted = gate.finish(ok) if gate.stopped else True
restarted = gate.finish(ok, changed) if gate.stopped else True
if error is not None:
raise error
out["models"] = gate.report()
+41
View File
@@ -499,6 +499,47 @@ class TheCommand(ModelCase):
self.assertEqual((self.state(), self.resolved()), ("ready", "0.1.45"), "a check changes nothing")
class AfterTheStop(unittest.TestCase):
"""Found by the real DeepSeek flow: right after a big model was stopped, the next look still saw 'a running Strata server' (its engine had not let
go of the shared lease yet) and the update was refused half way."""
def gate(self, **kw):
stopped = []
asked = []
gate = engine_update.ModelGate(ask=lambda q, d: asked.append(q) or True, stop=stopped.append, start=lambda m: (True, "ok"), **kw)
model = engine_update.RunningModel("launch-x", "X", port=8080)
gate.consented["launch-x"] = model
gate.asked, gate.stops = asked, stopped
return gate, model
def test_the_update_waits_until_the_stopped_engine_has_let_go(self):
gate, _ = self.gate(settle_s=30)
answers = [[{"profile": "launch-x"}], [{"profile": "a running Strata server"}], [{"profile": "a running Strata server"}], []]
with mock.patch("time.sleep"):
gate.release(lambda: answers.pop(0) if answers else [])
self.assertEqual(len(gate.stops), 1)
self.assertEqual(answers, [], "it looked until nothing was serving")
def test_an_engine_that_never_lets_go_is_said_plainly_and_nothing_more_is_changed(self):
gate, _ = self.gate(settle_s=0.3)
calls = {"n": 0}
def serving():
calls["n"] += 1
return [{"profile": "launch-x"}] if calls["n"] == 1 else [{"profile": "a running Strata server"}]
with self.assertRaises(updater.UpdateError) as caught:
gate.release(serving)
self.assertEqual(caught.exception.exit_code, 7)
self.assertIn("has not let go of the graphics card", str(caught.exception))
def test_the_start_again_question_says_same_settings_when_part_of_the_update_was_made(self):
for changed, text in ((True, "with the same settings"), (False, "The old engine is unchanged")):
gate, model = self.gate()
gate.stopped.append(model)
gate.finish(False, changed)
self.assertIn(text, gate.asked[0], changed)
class StartWait(unittest.TestCase):
"""Found by the real DeepSeek flow: a DeepSeek run config asks for a long engine wait; runtime.start takes 0..1800 only, so the model did not start again."""
+1 -1
View File
@@ -90,7 +90,7 @@ else
kill "$PIDCUT"; wait "$PIDCUT" 2>/dev/null
say "5: yes, then yes to start again"
python3 "$HERE/pty_drive.py" --timeout 2400 --answer 'for the update\?.*=>y' --answer 'again with the same settings\?.*=>y' -- "$S" update --feed "$F29" --no-control --backend cuda > "$W/logs/ds-yes.txt" 2>&1; RC=$?
python3 "$HERE/pty_drive.py" --timeout 1500 --answer 'for the update\?.*=>y' --answer 'again with the same settings\?.*=>y' -- "$S" update --feed "$F29" --no-control --backend cuda > "$W/logs/ds-yes.txt" 2>&1; RC=$?
sed 's/\x1b\[[0-9;?]*[A-Za-z]//g' "$W/logs/ds-yes.txt" | tr -d '\r' | grep -v '^$' | tail -20
check "exit 0" "[ $RC -eq 0 ]"
check "the old engine file was never touched while the model ran: the 0.1.54 package is still installed" "[ -d '$DSDIR'/0.1.54-linux-x86_64-cuda-cuda13 ] && [ -x '$OLDEXE' ]"