-
Notifications
You must be signed in to change notification settings - Fork 3
feat(speculation): add probability path scorer #333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
behinddwalls
merged 1 commit into
main
from
preetam/int/speculation-pathscorer-probability
Jul 14, 2026
+438
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
26 changes: 26 additions & 0 deletions
26
submitqueue/extension/speculation/pathscorer/probability/BUILD.bazel
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| load("@rules_go//go:def.bzl", "go_library", "go_test") | ||
|
|
||
| go_library( | ||
| name = "go_default_library", | ||
| srcs = ["probability.go"], | ||
| importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/pathscorer/probability", | ||
| visibility = ["//visibility:public"], | ||
| deps = [ | ||
| "//submitqueue/entity:go_default_library", | ||
| "//submitqueue/extension/speculation/pathscorer:go_default_library", | ||
| "//submitqueue/extension/storage:go_default_library", | ||
| ], | ||
| ) | ||
|
|
||
| go_test( | ||
| name = "go_default_test", | ||
| srcs = ["probability_test.go"], | ||
| embed = [":go_default_library"], | ||
| deps = [ | ||
| "//submitqueue/entity:go_default_library", | ||
| "//submitqueue/extension/storage/mock:go_default_library", | ||
| "@com_github_stretchr_testify//assert:go_default_library", | ||
| "@com_github_stretchr_testify//require:go_default_library", | ||
| "@org_uber_go_mock//gomock:go_default_library", | ||
| ], | ||
| ) |
11 changes: 11 additions & 0 deletions
11
submitqueue/extension/speculation/pathscorer/probability/README.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # Probability Path Scorer | ||
|
|
||
| The probability `pathscorer.Scorer` scores every path in a batch's speculation tree as the probability that exactly that path's assumption holds: its base dependencies all land, and every other active dependency of the head fails to land. Concretely, a path's score is the head batch's probability, multiplied by the probability of each base dependency, multiplied by one minus the probability of each of the head's dependencies not in the base. | ||
|
|
||
| Each batch's probability is resolved from its state, so a resolved outcome overrides the prediction. A batch that has succeeded contributes certainty 1; a batch that has failed or been cancelled contributes 0; a batch still in flight contributes `entity.Batch.Score`, the per-batch success probability the score stage sets before a batch ever reaches speculation. A batch in the best-effort `cancelling` state keeps its prediction — it may still succeed. This state resolution is what redistributes score when the world changes: a dead dependency zeroes every path that built on it and boosts every path that excluded it by the full `(1 − p) = 1` factor, with no cross-path coupling needed. A worked example of this redistribution lives in the package documentation. | ||
|
|
||
| ## Scope | ||
|
|
||
| The model is deliberately simple: it treats dependency outcomes as independent, and does not weigh how long a batch has waited, its historical pass rate, or any correlation between siblings. It scores every path in the tree regardless of status, returning one path-ID-keyed score per path — the controller merges them into the tree and overwrites `Score` wholesale on every respeculate; nothing else about a path passes through the scorer. Folding resolved outcomes into path *status* (dead-pathing, cancellation) is the controller's reconcile job, not the scorer's; the scorer only makes the scores reflect them. | ||
|
|
||
| Batches are read one at a time through the injected `storage.BatchStore`, matching the store's key/value contract, and each batch referenced by the tree is loaded at most once per `Score` call. A store error, including a missing batch, is returned wrapped and unclassified — scorer implementations, like all extensions, leave user/infra classification to the controller's error classifier. | ||
158 changes: 158 additions & 0 deletions
158
submitqueue/extension/speculation/pathscorer/probability/probability.go
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| // Copyright (c) 2025 Uber Technologies, Inc. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| // Package probability provides a pathscorer.Scorer that scores each path as | ||
| // the probability that exactly that path's assumption holds: every base | ||
| // dependency lands and every non-base dependency of the head does not. Each | ||
| // dependency's probability is resolved from its batch's state — certainty | ||
| // (1 or 0) once the batch is terminal, its predicted build-success score | ||
| // while it is in flight — so a resolved dependency automatically kills the | ||
| // paths that bet against the outcome and boosts the paths consistent with | ||
| // it. Dependency outcomes are treated as independent; there is no adjustment | ||
| // for wait time, historical pass rate, or correlation between outcomes. | ||
| // | ||
| // # Worked example | ||
| // | ||
| // Batch C has active dependencies A and B, with predicted success | ||
| // probabilities p(C) = 0.8, p(A) = 0.9, p(B) = 0.6, and a three-path tree: | ||
| // | ||
| // path base formula score | ||
| // chain [A B] 0.8 * 0.9 * 0.6 0.432 | ||
| // drop-B [A] 0.8 * 0.9 * (1 - 0.6) 0.288 | ||
| // alone [] 0.8 * (1 - 0.9) * (1 - 0.6) 0.032 | ||
| // | ||
| // The chain path leads while everything looks healthy. When B's build fails, | ||
| // p(B) resolves to 0: | ||
| // | ||
| // path base formula score | ||
| // chain [A B] 0.8 * 0.9 * 0 0 | ||
| // drop-B [A] 0.8 * 0.9 * 1 0.72 | ||
| // alone [] 0.8 * 0.1 * 1 0.08 | ||
| // | ||
| // The chain path — a bet on B landing — dies, and the drop-B path inherits | ||
| // its score mass. If A then lands, p(A) resolves to 1: drop-B rises to 0.8 | ||
| // and alone falls to 0. The selector and prioritizer rank on these scores, | ||
| // so builds follow the paths still consistent with reality. | ||
| package probability | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/uber/submitqueue/submitqueue/entity" | ||
| "github.com/uber/submitqueue/submitqueue/extension/speculation/pathscorer" | ||
| "github.com/uber/submitqueue/submitqueue/extension/storage" | ||
| ) | ||
|
|
||
| // probabilityScorer computes each path's Score as the probability that | ||
| // exactly its assumption holds: every base dependency lands and every other | ||
| // active dependency of the head fails to land — so this exact path, and no | ||
| // sibling, is the one that materializes. Batch probabilities come from | ||
| // batchProbability, which prefers a terminal outcome over the prediction. | ||
| // Folding resolved outcomes into path Status (dead-pathing, cancellation) | ||
| // remains the controller's reconcile concern; this scorer only recomputes | ||
| // Score. | ||
| type probabilityScorer struct { | ||
| // batches resolves a batch ID to its current state and predicted-success | ||
| // probability (entity.Batch.State / entity.Batch.Score) and, for the | ||
| // tree's head batch, its full active-dependency set | ||
| // (entity.Batch.Dependencies). | ||
| batches storage.BatchStore | ||
| } | ||
|
|
||
| // New returns a pathscorer.Scorer implementing the path-probability | ||
| // heuristic, reading batch states and success probabilities from batches. | ||
| func New(batches storage.BatchStore) pathscorer.Scorer { | ||
| return &probabilityScorer{batches: batches} | ||
| } | ||
|
|
||
| // batchProbability resolves the probability that b lands. A terminal state | ||
| // is certainty — 1 for succeeded, 0 for failed or cancelled — and overrides | ||
| // any prediction. Otherwise it is b.Score, the predicted build-success | ||
| // probability: the score stage sets it before a batch ever reaches | ||
| // speculation, so every batch the scorer sees carries a real prediction. | ||
| // Cancelling is deliberately not treated as terminal: cancellation is | ||
| // best-effort and the batch may still succeed, so the prediction stands | ||
| // until the state settles. | ||
| func batchProbability(b entity.Batch) float64 { | ||
| switch b.State { | ||
| case entity.BatchStateSucceeded: | ||
| return 1 | ||
| case entity.BatchStateFailed, entity.BatchStateCancelled: | ||
| return 0 | ||
| } | ||
| return b.Score | ||
| } | ||
|
|
||
| // Score returns one PathScore per path in tree, regardless of Status, each | ||
| // naming its path by ID. A path's score is: | ||
| // | ||
| // p(head) * Π p(d) for d in path.Base * Π (1 - p(d)) for d in head.Dependencies but not in path.Base | ||
| // | ||
| // where p(b) is 1 when b has succeeded, 0 when b has failed or been | ||
| // cancelled, and otherwise b.Score — see batchProbability. Each batch | ||
| // referenced by the tree is loaded from the store at most once. Any store | ||
| // error (including not-found) is returned wrapped, unclassified — extensions | ||
| // never classify errors as user or infra. | ||
| func (s *probabilityScorer) Score(ctx context.Context, tree entity.SpeculationTree) ([]entity.PathScore, error) { | ||
| if len(tree.Paths) == 0 { | ||
| return nil, nil | ||
| } | ||
|
|
||
| head, err := s.batches.Get(ctx, tree.BatchID) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("probability: get head batch %q: %w", tree.BatchID, err) | ||
| } | ||
|
|
||
| probabilities := map[string]float64{} | ||
| probabilityOf := func(batchID string) (float64, error) { | ||
| if p, ok := probabilities[batchID]; ok { | ||
| return p, nil | ||
| } | ||
| b, err := s.batches.Get(ctx, batchID) | ||
| if err != nil { | ||
| return 0, fmt.Errorf("probability: get dependency batch %q: %w", batchID, err) | ||
| } | ||
| p := batchProbability(b) | ||
| probabilities[batchID] = p | ||
| return p, nil | ||
| } | ||
|
|
||
| scores := make([]entity.PathScore, len(tree.Paths)) | ||
| for i, path := range tree.Paths { | ||
| inBase := make(map[string]bool, len(path.Path.Base)) | ||
| score := batchProbability(head) | ||
| for _, dep := range path.Path.Base { | ||
| inBase[dep] = true | ||
| p, err := probabilityOf(dep) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| score *= p | ||
| } | ||
| for _, dep := range head.Dependencies { | ||
| if inBase[dep] { | ||
| continue | ||
| } | ||
| p, err := probabilityOf(dep) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| score *= 1 - p | ||
| } | ||
| scores[i] = entity.PathScore{PathID: path.ID, Score: float32(score)} | ||
| } | ||
|
|
||
| return scores, nil | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.