Skip to content
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

Use stable Python C API for building Rust extension #10120

Merged
merged 22 commits into from
Jun 12, 2023

Conversation

mtreinish
Copy link
Member

@mtreinish mtreinish commented May 16, 2023

Summary

This commit tweaks the rust extension code to start using the PyO3 abi3 flag to build binaries that are compatible with all python versions, not just a single release. Previously, we were building against the version specific C API and that resulted in needing a binary file for each supported python version on each supported platform/architecture. By using the abi3 feature flag and marking the wheels as being built with the limited api we can reduce our packaging overhead to just having one wheel file per supported platform/architecture.

The only real code change needed here was to update the memory marginalization function. PyO3's abi3 feature is incompatible with returning a big int object from rust (the C API they use for that conversion isn't part of the stable C API). So this commit updates the function to convert to create a python int manually using the PyO3 api where that was being done before.

Details and comments

TODO:

  • Release note
  • Test cibuildwheel with this change
  • Run Benchmarks to see if there is a performance impact

This commit tweaks the rust extension code to start using the PyO3 abi3
flag to build binaries that are compatible with all python versions, not
just a single release. Previously, we were building against the version
specific C API and that resulted in needing abinary file for each
supported python version on each supported platform/architecture. By
using the abi3 feature flag and marking the wheels as being built with
the limited api we can reduce our packaging overhead to just having one
wheel file per supported platform/architecture.

The only real code change needed here was to update the memory
marginalization function. PyO3's abi3 feature is incompatible with
returning a big int object from rust (the C API they use for that
conversion isn't part of the stable C API). So this commit updates the
function to convert to create a python int manually using the PyO3 api
where that was being done before.

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
@mtreinish mtreinish added type: qa Issues and PRs that relate to testing and code quality Changelog: API Change Include in the "Changed" section of the changelog Rust This PR or issue is related to Rust code in the repository labels May 16, 2023
@mtreinish mtreinish added this to the 0.25.0 milestone May 16, 2023
@mtreinish mtreinish requested a review from a team as a code owner May 16, 2023 16:12
@qiskit-bot
Copy link
Collaborator

One or more of the the following people are requested to review this:

Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

fwiw I'm happy with the code changes in here, but we need another reviewer (and the release note + cibuildwheel checks) because I was involved in writing the patch.

crates/accelerate/Cargo.toml Outdated Show resolved Hide resolved
Eric-Arellano
Eric-Arellano previously approved these changes May 16, 2023
Copy link
Collaborator

@Eric-Arellano Eric-Arellano left a comment

Choose a reason for hiding this comment

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

Excellent. We were very sad when we had to stop using abi3 in Pantsbuild because of some IO stuff we were doing. This is a good change.

+1 to setting to abi3-py38

Eric-Arellano
Eric-Arellano previously approved these changes May 16, 2023
According to the docs for the setuptools-rust RustExtension class:
https://setuptools-rust.readthedocs.io/en/latest/reference.html#setuptools_rust.RustExtension
The best setting to use for the py_limited_api argument is `"auto"` as
this will use the setting in the PyO3 module to determine the correct
value to set. This commit updates the setup.py to follow the
recommendation in the docs.
@mtreinish
Copy link
Member Author

Hmm, the test failures (both unit test and neko) look like there is an issue using Complex64 and abi3 in pyo3. This will be a blocker for this because we need to be able to pass complex numbers back and forth to rust code. I guess we could refactor the code to manually pass 2 floats for the real and imaginary parts for each complex number, and build Complex64s in the rust from that. But that doesn't seem like a great solution

@mtreinish mtreinish added the on hold Can not fix yet label May 16, 2023
@jakelishman
Copy link
Member

It looks like the problem here is actually a little split between us and PyO3, but fortunately enough of the bug is on our end that we should be able to get a working version. PyO3 has (for some reason) special handling to allow conversion of length-1 arrays to scalars, but the conversion into Complex64 via this scalarisation path doesn't appear to work correctly when it's in limited-API mode.

For example, I made a dummy PyO3 project with a Rust function:

use num_complex::Complex64;

fn noop_complex(a: Complex64) -> Complex64 {
    a
}

I wrapped that into a PyO3 module pyo3_test, and then used the Python test script:

import numpy as np
from pyo3_test import noop_complex

noop_complex(np.array([1j]))

If you don't set the limited API, the return result is 1j. If you do, you get

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[1], line 4
      1 import numpy as np
      2 from pyo3_test import noop_complex
----> 4 noop_complex(np.array([1j]))

TypeError: argument 'a': float() argument must be a string or a real number, not 'complex'

That's the same error in the Neko test. It's not identical to the one in the regular test suite, but I'm guessing/hoping it's related.

The pauli_expval module in Rust that Statevector and DensityMatrix
leverage when computing defines the input type of the phase argument as
Complex64. Previously, the quantum info code in the Statevector and
DensityMatrix classes were passing in a 1 element ndarray for this
parameter. When using the the version specific Python C API in pyo3 it
would convert the single element array to a scalar value. However when
using abi3 this handling was not done (or was not done correctly) and
this caused the tests to fail. This commit updates the quantum info
module to pass the phase as a complex value instead of a 1 element numpy
array to bypass this behavior change in PyO3 when using abi3.

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
@coveralls
Copy link

coveralls commented May 16, 2023

Pull Request Test Coverage Report for Build 5179802304

  • 8 of 8 (100.0%) changed or added relevant lines in 3 files are covered.
  • 22 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.07%) to 85.923%

Files with Coverage Reduction New Missed Lines %
crates/accelerate/src/sabre_swap/mod.rs 1 99.77%
crates/qasm2/src/lex.rs 3 90.89%
crates/qasm2/src/parse.rs 18 96.65%
Totals Coverage Status
Change from base Build 5177933484: 0.07%
Covered Lines: 71353
Relevant Lines: 83043

💛 - Coveralls

@jlapeyre
Copy link
Contributor

the problem here is actually a little split between us and PyO3 ...

"All checks have passed". But I don't see any commits address the conversion of complex problem. Am I missing something?

@jakelishman
Copy link
Member

jakelishman commented May 16, 2023

py_limited_api="auto" is the default, and my experiments locally were that pip install . will not cause --py-limited-api to be passed to setup.py bdist_wheel and that the versioned API will be generated. I suspect that in the most recent test round, we didn't actually build against the limited API. Certainly when building from the sdist (doing so builds a wheel from the sdist then installs) we didn't:

  Created wheel for qiskit-terra: filename=qiskit_terra-0.25.0-cp311-cp311-linux_x86_64.whl size=5896032 sha256=5be4381592c21e5cbf9b422fdd8adffe7b16dc78c84ae6e3b42cab099df56f4b

I think we'll either need to enforce that all instances in our CI that build Terra from source specify --py-limited-api in the build options to pip for that wheel, or we might just be safer setting py_limited_api=True to force it from the setuptools_rust.Extension interface.

John: the test successes may be because of the above, but also commit 7e5b027 fixes the badly typed values that were incorrectly being passed, in the way I suggested in my comment above. That should sidestep this PyO3 bug.

edit: On rereading, I realise that I didn't include the context in the previous comment that Matthew and I had tracked down the issue to badly typed values in the quantum_info module, sorry. I thought I'd mentioned that, but I didn't.

@mtreinish
Copy link
Member Author

Oops, I didn't actually mean to commit "auto" I set that locally for testing and came to the same conclusion that we needed to set it explicitly. I forgot to undo my local edits before running git commit -a when I made the phase type fixes. Let me revert that and go back to True

jakelishman
jakelishman previously approved these changes May 19, 2023
Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

I'm happy with this, though we ought to have a second in-depth review.

Fwiw, I filed the PyO3 inconsistency we found (via our own bug) as PyO3/pyo3#3164.

