This is an automated email from the ASF dual-hosted git repository.
ryankert01 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 6cfbf10ec feat(qdp): Encoding + Dtype enums, static encoder dispatch
(#1276)
6cfbf10ec is described below
commit 6cfbf10ecb023ddb870c5fe80623b0893442f6ac
Author: KUAN-HAO HUANG <[email protected]>
AuthorDate: Mon May 11 18:24:28 2026 +0800
feat(qdp): Encoding + Dtype enums, static encoder dispatch (#1276)
---
qdp/DEVELOPMENT.md | 6 +
qdp/qdp-core/src/encoding/mod.rs | 12 +-
qdp/qdp-core/src/gpu/encodings/iqp.rs | 16 ++
qdp/qdp-core/src/gpu/encodings/mod.rs | 20 +-
qdp/qdp-core/src/gpu/mod.rs | 2 +-
qdp/qdp-core/src/lib.rs | 55 +++++-
qdp/qdp-core/src/pipeline_runner.rs | 232 +++++++++++-----------
qdp/qdp-core/src/types.rs | 160 +++++++++++++++
qdp/qdp-core/tests/gpu_angle_encoding.rs | 22 +++
qdp/qdp-core/tests/gpu_iqp_encoding.rs | 4 +-
qdp/qdp-core/tests/gpu_validation.rs | 4 +-
qdp/qdp-core/tests/types.rs | 73 +++++++
qdp/qdp-python/README.md | 10 +
qdp/qdp-python/qumat_qdp/api.py | 4 +-
qdp/qdp-python/qumat_qdp/loader.py | 15 ++
qdp/qdp-python/src/engine.rs | 283 +++++++--------------------
qdp/qdp-python/src/lib.rs | 10 +-
qdp/qdp-python/src/loader.rs | 16 +-
qdp/qdp-python/src/pytorch.rs | 42 ++--
testing/qdp/test_bindings.py | 6 +-
testing/qdp_python/test_dlpack_validation.py | 4 +-
21 files changed, 584 insertions(+), 412 deletions(-)
diff --git a/qdp/DEVELOPMENT.md b/qdp/DEVELOPMENT.md
index fc927022d..42ad61c4b 100644
--- a/qdp/DEVELOPMENT.md
+++ b/qdp/DEVELOPMENT.md
@@ -84,6 +84,12 @@ cargo test --workspace
cd ..
```
+**Encoding / pipeline dtype:** `qdp_core::Encoding::supports_f32` gates whether
+`PipelineConfig::normalize()` keeps `dtype = Float32` for the synthetic
pipeline. It reflects
+**which encoders implement `encode_batch_f32` today** (currently amplitude
only), not every
+encoding that might eventually get a batch f32 path. When angle/basis gain
real batch f32
+support, widen `supports_f32` and adjust tests accordingly.
+
Run Python tests:
```bash
diff --git a/qdp/qdp-core/src/encoding/mod.rs b/qdp/qdp-core/src/encoding/mod.rs
index d795ca4a7..2d09b5146 100644
--- a/qdp/qdp-core/src/encoding/mod.rs
+++ b/qdp/qdp-core/src/encoding/mod.rs
@@ -63,6 +63,7 @@ use crate::dlpack::DLManagedTensor;
use crate::gpu::PipelineContext;
use crate::gpu::memory::{GpuStateVector, PinnedHostBuffer};
use crate::reader::StreamingDataReader;
+use crate::types::Encoding;
use crate::{MahoutError, QdpEngine, Result};
/// 512MB staging buffer for large Parquet row groups (reduces fragmentation)
@@ -370,22 +371,23 @@ pub(crate) fn encode_from_parquet(
num_qubits: usize,
encoding_method: &str,
) -> Result<*mut DLManagedTensor> {
- match encoding_method {
- "amplitude" => {
+ let encoding = Encoding::from_str_ci(encoding_method)?;
+ match encoding {
+ Encoding::Amplitude => {
crate::profile_scope!("Mahout::EncodeAmplitudeFromParquet");
stream_encode(engine, path, num_qubits,
amplitude::AmplitudeEncoder)
}
- "angle" => {
+ Encoding::Angle => {
crate::profile_scope!("Mahout::EncodeAngleFromParquet");
stream_encode(engine, path, num_qubits, angle::AngleEncoder)
}
- "basis" => {
+ Encoding::Basis => {
crate::profile_scope!("Mahout::EncodeBasisFromParquet");
stream_encode(engine, path, num_qubits, basis::BasisEncoder)
}
_ => Err(MahoutError::NotImplemented(format!(
"Encoding method '{}' not supported for streaming",
- encoding_method
+ encoding.as_str()
))),
}
}
diff --git a/qdp/qdp-core/src/gpu/encodings/iqp.rs
b/qdp/qdp-core/src/gpu/encodings/iqp.rs
index c6ecf1762..33d18cfaf 100644
--- a/qdp/qdp-core/src/gpu/encodings/iqp.rs
+++ b/qdp/qdp-core/src/gpu/encodings/iqp.rs
@@ -23,6 +23,7 @@ use crate::error::{MahoutError, Result};
use crate::gpu::memory::{GpuStateVector, Precision};
use cudarc::driver::CudaDevice;
use std::sync::Arc;
+use std::sync::OnceLock;
#[cfg(target_os = "linux")]
use crate::gpu::memory::map_allocation_error;
@@ -405,3 +406,18 @@ impl QuantumEncoder for IqpEncoder {
}
}
}
+
+static IQP_FULL: OnceLock<IqpEncoder> = OnceLock::new();
+static IQP_Z_ONLY: OnceLock<IqpEncoder> = OnceLock::new();
+
+/// Shared `'static` IQP encoder (full ZZ). Used by
[`crate::Encoding::encoder`](crate::Encoding::encoder).
+#[must_use]
+pub fn iqp_full_encoder() -> &'static IqpEncoder {
+ IQP_FULL.get_or_init(IqpEncoder::full)
+}
+
+/// Shared `'static` IQP-Z encoder. Used by
[`crate::Encoding::encoder`](crate::Encoding::encoder).
+#[must_use]
+pub fn iqp_z_encoder() -> &'static IqpEncoder {
+ IQP_Z_ONLY.get_or_init(IqpEncoder::z_only)
+}
diff --git a/qdp/qdp-core/src/gpu/encodings/mod.rs
b/qdp/qdp-core/src/gpu/encodings/mod.rs
index fa6362d4c..3f256e68a 100644
--- a/qdp/qdp-core/src/gpu/encodings/mod.rs
+++ b/qdp/qdp-core/src/gpu/encodings/mod.rs
@@ -58,7 +58,7 @@ pub fn validate_qubit_count(num_qubits: usize) -> Result<()> {
/// Quantum encoding strategy interface
/// Implemented by: AmplitudeEncoder, AngleEncoder, BasisEncoder
-pub trait QuantumEncoder: Send + Sync {
+pub trait QuantumEncoder: Send + Sync + 'static {
/// Encode classical data to quantum state on GPU
fn encode(
&self,
@@ -181,21 +181,5 @@ pub mod phase;
pub use amplitude::AmplitudeEncoder;
pub use angle::AngleEncoder;
pub use basis::BasisEncoder;
-pub use iqp::IqpEncoder;
+pub use iqp::{IqpEncoder, iqp_full_encoder, iqp_z_encoder};
pub use phase::PhaseEncoder;
-
-/// Create encoder by name: "amplitude", "angle", "basis", "iqp", or "iqp-z"
-pub fn get_encoder(name: &str) -> Result<Box<dyn QuantumEncoder>> {
- match name.to_lowercase().as_str() {
- "amplitude" => Ok(Box::new(AmplitudeEncoder)),
- "angle" => Ok(Box::new(AngleEncoder)),
- "basis" => Ok(Box::new(BasisEncoder)),
- "iqp" => Ok(Box::new(IqpEncoder::full())),
- "iqp-z" => Ok(Box::new(IqpEncoder::z_only())),
- "phase" => Ok(Box::new(PhaseEncoder)),
- _ => Err(crate::error::MahoutError::InvalidInput(format!(
- "Unknown encoder: {}. Available: amplitude, angle, basis, iqp,
iqp-z, phase",
- name
- ))),
- }
-}
diff --git a/qdp/qdp-core/src/gpu/mod.rs b/qdp/qdp-core/src/gpu/mod.rs
index 7e16be7be..73c4d4628 100644
--- a/qdp/qdp-core/src/gpu/mod.rs
+++ b/qdp/qdp-core/src/gpu/mod.rs
@@ -31,7 +31,7 @@ pub(crate) mod cuda_ffi;
#[cfg(target_os = "linux")]
pub use buffer_pool::{PinnedBufferHandle, PinnedBufferPool};
-pub use encodings::{AmplitudeEncoder, AngleEncoder, BasisEncoder,
QuantumEncoder, get_encoder};
+pub use encodings::{AmplitudeEncoder, AngleEncoder, BasisEncoder,
QuantumEncoder};
pub use memory::GpuStateVector;
pub use pipeline::run_dual_stream_pipeline;
diff --git a/qdp/qdp-core/src/lib.rs b/qdp/qdp-core/src/lib.rs
index 799eb7b18..4828297d2 100644
--- a/qdp/qdp-core/src/lib.rs
+++ b/qdp/qdp-core/src/lib.rs
@@ -31,12 +31,14 @@ pub mod readers;
#[cfg(feature = "remote-io")]
pub mod remote;
pub mod tf_proto;
+pub mod types;
#[macro_use]
mod profiling;
pub use error::{MahoutError, Result, cuda_error_to_string};
pub use gpu::memory::Precision;
pub use reader::{NullHandling, handle_float64_nulls};
+pub use types::{Dtype, Encoding};
// Throughput/latency pipeline runner: single path using QdpEngine and
encode_batch in Rust.
#[cfg(target_os = "linux")]
@@ -52,7 +54,6 @@ use std::ffi::c_void;
use std::sync::Arc;
use crate::dlpack::DLManagedTensor;
-use crate::gpu::get_encoder;
use cudarc::driver::CudaDevice;
#[cfg(target_os = "linux")]
@@ -160,7 +161,8 @@ impl QdpEngine {
) -> Result<*mut DLManagedTensor> {
crate::profile_scope!("Mahout::Encode");
- let encoder = get_encoder(encoding_method)?;
+ let encoding = Encoding::from_str_ci(encoding_method)?;
+ let encoder = encoding.encoder();
let state_vector = encoder.encode(&self.device, data, num_qubits)?;
let state_vector = state_vector.to_precision(&self.device,
self.precision)?;
let dlpack_ptr = {
@@ -205,10 +207,23 @@ impl QdpEngine {
sample_size: usize,
num_qubits: usize,
encoding_method: &str,
+ ) -> Result<*mut DLManagedTensor> {
+ let encoding = Encoding::from_str_ci(encoding_method)?;
+ self.encode_batch_for_pipeline(batch_data, num_samples, sample_size,
num_qubits, encoding)
+ }
+
+ /// Same as [`encode_batch`](Self::encode_batch) with a resolved
[`Encoding`] (no string parse).
+ pub(crate) fn encode_batch_for_pipeline(
+ &self,
+ batch_data: &[f64],
+ num_samples: usize,
+ sample_size: usize,
+ num_qubits: usize,
+ encoding: Encoding,
) -> Result<*mut DLManagedTensor> {
crate::profile_scope!("Mahout::EncodeBatch");
- let encoder = get_encoder(encoding_method)?;
+ let encoder = encoding.encoder();
let state_vector = encoder.encode_batch(
&self.device,
batch_data,
@@ -230,10 +245,29 @@ impl QdpEngine {
sample_size: usize,
num_qubits: usize,
encoding_method: &str,
+ ) -> Result<*mut DLManagedTensor> {
+ let encoding = Encoding::from_str_ci(encoding_method)?;
+ self.encode_batch_f32_for_pipeline(
+ batch_data,
+ num_samples,
+ sample_size,
+ num_qubits,
+ encoding,
+ )
+ }
+
+ /// Same as [`encode_batch_f32`](Self::encode_batch_f32) with a resolved
[`Encoding`].
+ pub(crate) fn encode_batch_f32_for_pipeline(
+ &self,
+ batch_data: &[f32],
+ num_samples: usize,
+ sample_size: usize,
+ num_qubits: usize,
+ encoding: Encoding,
) -> Result<*mut DLManagedTensor> {
crate::profile_scope!("Mahout::EncodeBatchF32");
- let encoder = get_encoder(encoding_method)?;
+ let encoder = encoding.encoder();
let state_vector = encoder.encode_batch_f32(
&self.device,
batch_data,
@@ -263,8 +297,9 @@ impl QdpEngine {
encoding_method: &str,
) -> Result<()> {
crate::profile_scope!("Mahout::RunDualStreamEncode");
- match encoding_method.to_lowercase().as_str() {
- "amplitude" => {
+ let encoding = Encoding::from_str_ci(encoding_method)?;
+ match encoding {
+ Encoding::Amplitude => {
gpu::encodings::amplitude::AmplitudeEncoder::run_amplitude_dual_stream_pipeline(
&self.device,
host_data,
@@ -273,7 +308,7 @@ impl QdpEngine {
}
_ => Err(MahoutError::InvalidInput(format!(
"run_dual_stream_encode supports only 'amplitude' for now, got
'{}'",
- encoding_method
+ encoding.as_str()
))),
}
}
@@ -507,7 +542,8 @@ impl QdpEngine {
validate_cuda_input_ptr(&self.device, input_d)?;
- let encoder = get_encoder(encoding_method)?;
+ let encoding = Encoding::from_str_ci(encoding_method)?;
+ let encoder = encoding.encoder();
let state_vector = unsafe {
encoder.encode_from_gpu_ptr(&self.device, input_d, input_len,
num_qubits, stream)
}?;
@@ -913,7 +949,8 @@ impl QdpEngine {
validate_cuda_input_ptr(&self.device, input_batch_d)?;
- let encoder = get_encoder(encoding_method)?;
+ let encoding = Encoding::from_str_ci(encoding_method)?;
+ let encoder = encoding.encoder();
let batch_state_vector = unsafe {
encoder.encode_batch_from_gpu_ptr(
&self.device,
diff --git a/qdp/qdp-core/src/pipeline_runner.rs
b/qdp/qdp-core/src/pipeline_runner.rs
index fc19dd6a3..bfbf4bc81 100644
--- a/qdp/qdp-core/src/pipeline_runner.rs
+++ b/qdp/qdp-core/src/pipeline_runner.rs
@@ -24,9 +24,11 @@ use std::time::Instant;
use crate::QdpEngine;
use crate::dlpack::DLManagedTensor;
use crate::error::{MahoutError, Result};
+use crate::gpu::memory::Precision;
use crate::io;
use crate::reader::{NullHandling, StreamingDataReader};
use crate::readers::ParquetStreamingReader;
+use crate::types::Encoding;
/// Configuration for throughput/latency pipeline runs (Python
run_throughput_pipeline_py).
#[derive(Clone, Debug)]
@@ -35,24 +37,31 @@ pub struct PipelineConfig {
pub num_qubits: u32,
pub batch_size: usize,
pub total_batches: usize,
- pub encoding_method: String,
+ pub encoding: Encoding,
pub seed: Option<u64>,
pub warmup_batches: usize,
pub null_handling: NullHandling,
- pub float32_pipeline: bool,
+ /// Pipeline element dtype for synthetic batch fill and `encode_batch`
dispatch.
+ ///
+ /// If [`Encoding::supports_f32`](crate::types::Encoding::supports_f32) is
false for the
+ /// chosen [`encoding`](PipelineConfig::encoding),
[`normalize`](PipelineConfig::normalize)
+ /// downgrades this to [`Precision::Float64`] (see `types` module docs:
batch f32 is wired
+ /// only for encodings with a real `encode_batch_f32` today).
+ pub dtype: Precision,
pub prefetch_depth: usize,
}
impl PipelineConfig {
- /// Normalizes the configuration, such as falling back to f64 if f32 is
requested
- /// but the encoding doesn't support it.
+ /// Normalizes the configuration: if `dtype` is float32 but the encoding
cannot use the
+ /// f32 batch encode path
([`Encoding::supports_f32`](crate::types::Encoding::supports_f32)),
+ /// falls back to float64.
pub fn normalize(&mut self) {
- if self.float32_pipeline &&
!encoding_supports_f32(&self.encoding_method) {
+ if matches!(self.dtype, Precision::Float32) &&
!self.encoding.supports_f32() {
log::info!(
- "float32_pipeline requested but encoding '{}' does not support
f32; falling back to f64",
- self.encoding_method
+ "float32 pipeline requested but encoding '{}' does not support
f32; falling back to f64",
+ self.encoding.as_str()
);
- self.float32_pipeline = false;
+ self.dtype = Precision::Float64;
}
}
}
@@ -64,11 +73,11 @@ impl Default for PipelineConfig {
num_qubits: 16,
batch_size: 64,
total_batches: 100,
- encoding_method: "amplitude".to_string(),
+ encoding: Encoding::Amplitude,
seed: None,
warmup_batches: 0,
null_handling: NullHandling::FillZero,
- float32_pipeline: false,
+ dtype: Precision::Float64,
prefetch_depth: 16,
}
}
@@ -99,12 +108,6 @@ pub trait BatchProducer: Send + 'static {
fn produce(&mut self, recycled: Option<BatchData>) ->
Result<Option<PrefetchedBatch>>;
}
-/// Returns true if the given encoding method has a native f32 GPU kernel.
-/// Used to auto-gate `float32_pipeline` so unsupported encodings fall back to
f64.
-fn encoding_supports_f32(encoding_method: &str) -> bool {
- matches!(encoding_method.to_lowercase().as_str(), "amplitude")
-}
-
pub struct SyntheticProducer {
pub config: PipelineConfig,
pub vector_len: usize,
@@ -131,16 +134,16 @@ impl BatchProducer for SyntheticProducer {
}
let mut data = match recycled {
- Some(BatchData::F32(mut buf)) if self.config.float32_pipeline => {
+ Some(BatchData::F32(mut buf)) if matches!(self.config.dtype,
Precision::Float32) => {
buf.resize(self.config.batch_size * self.vector_len, 0.0);
BatchData::F32(buf)
}
- Some(BatchData::F64(mut buf)) if !self.config.float32_pipeline => {
+ Some(BatchData::F64(mut buf)) if matches!(self.config.dtype,
Precision::Float64) => {
buf.resize(self.config.batch_size * self.vector_len, 0.0);
BatchData::F64(buf)
}
_ => {
- if self.config.float32_pipeline {
+ if matches!(self.config.dtype, Precision::Float32) {
BatchData::F32(vec![0.0f32; self.config.batch_size *
self.vector_len])
} else {
BatchData::F64(vec![0.0f64; self.config.batch_size *
self.vector_len])
@@ -366,7 +369,7 @@ pub struct PipelineIterator {
impl PipelineIterator {
pub fn new_synthetic(engine: QdpEngine, mut config: PipelineConfig) ->
Result<Self> {
config.normalize();
- let vector_len = vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = vector_len(config.num_qubits, config.encoding);
let producer = SyntheticProducer::new(config.clone(), vector_len);
let prefetch_depth = config.prefetch_depth;
let (rx, recycle_tx, _producer_handle) = spawn_producer(producer,
prefetch_depth)?;
@@ -393,13 +396,16 @@ impl PipelineIterator {
config.normalize();
let path = path.as_ref();
let (data, num_samples, sample_size) = read_file_by_extension(path,
config.null_handling)?;
- let vector_len = vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = vector_len(config.num_qubits, config.encoding);
// Dimension validation at construction.
if sample_size != vector_len {
return Err(MahoutError::InvalidInput(format!(
"File feature length {} does not match vector_len {} for
num_qubits={}, encoding={}",
- sample_size, vector_len, config.num_qubits,
config.encoding_method
+ sample_size,
+ vector_len,
+ config.num_qubits,
+ config.encoding.as_str()
)));
}
if data.len() != num_samples * sample_size {
@@ -454,7 +460,7 @@ impl PipelineIterator {
Some(DEFAULT_PARQUET_ROW_GROUP_SIZE),
config.null_handling,
)?;
- let vector_len = vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = vector_len(config.num_qubits, config.encoding);
// Read first chunk to learn sample_size; reuse as initial buffer.
const INITIAL_CHUNK_CAP: usize = 64 * 1024;
@@ -474,7 +480,10 @@ impl PipelineIterator {
if sample_size != vector_len {
return Err(MahoutError::InvalidInput(format!(
"File feature length {} does not match vector_len {} for
num_qubits={}, encoding={}",
- sample_size, vector_len, config.num_qubits,
config.encoding_method
+ sample_size,
+ vector_len,
+ config.num_qubits,
+ config.encoding.as_str()
)));
}
@@ -511,19 +520,19 @@ impl PipelineIterator {
Err(_) => return Ok(None),
};
let ptr = match &batch.data {
- BatchData::F64(buf) => self.engine.encode_batch(
+ BatchData::F64(buf) => self.engine.encode_batch_for_pipeline(
buf,
batch.batch_n,
batch.sample_size,
batch.num_qubits,
- &self.config.encoding_method,
+ self.config.encoding,
)?,
- BatchData::F32(buf) => self.engine.encode_batch_f32(
+ BatchData::F32(buf) => self.engine.encode_batch_f32_for_pipeline(
buf,
batch.batch_n,
batch.sample_size,
batch.num_qubits,
- &self.config.encoding_method,
+ self.config.encoding,
)?,
};
let _ = self.recycle_tx.lock().unwrap().send(batch.data);
@@ -532,47 +541,32 @@ impl PipelineIterator {
}
/// Vector length per sample for given encoding (used by pipeline and
iterator).
-pub fn vector_len(num_qubits: u32, encoding_method: &str) -> usize {
- let n = num_qubits as usize;
- match encoding_method.to_lowercase().as_str() {
- "angle" => n,
- "basis" => 1,
- "iqp-z" => n,
- "iqp" => n + n.saturating_mul(n.saturating_sub(1)) / 2,
- _ => 1 << n, // amplitude
- }
+pub fn vector_len(num_qubits: u32, encoding: Encoding) -> usize {
+ encoding.vector_len(num_qubits)
}
-/// Deterministic sample generation matching Python benchmark helpers.
-fn fill_sample(seed: u64, out: &mut [f64], encoding_method: &str, num_qubits:
usize) -> Result<()> {
+/// Deterministic sample generation matching Python utils.build_sample.
+fn fill_sample(seed: u64, out: &mut [f64], encoding: Encoding, num_qubits:
usize) -> Result<()> {
let len = out.len();
if len == 0 {
return Ok(());
}
- match encoding_method.to_lowercase().as_str() {
- "basis" => {
+ match encoding {
+ Encoding::Basis => {
// For basis encoding, use 2^num_qubits as the state space size
for mask calculation
let state_space_size = 1 << num_qubits;
let mask = (state_space_size - 1) as u64;
let idx = seed & mask;
out[0] = idx as f64;
}
- "angle" => {
- let scale = (2.0 * PI) / len as f64;
- for (i, v) in out.iter_mut().enumerate() {
- let mixed = (i as u64 + seed) % (len as u64);
- *v = mixed as f64 * scale;
- }
- }
- "iqp-z" | "iqp" => {
+ Encoding::Angle | Encoding::Iqp | Encoding::IqpZ | Encoding::Phase => {
let scale = (2.0 * PI) / len as f64;
for (i, v) in out.iter_mut().enumerate() {
let mixed = (i as u64 + seed) % (len as u64);
*v = mixed as f64 * scale;
}
}
- _ => {
- // amplitude
+ Encoding::Amplitude => {
let mask = (len - 1) as u64;
let scale = 1.0 / len as f64;
for (i, v) in out.iter_mut().enumerate() {
@@ -609,7 +603,7 @@ fn fill_batch_inplace(
let _ = fill_sample(
seed_base + i as u64,
&mut batch_buf[offset..offset + vector_len],
- &config.encoding_method,
+ config.encoding,
config.num_qubits as usize,
);
}
@@ -619,36 +613,28 @@ fn fill_batch_inplace(
fn fill_sample_f32(
seed: u64,
out: &mut [f32],
- encoding_method: &str,
+ encoding: Encoding,
num_qubits: usize,
) -> Result<()> {
let len = out.len();
if len == 0 {
return Ok(());
}
- match encoding_method.to_lowercase().as_str() {
- "basis" => {
+ match encoding {
+ Encoding::Basis => {
let state_space_size = 1 << num_qubits;
let mask = (state_space_size - 1) as u64;
let idx = seed & mask;
out[0] = idx as f32;
}
- "angle" => {
- let scale = (2.0 * std::f32::consts::PI) / len as f32;
- for (i, v) in out.iter_mut().enumerate() {
- let mixed = (i as u64 + seed) % (len as u64);
- *v = mixed as f32 * scale;
- }
- }
- "iqp-z" | "iqp" => {
+ Encoding::Angle | Encoding::Iqp | Encoding::IqpZ | Encoding::Phase => {
let scale = (2.0 * std::f32::consts::PI) / len as f32;
for (i, v) in out.iter_mut().enumerate() {
let mixed = (i as u64 + seed) % (len as u64);
*v = mixed as f32 * scale;
}
}
- _ => {
- // amplitude
+ Encoding::Amplitude => {
let mask = (len - 1) as u64;
let scale = 1.0 / len as f32;
for (i, v) in out.iter_mut().enumerate() {
@@ -676,7 +662,7 @@ fn fill_batch_inplace_f32(
let _ = fill_sample_f32(
seed_base + i as u64,
&mut batch_buf[offset..offset + vector_len],
- &config.encoding_method,
+ config.encoding,
config.num_qubits as usize,
);
}
@@ -699,20 +685,20 @@ pub fn run_throughput_pipeline(config: &PipelineConfig)
-> Result<PipelineRunRes
config.normalize();
let engine = QdpEngine::new(config.device_id)?;
- let vector_len = vector_len(config.num_qubits, &config.encoding_method);
+ let vector_len = vector_len(config.num_qubits, config.encoding);
let num_qubits = config.num_qubits as usize;
// Warmup
- if config.float32_pipeline {
+ if matches!(config.dtype, Precision::Float32) {
let mut batch_buf = vec![0.0f32; config.batch_size * vector_len];
for b in 0..config.warmup_batches {
fill_batch_inplace_f32(&config, b, vector_len, &mut batch_buf);
- let ptr = engine.encode_batch_f32(
+ let ptr = engine.encode_batch_f32_for_pipeline(
&batch_buf,
config.batch_size,
vector_len,
num_qubits,
- &config.encoding_method,
+ config.encoding,
)?;
unsafe { release_dlpack(ptr) };
}
@@ -720,12 +706,12 @@ pub fn run_throughput_pipeline(config: &PipelineConfig)
-> Result<PipelineRunRes
let mut batch_buf = vec![0.0f64; config.batch_size * vector_len];
for b in 0..config.warmup_batches {
fill_batch_inplace(&config, b, vector_len, &mut batch_buf);
- let ptr = engine.encode_batch(
+ let ptr = engine.encode_batch_for_pipeline(
&batch_buf,
config.batch_size,
vector_len,
num_qubits,
- &config.encoding_method,
+ config.encoding,
)?;
unsafe { release_dlpack(ptr) };
}
@@ -742,19 +728,19 @@ pub fn run_throughput_pipeline(config: &PipelineConfig)
-> Result<PipelineRunRes
while let Ok(result) = rx.recv() {
let batch = result?;
let ptr = match &batch.data {
- BatchData::F64(buf) => engine.encode_batch(
+ BatchData::F64(buf) => engine.encode_batch_for_pipeline(
buf,
batch.batch_n,
batch.sample_size,
batch.num_qubits,
- &config.encoding_method,
+ config.encoding,
)?,
- BatchData::F32(buf) => engine.encode_batch_f32(
+ BatchData::F32(buf) => engine.encode_batch_f32_for_pipeline(
buf,
batch.batch_n,
batch.sample_size,
batch.num_qubits,
- &config.encoding_method,
+ config.encoding,
)?,
};
unsafe { release_dlpack(ptr) };
@@ -797,12 +783,12 @@ mod tests {
let config = PipelineConfig {
num_qubits: 5,
batch_size: 8,
- encoding_method: encoding_method.to_string(),
+ encoding: Encoding::from_str_ci(encoding_method).unwrap(),
seed: Some(123),
..Default::default()
};
- let vector_len = vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = vector_len(config.num_qubits, config.encoding);
// Test edge cases: 0 and batch_size-1
for batch_idx in [0, config.batch_size - 1, 7] {
@@ -818,12 +804,12 @@ mod tests {
let config = PipelineConfig {
num_qubits: 5,
batch_size: 8,
- encoding_method: encoding_method.to_string(),
+ encoding: Encoding::from_str_ci(encoding_method).unwrap(),
seed: Some(123),
..Default::default()
};
- let vector_len = vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = vector_len(config.num_qubits, config.encoding);
let batch0 = generate_batch(&config, 0, vector_len);
let batch1 = generate_batch(&config, 1, vector_len);
@@ -885,12 +871,12 @@ mod tests {
let config = PipelineConfig {
num_qubits: 5,
batch_size: 8,
- encoding_method: "amplitude".to_string(),
+ encoding: Encoding::Amplitude,
seed: None,
..Default::default()
};
- let vector_len = vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = vector_len(config.num_qubits, config.encoding);
let batch = generate_batch(&config, 0, vector_len);
assert_eq!(batch.len(), config.batch_size * vector_len);
@@ -904,12 +890,12 @@ mod tests {
let config = PipelineConfig {
num_qubits: 5,
batch_size: 1,
- encoding_method: "amplitude".to_string(),
+ encoding: Encoding::Amplitude,
seed: Some(123),
..Default::default()
};
- let vector_len = vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = vector_len(config.num_qubits, config.encoding);
let batch = generate_batch(&config, 0, vector_len);
assert_eq!(batch.len(), vector_len);
@@ -927,7 +913,7 @@ mod tests {
let config_lower = PipelineConfig {
num_qubits: 5,
batch_size: 8,
- encoding_method: "amplitude".to_string(),
+ encoding: Encoding::Amplitude,
seed: Some(123),
..Default::default()
};
@@ -935,12 +921,12 @@ mod tests {
let config_upper = PipelineConfig {
num_qubits: 5,
batch_size: 8,
- encoding_method: "AMPLITUDE".to_string(),
+ encoding: Encoding::from_str_ci("AMPLITUDE").unwrap(),
seed: Some(123),
..Default::default()
};
- let vector_len = vector_len(config_lower.num_qubits,
&config_lower.encoding_method);
+ let vector_len = vector_len(config_lower.num_qubits,
config_lower.encoding);
let batch_lower = generate_batch(&config_lower, 0, vector_len);
let batch_upper = generate_batch(&config_upper, 0, vector_len);
assert_eq!(batch_lower, batch_upper);
@@ -951,12 +937,12 @@ mod tests {
let config = PipelineConfig {
num_qubits: 5,
batch_size: 8,
- encoding_method: "amplitude".to_string(),
+ encoding: Encoding::Amplitude,
seed: Some(123),
..Default::default()
};
- let vector_len = vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = vector_len(config.num_qubits, config.encoding);
for batch_idx in 0..5 {
let batch = generate_batch(&config, batch_idx, vector_len);
@@ -976,12 +962,12 @@ mod tests {
let config = PipelineConfig {
num_qubits: 5,
batch_size: 8,
- encoding_method: "amplitude".to_string(),
+ encoding: Encoding::Amplitude,
seed: None,
..Default::default()
};
- let vector_len = vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = vector_len(config.num_qubits, config.encoding);
let batch = generate_batch(&config, 0, vector_len);
for &value in &batch {
@@ -998,12 +984,12 @@ mod tests {
let config = PipelineConfig {
num_qubits: 5,
batch_size: 1,
- encoding_method: "amplitude".to_string(),
+ encoding: Encoding::Amplitude,
seed: Some(123),
..Default::default()
};
- let vector_len = vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = vector_len(config.num_qubits, config.encoding);
let batch = generate_batch(&config, 0, vector_len);
for &value in &batch {
@@ -1020,10 +1006,10 @@ mod tests {
total_batches: 5,
num_qubits: 3,
batch_size: 4,
- encoding_method: "amplitude".to_string(),
+ encoding: Encoding::Amplitude,
..Default::default()
};
- let vector_len = super::vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = super::vector_len(config.num_qubits, config.encoding);
let mut producer = SyntheticProducer::new(config, vector_len);
let mut count = 0;
@@ -1039,10 +1025,10 @@ mod tests {
total_batches: 1,
num_qubits: 3,
batch_size: 4,
- encoding_method: "amplitude".to_string(),
+ encoding: Encoding::Amplitude,
..Default::default()
};
- let vector_len = super::vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = super::vector_len(config.num_qubits, config.encoding);
let mut producer = SyntheticProducer::new(config.clone(), vector_len);
let batch_from_producer = producer.produce(None).unwrap().unwrap();
@@ -1056,7 +1042,7 @@ mod tests {
let config = PipelineConfig {
batch_size: 5,
num_qubits: 2,
- encoding_method: "amplitude".to_string(),
+ encoding: Encoding::Amplitude,
..Default::default()
};
let sample_size = 4; // 2^2
@@ -1086,7 +1072,7 @@ mod tests {
prefetch_depth: 16,
..Default::default()
};
- let vector_len = super::vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = super::vector_len(config.num_qubits, config.encoding);
let producer = SyntheticProducer::new(config, vector_len);
let (rx, _recycle_tx, handle) = spawn_producer(producer, 16).unwrap();
@@ -1108,7 +1094,7 @@ mod tests {
prefetch_depth: 16,
..Default::default()
};
- let vector_len = super::vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = super::vector_len(config.num_qubits, config.encoding);
let producer = SyntheticProducer::new(config, vector_len);
let (rx, _recycle_tx, handle) = spawn_producer(producer, 16).unwrap();
@@ -1129,18 +1115,18 @@ mod tests {
total_batches: 2,
num_qubits: 3,
batch_size: 4,
- encoding_method: "amplitude".to_string(),
- float32_pipeline: true,
+ encoding: Encoding::Amplitude,
+ dtype: Precision::Float32,
..Default::default()
};
config.normalize();
- let vector_len = super::vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = super::vector_len(config.num_qubits, config.encoding);
let mut producer = SyntheticProducer::new(config, vector_len);
let batch = producer.produce(None).unwrap().unwrap();
assert!(
matches!(batch.data, BatchData::F32(_)),
- "amplitude with float32_pipeline=true should produce F32 data"
+ "amplitude with dtype=Float32 should produce F32 data"
);
// Verify data is non-zero (was actually filled)
@@ -1158,18 +1144,18 @@ mod tests {
total_batches: 1,
num_qubits: 3,
batch_size: 4,
- encoding_method: "angle".to_string(),
- float32_pipeline: true, // requested f32, but angle doesn't
support it
+ encoding: Encoding::Angle,
+ dtype: Precision::Float32, // requested f32, but angle doesn't
support native f32 batch path
..Default::default()
};
config.normalize();
- let vector_len = super::vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = super::vector_len(config.num_qubits, config.encoding);
let mut producer = SyntheticProducer::new(config, vector_len);
let batch = producer.produce(None).unwrap().unwrap();
assert!(
matches!(batch.data, BatchData::F64(_)),
- "angle with float32_pipeline=true should fall back to F64 data"
+ "angle with requested Float32 should fall back to F64 batch data
(no encode_batch_f32 yet)"
);
}
@@ -1179,36 +1165,36 @@ mod tests {
total_batches: 1,
num_qubits: 3,
batch_size: 4,
- encoding_method: "basis".to_string(),
- float32_pipeline: true,
+ encoding: Encoding::Basis,
+ dtype: Precision::Float32,
..Default::default()
};
config.normalize();
- let vector_len = super::vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = super::vector_len(config.num_qubits, config.encoding);
let mut producer = SyntheticProducer::new(config, vector_len);
let batch = producer.produce(None).unwrap().unwrap();
assert!(
matches!(batch.data, BatchData::F64(_)),
- "basis with float32_pipeline=true should fall back to F64 data"
+ "basis with requested Float32 should fall back to F64 batch data
(no encode_batch_f32 yet)"
);
}
#[test]
fn test_encoding_supports_f32() {
- assert!(super::encoding_supports_f32("amplitude"));
- assert!(super::encoding_supports_f32("Amplitude"));
- assert!(super::encoding_supports_f32("AMPLITUDE"));
- assert!(!super::encoding_supports_f32("angle"));
- assert!(!super::encoding_supports_f32("basis"));
- assert!(!super::encoding_supports_f32("iqp-z"));
- assert!(!super::encoding_supports_f32("iqp"));
+ assert!(Encoding::Amplitude.supports_f32());
+ assert!(Encoding::from_str_ci("Amplitude").unwrap().supports_f32());
+ assert!(Encoding::from_str_ci("AMPLITUDE").unwrap().supports_f32());
+ assert!(!Encoding::Angle.supports_f32());
+ assert!(!Encoding::Basis.supports_f32());
+ assert!(!Encoding::Iqp.supports_f32());
+ assert!(!Encoding::IqpZ.supports_f32());
}
#[test]
fn test_vector_len_for_iqp_variants() {
- assert_eq!(super::vector_len(4, "iqp-z"), 4);
- assert_eq!(super::vector_len(4, "iqp"), 10);
+ assert_eq!(super::vector_len(4, Encoding::IqpZ), 4);
+ assert_eq!(super::vector_len(4, Encoding::Iqp), 10);
}
#[test]
@@ -1216,12 +1202,12 @@ mod tests {
let config = PipelineConfig {
num_qubits: 4,
batch_size: 3,
- encoding_method: "iqp".to_string(),
+ encoding: Encoding::Iqp,
seed: Some(7),
..Default::default()
};
- let vector_len = super::vector_len(config.num_qubits,
&config.encoding_method);
+ let vector_len = super::vector_len(config.num_qubits, config.encoding);
let batch = generate_batch(&config, 0, vector_len);
let upper = 2.0 * PI;
for &value in &batch {
diff --git a/qdp/qdp-core/src/types.rs b/qdp/qdp-core/src/types.rs
new file mode 100644
index 000000000..f8a98834b
--- /dev/null
+++ b/qdp/qdp-core/src/types.rs
@@ -0,0 +1,160 @@
+//
+// 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.
+
+//! Canonical domain types for encodings and element dtypes (`Dtype`).
+//!
+//! ## `Encoding::supports_f32`
+//!
+//! A future shape of this API may return true for amplitude, angle, and basis
once each encoder
+//! has a batch float32 GPU path. **Today only amplitude implements**
+//! [`QuantumEncoder::encode_batch_f32`] for the synthetic prefetch pipeline,
so
+//! [`Encoding::supports_f32`](Encoding::supports_f32) stays amplitude-only and
+//! [`crate::pipeline_runner::PipelineConfig::normalize`] avoids routing other
encodings through
+//! `encode_batch_f32`. Widen this method when angle/basis gain real
`encode_batch_f32`
+//! implementations.
+
+use crate::error::{MahoutError, Result};
+use crate::gpu::encodings::{
+ AmplitudeEncoder, AngleEncoder, BasisEncoder, PhaseEncoder,
QuantumEncoder, iqp_full_encoder,
+ iqp_z_encoder,
+};
+
+/// Dtype for pipeline configuration (re-export of
[`crate::gpu::memory::Precision`]).
+pub use crate::gpu::memory::Precision as Dtype;
+
+impl crate::gpu::memory::Precision {
+ /// Parse dtype from a short user string (case-insensitive, trimmed).
+ pub fn from_str_ci(s: &str) -> Result<Self> {
+ let t = s.trim();
+ if t.eq_ignore_ascii_case("f32")
+ || t.eq_ignore_ascii_case("float32")
+ || t.eq_ignore_ascii_case("float")
+ {
+ Ok(Self::Float32)
+ } else if t.eq_ignore_ascii_case("f64")
+ || t.eq_ignore_ascii_case("float64")
+ || t.eq_ignore_ascii_case("double")
+ {
+ Ok(Self::Float64)
+ } else {
+ Err(MahoutError::InvalidInput(format!(
+ "Unknown dtype: {s}. Use 'f32' or 'f64'."
+ )))
+ }
+ }
+
+ /// Element size in bytes for real scalar components (f32/f64).
+ #[must_use]
+ pub const fn bytes(self) -> usize {
+ match self {
+ Self::Float32 => 4,
+ Self::Float64 => 8,
+ }
+ }
+}
+
+/// Quantum encoding method (canonical; parse user strings once at API
boundaries).
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub enum Encoding {
+ Amplitude,
+ Angle,
+ Basis,
+ Iqp,
+ IqpZ,
+ Phase,
+}
+
+impl Encoding {
+ /// Parse encoding name (case-insensitive ASCII, stack buffer; no heap
allocation).
+ pub fn from_str_ci(s: &str) -> Result<Self> {
+ let mut buf = [0u8; 16];
+ let bytes = s.as_bytes();
+ if bytes.len() > buf.len() {
+ return Err(MahoutError::InvalidInput(format!(
+ "Unknown encoding: {s}. Available: amplitude, angle, basis,
iqp, iqp-z, phase"
+ )));
+ }
+ for (i, b) in bytes.iter().enumerate() {
+ buf[i] = b.to_ascii_lowercase();
+ }
+ match &buf[..bytes.len()] {
+ b"amplitude" => Ok(Self::Amplitude),
+ b"angle" => Ok(Self::Angle),
+ b"basis" => Ok(Self::Basis),
+ b"iqp" => Ok(Self::Iqp),
+ b"iqp-z" => Ok(Self::IqpZ),
+ b"phase" => Ok(Self::Phase),
+ _ => Err(MahoutError::InvalidInput(format!(
+ "Unknown encoding: {s}. Available: amplitude, angle, basis,
iqp, iqp-z, phase"
+ ))),
+ }
+ }
+
+ #[must_use]
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Amplitude => "amplitude",
+ Self::Angle => "angle",
+ Self::Basis => "basis",
+ Self::Iqp => "iqp",
+ Self::IqpZ => "iqp-z",
+ Self::Phase => "phase",
+ }
+ }
+
+ /// Input feature dimension per sample for this encoding and qubit count.
+ ///
+ /// Matches each encoder's `expected_data_len` / `sample_size` contract:
+ /// - `Amplitude`: full state vector (`2^n`)
+ /// - `Angle` / `IqpZ` / `Phase`: one value per qubit (`n`)
+ /// - `Iqp`: single-qubit + pairwise ZZ terms (`n + n*(n-1)/2`)
+ /// - `Basis`: single integer index (`1`)
+ #[must_use]
+ pub const fn vector_len(self, num_qubits: u32) -> usize {
+ let n = num_qubits as usize;
+ match self {
+ Self::Amplitude => 1 << n,
+ Self::Angle | Self::IqpZ | Self::Phase => n,
+ Self::Iqp => n + n * n.saturating_sub(1) / 2,
+ Self::Basis => 1,
+ }
+ }
+
+ /// Whether the **synthetic batch pipeline** may keep
[`crate::gpu::memory::Precision::Float32`]
+ /// end-to-end (prefetched host `Vec<f32>` plus
[`crate::QdpEngine::encode_batch_f32`]).
+ ///
+ /// This must match encoders that actually implement
[`QuantumEncoder::encode_batch_f32`].
+ /// Long-term design may include angle/basis here; today only amplitude
does, so angle/basis
+ /// still normalize to `Float64` in
[`crate::pipeline_runner::PipelineConfig::normalize`]
+ /// until their batch f32 GPU paths exist in the encoder implementations.
+ #[must_use]
+ pub const fn supports_f32(self) -> bool {
+ matches!(self, Self::Amplitude)
+ }
+
+ /// Static encoder dispatch (no per-call heap allocation).
+ #[must_use]
+ pub fn encoder(self) -> &'static dyn QuantumEncoder {
+ match self {
+ Self::Amplitude => &AmplitudeEncoder,
+ Self::Angle => &AngleEncoder,
+ Self::Basis => &BasisEncoder,
+ Self::Iqp => iqp_full_encoder(),
+ Self::IqpZ => iqp_z_encoder(),
+ Self::Phase => &PhaseEncoder,
+ }
+ }
+}
diff --git a/qdp/qdp-core/tests/gpu_angle_encoding.rs
b/qdp/qdp-core/tests/gpu_angle_encoding.rs
index 6b60d5153..dee51331b 100644
--- a/qdp/qdp-core/tests/gpu_angle_encoding.rs
+++ b/qdp/qdp-core/tests/gpu_angle_encoding.rs
@@ -114,6 +114,28 @@ fn test_angle_infinity_rejected() {
// ---- Successful encoding (kernel launch path) ----
+/// Regression: streaming Parquet path accepts mixed-case encoding names via
`Encoding::from_str_ci`.
+#[test]
+fn test_angle_parquet_encoding_case_insensitive() {
+ let Some(engine) = common::qdp_engine() else {
+ return;
+ };
+
+ let num_qubits = 2;
+ let data: Vec<f64> = vec![0.1, 0.2];
+ let path = "/tmp/test_angle_case.parquet";
+ common::write_fixed_size_list_parquet(path, &data, num_qubits);
+
+ let dlpack_ptr = engine
+ .encode_from_parquet(path, num_qubits, "Angle")
+ .expect("mixed-case 'Angle' should match streaming angle encoder");
+ let _ = std::fs::remove_file(path);
+
+ unsafe {
+ common::assert_dlpack_shape_2d_and_delete(dlpack_ptr, 1, (1 <<
num_qubits) as i64);
+ }
+}
+
#[test]
fn test_angle_successful_encoding_from_parquet() {
let Some(engine) = common::qdp_engine() else {
diff --git a/qdp/qdp-core/tests/gpu_iqp_encoding.rs
b/qdp/qdp-core/tests/gpu_iqp_encoding.rs
index f45ba3eac..4954ab5b3 100644
--- a/qdp/qdp-core/tests/gpu_iqp_encoding.rs
+++ b/qdp/qdp-core/tests/gpu_iqp_encoding.rs
@@ -797,7 +797,7 @@ fn test_iqp_fwt_zero_parameters_identity() {
#[test]
#[cfg(target_os = "linux")]
fn test_iqp_encoder_via_factory() {
- println!("Testing IQP encoder creation via get_encoder...");
+ println!("Testing IQP encoder creation via Encoding::from_str_ci /
encode...");
let Some(engine) = common::qdp_engine() else {
println!("SKIP: No GPU available");
@@ -836,7 +836,7 @@ fn test_iqp_encoder_via_factory() {
#[test]
#[cfg(target_os = "linux")]
fn test_iqp_z_encoder_via_factory() {
- println!("Testing IQP-Z encoder creation via get_encoder...");
+ println!("Testing IQP-Z encoder creation via encode...");
let Some(engine) = common::qdp_engine() else {
println!("SKIP: No GPU available");
diff --git a/qdp/qdp-core/tests/gpu_validation.rs
b/qdp/qdp-core/tests/gpu_validation.rs
index 3235b24fa..291f92dce 100644
--- a/qdp/qdp-core/tests/gpu_validation.rs
+++ b/qdp/qdp-core/tests/gpu_validation.rs
@@ -38,8 +38,8 @@ fn test_input_validation_invalid_strategy() {
match result {
Err(MahoutError::InvalidInput(msg)) => {
assert!(
- msg.contains("Unknown encoder"),
- "Error message should mention unknown encoder"
+ msg.contains("Unknown encoding"),
+ "Error message should mention unknown encoding"
);
println!("PASS: Correctly rejected invalid strategy: {}", msg);
}
diff --git a/qdp/qdp-core/tests/types.rs b/qdp/qdp-core/tests/types.rs
new file mode 100644
index 000000000..cf2c8b174
--- /dev/null
+++ b/qdp/qdp-core/tests/types.rs
@@ -0,0 +1,73 @@
+//
+// 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::Encoding`] and [`qdp_core::Dtype`].
+
+use qdp_core::{Dtype, Encoding};
+
+#[test]
+fn encoding_case_insensitive() {
+ assert_eq!(
+ Encoding::from_str_ci("Amplitude").unwrap(),
+ Encoding::Amplitude
+ );
+ assert_eq!(
+ Encoding::from_str_ci("AMPLITUDE").unwrap(),
+ Encoding::Amplitude
+ );
+ assert_eq!(Encoding::from_str_ci("iqp-z").unwrap(), Encoding::IqpZ);
+}
+
+#[test]
+fn encoding_unknown_returns_err() {
+ assert!(Encoding::from_str_ci("not_real").is_err());
+}
+
+#[test]
+fn vector_len_matches_encoder_contracts() {
+ let n = 5u32;
+ assert_eq!(Encoding::Amplitude.vector_len(n), 32); // 2^5
+ assert_eq!(Encoding::Angle.vector_len(n), 5); // n
+ assert_eq!(Encoding::IqpZ.vector_len(n), 5); // n (z-only)
+ assert_eq!(Encoding::Phase.vector_len(n), 5); // n (one angle per qubit)
+ assert_eq!(Encoding::Iqp.vector_len(n), 5 + 5 * 4 / 2); // n + n*(n-1)/2 =
15
+ assert_eq!(Encoding::Basis.vector_len(n), 1);
+}
+
+#[test]
+fn static_encoder_same_instance_across_calls() {
+ assert!(
+ std::ptr::eq(Encoding::Amplitude.encoder(),
Encoding::Amplitude.encoder(),),
+ "static dispatch must return the same 'static reference"
+ );
+}
+
+#[test]
+fn supports_f32_amplitude_only() {
+ assert!(Encoding::Amplitude.supports_f32());
+ assert!(!Encoding::Angle.supports_f32());
+ assert!(!Encoding::Basis.supports_f32());
+ assert!(!Encoding::Iqp.supports_f32());
+ assert!(!Encoding::IqpZ.supports_f32());
+ assert!(!Encoding::Phase.supports_f32());
+}
+
+#[test]
+fn dtype_from_str_ci() {
+ assert_eq!(Dtype::from_str_ci("f32").unwrap(), Dtype::Float32);
+ assert_eq!(Dtype::from_str_ci("Float64").unwrap(), Dtype::Float64);
+ assert!(Dtype::from_str_ci("bf16").is_err());
+}
diff --git a/qdp/qdp-python/README.md b/qdp/qdp-python/README.md
index cacfa0417..a81f95324 100644
--- a/qdp/qdp-python/README.md
+++ b/qdp/qdp-python/README.md
@@ -86,6 +86,16 @@ Backend support boundary:
a follow-up.
- AMD (`QdpEngine(..., backend="amd")`): `amplitude`, `angle`, `basis`, `iqp`,
`iqp-z`, `phase`
+### Pipeline / loader dtype (Rust internals)
+
+`QuantumDataLoader` and `run_throughput_pipeline` build a Rust
`PipelineConfig` with an
+`encoding` plus a `dtype` (float32 vs float64). The prefetch thread can only
keep an
+end-to-end **float32 host batch** for encodings whose GPU stack implements the
batch **f32**
+path (`encode_batch_f32`). **Today that is amplitude only.** Angle and basis
still fall back
+to float64 for that loop until their batch f32 implementations exist. The
eventual full
+matrix (e.g. angle/basis under `supports_f32` once kernels are wired) is
broader than what
+the pipeline uses today.
+
## Input Sources
```python
diff --git a/qdp/qdp-python/qumat_qdp/api.py b/qdp/qdp-python/qumat_qdp/api.py
index 2ae4e45e6..6493dd0f3 100644
--- a/qdp/qdp-python/qumat_qdp/api.py
+++ b/qdp/qdp-python/qumat_qdp/api.py
@@ -161,7 +161,7 @@ class QdpBenchmark:
encoding_method=self._encoding_method,
warmup_batches=self._warmup_batches,
seed=None,
- float32_pipeline=True,
+ dtype="f32",
)
return ThroughputResult(
duration_sec=duration_sec, vectors_per_sec=vectors_per_sec
@@ -177,7 +177,7 @@ class QdpBenchmark:
encoding_method=self._encoding_method,
warmup_batches=self._warmup_batches,
seed=None,
- float32_pipeline=True,
+ dtype="f32",
)
return LatencyResult(
duration_sec=duration_sec,
diff --git a/qdp/qdp-python/qumat_qdp/loader.py
b/qdp/qdp-python/qumat_qdp/loader.py
index 7873f2bf6..5a53f6c19 100644
--- a/qdp/qdp-python/qumat_qdp/loader.py
+++ b/qdp/qdp-python/qumat_qdp/loader.py
@@ -41,6 +41,11 @@ if TYPE_CHECKING:
# Seed must fit Rust u64: 0 <= seed <= 2^64 - 1.
_U64_MAX = 2**64 - 1
+# Canonical encoding names (must match Encoding enum in qdp-core/src/types.rs).
+_VALID_ENCODINGS: frozenset[str] = frozenset(
+ {"amplitude", "angle", "basis", "iqp", "iqp-z", "phase"}
+)
+
# Fallback-supported file extensions (loadable without _qdp).
_TORCH_FILE_EXTS = frozenset({".pt", ".pth"})
_NUMPY_FILE_EXTS = frozenset({".npy"})
@@ -71,6 +76,11 @@ def _validate_loader_args(
raise ValueError(
f"encoding_method must be a non-empty string, got
{encoding_method!r}"
)
+ if encoding_method.lower() not in _VALID_ENCODINGS:
+ raise ValueError(
+ f"Unknown encoding_method {encoding_method!r}. "
+ f"Valid options: {sorted(_VALID_ENCODINGS)}"
+ )
if seed is not None:
if not isinstance(seed, int):
raise ValueError(
@@ -172,6 +182,11 @@ class QuantumDataLoader:
raise ValueError(
f"encoding_method must be a non-empty string, got {method!r}"
)
+ if method.lower() not in _VALID_ENCODINGS:
+ raise ValueError(
+ f"Unknown encoding {method!r}. "
+ f"Valid options: {sorted(_VALID_ENCODINGS)}"
+ )
self._encoding_method = method
return self
diff --git a/qdp/qdp-python/src/engine.rs b/qdp/qdp-python/src/engine.rs
index 58c46babc..cfebfd86e 100644
--- a/qdp/qdp-python/src/engine.rs
+++ b/qdp/qdp-python/src/engine.rs
@@ -23,41 +23,17 @@ use crate::tensor::QuantumTensor;
use numpy::{PyReadonlyArray1, PyReadonlyArray2, PyUntypedArrayMethods};
use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
-use qdp_core::{Precision, QdpEngine as CoreEngine};
+use qdp_core::{Dtype, Encoding, QdpEngine as CoreEngine};
#[cfg(target_os = "linux")]
use crate::loader::{PyQuantumLoader, config_from_args, parse_null_handling,
path_from_py};
-struct CudaEngineAdapter {
- engine: CoreEngine,
-}
-
-impl CudaEngineAdapter {
- fn new(device_id: usize, precision: Precision) -> PyResult<Self> {
- let engine = CoreEngine::new_with_precision(device_id,
precision).map_err(|e| {
- PyRuntimeError::new_err(format!("Failed to initialize CUDA
backend: {}", e))
- })?;
- Ok(Self { engine })
- }
-
- fn engine(&self) -> &CoreEngine {
- &self.engine
- }
-}
-
-/// PyO3 wrapper for the Rust/CUDA QdpEngine.
+/// PyO3 wrapper for QdpEngine
///
-/// The public Python facade routes AMD/Triton directly in `qumat_qdp.backend`.
-/// `_qdp.QdpEngine` stays focused on the Rust CUDA core and its tensor
contract.
-
+/// Provides Python bindings for GPU-accelerated quantum state encoding.
#[pyclass]
pub struct QdpEngine {
- engine: CudaEngineAdapter,
- #[allow(dead_code)]
- device_id: usize,
- #[allow(dead_code)]
- precision: Precision,
- backend: String,
+ pub engine: CoreEngine,
}
#[pymethods]
@@ -74,44 +50,14 @@ impl QdpEngine {
/// Raises:
/// RuntimeError: If CUDA device initialization fails
#[new]
- #[pyo3(signature = (device_id=0, precision="float32", backend="cuda"))]
- fn new(device_id: usize, precision: &str, backend: &str) -> PyResult<Self>
{
- let precision = match precision.to_ascii_lowercase().as_str() {
- "float32" | "f32" | "float" => Precision::Float32,
- "float64" | "f64" | "double" => Precision::Float64,
- other => {
- return Err(PyRuntimeError::new_err(format!(
- "Unsupported precision '{}'. Use 'float32' (default) or
'float64'.",
- other
- )));
- }
- };
-
- let backend_name = backend.to_ascii_lowercase();
- let (engine, resolved_backend) = match backend_name.as_str() {
- "cuda" => (
- CudaEngineAdapter::new(device_id, precision)?,
- "cuda".to_string(),
- ),
- "amd" | "triton_amd" => {
- return Err(PyRuntimeError::new_err(
- "AMD/Triton routing is provided by the Python facade
`qumat_qdp.QdpEngine`; `_qdp.QdpEngine` only supports the Rust CUDA backend.",
- ));
- }
- other => {
- return Err(PyRuntimeError::new_err(format!(
- "Unsupported backend '{}'. Use 'cuda'.",
- other
- )));
- }
- };
+ #[pyo3(signature = (device_id=0, precision="float32"))]
+ fn new(device_id: usize, precision: &str) -> PyResult<Self> {
+ let precision =
+ Dtype::from_str_ci(precision).map_err(|e|
PyRuntimeError::new_err(e.to_string()))?;
- Ok(Self {
- engine,
- device_id,
- precision,
- backend: resolved_backend,
- })
+ let engine = CoreEngine::new_with_precision(device_id, precision)
+ .map_err(|e| PyRuntimeError::new_err(format!("Failed to
initialize: {}", e)))?;
+ Ok(Self { engine })
}
/// Encode classical data into quantum state (auto-detects input type)
@@ -125,11 +71,10 @@ impl QdpEngine {
/// - String path: .parquet, .arrow, .feather, .npy, .pt, .pth,
.pb file
/// - pathlib.Path: Path object (converted via os.fspath())
/// num_qubits: Number of qubits for encoding
- /// encoding_method: Encoding strategy ("amplitude" default, "angle",
"basis",
- /// "iqp", or "iqp-z"). CUDA tensor notes:
- /// - amplitude and angle accept float64 and float32
- /// - basis requires int64
- /// - iqp and iqp-z require float64
+ /// encoding_method: Encoding strategy ("amplitude" default, "angle",
or "basis")
+ /// CUDA tensor note:
+ /// - amplitude accepts float64 and float32
+ /// - angle accepts float64 generally, plus float32 for 1D
single-sample tensors
///
/// Returns:
/// QuantumTensor: DLPack-compatible tensor for zero-copy PyTorch
integration
@@ -152,94 +97,6 @@ impl QdpEngine {
data: &Bound<'_, PyAny>,
num_qubits: usize,
encoding_method: &str,
- ) -> PyResult<QuantumTensor> {
- self.encode_with_core(data, num_qubits, encoding_method)
- }
-
- fn backend(&self) -> &str {
- &self.backend
- }
-
- #[cfg(target_os = "linux")]
- #[pyo3(signature = (total_batches, batch_size, num_qubits,
encoding_method, seed=None, null_handling=None))]
- fn create_synthetic_loader(
- &self,
- total_batches: usize,
- batch_size: usize,
- num_qubits: u32,
- encoding_method: &str,
- seed: Option<u64>,
- null_handling: Option<&str>,
- ) -> PyResult<PyQuantumLoader> {
- self.create_synthetic_loader_impl(
- total_batches,
- batch_size,
- num_qubits,
- encoding_method,
- seed,
- null_handling,
- )
- }
-
- #[cfg(target_os = "linux")]
- #[allow(clippy::too_many_arguments)]
- #[pyo3(signature = (path, batch_size, num_qubits, encoding_method,
batch_limit=None, null_handling=None))]
- fn create_file_loader(
- &self,
- py: Python<'_>,
- path: &Bound<'_, PyAny>,
- batch_size: usize,
- num_qubits: u32,
- encoding_method: &str,
- batch_limit: Option<usize>,
- null_handling: Option<&str>,
- ) -> PyResult<PyQuantumLoader> {
- self.create_file_loader_impl(
- py,
- path,
- batch_size,
- num_qubits,
- encoding_method,
- batch_limit,
- null_handling,
- )
- }
-
- #[cfg(target_os = "linux")]
- #[allow(clippy::too_many_arguments)]
- #[pyo3(signature = (path, batch_size, num_qubits, encoding_method,
batch_limit=None, null_handling=None))]
- fn create_streaming_file_loader(
- &self,
- py: Python<'_>,
- path: &Bound<'_, PyAny>,
- batch_size: usize,
- num_qubits: u32,
- encoding_method: &str,
- batch_limit: Option<usize>,
- null_handling: Option<&str>,
- ) -> PyResult<PyQuantumLoader> {
- self.create_streaming_file_loader_impl(
- py,
- path,
- batch_size,
- num_qubits,
- encoding_method,
- batch_limit,
- null_handling,
- )
- }
-}
-
-impl QdpEngine {
- fn core_engine(&self) -> PyResult<&CoreEngine> {
- Ok(self.engine.engine())
- }
-
- fn encode_with_core(
- &self,
- data: &Bound<'_, PyAny>,
- num_qubits: usize,
- encoding_method: &str,
) -> PyResult<QuantumTensor> {
// Check if it's a string path
if let Ok(path) = data.extract::<String>() {
@@ -293,7 +150,7 @@ impl QdpEngine {
PyRuntimeError::new_err("NumPy array must be contiguous
(C-order)")
})?;
let ptr = self
- .core_engine()?
+ .engine
.encode(data_slice, num_qubits, encoding_method)
.map_err(|e| PyRuntimeError::new_err(format!("Encoding
failed: {}", e)))?;
Ok(QuantumTensor {
@@ -315,7 +172,7 @@ impl QdpEngine {
PyRuntimeError::new_err("NumPy array must be contiguous
(C-order)")
})?;
let ptr = self
- .core_engine()?
+ .engine
.encode_batch(
data_slice,
num_samples,
@@ -345,7 +202,7 @@ impl QdpEngine {
// Validate CUDA tensor for direct GPU encoding
validate_cuda_tensor_for_encoding(
data,
- self.core_engine()?.device().ordinal(),
+ self.engine.device().ordinal(),
encoding_method,
)?;
@@ -374,7 +231,7 @@ impl QdpEngine {
// (held by Python's GIL), and we validated
dtype/contiguity/device above.
// The DLPackTensorInfo RAII wrapper will call deleter
when dropped.
let ptr = unsafe {
- self.core_engine()?
+ self.engine
.encode_from_gpu_ptr(
dlpack_info.data_ptr,
input_len,
@@ -396,7 +253,7 @@ impl QdpEngine {
let sample_size = dlpack_info.shape[1] as usize;
// SAFETY: Same as above - pointer from validated DLPack
tensor
let ptr = unsafe {
- self.core_engine()?
+ self.engine
.encode_batch_from_gpu_ptr(
dlpack_info.data_ptr,
num_samples,
@@ -452,7 +309,7 @@ impl QdpEngine {
)
})?;
let ptr = self
- .core_engine()?
+ .engine
.encode(data_slice, num_qubits, encoding_method)
.map_err(|e| PyRuntimeError::new_err(format!("Encoding
failed: {}", e)))?;
Ok(QuantumTensor {
@@ -478,7 +335,7 @@ impl QdpEngine {
)
})?;
let ptr = self
- .core_engine()?
+ .engine
.encode_batch(
data_slice,
num_samples,
@@ -509,7 +366,7 @@ impl QdpEngine {
)
})?;
let ptr = self
- .core_engine()?
+ .engine
.encode(&vec_data, num_qubits, encoding_method)
.map_err(|e| PyRuntimeError::new_err(format!("Encoding failed:
{}", e)))?;
Ok(QuantumTensor {
@@ -540,31 +397,31 @@ impl QdpEngine {
};
let ptr = if path.ends_with(".parquet") {
- self.core_engine()?
+ self.engine
.encode_from_parquet(path, num_qubits, encoding_method)
.map_err(|e| {
PyRuntimeError::new_err(format!("Encoding from parquet
failed: {}", e))
})?
} else if path.ends_with(".arrow") || path.ends_with(".feather") {
- self.core_engine()?
+ self.engine
.encode_from_arrow_ipc(path, num_qubits, encoding_method)
.map_err(|e| {
PyRuntimeError::new_err(format!("Encoding from Arrow IPC
failed: {}", e))
})?
} else if path.ends_with(".npy") {
- self.core_engine()?
+ self.engine
.encode_from_numpy(path, num_qubits, encoding_method)
.map_err(|e| {
PyRuntimeError::new_err(format!("Encoding from NumPy
failed: {}", e))
})?
} else if path.ends_with(".pt") || path.ends_with(".pth") {
- self.core_engine()?
+ self.engine
.encode_from_torch(path, num_qubits, encoding_method)
.map_err(|e| {
PyRuntimeError::new_err(format!("Encoding from PyTorch
failed: {}", e))
})?
} else if path.ends_with(".pb") {
- self.core_engine()?
+ self.engine
.encode_from_tensorflow(path, num_qubits, encoding_method)
.map_err(|e| {
PyRuntimeError::new_err(format!("Encoding from TensorFlow
failed: {}", e))
@@ -596,7 +453,6 @@ impl QdpEngine {
/// >>> engine = QdpEngine(device_id=0)
/// >>> batched = engine.encode_from_tensorflow("data.pb", 16,
"amplitude")
/// >>> torch_tensor = torch.from_dlpack(batched) # Shape: [200,
65536]
- #[allow(dead_code)]
fn encode_from_tensorflow(
&self,
path: &str,
@@ -604,7 +460,7 @@ impl QdpEngine {
encoding_method: &str,
) -> PyResult<QuantumTensor> {
let ptr = self
- .core_engine()?
+ .engine
.encode_from_tensorflow(path, num_qubits, encoding_method)
.map_err(|e| {
PyRuntimeError::new_err(format!("Encoding from TensorFlow
failed: {}", e))
@@ -625,21 +481,19 @@ impl QdpEngine {
num_qubits: usize,
encoding_method: &str,
) -> PyResult<QuantumTensor> {
- validate_cuda_tensor_for_encoding(
+ let encoding = validate_cuda_tensor_for_encoding(
data,
- self.core_engine()?.device().ordinal(),
+ self.engine.device().ordinal(),
encoding_method,
)?;
-
let dtype = data.getattr("dtype")?;
let dtype_str: String = dtype.str()?.extract()?;
- let dtype_str_lower = dtype_str.to_ascii_lowercase();
- let is_f32 = dtype_str_lower.contains("float32");
- let method = encoding_method.to_ascii_lowercase();
+ let is_f32 = dtype_str.to_ascii_lowercase().contains("float32");
let ndim: usize = data.call_method0("dim")?.extract()?;
let tensor_info = extract_cuda_tensor_info(data)?;
- if is_f32 && matches!(method.as_str(), "amplitude" | "angle") {
+ let f32_fast_path = is_f32 && matches!(encoding, Encoding::Amplitude |
Encoding::Angle);
+ if f32_fast_path {
match ndim {
1 => {
let input_len: usize =
data.call_method0("numel")?.extract()?;
@@ -648,9 +502,9 @@ impl QdpEngine {
let data_ptr = data_ptr_u64 as *const f32;
let ptr = unsafe {
- match method.as_str() {
- "amplitude" => self
- .core_engine()?
+ match encoding {
+ Encoding::Amplitude => self
+ .engine
.encode_from_gpu_ptr_f32_with_stream(
data_ptr, input_len, num_qubits,
stream_ptr,
)
@@ -660,8 +514,8 @@ impl QdpEngine {
e
))
})?,
- "angle" => self
- .core_engine()?
+ Encoding::Angle => self
+ .engine
.encode_angle_from_gpu_ptr_f32_with_stream(
data_ptr, input_len, num_qubits,
stream_ptr,
)
@@ -671,7 +525,7 @@ impl QdpEngine {
e
))
})?,
- _ => unreachable!("unreachable: unhandled f32
encoding method"),
+ _ => unreachable!("f32_fast_path guard allows only
Amplitude or Angle"),
}
};
@@ -688,9 +542,9 @@ impl QdpEngine {
let data_ptr = data_ptr_u64 as *const f32;
let ptr = unsafe {
- match method.as_str() {
- "amplitude" => self
- .core_engine()?
+ match encoding {
+ Encoding::Amplitude => self
+ .engine
.encode_batch_from_gpu_ptr_f32_with_stream(
data_ptr,
num_samples,
@@ -704,8 +558,8 @@ impl QdpEngine {
e
))
})?,
- "angle" => self
- .core_engine()?
+ Encoding::Angle => self
+ .engine
.encode_angle_batch_from_gpu_ptr_f32_with_stream(
data_ptr,
num_samples,
@@ -719,7 +573,7 @@ impl QdpEngine {
e
))
})?,
- _ => unreachable!("unreachable: unhandled f32
batch encoding method"),
+ _ => unreachable!("f32_fast_path guard allows only
Amplitude or Angle"),
}
};
@@ -741,7 +595,7 @@ impl QdpEngine {
1 => {
let input_len = tensor_info.shape[0] as usize;
let ptr = unsafe {
- self.core_engine()?
+ self.engine
.encode_from_gpu_ptr_with_stream(
tensor_info.data_ptr as *const
std::ffi::c_void,
input_len,
@@ -762,7 +616,7 @@ impl QdpEngine {
let num_samples = tensor_info.shape[0] as usize;
let sample_size = tensor_info.shape[1] as usize;
let ptr = unsafe {
- self.core_engine()?
+ self.engine
.encode_batch_from_gpu_ptr_with_stream(
tensor_info.data_ptr as *const
std::ffi::c_void,
num_samples,
@@ -791,7 +645,9 @@ impl QdpEngine {
// --- Loader factory methods (Linux only) ---
#[cfg(target_os = "linux")]
- fn create_synthetic_loader_impl(
+ /// Create a synthetic-data pipeline iterator (for
QuantumDataLoader.source_synthetic()).
+ #[pyo3(signature = (total_batches, batch_size, num_qubits,
encoding_method, seed=None, null_handling=None))]
+ fn create_synthetic_loader(
&self,
total_batches: usize,
batch_size: usize,
@@ -800,27 +656,28 @@ impl QdpEngine {
seed: Option<u64>,
null_handling: Option<&str>,
) -> PyResult<PyQuantumLoader> {
- let engine = self.core_engine()?.clone();
let nh = parse_null_handling(null_handling)?;
let config = config_from_args(
- &engine,
+ &self.engine,
batch_size,
num_qubits,
encoding_method,
total_batches,
seed,
nh,
- true,
- );
- let iter = qdp_core::PipelineIterator::new_synthetic(engine,
config).map_err(|e| {
- PyRuntimeError::new_err(format!("create_synthetic_loader failed:
{}", e))
- })?;
+ Dtype::Float32,
+ )?;
+ let iter =
qdp_core::PipelineIterator::new_synthetic(self.engine.clone(), config).map_err(
+ |e| PyRuntimeError::new_err(format!("create_synthetic_loader
failed: {}", e)),
+ )?;
Ok(PyQuantumLoader::new(Some(iter)))
}
#[cfg(target_os = "linux")]
+ /// Create a file-backed pipeline iterator (full read then batch; for
QuantumDataLoader.source_file(path)).
#[allow(clippy::too_many_arguments)]
- fn create_file_loader_impl(
+ #[pyo3(signature = (path, batch_size, num_qubits, encoding_method,
batch_limit=None, null_handling=None))]
+ fn create_file_loader(
&self,
py: Python<'_>,
path: &Bound<'_, PyAny>,
@@ -832,18 +689,18 @@ impl QdpEngine {
) -> PyResult<PyQuantumLoader> {
let path_str = path_from_py(path)?;
let batch_limit = batch_limit.unwrap_or(usize::MAX);
- let engine = self.core_engine()?.clone();
let nh = parse_null_handling(null_handling)?;
let config = config_from_args(
- &engine,
+ &self.engine,
batch_size,
num_qubits,
encoding_method,
0,
None,
nh,
- true, // float32_pipeline
- );
+ Dtype::Float32,
+ )?;
+ let engine = self.engine.clone();
// Resolve remote URLs before detaching from GIL. The _resolved guard
keeps the
// temp file alive until after the file is fully read inside py.detach.
#[cfg(feature = "remote-io")]
@@ -866,8 +723,10 @@ impl QdpEngine {
}
#[cfg(target_os = "linux")]
+ /// Create a streaming Parquet pipeline iterator (for
QuantumDataLoader.source_file(path, streaming=True)).
#[allow(clippy::too_many_arguments)]
- fn create_streaming_file_loader_impl(
+ #[pyo3(signature = (path, batch_size, num_qubits, encoding_method,
batch_limit=None, null_handling=None))]
+ fn create_streaming_file_loader(
&self,
py: Python<'_>,
path: &Bound<'_, PyAny>,
@@ -879,18 +738,18 @@ impl QdpEngine {
) -> PyResult<PyQuantumLoader> {
let path_str = path_from_py(path)?;
let batch_limit = batch_limit.unwrap_or(usize::MAX);
- let engine = self.core_engine()?.clone();
let nh = parse_null_handling(null_handling)?;
let config = config_from_args(
- &engine,
+ &self.engine,
batch_size,
num_qubits,
encoding_method,
0,
None,
nh,
- true, // float32_pipeline
- );
+ Dtype::Float32,
+ )?;
+ let engine = self.engine.clone();
// Resolve remote URLs before detaching from GIL. The _resolved guard
keeps the
// temp file alive; the streaming reader's open fd preserves data
after drop.
#[cfg(feature = "remote-io")]
diff --git a/qdp/qdp-python/src/lib.rs b/qdp/qdp-python/src/lib.rs
index 5348c3f4a..04d772a90 100644
--- a/qdp/qdp-python/src/lib.rs
+++ b/qdp/qdp-python/src/lib.rs
@@ -31,7 +31,7 @@ use loader::PyQuantumLoader;
#[cfg(target_os = "linux")]
#[pyfunction]
-#[pyo3(signature = (device_id, num_qubits, batch_size, total_batches,
encoding_method, warmup_batches=0, seed=None, float32_pipeline=false))]
+#[pyo3(signature = (device_id, num_qubits, batch_size, total_batches,
encoding_method, warmup_batches=0, seed=None, dtype="f64"))]
#[allow(clippy::too_many_arguments)]
fn run_throughput_pipeline_py(
py: Python<'_>,
@@ -42,18 +42,20 @@ fn run_throughput_pipeline_py(
encoding_method: String,
warmup_batches: usize,
seed: Option<u64>,
- float32_pipeline: bool,
+ dtype: &str,
) -> PyResult<(f64, f64, f64)> {
let config = qdp_core::PipelineConfig {
device_id,
num_qubits,
batch_size,
total_batches,
- encoding_method,
+ encoding: qdp_core::Encoding::from_str_ci(&encoding_method)
+ .map_err(|e| PyRuntimeError::new_err(format!("Invalid
encoding_method: {e}")))?,
seed,
warmup_batches,
null_handling: qdp_core::NullHandling::default(),
- float32_pipeline,
+ dtype: qdp_core::Dtype::from_str_ci(dtype)
+ .map_err(|e| PyRuntimeError::new_err(format!("Invalid dtype:
{e}")))?,
prefetch_depth: 16,
};
let result = py
diff --git a/qdp/qdp-python/src/loader.rs b/qdp/qdp-python/src/loader.rs
index 7ad7632cb..a43f94794 100644
--- a/qdp/qdp-python/src/loader.rs
+++ b/qdp/qdp-python/src/loader.rs
@@ -21,7 +21,7 @@ mod loader_impl {
use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use qdp_core::reader::NullHandling;
- use qdp_core::{PipelineConfig, PipelineIterator, QdpEngine as CoreEngine};
+ use qdp_core::{Dtype, Encoding, PipelineConfig, PipelineIterator,
QdpEngine as CoreEngine};
/// Rust-backed iterator yielding one QuantumTensor per batch; used by
QuantumDataLoader.
#[pyclass]
@@ -93,20 +93,22 @@ mod loader_impl {
total_batches: usize,
seed: Option<u64>,
null_handling: NullHandling,
- float32_pipeline: bool,
- ) -> PipelineConfig {
- PipelineConfig {
+ dtype: Dtype,
+ ) -> PyResult<PipelineConfig> {
+ let encoding = Encoding::from_str_ci(encoding_method)
+ .map_err(|e| PyRuntimeError::new_err(format!("Invalid encoding:
{e}")))?;
+ Ok(PipelineConfig {
device_id: 0,
num_qubits,
batch_size,
total_batches,
- encoding_method: encoding_method.to_string(),
+ encoding,
seed,
warmup_batches: 0,
null_handling,
- float32_pipeline,
+ dtype,
prefetch_depth: 16,
- }
+ })
}
/// Resolve path from Python str or pathlib.Path (__fspath__).
diff --git a/qdp/qdp-python/src/pytorch.rs b/qdp/qdp-python/src/pytorch.rs
index af014c3f3..e3bb0ca22 100644
--- a/qdp/qdp-python/src/pytorch.rs
+++ b/qdp/qdp-python/src/pytorch.rs
@@ -18,7 +18,8 @@ use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use std::ffi::c_void;
-use crate::constants::{CUDA_ENCODING_METHODS,
format_supported_cuda_encoding_methods};
+use crate::constants::format_supported_cuda_encoding_methods;
+use qdp_core::Encoding;
/// Helper to detect PyTorch tensor
pub fn is_pytorch_tensor(obj: &Bound<'_, PyAny>) -> PyResult<bool> {
@@ -143,16 +144,21 @@ pub fn get_torch_cuda_stream_ptr(tensor: &Bound<'_,
PyAny>) -> PyResult<*mut c_v
})
}
-/// Validate a CUDA tensor for direct GPU encoding
-/// Checks: dtype matches encoding method, contiguous, non-empty, device_id
matches engine
+/// Validate a CUDA tensor for direct GPU encoding and return the parsed
`Encoding`.
+///
+/// Checks dtype compatibility, contiguity, non-empty, and device match.
+/// Returns the parsed `Encoding` so the caller avoids re-parsing the same
string.
pub fn validate_cuda_tensor_for_encoding(
tensor: &Bound<'_, PyAny>,
expected_device_id: usize,
encoding_method: &str,
-) -> PyResult<()> {
- let method = encoding_method.to_ascii_lowercase();
+) -> PyResult<Encoding> {
+ let encoding = Encoding::from_str_ci(encoding_method)
+ .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
- if !CUDA_ENCODING_METHODS.contains(&method.as_str()) {
+ // Phase encoding has no zero-copy CUDA tensor kernel yet; the user-facing
+ // error below tells callers to fall back to a CPU tensor.
+ if matches!(encoding, Encoding::Phase) {
return Err(PyRuntimeError::new_err(format!(
"CUDA tensor encoding currently only supports {} methods, got
'{}'. \
Use tensor.cpu() to convert to CPU tensor for other encoding
methods.",
@@ -161,30 +167,31 @@ pub fn validate_cuda_tensor_for_encoding(
)));
}
- // Check encoding method support and dtype (ASCII lowercase for
case-insensitive match).
let dtype = tensor.getattr("dtype")?;
let dtype_str: String = dtype.str()?.extract()?;
let dtype_str_lower = dtype_str.to_ascii_lowercase();
- match method.as_str() {
- "amplitude" | "angle" => {
+ match encoding {
+ Encoding::Amplitude | Encoding::Angle => {
if !(dtype_str_lower.contains("float64") ||
dtype_str_lower.contains("float32")) {
return Err(PyRuntimeError::new_err(format!(
"CUDA tensor must have dtype float64 or float32 for {}
encoding, got {}. \
Use tensor.to(torch.float64) or tensor.to(torch.float32)",
- method, dtype_str
+ encoding.as_str(),
+ dtype_str
)));
}
}
- "iqp" | "iqp-z" => {
+ Encoding::Iqp | Encoding::IqpZ => {
if !dtype_str_lower.contains("float64") {
return Err(PyRuntimeError::new_err(format!(
"CUDA tensor must have dtype float64 for {} encoding, got
{}. \
Use tensor.to(torch.float64)",
- method, dtype_str
+ encoding.as_str(),
+ dtype_str
)));
}
}
- "basis" => {
+ Encoding::Basis => {
if !dtype_str_lower.contains("int64") {
return Err(PyRuntimeError::new_err(format!(
"CUDA tensor must have dtype int64 for basis encoding, got
{}. \
@@ -193,12 +200,7 @@ pub fn validate_cuda_tensor_for_encoding(
)));
}
}
- _ => {
- return Err(PyRuntimeError::new_err(format!(
- "Internal error: missing CUDA validation branch for supported
method '{}'",
- method
- )));
- }
+ Encoding::Phase => unreachable!("Phase filtered above"),
}
// Check contiguous
@@ -225,7 +227,7 @@ pub fn validate_cuda_tensor_for_encoding(
)));
}
- Ok(())
+ Ok(encoding)
}
/// Minimal CUDA tensor metadata extracted via PyTorch APIs.
diff --git a/testing/qdp/test_bindings.py b/testing/qdp/test_bindings.py
index 66d1f26a5..b6a2b60f5 100644
--- a/testing/qdp/test_bindings.py
+++ b/testing/qdp/test_bindings.py
@@ -398,9 +398,7 @@ def test_encode_cuda_tensor_angle_float16_rejected():
engine = QdpEngine(0)
data = torch.tensor([0.0, torch.pi / 2], dtype=torch.float16,
device="cuda:0")
- with pytest.raises(
- RuntimeError, match="float64 for angle encoding|supports only 1D"
- ):
+ with pytest.raises(RuntimeError, match="float64 or float32 for angle
encoding"):
engine.encode(data, 2, "angle")
@@ -537,7 +535,7 @@ def test_encode_cuda_tensor_invalid_encoding_method():
with pytest.raises(
RuntimeError,
- match="only supports .*amplitude.*angle.*basis.*iqp.*iqp-z.*Use
tensor.cpu",
+ match="Unknown encoding: unknown-encoding",
):
engine.encode(data, 2, "unknown-encoding")
diff --git a/testing/qdp_python/test_dlpack_validation.py
b/testing/qdp_python/test_dlpack_validation.py
index 5ac462d5f..b093ee1cb 100644
--- a/testing/qdp_python/test_dlpack_validation.py
+++ b/testing/qdp_python/test_dlpack_validation.py
@@ -183,9 +183,7 @@ def test_cuda_float16_angle_rejected() -> None:
engine = _engine()
t = torch.tensor([0.0, torch.pi / 2], dtype=torch.float16, device="cuda")
- with pytest.raises(
- RuntimeError, match="float64 for angle encoding|supports only 1D"
- ):
+ with pytest.raises(RuntimeError, match="float64 or float32 for angle
encoding"):
engine.encode(t, num_qubits=2, encoding_method="angle")