xref: /aosp_15_r20/external/executorch/devtools/bundled_program/bundled_program.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 <executorch/devtools/bundled_program/bundled_program.h>
10 
11 #include <cmath>
12 #include <cstddef>
13 #include <cstring>
14 
15 #ifdef USE_ATEN_LIB
16 #include <ATen/ATen.h>
17 #endif // USE_ATEN_LIB
18 
19 #include <executorch/devtools/bundled_program/schema/bundled_program_schema_generated.h>
20 #include <executorch/runtime/core/event_tracer_hooks.h>
21 #include <executorch/runtime/core/exec_aten/util/dim_order_util.h>
22 #include <executorch/runtime/core/memory_allocator.h>
23 #include <executorch/runtime/executor/method.h>
24 #include <executorch/runtime/platform/log.h>
25 
26 using exec_aten::ArrayRef;
27 using exec_aten::Half;
28 using exec_aten::ScalarType;
29 using exec_aten::Tensor;
30 using ::executorch::runtime::Error;
31 using ::executorch::runtime::EValue;
32 using ::executorch::runtime::Method;
33 using ::executorch::runtime::Result;
34 
35 namespace executorch {
36 namespace bundled_program {
37 
38 namespace {
39 
40 constexpr size_t kMaxDim = 16;
41 
42 #ifdef USE_ATEN_LIB
43 
44 // Create an aten tensor with same content using bundled tensor
tensor_like(bundled_program_flatbuffer::Tensor * bundled_tensor)45 at::Tensor tensor_like(bundled_program_flatbuffer::Tensor* bundled_tensor) {
46   ET_CHECK(bundled_tensor->sizes()->size() <= kMaxDim);
47   int64_t ret_t_sizes[kMaxDim];
48 
49   for (size_t i = 0; i < bundled_tensor->sizes()->size(); i++) {
50     ret_t_sizes[i] = static_cast<int64_t>(bundled_tensor->sizes()->data()[i]);
51   }
52 
53   at::Tensor ret_tensor = at::zeros(
54       {ret_t_sizes, bundled_tensor->sizes()->size()},
55       at::dtype(static_cast<ScalarType>(bundled_tensor->scalar_type())));
56   memcpy(
57       ret_tensor.mutable_data_ptr(),
58       static_cast<const void*>(bundled_tensor->data()->Data()),
59       ret_tensor.nbytes());
60   return ret_tensor;
61 }
62 
63 #else // !USE_ATEN_LIB
64 using torch::executor::TensorImpl;
65 // Create a tensorimpl with same content using bundled tensor
impl_like(bundled_program_flatbuffer::Tensor * bundled_tensor)66 TensorImpl impl_like(bundled_program_flatbuffer::Tensor* bundled_tensor) {
67   ScalarType scalar_type =
68       static_cast<ScalarType>(bundled_tensor->scalar_type());
69   ssize_t dim = bundled_tensor->sizes()->size();
70   exec_aten::SizesType* sizes = bundled_tensor->mutable_sizes()->data();
71   void* data = bundled_tensor->mutable_data()->data();
72   exec_aten::DimOrderType* dim_order =
73       bundled_tensor->mutable_dim_order()->data();
74 
75   // The strides of created tensorimpl will only be actually used when
76   // comparsion (`tensor_are_close` below). To eliminate the usage of memory
77   // allocator, here we set the initial strides as null and reconstruct the
78   // stride array as temporary varible when comparsion.
79   exec_aten::StridesType* strides = nullptr;
80   return TensorImpl(scalar_type, dim, sizes, data, dim_order, strides);
81 }
82 #endif
83 
84 /**
85  * Returns true if the two elements are close according to the description on
86  * `tensors_are_close()`.
87  *
88  * T must be a floating point type. Non-floating point data should be compared
89  * directly.
90  */
91 template <
92     typename T,
93     typename = std::enable_if_t<std::is_floating_point<T>::value>>
elem_is_close(const T & ai,const T & bi,double rtol,double atol)94 bool elem_is_close(const T& ai, const T& bi, double rtol, double atol) {
95   if (std::isnan(ai) && std::isnan(bi)) {
96     // NaN == NaN
97   } else if (
98       !std::isfinite(ai) && !std::isfinite(bi) && ((ai > 0) == (bi > 0))) {
99     // -Inf == -Inf
100     // +Inf == +Inf
101   } else if (rtol == 0 && atol == 0) {
102     // Exact comparison; avoid unnecessary math.
103     if (ai != bi) {
104       return false;
105     }
106   } else {
107     auto allowed_error = atol + std::abs(rtol * bi);
108     auto actual_error = std::abs(ai - bi);
109     if (!std::isfinite(actual_error) || actual_error > allowed_error) {
110       return false;
111     }
112   }
113   return true;
114 }
115 
116 template <
117     typename T,
118     typename = std::enable_if_t<std::is_floating_point<T>::value>>
data_is_close(const T * a,const T * b,size_t numel,double rtol,double atol)119 bool data_is_close(
120     const T* a,
121     const T* b,
122     size_t numel,
123     double rtol,
124     double atol) {
125   for (size_t i = 0; i < numel; i++) {
126     if (!elem_is_close(a[i], b[i], rtol, atol)) {
127       return false;
128     }
129   }
130   return true;
131 }
132 
data_is_close_half(const Half * a,const Half * b,size_t numel,double rtol,double atol)133 bool data_is_close_half(
134     const Half* a,
135     const Half* b,
136     size_t numel,
137     double rtol,
138     double atol) {
139   for (size_t i = 0; i < numel; i++) {
140     if (!elem_is_close(
141             static_cast<double>(a[i]), static_cast<double>(b[i]), rtol, atol)) {
142       return false;
143     }
144   }
145   return true;
146 }
147 
tensors_are_close(const Tensor & bundled_tensor,const Tensor & method_output_tensor,double rtol,double atol)148 bool tensors_are_close(
149     const Tensor& bundled_tensor,
150     const Tensor& method_output_tensor,
151     double rtol,
152     double atol) {
153   if (bundled_tensor.scalar_type() != method_output_tensor.scalar_type() ||
154       bundled_tensor.sizes() != method_output_tensor.sizes()) {
155     return false;
156   }
157 
158 #ifdef USE_ATEN_LIB
159 
160   ET_CHECK_MSG(
161       bundled_tensor.strides() == method_output_tensor.strides(),
162       "The two inputs of `tensors_are_close` function shall have same strides");
163 
164 #else // !USE_ATEN_LIB
165 
166   // Contruct stride array for bundled tensor based on its dim order since
167   // strides of bundled_tensor in lean mode is null.
168   exec_aten::StridesType strides[kMaxDim] = {0};
169   auto status = torch::executor::dim_order_to_stride(
170       bundled_tensor.sizes().data(),
171       bundled_tensor.dim_order().data(),
172       bundled_tensor.dim(),
173       strides);
174   ET_CHECK_MSG(
175       status == Error::Ok, "dim_order_to_stride returned invalid status");
176 
177   // TODO(T132992348): support comparison between tensors of different strides
178   ET_CHECK_MSG(
179       ArrayRef<exec_aten::StridesType>(strides, bundled_tensor.dim()) ==
180           method_output_tensor.strides(),
181       "The two inputs of `tensors_are_close` function shall have same strides");
182 #endif
183 
184   // Since the two tensors have same shape and strides, any two elements that
185   // share same index from underlying data perspective will also share same
186   // index from tensor perspective, whatever the size and strides really are.
187   // e.g. if a[i_1, i_2, ... i_n] = a.const_data_ptr()[m], we can assert
188   // b[i_1, i_2, ... i_n] = b.const_data_ptr()[m])
189   // So we can just compare the two underlying data sequentially to figure out
190   // if the two tensors are same.
191 
192   if (bundled_tensor.nbytes() == 0) {
193     // Note that this case is important. It's valid for a zero-size tensor to
194     // have a null data pointer, but in some environments it's invalid to pass a
195     // null pointer to memcmp() even when the size is zero.
196     return true;
197   } else if (bundled_tensor.scalar_type() == ScalarType::Float) {
198     return data_is_close<float>(
199         bundled_tensor.const_data_ptr<float>(),
200         method_output_tensor.const_data_ptr<float>(),
201         bundled_tensor.numel(),
202         rtol,
203         atol);
204   } else if (bundled_tensor.scalar_type() == ScalarType::Double) {
205     return data_is_close<double>(
206         bundled_tensor.const_data_ptr<double>(),
207         method_output_tensor.const_data_ptr<double>(),
208         bundled_tensor.numel(),
209         rtol,
210         atol);
211   } else if (bundled_tensor.scalar_type() == ScalarType::Half) {
212     return data_is_close_half(
213         bundled_tensor.const_data_ptr<Half>(),
214         method_output_tensor.const_data_ptr<Half>(),
215         bundled_tensor.numel(),
216         rtol,
217         atol);
218   } else {
219     // Non-floating-point types can be compared bitwise.
220     return memcmp(
221                bundled_tensor.const_data_ptr(),
222                method_output_tensor.const_data_ptr(),
223                bundled_tensor.nbytes()) == 0;
224   }
225 }
226 
227 Result<bundled_program_flatbuffer::BundledMethodTestSuite*>
get_method_test_suite(const bundled_program_flatbuffer::BundledProgram * bundled_program,Method & method)228 get_method_test_suite(
229     const bundled_program_flatbuffer::BundledProgram* bundled_program,
230     Method& method) {
231   const char* method_name = method.method_meta().name();
232   auto method_test_suites = bundled_program->method_test_suites();
233   for (size_t i = 0; i < method_test_suites->size(); i++) {
234     auto m_test = method_test_suites->GetMutableObject(i);
235     if (std::strcmp(m_test->method_name()->c_str(), method_name) == 0) {
236       return m_test;
237     }
238   }
239   ET_LOG(Error, "No method named '%s' in given bundled program", method_name);
240   return Error::InvalidArgument;
241 }
242 
243 } // namespace
244 
245 // Load testset_idx-th bundled data into the Method
load_bundled_input(Method & method,SerializedBundledProgram * bundled_program_ptr,size_t testset_idx)246 ET_NODISCARD Error load_bundled_input(
247     Method& method,
248     SerializedBundledProgram* bundled_program_ptr,
249     size_t testset_idx) {
250   ET_CHECK_OR_RETURN_ERROR(
251       bundled_program_flatbuffer::BundledProgramBufferHasIdentifier(
252           bundled_program_ptr),
253       NotSupported,
254       "The input buffer should be a bundled program.");
255 
256   auto method_test = get_method_test_suite(
257       bundled_program_flatbuffer::GetBundledProgram(bundled_program_ptr),
258       method);
259 
260   if (!method_test.ok()) {
261     return method_test.error();
262   }
263 
264   auto bundled_inputs =
265       method_test.get()->test_cases()->Get(testset_idx)->inputs();
266 
267   for (size_t input_idx = 0; input_idx < method.inputs_size(); input_idx++) {
268     auto bundled_input = bundled_inputs->GetMutableObject(input_idx);
269 
270     // The EValue variable will contain the info set to input_idx Method input.
271     EValue e_input;
272 
273     // Status for set_input function in this scope.
274     Error status;
275 
276     // Set e_input with bundled_input based on different types.
277     switch (bundled_input->val_type()) {
278       case bundled_program_flatbuffer::ValueUnion::Tensor: {
279         auto bundled_input_tensor =
280             static_cast<bundled_program_flatbuffer::Tensor*>(
281                 bundled_input->mutable_val());
282 
283 #ifdef USE_ATEN_LIB
284         Tensor t = tensor_like(bundled_input_tensor);
285 #else // !USE_ATEN_LIB
286         TensorImpl impl = impl_like(bundled_input_tensor);
287         Tensor t = Tensor(&impl);
288 #endif
289         // Use t to create EValue as Method's input.
290         e_input = EValue(t);
291         // Setting input like this is safe because the `set_input` function only
292         // copies the underlying data blob of TensorImpl impl into the method,
293         // not the pointer of impl, Tensor t or even the Evalue e_input. So
294         // their lifetime will not impact the safety. Also there's a specific
295         // memory space with enough lifetime holding the underlying data blob,
296         // so the lifetime of the data blob is not an issue.
297         status = method.set_input(e_input, input_idx);
298         break;
299       }
300       case bundled_program_flatbuffer::ValueUnion::Int: {
301         auto bundled_input_int = bundled_input->val_as_Int();
302         e_input = EValue(bundled_input_int->int_val());
303         status = method.set_input(e_input, input_idx);
304         break;
305       }
306       case bundled_program_flatbuffer::ValueUnion::Double: {
307         auto bundled_input_int = bundled_input->val_as_Double();
308         e_input = EValue(bundled_input_int->double_val());
309         status = method.set_input(e_input, input_idx);
310         break;
311       }
312       case bundled_program_flatbuffer::ValueUnion::Bool: {
313         auto bundled_input_int = bundled_input->val_as_Bool();
314         e_input = EValue(bundled_input_int->bool_val());
315         status = method.set_input(e_input, input_idx);
316         break;
317       }
318       default: {
319         ET_CHECK_OR_RETURN_ERROR(
320             false,
321             NotSupported,
322             "Data type %hhu not supported",
323             static_cast<uint8_t>(bundled_input->val_type()));
324         break;
325       }
326     }
327 
328     ET_CHECK_OR_RETURN_ERROR(
329         status == Error::Ok,
330         NotSupported,
331         "set_input failed during load bundled inputs with status 0%" PRIx32,
332         static_cast<uint32_t>(status));
333   }
334 
335   ::executorch::runtime::internal::event_tracer_set_bundled_input_index(
336       method.get_event_tracer(), testset_idx);
337 
338   return Error::Ok;
339 }
340 
verify_method_outputs(Method & method,SerializedBundledProgram * bundled_program_ptr,size_t testset_idx,double rtol,double atol)341 ET_NODISCARD Error verify_method_outputs(
342     Method& method,
343     SerializedBundledProgram* bundled_program_ptr,
344     size_t testset_idx,
345     double rtol,
346     double atol) {
347   ET_CHECK_OR_RETURN_ERROR(
348       bundled_program_flatbuffer::BundledProgramBufferHasIdentifier(
349           bundled_program_ptr),
350       NotSupported,
351       "The input buffer should be a bundled program.");
352 
353   auto method_test = get_method_test_suite(
354       bundled_program_flatbuffer::GetBundledProgram(bundled_program_ptr),
355       method);
356 
357   if (!method_test.ok()) {
358     return method_test.error();
359   }
360 
361   auto bundled_expected_outputs =
362       method_test.get()->test_cases()->Get(testset_idx)->expected_outputs();
363 
364   for (size_t output_idx = 0; output_idx < method.outputs_size();
365        output_idx++) {
366     auto bundled_expected_output =
367         bundled_expected_outputs->GetMutableObject(output_idx);
368     auto method_output = method.get_output(output_idx);
369     switch (bundled_expected_output->val_type()) {
370       case bundled_program_flatbuffer::ValueUnion::Tensor: {
371         auto bundled_expected_output_tensor =
372             static_cast<bundled_program_flatbuffer::Tensor*>(
373                 bundled_expected_output->mutable_val());
374         const auto method_output_tensor = method_output.toTensor();
375 
376 #ifdef USE_ATEN_LIB
377         Tensor t = tensor_like(bundled_expected_output_tensor);
378 #else // !USE_ATEN_LIB
379         TensorImpl impl = impl_like(bundled_expected_output_tensor);
380         Tensor t = Tensor(&impl);
381 #endif
382         ET_CHECK_OR_RETURN_ERROR(
383             tensors_are_close(t, method_output_tensor, rtol, atol),
384             NotFound, // maybe some new error tag?
385             "Method's output data mismatched the expected one.");
386         break;
387       }
388       default: {
389         ET_CHECK_OR_RETURN_ERROR(
390             false,
391             NotSupported,
392             "Data type %hhd not supported",
393             static_cast<uint8_t>(bundled_expected_output->val_type()));
394         break;
395       }
396     }
397   }
398 
399   return Error::Ok;
400 }
401 
get_program_data(void * file_data,size_t file_data_len,const void ** out_program_data,size_t * out_program_data_len)402 ET_NODISCARD Error get_program_data(
403     void* file_data,
404     size_t file_data_len,
405     const void** out_program_data,
406     size_t* out_program_data_len) {
407   if (is_bundled_program(file_data, file_data_len)) {
408     auto program_bundled =
409         bundled_program_flatbuffer::GetBundledProgram(file_data);
410     *out_program_data = program_bundled->program()->data();
411     *out_program_data_len = program_bundled->program()->size();
412   } else {
413     ET_LOG(
414         Error,
415         "Unrecognized bundled program flatbuffer identifier '%.4s'",
416         flatbuffers::GetBufferIdentifier(file_data));
417     return Error::NotSupported;
418   }
419   return Error::Ok;
420 }
421 
is_bundled_program(void * file_data,ET_UNUSED size_t file_data_len)422 bool is_bundled_program(void* file_data, ET_UNUSED size_t file_data_len) {
423   // Even though the flatbuffer API doesn't accept a length, it's important to
424   // require one so that we could change the internal representation, or use a
425   // future API that does require a length.
426   return bundled_program_flatbuffer::BundledProgramBufferHasIdentifier(
427       file_data);
428 }
429 
430 } // namespace bundled_program
431 } // namespace executorch
432