@jakelishman jakelishman dismissed their stale review May 19, 2023 10:38

Forgot we still need a release note.

Eric-Arellano
Eric-Arellano previously approved these changes May 22, 2023
kevinhartman
kevinhartman previously approved these changes May 22, 2023
Copy link
Contributor

@kevinhartman kevinhartman left a comment

Choose a reason for hiding this comment

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

LGTM once a release note is added.

@Eric-Arellano
Copy link
Collaborator

LGTM once a release note is added.

Most people won't care about this. Pip will do its thing just fine. But I agree it's still worth documenting, e.g. for people with lockfiles that write down the checksum of every artifact for the package.

@mtreinish mtreinish removed the on hold Can not fix yet label Jun 2, 2023
@mtreinish
Copy link
Member Author

This should be ready for final review now. I've added the missing release note and also tested that cibuildwheel works and is producing a single binary and testing across all python versions.

Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

Aside from the release-note comments, I'm happy to merge this at this point.

I left a couple of comments inline for posterity. Another one Matt and I discussed more offline: it's not technically necessary to specify abi3-py38 in the Cargo.toml files when passing --py-limited-api=cp38 to bdist_wheel as we do universally in setup.py (it's inferred by setuptools-rust), but we chose to do it this way so that the build_rust command to setup.py and direct cargo-based commands will still build in limited API mode, since these are common workflows for our Rust developers. The number of times we'll need to change the flag is very small, and once we're at an MSRV of 1.64, we can inherit one rule from the workspace's Cargo.toml anyway, reducing the duplication.

Comment on lines +25 to +31
repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel} && pipx run abi3audit --strict --report {wheel}"

[tool.cibuildwheel.macos]
repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} && pipx run abi3audit --strict --report {wheel}"

[tool.cibuildwheel.windows]
repair-wheel-command = "cp {wheel} {dest_dir}/. && pipx run abi3audit --strict --report {wheel}"
Copy link
Member

Choose a reason for hiding this comment

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

For posterity: the pre-&& components of the repair commands for Linux and macOS are copied from the cibuildwheel defaults. Windows has no default command.

releasenotes/notes/use-abi3-4a935e0557d3833b.yaml Outdated Show resolved Hide resolved
releasenotes/notes/use-abi3-4a935e0557d3833b.yaml Outdated Show resolved Hide resolved
@jakelishman
Copy link
Member

I'll leave this unmerged for a little while to see if any of the other people interested in the Rust/Python API layer want to comment on the final form, since I was very involved with this PR.

@jakelishman
Copy link
Member

Merging now, since it's been a week.

@jakelishman jakelishman added this pull request to the merge queue Jun 12, 2023
Merged via the queue into Qiskit:main with commit c463b3c Jun 12, 2023
@mtreinish mtreinish deleted the abi3 branch June 12, 2023 14:54
thspreetham98 pushed a commit to thspreetham98/qiskit-terra that referenced this pull request Jun 19, 2023
* Use stable Python C API for building Rust extension

This commit tweaks the rust extension code to start using the PyO3 abi3
flag to build binaries that are compatible with all python versions, not
just a single release. Previously, we were building against the version
specific C API and that resulted in needing abinary file for each
supported python version on each supported platform/architecture. By
using the abi3 feature flag and marking the wheels as being built with
the limited api we can reduce our packaging overhead to just having one
wheel file per supported platform/architecture.

