| 
 | 1 | +#include <gtest/gtest.h>  | 
 | 2 | + | 
 | 3 | +#include <iostream>  | 
 | 4 | +#include <string>  | 
 | 5 | +#include <thread>  | 
 | 6 | +#include <vector>  | 
 | 7 | + | 
 | 8 | +#include <executorch/runtime/executor/program.h>  | 
 | 9 | +#include <executorch/runtime/platform/runtime.h>  | 
 | 10 | + | 
 | 11 | +#include <executorch/extension/data_loader/file_data_loader.h>  | 
 | 12 | +#include <executorch/extension/memory_allocator/malloc_memory_allocator.h>  | 
 | 13 | +#include <executorch/extension/runner_util/inputs.h>  | 
 | 14 | + | 
 | 15 | +using executorch::runtime::Error;  | 
 | 16 | +using executorch::runtime::EValue;  | 
 | 17 | +using executorch::runtime::HierarchicalAllocator;  | 
 | 18 | +using executorch::runtime::MemoryManager;  | 
 | 19 | +using executorch::runtime::Method;  | 
 | 20 | +using executorch::runtime::MethodMeta;  | 
 | 21 | +using executorch::runtime::Program;  | 
 | 22 | +using executorch::runtime::Result;  | 
 | 23 | +using executorch::runtime::Span;  | 
 | 24 | + | 
 | 25 | +using executorch::extension::FileDataLoader;  | 
 | 26 | +using executorch::extension::MallocMemoryAllocator;  | 
 | 27 | +using executorch::extension::prepare_input_tensors;  | 
 | 28 | + | 
 | 29 | +/*  | 
 | 30 | + * Backend agnostic base class.  | 
 | 31 | + */  | 
 | 32 | +class ETPTEMethodRunBaseTest : public ::testing::Test {  | 
 | 33 | + protected:  | 
 | 34 | +  void SetUp() override {  | 
 | 35 | +    executorch::runtime::runtime_init();  | 
 | 36 | +  }  | 
 | 37 | + | 
 | 38 | +  // Runs the PTE e2e without using outside resources.  | 
 | 39 | +  // This will run in a single thread.  | 
 | 40 | +  // TODO(T208989128) - Add Synchronizer based run method.  | 
 | 41 | +  void run(  | 
 | 42 | +      const int id,  | 
 | 43 | +      const std::string& kTestPTEPath,  | 
 | 44 | +      const std::string& kMethodName,  | 
 | 45 | +      std::atomic<size_t>& count) const {  | 
 | 46 | +    Result<FileDataLoader> loader = FileDataLoader::from(kTestPTEPath.c_str());  | 
 | 47 | +    ASSERT_EQ(loader.error(), Error::Ok);  | 
 | 48 | + | 
 | 49 | +    Result<Program> program = Program::load(  | 
 | 50 | +        &loader.get(), Program::Verification::InternalConsistency);  | 
 | 51 | +    ASSERT_EQ(program.error(), Error::Ok);  | 
 | 52 | + | 
 | 53 | +    Result<MethodMeta> method_meta = program->method_meta(kMethodName.c_str());  | 
 | 54 | +    ASSERT_EQ(method_meta.error(), Error::Ok);  | 
 | 55 | + | 
 | 56 | +    const size_t num_memory_planned_buffers =  | 
 | 57 | +        method_meta->num_memory_planned_buffers();  | 
 | 58 | + | 
 | 59 | +    std::vector<std::unique_ptr<uint8_t[]>> planned_buffers;  | 
 | 60 | +    std::vector<Span<uint8_t>> planned_spans;  | 
 | 61 | +    for (size_t i = 0; i < num_memory_planned_buffers; ++i) {  | 
 | 62 | +      const size_t buffer_size =  | 
 | 63 | +          static_cast<size_t>(method_meta->memory_planned_buffer_size(i).get());  | 
 | 64 | +      planned_buffers.push_back(std::make_unique<uint8_t[]>(buffer_size));  | 
 | 65 | +      planned_spans.push_back({planned_buffers.back().get(), buffer_size});  | 
 | 66 | +    }  | 
 | 67 | + | 
 | 68 | +    auto method_allocator = std::make_unique<MallocMemoryAllocator>();  | 
 | 69 | +    auto memory_planned_allocator = std::make_unique<HierarchicalAllocator>(  | 
 | 70 | +        Span(planned_spans.data(), planned_spans.size()));  | 
 | 71 | +    auto temp_allocator = std::make_unique<MallocMemoryAllocator>();  | 
 | 72 | + | 
 | 73 | +    auto memory_manager = std::make_unique<MemoryManager>(  | 
 | 74 | +        method_allocator.get(),  | 
 | 75 | +        memory_planned_allocator.get(),  | 
 | 76 | +        temp_allocator.get());  | 
 | 77 | + | 
 | 78 | +    Result<Method> method =  | 
 | 79 | +        program->load_method(kMethodName.c_str(), memory_manager.get());  | 
 | 80 | +    ASSERT_EQ(method.error(), Error::Ok);  | 
 | 81 | + | 
 | 82 | +    auto inputs = prepare_input_tensors(*method);  | 
 | 83 | +    ASSERT_EQ(inputs.error(), Error::Ok);  | 
 | 84 | + | 
 | 85 | +    Error err = method->execute();  | 
 | 86 | +    for (int i = 0; i < id % 7; i++) {  | 
 | 87 | +      err = method->execute();  | 
 | 88 | +      ASSERT_EQ(err, Error::Ok);  | 
 | 89 | +    }  | 
 | 90 | + | 
 | 91 | +    std::vector<EValue> outputs(method->outputs_size());  | 
 | 92 | +    err = method->get_outputs(outputs.data(), outputs.size());  | 
 | 93 | +    ET_CHECK(err == Error::Ok);  | 
 | 94 | +    // TODO(T208989129) - Add validation of outputs using bundled  | 
 | 95 | +    // inputs/outputs.  | 
 | 96 | +    count++;  | 
 | 97 | +  }  | 
 | 98 | +};  | 
 | 99 | + | 
 | 100 | +class XNNPACKMultiDelegateTest : public ETPTEMethodRunBaseTest {  | 
 | 101 | + protected:  | 
 | 102 | +  std::string kTestPTE1Path, kTestPTE2Path;  | 
 | 103 | +  std::string kMethodName;  | 
 | 104 | +  int num_threads;  | 
 | 105 | + | 
 | 106 | +  void SetUp() override {  | 
 | 107 | +    ETPTEMethodRunBaseTest::SetUp();  | 
 | 108 | +    const char* pte1_path =  | 
 | 109 | +        std::getenv("ET_XNNPACK_GENERATED_ADD_LARGE_PTE_PATH");  | 
 | 110 | +    if (pte1_path == nullptr) {  | 
 | 111 | +      std::cerr << "ET_XNNPACK_GENERATED_ADD_LARGE_PTE_PATH is not set"  | 
 | 112 | +                << std::endl;  | 
 | 113 | +      FAIL();  | 
 | 114 | +    }  | 
 | 115 | +    kTestPTE1Path = std::string(pte1_path);  | 
 | 116 | + | 
 | 117 | +    const char* pte2_path =  | 
 | 118 | +        std::getenv("ET_XNNPACK_GENERATED_SUB_LARGE_PTE_PATH");  | 
 | 119 | +    if (pte1_path == nullptr) {  | 
 | 120 | +      std::cerr << "ET_XNNPACK_GENERATED_SUB_LARGE_PTE_PATH is not set"  | 
 | 121 | +                << std::endl;  | 
 | 122 | +      FAIL();  | 
 | 123 | +    }  | 
 | 124 | +    kTestPTE2Path = std::string(pte2_path);  | 
 | 125 | + | 
 | 126 | +    num_threads = 40;  | 
 | 127 | +    kMethodName = "forward";  | 
 | 128 | +  }  | 
 | 129 | +};  | 
 | 130 | + | 
 | 131 | +// This test is to validate the assumption that the delegate is thread safe.  | 
 | 132 | +// That includes the following:  | 
 | 133 | +// 1. The delegate can be initilized by multiple threads in parallel.  | 
 | 134 | +// 2. The delegate can be executed by multiple threads in parallel.  | 
 | 135 | +// 3. The delegate can be destroyed by multiple threads in parallel.  | 
 | 136 | +// Regardless of the underlying implementation of the delegate.  | 
 | 137 | +// This is particularly important when we have shared resources across  | 
 | 138 | +// delegate instances through a singleton backend instance.  | 
 | 139 | +TEST_F(XNNPACKMultiDelegateTest, MultipleThreads) {  | 
 | 140 | +  ASSERT_NE(kTestPTE1Path.size(), 0);  | 
 | 141 | +  ASSERT_NE(kTestPTE2Path.size(), 0);  | 
 | 142 | +  ASSERT_NE(num_threads, 0);  | 
 | 143 | +  ASSERT_NE(kMethodName.size(), 0);  | 
 | 144 | + | 
 | 145 | +  std::vector<std::thread> threads(num_threads);  | 
 | 146 | +  std::atomic<size_t> count{0};  | 
 | 147 | + | 
 | 148 | +  for (int i = 0; i < num_threads; i++) {  | 
 | 149 | +    threads[i] = std::thread([&, i]() {  | 
 | 150 | +      run(i, i % 7 ? kTestPTE1Path : kTestPTE2Path, kMethodName, count);  | 
 | 151 | +    });  | 
 | 152 | +  }  | 
 | 153 | +  for (int i = 0; i < num_threads; i++) {  | 
 | 154 | +    threads[i].join();  | 
 | 155 | +  }  | 
 | 156 | +  ASSERT_EQ(count, num_threads);  | 
 | 157 | +}  | 
 | 158 | + | 
 | 159 | +// TODO(T208989291): Add more tests here. For example,  | 
 | 160 | +// - PTEs with multiple methods  | 
 | 161 | +// - PTEs with proucer and consumer relationships in different threads  | 
 | 162 | +// - PTEs with more than 1 delegate instances  | 
 | 163 | +// - PTEs with different type of delegate instances  | 
 | 164 | +// - Add more patterns of delegate initialization and execution  | 
0 commit comments