Skip to content

docs(rfc): queue-scoped speculation design#413

Open
behinddwalls wants to merge 1 commit into
mainfrom
speculation-rfc
Open

docs(rfc): queue-scoped speculation design#413
behinddwalls wants to merge 1 commit into
mainfrom
speculation-rfc

Conversation

@behinddwalls

@behinddwalls behinddwalls commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Why?

A merge queue that verifies one change at a time is bounded by its slowest build. Speculation builds a batch early against a guess at how its unresolved dependencies will resolve, so a matching build is ready when reality settles while merging stays strict. Getting that right on an eventually consistent, transactionless store needs an explicit design for how a path is represented, which paths build under a bounded CI budget, what is persisted, and where each decision lives.

What?

This RFC designs speculation for SubmitQueue end to end. Each queue change fires one run per queue: the speculate controller reads the current facts once, reconciles path state, refutes broken bets, finalizes verdicts, asks one pluggable extension which paths to build or preempt, and writes the result. Only chosen paths are persisted; every other possibility is recomputed each run.

The Speculator is the only controller-facing extension. It chooses which paths to build and which running ones to cancel, while verdicts and correctness cancels remain controller rules. The default Speculator internally composes a Generator, which produces a lazy best-first candidate stream, and an Allocator, which spends the CI budget and settles in-flight paths by ID; alternate Speculators need not preserve that split.

A path is self-describing: it carries an included, excluded, or dropped bet on every dependency. Its identity hashes the head plus its bets, build dispatches are replayed idempotently by path and attempt, and pending, building, and cancelling paths continue to charge the hard CI budget until terminal. The design also covers conflict relaxation and bypassing a large dependency once passed builds cover every possible resolution.

@behinddwalls
behinddwalls marked this pull request as ready for review July 21, 2026 03:28
@behinddwalls
behinddwalls requested review from a team and sbalabanov as code owners July 21, 2026 03:28
@behinddwalls
behinddwalls force-pushed the speculation-rfc branch 2 times, most recently from e0ac903 to 55dd290 Compare July 21, 2026 21:21

@sbalabanov sbalabanov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

back to you based on brainstorming session

Comment thread doc/rfc/submitqueue/speculation.md Outdated
Comment thread doc/rfc/submitqueue/speculation.md Outdated
Comment thread doc/rfc/submitqueue/speculation.md Outdated
Comment thread doc/rfc/submitqueue/speculation.md Outdated
Comment thread doc/rfc/submitqueue/speculation.md Outdated
Comment thread doc/rfc/submitqueue/speculation.md Outdated
Comment thread doc/rfc/submitqueue/speculation.md Outdated

### The default Speculator

Composed in wiring from two swappable interfaces — swap the whole Speculator for a joint optimizer or learned model, or tune behavior by replacing just the Generator or Allocator. Its entire body: open the candidate stream over the batches and their path sets, then hand the stream and the path sets to the Allocator.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

swap the whole Speculator for a joint optimizer or learned model, or tune behavior by replacing just the Generator or Allocator

I feel like this is unnecessary, the extension model implies this, which begs the question: why are generator and allocator not extensions by themselves? Don't we intend to try and swap those out?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are swappable, but intentionally remain composition points inside the default Speculator rather than controller-facing extensions. The controller depends only on the Speculator contract, allowing an alternative joint optimizer to replace it without preserving the Generator/Allocator split. I clarified this in the RFC.

Comment thread doc/rfc/submitqueue/speculation.md Outdated
Comment thread doc/rfc/submitqueue/speculation.md Outdated
Composed in wiring from two swappable interfaces — swap the whole Speculator for a joint optimizer or learned model, or tune behavior by replacing just the Generator or Allocator. Its entire body: open the candidate stream over the batches and their path sets, then hand the stream and the path sets to the Allocator.

