Skip to content

feat(unsloth): support Nemotron-H hybrid MoE fine-tuning on DGX Spark#338

Merged
albcui merged 2 commits into
mainfrom
albcui/customizer/unsloth-nemotron
Jun 16, 2026
Merged

feat(unsloth): support Nemotron-H hybrid MoE fine-tuning on DGX Spark#338
albcui merged 2 commits into
mainfrom
albcui/customizer/unsloth-nemotron

Conversation

@albcui

@albcui albcui commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Enable fine-tuning of hybrid Mamba-2 + MoE models (NVIDIA Nemotron-H, e.g. Nemotron-3-Nano-30B-A3B) on the Unsloth customization backend, plus two supporting fixes uncovered bringing the backend up on a unified-memory GB10 (DGX Spark, arm64/Blackwell).

  • Add mamba-ssm + causal-conv1d to the nmp-unsloth-training image, installed from the same prebuilt cu13.1.1/cp312 wheels as nmp-automodel-base via the causal-conv1d-wheel / mamba-ssm-wheel bake contexts (versions single-sourced, no recompile). Nemotron-H remote code imports mamba_ssm at load time.
  • Add configurable model.device_map (default {"": 0}). Without an explicit single-device map, accelerate's device_map="auto" sizes the GPU budget from CUDA free memory, which on unified-memory parts reports a small dynamic carve-out and spills layers to CPU -- aborting 4-bit loads.
  • Fix file_io download crash: get_percentage now clamps instead of raising when the live file count exceeds the pre-listing total (off-by-one on sources with nested directories), so a cosmetic progress value can't fail a multi-GB download mid-transfer.
  • Register cpu/gpu + gpu/gpu docker execution profiles in the local runner config so the customization backends run out-of-the-box on a local platform.
  • Docs: document model.device_map and the 16-bit-LoRA path for hybrid MoE models in the customizer hyperparameters reference; refresh the unsloth image README build steps (shared wheels / USE_LOCAL_WHEELS, accurate step list).

Validated end-to-end on DGX Spark: Nemotron-3-Nano-30B-A3B loads and trains in 16-bit LoRA (load_in_4bit=false).

Summary by CodeRabbit

  • New Features
    • Added device_map support for Unsloth model loading to control GPU placement (including single-GPU defaults and explicit mappings).
    • Updated local executor configuration to enable Docker-backed CPU/GPU options and LoRA sidecar settings for adapter deployments.
  • Bug Fixes
    • Made transfer progress calculation robust to out-of-range counters by safely clamping values.
  • Documentation
    • Updated Unslo th training image and hyperparameter guidance to use prebuilt CUDA-extension wheels and clarified Flash Attention 2 is not installed.
    • Improved local/air-gapped build instructions for Buildx bake/build-context wheel handling.

@github-actions github-actions Bot added the feat label Jun 15, 2026
@albcui
albcui marked this pull request as ready for review June 15, 2026 20:09
@albcui
albcui requested review from a team as code owners June 15, 2026 20:09
@albcui albcui changed the title feat(unsloth): support Nemotron-H hybrid Mamba MoE fine-tuning feat(unsloth): support Nemotron-H hybrid MoE fine-tuning Jun 15, 2026
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f283db86-bcbe-42a5-b01f-c1fc8a0eb4df

📥 Commits

Reviewing files that changed from the base of the PR and between 926b0f0 and 1cae724.

📒 Files selected for processing (9)
  • docker-bake.hcl
  • docker/Dockerfile.nmp-unsloth-training
  • docker/unsloth/README.md
  • packages/nmp_platform/config/local.yaml
  • packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml
  • plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md
  • services/unsloth/src/nmp/unsloth/schemas.py
  • services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py
  • services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py
✅ Files skipped from review due to trivial changes (1)
  • plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • docker-bake.hcl
  • services/unsloth/src/nmp/unsloth/schemas.py
  • docker/unsloth/README.md
  • packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml
  • services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py
  • services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py
  • packages/nmp_platform/config/local.yaml
  • docker/Dockerfile.nmp-unsloth-training

