Skip to content

feat(megatron-mx): Megatron-Core MX path — Phases A+C+D#2

Merged
jthomson04 merged 12 commits into
jthomson04:dynamo-k8s-integrationfrom
KavinKrishnan:kavink/megatron-mx
Jun 12, 2026
Merged

feat(megatron-mx): Megatron-Core MX path — Phases A+C+D#2
jthomson04 merged 12 commits into
jthomson04:dynamo-k8s-integrationfrom
KavinKrishnan:kavink/megatron-mx

Conversation

@KavinKrishnan

Copy link
Copy Markdown

Summary

Adds the Megatron-Core path for NemoRL's update_weights_via_mx weight-refit, layered on top of dynamo-k8s-integration-mx. Companion to ai-dynamo/modelexpress#421 (which carries the MX-side slice planner + receiver translator + vendored Bridge helpers).

The architecture is receiver-driven multi-source slicing: each Megatron TP/PP/EP rank publishes its native shard with no allgather; the inference receiver discovers a per-tensor slice plan via the MX catalog, pulls slices in parallel from the owning ranks, and reassembles locally using vendored Megatron-Bridge-style transforms (QKV un-interleave, gated-MLP split, column/row-parallel concat, per-expert split). It generalises the FSDP+EP planner from ai-dynamo/modelexpress#349's Phase 4 to cover Megatron's column-parallel, row-parallel, and head-interleaved fused-QKV layouts.

Full design: temp/NemoRL_Megatron_MX_Design.md (in the kavink/RL doc tree on the MX repo).

Commits

# Commit What
1 69027ad30 Phase A: MegatronPolicyWorker.stream_weights_via_mx + detect_megatron_role classifier in new nemo_rl/distributed/mx_megatron_helpers.py
2 ac014c8b2 Refactor classifier to consult Bridge's AutoMapping._MODULE_TYPE_REGISTRY (catches every TE/Inference/Quant variant)
3 dd13425ce Trainer publishes MegatronTransformerConfig + Megatron→HF name map in v2 sidecar via Bridge introspection (no weight transfer)
4 648855bdd Phase D: receiver-side _update_weights_via_mx_megatron on VllmInternalWorkerExtension with first-cycle Megatron-mode latching
5 790e0646b Phase D pre-pull fast path: bulk receive_weights + pre_assembled_buffers for matched-TP

Path validated this session

  • ✅ Bridge byte-identity smoke check on the live trainer image (nemorl-mx-trainer-gpu-workers-worker-zzjbd in kavin namespace, /opt/ray_venvs/.../MegatronPolicyWorker/bin/python). All 7 architectural assumptions confirmed: mpu.is_initialized()=False fallback works, helpers byte-identity round-trip, AutoMapping registry accessible, AutoBridge importable.
  • ✅ Vendored helpers cross-checked against Bridge upstream byte-identical for 5 attention shapes (MHA + GQA at Llama-3-8B + Llama-3-70B sizes) on the trainer image.
  • ✅ Full Phase B+C+D chain end-to-end CPU test (synthetic publishes, GQA un-interleave, Llama-3.1-8B-shape attention assembled across TP=2 trainer ranks, byte-identical roundtrip).
  • ✅ 54/54 v2 + Megatron tests green on the MX side.
  • ⏳ Cluster end-to-end validation (image bake + GRPO Llama-3.1-8B run) is the next step; not in this PR.

What's NOT in this PR (deferred)

  • Mixed-TP (target_tp != source_tp) NIXL plumbing. The Phase B planner already returns the right MegatronSlicePlan with source_subslice set; the gap is just the per-source MxRefitReceiver.pull_to(src, dest, source_subslice=...) primitive and a refactor of the receiver's pre-allocate-buffers step. The matched-TP code path prints an explicit [mx-megatron] ... v1 mixed-TP NIXL plumbing not yet wired message when no source's tp_rank matches the receiver's, and falls through. ~1 day of focused work + a Megatron model in the test loop.
  • attention_output_gate=True, FP8 blockwise scale tensors, KV-only fused linear, Mamba/GDN layouts. Vendored helpers explicitly raise NotImplementedError for FP8; other variants would need a vendoring extension.
  • Wiring into Dynamo's MxRefitWorkerExtension (a separate Dynamo repo edit, ~5-line shim that imports run_refit_cycle).

Test plan

  • python -m pytest tests/test_v2_megatron_slice_planner.py tests/test_megatron_helpers.py tests/test_megatron_translator.py — 36 tests, all pass (in the MX repo)
  • Bridge byte-identity cross-check on the trainer image
  • Cluster Phase E shape 1: Llama-3.1-8B Megatron TP=2 → vLLM TP=2 GRPO smoke (acceptance: logits within bf16 tolerance vs NCCL-broadcast control; trainer no full-tensor allocation; ≥20 clean refit cycles)
  • Cluster Phase E shape 2: Qwen3-MoE-30B-A3B mixed-TP/EP (gated by mixed-TP NIXL plumbing v1)

References

@KavinKrishnan

Copy link
Copy Markdown
Author

Cluster validation — 2026-06-08

Cross-pod end-to-end smoke validated on the GB200 kavin cluster. All 8 HF-shaped output tensors byte-identical to trainer ground truth across qkv_column / gated_mlp_column / column / row / replicated roles. See the full writeup at pensieve/RL/NemoRL/13_megatron_mx_validation_2026_06_08.md (in ai-dynamo/modelexpress@kavink/RL).

This run validates the publisher (Phase A) + Bridge-derived sidecar emit (_build_megatron_sidecar) end-to-end against the receiver-side translator path; the VllmInternalWorkerExtension._update_weights_via_mx_megatron dispatch is wired but not directly invoked by the smoke harness (the harness bypasses vLLM startup). Next milestone: Phase E shape 1 — Llama-3.1-8B Megatron TP=2 → vLLM TP=2 via real GRPO smoke.

@KavinKrishnan

Copy link
Copy Markdown
Author

Phase E shape 1 validation on GB200 (kavin namespace) — real Megatron model byte-identity confirmed.

Loaded Qwen/Qwen3-4B-Thinking-2507 as a real Megatron-Core model via Megatron-Bridge (TP=PP=EP=1, bf16, ~4B params), ran the full Phase A → B → C → D pipeline across two pods, and asserted byte-identity against bridge.export_hf_weights ground truth.

Result: 398/398 HF tensors byte-identical, 0 drift, 0 missing, 0 extras.

Metric Value
Model Qwen3-4B-Thinking (32 q-heads, GQA 4:1, 36 layers)
Tensors published 290
HF tensors expected 398
HF tensors byte-identical 398 / 398
Bulk RDMA pull 8.04 GB, 0.21 s, 313 Gbps single-NIC
Translation (run_refit_cycle) 398 HF tensors in 0.57 s

What this validates beyond the earlier synthetic-shape smoke:

  • Phase A detect_megatron_role against real TE-fused classes (TELayerNormColumnParallelLinear, TERowParallelLinear, VocabParallelEmbedding, RMSNorm) — all routed correctly via Bridge's AutoMapping._MODULE_TYPE_REGISTRY.
  • Phase A collect_megatron_publish_set walking a real 290-parameter graph.
  • Phase A _build_megatron_sidecar against real Bridge get_conversion_tasks (290 name_map entries, QKVMapping dict + DirectMapping string both handled).
  • Phase B per-tensor megatron_role round-trips through TensorDescriptorV2.megatron_role and the 290-entry sidecar.
  • Phase C split_qkv_weights on real GQA 4:1 attention (32 q-heads × 8 kv-heads × head_dim=128); split_gated_mlp on real Qwen3 fused gate+up (intermediate=9728).
  • Bridge byte-identity (the vendored helpers in modelexpress.megatron_helpers round-trip cleanly against the upstream Bridge implementation in the trainer image).

Two small bugs surfaced + fixed during this run:

  1. Bridge API name: build_conversion_tasksget_conversion_tasks (this PR / the matching NemoRL commit).
  2. model.named_parameters() returns module.<...> prefixed names but Bridge's get_conversion_tasks returns unprefixed; the publisher now normalizes in collect_megatron_publish_set, and the receiver wire-up has a defensive strip too.

Full writeup + harness scripts: pensieve/RL/NemoRL/14_megatron_mx_phase_e_shape1_2026_06_08.md.

Not yet validated (Phase E shape 2): mixed-TP (target_tp ≠ source_tp), MoE per-expert routing, real GRPO loop. The mixed-TP gap is just the v1 MxRefitReceiver.pull_to(..., source_subslice=...) NIXL primitive — Phase B's planner already returns the right plans (proven in test_column_mixed_tp_target_wider/narrower).

@KavinKrishnan

Copy link
Copy Markdown
Author

Phase E shape 2 progress update — partial validation on Qwen3-MoE-30B-A3B-Instruct-2507 (kavin GB200, 60 GB, 128 experts, 48 layers).

What's now validated on the real MoE model:

  • Phase A grouped-MoE per-expert classifier on Qwen3-MoE-30B-A3B: all 12 288 expert tensors correctly classified (expert_column 6144 for linear_fc1.weight<N>, expert_row 6144 for linear_fc2.weight<N>) across 48 layers × 128 experts.
  • Trainer-side end-to-end publish: 12 627 Megatron-shaped tensors registered + published in ~5 s, source_id stable, sidecar with full Megatron→HF name map (12 627 entries) round-trips.

Two real-model bugs surfaced + fixed during this run (commit 89e2916d5):

  1. _enclosing_module previously only recognised weight/bias/scale/_extra_state as parameter leaves. TE-grouped MoE classes (TEColumnParallelGroupedLinear, TERowParallelGroupedLinear) expose per-expert parameters as weight0, weight1, ..., weight127; the old logic walked past them into the Parameter object → unknown class → fell to replicated. Extended the leaf detector to match weight<digits> / bias<digits> / scale<digits>.
  2. detect_megatron_role per-expert branch previously required ep_size > 1 (designed for the legacy leading-axis-grouped layout). TE-grouped exposes per-expert parameters even at EP=1; added a 2a branch that extracts the expert id from the trailing digit and emits is_expert=True, expert_axis=0, owned_expert_ids={N}, descriptor_extras={"expert_layout": "grouped", ...} regardless of EP world size.

Pre-fix vs post-fix role distribution on real Qwen3-30B-A3B:

pre-fix:    replicated 12 529   (every expert wrongly replicated)
            row 48, qkv_column 48, vocab_parallel 1, column 1

post-fix:   expert_column 6 144  (= 48 x 128 of linear_fc1)
            expert_row    6 144  (= 48 x 128 of linear_fc2)
            replicated      241  (norms, scalars, routers)
            row              48, qkv_column 48, vocab_parallel 1, column 1
                         ------
                         12 627   matches Bridge get_conversion_tasks count

Also landed (cluster validation pending a 2-GPU trainer pod):

Mixed-TP receiver path in _update_weights_via_mx_megatron. Replaces the previous matched-TP-only gate: when source_tp ≠ target_tp, pulls each Megatron source's full manifest into scratch via receive_weights_scratch, then satisfies per-source slice requests by copying from scratch into the planner's pre-narrowed destination view, honouring MegatronSliceSource.source_subslice. Bandwidth is O(sum(source_bytes)) per receiver — minimal for target-wider; over-pulls 2× for target-narrower. A v1 NIXL sliced-pull primitive (offset+size descriptor on the source side) is the follow-up that cuts narrow-target cost. Commit 64195fabb.

What still needs to happen for cluster-validated shape 2:

  1. Receiver-side per-expert grouped translator extension. Phase B's planner currently picks assembly="per_expert" for expert_* roles, which is the legacy EP>1 leading-axis layout. The grouped layout (one Megatron param = one expert's tensor) needs a passthrough plan choice + a len(hf_names) == 2 gate+up split inside the per-expert path. Translator-side logic only — no wire-protocol change.
  2. 2-GPU trainer pod (nvidia.com/gpu: 2 in 01-trainer-raycluster.yaml) for Megatron TP=2 → vLLM TP=4 mixed-TP cluster exercise.
  3. (Optional) v1 NIXL sliced-pull primitive for the bandwidth optimization on target-narrower mixed-TP.

Full writeup: pensieve/RL/NemoRL/15_megatron_mx_phase_e_shape2_2026_06_08.md. Harness: pensieve/RL/NemoRL/megatron_mx_phase_e_shape2_2026_06_08/smoke_qwen3_moe_publisher.py.

…lassifier

Phase A of the Megatron-Core MX integration plan
(temp/NemoRL_Megatron_MX_Design.md §4). Adds the trainer-side publisher
that emits per-rank Megatron-native shards to ModelExpress, with each
parameter classified into one of seven Megatron roles (qkv_column,
gated_mlp_column, column, row, vocab_parallel, replicated,
expert_column / expert_row). Receiver-side slice planner is in
ai-dynamo/modelexpress PR NVIDIA-NeMo#421 commits 12c73a7 + b26e80f.

What's new
    * nemo_rl/distributed/mx_megatron_helpers.py
        - detect_megatron_role(name, param, model, ...) — classifies
          parameters by walking to the enclosing module and matching
          against Megatron-Core's parallel-layer class names
          (ColumnParallelLinear, RowParallelLinear,
          VocabParallelEmbedding, plus heuristic detection of fused
          QKV / fused gated MLP via name patterns).
        - collect_megatron_publish_set(model, tp_size=..., pp_rank=...,
          ...) — generator that yields (name, local_shard, role_spec,
          full_extras) tuples ready for MxV2TrainingPublisher.add_tensor.
          Skips replicated tensors on tp_rank > 0 (rank 0 publishes for
          the replicated set).
        - Role overrides via MxConfig.megatron_role_overrides for
          non-mainline Megatron forks with different class / name
          conventions.
        - Expert pattern via NRL_MX_EXPERT_TENSOR_PATTERN env var,
          mirroring detect_moe_expert_layout's convention.

    * MegatronPolicyWorkerImpl.stream_weights_via_mx
        - Lazy-init the v2 publisher (once per worker lifetime).
        - Resolve TP / PP / EP rank position via parallel_state.
        - Pull num_attention_heads / num_query_groups / kv_channels
          from the trainer's transformer_config; descriptor extras for
          qkv_column become receiver-checkable.
        - Stamp the publisher's mesh position before publish() so the
          source identity / sidecar carry tp_rank / pp_rank / ep_rank.
        - For each parameter: classify role, emit local shard with
          per-tensor megatron_role + megatron_extras in the registry
          via add_tensor's new kwargs.
        - publish(version=N) + mark_ready() — same shape as the
          DTensor stream_weights_via_mx.

Memory invariant
    Trainer never materialises the full tensor. Megatron-Core stores
    native shards; the publisher passes them through (tensor.detach()
    + dtype cast + .contiguous()). Peak GPU memory for any layer
    stays at model_weights / (tp * pp * ep).

