Skip to content

Commit a04a76e

Browse files
authored
[Synth] Add GenericLUTMapper pass for LUT mapping (#8888)
This commit introduces a pass that performs technology-independent LUT mapping from logic network to generic lookup tables (comb.truth_table). The pass is primarily designed for benchmarking purposes and provides functionality equivalent to ABC's `if -K 6` command for K-LUT mapping. The pass supports configurable LUT sizes and uses a cut-based rewriting approach to efficiently map combinational logic from AIG nodes to truth tables. It includes options for maximum LUT size and maximum cuts per root node to control the mapping quality and compilation time. The implementation provides technology-independent LUT mapping with configurable K-LUT support (default K=6), cut enumeration with configurable limits, truth table generation for optimal LUT utilization, and seamless integration with the existing synthesis pipeline.
1 parent c47d240 commit a04a76e

File tree

8 files changed

+259
-25
lines changed

8 files changed

+259
-25
lines changed

include/circt/Dialect/Synth/Transforms/SynthPasses.td

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,24 @@ def TestPriorityCuts : Pass<"synth-test-priority-cuts", "hw::HWModuleOp"> {
2626
];
2727
}
2828

29-
def TechMapper : Pass<"synth-tech-mapper", "mlir::ModuleOp"> {
29+
class CutRewriterPassBase<string name, string op> : Pass<name, op> {
30+
list<Option> baseOptions = [
31+
Option<"maxCutsPerRoot", "max-cuts-per-root", "int", "6",
32+
"Maximum number of cuts to maintain per node">,
33+
Option<"strategy", "strategy", "synth::OptimizationStrategy",
34+
/*default=*/"synth::OptimizationStrategyTiming",
35+
"Optimization strategy (area vs. timing)",
36+
[{::llvm::cl::values(
37+
clEnumValN(synth::OptimizationStrategyArea, "area",
38+
"Optimize for area"),
39+
clEnumValN(synth::OptimizationStrategyTiming, "timing",
40+
"Optimize for timing")
41+
)}]>,
42+
Option<"test", "test", "bool", "false", "Attach timing to IR for testing">
43+
];
44+
}
45+
46+
def TechMapper : CutRewriterPassBase<"synth-tech-mapper", "mlir::ModuleOp"> {
3047
let summary = "Technology mapping using cut rewriting";
3148
let description = [{
3249
This pass performs technology mapping by converting logic network
@@ -41,21 +58,24 @@ def TechMapper : Pass<"synth-tech-mapper", "mlir::ModuleOp"> {
4158

4259
Supports both area and timing optimization strategies.
4360
}];
44-
let options = [
45-
Option<"maxCutsPerRoot", "max-cuts-per-root", "int", "6",
46-
"Maximum number of cuts to maintain per node">,
47-
Option<"strategy", "strategy", "synth::OptimizationStrategy",
48-
/*default=*/"synth::OptimizationStrategyTiming",
49-
"Optimization strategy (area vs. timing)",
50-
[{::llvm::cl::values(
51-
clEnumValN(synth::OptimizationStrategyArea, "area",
52-
"Optimize for area"),
53-
clEnumValN(synth::OptimizationStrategyTiming, "timing",
54-
"Optimize for timing")
55-
)}]>,
56-
Option<"test", "test", "bool", "false", "Attach timing to IR for testing">,
61+
let options = baseOptions;
62+
let dependentDialects = ["hw::HWDialect"];
63+
}
64+
65+
def GenericLutMapper : CutRewriterPassBase<"synth-generic-lut-mapper",
66+
"hw::HWModuleOp"> {
67+
let summary = "LUT mapping using generic K-input LUTs";
68+
let description = [{
69+
This pass performs technology mapping using generic K-input lookup tables
70+
(LUTs). It converts combinational logic networks into implementations
71+
using K-input LUTs (comb.truth_table) with unit area cost and delay.
72+
}];
73+
let options = baseOptions # [
74+
Option<"maxLutSize", "max-lut-size", "unsigned", /*default=*/"6",
75+
"Maximum number of inputs per LUT">
5776
];
58-
let dependentDialects = ["circt::hw::HWDialect"];
77+
let dependentDialects = ["comb::CombDialect"];
5978
}
6079

80+
6181
#endif // CIRCT_DIALECT_SYNTH_TRANSFORMS_PASSES_TD

integration_test/circt-synth/mapping-lec.mlir

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
// REQUIRES: libz3
22
// REQUIRES: circt-lec-jit
33

4+
// RUN: circt-opt %s --convert-aig-to-comb -o %t.comb.mlir
5+
46
// RUN: circt-synth %s -o %t1.mlir -convert-to-comb --top mul
57
// RUN: cat %t1.mlir | FileCheck %s
6-
// RUN: circt-opt %s --convert-aig-to-comb -o %t2.mlir
7-
// RUN: circt-lec %t1.mlir %t2.mlir -c1=mul -c2=mul --shared-libs=%libz3 | FileCheck %s --check-prefix=COMB_MUL_TECHMAP
8+
// RUN: circt-lec %t1.mlir %t.comb.mlir -c1=mul -c2=mul --shared-libs=%libz3 | FileCheck %s --check-prefix=COMB_MUL_TECHMAP
89

910
// COMB_MUL_TECHMAP: c1 == c2
1011

12+
// RUN: circt-synth %s -o %t.lut.mlir --top mul --lower-to-k-lut 6
13+
// RUN: cat %t.lut.mlir | FileCheck %s --check-prefix=LUT
14+
// RUN: circt-opt -convert-aig-to-comb -lower-comb %t.lut.mlir -o %t2.mlir
15+
// RUN: circt-lec %t2.mlir %t.comb.mlir -c1=mul -c2=mul --shared-libs=%libz3 | FileCheck %s --check-prefix=COMB_MUL_LUT
16+
17+
// COMB_MUL_LUT: c1 == c2
18+
1119
// Set delay for binary and inv op to 5 so that others will be prioritized
1220
hw.module @and_inv(in %a : i1, in %b : i1, out result : i1) attributes {hw.techlib.info = {area = 1.0 : f64, delay = [[5], [5]]}} {
1321
%0 = aig.and_inv %a, %b : i1
@@ -48,6 +56,12 @@ hw.module @some(in %a : i1, in %b : i1, out result : i1) attributes {hw.techlib.
4856
// CHECK-DAG: hw.instance {{".+"}} @nand_nand
4957
// CHECK-DAG: hw.instance {{".+"}} @and_inv_n
5058
// CHECK-DAG: hw.instance {{".+"}} @and_inv_nn
59+
// LUT: hw.module @mul
60+
// LUT: comb.truth_table
61+
// LUT-NOT: aig.and_inv
62+
// LUT-NOT: comb.and
63+
// LUT-NOT: comb.xor
64+
// LUT-NOT: hw.instance
5165
hw.module @mul(in %arg0: i4, in %arg1: i4, out add: i4) {
5266
%0 = comb.mul %arg0, %arg1 : i4
5367
hw.output %0 : i4

lib/Dialect/Synth/Transforms/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
add_circt_dialect_library(CIRCTSynthTransforms
1010
CutRewriter.cpp
11+
GenericLUTMapper.cpp
1112
SynthesisPipeline.cpp
1213
TechMapper.cpp
1314
TestPriorityCuts.cpp

lib/Dialect/Synth/Transforms/CutRewriter.cpp

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -581,14 +581,17 @@ CutRewritePatternSet::CutRewritePatternSet(
581581
for (auto &pattern : this->patterns) {
582582
SmallVector<NPNClass, 2> npnClasses;
583583
auto result = pattern->useTruthTableMatcher(npnClasses);
584-
(void)result;
585-
assert(result && "Currently all patterns must use truth table matcher");
586-
587-
for (auto npnClass : npnClasses) {
588-
// Create a NPN class from the truth table
589-
npnToPatternMap[{npnClass.truthTable.table,
590-
npnClass.truthTable.numInputs}]
591-
.push_back(std::make_pair(std::move(npnClass), pattern.get()));
584+
if (result) {
585+
for (auto npnClass : npnClasses) {
586+
// Create a NPN class from the truth table
587+
npnToPatternMap[{npnClass.truthTable.table,
588+
npnClass.truthTable.numInputs}]
589+
.push_back(std::make_pair(std::move(npnClass), pattern.get()));
590+
}
591+
} else {
592+
// If the pattern does not provide NPN classes, we use a special key
593+
// to indicate that it should be considered for all cuts.
594+
nonTruthTablePatterns.push_back(pattern.get());
592595
}
593596
}
594597
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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+
// This file implements a generic LUT mapper pass using the cut-based rewriting
10+
// framework. It performs technology mapping by converting combinational logic
11+
// networks into implementations using K-input lookup tables (LUTs).
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
#include "circt/Dialect/Comb/CombOps.h"
16+
#include "circt/Dialect/HW/HWOps.h"
17+
#include "circt/Dialect/Synth/Transforms/CutRewriter.h"
18+
#include "circt/Dialect/Synth/Transforms/SynthPasses.h"
19+
#include "circt/Support/LLVM.h"
20+
#include "circt/Support/NPNClass.h"
21+
#include "mlir/IR/Builders.h"
22+
#include "mlir/Pass/Pass.h"
23+
#include "llvm/Support/Debug.h"
24+
25+
#define DEBUG_TYPE "synth-generic-lut-mapper"
26+
27+
using namespace circt;
28+
using namespace circt::synth;
29+
30+
namespace circt {
31+
namespace synth {
32+
#define GEN_PASS_DEF_GENERICLUTMAPPER
33+
#include "circt/Dialect/Synth/Transforms/SynthPasses.h.inc"
34+
} // namespace synth
35+
} // namespace circt
36+
37+
//===----------------------------------------------------------------------===//
38+
// Generic LUT Pattern
39+
//===----------------------------------------------------------------------===//
40+
41+
/// A generic K-input LUT pattern that can implement any boolean function
42+
/// with up to K inputs using a lookup table.
43+
struct GenericLUT : public CutRewritePattern {
44+
unsigned k; // Maximum number of inputs for this LUT
45+
46+
GenericLUT(mlir::MLIRContext *context, unsigned k)
47+
: CutRewritePattern(context), k(k) {}
48+
49+
bool match(const Cut &cut) const override {
50+
// This pattern can implement any cut with at most k inputs
51+
return cut.getInputSize() <= k && cut.getOutputSize() == 1;
52+
}
53+
54+
llvm::FailureOr<Operation *> rewrite(mlir::OpBuilder &rewriter,
55+
Cut &cut) const override {
56+
// NOTE: Don't use NPN since it's unnecessary.
57+
auto truthTable = cut.getTruthTable();
58+
LLVM_DEBUG({
59+
llvm::dbgs() << "Rewriting cut with " << cut.getInputSize()
60+
<< " inputs and " << cut.getInputSize()
61+
<< " operations to a generic LUT with " << k << " inputs.\n";
62+
cut.dump(llvm::dbgs());
63+
llvm::dbgs() << "Truth table details:\n";
64+
truthTable.dump(llvm::dbgs());
65+
});
66+
67+
SmallVector<bool> lutTable;
68+
// Convert the truth table to a LUT table
69+
for (uint32_t i = 0; i < truthTable.table.getBitWidth(); ++i)
70+
lutTable.push_back(truthTable.table[i]);
71+
72+
auto arrayAttr = rewriter.getBoolArrayAttr(
73+
lutTable); // Create a boolean array attribute.
74+
75+
// Reverse the inputs to match the LUT input order
76+
SmallVector<Value> lutInputs(cut.inputs.rbegin(), cut.inputs.rend());
77+
78+
// Generate comb.truth table operation.
79+
auto truthTableOp = rewriter.create<comb::TruthTableOp>(
80+
cut.getRoot()->getLoc(), lutInputs, arrayAttr);
81+
82+
// Replace the root operation with the truth table operation
83+
return truthTableOp.getOperation();
84+
}
85+
86+
double getArea(const Cut &cut) const override {
87+
// Each LUT has unit area regardless of the function it implements
88+
return 1.0;
89+
}
90+
91+
DelayType getDelay(unsigned inputIndex, unsigned outputIndex) const override {
92+
// All LUTs have unit delay from any input to any output
93+
return 1;
94+
}
95+
96+
unsigned getNumInputs() const override { return k; }
97+
98+
unsigned getNumOutputs() const override { return 1; }
99+
100+
StringRef getPatternName() const override { return "GenericLUT"; }
101+
};
102+
103+
//===----------------------------------------------------------------------===//
104+
// Generic LUT Mapper Pass
105+
//===----------------------------------------------------------------------===//
106+
107+
struct GenericLUTMapperPass
108+
: public impl::GenericLutMapperBase<GenericLUTMapperPass> {
109+
using GenericLutMapperBase<GenericLUTMapperPass>::GenericLutMapperBase;
110+
111+
void runOnOperation() override {
112+
auto module = getOperation();
113+
// Create the cut rewriter options
114+
CutRewriterOptions options;
115+
options.strategy = strategy;
116+
options.maxCutInputSize = maxLutSize;
117+
options.maxCutSizePerRoot = maxCutsPerRoot;
118+
options.allowNoMatch = false;
119+
options.attachDebugTiming = test;
120+
121+
// Create the pattern for generic K-LUT
122+
SmallVector<std::unique_ptr<CutRewritePattern>, 4> patterns;
123+
patterns.push_back(
124+
std::make_unique<GenericLUT>(module->getContext(), maxLutSize));
125+
126+
// Create the pattern set
127+
CutRewritePatternSet patternSet(std::move(patterns));
128+
129+
// Create the cut rewriter
130+
CutRewriter rewriter(options, patternSet);
131+
132+
// Apply the rewriting
133+
if (failed(rewriter.run(module)))
134+
return signalPassFailure();
135+
}
136+
};

test/Dialect/Synth/lut-mapper.mlir

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// FIXME: max-cuts-per-root=20 is due to a lack of non-minimal cut filtering.
2+
// RUN: circt-opt --pass-pipeline='builtin.module(hw.module(synth-generic-lut-mapper{test=true max-cuts-per-root=20}))' %s | FileCheck %s --check-prefixes CHECK,LUT
3+
// RUN: circt-opt --pass-pipeline='builtin.module(hw.module(synth-generic-lut-mapper{test=true max-lut-size=2}))' %s | FileCheck %s --check-prefixes CHECK,LUT2
4+
5+
// CHECK: %[[B_0:.+]] = comb.extract %b from 0 : (i2) -> i1
6+
// CHECK-NEXT: %[[B_1:.+]] = comb.extract %b from 1 : (i2) -> i1
7+
// CHECK-NEXT: %[[A_0:.+]] = comb.extract %a from 0 : (i2) -> i1
8+
// CHECK-NEXT: %[[A_1:.+]] = comb.extract %a from 1 : (i2) -> i1
9+
10+
// LUT-NEXT: %[[C_0:.+]] = comb.truth_table %[[A_0]], %[[B_0]] -> [false, true, true, false]
11+
// LUT-SAME: test.arrival_times = [1]
12+
// LUT-NEXT: %[[C_1:.+]] = comb.truth_table %[[B_1]], %[[A_1]], %[[A_0]], %[[B_0]] -> [false, false, false, true, true, true, true, false, true, true, true, false, false, false, false, true]
13+
// LUT-SAME: test.arrival_times = [1]
14+
// LUT-NEXT: %[[C_2:.+]] = comb.concat %[[C_1]], %[[C_0]] : i1, i1
15+
// LUT-NEXT: hw.output %[[C_2]] : i2
16+
17+
// LUT2: %[[C_0:.+]] = comb.truth_table %[[A_0]], %[[B_0]] -> [false, false, false, true]
18+
// LUT2-SAME: test.arrival_times = [1]
19+
// LUT2-NEXT: %[[C_1:.+]] = comb.truth_table %[[A_0]], %[[B_0]] -> [false, true, true, false]
20+
// LUT2-SAME: test.arrival_times = [1]
21+
// LUT2-NEXT: %[[C_2:.+]] = comb.truth_table %[[C_0]], %[[A_1]] -> [false, true, true, false]
22+
// LUT2-SAME: test.arrival_times = [2]
23+
// LUT2-NEXT: %[[C_3:.+]] = comb.truth_table %[[C_2]], %[[B_1]] -> [false, true, true, false]
24+
// LUT2-SAME: test.arrival_times = [3]
25+
// LUT2-NEXT: %[[C_4:.+]] = comb.concat %[[C_3]], %[[C_1]] : i1, i1
26+
// LUT2-NEXT: hw.output %[[C_4]] : i2
27+
hw.module @add(in %a : i2, in %b : i2, out result : i2) {
28+
%0 = comb.extract %b from 0 : (i2) -> i1
29+
%1 = comb.extract %b from 1 : (i2) -> i1
30+
%2 = comb.extract %a from 0 : (i2) -> i1
31+
%3 = comb.extract %a from 1 : (i2) -> i1
32+
%4 = aig.and_inv not %0, not %2 : i1
33+
%5 = aig.and_inv %0, %2 : i1
34+
%6 = aig.and_inv not %4, not %5 : i1
35+
%7 = aig.and_inv not %3, not %5 : i1
36+
%8 = aig.and_inv %3, %5 : i1
37+
%9 = aig.and_inv not %7, not %8 : i1
38+
%10 = aig.and_inv not %1, not %9 : i1
39+
%11 = aig.and_inv %1, %9 : i1
40+
%12 = aig.and_inv not %10, not %11 : i1
41+
%13 = comb.concat %12, %6 : i1, i1
42+
hw.output %13 : i2
43+
}

tools/circt-synth/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ target_link_libraries(circt-synth
66
CIRCTAIGAnalysis
77
CIRCTComb
88
CIRCTCombToDatapath
9+
CIRCTCombTransforms
910
CIRCTDatapath
1011
CIRCTDatapathToComb
1112
CIRCTDebug

tools/circt-synth/circt-synth.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "circt/Dialect/AIG/Analysis/LongestPathAnalysis.h"
1818
#include "circt/Dialect/Comb/CombDialect.h"
1919
#include "circt/Dialect/Comb/CombOps.h"
20+
#include "circt/Dialect/Comb/CombPasses.h"
2021
#include "circt/Dialect/Debug/DebugDialect.h"
2122
#include "circt/Dialect/Emit/EmitDialect.h"
2223
#include "circt/Dialect/HW/HWDialect.h"
@@ -167,6 +168,11 @@ static cl::opt<synth::OptimizationStrategy> synthesisStrategy(
167168
"Optimize for timing")),
168169
cl::init(synth::OptimizationStrategyTiming), cl::cat(mainCategory));
169170

171+
static cl::opt<int>
172+
lowerToKLUTs("lower-to-k-lut",
173+
cl::desc("Lower to generic a truth table op with K inputs"),
174+
cl::init(0), cl::cat(mainCategory));
175+
170176
//===----------------------------------------------------------------------===//
171177
// Main Tool Logic
172178
//===----------------------------------------------------------------------===//
@@ -216,6 +222,14 @@ static void populateCIRCTSynthPipeline(PassManager &pm) {
216222
optimizationOptions.disableWordToBits.setValue(disableWordToBits);
217223

218224
circt::synth::buildAIGOptimizationPipeline(pm, optimizationOptions);
225+
if (untilReached(UntilMapping))
226+
return;
227+
if (lowerToKLUTs) {
228+
circt::synth::GenericLutMapperOptions lutOptions;
229+
lutOptions.maxLutSize = lowerToKLUTs;
230+
lutOptions.maxCutsPerRoot = maxCutSizePerRoot;
231+
pm.addPass(circt::synth::createGenericLutMapper(lutOptions));
232+
}
219233
};
220234

221235
nestOrAddToHierarchicalRunner(pm, pipeline, topName);
@@ -241,6 +255,8 @@ static void populateCIRCTSynthPipeline(PassManager &pm) {
241255
pm,
242256
[&](OpPassManager &pm) {
243257
pm.addPass(circt::createConvertAIGToComb());
258+
if (lowerToKLUTs)
259+
pm.addPass(circt::comb::createLowerComb());
244260
pm.addPass(createCSEPass());
245261
},
246262
topName);

0 commit comments

Comments
 (0)