<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Practical ML Systems]]></title><description><![CDATA[Practical ML Systems]]></description><link>https://practical-ml-systems.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Practical ML Systems</title><link>https://practical-ml-systems.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 17 Sep 2026 12:00:18 GMT</lastBuildDate><atom:link href="https://practical-ml-systems.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[You don't need a GPU to test distributed PyTorch]]></title><description><![CDATA[Most teams treat distributed PyTorch as something you can only validate on accelerator hardware. Two GPUs minimum, NCCL, a scheduler, and a queue to wait in. So distributed code paths get tested late,]]></description><link>https://practical-ml-systems.hashnode.dev/you-don-t-need-a-gpu-to-test-distributed-pytorch</link><guid isPermaLink="true">https://practical-ml-systems.hashnode.dev/you-don-t-need-a-gpu-to-test-distributed-pytorch</guid><category><![CDATA[pytorch]]></category><category><![CDATA[distributed systems]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Python]]></category><category><![CDATA[distributedpytorch]]></category><dc:creator><![CDATA[reshmi aravind]]></dc:creator><pubDate>Thu, 03 Sep 2026 16:40:59 GMT</pubDate><content:encoded><![CDATA[<p>Most teams treat distributed PyTorch as something you can only validate on accelerator hardware. Two GPUs minimum, NCCL, a scheduler, and a queue to wait in. So distributed code paths get tested late, on scarce hardware, by whoever managed to book a node.</p>
<p>A large share of the things that break in distributed training have nothing to do with the accelerator. Process groups fail to initialize. The launcher doesn't spawn the workers you asked for. A collective hangs because one rank took a different branch. A wrapper silently stops synchronizing gradients. Many of those failures reproduce on CPU with the Gloo backend, often in a few seconds.</p>
<p>This post covers what you can meaningfully test on CPU, with runnable examples, and is honest about where the CPU story stops.</p>
<h2>What Gloo gives you</h2>
<p>Gloo is PyTorch's primary CPU collective communication backend and is available in standard Linux PyTorch distributions. For standard Linux PyTorch distributions it typically requires no separate communication runtime, and it implements the collectives that matter for the common training patterns: <code>all_reduce</code>, <code>all_gather</code>, <code>broadcast</code>, <code>reduce</code>, and <code>barrier</code>. It is not a performance backend and it is not feature-complete against NCCL. That is fine. You are not benchmarking. You are asking whether the code is correct and whether the plumbing is connected.</p>
<p>Check what your build actually has:</p>
<pre><code class="language-python">import torch
import torch.distributed as dist

print("Torch version:", torch.__version__)
print("Distributed available:", dist.is_available())
print("Gloo available:", dist.is_gloo_available())
</code></pre>
<p>On a CPU-only container this reports Gloo available and NCCL not available, which is exactly the configuration we want to test in.</p>
<h2>Two processes, one machine</h2>
<p><code>torchrun --standalone</code> spawns multiple worker processes on a single node and wires up rendezvous for you. No cluster, no scheduler:</p>
<pre><code class="language-bash">torchrun --standalone --nnodes=1 --nproc-per-node=2 smoke.py
</code></pre>
<p>The smallest useful test is a collective with a known answer:</p>
<pre><code class="language-python">import torch
import torch.distributed as dist

def main():
    dist.init_process_group(backend="gloo")
    rank = dist.get_rank()
    world_size = dist.get_world_size()

    tensor = torch.tensor([rank + 1.0])
    dist.all_reduce(tensor, op=dist.ReduceOp.SUM)

    expected = world_size * (world_size + 1) / 2
    assert tensor.item() == expected

    dist.barrier()
    if rank == 0:
        print("PASS: Gloo all_reduce")
    dist.destroy_process_group()

if __name__ == "__main__":
    main()
</code></pre>
<p>Two ranks contribute 1.0 and 2.0, both see 3.0. The assertion is the point. Checking that <code>dist.is_available()</code> returns <code>True</code> proves the API is importable; checking that <code>all_reduce</code> produced the right number proves two processes actually talked to each other.</p>
<h2>DDP: assert on synchronization, not loss</h2>
<p>The next layer up is <code>DistributedDataParallel</code>. Most DDP examples print the loss and stop there, which tells you nothing, because with different input per rank you expect different loss values. What you actually want to know is whether parameters agree after the optimizer step.</p>
<pre><code class="language-python">import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP

def main():
    dist.init_process_group("gloo")
    rank = dist.get_rank()
    world_size = dist.get_world_size()

    torch.manual_seed(1234)
    model = DDP(torch.nn.Linear(4, 1))
    optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

    x = torch.ones(4, 4) * (rank + 1)      # different data per rank
    target = torch.zeros(4, 1)

    optimizer.zero_grad()
    loss = torch.nn.functional.mse_loss(model(x), target)
    loss.backward()
    optimizer.step()

    weight = model.module.weight.detach().clone()
    gathered = [torch.empty_like(weight) for _ in range(world_size)]
    dist.all_gather(gathered, weight)
    for other in gathered:
        assert torch.allclose(weight, other)

    dist.destroy_process_group()
</code></pre>
<p>Different data in, identical weights out. Using different input on each rank is intentional: without gradient synchronization, the two models would take different optimizer steps, and the parameter comparison would expose it. If gradient all-reduce were broken or the model were never really wrapped, this assertion fails immediately. It runs in seconds on CPU and it is one of the highest-value distributed correctness tests you can put in CPU CI.</p>
<p>Everything up to this point is established PyTorch distributed functionality. It is still valuable Day-0 and CI coverage, but none of it is new. The next two tests exercise distributed changes introduced in PyTorch 2.14.</p>
<h2>Make timeouts fail on purpose</h2>
<p>Here is a failure mode CPU testing is particularly good at. When a rank stalls, the process group waits. For Gloo, the default process-group timeout is 30 minutes. In CI that means a job that is already dead sits burning a runner for half an hour before anyone finds out.</p>
<p>PyTorch 2.14 promotes <code>torch.distributed.set_timeout()</code> to a stable API, letting you change the timeout of an already-initialized process group. You can use it to verify that your timeout handling works at all, by inducing a hang deliberately:</p>
<pre><code class="language-python">import time
from datetime import timedelta
import torch
import torch.distributed as dist

dist.init_process_group(backend="gloo", timeout=timedelta(seconds=30))
rank = dist.get_rank()

dist.all_reduce(torch.tensor([rank + 1.0]))   # baseline: group is healthy

dist.set_timeout(timedelta(seconds=2))

if rank == 1:
    time.sleep(5)          # rank 1 arrives late, on purpose

start = time.monotonic()
try:
    dist.barrier()
    print(f"rank {rank}: FAIL - barrier should not have succeeded")
except Exception as exc:
    print(f"rank {rank}: timed out after {time.monotonic() - start:.2f}s")
    print(f"rank {rank}: {type(exc).__name__}")
</code></pre>
<p>Rank 0 fails at roughly 2.02 seconds with a Gloo receive timeout. Rank 1 then sees a closed peer connection, because rank 0 gave up and tore down the path. Both are correct outcomes.</p>
<p>Run the baseline collective before changing the timeout. Otherwise a failure tells you nothing, since you can't distinguish a working timeout from a process group that was broken from the start.</p>
<h2>Reconfiguration in 2.14, with caveats</h2>
<p>PyTorch 2.14 adds an experimental reconfiguration path to Gloo: <code>_supports_reconfigure()</code>, <code>_get_reconfigure_handle()</code>, and <code>_reconfigure()</code> in <code>distributed_c10d</code>. Initialize with <code>enable_reconfigure=True</code>, exchange opaque handles between ranks through a store, create the communicator through the reconfiguration API, then reconfigure the same process group in place and keep using it.</p>
<p>The core flow looks like this. It is abridged; the full runnable script, including the <code>exchange_handles()</code> helper that moves handles between ranks via a <code>FileStore</code>, is linked at the end of the post.</p>
<pre><code class="language-python">from torch.distributed import distributed_c10d as c10d

dist.init_process_group(backend="gloo", enable_reconfigure=True)
assert c10d._supports_reconfigure()

uuid, handles = exchange_handles(store, rank, world_size, "initial")
c10d._reconfigure(uuid=uuid, handles=handles, timeout=timedelta(seconds=30)).wait()

dist.all_reduce(tensor)   # works before

uuid2, handles2 = exchange_handles(store, rank, world_size, "second")
c10d._reconfigure(uuid=uuid2, handles=handles2, timeout=timedelta(seconds=30)).wait()

dist.all_reduce(tensor)   # and after
</code></pre>
<p>Collectives succeed on both sides of the in-place reconfiguration. That is real, and it works today on CPU.</p>
<p>Be clear about what it isn't. Reconfiguring a healthy process group is not fault tolerance. No worker was killed, no rank was replaced, world size never changed, and no training run was resumed. The API is marked experimental and may change or be removed. What this demonstrates is that the mechanism is present and functional, which is a prerequisite for elastic recovery rather than a demonstration of it.</p>
<h2>Where CPU testing stops</h2>
<p>Being honest about the limits is what makes the rest credible. Testing on Gloo tells you nothing about:</p>
<ul>
<li><p>CUDA or ROCm kernel behavior, or anything involving device tensors</p>
</li>
<li><p>NCCL and RCCL semantics, which differ from Gloo in error handling and in which collectives are supported</p>
</li>
<li><p>Multi-GPU DDP, tensor parallelism, or sharded strategies at realistic scale</p>
</li>
<li><p>Anything crossing a host boundary: container networking, TCP rendezvous between machines, InfiniBand, RDMA, NVLink</p>
</li>
<li><p>Performance of any kind</p>
</li>
</ul>
<p>The natural next step, and still no accelerator required, is to run rank 0 and rank 1 in separate containers with TCP rendezvous instead of <code>--standalone</code>. That moves you from inter-process communication inside one namespace to real network communication, and catches a different class of bug: unreachable addresses, wrong interface bindings, firewall rules.</p>
<h2>Put it in CI</h2>
<p>None of this needs special hardware, which means all of it can run on every commit. A reasonable gate for any project shipping PyTorch builds or containers:</p>
<ul>
<li><p><code>torch.distributed.is_available()</code> and <code>is_gloo_available()</code> both return true</p>
</li>
<li><p><code>torchrun</code> launches two workers and both report <code>world_size=2</code></p>
</li>
<li><p><code>all_reduce</code> returns the mathematically expected value on every rank</p>
</li>
<li><p>DDP parameters match across ranks after a training step</p>
</li>
<li><p>A deliberately induced timeout fails at the configured deadline rather than hanging</p>
</li>
</ul>
<p>CPU distributed testing does not replace GPU validation. It moves a large class of correctness failures earlier in the development cycle, where they are cheaper to reproduce and cheaper to fix. Test rendezvous, process groups, collectives, DDP synchronization, and timeout handling on every commit. Then spend accelerator time on the things only accelerators can tell you: NCCL and RCCL behavior, device correctness, topology, scale, and performance.</p>
<p>Save the accelerator hours for the questions only accelerators can answer.</p>
<hr />
<p>Full runnable scripts: <a href="https://github.com/raravind007/distributed-pytorch-cpu-tests">https://github.com/raravind007/distributed-pytorch-cpu-tests</a></p>
]]></content:encoded></item></channel></rss>