serve: opt-in prevent_sleep keeps Windows awake while a request is running or queued (#1727)

This commit is contained in:
Niko1221 committed 2026-10-09 16:45:37 +02:00
1 parent 392841e586
commit a084fd2d72
4 files changed
+216

No files matched your search

+4
View File
@@ -718,6 +718,10 @@ print(r.choices[0].message.content)
`high`) in `strata-<model>.json`, outside `"args"` (#1641): the engine has no such option, and `--reasoning-effort` in
`"args"` is moved to this key with a note at startup. The web app's shared Chat settings ("Use for other apps too") and a
request's own level win over it.
- **Keep Windows awake while it works (opt-in, #1727).** `"prevent_sleep": true` in `strata-<model>.json` keeps the PC
from going to sleep while at least one request is running or waiting (Windows' `SetThreadExecutionState` with
`ES_SYSTEM_REQUIRED`; the screen may still turn off), and gives the normal sleep rules back when the server is idle
and when it exits. Default off. On Linux and macOS it does nothing (one line at startup says so).
- **A hard thinking budget (opt-in).** `"reasoning_budget_tokens": N` in a request (OpenAI or Anthropic) caps the
thinking at N tokens: when it gets there the server ends it with a short wrap-up line and `</think>`, and the model
answers from there (the engine continues from what it already holds, so nothing is read again). The wrap-up is
+94
View File
@@ -0,0 +1,94 @@
"""Opt-in "prevent_sleep" (#1727): while at least one request is running or queued, the PC is kept awake.
Windows only. SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) keeps the system from idling into sleep
(not the display: no ES_DISPLAY_REQUIRED); ES_CONTINUOUS alone puts the normal behaviour back. The state belongs
to the thread that set it, so one daemon thread sets and clears it; it is cleared when idle and on exit.
Elsewhere this does nothing.
"""
from __future__ import annotations
import atexit
import sys
import threading
ES_CONTINUOUS = 0x80000000
ES_SYSTEM_REQUIRED = 0x00000001
def _windows_call():
import ctypes
fn = ctypes.windll.kernel32.SetThreadExecutionState
fn.argtypes = [ctypes.c_uint32]
fn.restype = ctypes.c_uint32
return fn
class KeepAwake:
def __init__(self, call=None):
self._call = call
self._count = 0
self._cond = threading.Condition()
self._closed = False
self._thread = None
@classmethod
def create(cls, enabled: bool, say=print):
"""None when off. On a system without SetThreadExecutionState: one line, and None."""
if not enabled:
return None
if not sys.platform.startswith("win"):
say('[strata] "prevent_sleep" is for Windows only: ignored here', flush=True)
return None
try:
ka = cls(_windows_call())
except (ImportError, AttributeError, OSError) as e:
say(f'[strata] "prevent_sleep" could not be set up: {e}', flush=True)
return None
atexit.register(ka.close)
say('[strata] prevent_sleep: Windows stays awake while a request is running or queued', flush=True)
return ka
def _loop(self):
held = False
while True:
with self._cond:
while not self._closed and (self._count > 0) == held:
self._cond.wait()
if self._closed:
break
want = self._count > 0
try:
self._call(ES_CONTINUOUS | ES_SYSTEM_REQUIRED if want else ES_CONTINUOUS)
held = want
except Exception: # noqa: BLE001 never fail a request over this
held = want
try:
self._call(ES_CONTINUOUS)
except Exception: # noqa: BLE001
pass
def _start(self):
if self._thread is None:
self._thread = threading.Thread(target=self._loop, daemon=True, name="keep-awake")
self._thread.start()
def acquire(self):
with self._cond:
if self._closed:
return
self._count += 1
self._start()
self._cond.notify_all()
def release(self):
with self._cond:
self._count = max(0, self._count - 1)
self._cond.notify_all()
def close(self):
with self._cond:
self._closed = True
self._cond.notify_all()
t = self._thread
if t is not None and t is not threading.current_thread():
t.join(timeout=2)
+16
View File
@@ -2708,6 +2708,7 @@ class Service:
self.shared = {} # the web app's Chat settings for every client (POST /settings)
self.shared_path = None # where they are kept between starts (next to the config)
self.config_effort = None # #1641: the config's "reasoning_effort": the level for requests with none
self.keep_awake = None # #1727 (opt-in): serve.keepawake.KeepAwake, the config's "prevent_sleep"
self.fifo = threading.Lock()
self.slot_save_path = None # --slot-save-path: /slots/0?action=save|restore (off when None)
self.embeddings = threading.local() # the current request's image embeddings file (GENI)
@@ -3563,6 +3564,17 @@ class Service:
return now
def run(self, ids, thinking, tools, max_new, sampling, cancel, force=None) -> Iterator[tuple[str, object]]:
"""The request's events (see _run); with "prevent_sleep" in the config, the PC stays awake while it runs or waits."""
ka = self.keep_awake
if ka is not None:
ka.acquire()
try:
yield from self._run(ids, thinking, tools, max_new, sampling, cancel, force)
finally:
if ka is not None:
ka.release()
def _run(self, ids, thinking, tools, max_new, sampling, cancel, force=None) -> Iterator[tuple[str, object]]:
"""Yields ("event", Event) as text arrives, then ("done", {"finish": .., "completion_tokens": ..}).
`force` (forced_call): the opening of the call the reply must make - see prepare()."""
defaults = {**self.sampling_defaults, **self.shared} # the config's, then the Chat settings shared with apps
@@ -6093,6 +6105,10 @@ def main() -> int:
sampling_defaults=sampling_defaults,
fit_max_tokens=a.fit_max_tokens or cfg.get("fit_max_tokens") is True)
svc.reasoning_close_retry = cfg.get("reasoning_close_retry") is True # #1053: opt-in, off by default
if cfg.get("prevent_sleep") is not None and not isinstance(cfg["prevent_sleep"], bool):
raise SystemExit(f"[strata] config \"prevent_sleep\" must be true or false, not {cfg['prevent_sleep']!r}")
from serve.keepawake import KeepAwake
svc.keep_awake = KeepAwake.create(cfg.get("prevent_sleep") is True) # #1727: opt-in, Windows only
if cfg.get("reasoning_effort") is not None: # #1641: the thinking level of requests that name none
try:
svc.config_effort = clean_shared_defaults({"reasoning_effort": cfg["reasoning_effort"]}).get("reasoning_effort")
+102
View File
@@ -0,0 +1,102 @@
"""#1727: prevent_sleep. The Windows call is mocked; the real call is tested on Windows by hand.
python -m unittest serve.test_keepawake -v
"""
import threading
import time
import unittest
from pathlib import Path
from unittest import mock
from serve.frontend import ChatTemplate
from serve.keepawake import ES_CONTINUOUS, ES_SYSTEM_REQUIRED, KeepAwake
from serve.server import ByteTokenizer, MockEngine, Service
ROOT = Path(__file__).resolve().parents[1]
ON = ES_CONTINUOUS | ES_SYSTEM_REQUIRED
def wait_for(cond, t=3.0):
end = time.time() + t
while time.time() < end:
if cond():
return True
time.sleep(0.01)
return cond()
class Recorder:
def __init__(self):
self.calls, self.threads = [], set()
def __call__(self, flags):
self.calls.append(flags)
self.threads.add(threading.get_ident())
return 0
class KeepAwakeTests(unittest.TestCase):
def test_held_while_busy_and_released_when_idle(self):
rec = Recorder()
ka = KeepAwake(rec)
self.assertEqual(rec.calls, []) # nothing until a request
ka.acquire()
ka.acquire() # two requests: still one hold
self.assertTrue(wait_for(lambda: rec.calls == [ON]))
ka.release()
time.sleep(0.1)
self.assertEqual(rec.calls, [ON]) # one still running
ka.release()
self.assertTrue(wait_for(lambda: rec.calls == [ON, ES_CONTINUOUS]))
ka.close()
self.assertEqual(rec.calls[-1], ES_CONTINUOUS)
self.assertEqual(len(rec.threads), 1) # set and cleared by the same thread
self.assertFalse(ON & 0x2) # never ES_DISPLAY_REQUIRED
def test_exit_clears_a_held_state(self):
rec = Recorder()
ka = KeepAwake(rec)
ka.acquire()
self.assertTrue(wait_for(lambda: rec.calls == [ON]))
ka.close()
self.assertEqual(rec.calls, [ON, ES_CONTINUOUS])
def test_a_failing_call_never_breaks_a_request(self):
def boom(flags):
raise OSError("no")
ka = KeepAwake(boom)
ka.acquire()
ka.release()
ka.close()
def test_off_and_non_windows(self):
say = mock.Mock()
self.assertIsNone(KeepAwake.create(False, say))
say.assert_not_called()
with mock.patch("serve.keepawake.sys.platform", "linux"):
self.assertIsNone(KeepAwake.create(True, say))
self.assertIn("Windows only", say.call_args[0][0])
def test_service_holds_for_the_whole_request_including_early_close(self):
tok = ByteTokenizer()
svc = Service(MockEngine(tok, "ok"), tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
rec = Recorder()
svc.keep_awake = ka = KeepAwake(rec)
kinds = [k for k, _ in svc.run(tok.encode("hi"), False, None, 8, {}, threading.Event())]
self.assertIn("done", kinds)
self.assertTrue(wait_for(lambda: rec.calls == [ON, ES_CONTINUOUS]))
gen = svc.run(tok.encode("hi"), False, None, 8, {}, threading.Event())
next(gen)
self.assertTrue(wait_for(lambda: rec.calls[-1] == ON))
gen.close() # the client left
self.assertTrue(wait_for(lambda: rec.calls[-1] == ES_CONTINUOUS))
ka.close()
def test_default_service_has_none(self):
tok = ByteTokenizer()
svc = Service(MockEngine(tok, "ok"), tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
self.assertIsNone(svc.keep_awake)
if __name__ == "__main__":
unittest.main()