Skip to content

Built-in metric mappings merge histograms whose buckets don't match #523

Closed

Description

@dennis-upbound

What happened?

The built-in mappings rename histograms from different engines, engine versions and deployments onto one modelplane_* name, but nothing guarantees those sources share bucket boundaries. When they don't, a fleet quantile over the shared name is wrong while looking plausible.

The module docstring (metrics.py#L21-L27) and the MetricMapping XRD both say a histogram is only renamed onto a shared name where its buckets match. That holds when a histogram's buckets are fixed per component, as they are for the gateway and the endpoint picker. It doesn't hold for these:

  1. vLLM and SGLang token counts. metrics.py#L50-L51 and #L72-L73 both feed modelplane_request_input_tokens and modelplane_request_output_tokens. vLLM v0.23.0 buckets them 1, 2, 5, 10, 20, 50, 100, 200, 500, …; SGLang v0.5.12.post1 uses 100, 300, 500, 700, 1000, 1500, …
  2. vLLM token counts across deployments. vLLM builds both token histograms with build_1_2_5_buckets(max_model_len), so the top bucket follows each deployment's --max-model-len. The examples use 8192, 16384, 40960 and 131072, which gives top buckets from 5000 to 100000. Two vLLM deployments on the same version are enough to break the quantile.
  3. SGLang token counts across versions and flags. The default list changed between v0.5.10.post1 (…, 12000, 15000, 20000, 22000, …, 66000, 99000, 132000, …) and v0.5.12.post1 (…, 12500, 15000, 17500, …, 60000, 80000, 100000, 200000, …). The repo pins both. --prompt-tokens-buckets and --generation-tokens-buckets override it per deployment.
  4. vLLM latency across versions. v0.7.3 runs the V0 engine by default. Its TTFT buckets stop at 10.0 and its e2e and queue buckets stop at 60.0. v0.9.2 and later extend them to 2560 and 7680. The llama-3.1-8b recipe pins v0.7.3, and the others pin v0.11.0 or later.

Run through the collector, a mismatch shows up two ways. Where one source has a bucket the other lacks, sum by (le) stops being monotonic. Where one source's top bucket is lower, its observations above that bucket only count toward +Inf, so the fleet histogram puts them above the other source's highest bucket. Both happen in the reproduction below. In each case every engine serves the same 1000 requests:

# Case 1: vLLM v0.23.0 and SGLang v0.5.12.post1, every prompt 150 tokens
sum by (le) (modelplane_request_input_tokens_bucket)
  le=100 0   le=200 1000   le=300 1000   le=500 2000   le=700 1000   le=1000 2000
histogram_quantile(0.9, sum by (le) (...))                 460    (true: 150)
by (deployment, le)                          vLLM 190, SGLang 280

# Case 2: vLLM v0.23.0, max_model_len 4096 and 32768, every prompt 3000 tokens
sum by (le) (modelplane_request_input_tokens_bucket)
  le=2000 0   le=5000 1000   le=10000 1000   le=20000 1000   le=+Inf 2000
histogram_quantile(0.5, sum by (le) (...))                5000    (true: 3000)
histogram_quantile(0.9, sum by (le) (...))               20000

In case 2 the fleet histogram says half the requests had prompts over 20,000 tokens, but the longest prompt was 3000.

Grouping by deployment avoids the cross-deployment cases but not the cross-version one during a rolling engine upgrade, and nothing on a series says which engine or version produced it: engine holds the engine's name from the ModelReplica, not vLLM or SGLang.

How can we reproduce it?

  1. Render the collector config with collector.config(...) from main, metrics.BUILTIN_MAPPINGS and a prometheus sink. Replace the receiver's scrape configs with static targets labeled with deployment, replica, engine and namespace.
  2. Serve a static /metrics from each target with the source histograms above, using each engine's real bucket list and 1000 identical observations.
  3. Run otel/opentelemetry-collector-contrib:0.161.0 with that config, and have Prometheus v3.5.0 scrape its exporter.
  4. Query histogram_quantile(0.9, sum by (le) (modelplane_request_input_tokens_bucket)).

What environment did it happen in?

Modelplane version: main at 0ee6ab4 (also v0.5.0)
Collector: otel/opentelemetry-collector-contrib:0.161.0
Engines: bucket layouts from vLLM v0.7.3, v0.11.0 and v0.23.0, and SGLang v0.5.10.post1 and v0.5.12.post1, the versions the repo pins
Backend: Prometheus v3.5.0

Activity

  1. dennis-upbound commented on Oct 20, 2026

    @dennis-upbound
    CollaboratorAuthor

    Options for this, before anyone builds one. Only modelplane_request_input_tokens and modelplane_request_output_tokens take histograms from two engines. The latency histograms (ttft, duration, queue, prefill, decode) come from vLLM alone, so the mismatch there is only between vLLM versions.

    1. Group by deployment. A quantile over these names has to be sum by (deployment, le), never sum by (le). A deployment's engines are one kind of engine and normally share a --max-model-len, which fixes cases 1 and 2. Rolling a deployment to a new engine version still mixes buckets for the length of the roll. The telemetry docs would say this, beside each histogram.
    2. Give the token histograms per-engine names, such as modelplane_vllm_request_input_tokens. This keeps vLLM and SGLang apart even under sum by (le), but vLLM deployments with different --max-model-len still don't share buckets. It also breaks any dashboard that compares engines. Comparing engines is why the names are shared.
    3. Label the engine image. The collector's engine scrape job copies __meta_kubernetes_pod_container_image into an image label. Every engine series then says which engine and version produced it. Grouping by (deployment, image, le) then also separates a rolling upgrade's two versions. It's one relabel rule, and adds one label with a few values per deployment.

    I'd do 1 and 3. Option 2 costs the cross-engine comparison and doesn't fix the per-deployment case that 1 already handles. @negz, does an image label on engine series fit the modelplane_* metrics?

  2. dennis-upbound commented on Oct 20, 2026

    @dennis-upbound
    CollaboratorAuthor

    Replacing the options in my comment above with a direction, since we want this fixed for v0.6.

    Those options left a shared name with more than one bucket layout and asked whoever writes the query to group around it. A fleet dashboard is written once, by someone who hasn't read that rule, and sum by (le) gives a wrong number with only a Prometheus info annotation to say so. I ran the repro into Prometheus v3.5 and v3.15, classic and NHCB: no setting gets the right answer, and v3.15's NHCB bucket reconciliation still gives p90 = 460 against a true 150. A central Prometheus doesn't change that either, since the repro already used one.

    The rule I'd adopt: every modelplane_* histogram has exactly one bucket layout across the fleet, so the obvious query is the right one.

    That already holds for the gateway and the picker. It holds for the engine latency histograms once nothing runs vLLM older than v0.9.2: their buckets are identical from v0.9.2 to v0.27.1, and only the llama-3.1-8b recipe still pins v0.7.3. It can't hold for the token histograms. vLLM sizes them from --max-model-len, so two vLLM deployments disagree, and SGLang's change between releases and with --prompt-tokens-buckets. Per-engine names wouldn't help, because vLLM disagrees with itself.

    For v0.6:

    1. Export the engine token histograms as counters only. Take each one's sum and count with part: Sum and part: Count, onto modelplane_engine_tokens_total{direction} and modelplane_engine_requests_total, and stop exporting the buckets under modelplane_request_input_tokens and modelplane_request_output_tokens. I ran this through the collector against vLLM and SGLang bucket layouts and against two vLLM context lengths. Totals per deployment and across the fleet were exact, and so was the fleet mean prompt length (1575, the true value), with no bucket series left. The engine_ prefix keeps these apart from the gateway's modelplane_tokens_total in the design, so the two can't be added together by accident.
    2. Move the llama-3.1-8b recipe off v0.7.3. That's Some shipped recipes and cloud stacks leave built-in metrics empty #524 as well.
    3. Add a test that fails when two built-in mappings send a histogram to the same target. Map SGLang's prefix cache and retraction counters #544 left sglang:queue_time_seconds out by hand for this reason, and the test means the next mapping doesn't depend on someone remembering.

    What it costs: the normalized engine metrics no longer carry a token-length distribution, including the per-deployment one, which was correct. Two places can supply it later, and neither blocks this. The gateway's gen_ai_client_token_usage has fixed buckets and is planned for modelplane_tokens_total in #476. passthrough on a MetricMapping (telemetry.md#L225) is in the design but not in the XRD yet. Anyone on v0.5.0 using the buckets of modelplane_request_input_tokens loses them, so this needs a release note and an update to the metric surface table (telemetry.md#L621).

    The engine counters still double when a prefill/decode deployment's two legs are summed. That's #525, which needs a phase label rather than anything here.

    An image label (option 3 above) would still help compare engines and separate a rolling upgrade, but this issue no longer needs it.

    @negz does this work for you?

  3. moved this from Backlog to In progress in Modelplane Roadmapon Oct 20, 2026
  4. added 11 commits that reference this issue on Oct 20, 2026
    5f7b7cb
    9213d61
    0005770
    d867e6c
    a49c4fc
    d24c98c
    0d20097
    6aafd50
    bef5e2d
    7c6ea9c
    1591000
  5. moved this from In progress to In Review in Modelplane Roadmapon Oct 21, 2026
  6. moved this from In Review to Done in Modelplane Roadmapon Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions