main
py 150 lines 5.59 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import torch
4
5 from vllm.logger import init_logger
6
7 logger = init_logger(__name__)
8
9
10 def _detect_space_prefix(tokenizer) -> tuple[str, ...]:
11 """Detect the space-prefix character(s) by tokenizing a literal space.
12
13 Different tokenizer families mark word-initial spaces differently:
14 BPE uses 'Ġ' (U+0120), SentencePiece uses '▁' (U+2581). Probing at
15 runtime avoids hardcoding assumptions and correctly handles mixed-family
16 pairs (e.g. BPE draft + SentencePiece target).
17 """
18 try:
19 space_ids = tokenizer.encode(" a", add_special_tokens=False)
20 if space_ids:
21 tok_str = tokenizer.convert_ids_to_tokens(space_ids[0])
22 if (
23 isinstance(tok_str, str)
24 and len(tok_str) > 1
25 and tok_str.endswith("a")
26 and tok_str[0] not in (" ", " ")
27 ):
28 return (tok_str[:-1],)
29 except Exception:
30 pass
31 # Fallback: cover both BPE (Ġ U+0120) and SentencePiece (▁ U+2581)
32 return ("\u0120", "\u2581")
33
34
35 def _normalize_token(token: str, space_prefixes: tuple[str, ...]) -> str:
36 for prefix in space_prefixes:
37 if token.startswith(prefix):
38 return " " + token[len(prefix) :]
39 return token
40
41
42 def _get_unk_token_id(tokenizer, role: str) -> int:
43 """Return a safe fallback token ID for out-of-intersection tokens.
44
45 Preferred: unk_token_id → eos_token_id → ValueError.
46 Checking with ``is not None`` is required because token ID 0 is a valid
47 (and common) unk ID on many tokenizers; using ``or 0`` would silently
48 mishandle those cases.
49 """
50 unk = getattr(tokenizer, "unk_token_id", None)
51 if unk is not None:
52 return unk
53 eos = getattr(tokenizer, "eos_token_id", None)
54 if eos is not None:
55 logger.warning(
56 "VocabMapping: %s has no unk_token_id; "
57 "falling back to eos_token_id=%d for out-of-intersection tokens",
58 role,
59 eos,
60 )
61 return eos
62 raise ValueError(
63 f"VocabMapping: {role} has neither unk_token_id nor eos_token_id; "
64 "cannot safely map out-of-intersection tokens"
65 )
66
67
68 class VocabMapping:
69 def __init__(
70 self,
71 target_tokenizer,
72 draft_tokenizer,
73 target_vocab_size,
74 draft_vocab_size,
75 device,
76 ):
77 self.target_vocab_size = target_vocab_size
78 self.draft_vocab_size = draft_vocab_size
79 self.device = device
80 self.target_unk_token_id = _get_unk_token_id(
81 target_tokenizer, "target tokenizer"
82 )
83 self.draft_unk_token_id = _get_unk_token_id(draft_tokenizer, "draft tokenizer")
84
85 target_prefixes = _detect_space_prefix(target_tokenizer)
86 draft_prefixes = _detect_space_prefix(draft_tokenizer)
87
88 target_vocab = target_tokenizer.get_vocab()
89 draft_vocab = draft_tokenizer.get_vocab()
90
91 target_normalized = {}
92 for token, tid in target_vocab.items():
93 norm = _normalize_token(token, target_prefixes)
94 if norm not in target_normalized:
95 target_normalized[norm] = tid
96
97 draft_normalized = {}
98 for token, tid in draft_vocab.items():
99 norm = _normalize_token(token, draft_prefixes)
100 if norm not in draft_normalized:
101 draft_normalized[norm] = tid
102
103 common_tokens = set(target_normalized.keys()) & set(draft_normalized.keys())
104
105 draft_to_target = torch.full((draft_vocab_size,), -1, dtype=torch.long)
106 target_to_draft = torch.full((target_vocab_size,), -1, dtype=torch.long)
107 intersection_mask_draft = torch.zeros(draft_vocab_size, dtype=torch.bool)
108
109 for norm_token in common_tokens:
110 t_id = target_normalized[norm_token]
111 d_id = draft_normalized[norm_token]
112 if t_id < target_vocab_size and d_id < draft_vocab_size:
113 draft_to_target[d_id] = t_id
114 target_to_draft[t_id] = d_id
115 intersection_mask_draft[d_id] = True
116
117 self.draft_to_target_ids = draft_to_target.to(device)
118 self.target_to_draft_ids = target_to_draft.to(device)
119 self.intersection_mask_draft = intersection_mask_draft.to(device)
120 self.intersection_size = int(intersection_mask_draft.sum().item())
121
122 logger.info(
123 "VocabMapping initialized: target_vocab=%d, draft_vocab=%d, "
124 "intersection=%d (%.1f%% of draft, %.1f%% of target)",
125 target_vocab_size,
126 draft_vocab_size,
127 self.intersection_size,
128 100.0 * self.intersection_size / max(draft_vocab_size, 1),
129 100.0 * self.intersection_size / max(target_vocab_size, 1),
130 )
131
132 if self.intersection_size < 100:
133 logger.warning(
134 "Very small vocabulary intersection (%d tokens).",
135 self.intersection_size,
136 )
137
138 def map_target_to_draft_ids(self, target_ids):
139 draft_ids = self.target_to_draft_ids[target_ids] # new tensor; no clone needed
140 draft_ids[draft_ids == -1] = self.draft_unk_token_id
141 return draft_ids.to(target_ids.dtype)
142
143 def map_draft_to_target_ids(self, draft_ids):
144 target_ids = self.draft_to_target_ids[draft_ids] # new tensor; no clone needed
145 target_ids[target_ids == -1] = self.target_unk_token_id
146 return target_ids.to(draft_ids.dtype)
147
148 def constrain_draft_logits(self, logits):
149 # masked_fill returns a new tensor; no clone needed
150 return logits.masked_fill(~self.intersection_mask_draft, float("-inf"))