This is an automated email from the ASF dual-hosted git repository.

rich7420 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/mahout.git


The following commit(s) were added to refs/heads/main by this push:
     new 206ff2fe8 Add estimate_memory() for pipeline CPU/GPU footprint (#1454)
206ff2fe8 is described below

commit 206ff2fe863f8bf7fc59f6bc938399ada842c8e3
Author: ChenChen Lai <[email protected]>
AuthorDate: Sat Aug 8 14:14:05 2026 +0800

    Add estimate_memory() for pipeline CPU/GPU footprint (#1454)
---
 qdp/qdp-core/src/estimate.rs   | 182 +++++++++++++++++++++++++++++++++++++++++
 qdp/qdp-core/src/lib.rs        |   2 +
 qdp/qdp-core/tests/estimate.rs | 138 +++++++++++++++++++++++++++++++
 3 files changed, 322 insertions(+)

diff --git a/qdp/qdp-core/src/estimate.rs b/qdp/qdp-core/src/estimate.rs
new file mode 100644
index 000000000..cd2247a0d
--- /dev/null
+++ b/qdp/qdp-core/src/estimate.rs
@@ -0,0 +1,182 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Pure, allocation-free memory estimation for pipeline configurations.
+//!
+//! [`estimate_memory`] answers "how much memory will this config need?" from 
the config alone —
+//! no allocation, no CUDA calls, no device queries — so callers can fail fast 
before the first
+//! batch instead of discovering an overflow mid-run.
+
+use crate::error::{MahoutError, Result};
+use crate::types::{Dtype, Encoding};
+
+/// Predicted memory footprint of a pipeline configuration, in bytes.
+///
+/// Produced by [`estimate_memory`]. Both fields are upper-bound estimates 
derived from config
+/// arithmetic only; nothing here reflects an actual allocation. See 
[`estimate_memory`] for the
+/// formulas behind each field and a worked example.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct MemoryEstimate {
+    /// Host-side prefetch pool: `prefetch_depth` batches of raw (unencoded) 
input.
+    pub cpu_prefetch_bytes: u64,
+    /// Device-side state-vector buffer, including a double-buffering 
allowance.
+    pub gpu_state_bytes: u64,
+}
+
+impl MemoryEstimate {
+    /// Combined host + device footprint in bytes.
+    ///
+    /// Saturates instead of overflowing: values produced by 
[`estimate_memory`] are checked to sum
+    /// without overflow, but the fields are public and may be set directly.
+    #[must_use]
+    pub const fn total(&self) -> u64 {
+        self.cpu_prefetch_bytes.saturating_add(self.gpu_state_bytes)
+    }
+}
+
+/// Estimate the CPU prefetch-pool and GPU state-buffer footprint of a 
pipeline configuration.
+///
+/// Pure function: performs no allocation, touches no device, and never panics 
— every product is
+/// computed with checked arithmetic and an overflow becomes 
[`MahoutError::InvalidInput`] naming
+/// the parameter responsible.
+///
+/// # Formulas
+///
+/// Let `n = num_qubits`, `bpe = dtype.bytes()` (4 for f32, 8 for f64), and
+/// `complex_bytes = 2 * bpe` (real + imaginary component).
+///
+/// | Field                | Formula                                           
  |
+/// 
|----------------------|-----------------------------------------------------|
+/// | `cpu_prefetch_bytes` | `prefetch_depth * batch_size * sample_len * bpe`  
   |
+/// | `gpu_state_bytes`    | `2 * batch_size * state_len * complex_bytes`      
   |
+/// | `total()`            | `cpu_prefetch_bytes + gpu_state_bytes`            
  |
+///
+/// `sample_len` is the per-sample **input** length and depends on the encoding
+/// ([`Encoding::vector_len`]): `2^n` for amplitude, `n` for angle/iqp-z/phase,
+/// `n + n*(n-1)/2` for iqp, `1` for basis. `state_len` is always `2^n` — 
every encoding produces a
+/// full state vector on the device regardless of how small its input is.
+///
+/// # `prefetch_depth`
+///
+/// Pass an **already-resolved** depth: this function does not replicate the 
pipeline's
+/// auto-compute logic (`PipelineConfig::normalize` / 
`compute_optimal_prefetch_depth`). `0` is that
+/// logic's sentinel, so passing it estimates a zero-byte pool rather than 
what the pipeline would
+/// actually allocate — run the config through `normalize` first.
+///
+/// # dtype fallback
+///
+/// A `Float32` request for an encoding without an f32 batch path
+/// ([`Encoding::supports_f32`]) is downgraded to `Float64` before estimating, 
mirroring
+/// `PipelineConfig::normalize` — otherwise the estimate would report half the 
memory the pipeline
+/// actually uses.
+///
+/// # Modeling caveats
+///
+/// 1. **The leading `2×` on `gpu_state_bytes` is a double-buffering allowance 
(state vector +
+///    recycle buffer), not the complex real/imaginary factor** — the 
real/imag pair is already
+///    inside `complex_bytes`. The batch path (`GpuStateVector::new_batch`) 
allocates a single
+///    `batch_size × state_len × complex_bytes` buffer, so the `2×` is a 
deliberate conservative
+///    upper bound.
+/// 2. **`cpu_prefetch_bytes` covers exactly `prefetch_depth` batches.** The 
true instantaneous peak
+///    can reach `prefetch_depth + 2` batches (one being produced and one 
being consumed alongside a
+///    full channel); this `+2` is **not** folded into the returned value, so 
the number stays
+///    aligned with the formula above. Budget headroom accordingly.
+/// 3. **The host-side streaming chunk buffer (Parquet reader) is excluded.** 
This estimate covers
+///    only the prefetch pool and GPU state buffer; folding in the reader's 
chunk buffer needs
+///    reader config beyond this signature and is tracked as a follow-up. B2 
must account for it
+///    separately for streaming sources.
+///
+/// # Errors
+///
+/// Returns [`MahoutError::InvalidInput`] when `num_qubits` is too large for 
`2^n` to be
+/// representable, or when any product overflows `u64`.
+///
+/// # Examples
+///
+/// 16-qubit amplitude, f32, batch 64, prefetch depth 16:
+///
+/// ```
+/// use qdp_core::{Dtype, Encoding, estimate_memory};
+///
+/// let est = estimate_memory(Encoding::Amplitude, 16, 64, Dtype::Float32, 
16).unwrap();
+/// // cpu: 16 * 64 * 65536 * 4 bytes
+/// assert_eq!(est.cpu_prefetch_bytes, 256 * 1024 * 1024);
+/// // gpu: 2 * 64 * 65536 * 8 bytes
+/// assert_eq!(est.gpu_state_bytes, 64 * 1024 * 1024);
+/// assert_eq!(est.total(), 320 * 1024 * 1024);
+/// ```
+pub fn estimate_memory(
+    encoding: Encoding,
+    num_qubits: u32,
+    batch_size: usize,
+    dtype: Dtype,
+    prefetch_depth: usize,
+) -> Result<MemoryEstimate> {
+    // Mirror PipelineConfig::normalize: f32 is only kept for encodings with a 
real f32 batch path.
+    let dtype = if matches!(dtype, Dtype::Float32) && !encoding.supports_f32() 
{
+        Dtype::Float64
+    } else {
+        dtype
+    };
+
+    // Device side always holds a full 2^n state vector per sample, whatever 
the input length is.
+    // Compute it with checked_shl rather than Encoding::vector_len, which 
would panic on `1 << n`
+    // for large n.
+    let state_len = 1u64.checked_shl(num_qubits).ok_or_else(|| {
+        MahoutError::InvalidInput(format!(
+            "num_qubits too large to estimate: {num_qubits} (2^n state vector 
is not representable)"
+        ))
+    })?;
+
+    let sample_len = match encoding {
+        Encoding::Amplitude => state_len,
+        _ => encoding.vector_len(num_qubits) as u64,
+    };
+
+    let bytes_per_elem = dtype.bytes() as u64;
+    let complex_bytes = 2 * bytes_per_elem;
+    let batch = batch_size as u64;
+    let depth = prefetch_depth as u64;
+
+    let cpu_prefetch_bytes = depth
+        .checked_mul(batch)
+        .and_then(|v| v.checked_mul(sample_len))
+        .and_then(|v| v.checked_mul(bytes_per_elem))
+        .ok_or_else(|| overflow_err("CPU prefetch pool", num_qubits, 
batch_size, prefetch_depth))?;
+
+    let gpu_state_bytes = 2u64
+        .checked_mul(batch)
+        .and_then(|v| v.checked_mul(state_len))
+        .and_then(|v| v.checked_mul(complex_bytes))
+        .ok_or_else(|| overflow_err("GPU state buffer", num_qubits, 
batch_size, prefetch_depth))?;
+
+    // total() adds the two; make sure that cannot overflow either.
+    cpu_prefetch_bytes
+        .checked_add(gpu_state_bytes)
+        .ok_or_else(|| overflow_err("total footprint", num_qubits, batch_size, 
prefetch_depth))?;
+
+    Ok(MemoryEstimate {
+        cpu_prefetch_bytes,
+        gpu_state_bytes,
+    })
+}
+
+fn overflow_err(what: &str, num_qubits: u32, batch_size: usize, depth: usize) 
-> MahoutError {
+    MahoutError::InvalidInput(format!(
+        "{what} size overflows u64 for num_qubits={num_qubits}, 
batch_size={batch_size}, \
+         prefetch_depth={depth}; reduce num_qubits or batch_size"
+    ))
+}
diff --git a/qdp/qdp-core/src/lib.rs b/qdp/qdp-core/src/lib.rs
index 95259793c..4454df953 100644
--- a/qdp/qdp-core/src/lib.rs
+++ b/qdp/qdp-core/src/lib.rs
@@ -22,6 +22,7 @@ pub mod dlpack;
 #[cfg(target_os = "linux")]
 mod encoding;
 pub mod error;
+pub mod estimate;
 pub mod gpu;
 pub mod io;
 mod platform;
@@ -36,6 +37,7 @@ pub mod types;
 mod profiling;
 
 pub use error::{MahoutError, Result, cuda_error_to_string};
+pub use estimate::{MemoryEstimate, estimate_memory};
 pub use gpu::cuda_ffi::cuda_runtime_available;
 pub use gpu::memory::Precision;
 pub use reader::{FloatElem, NullHandling, handle_float32_nulls, 
handle_float64_nulls};
diff --git a/qdp/qdp-core/tests/estimate.rs b/qdp/qdp-core/tests/estimate.rs
new file mode 100644
index 000000000..b0dff0375
--- /dev/null
+++ b/qdp/qdp-core/tests/estimate.rs
@@ -0,0 +1,138 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Tests for [`qdp_core::estimate_memory`].
+//!
+//! Byte values are derived from the memory model in issue #1429:
+//!   cpu_prefetch_bytes = prefetch_depth * batch_size * sample_len * 
bytes_per_elem
+//!   gpu_state_bytes    = 2 * batch_size * state_len * complex_bytes  
(complex_bytes = 2 * bpe)
+
+use qdp_core::{Dtype, Encoding, estimate_memory};
+
+const MIB: u64 = 1024 * 1024;
+const GIB: u64 = 1024 * 1024 * 1024;
+
+/// Reference case A from the issue: 16-qubit amplitude f32, batch 64, 
prefetch depth 16.
+#[test]
+fn reference_case_a_amplitude_16q_f32() {
+    let est = estimate_memory(Encoding::Amplitude, 16, 64, Dtype::Float32, 
16).unwrap();
+
+    // cpu: 16 * 64 * 2^16 * 4 = 268,435,456 (256 MiB)
+    assert_eq!(est.cpu_prefetch_bytes, 268_435_456);
+    assert_eq!(est.cpu_prefetch_bytes, 256 * MIB);
+
+    // gpu: 2 * 64 * 2^16 * 8 = 67,108,864 (64 MiB)
+    assert_eq!(est.gpu_state_bytes, 67_108_864);
+    assert_eq!(est.gpu_state_bytes, 64 * MIB);
+
+    // total: 335,544,320 (320 MiB)
+    assert_eq!(est.total(), 335_544_320);
+    assert_eq!(est.total(), 320 * MIB);
+}
+
+/// Reference case B from the issue: 20-qubit amplitude f32, batch 64, 
prefetch depth 1.
+#[test]
+fn reference_case_b_amplitude_20q_f32() {
+    let est = estimate_memory(Encoding::Amplitude, 20, 64, Dtype::Float32, 
1).unwrap();
+
+    // cpu: 1 * 64 * 2^20 * 4 = 268,435,456 (256 MiB)
+    assert_eq!(est.cpu_prefetch_bytes, 268_435_456);
+    assert_eq!(est.cpu_prefetch_bytes, 256 * MIB);
+
+    // gpu: 2 * 64 * 2^20 * 8 = 1,073,741,824 (1 GiB)
+    assert_eq!(est.gpu_state_bytes, 1_073_741_824);
+    assert_eq!(est.gpu_state_bytes, GIB);
+
+    // total: 1,342,177,280 (1.25 GiB)
+    assert_eq!(est.total(), 1_342_177_280);
+}
+
+/// f64 doubles both terms of case A via `Dtype::bytes()` (8 vs 4).
+#[test]
+fn case_a_f64_doubles_both_terms() {
+    let f64_est = estimate_memory(Encoding::Amplitude, 16, 64, Dtype::Float64, 
16).unwrap();
+
+    // cpu: 536,870,912 (512 MiB), gpu: 134,217,728 (128 MiB), total: 
671,088,640 (640 MiB)
+    assert_eq!(f64_est.cpu_prefetch_bytes, 536_870_912);
+    assert_eq!(f64_est.gpu_state_bytes, 134_217_728);
+    assert_eq!(f64_est.total(), 671_088_640);
+
+    // Exactly double the f32 case A.
+    let f32_est = estimate_memory(Encoding::Amplitude, 16, 64, Dtype::Float32, 
16).unwrap();
+    assert_eq!(f64_est.cpu_prefetch_bytes, 2 * f32_est.cpu_prefetch_bytes);
+    assert_eq!(f64_est.gpu_state_bytes, 2 * f32_est.gpu_state_bytes);
+    assert_eq!(f64_est.total(), 2 * f32_est.total());
+}
+
+#[test]
+fn angle_and_basis_have_tiny_cpu_side() {
+    // Angle input is one value per qubit; basis is a single index. The GPU 
state vector is 2^n
+    // either way, so the device side dominates by orders of magnitude.
+    let angle = estimate_memory(Encoding::Angle, 16, 64, Dtype::Float32, 
4).unwrap();
+    assert_eq!(angle.cpu_prefetch_bytes, 4 * 64 * 16 * 4);
+
+    let basis = estimate_memory(Encoding::Basis, 16, 64, Dtype::Float32, 
4).unwrap();
+    // depth 4 * batch 64 * sample_len 1 * 4 bytes
+    assert_eq!(basis.cpu_prefetch_bytes, 4 * 64 * 4);
+
+    let amplitude = estimate_memory(Encoding::Amplitude, 16, 64, 
Dtype::Float32, 4).unwrap();
+    assert_eq!(angle.gpu_state_bytes, amplitude.gpu_state_bytes);
+    assert_eq!(basis.gpu_state_bytes, amplitude.gpu_state_bytes);
+    assert!(angle.cpu_prefetch_bytes < amplitude.cpu_prefetch_bytes / 1000);
+}
+
+#[test]
+fn iqp_f32_falls_back_to_f64() {
+    // IQP has no f32 batch encode path, so a Float32 request must estimate as 
Float64 —
+    // otherwise the estimate would report half the memory the pipeline really 
uses.
+    let requested_f32 = estimate_memory(Encoding::Iqp, 30, 8, Dtype::Float32, 
2).unwrap();
+    let explicit_f64 = estimate_memory(Encoding::Iqp, 30, 8, Dtype::Float64, 
2).unwrap();
+    assert_eq!(requested_f32, explicit_f64);
+
+    // vector_len(iqp, 30) = 30 + 30*29/2 = 465
+    assert_eq!(requested_f32.cpu_prefetch_bytes, 2 * 8 * 465 * 8);
+    assert_eq!(requested_f32.gpu_state_bytes, 2 * 8 * (1u64 << 30) * 16);
+}
+
+#[test]
+fn amplitude_keeps_f32() {
+    let f32_est = estimate_memory(Encoding::Amplitude, 12, 16, Dtype::Float32, 
2).unwrap();
+    let f64_est = estimate_memory(Encoding::Amplitude, 12, 16, Dtype::Float64, 
2).unwrap();
+    assert_ne!(f32_est, f64_est);
+}
+
+#[test]
+fn oversized_qubit_count_errors_instead_of_panicking() {
+    // 2^63 is representable but every downstream product overflows.
+    assert!(estimate_memory(Encoding::Amplitude, 63, 64, Dtype::Float32, 
1).is_err());
+    // 2^64 is not representable at all.
+    assert!(estimate_memory(Encoding::Amplitude, 64, 1, Dtype::Float32, 
1).is_err());
+    assert!(estimate_memory(Encoding::Angle, 200, 1, Dtype::Float32, 
1).is_err());
+}
+
+#[test]
+fn oversized_batch_errors_instead_of_panicking() {
+    assert!(estimate_memory(Encoding::Amplitude, 30, usize::MAX, 
Dtype::Float64, 1).is_err());
+    assert!(estimate_memory(Encoding::Amplitude, 20, 1 << 40, Dtype::Float64, 
1024).is_err());
+}
+
+#[test]
+fn zero_batch_size_estimates_zero() {
+    let est = estimate_memory(Encoding::Amplitude, 16, 0, Dtype::Float32, 
4).unwrap();
+    assert_eq!(est.cpu_prefetch_bytes, 0);
+    assert_eq!(est.gpu_state_bytes, 0);
+    assert_eq!(est.total(), 0);
+}

Reply via email to