frontend: attribute offered load to the selected worker namespace - #37
frontend: attribute offered load to the selected worker namespace#37Pernekhan wants to merge 2 commits into
Conversation
A frontend that serves several worker namespaces for one model -- the
`<stem>--<group>` shape namespace grouping produces -- labels all of its own
metrics with its own `dynamo_namespace`, which is the model stem. Every group's
planner therefore reads the same model-wide `requests_started_total` and sees
the whole model's demand instead of its own, so each one scales for traffic
that another group is actually serving.
Add `dynamo_frontend_worker_namespace_requests_started_total{model,
worker_namespace}`, incremented where the WorkerSet is chosen. That point is
before any queue or scheduler work, so it measures offered load: a request
later rejected with 529 still counts as demand, which is what a scaling
decision needs. Selection is refactored to hand back the chosen WorkerSet
alongside the extracted engine so there is exactly one increment site covering
every entry path, including the single-set fast path.
The router's own `requests_started_total` is not an alternative here:
`RouterRequestMetrics::from_component` is a process-global OnceLock, so it
cannot carry per-group series, and it increments only after admission.
Planner side, `get_avg_request_count` prefers the new counter filtered on its
own namespace and falls back to the model-wide one, so a planner running
against an older frontend is unaffected. The histogram namespace test becomes a
stem-prefix match rather than equality: TTFT/ITL series only exist model-wide,
and under equality a grouped planner would read zero for both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rS1uFFKT2xV4x63Amfvtw
The stem match shipped in the previous commit accepted any prefix and treated an absent label as a wildcard. Both are too loose. Prod carries frontend series with no `dynamo_namespace` at all -- on the rig, `deepseek-ai/DeepSeek-V3.2-dynamo` has six such series alongside its labelled ones. A wildcard would sum those into every planner's reading for that model and inflate its offered load. Equality rejected them before, so rejecting an empty label keeps the prior behaviour rather than changing it. Requiring a `--` boundary likewise stops `<stem>-extra` from matching `<stem>`, which is a different model rather than a group of one. Add the truth table as a unit test, using the rig's real namespace strings, and cover both branches of the new offered-load query: the per-group counter when it has series, and the fall back to the model-wide counter when it does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017rS1uFFKT2xV4x63Amfvtw
Measured on a real two-group family, not just unit-testedA two-group dynamo family was stood up on our cluster to exercise this directly: The setup is exactly the case this PR addresses:
A prefix frontend is model-wide, so it can only carry the stem. Confirmed in the metric store: One series, labelled with the stem. The planner filters on The planner is consequently blind on every arm, from its own logs: TTFT and ITL fall to Demand is genuinely per-group and genuinely observable — just not on the arm the planner reads: Traffic through the single prefix frontend splits across both groups (consistent with weighted-random One caveat worth recording for anyone verifying on an agg-only family: the load-based arm is dead there for an unrelated reason — |
Problem
With namespace grouping, one frontend serves several worker namespaces (
<stem>--<group>) for a single model. The frontend labels all of its own metrics with its owndynamo_namespace, which is the model stem — not the group a request landed on.So every group's planner reads the same model-wide
dynamo_frontend_requests_started_totaland sees the whole model's demand rather than its own. Each planner then scales for traffic another group is actually serving.Change
Frontend. New counter:
incremented where the
WorkerSetis chosen. That point sits before any queue or scheduler work, so it measures offered load — a request later rejected with 529 still counts as demand, which is what a scaling decision needs.select_worker_set_withis split so the selection proper hands back the chosenWorkerSetalongside the extracted engine. That gives exactly one increment site covering every entry path, including the single-WorkerSetfast path, rather than one per typed wrapper.Ungrouped models emit the same series shape with their single namespace, so old and new deployments look alike to the planner.
Planner.
get_avg_request_countprefers the new counter filtered on its own namespace, and falls back to the model-widerequests_started_totalwhen the metric has no series — a planner pointed at an older frontend is unaffected. Two existing tests pinned the query order and are updated for the extra leading query.The histogram namespace test becomes a stem match instead of equality. TTFT/ITL series only exist model-wide; under equality a grouped planner reads zero for both and its SLA arms go dead.
The match is deliberately narrower than a bare prefix:
<stem>--g1<stem><stem>--g1<stem>--g1<stem>--g1<stem>--g2<stem>--g1""/ absent<stem><stem>aa--Foo-extraaa--Foo--boundaryRejecting an empty label matters in practice: on our cluster
deepseek-ai/DeepSeek-V3.2-dynamoemits six frontend series carrying nodynamo_namespaceat all. Treating absent as a wildcard would sum those into every planner's reading for that model. Equality rejected them before, so this preserves the prior behaviour rather than loosening it.Why not the router's counter
RouterRequestMetrics::from_componentis a process-globalOnceLock(kv_router/metrics.rs:836-880), sometrics_source="router"cannot carry per-group series. It also increments only after admission (request_guard.rs:309), which measures accepted load, not offered load.Cardinality
Two labels, one series per (model, group). Groups are expected to stay in the low single digits per model.
Validation
cargo check -p dynamo-llmclean, no new warnings.cargo test -p dynamo-llm --lib discovery::model— 111 passed. New test asserts the fast path counts, that the weighted path credits the group actually drawn, and that a non-selection (extractreturnsNone) does not count.test_prometheus.pyrun against a matched module build, before vs after this change: identical failure set (11 pre-existing, from running a v1.4.1 test file against a July-9 planner image), +10 passing — the truth table above plus both branches of the new offered-load query.🤖 Generated with Claude Code
https://claude.ai/code/session_017rS1uFFKT2xV4x63Amfvtw