Background
PR #116 introduced the experimental Proxy-side least_load Instance selection strategy.
PR #118 wired Proxy-maintained per-Instance inflight lifecycle counters into request execution and exposed /debug/instance_loads.
PR #121 extends least_load to use instantaneous per-Instance queue pressure from QueueManager.queue_depth_snapshot(), including:
active_prepare
active_ready
prepare_queue_depth
ready_queue_depth
This is a useful first step, but runtime testing shows that with low RPS, especially rps=1, least_load_score.total can remain 0 most of the time. This is expected for instantaneous counters because queue workers can immediately drain prepare_q / ready_q, and active_prepare / active_ready can be very short-lived.
However, QueueManager already maintains richer predicted pressure state, including:
_instance_pending_tasks
_instance_decode_tasks
_instance_slot_free_ts_s
_instance_prefill_free_ts_s
reservation_seq / prediction timeline
pred_forward_start_ts_ms
pred_first_token_ts_ms
pred_forward_end_ts_ms
pred_worker_free_ts_ms
These internal prediction structures better represent future per-Instance work even after actual Python queues have been drained.
Goal
Expose a safe, Proxy-local per-Instance predicted pressure snapshot from QueueManager, and let least_load use it as an additional scoring input.
The goal is to make least_load_score.total reflect not only instantaneous queue depth, but also predicted unfinished work already assigned to an Instance.
This should make the strategy meaningful even when:
prepare_queue_depth = 0
ready_queue_depth = 0
active_prepare = 0
active_ready = 0
but the Instance still has unfinished predicted prefill/decode work.
Branch hygiene
Follow doc/codex_workflow.md if present.
Start from latest origin/main and use a fresh branch for this issue. Do not continue from previous Codex branches or include previous unmerged PR changes.
Suggested branch name:
codex/issue-123-predicted-instance-pressure
Scope
Expected files may include:
proxy/queue/manager.py
proxy/strategy/least_load.py
proxy/resource/instance_pool.py
proxy/resource/p_control_plane.py
proxy/proxy.py
proxy/README.md
Only modify files needed to expose predicted per-Instance pressure, wire it into least_load, and document/debug it.
Required behavior
1. Add predicted pressure to QueueManager snapshots
Extend QueueManager.queue_depth_snapshot() or add a new method such as:
queue_pressure_snapshot() -> Dict[str, Any]
The snapshot should include the existing instantaneous fields:
prepare_queue_depth
ready_queue_depth
active_prepare
active_ready
and add predicted per-Instance fields derived from reservation/pending/decode state, for example:
pending_prefill_count
active_decode_count
predicted_wait_ms
predicted_prefill_backlog_ms
predicted_decode_backlog_ms
predicted_total_backlog_ms
next_slot_ready_in_ms
prefill_free_in_ms
The exact field names can differ, but they must be stable and documented.
2. Safe derivation rules
Do not expose task payloads or full request content.
Only expose aggregate numeric pressure metrics per Instance.
Suggested derivation:
pending_prefill_count = len(_instance_pending_tasks[instance_id])
active_decode_count = len(_instance_decode_tasks[instance_id])
next_slot_ready_in_ms = max(0, min(_instance_slot_free_ts_s[instance_id]) - now) * 1000
prefill_free_in_ms = max(0, _instance_prefill_free_ts_s[instance_id] - now) * 1000
predicted_total_backlog_ms = max(next_slot_ready_in_ms, prefill_free_in_ms)
If there are stronger existing predicted fields that better represent outstanding work, use them, but keep the snapshot simple and non-invasive.
3. Integrate predicted pressure into least_load scoring
Update LeastLoadStrategy.compute_score(...) to consider predicted pressure when present in the hint.
Potential additional weighted components:
pending_prefill_count
active_decode_count
predicted_total_backlog_ms
Use conservative default weights. Example:
pending_prefill_count_weight: 1.0
active_decode_count_weight: 0.5
predicted_total_backlog_ms_weight: 0.001 # 1000 ms adds 1.0 score
The exact weights can differ, but they must be explained in code comments and README.
The existing score components from PR #121 should remain:
inflight
active_prepare
active_ready
prepare_queue_depth
ready_queue_depth
qps_1m disabled or low weight by default
4. Preserve fallback semantics
If predicted pressure is unavailable, least_load must still work with the existing PR #121 queue-aware score.
If queue/prediction hints are entirely unavailable, fallback to inflight-only scoring.
If all load signals are unknown, fallback to round-robin.
Unknown metrics must not be misrepresented as measured zero.
5. Debug visibility
Update /debug/instance_loads so it exposes the predicted pressure fields and shows how they affect least_load_score.
Debug output should include enough information to explain selection decisions, for example:
{
"instance_id": "instance_127.0.0.1:9001",
"inflight": 1,
"active_prepare": 0,
"active_ready": 0,
"prepare_queue_depth": 0,
"ready_queue_depth": 0,
"pending_prefill_count": 2,
"active_decode_count": 1,
"predicted_total_backlog_ms": 1350,
"least_load_score": {
"total": 4.85,
"components": {
"inflight": 1,
"pending_prefill_count": 2,
"active_decode_count": 1,
"predicted_total_backlog_ms": 1350
},
"weighted_components": {
"inflight": 1.0,
"pending_prefill_count": 2.0,
"active_decode_count": 0.5,
"predicted_total_backlog_ms": 1.35
},
"source": {
"inflight": "proxy_lifecycle_counter",
"predicted_total_backlog_ms": "proxy_queue_reservation_timeline"
}
}
}
Exact shape can differ, but it must show:
raw predicted pressure fields
weighted score contribution
metric source
weights used
6. Selection-time hint
Ensure the same predicted pressure snapshot used by /debug/instance_loads is also available to least_load during select_instance(...).
Recommended hint shape:
hint = {
"request": req_obj,
"queue_depths": queue_mgr.queue_pressure_snapshot(),
}
or keep the existing queue_depths key but extend its content.
Do not change round_robin; it should keep ignoring hints.
7. Documentation
Update proxy/README.md to explain that least_load now considers three layers:
1. Proxy lifecycle pressure: inflight
2. Instantaneous queue pressure: active_prepare / active_ready / prepare_q / ready_q
3. Predicted reservation pressure: pending prefill/decode and predicted backlog time
Document how to inspect the debug output:
curl -sS http://127.0.0.1:8002/debug/instance_loads | python3 -m json.tool
Also document why low RPS may show zero instantaneous queue depth but nonzero predicted pressure during overlapping requests.
Non-goals
Do not implement yet:
kv_aware strategy
KVCache locality scoring
KDN-aware routing
Scheduler Proxy selection based on pool_resource
Scheduler resource-aware routing
IWS decision changes
queue scheduling/release behavior changes
request forwarding behavior changes
Resource Agent sampling changes
pool_resource schema changes
This issue is only about exposing Proxy-local predicted per-Instance pressure and using it inside the existing least_load strategy.
Validation plan
Compile check
PYTHONDONTWRITEBYTECODE=1 python3 -m py_compile \
proxy/queue/manager.py \
proxy/strategy/least_load.py \
proxy/strategy/factory.py \
proxy/proxy.py \
proxy/resource/instance_pool.py \
proxy/resource/p_control_plane.py \
proxy/queue/*.py \
test/demo_proxy.py
Unit-style smoke tests
Use fake instances and fake pressure hints to verify:
lower inflight still wins when all other pressure is equal
higher pending_prefill_count loses when inflight is equal
higher predicted_total_backlog_ms loses when instantaneous queue depth is zero
missing predicted fields fall back to PR #121 queue-aware score
missing all queue hints falls back to inflight-only score
all unknown metrics fallback to round-robin
ties are still broken by round-robin
Runtime smoke test
Start Scheduler, Proxy, and at least two Instances.
Start Proxy with:
python3 test/demo_proxy.py --strategy least_load --injection-strategy iws
Observe debug output at high frequency:
while true; do
date +"%H:%M:%S.%3N"
curl -sS http://127.0.0.1:8002/debug/instance_loads | python3 -m json.tool
sleep 0.1
done
Send overlapping requests and verify:
least_load_score.total changes with inflight and predicted backlog
predicted pressure can remain visible even when prepare_q/ready_q are zero
least_load avoids Instances with higher predicted backlog
inflight returns to 0 after requests finish
round_robin still works when selected explicitly
Acceptance criteria
- QueueManager exposes per-Instance predicted pressure without exposing task payloads.
least_load uses predicted pressure when available.
/debug/instance_loads displays predicted pressure and weighted score contributions.
- Low-RPS or quickly-drained-queue cases can still show meaningful predicted pressure during overlapping requests.
- Missing predicted pressure does not break existing
least_load behavior.
round_robin remains unchanged.
- No Scheduler routing, KDN, KVCache, IWS, queue release, request forwarding, Resource Agent, or pool_resource schema behavior is changed.
Background
PR #116 introduced the experimental Proxy-side
least_loadInstance selection strategy.PR #118 wired Proxy-maintained per-Instance
inflightlifecycle counters into request execution and exposed/debug/instance_loads.PR #121 extends
least_loadto use instantaneous per-Instance queue pressure fromQueueManager.queue_depth_snapshot(), including:This is a useful first step, but runtime testing shows that with low RPS, especially
rps=1,least_load_score.totalcan remain0most of the time. This is expected for instantaneous counters because queue workers can immediately drainprepare_q/ready_q, andactive_prepare/active_readycan be very short-lived.However, QueueManager already maintains richer predicted pressure state, including:
These internal prediction structures better represent future per-Instance work even after actual Python queues have been drained.
Goal
Expose a safe, Proxy-local per-Instance predicted pressure snapshot from
QueueManager, and letleast_loaduse it as an additional scoring input.The goal is to make
least_load_score.totalreflect not only instantaneous queue depth, but also predicted unfinished work already assigned to an Instance.This should make the strategy meaningful even when:
but the Instance still has unfinished predicted prefill/decode work.
Branch hygiene
Follow
doc/codex_workflow.mdif present.Start from latest
origin/mainand use a fresh branch for this issue. Do not continue from previous Codex branches or include previous unmerged PR changes.Suggested branch name:
Scope
Expected files may include:
Only modify files needed to expose predicted per-Instance pressure, wire it into
least_load, and document/debug it.Required behavior
1. Add predicted pressure to QueueManager snapshots
Extend
QueueManager.queue_depth_snapshot()or add a new method such as:The snapshot should include the existing instantaneous fields:
and add predicted per-Instance fields derived from reservation/pending/decode state, for example:
The exact field names can differ, but they must be stable and documented.
2. Safe derivation rules
Do not expose task payloads or full request content.
Only expose aggregate numeric pressure metrics per Instance.
Suggested derivation:
If there are stronger existing predicted fields that better represent outstanding work, use them, but keep the snapshot simple and non-invasive.
3. Integrate predicted pressure into least_load scoring
Update
LeastLoadStrategy.compute_score(...)to consider predicted pressure when present in the hint.Potential additional weighted components:
Use conservative default weights. Example:
The exact weights can differ, but they must be explained in code comments and README.
The existing score components from PR #121 should remain:
4. Preserve fallback semantics
If predicted pressure is unavailable,
least_loadmust still work with the existing PR #121 queue-aware score.If queue/prediction hints are entirely unavailable, fallback to inflight-only scoring.
If all load signals are unknown, fallback to round-robin.
Unknown metrics must not be misrepresented as measured zero.
5. Debug visibility
Update
/debug/instance_loadsso it exposes the predicted pressure fields and shows how they affectleast_load_score.Debug output should include enough information to explain selection decisions, for example:
{ "instance_id": "instance_127.0.0.1:9001", "inflight": 1, "active_prepare": 0, "active_ready": 0, "prepare_queue_depth": 0, "ready_queue_depth": 0, "pending_prefill_count": 2, "active_decode_count": 1, "predicted_total_backlog_ms": 1350, "least_load_score": { "total": 4.85, "components": { "inflight": 1, "pending_prefill_count": 2, "active_decode_count": 1, "predicted_total_backlog_ms": 1350 }, "weighted_components": { "inflight": 1.0, "pending_prefill_count": 2.0, "active_decode_count": 0.5, "predicted_total_backlog_ms": 1.35 }, "source": { "inflight": "proxy_lifecycle_counter", "predicted_total_backlog_ms": "proxy_queue_reservation_timeline" } } }Exact shape can differ, but it must show:
6. Selection-time hint
Ensure the same predicted pressure snapshot used by
/debug/instance_loadsis also available toleast_loadduringselect_instance(...).Recommended hint shape:
or keep the existing
queue_depthskey but extend its content.Do not change
round_robin; it should keep ignoring hints.7. Documentation
Update
proxy/README.mdto explain thatleast_loadnow considers three layers:Document how to inspect the debug output:
curl -sS http://127.0.0.1:8002/debug/instance_loads | python3 -m json.toolAlso document why low RPS may show zero instantaneous queue depth but nonzero predicted pressure during overlapping requests.
Non-goals
Do not implement yet:
This issue is only about exposing Proxy-local predicted per-Instance pressure and using it inside the existing
least_loadstrategy.Validation plan
Compile check
PYTHONDONTWRITEBYTECODE=1 python3 -m py_compile \ proxy/queue/manager.py \ proxy/strategy/least_load.py \ proxy/strategy/factory.py \ proxy/proxy.py \ proxy/resource/instance_pool.py \ proxy/resource/p_control_plane.py \ proxy/queue/*.py \ test/demo_proxy.pyUnit-style smoke tests
Use fake instances and fake pressure hints to verify:
Runtime smoke test
Start Scheduler, Proxy, and at least two Instances.
Start Proxy with:
Observe debug output at high frequency:
Send overlapping requests and verify:
Acceptance criteria
least_loaduses predicted pressure when available./debug/instance_loadsdisplays predicted pressure and weighted score contributions.least_loadbehavior.round_robinremains unchanged.