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

Implement some standard gates in rust #12507

Closed
wants to merge 46 commits into from

Conversation

jlapeyre
Copy link
Contributor

@jlapeyre jlapeyre commented Jun 4, 2024

This PR ...

  • Implements RGate in Rust
  • Does some refactoring and rewriting to reduce boilerplate and improve ergonomics. These are applied throughout all crates.
    • ZERO, ONE, IM, M_IM, M_ONE, etc.
    • c64! and method c64 for constructing Complex64. Perhaps we only need the former.
    • GateArray2Q and similar type aliases for very common types ([[Complex64; 4]; 4];)

This PR builds on:

This PR is blocked by #12459.

Here is a diff with far fewer commits, taking #12459 as the base: mtreinish/qiskit-core@gates-in-rust...jlapeyre:qiskit-core:new-some-gates-in-rust

It really looks like the online diff between this PR branch and #12459 is broken. The diff does not agree with a local diff. Seems to have been a GH glitch that has resolved.

mtreinish and others added 30 commits May 25, 2024 09:04
This commit adds a native representation of Gates, Instruction, and
Operations to rust's circuit module. At a high level this works by
either wrapping the Python object in a rust wrapper struct that tracks
metadata about the operations (name, num_qubits, etc) and then for other
details it calls back to Python to get dynamic details like the
definition, matrix, etc. For standard library gates like Swap, CX, H,
etc this replaces the on-circuit representation with a new rust enum
StandardGate. The enum representation is much more efficient and has a
minimal memory footprint (just the enum variant and then any parameters
or other mutable state stored in the circuit instruction). All the gate
properties such as the matrix, definiton, name, etc are statically
defined in rust code based on the enum variant (which represents the
gate).

The use of an enum to represent standard gates does mean a change in
what we store on a CircuitInstruction. To represent a standard gate
fully we need to store the mutable properties of the existing Gate class
on the circuit instruction as the gate by itself doesn't contain this
detail. That means, the parameters, label, unit, duration, and condition
are added to the rust side of circuit instrucion. However no Python side
access methods are added for these as they're internal only to the Rust
code. In Qiskit 2.0 to simplify this storage we'll be able to drop, unit,
duration, and condition from the api leaving only label and parameters.
But for right now we're tracking all of the fields.

To facilitate working with circuits and gates full from rust the
setting the `operation` attribute of a `CircuitInstruction` object now
transltates the python object to an internal rust representation.
For standard gates this translates it to the enum form described earlier,
and for other circuit operations 3 new Rust structs: PyGate,
PyInstruction, and PyOperation are used to wrap the underlying Python
object in a Rust api. These structs cache some commonly accessed static
properties of the operation, such as the name, number of qubits, etc.
However for dynamic pieces, such as the definition or matrix, callback
to python to get a rust representation for those.

Similarly whenever the `operation` attribute is accessed from Python
it converts it back to the normal Python object representation. For
standard gates this involves creating a new instance of a Python object
based on it's internal rust representation. For the wrapper structs a
reference to the wrapped PyObject is returned.

To manage the 4 variants of operation (`StandardGate`, `PyGate`,
`PyInstruction`, and `PyOperation`) a new Rust trait `Operation` is
created that defines a standard interface for getting the properties
of a given circuit operation. This common interface is implemented for
the 4 variants as well as the `OperationType` enum which wraps all 4
(and is used as the type for `CircuitInstruction.operation` in the
rust code.

As everything in the `QuantumCircuit` data model is quite coupled moving
the source of truth for the operations to exist in Rust means that more
of the underlying `QuantumCircuit`'s responsibility has to move to Rust
as well. Primarily this involves the `ParameterTable` which was an
internal class for tracking which instructions in the circuit have a
`ParameterExpression` parameter so that when we go to bind parameters we
can lookup which operations need to be updated with the bind value.
Since the representation of those instructions now lives in Rust and
Python only recieves a ephemeral copy of the instructions the
ParameterTable had to be reimplemented in Rust to track the
instructions. This new parameter table maps the Parameter's uuid (as a
u128) as a unique identifier for each parameter and maps this to a
positional index in the circuit data to the underlying instruction using
that parameter. This is a bit different from the Python parameter table
which was mapping a parameter object to the id of the operation object
using that parmaeter. This also leads to a difference in the binding
mechanics as the parameter assignment was done by reference in the old
model, but now we need to update the entire instruction more explicitly
in rust. Additionally, because the global phase of a circuit can be
parameterized the ownership of global phase is moved from Python into
Rust in this commit as well.

