Skip to content

AIE Passes

-aie-assign-bd-ids

Assign bd ids to aie.dma_bd ops.

-aie-assign-buffer-addresses

Assign memory locations for buffers in each tile

Buffers in a tile generally have an address that does not significantly matter in the design. Hence, most of the time we can instantiate aie.buffer operations without an address. This pass determines updates each aie.buffer operation without an address to have a well-defined address. This enables later passes to have a consistent view of the memory map of a system.

A core that declares data_size also gets an aie.buffer marked core_data, covering the bytes its own .data, .rodata and .bss occupy. The pass places that buffer with the rest, and AIETargetLdScript emits its extent as the data MEMORY region of the core's linker script.

Options

-placement-budget : Placements each search, ranked then exhaustive, may try per tile before it gives up. A node count rather than a time limit, so a build stays reproducible; raise it for a tile that reports hitting it.

Statistics

tiles-needing-search : Tiles where the first ranked placement did not work out
backtracks           : Placements undone and retried
budget-exhausted     : Tiles abandoned at the placement budget
tiles-compacted      : Tiles only the exhaustive search could place

Infer per-core link_files/link_merge_files from func-level link_with

Walks each aie.core and collects the external artifacts it needs by tracing direct func.call edges to func.func declarations that carry a "link_with" string attribute.

Each artifact is routed by its optional "link_with_mode" attribute:

  • no "link_with_mode" -> the CoreOp's "link_files"
  • "link_with_mode" = "merge" -> the CoreOp's "link_merge_files"

Both attributes are StrArrayAttrs and may coexist on one core. "merge" is currently the only supported mode; any other value, or a "link_with_mode" without a "link_with", is an error.

Only direct calls (func.call) are resolved. Indirect calls (func.call_indirect) inside a core body emit a warning and are not resolved; add a direct func.call to the required func.func declaration so the pass can trace the dependency.

Core-level "link_with" (deprecated) is also migrated: its value is folded into "link_files" -- it has no way to request a mode -- and the attribute is removed from the CoreOp.

func.func declarations that carry "link_with" but are never called from any core emit a warning; their artifacts will not appear in any core's list.

The pass rejects a device in which one artifact is merged by one core and object-linked by another. aiecc's unified mode compiles all cores of a device from a single llvm-linked module, so such an artifact would have its symbols defined twice.

-aie-assign-lock-ids

Assigns the lockIDs of locks that do not have IDs.

Assigns the lockIDs of locks that do not have IDs.

-aie-assign-tile-controller-ids

Assign controller id per aie.tile_op

For each aie.tile_op used in the design, assign a unique controller ID.

Options

-column-wise-unique-ids : Flag to generate controller ids only unique within each column. Otherwise globally unique.

-aie-canonicalize-device

Canonicalize Designs to include a toplevel device

This pass inserts a toplevel device operation in designs that do not have one. This allows us to support backwards compatability for older models targetting the VC1902 device without explicit device operations.

-aie-check-against-image

Check an instruction stream's static design against its image's

An instruction stream built for an image compiled elsewhere programs the resources the image allocated. Run on the stream's resolved module (its input_with_addresses.mlir), this pass compares the stream's static allocation with the image's, given as image: each shim end, DMA chain and runtime task channel, the tile holding each buffer, buffer addresses, lock ids, BD ids and switchbox connections, each under a name that does not depend on where it was put. Where they differ, it fails, listing each differing resource and what to pin to keep it stable across shapes.

Options

-image : The image's resolved module (its input_with_addresses.mlir)

-aie-core-int-range-narrowing

Narrow index loops and arithmetic in core code to 32 bits where integer range analysis proves it safe

index lowers to i64 on the cores, which have 32-bit registers: every index loop counter costs two registers and a carry, and every index add or compare a multi-instruction sequence. This pass runs the upstream arith-int-range-narrowing patterns over the body of each aie.core and each func.func in the device. Where integer range analysis proves the values fit, scf.for bounds, steps and induction variables become i32 and index arithmetic is done in i32, with casts back to index where an op needs one (such as a memref.load index). A bound read at run time fits too when its type does: an i32 loaded from one of the core's buffers and cast to index, as in IRON's range_(buf[i]).

