.. _ddp:

Distributed Data Parallel
=========================

.. warning::
  The implementation of :class:`torch.nn.parallel.DistributedDataParallel`
  evolves over time. This design note is written based on the state as of v1.4.


:class:`torch.nn.parallel.DistributedDataParallel` (DDP) transparently performs
distributed data parallel training. This page describes how it works and reveals
implementation details.

Example
^^^^^^^

Let us start with a simple :class:`torch.nn.parallel.DistributedDataParallel`
example. This example uses a :class:`torch.nn.Linear` as the local model, wraps
it with DDP, and then runs one forward pass, one backward pass, and an optimizer
step on the DDP model. After that, parameters on the local model will be
updated, and all models on different processes should be exactly the same.

.. code::

    import torch
    import torch.distributed as dist
    import torch.multiprocessing as mp
    import torch.nn as nn
    import torch.optim as optim
    from torch.nn.parallel import DistributedDataParallel as DDP


    def example(rank, world_size):
        # create default process group
        dist.init_process_group("gloo", rank=rank, world_size=world_size)
        # create local model
        model = nn.Linear(10, 10).to(rank)
        # construct DDP model
        ddp_model = DDP(model, device_ids=[rank])
        # define loss function and optimizer
        loss_fn = nn.MSELoss()
        optimizer = optim.SGD(ddp_model.parameters(), lr=0.001)

        # forward pass
        outputs = ddp_model(torch.randn(20, 10).to(rank))
        labels = torch.randn(20, 10).to(rank)
        # backward pass
        loss_fn(outputs, labels).backward()
        # update parameters
        optimizer.step()

    def main():
        world_size = 2
        mp.spawn(example,
            args=(world_size,),
            nprocs=world_size,
            join=True)

    if __name__=="__main__":
        # Environment variables which need to be
        # set when using c10d's default "env"
        # initialization mode.
        os.environ["MASTER_ADDR"] = "localhost"
        os.environ["MASTER_PORT"] = "29500"
        main()

DDP works with TorchDynamo.  When used with TorchDynamo, apply the DDP model wrapper
before compiling the model, such that torchdynamo can apply ``DDPOptimizer``
(graph-break optimizations) based on DDP bucket sizes.  (See `TorchDynamo DDPOptimizer <./ddp.html#torchdynamo-ddpoptimizer>`_ for more information.)

TorchDynamo support for DDP currently requires setting `static_graph=False`, due to
interactions between the graph tracing process and DDP's mechanism for observing operations happening on its module,
but this should be fixed ultimately.

.. code::

        ddp_model = DDP(model, device_ids=[rank])
        ddp_model = torch.compile(ddp_model)

Internal Design
^^^^^^^^^^^^^^^

This section reveals how it works under the hood of
:class:`torch.nn.parallel.DistributedDataParallel` by diving into details of
every step in one iteration.

- **Prerequisite**: DDP relies on c10d ``ProcessGroup`` for communications.
  Hence, applications must create ``ProcessGroup`` instances before constructing
  DDP.
- **Construction**: The DDP constructor takes a reference to the local module,
  and broadcasts ``state_dict()`` from the process with rank 0 to all other
  processes in the group to make sure that all model replicas start from the
  exact same state. Then, each DDP process creates a local ``Reducer``, which
  later will take care of the gradients synchronization during the backward
  pass. To improve communication efficiency, the ``Reducer`` organizes parameter
  gradients into buckets, and reduces one bucket at a time. Bucket size can be
  configured by setting the `bucket_cap_mb` argument in DDP constructor. The
  mapping from parameter gradients to buckets is determined at the construction
  time, based on the bucket size limit and parameter sizes. Model parameters are
  allocated into buckets in (roughly) the reverse order of
  ``Model.parameters()`` from the given model. The reason for using the reverse
  order is because DDP expects gradients to become ready during the backward
  pass in approximately that order. The figure below shows an example. Note
  that, the ``grad0`` and ``grad1`` are in ``bucket1``, and the other two
  gradients are in ``bucket0``. Of course, this assumption might not always
  be true, and when that happens it could hurt DDP backward speed as the
  ``Reducer`` cannot kick off the communication at the earliest possible time.
  Besides bucketing, the ``Reducer`` also registers autograd hooks during
  construction, one hook per parameter. These hooks will be triggered during
  the backward pass when the gradient becomes ready.
- **Forward Pass**: The DDP takes the input and passes it to the local model,
  and then analyzes the output from the local model if
  ``find_unused_parameters`` is set to ``True``. This mode allows running
  backward on a subgraph of the model, and DDP finds out which parameters are
  involved in the backward pass by traversing the autograd graph from the model
  output and marking all unused parameters as ready for reduction. During the
  backward pass, the ``Reducer`` would only wait for unready parameters, but it
  would still reduce all buckets. Marking a parameter gradient as ready does not
  help DDP skip buckets as for now, but it will prevent DDP from waiting for
  absent gradients forever during the backward pass. Note that traversing the
  autograd graph introduces extra overheads, so applications should only set
  ``find_unused_parameters`` to ``True`` when necessary.
- **Backward Pass**: The ``backward()`` function is directly invoked on the loss
  ``Tensor``, which is out of DDP's control, and DDP uses autograd hooks
  registered at construction time to trigger gradients synchronizations. When
  one gradient becomes ready, its corresponding DDP hook on that grad
  accumulator will fire, and DDP will then mark that parameter gradient as
  ready for reduction. When gradients in one bucket are all ready, the
  ``Reducer`` kicks off an asynchronous ``allreduce`` on that bucket to
  calculate mean of gradients across all processes. When all buckets are ready,
  the ``Reducer`` will block waiting for all ``allreduce`` operations to finish.
  When this is done, averaged gradients are written to the ``param.grad`` field
  of all parameters. So after the backward pass, the `grad` field on the same
  corresponding parameter across different DDP processes should be the same.
- **Optimizer Step**: From the optimizer's perspective, it is optimizing a local
  model. Model replicas on all DDP processes can keep in sync because they all
  start from the same state and they have the same averaged gradients in
  every iteration.


.. image:: https://user-images.githubusercontent.com/16999635/72401724-d296d880-371a-11ea-90ab-737f86543df9.png
    :alt: ddp_grad_sync.png
    :width: 700 px

.. note::
  DDP requires ``Reducer`` instances on all processes to invoke ``allreduce``
  in exactly the same order, which is done by always running ``allreduce``
  in the bucket index order instead of actual bucket ready order. Mismatched
  ``allreduce`` order across processes can lead to wrong results or DDP backward
  hang.

Implementation
^^^^^^^^^^^^^^

Below are pointers to the DDP implementation components. The stacked graph shows
the structure of the code.

ProcessGroup
------------

- `ProcessGroup.hpp <https://github.com/pytorch/pytorch/blob/v1.7.0/torch/lib/c10d/ProcessGroup.hpp>`__:
  contains the abstract API of all process group implementations. The ``c10d``
  library provides 3 implementations out of the box, namely,
  `ProcessGroupGloo`, `ProcessGroupNCCL`, and `ProcessGroupMPI`.
  ``DistributedDataParallel`` uses ``ProcessGroup::broadcast()`` to send
  model states from the process with rank 0 to others during initialization
  and ``ProcessGroup::allreduce()`` to sum gradients.


- `Store.hpp <https://github.com/pytorch/pytorch/blob/v1.7.0/torch/lib/c10d/Store.hpp>`__:
  assists the rendezvous service for process group instances to find each other.

DistributedDataParallel
-----------------------

- `distributed.py <https://github.com/pytorch/pytorch/blob/v1.7.0/torch/nn/parallel/distributed.py>`__:
  is the Python entry point for DDP. It implements the initialization steps and
  the ``forward`` function for the ``nn.parallel.DistributedDataParallel``
  module which call into C++ libraries. Its ``_sync_param`` function performs
  intra-process parameter synchronization when one DDP process works on multiple
  devices, and it also broadcasts model buffers from the process with rank 0 to
  all other processes. The inter-process parameter synchronization happens in
  ``Reducer.cpp``.

- `comm.h <https://github.com/pytorch/pytorch/blob/v1.7.0/torch/csrc/distributed/c10d/comm.h>`__:
  implements the coalesced broadcast helper function which is invoked to
  broadcast model states during initialization and synchronize model buffers
  before the forward pass.

- `reducer.h <https://github.com/pytorch/pytorch/blob/v1.7.0/torch/csrc/distributed/c10d/reducer.h>`__:
  provides the core implementation for gradient synchronization in the backward
  pass. It has three entry point functions:

  * ``Reducer``: The constructor is called in ``distributed.py`` which registers
    ``Reducer::autograd_hook()`` to gradient accumulators.
  * ``autograd_hook()`` function will be invoked by the autograd engine when
    a gradient becomes ready.
  * ``prepare_for_backward()`` is called at the end of DDP forward pass in
    ``distributed.py``. It traverses the autograd graph to find unused
    parameters when ``find_unused_parameters`` is set to ``True`` in DDP
    constructor.

.. image:: https://user-images.githubusercontent.com/16999635/72313120-4e7c1c80-3658-11ea-9c6d-44336b2daeac.png
    :alt: ddp_code.png
    :width: 400 px


TorchDynamo DDPOptimizer
------------------------

DDP's performance advantage comes from overlapping allreduce collectives with computations during backwards.
AotAutograd prevents this overlap when used with TorchDynamo for compiling a whole forward and whole backward graph,
because allreduce ops are launched by autograd hooks _after_ the whole optimized backwards computation finishes.

TorchDynamo's DDPOptimizer helps by breaking the forward graph at the logical boundaries of DDP's allreduce buckets
during backwards.  Note: the goal is to break the graph during backwards, and the simplest implementation is to
break the forward graphs and then call AotAutograd and compilation on each section.  This allows DDP's allreduce hooks
to fire in-between sections of backwards, and schedule communications to overlap with compute.

See `this blog post <https://dev-discuss.pytorch.org/t/torchdynamo-update-9-making-ddp-work-with-torchdynamo/860/1>`_ for
a more in-depth explanation and experimental results, or read the docs and code at
`torch/_dynamo/optimizations/distributed.py <https://github.com/pytorch/pytorch/blob/4908a12542798a3e8641faae6b74f068fdfc6778/torch/_dynamo/optimizations/distributed.py#L56>`_

To Debug DDPOptimizer, set `torch._dynamo.config.log_level` to DEBUG (for full graph dumps) or INFO
(for basic info about bucket boundaries).  To disable DDPOptimizer, set `torch._dynamo.config.optimize_ddp=False`.
DDP and TorchDynamo should still work correctly without DDPOptimizer, but with performance degradation.