After this commit the only properties of a circuit that are not defined
in Rust for the source of truth are the bits (and vars) of the circuit,
and when creating circuits from rust this is what causes a Python
interaction to still be required.

This commit does not translate the full standard library of gates as
that would make the pull request huge, instead this adds the basic
infrastructure for having a more efficient standard gate representation
on circuits. There will be follow up pull requests to add the missing
gates and round out support in rust.

The goal of this pull request is primarily to add the infrastructure for
representing the full circuit model (and dag model in the future) in
rust. By itself this is not expected to improve runtime performance (if
anything it will probably hurt performance because of extra type
conversions) but it is intended to enable writing native circuit
manipulations in Rust, including transpiler passes without needing
involvement from Python. Longer term this should greatly improve the
runtime performance and reduce the memory overhead of Qiskit. But,
this is just an early step towards that goal, and is more about
unlocking the future capability. The next steps after this commit are
to finish migrating the standard gate library and also update the
`QuantumCircuit` methods to better leverage the more complete rust
representation (which should help offset the performance penalty
introduced by this).

Fixes: Qiskit#12205
This commit adds a custom implementation of the FromPyObject trait for
the Param enum. Previously, the Param trait derived it's impl of the
trait, but this logic wasn't perfect. In cases whern a
ParameterExpression was effectively a constant (such as `0 * x`) the
trait's attempt to coerce to a float first would result in those
ParameterExpressions being dropped from the circuit at insertion time.
This was a change in behavior from before having gates in Rust as the
parameters would disappear from the circuit at insertion time instead of
at bind time. This commit fixes this by having a custom impl for
FromPyObject that first tries to figure out if the parameter is a
ParameterExpression (or a QuantumCircuit) by using a Python isinstance()
check, then tries to extract it as a float, and finally stores a
non-parameter object; which is a new variant in the Param enum. This
new variant also lets us simplify the logic around adding gates to the
parameter table as we're able to know ahead of time which gate
parameters are `ParameterExpression`s and which are other objects (and
don't need to be tracked in the parameter table.

Additionally this commit tweaks two tests, the first is
test.python.circuit.library.test_nlocal.TestNLocal.test_parameters_setter
which was adjusted in the previous commit to workaround the bug fixed
by this commit. The second is test.python.circuit.test_parameters which
was testing that a bound ParameterExpression with a value of 0 defaults
to an int which was a side effect of passing an int input to symengine
for the bind value and not part of the api and didn't need to be
checked. This assertion was removed from the test because the rust
representation is only storing f64 values for the numeric parameters
and it is never an int after binding from the Python perspective it
isn't any different to have float(0) and int(0) unless you explicit
isinstance check like the test previously was.
This commit fixes the handling of standard gates in Qiskit when the user
specifies excluding the use of the stdgates.inc file from the exported
qasm. Previously the object id of the standard gates were used to
maintain a lookup table of the global definitions for all the standard
gates explicitly in the file. However, the rust refactor means that
every time the exporter accesses `circuit.data[x].operation` a new
instance is returned. This means that on subsequent lookups for the
definition the gate definitions are never found. To correct this issue
this commit adds to the lookup table a fallback of the gate name +
parameters to do the lookup for. This should be unique for any standard
gate and not interfere with the previous logic that's still in place and
functional for other custom gate definitions.

While this fixes the logic in the exporter the test is still failing
because the test is asserting the object ids are the same in the qasm3
file, which isn't the case anymore. The test will be updated in a
subsequent commit to validate the qasm3 file is correct without using
a hardcoded object id.
When ALAPScheduleAnalysis and ASAPScheduleAnalysis were setting the
duration of a gate they were doing `node.op.duration = duration` this
wasn't always working because if `node.op` was a standard gate it
returned a new Python object created from the underlying rust
representation. This commit fixes the passes so that they modify the
duration and then explicit set the operation to update it's rust
representation.
While the logic for the qasm3 exporter was fixed
in commit a6e69ba to handle the edge
case of a user specifying that the qasm exporter does not use the
stdgates.inc include file in the output, but also has qiskit's standard
gates in their circuit being exported. The one unit test to provide
coverage for that scenario was not passing because when an id was used
for the gate definitions in the qasm3 file it was being referenced
against a temporary created by accessing a standard gate from the
circuit and the ids weren't the same so the reference string didn't
match what the exporter generated. This commit fixes this by changing
the test to not do an exact string comparison, but instead a line by
line comparison that either does exact equality check or a regex search
for the expected line and the ids are checked as being any 15 character
integer.
This commit updates the QuantumCircuit gate methods which add a given
gate to the circuit to bypass the python gate object creation and
directly insert a rust representation of the gate. This avoids a
conversion in the rust side of the code. While in practice this is just
the Python side object creation and a getattr for the rust code to
determine it's a standard gate that we're skipping. This may add up over
time if there are a lot of gates being created by the method.

To accomplish this the rust code handling the mapping of rust
StandardGate variants to the Python classes that represent those gates
needed to be updated as well. By bypassing the python object creation
we need a fallback to populate the gate class for when a user access the
operation object from Python. Previously this mapping was only being
populated at insertion time and if we never insert the python object
(for a circuit created only via the methods) then we need a way to find
what the gate class is. A static lookup table of import paths and class names
are added to `qiskit_circuit::imports` module to faciliate this and
helper functions are added to facilitate interacting with the class
objects that represent each gate.
This commit adds a new rust crate feature flag for the qiskit-circuits
and qiskit-pyext that enables caching the output from
CircuitInstruction.operation to python space. Previously, for memory
efficiency we were reconstructing the python object on demand for every
access. This was to avoid carrying around an extra pointer and keeping
the ephemeral python object around longer term if it's only needed once.
But right now nothing is directly using the rust representation yet and
everything is accessing via the python interface, so recreating gate
objects on the fly has a huge performance penalty. To avoid that this
adds caching by default as a temporary solution to avoid this until we
have more usage of the rust representation of gates.

There is an inherent tension between an optimal rust representation
and something that is performant for Python access and there isn't a
clear cut answer on which one is better to optimize for. A build time
feature lets the user pick, if what we settle on for the default doesn't
agree with their priorities or use case. Personally I'd like to see us
disable the caching longer term (hopefully before releasing this
functionality), but that's dependent on a sufficent level of usage from
rust superseding the current Python space usage in the core of Qiskit.
This commit adds a native rust implementation to rust for the
num_nonlocal_gates method on QuantumCircuit. Now that we have a rust
representation of gates it is potentially faster to do the count because
the iteration and filtering is done rust side.
This commit fixes some performance issues with the addition of standard
gates to a circuit. To workaround potential reference cycles in Python
when calling rust we need to check the parameters of the operation. This
was causing our fast path for standard gates to access the `operation`
attribute to get the parameters. This causes the gate to be eagerly
constructed on the getter. However, the reference cycle case can only
happen in situations without a standard gate, and the fast path for
adding standard gates directly won't need to run this so a skip is added
if we're adding a standard gate.
In the previous commit a side effect of the accidental eager operation
creation was that the parameter input for gates were being validated by
that. By fixing that in the previous commit the validation of input
parameters on the circuit methods was broken. This commit fixes that
oversight and adds back the validation.
This commit fixes a performance regression in the
`QuantumCircuit.copy()` method which was previously using Python to copy
the operations which had extra overhead to go from rust to python and
vice versa. This moves that logic to exist in rust and improve the copy
performance.
This commit moves the deepcopy handling to occur solely in Rust.
Previously each instruction would be directly deepcopied by iterating
over the circuit data. However, we can do this rust side now and doing
this is more efficient because while we need to rely on Python to run a
deepcopy we can skip it for the Rust standard gates and rely on Rust to
copy those gates.
This commit fixes a performance regression in the compose() method. This
was caused by the checking for classical conditions in the method
requiring eagerly converting all standard gates to a Python object. This
changes the logic to do this only if we know we have a condition (which
we can determine Python side now).
This commit fixes several docs typos that were caught during code review.

Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com>
@jlapeyre jlapeyre force-pushed the new-some-gates-in-rust branch from b373e4d to 63a559b Compare June 5, 2024 19:54
@coveralls
Copy link

coveralls commented Jun 5, 2024

Pull Request Test Coverage Report for Build 9390843929

Details

  • 1829 of 2182 (83.82%) changed or added relevant lines in 36 files are covered.
  • 11 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-0.2%) to 89.392%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/circuit/quantumcircuit.py 123 125 98.4%
