Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eliminate double inverts in pyrtl.optimize #462

Open
wants to merge 4 commits into
base: development
Choose a base branch
from

Conversation

gaborszita
Copy link
Contributor

Closes #447

Example:

Block before optimize Block after optimize (without my changes) Block after optimize (with my changes)
image image image

Copy link

codecov bot commented Jan 12, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 91.8%. Comparing base (5077c24) to head (6e872b3).

Additional details and impacted files
@@              Coverage Diff              @@
##           development    #462     +/-   ##
=============================================
+ Coverage         91.7%   91.8%   +0.2%     
=============================================
  Files               24      24             
  Lines             6403    6454     +51     
=============================================
+ Hits              5866    5920     +54     
+ Misses             537     534      -3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@fdxmw fdxmw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for fixing this issue! A couple high-level comments before we get into coding details:

  1. O(N^2) nested for loops over block.logic should generally be avoided in passes because they don't scale well as the number of nets increases - the pass will take too long to run. It seems possible to iterate over block.logic once and build a list of all the inverter chains in the block (see how other passes create a _ProducerList for example). Then I think we can iterate over that list and decide if each inverter chain can be simplified?
  2. This really needs a test :) Some test cases that come to mind:
    1. Chains of 1, 2, 3, and 4 inverters (even numbers of inverters should be removed, and odd numbers of inverters should be simplified to one inverter)
    2. A chain of 2 inverters with another user of the first inverter's output (should not be simplified)

@gaborszita
Copy link
Contributor Author

Thank you for fixing this issue! A couple high-level comments before we get into coding details:

  1. O(N^2) nested for loops over block.logic should generally be avoided in passes because they don't scale well as the number of nets increases - the pass will take too long to run. It seems possible to iterate over block.logic once and build a list of all the inverter chains in the block (see how other passes create a _ProducerList for example). Then I think we can iterate over that list and decide if each inverter chain can be simplified?

  2. This really needs a test :) Some test cases that come to mind:

    1. Chains of 1, 2, 3, and 4 inverters (even numbers of inverters should be removed, and odd numbers of inverters should be simplified to one inverter)
    2. A chain of 2 inverters with another user of the first inverter's output (should not be simplified)

All addressed

@gaborszita gaborszita requested a review from fdxmw January 17, 2025 22:24
Copy link
Contributor

@fdxmw fdxmw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please start with the comments on line 109 and 81 first. If those can be addressed, they will probably make many other comments obsolete. Thank you for improving this code!

for net in block.logic:
if net.op == "~":
invert_destination_wires[net.dests[0].name] = net
# If double invert nets are removed randomly, this may leave some double inverts behind.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We iterate over the block in topological order (see comment here

""" BlockIterator iterates over the block passed on init in topographic order.
) so the inverters will be added to invert_destination_wires in topological order. So this code shouldn't randomly remove inverters in this loop, since dictionaries preserve insertion order, see the bottom of this section https://docs.python.org/3/library/stdtypes.html#typesmapping

invert_destination_wires = {}
for net in block.logic:
if net.op == "~":
invert_destination_wires[net.dests[0].name] = net
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify this by mapping from the destination wirevector itself, rather than its name? invert_destination_wires[net.dests[0]] = net

# If the argument of the net is in invert_destination_wires, then it is a double invert
# If the net is in net_exclude_set, then it was already removed, so we do not process it
if net.args[0].name in invert_destination_wires and net not in net_exclude_set:
previous_net = invert_destination_wires[net.args[0].name]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repeats the dictionary lookup from the previous line, we can avoid one lookup with something like

previous_net = invert_destination_wires.get(net.args[0].name)
if previous_net is not None and ...

I think this also lets you combine this if statement with the next one

# Dictionary, key is the destination wire of the invert net, value is the invert net
invert_destination_wires = {}
for net in block.logic:
if net.op == "~":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove the used_elsewhere check if we check here for multiple destinations? Something like

if net.op == "~" and len(net.dests) == 1:

return False

new_logic = set()
net_exclude_set = set() # removed nets
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems better to call this removed_nets and remove the comment? :)

wire_removal_set.add(net.args[0])
removed_nets.add(net)
removed_nets.add(previous_net)
# remove removed_nets from invert_destination_wires to optimize the for loop
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is needed to ensure the loop eventually stops (it's not just an optimization?) since line 106 will keep repeating until the net is removed from invert_destination_wires

if previous_net.args[0].name in invert_destination_wires:
repeat = True
else:
new_logic.add(LogicNet('w', None, args=previous_net.args, dests=net.dests))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a wire net, which can't be removed by _remove_wire_nets since that pass ran already in optimize().

It seems be possible to do this without adding another wire, see how _remove_wire_nets rewrites net.args for other nets. I think we could do something similar here, with a data structure like _ProducerList that tracks even-numbered chains of inverters, and identifies the ultimate source of these chains

@gaborszita
Copy link
Contributor Author

I will fix these next week, I was busy this weekend.

@fdxmw
Copy link
Contributor

fdxmw commented Jan 27, 2025

I will fix these next week, I was busy this weekend.

Sounds great, no rush! Let me know if you have any questions about my comments, happy to discuss.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Optimize is currently missing the elimination of "double not"
2 participants