mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 17:38:20 -10:00
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:
1 parent
f7ada7119a
commit
3165c2e8a5
3 files changed
+61
-7
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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' ]"
|
||||
|
||||
Reference in new issue
Block a user