Skip to content

State Offloading

About

The d9d.core.offload package moves GPU-resident training state to host (CPU) memory and back. It is the base of the sleep / wake API of the Trainer. Sleep frees the GPUs for a colocated workload, for example a rollout engine that shares the GPUs in colocated reinforcement learning. The user entry points Trainer.sleep(), Trainer.wake() and Trainer.is_sleeping() are documented on the Training Loop page. This page covers the primitives they are built on.

Building Blocks

The package defines two things:

  1. The Offloadable protocol: The contract for a subsystem that owns GPU memory and can release and restore it.
  2. The tensor primitives offload_tensor and onload_tensor: they move the storage of one tensor to the host and back. The tensor object, and the DTensor wrapper, stay the same.

The Round-Trip Guarantee

An offload followed by an onload changes nothing observable. Across the round trip:

  • Parameters and buffers keep their object identity.
  • The optimizer keeps its state dict keys and the tensor objects they map to.
  • DTensor wrapper instances keep their device_mesh, placements, global shape, stride and dtype.

Only the device storage is allocated again. So external references stay valid after wake-up. Gradient hooks, optimizer state keyed by parameter and a frozen reference model held by a task all point at the same objects.

The primitives swap the storage in place instead of creating new tensors. For a DTensor, only the storage of the local shard moves. The distributed metadata stays on the wrapper.

Sleep Tags

SleepTag selects the subsystems that Trainer.sleep and Trainer.wake act on:

  • SleepTag.TENSOR_STATES: All GPU tensor state (model parameters and buffers, optimizer state, gradient buckets and the residual loss accumulator). They are offloaded together. DEFAULT_SLEEP_TAGS holds only this tag.
  • SleepTag.COMMS: NCCL process groups. Opt-in and not implemented yet. Requesting it raises NotImplementedError.

Usage

Offloading a Tensor

offload_tensor returns an OffloadedTensor handle. Keep it until you call onload_tensor, and pass the same tensor object to both calls.

import torch
from d9d.core.offload import offload_tensor, onload_tensor

device = torch.device("cuda")
param = torch.randn(4096, 4096, device=device)

# Release the GPU storage; `param` now lives in host memory.
handle = offload_tensor(param, pin_memory=True)
assert param.device.type == "cpu"

# ... a colocated workload runs on the freed GPU ...

# Restore the GPU storage in place; `param` is the same object as before.
onload_tensor(param, handle, device=device)
assert param.device.type == "cuda"

For a DTensor, pass the wrapper. Only its local shard moves:

1
2
3
4
5
6
from torch.distributed.tensor import DTensor

dt: DTensor = ...                          # A sharded parameter
handle = offload_tensor(dt, pin_memory=True)
# dt.device_mesh, dt.placements and dt.shape are unchanged here.
onload_tensor(dt, handle, device=device)

Implementing Offloadable

A subsystem that owns GPU state implements the protocol, so that the Trainer can offload and onload it together with the others. A typical implementation does three things:

  1. Keep the handles in a mirror.
  2. Wait for the asynchronous copies with torch.cuda.synchronize.
  3. Reject a second offload or onload in a row.
import torch
from d9d.core.offload import Offloadable, OffloadContext, OffloadedTensor, OnloadContext, offload_tensor, onload_tensor


class MySubsystem(Offloadable):
    def __init__(self, tensors: list[torch.Tensor]):
        self._tensors = tensors
        self._mirror: dict[int, OffloadedTensor] | None = None

    def offload(self, ctx: OffloadContext) -> None:
        if self._mirror is not None:
            raise RuntimeError("MySubsystem is already offloaded.")
        self._mirror = {id(t): offload_tensor(t, pin_memory=ctx.pin_memory) for t in self._tensors}
        # Wait for the non-blocking device-to-host copies before the storage is freed.
        torch.cuda.synchronize(ctx.dist_context.current_device)

    def onload(self, ctx: OnloadContext) -> None:
        if self._mirror is None:
            raise RuntimeError("MySubsystem is not offloaded.")
        device = ctx.dist_context.current_device
        for t in self._tensors:
            onload_tensor(t, self._mirror[id(t)], device=device)
        torch.cuda.synchronize(device)
        self._mirror = None

    def is_offloaded(self) -> bool:
        return self._mirror is not None

The built-in implementations follow the same pattern: TrackedModules (model parameters and buffers), PipelinedOptimizer (optimizer state) and GradientManager (gradient buckets and the residual loss accumulator).

API Reference

d9d.core.offload

Offloading of GPU training state to host memory and back.

DEFAULT_SLEEP_TAGS = frozenset({SleepTag.TENSOR_STATES}) module-attribute

The default tags for Trainer.sleep and Trainer.wake: tensor state only, no comms.

OffloadContext dataclass

Context passed to Offloadable.offload.

Attributes:

Name Type Description
dist_context DistributedContext

The distributed context the subsystem was built under.

pin_memory bool

Whether to allocate the host buffer in pinned memory.

Offloadable

Bases: Protocol

Protocol for subsystems that own GPU-resident state and can release it to host memory.

An offload followed by an onload must not change anything observable. Parameter identities, optimizer state keys, DTensor instances, placements and dtypes stay the same. Only the device storage is allocated again.

is_offloaded()

Reports whether this subsystem currently has its state in host memory.

Returns:

Type Description
bool

True if the subsystem is offloaded, False otherwise.

offload(ctx)

Releases the GPU memory owned by this subsystem, moving its state to host memory.

Parameters:

Name Type Description Default
ctx OffloadContext

Context for this operation.

required
onload(ctx)

Moves the state that offload released back to GPU memory.

Parameters:

Name Type Description Default
ctx OnloadContext

Context for this operation.

required

OffloadedTensor dataclass

Handle to a tensor whose local storage was moved to host memory.

offload_tensor produces it and onload_tensor consumes it. Subsystems hold it as an opaque handle between an offload and the matching onload.

Attributes:

Name Type Description
host Tensor

The host buffer that holds the local storage of the offloaded tensor.

OnloadContext dataclass

Context passed to Offloadable.onload.

Attributes:

Name Type Description
dist_context DistributedContext

The distributed context the subsystem was built under.

SleepTag

Bases: StrEnum

Subsystems that Trainer.sleep and Trainer.wake act on.

Attributes:

Name Type Description
TENSOR_STATES

All GPU tensor state: model parameters and buffers, optimizer state, gradient buckets and the residual loss accumulator. They are always offloaded together.

COMMS

NCCL process groups. Opt-in. It is not implemented yet, so requesting it raises NotImplementedError.

offload_tensor(tensor, *, pin_memory)

Moves the local storage of tensor to host memory, in place.

The tensor object stays the same, so external references to it stay valid. For a DTensor, only the storage of the local shard moves. The wrapper keeps its device_mesh, placements, global shape and global stride.

Parameters:

Name Type Description Default
tensor Tensor

The device tensor to offload. Can be a plain tensor or a DTensor.

required
pin_memory bool

Whether to allocate the host buffer in pinned memory.

required

Returns:

Type Description
OffloadedTensor

A handle holding the host buffer. Pass it to onload_tensor together with the same tensor.

onload_tensor(tensor, offloaded, *, device)

Moves the local storage of tensor from the host buffer back to device, in place.

The tensor object, and the DTensor wrapper, stay the same instances as before the offload. Only the device storage is allocated again.

Parameters:

Name Type Description Default
tensor Tensor

The tensor that was passed to offload_tensor.

required
offloaded OffloadedTensor

The handle returned by offload_tensor.

required
device device

The device to move the local storage to.

required