Skip to content

[TRTLLM-14715][feat] preserve native MoE A2A graph VAs across restore - #16632

Open
hhzhang16 wants to merge 3 commits into
NVIDIA:mainfrom
hhzhang16:hannahz/dep-1082-port-flashinfer-stable-va-lifecycle-for-native-moe-all-to
Open

[TRTLLM-14715][feat] preserve native MoE A2A graph VAs across restore#16632
hhzhang16 wants to merge 3 commits into
NVIDIA:mainfrom
hhzhang16:hannahz/dep-1082-port-flashinfer-stable-va-lifecycle-for-native-moe-all-to

Conversation

@hhzhang16

@hhzhang16 hhzhang16 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added checkpoint preparation and restoration for MNNVL-backed MoE communication workflows.
    • Preserved memory layouts while safely detaching and remapping workspace handles during checkpoints.
    • Added validation to prevent communication operations when required workspace mappings are unavailable.
  • Bug Fixes

    • Improved cleanup of partially created or detached memory allocations.
    • Added safeguards against restoring checkpoints with incompatible communicator layouts.
  • Tests

    • Added coverage for memory lifecycle, checkpointing, mapping failures, and invalid communication states.

Description

This change ports the FlashInfer stable-VA lifecycle to TensorRT-LLM's native MoE all-to-all resources. FlashInfer implemented this lifecycle for its TRT-LLM-style MoE all-to-all workspace in flashinfer-ai/flashinfer#3727.

Test Coverage

PR Checklist

Please review the following before submitting your PR:

  • PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.

  • PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.

  • Test cases are provided for new code paths (see test instructions)

  • If PR introduces API changes, an appropriate PR label is added - either api-compatible or api-breaking. For api-breaking, include BREAKING in the PR title.

  • Any new dependencies have been scanned for license and vulnerabilities

  • CODEOWNERS updated if ownership changes

  • Documentation updated as needed

  • Update tava architecture diagram if there is a significant design change in PR.

  • The reviewers assigned automatically/manually are appropriate for the PR.

  • Please check this after reviewing the above items as appropriate for this PR.

GitHub Bot Help

To see a list of available CI bot commands, please comment /bot help.

Signed-off-by: Hannah Zhang <hannahz@nvidia.com>
Signed-off-by: Hannah Zhang <hannahz@nvidia.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

MNNVL memory now tracks mapped allocation records, separates virtual-address reservations from backing handles, and supports checkpoint detach/restore. MoE communication paths expose checkpoint lifecycle methods, enforce mapped workspaces, refresh metadata, and reset dispatch state.

Changes

MNNVL checkpoint lifecycle

Layer / File(s) Summary
Allocation records and handle mapping
tensorrt_llm/_mnnvl_utils.py, tests/unittest/_torch/test_mnnvl_memory_lifecycle.py
Structured allocation records and centralized handle mapping support FABRIC and non-FABRIC paths with cleanup on partial failures.
Memory teardown and checkpoint state
tensorrt_llm/_mnnvl_utils.py, tests/unittest/_torch/test_mnnvl_memory_lifecycle.py
Teardown conditionally releases mapped handles, while checkpointing preserves VA reservations and restores fresh mappings for both MoE workspaces.
Communication checkpoint APIs
tensorrt_llm/_torch/distributed/moe_alltoall.py, tensorrt_llm/_torch/modules/fused_moe/communication/*
MoE All-to-All and NVLink classes add checkpoint preparation and restoration, metadata refresh, synchronization, and dispatch-state reset behavior.
Mapped-state enforcement and dispatch cleanup
tensorrt_llm/_torch/distributed/moe_alltoall.py, tensorrt_llm/_torch/modules/fused_moe/communication/*, tests/unittest/_torch/test_mnnvl_memory_lifecycle.py
Communication entry points reject unmapped workspaces, and two-sided combine clears retained dispatch state.

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

Sequence Diagram(s)

sequenceDiagram
  participant MoeAlltoAll
  participant MnnvlMemory
  participant NVLinkCommunication
  participant Communicator
  MoeAlltoAll->>MnnvlMemory: checkpoint_prepare()
  MnnvlMemory-->>NVLinkCommunication: backing handles detached
  MoeAlltoAll->>MnnvlMemory: checkpoint_restore(comm)
  MnnvlMemory-->>NVLinkCommunication: fresh handles remapped
  NVLinkCommunication->>Communicator: synchronize and barrier
  NVLinkCommunication-->>MoeAlltoAll: communication state reset
Loading

Suggested reviewers: rosong11, sunnyqgg

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description has a clear summary and checklist, but the required Test Coverage section is empty. Add the relevant tests or test files under Test Coverage and briefly explain how they cover the new checkpoint lifecycle.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title matches the main change and clearly states the preserved native MoE A2A VA restore behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@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.

🧹 Nitpick comments (1)
tensorrt_llm/_torch/modules/fused_moe/communication/nvlink_one_sided.py (1)

703-722: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider mirroring the _require_mapped() guard here for cross-implementation consistency.

MoeAlltoAll.get_combine_payload_tensor_in_workspace() calls self._require_mapped() before the phase check, but this NVLink one-sided variant does not. In practice the phase != "dispatched" guard already blocks reaching this while handles are detached (checkpointing requires the idle phase), so this is a defense-in-depth consistency nit rather than a live bug. Aligning both keeps the two frontends symmetric if the phase semantics ever change.

♻️ Optional consistency tweak
     def get_combine_payload_tensor_in_workspace(
         self, runtime_max_tokens_per_rank: int, hidden_size: int, dtype: torch.dtype
     ) -> torch.Tensor:
         ...
+        self._require_mapped()
         if self._dispatch_state.get("phase") != "dispatched":
             raise RuntimeError(
                 "get_combine_payload_tensor_in_workspace called before a successful dispatch"
             )
🤖 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/modules/fused_moe/communication/nvlink_one_sided.py`
around lines 703 - 722, Update get_combine_payload_tensor_in_workspace to call
the existing _require_mapped() guard before checking the dispatch phase,
matching MoeAlltoAll’s implementation while preserving the current phase
validation and behavior.
🤖 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.

Nitpick comments:
In `@tensorrt_llm/_torch/modules/fused_moe/communication/nvlink_one_sided.py`:
- Around line 703-722: Update get_combine_payload_tensor_in_workspace to call
the existing _require_mapped() guard before checking the dispatch phase,
matching MoeAlltoAll’s implementation while preserving the current phase
validation and behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: bc5812ec-a3d9-4c6c-9695-5f1fa61aa472

📥 Commits

Reviewing files that changed from the base of the PR and between 445742c and 297d098.

📒 Files selected for processing (5)
  • tensorrt_llm/_mnnvl_utils.py
  • tensorrt_llm/_torch/distributed/moe_alltoall.py
  • tensorrt_llm/_torch/modules/fused_moe/communication/nvlink_one_sided.py
  • tensorrt_llm/_torch/modules/fused_moe/communication/nvlink_two_sided.py
  • tests/unittest/_torch/test_mnnvl_memory_lifecycle.py

@chienchunhung chienchunhung 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 for working on this. I left two lifecycle questions inline.

return
torch.cuda.synchronize()
record.comm.barrier()
cls._unmap_and_release_handles(record)

@chienchunhung chienchunhung Jul 23, 2026

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.

Could we make this lifecycle transition fail closed?

If _unmap_and_release_handles() raises after releasing only some ranks, record.mapped remains True, so a later _require_mapped() can treat a partially unmapped workspace as usable.

The inverse can happen during restore: the allocation is marked mapped before the frontend has reinitialized its metadata and completed collective readiness.

Would it make sense to track an explicit transitioning or broken state, reject data-path access while in that state, and only mark the workspace ready after initialization and the final barrier have completed?

A focused failure-injection test for an error partway through unmap or restore would also help protect this contract.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added explicit allocation states in _mnnvl_utils.py in this MR hhzhang16/TensorRT-LLM#1, with data-path access allowed only in MAPPED. The shared lifecycle publishes a restored allocation only after frontend initialization, synchronization, and protocol reset, and marks failed transitions BROKEN. Included failure-injection coverage in the lifecycle tests.

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 for putting this together. I checked the current PR head (db678080), and I don’t see the allocation-state changes from the follow-up MR yet—the only update after 297d098b adds _require_mapped() to the one-sided payload accessor. Could you please bring the transitioning/BROKEN state and failure-injection tests into this PR so we can review the fail-closed behavior against the code that will merge?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Those changes are intentionally isolated in hhzhang16#1 so the shared lifecycle and failure-handling changes can be reviewed there first. Then I’ll merge them into #16632.

The local phase and watchdog checks are defense-in-depth only; they do
not prove that every wrapper sharing this workspace is quiescent.
"""
if self._dispatch_state.get("phase") != "idle":

@chienchunhung chienchunhung Jul 23, 2026

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.

Could we move this preflight to the shared workspace owner? The phase and watchdog checks here cover only this instance, but mnnvl_mem is shared by every wrapper using the same workspace. An idle instance could therefore detach the handles while a sibling is still dispatched or has a watchdog using the workspace. The docstring correctly calls out the need for global quiescence, but I could not find a Communication, ConfigurableMoE, or executor-level path that establishes it before this call. Could we register and preflight all shared owners before the first unmap, then expose the transition through the lifecycle owner?

PS: The shared instances pattern in PR #15895 may be reusable here. A two-owner test where one remains active would help verify that no detach occurs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In MR hhzhang16/TensorRT-LLM#1: added _MnnvlAlltoAllWorkspaceLifecycle as the allocation-scoped owner. Both frontends register with it, and it checks every registered owner plus rank-wide readiness before the first unmap. The MR also includes two-owner coverage where one owner remains active.

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, that approach sounds aligned with what I had in mind. I checked db678080, though, and _MnnvlAlltoAllWorkspaceLifecycle, the shared-owner preflight, and the two-owner negative test are not in this PR yet. Could you please bring those changes over? As the current code stands, an idle instance can still unmap a workspace while another owner is active.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Those changes are intentionally isolated in hhzhang16#1 so the shared lifecycle and failure-handling changes can be reviewed there first. Then I’ll merge them into #16632.

if self._state.phase != "idle":
raise RuntimeError(
"Cannot checkpoint during an active MoE All-to-All phase")
if self._alltoall_watchdog is not 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.

should we fail if there is a watchdog? why not just tear it down during checkpoint_prepare?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call, I changed it in this MR: hhzhang16#1 so that the shared lifecycle now owns the workspace watchdog. It suspends the watchdog after readiness preflight and before unmapping, then recreates it with the same configuration after metadata refresh and collective restore readiness.

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—that shared ownership model makes sense. The current PR head still rejects checkpointing when a watchdog exists and does not suspend or recreate it. Could you please bring the lifecycle-owned watchdog transition into this PR as well, including preservation of its configuration across restore?

cls.current_rank_stride,
cls.current_mem_offset,
)
# all_handles_data like b'\x00\x00\x00 \x00\x00\x00\x00\x8f\xec\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x1d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # noqa: E501

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.

not sure what this comment is doing here. I know this was in the FI PR but do we need this here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤷

Signed-off-by: Hannah Zhang <hannahz@nvidia.com>
@Funatiq Funatiq changed the title [None][feat] preserve native MoE A2A graph VAs across restore [TRTLLM-14715][feat] preserve native MoE A2A graph VAs across restore Jul 28, 2026
@chienchunhung
chienchunhung self-requested a review July 31, 2026 00:51
cls._unmap_and_release_handles(record)
record.mem_handles = [None] * record.comm_size
record.mapped = False
record.comm.barrier()

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.

Could we make the failure handling collective and bounded here?

If cuMemUnmap() fails on one rank, that rank exits before this barrier while ranks that succeeded can block here indefinitely. Restore has a similar risk if one rank fails before or during _create_and_map_handles(): peers may remain in an allgather() or the frontend barrier after some mappings are already live.

Would it make sense for the shared lifecycle owner to aggregate errors through PREPARE/COMMIT/ABORT, with a multi-rank failure-injection test after one rank has unmapped or remapped?

del obj.ptr


def test_checkpoint_prepare_preserves_va_and_is_idempotent(memory):

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.

Could we add a real multi-rank MNNVL regression test for the behavior this PR introduces?

The current tests mock all CUDA calls and use _FakeComm, so they cannot catch unmatched collectives, incorrect handle ownership, changed pointers, or unsafe CUDA graph replay. It would be helpful to exercise both one- and two-sided paths through repeated prepare/restore cycles on supported hardware, verify stable VAs/pointers and outputs, replay a captured graph, and confirm that mapped memory returns to baseline.

return
torch.cuda.synchronize()
record.comm.barrier()
cls._unmap_and_release_handles(record)

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 for putting this together. I checked the current PR head (db678080), and I don’t see the allocation-state changes from the follow-up MR yet—the only update after 297d098b adds _require_mapped() to the one-sided payload accessor. Could you please bring the transitioning/BROKEN state and failure-injection tests into this PR so we can review the fail-closed behavior against the code that will merge?

The local phase and watchdog checks are defense-in-depth only; they do
not prove that every wrapper sharing this workspace is quiescent.
"""
if self._dispatch_state.get("phase") != "idle":

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, that approach sounds aligned with what I had in mind. I checked db678080, though, and _MnnvlAlltoAllWorkspaceLifecycle, the shared-owner preflight, and the two-owner negative test are not in this PR yet. Could you please bring those changes over? As the current code stands, an idle instance can still unmap a workspace while another owner is active.

if self._state.phase != "idle":
raise RuntimeError(
"Cannot checkpoint during an active MoE All-to-All phase")
if self._alltoall_watchdog is not 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.

Thanks—that shared ownership model makes sense. The current PR head still rejects checkpointing when a watchdog exists and does not suspend or recreate it. Could you please bring the lifecycle-owned watchdog transition into this PR as well, including preservation of its configuration across restore?

@hhzhang16

Copy link
Copy Markdown
Contributor Author

@chienchunhung A lot of your requests are intentionally isolated in hhzhang16#1 so the shared lifecycle and failure-handling changes can be reviewed there first. Then I’ll merge them into #16632.

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.

3 participants