diff --git a/Cargo.lock b/Cargo.lock index 467a9b654f4d..bffc5d932528 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1500,6 +1500,8 @@ dependencies = [ "qiskit-circuit", "qiskit-quantum-info", "qiskit-transpiler", + "rand 0.9.2", + "rand_pcg 0.9.0", "smallvec", "thiserror 2.0.16", ] diff --git a/crates/cext/Cargo.toml b/crates/cext/Cargo.toml index 2f1e69ecbf68..4112c762c6d7 100644 --- a/crates/cext/Cargo.toml +++ b/crates/cext/Cargo.toml @@ -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" diff --git a/crates/cext/src/transpiler/passes/mod.rs b/crates/cext/src/transpiler/passes/mod.rs index 720e55ba5df1..4d5720dcc5ca 100644 --- a/crates/cext/src/transpiler/passes/mod.rs +++ b/crates/cext/src/transpiler/passes/mod.rs @@ -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; diff --git a/crates/cext/src/transpiler/passes/sabre_layout.rs b/crates/cext/src/transpiler/passes/sabre_layout.rs new file mode 100644 index 000000000000..16306dce3b0b --- /dev/null +++ b/crates/cext/src/transpiler/passes/sabre_layout.rs @@ -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, +} + +/// @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. +/// +/// 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. +/// +/// # 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, + 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, + ); + 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, + ))) +} diff --git a/crates/transpiler/src/passes/sabre/mod.rs b/crates/transpiler/src/passes/sabre/mod.rs index 8a6ba2241df3..e3e0f8c82cd9 100644 --- a/crates/transpiler/src/passes/sabre/mod.rs +++ b/crates/transpiler/src/passes/sabre/mod.rs @@ -12,7 +12,7 @@ mod dag; mod distance; -mod heuristic; +pub mod heuristic; mod layer; mod layout; mod neighbors; @@ -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) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(route::sabre_routing))?; m.add_wrapped(wrap_pyfunction!(layout::sabre_layout_and_routing))?; diff --git a/docs/cdoc/index.h b/docs/cdoc/index.h index 4bcef24d3198..dc7a4aebc291 100644 --- a/docs/cdoc/index.h +++ b/docs/cdoc/index.h @@ -45,3 +45,7 @@ /** * @defgroup QkTranspileLayout QkTranspileLayout */ + +/** + * @defgroup QkSabreLayoutOptions QkSabreLayoutOptions + */ diff --git a/docs/cdoc/index.rst b/docs/cdoc/index.rst index 9d62d5bdf5d8..700ebe13de64 100644 --- a/docs/cdoc/index.rst +++ b/docs/cdoc/index.rst @@ -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 diff --git a/docs/cdoc/qk-sabre-layout-options.rst b/docs/cdoc/qk-sabre-layout-options.rst new file mode 100644 index 000000000000..2002fcb64adf --- /dev/null +++ b/docs/cdoc/qk-sabre-layout-options.rst @@ -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: diff --git a/test/c/common.c b/test/c/common.c index a0dc33c37e66..c5d48ba9c240 100644 --- a/test/c/common.c +++ b/test/c/common.c @@ -11,7 +11,9 @@ // that they have been altered from the originals. #include "common.h" +#include #include +#include // 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 @@ -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; +} diff --git a/test/c/common.h b/test/c/common.h index 8b10c407ebcc..8c3f68ed81c1 100644 --- a/test/c/common.h +++ b/test/c/common.h @@ -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); diff --git a/test/c/test_sabre_layout.c b/test/c/test_sabre_layout.c new file mode 100644 index 000000000000..6bd662fe7f84 --- /dev/null +++ b/test/c/test_sabre_layout.c @@ -0,0 +1,216 @@ +// 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. + +#include "common.h" +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * Test running sabre layout that requires layout and routing + */ +int test_sabre_layout_applies_layout(void) { + int result = Ok; + + const uint32_t num_qubits = 5; + QkTarget *target = qk_target_new(num_qubits); + + qk_target_add_instruction(target, qk_target_entry_new(QkGate_U)); + QkTargetEntry *cx_entry = qk_target_entry_new(QkGate_CX); + for (uint32_t i = 0; i < num_qubits - 1; i++) { + uint32_t qargs[2] = {i, i + 1}; + double inst_error = 0.0090393 * (num_qubits - i); + double inst_duration = 0.020039; + qk_target_entry_add_property(cx_entry, qargs, 2, inst_duration, inst_error); + } + + qk_target_add_instruction(target, cx_entry); + + QkCircuit *qc = qk_circuit_new(5, 0); + for (uint32_t i = 0; i < qk_circuit_num_qubits(qc) - 1; i++) { + uint32_t qargs[2] = {0, i + 1}; + qk_circuit_gate(qc, QkGate_CX, qargs, NULL); + } + QkSabreLayoutOptions options = qk_sabre_layout_options_default(); + options.seed = 2025; + QkTranspileLayout *layout_result = + qk_transpiler_pass_standalone_sabre_layout(qc, target, &options); + + QkOpCounts op_counts = qk_circuit_count_ops(qc); + if (op_counts.len != 2) { + printf("More than 2 types of gates in circuit, circuit's instructions are:\n"); + print_circuit(qc); + result = EqualityError; + goto layout_cleanup; + } + for (int i = 0; i < op_counts.len; i++) { + int swap_gate = strcmp(op_counts.data[i].name, "swap"); + int cx_gate = strcmp(op_counts.data[i].name, "cx"); + if (cx_gate != 0 && swap_gate != 0) { + printf("Gate type of %s found in the circuit which isn't expected\n"); + result = EqualityError; + goto layout_cleanup; + } + if (swap_gate == 0 && op_counts.data[i].count != 2) { + printf("Unexpected number of swaps %d found in the circuit.\n"); + result = EqualityError; + goto layout_cleanup; + } + } + uint32_t expected_initial_layout[5] = {1, 0, 2, 3, 4}; + uint32_t result_initial_layout[5]; + qk_transpile_layout_initial_layout(layout_result, false, result_initial_layout); + for (uint32_t i = 0; i < 5; i++) { + if (result_initial_layout[i] != expected_initial_layout[i]) { + printf("Initial layout maps qubit %d to %d, expected %d instead\n", i, + result_initial_layout[i], expected_initial_layout[i]); + result = EqualityError; + goto layout_cleanup; + } + } + + uint32_t expected_permutation[5] = {0, 3, 1, 2, 4}; + uint32_t result_permutation[5]; + qk_transpile_layout_output_permutation(layout_result, result_permutation); + for (uint32_t i = 0; i < 5; i++) { + if (result_permutation[i] != expected_permutation[i]) { + printf("Output permutation maps qubit %d to %d, expected %d instead\n", i, + result_permutation[i], expected_permutation[i]); + result = EqualityError; + goto layout_cleanup; + } + } + QkCircuit *expected_circuit = qk_circuit_new(5, 0); + uint32_t qargs[2] = {1, 0}; + qk_circuit_gate(expected_circuit, QkGate_CX, qargs, NULL); + qargs[0] = 1; + qargs[1] = 2; + qk_circuit_gate(expected_circuit, QkGate_CX, qargs, NULL); + qargs[0] = 2; + qargs[1] = 1; + qk_circuit_gate(expected_circuit, QkGate_Swap, qargs, NULL); + qargs[0] = 2; + qargs[1] = 3; + qk_circuit_gate(expected_circuit, QkGate_CX, qargs, NULL); + qargs[0] = 3; + qargs[1] = 2; + qk_circuit_gate(expected_circuit, QkGate_Swap, qargs, NULL); + qargs[0] = 3; + qargs[1] = 4; + qk_circuit_gate(expected_circuit, QkGate_CX, qargs, NULL); + bool compare_result = compare_circuits(qc, expected_circuit); + if (!compare_result) { + result = EqualityError; + } + qk_circuit_free(expected_circuit); + +layout_cleanup: + qk_opcounts_free(op_counts); + qk_transpile_layout_free(layout_result); +circuit_cleanup: + qk_circuit_free(qc); +cleanup: + qk_target_free(target); + return result; +} + +/** + * Test running sabre layout that performs no transformation. + */ +int test_sabre_layout_no_swap(void) { + int result = Ok; + + const uint32_t num_qubits = 5; + QkTarget *target = qk_target_new(num_qubits); + qk_target_add_instruction(target, qk_target_entry_new(QkGate_U)); + QkTargetEntry *cx_entry = qk_target_entry_new(QkGate_CX); + for (uint32_t i = 0; i < num_qubits - 1; i++) { + uint32_t qargs[2] = {i, i + 1}; + double inst_error = 0.0090393 * (num_qubits - i); + double inst_duration = 0.020039; + + qk_target_entry_add_property(cx_entry, qargs, 2, inst_duration, inst_error); + } + + qk_target_add_instruction(target, cx_entry); + + QkCircuit *qc = qk_circuit_new(5, 0); + for (uint32_t i = 0; i < qk_circuit_num_qubits(qc) - 1; i++) { + uint32_t qargs[2] = {i, i + 1}; + for (uint32_t j = 0; j < i + 1; j++) { + qk_circuit_gate(qc, QkGate_CX, qargs, NULL); + } + } + QkSabreLayoutOptions options = qk_sabre_layout_options_default(); + options.seed = 2025; + QkTranspileLayout *layout_result = + qk_transpiler_pass_standalone_sabre_layout(qc, target, &options); + QkCircuit *expected_circuit = qk_circuit_new(5, 0); + for (uint32_t i = 0; i < qk_circuit_num_qubits(qc) - 1; i++) { + uint32_t qargs[2] = {i, i + 1}; + for (uint32_t j = 0; j < i + 1; j++) { + qk_circuit_gate(expected_circuit, QkGate_CX, qargs, NULL); + } + } + bool circuit_eq = compare_circuits(qc, expected_circuit); + if (!circuit_eq) { + result = EqualityError; + goto layout_cleanup; + } + uint32_t expected_initial_layout[5] = {0, 1, 2, 3, 4}; + uint32_t result_initial_layout[5]; + qk_transpile_layout_initial_layout(layout_result, false, result_initial_layout); + for (uint32_t i = 0; i < 5; i++) { + if (result_initial_layout[i] != expected_initial_layout[i]) { + printf("Initial layout maps qubit %d to %d, expected %d instead\n", i, + result_initial_layout[i], expected_initial_layout[i]); + result = EqualityError; + goto layout_cleanup; + } + } + uint32_t expected_permutation[5] = {0, 1, 2, 3, 4}; + uint32_t result_permutation[5]; + qk_transpile_layout_output_permutation(layout_result, result_permutation); + for (uint32_t i = 0; i < 5; i++) { + if (result_permutation[i] != expected_permutation[i]) { + printf("Output permutation maps qubit %d to %d, expected %d instead\n", i, + result_permutation[i], expected_permutation[i]); + result = EqualityError; + goto layout_cleanup; + } + } + +layout_cleanup: + qk_circuit_free(expected_circuit); + qk_transpile_layout_free(layout_result); +circuit_cleanup: + qk_circuit_free(qc); +cleanup: + qk_target_free(target); + return result; +} + +int test_sabre_layout(void) { + int num_failed = 0; + num_failed += RUN_TEST(test_sabre_layout_no_swap); + num_failed += RUN_TEST(test_sabre_layout_applies_layout); + + fflush(stderr); + fprintf(stderr, "=== Number of failed subtests: %i\n", num_failed); + + return num_failed; +}