-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Oxidize NLocal
& family
#13310
Oxidize NLocal
& family
#13310
Conversation
Pull Request Test Coverage Report for Build 11723083102Warning: 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
💛 - Coveralls |
missing Mr EvolvedOperatorAnsatz and Ms QAOAAnsatz
One or more of the following people are relevant to this code:
|
/// their underlying representation (e.g. a string or a list of connections) and returning | ||
/// these when given a layer-index. | ||
pub struct Entanglement { | ||
entanglement_vec: Vec<LayerEntanglement>, |
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.
So it's Vec<Vec<Vec<u32>>
?
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.
Correct, we can think of the structure as
Connection: Vec<u32> // indices that the multi-qubit gate acts on
BlockEntanglement: Vec<Connection> // entanglement for single block
LayerEntanglement: Vec<BlockEntanglement> // entanglements for all blocks in the layer
Entanglement: Vec<LayerEntanglement> // entanglement for every layer
Maybe this would be nice to add as explanatory comment?
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.
A comment wouldn't hurt. I assume you're using the type aliases here to make it simpler. It would be easier for me personally to understand of Vec<u32>
Vec<Vec<u32>>
, etc rather than Connection
, BlockEntanglement
, etc. But I guess cargo doesn't like that. Either way if you want to add a comment that would be nice for people reading the code.
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 also found myself going back checking whether BlockEntanglement
is now a 2 or 3 times nested Vec, but I think in the end the explanation plus the types might make it a bit easier to read 🙂
if !insert_barriers { | ||
Ok(Self { barrier: None }) | ||
} else { | ||
let barrier_cls = imports::BARRIER.get_bound(py); |
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'm looking forward to having a solution for #12966 so we can do this without needing python.
>>> ansatz = ExcitationPreserving(3, reps=1, insert_barriers=True, entanglement='linear') | ||
>>> print(ansatz) # show the circuit | ||
>>> print(ansatz.decompose()) # show the circuit |
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.
Did you want to use flatten
here instead so you don't need to add the decompose()
call
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'll add an example with flatten, but would also like to show the decompose flow 😄
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.
Why though? My concern here is that doing decompose()
here is the less efficient path so we're implicitly encouraging a pattern that will result in subpar performance. But we have the seealso
now pointing people to the faster function so that's probably enough. It's not worth blocking over either way, so I'm fine if you don't want to change this.
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 thought just one example of decompose
would be good so users know how to handle the "vanilla" format of the circuits, where one just gives the minimal amount of argument possible 😄 Except the first example, all others now use flatten
🙂
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. I have a few questions ?
@@ -49,6 +54,18 @@ | |||
from test import QiskitTestCase # pylint: disable=wrong-import-order | |||
|
|||
|
|||
class Gato(Gate): |
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.
🐱 ?
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.
Si, correcto 😛
>>> ansatz = ExcitationPreserving(3, reps=1, insert_barriers=True, entanglement='linear') | ||
>>> print(ansatz) # show the circuit | ||
>>> print(ansatz.decompose()) # show the circuit |
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.
Why though? My concern here is that doing decompose()
here is the less efficient path so we're implicitly encouraging a pattern that will result in subpar performance. But we have the seealso
now pointing people to the faster function so that's probably enough. It's not worth blocking over either way, so I'm fine if you don't want to change this.
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 think I'm mostly happy with this. There is a lot of code so I'm not 100% convinced I kept the full model of everything in my head, especially between the python and rust boundary, so I don't feel super convinced it's perfect. But I think it's probably good enough to merge and we can iterate on it in bugfix releases and anything else that comes up. Especially because this is a new function it's more important that the public interface is sustainable, and I think it looks reasonable and maintainable as a subset of the options the existing classes provided. Besides the open question about two local and other functions from @ShellyGarion I left a few small inline comments.
The only other thing I think is there are some small test coverage gaps that coveralls is flagging which would be good to exercise those paths especially as they're typical conditions for edge cases in the input which would be good to enforce the behavior of.
match self { | ||
Self::Standard { gate } => Ok(( | ||
(*gate).into(), | ||
SmallVec::from_iter(params.iter().map(|&p| p.clone())), |
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.
Maybe?:
SmallVec::from_iter(params.iter().map(|&p| p.clone())), | |
SmallVec::from_iter(params.iter().cloned()), |
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.
Ah nice I didn't know about this! But it doesn't like that because it looks like from_iter
expects &Param
and .cloned
returns Param
types 🤔
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.
But doesn't p.clone()
also return a Param
instance too? It's not a big deal, I kind of wrote this assuming clippy would be grumpy about it.
improve coverage & remove dangling prints
069f45c added tests for the flagged lines in Coveralls (for the new functions, not for the existing classes), so I think we should have the 100% on that 🙂 |
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.
Two small inline comments, that I caught through another scan through the code. They're just small but the python side one is a potential interface issue which could cause issue depending on how things are passed in. The rust comment is more just to avoid using Box
when it's not needed. I think I'm ready to approve once these are addressed
let mut packed_insts: Box<dyn Iterator<Item = PyResult<Instruction>>> = | ||
Box::new((0..reps).flat_map(|layer| { | ||
rotation_layer( | ||
py, | ||
num_qubits, | ||
rotation_blocks, | ||
ledger.get_parameters(LayerType::Rotation, layer), | ||
&skipped_qubits, | ||
) | ||
.chain(maybe_barrier.get()) | ||
.chain(entanglement_layer( | ||
py, | ||
entanglement.get_layer(layer), | ||
entanglement_blocks, | ||
ledger.get_parameters(LayerType::Entangle, layer), | ||
)) | ||
.chain(maybe_barrier.get()) | ||
})); | ||
if !skip_final_rotation_layer { | ||
packed_insts = Box::new(packed_insts.chain(rotation_layer( | ||
py, | ||
num_qubits, | ||
rotation_blocks, | ||
ledger.get_parameters(LayerType::Rotation, reps), | ||
&skipped_qubits, | ||
))) | ||
} | ||
|
||
CircuitData::from_packed_operations(py, num_qubits, 0, packed_insts, Param::Float(0.0)) |
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.
You don't need a box here, you can do something like:
let mut packed_insts: Box<dyn Iterator<Item = PyResult<Instruction>>> = | |
Box::new((0..reps).flat_map(|layer| { | |
rotation_layer( | |
py, | |
num_qubits, | |
rotation_blocks, | |
ledger.get_parameters(LayerType::Rotation, layer), | |
&skipped_qubits, | |
) | |
.chain(maybe_barrier.get()) | |
.chain(entanglement_layer( | |
py, | |
entanglement.get_layer(layer), | |
entanglement_blocks, | |
ledger.get_parameters(LayerType::Entangle, layer), | |
)) | |
.chain(maybe_barrier.get()) | |
})); | |
if !skip_final_rotation_layer { | |
packed_insts = Box::new(packed_insts.chain(rotation_layer( | |
py, | |
num_qubits, | |
rotation_blocks, | |
ledger.get_parameters(LayerType::Rotation, reps), | |
&skipped_qubits, | |
))) | |
} | |
CircuitData::from_packed_operations(py, num_qubits, 0, packed_insts, Param::Float(0.0)) | |
let packed_insts = (0..reps).flat_map(|layer| { | |
rotation_layer( | |
py, | |
num_qubits, | |
rotation_blocks, | |
ledger.get_parameters(LayerType::Rotation, layer), | |
&skipped_qubits, | |
) | |
.chain(maybe_barrier.get()) | |
.chain(entanglement_layer( | |
py, | |
entanglement.get_layer(layer), | |
entanglement_blocks, | |
ledger.get_parameters(LayerType::Entangle, layer), | |
)) | |
.chain(maybe_barrier.get()) | |
}); | |
if !skip_final_rotation_layer { | |
let packed_insts = packed_insts.chain(rotation_layer( | |
py, | |
num_qubits, | |
rotation_blocks, | |
ledger.get_parameters(LayerType::Rotation, reps), | |
&skipped_qubits, | |
)); | |
CircuitData::from_packed_operations(py, num_qubits, 0, packed_insts, Param::Float(0.0)) | |
} else { | |
CircuitData::from_packed_operations(py, num_qubits, 0, packed_insts, Param::Float(0.0)) | |
} |
The trick is to avoid the dynamic type when you call from_packed_operations
if isinstance(entanglement, str): | ||
return [entanglement] * num_entanglement_blocks | ||
|
||
elif isinstance(entanglement, Iterable): |
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.
So checking isinstance
Iterable
isn't really the ideal way to check if something is actually iterable. It only checks if the class has been registered as an Iterable using the abc mechanisms (either as a subclass or manual registration). But python will let you iterate over some objects if __getitem__
is implemented for sequences. The only surefire way to detect if something is actually iterable is to try running iter()
.
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.
We can just turn this around and use not callable(entanglement)
as check, then the only valid type would be the Iterable
🙂
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.
It's a broader thing there are a few places where isinstance(..., Iterable)
is called and they'd all suffer from this same issue.
Summary
Part of #13046: create the
n_local
function and port the internals to Rust. This will include other parts of theNLocal
family, such asTwoLocal
,EfficientSU2
,RealAmplitudes
etc.EvolvedOperatorAnsatz
is potentially a bit more involved and might be done in a follow-up.Details and comments
The new code is particularly fast when dealing with gates we already have in Rust and that don't need any special re-parameterization. In this case:
the new code is ~22x faster for 100 qubits, full entanglement and 10 reps
But if custom gates or special parameterizations are used (e.g.
RXGate(x + 3 * y)
) we need to go back to Python space to correctly handle the parameter logic. This slows down the code and in this case:the new code is ~3.5x faster for 100 qubits, linear entanglement (full took too long) and 10 reps
To do
entanglement
handling rust-side a bit better to understand instead of a 4-times nested vecUseThis might not be possible sincen_local
inNLocal
(i.e. fast path for Python code)NLocal
works withQuantumCircuit
s as blocks, butn_local
needs gates. Functionally it'd be the same but the circuits are nested differently, which is not backward compatible. We could add a fast-path in case the inputs are not circuits but gates.efficient_su2
etc.