Skip to content

Hardware

A hardware target in Stream is a system of heterogeneous dataflow cores. You describe it with YAML in two layers:

  1. An accelerator file - names the system, lists its cores, and defines how the cores are connected.
  2. 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.

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) or aie2 (an AMD AIE tile).
  • kind - compute, memory, shim, or offchip.

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/.