Skip to content

[KV Offload] SimpleCPUOffload: one host buffer per rank, opt-in HugeTLB pages - #60130

Open
GirasoleY wants to merge 10 commits into
vllm-project:mainfrom
GirasoleY:simple-offload-hugepage
Open

GirasoleY wants to merge 10 commits into
vllm-project:mainfrom
GirasoleY:simple-offload-hugepage

Conversation

@GirasoleY

@GirasoleY GirasoleY commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Functionality

  • SimpleCPUOffloadConnector allocates each rank's CPU KV caches as views into one host mapping sized to the resolved KV blocks, rounded up to the selected page size. cpu_bytes_to_use_per_rank remains the cache budget. Registration is split into chunks of at most 256 GiB, the limit HiSparse already uses, without bisecting a KV block or backing page.
  • The new cpu_hugepage_block_size key ("1GB" or "2MB") backs that mapping with explicit HugeTLB pages; without it, ordinary pages are used.
  • The mapping inherits the worker's NUMA memory policy, including vLLM's NUMA binding options or an external launcher policy.
  • A HugeTLB pool or container hugetlb limit that's too small fails at startup with an error naming the shortfall, never falling back to 4 KiB pages.
  • The mapping is excluded from fork() (MADV_DONTFORK), so child processes can't make pinned pages copy-on-write.
{"kv_connector": "SimpleCPUOffloadConnector", "kv_role": "kv_both",
 "kv_connector_extra_config": {"cpu_bytes_to_use_per_rank": 137438953472,
                               "cpu_hugepage_block_size": "1GB"}}

Performance evidence from the earlier revision

  • In vllm serve (TP=2, 64 GiB per rank, ordinary pages), engine init takes 19.0 s vs 20.2 s before.
  • With 1 GiB pages, pinning 128 GiB takes 0.19 s instead of 2.5 s on ordinary pages.
  • GPU↔CPU copy bandwidth is unchanged at about 42 GB/s.

