Skip to content

fix(comm): run collectives on their operand's device - #57

Merged
zhaoyinglia merged 1 commit into
flagos-ai:mainfrom
lvyufeng:fix-comm-device-index
Aug 6, 2026
Merged

fix(comm): run collectives on their operand's device#57
zhaoyinglia merged 1 commit into
flagos-ai:mainfrom
lvyufeng:fix-comm-device-index

Conversation

@lvyufeng

@lvyufeng lvyufeng commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the device-index bug that causes collectives to enqueue device-N buffers onto a device-0 stream, resulting in VMFault on FlagCX/DCU and silent wrong results on other backends.

Root Cause

csrc/runtime/accelerator/cuda/device.cc maintained a thread_local int gCurrentDevice shadow variable that GetDevice returned instead of asking the driver. Since flagos aliases the same physical GPUs as torch.cuda, and cudaSetDevice's current device is already thread-local, this shadow copy could only diverge:

  • torch.cuda.set_device(1) moves the driver's index without going through our SetDevice, leaving the shadow at 0
  • Any DeviceGuard then reads the stale 0 as "previous" and restores the process to a device that was never current
  • Observed symptom: allocating a flagos:1 tensor resets torch.cuda.current_device() back to 0

ProcessGroupFlagOS delegates to an inner backend (FlagCX or RCCL) that resolves its device from whatever is current, so rank 1's device-1 collective gets enqueued on the device-0 stream FlagCX cached. On DCU this triggers VMFault; on other hardware it silently computes wrong results or hangs.

Changes

  1. C++ fix (device.cc): Delete the shadow variable; make GetDevice call cudaGetDevice directly
  2. Dedup (process_group.py): Remove duplicate _allgather_base and _reduce_scatter_base definitions that were silently shadowing each other
  3. Regression test: tests/manual/test_comm_device_index.py deliberately calls only torch.cuda.set_device(rank) to catch this class of bug

Verification

  • FlagCX path (the one that VMFaulted): 2/4 cards, test_comm_device_index.py + test_flagos_dist_live.py (20/20 collectives + DDP), all pass
  • RCCL path: 4 cards, 20/20 collectives, exit 0 (confirms no regression)
  • Compute side: test_compute_device_index.py 15 passed (this fix also resolves the twin bug that PR fix(flagos): run FlagGems Python ops on their operand's device #54 patched in Python)

Impact

This also fixes the device-index hazard that #54 addressed on the compute side (CallPythonOp_*). Both were symptoms of the same C++ accounting bug.

ProcessGroupFlagOS delegates to an inner backend (FlagCX or RCCL/HCCL) that
resolves the device it works on from whatever is *current*, not from the tensor.
A caller that binds only with torch.cuda.set_device(rank) -- the natural thing to
do -- had its collectives enqueued on the wrong device.

The cause is in the runtime, not the process group. accelerator/cuda/device.cc
kept a thread_local gCurrentDevice shadow copy of the current device index and
GetDevice returned it instead of asking the driver. flagos aliases the same
physical GPU as torch's CUDA backend, and cudaSetDevice's current device is
already thread-local, so a second copy could only diverge from it:
torch.cuda.set_device() moves the driver's index without passing through our
SetDevice, leaving the shadow stale. Measured on DTK before this change:

    torch.cuda.set_device(1)          -> cuda=1  flagos=0   diverged
    torch.ones(4, device='flagos:1')  -> cuda=0  flagos=0   cuda reset to 0
    torch.cuda.set_device(0)          -> cuda=0  flagos=1   diverged

The middle line is a DeviceGuard reading the stale 0 as "previous" and restoring
the process to a device that was never current. After it, every rank had device 0
current, so rank 1's collective targeted the wrong GPU.

FlagCX turns that into a GPU fault rather than a wrong answer: getStreamByIndex
lazily streamCreate()s one stream on the current device and caches it by index
alone, so the first collective binds the comm to device 0 permanently. Rank 1
then enqueued device-1 buffers onto a device-0 stream and RCCL faulted
(VMFault / "invalid resource handle") on the first all_reduce. A process kill,
not an exception.

Dropping the shadow variable and calling cudaGetDevice directly makes the two
currents identical by construction; all three lines above now agree. No guard is
needed in the process group, which is why this touches 20 delegation sites less
than it looks like it should. It also removes the need for the Python-side
DeviceGuard added to the CallPythonOp_* callers in flagos-ai#54 -- the compute-side twin
of the same accounting bug.

Only accelerator/cuda/device.cc is changed here. ascend, metax and tsingmicro
carry the same shadow variable, but their GetDevice semantics need confirming on
that hardware before being changed the same way.

Also merges two pairs of duplicate _allgather_base / _reduce_scatter_base
definitions in process_group.py, where the second silently shadowed the first.
That is unrelated to the device index; the surviving copy keeps the more
informative comment.

test_flagos_dist_live.py passed with this bug present -- it happened to leave the
two currents agreeing -- so the new tests/manual/test_comm_device_index.py
deliberately calls only torch.cuda.set_device(rank) and never touches
torch_fl.flagos.set_device.

Verified on Hygon DCU (DTK) with the FlagCX inner backend confirmed live
(pg._inner.name() == "flagcx"), with process_group.py reverted to its pre-change
state to prove the runtime fix alone is sufficient:

    test_comm_device_index.py    2 and 4 cards, exit 0 (previously VMFault)
    test_flagos_dist_live.py     4 cards, 20/20 collectives + DDP, grads
                                 identical across ranks, exit 0
    test_compute_device_index.py 15 passed

Local ruff check and ruff format pass across the repo (122 files).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lvyufeng
lvyufeng force-pushed the fix-comm-device-index branch from a74a9cc to 65e6e01 Compare August 6, 2026 05:39
@zhaoyinglia
zhaoyinglia merged commit 68e3c2e into flagos-ai:main Aug 6, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants