[PD][PushConnector] Record last activity of remotes to allow clean up of stale ones (#50234)

Signed-off-by: NickLucche <nicolo.lucchesi@mistral.ai>
This commit is contained in:
Nicolò Lucchesi
2026-08-07 09:43:29 +00:00
committed by GitHub
parent 4f76c8ad9d
commit 5ec47f3e48
3 changed files with 59 additions and 2 deletions
@@ -341,6 +341,7 @@ class _StubWriterWorker(NixlPushConnectorWorker):
# Single non-hybrid attention group, matching the stub block id lists.
w._has_mamba = False
w._group_spec_types = (FullAttentionSpec,)
w._engine_last_active = {}
# Track _do_start_push_kv invocations.
calls: list[tuple[str, Any, dict[str, Any]]] = []
@@ -619,6 +620,60 @@ def test_writer_loop_drains_deferred_push_inbox():
assert w.start_push_calls[0][0] == "req-retry"
def _eviction_worker(engine_ttl: float) -> NixlPushConnectorWorker:
"""A push worker wired to drive the base ``_ensure_handshake`` eviction
path (``_evict_stale_engines`` + ``_cleanup_remote_engine``)."""
w = _StubWriterWorker.fresh()
w._engine_ttl = engine_ttl
w._engine_last_active = {}
w._engine_clock_offset = {}
w._handshake_lock = threading.RLock()
# _cleanup_remote_engine touches these when reaping an engine.
w.nixl_wrapper = MagicMock()
w.dst_xfer_side_handles = {}
w.kv_caches_base_addr = {}
w.dst_num_blocks = {}
w.tp_mappings = {}
w.transfer_topo = None
w._logical_to_kernel_block_ids = lambda blocks, ratio: blocks
w.writes = []
w._xfer_blocks_for_req = lambda req_id, meta: w.writes.append(req_id)
return w
def test_active_push_refreshes_engine_last_active():
"""The base ``_ensure_handshake`` refreshes liveness only on a new
handshake, so an active push to an already-connected engine must refresh
``_engine_last_active`` itself or it is reaped mid-stream (S1)."""
w = _eviction_worker(engine_ttl=3600.0)
# Already-connected D -> _ensure_handshake returns None, WRITE runs inline.
w._remote_agents["decode-engine"] = {(0, 0): "agent-decode"}
stale = time.perf_counter() - 5.0
w._engine_last_active["decode-engine"] = stale
_real_do_start_push_kv(w, "req", ([1, 2, 3],), _registration_data("req"))
assert w._engine_last_active["decode-engine"] > stale
assert w.writes == ["req"]
def test_stale_engine_evicted_on_push():
"""A push drives ``_ensure_handshake`` -> ``_evict_stale_engines``, so a D
engine silent past its TTL is reaped -- no _remote_agents / NIXL agent leak
across D scale up/down (S1)."""
w = _eviction_worker(engine_ttl=30.0)
w._remote_agents["D-old"] = {(0, 0): "agent-D-old"}
w._engine_last_active["D-old"] = time.perf_counter() - 10_000.0
# The engine being pushed to is already connected.
w._remote_agents["decode-engine"] = {(0, 0): "agent-decode"}
_real_do_start_push_kv(w, "req", ([1, 2, 3],), _registration_data("req"))
assert "D-old" not in w._remote_agents
assert "D-old" not in w._engine_last_active
w.nixl_wrapper.remove_remote_agent.assert_called_once_with("agent-D-old")
class TestPushWriterNotifs:
def test_get_new_notifs_processes_forwarded_completion_notif(self):
"""Non-PUSH_REG notifs forwarded by the writer thread are drained
@@ -2560,8 +2560,7 @@ class NixlBaseConnectorWorker:
# Drop the cached clock offset; it is re-measured on the next handshake.
self._engine_clock_offset.pop(engine_id, None)
# Push P-side engines are tracked in _remote_agents but not in
# _engine_last_active (they don't participate in stale eviction), so
# A just-completed handshake may not have recorded activity yet, so
# tolerate a missing entry.
last_active = self._engine_last_active.pop(engine_id, None)
if log_eviction and last_active is not None:
@@ -450,6 +450,9 @@ class NixlPushConnectorWorker(NixlBaseConnectorWorker):
fut.add_done_callback(_on_handshake)
return
# Keep the engine alive while it is actively receiving pushes, mirroring
# how pull-mode transfers touch _engine_last_active in start_load_kv.
self._engine_last_active[decode_engine_id] = time.perf_counter()
# Both sides stay logical here; ``_xfer_blocks_for_req`` converts each
# to physical with its own physical-blocks-per-logical ratio -- P uses