Known limitations

  • Registration uses bounded chunks to avoid oversized CUDA registrations ([Bugfix][KV Offload] Register the offload region in chunks #51081). pin_tensor, unpin_tensor, and the registration boundary calculation live in vllm/utils/host_memory.py, shared by HiSparse and CPU/disk offload as applicable.
  • Each rank needs ceil(resolved cache bytes / page size) HugeTLB pages, reserved on the host and, on Kubernetes, requested as hugepages-<size>.
  • Up-front population needs Linux 5.14+ for HugeTLB pages; ordinary pages fall back to first touch on older kernels.

Related: #45693 (same key name for OffloadingConnector), #58852, #59430, #51081.

Validation

The AMD measurements below describe the earlier revision. Current CUDA validation is recorded separately below. NUMA placement now follows the worker policy.

MI350X (gfx950), ROCm 7.2.3, Linux 6.8, vllm/vllm-openai-rocm:nightly-92044241a0 with this PR's three changed files overlaid; the image's copies were byte-identical to this PR's base.

Test Result
pytest tests/v1/simple_kv_offload/test_host_buffer.py tests/v1/simple_kv_offload/test_worker.py 32 passed
8 ranks × 38 GiB (1 GiB pages), random store/load with byte-level checks, 120 s 0 mismatches; every rank's pages on its GPU's NUMA node
More 1 GiB pages than the host has (330 on 320) Fails at mmap with a clear error
150 GiB soak, 10 min, 6,724 concurrent fork()s 0 mismatches
vllm serve (TP=2, 32 GiB of 1 GiB pages per rank): SIGKILL a worker, SIGKILL the engine core, SIGTERM, then restart; plus SIGKILL/SIGSEGV/exception at buffer level, including mid-populate, mid-pin, and with forked children still alive Requests served; the HugeTLB pool and GPU memory return to baseline within 5 s each time, and the restarted engine serves

The 8-rank, soak and pool rows ran on the previous revision, which populated pages with 16 threads; this revision populates with one madvise call and was re-checked with the unit tests and the vllm serve crash/restart row.

The earlier AMD validation did not include CUDA or an offload-on/off output comparison. The subsequent CUDA runs below include a small-model output smoke comparison; a full model-accuracy evaluation remains outstanding. AMD CI already runs tests/v1/simple_kv_offload; the HugeTLB tests skip on hosts without a pool.

Review fixes (76365e976b, de6a707b43)

These AI-assisted fixes allocate only the resolved cache bytes, inherit NUMA policy, and preserve one registration per matching unregister. Shared helpers moved to vllm/utils/host_memory.py; their function ASTs are unchanged by the move. The independent HIP SDMA optimization is handled by #65559.

  • An isolated reproduction showed that the original chunked pin_tensor left one registered range after HiSparse teardown for a tensor larger than 1 GiB. pin_tensor now registers exactly one range again.
  • Pre-commit checks passed, including Ruff and mypy.
  • tests/v1/worker/test_utils.py::test_hisparse_registration_chunks_end_between_host_blocks: collection blocked by missing FlashAttention CUDA extensions. The environment was not modified.
  • Subsequent CUDA integration and output smoke results are recorded below. No before/after performance benchmark was run.

CUDA validation and alignment fix (26bcba935b, 2026-11-05)

Validated on an exclusive Slurm node with 4 NVIDIA GB200 GPUs, driver 580.126.09, CUDA 13.0 / PyTorch 2.14.0, and Linux 6.17 with 64 KiB OS pages. The isolated environment used this PR's Python source and native/generated dependencies from vllm/vllm-openai:nightly-40941f2c4ba96fba0a7e282a5355e89cd74d9ac5.

The first large HugeTLB run exposed a real bug: under NUMA binding, a registration ending inside a 2 MiB backing page returned cudaErrorInvalidValue. For example, registering 1 GiB - 64 KiB failed while a whole 1 GiB succeeded on the same mapping. The shared boundary helper now receives the actual backing-page size; HiSparse keeps its OS-page default. The previously failing four-rank scenario passed after this correction.

Check Result
Targeted host-buffer, worker, mixed-layout, ordering, boundary and NUMA integration tests 16 passed; 1 skipped because this kernel has no 1 GiB HugeTLB support
Four concurrent HugeTLB ranks, 38/38/12/12 GiB, 120 s 21,792 byte-exact round trips, zero mismatches; all pages on the configured NUMA nodes
150 GiB ordinary-page soak, 600 s, concurrent forks 18,490 round trips and 36,980 forks, zero mismatches
Qwen3-0.6B NUMA integration Restored 16 CPU KV blocks; generated token IDs matched the cold result
Qwen3-0.6B serving, TP=2, 32 GiB HugeTLB per rank Three greedy prompts matched offload-disabled outputs; restart served successfully
Worker SIGKILL / engine SIGKILL / server SIGTERM HugeTLB pool and GPU memory returned to baseline in 4.10 / 3.38 / 0.58 s
32 GiB buffer: SIGKILL during population, SIGKILL after partial registration, SIGSEGV, exception All resources recovered in under 1 s; core dumps disabled
Final cleanup HugeTLB configuration restored to zero pages, all GPUs at zero allocated MiB, container removed, Slurm allocation released

The ordinary-page soak started before the HugeTLB alignment correction; its OS-page arithmetic is unchanged, and ordinary-page focused tests passed again afterward. The CUDA run uses four ranks rather than the earlier eight-rank AMD setup. NUMA node 1 could reserve only about 25.9 GiB of 2 MiB pages, so its two ranks used 12 GiB each. This validates 2 MiB HugeTLB on GB200, not 1 GiB pages or all CUDA platforms. The output check is a three-prompt serving smoke test, not a full accuracy evaluation. No speedup is claimed from these runs.

This commit also extended the existing boundary test to HugeTLB page sizes. Pre-commit checks, including mypy, passed.

Simplification (2a6867aa5c)

  • The registration chunk limit is now the shared HOST_REGISTER_CHUNK_BYTES (256 GiB) instead of 1 GiB. With a 1 GiB limit and 1 GiB pages, every boundary had to be a 1 GiB multiple that is also a block boundary, so any block size that does not divide 1 GiB (for example MLA: 576 × 2 × 64 = 73728 B) failed at startup. With the larger limit, a boundary exists at lcm(block, page), which is 9 GiB in that example. HiSparse behavior is unchanged.
  • HostBuffer is now a function, alloc_host_buffer(), because torch.frombuffer keeps the mapping alive. It reuses the existing MADV_POPULATE_WRITE helper and its pre-5.14 fallback from shared_offload_region.py. cpu_hugepage_block_size accepts only the documented 2MB, 1GB and none.
  • The docs section, comments and the populate/pin timing log are shorter.
  • Tests check page rounding, HugeTLB pool consumption, fail-closed allocation and a GPU→CPU→GPU round trip on ordinary and HugeTLB pages. They no longer read smaps flags, kernel zeroing or private attributes. The single-fix pin/unpin test and the unrelated served_model_name change to the integration test were removed.
  • Validation: pre-commit (ruff, mypy) passed. tests/v1/simple_kv_offload/test_host_buffer.py: 7 passed, 2 skipped (no free hugepages on the host). test_worker.py and tests/v1/worker/test_utils.py could not be collected locally because the FlashAttention CUDA extensions are missing; they rely on CI. The GB200 runs above predate this commit.

Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

Not a duplicate: the related PRs target OffloadingConnector and HiSparse.

🤖 Generated with Claude Code

@mergify mergify Bot added the kv-connector label Oct 5, 2026
@GirasoleY
GirasoleY force-pushed the simple-offload-hugepage branch from ce10e69 to 9349d78 Compare October 5, 2026 22:51
@GirasoleY
GirasoleY marked this pull request as ready for review October 5, 2026 22:56

@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.

@GirasoleY
GirasoleY force-pushed the simple-offload-hugepage branch from 9349d78 to bbe189c Compare October 6, 2026 02:46
Every per-layer CPU cache of a rank is now a 4 KiB-aligned view into one
host mapping. A new opt-in key only selects the page source:

  "kv_connector_extra_config": {
    "cpu_bytes_to_use_per_rank": ...,
    "cpu_hugepage_block_size": "1GB",   # or "2MB"; default: ordinary pages
    "cpu_numa_strict": false
  }

For either source the mapping is anonymous MAP_PRIVATE with
MADV_DONTFORK, placed on the GPU's NUMA node (preferred, or bound with
cpu_numa_strict), populated up front with MADV_POPULATE_WRITE across up
to 16 threads (first-touch fallback for ordinary pages on pre-5.14
kernels), and pinned with a single cudaHostRegister, so every block copy
stays inside one registration. A single registration of >= 512 GiB fails
on CUDA (vllm-project#51081); per-rank buffers must stay below that.

- HugeTLB fails closed: a short host pool fails at mmap, and a short
  hugetlb cgroup limit or node pool fails at populate, instead of
  falling back to 4 KiB pages or raising SIGBUS later. No hugetlbfs mount
  is needed; under Kubernetes only the hugepages-<size> limit applies.
  The key name matches the OffloadingConnector proposal (vllm-project#45693).
- MADV_DONTFORK: fork() neither copies pinned pages nor leaves them
  copy-on-write under a GPU mapping.
- The block count still matches the scheduler; the mapping rounds up to
  whole pages.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Summer Yang <girasoleyang@gmail.com>
@GirasoleY
GirasoleY force-pushed the simple-offload-hugepage branch from bbe189c to 5c5a882 Compare October 6, 2026 07:10

@YannikHinteregger YannikHinteregger 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.

Thanks for the contribution! I left some inline comments and hope I provided enough context.

Could you please also include benchmarks showing total engine startup time before and after this change

try:
# Fault every page now: a cgroup or pool shortfall becomes an
# error here instead of SIGBUS or an OOM kill while serving.
self._mmap.madvise(_MADV_POPULATE_WRITE)

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.

Could we keep one buffer but register it in chunks? A single registration of 512 GiB or more fails on CUDA (#51081) so large per-rank buffers would fail at startup.

We’re also working on moving CPU pinning into a background thread. That path needs smaller chunks with a short yield between calls because a long registration can block other CUDA calls and delay inference (CUDA docs)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

addressed in 76e2461

Currently left this in main thread, but can definitely move to a background thread if needed.

flags, policy = mapping_info(buf.tensor.data_ptr())
assert "dc" in flags # MADV_DONTFORK sets VM_DONTCOPY
assert ("ht" in flags) == hugepages
assert policy == "prefer:0"

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.

Could you please mark this test to skip on systems where NUMA placement is unsupported or blocked

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

addressed in 76e2461

flags |= _MAP_HUGETLB | (hugepage_size.bit_length() - 1) << _MAP_HUGE_SHIFT
try:
# HugeTLB reserves every page from the host pool here.
self._mmap = mmap.mmap(-1, self.nbytes, flags=flags)

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.

Switching from torch.zeros to mmap bypasses PyTorch’s allocator, which honors THP_MEM_ALLOC_ENABLE=1. Could we apply MADV_HUGEPAGE ourselves when this flag is enabled and explicit HugeTLB pages are disabled?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

addressed in 76e2461

@mergify

mergify Bot commented Oct 17, 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, @GirasoleY.

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

…ies on SDMA

From HIP 7.16, hipMemcpyBatchAsync without hipMemcpyFlagExtPreferCE runs the
batch as a blit kernel on the CUs (__amd_rocclr_copyBufferBatch), which slows
concurrent decode like the Triton copier does. Pass the flag in the batch copy
attributes on 7.16+, so the copies stay on the SDMA engines at the same
bandwidth. CUDA is unchanged (flags stay 0).

Co-authored-by: Taoyu Zhu <zhutaoyu97@gmail.com>
Co-authored-by: v <v@inferact.ai>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
@mergify mergify Bot added the nvidia label Oct 29, 2026
@mergify mergify Bot added the nixl NIXL KV connector / multi-tiered KV offloading (KVCR) label Oct 29, 2026
Update the PR onto main while preserving its published history. Register
host memory in bounded chunks, roll back partial pinning on failure,
preserve transparent hugepage advice, and skip NUMA placement checks
when placement is unsupported or blocked. Document HugeTLB configuration.

The resulting tree matches the locally tested rebased revision.

Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: Summer Yang <girasoleyang@gmail.com>
@mergify

mergify Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Documentation preview: https://vllm--60130.org.readthedocs.build/en/60130/

@mergify mergify Bot added documentation Improvements or additions to documentation and removed needs-rebase labels Oct 29, 2026
@GirasoleY GirasoleY added the ready ONLY add when PR is ready to merge/full CI is needed label Oct 30, 2026
@GirasoleY

GirasoleY commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

/ci run

@GirasoleY GirasoleY added the verified Run pre-commit for new contributors without triggering other tests label Oct 30, 2026
@GirasoleY

Copy link
Copy Markdown
Contributor Author

start up time before after for Qwen3-8b
Screenshot 2026-10-29 at 6 13 52 PM

ivanium and others added 2 commits November 5, 2026 00:09
…aries

Allocate only the resolved CPU cache size and inherit the worker's NUMA memory policy. Keep pin/unpin paired per range and reuse HiSparse block-safe registration boundaries for chunked CPU offload pinning.

Remove the independent HIP copy-engine optimization, tracked in vllm-project#65559.

Validation: pin/unpin regression tests passed (2 cases); original registration leak reproduced; Ruff and diff checks passed. Registration-boundary test collection was blocked by missing FlashAttention CUDA extensions. GPU and end-to-end tests were not run.

Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
Move pin_tensor, unpin_tensor, and host_registration_ranges into vllm.utils.host_memory. Update HiSparse, CPU offload, and disk offload to import the shared helpers directly.

The three function ASTs are unchanged. The existing registration/teardown regression test passed both cases; Ruff and diff checks passed.

Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
@mergify mergify Bot added the hisparse Related to HiSparse label Nov 5, 2026
@mergify

mergify Bot commented Nov 5, 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, @GirasoleY.

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 Nov 5, 2026
ivanium and others added 2 commits November 5, 2026 01:59
CUDA on GB200 rejects a HugeTLB registration ending inside a backing page under NUMA binding. Pass the buffer page size to the shared boundary calculation, preserving the OS-page default for HiSparse.

Use a real worker test config, cover both HugeTLB pool sizes, and stabilize the NUMA integration test's metric label for cached model paths.

CUDA validation on Slurm job 45182: 16 targeted cases passed, one unsupported 1 GiB-page case skipped. Four HugeTLB ranks ran for 120 seconds with zero mismatches. The 150 GiB ordinary-page soak completed 18490 round trips and 36980 forks. TP=2 Qwen3-0.6B outputs matched offload-disabled results; worker/engine kills, SIGTERM, restart and buffer-level exits recovered resources within 5 seconds. HugeTLB settings were restored and the allocation released.

Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
Reuse HOST_REGISTER_CHUNK_BYTES (256 GiB) as the shared registration chunk
limit. With a 1 GiB cap, 1 GiB HugeTLB pages failed at startup for any block
size that does not divide 1 GiB (for example MLA layouts); the larger cap
leaves room for a boundary at lcm(block, page). HiSparse keeps the same limit.

Replace HostBuffer with alloc_host_buffer(): torch.frombuffer keeps the
mapping alive. Reuse the existing MADV_POPULATE_WRITE helper and its
pre-5.14 fallback, accept only the documented page sizes, and drop the
unused timing log, pin yield and stale connector comment. Shorten the docs.

Tests: check page rounding, pool consumption and fail-closed behavior instead
of smaps flags and kernel zeroing; make the worker test a store/load
round trip without private attributes; move hugepage helpers to conftest;
drop the single-fix pin/unpin test and the unrelated integration-test change.

Validation: pre-commit (ruff, mypy) passed; test_host_buffer.py 7 passed,
2 skipped (no free hugepages). test_worker.py and tests/v1/worker/test_utils.py
could not be collected locally (missing FlashAttention CUDA extensions).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>

@ivanium ivanium 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.

Thanks for the PR! @GirasoleY I made a bunch of small simplifications; please help double check

@github-project-automation github-project-automation Bot moved this to Ready in NVIDIA Nov 5, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
@mergify mergify Bot removed the needs-rebase label Nov 5, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation hisparse Related to HiSparse kv-connector nixl NIXL KV connector / multi-tiered KV offloading (KVCR) nvidia ready ONLY add when PR is ready to merge/full CI is needed verified Run pre-commit for new contributors without triggering other tests

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

4 participants