3
0
Fork 0
mirror of https://github.com/YosysHQ/yosys synced 2026-08-09 23:51:08 +00:00

opt_modadd_tree, opt_vps: drop the quadratic scans

Both passes re-derived the same information once per candidate, so runtime
grew with the square of design size: opt_modadd_tree took 56s on nvdla and
23s on VeeR, and opt_vps went superlinear as soon as a module held many
variable-part-select decoders.

opt_modadd_tree rebuilt the fan-in cone and re-ran the cut test for every
candidate in a root's cone.  Build the cone once into a ConeIndex holding
per-cell ancestor and escape sets as bitsets, so a candidate's cone and cut
queries are word operations.  The index propagates by fixed point rather
than a plain topological sweep because cones that contain unflattened
register modules are not DAGs, and a DAG-only version silently lost matches.

opt_vps scanned every cell in the module once per decoder in each of its
three phases, and resolved each select bit by walking the decoder's whole Y
port.  Index the decoder output bits once, bucket each $pmux under the
decoder its select bits come from, and hoist the per-decoder index roots and
the $reduce_or lookup out of the candidate loops.  Because a $pmux can land
in two buckets when its overflow AND mixes decoders, removals go through
remove_cell() so a later bucket skips cells an earlier decoder killed.

Post-pass netlists are unchanged on VeeR, nvdla, sargantana and a synthetic
decoder ladder.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Akash Levy 2026-08-03 14:58:21 -07:00
parent 0259898834
commit 4c3c6df3a7
2 changed files with 437 additions and 110 deletions

View file

@ -182,6 +182,225 @@ struct OptModAddTreeWorker : CutRegionWorker
return reached;
}
// --------------------------------------------------------- cone index
//
// Every same-width bus produced in the tail cone is a candidate cut, so
// find_chain tests O(cone) of them. Walking a fresh fanin cone and a
// fresh escape check per candidate makes that quadratic in the cone
// size, which dominates the pass on real designs. Index the cone once
// instead: per cell, the bitset of its ancestors (itself included) and
// the bitset of in-cone cells reading any of those ancestors. A
// candidate's cone and its escaping frontier are then a few word-ORs,
// and only the frontier has to be inspected cell by cell.
struct ConeIndex {
vector<Cell *> cells;
dict<Cell *, int> id;
int words = 0;
vector<uint64_t> anc; // anc[i] = ancestors of cells[i], with itself
vector<uint64_t> esc; // esc[i] = in-cone readers of anything in anc[i]
const uint64_t *anc_of(int i) const { return &anc[size_t(i) * words]; }
const uint64_t *esc_of(int i) const { return &esc[size_t(i) * words]; }
};
// The index costs cells^2/4 bytes, which max_cone_cells keeps far below
// this; the guard only stops that bound from turning into a huge
// allocation if it is ever raised.
static const int max_index_cells = 8192;
// Returns true when `dst` gained a bit, which drives the fixed point in
// build_cone_index.
static bool bits_or(uint64_t *dst, const uint64_t *src, int words)
{
bool grew = false;
for (int w = 0; w < words; w++) {
uint64_t old = dst[w];
dst[w] = old | src[w];
grew |= dst[w] != old;
}
return grew;
}
// Is every bit of `sub` also set in `sup`?
static bool bits_subset(const uint64_t *sub, const uint64_t *sup, int words)
{
for (int w = 0; w < words; w++)
if (sub[w] & ~sup[w])
return false;
return true;
}
static int bits_count(const uint64_t *a, int words)
{
int n = 0;
for (int w = 0; w < words; w++)
n += __builtin_popcountll(a[w]);
return n;
}
// Call `fn(i)` for every index in `a` (optionally masked to those not in
// `minus`), scanning words rather than the whole cone.
template <typename Fn>
static void bits_each(const uint64_t *a, const uint64_t *minus, int words, Fn fn)
{
for (int w = 0; w < words; w++) {
uint64_t v = minus ? (a[w] & ~minus[w]) : a[w];
while (v) {
int b = __builtin_ctzll(v);
v &= v - 1;
fn(w * 64 + b);
}
}
}
// Index the cone. Cells are visited in topological order where one
// exists, but a cone can contain feedback through a cell this pass does
// not recognise as sequential (an unflattened register module), so the
// accumulation below is a fixed point rather than a single sweep.
bool build_cone_index(const pool<Cell *> &cone, ConeIndex &ix)
{
int n = GetSize(cone);
if (n > max_index_cells)
return false;
ix.words = (n + 63) / 64;
ix.cells.clear();
ix.id.clear();
// In-cone driver edges, then Kahn ordering over them
dict<Cell *, vector<Cell *>> preds, succs;
dict<Cell *, int> npreds;
vector<Cell *> ready;
for (auto c : cone) {
pool<Cell *> cp;
for (auto &conn : c->connections()) {
if (!c->input(conn.first))
continue;
for (auto bit : sigmap(conn.second)) {
if (!bit.wire)
continue;
Cell *drv = bit_to_driver.at(bit, nullptr);
if (drv != nullptr && drv != c && cone.count(drv))
cp.insert(drv);
}
}
npreds[c] = GetSize(cp);
for (auto p : cp) {
preds[c].push_back(p);
succs[p].push_back(c);
}
if (cp.empty())
ready.push_back(c);
}
for (size_t head = 0; head < ready.size(); head++) {
Cell *c = ready[head];
ix.id[c] = GetSize(ix.cells);
ix.cells.push_back(c);
for (auto s : succs.at(c, vector<Cell *>()))
if (--npreds.at(s) == 0)
ready.push_back(s);
}
// Whatever the ordering left over sits on a feedback loop; append it
// so every cone cell is still indexed.
for (auto c : cone)
if (!ix.id.count(c)) {
ix.id[c] = GetSize(ix.cells);
ix.cells.push_back(c);
}
// Direct in-cone readers of each cell's outputs
vector<uint64_t> succ_bits(size_t(n) * ix.words, 0);
for (int i = 0; i < n; i++) {
Cell *c = ix.cells[i];
for (auto &conn : c->connections()) {
if (!c->output(conn.first))
continue;
for (auto bit : sigmap(conn.second)) {
if (!bit.wire)
continue;
for (auto sink : bit_to_sinks.at(bit, pool<Cell *>())) {
int j = ix.id.at(sink, -1);
if (j >= 0)
succ_bits[size_t(i) * ix.words + j / 64] |=
1ULL << (j % 64);
}
}
}
}
// Accumulate both sets. Sets only grow and are bounded by the cone,
// so the fixed point converges; an acyclic cone reaches it on the
// first sweep and only pays for the confirming one.
ix.anc.assign(size_t(n) * ix.words, 0);
ix.esc.assign(size_t(n) * ix.words, 0);
for (int i = 0; i < n; i++) {
ix.anc[size_t(i) * ix.words + i / 64] |= 1ULL << (i % 64);
bits_or(&ix.esc[size_t(i) * ix.words], &succ_bits[size_t(i) * ix.words],
ix.words);
}
for (bool grew = true; grew;) {
grew = false;
for (int i = 0; i < n; i++) {
uint64_t *a = &ix.anc[size_t(i) * ix.words];
uint64_t *e = &ix.esc[size_t(i) * ix.words];
for (auto p : preds.at(ix.cells[i], vector<Cell *>())) {
int j = ix.id.at(p);
grew |= bits_or(a, ix.anc_of(j), ix.words);
grew |= bits_or(e, ix.esc_of(j), ix.words);
}
charge_walk(1);
}
}
return true;
}
// Fanin cone of `x` inside the indexed cone, as a bitset. False when a
// bit of `x` is not produced by an indexed cell.
bool indexed_cone(const ConeIndex &ix, const SigSpec &x, vector<uint64_t> &cx,
vector<uint64_t> &esc)
{
cx.assign(ix.words, 0);
esc.assign(ix.words, 0);
for (auto bit : sigmap(x)) {
Cell *drv = bit_to_driver.at(bit, nullptr);
int i = drv == nullptr ? -1 : ix.id.at(drv, -1);
if (i < 0)
return false;
bits_or(cx.data(), ix.anc_of(i), ix.words);
bits_or(esc.data(), ix.esc_of(i), ix.words);
}
return true;
}
// `is_cut` restated over the frontier: nothing outside the candidate's
// cone may read a signal produced inside it other than the cut itself.
// Only cells that actually read into the cone are inspected.
bool indexed_is_cut(const ConeIndex &ix, const SigSpec &x, const vector<uint64_t> &cx,
const vector<uint64_t> &esc)
{
pool<SigBit> xbits = sig_bit_pool(x);
bool ok = true;
bits_each(esc.data(), cx.data(), ix.words, [&](int i) {
if (!ok)
return;
Cell *s = ix.cells[i];
charge_walk(1);
for (auto &conn : s->connections()) {
if (!s->input(conn.first))
continue;
for (auto bit : sigmap(conn.second)) {
if (!bit.wire || xbits.count(bit))
continue;
Cell *drv = bit_to_driver.at(bit, nullptr);
int j = drv == nullptr ? -1 : ix.id.at(drv, -1);
if (j >= 0 && (cx[j / 64] >> (j % 64)) & 1)
ok = false;
}
}
});
return ok;
}
// Rough post-techmap depth of a cell, so a wide adder is not counted as
// one level next to a mux.
static int cell_depth(Cell *c)
@ -263,52 +482,53 @@ struct OptModAddTreeWorker : CutRegionWorker
if (GetSize(cands) + 1 < min_nodes)
return false;
ConeIndex ix;
if (!build_cone_index(cone, ix))
return false;
// Keep the valid cuts, ordered by how much of the cone they cover.
vector<std::pair<int, int>> ranked;
vector<pool<Cell *>> cand_cones(GetSize(cands));
vector<vector<uint64_t>> cand_cones(GetSize(cands));
for (int i = 0; i < GetSize(cands); i++) {
if (walk_exhausted())
return false;
pool<SigBit> sub_leaves;
if (!get_cone(cands[i], cand_cones[i], sub_leaves, max_cone_cells, max_leaf_bits))
continue;
// Cheapest test first: it only reads the bus's own fanout, so
// buses that merely pass through the cone cost O(w).
if (!cut_is_internal(cands[i], cone))
continue;
if (!is_cut(cands[i], cand_cones[i], cone))
vector<uint64_t> esc;
if (!indexed_cone(ix, cands[i], cand_cones[i], esc))
continue;
ranked.push_back({GetSize(cand_cones[i]), i});
if (!indexed_is_cut(ix, cands[i], cand_cones[i], esc))
continue;
ranked.push_back({bits_count(cand_cones[i].data(), ix.words), i});
}
std::sort(ranked.begin(), ranked.end());
// Valid cuts nest; drop any that does not extend the previous one.
vector<SigSpec> fine_cuts;
vector<pool<Cell *>> fine_nodes;
pool<Cell *> covered;
vector<uint64_t> covered(ix.words, 0);
int covered_size = 0;
for (auto &r : ranked) {
const pool<Cell *> &cx = cand_cones[r.second];
if (GetSize(cx) <= GetSize(covered))
const vector<uint64_t> &cx = cand_cones[r.second];
if (r.first <= covered_size)
continue;
bool nests = true;
for (auto c : covered)
if (!cx.count(c)) {
nests = false;
break;
}
if (!nests)
if (!bits_subset(covered.data(), cx.data(), ix.words))
continue;
pool<Cell *> node;
for (auto c : cx)
if (!covered.count(c))
node.insert(c);
bits_each(cx.data(), covered.data(), ix.words,
[&](int i) { node.insert(ix.cells[i]); });
fine_cuts.push_back(cands[r.second]);
fine_nodes.push_back(node);
covered = cx;
covered_size = r.first;
}
{
pool<Cell *> node;
for (auto c : cone)
if (!covered.count(c))
node.insert(c);
for (int i = 0; i < GetSize(ix.cells); i++)
if (!((covered[i / 64] >> (i % 64)) & 1))
node.insert(ix.cells[i]);
if (node.empty() && !fine_cuts.empty()) {
fine_cuts.pop_back();
fine_nodes.pop_back();

View file

@ -51,6 +51,20 @@ struct OptVpsWorker
int min_stride;
pool<Cell *> vps_shr_cells;
// Decoder $shl output bit -> (decoder, one-hot position). Lets the
// per-decoder scans resolve a select bit in O(1) instead of walking
// every decoder's whole Y port.
dict<SigBit, std::pair<Cell *, int>> decoder_bit_pos;
// Cells handed to remove_cell(); the $pmux buckets below are built
// once, so iterations must skip entries killed by an earlier decoder.
pool<Cell *> removed_cells;
// OR-reduced select vector -> its $reduce_or cell, built on first use
// and maintained incrementally (rebuilding per group is O(cells)).
dict<SigSpec, Cell *> reduce_or_map;
bool reduce_or_map_valid = false;
OptVpsWorker(Module *module, int min_stride)
: module(module), sigmap(module), min_stride(min_stride)
{
@ -103,65 +117,157 @@ struct OptVpsWorker
return true;
}
// Index every decoder's one-hot output bits. First writer wins so that
// aliased outputs resolve like the old first-decoder-in-list scan.
void index_decoders(const std::vector<Cell *> &decoders)
{
decoder_bit_pos.clear();
for (auto decoder : decoders) {
SigSpec y = decoder->getPort(ID::Y);
for (int i = 0; i < GetSize(y); i++) {
SigBit b = sigmap(y[i]);
if (!decoder_bit_pos.count(b))
decoder_bit_pos[b] = std::make_pair(decoder, i);
}
}
}
// One-hot position of an already-sigmapped bit within a specific
// decoder's output, or -1 if it is not that decoder's bit.
int decoder_pos_of(SigBit mapped, Cell *decoder)
{
auto it = decoder_bit_pos.find(mapped);
if (it == decoder_bit_pos.end() || it->second.first != decoder)
return -1;
return it->second.second;
}
// The two inputs of a single-bit AND gate, or false if not one.
bool and_gate_inputs(Cell *driver, SigBit &a, SigBit &b)
{
if (driver->type == ID($and)) {
SigSpec port_a = driver->getPort(ID::A);
SigSpec port_b = driver->getPort(ID::B);
if (GetSize(port_a) != 1 || GetSize(port_b) != 1)
return false;
a = sigmap(port_a[0]);
b = sigmap(port_b[0]);
return true;
}
if (driver->type == ID($_AND_)) {
a = sigmap(driver->getPort(ID::A));
b = sigmap(driver->getPort(ID::B));
return true;
}
return false;
}
// Trace an S-port bit back through an optional AND gate to find
// which decoder output position it comes from. Returns -1 on failure.
// If overflow_cond is non-null, stores the non-decoder input of the
// AND gate (the overflow mask bit), or State::S1 if direct.
int trace_to_decoder_pos(SigBit bit, SigSpec &decoder_y,
int trace_to_decoder_pos(SigBit bit, Cell *decoder,
SigBit *overflow_cond = nullptr)
{
SigBit mapped = sigmap(bit);
for (int i = 0; i < GetSize(decoder_y); i++)
if (sigmap(decoder_y[i]) == mapped) {
if (overflow_cond)
*overflow_cond = State::S1;
return i;
}
int pos = decoder_pos_of(mapped, decoder);
if (pos >= 0) {
if (overflow_cond)
*overflow_cond = State::S1;
return pos;
}
Cell *driver = bit_drivers.at(mapped, nullptr);
if (!driver)
SigBit a, b;
if (!driver || !and_gate_inputs(driver, a, b))
return -1;
if (driver->type == ID($and)) {
SigSpec port_a = driver->getPort(ID::A);
SigSpec port_b = driver->getPort(ID::B);
if (GetSize(port_a) == 1 && GetSize(port_b) == 1) {
SigBit a = sigmap(port_a[0]);
SigBit b = sigmap(port_b[0]);
for (int i = 0; i < GetSize(decoder_y); i++) {
SigBit dy = sigmap(decoder_y[i]);
if (dy == a) {
if (overflow_cond) *overflow_cond = b;
return i;
}
if (dy == b) {
if (overflow_cond) *overflow_cond = a;
return i;
}
}
}
// Both AND inputs can be decoder bits; the old linear scan over Y
// returned the lower position, so keep that tie-break.
int pa = decoder_pos_of(a, decoder), pb = decoder_pos_of(b, decoder);
if (pa >= 0 && (pb < 0 || pa <= pb)) {
if (overflow_cond) *overflow_cond = b;
return pa;
}
if (driver->type == ID($_AND_)) {
SigBit a = sigmap(driver->getPort(ID::A));
SigBit b = sigmap(driver->getPort(ID::B));
for (int i = 0; i < GetSize(decoder_y); i++) {
SigBit dy = sigmap(decoder_y[i]);
if (dy == a) {
if (overflow_cond) *overflow_cond = b;
return i;
}
if (dy == b) {
if (overflow_cond) *overflow_cond = a;
return i;
}
}
if (pb >= 0) {
if (overflow_cond) *overflow_cond = a;
return pb;
}
return -1;
}
// Bucket every $pmux under the decoder(s) its select bits can come
// from. All three per-decoder scans need every S bit to resolve
// against one decoder, so a bucket is a superset of what they match
// and each scan shrinks from O(cells) to O(its own candidates).
void bucket_pmuxes(dict<Cell *, std::vector<Cell *>> &buckets)
{
buckets.clear();
std::vector<Cell *> hits, seen;
for (auto cell : module->selected_cells()) {
if (cell->type != ID($pmux))
continue;
SigSpec sig_s = cell->getPort(ID::S);
seen.clear();
for (int i = 0; i < GetSize(sig_s); i++) {
decoders_of_bit(sig_s[i], hits);
for (auto d : hits)
// a $pmux reaches one or two decoders, so scanning
// seen linearly beats hashing
if (std::find(seen.begin(), seen.end(), d) == seen.end()) {
seen.push_back(d);
buckets[d].push_back(cell);
}
}
}
}
// Decoders whose one-hot output can reach this select bit, directly or
// through the overflow AND gate (whose two inputs may differ).
void decoders_of_bit(SigBit bit, std::vector<Cell *> &out)
{
out.clear();
auto add = [&](SigBit b) {
auto it = decoder_bit_pos.find(sigmap(b));
if (it != decoder_bit_pos.end())
out.push_back(it->second.first);
};
SigBit mapped = sigmap(bit);
add(mapped);
if (!out.empty())
return;
Cell *driver = bit_drivers.at(mapped, nullptr);
SigBit a, b;
if (driver && and_gate_inputs(driver, a, b)) {
add(a);
add(b);
}
}
// Fill reduce_or_map on first use. Callers erase the entries they
// consume and register the cells they add, so this stays a one-off
// instead of an O(cells) rebuild per group.
void build_reduce_or_map()
{
if (reduce_or_map_valid)
return;
for (auto cell : module->cells())
if (cell->type == ID($reduce_or))
reduce_or_map[sigmap(cell->getPort(ID::A))] = cell;
reduce_or_map_valid = true;
}
// Remove a cell, remembering it so stale $pmux bucket entries are
// skipped rather than dereferenced.
void remove_cell(Cell *cell)
{
removed_cells.insert(cell);
module->remove(cell);
}
// Extract the constant addend from a binary_index signal.
// If binary_index = $add(dynamic, C) or $add(C, dynamic),
// return C. Otherwise return 0. Handles chains of
@ -847,7 +953,7 @@ struct OptVpsWorker
for (auto &c : cands) {
module->connect(c.ybit, SigBit(shifted, (c.konst - dmin) % M));
module->remove(c.cell);
remove_cell(c.cell);
pmux_replaced++;
}
@ -893,7 +999,7 @@ struct OptVpsWorker
for (auto &c : cands) {
module->connect(c.ybit, SigBit(shifted, c.konst - dmin));
module->remove(c.cell);
remove_cell(c.cell);
pmux_replaced++;
}
@ -919,6 +1025,11 @@ struct OptVpsWorker
if (is_decoder_shl(cell))
decoders.push_back(cell);
index_decoders(decoders);
dict<Cell *, std::vector<Cell *>> pmux_by_decoder;
bucket_pmuxes(pmux_by_decoder);
// --- Cross-decoder VPS read merge ---
// Collect stride-1 VPS read candidates across ALL decoders.
// Group by the underlying SOURCE REGISTER (identified by the
@ -943,11 +1054,16 @@ struct OptVpsWorker
std::vector<XReadCandidate> all_reads;
for (auto decoder : decoders) {
SigSpec decoder_y = decoder->getPort(ID::Y);
auto bucket = pmux_by_decoder.find(decoder);
if (bucket == pmux_by_decoder.end())
continue;
for (auto cell : module->selected_cells()) {
if (cell->type != ID($pmux))
continue;
// Decoder-invariant, so hoisted out of the candidate loop
SigSpec binary_idx = decoder->getPort(ID::B);
SigSpec roots = trace_input_roots(binary_idx);
int idx_c = eval_at_zero(binary_idx);
for (auto cell : bucket->second) {
int W = cell->getParam(ID::WIDTH).as_int();
if (W <= 1) continue;
SigSpec sig_a = cell->getPort(ID::A);
@ -961,7 +1077,7 @@ struct OptVpsWorker
for (int i = 0; i < N; i++) {
SigBit sb = sigmap(sig_s[i]);
if (sb == State::S0) continue;
int pos = trace_to_decoder_pos(sig_s[i], decoder_y);
int pos = trace_to_decoder_pos(sig_s[i], decoder);
if (pos < 0) break;
dec_positions.push_back(pos);
s_indices.push_back(i);
@ -1006,10 +1122,6 @@ struct OptVpsWorker
}
if (!reg_wire) continue;
SigSpec binary_idx = decoder->getPort(ID::B);
SigSpec roots = trace_input_roots(binary_idx);
int idx_c = eval_at_zero(binary_idx);
all_reads.push_back({decoder, cell, W, base, sliding_n,
{s_indices.begin(), s_indices.begin() + sliding_n},
reg_wire, reg_offset, idx_c, roots});
@ -1224,7 +1336,7 @@ struct OptVpsWorker
log_id(r.pmux->name), r.W, eff_offset(r),
byte_offset + r.W - 1, byte_offset);
module->remove(r.pmux);
remove_cell(r.pmux);
pmux_replaced++;
vps_reads_replaced++;
}
@ -1415,7 +1527,7 @@ struct OptVpsWorker
log_id(r.pmux->name), r.W, r.base,
byte_offset + r.W - 1, byte_offset);
module->remove(r.pmux);
remove_cell(r.pmux);
pmux_replaced++;
vps_reads_replaced++;
}
@ -1433,8 +1545,12 @@ struct OptVpsWorker
// Process remaining decoders normally (for VPS writes and
// unmerged VPS reads — merged reads' $pmux cells were
// already removed, so they won't be found again)
for (auto decoder : decoders)
process_decoder(decoder);
bucket_pmuxes(pmux_by_decoder);
for (auto decoder : decoders) {
auto bucket = pmux_by_decoder.find(decoder);
if (bucket != pmux_by_decoder.end())
process_decoder(decoder, bucket->second);
}
// --- Shared barrel shifter merge ---
// After all VPS reads have been converted to $shr cells,
@ -1609,16 +1725,15 @@ struct OptVpsWorker
info.reg_offset, off,
off, off + info.output_width - 1);
module->remove(info.shr);
remove_cell(info.shr);
}
groups_optimized++;
}
}
void process_vps_reads(Cell *decoder)
void process_vps_reads(Cell *decoder, const std::vector<Cell *> &pmuxes)
{
SigSpec decoder_y = decoder->getPort(ID::Y);
SigSpec binary_index = decoder->getPort(ID::B);
struct ReadCandidate {
@ -1630,8 +1745,8 @@ struct OptVpsWorker
};
std::vector<ReadCandidate> read_candidates;
for (auto cell : module->selected_cells()) {
if (cell->type != ID($pmux))
for (auto cell : pmuxes) {
if (removed_cells.count(cell))
continue;
int W = cell->getParam(ID::WIDTH).as_int();
if (W <= 1)
@ -1654,7 +1769,7 @@ struct OptVpsWorker
SigBit sb = sigmap(sig_s[i]);
if (sb == State::S0)
continue;
int pos = trace_to_decoder_pos(sig_s[i], decoder_y);
int pos = trace_to_decoder_pos(sig_s[i], decoder);
if (pos < 0)
break;
dec_positions.push_back(pos);
@ -1916,7 +2031,7 @@ struct OptVpsWorker
log2_align > 0 ?
stringf(", align=%d", 1 << log2_align).c_str() : "");
module->remove(cell);
remove_cell(cell);
pmux_replaced++;
vps_reads_replaced++;
}
@ -1925,14 +2040,12 @@ struct OptVpsWorker
groups_optimized++;
}
void process_decoder(Cell *decoder)
void process_decoder(Cell *decoder, const std::vector<Cell *> &pmuxes)
{
SigSpec decoder_y = decoder->getPort(ID::Y);
std::vector<PmuxInfo> candidates;
for (auto cell : module->selected_cells()) {
if (cell->type != ID($pmux))
for (auto cell : pmuxes) {
if (removed_cells.count(cell))
continue;
if (cell->getParam(ID::WIDTH).as_int() != 1)
continue;
@ -1949,7 +2062,7 @@ struct OptVpsWorker
bool valid = true;
for (int i = 0; i < s_width; i++) {
int pos = trace_to_decoder_pos(sig_s[i], decoder_y);
int pos = trace_to_decoder_pos(sig_s[i], decoder);
if (pos < 0) { valid = false; break; }
positions.push_back(pos);
}
@ -1970,7 +2083,7 @@ struct OptVpsWorker
}
// Detect VPS read patterns (WIDTH > 1) from this decoder
process_vps_reads(decoder);
process_vps_reads(decoder, pmuxes);
if (candidates.empty())
return;
@ -2048,8 +2161,6 @@ struct OptVpsWorker
log(" VPS group: decoder %s, base=%d, %d bits, stride=%d, %d lanes\n",
log_id(decoder->name), base, N, W, lane_count);
SigSpec decoder_y = decoder->getPort(ID::Y);
// Collect gated decoder bits and overflow conditions
dict<int, SigBit> gated_bits;
dict<int, SigBit> overflow_bits;
@ -2069,7 +2180,7 @@ struct OptVpsWorker
} else {
gated_bits[pos] = sb;
SigBit ov_cond;
trace_to_decoder_pos(sb, decoder_y, &ov_cond);
trace_to_decoder_pos(sb, decoder, &ov_cond);
overflow_bits[pos] = ov_cond;
}
}
@ -2135,8 +2246,11 @@ struct OptVpsWorker
lane_en[L] = lane_bits[0];
} else {
Wire *w = module->addWire(NEW_ID_SUFFIX("vps_lane_en"), 1);
module->addReduceOr(NEW_ID_SUFFIX("vps_lane_or"), lane_bits, w,
false, cell_src(candidates[group_start + L * W].cell));
Cell *ror = module->addReduceOr(NEW_ID_SUFFIX("vps_lane_or"),
lane_bits, w, false,
cell_src(candidates[group_start + L * W].cell));
if (reduce_or_map_valid)
reduce_or_map[sigmap(lane_bits)] = ror;
lane_en[L] = SigBit(w);
}
}
@ -2202,14 +2316,7 @@ struct OptVpsWorker
}
}
// Build lookup: S SigSpec (through sigmap) -> $reduce_or cell
dict<SigSpec, Cell *> reduce_or_map;
for (auto cell : module->cells()) {
if (cell->type != ID($reduce_or))
continue;
SigSpec a = sigmap(cell->getPort(ID::A));
reduce_or_map[a] = cell;
}
build_reduce_or_map();
if (full_collapse) {
log(" full feedback collapse: %d lanes, wr_en mux %s\n",
@ -2259,7 +2366,7 @@ struct OptVpsWorker
}
for (auto c : cells_to_remove)
module->remove(c);
remove_cell(c);
// Remove redundant top-level wr_en mux if all its B-port
// bits are now driven by the per-lane muxes.
@ -2267,7 +2374,7 @@ struct OptVpsWorker
SigSpec wr_y = top_wr_mux->getPort(ID::Y);
SigSpec wr_b = top_wr_mux->getPort(ID::B);
module->connect(wr_y, wr_b);
module->remove(top_wr_mux);
remove_cell(top_wr_mux);
log(" removed redundant top-level wr_en mux %s\n",
log_id(top_wr_mux->name));
}
@ -2293,12 +2400,12 @@ struct OptVpsWorker
if (it != reduce_or_map.end()) {
Cell *ror = it->second;
module->connect(ror->getPort(ID::Y), lane_en[L]);
module->remove(ror);
remove_cell(ror);
reduce_or_map.erase(it);
reduce_or_replaced++;
}
module->remove(pmux_cell);
remove_cell(pmux_cell);
pmux_replaced++;
}
}