Skip to content

Model State I/O

About

The d9d.model_state.io package reads and writes model checkpoints. It is built on the d9d.model_state.mapper framework, so it can transform model states on the fly while it streams them. It does not load the whole checkpoint into memory at once.

Checkpoint Format

d9d uses the Hugging Face checkpoint format. Parameter tensors live in sharded model-00001-of-XXXXX.safetensors files. The model.safetensors.index.json file maps every tensor to its file. The reader also accepts a single unindexed model.safetensors file.

Why I/O Supports Transformations

In d9d, reading and writing model states always goes through a mapper. This decouples the checkpoint architecture (how weights are stored on disk) from the model architecture (how weights are used in PyTorch code).

This matters for:

  • Hugging Face compatibility: Your model can use a single packed qkv_proj tensor and still read community checkpoints that store q_proj, k_proj and v_proj separately. The mapper stacks them while it reads. You do not need conversion scripts or converted copies of large models.
  • Runtime structure changes, e.g. LoRA: Adapters such as LoRA wrap the original layers. A some_linear.weight key on disk then needs to load into some_linear.base.weight. The mapper reroutes these keys without loading the full state dict first.

How Loading Works

Standard loading reads the whole state dict into CPU RAM, processes it and moves the results to the GPU. This uses a lot of memory and CPU-GPU transfers, and every pipeline-parallel rank repeats the same work.

d9d loads differently:

  • Streaming: A tensor stays in memory only until its mapper group runs. For example, once the "stack q, k, v" group runs, d9d frees the source tensors.
  • Selective reading: The reader inspects the ModelStateMapper and reads only the files that contain its inputs.

How Saving Works

Standard saving often gathers all parameters on a single rank, which can run out of memory. It can also require manual handling of file names and indices across many GPUs.

d9d saves differently:

  • Streaming: A source tensor stays in memory only until its mapper group runs. Output tensors stay in memory only until they are flushed to a .safetensors file.
  • Distributed writing: Besides local saving, the writer accepts a ProcessGroup or a DeviceMesh. With pipeline parallelism, only one rank per pipeline stage writes, so every parameter is written exactly once.

Usage

These examples load and save model states without transforming them. For complex mappings, see the Model State Mapper page.

Raw I/O: Streamed Loading

This example reads a checkpoint tensor by tensor.

from pathlib import Path
from d9d.model_state.io import read_model_state
from d9d.model_state.mapper.adapters import identity_mapper_from_module

model = ...

# Load every state of the model unchanged.
mapper = identity_mapper_from_module(model)

# src_dir must contain either sharded .safetensors files with model.safetensors.index.json,
# or a single unindexed model.safetensors.
loader_stream = read_model_state(
    src_dir=Path("./checkpoint"),
    mapper=mapper,
    device="cpu"  # Or "cuda:0"
)

state_dict = {}
for name, tensor in loader_stream:
    print(f"Loaded and transformed: {name} -> {tensor.shape}")
    state_dict[name] = tensor

Raw I/O: Streamed Saving

This example saves a model locally in shards of at most 1 GiB.

from pathlib import Path
from d9d.model_state.io import write_model_state_local
from d9d.model_state.mapper.adapters import identity_mapper_from_module

model = ...

# The writer creates the shard files and the index file.
write_model_state_local(
    dest_dir=Path("./output_checkpoint"),
    mapper=identity_mapper_from_module(model),
    state_generator=model.state_dict().items(),
    shard_size_gb=1.0
)

Raw I/O: Distributed Checkpoint Conversion

You can convert a checkpoint offline on a cluster. For example, you can convert a Hugging Face checkpoint into a format with packed qkv tensors. You do not need a single machine with enough RAM for the whole model. Each of N GPUs processes 1/N of the keys and writes its part of a new sharded checkpoint.

import torch.distributed as dist
from pathlib import Path
from d9d.model_state.io import read_model_state, write_model_state_distributed
from d9d.model_state.mapper.compose import ModelStateMapperShard
from d9d.model_state.mapper.adapters import identity_mapper_from_mapper_outputs

dist.init_process_group("nccl")
rank = dist.get_rank()
world_size = dist.get_world_size()

# Describe how the ENTIRE model is converted,
# e.g. "stack q, k, v", "rename MLP", "load everything else as is".
mapper = build_my_custom_mapper()

# Each rank processes only its share of the dependency groups,
# so no two ranks load, process or save the same tensors.
local_work_mapper = ModelStateMapperShard(
    sub_mapper=mapper,
    total_shards=world_size,
    current_shard=rank
)

# read_model_state yields tensors that already have their target names and shapes,
# so the writer only passes them through.
writer_mapper = identity_mapper_from_mapper_outputs(local_work_mapper)

