MLIR-AIE 1.4.5.dev
SplitVectorLoadUpsChains.cpp
Go to the documentation of this file.
1//===- SplitVectorLoadUpsChains.cpp - Split Load+UPS Chains ----*- C++ -*-===//
2//
3// Copyright (C) 2025 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7// Splits 1024-bit vector loads and stores into 512-bit halves where the
8// value is widened or narrowed one half at a time on AIE2P: vector.load +
9// aievec.ups, aievec.srs + vector.store, and bf16 loads feeding the LHS of
10// aievec.matmul_aie2p. Each half then maps to its own memory access instead of
11// a shuffle of the full-width value.
12//===----------------------------------------------------------------------===//
13
16#include "mlir/Dialect/Affine/IR/AffineOps.h"
17#include "mlir/Dialect/Arith/IR/Arith.h"
18#include "mlir/Dialect/MemRef/IR/MemRef.h"
19#include "mlir/Dialect/Vector/IR/VectorOps.h"
20#include "mlir/IR/PatternMatch.h"
21#include "mlir/Pass/Pass.h"
22#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
23#include "llvm/ADT/Sequence.h"
24
25#define DEBUG_TYPE "aievec-split-load-ups-chains"
26
27using namespace mlir;
28using namespace xilinx::aievec;
29
30namespace {
31
32/// Pattern to optimize vector.load + aievec.ups chains by splitting them.
33///
34/// This pattern detects cases where a 1024-bit vector is loaded and then
35/// passed to an aievec.ups operation that produces a 2048-bit result.
36/// Instead of the inefficient approach of:
37/// 1. Load 1024 bits
38/// 2. Shuffle to split into 2×512 bits
39/// 3. Apply 2× UPS operations
40/// 4. Shuffle to concatenate results
41///
42/// It transforms to:
43/// 1. Load 2×512 bits directly
44/// 2. Apply 2× UPS operations immediately
45/// 3. Shuffle once to concatenate results
46///
47/// This reduces shuffle operations from 3 to 1.
48struct SplitVectorLoadUpsChainPattern : public OpRewritePattern<UPSOp> {
50
51 LogicalResult matchAndRewrite(UPSOp upsOp,
52 PatternRewriter &rewriter) const override {
53 // Get source value and its type
54 Value source = upsOp.getSource();
55 auto srcVecTy = dyn_cast<VectorType>(source.getType());
56 if (!srcVecTy)
57 return failure();
58
59 // Get result type
60 auto resultVecTy = dyn_cast<VectorType>(upsOp.getResult().getType());
61 if (!resultVecTy)
62 return failure();
63
64 // Check if this is a 1024-bit -> 2048-bit integer UPS
65 Type srcElemTy = srcVecTy.getElementType();
66 Type resultElemTy = resultVecTy.getElementType();
67
68 if (!srcElemTy.isInteger() || !resultElemTy.isInteger())
69 return failure();
70
71 unsigned srcBitWidth = srcElemTy.getIntOrFloatBitWidth();
72 unsigned resultBitWidth = resultElemTy.getIntOrFloatBitWidth();
73 int64_t srcLanes = srcVecTy.getNumElements();
74 int64_t resultLanes = resultVecTy.getNumElements();
75
76 int64_t srcVectorSize = srcBitWidth * srcLanes;
77 int64_t resultVectorSize = resultBitWidth * resultLanes;
78
79 // Only optimize the 1024-bit -> 2048-bit case
80 // (e.g., v64int16 -> v64acc32)
81 if (srcVectorSize != 1024 || resultVectorSize != 2048)
82 return failure();
83
84 // Check that the UPS result width is 32 and source width is 16
85 if (resultBitWidth != 32 || srcBitWidth != 16)
86 return failure();
87
88 // Check if source is directly from a vector.load
89 auto loadOp = source.getDefiningOp<vector::LoadOp>();
90 if (!loadOp)
91 return failure();
92
93 // Ensure the load is only used by this UPS operation
94 if (!loadOp.getResult().hasOneUse())
95 return failure();
96
97 Location loc = upsOp.getLoc();
98
99 // Get load operation details
100 Value memRef = loadOp.getBase();
101 ValueRange indices = loadOp.getIndices();
102
103 // Create element type for half-sized vector (v32int16)
104 int64_t halfLanes = srcLanes / 2;
105 auto halfSrcVecTy = VectorType::get({halfLanes}, srcElemTy);
106 auto halfResultVecTy = VectorType::get({halfLanes}, resultElemTy);
107
108 // Calculate offset for second half load
109 // For v64int16, we need to offset by 32 elements (64 bytes for i16)
110 int64_t elementOffset = halfLanes;
111
112 // Create indices for first half load (same as original)
113 SmallVector<Value> firstHalfIndices(indices.begin(), indices.end());
114
115 // Create indices for second half load
116 SmallVector<Value> secondHalfIndices(indices.begin(), indices.end());
117
118 // Adjust the last index by the element offset
119 if (!indices.empty()) {
120 Value lastIdx = indices.back();
121 Value offsetVal =
122 arith::ConstantIndexOp::create(rewriter, loc, elementOffset);
123 Value newLastIdx =
124 arith::AddIOp::create(rewriter, loc, lastIdx, offsetVal);
125 secondHalfIndices.back() = newLastIdx;
126 }
127
128 // Create first half load
129 auto loadHalf0 = vector::LoadOp::create(rewriter, loc, halfSrcVecTy, memRef,
130 firstHalfIndices);
131
132 // Create second half load
133 auto loadHalf1 = vector::LoadOp::create(rewriter, loc, halfSrcVecTy, memRef,
134 secondHalfIndices);
135
136 // Create UPS for first half
137 auto upsHalf0 = UPSOp::create(rewriter, loc, halfResultVecTy,
138 loadHalf0.getResult(), upsOp.getShift());
139
140 // Create UPS for second half
141 auto upsHalf1 = UPSOp::create(rewriter, loc, halfResultVecTy,
142 loadHalf1.getResult(), upsOp.getShift());
143
144 // Concatenate the two halves using vector.shuffle
145 // The mask is sequential from 0 to 63 to concatenate [half0; half1]
146 SmallVector<int64_t> concatMask;
147 for (int64_t i = 0; i < resultLanes; ++i) {
148 concatMask.push_back(i);
149 }
150
151 auto concatOp = vector::ShuffleOp::create(
152 rewriter, loc, upsHalf0.getResult(), upsHalf1.getResult(), concatMask);
153
154 // Replace the original UPS operation with the concatenated result
155 rewriter.replaceOp(upsOp, concatOp.getResult());
156
157 // The original load will be removed by dead code elimination
158 // since it no longer has any uses
159
160 return success();
161 }
162};
163
164/// Pattern to optimize aievec.srs + vector.store chains by splitting them.
165///
166/// This pattern detects cases where a 2048-bit vector is passed to an
167/// aievec.srs operation that produces a 1024-bit result, which is then stored.
168/// Instead of the inefficient approach of:
169/// 1. Shuffle to split 2048-bit into 2×1024 bits
170/// 2. Apply 2× SRS operations
171/// 3. Shuffle to concatenate results
172/// 4. Store 1024 bits
173///
174/// It transforms to:
175/// 1. Split source via shuffle into 2×1024 bits (for SRS input)
176/// 2. Apply 2× SRS operations to get 2×512 bits
177/// 3. Store 2×512 bits directly
178///
179/// This reduces shuffle operations from 3 to 1.
180struct SplitVectorSrsStoreChainPattern
181 : public OpRewritePattern<vector::StoreOp> {
182 using OpRewritePattern<vector::StoreOp>::OpRewritePattern;
183
184 LogicalResult matchAndRewrite(vector::StoreOp storeOp,
185 PatternRewriter &rewriter) const override {
186 // Get the value being stored and its type
187 Value valueToStore = storeOp.getValueToStore();
188 auto storeVecTy = dyn_cast<VectorType>(valueToStore.getType());
189 if (!storeVecTy)
190 return failure();
191
192 // Check if the value comes from an aievec.srs operation
193 auto srsOp = valueToStore.getDefiningOp<SRSOp>();
194 if (!srsOp)
195 return failure();
196
197 // Ensure the SRS is only used by this store operation
198 if (!srsOp.getResult().hasOneUse())
199 return failure();
200
201 // Get source and result types of SRS
202 Value srsSource = srsOp.getSource();
203 auto srcVecTy = dyn_cast<VectorType>(srsSource.getType());
204 if (!srcVecTy)
205 return failure();
206
207 Type srcElemTy = srcVecTy.getElementType();
208 Type resultElemTy = storeVecTy.getElementType();
209
210 if (!srcElemTy.isInteger() || !resultElemTy.isInteger())
211 return failure();
212
213 unsigned srcBitWidth = srcElemTy.getIntOrFloatBitWidth();
214 unsigned resultBitWidth = resultElemTy.getIntOrFloatBitWidth();
215 int64_t srcLanes = srcVecTy.getNumElements();
216 int64_t resultLanes = storeVecTy.getNumElements();
217
218 int64_t srcVectorSize = srcBitWidth * srcLanes;
219 int64_t resultVectorSize = resultBitWidth * resultLanes;
220
221 // Only optimize the 2048-bit -> 1024-bit case
222 // (e.g., v64acc32 -> v64int16)
223 if (srcVectorSize != 2048 || resultVectorSize != 1024)
224 return failure();
225
226 // Check that the SRS source width is 32 and result width is 16
227 if (srcBitWidth != 32 || resultBitWidth != 16)
228 return failure();
229
230 Location loc = storeOp.getLoc();
231
232 // Get store operation details
233 Value memRef = storeOp.getBase();
234 ValueRange indices = storeOp.getIndices();
235
236 // Create element types for half-sized vectors
237 int64_t halfSrcLanes = srcLanes / 2;
238 int64_t halfResultLanes = resultLanes / 2;
239 auto halfResultVecTy = VectorType::get({halfResultLanes}, resultElemTy);
240
241 // Split the SRS source into two halves using shuffle
242 SmallVector<int64_t> firstHalfMask, secondHalfMask;
243 for (int64_t i = 0; i < halfSrcLanes; ++i) {
244 firstHalfMask.push_back(i);
245 secondHalfMask.push_back(halfSrcLanes + i);
246 }
247
248 auto srcHalf0 = vector::ShuffleOp::create(rewriter, loc, srsSource,
249 srsSource, firstHalfMask);
250 auto srcHalf1 = vector::ShuffleOp::create(rewriter, loc, srsSource,
251 srsSource, secondHalfMask);
252
253 // Create SRS for first half
254 auto srsHalf0 = SRSOp::create(rewriter, loc, halfResultVecTy,
255 srcHalf0.getResult(), srsOp.getShift());
256
257 // Create SRS for second half
258 auto srsHalf1 = SRSOp::create(rewriter, loc, halfResultVecTy,
259 srcHalf1.getResult(), srsOp.getShift());
260
261 // Calculate offset for second half store
262 int64_t elementOffset = halfResultLanes;
263
264 // Create indices for first half store (same as original)
265 SmallVector<Value> firstHalfIndices(indices.begin(), indices.end());
266
267 // Create indices for second half store
268 SmallVector<Value> secondHalfIndices(indices.begin(), indices.end());
269
270 // Adjust the last index by the element offset
271 if (!indices.empty()) {
272 Value lastIdx = indices.back();
273 Value offsetVal =
274 arith::ConstantIndexOp::create(rewriter, loc, elementOffset);
275 Value newLastIdx =
276 arith::AddIOp::create(rewriter, loc, lastIdx, offsetVal);
277 secondHalfIndices.back() = newLastIdx;
278 }
279
280 // Create first half store
281 vector::StoreOp::create(rewriter, loc, srsHalf0.getResult(), memRef,
282 firstHalfIndices);
283
284 // Create second half store
285 vector::StoreOp::create(rewriter, loc, srsHalf1.getResult(), memRef,
286 secondHalfIndices);
287
288 // Erase the original store operation
289 rewriter.eraseOp(storeOp);
290
291 // The original SRS will be removed by dead code elimination
292 // since it no longer has any uses
293
294 return success();
295 }
296};
297
298/// Splits a 1024-bit bf16 vector.load that only feeds the LHS of bf16 8x8x8
299/// aievec.matmul_aie2p ops into two 512-bit loads joined by a vector.shuffle.
300///
301/// The matmul lowering widens the LHS to accfloat one 32-lane half at a time.
302/// With a single 64-lane load, each half is extracted by a shuffle before it is
303/// widened. After this split, and once LLVM folds the halves of the joining
304/// shuffle back to the two loads, each widening reads its own 512-bit load,
305/// which the backend can fuse into a single load-and-convert.
306struct SplitVectorLoadMatMulLhsPattern
307 : public OpRewritePattern<vector::LoadOp> {
308 using OpRewritePattern<vector::LoadOp>::OpRewritePattern;
309
310 LogicalResult matchAndRewrite(vector::LoadOp loadOp,
311 PatternRewriter &rewriter) const override {
312 VectorType loadTy = loadOp.getVectorType();
313 if (loadTy.getRank() != 1 || loadTy.getNumElements() != 64 ||
314 !loadTy.getElementType().isBF16())
315 return failure();
316
317 auto bf16Tile = VectorType::get({8, 8}, rewriter.getBF16Type());
318 auto f32Tile = VectorType::get({8, 8}, rewriter.getF32Type());
319 auto feedsOnlyMatMulLhs = [&](Operation *user) {
320 auto cast = dyn_cast<vector::ShapeCastOp>(user);
321 if (!cast || cast.getResultVectorType() != bf16Tile)
322 return false;
323 return llvm::all_of(cast->getUses(), [&](OpOperand &use) {
324 auto matmul = dyn_cast<MatMulOp_AIE2P>(use.getOwner());
325 return matmul && use.getOperandNumber() == 0 &&
326 matmul.getRhs().getType() == bf16Tile &&
327 matmul.getAcc().getType() == f32Tile;
328 });
329 };
330 if (loadOp->use_empty() ||
331 !llvm::all_of(loadOp->getUsers(), feedsOnlyMatMulLhs))
332 return failure();
333
334 Location loc = loadOp.getLoc();
335 auto halfTy = VectorType::get({32}, rewriter.getBF16Type());
336 SmallVector<Value> hiIndices(loadOp.getIndices());
337 Value c32 = arith::ConstantIndexOp::create(rewriter, loc, 32);
338 hiIndices.back() =
339 arith::AddIOp::create(rewriter, loc, hiIndices.back(), c32);
340 Value lo = vector::LoadOp::create(rewriter, loc, halfTy, loadOp.getBase(),
341 loadOp.getIndices());
342 Value hi = vector::LoadOp::create(rewriter, loc, halfTy, loadOp.getBase(),
343 hiIndices);
344 SmallVector<int64_t> concatMask =
345 llvm::to_vector(llvm::seq<int64_t>(0, 64));
346 rewriter.replaceOpWithNewOp<vector::ShuffleOp>(loadOp, lo, hi, concatMask);
347 return success();
348 }
349};
350
351/// Pass to split vector.load + aievec.ups chains for better performance
352struct SplitVectorLoadUpsChainsPass
353 : public PassWrapper<SplitVectorLoadUpsChainsPass, OperationPass<>> {
354 MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(SplitVectorLoadUpsChainsPass)
355
356 StringRef getArgument() const final { return "aievec-split-load-ups-chains"; }
357
358 StringRef getDescription() const final {
359 return "Split 1024-bit vector loads and stores whose halves are widened "
360 "or narrowed separately";
361 }
362
363 void getDependentDialects(DialectRegistry &registry) const override {
364 registry.insert<vector::VectorDialect, arith::ArithDialect,
365 memref::MemRefDialect, affine::AffineDialect,
366 xilinx::aievec::AIEVecDialect>();
367 }
368
369 void runOnOperation() override {
370 Operation *op = getOperation();
371 MLIRContext *context = &getContext();
372 RewritePatternSet patterns(context);
373
374 patterns
375 .add<SplitVectorLoadUpsChainPattern, SplitVectorSrsStoreChainPattern,
376 SplitVectorLoadMatMulLhsPattern>(context);
377
378 if (failed(applyPatternsGreedily(op, std::move(patterns)))) {
379 signalPassFailure();
380 }
381 }
382};
383
384} // namespace
385
386namespace xilinx {
387namespace aievec {
388
389std::unique_ptr<::mlir::Pass> createSplitVectorLoadUpsChainsPass() {
390 return std::make_unique<SplitVectorLoadUpsChainsPass>();
391}
392
393} // namespace aievec
394} // namespace xilinx
std::unique_ptr<::mlir::Pass > createSplitVectorLoadUpsChainsPass()
Create a pass that splits vector.load + aievec.ups chains to reduce shuffle operations for AIE2p targ...