@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / f6bb18fd9a
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 15, 21:00 UTC

[BugFix] MLA + V1, illegal memory access and accuracy issues (#14253)

Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>

Lucas Wilkinson committed Mar 5, 2025 at 20:10 UTC f6bb18fd9a19e5e4fb1991339638fc666d06b27a
7 files changed +334 -161
tests/v1/worker/test_gpu_input_batch.py
+89 -1
@@ -1,5 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2
3 +import inspect
4 from typing import Optional
5
6 import numpy as np
@@ -9,7 +10,8 @@ import torch
10 from vllm.sampling_params import SamplingParams
11 from vllm.utils import is_pin_memory_available, make_tensor_with_pad
12 from vllm.v1.sample.metadata import SamplingMetadata
12 -from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch
13 +from vllm.v1.worker.gpu_input_batch import (BlockTable, CachedRequestState,
14 + InputBatch)
15
16 VOCAB_SIZE = 1024
17 NUM_OUTPUT_TOKENS = 20
@@ -20,6 +22,34 @@ CUDA_DEVICES = [
22 MAX_NUM_PROMPT_TOKENS = 64
23
24
25 +def _compare_objs(obj1, obj2):
26 + attrs = inspect.getmembers(obj1, lambda a: not (inspect.isroutine(a)))
27 + attr_names = set([
28 + a[0] for a in attrs
29 + if not (a[0].startswith('__') and a[0].endswith('__'))
30 + ])
31 + for attr_name in attr_names:
32 + a = getattr(obj1, attr_name)
33 + b = getattr(obj2, attr_name)
34 +
35 + is_same = False
36 + if isinstance(a, torch.Tensor):
37 + if (a.numel() == 0 or b.numel() == 0):
38 + is_same = (a.numel() == 0 and b.numel() == 0)
39 + elif torch.allclose(a, b):
40 + is_same = True
41 + elif isinstance(a, np.ndarray):
42 + if np.allclose(a, b):
43 + is_same = True
44 + elif isinstance(a, (BlockTable, SamplingMetadata)):
45 + _compare_objs(a, b)
46 + is_same = True # if we make it here must be same
47 + elif a == b:
48 + is_same = True
49 + assert is_same, f"Attribute {attr_name} is different"\
50 + f" in {obj1} and {obj2}: {a} != {b}"
51 +
52 +
53 def _remove_requests(
54 input_batch: InputBatch, batch_size: int,
55 reqs: list[CachedRequestState]) -> tuple[set[str], list[int]]:
@@ -254,3 +284,61 @@ def test_sampling_metadata_in_input_batch(device: str, batch_size: int):
284 assert torch.allclose(
285 expected_sampling_metadata.allowed_token_ids_mask,
286 sampling_metadata.allowed_token_ids_mask)
287 +
288 +
289 +@pytest.mark.parametrize("device", CUDA_DEVICES)
290 +@pytest.mark.parametrize("batch_size", [32])
291 +@pytest.mark.parametrize("swap_list", [((0, 1), )])
292 +def test_swap_states_in_input_batch(device: str, batch_size: int,
293 + swap_list: list):
294 + """
295 + Tests the logic for managing sampling metadata in the InputBatch.
296 +
297 + This test involves adding a set of requests to the InputBatch,
298 + followed by removing a subset of them. Afterward, the batch is compacted,
299 + and the `make_sampling_metadata` method is invoked on the batch. The
300 + output of `make_sampling_metadata` is then compared against the expected
301 + results to ensure correctness.
302 + """
303 + input_batch: InputBatch = InputBatch(
304 + max_num_reqs=batch_size,
305 + max_model_len=1024,
306 + max_num_blocks_per_req=10,
307 + device=torch.device(device),
308 + pin_memory=is_pin_memory_available(),
309 + vocab_size=1024,
310 + )
311 + ref_input_batch: InputBatch = InputBatch(
312 + max_num_reqs=batch_size,
313 + max_model_len=1024,
314 + max_num_blocks_per_req=10,
315 + device=torch.device(device),
316 + pin_memory=is_pin_memory_available(),
317 + vocab_size=1024,
318 + )
319 +
320 + reqs: list[CachedRequestState] = []
321 + req_id_reqs = {}
322 + req_id_output_token_ids = {}
323 + # Add requests
324 + for req_index in range(batch_size):
325 + req: CachedRequestState = _construct_cached_request_state(req_index)
326 + input_batch.add_request(req, req_index)
327 + reqs.append(req)
328 + req_id_reqs[req.req_id] = req
329 + req_id_output_token_ids[req.req_id] = req.output_token_ids
330 +
331 + reordered_reqs = reqs.copy()
332 + for swap_pair in swap_list:
333 + reordered_reqs[swap_pair[0]], reordered_reqs[swap_pair[1]] = \
334 + reordered_reqs[swap_pair[1]], reordered_reqs[swap_pair[0]]
335 + input_batch.swap_states(swap_pair[0], swap_pair[1])
336 +
337 + for req_index in range(batch_size):
338 + req = reordered_reqs[req_index]
339 + ref_input_batch.add_request(req, req_index)
340 +
341 + input_batch.refresh_sampling_metadata()
342 + ref_input_batch.refresh_sampling_metadata()
343 +
344 + _compare_objs(input_batch, ref_input_batch)
vllm/v1/attention/backends/flash_attn.py
+2 -2
@@ -100,8 +100,8 @@ class FlashAttentionMetadataBuilder:
100 self.runner = runner
101
102 def reorder_batch(self, input_batch: "InputBatch",
103 - scheduler_output: "SchedulerOutput"):
104 - pass
103 + scheduler_output: "SchedulerOutput") -> bool:
104 + return False
105
106 def build(self, num_reqs: int, num_actual_tokens: int, max_query_len: int,
107 common_prefix_len: int):
vllm/v1/attention/backends/mla/common.py
+178 -127
@@ -275,17 +275,47 @@ class MLACommonBackend(AttentionBackend):
275
276
277 @dataclass
278 -class MLACommonMetadata:
279 - """Metadata for MLACommon.
278 +class MLACommonPrefillMetadata:
279 + """ Prefill Specific Metadata """
280 +
281 + @dataclass
282 + class ChunkedContextMetadata:
283 + # New for MLA (compared to FlashAttention)
284 + # For handling chunked prefill
285 + cu_seq_lens: torch.Tensor
286 + starts: torch.Tensor
287 + seq_tot: list[int]
288 + max_seq_lens: list[int]
289 + workspace: torch.Tensor
290
281 - NOTE: Please read the comment at the top of the file before trying to
282 - understand this class
283 - """
284 - # New for MLA (compared to FlashAttention)
291 # Input positions for rotrary embeddings since for MLA the rotary
292 # position embeddings are applied inside the attention backend
293 input_positions: torch.Tensor
294 + block_table: torch.Tensor
295 + query_start_loc: torch.Tensor
296 + max_query_len: int
297 + chunked_context: Optional[ChunkedContextMetadata] = None
298 +
299
300 +@dataclass
301 +class MLACommonDecodeMetadata:
302 + # Input positions for rotrary embeddings since for MLA the rotary
303 + # position embeddings are applied inside the attention backend
304 + input_positions: torch.Tensor
305 + block_table: torch.Tensor
306 + seq_lens: torch.Tensor
307 +
308 +
309 +D = TypeVar("D", bound=MLACommonDecodeMetadata)
310 +
311 +
312 +@dataclass
313 +class MLACommonMetadata(Generic[D]):
314 + """Metadata for MLACommon.
315 +
316 + NOTE: Please read the comment at the top of the file before trying to
317 + understand this class
318 + """
319 # NOTE(sang): Definition of context_len, query_len, and seq_len.
320 # |---------- N-1 iteration --------|
321 # |---------------- N iteration ---------------------|
@@ -295,30 +325,23 @@ class MLACommonMetadata:
325 # |-- query_len ---|
326
327 num_actual_tokens: int # Number of tokens excluding padding.
298 - max_query_len: int
328 query_start_loc: torch.Tensor
300 - max_seq_len: int
301 - seq_lens: torch.Tensor
302 - block_table: torch.Tensor
329 slot_mapping: torch.Tensor
330
331 + # New for MLA (compared to FlashAttention)
332 + # For handling prefill decode split
333 + num_decodes: int
334 + num_decode_tokens: int
335 + num_prefills: int
336 +
337 # For logging.
338 num_input_tokens: int = 0 # Number of tokens including padding.
339
340 # The dimension of the attention heads
341 head_dim: Optional[int] = None
342
311 - # New for MLA (compared to FlashAttention)
312 - # For chunked prefill
313 - num_decodes: Optional[int] = None
314 - num_decode_tokens: Optional[int] = None
315 - num_prefills: Optional[int] = None
316 - has_context: bool = False
317 - context_chunk_cu_seq_lens: Optional[torch.Tensor] = None
318 - context_chunk_starts: Optional[torch.Tensor] = None
319 - context_chunk_seq_tot: Optional[list[int]] = None
320 - context_chunk_max_seq_lens: Optional[list[int]] = None
321 - chunked_prefill_workspace: Optional[torch.Tensor] = None
343 + decode: Optional[D] = None
344 + prefill: Optional[MLACommonPrefillMetadata] = None
345
346 def __post_init__(self):
347 supported_head_sizes = MLACommonBackend.get_supported_head_sizes()
@@ -329,10 +352,10 @@ class MLACommonMetadata:
352 f"received {self.head_dim}.")
353
354
332 -T = TypeVar("T", bound=MLACommonMetadata)
355 +M = TypeVar("M", bound=MLACommonMetadata)
356
357
335 -class MLACommonMetadataBuilder(Generic[T]):
358 +class MLACommonMetadataBuilder(Generic[M]):
359 """
360 NOTE: Please read the comment at the top of the file before trying to
361 understand this class
@@ -340,8 +363,9 @@ class MLACommonMetadataBuilder(Generic[T]):
363
364 def __init__(self,
365 runner: "GPUModelRunner",
343 - cls: Optional[type[T]] = None):
344 - self.cls = cls if cls is not None else MLACommonMetadata
366 + metadata_cls: Optional[type[M]] = None):
367 + self.metadata_cls = metadata_cls \
368 + if metadata_cls is not None else MLACommonMetadata
369 self.runner = runner
370 scheduler_config = runner.scheduler_config
371 model_config = runner.model_config
@@ -375,7 +399,7 @@ class MLACommonMetadataBuilder(Generic[T]):
399 self.page_size = self.runner.block_size
400
401 def reorder_batch(self, input_batch: "InputBatch",
378 - scheduler_output: "SchedulerOutput"):
402 + scheduler_output: "SchedulerOutput") -> bool:
403 # We now want to reorder the batch so that the "decode" requests are and
404 # the front and the "prefill" requests are at the using the least amount
405 # swaps possible. (NOTE for now we loosely use "decode" to mean requests
@@ -413,6 +437,7 @@ class MLACommonMetadataBuilder(Generic[T]):
437 num_decodes = len(decodes)
438 num_prefills = len(prefills)
439 first_prefill = 0
440 + modified_batch = False
441
442 for i in range(1, min(num_decodes, num_prefills) + 1):
443 # If the decode is at the "back" of the batch, i, we can swap it
@@ -421,6 +446,7 @@ class MLACommonMetadataBuilder(Generic[T]):
446 input_batch.swap_states(prefills[first_prefill],
447 decodes[num_decodes - i])
448 first_prefill += 1
449 + modified_batch = True
450 else:
451 break
452
@@ -432,10 +458,21 @@ class MLACommonMetadataBuilder(Generic[T]):
458 self._num_decode_tokens = num_decode_tokens
459 self._num_prefill_tokens = num_prefill_tokens
460
461 + return modified_batch
462 +
463 + def _build_decode(self, input_positions: torch.Tensor,
464 + block_table: torch.Tensor, seq_lens: torch.Tensor):
465 + return MLACommonDecodeMetadata(
466 + input_positions=input_positions,
467 + block_table=block_table,
468 + seq_lens=seq_lens,
469 + )
470 +
471 def build(self, num_reqs: int, num_actual_tokens: int, max_query_len: int,
436 - common_prefix_len: int) -> T:
472 + common_prefix_len: int) -> M:
473 + assert self._num_decodes + self._num_prefills == num_reqs
474 +
475 device = self.runner.device
438 - max_seq_len = self.runner.seq_lens_np[:num_reqs].max()
476 query_start_loc = self.runner.query_start_loc_cpu[:num_reqs + 1].to(
477 device, non_blocking=True)
478 seq_lens = self.runner.seq_lens_cpu[:num_reqs].to(device,
@@ -447,85 +484,103 @@ class MLACommonMetadataBuilder(Generic[T]):
484 input_positions = self.runner.positions_cpu[:num_actual_tokens].to(
485 device, non_blocking=True).long()
486
450 - context_chunk_cu_seq_lens = None
451 - context_chunk_starts = None
452 - context_chunk_seq_tot = None
453 - context_chunk_max_seq_lens = None
454 -
455 - num_computed_tokens_cpu_tensor = \
456 - self.runner.input_batch.num_computed_tokens_cpu_tensor[:num_reqs]
457 - context_lens_tensor = \
458 - num_computed_tokens_cpu_tensor.to(device, non_blocking=True)
459 -
460 - if self.chunked_prefill_enabled and self._num_prefills > 0 \
461 - and context_lens_tensor[self._num_decodes:].max() > 0:
462 - # NOTE: it is recommend you read the `Chunked Prefill` section in
463 - # the comment at the top of the file before trying to understand
464 - # the following code
465 -
466 - self.has_context = True
467 -
468 - num_prefills_with_context = \
469 - (context_lens_tensor[self._num_decodes:] > 0).sum().item()
470 -
471 - # currently we allocate an equal amount of workspace for each
472 - # prefill in the batch, we could probably use a more advanced
473 - # algorithm here and allocate more workspace to prefills with
474 - # longer context lengths
475 - max_context_chunk = \
476 - self.chunked_prefill_workspace_size // num_prefills_with_context
477 -
478 - # align max_context_chunk to page_size by rounding down,
479 - # currently the `gather_cache` kernel cannot handle
480 - # `context_chunk_starts` that are not aligned to page_size
481 - max_context_chunk = round_down(max_context_chunk, self.page_size)
482 - assert max_context_chunk > 0
483 - num_chunks = cdiv(context_lens_tensor.max(), max_context_chunk)
484 -
485 - # if `max_context_chunk = 256`, `num_chunks = 3`, and
486 - # `num_prefills_with_context = 4`, create a tensor that looks like
487 - # [[0, 0, 0, 0], [256, 256, 256, 256], [512, 512, 512, 512]]
488 - context_chunk_starts = \
489 - torch.arange(num_chunks, device=device, dtype=torch.int32) \
490 - .unsqueeze(1).expand(-1, self._num_prefills) \
491 - * max_context_chunk
492 - chunk_ends = torch.min(context_lens_tensor[self._num_decodes:] \
493 - .unsqueeze(0), context_chunk_starts + max_context_chunk)
494 - chunk_seq_lens = (chunk_ends - context_chunk_starts).clamp(min=0)
495 - _context_chunk_cu_seq_lens = chunk_seq_lens.cumsum(dim=1).to(
496 - torch.int32)
497 - zero = torch.zeros(num_chunks, dtype=torch.int32, device=device) \
498 - .unsqueeze(-1)
499 - context_chunk_cu_seq_lens = \
500 - torch.cat([zero, _context_chunk_cu_seq_lens], dim=1)
501 - context_chunk_max_seq_lens = \
502 - chunk_seq_lens.max(dim=1).values.tolist()
503 - context_chunk_seq_tot = chunk_seq_lens.sum(dim=1).tolist()
504 - assert max(context_chunk_seq_tot) <= \
505 - self.chunked_prefill_workspace_size
506 -
507 - return self.cls(
508 - input_positions=input_positions,
487 + prefill_metadata = None
488 + if self._num_prefills > 0:
489 + reqs_start = self._num_decodes # prefill_start
490 + tokens_start = self._num_decode_tokens
491 +
492 + context_lens_cpu = self.runner.input_batch.\
493 + num_computed_tokens_cpu_tensor[reqs_start:num_reqs]
494 + context_lens = context_lens_cpu.to(device, non_blocking=True)
495 +
496 + chunked_context_metadata = None
497 + if self.chunked_prefill_enabled and self._num_prefills > 0 \
498 + and context_lens.max() > 0:
499 + # NOTE: it is recommend you read the `Chunked Prefill` section
500 + # in the comment at the top of the file before trying to
501 + # understand the following code
502 +
503 + num_prefills_with_context = (context_lens > 0).sum().item()
504 +
505 + # currently we allocate an equal amount of workspace for each
506 + # prefill in the batch, we could probably use a more advanced
507 + # algorithm here and allocate more workspace to prefills with
508 + # longer context lengths
509 + max_context_chunk = \
510 + self.chunked_prefill_workspace_size \
511 + // num_prefills_with_context
512 +
513 + # align max_context_chunk to page_size by rounding down,
514 + # currently the `gather_cache` kernel cannot handle
515 + # `context_chunk_starts` that are not aligned to page_size
516 + max_context_chunk = round_down(max_context_chunk,
517 + self.page_size)
518 +
519 + assert max_context_chunk > 0
520 + num_chunks = cdiv(context_lens.max(), max_context_chunk)
521 +
522 + # if `max_context_chunk = 256`, `num_chunks = 3`, and
523 + # `num_prefills_with_context = 4`, create a tensor that looks
524 + # like
525 + # [[0, 0, 0, 0], [256, 256, 256, 256], [512, 512, 512, 512]]
526 + chunk_starts = \
527 + torch.arange(num_chunks, device=device, dtype=torch.int32) \
528 + .unsqueeze(1).expand(-1, self._num_prefills) \
529 + * max_context_chunk
530 + chunk_ends = torch.min(context_lens.unsqueeze(0),
531 + chunk_starts + max_context_chunk)
532 + chunk_seq_lens = (chunk_ends - chunk_starts).clamp(min=0)
533 + _chunk_cu_seq_lens = chunk_seq_lens.cumsum(dim=1).to(
534 + torch.int32)
535 + zero = torch.zeros(num_chunks,
536 + dtype=torch.int32,
537 + device=device).unsqueeze(-1)
538 +
539 + chunked_context_metadata = \
540 + MLACommonPrefillMetadata.ChunkedContextMetadata(
541 + cu_seq_lens=torch.cat(
542 + [zero, _chunk_cu_seq_lens], dim=1),
543 + starts=chunk_starts,
544 + seq_tot=chunk_seq_lens.sum(dim=1).tolist(),
545 + max_seq_lens=chunk_seq_lens.max(dim=1).values.tolist(),
546 + workspace=self.chunked_prefill_workspace,
547 + )
548 +
549 + assert max(chunked_context_metadata.max_seq_lens) <= \
550 + self.chunked_prefill_workspace_size
551 +
552 + prefill_metadata = MLACommonPrefillMetadata(
553 + input_positions=input_positions[tokens_start:],
554 + block_table=block_table[reqs_start:, ...],
555 + query_start_loc=query_start_loc[reqs_start:] -
556 + query_start_loc[reqs_start],
557 + max_query_len=seq_lens[reqs_start:].max().item(),
558 + chunked_context=chunked_context_metadata,
559 + )
560 +
561 + decode_metadata = None
562 + if self._num_decodes > 0:
563 + decode_metadata = self._build_decode(
564 + input_positions=input_positions[:self._num_decode_tokens],
565 + block_table=block_table[:self._num_decodes, ...],
566 + seq_lens=seq_lens[:self._num_decodes],
567 + )
568 +
569 + return self.metadata_cls(
570 num_actual_tokens=num_actual_tokens,
510 - max_query_len=max_query_len,
571 query_start_loc=query_start_loc,
512 - max_seq_len=max_seq_len,
513 - seq_lens=seq_lens,
514 - block_table=block_table,
572 slot_mapping=slot_mapping,
573 head_dim=self.runner.model_config.get_head_size(),
574 # MLACommonMetadata Chunk prefill specific
575 num_decodes=self._num_decodes,
576 num_decode_tokens=self._num_decode_tokens,
577 num_prefills=self._num_prefills,
521 - context_chunk_cu_seq_lens=context_chunk_cu_seq_lens,
522 - context_chunk_starts=context_chunk_starts,
523 - context_chunk_seq_tot=context_chunk_seq_tot,
524 - context_chunk_max_seq_lens=context_chunk_max_seq_lens,
578 + prefill=prefill_metadata,
579 + decode=decode_metadata,
580 )
581
582
528 -class MLACommonImpl(MLAAttentionImpl[T], Generic[T]):
583 +class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
584 """
585 NOTE: Please read the comment at the top of the file before trying to
586 understand this class
@@ -798,28 +853,24 @@ class MLACommonImpl(MLAAttentionImpl[T], Generic[T]):
853 kv_c_and_k_pe_cache: torch.Tensor,
854 attn_metadata: MLACommonMetadata,
855 ):
801 - assert attn_metadata.num_prefills is not None
802 - assert attn_metadata.context_chunk_seq_tot is not None
803 - assert attn_metadata.context_chunk_cu_seq_lens is not None
804 - assert attn_metadata.context_chunk_starts is not None
805 - assert attn_metadata.context_chunk_max_seq_lens is not None
856 + assert attn_metadata.prefill is not None
857 + prefill_metadata = attn_metadata.prefill
858 + assert prefill_metadata.chunked_context is not None
859
860 output = None
808 - iters = len(attn_metadata.context_chunk_seq_tot)
809 -
810 - assert attn_metadata.chunked_prefill_workspace is not None
811 - workspace = attn_metadata.chunked_prefill_workspace
861 + iters = len(prefill_metadata.chunked_context.seq_tot)
862 + workspace = prefill_metadata.chunked_context.workspace
863
864 for i in range(iters):
814 - toks = attn_metadata.context_chunk_seq_tot[i]
865 + toks = prefill_metadata.chunked_context.seq_tot[i]
866
867 ops.gather_cache(
868 src_cache=kv_c_and_k_pe_cache,
869 dst=workspace,
819 - block_table=attn_metadata.block_table,
820 - cu_seq_lens=attn_metadata.context_chunk_cu_seq_lens[i],
870 + block_table=prefill_metadata.block_table,
871 + cu_seq_lens=prefill_metadata.chunked_context.cu_seq_lens[i],
872 batch_size=attn_metadata.num_prefills,
822 - seq_starts=attn_metadata.context_chunk_starts[i],
873 + seq_starts=prefill_metadata.chunked_context.starts[i],
874 )
875
876 kv_c_normed = workspace[:toks]\
@@ -845,10 +896,10 @@ class MLACommonImpl(MLAAttentionImpl[T], Generic[T]):
896 q=q,
897 k=k,
898 v=v_padded,
848 - cu_seqlens_q=attn_metadata.query_start_loc,
849 - cu_seqlens_k=attn_metadata.context_chunk_cu_seq_lens[i],
850 - max_seqlen_q=attn_metadata.max_query_len,
851 - max_seqlen_k=attn_metadata.context_chunk_max_seq_lens[i],
899 + cu_seqlens_q=prefill_metadata.query_start_loc,
900 + cu_seqlens_k=prefill_metadata.chunked_context.cu_seq_lens[i],
901 + max_seqlen_q=prefill_metadata.max_query_len,
902 + max_seqlen_k=prefill_metadata.chunked_context.max_seq_lens[i],
903 softmax_scale=self.scale,
904 causal=False, # Context is unmasked
905 return_softmax_lse=True,
@@ -881,7 +932,9 @@ class MLACommonImpl(MLAAttentionImpl[T], Generic[T]):
932 kv_c_and_k_pe_cache: torch.Tensor,
933 attn_metadata: MLACommonMetadata,
934 ) -> torch.Tensor:
884 - has_context = attn_metadata.has_context
935 + assert attn_metadata.prefill is not None
936 +
937 + has_context = attn_metadata.prefill.chunked_context is not None
938 kv_nope = self.kv_b_proj(kv_c_normed)[0].view(\
939 -1, self.num_heads, self.qk_nope_head_dim + self.v_head_dim)
940 k_nope, v = kv_nope\
@@ -898,10 +951,10 @@ class MLACommonImpl(MLAAttentionImpl[T], Generic[T]):
951 q=q,
952 k=k,
953 v=v_padded,
901 - cu_seqlens_q=attn_metadata.query_start_loc,
902 - cu_seqlens_k=attn_metadata.query_start_loc,
903 - max_seqlen_q=attn_metadata.max_query_len,
904 - max_seqlen_k=attn_metadata.max_seq_len,
954 + cu_seqlens_q=attn_metadata.prefill.query_start_loc,
955 + cu_seqlens_k=attn_metadata.prefill.query_start_loc,
956 + max_seqlen_q=attn_metadata.prefill.max_query_len,
957 + max_seqlen_k=attn_metadata.prefill.max_query_len,
958 softmax_scale=self.scale,
959 causal=True,
960 return_softmax_lse=has_context,
@@ -934,7 +987,7 @@ class MLACommonImpl(MLAAttentionImpl[T], Generic[T]):
987 q_nope: torch.Tensor,
988 q_pe: torch.Tensor,
989 kv_c_and_k_pe_cache: torch.Tensor,
937 - attn_metadata: T,
990 + attn_metadata: M,
991 ) -> torch.Tensor:
992 raise NotImplementedError
993
@@ -945,7 +998,7 @@ class MLACommonImpl(MLAAttentionImpl[T], Generic[T]):
998 k_c_normed: torch.Tensor, # key in unified attn
999 k_pe: torch.Tensor, # value in unified attn
1000 kv_cache: torch.Tensor,
948 - attn_metadata: T,
1001 + attn_metadata: M,
1002 output: Optional[torch.Tensor] = None,
1003 ) -> torch.Tensor:
1004
@@ -966,7 +1019,6 @@ class MLACommonImpl(MLAAttentionImpl[T], Generic[T]):
1019
1020 # Restore head dim (for rotary embedding)
1021 k_pe = k_pe.unsqueeze(1)
969 - assert hasattr(attn_metadata, "input_positions")
1022
1023 assert attn_metadata.num_decodes is not None and \
1024 attn_metadata.num_prefills is not None and \
@@ -978,28 +1030,27 @@ class MLACommonImpl(MLAAttentionImpl[T], Generic[T]):
1030
1031 decode_hs_or_q_c = hidden_states_or_q_c[:num_decode_tokens]
1032 decode_k_pe = k_pe[:num_decode_tokens]
981 - decode_input_positions = \
982 - attn_metadata.input_positions[:num_decode_tokens]
1033
1034 prefill_hs_or_q_c = hidden_states_or_q_c[num_decode_tokens:]
1035 prefill_k_pe = k_pe[num_decode_tokens:]
986 - prefill_input_positions = \
987 - attn_metadata.input_positions[num_decode_tokens:]
1036 prefill_k_c_normed = k_c_normed[num_decode_tokens:]
1037
1038 if has_decode:
1039 + assert attn_metadata.decode is not None
1040 decode_q_nope = self._q_proj_and_k_up_proj(decode_hs_or_q_c)
1041 decode_q_pe = torch.matmul(decode_hs_or_q_c, self.W_QR)\
1042 .view(-1, self.num_heads, self.qk_rope_head_dim)
1043 decode_q_pe[...], decode_k_pe[...] = self.rotary_emb(
995 - decode_input_positions, decode_q_pe, decode_k_pe)
1044 + attn_metadata.decode.input_positions, decode_q_pe, decode_k_pe)
1045
1046 if has_prefill:
1047 + assert attn_metadata.prefill is not None
1048 prefill_q = self.q_proj(prefill_hs_or_q_c)[0]\
1049 .view(-1, self.num_heads, self.qk_head_dim)
1050 prefill_q_pe = prefill_q[..., self.qk_nope_head_dim:]
1051 prefill_q_pe[...], prefill_k_pe[...] = self.rotary_emb(
1002 - prefill_input_positions, prefill_q_pe, prefill_k_pe)
1052 + attn_metadata.prefill.input_positions, prefill_q_pe,
1053 + prefill_k_pe)
1054
1055 # write the latent and rope to kv cache
1056 if kv_cache.numel() > 0:
vllm/v1/attention/backends/mla/flashmla.py
+33 -25
@@ -11,6 +11,7 @@ from vllm.attention.ops.flashmla import (flash_mla_with_kvcache,
11 is_flashmla_supported)
12 from vllm.logger import init_logger
13 from vllm.v1.attention.backends.mla.common import (MLACommonBackend,
14 + MLACommonDecodeMetadata,
15 MLACommonImpl,
16 MLACommonMetadata,
17 MLACommonMetadataBuilder)
@@ -38,34 +39,41 @@ class FlashMLABackend(MLACommonBackend):
39
40
41 @dataclass
41 -class FlashMLAMetadata(MLACommonMetadata):
42 - decode_tile_scheduler_metadata: Optional[tuple[torch.Tensor,
43 - torch.Tensor]] = None
44 - decode_num_splits: Optional[torch.Tensor] = None
42 +class FlashMLADecodeMetadata(MLACommonDecodeMetadata):
43 + tile_scheduler_metadata: tuple[torch.Tensor, torch.Tensor]
44 + num_splits: torch.Tensor
45 +
46 +
47 +@dataclass
48 +class FlashMLAMetadata(MLACommonMetadata[FlashMLADecodeMetadata]):
49 + pass
50
51
52 class FlashMLAMetadataBuilder(MLACommonMetadataBuilder[FlashMLAMetadata]):
53
54 def __init__(self, runner):
50 - super().__init__(runner, cls=FlashMLAMetadata)
55 + super().__init__(runner)
56
57 self.num_q_heads = self.runner.model_config.get_num_attention_heads(
58 self.runner.parallel_config)
59
55 - def build(self, num_reqs: int, num_actual_tokens: int, max_query_len: int,
56 - common_prefix_len: int):
57 - m = super().build(num_reqs, num_actual_tokens, max_query_len,
58 - common_prefix_len)
59 -
60 - if m.num_decode_tokens is not None and m.num_decode_tokens > 0:
61 - m.decode_tile_scheduler_metadata, m.decode_num_splits = \
62 - get_mla_metadata(
63 - m.seq_lens[:m.num_decode_tokens],
64 - self.num_q_heads,
65 - 1, # MQA for the decode path
66 - )
60 + def _build_decode(self, input_positions: torch.Tensor,
61 + block_table: torch.Tensor,
62 + seq_lens: torch.Tensor) -> FlashMLADecodeMetadata:
63 + tile_scheduler_metadata, num_splits = \
64 + get_mla_metadata(
65 + seq_lens,
66 + self.num_q_heads,
67 + 1, # MQA for the decode path
68 + )
69
68 - return m
70 + return FlashMLADecodeMetadata(
71 + input_positions=input_positions,
72 + block_table=block_table,
73 + seq_lens=seq_lens,
74 + tile_scheduler_metadata=tile_scheduler_metadata,
75 + num_splits=num_splits,
76 + )
77
78
79 class FlashMLAImpl(MLACommonImpl[FlashMLAMetadata]):
@@ -115,6 +123,8 @@ class FlashMLAImpl(MLACommonImpl[FlashMLAMetadata]):
123 attn_metadata: FlashMLAMetadata,
124 ) -> torch.Tensor:
125 assert kv_c_and_k_pe_cache.numel() > 0
126 + assert attn_metadata.decode is not None
127 +
128 if self.kv_cache_dtype.startswith("fp8"):
129 raise NotImplementedError("FP8 FlashMLA not yet supported")
130
@@ -124,14 +134,12 @@ class FlashMLAImpl(MLACommonImpl[FlashMLAMetadata]):
134 o, _ = flash_mla_with_kvcache(
135 q=q,
136 k_cache=kv_c_and_k_pe_cache.unsqueeze(-2), # Add head dim of 1
127 - block_table=attn_metadata.block_table[:attn_metadata.num_decodes,
128 - ...],
129 - cache_seqlens=attn_metadata.seq_lens[:attn_metadata.
130 - num_decode_tokens],
137 + block_table=attn_metadata.decode.block_table,
138 + cache_seqlens=attn_metadata.decode.seq_lens,
139 head_dim_v=self.kv_lora_rank,
132 - tile_scheduler_metadata=attn_metadata.
133 - decode_tile_scheduler_metadata,
134 - num_splits=attn_metadata.decode_num_splits,
140 + tile_scheduler_metadata=attn_metadata.decode.
141 + tile_scheduler_metadata,
142 + num_splits=attn_metadata.decode.num_splits,
143 softmax_scale=self.scale,
144 causal=True,
145 )
vllm/v1/attention/backends/mla/triton_mla.py
+5 -2
@@ -69,6 +69,8 @@ class TritonMLAImpl(MLACommonImpl[MLACommonMetadata]):
69 attn_metadata: MLACommonMetadata,
70 ) -> torch.Tensor:
71 assert kv_c_and_k_pe_cache.numel() > 0
72 + assert attn_metadata.decode is not None
73 +
74 if self.kv_cache_dtype.startswith("fp8"):
75 raise NotImplementedError("FP8 Triton MLA not yet supported")
76
@@ -104,7 +106,8 @@ class TritonMLAImpl(MLACommonImpl[MLACommonMetadata]):
106
107 # Run MQA
108 decode_attention_fwd(q, kv_c_and_k_pe_cache, kv_c_cache, o,
107 - attn_metadata.block_table, attn_metadata.seq_lens,
108 - attn_logits, num_kv_splits, self.scale, PAGE_SIZE)
109 + attn_metadata.decode.block_table,
110 + attn_metadata.decode.seq_lens, attn_logits,
111 + num_kv_splits, self.scale, PAGE_SIZE)
112
113 return self._v_up_proj_and_o_proj(o)
vllm/v1/worker/gpu_input_batch.py
+23 -2
@@ -383,8 +383,6 @@ class InputBatch:
383 self.req_id_to_index[old_id_i2], self.req_id_to_index[old_id_i1]
384 self.num_tokens[i1], self.num_tokens[i2] =\
385 self.num_tokens[i2], self.num_tokens[i1]
386 - self.token_ids_cpu[i1, ...], self.token_ids_cpu[i2, ...], =\
387 - self.token_ids_cpu[i2, ...], self.token_ids_cpu[i1, ...]
386 self.num_tokens_no_spec[i1], self.num_tokens_no_spec[i2] =\
387 self.num_tokens_no_spec[i2], self.num_tokens_no_spec[i1]
388 self.num_prompt_tokens[i1], self.num_prompt_tokens[i2] =\
@@ -406,24 +404,47 @@ class InputBatch:
404 self.min_p_cpu[i1], self.min_p_cpu[i2] =\
405 self.min_p_cpu[i2], self.min_p_cpu[i1]
406
407 + # NOTE: the following is unsafe
408 + # self.token_ids_cpu[i1, ...], self.token_ids_cpu[i2, ...], =\
409 + # self.token_ids_cpu[i2, ...], self.token_ids_cpu[i1, ...]
410 + # instead, we need to temporiarily copy the data for one of the indices
411 + # TODO(lucas): optimize this by only copying valid indices
412 + tmp = self.token_ids_cpu[i1, ...].copy()
413 + self.token_ids_cpu[i1, ...] = self.token_ids_cpu[i2, ...]
414 + self.token_ids_cpu[i2, ...] = tmp
415 +
416 g1 = self.generators.get(i1)
417 g2 = self.generators.get(i2)
418 if g1 is not None:
419 self.generators[i2] = g1
420 + else:
421 + self.generators.pop(i2, None)
422 if g2 is not None:
423 self.generators[i1] = g2
424 + else:
425 + self.generators.pop(i1, None)
426
427 t1 = self.min_tokens.get(i1)
428 t2 = self.min_tokens.get(i2)
429 if t1 is not None:
430 self.min_tokens[i2] = t1
431 + else:
432 + self.min_tokens.pop(i2, None)
433 if t2 is not None:
434 self.min_tokens[i1] = t2
435 + else:
436 + self.min_tokens.pop(i1, None)
437
438 self.request_lora_mapping[i1], self.request_lora_mapping[i2] =\
439 self.request_lora_mapping[i2], self.request_lora_mapping[i1]
440 self.logit_bias[i1], self.logit_bias[i2] =\
441 self.logit_bias[i2], self.logit_bias[i1]
442 +
443 + if self.allowed_token_ids_mask_cpu_tensor is not None:
444 + self.allowed_token_ids_mask_cpu_tensor[i1], \
445 + self.allowed_token_ids_mask_cpu_tensor[i2] =\
446 + self.allowed_token_ids_mask_cpu_tensor[i2], \
447 + self.allowed_token_ids_mask_cpu_tensor[i1]
448 self.block_table.swap_row(i1, i2)
449
450 def condense(self, empty_req_indices: list[int]) -> None:
vllm/v1/worker/gpu_model_runner.py
+4 -2
@@ -456,8 +456,10 @@ class GPUModelRunner(LoRAModelRunnerMixin):
456 # Some attention backends (namely MLA) may want to separate requests
457 # based on if the attention computation will be compute-bound or
458 # memory-bound. This gives them a hook to do that.
459 - self.attn_metadata_builder.reorder_batch(self.input_batch,
460 - scheduler_output)
459 + modified_batch = self.attn_metadata_builder.reorder_batch(
460 + self.input_batch, scheduler_output)
461 + if modified_batch:
462 + self.input_batch.refresh_sampling_metadata()
463
464 # OPTIMIZATION: Start copying the block table first.
465 # This way, we can overlap the copy with the following CPU operations.