CIRCT 24.0.0git
Loading...
Searching...
No Matches
LowerExtmemToHW.cpp
Go to the documentation of this file.
1//===- LowerExtmemToHW.cpp - lock functions pass ----------------*- C++ -*-===//
2//
3// Part of the LLVM Project, 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//===----------------------------------------------------------------------===//
8//
9// Contains the definitions of the lower extmem pass.
10//
11//===----------------------------------------------------------------------===//
12
22#include "mlir/Dialect/Arith/IR/Arith.h"
23#include "mlir/IR/PatternMatch.h"
24#include "mlir/Pass/Pass.h"
25#include "mlir/Rewrite/FrozenRewritePatternSet.h"
26#include "mlir/Transforms/DialectConversion.h"
27#include "llvm/Support/Debug.h"
28
29namespace circt {
30namespace handshake {
31#define GEN_PASS_DEF_HANDSHAKELOWEREXTMEMTOHW
32#include "circt/Dialect/Handshake/HandshakePasses.h.inc"
33} // namespace handshake
34} // namespace circt
35
36using namespace circt;
37using namespace handshake;
38using namespace mlir;
39namespace {
40using NamedType = std::pair<StringAttr, Type>;
41struct HandshakeMemType {
42 llvm::SmallVector<NamedType> inputTypes, outputTypes;
43 MemRefType memRefType;
44 unsigned loadPorts, storePorts;
45};
46
47struct LoadName {
48 StringAttr dataIn;
49 StringAttr addrOut;
50
51 static LoadName get(MLIRContext *ctx, unsigned idx) {
52 return {StringAttr::get(ctx, "ld" + std::to_string(idx) + ".data"),
53 StringAttr::get(ctx, "ld" + std::to_string(idx) + ".addr")};
54 }
55};
56
57struct StoreNames {
58 StringAttr doneIn;
59 StringAttr out;
60
61 static StoreNames get(MLIRContext *ctx, unsigned idx) {
62 return {StringAttr::get(ctx, "st" + std::to_string(idx) + ".done"),
63 StringAttr::get(ctx, "st" + std::to_string(idx))};
64 }
65};
66
67} // namespace
68
69static Type indexToMemAddr(Type t, MemRefType memRef) {
70 assert(isa<IndexType>(t) && "Expected index type");
71 auto shape = memRef.getShape();
72 assert(shape.size() == 1 && "Expected 1D memref");
73 unsigned addrWidth = llvm::Log2_64_Ceil(shape[0]);
74 return IntegerType::get(t.getContext(), addrWidth);
75}
76
77static HandshakeMemType getMemTypeForExtmem(Value v) {
78 auto *ctx = v.getContext();
79 assert(isa<mlir::MemRefType>(v.getType()) && "Value is not a memref type");
80 auto extmemOp = cast<handshake::ExternalMemoryOp>(*v.getUsers().begin());
81 HandshakeMemType memType;
82 llvm::SmallVector<hw::detail::FieldInfo> inFields, outFields;
83
84 // Add memory type.
85 memType.memRefType = cast<MemRefType>(v.getType());
86 memType.loadPorts = extmemOp.getLdCount();
87 memType.storePorts = extmemOp.getStCount();
88
89 // Add load ports.
90 for (auto [i, ldif] : llvm::enumerate(extmemOp.getLoadPorts())) {
91 auto names = LoadName::get(ctx, i);
92 memType.inputTypes.push_back({names.dataIn, ldif.dataOut.getType()});
93 memType.outputTypes.push_back(
94 {names.addrOut,
95 indexToMemAddr(ldif.addressIn.getType(), memType.memRefType)});
96 }
97
98 // Add store ports.
99 for (auto [i, stif] : llvm::enumerate(extmemOp.getStorePorts())) {
100 auto names = StoreNames::get(ctx, i);
101
102 // Incoming store data and address
103 llvm::SmallVector<hw::StructType::FieldInfo> storeOutFields;
104 storeOutFields.push_back(
105 {StringAttr::get(ctx, "address"),
106 indexToMemAddr(stif.addressIn.getType(), memType.memRefType)});
107 storeOutFields.push_back(
108 {StringAttr::get(ctx, "data"), stif.dataIn.getType()});
109 auto inType = hw::StructType::get(ctx, storeOutFields);
110 memType.outputTypes.push_back({names.out, inType});
111 memType.inputTypes.push_back({names.doneIn, stif.doneOut.getType()});
112 }
113
114 return memType;
115}
116
117namespace {
118struct HandshakeLowerExtmemToHWPass
119 : public circt::handshake::impl::HandshakeLowerExtmemToHWBase<
120 HandshakeLowerExtmemToHWPass> {
121
122 HandshakeLowerExtmemToHWPass(std::optional<bool> createESIWrapper) {
123 if (createESIWrapper)
124 this->createESIWrapper = *createESIWrapper;
125 }
126
127 void runOnOperation() override {
128 auto op = getOperation();
129 for (auto func : op.getOps<handshake::FuncOp>()) {
130 if (failed(lowerExtmemToHW(func))) {
131 signalPassFailure();
132 return;
133 }
134 }
135 };
136
137 LogicalResult lowerExtmemToHW(handshake::FuncOp func);
138 LogicalResult
139 wrapESI(handshake::FuncOp func, hw::ModulePortInfo origPorts,
140 const std::map<unsigned, HandshakeMemType> &argReplacements);
141};
142
143LogicalResult HandshakeLowerExtmemToHWPass::wrapESI(
145 const std::map<unsigned, HandshakeMemType> &argReplacements) {
146 auto *ctx = func.getContext();
147 OpBuilder b(func);
148 auto loc = func.getLoc();
149
150 // Create external module which will match the interface of 'func' after it's
151 // been lowered to HW.
152 b.setInsertionPoint(func);
153 auto newPortInfo = handshake::getPortInfoForOpTypes(
154 func, func.getArgumentTypes(), func.getResultTypes());
155 auto extMod = hw::HWModuleExternOp::create(
156 b, loc, StringAttr::get(ctx, "__" + func.getName() + "_hw"), newPortInfo);
157
158 // Add an attribute to the original handshake function to indicate that it
159 // needs to resolve to extMod in a later pass.
160 func->setAttr(kPredeclarationAttr,
161 FlatSymbolRefAttr::get(ctx, extMod.getName()));
162
163 // Create wrapper module. This will have the same ports as the original
164 // module, sans the replaced arguments.
165 auto wrapperModPortInfo = origPorts;
166 llvm::SmallVector<unsigned> argReplacementsIdxs;
167 llvm::transform(argReplacements, std::back_inserter(argReplacementsIdxs),
168 [](auto &pair) { return pair.first; });
169 for (auto i : llvm::reverse(argReplacementsIdxs))
170 wrapperModPortInfo.eraseInput(i);
171 auto wrapperMod = hw::HWModuleOp::create(
172 b, loc, StringAttr::get(ctx, func.getName() + "_esi_wrapper"),
173 wrapperModPortInfo);
174 Value clk = wrapperMod.getBodyBlock()->getArgument(
175 wrapperMod.getBodyBlock()->getNumArguments() - 2);
176 Value rst = wrapperMod.getBodyBlock()->getArgument(
177 wrapperMod.getBodyBlock()->getNumArguments() - 1);
178 SmallVector<Value> clkRes = {clk, rst};
179
180 b.setInsertionPointToStart(wrapperMod.getBodyBlock());
181 BackedgeBuilder bb(b, loc);
182
183 // Create backedges for the results of the external module. These will be
184 // replaced by the service instance requests if associated with a memory.
185 llvm::SmallVector<Backedge> backedges;
186 for (auto resType : extMod.getOutputTypes())
187 backedges.push_back(bb.get(resType));
188
189 // Maintain which index we're currently at in the lowered handshake module's
190 // return.
191 unsigned resIdx = origPorts.sizeOutputs();
192
193 // Maintain the arguments which each memory will add to the inner module
194 // instance.
195 llvm::SmallVector<llvm::SmallVector<Value>> instanceArgsForMem;
196
197 for (auto [i, memType] : argReplacements) {
198
199 b.setInsertionPoint(wrapperMod);
200 // Create a memory service declaration for each memref argument that was
201 // served.
202 auto origPortInfo = origPorts.atInput(i);
203 auto memrefShape = memType.memRefType.getShape();
204 auto dataType = memType.memRefType.getElementType();
205 assert(memrefShape.size() == 1 && "Only 1D memrefs are supported");
206 unsigned memrefSize = memrefShape[0];
207 auto memServiceDecl = esi::RandomAccessMemoryDeclOp::create(
208 b, loc, origPortInfo.name, /*sym_visibility=*/{},
209 TypeAttr::get(dataType), b.getI64IntegerAttr(memrefSize));
210 esi::ServicePortInfo writePortInfo = memServiceDecl.writePortInfo();
211 esi::ServicePortInfo readPortInfo = memServiceDecl.readPortInfo();
212
213 SmallVector<Value> instanceArgsFromThisMem;
214
215 // Create service requests. This MUST follow the order of which ports were
216 // added in other parts of this pass (load ports first, then store ports).
217 b.setInsertionPointToStart(wrapperMod.getBodyBlock());
218
219 // Load ports:
220 for (unsigned i = 0; i < memType.loadPorts; ++i) {
221 auto req = esi::RequestConnectionOp::create(
222 b, loc, readPortInfo.type, readPortInfo.port,
223 esi::AppIDAttr::get(ctx, b.getStringAttr("load"), resIdx),
224 /*options=*/DictionaryAttr());
225 auto reqUnpack = esi::UnpackBundleOp::create(
226 b, loc, req.getToClient(), ValueRange{backedges[resIdx]});
227 instanceArgsFromThisMem.push_back(
228 reqUnpack.getToChannels()
229 [esi::RandomAccessMemoryDeclOp::RespDirChannelIdx]);
230 ++resIdx;
231 }
232
233 // Store ports:
234 for (unsigned i = 0; i < memType.storePorts; ++i) {
235 auto req = esi::RequestConnectionOp::create(
236 b, loc, writePortInfo.type, writePortInfo.port,
237 esi::AppIDAttr::get(ctx, b.getStringAttr("store"), resIdx),
238 /*options=*/DictionaryAttr());
239 auto reqUnpack = esi::UnpackBundleOp::create(
240 b, loc, req.getToClient(), ValueRange{backedges[resIdx]});
241 instanceArgsFromThisMem.push_back(
242 reqUnpack.getToChannels()
243 [esi::RandomAccessMemoryDeclOp::RespDirChannelIdx]);
244 ++resIdx;
245 }
246
247 instanceArgsForMem.emplace_back(std::move(instanceArgsFromThisMem));
248 }
249
250 // Stitch together arguments from the top-level ESI wrapper and the instance
251 // arguments generated from the service requests.
252 llvm::SmallVector<Value> instanceArgs;
253
254 // Iterate over the arguments of the original handshake.func and determine
255 // whether to grab operands from the arg replacements or the wrapper module.
256 unsigned wrapperArgIdx = 0;
257
258 for (unsigned i = 0, e = func.getNumArguments(); i < e; i++) {
259 // Arg replacement indices refer to the original handshake.func argument
260 // index.
261 if (argReplacements.count(i)) {
262 // This index was originally a memref - pop the instance arguments for the
263 // next-in-line memory and add them.
264 auto &memArgs = instanceArgsForMem.front();
265 instanceArgs.append(memArgs.begin(), memArgs.end());
266 instanceArgsForMem.erase(instanceArgsForMem.begin());
267 } else {
268 // Add the argument from the wrapper mod. This is maintained by its own
269 // counter (memref arguments are removed, so if there was an argument at
270 // this point, it needs to come from the wrapper module).
271 instanceArgs.push_back(
272 wrapperMod.getBodyBlock()->getArgument(wrapperArgIdx++));
273 }
274 }
275
276 // Add any missing arguments from the wrapper module (this will be clock and
277 // reset)
278 for (; wrapperArgIdx < wrapperMod.getBodyBlock()->getNumArguments();
279 ++wrapperArgIdx)
280 instanceArgs.push_back(
281 wrapperMod.getBodyBlock()->getArgument(wrapperArgIdx));
282
283 // Instantiate the inner module.
284 auto instance =
285 hw::InstanceOp::create(b, loc, extMod, func.getName(), instanceArgs);
286
287 // And resolve the backedges.
288 for (auto [res, be] : llvm::zip(instance.getResults(), backedges))
289 be.setValue(res);
290
291 // Finally, grab the (non-memory) outputs from the inner module and return
292 // them through the wrapper.
293 auto outputOp =
294 cast<hw::OutputOp>(wrapperMod.getBodyBlock()->getTerminator());
295 b.setInsertionPoint(outputOp);
296 hw::OutputOp::create(
297 b, outputOp.getLoc(),
298 instance.getResults().take_front(wrapperMod.getNumOutputPorts()));
299 outputOp.erase();
300
301 return success();
302}
303
304// Truncates the index-typed 'v' into an integer-type of the same width as the
305// 'memref' argument.
306// Uses arith operations since these are supported in the HandshakeToHW
307// lowering.
308static Value truncateToMemoryWidth(Location loc, OpBuilder &b, Value v,
309 MemRefType memRefType) {
310 assert(isa<IndexType>(v.getType()) && "Expected an index-typed value");
311 auto addrWidth = llvm::Log2_64_Ceil(memRefType.getShape().front());
312 if (addrWidth == 0) {
313 // Arith doesn't support i0, just create a constant i0 with control
314 // dependency on the value.
315 auto ctrl = handshake::JoinOp::create(b, loc, v).getResult();
316 return handshake::ConstantOp::create(
317 b, loc, b.getIntegerType(0), b.getIntegerAttr(b.getIntegerType(0), 0),
318 ctrl);
319 }
320 return arith::IndexCastOp::create(b, loc, b.getIntegerType(addrWidth), v);
321}
322
323static Value plumbLoadPort(Location loc, OpBuilder &b,
324 handshake::MemLoadInterface &ldif, Value loadData,
325 MemRefType memrefType) {
326 // We need to feed both the load data and the load done outputs.
327 // Fork the extracted load data into two, and 'join' the second one to
328 // generate a none-typed output to drive the load done.
329 auto dataFork = ForkOp::create(b, loc, loadData, 2);
330
331 auto dataOut = dataFork.getResult()[0];
332 llvm::SmallVector<Value> joinArgs = {dataFork.getResult()[1]};
333 auto dataDone = JoinOp::create(b, loc, joinArgs);
334
335 ldif.dataOut.replaceAllUsesWith(dataOut);
336 ldif.doneOut.replaceAllUsesWith(dataDone);
337
338 // Return load address, to be fed to the top-level output, truncated to the
339 // width of the memory that is accessed.
340 return truncateToMemoryWidth(loc, b, ldif.addressIn, memrefType);
341}
342
343static Value plumbStorePort(Location loc, OpBuilder &b,
344 handshake::MemStoreInterface &stif, Value done,
345 Type outType, MemRefType memrefType) {
346 stif.doneOut.replaceAllUsesWith(done);
347 // Return the store address and data to be fed to the top-level output.
348 // Address is truncated to the width of the memory that is accessed.
349 llvm::SmallVector<Value> structArgs = {
350 truncateToMemoryWidth(loc, b, stif.addressIn, memrefType), stif.dataIn};
351
352 return hw::StructCreateOp::create(b, loc, cast<hw::StructType>(outType),
353 structArgs)
354 .getResult();
355}
356
357static void appendToStringArrayAttr(Operation *op, StringRef attrName,
358 StringRef attrVal) {
359 auto *ctx = op->getContext();
360 llvm::SmallVector<Attribute> newArr;
361 llvm::copy(op->getAttrOfType<ArrayAttr>(attrName).getValue(),
362 std::back_inserter(newArr));
363 newArr.push_back(StringAttr::get(ctx, attrVal));
364 op->setAttr(attrName, ArrayAttr::get(ctx, newArr));
365}
366
367static void insertInStringArrayAttr(Operation *op, StringRef attrName,
368 StringRef attrVal, unsigned idx) {
369 auto *ctx = op->getContext();
370 llvm::SmallVector<Attribute> newArr;
371 llvm::copy(op->getAttrOfType<ArrayAttr>(attrName).getValue(),
372 std::back_inserter(newArr));
373 newArr.insert(newArr.begin() + idx, StringAttr::get(ctx, attrVal));
374 op->setAttr(attrName, ArrayAttr::get(ctx, newArr));
375}
376
377static void eraseFromArrayAttr(Operation *op, StringRef attrName,
378 unsigned idx) {
379 auto *ctx = op->getContext();
380 llvm::SmallVector<Attribute> newArr;
381 llvm::copy(op->getAttrOfType<ArrayAttr>(attrName).getValue(),
382 std::back_inserter(newArr));
383 newArr.erase(newArr.begin() + idx);
384 op->setAttr(attrName, ArrayAttr::get(ctx, newArr));
385}
386
387struct ArgTypeReplacement {
388 unsigned index;
389 TypeRange ins;
390 TypeRange outs;
391};
392
393LogicalResult
394HandshakeLowerExtmemToHWPass::lowerExtmemToHW(handshake::FuncOp func) {
395 // Gather memref ports to be converted. This is an ordered map, and will be
396 // iterated from lo to hi indices.
397 std::map<unsigned, Value> memrefArgs;
398 for (auto [i, arg] : llvm::enumerate(func.getArguments()))
399 if (isa<MemRefType>(arg.getType()))
400 memrefArgs[i] = arg;
401
402 if (memrefArgs.empty())
403 return success(); // nothing to do.
404
405 // Record which arg indices were replaces with handshake memory ports.
406 // This is an ordered map, and will be iterated from lo to hi indices.
407 std::map<unsigned, HandshakeMemType> argReplacements;
408
409 // Record the hw.module i/o of the original func (used for ESI wrapper).
410 auto origPortInfo = handshake::getPortInfoForOpTypes(
411 func, func.getArgumentTypes(), func.getResultTypes());
412
413 OpBuilder b(func);
414 for (auto it : memrefArgs) {
415 // Do not use structured bindings for 'it' - cannot reference inside lambda.
416 unsigned i = it.first;
417 auto arg = it.second;
418 auto loc = arg.getLoc();
419 // Get the attached extmemory external module.
420 auto extmemOp = cast<handshake::ExternalMemoryOp>(*arg.getUsers().begin());
421 b.setInsertionPoint(extmemOp);
422
423 // Add memory input - this is the output of the extmemory op.
424 auto memIOTypes = getMemTypeForExtmem(arg);
425 MemRefType memrefType = cast<MemRefType>(arg.getType());
426
427 auto oldReturnOp =
428 cast<handshake::ReturnOp>(func.getBody().front().getTerminator());
429 llvm::SmallVector<Value> newReturnOperands = oldReturnOp.getOperands();
430 unsigned addedInPorts = 0;
431 auto memName = func.getArgName(i);
432 auto addArgRes = [&](unsigned id, NamedType &argType,
433 NamedType &resType) -> FailureOr<Value> {
434 // Function argument
435 unsigned newArgIdx = i + addedInPorts;
436 if (failed(
437 func.insertArgument(newArgIdx, argType.second, {}, arg.getLoc())))
438 return failure();
439 insertInStringArrayAttr(func, "argNames",
440 memName.str() + "_" + argType.first.str(),
441 newArgIdx);
442 auto newInPort = func.getArgument(newArgIdx);
443 ++addedInPorts;
444
445 // Function result.
446 if (failed(func.insertResult(func.getNumResults(), resType.second, {})))
447 return failure();
448 appendToStringArrayAttr(func, "resNames",
449 memName.str() + "_" + resType.first.str());
450 return newInPort;
451 };
452
453 // Plumb load ports.
454 unsigned portIdx = 0;
455 for (auto loadPort : extmemOp.getLoadPorts()) {
456 auto newInPort = addArgRes(loadPort.index, memIOTypes.inputTypes[portIdx],
457 memIOTypes.outputTypes[portIdx]);
458 if (failed(newInPort))
459 return failure();
460 newReturnOperands.push_back(
461 plumbLoadPort(loc, b, loadPort, *newInPort, memrefType));
462 ++portIdx;
463 }
464
465 // Plumb store ports.
466 for (auto storePort : extmemOp.getStorePorts()) {
467 auto newInPort =
468 addArgRes(storePort.index, memIOTypes.inputTypes[portIdx],
469 memIOTypes.outputTypes[portIdx]);
470 if (failed(newInPort))
471 return failure();
472 newReturnOperands.push_back(
473 plumbStorePort(loc, b, storePort, *newInPort,
474 memIOTypes.outputTypes[portIdx].second, memrefType));
475 ++portIdx;
476 }
477
478 // Replace the return op of the function with a new one that returns the
479 // memory output struct.
480 b.setInsertionPoint(oldReturnOp);
481 ReturnOp::create(b, arg.getLoc(), newReturnOperands);
482 oldReturnOp.erase();
483
484 // Erase the extmemory operation since I/O plumbing has replaced all of its
485 // results.
486 extmemOp.erase();
487
488 // Erase the original memref argument of the top-level i/o now that it's
489 // use has been removed.
490 if (failed(func.eraseArgument(i + addedInPorts)))
491 return failure();
492 eraseFromArrayAttr(func, "argNames", i + addedInPorts);
493
494 argReplacements[i] = memIOTypes;
495 }
496
497 if (createESIWrapper)
498 if (failed(wrapESI(func, origPortInfo, argReplacements)))
499 return failure();
500
501 return success();
502}
503
504} // namespace
505
506std::unique_ptr<mlir::Pass>
508 std::optional<bool> createESIWrapper) {
509 return std::make_unique<HandshakeLowerExtmemToHWPass>(createESIWrapper);
510}
assert(baseType &&"element must be base type")
static Type indexToMemAddr(Type t, MemRefType memRef)
static HandshakeMemType getMemTypeForExtmem(Value v)
false return failure()
Parse an optional keyword or string and set instance into 'result'.
return success()
create(elements, Type result_type=None)
Definition hw.py:547
Direction get(bool isOutput)
Returns an output direction if isOutput is true, otherwise returns an input direction.
Definition CalyxOps.cpp:56
std::unique_ptr< mlir::Pass > createHandshakeLowerExtmemToHWPass(std::optional< bool > createESIWrapper={})
The InstanceGraph op interface, see InstanceGraphInterface.td for more details.
ChannelBundleType type
Definition ESIOps.h:40
hw::InnerRefAttr port
Definition ESIOps.h:39
This holds a decoded list of input/inout and output ports for a module or instance.
PortInfo & atInput(size_t idx)