[Scheduler] Add HRRN schedule policy to significantly reduce TTFT (#32911)
Co-authored-by: Xiaoyu Zhang <1182563586@qq.com>
This commit is contained in:
co-authored by
Xiaoyu Zhang
parent
5097f9ac95
commit
4df5df911b
@@ -95,6 +95,7 @@ class Schedule:
|
||||
"lof",
|
||||
"priority",
|
||||
"routing-key",
|
||||
"hrrn",
|
||||
],
|
||||
),
|
||||
] = "fcfs"
|
||||
|
||||
@@ -486,6 +486,9 @@ class PrefillBootstrapQueue:
|
||||
bootstrapped_reqs.append(req)
|
||||
indices_to_remove.add(i)
|
||||
req.time_stats.set_wait_queue_entry_time()
|
||||
req.arrival_processed_tokens = (
|
||||
self.scheduler.processed_tokens_counter
|
||||
)
|
||||
elif poll == KVPoll.WaitingForInput:
|
||||
if should_force_retry(req): # skip checking for testing
|
||||
if not self.ensure_metadata_buffer(req):
|
||||
@@ -496,6 +499,7 @@ class PrefillBootstrapQueue:
|
||||
bootstrapped_reqs.append(req)
|
||||
indices_to_remove.add(i)
|
||||
req.time_stats.set_wait_queue_entry_time()
|
||||
req.arrival_processed_tokens = self.scheduler.processed_tokens_counter
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Unexpected poll state {poll} for req {req.rid} in pop_bootstrapped"
|
||||
@@ -1457,4 +1461,5 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
if self.metrics_reporter.enable_metrics:
|
||||
self.metrics_collector.increment_prefill_retries(1)
|
||||
req.time_stats.set_wait_queue_entry_time()
|
||||
req.arrival_processed_tokens = self.processed_tokens_counter
|
||||
self.waiting_queue.insert(0, req)
|
||||
|
||||
@@ -1323,6 +1323,9 @@ class Req(ReqDllmMixin):
|
||||
# For hisparse
|
||||
self.hisparse_staging = False
|
||||
|
||||
# Snapshot of the scheduler prefill-token counter taken at waiting_queue entry; used by HRRN aging.
|
||||
self.arrival_processed_tokens: int = 0
|
||||
|
||||
@property
|
||||
def seqlen(self) -> int:
|
||||
"""Get the current sequence length of the request."""
|
||||
|
||||
@@ -207,6 +207,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
|
||||
|
||||
|
||||
class CacheAgnosticPolicy(Enum):
|
||||
@@ -240,7 +241,10 @@ class SchedulePolicy:
|
||||
self.waiting_queue_radix_tree = RadixCache.create_simulated()
|
||||
|
||||
def calc_priority(
|
||||
self, waiting_queue: List[Req], running_batch: Optional[ScheduleBatch] = None
|
||||
self,
|
||||
waiting_queue: List[Req],
|
||||
running_batch: Optional[ScheduleBatch] = None,
|
||||
processed_tokens: int = 0,
|
||||
) -> None:
|
||||
policy = self._determine_active_policy(waiting_queue)
|
||||
|
||||
@@ -273,6 +277,10 @@ class SchedulePolicy:
|
||||
)
|
||||
elif policy == CacheAwarePolicy.DFS_WEIGHT:
|
||||
SchedulePolicy._sort_by_dfs_weight(waiting_queue, self.tree_cache)
|
||||
elif policy == CacheAwarePolicy.HRRN:
|
||||
SchedulePolicy._sort_by_hrrn(
|
||||
waiting_queue, temporary_deprioritized, processed_tokens
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown CacheAware Policy: {policy=}")
|
||||
else:
|
||||
@@ -293,7 +301,14 @@ class SchedulePolicy:
|
||||
raise ValueError(f"Unknown CacheAgnostic Policy: {policy=}")
|
||||
|
||||
def _determine_active_policy(self, waiting_queue: List[Req]) -> Policy:
|
||||
if self.policy == CacheAwarePolicy.LPM and len(waiting_queue) > 128:
|
||||
if (
|
||||
self.policy
|
||||
in (
|
||||
CacheAwarePolicy.LPM,
|
||||
CacheAwarePolicy.HRRN,
|
||||
)
|
||||
and len(waiting_queue) > 128
|
||||
):
|
||||
# Turn off the expensive prefix matching and sorting when the #queue is large.
|
||||
return CacheAgnosticPolicy.FCFS
|
||||
return self.policy
|
||||
@@ -395,6 +410,48 @@ class SchedulePolicy:
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _uncached_len(r: Req) -> int:
|
||||
"""Number of tokens that must actually be prefilled for this req
|
||||
(all cache levels — device + host via hicache — counted as cached)."""
|
||||
return max(0, len(r.origin_input_ids) - r.num_matched_prefix_tokens)
|
||||
|
||||
@staticmethod
|
||||
def _sort_by_hrrn(
|
||||
waiting_queue: List[Req],
|
||||
temporary_deprioritized: Set[int],
|
||||
processed_tokens: int,
|
||||
) -> None:
|
||||
"""Highest Response Ratio Next, with token-based aging.
|
||||
|
||||
Equivalence with classic HRRN when throughput is constant:
|
||||
ratio = 1 + wait_sec / est_prefill_time
|
||||
= 1 + (processed_tokens - arrival_processed_tokens) / uncached
|
||||
|
||||
Caller (Scheduler) contract:
|
||||
- Maintain a monotonically increasing counter of prefill tokens processed so far
|
||||
(accumulate batch.extend_num_tokens per forward). Pass it in as `processed_tokens`.
|
||||
- Snapshot `req.arrival_processed_tokens = counter` when the req enters waiting_queue
|
||||
(pop_bootstrapped for disagg prefill, _add_request_to_queue for unified).
|
||||
|
||||
Call sites that omit `processed_tokens` (dllm, disagg decode)
|
||||
degrade to rid-lexicographic order; those queues carry no prefill work.
|
||||
"""
|
||||
|
||||
def _key(r: Req):
|
||||
rid = r.rid
|
||||
if rid in temporary_deprioritized:
|
||||
return (float("inf"), rid)
|
||||
uncached = SchedulePolicy._uncached_len(r)
|
||||
if uncached <= 0:
|
||||
# No prefill work; drain immediately.
|
||||
return (-float("inf"), rid)
|
||||
waited_tokens = max(0, processed_tokens - r.arrival_processed_tokens)
|
||||
ratio_delta = waited_tokens / uncached
|
||||
return (-ratio_delta, rid)
|
||||
|
||||
waiting_queue.sort(key=_key)
|
||||
|
||||
@staticmethod
|
||||
def _sort_by_dfs_weight(
|
||||
waiting_queue: List[Req], tree_cache: BasePrefixCache
|
||||
|
||||
@@ -456,6 +456,8 @@ class Scheduler(
|
||||
self.is_initializing = True
|
||||
# init_soft_watchdog starts a daemon thread that reads these on its first tick.
|
||||
self.forward_ct: int = 0
|
||||
# Prefill tokens processed so far; used as the aging axis for the HRRN scheduling policy. Reqs snapshot this at waiting_queue entry.
|
||||
self.processed_tokens_counter: int = 0
|
||||
self.cur_batch_for_debug: Optional[ScheduleBatch] = None
|
||||
self.init_soft_watchdog()
|
||||
|
||||
@@ -3152,6 +3154,7 @@ class Scheduler(
|
||||
self._prefetch_kvcache(req)
|
||||
self.waiting_queue.append(req)
|
||||
req.time_stats.set_wait_queue_entry_time()
|
||||
req.arrival_processed_tokens = self.processed_tokens_counter
|
||||
elif self.disaggregation_mode == DisaggregationMode.PREFILL:
|
||||
self._prefetch_kvcache(req)
|
||||
self.disagg_prefill_bootstrap_queue.add(
|
||||
@@ -3745,7 +3748,11 @@ class Scheduler(
|
||||
return None, running_batch
|
||||
|
||||
# Get priority queue
|
||||
self.policy.calc_priority(self.waiting_queue, running_batch)
|
||||
self.policy.calc_priority(
|
||||
self.waiting_queue,
|
||||
running_batch,
|
||||
processed_tokens=self.processed_tokens_counter,
|
||||
)
|
||||
|
||||
if TEST_RETRACT and running_bs > TEST_RETRACT_NO_PREFILL_BS:
|
||||
# If we are testing retraction and the running batch size exceeds
|
||||
@@ -4203,6 +4210,10 @@ class Scheduler(
|
||||
batch.after_idle_gap = self._sched_idled
|
||||
self._sched_idled = False
|
||||
|
||||
# Accumulate the prefill-token counter used by the HRRN scheduling policy. Decode / prebuilt batches contribute 0.
|
||||
if batch.extend_num_tokens:
|
||||
self.processed_tokens_counter += batch.extend_num_tokens
|
||||
|
||||
if self.scripted_scheduler_hook is not None:
|
||||
self.scripted_scheduler_hook.on_run_batch(batch)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user