Skip to content

Add fast-path parameter binding #14782

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

jakelishman
Copy link
Member

The new ParameterExpresion.bind_all is a fast path for producing a numeric result. This has a couple of advantages over ParameterExpression.bind:

  • we can allocate far fewer Python objects, since we do not need to allocate new versions of all the additional data that goes along with a complete ParameterExpression object; we know the output must be numeric.

  • we have no historical API reasons to scan through the incoming mapping to search for invalid keys or values; this is a huge performance improvement for the case of using the same mapping to bind many different expressions (though there are ways that ParameterExpression.bind could be improved and its interface safely evolved to remove the worst cases of the scaling).

There is still a sizeable amount of performance gain to be had, because the interfaces in the Rust-space ParameterExpression and SymbolExpr require rather more heap allocations per binding call than are entirely necessary. That said, this already provides a large performance improvement, and a complexity improvement for the case of a large values dictionary being used many times.

Summary

Details and comments

Fix #14471.

Currently in draft because there's no tests - I'm just putting it up so Sam and Ian from #14471 can test it out for their use case. For the explicit example in that issue, a complete comparison on my machine:

In [1]: from qiskit.circuit import Parameter, ParameterExpression
   ...:
   ...: N: int = 100_000
   ...:
   ...: parameter_values = {Parameter(f"th_{i}"): 1 for i in range(N)}
   ...: parameter_values[param := Parameter("my_param")] = 1
   ...:
   ...: print("Using the specialised `Parameter` methods:")
   ...: %timeit param.bind(parameter_values, allow_unknown_parameters=True)
   ...: %timeit param.bind_all(parameter_values)
   ...:
   ...: print("Using the general `ParameterExpression` methods:")
   ...: %timeit ParameterExpression.bind(param, parameter_values, allow_unknown_parameters=True)
   ...: %timeit ParameterExpression.bind_all(param, parameter_values)
Using the specialised `Parameter` methods:
32.8 ms ± 535 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
68.5 ns ± 0.767 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
Using the general `ParameterExpression` methods:
34.2 ms ± 1.29 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
974 ns ± 8.14 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

@jakelishman jakelishman added this to the 2.2.0 milestone Jul 22, 2025
@jakelishman jakelishman added performance Changelog: New Feature Include in the "Added" section of the changelog mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library labels Jul 22, 2025
@coveralls
Copy link

coveralls commented Jul 22, 2025

Pull Request Test Coverage Report for Build 16468882750

Details

  • 5 of 5 (100.0%) changed or added relevant lines in 2 files are covered.
  • 23 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-0.02%) to 87.761%

Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/lex.rs 5 91.75%
crates/circuit/src/symbol_expr.rs 6 73.73%
crates/qasm2/src/parse.rs 12 97.09%
Totals Coverage Status
Change from base Build 16420779601: -0.02%
Covered Lines: 81474
Relevant Lines: 92836

💛 - Coveralls

The new `ParameterExpresion.bind_all` is a fast path for producing a
numeric result.  This has a couple of advantages over
`ParameterExpression.bind`:

- we can allocate far fewer Python objects, since we do not need to
  allocate new versions of all the additional data that goes along with
  a complete `ParameterExpression` object; we know the output must be
  numeric.

- we have no historical API reasons to scan through the incoming mapping
  to search for invalid keys or values; this is a huge performance
  improvement for the case of using the same mapping to bind many
  different expressions (though there are ways that
  `ParameterExpression.bind` could be improved and its interface safely
  evolved to remove the worst cases of the scaling).

There is still a sizeable amount of performance gain to be had, because
the interfaces in the Rust-space `ParameterExpression` and `SymbolExpr`
require rather more heap allocations per binding call than are entirely
necessary.  That said, this already provides a large performance
improvement, and a complexity improvement for the case of a large values
dictionary being used many times.
@jakelishman jakelishman marked this pull request as ready for review July 23, 2025 11:04
@jakelishman jakelishman requested a review from a team as a code owner July 23, 2025 11:04
@qiskit-bot
Copy link
Collaborator

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

  • @Qiskit/terra-core

Copy link
Contributor

@Cryoris Cryoris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to expose a similar bind_all to the circuit, where we know that all parameters are being bound and we can call this fast path?

And another question actually: what do you think about changing Qiskit's default to not check for parameter existence? Indeed we could be faster and maybe we ought to speed up the default behavior. I think most symbolic engines do not have these existence checks we added.

@jakelishman
Copy link
Member Author

QuantumCircuit.assign_parameters uses almost completely separate mechanisms to fast-path itself, and will have no need to interact with the Python-space methods of ParameterExpression once you've finished moving it to Rust properly. This is addressing a particular use in the Primitives (think: parameter binding in a separate EstimatorPub). They already set allow_unused_parameters=True in the calls to bind, but there's tonnes of other inefficiencies in there, not to mention that ParameterExpression.bind is API-stability required to return a ParameterExpression, even if it's fully bound.

There's a bunch of ways to reduce the overhead and the unnecessary looping through the parameter dictionaries in ParameterExpression.bind, but even if we did that, it'd still not be as fast as this method, so it can be in follow-ups if it's needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Addressing performance bottlenecks in ParameterExpression.bind
4 participants