crates/circuit/src/parameter_table.rs 90 100 90.0%
crates/circuit/src/circuit_instruction.rs 496 513 96.69%
crates/circuit/src/circuit_data.rs 509 590 86.27%
crates/circuit/src/operations.rs 408 651 62.67%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/expr.rs 1 94.02%
crates/qasm2/src/lex.rs 4 92.11%
crates/qasm2/src/parse.rs 6 97.61%
Totals Coverage Status
Change from base Build 9372719342: -0.2%
Covered Lines: 63886
Relevant Lines: 71467

💛 - Coveralls

@jlapeyre jlapeyre force-pushed the new-some-gates-in-rust branch from 05c08ef to e7d07f7 Compare June 5, 2024 22:54
@Qiskit Qiskit deleted a comment from coveralls Jun 5, 2024
@Qiskit Qiskit deleted a comment from coveralls Jun 5, 2024
@Qiskit Qiskit deleted a comment from coveralls Jun 5, 2024
@Qiskit Qiskit deleted a comment from coveralls Jun 5, 2024
@coveralls
Copy link

coveralls commented Jun 6, 2024

Pull Request Test Coverage Report for Build 9394549547

Details

  • 1930 of 2283 (84.54%) changed or added relevant lines in 45 files are covered.
  • 52 unchanged lines in 5 files lost coverage.
  • Overall coverage decreased (-0.2%) to 89.376%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/circuit/quantumcircuit.py 133 135 98.52%
