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