Prevent duplicate CP HiCache write acks from crashing split drain
CP per-layer write-through can leave duplicated ready ack ids in the ack queue while pending-split insertion drains write visibility. The pre-scan observed both duplicates as registered before the first completion removed radix state, so the second completion raised KeyError and killed the scheduler. The write-check path now records completed ack ids until matching queue entries are drained. Duplicate completed acks are removed with a warning, while genuinely unknown or unregistered acks still fail fast. Constraint: CP HiCache pending-split drain may call writing_check outside the normal idle event path. Rejected: Blind pop(..., None) for all unknown acks | would silently hide truly unattached or corrupt ack state. Confidence: high Scope-risk: moderate Directive: Do not remove the completed-ack short-term memory unless duplicate and stale ack queues are proven impossible across TP MIN synchronization. Tested: Local py_compile for hiradix_cache.py and test_cp_hicache_metadata.py. Tested: Remote cjy-glm5-new py_compile and full test_cp_hicache_metadata.py, 121 passed. Not-tested: Full ETE prefill workload after restarting service. Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
@@ -10,7 +10,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from queue import Empty
|
from queue import Empty
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
@@ -2877,13 +2877,36 @@ class HiRadixCache(RadixCache):
|
|||||||
self.write_backup(node)
|
self.write_backup(node)
|
||||||
|
|
||||||
def writing_check(self, write_back=False):
|
def writing_check(self, write_back=False):
|
||||||
def complete_write_ack_node(ack_id: int) -> TreeNode:
|
completed_write_ack_ids = getattr(self, "_completed_write_ack_ids", None)
|
||||||
|
if completed_write_ack_ids is None:
|
||||||
|
completed_write_ack_ids = set()
|
||||||
|
self._completed_write_ack_ids = completed_write_ack_ids
|
||||||
|
|
||||||
|
def complete_write_ack_node(ack_id: int) -> Optional[TreeNode]:
|
||||||
if ack_id in self.pending_host_backups:
|
if ack_id in self.pending_host_backups:
|
||||||
backuped_node = self._commit_pending_backup(ack_id)
|
backuped_node = self._commit_pending_backup(ack_id)
|
||||||
self.ongoing_write_through.pop(ack_id, None)
|
self.ongoing_write_through.pop(ack_id, None)
|
||||||
|
completed_write_ack_ids.add(ack_id)
|
||||||
return backuped_node
|
return backuped_node
|
||||||
backuped_node = self.ongoing_write_through.pop(ack_id)
|
backuped_node = self.ongoing_write_through.pop(ack_id, None)
|
||||||
|
if backuped_node is None:
|
||||||
|
if ack_id not in completed_write_ack_ids:
|
||||||
|
raise KeyError(ack_id)
|
||||||
|
# A duplicated ready ack can pass the pre-scan while the first
|
||||||
|
# occurrence still owns radix state, then become already-consumed
|
||||||
|
# by the time this loop reaches the second occurrence. Keep
|
||||||
|
# genuinely unattached acks deferred in the pre-scan, but make
|
||||||
|
# duplicate completion idempotent instead of crashing scheduler.
|
||||||
|
logger.warning(
|
||||||
|
"[HiCache-write] writing_check skipped already-consumed write ack: "
|
||||||
|
"ack_id=%d pending=%s ongoing=%s",
|
||||||
|
ack_id,
|
||||||
|
list(self.pending_host_backups.keys()),
|
||||||
|
list(self.ongoing_write_through.keys()),
|
||||||
|
)
|
||||||
|
return None
|
||||||
self.dec_node_lock_ref(backuped_node)
|
self.dec_node_lock_ref(backuped_node)
|
||||||
|
completed_write_ack_ids.add(ack_id)
|
||||||
return backuped_node
|
return backuped_node
|
||||||
|
|
||||||
if write_back:
|
if write_back:
|
||||||
@@ -2893,7 +2916,7 @@ class HiRadixCache(RadixCache):
|
|||||||
finish_event.synchronize()
|
finish_event.synchronize()
|
||||||
for ack_id in ack_list:
|
for ack_id in ack_list:
|
||||||
backuped_node = complete_write_ack_node(ack_id)
|
backuped_node = complete_write_ack_node(ack_id)
|
||||||
if self.enable_storage:
|
if backuped_node is not None and self.enable_storage:
|
||||||
self.write_backup_storage(backuped_node)
|
self.write_backup_storage(backuped_node)
|
||||||
self.cache_controller.ack_write_queue.clear()
|
self.cache_controller.ack_write_queue.clear()
|
||||||
assert len(self.ongoing_write_through) == 0
|
assert len(self.ongoing_write_through) == 0
|
||||||
@@ -2918,6 +2941,7 @@ class HiRadixCache(RadixCache):
|
|||||||
return (
|
return (
|
||||||
ack_id in self.pending_host_backups
|
ack_id in self.pending_host_backups
|
||||||
or ack_id in self.ongoing_write_through
|
or ack_id in self.ongoing_write_through
|
||||||
|
or ack_id in completed_write_ack_ids
|
||||||
)
|
)
|
||||||
|
|
||||||
finish_count = 0
|
finish_count = 0
|
||||||
@@ -2966,10 +2990,17 @@ class HiRadixCache(RadixCache):
|
|||||||
finish_event.synchronize()
|
finish_event.synchronize()
|
||||||
for ack_id in ack_list:
|
for ack_id in ack_list:
|
||||||
backuped_node = complete_write_ack_node(ack_id)
|
backuped_node = complete_write_ack_node(ack_id)
|
||||||
|
if backuped_node is None:
|
||||||
|
continue
|
||||||
released_nodes.append(ack_id)
|
released_nodes.append(ack_id)
|
||||||
if self.enable_storage:
|
if self.enable_storage:
|
||||||
self.write_backup_storage(backuped_node)
|
self.write_backup_storage(backuped_node)
|
||||||
finish_count -= 1
|
finish_count -= 1
|
||||||
|
if completed_write_ack_ids:
|
||||||
|
remaining_ack_ids = set()
|
||||||
|
for ack in self.cache_controller.ack_write_queue:
|
||||||
|
remaining_ack_ids.update(ack.node_ids)
|
||||||
|
completed_write_ack_ids.intersection_update(remaining_ack_ids)
|
||||||
if released_nodes:
|
if released_nodes:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[HiCache-write] writing_check released %d write locks: node_ids=%s remaining_ongoing=%d",
|
"[HiCache-write] writing_check released %d write locks: node_ids=%s remaining_ongoing=%d",
|
||||||
|
|||||||
@@ -2174,6 +2174,72 @@ class TestHiRadixCacheCPBackup(CustomTestCase):
|
|||||||
self.assertEqual(cache.ongoing_write_through, {53: attached_node})
|
self.assertEqual(cache.ongoing_write_through, {53: attached_node})
|
||||||
self.assertEqual(dec_locked, [])
|
self.assertEqual(dec_locked, [])
|
||||||
|
|
||||||
|
def test_writing_check_ignores_duplicate_ready_ack_for_same_node(self):
|
||||||
|
cache = HiRadixCache.__new__(HiRadixCache)
|
||||||
|
cache._uses_cp_hicache = True
|
||||||
|
cache.tp_world_size = 1
|
||||||
|
cache.enable_storage = False
|
||||||
|
cache.pending_host_backups = {}
|
||||||
|
|
||||||
|
class ReadyEvent:
|
||||||
|
def query(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def synchronize(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
node = TreeNode()
|
||||||
|
node.id = 4614
|
||||||
|
dec_locked = []
|
||||||
|
cache.ongoing_write_through = {4614: node}
|
||||||
|
cache.dec_node_lock_ref = lambda released_node: dec_locked.append(released_node.id)
|
||||||
|
cache.cache_controller = types.SimpleNamespace(
|
||||||
|
ack_write_queue=[
|
||||||
|
HiCacheAck(ReadyEvent(), ReadyEvent(), [4614]),
|
||||||
|
HiCacheAck(ReadyEvent(), ReadyEvent(), [4614]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.writing_check()
|
||||||
|
|
||||||
|
self.assertEqual(cache.ongoing_write_through, {})
|
||||||
|
self.assertEqual(cache.cache_controller.ack_write_queue, [])
|
||||||
|
self.assertEqual(dec_locked, [4614])
|
||||||
|
|
||||||
|
def test_writing_check_drops_stale_duplicate_ack_before_valid_ack(self):
|
||||||
|
cache = HiRadixCache.__new__(HiRadixCache)
|
||||||
|
cache._uses_cp_hicache = True
|
||||||
|
cache.tp_world_size = 1
|
||||||
|
cache.enable_storage = False
|
||||||
|
cache.pending_host_backups = {}
|
||||||
|
cache._completed_write_ack_ids = {4614}
|
||||||
|
|
||||||
|
class ReadyEvent:
|
||||||
|
def query(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def synchronize(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
node = TreeNode()
|
||||||
|
node.id = 4615
|
||||||
|
dec_locked = []
|
||||||
|
cache.ongoing_write_through = {4615: node}
|
||||||
|
cache.dec_node_lock_ref = lambda released_node: dec_locked.append(released_node.id)
|
||||||
|
cache.cache_controller = types.SimpleNamespace(
|
||||||
|
ack_write_queue=[
|
||||||
|
HiCacheAck(ReadyEvent(), ReadyEvent(), [4614]),
|
||||||
|
HiCacheAck(ReadyEvent(), ReadyEvent(), [4615]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.writing_check()
|
||||||
|
|
||||||
|
self.assertEqual(cache.ongoing_write_through, {})
|
||||||
|
self.assertEqual(cache.cache_controller.ack_write_queue, [])
|
||||||
|
self.assertEqual(dec_locked, [4615])
|
||||||
|
self.assertEqual(cache._completed_write_ack_ids, set())
|
||||||
|
|
||||||
def test_write_backup_cp_failfast_on_unplanned_reservation_failure(self):
|
def test_write_backup_cp_failfast_on_unplanned_reservation_failure(self):
|
||||||
cache = HiRadixCache.__new__(HiRadixCache)
|
cache = HiRadixCache.__new__(HiRadixCache)
|
||||||
cache._uses_cp_hicache = True
|
cache._uses_cp_hicache = True
|
||||||
|
|||||||
Reference in New Issue
Block a user