3
0
Fork 0
mirror of https://github.com/YosysHQ/yosys synced 2026-07-15 11:45:41 +00:00

Merge pull request #202 from Silimate/akashlevy/qor-pattern-passes

opt: recognize three QoR logic-depth patterns
This commit is contained in:
Akash Levy 2026-07-06 13:56:29 -07:00 committed by GitHub
commit 087f5bd254
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1384 additions and 14 deletions

View file

@ -108,6 +108,10 @@ yosys_pass(opt_first_fit_alloc
opt_first_fit_alloc.cc
)
yosys_pass(opt_priokey
opt_priokey.cc
)
pmgen_command(peepopt
peepopt_shiftmul_right.pmg
peepopt_shiftmul_left.pmg

View file

@ -165,6 +165,56 @@ struct OptFirstFitAllocWorker : CutRegionWorker {
return r;
}
// ----------------------------------------------------------------
// Reference semantics of the "coalesce matrix" allocator variant.
//
// Leadership and slot assignment are identical to the greedy first-fit
// above, but the per-lane rank does NOT depend on the lane's own enable:
// every lane k (enabled or not) inherits the slot of the first leader at
// or before k (in priority order) whose category matches cat[k]. This
// models RTL that precomputes a per-leader "same_cat[i][k]" mask (gated
// only on the leader's enable) and forward-coalesces into lane k without
// re-checking en[k]. There is no broadcast lane in this variant.
// ----------------------------------------------------------------
AllocResult compute_alloc_coalesce(const vector<int> &en, const vector<int> &cat,
int n) const
{
AllocResult r = compute_alloc(en, vector<int>(n, 0), cat, n);
for (int k = 0; k < n; k++) {
r.dsel[k] = 0;
for (int i = 0; i <= k; i++)
if (r.leader[i] && cat[i] == cat[k]) {
r.dsel[k] = r.slot[i];
break;
}
}
return r;
}
AllocResult compute_alloc_coalesce_dir(const vector<int> &en, const vector<int> &cat,
int n, bool msb_first) const
{
if (!msb_first)
return compute_alloc_coalesce(en, cat, n);
vector<int> er(n), cr(n);
for (int i = 0; i < n; i++) {
er[i] = en[n - 1 - i];
cr[i] = cat[n - 1 - i];
}
AllocResult rr = compute_alloc_coalesce(er, 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
@ -412,6 +462,10 @@ struct OptFirstFitAllocWorker : CutRegionWorker {
int field_w = 0;
SigSpec en_sig, bc_sig, cat_sig;
bool has_bc = false;
// Enable-independent forward coalescing: lanes inherit the slot of the
// first same-category leader at or before them in priority order,
// regardless of their own enable (the "same_cat matrix" RTL shape).
bool coalesce = false;
int c = 0;
bool msb_first = false;
Cell *anchor = nullptr;
@ -594,9 +648,39 @@ struct OptFirstFitAllocWorker : CutRegionWorker {
{
Cell *anchor = rg.anchor;
int n = rg.n, c = rg.c, w = rg.field_w;
SigSpec cat = sigmap(rg.cat_sig);
// Enable-independent forward coalescing: lane k inherits the slot of the
// unique same-category leader at or before k in priority order, with no
// enable/broadcast gating. The priority position of a lane is a compile-
// time constant, so the "leader at or before k" restriction is static.
if (rg.coalesce) {
auto pos = [&](int l) { return rg.msb_first ? (n - 1 - l) : l; };
SigSpec out;
for (int k = 0; k < n; k++) {
SigSpec cat_k = cat.extract(k * c, c);
vector<SigBit> g(n, SigBit(State::S0));
for (int i = 0; i < n; i++) {
if (pos(i) > pos(k))
continue;
SigBit eq = emit_eq_sig(anchor, cat.extract(i * c, c), cat_k);
g[i] = emit_and(anchor, leader[i], eq);
}
SigSpec rank(Const(0, cnt_w));
for (int b = 0; b < cnt_w; b++) {
SigSpec terms;
for (int i = 0; i < n; i++)
if (pos(i) <= pos(k))
terms.append(emit_and(anchor, g[i], slot[i][b]));
rank[b] = emit_reduce_or(anchor, terms);
}
out.append(zext_sig(rank, w));
}
return out;
}
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);
@ -707,15 +791,22 @@ struct OptFirstFitAllocWorker : CutRegionWorker {
internal_bits.insert(bit);
pool<SigSpec> seen;
auto all_internal = [&](const SigSpec &s) {
// Accept a bus bit if it is a cone-internal (computed) signal or a
// primary input / undriven bit. The enable/broadcast lanes are usually
// computed signals (e.g. valid & format), but some RTL drives the scan
// straight from a top-level request port (e.g. lane_en), so input buses
// must be admissible too. Inputs sort shallowest (depth 0) below, so they
// survive the candidate cap ahead of the deep intermediate nets.
auto all_internal_or_input = [&](const SigSpec &s) {
for (auto bit : s)
if (!bit.wire || !internal_bits.count(bit))
if (!bit.wire || (!internal_bits.count(bit) &&
bit_to_driver.at(bit, nullptr) != nullptr))
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))
if (GetSize(s) != n || !sig_bus_ok(s) || !all_internal_or_input(s))
return;
if (!seen.insert(s).second)
return;
@ -868,17 +959,28 @@ struct OptFirstFitAllocWorker : CutRegionWorker {
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(),
// Standard first-fit failed: try the enable-independent
// forward-coalescing variant (no broadcast lane).
bool coalesce = false;
if (!fpm && bc_bus == nullptr) {
fpm = fingerprint_dsel(ce, root_sig, n, field_w, en_bus.sig,
SigSpec(), false, cat_sig, c, msb_first,
cone_est, /*coalesce=*/true);
coalesce = fpm;
}
log_debug(" en=%s bc=%s cat=%dx%d %s: fingerprint %s%s\n", en_bus.name.c_str(),
bc_bus ? bc_bus->name.c_str() : "-", n, c,
msb_first ? "MSB" : "LSB", fpm ? "MATCH" : "no");
msb_first ? "MSB" : "LSB", fpm ? "MATCH" : "no",
coalesce ? " (coalesce)" : "");
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.bc_sig = (!coalesce && bc_bus) ? bc_bus->sig : SigSpec();
out.has_bc = (!coalesce && bc_bus != nullptr);
out.coalesce = coalesce;
out.cat_sig = cat_sig;
out.c = c;
out.msb_first = msb_first;
@ -901,7 +1003,8 @@ struct OptFirstFitAllocWorker : CutRegionWorker {
// 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)
const SigSpec &cat_sig, int c, bool msb_first, int64_t cone_est,
bool coalesce = false)
{
int nval = 1 << c;
vector<TestVector> vs = make_vectors(n, nval, has_bc);
@ -920,7 +1023,9 @@ struct OptFirstFitAllocWorker : CutRegionWorker {
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);
AllocResult ar = coalesce
? compute_alloc_coalesce_dir(tv.en, tv.label, n, msb_first)
: 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);
@ -1239,11 +1344,12 @@ struct OptFirstFitAllocWorker : CutRegionWorker {
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(" %s: %s <- first_fit_alloc(en=%s%s, cat=%dx%d, %s%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");
rg.n, rg.c, rg.msb_first ? "MSB-first" : "LSB-first",
rg.coalesce ? ", coalesce" : "");
if (have_xbar) {
SigSpec new_xbar = emit_xbar(rg, xb, leader, slot, cnt_w);

View file

@ -82,6 +82,7 @@ struct OptPriEncWorker {
// Configuration.
bool detect_clz = true;
bool detect_ctz = true;
bool detect_rr = true;
int max_input_width = 256;
int min_input_width = 4;
@ -390,6 +391,171 @@ struct OptPriEncWorker {
return full;
}
// ------------------------------------------------------------------
// Round-robin (rotated priority) detection + rewrite.
//
// A round-robin arbiter grants the first set request bit scanning
// *upward* (increasing index, wrapping) starting just after a stored
// pointer `s` (= idx_last):
//
// grant = anyreq ? (first set bit at index > s, else first set
// bit overall) : 0
// idx_next = anyreq ? grant : s
//
// RTL usually spells this as a DEPTH-iteration loop that walks `idx`
// downward from idx_last with wraparound and keeps the last hit, which
// elaborates into a serial mux/shift chain of depth ~DEPTH. The rewrite
// below is log-depth:
//
// above[i] = (i > s) (per-bit threshold mask)
// mask_hi = req & above
// grant = anyreq ? (|mask_hi ? ctz(mask_hi) : ctz(req)) : 0
//
// where ctz() reuses the log-depth CTZ network. For power-of-2 DEPTH the
// rewrite is fully combinationally equivalent for every pointer value;
// for non-power-of-2 DEPTH it is equivalent for every *reachable* pointer
// (idx_last only ever holds a valid index in [0,DEPTH)), which is the
// range the fingerprint checks. Detection therefore sweeps s over
// [0,DEPTH) only.
// ------------------------------------------------------------------
// kind: 0 = grant, 1 = idx_next.
int rr_expected(const Const& reqv, int s, int N, int W, int kind) {
auto bits = reqv.to_bits();
int lo_all = -1, lo_hi = -1;
for (int i = 0; i < N; i++) {
bool set = (i < (int)bits.size() && bits[i] == State::S1);
if (!set) continue;
if (lo_all < 0) lo_all = i;
if (i > s && lo_hi < 0) lo_hi = i;
}
bool anyreq = (lo_all >= 0);
int gsel = (lo_hi >= 0) ? lo_hi : (lo_all >= 0 ? lo_all : 0);
int val = kind == 0 ? (anyreq ? gsel : 0)
: (anyreq ? gsel : s);
return val & ((W >= 31) ? -1 : ((1 << W) - 1));
}
// Returns matched kind (0 grant, 1 idx_next), or -1 for no match.
int fingerprint_rr(SigSpec req_sig, SigSpec start_sig, SigSpec S_sig,
int N, int W) {
ConstEval ce(module);
bool ok0 = true, ok1 = true;
auto deck = gen_test_vectors(N);
int checks = 0;
for (auto& rv : deck) {
for (int s = 0; s < N; s++) {
ce.push();
ce.set(req_sig, rv);
ce.set(start_sig, Const(s, W));
SigSpec out = S_sig, undef;
bool ok = ce.eval(out, undef);
ce.pop();
if (!ok || !out.is_fully_const()) return -1;
int ov = out.as_const().as_int();
if (ok0 && ov != rr_expected(rv, s, N, W, 0)) ok0 = false;
if (ok1 && ov != rr_expected(rv, s, N, W, 1)) ok1 = false;
checks++;
if (!ok0 && !ok1) return -1;
}
}
if (checks < 2 * N) return -1;
if (ok0) return 0;
if (ok1) return 1;
return -1;
}
// Emit the log-depth round-robin network. Shared subexpressions across
// the grant / idx_next pair for the same (req, start) inputs are cached.
dict<std::pair<Wire*, Wire*>, std::tuple<SigSpec, SigSpec, SigBit>> rr_core_cache;
SigSpec emit_rr(Wire* req_wire, Wire* start_wire, int N, int W, int kind) {
SigSpec req = sigmap(SigSpec(req_wire));
SigSpec s = sigmap(SigSpec(start_wire));
SigSpec gsel;
SigBit anyreq;
auto key = std::make_pair(req_wire, start_wire);
auto it = rr_core_cache.find(key);
if (it != rr_core_cache.end()) {
SigSpec cached_gsel;
std::tie(cached_gsel, std::ignore, anyreq) = it->second;
gsel = cached_gsel;
} else {
SigSpec above;
for (int i = 0; i < N; i++) {
above.append(module->Lt(NEW_ID2_SUFFIX("rrabove"), s, SigSpec(Const(i, W))));
cells_added++;
}
SigSpec mask_hi = module->And(NEW_ID2_SUFFIX("rrmask"), req, above);
cells_added++;
SigSpec cz_hi = emit_ctz_full(mask_hi, N);
SigSpec cz_all = emit_ctz_full(req, N);
auto low_w = [&](SigSpec x) {
if (GetSize(x) > W) return x.extract(0, W);
while (GetSize(x) < W) x.append(SigSpec(State::S0));
return x;
};
cz_hi = low_w(cz_hi);
cz_all = low_w(cz_all);
SigBit any_hi = module->ReduceOr(NEW_ID2_SUFFIX("rranyhi"), mask_hi);
cells_added++;
anyreq = module->ReduceOr(NEW_ID2_SUFFIX("rranyreq"), req);
cells_added++;
// any_hi ? cz_hi : cz_all
gsel = module->Mux(NEW_ID2_SUFFIX("rrgsel"), cz_all, cz_hi, any_hi);
cells_added++;
rr_core_cache[key] = std::make_tuple(gsel, SigSpec(), anyreq);
}
SigSpec fallback = (kind == 0) ? SigSpec(Const(0, W)) : s;
// anyreq ? gsel : fallback
SigSpec res = module->Mux(NEW_ID2_SUFFIX("rrsel"), fallback, gsel, anyreq);
cells_added++;
return res;
}
// Generalisation of cone_depends_only_on_T to a set of allowed leaf bits.
bool cone_depends_only_on_set(SigSpec S_sig, const pool<SigBit>& allowed) {
pool<SigBit> visited;
std::queue<SigBit> worklist;
for (auto bit : sigmap(S_sig)) {
if (!bit.wire) continue;
if (visited.insert(bit).second) worklist.push(bit);
}
while (!worklist.empty()) {
SigBit bit = worklist.front();
worklist.pop();
if (allowed.count(bit)) continue;
if (input_port_bits.count(bit)) return false;
auto it = bit_to_driver.find(bit);
if (it == bit_to_driver.end()) return false;
Cell* drv = it->second;
if (sequential_cells.count(drv)) return false;
for (auto& conn : drv->connections()) {
if (!drv->input(conn.first)) continue;
for (auto in_bit : sigmap(conn.second)) {
if (!in_bit.wire) continue;
if (visited.insert(in_bit).second) worklist.push(in_bit);
}
}
}
return true;
}
struct RRRewrite {
Wire* S_wire;
Wire* req_wire;
Wire* start_wire;
int N;
int W;
int kind;
Cell* sole_driver;
IdString out_port;
};
struct Rewrite {
Wire* S_wire;
Wire* T_wire;
@ -584,6 +750,77 @@ struct OptPriEncWorker {
}
}
// Stage 3: round-robin (rotated priority) detection. Reuses the same
// candidate cones; an output S is grant/idx_next of a round-robin
// arbiter over a wide request bus `req` and a same-width-as-S pointer
// `start`, both bottoming out the cone.
vector<RRRewrite> rr_rewrites;
if (detect_rr) {
const int max_pairs = 64;
for (auto& cand : candidates) {
if (claimed_outputs.count(cand.S_wire)) continue;
if (claimed_drivers.count(cand.sole_driver)) continue;
int W = cand.S_wire->width;
if (W < 2 || W > max_W) continue;
SigSpec S_sig = sigmap(SigSpec(cand.S_wire));
vector<Wire*> req_cands, start_cands;
for (Wire* w : wires_snapshot) {
if (w == cand.S_wire) continue;
bool all_in = true;
for (auto bit : sigmap(SigSpec(w)))
if (!cand.cone_bits.count(bit)) { all_in = false; break; }
if (!all_in) continue;
int wn = w->width;
if (wn >= min_input_width && wn <= max_input_width &&
clog2_int(wn) == W)
req_cands.push_back(w);
if (wn == W)
start_cands.push_back(w);
}
std::sort(req_cands.begin(), req_cands.end(),
[](Wire* a, Wire* b) { return a->width > b->width; });
bool matched = false;
for (Wire* req_wire : req_cands) {
if (matched) break;
int N = req_wire->width;
SigSpec req_sig = sigmap(SigSpec(req_wire));
pool<SigBit> req_bits;
for (auto bit : req_sig)
if (bit.wire) req_bits.insert(bit);
// Per-req_wire fingerprint budget: a start-candidate-heavy
// first req size must not exhaust a shared budget and starve
// later (narrower) req sizes.
int pairs = 0;
for (Wire* start_wire : start_cands) {
if (start_wire == req_wire) continue;
if (++pairs > max_pairs) break;
SigSpec start_sig = sigmap(SigSpec(start_wire));
pool<SigBit> allowed = req_bits;
for (auto bit : start_sig)
if (bit.wire) allowed.insert(bit);
if (!cone_depends_only_on_set(S_sig, allowed)) continue;
int kind = fingerprint_rr(req_sig, start_sig, S_sig, N, W);
if (kind < 0) continue;
log(" %s: %s <- round_robin_%s(req=%s, start=%s) [N=%d, W=%d]\n",
log_id(module), log_id(cand.S_wire),
kind == 0 ? "grant" : "next",
log_id(req_wire), log_id(start_wire), N, W);
rr_rewrites.push_back({cand.S_wire, req_wire, start_wire, N, W,
kind, cand.sole_driver, cand.out_port});
claimed_outputs.insert(cand.S_wire);
claimed_drivers.insert(cand.sole_driver);
matched = true;
break;
}
}
}
}
// Apply rewrites. We collected first to avoid the index growing stale
// while we add new cells/wires.
for (auto& r : rewrites) {
@ -595,6 +832,14 @@ struct OptPriEncWorker {
module->connect(SigSpec(r.S_wire), new_S);
regions_rewritten++;
}
for (auto& r : rr_rewrites) {
cell = r.sole_driver;
SigSpec new_S = emit_rr(r.req_wire, r.start_wire, r.N, r.W, r.kind);
Wire* dangling = module->addWire(NEW_ID2_SUFFIX("dangling"), r.W);
r.sole_driver->setPort(r.out_port, dangling);
module->connect(SigSpec(r.S_wire), new_S);
regions_rewritten++;
}
}
};
@ -624,11 +869,23 @@ struct OptPriEncPass : public Pass {
log(" ctz_full : symmetric to clz_full from the LSB side.\n");
log(" ctz_short : symmetric to clz_short from the LSB side.\n");
log("\n");
log("In addition, the pass detects round-robin (rotated priority)\n");
log("arbiters: grant / idx_next = first set request bit scanning upward\n");
log("(wrapping) from just after a stored pointer idx_last. RTL typically\n");
log("spells this as a DEPTH-iteration idx-- loop over req[idx], which\n");
log("elaborates into a serial chain; it is replaced with a log-depth\n");
log("threshold-mask + CTZ network. For power-of-2 DEPTH the rewrite is\n");
log("equivalent for every pointer value; for other widths it is\n");
log("equivalent for every reachable pointer (idx_last in [0,DEPTH)).\n");
log("\n");
log(" -clz\n");
log(" detect CLZ patterns only.\n");
log(" detect CLZ patterns only (also disables round-robin).\n");
log("\n");
log(" -ctz\n");
log(" detect CTZ patterns only.\n");
log(" detect CTZ patterns only (also disables round-robin).\n");
log("\n");
log(" -no-rr\n");
log(" disable round-robin / rotated-priority detection.\n");
log("\n");
log(" -max-width N\n");
log(" maximum input bus width to consider (default 64).\n");
@ -648,6 +905,7 @@ struct OptPriEncPass : public Pass {
bool only_clz = false;
bool only_ctz = false;
bool no_rr = false;
int max_width = 64;
int min_width = 4;
@ -655,6 +913,7 @@ struct OptPriEncPass : public Pass {
for (argidx = 1; argidx < args.size(); argidx++) {
if (args[argidx] == "-clz") { only_clz = true; continue; }
if (args[argidx] == "-ctz") { only_ctz = true; continue; }
if (args[argidx] == "-no-rr") { no_rr = true; continue; }
if (args[argidx] == "-max-width" && argidx + 1 < args.size()) {
max_width = std::stoi(args[++argidx]); continue;
}
@ -664,6 +923,9 @@ struct OptPriEncPass : public Pass {
break;
}
extra_args(args, argidx, design);
// -clz / -ctz select a single leading/trailing variant and disable
// round-robin detection unless the user re-enables it explicitly.
if (only_clz || only_ctz) no_rr = true;
int total_regions = 0;
int total_cells_added = 0;
@ -671,6 +933,7 @@ struct OptPriEncPass : public Pass {
OptPriEncWorker worker(module);
worker.detect_clz = !only_ctz;
worker.detect_ctz = !only_clz;
worker.detect_rr = !no_rr;
worker.max_input_width = max_width;
worker.min_input_width = min_width;
worker.run();

464
passes/opt/opt_priokey.cc Normal file
View file

@ -0,0 +1,464 @@
/*
* yosys -- Yosys Open SYnthesis Suite
*
* Copyright (C) 2026 Akash Levy <akash@silimate.com>
*
* 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"
USING_YOSYS_NAMESPACE
PRIVATE_NAMESPACE_BEGIN
// ---------------------------------------------------------------------------
// opt_priokey: priority-by-key deduplication ("taken" accumulator) rewrite.
//
// RTL that resolves conflicts between several sources that each carry a small
// key is often written as a serial scan over a wide one-hot "set" accumulator
// indexed by the key:
//
// taken = '0;
// for (i = 0; i < P; i++)
// if (act[i] && !taken[key[i]]) begin // this source wins its key
// taken[key[i]] = 1'b1; // claim the key
// ...use win[i]...
// end
//
// This elaborates into a serial chain of dynamic-index reads (taken[key[i]] =
// $shiftx into an S-bit vector) and dynamic-index writes (taken | 1<<key[i],
// muxed by the winner). Each dynamic access is an O(log S) wide mux, so the
// critical path grows with both P and S even though the underlying decision
// only needs pairwise key comparisons:
//
// taken[key[j]] (at step j) == OR over i<j of ( win_guard[i] & key[i]==key[j] )
//
// The pass identifies the accumulator chain, then replaces each dynamic read
// with the equivalent pairwise-key-compare reduction, eliminating the wide
// dynamic indexing entirely. Correctness of every rewrite is validated by a
// ConstEval fingerprint (the read output vs. the compare reduction, over the
// free key/guard signals) before it is applied. For non-power-of-two S the
// rewrite is equivalent over the reachable key range [0,S), which the
// fingerprint sweeps.
// ---------------------------------------------------------------------------
struct OptPrioKeyWorker {
Module *module;
SigMap sigmap;
Cell *cell = nullptr;
dict<SigBit, Cell *> bit_to_driver;
int max_slots = 1 << 14; // maximum accumulator width S
int max_chain = 256; // maximum number of sources P
int fp_trials = 256; // ConstEval validation vectors
bool strict = false; // validate over the full key range, not [0,S)
int regions_rewritten = 0;
int cells_added = 0;
OptPrioKeyWorker(Module *m) : module(m), sigmap(m) { build_index(); }
void build_index() {
for (auto c : module->cells())
for (auto &conn : c->connections())
if (c->output(conn.first))
for (auto bit : sigmap(conn.second))
if (bit.wire) bit_to_driver[bit] = c;
}
Cell *sole_driver(const SigSpec &s) {
SigSpec ss = sigmap(s);
Cell *d = nullptr;
for (auto bit : ss) {
if (!bit.wire) return nullptr;
auto it = bit_to_driver.find(bit);
if (it == bit_to_driver.end()) return nullptr;
if (d && d != it->second) return nullptr;
d = it->second;
}
return d;
}
bool is_all_zero(const SigSpec &s) {
for (auto bit : s)
if (bit != SigBit(State::S0)) return false;
return true;
}
bool is_const_one(const SigSpec &s) {
SigSpec ss = sigmap(s);
if (!ss.is_fully_const()) return false;
return ss.as_const().as_int() == 1;
}
// Recover the index bus `key` from a one-hot set-mask (1 << key), spelled
// as $shl(1, key) or $shift(1, -key).
SigSpec decode_onehot_key(const SigSpec &mask) {
Cell *d = sole_driver(mask);
if (!d) return SigSpec();
if ((d->type == ID($shl) || d->type == ID($sshl)) &&
is_const_one(d->getPort(ID::A)))
return sigmap(d->getPort(ID::B));
if (d->type == ID($shift) && is_const_one(d->getPort(ID::A))) {
Cell *neg = sole_driver(d->getPort(ID::B));
if (neg && neg->type == ID($neg))
return sigmap(neg->getPort(ID::A));
}
return SigSpec();
}
// A set-arm is `taken_prev` with one bit (1 << key) OR'ed in. After opt it
// appears either as the raw one-hot (prev was 0) or as an $or whose two
// operands are the one-hot mask and the (masked) previous state. Return the
// key bus of the one-hot mask.
SigSpec extract_key_from_setarm(const SigSpec &arm) {
SigSpec k = decode_onehot_key(arm);
if (GetSize(k)) return k;
Cell *d = sole_driver(arm);
if (d && d->type == ID($or)) {
k = decode_onehot_key(d->getPort(ID::A));
if (GetSize(k)) return k;
k = decode_onehot_key(d->getPort(ID::B));
if (GetSize(k)) return k;
}
return SigSpec();
}
// One guarded "set key" applied to the accumulator (guard may be const-1
// for an unconditional set).
struct SetStep {
SigBit guard;
SigSpec key;
};
// Cache of trace results: chained reads share accumulator prefixes (read j
// indexes the state produced by sets 0..j-1), so without memoization the
// same chain is re-walked once per read -> O(P^2 * S). The cache makes the
// total walk O(P * S).
dict<SigSpec, std::pair<bool, vector<SetStep>>> acc_memo;
// Walk an accumulator value back to constant zero, collecting the guarded
// key-sets that produced it. Returns false (leaving `steps` unchanged) if it
// is not a pure set-only accumulator (mux/or of prev with a one-hot key)
// rooted at 0.
bool trace_acc(SigSpec acc, vector<SetStep> &steps, int depth) {
acc = sigmap(acc);
auto mit = acc_memo.find(acc);
if (mit != acc_memo.end()) {
if (mit->second.first)
steps.insert(steps.end(), mit->second.second.begin(),
mit->second.second.end());
return mit->second.first;
}
int start = GetSize(steps);
bool ok = trace_acc_uncached(acc, steps, depth);
if (ok) {
acc_memo[acc] = {true, vector<SetStep>(steps.begin() + start,
steps.end())};
} else {
steps.resize(start); // discard any partial trace
acc_memo[acc] = {false, {}};
}
return ok;
}
bool trace_acc_uncached(SigSpec acc, vector<SetStep> &steps, int depth) {
if (is_all_zero(acc))
return true;
if (depth > max_chain)
return false;
Cell *d = sole_driver(acc);
if (!d)
return false;
if (d->type == ID($mux)) {
SigSpec s = sigmap(d->getPort(ID::S));
if (GetSize(s) != 1)
return false;
if (!trace_acc(d->getPort(ID::A), steps, depth + 1))
return false;
SigSpec key = extract_key_from_setarm(d->getPort(ID::B));
if (GetSize(key) == 0)
return false;
steps.push_back(SetStep{s[0], key});
return true;
}
if (d->type == ID($or)) {
SigSpec a = d->getPort(ID::A), b = d->getPort(ID::B);
SigSpec ka = decode_onehot_key(a), kb = decode_onehot_key(b);
if (GetSize(ka) && trace_acc(b, steps, depth + 1)) {
steps.push_back(SetStep{SigBit(State::S1), ka});
return true;
}
if (GetSize(kb) && trace_acc(a, steps, depth + 1)) {
steps.push_back(SetStep{SigBit(State::S1), kb});
return true;
}
}
return false;
}
// A dynamic read of the accumulator: the 1-bit read cell, the accumulator
// value it indexes (with any out-of-range x-padding stripped) and the index.
struct Read {
Cell *cell;
SigSpec acc;
SigSpec key;
};
// Recognize a 1-bit dynamic read of a vector: either $shiftx(A=acc, B=key)
// or $bmux(A={x.., acc}, S=key). Returns false otherwise.
bool match_read(Cell *c, Read &r) {
if (c->type == ID($shiftx)) {
if (GetSize(c->getPort(ID::Y)) != 1)
return false;
r.cell = c;
r.acc = sigmap(c->getPort(ID::A));
r.key = sigmap(c->getPort(ID::B));
return GetSize(r.acc) >= 2;
}
if (c->type == ID($bmux)) {
if (c->getParam(ID::WIDTH).as_int() != 1)
return false;
SigSpec a = sigmap(c->getPort(ID::A));
// Strip the high x-padding to recover the real accumulator bits.
int w = 0;
while (w < GetSize(a) && a[w] != SigBit(State::Sx))
w++;
if (w < 2)
return false;
r.cell = c;
r.acc = a.extract(0, w);
r.key = sigmap(c->getPort(ID::S));
return true;
}
return false;
}
// Prove read == OR over steps of ( guard & key == read_key ) by ConstEval
// fingerprinting over the reachable key range [0,S). Guards and key buses
// (which are disjoint sel slices) are driven as free inputs.
bool validate_read(const Read &rd, const vector<SetStep> &steps, int S) {
Cell *read = rd.cell;
SigSpec read_key = rd.key;
// In strict mode sweep the full key range so the rewrite is proven for
// every value the index bus can take (out-of-range reads included);
// otherwise sweep only the reachable slots [0,S).
int kw = GetSize(read_key);
uint64_t range = (uint64_t)S;
if (strict) {
int cap = kw < 30 ? kw : 30;
range = 1ULL << cap;
}
ConstEval ce(module);
uint64_t lfsr = 0x9e3779b97f4a7c15ULL ^ (uintptr_t)read;
auto rnd = [&]() {
lfsr ^= lfsr << 13; lfsr ^= lfsr >> 7; lfsr ^= lfsr << 17;
return lfsr;
};
for (int t = 0; t < fp_trials; t++) {
ce.push();
int rk = (int)(rnd() % range);
ce.set(read_key, Const(rk, GetSize(read_key)));
vector<int> kv(GetSize(steps));
vector<int> gv(GetSize(steps));
for (int i = 0; i < GetSize(steps); i++) {
kv[i] = (int)(rnd() % range);
ce.set(steps[i].key, Const(kv[i], GetSize(steps[i].key)));
if (steps[i].guard == State::S1) {
gv[i] = 1;
} else {
gv[i] = (int)(rnd() & 1);
ce.set(SigSpec(steps[i].guard), Const(gv[i], 1));
}
}
SigSpec out(read->getPort(ID::Y));
SigSpec undef;
bool ok = ce.eval(out, undef) && out.is_fully_const();
int actual = ok ? (out.as_const().as_int() & 1) : -1;
int expect = 0;
for (int i = 0; i < GetSize(steps); i++)
if (gv[i] && kv[i] == rk) { expect = 1; break; }
ce.pop();
if (!ok || actual != expect)
return false;
}
return true;
}
void rewrite_read(const Read &rd, const vector<SetStep> &steps) {
Cell *read = rd.cell;
cell = read;
SigSpec read_key = rd.key;
SigSpec new_r;
if (steps.empty()) {
new_r = SigSpec(State::S0);
} else {
SigSpec terms;
for (auto &st : steps) {
SigSpec eq = module->Eq(NEW_ID2_SUFFIX("priokey_eq"),
st.key, read_key);
cells_added++;
SigSpec g = module->And(NEW_ID2_SUFFIX("priokey_and"),
SigSpec(st.guard), eq);
cells_added++;
terms.append(g);
}
new_r = module->ReduceOr(NEW_ID2_SUFFIX("priokey_or"), terms);
cells_added++;
}
// Tag wire so the rewrite is externally observable, then detach the old
// dynamic read and drive its consumers from the reduction.
Wire *tag = module->addWire(NEW_ID2_SUFFIX("priokey_read"), 1);
module->connect(SigSpec(tag), new_r);
SigSpec old_y = sigmap(read->getPort(ID::Y));
Wire *dangling = module->addWire(NEW_ID2_SUFFIX("priokey_dangling"),
GetSize(read->getPort(ID::Y)));
read->setPort(ID::Y, dangling);
module->connect(old_y, SigSpec(tag));
regions_rewritten++;
}
void run() {
vector<Read> reads;
for (auto c : module->cells()) {
Read r;
if (!match_read(c, r))
continue;
if (GetSize(r.acc) > max_slots)
continue;
reads.push_back(r);
}
int max_sources = 0;
int accum_width = 0;
vector<Read> zero_reads; // read of the all-zero head (== 0)
for (auto &rd : reads) {
int S = GetSize(rd.acc);
vector<SetStep> steps;
if (!trace_acc(rd.acc, steps, 0))
continue;
// The all-zero head read is only rewritten (to 0) once we know the
// pattern is really present in this module.
if (steps.empty()) {
zero_reads.push_back(rd);
continue;
}
if (!validate_read(rd, steps, S))
continue;
rewrite_read(rd, steps);
max_sources = std::max(max_sources, GetSize(steps) + 1);
accum_width = S;
}
if (regions_rewritten)
for (auto &rd : zero_reads)
rewrite_read(rd, {});
if (regions_rewritten)
log(" %s: priority-by-key dedup, up to %d source(s), "
"%d-slot accumulator\n",
log_id(module), max_sources, accum_width);
}
};
struct OptPrioKeyPass : public Pass {
OptPrioKeyPass() : Pass("opt_priokey",
"detect and rewrite priority-by-key deduplication accumulators") {}
void help() override {
// |---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|
log("\n");
log(" opt_priokey [options] [selection]\n");
log("\n");
log("This pass detects a serial 'set accumulator' that resolves conflicts\n");
log("between several sources that each carry a small key:\n");
log("\n");
log(" taken = '0;\n");
log(" for (i = 0; i < P; i++)\n");
log(" if (act[i] && !taken[key[i]]) begin taken[key[i]] = 1; ... end\n");
log("\n");
log("Such RTL elaborates into a chain of dynamic-index reads/writes into a\n");
log("wide one-hot vector ($shiftx / $shift), whose depth grows with both the\n");
log("number of sources and the accumulator width. Each dynamic read\n");
log("taken[key[j]] is provably equal to\n");
log("\n");
log(" OR over i<j of ( set_guard[i] & key[i] == key[j] )\n");
log("\n");
log("so the pass replaces every read with that pairwise-key-compare\n");
log("reduction, removing the wide dynamic indexing. Each rewrite is\n");
log("validated by a ConstEval fingerprint before being applied; for\n");
log("non-power-of-two accumulator widths the rewrite holds over the\n");
log("reachable key range [0,S).\n");
log("\n");
log(" -max-slots N\n");
log(" maximum accumulator width to consider (default 16384).\n");
log("\n");
log(" -max-sources N\n");
log(" maximum number of chained sources to consider (default 256).\n");
log("\n");
log(" -strict\n");
log(" validate every rewrite over the full index range instead of\n");
log(" only the reachable slots [0,S). Rewrites that hold merely by\n");
log(" out-of-range don't-care freedom are then rejected (use this\n");
log(" under equiv_opt -assert / formal flows).\n");
log("\n");
log("This pass is not invoked by the default 'opt' script; users opt in.\n");
log("After rewriting, the dead accumulator chain is removed by the trailing\n");
log("'clean -purge'.\n");
log("\n");
}
void execute(std::vector<std::string> args, RTLIL::Design *design) override {
log_header(design, "Executing OPT_PRIOKEY pass (priority-by-key dedup).\n");
int max_slots = 1 << 14;
int max_sources = 256;
bool strict = false;
size_t argidx;
for (argidx = 1; argidx < args.size(); argidx++) {
if (args[argidx] == "-max-slots" && argidx + 1 < args.size()) {
max_slots = std::stoi(args[++argidx]); continue;
}
if (args[argidx] == "-max-sources" && argidx + 1 < args.size()) {
max_sources = std::stoi(args[++argidx]); continue;
}
if (args[argidx] == "-strict") {
strict = true; continue;
}
break;
}
extra_args(args, argidx, design);
int total_regions = 0, total_cells = 0;
for (auto module : design->selected_modules()) {
OptPrioKeyWorker worker(module);
worker.max_slots = max_slots;
worker.max_chain = max_sources;
worker.strict = strict;
worker.run();
total_regions += worker.regions_rewritten;
total_cells += worker.cells_added;
}
log("Rewrote %d dynamic key-read(s); emitted %d new cell(s).\n",
total_regions, total_cells);
if (total_regions)
Yosys::run_pass("clean -purge");
}
} OptPrioKeyPass;
PRIVATE_NAMESPACE_END

View file

@ -590,3 +590,170 @@ design -load postopt
select -assert-min 1 w:*ffa_*
design -reset
log -pop
# ============================================================================
# Group G: coalesce-matrix variant (precomputed same_cat[i][k], raw-input en)
# ============================================================================
#
# Some RTL precomputes a per-leader "same_cat[i][k]" mask (gated ONLY on the
# leader's enable) and forward-coalesces the leader's slot into lane k without
# re-checking en[k]. Disabled lanes after a same-category leader therefore
# inherit that leader's slot (rather than 0). The pass detects this as the
# enable-independent forward-coalescing variant. These modules also drive the
# scan straight from a top-level request port (lane_en), exercising the
# primary-input enable/broadcast candidate path.
# G1: coalesce-matrix dsel-only, raw-input enable, N=8 (equiv).
log -header "G1: coalesce-matrix allocator, raw-input enable, N=8 (equiv)"
log -push
design -reset
read_verilog -sv <<EOF
module top #(parameter N=8, NB=4, C=2, W=2) (
input logic [N-1:0] lane_en,
input logic [N*C-1:0] cat_flat,
output logic [N*W-1:0] dsel_flat
);
logic [N-1:0] same_cat [0:N-1];
always_comb begin
for (int a=0;a<N;a++) begin
same_cat[a] = '0;
if (lane_en[a])
for (int b=a;b<N;b++)
if (cat_flat[a*C +: C]==cat_flat[b*C +: C]) same_cat[a][b] = 1'b1;
end
end
logic [W-1:0] dsel [0:N-1];
logic [NB-1:0] taken;
logic [N-1:0] done;
always_comb begin
for (int i=0;i<N;i++) dsel[i] = '0;
taken = '0; done = '0;
for (int i=0;i<N;i++)
if (lane_en[i])
for (int j=0;j<NB;j++)
if (!taken[j] && !done[i]) begin
dsel[i] = W'(j); done[i] = 1'b1; taken[j] = 1'b1;
for (int k=0;k<N;k++)
if (same_cat[i][k]) begin dsel[k] = W'(j); done[k] = 1'b1; end
end
end
for (genvar g=0;g<N;g++) assign dsel_flat[g*W +: W] = dsel[g];
endmodule
EOF
hierarchy -top top
proc
opt
check -assert
equiv_opt -assert opt_first_fit_alloc
design -load postopt
# The coalesce variant fired (log shows "coalesce").
select -assert-min 1 w:*ffa_*
design -reset
log -pop
# G2: coalesce-matrix dsel + xbar, N=16 -- structural. Both deep cones must
# collapse from the shared scan (dsel via the coalesce gather, xbar via the
# per-slot field gather); full equiv at N=16 is SAT-hard (see group C).
log -header "G2: coalesce-matrix dsel + xbar, N=16 structural"
log -push
design -reset
read_verilog -sv <<EOF
module top #(parameter N=16, NB=8, C=3, W=3, A=5) (
input logic [N-1:0] lane_en,
input logic [N*C-1:0] cat_flat,
input logic [N-1:0] swap_bit,
output logic [N*W-1:0] dsel_flat,
output logic [32*A-1:0] xbar_flat
);
logic [N-1:0] same_cat [0:N-1];
always_comb begin
for (int a=0;a<N;a++) begin
same_cat[a] = '0;
if (lane_en[a])
for (int b=a;b<N;b++)
if (cat_flat[a*C +: C]==cat_flat[b*C +: C]) same_cat[a][b] = 1'b1;
end
end
logic [NB-1:0] taken;
logic [N-1:0] done;
logic [W-1:0] dsel [0:N-1];
logic [A-1:0] xbar [0:31];
always_comb begin
for (int i=0;i<N;i++) dsel[i] = '0;
for (int i=0;i<32;i++) xbar[i] = '0;
taken = '0; done = '0;
for (int i=0;i<N;i++)
if (lane_en[i])
for (int j=0;j<NB;j++)
if (!taken[j] && !done[i]) begin
dsel[i] = W'(j); done[i] = 1'b1; taken[j] = 1'b1;
for (int l=0;l<4;l++)
xbar[(j*4)+l] = (A'(({2'b0,cat_flat[i*C +: C]}*4)+l)) ^ {3'b0, swap_bit[i], 1'b0};
for (int k=0;k<N;k++)
if (same_cat[i][k]) begin dsel[k] = W'(j); done[k] = 1'b1; end
end
end
for (genvar g=0;g<N;g++) assign dsel_flat[g*W +: W] = dsel[g];
for (genvar g=0;g<32;g++) assign xbar_flat[g*A +: A] = xbar[g];
endmodule
EOF
hierarchy -top top
proc
memory -nomap -norom -nordff
opt
select -assert-min 1000 t:$mux
opt_first_fit_alloc
opt_clean
select -assert-min 1 w:*ffa_*
# The deep mux chains collapse and the xbar emits $bmux table-lookups.
select -assert-max 200 t:$mux
select -assert-min 1 t:$bmux
design -reset
log -pop
# G3: coalesce-matrix but LAST-fit slot choice (scans free slots NB-1..0). The
# same_cat coalescing is present but the slot assignment is not first-fit, so
# neither the standard nor the coalesce fingerprint may match.
log -header "G3: coalesce-matrix last-fit near-miss -> no rewrite"
log -push
design -reset
read_verilog -sv <<EOF
module top #(parameter N=8, NB=4, C=2, W=2) (
input logic [N-1:0] lane_en,
input logic [N*C-1:0] cat_flat,
output logic [N*W-1:0] dsel_flat
);
logic [N-1:0] same_cat [0:N-1];
always_comb begin
for (int a=0;a<N;a++) begin
same_cat[a] = '0;
if (lane_en[a])
for (int b=a;b<N;b++)
if (cat_flat[a*C +: C]==cat_flat[b*C +: C]) same_cat[a][b] = 1'b1;
end
end
logic [W-1:0] dsel [0:N-1];
logic [NB-1:0] taken;
logic [N-1:0] done;
always_comb begin
for (int i=0;i<N;i++) dsel[i] = '0;
taken = '0; done = '0;
for (int i=0;i<N;i++)
if (lane_en[i])
for (int j=NB-1;j>=0;j--)
if (!taken[j] && !done[i]) begin
dsel[i] = W'(j); done[i] = 1'b1; taken[j] = 1'b1;
for (int k=0;k<N;k++)
if (same_cat[i][k]) begin dsel[k] = W'(j); done[k] = 1'b1; end
end
end
for (genvar g=0;g<N;g++) assign dsel_flat[g*W +: W] = dsel[g];
endmodule
EOF
hierarchy -top top
proc
opt
opt_first_fit_alloc
select -assert-count 0 w:*ffa_*
design -reset
log -pop

View file

@ -672,3 +672,129 @@ equiv_opt -assert opt_prienc
design -load postopt
design -reset
log -pop
# ============================================================================
# Group RR: round-robin (rotated priority) arbiters
# ============================================================================
#
# grant / idx_next = first set request bit scanning upward (wrapping) from
# just after a stored pointer idx_last. RTL spells this as a DEPTH-iteration
# idx-- loop over req[idx] that elaborates into a serial mux/shift chain; the
# pass replaces it with a log-depth threshold-mask + CTZ network.
# RR1: power-of-2 DEPTH -- full sequential equivalence. Both grant and
# idx_next collapse and every serial req[idx] $shiftx is removed.
log -header "RR1: round-robin arbiter, DEPTH=16 (sequential equiv)"
log -push
design -reset
read_verilog -sv <<EOF
module test #(parameter int DEPTH = 16) (
input logic clk,
input logic rst_n,
input logic [DEPTH-1:0] req,
output logic [$clog2(DEPTH)-1:0] grant
);
typedef logic [$clog2(DEPTH)-1:0] idx_t;
idx_t idx, idx_next, idx_last;
always_comb begin
idx = idx_last; idx_next = idx_last; grant = '0;
for (int i = 0; i < DEPTH; i++) begin
if (req[idx]) begin grant = idx; idx_next = idx; end
if (idx == 0) idx = idx_t'(DEPTH-1); else idx--;
end
end
always_ff @(posedge clk) begin
if (!rst_n) idx_last <= '0;
else if (idx_last != idx_next) idx_last <= idx_next;
end
endmodule
EOF
hierarchy -top test
proc
opt
equiv_opt -assert -multiclock opt_prienc
design -load postopt
select -assert-min 1 w:*rr*
select -assert-count 0 t:$shiftx
design -reset
log -pop
# RR2: non-power-of-2 DEPTH -- combinational equivalence over the reachable
# pointer range (idx_last in [0,DEPTH)). We rewrite one copy of the arbiter,
# leave a reference copy untouched, and SAT-prove they agree once the pointer
# input is mapped into [0,DEPTH).
log -header "RR2: round-robin arbiter, DEPTH=13 (reachable-range equiv)"
log -push
design -reset
read_verilog -sv <<EOF
module rr_dut #(parameter int N=13, parameter int W=4) (
input logic [N-1:0] req, input logic [W-1:0] s,
output logic [W-1:0] grant, output logic [W-1:0] idx_next
);
always_comb begin
logic [W-1:0] idx; idx=s; idx_next=s; grant='0;
for (int i=0;i<N;i++) begin
if (req[idx]) begin grant=idx; idx_next=idx; end
if (idx==0) idx=W'(N-1); else idx--;
end
end
endmodule
EOF
hierarchy -top rr_dut
proc
opt
design -save rr_gold
opt_prienc
# The serial chain collapsed into the log-depth network.
select -assert-min 1 w:*rr*
select -assert-count 0 t:$shiftx
design -save rr_gate
design -reset
design -copy-from rr_gold -as rr_ref rr_dut
design -copy-from rr_gate -as rr_dut rr_dut
read_verilog -sv <<EOF
module tb #(parameter int N=13, parameter int W=4)(input logic [N-1:0] req, input logic [W-1:0] si);
logic [W-1:0] s, g1,n1,g2,n2;
assign s = (si >= N) ? (si - N) : si;
rr_dut #(N,W) u1(.req(req),.s(s),.grant(g1),.idx_next(n1));
rr_ref #(N,W) u2(.req(req),.s(s),.grant(g2),.idx_next(n2));
always_comb begin
assert (g1 == g2);
assert (n1 == n2);
end
endmodule
EOF
hierarchy -top tb
flatten
chformal -lower
opt -full
sat -verify -prove-asserts -show-ports tb
design -reset
log -pop
# RR3: negative -- a downward-scanning arbiter (opposite rotation) is a
# different function and must not be rewritten as round-robin.
log -header "RR3: downward-scan arbiter -> no round-robin rewrite"
log -push
design -reset
read_verilog -sv <<EOF
module test #(parameter int N=13, parameter int W=4) (
input logic [N-1:0] req, input logic [W-1:0] s,
output logic [W-1:0] grant
);
always_comb begin
logic [W-1:0] idx; idx=s; grant='0;
for (int i=0;i<N;i++) begin
if (req[idx]) grant=idx;
if (idx==W'(N-1)) idx='0; else idx++; // scans the other way
end
end
endmodule
EOF
hierarchy -top test
proc
opt
opt_prienc
select -assert-count 0 w:*rr*
design -reset
log -pop

240
tests/opt/opt_priokey.ys Normal file
View file

@ -0,0 +1,240 @@
# Tests for opt_priokey
#
# The pass detects a serial "priority-by-key" set accumulator: several sources
# each carry a small key and claim it if no earlier source already did. This
# elaborates into a chain of dynamic-index reads/writes ($shiftx / $shift) into
# a wide one-hot "taken" vector, whose depth grows with both the number of
# sources P and the accumulator width S. Every dynamic read taken[key[j]] is
# provably equal to the pairwise reduction
#
# OR over i<j of ( set_guard[i] & key[i] == key[j] )
#
# so the pass replaces each read with that compare reduction and drops the wide
# dynamic indexing. Correctness of every rewrite is validated by an in-pass
# ConstEval fingerprint over the reachable key range [0,S).
#
# Each group exercises a specific facet:
# A: formal equivalence across (P,S) shapes (power-of-two S -> full equiv).
# B: structural win -- the $shiftx chain is gone after the rewrite.
# C: negative / no-op cases (no false rewrites).
#
# Convention: every object the pass emits is named with a `priokey_` suffix, so
# `select w:*priokey*` is a reliable "did the rewrite fire" probe.
# ============================================================================
# Group A: formal equivalence (equiv_opt -assert)
# ============================================================================
# A1: P=3 sources into a 64-slot accumulator (SW=6, all keys reachable).
log -header "A1: priority-by-key dedup P=3 S=64 (equiv)"
log -push
design -reset
read_verilog -sv <<EOF
module t #(parameter int P=3, parameter int S=64, parameter int SW=6)(
input logic [P-1:0] act,
input logic [P-1:0][SW-1:0] sel,
input logic [P-1:0] src,
output logic [P-1:0] win
);
logic [S-1:0] taken;
always_comb begin
taken = '0; win = '0;
for (int i=0;i<P;i++)
if (act[i] && !taken[sel[i]]) begin
taken[sel[i]] = 1'b1;
win[i] = src[i];
end
end
endmodule
EOF
hierarchy -top t
proc
opt
check -assert
equiv_opt -assert opt_priokey
design -load postopt
select -assert-min 1 w:*priokey*
select -assert-count 0 t:$shiftx
design -reset
log -pop
# A2: P=4 sources into a 16-slot accumulator (SW=4).
log -header "A2: priority-by-key dedup P=4 S=16 (equiv)"
log -push
design -reset
read_verilog -sv <<EOF
module t #(parameter int P=4, parameter int S=16, parameter int SW=4)(
input logic [P-1:0] act,
input logic [P-1:0][SW-1:0] sel,
input logic [P-1:0] src,
output logic [P-1:0] win
);
logic [S-1:0] taken;
always_comb begin
taken = '0; win = '0;
for (int i=0;i<P;i++)
if (act[i] && !taken[sel[i]]) begin
taken[sel[i]] = 1'b1;
win[i] = src[i];
end
end
endmodule
EOF
hierarchy -top t
proc
opt
check -assert
equiv_opt -assert opt_priokey
design -load postopt
select -assert-min 1 w:*priokey*
select -assert-count 0 t:$shiftx
design -reset
log -pop
# A3: P=5 sources into an 8-slot accumulator (SW=3) -- deeper source chain.
log -header "A3: priority-by-key dedup P=5 S=8 (equiv)"
log -push
design -reset
read_verilog -sv <<EOF
module t #(parameter int P=5, parameter int S=8, parameter int SW=3)(
input logic [P-1:0] act,
input logic [P-1:0][SW-1:0] sel,
input logic [P-1:0] src,
output logic [P-1:0] win
);
logic [S-1:0] taken;
always_comb begin
taken = '0; win = '0;
for (int i=0;i<P;i++)
if (act[i] && !taken[sel[i]]) begin
taken[sel[i]] = 1'b1;
win[i] = src[i];
end
end
endmodule
EOF
hierarchy -top t
proc
opt
check -assert
equiv_opt -assert opt_priokey
design -load postopt
select -assert-min 1 w:*priokey*
select -assert-count 0 t:$shiftx
design -reset
log -pop
# ============================================================================
# Group B: structural win (the dynamic-index chain disappears)
# ============================================================================
# B1: P=6 sources, 64-slot accumulator. Before the rewrite the serial scan uses
# per-source dynamic reads ($shiftx). After the rewrite those reads are gone,
# replaced by pairwise $eq comparisons -- the QoR (depth) win.
log -header "B1: priority-by-key structural, P=6 S=64"
log -push
design -reset
read_verilog -sv <<EOF
module t #(parameter int P=6, parameter int S=64, parameter int SW=6)(
input logic [P-1:0] act,
input logic [P-1:0][SW-1:0] sel,
input logic [P-1:0] src,
output logic [P-1:0] win
);
logic [S-1:0] taken;
always_comb begin
taken = '0; win = '0;
for (int i=0;i<P;i++)
if (act[i] && !taken[sel[i]]) begin
taken[sel[i]] = 1'b1;
win[i] = src[i];
end
end
endmodule
EOF
hierarchy -top t
proc
opt
# Serial baseline: the dynamic-index reads are present.
select -assert-min 1 t:$shiftx
opt_priokey
opt_clean
# The wide dynamic indexing is gone, replaced by pairwise key comparisons.
select -assert-count 0 t:$shiftx
select -assert-min 1 t:$eq
select -assert-min 1 w:*priokey*
design -reset
log -pop
# ============================================================================
# Group C: negative / no-op cases (no false rewrites)
# ============================================================================
# C1: the "taken" vector is a primary input, not a set accumulator rooted at 0.
# The dynamic read has no traceable set history, so the pass must not fire.
log -header "C1: non-accumulator dynamic read -> no rewrite"
log -push
design -reset
read_verilog -sv <<EOF
module t #(parameter int S=64, parameter int SW=6)(
input logic [S-1:0] taken,
input logic [SW-1:0] sel,
output logic hit
);
assign hit = taken[sel];
endmodule
EOF
hierarchy -top t
proc
opt
opt_priokey
select -assert-count 0 w:*priokey*
design -reset
log -pop
# C2: a plain per-lane passthrough -- no dynamic indexing at all.
log -header "C2: per-lane passthrough -> no rewrite"
log -push
design -reset
read_verilog -sv <<EOF
module t #(parameter int P=4)(
input logic [P-1:0] a,
input logic [P-1:0] b,
output logic [P-1:0] y
);
assign y = a & b;
endmodule
EOF
hierarchy -top t
proc
opt
opt_priokey
select -assert-count 0 w:*priokey*
design -reset
log -pop
# C3: a dynamic write accumulator with NO conflict check -- every source
# unconditionally sets its key and reads are absent (win is a direct index).
# There is no taken[]-guarded read chain to rewrite.
log -header "C3: unconditional writes, no guarded read -> no rewrite"
log -push
design -reset
read_verilog -sv <<EOF
module t #(parameter int P=3, parameter int S=16, parameter int SW=4)(
input logic [P-1:0][SW-1:0] sel,
output logic [S-1:0] taken
);
always_comb begin
taken = '0;
for (int i=0;i<P;i++)
taken[sel[i]] = 1'b1;
end
endmodule
EOF
hierarchy -top t
proc
opt
opt_priokey
select -assert-count 0 w:*priokey*
design -reset
log -pop