speculator.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. from abc import ABC, abstractmethod
  4. from collections.abc import Mapping
  5. from dataclasses import replace
  6. from typing import TYPE_CHECKING, Any
  7. import os
  8. import numpy as np
  9. import torch
  10. import torch.nn as nn
  11. from vllm.config import VllmConfig, get_layers_from_vllm_config
  12. from vllm.config.compilation import CUDAGraphMode
  13. from vllm.distributed.eplb.eplb_state import EplbState
  14. from vllm.logger import init_logger
  15. from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
  16. from vllm.model_executor.models import supports_multimodal_embeddings
  17. from vllm.v1.kv_cache_interface import KVCacheConfig
  18. from vllm.v1.watermarking import create_watermarker
  19. from vllm.v1.watermarking.spec_decode import (
  20. DraftWatermarker,
  21. create_speculative_draft_watermarker,
  22. )
  23. from vllm.v1.worker.gpu.attn_utils import (
  24. build_attn_metadata,
  25. init_attn_backend,
  26. )
  27. from vllm.v1.worker.gpu.block_table import BlockTables
  28. from vllm.v1.worker.gpu.cp_utils import prepare_dcp_local_seq_lens
  29. from vllm.v1.worker.gpu.cudagraph_utils import BatchExecutionDescriptor
  30. from vllm.v1.worker.gpu.dp_utils import DPSyncState
  31. from vllm.v1.worker.gpu.input_batch import InputBatch, InputBuffers
  32. from vllm.v1.worker.gpu.model_states.interface import ModelState
  33. from vllm.v1.worker.gpu.sample.gumbel import gumbel_sample
  34. from vllm.v1.worker.gpu.spec_decode.acceptance_estimator import (
  35. OnlineAcceptanceEstimator,
  36. )
  37. from vllm.v1.worker.utils import AttentionGroup
  38. if TYPE_CHECKING:
  39. from vllm.v1.worker.gpu.pcp_manager import PCPManager
  40. logger = init_logger(__name__)
  41. def _target_feeds_hc_residual(vllm_config: VllmConfig) -> bool:
  42. """Whether the target replaces the drafter's input with its HC residual.
  43. Keyed on the same hook the model runner calls to perform that swap. It is
  44. resolved from the target model class because speculators are built before
  45. the target model is instantiated.
  46. """
  47. from vllm.model_executor.model_loader.utils import get_model_cls
  48. target_cls = get_model_cls(vllm_config.model_config)
  49. return hasattr(target_cls, "get_mtp_target_hidden_states")
  50. class BaseSpeculator(ABC):
  51. num_query_per_req: int = 1
  52. # Extra query slots reserved per request outside the regular queries.
  53. num_extra_query_per_req: int = 0
  54. @abstractmethod
  55. def init_cudagraph_manager(self, cudagraph_mode: CUDAGraphMode) -> None:
  56. pass
  57. @abstractmethod
  58. def capture(self) -> None:
  59. pass
  60. @abstractmethod
  61. def propose(
  62. self,
  63. input_batch: InputBatch,
  64. attn_metadata: dict[str, Any],
  65. slot_mappings: dict[str, torch.Tensor],
  66. # [num_tokens, hidden_size]
  67. last_hidden_states: torch.Tensor,
  68. # num_layers x [num_tokens, hidden_size]
  69. aux_hidden_states: list[torch.Tensor] | None,
  70. # [num_reqs]
  71. num_sampled: torch.Tensor,
  72. # [num_reqs]
  73. num_rejected: torch.Tensor,
  74. # [max_num_reqs]
  75. last_sampled: torch.Tensor,
  76. # [num_prefill_lookahead, max_num_reqs]
  77. next_prefill_tokens: torch.Tensor,
  78. # [max_num_reqs]
  79. temperature: torch.Tensor,
  80. # [max_num_reqs]
  81. seeds: torch.Tensor,
  82. dp_sync: DPSyncState | None = None,
  83. dummy_run: bool = False,
  84. skip_attn_for_dummy_run: bool = False,
  85. mm_inputs: tuple[list[torch.Tensor], torch.Tensor] | None = None,
  86. is_profile: bool = False,
  87. ) -> torch.Tensor:
  88. pass
  89. class DraftModelSpeculator(BaseSpeculator):
  90. def __init__(self, vllm_config: VllmConfig, device: torch.device):
  91. self.vllm_config = vllm_config
  92. self.device = device
  93. assert vllm_config.speculative_config is not None
  94. self.speculative_config = vllm_config.speculative_config
  95. self.method = self.speculative_config.method
  96. self.num_speculative_steps = self.speculative_config.num_speculative_tokens
  97. self.draft_model_config = self.speculative_config.draft_model_config
  98. self.scheduler_config = vllm_config.scheduler_config
  99. self.max_num_reqs = self.scheduler_config.max_num_seqs
  100. self.max_num_tokens = self.scheduler_config.max_num_batched_tokens
  101. self.max_model_len = vllm_config.model_config.max_model_len
  102. self.draft_max_seq_len = self.max_model_len
  103. # We need to get the hidden size from the draft model config because
  104. # the draft model's hidden size can be different from the target model's
  105. # hidden size (e.g., Llama 3.3 70B).
  106. self.hidden_size = self.draft_model_config.get_hidden_size()
  107. # Widen for HC-multiplexed residuals: a target that implements
  108. # get_mtp_target_hidden_states() (e.g. DeepSeek V4) hands the drafter
  109. # its pre-hc_head (T, hc_mult * hidden_size) residual in place of the
  110. # collapsed hidden states, so the drafter's buffers must match. Key off
  111. # that hook rather than hc_mult alone -- HY V4 runs iHC in its backbone
  112. # (hc_mult=4) but its MTP head consumes the collapsed states, so
  113. # widening it feeds propose() a 4x-too-wide buffer.
  114. if _target_feeds_hc_residual(vllm_config):
  115. hc_mult = getattr(self.draft_model_config.hf_config, "hc_mult", 1)
  116. self.hidden_size = self.hidden_size * hc_mult
  117. self.vocab_size = self.draft_model_config.get_vocab_size()
  118. self.dtype = vllm_config.model_config.dtype
  119. self.use_fp64_gumbel = vllm_config.model_config.use_fp64_gumbel
  120. self.use_local_argmax_reduction = (
  121. self.speculative_config.use_local_argmax_reduction
  122. )
  123. # DP configuration
  124. self.dp_size = vllm_config.parallel_config.data_parallel_size
  125. self.dp_rank = vllm_config.parallel_config.data_parallel_rank
  126. self.dcp_size = vllm_config.parallel_config.decode_context_parallel_size
  127. self.eplb_state: EplbState | None = None
  128. self.input_buffers = InputBuffers(
  129. max_num_reqs=self.max_num_reqs,
  130. max_num_tokens=self.max_num_tokens,
  131. device=device,
  132. )
  133. self.idx_mapping = torch.zeros(
  134. self.max_num_reqs, dtype=torch.int32, device=device
  135. )
  136. self.temperature = torch.zeros(
  137. self.max_num_reqs, dtype=torch.float32, device=device
  138. )
  139. self.seeds = torch.zeros(self.max_num_reqs, dtype=torch.int64, device=device)
  140. self.draft_tokens = torch.zeros(
  141. self.max_num_reqs,
  142. self.num_speculative_steps,
  143. dtype=torch.int64,
  144. device=device,
  145. )
  146. self.arange_np = np.arange(self.max_num_reqs + 1, dtype=np.int32)
  147. self.draft_is_prefilling = torch.zeros(self.max_num_reqs, dtype=torch.bool)
  148. self.draft_token_confidence_probs = torch.empty_like(
  149. self.draft_tokens, dtype=torch.float32
  150. )
  151. self.enable_adaptive_verification = (
  152. self.speculative_config.enable_adaptive_verification
  153. )
  154. # Draft truncation's log wants the confidences without the trimming.
  155. self.use_acceptance_estimator = self.enable_adaptive_verification or bool(
  156. os.environ.get("VLLM_DRAFT_TRUNC_LOG") or os.environ.get("VLLM_DRAFT_TRUNC_CONTROL")
  157. or float(os.environ.get("VLLM_DRAFT_TRUNC_TAU") or 0) > 0
  158. )
  159. self.acceptance_estimator: OnlineAcceptanceEstimator | None = None
  160. self.draft_logits: torch.Tensor | None = None
  161. if self.speculative_config.draft_sample_method == "probabilistic":
  162. # Pre-temperature logits, cached from the previous decode step.
  163. dtype, fill = self.draft_logits_spec(vllm_config)
  164. self.draft_logits = torch.full(
  165. (
  166. self.max_num_reqs,
  167. self.num_speculative_steps,
  168. self.vocab_size,
  169. ),
  170. fill,
  171. dtype=dtype,
  172. device=device,
  173. )
  174. self.draft_watermarker: DraftWatermarker | None = None
  175. watermark_config = getattr(vllm_config, "watermark_config", None)
  176. if watermark_config is not None:
  177. watermarker = create_watermarker(watermark_config)
  178. self.draft_watermarker = create_speculative_draft_watermarker(
  179. watermarker,
  180. self.max_num_reqs,
  181. device,
  182. watermark_config.allow_target_only_watermarking,
  183. )
  184. self.supports_mm_inputs = False
  185. self.pcp_manager: PCPManager | None = None
  186. @abstractmethod
  187. def load_draft_model(
  188. self, target_model: nn.Module, target_attn_layer_names: set[str]
  189. ) -> nn.Module:
  190. pass
  191. def load_model(self, target_model: nn.Module) -> None:
  192. target_attn_layer_names = set(
  193. get_layers_from_vllm_config(
  194. self.vllm_config,
  195. AttentionLayerBase, # type: ignore[type-abstract]
  196. ).keys()
  197. )
  198. self.model = self.load_draft_model(target_model, target_attn_layer_names)
  199. self._validate_local_argmax_reduction()
  200. all_attn_layers = get_layers_from_vllm_config(
  201. self.vllm_config,
  202. AttentionLayerBase, # type: ignore[type-abstract]
  203. )
  204. self.draft_attn_layer_names = set(all_attn_layers) - target_attn_layer_names
  205. for layer_name in self.draft_attn_layer_names:
  206. all_attn_layers[layer_name].is_draft_layer = True
  207. target_supports_mm = self.vllm_config.model_config.supports_multimodal_inputs
  208. draft_supports_mm = supports_multimodal_embeddings(self.model)
  209. self.supports_mm_inputs = target_supports_mm and draft_supports_mm
  210. if target_supports_mm and not draft_supports_mm:
  211. logger.warning_once(
  212. "Draft model %s does not support external multimodal embeddings. "
  213. "Embeddings from the target model will not be passed to the "
  214. "drafter; using text-only draft inputs instead.",
  215. type(self.model).__name__,
  216. )
  217. if self.use_acceptance_estimator:
  218. if self.use_local_argmax_reduction:
  219. raise ValueError(
  220. "Adaptive verification without a confidence head estimates "
  221. "per-position acceptance from the draft logits, which "
  222. "use_local_argmax_reduction never materializes. Disable one "
  223. "of them."
  224. )
  225. self.acceptance_estimator = OnlineAcceptanceEstimator(
  226. self.max_num_reqs,
  227. self.num_speculative_steps,
  228. self.device,
  229. )
  230. def set_eplb_state(self, eplb_state: EplbState) -> None:
  231. """Inject EPLB state after construction."""
  232. self.eplb_state = eplb_state
  233. def _prepare_eplb_forward(self, num_unpadded_tokens: int) -> None:
  234. """Call EPLB prepare_forward if EPLB is active for the draft model."""
  235. if self.eplb_state is not None:
  236. self.eplb_state.prepare_forward(
  237. self.speculative_config.draft_model_config,
  238. num_unpadded_tokens,
  239. )
  240. @property
  241. def attn_vllm_config(self) -> VllmConfig:
  242. """Config for the draft's attention metadata builders. Overridden by
  243. speculators whose attention mode differs from the target's."""
  244. return self.vllm_config
  245. def set_attn(
  246. self,
  247. model_state: ModelState,
  248. kv_cache_config: KVCacheConfig,
  249. block_tables: BlockTables,
  250. target_input_buffers: InputBuffers,
  251. target_attn_groups: list[list[AttentionGroup]],
  252. ) -> None:
  253. self.model_state = model_state
  254. self.kv_cache_config = kv_cache_config
  255. self.attn_groups, self.attn_cg_support, _ = init_attn_backend(
  256. kv_cache_config,
  257. self.attn_vllm_config,
  258. self.device,
  259. active_layer_names=self.draft_attn_layer_names,
  260. )
  261. self.block_tables = block_tables
  262. # The target model runner's buffers and attention groups. Draft
  263. # prefill reuses the target model's attention metadata, so its
  264. # cudagraph capture must build dummy metadata through the same
  265. # builders and buffers.
  266. self.target_input_buffers = target_input_buffers
  267. self.target_attn_groups = target_attn_groups
  268. def _build_attn_metadata(
  269. self,
  270. num_reqs: int,
  271. batch_desc: BatchExecutionDescriptor,
  272. query_start_loc_np: np.ndarray,
  273. seq_lens_cpu_upper_bound: torch.Tensor,
  274. step: int,
  275. causal: bool | Mapping[int, bool] = True,
  276. dcp_local_seq_lens: torch.Tensor | None = None,
  277. slot_mappings: torch.Tensor | None = None,
  278. ) -> dict[str, Any] | None:
  279. num_reqs_padded = batch_desc.num_reqs or num_reqs
  280. # A FULL graph replays a captured shape whose padded requests each hold
  281. # a full query width, so attention must see the padded token count.
  282. # PIECEWISE/eager needs the actual token count, because batch_desc may
  283. # carry graph or DP padding that no request owns, which would desync it
  284. # from query_start_loc.
  285. num_tokens = (
  286. batch_desc.num_tokens
  287. if batch_desc.cg_mode == CUDAGraphMode.FULL
  288. else int(query_start_loc_np[-1])
  289. )
  290. query_start_loc_cpu = torch.empty(num_reqs_padded + 1, dtype=torch.int32)
  291. query_start_loc_cpu[: num_reqs + 1] = torch.from_numpy(
  292. query_start_loc_np[: num_reqs + 1]
  293. )
  294. query_start_loc_cpu[num_reqs:] = query_start_loc_cpu[num_reqs]
  295. max_query_len = int((query_start_loc_cpu[1:] - query_start_loc_cpu[:-1]).max())
  296. block_tables = [
  297. x[:num_reqs_padded] for x in self.block_tables.input_block_tables
  298. ]
  299. if slot_mappings is None:
  300. slot_mappings = self.block_tables.slot_mappings[:, :num_tokens]
  301. draft_seq_lens_cpu_upper_bound = torch.zeros(
  302. num_reqs_padded, dtype=torch.int32, device="cpu"
  303. )
  304. torch.add(
  305. seq_lens_cpu_upper_bound[:num_reqs],
  306. step,
  307. out=draft_seq_lens_cpu_upper_bound[:num_reqs],
  308. )
  309. draft_seq_lens_cpu_upper_bound[:num_reqs].clamp_(max=self.max_model_len)
  310. if dcp_local_seq_lens is None and self.dcp_size > 1:
  311. # Draft steps advance and rewind their own global sequence lengths,
  312. # so the target model's DCP-local lengths may already be stale.
  313. dcp_local_seq_lens = prepare_dcp_local_seq_lens(
  314. self.input_buffers.dcp_local_seq_lens,
  315. self.input_buffers.seq_lens,
  316. num_reqs,
  317. self.dcp_size,
  318. self.block_tables.cp_rank,
  319. self.block_tables.cp_interleave,
  320. )
  321. attn_metadata = build_attn_metadata(
  322. attn_groups=self.attn_groups,
  323. num_reqs=num_reqs_padded,
  324. num_tokens=num_tokens,
  325. query_start_loc_gpu=self.input_buffers.query_start_loc[
  326. : num_reqs_padded + 1
  327. ],
  328. query_start_loc_cpu=query_start_loc_cpu,
  329. max_query_len=max_query_len,
  330. seq_lens=self.input_buffers.seq_lens[:num_reqs_padded],
  331. dcp_local_seq_lens=(
  332. None
  333. if dcp_local_seq_lens is None
  334. else dcp_local_seq_lens[:num_reqs_padded]
  335. ),
  336. max_seq_len=self.draft_max_seq_len,
  337. block_tables=block_tables,
  338. slot_mappings=slot_mappings,
  339. kv_cache_config=self.kv_cache_config,
  340. causal=causal,
  341. seq_lens_cpu_upper_bound=draft_seq_lens_cpu_upper_bound,
  342. is_prefilling=self.draft_is_prefilling[:num_reqs_padded],
  343. )
  344. return attn_metadata
  345. def draft_logits_spec(self, vllm_config: VllmConfig) -> tuple[torch.dtype, float]:
  346. """Dtype and fill for the cached proposal distribution.
  347. Speculators that write only a subset of columns each step override this.
  348. """
  349. return vllm_config.model_config.head_dtype, 0.0
  350. def _validate_local_argmax_reduction(self) -> None:
  351. if not self.use_local_argmax_reduction:
  352. return
  353. if self.speculative_config.draft_sample_method == "probabilistic":
  354. raise ValueError(
  355. "use_local_argmax_reduction is not compatible with "
  356. "draft_sample_method='probabilistic'."
  357. )
  358. if not hasattr(self.model, "get_top_tokens"):
  359. raise ValueError(
  360. "use_local_argmax_reduction is enabled but draft model "
  361. f"{self.model.__class__.__name__} does not implement "
  362. "get_top_tokens()."
  363. )
  364. logger.info(
  365. "Using local argmax reduction for draft token generation "
  366. "(communication: O(2*tp_size) vs O(vocab_size))."
  367. )
  368. def sample_draft(
  369. self,
  370. hidden_states: torch.Tensor,
  371. sample_src_positions: torch.Tensor,
  372. idx_mapping: torch.Tensor,
  373. temperature: torch.Tensor,
  374. seeds: torch.Tensor,
  375. draft_step: torch.Tensor,
  376. draft_logits: torch.Tensor | None,
  377. ) -> torch.Tensor:
  378. if draft_logits is not None:
  379. logits = self.model.compute_logits(hidden_states)
  380. sampled = gumbel_sample(
  381. logits,
  382. idx_mapping,
  383. temperature,
  384. seeds,
  385. sample_src_positions,
  386. apply_temperature=True,
  387. is_drafting=True,
  388. logits_cache=draft_logits,
  389. logits_cache_col=draft_step,
  390. use_fp64=self.use_fp64_gumbel,
  391. )
  392. if self.draft_watermarker is not None:
  393. sampled = self.draft_watermarker.sample(
  394. logits, sampled, idx_mapping, temperature
  395. )
  396. elif self.use_local_argmax_reduction:
  397. return self.model.get_top_tokens(hidden_states)
  398. else:
  399. logits = self.model.compute_logits(hidden_states)
  400. sampled = logits.argmax(dim=-1)
  401. self._maybe_predict_acceptance(logits, idx_mapping, draft_step)
  402. return sampled
  403. def _maybe_predict_acceptance(
  404. self, logits: torch.Tensor, idx_mapping: torch.Tensor, draft_step: torch.Tensor
  405. ) -> None:
  406. if self.acceptance_estimator is not None:
  407. self.acceptance_estimator.predict(
  408. logits,
  409. idx_mapping,
  410. draft_step,
  411. self.draft_token_confidence_probs,
  412. self.temperature,
  413. )
  414. def observe_verification(
  415. self,
  416. idx_mapping: torch.Tensor,
  417. num_sampled: torch.Tensor,
  418. num_rejected: torch.Tensor,
  419. ) -> None:
  420. """Fold the target's verdict on the last drafts into the estimator.
  421. Must run before the next `propose`, which overwrites the per-slot
  422. features the verdict grades.
  423. """
  424. if self.acceptance_estimator is not None:
  425. self.acceptance_estimator.step(idx_mapping, num_sampled, num_rejected)
  426. def prepare_watermarking(
  427. self, contexts: torch.Tensor, watermarking: torch.Tensor
  428. ) -> None:
  429. if self.draft_watermarker is None:
  430. return
  431. self.draft_watermarker.prepare(contexts, watermarking)
  432. def _copy_request_inputs(
  433. self,
  434. num_reqs: int,
  435. # [num_reqs]
  436. idx_mapping: torch.Tensor,
  437. # [max_num_reqs]
  438. temperature: torch.Tensor,
  439. # [max_num_reqs]
  440. seeds: torch.Tensor,
  441. dummy_run: bool = False,
  442. ) -> None:
  443. # Copy temperature, seeds, and idx mapping to the pre-allocated buffers.
  444. # NOTE(woosuk): For draft sampling, we only consider the temperature
  445. # and ignore the other sampling parameters such as top_k and top_p,
  446. # for simplicity and performance.
  447. # While this may slightly degrade the acceptance rate, it does not
  448. # affect the output distribution after rejection sampling.
  449. self.temperature.copy_(temperature)
  450. self.seeds.copy_(seeds)
  451. # idx_mapping == -1 marks a row the drafter must not act on: sampling
  452. # skips it and compute_slot_mappings emits PAD. CUDA-graph padded rows
  453. # always get it; a dummy batch gets it for every row, since its arange
  454. # idx_mapping names request-state slots with stale block tables.
  455. if dummy_run:
  456. self.idx_mapping.fill_(-1)
  457. else:
  458. self.idx_mapping[:num_reqs].copy_(idx_mapping)
  459. self.idx_mapping[num_reqs:].fill_(-1)
  460. def _build_uniform_batch_dp_sync(
  461. self,
  462. target_dp_sync: DPSyncState,
  463. num_reqs: int,
  464. num_query_per_req: int = 1,
  465. ) -> tuple[DPSyncState, int]:
  466. num_batch_tokens = target_dp_sync.num_reqs * num_query_per_req
  467. assert num_reqs * num_query_per_req <= num_batch_tokens, (
  468. "reusing a DP sync that does not cover this batch's requests"
  469. )
  470. return replace(
  471. target_dp_sync,
  472. num_tokens_across_dp=torch.full_like(
  473. target_dp_sync.num_tokens_across_dp, num_batch_tokens
  474. ),
  475. uniform_token_count=num_query_per_req,
  476. eager=False,
  477. ), num_batch_tokens
  478. def _build_uniform_attn_metadata(
  479. self,
  480. batch_desc: BatchExecutionDescriptor,
  481. num_reqs: int,
  482. num_query_per_req: int,
  483. seq_lens_cpu_upper_bound: torch.Tensor,
  484. step: int,
  485. causal: bool | Mapping[int, bool] = True,
  486. dcp_local_seq_lens: torch.Tensor | None = None,
  487. ) -> dict[str, Any] | None:
  488. query_start_loc_np = self.arange_np[: num_reqs + 1] * num_query_per_req
  489. return self._build_attn_metadata(
  490. num_reqs=num_reqs,
  491. batch_desc=batch_desc,
  492. query_start_loc_np=query_start_loc_np,
  493. seq_lens_cpu_upper_bound=seq_lens_cpu_upper_bound,
  494. step=step,
  495. causal=causal,
  496. dcp_local_seq_lens=dcp_local_seq_lens,
  497. )