diff --git a/dali/c_api_2/data_objects.cc b/dali/c_api_2/data_objects.cc new file mode 100644 index 0000000000..544f11106f --- /dev/null +++ b/dali/c_api_2/data_objects.cc @@ -0,0 +1,434 @@ +// Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Licensed 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. + +#include "dali/c_api_2/data_objects.h" +#include "dali/c_api_2/error_handling.h" +#include "dali/c_api_2/utils.h" + +namespace dali::c_api { + +RefCountedPtr ITensor::Create(daliBufferPlacement_t placement) { + Validate(placement); + switch (placement.device_type) { + case DALI_STORAGE_CPU: + { + auto tl = std::make_shared>(); + tl->set_pinned(placement.pinned); + if (placement.pinned) + tl->set_device_id(placement.device_id); + return Wrap(std::move(tl)); + } + case DALI_STORAGE_GPU: + { + auto tl = std::make_shared>(); + tl->set_pinned(placement.pinned); + tl->set_device_id(placement.device_id); + return Wrap(std::move(tl)); + } + default: + assert(!"Unreachable code"); + return {}; + } +} + +RefCountedPtr ITensorList::Create(daliBufferPlacement_t placement) { + Validate(placement); + switch (placement.device_type) { + case DALI_STORAGE_CPU: + { + auto tl = std::make_shared>(); + tl->set_pinned(placement.pinned); + if (placement.pinned) + tl->set_device_id(placement.device_id); + return Wrap(std::move(tl)); + } + case DALI_STORAGE_GPU: + { + auto tl = std::make_shared>(); + tl->set_pinned(placement.pinned); + tl->set_device_id(placement.device_id); + return Wrap(std::move(tl)); + } + default: + assert(!"Unreachable code"); + return {}; + } +} + +ITensor *ToPointer(daliTensor_h handle) { + if (!handle) + throw NullHandle("Tensor"); + return static_cast(handle); +} + +ITensorList *ToPointer(daliTensorList_h handle) { + if (!handle) + throw NullHandle("TensorList"); + return static_cast(handle); +} + +} // namespace dali::c_api + +using namespace dali::c_api; // NOLINT + +////////////////////////////////////////////////////////////////////////////// +// Tensor +////////////////////////////////////////////////////////////////////////////// + +daliResult_t daliTensorCreate(daliTensor_h *out, daliBufferPlacement_t placement) { + DALI_PROLOG(); + CHECK_OUTPUT(out); + auto t = dali::c_api::ITensor::Create(placement); + *out = t.release(); // no throwing allowed after this line! + DALI_EPILOG(); +} + +daliResult_t daliTensorIncRef(daliTensor_h t, int *new_ref) { + DALI_PROLOG(); + auto *ptr = ToPointer(t); + int r = ptr->IncRef(); + if (new_ref) + *new_ref = r; + DALI_EPILOG(); +} + +daliResult_t daliTensorDecRef(daliTensor_h t, int *new_ref) { + DALI_PROLOG(); + auto *ptr = ToPointer(t); + int r = ptr->DecRef(); + if (new_ref) + *new_ref = r; + DALI_EPILOG(); +} + +daliResult_t daliTensorRefCount(daliTensor_h t, int *ref) { + DALI_PROLOG(); + auto *ptr = ToPointer(t); + int r = ptr->RefCount(); + CHECK_OUTPUT(ref); + *ref = r; + DALI_EPILOG(); +} + +daliResult_t daliTensorAttachBuffer( + daliTensor_h tensor, + int ndim, + const int64_t *shape, + daliDataType_t dtype, + const char *layout, + void *data, + daliDeleter_t deleter) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor); + ptr->AttachBuffer(ndim, shape, dtype, layout, data, deleter); + DALI_EPILOG(); +} + +daliResult_t daliTensorResize( + daliTensor_h tensor, + int ndim, + const int64_t *shape, + daliDataType_t dtype, + const char *layout) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor); + ptr->Resize(ndim, shape, dtype, layout); + DALI_EPILOG(); +} + +daliResult_t daliTensorGetBufferPlacement( + daliTensor_h tensor, + daliBufferPlacement_t *out_placement) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor); + CHECK_OUTPUT(out_placement); + *out_placement = ptr->GetBufferPlacement(); + DALI_EPILOG(); +} + +daliResult_t daliTensorSetLayout( + daliTensor_h tensor, + const char *layout) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor); + ptr->SetLayout(layout); + DALI_EPILOG(); +} + +daliResult_t daliTensorGetLayout( + daliTensor_h tensor, + const char **layout) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor); + CHECK_OUTPUT(layout); + *layout = ptr->GetLayout(); + DALI_EPILOG(); +} + +daliResult_t daliTensorGetStream( + daliTensor_h tensor, + cudaStream_t *out_stream) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor); + CHECK_OUTPUT(out_stream); + auto str = ptr->GetStream(); + *out_stream = str.has_value() ? *str : cudaStream_t(-1); + return str.has_value() ? DALI_SUCCESS : DALI_NO_DATA; + DALI_EPILOG(); +} + +daliResult_t daliTensorSetStream( + daliTensor_h tensor, + const cudaStream_t *stream, + daliBool synchronize) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor); + ptr->SetStream(ToOptional(stream), synchronize); + DALI_EPILOG(); +} + +daliResult_t daliTensorGetDesc( + daliTensor_h tensor, + daliTensorDesc_t *out_desc) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor); + CHECK_OUTPUT(out_desc); + *out_desc = ptr->GetDesc(); + DALI_EPILOG(); +} + +daliResult_t daliTensorGetShape( + daliTensor_h tensor, + int *out_ndim, + const int64_t **out_shape) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor); + auto &shape = ptr->GetShape(); + if (out_ndim) + *out_ndim = shape.sample_dim(); + if (out_shape) + *out_shape = shape.data(); + DALI_EPILOG(); +} + +daliResult_t daliTensorGetSourceInfo( + daliTensor_h tensor, + const char **out_source_info) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor); + CHECK_OUTPUT(out_source_info); + *out_source_info = ptr->GetSourceInfo(); + DALI_EPILOG(); +} + +daliResult_t daliTensorSetSourceInfo( + daliTensor_h tensor, + const char *source_info) { + DALI_PROLOG(); + ToPointer(tensor)->SetSourceInfo(source_info); + DALI_EPILOG(); +} + +////////////////////////////////////////////////////////////////////////////// +// TensorList +////////////////////////////////////////////////////////////////////////////// + +daliResult_t daliTensorListCreate(daliTensorList_h *out, daliBufferPlacement_t placement) { + DALI_PROLOG(); + CHECK_OUTPUT(out); + auto tl = dali::c_api::ITensorList::Create(placement); + *out = tl.release(); // no throwing allowed after this line! + DALI_EPILOG(); +} + +daliResult_t daliTensorListIncRef(daliTensorList_h tl, int *new_ref) { + DALI_PROLOG(); + auto *ptr = ToPointer(tl); + int r = ptr->IncRef(); + if (new_ref) + *new_ref = r; + DALI_EPILOG(); +} + +daliResult_t daliTensorListDecRef(daliTensorList_h tl, int *new_ref) { + DALI_PROLOG(); + auto *ptr = ToPointer(tl); + int r = ptr->DecRef(); + if (new_ref) + *new_ref = r; + DALI_EPILOG(); +} + +daliResult_t daliTensorListRefCount(daliTensorList_h tl, int *ref) { + DALI_PROLOG(); + auto *ptr = ToPointer(tl); + CHECK_OUTPUT(ref); + int r = ptr->RefCount(); + *ref = r; + DALI_EPILOG(); +} + +daliResult_t daliTensorListAttachBuffer( + daliTensorList_h tensor_list, + int num_samples, + int ndim, + const int64_t *shapes, + daliDataType_t dtype, + const char *layout, + void *data, + const ptrdiff_t *sample_offsets, + daliDeleter_t deleter) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor_list); + ptr->AttachBuffer(num_samples, ndim, shapes, dtype, layout, data, sample_offsets, deleter); + DALI_EPILOG(); +} + +daliResult_t daliTensorListAttachSamples( + daliTensorList_h tensor_list, + int num_samples, + int ndim, + daliDataType_t dtype, + const char *layout, + const daliTensorDesc_t *samples, + const daliDeleter_t *sample_deleters) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor_list); + ptr->AttachSamples(num_samples, ndim, dtype, layout, samples, sample_deleters); + DALI_EPILOG(); +} + +daliResult_t daliTensorListResize( + daliTensorList_h tensor_list, + int num_samples, + int ndim, + const int64_t *shapes, + daliDataType_t dtype, + const char *layout) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor_list); + ptr->Resize(num_samples, ndim, shapes, dtype, layout); + DALI_EPILOG(); +} + +daliResult_t daliTensorListGetBufferPlacement( + daliTensorList_h tensor_list, + daliBufferPlacement_t *out_placement) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor_list); + CHECK_OUTPUT(out_placement); + *out_placement = ptr->GetBufferPlacement(); + DALI_EPILOG(); +} + + +daliResult_t daliTensorListSetLayout( + daliTensorList_h tensor_list, + const char *layout) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor_list); + ptr->SetLayout(layout); + DALI_EPILOG(); +} + +daliResult_t daliTensorListGetLayout( + daliTensorList_h tensor_list, + const char **layout) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor_list); + CHECK_OUTPUT(layout); + *layout = ptr->GetLayout(); + DALI_EPILOG(); +} + +daliResult_t daliTensorListGetStream( + daliTensorList_h tensor_list, + cudaStream_t *out_stream) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor_list); + CHECK_OUTPUT(out_stream); + auto str = ptr->GetStream(); + *out_stream = str.has_value() ? *str : cudaStream_t(-1); + return str.has_value() ? DALI_SUCCESS : DALI_NO_DATA; + DALI_EPILOG(); +} + +daliResult_t daliTensorListSetStream( + daliTensorList_h tensor_list, + const cudaStream_t *stream, + daliBool synchronize) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor_list); + ptr->SetStream(ToOptional(stream), synchronize); + DALI_EPILOG(); +} + +daliResult_t daliTensorListGetShape( + daliTensorList_h tensor, + int *out_num_samples, + int *out_ndim, + const int64_t **out_shape) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor); + auto &shape = ptr->GetShape(); + if (out_ndim) + *out_ndim = shape.sample_dim(); + if (out_num_samples) + *out_num_samples = shape.num_samples(); + if (out_shape) + *out_shape = shape.shapes.data(); + DALI_EPILOG(); +} + +daliResult_t daliTensorListGetTensorDesc( + daliTensorList_h tensor_list, + daliTensorDesc_t *out_tensor, + int sample_idx) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor_list); + CHECK_OUTPUT(out_tensor); + *out_tensor = ptr->GetTensorDesc(sample_idx); + DALI_EPILOG(); +} + +daliResult_t daliTensorListGetSourceInfo( + daliTensorList_h tensor_list, + const char **out_source_info, + int sample_idx) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor_list); + CHECK_OUTPUT(out_source_info); + *out_source_info = ptr->GetSourceInfo(sample_idx); + DALI_EPILOG(); +} + +daliResult_t daliTensorListSetSourceInfo( + daliTensorList_h tensor_list, + int sample_idx, + const char *source_info) { + DALI_PROLOG(); + ToPointer(tensor_list)->SetSourceInfo(sample_idx, source_info); + DALI_EPILOG(); +} + +daliResult_t daliTensorListViewAsTensor( + daliTensorList_h tensor_list, + daliTensor_h *out_tensor) { + DALI_PROLOG(); + auto *ptr = ToPointer(tensor_list); + CHECK_OUTPUT(out_tensor); + auto t = ptr->ViewAsTensor(); + *out_tensor = t.release(); // no throwing allowed after this line + DALI_EPILOG(); +} diff --git a/dali/c_api_2/data_objects.h b/dali/c_api_2/data_objects.h new file mode 100644 index 0000000000..b6139bdaac --- /dev/null +++ b/dali/c_api_2/data_objects.h @@ -0,0 +1,718 @@ +// Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Licensed 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. + +#ifndef DALI_C_API_2_DATA_OBJECTS_H_ +#define DALI_C_API_2_DATA_OBJECTS_H_ + +#include +#include +#include +#include +#include +#include +#define DALI_ALLOW_NEW_C_API +#include "dali/dali.h" +#include "dali/pipeline/data/tensor_list.h" +#include "dali/c_api_2/ref_counting.h" +#include "dali/c_api_2/validation.h" + + +struct _DALITensorList {}; +struct _DALITensor {}; + +namespace dali { +namespace c_api { + +////////////////////////////////////////////////////////////////////////////// +// Interfaces +////////////////////////////////////////////////////////////////////////////// + + +/** A DALI C API Tensor interface + * + * Please refer to the relevant C API documentation - e.g. for Resize, see daliTensorResize. + */ +class ITensor : public _DALITensor, public RefCountedObject { + public: + virtual ~ITensor() = default; + + virtual void Resize( + int ndim, + const int64_t *shape, + daliDataType_t dtype, + const char *layout) = 0; + + virtual void AttachBuffer( + int ndim, + const int64_t *shape, + daliDataType_t dtype, + const char *layout, + void *data, + daliDeleter_t deleter) = 0; + + virtual daliBufferPlacement_t GetBufferPlacement() const = 0; + + virtual const char *GetLayout() const = 0; + + virtual void SetLayout(const char *layout) = 0; + + virtual void SetStream(std::optional stream, bool synchronize) = 0; + + virtual std::optional GetStream() const = 0; + + virtual std::optional GetReadyEvent() const = 0; + + virtual cudaEvent_t GetOrCreateReadyEvent() = 0; + + virtual daliTensorDesc_t GetDesc() const = 0; + + virtual const TensorShape<> &GetShape() const & = 0; + + virtual const char *GetSourceInfo() const & = 0; + + virtual void SetSourceInfo(const char *source_info) = 0; + + /** Retrieves the underlying DALI Tensor pointer. + * + * Returns a shared pointer to the underlying DALI object. If the backend doesn't match, + * a null pointer is returned. + */ + template + const std::shared_ptr> &Unwrap() const &; + + static RefCountedPtr Create(daliBufferPlacement_t placement); +}; + + +/** A DALI C API TensorList interface + * + * Please refer to the relevant C API documentation - e.g. for Resize, see daliTensorListResize. + */ +class ITensorList : public _DALITensorList, public RefCountedObject { + public: + virtual ~ITensorList() = default; + + virtual void Resize( + int num_samples, + int ndim, + const int64_t *shapes, + daliDataType_t dtype, + const char *layout) = 0; + + virtual void AttachBuffer( + int num_samples, + int ndim, + const int64_t *shapes, + daliDataType_t dtype, + const char *layout, + void *data, + const ptrdiff_t *sample_offsets, + daliDeleter_t deleter) = 0; + + virtual void AttachSamples( + int num_samples, + int ndim, + daliDataType_t dtype, + const char *layout, + const daliTensorDesc_t *samples, + const daliDeleter_t *sample_deleters) = 0; + + virtual daliBufferPlacement_t GetBufferPlacement() const = 0; + + virtual const char *GetLayout() const = 0; + + virtual void SetLayout(const char *layout) = 0; + + virtual void SetStream(std::optional stream, bool synchronize) = 0; + + virtual std::optional GetStream() const = 0; + + virtual std::optional GetReadyEvent() const = 0; + + virtual cudaEvent_t GetOrCreateReadyEvent() = 0; + + virtual daliTensorDesc_t GetTensorDesc(int sample) const = 0; + + virtual const TensorListShape<> &GetShape() const & = 0; + + virtual RefCountedPtr ViewAsTensor() const = 0; + + virtual const char *GetSourceInfo(int sample) const & = 0; + + virtual void SetSourceInfo(int sample, const char *source_info) = 0; + + /** Retrieves the underlying DALI TensorList pointer. + * + * Returns a shared pointer to the underlying DALI object. If the backend doesn't match, + * a null pointer is returned. + */ + template + const std::shared_ptr> &Unwrap() const &; + + static RefCountedPtr Create(daliBufferPlacement_t placement); +}; + + +////////////////////////////////////////////////////////////////////////////// +// Implementation +////////////////////////////////////////////////////////////////////////////// + + +struct BufferDeleter { + daliDeleter_t deleter; + AccessOrder deletion_order; + + void operator()(void *data) { + if (deleter.delete_buffer) { + cudaStream_t stream = deletion_order.stream(); + deleter.delete_buffer(deleter.deleter_ctx, data, + deletion_order.is_device() ? &stream : nullptr); + } + if (deleter.destroy_context) { + deleter.destroy_context(deleter.deleter_ctx); + } + } +}; + +template +class TensorWrapper : public ITensor { + public: + explicit TensorWrapper(std::shared_ptr> t) : t_(std::move(t)) {} + + void Resize( + int ndim, + const int64_t *shape, + daliDataType_t dtype, + const char *layout) override { + ValidateShape(ndim, shape); + Validate(dtype); + if (layout) + Validate(TensorLayout(layout), ndim); + t_->Resize(TensorShape<>(make_cspan(shape, ndim)), dtype); + if (layout) + t_->SetLayout(layout); + } + + void AttachBuffer( + int ndim, + const int64_t *shape, + daliDataType_t dtype, + const char *layout, + void *data, + daliDeleter_t deleter) override { + ValidateShape(ndim, shape); + Validate(dtype); + if (layout) + Validate(TensorLayout(layout), ndim); + + TensorShape<> tshape(make_cspan(shape, ndim)); + size_t num_elements = volume(tshape); + if (num_elements > 0 && !data) + throw std::invalid_argument("The data buffer must not be NULL for a non-empty tensor."); + + TensorLayout new_layout = {}; + + if (!layout) { + if (ndim == t_->ndim()) + new_layout = t_->GetLayout(); + } else { + new_layout = layout; + Validate(new_layout, ndim); + } + + t_->Reset(); + auto type_info = TypeTable::GetTypeInfo(dtype); + auto element_size = type_info.size(); + + std::shared_ptr buffer; + if (!deleter.delete_buffer && !deleter.destroy_context) { + buffer = std::shared_ptr(data, [](void *){}); + } else { + buffer = std::shared_ptr(data, BufferDeleter{deleter, t_->order()}); + } + + t_->ShareData( + std::move(buffer), + num_elements * element_size, + t_->is_pinned(), + tshape, + dtype, + t_->device_id(), + t_->order()); + + if (layout) + t_->SetLayout(new_layout); + } + + daliBufferPlacement_t GetBufferPlacement() const override { + daliBufferPlacement_t placement; + placement.device_id = t_->device_id(); + StorageDevice dev = backend_to_storage_device::value; + placement.device_type = static_cast(dev); + placement.pinned = t_->is_pinned(); + return placement; + } + + void SetStream(std::optional stream, bool synchronize) override { + t_->set_order(stream.has_value() ? AccessOrder(*stream) : AccessOrder::host(), synchronize); + } + + void SetLayout(const char *layout_string) override { + if (layout_string) { + TensorLayout layout(layout_string); + Validate(layout, t_->ndim()); + t_->SetLayout(layout); + } else { + t_->SetLayout(""); + } + } + + const char *GetLayout() const override { + auto &layout = t_->GetLayout(); + return !layout.empty() ? layout.data() : nullptr; + } + + std::optional GetStream() const override { + auto o = t_->order(); + if (o.is_device()) + return o.stream(); + else + return std::nullopt; + } + + std::optional GetReadyEvent() const override { + auto &e = t_->ready_event(); + if (e) + return e.get(); + else + return std::nullopt; + } + + cudaEvent_t GetOrCreateReadyEvent() override { + auto &e = t_->ready_event(); + if (e) + return e.get(); + int device_id = t_->device_id(); + if (device_id < 0) + throw std::runtime_error("The tensor list is not associated with a CUDA device."); + t_->set_ready_event(CUDASharedEvent::Create(device_id)); + return t_->ready_event().get(); + } + + daliTensorDesc_t GetDesc() const override { + auto &shape = t_->shape(); + daliTensorDesc_t desc{}; + desc.ndim = shape.sample_dim(); + desc.data = t_->raw_mutable_data(); + desc.dtype = t_->type(); + desc.layout = GetLayout(); + desc.shape = shape.data(); + return desc; + } + + const TensorShape<> &GetShape() const & override { + return t_->shape(); + } + + const char *GetSourceInfo() const & override { + const char *info = t_->GetMeta().GetSourceInfo().c_str(); + if (info && !*info) + return nullptr; + return info; + } + + void SetSourceInfo(const char *source_info) override { + t_->SetSourceInfo(source_info ? source_info : ""); + } + + const auto &NativePtr() const & { + return t_; + } + + private: + std::shared_ptr> t_; +}; + +template +RefCountedPtr> Wrap(std::shared_ptr> tl) { + return RefCountedPtr>(new TensorWrapper(std::move(tl))); +} + +template +class TensorListWrapper : public ITensorList { + public: + explicit TensorListWrapper(std::shared_ptr> tl) : tl_(std::move(tl)) {} + + void Resize( + int num_samples, + int ndim, + const int64_t *shapes, + daliDataType_t dtype, + const char *layout) override { + Validate(dtype); + ValidateShape(num_samples, ndim, shapes); + if (layout) + Validate(TensorLayout(layout), ndim); + std::vector shape_data(shapes, shapes + ndim * num_samples); + tl_->Resize(TensorListShape<>(std::move(shape_data), num_samples, ndim), dtype); + if (layout) + tl_->SetLayout(layout); + } + + void AttachBuffer( + int num_samples, + int ndim, + const int64_t *shapes, + daliDataType_t dtype, + const char *layout, + void *data, + const ptrdiff_t *sample_offsets, + daliDeleter_t deleter) override { + ValidateShape(num_samples, ndim, shapes); + Validate(dtype); + + if (!data && num_samples > 0) { + for (int i = 0; i < num_samples; i++) { + auto sample_shape = make_cspan(&shapes[i*ndim], ndim); + + if (volume(sample_shape) > 0) + throw std::invalid_argument( + "The pointer to the data buffer must not be null for a non-empty tensor list."); + + if (sample_offsets && sample_offsets[i]) + throw std::invalid_argument( + "All sample_offsets must be zero when the data pointer is NULL."); + } + } + + TensorLayout new_layout = {}; + + if (!layout) { + if (ndim == tl_->sample_dim()) + new_layout = tl_->GetLayout(); + } else { + new_layout = layout; + Validate(new_layout, ndim); + } + + tl_->Reset(); + tl_->SetSize(num_samples); + tl_->set_sample_dim(ndim); + tl_->SetLayout(new_layout); + tl_->set_type(dtype); + ptrdiff_t next_offset = 0; + auto type_info = TypeTable::GetTypeInfo(dtype); + auto element_size = type_info.size(); + + std::shared_ptr buffer; + if (!deleter.delete_buffer && !deleter.destroy_context) { + buffer = std::shared_ptr(data, [](void *){}); + } else { + buffer = std::shared_ptr(data, BufferDeleter{deleter, tl_->order()}); + } + + bool is_contiguous = true; + if (sample_offsets) { + for (int i = 0; i < num_samples; i++) { + if (sample_offsets[i] != next_offset) { + is_contiguous = false; + break; + } + auto num_elements = volume(make_cspan(&shapes[i*ndim], ndim)); + next_offset += num_elements * element_size; + } + } + + if (is_contiguous) { + tl_->SetContiguity(BatchContiguity::Contiguous); + std::vector shape_data(shapes, shapes + ndim * num_samples); + TensorListShape<> tl_shape(shape_data, num_samples, ndim); + tl_->ShareData( + std::move(buffer), + next_offset, + tl_->is_pinned(), + tl_shape, + dtype, + tl_->device_id(), + tl_->order(), + new_layout); + } else { + tl_->SetContiguity(BatchContiguity::Automatic); + next_offset = 0; + + for (int i = 0; i < num_samples; i++) { + TensorShape<> sample_shape(make_cspan(&shapes[i*ndim], ndim)); + void *sample_data; + size_t sample_bytes = volume(sample_shape) * element_size; + if (sample_offsets) { + sample_data = static_cast(data) + sample_offsets[i]; + } else { + sample_data = static_cast(data) + next_offset; + next_offset += sample_bytes; + } + tl_->SetSample( + i, + std::shared_ptr(buffer, sample_data), + sample_bytes, + tl_->is_pinned(), + sample_shape, + dtype, + tl_->device_id(), + tl_->order(), + new_layout); + } + } + } + + void AttachSamples( + int num_samples, + int ndim, + daliDataType_t dtype, + const char *layout, + const daliTensorDesc_t *samples, + const daliDeleter_t *sample_deleters) override { + ValidateNumSamples(num_samples); + if (num_samples > 0 && !samples) + throw std::invalid_argument("The pointer to sample descriptors must not be NULL."); + if (ndim < 0) { + if (num_samples == 0) + throw std::invalid_argument( + "The number of dimensions must not be negative when num_samples is 0."); + else + ndim = samples[0].ndim; + ValidateNDim(ndim); + } + if (dtype == DALI_NO_TYPE) { + if (num_samples == 0) + throw std::invalid_argument( + "A valid data type must be provided when there's no sample to take it from."); + dtype = samples[0].dtype; + } + Validate(dtype); + + TensorLayout new_layout = {}; + + if (!layout) { + if (num_samples > 0) { + new_layout = samples[0].layout; + Validate(new_layout, ndim); + } else if (ndim == tl_->sample_dim()) { + new_layout = tl_->GetLayout(); + } + } else { + new_layout = layout; + Validate(new_layout, ndim); + } + + for (int i = 0; i < num_samples; i++) { + if (ndim && !samples[i].shape) + throw std::invalid_argument(make_string("Got NULL shape in sample ", i, ".")); + if (samples[i].dtype != dtype) + throw std::invalid_argument(make_string("Unexpected data type in sample ", i, ". Got: ", + samples[i].dtype, ", expected ", dtype, ".")); + ValidateSampleShape(i, make_cspan(samples[i].shape, samples[i].ndim), ndim); + if (samples[i].layout && new_layout != samples[i].layout) + throw std::invalid_argument(make_string("Unexpected layout \"", samples[i].layout, + "\" in sample ", i, ". Expected: \"", new_layout, "\".")); + + if (!samples[i].data && volume(make_cspan(samples[i].shape, ndim))) + throw std::invalid_argument(make_string( + "Got NULL data pointer in a non-empty sample ", i, ".")); + } + + tl_->Reset(); + tl_->SetSize(num_samples); + tl_->set_type(dtype); + tl_->set_sample_dim(ndim); + tl_->SetLayout(new_layout); + + auto deletion_order = tl_->order(); + + auto type_info = TypeTable::GetTypeInfo(dtype); + auto element_size = type_info.size(); + for (int i = 0; i < num_samples; i++) { + TensorShape<> sample_shape(make_cspan(samples[i].shape, samples[i].ndim)); + size_t sample_bytes = volume(sample_shape) * element_size; + std::shared_ptr sample_ptr; + if (sample_deleters) { + sample_ptr = std::shared_ptr( + samples[i].data, + BufferDeleter{sample_deleters[i], deletion_order}); + } else { + sample_ptr = std::shared_ptr(samples[i].data, [](void*) {}); + } + + tl_->SetSample( + i, + sample_ptr, + sample_bytes, + tl_->is_pinned(), + sample_shape, + dtype, + tl_->device_id(), + tl_->order(), + new_layout); + } + } + + daliBufferPlacement_t GetBufferPlacement() const override { + daliBufferPlacement_t placement; + placement.device_id = tl_->device_id(); + StorageDevice dev = backend_to_storage_device::value; + placement.device_type = static_cast(dev); + placement.pinned = tl_->is_pinned(); + return placement; + } + + void SetStream(std::optional stream, bool synchronize) override { + tl_->set_order(stream.has_value() ? AccessOrder(*stream) : AccessOrder::host(), synchronize); + } + + void SetLayout(const char *layout_string) override { + if (layout_string) { + TensorLayout layout(layout_string); + Validate(layout, tl_->sample_dim()); + tl_->SetLayout(layout); + } else { + tl_->SetLayout(""); + } + } + + const char *GetLayout() const override { + auto &layout = tl_->GetLayout(); + return !layout.empty() ? layout.data() : nullptr; + } + + std::optional GetStream() const override { + auto o = tl_->order(); + if (o.is_device()) + return o.stream(); + else + return std::nullopt; + } + + std::optional GetReadyEvent() const override { + auto &e = tl_->ready_event(); + if (e) + return e.get(); + else + return std::nullopt; + } + + cudaEvent_t GetOrCreateReadyEvent() override { + auto &e = tl_->ready_event(); + if (e) + return e.get(); + int device_id = tl_->device_id(); + if (device_id < 0) + throw std::runtime_error("The tensor list is not associated with a CUDA device."); + tl_->set_ready_event(CUDASharedEvent::Create(device_id)); + return tl_->ready_event().get(); + } + + daliTensorDesc_t GetTensorDesc(int sample) const override { + ValidateSampleIdx(sample); + daliTensorDesc_t desc{}; + auto &shape = tl_->shape(); + desc.ndim = shape.sample_dim(); + desc.data = tl_->raw_mutable_tensor(sample); + desc.dtype = tl_->type(); + desc.layout = GetLayout(); + desc.shape = shape.tensor_shape_span(sample).data(); + return desc; + } + + const TensorListShape<> &GetShape() const & override { + return tl_->shape(); + } + + const char *GetSourceInfo(int sample) const & override { + ValidateSampleIdx(sample); + const char *info = tl_->GetMeta(sample).GetSourceInfo().c_str(); + if (info && !*info) + return nullptr; // return empty string as NULL + return info; + } + + void SetSourceInfo(int sample, const char *source_info) override { + ValidateSampleIdx(sample); + tl_->SetSourceInfo(sample, source_info ? source_info : ""); + } + + RefCountedPtr ViewAsTensor() const override { + if (!tl_->IsContiguous()) + throw std::runtime_error( + "The TensorList is not contiguous and cannot be viewed as a Tensor."); + + auto t = std::make_shared>(); + auto buf = unsafe_owner(*tl_); + auto &lshape = tl_->shape(); + TensorShape<> tshape = shape_cat(lshape.num_samples(), lshape[0]); + t->ShareData( + std::move(buf), + tl_->nbytes(), + tl_->is_pinned(), + tshape, + tl_->type(), + tl_->device_id(), + tl_->order(), + tl_->ready_event()); + TensorLayout layout = tl_->GetLayout(); + if (layout.size() == lshape.sample_dim()) { + t->SetLayout("N" + layout); + } + return Wrap(std::move(t)); + } + + const auto &NativePtr() const & { + return tl_; + } + + inline void ValidateSampleIdx(int idx) const { + if (idx < 0 || idx >= tl_->num_samples()) { + std::string message = make_string("The sample index ", idx, " is out of range."); + if (tl_->num_samples() == 0) + message += " The TensorList is empty."; + else + message += make_string("Valid indices are [0..", tl_->num_samples() - 1, "]."); + throw std::out_of_range(std::move(message)); + } + } + + private: + std::shared_ptr> tl_; +}; + +template +RefCountedPtr> Wrap(std::shared_ptr> tl) { + return RefCountedPtr>(new TensorListWrapper(std::move(tl))); +} + +template +const std::shared_ptr> &ITensor::Unwrap() const & { + return dynamic_cast &>(*this).NativePtr(); +} + +template +const std::shared_ptr> &ITensorList::Unwrap() const & { + return dynamic_cast &>(*this).NativePtr(); +} + +ITensor *ToPointer(daliTensor_h handle); +ITensorList *ToPointer(daliTensorList_h handle); + +} // namespace c_api +} // namespace dali + +#endif // DALI_C_API_2_DATA_OBJECTS_H_ diff --git a/dali/c_api_2/data_objects_test.cc b/dali/c_api_2/data_objects_test.cc new file mode 100644 index 0000000000..f0eef056f6 --- /dev/null +++ b/dali/c_api_2/data_objects_test.cc @@ -0,0 +1,565 @@ +// Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Licensed 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. + +#include "dali/c_api_2/data_objects.h" +#include +#include "dali/c_api_2/managed_handle.h" +#include "dali/core/span.h" +#include "dali/core/device_guard.h" + +TEST(CAPI2_TensorListTest, NullHandle) { + daliTensorList_h h = nullptr; + int ref = 0; + EXPECT_EQ(DALI_ERROR_INVALID_HANDLE, daliTensorListIncRef(h, &ref)); + EXPECT_EQ(DALI_ERROR_INVALID_HANDLE, daliTensorListDecRef(h, &ref)); + EXPECT_EQ(DALI_ERROR_INVALID_HANDLE, daliTensorListRefCount(h, &ref)); +} + +TEST(CAPI2_TensorListTest, CreateDestroy) { + daliBufferPlacement_t placement{}; + placement.device_type = DALI_STORAGE_CPU; + placement.pinned = false; + daliTensorList_h h = nullptr; + daliResult_t r = daliTensorListCreate(&h, placement); + ASSERT_NE(h, nullptr); + dali::c_api::TensorListHandle tl(h); + ASSERT_EQ(h, tl.get()); + ASSERT_EQ(r, DALI_SUCCESS) << daliGetLastErrorMessage(); + + int ref = -1; + EXPECT_EQ(daliTensorListRefCount(h, &ref), DALI_SUCCESS) << daliGetLastErrorMessage(); + EXPECT_EQ(ref, 1); + ref = -1; + + h = tl.release(); + EXPECT_EQ(daliTensorListDecRef(h, &ref), DALI_SUCCESS) << daliGetLastErrorMessage(); + EXPECT_EQ(ref, 0); +} + +inline auto CreateTensorList(daliBufferPlacement_t placement) { + daliTensorList_h handle; + auto err = daliTensorListCreate(&handle, placement); + switch (err) { + case DALI_SUCCESS: + break; + case DALI_ERROR_OUT_OF_MEMORY: + throw std::bad_alloc(); + case DALI_ERROR_INVALID_ARGUMENT: + throw std::invalid_argument(daliGetLastErrorMessage()); + default: + throw std::runtime_error(daliGetLastErrorMessage()); + } + return dali::c_api::TensorListHandle(handle); +} + +void TestTensorListResize(daliStorageDevice_t storage_device) { + daliBufferPlacement_t placement{}; + placement.device_type = storage_device; + placement.pinned = true; + int64_t shapes[] = { + 480, 640, 3, + 600, 800, 4, + 348, 720, 1, + 1080, 1920, 3 + }; + daliDataType_t dtype = DALI_UINT32; + + auto tl = CreateTensorList(placement); + + daliBufferPlacement_t test_placement{}; + EXPECT_EQ(daliTensorListGetBufferPlacement(tl, &test_placement), DALI_SUCCESS); + EXPECT_EQ(test_placement.device_type, placement.device_type); + EXPECT_EQ(test_placement.device_id, placement.device_id); + EXPECT_EQ(test_placement.pinned, placement.pinned); + + EXPECT_EQ(daliTensorListResize(tl, 4, 3, nullptr, dtype, nullptr), DALI_ERROR_INVALID_ARGUMENT); + EXPECT_EQ(daliTensorListResize(tl, -1, 3, shapes, dtype, nullptr), DALI_ERROR_INVALID_ARGUMENT); + EXPECT_EQ(daliTensorListResize(tl, 4, -1, shapes, dtype, nullptr), DALI_ERROR_INVALID_ARGUMENT); + EXPECT_EQ(daliTensorListResize(tl, 4, 3, shapes, dtype, "ABCD"), DALI_ERROR_INVALID_ARGUMENT); + shapes[0] = -1; + EXPECT_EQ(daliTensorListResize(tl, 4, 3, shapes, dtype, "HWC"), DALI_ERROR_INVALID_ARGUMENT); + shapes[0] = 480; + EXPECT_EQ(daliTensorListResize(tl, 1, 3, shapes, dtype, "HWC"), DALI_SUCCESS) + << daliGetLastErrorMessage(); + // resize, but keep the layout + EXPECT_EQ(daliTensorListResize(tl, 4, 3, shapes, dtype, nullptr), DALI_SUCCESS) + << daliGetLastErrorMessage(); + + size_t element_size = dali::TypeTable::GetTypeInfo(dtype).size(); + + EXPECT_EQ(daliTensorListGetShape(tl, nullptr, nullptr, nullptr), DALI_SUCCESS) + << daliGetLastErrorMessage(); + { + int nsamples = -1, ndim = -1; + const int64_t *shape_data = nullptr; + EXPECT_EQ(daliTensorListGetShape(tl, &nsamples, &ndim, &shape_data), DALI_SUCCESS) + << daliGetLastErrorMessage(); + ASSERT_NE(shape_data, nullptr); + EXPECT_EQ(nsamples, 4); + EXPECT_EQ(ndim, 3); + for (int i = 0, k = 0; i < 4; i++) + for (int d = 0; d < 3; d++, k++) { + EXPECT_EQ(shapes[k], shape_data[k]) << " @ sample " << i << " dim " << d; + } + } + + ptrdiff_t offset = 0; + const char *base; + for (int i = 0; i < 4; i++) { + daliTensorDesc_t desc{}; + EXPECT_EQ(daliTensorListGetTensorDesc(tl, &desc, i), DALI_SUCCESS) << daliGetLastErrorMessage(); + ASSERT_EQ(desc.ndim, 3); + if (i == 0) + base = static_cast(desc.data); + EXPECT_EQ(desc.data, base + offset); + EXPECT_EQ(desc.dtype, dtype); + ASSERT_NE(desc.shape, nullptr); + for (int j = 0; j < 3; j++) + EXPECT_EQ(desc.shape[j], shapes[3 * i + j]); + size_t sample_bytes = volume(dali::make_cspan(desc.shape, desc.ndim)) * element_size; + if (storage_device == DALI_STORAGE_GPU) { + // Check that the data is accessible for the GPU + EXPECT_EQ(cudaMemset(desc.data, 0, sample_bytes), cudaSuccess); + } else { + // Check that the data is accessible for the CPU + memset(desc.data, 0, sample_bytes); // just not crashing is OK + } + offset += sample_bytes; + } + if (storage_device == DALI_STORAGE_GPU) { + EXPECT_EQ(cudaDeviceSynchronize(), cudaSuccess); + } +} + +struct TestDeleterCtx { + void *expected_data; + int buffer_delete_count; + int context_delete_count; +}; + +template +inline std::pair> +MakeTestDeleter(element_t *expected_data) { + auto ctx = std::unique_ptr(new TestDeleterCtx{ expected_data, 0, 0 }); + daliDeleter_t deleter = {}; + deleter.deleter_ctx = ctx.get(); + deleter.delete_buffer = [](void *vctx, void *data, const cudaStream_t *stream) { + ASSERT_NE(data, nullptr); + auto *ctx = static_cast(vctx); + EXPECT_EQ(ctx->context_delete_count, 0); + EXPECT_EQ(ctx->buffer_delete_count, 0); + EXPECT_EQ(data, ctx->expected_data); + ctx->buffer_delete_count++; + // do not actually delete the data + }; + deleter.destroy_context = [](void *vctx) { + auto *ctx = static_cast(vctx); + EXPECT_EQ(ctx->context_delete_count, 0); + EXPECT_EQ(ctx->buffer_delete_count, 1); + ctx->context_delete_count++; + }; + return { deleter, std::move(ctx) }; +} + +TEST(CAPI2_TensorListTest, AttachBuffer) { + daliBufferPlacement_t placement{}; + placement.device_type = DALI_STORAGE_CPU; + using element_t = int; + daliDataType_t dtype = dali::type2id::value; + dali::TensorListShape<> lshape({ + { 480, 640, 3 }, + { 600, 800, 4 }, + { 348, 720, 1 }, + { 1080, 1920, 3 } + }); + auto size = lshape.num_elements(); + std::unique_ptr data(new element_t[size]); + + ptrdiff_t offsets[4] = {}; + for (int i = 1; i < 4; i++) + offsets[i] = offsets[i - 1] + volume(lshape[i - 1]) * sizeof(element_t); + + auto [deleter, ctx] = MakeTestDeleter(data.get()); + + auto tl = CreateTensorList(placement); + ASSERT_EQ(daliTensorListAttachBuffer( + tl, + lshape.num_samples(), + lshape.sample_dim(), + lshape.data(), + dtype, + "HWC", + data.get(), + offsets, + deleter), DALI_SUCCESS) << daliGetLastErrorMessage(); + + // The deleter doesn't actually delete - we still own the data. + + ptrdiff_t offset = 0; + const char *base = reinterpret_cast(data.get()); + for (int i = 0; i < 4; i++) { + daliTensorDesc_t desc{}; + EXPECT_EQ(daliTensorListGetTensorDesc(tl, &desc, i), DALI_SUCCESS) << daliGetLastErrorMessage(); + ASSERT_EQ(desc.ndim, 3); + EXPECT_EQ(desc.data, base + offset); + EXPECT_EQ(desc.dtype, dtype); + ASSERT_NE(desc.shape, nullptr); + for (int j = 0; j < 3; j++) + EXPECT_EQ(desc.shape[j], lshape[i][j]); + EXPECT_STREQ(desc.layout, "HWC"); + size_t sample_bytes = volume(dali::make_cspan(desc.shape, desc.ndim)) * sizeof(element_t); + offset += sample_bytes; + } + + tl.reset(); + + EXPECT_EQ(ctx->buffer_delete_count, 1) << "Buffer deleter not called"; + EXPECT_EQ(ctx->context_delete_count, 1) << "Deleter context not destroyed"; +} + + +TEST(CAPI2_TensorListTest, AttachSamples) { + daliBufferPlacement_t placement{}; + placement.device_type = DALI_STORAGE_CPU; + using element_t = int; + daliDataType_t dtype = dali::type2id::value; + dali::TensorListShape<> lshape({ + { 480, 640, 3 }, + { 600, 800, 4 }, + { 348, 720, 1 }, + { 1080, 1920, 3 } + }); + auto size = lshape.num_elements(); + int N = lshape.num_samples(); + std::vector> data(N); + + for (int i = 0; i < N; i++) { + data[i].reset(new element_t[size]); + } + + std::vector deleters(N); + std::vector> deleter_ctxs(N); + + for (int i = 0; i < N; i++) { + std::tie(deleters[i], deleter_ctxs[i]) = MakeTestDeleter(data[i].get()); + } + + std::vector samples(N); + + for (int i = 0; i < N; i++) { + samples[i].ndim = lshape.sample_dim(); + samples[i].dtype = dtype; + samples[i].layout = i == 0 ? "HWC" : nullptr; + samples[i].shape = lshape.tensor_shape_span(i).data(); + samples[i].data = data[i].get(); + } + + auto tl = CreateTensorList(placement); + ASSERT_EQ(daliTensorListAttachSamples( + tl, + lshape.num_samples(), + -1, + DALI_NO_TYPE, + nullptr, + samples.data(), + deleters.data()), DALI_SUCCESS) << daliGetLastErrorMessage(); + + // The deleter doesn't actually delete - we still own the data. + for (int i = 0; i < 4; i++) { + daliTensorDesc_t desc{}; + EXPECT_EQ(daliTensorListGetTensorDesc(tl, &desc, i), DALI_SUCCESS) << daliGetLastErrorMessage(); + ASSERT_EQ(desc.ndim, 3); + EXPECT_EQ(desc.data, data[i].get()); + EXPECT_EQ(desc.dtype, dtype); + ASSERT_NE(desc.shape, nullptr); + for (int j = 0; j < 3; j++) + EXPECT_EQ(desc.shape[j], lshape[i][j]); + EXPECT_STREQ(desc.layout, "HWC"); + } + + tl.reset(); + + for (auto &ctx : deleter_ctxs) { + EXPECT_EQ(ctx->buffer_delete_count, 1) << "Buffer deleter not called"; + EXPECT_EQ(ctx->context_delete_count, 1) << "Deleter context not destroyed"; + } +} + + +TEST(CAPI2_TensorListTest, ViewAsTensor) { + int num_dev = 0; + CUDA_CALL(cudaGetDeviceCount(&num_dev)); + // use the last device + dali::DeviceGuard dg(num_dev - 1); + + daliBufferPlacement_t placement{}; + placement.device_type = DALI_STORAGE_CPU; + placement.pinned = true; + using element_t = int; + daliDataType_t dtype = dali::type2id::value; + dali::TensorListShape<> lshape = dali::uniform_list_shape(4, { 480, 640, 3 }); + auto size = lshape.num_elements(); + std::unique_ptr data(new element_t[size]); + + ptrdiff_t sample_size = volume(lshape[0]) * sizeof(element_t); + + ptrdiff_t offsets[4] = { + 0, + 1 * sample_size, + 2 * sample_size, + 3 * sample_size, + }; + + auto [deleter, ctx] = MakeTestDeleter(data.get()); + + auto tl = CreateTensorList(placement); + ASSERT_EQ(daliTensorListAttachBuffer( + tl, + lshape.num_samples(), + lshape.sample_dim(), + lshape.data(), + dtype, + "HWC", + data.get(), + offsets, + deleter), DALI_SUCCESS) << daliGetLastErrorMessage(); + + // The deleter doesn't actually delete - we still own the data. + + daliTensor_h ht = nullptr; + EXPECT_EQ(daliTensorListViewAsTensor(tl, &ht), DALI_SUCCESS) << daliGetLastErrorMessage(); + ASSERT_NE(ht, nullptr); + dali::c_api::TensorHandle t(ht); + + daliBufferPlacement_t tensor_placement{}; + EXPECT_EQ(daliTensorGetBufferPlacement(ht, &tensor_placement), DALI_SUCCESS); + EXPECT_EQ(tensor_placement.device_type, placement.device_type); + EXPECT_EQ(tensor_placement.device_id, placement.device_id); + EXPECT_EQ(tensor_placement.pinned, placement.pinned); + + daliTensorDesc_t desc{}; + EXPECT_EQ(daliTensorGetDesc(t, &desc), DALI_SUCCESS) << daliGetLastErrorMessage(); + EXPECT_EQ(desc.data, data.get()); + EXPECT_EQ(desc.shape[0], lshape.num_samples()); + ASSERT_EQ(desc.ndim, 4); + ASSERT_NE(desc.shape, nullptr); + EXPECT_EQ(desc.shape[1], lshape[0][0]); + EXPECT_EQ(desc.shape[2], lshape[0][1]); + EXPECT_EQ(desc.shape[3], lshape[0][2]); + EXPECT_STREQ(desc.layout, "NHWC"); + EXPECT_EQ(desc.dtype, dtype); + EXPECT_EQ(daliTensorGetShape(t, nullptr, nullptr), DALI_SUCCESS) << daliGetLastErrorMessage(); + int ndim = -1; + const int64_t *shape = nullptr; + EXPECT_EQ(daliTensorGetShape(t, &ndim, &shape), DALI_SUCCESS) << daliGetLastErrorMessage(); + EXPECT_EQ(ndim, 4); + EXPECT_EQ(shape, desc.shape); + + tl.reset(); + + EXPECT_EQ(ctx->buffer_delete_count, 0) << "Buffer prematurely destroyed"; + EXPECT_EQ(ctx->context_delete_count, 0) << "Deleter context prematurely destroyed"; + + t.reset(); + + EXPECT_EQ(ctx->buffer_delete_count, 1) << "Buffer deleter not called"; + EXPECT_EQ(ctx->context_delete_count, 1) << "Deleter context not destroyed"; +} + + +TEST(CAPI2_TensorListTest, ViewAsTensorError) { + daliBufferPlacement_t placement{}; + placement.device_type = DALI_STORAGE_CPU; + using element_t = int; + daliDataType_t dtype = dali::type2id::value; + dali::TensorListShape<> lshape = dali::uniform_list_shape(4, { 480, 640, 3 }); + auto size = lshape.num_elements(); + std::unique_ptr data(new element_t[size]); + + ptrdiff_t sample_size = volume(lshape[0]) * sizeof(element_t); + + // The samples are not in order + ptrdiff_t offsets[4] = { + 0, + 2 * sample_size, + 1 * sample_size, + 3 * sample_size, + }; + + auto [deleter, ctx] = MakeTestDeleter(data.get()); + + auto tl = CreateTensorList(placement); + ASSERT_EQ(daliTensorListAttachBuffer( + tl, + lshape.num_samples(), + lshape.sample_dim(), + lshape.data(), + dtype, + "HWC", + data.get(), + offsets, + deleter), DALI_SUCCESS) << daliGetLastErrorMessage(); + + // The deleter doesn't actually delete - we still own the data. + + daliTensor_h ht = nullptr; + EXPECT_EQ(daliTensorListViewAsTensor(tl, &ht), DALI_ERROR_INVALID_OPERATION); +} + + +TEST(CAPI2_TensorListTest, ResizeCPU) { + TestTensorListResize(DALI_STORAGE_CPU); +} + +TEST(CAPI2_TensorListTest, ResizeGPU) { + TestTensorListResize(DALI_STORAGE_GPU); +} + + + + +TEST(CAPI2_TensorTest, NullHandle) { + daliTensor_h h = nullptr; + int ref = 0; + EXPECT_EQ(DALI_ERROR_INVALID_HANDLE, daliTensorIncRef(h, &ref)); + EXPECT_EQ(DALI_ERROR_INVALID_HANDLE, daliTensorDecRef(h, &ref)); + EXPECT_EQ(DALI_ERROR_INVALID_HANDLE, daliTensorRefCount(h, &ref)); +} + +TEST(CAPI2_TensorTest, CreateDestroy) { + daliBufferPlacement_t placement{}; + placement.device_type = DALI_STORAGE_CPU; + placement.pinned = false; + daliTensor_h h = nullptr; + daliResult_t r = daliTensorCreate(&h, placement); + ASSERT_NE(h, nullptr); + dali::c_api::TensorHandle tl(h); + ASSERT_EQ(h, tl.get()); + ASSERT_EQ(r, DALI_SUCCESS) << daliGetLastErrorMessage(); + + int ref = -1; + EXPECT_EQ(daliTensorRefCount(h, &ref), DALI_SUCCESS) << daliGetLastErrorMessage(); + EXPECT_EQ(ref, 1); + ref = -1; + + h = tl.release(); + EXPECT_EQ(daliTensorDecRef(h, &ref), DALI_SUCCESS) << daliGetLastErrorMessage(); + EXPECT_EQ(ref, 0); +} + + +inline auto CreateTensor(daliBufferPlacement_t placement) { + daliTensor_h handle; + auto err = daliTensorCreate(&handle, placement); + switch (err) { + case DALI_SUCCESS: + break; + case DALI_ERROR_OUT_OF_MEMORY: + throw std::bad_alloc(); + case DALI_ERROR_INVALID_ARGUMENT: + throw std::invalid_argument(daliGetLastErrorMessage()); + default: + throw std::runtime_error(daliGetLastErrorMessage()); + } + return dali::c_api::TensorHandle(handle); +} + +void TestTensorResize(daliStorageDevice_t storage_device) { + daliBufferPlacement_t placement{}; + placement.device_type = storage_device; + auto t = CreateTensor(placement); + int64_t shape[] = { + 1080, 1920, 3 + }; + daliDataType_t dtype = DALI_INT16; + + EXPECT_EQ(daliTensorResize(t, 3, nullptr, dtype, nullptr), DALI_ERROR_INVALID_ARGUMENT); + EXPECT_EQ(daliTensorResize(t, -1, shape, dtype, nullptr), DALI_ERROR_INVALID_ARGUMENT); + EXPECT_EQ(daliTensorResize(t, 3, shape, dtype, "ABCD"), DALI_ERROR_INVALID_ARGUMENT); + shape[0] = -1; + EXPECT_EQ(daliTensorResize(t, 3, shape, dtype, "HWC"), DALI_ERROR_INVALID_ARGUMENT); + shape[0] = 1; + EXPECT_EQ(daliTensorResize(t, 3, shape, dtype, "HWC"), DALI_SUCCESS) + << daliGetLastErrorMessage(); + + shape[0] = 1080; + EXPECT_EQ(daliTensorResize(t, 3, shape, dtype, nullptr), DALI_SUCCESS) + << daliGetLastErrorMessage(); + + size_t element_size = dali::TypeTable::GetTypeInfo(dtype).size(); + + ptrdiff_t offset = 0; + daliTensorDesc_t desc{}; + EXPECT_EQ(daliTensorGetDesc(t, &desc), DALI_SUCCESS) << daliGetLastErrorMessage(); + ASSERT_EQ(desc.ndim, 3); + EXPECT_STREQ(desc.layout, "HWC"); + EXPECT_EQ(desc.dtype, dtype); + ASSERT_NE(desc.shape, nullptr); + for (int j = 0; j < 3; j++) + EXPECT_EQ(desc.shape[j], shape[j]); + size_t sample_bytes = volume(dali::make_cspan(desc.shape, desc.ndim)) * element_size; + if (storage_device == DALI_STORAGE_GPU) { + // Check that the data is accessible for the GPU + EXPECT_EQ(cudaMemset(desc.data, 0, sample_bytes), cudaSuccess); + } else { + // Check that the data is accessible for the CPU + memset(desc.data, 0, sample_bytes); // just not crashing is OK + } + if (storage_device == DALI_STORAGE_GPU) { + EXPECT_EQ(cudaDeviceSynchronize(), cudaSuccess); + } +} + +TEST(CAPI2_TensorTest, ResizeCPU) { + TestTensorResize(DALI_STORAGE_CPU); +} + +TEST(CAPI2_TensorTest, ResizeGPU) { + TestTensorResize(DALI_STORAGE_GPU); +} + +TEST(CAPI2_TensorTest, SourceInfo) { + auto t = CreateTensor({}); + const char *out_src_info = "junk"; + EXPECT_EQ(daliTensorGetSourceInfo(t, &out_src_info), DALI_SUCCESS); + EXPECT_EQ(out_src_info, nullptr); + + EXPECT_EQ(daliTensorSetSourceInfo(t, "source_info"), DALI_SUCCESS); + EXPECT_EQ(daliTensorGetSourceInfo(t, &out_src_info), DALI_SUCCESS); + EXPECT_STREQ(out_src_info, "source_info"); +} + +TEST(CAPI2_TensorListTest, SourceInfo) { + auto t = CreateTensorList({}); + ASSERT_EQ(daliTensorListResize(t, 5, 0, nullptr, DALI_UINT8, nullptr), DALI_SUCCESS); + + const char *out_src_info = "junk"; + EXPECT_EQ(daliTensorListGetSourceInfo(t, &out_src_info, 0), DALI_SUCCESS); + EXPECT_EQ(out_src_info, nullptr); + + EXPECT_EQ(daliTensorListSetSourceInfo(t, 0, "quick"), DALI_SUCCESS); + EXPECT_EQ(daliTensorListSetSourceInfo(t, 2, "brown"), DALI_SUCCESS); + EXPECT_EQ(daliTensorListSetSourceInfo(t, 4, "fox"), DALI_SUCCESS); + + EXPECT_EQ(daliTensorListGetSourceInfo(t, &out_src_info, 0), DALI_SUCCESS); + EXPECT_STREQ(out_src_info, "quick"); + EXPECT_EQ(daliTensorListGetSourceInfo(t, &out_src_info, 1), DALI_SUCCESS); + EXPECT_EQ(out_src_info, nullptr); + EXPECT_EQ(daliTensorListGetSourceInfo(t, &out_src_info, 2), DALI_SUCCESS); + EXPECT_STREQ(out_src_info, "brown"); + EXPECT_EQ(daliTensorListGetSourceInfo(t, &out_src_info, 3), DALI_SUCCESS); + EXPECT_EQ(out_src_info, nullptr); + EXPECT_EQ(daliTensorListGetSourceInfo(t, &out_src_info, 4), DALI_SUCCESS); + EXPECT_STREQ(out_src_info, "fox"); +} diff --git a/dali/c_api_2/ref_counting.h b/dali/c_api_2/ref_counting.h index d4a161be24..54fb799a3d 100644 --- a/dali/c_api_2/ref_counting.h +++ b/dali/c_api_2/ref_counting.h @@ -75,7 +75,8 @@ class RefCountedPtr { return *this; if (other.ptr_) other.ptr_->IncRef(); - ptr_->DecRef(); + if (ptr_) + ptr_->DecRef(); ptr_ = other.ptr_; return *this; } diff --git a/dali/c_api_2/utils.h b/dali/c_api_2/utils.h new file mode 100644 index 0000000000..7c64bc9281 --- /dev/null +++ b/dali/c_api_2/utils.h @@ -0,0 +1,34 @@ +// Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// Licensed 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. + +#ifndef DALI_C_API_2_UTILS_H_ +#define DALI_C_API_2_UTILS_H_ + +#include +#include +#include + +namespace dali::c_api { + +template +std::optional ToOptional(const T *nullable) { + if (nullable == nullptr) + return std::nullopt; + else + return *nullable; +} + +} // namespace dali::c_api + +#endif // DALI_C_API_2_UTILS_H_ diff --git a/dali/c_api_2/validation.h b/dali/c_api_2/validation.h index 62c9f3425a..45b7dcf2b0 100644 --- a/dali/c_api_2/validation.h +++ b/dali/c_api_2/validation.h @@ -15,8 +15,9 @@ #ifndef DALI_C_API_2_VALIDATION_H_ #define DALI_C_API_2_VALIDATION_H_ -#include #include +#include +#include #define DALI_ALLOW_NEW_C_API #include "dali/dali.h" #include "dali/core/format.h" @@ -107,6 +108,23 @@ inline void Validate(const daliBufferPlacement_t &placement) { ValidateDeviceId(placement.device_id, placement.pinned); } +inline void CheckArg(bool assertion, const std::string &what) { + if (!assertion) + throw std::invalid_argument(what); +} + +template +void CheckNotNull(T *x, std::string_view what) { + CheckArg(x != nullptr, make_string(what, " must not be NULL.")); +} + +#define CHECK_OUTPUT(output_param) \ + ::dali::c_api::CheckNotNull(output_param, "The output parameter `" #output_param "`"); + +#define NOT_NULL(param) \ + ::dali::c_api::CheckNotNull(param, "The parameter `" #param "`"); + + } // namespace dali::c_api #endif // DALI_C_API_2_VALIDATION_H_ diff --git a/include/dali/dali.h b/include/dali/dali.h index 3a431e9c84..a6017bf901 100644 --- a/include/dali/dali.h +++ b/include/dali/dali.h @@ -723,12 +723,13 @@ DALI_API daliResult_t daliTensorListAttachBuffer( * @param dtype the type of the element of the tensor; * if dtype is DALI_NO_TYPE, then the type is taken from samples[0].dtype * @param layout a layout string describing the order of axes in each sample (e.g. HWC), - * if NULL, and the TensorList's number of dimensions is equal to `ndim`, - * then the current layout is kept; + * if NULL, the layout is taken from samples[0].layout; if it's still NULL, + * the current layout is kept, if possible; * if `layout` is an empty string, the tensor list's layout is cleared * @param samples the descriptors of the tensors to be attached to the TensorList; * the `ndim` and `dtype` of the samples must match and they must match the - * values of `ndim` and `dtype` parameters. + * values of `ndim` and `dtype` parameters; the layout must be either NULL + * or match the `layout` argument (if provided). * @param sample_deleters optional deleters, one for each sample * * NOTE: If the sample_deleters specify the same object multiple times, its destructor must @@ -873,6 +874,22 @@ DALI_API daliResult_t daliTensorListGetSourceInfo( const char **out_source_info, int sample_idx); +/** Sets the "source info" metadata of a tensor in a list. + * + * A tensor can be associated with a "source info" string, which typically is the file name, + * but can also contain an index in a container, key, etc. + * + * @param tensor_list [in] The tensor list + * @param sample_idx [in] The index of the sample, whose source info will is being set. + * @param source_info [in] A source info string (e.g. filename) to associate with the tensor. + * Passing NULL is equivalent to passing an empty string. + */ +DALI_API daliResult_t daliTensorListSetSourceInfo( + daliTensorList_h tensor_list, + int sample_idx, + const char *source_info); + + /** Gets the tensor descriptor of the specified sample. * * @param tensor_list [in] The tensor list @@ -1106,6 +1123,19 @@ DALI_API daliResult_t daliTensorGetSourceInfo( daliTensor_h tensor, const char **out_source_info); +/** Sets the "source info" metadata of a tensor. + * + * A tensor can be associated with a "source info" string, which typically is the file name, + * but can also contain an index in a container, key, etc. + * + * @param tensor [in] The tensor + * @param source_info [in] A source info string (e.g. filename) to associate with the tensor. + * Passing NULL is equivalent to passing an empty string. + */ +DALI_API daliResult_t daliTensorSetSourceInfo( + daliTensor_h tensor, + const char *source_info); + /** Gets the descriptor of the data in the tensor. * * @param tensor [in] The tensor