[Scheduler] Add shortest-prefill-first scheduling (#40024)
This commit is contained in:
@@ -99,6 +99,7 @@ class Schedule(msgspec.Struct):
|
||||
"priority",
|
||||
"routing-key",
|
||||
"hrrn",
|
||||
"shortest-prefill-first",
|
||||
],
|
||||
),
|
||||
] = "fcfs"
|
||||
|
||||
@@ -221,6 +221,7 @@ class CacheAwarePolicy(Enum):
|
||||
LPM = "lpm" # longest prefix match
|
||||
DFS_WEIGHT = "dfs-weight" # depth-first search weighting
|
||||
HRRN = "hrrn" # highest response ratio next, token-based aging
|
||||
SHORTEST_PREFILL_FIRST = "shortest-prefill-first"
|
||||
|
||||
|
||||
class CacheAgnosticPolicy(Enum):
|
||||
@@ -249,6 +250,7 @@ class SchedulePolicy:
|
||||
self.enable_priority_scheduling = enable_priority_scheduling
|
||||
self.schedule_low_priority_values_first = schedule_low_priority_values_first
|
||||
self.priority_sign = 1 if schedule_low_priority_values_first else -1
|
||||
self._shortest_prefill_calls = 0
|
||||
|
||||
# It is used to find the matching prefix for in-batch prefix caching.
|
||||
self.waiting_queue_radix_tree = RadixCache.create_simulated()
|
||||
@@ -294,6 +296,17 @@ class SchedulePolicy:
|
||||
SchedulePolicy._sort_by_hrrn(
|
||||
waiting_queue, temporary_deprioritized, processed_tokens
|
||||
)
|
||||
elif policy == CacheAwarePolicy.SHORTEST_PREFILL_FIRST:
|
||||
SchedulePolicy._sort_by_shortest_prefill(
|
||||
waiting_queue, temporary_deprioritized
|
||||
)
|
||||
self._shortest_prefill_calls += 1
|
||||
if waiting_queue and self._shortest_prefill_calls % 128 == 1:
|
||||
logger.info(
|
||||
"Experimental shortest-prefill-first: queue=%d shortest_uncached=%d",
|
||||
len(waiting_queue),
|
||||
self._shortest_prefill_work(waiting_queue[0]),
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown CacheAware Policy: {policy=}")
|
||||
else:
|
||||
@@ -342,6 +355,10 @@ class SchedulePolicy:
|
||||
try:
|
||||
policy_enum = CacheAwarePolicy(policy)
|
||||
if getattr(tree_cache, "disable", True):
|
||||
if policy_enum == CacheAwarePolicy.SHORTEST_PREFILL_FIRST:
|
||||
raise ValueError(
|
||||
"Experimental shortest-prefill-first requires prefix caching"
|
||||
)
|
||||
# If tree_cache is disabled, using CacheAgnosticPolicy policy
|
||||
return CacheAgnosticPolicy.FCFS
|
||||
return policy_enum
|
||||
@@ -410,6 +427,50 @@ class SchedulePolicy:
|
||||
)
|
||||
return temporary_deprioritized
|
||||
|
||||
@staticmethod
|
||||
def _shortest_prefill_work(r: Req) -> int:
|
||||
return max(
|
||||
1,
|
||||
len(r.origin_input_ids) + len(r.output_ids) - r.num_matched_prefix_tokens,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _sort_by_shortest_prefill(
|
||||
waiting_queue: List[Req], temporary_deprioritized: Set[int]
|
||||
) -> None:
|
||||
# Prioritize short uncached prefills while deferring duplicate prefixes.
|
||||
waiting_queue.sort(
|
||||
key=lambda r: (
|
||||
r.rid in temporary_deprioritized,
|
||||
SchedulePolicy._shortest_prefill_work(r),
|
||||
r.time_stats.wait_queue_entry_time,
|
||||
)
|
||||
)
|
||||
|
||||
def shortest_prefill_chunk_limit(
|
||||
self, chunked_req: Req, waiting_queue: List[Req], budget: int, page_size: int
|
||||
) -> Optional[int]:
|
||||
"""Cap the active prefill chunk to reserve tokens for shorter waiting requests."""
|
||||
if (
|
||||
self.policy != CacheAwarePolicy.SHORTEST_PREFILL_FIRST
|
||||
or budget < 2 * page_size
|
||||
):
|
||||
return None
|
||||
remaining = len(chunked_req.full_untruncated_fill_ids) - len(
|
||||
chunked_req.prefix_indices
|
||||
)
|
||||
reserved = 0
|
||||
for req in waiting_queue:
|
||||
work = self._shortest_prefill_work(req)
|
||||
charge = _ceil_div(work, page_size) * page_size
|
||||
if work >= remaining or reserved + charge > budget - page_size:
|
||||
break
|
||||
reserved += charge
|
||||
if not reserved:
|
||||
return None
|
||||
# Page alignment keeps continuation boundaries allocator-compatible.
|
||||
return (budget - reserved) // page_size * page_size
|
||||
|
||||
@staticmethod
|
||||
def _sort_by_longest_prefix(
|
||||
waiting_queue: List[Req], temporary_deprioritized: Set[int]
|
||||
@@ -583,6 +644,7 @@ class PrefillAdder:
|
||||
self.new_token_ratio = new_token_ratio
|
||||
self.rem_input_tokens = rem_input_tokens - num_mixed_decode_tokens
|
||||
self.rem_chunk_tokens = rem_chunk_tokens
|
||||
self.chunked_req_limit: Optional[int] = None
|
||||
self.dllm_config = dllm_config
|
||||
self.exact_chunk_fill = _use_exact_chunk_fill() and dllm_config is None
|
||||
|
||||
@@ -968,6 +1030,10 @@ class PrefillAdder:
|
||||
waiting_queue_len=self.waiting_queue_len,
|
||||
)
|
||||
|
||||
if self.chunked_req_limit is not None:
|
||||
assert self.chunked_req_limit > 0
|
||||
_rem_tokens = min(_rem_tokens, self.chunked_req_limit)
|
||||
|
||||
cand_extend_input_len = len(req.full_untruncated_fill_ids) - len(
|
||||
req.prefix_indices
|
||||
)
|
||||
@@ -1189,6 +1255,7 @@ class PrefillAdder:
|
||||
host_hit_length=req.host_hit_length,
|
||||
swa_host_hit_length=req.swa_host_hit_length,
|
||||
truncation_align_size=truncation_align_size,
|
||||
has_chunked_req=has_chunked_req,
|
||||
)
|
||||
if isinstance(admission, AddReqResult):
|
||||
return admission
|
||||
@@ -1252,6 +1319,7 @@ class PrefillAdder:
|
||||
host_hit_length=0,
|
||||
swa_host_hit_length=0,
|
||||
truncation_align_size=truncation_align_size,
|
||||
has_chunked_req=has_chunked_req,
|
||||
)
|
||||
if isinstance(admission, AddReqResult):
|
||||
return admission
|
||||
@@ -1272,6 +1340,7 @@ class PrefillAdder:
|
||||
host_hit_length: int,
|
||||
swa_host_hit_length: int,
|
||||
truncation_align_size: Optional[int],
|
||||
has_chunked_req: bool = False,
|
||||
) -> _PrefillAdmission | AddReqResult:
|
||||
"""Select a prefill shape without allocating or publishing cached KV."""
|
||||
prefix_len = len(req.prefix_indices) + host_hit_length
|
||||
@@ -1314,6 +1383,12 @@ class PrefillAdder:
|
||||
return AddReqResult.OTHER
|
||||
max_new_tokens = 0
|
||||
elif chunk_tokens_limit is not None and chunk_fit_tokens > chunk_tokens_limit:
|
||||
if (
|
||||
has_chunked_req
|
||||
and get_schedule().schedule_policy == "shortest-prefill-first"
|
||||
):
|
||||
# Only one unfinished chunked request can be tracked.
|
||||
return AddReqResult.OTHER
|
||||
if self.exact_chunk_fill:
|
||||
# Take the remainder verbatim so the batch hits exactly
|
||||
# chunked_prefill_size. `chunk_fit_tokens > chunk_tokens_limit`
|
||||
|
||||
@@ -3921,6 +3921,12 @@ class Scheduler(
|
||||
|
||||
if self.chunked_req is not None:
|
||||
self.chunked_req.init_next_round_input()
|
||||
adder.chunked_req_limit = self.policy.shortest_prefill_chunk_limit(
|
||||
self.chunked_req,
|
||||
self.waiting_queue,
|
||||
adder.rem_chunk_tokens or 0,
|
||||
self.page_size,
|
||||
)
|
||||
self.chunked_req = adder.add_chunked_req(self.chunked_req)
|
||||
|
||||
if self.enable_lora:
|
||||
|
||||
Reference in New Issue
Block a user