# The reader loads and transforms the source tensors, and the writer saves them.
# Rank 0 then writes model.safetensors.index.json for all ranks and gives the files their final names.
write_model_state_distributed(
    dest_dir=Path("./converted_checkpoint"),
    mapper=writer_mapper,
    state_generator=read_model_state(
        src_dir=Path("./original_checkpoint"),
        mapper=local_work_mapper,
        device="cuda",
        show_progress=False  # Hide reader progress bars to keep the output readable
    ),
    process_group=dist.group.WORLD,
    shard_size_gb=4.0,
    show_progress=True
)

PyTorch Module I/O: Streamed Loading

This example loads a checkpoint whose keys match the model keys. identity_mapper_from_module makes sure that only the model's own states are loaded.

from pathlib import Path
from d9d.model_state.io import load_model_state
from d9d.model_state.mapper.adapters import identity_mapper_from_module

model = ...

# Load every key of the model as is.
mapper = identity_mapper_from_module(model)

load_model_state(
    src_dir=Path("./checkpoints/v1"),
    mapper=mapper,
    device="cuda",
    model=model
)

PyTorch Module I/O: Streamed Saving with a DeviceMesh

save_model_state_pipeline_parallel saves a model that is distributed over a PyTorch DeviceMesh with any parallelism dimensions:

  • DTensor gathering: It gathers DTensor shards into full tensors before writing.
  • One writer per pipeline stage: With data or tensor parallelism, several GPUs hold copies or shards of the same parameters. Only the rank at coordinate 0 of every non-pipeline dimension writes to disk.
  • Index merging: Each pipeline rank writes its own files. Pipeline rank 0 then merges their indices into one global index file.

Every rank must call the function, because the gathering is collective.

from pathlib import Path

from torch.distributed.device_mesh import init_device_mesh
from d9d.model_state.io import save_model_state_pipeline_parallel
from d9d.model_state.mapper.compose import ModelStateMapperParallel
from d9d.model_state.mapper.adapters import identity_mapper_from_module

# pp=2 (pipeline), dp=2 (data), tp=2 (tensor).
mesh = init_device_mesh("cuda", (2, 2, 2), mesh_dim_names=("pp", "dp", "tp"))

# Each pipeline rank holds two stages of the model.
my_stages = [TransformerStage(...), TransformerStage(...)]

# Combine the states of all local stages into one mapper.
mapper = ModelStateMapperParallel([
    identity_mapper_from_module(stage) for stage in my_stages
])

save_model_state_pipeline_parallel(
    dest_dir=Path("./checkpoint"),
    mapper=mapper,
    device_mesh=mesh,
    pipeline_dim_name="pp",
    models=my_stages,
    shard_size_gb=4.0
)

API Reference

d9d.model_state.io

Streaming readers and writers for model checkpoints.

load_model_state(src_dir, mapper, device, model, show_progress=True, position=None)

Streams a checkpoint from disk into a PyTorch module.

  1. Mapping: mapper renames, stacks or reshapes on-disk states into model states.
  2. Distribution: If a model state is a DTensor, the loaded tensor is distributed to match its device mesh and placements.
  3. Injection: Each transformed state is loaded into model with load_state_dict as soon as it is ready.

Only states listed in the outputs of mapper are loaded. To load every model state unchanged, build the mapper with d9d.model_state.mapper.adapters.identity_mapper_from_module.

Parameters:

Name Type Description Default
src_dir Path

Directory containing the checkpoint: either sharded .safetensors files described by a model.safetensors.index.json file, or a single unindexed model.safetensors file.

required
mapper ModelStateMapper

The mapper from on-disk keys to model keys.

required
device str

The device to load tensors onto, e.g. "cpu" or "cuda".

required
model Module

The model instance to load weights into.

required
show_progress bool

Whether to display the loading progress bar.

True
position int | None

Row index for the tqdm bar. Pass the process local rank to stack one bar per rank without interleaving. None lets tqdm use its default (single bar).

None

read_model_state(src_dir, mapper, device, show_progress=True, position=None)

Reads a model checkpoint from disk and transforms it on the fly with a state mapper.

Only files that hold inputs of mapper are read. A tensor stays in memory only until the mapper has processed its group.

Parameters:

Name Type Description Default
src_dir Path

The directory containing the checkpoint: either sharded .safetensors files described by a model.safetensors.index.json file, or a single unindexed model.safetensors file.

required
mapper ModelStateMapper

The mapper from on-disk keys to output keys.

required
device str

The device to load tensors onto, e.g. "cpu" or "cuda:0".

required
show_progress bool

Whether to display a progress bar.

True
position int | None

Row index for the tqdm bar. Pass the process local rank to stack one bar per rank without interleaving. None lets tqdm use its default (single bar).

None

Yields:

Type Description
Iterable[tuple[str, Tensor]]

Pairs of transformed state name and tensor.

save_model_state(dest_dir, mapper, model, shard_size_gb=4.0, show_progress=True)

Saves a PyTorch module to disk from a single process.

DTensor states are gathered into full tensors before saving.

Only states listed in the inputs of mapper are saved. To save every model state unchanged, build the mapper with d9d.model_state.mapper.adapters.identity_mapper_from_module.

Parameters:

Name Type Description Default
dest_dir Path

The directory to save .safetensors shards and the index to.

required
mapper ModelStateMapper

The mapper from model keys to on-disk keys.

required
model Module

The PyTorch module to save.

required
shard_size_gb float

Maximum size of a shard file in GiB.

4.0
show_progress bool

Whether to display a progress bar.

True

save_model_state_pipeline_parallel(dest_dir, mapper, device_mesh, pipeline_dim_name, models, shard_size_gb=4.0, show_progress=True, position=None)

Saves a pipeline-parallel model to disk.

Every rank must call this function.

  1. Gathering: DTensor states are gathered into full tensors before saving.
  2. Single writer: For each pipeline stage, only one rank writes files, so ranks do not overwrite each other.
  3. Index merging: The indices of all pipeline stages are merged into one global index file.

Only states listed in the inputs of mapper are saved. To save every model state unchanged, build the mapper with d9d.model_state.mapper.adapters.identity_mapper_from_module.

Parameters:

Name Type Description Default
dest_dir Path

The directory to save .safetensors shards and the index to.

required
mapper ModelStateMapper

The mapper from model keys to on-disk keys.

required
device_mesh DeviceMesh

The device mesh of the whole job.

required
pipeline_dim_name str

The name of the pipeline-parallel dimension in device_mesh.

required
models list[Module]

The modules (pipeline stages) held by this rank.

required
shard_size_gb float

Maximum size of a shard file in GiB.

4.0
show_progress bool

Whether to display a progress bar.

True
position int | None

Row index for the tqdm bar. Pass the process local rank to stack one bar per rank without interleaving. None lets tqdm use its default (single bar).

None

write_model_state_distributed(dest_dir, mapper, state_generator, process_group, shard_size_gb=4.0, show_progress=True, position=None)

Saves model states from multiple processes.

A source tensor stays in memory only until all inputs of its mapper group arrive and the group is processed.

Every rank in process_group must call this function and writes its own shards. Rank 0 gathers the indices and finalizes the checkpoint.

Parameters:

Name Type Description Default
dest_dir Path

Destination directory.

required
mapper ModelStateMapper

Mapping to apply to states before saving.

required
state_generator Iterable[tuple[str, Tensor]]

Stream of (name, tensor) pairs from the model.

required
process_group ProcessGroup

The process group of all writing ranks.

required
shard_size_gb float

Maximum shard size in GiB.

4.0
show_progress bool

Whether to show the progress bar.

True
position int | None

Row index for the tqdm bar. Pass the process local rank to stack one bar per rank without interleaving. None lets tqdm use its default (single bar).

None

write_model_state_local(dest_dir, mapper, state_generator, shard_size_gb=4.0, show_progress=True)

Saves model states to disk in a single local process.

A source tensor stays in memory only until all inputs of its mapper group arrive and the group is processed.

Parameters:

Name Type Description Default
dest_dir Path

Destination directory.

required
mapper ModelStateMapper

Mapping to apply to states before saving.

required
state_generator Iterable[tuple[str, Tensor]]

Stream of (name, tensor) pairs to save.

required
shard_size_gb float

Maximum size of a single .safetensors file in GiB.

4.0
show_progress bool

Whether to show the progress bar.

True

write_model_state_pipeline_parallel(dest_dir, mapper, state_generator, device_mesh, pipeline_dim_name, shard_size_gb=4.0, show_progress=True, position=None)

Saves model states from a pipeline-parallel setup with any other mesh dimensions.

A source tensor stays in memory only until all inputs of its mapper group arrive and the group is processed.

Every rank in device_mesh must call this function. Only one rank per pipeline stage writes to disk, so no data is written twice.

Parameters:

Name Type Description Default
dest_dir Path

Destination directory.

required
mapper ModelStateMapper

Mapping to apply to states before saving.

required
state_generator Iterable[tuple[str, Tensor]]

Stream of (name, tensor) pairs from the model.

required
device_mesh DeviceMesh

The device mesh of the whole job.

required
pipeline_dim_name str

The name of the pipeline-parallel dimension in device_mesh.

required
shard_size_gb float

Maximum shard size in GiB.

4.0
show_progress bool

Whether to show the progress bar.

True
position int | None

Row index for the tqdm bar. Pass the process local rank to stack one bar per rank without interleaving. None lets tqdm use its default (single bar).

None

Raises:

Type Description
ValueError

If device_mesh has no dimension names or does not contain the current rank.