CIRCT 24.0.0git
Loading...
Searching...
No Matches
OutlineArcs.cpp
Go to the documentation of this file.
1//===----------------------------------------------------------------------===//
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
15#include "mlir/Analysis/TopologicalSortUtils.h"
16#include "mlir/IR/SymbolTable.h"
17#include "mlir/Pass/Pass.h"
18#include "llvm/ADT/DenseMap.h"
19#include "llvm/ADT/SetVector.h"
20
21#define DEBUG_TYPE "arc-outline"
22
23namespace circt {
24namespace arc {
25#define GEN_PASS_DEF_OUTLINEARCSPASS
26#include "circt/Dialect/Arc/ArcPasses.h.inc"
27} // namespace arc
28} // namespace circt
29
30using namespace circt;
31using namespace arc;
32using namespace mlir;
33using llvm::BumpPtrAllocator;
34using llvm::SmallDenseSet;
35
36namespace {
37struct OutlineArcsPass
38 : public arc::impl::OutlineArcsPassBase<OutlineArcsPass> {
39 void runOnOperation() override;
40 friend struct Colorer;
41 friend struct Outliner;
42};
43} // namespace
44
45static bool isArcBreaker(Operation *op) {
46 return isa<ClockedOpInterface, seq::Clocked, CallOp>(op) ||
47 op->hasTrait<OpTrait::IsTerminator>() ||
48 op->hasTrait<OpTrait::ConstantLike>() ||
49 !mlir::isMemoryEffectFree(op) ||
50 // `llhd.prb` changes memory effects based on its parent op. Yikes.
51 isa<llhd::ProbeOp>(op);
52}
53
54//===----------------------------------------------------------------------===//
55// Coloring
56//===----------------------------------------------------------------------===//
57
58namespace {
59
60/// A color assigned to an operation or arc-breaking operand.
61struct Color {
62 /// A unique identifier for this color. Mainly for debugging and picking names
63 /// for outlined arcs.
64 unsigned index;
65 /// A unique and sorted list of terminal colors this color feeds into. A
66 /// terminal is an `OpOperand` on an arc-breaking operation.
67 ArrayRef<unsigned> terminals;
68};
69
70static llvm::raw_ostream &operator<<(llvm::raw_ostream &os,
71 const Color &color) {
72 os << "#" << color.index << "[";
73 llvm::interleaveComma(color.terminals, os);
74 os << "]";
75 return os;
76}
77
78/// Special handling for the set interning `Color` instances.
79struct ColorInfo : DenseMapInfo<Color *> {
80 static unsigned getHashValue(const Color *color) {
81 return llvm::hash_combine_range(color->terminals);
82 }
83 static unsigned getHashValue(ArrayRef<unsigned> terminals) {
84 return llvm::hash_combine_range(terminals);
85 }
86 static bool isEqual(const Color *lhs, const Color *rhs) {
87 return lhs == rhs || lhs->terminals == rhs->terminals;
88 }
89 static bool isEqual(ArrayRef<unsigned> lhs, const Color *rhs) {
90 return lhs == rhs->terminals;
91 }
92};
93
94/// A table of interned colors.
95struct ColorTable {
96 DenseSet<Color *, ColorInfo> colors;
97 BumpPtrAllocator alloc;
98
99 Color *get(ArrayRef<unsigned> terminals) {
100 // Check if we already have a color for these terminals.
101 if (auto it = colors.find_as(terminals); it != colors.end())
102 return *it;
103
104 // Otherwise allocate a new color.
105 auto *color = new (alloc) Color{colors.size(), terminals.copy(alloc)};
106 colors.insert(color);
107 return color;
108 }
109};
110
111/// The result of coloring the ops in a module.
112struct Coloring {
113 /// The color allocator and interning table.
114 ColorTable table;
115 /// The colors assigned to each non-arc-breaking operation.
116 DenseMap<Operation *, Color *> colors;
117};
118
119/// A helper struct that colors the ops in a module.
120struct Colorer {
121 Colorer(hw::HWModuleOp moduleOp, OutlineArcsPass &pass)
122 : moduleOp(moduleOp), pass(pass) {}
123
124 Coloring run();
125 void colorOpFanout(Operation *op);
126 bool updateOpColor(Operation *op);
127
128 hw::HWModuleOp moduleOp;
129 OutlineArcsPass &pass;
130
131 /// The resulting coloring.
132 Coloring coloring;
133 /// The terminal IDs assigned to operands of arc-breaking ops.
134 DenseMap<OpOperand *, unsigned> terminals;
135
136 /// The worklist used to color operations.
137 struct WorklistItem {
138 Operation *op;
139 Operation::use_iterator use, end;
140 };
141 SmallVector<WorklistItem> worklist;
142
143 /// A worklist of operations that require a lattice-based fixup after the
144 /// initial DFS color assignment. This allows us to properly color loops which
145 /// the DFS part misses.
146 SetVector<Operation *> dirtyOps;
147};
148
149} // namespace
150
151/// Color the operations in the module.
152Coloring Colorer::run() {
153 // Perform a first depth-first traversal along the op user chain.
154 for (auto &op : *moduleOp.getBodyBlock())
155 if (!isArcBreaker(&op))
156 colorOpFanout(&op);
157
158 // To support cycles, perform a lattice-based color update pass until all op
159 // colors stabilize.
160 while (!dirtyOps.empty()) {
161 auto *op = dirtyOps.pop_back_val();
162 pass.numFixups++;
163 if (updateOpColor(op)) {
164 op->walk([&](Operation *op) {
165 for (auto operand : op->getOperands())
166 if (auto *defOp = operand.getDefiningOp())
167 if (defOp->getBlock() == moduleOp.getBodyBlock())
168 if (!isArcBreaker(defOp))
169 dirtyOps.insert(defOp);
170 });
171 }
172 }
173
174 return std::move(coloring);
175}
176
177/// Color an operation and transitively all its users.
178void Colorer::colorOpFanout(Operation *op) {
179 // Assign a sentinel color to the op such that we can detect recursion. If the
180 // insertion fails, we already have a color for the op.
181 if (!coloring.colors.insert({op, nullptr}).second)
182 return;
183
184 // Perform a depth-first traversal of the op's users and color each. An op's
185 // color is the union of the arc-breaking op operands in its fanout.
186 worklist.push_back({op, op->use_begin(), op->use_end()});
187 while (!worklist.empty()) {
188 auto &item = worklist.back();
189 if (item.use == item.end) {
190 // All uses are colored. Time to color this op and pop it off the
191 // worklist.
192 updateOpColor(item.op);
193 worklist.pop_back();
194 } else {
195 // Lookup the user op and advance the use iterator. If the user is in a
196 // nested op, zip up to the parent op that sits directly in the module.
197 auto *userOp = (item.use++)->getOwner();
198 userOp = moduleOp.getBodyBlock()->findAncestorOpInBlock(*userOp);
199
200 // Push the user op onto the worklist if it isn't an arc-breaking op and
201 // the op hasn't already been colored (or has a sentinel null color to
202 // break cycles).
203 if (!isArcBreaker(userOp))
204 if (coloring.colors.insert({userOp, nullptr}).second)
205 worklist.push_back({userOp, userOp->use_begin(), userOp->use_end()});
206 }
207 }
208}
209
210/// Update the color of an operation based on the colors of its users. Returns
211/// true if the color changed, false otherwise.
212bool Colorer::updateOpColor(Operation *op) {
213 // Compute the union of all terminals in the op's users.
214 SmallDenseSet<unsigned, 8> fanoutTerminals;
215 for (auto &use : op->getUses()) {
216 // If the user is in a nested op, zip up to the parent op that sits directly
217 // in the module.
218 auto *userOp = use.getOwner();
219 userOp = moduleOp.getBodyBlock()->findAncestorOpInBlock(*userOp);
220
221 if (isArcBreaker(userOp)) {
222 // The use is an operand of an arc-breaking op. Add that operand's unique
223 // terminal ID to the set.
224 auto terminal = terminals.insert({&use, terminals.size()}).first->second;
225 fanoutTerminals.insert(terminal);
226 } else if (auto *color = coloring.colors.at(userOp)) {
227 // Add the user op's terminals.
228 fanoutTerminals.insert_range(color->terminals);
229 } else {
230 // If we get here we've hit a null color sentinel indicating that this op
231 // is on a cycle. Mark it as dirty such that its color gets updated during
232 // the lattice-based fixup after the initial DFS coloring.
233 dirtyOps.insert(op);
234 }
235 }
236
237 // Sort the terminals such that we can assign them a color.
238 SmallVector<unsigned, 8> sortedTerminals;
239 sortedTerminals.reserve(fanoutTerminals.size());
240 sortedTerminals.append(fanoutTerminals.begin(), fanoutTerminals.end());
241 llvm::sort(sortedTerminals);
242
243 // Turn the list of terminals into a color and update the op's color.
244 auto &color = coloring.colors[op];
245 auto *oldColor = color;
246 color = coloring.table.get(sortedTerminals);
247 pass.numUpdates++;
248 if (color != oldColor)
249 return true;
250 pass.numVacuousUpdates++;
251 return false;
252}
253
254//===----------------------------------------------------------------------===//
255// Outlining
256//===----------------------------------------------------------------------===//
257
258namespace {
259
260/// An outlined set of operations.
261struct OutlinedArc {
262 unsigned index;
263 std::unique_ptr<Block> block;
264 SmallVector<OpOperand *> forwardRefs;
265 OutputOp terminator;
266 CallOp call;
267};
268
269/// A helper struct to outline the colored ops in a module.
270struct Outliner {
271 Outliner(hw::HWModuleOp moduleOp, SymbolTable &symbolTable,
272 Coloring &coloring, OutlineArcsPass &pass)
273 : moduleOp(moduleOp), symbolTable(symbolTable), coloring(coloring),
274 pass(pass) {}
275
276 void run();
277 void outlineOp(Operation *op);
278 void sortAndBreakCycles(OutlinedArc &arc);
279 void createTerminator(OutlinedArc &arc);
280 void createAndCallArc(OutlinedArc &arc, OpBuilder &arcBuilder,
281 OpBuilder &callBuilder);
282 void useCallResults(OutlinedArc &arc);
283
284 hw::HWModuleOp moduleOp;
285 SymbolTable &symbolTable;
286 Coloring &coloring;
287 OutlineArcsPass &pass;
288
289 /// The outlined block for each color.
290 MapVector<Color *, OutlinedArc> arcs;
291};
292
293} // namespace
294
295void Outliner::run() {
296 // Move all colored ops into a dedicated block for each color.
297 for (auto &op : llvm::make_early_inc_range(*moduleOp.getBodyBlock()))
298 if (!isArcBreaker(&op))
299 outlineOp(&op);
300 pass.numArcs += arcs.size();
301
302 // Sort the ops in outlined blocks and take note of any cycles.
303 for (auto &[color, arc] : arcs)
304 sortAndBreakCycles(arc);
305
306 // Add a terminator to each block.
307 for (auto &[color, arc] : arcs)
308 createTerminator(arc);
309
310 // Create a call in the module body for each outlined block.
311 OpBuilder arcBuilder(moduleOp);
312 OpBuilder callBuilder(moduleOp.getBodyBlock()->getTerminator());
313 for (auto &[color, arc] : arcs)
314 createAndCallArc(arc, arcBuilder, callBuilder);
315
316 // Replace any direct uses of results in an arc with the corresponding call
317 // op's result.
318 for (auto &[color, arc] : arcs)
319 useCallResults(arc);
320}
321
322/// Move the given operation into the outlined arc for its color.
323void Outliner::outlineOp(Operation *op) {
324 auto *color = coloring.colors.at(op);
325 auto &arc = arcs[color];
326 if (!arc.block) {
327 arc.index = arcs.size() - 1;
328 arc.block = std::make_unique<Block>();
329 LLVM_DEBUG(llvm::dbgs() << "- Create arc " << arc.index << " for color "
330 << *color << "\n");
331 }
332 op->moveBefore(arc.block.get(), arc.block->end());
333}
334
335/// Sort the ops in an outlined block topologically and note any uses of a
336/// result before the defining op. We'll later move the cycle outside the arc
337/// such that the arc body can be an SSACFG region.
338void Outliner::sortAndBreakCycles(OutlinedArc &arc) {
339 // Topologically sort the arc body. We can't use the function's return value
340 // to check if any cycles were present, since it does not return false for ops
341 // that depend on themselves.
342 mlir::sortTopologically(arc.block.get());
343
344 // Take note of any uses-before-defs.
345 arc.block->walk([&](Operation *op) {
346 auto *blockOp = arc.block->findAncestorOpInBlock(*op);
347 for (auto &operand : op->getOpOperands())
348 if (auto *defOp = operand.get().getDefiningOp())
349 if (defOp->getBlock() == arc.block.get())
350 if (defOp == op || !defOp->isBeforeInBlock(blockOp))
351 arc.forwardRefs.push_back(&operand);
352 });
353 pass.numForwardRefs += arc.forwardRefs.size();
354
355 LLVM_DEBUG({
356 if (!arc.forwardRefs.empty())
357 llvm::dbgs() << "- Arc " << arc.index << " has " << arc.forwardRefs.size()
358 << " forward references\n";
359 });
360}
361
362void Outliner::createTerminator(OutlinedArc &arc) {
363 // Collect all op results that have uses outside the block.
364 SmallSetVector<Value, 8> results;
365 for (auto &op : *arc.block) {
366 for (auto result : op.getResults()) {
367 for (auto *userOp : result.getUsers()) {
368 if (!arc.block->findAncestorOpInBlock(*userOp)) {
369 results.insert(result);
370 break;
371 }
372 }
373 }
374 }
375
376 // Append values involved in a forward reference.
377 for (auto *operand : arc.forwardRefs)
378 results.insert(operand->get());
379
380 // Create an `arc.output` terminator op with these results as operands.
381 OpBuilder builder(moduleOp);
382 builder.setInsertionPointToEnd(arc.block.get());
383 arc.terminator =
384 OutputOp::create(builder, moduleOp.getLoc(), results.getArrayRef());
385}
386
387void Outliner::createAndCallArc(OutlinedArc &arc, OpBuilder &arcBuilder,
388 OpBuilder &callBuilder) {
389 // Collect all values used in the block that are defined outside the block,
390 // and create a block argument for each.
391 SmallVector<Value, 8> values;
392 SmallMapVector<Value, Value, 8> args;
393
394 auto addArg = [&](Value value) {
395 auto &arg = args[value];
396 if (!arg) {
397 arg = arc.block->addArgument(value.getType(), value.getLoc());
398 values.push_back(value);
399 }
400 return arg;
401 };
402
403 OpBuilder constBuilder(arc.terminator);
404 constBuilder.setInsertionPointToStart(arc.block.get());
405
406 arc.block->walk([&](Operation *op) {
407 for (auto &operand : op->getOpOperands()) {
408 // Get the defining block and zip up to the root block. Since the arc
409 // blocks are all detached at the moment, values defined inside an arc
410 // will stop at the arc's block, and all others will zip all the way up to
411 // the root module. We are only interested in values where this block is
412 // not the arc's block.
413 auto *defBlock = operand.get().getParentBlock();
414 while (defBlock && defBlock->getParentOp())
415 defBlock = defBlock->getParentOp()->getBlock();
416 if (defBlock == arc.block.get())
417 continue;
418
419 // Clone constant-like ops into the arc.
420 if (auto *defOp = operand.get().getDefiningOp();
421 defOp && defOp->hasTrait<OpTrait::ConstantLike>()) {
422 if (!args.count(operand.get())) {
423 pass.numClonedConstants++;
424 auto *clonedOp = constBuilder.clone(*defOp);
425 for (auto [oldResult, newResult] :
426 llvm::zip(defOp->getResults(), clonedOp->getResults()))
427 args[oldResult] = newResult;
428 }
429 operand.set(args.at(operand.get()));
430 continue;
431 }
432
433 // Create a block argument if none exists yet.
434 operand.set(addArg(operand.get()));
435 }
436 });
437
438 // Add arguments for all forward references.
439 for (auto *operand : arc.forwardRefs)
440 operand->set(addArg(operand->get()));
441
442 // Create an arc definition.
443 auto arcOp = DefineOp::create(
444 arcBuilder, arc.terminator.getLoc(),
445 arcBuilder.getStringAttr(moduleOp.getModuleName() + "_arc" +
446 Twine(arc.index)),
447 arcBuilder.getFunctionType(arc.block->getArgumentTypes(),
448 arc.terminator->getOperandTypes()));
449 arcOp.getBody().push_back(arc.block.release());
450 symbolTable.insert(arcOp);
451
452 // Create a call to the arc.
453 arc.call = CallOp::create(callBuilder, arcOp.getLoc(), arcOp, values);
454}
455
456/// Replace any external uses of an arc's internal results with the
457/// corresponding call result.
458void Outliner::useCallResults(OutlinedArc &arc) {
459 auto *arcBlock = arc.terminator->getBlock();
460 for (auto [valueInside, valueOutside] :
461 llvm::zip(arc.terminator.getOperands(), arc.call.getResults())) {
462 valueInside.replaceUsesWithIf(valueOutside, [&](OpOperand &use) {
463 return !arcBlock->findAncestorOpInBlock(*use.getOwner());
464 });
465 }
466}
467
468//===----------------------------------------------------------------------===//
469// Pass Implementation
470//===----------------------------------------------------------------------===//
471
472void OutlineArcsPass::runOnOperation() {
473 auto &symbolTable = getAnalysis<SymbolTable>();
474 for (auto moduleOp : getOperation().getOps<hw::HWModuleOp>()) {
475 LLVM_DEBUG(llvm::dbgs()
476 << "Outline arcs from @" << moduleOp.getModuleName() << "\n");
477 auto coloring = Colorer(moduleOp, *this).run();
478 Outliner(moduleOp, symbolTable, coloring, *this).run();
479 }
480}
static bool isArcBreaker(Operation *op)
Definition arc.py:1
Direction get(bool isOutput)
Returns an output direction if isOutput is true, otherwise returns an input direction.
Definition CalyxOps.cpp:56
OS & operator<<(OS &os, const InnerSymTarget &target)
Printing InnerSymTarget's.
The InstanceGraph op interface, see InstanceGraphInterface.td for more details.
int run(Type[Generator] generator=CppGenerator, List[str] cmdline_args=sys.argv)