xref: /aosp_15_r20/external/executorch/runtime/executor/test/kernel_integration_test.cpp (revision 523fa7a60841cd1ecfb9cc4201f1ca8b03ed023a)
1 /*
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  * All rights reserved.
4  *
5  * This source code is licensed under the BSD-style license found in the
6  * LICENSE file in the root directory of this source tree.
7  */
8 
9 #include <cctype>
10 #include <filesystem>
11 
12 #include <cstring>
13 #include <memory>
14 
15 #include <executorch/extension/data_loader/file_data_loader.h>
16 #include <executorch/extension/runner_util/inputs.h>
17 #include <executorch/runtime/core/error.h>
18 #include <executorch/runtime/core/result.h>
19 #include <executorch/runtime/executor/method.h>
20 #include <executorch/runtime/executor/program.h>
21 #include <executorch/runtime/executor/test/managed_memory_manager.h>
22 #include <executorch/runtime/kernel/kernel_runtime_context.h>
23 #include <executorch/runtime/kernel/operator_registry.h>
24 #include <executorch/runtime/platform/compiler.h>
25 #include <executorch/runtime/platform/runtime.h>
26 
27 #include <gtest/gtest.h>
28 
29 using namespace ::testing;
30 using executorch::runtime::ArrayRef;
31 using executorch::runtime::Error;
32 using executorch::runtime::EValue;
33 using executorch::runtime::FreeableBuffer;
34 using executorch::runtime::Kernel;
35 using executorch::runtime::KernelKey;
36 using executorch::runtime::KernelRuntimeContext;
37 using executorch::runtime::MemoryAllocator;
38 using executorch::runtime::Method;
39 using executorch::runtime::Program;
40 using executorch::runtime::Result;
41 using executorch::runtime::testing::ManagedMemoryManager;
42 using torch::executor::util::FileDataLoader;
43 
44 constexpr size_t kDefaultNonConstMemBytes = 32 * 1024U;
45 constexpr size_t kDefaultRuntimeMemBytes = 32 * 1024U;
46 
47 /**
48  * Used to control and observe the behavior of a kernel.
49  */
50 struct KernelControl {
51  public:
52   // The number of times the kernel has been called.
53   int call_count = 0;
54 
55   // If true, the kernel should call `context.fail(error_to_set)`. If false,
56   // the kernel should not call `context.fail()`.
57   bool call_context_fail = true;
58 
59   // The error value that the kernel should pass to `context.fail()` before
60   // returning.
61   Error fail_value = Error::Ok;
62 
63   // If true, the kernel should allocate temporary memory.
64   bool allocate_temp_memory = false;
65 
66   // If true, the kernel should simulate allocating temporary memory.
67   bool simulate_temp_memory_allocation = false;
68 
69   // The size of the temporary memory to allocate.
70   int temp_memory_size = 0;
71 
72   // The total size of all allocations.
73   int total_allocated_size = 0;
74 
resetKernelControl75   void reset() {
76     call_count = 0;
77     call_context_fail = false;
78     fail_value = Error::Ok;
79     allocate_temp_memory = false;
80     simulate_temp_memory_allocation = false;
81     temp_memory_size = 0;
82     total_allocated_size = 0;
83   }
84 
85   /**
86    * Registers a kernel that uses the singleton instance to record and control
87    * its behavior.
88    */
register_singletonKernelControl89   static void register_singleton() {
90     if (registered_) {
91       return;
92     }
93 
94     // This test helper installs itself as aten::add.out:
95     //
96     // add.out(Tensor self, Tensor other, *, Scalar alpha=1, Tensor(a!) out) ->
97     //     Tensor(a!)
98     //
99     // The arguments are: `self, other, out, out` (we repeat the out argument in
100     // the program). And since we traced using randn(2, 2), all the args are
101     // Float with dim order (0, 1)
102 
103     // Construct a kernel key with the following meta:
104     // exec_aten::DimOrderType contiguous[] = {0, 1};
105     // TensorMeta float_contiguous[] = {
106     //     TensorMeta(ScalarType::Float, contiguous), // self
107     //     TensorMeta(ScalarType::Float, contiguous), // other
108     //     TensorMeta(ScalarType::Float, contiguous), // out
109     //     TensorMeta(ScalarType::Float, contiguous)}; // out (repeated)
110     KernelKey key =
111         executorch::runtime::KernelKey("v1/6;0,1|6;0,1|6;0,1|6;0,1");
112     Kernel kernel = executorch::runtime::Kernel(
113         "aten::add.out", key, KernelControl::kernel_hook);
114     Error err = executorch::runtime::register_kernel(kernel);
115     EXPECT_EQ(err, Error::Ok);
116 
117     registered_ = true;
118   }
119 
singletonKernelControl120   static KernelControl* singleton() {
121     return &singleton_;
122   }
123 
124  private:
125   /**
126    * An OpFunction-compatible function that uses the singleton KernelControl
127    * to record and determine its behavior.
128    */
kernel_hookKernelControl129   static void kernel_hook(
130       KernelRuntimeContext& context,
131       ET_UNUSED EValue** args) {
132     auto* control = KernelControl::singleton();
133     control->call_count++;
134     if (control->call_context_fail) {
135       context.fail(control->fail_value);
136     }
137 
138     // Allocate temporary memory.
139     if (control->allocate_temp_memory) {
140       Result<void*> temp_mem_res =
141           context.allocate_temp(control->temp_memory_size);
142       if (temp_mem_res.ok()) {
143         control->total_allocated_size += control->temp_memory_size;
144         // We actually use the memory, to test default memory allocation was
145         // successful.
146         uint8_t* array = (uint8_t*)(temp_mem_res.get());
147         for (int i = 0; i < control->temp_memory_size; i++) {
148           array[i] = i % 256;
149         }
150       }
151     }
152 
153     // Simulate allocating temporary memory. We use this, for testing that when
154     // a temp allocator is provided, the kernel will use it, instead of
155     // allocating memory with the default platform memory allocator.
156     // The provided TempMemoryAllocator class in this file, simulates allocating
157     // memory instead of actually allocating anything.
158     if (control->simulate_temp_memory_allocation) {
159       Result<void*> temp_mem_res =
160           context.allocate_temp(control->temp_memory_size);
161       control->total_allocated_size += control->temp_memory_size;
162       EXPECT_EQ(temp_mem_res.error(), Error::Ok);
163     }
164   }
165 
166   static bool registered_;
167   static KernelControl singleton_;
168 };
169 
170 bool KernelControl::registered_ = false;
171 KernelControl KernelControl::singleton_;
172 
173 /**
174  * MemoryAllocator that keeps track of the number/sizes of its allocations,
175  * to test the case where the user provides a temp allocator.
176  */
177 class TempMemoryAllocator final : public MemoryAllocator {
178  public:
TempMemoryAllocator()179   TempMemoryAllocator() : MemoryAllocator(0, nullptr) {}
180 
181   // The number of times allocate() has been called.
182   int number_of_allocations = 0;
183 
184   // The number of times reset() has been called.
185   int number_of_resets = 0;
186 
187   // The amount of memory currently allocated (should go to 0 when reset is
188   // called).
189   int currently_allocated_size = 0;
190 
191   // The total size of all allocations.
192   int total_allocated_size = 0;
193 
allocate(size_t size,ET_UNUSED size_t alignment=kDefaultAlignment)194   void* allocate(size_t size, ET_UNUSED size_t alignment = kDefaultAlignment)
195       override {
196     number_of_allocations += 1;
197     currently_allocated_size += size;
198     total_allocated_size += size;
199     // This is a simulation, we don't actually allocate memory. But we need to
200     // return a non-null pointer, so we return a bad, non-zero address that will
201     // crash if anyone tries to dereference it.
202     return (void*)1;
203   }
204 
reset()205   void reset() override {
206     number_of_resets += 1;
207     currently_allocated_size = 0;
208   }
209 };
210 
211 class KernelIntegrationTest : public ::testing::Test {
212  protected:
SetUp()213   void SetUp() override {
214     executorch::runtime::runtime_init();
215 
216     // Register the controllable kernel hook.
217     KernelControl::register_singleton();
218     // Ensure that its state is clear.
219     KernelControl::singleton()->reset();
220     // Provide the singleton to the tests.
221     control_ = KernelControl::singleton();
222 
223     // Create a loader for the serialized ModuleAdd program.
224     const char* path = std::getenv("ET_MODULE_ADD_PATH");
225     Result<FileDataLoader> loader = FileDataLoader::from(path);
226     ASSERT_EQ(loader.error(), Error::Ok);
227     loader_ = std::make_unique<FileDataLoader>(std::move(loader.get()));
228 
229     // Use it to load the program.
230     Result<Program> program = Program::load(
231         loader_.get(), Program::Verification::InternalConsistency);
232     ASSERT_EQ(program.error(), Error::Ok);
233     program_ = std::make_unique<Program>(std::move(program.get()));
234 
235     // Load the forward method.
236     mmm_ = std::make_unique<ManagedMemoryManager>(
237         kDefaultNonConstMemBytes,
238         kDefaultRuntimeMemBytes,
239         temp_allocator_.get());
240     Result<Method> method = program_->load_method("forward", &mmm_->get());
241     ASSERT_EQ(method.error(), Error::Ok);
242     method_ = std::make_unique<Method>(std::move(method.get()));
243 
244     // Set up its inputs.
245     auto inputs_cleanup =
246         executorch::extension::prepare_input_tensors(*method_);
247     ASSERT_EQ(inputs_cleanup.error(), Error::Ok);
248     inputs_cleanup_ = std::make_unique<executorch::extension::BufferCleanup>(
249         std::move(*inputs_cleanup));
250   }
251 
TearDown()252   void TearDown() override {
253     inputs_cleanup_.reset();
254   }
255 
256  private:
257   // Must outlive program_
258   std::unique_ptr<FileDataLoader> loader_;
259 
260   // Must outlive method_
261   std::unique_ptr<Program> program_;
262   std::unique_ptr<ManagedMemoryManager> mmm_;
263   std::unique_ptr<executorch::extension::BufferCleanup> inputs_cleanup_;
264 
265  protected:
266   // An executable method that will call the kernel associated with control_.
267   // Its inputs will have been allocated and initialized.
268   std::unique_ptr<Method> method_;
269 
270   // The KernelControl associated with method_.
271   KernelControl* control_;
272 
273   // The temp memory allocator provided by the user. By default, none is
274   // provided.
275   std::unique_ptr<TempMemoryAllocator> temp_allocator_ = nullptr;
276 };
277 
278 class KernelTempMemoryAllocatorIntegrationTest : public KernelIntegrationTest {
279  protected:
SetUp()280   void SetUp() override {
281     // Create a temp allocator for the test before calling the parent SetUp.
282     temp_allocator_ = std::make_unique<TempMemoryAllocator>();
283     KernelIntegrationTest::SetUp();
284   }
285 };
286 
TEST_F(KernelIntegrationTest,KernelHookIsCalled)287 TEST_F(KernelIntegrationTest, KernelHookIsCalled) {
288   // Demonstrate that the kernel hook is called in the default state.
289   EXPECT_EQ(control_->call_count, 0);
290   Error err = method_->execute();
291   EXPECT_EQ(err, Error::Ok);
292   EXPECT_EQ(control_->call_count, 1);
293 
294   // Calling it again bumps the count.
295   err = method_->execute();
296   EXPECT_EQ(err, Error::Ok);
297   EXPECT_EQ(control_->call_count, 2);
298 }
299 
TEST_F(KernelIntegrationTest,FailurePropagates)300 TEST_F(KernelIntegrationTest, FailurePropagates) {
301   // Tell the kernel to fail.
302   control_->call_context_fail = true;
303 
304   // We should see the error from the kernel.
305   control_->fail_value = Error::InvalidArgument;
306   Error err = method_->execute();
307   EXPECT_EQ(err, Error::InvalidArgument);
308   EXPECT_EQ(control_->call_count, 1);
309 
310   // Have it fail with a different error to show that it's not a coincidence.
311   control_->fail_value = Error::MemoryAllocationFailed;
312   err = method_->execute();
313   EXPECT_EQ(err, Error::MemoryAllocationFailed);
314   EXPECT_EQ(control_->call_count, 2);
315 
316   // Returning an Ok does not cause the execution to fail.
317   control_->fail_value = Error::Ok;
318   err = method_->execute();
319   EXPECT_EQ(err, Error::Ok);
320   EXPECT_EQ(control_->call_count, 3);
321 }
322 
TEST_F(KernelIntegrationTest,DefaultPlatformMemoryAllocator)323 TEST_F(KernelIntegrationTest, DefaultPlatformMemoryAllocator) {
324   // Tell the kernel to allocate memory. Since no temp allocator is provided,
325   // this will allocate memory using the default platform memory allocator.
326   control_->allocate_temp_memory = true;
327 
328   control_->temp_memory_size = 4;
329   // This is not a simulation. This actually allocates memory, using the
330   // default platform memory allocator.
331   Error err = method_->execute();
332   EXPECT_EQ(err, Error::Ok);
333   EXPECT_EQ(control_->call_count, 1);
334   EXPECT_EQ(control_->total_allocated_size, 4);
335 
336   control_->temp_memory_size = 8;
337   // This is not a simulation. This actually allocates memory, using the
338   // default platform memory allocator.
339   err = method_->execute();
340   EXPECT_EQ(err, Error::Ok);
341   EXPECT_EQ(control_->call_count, 2);
342   EXPECT_EQ(control_->total_allocated_size, 12);
343 }
344 
TEST_F(KernelTempMemoryAllocatorIntegrationTest,UsingTempMemoryAllocator)345 TEST_F(KernelTempMemoryAllocatorIntegrationTest, UsingTempMemoryAllocator) {
346   // In this test we provide a temp allocator to the method, and tell the kernel
347   // to allocate memory using it. We want to make sure that the kernel uses the
348   // temp allocator, and that the temp allocator is reset after the execution.
349   // Since we are testing that the kernel uses the temp allocator, and not the
350   // temp allocator itself, we don't need to test the actual allocation of
351   // memory. Therefore, we set simulate_temp_memory_allocation to true, so that
352   // the kernel will not actually allocate memory, but will instead simulate
353   // allocating memory.
354   // The provided TempMemoryAllocator, simulates allocating memory by increasing
355   // total_allocated_size and currently_allocated_size by the requested size.
356   // We simulate resetting the allocator by setting currently_allocated_size
357   // back to 0.
358   control_->simulate_temp_memory_allocation = true;
359 
360   control_->temp_memory_size = 4;
361   Error err = method_->execute();
362   EXPECT_EQ(err, Error::Ok);
363   EXPECT_EQ(control_->call_count, 1);
364   EXPECT_EQ(control_->total_allocated_size, 4);
365   EXPECT_EQ(temp_allocator_->number_of_allocations, 1);
366   EXPECT_EQ(temp_allocator_->total_allocated_size, 4);
367   // The temp allocator should have been reset after the execution.
368   EXPECT_EQ(temp_allocator_->number_of_resets, 1);
369   EXPECT_EQ(temp_allocator_->currently_allocated_size, 0);
370 
371   control_->temp_memory_size = 8;
372   err = method_->execute();
373   EXPECT_EQ(err, Error::Ok);
374   EXPECT_EQ(control_->call_count, 2);
375   EXPECT_EQ(control_->total_allocated_size, 12);
376   EXPECT_EQ(temp_allocator_->number_of_allocations, 2);
377   EXPECT_EQ(temp_allocator_->total_allocated_size, 12);
378   // The temp allocator should have been reset after the execution.
379   EXPECT_EQ(temp_allocator_->number_of_resets, 2);
380   EXPECT_EQ(temp_allocator_->currently_allocated_size, 0);
381 }
382