dflash.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. from dataclasses import replace
  4. from typing import Any
  5. import torch
  6. from typing_extensions import override
  7. from vllm.config import VllmConfig
  8. from vllm.forward_context import set_forward_context
  9. from vllm.logger import init_logger
  10. from vllm.v1.attention.backend import CommonAttentionMetadata
  11. from vllm.v1.spec_decode.llm_base_proposer import SpecDecodeBaseProposer
  12. from vllm.v1.spec_decode.utils import (
  13. copy_and_expand_dflash_inputs_kernel,
  14. next_power_of_2,
  15. )
  16. logger = init_logger(__name__)
  17. class DFlashProposer(SpecDecodeBaseProposer):
  18. def __init__(
  19. self,
  20. vllm_config: VllmConfig,
  21. device: torch.device,
  22. runner=None,
  23. ):
  24. assert vllm_config.speculative_config is not None
  25. assert vllm_config.speculative_config.method == "dflash"
  26. super().__init__(
  27. vllm_config=vllm_config,
  28. device=device,
  29. pass_hidden_states_to_model=True,
  30. runner=runner,
  31. )
  32. # Only next_token_ids and mask tokens are query tokens, all other context is K/V
  33. self.max_query_tokens = self.max_batch_size * (1 + self.num_speculative_tokens)
  34. self.max_padded_query_tokens = max(
  35. self.max_query_tokens,
  36. vllm_config.compilation_config.max_cudagraph_capture_size or 0,
  37. )
  38. # Positions covers both context states + query states
  39. self.max_positions = self.max_num_tokens + self.max_padded_query_tokens
  40. # Separate context buffers to keep query buffer addresses stable for CUDA graphs
  41. self._context_slot_mapping_buffer = torch.zeros(
  42. self.max_num_tokens,
  43. dtype=torch.int64,
  44. device=device,
  45. )
  46. self._slot_mapping_buffer = torch.zeros(
  47. self.max_padded_query_tokens,
  48. dtype=torch.int64,
  49. device=device,
  50. )
  51. self._context_positions_buffer = torch.zeros(
  52. self.max_num_tokens,
  53. dtype=torch.int64,
  54. device=device,
  55. )
  56. self.positions = torch.zeros(
  57. self.max_padded_query_tokens,
  58. dtype=torch.int64,
  59. device=device,
  60. )
  61. self.arange = torch.arange(
  62. self.max_positions + 1, device=device, dtype=torch.int32
  63. )
  64. # For DFlash we use the input embeddings to embed the mask token
  65. self.parallel_drafting_hidden_state_tensor = None
  66. from vllm.model_executor.models.qwen3_dflash import dflash_has_any_non_causal
  67. self.dflash_causal = not dflash_has_any_non_causal(
  68. self.draft_model_config.hf_config
  69. )
  70. @override
  71. def load_model(self, target_model: torch.nn.Module) -> None:
  72. from vllm.model_executor.models.qwen3_dflash import (
  73. dflash_target_rope_is_neox_style,
  74. )
  75. draft_model_config = self.speculative_config.draft_model_config
  76. assert draft_model_config is not None
  77. # The drafter must rotate Q/K the way its target does. Take that from the
  78. # built target before super() constructs the draft.
  79. is_neox_style = dflash_target_rope_is_neox_style(target_model)
  80. if is_neox_style is not None:
  81. draft_model_config.hf_config.is_neox_style = is_neox_style
  82. super().load_model(target_model)
  83. @override
  84. def _create_draft_vllm_config(self) -> VllmConfig:
  85. base = super()._create_draft_vllm_config()
  86. # The draft model is text-only — clear the target's multimodal
  87. # flag so flash_attn is not rejected for mm_prefix support.
  88. arch = base.model_config.model_arch_config
  89. if arch.is_mm_prefix_lm:
  90. base.model_config.model_arch_config = replace(arch, is_mm_prefix_lm=False)
  91. return replace(
  92. base,
  93. attention_config=replace(
  94. base.attention_config,
  95. use_non_causal=not self.dflash_causal,
  96. ),
  97. )
  98. @override
  99. def _warn_if_multimodal(self):
  100. # Override to allow multimodal inputs since DFlash supports Qwen3.5 models
  101. pass
  102. @override
  103. def set_inputs_first_pass(
  104. self,
  105. target_token_ids: torch.Tensor,
  106. next_token_ids: torch.Tensor,
  107. target_positions: torch.Tensor,
  108. target_hidden_states: torch.Tensor,
  109. token_indices_to_sample: torch.Tensor | None,
  110. cad: CommonAttentionMetadata,
  111. num_rejected_tokens_gpu: torch.Tensor | None,
  112. ) -> tuple[int, torch.Tensor, CommonAttentionMetadata]:
  113. # DFlash cross-attention: context K/V from target hidden states,
  114. # Q from query embeddings (bonus + mask tokens).
  115. batch_size = cad.batch_size()
  116. num_context = target_token_ids.shape[0]
  117. num_query_per_req = 1 + self.num_speculative_tokens
  118. num_query_total = batch_size * num_query_per_req
  119. # Store for build_model_inputs_first_pass to use
  120. self._dflash_num_context = num_context
  121. # We don't need to copy into a buffer here since the context preprocessing
  122. # does not run in a CUDA graph
  123. self._dflash_hidden_states = target_hidden_states
  124. token_indices_to_sample = torch.empty(
  125. batch_size * self.num_speculative_tokens,
  126. dtype=torch.int32,
  127. device=self.device,
  128. )
  129. # Launch fused triton kernel for input_ids, positions, slot_mapping,
  130. # and token_indices_to_sample
  131. max_ctx_per_req = cad.max_query_len
  132. max_tokens_per_req = max_ctx_per_req + num_query_per_req
  133. BLOCK_SIZE = min(256, next_power_of_2(max_tokens_per_req))
  134. num_blocks = (max_tokens_per_req + BLOCK_SIZE - 1) // BLOCK_SIZE
  135. grid = (batch_size, num_blocks)
  136. has_num_rejected = num_rejected_tokens_gpu is not None
  137. copy_and_expand_dflash_inputs_kernel[grid](
  138. # Inputs
  139. next_token_ids_ptr=next_token_ids,
  140. target_positions_ptr=target_positions,
  141. # Outputs
  142. out_input_ids_ptr=self.input_ids,
  143. out_context_positions_ptr=self._context_positions_buffer,
  144. out_query_positions_ptr=self.positions,
  145. out_context_slot_mapping_ptr=self._context_slot_mapping_buffer,
  146. out_query_slot_mapping_ptr=self._slot_mapping_buffer,
  147. out_token_indices_ptr=token_indices_to_sample,
  148. # Block table
  149. block_table_ptr=cad.block_table_tensor,
  150. block_table_stride=cad.block_table_tensor.stride(0),
  151. # Metadata
  152. query_start_loc_ptr=cad.query_start_loc,
  153. num_rejected_tokens_ptr=(
  154. num_rejected_tokens_gpu if has_num_rejected else 0
  155. ),
  156. # Scalars
  157. parallel_drafting_token_id=self.parallel_drafting_token_id,
  158. block_size=self.block_size,
  159. num_query_per_req=num_query_per_req,
  160. num_speculative_tokens=self.num_speculative_tokens,
  161. total_input_tokens=num_context,
  162. BLOCK_SIZE=BLOCK_SIZE,
  163. HAS_NUM_REJECTED=has_num_rejected,
  164. )
  165. query_slot_mapping = self._slot_mapping_buffer[:num_query_total]
  166. new_query_start_loc = self.arange[: batch_size + 1] * num_query_per_req
  167. # In padded mode, cad.seq_lens includes rejected tokens. Subtract
  168. # them so attention only sees the valid prefix of context states.
  169. effective_seq_lens = cad.seq_lens
  170. if has_num_rejected:
  171. effective_seq_lens = effective_seq_lens - num_rejected_tokens_gpu
  172. # Skip num_rejected_tokens (GPU-only); overestimating is fine here.
  173. new_seq_lens_cpu_upper_bound = (
  174. cad.seq_lens_cpu_upper_bound + num_query_per_req
  175. if cad.seq_lens_cpu_upper_bound is not None
  176. else None
  177. )
  178. new_cad = CommonAttentionMetadata(
  179. query_start_loc=new_query_start_loc,
  180. seq_lens=effective_seq_lens + num_query_per_req,
  181. query_start_loc_cpu=(
  182. torch.from_numpy(self.token_arange_np[: batch_size + 1]).clone()
  183. * num_query_per_req
  184. ),
  185. _seq_lens_cpu=None,
  186. _num_computed_tokens_cpu=None,
  187. seq_lens_cpu_upper_bound=new_seq_lens_cpu_upper_bound,
  188. num_reqs=cad.num_reqs,
  189. num_actual_tokens=num_query_total,
  190. max_query_len=num_query_per_req,
  191. max_seq_len=cad.max_seq_len + num_query_per_req,
  192. block_table_tensor=cad.block_table_tensor,
  193. slot_mapping=query_slot_mapping,
  194. causal=self.dflash_causal,
  195. )
  196. return num_query_total, token_indices_to_sample, new_cad
  197. @override
  198. @torch.inference_mode()
  199. def dummy_run(
  200. self,
  201. num_tokens: int,
  202. use_cudagraphs: bool = True,
  203. is_graph_capturing: bool = False,
  204. slot_mappings: dict[str, torch.Tensor] | None = None,
  205. ) -> None:
  206. """
  207. Key differences to default dummy_run:
  208. - Only one forward pass due to parallel drafting
  209. - DFlash uses context states as unpadded metadata, so hidden_states will
  210. use the unpadded num_tokens instead of num_input_tokens
  211. - max_query_tokens is quite small, DFlash only sees spec tokens as queries
  212. - Multimodal inputs are not currently supported
  213. """
  214. num_query_tokens = min(num_tokens, self.max_query_tokens)
  215. cudagraph_runtime_mode, num_input_tokens, num_tokens_across_dp = (
  216. self._determine_batch_execution_and_padding(
  217. num_query_tokens, use_cudagraphs=use_cudagraphs
  218. )
  219. )
  220. # Slot mapping sized to num_input_tokens (query only), matching
  221. # the K/V tensor size from the model forward. Context KVs are
  222. # pre-inserted separately and don't flow through the model.
  223. if (
  224. self._draft_attn_layer_names
  225. and slot_mappings is not None
  226. and next(iter(self._draft_attn_layer_names)) in slot_mappings
  227. ):
  228. slot_mapping_dict = self._get_slot_mapping(num_input_tokens)
  229. else:
  230. slot_mapping_dict = slot_mappings or {}
  231. # Context and query positions use separate buffers; no copy needed.
  232. context_positions = self._context_positions_buffer[:num_tokens]
  233. # Context states will be passed directly to the precomputation without
  234. # going through the buffer, since no CUDA graph is used for the precomputation.
  235. # For the dummy run, we use the dummy buffer.
  236. context_states = self.hidden_states[:num_tokens]
  237. # Run the KV projection (GEMM + norms + RoPE) for memory profiling,
  238. self.model.precompute_and_store_context_kv(context_states, context_positions)
  239. with set_forward_context(
  240. None,
  241. self.vllm_config,
  242. num_tokens=num_input_tokens,
  243. num_tokens_across_dp=num_tokens_across_dp,
  244. cudagraph_runtime_mode=cudagraph_runtime_mode,
  245. slot_mapping=slot_mapping_dict,
  246. ):
  247. self.model(
  248. input_ids=self.input_ids[:num_input_tokens],
  249. positions=self._get_positions(num_input_tokens),
  250. inputs_embeds=None,
  251. )
  252. @override
  253. def build_model_inputs_first_pass(
  254. self,
  255. num_tokens: int,
  256. num_input_tokens: int,
  257. mm_embed_inputs: tuple[list[torch.Tensor], torch.Tensor] | None,
  258. ) -> tuple[dict[str, Any], int]:
  259. # Context and query positions/slots were written to separate
  260. # buffers by the kernel — no copy needed.
  261. num_context = self._dflash_num_context
  262. # Pre-insert context KVs directly into cache
  263. self.model.precompute_and_store_context_kv(
  264. self._dflash_hidden_states, # Shape is already [num_context, hidden_size]
  265. self._context_positions_buffer[:num_context],
  266. self._context_slot_mapping_buffer[:num_context],
  267. )
  268. return (
  269. dict(
  270. input_ids=self.input_ids[:num_input_tokens],
  271. positions=self._get_positions(num_input_tokens),
  272. inputs_embeds=None,
  273. ),
  274. num_input_tokens,
  275. )
  276. @override
  277. def build_per_group_and_layer_attn_metadata(
  278. self, cad: CommonAttentionMetadata, draft_index: int = 0
  279. ) -> tuple[list[object], dict[str, object]]:
  280. per_group, per_layer = super().build_per_group_and_layer_attn_metadata(
  281. cad, draft_index
  282. )
  283. if not self.dflash_causal:
  284. # Require all layers to support non-causal attention when required by DFlash
  285. for layer_name, attn_metadata in per_layer.items():
  286. assert getattr(attn_metadata, "causal", None) is False, (
  287. f"Attention metadata for layer {layer_name} does not have"
  288. " non-causal support, which is required for DFlash."
  289. " Consider using a different attention backend, e.g FlashAttention."
  290. )
  291. return per_group, per_layer
  292. @override
  293. def _get_eagle3_use_aux_hidden_state_from_config(self):
  294. return self.dflash_config.get("use_aux_hidden_state", True)
  295. @property
  296. def dflash_config(self):
  297. return getattr(self.draft_model_config.hf_config, "dflash_config", None) or {}