Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
09c0b7c
Add SabreLayout to the C API
mtreinish Jul 8, 2025
9435002
Improve testing
mtreinish Jul 9, 2025
28f3737
Merge remote-tracking branch 'origin/main' into sabre_at_c
mtreinish Aug 18, 2025
94b80bc
Fix tests
mtreinish Aug 18, 2025
ac2792a
Fix docs
mtreinish Aug 19, 2025
b44e527
Merge branch 'main' into sabre_at_c
mtreinish Aug 19, 2025
b26658f
Finish API docs for standalone function
mtreinish Aug 19, 2025
127d6d7
Merge remote-tracking branch 'origin/main' into sabre_at_c
mtreinish Aug 19, 2025
0aa1f10
Pivot interface to use TranspileLayout
mtreinish Aug 19, 2025
05725ba
Remove stale references to QkSabreLayoutResult in docs
mtreinish Aug 19, 2025
6d95b87
Fix test memory leaks
mtreinish Aug 20, 2025
dcc672b
Make input to pass function an options struct
mtreinish Aug 20, 2025
be8e750
Set number of input qubits correctly in output layout
mtreinish Aug 20, 2025
6bf9a6e
Move helper functions to common.c
mtreinish Aug 20, 2025
87e98f1
Use a stack variable instructions in compare_circuits()
mtreinish Aug 20, 2025
cb8960d
Fix output_permutation in the return
mtreinish Aug 20, 2025
6f5ffe5
Merge branch 'main' into sabre_at_c
mtreinish Aug 20, 2025
58a6e45
Merge branch 'main' into sabre_at_c
mtreinish Aug 21, 2025
8d54e2c
Apply suggestions from code review
mtreinish Aug 21, 2025
6209b8d
Update docstring for circuit arg
mtreinish Aug 21, 2025
59d316d
Don't use PyErr debug or display
mtreinish Aug 21, 2025
4ef73e4
Add missing newlines to test failure strings
mtreinish Aug 21, 2025
fd4e06d
Update error messages about output permutation test failures
mtreinish Aug 21, 2025
df6b2e4
Correct permutation variable names
jakelishman Aug 21, 2025
5775678
Correct copyright header
jakelishman Aug 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/cext/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ smallvec.workspace = true
ndarray.workspace = true
nalgebra.workspace = true
hashbrown.workspace = true
rand.workspace = true
rand_pcg.workspace = true

[build-dependencies]
cbindgen = "0.29"
Expand Down
1 change: 1 addition & 0 deletions crates/cext/src/transpiler/passes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ pub mod elide_permutations;
pub mod gate_direction;
pub mod remove_diagonal_gates_before_measure;
pub mod remove_identity_equiv;
pub mod sabre_layout;
pub mod vf2;
164 changes: 164 additions & 0 deletions crates/cext/src/transpiler/passes/sabre_layout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2025
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

use crate::pointers::{const_ptr_as_ref, mut_ptr_as_ref};
use rand::{Rng, SeedableRng};
use rand_pcg::Pcg64Mcg;

use qiskit_circuit::circuit_data::CircuitData;
use qiskit_circuit::converters::dag_to_circuit;
use qiskit_circuit::dag_circuit::DAGCircuit;
use qiskit_circuit::{PhysicalQubit, Qubit};
use qiskit_transpiler::passes::sabre::heuristic;
use qiskit_transpiler::passes::sabre::sabre_layout_and_routing;
use qiskit_transpiler::target::Target;
use qiskit_transpiler::transpile_layout::TranspileLayout;

/// The options for running ``qk_transpiler_pass_standalone_sabre_layout``. This struct is used
/// as an input to control the behavior of the layout and routing algorithms.
#[repr(C)]
pub struct QkSabreLayoutOptions {
/// The number of forward-backward iterations in the sabre routing algorithm
max_iterations: usize,
/// The number of trials to run of the sabre routing algorithm for each iteration. When > 1 the
/// trial that routing trial that results in the output with the fewest swap gates will be
/// selected.
num_swap_trials: usize,
/// The number of random layout trials to run. The trial that results in the output with the
/// fewest swap gates will be selected.
num_random_trials: usize,
/// A seed value for the pRNG used internally.
seed: u64,
}
Comment on lines +28 to +41

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In VF2 land I went for making the internals of the struct private to let us extend it in an API-safe way, but I think it's good to try both ways and see which feels better, while we're still uncommitted to a stable interface - let's leave this as-is.


/// @ingroup QkSabreLayoutOptions
///
/// Build a default sabre layout options object. This builds a sabre layout with ``max_iterations``
/// set to 4, both ``num_swap_trials`` and ``num_random_trials`` set to 20, and the seed selected
/// by a RNG seeded from system entropy.
#[no_mangle]
pub extern "C" fn qk_sabre_layout_options_default() -> QkSabreLayoutOptions {
QkSabreLayoutOptions {
max_iterations: 4,
num_swap_trials: 20,
num_random_trials: 20,
seed: Pcg64Mcg::from_os_rng().random(),
}
}

/// @ingroup QkTranspilerPasses
/// Run the SabreLayout transpiler pass on a circuit.
///
/// The SabreLayout pass chooses a layout via an iterative bidirectional routing of the input
/// circuit.
///
/// Starting with a random initial Layout, the algorithm does a full routing of the circuit to end up with a final_layout.
/// This final_layout is then used as the initial_layout for routing the reverse circuit. The algorithm iterates a number
/// of times until it finds an initial_layout that reduces full routing cost.
///
/// This method exploits the reversibility of quantum circuits, and tries to include global circuit information in the
/// choice of initial_layout.
///
/// This pass will run both layout and routing and will transform the circuit so that the layout is applied to the input
/// (meaning that the output circuit will have ancilla qubits allocated for unused qubits on the coupling map and the
/// qubits will be reordered to match the mapped physical qubits) and then routing will be applied. This is done because
/// the pass will run parallel seed trials with different random seeds for selecting the random initial layout and then
/// selecting the routed output which results in the least number of swap gates needed. This final
/// swap calculation is the same as performing a final routing, so it's more efficient to apply it
/// after computing it.
Comment thread
jakelishman marked this conversation as resolved.
///
/// This function is multithreaded and will launch a thread pool with threads equal to the number
/// of CPUs by default. You can tune the number of threads with the ``RAYON_NUM_THREADS``
/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would limit the thread pool
/// to 4 threads.
Comment on lines +79 to +82

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In the future, we're almost certainly going to want to expose some sort of per-function and per-process "threading configuration" objects into C space, given the target audience.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, I am putting these comments in the docstrings because I've already had discussions about calling some of the C API functions in an OpenMP context and wanted to make sure we were at least documenting how it worked now. But I agree we'll probably want to expose a structure maybe as part of TranspileOptions to control the threading behavior.

///
/// # References
///
/// [1] Henry Zou and Matthew Treinish and Kevin Hartman and Alexander Ivrii and Jake Lishman.
/// "LightSABRE: A Lightweight and Enhanced SABRE Algorithm"
/// [arXiv:2409.08368](https://doi.org/10.48550/arXiv.2409.08368)
///
/// [2] Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem
/// for NISQ-era quantum devices." ASPLOS 2019.
/// [`arXiv:1809.02573](https://arxiv.org/pdf/1809.02573.pdf)
///
/// @param circuit A pointer to the circuit to run SabreLayout on. The circuit
/// is modified in place and the original circuit's allocations are freed by this function.
/// @param target A pointer to the target to run SabreLayout on
/// @param options A pointer to the options for SabreLayout
///
/// @return The transpile layout that describes the layout and output permutation caused
/// by the pass
///
/// # Safety
///
/// Behavior is undefined if ``circuit`` or ``target`` is not a valid, non-null pointer to a ``QkCircuit`` and ``QkTarget``.
#[no_mangle]
#[cfg(feature = "cbinding")]
pub unsafe extern "C" fn qk_transpiler_pass_standalone_sabre_layout(
circuit: *mut CircuitData,
Comment thread
mtreinish marked this conversation as resolved.
target: *const Target,
options: *const QkSabreLayoutOptions,
) -> *mut TranspileLayout {
// SAFETY: Per documentation, the pointer is non-null and aligned.
let circuit = unsafe { mut_ptr_as_ref(circuit) };
let target = unsafe { const_ptr_as_ref(target) };
let options = unsafe { const_ptr_as_ref(options) };
let mut dag = DAGCircuit::from_circuit_data(circuit, false, None, None, None, None)
.unwrap_or_else(|_| panic!("Internal circuit to DAG conversion failed."));
let heuristic = heuristic::Heuristic::new(
Some(heuristic::BasicHeuristic::new(
1.0,
heuristic::SetScaling::Constant,
)),
Some(heuristic::LookaheadHeuristic::new(
0.5,
20,
heuristic::SetScaling::Size,
)),
Some(heuristic::DecayHeuristic::new(0.001, 5)),
Some(10 * target.num_qubits.unwrap() as usize),
1e-10,
Comment thread
jakelishman marked this conversation as resolved.
);
let (result, initial_layout, final_layout) = sabre_layout_and_routing(
&mut dag,
target,
&heuristic,
options.max_iterations,
options.num_swap_trials,
options.num_random_trials,
Some(options.seed),
Vec::new(),
false,
)
.unwrap_or_else(|_| panic!("Sabre layout failed."));
let out_circuit = dag_to_circuit(&result, false)
.unwrap_or_else(|_| panic!("Internal DAG to circuit conversion failed"));
let num_input_qubits = circuit.num_qubits() as u32;
*circuit = out_circuit;
let out_permutation = (0..result.num_qubits() as u32)
.map(|ref q| {
Qubit(
final_layout
.virtual_to_physical(initial_layout.physical_to_virtual(PhysicalQubit(*q)))
.0,
)
})
.collect();

Box::into_raw(Box::new(TranspileLayout::new(
Some(initial_layout),
Some(out_permutation),
result.qubits().objects().clone(),
num_input_qubits,
)))
}
4 changes: 3 additions & 1 deletion crates/transpiler/src/passes/sabre/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

mod dag;
mod distance;
mod heuristic;
pub mod heuristic;
mod layer;
mod layout;
mod neighbors;
Expand All @@ -21,6 +21,8 @@ mod route;
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

pub use layout::sabre_layout_and_routing;

pub fn sabre(m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(route::sabre_routing))?;
m.add_wrapped(wrap_pyfunction!(layout::sabre_layout_and_routing))?;
Expand Down
4 changes: 4 additions & 0 deletions docs/cdoc/index.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@
/**
* @defgroup QkTranspileLayout QkTranspileLayout
*/

/**
* @defgroup QkSabreLayoutOptions QkSabreLayoutOptions
*/
1 change: 1 addition & 0 deletions docs/cdoc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ results transpiling circuits from Python via the C API.
qk-transpile-layout
qk-transpiler-passes
qk-vf2-layout-result
qk-sabre-layout-options
18 changes: 18 additions & 0 deletions docs/cdoc/qk-sabre-layout-options.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
====================
QkSabreLayoutOptions
====================

When running the ``qk_transpiler_pass_standalone_sabre_layout`` function this type defines the options for running the pass.

Data Types
==========

.. doxygenstruct:: QkSabreLayoutOptions
:members:

Functions
=========

.. doxygengroup:: QkSabreLayoutOptions
:members:
:content-only:
101 changes: 101 additions & 0 deletions test/c/common.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
// that they have been altered from the originals.

#include "common.h"
#include <qiskit.h>
#include <stdio.h>
#include <string.h>

// A function to run a test function of a given name. This function will also
// post-process the returned `TestResult` to product a minimal info message for
Expand All @@ -36,3 +38,102 @@ int run(const char *name, int (*test_function)(void)) {

return did_fail;
}

void print_circuit(const QkCircuit *qc) {
size_t num_instructions = qk_circuit_num_instructions(qc);
QkCircuitInstruction inst;
for (size_t i = 0; i < num_instructions; i++) {
qk_circuit_get_instruction(qc, i, &inst);
printf("%s: qubits: (", inst.name);
for (uint32_t j = 0; j < inst.num_qubits; j++) {
printf("%d,", inst.qubits[j]);
}
printf(")");
uint32_t num_clbits = inst.num_clbits;
if (num_clbits > 0) {
printf(", clbits: (");
for (uint32_t j = 0; j < num_clbits; j++) {
printf("%d,", inst.clbits[j]);
}
printf(")");
}
printf("\n");
qk_circuit_instruction_clear(&inst);
}
}

bool compare_circuits(const QkCircuit *res, const QkCircuit *expected) {
if (qk_circuit_num_instructions(res) != qk_circuit_num_instructions(expected)) {
printf("Number of instructions in circuit is mismatched\n");
return false;
}
QkCircuitInstruction res_inst;
QkCircuitInstruction expected_inst;
for (size_t i = 0; i < qk_circuit_num_instructions(res); i++) {
qk_circuit_get_instruction(res, i, &res_inst);
qk_circuit_get_instruction(expected, i, &expected_inst);
int result = strcmp(res_inst.name, expected_inst.name);
if (result != 0) {
printf("Gate %d have different gates %s was found and expected %s\n", i, res_inst.name,
expected_inst.name);
qk_circuit_instruction_clear(&res_inst);
qk_circuit_instruction_clear(&expected_inst);
return false;
}
if (res_inst.num_qubits != expected_inst.num_qubits) {
printf("Gate %d have different number of qubits %d was found and expected %d\n", i,
res_inst.num_qubits, expected_inst.num_qubits);
qk_circuit_instruction_clear(&res_inst);
qk_circuit_instruction_clear(&expected_inst);
return false;
}
for (uint32_t j = 0; j < res_inst.num_qubits; j++) {
if (res_inst.qubits[j] != expected_inst.qubits[j]) {
printf("Qubit %d for gate %d are different %d was found and expected %d\n", j, i,
res_inst.qubits[j] != expected_inst.qubits[j]);
printf("Expected circuit instructions:\n");
print_circuit(expected);
printf("Result circuit:\n");
print_circuit(res);
qk_circuit_instruction_clear(&res_inst);
qk_circuit_instruction_clear(&expected_inst);
return false;
}
}
if (res_inst.num_clbits != expected_inst.num_clbits) {
printf("Gate %d have different number of clbits %d was found and expected %d\n", i,
res_inst.num_clbits, expected_inst.num_clbits);
qk_circuit_instruction_clear(&res_inst);
qk_circuit_instruction_clear(&expected_inst);
return false;
}
for (uint32_t j = 0; j < res_inst.num_clbits; j++) {
if (res_inst.clbits[j] != expected_inst.clbits[j]) {
printf("Clbit %d for gate %d are different %d was found and expected %d\n", j, i,
res_inst.clbits[j] != expected_inst.clbits[j]);
qk_circuit_instruction_clear(&res_inst);
qk_circuit_instruction_clear(&expected_inst);
return false;
}
}
if (res_inst.num_params != expected_inst.num_params) {
printf("Gate %d have different number of params %d was found and expected %d\n", i,
res_inst.num_params, expected_inst.num_params);
qk_circuit_instruction_clear(&res_inst);
qk_circuit_instruction_clear(&expected_inst);
return false;
}
for (uint32_t j = 0; j < res_inst.num_params; j++) {
if (res_inst.params[j] != expected_inst.params[j]) {
printf("Parameter %d for gate %d are different %d was found and expected %d\n", j,
i, res_inst.params[j] != expected_inst.params[j]);
qk_circuit_instruction_clear(&res_inst);
qk_circuit_instruction_clear(&expected_inst);
return false;
}
}
qk_circuit_instruction_clear(&res_inst);
qk_circuit_instruction_clear(&expected_inst);
}
return true;
}
3 changes: 3 additions & 0 deletions test/c/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ enum TestResult {
// post-process the returned `TestResult` to product a minimal info message for
// the developer running the test suite.
int run(const char *name, int (*test_function)(void));

bool compare_circuits(const QkCircuit *res, const QkCircuit *expected);
void print_circuit(const QkCircuit *qc);
Loading
Loading