Distributed communication package - tensorplay.distributed
Note
Please refer to TensorPlay Distributed Overview for a brief introduction to all features related to distributed training.
Backends
tensorplay.distributed supports four built-in backends, each with
different capabilities. The table below shows which functions are available
for use with a CPU or GPU for each backend. For NCCL, GPU refers to CUDA GPU
while for XCCL to XPU GPU.
MPI supports CUDA only if the implementation used to build TensorPlay supports it.
Backend |
|
|
|
|
||||
|---|---|---|---|---|---|---|---|---|
Device |
CPU |
GPU |
CPU |
GPU |
CPU |
GPU |
CPU |
GPU |
send |
✓ |
✘ |
✓ |
? |
✘ |
✓ |
✘ |
✓ |
recv |
✓ |
✘ |
✓ |
? |
✘ |
✓ |
✘ |
✓ |
broadcast |
✓ |
✓ |
✓ |
? |
✘ |
✓ |
✘ |
✓ |
all_reduce |
✓ |
✓ |
✓ |
? |
✘ |
✓ |
✘ |
✓ |
reduce |
✓ |
✓ |
✓ |
? |
✘ |
✓ |
✘ |
✓ |
all_gather |
✓ |
✓ |
✓ |
? |
✘ |
✓ |
✘ |
✓ |
gather |
✓ |
✓ |
✓ |
? |
✘ |
✓ |
✘ |
✓ |
scatter |
✓ |
✓ |
✓ |
? |
✘ |
✓ |
✘ |
✓ |
reduce_scatter |
✓ |
✓ |
✘ |
✘ |
✘ |
✓ |
✘ |
✓ |
all_to_all |
✘ |
✘ |
✓ |
? |
✘ |
✓ |
✘ |
✓ |
barrier |
✓ |
✘ |
✓ |
? |
✘ |
✓ |
✘ |
✓ |
Backends that come with TensorPlay
TensorPlay distributed package supports Linux (stable), macOS (stable), and Windows (prototype). By default for Linux, the Gloo and NCCL backends are built and included in TensorPlay distributed (NCCL only when building with CUDA). MPI is an optional backend that can only be included if you build TensorPlay from source. (e.g. building TensorPlay on a host that has MPI installed.)
Note
As of TensorPlay v1.8, Windows supports all collective communications backends but NCCL,
If the init_method argument of init_process_group() points to a file it must adhere
to the following schema:
Local file system,
init_method="file:///d:/tmp/some_file"Shared file system,
init_method="file://////{machine_name}/{share_folder_name}/some_file"Same as on Linux platform, you can enable TcpStore by setting environment variables, MASTER_ADDR and MASTER_PORT.
Which backend to use?
In the past, we were often asked: “which backend should I use?”.
Rule of thumb
Use the NCCL backend for distributed training with CUDA GPU.
Use the XCCL backend for distributed training with XPU GPU.
Use the Gloo backend for distributed training with CPU.
GPU hosts with InfiniBand interconnect
Use NCCL, since it’s the only backend that currently supports InfiniBand and GPUDirect.
GPU hosts with Ethernet interconnect
Use NCCL, since it currently provides the best distributed GPU training performance, especially for multiprocess single-node or multi-node distributed training. If you encounter any problem with NCCL, use Gloo as the fallback option. (Note that Gloo currently runs slower than NCCL for GPUs.)
CPU hosts with InfiniBand interconnect
If your InfiniBand has enabled IP over IB, use Gloo, otherwise, use MPI instead. We are planning on adding InfiniBand support for Gloo in the upcoming releases.
CPU hosts with Ethernet interconnect
Use Gloo, unless you have specific reasons to use MPI.
Choosing the network interface to use
By default, both the NCCL and Gloo backends will try to find the right network interface to use. If the automatically detected interface is not correct, you can override it using the following environment variables (applicable to the respective backend):
NCCL_SOCKET_IFNAME, for example
export NCCL_SOCKET_IFNAME=eth0GLOO_SOCKET_IFNAME, for example
export GLOO_SOCKET_IFNAME=eth0If you’re using the Gloo backend, you can specify multiple interfaces by separating them by a comma, like this:export GLOO_SOCKET_IFNAME=eth0,eth1,eth2,eth3. The backend will dispatch operations in a round-robin fashion across these interfaces. It is imperative that all processes specify the same number of interfaces in this variable.
Other NCCL environment variables
Debugging - in case of NCCL failure, you can set NCCL_DEBUG=INFO to print an explicit
warning message as well as basic NCCL initialization information.
You may also use NCCL_DEBUG_SUBSYS to get more details about a specific
aspect of NCCL. For example, NCCL_DEBUG_SUBSYS=COLL would print logs of
collective calls, which may be helpful when debugging hangs, especially those
caused by collective type or message size mismatch. In case of topology
detection failure, it would be helpful to set NCCL_DEBUG_SUBSYS=GRAPH
to inspect the detailed detection result and save as reference if further help
from NCCL team is needed.
Performance tuning - NCCL performs automatic tuning based on its topology detection to save users’
tuning effort. On some socket-based systems, users may still try tuning
NCCL_SOCKET_NTHREADS and NCCL_NSOCKS_PERTHREAD to increase socket
network bandwidth. These two environment variables have been pre-tuned by NCCL
for some cloud providers, such as AWS or GCP.
For a full list of NCCL environment variables, please refer to
NVIDIA NCCL’s official documentation
You can tune NCCL communicators even further using tensorplay.distributed.ProcessGroupNCCL.NCCLConfig
and tensorplay.distributed.ProcessGroupNCCL.Options. Learn more about them using help
(e.g. help(tensorplay.distributed.ProcessGroupNCCL.NCCLConfig)) in the interpreter.
Basics
The tensorplay.distributed package provides TensorPlay support and communication primitives
for multiprocess parallelism across several computation nodes running on one or more
machines. The class tensorplay.nn.parallel.DistributedDataParallel() builds on this
functionality to provide synchronous distributed training as a wrapper around any
TensorPlay model. This differs from the kinds of parallelism provided by
Multiprocessing package - tensorplay.multiprocessing and tensorplay.nn.DataParallel() in that it supports
multiple network-connected machines and in that the user must explicitly launch a separate
copy of the main training script for each process.
In the single-machine synchronous case, tensorplay.distributed or the
tensorplay.nn.parallel.DistributedDataParallel() wrapper may still have advantages over other
approaches to data-parallelism, including tensorplay.nn.DataParallel():
Each process maintains its own optimizer and performs a complete optimization step with each iteration. While this may appear redundant, since the gradients have already been gathered together and averaged across processes and are thus the same for every process, this means that no parameter broadcast step is needed, reducing time spent transferring tensors between nodes.
Each process contains an independent Python interpreter, eliminating the extra interpreter overhead and “GIL-thrashing” that comes from driving several execution threads, model replicas, or GPUs from a single Python process. This is especially important for models that make heavy use of the Python runtime, including models with recurrent layers or many small components.
Initialization
The package needs to be initialized using the tensorplay.distributed.init_process_group()
or tensorplay.distributed.device_mesh.init_device_mesh() function before calling any other methods.
Both block until all processes have joined.
Warning
Initialization is not thread-safe. Process group creation should be performed from a single thread, to prevent inconsistent ‘UUID’ assignment across ranks, and to prevent races during initialization that can lead to hangs.
Currently three initialization methods are supported:
TCP initialization
There are two ways to initialize using TCP, both requiring a network address
reachable from all processes and a desired world_size. The first way
requires specifying an address that belongs to the rank 0 process. This
initialization method requires that all processes have manually specified ranks.
Note that multicast address is not supported anymore in the latest distributed
package. group_name is deprecated as well.
import tensorplay.distributed as dist
# Use address of one of the machines
dist.init_process_group(backend, init_method='tcp://10.1.1.20:23456',
rank=args.rank, world_size=4)
Environment variable initialization
This method will read the configuration from environment variables, allowing one to fully customize how the information is obtained. The variables to be set are:
MASTER_PORT- required; has to be a free port on machine with rank 0MASTER_ADDR- required (except for rank 0); address of rank 0 nodeWORLD_SIZE- required; can be set either here, or in a call to init functionRANK- required; can be set either here, or in a call to init function The machine with rank 0 will be used to set up all connections. This is the default method, meaning thatinit_methoddoes not have to be specified (or can beenv://).
Post-Initialization
Once tensorplay.distributed.init_process_group() was run, the following functions can be used. To
check whether the process group has already been initialized use tensorplay.distributed.is_initialized().
Experimental collective time estimation
The context manager and the backend’s _supports_time_estimate capability probe are
experimental and may change without notice.
Shutdown
It is important to clean up resources on exit by calling destroy_process_group().
The simplest pattern to follow is to destroy every process group and backend by calling
destroy_process_group() with the default value of None for the group argument, at a
point in the training script where communications are no longer needed, usually near the
end of main(). The call should be made once per trainer-process, not at the outer
process-launcher level.
if destroy_process_group() is not called by all ranks in a pg within the timeout duration,
especially when there are multiple process-groups in the application e.g. for N-D parallelism,
hangs on exit are possible. This is because the destructor for ProcessGroupNCCL calls ncclCommAbort,
which must be called collectively, but the order of calling ProcessGroupNCCL’s destructor if called
by python’s GC is not deterministic. Calling destroy_process_group() helps by ensuring
ncclCommAbort is called in a consistent order across ranks, and avoids calling ncclCommAbort
during ProcessGroupNCCL’s destructor.
Reinitialization
destroy_process_group can also be used to destroy individual process groups. One use
case could be fault tolerant training, where a process group may be destroyed and then
a new one initialized during runtime. In this case, it’s critical to synchronize the trainer
processes using some means other than tensorplay.distributed primitives _after_ calling destroy and
before subsequently initializing. This behavior is currently unsupported/untested, due to
the difficulty of achieving this synchronization, and is considered a known issue. Please file
a github issue or RFC if this is a use case that’s blocking you.
Groups
By default collectives operate on the default group (also called the world) and
require all processes to enter the distributed function call. However, some workloads can benefit
from more fine-grained communication. This is where distributed groups come
into play. new_group() function can be
used to create new groups, with arbitrary subsets of all processes. It returns
an opaque group handle that can be given as a group argument to all collectives
(collectives are distributed functions to exchange information in certain well-known programming patterns).
- tensorplay.distributed.distributed_core.GroupMember = <class 'tensorplay.distributed.distributed_core.GroupMember'>[source]
DeviceMesh
DeviceMesh is a higher level abstraction that manages process groups (or NCCL communicators).
It allows user to easily create inter node and intra node process groups without worrying about
how to set up the ranks correctly for different sub process groups, and it helps manage those
distributed process group easily. init_device_mesh() function can be
used to create new DeviceMesh, with a mesh shape describing the device topology.
Point-to-point communication
isend() and irecv()
return distributed request objects when used. In general, the type of this object is unspecified
as they should never be created manually, but they are guaranteed to support two methods:
is_completed()- returns True if the operation has finishedwait()- will block the process until the operation is finished.is_completed()is guaranteed to return True once it returns.
Synchronous and asynchronous collective operations
Every collective operation function supports the following two kinds of operations,
depending on the setting of the async_op flag passed into the collective:
Synchronous operation - the default mode, when async_op is set to False.
When the function returns, it is guaranteed that
the collective operation is performed. In the case of CUDA operations, it is not guaranteed
that the CUDA operation is completed, since CUDA operations are asynchronous. For CPU collectives, any
further function calls utilizing the output of the collective call will behave as expected. For CUDA collectives,
function calls utilizing the output on the same CUDA stream will behave as expected. Users must take care of
synchronization under the scenario of running under different streams. For details on CUDA semantics such as stream
synchronization, see CUDA Semantics.
See the below script to see examples of differences in these semantics for CPU and CUDA operations.
Asynchronous operation - when async_op is set to True. The collective operation function
returns a distributed request object. In general, you don’t need to create it manually and it
is guaranteed to support two methods:
is_completed()- in the case of CPU collectives, returnsTrueif completed. In the case of CUDA operations, returnsTrueif the operation has been successfully enqueued onto a CUDA stream and the output can be utilized on the default stream without further synchronization.wait()- in the case of CPU collectives, will block the process until the operation is completed. In the case of CUDA collectives, will block the currently active CUDA stream until the operation is completed (but will not block the CPU).get_future()- returnstensorplay._C.Futureobject. Supported for NCCL, also supported for most operations on GLOO and MPI, except for peer to peer operations. Note: as we continue adopting Futures and merging APIs,get_future()call might become redundant. Example The following code can serve as a reference regarding semantics for CUDA operations when using distributed collectives. It shows the explicit need to synchronize when using collective outputs on different CUDA streams:
# Code runs on each rank.
dist.init_process_group("nccl", rank=rank, world_size=2)
output = tensorplay.tensor([rank]).cuda(rank)
s = tensorplay.cuda.Stream()
handle = dist.all_reduce(output, async_op=True)
# Wait ensures the operation is enqueued, but not necessarily complete.
handle.wait()
# Using result on non-default stream.
with tensorplay.cuda.stream(s):
s.wait_stream(tensorplay.cuda.default_stream())
output.add_(100)
if rank == 0:
# if the explicit call to wait_stream was omitted, the output below will be
# non-deterministically 1 or 101, depending on whether the allreduce overwrote
# the value after the add completed.
print(output)
Collective functions
- class reduce_op
Deprecated enum-like class for reduction operations:
SUM,PRODUCT,MIN, andMAX.ReduceOpis recommended to use instead.
Distributed Key-Value Store
The distributed package comes with a distributed key-value store, which can be
used to share information between processes in the group as well as to
initialize the distributed package in
tensorplay.distributed.init_process_group() (by explicitly creating the store
as an alternative to specifying init_method.) There are 3 choices for
Key-Value Stores: TCPStore,
FileStore, and HashStore.
Profiling Collective Communication
Note that you can use tensorplay.profiler (recommended, only available after 1.8.1) or tensorplay.autograd.profiler to profile collective communication and point-to-point communication APIs mentioned here. All out-of-the-box backends (gloo,
nccl, mpi) are supported and collective communication usage will be rendered as expected in profiling output/traces. Profiling your code is the same as any regular tensorplay operator:
import tensorplay
import tensorplay.distributed as dist
with tensorplay.profiler():
tensor = tensorplay.randn(20, 10)
dist.all_reduce(tensor)
Please refer to the profiler documentation for a full overview of profiler features.
NCCL Symmetric Kernels
NCCL 2.27 and later ship device kernels written specifically for symmetric,
window-registered buffers, using low-latency, multimem/NVLS, and TMA algorithms
rather than the generic ring/tree path. all_reduce, all_gather_into_tensor
and reduce_scatter_tensor dispatch to them automatically once their buffers
are registered — the call site does not change. When the buffers are not
registered, or the op/dtype combination has no symmetric implementation, NCCL
silently falls back to the regular path.
For how to register buffers, the supported op/dtype matrix, and how to confirm
the symmetric kernels actually ran, see
NCCL Symmetric Kernels above.
Copy Engine Collectives
When NCCL collective operations are performed on symmetric memory tensors with the zero-CTA policy, data movement is offloaded to the GPU’s copy engines (DMA engines) instead of using CUDA streaming multiprocessors (SMs). This frees up SMs for compute work, enabling better overlap of communication and computation. For setup instructions, requirements, and examples, see Copy Engine Collectives above.
Higher-Precision Reduction
When NCCL collectives such as reduce_scatter and all_reduce operate on
symmetric memory tensors, NCCL’s symmetric kernel implementation automatically
performs internal reduction with higher precision (e.g., BF16/FP16 in → FP32
accumulate → BF16/FP16 out). This improves numerical accuracy without any code
changes to the collective call.
For details on scope, supported domains, and version requirements, see
Higher-Precision Reduction above.
Multi-GPU collective functions
Warning
The multi-GPU functions (which stand for multiple GPUs per CPU thread) are deprecated. As of today, TensorPlay Distributed’s preferred programming model is one device per thread, as exemplified by the APIs in this document. If you are a backend developer and want to support multiple devices per thread, please contact TensorPlay Distributed’s maintainers.
Object collectives
Warning
Object collectives have a number of serious limitations. Read further to determine if they are safe to use for your use case.
Object collectives are a set of collective-like operations that work on arbitrary Python objects, as long as they can be pickled. There are various collective patterns implemented (e.g. broadcast, all_gather, …) but they each roughly follow this pattern:
convert the input object into a pickle (raw bytes), then shove it into a byte tensor
communicate the size of this byte tensor to peers (first collective operation)
allocate appropriately sized tensor to perform the real collective
communicate the object data (second collective operation)
convert raw data back into Python (unpickle) Object collectives sometimes have surprising performance or memory characteristics that lead to long runtimes or OOMs, and thus they should be used with caution. Here are some common issues. Asymmetric pickle/unpickle time - Pickling objects can be slow, depending on the number, type and size of the objects. When the collective has a fan-in (e.g. gather_object), the receiving rank(s) must unpickle N times more objects than the sending rank(s) had to pickle, which can cause other ranks to time out on their next collective. Inefficient tensor communication - Tensors should be sent via regular collective APIs, not object collective APIs. It is possible to send Tensors via object collective APIs, but they will be serialized and deserialized (including a CPU-sync and device-to-host copy in the case of non-CPU tensors), and in almost every case other than debugging or troubleshooting code, it would be worth the trouble to refactor the code to use non-object collectives instead. Unexpected tensor devices - If you still want to send tensors via object collectives, there is another aspect specific to cuda (and possibly other accelerators) tensors. If you pickle a tensor that is currently on
cuda:3, and then unpickle it, you will get another tensor oncuda:3regardless of which process you are on, or which CUDA device is the ‘default’ device for that process. With regular tensor collective APIs, ‘output tensors’ will always be on the same, local device, which is generally what you’d expect. Unpickling a tensor will implicitly activate a CUDA context if it is the first time a GPU is used by the process, which can waste significant amounts of GPU memory. This issue can be avoided by moving tensors to CPU before passing them as inputs to an object collective.
Third-party backends
Besides the builtin GLOO/MPI/NCCL backends, TensorPlay distributed supports
third-party backends through a run-time register mechanism.
For references on how to develop a third-party backend through C++ Extension,
please refer to Tutorials - Custom C++ and CUDA Extensions and
the samples under test/cpp_extension/. The capability of third-party
backends are decided by their own implementations.
The new backend derives from tensorplay.distributed.ProcessGroup and registers the backend
name and the instantiating interface through tensorplay.distributed.Backend.register_backend()
when imported.
When manually importing this backend and invoking tensorplay.distributed.init_process_group()
with the corresponding backend name, the tensorplay.distributed package runs on
the new backend.
Warning
The support of third-party backend is experimental and subject to change.
TorchComms backend
TorchComms is an optional
communication backend for tensorplay.distributed. When enabled, it
overrides the normal backend instantiation in init_process_group()
so that all process groups are created through TorchComms instead of
the built-in ProcessGroup implementations.
Note
TorchComms is experimental and must be installed separately.
The torchcomms package must be importable for the flags below to
take effect.
Enabling TorchComms
Set the TP_DISTRIBUTED_USE_TORCHCOMMS environment variable
before calling init_process_group():
export TP_DISTRIBUTED_USE_TORCHCOMMS=1
Or set the config flag programmatically:
import tensorplay.distributed.config as dist_config
dist_config.use_torchcomms = True
The backend argument to init_process_group() (e.g. "nccl",
"gloo") is still respected – it is forwarded to TorchComms, which
selects the corresponding vendor plugin. No other application code
changes are required; all tensorplay.distributed collective APIs continue
to work as before.
Behavior when enabled
When TorchComms is enabled, init_process_group() changes its
backend instantiation path for every device/backend pair in the process
group (except the fake backend, which is always handled natively):
A TorchComms communicator is created via
torchcomms.new_comm()using the requested backend string and device.The communicator is wrapped in a
_BackendWrapperthat plugs into the backend registry, making it a drop-in replacement for the nativeProcessGroupbackends.A
FlightRecorderHookis automatically registered on the communicator for trace capture with a configurable buffer size.destroy_process_group()callsfinalize()on TorchComms communicators during cleanup.split_group()creates sub-communicators through TorchComms’ native splitting rather than constructing a new process group from scratch.
Eager initialization
TorchComms communicators are eagerly initialized during
init_process_group() and only support a single backend device per
group. The device_id argument must be specified at initialization
time:
dist.init_process_group(backend="nccl", device_id=tensorplay.device("cuda", local_rank))
Point-to-point operation concurrency
Each TorchComms process group maps 1:1 to a single underlying
communicator. Point-to-point operations (send/recv) issued on
the same group and stream are not guaranteed to run concurrently.
Code that relies on concurrent point-to-point operations must either:
Use the batched P2P APIs (
batch_isend_irecv()), orIssue the operations on separate groups or communicators.
Launch utility
The tensorplay.distributed package also provides a launch utility in
tensorplay.distributed.launch. This helper utility can be used to launch
multiple processes per node for distributed training.
Spawn utility
The Multiprocessing package - tensorplay.multiprocessing package also provides a spawn
function in tensorplay.multiprocessing.spawn(). This helper function
can be used to spawn multiple processes. It works by passing in the
function that you want to run and spawns N processes to run it. This
can be used for multiprocess distributed training as well.
For references on how to use it, please refer to TensorPlay example - ImageNet
implementation
Note that this function requires Python 3.4 or higher.
Debugging tensorplay.distributed applications
Debugging distributed applications can be challenging due to hard to understand hangs, crashes, or inconsistent behavior across ranks. tensorplay.distributed provides
a suite of tools to help debug training applications in a self-serve fashion:
Python Breakpoint
It is extremely convenient to use python’s debugger in a distributed environment, but because it does not work out of the box many people do not use it at all.
TensorPlay offers a customized wrapper around pdb that streamlines the process.
tensorplay.distributed.breakpoint makes this process easy. Internally, it customizes pdb’s breakpoint behavior in two ways but otherwise behaves as normal pdb.
Attaches the debugger only on one rank (specified by the user).
Ensures all other ranks stop, by using a
tensorplay.distributed.barrier()that will release once the debugged rank issues acontinueReroutes stdin from the child process such that it connects to your terminal. To use it, simply issue
tensorplay.distributed.breakpoint(rank)on all ranks, using the same value forrankin each case.
Monitored Barrier
As of v1.10, tensorplay.distributed.monitored_barrier() exists as an alternative to tensorplay.distributed.barrier() which fails with helpful information about which rank may be faulty
when crashing, i.e. not all ranks calling into tensorplay.distributed.monitored_barrier() within the provided timeout. tensorplay.distributed.monitored_barrier() implements a host-side
barrier using send/recv communication primitives in a process similar to acknowledgements, allowing rank 0 to report which rank(s) failed to acknowledge
the barrier in time. As an example, consider the following function where rank 1 fails to call into tensorplay.distributed.monitored_barrier() (in practice this could be due
to an application bug or hang in a previous collective):
import os
from datetime import timedelta
import tensorplay
import tensorplay.distributed as dist
import tensorplay.multiprocessing as mp
def worker(rank):
dist.init_process_group("nccl", rank=rank, world_size=2)
# monitored barrier requires gloo process group to perform host-side sync.
group_gloo = dist.new_group(backend="gloo")
if rank not in [1]:
dist.monitored_barrier(group=group_gloo, timeout=timedelta(seconds=2))
if __name__ == "__main__":
os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = "29501"
mp.spawn(worker, nprocs=2, args=())
The following error message is produced on rank 0, allowing the user to determine which rank(s) may be faulty and investigate further:
RuntimeError: Rank 1 failed to pass monitoredBarrier in 2000 ms
Original exception:
[gloo/transport/tcp/pair.cc:598] Connection closed by peer [2401:db00:eef0:1100:3560:0:1c05:25d]:8594
tensorplay.distributed.debug HTTP Server
The tensorplay.distributed.debug module provides an HTTP server that can be used
to debug distributed applications. It lets you inspect live distributed
training jobs across all ranks from a single browser tab — collecting stack
traces, flight-recorder events, NCCL traces, profiler captures, wait-counter
metrics, and TCPStore contents without restarting or redeploying your job.
Warning
The debug server is intended for trusted network environments only. It is not designed to be secure and must not be exposed to the public internet.
Note
This is an experimental feature and may change at any time.
Architecture Overview
The debug server has a two-tier architecture:
┌───────────────────────────────────┐
│ Frontend Server (rank 0) │
│ HTTP server on a fixed port │
│ Renders HTML dashboards │
│ Fans out requests to all ranks │
└──────────┬────────────────────────┘
│ HTTP POST /handler/<endpoint>?<args>
▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ WorkerServer (0) │ │ WorkerServer (1) │ │ WorkerServer (N) │
│ Per-rank C++ │ │ Per-rank C++ │ │ Per-rank C++ │
│ HTTP server │ │ HTTP server │ │ HTTP server │
│ Exposes handlers │ │ Exposes handlers │ │ Exposes handlers │
└──────────────────┘ └──────────────────┘ └──────────────────┘
WorkerServer (
_WorkerServer): A lightweight C++ HTTP server started on every rank. It serves handler endpoints registered via_register_handler()(Python) orRegisterHandler(C++). Each handler receives a_Requestand writes to a_Response.FrontendServer: A Python HTTP server that starts only on rank 0. It aggregates data from all worker servers and renders HTML dashboards using Jinja2 templates.
TCPStore: Workers publish their addresses (hostname + port) to the existing
TCPStoreso the frontend knows where to find each rank.
Quick Start
Prerequisites Install required dependencies (not bundled with TensorPlay by default):
pip install jinja2 aiohttp tabulate
aiohttp is optional — the server falls back to requests + thread pool if
aiohttp is unavailable.
Basic Usage
Call start_debug_server() after dist.init_process_group() on every rank:
import tensorplay
import tensorplay.distributed as dist
from tensorplay.distributed.debug import start_debug_server, stop_debug_server
dist.init_process_group("nccl")
# Start the debug server on all ranks.
# The frontend (browser UI) is served on rank 0 at port 25999.
start_debug_server(port=25999)
# ... your training loop ...
stop_debug_server()
dist.destroy_process_group()
Then open http://<rank0-hostname>:25999 in a browser.
Minimal elastic launcher Example
# train.py
import tensorplay
import tensorplay.distributed as dist
from tensorplay.distributed.debug import start_debug_server
def main():
dist.init_process_group("nccl")
start_debug_server()
rank = dist.get_rank()
device = tensorplay.device(f"cuda:{rank}")
model = tensorplay.nn.Linear(10, 10).to(device)
model = tensorplay.nn.parallel.DistributedDataParallel(model, device_ids=[rank])
for _ in range(1000):
x = tensorplay.randn(32, 10, device=device)
loss = model(x).sum()
loss.backward()
dist.destroy_process_group()
if __name__ == "__main__":
main()
python -m tensorplay.distributed.run --nproc-per-node=2 train.py
# Open http://localhost:25999
Configuration Reference
start_debug_server() accepts the following parameters:
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Port for the frontend HTTP server (rank 0 only). |
|
|
|
Port for the per-rank worker server. |
|
|
|
Multiprocessing start method ( |
|
|
|
Directory for periodic debug dumps. |
|
|
|
Seconds between periodic dumps. |
|
|
|
Handler dump filenames to enable (e.g. |
|
|
|
Custom handler list. |
|
|
|
Timeout (seconds) when fetching data from workers. |
Environment Variables The debug server reads these standard environment variables (set automatically by the elastic agent):
Variable |
Description |
|---|---|
|
Current process rank. |
|
Total number of ranks. |
|
Address of the TCPStore master. |
|
TCP port used by the TCPStore master. |
Frontend Endpoints (Browser UI)
The frontend server (rank 0) exposes the following endpoints. Each one
aggregates data from all worker ranks and renders an HTML page.
Home /
The landing page with navigation links to all other endpoints.
Python Stack Traces /stacks
Calls dump_traceback on every worker to collect Python stack traces using
faulthandler.dump_traceback(). Useful for diagnosing hangs or deadlocks
without attaching a debugger. Displays a <pre> block per rank showing the
Python call stack of every thread.
py-spy Stack Traces /pyspy_dump
Calls pyspy_dump on every worker. Uses
py-spy <https://github.com/benfred/py-spy>_ to dump both Python and
optionally native (C/C++) stack traces. Accepts query parameters
native=1 (include native frames) and subprocesses=1 (include
subprocesses). nonblocking=1 is added automatically.
Note
py-spy must be installed and the process must have SYS_PTRACE
capability.
FlightRecorder CPU /fr_trace
Fetches CPU-side flight recorder data from fr_trace_json on all workers,
parses it into structured tables (Groups, Memberships, Collectives, NCCL
Calls), and renders them as HTML tables.
FlightRecorder CPU JSON /fr_trace_json
Same data as /fr_trace but rendered as raw formatted JSON per rank.
TorchComms FlightRecorder /torchcomms_fr_trace
Fetches TorchComms flight recorder data from torchcomms_fr_trace_json
(with onlyactive=true) on all workers. Renders the same structured tables
as the FlightRecorder views (Groups, Memberships, Collectives, NCCL Calls).
TorchComms FlightRecorder JSON /torchcomms_fr_trace_json
Same data as /torchcomms_fr_trace but rendered as raw formatted JSON.
tensorplay.profiler /profile
Triggers tensorplay.profiler.profile() on every worker for a configurable
duration, then returns the Chrome trace JSON. The frontend page provides a
View button per rank that opens the trace directly in
Perfetto UI <https://ui.perfetto.dev/>_ (no download required). Accepts
query parameter duration (profiling duration in seconds, default 1,
range 1–60).
Wait Counters /wait_counters
Fetches wait_counter_values from all workers and renders the JSON data.
Wait counters track time spent waiting in collective operations, useful for
identifying stragglers and load imbalance.
TCPStore Keys /tcpstore
Connects to the TCPStore and lists all keys with their values (truncated to
100 characters). Useful for inspecting the state of the distributed
key-value store.
Worker-Level Endpoints (Per-Rank HTTP API)
Each WorkerServer instance exposes handler endpoints at:
POST http://<worker-host>:<worker-port>/handler/<handler_name>?<params>
The built-in handlers are:
ping — Simple health check. Returns "pong" (text/plain, 200).
curl -X POST \
http://worker-host:port/handler/ping # @lint-ignore
dump_traceback — Python stack traces of all threads (text/plain). Uses
faulthandler.dump_traceback() to capture every thread’s Python stack.
Requires the GIL.
curl -X POST \
http://worker-host:port/handler/dump_traceback # @lint-ignore
pyspy_dump — py-spy stack dump output (text/plain). Runs
py-spy dump --pid <pid> to capture stack traces without stopping the
process. Accepts parameters native, subprocesses, and nonblocking.
curl -X POST \
"http://worker-host:port/handler/pyspy_dump?nonblocking=1&native=1" # @lint-ignore
fr_trace_json — CPU flight-recorder trace (application/json). Returns
the flight-recorder ring buffer contents as JSON, including all recorded
collective operations, their metadata, and timing information.
curl -X POST \
http://worker-host:port/handler/fr_trace_json # @lint-ignore
torchcomms_fr_trace_json — TorchComms flight-recorder trace
(application/json). Fetches the TorchComms communication layer recorder.
Accepts parameter onlyactive
(true/false, default false).
curl -X POST \
"http://worker-host:port/handler/torchcomms_fr_trace_json?onlyactive=true" # @lint-ignore
debug_profile — Chrome trace JSON (application/json). Runs
tensorplay.profiler.profile() for the specified duration and returns the
Chrome trace format JSON. Requires parameter duration (seconds).
curl -X POST \
--output trace.json \
"http://worker-host:port/handler/debug_profile?duration=5" # @lint-ignore
# Open trace.json in chrome://tracing or https://ui.perfetto.dev
wait_counter_values — Wait counter values (application/json). Returns
a JSON object with wait-counter metrics tracking time spent waiting in
distributed collective operations.
curl -X POST \
http://worker-host:port/handler/wait_counter_values # @lint-ignore
Periodic Dumping
Enable periodic dumping to automatically save debug data to disk at regular intervals. This is useful for post-mortem analysis when a job hangs or crashes.
start_debug_server(
dump_dir="/shared/nfs/debug_dumps",
dump_interval=120.0, # dump every 2 minutes
enabled_dumps={"stacks", "fr_trace", "pyspy_dump", "wait_counters", "tcpstore"},
)
Dump files are saved as <handler_name>_<timestamp>.txt (e.g.,
stacks_20250330_192000.txt).
Handlers that support dumping:
Handler |
Dump filename |
Content |
|---|---|---|
|
|
Python stack traces for all ranks. |
|
|
py-spy stack dumps (nonblocking) for all ranks. |
|
|
CPU + NCCL flight-recorder tables. |
|
|
TorchComms flight-recorder tables. |
|
|
Wait counter JSON for all ranks. |
|
|
All TCPStore key-value pairs. |
By default (when enabled_dumps=None), only "stacks" and "fr_trace"
are enabled.
Registering Custom Handlers
You can extend the debug server with custom frontend handlers by subclassing
DebugHandler:
class DebugHandler(ABC):
fetch_timeout: float = _DEFAULT_FETCH_TIMEOUT
@abstractmethod
def routes(self) -> list[Route]: ...
@abstractmethod
def nav_links(self) -> list[NavLink]: ...
def templates(self) -> dict[str, str]:
return {}
def dump(self) -> str | None:
return None
def dump_filename(self) -> str:
return type(self).__name__.lower()
routes()— URL paths and their request handlers.nav_links()— links shown in the navigation bar.templates()— optional Jinja2 templates for HTML rendering.dump()/dump_filename()— optional support for periodic dumping. For a complete example, seeStacksHandler <https://github.com/tensorplay/tensorplay/blob/main/tensorplay/distributed/debug/_debug_handlers.py>_ intensorplay/distributed/debug/_debug_handlers.py. Pass custom handlers tostart_debug_server:
from tensorplay.distributed.debug._debug_handlers import default_handlers
handlers = default_handlers() + [MyCustomHandler()]
start_debug_server(handlers=handlers)
Debug Server Troubleshooting
AssertionError: debug server already started
start_debug_server() was called twice. Ensure you only call it once per
process.
Workers fail to respond (408/503 errors)
Check that all ranks have started and registered their addresses in TCPStore.
Increase
fetch_timeoutfor large clusters or slow networks.Verify network connectivity between rank 0 and all worker hosts.
ImportError: No module named 'jinja2'Install the required dependencies:pip install jinja2 aiohttp tabulatepy-spy returns errorsEnsure
py-spyis installed:pip install py-spyThe process may need
SYS_PTRACEcapability. In Docker usedocker run --cap-add SYS_PTRACE ...On Linux, you may need to set
echo 0 > /proc/sys/kernel/yama/ptrace_scopePerfetto UI popup blocked Allow popups for the debug server’s origin in your browser settings. The/profilepage opens Perfetto UI in a new window usingwindow.open(). Frontend server not reachableEnsure port
25999(or your custom port) is not blocked by a firewall.The frontend only starts on rank 0. Connect to the rank 0 host.
Check the logs for
Frontend server started on port <port>.spawnstart method required with CUDA If you see CUDA re-initialization errors, usestart_debug_server(start_method="spawn")to ensure the frontend server process does not inherit CUDA state from the parent process.
Logging
The underlying C++ library of tensorplay.distributed outputs log
messages at various levels. These messages can be helpful to understand the execution state of a distributed training job and to troubleshoot problems such as network connection failures.
Watchdog (Experimental)
The tensorplay.distributed._watchdog module provides a pure-Python watchdog for
detecting hung operations in Python-based distributed backends and related
distributed primitives. It monitors both CPU-side hangs (e.g., a stuck
rendezvous) and GPU-side hangs (e.g., a kernel that never completes) by running
an asyncio event loop on a background daemon thread. By default, timeouts dump
all thread stack traces and abort the process.
Warning
This module is experimental and subject to change.

