Skip to content

feat(archon): add DCP-based HF checkpoint save/load with MoE expert support#849

Merged
garrett4wade merged 1 commit into
mainfrom
rchardx/state_dict
Jan 26, 2026
Merged

feat(archon): add DCP-based HF checkpoint save/load with MoE expert support#849
garrett4wade merged 1 commit into
mainfrom
rchardx/state_dict

Conversation

@rchardx

@rchardx rchardx commented Jan 23, 2026

Copy link
Copy Markdown
Collaborator

Description

Replace manual safetensors handling with PyTorch DCP infrastructure for HuggingFace format checkpoints. Previously, save/load required gathering full state dict to rank 0, causing OOM on large MoE models. Now uses HuggingFaceStorageWriter/Reader for distributed operations.

Key changes:

  • Add MoEStateDictAdapter base class with DTensor-aware expert weight conversion (3D grouped <-> 2D individual), supporting EP and ETP placements via local expert index calculation
  • Fix in-place tensor ops (masked_fill_, scatter_) for torch.compile and checkpointing compatibility
  • Large MoE checkpoint save/load without OOM (no full tensor gather)
  • Correct EP/ETP checkpoint handling with distributed expert weights
  • Incremental weight sync to inference engines via convert_single_to_hf()
  • Foundation for PP support: layer-indexed key mapping and extensible DTensor placement logic for per-stage conversion

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not
    work as expected)
  • Documentation update
  • Code refactoring (no functional changes)
  • Performance improvement
  • Test coverage improvement

Checklist

  • I have read the Contributing Guide
  • I have run formatting tools (pre-commit or manual)
  • I have run relevant unit tests and they pass
  • I have added tests for new functionality
  • I have updated documentation if needed
  • My branch is up to date with main
  • This PR introduces breaking changes (if yes, fill out details below)
  • If this PR changes documentation, I have built and previewed it locally with
    jb build docs
  • No critical issues raised by AI reviewers (/gemini review)

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @rchardx, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement to the checkpointing mechanism for Archon models, particularly focusing on Mixture of Experts (MoE) models. It leverages PyTorch's Distributed Checkpoint (DCP) infrastructure to enable distributed saving and loading of model weights, addressing out-of-memory (OOM) issues that arise when dealing with large models. The changes include a new state dict adapter for MoE models, fixes for torch.compile compatibility, and extensive testing to ensure the correctness and efficiency of the new checkpointing process.

Highlights

  • DCP-based Checkpointing: Implements distributed checkpointing using PyTorch's Distributed Checkpoint (DCP) infrastructure, replacing manual safetensors handling.
  • MoE Support: Adds MoEStateDictAdapter for handling Mixture of Experts (MoE) models, including DTensor-aware expert weight conversion.
  • Performance Improvement: Avoids gathering the full state dict to rank 0 during save/load, preventing OOM errors with large MoE models.
  • Compatibility Fixes: Addresses in-place tensor operations for compatibility with torch.compile and checkpointing.
  • End-to-End Testing: Introduces comprehensive end-to-end tests for checkpointing, covering multi-file safetensors loading, ArchonEngine integration, and weight comparison after conversion.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request includes changes to the archon engine and model, specifically focusing on improving checkpointing and state dictionary adapter functionality for MoE models. It introduces a MoEStateDictAdapter for handling expert weight conversion, adds support for multi-file safetensors loading, and integrates Hugging Face storage writer/reader for saving and loading models. Review comments suggest improvements such as using masked_fill_ for in-place operations, adding checks for dictionary keys before iteration, raising more specific exception types, adding comments to explain code sections, and improving error messages for better debugging.

Comment thread areal/experimental/models/archon/attention.py
Comment thread areal/experimental/models/archon/base.py
Comment thread areal/experimental/models/archon/qwen3/model/state_dict_adapter.py
Comment thread areal/experimental/models/archon/qwen3/model/state_dict_adapter.py
Comment thread areal/tests/experimental/archon/test_checkpoint_e2e.py
Comment thread areal/tests/experimental/archon/torchrun/run_checkpoint_tests.py

Copilot AI 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.

Pull request overview

