[Draft][MoE][Offload] Expert pool: shared GPU bank of NVFP4 Marlin expert rows with a device-side planner (moe_expert_pool_rows) - #48
Conversation
…th a device-side planner (moe_expert_pool_rows) Opt-in via --moe-expert-pool-rows N (default 0: unchanged). MoE layers keep their expert tensors in pinned host memory; after loading, one VRAM bank shared by all layers holds N rows per layer, a device-side LRU step program plans promotions per forward (no host code in the forward, so the MoE op stays inside CUDA graphs), and a Marlin consumer runs on the bank with logical alignment and a physical-row remap. Wider batches take a bank + host-view partition path. The placement is frozen (gate closed) through profiling and graph capture and opened at the end of warm-up. Supported: ModelOpt NVFP4 Marlin MoE backend, no EP/DP. Rejected at layer construction otherwise. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QWXP5rMj1rGh9xasXNLyT Signed-off-by: 01554 <24953377+01554@users.noreply.github.com>
|
Review 1 — static extraction review of 69fd13b (base a97dacb), with integrated PLE head 9a67b70. No required changes found in the reviewed extraction diff. Relative to the accepted bc8 configuration, the expert_pool package changes only the installation attribute names and documentation; planner, copy and consumer code are unchanged. The integrated PLE model and runner paths are unchanged. Checked the default-off pool knob, nonnegative validation, hash inclusion, CLI and engine-config pass-through, early Marlin/EP/DP/SP restrictions, pending installation guard, shared-expert overlap behavior, and post-warm-up gate boundary. The standard loader runs per-layer finalization inside device_loading_context, restores CPU parameters, then installs the pool after all layers. Removing the chunked host repack path is consistent with that standard loader path; this is a source-reading conclusion, not a claim about every loader. Pool-owned pinned source normalization and lifetime references remain. Removing the loader marker/helper restores upstream behavior; the final load peak and temporary Parameter lifetime still need verification on this exact assembly. Optional follow-ups: directly exercise the new early DP/SP and rows < top_k guards in CPU tests. The CLI test currently constructs OffloadConfig manually, so it should not be described as executing create_engine_config (the production pass-through was checked statically). I have not executed full-environment tests for this head. Exact regression tests and final 48 GiB capacity, fresh-three-run speed, and 32K quality acceptance remain pending. Earlier bc8 acceptance does not establish acceptance of this minimal assembly. Keep Draft pending both reviewers and final validation. |
…t scope note Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QWXP5rMj1rGh9xasXNLyT Signed-off-by: 01554 <24953377+01554@users.noreply.github.com>
856144b to
e5cc8d6
Compare
|
Review 1 follow-up: checked e5cc8d6 against 69fd13b. Only two test files changed; production code is unchanged. The added tests directly exercise the supported early-guard case and six rejection cases, and the CLI test now correctly states that create_engine_config is not executed. No required changes found; the static review carries forward to this head. Execution results and final integrated acceptance remain pending. |
|
Review 1 correction after exact-environment execution: independently checked the 9a67 test log (40 passed, 2 failed). The migrated CUDA fixture still calls process_weights_after_loading directly with CPU expert tensors, relying on the host-repack path removed by this extraction. I missed this fixture dependency in the initial static review. It must use the standard device_loading_context around finalization, verify device processing and CPU/pinned restoration, then reach install and the pool oracle. The CLI test also needs a local model path to avoid unrelated offline snapshot lookup. Both issues remain in the e5cc8 test version. These are required test fixes before acceptance; the pool oracle has not been reached in this run, and these failures do not establish a production runtime defect. Author and GPU reviewer have been notified. |
…ding_context; CLI test without model resolution Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QWXP5rMj1rGh9xasXNLyT Signed-off-by: 01554 <24953377+01554@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QWXP5rMj1rGh9xasXNLyT Signed-off-by: 01554 <24953377+01554@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com> Signed-off-by: 01554 <24953377+01554@users.noreply.github.com>
Draft for team review (author claude, review 1 mid, GPU/review 2 astra). Fork-only; nothing is proposed upstream from here without the user's go.
What
Opt-in expert pool for MoE models whose experts do not fit the GPU:
--moe-expert-pool-rows N(OffloadConfig.moe_expert_pool_rows, default 0 = unchanged behaviour).process_weights_after_loading,install_expert_poolallocates one VRAM bank shared by all MoE layers (N rows per layer resident at start, plus top_k staging rows), keyed by (layer, expert).moe_align_block_size(..., ignore_invalid_experts=True)) and a physical-row remap. The device-planned decode path runs no host code between the router and the Marlin call, so the MoE op stays inside CUDA graphs (FULL_DECODE_ONLY works); the Python dispatch around it and the prefill/partition path are ordinary eager code.compile_or_warm_up_modelafter a table check and a sampled bank/host comparison.Supported: ModelOpt NVFP4 with the Marlin MoE backend, no EP/DP/SP; rejected at layer construction otherwise.
Base and dependencies
Base
lab/upstream-base-20260909= upstream main a97dacb. No unmerged dependency (the earlier pool head on PR vllm-project#37190 is replaced by this standalone form; the host cache providers, breakable-graph break points, loader marker and chunked host repack are dropped, see the lab ledgernotes/upstream-pool-standalone-ledger-2026-09-09.md).Tests
tests/kernels/expert_pool/test_pool_marlin_cuda.py(two layers sharing one bank through the real Marlin consumer, with the host-source fixture run under the loader'sdevice_loading_context), the guard, shared-experts and config tests.test_pool_tables.py+test_pool_layer_helpers.py, 18 passed. pre-commit (ruff, format, mypy 3.10, config docstring check) passed.--no-deps, this head's Python sources overlaid, and the_ple_memopsextension built in addition; the candidate head is not rebuilt as a full wheel per run.Measurements
Purpose: run Qwen3.8-Flash-Next NVFP4 (48 layers x 512 experts) on an RTX 6000 Ada (48 GB). Development and measurement used an RTX PRO 6000 Blackwell Max-Q (96 GiB) with a separate process holding GPU memory so that vLLM had 48 GiB available; the container's host memory limit was 100 GiB.
Code measured
1970f3ed4, vllm-project#54129 branch50a061f7a97dacb7, this PR5fbc240ba, pull/464f859de9d; combined verification commit7dedc6d8dThe combined commit identifies the code that was measured; it is not the head of either PR. The server ran the candidate's Python sources over a wheel built from
a97dacb7, plus the_ple_memopsextension built separately.Speed (max-model-len 4096, one request at a time)
Two frozen prompts are sent in order to a freshly started server: a warmup request (1070 input tokens) and the measured request (753 input tokens); both ask for a fix proposal and a verification plan for a bug report.
temperature=0,top_p=1,seed=0,max_tokens=2048, thinking disabled. Decode speed = (completion_tokens - 1) / (time between the first and last text event), measured by the client. Prompts, the client, and the exact launch commands are inbenchmarks/expert_pool/on this branch.--offload-backend uva --cpu-offload-gb 40 --cpu-offload-params experts--enforce-eager, PLE mmap--moe-expert-pool-rows 258(about 32 GiB bank)cudagraph_mode=FULL_DECODE_ONLY, PLE mmap + deferred rows (VLLM_PLE_MMAP_DEFERRED=1)Both rows differ in source version and execution mode, as listed. Common launch arguments:
Common environment:
VLLM_USE_V2_MODEL_RUNNER=1 VLLM_PLE_MMAP=1 VLLM_PLE_MMAP_PREWARM=0 VLLM_PLE_MMAP_PINNED=0. The proposed row adds--compilation-config '{"cudagraph_mode":"FULL_DECODE_ONLY"}' --default-chat-template-kwargs '{"enable_thinking":false}'.The acceptance line for this work was 62.985 tok/s (our internal reference build's 66.30 minus 5%); the median above is a check against that line, not a tuned number.
Quality (max-model-len 32768, separate launch, proposed configuration)
Long inputs (separate capacity configuration:
--moe-expert-pool-rows 208,--max-model-len 262144)At N=258 the 262144 context does not fit (KV needs 6.55 GiB, 1.24 GiB free), so the long-input runs use N=208 (about 6.2 GiB moved from the bank to KV); everything else as above. Each length is one request: a document with five key/value facts at 5/25/50/75/95 %, exact input length built from token ids,
max_tokens=512, answer requested as JSON. Client-observed; prefill = input tokens / TTFT.The 4K/64K/128K requests were sent in order to one server (prefix caching may apply); 261632 was a fresh single request. Results: results/goal66-long-context-262k-n208-20260909 and -resumed-20260909 in the lab repo; client
scripts/long_context_pool_check.pythere.For reference, the same frozen inputs on unmodified FreeToken af71ba4 (its own offload backend, resident expert bytes matched to N=208, KV for 262656 tokens, prefill chunk 512, single runs): TTFT 54.85 / 720.58 / 1441.27 / 2884.37 s, i.e. prefill 75 / 91 / 91 / 91 tok/s, decode 39.4 / 67.3 / 71.6 / 38.3 tok/s, 5/5 at every length. Up to 128K the pool's TTFT is shorter; at 261632 it is 17.7 % longer. Per-chunk cost is the candidate reading (both engines move expert bytes over PCIe per 512-token chunk); no transfer volume was measured and the cause is not isolated. Not tested on either engine: long inputs combined with long outputs.
Capacity
29 startup + 243 in-flight samples with no errors under the 48 GiB limit, minimum free 4449 / 4295 MiB, final exit 0, container OOMKilled=false. Retained observations: one allocator OOM retry during startup, and an EngineDeadError with a held semaphore after the external shutdown.
🤖 Generated with Claude Code
https://claude.ai/code/session_016QWXP5rMj1rGh9xasXNLyT