Distributed Training¶
TorchEBM is distributed-transparent rather than distributed-aware: components never require an initialized process group, no default forward() or sample() path issues a collective, and the standard PyTorch wrappers do the parallelism. You wrap the energy network, use your own launcher and data sharding, and the components behave identically. Collectives exist only behind explicit opt-ins (process_group= arguments and explicit methods) where the math is batch-global.
FSDP2 (fully_shard with DTensor parameters) is the primary target and the path validated on multi-GPU NCCL hardware. DDP also runs, but drives no design decisions.
The shard-inside pattern¶
Shard the network inside your BaseModel subclass and keep the model object as the user-facing type. Samplers and losses stay unaware of sharding; every first-order path (gradient(), Langevin and HMC chains, CD, implicit EqM) runs through FSDP2's hooks unchanged.
Never wrap samplers or losses; they hold the model and route through its forward. TorchEBMModule resolves model.device and model.dtype correctly from DTensor parameters.
Training loop (torchrun)¶
Three library-specific points, everything else is a standard FSDP2 loop:
- Pass a per-rank
generator. The library holds no hidden RNG state; with a shared seed every rank runs identical chains and you pay for \(N\) GPUs to get one GPU of sample diversity. Generators are device-bound: create them on the compute device. - Reduction semantics. Losses reduce with means over the local batch; with gradient averaging this equals the global-batch mean exactly when per-rank batch sizes are equal (keep
drop_last=True). -
k-step MCMC and resharding. With default reshard-after-forward, every Langevin step re-gathers the parameters. For sampling-only loops (not inside a training forward/backward), hold them gathered:
Score matching: the functional path¶
FSDP2 hooks cannot run the double backward score matching needs: the post-backward hook reshards parameter storage that the second-order graph still references, independent of reshard_after_forward. Score-matching losses therefore take a hook-free functional path under sharding, selected at construction:
The template supplies the module structure for torch.func.functional_call; the sharded DTensor parameters are injected per call and the double backward runs through DTensor's differentiable collectives. Gradients match an unsharded reference with global-batch-mean semantics and land with the parameter's own placement (the loss reduce-scatters the per-rank contributions at accumulation time), so standard optimizers step them directly. The default autograd path fails fast with an actionable error when it sees DTensor parameters.
One dtype caveat: a bf16 MixedPrecisionPolicy affects the hook path only. The functional path reads the fp32 sharded parameters directly, so its compute stays fp32 regardless of the policy.
Objectives that cannot shard (yet)¶
Energy Matching training and explicit EqM energies (energy_type='dot'/'l2'/'mean') backpropagate through an input-gradient built with create_graph=True, the same second-order pattern that breaks under FSDP2 hooks, and no functional rewrite exists for them yet. In training mode with DTensor parameters they raise immediately with the alternatives:
- train with DDP (full replica per GPU; the pattern works there because the single
loss.backward()fires the reducer once), or - train unsharded and shard only for evaluation/sampling, which is first-order and works.
Implicit EqM (the default energy_type='none') is a plain regression and shards fine.
PCD replay buffers across ranks¶
Persistent CD buffers are rank-local by design: each rank keeps independent chains, so the world size multiplies chain diversity at zero cost, and no collective touches the buffer in forward. To occasionally exchange chains between ranks, call the explicit collective between steps:
It gathers the pooled chains, applies one shared permutation (broadcast from rank 0, so per-rank generators cannot desynchronize it), and keeps the local shard: no chain is duplicated or lost.
Global-batch OT coupling¶
Minibatch OT couplings are biased toward the batch size. With a process group, SinkhornCoupling solves on the pooled global batch instead, shrinking that bias at fixed per-rank batch size:
Every rank gathers both batches, solves the identical pooled problem, and keeps its own rows; the row-conditional draw is broadcast from rank 0. Budget for the \((\text{world\_size} \times \text{batch})^2\) cost matrix on every rank. Exact and greedy couplings reject a process group (assignment solvers scale quadratically with the pooled batch); couple becomes a collective all ranks must enter together.
EMA¶
update_ema works on sharded parameters when the EMA model and the training model are sharded identically (same mesh, same policy); each local shard updates in place with no collective.
Checkpointing¶
Sharded state and rank-local state take different routes:
Do not put rank-local buffers into the DCP state dict: its planner deduplicates non-sharded tensors as replicated and silently keeps rank 0's chains. Restore rank-local files with the same world size, or simply re-initialize the buffer from noise (chains re-warm within a few hundred steps).
Large-scale recipe¶
For models too large to materialize on one device, compose the standard FSDP2 ingredients; every piece is validated with TorchEBM components:
With a bf16 policy, gradient() returns gradients in the input's dtype; set model.force_fp32_gradient = True if a low-precision model needs fp32-precision sampler gradients. Combine with the DCP checkpointing above.
Other frameworks¶
The shard-inside pattern is framework-agnostic: anything that wraps an nn.Module composes the same way.
- DDP:
model.net = DistributedDataParallel(model.net). Everything runs, including the second-order objectives that FSDP2 rejects. - DeepSpeed ZeRO-3 / HF Accelerate / Lightning Fabric: wrap or
preparethe inner network, keep theBaseModelfacade outside. The FSDP2 caveats carry over wherever parameters are partitioned: second-order objectives need a replica-style engine (ZeRO-½, DDP) and score matching needs the functional path under partitioning. - Tensor parallel (DTensor): the functional score path requires a 1-D device mesh; TP meshes are untested territory.
Validation status¶
The distributed test suite (pytest tests/distributed) runs 2-process gloo/CPU in CI and validates the contract; the same suite runs under NCCL with one process per GPU via TORCHEBM_DIST_DEVICE=cuda, and benchmarks/distributed_fsdp2.py exercises full CD and functional-DSM training steps at hundred-million-parameter scale on multi-GPU hardware.