[BugFix] NCCL deadlock in HiCache writing_check by making all_reduce unconditional (#26923)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zhiqiang Xie <xiezhq@stanford.edu>
Co-authored-by: Zhangheng <hzh0425@apache.org>
This commit is contained in:
Jeremy Zhang
2026-06-22 01:47:44 +08:00
committed by GitHub
co-authored by Claude Zhiqiang Xie Zhangheng
parent 5351800700
commit 7f67965b4d
2 changed files with 28 additions and 14 deletions
@@ -396,10 +396,11 @@ class HiMambaRadixCache(MambaRadixCache):
assert len(self.ongoing_write_through) == 0 assert len(self.ongoing_write_through) == 0
return return
if len(self.ongoing_write_through) == 0: # Every rank must enter the all_reduce below; ongoing_write_through can
return # diverge across ranks because loading_check processes DMA completions
# independently (no cross-rank sync).
finish_count = 0 finish_count = 0
if len(self.ongoing_write_through) > 0:
for _, finish_event, ack_list in self.cache_controller.ack_write_queue: for _, finish_event, ack_list in self.cache_controller.ack_write_queue:
if not finish_event.query(): if not finish_event.query():
break break
@@ -426,17 +427,30 @@ class HiMambaRadixCache(MambaRadixCache):
finish_count -= 1 finish_count -= 1
def loading_check(self): def loading_check(self):
# Every rank must enter the all_reduce below; ongoing_load_back can
# diverge across ranks.
finish_count = 0 finish_count = 0
for _, finish_event, ack_list in self.cache_controller.ack_load_queue: for _, finish_event, ack_list in self.cache_controller.ack_load_queue:
if not finish_event.query(): if not finish_event.query():
# the KV cache loading is still ongoing
break break
finish_count += 1 finish_count += 1
queue_size = torch.tensor(finish_count, dtype=torch.int, device="cpu")
if self.tp_world_size > 1:
torch.distributed.all_reduce(
queue_size,
op=torch.distributed.ReduceOp.MIN,
group=self.tp_group,
)
finish_count = int(queue_size.item())
while finish_count > 0:
_, finish_event, ack_list = self.cache_controller.ack_load_queue.pop(0)
finish_event.synchronize()
for ack_id in ack_list: for ack_id in ack_list:
end_node = self.ongoing_load_back.pop(ack_id) end_node = self.ongoing_load_back.pop(ack_id)
self.dec_lock_ref(end_node) self.dec_lock_ref(end_node)
finish_count -= 1
del self.cache_controller.ack_load_queue[:finish_count]
def ready_to_load_host_cache(self) -> int: def ready_to_load_host_cache(self) -> int:
return self.cache_controller.start_loading() return self.cache_controller.start_loading()
+4 -4
View File
@@ -916,10 +916,10 @@ class HiRadixCache(RadixCache):
assert len(self.ongoing_write_through) == 0 assert len(self.ongoing_write_through) == 0
return return
# NOTE: all ranks has the same ongoing_write_through, can skip sync if empty # Every rank must enter the all_reduce below; ongoing_write_through can
if len(self.ongoing_write_through) == 0: # diverge across ranks (e.g. write_backup returning 0 on a subset under
return # host memory pressure), so a conditional skip desyncs the NCCL op
# sequence and deadlocks under TP > 1. (Matches UnifiedRadixCache.)
finish_count = 0 finish_count = 0
if self.pp_rank == 0: if self.pp_rank == 0:
for _, finish_event, ack_list in self.cache_controller.ack_write_queue: for _, finish_event, ack_list in self.cache_controller.ack_write_queue: