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, releaseconsumeLock - draining actor: acquire
consumeLock, releaseproduceLock
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.
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.
Windows: a link walking the whole stream¶
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);
padDimensionsmay 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
buffersandlocks, when present, havedepthentries- 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
segmentsis valid on a single-segment pool - a core endpoint's segments are contiguous
dimensionsandpadDimensionshave one entry per selected segment, with matching ranks, within segment boundspadDimensionsappears on a draining endpoint, and a nonzeropadValuerequires 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
acquireandreleasesit inside a core, andacquiretakes 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:
turnsnames the endpoint itself, and every name is a DMA endpoint on the same tile and channel, one way, listing the sameturns- an endpoint taking turns does not also dispatch, carry a header, stop after
an
iterCountor 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 (iterCounttimes 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_idfits 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_endpointwhose channel an existingaie.dma_startalready programs