The only real code change needed here was to update the memory
marginalization function. PyO3's abi3 feature is incompatible with
returning a big int object from rust (the C API they use for that
conversion isn't part of the stable C API). So this commit updates the
function to convert to create a python int manually using the PyO3 api
where that was being done before.

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>

* Set minimum version on abi3 flag to Python 3.8

* Fix lint

* Use py_limited_api="auto" on RustExtension

According to the docs for the setuptools-rust RustExtension class:
https://setuptools-rust.readthedocs.io/en/latest/reference.html#setuptools_rust.RustExtension
The best setting to use for the py_limited_api argument is `"auto"` as
this will use the setting in the PyO3 module to determine the correct
value to set. This commit updates the setup.py to follow the
recommendation in the docs.

* Update handling of phase input to expval rust calls

The pauli_expval module in Rust that Statevector and DensityMatrix
leverage when computing defines the input type of the phase argument as
Complex64. Previously, the quantum info code in the Statevector and
DensityMatrix classes were passing in a 1 element ndarray for this
parameter. When using the the version specific Python C API in pyo3 it
would convert the single element array to a scalar value. However when
using abi3 this handling was not done (or was not done correctly) and
this caused the tests to fail. This commit updates the quantum info
module to pass the phase as a complex value instead of a 1 element numpy
array to bypass this behavior change in PyO3 when using abi3.

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>

* Set py_limited_api explicitly to True

* DNM: Test cibuildwheel works with abi3

* Add abi3audit to cibuildwheel repair step

* Force setuptools to use abi3 tag

* Add wheel to sdist build

* Workaround abiaudit3 not moving wheels and windows not having a default repair command

* Add source of setup.py hack

* Add comment about pending pyo3 abi3 bigint support

* Revert "DNM: Test cibuildwheel works with abi3"

This reverts commit 8ca24cf.

* Add release note

* Simplify setting abi3 tag in built wheels

* Update releasenotes/notes/use-abi3-4a935e0557d3833b.yaml

Co-authored-by: Jake Lishman <jake@binhbar.com>

* Update release note

* Update releasenotes/notes/use-abi3-4a935e0557d3833b.yaml

---------

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
Co-authored-by: Jake Lishman <jake@binhbar.com>
mtreinish added a commit to mtreinish/qiskit-core that referenced this pull request Aug 21, 2023
In Qiskit#10120 we moved to using the Python stable C API for the qiskit
binaries we build. In that PR we encountered a limitation with PyO3 at
the time when using abi3 it was unable to convert a BigUInt into a
Python int directly. To workaround this we side stepped the issue by
generating a string representation of the integer converting that to
python and then having python go from a string to a int. This has some
performance penalty and also prevented parallelism because a GIL handle
was needed to do the conversion. In PyO3 0.19.1 this limitation was
fixed and the library can handle the conversion directly now with abi3
and this commit restores the code that existed in the marginalization
module prior to Qiskit#10120.
github-merge-queue bot pushed a commit that referenced this pull request Aug 22, 2023
In #10120 we moved to using the Python stable C API for the qiskit
binaries we build. In that PR we encountered a limitation with PyO3 at
the time when using abi3 it was unable to convert a BigUInt into a
Python int directly. To workaround this we side stepped the issue by
generating a string representation of the integer converting that to
python and then having python go from a string to a int. This has some
performance penalty and also prevented parallelism because a GIL handle
was needed to do the conversion. In PyO3 0.19.1 this limitation was
fixed and the library can handle the conversion directly now with abi3
and this commit restores the code that existed in the marginalization
module prior to #10120.
SamD-1998 pushed a commit to SamD-1998/qiskit-terra that referenced this pull request Sep 7, 2023
)

In Qiskit#10120 we moved to using the Python stable C API for the qiskit
binaries we build. In that PR we encountered a limitation with PyO3 at
the time when using abi3 it was unable to convert a BigUInt into a
Python int directly. To workaround this we side stepped the issue by
generating a string representation of the integer converting that to
python and then having python go from a string to a int. This has some
performance penalty and also prevented parallelism because a GIL handle
was needed to do the conversion. In PyO3 0.19.1 this limitation was
fixed and the library can handle the conversion directly now with abi3
and this commit restores the code that existed in the marginalization
module prior to Qiskit#10120.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: API Change Include in the "Changed" section of the changelog Rust This PR or issue is related to Rust code in the repository type: qa Issues and PRs that relate to testing and code quality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants