Skip to content

Commit 9df378f

Browse files
committed
opt_carry_select pass
1 parent d928afe commit 9df378f

4 files changed

Lines changed: 518 additions & 0 deletions

File tree

passes/opt/peepopt_muxorder.pmg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ match op
1818
// Select OP
1919
select op->type.in($add, $mul, $and, $or, $xor, $xnor)
2020

21+
// Leave carry-select structures alone: folding `s ? (a+1) : a` back into
22+
// `a + s` would put a late select signal onto a wide ripple carry.
23+
filter !op->get_bool_attribute(\carry_select)
24+
2125
// Set ports, allowing A and B to be swapped
2226
choice <IdString> A {\A, \B}
2327
define <IdString> B (A == \A ? \B : \A)

passes/silimate/Makefile.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ OBJS += passes/silimate/clkmerge.o
2323
OBJS += passes/silimate/opt_boundary.o
2424
OBJS += passes/silimate/opt_vps.o
2525
OBJS += passes/silimate/opt_compact_prefix.o
26+
OBJS += passes/silimate/opt_carry_select.o
2627
OBJS += passes/silimate/infer_icg.o
2728

2829
OBJS += passes/silimate/opt_expand.o
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
/*
2+
* yosys -- Yosys Open SYnthesis Suite
3+
*
4+
* Copyright (C) 2026 Akash Levy <akash@silimate.com>
5+
*
6+
* Permission to use, copy, modify, and/or distribute this software for any
7+
* purpose with or without fee is hereby granted, provided that the above
8+
* copyright notice and this permission notice appear in all copies.
9+
*
10+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11+
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12+
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13+
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14+
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15+
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16+
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17+
*
18+
*/
19+
20+
#include "kernel/yosys.h"
21+
#include "kernel/sigtools.h"
22+
#include "kernel/celltypes.h"
23+
#include <cmath>
24+
25+
USING_YOSYS_NAMESPACE
26+
PRIVATE_NAMESPACE_BEGIN
27+
28+
// Marks the incrementer/mux a carry-select rewrite emits so peepopt -muxorder
29+
// (which would otherwise fold `s ? (a+1) : a` back into `a + s`, putting the
30+
// late select on a wide ripple carry) leaves them alone.
31+
static const IdString kAttrCarrySelect = ID(carry_select);
32+
33+
static inline double log2p1(int n) { return std::log2(double(std::max(1, n)) + 1.0); }
34+
35+
// Heuristic, model-free per-cell delay used only to rank "early" vs "late"
36+
// operands. Mirrors opt_timing_balance's heuristic so the two passes agree on
37+
// which arrivals dominate.
38+
static double estimate_cell_delay(const Cell *cell, int out_width)
39+
{
40+
if (cell == nullptr)
41+
return 1.0;
42+
IdString t = cell->type;
43+
if (t.in(ID($add), ID($sub), ID($neg), ID($alu), ID($shl), ID($shr), ID($sshl), ID($sshr)))
44+
return log2p1(out_width);
45+
if (t.in(ID($mul), ID($div), ID($mod)))
46+
return out_width;
47+
if (t == ID($pmux)) {
48+
int s_width = cell->hasParam(ID::S_WIDTH) ? cell->getParam(ID::S_WIDTH).as_int() : 1;
49+
return log2p1(s_width);
50+
}
51+
if (t.in(ID($and), ID($or)))
52+
return 0.5;
53+
// $mux, $xor, $xnor, $not, gate-level $_*_ and everything else: one level.
54+
return 1.0;
55+
}
56+
57+
struct OptCarrySelectWorker {
58+
Module *module;
59+
SigMap sigmap;
60+
CellTypes cell_types;
61+
62+
dict<SigBit, Cell*> driver; // sigmap'd output bit -> driving cell
63+
dict<SigBit, double> arrival_cache; // memoized arrival per bit
64+
pool<SigBit> on_stack; // combinational-loop guard
65+
66+
int max_narrow;
67+
int min_wide;
68+
double margin;
69+
70+
int converted = 0;
71+
72+
OptCarrySelectWorker(Module *m, int max_narrow, int min_wide, double margin)
73+
: module(m), sigmap(m), max_narrow(max_narrow), min_wide(min_wide), margin(margin)
74+
{
75+
cell_types.setup();
76+
for (auto cell : module->cells())
77+
for (auto &conn : cell->connections())
78+
if (cell->output(conn.first))
79+
for (auto bit : sigmap(conn.second))
80+
if (bit.wire)
81+
driver[bit] = cell;
82+
}
83+
84+
bool is_boundary(Cell *cell) {
85+
if (cell == nullptr)
86+
return true;
87+
if (cell->get_bool_attribute(ID::keep) || cell->get_bool_attribute(ID::blackbox))
88+
return true;
89+
if (cell->is_builtin_ff())
90+
return true;
91+
if (cell->type.in(ID($dlatch), ID($adlatch), ID($dlatchsr),
92+
ID($mem), ID($mem_v2), ID($memrd), ID($memrd_v2),
93+
ID($memwr), ID($memwr_v2), ID($meminit), ID($meminit_v2)))
94+
return true;
95+
return !cell_types.cell_known(cell->type);
96+
}
97+
98+
Cell *driver_of(SigBit bit) {
99+
auto it = driver.find(bit);
100+
return (it == driver.end()) ? nullptr : it->second;
101+
}
102+
103+
// Explicit-stack post-order traversal so arbitrarily deep combinational cones
104+
// cannot overflow the C++ stack (worker threads can have small stacks).
105+
double arrival_bit(SigBit start) {
106+
start = sigmap(start);
107+
if (!start.wire)
108+
return 0.0;
109+
if (auto it = arrival_cache.find(start); it != arrival_cache.end())
110+
return it->second;
111+
112+
struct Frame { SigBit bit; bool finalize; };
113+
std::vector<Frame> stack;
114+
stack.push_back({start, false});
115+
116+
while (!stack.empty()) {
117+
Frame &top = stack.back();
118+
SigBit bit = top.bit;
119+
if (!bit.wire || arrival_cache.count(bit)) {
120+
stack.pop_back();
121+
continue;
122+
}
123+
Cell *drv = driver_of(bit);
124+
if (drv == nullptr || is_boundary(drv)) {
125+
arrival_cache[bit] = 0.0;
126+
stack.pop_back();
127+
continue;
128+
}
129+
130+
if (!top.finalize) {
131+
// Expand: schedule finalize, then push not-yet-resolved inputs.
132+
top.finalize = true;
133+
on_stack.insert(bit);
134+
for (auto &conn : drv->connections())
135+
if (drv->input(conn.first))
136+
for (auto in_bit : sigmap(conn.second)) {
137+
if (!in_bit.wire || arrival_cache.count(in_bit))
138+
continue;
139+
if (on_stack.count(in_bit)) // combinational loop: break it
140+
continue;
141+
stack.push_back({in_bit, false});
142+
}
143+
continue;
144+
}
145+
146+
// Finalize: all resolvable inputs are now cached.
147+
double max_in = 0.0;
148+
int out_width = 1;
149+
for (auto &conn : drv->connections())
150+
if (drv->output(conn.first))
151+
out_width = std::max(out_width, GetSize(conn.second));
152+
for (auto &conn : drv->connections())
153+
if (drv->input(conn.first))
154+
for (auto in_bit : sigmap(conn.second)) {
155+
auto it = arrival_cache.find(in_bit);
156+
if (it != arrival_cache.end())
157+
max_in = std::max(max_in, it->second);
158+
}
159+
arrival_cache[bit] = max_in + estimate_cell_delay(drv, out_width);
160+
on_stack.erase(bit);
161+
stack.pop_back();
162+
}
163+
return arrival_cache.at(start);
164+
}
165+
166+
double arrival(const SigSpec &sig) {
167+
double t = 0.0;
168+
for (auto bit : sigmap(sig))
169+
t = std::max(t, arrival_bit(bit));
170+
return t;
171+
}
172+
173+
// Decision record so we never iterate over a mutating cell list.
174+
struct Plan {
175+
Cell *add;
176+
bool wide_is_a;
177+
int k; // narrow width / split point
178+
int w; // result width
179+
};
180+
181+
bool qualifies(Cell *c, Plan &plan) {
182+
if (c->type != ID($add))
183+
return false;
184+
if (c->get_bool_attribute(kAttrCarrySelect))
185+
return false;
186+
if (c->getParam(ID::A_SIGNED).as_bool() || c->getParam(ID::B_SIGNED).as_bool())
187+
return false; // v1: unsigned operands only (exact zero-extension)
188+
189+
int a_w = c->getParam(ID::A_WIDTH).as_int();
190+
int b_w = c->getParam(ID::B_WIDTH).as_int();
191+
int w = c->getParam(ID::Y_WIDTH).as_int();
192+
193+
bool wide_is_a = a_w >= b_w;
194+
int narrow_w = std::min(a_w, b_w);
195+
int k = narrow_w;
196+
197+
if (k < 1 || k >= w)
198+
return false;
199+
if (k > max_narrow)
200+
return false;
201+
if (w - k < min_wide)
202+
return false;
203+
204+
SigSpec wide_sig = c->getPort(wide_is_a ? ID::A : ID::B);
205+
SigSpec narrow_sig = c->getPort(wide_is_a ? ID::B : ID::A);
206+
207+
double arr_wide = arrival(wide_sig);
208+
double arr_narrow = arrival(narrow_sig);
209+
if (arr_narrow <= arr_wide + margin)
210+
return false; // no timing benefit: narrow operand is not the late one
211+
212+
plan.add = c;
213+
plan.wide_is_a = wide_is_a;
214+
plan.k = k;
215+
plan.w = w;
216+
return true;
217+
}
218+
219+
void apply(const Plan &plan) {
220+
Cell *cell = plan.add;
221+
int k = plan.k;
222+
int w = plan.w;
223+
std::string src = cell->get_src_attribute();
224+
225+
SigSpec wide_sig = cell->getPort(plan.wide_is_a ? ID::A : ID::B);
226+
SigSpec narrow_sig = cell->getPort(plan.wide_is_a ? ID::B : ID::A);
227+
SigSpec y = cell->getPort(ID::Y);
228+
229+
// Zero-extend the wide operand to the full result width.
230+
SigSpec wext = wide_sig;
231+
wext.extend_u0(w, /*is_signed=*/false);
232+
SigSpec wlow = wext.extract(0, k);
233+
SigSpec whigh = wext.extract(k, w - k);
234+
235+
// Low add: produces the low k sum bits plus the carry into bit k.
236+
Wire *losum = module->addWire(NEW_ID2_SUFFIX("cs_lo"), k + 1);
237+
module->addAdd(NEW_ID2_SUFFIX("cs_lo_add"), wlow, narrow_sig, SigSpec(losum), /*is_signed=*/false, src);
238+
SigSpec losum_low = SigSpec(losum).extract(0, k);
239+
SigBit carry = SigSpec(losum)[k];
240+
241+
// High part precomputed from the (early) wide operand, then selected by
242+
// the (late) low carry: hi = carry ? (whigh + 1) : whigh.
243+
Wire *hiinc = module->addWire(NEW_ID2_SUFFIX("cs_hi_inc"), w - k);
244+
Cell *inc = module->addAdd(NEW_ID2_SUFFIX("cs_hi_inc_add"), whigh, SigSpec(State::S1), SigSpec(hiinc), /*is_signed=*/false, src);
245+
inc->set_bool_attribute(kAttrCarrySelect);
246+
247+
Wire *hi = module->addWire(NEW_ID2_SUFFIX("cs_hi"), w - k);
248+
Cell *mux = module->addMux(NEW_ID2_SUFFIX("cs_hi_mux"), /*A=*/whigh, /*B=*/SigSpec(hiinc), /*S=*/carry, SigSpec(hi), src);
249+
mux->set_bool_attribute(kAttrCarrySelect);
250+
251+
// Reassemble result and drive the original adder output.
252+
SigSpec result = losum_low;
253+
result.append(SigSpec(hi));
254+
module->connect(y, result);
255+
module->remove(cell);
256+
converted++;
257+
}
258+
259+
void run() {
260+
vector<Cell*> adds;
261+
for (auto cell : module->cells())
262+
if (cell->type == ID($add))
263+
adds.push_back(cell);
264+
265+
vector<Plan> plans;
266+
for (auto c : adds) {
267+
Plan plan;
268+
if (qualifies(c, plan))
269+
plans.push_back(plan);
270+
}
271+
for (auto &plan : plans)
272+
apply(plan);
273+
}
274+
};
275+
276+
struct OptCarrySelectPass : public Pass {
277+
OptCarrySelectPass() : Pass("opt_carry_select",
278+
"decompose wide-early + narrow-late adders into carry-select form") {}
279+
280+
void help() override {
281+
// |---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|
282+
log("\n");
283+
log(" opt_carry_select [options] [selection]\n");
284+
log("\n");
285+
log("Rewrites a $add of the shape `wide_early + narrow_late` into a low add plus\n");
286+
log("a high carry-select. The low add absorbs the late, narrow operand and emits a\n");
287+
log("carry; the wide high half is precomputed from the early operand (as `hi` and\n");
288+
log("`hi + 1`) and selected by that carry through a single mux. This moves the wide\n");
289+
log("carry propagation onto the early operand, so the late operand only sees a\n");
290+
log("small add plus one mux instead of a full-width ripple carry.\n");
291+
log("\n");
292+
log("Only unsigned adders whose narrow operand arrives later (per a heuristic\n");
293+
log("logic-depth model) than the wide operand are rewritten, so adders that would\n");
294+
log("not benefit are left untouched. Emitted cells are tagged so peepopt -muxorder\n");
295+
log("does not fold the carry-select back into a late-carry ripple adder.\n");
296+
log("\n");
297+
log(" -max-narrow N\n");
298+
log(" only rewrite when the narrow operand width is <= N (default 16).\n");
299+
log("\n");
300+
log(" -min-wide N\n");
301+
log(" only rewrite when the wide (high) part width is >= N (default 8).\n");
302+
log("\n");
303+
log(" -margin F\n");
304+
log(" require narrow_arrival > wide_arrival + F (default 0.0).\n");
305+
log("\n");
306+
}
307+
308+
void execute(std::vector<std::string> args, RTLIL::Design *design) override {
309+
log_header(design, "Executing OPT_CARRY_SELECT pass (late-operand carry-select).\n");
310+
311+
int max_narrow = 16;
312+
int min_wide = 8;
313+
double margin = 0.0;
314+
315+
size_t argidx;
316+
for (argidx = 1; argidx < args.size(); argidx++) {
317+
if (args[argidx] == "-max-narrow" && argidx + 1 < args.size()) {
318+
max_narrow = atoi(args[++argidx].c_str());
319+
continue;
320+
}
321+
if (args[argidx] == "-min-wide" && argidx + 1 < args.size()) {
322+
min_wide = atoi(args[++argidx].c_str());
323+
continue;
324+
}
325+
if (args[argidx] == "-margin" && argidx + 1 < args.size()) {
326+
margin = atof(args[++argidx].c_str());
327+
continue;
328+
}
329+
break;
330+
}
331+
extra_args(args, argidx, design);
332+
333+
int total = 0;
334+
for (auto module : design->selected_modules()) {
335+
OptCarrySelectWorker worker(module, max_narrow, min_wide, margin);
336+
worker.run();
337+
total += worker.converted;
338+
}
339+
log("Converted %d adder(s) to carry-select form.\n", total);
340+
}
341+
} OptCarrySelectPass;
342+
343+
PRIVATE_NAMESPACE_END

0 commit comments

Comments
 (0)