Gates → GPUs · Notes
Unit 6 · Lessons 25–28

Connect the modern system

Unit 5 examined the work inside a GPU kernel. An application must also prepare its inputs, place them in memory, transfer them when necessary, and make the results available to their next consumer. This unit connects those stages: matrix arithmetic and number formats determine which values are computed; shared systems and physical memory determine where they are stored and accessed; several GPUs combining their work add communication and completion dependencies.

You'll be able to
Expand a matrix multiply-accumulate and count its work. Compare number formats by range and precision, and calculate quantization error. Distinguish the meanings of "shared memory" in a heterogeneous system and schedule a transfer–compute pipeline. Calculate raw HBM bandwidth and the cost of remote NUMA misses. Trace a ring all-reduce and account for the communication cost when adding GPUs.
Carries over
Tiles and reuse (Lesson 23), the Roofline and byte boundaries (Lesson 24), coherence versus ordering (Lessons 16–17), latency versus throughput (throughout).
A note on models
The machine layouts, memory-stack configurations, link speeds and timings in the examples are invented to make the calculations visible. They are not product measurements. GB means 10⁹ bytes and GB/s means 10⁹ bytes per second. Each example states its assumptions; hardware rules and measured performance must be checked for the target system.
To explain the application's behavior, specify how values are represented and rounded, where their storage is located, which links carry them, and which operations must finish before others can begin.
Lesson 25

Matrix hardware and numerical formats

LimitationA tiled multiplication reuses inputs, but each thread still issues arithmetic instructions for individual output values.
New mechanismMatrix units perform multiply-adds across a tile. Narrow number formats use fewer bits for each input, reducing storage and transfer costs.
New statePieces of operand tiles held across threads' registers, running sums for the outputs, and scales used to interpret quantized values.
CostOperands must fit supported shapes and layouts. Numerical formats limit both the range of values and the precision of results.

Compute a tile with a matrix instruction

In Unit 5's tiled multiplication, each thread calculated products and added them to its output. A matrix instruction asks specialized hardware to perform these products and sums across a whole tile. It multiplies input matrices A and B and adds the result to an existing matrix C:

D = C + A × B
D[i,j] = C[i,j] + Σ_k A[i,k] × B[k,j]// A is M×K, B is K×N, C and D are M×N

Here i selects an output row and j an output column. The sum over k pairs values across row i of A with values down column j of B. For example, D[0,1] in the explorer below is 0 + 1 × 6 + 2 × 8 = 22. Each A value contributes to several output columns, and each B value to several output rows. Matrix hardware can reuse those values while forming the tile.

Press Add next k once. At a fixed k, multiplying each value in column k of A by each value in row k of B produces a contribution to every output. The explorer adds all four contributions to D together.

Building the result one k at a time. Multiplying a column by a row this way is an outer product. Each step adds that outer product to the output tile. With A = [[1, 2], [3, 4]], B = [[5, 6], [7, 8]] and C = [[10, 0], [0, 10]], the final tile is D = [[29, 22], [43, 60]]. Eight multiplies and eight adds count as 16 floating-point operations (FLOPs).

Real matrix instructions accept particular tile shapes and input types. CUDA's warp matrix multiply-accumulate (WMMA) operations commonly support a 16 × 16 × 16 shape: M = 16 output rows, N = 16 output columns, and K = 16 products per output. Other supported shapes depend on the types and hardware. To use such a shape for a 2 × 2 problem, a program could pad the inputs with zeros. The extra products would occupy hardware without contributing useful work. The explorer's two steps describe the calculation; they do not specify hardware instructions or cycles.

The hardware also requires a particular layout, or arrangement of operands in memory and registers. A fragment is the portion of a matrix held by one participating thread; it need not be a complete row. The wmma family coordinates threads within a warp. The wgmma family, or warp-group matrix multiply-accumulate, coordinates four warps and has its own completion rules. A tuned matrix library provides a useful performance baseline because it handles these details. When comparing advertised peaks, check the input formats and the format of the running sums (accumulators), supported shapes, FLOP-counting convention, and whether the rate assumes sparsity—a required pattern of zero inputs. The formats deserve particular attention: fewer input bits reduce data movement, but also change which values survive conversion.

Choose a format for range and precision

A binary floating-point value combines a sign, a significand containing its significant binary digits, and a power-of-two exponent that scales it. The encoding stores exponent and fraction fields; for normal values, a leading significand bit is implied. More exponent bits usually widen the range of magnitudes. More fraction bits put representable values closer together, improving precision. FP32, FP16 and FP8 use 32, 16 and 8 bits respectively; BF16, short for bfloat16, also uses 16 bits. Their field sizes explain the differences below.

FormatSign / exponent / fraction bitsLargest finite valueGap just above 1
FP321 / 8 / 23≈ 3.40 × 10³⁸2⁻²³
FP161 / 5 / 1065,5042⁻¹⁰
BF161 / 8 / 7≈ 3.39 × 10³⁸2⁻⁷
FP8 E4M3FN1 / 4 / 34482⁻³
FP8 E5M21 / 5 / 257,3442⁻²
Value x =or try:
FP8 overflow:

Converting a value to five formats. Each row shows the stored value, relative error, and gap to the next larger representable finite magnitude. For negative inputs, this measures spacing away from zero. Conversion chooses the nearest representable value; a tie goes to the one whose last significand bit is zero. The bit fields on the left are drawn to scale. Try 70000 to see a range limit, then 1.001 to compare spacing. Normal and subnormal results use different encodings, explained below.

FP16 and BF16 both use 2 bytes. Near 1, FP16's gap is 8 times smaller (2⁻¹⁰ versus 2⁻⁷), but BF16 can approximate 70000 while FP16 overflows. Choosing between them requires both the input range and the error the application can tolerate. The FP8 names identify different field sizes: E4M3 has 4 exponent bits and 3 fraction bits; E5M2 has 5 and 2. E4M3FN is a finite-number variant with no infinity encoding, which affects its overflow behaviour.

A value slightly above the largest finite number can still round to that number: 65505 rounds to FP16's 65504. For FP8 values that round beyond the finite range, the explorer offers two conversion modes. Non-saturating conversion produces NaN (not a number) for E4M3FN and infinity for E5M2. Saturating conversion limits the magnitude to 448 or 57,344, preserving the sign. Compare 460 and 470 in E4M3FN: the first rounds to 448, while the second overflows without saturation. FP32, FP16 and BF16 also encode positive and negative infinity. The explorer accepts finite inputs and omits signed-zero distinctions and some hardware conversion details.

Absolute error measures the difference |stored − x|. For nonzero x, relative error divides it by |x|. The same absolute error is therefore a larger proportion of a smaller input. At x = 0 that ratio is undefined, so the explorer reports an exact stored zero instead. A nonzero input rounded to zero has 100% relative error even if the absolute difference is tiny. Subnormal values extend the range below the smallest normal value before conversion reaches zero.

Represent small values with subnormals

A normal FP16 value has an implicit leading 1 before its 10 stored fraction bits, giving 11 significant bits. The smallest positive normal value is 2⁻¹⁴ (about 6.1 × 10⁻⁵). Below it, subnormal values use a leading 0 and fixed spacing of 2⁻²⁴. Their magnitudes are integer multiples of that step, down to the smallest positive FP16 value, 2⁻²⁴ (about 6 × 10⁻⁸). As the multiples get smaller, fewer significant bits remain.

02⁻¹⁴2⁻¹³ subnormals: step 2⁻²⁴first normal range: step 2⁻²⁴next range: step 2⁻²³ relative precision gets worse toward 011 significant bits11 significant bits
Gradual underflow. There are 1,024 equally spaced intervals from zero to the first normal value, containing 1,023 positive subnormal values. The drawing shows only a few. Each step becomes a larger fraction of the value as zero approaches; at 2⁻²⁴, the step is 100% of the value. Try 0.00001 above: FP16 stores it as 168 × 2⁻²⁴ ≈ 1.0014 × 10⁻⁵.

The encoding permits this gradual approach to zero. Arithmetic instructions can handle subnormals differently: some preserve them, while others replace tiny inputs or results with zero, a behaviour called flush to zero. Processing speed can also differ. Check the instruction, format and execution settings when small values matter; the conversion explorer does not model an entire arithmetic pipeline.

Choose how to accumulate the products

Adding products to an accumulator can introduce rounding beyond the loss from converting inputs. Matrix units often combine narrow inputs with a wider accumulator to retain more of the partial sum. This reduces further loss; it cannot recover an input already converted to zero. The stored accumulator type does not specify every internal rounding step, so check the arithmetic rules of the matrix instruction you use.

The effect of rounding is visible even in ordinary FP32 addition. From 2²⁴ upward in that exponent range, neighbouring values are two apart. The exact sum 2²⁴ + 1 lies halfway between 2²⁴ and 2²⁴ + 2. Round-to-nearest, ties-to-even chooses 2²⁴ because its last significand bit is zero; the other candidate's last bit is one. Both values happen to be even integers, so integer parity cannot decide this tie.

With each addition or subtraction rounded to FP32, (2²⁴ + 1) − 2²⁴ evaluates to 0, while 2²⁴ + (1 − 2²⁴) evaluates to 1. A parallel reduction can group the same additions differently, producing different intermediate rounding and final bits. Test both the result's accuracy and its reproducibility when changing the reduction or hardware.

Choose a scale for integer quantization

Quantization maps inputs to a limited set of representable values. One method stores a small integer q and reconstructs the value by multiplying q by a shared positive scale, s. In this b-bit integer quantizer, the codes run symmetrically from −Q to Q, where Q = 2b−1 − 1 (7 for 4 bits). Divide each input by s, round to an integer, then clip any code outside the range to the nearest endpoint:

q = clip(round_to_nearest_even(x / s), −Q, Q)
x_reconstructed = s × q
Bits
Scale s =

Changing the scale changes spacing and range together. Tick marks at 4 bits show the values s × q; the 255 levels at 8 bits would be too dense to draw. The shaded band covers [−Qs, Qs]. Circles mark original values and diamonds their reconstructions. The view ends at ±8.5; edge arrows show when the range continues beyond it.

At 4 bits and s = 0.5, neighbouring levels are 0.5 apart and the largest reconstruction is 7 × 0.5 = 3.5. The input 6.2 clips to 3.5, while 0.3 rounds to 0.5. With s = 1, 6.2 rounds to 6 without clipping, but 0.3 rounds to zero. A larger scale accommodates large values by making the spacing coarser. The codes −7…7 use 15 of the 16 possible 4-bit patterns; −8 is unused to keep this range symmetric. Unlike this integer quantizer, floating-point formats have spacing that changes with magnitude.

A tensor is an array of numbers, such as a vector or matrix. If all its values share one scale, an outlier can require spacing too coarse for the smaller values. Per-channel scaling gives each feature group, such as a matrix column, its own scale; per-block scaling does the same for small groups of elements. Both require storing and applying more scales. Some narrow floating-point matrix formats also use block scales. Evaluate the final application result with representative inputs: error acceptable for inference, which uses a trained model to make predictions, may impair training, which updates the model, or a solver that repeatedly refines a numerical solution.

PredictCheck with the explorers.
  1. In the matrix example, what is C plus only the k = 0 contribution? What is the final D, and how many FLOPs is that?
    Show answer

    The k = 0 outer product is [[5, 6], [15, 18]], so adding C gives [[15, 6], [15, 28]]. The k = 1 contribution is [[14, 16], [28, 32]], producing D = [[29, 22], [43, 60]]. Four outputs × two multiply-adds each = 8 multiplies + 8 adds = 16 FLOPs. This count describes useful arithmetic; it does not determine the hardware's cycle count.

  2. FP16 and BF16 use the same bytes. Which has finer spacing near 1, by what factor, and which has the wider range?
    Show answer

    FP16 has 8× finer spacing near 1 (2⁻¹⁰ versus 2⁻⁷). BF16 has a much wider range because it uses 8 exponent bits rather than 5. The spacing comparison applies near 1; conversion error also depends on the input's magnitude and position between representable values.

  3. With the 4-bit quantizer, reconstruct 6.2 and 0.3 at s = 0.5 and at s = 1. Which removes clipping, and at what cost?
    Show answer

    s = 0.5: 6.2 → 12.4 → 12, clipped to 7 → 3.5 (absolute error 2.7); 0.3 → 0.6 → 1 → 0.5 (absolute error 0.2). s = 1: 6.2 → 6 (absolute error 0.2); 0.3 → 0 → 0 (absolute error 0.3). The larger scale avoids clipping 6.2, but its wider spacing gives a less accurate reconstruction of 0.3.

  4. Can an FP32 accumulator restore a small input that rounded to zero on conversion? Why can reordering an FP32 reduction change its result?
    Show answer

    A wider accumulator cannot recover a value that conversion has already replaced with zero. It can reduce further rounding loss. Reordering changes the intermediate sums: in FP32, adding 1 to 2²⁴ rounds back to 2²⁴, whereas subtracting 2²⁴ from 1 first retains the contribution in the example above.

  5. A matrix unit advertises twice the FLOP/s of another. What do you need to know before predicting a speedup?
    Show answer

    Check the input and accumulator types, supported tile shapes and layouts, and padding required by your problem dimensions. Determine whether the peak assumes sparsity and how it counts FLOPs. Then account for the workload's data movement and arithmetic intensity, plus conversion and launch costs. A doubled arithmetic ceiling can improve speed only to the extent that the operation uses that hardware and arithmetic limits its runtime.

  6. Transfer. An input vector is represented accurately in BF16, yet a long sum over it still loses the small contributions. Would changing only the input format fix it?
    Show answer

    Changing accurately represented inputs may leave the problem unchanged. As the running sum grows, a small term may be too small to change its stored value. A wider accumulator can reduce this loss. A different reduction order, such as a tree that combines similarly sized partial sums, may also help. Compensated summation keeps an extra correction for low-order contributions lost to rounding.

To evaluate a matrix operation, check which input values survive conversion, how partial sums are computed, and how the operands fit the supported tiles. Count useful arithmetic and data movement, then measure both runtime and numerical error.

Lesson 26

Heterogeneous systems and shared memory

LimitationControl decisions, many independent data items and repeated matrix operations place different demands on hardware.
New mechanismA system-on-chip combines different processing engines, a communication fabric and memory controllers.
New stateBuffers with physical locations and rules for access, plus completion events that allow storage to pass to the next engine.
CostTransfers, format conversions and synchronization take time; engines also compete for memory bandwidth and power.

Assign work to different processing engines

The camera pipeline below uses several kinds of processor. A CPU runs general software and makes control decisions. A GPU can apply an operation to many data items in parallel. A neural processing unit (NPU) provides specialized hardware for supported neural-network operations, such as matrix arithmetic. A direct memory access (DMA) engine moves data after software sets up a transfer, saving the CPU from executing a load/store loop for every element. A system with different kinds of processing engine is called heterogeneous.

A system-on-chip (SoC) integrates processing engines, memory interfaces and input/output circuitry on a chip. Its internal fabric is the network of links that carries requests between them. In this example, the engines reach DRAM through shared memory controllers. Each numbered stage produces data for the next; transfers and coordination between stages contribute to the time for the complete pipeline.

A hypothetical camera pipeline. (1) The camera input engine writes a frame into memory. (2) The NPU transforms it. (3) The GPU renders an overlay. (4) The CPU consumes a compact result. Requests from several engines can compete for one controller or link, increasing the time for more than one stage.

Distinguish the ways memory can be shared

The figure shows several engines using memory. To program them correctly, establish where the storage resides, which addresses each engine can use, how cached copies stay consistent and whether data moves between memories. These are separate properties:

PropertyWhat it gives youWhat it does not establish on its own
Shared physical memoryEngines use the same pool of DRAMEqual latency or bandwidth, cache coherence, or safe concurrent access
Unified virtual addressingOne address-space scheme for naming allocationsThat every engine may dereference every pointer, or where the data physically is
Coherent accessCaches follow a protocol that keeps shared locations consistentOrdering across different locations, freedom from races, or that queued work has finished
Managed migrationThe runtime or OS moves supported allocations where they're usedThat no movement happens, or that first touch is free

For example, CPU and GPU code may use the same pointer value while the allocation's physical pages reside in memory attached to either processor. In CUDA, unified virtual addressing supplies a common naming scheme; Unified Memory manages access to supported allocations and may migrate their pages. The behaviour depends on the platform and its reported capabilities. An input/output memory management unit (IOMMU) translates device addresses to physical memory addresses and enforces access permissions. These mappings let a device reach storage; a separate synchronization mechanism establishes when the data is ready.

Transfer ownership after completion

Consider the camera frame in the figure. The input engine must finish writing it, and those writes must become visible to the NPU, before the NPU starts reading. The input engine must then leave the buffer unchanged until the NPU has finished using it. Software can track these transitions with completion events that record when particular work has finished. Completion of the NPU's reads allows the input buffer to be reused; completion of its output writes allows the GPU to consume that output. These rules assign ownership: who may modify each buffer, and when.

On a non-coherent path, a cache may retain old data after another engine writes memory. The platform's handoff API may therefore need cache maintenance: writing modified cache lines to memory or invalidating old copies, as appropriate for the transfer. A flag alone does not perform that work. On a coherent path, cached copies follow the coherence protocol, but the program must still order the producer and consumer and prevent conflicting simultaneous accesses. Use the event or synchronization operation whose scope covers the handoff; an event for one operation does not establish completion of unrelated queued work.

Try it: overlap work on independent chunks

Suppose each input chunk must be copied to a device, processed and copied back. For that chunk, the three stages depend on one another and must run in order. Different chunks can overlap: chunk 2 can copy in while chunk 1 computes, and chunk 1 can copy out while later chunks progress. This is pipelining. Separate engines, enough buffers and enough bandwidth let it continue through the batch.

Chunks =
Copy in =
Compute =
Copy out =

A pipeline of four chunks. With 2 ms copy-in, 3 ms compute and 1 ms copy-out, four chunks take 4 × 6 = 24 ms serially. In the pipeline, the first result takes 6 ms. Thereafter the 3 ms compute stage sets the output interval, so the fourth result arrives at 6 + 3 × 3 = 15 ms. Each chunk still follows copy-in → compute → copy-out.

The schedule assumes fixed stage times, separate engines, enough buffers and no contention. An asynchronous API allows the CPU to continue without waiting for device completion; returning from the call does not establish that two device stages overlapped. Their stream dependencies, available copy engines and buffer properties determine whether overlap is possible. Pinned host pages remain at stable physical locations during a DMA transfer. The platform may require pinned buffers for asynchronous host/device copies. Transfers through separate copy engines can still slow each other if they share a saturated memory interface.

Account for shared power and heat

CPUs, GPUs and accelerators often share a package's limits on power consumption and heat dissipation. Increasing one engine's activity can leave less power available to another, reducing the clock frequency it can sustain. Dynamic power is roughly proportional to activity × capacitance × voltage² × frequency: switching more circuitry, charging larger electrical loads or running at higher voltage and frequency consumes more power. Leakage adds power consumption even when circuitry is not switching.

Compare energy by measuring joules for the whole completed task, including transfers and conversions as well as arithmetic. An NPU's arithmetic may consume less energy while the complete route consumes more. Measure a representative sustained workload too: a short test can finish before the system heats enough to lower its clocks. The next lesson examines how the physical path to memory affects access time and bandwidth.

PredictCheck the pipeline with the explorer.
  1. CPU and GPU can name an allocation with one virtual address. Does that establish shared physical DRAM, coherent access, or free transfers?
    Show answer

    A common virtual address establishes none of those properties by itself. The address scheme can include allocations in separate physical memories. Check access permissions, coherence, migration behaviour and the route to the physical storage independently.

  2. A CPU submits a device operation and immediately reuses its input buffer. What dependency is missing, even if memory is coherent?
    Show answer

    The CPU must establish device completion before reusing the buffer. After an asynchronous submission returns, work may still be queued or executing. If the device reads after the CPU overwrites the buffer, it receives different input from what the program intended. Coherent caches cannot preserve the old input. Keep ownership with the device until the relevant completion event has been observed.

  3. With 2 / 3 / 1 ms stages, how long do four chunks take serially and pipelined? What are the first-result latency and steady interval?
    Show answer

    Serial 24 ms; pipelined 15 ms. First result at 6 ms, then one every 3 ms: outputs at 6, 9, 12 and 15 ms.

  4. If the copy engines and the computation share one saturated memory interface, which assumption fails? Why is an asynchronous call not evidence of overlap?
    Show answer

    The assumption of fixed stage times without contention fails: overlapping stages can slow down when they compete for the interface. An asynchronous API permits the CPU to continue without waiting for completion. It does not establish the device's execution schedule. Check dependencies, buffers and engine capabilities, then measure a timeline that includes completion.

  5. An NPU kernel takes 2 ms against the GPU's 5 ms, but adds 4 ms of conversion and transfer that don't overlap. Which route finishes first? What must an energy comparison measure?
    Show answer

    NPU route: 2 + 4 = 6 ms; GPU route: 5 ms. The GPU route finishes 1 ms earlier. Compare energy in joules for the whole route, including computation, transfers, conversions and other system activity throughout the interval.

  6. Transfer. The CPU and GPU use the same pointer value, but the GPU's first access through it is slow. Which observations would tell apart page migration, remote access and first-use initialization?
    Show answer

    The pointer value alone does not establish the allocation's physical location. Migration can produce page-fault or migration events and change the pages' physical placement; later accesses may be faster while they remain local. Remote access can show interconnect traffic without migration, but a remote cache line may also make later reads fast. First-use initialization (zero-filling or allocating pages on first touch) is another one-time cost, even for memory the CPU never wrote. Compare profiler migration events, placement information, interconnect counters and controlled repeated timings; the timing pattern alone cannot distinguish the cases.

Draw the data path. For each buffer, mark who may write it, who must finish reading it before reuse, and which event makes the next stage safe to begin.

Lesson 27

Packaging, HBM, and memory locality

LimitationA larger piece of silicon is more likely to contain a manufacturing defect. A processor may also need more memory bandwidth than board-level wiring can provide.
New mechanismChiplets combine smaller silicon dies in one package. HBM combines stacked DRAM dies with a wide interface close to the processor.
New stateWhich die or memory controller serves an address, and which memory node holds a page.
CostAdditional interfaces and assembly work, finite memory capacity, and access costs that depend on the requester and the data's location.

How dies connect inside a package

Lesson 26 described how several engines share a system. Their physical construction determines the connections available to them. A die is a piece of silicon containing fabricated circuits. A chiplet is a die designed to work with other dies in one system. The package protects the dies, provides electrical connections and attaches them to the board. An interposer is a layer carrying dense wiring beneath neighbouring dies; a silicon bridge provides such wiring over a smaller area. Dies can also be stacked vertically through 3D integration.

The views below show compute dies and memory stacks in one package. Many parallel connections give the processor a wide memory interface, while their short length helps reduce the energy needed to transfer data. Access to memory elsewhere may require a longer route with less bandwidth.

Compute and memory connect through the interposer. The side cross-section (left) passes through two compute chiplets and two of the four HBM stacks. Fine wiring in the silicon interposer connects them; the package substrate connects the assembly to the board. The top view (right) shows all four stacks. In this teaching arrangement, each mostly serves its neighbouring compute die. The drawing is stylized and not to scale.

Smaller dies can improve manufacturing yield, the fraction produced that meet requirements: each die is less likely to contain a defect. Chiplets also let designers reuse components or manufacture different functions with different processes. Connecting and assembling these dies adds cost, and accesses crossing between dies may have different performance. The number of dies alone therefore does not tell software how many processors or memory domains it can use. Universal Chiplet Interconnect Express (UCIe) standardizes a die-to-die connection; compatible interfaces still need compatible functions and system integration.

Try it: where HBM's bandwidth comes from

High Bandwidth Memory (HBM) stacks DRAM dies to fit memory into a small footprint and connects to the processor through a wide interface. An interposer can carry many parallel wires in the available space. Each data pin transfers bits at a specified rate, so increasing the pin count raises the total number of bits that can cross the interface per second. The calculator multiplies that count by the rate per pin and converts bits to bytes:

Data pins per stack =
Rate per pin =
Stacks =
Capacity per stack =

Raw bandwidth = pins × bits per second per pin ÷ 8. With 1,024 pins at 4 Gb/s each, one stack provides 512 GB/s. Four stacks provide 2,048 GB/s (2.048 TB/s) raw and, at 16 GB each, 64 GB of capacity. The given rate already counts transferred bits, so multiplying by two for “double data rate” would count them twice. These are invented teaching inputs, not a real HBM generation.

Reaching the four-stack ceiling requires requests to use all four interfaces. The hardware's address mapping determines how requests are distributed; addresses served mostly by one stack cannot use the full aggregate bandwidth. Delivered throughput also depends on controller scheduling, DRAM refresh, the access pattern and competing traffic. Increasing capacity per stack lets a larger allocation fit, but leaves this interface-bandwidth calculation unchanged.

A wide interface helps move many bytes, but one dependent load must still wait for its value. When it misses in all caches, its request passes through the on-chip interconnect, a memory controller and DRAM. If that value supplies the next load's address, the second request cannot begin until the first completes. The next example examines how memory placement changes this waiting time.

Try it: local and remote memory access

In a NUMA (non-uniform memory access) system, the latency and available bandwidth of memory depend on the requester and the data's location. The operating system describes this locality through nodes, which group resources such as CPU cores and memory. In our machine, a core reaches its local node's controller through the local interconnect; reaching the other node's controller requires an additional link. Moving a page between nodes can change this route while leaving its virtual address unchanged.

Our two-node teaching machine runs a thread on A. A load that misses in all caches takes 80 ns from A's DRAM or 140 ns from B's. The thread follows a chain of pointers: each load reveals the next address, allowing only one request at a time. The slider changes the fraction of requests served by B.

Fraction of loads that go to remote node B:

mean latency = (1 − f) × 80 + f × 140 ns. Because the loads are dependent, a million take a million times the mean: at f = 0.5, 110 ns each and 110 ms in total. The model omits other execution overhead and contention. Independent loads can overlap, so a streaming loop's throughput also depends on how many requests it keeps in flight and the capacity of the controllers and links.

Page placement begins when physical storage is assigned. Under Linux's usual local allocation policy, newly allocated heap pages are placed near the CPU that triggers their allocation, if memory is available there. With lazy allocation, the first write commonly triggers this step, giving the policy its “first touch” name. A successful malloc may reserve virtual space or reuse existing pages; it does not guarantee placement near the eventual worker. If initialization on B places an array there, pointer chasing from A may incur the 140 ns remote path on every cache miss.

Having workers initialize the pages they will use can improve locality. Explicit memory policies, insufficient local memory or later page migration can change the placement. Pinning a worker keeps it on specified CPUs but does not itself move its pages. Check the worker's CPU location and the pages' measured nodes to explain the observed traffic; include the cost of migration if you move the data.

Memory attached to one GPU can likewise require a remote path from another GPU. For a frequently accessed allocation, trace requester → caches → fabric → controller → memory, where the fabric is the interconnect carrying requests between components. Identify which links and controllers the requests use, and what traffic competes for those resources. This connects the package drawing to the latency and bandwidth the workload experiences.

PredictCheck with the explorers.
  1. For the four-stack example, what are the raw bandwidth and capacity? Why not multiply the pin rate by two?
    Show answer

    Per stack 1024 × 4 × 10⁹ / 8 = 512 GB/s; four stacks provide 2,048 GB/s and 64 GB. The rate already gives the bits transferred per second per pin, including any transfers on both clock edges. Multiplying it by two would count the same transfers twice.

  2. A thread on A sends 25% of its dependent cache-missing reads to B. What are the mean latency and the time for a million reads?
    Show answer

    0.75 × 80 + 0.25 × 140 = 95 ns; a million serialized reads take 95 ms.

  3. Why doesn't that calculation predict the bandwidth of a loop with many independent reads? Name two resources that might limit it.
    Show answer

    The pointer chain permits one request at a time. Independent reads can overlap and increase throughput until a resource limits them: memory controllers and DRAM channels, the inter-node link, cache interfaces, or the core's capacity to track outstanding misses. The mean latency alone gives none of those limits.

  4. Does moving a worker from B to A move its already-allocated pages? How could initialization and policy change the outcome?
    Show answer

    No. Moving a worker changes where it executes; its pages move only if a separate mechanism migrates them. Under local first-touch allocation, having that worker first write pages that still need physical storage tends to place them nearby. Existing backing pages, explicit policies, allocation on another node when local memory is unavailable, and migration can change the result. Check actual page placement.

  5. A processor goes from one die to four chiplets. What evidence would you need before claiming four NUMA domains, or a fourfold speedup?
    Show answer

    For NUMA, examine the memory controllers, inter-die links, caches, coherence domains, address mapping and topology exposed by the OS. Establish whether access cost depends on the requesting core and the memory node. For speedup, measure available parallel work, usable execution capacity, memory bandwidth and communication costs. A die count provides only the physical component count.

  6. Transfer. A workload only fits after HBM capacity is added, while raw interface bandwidth stays the same. Why can its runtime still improve drastically?
    Show answer

    Once the data fits, repeated accesses can use the HBM path. The workload may avoid staging data from host memory over PCIe, or avoid recomputing intermediates it previously could not retain. Eliminating those transfers or computations can save substantial time even though the HBM interface's raw bandwidth is unchanged.

Trace an access from its requester to the memory that serves it. Use the route and access pattern to explain its cost, then check which parts of that physical topology software exposes as locality domains.

Lesson 28

Multiple GPUs and nodes

LimitationA job can exceed one accelerator's memory or computing capacity. Dividing it among devices creates dependencies between their results.
New mechanismCollective operations combine or distribute data among a group of devices. A ring is one way to implement them over GPU links and networks.
New stateParticipant identities called ranks, communication buffers, and matching operations issued by every participant.
CostData transfers, competition for physical links, and waiting for results that other devices must produce.

Divide the work and identify the required exchanges

Adding GPUs gives a job more compute and memory. Whenever one GPU needs a result produced by another, the program must transfer that result and wait before using it. Strong scaling keeps the total work fixed and gives each GPU a smaller share. Weak scaling keeps each GPU's share roughly fixed and grows the total job. A fixed communication time becomes a larger fraction of the iteration as strong scaling reduces each GPU's compute time.

Model training provides a common example. Training adjusts numerical parameters to reduce a loss, a measure of prediction error. For a batch of examples, it computes gradients: derivatives that describe how the loss changes with each parameter. In data-parallel training, GPUs hold copies of the same model parameters, process different examples, and combine their gradients before applying the same update. If every GPU computes a mean over an equally sized local batch, adding those means and dividing by the GPU count gives the combined mean. Model computations operate on tensors, multidimensional arrays; a layer is one stage of the model, and its outputs are called activations. Tensor partitioning divides an operation within a layer among GPUs. Pipeline partitioning assigns different layers to different devices and transfers activations between them. Each partition determines which data must cross device boundaries.

Communication libraries call a participant a rank, an identity numbered within a communication group; here each rank corresponds to one GPU. A collective involves the whole group. Its participants must issue matching calls in the same order, with compatible sizes, data types and operations. A mismatch can produce incorrect results or leave ranks waiting indefinitely.

Choose the collective by its result

The diagram shows each operation's inputs and outputs. A root is a designated rank: broadcast copies its data to every rank, while reduce delivers the combined result to that root. Reduction combines corresponding array elements with an operation such as sum or maximum; it does not concatenate the arrays. All-reduce delivers that reduced array to every rank. Reduce-scatter reduces the array and gives each rank a different completed piece. All-gather collects pieces in rank order and gives the full collection to everyone. These output rules are the operations' semantics. They determine which collective the program needs, while leaving the transfer algorithm to the implementation.

Before and after, on four ranks. Colours show which ranks contributed to a value, Σ marks the sum of all four, and — marks a buffer without a defined result. After reduce, only root R0 is guaranteed to hold the sum. Reduce-scatter followed by all-gather gives every rank the full reduced array, as all-reduce does.

The ring below implements all-reduce with a reduce-scatter phase followed by an all-gather phase. NVIDIA's Collective Communication Library (NCCL) can choose different algorithms based on message size and the physical connections between devices. Those choices affect time and traffic while preserving the required output. A consumer also needs to know when that output is ready: submission queues the collective, and completion means its result can be used.

For an ordinary NCCL call using a blocking communicator, successful host return means the operation was enqueued, not completed; the host may wait for other ranks during submission. Once enqueued, it executes asynchronously, and a later kernel in the same CUDA stream is ordered after it. A consumer in an independent stream needs a dependency, such as an event recorded after the collective. Grouped calls can defer enqueueing until ncclGroupEnd; nonblocking communicators require checking submission status before dependent CUDA calls. Finally, completion on one rank's stream establishes readiness there. It does not establish simultaneous completion on every rank or provide a general barrier for unrelated work.

Try it: a ring all-reduce

Four ranks form a directed ring R0 → R1 → R2 → R3 → R0. Their starting vectors are R0 = [1, 2, 3, 4], R1 = [10, 20, 30, 40], R2 = [100, 200, 300, 400] and R3 = [1000, 2000, 3000, 4000]. Each vector has four 4-byte elements, divided into four one-element chunks. The first desired output is 1 + 10 + 100 + 1000 = 1111; the full result is [1111, 2222, 3333, 4444].

During three reduce-scatter rounds, each rank sends a partial chunk to its next neighbour, which adds it to the corresponding value it already holds. After the third round, each rank owns one chunk containing all four contributions. During three all-gather rounds, ranks forward completed chunks until everyone has all four. Every send uses the state from before that round, so a value cannot traverse several ring edges in a single round.

Track contributions through each round. Each cell shows its value and contributing ranks; a solid cell contains all four contributions exactly once. Arrows show the current sends. All-gather overwrites the remaining partial sums with completed chunks. This internal schedule ends reduce-scatter with R0 owning chunk 1, R1 chunk 2, R2 chunk 3 and R3 chunk 0. That rotated placement is specific to the explorer: a standalone NCCL ReduceScatter call assigns rank i chunk i. The subsequent all-gather here restores every chunk to its original array position.

Each rank began with 4 × 4 = 16 bytes and sent one 4-byte chunk in each of six rounds: 24 bytes sent per rank. Each rank also received 24 bytes. Dividing the vector into chunks lets all four ring edges carry data concurrently, with each round transferring only one chunk per edge.

Derive the communication cost

Replace the four ranks by P ranks, each contributing an M-byte vector. Splitting each vector into P equal chunks makes a send carry M/P bytes. There are P − 1 rounds in each of the two phases, so each rank sends that chunk size 2(P − 1) times:

bytes sent per rank = 2 × (P − 1)/P × M
T_ring ≈ 2 × (P − 1) × α + 2 × (P − 1)/P × M / β

Here α is a fixed startup time per round and β is the payload bandwidth of each directed ring link. Thus one round takes approximately α + (M/P)/β. The model assumes equal chunks, identical full-duplex links that can send and receive simultaneously, no competing traffic and negligible addition time. Each rank receives while sending, so receive time is not added again. For P = 4 and M = 100 MB, each chunk is 25 MB. Six sends total 150 MB per rank. At β = 25 GB/s and α = 5 μs, that is 6 ms of transfer plus 6 × 5 μs of startup, or 6.03 ms.

Now keep the vector size fixed while adding ranks. Compute per rank may shrink, but each rank sends 2(P−1)/P × M bytes, a quantity that rises toward 2M. The bandwidth term approaches 2M/β from below, and the startup term 2(P−1)α continues to grow. Even this ideal ring's communication time therefore increases with rank count:

RanksBandwidth termStartup termAll-reduce time
46.00 ms0.03 ms6.03 ms
87.00 ms0.07 ms7.07 ms
167.50 ms0.15 ms7.65 ms

The table describes this ideal ring. Actual libraries may divide messages differently or choose another algorithm; shared links and software overhead also affect runtime. The next explorer models strong scaling with a fixed total compute time and fixed gradient size. Speedup is the one-rank time divided by the multi-rank time; efficiency divides that speedup by the rank count.

Gradient size M =
Link bandwidth β =
Total compute on 1 rank =

Strong scaling with serialized communication. Each bar is one training iteration: compute divided ideally among the ranks (orange), followed by a ring all-reduce (teal), with α = 5 μs and no overlap. With 40 ms of compute and a 100 MB gradient at 25 GB/s: 4 ranks take 10 + 6.03 = 16.03 ms, a 2.50× speedup (62% efficient); 8 ranks take 5 + 7.07 = 12.07 ms, a 3.31× speedup.

A logical ring still runs on physical links

The ring formula gives every directed edge its own β. On a physical machine, several edges may share a network card or a switch port whose capacity is lower than the combined traffic feeding it. Those edges compete for that capacity. A node is one machine in the cluster: transfers within it may cross GPU links, PCIe switches or CPU interconnects, while transfers to another node also use network interfaces and switches. The route determines the bandwidth, latency and competing traffic each transfer encounters.

A bandwidth figure needs a stated boundary: per link or aggregate, one direction or both summed, raw line rate or useful payload, and within one node or across a network. A “direct” device-to-device transfer avoids staging through a CPU-memory buffer. It still takes time and requires accessible buffers plus correct synchronization. The route itself does not establish cache coherence or tell a consumer when the result is ready.

Overlap follows the dependency graph

The scaling bars place all communication after all computation. Training software can group parameter gradients into communication buffers called buckets. Once all gradients in an early bucket are ready, its all-reduce can run while later gradients are computed. In this schedule a bucket waits for its producers, and the parameter update waits for the reduced result. The final bucket often finishes too late to hide its communication behind more gradient computation. That waiting remains on the critical path, the dependency chain that determines the iteration's completion time. Communication and compute may also compete for memory bandwidth while they overlap.

For fixed compute and communication durations, their sum gives the serialized time; max(compute, communication) is an optimistic lower bound with perfect overlap. Data dependencies and shared resources determine how close a program can get to that bound. Inspect a timeline through final synchronization and measure the whole iteration. If the transfer algorithm changes the order of floating-point additions, also recheck the numerical result: the rounding effects from Lesson 25 apply to reductions across GPUs too.

PredictCheck with the ring and scaling explorers.
  1. After the three reduce-scatter rounds, does every rank have the complete result? Which complete chunk does each own?
    Show answer

    Each rank owns one complete chunk in this schedule: R0 has chunk 1 = 2222, R1 chunk 2 = 3333, R2 chunk 3 = 4444, R3 chunk 0 = 1111. The all-gather rounds distribute those chunks until every rank has the complete array.

  2. Four ranks, 100 MB each: how many bytes does each rank send across both phases? What's the modelled time at β = 25 GB/s, α = 5 μs?
    Show answer

    2 × 3/4 × 100 MB = 150 MB, taking 6 ms, plus 6 rounds × 5 μs = 0.03 ms: 6.03 ms. Each rank also receives 150 MB, at the same time over the full-duplex links, so that isn't added again.

  3. With 40 ms of total compute, what are the runtime, speedup and efficiency at four ranks in the blocking model? And at eight?
    Show answer

    P = 4: 10 + 6.03 = 16.03 ms, speedup ≈ 2.50×, efficiency ≈ 62%. P = 8: 5 + 7 + 0.07 = 12.07 ms, speedup ≈ 3.31×, efficiency ≈ 41%. Dividing compute among more ranks saves time, but all-reduce takes longer and reduces the scaling efficiency.

  4. Two logical ring edges share one physical link with 25 GB/s aggregate capacity. Why is the model optimistic? Does a "direct" path remove the need to wait for completion?
    Show answer

    The model gave each edge its own 25 GB/s; two edges sharing a link split it, so transfers slow or queue. A direct path skips a CPU buffer but still takes time, and anyone consuming the data or reusing the buffer must still wait for completion.

  5. A collective needs a gradient bucket that hasn't been computed yet. Can launching it asynchronously overlap that bucket's production with its transfer? How can earlier buckets still overlap?
    Show answer

    The bucket's all-reduce must wait for its gradients to be produced. Asynchronous submission allows the host to continue; the stream dependency still delays the transfer. Earlier completed buckets can be reduced while later gradients are computed, if resources permit. The last bucket's communication often remains on the critical path.

  6. Transfer. In the 4-rank ring, the edges R0 → R1 and R2 → R3 share one physical link whose aggregate capacity is 25 GB/s across both directions. For M = 100 MB, what lower bound does that shared link put on the all-reduce's transfer time?
    Show answer

    Each ring edge carries 2 × 3/4 × 100 MB = 150 MB. Both count against the same 25 GB/s aggregate capacity, so at least 300 MB crosses that bottleneck: 300 MB ÷ 25 GB/s = 12 ms, before the α terms. If the link instead provided an independent 25 GB/s in each direction and the flows used opposite directions, they could overlap and this particular transfer lower bound would be 6 ms. Mark both the route and the capacity convention before adding bytes.

Before quoting a bandwidth or claiming a scaling result, draw the collective's data dependencies and the physical routes it uses.

Unit 6 · Summary

Putting it together

Consider one iteration of training a model. A CPU prepares input in host memory. Four GPUs each receive a chunk and run low-precision matrix operations with a specified accumulator. Each GPU produces a gradient: values used to determine how the model's parameters should change. An all-reduce combines their FP32 gradient elements (4 bytes each), and each GPU returns a compact result to the CPU. The four lessons let us describe the numerical choices, storage, transfers and dependencies along this path.

  1. Name the representations. Specify the formats of the inputs, scale metadata, accumulators and communication buffers, including any output conversion. Quantizing the inputs leaves the FP32 gradient buffer unchanged unless that buffer is also converted.
  2. Name the physical homes. Identify the CPU node holding the host data and the GPU holding each device allocation. A pointer identifies an address; placement determines the physical route an access takes.
  3. Draw every movement. Include host → GPU, HBM → on-chip tiles, GPU → neighbour GPU, and GPU → host. One byte can cross several boundaries, each with its own traffic count.
  4. Establish the handoffs. Input produced before transfer; transfer before use; local gradients before the reduction; reduction complete before its result is used; output transfer complete before the CPU reads it.
  5. Find the critical path. Trace the longest chain of dependent stages that determines when the result becomes available. Independent chunks can overlap where engines and bandwidth allow. Include initialization, conversion, launches, transfers, collectives and synchronization in the interval you optimize.
  6. Validate. Compare the result with a numerical reference using the application's error tolerance, then measure steady-state and first-use behaviour separately.
One segment, worked out

Suppose local compute takes 10 ms per GPU, followed by a 100 MB gradient all-reduced on Lesson 28's 25 GB/s ring. The segment takes 10 + 6.03 = 16.03 ms. Host transfers, conversions and CPU consumption lie outside that interval. Measure the complete path to find which stages add time without overlapping other work. Then change one factor, such as byte count, placement, conversion cost, compute time or permitted overlap, and check whether the predicted stage becomes shorter. Recheck numerical results when changing precision or reduction order.

Transfer exercise: annotate a path and a time budget

Use a fresh hypothetical machine. One CPU node holds a 400 MB input batch; four GPUs each copy a 100 MB share over an independent 25 GB/s host link. Each GPU then computes for 12 ms and produces its own 200 MB FP32 gradient. A ring all-reduce uses β = 50 GB/s per link and α = 10 μs per round. Finally, each GPU copies a 4 MB result to the host over its link, where the CPU reads it. Here MB means 10⁶ bytes.

For the estimate, assume the host links and host memory do not contend, and the four GPUs perform corresponding stages concurrently. A stage waits for the previous stage's completion, so there is no overlap between stages. Compute time and each GPU's returned result size scale with its local input; the gradient size stays 200 MB because it depends on the model. Treat the CPU's final read time as negligible, since no time for it is given.

  1. Draw where the input, the gradients and the outputs live, and mark every copy and every completion dependency.
  2. Compute each stage's time and the critical-path estimate for one iteration.
  3. Name the bottleneck, and propose one measurement that could disprove your prediction.
  4. Now use 8 GPUs. Compare strong scaling (same 400 MB batch) with weak scaling (each GPU keeps 100 MB, so the batch doubles). The gradient stays 200 MB because it depends on the model, not the batch.
Show a worked answer

Path. Host memory on the CPU's node → copy and completion → each GPU's HBM → compute → gradient in HBM → ring all-reduce over GPU links → 4 MB result copied to host memory. Each GPU's consumer waits for its own all-reduce to finish; the CPU waits for the output copy before reading. Neither wait requires a global barrier.

Stages. Copy in: 100 MB ÷ 25 GB/s = 4 ms. Compute: 12 ms. All-reduce: 2 × 3/4 × 200 MB ÷ 50 GB/s = 6 ms, plus 6 × 10 μs = 0.06 ms, so 6.06 ms. Copy out: 4 MB ÷ 25 GB/s = 0.16 ms. Critical path: 4 + 12 + 6.06 + 0.16 = 22.22 ms.

Bottleneck. Compute is the largest stage at 12 ms (54% of the estimate); the 6.06 ms all-reduce comes next (27%). One experiment is to reduce ready gradient buckets while later gradients are still computed. That can hide some communication, but a bucket cannot be sent before it exists, so the final ready bucket may leave a tail. Other independent work or a different schedule could change how much remains exposed.

A disproving measurement. Profile one iteration's timeline (for example with Nsight Systems) and time the all-reduce by itself at 200 MB. A time much longer than 6.06 ms would show that the model omitted a cost, such as shared links, lower delivered bandwidth or software overhead. Communication could then exceed the 12 ms compute stage, overturning the predicted bottleneck.

8 GPUs, strong scaling. The 400 MB batch gives each GPU 50 MB: copy-in falls to 2 ms and compute to 6 ms. The total result remains 16 MB, so each GPU copies out 2 MB in 0.08 ms. The 200 MB gradient still needs an all-reduce: 2 × 7/8 × 200 MB ÷ 50 GB/s = 7 ms, plus 14 × 10 μs = 0.14 ms. Total 15.22 ms, a speedup of 22.22 / 15.22 ≈ 1.46×. The all-reduce grew while the other stages shrank.

If each GPU instead returned a fixed 4 MB, copy-out would remain 0.16 ms and the total would be 15.30 ms, about 1.45× faster than four GPUs. The result-size assumption matters, though little in this particular example.

8 GPUs, weak scaling. Copy in 4 ms, compute 12 ms, all-reduce 7.14 ms, copy out 0.16 ms: 23.30 ms for an 800 MB batch. Throughput goes from 400/22.22 ≈ 18.0 to 800/23.30 ≈ 34.3 MB of input per ms, about 1.91×. The input batch doubles while iteration time grows only from 22.22 to 23.30 ms: each GPU retains its 12 ms compute share, and the all-reduce adds 1.08 ms.

QuestionWhat answers it
Which numerical contract?Input format, scales, accumulator type, rounding and reduction order
Which physical home?The node or device holding the pages, and the controller or stack serving each address
Which communication path?The physical links and controllers each transfer crosses, and what else shares them
Which dependency permits overlap?Completion events and data readiness; overlap only between truly independent work

Further reading and model limits

Every machine, memory stack, link speed and timing on this page is an invented teaching model. To check the real rules:

Based on the Unit 6 outline and worked answers in chatgpt-reference/. The examples use the teaching assumptions stated in each lesson; the sources above document the corresponding hardware and programming rules.