From dc995eba9831feaacd2a94dc6806920885b6ae42 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sun, 21 Jun 2026 00:56:39 -0700 Subject: [PATCH 1/2] opt_first_fit_alloc --- passes/opt/Makefile.inc | 1 + passes/opt/opt_first_fit_alloc.cc | 1334 +++++++++++++++++++++++++++++ tests/opt/opt_first_fit_alloc.ys | 592 +++++++++++++ 3 files changed, 1927 insertions(+) create mode 100644 passes/opt/opt_first_fit_alloc.cc create mode 100644 tests/opt/opt_first_fit_alloc.ys diff --git a/passes/opt/Makefile.inc b/passes/opt/Makefile.inc index 6636e9d10..2e6f383ff 100644 --- a/passes/opt/Makefile.inc +++ b/passes/opt/Makefile.inc @@ -30,6 +30,7 @@ OBJS += passes/opt/opt_balance_tree.o OBJS += passes/opt/opt_parallel_prefix.o OBJS += passes/opt/opt_prienc.o OBJS += passes/opt/opt_priority_onehot.o +OBJS += passes/opt/opt_first_fit_alloc.o OBJS += passes/opt/peepopt.o GENFILES += passes/opt/peepopt_pm.h diff --git a/passes/opt/opt_first_fit_alloc.cc b/passes/opt/opt_first_fit_alloc.cc new file mode 100644 index 000000000..a4bcb0d38 --- /dev/null +++ b/passes/opt/opt_first_fit_alloc.cc @@ -0,0 +1,1334 @@ +/* + * yosys -- Yosys Open SYnthesis Suite + * + * Copyright (C) 2026 Silimate Inc. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#include "kernel/yosys.h" +#include "kernel/sigtools.h" +#include "kernel/consteval.h" +#include +#include +#include +#include +#include + +USING_YOSYS_NAMESPACE +PRIVATE_NAMESPACE_BEGIN + +static int clog2_int(int x) +{ + int r = 0; + while ((1 << r) < x) + r++; + return r; +} + +// Pack a per-lane integer vector into a Const with elem_w bits per lane, +// lane-major (lane k occupies bits [k*elem_w +: elem_w]). +static Const pack_lanes(const vector &vals, int elem_w) +{ + vector bits(vals.size() * elem_w, State::S0); + for (int k = 0; k < GetSize(vals); k++) + for (int b = 0; b < elem_w && b < 31; b++) + if ((vals[k] >> b) & 1) + bits[k * elem_w + b] = State::S1; + return Const(bits); +} + +#include "passes/opt/cut_region.h" + +struct OptFirstFitAllocWorker : CutRegionWorker { + Cell *cell = nullptr; + + int min_n = 4; + int max_n = 64; + int max_field_w = 6; // max index (dsel) element width + int max_cat_w = 6; // max category width c + int max_attr_w = 8; // max per-lane attr width for the xbar field gather + + int regions_rewritten = 0; + int cells_added = 0; + + OptFirstFitAllocWorker(Module *module) : CutRegionWorker(module) {} + + // ---------------------------------------------------------------- + // Reference closed-form semantics of the greedy first-fit allocator. + // + // Lanes are processed in priority order (LSB-first here; the caller + // reverses for MSB-first). A "leader" is the first enabled lane of each + // category that is reached before its category's slot is taken; broadcast + // (bc) lanes after the first leader are suppressed. Leaders take slots + // 0,1,2,... in order; the per-lane rank (dsel) is the slot of the leader + // matching that lane's category, except broadcast lanes which snap to the + // last leader's slot. This is SAT-proven equivalent to the unrolled RTL + // allocation loop (taken[]/done[] scan). + // ---------------------------------------------------------------- + struct AllocResult { + vector dsel; // per-lane rank + vector leader; // 1 iff lane is a slot leader + vector slot; // exclusive prefix count of leaders + int M = 0; // number of leaders + }; + + AllocResult compute_alloc(const vector &en, const vector &bc, + const vector &cat, int n) const + { + AllocResult r; + r.dsel.assign(n, 0); + r.leader.assign(n, 0); + r.slot.assign(n, 0); + + vector any_en_before(n, 0); + int acc = 0; + for (int i = 0; i < n; i++) { + any_en_before[i] = acc ? 1 : 0; + acc |= en[i]; + } + int e0 = -1; + for (int i = 0; i < n; i++) + if (en[i] && !any_en_before[i]) { e0 = i; break; } + int cat_e0 = (e0 >= 0) ? cat[e0] : 0; + + for (int i = 0; i < n; i++) { + bool is_e0 = (i == e0); + bool blocked_mid = false; + for (int j = 0; j < i; j++) + if (any_en_before[j] && en[j] && !bc[j] && cat[j] == cat[i]) { + blocked_mid = true; + break; + } + bool eq_e0 = (cat[i] == cat_e0); + r.leader[i] = en[i] && (is_e0 || (!bc[i] && !eq_e0 && !blocked_mid)); + } + + int cnt = 0; + for (int i = 0; i < n; i++) { + r.slot[i] = cnt; + if (r.leader[i]) + cnt++; + } + r.M = cnt; + + for (int k = 0; k < n; k++) { + if (bc[k]) { + r.dsel[k] = (r.M >= 1) ? (r.M - 1) : 0; + } else if (en[k]) { + for (int i = 0; i < n; i++) + if (r.leader[i] && cat[i] == cat[k]) { + r.dsel[k] = r.slot[i]; + break; + } + } + } + return r; + } + + // Direction-aware wrapper: reverse inputs for MSB-first scans. + AllocResult compute_alloc_dir(const vector &en, const vector &bc, + const vector &cat, int n, bool msb_first) const + { + if (!msb_first) + return compute_alloc(en, bc, cat, n); + vector er(n), br(n), cr(n); + for (int i = 0; i < n; i++) { + er[i] = en[n - 1 - i]; + br[i] = bc[n - 1 - i]; + cr[i] = cat[n - 1 - i]; + } + AllocResult rr = compute_alloc(er, br, cr, n); + AllocResult r; + r.dsel.assign(n, 0); + r.leader.assign(n, 0); + r.slot.assign(n, 0); + r.M = rr.M; + for (int i = 0; i < n; i++) { + r.dsel[i] = rr.dsel[n - 1 - i]; + r.leader[i] = rr.leader[n - 1 - i]; + r.slot[i] = rr.slot[n - 1 - i]; + } + return r; + } + + // ---------------------------------------------------------------- + // Test vectors. `nval` is the number of distinct label values (2^c for + // the category, 2^a for the xbar attribute). The vectors deliberately + // include the all-distinct/all-enabled case so that an allocator whose + // slot count is smaller than the number of distinct categories (i.e. one + // that can overflow and diverge from the closed form) is rejected. + // ---------------------------------------------------------------- + struct TestVector { + vector en; + vector bc; + vector label; + }; + + vector make_vectors(int n, int nval, bool with_bc) const + { + vector vs; + auto lab = [&](int mul, int add) { + vector f(n); + for (int k = 0; k < n; k++) + f[k] = ((k * mul + add) % nval + nval) % nval; + return f; + }; + + // All disabled. + { TestVector t; t.en.assign(n, 0); t.bc.assign(n, 0); t.label = lab(1, 0); vs.push_back(t); } + + // Single enabled lane (each lane), no bc. + for (int k = 0; k < n; k++) { + TestVector t; t.en.assign(n, 0); t.bc.assign(n, 0); + t.en[k] = 1; t.label = lab(1, 0); vs.push_back(t); + } + + // All enabled, all same category. + { TestVector t; t.en.assign(n, 1); t.bc.assign(n, 0); t.label.assign(n, 0); vs.push_back(t); } + // All enabled, all distinct categories (overflow boundary). + { TestVector t; t.en.assign(n, 1); t.bc.assign(n, 0); t.label = lab(1, 0); vs.push_back(t); } + { TestVector t; t.en.assign(n, 1); t.bc.assign(n, 0); t.label = lab(3, 1); vs.push_back(t); } + { TestVector t; t.en.assign(n, 1); t.bc.assign(n, 0); t.label = lab(-1, nval - 1); vs.push_back(t); } + + // Prefix enable masks en[k..n-1]. + for (int k = 0; k < n; k++) { + TestVector t; t.en.assign(n, 0); t.bc.assign(n, 0); + for (int j = k; j < n; j++) t.en[j] = 1; + t.label = lab(5, 2); vs.push_back(t); + } + // Suffix enable masks en[0..k]. + for (int k = 0; k < n; k++) { + TestVector t; t.en.assign(n, 0); t.bc.assign(n, 0); + for (int j = 0; j <= k; j++) t.en[j] = 1; + t.label = lab(7, 3); vs.push_back(t); + } + + // Pairs with distinct categories. + for (int i = 0; i < n; i++) + for (int j = i + 1; j < n && j < i + 3; j++) { + TestVector t; t.en.assign(n, 0); t.bc.assign(n, 0); + t.en[i] = 1; t.en[j] = 1; t.label = lab(2, 0); + t.label[i] = (i + 1) % nval; t.label[j] = (j + 5) % nval; + vs.push_back(t); + } + + // Broadcast corners. + if (with_bc) { + // One bc lane among several enabled lanes. + for (int b = 0; b < n; b += std::max(1, n / 6)) { + TestVector t; t.en.assign(n, 1); t.bc.assign(n, 0); + t.bc[b] = 1; t.label = lab(3, 0); vs.push_back(t); + } + // Leading bc lane (bc lane is E0). + { TestVector t; t.en.assign(n, 1); t.bc.assign(n, 0); t.bc[0] = 1; t.label = lab(1, 0); vs.push_back(t); } + // bc lane not enabled. + { TestVector t; t.en.assign(n, 0); t.bc.assign(n, 0); + for (int j = 0; j < n; j += 2) t.en[j] = 1; + t.bc[1] = 1; t.bc[3 % n] = 1; t.label = lab(2, 1); vs.push_back(t); } + // All bc, all enabled. + { TestVector t; t.en.assign(n, 1); t.bc.assign(n, 1); t.label = lab(1, 0); vs.push_back(t); } + } + + // Pseudo-random coverage. + uint64_t lfsr = 0x1234567089abcdefULL; + for (int r = 0; r < 40; r++) { + lfsr ^= lfsr << 13; + lfsr ^= lfsr >> 7; + lfsr ^= lfsr << 17; + TestVector t; t.en.resize(n); t.bc.resize(n); t.label.resize(n); + uint64_t f = lfsr * 2654435761ULL; + for (int k = 0; k < n; k++) { + t.en[k] = (lfsr >> (k % 64)) & 1; + t.bc[k] = with_bc ? ((lfsr >> ((k + 7) % 64)) & 1) : 0; + t.label[k] = (int)((f >> ((k * 3) % 60)) % (uint64_t)nval); + } + vs.push_back(t); + } + return vs; + } + + // ---------------------------------------------------------------- + // Evaluate `out_sig` under input assignments, returning the full Const. + // ---------------------------------------------------------------- + bool eval_root(ConstEval &ce, const vector> &sets, + const SigSpec &out_sig, Const &result, int64_t cone_est) + { + charge_eval(cone_est); + ce.push(); + for (auto &s : sets) + ce.set(s.first, s.second); + SigSpec out = out_sig; + SigSpec undef; + bool ok = ce.eval(out, undef); + if (ok && out.is_fully_const()) + result = out.as_const(); + else + ok = false; + ce.pop(); + return ok; + } + + static int lane_val(const Const &c, int lane, int elem_w) + { + int v = 0; + for (int b = 0; b < elem_w; b++) { + int p = lane * elem_w + b; + if (p < GetSize(c) && c[p] == State::S1) + v |= 1 << b; + } + return v; + } + + // ---------------------------------------------------------------- + // Group a set of cone-leaf bits into per-lane fields of equal width, + // using each bit's wire/offset and the wire's lane stride (width / n). + // Returns a lane-major SigSpec (lane k -> bits [k*c +: c]) and the per- + // lane width c, plus, for each cat key, the (wire,offset) identity so a + // caller can locate the cat sub-field inside a wider attr field. + // ---------------------------------------------------------------- + // A per-lane sub-field is identified by (source id, within-lane offset), + // where the source is either a flat lane-strided bus (id = wire name) or + // a group of per-lane indexed wires "base[i]" (id = base name). This lets + // a category span several differently-laid-out buses (e.g. a flat set_q[] + // plus per-lane bank_q[i][1:0] wires). + struct FieldKey { + std::string id; + int offset; + bool operator<(const FieldKey &o) const { + if (id != o.id) + return id < o.id; + return offset < o.offset; + } + bool operator==(const FieldKey &o) const { + return id == o.id && offset == o.offset; + } + }; + + bool lane_of_bit(SigBit bit, int n, std::string &id, int &lane, int &offset) + { + if (!bit.wire) + return false; + Wire *w = bit.wire; + std::string base; + int idx = -1; + if (parse_indexed_port_name(w, base, idx)) { + id = base; + lane = idx; + offset = bit.offset; + } else { + int width = GetSize(w); + if (width % n != 0) + return false; + int stride = width / n; + id = w->name.str(); + lane = bit.offset / stride; + offset = bit.offset % stride; + } + return lane >= 0 && lane < n; + } + + bool group_lane_field(const pool &bits, int n, SigSpec &field, + int &c, vector *keys_out = nullptr) + { + if (bits.empty() || n <= 0) + return false; + // Per-lane indexed wires may start at a non-zero base index; normalize + // each source's lane numbering to start at its minimum. + dict src_min_lane; + vector> recs; // id, lane, off, bit + for (auto bit : bits) { + std::string id; + int lane, off; + if (!lane_of_bit(bit, n, id, lane, off)) + return false; + if (!src_min_lane.count(id) || lane < src_min_lane[id]) + src_min_lane[id] = lane; + recs.push_back({id, lane, off, bit}); + } + vector> per_lane(n); + for (auto &r : recs) { + std::string id = std::get<0>(r); + int lane = std::get<1>(r) - src_min_lane[id]; + int off = std::get<2>(r); + if (lane < 0 || lane >= n) + return false; + if (!per_lane[lane].emplace(FieldKey{id, off}, std::get<3>(r)).second) + return false; + } + std::set keys; + for (auto &kv : per_lane[0]) + keys.insert(kv.first); + if (keys.empty()) + return false; + for (int k = 0; k < n; k++) { + if (GetSize(per_lane[k]) != GetSize(keys)) + return false; + for (auto &kv : per_lane[k]) + if (!keys.count(kv.first)) + return false; + } + c = GetSize(keys); + field = SigSpec(); + vector ordered(keys.begin(), keys.end()); + for (int k = 0; k < n; k++) + for (auto &key : ordered) + field.append(per_lane[k].at(key)); + if (keys_out != nullptr) + *keys_out = ordered; + return true; + } + + // ---------------------------------------------------------------- + // A matched region: the shared (en,bc,cat) scan plus the dsel root. + // ---------------------------------------------------------------- + struct Region { + // dsel + SigSpec dsel_sig; + std::string dsel_name; + int n = 0; + int field_w = 0; + SigSpec en_sig, bc_sig, cat_sig; + bool has_bc = false; + int c = 0; + bool msb_first = false; + Cell *anchor = nullptr; + pool dsel_cut_cells; + vector cat_keys; + }; + + // ---- small emit helpers ---- + SigBit emit_not(Cell *anchor, SigBit a) + { + Cell *cell = anchor; + SigBit o = module->Not(NEW_ID2_SUFFIX("ffa_not"), SigSpec(a))[0]; + cells_added++; + return o; + } + SigBit emit_and(Cell *anchor, SigBit a, SigBit b) + { + Cell *cell = anchor; + SigBit o = module->And(NEW_ID2_SUFFIX("ffa_and"), SigSpec(a), SigSpec(b))[0]; + cells_added++; + return o; + } + SigBit emit_or(Cell *anchor, SigBit a, SigBit b) + { + Cell *cell = anchor; + SigBit o = module->Or(NEW_ID2_SUFFIX("ffa_or"), SigSpec(a), SigSpec(b))[0]; + cells_added++; + return o; + } + SigBit emit_reduce_or(Cell *anchor, SigSpec bits) + { + bits = sigmap(bits); + if (GetSize(bits) == 0) + return State::S0; + if (GetSize(bits) == 1) + return bits[0]; + Cell *cell = anchor; + SigBit o = module->ReduceOr(NEW_ID2_SUFFIX("ffa_ror"), bits)[0]; + cells_added++; + return o; + } + SigBit emit_eq_sig(Cell *anchor, SigSpec a, SigSpec b) + { + Cell *cell = anchor; + SigBit o = module->Eq(NEW_ID2_SUFFIX("ffa_eq"), a, b)[0]; + cells_added++; + return o; + } + SigBit emit_eq_const(Cell *anchor, SigSpec a, int value, int width) + { + return emit_eq_sig(anchor, zext_sig(a, width), Const(value, width)); + } + SigSpec emit_mux(Cell *anchor, SigSpec a, SigSpec b, SigBit s) + { + Cell *cell = anchor; + SigSpec o = module->Mux(NEW_ID2_SUFFIX("ffa_mux"), a, b, SigSpec(s)); + cells_added++; + return o; + } + SigSpec emit_bmux(Cell *anchor, SigSpec table, SigSpec sel, int width) + { + Cell *cell = anchor; + Wire *o = module->addWire(NEW_ID2_SUFFIX("ffa_bmux"), width); + module->addBmux(NEW_ID2_SUFFIX("ffa_bmux_cell"), table, sel, o); + cells_added++; + return SigSpec(o); + } + + // Exclusive prefix-sum of `bits` as a linear $add cascade. Each running + // sum is consumed downstream, so opt_parallel_prefix -arith later rebuilds + // the cascade into a shared log-depth network. Returns slot[i] and the + // total M (inclusive sum). + void emit_prefix_count(Cell *anchor, const vector &bits, int cnt_w, + vector &slot, SigSpec &total) + { + Cell *cell = anchor; + int n = GetSize(bits); + slot.assign(n, SigSpec()); + SigSpec acc = Const(0, cnt_w); + for (int i = 0; i < n; i++) { + slot[i] = acc; + Wire *sum = module->addWire(NEW_ID2_SUFFIX("ffa_pre"), cnt_w); + module->addAdd(NEW_ID2_SUFFIX("ffa_pre_add"), acc, SigSpec(bits[i]), sum); + cells_added++; + acc = SigSpec(sum); + } + total = acc; + } + + // Emit the shared leader/slot scan from (en,bc,cat). Fills leader[], + // slot[] (cnt_w bits), total M, and the lane categories cat_lane[]. + void emit_scan(const Region &rg, vector &leader, vector &slot, + SigSpec &total, int cnt_w, vector &cat_lane) + { + Cell *anchor = rg.anchor; + int n = rg.n, c = rg.c; + SigSpec en = sigmap(rg.en_sig); + SigSpec bc = rg.has_bc ? sigmap(rg.bc_sig) : SigSpec(); + SigSpec cat = sigmap(rg.cat_sig); + + // priority order: index p -> lane + auto lane_of = [&](int p) { return rg.msb_first ? (n - 1 - p) : p; }; + + vector en_p(n), bc_p(n); + cat_lane.assign(n, SigSpec()); + for (int p = 0; p < n; p++) { + int l = lane_of(p); + en_p[p] = en[l]; + bc_p[p] = rg.has_bc ? bc[l] : SigBit(State::S0); + cat_lane[p] = cat.extract(l * c, c); + } + + // anyEnBefore[p] = OR_{q any_before(n); + for (int p = 0; p < n; p++) { + SigSpec prev; + for (int q = 0; q < p; q++) + prev.append(en_p[q]); + any_before[p] = emit_reduce_or(anchor, prev); + } + // isE0[p] = en_p[p] & ~anyEnBefore[p] + vector is_e0(n); + for (int p = 0; p < n; p++) + is_e0[p] = emit_and(anchor, en_p[p], emit_not(anchor, any_before[p])); + // catE0[b] = OR_p (isE0[p] & cat[p][b]) + SigSpec cat_e0; + for (int b = 0; b < c; b++) { + SigSpec terms; + for (int p = 0; p < n; p++) + terms.append(emit_and(anchor, is_e0[p], cat_lane[p][b])); + cat_e0.append(emit_reduce_or(anchor, terms)); + } + // eqE0[p] = (cat[p]==catE0) + vector eq_e0(n); + for (int p = 0; p < n; p++) + eq_e0[p] = emit_eq_sig(anchor, cat_lane[p], cat_e0); + + // qual[p] = anyEnBefore[p] & en_p[p] & ~bc_p[p] + vector qual(n); + for (int p = 0; p < n; p++) { + SigBit t = emit_and(anchor, any_before[p], en_p[p]); + qual[p] = emit_and(anchor, t, emit_not(anchor, bc_p[p])); + } + // catEq[p][q] = (cat[p]==cat[q]) (only q

blocked_mid(n); + for (int p = 0; p < n; p++) { + SigSpec terms; + for (int q = 0; q < p; q++) { + SigBit eq = emit_eq_sig(anchor, cat_lane[p], cat_lane[q]); + terms.append(emit_and(anchor, qual[q], eq)); + } + blocked_mid[p] = emit_reduce_or(anchor, terms); + } + // leader[p] = en_p[p] & (isE0[p] | (~bc_p[p] & ~eqE0[p] & ~blockedMid[p])) + vector leader_p(n); + for (int p = 0; p < n; p++) { + SigBit a = emit_and(anchor, emit_not(anchor, bc_p[p]), emit_not(anchor, eq_e0[p])); + a = emit_and(anchor, a, emit_not(anchor, blocked_mid[p])); + SigBit b = emit_or(anchor, is_e0[p], a); + leader_p[p] = emit_and(anchor, en_p[p], b); + } + // slot[p] = exclusive prefix count + vector slot_p; + emit_prefix_count(anchor, leader_p, cnt_w, slot_p, total); + + // map priority order back to lanes + leader.assign(n, SigBit()); + slot.assign(n, SigSpec()); + for (int p = 0; p < n; p++) { + int l = lane_of(p); + leader[l] = leader_p[p]; + slot[l] = slot_p[p]; + } + } + + // dsel gather. Returns lane-major SigSpec (lane k -> [k*field_w +: field_w]). + SigSpec emit_dsel(const Region &rg, const vector &leader, + const vector &slot, SigSpec total, int cnt_w) + { + Cell *anchor = rg.anchor; + int n = rg.n, c = rg.c, w = rg.field_w; + SigSpec en = sigmap(rg.en_sig); + SigSpec bc = rg.has_bc ? sigmap(rg.bc_sig) : SigSpec(); + SigSpec cat = sigmap(rg.cat_sig); + + // bc rank: (M>=1) ? M-1 : 0 + SigBit any_leader = emit_reduce_or(anchor, total); + Cell *cell = anchor; + Wire *mm1w = module->addWire(NEW_ID2_SUFFIX("ffa_Mm1"), cnt_w); + module->addSub(NEW_ID2_SUFFIX("ffa_Mm1_sub"), total, Const(1, cnt_w), mm1w); + cells_added++; + SigSpec bc_rank = emit_mux(anchor, Const(0, cnt_w), SigSpec(mm1w), any_leader); + + SigSpec out; + for (int k = 0; k < n; k++) { + SigSpec cat_k = cat.extract(k * c, c); + // en rank: slot of the unique leader with cat==cat[k] + vector g(n); + for (int i = 0; i < n; i++) { + SigBit eq = emit_eq_sig(anchor, cat.extract(i * c, c), cat_k); + g[i] = emit_and(anchor, leader[i], eq); + } + SigSpec en_rank(Const(0, cnt_w)); + for (int b = 0; b < cnt_w; b++) { + SigSpec terms; + for (int i = 0; i < n; i++) + terms.append(emit_and(anchor, g[i], slot[i][b])); + en_rank[b] = emit_reduce_or(anchor, terms); + } + // dsel[k] = bc[k] ? bc_rank : (en[k] ? en_rank : 0) + SigSpec sel_en = emit_mux(anchor, Const(0, cnt_w), en_rank, en[k]); + SigSpec val = sel_en; + if (rg.has_bc) + val = emit_mux(anchor, sel_en, bc_rank, bc[k]); + out.append(zext_sig(val, w)); + } + return out; + } + + // ---------------------------------------------------------------- + // xbar (per-slot field gather): xbar_slot[s] = (s ftab; // 2^a entries, slot_w bits each + vector attr_keys; + Cell *anchor = nullptr; + pool cut_cells; + }; + + SigSpec emit_xbar(const Region &rg, const XbarCand &xb, const vector &leader, + const vector &slot, int cnt_w) + { + Cell *anchor = rg.anchor; + int n = rg.n, a = xb.a; + SigSpec attr = sigmap(xb.attr_sig); + + // Build the function table as a flat Const for bmux. + SigSpec table; + for (int v = 0; v < (1 << a); v++) + table.append(xb.ftab[v]); + + SigSpec out; + for (int s = 0; s < xb.nb; s++) { + // pick[i] = leader[i] && slot[i]==s + vector pick(n); + SigSpec valid_terms; + for (int i = 0; i < n; i++) { + SigBit eqs = emit_eq_const(anchor, slot[i], s, cnt_w); + pick[i] = emit_and(anchor, leader[i], eqs); + valid_terms.append(pick[i]); + } + // attrGather[s] = attr of the leader at slot s (one-hot select) + SigSpec attr_gather(Const(0, a)); + for (int b = 0; b < a; b++) { + SigSpec terms; + for (int i = 0; i < n; i++) + terms.append(emit_and(anchor, pick[i], attr[i * a + b])); + attr_gather[b] = emit_reduce_or(anchor, terms); + } + SigBit valid = emit_reduce_or(anchor, valid_terms); + SigSpec block = emit_bmux(anchor, table, attr_gather, xb.slot_w); + SigSpec gated = emit_mux(anchor, Const(0, xb.slot_w), block, valid); + out.append(gated); + } + return out; + } + + // ---------------------------------------------------------------- + // Candidate bus collection for a root cone. + // ---------------------------------------------------------------- + // Candidate enable/broadcast buses: width-N buses whose every bit is an + // internal (cone-cell-output) signal. The enable/broadcast lanes are + // computed signals (e.g. valid & format), not the leaf categories, so + // restricting to internal bits sidesteps the flood of wide intermediate + // nets that otherwise swamps a generic wire-run sweep. + void collect_lane_buses(const pool &cone_cells, int n, + vector &width_n_buses) + { + pool internal_bits; + for (auto c : cone_cells) + for (auto &conn : c->connections()) + if (c->output(conn.first)) + for (auto bit : sigmap(conn.second)) + if (bit.wire) + internal_bits.insert(bit); + + pool seen; + auto all_internal = [&](const SigSpec &s) { + for (auto bit : s) + if (!bit.wire || !internal_bits.count(bit)) + return false; + return true; + }; + auto add = [&](const SigSpec &sig, const std::string &nm) { + SigSpec s = sigmap(sig); + if (GetSize(s) != n || !sig_bus_ok(s) || !all_internal(s)) + return; + if (!seen.insert(s).second) + return; + width_n_buses.push_back({s, nm, 0, 0}); + }; + + for (auto w : module->wires()) + if (GetSize(w) == n) + add(SigSpec(w), w->name.str()); + for (auto &bus : collect_cone_split_buses(internal_bits)) + if (bus.elem_width == 1 && bus.entries == n) + add(bus.sig, bus.name); + + // Region inputs (enable/broadcast) sit just above the leaves; order + // candidates shallowest-first so the deep intermediate nets of the + // allocation network are only reached if the budget allows. + dict depth = compute_cone_depths(cone_cells); + auto bus_depth = [&](const SigSpec &s) { + int d = 0; + for (auto bit : s) { + Cell *drv = bit_to_driver.at(bit, nullptr); + if (drv != nullptr) + d = std::max(d, depth.at(drv, 1 << 30)); + } + return d; + }; + std::stable_sort(width_n_buses.begin(), width_n_buses.end(), + [&](const BusCand &a, const BusCand &b) { + return bus_depth(a.sig) < bus_depth(b.sig); + }); + const int max_en_bc = 24; + if (GetSize(width_n_buses) > max_en_bc) + width_n_buses.resize(max_en_bc); + + log_debug(" collect_lane_buses: %d width-%d internal bus(es) (capped)\n", + GetSize(width_n_buses), n); + for (auto &b : width_n_buses) + log_debug(" en/bc cand %s (depth %d)\n", b.name.c_str(), bus_depth(b.sig)); + } + + // ---------------------------------------------------------------- + // Try to match a dsel region rooted at `root_sig` (n lanes x field_w). + // ---------------------------------------------------------------- + bool match_dsel(const SigSpec &root_sig, const std::string &root_name, int n, int field_w, + Region &out) + { + pool cone_cells; + pool leaf_bits; + int max_cone = std::max(512, max_n * 256); + int max_leaf = max_n * max_n + max_n * 64 + max_n; + if (!get_cone(root_sig, cone_cells, leaf_bits, max_cone, max_leaf)) + return false; + if (cone_cells.empty()) + return false; + + log_debug("ffa: match_dsel root %s (n=%d, w=%d): cone %d cells, %d leaves\n", + root_name.c_str(), n, field_w, GetSize(cone_cells), GetSize(leaf_bits)); + + vector lane_buses; + collect_lane_buses(cone_cells, n, lane_buses); + log_debug(" %d width-%d lane bus candidate(s)\n", GetSize(lane_buses), n); + for (auto &b : lane_buses) + log_debug(" lane bus %s\n", b.name.c_str()); + if (GetSize(lane_buses) < 1) + return false; + + ConstEval ce(module); + int64_t cone_est = GetSize(cone_cells) + 16; + const int max_fp = 48; + int fp = 0; + + for (auto &en_bus : lane_buses) { + if (fp >= max_fp || walk_exhausted() || eval_exhausted()) + break; + pool en_bits = sig_bit_pool(en_bus.sig); + + // bc options: none, or each other 1-bit/lane bus. + vector bc_opts; + bc_opts.push_back(nullptr); + for (auto &b : lane_buses) + if (b.sig != en_bus.sig) + bc_opts.push_back(&b); + + for (auto bc_bus : bc_opts) { + if (fp >= max_fp || walk_exhausted() || eval_exhausted()) + break; + + pool allowed_eb = en_bits; + if (bc_bus) + for (auto bit : sig_bit_pool(bc_bus->sig)) + allowed_eb.insert(bit); + + // Find the category leaves: cut at (en,bc), gather extras. + pool extra; + if (!cut_cone_extra_leaves(root_sig, allowed_eb, GetSize(cone_cells) + 32, + extra, n * max_cat_w + 8)) { + log_debug(" en=%s bc=%s: too many extra leaves\n", en_bus.name.c_str(), + bc_bus ? bc_bus->name.c_str() : "-"); + continue; + } + if (extra.empty()) + continue; + + SigSpec cat_sig; + int c = 0; + vector cat_keys; + if (!group_lane_field(extra, n, cat_sig, c, &cat_keys)) { + log_debug(" en=%s bc=%s: cat grouping failed (%d extra leaves)\n", + en_bus.name.c_str(), bc_bus ? bc_bus->name.c_str() : "-", GetSize(extra)); + continue; + } + if (c < 1 || c > max_cat_w) + continue; + if ((1 << c) > (1 << field_w)) // need ranks to fit the field + continue; + log_debug(" en=%s bc=%s: cat=%dx%d candidate\n", en_bus.name.c_str(), + bc_bus ? bc_bus->name.c_str() : "-", n, c); + + // Confirm the cut closes at exactly (en,bc,cat). + pool allowed = allowed_eb; + for (auto bit : sig_bit_pool(cat_sig)) + allowed.insert(bit); + pool hit; + pool cut_cells; + if (!cut_cone_walk(root_sig, allowed, GetSize(cone_cells) + 32, &hit, &cut_cells, + &allowed, &leaf_bits, &cone_cells)) { + log_debug(" en=%s bc=%s cat=%dx%d: cut not closed (%s)\n", en_bus.name.c_str(), + bc_bus ? bc_bus->name.c_str() : "-", n, c, last_cut_fail.c_str()); + continue; + } + + // Forced inputs must not be driven inside the cut cone. + bool forced_conflict = false; + for (auto bit : allowed) { + Cell *drv = bit_to_driver.at(bit, nullptr); + if (drv != nullptr && cut_cells.count(drv)) { + forced_conflict = true; + break; + } + } + if (forced_conflict) + continue; + + // Fingerprint both scan directions. + for (int dir = 0; dir < 2; dir++) { + if (fp >= max_fp || eval_exhausted()) + break; + bool msb_first = (dir == 1); + fp++; + bool fpm = fingerprint_dsel(ce, root_sig, n, field_w, en_bus.sig, + bc_bus ? bc_bus->sig : SigSpec(), bc_bus != nullptr, + cat_sig, c, msb_first, cone_est); + log_debug(" en=%s bc=%s cat=%dx%d %s: fingerprint %s\n", en_bus.name.c_str(), + bc_bus ? bc_bus->name.c_str() : "-", n, c, + msb_first ? "MSB" : "LSB", fpm ? "MATCH" : "no"); + if (fpm) { + out.dsel_sig = root_sig; + out.dsel_name = root_name; + out.n = n; + out.field_w = field_w; + out.en_sig = en_bus.sig; + out.bc_sig = bc_bus ? bc_bus->sig : SigSpec(); + out.has_bc = (bc_bus != nullptr); + out.cat_sig = cat_sig; + out.c = c; + out.msb_first = msb_first; + out.cat_keys = cat_keys; + out.dsel_cut_cells = cut_cells; + find_anchor_driver(root_sig, out.anchor); + return true; + } + } + } + } + return false; + } + + bool fingerprint_dsel(ConstEval &ce, const SigSpec &root, int n, int field_w, + const SigSpec &en_sig, const SigSpec &bc_sig, bool has_bc, + const SigSpec &cat_sig, int c, bool msb_first, int64_t cone_est) + { + int nval = 1 << c; + vector vs = make_vectors(n, nval, has_bc); + SigSpec en_s = sigmap(en_sig); + SigSpec bc_s = has_bc ? sigmap(bc_sig) : SigSpec(); + SigSpec cat_s = sigmap(cat_sig); + + for (auto &tv : vs) { + vector> sets; + sets.push_back({en_s, pack_lanes(tv.en, 1)}); + if (has_bc) + sets.push_back({bc_s, pack_lanes(tv.bc, 1)}); + sets.push_back({cat_s, pack_lanes(tv.label, c)}); + + Const res; + if (!eval_root(ce, sets, root, res, cone_est)) + return false; + + AllocResult ar = compute_alloc_dir(tv.en, tv.bc, tv.label, n, msb_first); + for (int k = 0; k < n; k++) { + int got = lane_val(res, k, field_w); + int exp = ar.dsel[k] & ((1 << field_w) - 1); + if (got != exp) + return false; + } + } + return true; + } + + // ---------------------------------------------------------------- + // Try to match the xbar per-slot field gather sharing the region scan. + // ---------------------------------------------------------------- + bool match_xbar(const Region &rg, const BusCand &cand, XbarCand &out) + { + int total_bits = GetSize(cand.sig); + int field_w = rg.field_w; + int nb = 1 << field_w; // slot count = 2^W + if (nb < 2 || total_bits % nb != 0) + return false; + int slot_w = total_bits / nb; + if (slot_w < 1 || slot_w > 64) { + log_debug(" xbar %s: slot_w=%d out of range\n", cand.name.c_str(), slot_w); + return false; + } + + SigSpec root = sigmap(cand.sig); + pool cone_cells; + pool leaf_bits; + int max_cone = std::max(512, max_n * 256); + int max_leaf = max_n * max_n + max_n * 64 + max_n; + if (!get_cone(root, cone_cells, leaf_bits, max_cone, max_leaf)) + return false; + if (cone_cells.empty()) + return false; + + // Cut at (en,bc); the remaining leaves are the per-lane attr field. + pool allowed_eb = sig_bit_pool(rg.en_sig); + if (rg.has_bc) + for (auto bit : sig_bit_pool(rg.bc_sig)) + allowed_eb.insert(bit); + pool extra; + if (!cut_cone_extra_leaves(root, allowed_eb, GetSize(cone_cells) + 32, + extra, rg.n * max_attr_w + 8)) { + log_debug(" xbar %s: too many extra leaves on (en,bc) cut\n", cand.name.c_str()); + return false; + } + if (extra.empty()) + return false; + + SigSpec attr_sig; + int a = 0; + vector attr_keys; + if (!group_lane_field(extra, rg.n, attr_sig, a, &attr_keys)) { + log_debug(" xbar %s: attr grouping failed (%d extra leaves)\n", + cand.name.c_str(), GetSize(extra)); + return false; + } + if (a < rg.c || a > max_attr_w) { + log_debug(" xbar %s: attr width %d out of range [%d,%d]\n", + cand.name.c_str(), a, rg.c, max_attr_w); + return false; + } + log_debug(" xbar %s: nb=%d slot_w=%d attr=%dx%d\n", cand.name.c_str(), nb, slot_w, rg.n, a); + + // Confirm closure at (en,bc,attr). + pool allowed = allowed_eb; + for (auto bit : sig_bit_pool(attr_sig)) + allowed.insert(bit); + pool hit; + pool cut_cells; + if (!cut_cone_walk(root, allowed, GetSize(cone_cells) + 32, &hit, &cut_cells, + &allowed, &leaf_bits, &cone_cells)) { + log_debug(" xbar %s: cut not closed (%s)\n", cand.name.c_str(), last_cut_fail.c_str()); + return false; + } + for (auto bit : allowed) { + Cell *drv = bit_to_driver.at(bit, nullptr); + if (drv != nullptr && cut_cells.count(drv)) { + log_debug(" xbar %s: forced bit driven inside cone\n", cand.name.c_str()); + return false; + } + } + + // The attr field must be a superset of the category field (so the + // scan's leadership and the xbar gather see consistent lanes). + // (cat keys identify wire/offset; verify each is present in attr.) + // We do not strictly require this for correctness (the fingerprint + // is authoritative), but it weeds out unrelated buses cheaply. + + ConstEval ce(module); + int64_t cone_est = GetSize(cone_cells) + 16; + + // Learn f(v): force lane 0 (priority E0) the sole leader with attr=v. + int e0_lane = rg.msb_first ? (rg.n - 1) : 0; + vector ftab(1 << a); + SigSpec en_s = sigmap(rg.en_sig); + SigSpec bc_s = rg.has_bc ? sigmap(rg.bc_sig) : SigSpec(); + SigSpec attr_s = sigmap(attr_sig); + SigSpec slot0 = root.extract(0, slot_w); + for (int v = 0; v < (1 << a); v++) { + vector en(rg.n, 0), bc(rg.n, 0), attr(rg.n, 0); + en[e0_lane] = 1; + attr[e0_lane] = v; + vector> sets; + sets.push_back({en_s, pack_lanes(en, 1)}); + if (rg.has_bc) + sets.push_back({bc_s, pack_lanes(bc, 1)}); + sets.push_back({attr_s, pack_lanes(attr, a)}); + Const res; + if (!eval_root(ce, sets, slot0, res, cone_est)) { + log_debug(" xbar %s: ftab learn failed at v=%d\n", cand.name.c_str(), v); + return false; + } + ftab[v] = res; + } + + // Fingerprint the full xbar against the gather+table reference. + int nval = 1 << a; + vector vs = make_vectors(rg.n, nval, rg.has_bc); + for (auto &tv : vs) { + vector> sets; + sets.push_back({en_s, pack_lanes(tv.en, 1)}); + if (rg.has_bc) + sets.push_back({bc_s, pack_lanes(tv.bc, 1)}); + sets.push_back({attr_s, pack_lanes(tv.label, a)}); + Const res; + if (!eval_root(ce, sets, root, res, cone_est)) { + log_debug(" xbar %s: eval failed during fingerprint\n", cand.name.c_str()); + return false; + } + + // Derive cat labels from the attr labels via the cat keys' bit + // positions inside attr. + vector catlab(rg.n); + for (int k = 0; k < rg.n; k++) + catlab[k] = cat_from_attr(tv.label[k], rg.cat_keys, attr_keys); + AllocResult ar = compute_alloc_dir(tv.en, tv.bc, catlab, rg.n, rg.msb_first); + + // Expected per-slot block. + for (int s = 0; s < nb; s++) { + int leader_lane = -1; + for (int i = 0; i < rg.n; i++) + if (ar.leader[i] && ar.slot[i] == s) { leader_lane = i; break; } + for (int b = 0; b < slot_w; b++) { + bool got = (s * slot_w + b < GetSize(res)) && res[s * slot_w + b] == State::S1; + bool exp; + if (leader_lane < 0) + exp = false; + else { + const Const &fv = ftab[tv.label[leader_lane]]; + exp = (b < GetSize(fv)) && fv[b] == State::S1; + } + if (got != exp) { + log_debug(" xbar %s: fingerprint mismatch slot %d bit %d\n", + cand.name.c_str(), s, b); + return false; + } + } + } + } + log_debug(" xbar %s: MATCH\n", cand.name.c_str()); + + out.sig = root; + out.name = cand.name; + out.nb = nb; + out.slot_w = slot_w; + out.attr_sig = attr_sig; + out.a = a; + out.ftab = ftab; + out.attr_keys = attr_keys; + out.cut_cells = cut_cells; + find_anchor_driver(root, out.anchor); + return true; + } + + // Map an attr label value to its category label value, using the shared + // (id,offset) keys: cat bit cb corresponds to the attr bit whose key + // matches cat_keys[cb]. + int cat_from_attr(int attr_val, const vector &cat_keys, + const vector &attr_keys) + { + int catval = 0; + for (int cb = 0; cb < GetSize(cat_keys); cb++) + for (int b = 0; b < GetSize(attr_keys); b++) + if (attr_keys[b] == cat_keys[cb]) { + if ((attr_val >> b) & 1) + catval |= 1 << cb; + break; + } + return catval; + } + + // Drive `new_val` onto the cell-driven bits of `root` (bit-aligned), + // leaving folded-constant / undriven bits untouched (they already hold + // the correct value and are not on the deep path). Returns the number of + // bits actually re-driven. + int connect_driven(const SigSpec &root, const SigSpec &new_val, Cell *anchor, + const char *suffix) + { + log_assert(GetSize(root) == GetSize(new_val)); + SigSpec lhs, rhs; + for (int i = 0; i < GetSize(root); i++) { + SigBit rb = sigmap(root[i]); + if (rb.wire && bit_to_driver.at(rb, nullptr) != nullptr) { + lhs.append(root[i]); + rhs.append(new_val[i]); + } + } + if (GetSize(lhs) == 0) + return 0; + disconnect_root(lhs, anchor, suffix); + module->connect(lhs, rhs); + return GetSize(lhs); + } + + // ---------------------------------------------------------------- + // Collect root candidates: per-lane split buses (entries in range). + // ---------------------------------------------------------------- + struct RootSplit { + SigSpec sig; + std::string name; + int n, elem; + }; + vector collect_split_roots() + { + vector roots; + vector all; + for (auto w : module->wires()) + all.push_back(w); + for (auto &bus : collect_split_buses(all)) { + if (bus.entries < min_n || bus.entries > max_n) + continue; + if (bus.elem_width < 1 || bus.elem_width > max_field_w) + continue; + // Keep the raw (un-sigmapped) wire bits so the root stays + // drivable; some field bits may fold to constants (e.g. the + // always-zero MSB of an xbar entry) and only the cell-driven + // bits get re-driven later. So we do NOT require sig_bus_ok + // (which would reject any constant-folded bit); the split-bus + // bits are real wire bits by construction. We only require at + // least one cell-driven bit so pure input ports are skipped. + bool raw_ok = true; + for (auto bit : bus.sig) + if (!bit.wire) { + raw_ok = false; + break; + } + if (!raw_ok) + continue; + bool any_driven = false; + for (auto bit : sigmap(bus.sig)) + if (bit.wire && bit_to_driver.at(bit, nullptr) != nullptr) { + any_driven = true; + break; + } + if (!any_driven) + continue; + roots.push_back({bus.sig, bus.name, bus.entries, bus.elem_width}); + } + // widest fields first (real index buses are the deep ones) + std::stable_sort(roots.begin(), roots.end(), + [](const RootSplit &a, const RootSplit &b) { return a.n > b.n; }); + return roots; + } + + void run() + { + if (module->has_processes_warn()) + return; + + vector roots = collect_split_roots(); + log_debug("ffa: %d split-root candidate(s) in %s\n", GetSize(roots), log_id(module)); + for (auto &r : roots) + log_debug(" cand root %s (n=%d, elem=%d)\n", r.name.c_str(), r.n, r.elem); + + for (auto &root : roots) { + if (walk_exhausted() || eval_exhausted()) + break; + if (root_claimed(root.sig)) + continue; + + Region rg; + if (!match_dsel(root.sig, root.name, root.n, root.elem, rg)) + continue; + + // Find a sibling xbar root that shares this scan. + XbarCand xb; + bool have_xbar = false; + for (auto &cand : roots) { + if (cand.sig == rg.dsel_sig) + continue; + if (root_claimed(cand.sig)) + continue; + // xbar bits must split evenly into 2^W per-slot blocks. + if (GetSize(cand.sig) % (1 << rg.field_w) != 0) + continue; + log_debug("ffa: try xbar cand %s (%d bits) for region %s\n", + cand.name.c_str(), GetSize(cand.sig), rg.dsel_name.c_str()); + if (match_xbar(rg, BusCand{cand.sig, cand.name, cand.n, cand.elem}, xb)) { + have_xbar = true; + break; + } + } + + // Emit the shared scan once. + cell = rg.anchor; + int cnt_w = clog2_int(rg.n + 1); + vector leader; + vector slot; + SigSpec total; + vector cat_lane; + emit_scan(rg, leader, slot, total, cnt_w, cat_lane); + + SigSpec new_dsel = emit_dsel(rg, leader, slot, total, cnt_w); + connect_driven(rg.dsel_sig, new_dsel, rg.anchor, "ffa_dangling"); + claim_region(rg.dsel_sig, rg.dsel_cut_cells); + regions_rewritten++; + + log(" %s: %s <- first_fit_alloc(en=%s%s, cat=%dx%d, %s)\n", + log_id(module), rg.dsel_name.c_str(), + log_signal(rg.en_sig), + rg.has_bc ? stringf(", bc=%s", log_signal(rg.bc_sig)).c_str() : "", + rg.n, rg.c, rg.msb_first ? "MSB-first" : "LSB-first"); + + if (have_xbar) { + SigSpec new_xbar = emit_xbar(rg, xb, leader, slot, cnt_w); + int dn = connect_driven(xb.sig, new_xbar, xb.anchor, "ffa_xbar_dangling"); + claim_region(xb.sig, xb.cut_cells); + log(" + xbar field gather: %s [slots=%d, slot_w=%d, attr=%dx%d, %d bit(s) re-driven]\n", + xb.name.c_str(), xb.nb, xb.slot_w, rg.n, xb.a, dn); + } + } + } +}; + +struct OptFirstFitAllocPass : public Pass { + OptFirstFitAllocPass() : Pass("opt_first_fit_alloc", + "rewrite greedy first-fit resource allocators into log-depth scans") {} + + void help() override + { + // |---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---| + log("\n"); + log(" opt_first_fit_alloc [options] [selection]\n"); + log("\n"); + log("This pass uses functional fingerprinting to detect combinational regions\n"); + log("that implement a greedy first-fit resource allocator: enabled request\n"); + log("lanes are scanned in priority order and the first lane of each category\n"); + log("(a 'leader') is assigned the next free slot, while later lanes of the\n"); + log("same category (and broadcast lanes) inherit that slot.\n"); + log("\n"); + log("The serial loop-carried taken[]/done[] scan produced by the RTL is\n"); + log("replaced with a log-depth network: a priority-encode of the first\n"); + log("enabled lane, an all-pairs category-equality leader test, a prefix-sum\n"); + log("slot assignment, and a rank gather. Where a per-slot field table (an\n"); + log("'xbar') is driven from the same allocation, it is rewritten as a shared\n"); + log("per-slot field gather driven from the same scan.\n"); + log("\n"); + log("The prefix-sums are emitted as linear $add cascades so a subsequent\n"); + log("opt_parallel_prefix pass rebuilds them into shared log-depth networks.\n"); + log("\n"); + log(" -min-width N, -max-width N\n"); + log(" lane-count range to consider (default 4..64).\n"); + log("\n"); + log(" -walk-budget N, -eval-budget N, -attempt-budget N\n"); + log(" per-module work limits for the candidate search.\n"); + log("\n"); + } + + void execute(std::vector args, RTLIL::Design *design) override + { + log_header(design, "Executing OPT_FIRST_FIT_ALLOC pass (greedy first-fit allocator rewrite).\n"); + + int min_n = 4, max_n = 64; + int64_t walk_budget = -1, eval_budget = -1, attempt_budget = -1; + size_t argidx; + for (argidx = 1; argidx < args.size(); argidx++) { + if ((args[argidx] == "-min-width" || args[argidx] == "-min_width") && argidx + 1 < args.size()) { + min_n = std::stoi(args[++argidx]); + continue; + } + if ((args[argidx] == "-max-width" || args[argidx] == "-max_width") && argidx + 1 < args.size()) { + max_n = std::stoi(args[++argidx]); + continue; + } + if ((args[argidx] == "-walk-budget" || args[argidx] == "-walk_budget") && argidx + 1 < args.size()) { + walk_budget = std::stoll(args[++argidx]); + continue; + } + if ((args[argidx] == "-eval-budget" || args[argidx] == "-eval_budget") && argidx + 1 < args.size()) { + eval_budget = std::stoll(args[++argidx]); + continue; + } + if ((args[argidx] == "-attempt-budget" || args[argidx] == "-attempt_budget") && argidx + 1 < args.size()) { + attempt_budget = std::stoll(args[++argidx]); + continue; + } + break; + } + extra_args(args, argidx, design); + + int total_regions = 0; + int total_cells = 0; + for (auto module : design->selected_modules()) { + OptFirstFitAllocWorker worker(module); + worker.min_n = min_n; + worker.max_n = max_n; + if (walk_budget > 0) + worker.walk_budget = walk_budget; + if (eval_budget > 0) + worker.eval_budget = eval_budget; + if (attempt_budget > 0) + worker.attempt_budget = attempt_budget; + worker.run(); + total_regions += worker.regions_rewritten; + total_cells += worker.cells_added; + } + + log("Rewrote %d first-fit alloc region(s); emitted %d new cell(s).\n", + total_regions, total_cells); + + if (total_regions) + Yosys::run_pass("clean -purge"); + } +} OptFirstFitAllocPass; + +PRIVATE_NAMESPACE_END diff --git a/tests/opt/opt_first_fit_alloc.ys b/tests/opt/opt_first_fit_alloc.ys new file mode 100644 index 000000000..07101da6a --- /dev/null +++ b/tests/opt/opt_first_fit_alloc.ys @@ -0,0 +1,592 @@ +# Tests for opt_first_fit_alloc +# +# The pass detects greedy first-fit "running-rank" resource allocators (the +# serial taken[]/done[] loop) and replaces the loop-carried cone with a +# log-depth priority-encode + all-pairs category-equality + prefix-sum + gather +# network. Where a per-slot field table (an "xbar") is driven from the same +# allocation, it is rewritten from the shared scan. +# +# Each group exercises a specific facet: +# A: basic detection + formal equivalence across allocator shapes (N=8). +# B: depth / cell-count bounds after rewrite (the actual QoR win). +# C: the customer slice (dsel + xbar) at N=8 (equiv) and N=16 (structural). +# D: variant detection (bc on/off, LSB/MSB-first scan). +# E: negative / no-op cases (no false rewrites). +# F: extra fanout / input reuse. +# +# Convention: every object the pass emits is named with an `ffa_` suffix, so +# `select w:*ffa_*` is a reliable "did the rewrite fire" probe. + +# ============================================================================ +# Group A: basic shapes (equiv_opt -assert) +# ============================================================================ + +# A1: dsel-only first-fit allocator, N=8 NB=4 (W=2) C=2, LSB-first, with bc. +log -header "A1: dsel-only allocator N=8 (equiv)" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv < no candidate root. +log -header "E3: width below min-width -> no rewrite" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog < Date: Sun, 21 Jun 2026 01:21:06 -0700 Subject: [PATCH 2/2] opt_first_fit_alloc: address Greptile review - pack_lanes: assert elem_w < 32 and pack the full element width instead of silently dropping bits >= 31. - Remove the dead `cell` struct member and its unused assignment in run() (every emit helper shadows it with its own local `cell`). - Decorrelate the pseudo-random bc bits from en (independent mix) so they no longer share an LFSR bit (e.g. en[7]/bc[0] for n=8). - Add purpose comments to fingerprint_dsel and lane_of_bit. Declined the std::stoi/std::stoll arg-parsing suggestion: it matches the established convention in sibling passes (opt_argmax, opt_priority_onehot). Co-authored-by: Cursor --- passes/opt/opt_first_fit_alloc.cc | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/passes/opt/opt_first_fit_alloc.cc b/passes/opt/opt_first_fit_alloc.cc index a4bcb0d38..a2e2af74b 100644 --- a/passes/opt/opt_first_fit_alloc.cc +++ b/passes/opt/opt_first_fit_alloc.cc @@ -41,9 +41,13 @@ static int clog2_int(int x) // lane-major (lane k occupies bits [k*elem_w +: elem_w]). static Const pack_lanes(const vector &vals, int elem_w) { + // Lane values come from the small field widths above (<= max_attr_w), so a + // 32-bit int always holds them. Assert rather than silently truncating, and + // keep b < 32 so we never shift an int by >= its width (undefined behaviour). + log_assert(elem_w < 32); vector bits(vals.size() * elem_w, State::S0); for (int k = 0; k < GetSize(vals); k++) - for (int b = 0; b < elem_w && b < 31; b++) + for (int b = 0; b < elem_w; b++) if ((vals[k] >> b) & 1) bits[k * elem_w + b] = State::S1; return Const(bits); @@ -52,8 +56,6 @@ static Const pack_lanes(const vector &vals, int elem_w) #include "passes/opt/cut_region.h" struct OptFirstFitAllocWorker : CutRegionWorker { - Cell *cell = nullptr; - int min_n = 4; int max_n = 64; int max_field_w = 6; // max index (dsel) element width @@ -249,9 +251,13 @@ struct OptFirstFitAllocWorker : CutRegionWorker { lfsr ^= lfsr << 17; TestVector t; t.en.resize(n); t.bc.resize(n); t.label.resize(n); uint64_t f = lfsr * 2654435761ULL; + // bc draws from an independently mixed word so en[k] and bc[j] never + // share an LFSR bit (a shifted view of `lfsr` would alias them, e.g. + // en[7] and bc[0] for n=8), keeping the random coverage independent. + uint64_t g = lfsr * 0x9E3779B97F4A7C15ULL; for (int k = 0; k < n; k++) { t.en[k] = (lfsr >> (k % 64)) & 1; - t.bc[k] = with_bc ? ((lfsr >> ((k + 7) % 64)) & 1) : 0; + t.bc[k] = with_bc ? ((g >> (k % 64)) & 1) : 0; t.label[k] = (int)((f >> ((k * 3) % 60)) % (uint64_t)nval); } vs.push_back(t); @@ -316,6 +322,11 @@ struct OptFirstFitAllocWorker : CutRegionWorker { } }; + // Resolve a single cone-leaf bit to the lane it belongs to. Returns the + // source `id` (flat bus wire name, or base name for per-lane "base[i]" + // wires), the `lane` index in [0,n), and the bit's `offset` within that + // lane's sub-field. Returns false if the bit has no wire or cannot be + // assigned to one of the n lanes. bool lane_of_bit(SigBit bit, int n, std::string &id, int &lane, int &offset) { if (!bit.wire) @@ -882,6 +893,12 @@ struct OptFirstFitAllocWorker : CutRegionWorker { return false; } + // Functional fingerprint of a candidate dsel region: drive (en,bc,cat) with + // each generated test vector, ConstEval the root, and compare every lane's + // evaluated rank against the closed-form first-fit reference + // (compute_alloc_dir). Returns true iff all lanes match on every vector, + // i.e. the region implements the first-fit dsel for the given direction. + // Any eval failure or single mismatch rejects the candidate. bool fingerprint_dsel(ConstEval &ce, const SigSpec &root, int n, int field_w, const SigSpec &en_sig, const SigSpec &bc_sig, bool has_bc, const SigSpec &cat_sig, int c, bool msb_first, int64_t cone_est) @@ -1210,7 +1227,6 @@ struct OptFirstFitAllocWorker : CutRegionWorker { } // Emit the shared scan once. - cell = rg.anchor; int cnt_w = clog2_int(rg.n + 1); vector leader; vector slot;