SIMD today: packed extensions, GPUs, tensor cores and NPUs

After this lesson you will explain Table 30 row by row, describe a GPU's streaming multiprocessor as a multithreaded vector processor with a memory of its own, and say what a tensor core multiplies and what a TPU keeps stationary.

In 2015 Google started running a chip of its own design in its datacenters. It held 65,536 multipliers and none of the machinery this course has built since lesson 4: no caches, no branch prediction, no out-of-order execution, no multithreading. Its control logic took under 2 % of the die. On neural-network inference it was 15 to 30 times faster than the CPUs and GPUs of its day, and 30 to 80 times better in operations per watt.

How can a machine get faster by throwing away the hardware you spent a semester learning? Keep the question; the last section answers it. The way there runs through three machines you use every day — the SIMD unit of your laptop's CPU, the GPU, and the tensor cores inside the GPU — and each of them is Cray's vector idea of lessons 30–31, rebuilt for a different world.

Packed SIMD: the vector length written into the opcode

Start with the loop of the exam's strip-mining quiz, for (i=0; i<260; i++) Y[i] = X[i] + Y[i]; (lesson 21 takes its wrong options apart), and compile it not for VMIPS but for the CPU in your laptop.

From the mid-1990s the general-purpose CPUs (x86, ARM, PowerPC) added SIMD instruction sets (p. 414): Intel MMX (1996), then SSE, AVX and AVX-512; ARM NEON. A register of 128, 256 or 512 bits is treated as a short row of numbers packed side by side, and one instruction applies the same operation to all of them: 4 doubles in a 256-bit AVX register, 8 in a 512-bit AVX-512 register. The notes call these "SIMD ISA extensions", Hennessy & Patterson "multimedia SIMD extensions"; this lesson says packed SIMD.

For AVX, 260 = 65 × 4: the compiler emits a loop of 65 trips, one 4-wide add each. For AVX-512, 260 = 32 × 8 + 4: 32 trips of the 8-wide loop, and then four elements that no 8-wide add can take. They need a second loop, the scalar remainder loop:

// AVX-512: 8 doubles in every add
// main loop, 32 trips:
for (i = 0; i + 8 <= n; i += 8)
  Y[i:i+8] = X[i:i+8] + Y[i:i+8];
// remainder loop, 4 scalar trips:
for (; i < n; i++)
  Y[i] = X[i] + Y[i];

On a vector machine the same loop is the strip-mined loop of lesson 31. With MVL = 64, 260 = 4 × 64 + 4: five strips, each the same four vector instructions with VLR set to the strip's length, and no second loop:

// vector ISA: the length is in VLR
// strips: VLR = 64, 64, 64, 64, 4
for (i = 0; i < n; i += VLR) {
  VLR = min(n - i, MVL);
  // L.V, L.V, ADDVV.D, S.V:
  Y[i:i+VLR] = X[i:i+VLR]
             + Y[i:i+VLR];
}

The difference fits in one sentence, and Table 30 of the notes compares the two machines row by row: in packed SIMD the vector length is part of the opcode, in a vector ISA it is the content of a register.

Table 30 of the notes (p. 414), row by row. The memory-access cell is corrected (red pen below); the other cells say what the notes say.
AspectTrue vector machines (Cray, VMIPS)SIMD ISA extensions (SSE/AVX)
Vector lengtharbitrary, controlled by VLR, up to MVLfixed by the register width (e.g. 4, 8, 16 elements)
Scalabilitythe same code runs efficiently if MVL grows (e.g. 64 → 128 elements)code must be recompiled or rewritten when the width changes (SSE 128-bit vs AVX 256-bit)
Memory accessrich modes: unit stride, non-unit stride (LVWS), gather/scattermostly contiguous aligned loads/stores; gather only from AVX2, scatter only from AVX-512, both slower
Masking / predicationvector mask registersonly with the k-mask registers of AVX-512; older SIMD had no clean predication
Instruction countone vector instruction covers VL operations, whatever the MVLone instruction covers only what fits in the register; long vectors need many
Loop handlingstrip mining + VLR: the same loop handles any sizeleftovers need a scalar remainder loop or a separate vector clean-up
Philosophyan architecturally first-class vector modela scalar ISA "extended" with packed data operations

Three rows — vector length, instruction count, loop handling — are one fact seen three times. The register width fixes the length, so one instruction covers only what fits in the register, and whatever does not fit at the end goes to the remainder loop. For a loop of \(n\) elements on a width of \(W\):

\[ I = \lfloor n/W \rfloor + (n \bmod W) \]
\(I\)
add instructions executed for the loop
\(\lfloor n/W \rfloor\)
trips of the SIMD loop, one \(W\)-wide add each
\(n \bmod W\)
leftovers, one scalar add each in the remainder loop

A vector instruction covers VLR elements whatever MVL is, and the last strip simply sets a shorter VLR: \(\lceil n/\mathrm{MVL} \rceil\) instructions per operation, no second loop.

The width in the opcode also freezes the binary. A program compiled for AVX keeps issuing 4-wide adds on a core with 512-bit hardware: half the lanes stay idle until someone recompiles it. A program compiled for AVX-512 does not run at all on a core without AVX-512. A vector binary runs on any number of lanes — the transparent scaling of lesson 31 — and gets longer strips when MVL grows, provided its loop takes MVL from the hardware instead of writing 64 into the code.

Why can't a wider core simply widen an SSE instruction?

Because the width is part of what the instruction means — the register names say it: xmm is 128 bits, ymm 256, zmm 512 — and the program relies on exactly that many doubles per register. In a VLR machine the width lives in a register, so the same ADDVV.D fills whatever MVL the hardware has.

One loop, two kinds of binary

The loop Y[i] = X[i] + Y[i] for \(n\) elements, add instructions only: one column per clock cycle, one row per lane. Above, a packed-SIMD binary (one SIMD add per cycle; the leftovers run as scalar adds on one lane). Below, one vector binary that sets VLR to min(remaining, MVL) for each strip; an ADDVV.D with VLR = \(v\) on \(L\) lanes takes \(\lceil v/L \rceil\) cycles. The older core has 256-bit SIMD and a vector unit with MVL 64 on 4 lanes, the newer one 512-bit SIMD, MVL 128 and 8 lanes. Switching the core recompiles nothing; only the packed-binary menu does.

  1. Older core, AVX binary. Predict the cycles of both machines at n = 64, then at n = 63.
    What you should see

    At n = 64 both take 16 cycles: sixteen 4-wide adds, and one ADDVV.D that needs 64 / 4 cycles on 4 lanes. At n = 63 the packed loop runs 15 SIMD adds and a remainder loop of 3 scalar adds: 18 cycles. The vector machine sets VLR = 63 and needs ⌈63 / 4⌉ = 16. One element fewer, two cycles more.

  2. Keep n = 63 and switch to the newer core, still with the AVX binary. Predict the packed count before you look.
    What you should see

    Still 18: every add in the binary says "4 doubles", so 4 of the 8 lanes idle and lanes busy drops to 44 %. The vector binary, untouched, falls to 8 cycles: MVL 128 gives one strip, 8 lanes give ⌈63 / 8⌉ = 8. That is the scalability row of Table 30.

  3. Now pick the AVX-512 binary on the newer core. Predict n = 30, then n = 7.
    What you should see

    At n = 30: 3 SIMD adds + 6 scalar adds = 9 cycles, no better than the AVX binary's 7 + 2 = 9, because the remainder grew with the width. At n = 7: seven scalar adds and not a single SIMD instruction — the loop is shorter than one register. A wider register sends more of a short or ragged loop to the remainder loop, up to \(W - 1\) elements. Try this binary on the older core as well: it does not run.

  4. Choose the masked tail. What do n = 30 and n = 7 cost now, and which machine does it imitate?
    What you should see

    4 cycles and 1 cycle. The k-mask registers of AVX-512 let one masked add take all the leftovers, which is what VLR does on a vector machine. Masks reached packed SIMD only with AVX-512, the last extension in the notes' list — the masking row of Table 30.

The two rows left, memory access and masking, are about what packed SIMD lacked at birth. It loaded only contiguous, aligned blocks. Strided access, gather/scatter and per-element masks — all in vector ISAs since the Cray era — arrived generations later, if at all: gather with AVX2 (strided loads only as gathers), scatter and the k-mask registers with AVX-512. The notes misdate gather:

"AVX-512 is a vector processor." It is packed SIMD: a fixed 512-bit width in the opcode and no vector-length register. It borrowed masks and gather/scatter from vector machines, not the length register: a masked add can take the leftovers, but the compiler must build that mask for the tail, and a wider register still means new opcodes and a recompiled binary.

What packed SIMD buys, and what it costs

Why did the CPU makers take this road anyway? The notes list three gains (p. 415): compatibility with scalar CPUs — "no need for separate vector hardware, just extra datapaths in the same ALU pipeline"; widespread adoption, so compilers and libraries target it; and a good fit for multimedia and data-parallel kernels: audio, video, ML inner loops. Hennessy & Patterson add the hardware side: short vectors add little state to save on a context switch, they are fed by the caches instead of the banked memory a vector unit needs, and a 32- or 64-byte access touches at most two pages, where one 64-element vector load can touch many.

The notes' list of losses (p. 415) mirrors the other column of Table 30: no performance portability to wider registers, compilers that must generate remainder loops, clumsy strided and sparse access. The fourth bullet carries the wrong label:

ARM announced its Scalable Vector Extension (SVE) in 2016, and RISC-V ratified its "V" vector extension in 2021. Both are vector ISAs in Cray's sense: the length is not in the opcode, and they have mask registers and gather/scatter. A RISC-V loop opens every strip with vsetvli: it asks for the number of elements still to do and gets back how many this hardware will take, at most its maximum vector length; SVE gets the same effect with a mask that switches off the lanes past the end. One binary runs on every implementation, from 128-bit to 2048-bit registers. After twenty years in the opcode, the vector length moved back out of it.

Packed SIMD writes the vector length into the opcode; a vector ISA keeps it in a register (VLR). Remainder loops and recompilation for every new width both follow from that one choice.

A GPU is a vector processor that juggles threads

"The notes skip GPUs, so they are not examined." Chapter 9 of the notes is one paragraph and a QR code to another course's notes (p. 416). But the professor's exam-topic list names "Intro to GP-GPUs; Intro to Tensor Cores" and "Intro to Neural Processing Units", and the June 2025 paper asked a true/false on the GPU programming model. From here to the end of the lesson the content is background from Hennessy & Patterson (ch. 4 and ch. 7) and the vendors' documents — not in the notes, and not quoted from the slides, which are not available.

Here is DAXPY, Y = a·X + Y, written for an NVIDIA GPU in CUDA. There is no loop and no vector instruction: you write the body for one element and launch as many copies of it as there are elements.

__global__
void daxpy(int n, double a,
           double *x, double *y)
{
  // which element am I?
  int i = blockIdx.x * blockDim.x
          + threadIdx.x;
  if (i < n)   // the tail idles
    y[i] = a * x[i] + y[i];
}
// host: n threads, 256 per block
daxpy<<<(n + 255) / 256, 256>>>
     (n, a, d_x, d_y);

Each copy is a CUDA thread with its own index i and its own registers. The launch line creates a grid of blocks of 256 threads, and the hardware cuts every block into groups of 32 consecutive threads, the warps. A warp is the real unit of execution: one instruction is fetched for it, and its 32 threads execute that instruction in lockstep on the SM's SIMD lanes, each on its own registers. In the words of lessons 30–31, a warp instruction is a vector instruction with VL = 32: on 16 lanes it takes 32 / 16 = 2 cycles. NVIDIA calls the style SIMT: you program threads, the machine runs vectors.

The hardware that runs the warps is NVIDIA's streaming multiprocessor (SM); Hennessy & Patterson call it a multithreaded SIMD processor. Figure 30 of the notes (p. 392) draws it next to a vector processor, and the widget turns one into the other.

From a vector processor to a GPU, three boxes at a time

Figure 30 of the notes as one picture. The slider swaps, one at a time, the parts in which the GPU's SIMD processor differs from the vector processor; neutral boxes are the same in both machines.

  1. Before you move the slider: if a GPU hides latency with many threads instead of long vectors, which parts of the vector processor must change? Then slide to step 3 and count what changed.
    What you should see

    Three parts, and only three. The front end: one PC and a control processor become one PC per warp, a warp scheduler and a dispatch unit. The registers: a 16-element slice of each vector register becomes 1024 registers private to the threads of the lane. The memory path gains an address coalescing unit. The instruction cache, the single instruction register, the masked functional units in their lanes, the load/store unit and the memory interface stay: the SIMD spine is the same.

  2. Step 1 has many PCs but still one instruction register. Predict how many warps execute an instruction in a given cycle.
    What you should see

    One. The scheduler picks one ready warp at a time, and its instruction goes to all the lanes — on four lanes a 32-thread instruction passes through them in 8 cycles, like a vector instruction of length 32. SIMD inside the warp, fine-grained multithreading across warps (lesson 29). Real SMs have several schedulers, each with its own dispatch path and its own lanes; Figure 30 draws one.

  3. Step 2: why does the GPU need 1024 registers in every lane, when the vector processor keeps a 16-element slice of each vector register there? Read the "memory latency hidden by" readout at step 1, then at step 2.
    What you should see

    Because a switch must cost nothing. The registers of every resident warp stay in the register file, so the scheduler can issue from another warp in the next cycle without saving or restoring anything. At step 1 the readout asks where the waiting warps' registers would live; step 2 answers. The vector processor needs the registers of one instruction stream only: it hides latency with long vectors and chaining.

  4. Step 3: the 32 threads of a warp execute y[i] = a * x[i] + y[i] with 32 consecutive i. What does the coalescing unit receive, and what does it send to memory? What told VMIPS the same thing?
    What you should see

    It receives 32 separate addresses, one per thread, finds that they are consecutive, and sends a few wide memory transactions instead of 32 narrow ones. On VMIPS the instruction says it: L.V is a unit-stride load by definition. The GPU discovers at run time what the vector ISA states in the opcode — and a column access defeats it, as a bad stride defeats the memory banks.

The tail and the if: masks instead of VLR

A GPU has no vector-length register. The kernel's if (i < n) does its job: in the last block, the threads with i ≥ n are switched off by the hardware's per-thread mask and their lanes idle — the masked tail of the first widget. The same mechanism handles any if. When the threads of a warp disagree, the warp runs both paths, each under the mask of its own threads: the vector mask register of lesson 31, set by the hardware. This is branch divergence, and it costs what masking costs. A warp split 8 against 24 across an if-else issues both bodies with part of its lanes idle; a warp whose 32 threads take 32 different paths runs at 1/32 of its speed.

Memory: coalescing finds the stride at run time

VMIPS knows the access pattern from the instruction: L.V means unit stride. A GPU receives 32 independent addresses, one per thread. The address coalescing unit compares them and merges them into as few wide transactions as it can: 32 threads reading 32 consecutive doubles cost a couple of transactions, 32 threads walking down a column of a row-major matrix cost 32. It is the bad-stride story of lesson 31 again. The cure is a traversal order or a data layout in which consecutive threads touch consecutive words; padding the rows, the cure for bank conflicts, does not merge a column's addresses.

Latency: switch warps instead of waiting

A load that misses goes to DRAM and takes hundreds of cycles (the memory wall of lesson 22). A vector machine pays that latency once per 64-element vector and streams behind it. A GPU has short vectors, 32 elements, and no out-of-order engine. Instead, every SM keeps dozens of warps resident, and its warp scheduler picks, every cycle, one whose operands are ready — the fine-grained multithreading of lesson 29. The switch costs nothing because the registers of every resident warp are already in the register file: that is what the 1024 registers per lane of Figure 30 are for.

cycle 0 4 8 W0 waiting for DRAM lanes idle issue 4 busy cycles out of 12

Before: one warp

cycle 0 4 8 W0 waiting W1 W2 W3 issue 12 busy cycles out of 12

After: four warps

Fig. 1. Hiding a DRAM access by switching warps, as a schematic: each warp issues two instructions (blue) and then waits six cycles for its load (orange); a real DRAM access takes hundreds of cycles. Left: alone, the warp keeps the lanes busy 2 cycles in every 8. Right: with four warps the scheduler always finds a ready one, and every cycle issues an instruction.

How many warps are enough? Each warp offers a run of \(r\) instructions before its next stall, and the other warps must fill the \(t_{wait}\) cycles of its wait:

\[ N_{warps} \approx 1 + \frac{t_{wait}}{r} \]
\(t_{wait}\)
cycles a warp waits for memory after its run
\(r\)
instructions a warp issues between two stalls

In Fig. 1, 1 + 6/2 = 4 warps are exactly enough. With runs of 10 instructions and a 400-cycle DRAM access you need 41. That is why an SM is built to hold dozens of warps at once, and why a single GPU thread is slow while the GPU is fast.

The GPU vocabulary mapped onto the vector vocabulary of lessons 30–31, after Hennessy & Patterson (ch. 4).
NVIDIA / CUDA termvector-machine equivalentwhat it is
CUDA threadone element, one iteration of the loophas its own registers; runs in one lane
warp (32 threads)a vector instruction stream with VL = 32 (H&P: a thread of SIMD instructions)the unit the scheduler issues
thread blockone strip of a strip-mined loopruns on one SM; its threads share memory and can wait at a barrier (__syncthreads()); blocks are independent, so a kernel scales with the number of SMs
gridthe whole vectorisable loopeverything one kernel launch computes
streaming multiprocessor (SM)a multithreaded vector processor (H&P: multithreaded SIMD processor)lanes, register file, warp schedulers
warp schedulervector issue logic + multithreadingpicks a ready warp every cycle
lane ("CUDA core")vector laneexecutes one element
global (device) memorymain memorythe GPU's own off-chip DRAM
shared memoryno VMIPS counterpart: a memory managed by softwarean on-chip scratchpad per SM, banked like vector memory

A streaming multiprocessor is a multithreaded SIMD processor: a warp is one vector instruction stream of length 32 with per-lane registers; masks replace the VLR, coalescing replaces the strided load, and switching warps replaces long vectors as the way to hide memory latency.

Host and device: two memories, explicit copies

The kernel is half of a CUDA program. The other half runs on the CPU, the host, and its main job is moving data, because the GPU, the device, has its own DRAM (GDDR or HBM) on its own board, linked to the host by PCIe or NVLink. DAXPY on the host side:

size_t bytes = n * sizeof(double);
double *d_x, *d_y;
// 1. reserve device memory
cudaMalloc(&d_x, bytes);
cudaMalloc(&d_y, bytes);
// 2. copy the inputs in
cudaMemcpy(d_x, x, bytes,
           cudaMemcpyHostToDevice);
cudaMemcpy(d_y, y, bytes,
           cudaMemcpyHostToDevice);
// 3. run the kernel on the device
daxpy<<<(n + 255) / 256, 256>>>
     (n, a, d_x, d_y);
// 4. copy the result out
cudaMemcpy(y, d_y, bytes,
           cudaMemcpyDeviceToHost);
host device CPU GPU (SMs) main memory x, y device memory d_x, d_y 1 2 cudaMemcpy 3 daxpy<<<…>>> 4 cudaMemcpy reads, writes two memories link: PCIe or NVLink
Fig. 2. The two memories of the GP-GPU programming model. The host reserves space in device memory (1), copies the inputs over the link (2), launches the kernel, whose threads read and write device memory (3), and copies the result back (4). Requests are orange, data blue.

The pointers d_x and d_y hold addresses in the device's memory. In the basic model the host reaches that memory only through cudaMemcpy, and the kernel cannot see x and y in host memory. That is the quiz of the June 2025 paper (Quizzes, Question 3, p. 11): "In the GP-GPU programming model, the CPU (host) and GPU (device) have separate memory address spaces." True. Later CUDA versions added unified memory, which migrates pages between the two memories automatically, but it is a convenience laid on top: the basic model, and the hardware, keep two memories, and every byte crosses the link.

The split has a price: the copies cost time, and the GPU's own bandwidth serves only data that is already on the device, so a kernel must do enough work per byte copied to pay for the trip.

In the GP-GPU programming model the host and the device have separate memory address spaces: the program allocates device memory, copies the data in, launches the kernel and copies the results back.

Tensor cores: make the operand a matrix

Multiply two 4 × 4 matrices and add a third: D = A × B + C. Each entry of D is a row of A times a column of B plus one entry of C — four multiply-adds, FMAs. Sixteen entries make 64 FMAs.

A B C D × + = FP16 FP16 FP16 or FP32 FP16 or FP32 D entry = row of A · column of B + C entry 16 entries × 4 FMAs = 64 FMAs, each input used 4 times
Fig. 3. What a Volta tensor core computes as one operation. The highlighted row of A meets the highlighted column of B: \(d_{12} = \sum_k a_{1k} b_{k2} + c_{12}\), four multiply-adds. Every element of A takes part in 4 of these dot products (its row meets 4 columns), and so does every element of B.

On ordinary lanes those 64 FMAs are 64 lane operations, each reading its own a and b from the register file: 128 operand reads. A tensor core, a functional unit NVIDIA added to every SM with the Volta generation (2017), takes the three 4 × 4 matrices and returns D as one operation: 64 FMAs per clock, A and B in FP16, C and D in FP16 or FP32. It reads the 32 values of A and B once and uses each of them 4 times inside the unit. A Tesla V100 has 8 tensor cores per SM, 640 in all, for up to 125 tensor TFLOPS (NVIDIA's figure). Software sees a warp-level instruction: the 32 threads of a warp hold 16 × 16 tiles of the matrices in their registers and issue the multiply together.

Why does it pay? For three reasons you have met before. One operation of the unit does 64 FMAs: the vector idea, in two dimensions. Each value read is used 4 times, so the register-file traffic per FMA drops by four. And FP16 halves the bytes per operand. The accumulation stays in FP32 because a product of two FP16 numbers loses little, while a sum of thousands of them would lose precision or overflow: accumulate wide, store narrow.

The cost is that a tensor core does nothing for work that is not a matrix product. DAXPY gains nothing, and a model from Hennessy & Patterson shows why.

The roofline: when is a faster unit useless?

The roofline model (H&P, ch. 4) caps the flop rate a kernel can reach by two ceilings: the peak of the arithmetic units, and the memory bandwidth times the kernel's arithmetic intensity — the flops it performs per byte it moves from DRAM:

\[ P_{att} = \min(P_{peak},\ BW \times AI) \]
\(P_{peak}\)
peak flop/s of the arithmetic units (the flat roof)
\(BW\)
DRAM bandwidth in bytes/s (the slope)
\(AI\)
arithmetic intensity: flops per byte moved from DRAM

DAXPY does 2 flops per element, a multiply and an add, and moves 24 bytes: X[i] and Y[i] loaded, Y[i] stored, 8 bytes each. A product of two \(n \times n\) matrices does \(2n^3\) flops on \(3n^2\) doubles, read and written once each:

\[ AI_{DAXPY} = \frac{2}{3 \times 8} = \frac{1}{12} \] \[ AI_{n \times n} = \frac{2n^3}{3n^2 \times 8} = \frac{n}{12} \]

DAXPY stays at 1/12 flop per byte whatever \(n\) is; the matrix product grows with \(n\), to 85 for \(n = 1024\) (four times more in FP16, the tensor cores' format). The slope meets a roof at its ridge point. A kernel left of the ridge, like DAXPY, is memory-bound and cannot use a higher roof; only a kernel to its right, like the large matrix product, can.

1/16 1/4 1 4 16 64 flops per byte moved from DRAM (log scale) attainable flop/s (log scale) bandwidth × intensity vector-unit peak tensor-core peak matmul, n = 1024: AI 85 DAXPY: AI 1/12
Fig. 4. The roofline, on log scales with schematic heights. A kernel can go no faster than the bandwidth times its arithmetic intensity (the slope) nor faster than the arithmetic units (the roofs). DAXPY, at 1/12 flop per byte, sits on the slope far below both roofs; the 1024 × 1024 matrix product, at 85, is under the roof, and only it is lifted by the tensor cores.

A tensor core turns the operand from a vector into a matrix tile: 64 FMAs per operation, each input used 4 times, in reduced precision. It raises the compute roof, so only kernels of high arithmetic intensity — matrix products — gain.

NPUs: the whole chip becomes the matrix unit

A neural processing unit (NPU) is a domain-specific accelerator for neural networks: a large array of low-precision multiply-accumulate units (MACs) fed from on-chip memory, with none of the general-purpose machinery around it. The reference design is Google's first TPU (Jouppi et al., ISCA 2017): a 256 × 256 array of 8-bit MACs at 700 MHz, 28 MiB of on-chip memory managed by software, and about a dozen CISC-style instructions (Read_Weights, MatrixMultiply, Activate …) sent by the host over PCIe. Each MAC counts as two operations, so the peak is 92 TOPS, tera-operations per second:

\[ 2 \times 65\,536 \times 700\ \text{MHz} \] \[ \approx 92 \times 10^{12}\ \text{op/s} \]

The array is systolic: data moves between neighbouring cells in lockstep, pumped like a heartbeat, so every value fetched from memory is used again and again on its way across. The TPU's dataflow is weight-stationary: the weights are loaded into the cells and stay there; the activations enter from the left, each row one cycle after the row above, and move right; the partial sums move down and leave at the bottom into the accumulators. The widget runs it on a 3 × 3 array.

Pump a 3 × 3 weight-stationary array

The cells hold the weights \(W = \begin{pmatrix} 1 & 2 & 0 \\ 0 & 1 & 3 \\ 2 & 0 & 1 \end{pmatrix}\). Three input vectors \(x_0 = (1, 2, 3)\), \(x_1 = (2, 0, 1)\), \(x_2 = (1, 1, 1)\) stream through, and each output is \(y_b = x_b W\): \(y_b[j] = \sum_k x_b[k]\, W[k][j]\). Each "Next" is one clock cycle.

  1. Before pressing Next: which cell works in cycle 0, and in which cycle does the first finished output leave the bottom of the array?
    What you should see

    In cycle 0 only the top-left cell works: 0 + 1 × 1 = 1. The first output, y₀[0] = 1·1 + 2·0 + 3·2 = 7, leaves the bottom of column 0 in cycle 2, after one multiply-accumulate in each of the three rows.

  2. Go to cycle 3 and count the busy cells. Why not 9? How many would work in each cycle in the middle of a stream of 100 inputs?
    What you should see

    Seven. The work moves as a diagonal wavefront, and at the start and at the end of a stream some cells have nothing to do — fill and drain, as in a vector pipeline. With 100 inputs all 9 cells would be busy in every cycle of the middle part.

  3. At the last step, compare the two counters. In the TPU's 256 × 256 array, how many multiply-accumulates does one activation read feed?
    What you should see

    Here 9 reads feed 27 MACs: each activation is used once in each of the 3 columns, and each weight serves all 3 inputs. In the TPU one activation read feeds 256 MACs, and a weight, loaded once, serves every input of the batch. Operands never go back to a register file between operations: that reuse is where the energy is saved.

  4. How many cycles does a stream of B inputs take on this 3 × 3 array?
    What you should see

    B + 4, here 7: the last input enters row 0 in cycle B − 1 and still has 2 rows and 2 columns to cross. For a K × N array it is B + K + N − 2: a fill-and-drain cost of K + N − 2 cycles paid once per stream, like the start-up of a vector instruction.

The systolic array is to a matrix what the vector pipeline of lesson 30 is to a vector: the latency of the wavefront is paid once, then results stream out, one per column per cycle.

Hennessy & Patterson's five guidelines

Hennessy & Patterson (6th ed., ch. 7) distil such designs into five guidelines for domain-specific architectures. The TPU follows each of them:

  1. Dedicated memories to minimise data movement — a 24 MiB unified buffer for activations and 4 MiB of accumulators, both managed by software; no caches.
  2. Spend the resources saved on advanced microarchitecture on more arithmetic units or bigger memories — 65,536 MACs, and control logic under 2 % of the die.
  3. The easiest form of parallelism that matches the domain — SIMD in two dimensions, a systolic array; no MIMD.
  4. Reduce data size and type to the simplest the domain needs — 8-bit integers. Quantising Google's Inception network shrank it from 91 MB to 23 MB.
  5. A domain-specific programming language — models written in TensorFlow, compiled for the TPU.

The NPUs of phones and laptops (Apple's Neural Engine, Qualcomm's Hexagon, the NPU blocks of recent Intel and AMD chips) apply the same recipe at a few watts: 8-bit or FP16 MAC arrays and large local SRAM.

Back to the puzzle

The TPU of the hook beat the CPUs and GPUs of its day without caches, branch prediction, out-of-order execution or multithreading. That machinery exists for programs that hide their future: branches whose direction is unknown until they execute, loads whose latency depends on a cache, independent instructions that the hardware must discover inside a window of a few hundred (lesson 8). A neural-network layer announces everything in advance — a matrix product of known size, a fixed dataflow, no data-dependent branches. Hardware that rediscovers what the program already states wastes silicon and energy; the TPU spends them on multipliers and on-chip memory instead. It is the choice Cray made in lesson 30, taken as far as it goes.

Every machine of this lesson is a step on the same road. Packed SIMD writes the parallelism into the opcode, a GPU into thousands of threads, a tensor core into a matrix tile, and an NPU into the whole chip.

An NPU is the vector idea pushed to two dimensions and to one domain: a systolic array that reuses every operand hundreds of times, fed from software-managed on-chip memory, in low precision, with none of the hardware that discovers parallelism at run time.

Exam-style questions

Explain the main concepts and benefits of each one of the following techniques used by data-parallel architectures:

  1. SIMD ISA extensions (e.g. SSE/AVX)
  2. Vector length register (VLR) with strip mining
  3. Vector mask registers
  4. Multithreaded SIMD execution in GP-GPUs (warps)
  5. Separate host and device memories in the GP-GPU programming model
  6. Address coalescing in GP-GPUs
  7. Tensor cores
  8. Systolic arrays in Neural Processing Units
Show model answer
  1. A scalar ISA extended with instructions that apply one operation to the elements packed in a wide register (128, 256 or 512 bits: 2, 4 or 8 doubles). Benefits: several data operations per fetched and decoded instruction; cheap to add as extra datapaths in the existing pipeline; little extra state, fed by the caches; available in every CPU, so compilers and libraries use it for media and ML kernels. Limits: the width is fixed in the opcode, so a wider register needs new instructions and recompiled code, leftovers need a scalar remainder loop, and strided/sparse access and masks came late (gather with AVX2, scatter and masks with AVX-512).

  2. VLR holds the number of elements, 0 ≤ VLR ≤ MVL, that every vector instruction processes; MVL is the hardware length of a vector register. Strip mining (a compiler transformation) splits a loop of n iterations into ⌈n/MVL⌉ strips: full strips with VLR = MVL and one strip of n mod MVL. Benefits: one loop handles any n with no remainder loop, nothing beyond the arrays is touched, and the same binary uses more lanes or a longer MVL if the loop takes its strip length from the hardware (ARM SVE, RISC-V V).

  3. A mask register holds one bit per element; a vector compare sets it, and the following vector instructions update only the elements whose bit is 1, leaving the others unchanged (predication). Benefits: loops with if statements stay vectorised, with no branch per element, and masked elements raise no exceptions. Cost: masked elements still use their slot, so an if-else executes both bodies. GPUs do the same per thread (branch divergence).

  4. A GP-GPU streaming multiprocessor is a multithreaded SIMD processor. Threads are grouped into warps of 32; one instruction is fetched per warp and executed in lockstep on the SIMD lanes for all 32 threads, each thread with private registers (SIMT). Many warps are resident; every cycle the warp scheduler issues from a ready one, and since all their registers stay in a large register file, switching costs nothing. Benefits: DRAM latency of hundreds of cycles is hidden by the other warps instead of by caches or out-of-order logic; the programmer writes sequential code for one element; the same kernel scales over any number of SMs.

  5. The CPU (host) and the GPU (device) have separate memory address spaces, physically two DRAMs linked by PCIe/NVLink. The host allocates device memory (cudaMalloc), copies the inputs (cudaMemcpy), launches the kernel and copies the results back. Benefit: the device memory (GDDR/HBM) is built for bandwidth and serves the kernel at full speed. Cost: every transfer crosses the link, so a kernel must do enough work per byte copied; unified memory only automates the migration.

  6. Each thread of a warp computes its own address; the coalescing unit merges the 32 addresses of a warp's load or store into as few wide memory transactions as possible. Consecutive threads touching consecutive words cost one or two transactions — the run-time equivalent of a unit-stride L.V — while column or scattered accesses cost up to 32. Benefit: memory bandwidth is used efficiently without the program stating the stride; it rewards unit-stride data layouts.

  7. A tensor core is a functional unit in each SM that computes D = A × B + C on small matrices as one operation (Volta: 4 × 4, A and B in FP16, accumulation in FP16 or FP32, 64 FMAs per clock), used through a warp-level matrix instruction. Benefits: one instruction for 64 FMAs; each input reused 4 times inside the unit, so less register traffic per flop; reduced precision halves data movement — up to 125 TFLOPS on a V100. Only matrix-shaped, high-intensity work (dense linear algebra, deep learning) gains.

  8. A systolic array is a grid of multiply-accumulate cells in which data moves between neighbours in lockstep, so each operand fetched from memory is used by a whole row or column. In the TPU (256 × 256 8-bit MACs, weight-stationary) the weights stay in the cells, activations flow right and partial sums flow down into accumulators: one activation read feeds 256 MACs. Benefits: very high throughput (92 TOPS) at low energy per operation, simple control, software-managed on-chip memory instead of caches. Limits: only dense matrix work, and a fill/drain latency per stream.

The June 2026 paper asked exactly this form for superscalar techniques: eight sub-points, five lines each. For every point write the mechanism (what the hardware or the compiler does), the benefit, and one cost. Points 2–3 are the notes' own material (lesson 31); points 4–8 are the background of this lesson, which the topic list ("Intro to GP-GPUs; Intro to Tensor Cores", "Intro to Neural Processing Units") makes examinable but the notes do not cover.

Compare a vector processor (VMIPS-like) and the streaming multiprocessor of a GP-GPU. For each one describe: the main concepts; how the length of a loop is handled; how a conditional statement inside the loop is handled; how memory latency is hidden; the main benefits; the main drawbacks.

Show model answer
Vector processor (VMIPS)GP-GPU streaming multiprocessor
Main conceptsSIMD with one PC; vector registers of MVL = 64 elements; deeply pipelined vector FUs in lanes with chaining; one vector load/store unit; a scalar control processor. One instruction = a whole vector.Multithreaded SIMD: threads grouped in warps of 32; one instruction per warp, executed in lockstep for its 32 threads, each with private registers (SIMT); dozens of resident warps, a warp scheduler issues from a ready one each cycle; a host CPU and a separate device memory.
Loop lengthVLR (≤ MVL) and strip mining: ⌈n/MVL⌉ strips.A grid of n threads in blocks; the tail is masked off by if (i < n); no VLR.
ConditionalsA vector compare sets the mask register; the instructions execute under the mask.Branch divergence: the warp executes both paths, each under a per-thread mask.
Memory latencyPaid once per vector, then one element per cycle from interleaved banks; strided and gather/scatter loads; little or no data cache.Hidden by switching among resident warps, whose registers stay in the register file; coalescing merges a warp's accesses; small caches save bandwidth.
BenefitsFew instructions (DAXPY: 6 instead of 578), no loop overhead, deep pipelines without hazards, energy-efficient, scales with lanes under the same code.Very high throughput on massively parallel work; latency hiding without out-of-order logic; sequential code for one thread; scales with the number of SMs.
DrawbacksOnly vectorisable loops; needs a high-bandwidth memory; masked elements still cost cycles; bank conflicts with bad strides.Divergence and uncoalesced accesses waste lanes; host–device copies; one thread is slow; needs thousands of threads to fill the machine.

The forms of July 2024 (fine-grained vs simultaneous multithreading) and June 2025 (a dual-core chip) were tables with blank lines: one precise line per cell, naming the mechanism and not only the term. The loop-length, conditional and latency rows are the mapping of this lesson — VLR against a masked tail, mask register against divergence, latency amortised by long vectors against latency hidden by warps.

In the GP-GPU programming model, the CPU (host) and GPU (device) have separate memory address spaces. (True/False)

  • True The host allocates device memory, copies the data in with cudaMemcpy, launches the kernel and copies the results back: two memories, two address spaces.
  • False Unified memory only migrates pages between the two memories automatically; the basic model and the hardware keep two separate memories.
Show model answer

True. The device has its own DRAM; pointers to device memory are valid only on the device, and data moves explicitly (cudaMalloc, cudaMemcpy host to device, kernel launch, cudaMemcpy device to host).

Question 3 of the quizzes of 10 June 2025, word for word — the only GPU question of the three papers. The wrong answer usually comes from unified memory, which automates the copies but does not merge the memories.

Let's consider the loop for (i=0; i<260; i++) Y[i] = X[i] + Y[i]; compiled for a processor with the AVX-512 SIMD extension (512-bit registers, 64-bit elements), without masked instructions: the leftover elements are handled by a scalar remainder loop. How many SIMD add instructions and how many scalar add instructions are executed? (SINGLE ANSWER)

  • Answer 1: 32 SIMD adds and 4 scalar adds 8 doubles per register: 260 = 32 × 8 + 4.
  • Answer 2: 33 SIMD adds and 0 scalar adds That needs a masked last add, which the question excludes.
  • Answer 3: 65 SIMD adds and 0 scalar adds That is AVX, with 4 doubles per 256-bit register: 260 = 65 × 4.
  • Answer 4: 5 SIMD adds and 0 scalar adds That is the number of strips on VMIPS with MVL = 64, a vector machine, not packed SIMD.
  • Answer 5: 8 SIMD adds and 4 scalar adds 8 is the width of the register, not the number of trips of the loop.
Show model answer

Answer 1. A 512-bit register holds 8 doubles, and 260 = 32 × 8 + 4: the SIMD loop runs 32 times, and the 4 leftover elements go to the scalar remainder loop. With the k-mask registers of AVX-512 the 4 leftovers would take one masked add (33 instructions); on a vector machine the loop is strip-mined with VLR instead (5 strips with MVL = 64).

The same loop as the strip-mining quiz of the papers, on a different machine. The distractors are the counts of the neighbouring machines — 65 for AVX, 33 for the masked version, 5 for the VMIPS strips — in the style the papers build their options.

Which of the following statements about the streaming multiprocessor of a GP-GPU are true? (MULTIPLE ANSWERS)

  • A warp executes one instruction for its 32 threads in lockstep, each thread with its own registers. SIMT: a warp instruction is a vector instruction of length 32 with per-lane registers.
  • The latency of a DRAM access is hidden mainly by switching to another ready warp. Fine-grained multithreading at no switch cost: the registers of all resident warps stay in the register file.
  • The tail of a loop is handled with a vector length register, as in VMIPS. There is no VLR: the threads with i ≥ n are masked off by the if of the kernel.
  • When the threads of a warp take different paths of an if, both paths are executed, each under a mask. Branch divergence: the vector mask of lesson 31, managed per thread by the hardware.
  • A tensor core performs one fused multiply-add per lane per clock. A tensor core computes D = A × B + C on 4 × 4 matrices: 64 FMAs per clock as one operation.
Show model answer

Answers 1, 2 and 4. A warp is one instruction stream for 32 threads with private registers; latency is hidden by switching warps; divergent paths run one after the other under masks. A GPU has no VLR (the tail is masked), and a tensor core is a matrix unit, not a wider FMA.

The two false options are the traps: carrying the VLR over from the vector machine, and imagining the tensor core as a faster ordinary lane.

Check yourself

A program compiled for SSE2 (two doubles per add, 128-bit registers) runs on a core with AVX-512 hardware. What happens?

  • It runs, but every add uses 2 of the 8 lanes The width is part of each instruction; the wider hardware cannot widen it, so the program needs recompiling to use the lanes.
  • It runs on all 8 lanes automatically That is what a VLR binary does on a vector machine; a packed-SIMD instruction keeps the width of its opcode.
  • It does not run Old instructions remain supported; it is the newer binary on the older core that does not run.
  • It runs, but needs a remainder loop it did not have before The remainder depends on the width the program was compiled for, which has not changed.

Which of these did vector ISAs (Cray, VMIPS) have from the start, while packed SIMD got them late or never?

  • A length register that the same binary reads on any hardware VLR; SSE and AVX never had one. RISC-V V brought it back, and ARM SVE gets the same effect with masks.
  • Per-element mask registers Packed SIMD got clean predication only with the k-mask registers of AVX-512.
  • Indexed loads and stores (gather/scatter) Gather arrived with AVX2, scatter with AVX-512.
  • A separate opcode for every register width That is the packed-SIMD way, and the reason for its bloat; a vector ISA has one ADDVV.D for every length.
  • A scalar remainder loop for the leftovers The other way round: packed SIMD needs it, strip mining with VLR does not.

Hennessy & Patterson list exactly these three omissions of multimedia SIMD. AVX2 and AVX-512 added indexed access and masks; a length kept out of the opcode came back only with RISC-V V and ARM SVE.

A warp of 32 threads reaches an if-else. 8 threads take the then-body, 24 the else-body; each body is 10 instructions. How many warp instructions are issued for the two bodies, and what fraction of the lane slots does useful work?

  • 10 instructions, 100 % Only if all 32 threads took the same path; here both paths must run.
  • 20 instructions, 50 % Both bodies run, each under a mask: 8 × 10 + 24 × 10 = 320 useful lane slots out of 20 × 32 = 640.
  • 20 instructions, 25 % The else-body keeps 24 lanes busy, not 8.
  • 40 instructions, 50 % Each body is issued once, under its mask — not once per group of threads.

With two bodies of equal length the efficiency is 50 % however the threads split, as long as both paths are taken.

The 32 threads of a warp read the first column of a row-major matrix of doubles with 32 columns: thread t loads M[t][0]. What does the coalescing unit produce?

  • One wide transaction The addresses are 32 × 8 = 256 bytes apart: no single transaction covers them.
  • Two 128-byte transactions That is the case of a row: 32 consecutive doubles.
  • 32 separate transactions Each address falls in a different memory segment — the column access is the bad stride of lesson 31.
  • 8 transactions, one per memory bank Coalescing groups addresses into segments; with a 256-byte stride no two share one.

A tensor core computes D = A × B + C on 4 × 4 matrices. How many of its 64 multiplications use the element a₀₀?

  • 1 That is the ordinary FMA on a lane, where every operand is read for one use.
  • 4 a₀₀ meets b₀₀, b₀₁, b₀₂ and b₀₃: it takes part in d₀₀, d₀₁, d₀₂ and d₀₃.
  • 16 16 is the number of entries of D; a₀₀ appears only in row 0 of D.
  • 64 64 is the total number of multiplications.

You move DAXPY to a GPU that has tensor cores. How much faster does DAXPY get thanks to the tensor cores?

  • About 8 times, the extra compute of the tensor cores DAXPY sits on the bandwidth slope of the roofline; a higher compute roof does not reach it.
  • Not at all At 1/12 flop per byte DAXPY is memory-bound, and it is not a matrix product: tensor cores raise the compute roof only for kernels of high intensity.
  • About 2 times, thanks to FP16 The tensor cores do not run DAXPY at all; it is a vector operation, not a matrix multiply-accumulate.

In the TPU's 256 × 256 weight-stationary array, one activation value is read once from the unified buffer. How many multiply-accumulates use it?

  • 1 That is a MAC unit reading its operands from a register file, the design the systolic array avoids.
  • 256 The activation travels along its row and meets the 256 weights of that row, one MAC per cell.
  • 512 It crosses one row of 256 cells, not a row and a column.
  • 65,536 That is the number of cells; each activation crosses only its own row.

Key take-aways

What to watch and read