mirror of
https://github.com/vllm-project/vllm.git
synced 2026-08-24 22:50:15 +00:00
[Attention][MLA] Per-request scheduling for MLA chunked context (#50613)
Signed-off-by: Matthew Bonanni <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]> Co-authored-by: OpenAI Codex <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 5
OpenAI Codex
mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent
e7b8d59460
commit
b38e111d3e
@@ -11,9 +11,6 @@ from vllm._custom_ops import (
|
||||
scaled_fp8_quant,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.v1.attention.ops.triton_merge_attn_states import (
|
||||
mask_empty_context,
|
||||
)
|
||||
from vllm.v1.attention.ops.triton_merge_attn_states import (
|
||||
merge_attn_states as merge_attn_states_triton,
|
||||
)
|
||||
@@ -76,32 +73,6 @@ DTYPES = [torch.float32, torch.half, torch.bfloat16]
|
||||
all_case_info: list[tuple] = []
|
||||
|
||||
|
||||
def test_mask_empty_context() -> None:
|
||||
query_lens = torch.tensor([2] + [1] * 31 + [131, 1], dtype=torch.int32)
|
||||
query_start_loc = torch.cat(
|
||||
(torch.zeros(1, dtype=torch.int32), query_lens.cumsum(0))
|
||||
).cuda()
|
||||
context_lens = torch.tensor([4] * 32 + [0, 3], dtype=torch.int32)
|
||||
context_start_loc = torch.cat(
|
||||
(torch.zeros(1, dtype=torch.int32), context_lens.cumsum(0))
|
||||
).cuda()
|
||||
num_heads, num_tokens, head_dim = 4, 165, 16
|
||||
lse = torch.randn(num_heads, num_tokens, device="cuda")
|
||||
output = torch.randn(num_tokens, num_heads, head_dim, device="cuda")
|
||||
# Empty-context rows carry undefined (possibly non-finite) attention output.
|
||||
output[33:164] = float("nan")
|
||||
|
||||
expected_lse = lse.clone()
|
||||
expected_lse[:, 33:164] = float("-inf")
|
||||
expected_output = output.clone()
|
||||
expected_output[33:164] = 0.0
|
||||
|
||||
mask_empty_context(lse, output, query_start_loc, context_start_loc)
|
||||
|
||||
torch.testing.assert_close(lse, expected_lse)
|
||||
torch.testing.assert_close(output, expected_output)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("merge_fn", [merge_attn_states_cuda, merge_attn_states_triton])
|
||||
@pytest.mark.parametrize("output_dtype", [torch.float32, torch.half, torch.bfloat16])
|
||||
def test_merge_attn_states_both_empty(merge_fn, output_dtype) -> None:
|
||||
@@ -117,8 +88,7 @@ def test_merge_attn_states_both_empty(merge_fn, output_dtype) -> None:
|
||||
)
|
||||
suffix_lse = torch.randn(num_heads, num_tokens, device="cuda")
|
||||
|
||||
# Tokens 2 and 3 are empty on both sides (mask_empty_context already zeroed
|
||||
# their outputs and set both LSEs to -inf).
|
||||
# Tokens 2 and 3 are empty on both sides.
|
||||
empty = slice(2, 4)
|
||||
prefix_lse[:, empty] = float("-inf")
|
||||
suffix_lse[:, empty] = float("-inf")
|
||||
|
||||
@@ -280,6 +280,10 @@ BATCH_SPECS = {
|
||||
"spec_decode_medium": BatchSpec(
|
||||
seq_lens=[512, 1024, 2048, 512, 1024, 2048], query_lens=[8, 8, 8, 8, 8, 8]
|
||||
),
|
||||
"chunked_context_prefill": BatchSpec(
|
||||
seq_lens=[1568, 520, 8, 80, 96, 8],
|
||||
query_lens=[32, 8, 8, 16, 16, 8],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1049,6 +1053,7 @@ def run_attention_backend(
|
||||
k_scale: float,
|
||||
kv_cache_dtype: str = "auto",
|
||||
prefill_backend: MLAPrefillBackendEnum | None = None,
|
||||
chunked_prefill_workspace_size: int | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Run attention computation using the specified backend's AttentionImpl."""
|
||||
|
||||
@@ -1137,6 +1142,13 @@ def run_attention_backend(
|
||||
|
||||
# Build metadata
|
||||
builder = builder_cls(kv_cache_spec, layer_names, vllm_config, device)
|
||||
if chunked_prefill_workspace_size is not None:
|
||||
builder.chunked_prefill_workspace_size = chunked_prefill_workspace_size
|
||||
builder.chunked_prefill_workspace = (
|
||||
builder.chunked_prefill_workspace.new_empty(
|
||||
chunked_prefill_workspace_size, head_size
|
||||
)
|
||||
)
|
||||
attn_metadata = builder.build(
|
||||
common_prefix_len=0,
|
||||
common_attn_metadata=common_attn_metadata,
|
||||
@@ -1156,32 +1168,7 @@ def run_attention_backend(
|
||||
return output
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"batch_spec_name",
|
||||
[
|
||||
"small_decode",
|
||||
"small_prefill",
|
||||
"mixed_small",
|
||||
"medium_decode",
|
||||
"medium_prefill",
|
||||
"mixed_medium",
|
||||
"large_decode",
|
||||
"large_prefill",
|
||||
"single_decode",
|
||||
"single_prefill",
|
||||
"spec_decode_small",
|
||||
"spec_decode_medium",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("model", ["deepseek-ai/DeepSeek-R1"])
|
||||
@pytest.mark.parametrize("tensor_parallel_size", [1, 4, 8, 16])
|
||||
@pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8", "fp8_e4m3"])
|
||||
@pytest.mark.parametrize(("q_scale", "k_scale"), [(1.0, 1.0), (2.0, 3.0)])
|
||||
@pytest.mark.parametrize(
|
||||
("prefill_backend", "qk_nope_head_dim", "v_head_dim"),
|
||||
_prefill_backend_dimension_params(),
|
||||
)
|
||||
def test_backend_correctness(
|
||||
def _run_backend_correctness(
|
||||
default_vllm_config,
|
||||
dist_init,
|
||||
workspace_init,
|
||||
@@ -1194,6 +1181,7 @@ def test_backend_correctness(
|
||||
prefill_backend: MLAPrefillBackendEnum,
|
||||
qk_nope_head_dim: int,
|
||||
v_head_dim: int,
|
||||
chunked_prefill_workspace_size: int | None = None,
|
||||
):
|
||||
"""
|
||||
Test that all backends produce similar outputs to a reference implementation
|
||||
@@ -1592,6 +1580,7 @@ def test_backend_correctness(
|
||||
q_scale=q_scale,
|
||||
k_scale=k_scale,
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
chunked_prefill_workspace_size=chunked_prefill_workspace_size,
|
||||
)
|
||||
|
||||
# Use backend_idx to get the correct SDPA output for this backend
|
||||
@@ -1643,3 +1632,90 @@ def test_backend_correctness(
|
||||
summary = f"{len(failures)} backend(s) failed: {', '.join(backend_names)}"
|
||||
detailed_msg = "\n".join(failures)
|
||||
pytest.fail(f"{summary}\n{detailed_msg}")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"batch_spec_name",
|
||||
[
|
||||
"small_decode",
|
||||
"small_prefill",
|
||||
"mixed_small",
|
||||
"medium_decode",
|
||||
"medium_prefill",
|
||||
"mixed_medium",
|
||||
"large_decode",
|
||||
"large_prefill",
|
||||
"single_decode",
|
||||
"single_prefill",
|
||||
"spec_decode_small",
|
||||
"spec_decode_medium",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("model", ["deepseek-ai/DeepSeek-R1"])
|
||||
@pytest.mark.parametrize("tensor_parallel_size", [1, 4, 8, 16])
|
||||
@pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8", "fp8_e4m3"])
|
||||
@pytest.mark.parametrize(("q_scale", "k_scale"), [(1.0, 1.0), (2.0, 3.0)])
|
||||
@pytest.mark.parametrize(
|
||||
("prefill_backend", "qk_nope_head_dim", "v_head_dim"),
|
||||
_prefill_backend_dimension_params(),
|
||||
)
|
||||
def test_backend_correctness(
|
||||
default_vllm_config,
|
||||
dist_init,
|
||||
workspace_init,
|
||||
batch_spec_name: str,
|
||||
model: str,
|
||||
tensor_parallel_size: int,
|
||||
kv_cache_dtype: str,
|
||||
q_scale: float,
|
||||
k_scale: float,
|
||||
prefill_backend: MLAPrefillBackendEnum,
|
||||
qk_nope_head_dim: int,
|
||||
v_head_dim: int,
|
||||
):
|
||||
_run_backend_correctness(
|
||||
default_vllm_config,
|
||||
dist_init,
|
||||
workspace_init,
|
||||
batch_spec_name,
|
||||
model,
|
||||
tensor_parallel_size,
|
||||
kv_cache_dtype,
|
||||
q_scale,
|
||||
k_scale,
|
||||
prefill_backend,
|
||||
qk_nope_head_dim,
|
||||
v_head_dim,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("prefill_backend", "qk_nope_head_dim", "v_head_dim"),
|
||||
_prefill_backend_dimension_params(),
|
||||
)
|
||||
@pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8"])
|
||||
def test_chunked_context_backend_correctness(
|
||||
default_vllm_config,
|
||||
dist_init,
|
||||
workspace_init,
|
||||
prefill_backend: MLAPrefillBackendEnum,
|
||||
qk_nope_head_dim: int,
|
||||
v_head_dim: int,
|
||||
kv_cache_dtype: str,
|
||||
):
|
||||
"""Split, packed, and context-free requests match the SDPA reference."""
|
||||
_run_backend_correctness(
|
||||
default_vllm_config,
|
||||
dist_init,
|
||||
workspace_init,
|
||||
batch_spec_name="chunked_context_prefill",
|
||||
model="deepseek-ai/DeepSeek-R1",
|
||||
tensor_parallel_size=16,
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
q_scale=1.0,
|
||||
k_scale=1.0,
|
||||
prefill_backend=prefill_backend,
|
||||
qk_nope_head_dim=qk_nope_head_dim,
|
||||
v_head_dim=v_head_dim,
|
||||
chunked_prefill_workspace_size=1024,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Per-request scheduling of MLA chunked-context prefill.
|
||||
|
||||
These cover the metadata contract `_compute_prefill_context` relies on: every
|
||||
context row is gathered exactly once, no chunk exceeds the workspace, and a
|
||||
chunk never covers a prefill without context (which is why the partial no
|
||||
longer needs an empty-span masking pass).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.model_executor.layers.attention.mla_attention import (
|
||||
build_mla_chunked_context_metadata,
|
||||
init_mla_context_partial,
|
||||
reorg_kvcache,
|
||||
)
|
||||
|
||||
BLOCK_SIZE = 16
|
||||
|
||||
|
||||
def build_chunked_context(
|
||||
context_lens: list[int],
|
||||
query_lens: list[int],
|
||||
workspace_size: int,
|
||||
block_size: int = BLOCK_SIZE,
|
||||
dcp_world_size: int = 1,
|
||||
dcp_local_block_size: int = 1,
|
||||
):
|
||||
query_start_loc = torch.zeros(len(query_lens) + 1, dtype=torch.int32)
|
||||
query_start_loc[1:] = torch.tensor(query_lens, dtype=torch.int32).cumsum(0)
|
||||
workspace_rows = workspace_size + workspace_size // dcp_world_size
|
||||
return build_mla_chunked_context_metadata(
|
||||
context_lens_cpu=torch.tensor(context_lens, dtype=torch.int32),
|
||||
prefill_query_start_loc_cpu=query_start_loc,
|
||||
chunked_prefill_workspace=torch.empty((workspace_rows, 1)),
|
||||
chunked_prefill_workspace_size=workspace_size,
|
||||
block_size=block_size,
|
||||
align_chunk_to_block=True,
|
||||
device=torch.device("cpu"),
|
||||
dcp_world_size=dcp_world_size,
|
||||
dcp_local_block_size=dcp_local_block_size,
|
||||
dcp_virtual_block_size=dcp_local_block_size * dcp_world_size,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"context_lens,workspace_size",
|
||||
[
|
||||
# Heterogeneous batch: the long request must not shrink the chunk the
|
||||
# short ones share.
|
||||
([960, 16, 16, 16, 16, 16, 16, 16], 1024),
|
||||
# A single request that does not fit is split on its own.
|
||||
([2048, 320], 1024),
|
||||
# Every request needs its own chunk.
|
||||
([1024, 1024, 1024], 1024),
|
||||
# Unaligned tails.
|
||||
([37, 1000, 5], 1024),
|
||||
([1], 1024),
|
||||
],
|
||||
)
|
||||
def test_chunks_gather_every_context_row_exactly_once(context_lens, workspace_size):
|
||||
"""Chunks tile the batch's context without gaps, overlap, or overflow.
|
||||
|
||||
This is the invariant the whole schedule rests on: attention over a chunk
|
||||
only sees the rows that chunk gathered, so a row counted twice or dropped
|
||||
silently corrupts the context partial.
|
||||
"""
|
||||
query_lens = [4] * len(context_lens)
|
||||
metadata = build_chunked_context(context_lens, query_lens, workspace_size)
|
||||
assert metadata is not None
|
||||
|
||||
gathered: dict[int, list[tuple[int, int]]] = {}
|
||||
previous_request_start = -1
|
||||
for chunk in metadata.chunks:
|
||||
assert chunk.num_context_tokens <= workspace_size
|
||||
assert chunk.num_context_tokens == int(chunk.cu_seq_lens[-1])
|
||||
assert chunk.token_to_seq.shape[0] == chunk.num_context_tokens
|
||||
# Chunks are emitted in request order, which is what lets the partial
|
||||
# treat only a chunk's first request as a continuation.
|
||||
assert chunk.request_slice.start >= previous_request_start
|
||||
previous_request_start = chunk.request_slice.start
|
||||
|
||||
starts = chunk.starts.tolist()
|
||||
seq_lens = chunk.seq_lens.tolist()
|
||||
assert len(starts) == len(seq_lens) == chunk.num_requests
|
||||
for offset, (start, length) in enumerate(zip(starts, seq_lens)):
|
||||
assert length > 0, "a chunk must not cover an empty context span"
|
||||
gathered.setdefault(chunk.request_slice.start + offset, []).append(
|
||||
(start, length)
|
||||
)
|
||||
|
||||
expected = {i: length for i, length in enumerate(context_lens) if length > 0}
|
||||
assert gathered.keys() == expected.keys()
|
||||
for request, spans in gathered.items():
|
||||
cursor = 0
|
||||
for start, length in spans:
|
||||
assert start == cursor
|
||||
cursor += length
|
||||
assert cursor == expected[request]
|
||||
|
||||
|
||||
def test_continuation_is_confined_to_a_chunks_first_request():
|
||||
"""Only a chunk's first request may continue an earlier chunk.
|
||||
|
||||
`accumulate_mla_context_chunk` merges the continuation token slice and
|
||||
writes the rest, so a chunk that continues a request other than its first,
|
||||
or reports the wrong boundary, folds the partial into the wrong tokens.
|
||||
"""
|
||||
context_lens = [2048, 32, 32]
|
||||
query_lens = [3, 5, 7]
|
||||
metadata = build_chunked_context(context_lens, query_lens, 1024)
|
||||
assert metadata is not None
|
||||
query_start_loc = [0, 3, 8, 15]
|
||||
|
||||
assert [chunk.is_continuation for chunk in metadata.chunks] == [
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
]
|
||||
for chunk in metadata.chunks:
|
||||
starts = chunk.starts.tolist()
|
||||
assert chunk.is_continuation == (starts[0] > 0)
|
||||
# Requests after the first always start at the beginning of their
|
||||
# context, so they can only ever be initialized, never merged.
|
||||
assert all(start == 0 for start in starts[1:])
|
||||
request_start = chunk.request_slice.start
|
||||
request_end = chunk.request_slice.stop
|
||||
assert chunk.continuation_token_end == query_start_loc[request_start + 1]
|
||||
assert chunk.token_slice == slice(
|
||||
query_start_loc[request_start], query_start_loc[request_end]
|
||||
)
|
||||
assert chunk.query_start_loc.tolist() == [
|
||||
offset - chunk.token_slice.start
|
||||
for offset in query_start_loc[request_start : request_end + 1]
|
||||
]
|
||||
|
||||
|
||||
def test_tail_splitting_minimizes_chunks():
|
||||
"""A tail request may fill one chunk and continue at the next chunk's head.
|
||||
|
||||
Without tail splitting these contexts need three chunks. Splitting on the
|
||||
block boundary reduces that to the workspace lower bound of two.
|
||||
"""
|
||||
metadata = build_chunked_context([768, 512, 512], [3, 5, 7], 1024)
|
||||
assert metadata is not None
|
||||
|
||||
assert len(metadata.chunks) == 2
|
||||
first, second = metadata.chunks
|
||||
assert first.starts.tolist() == [0, 0]
|
||||
assert first.seq_lens.tolist() == [768, 256]
|
||||
assert not first.is_continuation
|
||||
|
||||
assert second.starts.tolist() == [256, 0]
|
||||
assert second.seq_lens.tolist() == [256, 512]
|
||||
assert second.is_continuation
|
||||
|
||||
|
||||
def test_prefills_without_context_are_skipped_and_neutralized():
|
||||
"""Context-free prefills get neutral partials instead of chunk work.
|
||||
|
||||
If the full-query partial leaves either an internal or trailing gap
|
||||
uninitialized, the final merge combines the suffix with undefined scratch.
|
||||
"""
|
||||
metadata = build_chunked_context([64, 0, 64, 0], [4, 4, 4, 4], 1024)
|
||||
assert metadata is not None
|
||||
|
||||
covered = {
|
||||
request
|
||||
for chunk in metadata.chunks
|
||||
for request in range(chunk.request_slice.start, chunk.request_slice.stop)
|
||||
}
|
||||
assert covered == {0, 2}
|
||||
assert metadata.empty_token_slices == [slice(4, 8), slice(12, 16)]
|
||||
|
||||
output, output_lse = init_mla_context_partial(
|
||||
metadata,
|
||||
attn_output=torch.empty(4, 2, 3),
|
||||
attn_softmax_lse=torch.empty(2, 4),
|
||||
num_tokens=16,
|
||||
)
|
||||
assert output.shape == (16, 2, 3)
|
||||
assert output_lse.shape == (2, 16)
|
||||
for token_slice in metadata.empty_token_slices:
|
||||
assert not torch.count_nonzero(output[token_slice])
|
||||
assert torch.isneginf(output_lse[:, token_slice]).all()
|
||||
|
||||
|
||||
def test_no_context_needs_no_chunks():
|
||||
assert build_chunked_context([0, 0], [4, 4], 1024) is None
|
||||
|
||||
|
||||
def test_dcp_chunks_fit_the_per_rank_row_budget():
|
||||
"""Under DCP each rank gathers its own shard, so the budget is 1/world.
|
||||
|
||||
The local starts and lengths must tile the rank's padded context the same
|
||||
way the global ones tile the real context, since `reorg_kvcache` unpads
|
||||
using them.
|
||||
"""
|
||||
dcp_world_size, interleave, workspace_size = 2, 64, 1024
|
||||
context_lens = [3000, 200, 200]
|
||||
metadata = build_chunked_context(
|
||||
context_lens,
|
||||
[4] * len(context_lens),
|
||||
workspace_size,
|
||||
block_size=128,
|
||||
dcp_world_size=dcp_world_size,
|
||||
dcp_local_block_size=interleave,
|
||||
)
|
||||
assert metadata is not None
|
||||
virtual_block_size = interleave * dcp_world_size
|
||||
|
||||
local_cursor: dict[int, int] = {}
|
||||
for chunk in metadata.chunks:
|
||||
assert chunk.num_local_context_tokens <= workspace_size // dcp_world_size
|
||||
assert chunk.local_starts == chunk.starts.tolist()
|
||||
assert chunk.padded_local_cu_seq_lens.tolist() == [0] + list(
|
||||
torch.tensor(chunk.padded_local_seq_lens).cumsum(0)
|
||||
)
|
||||
assert (
|
||||
chunk.padded_local_token_to_seq.shape[0] == chunk.num_local_context_tokens
|
||||
)
|
||||
for offset, (start, length) in enumerate(
|
||||
zip(chunk.local_starts, chunk.padded_local_seq_lens)
|
||||
):
|
||||
request = chunk.request_slice.start + offset
|
||||
assert start == local_cursor.get(request, 0)
|
||||
local_cursor[request] = start + length
|
||||
|
||||
for request, length in enumerate(context_lens):
|
||||
padded_local = -(-length // virtual_block_size) * interleave
|
||||
assert local_cursor[request] == padded_local
|
||||
|
||||
|
||||
def test_dcp_reorg_uses_each_chunks_local_starts():
|
||||
"""Reorganization drops DCP padding at each request's own chunk offset."""
|
||||
dcp_world_size, interleave, workspace_size = 2, 64, 1024
|
||||
metadata = build_chunked_context(
|
||||
[3000, 200, 200],
|
||||
[4, 4, 4],
|
||||
workspace_size,
|
||||
block_size=128,
|
||||
dcp_world_size=dcp_world_size,
|
||||
dcp_local_block_size=interleave,
|
||||
)
|
||||
assert metadata is not None
|
||||
|
||||
for chunk in metadata.chunks:
|
||||
assert chunk.padded_local_seq_lens is not None
|
||||
assert chunk.local_context_lens_allranks is not None
|
||||
assert chunk.local_starts is not None
|
||||
|
||||
toks = chunk.num_local_context_tokens
|
||||
rank_buffers = [torch.full((toks, 1, 1), -1) for _ in range(dcp_world_size)]
|
||||
expected = []
|
||||
src_token_idx = 0
|
||||
for request, (padded_len, local_lens, local_start) in enumerate(
|
||||
zip(
|
||||
chunk.padded_local_seq_lens,
|
||||
chunk.local_context_lens_allranks,
|
||||
chunk.local_starts,
|
||||
)
|
||||
):
|
||||
for rank, local_len in enumerate(local_lens):
|
||||
actual_len = min(max(0, local_len - local_start), padded_len)
|
||||
values = (
|
||||
request * 100_000
|
||||
+ rank * 10_000
|
||||
+ torch.arange(local_start, local_start + actual_len)
|
||||
).view(-1, 1, 1)
|
||||
rank_buffers[rank][src_token_idx : src_token_idx + actual_len] = values
|
||||
expected.append(values)
|
||||
src_token_idx += padded_len
|
||||
|
||||
allgathered = torch.cat(rank_buffers)
|
||||
reorganized, _ = reorg_kvcache(
|
||||
allgathered,
|
||||
allgathered,
|
||||
padded_local_chunk_seq_lens_lst=chunk.padded_local_seq_lens,
|
||||
local_context_lens_allranks=chunk.local_context_lens_allranks,
|
||||
local_starts=chunk.local_starts,
|
||||
sum_seq_len=chunk.num_context_tokens,
|
||||
max_seq_len=chunk.max_seq_len,
|
||||
toks=toks,
|
||||
)
|
||||
|
||||
torch.testing.assert_close(reorganized, torch.cat(expected))
|
||||
@@ -24,7 +24,7 @@ class CustomMLAPrefillBackend(MLAPrefillBackend):
|
||||
def run_prefill_new_tokens(self, q, k, v, return_softmax_lse):
|
||||
raise NotImplementedError
|
||||
|
||||
def run_prefill_context_chunk(self, chunk_idx, q, k, v):
|
||||
def run_prefill_context_chunk(self, chunk, q, k, v):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ def test_register_custom_backend_as_decorator():
|
||||
def run_prefill_new_tokens(self, q, k, v, return_softmax_lse):
|
||||
raise NotImplementedError
|
||||
|
||||
def run_prefill_context_chunk(self, chunk_idx, q, k, v):
|
||||
def run_prefill_context_chunk(self, chunk, q, k, v):
|
||||
raise NotImplementedError
|
||||
|
||||
assert MLAPrefillBackendEnum.CUSTOM.is_overridden()
|
||||
|
||||
@@ -875,7 +875,7 @@ def test_masked_mha_workspace_fits_single_request_boundary(max_query_len, expect
|
||||
_masked_mha_workspace_fits(
|
||||
batch_size=1,
|
||||
max_query_len=max_query_len,
|
||||
context_chunk_max_seq_lens=None,
|
||||
max_context_chunk_seq_len=0,
|
||||
workspace_numel=GLOBAL_TOPK_MASK_MAX_BYTES // torch.int32.itemsize,
|
||||
)
|
||||
is expected
|
||||
@@ -884,7 +884,7 @@ def test_masked_mha_workspace_fits_single_request_boundary(max_query_len, expect
|
||||
|
||||
def test_masked_mha_workspace_fits_accounts_for_batch_and_context():
|
||||
"""Request count and context chunk length are independent multipliers."""
|
||||
base = dict(batch_size=2, max_query_len=2048, context_chunk_max_seq_lens=[2048])
|
||||
base = dict(batch_size=2, max_query_len=2048, max_context_chunk_seq_len=2048)
|
||||
exact = math.prod(_topk_mask_shape(2, 2048, 2048))
|
||||
|
||||
assert _masked_mha_workspace_fits(**base, workspace_numel=exact)
|
||||
@@ -892,7 +892,7 @@ def test_masked_mha_workspace_fits_accounts_for_batch_and_context():
|
||||
**{**base, "batch_size": 3}, workspace_numel=exact
|
||||
)
|
||||
assert not _masked_mha_workspace_fits(
|
||||
**{**base, "context_chunk_max_seq_lens": [4096]}, workspace_numel=exact
|
||||
**{**base, "max_context_chunk_seq_len": 4096}, workspace_numel=exact
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -133,9 +133,19 @@ fixed workspace size.
|
||||
|
||||
The chunked prefill approach is as follows:
|
||||
|
||||
MCC Max chunk of context to process per iter, computed dynamically,
|
||||
W Workspace rows, i.e. the context rows we may gather at once,
|
||||
used to bound the memory usage
|
||||
|
||||
The context is scheduled per request: requests are packed in order, splitting
|
||||
the next request on an aligned boundary when needed to fill `W`.
|
||||
So a chunk covers a contiguous run of prefills and only ever the tokens and
|
||||
context rows of those prefills — attention, up-projection and merging are
|
||||
charged only to the requests it covers, and no chunk contains an empty context
|
||||
span. See `plan_mla_context_chunks`.
|
||||
|
||||
Only a chunk's first request can be a continuation, so accumulation performs at
|
||||
most one request-slice merge and one bulk write per chunk.
|
||||
|
||||
q_c = h_t @ W_DQ
|
||||
q_nope = (q_c @ W_UQ).view(Sq, N, P)
|
||||
q_pe = RoPE(q_c @ W_QR).view(Sq, N, R)
|
||||
@@ -157,10 +167,12 @@ curr_o, curr_lse = scaled_dot_product_attention(
|
||||
return_softmax_lse=True
|
||||
)
|
||||
|
||||
// Compute attention with the already existing context
|
||||
for chunk_idx in range(cdiv(C, MCC)):
|
||||
chunk_start = chunk_idx * MCC
|
||||
chunk_end = min(chunk_start + MCC, C)
|
||||
// Compute attention with the already existing context. Shown for a single
|
||||
// request; with a batch, a chunk covers a run of requests and both `q` and the
|
||||
// gathered context below are sliced to that run.
|
||||
for chunk_idx in range(cdiv(C, W)):
|
||||
chunk_start = chunk_idx * W
|
||||
chunk_end = min(chunk_start + W, C)
|
||||
Sc = chunk_end - chunk_start
|
||||
cache_kv_c_chunk = cache_kv_c[chunk_start:chunk_end]
|
||||
cache_k_pe_chunk = cache_k_pe[chunk_start:chunk_end]
|
||||
@@ -188,11 +200,15 @@ return curr_o @ W_O
|
||||
"""
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
import math
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import ClassVar, Generic, TypeVar, cast
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from tqdm import tqdm
|
||||
@@ -256,6 +272,7 @@ from vllm.utils.torch_utils import (
|
||||
direct_register_custom_op,
|
||||
is_quantized_kv_cache,
|
||||
kv_cache_dtype_str_to_dtype,
|
||||
np_to_pinned_tensor,
|
||||
)
|
||||
from vllm.v1.attention.backend import (
|
||||
AttentionBackend,
|
||||
@@ -278,7 +295,6 @@ from vllm.v1.attention.backends.utils import (
|
||||
from vllm.v1.attention.ops.common import cp_lse_ag_out_ar, cp_lse_ag_out_rs
|
||||
from vllm.v1.attention.ops.dcp_alltoall import dcp_a2a_lse_reduce
|
||||
from vllm.v1.attention.ops.merge_attn_states import merge_attn_states
|
||||
from vllm.v1.attention.ops.triton_merge_attn_states import mask_empty_context
|
||||
from vllm.v1.attention.selector import get_attn_backend
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
AttentionSpec,
|
||||
@@ -1375,28 +1391,42 @@ class MLACommonPrefillMetadata:
|
||||
"""Prefill Specific Metadata"""
|
||||
|
||||
@dataclass
|
||||
class ChunkedContextMetadata:
|
||||
# New for MLA (compared to FlashAttention)
|
||||
# For handling chunked prefill
|
||||
class ContextChunk:
|
||||
"""One workspace-sized slice of paged context for a run of prefills."""
|
||||
|
||||
index: int
|
||||
request_slice: slice
|
||||
token_slice: slice
|
||||
continuation_token_end: int
|
||||
is_continuation: bool
|
||||
num_context_tokens: int
|
||||
query_start_loc: torch.Tensor
|
||||
max_query_len: int
|
||||
cu_seq_lens: torch.Tensor
|
||||
starts: torch.Tensor
|
||||
seq_tot: list[int]
|
||||
max_seq_lens: list[int]
|
||||
max_seq_len: int
|
||||
seq_lens: torch.Tensor
|
||||
context_lens: torch.Tensor
|
||||
workspace: torch.Tensor
|
||||
token_to_seq: torch.Tensor
|
||||
chunk_total_token: list[int]
|
||||
has_empty_context: list[bool]
|
||||
|
||||
# for mla DCP
|
||||
padded_local_chunk_seq_lens: list[list[int]] | None = None
|
||||
padded_local_seq_lens: list[int] | None = None
|
||||
local_context_lens_allranks: list[list[int]] | None = None
|
||||
padded_local_cu_seq_lens: torch.Tensor | None = None
|
||||
padded_local_token_to_seq: torch.Tensor | None = None
|
||||
cu_seq_lens_lst: list[list[int]] | None = None
|
||||
chunk_size: int | None = None
|
||||
prefill_tokens_with_context: int | None = None
|
||||
num_local_context_tokens: int = 0
|
||||
local_starts: list[int] | None = None
|
||||
|
||||
@property
|
||||
def num_requests(self) -> int:
|
||||
return self.request_slice.stop - self.request_slice.start
|
||||
|
||||
@dataclass
|
||||
class ChunkedContextMetadata:
|
||||
context_lens: torch.Tensor
|
||||
workspace: torch.Tensor
|
||||
chunks: "list[MLACommonPrefillMetadata.ContextChunk]"
|
||||
context_lens_list: list[int]
|
||||
empty_token_slices: list[slice]
|
||||
|
||||
block_table: torch.Tensor
|
||||
query_start_loc: torch.Tensor
|
||||
@@ -1566,11 +1596,98 @@ def backend_supports_prefill_query_quantization() -> bool:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ContextChunkPlan:
|
||||
"""Request-space layout of one context chunk, before tensors are built."""
|
||||
|
||||
request_start: int
|
||||
request_end: int
|
||||
# Per-request context offset and row count, in request order.
|
||||
starts: list[int]
|
||||
seq_lens: list[int]
|
||||
is_continuation: bool
|
||||
|
||||
|
||||
def plan_mla_context_chunks(
|
||||
context_lens: list[int],
|
||||
row_budget: int,
|
||||
max_context_chunk: int,
|
||||
split_alignment: int,
|
||||
padded_rows: Callable[[int], int],
|
||||
) -> list[_ContextChunkPlan]:
|
||||
"""Pack per-request contexts into workspace-sized chunks."""
|
||||
assert max_context_chunk > 0
|
||||
assert split_alignment > 0
|
||||
|
||||
def aligned_split_len(start: int, remaining: int, available: int) -> int:
|
||||
max_split = min(
|
||||
max_context_chunk,
|
||||
round_down(remaining - 1, split_alignment),
|
||||
)
|
||||
low, high = 0, max_split // split_alignment
|
||||
while low < high:
|
||||
mid = (low + high + 1) // 2
|
||||
length = mid * split_alignment
|
||||
rows = padded_rows(start + length) - padded_rows(start)
|
||||
if rows <= available:
|
||||
low = mid
|
||||
else:
|
||||
high = mid - 1
|
||||
return low * split_alignment
|
||||
|
||||
plans: list[_ContextChunkPlan] = []
|
||||
num_requests = len(context_lens)
|
||||
request = 0
|
||||
start = 0
|
||||
while request < num_requests:
|
||||
context_len = context_lens[request]
|
||||
if context_len == 0:
|
||||
assert start == 0
|
||||
request += 1
|
||||
continue
|
||||
|
||||
request_start = request
|
||||
starts: list[int] = []
|
||||
seq_lens: list[int] = []
|
||||
rows = 0
|
||||
while request < num_requests and context_lens[request] > 0:
|
||||
remaining = context_lens[request] - start
|
||||
request_rows = padded_rows(start + remaining) - padded_rows(start)
|
||||
if rows + request_rows > row_budget:
|
||||
split_len = aligned_split_len(start, remaining, row_budget - rows)
|
||||
if split_len > 0:
|
||||
starts.append(start)
|
||||
seq_lens.append(split_len)
|
||||
rows += padded_rows(start + split_len) - padded_rows(start)
|
||||
start += split_len
|
||||
break
|
||||
rows += request_rows
|
||||
starts.append(start)
|
||||
seq_lens.append(remaining)
|
||||
request += 1
|
||||
start = 0
|
||||
assert seq_lens
|
||||
plans.append(
|
||||
_ContextChunkPlan(
|
||||
request_start=request_start,
|
||||
request_end=request_start + len(seq_lens),
|
||||
starts=starts,
|
||||
seq_lens=seq_lens,
|
||||
is_continuation=starts[0] > 0,
|
||||
)
|
||||
)
|
||||
return plans
|
||||
|
||||
|
||||
def _flat_int32(values: list[int] | np.ndarray) -> torch.Tensor:
|
||||
"""Pinned int32 CPU tensor backing one concatenated per-chunk field."""
|
||||
return np_to_pinned_tensor(np.asarray(values, dtype=np.int32))
|
||||
|
||||
|
||||
def build_mla_chunked_context_metadata(
|
||||
*,
|
||||
context_lens_cpu: torch.Tensor,
|
||||
prefill_query_start_loc_cpu: torch.Tensor,
|
||||
num_prefills: int,
|
||||
chunked_prefill_workspace: torch.Tensor,
|
||||
chunked_prefill_workspace_size: int,
|
||||
block_size: int,
|
||||
@@ -1582,14 +1699,13 @@ def build_mla_chunked_context_metadata(
|
||||
) -> "MLACommonPrefillMetadata.ChunkedContextMetadata | None":
|
||||
"""Build chunked-context metadata for an MLA prefill.
|
||||
|
||||
Shared by dense and sparse builders. Splits each prefill's context
|
||||
into workspace-sized chunks and, under DCP, plans the per-rank interleaved
|
||||
local chunks the all-gather reduction consumes.
|
||||
Shared by dense and sparse builders. Packs the prefill contexts into a flat
|
||||
list of workspace-sized per-request chunks and, under DCP, plans the
|
||||
per-rank interleaved local chunks the all-gather reduction consumes.
|
||||
|
||||
Args:
|
||||
context_lens_cpu: Per-prefill context length (seq_len - query_len).
|
||||
prefill_query_start_loc_cpu: Prefill query cumulative offsets (0-based).
|
||||
num_prefills: Number of prefill requests.
|
||||
chunked_prefill_workspace: Scratch buffer the context gather writes to.
|
||||
chunked_prefill_workspace_size: Row capacity of the workspace.
|
||||
block_size: KV cache page size for chunk-start alignment.
|
||||
@@ -1604,147 +1720,189 @@ def build_mla_chunked_context_metadata(
|
||||
"""
|
||||
# NOTE: it is recommended you read the `Chunked Prefill` section in the
|
||||
# comment at the top of the file before trying to understand this code.
|
||||
max_context_len = context_lens_cpu.max().item()
|
||||
if max_context_len <= 0:
|
||||
context_lens = context_lens_cpu.tolist()
|
||||
if max(context_lens, default=0) <= 0:
|
||||
return None
|
||||
num_prefills_with_context = int((context_lens_cpu > 0).sum().item())
|
||||
|
||||
# Currently we allocate an equal amount of workspace for each prefill with
|
||||
# context; we could probably use a more advanced algorithm here and allocate
|
||||
# more workspace to prefills with longer context lengths.
|
||||
max_context_chunk = chunked_prefill_workspace_size // num_prefills_with_context
|
||||
if align_chunk_to_block:
|
||||
# The `gather_and_maybe_dequant_cache` kernel cannot handle chunk
|
||||
# starts that are not aligned to block_size, so round down.
|
||||
max_context_chunk = round_down(max_context_chunk, block_size)
|
||||
assert max_context_chunk > 0
|
||||
|
||||
num_chunks = cdiv(max_context_len, max_context_chunk)
|
||||
# e.g. max_context_chunk=256, num_chunks=3, num_prefills=4 ->
|
||||
# [[0, 0, 0, 0], [256, 256, 256, 256], [512, 512, 512, 512]]
|
||||
# Note(simon): this is done on CPU because of downstream's use of `to_list`.
|
||||
chunk_starts = torch.empty(
|
||||
num_chunks, num_prefills, dtype=torch.int32, pin_memory=True
|
||||
).copy_(
|
||||
torch.arange(num_chunks, dtype=torch.int32)
|
||||
.multiply_(max_context_chunk)
|
||||
.unsqueeze(1)
|
||||
)
|
||||
chunk_ends = torch.min(
|
||||
context_lens_cpu.unsqueeze(0), chunk_starts + max_context_chunk
|
||||
)
|
||||
chunk_seq_lens = chunk_ends - chunk_starts
|
||||
chunk_seq_lens.clamp_(min=0)
|
||||
has_empty_context = torch.any(chunk_seq_lens == 0, dim=1).tolist()
|
||||
|
||||
cu_seq_lens_cpu = torch.zeros(
|
||||
num_chunks, num_prefills + 1, dtype=torch.int32, pin_memory=True
|
||||
)
|
||||
torch.cumsum(chunk_seq_lens, dim=1, out=cu_seq_lens_cpu[:, 1:], dtype=torch.int32)
|
||||
chunk_total_token = cu_seq_lens_cpu[:, -1]
|
||||
|
||||
max_tokens_over_chunk = chunk_total_token.max().item()
|
||||
token_to_seq_cpu = torch.zeros(
|
||||
(num_chunks, max_tokens_over_chunk), dtype=torch.int32, pin_memory=True
|
||||
)
|
||||
req_indices = torch.arange(num_prefills, dtype=torch.int32)
|
||||
for i in range(num_chunks):
|
||||
token_to_seq = torch.repeat_interleave(req_indices, chunk_seq_lens[i])
|
||||
token_to_seq_cpu[i, : token_to_seq.shape[0]] = token_to_seq
|
||||
|
||||
prefill_tokens_with_context = prefill_query_start_loc_cpu[
|
||||
num_prefills_with_context
|
||||
].item()
|
||||
|
||||
metadata_cls = MLACommonPrefillMetadata.ChunkedContextMetadata
|
||||
# The `gather_and_maybe_dequant_cache` kernel cannot handle chunk starts
|
||||
# that are not aligned to block_size, so a split request advances in
|
||||
# block-aligned steps.
|
||||
chunk_alignment = block_size if align_chunk_to_block else 1
|
||||
if dcp_world_size > 1:
|
||||
local_context_lens_allranks = get_dcp_local_seq_lens(
|
||||
context_lens_cpu, dcp_world_size, None, dcp_local_block_size
|
||||
)
|
||||
# Note(qcs): Per-rank local context lengths, padded to
|
||||
# `dcp_local_block_size`.
|
||||
padded_local_context_lens_cpu: torch.Tensor = (
|
||||
cdiv(context_lens_cpu, dcp_virtual_block_size) * dcp_local_block_size
|
||||
)
|
||||
# Note(hc): The above max_context_chunk already enforces block_size
|
||||
# alignment; DCP only requires block_size be divisible by dcp_world_size,
|
||||
# because DCP uses cp_gather_cache, which does not require chunk starts
|
||||
# aligned to block_size.
|
||||
assert max_context_chunk % dcp_world_size == 0
|
||||
padded_local_max_context_chunk = (
|
||||
cdiv(max_context_chunk, dcp_virtual_block_size) * dcp_local_block_size
|
||||
)
|
||||
local_chunk_starts = torch.empty(
|
||||
num_chunks, num_prefills, dtype=torch.int32, pin_memory=True
|
||||
).copy_(
|
||||
torch.arange(num_chunks, dtype=torch.int32)
|
||||
.multiply_(padded_local_max_context_chunk)
|
||||
.unsqueeze(1)
|
||||
)
|
||||
local_chunk_ends = torch.min(
|
||||
padded_local_context_lens_cpu.unsqueeze(0),
|
||||
local_chunk_starts + padded_local_max_context_chunk,
|
||||
)
|
||||
padded_local_chunk_seq_lens = local_chunk_ends - local_chunk_starts
|
||||
padded_local_chunk_seq_lens.clamp_(min=0)
|
||||
# Each rank gathers only its own shard, so a chunk's workspace cost is
|
||||
# 1/dcp_world_size of its context rows, rounded up to the interleave
|
||||
# block size. Sub-chunks are additionally aligned to the virtual block
|
||||
# size so that every rank's local start stays interleave-aligned.
|
||||
row_budget = chunked_prefill_workspace_size // dcp_world_size
|
||||
chunk_alignment = math.lcm(chunk_alignment, dcp_virtual_block_size)
|
||||
|
||||
padded_local_cu_seq_lens_cpu = torch.zeros(
|
||||
num_chunks, num_prefills + 1, dtype=torch.int32, pin_memory=True
|
||||
)
|
||||
torch.cumsum(
|
||||
padded_local_chunk_seq_lens,
|
||||
dim=1,
|
||||
out=padded_local_cu_seq_lens_cpu[:, 1:],
|
||||
dtype=torch.int32,
|
||||
)
|
||||
max_padded_local_tokens = padded_local_cu_seq_lens_cpu[:, -1].max().item()
|
||||
padded_local_token_to_seq_cpu = torch.zeros(
|
||||
(num_chunks, max_padded_local_tokens), dtype=torch.int32
|
||||
)
|
||||
for i in range(num_chunks):
|
||||
tts = torch.repeat_interleave(req_indices, padded_local_chunk_seq_lens[i])
|
||||
padded_local_token_to_seq_cpu[i, : tts.shape[0]] = tts
|
||||
|
||||
chunked_context_metadata = metadata_cls(
|
||||
cu_seq_lens=cu_seq_lens_cpu.to(device, non_blocking=True),
|
||||
starts=local_chunk_starts.to(device, non_blocking=True),
|
||||
seq_tot=padded_local_chunk_seq_lens.sum(dim=1).tolist(),
|
||||
max_seq_lens=chunk_seq_lens.max(dim=1).values.tolist(),
|
||||
seq_lens=chunk_seq_lens,
|
||||
context_lens=context_lens_cpu.to(device, non_blocking=True),
|
||||
token_to_seq=token_to_seq_cpu.to(device, non_blocking=True),
|
||||
chunk_total_token=chunk_total_token.tolist(),
|
||||
workspace=chunked_prefill_workspace,
|
||||
has_empty_context=has_empty_context,
|
||||
prefill_tokens_with_context=prefill_tokens_with_context,
|
||||
padded_local_chunk_seq_lens=padded_local_chunk_seq_lens.tolist(),
|
||||
local_context_lens_allranks=local_context_lens_allranks.tolist(),
|
||||
padded_local_cu_seq_lens=padded_local_cu_seq_lens_cpu.to(
|
||||
device, non_blocking=True
|
||||
),
|
||||
padded_local_token_to_seq=padded_local_token_to_seq_cpu.to(
|
||||
device, non_blocking=True
|
||||
),
|
||||
cu_seq_lens_lst=cu_seq_lens_cpu.tolist(),
|
||||
chunk_size=padded_local_max_context_chunk,
|
||||
)
|
||||
def padded_rows(rows: int) -> int:
|
||||
return cdiv(rows, dcp_virtual_block_size) * dcp_local_block_size
|
||||
else:
|
||||
chunked_context_metadata = metadata_cls(
|
||||
cu_seq_lens=cu_seq_lens_cpu.to(device, non_blocking=True),
|
||||
starts=chunk_starts.to(device, non_blocking=True),
|
||||
seq_tot=chunk_seq_lens.sum(dim=1).tolist(),
|
||||
max_seq_lens=chunk_seq_lens.max(dim=1).values.tolist(),
|
||||
seq_lens=chunk_seq_lens,
|
||||
context_lens=context_lens_cpu.to(device, non_blocking=True),
|
||||
token_to_seq=token_to_seq_cpu.to(device, non_blocking=True),
|
||||
chunk_total_token=chunk_total_token,
|
||||
workspace=chunked_prefill_workspace,
|
||||
has_empty_context=has_empty_context,
|
||||
prefill_tokens_with_context=prefill_tokens_with_context,
|
||||
row_budget = chunked_prefill_workspace_size
|
||||
|
||||
def padded_rows(rows: int) -> int:
|
||||
return rows
|
||||
|
||||
max_context_chunk = round_down(chunked_prefill_workspace_size, chunk_alignment)
|
||||
assert max_context_chunk > 0, (
|
||||
f"chunked prefill workspace ({chunked_prefill_workspace_size} rows) is "
|
||||
f"smaller than the context chunk alignment ({chunk_alignment} rows)"
|
||||
)
|
||||
|
||||
plans = plan_mla_context_chunks(
|
||||
context_lens,
|
||||
row_budget,
|
||||
max_context_chunk,
|
||||
chunk_alignment,
|
||||
padded_rows,
|
||||
)
|
||||
|
||||
query_start_loc = prefill_query_start_loc_cpu.tolist()
|
||||
empty_token_slices = [
|
||||
slice(query_start_loc[i], query_start_loc[i + 1])
|
||||
for i, context_len in enumerate(context_lens)
|
||||
if context_len == 0
|
||||
]
|
||||
|
||||
use_dcp = dcp_world_size > 1
|
||||
local_context_lens_allranks = (
|
||||
get_dcp_local_seq_lens(
|
||||
context_lens_cpu, dcp_world_size, None, dcp_local_block_size
|
||||
).tolist()
|
||||
if use_dcp
|
||||
else None
|
||||
)
|
||||
|
||||
# Concatenate the per-chunk fields so each one costs a single
|
||||
# host-to-device transfer, and remember where every chunk's slice lands.
|
||||
starts_flat: list[int] = []
|
||||
seq_lens_flat: list[int] = []
|
||||
cu_seq_lens_flat: list[int] = []
|
||||
cu_seqlens_q_flat: list[int] = []
|
||||
token_to_seq_parts: list[np.ndarray] = []
|
||||
padded_local_cu_seq_lens_flat: list[int] = []
|
||||
padded_local_token_to_seq_parts: list[np.ndarray] = []
|
||||
local_starts_per_chunk: list[list[int]] = []
|
||||
local_seq_lens_per_chunk: list[list[int]] = []
|
||||
layouts: list[tuple[slice, slice, slice, slice]] = []
|
||||
|
||||
request_offset = boundary_offset = token_offset = local_token_offset = 0
|
||||
for plan in plans:
|
||||
num_requests = len(plan.seq_lens)
|
||||
num_tokens = sum(plan.seq_lens)
|
||||
num_local_tokens = num_tokens
|
||||
|
||||
seq_lens_flat.extend(plan.seq_lens)
|
||||
cu_seq_lens_flat.extend(itertools.accumulate(plan.seq_lens, initial=0))
|
||||
query_base = query_start_loc[plan.request_start]
|
||||
cu_seqlens_q_flat.extend(
|
||||
query_start_loc[request] - query_base
|
||||
for request in range(plan.request_start, plan.request_end + 1)
|
||||
)
|
||||
token_to_seq_parts.append(
|
||||
np.repeat(np.arange(num_requests, dtype=np.int32), plan.seq_lens)
|
||||
)
|
||||
|
||||
assert max(chunked_context_metadata.max_seq_lens) <= chunked_prefill_workspace_size
|
||||
return chunked_context_metadata
|
||||
if use_dcp:
|
||||
# A request's local rows are its context rows sharded across ranks
|
||||
# and rounded up to the interleave block size, so the local start of
|
||||
# a continuation is the padded row count of the context before it.
|
||||
local_starts = [padded_rows(start) for start in plan.starts]
|
||||
local_seq_lens = [
|
||||
padded_rows(start + length) - local_start
|
||||
for start, length, local_start in zip(
|
||||
plan.starts, plan.seq_lens, local_starts
|
||||
)
|
||||
]
|
||||
local_starts_per_chunk.append(local_starts)
|
||||
local_seq_lens_per_chunk.append(local_seq_lens)
|
||||
padded_local_cu_seq_lens_flat.extend(
|
||||
itertools.accumulate(local_seq_lens, initial=0)
|
||||
)
|
||||
padded_local_token_to_seq_parts.append(
|
||||
np.repeat(np.arange(num_requests, dtype=np.int32), local_seq_lens)
|
||||
)
|
||||
num_local_tokens = sum(local_seq_lens)
|
||||
# The gather takes per-rank local offsets under DCP.
|
||||
starts_flat.extend(local_starts)
|
||||
else:
|
||||
starts_flat.extend(plan.starts)
|
||||
assert num_local_tokens <= row_budget
|
||||
|
||||
layouts.append(
|
||||
(
|
||||
slice(request_offset, request_offset + num_requests),
|
||||
slice(boundary_offset, boundary_offset + num_requests + 1),
|
||||
slice(token_offset, token_offset + num_tokens),
|
||||
slice(local_token_offset, local_token_offset + num_local_tokens),
|
||||
)
|
||||
)
|
||||
request_offset += num_requests
|
||||
boundary_offset += num_requests + 1
|
||||
token_offset += num_tokens
|
||||
local_token_offset += num_local_tokens
|
||||
|
||||
seq_lens_cpu = _flat_int32(seq_lens_flat)
|
||||
starts = _flat_int32(starts_flat).to(device, non_blocking=True)
|
||||
cu_seq_lens = _flat_int32(cu_seq_lens_flat).to(device, non_blocking=True)
|
||||
cu_seqlens_q = _flat_int32(cu_seqlens_q_flat).to(device, non_blocking=True)
|
||||
token_to_seq = _flat_int32(np.concatenate(token_to_seq_parts)).to(
|
||||
device, non_blocking=True
|
||||
)
|
||||
if use_dcp:
|
||||
padded_local_cu_seq_lens = _flat_int32(padded_local_cu_seq_lens_flat).to(
|
||||
device, non_blocking=True
|
||||
)
|
||||
padded_local_token_to_seq = _flat_int32(
|
||||
np.concatenate(padded_local_token_to_seq_parts)
|
||||
).to(device, non_blocking=True)
|
||||
|
||||
chunks: list[MLACommonPrefillMetadata.ContextChunk] = []
|
||||
for index, (plan, layout) in enumerate(zip(plans, layouts)):
|
||||
request_slice, boundary_slice, token_slice, local_token_slice = layout
|
||||
query_lens = [
|
||||
query_start_loc[request + 1] - query_start_loc[request]
|
||||
for request in range(plan.request_start, plan.request_end)
|
||||
]
|
||||
chunk = MLACommonPrefillMetadata.ContextChunk(
|
||||
index=index,
|
||||
request_slice=slice(plan.request_start, plan.request_end),
|
||||
token_slice=slice(
|
||||
query_start_loc[plan.request_start], query_start_loc[plan.request_end]
|
||||
),
|
||||
continuation_token_end=query_start_loc[plan.request_start + 1],
|
||||
is_continuation=plan.is_continuation,
|
||||
num_context_tokens=token_slice.stop - token_slice.start,
|
||||
query_start_loc=cu_seqlens_q[boundary_slice],
|
||||
max_query_len=max(query_lens),
|
||||
cu_seq_lens=cu_seq_lens[boundary_slice],
|
||||
starts=starts[request_slice],
|
||||
max_seq_len=max(plan.seq_lens),
|
||||
seq_lens=seq_lens_cpu[request_slice],
|
||||
token_to_seq=token_to_seq[token_slice],
|
||||
num_local_context_tokens=local_token_slice.stop - local_token_slice.start,
|
||||
)
|
||||
if use_dcp:
|
||||
assert local_context_lens_allranks is not None
|
||||
chunk.padded_local_seq_lens = local_seq_lens_per_chunk[index]
|
||||
chunk.local_context_lens_allranks = local_context_lens_allranks[
|
||||
plan.request_start : plan.request_end
|
||||
]
|
||||
chunk.padded_local_cu_seq_lens = padded_local_cu_seq_lens[boundary_slice]
|
||||
chunk.padded_local_token_to_seq = padded_local_token_to_seq[
|
||||
local_token_slice
|
||||
]
|
||||
chunk.local_starts = local_starts_per_chunk[index]
|
||||
chunks.append(chunk)
|
||||
|
||||
return MLACommonPrefillMetadata.ChunkedContextMetadata(
|
||||
context_lens=context_lens_cpu.to(device, non_blocking=True),
|
||||
workspace=chunked_prefill_workspace,
|
||||
chunks=chunks,
|
||||
context_lens_list=context_lens,
|
||||
empty_token_slices=empty_token_slices,
|
||||
)
|
||||
|
||||
|
||||
class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
|
||||
@@ -1796,13 +1954,7 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
|
||||
64 * 1024,
|
||||
)
|
||||
|
||||
# Enforce that we enough for at least 1 page per request
|
||||
chunked_prefill_workspace_size = max(
|
||||
chunked_prefill_workspace_size,
|
||||
scheduler_config.max_num_seqs * cache_config.block_size,
|
||||
)
|
||||
|
||||
return chunked_prefill_workspace_size
|
||||
return max(chunked_prefill_workspace_size, cache_config.block_size)
|
||||
|
||||
@staticmethod
|
||||
def determine_prefill_query_data_type(
|
||||
@@ -2063,7 +2215,6 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
|
||||
chunked_context_metadata = build_mla_chunked_context_metadata(
|
||||
context_lens_cpu=context_lens_cpu,
|
||||
prefill_query_start_loc_cpu=prefill_query_start_loc_cpu,
|
||||
num_prefills=num_prefills,
|
||||
chunked_prefill_workspace=self.chunked_prefill_workspace,
|
||||
chunked_prefill_workspace_size=self.chunked_prefill_workspace_size,
|
||||
block_size=self.page_size,
|
||||
@@ -2138,10 +2289,9 @@ def reorg_kvcache(
|
||||
allgatered_k_pe: torch.Tensor,
|
||||
padded_local_chunk_seq_lens_lst: list[int],
|
||||
local_context_lens_allranks: list[list[int]],
|
||||
local_starts: list[int],
|
||||
sum_seq_len: int,
|
||||
max_seq_len: int,
|
||||
chunk_size: int,
|
||||
chunk_idx: int,
|
||||
toks: int,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""
|
||||
@@ -2155,19 +2305,18 @@ def reorg_kvcache(
|
||||
padded_local_chunk_seq_lens_lst: local chunk context lengths
|
||||
under current CP rank.
|
||||
local_context_lens_allranks: local context lengths on each CP rank.
|
||||
local_starts: per-request local offset into the context this chunk
|
||||
starts at.
|
||||
sum_seq_len: the sum of cp_chunk_seq_lens_lst.
|
||||
max_seq_len: the max value of cp_chunk_seq_lens_lst.
|
||||
chunk_size: the local padded max context chunk from
|
||||
chunked_context_metadata building.
|
||||
chunk_idx: chunk idx of chunked_prefill.
|
||||
toks: the number of tokens for local gather cache.
|
||||
"""
|
||||
kv_c_segments = []
|
||||
k_pe_segments = []
|
||||
src_token_idx = 0
|
||||
max_seq_len_check = 0
|
||||
for padded_local_chunk_seq_len, local_context_lens in zip(
|
||||
padded_local_chunk_seq_lens_lst, local_context_lens_allranks
|
||||
for padded_local_chunk_seq_len, local_context_lens, local_start in zip(
|
||||
padded_local_chunk_seq_lens_lst, local_context_lens_allranks, local_starts
|
||||
):
|
||||
cur_seq_len = 0
|
||||
for rank, local_context_len in enumerate(local_context_lens):
|
||||
@@ -2179,7 +2328,7 @@ def reorg_kvcache(
|
||||
# local_chunk_len in dcp1: |-----|-----|--|
|
||||
# so we need update the last chunk length in dcp1.
|
||||
local_chunk_len = min(
|
||||
max(0, local_context_len - chunk_idx * chunk_size),
|
||||
max(0, local_context_len - local_start),
|
||||
padded_local_chunk_seq_len,
|
||||
)
|
||||
if local_chunk_len != 0:
|
||||
@@ -2206,6 +2355,66 @@ def reorg_kvcache(
|
||||
return reorganized_kv_c_normed, reorganized_k_pe
|
||||
|
||||
|
||||
def init_mla_context_partial(
|
||||
chunked_context: "MLACommonPrefillMetadata.ChunkedContextMetadata",
|
||||
attn_output: torch.Tensor,
|
||||
attn_softmax_lse: torch.Tensor,
|
||||
num_tokens: int,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Allocate the running context partial over all prefill tokens.
|
||||
|
||||
Laid out like the chunk partials so the final whole-batch merge against the
|
||||
suffix partial sees matching head strides.
|
||||
"""
|
||||
output = torch.empty(
|
||||
(num_tokens, *attn_output.shape[1:]),
|
||||
dtype=attn_output.dtype,
|
||||
device=attn_output.device,
|
||||
)
|
||||
output_lse = torch.empty(
|
||||
(attn_softmax_lse.shape[0], num_tokens),
|
||||
dtype=attn_softmax_lse.dtype,
|
||||
device=attn_softmax_lse.device,
|
||||
)
|
||||
# No chunk covers a prefill without context, so neutralize its partial.
|
||||
for token_slice in chunked_context.empty_token_slices:
|
||||
output[token_slice].zero_()
|
||||
output_lse[:, token_slice].fill_(float("-inf"))
|
||||
return output, output_lse
|
||||
|
||||
|
||||
def accumulate_mla_context_chunk(
|
||||
chunk: "MLACommonPrefillMetadata.ContextChunk",
|
||||
attn_output: torch.Tensor,
|
||||
attn_softmax_lse: torch.Tensor,
|
||||
output: torch.Tensor,
|
||||
output_lse: torch.Tensor,
|
||||
) -> None:
|
||||
"""Fold one chunk's partial into the running context partial.
|
||||
|
||||
Only the first request may be a continuation; its tokens are merged and the
|
||||
remaining token range is initialized.
|
||||
"""
|
||||
token_start = chunk.token_slice.start
|
||||
token_end = chunk.token_slice.stop
|
||||
init_start = token_start
|
||||
if chunk.is_continuation:
|
||||
init_start = chunk.continuation_token_end
|
||||
num_merged = init_start - token_start
|
||||
merge_attn_states(
|
||||
output=output[token_start:init_start],
|
||||
output_lse=output_lse[:, token_start:init_start],
|
||||
prefix_output=output[token_start:init_start],
|
||||
prefix_lse=output_lse[:, token_start:init_start],
|
||||
suffix_output=attn_output[:num_merged],
|
||||
suffix_lse=attn_softmax_lse[:, :num_merged],
|
||||
)
|
||||
if init_start < token_end:
|
||||
written = init_start - token_start
|
||||
output[init_start:token_end].copy_(attn_output[written:])
|
||||
output_lse[:, init_start:token_end].copy_(attn_softmax_lse[:, written:])
|
||||
|
||||
|
||||
class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
|
||||
"""
|
||||
Shared MLA base providing dense-MHA prefill (via the selected
|
||||
@@ -2282,53 +2491,53 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
|
||||
assert attn_metadata.prefill is not None
|
||||
prefill_metadata = attn_metadata.prefill
|
||||
assert prefill_metadata.prefill_backend is not None
|
||||
assert prefill_metadata.chunked_context is not None
|
||||
chunked_context = prefill_metadata.chunked_context
|
||||
assert chunked_context is not None
|
||||
|
||||
use_fp8_prefill = prefill_metadata.q_data_type == current_platform.fp8_dtype()
|
||||
kv_b_proj_input_dtype = _get_kv_b_proj_input_dtype(
|
||||
self.kv_b_proj, use_fp8_prefill
|
||||
)
|
||||
|
||||
output = None
|
||||
merge_output = None
|
||||
iters = len(prefill_metadata.chunked_context.seq_tot)
|
||||
workspace = prefill_metadata.chunked_context.workspace
|
||||
workspace = chunked_context.workspace
|
||||
|
||||
if use_fp8_prefill:
|
||||
q = q.to(prefill_metadata.q_data_type)
|
||||
|
||||
for i in range(iters):
|
||||
toks = prefill_metadata.chunked_context.seq_tot[i]
|
||||
output = None
|
||||
output_lse = None
|
||||
for chunk in chunked_context.chunks:
|
||||
toks = chunk.num_context_tokens
|
||||
block_table = prefill_metadata.block_table[chunk.request_slice]
|
||||
if self.kv_cache_dtype == "fp8_ds_mla":
|
||||
ops.cp_gather_and_upconvert_fp8_kv_cache(
|
||||
src_cache=kv_c_and_k_pe_cache,
|
||||
dst=workspace[:toks],
|
||||
block_table=prefill_metadata.block_table,
|
||||
workspace_starts=prefill_metadata.chunked_context.cu_seq_lens[i],
|
||||
batch_size=attn_metadata.num_prefills,
|
||||
seq_starts=prefill_metadata.chunked_context.starts[i],
|
||||
block_table=block_table,
|
||||
workspace_starts=chunk.cu_seq_lens,
|
||||
batch_size=chunk.num_requests,
|
||||
seq_starts=chunk.starts,
|
||||
)
|
||||
elif not use_fp8_prefill:
|
||||
ops.gather_and_maybe_dequant_cache(
|
||||
src_cache=kv_c_and_k_pe_cache,
|
||||
dst=workspace,
|
||||
block_table=prefill_metadata.block_table,
|
||||
cu_seq_lens=prefill_metadata.chunked_context.cu_seq_lens[i],
|
||||
token_to_seq=prefill_metadata.chunked_context.token_to_seq[i],
|
||||
num_tokens=prefill_metadata.chunked_context.chunk_total_token[i],
|
||||
block_table=block_table,
|
||||
cu_seq_lens=chunk.cu_seq_lens,
|
||||
token_to_seq=chunk.token_to_seq,
|
||||
num_tokens=toks,
|
||||
kv_cache_dtype=self.kv_cache_dtype,
|
||||
scale=k_scale,
|
||||
seq_starts=prefill_metadata.chunked_context.starts[i],
|
||||
seq_starts=chunk.starts,
|
||||
)
|
||||
else:
|
||||
# FP8 path: gather cache without dequantization
|
||||
ops.cp_gather_cache(
|
||||
src_cache=kv_c_and_k_pe_cache,
|
||||
dst=workspace,
|
||||
block_table=prefill_metadata.block_table,
|
||||
cu_seq_lens=prefill_metadata.chunked_context.cu_seq_lens[i],
|
||||
batch_size=attn_metadata.num_prefills,
|
||||
seq_starts=prefill_metadata.chunked_context.starts[i],
|
||||
block_table=block_table,
|
||||
cu_seq_lens=chunk.cu_seq_lens,
|
||||
batch_size=chunk.num_requests,
|
||||
seq_starts=chunk.starts,
|
||||
)
|
||||
|
||||
# Extract kv_c_normed from workspace
|
||||
@@ -2351,37 +2560,28 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
|
||||
|
||||
attn_output, attn_softmax_lse = (
|
||||
prefill_metadata.prefill_backend.run_prefill_context_chunk(
|
||||
chunk_idx=i,
|
||||
q=q,
|
||||
chunk=chunk,
|
||||
q=q[chunk.token_slice],
|
||||
k=k,
|
||||
v=v,
|
||||
)
|
||||
)
|
||||
if prefill_metadata.chunked_context.has_empty_context[i]:
|
||||
mask_empty_context(
|
||||
attn_softmax_lse,
|
||||
attn_output,
|
||||
prefill_metadata.query_start_loc,
|
||||
prefill_metadata.chunked_context.cu_seq_lens[i],
|
||||
)
|
||||
|
||||
if output is None:
|
||||
output = attn_output
|
||||
output_lse = attn_softmax_lse
|
||||
else:
|
||||
if merge_output is None:
|
||||
merge_output = torch.empty_like(output)
|
||||
merge_output_lse = torch.empty_like(output_lse)
|
||||
merge_attn_states(
|
||||
output=merge_output,
|
||||
output_lse=merge_output_lse,
|
||||
prefix_output=output,
|
||||
prefix_lse=output_lse,
|
||||
suffix_output=attn_output,
|
||||
suffix_lse=attn_softmax_lse,
|
||||
if (
|
||||
len(chunked_context.chunks) == 1
|
||||
and not chunked_context.empty_token_slices
|
||||
):
|
||||
return attn_output, attn_softmax_lse
|
||||
output, output_lse = init_mla_context_partial(
|
||||
chunked_context,
|
||||
attn_output,
|
||||
attn_softmax_lse,
|
||||
num_tokens=q.shape[0],
|
||||
)
|
||||
output, merge_output = merge_output, output
|
||||
output_lse, merge_output_lse = merge_output_lse, output_lse
|
||||
accumulate_mla_context_chunk(
|
||||
chunk, attn_output, attn_softmax_lse, output, output_lse
|
||||
)
|
||||
|
||||
return output, output_lse
|
||||
|
||||
@@ -2396,62 +2596,57 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
|
||||
assert attn_metadata.prefill is not None
|
||||
prefill_metadata = attn_metadata.prefill
|
||||
assert prefill_metadata.prefill_backend is not None
|
||||
assert prefill_metadata.chunked_context is not None
|
||||
assert prefill_metadata.chunked_context.padded_local_chunk_seq_lens is not None
|
||||
assert prefill_metadata.chunked_context.local_context_lens_allranks is not None
|
||||
assert prefill_metadata.chunked_context.padded_local_cu_seq_lens is not None
|
||||
assert prefill_metadata.chunked_context.padded_local_token_to_seq is not None
|
||||
assert prefill_metadata.chunked_context.cu_seq_lens_lst is not None
|
||||
assert prefill_metadata.chunked_context.chunk_size is not None
|
||||
chunked_context = prefill_metadata.chunked_context
|
||||
assert chunked_context is not None
|
||||
|
||||
use_fp8_prefill = prefill_metadata.q_data_type == current_platform.fp8_dtype()
|
||||
kv_b_proj_input_dtype = _get_kv_b_proj_input_dtype(
|
||||
self.kv_b_proj, use_fp8_prefill
|
||||
)
|
||||
output = None
|
||||
merge_output = None
|
||||
iters = len(prefill_metadata.chunked_context.seq_tot)
|
||||
workspace = prefill_metadata.chunked_context.workspace
|
||||
output_lse = None
|
||||
workspace = chunked_context.workspace
|
||||
|
||||
for i in range(iters):
|
||||
toks = prefill_metadata.chunked_context.seq_tot[i]
|
||||
if toks == 0:
|
||||
continue
|
||||
padded_local_cu_seq_lens = (
|
||||
prefill_metadata.chunked_context.padded_local_cu_seq_lens[i]
|
||||
)
|
||||
for chunk in chunked_context.chunks:
|
||||
assert chunk.padded_local_seq_lens is not None
|
||||
assert chunk.local_context_lens_allranks is not None
|
||||
assert chunk.padded_local_cu_seq_lens is not None
|
||||
assert chunk.padded_local_token_to_seq is not None
|
||||
assert chunk.local_starts is not None
|
||||
|
||||
toks = chunk.num_local_context_tokens
|
||||
padded_local_cu_seq_lens = chunk.padded_local_cu_seq_lens
|
||||
block_table = prefill_metadata.block_table[chunk.request_slice]
|
||||
if self.kv_cache_dtype == "fp8_ds_mla":
|
||||
ops.cp_gather_and_upconvert_fp8_kv_cache(
|
||||
src_cache=kv_c_and_k_pe_cache,
|
||||
dst=workspace[:toks],
|
||||
block_table=prefill_metadata.block_table,
|
||||
block_table=block_table,
|
||||
workspace_starts=padded_local_cu_seq_lens,
|
||||
batch_size=attn_metadata.num_prefills,
|
||||
seq_starts=prefill_metadata.chunked_context.starts[i],
|
||||
batch_size=chunk.num_requests,
|
||||
seq_starts=chunk.starts,
|
||||
)
|
||||
elif is_quantized_kv_cache(self.kv_cache_dtype):
|
||||
assert k_scale is not None
|
||||
ops.gather_and_maybe_dequant_cache(
|
||||
src_cache=kv_c_and_k_pe_cache,
|
||||
dst=workspace,
|
||||
block_table=prefill_metadata.block_table,
|
||||
block_table=block_table,
|
||||
cu_seq_lens=padded_local_cu_seq_lens,
|
||||
token_to_seq=prefill_metadata.chunked_context.padded_local_token_to_seq[
|
||||
i
|
||||
],
|
||||
token_to_seq=chunk.padded_local_token_to_seq,
|
||||
num_tokens=toks,
|
||||
kv_cache_dtype=self.kv_cache_dtype,
|
||||
scale=k_scale,
|
||||
seq_starts=prefill_metadata.chunked_context.starts[i],
|
||||
seq_starts=chunk.starts,
|
||||
)
|
||||
else:
|
||||
ops.cp_gather_cache(
|
||||
src_cache=kv_c_and_k_pe_cache,
|
||||
dst=workspace,
|
||||
block_table=prefill_metadata.block_table,
|
||||
block_table=block_table,
|
||||
cu_seq_lens=padded_local_cu_seq_lens,
|
||||
batch_size=attn_metadata.num_prefills,
|
||||
seq_starts=prefill_metadata.chunked_context.starts[i],
|
||||
batch_size=chunk.num_requests,
|
||||
seq_starts=chunk.starts,
|
||||
)
|
||||
# workspace
|
||||
# |------- N tokens --------|--------- N*dcp_size tokens ----------|
|
||||
@@ -2479,14 +2674,11 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
|
||||
kv_c_normed, k_pe = reorg_kvcache(
|
||||
allgatered_kv_c_normed,
|
||||
allgatered_k_pe,
|
||||
padded_local_chunk_seq_lens_lst=prefill_metadata.chunked_context.padded_local_chunk_seq_lens[
|
||||
i
|
||||
],
|
||||
local_context_lens_allranks=prefill_metadata.chunked_context.local_context_lens_allranks,
|
||||
sum_seq_len=prefill_metadata.chunked_context.cu_seq_lens_lst[i][-1],
|
||||
max_seq_len=prefill_metadata.chunked_context.max_seq_lens[i],
|
||||
chunk_size=prefill_metadata.chunked_context.chunk_size,
|
||||
chunk_idx=i,
|
||||
padded_local_chunk_seq_lens_lst=chunk.padded_local_seq_lens,
|
||||
local_context_lens_allranks=chunk.local_context_lens_allranks,
|
||||
local_starts=chunk.local_starts,
|
||||
sum_seq_len=chunk.num_context_tokens,
|
||||
max_seq_len=chunk.max_seq_len,
|
||||
toks=toks,
|
||||
)
|
||||
if kv_b_proj_input_dtype is not None:
|
||||
@@ -2503,37 +2695,28 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
|
||||
|
||||
attn_output, attn_softmax_lse = (
|
||||
prefill_metadata.prefill_backend.run_prefill_context_chunk(
|
||||
chunk_idx=i,
|
||||
q=q,
|
||||
chunk=chunk,
|
||||
q=q[chunk.token_slice],
|
||||
k=k,
|
||||
v=v,
|
||||
)
|
||||
)
|
||||
if prefill_metadata.chunked_context.has_empty_context[i]:
|
||||
mask_empty_context(
|
||||
attn_softmax_lse,
|
||||
attn_output,
|
||||
prefill_metadata.query_start_loc,
|
||||
prefill_metadata.chunked_context.cu_seq_lens[i],
|
||||
)
|
||||
|
||||
if output is None:
|
||||
output = attn_output
|
||||
output_lse = attn_softmax_lse
|
||||
else:
|
||||
if merge_output is None:
|
||||
merge_output = torch.empty_like(output)
|
||||
merge_output_lse = torch.empty_like(output_lse)
|
||||
merge_attn_states(
|
||||
output=merge_output,
|
||||
output_lse=merge_output_lse,
|
||||
prefix_output=output,
|
||||
prefix_lse=output_lse,
|
||||
suffix_output=attn_output,
|
||||
suffix_lse=attn_softmax_lse,
|
||||
if (
|
||||
len(chunked_context.chunks) == 1
|
||||
and not chunked_context.empty_token_slices
|
||||
):
|
||||
return attn_output, attn_softmax_lse
|
||||
output, output_lse = init_mla_context_partial(
|
||||
chunked_context,
|
||||
attn_output,
|
||||
attn_softmax_lse,
|
||||
num_tokens=q.shape[0],
|
||||
)
|
||||
output, merge_output = merge_output, output
|
||||
output_lse, merge_output_lse = merge_output_lse, output_lse
|
||||
accumulate_mla_context_chunk(
|
||||
chunk, attn_output, attn_softmax_lse, output, output_lse
|
||||
)
|
||||
|
||||
return output, output_lse
|
||||
|
||||
@@ -2615,7 +2798,6 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
|
||||
prefix_lse=context_lse,
|
||||
suffix_output=suffix_output,
|
||||
suffix_lse=suffix_lse,
|
||||
prefill_tokens_with_context=prefill_metadata.chunked_context.prefill_tokens_with_context,
|
||||
)
|
||||
elif output_scale is None:
|
||||
# With output_scale set, backend already wrote into `output` in place.
|
||||
|
||||
@@ -19,8 +19,10 @@ from vllm.model_executor.layers.attention.mla_attention import (
|
||||
MLACommonBaseImpl,
|
||||
MLACommonMetadata,
|
||||
MLACommonPrefillMetadata,
|
||||
accumulate_mla_context_chunk,
|
||||
build_mla_chunked_context_metadata,
|
||||
get_mla_dims,
|
||||
init_mla_context_partial,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.triton_utils import tl, triton
|
||||
@@ -60,17 +62,14 @@ def _topk_mask_shape(
|
||||
def _masked_mha_workspace_fits(
|
||||
batch_size: int,
|
||||
max_query_len: int,
|
||||
context_chunk_max_seq_lens: list[int] | None,
|
||||
max_context_chunk_seq_len: int,
|
||||
workspace_numel: int,
|
||||
) -> bool:
|
||||
"""Return whether the suffix and per-context-chunk masks fit the workspace.
|
||||
|
||||
The global mask is excluded: it always needs more, and has its own check.
|
||||
"""
|
||||
max_key_len = max(
|
||||
max_query_len,
|
||||
max(context_chunk_max_seq_lens or (), default=0),
|
||||
)
|
||||
max_key_len = max(max_query_len, max_context_chunk_seq_len)
|
||||
needed = math.prod(_topk_mask_shape(batch_size, max_query_len, max_key_len))
|
||||
return needed <= workspace_numel
|
||||
|
||||
@@ -185,10 +184,7 @@ class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
|
||||
64 * 1024,
|
||||
scheduler_config.max_num_seqs * topk_tokens,
|
||||
)
|
||||
return max(
|
||||
workspace_size,
|
||||
scheduler_config.max_num_seqs * cache_config.block_size,
|
||||
)
|
||||
return max(workspace_size, cache_config.block_size)
|
||||
|
||||
def _build_req_id_per_token(
|
||||
self,
|
||||
@@ -228,7 +224,6 @@ class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
|
||||
return build_mla_chunked_context_metadata(
|
||||
context_lens_cpu=context_lens_cpu,
|
||||
prefill_query_start_loc_cpu=prefill_query_start_loc_cpu,
|
||||
num_prefills=num_prefills,
|
||||
chunked_prefill_workspace=self.chunked_prefill_workspace,
|
||||
chunked_prefill_workspace_size=self.chunked_prefill_workspace_size,
|
||||
block_size=self.kv_cache_spec.block_size,
|
||||
@@ -542,14 +537,15 @@ class SparseMLACommonImpl(MLACommonBaseImpl[T], Generic[T]):
|
||||
workspace = prefill.topk_mask_workspace
|
||||
if workspace is None or prefill.query_lens_cpu is None:
|
||||
return False
|
||||
max_context_chunk_seq_len = 0
|
||||
if prefill.chunked_context is not None:
|
||||
max_context_chunk_seq_len = max(
|
||||
chunk.max_seq_len for chunk in prefill.chunked_context.chunks
|
||||
)
|
||||
fits = _masked_mha_workspace_fits(
|
||||
batch_size=len(prefill.query_lens_cpu),
|
||||
max_query_len=prefill.max_query_len,
|
||||
context_chunk_max_seq_lens=(
|
||||
None
|
||||
if prefill.chunked_context is None
|
||||
else prefill.chunked_context.max_seq_lens
|
||||
),
|
||||
max_context_chunk_seq_len=max_context_chunk_seq_len,
|
||||
workspace_numel=workspace.numel(),
|
||||
)
|
||||
if not fits:
|
||||
@@ -716,69 +712,70 @@ class SparseMLACommonImpl(MLACommonBaseImpl[T], Generic[T]):
|
||||
|
||||
chunked_context = prefill_metadata.chunked_context
|
||||
assert chunked_context is not None
|
||||
use_global_mask = dense_mask is not None
|
||||
output: torch.Tensor | None = None
|
||||
output_lse: torch.Tensor | None = None
|
||||
workspace = chunked_context.workspace
|
||||
|
||||
for i, toks in enumerate(chunked_context.seq_tot):
|
||||
if toks == 0:
|
||||
continue
|
||||
for chunk in chunked_context.chunks:
|
||||
toks = chunk.num_context_tokens
|
||||
requests = chunk.request_slice
|
||||
ops.gather_and_maybe_dequant_cache(
|
||||
src_cache=kv_c_and_k_pe_cache,
|
||||
dst=workspace,
|
||||
block_table=prefill_metadata.block_table,
|
||||
cu_seq_lens=chunked_context.cu_seq_lens[i],
|
||||
token_to_seq=chunked_context.token_to_seq[i],
|
||||
num_tokens=chunked_context.chunk_total_token[i],
|
||||
block_table=prefill_metadata.block_table[requests],
|
||||
cu_seq_lens=chunk.cu_seq_lens,
|
||||
token_to_seq=chunk.token_to_seq,
|
||||
num_tokens=toks,
|
||||
kv_cache_dtype=self.kv_cache_dtype,
|
||||
scale=k_scale,
|
||||
seq_starts=chunked_context.starts[i],
|
||||
seq_starts=chunk.starts,
|
||||
)
|
||||
|
||||
chunk_kv_c = workspace[:toks, : self.kv_lora_rank]
|
||||
chunk_k_pe = workspace[:toks, self.kv_lora_rank :].unsqueeze(1)
|
||||
k, v = self._project_kv(chunk_kv_c, chunk_k_pe)
|
||||
chunk_lens = chunked_context.seq_lens[i].tolist()
|
||||
chunk_topk = (
|
||||
topk_per_req
|
||||
if use_global_mask
|
||||
else self._remap_topk_to_ranges(
|
||||
topk_per_req,
|
||||
chunked_context.starts[i],
|
||||
chunk_lens,
|
||||
if dense_mask is not None:
|
||||
chunk_mask: torch.Tensor | None = dense_mask[requests]
|
||||
chunk_topk = topk_per_req[requests]
|
||||
key_starts: torch.Tensor | None = chunk.starts
|
||||
else:
|
||||
chunk_mask = None
|
||||
chunk_topk = self._remap_topk_to_ranges(
|
||||
topk_per_req[requests],
|
||||
chunk.starts,
|
||||
chunk.seq_lens.tolist(),
|
||||
)
|
||||
)
|
||||
key_starts = None
|
||||
attn_out, lse = self._run_masked_mha(
|
||||
q=q,
|
||||
q=q[chunk.token_slice],
|
||||
k=k,
|
||||
v=v,
|
||||
cu_seqlens_q=prefill_metadata.query_start_loc,
|
||||
cu_seqlens_k=chunked_context.cu_seq_lens[i],
|
||||
max_seqlen_q=prefill_metadata.max_query_len,
|
||||
max_seqlen_k=chunked_context.max_seq_lens[i],
|
||||
cu_seqlens_q=chunk.query_start_loc,
|
||||
cu_seqlens_k=chunk.cu_seq_lens,
|
||||
max_seqlen_q=chunk.max_query_len,
|
||||
max_seqlen_k=chunk.max_seq_len,
|
||||
topk_per_req=chunk_topk,
|
||||
q_lens=q_lens,
|
||||
q_lens=q_lens[requests],
|
||||
causal=False,
|
||||
return_softmax_lse=True,
|
||||
dense_mask=dense_mask,
|
||||
key_starts=(chunked_context.starts[i] if use_global_mask else None),
|
||||
dense_mask=chunk_mask,
|
||||
key_starts=key_starts,
|
||||
topk_mask_workspace=prefill_metadata.topk_mask_workspace,
|
||||
)
|
||||
|
||||
if output is None:
|
||||
output = attn_out
|
||||
output_lse = lse
|
||||
else:
|
||||
assert output_lse is not None
|
||||
merge_attn_states(
|
||||
output=output,
|
||||
output_lse=output_lse,
|
||||
prefix_output=output,
|
||||
prefix_lse=output_lse,
|
||||
suffix_output=attn_out,
|
||||
suffix_lse=lse,
|
||||
if (
|
||||
len(chunked_context.chunks) == 1
|
||||
and not chunked_context.empty_token_slices
|
||||
):
|
||||
return attn_out, lse
|
||||
output, output_lse = init_mla_context_partial(
|
||||
chunked_context,
|
||||
attn_out,
|
||||
lse,
|
||||
num_tokens=q.shape[0],
|
||||
)
|
||||
accumulate_mla_context_chunk(chunk, attn_out, lse, output, output_lse)
|
||||
|
||||
assert output is not None and output_lse is not None
|
||||
return output, output_lse
|
||||
@@ -844,7 +841,7 @@ class SparseMLACommonImpl(MLACommonBaseImpl[T], Generic[T]):
|
||||
output.copy_(attn_out[..., : self.v_head_dim].flatten(start_dim=-2))
|
||||
return
|
||||
|
||||
context_lens = chunked_context.seq_lens.sum(dim=0).tolist()
|
||||
context_lens = chunked_context.context_lens_list
|
||||
dense_mask = self._try_build_global_mask(
|
||||
topk_per_req,
|
||||
q_lens,
|
||||
@@ -889,5 +886,4 @@ class SparseMLACommonImpl(MLACommonBaseImpl[T], Generic[T]):
|
||||
prefix_lse=context_lse,
|
||||
suffix_output=suffix_output[..., : self.v_head_dim],
|
||||
suffix_lse=suffix_lse,
|
||||
prefill_tokens_with_context=chunked_context.prefill_tokens_with_context,
|
||||
)
|
||||
|
||||
@@ -763,7 +763,6 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
|
||||
prefix_lse=context_lse,
|
||||
suffix_output=suffix_output[..., : self.v_head_dim],
|
||||
suffix_lse=suffix_lse,
|
||||
prefill_tokens_with_context=prefill.chunked_context.prefill_tokens_with_context,
|
||||
)
|
||||
elif not writes_out:
|
||||
out.copy_(output_prefill[..., : self.v_head_dim].flatten(start_dim=-2))
|
||||
|
||||
@@ -17,6 +17,9 @@ from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.model_executor.layers.attention.mla_attention import (
|
||||
MLACommonPrefillMetadata,
|
||||
)
|
||||
from vllm.platforms.interface import DeviceCapability
|
||||
|
||||
|
||||
@@ -99,21 +102,19 @@ class AiterFlashAttnPrefillBackend(MLAPrefillBackend):
|
||||
|
||||
def run_prefill_context_chunk(
|
||||
self,
|
||||
chunk_idx: int,
|
||||
chunk: "MLACommonPrefillMetadata.ContextChunk",
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
assert self._prefill_metadata.chunked_context is not None
|
||||
chunked = self._prefill_metadata.chunked_context
|
||||
out, lse = self.flash_attn_varlen_func(
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
cu_seqlens_q=self._prefill_metadata.query_start_loc,
|
||||
cu_seqlens_k=chunked.cu_seq_lens[chunk_idx],
|
||||
max_seqlen_q=self._prefill_metadata.max_query_len,
|
||||
max_seqlen_k=chunked.max_seq_lens[chunk_idx],
|
||||
cu_seqlens_q=chunk.query_start_loc,
|
||||
cu_seqlens_k=chunk.cu_seq_lens,
|
||||
max_seqlen_q=chunk.max_query_len,
|
||||
max_seqlen_k=chunk.max_seq_len,
|
||||
softmax_scale=self.scale,
|
||||
causal=False,
|
||||
return_lse=True,
|
||||
|
||||
@@ -175,7 +175,7 @@ class MLAPrefillBackend(ABC):
|
||||
@abstractmethod
|
||||
def run_prefill_context_chunk(
|
||||
self,
|
||||
chunk_idx: int,
|
||||
chunk: "MLACommonPrefillMetadata.ContextChunk",
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
|
||||
@@ -27,7 +27,10 @@ from vllm.v1.attention.backends.mla.prefill.base import (
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.model_executor.layers.attention.mla_attention import MLADims
|
||||
from vllm.model_executor.layers.attention.mla_attention import (
|
||||
MLACommonPrefillMetadata,
|
||||
MLADims,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import QuantKey
|
||||
|
||||
if is_flash_attn_varlen_func_available():
|
||||
@@ -450,20 +453,19 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
|
||||
|
||||
def run_prefill_context_chunk(
|
||||
self,
|
||||
chunk_idx: int,
|
||||
chunk: "MLACommonPrefillMetadata.ContextChunk",
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
assert self._prefill_metadata.chunked_context is not None
|
||||
return self._flash_attn_varlen_diff_headdims(
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
cu_seqlens_q=self._prefill_metadata.query_start_loc,
|
||||
cu_seqlens_k=self._prefill_metadata.chunked_context.cu_seq_lens[chunk_idx],
|
||||
max_seqlen_q=self._prefill_metadata.max_query_len,
|
||||
max_seqlen_k=self._prefill_metadata.chunked_context.max_seq_lens[chunk_idx],
|
||||
cu_seqlens_q=chunk.query_start_loc,
|
||||
cu_seqlens_k=chunk.cu_seq_lens,
|
||||
max_seqlen_q=chunk.max_query_len,
|
||||
max_seqlen_k=chunk.max_seq_len,
|
||||
softmax_scale=self.scale,
|
||||
causal=False, # Context is unmasked
|
||||
return_softmax_lse=True,
|
||||
|
||||
@@ -150,8 +150,7 @@ class FlashInferPrefillBackend(MLAPrefillBackend):
|
||||
if has_context:
|
||||
chunked_context = prefill_metadata.chunked_context
|
||||
assert chunked_context is not None
|
||||
num_chunks = chunked_context.cu_seq_lens.shape[0]
|
||||
self._ensure_chunks(num_chunks, self._workspace_buffer)
|
||||
self._ensure_chunks(len(chunked_context.chunks), self._workspace_buffer)
|
||||
|
||||
num_qo_heads = self.num_heads
|
||||
num_kv_heads = num_qo_heads
|
||||
@@ -179,12 +178,10 @@ class FlashInferPrefillBackend(MLAPrefillBackend):
|
||||
if has_context:
|
||||
chunked_context = prefill_metadata.chunked_context
|
||||
assert chunked_context is not None
|
||||
for i in range(num_chunks):
|
||||
kv_indptr_chunk = chunked_context.cu_seq_lens[i]
|
||||
|
||||
self._prefill_chunks[i].plan(
|
||||
qo_indptr=qo_indptr,
|
||||
kv_indptr=kv_indptr_chunk,
|
||||
for chunk in chunked_context.chunks:
|
||||
self._prefill_chunks[chunk.index].plan(
|
||||
qo_indptr=chunk.query_start_loc,
|
||||
kv_indptr=chunk.cu_seq_lens,
|
||||
num_qo_heads=num_qo_heads,
|
||||
num_kv_heads=num_kv_heads,
|
||||
head_dim_qk=head_dim_qk,
|
||||
@@ -227,12 +224,12 @@ class FlashInferPrefillBackend(MLAPrefillBackend):
|
||||
|
||||
def run_prefill_context_chunk(
|
||||
self,
|
||||
chunk_idx: int,
|
||||
chunk: "MLACommonPrefillMetadata.ContextChunk",
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
attn_out, lse = self._prefill_chunks[chunk_idx].run(
|
||||
attn_out, lse = self._prefill_chunks[chunk.index].run(
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
|
||||
@@ -162,16 +162,13 @@ class TokenspeedMLAPrefillBackend(MLAPrefillBackend):
|
||||
|
||||
def run_prefill_context_chunk(
|
||||
self,
|
||||
chunk_idx: int,
|
||||
chunk: "MLACommonPrefillMetadata.ContextChunk",
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
from tokenspeed_mla import tokenspeed_mla_prefill
|
||||
|
||||
assert self._prefill_metadata.chunked_context is not None
|
||||
chunked = self._prefill_metadata.chunked_context
|
||||
|
||||
# See note in run_prefill_new_tokens — `v` is a split-view of `kv_nope`
|
||||
# in `_compute_prefill_context` and arrives non-contiguous.
|
||||
v = v.contiguous()
|
||||
@@ -180,15 +177,15 @@ class TokenspeedMLAPrefillBackend(MLAPrefillBackend):
|
||||
query=q,
|
||||
key=k,
|
||||
value=v,
|
||||
seq_lens=chunked.seq_lens[chunk_idx],
|
||||
cum_seq_lens=chunked.cu_seq_lens[chunk_idx],
|
||||
max_seq_len=chunked.max_seq_lens[chunk_idx],
|
||||
batch_size=chunked.seq_lens[chunk_idx].shape[0],
|
||||
seq_lens=chunk.seq_lens,
|
||||
cum_seq_lens=chunk.cu_seq_lens,
|
||||
max_seq_len=chunk.max_seq_len,
|
||||
batch_size=chunk.num_requests,
|
||||
softmax_scale=self.scale,
|
||||
is_causal=False,
|
||||
return_lse=True,
|
||||
cum_seq_lens_q=self._prefill_metadata.query_start_loc,
|
||||
max_seq_len_q=self._prefill_metadata.max_query_len,
|
||||
cum_seq_lens_q=chunk.query_start_loc,
|
||||
max_seq_len_q=chunk.max_query_len,
|
||||
enable_pdl=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -143,16 +143,13 @@ class TrtllmRaggedPrefillBackend(MLAPrefillBackend):
|
||||
|
||||
def run_prefill_context_chunk(
|
||||
self,
|
||||
chunk_idx: int,
|
||||
chunk: "MLACommonPrefillMetadata.ContextChunk",
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
from flashinfer.prefill import trtllm_ragged_attention_deepseek
|
||||
|
||||
assert self._prefill_metadata.chunked_context is not None
|
||||
assert self._prefill_metadata.chunked_context.seq_lens[chunk_idx] is not None
|
||||
|
||||
out = torch.empty(
|
||||
q.shape[0],
|
||||
q.shape[1],
|
||||
@@ -166,20 +163,16 @@ class TrtllmRaggedPrefillBackend(MLAPrefillBackend):
|
||||
key=k,
|
||||
value=v,
|
||||
workspace_buffer=self._workspace_buffer,
|
||||
seq_lens=self._prefill_metadata.chunked_context.seq_lens[chunk_idx],
|
||||
max_q_len=self._prefill_metadata.max_query_len,
|
||||
max_kv_len=self._prefill_metadata.chunked_context.max_seq_lens[chunk_idx],
|
||||
seq_lens=chunk.seq_lens,
|
||||
max_q_len=chunk.max_query_len,
|
||||
max_kv_len=chunk.max_seq_len,
|
||||
bmm1_scale=self.scale,
|
||||
bmm2_scale=1.0,
|
||||
o_sf_scale=1.0,
|
||||
batch_size=self._prefill_metadata.chunked_context.seq_lens[chunk_idx].shape[
|
||||
0
|
||||
],
|
||||
batch_size=chunk.num_requests,
|
||||
window_left=-1,
|
||||
cum_seq_lens_q=self._prefill_metadata.query_start_loc,
|
||||
cum_seq_lens_kv=self._prefill_metadata.chunked_context.cu_seq_lens[
|
||||
chunk_idx
|
||||
],
|
||||
cum_seq_lens_q=chunk.query_start_loc,
|
||||
cum_seq_lens_kv=chunk.cu_seq_lens,
|
||||
enable_pdl=False,
|
||||
is_causal=False,
|
||||
return_lse=True,
|
||||
|
||||
@@ -9,118 +9,6 @@ from vllm.triton_utils import tl, triton
|
||||
float8_info = torch.finfo(current_platform.fp8_dtype())
|
||||
|
||||
|
||||
def mask_empty_context(
|
||||
lse: torch.Tensor,
|
||||
output: torch.Tensor,
|
||||
query_start_loc: torch.Tensor,
|
||||
context_start_loc: torch.Tensor,
|
||||
) -> None:
|
||||
"""Neutralize context chunks that cover no keys before merging.
|
||||
|
||||
A prefill query whose context chunk is empty attended to no keys, so its
|
||||
partial attention is undefined: the backend leaves the output rows as
|
||||
uninitialized scratch (which may hold NaN/Inf) even when it reports an LSE
|
||||
of -inf. Sanitize both here so ``merge_attn_states`` can stay generic:
|
||||
force the LSE to -inf (zero softmax weight) and zero the undefined output
|
||||
rows (so a zero weight cannot combine with NaN/Inf). Emptiness is derived
|
||||
from the context offsets, not from the -inf LSE, so no merge kernel has to
|
||||
reason about undefined partials.
|
||||
|
||||
Args:
|
||||
lse: Chunk log-sum-exp, shape [num_heads, num_tokens].
|
||||
output: Chunk attention output, shape [num_tokens, num_heads, ...].
|
||||
query_start_loc: Prefill query cumulative offsets, shape [num_reqs + 1].
|
||||
context_start_loc: Chunk context cumulative offsets,
|
||||
shape [num_reqs + 1]; an empty chunk has a zero-length span.
|
||||
"""
|
||||
num_heads, num_tokens = lse.shape
|
||||
num_reqs = query_start_loc.shape[0] - 1
|
||||
block_size = 128
|
||||
# Reserve the worst-case number of request-local blocks.
|
||||
num_query_blocks = num_tokens // block_size + num_reqs
|
||||
is_empty = torch.zeros(num_tokens, dtype=torch.bool, device=lse.device)
|
||||
mask_empty_context_kernel[(num_query_blocks,)](
|
||||
lse,
|
||||
is_empty,
|
||||
query_start_loc,
|
||||
context_start_loc,
|
||||
lse.stride(0),
|
||||
lse.stride(1),
|
||||
num_reqs,
|
||||
NUM_HEADS=num_heads,
|
||||
BLOCK_SIZE=block_size,
|
||||
BLOCK_HEADS=8,
|
||||
num_warps=8,
|
||||
)
|
||||
output.masked_fill_(is_empty[:, None, None], 0.0)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def mask_empty_context_kernel(
|
||||
lse,
|
||||
is_empty,
|
||||
query_start_loc,
|
||||
context_start_loc,
|
||||
lse_head_stride,
|
||||
lse_token_stride,
|
||||
num_reqs,
|
||||
NUM_HEADS: tl.constexpr,
|
||||
BLOCK_SIZE: tl.constexpr,
|
||||
BLOCK_HEADS: tl.constexpr,
|
||||
):
|
||||
query_block_idx = tl.program_id(0)
|
||||
|
||||
lanes = tl.arange(0, 32)
|
||||
chunk_start = 0
|
||||
req_idx = 0
|
||||
req_idx_found = False
|
||||
while (chunk_start < num_reqs) & (not req_idx_found):
|
||||
req_offsets = chunk_start + lanes
|
||||
req_mask = req_offsets < num_reqs
|
||||
query_starts = tl.load(query_start_loc + req_offsets, mask=req_mask)
|
||||
# Assume the worst-case number of blocks for each request.
|
||||
req_block_starts = query_starts // BLOCK_SIZE + req_offsets
|
||||
matched_idx = tl.sum(
|
||||
(req_mask & (req_block_starts <= query_block_idx)).to(tl.int32)
|
||||
)
|
||||
# matched_idx == 32 means the match is past this warp chunk.
|
||||
req_idx = chunk_start + matched_idx - 1
|
||||
req_idx_found = matched_idx < 32
|
||||
chunk_start += 32
|
||||
|
||||
query_start = tl.load(query_start_loc + req_idx)
|
||||
query_end = tl.load(query_start_loc + req_idx + 1)
|
||||
query_len = query_end - query_start
|
||||
req_first_block = query_start // BLOCK_SIZE + req_idx
|
||||
block_in_req = query_block_idx - req_first_block
|
||||
token_offset = block_in_req * BLOCK_SIZE
|
||||
if token_offset >= query_len:
|
||||
return
|
||||
|
||||
context_start = tl.load(context_start_loc + req_idx)
|
||||
context_end = tl.load(context_start_loc + req_idx + 1)
|
||||
if context_start != context_end:
|
||||
return
|
||||
|
||||
token_offsets = token_offset + tl.arange(0, BLOCK_SIZE)
|
||||
token_indices = query_start + token_offsets
|
||||
token_lse_offsets = token_indices * lse_token_stride
|
||||
valid_tokens = token_offsets < query_len
|
||||
tl.store(is_empty + token_indices, True, mask=valid_tokens)
|
||||
head_offsets = tl.arange(0, BLOCK_HEADS)
|
||||
for head_start in range(0, NUM_HEADS, BLOCK_HEADS):
|
||||
head_indices = head_start + head_offsets
|
||||
lse_ptrs = (
|
||||
lse + head_indices[:, None] * lse_head_stride + token_lse_offsets[None, :]
|
||||
)
|
||||
valid_heads = head_indices < NUM_HEADS
|
||||
tl.store(
|
||||
lse_ptrs,
|
||||
float("-inf"),
|
||||
mask=valid_heads[:, None] & valid_tokens[None, :],
|
||||
)
|
||||
|
||||
|
||||
# Implements section 2.2 of https://www.arxiv.org/pdf/2501.01005
|
||||
# can be used to combine partial attention results (in the split-KV case)
|
||||
def merge_attn_states(
|
||||
@@ -317,8 +205,7 @@ def merge_attn_states_kernel(
|
||||
s_scale = s_se / out_se
|
||||
out = p_out * p_scale + s_out * s_scale
|
||||
# If both sides are empty (max_lse == -inf) the scales are 0/0 = NaN; emit
|
||||
# zeros rather than NaN. Callers with empty chunks (see mask_empty_context)
|
||||
# zero those inputs, so this only guards the fully-undefined corner.
|
||||
# zeros rather than NaN.
|
||||
out = tl.where(max_lse == float("-inf"), 0.0, out)
|
||||
|
||||
if USE_FP8:
|
||||
|
||||
Reference in New Issue
Block a user