Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: env install lint typecheck test check up down migrate fetch-azure ingest-azure evaluate evaluate-calibration validate-simulator evaluate-frontier evaluate-gpu evaluate-commitment snapshot api
.PHONY: env install lint typecheck test check up down migrate fetch-azure ingest-azure evaluate evaluate-calibration validate-simulator evaluate-frontier evaluate-gpu evaluate-commitment evaluate-joint evaluate-threshold evaluate-predictability evaluate-q13b snapshot api

env:
conda create -y -n delphi python=3.12
Expand Down Expand Up @@ -50,6 +50,18 @@ evaluate-gpu:
evaluate-commitment:
python scripts/evaluate_commitment.py

evaluate-joint:
python scripts/evaluate_joint_provisioning.py

evaluate-threshold:
python scripts/evaluate_threshold.py

evaluate-predictability:
python scripts/compare_predictability.py

evaluate-q13b:
python scripts/evaluate_q13b.py

snapshot:
python scripts/build_snapshot.py

Expand Down
115 changes: 109 additions & 6 deletions docs/EVAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,36 @@ once on the preceding validation stream from `{0.001, 0.005, 0.01, 0.02, 0.05}`;
on the test trace. Recovery is the first point at which trailing 24-hour coverage returns within
five percentage points of nominal p95. Run `make evaluate-calibration` to reproduce it.

**This answers Q2** — *does conformal calibration beat a fixed safety margin?* Registered
expectation: yes under drift, roughly neutral on stationary traces. **Verdict: confirmed.**
The fixed margin is the honest comparator and for some time this experiment did not include
one, measuring conformal only against an uncalibrated forecast — which is a strawman. Two are
now reported: the margin tuned on validation to hit nominal, and the conventional +15% that
Kubernetes VPA ships and that this repo's own `PercentileRecommender` defaults to.

| Method | p95 coverage | Mean p95 | Recovery steps (24 h window) |
|---|---:|---:|---:|
| `raw` | 0.906 | 98.751 | 46 |
| `fixed_margin_tuned_+0%` | 0.906 | 98.751 | 46 |
| `fixed_margin_conventional_+15%` | 0.917 | 113.563 | 46 |
| `split_conformal` | 0.903 | 98.505 | 46 |
| `aci_gamma_0.05` | 0.927 | 102.122 | 28 |
| `aci_gamma_0.05` | **0.924** | **101.976** | **28** |

**ACI wins on every axis that matters.** Against the conventional +15% margin it holds better
coverage (0.924 versus 0.917) while provisioning **11% less capacity** (101.98 versus 113.56),
and it recovers from the shift in 28 steps where the margin never recovers inside the window
at all — a fixed margin cannot recover, because it does not respond to anything.

**The tuned margin is the more interesting result.** Raw p95 already covers 96.9% on the
stationary validation stream, so the smallest margin reaching the 95% target is **+0%**: an
operator tuning headroom honestly on pre-shift data would add none, and would then be
completely unprotected when the level moved. Buying safety by tuning a constant on quiet data
is not conservative, it only looks conservative.

Static split conformal does not survive the distribution shift in this slice: its validation
correction slightly lowers test coverage. ACI remains below nominal over the full transient but
cuts recovery time by 18 steps. Coverage is retained observation-by-observation, not only as the
three scalar summaries above.
Static split conformal does not survive the distribution shift in this slice either: its
validation correction slightly lowers test coverage. ACI remains below nominal over the full
transient but cuts recovery time by 18 steps. Coverage is retained observation-by-observation,
not only as the scalar summaries above.

---

Expand Down Expand Up @@ -294,7 +314,13 @@ lane unchanged rather than introducing a second, unvalidated simulator.
load — the raw prefill:decode token ratio is 110.7:1 versus 15.5:1. Two workloads on the
same hardware with completely different capacity shapes.

## M16 — is request-rate autoscaling structurally wrong here?
## M16 — is request-rate autoscaling structurally wrong here? (**this answers Q6**)

> **Q6, pre-registered:** *Is CPU-threshold scaling structurally wrong for GPU inference?*
> Registered expectation: yes, large, and widening with `startup_seconds`. **Verdict:
> confirmed.** The measurement is below; it was carried out under its milestone number and
> went for some time without being tied back to the question it answers, which is a
> bookkeeping failure in a project whose central discipline is exactly that ledger.

**Yes, and it is measurable.** Every proxy below is first rescaled to the true demand's
mean, so what remains is error in *shape*: a proxy that merely needed a different constant
Expand Down Expand Up @@ -1370,3 +1396,80 @@ recommendation is withdrawn from the README.
- **A promising exploratory AUC was the winner's curse**, and pre-registration is what caught
it rather than hindsight.
- **This project publicly recommended an improvement that its own next experiment refuted.**

---

# Q9 — independent per-resource sizing, measured at last (2026-08-09)

Registered as *"do independent per-resource forecasts over-provision the joint plan?"* with
the expectation "yes; the interesting part is by how much". **It had never been run.**
`generate_multi_resource` existed and was unit-tested, but no experiment used it — while
`docs/ROADMAP.md` claimed the synthetic regime "carries the joint-provisioning test". It did
not, and that sentence is now corrected.

