// Copyright (c) 2025 PaddlePaddle Authors. 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 #include #include #include #include #include #include #include #include #include #if defined(PADDLE_WITH_CUDA) || defined(PADDLE_WITH_HIP) #include #include #include "paddle/phi/backends/gpu/gpu_info.h" #endif #include "ATen/ATen.h" #include "gtest/gtest.h" #include "paddle/phi/common/float16.h" #include "torch/all.h" namespace { void DeleteCharArray(void* p) { delete[] static_cast(p); } void DeleteIntPtr(void* p) { delete static_cast(p); } class RawCompatibleAllocator final : public c10::Allocator { public: c10::DataPtr allocate(size_t n) override { size_t bytes = n == 0 ? 1 : n; char* p = new char[bytes]; return c10::DataPtr( p, p, &DeleteCharArray, c10::Device(c10::DeviceType::CPU)); } void copy_data(void* dest, const void* src, std::size_t count) const override { default_copy_data(dest, src, count); } c10::DeleterFnPtr raw_deleter() const override { return &DeleteCharArray; } }; class RawIncompatibleAllocator final : public c10::Allocator { public: c10::DataPtr allocate(size_t /*n*/) override { int* ctx = new int(7); void* data = reinterpret_cast(reinterpret_cast(ctx) + 1); return c10::DataPtr( data, ctx, &DeleteIntPtr, c10::Device(c10::DeviceType::CPU)); } void copy_data(void* dest, const void* src, std::size_t count) const override { default_copy_data(dest, src, count); } c10::DeleterFnPtr raw_deleter() const override { return &DeleteIntPtr; } }; class NullRawDeleterAllocator final : public c10::Allocator { public: c10::DataPtr allocate(size_t n) override { size_t bytes = n == 0 ? 1 : n; char* p = new char[bytes]; return c10::DataPtr( p, p, &DeleteCharArray, c10::Device(c10::DeviceType::CPU)); } void copy_data(void* dest, const void* src, std::size_t count) const override { default_copy_data(dest, src, count); } c10::DeleterFnPtr raw_deleter() const override { return nullptr; } }; class DefaultRawDeleterAllocator final : public c10::Allocator { public: c10::DataPtr allocate(size_t n) override { size_t bytes = n == 0 ? 1 : n; char* p = new char[bytes]; return c10::DataPtr( p, p, &DeleteCharArray, c10::Device(c10::DeviceType::CPU)); } void copy_data(void* dest, const void* src, std::size_t count) const override { default_copy_data(dest, src, count); } }; } // namespace // Regression test (RT-2): tensor.storage() must count the tensor's own // StorageImpl ownership in use_count(), matching PyTorch where TensorImpl // holds a Storage handle that participates in use_count. TEST(StorageTest, StorageUseCountIncludesTensorRef) { at::TensorBase tensor = at::ones({2, 3}, at::kFloat); c10::Storage storage = tensor.storage(); // tensor.storage_ contributes 1, `storage` contributes 1. ASSERT_EQ(storage.use_count(), 2) << "use_count() must include the tensor's own StorageImpl reference"; ASSERT_FALSE(storage.unique()) << "unique() must be false because tensor also holds a reference"; } // Regression test (RT-3): additional TensorBase wrappers that reference the // same underlying TensorImpl must not increase Storage owner count. TEST(StorageTest, StorageUseCountInvariantAcrossIndependentWrappers) { at::TensorBase tensor = at::ones({2, 3}, at::kFloat); size_t baseline_count = 0; { c10::Storage baseline = tensor.storage(); baseline_count = baseline.use_count(); } // Build a wrapper from the same Paddle tensor impl through a fresh // TensorBase constructor path (not TensorBase copy ctor). at::TensorBase independent_wrapper(tensor._PD_GetInner()); c10::Storage current = tensor.storage(); ASSERT_EQ(current.use_count(), baseline_count) << "creating an independent wrapper around the same TensorImpl must not " "change storage use_count()"; } // Regression test (RT-4): storage pointer mutation through one wrapper must // persist for other wrappers that share the same TensorImpl, even after the // mutating wrapper and temporary handles are destroyed. TEST(StorageTest, StorageMutationPersistsAcrossWrappersAfterDestruction) { at::TensorBase tensor = at::ones({4}, at::kFloat); at::TensorBase peer_wrapper(tensor._PD_GetInner()); void* new_ptr = nullptr; { at::TensorBase mutating_wrapper(tensor._PD_GetInner()); c10::Storage storage = mutating_wrapper.storage(); RawCompatibleAllocator allocator; c10::DataPtr new_data = allocator.allocate(storage.nbytes()); new_ptr = new_data.get(); storage.set_data_ptr_noswap(std::move(new_data)); } ASSERT_EQ(peer_wrapper.data_ptr(), new_ptr) << "peer wrapper must observe updated storage pointer after mutating " "wrapper is destroyed"; c10::Storage peer_storage = peer_wrapper.storage(); ASSERT_EQ(peer_storage.data(), new_ptr) << "storage() from peer wrapper must keep the updated pointer"; } TEST(StorageTest, StorageOffsetAPI) { // Test storage_offset() API at::TensorBase tensor = at::ones({2, 3}, at::kFloat); // Test storage_offset() - should always return 0 for PaddlePaddle ASSERT_EQ(tensor.storage_offset(), 0); // Test sym_storage_offset() - should always return SymInt(0) for PaddlePaddle c10::SymInt sym_offset = tensor.sym_storage_offset(); ASSERT_EQ(sym_offset, c10::SymInt(0)); } TEST(StorageTest, IsAliasOfAPI) { // Test is_alias_of() API at::TensorBase tensor1 = at::ones({2, 3}, at::kFloat); at::TensorBase tensor2 = tensor1; // Shared storage, should be alias at::TensorBase tensor3 = at::ones({2, 3}, at::kFloat); // Different storage // Test that tensor1 and tensor2 are aliases (share same storage) ASSERT_TRUE(tensor1.is_alias_of(tensor2)); ASSERT_TRUE(tensor2.is_alias_of(tensor1)); // Test that tensor1 and tensor3 are not aliases (different storage) ASSERT_FALSE(tensor1.is_alias_of(tensor3)); ASSERT_FALSE(tensor3.is_alias_of(tensor1)); // Test that tensor is alias of itself ASSERT_TRUE(tensor1.is_alias_of(tensor1)); } TEST(StorageTest, BoolConversionOperator) { // Test operator bool() for Storage at::TensorBase tensor = at::ones({2, 3}, at::kFloat); const c10::Storage& storage = tensor.storage(); // Valid storage should convert to true ASSERT_TRUE(static_cast(storage)); // Default constructed storage should convert to false c10::Storage empty_storage; ASSERT_FALSE(static_cast(empty_storage)); ASSERT_FALSE(empty_storage.valid()); } TEST(StorageTest, ResizableAPI) { // Test resizable() API at::TensorBase tensor = at::ones({2, 3}, at::kFloat); const c10::Storage& storage = tensor.storage(); // Default storage from tensor should not be resizable ASSERT_FALSE(storage.resizable()); } TEST(StorageTest, DeviceAndDeviceTypeAPIs) { // Test device() and device_type() APIs at::TensorBase cpu_tensor = at::ones({2, 3}, at::kFloat); const c10::Storage& cpu_storage = cpu_tensor.storage(); // Test device_type() returns CPU ASSERT_EQ(cpu_storage.device_type(), phi::AllocationType::CPU); // Test device() returns valid place phi::Place place = cpu_storage.device(); ASSERT_EQ(place.GetType(), phi::AllocationType::CPU); #if defined(PADDLE_WITH_CUDA) || defined(PADDLE_WITH_HIP) if (at::cuda::is_available()) { at::TensorBase cuda_tensor = at::ones( {2, 3}, c10::TensorOptions().dtype(at::kFloat).device(at::kCUDA)); const c10::Storage& cuda_storage = cuda_tensor.storage(); // Test device_type() returns CUDA/GPU ASSERT_EQ(cuda_storage.device_type(), phi::AllocationType::GPU); // Test device() returns CUDA place phi::Place cuda_place = cuda_storage.device(); ASSERT_EQ(cuda_place.GetType(), phi::AllocationType::GPU); } #endif } TEST(StorageTest, DeviceAndAliasFallbackBranches) { c10::Storage empty; EXPECT_EQ(empty.device_type(), phi::AllocationType::CPU); EXPECT_EQ(empty.device().GetType(), phi::AllocationType::UNDEFINED); EXPECT_EQ(empty.use_count(), static_cast(0)); auto base = at::ones({2, 2}, at::kFloat); c10::Storage s0 = base.storage(); c10::Storage s1 = base.storage(); EXPECT_TRUE(s0.is_alias_of(s1)); auto holder = s0.ensureTensorHolder(); (void)holder; EXPECT_GE(s0.use_count(), static_cast(1)); } TEST(StorageTest, AllocatorAPI) { // Test allocator() API at::TensorBase tensor = at::ones({2, 3}, at::kFloat); const c10::Storage& storage = tensor.storage(); // Allocator may be nullptr for storage obtained from tensor // This is expected behavior in the compatibility layer phi::Allocator* allocator = storage.allocator(); // Note: allocator can be nullptr, this is just to verify the API works (void)allocator; } TEST(StorageTest, StorageCopyAndMove) { // Test copy and move constructors/operators at::TensorBase tensor = at::ones({2, 3}, at::kFloat); c10::Storage original = tensor.storage(); // Test copy constructor c10::Storage copied(original); ASSERT_EQ(copied.allocation(), original.allocation()); ASSERT_EQ(copied.nbytes(), original.nbytes()); ASSERT_TRUE(copied.valid()); // Test copy assignment c10::Storage copy_assigned; copy_assigned = original; ASSERT_EQ(copy_assigned.allocation(), original.allocation()); // Test move constructor c10::Storage to_move = original; auto alloc_before_move = to_move.allocation(); c10::Storage moved(std::move(to_move)); ASSERT_EQ(moved.allocation(), alloc_before_move); ASSERT_TRUE(moved.valid()); // Test move assignment c10::Storage to_move2 = original; alloc_before_move = to_move2.allocation(); c10::Storage move_assigned; move_assigned = std::move(to_move2); ASSERT_EQ(move_assigned.allocation(), alloc_before_move); } TEST(StorageTest, DefaultConstructedStorage) { // Test default constructed storage c10::Storage storage; ASSERT_FALSE(storage.valid()); ASSERT_FALSE(static_cast(storage)); ASSERT_EQ(storage.nbytes(), 0); ASSERT_EQ(storage.data(), nullptr); ASSERT_EQ(storage.mutable_data(), nullptr); ASSERT_EQ(storage.allocation(), nullptr); ASSERT_EQ(storage.use_count(), 0); ASSERT_FALSE(storage.resizable()); ASSERT_EQ(storage.allocator(), nullptr); } TEST(StorageTest, IsSharedStorageAliasFunction) { int* ctx = new int(21); c10::DataPtr ptr(ctx, ctx, &DeleteIntPtr, c10::Device(c10::DeviceType::CPU)); c10::Storage storage1( c10::Storage::use_byte_size_t{}, sizeof(int), std::move(ptr), nullptr); c10::Storage storage2 = storage1; c10::Storage storage3; ASSERT_TRUE(c10::isSharedStorageAlias(storage1, storage2)); ASSERT_TRUE(c10::isSharedStorageAlias(storage2, storage1)); ASSERT_FALSE(c10::isSharedStorageAlias(storage1, storage3)); c10::Storage empty_storage; ASSERT_FALSE(c10::isSharedStorageAlias(storage1, empty_storage)); ASSERT_FALSE(c10::isSharedStorageAlias(empty_storage, storage1)); ASSERT_FALSE(c10::isSharedStorageAlias(empty_storage, empty_storage)); } TEST(StorageTest, StorageIsAliasOfMethod) { // Test Storage::is_alias_of() method at::Tensor tensor1 = at::ones({2, 3}, at::kFloat); at::TensorBase tensor2 = tensor1.view({3, 2}); at::TensorBase tensor3 = at::ones({2, 3}, at::kFloat); c10::Storage storage1 = tensor1.storage(); c10::Storage storage2 = tensor2.storage(); c10::Storage storage3 = tensor3.storage(); // Same underlying allocation ASSERT_TRUE(storage1.is_alias_of(storage2)); ASSERT_TRUE(storage2.is_alias_of(storage1)); // Different allocations ASSERT_FALSE(storage1.is_alias_of(storage3)); // Self alias ASSERT_TRUE(storage1.is_alias_of(storage1)); // Empty storage c10::Storage empty_storage; ASSERT_FALSE(storage1.is_alias_of(empty_storage)); ASSERT_FALSE(empty_storage.is_alias_of(storage1)); } TEST(StorageTest, MaybeOwnedTraitsSpecialization) { // Test MaybeOwnedTraits specialization using Traits = c10::MaybeOwnedTraits; at::TensorBase tensor = at::ones({2, 3}, at::kFloat); c10::Storage original = tensor.storage(); // Test createBorrow Traits::borrow_type borrowed = Traits::createBorrow(original); ASSERT_EQ(borrowed.allocation(), original.allocation()); ASSERT_TRUE(borrowed.valid()); // Test referenceFromBorrow const Traits::owned_type& ref = Traits::referenceFromBorrow(borrowed); ASSERT_EQ(ref.allocation(), original.allocation()); // Test pointerFromBorrow const Traits::owned_type* ptr = Traits::pointerFromBorrow(borrowed); ASSERT_NE(ptr, nullptr); ASSERT_EQ(ptr->allocation(), original.allocation()); // Test debugBorrowIsValid ASSERT_TRUE(Traits::debugBorrowIsValid(borrowed)); // Test assignBorrow c10::Storage another_borrow; Traits::assignBorrow(another_borrow, borrowed); ASSERT_EQ(another_borrow.allocation(), original.allocation()); // Test destroyBorrow Traits::destroyBorrow(borrowed); ASSERT_FALSE(borrowed.valid()); } TEST(StorageTest, ExclusivelyOwnedTraitsSpecialization) { // Test ExclusivelyOwnedTraits specialization using Traits = c10::ExclusivelyOwnedTraits; // Test nullRepr Traits::repr_type null_repr = Traits::nullRepr(); ASSERT_FALSE(null_repr.valid()); // Test createInPlace with default constructor Traits::repr_type created = Traits::createInPlace(); ASSERT_FALSE(created.valid()); // Default constructed // Test moveToRepr at::TensorBase tensor = at::ones({2, 3}, at::kFloat); c10::Storage original = tensor.storage(); auto alloc = original.allocation(); Traits::repr_type moved = Traits::moveToRepr(std::move(original)); ASSERT_EQ(moved.allocation(), alloc); // Test take c10::Storage to_take = tensor.storage(); alloc = to_take.allocation(); c10::Storage taken = Traits::take(to_take); ASSERT_EQ(taken.allocation(), alloc); // Test getImpl (mutable) Traits::pointer_type ptr = Traits::getImpl(taken); ASSERT_NE(ptr, nullptr); ASSERT_EQ(ptr->allocation(), alloc); // Test getImpl (const) const c10::Storage& const_taken = taken; Traits::const_pointer_type const_ptr = Traits::getImpl(const_taken); ASSERT_NE(const_ptr, nullptr); ASSERT_EQ(const_ptr->allocation(), alloc); } // Custom deleter for testing external DataPtr static bool g_test_deleter_called = false; static void* g_test_deleter_context = nullptr; static void TestDeleter(void* ctx) { g_test_deleter_called = true; g_test_deleter_context = ctx; // In real usage, would free the memory here } TEST(StorageTest, ExternalDataPtrUseCount) { // Test use_count() semantics for external DataPtr (with deleter) // This verifies AC-1: single Storage use_count == 1, copy == 2 g_test_deleter_called = false; g_test_deleter_context = nullptr; void* test_ptr = reinterpret_cast(0x12345678); void* test_ctx = reinterpret_cast(0xABCDEF00); // Create external DataPtr with custom deleter c10::DataPtr external_ptr( test_ptr, test_ctx, &TestDeleter, c10::Device(c10::DeviceType::CPU)); // Create Storage from external DataPtr c10::Storage storage(c10::Storage::use_byte_size_t{}, 1024, std::move(external_ptr), nullptr, false); // Verify single Storage has use_count == 1 and unique() == true ASSERT_EQ(storage.use_count(), 1) << "Single external DataPtr Storage should have use_count == 1"; ASSERT_TRUE(storage.unique()) << "Single external DataPtr Storage should be unique"; // Copy the storage c10::Storage storage_copy(storage); // Verify both Storages have use_count == 2 ASSERT_EQ(storage.use_count(), 2) << "Original Storage should have use_count == 2 after copy"; ASSERT_EQ(storage_copy.use_count(), 2) << "Copied Storage should have use_count == 2"; ASSERT_FALSE(storage.unique()) << "Original Storage should not be unique after copy"; ASSERT_FALSE(storage_copy.unique()) << "Copied Storage should not be unique"; // Verify both point to the same data ASSERT_EQ(storage.data(), storage_copy.data()); } TEST(StorageTest, ExternalDataPtrDeleterPreserved) { // Test that data_ptr().get_deleter() returns original deleter (not wrapper) // This verifies AC-2: data_ptr() returns original DataPtr with correct // deleter g_test_deleter_called = false; g_test_deleter_context = nullptr; void* test_ptr = reinterpret_cast(0x12345678); void* test_ctx = reinterpret_cast(0xABCDEF00); // Create external DataPtr with custom deleter c10::DataPtr external_ptr( test_ptr, test_ctx, &TestDeleter, c10::Device(c10::DeviceType::CPU)); // Verify the original DataPtr has our deleter ASSERT_EQ(external_ptr.get_deleter(), &TestDeleter); // Create Storage from external DataPtr c10::Storage storage(c10::Storage::use_byte_size_t{}, 1024, std::move(external_ptr), nullptr, false); // Get the DataPtr from storage const c10::DataPtr& data_ptr = storage.data_ptr(); // Verify get_deleter() returns the original deleter (not a wrapper) ASSERT_EQ(data_ptr.get_deleter(), &TestDeleter) << "data_ptr().get_deleter() should return original deleter, not wrapper"; // Verify get_context() returns original context ASSERT_EQ(data_ptr.get_context(), test_ctx) << "data_ptr().get_context() should return original context"; } TEST(StorageTest, ExternalDataPtrCopyPreservesDeleter) { // Test that copying Storage preserves the original deleter g_test_deleter_called = false; g_test_deleter_context = nullptr; void* test_ptr = reinterpret_cast(0x12345678); void* test_ctx = reinterpret_cast(0xABCDEF00); // Create external DataPtr with custom deleter c10::DataPtr external_ptr( test_ptr, test_ctx, &TestDeleter, c10::Device(c10::DeviceType::CPU)); // Create Storage from external DataPtr c10::Storage storage(c10::Storage::use_byte_size_t{}, 1024, std::move(external_ptr), nullptr, false); // Copy the storage c10::Storage storage_copy(storage); // Verify both have the same deleter ASSERT_EQ(storage.data_ptr().get_deleter(), &TestDeleter); ASSERT_EQ(storage_copy.data_ptr().get_deleter(), &TestDeleter); // Verify both have the same context ASSERT_EQ(storage.data_ptr().get_context(), test_ctx); ASSERT_EQ(storage_copy.data_ptr().get_context(), test_ctx); } TEST(StorageTest, ExternalDataPtrMutableDataPtrCoW) { // Test CoW behavior for external DataPtr with deleter // With single-path design, CoW is skipped for DataPtr with deleter g_test_deleter_called = false; void* test_ptr = reinterpret_cast(0x12345678); void* test_ctx = reinterpret_cast(0xABCDEF00); // Create external DataPtr with custom deleter c10::DataPtr external_ptr( test_ptr, test_ctx, &TestDeleter, c10::Device(c10::DeviceType::CPU)); // Create Storage from external DataPtr c10::Storage storage(c10::Storage::use_byte_size_t{}, 1024, std::move(external_ptr), nullptr, false); // Copy the storage (now use_count should be 2) c10::Storage storage_copy(storage); ASSERT_EQ(storage.use_count(), 2); ASSERT_EQ(storage_copy.use_count(), 2); // Call mutable_data_ptr() - for external DataPtr with deleter, CoW is skipped // because we cannot clone arbitrary deleters c10::DataPtr& mutable_dp = storage_copy.mutable_data_ptr(); // The mutable_data_ptr should still point to the same data ASSERT_EQ(mutable_dp.get(), test_ptr); // The deleter should still be the original ASSERT_EQ(mutable_dp.get_deleter(), &TestDeleter); } TEST(StorageTest, DefaultConstructedStorageUseCount) { // Test that default constructed storage has use_count == 0 c10::Storage storage; ASSERT_EQ(storage.use_count(), 0) << "Default constructed Storage should have use_count == 0"; ASSERT_FALSE(storage.unique()); ASSERT_FALSE(storage.valid()); } TEST(StorageTest, MovedFromStorageIsGracefullyEmpty) { at::TensorBase tensor = at::ones({2, 3}, at::kFloat); c10::Storage original = tensor.storage(); c10::Storage moved(std::move(original)); ASSERT_TRUE(moved.valid()); ASSERT_FALSE(original.valid()); ASSERT_EQ(original.nbytes(), 0UL); ASSERT_FALSE(original.resizable()); ASSERT_EQ(original.data(), nullptr); ASSERT_EQ(original.mutable_data(), nullptr); ASSERT_EQ(original.allocation(), nullptr); ASSERT_EQ(original.allocator(), nullptr); ASSERT_EQ(original.device_type(), phi::AllocationType::CPU); ASSERT_EQ(original.use_count(), 0UL); } TEST(StorageTest, DataPtrCompareExchangeDeleterAndCastContext) { int* ctx = new int(42); c10::DataPtr dp(ctx, ctx, &DeleteIntPtr, c10::Device(c10::DeviceType::CPU)); ASSERT_EQ(dp.cast_context(&DeleteIntPtr), ctx); ASSERT_EQ(dp.cast_context(&DeleteCharArray), nullptr); ASSERT_TRUE(dp.compare_exchange_deleter(&DeleteIntPtr, &DeleteCharArray)); ASSERT_EQ(dp.get_deleter(), &DeleteCharArray); ASSERT_FALSE(dp.compare_exchange_deleter(&DeleteIntPtr, &DeleteIntPtr)); } TEST(StorageTest, DataPtrUnsafeResetDataAndCtx) { c10::DataPtr empty; ASSERT_TRUE(empty == nullptr); ASSERT_FALSE(empty != nullptr); void* p = reinterpret_cast(0x1234); ASSERT_TRUE(empty.unsafe_reset_data_and_ctx(p)); ASSERT_EQ(empty.get(), p); ASSERT_EQ(empty.get_context(), p); int* guarded_ctx = new int(5); c10::DataPtr guarded(guarded_ctx, guarded_ctx, &DeleteIntPtr, c10::Device(c10::DeviceType::CPU)); ASSERT_FALSE( guarded.unsafe_reset_data_and_ctx(reinterpret_cast(0x5678))); } TEST(StorageTest, DataPtrMoveAndReleaseContextHelpers) { int* ctx = new int(9); c10::DataPtr dp(ctx, ctx, &DeleteIntPtr, c10::Device(c10::DeviceType::CPU)); std::unique_ptr moved_ctx = dp.move_context(); ASSERT_EQ(moved_ctx.get(), ctx); ASSERT_EQ(dp.get_context(), nullptr); int* ctx2 = new int(11); c10::DataPtr dp2( ctx2, ctx2, &DeleteIntPtr, c10::Device(c10::DeviceType::CPU)); void* released = dp2.release_context(); ASSERT_EQ(released, ctx2); ASSERT_EQ(dp2.get_context(), nullptr); DeleteIntPtr(released); } TEST(StorageTest, AllocatorRawAllocateAndDeallocate) { RawCompatibleAllocator alloc; void* raw = alloc.raw_allocate(8); ASSERT_NE(raw, nullptr); alloc.raw_deallocate(raw); } TEST(StorageTest, AllocatorRawAllocateRejectsMismatchedContext) { RawIncompatibleAllocator alloc; EXPECT_THROW((void)alloc.raw_allocate(8), std::exception); } TEST(StorageTest, AllocatorRawDeallocateRequiresDeleter) { NullRawDeleterAllocator alloc; EXPECT_THROW(alloc.raw_deallocate(reinterpret_cast(0x1)), std::exception); } TEST(StorageTest, AllocatorDefaultRawDeleterIsNull) { DefaultRawDeleterAllocator alloc; ASSERT_EQ(alloc.raw_deleter(), nullptr); } TEST(StorageTest, AllocatorCloneCopiesBytes) { RawCompatibleAllocator alloc; c10::DataPtr src = alloc.allocate(4); auto* src_bytes = static_cast(src.get()); src_bytes[0] = 1; src_bytes[1] = 2; src_bytes[2] = 3; src_bytes[3] = 4; c10::DataPtr cloned = alloc.clone(src.get(), 4); auto* dst_bytes = static_cast(cloned.get()); ASSERT_EQ(dst_bytes[0], 1); ASSERT_EQ(dst_bytes[1], 2); ASSERT_EQ(dst_bytes[2], 3); ASSERT_EQ(dst_bytes[3], 4); } TEST(StorageTest, DataPtrHelpersAndAllocatorSimpleDataPtrChecks) { // Cover DataPtr(data, Device) ctor path where context is nullptr. int value = 7; c10::DataPtr dp(&value, c10::Device(c10::DeviceType::CPU)); ASSERT_EQ(dp.operator->(), &value); ASSERT_EQ(dp.get_context(), nullptr); // unsafe_set_device is used by callers that update metadata only. dp.unsafe_set_device(c10::Device(c10::DeviceType::CPU)); ASSERT_EQ(dp.device().type(), c10::DeviceType::CPU); // PyTorch only treats context==data as a simple DataPtr. RawCompatibleAllocator compatible_alloc; ASSERT_FALSE(compatible_alloc.is_simple_data_ptr(dp)); // is_simple_data_ptr: context==data branch. c10::DataPtr simple = compatible_alloc.allocate(4); ASSERT_TRUE(compatible_alloc.is_simple_data_ptr(simple)); // is_simple_data_ptr: context!=data branch. c10::DataPtr non_simple = RawIncompatibleAllocator().allocate(4); ASSERT_FALSE(compatible_alloc.is_simple_data_ptr(non_simple)); dp.clear(); ASSERT_EQ(dp.get(), nullptr); } TEST(StorageTest, CreateTensorStorageNullAndHolderReuse) { c10::Storage empty = c10::Storage::createTensorStorage(nullptr); ASSERT_FALSE(empty.valid()); at::TensorBase tensor = at::ones({2, 3}, at::kFloat); c10::Storage base_storage = tensor.storage(); // ensureTensorHolder should be stable and reusable. auto holder0 = base_storage.ensureTensorHolder(); auto holder1 = base_storage.ensureTensorHolder(); ASSERT_NE(holder0, nullptr); ASSERT_EQ(holder0, holder1); // createTensorStorage should reuse impl when holder is StorageHolderView. c10::Storage from_holder = c10::Storage::createTensorStorage(holder0); ASSERT_EQ(from_holder.get_impl(), base_storage.get_impl()); } TEST(StorageTest, MaybeOwnedAndExclusiveTraitsHelpers) { c10::Storage src = at::ones({2, 2}, at::kFloat).storage(); c10::Storage borrowed = c10::MaybeOwnedTraits::createBorrow(src); ASSERT_EQ(borrowed.get_impl(), src.get_impl()); c10::Storage assigned; c10::MaybeOwnedTraits::assignBorrow(assigned, borrowed); ASSERT_EQ(assigned.get_impl(), src.get_impl()); ASSERT_EQ(c10::MaybeOwnedTraits::referenceFromBorrow(assigned) .get_impl(), src.get_impl()); ASSERT_EQ(c10::MaybeOwnedTraits::pointerFromBorrow(assigned) ->get_impl(), src.get_impl()); ASSERT_TRUE( c10::MaybeOwnedTraits::debugBorrowIsValid(assigned)); c10::MaybeOwnedTraits::destroyBorrow(assigned); ASSERT_FALSE(assigned.valid()); using ET = c10::ExclusivelyOwnedTraits; c10::Storage null_repr = ET::nullRepr(); ASSERT_FALSE(null_repr.valid()); c10::Storage in_place = ET::createInPlace(); ASSERT_FALSE(in_place.valid()); c10::Storage moved = ET::moveToRepr(std::move(src)); ASSERT_TRUE(moved.valid()); c10::Storage taken = ET::take(moved); ASSERT_TRUE(taken.valid()); ASSERT_FALSE(moved.valid()); ASSERT_NE(ET::getImpl(taken), nullptr); const c10::Storage& c_taken = taken; ASSERT_NE(ET::getImpl(c_taken), nullptr); } TEST(StorageTest, SetDataPtrReturnsOldValues) { at::TensorBase tensor = at::ones({2, 3}, at::kFloat); at::TensorBase other = at::ones({2, 3}, at::kFloat); c10::Storage storage = tensor.storage(); const void* old_ptr = storage.data(); auto old_alloc = storage.allocation(); auto new_alloc = other.storage().allocation(); ASSERT_NE(new_alloc, nullptr); // shared_ptr overload returns previous allocation. auto returned_alloc = storage.set_data_ptr(new_alloc); ASSERT_EQ(returned_alloc, old_alloc); ASSERT_EQ(storage.allocation(), new_alloc); // DataPtr overload returns previous DataPtr and clears allocation-backed // state on write. c10::DataPtr new_data_ptr(other.data_ptr(), c10::Device(c10::DeviceType::CPU)); c10::DataPtr old_data_ptr = storage.set_data_ptr(std::move(new_data_ptr)); ASSERT_EQ(old_data_ptr.get(), new_alloc->ptr()); ASSERT_EQ(storage.data(), other.data_ptr()); ASSERT_EQ(storage.allocation(), nullptr); ASSERT_NE(old_data_ptr.get(), old_ptr); } // --------------------------------------------------------------------------- // Reference Semantics Tests // // These tests verify that Storage copies share a single underlying // StorageImpl, so mutations via set_data_ptr*(), set_nbytes(), and // mutable_data_ptr() are visible through all handles — matching the // observable contract of PyTorch's c10::Storage (which wraps a shared // StorageImpl via intrusive_ptr). // --------------------------------------------------------------------------- TEST(StorageTest, ReferenceSemanticsMutationVisibleThroughCopy) { // After copying a Storage, writing to one handle is visible via the other. at::TensorBase tensor1 = at::ones({2, 3}, at::kFloat); at::TensorBase tensor2 = at::ones({4, 5}, at::kFloat); c10::Storage storage_a = tensor1.storage(); c10::Storage storage_b = storage_a; // shares StorageImpl ASSERT_EQ(storage_a.data(), storage_b.data()) << "Copies should start with the same data pointer"; // Replace allocation via the shared_ptr overload auto new_alloc = tensor2.storage().allocation(); storage_a.set_data_ptr_noswap(new_alloc); ASSERT_EQ(storage_b.allocation(), new_alloc) << "storage_b should see the allocation change made through storage_a"; ASSERT_EQ(storage_a.data(), storage_b.data()) << "Both handles should point to the same data after mutation"; } TEST(StorageTest, ReferenceSemanticsMutableDataPtrShared) { // mutable_data_ptr() returns a reference into the shared StorageImpl, // so the reference obtained from one handle is the same object as the // data_ptr() accessed through its copy. at::TensorBase tensor = at::ones({2, 3}, at::kFloat); c10::Storage storage_a = tensor.storage(); c10::Storage storage_b = storage_a; c10::DataPtr& dp_via_a = storage_a.mutable_data_ptr(); ASSERT_EQ(&dp_via_a, &storage_b.data_ptr()) << "mutable_data_ptr() from one handle should be the same object as " "data_ptr() from another handle that shares the StorageImpl"; } TEST(StorageTest, ReferenceSemanticsMutationNotVisibleAcrossIndependent) { // Two Storage objects constructed independently (not by copying one from the // other) do NOT share a StorageImpl, so mutations through one do not affect // the other. at::TensorBase tensor1 = at::ones({2, 3}, at::kFloat); at::TensorBase tensor2 = at::ones({4, 5}, at::kFloat); // Two independently-created Storages — different StorageImpls c10::Storage storage_a = tensor1.storage(); c10::Storage storage_b = tensor2.storage(); const void* original_b_data = storage_b.data(); auto new_alloc = tensor2.storage().allocation(); storage_a.set_data_ptr_noswap(new_alloc); ASSERT_EQ(storage_b.data(), original_b_data) << "Independently-constructed Storage should not be affected by " "mutations to another Storage"; } TEST(StorageTest, ReferenceSemanticsTwoIndependentStorageCalls) { // Multiple calls to tensor.storage() on the same tensor return handles // sharing the same underlying StorageImpl, matching PyTorch's reference // semantics where TensorBase::storage() always refers to the same storage. at::TensorBase tensor = at::ones({2, 3}, at::kFloat); at::TensorBase tensor2 = at::ones({4, 5}, at::kFloat); c10::Storage storage_b = tensor.storage(); c10::Storage storage_c = tensor.storage(); // same impl as storage_b // Both handles point to the same underlying data. ASSERT_EQ(storage_b.data(), storage_c.data()) << "Two Storage handles from the same tensor should share the same data " "pointer initially"; // Mutation through one handle is visible through the other. auto new_alloc = tensor2.storage().allocation(); storage_b.set_data_ptr_noswap(new_alloc); ASSERT_EQ(storage_c.data(), storage_b.data()) << "Mutation through one Storage handle should be visible through " "another handle obtained from the same tensor"; } TEST(StorageTest, StorageMutationUpdatesTensorDataPtr) { at::TensorBase tensor = at::ones({2, 3}, at::kFloat); at::TensorBase other = at::ones({4, 5}, at::kFloat); c10::Storage storage = tensor.storage(); auto new_alloc = other.storage().allocation(); ASSERT_NE(new_alloc, nullptr); ASSERT_NE(tensor.data_ptr(), new_alloc->ptr()); storage.set_data_ptr_noswap(new_alloc); ASSERT_EQ(tensor.data_ptr(), new_alloc->ptr()) << "tensor.data_ptr() must follow mutations made through " "tensor.storage()"; ASSERT_EQ(tensor.storage().allocation(), new_alloc) << "Repeated storage() calls should observe the live allocation"; } TEST(StorageTest, StorageMutationPersistsAfterHandleDestruction) { at::TensorBase tensor = at::ones({2, 3}, at::kFloat); at::TensorBase other = at::ones({4, 5}, at::kFloat); auto new_alloc = other.storage().allocation(); ASSERT_NE(new_alloc, nullptr); { c10::Storage storage = tensor.storage(); storage.set_data_ptr_noswap(new_alloc); } ASSERT_EQ(tensor.data_ptr(), new_alloc->ptr()) << "Tensor should keep the storage alive after external handles die"; ASSERT_EQ(tensor.storage().allocation(), new_alloc); } TEST(StorageTest, RepeatedStorageCallsReturnSameReference) { at::TensorBase tensor = at::ones({2, 3}, at::kFloat); const c10::Storage& storage_a = tensor.storage(); const c10::Storage& storage_b = tensor.storage(); ASSERT_EQ(&storage_a, &storage_b) << "storage() must return the same reference for one tensor wrapper"; ASSERT_EQ(storage_a.get_impl(), storage_b.get_impl()); } TEST(StorageTest, CopiedTensorWrappersShareStorageImpl) { at::TensorBase tensor = at::ones({2, 3}, at::kFloat); at::TensorBase alias = tensor; at::TensorBase other = at::ones({4, 5}, at::kFloat); auto new_alloc = other.storage().allocation(); ASSERT_NE(new_alloc, nullptr); c10::Storage storage = tensor.storage(); storage.set_data_ptr_noswap(new_alloc); ASSERT_EQ(tensor.storage().get_impl(), alias.storage().get_impl()); ASSERT_EQ(alias.data_ptr(), new_alloc->ptr()) << "Copied TensorBase wrappers must observe shared storage mutations"; } TEST(StorageTest, AsStridedViewSharesStorageImplWithBaseTensor) { at::Tensor base = at::tensor({1, 2, 3, 4}, at::kInt); at::Tensor view = base.as_strided({3}, {1}, 1); ASSERT_EQ(base.storage().get_impl(), view.storage().get_impl()); view.resize_({4}); ASSERT_EQ(view.data_ptr(), base.data_ptr() + 1) << "Growing an as_strided view must update the shared compat storage " "visible from the base tensor"; } TEST(StorageTest, AliasWrapperDoesNotIncreaseTensorOwnedStorageCount) { at::TensorBase tensor = at::ones({2, 3}, at::kFloat); const c10::Storage& single_wrapper_storage = tensor.storage(); size_t single_wrapper_count = single_wrapper_storage.use_count(); ASSERT_GE(single_wrapper_count, 1UL); at::TensorBase alias = tensor; const c10::Storage& alias_storage = alias.storage(); ASSERT_EQ(single_wrapper_storage.get_impl(), alias_storage.get_impl()); ASSERT_EQ(single_wrapper_storage.use_count(), single_wrapper_count) << "A copied TensorBase wrapper sharing the same tensor impl must not " "add an extra tensor-owned Storage reference"; } TEST(StorageTest, ViewTensorWrappersShareStorageImpl) { at::Tensor tensor = at::ones({2, 3}, at::kFloat); at::TensorBase alias = tensor.view({3, 2}); c10::Storage tensor_storage = tensor.storage(); c10::Storage alias_storage = alias.storage(); ASSERT_TRUE(tensor_storage.is_alias_of(alias_storage)) << "Fresh wrappers over the same underlying storage should share a " "StorageImpl"; ASSERT_FALSE(c10::isSharedStorageAlias(tensor_storage, alias_storage)) << "isSharedStorageAlias() should follow DataPtr ownership semantics, " "not overlapping ranges"; } TEST(StorageTest, HasStorageTracksLiveStorageState) { at::TensorBase tensor = at::ones({2, 3}, at::kFloat); ASSERT_TRUE(tensor.has_storage()); c10::Storage storage = tensor.storage(); storage.set_data_ptr(at::DataPtr()); ASSERT_FALSE(tensor.has_storage()); tensor.reset(); ASSERT_FALSE(tensor.has_storage()); } TEST(StorageTest, ReferenceSemanticsSetNbytesVisibleThroughCopy) { // set_nbytes() on one handle is visible through its copy. at::TensorBase tensor = at::ones({2, 3}, at::kFloat); c10::Storage storage_a = tensor.storage(); c10::Storage storage_b = storage_a; size_t new_size = 42; storage_a.set_nbytes(new_size); ASSERT_EQ(storage_b.nbytes(), new_size) << "set_nbytes() change should be visible through all copies"; } #if defined(PADDLE_WITH_CUDA) || defined(PADDLE_WITH_HIP) TEST(StorageTest, CUDAAllocatorZeroBytePreservesDevice) { // getCUDADeviceAllocator()->allocate(0) must return a DataPtr whose device // is the current CUDA device, not a default-constructed CPU DataPtr. if (!at::cuda::is_available()) { return; // No CUDA device, skip } c10::Allocator* alloc = at::cuda::getCUDADeviceAllocator(); ASSERT_NE(alloc, nullptr); c10::DataPtr dp = alloc->allocate(0); // Pointer should be null for zero-byte allocation ASSERT_EQ(dp.get(), nullptr) << "Zero-byte allocation should return null pointer"; // Device type must be CUDA, not CPU. For HIP/ROCm builds, PyTorch's // compatibility convention is to expose DeviceType::CUDA rather than a // separate HIP device type, so we follow the same convention. ASSERT_EQ(dp.device().type(), c10::DeviceType::CUDA) << "Zero-byte CUDA allocator DataPtr should carry CUDA device type"; // Device index should match the current device int current_device = phi::backends::gpu::GetCurrentDeviceId(); ASSERT_EQ(static_cast(dp.device().index()), current_device) << "Zero-byte DataPtr should carry the current CUDA device index"; } #endif // PADDLE_WITH_CUDA || PADDLE_WITH_HIP #if defined(PADDLE_WITH_CUDA) || defined(PADDLE_WITH_HIP) TEST(StorageTest, CUDAAllocatorRawDeleterIsNull) { // PaddleCUDAAllocatorAdapter::raw_deleter() must return nullptr because the // c10::Allocator raw API contract requires get()==get_context() in the // returned DataPtr, but our adapter returns data=device_ptr, // context=phi::Allocation*, which violates that contract. // Returning nullptr signals that raw_allocate/raw_deallocate are unsafe. c10::Allocator* alloc = at::cuda::getCUDADeviceAllocator(); ASSERT_NE(alloc, nullptr); ASSERT_EQ(alloc->raw_deleter(), nullptr) << "PaddleCUDAAllocatorAdapter::raw_deleter() must return nullptr " "because get() != get_context() in its allocate() DataPtr"; } #endif // PADDLE_WITH_CUDA || PADDLE_WITH_HIP