Gherkin Labs

Documenting an undocumented chip: the TensorCore of TPU v5e

The aim of this post is to be an unofficial reference for the TensorCore of Google's TPU v5e. It presents the instruction set of this core, both what each instruction does and how many cycles it takes.

The data in this document has been collected experimentally by running probes on a TPU. We do not claim that this document is exhaustive or perfect, but it is a fair overview of our understanding of the core.

This document does not cover the SparseCore, later TPU versions, or topologies of multiple TPUs.

This work is not affiliated with Google.

Methodology

In our previous post we presented our work building a simulator for TPU v5. This simulator allowed us to deepen our theoretical understanding of TPUs. It let us confirm the communication primitives between a server and an attached TPU. It also allowed us to start deciphering the instruction set of TPUs for both versions 5 and 7. However, it still had its limits: precise semantics and latencies of the instruction set can only be recovered by running micro-kernels on a physical device.

The natural next step was to use the simulator to write a custom driver for TPUs. This driver is minimal: its only purpose is to connect to a TPU and run small binary probes on the TensorCore. These probes are assembled using an in-house TPU assembler. The driver exposes the same low-level performance counters as one would access when using the official driver.

Once the driver worked, we built a small harness for LLM agents to explore the TensorCore of v5e in depth. Given a question, the LLMs would write a sequence of probes: small programs, each meant to learn one microarchitectural detail of the TPU. The results of the probes were recorded and used to close the gap between our software simulator and a real TPU. We steered this semi-automatic exploration fairly heavily as LLMs tends to go toward complexity. This was especially valuable to prevent the simulator code from becoming a tangled mess of one-off and brittle fixes.

This work focused on the TPU v5e chip for cost reasons: we are unfunded and first wanted to check our methodology on a cheaper device.

The TensorCore at a glance

The chip

A v5e chip has one TensorCore and 16 GiB of HBM. The TensorCore can't read HBM directly: data reaches the core only by DMA, into its vector memory (VMEM), its scalar memory (SMEM) or its instruction memory (IMEM). The host loads a program with two DMA commands sent through the firmware: host memory to HBM, then HBM to IMEM. The host talks to the chip over PCIe. The core runs at 1.5 GHz. Every cycle count in this post is a core cycle.

The core

One TensorCore. IMEM feeds one bundle a cycle to the scalar side: slots s0 and s1, scalar registers s0 to s31, predicates p0 to p13, SMEM, 512 sync flags and the vector-to-scalar FIFO. The vector parts of the bundles wait in the vector queue, which issues the oldest first, to the vector side: four vector ALU slots, vector registers v0 to v63, mask registers vm0 to vm15, 4 matrix units, 3 cross-lane units and the EUP, each with a result queue, and 3 loads and 1 store between the registers and VMEM. push_v2s sends a value back from the vector side to the scalar side IMEM 4 MiB, 65,536 bundles one bundle a cycle scalar side slot s0 slot s1 s0..s31, 32 bits p0..p13, 1 bit SMEM 1 MiB sync flags 512 counters vector-to-scalar FIFO vpop.v2s pops it vector queue oldest issues first vector parts vector side valu0 valu1 valu2 valu3 v0..v63: 8 sublanes × 128 lanes × 32 bits vm0..vm15: a mask bit per byte 4 MXUs matrix 3 XLUs cross-lane EUP transcendentals result queue result queue result queue pops move results back into registers 3 loads 1 store VMEM 128 MiB, rows of 512 bytes push_v2s

Some words we use throughout:

When a program breaks a rule, the core ends in one of these ways:

An instruction is refused when the assembler or the chip rejects it.

The scalar side runs the control flow, computes addresses and does basic arithmetic. It has two scalar slots, 32 scalar registers of 32 bits, 14 predicate registers, SMEM and the sync flags.

The vector side does the computations, on vector registers of 8 sublanes × 128 lanes × 32 bits. It has four vector ALU slots, loads and stores between the registers and VMEM, and three kinds of slower units: 4 matrix units (MXU), 3 cross-lane units (XLU) and one unit for transcendentals (EUP).

Push, then pop. The slower units don't give their result back in the instruction that starts them. An instruction pushes the work to the unit, and the result goes into the unit's result queue. A later pop moves the oldest result from that queue into a vector register.

From fetch to issue. When a bundle is fetched, its scalar part runs at once and its vector part goes into the vector queue. The queue has 20 slots, and every fetched bundle takes one. A bundle gives its slot back 12 cycles after its vector part issues. If it has no vector part then it gives it back 4 cycles after its fetch. When no slot is free the fetch waits. The vector side issues the vector part of the oldest bundle in the queue as soon as all of its instructions can start.

One consequence of this design is that the scalar side runs ahead of the vector side by up to 20 bundles.

sfence makes the scalar side wait until every vector part in the queue has issued. vpop.v2s waits only for the value it pops.

The scalar side can't read vector registers. To get a vector value, the vector side pushes element 0 of a register with push_v2s, and the scalar side pops it with vpop.v2s (see Pops and the result FIFOs). For more data, a DMA can copy VMEM into SMEM.

Floating-point behavior

Bundles

One 512-bit bundle: the scalar slots s0 and s1; the vector ALU slots valu0 to valu3; the matrix or cross-lane slots vex0 and vex1; the pop slots vres0 and vres1; the load slots vld0 to vld2; the store slot vst; then the shared operands, 6 immediates and 4 scalar ports. Each of the 14 slots has a guard predicate. The scalar slots form the scalar part, which runs at fetch; the others form the vector part, queued and then issued in order s0 (p) s1 (p) scalar valu0 (p) valu1 (p) valu2 (p) valu3 (p) vector ALU vex0 (p) vex1 (p) matrix or cross-lane vres0 (p) vres1 (p) pops vld0 (p) vld1 (p) vld2 (p) loads vst (p) store 6 imm 4 vs shared operands scalar half runs at fetch vector half queued, then issued in order One 512-bit bundle. (p): the slot's guard predicate. A DMA takes s0 and s1; vld2 can hold a misc op instead.

A bundle is 512 bits and has 14 slots:

Slots Holds
s0, s1 scalar instructions: arithmetic, compares, branches, SMEM loads and stores. A DMA takes both.
valu0 to valu3 vector ALU instructions.
vex0, vex1 pushes to the matrix and cross-lane units
vres0, vres1 pops from the matrix units, the cross-lane units and the EUP
vld0 to vld2 vector loads. vld2 can hold one misc instruction instead.
vst the one vector store

The slots also share six immediates and four scalar register ports. These ports are how instructions (both scalar and vector) refer to a scalar register. The scalar registers are read at fetch, before the vector part goes into the vector queue.

Most instructions take a fixed number of cycles, given in the tables. No padding is needed between dependent instructions: if one reads a register before its value is ready, the core waits. A program is fast when dependent instructions are far enough apart that it doesn't.

Predicates. Every slot has a guard. A slot is considered empty if it is guarded by p15, "never".

State

How many Each
scalar registers s0..s31 32 32 bits
predicates p0..p13 14, plus p14 (true) and p15 (false) 1 bit
vector registers v0..v63 64 4 KiB: 8 sublanes × 128 lanes
mask registers vm0..vm15 16 4 bits per 32-bit element, one per byte
sync flags 512 32 bits, a done bit, a public bit
SMEM 1 MiB 32-bit words
VMEM 128 MiB rows of 512 bytes, the size of one sublane
IMEM 4 MiB 65,536 bundles of 64 bytes
HBM 16 GiB reached only by DMA

Sync flags are the TPU version of mutexes and atomics. A bundle can wait for a sync flag to reach a given value. DMA instructions credit sync flags to tell the core they are done. The core has 512 flags and the host 128.

Reading the instruction tables

Each section starts with a table of its instructions. Two rows, one from the scalar side and one from the matrix unit:

Instruction Slot Cycles What it does
sadd.s32 sD, sX, sY|imm s0, s1 1 Wrapping add.
matmul.{f32r,bf16,bf8,if8bf16}[.lgmr.msr{a,b}] uU, vS [, vmsk=vmK] vex0, vex1 125 / 131 Multiply each matrix row the register holds by the GMR.

Operand names:

Scalar side

Instructions

Instruction Slot Cycles What it does
Integer and bitwise
sadd.s32 sD, sX, sY|imm s0, s1 1 Wrapping add.
ssub.s32 sD, sX, sY|imm s0, s1 1 Wrapping subtract; computes sY − sX.
sadd.ovf sD, sX, sY|imm s0, s1 1 Add as sadd.s32. On a signed overflow the core faults.
ssub.ovf sD, sX, sY|imm s0, s1 1 Subtract (sY − sX) as ssub.s32, with the same overflow fault as sadd.ovf.
sand.u32 sD, sX, sY|imm s0, s1 1 Bitwise and.
sor.u32 sD, sX, sY|imm s0, s1 1 Bitwise or.
sxor.u32 sD, sX, sY|imm s0, s1 1 Bitwise xor.
sshll sD, sX, sY|imm s0, s1 1 Shift left; an amount of 32 or more gives 0.
sshrl sD, sX, sY|imm s0, s1 1 Logical shift right; an amount of 32 or more gives 0.
sashr sD, sX, sY|imm s0, s1 1 Arithmetic shift right; an amount of 32 or more shifts by 31, leaving only the sign.
smax.u32 sD, sX, sY|imm s0, s1 1 Unsigned max.
smin.u32 sD, sX, sY|imm s0, s1 1 Unsigned min.
smov sD, sY|imm s0, s1 1 Copy a register or an immediate.
sclz sD, sY|imm s0, s1 1 Leading zeros.
smul.u32 sD, sX, sY|imm s0 2 Low half of the integer product.
smulh.u32 sD, sX, sY|imm s0 2 High half of the integer product.
Float
smax.f32 sD, sX, sY|imm s0, s1 1 f32 max; a NaN operand gives NaN.
smin.f32 sD, sX, sY|imm s0, s1 1 f32 min; a NaN operand gives NaN.
sceil.f32 sD, sY|imm s0, s1 1 Float ceiling.
sfloor.f32 sD, sY|imm s0, s1 1 Float floor.
smul.f32 sD, sX, sY|imm s0 2 f32 product.
sadd.f32 sD, sX, sY|imm s1 2 f32 add.
ssub.f32 sD, sX, sY|imm s1 2 f32 subtract.
scvt.f32.s32 sD, sY|imm s0, s1 2 Convert s32 to f32, rounding to nearest even.
scvt.s32.f32 sD, sY|imm s0, s1 2 Convert f32 to s32, rounding to nearest even; out of range saturates, NaN gives 0.
Compares and predicates
scmp.{eq,ne} pN, sX, sY|imm s0, s1 2 Compare sX = sY (eq) or sX ≠ sY (ne), into a predicate register. These two take no type suffix.
scmp.{gt,ge,lt,le}.{s32,u32} pN, sX, sY|imm s0, s1 2 Compare sX >, ≥, < or ≤ sY, signed (s32) or unsigned (u32), into a predicate register.
scmp.{eq,ne,gt,ge,lt,le}.f32 pN, sX, sY|imm s0, s1 2 The same six compares on f32, into a predicate register.
scarry.u32 pN, sX, sY|imm s0, s1 2 Carry out of an add, into a predicate.
spred.or pN, pX[!], pY[!] s0, s1 2 OR of two predicates, each one optionally negated.
sinfnan.f32 pN, sX s0, s1 2 Test for inf or NaN, into a predicate.
Divide and result FIFOs
sdiv[.push_quotient|.push_remainder] sX, sY|imm s0 9 Unsigned divide; pushes quotient and remainder to the divide FIFO. On the chip the two push forms also push both words.
spop.drf sD s0, s1 1 Pop the divide FIFO.
vpop.sfrf sD s0, s1 1 Pop the sync-flag FIFO.
vpop.v2s sD s0, s1 1 Pop the vector-to-scalar FIFO.
SMEM
sld.smem sD, smem[sX|imm|sX + imm] s1 6 Load one SMEM word, at the address in sX, at an immediate address, or at sX plus an immediate.
sst.smem sX, smem[sY|imm] s1 11 Store one SMEM word, at the address in sY or at an immediate address.
Control flow
sbranch.{rel,abs} label|imm s0, s1 —2 Relative or absolute branch; conditional only through predication.
sbranch.ind sX s0, s1 —2 Branch to the address in a register; conditional only through predication.
scall.{rel,abs} label|imm, link=sD s0, s1 —2 Relative or absolute call; conditional only through predication.
shalt s0, s1 — Halt once every queued vector part has issued.
shalt.yield s0, s1 — Halt as shalt, reported to the host as yielded.
shalt.yield.cond s0, s1 — Conditional yield; on a one-core v5e it never yields and does nothing.
sdelay imm[, dest=d] s0, s1 32 × imm + d Stall the scalar side for a given number of cycles.
sfence s0, s1 123 Wait until every bundle in the vector queue has been issued.
Counters and hidden registers
srdreg sD, lcc.{lo,hi} s0, s1 1 Low or high half of the 64-bit core cycle counter; it counts stall and wait cycles too.
srdreg sD, gtc.{lo,hi} s0, s1 1 Low or high half of the 64-bit global time counter, an 800 MHz clock.
srdreg sD, {yield,tag,tracemark} s0, s1 1 The yield, tag or tracemark register.
set_tag sY|imm s0, s1 1 Write the tag register.
Core to core
spush.sccf sY|imm s0, s1 — Push onto the scalar core-to-core FIFO; faults on a one-core v5e.
spop.sccf sD s0, s1 — Pop the scalar core-to-core FIFO.

1 An sld.smem issued in the next bundle reads the newly stored value.

2 See Branches, calls and delay slots below.

3 On top of the time until every queued vector part has issued.

Details

Predication

Every slot carries a guard, written (pN): one of the predicate registers p0 to p13, p14="always" or p15="never". The two scalar slots can also invert it, written (pN!). A slot whose guard is false has no effect. There is no separate conditional branch as they are represented using a guarded unconditional branch (in (p1) sbranch.abs loop, loop is only taken if p1 is true).

Predicates are written only by the compares, sinfnan.f32, scarry.u32 and spred.or, and are ready 2 cycles later.

Since each operand can be negated and p15 always reads false, spred.or can be used to implement other operations.

Wanted Instructions Why
p3 = p1 spred.or p3, p1, p15 p1 | false
p3 = !p1 spred.or p3, p1!, p15 !p1 | false
p3 = !(p1 & p2) spred.or p3, p1!, p2! !p1 | !p2, by De Morgan
p4 = p1 & p2 spred.or p3, p1!, p2!
spred.or p4, p3!, p15
the line above, then its negation

When a predicate is used as a guard (as in (p4) sadd.s32 s5, s5, 1) the predicate needs to be available on issue. A guard in the bundle right after the one that writes its predicate stalls for one cycle.

scmp.lt.s32 p4, s1, s2
(p4) sadd.s32 s5, s5, 1 // <- this will stall 1 cycle

This is not the case for spred.or.

scmp.lt.s32 p1, s1, s2
spred.or    p3, p1!, p2!      // no stall

Branches, calls and delay slots

A branch changes the program counter 4 bundles later. The 4 bundles right after it always run. A taken sbranch.ind costs one more cycle than the other branches. A taken branch inside the delay slots of another taken branch faults the core.

Execution trace, one row per bundle run: pc 10 sbranch.abs 20; pc 11 to 14, its 4 delay slots, which always run; then pc 20, where the jump takes effect, and 21 pc bundle executed 10 sbranch.abs 20 11 … 12 … 13 … 14 … 20 … 21 … the branch the jump takes effect here its 4 delay slots: always run

A call saves its return address in the register named by link: scall.abs f, link=s10 saves it in s10. There is no return instruction; a function returns with sbranch.ind s10.

The return comes back one bundle early: to the call's last delay slot, not to the bundle after it. That bundle has already run once, so it runs a second time. In the trace below, the call is at 48, its delay slots are 49 to 52, and it saves 52. After the return, 52 runs again, then 53.

Execution trace, one row per bundle run: pc 48 scall.abs 100, link=s10, which sets s10 = 52; pc 49 to 52, the call's 4 delay slots; pc 100 to 102, the called function; pc 103 sbranch.ind s10; pc 104 to 107, the return's 4 delay slots; then pc 52 a second time, then 53 pc bundle executed 48 scall.abs 100, link=s10 49 … 50 … 51 … 52 … 100 … 101 … 102 … 103 sbranch.ind s10 104 … 105 … 106 … 107 … 52 … 53 … the call: s10 = 48 + 4 = 52 the called function starts the return: jump to s10 = 52 pc 52 runs a second time the call's 4 delay slots the return's 4 delay slots

Pops and the result FIFOs

The scalar side can't read the other units' results directly. Three FIFOs bring values to it, each with its own pop:

A pop takes the oldest value and costs 1 cycle once the value is there. A pop whose value is still on its way (the associated push was issued before the pop but the instruction is not yet retired) waits until the value arrives. A pop of an empty FIFO with nothing on its way faults the core.

sdiv pushes its results 9 cycles after it issues, read_sync 13 cycles after, and push_v2s 61 cycles after.

Divide

Only slot 0 has a divider. The divide is unsigned. Dividing by zero gives a quotient of all ones and the dividend as remainder. sdiv.push_quotient and sdiv.push_remainder push both the quotient and remainder as sdiv does.

SMEM

SMEM is the scalar side's own memory: 256 Ki 32-bit words (1 MiB), addressed by word. Only slot 1 can issue a load or store to SMEM. There's no concept of banks or ports with SMEM. A loaded value is ready 6 cycles after the load. A load in the bundle after a store reads the updated value.

Arithmetic specific to the scalar side

Scalar register used as a destination twice in one bundle

Two scalar slots writing the same scalar register in one bundle fault the core. Two writes to the same predicate in one bundle leave the AND of the two values.

Vector ALU

Instructions

Instruction Slot Cycles What it does
Integer and bitwise
vadd.s32 vD, vS, vT|sY.bcast|imm valu0..3 2 Add vS and vT, element by element; overflow wraps.
vsub.s32 vD, vS, vT|sY.bcast|imm valu0..3 2 Subtract, element by element: computes vT − vS; overflow wraps.
vmul.u32 vD, vS, vT|sY.bcast|imm valu0..3 2 Multiply vS and vT, element by element, keeping the low 32 bits of each product.
vand.u32 vD, vS, vT|sY.bcast|imm valu0..3 2 Bitwise and of vS and vT, element by element.
vor.u32 vD, vS, vT|sY.bcast|imm valu0..3 2 Bitwise or of vS and vT, element by element.
vxor.u32 vD, vS, vT|sY.bcast|imm valu0..3 2 Bitwise xor of vS and vT, element by element.
vshll.u32 vD, vS, vT|sY.bcast|imm valu0..3 2 Shift each element of vS left by vT bits; 32 or more gives 0.
vshrl.u32 vD, vS, vT|sY.bcast|imm valu0..3 2 Shift each element of vS right by vT bits, filling with zeros; 32 or more gives 0.
vashr.s32 vD, vS, vT|sY.bcast|imm valu0..3 2 Shift each element of vS right by vT bits, filling with its sign bit; 32 or more leaves only the sign.
vpopcnt vD, vS valu0..3 2 Count the bits set in each element.
vclz vD, vS valu0..3 2 Count the leading zero bits of each element; 32 for 0.
Float
vadd.f32 vD, vS, vT|sY.bcast|imm valu0..3 2 Add vS and vT as f32, element by element.
vsub.f32 vD, vS, vT|sY.bcast|imm valu0..3 2 Subtract as f32, element by element: computes vT − vS.
vmul.f32 vD, vS, vT|sY.bcast|imm valu0..3 2 Multiply vS and vT as f32, element by element.
vmax.f32 vD, vS, vT|sY.bcast|imm valu0..3 2 The larger of vS and vT, as f32, element by element; a NaN operand gives NaN, and +0 is above −0.
vmin.f32 vD, vS, vT|sY.bcast|imm valu0..3 2 The smaller of vS and vT, as f32, element by element; a NaN operand gives NaN, and −0 is below +0.
vrelux vD, vS, vT|sY.bcast|imm valu0..3 2 Clamp to [0, vT]: max(min(vS, vT), 0); a zero result is +0.
vclamp vD, vS, vT|sY.bcast|imm valu0..3 2 Clamp to [−vT, vT]; a negative vT gives +0, and a zero the clamp produces is +0.
vceil.f32 vD, vS valu0..3 2 Round each f32 element up to a whole number.
vfloor.f32 vD, vS valu0..3 2 Round each f32 element down to a whole number.
Converts
vcvt.f32.s32 vD, vS valu0..3 2 Convert s32 to f32, rounding to nearest even.
vcvt.s32.f32 vD, vS valu0..3 2 Convert f32 to s32, rounding to nearest even; out of range saturates, NaN gives 0.
vcvt.bf16.f32 vD, vS valu0..3 2 Convert f32 to bf16, rounding to nearest even, into the low half; the high half is all ones.
vcvt.hf16.f32 vD, vS valu0..3 2 Convert f32 to f16, rounding to nearest even, into the low half; overflows to inf.
vcvt.bf8.f32 vD, vS valu0..3 2 Convert f32 to bf8 (e5m2), rounding to nearest even, into the low byte; overflows to inf from 61440.
vcvt.if8.f32 vD, vS valu0..3 2 Convert f32 to if8 (e4m3, bias 11), into the low byte; saturates to 0x7f, as if8 has no inf.
vcvt.{hf16,bf16,bf8,if8}.f32.sr vD, vS, vT|sY.bcast|imm valu0..3 2 Convert f32 to f16, bf16, bf8 or if8, rounding stochastically on vT.
Packs and unpacks
vpack.bf16 vD, vS, vT|sY.bcast|imm valu0..3 2 Convert two registers to bf16 as vcvt.bf16.f32 does, then pack them compressed.
vpacki.bf16 vD, vS, vT|sY.bcast|imm valu0..3 2 Convert two registers to bf16 as vcvt.bf16.f32 does, then pack them interleaved.
vpack.{16,8,4,2,1} vD, vS, vT|sY.bcast|imm valu0..3 2 Pack the low N bits of every 2N-bit field of two registers (N = 16, 8, 4, 2 or 1), compressed.
vpacki.{16,8,4} vD, vS, vT|sY.bcast|imm valu0..3 2 The same, interleaved. There is no vpacki.2 or vpacki.1.
vunpack.{bf16,s16}.iN vD, vS valu0..3 2 Widen the low (N = 0) or high (N = 1) half of every word in place, bf16 to f32 or s16 to s32.
vunpack.{bf16,hf16,s16}.slN vD, vS valu0..3 2 Widen 16-bit packed rows 8N to 8N+7 (N = 0, 1), bf16 or f16 to f32, s16 to s32.
vunpack.{bf8,if8,s8}.slN vD, vS valu0..3 2 Widen 8-bit packed rows 8N to 8N+7 (N = 0..3), bf8 or if8 to f32, s8 to s32.
vunpack.s4.slN.s8 vD, vS valu0..3 2 Widen 4-bit packed rows 32N to 32N+31 (N = 0, 1) to 8-bit packed rows, sign-extended.
vunpack.s2.slN vD, vS valu0..3 2 Widen 2-bit packed rows 64N to 64N+63 (N = 0, 1) to 4-bit packed rows, sign-extended.
vunpack.s1.slN vD, vS valu0..3 2 Widen 1-bit packed rows 128N to 128N+127 (N = 0, 1) to 2-bit packed rows, sign-extended.
Sublane moves
vsrot vD, vS, vT|sY.bcast|imm valu0..3 2 Rotate each lane's sublanes: element (s, l) takes vS's element ((s + vT) mod 8, l). vT is read per element, low 3 bits only.
vsperm vD, vS, vT|sY.bcast|imm valu0..3 2 Gather within each lane: element (s, l) takes vS's element (vT mod 8, l). vT is read per element, low 3 bits only.
vlaneseq vD valu0..3 2 Each element's flat index, 128 × sublane + lane.
Moves and selects
vmov vD, [vS,] vT|sY.bcast|imm valu0..3 1 Copy a register, an immediate or a broadcast scalar.
vsel.vmskK vD, vS, vT|sY.bcast|imm valu0..3 1 Per byte: vT where the mask bit is set, vS where it is clear.
vsel.nvmskK vD, vS, vT|sY.bcast|imm valu0..3 1 Per byte: vS where the mask bit is set, vT where it is clear.
Compares and mask builders
vcmp.{eq,ne,gt,ge,lt,le}.s32 vmK, vS, vT|sY.bcast|imm valu0..3 1 Compare signed vS =, ≠, >, ≥, < or ≤ vT, element by element, into a mask register.
vcmp.{eq,ne,gt,ge,lt,le}.f32 vmK, vS, vT|sY.bcast|imm valu0..3 1 The same six compares on f32, into a mask register.
vcmp.{tlt,tle}.f32 vmK, vS, vT|sY.bcast|imm valu0..3 1 Compare total-order f32 vS < vT (tlt) or ≤ vT (tle), into a mask register.
vcmp.carry.u32 vmK, vS, vT|sY.bcast|imm valu0..3 1 Unsigned carry out of vS + vT, into a mask register.
vclassify vmK, vS, vT|sY.bcast|imm valu0..3 1 Test each element's float class against the class bits in vT, into a mask register.
vinfnan.f32 vmK, vS valu0..3 1 Test each element for ±inf or NaN, into a mask register.
vbytenez vmK, vT|sY.bcast|imm valu0..3 1 Set mask bit k where byte k of vT is nonzero.

Details

Packs and unpacks

A vector register holds 8 sublanes of 128 32-bit words. A word can also hold two 16-bit values, four 8-bit values, and so on. We call packed row a row of 128 values within a sublane. With 16-bit values a sublane holds 2 packed rows, with 8-bit values 4, and so on.

A pack instruction merges two registers vS and vT into one by halving the width of every value. The first kind of pack is a compressed pack that stacks the packed rows of the two inputs. The second kind of pack is an interleaved pack that, as its name indicates, interleaves them.

vS and vT each hold 8 32-bit rows, one per sublane. vpack.16 holds S0 S1 in sublane 0 up to S6 S7 in sublane 3, then T0 T1 up to T6 T7 in sublanes 4 to 7. vpacki.16 holds Sk then Tk in sublane k s0 s1 s2 s3 s4 s5 s6 s7 vS 32-bit rows S0 S1 S2 S3 S4 S5 S6 S7 vT 32-bit rows T0 T1 T2 T3 T4 T5 T6 T7 → vpack.16 16-bit rows S0 S1 S2 S3 S4 S5 S6 S7 T0 T1 T2 T3 T4 T5 T6 T7 vpacki.16 16-bit fields S0 T0 S1 T1 S2 T2 S3 T3 S4 T4 S5 T5 S6 T6 S7 T7 Sr: row r of vS Tr: row r of vT vS and vT each hold 16 16-bit rows, two per sublane. vpack.8 holds vS rows 0 to 15 in sublanes 0 to 3, four per sublane, then vT rows 0 to 15 in sublanes 4 to 7. vpacki.8 holds, in sublane k, S2k, T2k, S2k+1, T2k+1 s0 s1 s2 s3 s4 s5 s6 s7 vS 16-bit rows S0 S1 S2 S3 S4 S5 S6 S7 S8 S9 S10 S11 S12 S13 S14 S15 vT 16-bit rows T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14 T15 → vpack.8 8-bit rows S0 S1 S2 S3 S4 S5 S6 S7 S8 S9 S10 S11 S12 S13 S14 S15 T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14 T15 vpacki.8 8-bit fields S0 T0 S1 T1 S2 T2 S3 T3 S4 T4 S5 T5 S6 T6 S7 T7 S8 T8 S9 T9 S10 T10 S11 T11 S12 T12 S13 T13 S14 T14 S15 T15 Sr: row r of vS Tr: row r of vT vS and vT each hold 32 8-bit rows, four per sublane. vpack.4 holds vS rows 0 to 31 in sublanes 0 to 3, eight per sublane, then vT rows 0 to 31 in sublanes 4 to 7. vpacki.4 holds, in sublane k, the four rows of vS and of vT in sublane k, alternating s0 s1 s2 s3 s4 s5 s6 s7 vS 8-bit rows S0 S1 S2 S3 S4 S5 S6 S7 S8 S9 S10 S11 S12 S13 S14 S15 S16 S17 S18 S19 S20 S21 S22 S23 S24 S25 S26 S27 S28 S29 S30 S31 vT 8-bit rows T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14 T15 T16 T17 T18 T19 T20 T21 T22 T23 T24 T25 T26 T27 T28 T29 T30 T31 → vpack.4 4-bit rows S0–S7 S8–S15 S16–S23 S24–S31 T0–T7 T8–T15 T16–T23 T24–T31 vpacki.4 4-bit fields S,T 0–3 S,T 4–7 S,T 8–11 S,T 12–15 S,T 16–19 S,T 20–23 S,T 24–27 S,T 28–31 Sr: row r of vS Tr: row r of vT vS and vT each hold 64 4-bit rows, eight per sublane. vpack.2 holds vS rows 0 to 63 in sublanes 0 to 3, sixteen per sublane, then vT rows 0 to 63 in sublanes 4 to 7 s0 s1 s2 s3 s4 s5 s6 s7 vS 4-bit rows S0–S7 S8–S15 S16–S23 S24–S31 S32–S39 S40–S47 S48–S55 S56–S63 vT 4-bit rows T0–T7 T8–T15 T16–T23 T24–T31 T32–T39 T40–T47 T48–T55 T56–T63 → vpack.2 2-bit rows S0–S15 S16–S31 S32–S47 S48–S63 T0–T15 T16–T31 T32–T47 T48–T63 Sr: row r of vS Tr: row r of vT vS and vT each hold 128 2-bit rows, sixteen per sublane. vpack.1 holds vS rows 0 to 127 in sublanes 0 to 3, 32 per sublane, then vT rows 0 to 127 in sublanes 4 to 7 s0 s1 s2 s3 s4 s5 s6 s7 vS 2-bit rows S0–S15 S16–S31 S32–S47 S48–S63 S64–S79 S80–S95 S96–S111 S112–S127 vT 2-bit rows T0–T15 T16–T31 T32–T47 T48–T63 T64–T79 T80–T95 T96–T111 T112–T127 → vpack.1 1-bit rows S0–S31 S32–S63 S64–S95 S96–S127 T0–T31 T32–T63 T64–T95 T96–T127 Sr: row r of vS Tr: row r of vT

An unpack goes the other way: it widens the values of one register, vS. A slN unpack takes block N of the packed rows, as many as the result holds, and widens each, in order. In the figures, vS is colored by block, and each result cell keeps the number and the color of the packed row it came from.

vS holds 16 16-bit rows, two per sublane; rows 0 to 7 (sublanes 0 to 3) are one color, rows 8 to 15 another. s16.sl0 gives rows 0 to 7, one per sublane; s16.sl1 rows 8 to 15 s0 s1 s2 s3 s4 s5 s6 s7 vS 16-bit rows 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 → s16.sl0 32-bit rows 0 1 2 3 4 5 6 7 s16.sl1 32-bit rows 8 9 10 11 12 13 14 15 vS holds 16 16-bit rows, two per sublane: row 2k in the low half and row 2k+1 in the high half of sublane k. s16.i0 gives sublane k row 2k; s16.i1 gives sublane k row 2k+1 s0 s1 s2 s3 s4 s5 s6 s7 vS 16-bit rows 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 → s16.i0 32-bit rows 0 2 4 6 8 10 12 14 s16.i1 32-bit rows 1 3 5 7 9 11 13 15 low half high half vS holds 32 8-bit rows, four per sublane, colored in four blocks of two sublanes. s8.sl0 gives rows 0 to 7, one per sublane; sl1 rows 8 to 15; sl2 rows 16 to 23; sl3 rows 24 to 31 s0 s1 s2 s3 s4 s5 s6 s7 vS 8-bit rows 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 → s8.sl0 32-bit rows 0 1 2 3 4 5 6 7 s8.sl1 32-bit rows 8 9 10 11 12 13 14 15 s8.sl2 32-bit rows 16 17 18 19 20 21 22 23 s8.sl3 32-bit rows 24 25 26 27 28 29 30 31 vS holds 64 4-bit rows, eight per sublane; sublanes 0 to 3 are one block, 4 to 7 the other. s4.sl0.s8 gives rows 0 to 31 as 8-bit rows, four per sublane; s4.sl1.s8 rows 32 to 63 s0 s1 s2 s3 s4 s5 s6 s7 vS 4-bit rows 0–7 8–15 16–23 24–31 32–39 40–47 48–55 56–63 → s4.sl0.s8 8-bit rows 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 s4.sl1.s8 8-bit rows 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 vS holds 128 2-bit rows, sixteen per sublane; sublanes 0 to 3 are one block, 4 to 7 the other. s2.sl0 gives rows 0 to 63 as 4-bit rows, eight per sublane; s2.sl1 rows 64 to 127 s0 s1 s2 s3 s4 s5 s6 s7 vS 2-bit rows 0–15 16–31 32–47 48–63 64–79 80–95 96–111 112–127 → s2.sl0 4-bit rows 0–7 8–15 16–23 24–31 32–39 40–47 48–55 56–63 s2.sl1 4-bit rows 64–71 72–79 80–87 88–95 96–103 104–111 112–119 120–127 vS holds 256 1-bit rows, 32 per sublane; sublanes 0 to 3 are one block, 4 to 7 the other. s1.sl0 gives rows 0 to 127 as 2-bit rows, sixteen per sublane; s1.sl1 rows 128 to 255 s0 s1 s2 s3 s4 s5 s6 s7 vS 1-bit rows 0–31 32–63 64–95 96–127 128–159 160–191 192–223 224–255 → s1.sl0 2-bit rows 0–15 16–31 32–47 48–63 64–79 80–95 96–111 112–127 s1.sl1 2-bit rows 128–143 144–159 160–175 176–191 192–207 208–223 224–239 240–255

The illustrations above show the behavior as a property of the bit width and not the type. In practice the type matters for field widening (unpacking):

Packing looks at the type only on vpack.bf16 and vpacki.bf16: each field is read as an f32 and the conversion to bf16 follows the same logic as vcvt.bf16.f32. Other pack instructions operate on bits.

Sublane moves

Three vsrot examples on a register drawn as 8 sublanes A to H by lanes 0 to 3 and 127. With the immediate 1, every lane moves up by one: sublane 0 of the result holds B, the last holds A. With a register vT holding 0, 1, 2, 3 in lanes 0 to 3 and 11 in lane 127, each lane rotates by its own amount and lane 127 by 3. With vT holding each element's sublane number, every lane gets A, C, E, G, A, C, E, G vsrot vD, vS, 1 every lane rotates up by 1 s0 s1 s2 s3 s4 s5 s6 s7 vS A0 A1 A2 A3 … A127 B0 B1 B2 B3 … B127 C0 C1 C2 C3 … C127 D0 D1 D2 D3 … D127 E0 E1 E2 E3 … E127 F0 F1 F2 F3 … F127 G0 G1 G2 G3 … G127 H0 H1 H2 H3 … H127 0 1 2 3 127 lane amount: imm 1 (or sY.bcast), in every element 1 1 1 1 … 1 1 1 1 1 … 1 1 1 1 1 … 1 1 1 1 1 … 1 1 1 1 1 … 1 1 1 1 1 … 1 1 1 1 1 … 1 1 1 1 1 … 1 0 1 2 3 127 lane vD B0 B1 B2 B3 … B127 C0 C1 C2 C3 … C127 D0 D1 D2 D3 … D127 E0 E1 E2 E3 … E127 F0 F1 F2 F3 … F127 G0 G1 G2 G3 … G127 H0 H1 H2 H3 … H127 A0 A1 A2 A3 … A127 0 1 2 3 127 lane vsrot vD, vS, vT each lane rotates by its own amount; 11 counts as 3 s0 s1 s2 s3 s4 s5 s6 s7 vS A0 A1 A2 A3 … A127 B0 B1 B2 B3 … B127 C0 C1 C2 C3 … C127 D0 D1 D2 D3 … D127 E0 E1 E2 E3 … E127 F0 F1 F2 F3 … F127 G0 G1 G2 G3 … G127 H0 H1 H2 H3 … H127 0 1 2 3 127 lane amount: vT, read per element 0 1 2 3 … 11 0 1 2 3 … 11 0 1 2 3 … 11 0 1 2 3 … 11 0 1 2 3 … 11 0 1 2 3 … 11 0 1 2 3 … 11 0 1 2 3 … 11 0 1 2 3 127 lane vD A0 B1 C2 D3 … D127 B0 C1 D2 E3 … E127 C0 D1 E2 F3 … F127 D0 E1 F2 G3 … G127 E0 F1 G2 H3 … H127 F0 G1 H2 A3 … A127 G0 H1 A2 B3 … B127 H0 A1 B2 C3 … C127 0 1 2 3 127 lane vsrot vD, vS, vT amounts that change down a lane: a gather, not a rotation s0 s1 s2 s3 s4 s5 s6 s7 vS A0 A1 A2 A3 … A127 B0 B1 B2 B3 … B127 C0 C1 C2 C3 … C127 D0 D1 D2 D3 … D127 E0 E1 E2 E3 … E127 F0 F1 F2 F3 … F127 G0 G1 G2 G3 … G127 H0 H1 H2 H3 … H127 0 1 2 3 127 lane amount: vT = the element's sublane 0 0 0 0 … 0 1 1 1 1 … 1 2 2 2 2 … 2 3 3 3 3 … 3 4 4 4 4 … 4 5 5 5 5 … 5 6 6 6 6 … 6 7 7 7 7 … 7 0 1 2 3 127 lane vD A0 A1 A2 A3 … A127 C0 C1 C2 C3 … C127 E0 E1 E2 E3 … E127 G0 G1 G2 G3 … G127 A0 A1 A2 A3 … A127 C0 C1 C2 C3 … C127 E0 E1 E2 E3 … E127 G0 G1 G2 G3 … G127 0 1 2 3 127 lane
Two vsperm examples on a register drawn as 8 sublanes A to H by lanes 0 to 3 and 127. With the immediate 5, every sublane of the result holds F, sublane 5. With a register vT, each element names its own source sublane in its own lane: lane 0 reorders and repeats, lane 1 reverses, lane 2 keeps the order, lane 3 names 13 and 8, which act as 5 and 0, lane 127 names 0 everywhere vsperm vD, vS, 5 sublane 5 copied into all eight s0 s1 s2 s3 s4 s5 s6 s7 vS A0 A1 A2 A3 … A127 B0 B1 B2 B3 … B127 C0 C1 C2 C3 … C127 D0 D1 D2 D3 … D127 E0 E1 E2 E3 … E127 F0 F1 F2 F3 … F127 G0 G1 G2 G3 … G127 H0 H1 H2 H3 … H127 0 1 2 3 127 lane index: imm 5 (or sY.bcast), in every element 5 5 5 5 … 5 5 5 5 5 … 5 5 5 5 5 … 5 5 5 5 5 … 5 5 5 5 5 … 5 5 5 5 5 … 5 5 5 5 5 … 5 5 5 5 5 … 5 0 1 2 3 127 lane vD F0 F1 F2 F3 … F127 F0 F1 F2 F3 … F127 F0 F1 F2 F3 … F127 F0 F1 F2 F3 … F127 F0 F1 F2 F3 … F127 F0 F1 F2 F3 … F127 F0 F1 F2 F3 … F127 F0 F1 F2 F3 … F127 0 1 2 3 127 lane vsperm vD, vS, vT each element names its own source sublane; 13 counts as 5, 8 as 0 s0 s1 s2 s3 s4 s5 s6 s7 vS A0 A1 A2 A3 … A127 B0 B1 B2 B3 … B127 C0 C1 C2 C3 … C127 D0 D1 D2 D3 … D127 E0 E1 E2 E3 … E127 F0 F1 F2 F3 … F127 G0 G1 G2 G3 … G127 H0 H1 H2 H3 … H127 0 1 2 3 127 lane index: vT, read per element 3 7 0 13 … 0 3 6 1 8 … 0 0 5 2 13 … 0 7 4 3 8 … 0 1 3 4 13 … 0 1 2 5 8 … 0 6 1 6 13 … 0 2 0 7 8 … 0 0 1 2 3 127 lane vD D0 H1 A2 F3 … A127 D0 G1 B2 A3 … A127 A0 F1 C2 F3 … A127 H0 E1 D2 A3 … A127 B0 D1 E2 F3 … A127 B0 C1 F2 A3 … A127 G0 B1 G2 F3 … A127 C0 A1 H2 A3 … A127 0 1 2 3 127 lane

Converts

Each convert keeps what fits the target format and drops the rest. For the float formats, the sign and exponent carry over (re-biased) and the dropped part is the low end of the f32's 23-bit fraction:

The dropped bits are used to round the number. Line the dropped bits up at the top of a 23-bit field, padding with zeros on the right; call that frac. For bf16, frac is the low 16 bits of vS shifted left by 7.

vclassify

vclassify is true where vS's class is one of the bits set in vT. Quiet and signaling NaNs are alike, but the sign of a NaN counts:

Bit Class
0x001 negative NaN
0x002 −inf
0x004 −normal
0x008 −denormal
0x010 −0
0x020 +0
0x040 +denormal
0x080 +normal
0x100 +inf
0x200 positive NaN

So 0x102 means "infinite, either sign".

Masks

A vector register has 8 sublanes, each of 128 32-bit elements. Instructions often need to work on a subset of a vector register. A mask says which part. A mask register has the same shape as a vector register: 8 sublanes of 128 4-bit entries. Each bit stands for one byte of the associated 32-bit element. This allows masks to work with packed vectors.

The two mask builders below, vmkmask and vmksublane, run in the vector ALU slots. The mask ALU, which combines masks, runs in the misc slot.

Instructions

Instruction Slot Cycles What it does
vmkmask vmK, imm valu0..3 1 Build a sublane × lane rectangle from an immediate.
vmksublane vmK, imm valu0..3 1 Build a band of sublanes, starting and ending at a byte, from an immediate.
vmsk.mov vmD, vmK misc 1 Copy a mask register.
vmsk.not vmD, vmK misc 1 Invert every bit of a mask register.
vmsk.and vmD, vmK, vmL misc 1 Bitwise and of two mask registers.
vmsk.or vmD, vmK, vmL misc 1 Bitwise or of two mask registers.
vmsk.xor vmD, vmK, vmL misc 1 Bitwise xor of two mask registers.
vmsk.pack vmD, vmK, vmL misc 1 Merge two masks into one mask for 16-bit data packed by vpack.

Details

vmkmask

vmkmask builds a rectangular mask. Its immediate gives the rectangle's first and last sublane and its first and last lane:

vmkmask vm1, imm. The immediate gives four values: last lane 9, last sublane 5, first lane 3, first sublane 2. Below, a mask of 8 sublanes by lanes 0 to 11 with the rectangle sublanes 2 to 5 by lanes 3 to 9 set vmkmask vm1, imm last lane 9 last sublane 5 first lane 3 first sublane 2 0 3 6 9 … 127 s0 s1 s2 s3 s4 s5 s6 s7 sublanes 2 to 5 × lanes 3 to 9

vmksublane

vmksublane selects a band of sublanes across all 128 lanes. Unlike vmkmask, the band can start and end in the middle of a sublane, at a byte.

To say where, number the bytes of one lane from top to bottom: sublane 0 holds bytes 0 to 3, sublane 1 bytes 4 to 7, and so on down to byte 31. The immediate gives the first and the last byte of the band, both included. Every lane gets the same band.

vmksublane vm1, imm. The immediate gives two values: last byte 12, first byte 5. Below, lanes 0, 1 and 127 of the mask, each element drawn as its bytes b0 to b3, with lane 0's bytes numbered 0 to 31 down the sublanes. Bytes 5 to 12 are set in every lane: b1 to b3 of sublane 1, all of sublane 2, b0 of sublane 3 vmksublane vm1, imm last byte 12 first byte 5 … lane 0 b0 b1 b2 b3 lane 1 b0 b1 b2 b3 lane 127 b0 b1 b2 b3 s0 0 1 2 3 s1 4 5 6 7 s2 8 9 10 11 s3 12 13 14 15 s4 16 17 18 19 s5 20 21 22 23 s6 24 25 26 27 s7 28 29 30 31 ← starts at byte 5 ← ends at byte 12

Vector loads and stores (VMEM)

Each vector register is made of 8 sublanes of 128 words. VMEM is made of rows of 128 words, so one vector register fills 8 rows of VMEM, one row per sublane.

Every load and store takes an optional sublane mask sm=imm|sM. The plain and .base forms also take an optional stride=imm|sS, and every store an optional element mask vmsk=vmK. The table leaves these out.

Instructions

Instruction Slot Cycles What it does
Loads
vld vD, vmem[imm] vld0..2 1 Load 8 rows into vD, sublane k from row imm + stride × k. Masked-off sublanes read 0.
vld.base vD, vmem[sX + imm] vld0..2 1 The same, with a scalar register added to the address.
vld.shuf vD, vmem[imm], shuffle=imm|sX vld0..2 1 Load each sublane from its own row, one of the 16 rows starting at imm.
vld.shuf.base vD, vmem[sX + imm], shuffle=imm|sY vld0..2 1 The same, with a scalar register added to the address.
vld.indexed.N vD vld0..2 1 Gather through IARN (N = 0 or 1): each element from row IARN + sublane, same lane.
Stores
vst vS, vmem[imm] vst 14 Store vS, sublane k to row imm + stride × k.
vst.base vS, vmem[sX + imm] vst 14 The same, with a scalar register added to the address.
vst.shuf vS, vmem[imm], shuffle=imm|sX vst 14 Store each sublane to its own row. Two selected sublanes naming the same row hang the core.
vst.shuf.base vS, vmem[sX + imm], shuffle=imm|sY vst 14 The same, with a scalar register added to the address.
vst.indexed.N vS, vmem[imm] vst 15 Scatter through IARN (N = 0 or 1): each element to row imm + IARN + sublane, same lane.
Index registers (N = 0 or 1)
set_raw_iar_N vS vst 16 Copy vS into IARN as it is.
set_lane_iar_N vS vst 16 Copy sublane 0 of vS into all 8 sublanes of IARN.
set_sublane_iar_N vS vst 16 Copy vS into IARN, each word multiplied by 32.
Through the store slot
push_v2s vS, vmem[imm] vst 617 Send element 0 of vS (sublane 0, lane 0) to the scalar side, through the vector-to-scalar FIFO. The vmem operand is ignored.
set_prng vS vst 18 Seed the random-number generator from vS.

4 A vld of a row it wrote waits until 10 cycles after it.

5 Every later vld, whatever rows it reads, waits until 8 cycles after it.

6 A vld.indexed behind it waits until 8 cycles after it; a plain vld doesn't wait.

7 Until the value can be popped by vpop.v2s.

8 The PRNG stays busy for 3 cycles.

Details

Addressing modes

Contiguous. By default, rows are contiguous: sublane i = VMEM[base + i].

A contiguous load: sublane k of vD comes from VMEM row b plus k, for rows b to b plus 7 vld vD, vmem[b] VMEM vD b+0 b+1 b+2 b+3 b+4 b+5 b+6 b+7 b+8 b+9 b+10 b+11 b+12 b+13 b+14 b+15 sublane 0 sublane 1 sublane 2 sublane 3 sublane 4 sublane 5 sublane 6 sublane 7

Strided. Given a stride, sublane i = VMEM[base + i * stride].

A load with stride 2: sublane k of vD comes from VMEM row b plus 2k, every other row from b to b plus 14 vld vD, vmem[b], stride=2 VMEM vD b+0 b+1 b+2 b+3 b+4 b+5 b+6 b+7 b+8 b+9 b+10 b+11 b+12 b+13 b+14 b+15 sublane 0 sublane 1 sublane 2 sublane 3 sublane 4 sublane 5 sublane 6 sublane 7

Shuffled. The shuffle operand packs eight 4-bit offsets, one per sublane. sublane i = VMEM[base + shuffle[i]]. Offsets go from 0 to 15, so a shuffled access reaches the 16 rows from base.

A shuffled load with shuffle 5 0 12 3 3 9 14 7: each sublane comes from its own row among b to b plus 15, and row b plus 3 feeds sublanes 3 and 4 vld.shuf vD, vmem[b], shuffle=5 0 12 3 3 9 14 7 VMEM vD b+0 b+1 b+2 b+3 b+4 b+5 b+6 b+7 b+8 b+9 b+10 b+11 b+12 b+13 b+14 b+15 sublane 0 sublane 1 sublane 2 sublane 3 sublane 4 sublane 5 sublane 6 sublane 7 Row b+3 feeds sublanes 3 and 4: a load may name a row twice, a store may not.

Indexed. This does an indirect load or store according to an IAR. In pseudo notation this maps to (sublane i)[e] = VMEM[IAR[i][e] + i][e]. An IAR is similar to a vector register but is only used for indexed loads and stores. There are only two IARs. These instructions implement gathers and scatters. Only the low 18 bits of IAR[i][e] + i count: VMEM has 2^18 rows, and a larger row number wraps around.

An indexed load on 2 sublanes and 4 lanes: IAR plus the sublane number gives each element its row, and the element takes the word in that row and its own lane, so sublane 0 gets a3 b0 c4 d1 and sublane 1 gets a2 b5 c1 d3 vld.indexed.N vD, on 2 of the 8 sublanes and 4 of the 128 lanes s0 s1 IARN 3 0 4 1 1 4 0 2 l0 l1 l2 l3 + 0 = + 1 = row 3 0 4 1 2 5 1 3 l0 l1 l2 l3 → → vD a3 b0 c4 d1 a2 b5 c1 d3 l0 l1 l2 l3 VMEM row 0 a0 b0 c0 d0 row 1 a1 b1 c1 d1 row 2 a2 b2 c2 d2 row 3 a3 b3 c3 d3 row 4 a4 b4 c4 d4 row 5 a5 b5 c5 d5 l0 l1 l2 l3

Masks

Load and store instructions take a sublane mask sm. This 8-bit mask switches off some sublanes: a load returns 0 in a masked-off sublane, and a masked-off sublane takes no port. Stores can also take an element mask vmsk that writes only a subset of the vector register to VMEM. A 32-bit store looks at one mask bit per word, the bit of the word's lowest byte, and writes the whole word or none of it.

Ports: how many loads and stores fit in a cycle

VMEM has 32 ports. Row r maps to port r mod 32. Each port can read or write one row per cycle. A load or store touches up to 8 rows at a time. Reading or writing two different rows on the same port in one instruction makes the core hang, so a program must avoid it. A load may name the same row in several sublanes (load coalescing); a store may not.

A bundle might carry up to three loads. A port with a pending load makes later loads on it take longer. A bundle of loads costs as many cycles as the busiest port has loads (1, 2 or 3), whatever kind of load they are: plain, strided or shuffled.

VMEM rows on the left, ports on the right, an arrow from each row to port r mod 32. Two loads of rows 0 to 7 and 32 to 39 share ports 0 to 7, so the bundle takes 2 cycles. A load with stride 32 sends rows 0, 32, up to 224 all to port 0, which hangs the core vld v1, vmem[0] | vld v2, vmem[32] VMEM rows ports 0 to 7 of 32 row 0 row 1 row 2 row 3 row 4 row 5 row 6 row 7 ⋮ row 32 row 33 row 34 row 35 row 36 row 37 row 38 row 39 port 0 port 1 port 2 port 3 port 4 port 5 port 6 port 7 Row r is on port r mod 32: rows 0 and 32 share port 0, and so on. Each port reads one row a cycle, so this bundle takes 2 cycles. vld v1, vmem[0], stride=32 VMEM rows ports 0 to 7 of 32 row 0 row 32 row 64 row 96 row 128 row 160 row 192 row 224 port 0 port 1 port 2 port 3 port 4 port 5 port 6 port 7 All 8 sublanes land on port 0. One instruction naming two rows on one port hangs the core.

Write queues

Each port has a write queue. A store's rows reach their ports' write queues 6 cycles after the store issues. A queue holds 7 rows. A port writes its oldest queued row in every cycle no load reads that port: reads take priority over writes. When a store's row arrives at a full queue, it takes the cycle from the loads instead: the head is written and the new row joins. A store is also held at issue while any of its ports has 14 rows not yet written, counting the queue, the 6-cycle pipe, and the row written the cycle before.

One port: a store row goes through a 6-cycle pipe into a 7-row write queue, then to the port; loads reach the port from the other side and come first. The port writes the oldest queued row only in a cycle no load reads it; a row arriving at a full queue takes the port for that cycle vst 6-cycle pipe on the way write queue: 7 rows oldest port one row a cycle vld 1. A store's row reaches its port's queue 6 cycles after the store issues. 2. Loads come first: the port writes its oldest queued row only in a cycle no load reads it. 3. If a row arrives at a full queue, it takes the port for that cycle: the oldest row is written, and that cycle's load waits.

Cross-lane units (XLU)

The XLU's job is to move data across lanes: it transposes matrices held in one or more vectors, rotates, broadcasts and permutes lanes, and reduces each sublane to one value. There are three XLUs. Each operation can pick one through uU.

Instructions

The cycle counts below are for unit 0.9

Instruction Family Slot Cycles What it does
xlane.add uU, vS reduction vex0, vex1 115 Compute the sum of the 128 lanes of each sublane, then broadcast it across that sublane.
xlane.max uU, vS reduction vex0, vex1 115 Compute the maximum of the 128 lanes of each sublane, then broadcast it across that sublane.
xlane.min uU, vS reduction vex0, vex1 115 Compute the minimum of the 128 lanes of each sublane, then broadcast it across that sublane.
xlane.maxidx uU, vS reduction vex0, vex1 115 Index of the max lane of each sublane; a tie goes to the highest lane.
xlane.minidx uU, vS reduction vex0, vex1 115 Index of the min lane of each sublane; a tie goes to the highest lane.
lanerot imm|sX, uU, vS mover vex0, vex1 11410 Rotate the 128 lanes.
lanebcast imm|sX, uU, vS mover vex0, vex1 11410 Broadcast one lane to all lanes.
permute uU, vS mover vex0, vex1 11410 Gather lanes within each sublane, using the pattern latched by setpcr.
setpcr.{lanes,sublanes,bytes} uU, vS pattern latch vex0, vex1 —11 Latch one lane pattern used by every sublane (.lanes), a lane pattern per sublane (.sublanes), or a source lane per byte (.bytes).
xpose.start.W uU, vS transpose vex0, vex1 16310,12 Start a new transpose operation.
xpose.cont uU, vS transpose vex0, vex1 16310,12 Push a new vector into the current matrix being transposed.
xpose.end uU, vS transpose vex0, vex1 16310,12 End the current transpose operation.
xpose.W uU, vS transpose vex0, vex1 16310,12 Start and end a transpose in one push.
xpose.seg.W uU, vS transpose vex0, vex1 16310,12 Segmented transpose in one push: every W lanes of the vector are their own matrix.
xpose.seg.start.W uU, vS transpose vex0, vex1 16310,12 Start a new segmented transpose.
xpose.binary.vy uU, vT transpose vex0 171 Transpose a second 16-bit matrix alongside the 16-bit push in vex1 of the same bundle, interleaving the results.
vpop.xpose vD, uU pop vres0, vres1 113 Pop the oldest result of a cross-lane unit.

9 Unit 1 is the same. On unit 2, reductions and movers are 24 cycles sooner (91, 90, 98 packed) and transposes 11 sooner (152; 160 for 16-bit).

10 Number for the 32-bit variant. The 16-bit variants (lanerot.packed, lanebcast.packed, permute.packed; xpose.start.p16.W, xpose.p16.W, xpose.seg.p16.W, xpose.seg.start.p16.W) are 8 cycles slower.

11 No result. The next mover on the same unit (permute, lanerot, lanebcast) can start the cycle after a setpcr.lanes, and 8 cycles after a .sublanes or .bytes one.

12 For a stream: the first result, counted from the first push, with the pushes back to back. Each later result is ready 8 cycles after the one before (16 for 16-bit).

13 A pop holds the unit's output port for 8 cycles (one sublane per cycle), so one unit gives at most one pop every 8 cycles.

Details

Matrix and "stream"

The unit of computation of the XLU is a vector: 8 matrix rows of 128 32-bit values, one per sublane, or 16 matrix rows of 128 16-bit values, two packed rows per sublane. A matrix is represented by a stream of vectors. To transpose a 128 × 128 matrix of 32-bit entries, we start a transpose with xpose.start.128, push 14 xpose.cont and finish with an xpose.end: 16 pushes of 8 rows. The transposed matrix gets written vector by vector to the xpose queue, one vector every 8 cycles: 16 pops of 8 rows. The 16-bit variant is similar but output vectors come once every 16 cycles: a 128 × 128 matrix of 16-bit entries is 8 pushes and 8 pops.

Depth of the xpose queue

The queue backing vpop.xpose holds 49 results, on each of the three units. A transpose stream reserves room for all its result rows when it starts. The width of the transposed matrices is the instruction's W. A stream waits until there's enough room in the queue for its results, which in practical terms makes the core hang.

xpose.binary.vy

xpose.binary.vy goes in vex0 of the same bundle as a 16-bit xpose instruction in vex1, on the same unit: an xpose.start.p16.W, xpose.p16.W, xpose.cont or xpose.end of a 16-bit stream. If the stream's first push carries one, every later push must too; if it doesn't, none may.

By default a 16-bit transpose can only emit an output vector register every 16 cycles. If xpose.binary.vy is used then two 16-bit matrices are interleaved and we get one output vector register every 8 cycles.

One row per instruction against time, for a 32 by 32 transpose: 32-bit, four pushes of 8 rows 8 cycles apart and four pops from cycle 163, 8 apart; 16-bit, two pushes of 16 rows and two pops at 171 and 187; 16-bit with xpose.binary.vy, each push bundle also carries a register of a second matrix T, and four pops at 171, 179, 187, 195 return T and S rows alternately 32-bit, W = 32 a 32×32 matrix S goes in 8 rows a push; Sᵀ comes out 8 rows a pop, 8 cycles apart xpose.start.32 S 0–7 xpose.cont S 8–15 xpose.cont S 16–23 xpose.end S 24–31 vpop.xpose Sᵀ0–7 vpop.xpose Sᵀ8–15 vpop.xpose Sᵀ16–23 vpop.xpose Sᵀ24–31 … 0 8 16 24 163 171 179 187 16-bit, W = 32 S goes in 16 rows a push; Sᵀ comes out 16 rows a pop, 16 cycles apart xpose.start.p16.32 S 0–15 xpose.end S 16–31 vpop.xpose Sᵀ0–15 vpop.xpose Sᵀ16–31 … 0 16 171 187 16-bit, W = 32, with xpose.binary.vy a second matrix T rides in the same two bundles; Tᵀ and Sᵀ come out interleaved, 8 cycles apart xpose.start.p16.32 S 0–15 xpose.binary.vy T 0–15 xpose.end S 16–31 xpose.binary.vy T 16–31 vpop.xpose Tᵀ0–15 vpop.xpose Sᵀ0–15 vpop.xpose Tᵀ16–31 vpop.xpose Sᵀ16–31 … 0 16 171 179 187 195 Bars: the rows of the matrix a push brings in or a pop takes out (16-bit rows go two to a 32-bit sublane). Cycles from the stream's first push (unit 0; unit 2's results are 11 sooner). A push bar is how long it holds the unit, a pop bar the 8 cycles it holds the output port; each pop is drawn as soon as its result is ready. A shaded band: the two instructions are in one bundle (vex1 the push, vex0 the vy), on the same unit.

Segmented transpose

In the table above W must be a multiple of 8 (16 for a 16-bit transpose). xpose.start.W treats the first W lanes of each vector as one matrix of width W, and ignores the other lanes. With xpose.seg.start.W, each vector holds 128 / W independent matrices, all transposed at once.

Four registers pushed into a 32-wide transpose, as 8 by 8 tiles: xpose.start.32, two xpose.cont and xpose.end transpose only the first 32 lanes as one matrix; with xpose.seg.start.32 instead, each group of 32 lanes is transposed as its own matrix four pushed registers, A to D, as sixteen 8×8 tiles each (one tile = 8 sublanes × 8 lanes) vS A A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12 A13 A14 A15 vS B B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 B10 B11 B12 B13 B14 B15 vS C C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 C13 C14 C15 vS D D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 D11 D12 D13 D14 D15 lanes 0–31 lanes 32–63 lanes 64–95 lanes 96–127 xpose.start.32, cont, cont, end the first 32 lanes are one 32×32 matrix; the other 96 are not used four pops pop 0 A0ᵀ B0ᵀ C0ᵀ D0ᵀ pop 1 A1ᵀ B1ᵀ C1ᵀ D1ᵀ pop 2 A2ᵀ B2ᵀ C2ᵀ D2ᵀ pop 3 A3ᵀ B3ᵀ C3ᵀ D3ᵀ xpose.seg.start.32, cont, cont, end every 32 lanes are their own 32×32 matrix, all four transposed four pops pop 0 A0ᵀ B0ᵀ C0ᵀ D0ᵀ A4ᵀ B4ᵀ C4ᵀ D4ᵀ A8ᵀ B8ᵀ C8ᵀ D8ᵀ A12ᵀ B12ᵀ C12ᵀ D12ᵀ pop 1 A1ᵀ B1ᵀ C1ᵀ D1ᵀ A5ᵀ B5ᵀ C5ᵀ D5ᵀ A9ᵀ B9ᵀ C9ᵀ D9ᵀ A13ᵀ B13ᵀ C13ᵀ D13ᵀ pop 2 A2ᵀ B2ᵀ C2ᵀ D2ᵀ A6ᵀ B6ᵀ C6ᵀ D6ᵀ A10ᵀ B10ᵀ C10ᵀ D10ᵀ A14ᵀ B14ᵀ C14ᵀ D14ᵀ pop 3 A3ᵀ B3ᵀ C3ᵀ D3ᵀ A7ᵀ B7ᵀ C7ᵀ D7ᵀ A11ᵀ B11ᵀ C11ᵀ D11ᵀ A15ᵀ B15ᵀ C15ᵀ D15ᵀ Xᵀ: tile X transposed within itself; grey: zero

Permutation patterns and setpcr

Each unit has its own setpcr latch. A second setpcr replaces the first entirely, pattern and form, and a permute doesn't use it up. A unit that was never latched gathers with an all-zero pattern: lane 0 of each sublane, broadcast. setpcr and permute on the same unit in one bundle hang the core: the vpop.xpose behind them never gets a result.

What a permute does after each setpcr form: .lanes, one lane shuffle for every sublane; .sublanes, a shuffle per sublane; .bytes, a source lane per byte setpcr.lanes one shuffle for every sublane permute's vS a0 a1 a2 a3 b0 b1 b2 b3 pattern, from setpcr's vS 2 0 3 1 · · · · only sublane 0's row is used, for every row permute result a2 a0 a3 a1 b2 b0 b3 b1 setpcr.sublanes a shuffle per sublane permute's vS a0 a1 a2 a3 b0 b1 b2 b3 pattern, from setpcr's vS 2 0 3 1 1 1 0 3 permute result a2 a0 a3 a1 b1 b1 b0 b3 setpcr.bytes a source lane for each byte row a of vS, byte by byte 0 1 2 3 lane 0 0 1 2 3 lane 1 0 1 2 3 lane 2 0 1 2 3 lane 3 pattern for lane 0 3 b0 0 b1 2 b2 1 b3 result, lane 0 0 1 2 3 byte i from lane pattern[i], same byte position

Instruction family and performance

The family matters for timing. On a given unit, two ops of the same family can follow each other every 8 cycles (16 for packed movers). Two ops of different families cost more, depending on the unit and the families. Transposes are the slowest to follow, as subsequent instructions have to wait for the transposer to walk the stream: a reduction or a mover waits 82 to 105 cycles more past the walk, depending on the unit.

The table below gives, for each unit, how many cycles after the first op the next op on the same unit can start, when the two are back to back. Each table row is the first op, each column the next one. walk is max(W, 8 × pushes) for the transpose ahead (16 × pushes for a packed one). Where the units differ, a cell reads unit 0 / unit 1 / unit 2.

first ↓ / next → reduction mover packed mover pattern latch transpose
reduction 8 20 / 25 / 24 20 / 25 / 24 20 / 25 / 24 41 / 58 / 41
mover 42 / 42 / 41 8 8 8 34 / 53 / 38
packed mover 50 / 50 / 49 16 16 16 42 / 61 / 46
pattern latch 42 / 42 / 41 1 (.lanes), 8 1 (.lanes), 8 — 34 / 53 / 38
transpose walk + 105 / 88 / 105 walk + 102 / 82 / 97 walk + 102 / 82 / 97 walk + 102 / 82 / 97 walk

Summation order of xlane.add

Each group of 8 adjacent lanes gets added pairwise in a reduction tree. The resulting 16 partial sums then get added one after the other.

The order xlane.add sums a sublane's 128 lanes: inside each group of 8 adjacent lanes a pairwise tree, l0 plus l1, l2 plus l3 and so on; then the 16 group sums g0 to g15 added one after the other, starting from zero 1. Inside each group of 8 adjacent lanes: a pairwise tree l0 l1 l2 l3 l4 l5 l6 l7 + + + + + + + g0 lanes 0–7; g1 is lanes 8–15, … g15 is lanes 120–127 2. Then the 16 group sums, one after the other, from zero g0 g1 g2 g3 g4 g5 g6 g7 g8 g9 g10 g11 g12 g13 g14 g15 0 + + + + + + + + + + + + + + + + sum Every + rounds to 32 bits. The sum is then broadcast to the 128 lanes of its sublane; the 8 sublanes are summed independently.

Matrix units (MXU)

Instructions

Instruction Slot Cycles What it does
matpush.{f32r,bf16,bf8,if8bf16,u8,s8,u4,s4}.msr{a,b}[.t] uU, vS [, vmsk=vmK] vex0, vex1 2 for f32r, else 414 Push one register of that type into staging register msra or msrb. With .t, the matrix rows go in as columns. f32r is f32 data, which the unit rounds to bf16 on the way in.
lmatreg.gmr.msr{a,b} uU vex0, vex1 1 Copy msra or msrb into the GMR. Empty msra or msrb.
lmatreg.lmr.msr{a,b} uU vex0, vex1 3315 Copy msra or msrb into the LMR. Empty msra or msrb.
matmul.{f32r,bf16,bf8,if8bf16}[.lgmr.msr{a,b}] uU, vS [, vmsk=vmK] vex0, vex1 12516 / 13117 Multiply each matrix row the register holds by the GMR. .lgmr.msrX first does lmatreg.gmr.msrX.
matmul.{u8,s8,u4,s4}[.lgmr.msr{a,b}] uU, vS [, vmsk=vmK] vex0, vex1 11516 / 12117 The same for the integer types.
matmul.lmr[.lgmr.msr{a,b}] uU, width=N vex0, vex1 12516 / 13117,18 Multiply the next 8 rows of the LMR by the GMR. After N rows (8, 16, 32, 64 or 128) it starts again from the first.
vpop.mxu vD[, uU] vres0, vres1 1 Pop the oldest result register of a matrix unit.
c2c.move uU, vS vex0, vex1 — Core-to-core move of a register; unreachable on a one-core v5e.

14 Until the rows are in the staging register. A .t push takes twice as long.

15 Until the lmr register is fully filled. lmatreg.lmr copies the 128 rows of the staging register into the LMR, 4 rows a cycle.

16 Until the first result is pushed to the mxu queue. Latencies for unit 0 or 1.

17 Until the first result is pushed to the mxu queue. Latencies for unit 2 or 3.

18 With a float LMR. With an integer one, 115 / 121, like the integer multiplies.

Details

How the matrix units work

An MXU has one job: compute A · W, where A and W are two matrices of size up to 128 × 128 (256 deep for the 4-bit types). Our reading is that it has a 128 × 128 grid of multipliers that holds the values of W. A is streamed through the grid row by row, and the rows of the result are pushed onto the unit's mxu result queue.

One matrix unit. A vector register of rows of W goes by matpush into msra or msrb. lmatreg.gmr copies either into the GMR, which, in our reading, loads a systolic array: 128 by 128 multipliers, each holding one number of W. lmatreg.lmr copies either into the LMR (dashed lines). Rows of A enter the array from the left, streamed from a vector register by matmul, or taken from the LMR's columns by matmul.lmr; each row gives a row of the product. The rows go into the result FIFO, vpop.mxu moves them into vD, and vadd adds them into C. vS rows of W matpush one register at a time msra msrb GMR lmatreg.gmr lmatreg.lmr LMR A, stored … … … … ⋮ ⋮ ⋮ ⋮ ⋱ ⋮ … systolic array 128 × 128 multipliers, each holds one number of W a row of A goes in, a row of A · W comes out matmul.lmr vS rows of A, streamed matmul result FIFO rows of A · W, in order vpop.mxu vD vadd C

MXUs

A TensorCore has 4 independent MXUs. They are identical, except that units 2 and 3 take 6 cycles longer to give their first result.

Internal registers

Each MXU has 4 internal registers:

Types

A vector register is 8 sublanes of 128 lanes, one 32-bit word per lane. Lane l always holds column l. The type sets how many numbers share a word, and so how many matrix rows one register holds:

Type Numbers per word Rows per register Where row r is
f32r 1 8 sublane r
bf16 2 16 sublane r / 2; the low half if r is even, the high half if odd
bf8, if8bf16, u8, s8 4 32 sublane r / 4, byte r mod 4
u4, s4 4 bytes, each holding two numbers 32 rows of bytes sublane r / 4, byte r mod 4

A register given to matpush is that many rows of W (with .t, that many columns), and a register given to matmul that many rows of A.

A GMR loaded by lmatreg.gmr puts the unit in one of three modes: float (every float type), 8-bit integer (u8, s8) or 4-bit integer (u4, s4). A multiply runs only in its GMR's mode, and matmul.lmr in its LMR's. Signedness can differ between the two sides. Any other multiply computes nothing, so no rows get pushed to the mxu queue, and a vpop.mxu waiting for them hangs the core.

The staging registers are stricter: all the pushes into one must have the same type, except that f32r and bf16 pushes can be mixed.

Floats are rounded to bf16 (to nearest even) on the way in. Accumulation is done in f32.

Integers accumulate exactly in s32. In u4 and s4, each byte holds two numbers, one in its low 4 bits and one in its high 4 bits. The multiply pairs low with low and high with high: a W byte (w0, w1) meeting an A byte (x0, x1) adds w0·x0 + w1·x1 to the sum. So each result adds up 256 products instead of 128.

Push order and transposes

The staging register is a shift register of 128 rows. matpush.<type>.msr{a,b}[.t] pushes a vector register to it. The first push into an empty staging register decides how the pushes are done. With .t the push is done transposed and the vectors are pushed as columns. Without .t they are pushed as rows. Each push goes in at the top and moves everything already there. Past 128 rows the oldest rows drop out. If a push is done in the other orientation (plain or .t), or with a type that can't be mixed with the first, the push never completes and the core hangs.

An lmatreg resets the orientation and type.

Two rows of three 128 by 128 squares, after push 1, 2 and 16. Plain pushes: each push is a band of 8 rows that enters at the top and moves the older bands down; after push 16 the bands read 16 at the top to 1 at the bottom. .t pushes: each push is a band of 8 columns that enters at the left and moves the older bands right; after push 16 they read 16 at the left to 1 at the right. plain pushes after push 1 1 after push 2 2 1 after push 16 16 15 ⋮ 1 a push enters as rows 0 to 7 (top); the older rows move down .t pushes after push 1 1 after push 2 2 1 after push 16 16 15 ⋯ 1 a push enters as columns 0 to 7 (left); the older columns move right

Masks

A mask can be passed using vmsk. This will zero every byte whose mask bit is clear. It adds no latency. It is there for matrices smaller than the register: masking the part of a register that is outside the matrix makes it count as zeros, so whatever was left in it doesn't get into the sums.

Floating-point behavior

Two exceptions to the floating-point rules above: a result that would be denormal is +0 whatever its sign, and a zero sum is +0 even when every product is −0.

Systolic array

The MXU is based on a systolic array: a grid of identical cells that only talk to their neighbours. The weights are loaded first and stay in place: cell (k, j) keeps W[k][j]. The input tensor A flows through the grid. Each cycle, every cell takes a partial sum from the cell above and a value of A from the cell on its left. It adds the value times its weight to the sum, then passes the sum down and the value to the right. A[i][k] enters the grid k cycles after A[i][0], so it reaches cell (k, j) in the same cycle as the partial sum for out[i][j].

A 2 by 2 systolic array computing A · W with A = [[1, 2], [3, 4]] and W = [[5, 6], [7, 8]], so A · W = [[19, 22], [43, 50]], over four cycles. The cells keep W = 5, 6 in the top row and 7, 8 in the bottom row. Cycle 0: the top-left cell computes 0 + 1·5 = 5. Cycle 1: top-left 0 + 3·5 = 15, top-right 0 + 1·6 = 6, bottom-left 5 + 2·7 = 19, and 19 leaves the bottom of column 0. Cycle 2: top-right 0 + 3·6 = 18, bottom-left 15 + 4·7 = 43, bottom-right 6 + 2·8 = 22; 43 and 22 leave. Cycle 3: bottom-right 18 + 4·8 = 50 leaves. The values of A waiting to enter each row are drawn on the left; row 1 starts one cycle after row 0 A = 1 2 3 4 W = 5 6 7 8 A·W = 19 22 43 50 cycle 0 3 W = 5 0 + 1·5 = 5 W = 6 2 4 W = 7 W = 8 cycle 1 W = 5 0 + 3·5 = 15 W = 6 0 + 1·6 = 6 4 W = 7 5 + 2·7 = 19 W = 8 19 cycle 2 W = 5 W = 6 0 + 3·6 = 18 W = 7 15 + 4·7 = 43 W = 8 6 + 2·8 = 22 19 43 22 cycle 3 W = 5 W = 6 W = 7 W = 8 18 + 4·8 = 50 19 43 22 50 Each cell keeps one value of W. A moves right one cell a cycle, row k starting k cycles late; the sum moves down one cell a cycle. Each result leaves the bottom when its sum is complete.

The MXU is a variant of a systolic array where each cell takes 8 consecutive products at a time instead of one. Within a cell, the first 4 products are summed with 3 bits more precision than f32 and rounded to f32, then the last 4 the same way; the two partial sums are added together, then added to the sum coming from the cell above.

Example: a 2 by 2 systolic array computing A · W, with A of size 2 × 16 and W of size 16 × 2. Columns are outputs 0 and 1. Each cell keeps W[k][j] for 8 consecutive k, drawn as two groups of 4 squares. A[i][0..7] enters the first row of cells from the left and A[i][8..15] the second, moving right from cell to cell; A[i][k] meets W[k][j]. In each cell each group of 4 gives a g, the sum of its 4 products A[i][k]·W[k][j], summed with 3 bits more precision than f32 and rounded once to f32: g0 and g1 in the top cells, g2 and g3 in the bottom ones. The two g are added in f32, and that is added in f32 to the partial sum s, which runs down each column. The bottom cells give out[i][0] and out[i][1]. s s A[i][0..7] rows 0 to 7 g0 g1 + g0 g1 + A[i][8..15] rows 8 to 15 g2 g3 + g2 g3 + out[i][0] out[i][1] + + + + W[k][j] for one k, kept in the cell; A[i][k] meets it g: the sum of its 4 products A[i][k]·W[k][j] +: one f32 add, rounded Example: a 2 × 2 systolic array computing A · W, with A of size 2 × 16 and W of size 16 × 2.

For integer multiplies we couldn't recover the order of the additions as integer sums are associative and commutative.

Result queue

Results are always 32 bits, f32 for floats and s32 for integers, one per word. A multiply gives one result row per row of A, so matmul.f32r and matmul.lmr push one result register, matmul.bf16 two, and the 8-bit and 4-bit types 4.

Results go into the unit's queue one vector register at a time, and vpop.mxu takes them out oldest first. The queue has room for 48 slots. A register of float results takes 2 slots and a register of integer results 1. We do not know why: both hold 32-bit values. Said another way, the queue holds up to 1.5 128 × 128 matrices of f32 results, or 3 of s32 results, at a time.

A multiply's room is reserved when it issues. If there isn't enough room left, the multiply waits, and so do the vector instructions behind it, including the pops that would make room: the core hangs. Pop the earlier results before issuing a multiply that wouldn't fit.

How fast

The latency in the table is the time until the first result. After that, the unit goes through 1 row of A per cycle for the float types and 2 for the integer types:

Type Rows of A per multiply Cycles per multiply
f32r 8 8
bf16 16 16
bf8, if8bf16 32 32
u8, s8, u4, s4 32 16

So a 128-row A goes through in 128 cycles (floats) or 64 (integers), plus the latency.

At one row of A a cycle, an MXU does 128 · 128 multiply-adds a cycle. Four MXUs at 1.5 GHz give 4 · 128 · 128 · 2 · 1.5·10⁹ ≈ 197·10¹² flops, which matches Google's official number. The integer path's 2 rows a cycle doubles that to Google's published 394 TOPS for int8.

The LMR

The LMR can be used instead of A to compute A · W using matmul.lmr: push A with .t, copy it to the LMR with lmatreg.lmr, and each matmul.lmr multiplies the next 8 rows of A by the GMR. For example, with W in v0 to v15 and A in v16 to v31, 8 f32 rows per register:

for g = 15 down to 0:             # W, last rows first
    matpush.f32r.msra u0, v(g)
lmatreg.gmr.msra u0               # GMR = W
for g = 15 down to 0:             # A with .t, last rows first
    matpush.f32r.msrb.t u0, v(16+g)
lmatreg.lmr.msrb u0               # LMR = A
for i = 0 to 15:
    matmul.lmr u0, width=128      # rows 8i to 8i+7 of A · W
    vpop.mxu v(32+i), u0

This gives the same result, bit for bit, as streaming A with matmul.f32r u0, v(16+i).

Copying a staging register filled without .t to the LMR transposes it: the same code without .t on A's pushes computes transp(A) · W. As we can only observe the results, we cannot tell whether the transpose happens when lmatreg.lmr copies the matrix in or when matmul.lmr reads it.

Each call to matmul.lmr gives the next 8 rows of the result. width says how many rows of A it goes through before it starts again from row 0.

Two 128-row copies of A in the LMR. width=128: calls 1 to 16 read rows 0 to 7 through 120 to 127, and call 17 goes back to row 0. width=32: calls 1 to 4 read rows 0 to 31, call 5 goes back to row 0 (so calls 1, 5, 9 read rows 0 to 7), and rows 32 to 127 are never read. width=128 call 1 call 2 ⋮ call 16 row 0 row 127 call 17 width=32 calls 1, 5, 9 … calls 2, 6, … calls 3, 7, … calls 4, 8, … rows 32 to 127: never read row 0 row 32 row 127 call 5 each matmul.lmr reads the next 8 rows of A; after width rows it goes back to row 0

width is 8, 16, 32, 64 or 128. It only sets where the walk wraps: 8 and 128 cost the same.

The first matmul.lmr after lmatreg.lmr fixes width until the next lmatreg.lmr. A matmul.lmr with another width hangs the core.

A matrix that was never loaded counts as all zeros: with no LMR, or no GMR, every result is 0.

Transcendentals (EUP)

Instructions

Instruction Slot Cycles What it does
vrsqrt.f32 vS valu3 6 1/√vS, from the chip's own tables.
vexp2.f32 vS valu3 6 2^vS, from the chip's own tables.
vlog2.f32 vS valu3 6 log2 vS, from the chip's own tables.
vtanh.f32 vS valu3 6 tanh(vS), from the chip's own tables.
vsigmoid.f32 vS valu3 6 sigmoid(vS) − 0.5, equal bit for bit to tanh(vS/2)/2.
vrecip.f32 vS valu3 6 1/vS, from the chip's own tables.
veup.push vS valu3 6 Pass a register through the EUP unchanged.
vpop.eup vD vres0, vres1 1 Pop the oldest EUP result.

The latencies above are counted until the result is available in the EUP queue.

Details

EUP queue

EUP functions push their results to the EUP queue. The queue holds 16 results.

Special cases

vtanh returns exactly ±1 for |x| ≥ 8 and x itself for |x| < 2−7. vsigmoid returns exactly ±0.5 for |x| ≥ 16 and x/4 for |x| < 2−5. A NaN result of a function always comes out as 0x7fc00000 except for veup.push which passes a NaN through unchanged.

How the EUP computes its functions

The EUP does not compute its functions exactly. It approximates each one in three steps.

1. Reduce the input. An identity leaves each function a small range to compute:

2. Pick a segment. The top bits of the reduced input pick a segment, and a per-function table gives three integers for that segment.

3. Evaluate. The three integers describe a quadratic that gets interpolated to give the final answer.

Accuracy

For inputs between ½ and 2, the functions are good to 14 to 20 bits at the median. This is enough precision for bf16 and f16 but is well short of f32's 24 bits. vlog2 has the fewest correct bits: 11.4 at worst and 14.2 at the median. vtanh and vsigmoid are not always monotonic, especially for larger inputs.

For inputs between one half and 2, six bars show each EUP function's bits correct from worst to median, with bf16 (8), f16 (11) and f32 (24) marked: vrecip 15.9 to 17.7, vrsqrt 17.7 to 19.9, vexp2 17.6 to 19.7, vlog2 11.4 to 14.2, vtanh 14.2 to 16.8 and vsigmoid 14.0 to 16.3 Bits correct (−log₂ of the relative error), over every input x in [½, 2) bf16 · 8 fp16 · 11 f32 · 24 vrecip 15.9 17.7 worst median vrsqrt 17.7 19.9 vexp2 17.6 19.7 vlog2 11.4 14.2 vtanh 14.2 16.8 vsigmoid 14.0 16.3 8 12 16 20 24 ← fewer correct bits more correct bits →

DMA

DMA instructions copy data from one memory to another: this core's VMEM, SMEM and instruction memory, the chip's HBM, and the host's memory across PCIe. The program issues a transfer and carries on. When the transfer is done, it credits a sync flag, and the program learns that the copy is done by waiting on that flag.

A DMA moves data in granules of 32 bytes, 8 words of 4 bytes. Every length is a number of granules, whatever the two memories are, SMEM included. A VMEM row, the size of one sublane of a vector register, is 512 bytes or 16 granules, and a whole vector register is 128 granules.

Instructions

Instruction Slot Cycles What it does
dma.simple src, dst, len=sL|imm, flag=sF|imm
[, sync_mode=1, upper=sU, thread=1, trace=1, relaxed=1]
s0 and s1 varies Copy len granules from src to dst, then credit flag.
dma.strided src, dst, len=sL|imm, flag=sF|imm,
run=sR, dst_stride=sD, src_stride=sS
[, sync_mode=1, upper=sU, thread=1, trace=1, relaxed=1]
s0 and s1 varies The same copy, in strided runs.
dma.general src, dst, len=sL|imm,
override=sC, dest_flags=sF, src_flag=sG
[, stride_info=sI|imm, strided=imm, sync_mode=1, upper=sU, thread=1, trace=1, relaxed=1]
s0 and s1 varies Cross-device DMAs.

A bundle holds at most one DMA. A DMA instruction spans both s0 and s1.

The operands:

Details

Memories

Memory Address counts Aligned to Size Used as
VMEM
mem.vmem
granules 16 granules 128 MiB source and destination
HBM
mem.hbm
granules 16 granules 16 GiB source and destination
SMEM
mem.smem
4-byte words any word 1 MiB source and destination
IMEM
mem.imem
granules 8 granules (4 bundles) 4 MiB source and destination
host
mem.host
bytes none (low 5 bits dropped) both, not host → host
TensorCore 1
mem.tc1.*
Refused: a v5e chip has one TensorCore, so there is no TensorCore 1.
SparseCore N
mem.scN.*
Refused: these are a SparseCore's spmem, smem, imem and timem, and a v5e has no SparseCore.
all TensorCores' VMEM
mem.vmem_all
Refused, even though this core is the only TensorCore on the chip.
memory id 3, no name
vmem, vmem.noncore
Refused. The assembler has no name for it.

A misaligned address stops the core. A transfer that runs past the end of the memory stops the core.

Host addresses count bytes and are 64 bits wide, too wide for one scalar register. The address register holds the low 32 bits and upper=sU the high 32 bits.

A host address doesn't need to be aligned: the transfer starts at address & 0xffffffffffffffe0.

Not every direction is possible:

to VMEM to SMEM to HBM to IMEM to host
from VMEM runs runs runs runs19 runs
from SMEM runs runs runs runs19 runs
from HBM runs runs runs runs19 runs
from IMEM runs runs runs runs runs (dma.general)
from host runs runs runs refused19 refused

19 The copied data must be valid bundles. If not, the core stops.

Lengths

len is 0, a multiple of 16 granules (whole 512-byte rows), or 1:

Source and destination address

The source and destination addresses passed to a dma.general or a dma.simple can be either a vector register or a scalar register. In the case of a vector register the DMA only reads element 0 of the register.

dma.strided can't: both its addresses always come from scalar registers.

Strided transfers

dma.strided copies len granules as a series of runs. Everything in this section counts granules (32 bytes), except run, which counts rows of 16 granules:

dma.strided with len 128, run 2, src_stride 0x40 and dst_stride 0x20. Top, 16 HBM rows at granule offsets 0x00 to 0xf0; the runs are rows 0x00 and 0x10, 0x40 and 0x50, 0x80 and 0x90, 0xc0 and 0xd0, every fourth pair. Bottom, 8 VMEM rows at offsets 0x00 to 0x70. Each run lands right after the previous one: run k at destination offset 0x20 times k dma.strided …, len=128, run=2, src_stride=0x40, dst_stride=0x20 HBM VMEM 0x00 0x10 0x20 0x30 0x40 0x50 0x60 0x70 0x80 0x90 0xa0 0xb0 0xc0 0xd0 0xe0 0xf0 0x00 0x10 0x20 0x30 0x40 0x50 0x60 0x70 Each cell is a row of 16 granules, labelled with its address in granules.

A negative stride walks backward from the start address:

dma.strided with len 64, run 1, src_stride -0x40 and dst_stride 0x10. The source address points at row 0x100; the runs read rows 0x100, 0xc0, 0x80 and 0x40 and land in destination rows 0x00, 0x10, 0x20 and 0x30, so the order is reversed dma.strided …, len=64, run=1, src_stride=-0x40, dst_stride=0x10 src dst 0x00 0x10 0x20 0x30 0x40 0x50 0x60 0x70 0x80 0x90 0xa0 0xb0 0xc0 0xd0 0xe0 0xf0 0x100 0x00 0x10 0x20 0x30 0x40 0x50 0x60 0x70 Each cell is a row of 16 granules, labelled with its address in granules.

A zero src_stride copies one run again and again:

dma.strided with len 64, run 1, src_stride 0 and dst_stride 0x10. Source row 0x00 is copied to destination rows 0x00, 0x10, 0x20 and 0x30 dma.strided …, len=64, run=1, src_stride=0, dst_stride=0x10 src dst 0x00 0x10 0x20 0x30 0x40 0x50 0x60 0x70 0x00 0x10 0x20 0x30 0x40 0x50 0x60 0x70 Each cell is a row of 16 granules, labelled with its address in granules.

A zero dst_stride writes every run onto the same rows:

dma.strided with len 64, run 1, src_stride 0x10 and dst_stride 0. Source rows 0x00 to 0x30 are all written to destination row 0x00; the first three are overwritten and row 0x00 ends up holding source row 0x30 dma.strided …, len=64, run=1, src_stride=0x10, dst_stride=0 src dst 0x00 0x10 0x20 0x30 0x40 0x50 0x60 0x70 0x00 0x10 0x20 0x30 0x40 0x50 0x60 0x70 Each cell is a row of 16 granules, labelled with its address in granules.

In pseudocode, where src and dst are the two addresses in granules and the memories are arrays of granules:

run_len = run · 16          # granules in one run
k = 0
while k · run_len < len:
    # strides are signed 32-bit numbers
    s = src + k · src_stride
    d = dst + k · dst_stride
    # the last run can be cut short
    n = min(run_len, len − k · run_len)
    if s or d breaks the alignment rules of Memories: stop the core
    for i in 0 .. n − 1:
        dst_mem[d + i] = src_mem[s + i]
    k = k + 1
flag += len

SMEM is the other exception to counting in granules: its addresses count words. Its strides still count granules, so a stride of 1 moves 8 words.

General transfers

TPU chips in a slice are wired together by ICI, the inter-chip interconnect. dma.general is the one DMA that can send data across it, into another chip's memory; dma.simple and dma.strided stay on this chip or go to its host. As we worked on a single TPU v5e chip, we have not seen a remote transfer land.

dma.general adds three things to dma.simple: another chip as the destination, separate flags for the two ends, and a stride descriptor of several levels.

Override

override is a 32-bit register that can change a dma.general at run time without changing the program. Bits 16 to 28, except bit 23, are masks: each one is XORed into the matching field of the instruction: the source memory and core, relaxed, sync_mode, and the destination memory and core. The DMA then runs as if the instruction had encoded the result. The other bits are taken as they are. Bit 29 turns an SMEM-to-SMEM copy into a single 4-byte write. Bits 0 to 13 hold what we take to be a chip id, used only when bit 14 is set; with bit 14 clear the transfer stays on this chip or goes to its host. Bits 15 and 23 do nothing, alone or with thread, trace, relaxed or sync_mode set. Our reading is that bit 14 sends it over ICI to chip chip_id, and that the masks let one compiled kernel be pointed at different memories or chips at run time.

The 32-bit override word holding 0x08000000, split into fields: bit 31 ignored; bit 30 refused; bit 29 the 4-byte write; bits 28 to 26, destination core, here 2: self (0) XOR 2 = TensorCore 0; bits 25 to 24, destination memory; bit 23 no effect; bit 22 sync_mode; bit 21 relaxed; bits 20 to 18 source core; bits 17 to 16 source memory; bit 15 no effect; bit 14 remote; bits 13 to 0 chip_id. Bits 28 to 16 are XORed into the same fields of the instruction; bits 31 to 29, 14 and 13 to 0 are not override = 0x08000000, what libtpu writes for a copy into this core's VMEM or SMEM 31 0 ignored 30 0 refused 29 0 4-byte write 28–26 2 dst core 25–24 0 dst memory 23 0 no effect 22 0 sync_mode 21 0 relaxed 20–18 0 src core 17–16 0 src memory 15 0 no effect 14 0 remote 13–0 0 chip_id self (0) XOR 2 = TensorCore 0 not XORed XORed into the same fields of the instruction not XORed
Sync flags

A dma.general takes two flag operands, both registers. dest_flags is a flag word like dma.simple's, naming up to two flags, credited once the data has landed. src_flag is a single flag. Our reading is that on a remote transfer dest_flags are the receiving chip's and src_flag the sending chip's.

Strides

dma.general can step through memory with up to three nested strides. They sit in SMEM, in a stride descriptor the program writes before the DMA. strided says how many levels there are, between 0 and 3. stride_info is the SMEM word where the descriptor starts. It must be a multiple of 16 or the core faults. Each level is three words: source stride, destination stride and count. Strides count granules.

dma.general with len 96, stride_info 0x200 and strided 2. SMEM words 0x200 to 0x205 hold the descriptor: level 0 is 128 (source stride), 160 (destination stride), 2 (count); level 1 is 32, 48, 1. Words 0x206 to 0x208, where a third level would go, are not read. Level 0 moves the source by 128 and the destination by 160 a step, does 2 level-1 steps each, and repeats until len; level 1 moves them by 32 and 48 a step, and its count of 1 makes a run 1 row long dma.general …, len=96, stride_info=0x200, strided=2 SMEM 0x200 128 src stride 0x201 160 dst stride 0x202 2 count 0x203 32 src stride 0x204 48 dst stride 0x205 1 count 0x206 0x207 0x208 stride_info level 0 source +128, destination +160 a step; 2 level-1 steps each; repeats until len level 1 source +32, destination +48 a step; count 1: a run is 1 row (16 granules) level 2 not read: strided=2 means 2 levels

In pseudocode, for three levels, with the descriptor holding S0 D0 c0 | S1 D1 c1 | S2 D2 c2:

run_len = c2 · 16           # granules in one run
copied = 0
for i0 = 0, 1, 2, ...:      # until len granules are copied
    for i1 in 0 .. c0 − 1:
        for i2 in 0 .. c1 − 1:
            s = src + i0 · S0 + i1 · S1 + i2 · S2
            d = dst + i0 · D0 + i1 · D1 + i2 · D2
            n = min(run_len, len − copied)
            copy n granules from s to d
            copied = copied + n
            if copied == len: done

Filling instead of copying

All three DMA instructions take an optional field sync_mode=1, which turns the copy into a fill: the source register's value is written, unchanged, to every 32-bit word of the destination. The source memory isn't read, but the value must still be a valid source address: a multiple of 16 for a VMEM or HBM source, anything for an SMEM source. A fill to or from the host is refused.

relaxed=1 is refused on every copy. A fill accepts it, and it changes nothing visible there.

The sync flag

A DMA adds len to its flag, so the wait for a 16-granule copy is sync_greater_equal flag[n], 16.

A long transfer doesn't credit its flag all at once: it credits it about every 2048 granules (64 KiB). The done bit is set once, at the end of the transfer.

The add wraps at 32 bits, and mixes freely with the core's own add_sync and with other DMAs on the same flag.

The flag operand names one flag, flag=flag[6], or two, flag=(flag[6], host.flag[7]). The transfer credits both. Our reading is that this is mainly useful for tracing: one sync flag can be used for synchronization within the TensorCore, and the other one used to transfer the DMA progress back to the host. A credit to an invalid sync flag stops the core.

Ordering with vector code

A DMA is written in slots s0 and s1, and it runs in two halves. At fetch, like any scalar instruction, it reads its scalar registers: addresses, length, flag, upper. It then joins the vector queue and issues from it like any vector bundle.

The last sync flag credit is done with a bit of delay compared to when the transfer is done. The specific time is not fully deterministic and depends on the source and destination memories, but as a rule of thumb has been observed to be roughly 150 cycles. We have a weak suspicion that the DMA sync flag credit mechanism is done on a 1150 MHz clock but do not have much evidence and explanation for it.

Accesses to scalar memory using sld and sst always win against a pending DMA. The story is more subtile for vector memory access using vld and vst. The dma engine has an arbitration mechanism to avoid being fully starved off from core-issued vector memory accesses.

In practice busy polling the memory manually to detect the end of a DMA is a bad idea as it will slow the DMA.

What a transfer costs

DMA timing is less regular than the rest of the core. All times here are from the issue to a wait on the flag that returns, which adds about 17 cycles to the transfer itself.

Granules per cycle on long transfers:

to VMEM to SMEM to HBM to host
from VMEM ~15.8 1.0 ~15.820 ~0.3
from SMEM 1.0 0.5 1.0 not measured
from HBM ~15.8 ~0.97 ~6.3 ~0.3
from host ~0.3 not measured ~0.3 refused

20 15.8 for the first 0.7 to 1.4 MiB, then about 13.6.

From the above:

Queues and limits

DMAs go through a network of queues before being executed as shown below. Each final queue runs its transfers one after another in issue order, except when they are prioritized. Each stage passes its oldest DMA on when the next stage has room. A DMA issue might stall if the queues are full.

We couldn't pin down how deep these queues are and are not confident they really are queues (hardware pipelining can easily be mistaken for queueing!). The number of entries a DMA takes depends on the instruction (simple, strided, or general with one or more levels of strides), on where it goes and comes from, and on the address range. Host DMAs, for example, are counted differently from on-chip ones in ways we don't understand.

Issued DMAs enter a staging FIFO, in order. From there a DMA into SMEM, from VMEM, HBM or SMEM, goes straight to its queue. Every other DMA, host ones included, goes through a second in-order queue, the shared queue. From there an on-chip DMA goes to its own queue: VMEM or HBM into VMEM, VMEM or HBM into HBM, SMEM to HBM, SMEM to VMEM. A DMA with a host end goes to one host buffer, then to one of three host queues: VMEM, HBM or SMEM to the host; the host to VMEM or SMEM; the host to HBM issue staging FIFO, in order shared queue, in order host buffer {VMEM, HBM, SMEM} → SMEM {VMEM, HBM} → VMEM {VMEM, HBM} → HBM SMEM → HBM SMEM → VMEM {VMEM, HBM, SMEM} → host host → {VMEM, SMEM} host → HBM

Transfers in different queues run at the same time. Two transfers in different queues can still slow each other down where they use the same memory:

Priority

All three DMA instructions take an operand thread=0|1. On VMEM → HBM and HBM → VMEM only, thread=1 makes a transfer strictly low priority: a later thread=0 transfer in the same queue goes first, and the thread=1 one waits, with no limit, for as long as thread=0 transfers keep coming, even if it has already started. On every other path thread does nothing.

Misc

Instructions

Instruction Slot Cycles What it does
Writing a flag: flag n of this core
set_sync flag[n], sY|imm misc 321 Set a sync flag.
add_sync flag[n], sY|imm misc 321 Add to a sync flag.
set_sync_done flag[n], sY|imm misc 321 Set the flag and clear its done bit. The value is optional.
add_sync_done flag[n], sY|imm misc 321 Add to the flag and clear its done bit.
set_sync_done_inverted flag[n], sY|imm misc 321 Set the flag and set its done bit.
add_sync_done_inverted flag[n], sY|imm misc 321 Add to the flag and set its done bit.
set_public_access flag[n], sY|imm misc 1 Set a flag's public-access bit.
Waiting on a flag
sync_equal flag[n], sY|imm misc 222 Hold the vector side until the flag = the value.
sync_not_equal flag[n], sY|imm misc 222 Hold the vector side until the flag ≠ the value.
sync_greater_than flag[n], sY|imm misc 222 Hold the vector side until the flag > the value.
sync_greater_equal flag[n], sY|imm misc 222 Hold the vector side until the flag ≥ the value.
sync_less_than flag[n], sY|imm misc 222 Hold the vector side until the flag < the value.
sync_done flag[n] misc 222 Hold the vector side until the flag's done bit is set.
sync_not_done flag[n] misc 222 Hold the vector side until the flag's done bit is clear.
sync_*.yield … misc 222 The yieldable form of each wait above, with the same operands.
Reading a flag
read_sync flag[n] misc 123 Push a flag's value to the sync-flag result FIFO.
read_sync_done flag[n] misc 123 Push a flag's done bit to the sync-flag result FIFO.
read_sync_public_access flag[n] misc 123 Push a flag's public-access bit to the sync-flag result FIFO.
Atomics: flag n of any core, on any chip
atomic.write flag[chip:core:n], sY|imm misc 4624 Write the flag.
atomic.write.done flag[chip:core:n], sY|imm misc 4624 Write the flag and clear its done bit.
atomic.write.done.inv flag[chip:core:n], sY|imm misc 4624 Write the flag and set its done bit.
atomic.add flag[chip:core:n], sY|imm misc 4624 Add to the flag.
atomic.add.done flag[chip:core:n], sY|imm misc 4624 Add to the flag and clear its done bit.
atomic.add.done.inv flag[chip:core:n], sY|imm misc 4624 Add to the flag and set its done bit.
Holding the vector issue
delay imm misc N + 2 Hold the vector issue N + 2 cycles, N being a small immediate count.
vdelay imm|sX misc N + 1 Hold the vector issue N + 1 cycles, N being the operand's value.
vst_fence misc 1 Vector store fence.
Trace, interrupts and random numbers
trace imm|sX, imm|sY misc —25 Append a hardware trace record.
set_tracemark imm|sX misc —25 Set the trace record's mark.
core_interrupt imm misc —25 Raise programmable interrupt N mod 4 of this core's bank; the program runs on. A halt raises ProgramCompletion instead.
read_prng vD misc 1 Read the PRNG state.
vrandom vD misc 1 Draw a register of random words.
Core-to-core FIFO
vpop.ccrf vD vres0, vres1 — Pop the core-to-core vector FIFO.

21 A read_sync or a wait issued within 3 cycles of a flag write is held until those 3 cycles have passed.

22 Vector-issue cycles when the condition already holds. Otherwise stall until the condition holds.

23 The value can be popped with vpop.sfrf 13 cycles after the read_sync.

24 For a flag of this same core: the cycles from the atomic's issue until a read_sync reads the new value.

25 No result.

Details

Sync flags

There are two ways to read a flag. read_sync copies its value into a queue that the scalar side has access to. A wait (sync_greater_equal and the other sync_* instructions) holds the program until the flag reaches a value.

A sync flag is either public or private, and starts public. Both the software driver and a TPU instruction can set that property.

A private flag can be read from software but its value cannot be set. A public flag can be read and set from software. A host write to a private flag is dropped, and a running core stops at once. Later runs don't start until a PCI function reset, which also makes every flag public again.

A vpop.sfrf gets a read_sync's value 13 cycles after the read_sync; a pop issued sooner waits for it. The sfrf queue holds 16 values.

Waits

A wait whose condition already holds costs 2 cycles. Otherwise the vector side stalls until the condition is true. In the meantime the scalar side runs on until it is blocked on the vector side.

Atomics

Atomic instructions allow accessing sync flags from other cores and chips atomically.

A single v5e chip has only one TensorCore, so atomics are of little use there. An atomic write to a flag of this core can be read back 46 cycles later.

An atomic names its flag with three fields, written chip:core:n in the table above. In the instruction they are packed into one 32-bit word, an immediate or a register:

Bits Field
0–13 the flag number
14–16 the core selector
17–31 the chip
Selector Core
0 RESERVED_SELF
1 NON_CORE
2, 3 TensorCore 0, 1
4 to 7 SparseCore 0 to 3

Overflow of add_sync

An add_sync whose signed 32-bit sum overflows still writes the wrapped sum, then the core stops. A DMA credit that overflows a flag does the same.

Delay vs vdelay

delay and vdelay have the same purpose: to hold the vector issue for a number of cycles. delay takes only a small immediate count, while vdelay takes either a register or a larger immediate, at the cost of using one of the bundle's immediates.

vpop.ccrf

Transfers a vector from one TensorCore to another. It is backed by one queue per TensorCore. Its latency and precise semantics are unknown, as a TPU v5e has only a single TensorCore.

yield instructions

We were unable to pinpoint how .yield forms differ from non-yield forms.