Skip to content

fix(peft): save shared-outer MoE LoRA adapters in PEFT format - #6076

Merged
huvunvidia merged 3 commits into
NVIDIA-NeMo:mainfrom
radixark:upstream-pr/peft-save-shared-outer-adapters
Sep 15, 2026
Merged

huvunvidia merged 3 commits into
NVIDIA-NeMo:mainfrom
radixark:upstream-pr/peft-save-shared-outer-adapters

Conversation

@yushengsu-thu

@yushengsu-thu yushengsu-thu commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do ?

Makes save_hf_adapter handle shared-outer MoE LoRA (experts_shared_outer_loras=True), which today fails with the opaque ValueError: Incomplete adapter export for target parameter: model.layers.N.mlp.experts.<proj>. Shared-outer has no PEFT ParamWrapper representation, so the layout is exposed as an explicit serving-only export (allow_serving_layout=True) and refused with a clear message in the default PEFT mode.

Changelog

  • convert_adapter_weights_to_peft_state(..., allow_serving_layout=False): _classify_shared_outer_expert_export recognises a shared-outer export only when exactly one side has expert dim 1, that side is the one shared-outer shares (lora_A on gate/up incl. the 3D-MoE experts.base_layer stem, lora_B on down incl. the bare experts stem), the per-expert partner exists (numbered 2D experts.N.<proj> slices or an [E, ...] stack under the same name) and both sides agree on the rank. Unequal expert dims without a shared side (e.g. 8 vs 4), a shared factor on the wrong side, a rank mismatch or a missing partner raise instead of being written.
  • Default (PEFT mode): a shared-outer export raises with a message pointing at expand_shared_outer=True (replicates the shared factor into the PEFT-loadable per-expert layout) or allow_serving_layout=True. With the opt-in both sides are written exactly as exported (shared factor once as [1, ...] under the expert-agnostic name), which is the layout SGLang's experts_shared_outer_loras reader consumes; it is not loadable by PeftModel.from_pretrained, and the docstrings say so.
  • save_hf_adapter(..., allow_serving_layout=False) forwards the flag; its expand_shared_outer docstring no longer claims a "PEFT shared [1, ...] layout".
  • Tests: PEFT default refuses shared-outer (converter and save_hf_adapter end-to-end); serving opt-in keeps the shared factor as a module weight for the numbered-2D and the 3D-pair layouts, accepts the 3D-MoE stems and unknown projections; rejection of unequal expert dims, wrong shared side, rank mismatch and missing partner; incomplete export still raises.

The streamer's output layouts are unchanged; only the PEFT on-disk conversion learns to validate the layout it already produces and to name it for what it is.

GitHub Actions CI

See the CI section in the Contributing doc for how to trigger the CI. A Nvidia developer will need to approve and trigger the CI for external contributors.

Before your PR is "Ready for review"

Pre checks:

  • Make sure you read and followed Contributor guidelines
  • Did you write any new necessary tests?
  • Did you add or update any necessary documentation?
  • Does the PR affect components that are optional to install? — no

Additional Information

Validation

  • Revision 1 (b53f924): tests/unit_tests/peft + test_model_bridge.py + test_model_bridge_lora.py + test_adapter_export.py: 627 passed, 7 skipped (baseline upstream/main@2eae3c971 on the same box: 443 + 181 = 624). New tests PASSED by name: TestSaveHfAdapter::test_convert_shared_outer_adapter_keeps_shared_factor_as_module_weight, TestSaveHfAdapter::test_convert_shared_outer_stacked_pair_keeps_both_sides_as_module_weights, TestSaveHfAdapter::test_convert_incomplete_target_parameter_still_raises.
  • GPU: rx devbox 2× H200 (h200-sci-k8s), image nvcr.io/nvidia/pytorch:26.08-py3 (the upstream CI base image), TE 2.18.0+27486e03 (matches upstream uv.lock), Megatron-Core at .main.commit c9b53d0a8, Megatron-Bridge editable. Command mirrors CI's Launch_Unit_Tests_Core.sh: CUDA_VISIBLE_DEVICES=0,1 pytest -m "not pleasefixme". ruff check, ruff check --select I, ruff format --check (ruff 0.9.9) pass on the changed files.

Revision 2 (review: PEFT-format promise; classifier too permissive)

  • Shared-outer is now an explicit serving-only export (allow_serving_layout), refused by default with guidance; the classifier validates exactly-one-shared-side, expected side per projection, partner presence and rank agreement.
  • Verified on 2× RTX 4090 (nvcr.io/nvidia/pytorch:26.08-py3, TE 2.18.0+27486e03, Megatron-Core c9b53d0a8): fail-before with revision 1's sources + the new tests → 9 failed (7 × unexpected keyword argument 'allow_serving_layout', 2 × DID NOT RAISE ValueError for the PEFT-mode refusal in the converter and in save_hf_adapter); pass-after → 21 passed (the 9 new/changed tests plus the 12 existing shared-outer/expand tests selected by name); test_adapter_export.py + test_model_bridge_lora.py + test_auto_bridge.py + tests/unit_tests/peft → 691 passed, 7 skipped (baseline upstream/main 681 passed / 7 skipped, no new failures).

🤖 Generated with Claude Code

