docs(results): how much extraction leaves on the table — greedy DP vs exact - #1115
Merged
Conversation
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
🤖 Hi @jppittman, I've received your request, and I'm working on it now! You can track my progress in the logs for more details. |
…ct reference `extract_dag` is not argmin under its own cost model three ways over (issue #1111): it sums a TREE cost so sharing is never priced, it is a single DFS rather than a fixpoint so a class whose child is `on_stack` is scored `CYCLE_COST` and never revisited, and `total_cost` is read (extract.rs:1636) before `repair_choices_well_founded` may change the choices (:1639). That is why every budget-loosening change (#1101, #1109, #1114) improves most kernels and regresses a minority: a larger e-graph is not monotonically better under a lossy chooser, even though the exact optimum over it can only fall. This measures the gap. Two references, both under `CostModel::latency_prior()` and `Optimizer::production()`, so the comparison is chooser-vs-chooser: - Knuth's algorithm (Dijkstra generalized to AND-OR graphs) — the EXACT minimum tree cost, in polynomial time, on every kernel. That is the objective the DP names, so it isolates the DP failing its own objective from the objective being the wrong one. - A branch-and-bound over per-class choices for the true DAG optimum, with dominance filtering, a chain lower bound, and reduced-cost fixing. NP-hard, so it is budgeted and reports UNSOLVED rather than a truncated answer; a brute-force enumeration cross-check pins it on every instance small enough to enumerate. 302 kernels (206 real arena dumps + a synthetic size ladder). Headline: the DP misses its own tree optimum by p90 3.26% and worst 26.79%, attaining it on 230/302; the exact DAG optimum closes on 89 and greedy is optimal on 86 of those. `total_cost` fails to describe the returned term on 132/302. And on glyph16/32:U+004B — one of #1114's regressions — the roomier e-graph provably CONTAINS a term at 1013 while greedy returns 1132 against 1047 on the smaller graph: an extraction failure, not a budget failure. Measurement only; no production behavior changes. The probe lives in its own file rather than growing runtime.rs (2,631 lines and flagged for size), and the arena-dump loader both probes share moves to `arena_corpus`. Results: docs/results/2026-09-02-extraction-gap.{md,csv,json} Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…trades The measurement landed in the previous commit; this is what a sequencing decision needs from it, written into the same doc. Headline, with its caveats attached: the greedy DP leaves **0.258% pooled / 14.557% worst case** against the TREE optimum — the objective `extract_dag` itself names — over all 302 kernels with nothing excluded, and that is a floor. The true DAG optimum is lower but NP-hard: it closed on 89 of 302, and exactness stops at ~100-200 reachable classes while a production glyph saturates to a median 1,755. The two references also describe different populations — all 68 kernels the tree optimum beats are real (64 glyph, 4 shader), zero synthetic — so no single averaged ratio is honest. Mechanism split, and what each fix buys: - (i) single DFS, not a fixpoint — 5% of the proved loss, but **0.258% pooled over all 302 with no time limit, 68 kernels improved, 14.557% worst**, and it is the cheapest of the three. FIRST. - (ii) tree cost instead of DAG cost — 95% of the proved loss, but that is a small-kernel statement: beyond the tree optimum the entire certified headroom on this corpus is **0.028% pooled**. A research direction, not a fix. - (iii) `total_cost` read before `repair_choices_well_founded` — recovers no cycles at all, and is worse than "stale": of the 132/302 mismatches **not one** is ordinary-magnitude. 92 report exactly `usize::MAX` (the `.unwrap_or` at extract.rs:1636, taken when the DFS never resolves the root) and 40 report 1x, 2x or 3x `usize::MAX / 4`, the `Dwrt` sentinel from cost.rs:292. No shipped result is corrupted by it — #1101/#1109/#1114's harnesses re-score the arena (`arena_static_cost`) and `guide_headroom.rs:395`'s corpus holds no sentinel — so it is a live trap, not a live wound. Free to fix. Sequencing: fix extraction before taking #1101, #1109 or #1114. The two `U+004B` regressions in #1114 are extraction failures, not budget failures — the roomier e-graph provably contains a term at 1013 while greedy returns 1132 — so the fixpoint turns them into improvements, which no budget tuning can do. `psychedelic` stays unresolved rather than exonerated. Cost: extraction runs ONCE per compile; saturation runs 8,729,067 rule applications over 800 expressions in the guide-headroom corpus, and #1114 doubles that for +2.03%. A log factor on the once-per-compile pass is the cheap place to spend. Cross-links #1111, §1.L5 of docs/plans/2026-09-02-optimizer-api.md, #1101, #1109, #1114. Docs only; no code touched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jppittmanforce-pushed
the
claude/extraction-gap
branch
from
September 2, 2026 11:32
f99ec00 to
8efb595Comparejppittman
enabled auto-merge (squash)
September 2, 2026 11:33
Uh oh!
There was an error while loading. Please reload this page.
jppittman added a commit
that referenced
this pull request
Sep 2, 2026
…y returns (#1117) `extract_dag_scoped` read `total_cost` from the DP table at `:1636`, let `repair_choices_well_founded` rewrite the choice map at `:1639`, and returned the pre-repair number alongside the post-repair choices at `:1661`. #1115's harness measured the consequence: **the reported cost differs from the true cost of the returned term on 132 of 302 kernels** (`docs/results/2026-09-02-extraction-gap.md`, `reported_matches_returned`). `extract()` had the same shape, and `build_extracted_dag_from_choices` fabricated `total_cost: 0` outright. Correctness fix. **No objective change** — the DP chooses exactly what it chose before, and every `choices` vector is byte-identical. Only the reported numbers change. ## What the fix is A new `cost_of_choices(egraph, root, choices, costs, shape) -> ChoiceCost` costs *the choice map it is handed* — nothing is minimized, no node is reconsidered. A cyclic or incomplete map panics rather than returning a number for a term that cannot be materialized. Both extraction entry points call it after the repair. ## Why two fields, not one Per #1116's nuance: the DP minimizes a **tree** cost (each child summed, sharing never priced) while the emitted kernel pays a **DAG** cost (each distinct chosen class once). One number cannot answer both questions, so the field docs name which is which: | field | meaning | who wants it | |---|---|---| | `ExtractedDAG::total_cost` | **tree** cost of the returned term — the objective the DP optimizes | anything comparing the DP against a reference that minimizes the same thing | | `ExtractedDAG::dag_cost` | **DAG** cost of the returned term — what the emitted kernel pays | *"what will this kernel cost?"* | On `shader:julia_set` those are ~1.4e7 against 716 (a 20,000x sharing ratio). **The objective is unchanged — that is #1116 and JP's call.** This PR only makes the reported number honest about which one it is. `Optimized::cost` carries the pair, so `Optimizer::run` no longer discards the total it computes. ## Reader audit — what each caller actually wants | reader | wants | verdict | |---|---|---| | `oracle_filtered_budget_curves` (regret curves) | cost of the kernel each checkpoint would emit → **DAG** | **fixed** | | `guide_headroom`'s `extracted_cost` column | cost of the extracted kernel → **DAG** | **fixed** | | `dag_to_kernel_code`'s generated bench comment | cost of the benched kernel → **DAG** | **fixed** | | compiler's `optimize_via_model` | nothing — builds `ExtractedDAG` only for let-binding placement | **fixed**: passes `Optimized::cost` instead of fabricating `0` | | `production_telemetry`'s `dp_cost` | the DP's own objective value → **tree**, deliberately | correct; now read from `Optimized::cost.tree` (post-repair) instead of re-running the DP | | `extraction_gap`'s `greedy_reported` | the DP's *reported* number, deliberately, to check it | correct; `reported_matches_returned` is now the standing regression check | | `runtime.rs` / `ir_bridge.rs` `arena_cost` | **DAG**, from the materialized arena | correct; their doc comments no longer blame `CYCLE_COST` inflation, which is gone | | `swap_search_reproduces_extract_dags_choice…` | tree | correct | ## Which published numbers were affected Plainly, by harness: - **Safe.** #1101's rule-order results and `docs/results/2026-09-02-missing-congruence.*` — both re-cost the materialized arena (`arena_cost` / `arena_static_cost`), which was the right workaround and is unaffected. - **Safe.** `docs/results/2026-09-02-extraction-gap.md` (#1115) — its greedy/tree-optimal/exact costs come from `Instance::dag_cost` and `Instance::tree_cost`, computed independently of the reported field. The headline gap numbers stand. Only its `reported_matches_returned` / `reported_delta` columns describe the defect being fixed here, and re-running should now put them at 0/302. - **Safe conclusion, stale column.** `docs/results/2026-09-01-production-saturation-telemetry.csv` — the quality metric is `cost` = `arena_cost`; the raw `dp_cost` column carried the pre-repair total and will change on a re-run. - **Safe conclusion, stale column.** `docs/results/2026-08-30-guide-headroom.{md,json}` — the headline is the load-bearing ratio, which never touched cost; the `extracted_cost` column was a tree cost and is now a DAG cost. - **Not safe.** `docs/results/2026-08-30-oracle-filtered-budget-curves.{md,csv}` — its `cost` and `regret_pct` columns *are* `ExtractedDAG::total_cost`, i.e. pre-repair tree costs used as a stand-in for emitted-kernel cost. Those curves need a re-run before the numbers are quoted again. ## Tests - `reported_cost_follows_the_choice_the_repair_rewrote` — a class holding both `Sin(x)` and a self-referential `Neg`, under a cost function that prices `Sin` above the `CYCLE_COST` sentinel. The DP's minimum is the self-reference; the repair rewrites it to `Sin`. Verified to fail against the old read (`left: 4611686018427387903` vs `right: 9223372036854775807` — `CYCLE_COST` reported for a term costing twice that). - `dag_cost_equals_the_materialized_arenas_cost` — the property every measurement in this repo has been assuming, asserted directly against `choices_to_arena`. - `tree_cost_prices_a_shared_subterm_once_per_use_and_dag_cost_once` — pins that the two fields are genuinely different quantities. - `cost_of_choices_refuses_a_cyclic_choice_map`. ## Gates `cargo build --workspace`, `cargo test --workspace` (exit 0), `cargo clippy --workspace --all-targets -D warnings` (exit 0), `cargo fmt --all --check`, `cargo check -p pixelflow-ir --no-default-features`, `cargo check -p pixelflow-search --no-default-features` — all green. Closes#1111. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
jppittman pushed a commit
that referenced
this pull request
Sep 3, 2026
… is now the cost the kernel pays `extract_dag` summed each child's `best_cost`, a TREE cost, so a subterm used ten times was charged ten times in the objective and emitted once in the kernel. #1115 measured the consequence: of the 195 pooled cost units the greedy DP lost against an exact DAG optimum, 185 (~95%) were this, and `shader:julia_set` carried a tree cost of ~1.4e7 against a DAG cost of 716. The DP now carries, alongside each class's cost, the SET of classes its chosen sub-DAG contains, and prices that set — each member once. A parent unions its children's sets, so a class two siblings both reach is paid for once and `Mul(a, a)` pays for `a` once. At every class the quantity minimized is the true DAG cost of the sub-DAG rooted there; at the root it is exactly `ExtractedDAG::dag_cost`. The objective and the price are now the same quantity. The amortized iterate-to-fixpoint alternative was built first and rejected: charging `A[c]/r[c]` per use site telescopes exactly to DAG cost at the fixpoint, but `r` comes from the previous choice, so it can only price sharing the current term already exhibits — on the case that motivates the issue it converges immediately to the tree answer. No regression is structural rather than empirical: both objectives run and the cheaper term by true `dag_cost` wins, ties to the tree arm, so the returned cost is a minimum over a set containing the old answer. Neither DP is optimal, and the sharing arm alone is dearer on 90 of 206 real kernels — min-of-two buys robustness as much as it buys cost. Measured against #1115's own exact references, unchanged: 95.9% of the gap closed (187 of 195 pooled units), exactly DAG-optimal on the solved set 90/93 -> 92/93, and on the 206 real kernels 55 improved / 151 unchanged / 0 worse for -0.23% pooled and 2,303 cost units. Extraction costs 2.23x (median), which does not clear the 2x bar this was held to — see the PR's HOLD. docs/results/2026-09-02-extraction-objective.{md,csv,json} Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jppittman pushed a commit
that referenced
this pull request
Sep 3, 2026
… is now the cost the kernel pays `extract_dag` summed each child's `best_cost`, a TREE cost, so a subterm used ten times was charged ten times in the objective and emitted once in the kernel. #1115 measured the consequence: of the 195 pooled cost units the greedy DP lost against an exact DAG optimum, 185 (~95%) were this, and `shader:julia_set` carried a tree cost of ~1.4e7 against a DAG cost of 716. The DP now carries, alongside each class's cost, the SET of classes its chosen sub-DAG contains, and prices that set — each member once. A parent unions its children's sets, so a class two siblings both reach is paid for once and `Mul(a, a)` pays for `a` once. At every class the quantity minimized is the true DAG cost of the sub-DAG rooted there; at the root it is exactly `ExtractedDAG::dag_cost`. The objective and the price are now the same quantity. The amortized iterate-to-fixpoint alternative was built first and rejected: charging `A[c]/r[c]` per use site telescopes exactly to DAG cost at the fixpoint, but `r` comes from the previous choice, so it can only price sharing the current term already exhibits — on the case that motivates the issue it converges immediately to the tree answer. No regression is structural rather than empirical: both objectives run and the cheaper term by true `dag_cost` wins, ties to the tree arm, so the returned cost is a minimum over a set containing the old answer. Neither DP is optimal, and the sharing arm alone is dearer on 90 of 206 real kernels — min-of-two buys robustness as much as it buys cost. Measured against #1115's own exact references, unchanged: 95.9% of the gap closed (187 of 195 pooled units), exactly DAG-optimal on the solved set 90/93 -> 92/93, and on the 206 real kernels 55 improved / 151 unchanged / 0 worse for -0.23% pooled and 2,303 cost units. Extraction costs 2.23x (median), which does not clear the 2x bar this was held to — see the PR's HOLD. docs/results/2026-09-02-extraction-objective.{md,csv,json} Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three independent budget-loosening changes each improved most real kernels and regressed a minority — #1101 (numeric-first rule order, 146 up / 21 down), #1109 (removing the class-cap break, 140/16), #1114 (live-counted class budget, 155/4, +2.03% for 2.00x applications). The mechanism named in #1114 and established in #1111 is that
extract_dagis a greedy DP over a static prior and is not argmin (§1.L5 ofdocs/plans/2026-09-02-optimizer-api.md), so a larger e-graph is not monotonically better even though the optimum over it can only fall.This PR answers the question that sequences those trades: how much is the greedy chooser leaving on the table? Measurement + docs only — no production behavior changes.
Method
Two references, both under
Optimizer::production()andCostModel::latency_prior()— the same cost function the DP uses — so the comparison is chooser-vs-chooser, never cost-model-vs-cost-model. 302 kernels: 206 real.arenadumps + a 96-point synthetic size ladder.extract_dagnames, so it separates "the DP fails its own objective" from "the objective is the wrong one".UNSOLVEDrather than a truncated answer; cross-checked against brute-force enumeration on every instance small enough to enumerate.Headline
Caveats that travel with the number: it is against the tree optimum; exact DAG extraction stops being computable at ~100–200 reachable e-classes while a production glyph saturates to a median 1,755, so 213/302 are UNSOLVED and excluded from the exact statistic; and all 68 kernels the tree optimum beats are real (64 glyph, 4 shader) — zero synthetic — so the two references describe different populations.
Mechanism split — which of the three to fix
CYCLE_COST, never revisited)U+004Bregressionstotal_costread atextract.rs:1636before repair at:1639The 95/5 split is a small-kernel statement — it is computed only where the branch and bound closed. Over the full corpus the picture inverts, and (i) is both the larger measurable recovery and the cheaper fix. Cycle-priced classes are not rare: 238/302 kernels have at least one (median 10, max 284), and on 203/302 the best term the search holds uses one.
(iii) is worse than "stale": of the 132/302 mismatches not one is an ordinary-magnitude discrepancy. 92 report exactly
usize::MAX(the.unwrap_orat:1636, taken when the DFS never resolves the root class) and 40 report 1x, 2x or 3xusize::MAX / 4— theDwrtsentinel fromcost.rs:292, summed into a number typed as a cost. No shipped result is corrupted by it — #1101/#1109/#1114's harnesses re-score the returned arena (arena_static_cost,runtime.rs:1278) andguide_headroom.rs:395's 800-expression corpus holds no sentinel — so it is a live trap, not a live wound.The finding that settles the sequencing
On
glyph16:U+004Bandglyph32:U+004B— two of #1114's four regressions — the roomier e-graph provably contains a term costing 1013 while greedy returns 1132 on it and 1047 on the smaller production graph. The graph got strictly better and the chooser gave up 8.1% anyway: those regressions are extraction failures, not budget failures.shader:julia_setleaves 5.6% on the table at the production budget with no budget change in play.psychedelic(766 → 816) exhibited no cheaper term inside the limit — unresolved, not exonerated.Sequencing
Fix extraction before taking #1101, #1109 or #1114. The fixpoint is free at run time, recovers more on real kernels than #1114 buys for 2x the rule applications where they overlap, and turns #1114's
U+004Bregressions into improvements — which no budget tuning can do.Cost of a fixed extractor (estimate, not benchmarked): Knuth is
O(Σ arity · log V)against the DFS'sO(Σ arity)— a small constant and a log, not an order — and it completed on all 302 kernels including the 4,847-classpsychedelic. Extraction runs once per compile; saturation runs 8,729,067 rule applications over 800 expressions in the guide-headroom corpus (median 194.5, p90 31,197, max 996,047), and #1114 doubles that for +2.03%. This is the cheap place to spend.Shape
#[cfg(test)]+#[ignore]d —runtime.rswas already 2,631 lines and flagged for size..arenaloader moved out ofruntime.rsintoarena_corpus(net −124 lines there), so the two probes that read the corpus do not carry two copies of the dumpers' inverse.Results:
docs/results/2026-09-02-extraction-gap.{md,csv,json}.Gates: build,
cargo test --workspace,clippy --workspace --all-targets -D warnings,fmt --check, and both no_std checks (pixelflow-irandpixelflow-search,--no-default-features) all green locally.Refs #1111, #1101, #1109, #1114.
🤖 Generated with Claude Code