MLIR-AIE 1.4.5.dev
BdLowering.h
Go to the documentation of this file.
1//===- BdLowering.h ---------------------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7//
8// Shared BD (buffer-descriptor) size/stride encoding used by both the static
9// (constant-folded) and dynamic (runtime SSA) DMA lowering paths.
10//
11// The hardware size/stride computation lives here ONCE as a policy-templated
12// algorithm (encodeHardwareStridesWraps) so the constant path and the dynamic
13// arith-emitting path cannot drift -- a divergence would silently miscompile a
14// descriptor. The constant path instantiates it with ConstStridePolicy (plain
15// integer math); the dynamic path uses SsaStridePolicy (emits arith ops).
16//
17// The same reasoning extends across TILE TYPES: the register bit layout, which
18// genuinely differs between shim NOC, mem tile and core tile BDs, comes from
19// AIETargetModel::getDmaBdLayout, and both this packer and AIEDmaToNpu.cpp's
20// WriteBdToBlockWritePattern place fields through it.
21//
22//===----------------------------------------------------------------------===//
23
24#ifndef AIE_DIALECT_AIEX_UTILS_BDLOWERING_H
25#define AIE_DIALECT_AIEX_UTILS_BDLOWERING_H
26
27#include "mlir/Dialect/Utils/StaticValueUtils.h"
28#include "mlir/IR/Builders.h"
29#include "mlir/IR/BuiltinTypes.h"
30#include "mlir/IR/Operation.h"
31#include "mlir/IR/Value.h"
32#include "mlir/Support/LLVM.h"
33#include "llvm/ADT/ArrayRef.h"
34#include "llvm/ADT/Twine.h"
35
36#include <cstdint>
37#include <numeric>
38#include <tuple>
39
40namespace xilinx::AIE {
41class AIETargetModel;
42} // namespace xilinx::AIE
43
44namespace xilinx::AIEX {
45
46// The address generator transfers whole granules only, so a size or stride is
47// realizable iff its byte extent (value * elemWidth) is a granule multiple.
48// Dividing through by gcd(elemWidth, gran), that is `value % divisor == 0` with
49// divisor = gran / gcd(elemWidth, gran) -- the element-count multiple a size or
50// stride must land on (e.g. int8 against a 32-bit granule => divisor 4). Shared
51// so the static verifier, the dynamic verifier, and the runtime guard apply one
52// definition.
53//
54// These realizability predicates are header-inline (no AIEX-op dependencies) so
55// the AIEX dialect verifier can call them without the AIEX IR library depending
56// on AIEXUtils -- which would close a link cycle (AIEXUtils already uses AIEX
57// ops).
58inline int64_t bdGranuleDivisor(uint64_t elemWidth,
59 uint32_t addressGranularity) {
60 return addressGranularity / std::gcd(elemWidth, (uint64_t)addressGranularity);
61}
62
63// Whether a constant element count is realizable: value * elemWidth is a whole
64// number of granules. Equivalent to value % bdGranuleDivisor(...) == 0.
65inline bool isConstMultipleOfGranule(int64_t value, uint64_t elemWidth,
66 uint32_t addressGranularity) {
67 return value * (int64_t)elemWidth % (int64_t)addressGranularity == 0;
68}
69
70// Check the constant size/stride operands (innermost-first) of a shim-NOC BD
71// for realizability: d0 size and every non-unit stride must be a whole number
72// of granules (a unit innermost stride is the exempt contiguous case), and a
73// stride must be positive where its size > 1. Runtime operands are skipped
74// (buildBdWords guards them on the host). Shared by both dynamic paths;
75// emits a diagnostic on `op` and fails on the first violation.
76inline mlir::LogicalResult
77verifyConstBdRealizability(mlir::Operation *op,
78 llvm::ArrayRef<mlir::OpFoldResult> sizes,
79 llvm::ArrayRef<mlir::OpFoldResult> strides,
80 uint64_t elemWidth, uint32_t gran) {
81 std::optional<int64_t> d0Size =
82 sizes.empty() ? std::nullopt : mlir::getConstantIntValue(sizes[0]);
83 if (d0Size && !isConstMultipleOfGranule(*d0Size, elemWidth, gran))
84 return op->emitOpError("d0 size ")
85 << *d0Size << " elements at " << (elemWidth / 8)
86 << " bytes each is not a multiple of the " << (gran / 8)
87 << "-byte address-gen granule.";
88 for (int i = 0; i < (int)strides.size(); i++) {
89 auto s = mlir::getConstantIntValue(strides[i]);
90 if (!s)
91 continue;
92 // A unit innermost stride is the contiguous case: successive elements are
93 // packed with no gap, so the transfer is dense and the stride need not land
94 // on a granule boundary. Every other stride addresses a strided access and
95 // must be granule-aligned. A non-word element's innermost stride must be
96 // 1 (see verifyStridesWraps).
97 if (i == 0 && *s == 1)
98 continue;
99 if (!isConstMultipleOfGranule(*s, elemWidth, gran))
100 return op->emitOpError("stride ")
101 << i << " is " << *s << " elements at " << (elemWidth / 8)
102 << " bytes each, not a multiple of the " << (gran / 8)
103 << "-byte address-gen granule.";
104 if (i == 0 && elemWidth != gran && !(d0Size && *d0Size <= 1))
105 return op->emitOpError("stride 0 is ")
106 << *s << " elements, but must be 1 for " << (elemWidth / 8)
107 << "-byte elements: the DMA moves whole " << (gran / 8)
108 << "-byte granules.";
109 }
110 // A stride must be positive where its size > 1 (it is never applied when
111 // size == 1). The d3 iteration dimension is the exception: a zero stride
112 // there is the pure-repeat case (the BD wraps every iteration, repeat carried
113 // by the queue push), matching verifyStridesWraps' dim-3 `< 0` rule. Lists
114 // are innermost-first, so d3 is index 3 (present only for a full 4D
115 // descriptor).
116 //
117 // A RUNTIME size does not excuse the stride: the encoder would scale a zero
118 // stride to -1, an all-ones step field, so such a stride is only legal when
119 // the size is a compile-time 1, the one case where it is never applied. It
120 // is rejected here rather than by a host-side check at every dispatch.
121 constexpr int kIterDim = 3;
122 for (int i = 0; i < (int)sizes.size() && i < (int)strides.size(); i++) {
123 auto sz = mlir::getConstantIntValue(sizes[i]);
124 auto st = mlir::getConstantIntValue(strides[i]);
125 if (!st || (sz && *sz <= 1))
126 continue;
127 if (i == kIterDim ? *st < 0 : *st < 1)
128 return op->emitOpError("stride ")
129 << i
130 << (i == kIterDim
131 ? " must be non-negative unless its size is statically 1."
132 : " must be positive unless its size is statically 1.");
133 }
134 return mlir::success();
135}
136
137// Shared hardware size/stride encoder. The BD encodes each dimension's wrap
138// ("size") scaled to address-gen granules and step ("stride") biased by -1 (a
139// stored 0 means one granule). One algorithm drives both lowerings via a
140// Policy: ConstStridePolicy (int64 math) for the static path, SsaStridePolicy
141// (arith ops) for runtime operands -- keeping them bit-identical. The Policy
142// supplies cst/mul/div/sub and selectGT1/selectGT0/selectLt. Inputs/outputs are
143// 4-element arrays in innermost-first order [d0, d1, d2, d3/iter].
144template <typename Policy>
145void encodeHardwareStridesWraps(Policy &p, uint64_t elemWidth,
146 uint32_t addressGranularity,
147 typename Policy::V inputSizes[4],
148 typename Policy::V inputStrides[4],
149 typename Policy::V sizes[4],
150 typename Policy::V strides[4]) {
151 using V = typename Policy::V;
152 // Scale an element-count stride into hardware granules and apply the -1 bias:
153 // stride * elemWidth / addressGranularity - 1
154 auto biasedStride = [&](V inStride) -> V {
155 return p.sub(p.div(p.mul(inStride, elemWidth), addressGranularity), 1);
156 };
157
158 // d0_size, d0_stride
159 sizes[0] = p.div(p.mul(inputSizes[0], elemWidth), addressGranularity);
160 // d0_stride collapses to hardware 0 for a sub-granule stride (byte extent <
161 // one granule, i.e. the contiguous unit-stride case) or a wide element; else
162 // it is the biased stride. The wide-element test is compile-time; the
163 // sub-granule test is a policy select, so the stride may be runtime. A
164 // non-unit stride over elements narrower or wider than a granule is
165 // unrealizable and rejected/guarded elsewhere.
166 if (elemWidth > addressGranularity) {
167 strides[0] = p.cst(0);
168 } else {
169 strides[0] = p.selectLt(p.mul(inputStrides[0], elemWidth),
170 p.cst((int64_t)addressGranularity), p.cst(0),
171 biasedStride(inputStrides[0]));
172 }
173
174 // d1_size, d1_stride / d2_size, d2_stride: stride only matters when size > 1.
175 sizes[1] = inputSizes[1];
176 strides[1] =
177 p.selectGT1(inputSizes[1], biasedStride(inputStrides[1]), p.cst(0));
178 sizes[2] = inputSizes[2];
179 strides[2] =
180 p.selectGT1(inputSizes[2], biasedStride(inputStrides[2]), p.cst(0));
181
182 // iteration_size, iteration_stride. Size is stored biased by -1. The stride
183 // must be positive like the others, but a zero-stride "repeat" is encoded by
184 // leaving size at 1 (via a positive repeat_count on the queue push) so the BD
185 // wraps every iteration and never adds the stride. Hence stride is gated on
186 // BOTH size > 1 and inStride > 0.
187 sizes[3] = p.selectGT1(inputSizes[3], p.sub(inputSizes[3], 1), p.cst(0));
188 strides[3] = p.selectGT1(
189 inputSizes[3],
190 p.selectGT0(inputStrides[3], biasedStride(inputStrides[3]), p.cst(0)),
191 p.cst(0));
192}
193
194// Constant (compile-time int64) policy: plain integer arithmetic.
196 using V = int64_t;
197 static V cst(int64_t c) { return c; }
198 static V mul(V v, int64_t c) { return v * c; }
199 static V div(V v, int64_t c) { return v / c; }
200 static V sub(V v, int64_t c) { return v - c; }
201 static V selectGT1(V cond, V t, V e) { return cond > 1 ? t : e; }
202 static V selectGT0(V cond, V t, V e) { return cond > 0 ? t : e; }
203 static V selectLt(V a, V b, V t, V e) { return a < b ? t : e; }
204};
205
206// SSA (runtime arith) policy: emits i64 arith ops mirroring ConstStridePolicy.
207// Every primitive builds an arith op at the policy's insertion point, so the
208// innermost stride may be a runtime value like any other dimension.
210 using V = mlir::Value;
211 mlir::OpBuilder &builder;
212 mlir::Location loc;
213
214 SsaStridePolicy(mlir::OpBuilder &b, mlir::Location l) : builder(b), loc(l) {}
215
216 V cst(int64_t c) const;
217 V mul(V v, int64_t c) const;
218 V div(V v, int64_t c) const;
219 V sub(V v, int64_t c) const;
220 V selectGT1(V cond, V t, V e) const;
221 V selectGT0(V cond, V t, V e) const;
222 V selectLt(V a, V b, V t, V e) const;
223};
224
225// Require `cond` to hold when the runtime sequence runs. A condition that
226// folds to true emits nothing and one that folds to false is a compile-time
227// error at `loc`; otherwise a cf.assert carries `message` to the TXN builder,
228// which refuses the dispatch when it fails.
229mlir::LogicalResult emitRuntimeCheck(mlir::OpBuilder &builder,
230 mlir::Location loc, mlir::Value cond,
231 const llvm::Twine &message);
232
233// Erase the arith ops under `root` left unused once the runtime BD words and
234// guards built with these helpers have folded.
235void eraseDeadArith(mlir::Operation *root);
236
237// Coerce an OpFoldResult (constant attr or SSA value) to an SSA Value of the
238// given integer type, materializing an arith.constant / trunc / extui as
239// needed.
240mlir::Value getAsValue(mlir::OpBuilder &builder, mlir::Location loc,
241 mlir::OpFoldResult ofr, mlir::Type intType);
242
243// An integer or index OpFoldResult as an i64 Value, zero-extended (a negative
244// value reads as a huge one the guards reject). An operand wider than 64 bits
245// gets a host-side check that it fits; null if that check folds to false.
246mlir::Value getAsI64(mlir::OpBuilder &builder, mlir::Location loc,
247 mlir::OpFoldResult ofr);
248
249// Build the address-patch `arg_plus` (buffer BYTE offset):
250// sum(elementOffsets[i] * strides[i]) * elemWidthBytes + baseByteOffset, where
251// any entry may be runtime. A fully-constant set folds to one i32 constant (i64
252// when it does not fit), byte-identical to the static path; a runtime one is
253// i64 arith with a host-side check that it is `granuleBytes`-aligned. Null if
254// a check folds to false.
255mlir::Value buildArgPlusValue(mlir::OpBuilder &builder, mlir::Location loc,
256 llvm::ArrayRef<mlir::OpFoldResult> elementOffsets,
257 llvm::ArrayRef<mlir::OpFoldResult> strides,
258 int64_t elemWidthBytes, int64_t baseByteOffset,
259 uint32_t granuleBytes);
260
261// Require a runtime BD walk to stay inside its host buffer: the furthest
262// element it touches, sum(offsets[k] * offsetStrides[k]) +
263// sum((sizes[i] - 1) * strides[i]), must lie within `hostBufferType` past
264// `baseByteOffset`. An unknown (dynamic) shape bounds the walk at 2^32
265// elements, which still rejects a negative offset.
266mlir::LogicalResult
267guardWithinHostBuffer(mlir::OpBuilder &builder, mlir::Location loc,
268 mlir::BaseMemRefType hostBufferType,
269 int64_t baseByteOffset, int64_t elemWidthBytes,
270 llvm::ArrayRef<mlir::OpFoldResult> offsets,
271 llvm::ArrayRef<mlir::OpFoldResult> offsetStrides,
272 llvm::ArrayRef<mlir::OpFoldResult> sizes,
273 llvm::ArrayRef<mlir::OpFoldResult> strides);
274
275// Pack a set of (value, mask, shift) fields into a single i32 BD word via
276// arith and/shl/or. mask == 0xFFFFFFFF skips the AND; shift == 0 skips the SHL.
277mlir::Value
278buildBdWord(mlir::OpBuilder &builder, mlir::Location loc,
279 llvm::ArrayRef<std::tuple<mlir::Value, uint32_t, uint32_t>> fields);
280
281// The base register address of a BD block, `getDmaBdAddress(col,row,bd_id)`, as
282// an i32 Value. The address is linear in bd_id (base + bd_id*bdStride), so a
283// constant bdId folds to the exact literal the static path uses, while a
284// runtime bdId (dynamic free-list pool) is emitted as arith. Shared by the
285// BD-word encoder and the descriptor / address-patch lowering so every register
286// touched for one BD is computed from the same base.
287mlir::Value getBdRegisterBase(mlir::OpBuilder &builder, mlir::Location loc,
288 const xilinx::AIE::AIETargetModel &targetModel,
289 int tileCol, int tileRow,
290 mlir::OpFoldResult bdId);
291
292// Lock, packet and next_bd fields, gathered by the caller from the DMA
293// structure and baked into the template words. Only next_bd and the lock values
294// may be runtime: `next_bd_id_val`, when set, replaces `next_bd_id` (a chain
295// whose successor draws its id from the dynamic pool), and `lock_*_val_val`
296// replaces `lock_*_val` already in field encoding (an AcquireGE negated).
298 uint32_t use_next_bd = 0, next_bd_id = 0;
299 mlir::Value next_bd_id_val;
300 int32_t enable_packet = 0, packet_id = 0, packet_type = 0;
301 int32_t out_of_order_id = 0;
302 int32_t iteration_current = 0;
303 int32_t lock_rel_val = 0, lock_rel_id = 0;
306};
307
308// Build a BD's full register block as i32 SSA values, for the dynamic lowering
309// shared by dma_memcpy_nd and dma_task. The block is 8 words on a shim NOC or
310// mem tile and 6 on a core tile. Every word is written (unset slots are zero),
311// so a BD slot reused from the runtime pool cannot inherit a stale field.
312//
313// `mixedSizes`/`mixedStrides` are outermost-first (d3..d0), matching
314// NpuDmaMemcpyNdOp::getMixedSizes and AIE::DMABDOp::getMixedSizes.
315// buffer_length is the d0*d1*d2 extent in granules. `lenElems`, if set, is a
316// dma_task's explicit transfer length, which must agree with it.
317// `burstLength`/`axcache` are shim-NOC-only fields and are ignored on the
318// other tile types (their verifiers already require the defaults there). Every
319// size and stride gets a host-side check (emitRuntimeCheck) that it fits its BD
320// field and is granule-realizable, so a value the field would truncate refuses
321// the dispatch instead. `repeatCountOut` receives the outer-dim hardware repeat
322// for the caller's queue push.
323//
324// The words do not depend on the BD id -- only the register ADDRESS does, and
325// that is the caller's `getBdRegisterBase` + `npu.blockwrite_values` -- so one
326// routine serves both a pinned bd_id and one drawn from the runtime pool.
327//
328// The tile at (`tileCol`, `tileRow`) must have a target-model DmaBdLayout; the
329// caller is responsible for rejecting anything else with a user-facing
330// diagnostic before calling.
331mlir::LogicalResult
332buildBdWords(mlir::OpBuilder &builder, mlir::Location loc,
333 const xilinx::AIE::AIETargetModel &targetModel, int tileCol,
334 int tileRow, const BdTemplateFields &fields,
335 llvm::ArrayRef<mlir::OpFoldResult> mixedSizes,
336 llvm::ArrayRef<mlir::OpFoldResult> mixedStrides,
337 uint64_t elemWidth, uint32_t burstLength, uint32_t axcache,
338 mlir::OpFoldResult lenElems, mlir::Value &repeatCountOut,
339 llvm::SmallVectorImpl<mlir::Value> &wordsOut);
340
341} // namespace xilinx::AIEX
342
343#endif // AIE_DIALECT_AIEX_UTILS_BDLOWERING_H
int64_t bdGranuleDivisor(uint64_t elemWidth, uint32_t addressGranularity)
Definition BdLowering.h:58
mlir::Value buildBdWord(mlir::OpBuilder &builder, mlir::Location loc, llvm::ArrayRef< std::tuple< mlir::Value, uint32_t, uint32_t > > fields)
void encodeHardwareStridesWraps(Policy &p, uint64_t elemWidth, uint32_t addressGranularity, typename Policy::V inputSizes[4], typename Policy::V inputStrides[4], typename Policy::V sizes[4], typename Policy::V strides[4])
Definition BdLowering.h:145
mlir::Value buildArgPlusValue(mlir::OpBuilder &builder, mlir::Location loc, llvm::ArrayRef< mlir::OpFoldResult > elementOffsets, llvm::ArrayRef< mlir::OpFoldResult > strides, int64_t elemWidthBytes, int64_t baseByteOffset, uint32_t granuleBytes)
mlir::Value getAsValue(mlir::OpBuilder &builder, mlir::Location loc, mlir::OpFoldResult ofr, mlir::Type intType)
mlir::LogicalResult guardWithinHostBuffer(mlir::OpBuilder &builder, mlir::Location loc, mlir::BaseMemRefType hostBufferType, int64_t baseByteOffset, int64_t elemWidthBytes, llvm::ArrayRef< mlir::OpFoldResult > offsets, llvm::ArrayRef< mlir::OpFoldResult > offsetStrides, llvm::ArrayRef< mlir::OpFoldResult > sizes, llvm::ArrayRef< mlir::OpFoldResult > strides)
mlir::LogicalResult buildBdWords(mlir::OpBuilder &builder, mlir::Location loc, const xilinx::AIE::AIETargetModel &targetModel, int tileCol, int tileRow, const BdTemplateFields &fields, llvm::ArrayRef< mlir::OpFoldResult > mixedSizes, llvm::ArrayRef< mlir::OpFoldResult > mixedStrides, uint64_t elemWidth, uint32_t burstLength, uint32_t axcache, mlir::OpFoldResult lenElems, mlir::Value &repeatCountOut, llvm::SmallVectorImpl< mlir::Value > &wordsOut)
mlir::Value getAsI64(mlir::OpBuilder &builder, mlir::Location loc, mlir::OpFoldResult ofr)
void eraseDeadArith(mlir::Operation *root)
bool isConstMultipleOfGranule(int64_t value, uint64_t elemWidth, uint32_t addressGranularity)
Definition BdLowering.h:65
mlir::Value getBdRegisterBase(mlir::OpBuilder &builder, mlir::Location loc, const xilinx::AIE::AIETargetModel &targetModel, int tileCol, int tileRow, mlir::OpFoldResult bdId)
mlir::LogicalResult emitRuntimeCheck(mlir::OpBuilder &builder, mlir::Location loc, mlir::Value cond, const llvm::Twine &message)
mlir::LogicalResult verifyConstBdRealizability(mlir::Operation *op, llvm::ArrayRef< mlir::OpFoldResult > sizes, llvm::ArrayRef< mlir::OpFoldResult > strides, uint64_t elemWidth, uint32_t gran)
Definition BdLowering.h:77
static V div(V v, int64_t c)
Definition BdLowering.h:199
static V selectLt(V a, V b, V t, V e)
Definition BdLowering.h:203
static V sub(V v, int64_t c)
Definition BdLowering.h:200
static V selectGT0(V cond, V t, V e)
Definition BdLowering.h:202
static V mul(V v, int64_t c)
Definition BdLowering.h:198
static V selectGT1(V cond, V t, V e)
Definition BdLowering.h:201
V selectGT1(V cond, V t, V e) const
V div(V v, int64_t c) const
V sub(V v, int64_t c) const
SsaStridePolicy(mlir::OpBuilder &b, mlir::Location l)
Definition BdLowering.h:214
mlir::OpBuilder & builder
Definition BdLowering.h:211
V selectGT0(V cond, V t, V e) const
V mul(V v, int64_t c) const
V selectLt(V a, V b, V t, V e) const