Skip to content

Move ParameterVector to Rust#16228

Open
jakelishman wants to merge 1 commit into
Qiskit:mainfrom
jakelishman:rust-vector
Open

Move ParameterVector to Rust#16228
jakelishman wants to merge 1 commit into
Qiskit:mainfrom
jakelishman:rust-vector

Conversation

@jakelishman
Copy link
Copy Markdown
Member

@jakelishman jakelishman commented May 21, 2026

ParameterVector was previously a pure-Python concept, even though the Rust-space Symbol necessarily had to track an internal backreference to a vector Python object in order to convert itself to Python later. This hidden dependence on the Python interpreter was the root cause of a panicking bug in Qiskit 2.41.

All ParameterVectorElements in Python space have names and UUIDs that are pure functions of the underlying vector and the element's index. Now that this information is available in Rust space, it is far more efficient to simply not store all this derived information, but calculate it on the fly, as needed. This motivated the change in Symbol to an enum: the logic was already handling the two cases of "standalone" and "element", but this formally separates them to avoid storing unnecessary data. Doing this alone is already a performance benefit: construction of a list(ParameterVector("a", 10_000)) (to ensure all the same Python-space objects are created) went from 7.7ms to 1.4ms on my machine (~5x speedup).

This work here is far more aggressive at threading through shared Arc references of both Symbol and SymbolVector. The Arc<SymbolVector> is necessary for correctness in Python space, not just performance: we must have all(el.vector == els[0].vector for el in els), and we don't want to have to cache more Python objects within Symbol to achieve that. Unfortunately, ParameterVector never implemented true equality (only the default referential equality that Python uses to make mutable-state objects safely hashable by default), so we certainly need some other stable reference that can be shared.

The previous Python-space constructions of PyParameter and PyParameterVectorElement had not been done entirely correctly with respect to subclassing; the same (cloned) Symbol was present in multiple places, which was two more allocations than necessary (most places in Rust use Arc<Symbol>, which can be near-freely cloned). This corrects it so that PyParameterVectorElement is now just a simple marker type, and the majority of the Rust logic needn't concern itself with it at all.

The AtomicUsize used for the length of SymbolVector is to support the unfortunate ParameterVector.resize operation from Python space. The atomic operation makes the whole logic safe from data races, but does nothing to solve the potential race condition of two places querying a vector its length concurrently and getting different answers, or the problem that shortening the vector can invalidate already-held references to ParameterVectorElements or Symbols (a problem that was pre-existing in Python). The resize in-place method is fairly fundamentally unsound for these reasons, and we should consider removing it; it's only used in the deprecated NLocal subclass of BlueprintCircuit, and that use can probably be replaced by a simple lazier construction of the vector, or a complete object replacement rather than mutation.

Depends on #16222

Fix #16208

AI/LLM disclosure

  • I didn't use LLM tooling, or only used it privately.
  • I used the following tool to help write this PR description:
  • I used the following tool to generate or modify code:

Footnotes

  1. 16c8088: Fix panic in RemoveIdentityEquivalent with ParameterVector global phase (Fix panic in RemoveIdentityEquivalent with ParameterVector global phase #16054)

@jakelishman jakelishman added this to the 2.5.0 milestone May 21, 2026
@jakelishman jakelishman added on hold Can not fix yet mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library labels May 21, 2026
@jakelishman jakelishman added the Changelog: Performance Performance improvements without API and semantic changes. label May 21, 2026
@jakelishman jakelishman removed the on hold Can not fix yet label Jun 2, 2026
@jakelishman jakelishman marked this pull request as ready for review June 2, 2026 16:26
@jakelishman jakelishman requested a review from a team as a code owner June 2, 2026 16:26
@jakelishman jakelishman requested a review from Cryoris June 2, 2026 16:26
@qiskit-bot
Copy link
Copy Markdown
Collaborator

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

  • @Qiskit/terra-core
  • @mtreinish

@coveralls
Copy link
Copy Markdown

coveralls commented Jun 2, 2026

Coverage Report for CI Build 26897275791

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage decreased (-0.02%) to 87.458%

Details

  • Coverage decreased (-0.02%) from the base build.
  • Patch coverage: 75 uncovered changes across 11 files (326 of 401 lines covered, 81.3%).
  • 28 coverage regressions across 4 files.

Uncovered Changes

Top 10 Files by Coverage Impact Changed Covered %
crates/transpiler/src/target/mod.rs 18 0 0.0%
crates/qpy/src/params.rs 57 44 77.19%
crates/circuit/src/parameter/parameter_expression.rs 181 170 93.92%
crates/cext/src/transpiler/passes/unitary_synthesis.rs 9 0 0.0%
crates/transpiler/src/transpiler.rs 9 0 0.0%
crates/circuit/src/parameter/symbol_expr.rs 60 53 88.33%
crates/qpy/src/py_methods.rs 9 6 66.67%
crates/qpy/src/circuit_reader.rs 7 5 71.43%
crates/circuit/src/operations.rs 1 0 0.0%
crates/circuit/src/parameter_table.rs 4 3 75.0%
Total (22 files) 401 326 81.3%

Coverage Regressions

28 previously-covered lines in 4 files lost coverage.

File Lines Losing Coverage Coverage
crates/circuit/src/parameter/parameter_expression.rs 12 90.36%
crates/circuit/src/parameter/symbol_expr.rs 10 73.98%
crates/qasm2/src/lex.rs 5 92.54%
crates/qpy/src/params.rs 1 68.0%

Coverage Stats

Coverage Status
Relevant Lines: 124203
Covered Lines: 108625
Line Coverage: 87.46%
Coverage Strength: 970795.57 hits per line

💛 - Coveralls

`ParameterVector` was previously a pure-Python concept, even though the
Rust-space `Symbol` necessarily had to track an internal backreference
to a vector Python object in order to convert itself to Python later.
This hidden dependence on the Python interpreter was the root cause of a
panicking bug in Qiskit 2.4[^1].

All `ParameterVectorElement`s in Python space have names and UUIDs that
are pure functions of the underlying vector and the element's index.
Now that this information is available in Rust space, it is far more
efficient to simply not store all this derived information, but
calculate it on the fly, as needed.  This motivated the change in
`Symbol` to an `enum`: the logic was _already_ handling the two cases of
"standalone" and "element", but this formally separates them to avoid
storing unnecessary data.  Doing this alone is already a performance
benefit: construction of a `list(ParameterVector("a", 10_000))` (to
ensure all the same Python-space objects are created) went from 7.7ms
to 1.4ms on my machine (~5x speedup).

This work here is far more aggressive at threading through shared `Arc`
references of both `Symbol` and `SymbolVector`.  The `Arc<SymbolVector>`
is necessary for correctness in Python space, not just performance: we
must have `all(el.vector == els[0].vector for el in els)`, and we don't
want to have to cache more Python objects within `Symbol` to achieve
that.  Unfortunately, `ParameterVector` never implemented true equality
(only the default referential equality that Python uses to make
mutable-state objects safely hashable by default), so we certainly need
some other stable reference that can be shared.

The previous Python-space constructions of `PyParameter` and
`PyParameterVectorElement` had not been done entirely correctly with
respect to subclassing; the same (cloned) `Symbol` was present in
multiple places, which was two more allocations than necessary (most
places in Rust use `Arc<Symbol>`, which can be near-freely cloned).
This corrects it so that `PyParameterVectorElement` is now just a simple
marker type, and the majority of the Rust logic needn't concern itself
with it at all.

The `AtomicUsize` used for the length of `SymbolVector` is to support
the unfortunate `ParameterVector.resize` operation from Python space.
The atomic operation makes the whole logic safe from data races, but
does nothing to solve the potential race condition of two places
querying a vector its length concurrently and getting different answers,
or the problem that shortening the vector can invalidate already-held
references to `ParameterVectorElement`s or `Symbol`s (a problem that was
pre-existing in Python).  The `resize` in-place method is fairly
fundamentally unsound for these reasons, and we should consider removing
it; it's only used in the deprecated `NLocal` subclass of
`BlueprintCircuit`, and that use can probably be replaced by a simple
lazier construction of the vector, or a complete object replacement
rather than mutation.

[^1]: 16c8088: Fix panic in `RemoveIdentityEquivalent` with `ParameterVector` global phase (Qiskitgh-16054)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Changelog: Performance Performance improvements without API and semantic changes. mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

Move ParameterVector to Rust

4 participants