MLIR-AIE
AIECreateCores.cpp
Go to the documentation of this file.
1//===- AIECreateCores.cpp ---------------------------------------*- C++ -*-===//
2//
3// This file is licensed under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7// (c) Copyright 2019 Xilinx Inc.
8//
9//===----------------------------------------------------------------------===//
10
14
15#include "mlir/Dialect/Arith/IR/Arith.h"
16#include "mlir/Dialect/Func/IR/FuncOps.h"
17#include "mlir/IR/Attributes.h"
18#include "mlir/IR/IRMapping.h"
19#include "mlir/IR/PatternMatch.h"
20#include "mlir/Pass/Pass.h"
21#include "mlir/Tools/mlir-translate/MlirTranslateMain.h"
22#include "mlir/Transforms/DialectConversion.h"
23
24namespace xilinx::AIEX {
25#define GEN_PASS_DEF_AIECREATECORES
26#include "aie/Dialect/AIEX/Transforms/AIEXPasses.h.inc"
27} // namespace xilinx::AIEX
28
29using namespace mlir;
30using namespace xilinx;
31using namespace xilinx::AIE;
32using namespace xilinx::AIEX;
33
34struct RemoveAIEFuncs : public OpConversionPattern<func::FuncOp> {
36 DenseMap<func::FuncOp, std::pair<int, int>> &funcs;
37
38 RemoveAIEFuncs(MLIRContext *context,
39 DenseMap<func::FuncOp, std::pair<int, int>> &funcs,
40 PatternBenefit benefit = 1)
41 : OpConversionPattern<func::FuncOp>(context, benefit), funcs(funcs) {}
42
43 LogicalResult
44 matchAndRewrite(func::FuncOp op, OpAdaptor adaptor,
45 ConversionPatternRewriter &rewriter) const override {
46 Operation *Op = op.getOperation();
47 if (funcs.find(op) == funcs.end())
48 return failure();
49
50 rewriter.eraseOp(Op);
51 return success();
52 }
53};
54
55struct RemoveAIECalls : public OpConversionPattern<func::CallOp> {
57
58 RemoveAIECalls(MLIRContext *context, PatternBenefit benefit = 1)
59 : OpConversionPattern<func::CallOp>(context, benefit) {}
60
61 LogicalResult
62 matchAndRewrite(func::CallOp op, OpAdaptor adaptor,
63 ConversionPatternRewriter &rewriter) const override {
64 Operation *Op = op.getOperation();
65 if (!op->getAttr("aie.x") || !op->getAttr("aie.y"))
66 return failure();
67
68 rewriter.eraseOp(Op);
69 return success();
70 }
71};
72
74 : public xilinx::AIEX::impl::AIECreateCoresBase<AIECreateCoresPass> {
75 void runOnOperation() override {
76
77 DeviceOp device = getOperation();
78 OpBuilder builder = OpBuilder::atBlockTerminator(device.getBody());
79
80 DenseMap<TileID, Operation *> tiles;
81 DenseMap<Operation *, CoreOp> cores;
82 DenseMap<Operation *, MemOp> mems;
83 DenseMap<Value, Value> buffers;
84 DenseMap<func::FuncOp, std::pair<int, int>> funcs;
85
86 // Collect existing TileOps
87 for (auto tile : device.getOps<TileOp>()) {
88 int colIndex = tile.colIndex();
89 int rowIndex = tile.rowIndex();
90 tiles[{colIndex, rowIndex}] = tile;
91 }
92
93 // Bind FuncOp to an AIE core based on attributes of the CallOp
94 // A CoreOp will be created for the core, and the FuncOp body is cloned
95 // to the CoreOp region
96 for (auto callOp : device.getOps<func::CallOp>()) {
97 if (!callOp->getAttr("aie.x") || !callOp->getAttr("aie.y"))
98 continue;
99
100 SmallVector<Value, 4> callOperands(callOp.getArgOperands());
101 SmallVector<std::pair<MemRefType, int>, 4> coreBufTypes;
102
103 int colIndex = callOp->getAttrOfType<IntegerAttr>("aie.x").getInt();
104 int rowIndex = callOp->getAttrOfType<IntegerAttr>("aie.y").getInt();
105
106 // get or create TileOp
107 if (!tiles[{colIndex, rowIndex}]) {
108 builder.setInsertionPointToStart(device.getBody());
109 TileOp tile = TileOp::create(builder, builder.getUnknownLoc(), colIndex,
110 rowIndex);
111 tiles[{colIndex, rowIndex}] = tile;
112 }
113 Operation *tileOp = tiles[{colIndex, rowIndex}];
114 TileOp tile = dyn_cast<TileOp>(tileOp);
115 builder.setInsertionPointAfter(tileOp);
116
117 // create MemOp
118 if (!mems[tileOp]) {
119 for (unsigned i = 0; i < callOperands.size(); i++) {
120 Value operand = callOperands[i]; // Should be produced by an AllocOp
121 MemRefType t = nullptr;
122 if (llvm::isa<MemRefType>(operand.getType())) {
123 t = llvm::cast<MemRefType>(operand.getType());
124 } else if (operand.getType().isIntOrFloat()) {
125 // promote scalar type to memref type
126 int64_t shape[1] = {1};
127 t = MemRefType::get(shape, operand.getType());
128 }
129
130 assert(t && "Unsupported type!");
131 coreBufTypes.push_back({t, i});
132 BufferOp buf = BufferOp::create(
133 builder, builder.getUnknownLoc(), t, tile, /*sym_name*/ nullptr,
134 /*address*/ nullptr, /*initial_value*/ nullptr,
135 /*mem_bank*/ nullptr);
136 buffers[callOperands[i]] = buf;
137 operand.replaceAllUsesWith(buf.getResult());
138 }
139
140 MemOp mem = MemOp::create(builder, builder.getUnknownLoc(),
141 builder.getIndexType(), tile);
142 Region &r = mem.getBody();
143 Block *endBlock = builder.createBlock(&r);
144
145 // block terminator
146 builder.setInsertionPointToStart(endBlock);
147 EndOp::create(builder, builder.getUnknownLoc());
148 mems[tileOp] = mem;
149 }
150
151 // create CoreOp with buffer reference
152 if (CallOpInterface call =
153 dyn_cast<CallOpInterface>(callOp.getOperation())) {
154 Operation *callable = call.resolveCallable();
155 if (func::FuncOp func = dyn_cast<func::FuncOp>(callable)) {
156 funcs[func] = {colIndex, rowIndex};
157
158 IRMapping mapper;
159
160 builder.setInsertionPoint(callOp);
161
162 CoreOp core;
163 Block *currentBlock;
164
165 if (!cores[tileOp]) {
166 core = CoreOp::create(builder, builder.getUnknownLoc(), tile);
167 Region &r = core.getBody();
168 currentBlock = builder.createBlock(&r);
169 builder.setInsertionPointToStart(currentBlock);
170 } else {
171 core = cores[tileOp];
172 currentBlock = &core.getBody().back();
173 builder.setInsertionPoint(currentBlock->getTerminator());
174 }
175
176 // Mapping between function arguments (FuncOp) and AIE buffers
177 // (CoreOp) We will create one buffer for each function argument If
178 // the function argument's type is a scalar, we promote it to a
179 // one-element memref, and do a load to the buffer at index 0
180 for (auto pair : coreBufTypes) {
181 int operandID = pair.second;
182 Value arg = func.getArgument(operandID);
183 Value buf = buffers[callOperands[operandID]];
184 if (arg.getType().isIntOrFloat()) {
185 assert(pair.first.getShape().size() == 1 &&
186 "Expected MemRefType of shape 1");
187 assert(pair.first.getShape()[0] == 1 &&
188 "Expected MemRefType of single element");
189
190 Value zero = arith::ConstantIndexOp::create(
191 builder, builder.getUnknownLoc(), 0);
192 auto loadOp = memref::LoadOp::create(
193 builder, builder.getUnknownLoc(), arg.getType(), buf, zero);
194 mapper.map(arg, loadOp);
195 } else {
196 mapper.map(arg, buf);
197 }
198 }
199
200 // Clone ops from the original function to CoreOp's body
201 for (auto &childOp : func.getCallableRegion()->getOps()) {
202 // skip ReturnOp since it lives only within a funcOp
203 if (auto returnOp = dyn_cast<func::ReturnOp>(childOp))
204 continue;
205
206 builder.clone(childOp, mapper);
207 }
208 if (!cores[tileOp]) {
209 // block terminator
210 EndOp::create(builder, builder.getUnknownLoc());
211 cores[tileOp] = core;
212 }
213 }
214 }
215 }
216
217 // Setup FlowOps
218 // Since memcpy moves data from one memory module to another, we use
219 // WireBundle::DMA for both the source and the destination In addition, we
220 // only have two DMA ports per each direction (MM2S/S2MM), and in a
221 // circuit-switch mode, dest port/channel sharing is not possible.
222 // Therefore, we will generate error if the number of logical flows
223 // (streams) targeting the same destination (S2MM) is more than 2
224 // DenseMap<Value, int> destChannel;
225 // for (auto op : device.getOps<MemcpyOp>()) {
226 // builder.setInsertionPoint(op);
227 // TileOp srcTile = dyn_cast<TileOp>(op.srcTile().getDefiningOp());
228 // TileOp dstTile = dyn_cast<TileOp>(op.dstTile().getDefiningOp());
229 // // TODO: perhaps a better approach is to not assert here, but rather
230 // have a subsequent pass
231 // // that legally relocates the ports
232 // assert(destChannel[op.dstTile()] <= 2 &&
233 // "Could not allocate more than two dest. channel when creating
234 // FlowOp");
235 // // WireBundle[1] = DMA
236 // FlowOp::create(builder, builder.getUnknownLoc(), srcTile, 1, 0,
237 // dstTile, 1, destChannel[op.dstTile()]); destChannel[op.dstTile()]++;
238 // }
239
240 ConversionTarget target(getContext());
241 RewritePatternSet patterns(&getContext());
242 target.addLegalOp<DMAStartOp>();
243 target.addLegalOp<DMABDOp>();
244 target.addLegalOp<UseTokenOp>();
245 target.addLegalOp<NextBDOp>();
246
247 // Remove standard CallOps and FuncOps that are bound to AIE CoreOps
248 patterns.insert<RemoveAIECalls>(device.getContext());
249 patterns.insert<RemoveAIEFuncs>(device.getContext(), funcs);
250
251 if (failed(applyPartialConversion(device, target, std::move(patterns))))
252 signalPassFailure();
253 }
254};
255
256std::unique_ptr<OperationPass<DeviceOp>>
258 return std::make_unique<AIECreateCoresPass>();
259}
std::unique_ptr< mlir::OperationPass< AIE::DeviceOp > > createAIECreateCoresPass()
Include the generated interface declarations.
void runOnOperation() override
RemoveAIECalls(MLIRContext *context, PatternBenefit benefit=1)
LogicalResult matchAndRewrite(func::CallOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const override
RemoveAIEFuncs(MLIRContext *context, DenseMap< func::FuncOp, std::pair< int, int > > &funcs, PatternBenefit benefit=1)
LogicalResult matchAndRewrite(func::FuncOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const override
DenseMap< func::FuncOp, std::pair< int, int > > & funcs