Repository navigation
feat(v1): model pools: bind each session to a deployment with room - #2971
Closed
RGaonkar26 wants to merge 1 commit into
Closed
RGaonkar26 wants to merge 1 commit into
RGaonkar26 wants to merge 1 commit into
Conversation
An agent can name a pool of deployments in place of one model and client:
[env.solver.model_pool]
deployments = [
{ id = "ds-a", model = "internal/deepseek-v4", client = { base_url = "...", request_gate = { max_concurrent = 64 } } },
{ id = "glm", model = "glm-5.3", client = { request_gate = { max_concurrent = 100 } } },
]
The design is Parker Pettit's (f04a56a), on top of the request gate.
- `ModelPoolConfig` / `ModelDeployment` (`configs/model_pool.py`, exported as
`vf.ModelPoolConfig`, `vf.ModelDeployment`). Every deployment's client must
set a `request_gate`: that is its capacity. The gate's new `scope` defaults
to the deployment id, so deployments behind one URL and key keep separate
budgets. Ids are unique; a training pool needs one model and renderer.
- `AgentConfig.model_pool` (and `ModelContext.model_pool`). With it set and
`model` unset, the first deployment stands in until a session binds.
- At a pooled session's first model request, the interception server binds it
(`model_routing.request_binding`): the deployments are tried in random
order with one non-queuing admission each (`ModelGate.try_acquire`), and the
first with a free slot wins. With none free it retries after a wait that
doubles from 30 ms to about 1 s (jittered), so many waiting sessions cannot
flood the daemon; a session waiting to bind yields to requests already
queued at a gate (those of bound sessions). The
admitted slot is handed to that request through `reserved_permit`, so the
choice and the admission are one step. The session keeps the deployment,
model and client for every later request, each passing that deployment's
gate as usual. A resumed session keeps a saved deployment the pool still
has, else binds afresh; in a pool of different models that can change the
model mid-conversation (each call still records its own), which a
`same_model` pool rules out.
- The binding is recorded: `trace.info["model_binding"]` (deployment, model,
base URL), the trace's agent config (model, client), and a new
`ModelCall.deployment` on every call. A per-call record means any label or
score can be traced to the deployment and model that produced it.
- `same_model = true` requires every deployment to serve the same model id
(replicas of one model and revision), for results that must not mix models
(e.g. calibration labels).
- Off by default: with no pool, nothing is imported or bound, and the new
`model_pool` and `deployment` fields are left out of dumps, so configs and
traces stay readable by verifiers without them.
- A paragraph in docs/evaluation.md.
Sources (Parker Pettit, verifiers `feat/pipelines`): f04a56a ("Add
capacity-gated model pools and live session binding": `ModelPoolConfig`,
`_choose`, `request_binding`, `RequestPermit`, gate `scope`); #2946
c92797b (`try_acquire` on the daemon); dad67e0 (one gate across auth
dialects, so the choice's gate is the request's).
Differences from f04a56a:
- No live switch of a running session's model or pool (`steering.switch_model`,
`SUPPORTS_MODEL_SWITCH`): main has no steering registry. A pool is chosen
per rollout; flow can change which pool future calls get (as flow's
031b6be does) without it.
- No `AgentConfig.session_id`: without a live switch, the trace id stays the
affinity key.
- New: `ModelCall.deployment`, `same_model`, a backoff on the bind poll
(fp polls every 30-80 ms without bound), and the gate for a deployment is
built off the event loop.
- Tests (none existed on feat/pipelines): spread under caps, waiting at cap
and taking the freed slot, random order, stickiness and per-call records
through the interception server, the permit carrying the first request,
resume and a removed deployment, config validation, and no change without
a pool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 23, 2026
Author
|
Closing in favour of Parker's #2946, which is the same request-gate / model-pool work and where it should land. Nothing on verifiers main or our pipeline needs it right now. The branch stays up: it has shared-host hardening (private 0700 dir, peer-uid checks, single-daemon lock, restart window), live tests against the Prime Inference gateway, and the pool poll backoff, all of which apply to #2946 and can be cherry-picked. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An agent can name a
model_poolof deployments instead of onemodelandclient. Each rollout binds, at its first model request, to a deployment whose request gate has room, and stays on it. The trace records the deployment for every call.Why
Spreading rollouts over several replicas or models within each one's concurrency cap (for example several replicas of one model, or GLM-5.3 and GLM-5.3-flash) needs a binding step. Each deployment's request gate already holds its cap host-wide, so a pool is just several gates in the same daemon, and the daemon needs no change.
Change
ModelPoolConfig(deployments, same_model=False)andModelDeployment(id, model, client)inconfigs/model_pool.py, exported fromverifiers.v1, plusAgentConfig.model_pool. Every deployment must setclient.request_gate. Itsscopedefaults to the deployment id, so two deployments behind one URL and key keep separate caps.model_routing.py: at a session's first model request, the interception server shuffles the deployments and binds to the first one that grants a non-queuing admission (ModelGate.try_acquire). Such an admission yields to requests already queued at that gate. When every deployment is full, it retries with a jittered backoff from 30 ms up to 1 s.reserved_permit), so a session is admitted once, not twice. An unused slot is returned.trace.info["model_binding"],trace.agent.config.modeland.client, and a newModelCall.deployment.same_model = truerejects a pool whose deployments serve different model ids. It is meant for replica pools whose outputs must be attributable to one model.Behaviour
model_pool, no binding code runs. When unset,model_poolanddeploymentare left out of dumps, so configs and traces still load in older verifiers.same_modelrules this out.Tests
tests/unit/request_gate/test_model_pools.py, with fake providers. They cover the spread under caps of 1, 2 and 3, the backoff bounds, one admission for the choice plus the first request, resume, config validation, and off by default. Unit suite: 283 passed, 18 skipped.trace.info["model_binding"], and each deployment's upstream peak was 2.same_modelrejected a mixed pool. Two replicas at one URL and key kept separate caps (peak 1 + 1).Notes
GatedAsyncClient.send.feat/pipelinesf04a56a14(plustry_acquirefrom The request gate admits through one daemon per host #2946 anddad67e0b4). Differences: no live model switch (steering.switch_modelneeds a steering registry, which main lacks) and noAgentConfig.session_id. New here:ModelCall.deployment,same_model, the backoff on the bind poll, and tests.Parker, as with the gate: this could land on main and reach
feat/pipelinesat its next main merge, withswitch_modelstaying on your branch until main has steering. Or you could open it from this branch yourself. Which do you prefer?🤖 Generated with Claude Code