Skip to content

ObjectFifo Lowering

An aie.objectfifo enables data movement at object-size granularity in a first-in-first-out manner, either between two tiles, as a broadcast, or, using aie.objectfifo.link, as join and distribute patterns. The ObjectFifo provides buffering, and users may acquire and hold multiple objects at a time (e.g., sliding windows). The current implementation provides buffering using a rotating set of aie.buffers, synchronization between producers and consumers using aie.locks, routing using aie.flow or aie.packet_flow, movement of data either using shared memory or DMA programs (aie.mem, aie.memtile_dma and aie.shim_dma), and object access on cores.

This page documents how we arrive at a concrete lowering for a given high-level ObjectFifo in multiple passes. Users requiring lower-level control may find the intermediate levels useful.

Frontend operations

Operation What it holds
aie.objectfifo A data movement pipe between producer and consumer tiles, with the capacity to buffer depth objects of the given element type.
aie.objectfifo.link A connection between two or more fifos meeting on one tile; multiple input or output fifos describe a join or distribute, respectively, each taking a slice of every object. With mode = #aie.link_mode<time> they take turns with whole objects instead: a merge (one fan-in route into one channel) or a dispatch (one channel taking turns between the outputs).
aie.objectfifo.acquire / .release A core taking objects out of a fifo and giving them back.
aie.objectfifo.allocate An (optional) manual allocation indicating on which tile the objects of the fifo should be held.
aie.objectfifo.register_external_buffers DDR buffers backing a shim end.

Operations after splitting

--aie-objectfifo-split replaces the frontend operations with these:

Operation What it holds
aie.objectfifo.pool A set of buffers and the rules for accessing them: how the buffers are sliced into segments, and which locks users must acquire/release to access the segments.
aie.objectfifo.core_endpoint Information the acquire and release operations on a core need to fill or drain the next object in a pool.
aie.objectfifo.dma_endpoint A DMA channel and a reference to a pool; lowers to a DMA program that fills or drains the buffer pool from/onto the given channel.
aie.route_endpoint One end of a stream this compiler does not program: a shim the host runtime drives, a PLIO boundary, or a core's raw stream port.
aie.route A circuit- or packet-switched connection from one draining endpoint to the filling endpoints it feeds.

These are transient. By the end of the pipeline what remains is aie.buffer, aie.lock, aie.flow, aie.mem / aie.memtile_dma / aie.shim_dma, and aie.shim_dma_allocation.

Pools

A pool is a set of buffers and the contract for accessing them.

aie.objectfifo.pool @P(%tile) {
  depth   = 2 : i32,
  buffers = [@P_buff_0, @P_buff_1]
} : memref<36xi32> {
  aie.objectfifo.segment @s0 {offset = 0 : i32,  size = 16 : i32, produceLock = @p0, consumeLock = @c0}
  aie.objectfifo.segment @s1 {offset = 16 : i32, size = 20 : i32, produceLock = @p1, consumeLock = @c1}
}

The tile is where the buffers live and depth is how many of them rotate.

Buffers and segments are different axes. Buffers are the rotation axis: depth objects taking turns. Segments partition a single object, and every buffer carries every segment. Segments are listed in increasing offset order, do not overlap, and together cover the entire buffer.

An ordinary pool has one segment spanning the whole object. A joined or distributed pool has one per participant.

On devices with binary locks (AIE1) a pool carries locks, one per buffer, and has a single implicit segment. Join/distribute is not supported on AIE1.

The access contract

On AIE2 (semaphore locks), users of a pool are expected to acquire locks as follows before reading from or writing to buffers in the pool:

  • filling actor: acquire produceLock, release consumeLock
  • draining actor: acquire consumeLock, release produceLock

With semaphore locks the value counts how many objects are claimed.

AIE1 uses binary locks, which are supplied in the locks attribute. Producers and consumers toggle the same lock with opposite values (0/1).

Replication and iteration

repeatCount sits on the pool the data leaves from. It makes that pool's draining end put each object on the stream that many times, and the filling end covers a whole batch in one go: it takes and gives back repeatCount lock units at a time, with the lock initializers scaled to match.

iterCount sits on each DMA endpoint and says how many passes its chain makes. Both ends of a fifo carry depth * repeat_count * iter_count objects, so --aie-objectfifo-split resolves the fifo's iter_count differently for each:

endpoint descriptors in its chain passes it makes
draining depth * repeat_count iter_count
filling depth repeat_count * iter_count

A chain given an iterCount ends in aie.end and carries iterCount - 1 on its channel start queue. A chain without one loops back on itself and runs until the design stops. Locks gate every descriptor either way, so a bounded chain simply stops asking once it has made its passes.

Endpoints

An endpoint is an actor: something that fills or drains a pool.

Endpoints lower to code (DMA program or core code) that accesses the buffers in the referenced pool in sequence, following its expected synchronization protocol (lock acquire/releases).

aie.objectfifo.core_endpoint @c(%tile) fills  @P
aie.objectfifo.dma_endpoint  @d(%tile) drains @P {channelIndex = 0 : i32}

Every segment has exactly one filler and exactly one drainer. A pool with several fillers is a join; one with several drainers is a distribute.

Selecting segments

segments picks which of the pool's segments an actor handles. Indices are strictly increasing. Omitting the attribute selects the pool's only segment, and is invalid on a multi-segment pool.

aie.objectfifo.dma_endpoint @d(%tile) fills @P {segments = [@s0, @s1]}
// Results in a DMA program that accesses the first two segments of buffers in @P.

Per-segment transforms

A DMA endpoint may carry dimensions (a strided access pattern) and padDimensions. Both are positional: entry N describes the segment named by segments[N], and each must have exactly as many entries as segments. An empty inner array means no transform for that segment.

aie.objectfifo.dma_endpoint @d(%tile) drains @P {
  segments   = array<i32: 0, 1>,
  dimensions = #aie<bd_dim_layout_array_array[
    [<size = 4, stride = 4>, <size = 4, stride = 1>],   // segment 0
    []]>                                                // segment 1, linear
}

Padding applies on a draining endpoint, where data goes out on the stream.

Route endpoints

The route_endpoint is an atypical endpoint that does not name a pool: It is an escape hatch for endpoints that do not want to use the pool abstraction, as a stand-in for the other end of an aie.route. It names only a DMA channel on a tile, which a user can then program in other ways. Its direction comes from the side of the route it is named on.

aie.route_endpoint @in(%shim) DMA  {fifoName = "of_in"}
aie.route_endpoint @s(%tile)  Core {channelIndex = 0 : i32}

Routes

Routes name endpoints and lower to a circuit- or packet-switched connection between them.

aie.route from @d1 to [@d2, @d3]
aie.route from [@d1, @d4] to [@d2] {packet = #aie.packet_info<>}

Several destinations are a broadcast: one source channel feeding a multicast route. Several sources are a fan-in: their packets share the destination channel, which fills its next object with whichever arrives, so the destination's pool is a queue with several producers and no memory of which. A fan-in is packet-switched, since a circuit joins one port to its masters, and every end moves one object of the same size, since a packet is one pass of its sender's chain and a shorter one would leave two senders in one object. The one header is stamped by every source and the destination's chain is the ordinary ring. A consumer that must know the source is a different design: one route per source into its own pool. An endpoint is named by one route, except a dispatching one.

A draining DMA endpoint with dispatch takes turns between destinations over one channel: its k-th object goes to dispatch[k mod n]. It is the source of one packet-switched route per listed destination, each carrying its own header, and allocation records the headers in dispatchPackets. The chain runs lcm(depth, n) objects, so every object meets every turn before it repeats.

aie.objectfifo.dma_endpoint @hub(%mem) drains @y {dispatch = [@a_in, @b_in]}
aie.route from @hub to [@a_in] {packet = #aie.packet_info<>}
aie.route from @hub to [@b_in] {packet = #aie.packet_info<>}

DMA endpoints with turns share one channel the other way around: its k-th object belongs to the endpoint turns[k mod n], each endpoint keeping its own pool and locks. Every endpoint named lists the same turns, on one tile, one way. The order tells their objects apart, so one circuit route carries them all, and the endpoints at its other end take the same turns. Allocation writes turns itself when a tile runs out of channels (below).

aie.objectfifo.dma_endpoint @b_out(%mem) drains @mb {turns = [@b_out, @c_out]}
aie.objectfifo.dma_endpoint @c_out(%mem) drains @mc {turns = [@b_out, @c_out]}
aie.objectfifo.dma_endpoint @b_in(%core) fills @b {turns = [@b_in, @c_in]}
aie.objectfifo.dma_endpoint @c_in(%core) fills @c {turns = [@b_in, @c_in]}
aie.route from @b_out to [@b_in]
aie.route from @c_out to [@c_in]

A flow carrying a packet header becomes an aie.packet_flow and shares the stream; circuit flows reserve theirs. Both kinds coexist in one device. The header is the same #aie.packet_info the endpoints and buffer descriptors carry after allocation, only with its id still open: pkt_id pins the id, otherwise --aie-assign-packet-ids picks the lowest id nothing on the device holds, another packet flow, a pinned route or a trace packet, and fills it in.

aie.route from @d1 to [@d2] {packet = #aie.packet_info<>}
aie.route from @d1 to [@d2] {packet = #aie.packet_info<pkt_id = 7>}

At the frontend this is chosen per fifo through its transport, transport = #aie.transport<dma, packet = #aie.packet_info<pkt_id = 7>> on aie.objectfifo or ObjectFifo(..., transport=Transport.dma(packet=Packet(id=7))) in IRON. The same header rides on the route unchanged.

Examples

Core-to-Core FIFO via DMAs

aie.objectfifo.pool @producer_pool(%t02) ...
aie.objectfifo.core_endpoint @prod_core(%t02) fills  @producer_pool
aie.objectfifo.dma_endpoint  @prod_dma(%t02)  drains @producer_pool

aie.objectfifo.pool @consumer_pool(%t25) ...
aie.objectfifo.dma_endpoint  @cons_dma(%t25)  fills  @consumer_pool
aie.objectfifo.core_endpoint @cons_core(%t25) drains @consumer_pool

aie.route from @prod_dma to [@cons_dma]

Two pools, one per tile, joined by a stream.

Core-to-Core FIFO via Shared Memory

A fifo used within one core, or across neighboring cores that share memory, is one pool with two core endpoints:

aie.objectfifo.pool @smem(%t02) {depth = 4 : i32,
    } : memref<16xi32> {
      aie.objectfifo.segment @s0 {offset = 0 : i32, size = 16 : i32}
    }
aie.objectfifo.core_endpoint @prod(%t02) fills  @smem
aie.objectfifo.core_endpoint @cons(%t03) drains @smem

Buffered Pass-through via DMA

A one-to-one aie.objectfifo.link is a single pool with a DMA filling it and another draining it:

aie.objectfifo.pool @link_pool(%t21) ...
aie.objectfifo.dma_endpoint @link_in(%t21)  fills  @link_pool
aie.objectfifo.dma_endpoint @link_out(%t21) drains @link_pool

The two ends may move different amounts per transfer, which sets the stream granularity. The pool keeps one segment.

2:1 Join via DMA

aie.objectfifo.pool @out_pool(%t21) {
  depth = 2 : i32,
  } : memref<48xi32> {
  aie.objectfifo.segment @s0 {offset = 0 : i32,  size = 16 : i32}
  aie.objectfifo.segment @s1 {offset = 16 : i32, size = 32 : i32}
}

aie.objectfifo.dma_endpoint @fill_0(%t21)    fills  @out_pool {segments = [@s0]}
aie.objectfifo.dma_endpoint @fill_1(%t21)    fills  @out_pool {segments = [@s1]}
aie.objectfifo.dma_endpoint @drain_all(%t21) drains @out_pool {segments = [@s0, @s1]}

Two DMAs fill different segments of one pool, each ordered by its own locks, and one DMA drains the whole object, through one BD per segment with that segment's locks. Distribute is the same picture reversed: one endpoint fills the whole object and N endpoints each drain one segment.

A 1:1 link at a MemTile may cut overlapping windows out of its input stream. The output fifo's dimensionsToStream then walks the whole stream of input objects instead of one object (IRON: forward(to_stream=...), see Section 2c):

  • the outer dimension enumerates the outputs and steps whole input objects; its stride over the input object's size is how far each window advances;
  • the inner dimensions are one output's transfer, as many elements as the output object, and at most one of them steps by exactly one input object (the window's rows);
  • padDimensions may pad only the outer dimension, where (before, after) counts zero objects ahead of and behind the stream; walk coordinates start at the first zero object.

Nothing else is stated: the link infers the number of outputs, the advance, the span (input objects each output reads) and how many objects the stream holds, (outputs - 1) * advance + span - before - after. A walk that stays inside one object is the ordinary rate-converting link above.

// Three-row windows over four rows of 8, a zero row above and below.
aie.objectfifo @rows(%shim, {%mem}, 2 : i32) : !aie.objectfifo<memref<8xi32>>
aie.objectfifo @windows(%mem dimensionsToStream [<size = 4, stride = 8>,
    <size = 3, stride = 8>, <size = 8, stride = 1>], {%core}, 2 : i32)
    {padDimensions = #aie<bd_pad_layout_array[<const_pad_before = 1, const_pad_after = 1>,
        <const_pad_before = 0, const_pad_after = 0>, <const_pad_before = 0, const_pad_after = 0>]>}
    : !aie.objectfifo<memref<24xi32>>
aie.objectfifo.link [@rows] -> [@windows] ([] [])

Split gives the link one pool holding the whole stream in a single buffer, its zero objects included (initialized to zero, so they cost configuration bytes), with one lock pair counting objects; the fifos' depths do not apply. Lower-dmas fills it with one descriptor iterating over the stream's objects, and drains it with one descriptor per output at offset advance * j, each acquiring the input objects that output is the first to read and releasing those no later output reads (link_stream_walk.mlir shows the chain). The zero rows are stored rather than produced by BD padding because a four-dim MemTile walk cannot pad its d2 or d3 dimension.

Limits of this lowering: a stream holds at most as many objects as a lock counts (63), it yields at most one MemTile channel's descriptors (24) per pass, the link point is a MemTile, and the output neither repeats objects nor has an iteration count.

The pipeline

--aie-objectFifo-stateful-transform runs these in order. Its options: skip-verify=true drops step 2, and packet-sw-objFifos=true makes step 3 give every flow a packet header, whether or not it asked for one.

Pass Emits Consumes
--aie-objectfifo-split pools, endpoints, flows aie.objectfifo, aie.objectfifo.link
--aie-objectfifo-verify diagnostics only —
--aie-assign-packet-ids pkt_id on every packet header aie.route headers with no id
--aie-objectfifo-allocate pools with buffer/lock attributes, aie.buffer, aie.lock, channel indices, aie.flow / aie.packet_flow, aie.shim_dma_allocation aie.route, pools without buffer/lock attributes
--aie-objectfifo-lower-dmas BD chains dma_endpoint, route_endpoint
--aie-objectfifo-lower-cores use_lock and buffer selection core_endpoint, acquire, release
--aie-objectfifo-erase-pools — unreferenced pools

Each pass replaces the operations it consumes, so the pipeline is idempotent: lower, edit the result, and re-run. Adding a new aie.objectfifo to lowered IR and re-running lowers the new one and leaves the rest alone.

1. --aie-objectfifo-split

Splits aie.objectfifo and aie.objectfifo.link into pools, endpoints and flows, deciding which tiles hold objects and which ends need DMAs.

// before
aie.objectfifo @of1 (%tile02, {%tile25}, 2 : i32) : !aie.objectfifo<memref<16xi32>>
%e = aie.objectfifo.acquire @of1 (Produce, 1) : memref<16xi32>

// after
aie.objectfifo.pool @of1_pool(%tile_0_2) {depth = 2 : i32, fifoName = "of1",
    } : memref<16xi32> {
      aie.objectfifo.segment @s0 {offset = 0 : i32, size = 16 : i32}
    }
aie.objectfifo.core_endpoint @of1_prod(%tile_0_2) fills @of1_pool
aie.objectfifo.dma_endpoint @of1_prod_dma(%tile_0_2) drains @of1_pool {fifoName = "of1"}
aie.objectfifo.pool @of1_cons_pool(%tile_2_5) {depth = 2 : i32, fifoName = "of1",
    } : memref<16xi32> {
      aie.objectfifo.segment @s0 {offset = 0 : i32, size = 16 : i32}
    }
aie.objectfifo.core_endpoint @of1_cons(%tile_2_5) drains @of1_cons_pool
aie.objectfifo.dma_endpoint @of1_cons_dma(%tile_2_5) fills @of1_cons_pool {fifoName = "of1"}
aie.route from @of1_prod_dma to [@of1_cons_dma]

%e = aie.objectfifo.acquire @of1_prod (1) : memref<16xi32>

A core's acquire and release name the endpoint they work through; the endpoint's role gives the direction.

Pool depth covers the largest acquire a core on that tile makes, plus one so the core can hold an object while the next arrives. A tile no core touches gets the fifo's declared size.

The fifo's transport names the path. Absent (auto), split uses shared memory when both ends reach one memory module and nothing forces DMAs. #aie.transport<dma> forces the DMAs, and #aie.transport<shared_mem> fails split with the reason when shared memory cannot be had (the ends share no memory module, an end reshapes the data, repeat_count, a link, and so on).

Each end names what drives it, prod_port and one cons_ports entry per consumer. #aie.end_port<DMA : 1> pins that end's DMA endpoint to channel 1. #aie.end_port<Core : 0> wires the end to the core's stream port 0: it becomes an aie.route_endpoint with a Core bundle and holds no objects, so a core cannot acquire or release on it.

2. --aie-objectfifo-verify

Checks the completeness rules listed below. Designs that supply an endpoint by hand at BD level lower with skip-verify=true.

3. --aie-assign-packet-ids

Fills in the pkt_id of every route whose packet header left it open, taking the lowest id nothing on the device holds: another aie.packet_flow, a route or objectfifo that pinned one, or a trace packet. Routes are taken in structural order, so the assignment is a function of the design alone, whatever order or names it was declared with. A DMA program sourcing a route whose id is still open is an error: it states its BDs' headers itself. This is the one place ids are handed out; --aie-insert-trace-flows reads the same held set when it numbers traces, so the two never alias.

// before
aie.route from @d1 to [@d2] {packet = #aie.packet_info<>}
// after
aie.route from @d1 to [@d2] {packet = #aie.packet_info<pkt_id = 3>}

4. --aie-objectfifo-allocate

Gives pools their buffers and locks, and endpoints their channels, flows and shim allocations. A packet header reaching it has to carry its id.

%of1_buff_0 = aie.buffer(%tile_0_2) {sym_name = "of1_buff_0"} : memref<16xi32>
%of1_buff_1 = aie.buffer(%tile_0_2) {sym_name = "of1_buff_1"} : memref<16xi32>
%of1_prod_lock_0 = aie.lock(%tile_0_2) {init = 2 : i32, sym_name = "of1_prod_lock_0"}
%of1_cons_lock_0 = aie.lock(%tile_0_2) {init = 0 : i32, sym_name = "of1_cons_lock_0"}

aie.objectfifo.pool @of1_pool(%tile_0_2) {
    depth = 2 : i32, fifoName = "of1",
    buffers = [@of1_buff_0, @of1_buff_1]
} : memref<16xi32> {
    aie.objectfifo.segment @s0 {offset = 0 : i32, size = 16 : i32,
        produceLock = @of1_prod_lock_0, consumeLock = @of1_cons_lock_0}
}
// Note the set buffer and lock attributes.
aie.objectfifo.dma_endpoint @of1_prod_dma(%tile_0_2) drains @of1_pool {channelIndex = 0 : i32, fifoName = "of1"}
aie.flow(%tile_0_2, DMA : 0, %tile_2_5, DMA : 0)

Attributes already present are kept, so writing aie.buffer ops and naming them in the pool, attaching locks to a segment, or setting channelIndex on an endpoint overrides that choice; this pass fills in the rest.

When a shim tile runs out of channels, shim ends that are never in flight together take turns on one channel instead. Ends that different runtime sequences use never are: one dispatch runs one sequence and is taken to finish its transfers before the next starts, the same contract that lets one design be dispatched twice in a row. Within one sequence, a receiving (S2MM) end's transfers must be awaited or freed (aiex.dma_free_task) before the other's first is issued, and an end used inside a loop counts as in flight for the whole loop. A sending (MM2S) end stays in flight to the end of its sequence: a shim MM2S task reports itself complete once the shim has read its data, up to about 1580 words before they have entered the stream (measured on npu2), so awaiting it proves nothing about a later transfer queued behind it. An end no runtime sequence names is never shared. Only as many ends share as the tile needs. The routes of ends sharing a channel become packet-switched, with ids that differ from one another; other shared channels reuse the same ids, as the router keeps flows with one id apart wherever their destinations differ. Each end keeps its own aie.shim_dma_allocation, so every transfer stamps its own header. A design that fits without sharing is lowered exactly as before.

When a compute or mem tile runs out of channels, DMA endpoints whose objects one core needs in a fixed order take turns on one channel instead, in that order. The core's period is the innermost loop around every acquire and release on their pools, with the loops of static trip count inside it unrolled; each acquire needs the objects it does not already hold, and the order of a draining core's needs (or of a filling core's releases) once its holdings settle is turns. An end used under a branch, or in a loop of unknown trip count inside the period, has no fixed order and never shares. Each end must be one circuit route from another DMA endpoint, and those endpoints, on one tile, take the same turns. The turns are taken only if a chain moving objects in their order forever, through the pools' depths, never leaves the channel and the core both waiting: a window over one pool beside a one-deep pool would, as the chain's next object of the second waits for a slot the core frees only after it has the window's last. Only as many ends share as the tile needs, and a design that fits without sharing is lowered exactly as before.

--aie-place-tiles counts shim channels by the same rule (ShimTransferSpans): logical shim tiles are placed one channel per end as before, and only when no shim tile has room does it count the ends that can take turns once, so a pack whose members together need more shim channels than the device has can still be placed.

5. --aie-objectfifo-lower-dmas

Turns each dma_endpoint into the BD chain that walks its pool's buffers, buffer-major and segment-minor:

for each buffer b in pool.buffers:
  for each segment s selected by the endpoint:
    acquire( drains ? s.consumeLock : s.produceLock )
    dma_bd(b, s.offset, s.size)
    release( drains ? s.produceLock : s.consumeLock )

Endpoints taking turns lower to one chain: the k-th BD moves the next object of the endpoint turns names k-th, through its own pool's locks, unrolled until every endpoint is back at its first object.

%mem_0_2 = aie.mem(%tile_0_2) {
  %0 = aie.dma_start(MM2S, 0, ^bb1, ^bb3)
^bb1:
  aie.use_lock(%of1_cons_lock_0, AcquireGreaterEqual, %c1_i32)
  aie.dma_bd(%of1_buff_0 : memref<16xi32> offset = 0 len = 16)
  aie.use_lock(%of1_prod_lock_0, Release, %c1_i32_0)
  aie.next_bd ^bb2
^bb2:   // the same, with %of1_buff_1
  ...

6. --aie-objectfifo-lower-cores

Turns acquire and release operations in AIE core code into use_lock and a rotation of the pool's objects. The core keeps its objects in rotation order in rank-0 slots: an acquire reads the front ones, and a release rotates the released objects to the back.

%0 = arith.subi %c1_i32, %c0_i32 : i32   // acquire(N) is absolute:
%1 = arith.maxsi %0, %c0_i32_0 : i32     // delta = max(N - held, 0)
aie.use_lock(%of1_prod_lock_0, AcquireGreaterEqual, %1)
%obj = memref.load %front[] : memref<memref<16xi32>>
...
aie.use_lock(%of1_cons_lock_0, Release, %c1_i32)
%a = memref.load %front[] : memref<memref<16xi32>>   // release(1) rotates
%b = memref.load %back[] : memref<memref<16xi32>>    // the objects by one
memref.store %b, %front[] : memref<memref<16xi32>>
memref.store %a, %back[] : memref<memref<16xi32>>

--aie-objectFifo-unroll promotes the slots to SSA wherever each object then folds to a concrete buffer: on the cores it unrolls, and on dynamic cores that reach the slots only from straight-line code. A core whose objects would still be picked at run time, such as a dynamic core's loop or an unrolled loop whose trip count is not a multiple of the depth, keeps them in memory instead, where LLVM's SROA turns them into loop-carried buffer pointers. A pool with a single object hands that object out directly. AIE1 binary locks travel with their objects, so they are still selected by a rotating index.

7. --aie-objectfifo-erase-pools

Drops pool metadata once nothing refers to it. A pool still named by something else, such as a re-arm binding in a runtime sequence, stays.

What is checked

By the op verifiers

Pools:

  • segments are in increasing offset order, and there is at least one
  • buffers and locks, when present, have depth entries
  • the lock kind matches the device: binary locks in locks, counting locks on segments
  • more than one segment requires semaphore locks
  • segments tile the object: increasing offsets, no gaps or overlaps, covering it exactly

Endpoints:

  • the named pool exists
  • segment names resolve inside the pool and are in increasing offset order, and there is at least one
  • omitting segments is valid on a single-segment pool
  • a core endpoint's segments are contiguous
  • dimensions and padDimensions have one entry per selected segment, with matching ranks, within segment bounds
  • padDimensions appears on a draining endpoint, and a nonzero padValue requires one
  • a MemTile walk of four dimensions pads neither d2 nor d3, which the hardware ignores
  • a route endpoint with a DMA or PLIO bundle is on a shim tile, and its bundle is DMA, PLIO or Core

Flows and core accesses:

  • a flow has at least one destination, and its source and destinations are endpoints
  • acquire and release sit inside a core, and acquire takes at least one object of the pool's element type

Links walking the whole stream (windows):

  • the outer stride is a whole number of input objects, and the inner dimensions move exactly one output object
  • at most one inner dimension steps by exactly one input object
  • padding is on the outer dimension only, and every output reads some data
  • the link point is a MemTile, and the output has no repeat or iteration count
  • the stream's objects fit one lock's count (63), and its outputs one MemTile channel's descriptors (24)

Endpoints taking turns:

  • turns names the endpoint itself, and every name is a DMA endpoint on the same tile and channel, one way, listing the same turns
  • an endpoint taking turns does not also dispatch, carry a header, stop after an iterCount or repeat its objects

By --aie-objectfifo-verify

  • each segment has one filling and one draining endpoint. A filler may be absent when lock initializers mark the objects as starting full, and a pool over external buffers has a host-side actor with no op
  • every DMA and route endpoint appears in exactly one route, except that a dispatching endpoint is the source of one packet-switched route per destination it lists, and of nothing else
  • no loop body releases more than it acquires
  • no core must release more objects through an endpoint than the pool's DMA endpoint, bounded by an iterCount, will move (iterCount times depth). Only releases forced by static loop bounds count, and a loop meant to run forever (a trip count from the 24-bit BD-loop saturation value up) is left out, since its core stalling once the data stops is how such a design ends
  • every end of a fan-in route moves one object of the same size, counting a DMA endpoint's selected segments through its dimensions

By --aie-assign-packet-ids

  • a pinned pkt_id fits the target's header field, and the field has an id left for every open header

By --aie-objectfifo-allocate

  • every packet header on a route carries its id

By --aie-objectfifo-lower-dmas

  • a dma_endpoint whose channel an existing aie.dma_start already programs