main
py 164 lines 6.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """``get_top_tokens`` on the DeepSeek V4 MTP drafter.
4
5 The speculator opts into ``use_local_argmax_reduction`` by probing the draft
6 model for ``get_top_tokens``, and expects it to select the same greedy draft
7 token the full-vocab argmax would. V4 reaches its LM head through hc_head and
8 the shared head's RMSNorm, so both methods have to share that transform.
9 """
10
11 from types import SimpleNamespace
12 from unittest import mock
13
14 import pytest
15 import torch
16 from transformers import PreTrainedConfig
17
18 from vllm.model_executor.layers.logits_processor import LogitsProcessor
19 from vllm.model_executor.layers.mhc import HCHeadOp
20 from vllm.model_executor.models.deepseek_mtp import SharedHead
21 from vllm.platforms import current_platform
22
23 if not current_platform.is_rocm():
24 pytest.skip(
25 "get_top_tokens is implemented on the ROCm DeepSeek V4 MTP drafter only.",
26 allow_module_level=True,
27 )
28
29 from vllm.models.deepseek_v4.amd.mtp import ( # noqa: E402
30 DeepSeekV4MTP,
31 DeepSeekV4MultiTokenPredictor,
32 )
33
34 DEVICE_TYPE = current_platform.device_type
35 START_LAYER_IDX = 61
36 HIDDEN_SIZE = 4096
37 HC_MULT = 4
38 RMS_NORM_EPS = 1e-6
39 HC_EPS = 1e-6
40 _EMBEDDING = "vllm.model_executor.layers.vocab_parallel_embedding"
41
42
43 def _make_predictor(vocab_size: int) -> DeepSeekV4MultiTokenPredictor:
44 config = PreTrainedConfig()
45 config.hidden_size = HIDDEN_SIZE
46 config.vocab_size = vocab_size
47 config.rms_norm_eps = RMS_NORM_EPS
48
49 # A single vocab shard, so the reduction returns the local argmax without
50 # touching the process group.
51 with (
52 mock.patch(f"{_EMBEDDING}.get_tensor_model_parallel_rank", return_value=0),
53 mock.patch(
54 f"{_EMBEDDING}.get_tensor_model_parallel_world_size", return_value=1
55 ),
56 ):
57 shared_head = SharedHead(config=config, prefix="shared_head")
58
59 assert shared_head.head is not None
60 generator = torch.Generator().manual_seed(7)
61 with torch.no_grad():
62 shared_head.head.weight.normal_(0.0, 0.02, generator=generator)
63 # The default norm weight is all ones, and the argmax is invariant to
64 # the norm's per-row rescale, so a uniform gain would hide a skipped
65 # shared_head. Vary it so the comparison below can see one.
66 shared_head.norm.weight.normal_(1.0, 0.5, generator=generator)
67 shared_head = shared_head.to(device=DEVICE_TYPE, dtype=torch.bfloat16)
68
69 hc_head_fn = torch.randn(
70 HC_MULT, HC_MULT * HIDDEN_SIZE, dtype=torch.float32, generator=generator
71 )
72 # Non-trivial gate scale and bias: hc_head mixes the hc_mult streams with
73 # these, and defaults of 0 would make a skipped hc_head harder to see.
74 hc_head_scale = torch.randn(1, dtype=torch.float32, generator=generator) * 0.1
75 hc_head_base = torch.randn(HC_MULT, dtype=torch.float32, generator=generator) * 0.1
76
77 mtp_layer = SimpleNamespace(
78 config=config,
79 hc_mult=HC_MULT,
80 hc_eps=HC_EPS,
81 rms_norm_eps=RMS_NORM_EPS,
82 hc_head_op=HCHeadOp(),
83 hc_head_fn=(hc_head_fn * 1e-4).to(DEVICE_TYPE),
84 hc_head_scale=hc_head_scale.to(DEVICE_TYPE),
85 hc_head_base=hc_head_base.to(DEVICE_TYPE),
86 shared_head=shared_head,
87 )
88
89 # Only the attributes the two methods read; a real predictor would also
90 # build the MLA/MoE draft block, which is irrelevant here.
91 predictor = object.__new__(DeepSeekV4MultiTokenPredictor)
92 torch.nn.Module.__init__(predictor)
93 predictor.layers = {str(START_LAYER_IDX): mtp_layer}
94 predictor.mtp_start_layer_idx = START_LAYER_IDX
95 predictor.num_mtp_layers = 1
96 predictor.logits_processor = LogitsProcessor(vocab_size)
97 return predictor
98
99
100 def _hidden_states(num_tokens: int, seed: int) -> torch.Tensor:
101 """The flat pre-hc_head residual the MTP layer returns."""
102 generator = torch.Generator().manual_seed(seed)
103 hidden_states = torch.randn(
104 num_tokens, HC_MULT * HIDDEN_SIZE, dtype=torch.float32, generator=generator
105 )
106 return hidden_states.to(device=DEVICE_TYPE, dtype=torch.bfloat16)
107
108
109 def _assert_selects_max_logit(top: torch.Tensor, logits: torch.Tensor) -> None:
110 """The selected token must carry the row's maximum logit.
111
112 hc_head emits bf16, so rows can tie for the maximum and then no single
113 index is the right answer; compare the values the two paths select.
114 """
115 assert top.shape == (logits.shape[0],)
116 selected = logits.gather(1, top.unsqueeze(1)).squeeze(1)
117 assert torch.equal(selected, logits.max(dim=-1).values)
118
119
120 def test_get_top_tokens_matches_full_argmax(default_vllm_config):
121 """Same draft token as the path taken when the reduction is off.
122
123 ``get_top_tokens`` has to run the same pre-head transform as
124 ``compute_logits``: hc_head over the (T, hc_mult, D) residual, then the
125 shared head's RMSNorm. Dropping either one returns a different draft token
126 instead of failing, which is what this comparison catches.
127 """
128 predictor = _make_predictor(vocab_size=512)
129
130 for num_tokens in (1, 16, 128):
131 hidden_states = _hidden_states(num_tokens, seed=11)
132
133 logits = predictor.compute_logits(hidden_states)
134 top = predictor.get_top_tokens(hidden_states)
135
136 assert top.dtype == torch.int64
137 _assert_selects_max_logit(top, logits)
138
139
140 def test_get_top_tokens_honors_padded_vocab(default_vllm_config):
141 """Padding entries above ``org_vocab_size`` must never win the argmax."""
142 vocab_size = 500
143 predictor = _make_predictor(vocab_size)
144 head = predictor.layers[str(START_LAYER_IDX)].shared_head.head
145 assert head.shard_indices.num_org_vocab_padding > 0
146
147 hidden_states = _hidden_states(32, seed=13)
148 logits = predictor.compute_logits(hidden_states)
149 top = predictor.get_top_tokens(hidden_states)
150
151 _assert_selects_max_logit(top, logits)
152 assert (top < vocab_size).all()
153
154
155 def test_wrapper_forwards_spec_step_idx():
156 """``DeepSeekV4MTP`` is what the speculator probes, so it needs the method."""
157 assert hasattr(DeepSeekV4MTP, "get_top_tokens")
158
159 hidden_states = torch.empty(0)
160 wrapper = object.__new__(DeepSeekV4MTP)
161 wrapper.model = SimpleNamespace(get_top_tokens=lambda *args: args)
162
163 assert DeepSeekV4MTP.get_top_tokens(wrapper, hidden_states, 2) == (hidden_states, 2)
164 assert DeepSeekV4MTP.get_top_tokens(wrapper, hidden_states) == (hidden_states, 0)