[Runtime] Preflight full intranode P2P matrix - #730
Conversation
| local_device = torch.cuda.current_device() | ||
| rank_devices = _all_gather_object(group, (socket.gethostname(), local_device)) | ||
| local_access_results = build_local_peer_access_results(rank, rank_devices, torch.cuda.can_device_access_peer) |
There was a problem hiding this comment.
🔴 critical: 使用源进程可解析的 CUDA 设备标识: 当启动器为各 rank 分别设置 CUDA_VISIBLE_DEVICES 时,current_device() 返回的是进程本地序号;例如两个 rank 都会报告设备 0,随后实际查询的是 can_device_access_peer(0, 0),从而把有效的跨 GPU P2P 拒绝掉。不同的可见设备排序还可能查询错误的物理 GPU;应交换稳定的物理标识并转换为源进程的本地序号,或明确验证所有 rank 使用相同映射。
🤖 v6
There was a problem hiding this comment.
Fixed in 2161508. Ranks now exchange physical GPU UUIDs rather than process-local CUDA ordinals. Each source process maps UUIDs into its own visible ordinal space; if a peer is hidden by CUDA_VISIBLE_DEVICES, the query falls back to NVML READ/WRITE capability checks by UUID. Host-only regressions cover reordered visibility and the one-visible-GPU case, and an RTX 5080 real-CUDA/NCCL check passed with one visible device.
| # Close NVML | ||
| pynvml.nvmlShutdown() | ||
| local_device = torch.cuda.current_device() | ||
| rank_devices = _all_gather_object(group, (socket.gethostname(), local_device)) |
There was a problem hiding this comment.
🟡 warning: Intranode grouping keys on socket.gethostname(). In containerized deployments, ranks on the same physical host may report distinct hostnames (per-container), silently skipping real intranode edges, while ranks on different hosts may all report the same generic hostname (e.g. 'localhost'), producing false 'unsupported pair' failures for inter-node ranks. Consider a more robust host identity (e.g. host UUID, boot id, or MAC-derived id) or at least documenting this assumption.
🤖 v5
There was a problem hiding this comment.
Fixed in 2161508. Linux kernel boot ID is now the primary physical-node key, so containers on one machine can have different hostnames while different machines remain distinct. Hostname is only a portability fallback when the Linux boot ID is unavailable.
|
|
||
| # Close NVML | ||
| pynvml.nvmlShutdown() | ||
| local_device = torch.cuda.current_device() |
There was a problem hiding this comment.
🔵 suggestion: The preflight uses torch.cuda.current_device(), while the legacy runtime later derives the device from self.runtime.get_local_device_id(). If a caller constructs Buffer before calling torch.cuda.set_device(...), the preflight may validate the wrong device (typically device 0 on every rank). Consider asserting the device has been set, or deriving the device the same way the runtime will.
🤖 v5
There was a problem hiding this comment.
The preflight intentionally uses the current CUDA device because the native Buffer constructor immediately reads the same process state via cudaGetDevice. If the caller never selects the intended device, both paths use device 0. The cross-process ambiguity is addressed separately by exchanging physical UUIDs instead of current_device ordinals.
| local_host, local_device = rank_devices[rank] | ||
| access_results: List[PeerAccessResult] = [] | ||
| for peer_rank, (peer_host, peer_device) in enumerate(rank_devices): | ||
| if peer_rank != rank and peer_host == local_host: |
There was a problem hiding this comment.
🔵 suggestion: If two ranks on the same host map to the same CUDA device (GPU oversubscription), cudaDeviceCanAccessPeer(d, d) returns 0, so the pair is reported as an unsupported P2P edge. Failing is correct since DeepEP does not support shared devices, but the 'unsupported directed pairs' message is misleading for this case; detecting duplicate (host, device) tuples and reporting them explicitly would be more actionable.
🤖 v5
There was a problem hiding this comment.
Fixed in 2161508. The gathered (boot ID, GPU UUID) assignments are validated before queries, and duplicate rank assignments now produce an explicit same-physical-GPU error.
|
|
||
| # Normal kernels directly access every intranode peer buffer. Validate the | ||
| # complete directed P2P matrix before allocating the large IPC buffer. | ||
| if num_nvl_bytes > 0: |
There was a problem hiding this comment.
🔵 suggestion: Pure low-latency mode with allow_nvlink_for_low_latency_mode=True (NVSHMEM_DISABLE_P2P='0') still bypasses the preflight when num_nvl_bytes == 0, even though NVSHMEM may then use P2P transport. If NVSHMEM P2P has the same full-matrix requirement, consider extending the gate to that configuration in a follow-up.
🤖 v5
There was a problem hiding this comment.
Leaving this as a follow-up. This PR preflights the direct intranode buffer path guarded by num_nvl_bytes > 0. NVSHMEM owns transport selection in pure low-latency mode, and I do not yet have evidence that enabling its P2P transport requires the same full directed matrix.
|
|
||
| def format_p2p_preflight_error(unsupported_pairs: Sequence[UnsupportedPeerPair], num_required_pairs: int) -> str: | ||
| """Build one deterministic, actionable error for all unsupported pairs.""" | ||
| pair_list = ', '.join(f'(rank {src_rank} GPU {src_device} -> rank {dst_rank} GPU {dst_device})' |
There was a problem hiding this comment.
🔵 suggestion: On large partial-P2P hosts the aggregated error enumerates every unsupported pair in one line (56 entries for 8 GPUs, more for larger domains). Consider capping the enumerated list (e.g. first N pairs plus a total count) or formatting one pair per line to keep logs readable, while keeping the full count deterministic.
🤖 v5
There was a problem hiding this comment.
Fixed in 2161508. The error still reports every pair for an 8-GPU domain (up to 56 directed pairs), but caps larger lists at 64 entries and reports the deterministic omitted count plus the full unsupported/required total.
| # Close NVML | ||
| pynvml.nvmlShutdown() | ||
| local_device = torch.cuda.current_device() | ||
| rank_devices = _all_gather_object(group, (socket.gethostname(), local_device)) |
There was a problem hiding this comment.
🟡 warning: socket.gethostname() 与 torch.cuda.current_device() 都是进程本地视角,不能保证在同一物理节点的所有 rank 上一致:容器化部署中每个 pod/容器的 hostname 可能不同,导致同一节点的 rank 被当成跨节点而跳过必检的 P2P 边;若各 rank 的 CUDA_VISIBLE_DEVICES 不一致(例如每 rank 只暴露自己的 GPU),上报的 local_device 可能都是 0,can_access_peer(0, 0) 恒为 True,预检会误通过。这样仍会在后续 CUDA peer setup 阶段才失败,违背预检目的。建议使用全局唯一的物理设备标识(如 GPU UUID/PCI bus ID)或基于 NCCL 物理域信息来确定 intranode 拓扑,并在 device ID 无法映射时显式报错。
🤖 v4p
There was a problem hiding this comment.
Fixed in 2161508 by the same physical-identity change: boot ID replaces hostname as the primary node key, GPU UUID replaces process-local device ordinal, UUIDs are mapped in the source process, and hidden peers use NVML READ/WRITE status. Rank-local query failures are also gathered before raising so no rank waits in a later collective.
🤖 ds-review-bot Code Reviewv6新的预检错误地跨进程复用了 CUDA 逻辑设备序号,会在常见的单 GPU 可见性配置下阻止合法拓扑初始化。 v5This MR implements a full-matrix intranode CUDA P2P preflight before buffer construction, fixing #584. The change is well-scoped and initialization-only: a new pure control-plane module v4p本 MR 将 DeepEP 的 P2P 校验前移:在分配 legacy Files reviewed: 7 |
Fixes #584.
Problem
DeepEP currently reaches CUDA peer setup before validating that every participating intranode GPU can access every other GPU. On partial-P2P hosts this fails on the first unsupported pair, after initialization has already progressed, and does not show the complete incompatible topology.
Changes
CUDA_VISIBLE_DEVICESmasks._C.Bufferallocation and before elastic communicator/buffer setup.48/56unsupported directed pairs).Validation
python -m pytest -q tests/utils/test_p2p.py— 11 passed, including reordered visibility, single-GPU visibility fallback, duplicate assignments, and the deep_ep.cpp:200 init fails on partial CUDA peer access; preflight and report unsupported device pairs (cudaErrorPeerAccessUnsupported) #584 topology.uv run --isolated --with yapf==0.40.2 --with ruff==0.6.5 --with clang-format==15.0.7 -- bash ./format.sh— passed.git diff --check— passed.8-rank failure-path benchmark
Environment: 8x NVIDIA GeForce RTX 5060 Ti, CUDA 13, one NCCL rank per GPU. This PCIe host exposes no off-diagonal CUDA P2P edges, so the real topology is
56/56unsupported directed pairs. Results use 10 warmups and 100 measured iterations; each iteration reports the slowest rank.56/56The benchmark host validates the real distributed all-unsupported failure path. The exact partial-P2P topology from #584 is covered by the
48/56regression test.Scope
This is an initialization-only check. Dispatch and combine hot paths are unchanged.