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

Rewrite singleton handling including SingletonInstruction #11014

Merged
merged 5 commits into from
Oct 13, 2023

Conversation

jakelishman
Copy link
Member

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 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.

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 pass label 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:

from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library import XGate

immutable = QuantumCircuit(127)
for q in immutable.qubits:
    for _ in [None]*1_000:
        immutable.append(XGate(), [q], [])

mutable = QuantumCircuit(127)
for q in mutable.qubits:
    for _ in [None]*1_000:
        mutable.x(q)


%timeit XGate()
%timeit XGate(label="x")
%timeit immutable.copy()
%timeit mutable.copy()

The three points of comparison are the 0.25.2 tag, main at this commit's parent (7a550ad) and this PR.

Version X() /µs X(label) /µs copy /ms copy mut /ms
0.25.2 1.46(9) 1.56(1) 505(4) 506(6)
main 7.23(3) 7.91(3) 128(1) 1520(11)
PR 0.116(1) 1.94(1) 115(2) 502(7)

@jakelishman jakelishman added performance Changelog: New Feature Include in the "Added" section of the changelog mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library labels Oct 13, 2023
@jakelishman jakelishman added this to the 0.45.0 milestone Oct 13, 2023
@qiskit-bot
Copy link
Collaborator

One or more of the the following people are requested to review this:

  • @enavarro51
  • @Cryoris
  • @Qiskit/terra-core
  • @ajavadia
  • @mtreinish
  • @nkanazawa1989

@jakelishman
Copy link
Member Author

cc: @wshanks, since you'd found some problems with our initial implementation.

@coveralls
Copy link

coveralls commented Oct 13, 2023

Pull Request Test Coverage Report for Build 6511459959

  • 133 of 139 (95.68%) changed or added relevant lines in 24 files are covered.
  • 11 unchanged lines in 4 files lost coverage.
  • Overall coverage decreased (-0.02%) to 87.032%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/circuit/controlledgate.py 3 4 75.0%
qiskit/transpiler/passes/optimization/template_matching/template_substitution.py 5 6 83.33%
qiskit/circuit/singleton.py 59 63 93.65%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/expr.rs 1 93.76%
crates/qasm2/src/lex.rs 2 91.16%
qiskit/extensions/quantum_initializer/squ.py 2 84.15%
crates/qasm2/src/parse.rs 6 97.13%
Totals Coverage Status
Change from base Build 6508678248: -0.02%
Covered Lines: 74332
Relevant Lines: 85408

💛 - 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.
@jakelishman
Copy link
Member Author

QPY test failures are fixed by the unrelated #11015.

Copy link
Member

@mtreinish mtreinish left a 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.

qiskit/circuit/instruction.py Show resolved Hide resolved
@@ -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"):
Copy link
Member

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.

Copy link
Member Author

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.

qiskit/circuit/singleton.py Outdated Show resolved Hide resolved
qiskit/circuit/singleton.py Show resolved Hide resolved
qiskit/circuit/singleton.py Outdated Show resolved Hide resolved
__slots__ = ()

def c_if(self, classical, val):
return self.to_mutable().c_if(classical, val)
Copy link
Member

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.

Copy link
Member Author

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).

qiskit/circuit/singleton.py Show resolved Hide resolved
qiskit/circuit/singleton.py Show resolved Hide resolved
qiskit/circuit/singleton.py Outdated Show resolved Hide resolved
@wshanks
Copy link
Contributor

wshanks commented Oct 13, 2023

Very cool. I have never seen metaclasses used in practice before 🙂

I am curious about the benchmarks. Was SingletonGate primarily benchmarked for memory when it was proposed? I am surprised the 4x overhead was not noticed before. Maybe it was masked by the 4x speed up in copying?

@jakelishman
Copy link
Member Author

It was primarily benchmarked for memory not speed, but also when you're doing QuantumCircuit.append, there's a bunch of overhead related to checking the circuit invariants, resolving qubit specifiers, etc, which is longer than the construction time of the objects themselves.

Matt did notice that QuantumCircuit.copy performance wasn't as good as we hoped, but there were a few factors that masked us from seeing the full effects:

  • the considered circuits had a mix of singletons and non-singletons, and copy is improved for singletons even on main, but that's offset by the slowdown in copies of the mutable gates
  • idiomatic circuit construction like QuantumCircuit.x doesn't actually create singletons on main or in this PR (but I'm preparing another PR with a bit more machinery that will make that work as expected)
  • we were prepared to accept some cost in circuit construction in order to get the memory savings, and we skewed too lenient because the memory savings are what we expected, and that's a big deal for us.

@jakelishman
Copy link
Member Author

Also, our speed benchmarks in asv really don't seem to be turning up much - we need to devote a fair amount of time to looking at those and working out how we can make them more useful. At the very least, they're very out-of-date and don't even necessarily have idiomatic Terra usage.

Copy link
Member

@mtreinish mtreinish left a 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).

qiskit/circuit/singleton.py Outdated Show resolved Hide resolved
qiskit/circuit/singleton.py Outdated Show resolved Hide resolved
Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Copy link
Member

@mtreinish mtreinish left a 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.

@mtreinish mtreinish added this pull request to the merge queue Oct 13, 2023
Merged via the queue into Qiskit:main with commit bcf5ce4 Oct 13, 2023
14 checks passed
@jakelishman jakelishman deleted the singleton-instruction branch October 13, 2023 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library performance priority: high
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Change SingletonGate to be SingletonInstruction Overhead of SingletonGate.__new__
6 participants