Skip to content

[None][fix] Fall back to NCCL when CUDA IPC handles cannot be exchanged - #17034

Open
pjdurden wants to merge 10 commits into
NVIDIA:mainfrom
pjdurden:fix/16899
Open

pjdurden wants to merge 10 commits into
NVIDIA:mainfrom
pjdurden:fix/16899

Conversation

@pjdurden

@pjdurden pjdurden commented Jul 30, 2026 •

Copy link
Copy Markdown

Fall back to NCCL when CUDA IPC handles cannot be exchanged

Fixes #16899

1. Root cause

The reporter's follow-up (v1.3.0rc22) pins the failure precisely:

AllReduce.__init__
  → get_allreduce_workspace
    → CustomAllReduceHelper.allocate_allreduce_fusion_workspace
      → IpcMemory.__init__
        → IpcMemory.open_ipc_memory
          → cudaIpcOpenMemHandle  ← cudaErrorInvalidDevice (101)
RuntimeError: CUDA Runtime API error: <cudaError_t.cudaErrorInvalidDevice: 101>

and notes that can_access_peer() returned True on the same machine.

can_access_peer() (tensorrt_llm/_ipc_utils.py:39) only calls
cudaDeviceCanAccessPeer. That answers "can GPU A address GPU B's memory", which
is necessary but not sufficient for CUDA IPC: exporting and importing an IPC
memory handle is a separate capability that also fails on GPUs without CUDA IPC
support and when the ranks do not share an IPC namespace. On this system
cudaDeviceCanAccessPeer succeeds while cudaIpcOpenMemHandle does not.

Because can_access_peer() said True, IpcMemory was constructed with
open_ipc=True, and open_ipc_memory() passed the raw CUDA error straight to
_raise_if_error(). That unhandled RuntimeError aborts engine initialization.

This is a hard crash for a situation the runtime already fully supports. The
non-IPC path exists and works: IpcMemory(open_ipc=False) leaves all pointers
null, and on the C++ side AllReduceOp::ifFallbackToNCCL
(cpp/tensorrt_llm/thop/allreduceOp.cpp:1436) already routes every all-reduce to
NCCL when P2P/NVLink is absent, which is exactly this hardware (RTX 6000D is a
PCIe part with no NVLink, so mIsNVLINKSupported is false and the custom kernels
would never have been selected anyway). Only the eager Python-side workspace
allocation stood in the way.

One secondary note that matches the report: TRTLLM_DISABLE_CUSTOM_ALLREDUCE=1
had no effect because no such env var exists anywhere in the tree (grep returns
nothing). The supported knob is allreduce_strategy: NCCL in the LLM args, which
skips the workspace allocation entirely. That is a valid workaround, but users
should not need it.

2. The fix

Degrade to the already-supported NCCL path instead of crashing.

tensorrt_llm/_ipc_utils.py. IpcMemory.open_ipc_memory no longer raises
when IPC handles cannot be exchanged. It returns None, and IpcMemory.__init__
reacts by setting open_ipc = False and ipc_failed = True, leaving the
pointers null. Three details matter:

  • Collective agreement. The decision is allgathered across the TP group, so
    either every rank gets IPC buffers or none does. Without this, a rank that
    succeeded would keep using buffers its peers never mapped, and the subsequent
    tp_allgather calls could desync.
  • No garbage handles. A rank that cannot export contributes None to the
    handle allgather rather than an uninitialized cudaIpcMemHandle_t, so no rank
    ever passes an uninitialized handle to cudaIpcOpenMemHandle. That one
    collective carries both the payload and the agreement on the export step, so
    the total is still two collectives, as before.
  • Cleanup. Handles opened before the failure are closed and the local buffer
    is freed, so the fallback does not leak device memory.

cudaMalloc/cudaMemset failures still raise. Those are genuine errors, not a
capability gap.

tensorrt_llm/_torch/distributed/allreduce_helper.py. Gate
lamport_initialize() on lamport_buffers.open_ipc rather than on
is_p2p_supported. Those two can now disagree, and calling lamport_initialize
with a null local_ptr would be an illegal memory access.

tensorrt_llm/_torch/distributed/ops.py. get_allreduce_workspace returns
the workspace together with an ipc_failed flag, and AllReduce.__init__
downgrades the strategy to NCCL only when that flag is set. The flag
distinguishes the new failure mode (P2P reported, IPC handles unusable) from a
workspace that holds no IPC buffers by design, which is the ordinary inter-node
TP case. That distinction is load-bearing: reacting to the latter would rewrite
the strategy before the MNNVL block and silently disable mnnvl_allreduce on
multi-node NVLink systems. The LOWPRECISION workspace allocation moved below
the check so initialize_static_lowprecision_buffers is not called on a null
workspace.

SYMM_MEM is unaffected: AllReduce.forward tries self.symm_mem_allreduce
before consulting self.strategy, so the downgrade only changes the fallback
path.

Fused ops that have no NCCL path. MoEAllReduce and MiniMaxAllReduceRMS
hand the workspace to custom kernels that reinterpret it as void**, and neither
has a non-IPC alternative, so the strategy downgrade above does not cover them.
They now raise an explicit error when ipc_failed is set, rather than
dereferencing the null peer pointers inside the kernel. That is the same
init-time failure those paths had before this PR, with a message that says why.

3. Files changed

File Change
tensorrt_llm/_ipc_utils.py open_ipc_memory returns Optional[...]; collective IPC-failure agreement without exchanging uninitialized handles; cleanup; IpcMemory.ipc_failed; single warning on the group
tensorrt_llm/_torch/distributed/allreduce_helper.py gate lamport_initialize on the actual IPC state
tensorrt_llm/_torch/distributed/ops.py get_allreduce_workspace returns (workspace, ipc_failed); strategy downgrade to NCCL on real IPC failure only; reorder LOWPRECISION allocation; explicit error for MoEAllReduce / MiniMaxAllReduceRMS
tests/unittest/_torch/distributed/test_ipc_memory_fallback.py new, 11 tests, CUDA runtime and collectives stubbed

4. Reported validation on a second platform

The reporter of #16899 applied the first revision of this patch on 8x RTX 5090
(PCIe, no NVLink), which is a different platform from the RTX 6000D in the
original report:

  • Before: crash at engine init, cudaErrorInvalidDevice: 101 from
    cudaIpcOpenMemHandle, on all 8 ranks.
  • After: falls back to NCCL, engine initializes, warm-up and a 100-request
    benchmark run to completion.

So the failure is not specific to the RTX 6000D, it affects PCIe-only parts
generally. That run predates the changes in section 2 that narrow the downgrade
and rework the handle exchange.

