Data Parallelism › Code: Scaling Analysis

Michael BrenndoerferJanuary 16, 20269 min read
Language AI HandbookAll chapters
Part 31 of 67Training Infrastructure
Chapter 222Data Parallelism › Code: Scaling Analysis
Reading section 8 of 12 · Code: Scaling Analysis · 9 minSection 8 of 12 · 9 min

Book contents

Section 8 of 12 · 9 min read

Data Parallelism › Code: Scaling AnalysisOne section · Sections in this chapter8 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let us simulate the scaling behaviour of DDP and examine the impact of communication overhead analytically.

In[18]:
Code
# Model parameters affect gradient size
param_configs = {
    "125M (GPT-2 small)": 125e6,
    "1.3B (GPT-2 XL equiv)": 1.3e9,
    "7B (Llama-7B equiv)": 7e9,
}

# Hardware parameters
nvlink_bandwidth_gb = 600  # NVLink bandwidth per GPU in GB/s (intranode)
infiniband_bandwidth_gb = (
    25  # InfiniBand effective bandwidth per GPU in GB/s (internode)
)
nvlink_step_latency_ms = 0.02
infiniband_step_latency_ms = 0.05
bytes_per_param_bf16 = 2  # BF16 gradients

# Illustrative compute times chosen to expose the compute/communication balance
compute_times_ms = {
    "125M (GPT-2 small)": 3.0,
    "1.3B (GPT-2 XL equiv)": 50.0,
    "7B (Llama-7B equiv)": 400.0,
}


def ring_allreduce_time_ms(num_params, bandwidth_gb_per_s, N, step_latency_ms):
    """Estimate ring all-reduce time in milliseconds."""
    gradient_bytes = num_params * bytes_per_param_bf16
    bandwidth_bytes_per_ms = bandwidth_gb_per_s * 1e9 / 1e3
    # Ring all-reduce: send+receive ~ 2 * (N-1)/N * gradient_bytes
    comm_bytes = 2 * ((N - 1) / N) * gradient_bytes
    bandwidth_time_ms = comm_bytes / bandwidth_bytes_per_ms
    latency_time_ms = 2 * (N - 1) * step_latency_ms
    return bandwidth_time_ms + latency_time_ms


def compute_speedup(
    N_values, num_params, compute_ms, bandwidth_gb, step_latency_ms
):
    """Compute actual vs ideal speedup for a range of GPU counts."""
    speedups = []
    efficiencies = []
    for N in N_values:
        comm_ms = ring_allreduce_time_ms(
            num_params, bandwidth_gb, N, step_latency_ms
        )
        # Step time = compute (overlapped with comm) = max(compute, comm)
        # Plus a small serial fraction for sync, data loading, etc.
        serial_overhead_ms = 0.5  # fixed overhead per step
        step_time = max(compute_ms, comm_ms) + serial_overhead_ms
        single_gpu_step_ms = compute_ms + serial_overhead_ms
        speedup = (
            single_gpu_step_ms * N / step_time
        )  # effective throughput gain
        speedups.append(speedup)
        efficiencies.append(speedup / N * 100)
    return speedups, efficiencies


N_values = [1, 2, 4, 8, 16, 32, 64, 128]
Out[19]:
Console
Configuration summary:
Model                                Params   Grad size (BF16)
--------------------------------------------------------------
125M (GPT-2 small)                125000000             238 MB
1.3B (GPT-2 XL equiv)            1300000000            2480 MB
7B (Llama-7B equiv)              7000000000           13351 MB
In[20]:
Code
# Compute scaling curves for each model and bandwidth scenario
results = {}
for model_name, num_params in param_configs.items():
    compute_ms = compute_times_ms[model_name]
    speedups_nv, efficiencies_nv = compute_speedup(
        N_values,
        num_params,
        compute_ms,
        nvlink_bandwidth_gb,
        nvlink_step_latency_ms,
    )
    speedups_ib, efficiencies_ib = compute_speedup(
        N_values,
        num_params,
        compute_ms,
        infiniband_bandwidth_gb,
        infiniband_step_latency_ms,
    )
    results[model_name] = {
        "speedup_nvlink": speedups_nv,
        "speedup_infiniband": speedups_ib,
        "efficiency_nvlink": efficiencies_nv,
        "efficiency_infiniband": efficiencies_ib,
    }

# Compute ideal speedup
ideal_speedup = N_values
Out[21]:
Console
Scaling efficiency for 7B (Llama-7B equiv):
  GPUs   NVLink speedup  NVLink eff%   InfiniBand speedup    IB eff%
----------------------------------------------------------------------
     1              1.0        100.0                  1.0      100.0
     2              2.0        100.0                  1.4       71.4
     4              4.0        100.0                  1.9       47.6
     8              8.0        100.0                  3.3       40.8
    16             16.0        100.0                  6.1       38.1
    32             32.0        100.0                 11.8       36.8
    64             64.0        100.0                 23.1       36.1
   128            128.0        100.0                 45.6       35.6
Out[22]:
Visualization
Line chart showing DDP speedup vs GPU count for 125M, 1.3B, and 7B models on NVLink.
DDP scaling speedup vs number of GPUs for three model sizes using NVLink (intranode, 600 GB/s). Larger models scale more efficiently because compute time dominates communication time, keeping scaling efficiency high. The 7B model remains near linear through 128 GPUs in this illustrative model, while the 125M curve falls increasingly below ideal beyond 32 GPUs as collective latency overtakes its shorter compute time.
Line chart showing DDP speedup vs GPU count for 125M, 1.3B, and 7B models on InfiniBand.
DDP scaling speedup vs number of GPUs using InfiniBand (internode, 25 GB/s effective). Communication time becomes a bottleneck for all but the largest models, particularly beyond 32 GPUs. The 125M model scales poorly because its small compute time is overwhelmed by communication overhead at even modest GPU counts.

The speedup curves reveal a fundamental truth about data parallelism: larger models scale more efficiently than smaller ones. For a 7B parameter model on NVLink, compute time is so much larger than communication time that DDP achieves near-linear scaling up to 64 GPUs. For a 125M model on InfiniBand across nodes, the relatively short compute time is quickly dominated by communication overhead, and scaling efficiency collapses beyond 8 to 16 GPUs.

This has direct practical implications. If you are training a large language model, data parallelism is an excellent fit because the models are large enough that compute always dominates communication. If you are training a smaller model or performing frequent experimentation with small configurations, you may hit communication bottlenecks sooner and need to consider whether the parallelism overhead is worth the speedup.

Out[23]:
Visualization
Heatmap of DDP scaling efficiency percent on NVLink for 6 model sizes and 8 GPU counts.
DDP scaling efficiency (percentage of ideal speedup achieved) as a function of GPU count and model size on NVLink (intranode). Efficiency remains above 85% for larger models at all GPU counts shown, because compute time dwarfs communication time. Smaller models lose efficiency faster as GPU count grows, since their lower compute-to-communication ratio means all-reduce takes a growing fraction of each step.
Heatmap of DDP scaling efficiency percent on InfiniBand for 6 model sizes and 8 GPU counts.
DDP scaling efficiency on InfiniBand (internode, 25 GB/s). Cross-node communication is 24x slower than NVLink, causing efficiency to drop sharply for small and medium models beyond 8 GPUs. Only the largest models (7B and above) maintain useful efficiency at 32 or more GPUs on InfiniBand, making within-node NVLink training strongly preferred when possible.

Communication-Compute Overlap Efficiency

In[24]:
Code
# Simulate the effect of gradient bucketing on step time
# With perfect overlap: step_time = max(compute, comm)
# With no overlap: step_time = compute + comm
# Reality is between these extremes


def step_time_analysis(compute_ms, comm_ms, overlap_fraction=0.85):
    """
    Estimate step time with partial overlap.
    overlap_fraction: fraction of communication that overlaps with computation.
    """
    comm_serial = comm_ms * (1 - overlap_fraction)
    comm_overlapped = comm_ms * overlap_fraction

    # Overlapped communication hides within compute time
    effective_step = compute_ms + comm_serial
    return effective_step


model_name = "7B (Llama-7B equiv)"
num_params = param_configs[model_name]
compute_ms = compute_times_ms[model_name]

print(f"Analysis for {model_name}:")
print(f"Compute time per step: {compute_ms:.1f} ms")
print()

for N in [1, 2, 4, 8, 16, 32]:
    comm_nv = ring_allreduce_time_ms(
        num_params, nvlink_bandwidth_gb, N, nvlink_step_latency_ms
    )
    comm_ib = ring_allreduce_time_ms(
        num_params, infiniband_bandwidth_gb, N, infiniband_step_latency_ms
    )
    step_nv = step_time_analysis(compute_ms, comm_nv, overlap_fraction=0.85)
    step_ib = step_time_analysis(compute_ms, comm_ib, overlap_fraction=0.85)
    throughput_nv = N * compute_ms / step_nv  # relative throughput
    throughput_ib = N * compute_ms / step_ib
    print(
        f"  N={N:3d}  NVLink comm={comm_nv:5.1f} ms  IB comm={comm_ib:5.1f} ms  "
        f"  NVLink eff={throughput_nv:.1f}x  IB eff={throughput_ib:.1f}x"
    )
Out[25]:
Console
Analysis for 7B (Llama-7B equiv):
Compute time per step: 400.0 ms

   N  NVLink comm    IB comm   NVLink eff     IB eff
----------------------------------------------------
   1        0.0 ms      0.0 ms        1.0x      1.0x
   2       23.4 ms    560.1 ms        2.0x      1.7x
   4       35.1 ms    840.3 ms        3.9x      3.0x
   8       41.1 ms    980.7 ms        7.9x      5.8x
  16       44.4 ms   1051.5 ms       15.7x     11.5x
  32       46.4 ms   1088.1 ms       31.5x     22.7x

The communication time for ring all-reduce on a 7B model is substantial. On NVLink, the 14 GB of BF16 gradients can be transferred in roughly 45 ms at 600 GB/s. On InfiniBand at 25 GB/s effective bandwidth, the same communication takes over a second. With 250 ms of compute time, NVLink communication is safely within the compute window and can be largely overlapped. InfiniBand communication is four times the compute time, severely limiting scaling efficiency beyond a single node. This quantitative comparison explains why multi-node training at scale typically combines DDP with ZeRO optimization: ZeRO reduces the volume of gradient traffic that must cross the slow inter-node fabric.