winjob: opt the server and its children out of Windows power throttling (#691)

A minimized console window made Windows 11 treat the server and its children as background work and move their
threads to the E-cores (about 20% slower on a hybrid CPU). contain() now sets ProcessPowerThrottling off for each
child, and the server does it for itself. Windows 10 knows only the EXECUTION_SPEED bit, so it is tried on its own
when both bits are refused.

Reported with the patch by mozophe.
This commit is contained in:
Niko1221 committed 2026-10-05 15:58:36 +02:00
1 parent 035da7f8c1
commit 8fb2dd34cb
2 files changed
+39 -1

No files matched your search

+15
View File
@@ -65,3 +65,18 @@ class TestWinJob(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
@unittest.skipUnless(os.name == "nt", "power throttling is a Windows setting")
class TestNoThrottle(unittest.TestCase):
def test_a_contained_child_is_opted_out_of_power_throttling(self):
# #691: contain() also sets ProcessPowerThrottling (EcoQoS off), so a minimized server window does not move the
# engine to the E-cores
from serve import winjob
child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"])
try:
self.assertTrue(winjob.no_throttle(int(child._handle)))
self.assertTrue(winjob.contain(child))
finally:
child.kill()
child.wait(10)
+24 -1
View File
@@ -4,7 +4,8 @@ Closing the console window, Task Manager or a crash end the server without runni
the vision encoder and the MCP servers kept running on their own. `contain(proc)` puts a child in a job object that
is set to kill everything in it when its last handle closes - and the only handle is this process's, which the OS
closes however the server ends. Processes the child starts later join the same job. The server itself stays out of
the job, so a browser that `--open` starts is not tied to it. Elsewhere, and if the job cannot be made, a no-op.
the job, so a browser that `--open` starts is not tied to it. Each child (and the server) is also opted out of Windows
power throttling, see `no_throttle` (#691). Elsewhere, and if the job cannot be made, a no-op.
"""
from __future__ import annotations
@@ -57,6 +58,27 @@ if os.name == "nt":
return None
return job
class _PowerThrottling(ctypes.Structure):
_fields_ = [("Version", wintypes.ULONG), ("ControlMask", wintypes.ULONG), ("StateMask", wintypes.ULONG)]
_k32.SetProcessInformation.argtypes = (wintypes.HANDLE, ctypes.c_int, wintypes.LPVOID, wintypes.DWORD)
_k32.GetCurrentProcess.restype = wintypes.HANDLE
def no_throttle(handle) -> bool:
"""Opt a process out of Windows power throttling (EcoQoS). Without it Windows 11 treats a process whose console
window is minimized as background work and, on a hybrid CPU, moves its threads to the E-cores: generation
slowed by about 20% while the server window was minimized (#691, i9-13980HX: 36.6 -> 43.2 tok/s)."""
for mask in (0x1 | 0x4, 0x1): # EXECUTION_SPEED and IGNORE_TIMER_RESOLUTION, both off (Windows 11);
info = _PowerThrottling(1, mask, 0) # Windows 10 knows the first only: ERROR_INVALID_PARAMETER for both
if _k32.SetProcessInformation(handle, 4, ctypes.byref(info), ctypes.sizeof(info)): # ProcessPowerThrottling
return True
return False
no_throttle(_k32.GetCurrentProcess()) # the server itself (tokenizing, streaming) too
else:
def no_throttle(handle) -> bool:
return False
def contain(proc) -> bool:
"""Put a started subprocess.Popen in the kill-on-close job. True if it is in; False (harmlessly) otherwise."""
@@ -69,6 +91,7 @@ def contain(proc) -> bool:
if not _job:
return False
try:
no_throttle(int(proc._handle))
return bool(_k32.AssignProcessToJobObject(_job, int(proc._handle)))
except (AttributeError, OSError, ValueError):
return False