Skip to content

AMD XDNA NPU Architecture Reference

Devices

Family Architecture NPU columns × rows (compute) IRON device class
Phoenix / Hawk Point AIE2 a.k.a. XDNA 4 × 4 (NPU1) NPU1, NPU1Col1, NPU1Col2, NPU1Col4
Strix / Krackan Point AIE2P a.k.a. XDNA2 8 × 4 (NPU2) NPU2, NPU2Col1, NPU2Col4, NPU2Col8
from aie.iron.device import NPU1, NPU1Col1, NPU2, NPU2Col4
import aie.iron as iron

dev = iron.get_current_device()  # auto-detect the attached NPU at runtime
# or pick explicitly:
dev = NPU2()                     # all columns of an AIE2P NPU
dev = NPU1Col1()                 # just one column of an AIE2 NPU

Tile types (per column)

Bottom to top:

Row Tile Purpose
0 Shim DMA between external (DDR) memory and the AIE array
1 Mem L2 scratchpad shared between Shim and the column's compute tiles
2..5 Compute The actual cores running C++ kernels; each has a small L1

Throughout these skills, compute tile, core, and AIE tile all refer to these row-2..5 tiles.

Use tile(col, row) (lower-level API) only when you need explicit coordinates. In the high-level API, the compiler picks placement; you control parallelism by spawning N Workers.

Memory hierarchy & sizes (rule-of-thumb)

Level Size Latency Who fills it
L1 (per compute tile) ~64 KB 1 cycle ObjectFifo buffer + stack
L2 (per mem tile) ~512 KB a few cycles Mem tile DMA / forward / split / join
L3 (external DDR) GBs 100s of cycles Shim DMA via ObjectFifoHandle fill/drain

Keep working sets in L1; spill to L2 via Mem tile; reach DDR only at the start/end of a phase.

Vector registers & widths

AIE2/AIE2P has 512-bit vector registers. The number of lanes per aie::vector<T, N> follows N = 512 / bitwidth(T) for the natural width, but the AIE API supports smaller and (with grow<>) larger widths for software pipelining flexibility.

Type Bits Natural lanes / vector Accumulator
int8_t, uint8_t 8 64 acc32
int16_t, uint16_t 16 32 acc32 (or acc64 for wide multiplies)
int32_t, uint32_t 32 16 acc64
bfloat16 16 32 (a multiply is 16 on AIE2: AIE_BF16_LANES) accfloat (or accauto in MMUL)
float (fp32) 32 16 accfloat

Default to the natural lane count for the dtype (the middle column above: 32 for bfloat16/16-bit, 64 for int8, 16 for 32-bit). This is what the examples in this skill use. Drop to a narrower width only if the compiler reports register spills at the natural width.

MMUL shapes

The aie::mmul<r, s, t, T_in, T_w, accauto> intrinsic performs an r × s × s × t matrix multiply per call into an r × t accumulator. AIE2 and AIE2P support different shape sets:

Input × weight → output AIE2 shape (r×s×t) AIE2P shape (r×s×t) Notes
int8 → int8 4×8×8 8×8×8 AIE2P doubles M
int8 → int16 4×8×8 8×8×8
int8 → int32 4×8×8 8×8×8
int16 → int16 4×4×4 4×4×8 AIE2P doubles N
int16 → int32 4×4×4 4×4×8
bf16 → bf16 4×8×4 4×8×8 AIE2P doubles N
bf16 → float32 4×8×4 4×8×8

Building with -DAIE_API_EMULATE_BFLOAT16_MMUL_WITH_BFP16 swaps AIE2P's bf16 MMUL from native mul-acc to BFP16 emulation, which uses an 8×8×8 micro-kernel instead of 4×8×8. Other (arch, dtype) combinations are unaffected.

Don't hardcode these in code. Read the geometry off the kernel you're actually going to link, so it can't drift from the compiled .cc:

r, s, t = kernels.mm(dim_m=64, dim_k=64, dim_n=64,
                     input_dtype=bfloat16, output_dtype=np.float32).mac_dims

The table above is for reasoning about the divisibility constraints below; .mac_dims is what belongs in a design. (kernels.cascade_mm is a scalar kernel on both architectures — its mac_dims are (1, 1, 1) and its buffers must stay plain row-major.)

Divisibility constraints when calling MMUL with outer expansion (4x2, 4x4, etc.):

// Example: matmul_vectorized_4x8x4_bf16_bf16 with 4x4 expansion on AIE2
static_assert(m % (4 * r) == 0);   // 4 rows × r=4  ⇒ m % 16 == 0
static_assert(k % s == 0);          // k % 8  == 0
static_assert(n % (4 * t) == 0);   // 4 cols × t=4 ⇒ n % 16 == 0

Violate these and you get either a compile error or silently wrong results.

Routing / placement constraints (low-level API only)

  • Each column has limited switch resources; broadcasting one ObjectFifo to N consumers across distant columns may fail to route — prefer per-column producers or forward() through mem tiles.
  • Compute tiles only have direct DMA to/from their column's mem tile and adjacent compute tiles. Cross-column data goes through mem-tile broadcast.
  • The high-level placer respects these; if you hand-place tiles, verify with ctx.module.operation.verify().

Performance ballpark (per compute tile, peak)

Op AIE2 (Phoenix) AIE2P (Strix)
int8 MAC / cycle 256 512
bf16 MAC / cycle 128 256
Clock ~1.0–1.3 GHz ~1.5 GHz nominal; 1.76 GHz measured on Strix

Multiply per-tile peak by (rows × columns) for the full array. You won't hit peak without MMUL, restrict pointers, and pipelined loops.

Hardware limits to remember

  • Tile L1: ~64 KB. Bigger tensors must be tiled across iterations.
  • ObjectFifo depth: practical max ~8 in L1; use mem-tile ObjectFifos for deeper buffering.
  • BD (Buffer Descriptor) addressing dimensions are not uniform across tile types. On AIE2/AIE2P:
Tile type ND dims per BD BDs per tile Max transfer length
Mem tile 4 48 2^17 − 1
Compute (core) tile 3 16 2^14 − 1
Shim tile 3 16 2^32 − 1

This drives real design decisions: a reshape needing 4 (size, stride) dimensions must land on a mem-tile DMA. If a compute tile's to_stream or a shim's tap already needs 3 dimensions, you have no fourth to spend — route L3→L2→L1 and put the reshape on the mem-tile split()/join()/forward() instead. For access patterns beyond the limit, chain multiple BDs or use runtime repeat counts.

DMA transforms aren't the only way to reshape/translate data, though: aie::vshuffle can do in-register lane permutation on the compute tile itself, and being a vector op it can overlap with (be masked by) concurrent/pipelined MACs — worth considering as an alternative to an extra DMA hop when the reshape is small enough to fit in a shuffle. - Workers per Program: bounded by available compute tiles (16 on NPU1, 32 on NPU2).