3
0
Fork 0
mirror of https://github.com/YosysHQ/yosys synced 2026-02-12 03:44:09 +00:00
yosys/passes/opt/opt_balance_tree.cc
Natalia 60ac3670cb Fix truncation issue in opt_balance_tree pass
Only allow rebalancing of cells with "natural" output widths (no truncation).
This prevents equivalence failures when moving operands between adders
with different intermediate truncation points.

For each operation type, the natural width is:
- Addition: max(A_WIDTH, B_WIDTH) + 1 (for carry bit)
- Multiplication: A_WIDTH + B_WIDTH
- Logic ops: max(A_WIDTH, B_WIDTH)

Fixes widlarizer's counterexample in YosysHQ/yosys#5605 where an 8-bit
intermediate wire was intentionally truncating adder results, and
rebalancing would change where that truncation occurred.
2026-01-14 13:14:56 -08:00

377 lines
12 KiB
C++

/*
* yosys -- Yosys Open SYnthesis Suite
*
* Copyright (C) 2012 Claire Xenia Wolf <claire@yosyshq.com>
* 2019 Eddie Hung <eddie@fpgeh.com>
* 2024 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 <deque>
USING_YOSYS_NAMESPACE
PRIVATE_NAMESPACE_BEGIN
struct OptBalanceTreeWorker {
// Module and signal map
Module *module;
SigMap sigmap;
// Counts of each cell type that are getting balanced
dict<IdString, int> cell_count;
// Check if cell is of the right type and has matching input/output widths
// Only allow cells with "natural" output widths (no truncation) to prevent
// equivalence issues when rebalancing (see YosysHQ/yosys#5605)
bool is_right_type(Cell* cell, IdString cell_type) {
if (cell->type != cell_type)
return false;
int y_width = cell->getParam(ID::Y_WIDTH).as_int();
int a_width = cell->getParam(ID::A_WIDTH).as_int();
int b_width = cell->getParam(ID::B_WIDTH).as_int();
// Calculate the "natural" output width for this operation
int natural_width;
if (cell_type == ID($add)) {
// Addition produces max(A_WIDTH, B_WIDTH) + 1 (for carry bit)
natural_width = std::max(a_width, b_width) + 1;
} else if (cell_type == ID($mul)) {
// Multiplication produces A_WIDTH + B_WIDTH
natural_width = a_width + b_width;
} else {
// Logic operations ($and/$or/$xor) produce max(A_WIDTH, B_WIDTH)
natural_width = std::max(a_width, b_width);
}
// Only allow cells where Y_WIDTH equals the natural width (no truncation)
return y_width == natural_width;
}
// Create a balanced binary tree from a vector of source signals
SigSpec create_balanced_tree(vector<SigSpec> &sources, IdString cell_type, Cell* cell) {
// Base case: if we have no sources, return an empty signal
if (sources.size() == 0)
return SigSpec();
// Base case: if we have only one source, return it
if (sources.size() == 1)
return sources[0];
// Base case: if we have two sources, create a single cell
if (sources.size() == 2) {
// Create a new cell of the same type
Cell* new_cell = module->addCell(NEW_ID, cell_type);
// Copy attributes from reference cell
new_cell->attributes = cell->attributes;
// Create output wire
int out_width = cell->getParam(ID::Y_WIDTH).as_int();
if (cell_type == ID($add))
out_width = max(sources[0].size(), sources[1].size()) + 1;
else if (cell_type == ID($mul))
out_width = sources[0].size() + sources[1].size();
Wire* out_wire = module->addWire(NEW_ID, out_width);
// Connect ports and fix up parameters
new_cell->setPort(ID::A, sources[0]);
new_cell->setPort(ID::B, sources[1]);
new_cell->setPort(ID::Y, out_wire);
new_cell->fixup_parameters();
new_cell->setParam(ID::A_SIGNED, cell->getParam(ID::A_SIGNED));
new_cell->setParam(ID::B_SIGNED, cell->getParam(ID::B_SIGNED));
// Update count and return output wire
cell_count[cell_type]++;
return out_wire;
}
// Recursive case: split sources into two groups and create subtrees
int mid = (sources.size() + 1) / 2;
vector<SigSpec> left_sources(sources.begin(), sources.begin() + mid);
vector<SigSpec> right_sources(sources.begin() + mid, sources.end());
SigSpec left_tree = create_balanced_tree(left_sources, cell_type, cell);
SigSpec right_tree = create_balanced_tree(right_sources, cell_type, cell);
// Create a cell to combine the two subtrees
Cell* new_cell = module->addCell(NEW_ID, cell_type);
// Copy attributes from reference cell
new_cell->attributes = cell->attributes;
// Create output wire
int out_width = cell->getParam(ID::Y_WIDTH).as_int();
if (cell_type == ID($add))
out_width = max(left_tree.size(), right_tree.size()) + 1;
else if (cell_type == ID($mul))
out_width = left_tree.size() + right_tree.size();
Wire* out_wire = module->addWire(NEW_ID, out_width);
// Connect ports and fix up parameters
new_cell->setPort(ID::A, left_tree);
new_cell->setPort(ID::B, right_tree);
new_cell->setPort(ID::Y, out_wire);
new_cell->fixup_parameters();
new_cell->setParam(ID::A_SIGNED, cell->getParam(ID::A_SIGNED));
new_cell->setParam(ID::B_SIGNED, cell->getParam(ID::B_SIGNED));
// Update count and return output wire
cell_count[cell_type]++;
return out_wire;
}
OptBalanceTreeWorker(Module *module, const vector<IdString> cell_types) : module(module), sigmap(module) {
// Do for each cell type
for (auto cell_type : cell_types) {
// Index all of the nets in the module
dict<SigSpec, Cell*> sig_to_driver;
dict<SigSpec, pool<Cell*>> sig_to_sink;
for (auto cell : module->selected_cells())
{
for (auto &conn : cell->connections())
{
if (cell->output(conn.first))
sig_to_driver[sigmap(conn.second)] = cell;
if (cell->input(conn.first))
{
SigSpec sig = sigmap(conn.second);
if (sig_to_sink.count(sig) == 0)
sig_to_sink[sig] = pool<Cell*>();
sig_to_sink[sig].insert(cell);
}
}
}
// Need to check if any wires connect to module ports
pool<SigSpec> input_port_sigs;
pool<SigSpec> output_port_sigs;
for (auto wire : module->selected_wires())
if (wire->port_input || wire->port_output) {
SigSpec sig = sigmap(wire);
for (auto bit : sig) {
if (wire->port_input)
input_port_sigs.insert(bit);
if (wire->port_output)
output_port_sigs.insert(bit);
}
}
// Actual logic starts here
pool<Cell*> consumed_cells;
for (auto cell : module->selected_cells())
{
// If consumed or not the correct type, skip
if (consumed_cells.count(cell) || !is_right_type(cell, cell_type))
continue;
// BFS, following all chains until they hit a cell of a different type
// Pick the longest one
auto y = sigmap(cell->getPort(ID::Y));
pool<Cell*> sinks;
pool<Cell*> current_loads = sig_to_sink[y];
pool<Cell*> next_loads;
while (!current_loads.empty())
{
// Find each sink and see what they are
for (auto x : current_loads)
{
// If not the correct type, don't follow any further
// (but add the originating cell to the list of sinks)
if (!is_right_type(x, cell_type))
{
sinks.insert(cell);
continue;
}
auto xy = sigmap(x->getPort(ID::Y));
// If this signal drives a port, add it to the sinks
// (even though it may not be the end of a chain)
for (auto bit : xy) {
if (output_port_sigs.count(bit) && !consumed_cells.count(x)) {
sinks.insert(x);
break;
}
}
// Search signal's fanout
auto& next = sig_to_sink[xy];
for (auto z : next)
next_loads.insert(z);
}
// If we couldn't find any downstream loads, stop.
// Create a reduction for each of the max-length chains we found
if (next_loads.empty())
{
for (auto s : current_loads)
{
// Not one of our gates? Don't follow any further
if (!is_right_type(s, cell_type))
continue;
sinks.insert(s);
}
break;
}
// Otherwise, continue down the chain
current_loads = next_loads;
next_loads.clear();
}
// We have our list of sinks, now go tree balance the chains
for (auto head_cell : sinks)
{
// Avoid duplication if we already were covered
if (consumed_cells.count(head_cell))
continue;
// Get sources of the chain
dict<SigSpec, int> sources;
dict<SigSpec, bool> signeds;
int inner_cells = 0;
std::deque<Cell*> bfs_queue = {head_cell};
while (bfs_queue.size())
{
Cell* x = bfs_queue.front();
bfs_queue.pop_front();
for (IdString port: {ID::A, ID::B}) {
auto sig = sigmap(x->getPort(port));
Cell* drv = sig_to_driver[sig];
bool drv_ok = drv && is_right_type(drv, cell_type);
for (auto bit : sig) {
if (input_port_sigs.count(bit) && !consumed_cells.count(drv)) {
drv_ok = false;
break;
}
}
if (drv_ok) {
inner_cells++;
bfs_queue.push_back(drv);
} else {
sources[sig]++;
signeds[sig] = x->getParam(port == ID::A ? ID::A_SIGNED : ID::B_SIGNED).as_bool();
}
}
}
if (inner_cells)
{
// Create a tree
log_debug(" Creating tree for %s with %d sources and %d inner cells...\n", log_id(head_cell), GetSize(sources), inner_cells);
// Build a vector of all source signals
vector<SigSpec> source_signals;
vector<bool> signed_flags;
for (auto &source : sources) {
for (int i = 0; i < source.second; i++) {
source_signals.push_back(source.first);
signed_flags.push_back(signeds[source.first]);
}
}
// If not all signed flags are the same, do not balance
if (!std::all_of(signed_flags.begin(), signed_flags.end(), [&](bool flag) { return flag == signed_flags[0]; })) {
continue;
}
// Create the balanced tree
SigSpec tree_output = create_balanced_tree(source_signals, cell_type, head_cell);
// Connect the tree output to the head cell's output
SigSpec head_output = sigmap(head_cell->getPort(ID::Y));
int connect_width = std::min(head_output.size(), tree_output.size());
module->connect(head_output.extract(0, connect_width), tree_output.extract(0, connect_width));
if (head_output.size() > tree_output.size()) {
SigBit sext_bit = head_cell->getParam(ID::A_SIGNED).as_bool() ? head_output[connect_width - 1] : State::S0;
module->connect(head_output.extract(connect_width, head_output.size() - connect_width), SigSpec(sext_bit, head_output.size() - connect_width));
}
// Mark consumed cell for removal
consumed_cells.insert(head_cell);
}
}
}
// Remove all consumed cells, which now have been replaced by trees
for (auto cell : consumed_cells)
module->remove(cell);
}
}
};
struct OptBalanceTreePass : public Pass {
OptBalanceTreePass() : Pass("opt_balance_tree", "$and/$or/$xor/$add/$mul cascades to trees") { }
void help() override {
// |---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|
log("\n");
log(" opt_balance_tree [options] [selection]\n");
log("\n");
log("This pass converts cascaded chains of $and/$or/$xor/$add/$mul cells into\n");
log("trees of cells to improve timing.\n");
log("\n");
log(" -arith\n");
log(" only convert arithmetic cells.\n");
log("\n");
log(" -logic\n");
log(" only convert logic cells.\n");
log("\n");
}
void execute(std::vector<std::string> args, RTLIL::Design *design) override {
log_header(design, "Executing OPT_BALANCE_TREE pass (cell cascades to trees).\n");
// Handle arguments
size_t argidx;
vector<IdString> cell_types = {ID($and), ID($or), ID($xor), ID($add), ID($mul)};
for (argidx = 1; argidx < args.size(); argidx++) {
if (args[argidx] == "-arith") {
cell_types = {ID($add), ID($mul)};
continue;
}
if (args[argidx] == "-logic") {
cell_types = {ID($and), ID($or), ID($xor)};
continue;
}
break;
}
extra_args(args, argidx, design);
// Count of all cells that were packed
dict<IdString, int> cell_count;
for (auto module : design->selected_modules()) {
OptBalanceTreeWorker worker(module, cell_types);
for (auto cell : worker.cell_count) {
cell_count[cell.first] += cell.second;
}
}
// Log stats
for (auto cell_type : cell_types)
log("Converted %d %s cells into trees.\n", cell_count[cell_type], log_id(cell_type));
// Clean up
Yosys::run_pass("clean -purge");
}
} OptBalanceTreePass;
PRIVATE_NAMESPACE_END