Adds DCP-backed HuggingFace checkpoint save/load for Archon, including DTensor-aware MoE expert weight conversion to avoid rank-0 full state_dict gathers (and associated OOMs).

Changes:

  • Replaces HF checkpoint save/load in ArchonLMEngine with torch.distributed.checkpoint + HF storage reader/writer.
  • Introduces MoEStateDictAdapter and updates Qwen3/Qwen2 adapters for DTensor-aware MoE expert conversion and multi-file safetensors index support.
  • Adds new distributed/integration tests for DTensor checkpoint roundtrips, multi-file checkpoints, and engine save/load correctness.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
areal/experimental/engine/archon_engine.py Switches HF save/load to DCP + HF storage writer/reader; supports multi-file consolidation.
areal/experimental/models/archon/base.py Adds hf_assets_path + multi-file model.safetensors.index.json parsing and HF storage reader helper.
areal/experimental/models/archon/moe_state_dict_adapter.py New DTensor-aware MoE expert split/concat utilities for distributed checkpoints.
areal/experimental/models/archon/qwen3/model/state_dict_adapter.py Migrates to MoEStateDictAdapter and adds DTensor-aware MoE HF conversion paths.
areal/experimental/models/archon/qwen2/model/state_dict_adapter.py Extends adapter init for hf_assets_path and removes forced DTensor full gathers in HF conversion.
areal/experimental/models/archon/moe/router.py Replaces in-place scatter with out-of-place for compile/checkpointing compatibility.
areal/experimental/models/archon/attention.py Replaces in-place masked_fill with out-of-place for compile/checkpointing compatibility.
areal/experimental/models/archon/activation_checkpoint.py Docstring/comment cleanup.
areal/experimental/models/archon/init.py Exports MoEStateDictAdapter.
areal/tests/experimental/archon/torchrun/dist_utils.py Refactors/shortens docstrings; keeps shared distributed test utilities.
areal/tests/experimental/archon/torchrun/run_ep_tests.py Adds DTensor checkpoint roundtrip tests, placement-type checking, and Qwen2 dense DTensor tests.
areal/tests/experimental/archon/torchrun/run_checkpoint_tests.py New torchrun integration tests for engine HF save/load (dense + MoE).
areal/tests/experimental/archon/test_state_dict_adapter.py Adds unit tests for hf_assets_path, multi-file support, and MoE adapter helpers.
areal/tests/experimental/archon/test_distributed_ep.py Adds multi-GPU DTensor checkpoint roundtrip tests.
areal/tests/experimental/archon/test_checkpoint_e2e.py New end-to-end tests for multi-file index parsing and engine HF save/load.
areal/tests/experimental/archon/torchrun/run_vs_fsdp.py Updates test import to dist_utils.
areal/tests/experimental/archon/torchrun/run_qwen3_parallelize.py Updates test import to dist_utils.
areal/tests/experimental/archon/torchrun/run_parallel_dims.py Updates test import to dist_utils.
areal/tests/experimental/archon/torchrun/run_forward.py Minor comment cleanup.
Comments suppressed due to low confidence (1)

areal/experimental/models/archon/qwen3/model/state_dict_adapter.py:130

  • to_hf() does not strip wrapper name segments (e.g. "._checkpoint_wrapped_module", "._orig_mod") before key conversion, even though convert_single_to_hf() explicitly handles them. Since the new engine save/load path calls adapter.to_hf() on model state dicts, compiled / AC-wrapped models may silently drop keys (hf_key=None) and produce incomplete checkpoints. Suggest normalizing keys inside to_hf() (and any other bulk conversion entrypoints) the same way convert_single_to_hf() does before calling _convert_key_to_hf / MoE splitting.
        for key, value in state_dict.items():
            # Skip output.weight when weight tying is enabled
            # HF will reconstruct lm_head.weight from embed_tokens.weight on load
            if self.enable_weight_tying and key == "output.weight":
                continue

            if "moe.experts.w" in key:
                # Split 3D expert weight into list of 2D weights
                if isinstance(value, DTensor):
                    hf_pairs = self._split_moe_experts_distributed(key, value)
                else:
                    hf_pairs = self._split_moe_experts(key, value)
                hf_state_dict.update(hf_pairs)
            else:
                # Regular key mapping
                hf_key = self._convert_key_to_hf(key)
                if hf_key is not None:
                    hf_state_dict[hf_key] = value

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread areal/experimental/engine/archon_engine.py Outdated
Comment thread areal/tests/experimental/archon/torchrun/run_ep_tests.py Outdated
Comment thread areal/experimental/engine/archon_engine.py Outdated
Comment thread areal/experimental/models/archon/moe_weight_converter.py
@rchardx rchardx force-pushed the rchardx/state_dict branch from 136a73e to 07a1bbd Compare January 23, 2026 18:04
@rchardx rchardx added the safe-to-test Ready to run unit-tests in a PR. label Jan 23, 2026
@rchardx rchardx force-pushed the rchardx/state_dict branch from 07a1bbd to 10344b0 Compare January 23, 2026 19:03
@rchardx rchardx added safe-to-test Ready to run unit-tests in a PR. and removed safe-to-test Ready to run unit-tests in a PR. labels Jan 23, 2026
@rchardx rchardx force-pushed the rchardx/state_dict branch from 10344b0 to 144a9b2 Compare January 24, 2026 02:11
@rchardx rchardx added safe-to-test Ready to run unit-tests in a PR. and removed safe-to-test Ready to run unit-tests in a PR. labels Jan 24, 2026
@rchardx rchardx force-pushed the rchardx/state_dict branch from 144a9b2 to cbbf9d4 Compare January 24, 2026 02:32
@rchardx rchardx added safe-to-test Ready to run unit-tests in a PR. and removed safe-to-test Ready to run unit-tests in a PR. labels Jan 24, 2026
@rchardx rchardx temporarily deployed to AReaL-unittests January 24, 2026 02:36 — with GitHub Actions Inactive
Comment thread areal/experimental/models/archon/moe_state_dict_adapter.py Outdated
Comment thread areal/experimental/models/archon/moe_state_dict_adapter.py Outdated
Comment thread areal/experimental/models/archon/moe_state_dict_adapter.py Outdated
Comment on lines +62 to +76
hf_writer = HuggingFaceStorageWriter(
path=sharded_dir,
save_distributed=True,
fqn_to_index_mapping=fqn_to_index_mapping,
enable_consolidation=False,
)
dcp.save(hf_state_dict, storage_writer=hf_writer)

