Port ApplyLayout to Rust#14904
Conversation
d6f20cc to
3f6b57d
Compare
|
This PR should be ready now, it's just stacked on top of #14826. |
|
One or more of the following people are relevant to this code:
|
Pull Request Test Coverage Report for Build 17236197815Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
3f6b57d to
82e23b7
Compare
|
The current failure on that Windows test seems to be a true failure; on my machine I can reproduce with: from qiskit.circuit.library import quantum_volume
from qiskit import transpile
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.transpiler import passes
BOGOTA_CMAP = [[0, 1], [1, 0], [1, 2], [2, 1], [2, 3], [3, 2], [3, 4], [4, 3]]
qcomp = GenericBackendV2(
num_qubits=5,
coupling_map=BOGOTA_CMAP,
basis_gates=["id", "u1", "u2", "u3", "cx"],
seed=42,
)
def attempt(seed):
qv_circuit = quantum_volume(3, seed=seed)
gates_in_basis_true_count = 0
consolidate_blocks_count = 0
def counting_callback_func(pass_, property_set, **_):
nonlocal gates_in_basis_true_count
nonlocal consolidate_blocks_count
if isinstance(pass_, passes.GatesInBasis) and property_set["all_gates_in_basis"]:
gates_in_basis_true_count += 1
elif isinstance(pass_, passes.ConsolidateBlocks):
consolidate_blocks_count += 1
transpile(
qv_circuit,
backend=qcomp,
optimization_level=3,
callback=counting_callback_func,
translation_method="synthesis",
seed_transpiler=seed,
)
assert gates_in_basis_true_count + 2 == consolidate_blocks_count
attempt(3)so I'll look into it. |
|
Ok, the bug is arguably in This causes an error on main: from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Qubit
from qiskit.transpiler import CouplingMap
qc = QuantumCircuit([Qubit() for _ in range(3)])
qc.cx(0, 1)
transpile(qc, coupling_map=CouplingMap.from_line(3), basis_gates=["sx", "rz", "cx"]).layout.initial_index_layout()because the |
This adds and uses the Rust-space paths for `ApplyLayout`. The Rust interface is deliberately forwards looking; it uses the new Rust-space `TranspileLayout` and all the surrounding infrastructure to begin the process of transitioning our passes to move the `TranspileLayout` to an inherent field on `DAGCircuit`, and updated by all relevant passes in Qiskit directly, rather than the ad-hoc, frequently temporarily out-of-sync collection of properties in the `PropertySet`. The Rust-space version of `ApplyLayout` is split into the two forms: "apply a layout for the first time" (`apply_layout`) and "improve an already set layout" (`update_layout`). `apply_layout` can also handle the allocation of explicit ancillas automatically. The Python-space version of the class is not yet upgraded to allow this functionality, but that can be done in a follow-up.
82e23b7 to
2cb2df3
Compare
mtreinish
left a comment
There was a problem hiding this comment.
Overall this is looking good, I like the new logical flow it's much clearer especially in the Python code. I just have few inline comments and questions. The biggest one is maybe just because I haven't looked at vf2post in rust yet, but I am a bit lost in the typing expectations for the rust space caller of update_layout(). Mechanically I also am wondering about the choice of assert_eq! in several places, the panic message for that is very testing focused from what I remember.
| impl From<$id> for $crate::Qubit { | ||
| fn from(val: $id) -> Self { | ||
| $crate::Qubit(val.0) | ||
| } | ||
| } | ||
| impl From<$crate::Qubit> for $id { | ||
| fn from(val: $crate::Qubit) -> Self { | ||
| Self(val.0) | ||
| } | ||
| } |
There was a problem hiding this comment.
I thought you didn't want these conversions implemented to make it explicit when converting between Qubit types, or maybe I'm misremembering. It feels like a year ago we talked about this, but I can't find the PR discussion that I'm vaguely remembering. Maybe it was just the direct virt->phys and vice versa that we were talking about back then.
There was a problem hiding this comment.
It's quite possible I did think that / make that argument in the past. If so, I changed my mind a bit - I think I softened a lot on the From trait. I still think that From<VirtualQubit> for PhysicalQubit would be a mistake, but From<Qubit> for VirtualQubit is now (imo) fine - you're just saying "here's a qubit, I'm saying it's virtual". If I argued before against it, I guess it was on the basis of wanting to be explicit about having to write VirtualQubit, but if so, I definitely softened on that on the bases of a) it ends up being a bit too noisy and b) having the function/use-site have a type is already explicit/type-checked enough to catch the bugs, I think.
| let names = qregs.iter().map(|qreg| qreg.name()).collect::<HashSet<_>>(); | ||
| let mut i = 0; | ||
| loop { | ||
| let name = format!("{base}_{i}"); | ||
| if !names.contains(name.as_str()) { | ||
| return name; | ||
| } | ||
| i += 1; | ||
| } |
There was a problem hiding this comment.
This does change the exact register names from Python. While I agree this is a more foolproof implementation of what the intent was in FullAncillaAllocation in Python it just called the equivalent of:
format!("ancilla_{}", QuantumRegister::anonymous_instance_count().fetch_add(1, Ordering::Relaxed)While this will always use the lowest i value for ancilla_{i} that doesn't conflict with another name, it feels like it would have more overhead than just using the total number of created registers in the program like what python was doing. I guess the difference in a c context is that we may not be bumping the instance count for owned registers, was that your concern here?
There was a problem hiding this comment.
The Python-space version is bugged - there's nothing stopping you from defining ancilla1 yourself and then FullAncillaAllocation will happily just attempt to re-use it and explode:
from qiskit.circuit import QuantumCircuit, QuantumRegister
from qiskit.transpiler import CouplingMap, PassManager, passes
cm = CouplingMap.from_line(4)
qc = QuantumCircuit(QuantumRegister(1, "ancilla"), QuantumRegister(1, "ancilla0"))
PassManager([
passes.TrivialLayout(cm),
passes.FullAncillaAllocation(cm),
passes.EnlargeWithAncilla(),
]).run(qc)will raise a "duplicate register error" (if it's the first thing you run in your Python session).
There was a problem hiding this comment.
To speak to efficiency - I put the fast path in because 99.99% of the time, we're going to be going down that route, and it just outputs the regular name. If not, we already have larger performance problems with having tonnes of registers (iirc, QuantumCircuit.add_register is quadratic in register count),
We can't even have a test asserting anything about this, because I didn't have to change the test suite.
| } | ||
| ) | ||
| out_pass(first_layout_circ) | ||
| out_pass.run(circuit_to_dag(first_layout_circ)) |
There was a problem hiding this comment.
Was this needed for the property set handling differences in the pass? If so is there a backwards compat concern here?
There was a problem hiding this comment.
I don't think there's a concern. There's two parts to the change, both around the fact that the test as previously written used BasePass.__call__, which was relatively recently changed to go via PassManager.run.
Now:
- since
PassManager.runsets uporiginal_qubit_indicesitself on entry, theApplyLayoutpass was getting the wrong data. The fact that the pass's existing property set isn't completely clobbered on entry to a pass is already pretty weird - the correct way of feeding data throughBasePass.__call__has always been to pass aproperty_setargument, which existed even before we swapped over the mechanism. The test passes before becauseApplyLayoutdidn't used to notice iforiginal_qubit_indiceswas incorrect. - If I use
PassManager.run(implicitly), then we're not directly testing the output ofApplyLayout, because there's also a layout-normalisation step during the pass manager finalisation. So this makes the test more true to its original intention.
SincePassManager.runsets uporiginal_qubit_indicesitself now, and this pass was relying on historical behaviour that you could manually write to theproperty_setstored in the pass before execution and nothing would touch it. But the switch ofBasePass.__call__a couple of versions ago already changed that behaviour.
alexanderivrii
left a comment
There was a problem hiding this comment.
This PR is very-very-very-very nice. (And it also shows how much easier and error-prone it is to work with the new Rust-space TranspileLayout instead of the Python-space one).
| if cur_layout.initial_layout(false).is_some() { | ||
| panic!("cannot apply a layout when one is already set"); | ||
| } |
There was a problem hiding this comment.
The users would be able to hit this panic if they call ApplyLayout twice in a row, right? Should this be then a transpiler error of some kind instead of a panic?
There was a problem hiding this comment.
A Rust-space user can, but a Python-space user shouldn't be able to because the Python-space entry point shouldn't be able to construct a Rust-space TranspileLayout object that contains a set initial_layout. I can add a test that it's safe from Python space, though - that'd be a good addition.
There was a problem hiding this comment.
Actually, the only real test I can add of this would probably have to assert nonsense behaviour of the ApplyLayout pass; I'd have to attempt to to apply a layout to a circuit that already has one, and rely on the fact that we can't tell if a DAGCircuit had a layout applied yet. The test would become invalidated when we move TranspileLayout onto the DAGCircuit, because it'd stop spuriously succeeding and should begin to throw an exception.
mtreinish
left a comment
There was a problem hiding this comment.
LGTM, thanks for the updates.
* Port `ApplyLayout` to Rust This adds and uses the Rust-space paths for `ApplyLayout`. The Rust interface is deliberately forwards looking; it uses the new Rust-space `TranspileLayout` and all the surrounding infrastructure to begin the process of transitioning our passes to move the `TranspileLayout` to an inherent field on `DAGCircuit`, and updated by all relevant passes in Qiskit directly, rather than the ad-hoc, frequently temporarily out-of-sync collection of properties in the `PropertySet`. The Rust-space version of `ApplyLayout` is split into the two forms: "apply a layout for the first time" (`apply_layout`) and "improve an already set layout" (`update_layout`). `apply_layout` can also handle the allocation of explicit ancillas automatically. The Python-space version of the class is not yet upgraded to allow this functionality, but that can be done in a follow-up. * Remove equality message from assertion panics * Clarify logic of ancilla allocation * Distinguish layouts from relabellings of layouts * Clarify action of `ApplyLayout`
* Port `ApplyLayout` to Rust This adds and uses the Rust-space paths for `ApplyLayout`. The Rust interface is deliberately forwards looking; it uses the new Rust-space `TranspileLayout` and all the surrounding infrastructure to begin the process of transitioning our passes to move the `TranspileLayout` to an inherent field on `DAGCircuit`, and updated by all relevant passes in Qiskit directly, rather than the ad-hoc, frequently temporarily out-of-sync collection of properties in the `PropertySet`. The Rust-space version of `ApplyLayout` is split into the two forms: "apply a layout for the first time" (`apply_layout`) and "improve an already set layout" (`update_layout`). `apply_layout` can also handle the allocation of explicit ancillas automatically. The Python-space version of the class is not yet upgraded to allow this functionality, but that can be done in a follow-up. * Remove equality message from assertion panics * Clarify logic of ancilla allocation * Distinguish layouts from relabellings of layouts * Clarify action of `ApplyLayout`
Summary
This adds and uses the Rust-space paths for
ApplyLayout. The Rust interface is deliberately forwards looking; it uses the new Rust-spaceTranspileLayoutand all the surrounding infrastructure to begin the process of transitioning our passes to move theTranspileLayoutto an inherent field onDAGCircuit, and updated by all relevant passes in Qiskit directly, rather than the ad-hoc, frequently temporarily out-of-sync collection of properties in thePropertySet.The Rust-space version of
ApplyLayoutis split into the two forms: "apply a layout for the first time" (apply_layout) and "improve an already set layout" (update_layout).apply_layoutcan also handle the allocation of explicit ancillas automatically. The Python-space version of the class is not yet upgraded to allow this functionality, but that can be done in a follow-up.Details and comments
The actual mechanics of porting
ApplyLayoutare mostly straightforward. The reason this PR is tricky is because the Rust-spaceApplyLayoutinterface is forward-facing to the next iteration of Qiskit's compilation tracking. In other words, it's based around the Rust-spaceTranspileLayout, and sets up and then uses the Python-space paths for gentle migration from the Python-spaceTranspileLayoutto the Rust-space one within the transpiler. This means we have to deal with new pathways through the compositions ofinitial_layout,virtual_permutation_layout, andfinal_layout.Close #12262
edit: currently on hold because it's based on #14826, which needs reviewing first.