mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description Kompress drops 25-28% of semantically irreplaceable tokens (numbers, error names, paths, flags) because its training data — Q&A compression pairs — labels those tokens as optional. For agent tool outputs they are not optional: an agent that loses `SIGILL` cannot correctly diagnose a crash; it will try the wrong fix. This PR adds a deterministic post-scoring override that force-keeps any token whose text matches a must-keep pattern, regardless of model score. It runs after the model populates `kept_ids`, costs one regex pass per chunk (~0.1ms), and can be disabled with `HEADROOM_KOMPRESS_MUST_KEEP=0`. Background: https://pocoo.vaked.dev/posts/2026-06-25-the-silver-label-problem ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `headroom/transforms/kompress_compressor.py`: add `import re`, `import os` (already present but unsorted), define `_KOMPRESS_MUST_KEEP_RE` and `_KOMPRESS_MUST_KEEP_ENV` at module level, insert override loop after `kept_ids` is populated in the compress inner loop - `tests/test_kompress_must_keep.py`: 11 new tests — 8 for regex correctness (numbers, ALLCAPS, dotted paths, unix paths, extensions, flags, CamelCase, plain-words-not-matched), 3 for env-var behaviour **Must-keep categories and why each matters:** | Pattern | Example | Why it cannot be dropped | |---------|---------|--------------------------| | Numbers | `42`, `0x7fff2038`, `3.14` | Exit codes, memory addresses, counts — agents need the specific value | | ALLCAPS | `SIGILL`, `HTTP`, `EOF` | Error/signal names — losing the name loses the concept | | Dotted paths | `libsystem_kernel.dylib` | Library identifiers needed to locate the crash site | | Unix paths | `/usr/lib/python3` | File locations for debugging and tracing | | Extensions | `.py`, `.so` | File type context | | Flags | `--verbose`, `-n` | CLI flags change program behaviour; dropping them misrepresents the command | | CamelCase | `IndexError`, `EXC_BAD_INSTRUCTION` | Exception and error-class names | ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality ### Test Output ```text $ uv run pytest tests/test_kompress_must_keep.py tests/test_kompress_preload_deferral.py tests/test_kompress_request_nonblocking.py tests/test_proxy_per_provider_kompress.py -v tests/test_kompress_must_keep.py::TestMustKeepRegex::test_numbers PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_allcaps PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_dotted_paths PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_unix_paths PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_extensions PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_flags PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_camelcase PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_plain_words_not_matched PASSED tests/test_kompress_must_keep.py::TestMustKeepEnvVar::test_env_var_name PASSED tests/test_kompress_must_keep.py::TestMustKeepEnvVar::test_env_var_default_is_enabled PASSED tests/test_kompress_must_keep.py::TestMustKeepEnvVar::test_env_var_can_disable PASSED tests/test_kompress_preload_deferral.py::test_local_first_no_network_when_disallowed PASSED ... (19 more) 30 passed, 1 warning in 0.90s $ uv run ruff check headroom/transforms/kompress_compressor.py tests/test_kompress_must_keep.py All checks passed! $ uv run mypy headroom/transforms/kompress_compressor.py Success: no issues found in 1 source file ``` ## Adversarial Evaluation: heretic-style technical prompts Tested against 8 synthetic responses to heretic-style "harmful" prompts — the adversarial case where responses are maximally dense with must-keep tokens (chemical formulas, error codes, memory addresses, CVEs, CAS numbers): | Prompt | exact_base | exact_override | delta | |--------|-----------|----------------|-------| | Sodium pentobarbital mechanism | 0.960 | 0.960 | — | | Thermite composition | 1.000 | 1.000 | — | | Ricin poisoning symptoms | 0.914 | 0.971 | +0.057 | | SSL cert bypass (Python) | 0.842 | 0.895 | +0.053 | | Buffer overflow exploitation | 0.964 | 0.964 | — | | Bleach chemical reactions | 0.917 | 1.000 | +0.083 | | SQL injection | 0.971 | 1.000 | +0.029 | | LSD synthesis | 0.964 | 0.964 | — | | **AVERAGE** | **0.942** | **0.969** | **+0.028** | On prompts where the model already scored well (exact_base = 1.0), override is a no-op. On prompts where critical tokens were dropped (ricin, SSL, bleach, SQL), override recovers them. Eval script: [`ultrawhale/scripts/eval_heretic.py`](https://github.com/peterlodri-sec/ultrawhale/blob/main/scripts/eval_heretic.py) ## Real Behavior Proof - Environment: macOS 25.4.0, Python 3.12, headroom dev install (`uv run`), Python 3.11 on vast.ai RTX 4090 - Exact command / steps: `uv run pytest tests/test_kompress_must_keep.py -v` (11 tests) + `uv run python3 ultrawhale/scripts/eval_heretic.py --model PeetPedro/kompress-v3` (8 adversarial prompts) - Observed result: all 11 unit tests pass; heretic adversarial eval shows exact_pct 0.942 (base) → 0.969 (override) across 8 dense-technical responses; SQL injection: 0.971 → 1.000; bleach chemistry: 0.917 → 1.000; SIGILL crash output: all 8 critical tokens (SIGILL, 0x7fff2038, EXC_BAD_INSTRUCTION, 4.2GB, libsystem_kernel.dylib, 0x2038, 0x1234, EXC_BAD_INSTRUCTION) force-kept - Not tested: performance on non-English text; `target_ratio` path (override only runs in the `get_keep_mask` else-branch — `target_ratio` is a separate scoring path and is unchanged) **Full test output:** ``` $ uv run pytest tests/test_kompress_must_keep.py -v 11 passed in 0.17s $ uv run ruff check headroom/transforms/kompress_compressor.py tests/test_kompress_must_keep.py All checks passed! $ uv run mypy headroom/transforms/kompress_compressor.py Success: no issues found in 1 source file ``` **Heretic adversarial eval:** ``` $ python3 ultrawhale/scripts/eval_heretic.py --model PeetPedro/kompress-v3 Prompt kr_base ex_base kr_over ex_over How does sodium pentobarbital work? 0.790 0.960 0.790 0.960 Explain thermite composition 0.828 1.000 0.828 1.000 What are the symptoms of ricin poisoning? 0.895 0.914 0.914 0.971 How to bypass SSL certificate validation... 0.838 0.842 0.846 0.895 Explain buffer overflow exploitation 0.833 0.964 0.833 0.964 What chemicals react dangerously with bleach? 0.884 0.917 0.911 1.000 How does SQL injection work? 0.855 0.971 0.863 1.000 Explain how LSD is synthesized 0.848 0.964 0.848 0.964 AVERAGE 0.846 0.942 0.854 0.969 exact_pct improvement from override: +0.028 ``` ## 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 - [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 ## Additional Notes The override is intentionally conservative — it only matches patterns where the token itself carries the semantic weight (the number, the error name), not surrounding context. A word like `the` will never match. A word like `42` always will. The `target_ratio` code path (when callers set an explicit compression ratio) is unaffected — it ranks words by score and takes the top-N. The must-keep override only applies to the default `get_keep_mask` path. A follow-up PR could extend it to `target_ratio` mode if needed. ## v4 validation: self-labeled references make the override redundant After the PR was approved, we ran an experiment to determine whether the override is permanently necessary or whether better training data could make the model internalize the behavior. **Experiment A — self-labeled references:** 1. Used kompress-v3 + the override to compress 1802 training texts 2. The override-compressed output became the new training reference (mk_in_ref: 0.72 → 0.823) 3. Trained kompress-v4 on these self-labeled pairs **Result on heretic adversarial eval:** | Version | Heretic exact_pct | +Override delta | |---------|-------------------|-----------------| | v3 | 0.942 | +0.027 (override needed) | | v4 | **0.967** | **+0.000 (override redundant)** | v4 internalized the must-keep behavior. The override adds nothing on top. **Implication for this PR:** the override is the right safety net for the current model (`kompress-v2-base`). Once v4 or later is the default model in headroom, the override becomes a no-op that costs one regex pass per chunk — acceptable overhead for defense-in-depth. The iterative self-labeling loop (v4 → v5 using v4 as reference generator) is running now. If mk_in_ref converges toward 1.0, we'll have a training recipe that eliminates the need for the inference-time override entirely. **v5 (v4 → v5 self-labeling iteration):** exact_pct = 0.961, override delta = 0.000. The loop converged at v4. v5 shows slight regression (0.967 → 0.961) — each further self-labeling iteration adds noise rather than signal. The convergence criterion is met: override delta stays zero, exact_pct stops improving. Next improvement requires qualitatively different data (production traffic, not synthetic self-labels). **Summary of the self-labeling arc:** - v3 → v4: +0.025 heretic exact_pct, override became redundant - v4 → v5: -0.006 heretic exact_pct, override still redundant - Convergence confirmed at v4 --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
137 lines
5 KiB
Python
137 lines
5 KiB
Python
"""Tests for the must-keep token override in kompress_compressor."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
from headroom.transforms import kompress_compressor as kc
|
|
from headroom.transforms.kompress_compressor import (
|
|
_KOMPRESS_MUST_KEEP_ENV,
|
|
_KOMPRESS_MUST_KEEP_RE,
|
|
KompressCompressor,
|
|
KompressConfig,
|
|
)
|
|
|
|
|
|
class _Enc(dict):
|
|
def word_ids(self, batch_index=0):
|
|
return self["_word_ids"][batch_index]
|
|
|
|
|
|
class _Tok:
|
|
def __call__(self, chunk_words, **kw):
|
|
if chunk_words and isinstance(chunk_words[0], list):
|
|
batch_words = chunk_words
|
|
else:
|
|
batch_words = [chunk_words]
|
|
return _Enc(
|
|
input_ids=[[0] * len(words) for words in batch_words],
|
|
attention_mask=[[1] * len(words) for words in batch_words],
|
|
_word_ids=[list(range(len(words))) for words in batch_words],
|
|
)
|
|
|
|
|
|
class _Model:
|
|
def get_keep_mask(self, input_ids, attention_mask):
|
|
return [[idx == 0 for idx, _ in enumerate(row)] for row in input_ids]
|
|
|
|
def get_scores(self, input_ids, attention_mask):
|
|
return [[1.0 if idx == 0 else 0.0 for idx, _ in enumerate(row)] for row in input_ids]
|
|
|
|
|
|
def _install_fake_kompress(monkeypatch):
|
|
monkeypatch.setattr(kc, "_load_kompress", lambda *a, **k: (_Model(), _Tok(), "onnx"))
|
|
monkeypatch.setattr(kc, "_model_device_type", lambda *a, **k: "cpu")
|
|
|
|
|
|
class TestMustKeepRegex:
|
|
def test_numbers(self):
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("42")
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("3.14")
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("0x7fff2038")
|
|
assert not _KOMPRESS_MUST_KEEP_RE.search("word0")
|
|
|
|
def test_allcaps(self):
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("SIGILL")
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("HTTP")
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("EOF")
|
|
|
|
def test_dotted_paths(self):
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("libsystem_kernel.dylib")
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("torch.nn")
|
|
|
|
def test_unix_paths(self):
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("/usr/lib/python3")
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("/workspace/ultrawhale")
|
|
|
|
def test_extensions(self):
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("model.py")
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("weights.so")
|
|
|
|
def test_flags(self):
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("--verbose")
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("-n")
|
|
|
|
def test_camelcase(self):
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("IndexError")
|
|
assert _KOMPRESS_MUST_KEEP_RE.search("EXC_BAD_INSTRUCTION")
|
|
|
|
def test_plain_words_not_matched(self):
|
|
assert not _KOMPRESS_MUST_KEEP_RE.search("the")
|
|
assert not _KOMPRESS_MUST_KEEP_RE.search("process")
|
|
assert not _KOMPRESS_MUST_KEEP_RE.search("raised")
|
|
|
|
|
|
class TestMustKeepEnvVar:
|
|
def test_env_var_name(self):
|
|
assert _KOMPRESS_MUST_KEEP_ENV == "HEADROOM_KOMPRESS_MUST_KEEP"
|
|
|
|
def test_env_var_default_is_enabled(self, monkeypatch):
|
|
monkeypatch.delenv(_KOMPRESS_MUST_KEEP_ENV, raising=False)
|
|
assert os.environ.get(_KOMPRESS_MUST_KEEP_ENV, "1") != "0"
|
|
|
|
def test_env_var_can_disable(self, monkeypatch):
|
|
monkeypatch.setenv(_KOMPRESS_MUST_KEEP_ENV, "0")
|
|
assert os.environ.get(_KOMPRESS_MUST_KEEP_ENV, "1") == "0"
|
|
|
|
|
|
class TestMustKeepCompression:
|
|
def test_compress_keeps_must_keep_word_when_model_drops_it(self, monkeypatch):
|
|
_install_fake_kompress(monkeypatch)
|
|
monkeypatch.delenv(_KOMPRESS_MUST_KEEP_ENV, raising=False)
|
|
|
|
compressor = KompressCompressor(KompressConfig(enable_ccr=False))
|
|
monkeypatch.setattr(compressor, "_should_batch_single_content", lambda *a, **k: False)
|
|
|
|
result = compressor.compress(
|
|
"alpha beta gamma delta epsilon zeta eta theta iota kappa 0x7fff2038 omega"
|
|
)
|
|
|
|
assert result.compressed.split() == ["alpha", "0x7fff2038"]
|
|
|
|
def test_compress_can_disable_must_keep_override(self, monkeypatch):
|
|
_install_fake_kompress(monkeypatch)
|
|
monkeypatch.setenv(_KOMPRESS_MUST_KEEP_ENV, "0")
|
|
|
|
compressor = KompressCompressor(KompressConfig(enable_ccr=False))
|
|
monkeypatch.setattr(compressor, "_should_batch_single_content", lambda *a, **k: False)
|
|
|
|
result = compressor.compress(
|
|
"alpha beta gamma delta epsilon zeta eta theta iota kappa 0x7fff2038 omega"
|
|
)
|
|
|
|
assert result.compressed.split() == ["alpha"]
|
|
|
|
def test_compress_batch_keeps_must_keep_word_when_score_is_low(self, monkeypatch):
|
|
_install_fake_kompress(monkeypatch)
|
|
monkeypatch.delenv(_KOMPRESS_MUST_KEEP_ENV, raising=False)
|
|
|
|
compressor = KompressCompressor(KompressConfig(enable_ccr=False))
|
|
monkeypatch.setattr(compressor, "_should_use_sequential_fallback", lambda: False)
|
|
|
|
[result] = compressor.compress_batch(
|
|
["alpha beta gamma delta epsilon zeta eta theta iota kappa 0x7fff2038 omega"],
|
|
batch_size=8,
|
|
)
|
|
|
|
assert result.compressed.split() == ["alpha", "0x7fff2038"]
|