Hardware¶
A hardware target in Stream is a system of heterogeneous dataflow cores. You describe it with YAML in two layers:
- An accelerator file - names the system, lists its cores, and defines how the cores are connected.
- One core file per core type - describes a single core's compute array, memory hierarchy, and role.
Accelerator files live in stream/inputs/examples/hardware/; the core files they reference live in stream/inputs/examples/hardware/cores/. Several cores in an accelerator may point at the same core file (e.g. four identical compute cores).
The format is validated by stream/parser/accelerator_validator.py and built by stream/parser/accelerator_factory.py - those files are the authoritative schema if anything below is ambiguous.
Accelerator file¶
name: tpu_like_quad_core
cores:
0: ./cores/tpu_like.yaml # compute A
1: ./cores/tpu_like.yaml # compute B
2: ./cores/tpu_like.yaml # compute C
3: ./cores/tpu_like.yaml # compute D
4: ./cores/pooling.yaml # pooling engine
5: ./cores/simd.yaml # SIMD / vector unit
6: ./cores/offchip.yaml # DRAM controller
core_coordinates:
0: [0, 0]
1: [1, 0]
2: [1, 1]
3: [0, 1]
4: [2, 0]
5: [2, 1]
6: [1, 2]
offchip_core_id: 6 # core that fronts external memory
unit_energy_cost: 0 # default per-transfer energy for the links below
core_connectivity:
# 2-D mesh among the four compute cores
- type: link
cores: [0, 1]
bandwidth: 32
- type: link
cores: [1, 2]
bandwidth: 32
# … more links …
# shared bus to off-chip memory (all cores ↔ DRAM)
- type: bus
cores: [0, 1, 2, 3, 4, 5, 6]
bandwidth: 128
| Field | Required | Meaning |
|---|---|---|
name | yes | A label for the system. |
cores | yes | Map of integer core id → core file. Paths may be relative (./cores/foo.yaml); a bare filename is resolved against <accelerator_dir>/cores/ then <accelerator_dir>/. |
offchip_core_id | yes | The id of the core that fronts external memory (DRAM). Must appear in cores. No computation is ever placed here. |
kernel_library | no | Path, relative to this file, to the kernel library the target's AIE kernels are described by. |
bandwidth | no | Map of core id to the measured bandwidth every transfer through that core shares, in bits per cycle: ceiling for reads and writes together, contiguous one way, and strided.read / strided.write per contiguous span in bytes, or, where nothing is measured, burst, the bytes the memory moves per access, of which a shorter contiguous run uses only its share (see Data layout). The allocation then runs no transfer through the core faster than its access pattern allows, and bounds every step by the time the core spends on one iteration's transfers. A key can also name one memory port, see Memory ports. |
reconfiguration | no | What reconfiguring the array between the designs of one dispatch costs, read by the cores' namespace; for aie2, cycles_per_column per design and reset_cycles once. |
core_connectivity | yes | List of links and buses connecting the cores (see below). |
unit_energy_cost | no | Default energy per transferred word for every connection, unless overridden per-connection. Defaults to 0. |
core_coordinates | no | Map of core id → [col, row]. Used for placement-aware models; required for the AIE namespace, optional otherwise. |
core_memory_sharing | no | Groups of core ids that share their top-level memory, e.g. ["0, 1", "2, 3"]. Its capacity bounds everything the group's cores hold in it together: split tiles add up, a tensor each core holds whole counts once. Data between the group's cores moves in place, without a route; the group sends and receives through its first core where that one has links, and the shared memory is priced once. |
Memory ports¶
A bandwidth key <core id>.<memory>.<port> (e.g. 2.dram.rw_port_1) gives the measured model to one port of a top-level memory of a ZigZag core. It replaces that port's bandwidth_max, in bits per cycle. A core with a core-id key has no per-port model, so it takes no port key.
A port belongs to a physical memory: cores in one core_memory_sharing group share it, and memories listed in one memory_aliases group share the ports of the group's first memory.
The memory_ports constraint family, in the default set, uses these rates. Its interval option (default true) fits each port's bits per iteration in its rate times the initiation interval; burst (default true) fits each step's bits in its rate times that step's latency.
Connections: link vs bus¶
Each entry in core_connectivity is one connection:
| Field | Required | Meaning |
|---|---|---|
type | no | link (point-to-point between exactly two cores) or bus (shared medium between two or more cores). Defaults to link. |
cores | yes | The connected core ids. Exactly two for a link; two or more for a bus. |
bandwidth | yes | Peak bandwidth of the connection (bits per cycle), > 0. |
unit_energy_cost | no | Per-word energy for this connection; inherits the accelerator-level default if omitted. |
Connections are how the MILP allocator routes tensors between cores. A typical accelerator has fast point-to-point links on-chip and one wider bus to the off-chip core.
A transfer's route serves each target from the nearest core holding the data it reads, then joins each source's targets in one tree (a Steiner tree, grown from the shortest paths with each link weighted by the time the transfer already keeps it busy), so data shared by several targets crosses each link once. Partial sums, of a computation split over a dimension it reduces, are added up on their way to their target; one headed off-chip is completed in the on-chip memories next to it, a slice each, as a reduce-scatter. A route never passes through the off-chip core, and a bus carries what each core sends over it separately. Each link is charged the share of the transfer's data crossing it.
Core file¶
Every core declares a type of the form <namespace>.<kind>:
- namespace -
zigzag(a full dataflow core modelled with ZigZag's cost model) oraie2(an AMD AIE tile). - kind -
compute,memory,shim, oroffchip.
A bare kind (e.g. type: compute) is accepted and defaults to the zigzag namespace.
ZigZag cores (compute / offchip)¶
A ZigZag core has a memory hierarchy and an operational (MAC) array:
name: tpu_like
type: zigzag.compute
memories:
rf_2B: # a register file serving outputs
size: 16 # capacity in bits
r_cost: 0.021 # read energy (pJ)
w_cost: 0.021 # write energy (pJ)
area: 0
latency: 1
operands: [O] # which operands this memory holds
ports:
- name: r_port_1
type: read # read | write | read_write
bandwidth_min: 16 # bits / cycle
bandwidth_max: 16
allocation:
- O, tl # (operand, port-direction tag)
- name: w_port_1
type: write
bandwidth_min: 16
bandwidth_max: 16
allocation:
- O, fh
served_dimensions: [D2] # array dimensions this memory feeds
sram_2MB:
size: 16777216
r_cost: 416.16
w_cost: 378.4
latency: 1
operands: [I1, I2, O] # weights, activations, outputs
ports:
- name: r_port_1
type: read
bandwidth_min: 64
bandwidth_max: 2048
allocation: [I1, tl, I2, tl, O, tl, O, th]
- name: w_port_1
type: write
bandwidth_min: 64
bandwidth_max: 2048
allocation: [I1, fh, I2, fh, O, fh, O, fl]
served_dimensions: [D1, D2]
operational_array:
unit_energy: 0.04 # pJ per MAC
unit_area: 1
dimensions: [D1, D2] # spatial (unrolled) dimensions of the array
sizes: [32, 32] # 32 × 32 systolic array
Operands. Stream uses three algorithmic operands: I1 (weights / first input), I2 (activations / second input), and O (output). A memory's operands field lists which of these it stores.
Ports and allocation. Each memory exposes ports. An allocation entry pairs an operand with a port-direction tag - fh (write the high / final value), fl (write the low / partial value), tl (read low / from lower level), th (read high / to higher level). This is how Stream knows which port carries which data movement when it builds the cost model.
served_dimensions. The operational-array dimensions (D1, D2, …) that this memory feeds. An empty list means the memory is innermost (feeds a single MAC lane).
operational_array. The spatial compute fabric: dimensions names the unrolled axes, sizes gives their extent, and unit_energy / unit_area are the per-MAC cost.
The off-chip core¶
The off-chip / DRAM core is just a ZigZag core with type: zigzag.offchip, a single large DRAM memory holding all operands, and a zero-size operational array (it never computes):
name: offchip
type: zigzag.offchip
memories:
dram:
size: 10000000000
r_cost: 1000
w_cost: 1000
latency: 1
operands: [I1, I2, O]
ports:
- name: rw_port_1
type: read_write
bandwidth_min: 64
bandwidth_max: 64
allocation: [I1, fh, I1, tl, I2, fh, I2, tl, O, fh, O, tl, O, fl, O, th]
served_dimensions: [D1, D2]
operational_array:
unit_energy: 0
unit_area: 0
dimensions: [D1, D2]
sizes: [0, 0]
Restricting a core to certain operators¶
A compute core can be limited to specific operator types with an operator_types list. For example a pooling engine accepts only pooling ops:
name: pooling
type: zigzag.compute
# … memories, operational_array …
operator_types: [MaxPool, AveragePool, GlobalAveragePool]
If operator_types is omitted, the core accepts any operator. The auto-mapper (see Mapping) uses this field to decide which nodes a core is eligible for - e.g. SiLU/Mul go to the SIMD core, pooling to the pooling core, and Conv/Gemm to the general compute cores.
Operand precision¶
A compute core can declare the element types it computes in:
operand_precision:
input: bf16
accumulator: fp32
The accumulator sets how wide a matmul's or convolution's partial sums are while the core accumulates them; the result is written out at its output tensor's type. A core without an accumulator keeps partial sums at the output's type. input names the multiplier format; it is recorded in the core's hardware cost report and does not change latency or energy. The known types are int4, int8, uint8, int16, int24, bf16, fp16, int32 and fp32.
Memory banks¶
A core can declare how its memory is banked, for the layout report to place each tile's bytes in banks:
banks:
count: 8 # banks
width: 16 # bytes per bank word
stripe: 512 # words a bank holds before the next one does; 1 (the default) interleaves words
Byte address a sits in bank a // width // stripe % count. An AIE-ML tile's eight banks are each one contiguous 8 KB range (stripe: 512); a memory that interleaves words across its banks takes the default. The allocation does not price bank conflicts: the banks only describe where data lands.
Systolic arrays¶
An operational array whose operands move from one unit to the next, one unit per cycle, rather than being broadcast, lists the dimensions they move along:
operational_array:
dimensions: [D1, D2]
sizes: [256, 256]
systolic_dimensions: [D1, D2]
ZigZag then adds the cycles the last results take to leave the array, one per unit in use along each such dimension (510 on a full 256 x 256 array). A run's tiles follow each other through the array, so Stream charges this drain once per run, in its fill, rather than on every tile. A node that reads another's results waits for them to drain, while one that does not streams into its array behind it, so the fill adds the drains along the run's longest chain of dependent nodes: a SwiGLU's gate and up projections drain together, then its down projection. The TPU-like, TPU7x MXU and FuseMax arrays are systolic.
AIE cores¶
AIE tiles use the aie2 namespace and a lighter schema describing tile-local memory and the object-FIFO depth, rather than a full ZigZag hierarchy. These are used by the AIE codegen entry points and live under stream/inputs/aie/hardware/.
name: aie_tile
type: aie2.compute
max_object_fifo_depth: 12
memory:
capacity: 524288 # bits
bandwidth_min: 512 # bits / cycle
bandwidth_max: 512
Example systems¶
Eight non-AIE example accelerators ship under stream/inputs/examples/hardware/ and are exercised across both example workloads in the test matrix (see the Workload × Hardware matrix in the README):
eyeriss_like_single_core, eyeriss_like_dual_core, eyeriss_like_quad_core, tpu_like_quad_core, simba_small, simba (a 36-core chiplet mesh), fusemax, and meta_prototype_dual_core_simd_offchip.
AIE example systems (single_core, single column, full array, Strix variant) live under stream/inputs/aie/hardware/.