,

Distributed Training on Azure ML with ND GPU Clusters (Azure Gen AI Series, Part 19)

How to spread one training run across Azure ND H100 clusters with PyTorch, NCCL, and InfiniBand, when adding nodes pays off, and how to keep scaling efficiency from collapsing.

Azure Gen AI Series · Part 19 of 30

A single H100 will train a 7 billion parameter model. It will just take a week you do not have. So at some point you stop asking one GPU to do the whole job and spread a single training run across many of them, and the moment you do, three things you could ignore before start to matter at once: the InfiniBand fabric between the machines, the NCCL library that moves gradients over it, and the GPU quota you never checked. The failure that teaches this is quiet. Your job launches, every process reports in, and then it sits at the first gradient sync doing nothing, because the GPUs cannot find each other on the fast network.

Who this is for: You have deployed and trained models on Azure Machine Learning as single-GPU jobs, and now a model or a dataset has outgrown one card. You know what a compute cluster and a command job are. You have read Part 7 on the ND-series GPUs and Part 8 on regions and quotas, because both come back the moment you ask for more than one node. No prior multi-node experience needed. If you have never run a training job on Azure ML at all, start with Part 18.

Key takeaways

  • Distributed training splits one run three ways: data parallel copies the whole model to every GPU and splits the batch, tensor parallel splits a single layer across GPUs, and pipeline parallel puts different layers on different GPUs. Most jobs start with data parallel.
  • On Azure ML you get this by pointing a command job at a multi-node GPU cluster and setting a pytorch distribution with a process per GPU. Azure ML launches the ranks and sets the network environment for you.
  • The ND H100 v5 SKU gives each GPU a dedicated 400 Gb/s InfiniBand link, 3.2 Tbps per VM. That fabric, not the GPUs, is what lets a run scale past one node without the gradient sync eating your speedup.
  • Adding nodes cuts wall-clock time but never in a straight line. Scaling efficiency falls as you add GPUs, so more hardware can cost more money for less marginal speed. Size the cluster to a deadline, not to a wish.

Why one GPU stops being enough

Two limits push a training run off a single GPU. The first is memory. A model in training holds far more than its weights: it holds the optimizer states, the gradients, and the activations saved for the backward pass, and for a mixed-precision Adam run that overhead is several times the size of the weights alone. A 7 billion parameter model that looks like 14 GB of weights can need well over 80 GB once you add all of that, and a 70 billion parameter model does not fit on any single card made. The second limit is time. Even when a model fits, one GPU grinding through hundreds of billions of tokens can take days or weeks, and a run you cannot finish before the requirement changes is a run you did not really do.

Distributed training answers both, but with different tools. To beat the time wall you add more GPUs and give each one a slice of the work, which is data parallelism. To beat the memory wall you split the model itself across GPUs so no single card holds all of it, which is tensor or pipeline parallelism, or a sharded optimizer. Real large-model runs combine them. The reason the topic feels hard is that the moment work spans two machines, correctness depends on those machines exchanging data quickly and constantly, and that turns the network into part of your training loop.

Three ways to split one runPick by what you ran out of, memory or timeData parallelfull model per GPUbatch splitbeats the time wallsync gradientsTensor parallelone layer splitacross GPUsbeats the memory wallheavy chatterPipeline parallellayers ondifferent GPUsbeats the memory wallwatch the bubble
Data parallel is the one you reach for first. The other two exist because some models simply do not fit on one card, and real runs stack all three.

What a distributed job actually splits

Data parallelism is the workhorse, so understand it first. Every GPU holds a complete copy of the model. You split each training batch into shards, one per GPU, and each GPU runs a forward and backward pass on its shard to produce gradients. Before the weights update, every GPU has to agree on the average of all those gradients, and that agreement is a collective operation called an all-reduce. PyTorch implements this as DistributedDataParallel, DDP for short, which wraps your model and runs the all-reduce for you after each backward pass. The cost is obvious once you see it: every step, every GPU sends and receives a full set of gradients. On one machine that traffic rides NVLink between GPUs. Across machines it rides the network, which is why the fabric matters.

When the model itself will not fit, you shard it. A sharded optimizer, the approach DeepSpeed calls ZeRO and PyTorch calls Fully Sharded Data Parallel, or FSDP, splits the optimizer states, gradients, and optionally the weights across GPUs so no card holds the whole thing, while still training on split batches. Tensor parallelism cuts individual layers, so a matrix multiply is done in pieces on several GPUs and stitched back together, which means very heavy communication and so it stays inside one node where NVLink is fast. Pipeline parallelism assigns whole blocks of layers to different GPUs and streams micro-batches through them like an assembly line, trading a start-up and drain cost, the pipeline bubble, for the ability to hold a model too tall for one device. Azure ML supports the machinery for all of these because it runs your training script as is; it schedules the cluster and wires the network, and the parallelism lives in your PyTorch or DeepSpeed code.

StrategyWhat it splitsReach for it whenCommunication cost
Data parallel (DDP)The batch, model copied wholeModel fits on one GPU, you want it fasterOne gradient all-reduce per step
Sharded (ZeRO or FSDP)Optimizer, gradients, maybe weightsModel plus states will not fitMore collectives, gather on demand
Tensor parallelInside a single layerA layer is too wide for one GPUVery high, keep it in one node
Pipeline parallelBlocks of layers across GPUsModel is too deep for one GPULower, but adds a pipeline bubble

The ND family and the InfiniBand fabric

The reason multi-node training works on Azure at all is a network most cloud VMs do not have. The ND-series virtual machines carry InfiniBand, a low-latency remote direct memory access fabric that lets one GPU write into another GPU memory across machines without going through the operating system network stack the way ordinary Ethernet does. On the ND H100 v5 SKU, Standard_ND96isr_H100_v5, each of the eight H100 GPUs gets its own dedicated 400 Gb/s NVIDIA Quantum-2 InfiniBand link, which adds up to 3.2 Tbps of cross-node bandwidth per VM. That is the number that decides whether your all-reduce is free or fatal. Inside a VM the eight GPUs talk over NVLink; between VMs they talk over InfiniBand; and NCCL, the NVIDIA Collective Communications Library, is the software that picks the fast path automatically when everything is configured right.

This is also where a silent performance cliff hides. NCCL will use InfiniBand when it can see it and fall back to ordinary TCP sockets when it cannot, and a run on TCP still produces correct results, just many times slower, so nothing errors and you simply pay for a cluster that crawls. That happens when the cluster nodes are not placed close enough on the fabric, when the InfiniBand driver is missing from your container image, or when an environment variable disables it. The defense is to prove the fast path is live before you trust a long run, which I come back to below. The older ND A100 v4 generation gave each GPU a 200 Gb/s link, and the newer ND H200 v5 keeps the 400 Gb/s per-GPU fabric while roughly doubling GPU memory to fit larger models per node. Newer Blackwell-based ND generations extend this further. [VERIFY: current ND GB200 v6 SKU name, per-GPU InfiniBand rate, and general availability]

SKUGPUsPer-GPU InfiniBandFabric per VM
Standard_ND96amsr_A100_v48x A100 80GB200 Gb/s1.6 Tbps
Standard_ND96isr_H100_v58x H100 80GB400 Gb/s3.2 Tbps
Standard_ND96isr_H200_v58x H200 141GB400 Gb/s3.2 Tbps

The isr in the H100 and H200 names is the marker to look for: it signals the InfiniBand-equipped variant. A same-generation SKU without it may not carry the fabric. [VERIFY: exact A100 v4 InfiniBand SKU suffix and H200 v5 memory per GPU]

InfiniBand fabric per VMAggregate cross-node bandwidth, terabits per second012341.6ND A100 v43.2ND H100 v53.2ND H200 v5
The fabric doubled from the A100 to the H100 generation. That headroom is what lets a data-parallel all-reduce stay cheap as you add nodes, and losing it to a TCP fallback is the quiet killer.

In practice

Create the GPU cluster with a minimum node count of zero so it scales to nothing between jobs and stops billing, and set a maximum that matches the quota you actually hold. Use an Azure curated ACPT training image rather than a bare PyTorch container, because the curated images ship the InfiniBand user-space libraries and a matching NCCL, which is exactly the piece a hand-built image tends to miss. That one choice removes the most common cause of a silent TCP fallback.

Running a multi-node job on Azure ML

Here is the whole thing in the Azure ML v2 Python SDK. It submits a command job to a two-node H100 cluster, launches eight processes per node so one process drives each GPU, and lets Azure ML set the distributed environment. Your training script does not parse any of this; it reads the standard variables RANK, WORLD_SIZE, and MASTER_ADDR that Azure ML injects, and calls the normal PyTorch setup.

from azure.ai.ml import MLClient, command
from azure.identity import DefaultAzureCredential

ml_client = MLClient(
    credential=DefaultAzureCredential(),
    subscription_id='SUB_ID',
    resource_group_name='RG_NAME',
    workspace_name='WS_NAME',
)

job = command(
    code='./src',                       # folder holding train.py
    command='python train.py --epochs 3',
    environment='azureml://registries/azureml/environments/acpt-pytorch-2.2-cuda12.1/labels/latest',
    compute='nd-h100-cluster',          # a multi-node ND H100 v5 cluster
    instance_count=2,                   # 2 nodes
    distribution={'type': 'pytorch', 'process_count_per_instance': 8},
    display_name='sft-7b-2node',
)

returned = ml_client.jobs.create_or_update(job)
print(returned.studio_url)

Expected output: a studio URL printed at once, and in the job the World size reads 16, that is instance_count times process_count_per_instance, with 8 ranks on each node. Failure mode one: the ranks all start but the job hangs at init_process_group or the first all-reduce, which almost always means NCCL cannot reach InfiniBand and is waiting on a path that is not there. Failure mode two: you asked for 2 nodes of a SKU you have quota for only one of, and the job sits in Not started until it times out. Raise quota for the ND H100 family in the region first. [VERIFY: current ACPT curated environment name and tag, and the ND H100 v5 quota family label in your region]

Gotcha: before a long run, prove InfiniBand is live. Set NCCL_DEBUG to INFO for one short job and read the log. If NCCL prints lines mentioning NET/IB you are on the fast fabric; if it prints NET/Socket you are on TCP and every step is paying a tax. Also make sure nothing in your image or job sets NCCL_IB_DISABLE to 1, which is a flag people copy from a laptop guide and then forget, and it turns the fabric off by hand. Ten minutes of checking saves a training run that would otherwise run correct and slow for two days.

Scaling efficiency and what it really costs

Doubling the GPUs never halves the time, and treating scaling as if it did is how teams overspend. Every extra GPU adds communication that the compute has to hide behind, and past a point the all-reduce stops fitting neatly under the math and starts adding wall-clock time of its own. The number to track is scaling efficiency: the speedup you actually got divided by the speedup a perfect linear scale would give. At 90 percent efficiency, two nodes give you 1.8 times the throughput of one, not 2 times. At 75 percent, four nodes give you 3 times, not 4. The efficiency almost always falls as you add nodes, because communication grows faster than the compute you are hiding it behind.

Worked example

Take a fine-tune that needs about 168 GPU-hours of compute. On one ND H100 v5 node, eight GPUs at near 100 percent efficiency, that is roughly 21 hours of compute, call it 22.8 hours of wall clock once you count startup and checkpointing. Go to two nodes, 16 GPUs, and at an assumed 85 percent efficiency you get 1.7 times the throughput, so about 12.3 hours. Four nodes, 32 GPUs, at an assumed 75 percent efficiency give 3 times, about 7.0 hours. Now the cost. At an assumed 98 dollars per node-hour, one node for 22.8 hours is about 2,234 dollars, two nodes for 12.3 hours is about 2,411 dollars, and four nodes for 7.0 hours is about 2,744 dollars. You bought speed and paid for it: the four-node run finishes in under a third of the time and costs about 23 percent more. Whether that trade is worth it depends only on your deadline. [VERIFY: current Standard_ND96isr_H100_v5 hourly rate in your region and real measured efficiency for your model]

Wall-clock time by node countA 168 GPU-hour run, assumed efficiency falling as nodes grow0612182422.81 node12.32 nodes7.04 nodes
Time falls fast at first, then flattens. Each doubling buys less than the one before, and the cost line moves the other way. Measure your own efficiency before you scale a real run.

The practical takeaway is to size the cluster to a deadline. If the one-node run finishes inside your window, run it on one node and save the money. If it does not, add nodes only until the wall-clock time drops under the deadline, and stop, because the next doubling costs more and buys less. Measuring your real efficiency on a short run at one and two nodes tells you the slope of your own curve, which is worth far more than any rule of thumb. AWS runs the same arithmetic on its own resilient training service, which I cover in the SageMaker HyperPod part, and the lesson carries across clouds: the fabric and the efficiency curve decide the bill, not the GPU count on the invoice.

Where multi-node runs break

Four failures cover most of the wasted days. The first is the TCP fallback already named: correct output, terrible speed, no error, caught only by reading the NCCL log. The second is a straggler. In data-parallel training every step waits for the slowest GPU, so one node with a hot card, a bad disk, or a noisy neighbor drags the whole cluster to its pace, and the symptom is a step time that is fine most of the time and spikes for no visible reason. The third is data loading. Sixteen GPUs eat input far faster than eight, and a dataset pipeline that fed one node comfortably can starve two, leaving expensive GPUs idle while they wait on storage, so you move the data to a fast mounted store and give the loader enough worker processes.

The fourth is the one that hurts most: losing a long run to a single node failure. The more nodes and the longer the run, the higher the odds that one machine drops during training, and without checkpoints a failure at hour 20 of a 22 hour run costs you all 20 hours. Checkpoint to a durable store on a schedule and make the script resume from the latest checkpoint, so a lost node costs minutes, not the whole job. This is exactly the resilience that purpose-built training clusters automate, and it is why for very large multi-week runs the industry moved toward managed resilient training rather than raw command jobs. For the two-node, one-day jobs most teams actually run, disciplined checkpointing on a standard Azure ML cluster is enough. Later in this series, the reference-architecture part pulls the training cluster together with serving and evaluation into one production picture.

Disclaimer: A multi-node ND H100 cluster bills every node for every hour it runs, and a job left going overnight because a checkpoint loop never converged can cost thousands of dollars by morning. Set a maximum node count on the cluster, put a run-time cap on the job, add a budget alert on the resource group, and prove your checkpoint and resume path on a small job before you launch a long one. Treat the first large run as a rehearsal you watch, not a fire-and-forget submission.

Start on one node, add nodes only when the clock demands

My rule is boring and it saves money. Get the run correct and fast on a single ND node first, with DDP or FSDP working, checkpoints writing and resuming, and the data loader keeping the GPUs fed. Only then reach for a second node, and reach for it because a deadline demands it, not because more GPUs feel faster. When you do scale, measure your real efficiency at one and two nodes before committing to four or eight, because your curve, not a benchmark from a blog, tells you where the next node stops being worth it. And whatever the node count, prove InfiniBand is carrying the traffic before you trust a long run, because the difference between the fast fabric and a TCP fallback is the difference between a training bill you planned and one you did not.

So here is where I would start this week. Take a training job you already run on one GPU, move it to a single eight-GPU ND node with a pytorch distribution and DDP, confirm the World size and the NCCL InfiniBand lines in the log, and time it. That single number, your one-node wall clock, is the baseline every scaling decision after it depends on. Get that solid and the jump to two nodes is a change of one integer, not a leap into the unknown.

Azure Gen AI Series · Part 19 of 30
« Previous: Part 18  |  Guide  |  Next: Part 20 »

References

About The Author


Discover more from Journal of Intelligent Infrastructure – By Dr Pranay Jha

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure - By Dr Pranay Jha

Subscribe now to keep reading and get access to the full archive.

Continue reading