-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Add new API for inserting multiple operations into the DAGCircuit
in Rust.
#13335
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
Conversation
- Add struct that allows continuous addition of instructions into the back of the `DAGCircuit` by performing edge modifications in the output nodes at the end of the additions. - The new struct includes the following new methods: - `push_operation_back` which pushes a valid `PackedInstruction` into the back of the `DAGCircuit`. It behaves very similarly to `push_back`. - `apply_operation_back` which inserts a new operation into the back of the `DAGCircuit`. It has two variants which depend on the ownership of the bit indices (`Qubit`, `Clbit`). - `pack_instruction`: private method that basically does just that, it creates a valid instance of `PackedInstruction` based on other provided atributes. - Modified `DAGCircuit::extend` to use this new API, without any considerable losses.
One or more of the following people are relevant to this code:
|
DAGCircuit
in Rust.
Pull Request Test Coverage Report for Build 14517820673Warning: 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 |
- Add struct that allows usage of both slice or owned collections of bit indices. - Leverage use of `as_concat()` in `circuit_to_dag`.
- Leverage usage of new methods in `UnitarySynthesis` after Qiskit#13141 merged.
After rewriting the logic of the
|
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 is a very neat idea, and something I've been interested in in the past. Aside from the inline comments, I have one main consideration (as two chained questions):
- Is there any way we can get by with just
extend
as is?UnitarySynthesis
was doing that, so canBasisTranslator
be re-organised to do it too? There are ways around the set-up overhead of the method. - Failing that: I mentioned in one of the comments that there's a big unsafety here in that if you forget to call
DAGCircuitConcat::end
but just let the concatenator go out of scope, theDAG
is now irreparably broken (from the caller's perspective). We definitely need to make that harder to do, ideally impossible. Makingend
the destructor makes it harder to make the mistake, but not impossible. I note that the only places we callextend
(or the concatenator) are in situations where we're doing a full DAG rebuild, so we could actually take ownership of theDAGCircuit
. In that situation, now you can haveDAGCircuitConcat
take ownership, andend
return the ownership, so the caller can't ever observe theDAGCircuit
in a broken state.
crates/circuit/src/dag_circuit.rs
Outdated
/// Applies a new operation to the back of the circuit. This variant works with non-owned bit indices. | ||
pub fn apply_operation_back( | ||
&mut self, | ||
py: Python, | ||
op: PackedOperation, | ||
qubits: Option<OwnedOrSlice<Qubit>>, | ||
clbits: Option<OwnedOrSlice<Clbit>>, | ||
params: Option<SmallVec<[Param; 3]>>, | ||
extra_attrs: ExtraInstructionAttributes, | ||
#[cfg(feature = "cache_pygates")] py_op: Option<PyObject>, | ||
) -> NodeIndex { | ||
let instruction = self.pack_instruction( | ||
op, | ||
qubits, | ||
clbits, | ||
params, | ||
extra_attrs, | ||
#[cfg(feature = "cache_pygates")] | ||
py_op, | ||
); | ||
self.push_operation_back(py, instruction) | ||
} |
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 not sure this offers much over making pack_instruction
public and only exposing push_operation_back
- the output of pack_instruction
is already effectively public because you can retrieve the PackedInstruction
by indexing the DAG, so I'm not sure we're gaining anything by making it private.
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.
Hmm... sounds good, but I did want to keep an effectively similar method to DAGCircuit::apply_operation_back
. I think that both exposing the pack_instruction
method and keeping the apply_operation_back
would be a viable option.
crates/circuit/src/dag_circuit.rs
Outdated
/// Pushes a valid [PackedInstruction] to the back ot the circuit. | ||
pub fn push_operation_back(&mut self, py: Python<'_>, inst: PackedInstruction) -> NodeIndex { | ||
let op_name = inst.op.name(); | ||
let (all_cbits, vars): (Vec<Clbit>, Option<Vec<Var>>) = { |
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 are allocation inefficiencies in this function that we can definitely improve on with this new struct, but better to leave them for a follow-up, I think.
let mut out_dag_concat = out_dag.as_concat(); | ||
for out_node in synth_dag.topological_op_nodes()? { |
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 function (apply_synth_dag
) doesn't feel like it's high enough in the call tree, because we're allocating and de-allocating the concatenator for each synthesised unitary matrix, whereas in an ideal world we'd do that only once. Perhaps we should be passing the concatenator through the function calls?
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.
The call to out_dag.as_concat()
was moved higher up in the stack in bf26f56.
- Allow `DAGCircuitConcat` to take ownership of the original `DAGCircuit` it is being called from, and allow `DAGCircuitConcat::end` to return the original `DAGCircuit`. - Bypass taking ownership of the `DAGCircuit` during `DAGCircuit::extend` calls by swapping with a temporary replacement.
- A previous commit added some inefficient code into the `apply_synth_dag` function in `UnitarySynthesis`. The newer code should be much simpler to use. - Rename interner views in `DAGCircuitConcat` to match their `DAGCircuit` counterparts.
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 looks good overall.
A few comments. I think we should probably move away from having the interners pub
in a future PR, in favor of some way of accessing them that doesn't allow the user to replace them (i.e. make sure their semantics are append-only).
crates/circuit/src/dag_circuit.rs
Outdated
let mut new_nodes = Vec::new(); | ||
// TODO: Find a less hacky way of doing this | ||
let mut replacement_dag = DAGCircuit::new()?; | ||
std::mem::swap(self, &mut replacement_dag); | ||
let mut dag_concat = replacement_dag.into_concat(); | ||
for inst in iter { | ||
new_nodes.push(dag_concat.push_instruction_back(py, inst?)); | ||
} | ||
std::mem::swap(self, &mut dag_concat.end()); |
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 this is the only way to do this. If we implement Default
for DAGCircuit
, then you could use mem::take
instead and then *self = dag_concat.end()
at the end. But it'd be doing the same thing, it'd just look less noticeable that a dummy value is in play here.
@@ -197,9 +198,9 @@ pub struct DAGCircuit { | |||
cregs: RegisterData<ClassicalRegister>, | |||
|
|||
/// The cache used to intern instruction qargs. | |||
pub qargs_interner: Interner<[Qubit]>, | |||
qargs_interner: Interner<[Qubit]>, |
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 that these should not be mutable, the reason being that it makes it possible to modify a DAGCircuit
into an invalid state, e.g. we could std::mem::swap
an empty Interner
over this one, which opens us up to crashes and unpredictable behavior.
The pub
was originally needed for #13141, which makes the valid point that they're useful when building PackedInstruction
instances. You are still exposing them mutably from DAGCircuitConcat
, which is moving the problem.
It may be beyond the scope of this PR to address 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.
Nice, it looks like you were able to encapsulate this again. We'll see if it stays that way, but glad to see this!
crates/circuit/src/dag_circuit.rs
Outdated
} | ||
|
||
/// Returns a mutable view to the qubit interner | ||
pub fn qargs_interner_mut(&mut self) -> &mut Interner<[Qubit]> { |
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.
As mentioned higher up, these still leak a mutable Interner
from the DAGCircuit
(probably something we should address separately).
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.
For now, I can change the mutable views into just insert_cargs
and insert_qargs
, since it is the only way they're used.
Co-authored-by: Kevin Hartman <kevin@hart.mn>
- Create `_push_back` method to perform initial additions and parsing before pushing an instruction into the circuit. This will be used by both the `DAGCircuit` and the `DAGCircuitBuilder`. - Fix `DAGCircuitBuilder::end` docstring. - Remove `qarg/carg_interner_mut`, replaced by `insert_qargs/cargs`.
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.
Looking good, just a few more minor comments. Thanks for the changes!
@@ -197,9 +198,9 @@ pub struct DAGCircuit { | |||
cregs: RegisterData<ClassicalRegister>, | |||
|
|||
/// The cache used to intern instruction qargs. | |||
pub qargs_interner: Interner<[Qubit]>, | |||
qargs_interner: Interner<[Qubit]>, |
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.
Nice, it looks like you were able to encapsulate this again. We'll see if it stays that way, but glad to see this!
pub fn qargs_interner(&self) -> &Interner<[Qubit]> { | ||
&self.dag.qargs_interner | ||
} | ||
|
||
/// Returns an immutable view to the clbit interner | ||
pub fn cargs_interner(&self) -> &Interner<[Clbit]> { | ||
&self.dag.cargs_interner | ||
} |
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 recognize we have these very same methods on DAGCircuit
which expose a non-mutable ref to the Interner
s, but it feels a bit odd to me that we use wrapper methods for the mutable stuff (e.g. insert_qargs
) but expose the interners directly for read-only stuff. It'd be nice if everything was at the same level, i.e. either wrapper methods get_qargs
and insert_qargs
, or expose the interners directly (which of course has the mutability issue).
What you've got is probably fine for now, but IMO the InternerViewMut
stuff you mentioned offline might be a nice way to unify things in a future PR.
Co-authored-by: Kevin Hartman <kevin@hart.mn>
- Rename `_push_back` to `get_classical_resources`. - Rename variables to keep the naming scheme. - Remove `increment_op` step from `get_classical_resources`.
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.
Almost there, just a few minor comments, mostly just correcting old variable names.
Co-authored-by: Kevin Hartman <kevin@hart.mn>
- Remove mutably borrowing the `DAGCircuit` in `get_classical_resources`.
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.
Looks good to me!
Thanks for doing this, and for the changes. This is a neat tool.
…n Rust. (Qiskit#13335) * Initial: Add `DAGCircuitConcat` struct and new insertion API. - Add struct that allows continuous addition of instructions into the back of the `DAGCircuit` by performing edge modifications in the output nodes at the end of the additions. - The new struct includes the following new methods: - `push_operation_back` which pushes a valid `PackedInstruction` into the back of the `DAGCircuit`. It behaves very similarly to `push_back`. - `apply_operation_back` which inserts a new operation into the back of the `DAGCircuit`. It has two variants which depend on the ownership of the bit indices (`Qubit`, `Clbit`). - `pack_instruction`: private method that basically does just that, it creates a valid instance of `PackedInstruction` based on other provided atributes. - Modified `DAGCircuit::extend` to use this new API, without any considerable losses. * Add: `OwnedOrSlice<T>` - Add struct that allows usage of both slice or owned collections of bit indices. - Leverage use of `as_concat()` in `circuit_to_dag`. * Add: Optional qargs/cargs in `apply_operation_back` - Leverage usage of new methods in `UnitarySynthesis` after Qiskit#13141 merged. * Fix: Leverage usage in `BasisTranslator` * Fix: Replace `OnceCell` with `OnceLock` * Fix: Remove `SliceOrOwned` in favor of `Cow` * Fix: Expose the interners via references within `DAGCircuitConcat`. * Refactor: `DAGCircuit::as_concat` to `DAGCircuit::into_concat`. - Allow `DAGCircuitConcat` to take ownership of the original `DAGCircuit` it is being called from, and allow `DAGCircuitConcat::end` to return the original `DAGCircuit`. - Bypass taking ownership of the `DAGCircuit` during `DAGCircuit::extend` calls by swapping with a temporary replacement. * Fix: Remove inefficient code from `UnitarySynthesis` - A previous commit added some inefficient code into the `apply_synth_dag` function in `UnitarySynthesis`. The newer code should be much simpler to use. - Rename interner views in `DAGCircuitConcat` to match their `DAGCircuit` counterparts. * Fix: Remove obsolete pytoken. * Update crates/circuit/src/dag_circuit.rs Co-authored-by: Kevin Hartman <kevin@hart.mn> * Fix: Address comments from code review - Create `_push_back` method to perform initial additions and parsing before pushing an instruction into the circuit. This will be used by both the `DAGCircuit` and the `DAGCircuitBuilder`. - Fix `DAGCircuitBuilder::end` docstring. - Remove `qarg/carg_interner_mut`, replaced by `insert_qargs/cargs`. * Apply suggestions from code review Co-authored-by: Kevin Hartman <kevin@hart.mn> * Fix: Address review comments - Rename `_push_back` to `get_classical_resources`. - Rename variables to keep the naming scheme. - Remove `increment_op` step from `get_classical_resources`. * Fix: Limit `get_classical_resources` to only `Clbits` and `Vars`. - Remove `DAGNodeInfo`. * Fix: Missing `py` token added after modification to `DAGCircuit::num_vars()` * Apply suggestions from code review Co-authored-by: Kevin Hartman <kevin@hart.mn> * Fix: Remove usage of `end`. - Remove mutably borrowing the `DAGCircuit` in `get_classical_resources`. --------- Co-authored-by: Kevin Hartman <kevin@hart.mn>
Summary
Adds a new struct
DAGCircuitBuilder
(name has changed 😄) that enables chained addition of instructions both valid and invalid into theDAGCircuit
. Also, adapts existing structures to those new changes.Details and comments
DAGCircuit
by performing edge modifications in the output nodes at the end of the additions.push_back
which pushes a validPackedInstruction
into the back of theDAGCircuit
. It behaves very similarly topush_back
.apply_operation_back
which inserts a new operation into the back of theDAGCircuit
.It has two variants which depend on the ownership of the bit indices (.Qubit
,Clbit
)pack_instruction
: creates a valid instance ofPackedInstruction
based on other provided attributes.DAGCircuit::extend
to use this new API, without any considerable losses.What about front additions?
This struct currently does not support adding instructions continuously to the front of the
DAGCircuit
because the methodology for that is a little different. It requires doing much more shifting than adding to the back, therefore I suggest studying the commonality of the use case and checking if it could be done in a follow-up.Questions and comments?
Feel free to add these below as a review or comment 😸