Out of scope (this commit)
    * Phase C: receiver translator (_translate_megatron_to_hf,
      _uninterleave_qkv) — lives on the inference side, separate
      file + PR.
    * Phase D: GRPO refit branch + receiver auto-detect.
    * FP8 KV-cache scales (existing NotImplementedError matches the
      DTensor path's behaviour).

Tested by
    * Pre-existing unit suite continues to pass (no functional changes
      to existing paths).
    * MX-side planner: 36/36 v2 tests passing in
      ai-dynamo/modelexpress @ kavink/fix-publish-self-as-source-v2-transports.
    * End-to-end validation pending — needs Phase C + D + new image
      + GB200 deploy. Tracked in
      temp/NemoRL_Megatron_MX_Design.md §8 / §11.
…ole classification

Pivot the role classifier in detect_megatron_role to consult
megatron.bridge.models.conversion.param_mapping.AutoMapping._MODULE_TYPE_REGISTRY
as the authoritative source. Bridge's registry is curated to cover every
TE / Inference / Quant variant of column-parallel, row-parallel, and
replicated modules:

    column     ColumnParallelLinear, TEColumnParallelLinear,
               TELayerNormColumnParallelLinear, TEColumnParallelGroupedLinear,
               InferenceLayerNormColumnParallelLinear, QuantColumnParallelLinear,
               LinearCrossEntropyModule, VocabParallelEmbedding,
               DotProductAttention (attention sink), TEDotProductAttention
    row        RowParallelLinear, TERowParallelLinear, TERowParallelGroupedLinear,
               QuantRowParallelLinear, InferenceRowParallelLinear
    replicated TENorm, FusedLayerNorm, WrappedTorchNorm, LayerNorm, RMSNorm,
               L2Norm, InferenceTopKRouter, IdentityOp, LinearForLastLayer,
               TopKRouter

Previously the classifier matched on substrings of the base class names
("ColumnParallel", "RowParallel"), which silently misclassified many TE
variants as replicated and would have wasted publish bandwidth (best
case) or routed shards through the wrong assembly path (worst case).

When Bridge is not importable (e.g. CPU-only unit tests), the classifier
falls back to substring matching against the base names — sufficient for
mainline Megatron-Core. The fallback also handles the
LayerNormColumnParallelLinear special-case Bridge tracks separately.

Why this matters
    The receiver-side translator (Phase C) is a thin adapter over Bridge's
    own MegatronParamMapping.megatron_to_hf calls (and the standalone
    helpers like split_qkv_weights / merge_qkv_weights). By using Bridge's
    classification on the publisher side too, both halves of the transport
    agree on what a tensor's role is, regardless of whether it's a
    mainline Megatron-Core class or a TE variant. No more parallel
    truth-source to maintain.

See temp/NemoRL_Megatron_MX_Design.md §9b ("Architecture pivot:
Bridge-driven receiver") for the full design, and
temp/NemoRL_Megatron_MX_Phase_C_Handoff.md for the receiver-side
implementation plan that this Phase A change unblocks.
…in v2 sidecar

Trainer-side companion to modelexpress commit 76ac523. At trainer init,
the Megatron policy worker now derives:

  1. MegatronTransformerConfig (head counts + dims) from
     self.megatron_bridge.transformer_config
  2. Megatron→HF name map from a Bridge mapping-registry walk
     (no weight transfer; just iterates the conversion-task list and
     reads each task.mapping.hf_param)

…and stamps them onto the MxV2TrainingPublisher via
``set_megatron_sidecar(sidecar)``. The publisher then merges them into
the v2 sidecar JSON at every publish() call, so the receiver's
parse_megatron_sidecar (modelexpress.megatron_translator) gets:

  * transformer_config — head counts + dims for QKV un-interleave
  * hf_name_map — entries like
      [
        ["decoder.layers.0.self_attention.linear_qkv.weight",
         ["q_proj.weight", "k_proj.weight", "v_proj.weight"]],
        ["decoder.layers.0.self_attention.linear_proj.weight",
         ["o_proj.weight"]],
        ...
      ]

The receiver doesn't need to import Megatron-Bridge to know either —
exactly what we want for the production Dynamo VllmDecodeWorker image
which does not ship Bridge.

QKVMapping ordering: Bridge's QKVMapping uses a dict-shaped hf_param
({"q": ..., "k": ..., "v": ...}). The sidecar emit forces the
canonical [q, k, v] order so the receiver-side translator's
hf_names[0/1/2] line up with split_qkv_weights' (q, k, v) return
tuple. Other dict-shaped mappings (rare today) emit dict.values()
order; receivers can override per-tensor if needed.

Failure mode: if Bridge's build_conversion_tasks raises (older Bridge,
unsupported model arch), warn and emit just the transformer_config
piece. The receiver then falls back to deriving names independently
(e.g. from the inference vLLM model's state-dict + plan.role).
…erExtension

Phase D of the Megatron-MX integration plan. Wires the new
modelexpress.megatron_translator path into the existing
update_weights_via_mx flow on NemoRL's direct-vLLM extension.

What's new
    * First-cycle Megatron detection: peek at any candidate's
      megatron_meta. None → existing DTensor / FSDP path. Set →
      latched into self._mx_megatron_mode and routed through
      _update_weights_via_mx_megatron forevermore on this receiver.
    * _update_weights_via_mx_megatron: builds a MegatronReceiverContext
      once (transformer_config + Megatron→HF name map from the source
      sidecar; receive_specs from the per-tensor TensorDescriptorV2
      registry); per refit, calls run_refit_cycle which drives the full
      discover → plan → assemble → translate → yield(hf_name, tensor)
      pipeline. The yielded HF tensors flow through the existing
      _load_weights + FP8-KV-cache hooks.
    * Tree fan-out (publish_self_as_source) is preserved on the
      Megatron path identical to the DTensor path.

Backwards compat
    * DTensor / FSDP receivers see _mx_megatron_mode = False on the
      first cycle and stay on the existing path. PR NVIDIA-NeMo#2700's prime-rl
      receivers and John's Dynamo-side worker extension are
      unaffected.
    * Sources that advertise publisher_kind=megatron but are missing
      the transformer_config sidecar (older trainer image) trigger a
      one-shot warning and fall back to non-Megatron mode.

Out of scope (deferred)
    * The pull callback in _update_weights_via_mx_megatron uses
      self._mx_receiver._receiver._nixl.pull_to, which is the
      conceptual API; the actual NIXL plumbing for sliced pulls
      (registering a sub-view of a parent tensor + completing the
      transfer) needs a small wrapper in modelexpress.refit_receiver.
      v0 of the actual NIXL sliced-pull plumbing lands in a follow-up
      commit; this commit gets the Phase D control flow + receive
      context build right so the next commit only needs to fill in
      the pull function.
    * Wiring into Dynamo's MxRefitWorkerExtension (a separate Dynamo
      repo edit; same shape — import run_refit_cycle, call from the
      worker extension's update_weights_via_mx).
… receiver

Pivot the Phase D receiver path from 'per-source pull callback' to
'bulk receive_weights + pre-filled buffers' since:

  1. The existing MxRefitReceiver.receive_weights primitive already
     does the matched-TP single-source NIXL pull correctly (same
     primitive the DTensor path uses).
  2. modelexpress.megatron_translator's new
     pre_assembled_buffers arg lets run_refit_cycle skip
     assemble_into_destination + the per-source pull callback.

Result: matched-TP Megatron-MX path uses ZERO new NIXL plumbing.
Per-rank Megatron buffers are pre-allocated + registered with the
existing register_tensors call, bulk-filled via the existing
receive_weights call, then walked by the translator.

Mixed-TP support (target_tp != source_tp) is gated behind 'no
matched-TP source for this receiver's tp_rank' and prints a clear
'v1 not yet wired' message — the planner already returns the right
slice info; only the per-source NIXL plumbing remains.
Two small fixes surfaced while validating Phase E shape 1 on a real
Megatron-loaded Qwen3-4B-Thinking model on the kavin GB200 cluster.

1. _build_megatron_sidecar called bridge.build_conversion_tasks but the
   Bridge API is bridge.get_conversion_tasks. Rename.
2. model.named_parameters() returns 'module.<...>' prefixed names when
   the model is wrapped, but Bridge's get_conversion_tasks returns
   unprefixed names. The publisher now strips 'module.' in
   collect_megatron_publish_set so the catalog and the sidecar name_map
   use the same form. The receiver-side wire-up gets a defensive strip
   too in case an older publisher emits the prefix.

Phase E shape 1 validation result with these fixes: 398/398 HF tensors
byte-identical against bridge.export_hf_weights ground truth after a
full A->B->C->D round trip; 313 Gbps single-NIC bulk pull. Writeup in
pensieve/RL/NemoRL/14_megatron_mx_phase_e_shape1_2026_06_08.md.
Replaces the matched-TP gate in _update_weights_via_mx_megatron with a
mixed-TP path that pulls each source's full manifest into scratch via
receive_weights_scratch, then satisfies per-source slice requests by
copying from scratch into the planner's pre-narrowed dest view.

Handles MegatronSliceSource.source_subslice for target-narrower cases
(vLLM TP > source TP, each receiver carves a sub-range out of one
source's shard). For target-wider (vLLM TP < source TP, each receiver
concatenates multiple sources' full shards), source_subslice is None
and we copy the full source tensor into the corresponding dest range.

Bandwidth: O(sum(source_bytes)) per receiver. For target-wider this is
the minimum needed; for target-narrower it over-pulls by the target
fan-out factor. A v1 sliced-pull primitive (offset+size NIXL
descriptor on the source side) would cut narrow-target bandwidth by
target_tp / source_tp.

Matched-TP fast path is preserved via the is_matched_tp branch; only
mixed-TP falls into the scratch-then-copy path. Both branches share
the same translation step (assemble_into_destination +
translate_megatron_to_hf).
Megatron-Core's TE-grouped linears (TEColumnParallelGroupedLinear,
TERowParallelGroupedLinear) expose each local expert's slice as a
separate nn.Parameter named weight0, weight1, ..., weightN, rather
than a single grouped buffer. This holds even at EP=1.

Two classifier fixes needed:

1. _enclosing_module previously only recognised weight/bias/scale/
   _extra_state as parameter leaves; with grouped naming the
   per-expert leaf is e.g. weight17. Extended the leaf detector to
   match weight<digits>, bias<digits>, scale<digits>. Without this,
   _enclosing_module walks past the parameter into the leaf and the
   classifier sees class name "Parameter" → unclassified → falls to
   ROLE_REPLICATED.

2. detect_megatron_role's per-expert path required ep_size > 1
   because it assumed the legacy single-.weight-leading-axis grouped
   layout. The TE-grouped layout has N parameters even at EP=1.
   Added a 2a branch that fires whenever the param name matches the
   expert pattern AND the leaf is weight<digits>: extracts the
   expert id from the suffix, classifies as ROLE_EXPERT_COLUMN /
   ROLE_EXPERT_ROW based on the enclosing module class, and emits
   expert_id + expert_layout=grouped in descriptor_extras. The
   legacy ep_size>1 leading-axis path is kept as 2b for back-compat.

Validation on Qwen3-30B-A3B-Instruct-2507 (real MoE: 48 layers, 128
experts) on the GB200 trainer:

  pre-fix role distribution (broken — all experts replicated):
    replicated 12529, row 48, qkv_column 48, vocab_parallel 1, column 1

  post-fix role distribution (correct):
    expert_column 6144  (= 48 layers x 128 experts of linear_fc1)
    expert_row    6144  (= 48 layers x 128 experts of linear_fc2)
    replicated     241  (norms, scalars, routers)
    row             48  (linear_proj)
    qkv_column      48  (linear_qkv)
    vocab_parallel   1
    column           1

Total classified: 12627 (matches Bridge's get_conversion_tasks count
and the publisher's named_parameters iteration count).
@github-actions github-actions Bot added documentation Improvements or additions to documentation CI labels Jun 9, 2026
@KavinKrishnan
KavinKrishnan changed the base branch from dynamo-k8s-integration-mx to dynamo-k8s-integration June 9, 2026 20:27
Bug surfaced on Qwen3-MoE-30B-A3B during Phase E shape 2 cluster
validation:

The fix in 9bdf5e4 stripped the `module.` prefix from
`model.named_parameters()`'s name BEFORE passing it to
`detect_megatron_role`, so the publisher and Bridge's name_map (which
uses unprefixed names) would agree on the catalog key. That fix was
correct for the catalog/name-map side, but it broke the classifier's
model-walk: `_enclosing_module(name, model)` descends through
attribute lookups, and for a DDP-wrapped model the descent starts at
`model.module.decoder...`. With the prefix stripped, `getattr(model,
'decoder', None)` returns None on the first step, the walk fails,
and every TP-sharded non-expert role (column, row, qkv_column,
gated_mlp_column, vocab_parallel) falls through to ROLE_REPLICATED.

Expert tensors didn't show the bug because the grouped-MoE 2a branch
classifies via the trailing `weight<N>` suffix on the name string,
not via the model walk — that branch fires before _enclosing_module.

Cluster evidence (Qwen3-30B-A3B, kavin/GB200, pre-fix):

  pass1: detect_megatron_role(raw_name=`module.decoder...`)
    expert_column 6144  expert_row 6144
    replicated     241  row 48  qkv_column 48
    vocab_parallel   1  column 1                    ← correct

  pass2: collect_megatron_publish_set (stripped name)
    expert_column 12288 (= 6144 fc1 + 6144 fc2 — both → "_column")
    replicated      339 (everything else collapsed)
                                                    ← broken

Fix: pass the raw (prefixed) name to detect_megatron_role so the
model-walk succeeds; keep stripping the prefix for the published
name (so the name_map lookup on the receiver still works). This
restores the 7-role classification and unblocks Phase E shape 2.

Validated locally: classifier now produces 7 distinct roles on
Qwen3-30B-A3B (6144 expert_column + 6144 expert_row + 241 replicated
+ 48 row + 48 qkv_column + 1 vocab + 1 column = 12 627 total).
@github-actions github-actions Bot removed documentation Improvements or additions to documentation CI labels Jun 10, 2026
@KavinKrishnan

Copy link
Copy Markdown
Author

Phase E shape 2 + mixed-TP cluster validation update (kavin / GB200, 2026-06-10). Two outstanding gaps from the prior session both closed; byte-identity proof against bridge.export_hf_weights ground truth on a real MoE model.

Phase E shape 2 — Qwen3-MoE-30B-A3B end-to-end

Model: Qwen/Qwen3-30B-A3B-Instruct-2507
  128 experts × 48 layers, GQA 8:1, 30.5B params, 61.06 GB bf16
Megatron config: TP=1, EP=1

Full A → B → C → D pipeline cross-pod:

Metric Value
Tensors published 12,627 (all 7 roles correctly classified)
HF tensors expected 18,867
HF tensors byte-identical 18,867 / 18,867
Drift / missing / extras 0 / 0 / 0
Bulk RDMA pull 61.06 GB, 1.78 s, 274.5 Gbps single-NIC
Translation cycle 18,867 HF tensors in 141.76 s
By-role coverage expert_gate 6144 + expert_up 6144 + expert_down 6144 + replicated 241 + qkv 144 + o_proj 48 + vocab 2

This exercises every grouped-MoE code path on a production-shape model: 12,288 per-expert tensors split correctly via the new split_gated_mlp_tp for fc1 (gate+up) and identity-copy for fc2 (down_proj); all 48 layer-attention QKV un-interleaves on GQA 8:1; 48 row-parallel proj concats; 48 column-parallel; vocab + lm_head.

Mixed-TP cluster smoke — TP=2 → TP=1

Synthetic 2-rank Megatron source → 1 receiver, exercising the v0 host-side scratch+slice path against real NIXL RDMA bytes. Target-narrower direction (source_tp > target_tp) where v0 is bandwidth-optimal.

Role Sources Assembly Byte-identical
qkv_column 2 qkv_uninterleave
gated_mlp_column 2 gated_mlp_split (TP-aware)
row 2 concat_dim1
column 2 concat_dim0
replicated 1 passthrough

8 / 8 byte-identical across all 5 tp-sharded roles.

Three bugs surfaced + fixed during validation

  1. split_gated_mlp was naive for TP > 1. Megatron-Core's fused gate+up stores [gate_local; up_local] per rank; concat across TP gives [gate_r0; up_r0; gate_r1; up_r1; ...], not [gate_global; up_global]. New split_gated_mlp_tp(t, tp) un-interleaves via a single reshape. Translator dispatches on len(plan.sources). (MX c6550ff, 4 new tests covering tp ∈ {1, 2, 4, 8}.)

  2. Grouped per-expert planner + translator dispatch. TE-grouped MoE classes expose each expert's slice as a separate nn.Parameter (weight0, weight1, ..., weightN), so each Megatron tensor IS one expert's weight. Planner now picks passthrough (one source per Megatron tensor) instead of the legacy per_expert (dict). Translator extended to handle expert_* + passthrough with 1 hf_name (identity) and 2 hf_names (split_gated_mlp on per-expert tensor). Legacy expert_layout = "leading_axis" path preserved for backwards compatibility. (MX 0c2727e, 6 new tests.)

  3. Classifier model-walk broken by module. prefix strip. Shape 1 fix 9bdf5e499 stripped the module. prefix from model.named_parameters()'s name BEFORE passing it to detect_megatron_role. Right for the name_map side; broke the model-walk because _enclosing_module(name, model) descends through model.module.decoder.... Without the prefix, the walk fails at the first attribute lookup and every TP-sharded non-expert role falls through to ROLE_REPLICATED. Fixed by passing the raw (prefixed) name to the classifier; keep stripping for the published name. (NemoRL efa0631e9.)

Full writeup + harness scripts: pensieve/RL/NemoRL/16_megatron_mx_shape2_and_mixed_tp_2026_06_10.md.

This closes Phase E end-to-end on the correctness side. The remaining performance follow-up is the v1 NIXL sliced-pull primitive for target-wider mixed-TP bandwidth optimisation; the v0 path is correctness-complete.

Adds the v1 sliced-pull data plane to
VllmInternalWorkerExtension._update_weights_via_mx_megatron's mixed-TP
branch, in tandem with the new MxRefitReceiver.pull_to primitive
(MX-side commit).

Restructured the mixed-TP path into five phases:

  1. Pre-allocate per-plan dest tensors (target_shape) and classify
     each (plan, source) contribution: contiguous narrows along axis 0
     route to v1 sliced pull; non-contiguous (axis-1 row-parallel) or
     per_expert (dict assembly) route to v0 scratch+copy.

  2. NIXL-register the v1 dest buffers (one register_tensors call).

  3. Per-source combined sliced pull: each source's batch of
     SlicedTransferRequests issued as a single pull_to() call → one
     NIXL transfer with N descriptor pairs.

  4. v0 fallback: for the plans that need scratch (row-parallel, per
     expert), bulk-pull each contributing source via
     receive_weights_scratch into a per-source scratch dict.

  5. Assemble + translate per plan: v1 plans use the pre-filled dest
     directly (no host-side assembly); v0 plans use
     assemble_into_destination with the existing scratch-based pull
     callback. Both paths feed translate_megatron_to_hf identically.

Bandwidth profile change vs v0:

  * Target-narrower (source_tp > target_tp): unchanged — already
    bandwidth-optimal in v0 (validated 8/8 byte-identical on
    cluster, 2026-06-10).

  * Target-wider (source_tp < target_tp): wire bytes drop by
    target_tp/source_tp× for axis-0 roles. Cluster validation needs
    a vllm tensor_parallel_size>1 DGD worker; deferred to a separate
    deployment-only step.

Backwards-compatibility: matched-TP fast path (is_matched_tp branch)
unchanged. Existing 18 867/18 867 byte-identical validation on
Qwen3-MoE-30B-A3B continues to hold (matched-TP path doesn't touch
this code).
Smallest viable Megatron + MX GRPO recipe — Qwen2.5-1.5B Megatron
TP=1 EP=1 PP=1 with vLLM generation, weight_sync.method="mx" pointing
at the cluster-validated Megatron-MX data plane:

  trainer:  stream_weights_via_mx (per-rank native shards + sidecar)
  refit:    MX server catalogs the source, ready=True
  receiver: _update_weights_via_mx_megatron
              -> v1 sliced-pull (axis-0 roles) / v0 scratch+copy (axis-1)
              -> translate Megatron->HF (vendored Bridge helpers)
              -> vllm.model.load_weights()

5 GRPO steps with val_period=5 gives one refit cycle + one validation
in ~10-20 min wall clock. Bump max_num_steps after first run lands.

End-to-end byte-identity of the Megatron path is independently
validated on:
  * Qwen3-4B-Thinking-2507 (dense): 398/398 HF tensors byte-identical
  * Qwen3-MoE-30B-A3B-Instruct-2507: 18 867/18 867 byte-identical
    (12 288 grouped per-expert tensors via the new split_gated_mlp_tp)
  * Synthetic 2-rank Megatron TP=2 mixed-TP: 8/8 byte-identical

See pensieve/RL/NemoRL/17_megatron_mx_grpo_loop_runbook.md for the
deployment runbook + expected log lines + bumping guide.

Recipe is fully wired; actual GRPO loop run pending cluster session
restoration.
@KavinKrishnan

Copy link
Copy Markdown
Author

Step 4 + step 5 update (2026-06-10): v1 sliced-pull primitive + GRPO recipe landed; cluster validation queued for next K8s session.

Step 4 — v1 sliced-pull NIXL primitive

Adds the bandwidth-optimal mixed-TP data plane. Two new APIs:

# Lower-level (modelexpress.nixl_transfer)
NixlTransferManager.receive_sliced_from_source(
    source_metadata, source_tensors,
    slice_requests=[SlicedTransferRequest(name, source_offset_bytes,
                                          slice_bytes, dest_view), ...],
)

# Higher-level (modelexpress.refit_receiver)
MxRefitReceiver.pull_to(
    source: SourceRef,
    requests=[(name, source_subslice (lo, hi) | None, dest_view), ...],
)

Each request maps to one (source-slice, dest-view) descriptor pair in a single combined NIXL transfer. Caller passes a list of N requests → one transfer with N descriptors → one wait. The dest_view must be contiguous (axis-0 narrows are; axis-1 row-parallel narrows aren't, fall back to v0 scratch+copy for those).

Receiver wiring in VllmInternalWorkerExtension._update_weights_via_mx_megatron restructured into 5 phases:

  1. Pre-allocate per-plan dest tensors; classify each (plan, source) as v1 (contiguous narrow) or v0 (axis-1 / per_expert dict).
  2. NIXL-register the v1 dest buffers (one register_tensors call).
  3. Per-source combined sliced pull (one pull_to() per source).
  4. v0 fallback bulk pull via receive_weights_scratch for any plans that need it.
  5. Assemble + translate per plan — v1 plans use the pre-filled dest directly (no host-side assembly).

Bandwidth profile change:

Direction v0 v1
Matched-TP bulk receive_from (already optimal) unchanged
Target-narrower (source_tp > target_tp) scratch + host slice-copy unchanged (already optimal — each receiver consumes a different source's full data)
Target-wider (source_tp < target_tp), axis-0 roles scratch (over-pull target_tp/source_tp×) + host slice-copy direct sliced RDMA, bandwidth reduced to minimum
Target-wider, axis-1 row-parallel scratch + copy (non-contiguous dest) falls back to v0; no change

8 new unit tests in tests/test_sliced_pull.py with a stubbed NIXL agent that simulates the RDMA via ctypes.memmove:

  • full pull = byte-identity check
  • partial pull target-wider axis 0 = correct offset math + byte-identity
  • batched pulls = N requests → 1 NIXL transfer with N descriptors
  • non-contiguous dest rejection
  • out-of-range slice rejection
  • dest-size mismatch rejection
  • unknown tensor name rejection
  • empty request list no-op

60 / 60 unit tests pass across the full MX suite (planner + translator + helpers + sliced-pull + publish-self-as-source). Bridge byte-identity cross-checks skipped locally; pass on trainer image.

Cluster validation for the target-wider direction needs a DGD with vllm_cfg.tensor_parallel_size > 1 (single manifest field bump). Deferred to the next K8s session.

Step 5 — Megatron-MX GRPO recipe + runbook

Smallest viable Megatron + MX GRPO recipe at examples/configs/grpo_math_1B_megatron_mx.yaml:

defaults: "grpo_math_1B_megatron.yaml"          # Qwen2.5-1.5B Megatron TP=1
grpo:
  max_num_steps: 5
policy:
  megatron_cfg.enabled: true
  dtensor_cfg.enabled: false
cluster:
  weight_sync:
    method: "mx"                                  # routes through Megatron-MX
    mx_config:
      enabled: true
      mx_server_url: "modelexpress-server.kavin.svc.cluster.local:8001"
      same_rank_only: true
      tree_scale_out: true
      nixl_retry_budget: 3

GRPO loop was already wired to call stream_weights_via_mx + update_weights_via_mx when weight_sync.method == "mx" (nemo_rl/algorithms/grpo.py:1314). The receiver auto-dispatches into the Megatron path when sources advertise publisher_kind=megatron. So the recipe is the only new thing needed.

Companion runbook documents the prerequisites, image-overlay shortcut, run command, expected log lines (matched-TP + mixed-TP v1), known gotchas, and bumping guide: pensieve/RL/NemoRL/17_megatron_mx_grpo_loop_runbook.md.

Final scoreboard

Phase Direction Cluster-validated?
Phase E shape 1 (matched-TP, real Qwen3-4B) source_tp = target_tp = 1 ✅ 398/398 byte-identical
Phase E shape 2 (real Qwen3-MoE-30B-A3B) source_tp = target_tp = 1 (matched) ✅ 18 867/18 867 byte-identical
Mixed-TP target-narrower (synthetic, TP=2 → TP=1) source_tp > target_tp ✅ 8/8 byte-identical
Mixed-TP target-wider (v1 sliced-pull, TP=1 → TP=2) source_tp < target_tp ⏳ queued — needs DGD TP>1, next K8s session
GRPO loop integration matched-TP, in-loop refit ⏳ queued — recipe + runbook committed

Correctness side is done; remaining items are deployment / cluster-time exercises with no new code expected.

Companion to the matched-TP cluster smokes on Qwen3-4B-Thinking-2507
(shape 1, 398/398 byte-identical, 2026-06-08). Inherits from the
existing grpo_workplace_assistant_dynamo_mx.yaml (DTensor + MX recipe)
and flips back to Megatron:

  policy.megatron_cfg.enabled: true
  policy.dtensor_cfg.enabled:  false
  policy.optimizer:            null   (Megatron reads megatron_cfg.optimizer)
  policy.scheduler:            null

cluster.weight_sync.method: "mx" + mx_config points at the kavin
MX server. 2 GRPO steps for the first cycle.

Requires the DGD's dynamo/vllm/mx_refit/extension.py to be patched
with a Megatron dispatch (calls modelexpress.megatron_translator.
run_refit_cycle for Megatron sources). The patch is staged in
pensieve/RL/NemoRL/v1_targetwider_smoke_2026_06_11/dynamo_vllm_mx_
refit_extension_megatron.py as a drop-in overlay; the natural
follow-up is an upstream PR to ai-dynamo/dynamo.

Companion writeup: pensieve/RL/NemoRL/18_v1_targetwider_validated_
dynamo_megatron_patch_2026_06_11.md.
@KavinKrishnan

Copy link
Copy Markdown
Author

2026-06-11 update: v1 sliced-pull target-wider cluster-validated + Dynamo extension Megatron patch ready.

Both items pending from yesterday closed.

Step 2b — v1 target-wider cluster smoke

Synthetic source_tp=1 → target_tp=2 (each "target rank" pulls only half of each tp-sharded source tensor via MxRefitReceiver.pull_to). Six slice requests batched into one NIXL transfer per rank.

[rcv] === Target TP=2 rank 0 ===
  pulled 6 slices, 2.49 MB, 0.013s
  OK q_proj, k_proj, v_proj, gate, up, dense_col, o_proj, norm

[rcv] === Target TP=2 rank 1 ===
  pulled 6 slices, 2.49 MB, 0.001s   ← reuses NIXL connection from rank 0
  OK q_proj, k_proj, v_proj, gate, up, dense_col, o_proj, norm

byte-identical: 16
drift:          0

*** V1 SLICED-PULL TARGET-WIDER CLUSTER SMOKE VALIDATED ***

First wire-level proof of the bandwidth-optimal direction. The lower-level receive_sliced_from_source primitive produces the right (remote_addr + offset, slice_bytes, device_id) descriptors and the bytes land exactly where the planner says they go.

Step 5 — Dynamo extension Megatron patch

The DGD worker's dynamo/vllm/mx_refit/extension.py was DTensor-only (calls receive_weights_scratch then model.load_weights with HF-format tensors). Patched it with:

  • A _update_weights_via_mx_megatron(candidates, version, mx_config, model_name) method that:

    1. Pulls the transformer_config + name_map from the source's sidecar via discover_megatron_context.
    2. Builds ReceiveSpecs from the candidates' registries (unioned).
    3. Routes matched-TP through receive_from (bulk fast path) with pre-allocated NIXL-registered dest buffers.
    4. Routes mixed-TP through pull_to for axis-0 plans (v1 sliced-pull) and receive_weights_scratch + host slice-copy for axis-1 row-parallel (v0 fallback).
    5. Feeds the resulting (hf_name, hf_tensor) pairs to vLLM's model.load_weights.
    6. Optionally republishes self as inference_replica for tree fan-out.
  • A dispatch line in update_weights_via_mx that detects Megatron candidates (any c.megatron_meta is not None) and routes to the new handler.

~250 LOC of isolated change; DTensor path untouched. Cluster-validated correctness across all Megatron paths means the dispatch produces correct bytes whenever it's exercised.

Staged as a drop-in overlay at pensieve/.../dynamo_vllm_mx_refit_extension_megatron.py. Natural upstream candidate for ai-dynamo/dynamo — drops directly into components/src/dynamo/vllm/mx_refit/extension.py with no surrounding changes.

Companion recipe examples/nemo_gym/grpo_workplace_assistant_dynamo_megatron_mx.yaml committed to NemoRL: inherits from the existing dynamo+mx recipe, flips megatron_cfg.enabled=true, points at the patched DGD.

Final correctness scoreboard

Path Validation Date
Matched-TP (real Qwen3-4B-Thinking) ✅ 398 / 398 byte-identical 2026-06-08
Matched-TP MoE (real Qwen3-MoE-30B-A3B) ✅ 18 867 / 18 867 byte-identical 2026-06-10
Mixed-TP target-narrower (synthetic TP=2 → TP=1) ✅ 8 / 8 byte-identical 2026-06-10
Mixed-TP target-wider v1 sliced-pull (synthetic TP=1 → TP=2) ✅ 16 / 16 byte-identical 2026-06-11

Remaining for actual GRPO loop run

Three cluster-engineering items, no code work:

  1. Ray cluster reconnection — autoscaler shows (no active nodes) even though the worker's raylet is running (GCS lost track of the worker; needs Ray restart on head + worker, or RayCluster recreate).
  2. Recipe inheritance chaingrpo_workplace_assistant_*.yaml ancestors aren't baked in the current trainer image; need either an overlay or a fresh image build.
  3. Dataset / reward env staging — gsm8k auto-downloads via HF on first run; needs trainer pod egress or pre-staged cache.

All receiver-side code paths the GRPO loop would exercise are already byte-identity-proven via the standalone smokes. The GRPO run is the operational escalation, not a code one.

Full writeup: pensieve/RL/NemoRL/18_v1_targetwider_validated_dynamo_megatron_patch_2026_06_11.md.

@jthomson04
jthomson04 merged commit 1a2d3e7 into jthomson04:dynamo-k8s-integration Jun 12, 2026
4 checks passed
@KavinKrishnan

KavinKrishnan commented Jun 23, 2026

Copy link
Copy Markdown
Author

Perf fix: cache NIXL-registered dest buffers across refit cycles (cluster-validated on GB200, 2026-06-23).

Audit of refit-cycle wall time on multi-receiver setups surfaced a receiver-side bug: the Megatron MX path re-allocated + re-registered NIXL buffers on every refit cycle, paying ~0.15 s of ibv_reg_mr per cycle on Qwen3-4B and proportionally more on bigger models.

Plan shapes are deterministic for a fixed (source_tp, target_tp) layout, so cycle-N's allocations are identical to cycle-1's. The matched-TP path in vllm_backend.py already cached this (line 613 _mx_megatron_buffers); the mixed-TP plan_dests allocation didn't. The patched Dynamo extension didn't either (matched-TP or mixed-TP).

What changed

Three sites in two files:

  • nemo_rl/models/generation/vllm/vllm_backend.py — mixed-TP plan_dests now cached on self._mx_megatron_plan_dests with newly_allocated_this_cycle guard.
  • pensieve/RL/NemoRL/v1_targetwider_smoke_2026_06_11/dynamo_vllm_mx_refit_extension_megatron.py — matched-TP buffers cached on self._mx_megatron_buffers; mixed-TP plan_dests cached on self._mx_megatron_plan_dests.

The Dynamo extension overlay is the upstream-PR-ready file. Drops directly into components/src/dynamo/vllm/mx_refit/extension.py in ai-dynamo/dynamo.

Cluster validation — 3 back-to-back refit cycles on Qwen3-4B-Thinking-2507

Harness: a standalone multi-cycle benchmark. Exercises the same MxV2RefitReceiver + NIXL layer as both production receivers; MX_CACHE_BUFFERS env toggles caching for A/B compare.

                 alloc  register   pull  translate  total
BEFORE FIX (MX_CACHE_BUFFERS=0):
  Cycle 1       0.032     0.152   0.209     0.016   0.409
  Cycle 2       0.026     0.152   0.203     0.012   0.392
  Cycle 3       0.001     0.024   0.210     0.011   0.246

AFTER FIX (MX_CACHE_BUFFERS=1, emulating the code change):
  Cycle 1       0.028     0.085   0.206     0.014   0.333
  Cycle 2       0.000     0.000   0.204     0.010   0.215   (-45% vs cycle-2 baseline)
  Cycle 3       0.000     0.000   0.204     0.011   0.215
  • Pull step unchanged (~205-210 ms for 8 GB at 308 Gbps single-NIC). The fix touches setup overhead only.
  • Cycle 2's register cost (0.152 s) goes to zero after the fix → −45% wall on warm cycles.

Expected impact on larger multi-receiver setups

ibv_reg_mr cost scales roughly linearly with pinned-memory size + buffer count. Llama 3.1 8B is 16 GB / ~290 buffers, so per-receiver register cost is ~0.3-0.4 s. With many concurrent receivers all paying this on every cycle, the saved time per refit is meaningful but bounded by per-receiver register cost.

The fix is necessary but not sufficient for the broader multi-receiver perf story. Other items still on the list:

  1. MX is single-NIC-pinned by default on multi-NIC pods, leaving 75-87% of allocated bandwidth idle. Tracked in feat: refit speedup NVIDIA-NeMo/RL#449; PR feat: add context parallel. NVIDIA-NeMo/RL#450 adds an MX_RDMA_NIC_PIN=stripe mode.
  2. Tree fan-out (publish_self_as_source) — already implemented in MX but not enabled in production DGD configs.
  3. VMM arena single-MR registration (Deepscaler convergence NVIDIA-NeMo/RL#282) — cuts cycle-1 register cost ~10× by replacing N per-tensor MRs with 1 dmabuf MR.

Full writeup including the diagnostic + roadmap lives in the team's internal pensieve.

KavinKrishnan added a commit to ai-dynamo/modelexpress that referenced this pull request Jun 26, 2026
…timizations

Lands the v2 client surface and the surrounding RL workstream needed
to unblock the downstream Megatron-MX and perf PRs.

## What this contributes

### v2 client surface (this PR's primary deliverable)

* `MxV2TrainingPublisher` + `MxV2RefitReceiver` (modelexpress.nemo_rl_v2)
  -- the fat-client surface for per-rank shard publish and
  receiver-side multi-source assembly.
* `MxWeightTransferEngine` (modelexpress.vllm_weight_transfer) -- an
  adapter implementing vLLM's upstream WeightTransferEngine ABC.
* `TensorDescriptorV2.extra_parameters` (map<string,string>) plus
  `SourceIdentity.revision` (string) -- the two proto extensions that
  carry all per-tensor + per-source RL metadata.

### Phase 3a -- compile_target + compile_metadata on TensorDescriptorV2

New per-tensor fields default to ``hf_raw`` / ``{}``; the wire encoder
omits them when default so existing payloads stay byte-identical.
New constants: ``COMPILE_TARGET_HF_RAW``, ``_VLLM_FUSED``,
``_DEEPGEMM_FP8``, ``_CUTLASS_FP8``, ``_TRTLLM``. New helper
``compile_target_matches(descriptor, *, allowed_targets,
required_metadata=None)`` for receiver-side filtering with whitelist
plus required-metadata-subset semantics.

### Phase 3b -- compile_target_filter on discover_v2_sources

New kwargs ``compile_target_filter`` (whitelist set) and
``required_compile_metadata`` (subset-of-every-tensor's
compile_metadata). Candidates with no v2 registry are rejected when
either filter is set; candidates with mixed compile targets are
rejected if any tensor falls outside the allowed set.
``V2SourceCandidate.compile_targets: frozenset[str]`` exposed for
caller introspection.

### Phase 4 -- Multi-source slice discovery for mixed trainer/inference TP

New types: ``TargetTPLayout``, ``SliceSource``, ``SliceCoveragePlan``.
New methods ``MxV2RefitReceiver.discover_v2_sources_for_slice`` and
``MxV2RefitReceiver.receive_via_plan`` -- planner walks v2 candidates
per tensor, intersects each publisher's local_shard_range against the
receiver's requested slice, emits the minimal candidate set covering
it; surfaces coverage gaps and shard_axis mismatches in plan.missing.
``receive_via_plan`` orchestrates per-candidate scratch RDMA pulls
and stitches via torch.cat along the shard axis.

### Proto extensions

* ``TensorDescriptorV2.extra_parameters: map<string, string>`` -- the
  escape hatch downstream RL clients use for per-tensor metadata
  (megatron_role, compile_target, expert_id, revision, training_step,
  ...). Heavily used by #429.
* ``SourceIdentity.revision: string`` -- content-addressed weight
  version. Non-empty value guarantees two sources with identical
  SourceIdentity have bit-identical weight bytes, enabling
  decentralized modes (no central coordinator) to use mx_source_id
  as a full content check.
* Redis backend ``SourceAttributesJson`` carries the new fields plus
  the union of main's compatibility additions
  (backend_framework_version, torch_version, cuda_version,
  triton_version, gpu_arch, compile_config_digest).

## Tests

68/68 unit tests green on this branch's surface (test_v2_source_picker,
test_v2_shape_registry, test_source_id, test_types, test_vllm_adapter).

## Compatibility

Every new field has a backwards-compatible default and every new method
arg is optional. Existing demos and downstream consumers run unchanged.

## Downstream impact

This PR is the prerequisite for several PRs in flight:

* #421 -- publish_self_as_source tree fan-out fix
* #429 -- Megatron-Core MX clients
* #450 -- MX_RDMA_NIC_PIN=stripe multi-NIC mode
* ai-dynamo/dynamo#10900 -- first-time upstream port of
  dynamo.vllm.mx_refit extension
* ai-dynamo/dynamo#10901 -- buffer-caching perf fix in Dynamo extension
* jthomson04/RL#2 (merged) + #7 -- NeMo RL Megatron-MX integration plus
  perf fix

Once this PR lands, kavink/nemo_rl_moe (the integration branch holding

Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
#421 and #429) can be retired by rebasing those PRs onto main.
KavinKrishnan added a commit to ai-dynamo/modelexpress that referenced this pull request Jun 26, 2026
…timizations

Lands the v2 client surface and the surrounding RL workstream needed
to unblock the downstream Megatron-MX and perf PRs.

## What this contributes

### v2 client surface (this PR's primary deliverable)

* `MxV2TrainingPublisher` + `MxV2RefitReceiver` (modelexpress.nemo_rl_v2)
  -- the fat-client surface for per-rank shard publish and
  receiver-side multi-source assembly.
* `MxWeightTransferEngine` (modelexpress.vllm_weight_transfer) -- an
  adapter implementing vLLM's upstream WeightTransferEngine ABC.
* `TensorDescriptorV2.extra_parameters` (map<string,string>) plus
  `SourceIdentity.revision` (string) -- the two proto extensions that
  carry all per-tensor + per-source RL metadata.

### Phase 3a -- compile_target + compile_metadata on TensorDescriptorV2

New per-tensor fields default to ``hf_raw`` / ``{}``; the wire encoder
omits them when default so existing payloads stay byte-identical.
New constants: ``COMPILE_TARGET_HF_RAW``, ``_VLLM_FUSED``,
``_DEEPGEMM_FP8``, ``_CUTLASS_FP8``, ``_TRTLLM``. New helper
``compile_target_matches(descriptor, *, allowed_targets,
required_metadata=None)`` for receiver-side filtering with whitelist
plus required-metadata-subset semantics.

### Phase 3b -- compile_target_filter on discover_v2_sources

New kwargs ``compile_target_filter`` (whitelist set) and
``required_compile_metadata`` (subset-of-every-tensor's
compile_metadata). Candidates with no v2 registry are rejected when
either filter is set; candidates with mixed compile targets are
rejected if any tensor falls outside the allowed set.
``V2SourceCandidate.compile_targets: frozenset[str]`` exposed for
caller introspection.

### Phase 4 -- Multi-source slice discovery for mixed trainer/inference TP

New types: ``TargetTPLayout``, ``SliceSource``, ``SliceCoveragePlan``.
New methods ``MxV2RefitReceiver.discover_v2_sources_for_slice`` and
``MxV2RefitReceiver.receive_via_plan`` -- planner walks v2 candidates
per tensor, intersects each publisher's local_shard_range against the
receiver's requested slice, emits the minimal candidate set covering
it; surfaces coverage gaps and shard_axis mismatches in plan.missing.
``receive_via_plan`` orchestrates per-candidate scratch RDMA pulls
and stitches via torch.cat along the shard axis.

### Proto extensions

* ``TensorDescriptorV2.extra_parameters: map<string, string>`` -- the
  escape hatch downstream RL clients use for per-tensor metadata
  (megatron_role, compile_target, expert_id, revision, training_step,
  ...). Heavily used by #429.
* ``SourceIdentity.revision: string`` -- content-addressed weight
  version. Non-empty value guarantees two sources with identical
  SourceIdentity have bit-identical weight bytes, enabling
  decentralized modes (no central coordinator) to use mx_source_id
  as a full content check.
* Redis backend ``SourceAttributesJson`` carries the new fields plus
  the union of main's compatibility additions
  (backend_framework_version, torch_version, cuda_version,
  triton_version, gpu_arch, compile_config_digest).

## Tests

68/68 unit tests green on this branch's surface (test_v2_source_picker,
test_v2_shape_registry, test_source_id, test_types, test_vllm_adapter).

## Compatibility

Every new field has a backwards-compatible default and every new method
arg is optional. Existing demos and downstream consumers run unchanged.

## Downstream impact

This PR is the prerequisite for several PRs in flight:

* #421 -- publish_self_as_source tree fan-out fix
* #429 -- Megatron-Core MX clients
* #450 -- MX_RDMA_NIC_PIN=stripe multi-NIC mode
* ai-dynamo/dynamo#10900 -- first-time upstream port of
  dynamo.vllm.mx_refit extension
* ai-dynamo/dynamo#10901 -- buffer-caching perf fix in Dynamo extension
* jthomson04/RL#2 (merged) + #7 -- NeMo RL Megatron-MX integration plus
  perf fix

Once this PR lands, kavink/nemo_rl_moe (the integration branch holding

Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
#421 and #429) can be retired by rebasing those PRs onto main.
KavinKrishnan added a commit to ai-dynamo/modelexpress that referenced this pull request Jun 26, 2026
…timizations

Lands the v2 client surface and the surrounding RL workstream needed
to unblock the downstream Megatron-MX and perf PRs.

## What this contributes

### v2 client surface (this PR's primary deliverable)

* `MxV2TrainingPublisher` + `MxV2RefitReceiver` (modelexpress.nemo_rl_v2)
  -- the fat-client surface for per-rank shard publish and
  receiver-side multi-source assembly.
* `MxWeightTransferEngine` (modelexpress.vllm_weight_transfer) -- an
  adapter implementing vLLM's upstream WeightTransferEngine ABC.
* `TensorDescriptorV2.extra_parameters` (map<string,string>) plus
  `SourceIdentity.revision` (string) -- the two proto extensions that
  carry all per-tensor + per-source RL metadata.

### Phase 3a -- compile_target + compile_metadata on TensorDescriptorV2

New per-tensor fields default to ``hf_raw`` / ``{}``; the wire encoder
omits them when default so existing payloads stay byte-identical.
New constants: ``COMPILE_TARGET_HF_RAW``, ``_VLLM_FUSED``,
``_DEEPGEMM_FP8``, ``_CUTLASS_FP8``, ``_TRTLLM``. New helper
``compile_target_matches(descriptor, *, allowed_targets,
required_metadata=None)`` for receiver-side filtering with whitelist
plus required-metadata-subset semantics.

### Phase 3b -- compile_target_filter on discover_v2_sources

New kwargs ``compile_target_filter`` (whitelist set) and
``required_compile_metadata`` (subset-of-every-tensor's
compile_metadata). Candidates with no v2 registry are rejected when
either filter is set; candidates with mixed compile targets are
rejected if any tensor falls outside the allowed set.
``V2SourceCandidate.compile_targets: frozenset[str]`` exposed for
caller introspection.

### Phase 4 -- Multi-source slice discovery for mixed trainer/inference TP

New types: ``TargetTPLayout``, ``SliceSource``, ``SliceCoveragePlan``.
New methods ``MxV2RefitReceiver.discover_v2_sources_for_slice`` and
``MxV2RefitReceiver.receive_via_plan`` -- planner walks v2 candidates
per tensor, intersects each publisher's local_shard_range against the
receiver's requested slice, emits the minimal candidate set covering
it; surfaces coverage gaps and shard_axis mismatches in plan.missing.
``receive_via_plan`` orchestrates per-candidate scratch RDMA pulls
and stitches via torch.cat along the shard axis.

### Proto extensions

* ``TensorDescriptorV2.extra_parameters: map<string, string>`` -- the
  escape hatch downstream RL clients use for per-tensor metadata
  (megatron_role, compile_target, expert_id, revision, training_step,
  ...). Heavily used by #429.
* ``SourceIdentity.revision: string`` -- content-addressed weight
  version. Non-empty value guarantees two sources with identical
  SourceIdentity have bit-identical weight bytes, enabling
  decentralized modes (no central coordinator) to use mx_source_id
  as a full content check.
* Redis backend ``SourceAttributesJson`` carries the new fields plus
  the union of main's compatibility additions
  (backend_framework_version, torch_version, cuda_version,
  triton_version, gpu_arch, compile_config_digest).

## Tests

68/68 unit tests green on this branch's surface (test_v2_source_picker,
test_v2_shape_registry, test_source_id, test_types, test_vllm_adapter).

## Compatibility

Every new field has a backwards-compatible default and every new method
arg is optional. Existing demos and downstream consumers run unchanged.

## Downstream impact

This PR is the prerequisite for several PRs in flight:

* #421 -- publish_self_as_source tree fan-out fix
* #429 -- Megatron-Core MX clients
* #450 -- MX_RDMA_NIC_PIN=stripe multi-NIC mode
* ai-dynamo/dynamo#10900 -- first-time upstream port of
  dynamo.vllm.mx_refit extension
* ai-dynamo/dynamo#10901 -- buffer-caching perf fix in Dynamo extension
* jthomson04/RL#2 (merged) + #7 -- NeMo RL Megatron-MX integration plus
  perf fix

Once this PR lands, kavink/nemo_rl_moe (the integration branch holding

Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
#421 and #429) can be retired by rebasing those PRs onto main.
KavinKrishnan added a commit to ai-dynamo/modelexpress that referenced this pull request Jun 26, 2026
…timizations

Lands the v2 client surface and the surrounding RL workstream needed
to unblock the downstream Megatron-MX and perf PRs.

## What this contributes

### v2 client surface (this PR's primary deliverable)

* `MxV2TrainingPublisher` + `MxV2RefitReceiver` (modelexpress.nemo_rl_v2)
  -- the fat-client surface for per-rank shard publish and
  receiver-side multi-source assembly.
* `MxWeightTransferEngine` (modelexpress.vllm_weight_transfer) -- an
  adapter implementing vLLM's upstream WeightTransferEngine ABC.
* `TensorDescriptorV2.extra_parameters` (map<string,string>) plus
  `SourceIdentity.revision` (string) -- the two proto extensions that
  carry all per-tensor + per-source RL metadata.

### Phase 3a -- compile_target + compile_metadata on TensorDescriptorV2

New per-tensor fields default to ``hf_raw`` / ``{}``; the wire encoder
omits them when default so existing payloads stay byte-identical.
New constants: ``COMPILE_TARGET_HF_RAW``, ``_VLLM_FUSED``,
``_DEEPGEMM_FP8``, ``_CUTLASS_FP8``, ``_TRTLLM``. New helper
``compile_target_matches(descriptor, *, allowed_targets,
required_metadata=None)`` for receiver-side filtering with whitelist
plus required-metadata-subset semantics.

### Phase 3b -- compile_target_filter on discover_v2_sources

New kwargs ``compile_target_filter`` (whitelist set) and
``required_compile_metadata`` (subset-of-every-tensor's
compile_metadata). Candidates with no v2 registry are rejected when
either filter is set; candidates with mixed compile targets are
rejected if any tensor falls outside the allowed set.
``V2SourceCandidate.compile_targets: frozenset[str]`` exposed for
caller introspection.

### Phase 4 -- Multi-source slice discovery for mixed trainer/inference TP

New types: ``TargetTPLayout``, ``SliceSource``, ``SliceCoveragePlan``.
New methods ``MxV2RefitReceiver.discover_v2_sources_for_slice`` and
``MxV2RefitReceiver.receive_via_plan`` -- planner walks v2 candidates
per tensor, intersects each publisher's local_shard_range against the
receiver's requested slice, emits the minimal candidate set covering
it; surfaces coverage gaps and shard_axis mismatches in plan.missing.
``receive_via_plan`` orchestrates per-candidate scratch RDMA pulls
and stitches via torch.cat along the shard axis.

### Proto extensions

* ``TensorDescriptorV2.extra_parameters: map<string, string>`` -- the
  escape hatch downstream RL clients use for per-tensor metadata
  (megatron_role, compile_target, expert_id, revision, training_step,
  ...). Heavily used by #429.
* ``SourceIdentity.revision: string`` -- content-addressed weight
  version. Non-empty value guarantees two sources with identical
  SourceIdentity have bit-identical weight bytes, enabling
  decentralized modes (no central coordinator) to use mx_source_id
  as a full content check.
* Redis backend ``SourceAttributesJson`` carries the new fields plus
  the union of main's compatibility additions
  (backend_framework_version, torch_version, cuda_version,
  triton_version, gpu_arch, compile_config_digest).

## Tests

68/68 unit tests green on this branch's surface (test_v2_source_picker,
test_v2_shape_registry, test_source_id, test_types, test_vllm_adapter).

## Compatibility

Every new field has a backwards-compatible default and every new method
arg is optional. Existing demos and downstream consumers run unchanged.

## Downstream impact

This PR is the prerequisite for several PRs in flight:

* #421 -- publish_self_as_source tree fan-out fix
* #429 -- Megatron-Core MX clients
* #450 -- MX_RDMA_NIC_PIN=stripe multi-NIC mode
* ai-dynamo/dynamo#10900 -- first-time upstream port of
  dynamo.vllm.mx_refit extension
* ai-dynamo/dynamo#10901 -- buffer-caching perf fix in Dynamo extension
* jthomson04/RL#2 (merged) + #7 -- NeMo RL Megatron-MX integration plus
  perf fix

Once this PR lands, kavink/nemo_rl_moe (the integration branch holding

Signed-off-by: Kavin Krishnan <kavink@nvidia.com>
#421 and #429) can be retired by rebasing those PRs onto main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants