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

[RTL][Python] Add InstanceBuilder for constructing instances. #940

Merged
merged 45 commits into from May 5, 2021

Conversation

mikeurbach
Copy link
Contributor

This adds an InstanceBuilder class to help construct InstanceOps from
Python. The builder accepts a name for the instance and optional
values to assign to the input ports.

The InstanceBuilder supports attribute getters and setters for the
result and input ports, respectively. When all of an InstanceBuilder
input ports are set, either on construction or through a setter, the
instance is created.

A create method is added to RTLModuleOp, which constructs and returns
and InstanceBuilder.

This adds an InstanceBuilder class to help construct InstanceOps from
Python. The builder accepts a name for the instance and optional
values to assign to the input ports.

The InstanceBuilder supports attribute getters and setters for the
result and input ports, respectively. When all of an InstanceBuilder
input ports are set, either on construction or through a setter, the
instance is created.

A create method is added to RTLModuleOp, which constructs and returns
and InstanceBuilder.
@teqdruid
Copy link
Contributor

Looks great! I'll review tomorrow. Thanks!

@teqdruid
Copy link
Contributor

teqdruid commented Apr 21, 2021 via email

@mikeurbach
Copy link
Contributor Author

Yep, so this PR is for InstanceOp, but really this should play nice with all ops. If the ops' normal (eager) constructor is sufficient, which it may be for the comb.add situation, we already have the ability to pass operand Values to the constructor and retrieve result Values with an accessor. If that's not sufficient, and we need this lazy construction functionality, most of what's here isn't specific to InstanceOp, and could be pulled out and mixed in to the other classes that need it.

Is that addressing your point? I can add a test case with a comb.add to illustrate this. As long as we don't get into the cyclic situations, that will work with the current revision.

@teqdruid
Copy link
Contributor

The case I'm concerned about is the InstanceOp hasn't been constructed yet but you need its value to feed into the AddOp. If there are not cycles, the user can re-order the program to instantiate the lazy stuff first but I don't want to add unintuitive restrictions on ordering.

@mikeurbach
Copy link
Contributor Author

Ok, I see, that makes sense. If InstanceOps are constructed lazily, we'd kind of have to construct everything lazily to handle this case.

I'm starting to prefer to the backedge-style builder, where the InstanceOp is created right away, and is result Values are available, even before the operand Values get filled in. That still supports the incremental connections to the operands, but should work nicely with the default, eager builders out of the box.

Now that the API and tests are in, it should be easy to try out that approach and see how it goes. I'll experiment with that, and add tests with e.g. comb.add, as well as move the cycle test case from a failure scenario to a success one.

@teqdruid
Copy link
Contributor

Yeah, after trying this approach I think I prefer that as well. I think it'll compose better.

@teqdruid
Copy link
Contributor

Thanks!

@mikeurbach mikeurbach marked this pull request as draft April 22, 2021 05:01
@mikeurbach
Copy link
Contributor Author

I made some good progress with the backedge approach...

About to push a bunch of WIP that should be split out into separate changes. Notably, to make this work in a neat way, we need something similar to https://reviews.llvm.org/D99927, but for Values instead of Operations. I've prototyped this and will be submitting a patch upstream. If it's blocking, I can push the patch to the CIRCT fork of LLVM.

With all that, we are super close, and building cycles works well. The only issue I'm trying to figure out is how to nicely capture the scenario where a backedge never got updated. Right now that just asserts, but it should be a nice Python error.

@mikeurbach
Copy link
Contributor Author

@teqdruid here's the patch for PyValue <> MlirValue interop: circt/llvm@5fab6d3

Also bumped to ref in this PR, so you should be able to pull down this branch if you want to test.

@mikeurbach
Copy link
Contributor Author

Patch to upstream PyValue interop: https://reviews.llvm.org/D101090

@mikeurbach
Copy link
Contributor Author

mikeurbach commented Apr 26, 2021

Added the MLIR patches to this branch, and will file patches in phab: https://github.com/circt/llvm/commits/python-integration

@mikeurbach mikeurbach marked this pull request as ready for review May 4, 2021 20:50
@mikeurbach mikeurbach requested a review from teqdruid May 4, 2021 20:50
@mikeurbach
Copy link
Contributor Author

@teqdruid I think this is ready to go. It's functional, and provides detailed error messages if you don't supply all the necessary backedges. I personally think we can do more iterations of cleanups and API improvements, but this should be a decent starting place.