crates/circuit/src/parameter_table.rs 90 100 90.0%
crates/circuit/src/circuit_instruction.rs 496 513 96.69%
crates/circuit/src/circuit_data.rs 528 609 86.7%
crates/circuit/src/operations.rs 408 651 62.67%
Files with Coverage Reduction New Missed Lines %
crates/accelerate/src/sparse_pauli_op.rs 1 93.27%
crates/qasm2/src/lex.rs 2 92.62%
qiskit/circuit/parameterexpression.py 5 96.51%
crates/qasm2/src/parse.rs 12 96.69%
crates/circuit/src/circuit_data.rs 32 89.21%
Totals Coverage Status
Change from base Build 9372719342: -0.2%
Covered Lines: 63851
Relevant Lines: 71441

💛 - Coveralls

@coveralls
Copy link

coveralls commented Jun 6, 2024

Pull Request Test Coverage Report for Build 9399579283

Details

  • 1930 of 2283 (84.54%) changed or added relevant lines in 45 files are covered.
  • 42 unchanged lines in 5 files lost coverage.
  • Overall coverage decreased (-0.2%) to 89.401%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/circuit/quantumcircuit.py 133 135 98.52%
crates/circuit/src/parameter_table.rs 90 100 90.0%
crates/circuit/src/circuit_instruction.rs 496 513 96.69%
crates/circuit/src/circuit_data.rs 528 609 86.7%
crates/circuit/src/operations.rs 408 651 62.67%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/expr.rs 1 94.02%
crates/accelerate/src/sparse_pauli_op.rs 1 93.27%
crates/qasm2/src/lex.rs 3 92.88%
qiskit/circuit/parameterexpression.py 5 96.51%
crates/circuit/src/circuit_data.rs 32 89.21%
Totals Coverage Status
Change from base Build 9372719342: -0.2%
Covered Lines: 63869
Relevant Lines: 71441

💛 - Coveralls

@coveralls
Copy link

coveralls commented Jun 6, 2024

Pull Request Test Coverage Report for Build 9405792818

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 1808 of 2151 (84.05%) changed or added relevant lines in 44 files are covered.
  • 17 unchanged lines in 5 files lost coverage.
  • Overall coverage decreased (-0.2%) to 89.39%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/circuit/quantumcircuit.py 123 125 98.4%
crates/circuit/src/imports.rs 48 55 87.27%
crates/circuit/src/parameter_table.rs 90 100 90.0%
crates/circuit/src/circuit_instruction.rs 450 467 96.36%
crates/circuit/src/circuit_data.rs 485 548 88.5%
crates/circuit/src/operations.rs 376 620 60.65%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/expr.rs 1 94.02%
crates/accelerate/src/sparse_pauli_op.rs 1 93.27%
crates/qasm2/src/lex.rs 3 92.62%
qiskit/qasm2/parse.py 6 96.7%
crates/qasm2/src/parse.rs 6 97.61%
Totals Coverage Status
Change from base Build 9372719342: -0.2%
Covered Lines: 63778
Relevant Lines: 71348

💛 - Coveralls

This just switches Complex64::new(re, im) to be c64(re, im) to reduce
the amount of typing. c64 needs to be defined inplace so it can be a
const fn.
@coveralls
Copy link

coveralls commented Jun 6, 2024

Pull Request Test Coverage Report for Build 9407314161

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 1783 of 2116 (84.26%) changed or added relevant lines in 44 files are covered.
  • 20 unchanged lines in 5 files lost coverage.
  • Overall coverage decreased (-0.2%) to 89.396%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/circuit/quantumcircuit.py 124 126 98.41%
crates/circuit/src/imports.rs 48 55 87.27%
crates/circuit/src/parameter_table.rs 90 100 90.0%
crates/circuit/src/circuit_instruction.rs 450 467 96.36%
crates/circuit/src/circuit_data.rs 485 544 89.15%
crates/circuit/src/operations.rs 350 588 59.52%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/expr.rs 1 94.02%
crates/accelerate/src/sparse_pauli_op.rs 1 93.27%
qiskit/qasm2/parse.py 6 96.7%
crates/qasm2/src/lex.rs 6 92.11%
crates/qasm2/src/parse.rs 6 97.61%
Totals Coverage Status
Change from base Build 9372719342: -0.2%
Covered Lines: 63749
Relevant Lines: 71311

💛 - Coveralls

@Qiskit Qiskit deleted a comment from coveralls Jun 6, 2024
@Qiskit Qiskit deleted a comment from coveralls Jun 6, 2024
jlapeyre added 2 commits June 6, 2024 17:28
…t#28)

* Simplify initialization of array of elements that are not Copy

* Only generate array when necessary
* Implements `RGate` in Rust
* Does some refactoring and rewriting to reduce boilerplate and improve ergonomics. These are applied throughout all crates.
    * `ZERO`, `ONE`, `IM`, `M_IM`, `M_ONE`, etc.
    * `c64!` and method `c64` for constructing `Complex64`. Perhaps we only need the former.
    * `GateArray2Q` and similar type aliases for very common types (`[[Complex64; 4]; 4];`)
@jlapeyre jlapeyre force-pushed the new-some-gates-in-rust branch from dc1853d to d7f3c66 Compare June 7, 2024 22:26
@1ucian0 1ucian0 added the Rust This PR or issue is related to Rust code in the repository label Jun 11, 2024
@jlapeyre
Copy link
Contributor Author

I am closing this in favor of splitting this PR into two parts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Rust This PR or issue is related to Rust code in the repository
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants