StepTracker — step-counting delay with first-finish fast-path.
Manages a collection of (mm_hash, request_id) entries that need to be processed after a configurable number of engine steps have elapsed. Additionally, if the originating request finishes before the step-count expires, the entry is processed immediately (the GPU transfer is guaranteed complete by request finish time).
Used by ECCPUScheduler: one instance for readiness marking, one for unpins.
Classes:
-
StepTracker – Track pending operations with step-count delay and first-finish.
StepTracker
Track pending operations with step-count delay and first-finish.
Each add(mm_hash, request_id) registers an operation that will be returned from step() either when: (a) max_concurrent_batches steps elapse (deque expiry), OR (b) request_id appears in finished_req_ids (first-finish).
Each add is processed exactly once — never both (a) and (b).
Methods:
-
add – Register an mm_hash needing processing after a delay.
-
drain_all – Return all unprocessed entries and clear internal state.
-
step – Advance one step. Return mm_hashes ready for processing.
Source code in vllm/distributed/ec_transfer/ec_connector/cpu/scheduler/step_tracker.py
| class StepTracker:
"""Track pending operations with step-count delay and first-finish.
Each ``add(mm_hash, request_id)`` registers an operation that will be
returned from ``step()`` either when:
(a) ``max_concurrent_batches`` steps elapse (deque expiry), OR
(b) ``request_id`` appears in ``finished_req_ids`` (first-finish).
Each add is processed exactly once — never both (a) and (b).
"""
def __init__(self, max_concurrent_batches: int) -> None:
# One slot per past step; oldest at the right. When the deque is
# full the rightmost slot is popped and its entries expire.
self._slots: deque[list[_PendingEntry]] = deque(maxlen=max_concurrent_batches)
# Reverse index: request_id → entries across all slots + _current.
# Enables O(1) lookup for the first-finish fast-path.
self._req_index: dict[str, list[_PendingEntry]] = {}
# Entries added this step (via add()), committed to a new slot at
# the start of the next step() call.
self._current: list[_PendingEntry] = []
def add(self, mm_hash: str, request_id: str) -> None:
"""Register an mm_hash needing processing after a delay."""
entry = _PendingEntry(mm_hash=mm_hash, request_id=request_id)
self._current.append(entry)
self._req_index.setdefault(request_id, []).append(entry)
def step(self, finished_req_ids: set[str]) -> list[str]:
"""Advance one step. Return mm_hashes ready for processing.
Returns:
List of mm_hashes to process. May contain duplicates if the
same mm_hash was added multiple times (from different requests).
"""
result: list[str] = []
# Phase 1: first-finish fast-path.
for req_id in finished_req_ids:
entries = self._req_index.pop(req_id, None)
if entries is None:
continue
for entry in entries:
if not entry.processed:
entry.processed = True
result.append(entry.mm_hash)
# Phase 2: step-count expiry.
if len(self._slots) == self._slots.maxlen:
expired_slot = self._slots.pop()
for entry in expired_slot:
if not entry.processed:
entry.processed = True
result.append(entry.mm_hash)
self._cleanup_expired(expired_slot)
# Phase 3: commit current entries to new slot.
self._slots.appendleft(self._current)
self._current = []
return result
def drain_all(self) -> list[str]:
"""Return all unprocessed entries and clear internal state."""
result: list[str] = []
for entry in self._current:
if not entry.processed:
entry.processed = True
result.append(entry.mm_hash)
self._current = []
for slot in self._slots:
for entry in slot:
if not entry.processed:
entry.processed = True
result.append(entry.mm_hash)
self._slots.clear()
self._req_index.clear()
return result
def _cleanup_expired(self, slot: list[_PendingEntry]) -> None:
"""Prune reverse index for entries in an expired slot."""
for entry in slot:
entries_for_req = self._req_index.get(entry.request_id)
if entries_for_req is None:
# Already removed by fast-path (pop in step()).
continue
entries_for_req.remove(entry)
if not entries_for_req:
del self._req_index[entry.request_id]
|
_cleanup_expired(slot)
Prune reverse index for entries in an expired slot.
Source code in vllm/distributed/ec_transfer/ec_connector/cpu/scheduler/step_tracker.py
| def _cleanup_expired(self, slot: list[_PendingEntry]) -> None:
"""Prune reverse index for entries in an expired slot."""
for entry in slot:
entries_for_req = self._req_index.get(entry.request_id)
if entries_for_req is None:
# Already removed by fast-path (pop in step()).
continue
entries_for_req.remove(entry)
if not entries_for_req:
del self._req_index[entry.request_id]
|
add(mm_hash, request_id)
Register an mm_hash needing processing after a delay.
Source code in vllm/distributed/ec_transfer/ec_connector/cpu/scheduler/step_tracker.py
| def add(self, mm_hash: str, request_id: str) -> None:
"""Register an mm_hash needing processing after a delay."""
entry = _PendingEntry(mm_hash=mm_hash, request_id=request_id)
self._current.append(entry)
self._req_index.setdefault(request_id, []).append(entry)
|
drain_all()
Return all unprocessed entries and clear internal state.
Source code in vllm/distributed/ec_transfer/ec_connector/cpu/scheduler/step_tracker.py
| def drain_all(self) -> list[str]:
"""Return all unprocessed entries and clear internal state."""
result: list[str] = []
for entry in self._current:
if not entry.processed:
entry.processed = True
result.append(entry.mm_hash)
self._current = []
for slot in self._slots:
for entry in slot:
if not entry.processed:
entry.processed = True
result.append(entry.mm_hash)
self._slots.clear()
self._req_index.clear()
return result
|
step(finished_req_ids)
Advance one step. Return mm_hashes ready for processing.
Returns:
-
list[str] – List of mm_hashes to process. May contain duplicates if the
-
list[str] – same mm_hash was added multiple times (from different requests).
Source code in vllm/distributed/ec_transfer/ec_connector/cpu/scheduler/step_tracker.py
| def step(self, finished_req_ids: set[str]) -> list[str]:
"""Advance one step. Return mm_hashes ready for processing.
Returns:
List of mm_hashes to process. May contain duplicates if the
same mm_hash was added multiple times (from different requests).
"""
result: list[str] = []
# Phase 1: first-finish fast-path.
for req_id in finished_req_ids:
entries = self._req_index.pop(req_id, None)
if entries is None:
continue
for entry in entries:
if not entry.processed:
entry.processed = True
result.append(entry.mm_hash)
# Phase 2: step-count expiry.
if len(self._slots) == self._slots.maxlen:
expired_slot = self._slots.pop()
for entry in expired_slot:
if not entry.processed:
entry.processed = True
result.append(entry.mm_hash)
self._cleanup_expired(expired_slot)
# Phase 3: commit current entries to new slot.
self._slots.appendleft(self._current)
self._current = []
return result
|