Skip to content

Commit 06d0c5d

Browse files
[LLHD] Update LowerProcesses pass to work with process results (#8387)
Add an update version of the process lowering pass. This new version expects all probes and drives to have been hoisted out of processes. The pass then converts an `llhd.process` op to an `scf.execute_region` if the process - has a single wait op; - has a simple control flow loop through that single wait op; - has no destination operands on the wait op; and - has all operands that are used in the process but defined outside of the process observed by the wait op. This essentially lowers any combinational LLHD processes with a CFG loop through a wait op which triggers when any of the inputs change into a simple SCF execute region without the CFG loop. The assumption here is that the execute region will be re-evaluated if any of the values used within it change. Life would be a lot easier if we didn't have to chase through the observed values of the wait op for this lowering. Ideally LLHD would have a simplified version of a process that we can canonicalize to early on.
1 parent 218c185 commit 06d0c5d

File tree

4 files changed

+493
-0
lines changed

4 files changed

+493
-0
lines changed

include/circt/Dialect/LLHD/Transforms/LLHDPasses.td

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,11 @@ def HoistSignalsPass : Pass<"llhd-hoist-signals"> {
155155
let summary = "Hoist probes and promote drives to process results";
156156
}
157157

158+
def LowerProcessesPass : Pass<"llhd-lower-processes", "hw::HWModuleOp"> {
159+
let summary = "Convert combinational LLHD processes to SCF execute regions";
160+
let dependentDialects = [
161+
"mlir::scf::SCFDialect",
162+
];
163+
}
164+
158165
#endif // CIRCT_DIALECT_LLHD_TRANSFORMS_PASSES

lib/Dialect/LLHD/Transforms/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ add_circt_dialect_library(CIRCTLLHDTransforms
33
EarlyCodeMotionPass.cpp
44
FunctionEliminationPass.cpp
55
HoistSignals.cpp
6+
LowerProcesses.cpp
67
Mem2Reg.cpp
78
MemoryToBlockArgumentPass.cpp
89
ProcessLoweringPass.cpp
@@ -21,5 +22,6 @@ add_circt_dialect_library(CIRCTLLHDTransforms
2122
MLIRControlFlowDialect
2223
MLIRFuncDialect
2324
MLIRIR
25+
MLIRSCFDialect
2426
MLIRTransformUtils
2527
)
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
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+
9+
#include "circt/Dialect/Comb/CombOps.h"
10+
#include "circt/Dialect/LLHD/IR/LLHDOps.h"
11+
#include "circt/Dialect/LLHD/Transforms/LLHDPasses.h"
12+
#include "mlir/Analysis/Liveness.h"
13+
#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
14+
#include "mlir/Dialect/SCF/IR/SCF.h"
15+
#include "mlir/Transforms/RegionUtils.h"
16+
#include "llvm/Support/Debug.h"
17+
18+
#define DEBUG_TYPE "llhd-lower-processes"
19+
20+
namespace circt {
21+
namespace llhd {
22+
#define GEN_PASS_DEF_LOWERPROCESSESPASS
23+
#include "circt/Dialect/LLHD/Transforms/LLHDPasses.h.inc"
24+
} // namespace llhd
25+
} // namespace circt
26+
27+
using namespace mlir;
28+
using namespace circt;
29+
using namespace llhd;
30+
using llvm::SmallDenseSet;
31+
using llvm::SmallSetVector;
32+
33+
namespace {
34+
struct Lowering {
35+
Lowering(ProcessOp processOp) : processOp(processOp) {}
36+
void lower();
37+
bool matchControlFlow();
38+
void markObservedValues();
39+
bool allOperandsObserved();
40+
bool isObserved(Value value);
41+
42+
ProcessOp processOp;
43+
WaitOp waitOp;
44+
SmallDenseSet<Value> observedValues;
45+
};
46+
} // namespace
47+
48+
void Lowering::lower() {
49+
// Ensure that the process describes combinational logic.
50+
if (!matchControlFlow())
51+
return;
52+
markObservedValues();
53+
if (!allOperandsObserved())
54+
return;
55+
LLVM_DEBUG(llvm::dbgs() << "Lowering process " << processOp.getLoc() << "\n");
56+
57+
// Replace the process.
58+
OpBuilder builder(processOp);
59+
auto executeOp = builder.create<scf::ExecuteRegionOp>(
60+
processOp.getLoc(), processOp.getResultTypes());
61+
executeOp.getRegion().takeBody(processOp.getBody());
62+
processOp.replaceAllUsesWith(executeOp);
63+
processOp.erase();
64+
processOp = {};
65+
66+
// Replace the `llhd.wait` with an `scf.yield`.
67+
builder.setInsertionPoint(waitOp);
68+
builder.create<scf::YieldOp>(waitOp.getLoc(), waitOp.getYieldOperands());
69+
waitOp.erase();
70+
71+
// Simplify the execute op body region since disconnecting the control flow
72+
// loop through the wait op has potentially created unreachable blocks.
73+
IRRewriter rewriter(builder);
74+
(void)simplifyRegions(rewriter, executeOp->getRegions());
75+
}
76+
77+
/// Check that the process' entry block trivially joins a control flow loop
78+
/// immediately after the wait op.
79+
bool Lowering::matchControlFlow() {
80+
// Ensure that there is only a single wait op in the process and that it has
81+
// no destination operands.
82+
for (auto &block : processOp.getBody()) {
83+
if (auto op = dyn_cast<WaitOp>(block.getTerminator())) {
84+
if (waitOp) {
85+
LLVM_DEBUG(llvm::dbgs() << "Skipping process " << processOp.getLoc()
86+
<< ": multiple wait ops\n");
87+
return false;
88+
}
89+
waitOp = op;
90+
}
91+
}
92+
if (!waitOp) {
93+
LLVM_DEBUG(llvm::dbgs() << "Skipping process " << processOp.getLoc()
94+
<< ": no wait op\n");
95+
return false;
96+
}
97+
if (!waitOp.getDestOperands().empty()) {
98+
LLVM_DEBUG(llvm::dbgs() << "Skipping process " << processOp.getLoc()
99+
<< ": wait op has destination operands\n");
100+
return false;
101+
}
102+
103+
// Helper function to skip across empty blocks with only a single successor.
104+
auto skipToMergePoint = [&](Block *block) -> std::pair<Block *, ValueRange> {
105+
ValueRange operands;
106+
while (auto branchOp = dyn_cast<cf::BranchOp>(block->getTerminator())) {
107+
if (!block->without_terminator().empty())
108+
break;
109+
block = branchOp.getDest();
110+
operands = branchOp.getDestOperands();
111+
if (std::distance(block->pred_begin(), block->pred_end()) > 1)
112+
break;
113+
if (!operands.empty())
114+
break;
115+
}
116+
return {block, operands};
117+
};
118+
119+
// Ensure that the entry block and wait op converge on the same block and with
120+
// the same block arguments.
121+
auto &entry = processOp.getBody().front();
122+
auto [entryMergeBlock, entryMergeArgs] = skipToMergePoint(&entry);
123+
auto [waitMergeBlock, waitMergeArgs] = skipToMergePoint(waitOp.getDest());
124+
if (entryMergeBlock != waitMergeBlock) {
125+
LLVM_DEBUG(llvm::dbgs()
126+
<< "Skipping process " << processOp.getLoc()
127+
<< ": control from entry and wait does not converge\n");
128+
return false;
129+
}
130+
if (entryMergeArgs != waitMergeArgs) {
131+
LLVM_DEBUG(llvm::dbgs() << "Skipping process " << processOp.getLoc()
132+
<< ": control from entry and wait converges with "
133+
"different block arguments\n");
134+
return false;
135+
}
136+
137+
// Ensure that no values are live across the wait op.
138+
Liveness liveness(processOp);
139+
for (auto value : liveness.getLiveOut(waitOp->getBlock())) {
140+
if (value.getParentRegion()->isProperAncestor(&processOp.getBody()))
141+
continue;
142+
LLVM_DEBUG({
143+
llvm::dbgs() << "Skipping process " << processOp.getLoc() << ": value ";
144+
value.print(llvm::dbgs(), OpPrintingFlags().skipRegions());
145+
llvm::dbgs() << " live across wait\n";
146+
});
147+
return false;
148+
}
149+
150+
return true;
151+
}
152+
153+
/// Mark values the process observes that are defined outside the process.
154+
void Lowering::markObservedValues() {
155+
SmallVector<Value> worklist;
156+
auto markObserved = [&](Value value) {
157+
if (observedValues.insert(value).second)
158+
worklist.push_back(value);
159+
};
160+
161+
for (auto value : waitOp.getObserved())
162+
if (value.getParentRegion()->isProperAncestor(&processOp.getBody()))
163+
markObserved(value);
164+
165+
while (!worklist.empty()) {
166+
auto value = worklist.pop_back_val();
167+
auto *op = value.getDefiningOp();
168+
if (!op)
169+
continue;
170+
171+
// Look through probe ops to mark the probe signal as well, just in case
172+
// there may be multiple probes of the same signal.
173+
if (auto probeOp = dyn_cast<PrbOp>(op))
174+
markObserved(probeOp.getSignal());
175+
176+
// Look through operations that simply reshape incoming values into an
177+
// aggregate form from which any changes remain apparent.
178+
if (isa<hw::ArrayCreateOp, hw::StructCreateOp, comb::ConcatOp,
179+
hw::BitcastOp>(op))
180+
for (auto operand : op->getOperands())
181+
markObserved(operand);
182+
}
183+
}
184+
185+
/// Ensure that any value defined outside the process that is used inside the
186+
/// process is derived entirely from an observed value.
187+
bool Lowering::allOperandsObserved() {
188+
// Collect all ancestor regions such that we can easily check if a value is
189+
// defined outside the process.
190+
SmallPtrSet<Region *, 4> properAncestors;
191+
for (auto *region = processOp->getParentRegion(); region;
192+
region = region->getParentRegion())
193+
properAncestors.insert(region);
194+
195+
// Walk all operations under the process and check each operand.
196+
auto walkResult = processOp.walk([&](Operation *op) {
197+
for (auto operand : op->getOperands()) {
198+
// We only care about values defined outside the process.
199+
if (!properAncestors.count(operand.getParentRegion()))
200+
continue;
201+
202+
// If the value is observed, all is well.
203+
if (isObserved(operand))
204+
continue;
205+
206+
// Otherwise complain and abort.
207+
LLVM_DEBUG({
208+
llvm::dbgs() << "Skipping process " << processOp.getLoc()
209+
<< ": unobserved value ";
210+
operand.print(llvm::dbgs(), OpPrintingFlags().skipRegions());
211+
llvm::dbgs() << "\n";
212+
});
213+
return WalkResult::interrupt();
214+
}
215+
return WalkResult::advance();
216+
});
217+
return !walkResult.wasInterrupted();
218+
}
219+
220+
/// Check if a value is observed by the wait op, or all its operands are only
221+
/// derived from observed values.
222+
bool Lowering::isObserved(Value value) {
223+
// Check if the value is trivially observed.
224+
if (observedValues.contains(value))
225+
return true;
226+
227+
// Otherwise get the operation that defines it such that we can check if the
228+
// value is derived from purely observed values. If it isn't define by an op,
229+
// the value is unobserved.
230+
auto *defOp = value.getDefiningOp();
231+
if (!defOp)
232+
return false;
233+
234+
// Otherwise visit all ops in the fan-in cone and ensure that they are
235+
// observed. If any value is unobserved, immediately return false.
236+
SmallDenseSet<Operation *> seenOps;
237+
SmallVector<Operation *> worklist;
238+
seenOps.insert(defOp);
239+
worklist.push_back(defOp);
240+
while (!worklist.empty()) {
241+
auto *op = worklist.pop_back_val();
242+
243+
// Give up on ops with nested regions.
244+
if (op->getNumRegions() != 0)
245+
return false;
246+
247+
// Otherwise check that all operands are observed. If we haven't seen an
248+
// operand before, and it is not a signal, add it to the worklist to be
249+
// checked.
250+
for (auto operand : op->getOperands()) {
251+
if (observedValues.contains(operand))
252+
continue;
253+
if (isa<hw::InOutType>(operand.getType()))
254+
return false;
255+
auto *defOp = operand.getDefiningOp();
256+
if (!defOp || !isMemoryEffectFree(defOp))
257+
return false;
258+
if (seenOps.insert(defOp).second)
259+
worklist.push_back(defOp);
260+
}
261+
}
262+
263+
// If we arrive at this point, we weren't able to reach an unobserved value.
264+
// Therefore we consider this value derived from only observed values.
265+
observedValues.insert(value);
266+
return true;
267+
}
268+
269+
namespace {
270+
struct LowerProcessesPass
271+
: public llhd::impl::LowerProcessesPassBase<LowerProcessesPass> {
272+
void runOnOperation() override;
273+
};
274+
} // namespace
275+
276+
void LowerProcessesPass::runOnOperation() {
277+
SmallVector<ProcessOp> processOps(getOperation().getOps<ProcessOp>());
278+
for (auto processOp : processOps)
279+
Lowering(processOp).lower();
280+
}

0 commit comments

Comments
 (0)