-
Notifications
You must be signed in to change notification settings - Fork 22
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
Prototype combinational2/sequential2 interface #668
Conversation
Here's another proposal: we can have call method declare IO as before, and init can append new ports to the IO if desired, however this requires us to use a different code path than Generator2 since it doesn't follow this pattern, but it should use similar logic. This would then allow Peak to use the new version without any changes. |
Actually, the problem with the above method boils down to the parametrized signature. If we use init for generator parameters, then we can't declare types in call that refer to parameters, so we would have to use some other pattern for generators (e.g. functions), but I'm wary of introducing multiple ways to write generators (Our hope is to have the Generator2 pattern be the standard moving forward). Thoughts on an alternative syntax to Peak for declaring types using parameters from init? I know @rdaly525 desired init generator parameters but ran into this exact same issue. |
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.
(Ignore what you saw before, didn't realize it was a prototype.)
I also agree it would be nice if we could unify the interfaces for generators and for peak such that we don't have to have a custom meta class here etc
How I would deal with the partial function / double declartion of IO. Would be to have the IO inferred from the class Foo(sequential):
def __call__(self, x : T, y : S) -> R:
...
# becomes
class Foo(circuit):
io = ('x', In(T), 'y', In(S), 'out', Out(R))
# I don't know what the correct syntax is now but my intention should be clear
def __call__(self, x : T, y : S) -> R:
... I definitely don't want to declare the We should use https://docs.python.org/3/library/inspect.html#inspect.Signature.bind for building the params. |
Also preferably it would not rely on a metaclass. I would like to be able to use it in peak but if the metaclass adds a bunch of magma specific stuff it makes it much harder to use. |
Just to be clear, I was actually advocating passing in parameters that are semantically equivalent to CoreIR's ModuleArgs. These are arguments that specifically do not affect the type of the circuit and therefore I consider I different semantically from generator parameters. Things like register init values or constant values. But importantly the typing of these parameters (how many bits are in the register init value) could need to be derived from passed in generator arguments. |
I think the main issue with this proposal is it fundamentally mixes the concept of a generator with the concept of a circuit. I would expect something that does something along the lines of:
Obviously the syntax/structure/naming should be better to make it easier for users, but keeping a real distinction between the generator stage and the circuit stage seems important. For example, call annotations are not known until you pass in generator parameters and therefore it cannot be a method on the generator class and does not really make senes to be a method on the generator class. There should also be the ability to have a materialize() function that takes in generator parameters and exactly produces the old circuit.sequential class. Obviously I have a CoreIR-centric bias here, but just my two cents |
I think if we separate the notion of a circuit and generator to be different classes, we have a problem with unifying sequential and generators, because sequential requires a class definition (to separate init from call), so either the user has to define a metaclass (generator for sequential class) or we have to use a different pattern for generating sequential, e.g. function factory generates class. I don't think having the user specify a meta class is really in the cards, so either we need to mix the two syntaxes in the same class, or we have to revert to using the function factory generates sequential, which diverges from the m.circuit/m.generator pattern. |
Another option would be have to |
perhaps this also resolves the issue with generic |
I think this is the way to go. In some sense I think of a magma generator class as just syntax convenience for writing:
|
Using |
For Caleb's use case where he doesn't want to declare io inside |
I'll see how this looks sketching it out |
@rdaly525 yes, that's the intention of the syntax, it's also meant to standardize things like parameter handling and provide common re-useable infrastructure by leveraging class patterns. but at it's core it's really just a framework for doing the old function style generator syntax with more structure. |
Why get rid of the old sequential (which represents a circuit) then? Why not just build this generator_sequential as a different abstraction opposed to one that replaces it? You can still advocate users to use this new abstraction while allowing peak to just build upon the old one. |
That would solve caleb' issue about the init and call now having different semantics. init and call of sequential_generator is just a new magma-only thing that has different semantics and is unrelated to the init and call of circuit.sequential |
Hmm that's an option, I'll see how that works out. |
@cdonovick I've updated the implementation to greatly simplify. It doesn't introduce any semantic changes to sequential, instead it uses annotations to avoid exec, so the only exec is in the ssa transformation of call. It also updates call to use the ast_tools ssa, I will work on combinational in a separate PR, but I don't think that will affect you. Can you check it out and maybe test it on some simpler circuits? I'll have to get it up to feature completeness with the existing test suite, but perhaps there are only a subset of features you need to get started working with it. The overview of the new algorithm:
|
@leonardt current code structure looks great! Exactly what I had in mind. The SSA pass will be probably need to be updated to better handle calls, I can take that on. |
I hacked in an implementation of the register assign syntax using a monkey patched setattr, there's probably a better way to implement this using a descriptor, but we need some notion of a magma register primitive where we would define this. Right now the register is an ad-hoc user defined circuit (in mantle) so in general it's hard for us to detect whether the attribute is a register versus some other circuit. I think for now this should work with existing code (modulo minor bugs), but I will look into moving the register into magma as a primitive so we can use some more principled logic in handling them |
Hmm, I think something like |
It also makes it more clear that m.combinational is doing some rewrite magic |
sequential2 with explicit |
@cdonovick hmm, I was looking at this more. We can't quite just drop combinational2 into a pass since it does more than rewrite the tree (it wraps the result of the rewrite in a circuit object to implement the magma interface). So, we would need some way to have an "end_rewrite hook" (e.g. define a method that is invoked after all the rewrites are done). Not sure if there's a reasonable pattern for extending the apply_passes interface to this? Basically we need to: (1) rewrite the AST (standard apply passes pattern), (2) wrap the result of the rewrites in an object. My hope is to have the user only write one decorator (the above could be easily handled by a second decorator that consumed the result of the passes). In the current implementation, magma strips the combinational decorator, runs the passes, then wraps the result of the passes. Perhaps this isn't such an unreasonable design? The main issue is just the stripping of the combinational decorator, which will be hard in general but should work fine in the basic case that most users will be doing (e.g. |
@leonardt Would the following not work? class comb2(apply_ast_passes):
def __init__(self):
super().__init__(passes=passes_comb2_runs)
def exec(self, *args, **kwargs):
fn = super().exec(*args, **kwargs)
# post_rewrite_stuff |
https://github.com/leonardt/ast_tools/blob/master/ast_tools/passes/util.py#L157-L188 |
We could add another if you need it but I think exec is what you are looking for |
That seems like it could work, I'll try it out |
That worked, depends on leonardt/ast_tools#49 though |
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.
LGTM. My only request would be for a hook to add pre/post comb passes (both in seq and in comp) but we can handle that in a different PR if you want.
What would the interface look like? E.g. we could add an argument to comb/seq so something like
|
That seems good. I didn't have any specific interface interface in mind. |
I'm going to merge this since I think it's been through enough discussion/iteration. Let's try using it and see how it goes and manage issues in follow up PRs |
Here's an initial proposal for the new sequential syntax.
It becomes a sub-class of Generator2, which allows the user to declare self.io and instances inside the init method. This doesn't change anything, so effectively now allows init to be used to generate circuits. call is a special method that is rewritten using ast_tools ssa (to implement combinational semantics) and then is invoked with self (reference to generated circuit, so it can use things like instances of registers) and then the members of self.io that match the signature (we can improve this interface, but for now, the user has to declare them in two places, once in self.io and once in call, but call doesn't need a type since self.io has the type).
I think there's one major interface issue to resolve:
call now has different semantics depending on sequential versus normal circuit. normally it will wire up all inputs and return all outputs, in sequential here, call can specify a partial function from inputs to puts (the rest is defined in init). Ideally I'd like to make them consistent, so our options would be to have call satisfy the general circuit interface (declare all inputs/outputs even if some are unused), or use a different method for sequential (e.g. transition_function or update) to distinguish it from the call interface. The issue with this is it doesn't map directly to peak anymore, which uses the call interface, but perhaps that's not a problem since in peak the call interface should equal the update/transitionfunc interface, so it could just be a renaming