- **Generator** — the queue-wide ordered source of candidate paths. Given the batches and their path sets, it returns a single iterator that yields coherent, depth-capped candidates best-first across every head. Scoring and depth capping are internal. It prices each candidate as the product of its dependencies' landing chances: a dependency's chance starts at its `Batch.Score` (forced to 1 or 0 once resolved). Build evidence from its path set then sharpens it: a dependency whose speculative build has passed is likelier to land. It skips candidate paths already terminal in the path sets (passed, failed, cancelled), so best-first never re-emits a dead future. It caps **depth** — the number of unresolved dependencies a head's paths range over — so one deep chain can't blow up a run. A head beyond the cap is skipped this run and enters as its dependencies resolve.
- **Allocator** — spends the build budget (the queue's cap on concurrent builds) over the single iterator. It pulls candidates in order until the budget fills. It matches each against the path sets by ID: a path already building is kept, not re-triggered. Building and cancelling paths charge the budget (a cancelling build holds CI until observed terminal); passed, failed, and cancelled paths charge none. The sticky default fills only free slots and leaves every viable in-flight build running; the preempting default cancels in-flight paths that fall below the funded set. Budget is the only rationing lever: there is no score floor, so the default spends every slot on the best remaining candidates, however long the shot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Building and cancelling paths charge the budget (a cancelling build holds CI until observed terminal)

This is interesting.. let's say I have a 15 build budget, 14 ongoing. I speculate, and note that I can cancel 10 builds. Given that there is only 1 build left in the budget, that means I can't return more than 1 build to start, even though I know the budget will be there shortly (given I'm canceling 10 of em). Seems suboptimal that we'd have to re-run the speculator to make use of that budget?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this intentionally requires another run. Cancellation is best-effort and the CI slot remains occupied until terminal; allocating against expected capacity could exceed the hard cap if cancellation is slow or loses a race. The terminal result publishes dirty, so the next run fills the released slots. I clarified this in the RFC.

Comment thread doc/rfc/submitqueue/speculation.md Outdated

### Speculator

The controller calls the one extension, `Speculator.Speculate(batches, path sets)`, which returns paths to build and in-flight paths to preempt — nothing else. The controller validates that result, dropping any refuted, incoherent, or already-failed build.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we document an rough time-deadline for the Speculate call? Our controller should implement a bailout if a speculate call is taking too long (and effectively misbehaving).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer not to enforce a Speculator-specific deadline in the controller. Like our other extension calls, the controller passes its context and the implementation is responsible for honoring cancellation and keeping the call within its operational budget. That leaves each implementation free to make the right latency/quality tradeoff instead of baking an arbitrary timeout into the contract; we can instrument duration and optimize or bound a concrete implementation if it becomes necessary.

## Summary

### Why?

A merge queue that verifies one change at a time is bounded by its slowest build. Speculation builds a batch early against a guess at how its unresolved dependencies will resolve, so a matching build is ready when reality settles while merging stays strict. Getting that right on an eventually consistent, transactionless store needs an explicit design for how a path is represented, which paths build under a bounded CI budget, what is persisted, and where each decision lives.

### What?

This RFC designs speculation for SubmitQueue end to end. Each queue change fires one run per queue: the speculate controller reads the current facts once, reconciles path state, refutes broken bets, finalizes verdicts, asks one pluggable extension which paths to build or preempt, and writes the result. Only chosen paths are persisted; every other possibility is recomputed each run.

The Speculator is the only controller-facing extension. It chooses which paths to build and which running ones to cancel, while verdicts and correctness cancels remain controller rules. The default Speculator internally composes a Generator, which produces a lazy best-first candidate stream, and an Allocator, which spends the CI budget and settles in-flight paths by ID; alternate Speculators need not preserve that split.

A path is self-describing: it carries an included, excluded, or dropped bet on every dependency. Its identity hashes the head plus its bets, build dispatches are replayed idempotently by path and attempt, and pending, building, and cancelling paths continue to charge the hard CI budget until terminal. The design also covers conflict relaxation and bypassing a large dependency once passed builds cover every possible resolution.

The RFC fills the previously empty `doc/rfc/submitqueue/speculation.md`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants