Skip to content

Commit 68eb846

Browse files
committed
Add BFS initial placement to SABRE qubit-mapping pass
Replace identity placement with BFS-from-highest-degree-node as the default initial placement strategy. This places circuit qubits on the most centrally-connected device positions, improving routing quality on irregular topologies (heavy-hex, star). Add a 'placement' pass option (identity|bfs, default bfs) so users can select the strategy. Existing FileCheck tests updated to use placement=identity for backward compatibility. Addresses #4289. Signed-off-by: Thomas Alexander <talexander@nvidia.com>
1 parent ff62e76 commit 68eb846

8 files changed

Lines changed: 189 additions & 28 deletions

File tree

include/cudaq/Optimizer/Transforms/Passes.td

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,9 @@ def MappingFunc: Pass<"qubit-mapping-func", "mlir::func::FuncOp"> {
927927
"Number of rounds before decay is reset">,
928928
Option<"nonComposable", "raise-fatal-errors", "bool", /*default=*/"false",
929929
"Run the pass in a non-composable way, which may cause immediate "
930-
"internal compiler errors">
930+
"internal compiler errors">,
931+
Option<"placement", "placement", "std::string", /*default=*/"\"bfs\"",
932+
"Initial placement strategy: identity, bfs (default: bfs)">
931933
];
932934
}
933935

lib/Optimizer/Transforms/Mapping.cpp

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,53 @@ void identityPlacement(Placement &placement) {
4949
placement.map(Placement::VirtualQ(i), Placement::DeviceQ(i));
5050
}
5151

52+
/// Place circuit qubits on the most centrally-connected device positions using
53+
/// BFS from the highest-degree node. Remaining device qubits receive auxiliary
54+
/// virtual qubits in BFS order.
55+
void bfsPlacement(Placement &placement, const Device &device) {
56+
unsigned numDeviceQubits = device.getNumQubits();
57+
if (numDeviceQubits == 0)
58+
return;
59+
60+
// Find the highest-degree node (tie-break: lowest index).
61+
unsigned bestNode = 0;
62+
unsigned bestDegree = 0;
63+
for (unsigned i = 0; i < numDeviceQubits; ++i) {
64+
unsigned degree = device.getNeighbours(Device::Qubit(i)).size();
65+
if (degree > bestDegree) {
66+
bestDegree = degree;
67+
bestNode = i;
68+
}
69+
}
70+
71+
// BFS from bestNode to produce a device-qubit ordering.
72+
SmallVector<unsigned> bfsOrder;
73+
bfsOrder.reserve(numDeviceQubits);
74+
SmallVector<bool> visited(numDeviceQubits, false);
75+
SmallVector<unsigned> queue;
76+
queue.push_back(bestNode);
77+
visited[bestNode] = true;
78+
while (!queue.empty()) {
79+
SmallVector<unsigned> nextQueue;
80+
for (unsigned node : queue) {
81+
bfsOrder.push_back(node);
82+
for (auto neighbor : device.getNeighbours(Device::Qubit(node))) {
83+
if (!visited[neighbor.index]) {
84+
visited[neighbor.index] = true;
85+
nextQueue.push_back(neighbor.index);
86+
}
87+
}
88+
}
89+
queue = std::move(nextQueue);
90+
}
91+
92+
// Assign virtual qubits to physical qubits in BFS order.
93+
for (unsigned v = 0, end = placement.getNumVirtualQubits(); v < end; ++v) {
94+
unsigned p = (v < bfsOrder.size()) ? bfsOrder[v] : v;
95+
placement.map(Placement::VirtualQ(v), Placement::DeviceQ(p));
96+
}
97+
}
98+
5299
//===----------------------------------------------------------------------===//
53100
// Routing
54101
//===----------------------------------------------------------------------===//
@@ -843,6 +890,13 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase<MappingFunc> {
843890
return WalkResult::advance();
844891
});
845892

893+
// Count circuit qubits (user-provided, not auxiliary) before creating
894+
// auxiliary wires.
895+
unsigned numCircuitQubits = 0;
896+
for (auto &s : sources)
897+
if (s)
898+
++numCircuitQubits;
899+
846900
// Create or borrow auxillary qubits if needed. Place them after the last
847901
// allocated qubit.
848902
builder.setInsertionPointAfter(lastSource);
@@ -856,11 +910,19 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase<MappingFunc> {
856910
}
857911

858912
// Place
859-
Placement placement(sources.size(), deviceInstance->getNumQubits());
860-
identityPlacement(placement);
913+
Placement qPlacement(sources.size(), deviceInstance->getNumQubits());
914+
if (placement == "identity") {
915+
identityPlacement(qPlacement);
916+
} else if (placement == "bfs") {
917+
bfsPlacement(qPlacement, *deviceInstance);
918+
} else {
919+
func.emitWarning("Unknown placement strategy '" + placement +
920+
"', defaulting to bfs");
921+
bfsPlacement(qPlacement, *deviceInstance);
922+
}
861923

862924
// Route
863-
SabreRouter router(*deviceInstance, wireToVirtualQ, placement,
925+
SabreRouter router(*deviceInstance, wireToVirtualQ, qPlacement,
864926
extendedLayerSize, extendedLayerWeight, decayDelta,
865927
roundsDecayReset);
866928
router.route(*blocks.begin(), sources);
@@ -902,7 +964,7 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase<MappingFunc> {
902964
for (unsigned int v = 0; v < *highestIdentity + 1; v++)
903965
attrs[v] =
904966
IntegerAttr::get(builder.getIntegerType(64),
905-
placement.getPhy(Placement::VirtualQ(v)).index);
967+
qPlacement.getPhy(Placement::VirtualQ(v)).index);
906968

907969
func->setAttr("mapping_v2p", builder.getArrayAttr(attrs));
908970

@@ -919,7 +981,7 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase<MappingFunc> {
919981
measuredQubits.reserve(userQubitsMeasured.size());
920982
for (auto mq : userQubitsMeasured) {
921983
measuredQubits.emplace_back(
922-
mq, placement.getPhy(Placement::VirtualQ(mq)).index);
984+
mq, qPlacement.getPhy(Placement::VirtualQ(mq)).index);
923985
}
924986
// First sort the pairs according to the physical qubits.
925987
llvm::sort(measuredQubits,
@@ -961,6 +1023,7 @@ struct MappingPipelineOptions
9611023
DECLARE_SUB_OPTION(MappingFuncOptions, extendedLayerWeight);
9621024
DECLARE_SUB_OPTION(MappingFuncOptions, decayDelta);
9631025
DECLARE_SUB_OPTION(MappingFuncOptions, roundsDecayReset);
1026+
DECLARE_SUB_OPTION(MappingFuncOptions, placement);
9641027
PassOptions::Option<bool> nonComposable{*this, "raise-fatal-errors"};
9651028
};
9661029

@@ -989,6 +1052,7 @@ void registerMappingPipeline() {
9891052
setIt(funcOpts.decayDelta, opt.decayDelta);
9901053
setIt(funcOpts.roundsDecayReset, opt.roundsDecayReset);
9911054
setIt(funcOpts.nonComposable, opt.nonComposable);
1055+
setIt(funcOpts.placement, opt.placement);
9921056
pm.addNestedPass<func::FuncOp>(cudaq::opt::createMappingFunc(funcOpts));
9931057
});
9941058
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// ========================================================================== //
2+
// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. //
3+
// All rights reserved. //
4+
// //
5+
// This source code and the accompanying materials are made available under //
6+
// the terms of the Apache License 2.0 which accompanies this distribution. //
7+
// ========================================================================== //
8+
9+
// Test that auxiliary qubits are allocated for all device qubits and that
10+
// routing through them produces functionally correct circuits.
11+
12+
// 3-qubit circuit on star(5,0) with identity placement verifies auxiliary
13+
// wire allocation for all 5 device qubits (3 user + 2 auxiliary).
14+
// RUN: cudaq-opt --qubit-mapping="device=star(5,0) placement=identity" %s | CircuitCheck --up-to-mapping %s
15+
16+
// N=M scenario: 3 qubits on path(3), no auxiliaries needed.
17+
// RUN: cudaq-opt --qubit-mapping="device=path(3) placement=identity" %s | CircuitCheck --up-to-mapping %s
18+
19+
// 1-qubit circuit on a multi-qubit device compiles without error.
20+
// RUN: cudaq-opt --qubit-mapping="device=path(5) placement=identity" %s | CircuitCheck --up-to-mapping %s
21+
22+
quake.wire_set @wires[2147483647]
23+
24+
// 3-qubit circuit: CNOT chain requiring routing on non-trivial topologies.
25+
func.func @test_3q_cnot_chain() {
26+
%0 = quake.borrow_wire @wires[0] : !quake.wire
27+
%1 = quake.borrow_wire @wires[1] : !quake.wire
28+
%2 = quake.borrow_wire @wires[2] : !quake.wire
29+
%3:2 = quake.x [%0] %1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire)
30+
%4:2 = quake.x [%3#0] %2 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire)
31+
%5:2 = quake.x [%4#1] %3#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire)
32+
quake.return_wire %4#0 : !quake.wire
33+
quake.return_wire %5#0 : !quake.wire
34+
quake.return_wire %5#1 : !quake.wire
35+
return
36+
}
37+
38+
// 1-qubit circuit: single gate, no routing needed.
39+
func.func @test_1q_on_large_device() {
40+
%0 = quake.borrow_wire @wires[0] : !quake.wire
41+
%1 = quake.x %0 : (!quake.wire) -> !quake.wire
42+
quake.return_wire %1 : !quake.wire
43+
return
44+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// ========================================================================== //
2+
// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. //
3+
// All rights reserved. //
4+
// //
5+
// This source code and the accompanying materials are made available under //
6+
// the terms of the Apache License 2.0 which accompanies this distribution. //
7+
// ========================================================================== //
8+
9+
// Test BFS placement strategy on various topologies. Verifies that BFS
10+
// produces valid mapped circuits with mapping_v2p attributes.
11+
12+
// 3-qubit circuit on star(5,0) with BFS placement.
13+
// RUN: cudaq-opt --qubit-mapping="device=star(5,0)" %s | FileCheck --check-prefix=STAR %s
14+
15+
// 3-qubit circuit on path(5). Identity placement confirms correctness via
16+
// CircuitCheck. BFS FileCheck confirms structural output.
17+
// RUN: cudaq-opt --qubit-mapping="device=path(5) placement=identity" %s | CircuitCheck --up-to-mapping %s
18+
// RUN: cudaq-opt --qubit-mapping="device=path(5)" %s | FileCheck --check-prefix=PATH %s
19+
20+
// 3-qubit circuit on grid(3,3) with BFS placement.
21+
// RUN: cudaq-opt --qubit-mapping="device=grid(3,3)" %s | FileCheck --check-prefix=GRID %s
22+
23+
quake.wire_set @wires[2147483647]
24+
25+
func.func @test_bfs_placement() {
26+
%0 = quake.borrow_wire @wires[0] : !quake.wire
27+
%1 = quake.borrow_wire @wires[1] : !quake.wire
28+
%2 = quake.borrow_wire @wires[2] : !quake.wire
29+
%3:2 = quake.x [%0] %1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire)
30+
%4:2 = quake.x [%3#0] %2 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire)
31+
%5:2 = quake.x [%4#1] %3#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire)
32+
quake.return_wire %4#0 : !quake.wire
33+
quake.return_wire %5#0 : !quake.wire
34+
quake.return_wire %5#1 : !quake.wire
35+
return
36+
}
37+
38+
// Verify star(5,0) BFS produces valid mapped output.
39+
// STAR-LABEL: func.func @test_bfs_placement()
40+
// STAR-SAME: mapping_v2p
41+
// STAR: quake.borrow_wire @mapped_wireset
42+
43+
// Verify path(5) BFS produces valid mapped output.
44+
// PATH-LABEL: func.func @test_bfs_placement()
45+
// PATH-SAME: mapping_v2p
46+
// PATH: quake.borrow_wire @mapped_wireset
47+
48+
// Verify grid(3,3) BFS produces valid mapped output.
49+
// GRID-LABEL: func.func @test_bfs_placement()
50+
// GRID-SAME: mapping_v2p
51+
// GRID: quake.borrow_wire @mapped_wireset

test/Transforms/mapping_non_unitaries.qke

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@
66
// the terms of the Apache License 2.0 which accompanies this distribution. //
77
// ========================================================================== //
88

9-
// RUN: cudaq-opt --qubit-mapping=device=path\(5\) %s | CircuitCheck --up-to-mapping %s
10-
// RUN: cudaq-opt --qubit-mapping=device=ring\(5\) %s | CircuitCheck --up-to-mapping %s
11-
// RUN: cudaq-opt --qubit-mapping=device=star\(5,2\) %s | CircuitCheck --up-to-mapping %s
12-
// RUN: cudaq-opt --qubit-mapping=device=star\(5,0\) %s | CircuitCheck --up-to-mapping %s
13-
// RUN: cudaq-opt --qubit-mapping=device=grid\(3,3\) %s | CircuitCheck --up-to-mapping %s
14-
// RUN: cudaq-opt --qubit-mapping=device=grid\(1,5\) %s | CircuitCheck --up-to-mapping %s
15-
// RUN: cudaq-opt --qubit-mapping=device=grid\(5,1\) %s | CircuitCheck --up-to-mapping %s
16-
// RUN: cudaq-opt --qubit-mapping=device=path\(5\) %s | FileCheck %s
17-
// RUN: cudaq-opt --qubit-mapping=device=ring\(5\) %s | FileCheck %s
18-
// RUN: cudaq-opt --qubit-mapping=device=star\(5,2\) %s | FileCheck --check-prefix=STAR52 %s
19-
// RUN: cudaq-opt --qubit-mapping=device=star\(5,0\) %s | FileCheck --check-prefix=STAR50 %s
20-
// RUN: cudaq-opt --qubit-mapping=device=grid\(3,3\) %s | FileCheck %s
21-
// RUN: cudaq-opt --qubit-mapping=device=grid\(1,5\) %s | FileCheck %s
22-
// RUN: cudaq-opt --qubit-mapping=device=grid\(5,1\) %s | FileCheck %s
9+
// RUN: cudaq-opt --qubit-mapping="device=path(5) placement=identity" %s | CircuitCheck --up-to-mapping %s
10+
// RUN: cudaq-opt --qubit-mapping="device=ring(5) placement=identity" %s | CircuitCheck --up-to-mapping %s
11+
// RUN: cudaq-opt --qubit-mapping="device=star(5,2) placement=identity" %s | CircuitCheck --up-to-mapping %s
12+
// RUN: cudaq-opt --qubit-mapping="device=star(5,0) placement=identity" %s | CircuitCheck --up-to-mapping %s
13+
// RUN: cudaq-opt --qubit-mapping="device=grid(3,3) placement=identity" %s | CircuitCheck --up-to-mapping %s
14+
// RUN: cudaq-opt --qubit-mapping="device=grid(1,5) placement=identity" %s | CircuitCheck --up-to-mapping %s
15+
// RUN: cudaq-opt --qubit-mapping="device=grid(5,1) placement=identity" %s | CircuitCheck --up-to-mapping %s
16+
// RUN: cudaq-opt --qubit-mapping="device=path(5) placement=identity" %s | FileCheck %s
17+
// RUN: cudaq-opt --qubit-mapping="device=ring(5) placement=identity" %s | FileCheck %s
18+
// RUN: cudaq-opt --qubit-mapping="device=star(5,2) placement=identity" %s | FileCheck --check-prefix=STAR52 %s
19+
// RUN: cudaq-opt --qubit-mapping="device=star(5,0) placement=identity" %s | FileCheck --check-prefix=STAR50 %s
20+
// RUN: cudaq-opt --qubit-mapping="device=grid(3,3) placement=identity" %s | FileCheck %s
21+
// RUN: cudaq-opt --qubit-mapping="device=grid(1,5) placement=identity" %s | FileCheck %s
22+
// RUN: cudaq-opt --qubit-mapping="device=grid(5,1) placement=identity" %s | FileCheck %s
2323

2424
quake.wire_set @wires[2147483647]
2525

test/Transforms/mapping_phased_rx.qke

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// the terms of the Apache License 2.0 which accompanies this distribution. //
77
// ========================================================================== //
88

9-
// RUN: cudaq-opt '--qubit-mapping=device=star(5,2)' %s | FileCheck %s
9+
// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=identity' %s | FileCheck %s
1010

1111
module {
1212
quake.wire_set @wires[2147483647]

test/Transforms/mapping_qspan.qke

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
// the terms of the Apache License 2.0 which accompanies this distribution. //
77
// ========================================================================== //
88

9-
// RUN: cudaq-opt --qubit-mapping=device=path\(5\) --delay-measurements %s | FileCheck %s
10-
// RUN: cudaq-opt --qubit-mapping=device=ring\(5\) --delay-measurements %s | FileCheck --check-prefix RING %s
11-
// RUN: cudaq-opt --qubit-mapping=device=grid\(3,3\) --delay-measurements %s | FileCheck --check-prefix GRID %s
12-
// RUN: cudaq-opt --qubit-mapping=device=star\(5\) --delay-measurements %s | FileCheck --check-prefix STAR50 %s
13-
// RUN: cudaq-opt --qubit-mapping=device=star\(5,0\) --delay-measurements %s | FileCheck --check-prefix STAR50 %s
14-
// RUN: cudaq-opt --qubit-mapping=device=star\(5,2\) --delay-measurements %s | FileCheck --check-prefix STAR52 %s
9+
// RUN: cudaq-opt --qubit-mapping="device=path(5) placement=identity" --delay-measurements %s | FileCheck %s
10+
// RUN: cudaq-opt --qubit-mapping="device=ring(5) placement=identity" --delay-measurements %s | FileCheck --check-prefix RING %s
11+
// RUN: cudaq-opt --qubit-mapping="device=grid(3,3) placement=identity" --delay-measurements %s | FileCheck --check-prefix GRID %s
12+
// RUN: cudaq-opt --qubit-mapping="device=star(5) placement=identity" --delay-measurements %s | FileCheck --check-prefix STAR50 %s
13+
// RUN: cudaq-opt --qubit-mapping="device=star(5,0) placement=identity" --delay-measurements %s | FileCheck --check-prefix STAR50 %s
14+
// RUN: cudaq-opt --qubit-mapping="device=star(5,2) placement=identity" --delay-measurements %s | FileCheck --check-prefix STAR52 %s
1515

1616
module {
1717
quake.wire_set @wires[2147483647]

test/Transforms/mapping_with_meas-1.qke

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// the terms of the Apache License 2.0 which accompanies this distribution. //
77
// ========================================================================== //
88

9-
// RUN: cudaq-opt --qubit-mapping=device=path\(3\) %s | FileCheck %s
9+
// RUN: cudaq-opt --qubit-mapping="device=path(3) placement=identity" %s | FileCheck %s
1010
module {
1111
quake.wire_set @wires[2147483647]
1212
func.func @__nvqpp__mlirgen__function_foo._Z3foov() attributes {"cudaq-entrypoint", "cudaq-kernel", no_this} {

0 commit comments

Comments
 (0)