Skip to content

serve: add lazy startup and reliable Windows model cleanup - #333

Closed
KadoBOT wants to merge 1 commit into
Niko1221:mainfrom
KadoBOT:contrib/lazy-model-lifecycle
Closed

KadoBOT wants to merge 1 commit into
Niko1221:mainfrom
KadoBOT:contrib/lazy-model-lifecycle

Conversation

@KadoBOT

@KadoBOT KadoBOT commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Strata can unload and reload an idle model, but the HTTP server still loads it during startup. On a shared GPU, that prevents keeping the API available without first occupying the GPU. This adds opt-in cold startup and consolidates native-process shutdown so Windows' stdin reader receives EOF before the server waits for exit.

Behavior

  • --lazy or "lazy_load": true starts the text-only API with the model unloaded. The first generation uses the existing reload path, including before_load and the free-VRAM guard from server: optional idle unload, /unload and /load, a free-VRAM guard (share the GPU with other programs) #208. Eager startup remains the default; lazy startup with vision is explicitly rejected.
  • The configured model remains discoverable while unloaded. /v1/status exposes loaded and auto_load, /props reports the reload capability, and /api/health aliases /health.
  • Add JSON POST /v1/load and /v1/unload aliases accepting an optional model name. They enforce the existing key and browser-Origin checks, reject a different model with 404, and return 409 while work is active or queued. Existing /load and /unload routes remain available. Explicit loading refreshes the idle timer.
  • Native shutdown sends QUIT, closes stdin, waits for exit, and releases the pump/pipe/log handles. If forced shutdown still times out, retain process ownership and report failure instead of claiming it was unloaded. Preserve Windows kill-on-close job containment.

This extends #208 rather than reimplementing its idle-unload policy. It contains no model downloads, machine-specific configuration, or hardware tuning.

Validation

  • Native Windows: python -m unittest discover -s serve -p "test_*.py" -v — 86 tests, OK, 3 optional tests skipped.
  • Python compilation and git diff --check passed.
  • New HTTP tests cover discovery without loading, first-request loading, unload/reload, explicit loading and idle-timer refresh, model matching, queued/busy refusal, API keys, and Origin/content-type checks.
  • Process tests cover an entirely unloaded constructor, EOF before waiting, and retaining ownership after a shutdown timeout. The rebased branch was verified with mock generation/process fixtures; no native GPU benchmark was run for this PR.

@Niko1221

Niko1221 commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Thanks! This is in the 0.1.32 batch. We take it keeping the terminate/kill fallback in unload, and without letting except RuntimeError swallow EngineDied; every change goes through our identity gate (byte-identical output on four models) and a speed A/B before it ships.

@KadoBOT

KadoBOT commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for including this in the 0.1.32 batch. Agreed on preserving the terminate/kill fallback. The Windows cleanup fix is to send QUIT and close stdin before waiting, so the native stdin reader receives EOF; cleanup should retain process ownership and report failure if exit cannot be confirmed.

Also agreed on preserving EngineDied handling: it subclasses RuntimeError. The proposed HTTP handler catches EngineDied before the broader RuntimeError handler, retaining the crash response and next-request restart message; that ordering needs to survive integration.

Cold startup remains opt-in, with eager startup as the default. The first request uses the existing load path and GPU admission guard, and explicit load/unload controls refuse active or queued work. The mock HTTP/process regressions are in the PR; your four-model identity gate and speed A/B are still needed before release.

The latest public tag/main I can inspect is still 0.1.31, so I'll leave this open for integration tracking. Once the version with these safeguards lands in 0.1.32, this PR can be closed as incorporated.

Niko1221 added a commit that referenced this pull request Oct 1, 2026
…tuck instead of a catch-all RuntimeError

- StrataEngine.close() (now also the unload and the restart): QUIT and EOF, then terminate, then kill, each with 20 s
  as the unload had (#333 went from QUIT straight to kill after 10 s; an engine unpinning tens of GB can need more).
  A process that outlives all three raises EngineStuck and stays owned.
- do_POST caught every RuntimeError as a 503 - EngineDied, GpuBusy and ModelBusy are RuntimeErrors too, and any other
  failure was turned into a vague 503. Only EngineStuck is caught now (also around POST /unload, and the idle-unload
  thread survives it); EngineDied keeps its own answer.
- test_lifecycle: terminate before kill, EngineStuck, an EngineDied on load answered as such.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
Niko1221 added a commit that referenced this pull request Oct 1, 2026
…of __init__ until READY - #333's lazy start set ended=False before READY, so a request could again plan with context 0 (RestartWindow test)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
@Niko1221

Niko1221 commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Released in 0.1.32 keeping the terminate/kill fallback and a separate EngineStuck error. Merged under your name (GitHub doesn't show it as merged because it went in by hand). Thank you!

@Niko1221 Niko1221 closed this Oct 1, 2026
Niko1221 added a commit that referenced this pull request Oct 6, 2026
…tuck instead of a catch-all RuntimeError

- StrataEngine.close() (now also the unload and the restart): QUIT and EOF, then terminate, then kill, each with 20 s
  as the unload had (#333 went from QUIT straight to kill after 10 s; an engine unpinning tens of GB can need more).
  A process that outlives all three raises EngineStuck and stays owned.
- do_POST caught every RuntimeError as a 503 - EngineDied, GpuBusy and ModelBusy are RuntimeErrors too, and any other
  failure was turned into a vague 503. Only EngineStuck is caught now (also around POST /unload, and the idle-unload
  thread survives it); EngineDied keeps its own answer.
- test_lifecycle: terminate before kill, EngineStuck, an EngineDied on load answered as such.
Niko1221 added a commit that referenced this pull request Oct 6, 2026
…of __init__ until READY - #333's lazy start set ended=False before READY, so a request could again plan with context 0 (RestartWindow test)
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
… EngineStuck instead of a catch-all RuntimeError

- StrataEngine.close() (now also the unload and the restart): QUIT and EOF, then terminate, then kill, each with 20 s
  as the unload had (Niko1221#333 went from QUIT straight to kill after 10 s; an engine unpinning tens of GB can need more).
  A process that outlives all three raises EngineStuck and stays owned.
- do_POST caught every RuntimeError as a 503 - EngineDied, GpuBusy and ModelBusy are RuntimeErrors too, and any other
  failure was turned into a vague 503. Only EngineStuck is caught now (also around POST /unload, and the idle-unload
  thread survives it); EngineDied keeps its own answer.
- test_lifecycle: terminate before kill, EngineStuck, an EngineDied on load answered as such.
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
… from the start of __init__ until READY - Niko1221#333's lazy start set ended=False before READY, so a request could again plan with context 0 (RestartWindow test)
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
… EngineStuck instead of a catch-all RuntimeError

- StrataEngine.close() (now also the unload and the restart): QUIT and EOF, then terminate, then kill, each with 20 s
  as the unload had (Niko1221#333 went from QUIT straight to kill after 10 s; an engine unpinning tens of GB can need more).
  A process that outlives all three raises EngineStuck and stays owned.
- do_POST caught every RuntimeError as a 503 - EngineDied, GpuBusy and ModelBusy are RuntimeErrors too, and any other
  failure was turned into a vague 503. Only EngineStuck is caught now (also around POST /unload, and the idle-unload
  thread survives it); EngineDied keeps its own answer.
- test_lifecycle: terminate before kill, EngineStuck, an EngineDied on load answered as such.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
… from the start of __init__ until READY - Niko1221#333's lazy start set ended=False before READY, so a request could again plan with context 0 (RestartWindow test)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants