1use crate::backend::mps::MpsBackend;
2use crate::backend::product::ProductStateBackend;
3use crate::backend::sparse::SparseBackend;
4use crate::backend::stabilizer::StabilizerBackend;
5use crate::backend::statevector::StatevectorBackend;
6use crate::backend::tensornetwork::TensorNetworkBackend;
7use crate::backend::Backend;
8use crate::circuit::{Circuit, Instruction};
9use crate::error::{PrismError, Result};
10
11#[cfg(feature = "gpu")]
12use std::sync::Arc;
13
14#[cfg(feature = "gpu")]
15use crate::gpu::GpuContext;
16
17use super::{Probabilities, SimulationResult};
18
19pub(super) enum DispatchAction {
20 Backend(Box<dyn Backend>),
21 StabilizerRank,
22 StochasticPauli { num_samples: usize },
23 DeterministicPauli { epsilon: f64, max_terms: usize },
24}
25
26pub(super) fn max_statevector_qubits() -> usize {
27 static CACHED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
28 *CACHED.get_or_init(|| {
29 if let Ok(val) = std::env::var("PRISM_MAX_SV_QUBITS") {
30 if let Ok(n) = val.parse::<usize>() {
31 return n;
32 }
33 }
34 match detect_max_sv_qubits() {
35 Some(n) => n,
36 None => {
37 eprintln!(
38 "warning: could not detect system memory; statevector qubit cap is disabled. \
39 Large circuits may abort on allocation. Set PRISM_MAX_SV_QUBITS to suppress."
40 );
41 usize::MAX
42 }
43 }
44 })
45}
46
47#[cfg(windows)]
48fn detect_max_sv_qubits() -> Option<usize> {
49 #[repr(C)]
50 struct MemoryStatusEx {
51 dw_length: u32,
52 dw_memory_load: u32,
53 ull_total_phys: u64,
54 ull_avail_phys: u64,
55 ull_total_page_file: u64,
56 ull_avail_page_file: u64,
57 ull_total_virtual: u64,
58 ull_avail_virtual: u64,
59 ull_avail_extended_virtual: u64,
60 }
61
62 extern "system" {
63 fn GlobalMemoryStatusEx(lp_buffer: *mut MemoryStatusEx) -> i32;
64 }
65
66 let mut status: MemoryStatusEx = unsafe { std::mem::zeroed() };
68 status.dw_length = std::mem::size_of::<MemoryStatusEx>() as u32;
69 if unsafe { GlobalMemoryStatusEx(&mut status) } == 0 {
71 return None;
72 }
73
74 let budget = status.ull_total_phys / 2;
75 let max_elements = budget / 16;
76 if max_elements == 0 {
77 return None;
78 }
79 let n = 63 - max_elements.leading_zeros() as usize;
80 Some(n.min(33))
81}
82
83#[cfg(unix)]
84fn detect_max_sv_qubits() -> Option<usize> {
85 let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
86 for line in meminfo.lines() {
87 if let Some(rest) = line.strip_prefix("MemTotal:") {
88 let kb: u64 = rest.trim().trim_end_matches(" kB").trim().parse().ok()?;
89 let budget = (kb * 1024) / 2;
90 let max_elements = budget / 16;
91 if max_elements == 0 {
92 return None;
93 }
94 let n = 63 - max_elements.leading_zeros() as usize;
95 return Some(n.min(33));
96 }
97 }
98 None
99}
100
101#[cfg(not(any(windows, unix)))]
102fn detect_max_sv_qubits() -> Option<usize> {
103 None
104}
105
106pub(super) const AUTO_MPS_BOND_DIM: usize = 256;
107
108pub(super) const MAX_AUTO_T_COUNT_EXACT: usize = 18;
109
110pub(super) const MAX_AUTO_T_COUNT_APPROX: usize = 28;
111
112pub(super) const MAX_AUTO_T_COUNT_SHOTS: usize = 40;
113
114pub(super) const MAX_STABILIZER_RANK_QUBITS: usize = 25;
115
116pub(super) const AUTO_APPROX_MAX_TERMS: usize = 8192;
117
118pub(super) const MIN_QUBITS_FOR_SPD_AUTO: usize = 12;
119
120pub(super) const AUTO_SPD_MAX_TERMS: usize = 65536;
121
122pub(super) const MIN_FACTORED_STABILIZER_QUBITS: usize = 128;
123
124pub(super) const MIN_BLOCK_FOR_FACTORED_STAB: usize = 16;
125
126#[derive(Debug, Clone)]
142pub enum BackendKind {
143 Auto,
144 Statevector,
145 Stabilizer,
146 Sparse,
147 Mps {
148 max_bond_dim: usize,
149 },
150 ProductState,
151 TensorNetwork,
152 Factored,
153 StabilizerRank,
154 FilteredStabilizer,
155 FactoredStabilizer,
156 StochasticPauli {
157 num_samples: usize,
158 },
159 DeterministicPauli {
160 epsilon: f64,
161 max_terms: usize,
162 },
163 #[cfg(feature = "gpu")]
178 StatevectorGpu {
179 context: Arc<GpuContext>,
180 },
181 #[cfg(feature = "gpu")]
195 StabilizerGpu {
196 context: Arc<GpuContext>,
197 },
198}
199
200impl BackendKind {
201 pub fn supports_noisy_per_shot(&self) -> bool {
202 !matches!(
203 self,
204 BackendKind::StabilizerRank
205 | BackendKind::StochasticPauli { .. }
206 | BackendKind::DeterministicPauli { .. }
207 )
208 }
209
210 pub fn supports_general_noise(&self) -> bool {
211 match self {
212 BackendKind::Auto
213 | BackendKind::Statevector
214 | BackendKind::Sparse
215 | BackendKind::Mps { .. }
216 | BackendKind::ProductState
217 | BackendKind::Factored => true,
218 #[cfg(feature = "gpu")]
219 BackendKind::StatevectorGpu { .. } => true,
220 _ => false,
221 }
222 }
223
224 pub(crate) fn is_stabilizer_family(&self) -> bool {
225 matches!(
226 self,
227 BackendKind::Stabilizer
228 | BackendKind::FilteredStabilizer
229 | BackendKind::FactoredStabilizer
230 ) || {
231 #[cfg(feature = "gpu")]
232 {
233 matches!(self, BackendKind::StabilizerGpu { .. })
234 }
235 #[cfg(not(feature = "gpu"))]
236 {
237 false
238 }
239 }
240 }
241
242 pub(crate) fn general_noise_backend_names() -> &'static str {
243 #[cfg(feature = "gpu")]
244 {
245 "Auto, Statevector, StatevectorGpu, Sparse, Mps, ProductState, or Factored"
246 }
247 #[cfg(not(feature = "gpu"))]
248 {
249 "Auto, Statevector, Sparse, Mps, ProductState, or Factored"
250 }
251 }
252}
253
254pub(super) fn validate_explicit_backend(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
255 match kind {
256 BackendKind::Stabilizer
257 | BackendKind::FilteredStabilizer
258 | BackendKind::FactoredStabilizer
259 if !circuit.is_clifford_only() =>
260 {
261 return Err(PrismError::IncompatibleBackend {
262 backend: "stabilizer".into(),
263 reason: "circuit contains non-Clifford gates".into(),
264 });
265 }
266 #[cfg(feature = "gpu")]
267 BackendKind::StabilizerGpu { .. } if !circuit.is_clifford_only() => {
268 return Err(PrismError::IncompatibleBackend {
269 backend: "stabilizer".into(),
270 reason: "circuit contains non-Clifford gates".into(),
271 });
272 }
273 BackendKind::ProductState if circuit.has_entangling_gates() => {
274 return Err(PrismError::IncompatibleBackend {
275 backend: "productstate".into(),
276 reason: "circuit contains entangling gates".into(),
277 });
278 }
279 BackendKind::StabilizerRank if !circuit.has_t_gates() => {
280 return Err(PrismError::IncompatibleBackend {
281 backend: "stabilizer_rank".into(),
282 reason: "circuit has no T gates; use Stabilizer instead".into(),
283 });
284 }
285 _ => {}
286 }
287 Ok(())
288}
289
290pub(super) fn supports_fused_for_kind(kind: &BackendKind, circuit: &Circuit) -> bool {
291 match kind {
292 BackendKind::Stabilizer
293 | BackendKind::FilteredStabilizer
294 | BackendKind::FactoredStabilizer
295 | BackendKind::StabilizerRank
296 | BackendKind::StochasticPauli { .. }
297 | BackendKind::DeterministicPauli { .. } => false,
298 #[cfg(feature = "gpu")]
299 BackendKind::StabilizerGpu { .. } => false,
300 BackendKind::Auto => !(circuit.is_clifford_only() && circuit.has_entangling_gates()),
301 _ => true,
302 }
303}
304
305#[cfg(feature = "gpu")]
310fn statevector_gpu_with_crossover(
311 context: &Arc<GpuContext>,
312 circuit: &Circuit,
313 seed: u64,
314) -> StatevectorBackend {
315 if circuit.num_qubits >= crate::gpu::min_qubits() {
316 StatevectorBackend::new(seed).with_gpu(context.clone())
317 } else {
318 StatevectorBackend::new(seed)
319 }
320}
321
322#[cfg(feature = "gpu")]
326fn stabilizer_gpu_with_crossover(
327 context: &Arc<GpuContext>,
328 circuit: &Circuit,
329 seed: u64,
330) -> StabilizerBackend {
331 if circuit.num_qubits >= crate::gpu::stabilizer_min_qubits() {
332 StabilizerBackend::new(seed).with_gpu(context.clone())
333 } else {
334 StabilizerBackend::new(seed)
335 }
336}
337
338pub(super) fn select_dispatch(
339 kind: &BackendKind,
340 circuit: &Circuit,
341 seed: u64,
342 has_partial_independence: bool,
343) -> DispatchAction {
344 match kind {
345 BackendKind::Auto => {
346 if !circuit.has_entangling_gates() {
347 DispatchAction::Backend(Box::new(ProductStateBackend::new(seed)))
348 } else if circuit.is_clifford_only() {
349 DispatchAction::Backend(Box::new(StabilizerBackend::new(seed)))
350 } else if circuit.num_qubits > max_statevector_qubits() {
351 if circuit.is_sparse_friendly() {
352 DispatchAction::Backend(Box::new(SparseBackend::new(seed)))
353 } else {
354 DispatchAction::Backend(Box::new(MpsBackend::new(seed, AUTO_MPS_BOND_DIM)))
355 }
356 } else if has_partial_independence {
357 DispatchAction::Backend(Box::new(crate::backend::factored::FactoredBackend::new(
358 seed,
359 )))
360 } else {
361 DispatchAction::Backend(Box::new(StatevectorBackend::new(seed)))
362 }
363 }
364 BackendKind::Statevector => {
365 DispatchAction::Backend(Box::new(StatevectorBackend::new(seed)))
366 }
367 BackendKind::Stabilizer => DispatchAction::Backend(Box::new(StabilizerBackend::new(seed))),
368 BackendKind::FilteredStabilizer => DispatchAction::Backend(Box::new(
369 crate::backend::stabilizer::FilteredStabilizerBackend::new(seed),
370 )),
371 BackendKind::Sparse => DispatchAction::Backend(Box::new(SparseBackend::new(seed))),
372 BackendKind::Mps { max_bond_dim } => {
373 DispatchAction::Backend(Box::new(MpsBackend::new(seed, *max_bond_dim)))
374 }
375 BackendKind::ProductState => {
376 DispatchAction::Backend(Box::new(ProductStateBackend::new(seed)))
377 }
378 BackendKind::TensorNetwork => {
379 DispatchAction::Backend(Box::new(TensorNetworkBackend::new(seed)))
380 }
381 BackendKind::Factored => DispatchAction::Backend(Box::new(
382 crate::backend::factored::FactoredBackend::new(seed),
383 )),
384 BackendKind::FactoredStabilizer => DispatchAction::Backend(Box::new(
385 crate::backend::factored_stabilizer::FactoredStabilizerBackend::new(seed),
386 )),
387 BackendKind::StabilizerRank => DispatchAction::StabilizerRank,
388 BackendKind::StochasticPauli { num_samples } => DispatchAction::StochasticPauli {
389 num_samples: *num_samples,
390 },
391 BackendKind::DeterministicPauli { epsilon, max_terms } => {
392 DispatchAction::DeterministicPauli {
393 epsilon: *epsilon,
394 max_terms: *max_terms,
395 }
396 }
397 #[cfg(feature = "gpu")]
398 BackendKind::StatevectorGpu { context } => DispatchAction::Backend(Box::new(
399 statevector_gpu_with_crossover(context, circuit, seed),
400 )),
401 #[cfg(feature = "gpu")]
402 BackendKind::StabilizerGpu { context } => DispatchAction::Backend(Box::new(
403 stabilizer_gpu_with_crossover(context, circuit, seed),
404 )),
405 }
406}
407
408pub(super) fn select_backend(
409 kind: &BackendKind,
410 circuit: &Circuit,
411 seed: u64,
412 has_partial_independence: bool,
413) -> Box<dyn Backend> {
414 match select_dispatch(kind, circuit, seed, has_partial_independence) {
415 DispatchAction::Backend(b) => b,
416 _ => unreachable!("non-backend dispatch should be handled by caller"),
417 }
418}
419
420#[inline]
421pub(super) fn min_clifford_prefix_gates(num_qubits: usize) -> usize {
422 (num_qubits * 2).max(16)
423}
424
425pub(super) fn has_temporal_clifford_opportunity(kind: &BackendKind, circuit: &Circuit) -> bool {
426 if !matches!(kind, BackendKind::Auto) {
427 return false;
428 }
429 if circuit.num_qubits > max_statevector_qubits() {
430 return false;
431 }
432 let min_gates = min_clifford_prefix_gates(circuit.num_qubits);
433 let mut prefix_gates = 0;
434 for inst in &circuit.instructions {
435 match inst {
436 Instruction::Gate { gate, .. } => {
437 if !gate.is_clifford() {
438 break;
439 }
440 prefix_gates += 1;
441 }
442 Instruction::Measure { .. }
443 | Instruction::Reset { .. }
444 | Instruction::Conditional { .. } => break,
445 Instruction::Barrier { .. } => {}
446 }
447 }
448 prefix_gates >= min_gates && prefix_gates < circuit.instructions.len()
449}
450
451pub(super) fn try_temporal_clifford(
452 kind: &BackendKind,
453 circuit: &Circuit,
454 seed: u64,
455) -> Option<Result<SimulationResult>> {
456 if !matches!(kind, BackendKind::Auto) {
457 return None;
458 }
459 if circuit.num_qubits > max_statevector_qubits() {
460 return None;
461 }
462 let (prefix, tail) = circuit.clifford_prefix_split()?;
463 if prefix.gate_count() < min_clifford_prefix_gates(circuit.num_qubits) {
464 return None;
465 }
466
467 let mut stab = StabilizerBackend::new(seed);
468 if let Err(e) = stab.init(prefix.num_qubits, prefix.num_classical_bits) {
469 return Some(Err(e));
470 }
471 stab.enable_lazy_destab();
472 for inst in &prefix.instructions {
473 if let Err(e) = stab.apply(inst) {
474 return Some(Err(e));
475 }
476 }
477
478 let state = match stab.export_statevector() {
479 Ok(s) => s,
480 Err(e) => return Some(Err(e)),
481 };
482
483 let mut sv = StatevectorBackend::new(seed);
484 if let Err(e) = sv.init_from_state(state, tail.num_classical_bits) {
485 return Some(Err(e));
486 }
487
488 let fused_tail = crate::circuit::fusion::fuse_circuit(&tail, sv.supports_fused_gates());
489 for inst in &fused_tail.instructions {
490 if let Err(e) = sv.apply(inst) {
491 return Some(Err(e));
492 }
493 }
494
495 let probs = sv.probabilities().ok().map(Probabilities::Dense);
496
497 Some(Ok(SimulationResult {
498 classical_bits: sv.classical_results().to_vec(),
499 probabilities: probs,
500 }))
501}
502
503#[cfg(all(test, feature = "gpu"))]
504mod gpu_crossover_tests {
505 use super::*;
506 use crate::gates::Gate;
507 use crate::sim::run_with;
508
509 fn stub_kind() -> BackendKind {
510 BackendKind::StatevectorGpu {
511 context: GpuContext::stub_for_tests(),
512 }
513 }
514
515 #[test]
519 fn run_with_gpu_wraps_statevector_gpu_variant() {
520 let ctx = GpuContext::stub_for_tests();
521 let mut circuit = Circuit::new(4, 0);
522 circuit.add_gate(Gate::H, &[0]);
523 circuit.add_gate(Gate::Cx, &[0, 1]);
524
525 let direct = crate::sim::run_with_gpu(&circuit, 42, ctx.clone())
526 .expect("run_with_gpu must honor crossover and route to CPU");
527 let manual = run_with(stub_kind(), &circuit, 42).expect("manual variant reference");
528
529 let dp = direct.probabilities.expect("direct probs").to_vec();
530 let mp = manual.probabilities.expect("manual probs").to_vec();
531 assert_eq!(dp, mp);
532 }
533
534 #[test]
540 fn small_circuit_routes_to_cpu() {
541 let mut circuit = Circuit::new(4, 0);
542 circuit.add_gate(Gate::H, &[0]);
543 circuit.add_gate(Gate::Cx, &[0, 1]);
544 circuit.add_gate(Gate::H, &[2]);
545 circuit.add_gate(Gate::Cx, &[2, 3]);
546
547 let result = run_with(stub_kind(), &circuit, 42)
548 .expect("stub context must not be touched for a 4q circuit");
549 let probs = result
550 .probabilities
551 .expect("probabilities missing")
552 .to_vec();
553
554 let mut expected = [0.0_f64; 16];
555 expected[0b0000] = 0.25;
556 expected[0b0011] = 0.25;
557 expected[0b1100] = 0.25;
558 expected[0b1111] = 0.25;
559 for (i, (p, e)) in probs.iter().zip(&expected).enumerate() {
560 assert!((p - e).abs() < 1e-10, "p[{i}] = {p}, expected {e}");
561 }
562 }
563
564 #[test]
571 fn decomposable_16q_circuit_runs_per_block_on_cpu() {
572 let circuit = crate::circuits::independent_bell_pairs(8);
573 assert_eq!(circuit.num_qubits, 16);
574
575 let cpu = run_with(BackendKind::Statevector, &circuit, 42).expect("cpu baseline");
576 let gpu = run_with(stub_kind(), &circuit, 42).expect("stub must stay out of the way");
577
578 let cpu_p = cpu.probabilities.expect("cpu probs").to_vec();
579 let gpu_p = gpu.probabilities.expect("gpu probs").to_vec();
580 assert_eq!(cpu_p.len(), gpu_p.len());
581 for (i, (c, g)) in cpu_p.iter().zip(gpu_p.iter()).enumerate() {
582 assert!(
583 (c - g).abs() < 1e-10,
584 "prob[{i}] cpu={c}, gpu={g}, diff={}",
585 (c - g).abs()
586 );
587 }
588 }
589
590 fn stabilizer_stub_kind() -> BackendKind {
591 BackendKind::StabilizerGpu {
592 context: GpuContext::stub_for_tests(),
593 }
594 }
595
596 #[test]
600 fn stabilizer_gpu_small_circuit_routes_to_cpu() {
601 let mut circuit = Circuit::new(4, 4);
602 circuit.add_gate(Gate::H, &[0]);
603 circuit.add_gate(Gate::Cx, &[0, 1]);
604 circuit.add_gate(Gate::Cx, &[1, 2]);
605 circuit.add_gate(Gate::Cx, &[2, 3]);
606 circuit.add_measure(0, 0);
607 circuit.add_measure(1, 1);
608 circuit.add_measure(2, 2);
609 circuit.add_measure(3, 3);
610
611 let cpu_run = run_with(BackendKind::Stabilizer, &circuit, 42).expect("cpu baseline");
612 let gpu_run = run_with(stabilizer_stub_kind(), &circuit, 42)
613 .expect("stub must stay out of the way for small circuits");
614 assert_eq!(cpu_run.classical_bits, gpu_run.classical_bits);
615 }
616
617 #[test]
620 fn stabilizer_gpu_rejects_non_clifford_at_dispatch() {
621 let mut circuit = Circuit::new(2, 0);
622 circuit.add_gate(Gate::T, &[0]);
623 let err = run_with(stabilizer_stub_kind(), &circuit, 42).unwrap_err();
624 assert!(matches!(err, PrismError::IncompatibleBackend { .. }));
625 }
626}