Skip to content

feat(refit): add a ModelExpress-brokered nccl_reshard transport - #3720

Open
yixinh-nv wants to merge 11 commits into
NVIDIA-NeMo:mainfrom
yixinh-nv:yixinh/mx-collective-refit-transport
Open

feat(refit): add a ModelExpress-brokered nccl_reshard transport#3720
yixinh-nv wants to merge 11 commits into
NVIDIA-NeMo:mainfrom
yixinh-nv:yixinh/mx-collective-refit-transport

Conversation

@yixinh-nv

@yixinh-nv yixinh-nv commented Aug 19, 2026

Copy link
Copy Markdown

What does this PR do?

Adds refit_transport=mx_nccl_reshard: the existing nccl_reshard data path, with NCCL communicator bootstrap brokered by ModelExpress instead of a TCPStore.

Below communicator creation, the path is unchanged: nccl.m2n.reshard moves the sharded bulk tensors and the packed NCCL broadcast moves the miscellaneous tensors. MX coordinates admission, immutable membership, worker-generation fencing, plan agreement, per-lane UID publication, and readiness; it is not the tensor-transfer engine.

Design boundary

  • Data plane: existing nccl.m2n.reshard plus packed NCCL broadcast.
  • Control plane: expected roster, epoch, plan digest, registration lease, and lane bootstrap ID.
  • Communicator lifecycle: the MX path creates a bounded nonblocking bootstrap communicator, waits for every asynchronous setup operation, then splits it into a blocking child communicator. M2N receives that child because its current native implementation treats ncclInProgress as an error.
  • Lane ordering: rendezvous, all-ranks broadcast lane, then each PP-local reshard lane, with a cluster-wide barrier between phases. Overlapping communicators are never initialized concurrently.

Dependencies

PR Role
ModelExpress #661 Collective server: admission, TTL/liveness, epochs and report fencing
ModelExpress #665 Client: plan digest and registration lease renewal
ModelExpress #666 Collective execution/data-plane hardening

Matched H200 end-to-end results

Both runs used Nebius 8xH200 (two 4-GPU pods), Qwen3-30B-A3B BF16, Megatron TP2/PP2/EP2 to vLLM TP4, the same 240-prompt file (SHA-256 8fffc35d...be9da1), five GRPO steps, and the same 56.87 GiB refit payload: 54.00 GiB M2N bulk plus 2.87 GiB misc broadcast. The only transport selection was nccl_reshard versus mx_nccl_reshard.

Field Native, no MX (nccl_reshard) MX (mx_nccl_reshard)
First refit 6.63 s 7.52 s
Steady refits 0.62, 0.64, 0.92, 0.64 s 0.61, 0.86, 0.74, 0.64 s
Steady mean / median 0.71 / 0.64 s 0.71 / 0.69 s
Five KL errors 0.0029, 0.0032, 0.0034, 0.0025, 0.0029 0.0029, 0.0032, 0.0034, 0.0025, 0.0029
Native M2N proof All eight workers logged real nccl.m2n.reshard All eight workers logged real nccl.m2n.reshard
Workload result 5/5 GRPO steps completed 5/5 GRPO steps completed
Driver teardown Old launcher hit the known Ray finalizer exit bug after completion Exit status 0; zero live Ray actors/GPU use afterward

For the MX leg, the control plane admitted 8 workers, reached epoch 1 READY, agreed on the plan digest, and created all three lanes before the first refit.

These are enclosing prepare_for_generation/transfer_and_update_weights timings: they include Ray/actor dispatch and synchronization in addition to device work. They are not wire-bandwidth measurements. All samples are retained, including the cold first refit and the 0.92 s native outlier.

The validation used the pinned #2971 runtime base (0c595799) plus the validated MX integration hunks and the current PR's bootstrap/cleanup fixes. The run provenance records the exact source hashes, ModelExpress server 6f74f08d, and ModelExpress client/data 4f85110e.

The samples establish that MX preserves the native M2N execution and correctness path. They are too few, and the native leg predates the final Ray-cleanup fix, to claim an MX performance delta. A new native rerun at the final exact heads would make the performance comparison publication-ready.

Key fixes discovered during H200 validation

  • Poll nonblocking communicator progress before reading the bootstrap broadcast buffer; otherwise a lazily connected collective can leave a peer's verification tensor unchanged and trigger a cascading abort.
  • Promote the bounded bootstrap communicator to a blocking child before handing it to M2N, whose current native calls reject ncclInProgress.
  • Explicitly drain generation, policy, teacher, and placement-group owners before Ray teardown so late destructors cannot reinitialize a second CoreWorker during Python finalization.

Usage

uv run ./examples/run_grpo.py \
  --config <your_config>.yaml \
  policy.generation.colocated.enabled=false \
  policy.generation.refit_transport=mx_nccl_reshard \
  policy.generation.mx_server_url=<modelexpress-server>:8001

Every worker on both sides must receive the same ModelExpress server address. NCCL_COMM_ID must be unset for this path.

Validation status

Adds refit_transport=mx_nccl_reshard, which keeps the existing nccl_reshard
wire path and replaces only the communicator bootstrap.

The bulk path still ends in nccl.m2n.reshard with the same meshes and
placements, and the misc path still rides the packed broadcast, so the two
transports move identical bytes and can be compared directly.

What changes is the rendezvous. StatelessProcessGroup is a TCPStore whose only
job is to move 128 bytes of ncclUniqueId from rank 0 to everyone else, at an
address the driver allocates per PP stage. Brokering that through ModelExpress
adds admission against an expected participant set, fencing of worker
generations, and a readiness state the trainer can observe before entering the
collective -- so a missing worker becomes a bounded failure naming the slot
rather than a hang. It also drops the per-stage port allocation from the
driver.

mx_collective_plan.py is the only genuinely new logic: it expresses the
metadata build_nccl_reshard_refit_info already produces in MX's plan
vocabulary. It re-derives nothing. A mesh that is not contiguous and ascending
cannot be expressed as MX's (shape, rank_offset) and is rejected rather than
approximated, because a wrong mesh does not fail the collective, it moves the
wrong bytes.

Signed-off-by: Yixin Huang <yixinh@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Restructures the integration around what xferdtensor actually consumes from a
process group: nccl_communicator, plus broadcast for the packed misc path.
Producing an object with that surface whose ncclUniqueId came from
ModelExpress -- and dropping it where StatelessProcessGroup normally goes --
means nccl_reshard_refit runs unchanged.

That replaces the parallel refit loop the first draft added. It is less code,
and it is the difference between the claim 'only the bootstrap changes' being
approximately true and being structurally true: below the bootstrap the two
transports are now the same code path, so they cannot drift and a measured
comparison between them means something.

Everything here was found by running it on 8xH200:

- mx_collective_bootstrap.py: MxProcessGroup and the join/publish/await
  sequence. Lanes are created in ascending order because a generator joins
  every reshard lane while each lane's trainers are blocked inside their own
  bootstrap, so the two sides have to unblock in the same sequence.
- worker + actor-group methods on both sides, replacing the four that the
  first draft assumed existed.
- config.py and grpo.py accept the transport rather than routing it into the
  checkpoint-engine path.
- mx_server_url is now required instead of defaulting.

Signed-off-by: Yixin Huang <yixinh@nvidia.com>
@yixinh-nv
yixinh-nv requested review from a team as code owners August 20, 2026 02:37
Creating two NCCL communicators concurrently across overlapping rank sets
deadlocks. The nccl_reshard path already knows this -- it says so where it
separates model_update_group from the per-stage groups, and it enforces the
separation with a ray.get barrier between the two.

build_mx_groups did not. Each worker created every lane it belonged to in a
single loop, so a rank that finished one lane started the next while other
ranks were still inside the previous one. With a 2-stage pipeline that is a
6-rank reshard lane under construction at the same time as the 8-rank
broadcast lane, sharing ranks: the collective never completes and there is no
error, only silence.

Bootstrap is now driven in phases from the synchronizer -- rendezvous, then
one lane at a time in a fixed cluster-wide order, then finish -- with a
barrier between each. A worker that does not belong to a lane returns
immediately and waits at the barrier rather than racing ahead into its own
next lane. Broadcast goes first because every rank is in it, which makes the
barrier after it a full-cluster sync point.

No change to what is built, only to when: the same lanes, the same ranks, the
same identifiers. build_mx_groups survives as a single-process wrapper for
tests, where nothing else is creating communicators concurrently.

Signed-off-by: Yixin Huang <yixinh@nvidia.com>
ncclGetUniqueId opens the bootstrap listening socket as a side effect, so the
identifier is not inert data -- it names a socket that has to still be
accepting when the lane's peers dial in.

Minting every lane's identifier at rendezvous meant that socket had to survive
an actor-method return, a driver round-trip and a barrier before its lane was
built. When it did not, the peers got ECONNREFUSED from an address that looks
entirely correct, and NCCL reported it as "remote process exited or there was
a network error" -- which reads like a dead peer or a bad fabric, not a stale
listener.

Each lane now mints and publishes its identifier inside the phase that creates
the communicator, so minting and use sit microseconds apart in the same call,
the way the TCPStore path has always had it. Waiting is per-lane rather than
for whole-group READY, because with mint-at-use the later lanes are not
published yet and waiting for READY here would deadlock the ordering it is
meant to protect.

Client-side only: PublishGroupBootstrap and GetCollectiveGroup already carry
everything this needs, so the server and the proto are untouched.

Signed-off-by: Yixin Huang <yixinh@nvidia.com>
Signed-off-by: Yixin Huang <yixinh@nvidia.com>
Signed-off-by: Yixin Huang <yixinh@nvidia.com>
Signed-off-by: Yixin Huang <yixinh@nvidia.com>
Signed-off-by: Yixin Huang <yixinh@nvidia.com>
Signed-off-by: Yixin Huang <yixinh@nvidia.com>
Signed-off-by: Yixin Huang <yixinh@nvidia.com>
Signed-off-by: Yixin Huang <yixinh@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant