TensorPlay

Latest development documentation · Updated 2026-09-08. A documentation snapshot for package 1.0.0.dev20260909 is not available.

On this page

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

gloo

mpi

nccl

xccl

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=eth0

  • GLOO_SOCKET_IFNAME, for example export GLOO_SOCKET_IFNAME=eth0 If 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)

Shared file-system initialization

Another initialization method makes use of a file system that is shared and visible from all machines in a group, along with a desired world_size. The URL should start with file:// and contain a path to a non-existent file (in an existing directory) on a shared file system. File-system initialization will automatically create that file if it doesn’t exist, but will not delete the file. Therefore, it is your responsibility to make sure that the file is cleaned up before the next init_process_group() call on the same file path/name. Note that automatic rank assignment is not supported anymore in the latest distributed package and group_name is deprecated as well.

Warning

This method assumes that the file system supports locking using fcntl - most local systems and NFS support it.

Warning

This method will always create the file and try its best to clean up and remove the file at the end of the program. In other words, each initialization with the file init method will need a brand new empty file in order for the initialization to succeed. If the same file used by the previous initialization (which happens not to get cleaned up) is used again, this is unexpected behavior and can often cause deadlocks and failures. Therefore, even though this method will try its best to clean up the file, if the auto-delete happens to be unsuccessful, it is your responsibility to ensure that the file is removed at the end of the training to prevent the same file to be reused again during the next time. This is especially important if you plan to call init_process_group() multiple times on the same file name. In other words, if the file is not removed/cleaned up and you call init_process_group() again on that file, failures are expected. The rule of thumb here is that, make sure that the file is non-existent or empty every time init_process_group() is called.

import tensorplay.distributed as dist

# rank should always be specified
dist.init_process_group(backend, init_method='file:///mnt/nfs/sharedfile',
                        world_size=4, rank=args.rank)

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 0

  • MASTER_ADDR - required (except for rank 0); address of rank 0 node

  • WORLD_SIZE - required; can be set either here, or in a call to init function

  • RANK - 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 that init_method does not have to be specified (or can be env://).

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 finished

  • wait() - 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, returns True if completed. In the case of CUDA operations, returns True if 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() - returns tensorplay._C.Future object. 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, and MAX.

ReduceOp is 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:

  1. convert the input object into a pickle (raw bytes), then shove it into a byte tensor

  2. communicate the size of this byte tensor to peers (first collective operation)

  3. allocate appropriately sized tensor to perform the real collective

  4. communicate the object data (second collective operation)

  5. 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 on cuda:3 regardless 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):

  1. A TorchComms communicator is created via torchcomms.new_comm() using the requested backend string and device.

  2. The communicator is wrapped in a _BackendWrapper that plugs into the backend registry, making it a drop-in replacement for the native ProcessGroup backends.

  3. A FlightRecorderHook is automatically registered on the communicator for trace capture with a configurable buffer size.

  4. destroy_process_group() calls finalize() on TorchComms communicators during cleanup.

  5. 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()), or

  • Issue 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.

  1. Attaches the debugger only on one rank (specified by the user).

  2. Ensures all other ranks stop, by using a tensorplay.distributed.barrier() that will release once the debugged rank issues a continue

  3. Reroutes 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 for rank in 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) or RegisterHandler (C++). Each handler receives a _Request and 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 TCPStore so 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

int

25999

Port for the frontend HTTP server (rank 0 only).

worker_port

int

0

Port for the per-rank worker server. 0 = ephemeral.

start_method

str | None

None

Multiprocessing start method ("fork", "spawn", "forkserver"). "spawn" recommended with CUDA.

dump_dir

str | None

None

Directory for periodic debug dumps. None disables dumping.

dump_interval

float

60.0

Seconds between periodic dumps.

enabled_dumps

set[str] | None

None

Handler dump filenames to enable (e.g. {"stacks", "fr_trace"}). None = default set.

handlers

list[DebugHandler] | None

None

Custom handler list. None = all default handlers.

fetch_timeout

float

60.0

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

RANK

Current process rank.

WORLD_SIZE

Total number of ranks.

MASTER_ADDR

Address of the TCPStore master.

MASTER_PORT

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

StacksHandler

stacks

Python stack traces for all ranks.

PySpyHandler

pyspy_dump

py-spy stack dumps (nonblocking) for all ranks.

FlightRecorderHandler

fr_trace

CPU + NCCL flight-recorder tables.

TorchCommsFlightRecorderHandler

torchcomms_fr_trace

TorchComms flight-recorder tables.

WaitCountersHandler

wait_counters

Wait counter JSON for all ranks.

TCPStoreHandler

tcpstore

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, see StacksHandler <https://github.com/tensorplay/tensorplay/blob/main/tensorplay/distributed/debug/_debug_handlers.py>_ in tensorplay/distributed/debug/_debug_handlers.py. Pass custom handlers to start_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_timeout for 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 tabulate py-spy returns errors

  • Ensure py-spy is installed: pip install py-spy

  • The process may need SYS_PTRACE capability. In Docker use docker run --cap-add SYS_PTRACE ...

  • On Linux, you may need to set echo 0 > /proc/sys/kernel/yama/ptrace_scope Perfetto UI popup blocked Allow popups for the debug server’s origin in your browser settings. The /profile page opens Perfetto UI in a new window using window.open(). Frontend server not reachable

  • Ensure 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>. spawn start method required with CUDA If you see CUDA re-initialization errors, use start_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.

Search documentation

Search all 1,743 documentation pages.

Keyboard shortcuts

Global

  • /Focus search
  • ?This dialog
  • ,Open settings
  • jAI assistant

Search

  • Navigate results
  • Open result
  • escClose

Package

  • mMain information
  • dDocs
  • .Code
  • -Changelog
  • tTimeline
  • sStats
  • vVersions