use burn_core::tensor::{DType, Int, Shape, Tensor};
use burn_cubecl::cubecl::{cuda::CudaRuntime, Runtime};
use burn_cubecl::{ops::numeric::empty_device_contiguous_dtype, CubeBackend};
use burn_cuda::{Cuda, CudaDevice};
use burn_fusion::{get_client, stream::OperationStreams, NoOp};
use burn_ir::{BackendIr, InitOperationIr, OperationIr};
use j2k_cuda_runtime::{CudaContext, CudaExternalDeviceBufferViewMut};
use crate::BurnDecodeError;
type InnerCuda = CubeBackend<CudaRuntime, f32, i32, u8>;
pub(super) struct SubmittedBatchIntTensor<R, const D: usize> {
payload: R,
cube: burn_cubecl::tensor::CubeTensor<CudaRuntime>,
shape: Shape,
dtype: DType,
device: CudaDevice,
}
impl<R, const D: usize> SubmittedBatchIntTensor<R, D> {
pub(super) fn payload(&self) -> &R {
&self.payload
}
pub(super) fn into_parts(
self,
) -> (
burn_cubecl::tensor::CubeTensor<CudaRuntime>,
Shape,
DType,
CudaDevice,
R,
) {
let Self {
payload,
cube,
shape,
dtype,
device,
} = self;
(cube, shape, dtype, device, payload)
}
}
pub(super) fn fill_batch_int_tensor<R, const D: usize>(
shape: [usize; D],
dtype: DType,
device: &CudaDevice,
context: &CudaContext,
fill: impl FnOnce(&mut CudaExternalDeviceBufferViewMut<'_>) -> Result<R, BurnDecodeError>,
) -> Result<SubmittedBatchIntTensor<R, D>, BurnDecodeError> {
let logical_len = tensor_byte_len(&shape, dtype)?;
let burn_shape = Shape::from(shape.to_vec());
let client = CudaRuntime::client(device);
let cube = empty_device_contiguous_dtype(client, device.clone(), burn_shape.clone(), dtype);
let handle_len =
usize::try_from(cube.handle.size_in_used()).map_err(|_| BurnDecodeError::SizeOverflow)?;
if handle_len != logical_len {
return Err(interop(format!(
"CubeCL tensor handle exposes {handle_len} bytes; expected {logical_len}"
)));
}
let raw_stream = unsafe { cube.client.external_write_stream(&cube.handle) }
.map_err(|error| interop(format!("CubeCL stream handoff failed: {error}")))?;
let mut stream_owner = cube.client.clone();
let mut resource = cube
.client
.get_resource(cube.handle.clone())
.map_err(|error| interop(format!("CubeCL resource access failed: {error}")))?;
let raw = resource.resource();
let available = usize::try_from(raw.size).map_err(|_| BurnDecodeError::SizeOverflow)?;
if logical_len > available {
return Err(interop(format!(
"CubeCL resource exposes {available} bytes for a {logical_len}-byte tensor"
)));
}
let submission = unsafe {
let mut destination = CudaExternalDeviceBufferViewMut::from_raw_parts(
context,
raw.ptr,
logical_len,
dtype.size(),
&mut resource,
)
.map_err(|error| interop(error.to_string()))?;
let mut work_invoked = false;
let ordered = context.with_primary_stream_ordering(raw_stream, &mut stream_owner, || {
work_invoked = true;
fill(&mut destination)
});
drop(destination);
match ordered {
Ok(Ok(output)) => Ok(output),
Ok(Err(error)) => {
let quarantine = burn_error_completion_is_uncertain(&error);
Err((error, quarantine))
}
Err(error) => {
let quarantine = work_invoked && error.completion_is_uncertain();
Err((
interop(format!("CUDA stream event handoff failed: {error}")),
quarantine,
))
}
}
};
drop(resource);
let output = match submission {
Ok(output) => output,
Err((error, quarantine)) => {
if quarantine {
std::mem::forget(cube);
}
return Err(error);
}
};
Ok(SubmittedBatchIntTensor {
payload: output,
cube,
shape: burn_shape,
dtype,
device: device.clone(),
})
}
fn tensor_byte_len(shape: &[usize], dtype: DType) -> Result<usize, BurnDecodeError> {
shape
.iter()
.try_fold(dtype.size(), |size, dim| size.checked_mul(*dim))
.ok_or(BurnDecodeError::SizeOverflow)
}
pub(super) fn register_int_tensor<const D: usize>(
cube: burn_cubecl::tensor::CubeTensor<CudaRuntime>,
shape: Shape,
dtype: DType,
device: &CudaDevice,
) -> Tensor<Cuda, D, Int> {
let fusion = get_client::<InnerCuda>(device);
let handle = <InnerCuda as BackendIr>::int_tensor_handle(cube);
let desc = InitOperationIr::create(shape, dtype, || fusion.register_tensor_handle(handle));
let primitive = fusion
.register(
OperationStreams::default(),
OperationIr::Init(desc),
NoOp::<InnerCuda>::new(),
)
.remove(0);
Tensor::<Cuda, D, Int>::from_primitive(primitive)
}
fn interop(message: impl Into<String>) -> BurnDecodeError {
BurnDecodeError::AcceleratorInterop {
backend: "CUDA",
message: message.into(),
}
}
fn burn_error_completion_is_uncertain(error: &BurnDecodeError) -> bool {
matches!(error, BurnDecodeError::Cuda(source) if source.completion_is_uncertain())
}
#[cfg(test)]
mod tests {
use burn_core::tensor::DType;
use super::{burn_error_completion_is_uncertain, tensor_byte_len};
use crate::BurnDecodeError;
#[test]
fn tensor_byte_length_is_exact_and_overflow_checked_before_allocation() {
assert_eq!(tensor_byte_len(&[2, 3, 4], DType::U16).unwrap(), 48);
assert!(matches!(
tensor_byte_len(&[usize::MAX, 2], DType::U16),
Err(BurnDecodeError::SizeOverflow)
));
}
#[test]
fn only_uncertain_cuda_completion_requires_allocation_quarantine() {
let validation = BurnDecodeError::Cuda(j2k_cuda::CudaBatchError::GroupExecution {
source_indices: vec![0],
source: Box::new(j2k_cuda::Error::UnsupportedCudaRequest {
reason: "pre-submit validation",
}),
});
assert!(!burn_error_completion_is_uncertain(&validation));
let uncertain = BurnDecodeError::Cuda(j2k_cuda::CudaBatchError::GroupExecution {
source_indices: vec![0],
source: Box::new(j2k_cuda::Error::CudaRuntime {
source: j2k_cuda_runtime::CudaError::StatePoisoned {
message: "completion unknown".to_string(),
},
}),
});
assert!(burn_error_completion_is_uncertain(&uncertain));
}
}