-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Rewrite singleton handling including SingletonInstruction
#11014
Conversation
One or more of the the following people are requested to review this:
|
cc: @wshanks, since you'd found some problems with our initial implementation. |
Pull Request Test Coverage Report for Build 6511459959
💛 - Coveralls |
This is a large rewrite of the singleton gate handling, building off the work done across the library to make the initial implementation work. There are two main purposes to this commit: * Make `SingletonInstruction` available in addition to `SingletonGate`, and have these be easy to maintain in conjunction, not need to duplicate overrides between them, and not require inheritors to do any nasty multiple inheritance or the like. * Fix regressions in the construction time of `SingletonGate` instances. In the end, this turned into a fairly complete rewrite that completely switches out the method of defining the classes; it transpires that the previous way of having the "immutable overrides" on _all_ instances of the classes was the main source of slowdown, with `__setattr__` being a large problem. The previous method was far easier from an implementation perspective, but the runtime costs ended up being too high. This new form takes a vastly different strategy, explained in detail in `qiskit/circuit/singleton.py`. The gist is that we instead make the singleton instance a _subclass_ of the class object we're creating, and only it contains the overrides to make itself immutable. We get around the instantiation problem (`__init__` isn't special and doesn't skip the forbidden `__setattr__`) by constructing an instance of the _base_ class, and then dynamically switching in the overrides afterwards. Since the overrides and the dynamic singleton type of the instance are designed to be co-operative (including in `__slots__` layout), this can be done safely.
5a7c1d2
to
4cf52f2
Compare
QPY test failures are fixed by the unrelated #11015. |
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.
Overall this looks great to me. It's a very clever solution (although I'm mildly concerned it's too clever and it'll be confusing to maintain in a couple years time, but the very detailed comments you left inline should help greatly with that) to solve the problems we had with the original SingletonGate
implementation and it should make it simpler for us to expand the singleton pattern for other circuit object types in the future like: #10898 but also any other follow-ons. I just had a few comments inline mostly around documentation and comments.
@@ -48,14 +48,9 @@ class DCXGate(SingletonGate): | |||
\end{pmatrix} | |||
""" | |||
|
|||
def __init__(self, label=None, duration=None, unit=None, _condition=None): | |||
def __init__(self, label=None, *, duration=None, unit="dt"): |
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 agree making these keyword only makes sense, we should have done this in #10314. It'd be good to leave a comment on #11013 for @hunterkemeny to adopt this signature in that PR too.
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.
Oh, this actually was a bit of a mistake that I changed what you originally had - I was just writing it on autopilot, mechanically and manually undoing the mistake I made where I'd removed all the label
and duration
arguments. But yeah, since we've got the opportunity, let's just do it.
__slots__ = () | ||
|
||
def c_if(self, classical, val): | ||
return self.to_mutable().c_if(classical, val) |
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.
Oh, this is much neater and would have worked fine for the old class structure if we did out = self.to_mutable();out.condition = (...)
. I really should have done a pass to clean up all the overrides after adding the to_mutable()
method.
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.
Yeah - I think there's an instance in your ControlledGate
PR where it could do a similar thing as well (or maybe it's a case that does obj = obj.to_mutable(); obj.condition = ...
instead of obj = obj.c_if(...)
- I forgot).
Very cool. I have never seen metaclasses used in practice before 🙂 I am curious about the benchmarks. Was |
It was primarily benchmarked for memory not speed, but also when you're doing Matt did notice that
|
Also, our speed benchmarks in |
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.
This LGTM, thanks for the updates. I think this is ready to merge I just had two tiny inline nits with suggestions on the docs (but I worry one will fail lint, so we'll see).
Co-authored-by: Matthew Treinish <[email protected]>
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, thanks for the quick update. I think this is ready to go now.
Summary
This is a large rewrite of the singleton gate handling, building off the work done across the library to make the initial implementation work.
There are two main purposes to this commit:
Make
SingletonInstruction
available in addition toSingletonGate
, and have these be easy to maintain in conjunction, not need to duplicate overrides between them, and not require inheritors to do any nasty multiple inheritance or the like.Fix regressions in the construction time of
SingletonGate
instances.In the end, this turned into a fairly complete rewrite that completely switches out the method of defining the classes; it transpires that the previous way of having the "immutable overrides" on all instances of the classes was the main source of slowdown, with
__setattr__
being a large problem. The previous method was far easier from an implementation perspective, but the runtime costs ended up being too high.This new form takes a vastly different strategy, explained in detail in
qiskit/circuit/singleton.py
. The gist is that we instead make the singleton instance a subclass of the class object we're creating, and only it contains the overrides to make itself immutable. We get around the instantiation problem (__init__
isn't special and doesn't skip the forbidden__setattr__
) by constructing an instance of the base class, and then dynamically switching in the overrides afterwards. Since the overrides and the dynamic singleton type of the instance are designed to be co-operative (including in__slots__
layout), this can be done safely.Details and comments
Fix #10867
Close #11004
Close #10986 (obsolete after this PR - the overhead is so small it's not worth the extra logic).
Both this and the previous state of singleton gates are currently having less effect on idiomatically constructed circuits than is ideal, because methods such as
QuantumCircuit.x
always passlabel
as a kwarg, which suppresses use of the singletons. This PR does not yet attempt to change that; that would be a follow-up. In principle, it requires a system that_SingletonMeta.__call__
can use to determine if arguments are their defaults or not - this could be done explicitly, or potentially by introspection of the base__init__
method during the class creation.Some microbenchmarks to demonstrate the performance characteristics (since our asv suite doesn't really seem to be giving useful results at the moment). The four rows are defined in this script in order:
The three points of comparison are the 0.25.2 tag,
main
at this commit's parent (7a550ad) and this PR.