Skip to content

Commit 16c8088

Browse files
authored
Fix panic in RemoveIdentityEquivalent with ParameterVector global phase (#16054)
If a circuit has a global phase containing a `ParameterVectorElement`, the resulting `SymbolExpr` will transitively contain a `Py<PyAny>` pointing back to the `ParameterVector` object. When adding to the global phase, a name/symbol-map clone occurs, which requires us to be attached to the Python interpreter. Since `RemoveIdentityEquivalent` manually detaches from the Python interpreter, we may attempt to increment the refcount without being attached. This patch is intended to be minimal for backport to 2.4; there are several more invasive changes we could make (and largely _should_ make) to make `ParameterExpression` safer and more performant, and in general it is becoming clear we still have a lot of work to do to be able to remove the `py-clone` feature use of PyO3 and let the compiler catch these mistakes for us.
1 parent c2477ab commit 16c8088

3 files changed

Lines changed: 35 additions & 2 deletions

File tree

crates/transpiler/src/passes/remove_identity_equiv.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,23 @@ pub fn py_remove_identity_equiv(
242242
approx_degree: Option<f64>,
243243
target: Option<&Target>,
244244
) -> PyResult<()> {
245+
// TODO: This is a hack to avoid panicking in the case that the global phase contains `Py`
246+
// pointers (such as backrefs to `ParameterVector` objects in an expression `Symbol`) that would
247+
// get cloned when updating the global phase. It's easier to do it out here than to try to
248+
// reattach to Python-space within a pure-Rust function but only if we can spot a hiding `Py`
249+
// (or we'd break standalone C).
250+
//
251+
// This doesn't account for control-flow blocks which _also_ might have set global phases, byt
252+
// `run_remove_identity_equiv` as of Qiskit 2.4 doesn't recurse, so the hack should hold.
253+
let old_phase = dag.global_phase().clone();
254+
dag.set_global_phase_f64(0.0);
255+
245256
// Explicitly release GIL because threads may call Python to get
246257
// the matrix for a PyGate
247-
py.detach(|| run_remove_identity_equiv(dag, approx_degree, target))
258+
py.detach(|| run_remove_identity_equiv(dag, approx_degree, target))?;
259+
260+
dag.add_global_phase(&old_phase)?;
261+
Ok(())
248262
}
249263

250264
pub fn run_remove_identity_equiv(
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed a panic in :class:`.RemoveIdentityEquivalent` when the global phase of the circuit contained
5+
:class:`.ParameterVectorElement` objects. The message was:
6+
7+
Cannot clone pointer into Python heap without the thread being attached.
8+
9+
See `#16053 <https://github.com/Qiskit/qiskit/issues/16053>`__.

test/python/transpiler/test_remove_identity_equivalent.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import ddt
1616
import numpy as np
1717

18-
from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister, Gate
18+
from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister, Gate, ParameterVector
1919
from qiskit.circuit.library import (
2020
CPhaseGate,
2121
RXGate,
@@ -213,6 +213,16 @@ def test_parameterized_global_phase_ignored(self):
213213
transpiled = RemoveIdentityEquivalent()(qc)
214214
self.assertEqual(qc, transpiled)
215215

216+
def test_no_panic_on_parametervector_phase(self):
217+
"""Regression test for gh-16053."""
218+
pv = ParameterVector("v", 1)
219+
qc = QuantumCircuit(1, global_phase=pv[0])
220+
qc.rz(0.0, 0)
221+
qc = RemoveIdentityEquivalent()(qc)
222+
223+
expected = QuantumCircuit(1, global_phase=pv[0])
224+
self.assertEqual(qc, expected)
225+
216226
def test_pauli_evo_equals_stdgate(self):
217227
"""Test the Pauli evolution gate is consistent with std gates."""
218228

0 commit comments

Comments
 (0)