Skip to content

Commit 5bd2266

Browse files
authored
fix(kompress): raise the default execution-slot wait (headroomlabs-ai#2456)
## Description Concurrent Kompress requests currently fail open after a 25 ms execution-slot wait even though ordinary ONNX inference can hold the single slot for hundreds of milliseconds. This raises the existing default wait to 3000 ms while retaining concurrency one, the `HEADROOM_KOMPRESS_EXECUTION_TIMEOUT_MS` override, the tighter acquire and request budgets, and passthrough after a genuine timeout. The reproduction and validated 3000 ms setting come from headroomlabs-ai#2451 Closes headroomlabs-ai#2451 ## 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 - Raise the default Kompress execution-slot wait from 25 ms to 3000 ms. - Start the Kompress request deadline at call entry and carry it through single-item acquire, single-to-batch delegation, and sequential-fallback lineage. - Cap the raised execution-slot wait by that live request deadline on both single-item and batch acquire paths. - Keep the per-backend default concurrency at one and preserve all tighter time budgets. - Add queued single-item, batch, request-deadline, carried-deadline lineage, and router-watchdog lifecycle regressions at the same owner layer that currently fails. - Preserve the explicit short-timeout fail-open path. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_kompress_failsafe.py tests/test_kompress_request_nonblocking.py tests/test_content_router_single_item_deadline.py tests/test_transforms/test_kompress_deadline.py -v`) - [x] Linting passes (`uv run ruff check headroom/transforms/kompress_compressor.py tests/test_kompress_failsafe.py tests/test_content_router_single_item_deadline.py tests/test_transforms/test_kompress_deadline.py`) - [x] Formatting passes (`uv run ruff format headroom/transforms/kompress_compressor.py tests/test_kompress_failsafe.py tests/test_content_router_single_item_deadline.py tests/test_transforms/test_kompress_deadline.py --check`) - [x] New regression tests prove the saturation fix - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_kompress_failsafe.py tests/test_kompress_request_nonblocking.py tests/test_content_router_single_item_deadline.py tests/test_transforms/test_kompress_deadline.py -v 37 passed in 4.22s uv run ruff check headroom/transforms/kompress_compressor.py tests/test_kompress_failsafe.py tests/test_content_router_single_item_deadline.py tests/test_transforms/test_kompress_deadline.py All checks passed! uv run ruff format headroom/transforms/kompress_compressor.py tests/test_kompress_failsafe.py tests/test_content_router_single_item_deadline.py tests/test_transforms/test_kompress_deadline.py --check 4 files already formatted ``` ## Real Behavior Proof - Environment: worktree Python environment from `uv sync --extra dev`, focused pytest with real Python threads and `threading.BoundedSemaphore` - Exact command / steps: hold the sole execution slot with the environment override unset, start queued single-item and batch compression workers, wait until each worker proves it reached a blocked acquire on the shared execution semaphore, release the slot, rerun the explicit 1 ms timeout preservation case, then set `HEADROOM_COMPRESSION_DEADLINE_MS=10` and repeat the held-slot single-item and batch acquires plus a router single-cache-miss run whose Kompress load sleeps past the request deadline. - Observed result: The queued single-item and batch workers each proved a real blocked acquire before release, then acquired after release and compressed, while `HEADROOM_KOMPRESS_EXECUTION_TIMEOUT_MS=1` still passed through promptly, the 10 ms request deadline capped the raised default wait so both held-slot paths failed open before 200 ms without reaching model inference, the single-to-batch and sequential-fallback lineage regressions proved later branches inherit the original request start instead of resetting it, and the router lifecycle proof showed the carried deadline now allows slow Kompress load to start but still expires before model inference after the outer request has already failed open. - Not tested: live ONNX proxy savings under sustained concurrent load ## 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] My changes generate no new warnings - [x] I have added tests that prove my fix is effective - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes `CHANGELOG.md` stays unchanged because the release pipeline generates changelog entries from conventional commits. The fail-open path from headroomlabs-ai#1430 stays intact; this change stops it from firing spuriously under ordinary queueing.
1 parent a09ba6c commit 5bd2266

3 files changed

Lines changed: 306 additions & 10 deletions

File tree

headroom/transforms/kompress_compressor.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,12 @@ def _add_kompress_must_keep_words(
9696
KOMPRESS_COREML_CACHE_DIR_ENV = "HEADROOM_KOMPRESS_COREML_CACHE_DIR"
9797
KOMPRESS_MAX_CONCURRENT_ENV = "HEADROOM_KOMPRESS_MAX_CONCURRENT"
9898
KOMPRESS_EXECUTION_SEMAPHORE_WAIT_MS_ENV = "HEADROOM_KOMPRESS_EXECUTION_TIMEOUT_MS"
99-
KOMPRESS_EXECUTION_SEMAPHORE_WAIT_MS_DEFAULT = 25
99+
KOMPRESS_EXECUTION_SEMAPHORE_WAIT_MS_DEFAULT = 3000
100100
KOMPRESS_BATCH_SIZE_ENV = "HEADROOM_KOMPRESS_BATCH_SIZE"
101101
KOMPRESS_ACQUIRE_TIMEOUT_ENV = "HEADROOM_KOMPRESS_ACQUIRE_TIMEOUT_SECONDS"
102102
KOMPRESS_TIME_BUDGET_ENV = "HEADROOM_KOMPRESS_TIME_BUDGET_SECONDS"
103103
KOMPRESS_CANARY_THRESHOLD_ENV = "HEADROOM_KOMPRESS_CANARY_SECONDS"
104+
KOMPRESS_REQUEST_DEADLINE_ENV = "HEADROOM_COMPRESSION_DEADLINE_MS"
104105

105106
# Both defaults sit well under the proxy's 30s compression-stage timeout so a
106107
# slow model gives up (passthrough) before the request is abandoned. A thread
@@ -175,6 +176,13 @@ def _execution_wait_budget_seconds() -> float:
175176
return parsed / 1000.0
176177

177178

179+
def _request_deadline_seconds() -> float:
180+
try:
181+
return max(0.0, float(os.environ.get(KOMPRESS_REQUEST_DEADLINE_ENV, "20000")) / 1000.0)
182+
except ValueError:
183+
return 20.0
184+
185+
178186
def _acquire_execution_slot(
179187
backend: str,
180188
device_type: str,
@@ -1199,6 +1207,7 @@ def compress(
11991207
*,
12001208
allow_download: bool = True,
12011209
ccr_original: str | None = None,
1210+
_deadline_started_at: float | None = None,
12021211
) -> KompressResult:
12031212
"""Compress content using Kompress model.
12041213
@@ -1223,6 +1232,7 @@ def compress(
12231232
Returns:
12241233
KompressResult with compressed text.
12251234
"""
1235+
t_deadline = time.perf_counter() if _deadline_started_at is None else _deadline_started_at
12261236
words = content.split()
12271237
n_words = len(words)
12281238

@@ -1238,13 +1248,7 @@ def compress(
12381248
# Cached per instance: operator config, read once -- not per compress() call.
12391249
deadline_s = getattr(self, "_deadline_s", None)
12401250
if deadline_s is None:
1241-
try:
1242-
deadline_s = max(
1243-
0.0,
1244-
float(os.environ.get("HEADROOM_COMPRESSION_DEADLINE_MS", "20000")) / 1000.0,
1245-
)
1246-
except ValueError:
1247-
deadline_s = 20.0
1251+
deadline_s = _request_deadline_seconds()
12481252
self._deadline_s = deadline_s
12491253

12501254
try:
@@ -1263,6 +1267,7 @@ def compress(
12631267
target_ratio=[target_ratio],
12641268
batch_size=_batch_size(),
12651269
ccr_originals=[ccr_original],
1270+
_deadline_started_at=t_deadline,
12661271
)
12671272
if batch_result:
12681273
return batch_result[0]
@@ -1271,7 +1276,6 @@ def compress(
12711276
kept_ids: set[int] = set()
12721277
inference_ms = 0.0
12731278
chunk_count = 0
1274-
t_deadline = time.perf_counter()
12751279

12761280
acquire_timeout = _acquire_timeout_seconds()
12771281
budget = _time_budget_seconds()
@@ -1327,12 +1331,29 @@ def compress(
13271331
input_ids = input_ids.to(device)
13281332
attention_mask = attention_mask.to(device)
13291333

1334+
request_remaining: float | None = None
1335+
if deadline_s:
1336+
request_remaining = deadline_s - (time.perf_counter() - t_deadline)
1337+
if request_remaining <= 0:
1338+
kept_ids.update(range(chunk_start, n_words))
1339+
logger.warning(
1340+
"Kompress hit %.1fs deadline before acquire after %d/%d words "
1341+
"(%d chunks done); kept remainder verbatim to free the request "
1342+
"thread (#1171)",
1343+
deadline_s,
1344+
chunk_start,
1345+
n_words,
1346+
chunk_count,
1347+
)
1348+
break
1349+
13301350
acquire_bounds = [
13311351
bound
13321352
for bound in (
13331353
_execution_wait_budget_seconds(),
13341354
acquire_timeout,
13351355
remaining,
1356+
request_remaining,
13361357
)
13371358
if bound is not None
13381359
]
@@ -1471,6 +1492,7 @@ def compress_batch(
14711492
batch_size: int = 32,
14721493
*,
14731494
ccr_originals: list[str | None] | None = None,
1495+
_deadline_started_at: float | None = None,
14741496
) -> list[KompressResult]:
14751497
"""Compress multiple texts. Uses batched inference on GPU, sequential on CPU.
14761498
@@ -1531,6 +1553,7 @@ def compress_batch(
15311553
n = len(contents)
15321554
if n == 0:
15331555
return []
1556+
t_deadline = time.perf_counter() if _deadline_started_at is None else _deadline_started_at
15341557

15351558
# Normalize target_ratio to a per-text list
15361559
if isinstance(target_ratio, list):
@@ -1572,6 +1595,7 @@ def compress_batch(
15721595
question=question,
15731596
target_ratio=r,
15741597
ccr_original=ccr_source,
1598+
_deadline_started_at=t_deadline,
15751599
)
15761600
for content, r, ccr_source in zip(contents, ratios, ccr_sources, strict=True)
15771601
]
@@ -1608,6 +1632,10 @@ def compress_batch(
16081632
device_type = _model_device_type(model, backend)
16091633
kept_ids_per_text: dict[int, set[int]] = {i: set() for i in range(n) if results[i] is None}
16101634
inference_ms = 0.0
1635+
deadline_s = getattr(self, "_deadline_s", None)
1636+
if deadline_s is None:
1637+
deadline_s = _request_deadline_seconds()
1638+
self._deadline_s = deadline_s
16111639

16121640
acquire_timeout = _acquire_timeout_seconds()
16131641
budget = _time_budget_seconds()
@@ -1637,6 +1665,9 @@ def _bail_remaining(reason: str, batch_start: int) -> None:
16371665
if remaining <= 0:
16381666
_bail_remaining("time budget exhausted", batch_start)
16391667
break
1668+
if deadline_s and (deadline_s - (time.perf_counter() - t_deadline)) <= 0:
1669+
_bail_remaining("request deadline exhausted", batch_start)
1670+
break
16401671

16411672
batch = chunk_queue[batch_start : batch_start + batch_size]
16421673
batch_word_lists = [c[2] for c in batch]
@@ -1660,13 +1691,21 @@ def _bail_remaining(reason: str, batch_start: int) -> None:
16601691
input_ids = input_ids.to(device)
16611692
attention_mask = attention_mask.to(device)
16621693

1694+
request_remaining: float | None = None
1695+
if deadline_s:
1696+
request_remaining = deadline_s - (time.perf_counter() - t_deadline)
1697+
if request_remaining <= 0:
1698+
_bail_remaining("request deadline exhausted", batch_start)
1699+
break
1700+
16631701
# Single forward pass for all chunks in this batch.
16641702
acquire_bounds = [
16651703
bound
16661704
for bound in (
16671705
_execution_wait_budget_seconds(),
16681706
acquire_timeout,
16691707
remaining,
1708+
request_remaining,
16701709
)
16711710
if bound is not None
16721711
]

tests/test_content_router_single_item_deadline.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import time
44

5+
import headroom.transforms.kompress_compressor as kc
56
from headroom.transforms.content_detector import ContentType
67
from headroom.transforms.content_router import (
78
CompressionStrategy,
@@ -10,6 +11,7 @@
1011
RouterCompressionResult,
1112
RoutingDecision,
1213
)
14+
from headroom.transforms.kompress_compressor import KompressCompressor, KompressConfig
1315

1416

1517
class _Tokenizer:
@@ -48,7 +50,7 @@ def _messages() -> list[dict[str, str]]:
4850
{"role": "assistant", "content": "frozen prefix content remains unchanged"},
4951
{
5052
"role": "assistant",
51-
"content": "pending cache miss content takes the inline compression branch",
53+
"content": "pending cache miss content takes the inline compression branch today",
5254
},
5355
]
5456

@@ -112,3 +114,71 @@ def test_single_cache_miss_preserves_disabled_deadline(monkeypatch):
112114
)
113115

114116
assert result.messages[1]["content"] == "compressed output"
117+
118+
119+
def test_single_cache_miss_deadline_starts_before_kompress_load(monkeypatch, caplog):
120+
router = _router()
121+
122+
class _Encoding(dict):
123+
def __init__(self, rows: list[list[str]]):
124+
super().__init__(
125+
input_ids=[[0] * len(row) for row in rows],
126+
attention_mask=[[1] * len(row) for row in rows],
127+
)
128+
self._rows = rows
129+
130+
def word_ids(self, batch_index: int = 0):
131+
return list(range(len(self._rows[batch_index])))
132+
133+
class _Tokenizer:
134+
def count_text(self, content: str) -> int:
135+
return len(content.split())
136+
137+
def __call__(self, words, **_kwargs):
138+
rows = words if words and isinstance(words[0], list) else [words]
139+
return _Encoding(rows)
140+
141+
class _Model:
142+
def __init__(self):
143+
self.calls = 0
144+
145+
def get_keep_mask(self, input_ids, attention_mask):
146+
self.calls += 1
147+
return [[i % 2 == 0 for i in range(len(row))] for row in input_ids]
148+
149+
model = _Model()
150+
compressor = KompressCompressor(config=KompressConfig(enable_ccr=False))
151+
monkeypatch.setattr(compressor, "_should_batch_single_content", lambda *a, **k: False)
152+
load_state = {"calls": 0}
153+
154+
def _slow_load(*_args, **_kwargs):
155+
load_state["calls"] += 1
156+
time.sleep(0.05)
157+
return model, _Tokenizer(), "onnx"
158+
159+
monkeypatch.setattr(kc, "_load_kompress", _slow_load)
160+
monkeypatch.setattr(
161+
router,
162+
"compress",
163+
lambda content, *, context="", bias=1.0: _compression_result(
164+
content,
165+
compressor.compress(content).compressed,
166+
),
167+
)
168+
monkeypatch.setenv("HEADROOM_COMPRESSION_DEADLINE_MS", "10")
169+
170+
started = time.perf_counter()
171+
result = router.apply(
172+
_messages(),
173+
_Tokenizer(),
174+
frozen_message_count=1,
175+
min_tokens_to_compress=1,
176+
)
177+
elapsed = time.perf_counter() - started
178+
time.sleep(0.1)
179+
180+
assert elapsed < 0.12
181+
assert result.messages[1]["content"] == _messages()[1]["content"]
182+
assert "failing open via PASSTHROUGH" in caplog.text
183+
assert load_state["calls"] == 1
184+
assert model.calls == 0

0 commit comments

Comments
 (0)