main
py 148 lines 5.54 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 DynamicSDSchedule = list[tuple[int, int, int]]
5
6
7 def validate_and_normalize_dynamic_sd_schedule(
8 num_speculative_tokens_per_batch_size: object,
9 ) -> DynamicSDSchedule:
10 """Validate and normalize a Dynamic SD batch-size schedule.
11
12 The schedule is expressed as a list of inclusive ranges:
13
14 ``[(range_start, range_end, num_speculative_tokens), ...]``
15 """
16 if num_speculative_tokens_per_batch_size is None:
17 raise ValueError(
18 "num_speculative_tokens_per_batch_size is required for "
19 "dynamic speculative decoding."
20 )
21 if not isinstance(num_speculative_tokens_per_batch_size, list):
22 raise ValueError(
23 "num_speculative_tokens_per_batch_size must be a non-empty list of "
24 "(range_start, range_end, num_speculative_tokens) entries."
25 )
26 if not num_speculative_tokens_per_batch_size:
27 raise ValueError("num_speculative_tokens_per_batch_size must not be empty.")
28
29 parsed_schedule: DynamicSDSchedule = []
30 for entry in num_speculative_tokens_per_batch_size:
31 if not isinstance(entry, list | tuple) or len(entry) != 3:
32 raise ValueError(
33 "Each num_speculative_tokens_per_batch_size entry must be a "
34 "3-item sequence: (range_start, range_end, num_speculative_tokens)."
35 )
36
37 range_start, range_end, num_speculative_tokens = (
38 int(entry[0]),
39 int(entry[1]),
40 int(entry[2]),
41 )
42
43 if range_start <= 0 or range_end <= 0:
44 raise ValueError(
45 f"Batch-size range ({range_start}, {range_end}) must be positive."
46 )
47 if range_start > range_end:
48 raise ValueError(
49 "Batch-size range start must be <= end for "
50 f"({range_start}, {range_end}, {num_speculative_tokens})."
51 )
52 if num_speculative_tokens < 0:
53 raise ValueError(
54 "num_speculative_tokens_per_batch_size values must be >= 0."
55 )
56
57 parsed_schedule.append((range_start, range_end, num_speculative_tokens))
58
59 parsed_schedule.sort(key=lambda entry: entry[0])
60
61 previous_end = 0
62 for range_start, range_end, _ in parsed_schedule:
63 if range_start <= previous_end:
64 raise ValueError("Batch-size ranges must be non-overlapping and sorted.")
65 previous_end = range_end
66
67 first_range_start = parsed_schedule[0][0]
68 if first_range_start != 1:
69 raise ValueError(
70 "The first batch-size range must start at 1 so every runtime "
71 "batch size has a defined schedule."
72 )
73
74 return parsed_schedule
75
76
77 def build_dynamic_sd_schedule_lookup(
78 num_speculative_tokens_per_batch_size: object,
79 vllm_max_batch_size: int,
80 vllm_num_speculative_tokens: int,
81 ) -> list[int]:
82 """Expand the configured schedule into a dense batch_size -> K lookup.
83
84 "dense_schedule" means a 1-indexed lookup table where index ``batch_size``
85 stores the exact K to use for that runtime batch size. This lets the
86 scheduler do a simple array lookup instead of searching the configured
87 ranges on every scheduling step.
88 """
89 if vllm_max_batch_size <= 0:
90 raise ValueError("vllm_max_batch_size must be > 0.")
91 if vllm_num_speculative_tokens <= 0:
92 raise ValueError("vllm_num_speculative_tokens must be > 0.")
93
94 parsed_schedule = validate_and_normalize_dynamic_sd_schedule(
95 num_speculative_tokens_per_batch_size
96 )
97
98 # Index 0 is intentionally unused so that valid runtime batch sizes can be
99 # looked up directly as dense_schedule[batch_size].
100 dense_schedule = [0] * (vllm_max_batch_size + 1)
101 next_batch_size = 1
102 last_num_speculative_tokens: int | None = None
103
104 for range_start, range_end, num_speculative_tokens in parsed_schedule:
105 if range_start > next_batch_size and last_num_speculative_tokens is not None:
106 # Fill any gap before the next configured range by carrying forward
107 # the previous K. For example, [(1, 16, 3), (32, 128, 2)] should map
108 # batch sizes 17-31 to K=3.
109 for batch_size in range(
110 next_batch_size,
111 min(range_start, vllm_max_batch_size + 1),
112 ):
113 dense_schedule[batch_size] = min(
114 vllm_num_speculative_tokens,
115 last_num_speculative_tokens,
116 )
117
118 # Fill the current configured inclusive range with its K value.
119 for batch_size in range(
120 max(range_start, next_batch_size),
121 min(range_end, vllm_max_batch_size) + 1,
122 ):
123 dense_schedule[batch_size] = min(
124 vllm_num_speculative_tokens,
125 num_speculative_tokens,
126 )
127
128 next_batch_size = max(next_batch_size, range_end + 1)
129 last_num_speculative_tokens = num_speculative_tokens
130
131 if next_batch_size > vllm_max_batch_size:
132 break
133
134 if last_num_speculative_tokens is None:
135 raise ValueError(
136 "num_speculative_tokens_per_batch_size must contain at least "
137 "one valid batch-size range."
138 )
139
140 # Fill the tail after the final configured range by carrying forward the
141 # last K through vllm_max_batch_size.
142 for batch_size in range(next_batch_size, vllm_max_batch_size + 1):
143 dense_schedule[batch_size] = min(
144 vllm_num_speculative_tokens,
145 last_num_speculative_tokens,
146 )
147
148 return dense_schedule