Skip to content

[Model] DeepSeek-V4.1-Flash on 4x V100: 512K packed KV, faster prefill, API launch - #1277

Merged
yangzhuxinyzx merged 4 commits into
mainfrom
ds41-flash-v100-512k
Oct 27, 2026
Merged

yangzhuxinyzx merged 4 commits into
mainfrom
ds41-flash-v100-512k

Conversation

@yangzhuxinyzx

Copy link
Copy Markdown
Contributor

Summary

This PR runs DeepSeek-V4.1-Flash-1017 on 4× V100-32G (54633) with expert offloading:

  • GSQ-RCO IQ2_XXS GGUF (138 GB)
  • Engram IQ4_NL (110.6 GB)
  • DSpark5 draft

The hardware is TP4 behind a single x16 PCIe uplink at 12.2 GB/s, with 62 GB host RAM.

It adds the phase 2–3 work: prefill goals G1–G7, 512K context through lossless packed FP4 KV kept in host memory, and an OpenAI API launch with prefix cache, tool calls and thinking. Every new path has its own switch. The defaults are lossless.

The full handoff is benchmarks/deepseek_v41/HANDOFF.md. It covers launch, switches, results, dead ends and open items.

Changes

G1: activation memory

  • MoE down output goes to a per-tile scratch plus an FP32 accumulator.
  • expand_hc and large all-reduces run in place.
  • hc_transition is windowed.
  • all_gather goes through pynccl.
  • empty_cache runs before big passes.
  • Chunks go up to 16K.

G2: cold-expert staging ring

  • Staging is issued ahead across tiles and layers.
  • The host never blocks on a pinned row.

G3: routed down projection split by output rows (VLLM_V41_DOWN_ROWS)

  • Removes the duplicated TP boundary blocks: −2.25 GiB per GPU and about 10% fewer PCIe bytes.
  • Decode uses an IPC peer_gather kernel in v41k.

G4: long context

  • Layers ≥ 1 keep their sliding window in 256-row rings.
  • Packed KV (VLLM_V41_PACKED_KV):
    • Compressed latents use FP4 + E4M3 scales. They are stored once in a shared host memfd, plus a 32K GPU hot ring.
    • Index keys use FP4 + power-of-two scales and stay on the GPU.
    • Decode index scores run in a CUDA kernel, followed by a segment-parallel exact top-k.
  • RoPE rotates on the fly past its table.
  • The C2, Engram, guard and DSpark draft length limits now follow the target length.

G5: prefill compute

  • 64 Engram reader threads.
  • Adaptive indexer tile.
  • Persistent-grid activation kernel.

G6: short prompts

  • Prompts up to 1536 tokens stage only their routed experts.

G7: same-session prefix cache (VLLM_V41_SESSION_PREFIX)

  • The scheduler accepts a hit only when the request continues the last executed sequence.
  • The worker restore_prefix rewinds the Engram and C2 state.

Tooling (all under benchmarks/deepseek_v41/)

  • v41k extension source plus build.py.
  • serve-ds41-phase{2,3}.yaml.
  • tools/run_serve{2,3}.sh and the validation tools.
  • GPU micro-tests.

Configuration

  • About 90 V4.1 switches are registered in vllm/envs.py. The generic modules read them through vllm.envs, so the layering ratchet does not grow.
  • deepseek_v41_activations moved into the model package.
  • Two boundary entries were added to tools/config_boundaries.py.

Launch

.venv/bin/python benchmarks/deepseek_v41/v41k/build.py            # once, inside ds41-compute
docker exec -d ds41-compute-20261024 bash benchmarks/deepseek_v41/tools/run_serve3.sh serve3
python3 benchmarks/deepseek_v41/tools/tcp_forward.py 8000 <container-ip> 18000   # 1cat-tunnel DFLASH2-API

Results (vs phase 1)

Phase 1 This PR
TTFT short / 8K / 15K 5.6 / 27.7 / 44.0 s 0.8–1.1 / 5.8 / 11.1 s
TTFT 128K / 500K (packed) n/a (16K max) 110 / 651 s, needle passes
Follow-up turn (prefix hit) full prefill 0.9–1.7 s
Fresh-text decode 57.8 ms/round 53.5 ms (55.0 packed + 512K)
Steady VRAM 28.6 GiB 26.9 GiB

