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