5. Risk and uncertainty

  • I could not reproduce on the reporter's hardware. No RTX 6000D, and this
    environment has neither GPUs nor torch. The root cause is derived from the
    reporter's traceback plus reading the code; the specific reason
    cudaIpcOpenMemHandle returns 101 on that GPU is not established. The fix is
    deliberately agnostic to that reason, it handles "IPC handles cannot be
    exchanged" however it arises.
  • Behaviour change: what used to be a fatal error is now a warning plus a
    slower but correct NCCL path. That is the right trade-off here, since the
    runtime already treats non-P2P topologies this way, but it does mean a
    genuinely broken IPC setup degrades instead of failing loudly. The warning is
    emitted once per process, from open_ipc_memory where the condition is
    detected, and on every rank of the group rather than only on the ones that
    failed locally.
  • Why FP8 worked for the reporter is not established. Their FP8 runs did not
    hit this initialization path; I have no evidence about the mechanism and make
    no claim about it.
  • No extra collective. The IPC-failure agreement rides along with the handle
    allgather that already existed, plus the one that covers the open step. Two
    collectives per IpcMemory construction, unchanged.
  • Not addressed (out of scope): can_access_peer() still reports True on
    such systems, so self.is_p2p_supported in modeling_deepseekv3.py,
    modeling_glm.py, modeling_qwen3_moe.py and friends remains optimistic. A
    complete fix would also plumb the IPC verdict into
    AllReduceOp::setGroupTopology (cpp/tensorrt_llm/thop/allreduceOp.cpp) so
    that C++ and Python agree from the start; the strategy downgrade above covers
    the same ground from the Python side without touching C++.
  • The reporter's original 0.21 symptom (BF16 fails at concurrency 4 but works at
    concurrency 1) is a different failure from the rc22 init crash addressed here.
    Their rc22 report supersedes it, and that is what this fix targets.

6. How I verified it

Constraints: no GPU and no torch in this environment, so pytest cannot import
tensorrt_llm. The pytest file was not executed. Instead:

  1. Logic verified against the real edited source. A standalone harness (not
    committed) loads the actual tensorrt_llm/_ipc_utils.py via importlib with
    the CUDA runtime, Distributed, logger and Mapping stubbed, and runs six
    scenarios. All assertions pass:
    • IPC works: open_ipc=True, ipc_failed=False, non-null pointers, one
      handle opened, two collectives, no warning.
    • cudaIpcOpenMemHandle returns cudaErrorInvalidDevice(101), the reported
      failure: no exception, ipc_failed=True, all pointers null, local buffer
      freed, warning names the failing call.
    • Local rank succeeds but a peer failed to open: this rank also falls back,
      opened handles closed, buffer freed, and it still warns rather than
      degrading silently.
    • cudaIpcGetMemHandle fails locally: no handle is ever opened, buffer freed.
    • A peer failed to export: cudaIpcOpenMemHandle is never called, so no
      uninitialized handle reaches the driver.
    • IPC never requested, and TP larger than one node: open_ipc=False with
      ipc_failed=False, no allocation, no collective. This is the case that must
      not disturb strategy selection.
  2. Lint and format, matching this repo's split toolchain, with ruff pinned
    to the v0.9.4 this repo's pre-commit uses:
    • ruff check and ruff format --diff clean on _ipc_utils.py,
      allreduce_helper.py and the new test (non-legacy files).
    • yapf --diff and isort --diff clean on ops.py, which is in
      legacy-files.txt, and scripts/legacy_utils.py lint-precommit reports
      nothing on it.

Not verified: end-to-end trtllm-bench --tp 8 on affected hardware, and that
NCCL throughput is acceptable for this workload. Both need the reporter's
machine. The new tests are in tests/unittest/_torch/distributed, which #16498
moved from l0_dgx_h100.yml to the CPU-only l0_cpu.yml on 2026-08-04, so CI
runs them with zero GPUs. Everything there passes without a GPU; the three tests
that build an AllReduce skip if the trtllm custom ops are not registered, and
test_lamport_skipped skips because the workspace helper it drives allocates
device tensors. The CUDA stubbing is unittest.mock.patch scoped to a context
manager and touches no real CUDA or MPI state, matching how
test_safe_mpi_comm.py in the same directory already patches.

Rebased onto main on 2026-08-05 (was 162 behind, now 0). Clean rebase: none of
the three source files had upstream commits since the branch point.

Dev Engineer Review

  • IPC initialization now coordinates fallback across tensor-parallel ranks.
  • Failed handle opens and allocated buffers are cleaned up.
  • Lamport initialization now requires usable IPC.
  • AllReduce falls back to NCCL only after a genuine IPC exchange failure.
  • Allocation and memset errors remain fatal.
  • Fused paths without NCCL alternatives now raise explicit errors.
  • The updated get_allreduce_workspace return type is consistent with its callers.
  • No configuration or test-list files changed.
  • GPU-based pytest and end-to-end validation were not available. Static checks and a standalone logic harness passed.

QA Engineer Review

  • Added 11 tests in tests/unittest/_torch/distributed/test_ipc_memory_fallback.py.
  • Coverage includes successful IPC, local and peer export/open failures, cleanup, collective fallback agreement, inter-node tensor parallelism, strategy selection, NCCL downgrade, fused-operation errors, MNNVL preservation, and Lamport initialization.
  • The tests are not listed in tests/integration/test_lists/, test-db/, or qa/.
  • Verdict: sufficient for the added IPC fallback logic. GPU-based integration coverage remains unavailable.

Copilot AI review requested due to automatic review settings July 30, 2026 00:10
@pjdurden
pjdurden requested review from a team as code owners July 30, 2026 00:10
@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

CUDA IPC initialization now coordinates failures across tensor-parallel ranks, cleans up partial resources, and reports unavailable IPC. Allreduce initialization selects NCCL or raises an error based on IPC availability. Tests cover success, failure, fallback, and cleanup paths.

Changes

CUDA IPC fallback

Layer / File(s) Summary
IPC failure contract and cleanup
tensorrt_llm/_ipc_utils.py
IpcMemory returns an optional result, synchronizes IPC failures across ranks, cleans up handles and allocations, and disables IPC when setup fails.
Allreduce workspace fallback
tensorrt_llm/_torch/distributed/ops.py, tensorrt_llm/_torch/distributed/allreduce_helper.py
Allreduce detects failed IPC workspaces, selects NCCL when supported, rejects fused operations without an IPC workspace, and skips Lamport initialization for unopened IPC.
IPC fallback validation
tests/unittest/_torch/distributed/test_ipc_memory_fallback.py
CUDA and collective fakes test IPC setup, export and import failures, intentional IPC absence, strategy changes, errors, and cleanup.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: bowenfu, schetlur-nv

Sequence Diagram(s)

