blob: 01fa0c71369e03910dd49651b0552997c9f1530b [file]
//
// 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.
use pyo3::exceptions::PyRuntimeError;
use pyo3::ffi;
use pyo3::prelude::*;
use qdp_core::dlpack::DLManagedTensor;
/// Quantum tensor wrapper implementing DLPack protocol
///
/// This class wraps a GPU-allocated quantum state vector and implements
/// the DLPack protocol for zero-copy integration with PyTorch and other
/// array libraries.
///
/// Example:
/// >>> engine = QdpEngine(device_id=0)
/// >>> qtensor = engine.encode([1.0, 2.0, 3.0], num_qubits=2, encoding_method="amplitude")
/// >>> torch_tensor = torch.from_dlpack(qtensor)
#[pyclass]
pub struct QuantumTensor {
pub ptr: *mut DLManagedTensor,
pub consumed: bool,
}
#[pymethods]
impl QuantumTensor {
/// Implements DLPack protocol - returns PyCapsule for PyTorch
///
/// This method is called by torch.from_dlpack() to get the GPU memory pointer.
/// The capsule can only be consumed once to prevent double-free errors.
///
/// Args:
/// stream: Optional CUDA stream (DLPack 0.8+; 1=legacy default, 2=per-thread default)
///
/// Returns:
/// PyCapsule containing DLManagedTensor pointer
///
/// Raises:
/// RuntimeError: If the tensor has already been consumed
#[pyo3(signature = (stream=None))]
fn __dlpack__<'py>(&mut self, py: Python<'py>, stream: Option<i64>) -> PyResult<Py<PyAny>> {
if self.consumed {
return Err(PyRuntimeError::new_err(
"DLPack tensor already consumed (can only be used once)",
));
}
if self.ptr.is_null() {
return Err(PyRuntimeError::new_err("Invalid DLPack tensor pointer"));
}
if let Some(stream) = stream
&& stream > 0
{
let stream_ptr = qdp_core::dlpack::dlpack_stream_to_cuda(stream);
unsafe {
qdp_core::dlpack::synchronize_stream(stream_ptr).map_err(|e| {
PyRuntimeError::new_err(format!("CUDA stream sync failed: {}", e))
})?;
}
}
// Mark as consumed to prevent double-free
self.consumed = true;
// Create PyCapsule using FFI
// PyTorch will call the deleter stored in DLManagedTensor.deleter
// Use a static C string for the capsule name to avoid lifetime issues
const DLTENSOR_NAME: &[u8] = b"dltensor\0";
unsafe {
// Create PyCapsule without a destructor
// PyTorch will manually call the deleter from DLManagedTensor
let capsule_ptr = ffi::PyCapsule_New(
self.ptr as *mut std::ffi::c_void,
DLTENSOR_NAME.as_ptr() as *const i8,
None, // No destructor - PyTorch handles it
);
if capsule_ptr.is_null() {
return Err(PyRuntimeError::new_err("Failed to create PyCapsule"));
}
Ok(Py::from_owned_ptr(py, capsule_ptr))
}
}
/// Returns DLPack device information
///
/// Returns:
/// Tuple of (device_type, device_id) where device_type follows DLPack constants
fn __dlpack_device__(&self) -> PyResult<(i32, i32)> {
if self.ptr.is_null() {
return Err(PyRuntimeError::new_err("Invalid DLPack tensor pointer"));
}
unsafe {
let tensor = &(*self.ptr).dl_tensor;
// DLPack device_type: kDLCUDA = 2, kDLROCM = 10, kDLCPU = 1
let device_type = match tensor.device.device_type {
qdp_core::dlpack::DLDeviceType::kDLCUDA => 2,
qdp_core::dlpack::DLDeviceType::kDLROCM => 10,
qdp_core::dlpack::DLDeviceType::kDLCPU => 1,
};
// Read device_id from DLPack tensor metadata
Ok((device_type, tensor.device.device_id))
}
}
}
impl Drop for QuantumTensor {
fn drop(&mut self) {
// Only free if not consumed by __dlpack__
// If consumed, PyTorch/consumer will call the deleter
if !self.consumed && !self.ptr.is_null() {
unsafe {
// Defensive check: qdp-core always provides a deleter
debug_assert!(
(*self.ptr).deleter.is_some(),
"DLManagedTensor from qdp-core should always have a deleter"
);
// Call the DLPack deleter to free memory
if let Some(deleter) = (*self.ptr).deleter {
deleter(self.ptr);
}
}
}
}
}
// Safety: QuantumTensor can be sent between threads
// The DLManagedTensor pointer management is thread-safe via Arc in the deleter
unsafe impl Send for QuantumTensor {}
unsafe impl Sync for QuantumTensor {}