The only main change since last time is the removal of instance.a to get port "a" and instance.a = ... to set port "a". Rather than that python magic, I've just added methods for get_port() and set_port()

Copy link
Contributor

@teqdruid teqdruid left a comment

Choose a reason for hiding this comment

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

Looking good! I see you moved to an all python implementation of backedgebuilder -- I think it'll work out better.

I agree on this being ready to go, modulo any small changes you want to make as a result of my comments. It's got a few rough edges (which I've noted in my comments), but those can all be ironed out with further iteration. Perhaps we should keep track of them in a "cleanup instancebuilder" issue.

inst1 = one_output.create(module, "inst1")

# CHECK: rtl.instance "inst2" @one_input(%[[INST1_RESULT]])
inst2 = one_input.create(module, "inst2", {"a": inst1.get_port("a")})
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 going a bit too far in the other direction. By all means we should support this syntax, but let's also make inst1.a do the same thing. In terms of setting values, I personally think connect(inst4.b, inst1.a) is reasonable, but there's no need to settle this now. Maybe inst4.b.set(inst1.a) to be more consistent with the C++ syntax?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me think about it, but I think adding back getattr is a good idea. That does mean that if inst1.a returns a Value, then inst4.b.set would mean we need to put that method on Value. I'm starting to lean towards attribute like getters and a connect free function, but again let's continue to iterate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added back __getattr__ to get either input or output ports, and I think that should play nicely with whatever we do here. There are some considerations either way. I think if we do the connect() approach, it will need to be a method on the parent module, so we have access to its backedge builder. For now I will just use the explicit set_input_port call.

integration_test/Bindings/Python/dialects/rtl_errors.py Outdated Show resolved Hide resolved
lib/Bindings/Python/circt/dialects/_rtl_ops_ext.py Outdated Show resolved Hide resolved
lib/Bindings/Python/circt/dialects/_rtl_ops_ext.py Outdated Show resolved Hide resolved
# Put the value into the instance.
index = self.operand_indices[name]
self.instance.inputs[index] = value
self.parent_module.remove_backedge(self.backedges[index])
Copy link
Contributor

Choose a reason for hiding this comment

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

The parent module owns the backedge set... does this work if the parent module wasn't created via python?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll test that

Copy link
Contributor

Choose a reason for hiding this comment

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

Given that you call check after body_builder, I suspect the best we can hope for is not a crash.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me back up on this one. The backedge builder is only ever invoked from the Python constructor that is building the parent module. I'm having a hard time trying to imagine how to invoke this code without doing it through Python. If you build the parent module some other way, it just wouldn't use a backedge builder as you fill in the body.

@property
def body(self):
return self.regions[0]
self.backedge_builder.check()
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly, you seem to be assuming that the module's body is built entirely inside of the body_builder, but that need not be the case.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe BackedgeBuilder should be used in a with context and check when the context is left. That way you could use it here and it could be used elsewhere. InstanceBuilder could require that it be called in BackedgeBuilder context or one could be passed in. I'm not sure how to do this in pure python.

I'm fine if you leave this as-is and consider this to be a future refinement.

Copy link
Contributor Author

@mikeurbach mikeurbach May 5, 2021

Choose a reason for hiding this comment

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

I think there are a couple requests here. For one, making BackedgeBuilder a context manager so check is called automatically. That is easy.

When you're talking about InstanceBuilder requiring to be within a BackedgeBuilder context, or have one passed in, that starts to get into the somewhat magic ability to do things like with InsertionPoint(): and have everything in that block get an insertion point. That's going to require some special state and bookkeeping.

We already pass the parent module to InstanceBuilder, so for now I'll just add an assertion that there is an attached BackedgeBuilder in the constructor of InstanceBuilder.

Longer term, I have some ideas about how we can improve this API in general using context managers that implicitly make things like the parent module and backedge builder available, but I think I'll do the simple thing right now and then we can see about enhancements later.

lib/Bindings/Python/circt/support.py Outdated Show resolved Hide resolved
lib/Bindings/Python/circt/support.py Outdated Show resolved Hide resolved
lib/Bindings/Python/circt/support.py Outdated Show resolved Hide resolved
@mikeurbach
Copy link
Contributor Author

Still some open discussion points, but I think the low hanging fruit is addressed. I'm going to merge this in the current state and we can open smaller issues for follow-ups or enhanements.

@mikeurbach mikeurbach merged commit 6febae5 into main May 5, 2021
@mikeurbach mikeurbach deleted the python-rtl-instantiate branch May 5, 2021 19:12
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.

None yet

2 participants