Files
hao-ai-lab-FastVideo/v2/runtime/scheduler.py
T
Will Lin 1fe50c0092 [refactor] v2: group flat top-level into planes; isolate vendored under _vendor/
The v2 top level had grown to ~27 dirs + 13 loose files — half of them
single-concept abstraction shells (memory/ 86 LOC, transport/ 222, parity/ 178,
extend/ 226) and half vendored fastvideo code sitting as peers to the actual v2
design. Regroup to mirror README section 3 "Planes & dependency order":

  core/     enums+types, card, loop, program, parity, request, parallel
            (the model-native contracts; no kernels)
  runtime/  + folded-in substrate: cache, memory, transport, extend
            (the import graph shows only runtime consumes them; compile/cudagraph
            already lived here)
  serving/  + deploy/ (products / fleet)
  _vendor/  all copied fastvideo: models, layers, attention, configs, distributed,
            platforms, api, hooks, logging_utils, third_party + fastvideo_args/
            utils/logger/envs/forward_context — internal layout unchanged, still
            mirrors upstream for diffing

28 dirs + 13 root files -> 8 dirs + 6 root files. Pure mechanical move: 947
absolute-import paths rewritten (v2.X -> v2.{core,runtime,serving,_vendor}.X),
boundary-anchored so platform/ (native dispatch) and platforms/ (vendored CUDA
detect) no longer collide and neither does hooks/ vs extend/. Deleted the empty
v2/loader/. README section 3 + 16 updated; stale test count corrected
(34 files/216 tests -> 22 files/143 tests).

Validated: `import v2` stays torch-free; `v2/run_tests.py` and `pytest v2/tests/`
-> 143 passed, 2 skipped on the numpy toy backend. (On a GPU box force the toy
backend with CUDA_VISIBLE_DEVICES="" or detect() picks cuda.) No external importer
changed — examples use the re-exported `from v2 import VideoGenerator`.
2026-07-04 17:15:34 +00:00

76 lines
2.9 KiB
Python

"""The scheduler — a refundable memory (OOM) admission guard.
``AdmissionController`` is the only layer: do not admit a step unless its peak-activation memory can
be reserved (resident memory is reserved once for the whole loop). Reservations are *refundable* (a
concurrency gate, not a lifetime cap); an infeasible reservation (need > pool capacity) fails fast
with ``AdmissionInfeasible`` instead of spinning. No compute/time budget and no cross-request
batching — serving is pooled run-to-completion (concurrency bounded by the pool).
"""
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass, field
from v2.core.loop.contracts import WorkPlan
from v2.runtime.memory.allocator import MemoryManager, Reservation
class AdmissionInfeasible(RuntimeError):
"""A reservation can never be satisfied (need > pool capacity) — fail fast, don't spin."""
@dataclass
class StepTicket:
"""Returned by ``admit_step``; carries the memory reservation ``release`` must refund."""
memory_res: Reservation | None
@dataclass
class SchedulerMetrics:
admitted: int = 0
deferred: int = 0
stepped_units: int = 0
by_kind: dict[str, int] = field(default_factory=lambda: defaultdict(int))
class AdmissionController:
"""Reservation before admission — a refundable memory (OOM) guard. No compute/time budget
(pooled run-to-completion prices nothing; concurrency is bounded by the RolePool)."""
def __init__(self, memory: MemoryManager | None = None):
self.memory = memory or MemoryManager()
self.metrics = SchedulerMetrics()
def feasible_resident(self, nbytes: int) -> bool:
return nbytes <= self.memory.total_bytes
def feasible_step(self, plan: WorkPlan) -> bool:
"""Could this step EVER be admitted on an empty pool? If not, the caller fails fast."""
return plan.resources.peak_activation_bytes <= self.memory.total_bytes
def reserve_resident(self, tag: str, nbytes: int) -> Reservation | None:
if nbytes <= 0:
return None
if not self.memory.can_reserve(tag, nbytes):
return None
return self.memory.reserve(tag, nbytes)
def admit_step(self, plan: WorkPlan) -> StepTicket | None:
"""Reserve a step's peak activation memory. None ⇒ deferred (no free memory yet)."""
peak = plan.resources.peak_activation_bytes
res: Reservation | None = None
if peak > 0:
if not self.memory.can_reserve(plan.loop_id, peak):
self.metrics.deferred += 1
return None
res = self.memory.reserve(plan.loop_id, peak)
self.metrics.admitted += 1
self.metrics.by_kind[plan.kind.value] += 1
return StepTicket(res)
def release(self, ticket: StepTicket | None) -> None:
if ticket is None:
return
if ticket.memory_res is not None:
self.memory.release(ticket.memory_res)