aboutsummaryrefslogtreecommitdiff
path: root/libpsi-core/src/core/kernel.rs
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2025-12-10 11:11:53 +0100
committerhachem <im@hachem.wtf>2025-12-10 11:11:53 +0100
commit645846fe8e8ff185f57d5fd70c07c8d8d212bfc3 (patch)
tree86b03099b980c580acf7df2aa28a2e9c79d4672e /libpsi-core/src/core/kernel.rs
parent8066223d25be51627416fb7a05c5234d8c499e55 (diff)
[add]: composable runtime pipeline
Diffstat (limited to 'libpsi-core/src/core/kernel.rs')
-rw-r--r--libpsi-core/src/core/kernel.rs356
1 files changed, 356 insertions, 0 deletions
diff --git a/libpsi-core/src/core/kernel.rs b/libpsi-core/src/core/kernel.rs
index 9f65d09..7b66eea 100644
--- a/libpsi-core/src/core/kernel.rs
+++ b/libpsi-core/src/core/kernel.rs
@@ -3,27 +3,88 @@ use crate::maths::simd::{
};
use crate::{complex, Complex, Matrix};
use rayon::prelude::*;
+use std::collections::HashSet;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum GateType {
+ Diagonal,
+ NonDiagonal,
+ Controlled,
+}
#[derive(Clone)]
pub struct Kernel {
pub matrix: Matrix<Complex<f64>>,
pub targets: Vec<usize>,
pub name: String,
+ pub gate_type: GateType,
}
impl Kernel {
pub fn new(name: &str, matrix: Matrix<Complex<f64>>, targets: Vec<usize>) -> Self {
+ let gate_type = Self::detect_gate_type(name, &matrix);
Self {
matrix,
targets,
name: name.to_string(),
+ gate_type,
+ }
+ }
+
+ fn detect_gate_type(name: &str, matrix: &Matrix<Complex<f64>>) -> GateType {
+ let diagonal_gates = [
+ "Z", "S", "T", "Sdg", "Tdg", "Rz", "P", "U1", "CZ", "CP", "CRz",
+ ];
+ if diagonal_gates.iter().any(|&g| name.starts_with(g)) {
+ return GateType::Diagonal;
}
+
+ let controlled_gates = [
+ "CNOT", "CZ", "SWAP", "CRx", "CRy", "CRz", "CP", "CCNOT", "CSWAP",
+ ];
+ if controlled_gates.iter().any(|&g| name.starts_with(g)) {
+ return GateType::Controlled;
+ }
+
+ if matrix.rows == 2 && matrix.cols == 2 {
+ let is_diag = matrix.data[1].real.abs() < 1e-10
+ && matrix.data[1].imaginary.abs() < 1e-10
+ && matrix.data[2].real.abs() < 1e-10
+ && matrix.data[2].imaginary.abs() < 1e-10;
+ if is_diag {
+ return GateType::Diagonal;
+ }
+ }
+
+ GateType::NonDiagonal
}
pub fn num_qubits(&self) -> usize {
self.targets.len()
}
+ pub fn target_set(&self) -> HashSet<usize> {
+ self.targets.iter().cloned().collect()
+ }
+
+ pub fn shares_qubits(&self, other: &Kernel) -> bool {
+ self.targets.iter().any(|t| other.targets.contains(t))
+ }
+
+ pub fn commutes_with(&self, other: &Kernel) -> bool {
+ if !self.shares_qubits(other) {
+ return true;
+ }
+
+ if self.gate_type == GateType::Diagonal && other.gate_type == GateType::Diagonal {
+ if self.targets == other.targets {
+ return true;
+ }
+ }
+
+ false
+ }
+
pub fn can_fuse_with(&self, other: &Kernel) -> bool {
if self.targets.len() != 1 || other.targets.len() != 1 {
return false;
@@ -36,10 +97,17 @@ impl Kernel {
return None;
}
let fused_matrix = other.matrix.dot(&self.matrix)?;
+ let new_type =
+ if self.gate_type == GateType::Diagonal && other.gate_type == GateType::Diagonal {
+ GateType::Diagonal
+ } else {
+ GateType::NonDiagonal
+ };
Some(Kernel {
matrix: fused_matrix,
targets: self.targets.clone(),
name: format!("{}+{}", self.name, other.name),
+ gate_type: new_type,
})
}
}
@@ -264,3 +332,291 @@ impl KernelBuilder {
self.num_qubits
}
}
+
+#[derive(Clone)]
+pub struct ExecutionLayer {
+ pub kernels: Vec<Kernel>,
+}
+
+impl ExecutionLayer {
+ pub fn new() -> Self {
+ Self {
+ kernels: Vec::new(),
+ }
+ }
+
+ pub fn can_add(&self, kernel: &Kernel) -> bool {
+ !self.kernels.iter().any(|k| k.shares_qubits(kernel))
+ }
+
+ pub fn add(&mut self, kernel: Kernel) {
+ self.kernels.push(kernel);
+ }
+
+ pub fn affected_qubits(&self) -> HashSet<usize> {
+ self.kernels
+ .iter()
+ .flat_map(|k| k.targets.iter().cloned())
+ .collect()
+ }
+}
+
+impl Default for ExecutionLayer {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+pub struct StructureAwareKernelBatch {
+ kernels: Vec<Kernel>,
+ layers: Vec<ExecutionLayer>,
+ num_qubits: usize,
+ optimised: bool,
+}
+
+impl StructureAwareKernelBatch {
+ pub fn new(num_qubits: usize) -> Self {
+ Self {
+ kernels: Vec::new(),
+ layers: Vec::new(),
+ num_qubits,
+ optimised: false,
+ }
+ }
+
+ pub fn add(&mut self, kernel: Kernel) {
+ self.kernels.push(kernel);
+ self.optimised = false;
+ }
+
+ pub fn len(&self) -> usize {
+ self.kernels.len()
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.kernels.is_empty()
+ }
+
+ pub fn kernels(&self) -> &[Kernel] {
+ &self.kernels
+ }
+
+ pub fn layers(&self) -> &[ExecutionLayer] {
+ &self.layers
+ }
+
+ pub fn num_layers(&self) -> usize {
+ self.layers.len()
+ }
+
+ pub fn optimise(&mut self) {
+ if self.optimised || self.kernels.len() < 2 {
+ return;
+ }
+
+ self.reorder_commuting_gates();
+ self.multi_pass_fusion();
+ self.build_execution_layers();
+ self.optimised = true;
+ }
+
+ fn reorder_commuting_gates(&mut self) {
+ let mut changed = true;
+ let mut iterations = 0;
+ const MAX_ITERATIONS: usize = 100;
+
+ while changed && iterations < MAX_ITERATIONS {
+ changed = false;
+ iterations += 1;
+
+ for i in 0..self.kernels.len().saturating_sub(1) {
+ let current = &self.kernels[i];
+ let next = &self.kernels[i + 1];
+
+ if current.targets.len() == 1
+ && next.targets.len() == 1
+ && current.targets[0] != next.targets[0]
+ && current.commutes_with(next)
+ {
+ for j in (i + 2)..self.kernels.len() {
+ let candidate = &self.kernels[j];
+
+ if candidate.targets.len() == 1
+ && candidate.targets[0] == current.targets[0]
+ {
+ let can_move = (i + 1..j).all(|k| {
+ let between = &self.kernels[k];
+ !between.shares_qubits(current) || current.commutes_with(between)
+ });
+
+ if can_move && current.can_fuse_with(candidate) {
+ let kernel_to_move = self.kernels.remove(j);
+ self.kernels.insert(i + 1, kernel_to_move);
+ changed = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fn multi_pass_fusion(&mut self) {
+ let mut changed = true;
+ let mut iterations = 0;
+ const MAX_ITERATIONS: usize = 50;
+
+ while changed && iterations < MAX_ITERATIONS {
+ changed = false;
+ iterations += 1;
+
+ let mut new_kernels: Vec<Kernel> = Vec::with_capacity(self.kernels.len());
+ let mut i = 0;
+
+ while i < self.kernels.len() {
+ if i + 1 < self.kernels.len() {
+ let current = &self.kernels[i];
+ let next = &self.kernels[i + 1];
+
+ if let Some(fused) = current.fuse(next) {
+ new_kernels.push(fused);
+ i += 2;
+ changed = true;
+ continue;
+ }
+ }
+
+ new_kernels.push(self.kernels[i].clone());
+ i += 1;
+ }
+
+ self.kernels = new_kernels;
+ }
+ }
+
+ fn build_execution_layers(&mut self) {
+ self.layers.clear();
+
+ for kernel in &self.kernels {
+ let mut placed = false;
+
+ for layer in &mut self.layers {
+ if layer.can_add(kernel) {
+ layer.add(kernel.clone());
+ placed = true;
+ break;
+ }
+ }
+
+ if !placed {
+ let mut new_layer = ExecutionLayer::new();
+ new_layer.add(kernel.clone());
+ self.layers.push(new_layer);
+ }
+ }
+ }
+
+ pub fn execute(&self, state: &mut Vec<Complex<f64>>) {
+ for kernel in &self.kernels {
+ *state = apply_kernel(state, kernel, self.num_qubits);
+ }
+ }
+
+ pub fn execute_parallel(&self, state: &mut Vec<Complex<f64>>) {
+ for kernel in &self.kernels {
+ *state = apply_kernel_parallel(state, kernel, self.num_qubits);
+ }
+ }
+
+ pub fn execute_layered(&self, state: &mut Vec<Complex<f64>>) {
+ for layer in &self.layers {
+ for kernel in &layer.kernels {
+ *state = apply_kernel(state, kernel, self.num_qubits);
+ }
+ }
+ }
+
+ pub fn execute_layered_parallel(&self, state: &mut Vec<Complex<f64>>) {
+ for layer in &self.layers {
+ for kernel in &layer.kernels {
+ *state = apply_kernel_parallel(state, kernel, self.num_qubits);
+ }
+ }
+ }
+
+ pub fn execute_simd(&self, state: &mut Vec<Complex<f64>>) {
+ for kernel in &self.kernels {
+ if kernel.targets.len() == 1 {
+ let gate = matrix_to_2x2(&kernel.matrix);
+ apply_single_qubit_gate_simd(state, &gate, kernel.targets[0], self.num_qubits);
+ } else {
+ *state = apply_kernel(state, kernel, self.num_qubits);
+ }
+ }
+ }
+
+ pub fn execute_simd_parallel(&self, state: &mut Vec<Complex<f64>>) {
+ for kernel in &self.kernels {
+ if kernel.targets.len() == 1 && self.num_qubits >= 10 {
+ let gate = matrix_to_2x2(&kernel.matrix);
+ apply_single_qubit_gate_simd_parallel(
+ state,
+ &gate,
+ kernel.targets[0],
+ self.num_qubits,
+ );
+ } else if kernel.targets.len() == 1 {
+ let gate = matrix_to_2x2(&kernel.matrix);
+ apply_single_qubit_gate_simd(state, &gate, kernel.targets[0], self.num_qubits);
+ } else {
+ *state = apply_kernel_parallel(state, kernel, self.num_qubits);
+ }
+ }
+ }
+
+ pub fn stats(&self) -> KernelStats {
+ let single_qubit = self.kernels.iter().filter(|k| k.targets.len() == 1).count();
+ let two_qubit = self.kernels.iter().filter(|k| k.targets.len() == 2).count();
+ let multi_qubit = self.kernels.iter().filter(|k| k.targets.len() > 2).count();
+ let diagonal = self
+ .kernels
+ .iter()
+ .filter(|k| k.gate_type == GateType::Diagonal)
+ .count();
+
+ KernelStats {
+ total_kernels: self.kernels.len(),
+ single_qubit,
+ two_qubit,
+ multi_qubit,
+ diagonal,
+ execution_layers: self.layers.len(),
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct KernelStats {
+ pub total_kernels: usize,
+ pub single_qubit: usize,
+ pub two_qubit: usize,
+ pub multi_qubit: usize,
+ pub diagonal: usize,
+ pub execution_layers: usize,
+}
+
+impl std::fmt::Display for KernelStats {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "Kernels: {} (1q: {}, 2q: {}, 3q+: {}, diag: {}), Layers: {}",
+ self.total_kernels,
+ self.single_qubit,
+ self.two_qubit,
+ self.multi_qubit,
+ self.diagonal,
+ self.execution_layers
+ )
+ }
+}