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
Initialize ssa decorator #324
Conversation
Summary: This adds a module `magma.ssa` which contains a decorator `@ssa` which is intended to take in a Python function and return a new Python function in SSA form. This includes a basic set of test cases in `tests/test_ssa` Test Plan: Reviewers: Subscribers: Tasks: Tags: Blame Revision: Differential Revision: https://phabricator.intern.facebook.com/D13458300
ast example is very cool! |
Pull Request Test Coverage Report for Build 1322
💛 - Coveralls |
1 similar comment
Pull Request Test Coverage Report for Build 1322
💛 - Coveralls |
Pull Request Test Coverage Report for Build 1339
💛 - Coveralls |
Pull Request Test Coverage Report for Build 1322
💛 - Coveralls |
Cool! Is this meant to be used in conjunction with the combinational decorator? If so can you include an example of how they would both be used and what a useful use case/target output is? |
magma/ssa/ssa.py
Outdated
def visit_Name(self, node): | ||
if isinstance(node.ctx, ast.Store): | ||
self.var_counter[node.id] += 1 | ||
if isinstance(node.ctx, ast.Store) or node.id in self.var_counter: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if isinstance(node.ctx, ast.Store) or node.id in self.var_counter:
Redundant check on if it's a store, no?
node.body = flatten([self.visit(s) for s in node.body]) | ||
return node | ||
|
||
def visit_Name(self, node): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to rename inputs.
For example, this is handled incorrectly:
@ssa
def default(I: m.Bits(2), x_0: m.Bit) -> m.Bit:
x = I[1]
if x_0:
x = I[0]
return x
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to preserve interface names, I would suggest inserting input renamings as the first thing in the SSA-ified code, so the output might reasonably look like this:
def default(I: m.Bits(2), x_0: m.Bit) -> m.Bit:
I_0, x_0_0 = I, x_0 # this is done in a single tuple assign to avoid things like def default(x_0: m.Bits(2), x_0_0: m.Bit), which would make x_0_0 = x_0, x_0_0_0 = x_0_0, which is also wrong
# rest of ssa translated code here using new input names...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first example I posted generates this right now:
def default(I: m.Bits(2), x_0: m.Bit) ->m.Bit:
x_0 = I[1]
x_1 = I[0]
x_2 = phi([x_0, x_1], x_0)
return x_2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a test for the edge case I mentioned in the second comment:
@ssa
def default(x_0: m.Bits(2), x_0_0: m.Bit) -> m.Bit:
x = x_0[1]
if x_0_0:
x = x_0[0]
return x
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just going to piggyback on this and say it might be a bigger issue than you think, if this is intended to be used in conjunction with other passes, I wouldn't be surprised if there would be renaming of variables happening in other passes.
For example, creating a wrapper module - I might rename the wires on the internal one by appending _0 to the inputs if I need to modify the wires before feeding them into the internal module. Rigel did something like this. Combined with the ssa pass things would break in ways that would be pretty odd.
node.id += f"_{self.var_counter[node.id]}" | ||
return node | ||
|
||
def visit_If(self, node): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this (the visit_If
function) might be made simpler if you kept track of two things, a dict with the counters for stores, and a dict mapping the variable to its last name for loads.
This way you would preserve the store dict across both branches in the if, and reset the load mapping dict for each branch. This would also eliminate the need to go through everything in the false_var_counter and remap variable store names.
The phi logic would still need to be roughly the same though, but you would have the two load mappings and then you would insert a phi for every entry where the last load name is different between the two dicts for a variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's another bug case I think:
def test_wat():
@ssa
def basic_if(I: m.Bit, S: m.Bit) -> m.Bit:
x = I
if S:
x = x
else:
x = x
return x
print(inspect.getsource(basic_if))
assert inspect.getsource(basic_if) == """\
def basic_if(I: m.Bits(2), S: m.Bit) ->m.Bit:
x_0 = I
x_1 = x_0
x_2 = x_0
x_3 = phi([x_2, x_1], S)
return x_3
"""
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this phi be the other way around?
def test_default():
@ssa
def default(I: m.Bits(2), S: m.Bit) -> m.Bit:
x = I[1]
if S:
x = I[0]
return x
assert inspect.getsource(default) == """\
def default(I: m.Bits(2), S: m.Bit) ->m.Bit:
x_0 = I[1]
x_1 = I[0]
x_2 = phi([x_0, x_1], S)
return x_2
"""
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind on that last point.
node.id += f"_{self.var_counter[node.id]}" | ||
return node | ||
|
||
def visit_If(self, node): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this phi be the other way around?
def test_default():
@ssa
def default(I: m.Bits(2), S: m.Bit) -> m.Bit:
x = I[1]
if S:
x = I[0]
return x
assert inspect.getsource(default) == """\
def default(I: m.Bits(2), S: m.Bit) ->m.Bit:
x_0 = I[1]
x_1 = I[0]
x_2 = phi([x_0, x_1], S)
return x_2
"""
@THofstee looks like the tests pass when you renamed the inputs, but that's because it only checks that the generated code makes sense. The changes you pushed look good. I think this is okay, since this logic/tests are self contained in the sense that they don't test how this affects use with magma, but that's by design (in case someone wants to use this for some other DSL). Basically, I think any magma logic (say Can you confirm if you think the input name issue has been resolved? Are there are any other issues we should be looking in to? Otherwise, I vote we merge and start using it and open up specific issues as they arise. |
I addressed all the comments I made in my review so I don't think there's anything else for the time being. |
This PR is tracking an effort to build up a library of standard decorators that
perform Python -> Python transformations, intended to be used by higher
level DSLs being constructed on top of magma.
This adds a module
magma.ssa
which contains a decorator@ssa
whichis intended to take in a Python function and return a new Python
function in SSA form.
This includes a basic set of test cases in
tests/test_ssa
.It also refactors some of the
m.circuit.combinational
code to be reusedacross the two decorators.