Background
PR #116 added the first Proxy-side resource-aware Instance selection strategy: least_load.
At the moment, least_load reads InstanceInfo.load.inflight and InstanceInfo.load.qps_1m if they are available, and falls back to round-robin when all load metrics are unknown. This is correct as a strategy entry point, but the next step is to make those load fields reflect real Proxy-side runtime state.
Resource data is currently mainly used for observability. This issue should wire real Proxy-maintained per-Instance load counters into the data plane so least_load can make meaningful decisions.
The current route flow is roughly:
Proxy request handler
-> recover SchedulerRequest
-> select_instance(...)
-> create ProxyTask
-> queue_mgr.enqueue_prepare(task)
-> queue_mgr.iter_response(task)
select_instance(...) currently selects from InstancePool.list(include_dead=False) and calls the configured strategy. This issue should keep that strategy interface intact while ensuring the selected Instance has runtime load counters updated around request execution.
Goal
Add Proxy-maintained per-Instance load counters and expose them through InstanceInfo.load so least_load can select the least loaded live Instance using real local runtime state.
The first implementation should focus on:
per-instance inflight
optional per-instance queue depth / active queue counters if safely available
simple debug visibility for strategy inputs and scores
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-117-proxy-instance-load-counters
Scope
Expected files may include:
proxy/proxy.py
proxy/resource/instance_pool.py
proxy/resource/p_control_plane.py
proxy/queue/manager.py
proxy/queue/instance_queues.py
proxy/strategy/least_load.py
proxy/README.md
Only modify files that are necessary for Proxy-local load counters, least_load integration, and debug documentation.
Required behavior
1. Proxy-maintained per-Instance inflight
When Proxy has selected an Instance and the request is accepted into the Proxy data-plane path, increment that Instance's runtime inflight counter:
inflight[instance_id] += 1
When the request finishes, fails, raises, or the client disconnects, decrement it:
inflight[instance_id] -= 1
Important requirements:
- Use
try/finally or an equivalent lifecycle guard so inflight is decremented on success, error, and cancellation.
- Never allow the counter to go below 0.
- The counter must be thread-safe / async-safe enough for concurrent requests.
- The counter should update the same load view that
least_load reads, preferably InstanceInfo.load.inflight.
- Do not rely on Instance heartbeat to supply inflight for this first implementation; Proxy should maintain its own local counter.
2. Counter ownership and data model
Prefer adding small methods to InstancePool, for example:
begin_request(instance_id: str) -> bool
end_request(instance_id: str) -> bool
snapshot_instance_loads(...) -> Dict[str, Any]
or equivalent names.
The implementation should avoid direct mutation of InstanceInfo.load from many unrelated call sites. Keep the counter logic centralized.
begin_request(...) should:
- find the Instance by id
- increment
load.inflight
- optionally refresh or preserve liveness timestamp based on current design
- return
False if the Instance disappeared
end_request(...) should:
- find the Instance by id
- decrement
load.inflight with floor 0
- return
False if the Instance disappeared
3. Request lifecycle integration
Integrate the counters around both Proxy request paths:
POST /v1/chat/completions
POST /v1/completions
For chat streaming, the selected Instance remains active until the streaming generator is exhausted or cancelled. The decrement must happen when streaming finishes, not immediately after returning StreamingResponse.
For non-streaming completions, decrement after queue_mgr.iter_response(task) completes or errors.
Suggested approach:
- For non-streaming path, wrap the
enqueue_prepare + response collection block in try/finally.
- For streaming path, wrap the async generator that drains
queue_mgr.iter_response(task) so end_request(instance_id) is called from the generator's finally block.
- If increment succeeds but
enqueue_prepare fails, still decrement.
- If increment fails because the selected Instance disappeared, return a safe 503 or reselect only if the existing routing flow already supports that cleanly.
4. Queue depth integration
least_load already works with InstanceInfo.load.inflight / qps_1m.
For this issue, queue depth integration is optional but preferred if it can be done without invasive request-flow changes.
Possible minimal path:
- Keep
least_load primarily based on Proxy-maintained load.inflight.
- Add or expose a Proxy-local per-instance load debug endpoint that includes queue depth from
QueueManager.queue_depth_snapshot() / PerInstanceQueueMap.snapshot_depths() if available.
- Leave a TODO for using queue depth inside the strategy score if the strategy call site does not naturally receive per-instance queue hints yet.
If queue hints are added to least_load, they must remain optional and must not break the existing strategy interface.
5. Debug visibility
Add lightweight debug visibility so runtime tests can verify the counters and future strategy decisions.
Suggested endpoint:
GET /debug/instance_loads
Possible response:
{
"ok": true,
"strategy": "least_load",
"instances": [
{
"instance_id": "instance_127.0.0.1:9001",
"is_alive": true,
"inflight": 2,
"qps_1m": null,
"prepare_queue_depth": 1,
"ready_queue_depth": 0,
"active_prepare": 1,
"active_ready": 0,
"score": 2,
"score_source": "proxy_runtime_inflight"
}
]
}
The exact shape can differ, but it should clearly expose:
- per-instance inflight
- qps_1m if present
- queue depths if available
- liveness state
- enough score/provenance information to understand why
least_load would choose an Instance
6. qps_1m handling
This issue does not need to implement a full rolling QPS counter.
Acceptable options:
- leave
qps_1m unchanged / optional
- document that qps remains unavailable until a later rolling counter issue
- if implementing qps, keep it minimal and clearly separated from inflight
Do not block this issue on QPS.
7. Preserve least_load semantics
least_load must continue to obey the existing semantics from PR #116:
- unknown load is not treated as real zero
- if all load metrics are unknown, fallback to round-robin
- ties are broken by round-robin among tied candidates
- empty input raises
RuntimeError("no instances")
Once Proxy-maintained inflight is wired, selected Instances should normally have known inflight values.
Non-goals
Do not implement yet:
- Scheduler resource-aware routing
- Scheduler Proxy selection based on
pool_resource
- KV-aware Instance routing
- KDN-aware routing
- KVCache locality scoring
- changes to IWS decision logic
- changes to request forwarding semantics beyond lifecycle-safe load counter hooks
- Resource Agent sampling changes
pool_resource schema changes
- full rolling QPS calculation unless it is very small and isolated
This issue is only about making Proxy-side Instance load counters real and observable for least_load.
Suggested implementation direction
A practical first implementation:
- Add thread-safe per-instance inflight update methods in
InstancePool.
- Initialize
InstanceInfo.load.inflight to 0 only when the Proxy has begun maintaining that counter for the Instance.
- In
proxy_chat_completions, call begin_request(chosen.instance_id) before enqueueing the ProxyTask.
- In the streaming wrapper, call
end_request(chosen.instance_id) in finally after the stream ends or is cancelled.
- In
proxy_completions, call begin_request(...) before enqueueing and end_request(...) in finally after response collection finishes.
- Add
/debug/instance_loads in Proxy control plane.
- Optionally include queue depth data from
QueueManager.queue_depth_snapshot() in debug output.
- Update docs with a short note that
least_load now uses Proxy-maintained runtime inflight.
Validation plan
Compile check
PYTHONDONTWRITEBYTECODE=1 python3 -m py_compile \
proxy/resource/instance_pool.py \
proxy/resource/p_control_plane.py \
proxy/proxy.py \
proxy/queue/*.py \
proxy/strategy/least_load.py \
proxy/strategy/factory.py \
test/demo_proxy.py
Unit-style smoke test
Use fake InstanceInfo objects or a real InstancePool instance to verify:
begin_request(instance_id) increments inflight
end_request(instance_id) decrements inflight
- repeated
end_request does not make inflight negative
- unknown instance returns false / no crash
least_load chooses lower inflight
- equal inflight ties are round-robin
Runtime smoke test
Start Scheduler, Proxy, and two Instances.
Start Proxy with:
python3 test/demo_proxy.py --strategy least_load --injection-strategy iws
Inspect before load:
curl -sS http://127.0.0.1:8002/debug/instance_loads | python3 -m json.tool
Send several concurrent requests. During active requests, verify:
inflight increases on selected Instances
least_load tends to avoid Instances with higher inflight
inflight returns to 0 after requests complete
Also verify existing round-robin still starts:
python3 test/demo_proxy.py --strategy round_robin
Failure/cancellation sanity check
Trigger or simulate a failed request path and verify:
inflight does not leak
/debug/instance_loads eventually returns inflight=0
Acceptance criteria
- Proxy maintains real per-Instance inflight counters locally.
least_load sees known InstanceInfo.load.inflight values during request execution.
- Inflight increments when a selected Instance receives work and decrements when work completes, errors, or is cancelled.
- Inflight never becomes negative.
- Chat streaming decrements only when the stream finishes or is cancelled.
- Non-streaming completions decrement after response collection finishes or errors.
- A debug endpoint exposes per-instance load state and optional queue depths.
round_robin remains default and unchanged.
least_load continues to fallback to round-robin when all loads are unknown.
- No Scheduler routing, KDN, KVCache, IWS decision logic, Resource Agent, or
pool_resource schema changes are introduced.
Background
PR #116 added the first Proxy-side resource-aware Instance selection strategy:
least_load.At the moment,
least_loadreadsInstanceInfo.load.inflightandInstanceInfo.load.qps_1mif they are available, and falls back to round-robin when all load metrics are unknown. This is correct as a strategy entry point, but the next step is to make those load fields reflect real Proxy-side runtime state.Resource data is currently mainly used for observability. This issue should wire real Proxy-maintained per-Instance load counters into the data plane so
least_loadcan make meaningful decisions.The current route flow is roughly:
select_instance(...)currently selects fromInstancePool.list(include_dead=False)and calls the configured strategy. This issue should keep that strategy interface intact while ensuring the selected Instance has runtime load counters updated around request execution.Goal
Add Proxy-maintained per-Instance load counters and expose them through
InstanceInfo.loadsoleast_loadcan select the least loaded live Instance using real local runtime state.The first implementation should focus on:
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 that are necessary for Proxy-local load counters,
least_loadintegration, and debug documentation.Required behavior
1. Proxy-maintained per-Instance inflight
When Proxy has selected an Instance and the request is accepted into the Proxy data-plane path, increment that Instance's runtime inflight counter:
When the request finishes, fails, raises, or the client disconnects, decrement it:
Important requirements:
try/finallyor an equivalent lifecycle guard so inflight is decremented on success, error, and cancellation.least_loadreads, preferablyInstanceInfo.load.inflight.2. Counter ownership and data model
Prefer adding small methods to
InstancePool, for example:or equivalent names.
The implementation should avoid direct mutation of
InstanceInfo.loadfrom many unrelated call sites. Keep the counter logic centralized.begin_request(...)should:load.inflightFalseif the Instance disappearedend_request(...)should:load.inflightwith floor 0Falseif the Instance disappeared3. Request lifecycle integration
Integrate the counters around both Proxy request paths:
For chat streaming, the selected Instance remains active until the streaming generator is exhausted or cancelled. The decrement must happen when streaming finishes, not immediately after returning
StreamingResponse.For non-streaming completions, decrement after
queue_mgr.iter_response(task)completes or errors.Suggested approach:
enqueue_prepare+ response collection block intry/finally.queue_mgr.iter_response(task)soend_request(instance_id)is called from the generator'sfinallyblock.enqueue_preparefails, still decrement.4. Queue depth integration
least_loadalready works withInstanceInfo.load.inflight/qps_1m.For this issue, queue depth integration is optional but preferred if it can be done without invasive request-flow changes.
Possible minimal path:
least_loadprimarily based on Proxy-maintainedload.inflight.QueueManager.queue_depth_snapshot()/PerInstanceQueueMap.snapshot_depths()if available.If queue hints are added to
least_load, they must remain optional and must not break the existing strategy interface.5. Debug visibility
Add lightweight debug visibility so runtime tests can verify the counters and future strategy decisions.
Suggested endpoint:
Possible response:
{ "ok": true, "strategy": "least_load", "instances": [ { "instance_id": "instance_127.0.0.1:9001", "is_alive": true, "inflight": 2, "qps_1m": null, "prepare_queue_depth": 1, "ready_queue_depth": 0, "active_prepare": 1, "active_ready": 0, "score": 2, "score_source": "proxy_runtime_inflight" } ] }The exact shape can differ, but it should clearly expose:
least_loadwould choose an Instance6. qps_1m handling
This issue does not need to implement a full rolling QPS counter.
Acceptable options:
qps_1munchanged / optionalDo not block this issue on QPS.
7. Preserve least_load semantics
least_loadmust continue to obey the existing semantics from PR #116:RuntimeError("no instances")Once Proxy-maintained inflight is wired, selected Instances should normally have known inflight values.
Non-goals
Do not implement yet:
pool_resourcepool_resourceschema changesThis issue is only about making Proxy-side Instance load counters real and observable for
least_load.Suggested implementation direction
A practical first implementation:
InstancePool.InstanceInfo.load.inflightto0only when the Proxy has begun maintaining that counter for the Instance.proxy_chat_completions, callbegin_request(chosen.instance_id)before enqueueing theProxyTask.end_request(chosen.instance_id)infinallyafter the stream ends or is cancelled.proxy_completions, callbegin_request(...)before enqueueing andend_request(...)infinallyafter response collection finishes./debug/instance_loadsin Proxy control plane.QueueManager.queue_depth_snapshot()in debug output.least_loadnow uses Proxy-maintained runtime inflight.Validation plan
Compile check
PYTHONDONTWRITEBYTECODE=1 python3 -m py_compile \ proxy/resource/instance_pool.py \ proxy/resource/p_control_plane.py \ proxy/proxy.py \ proxy/queue/*.py \ proxy/strategy/least_load.py \ proxy/strategy/factory.py \ test/demo_proxy.pyUnit-style smoke test
Use fake InstanceInfo objects or a real
InstancePoolinstance to verify:begin_request(instance_id)increments inflightend_request(instance_id)decrements inflightend_requestdoes not make inflight negativeleast_loadchooses lower inflightRuntime smoke test
Start Scheduler, Proxy, and two Instances.
Start Proxy with:
Inspect before load:
curl -sS http://127.0.0.1:8002/debug/instance_loads | python3 -m json.toolSend several concurrent requests. During active requests, verify:
Also verify existing round-robin still starts:
Failure/cancellation sanity check
Trigger or simulate a failed request path and verify:
Acceptance criteria
least_loadsees knownInstanceInfo.load.inflightvalues during request execution.round_robinremains default and unchanged.least_loadcontinues to fallback to round-robin when all loads are unknown.pool_resourceschema changes are introduced.