mirror of
https://github.com/YosysHQ/yosys
synced 2026-07-15 19:55:41 +00:00
opt: recognize three QoR logic-depth patterns
Extend two existing opt passes and add one new pass to collapse serial/dynamic-index structures that were leaving high logic depth: - opt_first_fit_alloc: recognize the "coalesce-matrix" first-fit allocator variant (same_cat[i][k] coalescing gated on the leader's enable, driven from a raw input enable). Rewrite both the lane_slot allocation and the xbar field gather from one shared log-depth scan. - opt_prienc: detect round-robin / rotated-priority scans (req scanned from idx_last downward with wraparound) and rewrite the depth-N idx--/req[idx] mux chain to rotate -> log-depth priority-encode -> unrotate. - opt_priokey (new): recognize priority-by-key one-hot accumulators and replace each dynamic taken[key] read ($shiftx/$bmux) with the equivalent pairwise-key-compare reduction, dropping the wide dynamic indexing. Supports -strict for full-key-range formal validation. Each includes self-contained tests (equiv_opt / sat -prove-asserts, mux-bound and negative cases) in tests/opt/. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
25f4376018
commit
00e48706df
7 changed files with 1381 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,74 @@ 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; });
|
||||
|
||||
int pairs = 0;
|
||||
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);
|
||||
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 +829,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 +866,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 +902,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 +910,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 +920,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 +930,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
464
passes/opt/opt_priokey.cc
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
240
tests/opt/opt_priokey.ys
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue