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

Make NLayout's qubit types wrap Qubit #13734

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

Conversation

jakelishman
Copy link
Member

NLayout predates the circuit qubit indices being in Rust. Now, though, NLayout::VirtualQubit and NLayout::PhysicalQubit are promises made on top of circuit qubits, so it makes sense for them to wrap the other type.

This simplifies a reasonable amount of transpiler-pass code, since the conversations are easier. More importantly, this lays the groundwork for the Qubit type to restrict its size to be slightly less than 32 bits.

Summary

Details and comments

This is the first step in making the widths of the Qubit type more changeable from a circuit context, without pushing the complexity onto all transpiler passes.

`NLayout` predates the circuit qubit indices being in Rust.  Now,
though, `NLayout::VirtualQubit` and `NLayout::PhysicalQubit` are
promises made _on top_ of circuit qubits, so it makes sense for them to
wrap the other type.

This simplifies a reasonable amount of transpiler-pass code, since the
conversations are easier.  More importantly, this lays the groundwork
for the `Qubit` type to restrict its size to be slightly less than 32
bits.
@jakelishman jakelishman added Changelog: None Do not include in changelog Rust This PR or issue is related to Rust code in the repository labels Jan 24, 2025
@jakelishman jakelishman requested a review from a team as a code owner January 24, 2025 17:30
@qiskit-bot
Copy link
Collaborator

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

  • @Qiskit/terra-core

@coveralls
Copy link

Pull Request Test Coverage Report for Build 12954556034

Details

  • 78 of 91 (85.71%) changed or added relevant lines in 14 files are covered.
  • 8 unchanged lines in 3 files lost coverage.
  • Overall coverage increased (+0.007%) to 88.948%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/accelerate/src/sabre/neighbor_table.rs 13 14 92.86%
crates/accelerate/src/sabre/layout.rs 18 21 85.71%
crates/accelerate/src/nlayout.rs 12 21 57.14%
Files with Coverage Reduction New Missed Lines %
crates/accelerate/src/nlayout.rs 1 82.88%
crates/accelerate/src/unitary_synthesis.rs 1 92.97%
crates/qasm2/src/lex.rs 6 92.23%
Totals Coverage Status
Change from base Build 12951053301: 0.007%
Covered Lines: 79448
Relevant Lines: 89320

💛 - Coveralls

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.

I think this makes sense to do.

this lays the groundwork for the Qubit type to restrict its size to be slightly less than 32 bits.

Can you provide a little more context for this, also? I left a few comments which I think might be related to your direction here.

Comment on lines +47 to +51
impl From<Qubit> for $id {
fn from(val: Qubit) -> Self {
Self(val)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we have this conversion? I'm wondering if callsites might be more self-documenting if users are forced to use PhysicalQubit::new, for example.

Copy link
Member Author

Choose a reason for hiding this comment

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

I know I used to think that a bit more, but in usage, I've changed a bit. I think that wrapper marker types like this are most useful in the typing of the functions and structs that actually act on them, rather than at the construction sites of them. You can still make logic errors in the value of the Qubit you're putting in there, but that's a logic error only at a scale larger than a single constructor call - it's not a logic error without all the surrounding context. So I'm less worried about making the constructor sites very verbose to use, because the useful part of the typing is when something else consumes the PhysicalQubit, and the construction verbosity just makes them more annoying to type and read, rather than actually safer.

The difference is with something like OperationIndex, over in the other PR: that doesn't have a public constructor at all, because the ownership of one is proof of the provenance that you got it from a DAG that verified that it does in fact refer to an Operation node. We can't make that safe against using it in the wrong DAG, but without global context there's only so much we can achieve with static typing. Still, the idea is that the only public constructors of it (DAGCircuit methods) all have sufficient context to say that it was valid at the time of creation. We can't realistically do that with Qubit in the same way, and the constructors we have already do it as far as we can, I think.


impl $id {
#[inline]
pub fn new(val: u32) -> Self {
Self(val)
pub fn new(val: usize) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we want to encapsulate the storage of bit types in the future, we might want to change this to val: Qubit now.

Copy link
Member Author

Choose a reason for hiding this comment

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

There's no plan to have new take a Qubit - there's no need. Since Qubit now manages the validity of the object, it's totally safe to have the tuple constructor just be public, since you can't make an invalid PhysicalQubit by using it. new then matches Qubit::new, which is usize in the same way that NodeIndex does that - a technically fallible conversion, but the panicking constructor for it.

@@ -171,18 +186,21 @@ impl NLayout {
}

#[staticmethod]
pub fn generate_trivial_layout(num_qubits: u32) -> Self {
pub fn generate_trivial_layout(num_qubits: usize) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine for now, but it feels like we're going the wrong way with changing stuff to usize if we ultimately want a u32 for bits.

Our mistake seems to have been making {Qu,Cl}bit::new take a usize rather than a u32. It led me to create #13377 (which attempts to get us to consistently use usize when working with bit indices and u32 only when creating bits / at rest). We probably ought to make bit indices u32 everywhere and rely on the ::index method to get a usize, e.g. when indexing into collections. It's just a bit uglier since then we also end up with things like (0u32..).zip(qubits) instead of qubits.iter().enumerate().

Copy link
Member Author

Choose a reason for hiding this comment

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

The trouble is that u32 will also be incorrect, and usize is the more convenient size integer. u32 isn't making us any safer - we're still as casting from usize to it all the time, and as with NodeIndex, there's already a lot of places that bake in a runtime assumption, not a compile-time one, that the integers fit. Even a safely constructed Qubit isn't making us statically safe from panics, because there's runtime bounds checks on the number of Qubits in the circuit context.

All this is to say: I don't think the new method matters too much for the internal type any more - that's just the convenience entry point, and overall it seems to be the most convenient to have the dual of the index method. I made TryFrom impls for the actually fallible conversions in my follow-on PR, to avoid the panicking cases for places where that's more important.

Copy link
Member Author

Choose a reason for hiding this comment

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

The inner object of Qubit does still need to be private (in a follow up) so that all the constructors can make certain that you can't hold a Qubit whose bit pattern is invalid for the expectations of Wire (assuming a 30-bit int), but I think that's about the limits of what we want to enforce here via the type system. We can't actually enforce that the value stored inside makes sense, and at some point, that's the best we'd be doing by changing the integer types around, I think.

@jakelishman
Copy link
Member Author

The context for the "Qubit will be slightly less than 32 bits" was the team meeting on Thursday - I'd proposed cutting the maximum number of each bit and var to 30 bits (1 billion ish) so we can store a DAG Wire in 32 bits. It's not as drastic a saving as PackedOperation was, but I'm still hoping for a solid 5-10% memory reduction of DAGCircuit, and if we're at the 1 billion qubit mark, then u32 is probably already getting too dangerous.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: None Do not include in changelog Rust This PR or issue is related to Rust code in the repository
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants