From 3c00fdd4dd7b021a12d523ebd07f769705b975ab Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 27 Sep 2024 19:21:06 -0400 Subject: [PATCH] Initial: Move the rest of the `BasisTranslator` to Rust. Fixes Port `BasisTranslator` to Rust #12246 This is the final act of the efforts to move the `BasisTranslator` transpiler pass into Rust. With many of the parts of this pass already living in Rust, the following commits attempt to bring in the final changes to allow complete operation of this pass in the Rust space (with of course some interaction with Python.) Methodology: The way this works is by keeping the original `BasisTranslator` python class, and have it store the rust-space counterpart (now called `CoreBasisTranslator`) which will perform all of the operations leveraging the existent Rust API's available for the `Target` (#12292), `EquivalenceLibrary`(#12585) and the `BasisTranslator` methods `basis_search` (#12811) and `compose_transforms`(#13137). All of the inner methods will have private visibility and will not be accessible to `Python` as they're intended to be internal by design. By removing the extra layers of conversion we should be seeing a considerable speed-up, alongside all of the other incremental improvements we have made. Changes: - Add the pyo3 class/struct `BasisTranslator` that will contain allof the main data used by the transpiler pass to perform its operation. - Convert the `target_basis` into a set manually from python before sending it into the Rust space. - Remove the exposure of `basis_search` and `compose_transforms` to python. - Change `basis_search` so that it accepts references to `HashSet` instances instead of accepting a `HashSet<&str>` instance. - Change inner method's visibility for `basis_search` and `compose_transform` modules in rust. - Expose the exception imports from `Target` to the `accelerate` crate. - Expose `DAGCircuit::copy_empty_like` to the rest of the crates. - Remove all of the unused imports in the Python-side `BasisTranslator`. Blockers: - [ ] #12811 --- .../basis/basis_translator/basis_search.rs | 23 - .../basis_translator/compose_transforms.rs | 10 - .../src/basis/basis_translator/mod.rs | 906 +++++++++++++++++- .../accelerate/src/target_transpiler/mod.rs | 2 +- .../passes/basis/basis_translator.py | 339 +------ 5 files changed, 912 insertions(+), 368 deletions(-) diff --git a/crates/accelerate/src/basis/basis_translator/basis_search.rs b/crates/accelerate/src/basis/basis_translator/basis_search.rs index 2810765db741..4686ba9c4c3f 100644 --- a/crates/accelerate/src/basis/basis_translator/basis_search.rs +++ b/crates/accelerate/src/basis/basis_translator/basis_search.rs @@ -13,7 +13,6 @@ use std::cell::RefCell; use hashbrown::{HashMap, HashSet}; -use pyo3::prelude::*; use crate::equivalence::{EdgeData, Equivalence, EquivalenceLibrary, Key, NodeData}; use qiskit_circuit::operations::Operation; @@ -23,28 +22,6 @@ use rustworkx_core::traversal::{dijkstra_search, DijkstraEvent}; use super::compose_transforms::{BasisTransformIn, GateIdentifier}; -/// Search for a set of transformations from source_basis to target_basis. -/// Args: -/// equiv_lib (EquivalenceLibrary): Source of valid translations -/// source_basis (Set[Tuple[gate_name: str, gate_num_qubits: int]]): Starting basis. -/// target_basis (Set[gate_name: str]): Target basis. -/// -/// Returns: -/// Optional[List[Tuple[gate, equiv_params, equiv_circuit]]]: List of (gate, -/// equiv_params, equiv_circuit) tuples tuples which, if applied in order -/// will map from source_basis to target_basis. Returns None if no path -/// was found. -#[pyfunction] -#[pyo3(name = "basis_search")] -pub(crate) fn py_basis_search( - py: Python, - equiv_lib: &mut EquivalenceLibrary, - source_basis: HashSet, - target_basis: HashSet, -) -> PyObject { - basis_search(equiv_lib, &source_basis, &target_basis).into_py(py) -} - type BasisTransforms = Vec<(GateIdentifier, BasisTransformIn)>; /// Search for a set of transformations from source_basis to target_basis. /// diff --git a/crates/accelerate/src/basis/basis_translator/compose_transforms.rs b/crates/accelerate/src/basis/basis_translator/compose_transforms.rs index e4498af0f8a5..b1366c30bf2f 100644 --- a/crates/accelerate/src/basis/basis_translator/compose_transforms.rs +++ b/crates/accelerate/src/basis/basis_translator/compose_transforms.rs @@ -30,16 +30,6 @@ pub type GateIdentifier = (String, u32); pub type BasisTransformIn = (SmallVec<[Param; 3]>, CircuitFromPython); pub type BasisTransformOut = (SmallVec<[Param; 3]>, DAGCircuit); -#[pyfunction(name = "compose_transforms")] -pub(super) fn py_compose_transforms( - py: Python, - basis_transforms: Vec<(GateIdentifier, BasisTransformIn)>, - source_basis: HashSet, - source_dag: &DAGCircuit, -) -> PyResult> { - compose_transforms(py, &basis_transforms, &source_basis, source_dag) -} - pub(super) fn compose_transforms<'a>( py: Python, basis_transforms: &'a [(GateIdentifier, BasisTransformIn)], diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs index 18970065267c..727d041a1947 100644 --- a/crates/accelerate/src/basis/basis_translator/mod.rs +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -10,14 +10,914 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use compose_transforms::BasisTransformIn; +use compose_transforms::BasisTransformOut; +use compose_transforms::GateIdentifier; + +use basis_search::basis_search; +use compose_transforms::compose_transforms; +use hashbrown::{HashMap, HashSet}; +use itertools::Itertools; +use pyo3::intern; use pyo3::prelude::*; -pub mod basis_search; +mod basis_search; mod compose_transforms; +use pyo3::types::{IntoPyDict, PyComplex, PyDict, PyTuple}; +use pyo3::PyTypeInfo; +use qiskit_circuit::circuit_instruction::OperationFromPython; +use qiskit_circuit::converters::circuit_to_dag; +use qiskit_circuit::imports::DAG_TO_CIRCUIT; +use qiskit_circuit::imports::PARAMETER_EXPRESSION; +use qiskit_circuit::operations::Param; +use qiskit_circuit::packed_instruction::PackedInstruction; +use qiskit_circuit::{ + circuit_data::CircuitData, + dag_circuit::{DAGCircuit, NodeType}, + operations::{Operation, OperationRef}, +}; +use qiskit_circuit::{Clbit, Qubit}; +use smallvec::SmallVec; + +use crate::equivalence::EquivalenceLibrary; +use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::exceptions::TranspilerError; +use crate::target_transpiler::{Qargs, Target}; + +#[pyclass( + name = "CoreBasisTranslator", + module = "qiskit._accelerate.basis.basis_translator" +)] +pub struct BasisTranslator { + equiv_lib: EquivalenceLibrary, + target_basis: Option>, + target: Option, + non_global_operations: Option>, + qargs_with_non_global_operation: HashMap, HashSet>, + min_qubits: usize, +} + +type InstMap = HashMap; +type ExtraInstructionMap<'a> = HashMap<&'a Option, InstMap>; + +#[pymethods] +impl BasisTranslator { + #[new] + fn new( + equiv_lib: EquivalenceLibrary, + min_qubits: usize, + target_basis: Option>, + mut target: Option, + ) -> Self { + let mut non_global_operations = None; + let mut qargs_with_non_global_operation: HashMap, HashSet> = + HashMap::default(); + + if let Some(target) = target.as_mut() { + let non_global_from_target: HashSet = target + .get_non_global_operation_names(false) + .unwrap_or_default() + .iter() + .cloned() + .collect(); + for gate in &non_global_from_target { + for qarg in target[gate].keys() { + qargs_with_non_global_operation + .entry(qarg.cloned()) + .and_modify(|set| { + set.insert(gate.clone()); + }) + .or_insert(HashSet::from_iter([gate.clone()])); + } + } + non_global_operations = Some(non_global_from_target); + } + Self { + equiv_lib, + target_basis, + target, + non_global_operations, + qargs_with_non_global_operation, + min_qubits, + } + } + + fn run(&mut self, py: Python<'_>, dag: DAGCircuit) -> PyResult { + if self.target_basis.is_none() && self.target.is_none() { + return Ok(dag); + } + + let basic_instrs: HashSet; + let mut source_basis: HashSet = HashSet::default(); + let mut target_basis: HashSet; + let mut qargs_local_source_basis: HashMap, HashSet> = + HashMap::default(); + if let Some(target) = self.target.as_ref() { + basic_instrs = ["barrier", "snapshot", "store"] + .into_iter() + .map(|x| x.to_string()) + .collect(); + let non_global_str: HashSet<&str> = + if let Some(operations) = self.non_global_operations.as_ref() { + operations.iter().map(|x| x.as_str()).collect() + } else { + HashSet::default() + }; + let target_keys = target.keys().collect::>(); + target_basis = target_keys + .difference(&non_global_str) + .map(|x| x.to_string()) + .collect(); + self.extract_basis_target(py, &dag, &mut source_basis, &mut qargs_local_source_basis)?; + } else { + basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay", "store"] + .into_iter() + .map(|x| x.to_string()) + .collect(); + source_basis = self.extract_basis(py, &dag)?; + target_basis = self.target_basis.clone().unwrap(); + } + target_basis = target_basis + .union(&basic_instrs) + .map(|x| x.to_string()) + .collect(); + // If the source basis is a subset of the target basis and we have no circuit + // instructions on qargs that have non-global operations there is nothing to + // translate and we can exit early. + let source_basis_names: HashSet = + source_basis.iter().map(|x| x.0.clone()).collect(); + if source_basis_names.is_subset(&target_basis) && qargs_local_source_basis.is_empty() { + return Ok(dag); + } + let basis_transforms = basis_search(&mut self.equiv_lib, &source_basis, &target_basis); + let mut qarg_local_basis_transforms: HashMap< + Option, + Vec<(GateIdentifier, BasisTransformIn)>, + > = HashMap::default(); + for (qarg, local_source_basis) in qargs_local_source_basis.iter() { + // For any multiqubit operation that contains a subset of qubits that + // has a non-local operation, include that non-local operation in the + // search. This matches with the check we did above to include those + // subset non-local operations in the check here. + let mut expanded_target = target_basis.clone(); + if qarg.as_ref().is_some_and(|qarg| qarg.len() > 1) { + let qarg_as_set: HashSet = + HashSet::from_iter(qarg.as_ref().unwrap().iter().copied()); + for (non_local_qarg, local_basis) in self.qargs_with_non_global_operation.iter() { + if let Some(non_local_qarg) = non_local_qarg { + let non_local_qarg_as_set = + HashSet::from_iter(non_local_qarg.iter().copied()); + if qarg_as_set.is_superset(&non_local_qarg_as_set) { + expanded_target = expanded_target.union(local_basis).cloned().collect(); + } + } + } + } else { + expanded_target = expanded_target + .union(&self.qargs_with_non_global_operation[qarg]) + .cloned() + .collect(); + } + let local_basis_transforms = + basis_search(&mut self.equiv_lib, local_source_basis, &expanded_target); + if let Some(local_basis_transforms) = local_basis_transforms { + qarg_local_basis_transforms.insert(qarg.clone(), local_basis_transforms); + } else { + return Err(TranspilerError::new_err(format!( + "Unable to translate the operations in the circuit: \ + {:?} to the backend's (or manually specified) target \ + basis: {:?}. This likely means the target basis is not universal \ + or there are additional equivalence rules needed in the EquivalenceLibrary being \ + used. For more details on this error see: \ + https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes.\ + BasisTranslator#translation-errors", + local_source_basis + .iter() + .map(|x| x.0.as_str()) + .collect_vec(), + &expanded_target + ))); + } + } + + let Some(basis_transforms) = basis_transforms else { + return Err(TranspilerError::new_err(format!( + "Unable to translate the operations in the circuit: \ + {:?} to the backend's (or manually specified) target \ + basis: {:?}. This likely means the target basis is not universal \ + or there are additional equivalence rules needed in the EquivalenceLibrary being \ + used. For more details on this error see: \ + https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes. \ + BasisTranslator#translation-errors", + source_basis.iter().map(|x| x.0.as_str()).collect_vec(), + &target_basis + ))); + }; + + let instr_map: InstMap = compose_transforms(py, &basis_transforms, &source_basis, &dag)?; + let extra_inst_map: ExtraInstructionMap = qarg_local_basis_transforms + .iter() + .map(|(qarg, transform)| -> PyResult<_> { + Ok(( + qarg, + compose_transforms(py, transform, &qargs_local_source_basis[qarg], &dag)?, + )) + }) + .collect::>()?; + + let (out_dag, _) = + self.apply_translation(py, &dag, &target_basis, &instr_map, &extra_inst_map)?; + Ok(out_dag) + } + + fn __getstate__(slf: PyRef) -> PyResult> { + let state = PyDict::new_bound(slf.py()); + state.set_item( + intern!(slf.py(), "equiv_lib"), + slf.equiv_lib.clone().into_py(slf.py()), + )?; + state.set_item( + intern!(slf.py(), "target_basis"), + slf.target_basis.clone().into_py(slf.py()), + )?; + state.set_item( + intern!(slf.py(), "target"), + slf.target.clone().into_py(slf.py()), + )?; + state.set_item( + intern!(slf.py(), "non_global_operations"), + slf.non_global_operations.clone().into_py(slf.py()), + )?; + state.set_item( + intern!(slf.py(), "qargs_with_non_global_operation"), + slf.qargs_with_non_global_operation + .clone() + .into_py(slf.py()), + )?; + state.set_item(intern!(slf.py(), "min_qubits"), slf.min_qubits)?; + Ok(state) + } + + fn __setstate__(mut slf: PyRefMut, state: Bound) -> PyResult<()> { + slf.equiv_lib = state + .get_item(intern!(slf.py(), "equiv_lib"))? + .unwrap() + .extract()?; + slf.target_basis = state + .get_item(intern!(slf.py(), "target_basis"))? + .unwrap() + .extract()?; + slf.target = state + .get_item(intern!(slf.py(), "target"))? + .unwrap() + .extract()?; + slf.non_global_operations = state + .get_item(intern!(slf.py(), "non_global_operations"))? + .unwrap() + .extract()?; + slf.qargs_with_non_global_operation = state + .get_item(intern!(slf.py(), "qargs_with_non_global_operation"))? + .unwrap() + .extract()?; + slf.min_qubits = state + .get_item(intern!(slf.py(), "min_qubits"))? + .unwrap() + .extract()?; + Ok(()) + } + + fn __getnewargs__(slf: PyRef) -> PyResult { + let py = slf.py(); + Ok(( + slf.equiv_lib.clone(), + slf.min_qubits, + slf.target_basis.clone(), + slf.target.clone(), + ) + .into_py(py)) + } +} + +impl BasisTranslator { + /// Method that extracts all non-calibrated gate instances identifiers from a DAGCircuit. + fn extract_basis(&self, py: Python, circuit: &DAGCircuit) -> PyResult> { + let mut basis = HashSet::default(); + // Recurse for DAGCircuit + fn recurse_dag( + py: Python, + circuit: &DAGCircuit, + basis: &mut HashSet, + min_qubits: usize, + ) -> PyResult<()> { + for node in circuit.op_nodes(true) { + let Some(NodeType::Operation(operation)) = circuit.dag().node_weight(node) else { + unreachable!("Circuit op_nodes() returned a non-op node.") + }; + if !circuit.has_calibration_for_index(py, node)? + && circuit.get_qargs(operation.qubits).len() >= min_qubits + { + basis.insert((operation.op.name().to_string(), operation.op.num_qubits())); + } + if operation.op.control_flow() { + let OperationRef::Instruction(inst) = operation.op.view() else { + unreachable!("Control flow operation is not an instance of PyInstruction.") + }; + let inst_bound = inst.instruction.bind(py); + for block in inst_bound.getattr("blocks")?.iter()? { + recurse_circuit(py, block?, basis, min_qubits)?; + } + } + } + Ok(()) + } + + // Recurse for QuantumCircuit + fn recurse_circuit( + py: Python, + circuit: Bound, + basis: &mut HashSet, + min_qubits: usize, + ) -> PyResult<()> { + let circuit_data: PyRef = circuit + .getattr(intern!(py, "_data"))? + .downcast_into()? + .borrow(); + for (index, inst) in circuit_data.iter().enumerate() { + let instruction_object = circuit.get_item(index)?; + let has_calibration = circuit + .call_method1(intern!(py, "has_calibration_for"), (&instruction_object,))?; + if !has_calibration.is_truthy()? + && circuit_data.get_qargs(inst.qubits).len() >= min_qubits + { + basis.insert((inst.op.name().to_string(), inst.op.num_qubits())); + } + if inst.op.control_flow() { + let operation_ob = instruction_object.getattr(intern!(py, "operation"))?; + let blocks = operation_ob.getattr("blocks")?; + for block in blocks.iter()? { + recurse_circuit(py, block?, basis, min_qubits)?; + } + } + } + Ok(()) + } + + recurse_dag(py, circuit, &mut basis, self.min_qubits)?; + Ok(basis) + } + + /// Method that extracts a mapping of all the qargs in the local_source basis + /// obtained from the [Target], to all non-calibrated gate instances identifiers from a DAGCircuit. + /// When dealing with `ControlFlowOp` instances the function will perform a recursion call + /// to a variant design to handle instances of `QuantumCircuit`. + fn extract_basis_target( + &self, + py: Python, + dag: &DAGCircuit, + source_basis: &mut HashSet, + qargs_local_source_basis: &mut HashMap, HashSet>, + ) -> PyResult<()> { + for node in dag.op_nodes(true) { + let Some(NodeType::Operation(node_obj)) = dag.dag().node_weight(node) else { + unreachable!("This was supposed to be an op_node.") + }; + let qargs = dag.get_qargs(node_obj.qubits); + if dag.has_calibration_for_index(py, node)? || qargs.len() < self.min_qubits { + continue; + } + // Treat the instruction as on an incomplete basis if the qargs are in the + // qargs_with_non_global_operation dictionary or if any of the qubits in qargs + // are a superset for a non-local operation. For example, if the qargs + // are (0, 1) and that's a global (ie no non-local operations on (0, 1) + // operation but there is a non-local operation on (1,) we need to + // do an extra non-local search for this op to ensure we include any + // single qubit operation for (1,) as valid. This pattern also holds + // true for > 2q ops too (so for 4q operations we need to check for 3q, 2q, + // and 1q operations in the same manner) + let physical_qargs: SmallVec<[PhysicalQubit; 2]> = + qargs.iter().map(|x| PhysicalQubit(x.0)).collect(); + let physical_qargs_as_set: HashSet = + HashSet::from_iter(physical_qargs.iter().copied()); + if self + .qargs_with_non_global_operation + .contains_key(&Some(physical_qargs)) + || self + .qargs_with_non_global_operation + .keys() + .flatten() + .any(|incomplete_qargs| { + let incomplete_qargs = HashSet::from_iter(incomplete_qargs.iter().copied()); + physical_qargs_as_set.is_superset(&incomplete_qargs) + }) + { + qargs_local_source_basis + .entry(Some(physical_qargs_as_set.into_iter().collect())) + .and_modify(|set| { + set.insert((node_obj.op.name().to_string(), node_obj.op.num_qubits())); + }) + .or_insert(HashSet::from_iter([( + node_obj.op.name().to_string(), + node_obj.op.num_qubits(), + )])); + } else { + source_basis.insert((node_obj.op.name().to_string(), node_obj.op.num_qubits())); + } + if node_obj.op.control_flow() { + let OperationRef::Instruction(op) = node_obj.op.view() else { + unreachable!( + "Control flow op is not a control flow op. But control_flow is `true`" + ) + }; + let bound_inst = op.instruction.bind(py); + // Use python side extraction instead of the Rust method `op.blocks` due to + // required usage of a python-space method `QuantumCircuit.has_calibration_for`. + let blocks = bound_inst.getattr("blocks")?.iter()?; + for block in blocks { + self.extract_basis_target_circ( + &block?, + source_basis, + qargs_local_source_basis, + )?; + } + } + } + Ok(()) + } + + /// Variant of extract_basis_target that takes an instance of QuantumCircuit. + /// This needs to use a Python instance of `QuantumCircuit` due to it needing + /// to access `has_calibration_for()` which is unavailable through rust. However, + /// this API will be removed with the deprecation of `Pulse`. + fn extract_basis_target_circ( + &self, + circuit: &Bound, + source_basis: &mut HashSet, + qargs_local_source_basis: &mut HashMap, HashSet>, + ) -> PyResult<()> { + let py = circuit.py(); + let circ_data_bound = circuit.getattr("_data")?.downcast_into::()?; + let circ_data = circ_data_bound.borrow(); + for (index, node_obj) in circ_data.iter().enumerate() { + let qargs = circ_data.get_qargs(node_obj.qubits); + if circuit + .call_method1("has_calibration_for", (circuit.get_item(index)?,))? + .is_truthy()? + || qargs.len() < self.min_qubits + { + continue; + } + // Treat the instruction as on an incomplete basis if the qargs are in the + // qargs_with_non_global_operation dictionary or if any of the qubits in qargs + // are a superset for a non-local operation. For example, if the qargs + // are (0, 1) and that's a global (ie no non-local operations on (0, 1) + // operation but there is a non-local operation on (1,) we need to + // do an extra non-local search for this op to ensure we include any + // single qubit operation for (1,) as valid. This pattern also holds + // true for > 2q ops too (so for 4q operations we need to check for 3q, 2q, + // and 1q operations in the same manner) + let physical_qargs: SmallVec<[PhysicalQubit; 2]> = + qargs.iter().map(|x| PhysicalQubit(x.0)).collect(); + let physical_qargs_as_set: HashSet = + HashSet::from_iter(physical_qargs.iter().copied()); + if self + .qargs_with_non_global_operation + .contains_key(&Some(physical_qargs)) + || self + .qargs_with_non_global_operation + .keys() + .flatten() + .any(|incomplete_qargs| { + let incomplete_qargs = HashSet::from_iter(incomplete_qargs.iter().copied()); + physical_qargs_as_set.is_superset(&incomplete_qargs) + }) + { + qargs_local_source_basis + .entry(Some(physical_qargs_as_set.into_iter().collect())) + .and_modify(|set| { + set.insert((node_obj.op.name().to_string(), node_obj.op.num_qubits())); + }) + .or_insert(HashSet::from_iter([( + node_obj.op.name().to_string(), + node_obj.op.num_qubits(), + )])); + } else { + source_basis.insert((node_obj.op.name().to_string(), node_obj.op.num_qubits())); + } + if node_obj.op.control_flow() { + let OperationRef::Instruction(op) = node_obj.op.view() else { + unreachable!( + "Control flow op is not a control flow op. But control_flow is `true`" + ) + }; + let bound_inst = op.instruction.bind(py); + let blocks = bound_inst.getattr("blocks")?.iter()?; + for block in blocks { + self.extract_basis_target_circ( + &block?, + source_basis, + qargs_local_source_basis, + )?; + } + } + } + Ok(()) + } + + fn apply_translation( + &mut self, + py: Python, + dag: &DAGCircuit, + target_basis: &HashSet, + instr_map: &InstMap, + extra_inst_map: &ExtraInstructionMap, + ) -> PyResult<(DAGCircuit, bool)> { + let mut is_updated = false; + let mut out_dag = dag.copy_empty_like(py, "alike")?; + for node in dag.topological_op_nodes()? { + let Some(NodeType::Operation(node_obj)) = dag.dag().node_weight(node).cloned() else { + unreachable!("Node {:?} was in the output of topological_op_nodes, but doesn't seem to be an op_node", node) + }; + let node_qarg = dag.get_qargs(node_obj.qubits); + let node_carg = dag.get_cargs(node_obj.clbits); + let qubit_set: HashSet = HashSet::from_iter(node_qarg.iter().copied()); + let mut new_op: Option = None; + if target_basis.contains(node_obj.op.name()) || node_qarg.len() < self.min_qubits { + if node_obj.op.control_flow() { + let OperationRef::Instruction(control_op) = node_obj.op.view() else { + unreachable!("This instruction {} says it is of control flow type, but is not an Instruction instance", node_obj.op.name()) + }; + let mut flow_blocks = vec![]; + let bound_obj = control_op.instruction.bind(py); + let blocks = bound_obj.getattr("blocks")?; + for block in blocks.iter()? { + let block = block?; + let dag_block: DAGCircuit = + circuit_to_dag(py, block.extract()?, true, None, None)?; + let updated_dag: DAGCircuit; + (updated_dag, is_updated) = self.apply_translation( + py, + &dag_block, + target_basis, + instr_map, + extra_inst_map, + )?; + let flow_circ_block = if is_updated { + DAG_TO_CIRCUIT + .get_bound(py) + .call1((updated_dag,))? + .extract()? + } else { + block + }; + flow_blocks.push(flow_circ_block); + } + let replaced_blocks = + bound_obj.call_method1("replace_blocks", (flow_blocks,))?; + new_op = Some(replaced_blocks.extract()?); + } + if let Some(new_op) = new_op { + out_dag.apply_operation_back( + py, + new_op.operation, + node_qarg, + node_carg, + if new_op.params.is_empty() { + None + } else { + Some(new_op.params) + }, + new_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + None, + )?; + } else { + out_dag.apply_operation_back( + py, + node_obj.op.clone(), + node_qarg, + node_carg, + if node_obj.params_view().is_empty() { + None + } else { + Some( + node_obj + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(), + ) + }, + node_obj.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + None, + )?; + } + continue; + } + let node_qarg_as_physical: Option = + Some(node_qarg.iter().map(|x| PhysicalQubit(x.0)).collect()); + if self + .qargs_with_non_global_operation + .contains_key(&node_qarg_as_physical) + && self.qargs_with_non_global_operation[&node_qarg_as_physical] + .contains(node_obj.op.name()) + { + // out_dag.push_back(py, node_obj)?; + out_dag.apply_operation_back( + py, + node_obj.op.clone(), + node_qarg, + node_carg, + if node_obj.params_view().is_empty() { + None + } else { + Some( + node_obj + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(), + ) + }, + node_obj.extra_attrs, + #[cfg(feature = "cache_pygates")] + None, + )?; + continue; + } + + if dag.has_calibration_for_index(py, node)? { + out_dag.apply_operation_back( + py, + node_obj.op.clone(), + node_qarg, + node_carg, + if node_obj.params_view().is_empty() { + None + } else { + Some( + node_obj + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(), + ) + }, + node_obj.extra_attrs, + #[cfg(feature = "cache_pygates")] + None, + )?; + continue; + } + let unique_qargs: Option = if qubit_set.is_empty() { + None + } else { + Some(qubit_set.iter().map(|x| PhysicalQubit(x.0)).collect()) + }; + if extra_inst_map.contains_key(&unique_qargs) { + self.replace_node(py, &mut out_dag, node_obj, &extra_inst_map[&unique_qargs])?; + } else if instr_map + .contains_key(&(node_obj.op.name().to_string(), node_obj.op.num_qubits())) + { + self.replace_node(py, &mut out_dag, node_obj, instr_map)?; + } else { + return Err(TranspilerError::new_err(format!( + "BasisTranslator did not map {}", + node_obj.op.name() + ))); + } + is_updated = true; + } + + Ok((out_dag, is_updated)) + } + + fn replace_node( + &mut self, + py: Python, + dag: &mut DAGCircuit, + node: PackedInstruction, + instr_map: &HashMap, DAGCircuit)>, + ) -> PyResult<()> { + let (target_params, target_dag) = + &instr_map[&(node.op.name().to_string(), node.op.num_qubits())]; + if node.params_view().len() != target_params.len() { + return Err(TranspilerError::new_err(format!( + "Translation num_params not equal to op num_params. \ + Op: {:?} {} Translation: {:?}\n{:?}", + node.params_view(), + node.op.name(), + &target_params, + &target_dag + ))); + } + if node.params_view().is_empty() { + for inner_index in target_dag.topological_op_nodes()? { + let NodeType::Operation(inner_node) = &target_dag.dag()[inner_index] else { + unreachable!("Node returned by topological_op_nodes was not an Operation node.") + }; + let old_qargs = dag.get_qargs(node.qubits); + let old_cargs = dag.get_cargs(node.clbits); + let new_qubits: Vec = target_dag + .get_qargs(inner_node.qubits) + .iter() + .map(|qubit| old_qargs[qubit.0 as usize]) + .collect(); + let new_clbits: Vec = target_dag + .get_cargs(inner_node.clbits) + .iter() + .map(|clbit| old_cargs[clbit.0 as usize]) + .collect(); + let new_op = if inner_node.op.try_standard_gate().is_none() { + inner_node.op.py_copy(py)? + } else { + inner_node.op.clone() + }; + if node.condition().is_some() { + match new_op.view() { + OperationRef::Gate(gate) => { + gate.gate.setattr(py, "condition", node.condition())? + } + OperationRef::Instruction(inst) => { + inst.instruction + .setattr(py, "condition", node.condition())? + } + OperationRef::Operation(oper) => { + oper.operation.setattr(py, "condition", node.condition())? + } + _ => (), + } + } + let new_params: SmallVec<[Param; 3]> = inner_node + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(); + let new_extra_props = node.extra_attrs.clone(); + dag.apply_operation_back( + py, + new_op, + &new_qubits, + &new_clbits, + if new_params.is_empty() { + None + } else { + Some(new_params) + }, + new_extra_props, + #[cfg(feature = "cache_pygates")] + None, + )?; + } + dag.add_global_phase(py, target_dag.global_phase())?; + } else { + let parameter_map = target_params + .iter() + .zip(node.params_view()) + .into_py_dict_bound(py); + for inner_index in target_dag.topological_op_nodes()? { + let NodeType::Operation(inner_node) = &target_dag.dag()[inner_index] else { + unreachable!("Node returned by topological_op_nodes was not an Operation node.") + }; + let old_qargs = dag.get_qargs(node.qubits); + let old_cargs = dag.get_cargs(node.clbits); + let new_qubits: Vec = target_dag + .get_qargs(inner_node.qubits) + .iter() + .map(|qubit| old_qargs[qubit.0 as usize]) + .collect(); + let new_clbits: Vec = target_dag + .get_cargs(inner_node.clbits) + .iter() + .map(|clbit| old_cargs[clbit.0 as usize]) + .collect(); + let new_op = if inner_node.op.try_standard_gate().is_none() { + inner_node.op.py_copy(py)? + } else { + inner_node.op.clone() + }; + let mut new_params: SmallVec<[Param; 3]> = inner_node + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(); + if inner_node + .params_view() + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))) + { + new_params = SmallVec::new(); + for param in inner_node.params_view() { + if let Param::ParameterExpression(param_obj) = param { + let bound_param = param_obj.bind(py); + let exp_params = param.iter_parameters(py)?; + let bind_dict = PyDict::new_bound(py); + for key in exp_params { + let key = key?; + bind_dict.set_item(&key, parameter_map.get_item(&key)?)?; + } + let mut new_value: Bound; + let comparison = bind_dict.values().iter().any(|param| { + param + .is_instance(PARAMETER_EXPRESSION.get_bound(py)) + .is_ok_and(|x| x) + }); + if comparison { + new_value = bound_param.clone(); + for items in bind_dict.items() { + new_value = new_value.call_method1( + intern!(py, "assign"), + items.downcast::()?, + )?; + } + } else { + new_value = + bound_param.call_method1(intern!(py, "bind"), (&bind_dict,))?; + } + let eval = new_value.getattr(intern!(py, "parameters"))?; + if eval.is_empty()? { + new_value = new_value.call_method0(intern!(py, "numeric"))?; + } + new_params.push(new_value.extract()?); + } else { + new_params.push(param.clone_ref(py)); + } + } + if new_op.try_standard_gate().is_none() { + match new_op.view() { + OperationRef::Instruction(inst) => inst + .instruction + .bind(py) + .setattr("params", new_params.clone())?, + OperationRef::Gate(gate) => { + gate.gate.bind(py).setattr("params", new_params.clone())? + } + OperationRef::Operation(oper) => oper + .operation + .bind(py) + .setattr("params", new_params.clone())?, + _ => (), + } + } + } + dag.apply_operation_back( + py, + new_op, + &new_qubits, + &new_clbits, + if new_params.is_empty() { + None + } else { + Some(new_params) + }, + inner_node.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + None, + )?; + } + + if let Param::ParameterExpression(old_phase) = target_dag.global_phase() { + let bound_old_phase = old_phase.bind(py); + let bind_dict = PyDict::new_bound(py); + for key in target_dag.global_phase().iter_parameters(py)? { + let key = key?; + bind_dict.set_item(&key, parameter_map.get_item(&key)?)?; + } + let mut new_phase: Bound; + if bind_dict.values().iter().any(|param| { + param + .is_instance(PARAMETER_EXPRESSION.get_bound(py)) + .is_ok_and(|x| x) + }) { + new_phase = bound_old_phase.clone(); + for key_val in bind_dict.items() { + new_phase = + new_phase.call_method1(intern!(py, "assign"), key_val.downcast()?)?; + } + } else { + new_phase = bound_old_phase.call_method1(intern!(py, "bind"), (bind_dict,))?; + } + if !new_phase.getattr(intern!(py, "parameters"))?.is_truthy()? { + new_phase = new_phase.call_method0(intern!(py, "numeric"))?; + if new_phase.is_instance(&PyComplex::type_object_bound(py))? { + return Err(TranspilerError::new_err(format!( + "Global phase must be real, but got {}", + new_phase.repr()? + ))); + } + } + let new_phase: Param = new_phase.extract()?; + dag.add_global_phase(py, &new_phase)?; + } + } + + Ok(()) + } +} + #[pymodule] pub fn basis_translator(m: &Bound) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(basis_search::py_basis_search))?; - m.add_wrapped(wrap_pyfunction!(compose_transforms::py_compose_transforms))?; + m.add_class::()?; Ok(()) } diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index 65fc8e80d750..dfa573eefd08 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -43,7 +43,7 @@ use instruction_properties::InstructionProperties; use self::exceptions::TranspilerError; -mod exceptions { +pub(crate) mod exceptions { use pyo3::import_exception_bound; import_exception_bound! {qiskit.exceptions, QiskitError} import_exception_bound! {qiskit.transpiler.exceptions, TranspilerError} diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index a1d3e7f0d39c..442f87d0d984 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -13,23 +13,10 @@ """Translates gates to a target basis using a given equivalence library.""" -import time import logging -from functools import singledispatchmethod -from collections import defaultdict - -from qiskit.circuit import ( - ControlFlowOp, - QuantumCircuit, - ParameterExpression, -) -from qiskit.dagcircuit import DAGCircuit, DAGOpNode -from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit._accelerate.basis.basis_translator import basis_search, compose_transforms +from qiskit._accelerate.basis.basis_translator import CoreBasisTranslator logger = logging.getLogger(__name__) @@ -111,18 +98,12 @@ def __init__(self, equivalence_library, target_basis, target=None, min_qubits=0) """ super().__init__() - self._equiv_lib = equivalence_library - self._target_basis = target_basis - self._target = target - self._non_global_operations = None - self._qargs_with_non_global_operation = {} - self._min_qubits = min_qubits - if target is not None: - self._non_global_operations = self._target.get_non_global_operation_names() - self._qargs_with_non_global_operation = defaultdict(set) - for gate in self._non_global_operations: - for qarg in self._target[gate]: - self._qargs_with_non_global_operation[qarg].add(gate) + self._core = CoreBasisTranslator( + equivalence_library, + min_qubits, + None if target_basis is None else set(target_basis), + target, + ) def run(self, dag): """Translate an input DAGCircuit to the target basis. @@ -136,309 +117,5 @@ def run(self, dag): Returns: DAGCircuit: translated circuit. """ - if self._target_basis is None and self._target is None: - return dag - - qarg_indices = {qubit: index for index, qubit in enumerate(dag.qubits)} - - # Names of instructions assumed to supported by any backend. - if self._target is None: - basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay", "store"] - target_basis = set(self._target_basis) - source_basis = set(self._extract_basis(dag)) - qargs_local_source_basis = {} - else: - basic_instrs = ["barrier", "snapshot", "store"] - target_basis = self._target.keys() - set(self._non_global_operations) - source_basis, qargs_local_source_basis = self._extract_basis_target(dag, qarg_indices) - - target_basis = set(target_basis).union(basic_instrs) - # If the source basis is a subset of the target basis and we have no circuit - # instructions on qargs that have non-global operations there is nothing to - # translate and we can exit early. - source_basis_names = {x[0] for x in source_basis} - if source_basis_names.issubset(target_basis) and not qargs_local_source_basis: - return dag - - logger.info( - "Begin BasisTranslator from source basis %s to target basis %s.", - source_basis, - target_basis, - ) - - # Search for a path from source to target basis. - search_start_time = time.time() - basis_transforms = basis_search(self._equiv_lib, source_basis, target_basis) - - qarg_local_basis_transforms = {} - for qarg, local_source_basis in qargs_local_source_basis.items(): - expanded_target = set(target_basis) - # For any multiqubit operation that contains a subset of qubits that - # has a non-local operation, include that non-local operation in the - # search. This matches with the check we did above to include those - # subset non-local operations in the check here. - if len(qarg) > 1: - for non_local_qarg, local_basis in self._qargs_with_non_global_operation.items(): - if qarg.issuperset(non_local_qarg): - expanded_target |= local_basis - else: - expanded_target |= self._qargs_with_non_global_operation[tuple(qarg)] - - logger.info( - "Performing BasisTranslator search from source basis %s to target " - "basis %s on qarg %s.", - local_source_basis, - expanded_target, - qarg, - ) - local_basis_transforms = basis_search( - self._equiv_lib, local_source_basis, expanded_target - ) - - if local_basis_transforms is None: - raise TranspilerError( - "Unable to translate the operations in the circuit: " - f"{[x[0] for x in local_source_basis]} to the backend's (or manually " - f"specified) target basis: {list(expanded_target)}. This likely means the " - "target basis is not universal or there are additional equivalence rules " - "needed in the EquivalenceLibrary being used. For more details on this " - "error see: " - "https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes." - "BasisTranslator#translation-errors" - ) - - qarg_local_basis_transforms[qarg] = local_basis_transforms - - search_end_time = time.time() - logger.info( - "Basis translation path search completed in %.3fs.", search_end_time - search_start_time - ) - - if basis_transforms is None: - raise TranspilerError( - "Unable to translate the operations in the circuit: " - f"{[x[0] for x in source_basis]} to the backend's (or manually specified) target " - f"basis: {list(target_basis)}. This likely means the target basis is not universal " - "or there are additional equivalence rules needed in the EquivalenceLibrary being " - "used. For more details on this error see: " - "https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes." - "BasisTranslator#translation-errors" - ) - - # Compose found path into a set of instruction substitution rules. - - compose_start_time = time.time() - instr_map = compose_transforms(basis_transforms, source_basis, dag) - extra_instr_map = { - qarg: compose_transforms(transforms, qargs_local_source_basis[qarg], dag) - for qarg, transforms in qarg_local_basis_transforms.items() - } - - compose_end_time = time.time() - logger.info( - "Basis translation paths composed in %.3fs.", compose_end_time - compose_start_time - ) - - # Replace source instructions with target translations. - - replace_start_time = time.time() - - def apply_translation(dag, wire_map): - is_updated = False - out_dag = dag.copy_empty_like() - for node in dag.topological_op_nodes(): - node_qargs = tuple(wire_map[bit] for bit in node.qargs) - qubit_set = frozenset(node_qargs) - if node.name in target_basis or len(node.qargs) < self._min_qubits: - if node.name in CONTROL_FLOW_OP_NAMES: - flow_blocks = [] - for block in node.op.blocks: - dag_block = circuit_to_dag(block) - updated_dag, is_updated = apply_translation( - dag_block, - { - inner: wire_map[outer] - for inner, outer in zip(block.qubits, node.qargs) - }, - ) - if is_updated: - flow_circ_block = dag_to_circuit(updated_dag) - else: - flow_circ_block = block - flow_blocks.append(flow_circ_block) - node.op = node.op.replace_blocks(flow_blocks) - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - continue - if ( - node_qargs in self._qargs_with_non_global_operation - and node.name in self._qargs_with_non_global_operation[node_qargs] - ): - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - continue - - if dag.has_calibration_for(node): - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - continue - if qubit_set in extra_instr_map: - self._replace_node(out_dag, node, extra_instr_map[qubit_set]) - elif (node.name, node.num_qubits) in instr_map: - self._replace_node(out_dag, node, instr_map) - else: - raise TranspilerError(f"BasisTranslator did not map {node.name}.") - is_updated = True - return out_dag, is_updated - - out_dag, _ = apply_translation(dag, qarg_indices) - replace_end_time = time.time() - logger.info( - "Basis translation instructions replaced in %.3fs.", - replace_end_time - replace_start_time, - ) - - return out_dag - - def _replace_node(self, dag, node, instr_map): - target_params, target_dag = instr_map[node.name, node.num_qubits] - if len(node.params) != len(target_params): - raise TranspilerError( - "Translation num_params not equal to op num_params." - f"Op: {node.params} {node.name} Translation: {target_params}\n{target_dag}" - ) - if node.params: - parameter_map = dict(zip(target_params, node.params)) - for inner_node in target_dag.topological_op_nodes(): - new_node = DAGOpNode.from_instruction(inner_node._to_circuit_instruction()) - new_node.qargs = tuple( - node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs - ) - new_node.cargs = tuple( - node.cargs[target_dag.find_bit(x).index] for x in inner_node.cargs - ) - - if not new_node.is_standard_gate(): - new_node.op = new_node.op.copy() - if any(isinstance(x, ParameterExpression) for x in inner_node.params): - new_params = [] - for param in new_node.params: - if not isinstance(param, ParameterExpression): - new_params.append(param) - else: - bind_dict = {x: parameter_map[x] for x in param.parameters} - if any(isinstance(x, ParameterExpression) for x in bind_dict.values()): - new_value = param - for x in bind_dict.items(): - new_value = new_value.assign(*x) - else: - new_value = param.bind(bind_dict) - if not new_value.parameters: - new_value = new_value.numeric() - new_params.append(new_value) - new_node.params = new_params - if not new_node.is_standard_gate(): - new_node.op.params = new_params - dag._apply_op_node_back(new_node) - - if isinstance(target_dag.global_phase, ParameterExpression): - old_phase = target_dag.global_phase - bind_dict = {x: parameter_map[x] for x in old_phase.parameters} - if any(isinstance(x, ParameterExpression) for x in bind_dict.values()): - new_phase = old_phase - for x in bind_dict.items(): - new_phase = new_phase.assign(*x) - else: - new_phase = old_phase.bind(bind_dict) - if not new_phase.parameters: - new_phase = new_phase.numeric() - if isinstance(new_phase, complex): - raise TranspilerError(f"Global phase must be real, but got '{new_phase}'") - dag.global_phase += new_phase - - else: - for inner_node in target_dag.topological_op_nodes(): - new_node = DAGOpNode.from_instruction( - inner_node._to_circuit_instruction(), - ) - new_node.qargs = tuple( - node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs - ) - new_node.cargs = tuple( - node.cargs[target_dag.find_bit(x).index] for x in inner_node.cargs - ) - if not new_node.is_standard_gate: - new_node.op = new_node.op.copy() - # dag_op may be the same instance as other ops in the dag, - # so if there is a condition, need to copy - if getattr(node.op, "condition", None): - new_node_op = new_node.op.to_mutable() - new_node_op.condition = node.op.condition - new_node.op = new_node_op - dag._apply_op_node_back(new_node) - if target_dag.global_phase: - dag.global_phase += target_dag.global_phase - - @singledispatchmethod - def _extract_basis(self, circuit): - return circuit - - @_extract_basis.register - def _(self, dag: DAGCircuit): - for node in dag.op_nodes(): - if not dag.has_calibration_for(node) and len(node.qargs) >= self._min_qubits: - yield (node.name, node.num_qubits) - if node.name in CONTROL_FLOW_OP_NAMES: - for block in node.op.blocks: - yield from self._extract_basis(block) - - @_extract_basis.register - def _(self, circ: QuantumCircuit): - for instruction in circ.data: - operation = instruction.operation - if ( - not circ.has_calibration_for(instruction) - and len(instruction.qubits) >= self._min_qubits - ): - yield (operation.name, operation.num_qubits) - if isinstance(operation, ControlFlowOp): - for block in operation.blocks: - yield from self._extract_basis(block) - def _extract_basis_target( - self, dag, qarg_indices, source_basis=None, qargs_local_source_basis=None - ): - if source_basis is None: - source_basis = set() - if qargs_local_source_basis is None: - qargs_local_source_basis = defaultdict(set) - for node in dag.op_nodes(): - qargs = tuple(qarg_indices[bit] for bit in node.qargs) - if dag.has_calibration_for(node) or len(node.qargs) < self._min_qubits: - continue - # Treat the instruction as on an incomplete basis if the qargs are in the - # qargs_with_non_global_operation dictionary or if any of the qubits in qargs - # are a superset for a non-local operation. For example, if the qargs - # are (0, 1) and that's a global (ie no non-local operations on (0, 1) - # operation but there is a non-local operation on (1,) we need to - # do an extra non-local search for this op to ensure we include any - # single qubit operation for (1,) as valid. This pattern also holds - # true for > 2q ops too (so for 4q operations we need to check for 3q, 2q, - # and 1q operations in the same manner) - if qargs in self._qargs_with_non_global_operation or any( - frozenset(qargs).issuperset(incomplete_qargs) - for incomplete_qargs in self._qargs_with_non_global_operation - ): - qargs_local_source_basis[frozenset(qargs)].add((node.name, node.num_qubits)) - else: - source_basis.add((node.name, node.num_qubits)) - if node.name in CONTROL_FLOW_OP_NAMES: - for block in node.op.blocks: - block_dag = circuit_to_dag(block) - source_basis, qargs_local_source_basis = self._extract_basis_target( - block_dag, - { - inner: qarg_indices[outer] - for inner, outer in zip(block.qubits, node.qargs) - }, - source_basis=source_basis, - qargs_local_source_basis=qargs_local_source_basis, - ) - return source_basis, qargs_local_source_basis + return self._core.run(dag)