📝 Walkthrough

Walkthrough

Adds prebuilt CUDA extension wheels (causal-conv1d, mamba-ssm) to the nmp-unsloth-training Docker image via new build stages and bake contexts. Extends ModelLoadSpec with a device_map field forwarded to FastLanguageModel.from_pretrained. Enables local executor and LoRA sidecar configuration, fixes progress callback clamping.

Changes

Unsloth training: prebuilt CUDA wheels + device_map support

Layer / File(s) Summary
device_map schema field and training backend wiring
services/unsloth/src/nmp/unsloth/schemas.py, services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py, plugins/nemo-customizer/.../hyperparameters.md
ModelLoadSpec gains device_map: str | int | dict[str, int] | None; train_sft passes it to FastLanguageModel.from_pretrained, defaulting to {"": 0}; docs describe the field and Hybrid Mamba/MoE 16-bit requirements.
Prebuilt wheel Docker stages and bake contexts
docker/Dockerfile.nmp-unsloth-training, docker-bake.hcl
Dockerfile adds causal-conv1d-wheel-src and mamba-ssm-wheel-src build stages and Step 1c to install both via uv pip install --no-deps from mounted wheel paths. docker-bake.hcl adds the two wheel image contexts to the nmp-unsloth-training target.
README for local builds and air-gapped runbook
docker/unsloth/README.md
Documents prebuilt wheel build context requirements, replaces prior Flash Attention 2 build steps with mamba-ssm/causal-conv1d wheel install guidance, and adds explicit --build-context flags for air-gapped docker buildx build commands.
Local executor and LoRA sidecar configuration
services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py, packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml, packages/nmp_platform/config/local.yaml
get_percentage clamps current into [0, total] with debug logging instead of raising. Both local.yaml configs uncomment/enable cpu and gpu executor entries and add LoRA sidecar image/command for model controller backend.

Possibly Related PRs

  • NVIDIA-NeMo/nemo-platform#288: Introduces consolidated bake setup and helper wheel-context functions that this PR builds on for causal-conv1d and mamba-ssm contexts.

Suggested Reviewers

  • matthewgrossman
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main feature: supporting Nemotron-H hybrid MoE fine-tuning on DGX Spark, which aligns with the PR's primary objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch albcui/customizer/unsloth-nemotron

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
docker/Dockerfile.nmp-unsloth-training (1)

130-134: ⚡ Quick win

Wheel path version inconsistency.

mamba-ssm path hardcodes version 2.3.0 while causal-conv1d uses a full wildcard. If multiple causal-conv1d versions exist in the wheel directory, the wildcard could match unexpectedly. Consider hardcoding the causal-conv1d version for consistency (e.g., causal_conv1d-1.5.3-cp312*.whl), or document why version flexibility is intentional here.

🤖 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 `@docker/Dockerfile.nmp-unsloth-training` around lines 130 - 134, The
causal-conv1d wheel path uses a version wildcard pattern while mamba-ssm
hardcodes its version, creating inconsistency that could cause unpredictable
wheel selection if multiple versions exist. Modify the causal_conv1d wheel path
in the RUN command (currently using `causal_conv1d-*cp312*.whl`) to hardcode a
specific version number (e.g., `causal_conv1d-1.5.3-cp312*.whl`) to match the
explicit versioning approach used for mamba_ssm-2.3.0-cp312*.whl, ensuring
consistent and predictable dependency installation.
services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py (1)

22-39: ⚡ Quick win

Cross-service inconsistency: automodel still raises.

The automodel service's get_percentage (at services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py:18-45) still raises ValueError on invalid inputs. This divergence could confuse maintainers when similar functions behave differently across services.

Consider aligning both implementations to use the clamping approach.

