6.2 KiB
Gate Cascade (5-Gate Correctness Middleware)
Every code change the Recommendation / specialist agents propose is
validated by a strict 5-gate cascade before the change is considered
accepted. The cascade runs as middleware — not inside any agent
— so no agent can reorder or skip gates. The Correctness agent
receives an immutable GateVerdict and narrates it; that's the only
agent contact surface.
Source of truth: perfxpert/runtime/gate_cascade.py (spec §5, §5.0).
Cross-links:
- Agent hierarchy — where Correctness fits in the tier map
- Agentic mode — how verdicts surface to end-users (LLM vs air-gap)
The 5 gates
Strict order — ALL must pass:
| # | Gate | Tool delegated to | Reject condition |
|---|---|---|---|
| 1 | Compile / Run | tools.compile.build |
Build failure (non-zero exit) |
| 2 | SOL sanity (anti-Sakana) | tools.sol.sanity_check |
See below — two-tier |
| 3 | Bitwise / Numeric | tools.patch.verify_output |
Output drift > tolerance |
| 4 | Regression (weighted-geomean) | tools.regression.compare_runs |
See below — three conditions |
| 5 | Test Anchors | tools.anchors.check |
Any prior-pass test now fails |
Gates 1, 3, 4, and 5 invoke EXECUTION-class tools; see mcp-server.md for why this matters. Gate 2 invokes a READ_ONLY tool but is owned by the middleware layer for verdict-coherence reasons.
Two-tier SOL check (Gate 2)
Gate 2 is the primary anti-Sakana defense against reward-hacking (LLM claims a wildly unrealistic speedup to satisfy the optimization target).
It runs in two tiers:
-
Hard cap on speedup —
SOL_MAX_REASONABLE_SPEEDUP = 50.0. Anyclaimed_speedup > 50×is rejected immediately, regardless of architecture or absolute FLOPS. This catches gross reward-hacking with no GPU-specific setup required. -
Absolute-FLOPS check (optional) — when
achieved_flops_per_secis known, the gate compares it to the architecture's published peak (viaarch.lookup_peaks). Any optimized run that claimsflops > peakis rejected.
Either failing tier produces GateVerdict(status="reject", failing_gate="sol", ...).
Regression gate (Gate 4) — three reject paths
The regression gate passes only if all three of these are satisfied:
| Check | Threshold | Constant |
|---|---|---|
| Any individual hot kernel regressed | > 10% → reject | HOT_KERNEL_INDIVIDUAL_THRESHOLD_PCT |
| Weighted-geomean degradation on tail | > 5% → reject | TAIL_GEOMEAN_THRESHOLD_PCT |
| Total regression vs baseline | > 3% → reject | REGRESSION_NOISE_THRESHOLD_PCT |
"Hot kernel" = top-K covering 80% of cumulative runtime, plus any kernel ≥ 3% individually (spec §5 definition). Weighted-geomean is the noise-robust metric for the tail of non-hot kernels: sensitive enough to catch cross-the-board regressions that individual thresholds miss, but immune to single-run jitter.
The gate returns status="regressed" (not "reject") to distinguish
"code is semantically correct but slower" from "code is broken".
Test-anchor gate (Gate 5) — last line of defense
Test anchors are a small set of known-good unit or integration tests
that must stay green across every optimization cycle. If any anchor
test that previously passed now fails, the gate returns
status="reject", failing_gate="anchors".
Anchors are NOT the full test suite — they're a curated subset chosen to catch regressions the earlier gates miss (e.g. numerical drift inside a tolerance band that the bitwise gate accepted).
Verdict propagation
evaluate() / run_gate_cascade() return a single immutable
GateVerdict:
from perfxpert.runtime.gate_cascade import run_gate_cascade, GateInput
# Minimum required fields — everything else is optional and defaults to
# None. In this shape the cascade runs gates 2 (SOL) + 4 (regression)
# only; gates 1/3/5 short-circuit when their optional inputs are absent.
# For a full 5-gate run, supply source_file + diff_payload (compile),
# verify_output_baseline/new (bitwise), and test_anchor_baseline/new.
verdict = run_gate_cascade(GateInput(
kernel_name="my_kernel",
claimed_speedup=1.15,
arch="gfx942",
baseline_runtime_ns=1_000_000,
achieved_runtime_ns=870_000,
patch_sha="abc123",
))
print(verdict.status) # 'pass' | 'reject' | 'regressed'
print(verdict.failing_gate) # None on pass; gate name on reject
print(verdict.detail) # Human-readable reason
print(verdict.delta_pct) # Only meaningful for regression verdicts
Statuses:
pass— all 5 gates green; the patch is accepted.reject— a non-regression gate failed;failing_gatesays which.regressed— the code compiled + ran + matched output but was measurably slower. The Recommendation agent treats this as a weaker rejection (may try a different technique from the same specialist's knowledge file).
Note that GateVerdict is a frozen dataclass — agents that receive it
cannot mutate it. The Correctness agent's fence slice
(perfxpert/agents/fence/correctness.md) explicitly instructs it to
narrate detail verbatim and never override status.
Debug-loop caps (spec §5)
To bound cost in agentic-LLM mode, the cascade is invoked under three session-level caps:
| Constant | Value | Meaning |
|---|---|---|
MAX_OPTIMIZATION_CYCLES_PER_KERNEL |
5 | How many patch-then-verify rounds per hot kernel before giving up |
MAX_CONSECUTIVE_FAILURES |
3 | Stop after 3 consecutive verdicts ≠ pass |
MAX_SESSION_LLM_TURNS |
100 | Hard cap on total LLM turns in one session |
These constants are verified at runtime; see
tests/test_runtime/test_gate_cascade.py for coverage.
Why middleware, not agent
Design-review C1 in the spec: if the Correctness agent ran the
gates, it could re-order or skip them. Keeping gates in middleware
means the verdict is structurally incapable of being tampered with by
any agent — the adversarial model includes the LLM itself. This
invariant is test-enforced in
tests/test_agents/test_correctness.py (see
test_correctness_has_no_gate_tools), which asserts Correctness's
AgentSpec.tools allowlist contains zero EXECUTION tools.