{"id":"IL-0480","org":"Samsung SDS","date":"2026-08-06","docs":["https://docs.pytorch.org/tutorials/advanced/generic_join.html","https://docs.pytorch.org/docs/stable/distributed.algorithms.join.html"],"title":{"en":"Epoch-end NCCL deadlock hung a 16-GPU B300 fine-tuning run for three hours while GPU utilization read 100%","ko":"B300 16장 FSDP 미세 조정이 에포크 끝 체크포인트에서 NCCL 교착으로 세 시간 동안 조용히 멈췄다"},"domain":["ai-infra"],"impact":{"scope":"16-GPU B300 fine-tuning run hung for about 3 hours and lost about 5 hours of progress, about 130 GPU·h","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"GPU utilization reads 100% during an NCCL hang, while board power and the per-rank pattern show what the GPUs are really doing. For data-parallel jobs whose per-rank work depends on the data, a passing smoke test does not show that the full run is safe; the team now verifies invariants such as per-rank block balance before launch and watches runs from outside.","ko":"분산 학습에서 GPU 사용률은 실제 진행을 보여 주지 않는다. NCCL이 멈춰도 사용률은 100%로 나오므로 보드 전력과 랭크별 사용률의 모양을 봐야 한다. 랭크마다 작업량이 데이터에 따라 달라지는 작업에서는 스모크 테스트를 통과했다고 전체 실행이 안전한 것이 아니다. 실행 전에 불변 조건을 확인해야 한다. 스스로 교착에 빠진 노드는 자기 상태를 알릴 수 없으므로 실행 밖에서 지켜봐야 한다."},"related":[],"sources":[{"url":"https://arxiv.org/abs/2608.05944","kind":"paper","archived":"https://web.archive.org/web/20260811200333/https://arxiv.org/abs/2608.05944"}],"summary":{"en":"Samsung SDS engineers fully fine-tuned Qwen3-32B on two nodes of 8 NVIDIA B300 GPUs with PyTorch FSDP in FULL_SHARD mode (equivalent to ZeRO-3), InfiniBand NDR between the nodes and an NGC container with PyTorch 2.12, writing checkpoints to NFS. The data loader sharded records evenly across ranks, tokenized each record once and packed the tokens into fixed 2048-token blocks in a node-local file, and each rank trained on its own blocks. Because the records differ in length, the block counts differed after packing: rank 3 had the fewest (30,835) and rank 11 the most (30,888), a spread of 53 blocks or 0.17% of the per-rank work.\n\nIn the run that failed, each rank ran until its own blocks were used up. Rank 3 finished about 7 optimizer steps before rank 11, left the loop and entered the checkpoint barrier while the others were still reducing gradients, and the job deadlocked. The paper notes that this is documented PyTorch behaviour behind the Join context manager, but Join could not be used: neither FSDP1 nor FSDP2 implements Joinable, and the deadlock was at a manual dist.barrier() outside Join's scope. The team added evenfix, which all-reduces the block counts to the global minimum before the loop so that every rank stops at the same step, dropping at most 53 blocks on any rank, about 0.085% of all tokens. The failure cost about 48 GPU·h for the hang and about 85 GPU·h of progress that could not be recovered, because that run saved only at epoch end, about 130 GPU·h in total. A clean epoch afterwards ran 3,854 steps and its epoch-end save completed with no watchdog, abort or deadlock traces.","ko":"Samsung SDS의 엔지니어들은 두 노드에 나뉜 NVIDIA B300 16장에서 FSDP FULL_SHARD로 Qwen3-32B를 전체 미세 조정했다. 데이터 로더는 레코드를 랭크마다 고르게 나눴고, 각 레코드를 토큰화해 2048 토큰 블록으로 묶어 노드 로컬 NVMe 파일에 저장했다. 레코드 길이가 서로 달라 묶은 뒤의 블록 수가 랭크마다 달랐다. 랭크 3이 30,835개로 가장 적었고 랭크 11이 30,888개로 가장 많아 차이가 53블록이었다. 이 차이는 랭크당 작업량의 0.17%여서 다른 지표에는 보이지 않았다.\n\n사전 학습된 모델에서 시작한 실행은 약 5시간 동안 정상으로 돌았다. 그러다 랭크 3이 스텝당 8블록 기준으로 약 7 스텝 먼저 데이터를 다 써서 루프를 빠져나왔다. 랭크 3은 에포크 끝 체크포인트 저장 앞의 dist.barrier()가 낸 all_reduce에 들어갔고, 나머지 15개 랭크는 역전파의 reduce_scatter에 있었다. 랭크들이 같은 순번에서 서로 다른 집합 통신을 기다리게 되어 NCCL이 영원히 멈췄다. GPU 사용률은 100%로 남았고 오류도 없어 세 시간 동안 아무도 알아채지 못했다. 세 시간 뒤 NCCL watchdog이 멈춤을 드러냈다. 에포크 끝에만 체크포인트를 저장하던 때라 약 5시간의 진행도 되살릴 수 없었다. 한 번의 장애로 약 130 GPU·h, 에포크 약 1.5개 분량의 계산을 잃었다.\n\nPyTorch의 Join은 랭크마다 입력 수가 다를 때 집합 통신이 어긋나 멈추는 문제를 막는 기능이다. 그러나 Join은 FSDP를 지원하지 않고, 이번 교착이 난 수동 barrier도 Join이 다루는 범위 밖이었다. 엔지니어들은 학습 루프 전에 각 랭크의 블록 수를 all_reduce MIN으로 전체 최솟값에 맞추는 evenfix를 넣었다. 처음부터 다시 돌린 16 GPU 에포크는 3,854 스텝을 첫 시도에 끝냈고, 에포크 끝 저장도 watchdog이나 교착 흔적 없이 끝났다."},"trigger":{"en":"Rank 3, which had 53 fewer packed blocks than rank 11, ran out of data at the end of the epoch and entered the checkpoint barrier while the other ranks were still in the backward pass.","ko":"데이터가 가장 적은 랭크 3이 다른 랭크보다 약 7 옵티마이저 스텝 먼저 데이터를 다 써서 학습 루프를 빠져나와, 에포크 끝 체크포인트의 barrier에 들어갔다."},"severity":"unknown","symptoms":["nccl-timeout","training-job-failed"],"timeline":{"detected_after":"3h","resolved_after":null},"date_kind":"published","detection":{"en":"The NCCL watchdog surfaced the hang after three hours. The team then classified it as a deadlock from nvidia-smi board power and per-rank utilization and confirmed it with the NCCL flight recorder.","ko":"NCCL watchdog이 세 시간 뒤에 멈춤을 드러냈다. 엔지니어들은 nvidia-smi로 보드 전력과 랭크별 사용률을 보고 교착의 모양을 확인했고, NCCL flight recorder에서 랭크마다 서로 다른 집합 통신을 기다리는 것을 보고 확정했다."},"components":["nccl","pytorch","training-dataloader"],"root_cause":"software-bug","remediation":[{"text":{"en":"Added evenfix, which all-reduces each rank's packed-block count to the global minimum before the loop so that every rank stops at the same step","ko":"학습 루프 전에 각 랭크가 자기 블록 수를 all_reduce MIN으로 전체 최솟값에 맞추고, 그 수에 이르면 모든 랭크가 같은 스텝에서 루프를 멈추도록 했다."}},{"text":{"en":"Ran a clean epoch with evenfix in which the epoch-end save completed without a deadlock","ko":"수정한 코드로 처음부터 다시 돌린 16 GPU 에포크가 3,854 스텝을 첫 시도에 끝냈고, 에포크 끝 저장이 watchdog, abort, 교착 흔적 없이 끝났다."}}],"action_items":[{"text":{"en":"Run a preflight gate in the launcher that checks six invariants, including per-rank block balance, and refuses to start an unsafe run","ko":"실행 전에 불변 조건 여섯 개를 확인하고 안전하지 않은 실행은 시작하지 않는 사전 점검 게이트를 런처에 넣는다."}},{"text":{"en":"Watch each run from a separate host for completion, crash, hang, the power deadlock signature and a throughput floor, and send an out-of-band alert","ko":"별도 호스트에서 세션과 무관하게 도는 감시 프로그램이 완료, 크래시, 멈춤, 처리량 하한 위반을 잡아 대역 외 알림을 낸다."}},{"text":{"en":"Save checkpoints periodically, every 500 steps, instead of only at epoch end","ko":"500 스텝, 약 42분마다 체크포인트를 저장해 복구할 수 있게 한다."}},{"text":{"en":"Resume from weights only","ko":"가중치만으로 이어서 학습할 수 있게 한다."}},{"text":{"en":"Raise the watchdog timeout from 10 to 30 minutes","ko":"watchdog 타임아웃을 10분에서 30분으로 늘린다."}},{"text":{"en":"Retry failed launches automatically in the launcher","ko":"런처가 실패한 실행을 자동으로 다시 시도하게 한다."}}],"contributing":["test-coverage-gap","monitoring-blind-spot"],"recurrence_of":null,"symptoms_text":{"en":"A Qwen3-32B full fine-tuning run on 16 NVIDIA B300 GPUs in two nodes trained normally for about 5 hours, then hung at step 3,850 of 3,857 as it entered the end-of-epoch checkpoint. There was no error, no OOM in dmesg and no segfault, nvidia-smi still showed 100% GPU utilization although throughput was exactly zero, and the hang lasted three hours until the NCCL watchdog surfaced it.\n\nBefore the hang the run looked healthy: loss went from 1.60 to 0.74 to about 0.68, throughput was about 53k tok/s, and InfiniBand bandwidth was nominal. During the hang board power fell to about 190 W per GPU, with one rank at 0% utilization and the other fifteen at 100%. A normal checkpoint also sits near 190 W, but then only rank 0 drops to 0% while it writes the checkpoint, and the run resumes within about 2 minutes.\n\nThe NCCL flight recorder, enabled with TORCH_NCCL_TRACE_BUFFER_SIZE, showed rank 3 inside an all_reduce, the dist.barrier() that guards the end-of-epoch save_checkpoint, while ranks 0–2 and 4–15 were blocked in a reduce_scatter of the backward pass. With two different collectives at the same slot, NCCL waited forever.\n\nSmoke tests had passed. A run limited with max_steps=N exercises only the training loop, where all ranks behave the same, and ends before the rank with the least data runs out, so the fault could appear only at the end of a full epoch.","ko":"약 5시간 동안 정상으로 돌던 미세 조정 작업에서, 에포크 끝 체크포인트에 들어가던 3,857 스텝 중 3,850번째 스텝에 GPU 16개가 모두 멈췄다. GPU 사용률은 계속 100%로 나왔지만 처리량은 정확히 0이었고, NCCL watchdog이 세 시간 뒤에야 문제를 드러냈다.\n\n멈추기 전까지 손실은 1.60에서 0.74를 거쳐 약 0.68로 내려가고 있었고, 처리량은 약 53k tok/s였으며 16개 GPU 모두 사용률 100%였다. 멈춘 뒤에도 dmesg에 OOM이 없었고 segfault나 오류도 없었다.\n\nnvidia-smi로 본 보드 전력은 약 190 W로 떨어져 있었다. 랭크 하나는 사용률 0%이고 나머지는 100%였다. 정상 체크포인트에서는 랭크 0만 0%로 떨어지고 약 2분 안에 돌아오는데, 이번에는 모양이 반대였고 회복되지 않았다.\n\nNCCL flight recorder 덤프에서 랭크 3은 에포크 끝 save_checkpoint를 지키는 dist.barrier()의 all_reduce에 들어가 있었다. 나머지 15개 랭크는 역전파의 기울기 집계인 reduce_scatter에서 막혀 있었다. 랭크들이 같은 순번에서 서로 다른 집합 통신을 기다리고 있었다. 멈추기 전까지 처리량이 꾸준히 53k tok/s였고 IB 대역폭도 정상이었으므로 네트워크는 원인이 아니었다.\n\n이 문제는 스모크 테스트로 드러나지 않았다. max_steps=N으로 짧게 돌리면 모든 랭크가 대칭인 루프 본문만 실행하고, 데이터가 가장 적은 랭크가 데이터를 다 쓰기 전에 끝나기 때문이다."},"updated_at":"2026-11-14T12:34:12.627138Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0480","org_slug":"samsung-sds"}