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

Compose multiple final_layout attributes in routing #11399

Merged
merged 2 commits into from
Mar 27, 2024

Conversation

mtreinish
Copy link
Member

@mtreinish mtreinish commented Dec 11, 2023

Summary

This commit adds several usability methods to the Layout class. The method compose is used to compose two layouts together, the method inverse is used to find the inverse of a layout, and the method to_permutation is used to create a permutation corresponding to the layout.

This commit also adds functionality to the built-in routing passes for composing an existing final layout with the routing permutation.

In PRs #9523 and #11387 we're adding new passes that introduce a permutation prior to routing that needs to be tracked for the final output layout to be valid. The compose method introduced here can be used to combine the two layouts.

This commit adds functionality to the built-in routing passes for
composing an existing final layout with the routing permutation. In
PRs Qiskit#9523 and Qiskit#11387 we're adding new passes that introduce a
permutation prior to routing that needs to be tracked for the final
output layout to be valid. To faciliate this a new method compose() is
added to the Layout class which will combine the two layouts. Then the
built-in routing passes are updated to call compose when a layout has
already been set.
@mtreinish mtreinish added the Changelog: New Feature Include in the "Added" section of the changelog label Dec 11, 2023
@mtreinish mtreinish added this to the 1.0.0 milestone Dec 11, 2023
@mtreinish mtreinish requested a review from a team as a code owner December 11, 2023 22:07
@qiskit-bot
Copy link
Collaborator

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

  • @Qiskit/terra-core

@coveralls
Copy link

coveralls commented Dec 11, 2023

Pull Request Test Coverage Report for Build 8448544750

Warning: 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

  • 27 of 32 (84.38%) changed or added relevant lines in 6 files are covered.
  • 2068 unchanged lines in 164 files lost coverage.
  • Overall coverage increased (+1.8%) to 89.392%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/transpiler/passes/layout/sabre_layout.py 3 4 75.0%
qiskit/transpiler/passes/routing/lookahead_swap.py 2 3 66.67%
qiskit/transpiler/passes/routing/stochastic_swap.py 2 3 66.67%
qiskit/transpiler/passes/routing/basic_swap.py 4 6 66.67%
Files with Coverage Reduction New Missed Lines %
qiskit/transpiler/passes/layout/vf2_utils.py 1 96.34%
qiskit/synthesis/clifford/clifford_decompose_ag.py 1 98.72%
qiskit/pulse/instructions/directives.py 1 97.22%
qiskit/circuit/library/standard_gates/rz.py 1 98.46%
qiskit/circuit/library/standard_gates/swap.py 1 98.08%
qiskit/circuit/tools/pi_check.py 1 91.15%
qiskit/utils/multiprocessing.py 1 94.12%
crates/accelerate/src/sabre_swap/mod.rs 1 97.75%
qiskit/circuit/parametervector.py 1 93.33%
qiskit/circuit/delay.py 1 71.19%
Totals Coverage Status
Change from base Build 7163309461: 1.8%
Covered Lines: 59905
Relevant Lines: 67014

💛 - Coveralls

@alexanderivrii
Copy link
Contributor

While I understand the reason for this PR, I got quite confused about the terminology.

A layout is an injective map from virtual qubits to physical qubits, i.e. a function $f: V \rightarrow P$, with $P$ in general different from $V$. What does it mean to compose two layouts $f_1: V \rightarrow P$ and $f_2: V \rightarrow P$?

If $f:V\rightarrow P$ is a layout and $\pi: V \rightarrow V$ is a permutation of virtual qubits, then $f \circ \pi$ makes sense, similarly if $\sigma: P\rightarrow P$ is a permutation of physical qubits, then $\sigma \circ f$ also makes sense. Which of these cases is handled in the PR?

And I am also confused why the method compose needs qubits in addition to the two layouts.

@jakelishman
Copy link
Member

jakelishman commented Jan 7, 2024

Yeah, I agree with Sasha that using Layout to mean anything other than the $V \leftrightarrow P$ map (we currently require ancilla expansion in part so we can then assume bijection, though I've mentioned before I'm against that) is imo a large part of where these things have got confused before.

Sasha: the full story here though is that the qubits argument is the reason that this operation could represent something valid ($f$ for final, $i$ for initial):

  • read self as a bijection from the set of (virtual qubits + ancillas) $V$ to the the set of physical qubits $P$ at the completion of one circuit: $L_{f,1}: V \leftrightarrow P$
  • read other as the same bijection for another circuit: $L_{f,2}: V\leftrightarrow P$.
  • read qubits as a bijection between the same two spaces as the initial state of both circuits: $L_i$, where the physical qubits are represented by the positions in the list.

Each of the 2-tuples (qubits, self) and (qubits, other) does imply a permutation, though. Taking $L(v): V \to P$ and $L^{-1}(p): P \to V$, the permutation $\pi$ is such that each $v_j \in V$ is taken to $L_f^{-1} \bigl( L_i (v_j) \bigr)$. In composition terms, that's $\pi_{i\to f} : V \to V = L_f^{-1} \circ L_i$.

The aim of this function is to produce a third composed: Layout object, such that if we make the tuple (qubits, composed), the induced permutation $\pi_{\text{composed}} = \pi_{\text{other}} \circ \pi_{\text{self}}$.

edit: originally I wrote the footnote paragraph in the middle, but it's basically just agreeing with Sasha's terminology, so it's just noise.1

Footnotes

  1. Alone, I wouldn't call a Layout object a "permutation" because it's a mapping between two separate spaces, while a permutation would just be a re-ordering of objects within the same space. Part of our problem is that we do sometimes treat Layout as representing a permutation, but then we have problems, because that can only work when the actual set of virtual qubits has been expanded with ancillas, because a permutation must be a bijection but it's valid to have fewer virtual qubits than physical (so users get confused when Layout has "too many" virtual qubits).

@jakelishman
Copy link
Member

I haven't actually reviewed the maths in the PR to check that what I said is what the PR actually achieves, but it's my understanding of what Matt was trying to do with combining the passes.

@alexanderivrii
Copy link
Contributor

alexanderivrii commented Jan 8, 2024

UPDATE: ignore this post, it's all wrong

Thanks Jake, your explanation makes perfect sense. This is how I am thinking about all this right now:

At each point of the computation, we keep a track of the "current" layout $L_f: V\rightarrow P$. The initial layout is given by $L_i : V \rightarrow P$. The "change" between the initial and the current layout is represented by a permutation: $\pi_{i\rightarrow f} = L_f^{-1} \circ L_i: V \rightarrow V$. So, strictly speaking, we are not composing two layouts, but two "changes" between the layouts, aka two permutations.

Hence, if we first apply $f$-computation and then $g$-computation, the composed permutation is $L_g^{-1} \circ L_i \circ L_f^{-1} \circ L_i$. Letting $L_h$ denote the layout after applying both computations, we have that $L_h^{-1} \circ L_i = L_g^{-1} \circ L_i \circ L_f^{-1} \circ L_i$, or equivalently $L_h = L_g \circ L_i^{-1} \circ L_f$.

(This does not say anything new, just slightly restates what Jake has written).

Trying to check the maths, the code currently reads:

other_v2p = other.get_virtual_bits()
return Layout({virt: other_v2p[qubits[phys]] for virt, phys in self._v2p.items()})

If I read this correctly, given a virtual qubit virt, we first apply $L_f$ to it (to get phys), then $L_i$ (represented by qubits), then $L_g$ (represented by other_v2p), which would correspond to $L_g \circ L_i \circ L_f$. This does not fully match the equation before, and I believe we want "qubits^{-1}" instead of "qubits". Jake, Matthew, can you please double-check the maths?

The other question on the terminology is whether in compose self corresponds to $L_f$ and other to $L_g$ or the other way around. The way Jake wrote this, $\pi_{composed} = \pi_{other} \circ \pi_{self}$, so we are updating the layout for $L_f$ with the follow-up computation $L_g$. However, the tests read

second = [2, 3, 1, 0]
first_layout = Layout(dict(zip(qubits, first)))
second_layout = Layout(dict(zip(qubits, second)))
expected = Layout({qubits[0]: 1, qubits[1]: 2, qubits[2]: 3, qubits[3]: 0})
self.assertEqual(second_layout.compose(first_layout, qubits), expected)

and it seems that we are updating the layout for $L_g$ using the preceding computation $L_f$.

@jakelishman
Copy link
Member

Sasha: just flicking through the PR quickly now, I'm not certain that it actually is passing qubits argument the way I wrote, and I think the reason for that is that the final_layout property stored in the PropertySet at the moment is not a true Layout at all in the sense you and I talk about it. It's a sort of permutation - see the hoops that TranspileLayout jumps through to return proper Layout objects in final_virtual_layout for this reason, and the discussion in #10835 over this same problem. (I have #10835 (comment) where I say similar things in that issue, as well.)

With that in mind, I think that if Layout.compose is going to exist, then it should be in terms of the mathematics that Sasha and I were just talking about above, rather than the current (imo mis-)use of Layout for PropertySet["final_layout"]. Ideally, I think we ought to try and get the current final_layout stuff in terms of true $V \leftrightarrow P$ maps as well, rather than the de facto $P \to P$ permutation it actually represents right now, and this might be a convenient time to do so, because we won't need to introduce objects with new meanings: Layout.compose could get the signature (self: Layout, other_final: Layout, initial: Layout) -> Layout, where other_final and initial have the meanings I gave above (which are actual $V \to P$ maps, like Layout was originally intended for).

@jakelishman
Copy link
Member

To add: because of that, in the current state of the PR, other is actually already $\pi'_{\text{other}}$ in the terminology above, where I made it $\pi'$ because it's defined in terms of a $P\to P$ map rather than a virtual one (which imo is confusing for a permutation matrix, because I think of physical qubits as somehow "fixed", so I don't like thinking about permutations on them).

@jakelishman
Copy link
Member

Alternatively, if we don't want to tackle changing how final_layout is currently tracked within this PR, then I'd move this PR's Layout.compose to instead by a private function in qiskit.transpile somewhere, and very specifically (internally) document that it's only for Layout objects that refer to the $P \to P$ routing permutation, and not the $V \leftrightarrow P$ map that we publicly promote Layout as.

@alexanderivrii
Copy link
Contributor

After an offline discussion with @mtreinish and @sbrandhsn, I finally understand the terminology. Personally, I find it a bit confusing and would have named some things a bit differently, but the intent behind it makes perfect sense.

A Layout class in Qiskit tracks keeps track of a map from one set of qubits to another set of qubits. It is used to track the mapping from virtual qubits to physical qubits $V \rightarrow P$, to track the routing permutations of the physical qubits $P \rightarrow P$, and with the new pre-layout ElidePermutations and StarPreLayout passes also to track the routing permutations of the virtual qubits $V\rightarrow V$.

Note: I am not 100% sure about the following, but when reasoning about the maps $V \rightarrow P$, in some places in the code we may work with the original virtual qubits $V$, and in some places with the ancilla-expanded virtual qubits $V$, so one has to be careful about this and not always assume that a layout is bijective.

Additionally, what makes the code a bit confusing is that internally a Layout is not a map from Qubits-A to Qubits-B, but a map from Qubits-A to the positions of Qubits-B. The following example shows how a layout might be constructed in practice:

qr = QuantumRegister(4, "qr")
layout = Layout({qubits[0]: 1, qubits[1]: 2, qubits[2]: 3, qubits[3]: 0})

Note that the list of Qubits-B is not part of the Layout at all, so just from the Layout we do not know where exactly our qubits map to. In addition, a Layout stores a dict, so we do not know the order in which qubits[0], etc. appear in the circuit. In most cases when we use a layout we must also specify either the ordered list of source qubits, or the ordered list of target qubits, or both.

Going back to the previous discussion (that I got all wrong), intuitively the function that composes layouts is the usual composition: given $f: A\rightarrow B$ and $g:B\rightarrow C$, their composition is $h = g \circ f: A \rightarrow C$. However, as internally Layout tracks maps into positions rather then qubits, the first layout is actually a map $f: A \rightarrow Z$, the second layout a map $g: B \rightarrow Z$, and the composed layout is a map $h: A\rightarrow Z$, with $Z$ representing integer positions. To compute the composition, we also need to have the correspondence between the qubits in $B$ and their positions $qubits: B \rightarrow Z$, once we have that, $h = g \circ qubits^{-1} \circ f$, that is, we first apply $f$ (obtaining the position in $B$), then find the actual $B$-qubit, then apply $g$.

BTW, this is all quite extensively documented in the class description and the function docstrings, but it's very easy to get confused.

@sbrandhsn
Copy link
Contributor

sbrandhsn commented Mar 27, 2024

This mostly looks good to me, thanks for looking into this! :-)

The only issue I can see is about the requirements of the new methods; if the layout does not represent what the new methods assume, the output has no semantics. I think here we should:

  1. Raise an error when the method assumptions do not hold
  2. Change the wording of the documentation, e.g. instead of 'if a and b, this computes c' to 'computes c, assuming a and b'. The former formulation implies a stronger check of 'a and b' for me, i.e. I would assume that I get alerted when I supplied incorrect input.

I understand that checking the input assumptions might be as runtime-costly as executing the corresponding method. In these cases I think 2. would be preferable or we should these methods as private methods and only provide public methods that have these checks included...

Do you think it makes sense to open a new issue or a separate PR for expanding the documentation you found confusing with examples or similar?

On hindsight, these are minor points that can be improved on a follow-up PR.

@sbrandhsn sbrandhsn added this pull request to the merge queue Mar 27, 2024
Merged via the queue into Qiskit:main with commit df59ab0 Mar 27, 2024
12 checks passed
@mtreinish mtreinish deleted the compose-final_layout-routing branch March 29, 2024 00:54
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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants