[unitaryhack] Topology-aware initial placement for the qubit-mapping pass#4678
[unitaryhack] Topology-aware initial placement for the qubit-mapping pass#4678border-b wants to merge 9 commits into
Conversation
Signed-off-by: Seemanta Bhattacharjee <babune99@gmail.com>
Signed-off-by: Seemanta Bhattacharjee <babune99@gmail.com>
Signed-off-by: Seemanta Bhattacharjee <babune99@gmail.com>
Signed-off-by: Seemanta Bhattacharjee <babune99@gmail.com>
Signed-off-by: Seemanta Bhattacharjee <babune99@gmail.com>
Signed-off-by: Seemanta Bhattacharjee <babune99@gmail.com>
|
Nice repro. Two isomorphic stars routing to different SWAP counts is a clean way to surface the physical-numbering artifact, and scoring the greedy layout against identity and keeping whichever wins is a sensible guard against regressions. The thing worth raising is that the placement and the selection are optimising slightly different models. Two smaller things. The selection runs on a static distance proxy, which you flag honestly, and the stronger if costlier option is to route both candidate layouts and compare real SWAP counts. And the linear recency weighting reads as a little arbitrary without a sentence tying it to the router's own decay behaviour. Solid work overall, and the regression-guard instinct is the right one. |
There was a problem hiding this comment.
Hi @border-b, thank you for your contribution. It is a great start but will need some degree of rethinking to be ready for merge. As noted by @Zaneham in his comment the current placement, layout and router are optimizing different models. I also think we are missing larger demos/benchmarks on some quantum volume like circuits to understand that the PR is having the intended improvement.
The interaction placement builds the candidate from the total two-qubit interaction counts. This treats all interactions equally indepedent of when they happen in the circuit. Later on you use the layoutScore to choose between greedy and identity with a distance score. I don't think you should be silently falling back to identity if a different placement strategy was selected.
However, the bigger issue is that chooseSwap then does it's selection based on the dynamic front layer. This will invalidate most of the static assumptions and might lead to a situation like:
- Circuit has many late interactions between
q0and `q1 - Greedy placement puts
q0andq1close because of this. - The first front-layer gates use
q2-q3andq3-q4. - The router adds immediate swaps
- Swaps have not changed the placement so that original static score is invalidated
The issue is that these models are not working together. The first layout could optimize for pairs that matter often but not until late in the circuit. The second model could choose a layout based on early distances without understand the routing. Then the third model could invalidate all of these static assumptions by inserting early swaps.
I think a good direction would be to not make greedy the whole placement system and instead structure this as a small (internal) layout/routing system to which we could add the greedy candidate now and maybe SABRE/LightSABRE style candidates in the future (although one of these as an alternative to greedy is acceptable and preferred).
An ideal approach might look something like:
- Build a
RoutingProblemfrom the IR once. Capture the device, routeable operations, source wires, measurement constraints, and virtual-qubit mapping. - Generate starting
LayoutCandidates. Keep this as a small helper layer. Identity, greedy, dense, and random layouts should only propose starting layouts. Nowgreedyis just a potential placement seed to kickoff the search. - Add a
RoutingSearchStrategywhich should own the search. It should route candidates in analyze mode and then run the SABRE forward/backward refinement. It's output should be the final routing selected for the router to apply. - Select the best
RoutingResult. Choose by the routed SWAP count. Longer term we might want to make this selectable (eg.,swap-count,depth, etc.) or at the choice of the strategy. - Emit the selected result once through a
RoutingEmitter. Rewrite the IR only after the best routed result has been selected.
I would prefer if the the reverse traversal strategy was used from the original paper as opposed to the greedy solution. In this way something like a LightSABRE extension would just add a new strategy (evaluating many candidate layouts) in the future and not a new pass rewrite.
Please feel free to ask me any clarifying questions you might have 😄
| /// `vrToPhy` array without mutating a `cudaq::Placement`, so the caller can | ||
| /// score it against identity and apply only the winner. | ||
| SmallVector<unsigned> | ||
| interactionPlacement(const cudaq::Device &device, |
There was a problem hiding this comment.
This method is a monolith and should be refactored.
| const unsigned n = device.getNumQubits(); | ||
| SmallVector<unsigned> vrToPhy(n, 0); | ||
|
|
||
| // Logical weighted degree, and whether any two-qubit interaction exists. |
There was a problem hiding this comment.
What is meant by logical here? Is this the connectivity degree of a given qubit?
| // No two-qubit interactions: fall back to identity. | ||
| if (!hasInteraction) { | ||
| for (unsigned v = 0; v < n; ++v) | ||
| vrToPhy[v] = v; | ||
| return vrToPhy; | ||
| } |
There was a problem hiding this comment.
Is this replicating the current Cuda-Q behaviour? To me this appears to be a failure mode that should be communicated (no mapping can be found?)?
| // Physical centrality: lower distance-sum, then higher degree, then lower | ||
| // index. | ||
| SmallVector<unsigned> distanceSum(n, 0); | ||
| SmallVector<unsigned> physDegree(n, 0); | ||
| for (unsigned p = 0; p < n; ++p) { | ||
| for (unsigned q = 0; q < n; ++q) | ||
| distanceSum[p] += device.getDistance(Qubit(p), Qubit(q)); | ||
| physDegree[p] = | ||
| static_cast<unsigned>(device.getNeighbours(Qubit(p)).size()); | ||
| } | ||
| auto physBetter = [&](unsigned a, unsigned b) { | ||
| if (distanceSum[a] != distanceSum[b]) | ||
| return distanceSum[a] < distanceSum[b]; | ||
| if (physDegree[a] != physDegree[b]) | ||
| return physDegree[a] > physDegree[b]; | ||
| return a < b; | ||
| }; |
There was a problem hiding this comment.
This requires further comments to describe the intent and usage of these code sections (and should be refactored).
| }; | ||
|
|
||
| // Seed the highest-degree logical qubit onto the most central physical qubit. | ||
| unsigned seedVirtual = n; |
There was a problem hiding this comment.
This code should be refactored for clarity
| std::string placementStrategy = this->placement; | ||
| if (placementStrategy != "identity" && placementStrategy != "greedy") { |
There was a problem hiding this comment.
Can we please create an enum and mapping for placement strategies.
| cudaq::Placement placement(sources.size(), deviceInstance->getNumQubits()); | ||
| identityPlacement(placement); | ||
| cudaq::Placement layout(sources.size(), deviceInstance->getNumQubits()); | ||
| if (placementStrategy == "identity") { |
There was a problem hiding this comment.
Use enum for determining strategy.
| unsigned numV = layout.getNumVirtualQubits(); | ||
| SmallVector<unsigned> identityCand(numV); | ||
| for (unsigned v = 0; v < numV; ++v) | ||
| identityCand[v] = v; | ||
| SmallVector<unsigned> interactionCand = interactionPlacement( | ||
| *deviceInstance, interactions, userVirtualQubits); | ||
|
|
||
| // Ordered static-distance score; lower is better, ties favor interaction. | ||
| auto layoutScore = [&](ArrayRef<unsigned> vrToPhy) { | ||
| std::uint64_t total = 0; | ||
| unsigned numI = orderedInteractions.size(); | ||
| for (unsigned i = 0; i < numI; ++i) { | ||
| auto [v0, v1] = orderedInteractions[i]; | ||
| unsigned dist = deviceInstance->getDistance( | ||
| cudaq::Placement::DeviceQ(vrToPhy[v0]), | ||
| cudaq::Placement::DeviceQ(vrToPhy[v1])); | ||
| total += static_cast<std::uint64_t>(numI - i) * (dist - 1); | ||
| } | ||
| return total; | ||
| }; | ||
|
|
||
| ArrayRef<unsigned> chosen = | ||
| layoutScore(interactionCand) <= layoutScore(identityCand) | ||
| ? interactionCand | ||
| : identityCand; | ||
| for (unsigned v = 0; v < numV; ++v) | ||
| layout.map(cudaq::Placement::VirtualQ(v), | ||
| cudaq::Placement::DeviceQ(chosen[v])); |
There was a problem hiding this comment.
Why is this clause so complex and not a callout to a single function implementing the placement? Falling back to identity placement if better seems like a behaviour I would not expect naively if I selected a specific placement strategy. That might be fine for a strategy like "auto" which would select the best strategy (or if no arguments were provided).
|
Thanks @taalexander and @Zaneham for reviewing and the thoughtful comments. The point about the placement, selection, and router optimizing different models is well taken. I'm going back through the SABRE paper and the proposed restructuring now, with reverse traversal as the likely direction (I'd considered it early on but went with the smaller change at the time). I'll follow up with some questions in the next couple of days. |
|
Thank you @border-b, looking forward to seeing the new and improved version! |
Fixes #4289.
The qubit-mapping pass currently initializes the SABRE-style router with identity placement (
virtual i -> physical i). On irregular topologies this can make physical qubit numbering affect the routed SWAP count. The issue repro shows that on two isomorphic stars:star(5,2)emits 2 swaps whilestar(5,0)emits 1.This PR adds a greedy interaction-aware initial placement strategy and makes it the default. It keeps
placement=identityfor the old behavior.What changed
The new
placement=greedypath:Results
Swap counts for the issue repro:
star(5,2)now matchesstar(5,0), so the center-index artifact is gone. Mean SWAP count over the requested sweep goes from 1.25 to 1.00.I also checked a relocated-hub case and a reordered triangle. Greedy placement matched or improved identity there as well. The greedy-vs-identity choice is made on that static distance score, not on the post-routing SWAP count. So it guards against regressions below identity on these topologies rather than guaranteeing it on an arbitrary circuit.
The measured overhead is small: below
--mlir-timing's 0.1 ms reporting resolution on the small sweep, and about 0.3 ms on a 225-qubit grid. Timing was measured withcudaq-opt --mlir-timingon an 8-vCPU / 32 GB Linux build usingghcr.io/nvidia/cuda-quantum-devcontainer:cu12.6-gcc12-main.Tests
Added tests for the issue repro, a relocated interaction hub, gate-order selection, and invalid placement handling. A few existing exact-output mapping tests are pinned to
placement=identity.AI disclosure: I used Opus 4.8 via Claude Code to set up the remote build/test environment, polish the implementation, and the tests. The design decisions, final edits, and verification are mine.