MLIR-AIE 1.4.5.dev
AIEVectorToPointerLoops.cpp
Go to the documentation of this file.
1//===- AIEVectorToPointerLoops.cpp -----------------------------*- C++ -*-===//
2//
3// Copyright (C) 2025 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7//
8// This pass transforms vector.load/store operations with loop-carried indices
9// to use ptr dialect operations (ptr.to_ptr, ptr.ptr_add, ptr.from_ptr).
10//
11// Goal: Make pointer increment patterns explicit to help LLVM backend generate
12// efficient post-increment addressing modes (GEP fusion).
13//
14// Transformation:
15// Before:
16// scf.for iter_args(%idx = %0) -> (index) {
17// %vec = vector.load %memref[%idx]
18// %next_idx = arith.addi %idx, %stride
19// scf.yield %next_idx
20// }
21//
22// After:
23// %base_ptr = ptr.to_ptr %memref
24// %init_ptr = ptr.ptr_add %base_ptr, %0
25// scf.for iter_args(%ptr = %init_ptr) -> (!ptr.ptr<...>) {
26// %memref_tmp = ptr.from_ptr %ptr
27// %vec = vector.load %memref_tmp[%c0]
28// %next_ptr = ptr.ptr_add %ptr, %stride
29// scf.yield %next_ptr
30// }
31//
32//===----------------------------------------------------------------------===//
33
36
37#include "mlir/Dialect/Arith/IR/Arith.h"
38#include "mlir/Dialect/Func/IR/FuncOps.h"
39#include "mlir/Dialect/MemRef/IR/MemRef.h"
40#include "mlir/Dialect/Ptr/IR/PtrOps.h"
41#include "mlir/Dialect/SCF/IR/SCF.h"
42#include "mlir/Dialect/Vector/IR/VectorOps.h"
43#include "mlir/IR/PatternMatch.h"
44#include "mlir/Pass/Pass.h"
45#include "mlir/Transforms/DialectConversion.h"
46#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
47
48#include "llvm/ADT/DenseMap.h"
49#include "llvm/ADT/DenseSet.h"
50#include "llvm/ADT/SetVector.h"
51
52#define DEBUG_TYPE "aie-vector-to-pointer-loops"
53
54using namespace mlir;
55using namespace xilinx;
56using namespace xilinx::AIE;
57
58namespace {
59
60/// Check if a value is an iter_arg of an scf.for loop
61static bool isLoopCarriedValue(Value val, scf::ForOp forOp) {
62 auto blockArgs = forOp.getRegionIterArgs();
63 return llvm::is_contained(blockArgs, val);
64}
65
66/// Structure to track memref base and its uses in vector ops
67struct MemrefVectorAccess {
68 Value memref;
69 SmallVector<vector::LoadOp> loads;
70 SmallVector<vector::StoreOp> stores;
71 SmallVector<Value> indices; // Loop-carried indices used
72};
73
74/// Analyze loop to find vector load/store patterns with loop-carried indices
75static bool analyzeLoopForVectorAccesses(
76 scf::ForOp forOp, DenseMap<Value, MemrefVectorAccess> &memrefAccesses) {
77
78 bool foundPattern = false;
79
80 forOp.walk([&](vector::LoadOp loadOp) {
81 Value base = loadOp.getBase();
82 auto indices = loadOp.getIndices();
83
84 // Only handle 1D access for now. vector.load requires a unit innermost
85 // stride, so one index step of a 1D access is one element.
86 if (indices.size() != 1)
87 return;
88
89 Value idx = indices[0];
90
91 // Check if index is loop-carried
92 if (isLoopCarriedValue(idx, forOp)) {
93 memrefAccesses[base].memref = base;
94 memrefAccesses[base].loads.push_back(loadOp);
95 memrefAccesses[base].indices.push_back(idx);
96 foundPattern = true;
97 }
98 });
99
100 forOp.walk([&](vector::StoreOp storeOp) {
101 Value base = storeOp.getBase();
102 auto indices = storeOp.getIndices();
103
104 if (indices.size() != 1)
105 return;
106
107 Value idx = indices[0];
108
109 if (isLoopCarriedValue(idx, forOp)) {
110 memrefAccesses[base].memref = base;
111 memrefAccesses[base].stores.push_back(storeOp);
112 memrefAccesses[base].indices.push_back(idx);
113 foundPattern = true;
114 }
115 });
116
117 return foundPattern;
118}
119
120/// Whether the index iter_arg at `pos` can become a pointer into `memref`.
121/// The body rewrite only understands the index as the sole index of a
122/// top-level vector.load/store on `memref`, as an operand of a top-level
123/// arith.addi (whose result is then held to the same rules), and as the value
124/// the loop yields back to it. Any other use (inside a nested loop, a scalar
125/// memref.load, a comparison, the loop result) would receive a pointer where
126/// it expects an index.
127static bool canConvertIndexIterArg(scf::ForOp forOp, unsigned pos, Value memref,
128 DenseSet<Value> &chain) {
129 if (!forOp.isDefinedOutsideOfLoop(memref))
130 return false;
131 if (!forOp.getResult(pos).use_empty())
132 return false;
133
134 Block *body = forOp.getBody();
135 Operation *yield = body->getTerminator();
136 SmallVector<Value> worklist = {forOp.getRegionIterArgs()[pos]};
137 chain.insert(worklist.begin(), worklist.end());
138 while (!worklist.empty()) {
139 Value v = worklist.pop_back_val();
140 for (OpOperand &use : v.getUses()) {
141 Operation *user = use.getOwner();
142 if (user->getBlock() != body)
143 return false;
144 if (user == yield) {
145 if (use.getOperandNumber() != pos)
146 return false;
147 continue;
148 }
149 if (auto load = dyn_cast<vector::LoadOp>(user)) {
150 if (load.getBase() != memref || load.getIndices().size() != 1 ||
151 load.getIndices()[0] != v)
152 return false;
153 continue;
154 }
155 if (auto store = dyn_cast<vector::StoreOp>(user)) {
156 if (store.getBase() != memref || store.getIndices().size() != 1 ||
157 store.getIndices()[0] != v || store.getValueToStore() == v)
158 return false;
159 continue;
160 }
161 if (auto addi = dyn_cast<arith::AddIOp>(user)) {
162 // Only pointer + offset is supported, not pointer + pointer.
163 if (chain.contains(addi.getLhs()) && chain.contains(addi.getRhs()))
164 return false;
165 if (chain.insert(addi.getResult()).second)
166 worklist.push_back(addi.getResult());
167 continue;
168 }
169 return false;
170 }
171 }
172 // The yielded value becomes the next pointer, so it must be in the chain.
173 return chain.contains(yield->getOperand(pos));
174}
175
176/// Transform an scf.for loop to use pointer iter_args
177struct VectorToPointerLoopsPattern : public OpRewritePattern<scf::ForOp> {
178 using OpRewritePattern<scf::ForOp>::OpRewritePattern;
179
180 LogicalResult matchAndRewrite(scf::ForOp forOp,
181 PatternRewriter &rewriter) const override {
182
183 // Skip if loop already uses pointer iter_args (already transformed)
184 for (Value iterArg : forOp.getRegionIterArgs()) {
185 if (llvm::isa<ptr::PtrType>(iterArg.getType()))
186 return failure(); // Already transformed
187 }
188
189 // Analyze the loop for vector access patterns
190 DenseMap<Value, MemrefVectorAccess> memrefAccesses;
191 if (!analyzeLoopForVectorAccesses(forOp, memrefAccesses))
192 return failure();
193
194 if (memrefAccesses.empty())
195 return failure();
196
197 // Step 1: Identify which iter_args are indices used in vector ops. This
198 // step must not modify the IR: a pattern that returns failure after
199 // creating ops keeps the greedy driver from converging.
200 SmallVector<unsigned> indexIterArgPositions;
201 SmallVector<Value> correspondingMemrefs;
202
203 // Values derived from the iter_args accepted so far. Two chains that meet
204 // (e.g. %a + %b) would add a pointer to a pointer.
205 DenseSet<Value> converted;
206 for (auto [idx, iterArg] : llvm::enumerate(forOp.getRegionIterArgs())) {
207 Value owner;
208 for (auto &[memref, access] : memrefAccesses) {
209 if (!llvm::is_contained(access.indices, iterArg)) {
210 continue;
211 }
212 // A pointer is derived from a single memref, so an index shared by
213 // two memrefs cannot be converted.
214 if (owner) {
215 return failure();
216 }
217 owner = memref;
218 }
219 if (owner) {
220 DenseSet<Value> chain;
221 if (!canConvertIndexIterArg(forOp, idx, owner, chain)) {
222 return failure();
223 }
224 for (Value v : chain) {
225 if (!converted.insert(v).second) {
226 return failure();
227 }
228 }
229 indexIterArgPositions.push_back(idx);
230 correspondingMemrefs.push_back(owner);
231 }
232 }
233
234 if (indexIterArgPositions.empty())
235 return failure();
236
237 Location loc = forOp.getLoc();
238 OpBuilder::InsertionGuard guard(rewriter);
239 rewriter.setInsertionPoint(forOp);
240
241 // Step 2: Convert memrefs to pointers before the loop
242 DenseMap<Value, Value> memrefToPtrMap;
243 DenseMap<Value, Type>
244 memrefToGenericTypeMap; // Track generic-space memref types
245 DenseMap<Value, Value>
246 memrefToConvertedMap; // Track the converted memref value (for metadata)
247 auto genericSpace = ptr::GenericSpaceAttr::get(rewriter.getContext());
248
249 for (auto &[memref, access] : memrefAccesses) {
250 // Get the memory space from the memref type
251 auto memrefType = cast<MemRefType>(memref.getType());
252 Attribute memorySpace = memrefType.getMemorySpace();
253
254 Value memrefToConvert = memref;
255 Type genericMemrefType = memrefType;
256
257 // ptr.to_ptr requires the memref and pointer memory spaces to match,
258 // so cast anything not already in generic_space, including the default
259 // (attribute-less) space.
260 if (!llvm::isa_and_nonnull<ptr::GenericSpaceAttr>(memorySpace)) {
261 // Create new memref type with generic_space
262 auto newMemrefType =
263 MemRefType::get(memrefType.getShape(), memrefType.getElementType(),
264 memrefType.getLayout(), genericSpace);
265
266 // Insert unrealized_conversion_cast to convert to generic_space
267 auto castOp = UnrealizedConversionCastOp::create(rewriter, loc,
268 newMemrefType, memref);
269 memrefToConvert = castOp.getResult(0);
270 genericMemrefType = newMemrefType;
271 }
272
273 // Create pointer type with generic_space
274 auto ptrType = ptr::PtrType::get(rewriter.getContext(), genericSpace);
275 auto ptrOp =
276 ptr::ToPtrOp::create(rewriter, loc, ptrType, memrefToConvert);
277 memrefToPtrMap[memref] = ptrOp.getResult();
278 memrefToGenericTypeMap[memref] = genericMemrefType;
279 memrefToConvertedMap[memref] =
280 memrefToConvert; // Store the converted memref
281 }
282
283 // Step 3: Build new init args with pointers
284 SmallVector<Value> newInitArgs;
285 Value c0 = arith::ConstantIndexOp::create(rewriter, loc, 0);
286
287 for (auto [idx, initArg] : llvm::enumerate(forOp.getInitArgs())) {
288 auto *it = llvm::find(indexIterArgPositions, idx);
289 if (it != indexIterArgPositions.end()) {
290 // This is an index iter_arg - convert to pointer
291 size_t pos = std::distance(indexIterArgPositions.begin(), it);
292 Value memref = correspondingMemrefs[pos];
293 Value basePtr = memrefToPtrMap[memref];
294
295 // Get element size in bytes
296 auto memrefType = cast<MemRefType>(memref.getType());
297 unsigned elementSizeBits = memrefType.getElementTypeBitWidth();
298 unsigned elementSizeBytes = (elementSizeBits + 7) / 8;
299
300 // Scale index by element size: byteOffset = initArg * elementSizeBytes
301 Value byteOffset = initArg;
302 if (elementSizeBytes != 1) {
303 Value elementSize =
304 arith::ConstantIndexOp::create(rewriter, loc, elementSizeBytes);
305 byteOffset =
306 arith::MulIOp::create(rewriter, loc, initArg, elementSize);
307 }
308
309 // Create: ptr.ptr_add basePtr, byteOffset
310 auto initPtrOp =
311 ptr::PtrAddOp::create(rewriter, loc, basePtr, byteOffset);
312 newInitArgs.push_back(initPtrOp.getResult());
313 } else {
314 // Keep as-is
315 newInitArgs.push_back(initArg);
316 }
317 }
318
319 // Step 4: Create new loop with updated signature
320 auto newForOp =
321 scf::ForOp::create(rewriter, loc, forOp.getLowerBound(),
322 forOp.getUpperBound(), forOp.getStep(), newInitArgs);
323 // Keep the loop's attributes: unsignedCmp and discardable ones such as
324 // loop_annotation.
325 newForOp->setAttrs(forOp->getAttrDictionary());
326
327 // Step 5: Transform loop body (simplified - doesn't handle all cases yet)
328 IRMapping mapper;
329 mapper.map(forOp.getInductionVar(), newForOp.getInductionVar());
330
331 for (auto [oldArg, newArg] :
332 llvm::zip(forOp.getRegionIterArgs(), newForOp.getRegionIterArgs())) {
333 mapper.map(oldArg, newArg);
334 }
335
336 // The memref each pointer points into, used to scale its increments by
337 // the right element size. Holds the pointer iter_args and every
338 // ptr.ptr_add derived from them.
339 DenseMap<Value, Value> ptrToMemref;
340 for (auto [pos, memref] :
341 llvm::zip(indexIterArgPositions, correspondingMemrefs)) {
342 ptrToMemref[newForOp.getRegionIterArgs()[pos]] = memref;
343 }
344
345 rewriter.setInsertionPointToStart(newForOp.getBody());
346
347 // Clone operations with transformation (c0 already created above)
348 for (Operation &op : forOp.getBody()->without_terminator()) {
349 // Transform vector.load operations
350 if (auto loadOp = dyn_cast<vector::LoadOp>(&op)) {
351 Value idx = loadOp.getIndices()[0];
352 Value mappedIdx = mapper.lookupOrDefault(idx);
353
354 // Check if the index is now a pointer (was transformed)
355 if (llvm::isa<ptr::PtrType>(mappedIdx.getType())) {
356 // Get the generic-space memref type for this base
357 Type genericType = memrefToGenericTypeMap[loadOp.getBase()];
358
359 // Get metadata from the converted memref (with generic_space)
360 Value convertedMemref = memrefToConvertedMap[loadOp.getBase()];
361 auto metadataOp = ptr::GetMetadataOp::create(
362 rewriter, loadOp.getLoc(), convertedMemref);
363
364 // Transform: vector.load %memref[%ptr] -> ptr.from_ptr + vector.load
365 // [...[0]]
366 auto fromPtrOp =
367 ptr::FromPtrOp::create(rewriter, loadOp.getLoc(), genericType,
368 mappedIdx, metadataOp.getResult());
369 auto newLoad = vector::LoadOp::create(
370 rewriter, loadOp.getLoc(), loadOp.getVectorType(),
371 fromPtrOp.getResult(), ValueRange{c0});
372 mapper.map(loadOp.getResult(), newLoad.getResult());
373 continue;
374 }
375 }
376
377 // Transform vector.store operations
378 if (auto storeOp = dyn_cast<vector::StoreOp>(&op)) {
379 Value idx = storeOp.getIndices()[0];
380 Value mappedIdx = mapper.lookupOrDefault(idx);
381
382 if (llvm::isa<ptr::PtrType>(mappedIdx.getType())) {
383 // Get the generic-space memref type for this base
384 Type genericType = memrefToGenericTypeMap[storeOp.getBase()];
385
386 // Get metadata from the converted memref (with generic_space)
387 Value convertedMemref = memrefToConvertedMap[storeOp.getBase()];
388 auto metadataOp = ptr::GetMetadataOp::create(
389 rewriter, storeOp.getLoc(), convertedMemref);
390
391 // Transform: vector.store %val, %memref[%ptr] -> ptr.from_ptr +
392 // vector.store[0]
393 auto fromPtrOp =
394 ptr::FromPtrOp::create(rewriter, storeOp.getLoc(), genericType,
395 mappedIdx, metadataOp.getResult());
396 Value valueToStore =
397 mapper.lookupOrDefault(storeOp.getValueToStore());
398 vector::StoreOp::create(rewriter, storeOp.getLoc(), valueToStore,
399 fromPtrOp.getResult(), ValueRange{c0});
400 continue;
401 }
402 }
403
404 // Transform arith.addi to ptr.ptr_add when operating on pointers
405 if (auto addiOp = dyn_cast<arith::AddIOp>(&op)) {
406 Value lhs = mapper.lookupOrDefault(addiOp.getLhs());
407 Value rhs = mapper.lookupOrDefault(addiOp.getRhs());
408 if (llvm::isa<ptr::PtrType>(rhs.getType())) {
409 std::swap(lhs, rhs);
410 }
411
412 // If an operand is a pointer, convert to ptr.ptr_add
413 if (llvm::isa<ptr::PtrType>(lhs.getType())) {
414 Value memrefForPtr = ptrToMemref.lookup(lhs);
415
416 Value byteOffset = rhs;
417 if (memrefForPtr) {
418 auto memrefType = cast<MemRefType>(memrefForPtr.getType());
419 unsigned elementSizeBits = memrefType.getElementTypeBitWidth();
420 unsigned elementSizeBytes = (elementSizeBits + 7) / 8;
421
422 // Scale offset by element size: byteOffset = rhs * elementSizeBytes
423 if (elementSizeBytes != 1) {
424 Value elementSize = arith::ConstantIndexOp::create(
425 rewriter, addiOp.getLoc(), elementSizeBytes);
426 byteOffset = arith::MulIOp::create(rewriter, addiOp.getLoc(), rhs,
427 elementSize);
428 }
429 }
430
431 auto ptrAddOp =
432 ptr::PtrAddOp::create(rewriter, addiOp.getLoc(), lhs, byteOffset);
433 ptrToMemref[ptrAddOp.getResult()] = memrefForPtr;
434 mapper.map(addiOp.getResult(), ptrAddOp.getResult());
435 continue;
436 }
437 }
438
439 // Default: clone the operation
440 rewriter.clone(op, mapper);
441 }
442
443 // Clone yield
444 auto oldYield = cast<scf::YieldOp>(forOp.getBody()->getTerminator());
445 SmallVector<Value> newYieldOperands;
446 for (Value operand : oldYield.getOperands()) {
447 newYieldOperands.push_back(mapper.lookupOrDefault(operand));
448 }
449 scf::YieldOp::create(rewriter, loc, newYieldOperands);
450
451 // Step 6: Replace old loop
452 rewriter.replaceOp(forOp, newForOp.getResults());
453
454 // NOTE: This is a simplified implementation
455 // Full version needs to properly transform vector.load/store and arith.addi
456
457 return success();
458 }
459};
460
461struct AIEVectorToPointerLoopsPass
462 : public PassWrapper<AIEVectorToPointerLoopsPass, OperationPass<DeviceOp>> {
463
464 MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(AIEVectorToPointerLoopsPass)
465
466 StringRef getArgument() const override {
467 return "aie-vector-to-pointer-loops";
468 }
469
470 StringRef getDescription() const override {
471 return "Transform vector.load/store with loop-carried indices to use ptr "
472 "dialect";
473 }
474
475 void getDependentDialects(DialectRegistry &registry) const override {
476 registry.insert<ptr::PtrDialect>();
477 registry.insert<scf::SCFDialect>();
478 registry.insert<vector::VectorDialect>();
479 registry.insert<arith::ArithDialect>();
480 registry.insert<memref::MemRefDialect>();
481 }
482
483 void runOnOperation() override {
484 DeviceOp deviceOp = getOperation();
485
486 RewritePatternSet patterns(&getContext());
487 patterns.add<VectorToPointerLoopsPattern>(&getContext());
488
489 // Apply patterns to the entire device
490 // The pattern will match scf.for loops in aie.core regions
491 if (failed(applyPatternsGreedily(deviceOp, std::move(patterns)))) {
492 signalPassFailure();
493 }
494 }
495};
496
497} // namespace
498
499namespace xilinx {
500namespace AIE {
501
502std::unique_ptr<OperationPass<DeviceOp>> createAIEVectorToPointerLoopsPass() {
503 return std::make_unique<AIEVectorToPointerLoopsPass>();
504}
505
506} // namespace AIE
507} // namespace xilinx
std::unique_ptr< mlir::OperationPass< DeviceOp > > createAIEVectorToPointerLoopsPass()