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 58e4aff32 [Testing][QDP] f32 Parquet fidelity tests and benchmark
(#1422)
58e4aff32 is described below
commit 58e4aff32c52e115b8fffd66eb84756b74e303f3
Author: ChenChen Lai <[email protected]>
AuthorDate: Sat Jul 4 16:13:01 2026 +0800
[Testing][QDP] f32 Parquet fidelity tests and benchmark (#1422)
* f32 Parquet fidelity tests and benchmark
* fix error
* address comment
---
.github/workflows/python-testing.yml | 18 +-
qdp/qdp-core/tests/common/mod.rs | 77 ++++++-
qdp/qdp-core/tests/parquet_f32_fidelity.rs | 257 ++++++++++++++++++++++
qdp/qdp-python/benchmark/README.md | 24 ++
qdp/qdp-python/benchmark/benchmark_parquet_f32.py | 215 ++++++++++++++++++
5 files changed, 587 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/python-testing.yml
b/.github/workflows/python-testing.yml
index 23061d612..9f025647a 100644
--- a/.github/workflows/python-testing.yml
+++ b/.github/workflows/python-testing.yml
@@ -41,9 +41,10 @@ on:
jobs:
rust-check:
- # Fast gate: type-check both with and without CUDA stubs so duplicate
- # stub definitions or cfg mismatches fail in ~30s instead of surfacing
- # during the slower maturin build below.
+ # Early gate: type-check both with and without CUDA stubs so duplicate
+ # stub definitions or cfg mismatches fail quickly instead of surfacing
+ # during the slower maturin build below. Also runs qdp-core f32 Parquet
+ # CPU smoke tests (issue #1342).
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -57,6 +58,17 @@ jobs:
QDP_NO_CUDA: "1"
run: cargo check --workspace --tests
+ # CPU smoke for the f32 Parquet pipeline (issue #1342). No GPU in CI, so
+ # GPU fidelity cases self-skip when the probe finds no functional GPU
+ # (stub build: kernel launch returns Err; no driver: cudarc panics and
+ # catch_unwind treats it as skip). Reader-level assertions still run.
+ # Scoped to these two binaries — other gpu_*.rs tests lack probe-skip.
+ - name: Cargo test (f32 Parquet CPU smoke)
+ working-directory: qdp
+ env:
+ QDP_NO_CUDA: "1"
+ run: cargo test -p qdp-core --test parquet_f32 --test
parquet_f32_fidelity
+
test:
needs: rust-check
runs-on: ubuntu-latest
diff --git a/qdp/qdp-core/tests/common/mod.rs b/qdp/qdp-core/tests/common/mod.rs
index 7114ad613..241012d0f 100644
--- a/qdp/qdp-core/tests/common/mod.rs
+++ b/qdp/qdp-core/tests/common/mod.rs
@@ -17,7 +17,9 @@
#[cfg(target_os = "linux")]
use std::sync::Arc;
-use arrow::array::{FixedSizeListArray, Float64Array};
+use arrow::array::{
+ Array, FixedSizeListArray, Float32Builder, Float64Array, Float64Builder,
ListBuilder,
+};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use parquet::arrow::ArrowWriter;
@@ -81,6 +83,79 @@ pub fn write_fixed_size_list_parquet(path: &str, data:
&[f64], sample_size: usiz
writer.close().unwrap();
}
+/// Writes a `List<Float32>` Parquet file; each `sample_size` consecutive
values in
+/// `data` form one row. Mirrors [`write_list_parquet_f64`] for the f32 column
path
+/// (issue #1342 Parquet f32 fidelity tests).
+#[allow(dead_code)]
+#[allow(clippy::manual_is_multiple_of)]
+pub fn write_list_parquet_f32(path: &str, data: &[f32], sample_size: usize) {
+ assert!(sample_size > 0, "sample_size must be > 0");
+ assert!(
+ data.len() % sample_size == 0,
+ "Data length ({}) must be a multiple of sample size ({})",
+ data.len(),
+ sample_size
+ );
+ use std::fs::File;
+ use std::sync::Arc;
+
+ let mut builder = ListBuilder::new(Float32Builder::new());
+ for row in data.chunks(sample_size) {
+ builder.values().append_slice(row);
+ builder.append(true);
+ }
+ let list_array = builder.finish();
+
+ let schema = Arc::new(Schema::new(vec![Field::new(
+ "data",
+ list_array.data_type().clone(),
+ true,
+ )]));
+ let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(list_array)
as _]).unwrap();
+
+ let file = File::create(path).unwrap();
+ let props = WriterProperties::builder().build();
+ let mut writer = ArrowWriter::try_new(file, schema, Some(props)).unwrap();
+ writer.write(&batch).unwrap();
+ writer.close().unwrap();
+}
+
+/// Writes a `List<Float64>` Parquet file; each `sample_size` consecutive
values in
+/// `data` form one row. Companion to [`write_list_parquet_f32`].
+#[allow(dead_code)]
+#[allow(clippy::manual_is_multiple_of)]
+pub fn write_list_parquet_f64(path: &str, data: &[f64], sample_size: usize) {
+ assert!(sample_size > 0, "sample_size must be > 0");
+ assert!(
+ data.len() % sample_size == 0,
+ "Data length ({}) must be a multiple of sample size ({})",
+ data.len(),
+ sample_size
+ );
+ use std::fs::File;
+ use std::sync::Arc;
+
+ let mut builder = ListBuilder::new(Float64Builder::new());
+ for row in data.chunks(sample_size) {
+ builder.values().append_slice(row);
+ builder.append(true);
+ }
+ let list_array = builder.finish();
+
+ let schema = Arc::new(Schema::new(vec![Field::new(
+ "data",
+ list_array.data_type().clone(),
+ true,
+ )]));
+ let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(list_array)
as _]).unwrap();
+
+ let file = File::create(path).unwrap();
+ let props = WriterProperties::builder().build();
+ let mut writer = ArrowWriter::try_new(file, schema, Some(props)).unwrap();
+ writer.write(&batch).unwrap();
+ writer.close().unwrap();
+}
+
/// Returns a CUDA device handle, or `None` when CUDA is unavailable for the
test environment.
#[cfg(target_os = "linux")]
#[allow(dead_code)]
diff --git a/qdp/qdp-core/tests/parquet_f32_fidelity.rs
b/qdp/qdp-core/tests/parquet_f32_fidelity.rs
new file mode 100644
index 000000000..58f40b9bd
--- /dev/null
+++ b/qdp/qdp-core/tests/parquet_f32_fidelity.rs
@@ -0,0 +1,257 @@
+//
+// 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.
+
+//! End-to-end f32 Parquet fidelity tests — issue #1342.
+//!
+//! These tests extend `gpu_fidelity.rs` (which compares f32 vs f64 encodings
on
+//! in-memory data) by sourcing the data from Parquet: the same deterministic
+//! amplitude data is written as both a `List<Float32>` and a `List<Float64>`
+//! column, read back through `ParquetReader::<f32>` / `ParquetReader::<f64>`,
and
+//! encoded with the f32 / f64 kernels respectively. We then compare the GPU
+//! state vectors with `fidelity_cross_precision`.
+//!
+//! Two layers:
+//! * CPU smoke (no GPU, runs everywhere): the f32 reader and the f64 reader
+//! return the same logical values up to the f64→f32 rounding. This is the
+//! part that actually asserts in CI (ubuntu, no GPU).
+//! * GPU fidelity (Linux + CUDA, skipped otherwise): full reader→kernel
+//! pipeline. Thresholds match `gpu_fidelity.rs` — amplitude f32 norm
+//! precision means ~1e-3 at 8–16 qubits, NOT 0.99999. The issue's
+//! "≥ 0.99999 where applicable" does not apply to amplitude encoding; see
+//! `gpu_fidelity.rs::compare_f32_f64_amplitude` for the established
baseline.
+
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+use qdp_core::reader::{DataReader, NullHandling};
+use qdp_core::readers::parquet::ParquetReader;
+
+mod common;
+
+static FILE_COUNTER: AtomicUsize = AtomicUsize::new(0);
+
+/// Deterministic amplitude data, identical generator to `gpu_fidelity.rs`.
+fn make_amplitude_data(num_samples: usize, sample_size: usize) -> Vec<f64> {
+ let total = num_samples * sample_size;
+ (0..total)
+ .map(|i| ((i as f64) + 1.0) / total as f64)
+ .collect()
+}
+
+/// Returns `(f32_path, f64_path)` for two temp Parquet files holding the same
+/// logical data as `List<Float32>` and `List<Float64>` respectively.
+fn write_pair(data_f64: &[f64], sample_size: usize) -> (TempFile, TempFile) {
+ let n = FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
+ let pid = std::process::id();
+ let f32_path =
std::env::temp_dir().join(format!("mahout_f32fid_{pid}_{n}_f32.parquet"));
+ let f64_path =
std::env::temp_dir().join(format!("mahout_f32fid_{pid}_{n}_f64.parquet"));
+
+ let data_f32: Vec<f32> = data_f64.iter().map(|&x| x as f32).collect();
+ common::write_list_parquet_f32(f32_path.to_str().unwrap(), &data_f32,
sample_size);
+ common::write_list_parquet_f64(f64_path.to_str().unwrap(), data_f64,
sample_size);
+
+ (TempFile(f32_path), TempFile(f64_path))
+}
+
+struct TempFile(std::path::PathBuf);
+
+impl Drop for TempFile {
+ fn drop(&mut self) {
+ let _ = std::fs::remove_file(&self.0);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// CPU smoke (no GPU) — this is what actually asserts in CI.
+// ---------------------------------------------------------------------------
+
+/// The f32 reader and the f64 reader return the same shape, and the f32 values
+/// equal the f64 values narrowed with `as f32` (Arrow cast == IEEE
+/// round-to-nearest, same as the f64→f32 path the kernel would see).
+#[test]
+fn test_parquet_f32_f64_reader_consistency() {
+ let num_samples = 4;
+ let sample_size = 256; // 8 qubits
+ let data_f64 = make_amplitude_data(num_samples, sample_size);
+ let (f32_file, f64_file) = write_pair(&data_f64, sample_size);
+
+ let mut reader_f32 =
+ ParquetReader::<f32>::new(&f32_file.0, None,
NullHandling::FillZero).unwrap();
+ let (data32, n32, s32) = reader_f32.read_batch().unwrap();
+
+ let mut reader_f64 =
+ ParquetReader::<f64>::new(&f64_file.0, None,
NullHandling::FillZero).unwrap();
+ let (data64, n64, s64) = reader_f64.read_batch().unwrap();
+
+ assert_eq!((n32, s32), (num_samples, sample_size));
+ assert_eq!((n64, s64), (num_samples, sample_size));
+ assert_eq!(data32.len(), data64.len());
+
+ for (i, (&v32, &v64)) in data32.iter().zip(data64.iter()).enumerate() {
+ assert_eq!(
+ v32, v64 as f32,
+ "f32 reader value {v32} != f64 narrowed value {} at index {i}",
+ v64 as f32
+ );
+ }
+}
+
+// ---------------------------------------------------------------------------
+// GPU fidelity (Linux + CUDA) — mirrors compare_f32_f64_amplitude, Parquet
src.
+// ---------------------------------------------------------------------------
+
+#[cfg(target_os = "linux")]
+mod gpu {
+ use super::*;
+ use qdp_core::Precision;
+ use qdp_core::gpu::metrics::{
+ download_complex_f32, download_complex_f64, fidelity_cross_precision,
+ };
+
+ /// Returns `true` only when CUDA kernels actually launch. Two distinct
+ /// no-GPU environments must both be detected as "skip":
+ /// * No toolkit but a driver present (the `QDP_NO_CUDA` stub): engine
and
+ /// device creation and host→device copies all succeed — only a kernel
+ /// launch fails with code 999, surfacing as an `Err` here.
+ /// * No driver at all (CI runner without `libcuda.so`): `cudarc` loads
the
+ /// driver dynamically and **panics** on first use rather than
returning
+ /// an `Err`, so `.ok()` does not catch it.
+ ///
+ /// We probe with a trivial 1-qubit amplitude encode inside `catch_unwind`
and
+ /// treat any panic or `Err` as "no functional GPU". A real GPU that errors
+ /// here is a genuine bug, which the `.expect(...)` calls below surface
loudly.
+ fn cuda_is_functional() -> bool {
+ // `cudarc`'s dlopen failure panics with a long message; silence the
+ // default hook just for the probe so a skipped run isn't mistaken for
a
+ // failure. Serialized because the panic hook is process-global.
+ static PROBE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
+ let _guard = PROBE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
+ let prev_hook = std::panic::take_hook();
+ std::panic::set_hook(Box::new(|_| {}));
+ let probe = std::panic::catch_unwind(|| {
+ let engine =
common::qdp_engine_with_precision(Precision::Float32)?;
+ let dlpack = engine
+ .encode_batch_f32(&[1.0_f32, 0.0], 1, 2, 1, "amplitude")
+ .ok()?;
+ unsafe { common::take_deleter_and_delete(dlpack) };
+ Some(())
+ });
+ std::panic::set_hook(prev_hook);
+ matches!(probe, Ok(Some(())))
+ }
+
+ /// Read the same data from an f32 and an f64 Parquet column, encode at the
+ /// matching precision, and return the minimum per-sample cross-precision
+ /// fidelity. `None` when no functional GPU is available.
+ fn parquet_amplitude_fidelity(num_qubits: usize) -> Option<f64> {
+ if !cuda_is_functional() {
+ return None;
+ }
+ let engine_f64 =
common::qdp_engine_with_precision(Precision::Float64)?;
+ let engine_f32 =
common::qdp_engine_with_precision(Precision::Float32)?;
+ let device = common::cuda_device()?;
+
+ let state_dim = 1usize << num_qubits;
+ let num_samples = 4;
+ let sample_size = state_dim;
+
+ let data_f64 = make_amplitude_data(num_samples, sample_size);
+ let (f32_file, f64_file) = write_pair(&data_f64, sample_size);
+
+ // f32 column → f32 reader → f32 kernel
+ let mut reader_f32 =
+ ParquetReader::<f32>::new(&f32_file.0, None,
NullHandling::FillZero).unwrap();
+ let (host_in_f32, _, _) = reader_f32.read_batch().unwrap();
+ let dlpack_f32 = engine_f32
+ .encode_batch_f32(
+ &host_in_f32,
+ num_samples,
+ sample_size,
+ num_qubits,
+ "amplitude",
+ )
+ .expect("F32 encode_batch should succeed");
+
+ // f64 column → f64 reader → f64 kernel
+ let mut reader_f64 =
+ ParquetReader::<f64>::new(&f64_file.0, None,
NullHandling::FillZero).unwrap();
+ let (host_in_f64, _, _) = reader_f64.read_batch().unwrap();
+ let dlpack_f64 = engine_f64
+ .encode_batch(
+ &host_in_f64,
+ num_samples,
+ sample_size,
+ num_qubits,
+ "amplitude",
+ )
+ .expect("F64 encode_batch should succeed");
+
+ let f64_tensor = unsafe { &(*dlpack_f64).dl_tensor };
+ let f32_tensor = unsafe { &(*dlpack_f32).dl_tensor };
+
+ let total_elements = num_samples * state_dim;
+ let host_f64 =
+ download_complex_f64(&device, f64_tensor.data as *const _,
total_elements).unwrap();
+ let host_f32 =
+ download_complex_f32(&device, f32_tensor.data as *const _,
total_elements).unwrap();
+
+ let mut min_fidelity = 1.0_f64;
+ for s in 0..num_samples {
+ let off = s * state_dim * 2;
+ let sample_f64 = &host_f64[off..off + state_dim * 2];
+ let sample_f32 = &host_f32[off..off + state_dim * 2];
+ let f = fidelity_cross_precision(sample_f32, sample_f64).unwrap();
+ if f < min_fidelity {
+ min_fidelity = f;
+ }
+ }
+
+ unsafe {
+ common::take_deleter_and_delete(dlpack_f64);
+ common::take_deleter_and_delete(dlpack_f32);
+ }
+
+ Some(min_fidelity)
+ }
+
+ /// Run the Parquet f32-vs-f64 amplitude case at `num_qubits`, skipping
when
+ /// no GPU is available. Threshold matches `gpu_fidelity.rs` (1e-3 at
8-16).
+ fn assert_amplitude_fidelity(num_qubits: usize) {
+ let Some(fidelity) = parquet_amplitude_fidelity(num_qubits) else {
+ println!("SKIP: No GPU available");
+ return;
+ };
+ println!("Parquet F32 vs F64 fidelity @ {num_qubits} qubits:
{fidelity:.10}");
+ assert!(
+ fidelity > 1.0 - 1e-3,
+ "Fidelity too low at {num_qubits} qubits: {fidelity}"
+ );
+ }
+
+ #[test]
+ fn test_parquet_f32_vs_f64_amplitude_8_qubits() {
+ assert_amplitude_fidelity(8);
+ }
+
+ #[test]
+ fn test_parquet_f32_vs_f64_amplitude_12_qubits() {
+ assert_amplitude_fidelity(12);
+ }
+
+ #[test]
+ fn test_parquet_f32_vs_f64_amplitude_16_qubits() {
+ assert_amplitude_fidelity(16);
+ }
+}
diff --git a/qdp/qdp-python/benchmark/README.md
b/qdp/qdp-python/benchmark/README.md
index d30715a1c..6d11bfdf9 100644
--- a/qdp/qdp-python/benchmark/README.md
+++ b/qdp/qdp-python/benchmark/README.md
@@ -9,6 +9,8 @@ scripts:
that measures vectors/sec across Mahout, PennyLane, and Qiskit.
- `benchmark_latency.py`: Data-to-State latency benchmark (CPU RAM -> GPU
VRAM).
- `benchmark_phase.py`: GPU phase encoding latency benchmark (batch encode
timing).
+- `benchmark_parquet_f32.py`: end-to-end f32 vs f64 Parquet pipeline throughput
+ (vectors/sec and f32/f64 speedup); see [Parquet f32 vs
f64](#parquet-f32-vs-f64-throughput).
## Quick Start
@@ -34,10 +36,32 @@ uv run --project qdp/qdp-python python
qdp/qdp-python/benchmark/benchmark_e2e.py
uv run --project qdp/qdp-python python
qdp/qdp-python/benchmark/benchmark_latency.py
uv run --project qdp/qdp-python python
qdp/qdp-python/benchmark/benchmark_throughput.py
uv run --project qdp/qdp-python python
qdp/qdp-python/benchmark/benchmark_phase.py
+uv run --project qdp/qdp-python python
qdp/qdp-python/benchmark/benchmark_parquet_f32.py
```
This keeps all benchmark dependencies in the unified repo root venv
(`mahout/.venv`).
+## Parquet f32 vs f64 throughput
+
+`benchmark_parquet_f32.py` reads the same amplitude data from a Parquet file
+through `QuantumDataLoader` at `dtype("float32")` and `dtype("float64")`, and
+reports vectors/sec for each plus the f32/f64 speedup (issue #1342). The
+expected win is ~25-35% when the Parquet column is native f32.
+
+```bash
+uv run --project qdp/qdp-python python
qdp/qdp-python/benchmark/benchmark_parquet_f32.py \
+ --qubits 12 --batches 200 --batch-size 64
+```
+
+By default two temporary files holding the same data — a native
+`FixedSizeList<Float32>` and a native `FixedSizeList<Float64>` — are generated
+and each is read at its matching dtype, so the f32 run measures the zero-copy
+native-f32 path (not a f64→f32 cast). Pass `--parquet PATH` to benchmark your
+own file at both dtypes instead. The generated data is materialized in RAM and
+on disk and grows as `batches * batch_size * 2^qubits`, so raise `--qubits`
with
+that cost in mind. Requires an NVIDIA GPU (the native file loader is Linux +
+CUDA only); not run in CI.
+
## Manual Setup
If you prefer to set up manually (or if `make benchmark` is not available):
diff --git a/qdp/qdp-python/benchmark/benchmark_parquet_f32.py
b/qdp/qdp-python/benchmark/benchmark_parquet_f32.py
new file mode 100644
index 000000000..396836b42
--- /dev/null
+++ b/qdp/qdp-python/benchmark/benchmark_parquet_f32.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+#
+# 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.
+
+"""End-to-end f32 vs f64 Parquet pipeline throughput benchmark (issue #1342).
+
+Reads amplitude data from a Parquet file through ``QuantumDataLoader`` at
+``dtype("float32")`` and ``dtype("float64")`` and reports encoded
+vectors/second for each, plus the f32/f64 speedup. The expected win is
+~25-35% when the Parquet column is native f32 (see #1338).
+
+To actually exercise the native f32 path, the default (no ``--parquet``) run
+generates *two* files holding the same logical data — one native
+``FixedSizeList<Float32>`` and one native ``FixedSizeList<Float64>`` — and
reads
+each at its matching dtype, so the f32 run is the zero-copy native-f32 read,
not
+a f64→f32 cast. With ``--parquet PATH`` the same user file is read at both
+dtypes (a native-f32 column hits the zero-copy path; a f64 column is cast to
f32
+on read).
+
+This is a GPU/Linux-only benchmark: ``QuantumDataLoader``'s native file loader
is
+only available on Linux with CUDA, mirroring ``benchmark_phase.py``. It is NOT
+wired into CI; run it locally on a GPU box.
+
+Run from the repo root::
+
+ uv run --project qdp/qdp-python \\
+ python qdp/qdp-python/benchmark/benchmark_parquet_f32.py \\
+ --qubits 12 --batches 200 --batch-size 64
+
+The generated data is materialized in RAM and on disk and grows as
+``batches * batch_size * 2^qubits``, so raise ``--qubits`` with that cost in
mind.
+"""
+
+from __future__ import annotations
+
+import argparse
+import os
+import tempfile
+import time
+
+import numpy as np
+import pyarrow as pa
+import pyarrow.parquet as pq
+from qumat_qdp import QuantumDataLoader
+
+
+def _generate_parquet(
+ path: str,
+ total_rows: int,
+ sample_size: int,
+ np_dtype: type[np.floating],
+ pa_type: pa.DataType,
+) -> None:
+ """Write a ``FixedSizeList<pa_type, sample_size>`` Parquet file of random
data.
+
+ Each call generates its own data from a fixed seed, so the f32 and f64
files
+ hold the same logical values without the caller keeping both large host
+ arrays alive at once. A FixedSizeList has no offsets buffer, so this also
+ sidesteps the int32 offset overflow a variable ``List`` hits once
+ ``total_rows * sample_size`` exceeds 2^31 (large --qubits).
+ """
+ flat = np.random.default_rng(0).random(total_rows * sample_size)
+ if np_dtype != np.float64:
+ flat = flat.astype(np_dtype)
+ values = pa.array(flat, type=pa_type)
+ list_array = pa.FixedSizeListArray.from_arrays(values, sample_size)
+ pq.write_table(pa.table({"data": list_array}), path)
+
+
+def run_loader(
+ path: str,
+ dtype: str,
+ num_qubits: int,
+ total_batches: int,
+ batch_size: int,
+ encoding_method: str,
+) -> tuple[float, float]:
+ """Iterate the loader at the given dtype; return (duration_sec, vec/s).
+
+ The native file loader reads the *entire* file regardless of ``--batches``:
+ ``total_batches`` is ignored for file sources (only ``--batch-size`` sets
+ batch granularity), and iteration runs until EOF. So the loop processes
+ exactly the file's Parquet row count, and ``num_rows / elapsed`` is the
true
+ throughput for any ``--parquet`` file -- not just the generated case where
+ ``num_rows == batches * batch_size``. (Do not clamp to ``batches *
+ batch_size``: for a file larger than that, the whole file is still read, so
+ clamping would divide the full read time by an undercount.)
+ """
+ total_vectors = pq.read_metadata(path).num_rows
+ loader = (
+ QuantumDataLoader(device_id=0)
+ .qubits(num_qubits)
+ .encoding(encoding_method)
+ .batches(total_batches, size=batch_size)
+ .dtype(dtype)
+ .source_file(path)
+ )
+ start = time.perf_counter()
+ for _ in loader: # drive the pipeline; the encoded batch is not needed
+ pass
+ elapsed = max(time.perf_counter() - start, 1e-9)
+ return elapsed, total_vectors / elapsed
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(
+ description="f32 vs f64 Parquet pipeline throughput benchmark"
+ )
+ # Default sizes are deliberately modest: the generated data is materialized
+ # in host RAM and written to disk (one f32 + one f64 file), so it grows as
+ # batches * batch_size * 2^qubits. 12 qubits keeps the default run ~0.4 GB;
+ # raise --qubits with that cost in mind.
+ parser.add_argument("--qubits", type=int, default=12)
+ parser.add_argument(
+ "--batches",
+ type=int,
+ default=200,
+ help="Number of batches. Sizes the generated data (batches *
batch_size "
+ "rows); ignored with --parquet, where the whole file is always read.",
+ )
+ parser.add_argument("--batch-size", type=int, default=64)
+ parser.add_argument("--encoding", type=str, default="amplitude")
+ parser.add_argument("--trials", type=int, default=3)
+ parser.add_argument(
+ "--parquet",
+ type=str,
+ default=None,
+ help="Existing Parquet file to benchmark at both dtypes; "
+ "if omitted, native f32 and f64 temp files are generated.",
+ )
+ args = parser.parse_args()
+
+ sample_size = 1 << args.qubits
+ total_rows = args.batches * args.batch_size
+
+ tmp_paths: list[str] = []
+ # Map each dtype to the file it should read. For the generated case each
+ # dtype reads its own native-precision file (no cast); for --parquet both
+ # read the user's single file.
+ paths: dict[str, str]
+ if args.parquet is None:
+ f64_fd, f64_path = tempfile.mkstemp(suffix="_bench_f64.parquet")
+ f32_fd, f32_path = tempfile.mkstemp(suffix="_bench_f32.parquet")
+ os.close(f64_fd)
+ os.close(f32_fd)
+ # Register for cleanup before generating, so a failure mid-generation
+ # (e.g. MemoryError on large --qubits, or disk-full) still removes
them.
+ tmp_paths = [f64_path, f32_path]
+ paths = {"float64": f64_path, "float32": f32_path}
+ else:
+ paths = {"float64": args.parquet, "float32": args.parquet}
+
+ try:
+ if args.parquet is None:
+ print(
+ f"Generating {total_rows} rows x {sample_size} cols (native
f32 + f64)"
+ )
+ _generate_parquet(
+ paths["float64"], total_rows, sample_size, np.float64,
pa.float64()
+ )
+ _generate_parquet(
+ paths["float32"], total_rows, sample_size, np.float32,
pa.float32()
+ )
+
+ print("=" * 70)
+ print("Parquet f32 vs f64 pipeline throughput")
+ print(
+ f" qubits={args.qubits}, batches={args.batches}, "
+ f"batch_size={args.batch_size}, encoding={args.encoding}"
+ )
+ print("=" * 70)
+
+ results: dict[str, float] = {}
+ for dtype in ("float64", "float32"):
+ vps_trials: list[float] = []
+ for t in range(args.trials):
+ dur, vps = run_loader(
+ paths[dtype],
+ dtype,
+ args.qubits,
+ args.batches,
+ args.batch_size,
+ args.encoding,
+ )
+ vps_trials.append(vps)
+ print(f" [{dtype}] trial {t + 1}: {dur:.4f} s, {vps:.1f}
vec/s")
+ results[dtype] = sorted(vps_trials)[len(vps_trials) // 2]
+ print(f" [{dtype}] median: {results[dtype]:.1f} vec/s")
+
+ print("-" * 70)
+ if results["float64"] <= 0.0:
+ print(" No vectors encoded (empty file?); skipping speedup.")
+ else:
+ speedup = results["float32"] / results["float64"]
+ print(f" f32/f64 speedup: {speedup:.3f}x ({(speedup - 1.0) *
100:+.1f}%)")
+ finally:
+ for p in tmp_paths:
+ os.remove(p)
+
+
+if __name__ == "__main__":
+ main()