sequenceDiagram
  participant AllReduce
  participant IpcMemory
  participant TPCollective
  participant NCCL
  AllReduce->>IpcMemory: initialize IPC workspace
  IpcMemory->>TPCollective: exchange IPC failure status
  TPCollective-->>IpcMemory: return TP-wide status
  IpcMemory-->>AllReduce: return workspace and failure flag
  AllReduce->>NCCL: select NCCL when IPC failed
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)

Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #16899 by handling CUDA IPC exchange failures and using the supported NCCL fallback path.
Out of Scope Changes check ✅ Passed The source changes and tests are directly related to CUDA IPC fallback, all-reduce strategy handling, cleanup, and error reporting.
Title check ✅ Passed The title clearly summarizes the primary change and follows the repository's required [None][fix] format.
Description check ✅ Passed The description clearly explains the root cause, solution, risks, validation, and test coverage, despite using different section headings.
Finishing Touches

🧪 Generate unit tests (beta)

  • Create PR with unit tests
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/unittest/_torch/distributed/test_ipc_memory_fallback.py (1)

74-157: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Expand coverage for the remaining fallback contracts.

Coverage summary — insufficient. The three added tests cover successful IPC setup, local import failure, and peer-reported failure. CI/QA test-list membership cannot be verified because no tests/integration/test_lists/test-db/ or tests/integration/test_lists/qa/ file was provided.

  • Make FakeCudart.cudaIpcGetMemHandle() configurable and test its new dummy-handle/cleanup path.
  • In test_local_ipc_open_failure_falls_back_instead_of_raising, use FakeDist(TP_SIZE) so a local failure alone drives agreement; peer_ipc_ok=False currently masks failures to propagate the local error.
  • Add mocked coverage that unavailable IPC downgrades AllReduce to NCCL with workspace is None, and that lamport_initialize() is skipped.

