mirror of
https://github.com/kvcache-ai/AgentENV.git
synced 2026-11-05 01:17:36 +00:00
[PR #27] [CLOSED] feat(scheduler): add projected least-loaded placement strategy #126
Labels
No labels
bug
documentation
enhancement
enhancement
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/AgentENV#126
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/kvcache-ai/AgentENV/pull/27
Author: @catyans
Created: 7/28/2026
Status: ❌ Closed
Base:
main← Head:feat/least-loaded-scheduler📝 Commits (10+)
46403a4feat(scheduler): add projected least-loaded strategy13a53a0fix(scheduler): require capacity for requested resourcesf288cd4fix(scheduler): reserve projected placements0679b78docs(scheduler): explain placement reservations148a982fix(scheduler): reconcile reserved resources from allocation5d0addfrefactor(scheduler): scope least-loaded reservationsa493491fix(scheduler): reconcile on heartbeat generations121d728fix(scheduler): bound conservative reservation cleanup85ad4ebfix(scheduler): reconcile failed placements82b9a3cfix(scheduler): reconcile resource dimensions independently📊 Changes
14 files changed (+1354 additions, -11 deletions)
View changed files
📝
deploy/docker/config/default.json(+1 -0)📝
services/README.md(+34 -2)📝
services/config/local.json(+1 -0)📝
services/scheduler/cmd/main.go(+4 -1)➕
services/scheduler/internal/least_loaded.go(+433 -0)➕
services/scheduler/internal/least_loaded_test.go(+713 -0)📝
services/scheduler/internal/metrics.go(+2 -0)📝
services/scheduler/internal/node_registry.go(+16 -3)📝
services/scheduler/internal/node_registry_test.go(+32 -0)📝
services/scheduler/internal/service.go(+4 -2)📝
services/scheduler/internal/strategy.go(+24 -2)📝
services/scheduler/internal/types.go(+2 -1)📝
services/shared/config/config.go(+23 -0)📝
services/shared/config/config_test.go(+65 -0)📄 Description
Summary
least_loadedscheduler strategy;Motivation
The scheduler already passes
RichNodeheartbeat snapshots and structuredScheduleRequestHintvalues to every strategy, but the current built-instrategies (
round_robinandrandom) do not use either signal. Onheterogeneous clusters, round-robin assigns the same number of sandboxes to
small and large nodes and can create severe normalized load skew.
This PR provides a general load-aware fallback that can also be reused by the
cache-locality work proposed in #15.
Selection model
For each eligible node:
The request projection is available for
/sandboxes-cold; other request typesuse the current allocation snapshot.
Candidates are ordered by:
Equal candidates are selected round-robin. For requests carrying a
sandbox-creation hint, selection and reservation happen under one strategy
mutex. Subsequent requests include pending CPU, memory, and sandbox-count
reservations in their projected load. Ordinary read/list routing does not
reserve capacity. Creation candidates whose active+paused allocation plus the
request and pending reservations exceeds advertised capacity are excluded; if
all candidates are full, scheduling returns no nodes. Partial capacity
snapshots retain sandbox counts for fallback ranking. Newer heartbeats are
detected with a scheduler-local receipt
generation rather than the node wall clock. The monotonic
create_successescounter reconciles projected sandbox count independently from compatible
running CPU/memory allocation deltas, so a sandbox entering
startingdoes notrelease its reservation early. Resource deltas apply only to reservations that
predate that heartbeat and already own a create-success acknowledgement;
acknowledgements are never transferred between differently sized reservations.
CPU and memory acknowledgement are tracked independently, so eventually
consistent fields can reconcile across heartbeats without carrying generic
credit into later reservations. Unmatched delta is discarded instead of
leaking into later selections. Aggregate
create_failschanges do not releasereservations because they cannot identify which request failed; without a
correlated outcome, reservations remain conservative until a compatible
allocation update or TTL. Reconciliation is linear in pending reservations.
Requests with no observed outcome use the configurable
scheduler.placement_reservation_ttl, which defaults to 10 minutes sooperators can cover their maximum queueing, creation, and heartbeat
acknowledgement delay.
When a cold-sandbox request omits CPU or memory, the node applies its locally
configured default. That default is not currently reported in
NodeSnapshot,so the scheduler reserves the sandbox count but cannot project the omitted
resource dimension. Template-based
NewSandboxrequests likewise lack resolvedCPU/memory hints; they are conservatively rejected when either known node
dimension is already full.
An explicit request-level reservation lifecycle, including confirm/cancel,
shared replica ownership, and template-resource resolution, is proposed in
#90.
Cluster validation
Validated on three 192-logical-CPU Intel Xeon Platinum 8575C nodes:
On lingjun-099, the final reservation-aware implementation selects among 1000
candidates in approximately
54.3 µs/opwith145–146 B/op, 0 allocs/op. The originalround-robin baseline is
7.926 ns/op.In a replay with 16/64/192-CPU nodes and 300 requests of 2 CPU / 4 GiB:
The maximum normalized pressure decreased by 82.25%.
Verification
Scope
This PR intentionally does not:
Those can be discussed separately under #15 and #90.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.