Skip to content

[unitaryhack] Topology-aware initial placement for the qubit-mapping pass#4678

Open
border-b wants to merge 9 commits into
NVIDIA:mainfrom
border-b:issue/4289
Open

[unitaryhack] Topology-aware initial placement for the qubit-mapping pass#4678
border-b wants to merge 9 commits into
NVIDIA:mainfrom
border-b:issue/4289

Conversation

@border-b

@border-b border-b commented Jun 5, 2026

Copy link
Copy Markdown

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 while star(5,0) emits 1.

This PR adds a greedy interaction-aware initial placement strategy and makes it the default. It keeps placement=identity for the old behavior.

What changed

The new placement=greedy path:

  • collects weighted two-qubit interactions from the input circuit,
  • seeds the most-connected logical qubit on a central physical qubit,
  • grows the layout by keeping interacting qubits close,
  • keeps whichever of the greedy or identity layout scores better under a static layout-distance metric,
  • leaves the SABRE-style router itself unchanged.

Results

Swap counts for the issue repro:

device identity greedy
path(5) 1 1
star(5,0) 1 1
star(5,2) 2 1
grid(3,3) 1 1

star(5,2) now matches star(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 with cudaq-opt --mlir-timing on an 8-vCPU / 32 GB Linux build using ghcr.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.

border-b added 7 commits June 5, 2026 21:50
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>
Signed-off-by: Seemanta Bhattacharjee <babune99@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Jun 5, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@Zaneham

Zaneham commented Jun 8, 2026

Copy link
Copy Markdown

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. interactionPlacement builds the layout from an untimed interaction matrix where every two-qubit gate counts the same, while layoutScore weights interactions by gate order through the numI - i term. Because the router runs off the front layer with a lookahead window, a placement that only knows total interaction weight can seat a pair well by overall count and still badly for the gates the router reaches first. Making the placement itself front-layer aware, rather than only the final selection, is the more principled version, and it matches what the router actually rewards.

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.

@taalexander taalexander left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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:

  1. Circuit has many late interactions between q0 and `q1
  2. Greedy placement puts q0 and q1 close because of this.
  3. The first front-layer gates use q2-q3 and q3-q4.
  4. The router adds immediate swaps
  5. 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:

  1. Build a RoutingProblem from the IR once. Capture the device, routeable operations, source wires, measurement constraints, and virtual-qubit mapping.
  2. Generate starting LayoutCandidates. Keep this as a small helper layer. Identity, greedy, dense, and random layouts should only propose starting layouts. Now greedy is just a potential placement seed to kickoff the search.
  3. Add a RoutingSearchStrategy which 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.
  4. 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.
  5. 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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What is meant by logical here? Is this the connectivity degree of a given qubit?

Comment on lines +64 to +69
// No two-qubit interactions: fall back to identity.
if (!hasInteraction) {
for (unsigned v = 0; v < n; ++v)
vrToPhy[v] = v;
return vrToPhy;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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?)?

Comment on lines +71 to +87
// 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;
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This code should be refactored for clarity

Comment on lines +860 to +861
std::string placementStrategy = this->placement;
if (placementStrategy != "identity" && placementStrategy != "greedy") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use enum for determining strategy.

Comment on lines +1051 to +1078
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]));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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).

@border-b

border-b commented Jun 9, 2026

Copy link
Copy Markdown
Author

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.

@taalexander taalexander changed the title Topology-aware initial placement for the qubit-mapping pass [unitaryhack] Topology-aware initial placement for the qubit-mapping pass Jun 10, 2026
@taalexander

Copy link
Copy Markdown
Collaborator

Thank you @border-b, looking forward to seeing the new and improved version!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SABRE qubit-mapping pass: topology-aware initial placement to reduce SWAPs on irregular devices

3 participants