Skip to content

Commit 73addbe

Browse files
committed
feat(speculation): rank bestfirst on evidence Scorer
Compose evidence around a nested base from YAML type evidence. Drop the sibling predictor block. Overlay named factors; replace base wholesale.
1 parent dc9298f commit 73addbe

8 files changed

Lines changed: 177 additions & 225 deletions

File tree

service/submitqueue/orchestrator/server/BUILD.bazel

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,9 @@ go_library(
5252
"//submitqueue/extension/conflict/pathoverlap:go_default_library",
5353
"//submitqueue/extension/speculation/allocator/sticky:go_default_library",
5454
"//submitqueue/extension/speculation/generator/bestfirst:go_default_library",
55-
"//submitqueue/extension/speculation/predictor:go_default_library",
56-
"//submitqueue/extension/speculation/predictor/evidence:go_default_library",
5755
"//submitqueue/extension/speculation/scorer:go_default_library",
5856
"//submitqueue/extension/speculation/scorer/composite:go_default_library",
57+
"//submitqueue/extension/speculation/scorer/evidence:go_default_library",
5958
"//submitqueue/extension/speculation/scorer/fake:go_default_library",
6059
"//submitqueue/extension/speculation/scorer/heuristic:go_default_library",
6160
"//submitqueue/extension/speculation/speculator:go_default_library",
@@ -123,7 +122,6 @@ go_test(
123122
"//submitqueue/extension/buildrunner:go_default_library",
124123
"//submitqueue/extension/changeprovider:go_default_library",
125124
"//submitqueue/extension/conflict:go_default_library",
126-
"//submitqueue/extension/speculation/predictor:go_default_library",
127125
"//submitqueue/extension/speculation/scorer:go_default_library",
128126
"//submitqueue/extension/speculation/speculator:go_default_library",
129127
"//submitqueue/extension/storage:go_default_library",

service/submitqueue/orchestrator/server/config.go

Lines changed: 81 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const (
5555

5656
// Scorer types selectable from configuration.
5757
const (
58+
scorerTypeEvidence = "evidence"
5859
scorerTypeHeuristic = "heuristic"
5960
scorerTypeComposite = "composite"
6061
)
@@ -69,10 +70,7 @@ const (
6970
// Ways a composite scorer combines its components.
7071
const combineAvg = "avg"
7172

72-
// Predictor types selectable from configuration.
73-
const predictorTypeEvidence = "evidence"
74-
75-
// Evidence an evidence predictor prices, as named in configuration. The set is
73+
// Evidence an evidence scorer prices, as named in configuration. The set is
7674
// closed: a factor under any other name would be applied to nothing and never
7775
// noticed.
7876
const (
@@ -128,7 +126,6 @@ type namedQueueProfileConfig struct {
128126
Analyzer *analyzerConfig `yaml:"analyzer"`
129127
Scorer *scorerConfig `yaml:"scorer"`
130128
Speculator *speculatorConfig `yaml:"speculator"`
131-
Predictor *predictorConfig `yaml:"predictor"`
132129
}
133130

134131
// queueProfileConfig is the full set of extensions a queue resolves to.
@@ -138,7 +135,6 @@ type queueProfileConfig struct {
138135
Analyzer analyzerConfig `yaml:"analyzer"`
139136
Scorer scorerConfig `yaml:"scorer"`
140137
Speculator speculatorConfig `yaml:"speculator"`
141-
Predictor predictorConfig `yaml:"predictor"`
142138
}
143139

144140
// changeProviderConfig selects how change metadata is fetched. The github and
@@ -228,11 +224,19 @@ type analyzerConfig struct {
228224
FailAlways bool `yaml:"failAlways"`
229225
}
230226

231-
// scorerConfig selects how a queue ranks candidate speculation paths. There is
232-
// no scoring stage: the scorer feeds the queue's speculator, which is composed
233-
// from it rather than configured separately.
227+
// scorerConfig selects how a queue ranks candidate speculation paths. The
228+
// ranking scorer is evidence wrapping a nested content base. Heuristic and
229+
// composite belong on base (and on composite components), not at the top level.
234230
type scorerConfig struct {
235231
Type string `yaml:"type"`
232+
// Factors revise the base price, one per piece of evidence (evidence only).
233+
// An omitted key keeps the inherited value, or 1 if neither defaults nor
234+
// the queue named it.
235+
Factors map[string]float64 `yaml:"factors"`
236+
// Base is the content scorer evidence revises (evidence only). An omitted
237+
// base on defaults is the default heuristic; a present base on a queue
238+
// replaces the default base wholesale.
239+
Base *scorerConfig `yaml:"base"`
236240
// Buckets map a batch's total lines changed onto a score (heuristic only).
237241
Buckets []bucketConfig `yaml:"buckets"`
238242
// Components are the scorers a composite combines, keyed by name.
@@ -250,7 +254,7 @@ type bucketConfig struct {
250254
}
251255

252256
// speculatorConfig tunes how much CI a queue's speculation may occupy. It has no
253-
// `type`: there is one speculator, composed from the queue's predictor, and what
257+
// `type`: there is one speculator, composed from the queue's scorer, and what
254258
// varies between queues is what it is allowed to spend.
255259
type speculatorConfig struct {
256260
// BuildBudget caps how many builds this queue may have occupying CI at once,
@@ -259,18 +263,6 @@ type speculatorConfig struct {
259263
BuildBudget int `yaml:"buildBudget"`
260264
}
261265

262-
// predictorConfig tunes how a queue turns its scorer's price into the
263-
// probability the generator ranks on. The scorer being revised is the queue's
264-
// own, so it is not named again here.
265-
type predictorConfig struct {
266-
Type string `yaml:"type"`
267-
// Factors revise the scorer's price, one per piece of evidence and keyed by
268-
// evidence name. An omitted key keeps the inherited value, or 1 if neither
269-
// defaults nor the queue named it. An omitted predictor block inherits the
270-
// whole default, so every factor stays 1 until someone sets one.
271-
Factors map[string]float64 `yaml:"factors"`
272-
}
273-
274266
// loadProfilesConfig reads and validates the profiles configuration at path.
275267
func loadProfilesConfig(path string) (profilesConfig, error) {
276268
data, err := os.ReadFile(path)
@@ -323,7 +315,7 @@ func (c *profilesConfig) normalizeAndValidate() error {
323315
}
324316
}
325317
if q.Scorer != nil {
326-
if err := q.Scorer.normalizeAndValidate(where); err != nil {
318+
if err := q.Scorer.normalizeOverlay(where); err != nil {
327319
return err
328320
}
329321
}
@@ -332,11 +324,6 @@ func (c *profilesConfig) normalizeAndValidate() error {
332324
return err
333325
}
334326
}
335-
if q.Predictor != nil {
336-
if err := q.Predictor.normalizeAndValidate(where); err != nil {
337-
return err
338-
}
339-
}
340327
}
341328
return c.validateGitRepoPaths()
342329
}
@@ -390,33 +377,33 @@ func (c profilesConfig) resolve(q namedQueueProfileConfig) queueProfileConfig {
390377
profile.Analyzer = *q.Analyzer
391378
}
392379
if q.Scorer != nil {
393-
profile.Scorer = *q.Scorer
380+
profile.Scorer = overlayScorer(profile.Scorer, *q.Scorer)
394381
}
395382
if q.Speculator != nil {
396383
profile.Speculator = *q.Speculator
397384
}
398-
if q.Predictor != nil {
399-
profile.Predictor = overlayPredictor(profile.Predictor, *q.Predictor)
400-
}
401385
return profile
402386
}
403387

404-
// overlayPredictor keeps default factors the queue did not name. A present
405-
// predictor block is otherwise a normal extension override: type replaces when
406-
// set, and named factor keys win.
407-
func overlayPredictor(base, override predictorConfig) predictorConfig {
388+
// overlayScorer keeps default factors the queue did not name. A present base
389+
// replaces the default base wholesale. Type stays evidence unless the override
390+
// names one, which must still be evidence.
391+
func overlayScorer(base, override scorerConfig) scorerConfig {
408392
if override.Type != "" {
409393
base.Type = override.Type
410394
}
411-
if len(override.Factors) == 0 {
412-
return base
395+
if len(override.Factors) > 0 {
396+
merged := maps.Clone(base.Factors)
397+
if merged == nil {
398+
merged = make(map[string]float64, len(override.Factors))
399+
}
400+
maps.Copy(merged, override.Factors)
401+
base.Factors = merged
413402
}
414-
merged := maps.Clone(base.Factors)
415-
if merged == nil {
416-
merged = make(map[string]float64, len(override.Factors))
403+
if override.Base != nil {
404+
copied := *override.Base
405+
base.Base = &copied
417406
}
418-
maps.Copy(merged, override.Factors)
419-
base.Factors = merged
420407
return base
421408
}
422409

@@ -436,7 +423,7 @@ func (p *queueProfileConfig) normalizeAndValidate(where string) error {
436423
if err := p.Speculator.normalizeAndValidate(where); err != nil {
437424
return err
438425
}
439-
return p.Predictor.normalizeAndValidate(where)
426+
return nil
440427
}
441428

442429
func (c *changeProviderConfig) normalizeAndValidate(where string) error {
@@ -576,10 +563,56 @@ func (a *analyzerConfig) normalizeAndValidate(where string) error {
576563
}
577564
}
578565

579-
// normalizeAndValidate applies defaults and rejects a scorer that could not be
580-
// built. An empty block is a flat heuristic: every batch scores the same, which
581-
// is the neutral choice for a queue with no opinion about ordering.
566+
// normalizeAndValidate applies defaults and rejects a ranking scorer that
567+
// could not be built. An empty block is evidence wrapping the default
568+
// heuristic, with every factor neutral.
582569
func (s *scorerConfig) normalizeAndValidate(where string) error {
570+
return s.normalizeRanking(where, true)
571+
}
572+
573+
// normalizeOverlay validates a queue's scorer override without inventing a
574+
// base: omitted base means inherit the default base.
575+
func (s *scorerConfig) normalizeOverlay(where string) error {
576+
return s.normalizeRanking(where, false)
577+
}
578+
579+
func (s *scorerConfig) normalizeRanking(where string, fillBase bool) error {
580+
if s.Type == "" {
581+
s.Type = scorerTypeEvidence
582+
}
583+
if s.Type != scorerTypeEvidence {
584+
return fmt.Errorf("%s: scorer type %q belongs under base, not at the ranking layer", where, s.Type)
585+
}
586+
if err := validateFactors(where, s.Factors); err != nil {
587+
return err
588+
}
589+
if s.Base != nil {
590+
return s.Base.normalizeContent(where + " base")
591+
}
592+
if fillBase {
593+
s.Base = &scorerConfig{}
594+
return s.Base.normalizeContent(where + " base")
595+
}
596+
return nil
597+
}
598+
599+
func validateFactors(where string, factors map[string]float64) error {
600+
for name, factor := range factors {
601+
switch name {
602+
case factorPathPassed, factorPathFailed, factorMerging, factorCancelling:
603+
default:
604+
return fmt.Errorf("%s: unknown scorer factor %q", where, name)
605+
}
606+
// Zero would permanently pin matching batches to 0; negatives cannot
607+
// represent either direction in the factor contract.
608+
if !(factor > 0) || math.IsInf(factor, 0) {
609+
return fmt.Errorf("%s: scorer factor %q is %v, must be finite and positive", where, name, factor)
610+
}
611+
}
612+
return nil
613+
}
614+
615+
func (s *scorerConfig) normalizeContent(where string) error {
583616
if s.Type == "" {
584617
s.Type = scorerTypeHeuristic
585618
}
@@ -601,7 +634,7 @@ func (s *scorerConfig) normalizeAndValidate(where string) error {
601634
return fmt.Errorf("%s: composite scorer needs at least one component", where)
602635
}
603636
for name, component := range s.Components {
604-
if err := component.normalizeAndValidate(fmt.Sprintf("%s component %q", where, name)); err != nil {
637+
if err := component.normalizeContent(fmt.Sprintf("%s component %q", where, name)); err != nil {
605638
return err
606639
}
607640
s.Components[name] = component
@@ -618,31 +651,6 @@ func (s *scorerConfig) normalizeAndValidate(where string) error {
618651
return nil
619652
}
620653

621-
// normalizeAndValidate applies defaults and rejects a predictor that could not
622-
// be built. An empty block is an evidence predictor with every factor neutral,
623-
// which prices a batch at exactly its scorer's price.
624-
func (p *predictorConfig) normalizeAndValidate(where string) error {
625-
if p.Type == "" {
626-
p.Type = predictorTypeEvidence
627-
}
628-
if p.Type != predictorTypeEvidence {
629-
return fmt.Errorf("%s: unknown predictor type %q", where, p.Type)
630-
}
631-
for name, factor := range p.Factors {
632-
switch name {
633-
case factorPathPassed, factorPathFailed, factorMerging, factorCancelling:
634-
default:
635-
return fmt.Errorf("%s: unknown predictor factor %q", where, name)
636-
}
637-
// Zero would permanently pin matching batches to 0; negatives cannot
638-
// represent either direction in the factor contract.
639-
if !(factor > 0) || math.IsInf(factor, 0) {
640-
return fmt.Errorf("%s: predictor factor %q is %v, must be finite and positive", where, name, factor)
641-
}
642-
}
643-
return nil
644-
}
645-
646654
func (s *speculatorConfig) normalizeAndValidate(where string) error {
647655
// A negative budget is rejected rather than clamped: sticky would compute no
648656
// free slots from it, so the queue would batch and then never build anything,

0 commit comments

Comments
 (0)