speculator.py 21 KB

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