## Result: the registered direction is refuted

A replica supplies a fixed amount of each resource, so the replicas needed at time *t* are set
by whichever resource is tightest: `r_t = max(cpu_t/cpu_per, memory_t/memory_per)`. Sizing on
the first half of the trace, scoring on the second, with each replica scaled so **either
resource alone** needs 8 replicas at its own target quantile:

| offset | correlation | joint repl. | indep. repl. | joint coverage | indep. coverage |
|---:|---:|---:|---:|---:|---:|
| 0.000 | +0.778 | 9 | 8 | 0.9688 | 0.8442 |
| 0.125 | +0.529 | 9 | 8 | 0.9688 | 0.8284 |
| 0.250 | +0.010 | 9 | 8 | 0.9688 | 0.8229 |
| 0.375 | −0.518 | 9 | 8 | 0.9688 | 0.8075 |
| 0.500 | −0.770 | 9 | 8 | 0.9688 | **0.8016** |

*(target q = 0.90; `offset` lags memory's daily cycle by a fraction of a day, so 0 means the
two resources rise together and 0.5 means one peaks while the other troughs.)*

**Independent sizing under-provisions. It does not over-provision.** The arithmetic makes this
inevitable and the registered expectation had the sign backwards: since `max(a, b) >= a`, the
quantile of the joint requirement is never below either marginal quantile, so combining
independently-sized resources can only buy less than the joint plan — never more.

What is genuinely measured is the **coverage** that costs, and how it scales. Asked for 90%,
the independent method delivers 84.4% when the two resources move together and **80.2%** when
they are anti-phase, against the joint plan's steady 96.9%. The shortfall widens monotonically
as correlation falls from +0.78 to −0.77, which is the mechanism showing itself: the more the
peaks avoid each other, the more often the resource you are not looking at is the binding one.

The practical statement: **you cannot reach a joint SLO by watching one dashboard per
resource.** The gap is not a tuning error to be closed with headroom on each chart; it is
structural, and it grows precisely as the resources become more independent.

## Three nulls before a signal, and why they are in the record

The first three designs returned exact zeros, and each was a broken instrument rather than an
absent effect. They are recorded because a null from an instrument that cannot detect the
thing is not evidence of absence, and because the sequence is the honest account of how this
number was arrived at.

1. **Peaks narrower than the tail.** The fixture injects one peak per resource spanning 1.56%
of the series. A q = 0.95 quantile discards the top 5%, so the peaks sat entirely inside
the discarded region and could not move any sizing decision. Diagnosed by checking where
the peaks fell relative to the quantile, *before* interpreting the null.
2. **Peaks in only one half of the split.** Widening them did not help, because the fixture
places the CPU peak at 33% of the series and the memory peak at 66% — so the training half
contained only one of them and neither method could size for a peak it had never seen. The
fix was decorrelation that *recurs*: anti-phase daily cycles rather than one-shot events,
which is also what real multi-resource workloads look like.
3. **One resource dominating the maximum.** Scaling each replica off the resource's median
equalises the two medians but not their ranges, so the wider-swinging resource was the
binding one at every single step and decorrelation had nothing to act on — which is why
the numbers were identical to four decimal places across every phase offset. Sizing so
each resource alone needs the same replicas at its own target quantile fixed it.

Each change was made to an instrument that provably could not see the effect, not to a result
that was inconvenient. The tell in every case was a null that was *too clean* — identical
values across conditions that should have differed.

## Added to the negatives ledger

- **Q9's registered direction was wrong.** Independent per-resource sizing under-provisions
rather than over-provisions, and the sign follows from `max(a, b) >= a` without needing an
experiment at all.
- **A pre-registered question went unmeasured for the life of the project** while a shipped
document implied it had been answered.
8 changes: 6 additions & 2 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,9 @@ a considerably smaller claim than the one the project started with, and it is th
measurements support.

Bitbrains is not on the critical path because its canonical host could not be verified; the
Materna fleet traces and the synthetic multi-resource regime carry the joint-provisioning
test without redistributing unclearly licensed data.
Materna fleet traces stand in for it without redistributing unclearly licensed data.

The joint-provisioning question (Q9) is answered on the synthetic multi-resource regime — see
`docs/EVAL.md`. This sentence previously claimed that regime "carries the joint-provisioning
test" while no such experiment existed, which was an overclaim and is corrected rather than
quietly deleted.
62 changes: 62 additions & 0 deletions scripts/build_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,68 @@ def findings() -> list[Finding]:
"miscalibrated tail buys capacity that does not pay for itself."
),
),
Finding(
question_id="Q2",
question="Does conformal calibration beat a fixed safety margin?",
prior="Yes under drift, roughly neutral on stationary traces.",
answer=(
"Yes, on every axis. Through a level shift, adaptive conformal inference holds "
"better coverage than the conventional +15% headroom margin (0.924 vs 0.917) "
"while provisioning 11% less capacity, and recovers in 28 steps where the "
"margin never recovers inside the window."
),
verdict="confirmed",
evidence=(
"The comparator matters and for some time this experiment lacked one, "
"measuring conformal only against an uncalibrated forecast. Two fixed margins "
"are now reported. The tuned one is the more instructive: raw p95 already "
"covers 96.9% on the stationary validation stream, so the smallest margin "
"reaching the 95% target is +0% — an operator tuning headroom honestly on "
"quiet data adds none, and is then wholly unprotected when the level moves. "
"A fixed margin cannot recover from drift because it does not respond to it."
),
),
Finding(
question_id="Q9",
question="Do independent per-resource forecasts misprovision the joint plan?",
prior="Yes, they over-provision; the interesting part is by how much.",
answer=(
"They misprovision, but in the opposite direction to the one registered: "
"independent sizing under-provisions. Asked for 90% coverage it delivers "
"84.4% when CPU and memory move together and 80.2% when they are anti-phase, "
"against a joint plan's steady 96.9%."
),
verdict="refuted",
evidence=(
"A replica is sized by whichever resource is tightest, so the requirement is "
"max(cpu/cpu_per, memory/memory_per). Because max(a,b) >= a, the joint "
"quantile is never below either marginal one and combining independently "
"sized resources can only buy less — the registered direction was wrong on "
"arithmetic alone. The shortfall widens monotonically as correlation falls "
"from +0.78 to -0.77: the more the peaks avoid each other, the more often the "
"resource you are not watching is the binding one. You cannot reach a joint "
"SLO from one dashboard per resource."
),
),
Finding(
question_id="Q6",
question="Is CPU-threshold scaling structurally wrong for GPU inference?",
prior="Expected yes, expected large, expected to widen with cold start.",
answer=(
"Yes. Tracking request rate instead of token work correlates at 0.9961 and "
"still under-provisions by 39% in the tail, because correlation says nothing "
"about the peaks that breach an SLO."
),
verdict="confirmed",
evidence=(
"Every proxy is rescaled to the true demand's mean first, so what remains is "
"error in shape rather than a constant anyone could retune. Measured on the "
"Azure LLM inference traces where prefill and decode contend for one device. "
"This was carried out under its milestone number and went some time without "
"being tied back to the question it answers, which is a bookkeeping failure "
"in a project whose central discipline is exactly that ledger."
),
),
Finding(
question_id="Q10",
question="Is there a workload where no controller beats static provisioning?",
Expand Down
41 changes: 40 additions & 1 deletion scripts/evaluate_calibration.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
"""Measure raw, static conformal, and ACI p95 coverage through a level shift."""
"""Measure raw, fixed-margin, static conformal, and ACI p95 coverage through a level shift.

This answers **Q2** — *does conformal calibration beat a fixed safety margin?* The fixed
margin is the practitioner's alternative and the honest comparator: multiply the raw
forecast by ``1 + m`` and pick ``m`` as the smallest value hitting nominal coverage **on
validation only**, the same budget and the same discipline ACI's gamma gets. Comparing
conformal against an uncalibrated forecast alone would have been a strawman, and for some
time that is all this experiment did.
"""

from datetime import UTC, datetime

Expand Down Expand Up @@ -88,8 +96,39 @@ def main() -> None:
strict=True,
)
)
# --- Q2's comparator: a fixed safety margin, tuned on validation only ---------------
validation_raw = validation_predicted[:, p95_index]
margin = 1.0
for candidate in np.linspace(0.0, 1.0, 101):
if float(np.mean(validation_actual <= validation_raw * (1.0 + candidate))) >= 0.95:
margin = float(candidate)
break
fixed_margin_predictions = test_predicted[:, p95_index] * (1.0 + margin)
# The conventional margin as well as the tuned one. Kubernetes VPA ships a headroom
# multiplier and this repo's PercentileRecommender defaults to 0.15, so a comparator
# that only reports the tuned value would miss what an operator actually runs.
conventional = 0.15
conventional_predictions = test_predicted[:, p95_index] * (1.0 + conventional)
validation_raw_coverage = float(np.mean(validation_actual <= validation_raw))
print(
f"Raw p95 already covers {validation_raw_coverage:.1%} on validation, so the "
f"smallest margin reaching the 95% target is **+{margin:.0%}** — a margin tuned "
f"honestly on stationary data adds no headroom at all. The conventional "
f"+{conventional:.0%} is reported alongside it.\n"
)

methods = (
("raw", test_predicted[:, p95_index], tuple(test_actual <= test_predicted[:, p95_index])),
(
f"fixed_margin_tuned_{margin:+.0%}",
fixed_margin_predictions,
tuple(test_actual <= fixed_margin_predictions),
),
(
f"fixed_margin_conventional_{conventional:+.0%}",
conventional_predictions,
tuple(test_actual <= conventional_predictions),
),
("split_conformal", static_predictions, tuple(test_actual <= static_predictions)),
(
f"aci_gamma_{gamma.gamma:g}",
Expand Down
Loading
Loading