With packed KV, tokens at ≤16K are identical to the paged FP16 cache.

Test plan

  • GPU micro-tests in benchmarks/deepseek_v41/tests/: kv_pack, packed attention, packed indexer, parallel top-k, window ring, G3 down, peer gather, G1 memory, RoPE tail. Each is bit-exact or equivalent.
  • e2e 5/5 including the 16K check.
  • Needle retrieval at 4K, 32K, 128K and 500K.
  • API soak 80/80.
  • Thinking, tool calls and multi-turn through the public API.
  • Local hooks pass:
    • ruff check and format
    • SPDX
    • torch.cuda
    • forbidden imports
    • env registration and metadata
    • env reference
    • config inventory
    • layering
    • validate-config
    • lazy imports
  • The API server was restarted on this exact tree and smoke-tested.
  • Not run locally: clang-format, typos and mypy. The tools are not installed on 54633, and downloading them was avoided to save traffic.
  • check-layering reports moe_runner.py platform coupling 37 → 39. This was already on main before this PR.

🤖 Generated with Claude Code

yangzhuxinyzx and others added 4 commits October 27, 2026 22:43
…l, API launch

Runs DeepSeek-V4.1-Flash-1017 (GSQ-RCO IQ2_XXS GGUF + Engram IQ4_NL +
DSpark5 draft) on 4x V100-32G (TP4, one x16 PCIe uplink, 62 GB host RAM)
with expert offloading. All new paths are lossless and switchable.

Prefill (G1-G7):
- G1 activation memory: per-tile MoE scratch + FP32 accumulator, in-place
  expand_hc / large all-reduce, windowed hc_transition, pynccl all_gather,
  empty_cache before big passes; chunks up to 16K.
- G2 cold-expert staging ring issued ahead across tiles and layers.
- G3 routed down projection split by output rows (no duplicated TP
  boundary blocks, -2.25 GiB/GPU) with an IPC peer gather for decode.
- G5 Engram readers, adaptive indexer tile, persistent activation grid.
- G6 routed-only staging for short prompts.
- G7 same-session prefix cache (scheduler guard + worker restore_prefix).

Long context (G4):
- Window layers keep 256-row private rings.
- Packed KV (VLLM_V41_PACKED_KV): FP4 + E4M3 compressed latents in a
  shared host memfd plus a 32K GPU hot ring, FP4 index keys on GPU, CUDA
  decode index scores and segment-parallel exact top-k. Enables 512K.
- RoPE rotates on the fly past its table; C2/Engram/guard limits and
  the DSpark draft length cap are lifted to the target length.

Results vs phase 1: TTFT short 5.6 -> 0.8-1.1 s, 8K 27.7 -> 5.8 s,
15K 44.0 -> 11.1 s; 128K 110 s and 500K 651 s (needle passes); follow-up
turns hit the prefix cache in ~1-1.7 s; fresh-text decode 57.8 -> 53.5
ms/round. Tokens at <=16K are identical with packed KV.

Tooling: benchmarks/deepseek_v41/ gains the v41k SM70 extension source
and build script, serve configs (phase 2: 16K, phase 3: 512K + prefix
cache + tools + thinking), launch/validation tools, GPU micro-tests and
HANDOFF.md. V4.1 switches are registered in vllm/envs.py and read through
vllm.envs.

Signed-off-by: yangzhuxinyzx <153831768+yangzhuxinyzx@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apply the CI hook output for the new csrc/v41k sources and HANDOFF.md,
rename kend -> k_end, type the V4.1 env switches that have defaults as str
and add annotations / targeted type-ignore codes for the remaining mypy
findings. No runtime change.

Signed-off-by: yangzhuxinyzx <153831768+yangzhuxinyzx@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clang-format 21 is not idempotent on the brace-less `if (...) MACRO(...)
else ...` launch chains; give them explicit braces (same expansion) and
fence them with clang-format off/on.

Signed-off-by: yangzhuxinyzx <153831768+yangzhuxinyzx@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: yangzhuxinyzx <153831768+yangzhuxinyzx@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yangzhuxinyzx
yangzhuxinyzx merged commit 899e9ed into main Oct 27, 2026
2 of 3 checks passed
yangzhuxinyzx added a commit that referenced this pull request Oct 28, 2026
* [Bugfix] Keep the pynccl all-reduce out-of-place by default

#1277 made pynccl reduce every all-reduce of 4194304 or more elements in
place. The vllm.all_reduce op is registered as out-of-place, so compiled
graphs may read its input again after the call; reducing in place hands
those readers the summed tensor instead of the rank's own partial.

Qwen3.8-27B NVFP4 + DFlash2 on TP4 V100 crosses the threshold on every
prefill of about 820 tokens or more. Greedy target output is unchanged
(identical tokens on all eight 1K/8K fixtures, identical logprobs at 1K),
but the draft context changes: speed-fixture tokens per round fell from
2.875/2.974 to 2.655/2.613 at 1K/8K.

Unset VLLM_INPLACE_AR_MIN now means out-of-place, except with
VLLM_V41_PACKED_KV=1, where the DeepSeek-V4.1 packed-KV prefill keeps the
in-place reduction it was qualified with. An explicit value still wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Bugfix] Resolve the in-place all-reduce default in envs.py

Keep the communicator free of env and model coupling (check-layering):
envs.py now resolves the default, so the communicator reads
envs.VLLM_INPLACE_AR_MIN. Unset still means out-of-place, except with
VLLM_V41_PACKED_KV=1.

Also drop the model names that #1265 added to two comments in
SpeculativeConfig, which pushed its model coupling over the baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Doc] Regenerate the architecture runtime reference

#1265 added five lines to vllm/config/speculative.py, which moved the
SpeculativeConfig line anchors in runtime_reference.md and left
check-architecture-reference failing on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Peuqui added a commit to Peuqui/1Cat-vLLM that referenced this pull request Oct 28, 2026
…ather

The batched-matmul sparse MLA attention ran six elementwise torch ops
around its two BLAS calls: a broadcast masked_fill over the gathered
keys, then scale, copy into the sink-padded logits, mask, sink column,
softmax and the fp16 cast of the probabilities. On a 128-token pass with
64 heads and 640 keys they took 820 of 1434 us on RTX 8000 in the
per-stage nsys profile of the DSv4 PP5 entry (150k-token prefill); the
masked_fill alone, a non-vectorized broadcast kernel, took 395 us, twice
the gather.

Now the gather writes zeros for unused slots and never reads their indices
(a Triton row gather for prefill; the decode dequantizing gather takes the
same mask), and one Triton kernel reads the fp32 scores once and writes fp16
probabilities: scale, mask, sink column and the fp32 softmax with libdevice
exp, the same expf torch's softmax uses. The fp32 logits and probs buffers
go away (140 -> 110 MiB per 128-token pass at 640 keys).

DeepSeek-V4.1 (1CatAI#1277) calls attend_gathered_keys as well. Its two callers in
vllm/models/deepseek_v41/attention_ops.py now zero the unused slots
themselves, since their gathers leave them as read, and pass the
probabilities as [T, H, W] in the key dtype. Against main's function on
random data with NaN in the unused slots, 99.69-100 % of their outputs are
bit-identical and the rest differ by one fp16 step (at most 2.4e-4), on a
Tesla V100 and a Quadro RTX 8000.

Old against new in CUDA graphs (us per call, 10 calls per graph):
  prefill pass T128 H64 W640:  RTX 8000 1438.3 -> 755.7, V100 1178.7 -> 632.0
  prefill pass T128 H64 W2176: RTX 8000 4666.8 -> 2367.7, V100 3760.3 -> 1983.5
  decode T6 H64 C4 (128 + 512 keys): V100 146.5 -> 96.5
  decode T6 H64 SWA only (128 keys): V100 55.1 -> 36.6
Error against a float64 reference is unchanged (max and mean equal to three
digits); 99.2-99.8 % of the outputs are bit-identical, the rest differ by at
most 3e-5 (softmax reduction order).

Tests: tests/kernels/attention/test_dsv4_sparse_prefill_bmm.py and
test_dsv4_sm70_sparse_bmm.py 29 passed on V100; on RTX 8000 17 passed, 12
skipped (the decode reference kernel is Volta-only); pre-commit and
mypy-3.10 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YaG8VjXn5eN3Aq7eZrA3dp
Signed-off-by: Peuqui <43776522+Peuqui@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.

1 participant