# NOTE: consolidate_safetensors_files_on_every_rank() has internal barrier
consolidate_safetensors_files_on_every_rank(
input_dir=sharded_dir,
output_dir=path,
fqn_to_index_mapping=fqn_to_index_mapping,
num_threads=8,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What about the performance?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Our benchmark on a 12.55 GB model across 8 GPUs shows that DCP provides a modest 1.10x speedup (11.18s vs 12.27s), but the real advantage is memory efficiency: DCP uses 23x less CPU memory (0.45 GB vs 10.52 GB). The baseline approach with full_state_dict=True gathers the entire model to each rank before saving, consuming ~10.5 GB extra CPU memory per rank—this becomes the bottleneck for larger models where OOM is inevitable. DCP keeps data sharded throughout the save process, using only ~1.57 GB per rank. The time breakdown reveals that DCP's get_state_dict is 7x faster (0.39s vs 2.72s) by avoiding all-gather communication, though the two-phase write (DCP save + consolidate) takes longer than single-rank sequential write. In summary, DCP's primary value is enabling checkpoint saves for large models that would otherwise OOM, with a secondary benefit of slightly better throughput at scale.

Benchmarking: https://gist.github.com/rchardx/bc558b7bf7cab969e4075a71b17ed1a9

torchrun --nproc_per_node=8benchmark_hf_checkpoint_save.py --model-size large --warmup
Creating FSDP2 model: large
  World size: 8
  Estimated size: 12.55 GB
  Per-GPU shard: 1.57 GB
Model created and wrapped with FSDP2
  GPU memory after model init: 1.38 GB

Running warmup...

Running BASELINE benchmark...
Running DCP benchmark...

================================================================================
Model: large
  Layers: 32, Hidden: 4096
  Estimated size: 12.55 GB
  World size: 8 GPUs
  Per-GPU shard size: 1.57 GB
================================================================================

BASELINE (full_state_dict=True, rank 0 saves):
  Time:
    All-gather: 2.72s
    Save (rank 0): 9.54s
    Total: 12.27s
    Throughput: 1.02 GB/s
  Memory (rank 0):
    GPU before: 1.38 GB
    GPU peak (after gather): 1.63 GB
    GPU delta: +0.24 GB
    CPU before: 2.10 GB
    CPU after gather: 12.62 GB
    CPU delta: +10.52 GB

DCP (full_state_dict=False, parallel save + consolidate):
  Time:
    Get state dict: 0.39s
    DCP save: 7.51s
    Consolidate: 3.28s
    Total: 11.18s
    Throughput: 1.12 GB/s
  Memory (rank 0):
    GPU before: 1.38 GB
    GPU peak (after get): 1.38 GB
    GPU delta: +0.00 GB
    CPU before: 2.10 GB
    CPU after get: 2.55 GB
    CPU delta: +0.45 GB

--------------------------------------------------------------------------------
COMPARISON:
--------------------------------------------------------------------------------
  Time speedup (DCP vs Baseline): 1.10x
  CPU memory ratio (Baseline / DCP): 23.29x
    Baseline needs 10.52 GB extra CPU memory
    DCP needs 0.45 GB extra CPU memory

  Theoretical analysis:
    Model size: 12.55 GB
    Baseline gathers full model to each rank -> ~12.55 GB extra memory
    DCP keeps sharded data -> ~1.57 GB per rank
================================================================================

Comment on lines +63 to +103
def _load_safetensors_index(self, hf_assets_path: str) -> None:
"""Load model.safetensors.index.json to support multi-file checkpoint."""
mapping_path = os.path.join(hf_assets_path, "model.safetensors.index.json")
single_file_path = os.path.join(hf_assets_path, "model.safetensors")

try:
with open(mapping_path) as f:
hf_safetensors_index = json.load(f)
except FileNotFoundError:
if not os.path.exists(single_file_path):
logger.warning(
f"model.safetensors.index.json not found at hf_assets_path: {mapping_path}. "
"Defaulting to saving a single safetensors file if checkpoint is saved in HF format"
)
return

if hf_safetensors_index:
self.fqn_to_index_mapping = {}
for hf_key, raw_index in hf_safetensors_index["weight_map"].items():
match = re.search(r"\d+", raw_index)
if match:
self.fqn_to_index_mapping[hf_key] = int(match.group(0))

def get_hf_storage_reader(
self, path: str, from_quantized: bool = False
) -> HuggingFaceStorageReader:
"""Return HuggingFaceStorageReader to read HF checkpoint.

Args:
path: The path to read HF checkpoint from.
from_quantized: Whether loading from quantized checkpoint format.
Note: Loading from quantized format is not supported by default.

Returns:
HuggingFaceStorageReader instance for reading HF checkpoint.
"""
if from_quantized:
logger.warning(
"Loading from quantized checkpoint format is not supported for this model."
)
return HuggingFaceStorageReader(path)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

An abstract class should not implement any informative methods. It should only define the API.

If we indeed want to share some commen methods, we should wrap them in an indepenent class and use the object on-demand in subclasses. Prefer composition over inheritance.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This pattern is known as the Template Method Pattern, where an ABC defines an algorithm skeleton with concrete implementations while deferring certain steps to subclasses via abstract methods. It is widely used in Python's standard library. For example, collections.abc.Mapping only requires subclasses to implement __getitem__, __iter__, and __len__, but provides concrete implementations of get(), keys(), items(), and values() for free. Similarly, collections.abc.Set requires __contains__, __iter__, and __len__, but provides __le__, __and__, isdisjoint(), and other set operations as concrete methods. The shared implementation in BaseStateDictAdapter follows the same pattern and is minimal and stable. If it grows more complex or different subclasses need different implementations, we should revisit this decision and extract an HFAssetsHelper class for composition.

Comment thread areal/tests/experimental/archon/torchrun/run_ep_tests.py Outdated
Comment thread areal/tests/experimental/archon/torchrun/run_ep_tests.py Outdated
Comment thread areal/tests/experimental/archon/test_state_dict_adapter.py
@rchardx rchardx force-pushed the rchardx/state_dict branch 3 times, most recently from 5f19e3f to 3b6bf4b Compare January 26, 2026 06:45
@rchardx rchardx requested a review from garrett4wade January 26, 2026 06:58
…upport

Replace manual safetensors handling with PyTorch DCP infrastructure for
HuggingFace format checkpoints. Previously, save/load required gathering
full state dict to rank 0, causing OOM on large MoE models. Now uses
HuggingFaceStorageWriter/Reader for distributed operations.

Key changes:
- Add MoEStateDictAdapter base class with DTensor-aware expert weight
  conversion (3D grouped <-> 2D individual), supporting EP and ETP
  placements via local expert index calculation
- Fix in-place tensor ops (masked_fill_, scatter_) for torch.compile and
  checkpointing compatibility
- Large MoE checkpoint save/load without OOM (no full tensor gather)
- Correct EP/ETP checkpoint handling with distributed expert weights
- Incremental weight sync to inference engines via convert_single_to_hf()
- Foundation for PP support: layer-indexed key mapping and extensible
  DTensor placement logic for per-stage conversion
@rchardx rchardx force-pushed the rchardx/state_dict branch from 3b6bf4b to b5c5393 Compare January 26, 2026 07:14
@rchardx rchardx removed the safe-to-test Ready to run unit-tests in a PR. label Jan 26, 2026
@rchardx rchardx added the safe-to-test Ready to run unit-tests in a PR. label Jan 26, 2026
@rchardx rchardx temporarily deployed to AReaL-unittests January 26, 2026 07:17 — with GitHub Actions Inactive

@garrett4wade garrett4wade left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. Waiting for CI

@garrett4wade garrett4wade merged commit 38238e3 into main Jan 26, 2026
7 checks passed
@garrett4wade garrett4wade deleted the rchardx/state_dict branch January 26, 2026 08:41
leandermaben pushed a commit to leandermaben/AReaL that referenced this pull request Mar 24, 2026
…upport (areal-project#849)

Replace manual safetensors handling with PyTorch DCP infrastructure for
HuggingFace format checkpoints. Previously, save/load required gathering
full state dict to rank 0, causing OOM on large MoE models. Now uses
HuggingFaceStorageWriter/Reader for distributed operations.

Key changes:
- Add MoEStateDictAdapter base class with DTensor-aware expert weight
  conversion (3D grouped <-> 2D individual), supporting EP and ETP
  placements via local expert index calculation
- Fix in-place tensor ops (masked_fill_, scatter_) for torch.compile and
  checkpointing compatibility
- Large MoE checkpoint save/load without OOM (no full tensor gather)
- Correct EP/ETP checkpoint handling with distributed expert weights
- Incremental weight sync to inference engines via convert_single_to_hf()
- Foundation for PP support: layer-indexed key mapping and extensible
  DTensor placement logic for per-stage conversion
SathyaGnanakumar pushed a commit to danielkiely/AReaL that referenced this pull request Apr 29, 2026
…upport (areal-project#849)

Replace manual safetensors handling with PyTorch DCP infrastructure for
HuggingFace format checkpoints. Previously, save/load required gathering
full state dict to rank 0, causing OOM on large MoE models. Now uses
HuggingFaceStorageWriter/Reader for distributed operations.

Key changes:
- Add MoEStateDictAdapter base class with DTensor-aware expert weight
  conversion (3D grouped <-> 2D individual), supporting EP and ETP
  placements via local expert index calculation
- Fix in-place tensor ops (masked_fill_, scatter_) for torch.compile and
  checkpointing compatibility
- Large MoE checkpoint save/load without OOM (no full tensor gather)
- Correct EP/ETP checkpoint handling with distributed expert weights
- Incremental weight sync to inference engines via convert_single_to_hf()
- Foundation for PP support: layer-indexed key mapping and extensible
  DTensor placement logic for per-stage conversion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe-to-test Ready to run unit-tests in a PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants