From 4c3c6df3a7d692fc15c8278c64bf3295d6552675 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Mon, 3 Aug 2026 14:58:21 -0700 Subject: [PATCH] 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 --- passes/opt/opt_modadd_tree.cc | 264 ++++++++++++++++++++++++++++--- passes/silimate/opt_vps.cc | 283 +++++++++++++++++++++++----------- 2 files changed, 437 insertions(+), 110 deletions(-) diff --git a/passes/opt/opt_modadd_tree.cc b/passes/opt/opt_modadd_tree.cc index 487c285f0..611550259 100644 --- a/passes/opt/opt_modadd_tree.cc +++ b/passes/opt/opt_modadd_tree.cc @@ -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 cells; + dict id; + int words = 0; + vector anc; // anc[i] = ancestors of cells[i], with itself + vector 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 + 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 &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> preds, succs; + dict npreds; + vector ready; + for (auto c : cone) { + pool 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())) + 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 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())) { + 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())) { + 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 &cx, + vector &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 &cx, + const vector &esc) + { + pool 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> ranked; - vector> cand_cones(GetSize(cands)); + vector> cand_cones(GetSize(cands)); for (int i = 0; i < GetSize(cands); i++) { if (walk_exhausted()) return false; - pool 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 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 fine_cuts; vector> fine_nodes; - pool covered; + vector covered(ix.words, 0); + int covered_size = 0; for (auto &r : ranked) { - const pool &cx = cand_cones[r.second]; - if (GetSize(cx) <= GetSize(covered)) + const vector &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 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 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(); diff --git a/passes/silimate/opt_vps.cc b/passes/silimate/opt_vps.cc index e3e0739b9..d3de654df 100644 --- a/passes/silimate/opt_vps.cc +++ b/passes/silimate/opt_vps.cc @@ -51,6 +51,20 @@ struct OptVpsWorker int min_stride; pool 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> 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 removed_cells; + + // OR-reduced select vector -> its $reduce_or cell, built on first use + // and maintained incrementally (rebuilding per group is O(cells)). + dict 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 &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> &buckets) + { + buckets.clear(); + std::vector 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 &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> 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 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 &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 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 &pmuxes) { - SigSpec decoder_y = decoder->getPort(ID::Y); - std::vector 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 gated_bits; dict 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 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++; } }