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