As per path instructions, test changes require coverage and test-list status review.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unittest/_torch/distributed/test_ipc_memory_fallback.py` around lines
74 - 157, Expand the IPC fallback tests around FakeCudart.cudaIpcGetMemHandle,
making its result configurable and covering the dummy-handle and cleanup
behavior. Update test_local_ipc_open_failure_falls_back_instead_of_raising to
use FakeDist(TP_SIZE), then add mocked coverage verifying unavailable IPC
downgrades AllReduce to NCCL with workspace=None and skips lamport_initialize();
also review coverage and test-list status for these test changes.

Source: Path instructions

tensorrt_llm/_ipc_utils.py (1)

18-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use built-in generics in open_ipc_memory

Replace Optional[Tuple[List[int], int]] with tuple[list[int], int] | None. List is still needed by the other annotations in this file, so the typing import can’t be dropped yet.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tensorrt_llm/_ipc_utils.py` at line 18, Update the return annotation of
open_ipc_memory to use the built-in generic form tuple[list[int], int] | None
instead of Optional[Tuple[List[int], int]]. Retain the existing typing imports
because List remains required by other annotations in the file.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tensorrt_llm/_ipc_utils.py`:
- Around line 161-163: Update the CUDA IPC warning in the logger.warning call to
use the f-string conversion flag for ipc_error instead of explicitly calling
repr(), resolving Ruff RUF010 while preserving the existing message.

---

Nitpick comments:
In `@tensorrt_llm/_ipc_utils.py`:
- Line 18: Update the return annotation of open_ipc_memory to use the built-in
generic form tuple[list[int], int] | None instead of Optional[Tuple[List[int],
int]]. Retain the existing typing imports because List remains required by other
annotations in the file.

In `@tests/unittest/_torch/distributed/test_ipc_memory_fallback.py`:
- Around line 74-157: Expand the IPC fallback tests around
FakeCudart.cudaIpcGetMemHandle, making its result configurable and covering the
dummy-handle and cleanup behavior. Update
test_local_ipc_open_failure_falls_back_instead_of_raising to use
FakeDist(TP_SIZE), then add mocked coverage verifying unavailable IPC downgrades
AllReduce to NCCL with workspace=None and skips lamport_initialize(); also
review coverage and test-list status for these test changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ab9b4213-0365-4477-a4b7-557a34646fe2

📥 Commits

Reviewing files that changed from the base of the PR and between 960530b and 0c410e3.

📒 Files selected for processing (4)
  • tensorrt_llm/_ipc_utils.py
  • tensorrt_llm/_torch/distributed/allreduce_helper.py
  • tensorrt_llm/_torch/distributed/ops.py
  • tests/unittest/_torch/distributed/test_ipc_memory_fallback.py

Comment thread tensorrt_llm/_ipc_utils.py Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a Tensor Parallel initialization failure where CUDA IPC handle import/export can fail (e.g., cudaIpcOpenMemHandle returning cudaErrorInvalidDevice) even when cudaDeviceCanAccessPeer reports P2P capability, by degrading to the already-supported NCCL path instead of aborting engine initialization.

Changes:

  • Make IpcMemory.open_ipc_memory() return None on CUDA IPC handle exchange failures (with TP-wide agreement + cleanup), and have IpcMemory.__init__ disable IPC (null pointers) instead of raising.
  • Gate Lamport buffer initialization on lamport_buffers.open_ipc rather than the optimistic P2P check.
  • In AllReduce.__init__, detect workspaces that have no usable IPC buffers and downgrade the strategy to NCCL; add a focused unit test module with stubbed CUDA runtime / distributed collectives.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
tensorrt_llm/_ipc_utils.py Make CUDA IPC failures non-fatal by returning None and cleaning up; disables IPC state in IpcMemory on failure.
tensorrt_llm/_torch/distributed/allreduce_helper.py Prevent lamport_initialize() from running when IPC buffers are not actually available.
tensorrt_llm/_torch/distributed/ops.py Add IPC-workspace detection and force NCCL fallback when IPC buffers are missing; reorder LOWPRECISION allocation to avoid null-workspace usage.
tests/unittest/_torch/distributed/test_ipc_memory_fallback.py Add stubbed unit tests covering IPC-success, local IPC-open failure, and peer IPC failure agreement behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tensorrt_llm/_torch/distributed/ops.py Resolved
Comment thread tensorrt_llm/_ipc_utils.py Resolved
@pjdurden

Copy link
Copy Markdown
Author

@BowenFu @schetlur-nv @JunyiXu-nv @allisonlim-nv could one of you take a look when you get a chance?

The reporter of #16899 applied the patch on 8x RTX 5090 (PCIe, no NVLink), a different platform from the RTX 6000D in the original report:

Before: crash at engine init, cudaErrorInvalidDevice: 101 from cudaIpcOpenMemHandle, all 8 ranks.
After: falls back to NCCL, engine initializes, warmup and a 100 request benchmark run to completion.

So the failure is not specific to the RTX 6000D, it affects PCIe-only parts generally.

Also pushed one more commit for Ruff nit.

@bo-nv

bo-nv commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

/bot run

@brnguyen2 brnguyen2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _ipc_utils.py fix is the right one and the collective-agreement + cleanup details are handled carefully. Two things before merge.

The ops.py downgrade is broader than the bug. allreduce_workspace_has_ipc() is False whenever can_access_peer() is False — which includes the entirely normal inter-node TP case (can_access_peer returns False on a node-rank mismatch, and IpcMemory.__init__ already forces open_ipc=False when tp_size > gpus_per_node). See the inline comment at [ops.py:785](https://github.com/NVIDIA/TensorRT-LLM/pull/17034/files#diff-8343f10ef2d25d7245cbff50bea93ad47fd865db34a1e3c1d116ae57f7ab7ac1R785); as written this disables MNNVL on multi-node NVLink systems. The _ipc_utils + allreduce_helper changes alone already fix #16899; if you keep the ops.py guard, it should trigger only on the new failure mode (P2P reported, IPC unusable) and run after the MNNVL block.

Test coverage stops at _ipc_utils. The three new tests are good, but nothing covers the two behavioral changes that can break working hardware: the AllReduce.__init__ strategy downgrade and the lamport_initialize gating. A test asserting that an MNNVL-eligible mapping still gets mnnvl_allreduce set would have caught the issue above. tests/unittest/_torch/distributed is enrolled as a directory in l0_dgx_h100.yml, so the new file will run — worth confirming the cudart patching is safe alongside the real-GPU tests in that stage.

Description nits: the last paragraph is cut off mid-sentence ("trading a "), and the claim that FP8 "worked because it fits in fewer bytes, not because it dodges IPC" contradicts the following clause about not hitting the init path. Also, MoEAllReduce and MiniMaxAllReduceRMS still take the workspace unconditionally, so the "defense in depth" argument doesn't cover them — worth saying so explicitly rather than implying full coverage.

Comment thread tensorrt_llm/_torch/distributed/ops.py Outdated
# Every custom all-reduce kernel reads the peers' IPC buffers. When
# CUDA IPC is unavailable those pointers are null, so NCCL is the
# only strategy that can run. See allreduce_workspace_has_ipc.
if not allreduce_workspace_has_ipc(self.mapping):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fires far more often than the bug it targets. allreduce_workspace_has_ipc() is False for any workspace built with is_p2p_supported=False, and can_access_peer() returns False on a plain node-rank mismatch ([_ipc_utils.py:46](https://github.com/NVIDIA/TensorRT-LLM/pull/17034/files#diff-58a9acafed7455cb039511131ef7c8fb87d8d39200b75e733c93914b25e26eecR46)) — plus IpcMemory.__init__ forces open_ipc=False when tp_size > gpus_per_node. So on any multi-node TP config this rewrites self.strategy to NCCL, and the MNNVL block right below ([ops.py:795](https://github.com/NVIDIA/TensorRT-LLM/pull/17034/files#diff-8343f10ef2d25d7245cbff50bea93ad47fd865db34a1e3c1d116ae57f7ab7ac1R795)) is keyed on self.strategy in (AUTO, MNNVL) — so mnnvl_allreduce is never constructed. That's a silent perf regression on GB200-class multi-node NVLink, which is exactly the topology MNNVL exists for.

Two fixes, either works: (a) only downgrade when the new failure mode occurred — i.e. can_access_peer(mapping) was True but open_ipc came back False — so systems that never had P2P keep today's behavior; or (b) move this block below the MNNVL init and skip it when self.mnnvl_allreduce is not None. (a) is narrower and matches the PR's stated scope.

@pjdurden pjdurden Aug 5, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with (a). IpcMemory tracks ipc_failed separately from open_ipc now, set only when IPC was requested and the group fits in one node but the exchange still failed. can_access_peer False, node-rank mismatch, tp_size > gpus_per_node all leave it False, so multi-node TP does not touch self.strategy and the MNNVL block runs like before.

Added test_mnnvl_kept_without_p2p for it.

Comment thread tensorrt_llm/_torch/distributed/ops.py Outdated
# CUDA IPC is unavailable those pointers are null, so NCCL is the
# only strategy that can run. See allreduce_workspace_has_ipc.
if not allreduce_workspace_has_ipc(self.mapping):
logger.warning(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AllReduce is instantiated per decoder layer (often several per layer), so on an affected system this prints a warning ~100+ times per rank. Use logger.warning_once(..., key=...), or better, emit the message once from open_ipc_memory where the condition is actually detected.

@pjdurden pjdurden Aug 5, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it. warning_once with a key, out of open_ipc_memory where the condition is detected, so one line per process instead of one per AllReduce. Left a logger.debug at the strategy site that names what got downgraded.

Comment thread tensorrt_llm/_ipc_utils.py Outdated
ipc_error = None if error == cudart.cudaError_t.cudaSuccess else error
if ipc_error is not None:
# Exchange a dummy handle to keep the collective below symmetric.
local_handle = cudart.cudaIpcMemHandle_t()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exchanging an all-zero cudaIpcMemHandle_t means every other rank calls cudaIpcOpenMemHandle on garbage. It probably just returns an error you catch below, but you're feeding an uninitialized handle to the driver to preserve collective symmetry when a simpler option exists: allgather the cudaIpcGetMemHandle success flag first and skip the handle exchange + open loop entirely if any rank failed. Same number of collectives (you need a second agreement allgather for open failures either way), no garbage handles.

@pjdurden pjdurden Aug 5, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped the dummy handle. A rank that cannot export sends None in the handle allgather, and everyone bails before the open loop if any entry is None. That collective carries the payload and the export agreement together, so still two collectives, second one covers open failures.

Comment thread tensorrt_llm/_ipc_utils.py Outdated
peer_ptrs.append(ptr)
opened_ptrs.append(ptr)

if not all(dist.tp_allgather(ipc_error is None)):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only ranks that failed locally log; ranks that succeeded and are falling back because a peer failed degrade silently. Since the group decision is what matters operationally, log unconditionally in this branch (with the peer-driven case worded differently), so a user reading rank 0's log can tell why custom all-reduce is off.

@pjdurden pjdurden Aug 5, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches log now, through a shared disable_ipc(). Message is either failed here with or failed on a peer rank of the TP group.

Comment thread tensorrt_llm/_torch/distributed/ops.py Outdated
still cannot be imported. In that case the workspace pointers are null and only
NCCL can be used. get_allreduce_workspace must have been called first.
"""
allreduce_workspaces = getattr(_thread_local,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getattr without a default raises AttributeError if get_allreduce_workspace hasn't run for this pp_rank — the docstring notes the precondition, but an unguarded getattr/[mapping] turns a misuse into an opaque traceback. Cheap fix: take the workspace tuple as an argument, or have get_allreduce_workspace return both the tensor and the flag so the invariant is structural rather than documented.

@pjdurden pjdurden Aug 5, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_allreduce_workspace returns (workspace, ipc_failed) and computes the flag when it fills the thread-local, so no second reader and no bare getattr. MoEAllReduce and MiniMaxAllReduceRMS go through _require_ipc_workspace() which raises, they have no NCCL path.

pjdurden added a commit to pjdurden/TensorRT-LLM that referenced this pull request Aug 5, 2026
Addresses review feedback on NVIDIA#17034.

allreduce_workspace_has_ipc() was False for any workspace built without P2P,
which includes ordinary inter-node TP, so it rewrote the strategy to NCCL
before the MNNVL block and mnnvl_allreduce was never constructed on multi-node
NVLink systems. IpcMemory now records ipc_failed separately from open_ipc, and
only that failure mode, P2P reported but handles unusable, downgrades the
strategy. Configurations that never had P2P keep the behaviour they had.

get_allreduce_workspace returns the flag alongside the workspace, so a caller
cannot read the thread-local state before it has been populated.

The fallback is reported once, from open_ipc_memory where it is detected, and
on every rank of the group rather than only on the ones that failed locally.
AllReduce is constructed per decoder layer, so it no longer warns per instance.

A rank that cannot export its handle contributes None to the allgather instead
of an uninitialized cudaIpcMemHandle_t, so no rank imports a garbage handle.
The number of collectives is unchanged.

MoEAllReduce and MiniMaxAllReduceRMS have no NCCL path, so they now raise an
actionable error instead of letting the kernel dereference null peer pointers.

Tests cover the strategy downgrade, MNNVL still being selected when the
workspace has no IPC buffers by design, and the lamport_initialize gating.

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@pjdurden pjdurden changed the title [Bug]: TensorRT-LLM 1.3.0rc18 Llama-3-70B inference failure on 8x NVIDIA RTX 6000D [None][fix] Fall back to NCCL when CUDA IPC handles cannot be exchanged Aug 5, 2026
@pjdurden

pjdurden commented Aug 5, 2026 •

Copy link
Copy Markdown
Author

All five inline points addressed. Pushed as a separate commit on top of the original fix so the delta reads on its own, then re-signed the branch to clear DCO, it was failing on the first commit.

ops.py downgrade: went with (a). IpcMemory tracks ipc_failed separately from open_ipc, set only when IPC was requested and possible but the exchange failed. can_access_peer returning False leaves it False, so inter-node TP keeps current behaviour and still reaches the MNNVL block. get_allreduce_workspace returns (workspace, ipc_failed), which also kills the bare getattr at ops.py:62.

Tests are 11 now. New: test_mnnvl_kept_without_p2p (the guard for the above), test_downgrade_to_nccl, test_lamport_skipped which drives the real allocate_allreduce_fusion_workspace and asserts lamport_initialize is not called, test_moe_allreduce_raises, test_peer_export_fails, plus two for ipc_failed staying False.

On the cudart patching in that stage: every patch is unittest.mock.patch scoped to a with block on module attributes, nothing survives the test, and the fallback frees its fake pointers through the fake runtime before the patch unwinds. test_safe_mpi_comm.py in the same dir already patches this way. Only test_lamport_skipped touches real CUDA, it is gated on torch.cuda.is_available() because the helper allocates device tensors.

MoEAllReduce and MiniMaxAllReduceRMS: you are right that defense in depth overclaimed. Neither has a non-IPC path, so they raise explicitly when ipc_failed is set. Same init-time failure they had before this PR, just with a message instead of a null deref in the kernel. Called out in the description now.

Description rewritten. Truncated paragraph gone, FP8 claim dropped, I have no evidence for the mechanism there, only that those runs did not hit this path. Title follows [None][fix] now.

Same caveat as before: no GPU or torch here so pytest did not run. The _ipc_utils logic is covered by a standalone harness that loads the real edited module with the CUDA runtime and collectives stubbed, six scenarios, all pass. Section 6 of the description lists them. Rest needs CI.

DCO is green. Can you kick off another /bot run?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/unittest/_torch/distributed/test_ipc_memory_fallback.py (1)

271-302: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test coverage summary: insufficient.

Added tests cover IPC success, local and peer IPC failures, intentional IPC absence, MNNVL preservation, NCCL fallback, MoE failure, and Lamport initialization.

Add a direct MiniMaxAllReduceRMS construction test with ipc_failed=True. The production change now routes this class through _require_ipc_workspace(), but no test verifies that integration.

Test-list membership is not applicable to this review because no tests/integration/test_lists/ file was supplied. Run pytest tests/unittest/ after the update.

As per path instructions, test-code changes must include a coverage verdict.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unittest/_torch/distributed/test_ipc_memory_fallback.py` around lines
271 - 302, The tests lack direct coverage for MiniMaxAllReduceRMS when IPC is
unavailable. Add a unit test that constructs MiniMaxAllReduceRMS with
ipc_failed=True and verifies the production path raises the expected
requires-CUDA-IPC error through _require_ipc_workspace(); then run pytest
tests/unittest/ and include a coverage verdict for the added test.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tensorrt_llm/_torch/distributed/ops.py`:
- Around line 69-81: Update _require_ipc_workspace to distinguish IPC usability
from ipc_failed, and reject workspaces when IPC is intentionally unavailable as
well as when initialization fails; do not return a workspace containing null
peer pointers to MoEAllReduce or MiniMaxAllReduceRMS. Preserve the upstream
contract that intentional IPC absence leaves ipc_failed false, and add
inter-node coverage for both IPC-only fused operations.

In `@tests/unittest/_torch/distributed/test_ipc_memory_fallback.py`:
- Around line 140-143: Update _run so the IpcMemory created with open_ipc=True
is explicitly cleaned up while cudart_patch and dist_patch remain active, then
set its open_ipc attribute to False before returning it to prevent
IpcMemory.__del__ from performing a second cleanup.

---

Nitpick comments:
In `@tests/unittest/_torch/distributed/test_ipc_memory_fallback.py`:
- Around line 271-302: The tests lack direct coverage for MiniMaxAllReduceRMS
when IPC is unavailable. Add a unit test that constructs MiniMaxAllReduceRMS
with ipc_failed=True and verifies the production path raises the expected
requires-CUDA-IPC error through _require_ipc_workspace(); then run pytest
tests/unittest/ and include a coverage verdict for the added test.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f1588ceb-fd00-410f-8baf-31aafa0793da

📥 Commits

Reviewing files that changed from the base of the PR and between 1dfb7b1 and 7866550.

📒 Files selected for processing (4)
  • tensorrt_llm/_ipc_utils.py
  • tensorrt_llm/_torch/distributed/allreduce_helper.py
  • tensorrt_llm/_torch/distributed/ops.py
  • tests/unittest/_torch/distributed/test_ipc_memory_fallback.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tensorrt_llm/_torch/distributed/allreduce_helper.py
  • tensorrt_llm/_ipc_utils.py

Comment thread tensorrt_llm/_torch/distributed/ops.py Outdated
Comment on lines +69 to +81
def _require_ipc_workspace(mapping: Mapping, op_name: str) -> torch.LongTensor:
"""Workspace for fused ops that reinterpret it as `void**` and have no NCCL path.

AllReduce degrades to NCCL when CUDA IPC turns out to be unusable, but these
kernels cannot: they would dereference the null peer pointers. Fail here with an
actionable message rather than in the kernel with an illegal memory access.
"""
workspace, ipc_failed = get_allreduce_workspace(mapping)
if ipc_failed:
raise RuntimeError(
f"{op_name} requires CUDA IPC, which is unavailable on this system, "
"and it has no NCCL fallback.")
return workspace

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Reject workspaces with no usable IPC buffers.

ipc_failed is false when IPC was intentionally unavailable, such as inter-node TP. In that state, IpcMemory still serializes null peer pointers. Lines 76-81 return this workspace to MoEAllReduce and MiniMaxAllReduceRMS, and their fused kernels can dereference the null pointers.

Track IPC usability separately from IPC failure. Make _require_ipc_workspace() reject both states. Add inter-node tests for both IPC-only fused operations.

Based on the supplied upstream contract, intentionally absent IPC keeps ipc_failed=False and leaves peer pointers null.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tensorrt_llm/_torch/distributed/ops.py` around lines 69 - 81, Update
_require_ipc_workspace to distinguish IPC usability from ipc_failed, and reject
workspaces when IPC is intentionally unavailable as well as when initialization
fails; do not return a workspace containing null peer pointers to MoEAllReduce
or MiniMaxAllReduceRMS. Preserve the upstream contract that intentional IPC
absence leaves ipc_failed false, and add inter-node coverage for both IPC-only
fused operations.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the inter-node case is already the same on main: get_allreduce_workspace builds the IpcMemory buffers with open_ipc=False when tp_size > gpus_per_node, and MoEAllReduce and MiniMaxAllReduceRMS take that null workspace unconditionally. this PR only adds the failure case where P2P was reported but the handle exchange did not work. Rejecting the by-design inter-node workspace in those two ops would change which configurations can build them at all, which I think belongs in its own change rather than in this one. leaving it as is.

pjdurden added a commit to pjdurden/TensorRT-LLM that referenced this pull request Aug 5, 2026
Addresses review feedback on NVIDIA#17034.

allreduce_workspace_has_ipc() was False for any workspace built without P2P,
which includes ordinary inter-node TP, so it rewrote the strategy to NCCL
before the MNNVL block and mnnvl_allreduce was never constructed on multi-node
NVLink systems. IpcMemory now records ipc_failed separately from open_ipc, and
only that failure mode, P2P reported but handles unusable, downgrades the
strategy. Configurations that never had P2P keep the behaviour they had.

get_allreduce_workspace returns the flag alongside the workspace, so a caller
cannot read the thread-local state before it has been populated.

The fallback is reported once, from open_ipc_memory where it is detected, and
on every rank of the group rather than only on the ones that failed locally.
AllReduce is constructed per decoder layer, so it no longer warns per instance.

A rank that cannot export its handle contributes None to the allgather instead
of an uninitialized cudaIpcMemHandle_t, so no rank imports a garbage handle.
The number of collectives is unchanged.

MoEAllReduce and MiniMaxAllReduceRMS have no NCCL path, so they now raise an
actionable error instead of letting the kernel dereference null peer pointers.

Tests cover the strategy downgrade, MNNVL still being selected when the
workspace has no IPC buffers by design, and the lamport_initialize gating.

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