``save_hf_adapter`` failed for shared-outer MoE LoRA (``experts_shared_outer_loras``)
with ``ValueError: Incomplete adapter export for target parameter``. The default
export emits the shared factor once as a ``[1, ...]`` tensor under the expert-agnostic
HF name and the per-expert factor as numbered 2D slices, so
``convert_adapter_weights_to_peft_state`` saw a lone 3D tensor, treated it as a
packed ``target_parameters`` entry and found no partner to pack with. The
``stack_3d_moe`` layout (both sides 3D with different expert dims) hit the same
packer with mismatched shapes.

Recognise both shapes of a shared-outer export and keep them as exported module
weights instead of trying to fold them into a PEFT parameter target; a lone 3D
side without any per-expert sibling still raises, so a genuinely incomplete
export is not silently saved.

Based on #33 (@Arist12), re-applied on the current
export layout without changing what the streamer emits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yusheng Su <yushengsu.thu@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Sep 12, 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.

@yaoyu-33 yaoyu-33 added area:peft Parameter-efficient fine-tuning (LoRA, adapters) bug Something isn't working needs-more-tests Requires additional L0 and L1 test coverage before merge needs-review PR is ready for code review and waiting on a reviewer labels Sep 12, 2026
for base_name in shared_outer_targets:
for lora_suffix, tensor in parameter_weights[base_name].items():
name = f"{base_name}{lora_suffix}"
adapter_state[f"base_model.model.{name}"] = tensor

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.

save_hf_adapter promises a directory loadable by PeftModel.from_pretrained, but this removes the tensor from target_parameters and writes an expert-agnostic 3D key as a normal module weight. Standard PEFT has no matching module for that key and may ignore it while loading the per-expert partner. The new tests only inspect the rewritten dictionary. Please add a real PeftModel.from_pretrained load plus numerical adapter parity, as the existing packed-expert test does, or make this a clearly serving-specific export path rather than PEFT format.

@yushengsu-thu yushengsu-thu Sep 13, 2026 •

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.

Agreed — fixed in 1e4eaef by taking the second option: this layout is a serving-only export, not PEFT format, and the code now says so.

lora_a = weights.get(".lora_A.weight")
lora_b = weights.get(".lora_B.weight")
if lora_a is not None and lora_b is not None:
return lora_a.shape[0] != lora_b.shape[0]

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.

This classifies every unequal pair of leading dimensions as shared-outer, including malformed ordinary exports such as expert dimensions 8 and 4. That turns a shape mismatch into a silently accepted checkpoint. Please require exactly one side to have expert_dim=1 and validate the expected shared side/layout before bypassing target-parameter packing.

@yushengsu-thu yushengsu-thu Sep 13, 2026 •

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.

Fixed in 1e4eaef. _is_shared_outer_expert_export is replaced by _classify_shared_outer_expert_export, which returns the shared side only for a well-formed shared-outer pair and raises for everything else

…g layout

Review feedback on the shared-outer adapter save path:

* The classifier no longer treats every unequal pair of leading dims as
  shared-outer. ``_classify_shared_outer_expert_export`` requires exactly one
  side with expert dim 1, checks that the shared side is the one shared-outer
  actually shares (lora_A on gate/up, lora_B on down; the vLLM 3D-MoE stems
  ``experts.base_layer`` / ``experts`` map the same way), checks that the
  per-expert partner exists (numbered 2D slices or an ``[E, ...]`` stack) and
  that both sides agree on the rank. Anything else -- 8 vs 4 expert dims, a
  shared factor on the wrong side, a rank mismatch, a shared factor without a
  partner -- raises instead of being written as a checkpoint.
* The shared-outer layout is now an explicit serving-only export.
  ``convert_adapter_weights_to_peft_state`` and ``save_hf_adapter`` take
  ``allow_serving_layout``; by default a shared-outer export raises with a
  message pointing at ``expand_shared_outer=True`` (the PEFT-loadable form) or
  ``allow_serving_layout=True`` (the layout SGLang's
  ``experts_shared_outer_loras`` reader consumes, not loadable by
  ``PeftModel.from_pretrained``). Docstrings say so instead of claiming a PEFT
  shared layout.

Tests: PEFT default refuses shared-outer; serving opt-in keeps the shared
factor as a module weight for both the numbered-2D and the 3D pair layouts;
3D-MoE stems and unknown projections; rejection of unequal expert dims, wrong
shared side, rank mismatch and missing partner; save_hf_adapter end-to-end
(refuses by default, writes the serving layout with no target_parameters when
asked).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yusheng Su <yushengsu.thu@gmail.com>
@yaoyu-33

Copy link
Copy Markdown
Contributor

/ok to test 1e4eaef

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

LGTM. The latest changes address the review comments and add coverage for the affected boundary.

@yaoyu-33 yaoyu-33 added ready-to-merge PR is approved, current, and only waiting for CI to pass before merge and removed needs-review PR is ready for code review and waiting on a reviewer labels Sep 14, 2026
@huvunvidia

Copy link
Copy Markdown
Contributor

/ok to test 4d99c12

@huvunvidia
huvunvidia merged commit 9b52640 into NVIDIA-NeMo:main Sep 15, 2026
85 checks passed

This branch was successfully deployed

1 active deployment
test — 4d99c126 Deployed Sep 14, 2026 by copy-pr-bot[bot] via cicd-wait-in-queue #21809
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:peft Parameter-efficient fine-tuning (LoRA, adapters) bug Something isn't working community-request needs-more-tests Requires additional L0 and L1 test coverage before merge ready-to-merge PR is approved, current, and only waiting for CI to pass before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants