CHAPTER 10
What changes when matrix inputs use fewer bits?
Separate input rounding from accumulation, then explain how this connects to cooperative matrix hardware.
Start with the operation
PyTorch
import torch
row = torch.tensor([1.0003 if k < 8 else -1.0 for k in range(16)],
dtype=torch.float32, device="cuda")
a = row.repeat(16, 1).to(torch.float16)
b = torch.ones((16, 16), dtype=torch.float16, device="cuda")
c = a @ b
print(c[0, 0].float().item())A matrix multiplication in a neural network can store inputs in a smaller number format. torch.float16 stores each input in two bytes instead of FP32’s four. The conversion with .to(...) may round a value before @ uses it. Converting the result to FP32 for printing cannot restore discarded input information.
row.repeat(16, 1) makes A from sixteen copies of the row; torch.ones makes B entirely ones. Every output is therefore the same row–column dot product. The row starts with eight values written as 1.0003 and ends with eight −1 values. They first enter FP32 storage, then convert to FP16. .float() converts the selected result to FP32 and .item() reads its single value for printing. You choose the inputs and format; PyTorch chooses the matrix implementation. The browser uses a declared arithmetic order to isolate conversion and accumulation.
Run the examples below in your browser with no installation or GPU required. The browser simulates the calculation; it does not execute PyTorch or GPU code. To run the PyTorch snippet itself, use a Python environment with PyTorch and an NVIDIA GPU.
Idea 1 of 3
Inspect what reaches the multiplication
Definitions
- Number format
- A rule for representing numbers with a fixed set of bits. FP32 uses 32 bits, while FP16 and BF16 use 16.
- Rounding
- Replace a number by a nearby value that the chosen format can represent.
- Fraction precision
- How finely a format distinguishes nearby numbers at a given magnitude.
- Range
- The magnitudes a format can represent, from tiny non-zero values to large values.
FP16 distinguishes fewer nearby values than FP32. BF16 has fewer fraction bits than FP16, but a much wider range, similar to FP32. The two small formats therefore make different trade-offs. In this first experiment, keep the near-one inputs fixed and inspect the value before and after conversion.
Indices begin at zero: positions 0–7 contain the near-one input and 8–15 contain −1. Stepping selects an input position. B’s corresponding input is exactly one in every supported format.
Model assumptions: conversion rounds to the nearest representable value, with ties to an even last bit, including tiny values near zero. The trace is a CPU teaching calculation, with no hardware execution, performance or tensor-core mapping.
Try it
Predict which of the 16 row values change in FP16. Run and compare original and converted values. Change only the input format to BF16, then FP32; explain why −1 behaves differently from the near-one value.
Inspect the PyTorch example for these settings
PyTorch
import torch
row = torch.tensor([1.0003 if k < 8 else -1.0 for k in range(16)],
dtype=torch.float32, device="cuda")
a = row.repeat(16, 1).to(torch.float16)
b = torch.ones((16, 16), dtype=torch.float16, device="cuda")
c = a @ b
print(c[0, 0].float().item())Predict the result, then press Run.
Input position K = 0: original FP32 → FP16 conversion. Highlighting selects one position; conversion is not a hardware execution timeline.
1.0003 → 1
Rounded to a different value1.0003 → 1
Rounded to a different value1.0003 → 1
Rounded to a different value1.0003 → 1
Rounded to a different value1.0003 → 1
Rounded to a different value1.0003 → 1
Rounded to a different value1.0003 → 1
Rounded to a different value1.0003 → 1
Rounded to a different value-1 → -1
Unchanged-1 → -1
Unchanged-1 → -1
Unchanged-1 → -1
Unchanged-1 → -1
Unchanged-1 → -1
Unchanged-1 → -1
Unchanged-1 → -1
Unchanged- Selected original FP32 input
- 1.0003
- Selected converted input
- 1
- Changed inputs in this row
- 8
The input conversion happens before accumulation. Once a converted input has become exactly 1, no later addition can recover its original small excess.
Idea 2 of 3
Add products into an FP32 running sum
Definitions
- FP32 accumulator
- A running sum stored and rounded in FP32 after each addition in this model.
- Cancellation
- Adding values with opposite signs can leave a result much smaller than the individual inputs.
Each product here is the converted A input times one. Compare two FP32 running sums: one receives converted inputs; the reference receives original FP32 inputs. In the near-one pattern, eight negative ones cancel most of the positive sum. A small discarded input difference can then dominate the final answer.
Next try the small positive inputs to test range. Finally use alternating 1 and −0.5 to test values represented exactly in all three formats. The complete PyTorch example below uses an explicit loop on the CPU to match the model’s chosen order; it is an arithmetic explanation, not a GPU matrix implementation.
Model assumptions: each addition rounds to FP32 and K advances from 0 to 15. All A rows are identical and all B entries are one, so this one trace explains every C entry. Actual matrix kernels may use a different accumulation order or numeric rules.
Try it
Predict whether FP32 accumulation recovers the near-one input difference after FP16 conversion. Run, then step across K = 7 and K = 15 to compare sums before and after cancellation. Change only the pattern to small positive values and compare FP16 with BF16. Use the exact pattern as a final control.
Inspect the PyTorch example for these settings
PyTorch
import torch
row = torch.tensor([1.0003 if k < 8 else -1.0 for k in range(16)],
dtype=torch.float32)
converted = row.to(torch.float16).float()
total = torch.zeros((), dtype=torch.float32)
reference = torch.zeros((), dtype=torch.float32)
for k in range(16):
total = total + converted[k] * 1.0
reference = reference + row[k] * 1.0
print("Converted-input sum:", total.item())
print("Original-input sum:", reference.item())Predict the result, then press Run.
K = 0: multiply the converted input by B’s value of one, then add into the FP32 accumulator.
| Path | Previous sum | Input × B | FP32 sum after addition |
|---|---|---|---|
| Converted inputs | 0 | 1 × 1 | 1 |
| Original FP32 inputs | 0 | 1.0003 × 1 | 1.0003 |
- Final converted-input sum
- 0
- Final original-input reference
- 0.0024
- Final absolute difference
- 0.0024
- Changed inputs in this row
- 8
All 256 output entries equal 0 for these identical A rows and all-one B columns. Final values include all 16 additions, even while you inspect an earlier step. This declared sequential order is not a tensor-core trace.
Agreement on the exact pattern does not establish accuracy for every input. A useful accuracy check needs the values and formats that the real workload uses, as well as an appropriate reference.
Idea 3 of 3
Connect the arithmetic to tensor cores
Definitions
- Tensor core
- Specialised GPU hardware for supported matrix multiply-and-accumulate operations.
- Multiply–accumulate
- Multiply inputs and add their products to an existing output sum.
The previous chapter grouped outputs into tiles to reuse inputs. Tensor-core implementations also work on matrix groups: threads cooperate to supply input tiles and retain or store the output tile. On NVIDIA GPUs, the warp introduced earlier is one such cooperation group. A lane does not independently perform an entire matrix multiplication.
A 16 × 16 output tile with 16 K positions expresses 4,096 scalar multiply–accumulates. That describes arithmetic work, not hardware instructions, cycles or speed. Supported shapes, formats and cooperation rules depend on the GPU and programming interface.
The browser’s sequential additions do not execute or simulate tensor cores. For a @ b, PyTorch and its selected library implementation decide whether and how to use specialised hardware. A smaller dtype alone does not establish the implementation or its speed. The numeric exercise gives you an accuracy question to ask alongside a separate hardware measurement.
Try it
Explain two independent choices to a colleague: how the inputs are represented, and how products are accumulated. Then explain why the sequential browser trace cannot reveal which tensor-core instructions PyTorch selected.
Try a new case
Apply it
A new row alternates 1 and −0.5, and B remains all ones. Predict whether converting the inputs to FP16 changes the result of this model. Does that result prove that near-one inputs or very small inputs are safe too?
Compare your answer
Run the example on your desktop
Download this chapter’s opening PyTorch example. See the complete example index and setup appendix for CPU PyTorch, the supported GPGPU-Sim programs and optional NVIDIA GPU execution.
The browser still requires no installation. Downloads are generated from the displayed source; CPU checks and simulator checks do not establish GPU performance.
References
Use these official references to check the concepts and code on this page.
- PyTorch: floating-point type information (new tab)
Look up eps and the representable range of input formats.
- PyTorch: torch.matmul (new tab)
Separate the matrix-operation contract from a particular accumulation order.
- CUDA: warp matrix functions (new tab)
Further hardware reading: supported matrix operations cooperate across a warp.
These browser exercises do not run an installed PyTorch or CUDA toolkit. For optional local execution, choose documentation matching your installed tools. Links open online in a new tab; the questions and answer comparisons below work offline.
Use the documentation
Use the references to explain each answer, then apply it to this page’s example. No installation is needed.