mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description Fixes #2495 (tokensave / the proxy using ~100% of all cores). ONNX Runtime's intra-op (and inter-op) thread pools **spin-wait on every core between inferences** by default. Headroom is a long-lived process that keeps ONNX models loaded — the kompress code compressor ("tokensave"), the image technique/SigLIP routers, and the memory embedder — so once a model is loaded, its idle thread pool keeps every core busy even when no compression is running. That matches the report exactly: CPU climbs to ~100% of all cores "after a period of time" and the whole machine slows down, with no obvious trigger. `create_cpu_session_options` (the shared factory every CPU ONNX session goes through) configured threads and the memory arena but never touched spinning, so ORT's default (spin enabled) was in effect everywhere. ## Fix Disable intra-op and inter-op thread spinning in `create_cpu_session_options` so idle ORT threads block instead of spin-waiting. This applies to every ONNX session built through the factory (kompress + the image routers). It: - is **best-effort per key** (wrapped in try/except) so an older ORT build that doesn't recognize a config key still creates a session; - is **overridable** via `HEADROOM_ONNX_ALLOW_SPINNING=1` for a dedicated/batch box that wants ORT's peak-throughput spinning; - does not change active-inference throughput meaningfully — blocking threads wake on new work with only microsecond-scale latency, which is the recommended setting for a server/proxy with idle periods. The memory embedder already builds its own options with `intra_op_num_threads=1`; this change is orthogonal and additionally quiets its idle spinning if it were ever routed through the factory. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/onnx_runtime.py`: add `ONNX_ALLOW_SPINNING_ENV` + `onnx_thread_spinning_enabled()`; disable `session.intra_op.allow_spinning` / `session.inter_op.allow_spinning` in `create_cpu_session_options` unless spinning is explicitly re-enabled. - `tests/test_onnx_runtime.py`: spinning is disabled by default (both keys), `HEADROOM_ONNX_ALLOW_SPINNING=1` re-enables it, an explicit `0` disables it, and a config key an older ORT rejects doesn't break session creation. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_onnx_runtime.py -q 11 passed # with the fix reverted the new symbols don't exist, so the spinning tests # fail at import — the pre-fix factory left ORT's spinning at its (enabled) default $ uvx ruff@0.15.17 check headroom/onnx_runtime.py tests/test_onnx_runtime.py All checks passed! $ uvx mypy@1.20.2 --ignore-missing-imports headroom/onnx_runtime.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12, project venv (`uv sync --extra proxy`, onnxruntime 1.23.2 installed), `uvx ruff@0.15.17` / `uvx mypy@1.20.2`, pytest in the venv. - Exact command / steps: built a real `onnxruntime.SessionOptions` via `create_cpu_session_options(ort)` and read back `session.intra_op.allow_spinning` / `session.inter_op.allow_spinning`; repeated with `HEADROOM_ONNX_ALLOW_SPINNING=1`. - Observed result: by default both keys read back `"0"` (spinning disabled); with `HEADROOM_ONNX_ALLOW_SPINNING=1` neither key is set (ORT's default spinning restored). Against a real ORT the pre-fix factory set neither key, so ORT's default (spinning enabled) applied — the idle all-cores burn. Ran against the actual module and real onnxruntime. - Not tested: a live multi-hour VS Code + Claude session measuring CPU before/after (the spinning-disable is the documented ORT remedy for idle-CPU in a long-lived process; the config change itself is verified end to end against real ORT). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable --------- Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
217 lines
7.8 KiB
Python
217 lines
7.8 KiB
Python
import os
|
|
import sys
|
|
|
|
from headroom.onnx_runtime import (
|
|
ONNX_ALLOW_SPINNING_ENV,
|
|
ONNX_CPU_ARENA_ENV,
|
|
cpu_arena_enabled,
|
|
create_cpu_session_options,
|
|
hf_entry_known_absent,
|
|
onnx_thread_spinning_enabled,
|
|
)
|
|
|
|
|
|
class _FakeSessionOptions:
|
|
def __init__(self):
|
|
self.intra_op_num_threads = None
|
|
self.inter_op_num_threads = None
|
|
self.enable_cpu_mem_arena = True
|
|
self.enable_mem_pattern = True
|
|
self.config_entries: dict[str, str] = {}
|
|
|
|
def add_session_config_entry(self, key: str, value: str) -> None:
|
|
self.config_entries[key] = value
|
|
|
|
|
|
class _FakeOrt:
|
|
SessionOptions = _FakeSessionOptions
|
|
|
|
|
|
class _FakeSessionOptionsWithoutToggles:
|
|
def __init__(self):
|
|
self.intra_op_num_threads = None
|
|
self.inter_op_num_threads = None
|
|
|
|
def add_session_config_entry(self, key: str, value: str) -> None:
|
|
# No config storage on this stand-in; ORT here just accepts the call.
|
|
return None
|
|
|
|
|
|
class _FakeOrtWithoutToggles:
|
|
SessionOptions = _FakeSessionOptionsWithoutToggles
|
|
|
|
|
|
def test_create_cpu_session_options_disables_retention_features(monkeypatch):
|
|
"""Non-Windows keeps the legacy low-RSS behavior: arena + mem pattern off."""
|
|
monkeypatch.delenv(ONNX_CPU_ARENA_ENV, raising=False)
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
|
|
options = create_cpu_session_options(
|
|
_FakeOrt,
|
|
intra_op_num_threads=1,
|
|
inter_op_num_threads=2,
|
|
)
|
|
|
|
assert options.intra_op_num_threads == 1
|
|
assert options.inter_op_num_threads == 2
|
|
assert options.enable_cpu_mem_arena is False
|
|
assert options.enable_mem_pattern is False
|
|
|
|
|
|
def test_create_cpu_session_options_darwin_unchanged(monkeypatch):
|
|
monkeypatch.delenv(ONNX_CPU_ARENA_ENV, raising=False)
|
|
monkeypatch.setattr(sys, "platform", "darwin")
|
|
|
|
options = create_cpu_session_options(_FakeOrt)
|
|
|
|
assert options.enable_cpu_mem_arena is False
|
|
assert options.enable_mem_pattern is False
|
|
|
|
|
|
def test_create_cpu_session_options_keeps_arena_on_windows(monkeypatch):
|
|
"""Disabling the arena on Windows degrades inference by orders of
|
|
magnitude (onnxruntime#11627) — ORT defaults must stay untouched there."""
|
|
monkeypatch.delenv(ONNX_CPU_ARENA_ENV, raising=False)
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
|
|
options = create_cpu_session_options(_FakeOrt, intra_op_num_threads=3)
|
|
|
|
assert options.enable_cpu_mem_arena is True
|
|
assert options.enable_mem_pattern is True
|
|
assert options.intra_op_num_threads == 3
|
|
|
|
|
|
def test_arena_env_override_forces_on(monkeypatch):
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
monkeypatch.setenv(ONNX_CPU_ARENA_ENV, "1")
|
|
|
|
assert cpu_arena_enabled() is True
|
|
options = create_cpu_session_options(_FakeOrt)
|
|
assert options.enable_cpu_mem_arena is True
|
|
|
|
|
|
def test_arena_env_override_forces_off(monkeypatch):
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setenv(ONNX_CPU_ARENA_ENV, "0")
|
|
|
|
assert cpu_arena_enabled() is False
|
|
options = create_cpu_session_options(_FakeOrt)
|
|
assert options.enable_cpu_mem_arena is False
|
|
|
|
|
|
def test_arena_env_invalid_falls_back_to_platform_default(monkeypatch):
|
|
monkeypatch.setenv(ONNX_CPU_ARENA_ENV, "bananas")
|
|
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
assert cpu_arena_enabled() is True
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
assert cpu_arena_enabled() is False
|
|
|
|
|
|
def test_create_cpu_session_options_handles_older_session_options(monkeypatch):
|
|
monkeypatch.delenv(ONNX_CPU_ARENA_ENV, raising=False)
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
|
|
options = create_cpu_session_options(_FakeOrtWithoutToggles)
|
|
|
|
assert options.intra_op_num_threads is None
|
|
assert options.inter_op_num_threads is None
|
|
|
|
|
|
def test_thread_spinning_disabled_by_default(monkeypatch):
|
|
# #2495: ORT thread pools spin-wait on all cores between inferences, so a
|
|
# long-lived proxy pegs every core while idle. Disable spinning by default.
|
|
monkeypatch.delenv(ONNX_ALLOW_SPINNING_ENV, raising=False)
|
|
monkeypatch.delenv(ONNX_CPU_ARENA_ENV, raising=False)
|
|
|
|
assert onnx_thread_spinning_enabled() is False
|
|
options = create_cpu_session_options(_FakeOrt)
|
|
assert options.config_entries.get("session.intra_op.allow_spinning") == "0"
|
|
assert options.config_entries.get("session.inter_op.allow_spinning") == "0"
|
|
|
|
|
|
def test_thread_spinning_env_can_reenable(monkeypatch):
|
|
monkeypatch.setenv(ONNX_ALLOW_SPINNING_ENV, "1")
|
|
monkeypatch.delenv(ONNX_CPU_ARENA_ENV, raising=False)
|
|
|
|
assert onnx_thread_spinning_enabled() is True
|
|
options = create_cpu_session_options(_FakeOrt)
|
|
assert "session.intra_op.allow_spinning" not in options.config_entries
|
|
assert "session.inter_op.allow_spinning" not in options.config_entries
|
|
|
|
|
|
def test_thread_spinning_env_explicit_off(monkeypatch):
|
|
monkeypatch.setenv(ONNX_ALLOW_SPINNING_ENV, "0")
|
|
|
|
assert onnx_thread_spinning_enabled() is False
|
|
options = create_cpu_session_options(_FakeOrt)
|
|
assert options.config_entries.get("session.intra_op.allow_spinning") == "0"
|
|
|
|
|
|
def test_spinning_disable_is_best_effort_on_older_ort(monkeypatch):
|
|
# An ORT build that rejects the config key must not break session creation.
|
|
monkeypatch.delenv(ONNX_ALLOW_SPINNING_ENV, raising=False)
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
|
|
class _RejectingSessionOptions(_FakeSessionOptions):
|
|
def add_session_config_entry(self, key: str, value: str) -> None:
|
|
raise RuntimeError(f"unknown config key: {key}")
|
|
|
|
class _RejectingOrt:
|
|
SessionOptions = _RejectingSessionOptions
|
|
|
|
# Must not raise.
|
|
options = create_cpu_session_options(_RejectingOrt)
|
|
assert options.enable_cpu_mem_arena is False
|
|
|
|
|
|
def _write_fake_hf_cache(
|
|
root: str, repo_id: str, revision: str, *, no_exist_files: list[str]
|
|
) -> None:
|
|
"""Build a minimal on-disk HF hub cache layout for a single repo/revision.
|
|
|
|
Mirrors the real cache structure closely enough for
|
|
``huggingface_hub.try_to_load_from_cache`` to read it: a ``refs/<name>``
|
|
pointer file, a ``snapshots/<hash>`` directory, and a
|
|
``.no_exist/<hash>/<filename>`` marker per file whose absence is cached.
|
|
"""
|
|
from huggingface_hub.file_download import repo_folder_name
|
|
|
|
repo_folder = os.path.join(root, repo_folder_name(repo_id=repo_id, repo_type="model"))
|
|
os.makedirs(os.path.join(repo_folder, "refs"), exist_ok=True)
|
|
with open(os.path.join(repo_folder, "refs", revision), "w") as f:
|
|
f.write("abc123")
|
|
os.makedirs(os.path.join(repo_folder, "snapshots", "abc123"), exist_ok=True)
|
|
no_exist_dir = os.path.join(repo_folder, ".no_exist", "abc123")
|
|
os.makedirs(no_exist_dir, exist_ok=True)
|
|
for filename in no_exist_files:
|
|
open(os.path.join(no_exist_dir, filename), "w").close()
|
|
|
|
|
|
def test_hf_entry_known_absent_true_when_404_was_cached(tmp_path, monkeypatch):
|
|
from huggingface_hub import constants
|
|
|
|
_write_fake_hf_cache(str(tmp_path), "acme/widget", "main", no_exist_files=["merged.pt"])
|
|
monkeypatch.setattr(constants, "HF_HUB_CACHE", str(tmp_path))
|
|
monkeypatch.delenv("HEADROOM_HF_PIN", raising=False)
|
|
|
|
assert hf_entry_known_absent("acme/widget", "merged.pt") is True
|
|
|
|
|
|
def test_hf_entry_known_absent_false_when_never_checked(tmp_path, monkeypatch):
|
|
from huggingface_hub import constants
|
|
|
|
_write_fake_hf_cache(str(tmp_path), "acme/widget", "main", no_exist_files=[])
|
|
monkeypatch.setattr(constants, "HF_HUB_CACHE", str(tmp_path))
|
|
monkeypatch.delenv("HEADROOM_HF_PIN", raising=False)
|
|
|
|
assert hf_entry_known_absent("acme/widget", "merged.pt") is False
|
|
|
|
|
|
def test_hf_entry_known_absent_false_when_repo_not_cached_at_all(tmp_path, monkeypatch):
|
|
from huggingface_hub import constants
|
|
|
|
monkeypatch.setattr(constants, "HF_HUB_CACHE", str(tmp_path))
|
|
monkeypatch.delenv("HEADROOM_HF_PIN", raising=False)
|
|
|
|
assert hf_entry_known_absent("nobody/nothing", "merged.pt") is False
|