Skip to content

Commit 042561d

Browse files
authored
Add custom DAGCircuit.__deepcopy__ implementation (Qiskit#14965)
Previously we were relying on the pickling behaviour. This was desperately slow, and gets slower over time as more of our data is Rust native and need to be serialised to and from Python in order to interact with pickle. Instead, we can use the `Clone` implementation of `StableGraph`, which preserves node and edge ids (including holes), then only thread through the deepcopy nature to the few parts of the data model where we still store Python objects. In casual testing, this sped up a microbenchmark: ```python import copy from qiskit.converters import circuit_to_dag from qiskit.circuit.library import quantum_volume qv = quantum_volume(100, 100, seed=1).decompose() dag = circuit_to_dag(qv) %timeit copy.deepcopy(dag) ``` from about 1.05s to 6ms on my machine. This is directly relevant for the performance of `optimization_level=3`, which needs to deepcopy as part of its `MinimumPoint` check in the optimisation loop.
1 parent 34e9f03 commit 042561d

File tree

4 files changed

+71
-3
lines changed

4 files changed

+71
-3
lines changed

crates/circuit/src/dag_circuit.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,34 @@ impl DAGCircuit {
984984
Ok(())
985985
}
986986

987+
pub fn __copy__(&self) -> Self {
988+
self.clone()
989+
}
990+
991+
pub fn __deepcopy__<'py>(
992+
&self,
993+
py: Python<'py>,
994+
memo: Option<&Bound<'py, PyDict>>,
995+
) -> PyResult<Self> {
996+
let mut out = self.clone();
997+
let deepcopy = imports::DEEPCOPY.get_bound(py);
998+
// We only need to pass the deep-copying nature on to places where we store Python objects.
999+
out.metadata = out
1000+
.metadata
1001+
.map(|dict| deepcopy.call1((dict, memo)).map(|ob| ob.unbind()))
1002+
.transpose()?;
1003+
out.duration = out
1004+
.duration
1005+
.map(|dict| deepcopy.call1((dict, memo)).map(|ob| ob.unbind()))
1006+
.transpose()?;
1007+
for node in out.dag.node_weights_mut() {
1008+
if let NodeType::Operation(inst) = node {
1009+
inst.py_deepcopy_inplace(py, memo)?;
1010+
};
1011+
}
1012+
Ok(out)
1013+
}
1014+
9871015
/// Returns the current sequence of registered :class:`.Qubit` instances as a list.
9881016
///
9891017
/// .. warning::

crates/circuit/src/operations.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,20 @@ impl Param {
213213
Param::Obj(obj) => Param::Obj(obj.clone_ref(py)),
214214
}
215215
}
216+
217+
pub fn py_deepcopy<'py>(
218+
&self,
219+
py: Python<'py>,
220+
memo: Option<&Bound<'py, PyDict>>,
221+
) -> PyResult<Self> {
222+
match self {
223+
Param::Float(f) => Ok(Param::Float(*f)),
224+
_ => DEEPCOPY
225+
.get_bound(py)
226+
.call1((self.clone(), memo))?
227+
.extract(),
228+
}
229+
}
216230
}
217231

218232
// This impl allows for shared usage between [Param] and &[Param].

crates/circuit/src/packed_instruction.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use std::sync::OnceLock;
1515

1616
use pyo3::prelude::*;
17-
use pyo3::types::PyType;
17+
use pyo3::types::{PyDict, PyType};
1818

1919
use ndarray::Array2;
2020
use num_complex::Complex64;
@@ -24,8 +24,8 @@ use crate::circuit_data::CircuitData;
2424
use crate::imports::{get_std_gate_class, BARRIER, DELAY, MEASURE, RESET, UNITARY_GATE};
2525
use crate::interner::Interned;
2626
use crate::operations::{
27-
Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate,
28-
StandardInstruction, UnitaryGate,
27+
Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, PythonOperation,
28+
StandardGate, StandardInstruction, UnitaryGate,
2929
};
3030
use crate::{Clbit, Qubit};
3131

@@ -767,4 +767,24 @@ impl PackedInstruction {
767767
_ => Ok(false),
768768
}
769769
}
770+
771+
pub fn py_deepcopy_inplace<'py>(
772+
&mut self,
773+
py: Python<'py>,
774+
memo: Option<&Bound<'py, PyDict>>,
775+
) -> PyResult<()> {
776+
match self.op.view() {
777+
OperationRef::Gate(gate) => self.op = gate.py_deepcopy(py, memo)?.into(),
778+
OperationRef::Instruction(inst) => self.op = inst.py_deepcopy(py, memo)?.into(),
779+
OperationRef::Operation(op) => self.op = op.py_deepcopy(py, memo)?.into(),
780+
_ => (),
781+
};
782+
for param in self.params_mut() {
783+
*param = param.py_deepcopy(py, memo)?;
784+
}
785+
#[cfg(feature = "cache_pygates")]
786+
self.py_op.take();
787+
788+
Ok(())
789+
}
770790
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features_transpiler:
3+
- |
4+
:class:`.DAGCircuit` now has a manual implementation of :meth:`~object.__deepcopy__`. This is
5+
orders of magnitude faster than the previous implicit implementation from the pickle protocol,
6+
especially for large circuits, and directly benefits compilation at `optimization_level=3`.

0 commit comments

Comments
 (0)