Only provably safe rewrites are made. A loop whose bounds do not fit, such as a 0 to INT64_MAX "forever" loop, or whose bounds are unknown, keeps its index counter.

Runtime sequences are left alone. Must run before SCF-to-CF lowering, since loop bounds are only narrowed on scf.for.

Example: Before: scf.for %i = %c0 to %c32 step %c1 { %v = memref.load %buf[%i] : memref<32xi32> }

After: scf.for %i = %c0_i32 to %c32_i32 step %c1_i32 : i32 { %idx = arith.index_castui %i : i32 to index %v = memref.load %buf[%idx] : memref<32xi32> }

Options

-int-bitwidths-supported : Integer bitwidths to narrow to, tried in order

-aie-create-pathfinder-flows

Route aie.flow and aie.packetflow operations through switchboxes

Uses Pathfinder congestion-aware algorithm. Each aie.flow is replaced with aie.connect operation. Each aie.packetflow is replace with the set of aie.amsel, aie.masterset and aie.packet_rules operations.

An arbiter holds its grant until end of packet, so two slave ports sharing one arbiter serialize even on different master ports. A switchbox with more packet master ports than arbiters first turns packet hops that alone use both their ports into aie.connect, which passes the header through to the next switchbox and needs no arbiter. Where master ports still have to share, amsel allocation keeps apart the flows that can deadlock, judged from the design's locks, streams, BD lengths and runtime waits. Where a routing leaves no such allocation, the router routes the flows involved elsewhere. If flows that can deadlock pairwise must take more arbiters at some switchbox than it has, whatever the routing, or no routing keeps them apart, the pass fails and names the flows and what it assumed about them. It also fails where flows can deadlock however they are routed, as one source or receiver orders them, or where packet flows into receivers they share hold arbiters in a cycle no routing found breaks, unless allow-deadlock-prone is set, which only warns of those.

This reads buffer descriptor lengths from the DMA bodies (aie.mem, aie.memtile_dma, aie.shim_dma), so run the pass after those exist. A channel with no body here is assumed to wait on anything on its tile, and a flow whose volume is unknown to overrun its receiver.

Options

-route-circuit        : Flag to enable aie.flow lowering.
-route-packet         : Flag to enable aie.packetflow lowering.
-circuit-switch-hops  : Circuit-switch exclusive packet hops at switchboxes with more packet master ports than free arbiters.
-allow-deadlock-prone : Warn of, rather than fail on, flows that can deadlock however they are routed, and hold cycles through receivers packet flows share that no routing found avoids.

-aie-find-flows

Recover flows from switchbox configuration

Under normal circumstances, every configured aie.switchbox operation should contribute to describing an end-to-end flow from one point to another. These flows may be circuit-switched flows (represented by aie.flow) or a packet-switched connection (represensted by aie.packetflow). This pass is primarily used for testing automatic routing.

By default every configured connection is lifted, including partial flows whose source or destination is a switchbox port other than a core or DMA. Set keep-partial-flows=false to restrict recovery to flows that begin at a core/DMA and end on a core/DMA.

Options

-keep-partial-flows : Also recover flows that do not start or end at a core or DMA.
-emit-vias          : Emit each recovered flow with a via list pinning every switchbox hop, so the flow carries its own route and the router can skip it.
-remove-lifted      : Remove the interconnect configuration (connects, packet rules, mastersets, amsels, wires) that was lifted into recovered flows, leaving only configuration that could not be lifted.

-aie-generate-column-control-overlay

Spawns streaming interconnect network for column control

For each column of AIE tiles being employed in the design, spawn a network of control packet streaming interconnects which overlay on top of the design.

Options

-route-shim-to-tct       : Flag to generate TCT routing between tile CTRL and shim SOUTH ports. Available options: ['shim-only', 'all-tiles', 'disable']. 'shim-only' also routes any tile a runtime sequence task issues a token from.
-route-shim-to-tile-ctrl : Flag to generate routing between shim dma DMA and tile CTRL ports, for configuration.
-emit-standalone-overlay : Emit a standalone @ctrl_pkt_overlay device (and tag source devices) for the load-pdi-to-ctrl-pkt reconfigure flow. When false, the pass applies the overlay in place only.

-aie-hoist-vector-transfer-pointers

Hoist vector transfer pointer computations out of scf.for loops in aie.core regions

This pass optimizes vector transfer operations inside scf.for loops by hoisting pointer computations. It operates on aie.core regions within aie.device operations. When vector transfer operations have indices that depend on the loop induction variable, the pass:

  1. Flattens multi-dimensional memrefs to 1D
  2. Computes a base pointer before the loop
  3. Adds the pointer as a loop iter_arg
  4. Updates the pointer by a constant stride each iteration
  5. Replaces the transfer operations to use the iter_arg pointer

This eliminates redundant index computation and affine.apply operations within the loop body, replacing them with simple pointer arithmetic.

This pass must run before scf-to-cf conversion and before cores are outlined to functions, since it operates on scf.for loops within aie.core regions.

-aie-insert-trace-flows

Insert packet flows and runtime sequence trace setup

For each aie.trace operation, this pass: - Creates packet flows from trace ports to shim DMA - Groups traces by target shim (minimizes shim usage, ideally 1) - Inserts shim buffer descriptors and DMA control setup per shim tile - Inserts per-tile timer control register writes - Inserts per-shim broadcast and DMA control setup

All trace configuration is injected at the beginning of the runtime sequence, before user data transfer operations.

By default, multiple trace streams are routed to the same shim DMA channel and drained by a single buffer descriptor. With distribute-channels, traces are round-robin distributed across two S2MM DMA channels per shim tile, each with its own BD. Both channels share the same host buffer argument (arg_idx) and are split by offset: channel 0 starts at the base offset, channel 1 at base + buffer_size. The host must allocate 2x buffer_size for the trace buffer.

With lateral-routing, trace destinations are redirected from columns with active cores to spare shim NOC columns, reducing data path perturbation. Use lateral-target-col to force a specific target.

Options

-shim-channel        : S2MM DMA channel to use for trace (default: 1)
-default-bd-id       : Buffer descriptor ID for trace (default: 15)
-packet-id-start     : Starting packet ID for trace flows (default: 1)
-burst-length        : DMA burst length for trace transfers (default: 64 bytes)
-distribute-channels : Distribute traces across multiple S2MM channels per shim tile
-lateral-routing     : Route traces to spare columns to minimize data path perturbation
-lateral-target-col  : Force lateral routing target column (-1 = auto-detect nearest spare)

-aie-localize-locks

Convert global locks to a core-relative index

An individual lock can be referenced by 4 different AIE cores. However, each individual core accesses the lock through a different 'lock address space'. This pass converts a lock in the conceptual global address space into a local index. e.g.:

%lock = AIE.lock(%tile, 2)
AIE.core(%tile) {
  AIE.useLock(%lock, "Acquire", 1)
}
becomes
AIE.core(%tile) {
  %lockindex = arith.constant ? : index
  AIE.useLock(%lockindex, "Acquire", 1)
}

-aie-lower-cascade-flows

Lower aie.cascade_flow operations through aie.configure_cascade operations

Replace each aie.cascade_flow operation with an equivalent set of aie.configure_cascade operations.

-aie-normalize-address-spaces

Remove non-default address spaces

Early in the flow, it is convenient to represent multiple memories using different address spaces. However, after outlining code for AIE engines, the core itself only has access to a single address space. To avoid confusion, this pass normalizes any address spaces remaining in the code, converting them to the default address space.

-aie-normalize-dma-bd-dims

Strip degenerate dimensions from DMA BD descriptors.

Removes size-1 dimensions from aie.dma_bd ops whose strides never take effect. After stripping, if the remaining dimensions describe a contiguous row-major access, the BD is flattened to a single linear transfer.

-aie-objectFifo-unroll

Unroll scf.for loops that contain objectFifo operations

Unroll each scf.for loop (and its ancestor loops) inside an aie.core that contains aie.objectfifo.acquire/aie.objectfifo.release operations, using the standard scf loop unrolling infrastructure.

Each such loop is unrolled by the least common multiple of the depths of the objectFifos it accesses, so that after the objectFifo stateful transform and subsequent mem2reg/canonicalize the runtime buffer/lock bookkeeping folds into constant buffer references and constant lock counts.

This pass runs after aie-objectFifo-stateful-transform (which emits the dynamic bookkeeping and annotates every objectFifo loop with the aie.unroll_hint attribute this pass consumes). The default-dynamic option selects the lowering for cores that do not pin their own choice: when false (the default) their loops are unrolled into the static lowering, when true they are left rolled (dynamic, loop-preserving). A core overrides the default in either direction via its dynamic_objfifo_lowering attribute. Either way the hints are stripped and the runtime bookkeeping of any dynamic core is tidied by a fold/cleanup sub-pipeline.

A loop entered holding fewer objectFifo elements than it holds in steady state computes its first acquire differently from every later one, which keeps that count in a loop-carried value. peel-first-iteration peels one iteration so that the rest of the loop starts in the steady state. Peeling costs a copy of the loop body, so each peel is kept only where it turns the loop's lock values into constants.

Options

-default-dynamic      : Default objectFifo lowering for cores that do not set the dynamic_objfifo_lowering attribute: true leaves loops rolled (dynamic), false unrolls them (static).
-peel-first-iteration : Peel the first iteration off objectFifo loops whose lock values are computed at run time, keeping the peel where it makes them constant: "true", "false", or "auto" (peel unless default-dynamic is set).

-aie-objectfifo-allocate

Give pools and endpoints their buffers, locks and channels

Allocate missing pool buffers, synchronization locks and endpoint channels; lower routes to circuit/packet flows and emit shim runtime allocations. Existing buffers, locks and pinned channels stay fixed.

A route endpoint that a DMA program or runtime task names is given its channel after the objectFIFOs' endpoints, by the BDs its static chains need. Runtime tasks do not steer the choice, so the device image is the same whatever runtime sequence it is built with, and an instruction stream built for another sequence runs on it. Each aie.dma_start naming it then takes that channel's index, and each aiex.dma_configure_task_for naming it becomes an aiex.dma_configure_task on the endpoint's tile and channel.

Where these rules leave a choice (which of two endpoints takes a tile's first free channel, which pool is packed first, which route gets the next packet ID), it is settled by the design's structure: what each op is and, in turn, what it connects to. Op order and symbol names never decide, so a design declared in another order or under other names gets the same allocation.

MemTile memory checks count each fixed buffer once (including aliased tile coordinates), even without new allocations. Pool sizes use BufferOp's DataLayout byte sizing, including BFP. Planning shares the basic sequential address layout, including alignment and pinned ranges; bank constraints remain with address assignment.

Buffers may spill to accessible neighboring MemTiles. Locks must be reachable by their core and DMA users: per selected segment for counting pairs, per pool for binary locks. Generated locks prefer their nominal tile but may move to reachable tiles with capacity. DMA channel eligibility includes every accessed buffer and lock.

Preserve successful largest-first plans. On channel exhaustion, retry with selected pools' buffers and generated locks local to the accessing endpoint. If that search fails, prioritize on-home-tile DMA demand within each MemTile, choosing the largest eligible object across homes. Both orders check memory, locks and channels, with at most 64 locality combinations per distinct order.

This is bounded allocation, not a placement-time ban on spilling or a proof of infeasibility on failure; it does not undo tile-placement decisions. Before placement, each access must have a reachable physical pair compatible with known coordinates and tile types. These pairwise checks do not prove a jointly feasible placement.

Options

-packet-sw-objFifos : Flag to enable aie.packetflow lowering from objectfifos.

-aie-objectfifo-erase-pools

Drop pools that no endpoint works any more

Erase every aie.objectfifo.pool nothing refers to any more. Users may choose to skip this pass to retain the metadata about which buffers and locks belong to which lowered objectFifo.

-aie-objectfifo-liveness

Flag a specific under-buffered coupled-multicast objectFIFO class that hangs at runtime (not a general deadlock detector).

A coupled multicast (broadcast) objectFIFO whose consumers back-pressure the producer forms a dependency cycle. When the producer must re-acquire a slot on a later trip (replay, repeat_count >= 2) while the prior trip's tokens are still outstanding, the round-trip slack (2 * depth) can be smaller than the number of outstanding tokens demanded by the coupled fan-out (array_fan * trip_count). That is a static, structural deadlock: the IR compiles clean and then hangs the array at runtime.

This pass builds the objectFIFO data + back-pressure dependency graph, finds cyclic strongly-connected components (Tarjan), and applies the validated SDF model PER coupled-multicast group (grouped by name base). Each group is scoped independently so an unrelated cycle elsewhere in the device cannot inflate its demand:

demand = array_fan * T (array_fan = the group's multicast fan-out, summed across the SCCs it spans; T = max repeat_count among the fifos in those SCCs) slack = 2 * depth (depth = min depth in the group) DEADLOCK iff T >= 2 AND depth > 0 AND demand > slack

The T >= 2 replay guard avoids false positives on single-trip broadcasts. The analysis is sound for this static SDF class and conservative elsewhere (it never errors outside a proven cyclic under-buffered multicast); it does not claim to catch other deadlock classes.

-aie-objectfifo-lower-cores

Turn core acquires and releases into lock operations

Replace each core's aie.objectfifo.acquire and aie.objectfifo.release with the locks its endpoint guards and the objects at the front of its rotation, kept in rank-0 slots that each release rotates. Loops carrying accesses are annotated with the unroll factor over which those rotations fold to constants.

Consumes aie.objectfifo.core_endpoint.

-aie-objectfifo-lower-dmas

Turn DMA endpoints into buffer descriptor chains

Program each DMA endpoint's channel: for every object in its pool, for every segment it is responsible for, a buffer descriptor guarded by that segment's locks. A tile's chains are laid out by direction, then channel, so the BD ids they are later given do not follow the endpoints' op order.

Consumes aie.objectfifo.dma_endpoint.

-aie-objectfifo-split

Split objectFifos into pools and endpoints

Replace each aie.objectfifo and aie.objectfifo.link with the buffer pools its objects live in and one endpoint per actor working through them.

A fifo whose ends reach one memory module, and which asks for nothing that a DMA has to carry out, becomes a single pool with a core endpoint at each end. Reshaping the data on the way -- a layout transformation on either side, a link, a repeat count, an asymmetric consumer type -- puts it back on DMAs, as does the explicit via_DMA switch. Any other fifo becomes one pool per tile, a DMA endpoint alongside each core endpoint, and an aie.route connecting the two DMA ends. A link's participants meet on one pool, one segment per participant.

Buffers, locks and channels are left unassigned; --aie-objectfifo-allocate fills them in.

-aie-objectfifo-verify

Check that a pool and endpoint design is complete

Check the properties an op verifier cannot: that a pool's segments tile its element type, that each segment has one endpoint filling it and one draining it, that every DMA endpoint is connected by exactly one flow, and that no loop body releases more objects than it acquires.

These are completeness rules, so a design that is deliberately partial -- one whose other half is supplied elsewhere -- may decline to run this pass.

-aie-place-tiles

Place logical tiles onto physical AIE tiles

The sequential placer maps cores column-major and FIFO-connected Shim/Mem tiles nearby, allowing multiple logical tiles per physical tile. It checks DMA demand, and among tiles with the DMA channels it prefers one whose North/South stream ports still fit the merged ObjectFifos and, for a Shim tile, whose buffer descriptors still fit the runtime-sequence tasks live at once. Memory, locks and spill-dependent channel eligibility are checked by aie-objectfifo-allocate after splitting. Allocation failures do not trigger re-placement or undo merges.

Logical tiles are taken shims first, then MemTiles, then cores, and within those in an order set by the design's structure (what each tile's ops are and connect to) rather than op order or symbol names, so a design declared in another order or under other names is placed alike. Logical tiles whose flows name the same DMA channel by index never share a physical tile.

Options

-placer                  : Placement algorithm to use (default: sequential_placer)
-cores-per-col           : Limit number of cores per column (-1 = no limit). Spreads cores across columns for better trace packet routing.
-merge-logical-tiles     : When true (default), allow multiple non-core (ShimNOCTile / MemTile) aie.logical_tile ops to share one physical aie.tile if combined DMA channel demand fits. When false, each non-core aie.logical_tile is pinned to its own physical tile. CoreTile placement is unaffected.
-spread-unanchored-tiles : When true (default), a non-core aie.logical_tile with no CoreTile peers to derive a routing centroid from is ranked by current DMA load rather than by distance to column 0. Affects only designs with no core anchors (e.g. pure DMA passthrough); CoreTile placement and anchored non-core placement are unaffected.
-sa-seed                 : Random seed for SA placer (0 = non-deterministic).
-sa-effort               : Scales the SA placer's moves per temperature step and its greedy tail. Below 1.0 trades placement cost for compile time.

Statistics

sa-final-cost : SA placer cost of the placement it kept

-aie-prepare-buffers

Check buffer placement and give every buffer a name

Settles everything about a buffer that the core compilation depends on, except the buffer's address.

A buffer's name becomes a symbol in the core's object, so the name must exist before the core is lowered. This pass names every unnamed buffer and checks buffer placement, which rejects a malformed design before any core is compiled.

-aie-split-flow-vias

Split flow operations that pin via ports into segments

Each aie.flow or aie.packet_flow that carries a via list is rewritten into the disaggregated flows that meet on the via ports, plus a local flow at every via tile joining its pinned ingress and egress ports. A flow A via (B : in -> eg) to C becomes aie.flow(A, B:in), aie.flow(B:in, B:eg), and aie.flow(B:eg, C). The pass elides a segment whose source and destination coincide, which happens when the flow already begins or ends on a via port. The router assigns switchbox resources to the resulting flows.

-aie-standard-lowering

Lowering operations in AIE cores' regions to Standard

Outline code inside AIE.core operations into the llvm dialect. BufferOp operations are converted to a GlobalMemrefOp and references to those buffers are converted to GetGlobalMemrefOp. Other AIE operations inside the cores are generally lowered to appropriate function intrinsics. Other AIE operations (e.g. CoreOp, TileOp, LockOp) outside the core are removed.

Optionally, tileCol and tileRow can specify a single core to export

Options

-device  : Device to generate code for
-tilecol : X coordinate of tile to generate code for
-tilerow : Y coordinate of tile to generate code for

-aie-trace-pack-reg-writes

Pack multiple register field writes into single register writes

Packs multiple aie.trace.reg operations that write to the same register into a single aie.trace.reg operation with a combined mask and value.

This pass operates in two phases: 1. Convert field+value pairs to mask+shifted_value using register database 2. Merge multiple writes to the same register with non-overlapping masks

Example transformation:

// Before:
aie.trace.reg register="Trace_Event0" field="Trace_Event0" value=33
aie.trace.reg register="Trace_Event0" field="Trace_Event1" value=34

// After:
aie.trace.reg register="Trace_Event0" value=0x22210000 mask=0xFFFF0000

It is an error for two aie.trace.reg in the same aie.trace block to have overlapping masks for the same register.

-aie-trace-to-config

Lower high-level trace ops to register configuration ops

Converts aie.trace operations to aie.trace.config operations containing aie.trace.reg operations that specify register field writes.

This pass transforms declarative trace configurations into register-level specifications that can be further lowered to NPU register writes.

Note: This pass only does semantic lowering. NPU write generation happens in aie-inline-trace-config (AIEX dialect).

-aie-vector-to-pointer-loops

Transform vector load/store loops to use ptr dialect for explicit pointer arithmetic

This pass transforms scf.for loops containing vector.load/store operations with loop-carried index arguments to use ptr dialect operations.

The transformation makes pointer increment patterns explicit to help the LLVM backend generate efficient post-increment addressing modes (GEP fusion).

Example transformation: Before: scf.for iter_args(%idx = %0) { %v = vector.load %memref[%idx] %next = arith.addi %idx, %stride scf.yield %next }

After: %ptr = ptr.to_ptr %memref %init_ptr = ptr.ptr_add %ptr, %0 scf.for iter_args(%p = %init_ptr) { %m = ptr.from_ptr %p %v = vector.load %m[%c0] %next_p = ptr.ptr_add %p, %stride scf.yield %next_p }

-aie-vector-transfer-lowering

Lower vector.transfer_read/write to vector.load/store for AIE

This pass lowers vector.transfer_read operations to vector.load + vector.broadcast and vector.transfer_write operations to vector.store, when applicable. It's a wrapper for the upstream populateVectorTransferLoweringPatterns. TODO: Deprecate this pass once populateVectorTransferLoweringPatterns is included in convert-to-llvm.

Options

-max-transfer-rank : Maximum vector rank to lower. -1 means no limit.