pjdurden added a commit to pjdurden/TensorRT-LLM that referenced this pull request Aug 5, 2026
Addresses review feedback on NVIDIA#17034.

allreduce_workspace_has_ipc() was False for any workspace built without P2P,
which includes ordinary inter-node TP, so it rewrote the strategy to NCCL
before the MNNVL block and mnnvl_allreduce was never constructed on multi-node
NVLink systems. IpcMemory now records ipc_failed separately from open_ipc, and
only that failure mode, P2P reported but handles unusable, downgrades the
strategy. Configurations that never had P2P keep the behaviour they had.

get_allreduce_workspace returns the flag alongside the workspace, so a caller
cannot read the thread-local state before it has been populated.

The fallback is reported once, from open_ipc_memory where it is detected, and
on every rank of the group rather than only on the ones that failed locally.
AllReduce is constructed per decoder layer, so it no longer warns per instance.

A rank that cannot export its handle contributes None to the allgather instead
of an uninitialized cudaIpcMemHandle_t, so no rank imports a garbage handle.
The number of collectives is unchanged.

MoEAllReduce and MiniMaxAllReduceRMS have no NCCL path, so they now raise an
actionable error instead of letting the kernel dereference null peer pointers.

Tests cover the strategy downgrade, MNNVL still being selected when the
workspace has no IPC buffers by design, and the lamport_initialize gating.

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
@pjdurden

pjdurden commented Aug 5, 2026

Copy link
Copy Markdown
Author

Rebased onto main. It was 162 behind, now 0, and it was a clean rebase, none of _ipc_utils.py, ops.py or allreduce_helper.py had upstream commits since my base. All 3 commits signed off, DCO green.

One thing that changed under us and is worth flagging, since your review note assumed otherwise: #16498 moved tests/unittest/_torch/distributed out of l0_dgx_h100.yml and into l0_cpu.yml on 08-04. So the new file runs with zero GPUs, not on the H100 stage. Two consequences:

  • The AllReduce tests now actually execute in pre-merge CI, which is better coverage than before. I guarded the three that build an AllReduce behind a skipif on the trtllm custom ops being registered, because AllReduce.__init__ resolves torch.ops.trtllm.allreduce before any of the logic under test.
  • test_lamport_skipped needs a GPU (the workspace helper allocates device tensors) so it will skip on that stage. Say the word if you would rather I add an explicit entry somewhere GPU-backed instead.

On the cudart patching question, that also gets simpler: nothing in the file touches a real GPU on the CPU stage, and every patch is a unittest.mock.patch scoped to a with block on module attributes.

Separately, checking the MNNVL interaction more closely: is_mnnvl() requires mapping.is_multi_node(), and ipc_failed can only be set when can_access_peer() returned True, which requires single-node. So the two are mutually exclusive in production and option (a) fully covers the case you raised, no reachable config where the downgrade hides MNNVL. The one exception is the TLLM_TEST_MNNVL=1 bypass, which forces is_mnnvl true on a single node. If you want that covered too I can move the block below the MNNVL init as in your option (b), but it looked like unnecessary complexity for a test-only env var.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unittest/_torch/distributed/test_ipc_memory_fallback.py`:
- Around line 92-98: Update FakeCudart.cudaMalloc and FakeCudart.cudaMemset to
support configurable CUDA failures, then add tests covering each RuntimeError
path and verifying errors propagate without falling back from IPC to NCCL. Keep
the existing success behavior unchanged and exercise the failure configuration
through the relevant distributed IPC test flows.
- Around line 46-51: Update _has_allreduce_op to catch only AttributeError when
accessing torch.ops.trtllm.allreduce, allowing unrelated setup failures to
propagate while preserving the false result for a missing operator.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 39b3239f-8583-45ba-9fca-ce8317e5b2d7

📥 Commits

Reviewing files that changed from the base of the PR and between 1dfb7b1 and d5097de.

📒 Files selected for processing (4)
  • tensorrt_llm/_ipc_utils.py
  • tensorrt_llm/_torch/distributed/allreduce_helper.py
  • tensorrt_llm/_torch/distributed/ops.py
  • tests/unittest/_torch/distributed/test_ipc_memory_fallback.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tensorrt_llm/_torch/distributed/allreduce_helper.py
  • tensorrt_llm/_ipc_utils.py
  • tensorrt_llm/_torch/distributed/ops.py

cudaDeviceCanAccessPeer reporting P2P support does not guarantee that CUDA IPC
handles can be exported and imported. On 8x RTX 6000D, and on PCIe-only parts
generally, cudaIpcOpenMemHandle fails with cudaErrorInvalidDevice and engine
initialization aborted instead of using the NCCL path that already exists.

IpcMemory.open_ipc_memory now returns None on such a failure, after agreeing on
the outcome across the whole TP group and releasing whatever it managed to
allocate, so that either every rank gets IPC buffers or none of them does.
IpcMemory keeps null pointers in that case, and lamport_initialize is gated on
the buffers actually being open.

Fixes NVIDIA#16899

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
Addresses review feedback on NVIDIA#17034.

allreduce_workspace_has_ipc() was False for any workspace built without P2P,
which includes ordinary inter-node TP, so it rewrote the strategy to NCCL
before the MNNVL block and mnnvl_allreduce was never constructed on multi-node
NVLink systems. IpcMemory now records ipc_failed separately from open_ipc, and
only that failure mode, P2P reported but handles unusable, downgrades the
strategy. Configurations that never had P2P keep the behaviour they had.

get_allreduce_workspace returns the flag alongside the workspace, so a caller
cannot read the thread-local state before it has been populated.

The fallback is reported once, from open_ipc_memory where it is detected, and
on every rank of the group rather than only on the ones that failed locally.
AllReduce is constructed per decoder layer, so it no longer warns per instance.

A rank that cannot export its handle contributes None to the allgather instead
of an uninitialized cudaIpcMemHandle_t, so no rank imports a garbage handle.
The number of collectives is unchanged.

MoEAllReduce and MiniMaxAllReduceRMS have no NCCL path, so they now raise an
actionable error instead of letting the kernel dereference null peer pointers.

Tests cover the strategy downgrade, MNNVL still being selected when the
workspace has no IPC buffers by design, and the lamport_initialize gating.

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
NVIDIA#16498 moved tests/unittest/_torch/distributed from l0_dgx_h100 to l0_cpu, so
this file now runs with zero GPUs. The three tests that build an AllReduce skip
when the trtllm custom ops are not registered, since AllReduce.__init__ resolves
torch.ops.trtllm.allreduce before any of the logic under test. test_lamport_skipped
still needs a GPU because the workspace helper it drives allocates device tensors.

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
@pjdurden

Copy link
Copy Markdown
Author

Rebased onto current main and pushed as c4842236. It was 284 behind and is now 0, and the PR is mergeable again (it had gone conflicting since the Sep 10 rebase).

One real conflict this time, and it is worth flagging because it sits exactly on the code this PR rewrites. #19184 (Rubin kernels) refactored the handle exchange in IpcMemory.open_ipc_memory, adding _ipc_mem_handle_to_bytes / _ipc_mem_handle_from_bytes so the serialization works across cuda-python and cuda-bindings versions that do not all expose .reserved.

I resolved it by routing this PR's fallback through those new helpers rather than keeping the raw .reserved access:

  • dist.tp_allgather(_ipc_mem_handle_to_bytes(local_handle) if get_error is None else None), so a rank that cannot export still contributes None and no rank imports garbage
  • handle = _ipc_mem_handle_from_bytes(reserved) on the open path, replacing the manual cudaIpcMemHandle_t() + .reserved assignment

That is the only semantic change in the whole rebase. I diffed this PR's own net diff before and after: the four touched files are otherwise byte-identical. The fallback logic, the group agreement and the cleanup path are unchanged.

I also checked the helpers against the stubs in test_ipc_memory_fallback.py. The fake handle exposes .reserved, so it takes the first branch in both helpers and round-trips b"\xab" * 64 intact, and a default-constructed dummy handle serializes to 64 zero bytes as before. So the CPU-only stage assertions still hold.

On the actual blocker: the last pipeline run here is PR_Github #68209 from Aug 21, against d5097de. That is two rebases and several hundred commits ago, so whatever it caught is not informative about this head. The links are all internal, so I still cannot see which stage failed.

@bo-nv @JunyiXu-nv @schetlur-nv could one of you kick a fresh run on c4842236, and if it fails paste the stage or test name? Happy to turn a fix around quickly, I just have no way to see the failure from outside.

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>

# Conflicts:
#	tensorrt_llm/_torch/distributed/ops.py
@pjdurden

Copy link
Copy Markdown
Author

merged current main in as ee7bb0cb (was 532 behind and conflicting). only conflict was the import block in ops.py: main moved HelixCpMnnvlMemory/MnnvlMemory to _torch/distributed/mnnvl_memory, so I took that and kept the IpcMemory import. get_allreduce_workspace still has only the two call sites in ops.py, both unpack the tuple.

@JunyiXu-nv JunyiXu-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: after a rebase the tuple return breaks main's new get_allreduce_workspace caller, the MNNVL guard test is broken, and the fallback catches every CUDA IPC error.

Comment thread tensorrt_llm/_torch/distributed/ops.py Outdated


def get_allreduce_workspace(mapping: Mapping) -> torch.LongTensor:
def get_allreduce_workspace(mapping: Mapping) -> Tuple[torch.LongTensor, bool]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main gained another caller after your Oct 16 merge: allreduce_group_args() in tensorrt_llm/_torch/_experimental/modeling_v2/catalog/comm/allreduce.py:53 (#20267) passes get_allreduce_workspace(mapping) straight through as workspace. With the tuple return it hands (tensor, False) to trtllm::allreduce, which rejects it ("Expected a value of type 'Optional[Tensor]' for argument 'workspace' but instead found type 'tuple'"), and the rebase will not show a conflict in that file. Please update that caller here too (unpack, and use NCCL with no workspace when ipc_failed is set), or keep this function returning the tensor and expose ipc_failed through a separate helper.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kept get_allreduce_workspace returning the tensor, so main's callers are untouched. the flag is read through a separate allreduce_workspace_ipc_failed(mapping) now. also updated allreduce_group_args to switch to NCCL with no workspace when it is set, same as the AllReduce module, plus a test for it. merged main in, 946979a

Comment thread tensorrt_llm/_torch/distributed/ops.py Outdated
# P2P was reported but the IPC handles could not be exchanged those
# pointers are null, so NCCL is the only strategy left. The user
# already got a warning from open_ipc_memory at that point.
if ipc_failed:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ipc_failed is set for any non-success code from cudaIpcGetMemHandle / cudaIpcOpenMemHandle (_ipc_utils.py:186 and :209), not only the capability gap from #16899. So a bug like the broken handle round-trip #19184 fixed (cudaErrorInvalidValue / cudaErrorInvalidResourceHandle) would switch every custom all-reduce on NVLink nodes to NCCL with a single warning line, and numerics tests still pass, so CI would not catch it. Could the fallback be limited to the capability errors (cudaErrorInvalidDevice, cudaErrorNotSupported, ...) and raise otherwise? Allgathering the error code instead of None/bool would let every rank take the same raise-or-fallback decision.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

narrowed it. every rank allgathers its CUDA error code instead of None/bool, and the group only falls back when all failures are cudaErrorInvalidDevice, cudaErrorNotSupported or cudaErrorPeerAccessUnsupported. anything else (InvalidValue, InvalidResourceHandle, ...) releases the buffers and raises on every rank, naming the rank and error. tests cover local, peer and mixed cases. 946979a


class FakeMNNVLAllReduce:
@staticmethod
def is_mnnvl(mapping, dtype):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MNNVLAllReduce.is_mnnvl takes an explicitly_requested keyword since #18231, and AllReduce.__init__ passes it (ops.py:1163), so this fake now raises TypeError: FakeMNNVLAllReduce.is_mnnvl() got an unexpected keyword argument 'explicitly_requested'. test_mnnvl_kept_without_p2p therefore fails wherever the trtllm op is registered and is skipped elsewhere, so the MNNVL regression guard does not guard anything. Adding explicitly_requested=False to the fake fixes it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, the fake takes explicitly_requested=False now. the other ops tests patch get_allreduce_workspace / allreduce_workspace_ipc_failed separately to match the new API. 946979a

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>

# Conflicts:
#	tensorrt_llm/_torch/distributed/ops.py
…ty errors

get_allreduce_workspace returns the tensor again, so callers that pass it
straight to the op (allreduce_group_args in modeling_v2) keep working. The
IPC failure flag is read through allreduce_workspace_ipc_failed(), and
allreduce_group_args now switches to NCCL with no workspace when it is set,
like the AllReduce module.

open_ipc_memory allgathers the CUDA error code of each rank instead of a
None/bool, and only falls back to NCCL when every failure is a capability
error (cudaErrorInvalidDevice, cudaErrorNotSupported,
cudaErrorPeerAccessUnsupported). Any other error, a broken handle for
instance, releases the buffers and raises on every rank.

The MNNVL guard test fake takes the explicitly_requested keyword that
AllReduce passes to is_mnnvl, so the test runs again instead of raising a
TypeError.

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>

@JunyiXu-nv JunyiXu-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, all three of my comments are addressed. LGTM.

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>

# Conflicts:
#	tensorrt_llm/_torch/distributed/ops.py
@pjdurden

pjdurden commented Nov 6, 2026

Copy link
Copy Markdown
Author

merged current main in (e7813ca). only conflict was get_allreduce_workspace in ops.py: main replaced the _thread_local cache with the process-wide _allreduce_workspaces and a per-mapping lock, so ipc_failed is now the third field of that tuple and allreduce_workspace_ipc_failed reads it from there, outside the lock. fallback behaviour is the same. I could not run the tests here since this machine has no torch or GPU.

Comment thread tensorrt_llm/_torch/distributed/ops.py Outdated
Comment on lines +213 to +216
if allreduce_workspace_ipc_failed(mapping):
raise RuntimeError(
f"{op_name} requires CUDA IPC, which is unavailable on this system, "
"and it has no NCCL fallback.")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MoEAllReduce is built whenever TP>1 (Qwen3-MoE, DeepSeek-V3/V4, EXAONE-MoE; GLM and Llama4 always), but only called on the TRTLLM-nvfp4 fused path. So a BF16 Qwen3-MoE TP run on the #16899 GPUs still dies here at init, though the kernel never runs. Could we raise in forward, or fold allreduce_workspace_ipc_failed into the models' is_p2p_supported? MiniMax-M2's MiniMaxAllReduceRMS too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right, fixed in e66a68c. MoEAllReduce and MiniMaxAllReduceRMS now build fine and raise from forward / forward_qk instead, and I did both of your options: Qwen3-MoE, DeepSeek-V3/V4, EXAONE-MoE, GLM, the sm_107 GLM _model.py and MiniMaxRMSNorm fold allreduce_workspace_ipc_failed into is_p2p_supported, so they take their existing non-fused path, including the NCCL path in MiniMax-M2. Llama4 only calls moe_allreduce under cutlass_min_latency_mode, and MiniMax-M3 has no is_p2p_supported gate, so M3 still raises at its first forward on these GPUs. test_moe_allreduce_raises_in_forward, test_minimax_allreduce_rms_raises_in_forward and test_minimax_m2_norm_uses_nccl_without_ipc cover this, but I could not run them here since this machine has no torch or GPU.

Comment on lines +28 to +29
This directory runs in the CPU-only l0_cpu stage since #16498, so everything here has
to pass with zero GPUs. `test_lamport_skipped` is the exception and skips there: the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file has no pytest.mark.cpu_only, and the CPU stages run unit tests with -m cpu_only, so pytest_ignore_collect in tests/unittest/conftest.py drops it from the unittest/_torch/distributed entry. No GPU list has this dir either, so none of these tests run in CI. Could we add pytestmark = pytest.mark.cpu_only (and give test_lamport_skipped a GPU home, or drop it)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, the file has pytestmark = pytest.mark.cpu_only now, so pytest_ignore_collect keeps it in the unittest/_torch/distributed entry. test_lamport_skipped lost its skipif and runs on CPU too: it patches torch.tensor to drop the device argument, since the only thing it needed a GPU for was the flag and pointer tensors in allocate_allreduce_fusion_workspace. not run here, this machine has no torch. e66a68c

def is_mnnvl(mapping, dtype, explicitly_requested=False):
return True

def __init__(self, mapping, dtype):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AllReduce.__init__ now calls MNNVLAllReduce(mapping, dtype, max_num_tokens=..., hidden_size=...) (#20815), so this fake raises TypeError, the except Exception there swallows it and mnnvl_allreduce stays None. test_mnnvl_kept_without_p2p fails once it actually runs. Could the fake take **kwargs?

…back tests in CI

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Llama-3-70B FP16/BF16 inference fails while FP8 works on TensorRT-LLM 0.21.c-rc0 with 8x RTX 6000D

8 participants