Repository navigation
feat(devtools): stage_pipeline writes the memo after each step, the selected choice and the enumeration - #1024
Merged
Merged
Conversation
…elected choice and the enumeration The stage-pipeline document gains an optional `memo` section: the memo the production search built (stage0, pass1, pass2, concrete_states) from the planning round whose plan was selected, the selected plan's group -> candidate choice over the last step, and each step's candidate DAG count with the first choices composed. `--no-memo`, `--max-memo-candidates`, `--memo-enumeration-limit` and `--max-enumerated` control it. The search records the steps in `Stage1ToStage3Run::memo_steps`, and derives the selected choice from the selected candidate connections through each memo candidate's root id. `enumerate` leaves out choices whose candidates read each other in a cycle, and `compose` reports such a choice instead of overflowing the stack. Closes #1003. Part of #1008. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reject a selected choice that misses a needed group or cannot name a memo candidate, prune cyclic candidates while enumerating (bounded by the limit), label aggregates by their measures, and loosen the --no-memo usage. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This was referenced Oct 28, 2026
Closed
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 free
to 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.
Closes #1003. Part of #1008.
Why
To review the planner stage by stage (#1008) we need the memo after every step, which candidate the selected plan took for each group, and the candidate DAGs the memo stands for. The
asap-stage-pipeline/v1document had none of these, so neither the DAG viewer (#1005, #1006) nor pricing every enumerated DAG (#1004) could be built.What
stage_pipelinewrites an optionalmemosection (on by default,--no-memodrops it):steps(stage0,pass1,pass2,concrete_states) with each group and its candidates,selected_choice(group → candidate index over the last step), andenumeration(per_stepcounts up to--memo-enumeration-limit, default 1000; the first--max-enumerated, default 200, choices of one step composed into DAGs).--max-memo-candidates(default 64) caps the candidates listed per group. The format is the shared contract the viewer and pricing PRs build against.route.rsrecords it in the newStage1ToStage3Run::memo_steps; devtools does not re-run any step.memo::enumeration(feat(optimizer): enumerate a memo's candidate DAGs and compose each choice #1009):enumerateleaves out choices whose candidates read each other in a cycle, andcomposereturnsComposeError::Cyclefor one. Before, a workload with a rollup between two groupings (thescans_projecting_different_labels_are_sharedworkload) overflowed the stack incompose.examples/memo-ratio-of-quantiles.json, planned frommemo-examples/promql/ratio-of-quantiles-5m.json(the issue's workload), with anexamples.jsonentry.How
search_domainsnapshots the memo after Pass 1 (with the accuracy-scoped sizes), after local window composition, and after concrete state-group expansion. The Stage 0 and Pass 1 snapshots carry across pane-width rounds with the reused local memo.candidate_connection_searchkeeps each round's snapshots under its selected plan's namespaced id, and returns the snapshots of the round whose plan won (else the last round's).build_candidate_inventory_beforenow also returns each memo candidate's root physical id (memo_candidate_roots, indexed likeCandidateMemo::candidates). Terminal-reader factoring maps these ids without removing duplicates. Each selected configured candidate maps throughsource_candidateto a root id, and from there to its memo candidate. When factoring merged several memo candidates into one root, the one that reads the selected producers' groups wins.selected_choiceis null with a reason in three cases: the plan uses two candidates of one group, no memo candidate matches a selected root's producers, or a group needed from the query roots has no selected candidate.stage_pipelineserializes the snapshots with the existingFlatDagexport. It enumerates each step withmemo::enumeration::enumerateand composes the listed choices withcompose.Key code interfaces added / modified
crates/plan-selection/src/search/pipeline.rscrates/devtools/src/planning_stages.rscrates/logical-asap-optimization/src/memo/enumeration.rscrates/plan-selection/src/search/candidates/inventory.rs,factoring.rs(crate-private)stage_pipelineflags (new):--no-memo,--max-memo-candidates <n>(64),--memo-enumeration-limit <n>(1000),--max-enumerated <n>(200). Document: the new optionalmemosection, described in the header ofstage_pipeline.rsand intools/dag-viewer/README.md.Usage
Major algorithm changes
Enumeration of cyclic choices (
memo::enumeration)quantile by (host, job) (0.5, data)+quantile by (job) (0.5, data)(+ two range quantiles), group A's candidate reads group B and one of B's candidates reads A.enumerateproduced choices taking both, andcomposerecursed until the stack overflowed.walktries every candidate of each needed group, andcomposedmemoizes a group only after it finishes. After: when choosing a candidate for a group,walkskips the candidate if one of its children is that group or already reads it through the chosen candidates (reaches). Every edge is checked as its group is chosen, so no cyclic choice is completed, and the work stays bounded bylimit.composedkeeps a set of the groups it is still composing and returnsComposeError::Cyclewhen it meets one of them again.choices_whose_candidates_read_each_other_are_left_out.limitcomplete choices. Measured: the 4-query rollup workload (scans_projecting_different_labels_are_shared's) enumerates pass1 256, pass2 784 (both complete) and concrete_states up to 1000, with the wholestage_pipelinerun taking 30 s (planning included).Selected choice derivation: new, read-only (see How). It does not change selection.
Before this PR
The document had the Stage 0 DAG, the selected plan's Stage 1/2 DAGs and the search report, but no groups, candidates, rule origins or group → candidate map.
composecould overflow the stack on memos with rollups between groupings.After this PR
Every
stage_pipelinedocument carries the memo after each step, the selected choice, and the enumeration, ready for #1004 (pricing), #1005 and #1006 (viewer). Documents withoutmemostill validate and render.Evidence
quantile_over_time(0.5, data[5m])share one group. Production builds 7 groups, 19 Pass 1 candidates, 49 Pass 1 choices. The issue's 8 / 26 / 343 were measured without series identity (theexpanded_memotest helper), so the test asserts the production numbers.concrete_stateshas 336PaneChaingroups, each with its full subtree DAG; the document is pretty-printed).Verification
cargo test --release -p asap-devtools --test stage_pipeline: 48/49 pass. The one failure,scans_projecting_different_labels_are_shared(selected cost 0.2100 > 0.1518), fails identically onorigin/main(b6d838b). It is pre-existing and unrelated.memo_section_lists_each_step_the_selected_choice_and_the_enumeration: on the issue's workload, the steps are in order, with 7 groups / 19 candidates / 49 complete Pass 1 choices. Every group, candidate and choice DAG is a valid FlatDag,group_of_nodenames child groups, everyselected_choicecandidate exists in the last step, the listed step follows the rule, and--no-memodrops the section.composed_selected_choice_is_the_selected_stage1_dag: composingselected_choicegives the selected Stage 1 DAG on the issue's workload and planner-layering Example 1. The comparison applies Stage 2'simplement_candidate_operators(exact top-k → Project/Limit/Sort) andrecompose_guaranteesfirst, as Stage 1 reconstruction does (Memo: convert memo-group candidates into the vector of all candidate DAGs #1002's acceptance 3).workload_examples_preserve_data_and_valid_materialized_views: now 10 workload examples, including the new one.cargo test --release -p asap-devtools --test viewer_contract: pass.cargo test --release -p asap-integration-tests --test memo_enumeration: 4 pass. New:choices_whose_candidates_read_each_other_are_left_out.cargo test --release -p asap-plan-selection --lib: 379 pass.node --test tools/dag-viewer/stages.test.mjs15 pass. New: the validator accepts the example with and withoutmemo.python3 -m unittest test_viewer test_serverOK.cargo fmt --all --check;cargo clippy --release -p asap-plan-selection -p asap-devtools -p asap-integration-tests -p asap-logical-asap-optimization --all-targets -- -D warnings.Review fixes (second commit): the stricter
selected_choicechecks above, cycles pruned during enumeration rather than after, aggregate labels naming their measures (Aggregate quantile(q=0.5)), and--no-memodocumented as combinable with the cap flags (which it ignores). Re-run after the fixes:memo_enumeration(4), the three memo and example tests ofstage_pipeline,asap-logical-asap-optimization --lib enumeration,asap-plan-selection --lib route(18), clippy, fmt and the node tests. Example size after regeneration: 13,818,953 bytes.Architectural decisions
MemoStepsstruct on the run), not re-run in devtools, so the document shows exactly what the search selected from. It is always recorded: the clones shareRcnodes and cost far less than the MILP.record_roundalso keeps the latest round's snapshots on the archive, so a search that ends with an error (from any early return) still reports a memo. The reviewer's suggestion to keep them only inmemo_roundswould drop the memo on those paths.pass2holds only window composition, because production runs the other Pass 2 rules inside the concrete-state expansion. Each step is the memo exactly as production has it after that call.Limitations and follow-up
pricedis null until stage_pipeline: price every enumerated candidate DAG and flag any cheaper than the selected plan #1004.Human review — do not complete with an agent
🤖 Generated with Claude Code