From f4317aebaccfcebe724d57c18769f91310cc32e9 Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Mon, 4 May 2026 22:23:45 -0700 Subject: [PATCH 1/5] Manual memory-compute in RE --- library/std/src/Std/ResourceEstimation.qs | 24 +++- source/resource_estimator/src/counts.rs | 112 +++++++++++++++++- .../src/counts/memory_compute.rs | 38 ++++++ source/resource_estimator/src/counts/tests.rs | 32 ++++- 4 files changed, 196 insertions(+), 10 deletions(-) diff --git a/library/std/src/Std/ResourceEstimation.qs b/library/std/src/Std/ResourceEstimation.qs index 7d58ad9408..6660d14f3b 100644 --- a/library/std/src/Std/ResourceEstimation.qs +++ b/library/std/src/Std/ResourceEstimation.qs @@ -211,6 +211,26 @@ function LeastFrequentlyUsed() : Int { return 1; } + +/// Signals to resource estimator that this qubit is stored to memory, i.e. +/// transitions from "compute qubit" to "memory qubit". +/// Each qubit is allocated as "compute" by default. +/// MemoryQubitStore can be applied only to "compute" qubit. That is, qubit on which +/// MemoryQubitLoad wasn't applied or if it was applied, it was followed by +/// MemoryQubitLoad. +/// While qubit is in "memory" state, no gates or measurements can be applied to it. +function MemoryQubitStore(q: Qubit) : Unit { + body intrinsic; +} + +/// Signals to resource estimator that this qubit is loaded from memory, i.e. +/// transitions from "memory qubit" to "compute qubit". +/// Can be applied only to "memory qubit", i.e. on which previously MemoryQubitStore +/// was applied. +function MemoryQubitLoad(q: Qubit) : Unit { + body intrinsic; +} + export SingleVariant, BeginEstimateCaching, @@ -228,4 +248,6 @@ export RepeatEstimates, EnableMemoryComputeArchitecture, LeastRecentlyUsed, - LeastFrequentlyUsed; + LeastFrequentlyUsed, + MemoryQubitLoad, + MemoryQubitStore; diff --git a/source/resource_estimator/src/counts.rs b/source/resource_estimator/src/counts.rs index 1cf8d8066b..bce9b1ed64 100644 --- a/source/resource_estimator/src/counts.rs +++ b/source/resource_estimator/src/counts.rs @@ -14,7 +14,13 @@ use rustc_hash::FxHashMap; use std::{array, cell::RefCell, f64::consts::PI, fmt::Debug, iter::Sum}; use crate::{counts::memory_compute::CachingStrategy, system::LogicalResourceCounts}; -use memory_compute::MemoryComputeInfo; +use memory_compute::{MemoryComputeInfo, QubitPool}; + +#[derive(Clone, Copy)] +enum TypedQubit { + Compute(usize), + Memory(usize), +} /// Resource counter implementation /// @@ -53,6 +59,19 @@ pub struct LogicalCounter { /// Map to track any post-select measurements by their associated qubit. /// This value is used in a measurement, if present, before generating a random result. post_select_measurements: FxHashMap, + + /// Pool of typed compute qubits used by manual MemoryQubitStore/Load. + compute_qubit_pool: QubitPool, + /// Pool of typed memory qubits used by manual MemoryQubitStore/Load. + memory_qubit_pool: QubitPool, + /// Mapping from untyped allocated qubit id to its typed counterpart/state. + typed_qubit_map: FxHashMap, + /// Peak number of typed qubits alive at once in manual memory-qubit mode. + typed_qubit_peak: usize, + + manual_memory_reads: usize, + manual_memory_writes: usize, + manual_memory_mode: bool, } impl Default for LogicalCounter { @@ -73,6 +92,13 @@ impl Default for LogicalCounter { memory_compute: None, rnd: RefCell::new(StdRng::seed_from_u64(0)), post_select_measurements: FxHashMap::default(), + compute_qubit_pool: QubitPool::default(), + memory_qubit_pool: QubitPool::default(), + typed_qubit_map: FxHashMap::default(), + typed_qubit_peak: 0, + manual_memory_reads: 0, + manual_memory_writes: 0, + manual_memory_mode: false, } } } @@ -87,12 +113,24 @@ impl LogicalCounter { Some(memory_compute.read_from_memory_count() as u64), Some(memory_compute.write_to_memory_count() as u64), ) + } else if self.manual_memory_mode { + ( + Some(self.compute_qubit_pool.max_in_use() as u64), + Some(self.manual_memory_reads as u64), + Some(self.manual_memory_writes as u64), + ) } else { (None, None, None) }; + let extra_manual_typed_qubits = if self.manual_memory_mode { + self.typed_qubit_peak + } else { + 0 + }; + LogicalResourceCounts { - num_qubits: self.next_free as _, + num_qubits: (self.next_free + extra_manual_typed_qubits) as _, t_count: self.t_count as _, rotation_count: self.r_count as _, rotation_depth: self.layers.iter().filter(|layer| layer.r != 0).count() as _, @@ -461,8 +499,15 @@ impl LogicalCounter { } fn assert_compute_qubits(&mut self, qubits: impl IntoIterator) { + let qubits: Vec<_> = qubits.into_iter().collect(); if let Some(memory_compute) = &mut self.memory_compute { - memory_compute.assert_compute_qubits(qubits); + memory_compute.assert_compute_qubits(qubits.iter().copied()); + } + + for qubit in qubits { + if let Some(TypedQubit::Memory(_)) = self.typed_qubit_map.get(&qubit) { + panic!("cannot apply compute operation to a memory qubit"); + } } } @@ -602,17 +647,32 @@ impl Backend for LogicalCounter { fn z(&mut self, _q: usize) {} fn qubit_allocate(&mut self) -> usize { - if let Some(index) = self.free_list.pop() { + let index = if let Some(index) = self.free_list.pop() { index } else { let index = self.next_free; self.next_free += 1; self.max_layer.push(self.allocation_barrier); index - } + }; + + let compute_index = self.compute_qubit_pool.allocate(); + self.typed_qubit_map + .insert(index, TypedQubit::Compute(compute_index)); + self.typed_qubit_peak = self.typed_qubit_peak.max(self.typed_qubit_map.len()); + + index } fn qubit_release(&mut self, q: usize) -> bool { + if let Some(typed) = self.typed_qubit_map.remove(&q) { + match typed { + TypedQubit::Compute(compute_index) => { + self.compute_qubit_pool.release(compute_index) + } + TypedQubit::Memory(memory_index) => self.memory_qubit_pool.release(memory_index), + } + } self.free_list.push(q); true } @@ -630,6 +690,15 @@ impl Backend for LogicalCounter { if let Some(val) = q1_post_select { self.post_select_measurements.insert(q0, val); } + + let q0_typed = self.typed_qubit_map.remove(&q0); + let q1_typed = self.typed_qubit_map.remove(&q1); + if let Some(typed) = q0_typed { + self.typed_qubit_map.insert(q1, typed); + } + if let Some(typed) = q1_typed { + self.typed_qubit_map.insert(q0, typed); + } } fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { @@ -706,6 +775,39 @@ impl Backend for LogicalCounter { Some(Ok(Value::unit())) } + "MemoryQubitLoad" => { + self.manual_memory_mode = true; + let q = arg.unwrap_qubit().deref().0; + let Some(TypedQubit::Memory(memory_index)) = self.typed_qubit_map.get(&q).copied() + else { + return Some(Err( + "MemoryQubitLoad can only be applied to a memory qubit".to_string() + )); + }; + self.memory_qubit_pool.release(memory_index); + let compute_index = self.compute_qubit_pool.allocate(); + self.typed_qubit_map + .insert(q, TypedQubit::Compute(compute_index)); + self.manual_memory_reads += 1; + Some(Ok(Value::unit())) + } + "MemoryQubitStore" => { + self.manual_memory_mode = true; + let q = arg.unwrap_qubit().deref().0; + let Some(TypedQubit::Compute(compute_index)) = + self.typed_qubit_map.get(&q).copied() + else { + return Some(Err( + "MemoryQubitStore can only be applied to a compute qubit".to_string(), + )); + }; + self.compute_qubit_pool.release(compute_index); + let memory_index = self.memory_qubit_pool.allocate(); + self.typed_qubit_map + .insert(q, TypedQubit::Memory(memory_index)); + self.manual_memory_writes += 1; + Some(Ok(Value::unit())) + } _ => None, } } diff --git a/source/resource_estimator/src/counts/memory_compute.rs b/source/resource_estimator/src/counts/memory_compute.rs index e2df7d3fa5..4f188e68a6 100644 --- a/source/resource_estimator/src/counts/memory_compute.rs +++ b/source/resource_estimator/src/counts/memory_compute.rs @@ -5,6 +5,44 @@ use std::hash::Hash; #[cfg(test)] mod tests; +#[derive(Default)] +pub struct QubitPool { + free_list: Vec, + next_id: usize, + in_use: usize, + max_in_use: usize, +} + +impl QubitPool { + pub fn allocate(&mut self) -> usize { + let index = if let Some(index) = self.free_list.pop() { + index + } else { + let index = self.next_id; + self.next_id += 1; + index + }; + self.in_use += 1; + if self.in_use > self.max_in_use { + self.max_in_use = self.in_use; + } + + index + } + + pub fn release(&mut self, index: usize) { + self.free_list.push(index); + self.in_use = self + .in_use + .checked_sub(1) + .expect("releasing from an empty qubit pool"); + } + + pub fn max_in_use(&self) -> usize { + self.max_in_use + } +} + pub enum CachingStrategy { LeastRecentlyUsed(LeastRecentlyUsedPriorityQueue), LeastFrequentlyUsed(LeastFrequentlyUsedPriorityQueue), diff --git a/source/resource_estimator/src/counts/tests.rs b/source/resource_estimator/src/counts/tests.rs index f3fdc2a67f..425fe161f7 100644 --- a/source/resource_estimator/src/counts/tests.rs +++ b/source/resource_estimator/src/counts/tests.rs @@ -3,6 +3,7 @@ use std::convert::Into; +use crate::system::LogicalResourceCounts; use expect_test::{Expect, expect}; use indoc::indoc; use miette::Report; @@ -14,7 +15,7 @@ use qsc::{ use super::LogicalCounter; -fn verify_logical_counts(source: &str, entry: Option<&str>, expect: &Expect) { +fn run_logical_counts(source: &str, entry: Option<&str>) -> LogicalResourceCounts { let source_map = SourceMap::new([("test".into(), source.into())], entry.map(Into::into)); let (std_id, store) = qsc::compile::package_store_with_stdlib(TargetCapabilityFlags::all()); @@ -40,9 +41,7 @@ fn verify_logical_counts(source: &str, entry: Option<&str>, expect: &Expect) { let mut out = GenericReceiver::new(&mut stdout); match interpreter.eval_entry_with_sim(&mut counter, &mut out) { - Ok(_) => { - expect.assert_debug_eq(&counter.logical_resources()); - } + Ok(_) => counter.logical_resources(), Err(err) => { for e in err { let report = Report::from(e); @@ -53,6 +52,11 @@ fn verify_logical_counts(source: &str, entry: Option<&str>, expect: &Expect) { } } +fn verify_logical_counts(source: &str, entry: Option<&str>, expect: &Expect) { + let logical_counts = run_logical_counts(source, entry); + expect.assert_debug_eq(&logical_counts); +} + #[test] fn gates_are_counted() { verify_logical_counts( @@ -427,3 +431,23 @@ fn post_selection_can_take_impossible_branch() { "#]], ); } + +#[test] +fn manual_memory_qubits() { + let counts = run_logical_counts( + indoc! {" + import Std.Diagnostics.PostSelectZ; + + operation Main() : Unit { + use q = Qubit(); + Std.ResourceEstimation.MemoryQubitStore(q); + Std.ResourceEstimation.MemoryQubitLoad(q); + } + "}, + None, + ); + assert_eq!(counts.write_to_memory_count, Some(1)); + assert_eq!(counts.read_from_memory_count, Some(1)); + assert_eq!(counts.num_compute_qubits, Some(1)); + assert_eq!(counts.num_qubits, 2); +} From ec5f0622afa8a738be43f51294fa8bdeaee73ff9 Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Wed, 6 May 2026 22:27:56 -0700 Subject: [PATCH 2/5] Specify type of allocated qubits. --- library/std/src/Std/ResourceEstimation.qs | 17 +++- source/resource_estimator/src/counts.rs | 87 ++++++++++++------- source/resource_estimator/src/counts/tests.rs | 26 +++++- 3 files changed, 93 insertions(+), 37 deletions(-) diff --git a/library/std/src/Std/ResourceEstimation.qs b/library/std/src/Std/ResourceEstimation.qs index 6660d14f3b..4d2ea65aea 100644 --- a/library/std/src/Std/ResourceEstimation.qs +++ b/library/std/src/Std/ResourceEstimation.qs @@ -211,7 +211,6 @@ function LeastFrequentlyUsed() : Int { return 1; } - /// Signals to resource estimator that this qubit is stored to memory, i.e. /// transitions from "compute qubit" to "memory qubit". /// Each qubit is allocated as "compute" by default. @@ -231,6 +230,18 @@ function MemoryQubitLoad(q: Qubit) : Unit { body intrinsic; } + +/// All subsequent `use` statements will allocate compute qubits. +function AllocateComputeQubits() : Unit { + body intrinsic; +} + +/// All subsequent `use` statements will allocate memory qubits. +function AllocateMemoryQubits() : Unit { + body intrinsic; +} + + export SingleVariant, BeginEstimateCaching, @@ -250,4 +261,6 @@ export LeastRecentlyUsed, LeastFrequentlyUsed, MemoryQubitLoad, - MemoryQubitStore; + MemoryQubitStore, + AllocateComputeQubits, + AllocateMemoryQubits; diff --git a/source/resource_estimator/src/counts.rs b/source/resource_estimator/src/counts.rs index bce9b1ed64..e9bb4db2e3 100644 --- a/source/resource_estimator/src/counts.rs +++ b/source/resource_estimator/src/counts.rs @@ -16,10 +16,10 @@ use std::{array, cell::RefCell, f64::consts::PI, fmt::Debug, iter::Sum}; use crate::{counts::memory_compute::CachingStrategy, system::LogicalResourceCounts}; use memory_compute::{MemoryComputeInfo, QubitPool}; -#[derive(Clone, Copy)] -enum TypedQubit { - Compute(usize), - Memory(usize), +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +enum QubitType { + Compute, + Memory, } /// Resource counter implementation @@ -65,9 +65,11 @@ pub struct LogicalCounter { /// Pool of typed memory qubits used by manual MemoryQubitStore/Load. memory_qubit_pool: QubitPool, /// Mapping from untyped allocated qubit id to its typed counterpart/state. - typed_qubit_map: FxHashMap, + typed_qubit_map: FxHashMap, /// Peak number of typed qubits alive at once in manual memory-qubit mode. typed_qubit_peak: usize, + /// Type of next alloctaed qubit. + next_allocation_qubit_type: QubitType, manual_memory_reads: usize, manual_memory_writes: usize, @@ -96,6 +98,7 @@ impl Default for LogicalCounter { memory_qubit_pool: QubitPool::default(), typed_qubit_map: FxHashMap::default(), typed_qubit_peak: 0, + next_allocation_qubit_type: QubitType::Compute, manual_memory_reads: 0, manual_memory_writes: 0, manual_memory_mode: false, @@ -123,14 +126,14 @@ impl LogicalCounter { (None, None, None) }; - let extra_manual_typed_qubits = if self.manual_memory_mode { - self.typed_qubit_peak + let num_qubits = if self.manual_memory_mode { + self.compute_qubit_pool.max_in_use() + self.memory_qubit_pool.max_in_use() } else { - 0 + self.next_free }; LogicalResourceCounts { - num_qubits: (self.next_free + extra_manual_typed_qubits) as _, + num_qubits: num_qubits as _, t_count: self.t_count as _, rotation_count: self.r_count as _, rotation_depth: self.layers.iter().filter(|layer| layer.r != 0).count() as _, @@ -505,8 +508,10 @@ impl LogicalCounter { } for qubit in qubits { - if let Some(TypedQubit::Memory(_)) = self.typed_qubit_map.get(&qubit) { - panic!("cannot apply compute operation to a memory qubit"); + if let Some((qubit_type, _)) = self.typed_qubit_map.get(&qubit) { + if matches!(qubit_type, QubitType::Memory) { + panic!("cannot apply compute operation to a memory qubit"); + } } } } @@ -656,21 +661,22 @@ impl Backend for LogicalCounter { index }; - let compute_index = self.compute_qubit_pool.allocate(); + let typed_index = match self.next_allocation_qubit_type { + QubitType::Compute => self.compute_qubit_pool.allocate(), + QubitType::Memory => self.memory_qubit_pool.allocate(), + }; self.typed_qubit_map - .insert(index, TypedQubit::Compute(compute_index)); + .insert(index, (self.next_allocation_qubit_type, typed_index)); self.typed_qubit_peak = self.typed_qubit_peak.max(self.typed_qubit_map.len()); index } fn qubit_release(&mut self, q: usize) -> bool { - if let Some(typed) = self.typed_qubit_map.remove(&q) { - match typed { - TypedQubit::Compute(compute_index) => { - self.compute_qubit_pool.release(compute_index) - } - TypedQubit::Memory(memory_index) => self.memory_qubit_pool.release(memory_index), + if let Some((qubit_type, index)) = self.typed_qubit_map.remove(&q) { + match qubit_type { + QubitType::Compute => self.compute_qubit_pool.release(index), + QubitType::Memory => self.memory_qubit_pool.release(index), } } self.free_list.push(q); @@ -693,11 +699,11 @@ impl Backend for LogicalCounter { let q0_typed = self.typed_qubit_map.remove(&q0); let q1_typed = self.typed_qubit_map.remove(&q1); - if let Some(typed) = q0_typed { - self.typed_qubit_map.insert(q1, typed); + if let Some(typed_info) = q0_typed { + self.typed_qubit_map.insert(q1, typed_info); } - if let Some(typed) = q1_typed { - self.typed_qubit_map.insert(q0, typed); + if let Some(typed_info) = q1_typed { + self.typed_qubit_map.insert(q0, typed_info); } } @@ -778,36 +784,53 @@ impl Backend for LogicalCounter { "MemoryQubitLoad" => { self.manual_memory_mode = true; let q = arg.unwrap_qubit().deref().0; - let Some(TypedQubit::Memory(memory_index)) = self.typed_qubit_map.get(&q).copied() - else { + let Some((qubit_type, index)) = self.typed_qubit_map.get(&q).copied() else { return Some(Err( "MemoryQubitLoad can only be applied to a memory qubit".to_string() )); }; - self.memory_qubit_pool.release(memory_index); + if !matches!(qubit_type, QubitType::Memory) { + return Some(Err( + "MemoryQubitLoad can only be applied to a memory qubit".to_string() + )); + } + self.memory_qubit_pool.release(index); let compute_index = self.compute_qubit_pool.allocate(); self.typed_qubit_map - .insert(q, TypedQubit::Compute(compute_index)); + .insert(q, (QubitType::Compute, compute_index)); self.manual_memory_reads += 1; Some(Ok(Value::unit())) } "MemoryQubitStore" => { self.manual_memory_mode = true; let q = arg.unwrap_qubit().deref().0; - let Some(TypedQubit::Compute(compute_index)) = - self.typed_qubit_map.get(&q).copied() - else { + let Some((qubit_type, index)) = self.typed_qubit_map.get(&q).copied() else { return Some(Err( "MemoryQubitStore can only be applied to a compute qubit".to_string(), )); }; - self.compute_qubit_pool.release(compute_index); + if !matches!(qubit_type, QubitType::Compute) { + return Some(Err( + "MemoryQubitStore can only be applied to a compute qubit".to_string(), + )); + } + self.compute_qubit_pool.release(index); let memory_index = self.memory_qubit_pool.allocate(); self.typed_qubit_map - .insert(q, TypedQubit::Memory(memory_index)); + .insert(q, (QubitType::Memory, memory_index)); self.manual_memory_writes += 1; Some(Ok(Value::unit())) } + "AllocateComputeQubits" => { + self.manual_memory_mode = true; + self.next_allocation_qubit_type = QubitType::Compute; + Some(Ok(Value::unit())) + } + "AllocateMemoryQubits" => { + self.manual_memory_mode = true; + self.next_allocation_qubit_type = QubitType::Memory; + Some(Ok(Value::unit())) + } _ => None, } } diff --git a/source/resource_estimator/src/counts/tests.rs b/source/resource_estimator/src/counts/tests.rs index 425fe161f7..92db45d7dd 100644 --- a/source/resource_estimator/src/counts/tests.rs +++ b/source/resource_estimator/src/counts/tests.rs @@ -433,11 +433,9 @@ fn post_selection_can_take_impossible_branch() { } #[test] -fn manual_memory_qubits() { +fn manual_memory_qubits_load_store() { let counts = run_logical_counts( indoc! {" - import Std.Diagnostics.PostSelectZ; - operation Main() : Unit { use q = Qubit(); Std.ResourceEstimation.MemoryQubitStore(q); @@ -451,3 +449,25 @@ fn manual_memory_qubits() { assert_eq!(counts.num_compute_qubits, Some(1)); assert_eq!(counts.num_qubits, 2); } + +#[test] +fn manual_memory_qubits_allocator() { + let counts = run_logical_counts( + indoc! {" + operation Main() : Unit { + Std.ResourceEstimation.AllocateMemoryQubits(); + use q1 = Qubit[5]; + Std.ResourceEstimation.AllocateComputeQubits(); + use q2 = Qubit[2]; + Std.ResourceEstimation.MemoryQubitLoad(q1[0]); + } + "}, + None, + ); + + // Initially allocated 5 memory, 2 compute qubits. + // Then moved a qubit from memory to compute. + // So, in total need 5 memory and 3 compute qubits. + assert_eq!(counts.num_compute_qubits, Some(3)); + assert_eq!(counts.num_qubits, 8); +} From 12661e23c4ccba99933127770a25b31fa537bed3 Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Wed, 6 May 2026 23:21:11 -0700 Subject: [PATCH 3/5] no-op in sim --- source/compiler/qsc_eval/src/backend.rs | 6 +++++- source/compiler/qsc_partial_eval/src/lib.rs | 4 ++++ source/compiler/qsc_partial_eval/src/management.rs | 4 ++++ .../resource_estimator/src/counts/memory_compute.rs | 12 +----------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/source/compiler/qsc_eval/src/backend.rs b/source/compiler/qsc_eval/src/backend.rs index 36f4a42536..452e051525 100644 --- a/source/compiler/qsc_eval/src/backend.rs +++ b/source/compiler/qsc_eval/src/backend.rs @@ -1033,7 +1033,11 @@ impl Backend for SparseSim { | "AccountForEstimatesInternal" | "BeginRepeatEstimatesInternal" | "EndRepeatEstimatesInternal" - | "EnableMemoryComputeArchitecture" => Some(Ok(Value::unit())), + | "EnableMemoryComputeArchitecture" + | "MemoryQubitStore" + | "MemoryQubitLoad" + | "AllocateComputeQubits" + | "AllocateMemoryQubits" => Some(Ok(Value::unit())), "ConfigurePauliNoise" => { let [xv, yv, zv] = &*arg.unwrap_tuple() else { panic!("tuple arity for ConfigurePauliNoise intrinsic should be 3"); diff --git a/source/compiler/qsc_partial_eval/src/lib.rs b/source/compiler/qsc_partial_eval/src/lib.rs index 0994287cc9..d059294bdb 100644 --- a/source/compiler/qsc_partial_eval/src/lib.rs +++ b/source/compiler/qsc_partial_eval/src/lib.rs @@ -1798,6 +1798,10 @@ impl<'a> PartialEvaluator<'a> { | "BeginRepeatEstimatesInternal" | "EndRepeatEstimatesInternal" | "EnableMemoryComputeArchitecture" + | "MemoryQubitStore" + | "MemoryQubitLoad" + | "AllocateComputeQubits" + | "AllocateMemoryQubits" | "ApplyIdleNoise" | "GlobalPhase" | "Message" diff --git a/source/compiler/qsc_partial_eval/src/management.rs b/source/compiler/qsc_partial_eval/src/management.rs index 46f3355e65..8315e74f0a 100644 --- a/source/compiler/qsc_partial_eval/src/management.rs +++ b/source/compiler/qsc_partial_eval/src/management.rs @@ -149,6 +149,10 @@ impl Backend for QuantumIntrinsicsChecker { "BeginEstimateCaching" => Some(Ok(Value::Bool(true))), "EndEstimateCaching" | "EnableMemoryComputeArchitecture" + | "MemoryQubitStore" + | "MemoryQubitLoad" + | "AllocateComputeQubits" + | "AllocateMemoryQubits" | "GlobalPhase" | "ConfigurePauliNoise" | "ConfigureQubitLoss" diff --git a/source/resource_estimator/src/counts/memory_compute.rs b/source/resource_estimator/src/counts/memory_compute.rs index 4f188e68a6..8f2fcc8d59 100644 --- a/source/resource_estimator/src/counts/memory_compute.rs +++ b/source/resource_estimator/src/counts/memory_compute.rs @@ -9,8 +9,6 @@ mod tests; pub struct QubitPool { free_list: Vec, next_id: usize, - in_use: usize, - max_in_use: usize, } impl QubitPool { @@ -22,24 +20,16 @@ impl QubitPool { self.next_id += 1; index }; - self.in_use += 1; - if self.in_use > self.max_in_use { - self.max_in_use = self.in_use; - } index } pub fn release(&mut self, index: usize) { self.free_list.push(index); - self.in_use = self - .in_use - .checked_sub(1) - .expect("releasing from an empty qubit pool"); } pub fn max_in_use(&self) -> usize { - self.max_in_use + self.next_id } } From a44ca2850b0209701d332663bd36e6bcb5d2509a Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Thu, 7 May 2026 15:21:16 -0700 Subject: [PATCH 4/5] refactor into TypedQubitPools --- library/src/lib.rs | 4 + library/std/qsharp.json | 1 + library/std/src/Std/Memory.qs | 17 +++ library/std/src/Std/ResourceEstimation.qs | 37 +----- source/compiler/qsc_eval/src/backend.rs | 4 +- source/compiler/qsc_partial_eval/src/lib.rs | 4 +- .../qsc_partial_eval/src/management.rs | 2 - source/resource_estimator/src/counts.rs | 117 +++++------------- .../src/counts/memory_compute.rs | 32 +---- .../src/counts/qubit_pool.rs | 63 ++++++++++ source/resource_estimator/src/counts/tests.rs | 32 +---- 11 files changed, 125 insertions(+), 188 deletions(-) create mode 100644 library/std/src/Std/Memory.qs create mode 100644 source/resource_estimator/src/counts/qubit_pool.rs diff --git a/library/src/lib.rs b/library/src/lib.rs index 809a4ebe8e..665510de52 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -61,6 +61,10 @@ pub const STD_LIB: &[(&str, &str)] = &[ "qsharp-library-source:Std/Measurement.qs", include_str!("../std/src/Std/Measurement.qs"), ), + ( + "qsharp-library-source:Std/Memory.qs", + include_str!("../std/src/Std/Memory.qs"), + ), ( "qsharp-library-source:QIR/Intrinsic.qs", include_str!("../std/src/QIR/Intrinsic.qs"), diff --git a/library/std/qsharp.json b/library/std/qsharp.json index 9afd68a957..0deeba51b3 100644 --- a/library/std/qsharp.json +++ b/library/std/qsharp.json @@ -13,6 +13,7 @@ "src/Std/Logical.qs", "src/Std/Math.qs", "src/Std/Measurement.qs", + "src/Std/Memory.qs", "src/Std/Random.qs", "src/Std/Range.qs", "src/Std/ResourceEstimation.qs", diff --git a/library/std/src/Std/Memory.qs b/library/std/src/Std/Memory.qs new file mode 100644 index 0000000000..fb75347e19 --- /dev/null +++ b/library/std/src/Std/Memory.qs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + + +/// Loads a qubit from "memory/cold" qubit to "compute/hot" qubit. +/// Does nothing if qubit is already "hot". +function MemoryQubitLoad(q : Qubit) : Unit { + body intrinsic; +} + +/// Stores a qubit from "compute/hot" qubit to "memory/cold" qubit. +/// Does nothing if qubit is already "cold". +function MemoryQubitStore(q : Qubit) : Unit { + body intrinsic; +} + +export MemoryQubitLoad, MemoryQubitStore; diff --git a/library/std/src/Std/ResourceEstimation.qs b/library/std/src/Std/ResourceEstimation.qs index 4d2ea65aea..7d58ad9408 100644 --- a/library/std/src/Std/ResourceEstimation.qs +++ b/library/std/src/Std/ResourceEstimation.qs @@ -211,37 +211,6 @@ function LeastFrequentlyUsed() : Int { return 1; } -/// Signals to resource estimator that this qubit is stored to memory, i.e. -/// transitions from "compute qubit" to "memory qubit". -/// Each qubit is allocated as "compute" by default. -/// MemoryQubitStore can be applied only to "compute" qubit. That is, qubit on which -/// MemoryQubitLoad wasn't applied or if it was applied, it was followed by -/// MemoryQubitLoad. -/// While qubit is in "memory" state, no gates or measurements can be applied to it. -function MemoryQubitStore(q: Qubit) : Unit { - body intrinsic; -} - -/// Signals to resource estimator that this qubit is loaded from memory, i.e. -/// transitions from "memory qubit" to "compute qubit". -/// Can be applied only to "memory qubit", i.e. on which previously MemoryQubitStore -/// was applied. -function MemoryQubitLoad(q: Qubit) : Unit { - body intrinsic; -} - - -/// All subsequent `use` statements will allocate compute qubits. -function AllocateComputeQubits() : Unit { - body intrinsic; -} - -/// All subsequent `use` statements will allocate memory qubits. -function AllocateMemoryQubits() : Unit { - body intrinsic; -} - - export SingleVariant, BeginEstimateCaching, @@ -259,8 +228,4 @@ export RepeatEstimates, EnableMemoryComputeArchitecture, LeastRecentlyUsed, - LeastFrequentlyUsed, - MemoryQubitLoad, - MemoryQubitStore, - AllocateComputeQubits, - AllocateMemoryQubits; + LeastFrequentlyUsed; diff --git a/source/compiler/qsc_eval/src/backend.rs b/source/compiler/qsc_eval/src/backend.rs index 452e051525..6db8ea10db 100644 --- a/source/compiler/qsc_eval/src/backend.rs +++ b/source/compiler/qsc_eval/src/backend.rs @@ -1034,10 +1034,8 @@ impl Backend for SparseSim { | "BeginRepeatEstimatesInternal" | "EndRepeatEstimatesInternal" | "EnableMemoryComputeArchitecture" - | "MemoryQubitStore" | "MemoryQubitLoad" - | "AllocateComputeQubits" - | "AllocateMemoryQubits" => Some(Ok(Value::unit())), + | "MemoryQubitStore" => Some(Ok(Value::unit())), "ConfigurePauliNoise" => { let [xv, yv, zv] = &*arg.unwrap_tuple() else { panic!("tuple arity for ConfigurePauliNoise intrinsic should be 3"); diff --git a/source/compiler/qsc_partial_eval/src/lib.rs b/source/compiler/qsc_partial_eval/src/lib.rs index d059294bdb..2607b85545 100644 --- a/source/compiler/qsc_partial_eval/src/lib.rs +++ b/source/compiler/qsc_partial_eval/src/lib.rs @@ -1798,10 +1798,8 @@ impl<'a> PartialEvaluator<'a> { | "BeginRepeatEstimatesInternal" | "EndRepeatEstimatesInternal" | "EnableMemoryComputeArchitecture" - | "MemoryQubitStore" | "MemoryQubitLoad" - | "AllocateComputeQubits" - | "AllocateMemoryQubits" + | "MemoryQubitStore" | "ApplyIdleNoise" | "GlobalPhase" | "Message" diff --git a/source/compiler/qsc_partial_eval/src/management.rs b/source/compiler/qsc_partial_eval/src/management.rs index 8315e74f0a..541a273c43 100644 --- a/source/compiler/qsc_partial_eval/src/management.rs +++ b/source/compiler/qsc_partial_eval/src/management.rs @@ -151,8 +151,6 @@ impl Backend for QuantumIntrinsicsChecker { | "EnableMemoryComputeArchitecture" | "MemoryQubitStore" | "MemoryQubitLoad" - | "AllocateComputeQubits" - | "AllocateMemoryQubits" | "GlobalPhase" | "ConfigurePauliNoise" | "ConfigureQubitLoss" diff --git a/source/resource_estimator/src/counts.rs b/source/resource_estimator/src/counts.rs index e9bb4db2e3..bb340e9340 100644 --- a/source/resource_estimator/src/counts.rs +++ b/source/resource_estimator/src/counts.rs @@ -5,6 +5,7 @@ mod tests; mod memory_compute; +mod qubit_pool; use num_bigint::BigUint; use num_complex::Complex; @@ -14,13 +15,8 @@ use rustc_hash::FxHashMap; use std::{array, cell::RefCell, f64::consts::PI, fmt::Debug, iter::Sum}; use crate::{counts::memory_compute::CachingStrategy, system::LogicalResourceCounts}; -use memory_compute::{MemoryComputeInfo, QubitPool}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -enum QubitType { - Compute, - Memory, -} +use memory_compute::MemoryComputeInfo; +use qubit_pool::{QubitType, TypedQubitPools}; /// Resource counter implementation /// @@ -61,15 +57,7 @@ pub struct LogicalCounter { post_select_measurements: FxHashMap, /// Pool of typed compute qubits used by manual MemoryQubitStore/Load. - compute_qubit_pool: QubitPool, - /// Pool of typed memory qubits used by manual MemoryQubitStore/Load. - memory_qubit_pool: QubitPool, - /// Mapping from untyped allocated qubit id to its typed counterpart/state. - typed_qubit_map: FxHashMap, - /// Peak number of typed qubits alive at once in manual memory-qubit mode. - typed_qubit_peak: usize, - /// Type of next alloctaed qubit. - next_allocation_qubit_type: QubitType, + typed_qubit_pools: TypedQubitPools, manual_memory_reads: usize, manual_memory_writes: usize, @@ -94,11 +82,7 @@ impl Default for LogicalCounter { memory_compute: None, rnd: RefCell::new(StdRng::seed_from_u64(0)), post_select_measurements: FxHashMap::default(), - compute_qubit_pool: QubitPool::default(), - memory_qubit_pool: QubitPool::default(), - typed_qubit_map: FxHashMap::default(), - typed_qubit_peak: 0, - next_allocation_qubit_type: QubitType::Compute, + typed_qubit_pools: TypedQubitPools::default(), manual_memory_reads: 0, manual_memory_writes: 0, manual_memory_mode: false, @@ -118,7 +102,7 @@ impl LogicalCounter { ) } else if self.manual_memory_mode { ( - Some(self.compute_qubit_pool.max_in_use() as u64), + Some(self.typed_qubit_pools.max_in_use(QubitType::Compute) as u64), Some(self.manual_memory_reads as u64), Some(self.manual_memory_writes as u64), ) @@ -127,7 +111,8 @@ impl LogicalCounter { }; let num_qubits = if self.manual_memory_mode { - self.compute_qubit_pool.max_in_use() + self.memory_qubit_pool.max_in_use() + self.typed_qubit_pools.max_in_use(QubitType::Compute) + + self.typed_qubit_pools.max_in_use(QubitType::Memory) } else { self.next_free }; @@ -507,10 +492,12 @@ impl LogicalCounter { memory_compute.assert_compute_qubits(qubits.iter().copied()); } - for qubit in qubits { - if let Some((qubit_type, _)) = self.typed_qubit_map.get(&qubit) { - if matches!(qubit_type, QubitType::Memory) { - panic!("cannot apply compute operation to a memory qubit"); + if (self.manual_memory_mode) { + for qid in qubits { + if self.typed_qubit_pools.get_qubit_type(qid) == QubitType::Memory { + self.typed_qubit_pools.release(qid); + self.typed_qubit_pools.allocate(qid, QubitType::Compute); + self.manual_memory_reads += 1; } } } @@ -661,25 +648,14 @@ impl Backend for LogicalCounter { index }; - let typed_index = match self.next_allocation_qubit_type { - QubitType::Compute => self.compute_qubit_pool.allocate(), - QubitType::Memory => self.memory_qubit_pool.allocate(), - }; - self.typed_qubit_map - .insert(index, (self.next_allocation_qubit_type, typed_index)); - self.typed_qubit_peak = self.typed_qubit_peak.max(self.typed_qubit_map.len()); + self.typed_qubit_pools.allocate(index, QubitType::Compute); index } fn qubit_release(&mut self, q: usize) -> bool { - if let Some((qubit_type, index)) = self.typed_qubit_map.remove(&q) { - match qubit_type { - QubitType::Compute => self.compute_qubit_pool.release(index), - QubitType::Memory => self.memory_qubit_pool.release(index), - } - } self.free_list.push(q); + self.typed_qubit_pools.release(q); true } @@ -697,14 +673,7 @@ impl Backend for LogicalCounter { self.post_select_measurements.insert(q0, val); } - let q0_typed = self.typed_qubit_map.remove(&q0); - let q1_typed = self.typed_qubit_map.remove(&q1); - if let Some(typed_info) = q0_typed { - self.typed_qubit_map.insert(q1, typed_info); - } - if let Some(typed_info) = q1_typed { - self.typed_qubit_map.insert(q0, typed_info); - } + // TODO: do something correct here. } fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { @@ -783,52 +752,22 @@ impl Backend for LogicalCounter { } "MemoryQubitLoad" => { self.manual_memory_mode = true; - let q = arg.unwrap_qubit().deref().0; - let Some((qubit_type, index)) = self.typed_qubit_map.get(&q).copied() else { - return Some(Err( - "MemoryQubitLoad can only be applied to a memory qubit".to_string() - )); - }; - if !matches!(qubit_type, QubitType::Memory) { - return Some(Err( - "MemoryQubitLoad can only be applied to a memory qubit".to_string() - )); - } - self.memory_qubit_pool.release(index); - let compute_index = self.compute_qubit_pool.allocate(); - self.typed_qubit_map - .insert(q, (QubitType::Compute, compute_index)); - self.manual_memory_reads += 1; + let qid = arg.unwrap_qubit().deref().0; + + self.assert_compute_qubits([qid]); + Some(Ok(Value::unit())) } "MemoryQubitStore" => { self.manual_memory_mode = true; - let q = arg.unwrap_qubit().deref().0; - let Some((qubit_type, index)) = self.typed_qubit_map.get(&q).copied() else { - return Some(Err( - "MemoryQubitStore can only be applied to a compute qubit".to_string(), - )); - }; - if !matches!(qubit_type, QubitType::Compute) { - return Some(Err( - "MemoryQubitStore can only be applied to a compute qubit".to_string(), - )); + let qid = arg.unwrap_qubit().deref().0; + + if self.typed_qubit_pools.get_qubit_type(qid) == QubitType::Compute { + self.typed_qubit_pools.release(qid); + self.typed_qubit_pools.allocate(qid, QubitType::Memory); + self.manual_memory_writes += 1; } - self.compute_qubit_pool.release(index); - let memory_index = self.memory_qubit_pool.allocate(); - self.typed_qubit_map - .insert(q, (QubitType::Memory, memory_index)); - self.manual_memory_writes += 1; - Some(Ok(Value::unit())) - } - "AllocateComputeQubits" => { - self.manual_memory_mode = true; - self.next_allocation_qubit_type = QubitType::Compute; - Some(Ok(Value::unit())) - } - "AllocateMemoryQubits" => { - self.manual_memory_mode = true; - self.next_allocation_qubit_type = QubitType::Memory; + Some(Ok(Value::unit())) } _ => None, diff --git a/source/resource_estimator/src/counts/memory_compute.rs b/source/resource_estimator/src/counts/memory_compute.rs index 8f2fcc8d59..771470b285 100644 --- a/source/resource_estimator/src/counts/memory_compute.rs +++ b/source/resource_estimator/src/counts/memory_compute.rs @@ -5,34 +5,6 @@ use std::hash::Hash; #[cfg(test)] mod tests; -#[derive(Default)] -pub struct QubitPool { - free_list: Vec, - next_id: usize, -} - -impl QubitPool { - pub fn allocate(&mut self) -> usize { - let index = if let Some(index) = self.free_list.pop() { - index - } else { - let index = self.next_id; - self.next_id += 1; - index - }; - - index - } - - pub fn release(&mut self, index: usize) { - self.free_list.push(index); - } - - pub fn max_in_use(&self) -> usize { - self.next_id - } -} - pub enum CachingStrategy { LeastRecentlyUsed(LeastRecentlyUsedPriorityQueue), LeastFrequentlyUsed(LeastFrequentlyUsedPriorityQueue), @@ -74,6 +46,10 @@ impl MemoryComputeInfo { } } + pub fn store_to_memory(&mut self, qubit: usize) { + // TODO: implement. + } + pub fn compute_size(&self) -> usize { match &self.compute_qubits { CachingStrategy::LeastRecentlyUsed(lru) => lru.max_size(), diff --git a/source/resource_estimator/src/counts/qubit_pool.rs b/source/resource_estimator/src/counts/qubit_pool.rs new file mode 100644 index 0000000000..d0fba598cd --- /dev/null +++ b/source/resource_estimator/src/counts/qubit_pool.rs @@ -0,0 +1,63 @@ +use rustc_hash::FxHashMap; +use std::hash::Hash; + + +/// Pool of qubits for a specific type. +#[derive(Default)] +pub struct QubitPool { + current_in_use: usize, + pub max_in_use: usize, +} + +impl QubitPool { + pub fn allocate(&mut self) { + self.current_in_use += 1; + if self.current_in_use > self.max_in_use { + self.max_in_use = self.current_in_use; + } + } + + pub fn release(&mut self) { + self.current_in_use -= 1; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum QubitType { + Compute, + Memory, +} + +/// Maintains a mapping from qubit id to qubit type. +#[derive(Default)] +pub struct TypedQubitPools { + /// Qubit pool for each qubit type. + qubit_pools: FxHashMap, + /// Maps untyped qubit id to qubit type. + pub qubit_type_map: FxHashMap, +} + +impl TypedQubitPools { + /// Allocates typed qubit of given type. + pub fn allocate(&mut self, untyped_qubit_id: usize, qubit_type: QubitType) { + self.qubit_pools.entry(qubit_type).or_default().allocate(); + self.qubit_type_map.insert(untyped_qubit_id, qubit_type); + } + + /// Release a qubit back to whichever pool it belongs to, removing its mapping. + pub fn release(&mut self, untyped_qubit_id: usize) { + if let Some(qubit_type) = self.qubit_type_map.remove(&untyped_qubit_id) { + if let Some(pool) = self.qubit_pools.get_mut(&qubit_type) { + pool.release(); + } + } + } + + pub fn get_qubit_type(&self, untyped_qubit_id: usize) -> QubitType { + self.qubit_type_map.get(&untyped_qubit_id).unwrap().clone() + } + + pub fn max_in_use(&self, qubit_type: QubitType) -> usize { + self.qubit_pools.get(&qubit_type).unwrap().max_in_use + } +} diff --git a/source/resource_estimator/src/counts/tests.rs b/source/resource_estimator/src/counts/tests.rs index 92db45d7dd..f9c1bb9ba7 100644 --- a/source/resource_estimator/src/counts/tests.rs +++ b/source/resource_estimator/src/counts/tests.rs @@ -437,37 +437,15 @@ fn manual_memory_qubits_load_store() { let counts = run_logical_counts( indoc! {" operation Main() : Unit { - use q = Qubit(); - Std.ResourceEstimation.MemoryQubitStore(q); - Std.ResourceEstimation.MemoryQubitLoad(q); + use qs = Qubit[2]; + Std.Memory.MemoryQubitStore(qs[0]); + Std.Memory.MemoryQubitLoad(qs[0]); } "}, None, ); assert_eq!(counts.write_to_memory_count, Some(1)); assert_eq!(counts.read_from_memory_count, Some(1)); - assert_eq!(counts.num_compute_qubits, Some(1)); - assert_eq!(counts.num_qubits, 2); -} - -#[test] -fn manual_memory_qubits_allocator() { - let counts = run_logical_counts( - indoc! {" - operation Main() : Unit { - Std.ResourceEstimation.AllocateMemoryQubits(); - use q1 = Qubit[5]; - Std.ResourceEstimation.AllocateComputeQubits(); - use q2 = Qubit[2]; - Std.ResourceEstimation.MemoryQubitLoad(q1[0]); - } - "}, - None, - ); - - // Initially allocated 5 memory, 2 compute qubits. - // Then moved a qubit from memory to compute. - // So, in total need 5 memory and 3 compute qubits. - assert_eq!(counts.num_compute_qubits, Some(3)); - assert_eq!(counts.num_qubits, 8); + assert_eq!(counts.num_compute_qubits, Some(2)); + assert_eq!(counts.num_qubits, 3); } From 345060661f1ae6965ef87f4c932bc82cc0bab766 Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Fri, 8 May 2026 11:48:48 -0700 Subject: [PATCH 5/5] merge manual and automatic MemoryInfo --- source/resource_estimator/src/counts.rs | 95 +++++--------- .../src/counts/memory_compute.rs | 119 +++++++++++++----- .../src/counts/memory_compute/tests.rs | 1 + .../src/counts/qubit_pool.rs | 63 ---------- source/resource_estimator/src/counts/tests.rs | 10 +- 5 files changed, 122 insertions(+), 166 deletions(-) delete mode 100644 source/resource_estimator/src/counts/qubit_pool.rs diff --git a/source/resource_estimator/src/counts.rs b/source/resource_estimator/src/counts.rs index bb340e9340..90cc1d4ca6 100644 --- a/source/resource_estimator/src/counts.rs +++ b/source/resource_estimator/src/counts.rs @@ -5,7 +5,6 @@ mod tests; mod memory_compute; -mod qubit_pool; use num_bigint::BigUint; use num_complex::Complex; @@ -16,7 +15,6 @@ use std::{array, cell::RefCell, f64::consts::PI, fmt::Debug, iter::Sum}; use crate::{counts::memory_compute::CachingStrategy, system::LogicalResourceCounts}; use memory_compute::MemoryComputeInfo; -use qubit_pool::{QubitType, TypedQubitPools}; /// Resource counter implementation /// @@ -55,13 +53,6 @@ pub struct LogicalCounter { /// Map to track any post-select measurements by their associated qubit. /// This value is used in a measurement, if present, before generating a random result. post_select_measurements: FxHashMap, - - /// Pool of typed compute qubits used by manual MemoryQubitStore/Load. - typed_qubit_pools: TypedQubitPools, - - manual_memory_reads: usize, - manual_memory_writes: usize, - manual_memory_mode: bool, } impl Default for LogicalCounter { @@ -82,10 +73,6 @@ impl Default for LogicalCounter { memory_compute: None, rnd: RefCell::new(StdRng::seed_from_u64(0)), post_select_measurements: FxHashMap::default(), - typed_qubit_pools: TypedQubitPools::default(), - manual_memory_reads: 0, - manual_memory_writes: 0, - manual_memory_mode: false, } } } @@ -96,25 +83,16 @@ impl LogicalCounter { let (num_compute_qubits, read_from_memory_count, write_to_memory_count) = if let Some(memory_compute) = &self.memory_compute { ( - Some(memory_compute.compute_size() as u64), + Some(memory_compute.max_compute_qubits_count as u64), Some(memory_compute.read_from_memory_count() as u64), Some(memory_compute.write_to_memory_count() as u64), ) - } else if self.manual_memory_mode { - ( - Some(self.typed_qubit_pools.max_in_use(QubitType::Compute) as u64), - Some(self.manual_memory_reads as u64), - Some(self.manual_memory_writes as u64), - ) } else { (None, None, None) }; - - let num_qubits = if self.manual_memory_mode { - self.typed_qubit_pools.max_in_use(QubitType::Compute) - + self.typed_qubit_pools.max_in_use(QubitType::Memory) - } else { - self.next_free + let num_qubits = match &self.memory_compute { + Some(mc) => mc.max_compute_qubits_count + mc.max_memory_qubits_count, + None => self.next_free, }; LogicalResourceCounts { @@ -487,19 +465,8 @@ impl LogicalCounter { } fn assert_compute_qubits(&mut self, qubits: impl IntoIterator) { - let qubits: Vec<_> = qubits.into_iter().collect(); if let Some(memory_compute) = &mut self.memory_compute { - memory_compute.assert_compute_qubits(qubits.iter().copied()); - } - - if (self.manual_memory_mode) { - for qid in qubits { - if self.typed_qubit_pools.get_qubit_type(qid) == QubitType::Memory { - self.typed_qubit_pools.release(qid); - self.typed_qubit_pools.allocate(qid, QubitType::Compute); - self.manual_memory_reads += 1; - } - } + memory_compute.assert_compute_qubits(qubits); } } @@ -632,11 +599,17 @@ impl Backend for LogicalCounter { self.schedule_t(q); } - fn x(&mut self, _q: usize) {} + fn x(&mut self, q: usize) { + self.assert_compute_qubits([q]); + } - fn y(&mut self, _q: usize) {} + fn y(&mut self, q: usize) { + self.assert_compute_qubits([q]); + } - fn z(&mut self, _q: usize) {} + fn z(&mut self, q: usize) { + self.assert_compute_qubits([q]); + } fn qubit_allocate(&mut self) -> usize { let index = if let Some(index) = self.free_list.pop() { @@ -648,14 +621,14 @@ impl Backend for LogicalCounter { index }; - self.typed_qubit_pools.allocate(index, QubitType::Compute); - index } fn qubit_release(&mut self, q: usize) -> bool { self.free_list.push(q); - self.typed_qubit_pools.release(q); + if let Some(memory_compute) = &mut self.memory_compute { + memory_compute.release(q); + } true } @@ -672,8 +645,6 @@ impl Backend for LogicalCounter { if let Some(val) = q1_post_select { self.post_select_measurements.insert(q0, val); } - - // TODO: do something correct here. } fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { @@ -736,6 +707,18 @@ impl Backend for LogicalCounter { self.enable_memory_compute(compute_capacity, strategy); Some(Ok(Value::unit())) } + "MemoryQubitLoad" => { + let qid = arg.unwrap_qubit().deref().0; + self.assert_compute_qubits([qid]); + Some(Ok(Value::unit())) + } + "MemoryQubitStore" => { + if let Some(memory_compute) = &mut self.memory_compute { + let qid = arg.unwrap_qubit().deref().0; + memory_compute.move_to_memory(qid); + } + Some(Ok(Value::unit())) + } "GlobalPhase" | "ConfigurePauliNoise" | "ConfigureQubitLoss" | "ApplyIdleNoise" => { Some(Ok(Value::unit())) } @@ -750,26 +733,6 @@ impl Backend for LogicalCounter { Some(Ok(Value::unit())) } - "MemoryQubitLoad" => { - self.manual_memory_mode = true; - let qid = arg.unwrap_qubit().deref().0; - - self.assert_compute_qubits([qid]); - - Some(Ok(Value::unit())) - } - "MemoryQubitStore" => { - self.manual_memory_mode = true; - let qid = arg.unwrap_qubit().deref().0; - - if self.typed_qubit_pools.get_qubit_type(qid) == QubitType::Compute { - self.typed_qubit_pools.release(qid); - self.typed_qubit_pools.allocate(qid, QubitType::Memory); - self.manual_memory_writes += 1; - } - - Some(Ok(Value::unit())) - } _ => None, } } diff --git a/source/resource_estimator/src/counts/memory_compute.rs b/source/resource_estimator/src/counts/memory_compute.rs index 771470b285..774e0fd6aa 100644 --- a/source/resource_estimator/src/counts/memory_compute.rs +++ b/source/resource_estimator/src/counts/memory_compute.rs @@ -1,4 +1,5 @@ use rustc_hash::{FxHashMap, FxHashSet}; +use std::cmp::max; use std::collections::VecDeque; use std::hash::Hash; @@ -8,6 +9,7 @@ mod tests; pub enum CachingStrategy { LeastRecentlyUsed(LeastRecentlyUsedPriorityQueue), LeastFrequentlyUsed(LeastFrequentlyUsedPriorityQueue), + Manual, } impl CachingStrategy { @@ -18,65 +20,105 @@ impl CachingStrategy { pub fn least_frequently_used(capacity: usize) -> Self { CachingStrategy::LeastFrequentlyUsed(LeastFrequentlyUsedPriorityQueue::new(capacity)) } + + pub fn manual() -> Self { + CachingStrategy::Manual + } + + /// Inserts given qubits ids into the set of compute qubits. + /// Returns which compute qubits must be evicted to memory before given qubits can + /// be read or allocated. + fn insert_all(&mut self, qubit_ids: impl IntoIterator) -> Vec { + match self { + CachingStrategy::LeastRecentlyUsed(lru) => lru.insert_all(qubit_ids), + CachingStrategy::LeastFrequentlyUsed(lfu) => lfu.insert_all(qubit_ids), + CachingStrategy::Manual => vec![], + } + } } pub struct MemoryComputeInfo { - /// LRU or LFU set with qubits currently in compute mode - compute_qubits: CachingStrategy, - - /// Additional reads/writes not captured by the LRU or LFU set (e.g. when - /// manually counted for caching functions) - pub(crate) rfm_extra: usize, - pub(crate) wtm_extra: usize, + /// LRU or LFU set with qubits currently in compute mode. + strategy: CachingStrategy, + compute_qubits: FxHashSet, + memory_qubits: FxHashSet, + pub(crate) max_memory_qubits_count: usize, + pub(crate) max_compute_qubits_count: usize, + reads_count: usize, + writes_count: usize, } impl MemoryComputeInfo { pub fn new(strategy: CachingStrategy) -> Self { Self { - compute_qubits: strategy, - rfm_extra: 0, - wtm_extra: 0, + strategy: strategy, + compute_qubits: FxHashSet::default(), + memory_qubits: FxHashSet::default(), + max_memory_qubits_count: 0, + max_compute_qubits_count: 0, + reads_count: 0, + writes_count: 0, } } - pub fn assert_compute_qubits(&mut self, qubits: impl IntoIterator) { - match &mut self.compute_qubits { - CachingStrategy::LeastRecentlyUsed(lru) => lru.insert_all(qubits), - CachingStrategy::LeastFrequentlyUsed(lfu) => lfu.insert_all(qubits), + /// Moves this qubit to set of compute qubits. + /// If it was a memory qubit, records that as "read" operation. + /// External callers must call `assert_compute_qubits` instead of this method (so + /// automatic eviction can happen). + fn move_to_compute(&mut self, qid: usize) { + if self.memory_qubits.contains(&qid) { + self.memory_qubits.remove(&qid); + self.reads_count += 1; + } + self.compute_qubits.insert(qid); + self.max_compute_qubits_count = + max(self.max_compute_qubits_count, self.compute_qubits.len()); + } + + /// Moves this qubit to set of memory qubits. + /// If it was a compute qubit, records that as "write" operation. + pub fn move_to_memory(&mut self, qid: usize) { + if self.compute_qubits.contains(&qid) { + self.compute_qubits.remove(&qid); + self.writes_count += 1; } + self.memory_qubits.insert(qid); + self.max_memory_qubits_count = max(self.max_memory_qubits_count, self.memory_qubits.len()); } - pub fn store_to_memory(&mut self, qubit: usize) { - // TODO: implement. + /// Releases the qubit. + pub fn release(&mut self, qid: usize) { + self.compute_qubits.remove(&qid); + self.memory_qubits.remove(&qid); } - pub fn compute_size(&self) -> usize { - match &self.compute_qubits { - CachingStrategy::LeastRecentlyUsed(lru) => lru.max_size(), - CachingStrategy::LeastFrequentlyUsed(lfu) => lfu.max_size(), + /// Ensures that these qubits are compute qubits by doing reads if necessary. + /// If this requires evicting (writing) some qubits to memory, does that first. + pub fn assert_compute_qubits(&mut self, qubits: impl IntoIterator) { + let qubits_copy: Vec = qubits.into_iter().collect(); + let qubits_to_evict = self.strategy.insert_all(qubits_copy.clone()); + for qid in qubits_to_evict { + self.move_to_memory(qid); + } + for qid in qubits_copy { + self.move_to_compute(qid); } } pub fn read_from_memory_count(&self) -> usize { - match &self.compute_qubits { - CachingStrategy::LeastRecentlyUsed(lru) => lru.inserted_new_count() + self.rfm_extra, - CachingStrategy::LeastFrequentlyUsed(lfu) => lfu.inserted_new_count() + self.rfm_extra, - } + self.reads_count } pub fn write_to_memory_count(&self) -> usize { - match &self.compute_qubits { - CachingStrategy::LeastRecentlyUsed(lru) => lru.removed_count() + self.wtm_extra, - CachingStrategy::LeastFrequentlyUsed(lfu) => lfu.removed_count() + self.wtm_extra, - } + self.writes_count } pub fn increase_read_from_memory_count(&mut self, count: usize) { - self.rfm_extra += count; + self.reads_count += count; } pub fn increase_write_to_memory_count(&mut self, count: usize) { - self.wtm_extra += count; + self.writes_count += count; } } @@ -129,9 +171,9 @@ impl LeastRecentlyUsedPriorityQueue { /// Insert multiple keys ensuring they are all present afterwards. If more /// unique new keys are provided than capacity, only the most recently /// processed up to `capacity` will remain. - pub fn insert_all>(&mut self, keys: I) { + pub fn insert_all>(&mut self, keys: I) -> Vec { if self.capacity == 0 { - return; + return vec![]; } // Collect unique keys from input preserving order of first occurrence. let mut seen_input = FxHashSet::default(); @@ -148,6 +190,7 @@ impl LeastRecentlyUsedPriorityQueue { // Process each key in order; we evict as we go and since new elements // are moved front they will be retained if we exceed capacity. + let mut removed_keys: Vec = Vec::new(); for k in ordered { if self.contains(&k) { // Just update recency by moving element to front of deque @@ -166,6 +209,7 @@ impl LeastRecentlyUsedPriorityQueue { { self.map.remove(&key); self.removed += 1; + removed_keys.push(key); } self.map.insert(k.clone()); self.nodes.push_front(k); @@ -176,6 +220,8 @@ impl LeastRecentlyUsedPriorityQueue { if self.map.len() > self.max_size { self.max_size = self.map.len(); } + + return removed_keys; } } @@ -226,9 +272,10 @@ impl LeastFrequentlyUsedPriorityQueue { /// Insert multiple keys ensuring they are all present afterwards. If unique /// keys exceed capacity, only a subset up to capacity will remain. - pub fn insert_all>(&mut self, keys: I) { + /// Returns evicted keys. + pub fn insert_all>(&mut self, keys: I) -> Vec { if self.capacity == 0 { - return; + return vec![]; } let mut seen = FxHashSet::default(); let mut ordered: Vec = Vec::new(); @@ -245,6 +292,7 @@ impl LeastFrequentlyUsedPriorityQueue { // Evict as needed to make space for new keys. We need to evict before // adding the new elements, since frequency counters are low for new // elements and we risk to evict them before processing the whole input. + let mut removed_keys: Vec = Vec::new(); let new_missing = ordered .iter() .filter(|k| !self.map.contains_key(*k)) @@ -274,6 +322,7 @@ impl LeastFrequentlyUsedPriorityQueue { } if let Some(v) = victim { self.remove_key_internal(&v); + removed_keys.push(v); needed -= 1; } else { break; @@ -299,6 +348,8 @@ impl LeastFrequentlyUsedPriorityQueue { if self.map.len() > self.max_size { self.max_size = self.map.len(); } + + return removed_keys; } fn bump_bucket(&mut self, key: K, old_freq: u64, new_freq: u64) { diff --git a/source/resource_estimator/src/counts/memory_compute/tests.rs b/source/resource_estimator/src/counts/memory_compute/tests.rs index 96ef20c75a..8b100fba6a 100644 --- a/source/resource_estimator/src/counts/memory_compute/tests.rs +++ b/source/resource_estimator/src/counts/memory_compute/tests.rs @@ -1,6 +1,7 @@ use super::{LeastFrequentlyUsedPriorityQueue, LeastRecentlyUsedPriorityQueue}; // ---------------- LRU tests ----------------- + #[test] fn lru_insert_all_all_existing() { let mut lru = LeastRecentlyUsedPriorityQueue::new(4); diff --git a/source/resource_estimator/src/counts/qubit_pool.rs b/source/resource_estimator/src/counts/qubit_pool.rs deleted file mode 100644 index d0fba598cd..0000000000 --- a/source/resource_estimator/src/counts/qubit_pool.rs +++ /dev/null @@ -1,63 +0,0 @@ -use rustc_hash::FxHashMap; -use std::hash::Hash; - - -/// Pool of qubits for a specific type. -#[derive(Default)] -pub struct QubitPool { - current_in_use: usize, - pub max_in_use: usize, -} - -impl QubitPool { - pub fn allocate(&mut self) { - self.current_in_use += 1; - if self.current_in_use > self.max_in_use { - self.max_in_use = self.current_in_use; - } - } - - pub fn release(&mut self) { - self.current_in_use -= 1; - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum QubitType { - Compute, - Memory, -} - -/// Maintains a mapping from qubit id to qubit type. -#[derive(Default)] -pub struct TypedQubitPools { - /// Qubit pool for each qubit type. - qubit_pools: FxHashMap, - /// Maps untyped qubit id to qubit type. - pub qubit_type_map: FxHashMap, -} - -impl TypedQubitPools { - /// Allocates typed qubit of given type. - pub fn allocate(&mut self, untyped_qubit_id: usize, qubit_type: QubitType) { - self.qubit_pools.entry(qubit_type).or_default().allocate(); - self.qubit_type_map.insert(untyped_qubit_id, qubit_type); - } - - /// Release a qubit back to whichever pool it belongs to, removing its mapping. - pub fn release(&mut self, untyped_qubit_id: usize) { - if let Some(qubit_type) = self.qubit_type_map.remove(&untyped_qubit_id) { - if let Some(pool) = self.qubit_pools.get_mut(&qubit_type) { - pool.release(); - } - } - } - - pub fn get_qubit_type(&self, untyped_qubit_id: usize) -> QubitType { - self.qubit_type_map.get(&untyped_qubit_id).unwrap().clone() - } - - pub fn max_in_use(&self, qubit_type: QubitType) -> usize { - self.qubit_pools.get(&qubit_type).unwrap().max_in_use - } -} diff --git a/source/resource_estimator/src/counts/tests.rs b/source/resource_estimator/src/counts/tests.rs index f9c1bb9ba7..cc52fa8e7b 100644 --- a/source/resource_estimator/src/counts/tests.rs +++ b/source/resource_estimator/src/counts/tests.rs @@ -307,7 +307,7 @@ fn memory_annotations_work() { None, &expect![[r#" LogicalResourceCounts { - num_qubits: 20, + num_qubits: 21, t_count: 4, rotation_count: 8, rotation_depth: 5, @@ -318,10 +318,10 @@ fn memory_annotations_work() { 10, ), read_from_memory_count: Some( - 28, + 8, ), write_to_memory_count: Some( - 18, + 17, ), } "#]], @@ -437,7 +437,11 @@ fn manual_memory_qubits_load_store() { let counts = run_logical_counts( indoc! {" operation Main() : Unit { + Std.ResourceEstimation.EnableMemoryComputeArchitecture(100000, 0); + use qs = Qubit[2]; + X(qs[0]); + X(qs[1]); Std.Memory.MemoryQubitStore(qs[0]); Std.Memory.MemoryQubitLoad(qs[0]); }