| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| #include <gtest/gtest.h> |
| #include <tvm/ffi/container/tensor.h> |
| #include <tvm/ffi/extra/c_env_api.h> |
| |
| #include <atomic> |
| #include <chrono> |
| #include <condition_variable> |
| #include <mutex> |
| #include <thread> |
| #include <vector> |
| |
| namespace { |
| |
| using namespace tvm::ffi; |
| |
| struct CPUNDAlloc { |
| void AllocData(DLTensor* tensor) { tensor->data = malloc(GetDataSize(*tensor)); } |
| void FreeData(DLTensor* tensor) { free(tensor->data); } |
| }; |
| |
| inline Tensor Empty(const Shape& shape, DLDataType dtype, DLDevice device) { |
| return Tensor::FromNDAlloc(CPUNDAlloc(), shape, dtype, device); |
| } |
| |
| int TestDLPackManagedTensorAllocator(DLTensor* prototype, DLManagedTensorVersioned** out, |
| void* error_ctx, |
| void (*SetError)(void* error_ctx, const char* kind, |
| const char* message)) { |
| Shape shape(prototype->shape, prototype->shape + prototype->ndim); |
| Tensor nd = Empty(shape, prototype->dtype, prototype->device); |
| *out = nd.ToDLPackVersioned(); |
| return 0; |
| } |
| |
| int TestDLPackManagedTensorAllocatorError(DLTensor* prototype, DLManagedTensorVersioned** out, |
| void* error_ctx, |
| void (*SetError)(void* error_ctx, const char* kind, |
| const char* message)) { |
| SetError(error_ctx, "MemoryError", "TestDLPackManagedTensorAllocatorError"); |
| return -1; |
| } |
| |
| TEST(CEnvAPI, TVMFFIEnvSetDLPackManagedTensorAllocator) { |
| auto old_allocator = TVMFFIEnvGetDLPackManagedTensorAllocator(); |
| DLPackManagedTensorAllocator pre_allocator; |
| TVMFFIEnvSetDLPackManagedTensorAllocator(TestDLPackManagedTensorAllocator, 0, &pre_allocator); |
| EXPECT_EQ(old_allocator, pre_allocator); |
| TVMFFIEnvSetDLPackManagedTensorAllocator(old_allocator, 0, nullptr); |
| } |
| |
| TEST(CEnvAPI, TVMFFIEnvTensorAlloc) { |
| auto old_allocator = TVMFFIEnvGetDLPackManagedTensorAllocator(); |
| TVMFFIEnvSetDLPackManagedTensorAllocator(TestDLPackManagedTensorAllocator, 0, nullptr); |
| Tensor tensor = Tensor::FromEnvAlloc(TVMFFIEnvTensorAlloc, {1, 2, 3}, |
| DLDataType({kDLFloat, 32, 1}), DLDevice({kDLCPU, 0})); |
| EXPECT_EQ(tensor.use_count(), 1); |
| EXPECT_EQ(tensor.shape().size(), 3); |
| EXPECT_EQ(tensor.size(0), 1); |
| EXPECT_EQ(tensor.size(1), 2); |
| EXPECT_EQ(tensor.size(2), 3); |
| EXPECT_EQ(tensor.dtype().code, kDLFloat); |
| EXPECT_EQ(tensor.dtype().bits, 32); |
| EXPECT_EQ(tensor.dtype().lanes, 1); |
| EXPECT_EQ(tensor.device().device_type, kDLCPU); |
| EXPECT_EQ(tensor.device().device_id, 0); |
| EXPECT_NE(tensor.data_ptr(), nullptr); |
| TVMFFIEnvSetDLPackManagedTensorAllocator(old_allocator, 0, nullptr); |
| } |
| |
| TEST(CEnvAPI, TVMFFIEnvTensorAllocError) { |
| auto old_allocator = TVMFFIEnvGetDLPackManagedTensorAllocator(); |
| TVMFFIEnvSetDLPackManagedTensorAllocator(TestDLPackManagedTensorAllocatorError, 0, nullptr); |
| |
| EXPECT_THROW( |
| { |
| try { |
| Tensor::FromEnvAlloc(TVMFFIEnvTensorAlloc, {1, 2, 3}, DLDataType({kDLFloat, 32, 1}), |
| DLDevice({kDLCPU, 0})); |
| } catch (const tvm::ffi::Error& e) { |
| EXPECT_EQ(e.kind(), "MemoryError"); |
| EXPECT_EQ(e.message(), "TestDLPackManagedTensorAllocatorError"); |
| throw; |
| } |
| }, |
| tvm::ffi::Error); |
| TVMFFIEnvSetDLPackManagedTensorAllocator(old_allocator, 0, nullptr); |
| } |
| |
| // Helper functions for TVMFFIHandleInitDeinitOnce test |
| static int InitSuccess(void** handle_addr) { |
| *handle_addr = new int(42); |
| return 0; |
| } |
| |
| static int InitShouldNotBeCalled(void** handle_addr) { |
| *handle_addr = new int(999); |
| return 0; |
| } |
| |
| static int DeinitSuccess(void* h) { |
| delete static_cast<int*>(h); |
| return 0; |
| } |
| |
| static int DeinitShouldNotBeCalled(void* h) { |
| // Should not be called when handle is already null |
| return -1; |
| } |
| |
| static int InitWithError(void** handle_addr) { |
| TVMFFIErrorSetRaisedFromCStr("RuntimeError", "Initialization failed"); |
| return -1; |
| } |
| |
| static int InitReturnsNull(void** handle_addr) { |
| *handle_addr = nullptr; // Invalid: must return non-null handle |
| return 0; |
| } |
| |
| static int InitForDeinitError(void** handle_addr) { |
| *handle_addr = new int(100); |
| return 0; |
| } |
| |
| static int DeinitWithError(void* h) { |
| delete static_cast<int*>(h); |
| TVMFFIErrorSetRaisedFromCStr("RuntimeError", "Deinitialization failed"); |
| return -1; |
| } |
| |
| static int InitValue123(void** handle_addr) { |
| *handle_addr = new int(123); |
| return 0; |
| } |
| |
| static int InitValue456(void** handle_addr) { |
| *handle_addr = new int(456); |
| return 0; |
| } |
| |
| TEST(CEnvAPI, TVMFFIHandleInitDeinitOnce) { |
| // Test 1: Successful initialization |
| void* handle = nullptr; |
| int ret = TVMFFIHandleInitOnce(&handle, InitSuccess); |
| EXPECT_EQ(ret, 0); |
| EXPECT_NE(handle, nullptr); |
| EXPECT_EQ(*static_cast<int*>(handle), 42); |
| |
| // Test 2: Multiple init calls should not re-initialize (idempotent) |
| void* original_handle = handle; |
| ret = TVMFFIHandleInitOnce(&handle, InitShouldNotBeCalled); |
| EXPECT_EQ(ret, 0); |
| EXPECT_EQ(handle, original_handle); // Handle should remain unchanged |
| EXPECT_EQ(*static_cast<int*>(handle), 42); // Value should still be 42 |
| |
| // Test 3: Successful deinitialization |
| ret = TVMFFIHandleDeinitOnce(&handle, DeinitSuccess); |
| EXPECT_EQ(ret, 0); |
| EXPECT_EQ(handle, nullptr); |
| |
| // Test 4: Multiple deinit calls should be safe (idempotent) |
| ret = TVMFFIHandleDeinitOnce(&handle, DeinitShouldNotBeCalled); |
| EXPECT_EQ(ret, 0); |
| EXPECT_EQ(handle, nullptr); |
| |
| // Test 5: Init error - init_func returns error code |
| void* handle2 = nullptr; |
| ret = TVMFFIHandleInitOnce(&handle2, InitWithError); |
| EXPECT_NE(ret, 0); |
| EXPECT_EQ(handle2, nullptr); |
| |
| // Test 6: Init error - init_func returns nullptr (invalid) |
| void* handle3 = nullptr; |
| ret = TVMFFIHandleInitOnce(&handle3, InitReturnsNull); |
| EXPECT_NE(ret, 0); |
| EXPECT_EQ(handle3, nullptr); |
| |
| // Test 7: Deinit error - deinit_func returns error |
| void* handle4 = nullptr; |
| ret = TVMFFIHandleInitOnce(&handle4, InitForDeinitError); |
| EXPECT_EQ(ret, 0); |
| EXPECT_NE(handle4, nullptr); |
| |
| ret = TVMFFIHandleDeinitOnce(&handle4, DeinitWithError); |
| EXPECT_NE(ret, 0); |
| EXPECT_EQ(handle4, nullptr); // Handle should still be set to nullptr |
| |
| // Test 8: Init-deinit lifecycle |
| void* handle5 = nullptr; |
| ret = TVMFFIHandleInitOnce(&handle5, InitValue123); |
| EXPECT_EQ(ret, 0); |
| EXPECT_NE(handle5, nullptr); |
| EXPECT_EQ(*static_cast<int*>(handle5), 123); |
| |
| ret = TVMFFIHandleDeinitOnce(&handle5, DeinitSuccess); |
| EXPECT_EQ(ret, 0); |
| EXPECT_EQ(handle5, nullptr); |
| |
| // Test 9: Ensure subsequent init after deinit works |
| ret = TVMFFIHandleInitOnce(&handle5, InitValue456); |
| EXPECT_EQ(ret, 0); |
| EXPECT_NE(handle5, nullptr); |
| EXPECT_EQ(*static_cast<int*>(handle5), 456); |
| |
| // Clean up |
| ret = TVMFFIHandleDeinitOnce(&handle5, DeinitSuccess); |
| EXPECT_EQ(ret, 0); |
| } |
| |
| // Helper functions and data for multithreaded test |
| struct ThreadSafeCounter { |
| int value; |
| std::atomic<int>* init_count_ptr; |
| std::atomic<int>* deinit_count_ptr; |
| |
| ThreadSafeCounter(int v, std::atomic<int>* init_ptr, std::atomic<int>* deinit_ptr) |
| : value(v), init_count_ptr(init_ptr), deinit_count_ptr(deinit_ptr) {} |
| }; |
| |
| // Global pointers for the current test counters |
| static std::atomic<int>* g_init_count = nullptr; |
| static std::atomic<int>* g_deinit_count = nullptr; |
| |
| static int InitWithCounter(void** handle_addr) { |
| auto* counter = new ThreadSafeCounter(42, g_init_count, g_deinit_count); |
| if (counter->init_count_ptr) { |
| counter->init_count_ptr->fetch_add(1, std::memory_order_relaxed); |
| } |
| // Small delay to increase the race window |
| std::this_thread::sleep_for(std::chrono::microseconds(100)); |
| *handle_addr = counter; |
| return 0; |
| } |
| |
| static int DeinitWithCounter(void* h) { |
| auto* counter = static_cast<ThreadSafeCounter*>(h); |
| if (counter->deinit_count_ptr) { |
| counter->deinit_count_ptr->fetch_add(1, std::memory_order_relaxed); |
| } |
| // Small delay to increase the race window |
| std::this_thread::sleep_for(std::chrono::microseconds(100)); |
| delete counter; |
| return 0; |
| } |
| |
| TEST(CEnvAPI, TVMFFIHandleInitDeinitOnceMultithreaded) { |
| // Test 1: Multiple threads calling InitOnce - should initialize only once |
| { |
| void* handle = nullptr; |
| const int num_threads = 4; |
| std::vector<std::thread> threads; |
| threads.reserve(num_threads); |
| std::vector<int> results(num_threads); |
| std::mutex mtx; |
| std::condition_variable cv; |
| bool ready = false; |
| std::atomic<int> init_count{0}; |
| |
| // Set global counter pointers |
| g_init_count = &init_count; |
| g_deinit_count = nullptr; |
| |
| // Create threads that all try to initialize simultaneously |
| for (int i = 0; i < num_threads; ++i) { |
| threads.emplace_back([&handle, &results, &mtx, &cv, &ready, i]() { |
| // Wait for all threads to be ready |
| std::unique_lock<std::mutex> lock(mtx); |
| cv.wait(lock, [&ready] { return ready; }); |
| lock.unlock(); |
| |
| results[i] = TVMFFIHandleInitOnce(&handle, InitWithCounter); |
| }); |
| } |
| |
| // Signal all threads to start |
| { |
| std::scoped_lock<std::mutex> lock(mtx); |
| ready = true; |
| } |
| cv.notify_all(); |
| |
| // Wait for all threads to complete |
| for (auto& t : threads) { |
| t.join(); |
| } |
| |
| // All threads should succeed |
| for (int i = 0; i < num_threads; ++i) { |
| EXPECT_EQ(results[i], 0); |
| } |
| |
| // Handle should be initialized |
| EXPECT_NE(handle, nullptr); |
| auto* counter = static_cast<ThreadSafeCounter*>(handle); |
| EXPECT_EQ(counter->value, 42); |
| |
| // Init should have been called exactly once |
| EXPECT_EQ(init_count.load(), 1); |
| |
| // Clean up |
| int ret = TVMFFIHandleDeinitOnce(&handle, DeinitWithCounter); |
| EXPECT_EQ(ret, 0); |
| |
| // Reset global pointers |
| g_init_count = nullptr; |
| } |
| |
| // Test 2: Multiple threads calling DeinitOnce - should deinitialize only once |
| { |
| void* handle = nullptr; |
| std::atomic<int> init_count{0}; |
| std::atomic<int> deinit_count{0}; |
| |
| // Set global counter pointers |
| g_init_count = &init_count; |
| g_deinit_count = &deinit_count; |
| |
| // Initialize first |
| int ret = TVMFFIHandleInitOnce(&handle, InitWithCounter); |
| EXPECT_EQ(ret, 0); |
| EXPECT_NE(handle, nullptr); |
| |
| const int num_threads = 4; |
| std::vector<std::thread> threads; |
| threads.reserve(num_threads); |
| std::vector<int> results(num_threads); |
| std::mutex mtx; |
| std::condition_variable cv; |
| bool ready = false; |
| |
| // Create threads that all try to deinitialize simultaneously |
| for (int i = 0; i < num_threads; ++i) { |
| threads.emplace_back([&handle, &results, &mtx, &cv, &ready, i]() { |
| // Wait for all threads to be ready |
| std::unique_lock<std::mutex> lock(mtx); |
| cv.wait(lock, [&ready] { return ready; }); |
| lock.unlock(); |
| |
| results[i] = TVMFFIHandleDeinitOnce(&handle, DeinitWithCounter); |
| }); |
| } |
| |
| // Signal all threads to start |
| { |
| std::scoped_lock<std::mutex> lock(mtx); |
| ready = true; |
| } |
| cv.notify_all(); |
| |
| // Wait for all threads to complete |
| for (auto& t : threads) { |
| t.join(); |
| } |
| |
| // All threads should succeed |
| for (int i = 0; i < num_threads; ++i) { |
| EXPECT_EQ(results[i], 0); |
| } |
| |
| // Handle should be null |
| EXPECT_EQ(handle, nullptr); |
| |
| // Deinit should have been called exactly once |
| EXPECT_EQ(deinit_count.load(), 1); |
| |
| // Reset global pointers |
| g_init_count = nullptr; |
| g_deinit_count = nullptr; |
| } |
| } |
| } // namespace |