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

Prepare PauliEvolutionGate for Rustiq & port it to Rust #13295

Merged
merged 32 commits into from
Nov 4, 2024

Conversation

Cryoris
Copy link
Contributor

@Cryoris Cryoris commented Oct 8, 2024

Summary

Port the PauliEvolutionGate synthesis to Rust, plus, expose the Pauli network and allow other plugins to synthesize the gate. Also adds the plugin structure for the gate for #12789.

Details and comments

The larger the Pauli network to synthesis, the better the speedup from the port to Rust. Here I measured a Heisenberg Hamiltonian (XX+YY+ZZ on interacting qubits, plus 1-qubit Z on each qubit) on a square lattice for different settings:

1 timestep for first order Trotter: speedup @ 100 qubits is 2.8
N=25: 7.0
main: 0.007 +- 0.006
this: 0.001 +- 0.000

N=100: 2.8
main: 0.042 +- 0.000
this: 0.015 +- 0.000

N=255: 2.8
main: 0.198 +- 0.040
this: 0.071 +- 0.003

N=400: 2.3
main: 0.607 +- 0.001
this: 0.260 +- 0.000
10 timestep for 4th order Trotter: speedup @ 100 qubits is 9.4
N=25: 6.7
main: 0.040 +- 0.003
this: 0.006 +- 0.000

N=100: 9.4
main: 0.330 +- 0.037
this: 0.035 +- 0.001

N=255: 10.8
main: 1.291 +- 0.066
this: 0.119 +- 0.007

N=400: 11.5
main: 4.052 +- 0.051
this: 0.353 +- 0.009
1 timestep for first order Trotter but with wrap=True: speedup @ 100 qubits is 3.6
N=25: 3.4
main: 0.034 +- 0.012
this: 0.010 +- 0.004

N=100: 3.6
main: 0.187 +- 0.017
this: 0.052 +- 0.010

N=255: 3.1
main: 0.551 +- 0.048
this: 0.178 +- 0.019

N=400: 3.0
main: 1.367 +- 0.068
this: 0.458 +- 0.027

Side effects of this PR:

  • add SparsePauliOp.to_sparse_list to construct the sparse list format (i.e. op == SparsePauliOp.from_sparse_list(op.to_sparse_list()))
  • refactor LieTrotter to just be an alias of SuzukiTrotter and reduce code duplication
  • fix the docstrings to use actually non-commuting Paulis (XX and ZZ commute and would have no expansion error)

@coveralls
Copy link

coveralls commented Oct 8, 2024

Pull Request Test Coverage Report for Build 11667384085

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

  • 313 of 332 (94.28%) changed or added relevant lines in 12 files are covered.
  • 587 unchanged lines in 28 files lost coverage.
  • Overall coverage increased (+0.03%) to 88.756%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/circuit/src/operations.rs 1 2 50.0%
qiskit/synthesis/evolution/suzuki_trotter.py 18 20 90.0%
qiskit/synthesis/evolution/product_formula.py 41 45 91.11%
qiskit/synthesis/evolution/qdrift.py 14 18 77.78%
crates/accelerate/src/circuit_library/pauli_evolution.rs 204 212 96.23%
Files with Coverage Reduction New Missed Lines %
qiskit/transpiler/target.py 1 93.84%
qiskit/circuit/library/boolean_logic/quantum_or.py 1 98.08%
crates/accelerate/src/basis/basis_translator/basis_search.rs 1 99.31%
qiskit/circuit/library/boolean_logic/quantum_and.py 1 97.96%
qiskit/circuit/library/boolean_logic/inner_product.py 1 96.0%
qiskit/circuit/store.py 1 97.06%
qiskit/qasm2/export.py 1 98.48%
crates/accelerate/src/split_2q_unitaries.rs 2 96.43%
qiskit/transpiler/passmanager_config.py 3 96.1%
crates/accelerate/src/basis/basis_translator/compose_transforms.rs 3 97.48%
Totals Coverage Status
Change from base Build 11601286937: 0.03%
Covered Lines: 76579
Relevant Lines: 86280

💛 - Coveralls

--> expand should return float | ParameterExpression
@Cryoris Cryoris changed the title Prepare PauliEvolutionGate plugin structure & rustiq integration Prepare PauliEvolutionGate for Rustiq & port it to Rust Oct 15, 2024
@Cryoris Cryoris marked this pull request as ready for review October 15, 2024 11:49
@Cryoris Cryoris requested review from ShellyGarion and a team as code owners October 15, 2024 11:49
@qiskit-bot
Copy link
Collaborator

One or more of the following people are relevant to this code:

  • @Cryoris
  • @Qiskit/terra-core
  • @ajavadia

@raynelfss raynelfss linked an issue Oct 15, 2024 that may be closed by this pull request
@raynelfss raynelfss added this to the 1.3.0 milestone Oct 15, 2024
Copy link
Contributor

@raynelfss raynelfss left a comment

Choose a reason for hiding this comment

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

I quickly reviewed the rust code. Great job. There are some questions I have that I'm fully open to discussing.

time: Param,
phase_gate: bool,
do_fountain: bool,
) -> Box<dyn Iterator<Item = StandardInstruction> + 'a> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we using a Box here? From what I understand, using a Box pointer can save memory by storing things in the heap rather than the stack. But is it that much more expensive than just returning a regular iterator? Standard gates aren't supposed to be too heavy either.

This isn't critical as performance wouldn't be impacted from what I can tell since StandardGate instances are very light. When it comes to Param instances, it might be a bit trickier. But it is something to think about.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is because this function returns different types of Iterator depending on the input (e.g. Chain<Map<...>> vs Empty). The dynamic type was the only way I got to work, but I'm happy to change it if there's a better way 🙂

Copy link
Contributor

Choose a reason for hiding this comment

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

From a recent talk I had with @alexanderivrii it seems that you want to call the .rev method to be able to reverse the iterator. If so, perhaps you could change the return types to be DoubleEndedIterator instances instead of a Box<dyn Iterator<_>>.

So you could change some of the return types from:

pub fn foo() -> Box<dyn Iterator<Item = StandardInstruction> + 'a> {

to:

pub fn foo() -> impl DoubleEndedIterator<Item = StandardInstruction> + 'a {

This would allow you to use any iterator type as long as it can be reversed, which seems to be the case for many of the iterators used here.

crates/accelerate/src/circuit_library/pauli_evolution.rs Outdated Show resolved Hide resolved
Comment on lines 139 to 140
.clone()
.into_iter()
Copy link
Contributor

Choose a reason for hiding this comment

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

For this I think you might need to rewrite the logic quite a bit to avoid some cloning, since you're cloning the iterator's source each time. Maybe start by using iter().copied() here since both structures implement the Copy trait.
You might also be better off collecting some of them instead of cloning the source. At least for this one I think you could get away with collecting, and maybe the inverse_basis_change as well since you will re-append. Then you can call into-iter() at the end to consume them.

crates/accelerate/src/circuit_library/pauli_evolution.rs Outdated Show resolved Hide resolved
crates/accelerate/src/circuit_library/pauli_evolution.rs Outdated Show resolved Hide resolved
Comment on lines 29 to 58
/// Get an iterator that returns a barrier or an empty element.
pub fn maybe_barrier(
py: Python,
num_qubits: u32,
insert_barriers: bool,
) -> Box<dyn Iterator<Item = PyResult<Instruction>>> {
// TODO could speed this up by only defining the barrier class once
if !insert_barriers {
Box::new(std::iter::empty())
} else {
let barrier_cls = imports::BARRIER.get_bound(py);
let barrier = barrier_cls
.call1((num_qubits,))
.expect("Could not create Barrier Python-side");
let barrier_inst = PyInstruction {
qubits: num_qubits,
clbits: 0,
params: 0,
op_name: "barrier".to_string(),
control_flow: false,
instruction: barrier.into(),
};
Box::new(std::iter::once(Ok((
barrier_inst.into(),
smallvec![],
(0..num_qubits).map(Qubit).collect(),
vec![] as Vec<Clbit>,
))))
}
}
Copy link
Contributor

@raynelfss raynelfss Oct 23, 2024

Choose a reason for hiding this comment

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

This method is a little confusing:

  • From what I can gather it returns a Barrier as an Instruction but then it returns a Box<Iterator<_>> instance, even though it is one item. Is this a memory saving measure?
  • If the purpose of this operation is to just return a Barrier if a condition is met, you should probably have this labeled as #[inline] since it's only a conditional check. This prevents the compiler from seeing an extra call to this function.
  • Going back to @alexanderivrii's comment, could you maybe use insert_barrier.then_some().

Copy link
Contributor Author

@Cryoris Cryoris Oct 28, 2024

Choose a reason for hiding this comment

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

I changed to then_some in cf7d8b0 but I think this has one disadvantage: when we write something like

instructions.chain(
   insert_barrier.then_some(Ok(barrier.clone())
)

Then even if insert_barrier is false, I need to make the Python call to construct the barrier object. To avoid that, what I would like to do is

let barrier = match insert_barrier {
    true => Some(.... get barrier instruction...),
    false => None,
}

// in my iteration loop ...
    instructions.chain(
        insert_barrier.then_some(Ok(barrier.clone().unwrap()))
    )

but rust won't allow this since barrier can be None and I'm not allowed to unwrap 🤔

So it seems we have a performance vs. code legibility issue here, which is why I had the maybe_barrier function. How about making this into a struct with some documentation? Like

struct MaybeBarrier {
    barrier: Option<Instruction>
}

impl MaybeBarrier {
    fn from_py(...) // construct the barrier object
    fn get(condition)  // return None or the barrier based on the condition
}

@alexanderivrii
Copy link
Contributor

Hmm, after switching SX and SXdg in 2b6f4eb, some of the tests in pauli_feature_map now fail. I believe the tests were incorrect before 2b6f4eb, so it's good that they would be fixed, yet it's scary how difficult it is to distinguish between the correct and incorrect behaviors (I was actually looking at these before and they looked perfectly fine to me). Can we add more Operator-based asserts? I.e. in addition to checking that the ref circuit looks as it's supposed to, also that Operator(encoding) == Operator(ref)?

Other than that, I am very happy with this PR, thanks for the great work!

alexanderivrii added a commit to alexanderivrii/qiskit-terra that referenced this pull request Oct 31, 2024
Copy link
Contributor

@raynelfss raynelfss left a comment

Choose a reason for hiding this comment

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

I had a bit of a discussion with @alexanderivrii about the usage of Box here, and we believe to have found a better alternative to what's being done here. I haven't tested it myself but it might be worth taking a look.

time: Param,
phase_gate: bool,
do_fountain: bool,
) -> Box<dyn Iterator<Item = StandardInstruction> + 'a> {
Copy link
Contributor

Choose a reason for hiding this comment

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

From a recent talk I had with @alexanderivrii it seems that you want to call the .rev method to be able to reverse the iterator. If so, perhaps you could change the return types to be DoubleEndedIterator instances instead of a Box<dyn Iterator<_>>.

So you could change some of the return types from:

pub fn foo() -> Box<dyn Iterator<Item = StandardInstruction> + 'a> {

to:

pub fn foo() -> impl DoubleEndedIterator<Item = StandardInstruction> + 'a {

This would allow you to use any iterator type as long as it can be reversed, which seems to be the case for many of the iterators used here.

@alexanderivrii
Copy link
Contributor

alexanderivrii commented Oct 31, 2024

For the record, I am no longer sure what was our conclusion with @raynelfss (Ray is so ahead of me when discussing Rust types). I know that @Cryoris used boxing to be able to return both the usual and the double-ended iterators (and maybe something else too), yet if I remember correctly the double-ended iterators are a bit heavier than the usual ones. Does it make sense for us to return the double-ended iterators everywhere? Another possibility is maybe we should return the CircuitData objects (instead of Vec<Instruction> or iterator over that).

@raynelfss
Copy link
Contributor

In hindsight @Cryoris, we might not be able to use a simple impl Trait I believe I can now see why you used so many Box pointers.

This is because this function returns different types of Iterator depending on the input (e.g. Chain<Map<...>> vs Empty). The dynamic type was the only way I got to work, but I'm happy to change it if there's a better way 🙂

Even though both Once iterators and an example iterator such as Chain<Map<Iterator>> should technically both count as Iterator the types are different enough to require a dynamic block, and so you'd need to use the Box there. I can't think of another way to do this right now, so you can keep the Box here.

Copy link
Member

@ShellyGarion ShellyGarion left a comment

Choose a reason for hiding this comment

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

LGTM. I only have a few comments and questions.


// custom types for a more readable code
type StandardInstruction = (StandardGate, SmallVec<[Param; 3]>, SmallVec<[Qubit; 2]>);
type Instruction = (
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps the name "Instruction" can be confusing with Qiskit Instruction? Maybe call it something like "EvolutionIstruction" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This typedef represents a packed instruction, which can be used as input to CircuitData.from_packed_instructions (so it's not specific to an evolution). The same is used in some other places (e.g. quantum volume or pauli feature map), so I'd prefer keeping as is for now and potentially adding a general typedef used across all of the rust code 🙂

@@ -85,10 +86,10 @@ class PauliEvolutionGate(Gate):

def __init__(
self,
operator,
time: Union[int, float, ParameterExpression] = 1.0,
Copy link
Member

Choose a reason for hiding this comment

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

why is ParamterExpression replaced by ParameterValueType ? (isn't this considered as an API change) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's the same, ParameterValueType is a typedef used across the circuit and the gates, it's defined as

ParameterValueType = Union[ParameterExpression, float]

(see the quantumcircuit.py file)

qiskit/synthesis/evolution/lie_trotter.py Show resolved Hide resolved
qiskit/synthesis/evolution/suzuki_trotter.py Show resolved Hide resolved
Copy link
Contributor

@alexanderivrii alexanderivrii left a comment

Choose a reason for hiding this comment

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

Thanks @Cryoris! Let me approve the PR, but let's also give a chance to @ShellyGarion and @raynelfss to see if they have additional comments.

Copy link
Contributor

@raynelfss raynelfss left a comment

Choose a reason for hiding this comment

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

Taking all previous discussions into consideration, this LGTM!

@raynelfss raynelfss added this pull request to the merge queue Nov 4, 2024
Merged via the queue into Qiskit:main with commit db04339 Nov 4, 2024
17 checks passed
@Cryoris Cryoris deleted the paulievo branch November 4, 2024 21:07
@ShellyGarion ShellyGarion added the Changelog: New Feature Include in the "Added" section of the changelog label Nov 5, 2024
github-merge-queue bot pushed a commit that referenced this pull request Nov 7, 2024
* py version for expand

* starting to write some code

* implementing

* cleanup

* cleanup

* expand fully & simplify lie trotter

* use examples that actually do not commute

* add plugin structure

* fixing global phase for all-I rotations

* fixes

* fixing plugin names

* minor

* removing a random print statement

* additional improvements

* improving rustiq plugin

* merge with #13239

* Adding pauli evolution plugins to docstrings

* adding documentation on rustiq plugin

* fixes after refactoring

* typo

* more merges with #13295; adding more Rustiq tests

* more efficient append_sx and append_sxdg gates for cliffords

* review comments

* moving the pauli network synthesis logic into a separate file

* some code review suggestions

* simplifying the code by merging the oredered and unorderd version of rotation injection

* more review comments

* adding python tests

* more code review suggestions

* more review comments

* more review comments

* test for preserve_order

* lint

* upgrading rustiq-core to 0.0.10

* clippy: removing mutable ref

* Improving PauliEvolution synthesis tests.

Making sure that the number of rotation gates in the synthesized
circuit equals the number of non-trivial Pauli rotation gates.

* documentation fixes after the merge

---------

Co-authored-by: Julien Gacon <[email protected]>
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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add PauliEvolutionGate synthesis plugin using rustiq
6 participants