internal/router: priority queues so batch jobs yield to interactive requests
The GPU is a size-1 resource, so a single long job monopolises the box for its
whole duration and every interactive request queues behind it. Callers can now
declare intent with an X-LlamaSwap-Priority header and the serial scheduler
dispatches by score instead of by arrival.
- X-LlamaSwap-Priority: signed integer, 0 default, absent/unparseable means 0.
interactive/normal/batch aliases resolve to +100/0/-100. Values are not
clamped: the caller composes band and any per-user offset itself.
- serial dispatch score = priority + swap affinity + aging. Bands sit 100 apart
so a small caller offset orders work inside a band without crossing one;
aging is unbounded so low-priority work cannot starve.
- routing.scheduler.settings.serial.{agingDivisor,swapAffinityBonus}, defaulting
to 60s/point and +10. swapAffinityBonus is capped at 99 so it can never
promote a request into the next band.
- fifo adds the header to its per-model priority, so the header is not silently
ignored under that scheduler.
- /metrics exports per-band queue depth, oldest wait and dispatch counts, plus
counters for how often aging or swap affinity changed the pick. Each request
records its priority, band, score and queue wait in the activity log.
Note swapAffinityBonus defaults to 10, so equal-priority requests for the
already-loaded model now run before older requests that need a swap. Set it to
0 for the previous strict arrival order.
fixes #9
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01WUyhZBgv8BBCC5MduX88gE
This commit is contained in:
@@ -1720,3 +1720,77 @@ routing:
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 5, cfg.Routing.Scheduler.Settings.Fifo.Priority["gemma"])
|
||||
}
|
||||
|
||||
func TestConfig_Routing_SerialDefaults(t *testing.T) {
|
||||
cfg, err := LoadConfigFromReader(strings.NewReader(twoModels))
|
||||
require.NoError(t, err)
|
||||
|
||||
serial := cfg.Routing.Scheduler.Settings.Serial
|
||||
assert.Nil(t, serial.AgingDivisor, "unset should stay nil so the default applies")
|
||||
assert.Equal(t, DefaultAgingDivisor, serial.GetAgingDivisor())
|
||||
assert.Equal(t, DefaultSwapAffinityBonus, serial.GetSwapAffinityBonus())
|
||||
}
|
||||
|
||||
// TestConfig_Routing_SerialExplicitZero verifies an explicit 0 disables a term
|
||||
// rather than falling back to the default — the reason both fields are pointers.
|
||||
func TestConfig_Routing_SerialExplicitZero(t *testing.T) {
|
||||
yaml := twoModels + `
|
||||
routing:
|
||||
scheduler:
|
||||
use: serial
|
||||
settings:
|
||||
serial:
|
||||
agingDivisor: 0
|
||||
swapAffinityBonus: 0
|
||||
`
|
||||
cfg, err := LoadConfigFromReader(strings.NewReader(yaml))
|
||||
require.NoError(t, err)
|
||||
|
||||
serial := cfg.Routing.Scheduler.Settings.Serial
|
||||
assert.Equal(t, 0, serial.GetAgingDivisor())
|
||||
assert.Equal(t, 0, serial.GetSwapAffinityBonus())
|
||||
}
|
||||
|
||||
func TestConfig_Routing_SerialSettings(t *testing.T) {
|
||||
yaml := twoModels + `
|
||||
routing:
|
||||
scheduler:
|
||||
use: serial
|
||||
settings:
|
||||
serial:
|
||||
agingDivisor: 30
|
||||
swapAffinityBonus: 5
|
||||
`
|
||||
cfg, err := LoadConfigFromReader(strings.NewReader(yaml))
|
||||
require.NoError(t, err)
|
||||
|
||||
serial := cfg.Routing.Scheduler.Settings.Serial
|
||||
assert.Equal(t, 30, serial.GetAgingDivisor())
|
||||
assert.Equal(t, 5, serial.GetSwapAffinityBonus())
|
||||
}
|
||||
|
||||
func TestConfig_Routing_SerialInvalidSettings(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
settings string
|
||||
wantErr string
|
||||
}{
|
||||
{"negative aging divisor", "agingDivisor: -1", "agingDivisor"},
|
||||
{"negative affinity bonus", "swapAffinityBonus: -1", "swapAffinityBonus"},
|
||||
// 100 would let the bonus promote a request into the next band.
|
||||
{"affinity bonus reaches a band", "swapAffinityBonus: 100", "swapAffinityBonus"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
yaml := twoModels + `
|
||||
routing:
|
||||
scheduler:
|
||||
settings:
|
||||
serial:
|
||||
` + c.settings + "\n"
|
||||
_, err := LoadConfigFromReader(strings.NewReader(yaml))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), c.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user