🤖 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 `@services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py` around lines 22
- 39, The automodel service's get_percentage function still raises ValueError on
invalid inputs, while the unsloth service's get_percentage function now uses the
clamping approach to handle out-of-range values gracefully. Align the automodel
service's get_percentage function to match the unsloth implementation by
replacing the ValueError raising behavior with the same clamping logic (using
max/min to constrain current between 0 and total, and returning 0 when total is
invalid), while also adding the debug logging that explains why clamping occurs.
This ensures consistent error handling across services and prevents maintainer
confusion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml`:
- Around line 32-36: The CPU executor configuration in the local.yaml file has a
mismatch between its provider and profile settings. The `provider: cpu` at line
32 is paired with `profile: gpu` at line 33, which is incorrect. Change the
profile value from gpu to cpu to ensure the configuration properly reflects CPU
resources and jobs are scheduled with the correct resource expectations.
- Line 36: The launcher_tool_path in local.yaml uses a relative path that
depends on the repository being available at runtime and the platform starting
from the repo root. Replace the relative path
`./services/core/jobs/jobs-launcher/jobs-launcher` with an absolute path
pointing to the built jobs-launcher binary (e.g.,
`/full/path/to/nemo-platform/services/core/jobs/jobs-launcher/jobs-launcher`),
and ensure the documentation or configuration comments clarify that users must
first run `make build-jobs-launcher` to build the Go binary before the platform
can properly inject the launcher into containers.

---

Nitpick comments:
In `@docker/Dockerfile.nmp-unsloth-training`:
- Around line 130-134: The causal-conv1d wheel path uses a version wildcard
pattern while mamba-ssm hardcodes its version, creating inconsistency that could
cause unpredictable wheel selection if multiple versions exist. Modify the
causal_conv1d wheel path in the RUN command (currently using
`causal_conv1d-*cp312*.whl`) to hardcode a specific version number (e.g.,
`causal_conv1d-1.5.3-cp312*.whl`) to match the explicit versioning approach used
for mamba_ssm-2.3.0-cp312*.whl, ensuring consistent and predictable dependency
installation.

In `@services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py`:
- Around line 22-39: The automodel service's get_percentage function still
raises ValueError on invalid inputs, while the unsloth service's get_percentage
function now uses the clamping approach to handle out-of-range values
gracefully. Align the automodel service's get_percentage function to match the
unsloth implementation by replacing the ValueError raising behavior with the
same clamping logic (using max/min to constrain current between 0 and total, and
returning 0 when total is invalid), while also adding the debug logging that
explains why clamping occurs. This ensures consistent error handling across
services and prevents maintainer confusion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d32a186a-53d9-428d-8265-8e81d5aa8312

📥 Commits

Reviewing files that changed from the base of the PR and between a3c9e2f and 106949a.

📒 Files selected for processing (8)
  • docker-bake.hcl
  • docker/Dockerfile.nmp-unsloth-training
  • docker/unsloth/README.md
  • packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml
  • plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md
  • services/unsloth/src/nmp/unsloth/schemas.py
  • services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py
  • services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 19462/25829 75.3% 60.9%
Integration Tests 11372/24601 46.2% 20.2%

@albcui
albcui force-pushed the albcui/customizer/unsloth-nemotron branch 2 times, most recently from a522cdd to 9146ab2 Compare June 15, 2026 20:39
@albcui albcui changed the title feat(unsloth): support Nemotron-H hybrid MoE fine-tuning feat(unsloth): support Nemotron-H hybrid MoE fine-tuning on DGX Spark Jun 16, 2026
@albcui
albcui force-pushed the albcui/customizer/unsloth-nemotron branch from 9146ab2 to 926b0f0 Compare June 16, 2026 18:13

@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)
packages/nmp_platform/config/local.yaml (1)

78-87: ⚡ Quick win

Remove redundant launcher_tool_path from executor entries.

Both executor entries specify launcher_tool_path (lines 82, 87) with the same value already defined in executor_defaults.docker (line 95). The executor-level config unnecessarily duplicates the default.

♻️ Remove redundant configuration
     - provider: cpu
       profile: gpu
       backend: docker
-      config:
-        launcher_tool_path: ./services/core/jobs/jobs-launcher/jobs-launcher
     - provider: gpu
       profile: gpu
       backend: docker
-      config:
-        launcher_tool_path: ./services/core/jobs/jobs-launcher/jobs-launcher
🤖 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 `@packages/nmp_platform/config/local.yaml` around lines 78 - 87, Remove the
redundant launcher_tool_path configuration from both executor entries (the cpu
provider and gpu provider executor blocks) in the config file. The
launcher_tool_path value is already defined once in the executor_defaults.docker
section, so specifying it individually in each executor's config block creates
unnecessary duplication. Delete the launcher_tool_path lines from both the cpu
executor entry and the gpu executor entry, keeping only the entries in the
shared defaults section.
🤖 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 `@packages/nmp_platform/config/local.yaml`:
- Around line 78-87: Remove the redundant launcher_tool_path configuration from
both executor entries (the cpu provider and gpu provider executor blocks) in the
config file. The launcher_tool_path value is already defined once in the
executor_defaults.docker section, so specifying it individually in each
executor's config block creates unnecessary duplication. Delete the
launcher_tool_path lines from both the cpu executor entry and the gpu executor
entry, keeping only the entries in the shared defaults section.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 7e3a151c-1e29-4c39-8809-a9a988683e7a

📥 Commits

Reviewing files that changed from the base of the PR and between 9146ab2 and 926b0f0.

📒 Files selected for processing (9)
  • docker-bake.hcl
  • docker/Dockerfile.nmp-unsloth-training
  • docker/unsloth/README.md
  • packages/nmp_platform/config/local.yaml
  • packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml
  • plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md
  • services/unsloth/src/nmp/unsloth/schemas.py
  • services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py
  • services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py
  • plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md
  • services/unsloth/src/nmp/unsloth/schemas.py
  • docker/unsloth/README.md
  • docker-bake.hcl
  • services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py
  • docker/Dockerfile.nmp-unsloth-training

@albcui
albcui added this pull request to the merge queue Jun 16, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Jun 16, 2026
albcui added 2 commits June 16, 2026 16:52
Enable fine-tuning of hybrid Mamba-2 + MoE models (NVIDIA Nemotron-H,
e.g. Nemotron-3-Nano-30B-A3B) on the Unsloth customization backend, plus
two supporting fixes uncovered bringing the backend up on a unified-memory
GB10 (DGX Spark, arm64/Blackwell).

- Add mamba-ssm + causal-conv1d to the nmp-unsloth-training image, installed
  from the same prebuilt cu13.1.1/cp312 wheels as nmp-automodel-base via the
  causal-conv1d-wheel / mamba-ssm-wheel bake contexts (versions single-sourced,
  no recompile). Nemotron-H remote code imports mamba_ssm at load time.
- Add configurable model.device_map (default {"": 0}). Without an explicit
  single-device map, accelerate's device_map="auto" sizes the GPU budget from
  CUDA free memory, which on unified-memory parts reports a small dynamic
  carve-out and spills layers to CPU -- aborting 4-bit loads.
- Fix file_io download crash: get_percentage now clamps instead of raising when
  the live file count exceeds the pre-listing total (off-by-one on sources with
  nested directories), so a cosmetic progress value can't fail a multi-GB
  download mid-transfer.
- Register cpu/gpu + gpu/gpu docker execution profiles in the local runner
  config so the customization backends run out-of-the-box on a local platform.
- Docs: document model.device_map and the 16-bit-LoRA path for hybrid MoE models
  in the customizer hyperparameters reference; refresh the unsloth image README
  build steps (shared wheels / USE_LOCAL_WHEELS, accurate step list).

Validated end-to-end on DGX Spark: Nemotron-3-Nano-30B-A3B loads and trains in
16-bit LoRA (load_in_4bit=false).

Signed-off-by: Albert Cui <[email protected]>
@albcui
albcui force-pushed the albcui/customizer/unsloth-nemotron branch from 926b0f0 to 1cae724 Compare June 16, 2026 20:52
@albcui
albcui enabled auto-merge June 16, 2026 20:52
@albcui
albcui added this pull request to the merge queue Jun 16, 2026
Merged via the queue into main with commit 40f0d0d Jun 16, 2026
53 checks passed
@albcui
albcui deleted the albcui/customizer/unsloth-nemotron branch June 16, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants