Skip to content

feat(dcp, flashinfer): support FULL_DECODE_ONLY for flashinfer with DCP>1 - #36503

Open
pisceskkk wants to merge 6 commits into
vllm-project:mainfrom
pisceskkk:dcp/fdo
Open

feat(dcp, flashinfer): support FULL_DECODE_ONLY for flashinfer with DCP>1#36503
pisceskkk wants to merge 6 commits into
vllm-project:mainfrom
pisceskkk:dcp/fdo

Conversation

@pisceskkk

@pisceskkk pisceskkk commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Purpose

Allocate persistent buffers for DCP-related variables to enable FULL_DECODE_ONLY support.

Test Plan

bash .buildkite/lm-eval-harness/run-lm-eval-gsm-vllm-baseline.sh -m Qwen/Qwen2.5-1.5B-Instruct/ -b "auto" -l 1000 -f 5 -t 4

Test Result

TP4DCP1

vllm ({'pretrained': 'Qwen/Qwen2.5-1.5B-Instruct', 'tensor_parallel_size': 4, 'add_bos_token': True, 'max_model_len': 4096, 'decode_context_parallel_size': 1, 'attention_backend': 'FLASHINFER'}), gen_kwargs: ({}), limit: 1000.0, num_fewshot: 5, batch_size: auto
|Tasks|Version|     Filter     |n-shot|  Metric   |   |Value|   |Stderr|
|-----|------:|----------------|-----:|-----------|---|----:|---|-----:|
|gsm8k|      3|flexible-extract|     5|exact_match|↑  |0.606|±  |0.0155|
|     |       |strict-match    |     5|exact_match|↑  |0.552|±  |0.0157|

TP4DCP2(4ba38c8)

vllm ({'pretrained': '/data/weight/Qwen2.5-1.5B-Instruct', 'tensor_parallel_size': 4, 'decode_context_parallel_size': 2, 'prefill_context_parallel_size': 1, 'add_bos_token': True, 'max_model_len': 4096, 'attention_backend': 'FLASHINFER'}), gen_kwargs: ({}), limit: None, num_fewshot: 5, batch_size: 512
|Tasks|Version|     Filter     |n-shot|  Metric   |   |Value |   |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k|      3|flexible-extract|     5|exact_match|_  |0.5914|_  |0.0135|
|     |       |strict-match    |     5|exact_match|_  |0.5368|_  |0.0137|

Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.
  • (Optional) Release notes update. If your change is user facing, please update the release notes draft in the Google Doc.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces support for FULL_DECODE_ONLY in FlashInfer with DCP by allocating persistent buffers for DCP-related variables. The changes involve initializing these buffers in the FlashInferBackend and using them in the forward pass to avoid on-the-fly memory allocations. While the buffer management logic appears sound, I've identified a critical issue in vllm/v1/attention/backends/flashinfer.py where a variable is calculated and then immediately overwritten with a different, simpler logic. This is not only confusing and inefficient but also potentially a bug as the new calculation ignores DCP-specific parameters, which could lead to incorrect behavior. I've recommended refactoring this part to make the logic explicit and correct.

Comment thread vllm/v1/attention/backends/flashinfer.py Outdated
@pisceskkk
pisceskkk force-pushed the dcp/fdo branch 2 times, most recently from 73b42dc to aab57d7 Compare March 9, 2026 13:50
@pisceskkk
pisceskkk marked this pull request as ready for review March 9, 2026 13:50
@ehfd

ehfd commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

@cjackal This is the fix for DCP on Qwen3 and Qwen3.5, could you test together with #36480? (Cherry-picking both)

@cjackal

cjackal commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

@cjackal This is the fix for DCP on Qwen3 and Qwen3.5, could you test together with #36480? (Cherry-picking both)

I think #36070 (flashattn counterpart of this PR) is relevant as well for SM90. It seems to work for short inputs (like gsm8k), but for long inputs I still see infinite repetition / gibberish.

@pisceskkk

pisceskkk commented Mar 16, 2026

Copy link
Copy Markdown
Contributor Author

@cjackal This is the fix for DCP on Qwen3 and Qwen3.5, could you test together with #36480? (Cherry-picking both)

I think #36070 (flashattn counterpart of this PR) is relevant as well for SM90. It seems to work for short inputs (like gsm8k), but for long inputs I still see infinite repetition / gibberish.

Sorry for the late reply. I initially suspected there might be an issue with chunked prefill, but testing with chunked prefill didn’t show noticeable drop in accuracy. Could you provide a reproducible script and output samples?

test results with long_prefill_token_threshold=49:

vllm ({'pretrained': 'qwen/Qwen2.5-1.5B-Instruct', 'tensor_parallel_size': 4, 'decode_context_parallel_size': 2, 'long_prefill_token_threshold': 49, 'add_bos_token': True, 'max_model_len': 4096, 'attention_backend': 'FLASHINFER'}), gen_kwargs: ({}), limit: None, num_fewshot: 5, batch_size: 512
|Tasks|Version|     Filter     |n-shot|  Metric   |   |Value |   |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k|      3|flexible-extract|     5|exact_match|↑  |0.5762|±  |0.0136|
|     |       |strict-match    |     5|exact_match|↑  |0.5527|±  |0.0137|

There is indeed a slight precision drop, but it appears to be an edge case. Looking into it.

@cjackal

cjackal commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

Sorry for the late reply. I initially suspected there might be an issue with chunked prefill, but testing with chunked prefill didn’t show noticeable drop in accuracy. Could you provide a reproducible script and output samples?

I think there was a miscommunication - what I've mentioned before is for FA3 as my main computing pool is H100 and the default attention backend for H100 is FA3. I'd like to check the FI backend on H100 in any case, it would be great if DCP is back to functioning well with FI. Will come back with some benchmark results.

@pisceskkk

Copy link
Copy Markdown
Contributor Author

fix some bugs and now the accuracy with 'fully' chunked prefill looks better:

vllm ({'pretrained': 'qwen/Qwen2.5-1.5B-Instruct', 'tensor_parallel_size': 4, 'decode_context_parallel_size': 2, 'long_prefill_token_threshold': 49, 'add_bos_token': True, 'max_model_len': 4096, 'attention_backend': 'FLASHINFER'}), gen_kwargs: ({}), limit: None, num_fewshot: 5, batch_size: 512
|Tasks|Version|     Filter     |n-shot|  Metric   |   |Value |   |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k|      3|flexible-extract|     5|exact_match|↑  |0.5951|±  |0.0135|
|     |       |strict-match    |     5|exact_match|↑  |0.5421|±  |0.0137|

BTW, I noticed FlashInfer's LSE uses base-2, but merge_attn_states always sticks to natural logs. I put in a temporary scaling fix in f10579e.
@LucasWilkinson Do you think it's worth adding a base selection or a scaling factor to merge_attn_states like 5931320
?

@LucasWilkinson LucasWilkinson left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

overall looks pretty good to me! thanks for the contribution! i have a couple questions

if self.use_dcp:
assert seq_lens_cpu is not None
# Keep the shared metadata immutable for mixed decode/prefill batches.
seq_lens_cpu = seq_lens_cpu.clone()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why does it need to be cloned, i see it says it needs to be immutable but im confused where the mutation is coming from, async scheduling?

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.

Looks like that was leftover code from debugging the accuracy issue. I just confirmed that removing it doesn't change anything. The real cause for the accuracy issue was the LSE base value.

Tasks Version Filter n-shot Metric Value Stderr
gsm8k 3 flexible-extract 5 exact_match 0.600 ± 0.0155
strict-match 5 exact_match 0.546 ± 0.0158

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can we remove it then please?

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.

I remember deleting this line of code, but it might still be there due to a rebase error. deleted

(decode_query.size(0), decode_query.size(1)),
dtype=torch.float32,
device=decode_query.device,
decode_query_buffer.copy_(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why is this copy necessary?

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.

These buffers need to be assigned in-place. Do you mean using the decode_query_buffer[:] = get_dcp_group(...) syntax?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i guess i dont understand why we can't use the output of get_dcp_group().all_gather directly?

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.

Ah, yes, I had some misunderstandings about the persistence of tensors in CUDAGraph. This value indeed does not need to be persisted. It has been corrected.

Copilot AI review requested due to automatic review settings April 2, 2026 07:27
@pisceskkk
pisceskkk requested a review from vadiklyutiy as a code owner April 2, 2026 07:27

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.

Pull request overview

Adds persistent buffer allocation to enable FULL_DECODE_ONLY when using FlashInfer with decode_context_parallel_size > 1, and adjusts LSE handling for downstream attention-state merging.

Changes:

  • Allocate reusable decode buffers via the workspace manager for DCP all-gathered queries / outputs / LSE.
  • Convert FlashInfer LSE to the expected log base for merge_attn_states.
  • Update DCP/CP interleave sizing and adjust how CPU seq-lens are materialized for DCP/cascade paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

logger = init_logger(__name__)

trtllm_gen_workspace_buffer = None
LOG2_E_SCALE = math.log(2.0)

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

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

LOG2_E_SCALE is ambiguous/misleading: math.log(2.0) is ln(2), which is specifically a base-conversion factor (log2 -> ln). Consider renaming to something unambiguous like LN_2 or LOG2_TO_LN to make the intent (and direction) clear and avoid future mistakes.

Copilot uses AI. Check for mistakes.
Comment thread vllm/v1/attention/backends/flashinfer.py Outdated
Comment thread vllm/v1/attention/backends/flashinfer.py
self.dcp_world_size = 1
self.dcp_rank = 0
self.dcp_kv_cache_interleave_size = 1
self.cp_kv_cache_interleave_size = 1

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

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

The naming switches from dcp_* to cp_* while the surrounding fields remain dcp_world_size, dcp_rank, use_dcp, etc. This inconsistency makes it harder to reason about whether the interleave size is tied to DCP specifically or a broader CP concept. Either align the field name with the DCP terminology used in this class, or add a short clarifying comment explaining why the config uses cp_* while the runtime concept is DCP.

Copilot uses AI. Check for mistakes.
Comment thread vllm/v1/attention/backends/flashinfer.py
Comment thread vllm/v1/attention/backends/flashinfer.py Outdated
Signed-off-by: QiuChunshuo <qiuchunshuo@huawei.com>
if seq_lens_np is not None
else None
seq_lens_cpu = (
common_attn_metadata.seq_lens.cpu() if needs_seq_lens_cpu else None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why is this change needed? if its just refactoring I think we should break that into a separate PR

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.

I moved seq_lens_np to the bottom because the code snippet below might modify seq_lens_cpu, so I need to assign a value to seq_lens_np after that. However, this modification really shouldn't be included in this PR. I'll open a new PR to complete this modification.

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.

Open a new PR for this modifcation: #42205

Signed-off-by: QiuChunshuo <qiuchunshuo@huawei.com>
@pisceskkk
pisceskkk force-pushed the dcp/fdo branch 2 times, most recently from fc81fb2 to 7b65dea Compare May 10, 2026 05:05
Signed-off-by: QiuChunshuo <qiuchunshuo@huawei.com>
@mergify

mergify Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Hi @pisceskkk, the pre-commit checks have failed. Please run:

uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-files

Then, commit the changes and push to your branch.

For future commits, pre-commit will run automatically on changed files before each commit.

Tip

Is mypy failing?
mypy is run differently in CI. If the failure is related to this check, please use the following command to run it locally:
# For mypy (substitute "3.10" with the failing version if needed)
pre-commit run --hook-stage manual mypy-3.10

Signed-off-by: QiuChunshuo <qiuchunshuo@huawei.com>
@mergify

mergify Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Hi @pisceskkk, the pre-commit checks have failed. Please run:

uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-files

Then, commit the changes and push to your branch.

For future commits, pre-commit will run automatically on changed files before each commit.

Tip

Is mypy failing?
mypy is run differently in CI. If the failure is related to this check, please use the following command to run it locally:
# For mypy (substitute "3.10" with the failing version if needed)
pre-commit run --hook-stage manual mypy-3.10

if self.sinks is not None and self.sinks.dtype != torch.float32:
self.sinks = self.sinks.to(torch.float32)

def _get_dcp_decode_buffers(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can we just inline this?

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.

yes, have changed.

num_decode_tokens: int,
) -> tuple[torch.Tensor, torch.Tensor]:
assert self.dcp_world_size > 1
total_num_heads = self.num_heads * self.dcp_world_size

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is this always true? what about say TP=8 and DCP=2; our is this not truely total_num_heads just total_num_heads within DCP group?

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.

I think this is correct, right? The output should always maintain the same shape as decode_query.

https://github.com/pisceskkk/vllm/blob/13672b4b6fa59535377528338947a6ee9395c575/vllm/v1/attention/backends/flashinfer.py#L1742-L1744

decode_query = get_dcp_group().all_gather(
    decode_query.contiguous(), dim=-2
)

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.

our is this not truely total_num_heads just total_num_heads within DCP group?

Sorry, I misunderstood the meaning. This variable indeed should not be called "total_num_heads." I have changed the name to ensure it aligns with its intended meaning.

pisceskkk added 2 commits May 12, 2026 11:54
Signed-off-by: QiuChunshuo <qiuchunshuo@huawei.com>
Signed-off-by: QiuChunshuo <qiuchunshuo@huawei.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants