Skip to content

fix(plugin/dsv4): score 1-token prefill rows on the dense indexer path - #2501

Open
linsun12 wants to merge 1 commit into
mainfrom
linsun12/up-dsv4-1token-indexer
Open

linsun12 wants to merge 1 commit into
mainfrom
linsun12/up-dsv4-1token-indexer

Conversation

@linsun12

@linsun12 linsun12 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

This fixes a GPU memory access fault in the vLLM plugin's DeepSeek-V4 path on any prefill step whose rows are all one token long.

Problem

vLLM + ATOM plugin, DeepSeek-V4 (DeepseekV4ForCausalLM), prefix caching on, also with --enforce-eager: the engine dies with Memory access fault by GPU node-… on all TP ranks, then EngineDeadError.

It happens whenever the last prompt chunk of a request is a single token:

  • a prompt of 128·m + 1 tokens sent again: the block-aligned prefix hit leaves one token to compute (129, 897, 1025, 1153, 2049, 8193, … tokens);
  • a fresh prompt of 8192·k + 1 tokens with --max-num-batched-tokens 8192: its last chunk is one token;
  • a fresh 1-token prompt.

Repro on main (a0527c9db): send a 1025-token prompt, then the same prompt again. The first request is answered; the second returns HTTP 500 and the server log shows

Memory access fault by GPU node-3 (Agent handle: ...) on address 0xffff59bb2000. Reason: Unknown.
... (all 8 ranks)
vllm.v1.engine.exceptions.EngineDeadError: EngineCore encountered an issue.

Root cause

The bridge builds such a step as a prefill. build_atom_v4_attention_metadata then split the indexer rows by length only: the leading run of rows with the first row's length, if <= decode_q, went to the paged decode indexer. When every row is one token long, that is every row, so the dense prefill group was empty — but _populate_indexer's cu[-1] = max(cu[-1], 1) still claimed one committed row. IndexerVllm.indexer_score_topk then ran _score_topk_prefill on the empty slice and gathered one row through an empty block table: an out-of-bounds read.

Fix

  • _indexer_decode_group(lens, decode_q, is_prefilling): the paged decode group also ends at the first row that is still prefilling. Such a row takes the dense prefill path, as in native ATOM, so the dense group is not empty on these steps. Batches without a prefilling row split exactly as before. (is_prefilling is vLLM's CPU num_computed < num_prompt mask, so this adds no sync.)
  • IndexerVllm.indexer_score_topk: if no prefill rows are left, return the decode result instead of running the dense path on an empty slice.

Tests

  • New CPU test tests/plugin/test_vllm_deepseek_v4_indexer_split.py: a prefilling row of decode length ends the decode group; no dense call when no prefill rows are left; mixed batches keep their split.
  • tests/plugin (without sglang): same failures as main (63, unrelated: rtpllm, offload, kv-cache layout, proxy arena layout), plus the new tests passing.
  • GPU, rocm/atom-dev:vllm-v0.31.0-nightly_20261012, DeepSeek-V4-Pro-0813, TP8, fp8 KV, prefix caching on, --max-num-batched-tokens 8192:
main a0527c9db this PR
1025-token prompt sent twice (--enforce-eager) 2nd request: GPU memory access fault, EngineDeadError answered (1024 tokens from cache)
shape probe: 20 prompt lengths (1, 2, 3, 127–129, 896–898, 1000, 1024, 1025, 1153, 2049, 3000, 8193, 8194, 8200, 8320, 8321) × first send / re-send / fresh, two texts = 120 requests — --enforce-eager: 120 / 120 answered, no fault; default cudagraph: 120 / 120 answered, no fault
GSM8K 20-shot, default cudagraph (main: CI config; this PR: with prefix caching) 0.956 ± 0.0056 0.9545 ± 0.0057

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every eligible PR before approval:

  • ✅ Pre Checkin: Black, Ruff, catalog schema validation, non-GPU unit tests

Heavy model tests:

  • ✅ Run after the PR is approved and Pre Checkin passes
  • ✅ Run immediately when an approval review is submitted
  • ✅ Can be requested before approval with labels
Label Tests
ci:full Run all heavy PR model tests: native ATOM, vLLM, and SGLang
ci:atom Run native ATOM model accuracy tests
ci:vllm Run ATOM vLLM OOT model accuracy tests
ci:sglang Run ATOM SGLang model accuracy tests

Heavy jobs are skipped when the PR is not approved and no matching ci:* label is present.
Add labels via the sidebar or gh pr edit 2501 --add-label <label>

PerryZhang01 pushed a commit that referenced this pull request Oct 12, 2026
…ould see

#2501 and #2502 report, for DeepseekV4ForCausalLM: a GPU memory access fault
on any prefill step whose rows are all one token (reached by a `block*m + 1`
prompt sent twice, since the block-aligned hit leaves a 1-token tail), and
wrong output -- not a crash -- for prefill batches up to the largest capture
size. #2502's root cause does not transfer: V4 relies on ATOM's fx split at
its attention op and breakable cudagraph replaces it, while V4.1 does no fx
splitting and breakable is the only way it gets graphs at all. The SHAPES
still had to be checked, and this line had never sent one.

Neither class reproduces.

The fault: the trigger was reached, not just the shape. Six shapes produced
an uncached tail of exactly one token on their second send (257, 513, 1025,
2049, 8193 and max_batched+1, all with `cached = tokens - 1` read off the hit
counters), under FULL_AND_PIECEWISE with prefix caching on. Zero memory
access faults, zero engine deaths. A shape that took no hit (129, shorter
than a block) is reported as NOT REACHED rather than as passing.

The wrong output: with caching off on both arms and concurrency 1, every
shape's eager-vs-graph teacher-forced logprob deviation sits at or below that
shape's own noise floor -- ratios 0.40 to 1.12 across 14 shapes, including
the 1-token tails, the window-aligned shapes that are not block multiples,
and a block-aligned control.

Two criteria had to be discarded first, and the control is what discarded
them:

  Sampled text. 13 of 17 shapes had the EAGER arm differing from ITSELF
  between two sends, and the control shape -- implicated by neither PR --
  differed too. The oracle was not stable, so identity could not be the test.

  Logprobs across a cache hit. Two identical requests to one server with
  caching on agreed on 1 of 161 positions, deviating by up to 1.71.

And a sizing defect worth more than either: the chat endpoint's template adds
~88 tokens, so the first attempt's `block*m + 1` shapes were never sent at
all -- it measured 341 tokens while printing "257". The probes now build
prompts through `/v1/completions`, which applies no template, and verify the
count against `usage.prompt_tokens`; a shape that cannot be hit exactly is
reported MISSIZED. One target, a literal 1-token prompt, is MISSIZED and
remains untested.

What this could have seen is bounded by a thing it measured on the way: the
engine is not deterministic at concurrency 1 with caching off and temperature
0. Re-sending one prompt to one arm moves its logprobs by up to 7.6 nats, and
the per-shape mean floor runs from 3.4 nats at two tokens to 5e-3 at 2,048. A
defect smaller than that floor is invisible here. That nondeterminism is
measured and unexplained; it is not a finding about the shapes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DeepSeek-V4 on the vLLM plugin crashes with "Memory access fault by GPU"
(then EngineDeadError) on a prefill step whose rows are all one token long.
It happens with --enforce-eager too; it is not a cudagraph issue. Such a step
comes up whenever the last prompt chunk of a request is a single token:

- a prompt of 128*m + 1 tokens sent again: vLLM's block-aligned prefix hit
  leaves one token to compute (897, 1025, 1153, 2049, 8193, ... tokens);
- a fresh prompt of 8192*k + 1 tokens with --max-num-batched-tokens 8192:
  its last chunk is one token;
- a fresh 1-token prompt.

Root cause: the bridge correctly builds the step as a prefill
(is_prefilling), but its indexer split put every leading row of decode
length into the paged decode group. The dense prefill group was then empty
while _populate_indexer's cu[-1] = max(cu[-1], 1) still claimed one
committed row, so IndexerVllm.indexer_score_topk ran _score_topk_prefill on
an empty slice and cp_gather_indexer_k_quant_cache gathered through an empty
block table (out-of-bounds read).

Native ATOM does not crash on this shape: its scheduler runs the 1-token
chunk as its own prefill batch and the core indexer scores the row on
_score_topk_prefill with its own committed K. Do the same here:
_indexer_decode_group ends the paged decode group at the first row that is
still prefilling, as a longer row does, so such a row takes the dense
prefill path and the dense group is never empty on a prefill-classified
step. indexer_score_topk keeps a fallback that skips the dense path on an
empty slice.

Test: tests/plugin/test_vllm_deepseek_v4_indexer_split.py (CPU).

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: linsun12 <linsun12@amd.com>
@linsun12
linsun12 force-pushed the linsun12/up-dsv4-1token-indexer branch from db7e538 to 92a9ab6 Compare October 12, 2026 20:24
@linsun12
linsun12 marked this pull request as ready for review October 12, 2026 20:28
Copilot AI balanced review requested due to automatic review settings October 12, 2026 20:28

Copilot AI 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.

🟡 Changes recommended

The new regression tests are excluded from automated unit-test CI and additionally skip without AITER.

1 open finding
What changed in this PR

Fixes DeepSeek-V4 vLLM indexer routing for one-token prefill rows, preventing out-of-bounds GPU memory access.

Changes:

  • Routes prefilling rows through the dense indexer.
  • Skips dense scoring when no prefill rows remain.
  • Adds split and dispatch regression tests.
File Description
atom/​plugin/​vllm/​deepseek_v4_bridge.py Uses prefill state when splitting indexer rows.
atom/​plugin/​vllm/​models/​deepseek_v4.py Avoids dense scoring on empty prefill slices.
tests/​plugin/​test_vllm_deepseek_v4_indexer_split.py Tests one-token and mixed-batch routing.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

import pytest
import torch

pytest.importorskip("aiter", reason="the V4 model module reaches AITER ops")

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants