• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Bundled Program -- a Tool for ExecuTorch Model Validation
2
3## Introduction
4`BundledProgram` is a wrapper around the core ExecuTorch program designed to help users wrapping test cases with the model they deploy. `BundledProgram` is not necessarily a core part of the program and not needed for its execution, but is particularly important for various other use-cases, such as model correctness evaluation, including e2e testing during the model bring-up process.
5
6Overall, the procedure can be broken into two stages, and in each stage we are supporting:
7
8* **Emit stage**: Bundling the test I/O cases along with the ExecuTorch program, serializing into flatbuffer.
9* **Runtime stage**: Accessing, executing, and verifying the bundled test cases during runtime.
10
11## Emit stage
12This stage mainly focuses on the creation of a `BundledProgram` and dumping it out to the disk as a flatbuffer file. The main procedure is as follow:
131. Create a model and emit its ExecuTorch program.
142. Construct a `List[MethodTestSuite]` to record all test cases that needs to be bundled.
153. Generate `BundledProgram` by using the emited model and `List[MethodTestSuite]`.
164. Serialize the `BundledProgram` and dump it out to the disk.
17
18### Step 1: Create a Model and Emit its ExecuTorch Program.
19
20ExecuTorch Program can be emitted from user's model by using ExecuTorch APIs. Follow the [Generate Sample ExecuTorch program](./getting-started-setup.md) or [Exporting to ExecuTorch tutorial](./tutorials/export-to-executorch-tutorial).
21
22### Step 2: Construct `List[MethodTestSuite]` to hold test info
23
24In `BundledProgram`, we create two new classes, `MethodTestCase` and `MethodTestSuite`, to hold essential info for ExecuTorch program verification.
25
26`MethodTestCase` represents a single testcase. Each `MethodTestCase` contains inputs and expected outputs for a single execution.
27
28:::{dropdown} `MethodTestCase`
29
30```{eval-rst}
31.. autofunction:: executorch.devtools.bundled_program.config.MethodTestCase.__init__
32    :noindex:
33```
34:::
35
36`MethodTestSuite` contains all testing info for single method, including a str representing method name, and a `List[MethodTestCase]` for all testcases:
37
38:::{dropdown} `MethodTestSuite`
39
40```{eval-rst}
41.. autofunction:: executorch.devtools.bundled_program.config.MethodTestSuite
42    :noindex:
43```
44:::
45
46Since each model may have multiple inference methods, we need to generate `List[MethodTestSuite]` to hold all essential infos.
47
48
49### Step 3: Generate `BundledProgram`
50
51We provide `BundledProgram` class under `executorch/devtools/bundled_program/core.py` to bundled the `ExecutorchProgram`-like variable, including
52                            `ExecutorchProgram`, `MultiMethodExecutorchProgram` or `ExecutorchProgramManager`, with the `List[MethodTestSuite]`:
53
54:::{dropdown} `BundledProgram`
55
56```{eval-rst}
57.. autofunction:: executorch.devtools.bundled_program.core.BundledProgram.__init__
58    :noindex:
59```
60:::
61
62Construtor of `BundledProgram `will do sannity check internally to see if the given `List[MethodTestSuite]` matches the given Program's requirements. Specifically:
631. The method_names of each `MethodTestSuite` in `List[MethodTestSuite]` for should be also in program. Please notice that it is no need to set testcases for every method in the Program.
642. The metadata of each testcase should meet the requirement of the coresponding inference methods input.
65
66### Step 4: Serialize `BundledProgram` to Flatbuffer.
67
68To serialize `BundledProgram` to make runtime APIs use it, we provide two APIs, both under `executorch/devtools/bundled_program/serialize/__init__.py`.
69
70:::{dropdown} Serialize and Deserialize
71
72```{eval-rst}
73.. currentmodule:: executorch.devtools.bundled_program.serialize
74.. autofunction:: serialize_from_bundled_program_to_flatbuffer
75    :noindex:
76```
77
78```{eval-rst}
79.. currentmodule:: executorch.devtools.bundled_program.serialize
80.. autofunction:: deserialize_from_flatbuffer_to_bundled_program
81    :noindex:
82```
83:::
84
85### Emit Example
86
87Here is a flow highlighting how to generate a `BundledProgram` given a PyTorch model and the representative inputs we want to test it along with.
88
89```python
90import torch
91
92from executorch.exir import to_edge
93from executorch.devtools import BundledProgram
94
95from executorch.devtools.bundled_program.config import MethodTestCase, MethodTestSuite
96from executorch.devtools.bundled_program.serialize import (
97    serialize_from_bundled_program_to_flatbuffer,
98)
99from torch.export import export, export_for_training
100
101
102# Step 1: ExecuTorch Program Export
103class SampleModel(torch.nn.Module):
104    """An example model with multi-methods. Each method has multiple input and single output"""
105
106    def __init__(self) -> None:
107        super().__init__()
108        self.a: torch.Tensor = 3 * torch.ones(2, 2, dtype=torch.int32)
109        self.b: torch.Tensor = 2 * torch.ones(2, 2, dtype=torch.int32)
110
111    def forward(self, x: torch.Tensor, q: torch.Tensor) -> torch.Tensor:
112        z = x.clone()
113        torch.mul(self.a, x, out=z)
114        y = x.clone()
115        torch.add(z, self.b, out=y)
116        torch.add(y, q, out=y)
117        return y
118
119
120# Inference method name of SampleModel we want to bundle testcases to.
121# Notices that we do not need to bundle testcases for every inference methods.
122method_name = "forward"
123model = SampleModel()
124
125# Inputs for graph capture.
126capture_input = (
127    (torch.rand(2, 2) - 0.5).to(dtype=torch.int32),
128    (torch.rand(2, 2) - 0.5).to(dtype=torch.int32),
129)
130
131# Export method's FX Graph.
132method_graph = export(
133    export_for_training(model, capture_input).module(),
134    capture_input,
135)
136
137
138# Emit the traced method into ET Program.
139et_program = to_edge(method_graph).to_executorch()
140
141# Step 2: Construct MethodTestSuite for Each Method
142
143# Prepare the Test Inputs.
144
145# Number of input sets to be verified
146n_input = 10
147
148# Input sets to be verified.
149inputs = [
150    # Each list below is a individual input set.
151    # The number of inputs, dtype and size of each input follow Program's spec.
152    [
153        (torch.rand(2, 2) - 0.5).to(dtype=torch.int32),
154        (torch.rand(2, 2) - 0.5).to(dtype=torch.int32),
155    ]
156    for _ in range(n_input)
157]
158
159# Generate Test Suites
160method_test_suites = [
161    MethodTestSuite(
162        method_name=method_name,
163        test_cases=[
164            MethodTestCase(
165                inputs=input,
166                expected_outputs=(getattr(model, method_name)(*input), ),
167            )
168            for input in inputs
169        ],
170    ),
171]
172
173# Step 3: Generate BundledProgram
174bundled_program = BundledProgram(et_program, method_test_suites)
175
176# Step 4: Serialize BundledProgram to flatbuffer.
177serialized_bundled_program = serialize_from_bundled_program_to_flatbuffer(
178    bundled_program
179)
180save_path = "bundled_program.bpte"
181with open(save_path, "wb") as f:
182    f.write(serialized_bundled_program)
183
184```
185
186We can also regenerate `BundledProgram` from flatbuffer file if needed:
187
188```python
189from executorch.devtools.bundled_program.serialize import deserialize_from_flatbuffer_to_bundled_program
190save_path = "bundled_program.bpte"
191with open(save_path, "rb") as f:
192    serialized_bundled_program = f.read()
193
194regenerate_bundled_program = deserialize_from_flatbuffer_to_bundled_program(serialized_bundled_program)
195```
196
197## Runtime Stage
198This stage mainly focuses on executing the model with the bundled inputs and and comparing the model's output with the bundled expected output. We provide multiple APIs to handle the key parts of it.
199
200
201### Get ExecuTorch Program Pointer from `BundledProgram` Buffer
202We need the pointer to ExecuTorch program to do the execution. To unify the process of loading and executing `BundledProgram` and Program flatbuffer, we create an API:
203
204:::{dropdown} `get_program_data`
205
206```{eval-rst}
207.. doxygenfunction:: ::executorch::bundled_program::get_program_data
208```
209:::
210
211Here's an example of how to use the `get_program_data` API:
212```c++
213// Assume that the user has read the contents of the file into file_data using
214// whatever method works best for their application. The file could contain
215// either BundledProgram data or Program data.
216void* file_data = ...;
217size_t file_data_len = ...;
218
219// If file_data contains a BundledProgram, get_program_data() will return a
220// pointer to the Program data embedded inside it. Otherwise it will return
221// file_data, which already pointed to Program data.
222const void* program_ptr;
223size_t program_len;
224status = executorch::bundled_program::get_program_data(
225    file_data, file_data_len, &program_ptr, &program_len);
226ET_CHECK_MSG(
227    status == Error::Ok,
228    "get_program_data() failed with status 0x%" PRIx32,
229    status);
230```
231
232### Load Bundled Input to Method
233To execute the program on the bundled input, we need to load the bundled input into the method. Here we provided an API called `executorch::bundled_program::load_bundled_input`:
234
235:::{dropdown} `load_bundled_input`
236
237```{eval-rst}
238.. doxygenfunction:: ::executorch::bundled_program::load_bundled_input
239```
240:::
241
242### Verify the Method's Output.
243We call `executorch::bundled_program::verify_method_outputs` to verify the method's output with bundled expected outputs. Here's the details of this API:
244
245:::{dropdown} `verify_method_outputs`
246
247```{eval-rst}
248.. doxygenfunction:: ::executorch::bundled_program::verify_method_outputs
249```
250:::
251
252
253### Runtime Example
254
255Here we provide an example about how to run the bundled program step by step. Most of the code is borrowed from [executor_runner](https://github.com/pytorch/executorch/blob/main/examples/devtools/example_runner/example_runner.cpp), and please review that file if you need more info and context:
256
257```c++
258// method_name is the name for the method we want to test
259// memory_manager is the executor::MemoryManager variable for executor memory allocation.
260// program is the ExecuTorch program.
261Result<Method> method = program->load_method(method_name, &memory_manager);
262
263ET_CHECK_MSG(
264    method.ok(),
265    "load_method() failed with status 0x%" PRIx32,
266    method.error());
267
268// Load testset_idx-th input in the buffer to plan
269status = executorch::bundled_program::load_bundled_input(
270        *method,
271        program_data.bundled_program_data(),
272        FLAGS_testset_idx);
273ET_CHECK_MSG(
274    status == Error::Ok,
275    "load_bundled_input failed with status 0x%" PRIx32,
276    status);
277
278// Execute the plan
279status = method->execute();
280ET_CHECK_MSG(
281    status == Error::Ok,
282    "method->execute() failed with status 0x%" PRIx32,
283    status);
284
285// Verify the result.
286status = executorch::bundled_program::verify_method_outputs(
287        *method,
288        program_data.bundled_program_data(),
289        FLAGS_testset_idx,
290        FLAGS_rtol,
291        FLAGS_atol);
292ET_CHECK_MSG(
293    status == Error::Ok,
294    "Bundle verification failed with status 0x%" PRIx32,
295    status);
296
297```
298
299## Common Errors
300
301Errors will be raised if `List[MethodTestSuites]` doesn't match the `Program`. Here're two common situations:
302
303### Test input doesn't match model's requirement.
304
305Each inference method of PyTorch model has its own requirement for the inputs, like number of input, the dtype of each input, etc. `BundledProgram` will raise error if test input not meet the requirement.
306
307Here's the example of the dtype of test input not meet model's requirement:
308
309```python
310import torch
311
312from executorch.exir import to_edge
313from executorch.devtools import BundledProgram
314
315from executorch.devtools.bundled_program.config import MethodTestCase, MethodTestSuite
316from torch.export import export, export_for_training
317
318
319class Module(torch.nn.Module):
320    def __init__(self):
321        super().__init__()
322        self.a = 3 * torch.ones(2, 2, dtype=torch.float)
323        self.b = 2 * torch.ones(2, 2, dtype=torch.float)
324
325    def forward(self, x):
326        out_1 = torch.ones(2, 2, dtype=torch.float)
327        out_2 = torch.ones(2, 2, dtype=torch.float)
328        torch.mul(self.a, x, out=out_1)
329        torch.add(out_1, self.b, out=out_2)
330        return out_2
331
332
333model = Module()
334method_names = ["forward"]
335
336inputs = (torch.ones(2, 2, dtype=torch.float), )
337
338# Find each method of model needs to be traced my its name, export its FX Graph.
339method_graph = export(
340    export_for_training(model, inputs).module(),
341    inputs,
342)
343
344# Emit the traced methods into ET Program.
345et_program = to_edge(method_graph).to_executorch()
346
347# number of input sets to be verified
348n_input = 10
349
350# Input sets to be verified for each inference methods.
351# To simplify, here we create same inputs for all methods.
352inputs = {
353    # Inference method name corresponding to its test cases.
354    m_name: [
355        # NOTE: executorch program needs torch.float, but here is torch.int
356        [
357            torch.randint(-5, 5, (2, 2), dtype=torch.int),
358        ]
359        for _ in range(n_input)
360    ]
361    for m_name in method_names
362}
363
364# Generate Test Suites
365method_test_suites = [
366    MethodTestSuite(
367        method_name=m_name,
368        test_cases=[
369            MethodTestCase(
370                inputs=input,
371                expected_outputs=(getattr(model, m_name)(*input),),
372            )
373            for input in inputs[m_name]
374        ],
375    )
376    for m_name in method_names
377]
378
379# Generate BundledProgram
380
381bundled_program = BundledProgram(et_program, method_test_suites)
382```
383
384:::{dropdown} Raised Error
385
386```
387The input tensor tensor([[-2,  0],
388        [-2, -1]], dtype=torch.int32) dtype shall be torch.float32, but now is torch.int32
389---------------------------------------------------------------------------
390AssertionError                            Traceback (most recent call last)
391Cell In[1], line 72
392     56 method_test_suites = [
393     57     MethodTestSuite(
394     58         method_name=m_name,
395   (...)
396     67     for m_name in method_names
397     68 ]
398     70 # Step 3: Generate BundledProgram
399---> 72 bundled_program = create_bundled_program(program, method_test_suites)
400File /executorch/devtools/bundled_program/core.py:276, in create_bundled_program(program, method_test_suites)
401    264 """Create bp_schema.BundledProgram by bundling the given program and method_test_suites together.
402    265
403    266 Args:
404   (...)
405    271     The `BundledProgram` variable contains given ExecuTorch program and test cases.
406    272 """
407    274 method_test_suites = sorted(method_test_suites, key=lambda x: x.method_name)
408--> 276 assert_valid_bundle(program, method_test_suites)
409    278 bundled_method_test_suites: List[bp_schema.BundledMethodTestSuite] = []
410    280 # Emit data and metadata of bundled tensor
411File /executorch/devtools/bundled_program/core.py:219, in assert_valid_bundle(program, method_test_suites)
412    215 # type of tensor input should match execution plan
413    216 if type(cur_plan_test_inputs[j]) == torch.Tensor:
414    217     # pyre-fixme[16]: Undefined attribute [16]: Item `bool` of `typing.Union[bool, float, int, torch._tensor.Tensor]`
415    218     # has no attribute `dtype`.
416--> 219     assert cur_plan_test_inputs[j].dtype == get_input_dtype(
417    220         program, program_plan_id, j
418    221     ), "The input tensor {} dtype shall be {}, but now is {}".format(
419    222         cur_plan_test_inputs[j],
420    223         get_input_dtype(program, program_plan_id, j),
421    224         cur_plan_test_inputs[j].dtype,
422    225     )
423    226 elif type(cur_plan_test_inputs[j]) in (
424    227     int,
425    228     bool,
426    229     float,
427    230 ):
428    231     assert type(cur_plan_test_inputs[j]) == get_input_type(
429    232         program, program_plan_id, j
430    233     ), "The input primitive dtype shall be {}, but now is {}".format(
431    234         get_input_type(program, program_plan_id, j),
432    235         type(cur_plan_test_inputs[j]),
433    236     )
434AssertionError: The input tensor tensor([[-2,  0],
435        [-2, -1]], dtype=torch.int32) dtype shall be torch.float32, but now is torch.int32
436
437```
438
439:::
440
441### Method name in `BundleConfig` does not exist.
442
443Another common error would be the method name in any `MethodTestSuite` does not exist in Model. `BundledProgram` will raise error and show the non-exist method name:
444
445```python
446import torch
447
448from executorch.exir import to_edge
449from executorch.devtools import BundledProgram
450
451from executorch.devtools.bundled_program.config import MethodTestCase, MethodTestSuite
452from torch.export import export, export_for_training
453
454
455class Module(torch.nn.Module):
456    def __init__(self):
457        super().__init__()
458        self.a = 3 * torch.ones(2, 2, dtype=torch.float)
459        self.b = 2 * torch.ones(2, 2, dtype=torch.float)
460
461    def forward(self, x):
462        out_1 = torch.ones(2, 2, dtype=torch.float)
463        out_2 = torch.ones(2, 2, dtype=torch.float)
464        torch.mul(self.a, x, out=out_1)
465        torch.add(out_1, self.b, out=out_2)
466        return out_2
467
468
469model = Module()
470method_names = ["forward"]
471
472inputs = (torch.ones(2, 2, dtype=torch.float),)
473
474# Find each method of model needs to be traced my its name, export its FX Graph.
475method_graph = export(
476    export_for_training(model, inputs).module(),
477    inputs,
478)
479
480# Emit the traced methods into ET Program.
481et_program = to_edge(method_graph).to_executorch()
482
483# number of input sets to be verified
484n_input = 10
485
486# Input sets to be verified for each inference methods.
487# To simplify, here we create same inputs for all methods.
488inputs = {
489    # Inference method name corresponding to its test cases.
490    m_name: [
491        [
492            torch.randint(-5, 5, (2, 2), dtype=torch.float),
493        ]
494        for _ in range(n_input)
495    ]
496    for m_name in method_names
497}
498
499# Generate Test Suites
500method_test_suites = [
501    MethodTestSuite(
502        method_name=m_name,
503        test_cases=[
504            MethodTestCase(
505                inputs=input,
506                expected_outputs=(getattr(model, m_name)(*input),),
507            )
508            for input in inputs[m_name]
509        ],
510    )
511    for m_name in method_names
512]
513
514# NOTE: MISSING_METHOD_NAME is not an inference method in the above model.
515method_test_suites[0].method_name = "MISSING_METHOD_NAME"
516
517# Generate BundledProgram
518bundled_program = BundledProgram(et_program, method_test_suites)
519
520```
521
522:::{dropdown} Raised Error
523
524```
525All method names in bundled config should be found in program.execution_plan,          but {'MISSING_METHOD_NAME'} does not include.
526---------------------------------------------------------------------------
527AssertionError                            Traceback (most recent call last)
528Cell In[3], line 73
529     70 method_test_suites[0].method_name = "MISSING_METHOD_NAME"
530     72 # Generate BundledProgram
531---> 73 bundled_program = create_bundled_program(program, method_test_suites)
532File /executorch/devtools/bundled_program/core.py:276, in create_bundled_program(program, method_test_suites)
533    264 """Create bp_schema.BundledProgram by bundling the given program and method_test_suites together.
534    265
535    266 Args:
536   (...)
537    271     The `BundledProgram` variable contains given ExecuTorch program and test cases.
538    272 """
539    274 method_test_suites = sorted(method_test_suites, key=lambda x: x.method_name)
540--> 276 assert_valid_bundle(program, method_test_suites)
541    278 bundled_method_test_suites: List[bp_schema.BundledMethodTestSuite] = []
542    280 # Emit data and metadata of bundled tensor
543File /executorch/devtools/bundled_program/core.py:141, in assert_valid_bundle(program, method_test_suites)
544    138 method_name_of_program = {e.name for e in program.execution_plan}
545    139 method_name_of_test_suites = {t.method_name for t in method_test_suites}
546--> 141 assert method_name_of_test_suites.issubset(
547    142     method_name_of_program
548    143 ), f"All method names in bundled config should be found in program.execution_plan, \
549    144      but {str(method_name_of_test_suites - method_name_of_program)} does not include."
550    146 # check if method_tesdt_suites has been sorted in ascending alphabetical order of method name.
551    147 for test_suite_id in range(1, len(method_test_suites)):
552AssertionError: All method names in bundled config should be found in program.execution_plan,          but {'MISSING_METHOD_NAME'} does not include.
553```
554:::
555