Skip to content

opake.distributed

Distributed training utilities for differential privacy with DDP.

Overview

The opake.distributed module provides composable primitives for multi-GPU DP training:

  • Core: is_distributed(), get_rank(), get_world_size()
  • Collectives: all_reduce() (+ in-place variant in the collectives submodule)
  • Gradient aggregation: sum_gradients() (copy-returning) and sum_gradients_() (in-place)
  • State sync: sync() (type-dispatched; handles clipping + noise states and registered DP runtime objects)
  • Sharding: local_shard()

DDP is the only supported parallelism strategy. See User Guide: Distributed Training for usage.

Core Utilities

is_distributed()

Return True if torch.distributed is available and initialized.

Source code in packages/opake-engine/src/opake/api/engine/distributed/collectives.py
def is_distributed() -> bool:
    """Return True if ``torch.distributed`` is available and initialized."""
    return dist.is_available() and dist.is_initialized()

get_rank()

Return the current rank (0 if not distributed).

Source code in packages/opake-engine/src/opake/api/engine/distributed/collectives.py
def get_rank() -> int:
    """Return the current rank (0 if not distributed)."""
    return dist.get_rank() if is_distributed() else 0

get_world_size()

Return the world size (1 if not distributed).

Source code in packages/opake-engine/src/opake/api/engine/distributed/collectives.py
def get_world_size() -> int:
    """Return the world size (1 if not distributed)."""
    return dist.get_world_size() if is_distributed() else 1

all_reduce(tensor, op='sum')

Return a reduced clone of tensor; input is unchanged.

Source code in packages/opake-engine/src/opake/api/engine/distributed/collectives.py
def all_reduce(tensor: torch.Tensor, op: str = "sum") -> torch.Tensor:
    """Return a reduced clone of ``tensor``; input is unchanged."""
    reduced = tensor.clone()
    all_reduce_(reduced, op=op)
    return reduced

all_reduce_(tensor, op='sum')

All-reduce tensor across ranks in place.

Raises:

Type Description
RuntimeError

If torch.distributed is not initialized.

ValueError

If op is not a recognized reduction.

Source code in packages/opake-engine/src/opake/api/engine/distributed/collectives.py
def all_reduce_(tensor: torch.Tensor, op: str = "sum") -> None:
    """All-reduce ``tensor`` across ranks in place.

    Raises:
        RuntimeError: If ``torch.distributed`` is not initialized.
        ValueError: If ``op`` is not a recognized reduction.
    """
    reduce_op = _resolve_op(op)
    if not is_distributed():
        raise OperationError(
            *(
                "torch.distributed is not initialized. "
                "Call torch.distributed.init_process_group() first.",
            )
        )
    dist.all_reduce(tensor, op=reduce_op)

barrier()

Block until every rank reaches this call (no-op if not distributed).

Source code in packages/opake-engine/src/opake/api/engine/distributed/collectives.py
def barrier() -> None:
    """Block until every rank reaches this call (no-op if not distributed)."""
    if is_distributed():
        dist.barrier()

Gradient Aggregation

sum_gradients(gradients)

DP-specific alias for reduce_pytree(op="sum").

Source code in packages/opake-engine/src/opake/api/engine/distributed/gradients.py
def sum_gradients(gradients: Any) -> Any:
    """DP-specific alias for ``reduce_pytree(op="sum")``."""
    return reduce_pytree(gradients, op="sum")

sum_gradients_(gradients)

DP-specific alias for reduce_pytree_(op="sum").

Source code in packages/opake-engine/src/opake/api/engine/distributed/gradients.py
def sum_gradients_(gradients: Any) -> None:
    """DP-specific alias for ``reduce_pytree_(op="sum")``."""
    reduce_pytree_(gradients, op="sum")

reduce_pytree(pytree, op='sum')

Return a pytree with each tensor leaf reduced; input unchanged.

When passed ClippedPytree or NoisedPytree, preserves and updates the wrapper metadata for supported sum and mean reductions. Paired second-moment wrappers recurse into both child streams.

Source code in packages/opake-engine/src/opake/api/engine/distributed/gradients.py
def reduce_pytree(pytree: Any, op: str = "sum") -> Any:
    """Return a pytree with each tensor leaf reduced; input unchanged.

    When passed ``ClippedPytree`` or ``NoisedPytree``, preserves and updates the
    wrapper metadata for supported ``sum`` and ``mean`` reductions.  Paired
    second-moment wrappers recurse into both child streams.
    """
    if isinstance(pytree, SecondMomentClippingOutput):
        return SecondMomentClippingOutput(
            reduce_pytree(pytree.grads, op=op),
            reduce_pytree(pytree.squared_grads, op=op),
        )

    if isinstance(pytree, SecondMomentNoiseOutput):
        return SecondMomentNoiseOutput(
            reduce_pytree(pytree.noisy_grads, op=op),
            reduce_pytree(pytree.noisy_squared_grads, op=op),
        )

    if isinstance(pytree, ClippedPytree):
        _assert_wrapper_reduction_supported(pytree, op)
        _assert_public_metadata_equal(pytree.max_norm, name="ClippedPytree.max_norm")
        if _is_noised(pytree):
            _assert_public_metadata_equal(
                pytree.noise_stddev,
                name="NoisedPytree.noise_stddev",
            )
        reduced = pytree.clone()
        reduce_pytree_(reduced.pytree, op=op)
        return _reduced_metadata(reduced, op, _metadata_world_size())

    def _clone(leaf: Any) -> Any:
        if isinstance(leaf, torch.Tensor):
            return leaf.clone()
        raise InputTypeError(
            *(
                f"reduce_pytree expects tensor leaves after wrapper dispatch; "
                f"got {type(leaf).__name__}. Unwrap paired/custom containers "
                f"explicitly or register a reduction branch.",
            )
        )

    reduced = tree_map(_clone, pytree)
    reduce_pytree_(reduced, op=op)
    return reduced

reduce_pytree_(pytree, op='sum')

All-reduce tensor leaves in place.

In-place wrapper reductions are accepted only when the wrapper metadata stays unchanged. Use :func:reduce_pytree for reductions such as noised sum or clipped/noised mean that need updated metadata.

Source code in packages/opake-engine/src/opake/api/engine/distributed/gradients.py
def reduce_pytree_(pytree: Any, op: str = "sum") -> None:
    """All-reduce tensor leaves in place.

    In-place wrapper reductions are accepted only when the wrapper metadata
    stays unchanged.  Use :func:`reduce_pytree` for reductions such as noised
    ``sum`` or clipped/noised ``mean`` that need updated metadata.
    """
    if isinstance(pytree, SecondMomentClippingOutput):
        reduce_pytree_(pytree.grads, op=op)
        reduce_pytree_(pytree.squared_grads, op=op)
        return

    if isinstance(pytree, SecondMomentNoiseOutput):
        reduce_pytree_(pytree.noisy_grads, op=op)
        reduce_pytree_(pytree.noisy_squared_grads, op=op)
        return

    if isinstance(pytree, ClippedPytree):
        _assert_wrapper_reduction_supported(pytree, op)
        if _in_place_wrapper_metadata_changes(pytree, op):
            raise InputTypeError(
                *(
                    f"In-place {type(pytree).__name__} reduction would change metadata; "
                    "use reduce_pytree() instead.",
                )
            )
        _assert_public_metadata_equal(pytree.max_norm, name="ClippedPytree.max_norm")
        if _is_noised(pytree):
            _assert_public_metadata_equal(
                pytree.noise_stddev,
                name="NoisedPytree.noise_stddev",
            )
        reduce_pytree_(pytree.pytree, op=op)
        return

    if not is_distributed():
        return

    def _reduce(leaf: Any) -> Any:
        if isinstance(leaf, torch.Tensor):
            all_reduce_(leaf, op=op)
            return leaf
        raise InputTypeError(
            *(
                f"reduce_pytree_ expects tensor leaves after wrapper dispatch; "
                f"got {type(leaf).__name__}. Unwrap paired/custom containers "
                f"explicitly or register a reduction branch.",
            )
        )

    tree_map(_reduce, pytree)

State Synchronization

sync(*states)

Synchronize one or more registered state/aux objects across ranks.

Dispatches on object type — exact match first, then __mro__ — to whichever function was registered via :func:register_sync_type. An unregistered type raises TypeError; nothing is passed through unsynchronized.

Returns:

Type Description
Any

A single synchronized object if one argument was passed, otherwise a

Any

tuple matching the input order.

Source code in packages/opake-engine/src/opake/api/engine/distributed/_state.py
def sync(*states: Any) -> Any:
    """Synchronize one or more registered state/aux objects across ranks.

    Dispatches on object type — exact match first, then ``__mro__`` — to
    whichever function was registered via :func:`register_sync_type`.  An
    unregistered type raises ``TypeError``; nothing is passed through
    unsynchronized.

    Returns:
        A single synchronized object if one argument was passed, otherwise a
        tuple matching the input order.
    """

    def _sync_one(single: Any) -> Any:
        state_type = type(single)
        sync_fn = _resolve_sync_fn(state_type)
        if sync_fn is None:
            _ensure_builtin_sync_types_loaded()
            sync_fn = _resolve_sync_fn(state_type)
        if sync_fn is not None:
            return sync_fn(single)
        raise InputTypeError(
            *(
                f"No sync function registered for {state_type.__name__}. "
                f"Registered types: {[t.__name__ for t in _SYNC_REGISTRY]}",
            )
        )

    if len(states) == 1:
        return _sync_one(states[0])
    if len(states) > 1:
        return tuple(_sync_one(s) for s in states)
    return ()

The sync() machinery is type-dispatched: clipping and noise states register themselves with opake.distributed._state.register_sync_type and provide the right per-state aggregation rule. Lower-level scalar reductions, tensor gathers, and object syncs live in _state.py; they're internal plumbing for the registered DP runtime objects rather than headline API.

Privacy Ordering

The order of operations matters for DP guarantees:

grads, clip_state = grad_fn(params, x, y, state=clip_state)  # 1. Clip
grads = dist_utils.sum_gradients(grads)                       # 2. Aggregate (copy)
noisy_grads, noise_state = noise_fn(grads, noise_state)       # 3. Noise

sync accepts one or more objects and returns synchronized values in the same order:

clip_state, aux = dist_utils.sync(clip_state, aux)
noise_state = dist_utils.sync(noise_state)

See examples/distributed_dp_training.py for a complete working script.