Skip to content

[Model][Qwen4Exp] Split the pinned-host PLE table between device and host (#479) - #528

Merged
yangzhuxinyzx merged 3 commits into
1CatAI:mainfrom
Peuqui:qwen4exp-ple-split-placement
Sep 7, 2026
Merged

yangzhuxinyzx merged 3 commits into
1CatAI:mainfrom
Peuqui:qwen4exp-ple-split-placement

Conversation

@Peuqui

@Peuqui Peuqui commented Sep 6, 2026

Copy link
Copy Markdown

Purpose

Third slice of the PP enablement offered in #479 (loader fix: #485, PLE gate on the partition: #516), and the placement we run Qwen3.8 Flash Next with.

The pinned-host PLE path (#345, #374) keeps the whole FP8 n-gram shard in pinned host memory and gathers every row over UVA. Where the card has room beside its layers that is the slow option, and on a box with little host RAM it does not boot at all: the Flash Next table is 51 GB, a TP2 rank pins 25 GB. #471 answers the memory side with a disk-backed table.

This PR adds the option in between: rows stay in device memory as long as the stage's weights, the KV cache of the requested max_model_len and a reserve for the activation peak and the graph pool still fit; only the remainder is pinned on the host. The budget is derived from the real headroom when the first checkpoint shard arrives (every other weight of the stage is placed by then), or fixed per rank with VLLM_QWEN4EXP_PLE_HOST_GIB. Both halves are gathered by the existing qwen4_exp_ple_pinned_gather kernel; a row-less half is skipped without a device-to-host sync. The loader routes each shard into the half that owns its rows. Qwen4ExpPinnedHostEmbedding therefore starts with a row-less CPU placeholder and materializes its tables lazily (first shard, or prepare_accelerator_weight() for dummy weights).

Second commit: _should_use_pinned_host_ple() asked for the capability of device 0 of the visible list. On a heterogeneous pipeline the stage that owns the PLE layers is not necessarily that device. It now asks for the worker's own device, and every pre-Ampere card takes the split placement (the generic path dequantizes the whole FP8 table to fp16 inside the compiled graph, 47.7 GiB for Flash Next).

This is how we serve Qwen3.8 Flash Next NVFP4 at TP2/PP2 on 2x RTX 8000 + 2x V100 with VLLM_QWEN4EXP_PLE_HOST_GIB=6: 51.9 tok/s with MTP k=4, coherence 8/8 (measured 2026-08-28 on our fork, the same placement is in our 1.5.0 deployment). We cannot boot current main on this hardware, so the end-to-end evidence is from the fork; the unit tests below run against this branch.

Known limits: the automatic budget estimates the activation peak and the graph pool through a reserve (8 % of the device, at most 4 GiB, VLLM_QWEN4EXP_PLE_VRAM_RESERVE_GIB); the explicit host budget is the safe choice. The gather always reads both halves.

Test Plan

Wheel venv (1Cat 1.5.0) with PYTHONPATH on this checkout, V100 (SM70):

pytest tests/models/qwen4_exp/test_ple.py
  34 passed (3 of them exact-SM70: split gather with 0, 4 and 8 host rows)
Negative proof: pre-change ple_layer.py with the new tests
  6 failed (the six pinned-host tests), 24 passed
pytest tests/models/qwen4_exp/ tests/v1/worker/test_ple_offload_worker.py tests/v1/spec_decode/test_qwen4_exp.py --ignore=tests/models/qwen4_exp/test_qsa_amd.py
  234 passed, 3 failed -- the three (test_qsa_reference.py side/circular/compressed metadata)
  fail identically on unmodified 755baae; test_qsa_amd.py cannot be collected in the same
  process as the nvidia tree (duplicate custom-op registration), also on 755baae
pre-commit run --files <4 files>          all hooks passed (incl. check-torch-cuda-call, mypy-local)
pre-commit run mypy-3.10 --hook-stage manual --files <4 files>   passed

Duplicate check / AI assistance

gh pr list --state all --search "PLE host" / "pinned host PLE" / "PLE placement" / "PLE_HOST": #345, #374 (whole shard in pinned host), #471 (disk-backed table) -- all merged, none splits the table between device and host; no open PR touches this path. gh issue view 479 --comments: the third-slice offer is in the issue.

Written with AI assistance (Claude); every line was reviewed and the tests were run by me on 2x RTX 8000 + 3x V100.

🤖 Generated with Claude Code

Peuqui and others added 2 commits September 6, 2026 12:02
…host

The pinned-host PLE path keeps the whole FP8 n-gram shard in pinned host
memory and gathers every row over UVA. On a card that has room beside
its layers that is the slow option, and on a box with little host RAM
it does not boot at all: the Qwen3.8 Flash Next table is 51 GB, a
TP2 rank pins 25 GB.

Place the table by capacity instead: rows stay in device memory as long
as the stage's weights, the KV cache of the requested max_model_len and
a reserve for the activation peak and the graph pool still fit; only the
remainder goes to pinned host memory. The budget is derived from the
real headroom when the first checkpoint shard arrives (every other
weight of the stage is placed by then), or fixed per rank with
VLLM_QWEN4EXP_PLE_HOST_GIB. Both halves are gathered by the existing
pinned-gather kernel; a row-less half is skipped without a device-to-
host sync. The loader routes each shard into the half that owns its
rows.

This is how we serve Qwen3.8 Flash Next NVFP4 at TP2/PP2 on 2x RTX 8000
+ 2x V100 with VLLM_QWEN4EXP_PLE_HOST_GIB=6 (the rest of each shard on
the 48 GB cards): 51.9 tok/s with MTP k=4, coherence 8/8, measured
2026-08-28 on our fork and carried into our 1.5.0 deployment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Peuqui <peuqui@github.com>
_should_use_pinned_host_ple() asked current_platform for the capability
of device 0 of the visible list. On a heterogeneous pipeline the stage
that owns the PLE layers is not necessarily that device: with a Turing
card first the Volta stage lost its pinned-host path and hit the fp16
materialization of the generic embedding, with a Volta card first a
Turing PLE stage got it although it only qualifies with the split
placement. Ask for the capability of the worker's current device
instead.

The split placement also serves every pre-Ampere card, not only exact
SM70: the generic path dequantizes the whole FP8 table to fp16 inside
the compiled graph (47.7 GiB for Qwen3.8 Flash Next), which neither
Volta nor Turing can hold.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Peuqui <peuqui@github.com>
Every tensor-parallel rank of the stage that owns the PLE table derives its
host budget from MemAvailable, and all of them draw on the same host memory.
Reading it per rank double-books it: on a 30 GB host with 20 GB available,
two ranks each saw room for 7 GiB, pinned 14 GiB together and pushed the
engine processes, the checkpoint loading and everything else into swap. The
machine was unreachable for 40 minutes.

cap_host_budget_bytes bounds a rank's budget by (available - reserve) /
ranks_sharing_host. The reserve is a fraction of the host rather than a
fixed size, so the rule means the same on every machine; the default is a
quarter, overridable with VLLM_QWEN4EXP_PLE_HOST_RESERVE_GIB. What no
longer spills stays on the device, and if the requested context then does
not fit, the KV allocator reports the reachable max_model_len -- host
memory is the hard limit, context the negotiable one.

Measured on the reporting rig: 7.09 -> 3.18 GiB per rank, boot healthy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
yangzhuxinyzx added a commit that referenced this pull request Sep 7, 2026
…907-030709

[Bugfix][PLE] Integrate split placement with padding and loading repairs (#528)
@yangzhuxinyzx
yangzhuxinyzx merged commit 3537738 into 1CatAI:main Sep 7, 2026
2 checks passed
hubutui pushed a commit to hubutui/1Cat-vLLM that referenced this pull request Sep 7, 2026
Integrate 1CatAI#528 safely on current main; validate memory budgets, preserve retryable allocation state, and test graph replay with in-place reload.

Co-authored-by: OpenAI Codex <noreply@openai.com>
Signed-off-by: yangzhuxinyzx <153831768+yangzhuxinyzx@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants