|
1 |
| -# BundledIO |
| 1 | +# Bundled Program |
2 | 2 |
|
3 |
| -TBA |
| 3 | +## Introduction |
| 4 | +Bundled Program is a wrapper around the core ExecuTorch program designed to help users wrapping test cases and other related info with the models they deploy. Bundled Program is not necessarily a core part of the program and not needed for its execution but is more necessary for various other use-cases, especially for model correctness evaluation such as e2e testing during model bring-up etc. |
| 5 | + |
| 6 | +Overall procedure can be broken into two stages, and in each stage we are supporting: |
| 7 | +* **Emit stage**: Bundling test I/O cases as well as other useful info in key-value pairs along with the ExecuTorch program. |
| 8 | +* **Runtime stage**: Accessing, executing and verifying the bundled test cases during runtime. |
| 9 | + |
| 10 | +## Emit stage |
| 11 | + |
| 12 | + This stage mainly focuses on the creation of a BundledProgram, and dump it out to the disk as a flatbuffer file. The main procedure is as follow: |
| 13 | +1. Create a model and emit its executorch program. |
| 14 | +2. Construct a BundledConfig to record all info need to be bundled. |
| 15 | +3. Generate BundledProgram using the emited model and BundledProgram |
| 16 | +4. Serialize the BundledProgram and dump it out to the disk. |
| 17 | + |
| 18 | +### Step 1: Create a model and emit its executorch program. |
| 19 | + |
| 20 | +This is not the part BunledProgram focusing on, so we just give left an example here without detailed APIs usage. Most of the example is borrowed from bundled_program/tests/common.py: |
| 21 | + |
| 22 | +```python |
| 23 | + |
| 24 | +import torch |
| 25 | +from executorch import exir |
| 26 | +from executorch.exir import ExecutorchBackendConfig |
| 27 | +from executorch.exir.passes import MemoryPlanningPass, ToOutVarPass |
| 28 | + |
| 29 | + |
| 30 | +class SampleModel(torch.nn.Module): |
| 31 | + """An example model with multi-methods. Each method has multiple input and single output""" |
| 32 | + |
| 33 | + def __init__(self) -> None: |
| 34 | + super().__init__() |
| 35 | + self.a: torch.Tensor = 3 * torch.ones(2, 2, dtype=torch.int32) |
| 36 | + self.b: torch.Tensor = 2 * torch.ones(2, 2, dtype=torch.int32) |
| 37 | + |
| 38 | + def encode( |
| 39 | + self, x: torch.Tensor, q: torch.Tensor |
| 40 | + ) -> torch.Tensor: |
| 41 | + z = x.clone() |
| 42 | + torch.mul(self.a, x, out=z) |
| 43 | + y = x.clone() |
| 44 | + torch.add(z, self.b, out=y) |
| 45 | + torch.add(y, q, out=y) |
| 46 | + return y |
| 47 | + |
| 48 | + def decode( |
| 49 | + self, x: torch.Tensor, q: torch.Tensor |
| 50 | + ) -> torch.Tensor: |
| 51 | + y = x * q |
| 52 | + torch.add(y, self.b, out=y) |
| 53 | + return y |
| 54 | + |
| 55 | + |
| 56 | +method_names = ["encode", "decode"] |
| 57 | +model = SampleModel() |
| 58 | + |
| 59 | +capture_inputs = { |
| 60 | + m_name: ( |
| 61 | + (torch.rand(2, 2) - 0.5).to(dtype=torch.int32), |
| 62 | + (torch.rand(2, 2) - 0.5).to(dtype=torch.int32), |
| 63 | + ) |
| 64 | + for m_name in method_names |
| 65 | +} |
| 66 | + |
| 67 | +# Trace to FX Graph and emit the program |
| 68 | +program = ( |
| 69 | + exir.capture_multiple(model, capture_inputs) |
| 70 | + .to_edge() |
| 71 | + .to_executorch() |
| 72 | + .program |
| 73 | +) |
| 74 | + |
| 75 | +``` |
| 76 | + |
| 77 | +### Step 2: Construct BundledConfig |
| 78 | + |
| 79 | +BundledConfig is a class under `executorch/bundled_program/config.py` that contains all information needs to be bundled for model verification. Here's the constructor api to create BundledConfig: |
| 80 | + |
| 81 | +```python |
| 82 | +class BundledConfig: |
| 83 | + def __init__( |
| 84 | + self, |
| 85 | + method_names: List[str], |
| 86 | + inputs: List[List[Any]], |
| 87 | + expected_outputs: List[List[Any]], |
| 88 | + ) -> None: |
| 89 | + """Contruct the config given inputs and expected outputs |
| 90 | +
|
| 91 | + Args: |
| 92 | + method_names: All method names need to be verified in program. |
| 93 | + inputs: All sets of input need to be test on for all methods. Each list |
| 94 | + of `inputs` is all sets which will be run on the method in the |
| 95 | + program with corresponding method name. Each set of any `inputs` element should |
| 96 | + contain all inputs required by eager_model with the same inference function |
| 97 | + as corresponding execution plan for one-time execution. |
| 98 | +
|
| 99 | + expected_outputs: Expected outputs for inputs sharing same index. The size of |
| 100 | + expected_outputs should be the same as the size of inputs and provided method_names. |
| 101 | + """ |
| 102 | + |
| 103 | +``` |
| 104 | + |
| 105 | +Here's an example of creating a bundled program for SampleModel above: |
| 106 | + |
| 107 | +```python |
| 108 | + |
| 109 | +from executorch.bundled_program.config import BundledConfig |
| 110 | + |
| 111 | +# number of input sets needed to be verified |
| 112 | +n_input = 10 |
| 113 | + |
| 114 | +# All Input sets need to be verified for all execution plans. |
| 115 | +inputs = [ |
| 116 | + # The below list is all inputs for a single execution plan (inference method). |
| 117 | + [ |
| 118 | + # Each list below is a individual input set. |
| 119 | + # The number of inputs, dtype and size of each input follow Program's spec. |
| 120 | + [ |
| 121 | + (torch.rand(2, 2) - 0.5).to(dtype=torch.int32), |
| 122 | + (torch.rand(2, 2) - 0.5).to(dtype=torch.int32), |
| 123 | + ] |
| 124 | + for _ in range(n_input) |
| 125 | + ] |
| 126 | + for _ in range(len(program.execution_plan)) |
| 127 | +] |
| 128 | + |
| 129 | +# Expected outputs align with inputs. |
| 130 | +expected_outputs = [ |
| 131 | + [[getattr(model, m_name)(*x)] for x in inputs[i]] |
| 132 | + for i, m_name in enumerate(method_names) |
| 133 | +] |
| 134 | + |
| 135 | + |
| 136 | +bundled_config = BundledConfig( |
| 137 | + method_names, inputs, expected_outputs |
| 138 | +) |
| 139 | + |
| 140 | +``` |
| 141 | + |
| 142 | +### Step 3: Generate BundledProgram |
| 143 | + |
| 144 | +To create BundledProgram, we provice `create_bundled_program` under `executorch/bundled_program/core.py` to generate BundledProgram by bundling the emitted executorch program with the bundled_config: |
| 145 | + |
| 146 | +```python |
| 147 | + |
| 148 | +def create_bundled_program( |
| 149 | + program: Program, |
| 150 | + bundled_config: BundledConfig, |
| 151 | +) -> BundledProgram: |
| 152 | + """ |
| 153 | + Args: |
| 154 | + program: The program to be bundled. |
| 155 | + bundled_config: The config to be bundled. |
| 156 | + """ |
| 157 | +``` |
| 158 | + |
| 159 | +Example: |
| 160 | + |
| 161 | +```python |
| 162 | +from executorch.bundled_program.core import create_bundled_program |
| 163 | + |
| 164 | +bundled_program = create_bundled_program(program, bundled_config) |
| 165 | +``` |
| 166 | + |
| 167 | +### Step 4: Serialize BundledProgram to Flatbuffer. |
| 168 | + |
| 169 | +To serialize BundledProgram to make runtime APIs use it, we provide two APIs, both under `executorch/bundled_program/serialize/__init__.py`. |
| 170 | + |
| 171 | +Serialize BundledProgram to flatbuffer: |
| 172 | + |
| 173 | +```python |
| 174 | +def serialize_from_bundled_program_to_flatbuffer( |
| 175 | + bundled_program: BundledProgram, |
| 176 | +) -> bytes |
| 177 | +``` |
| 178 | + |
| 179 | +Deserialize flatbuffer to BundledProgram: |
| 180 | + |
| 181 | +```python |
| 182 | +def deserialize_from_flatbuffer_to_bundled_program( |
| 183 | + flatbuffer: bytes |
| 184 | +) -> BundledProgram |
| 185 | +``` |
| 186 | + |
| 187 | +Example: |
| 188 | +```python |
| 189 | +from executorch.bundled_program.serialize import ( |
| 190 | + serialize_from_bundled_program_to_flatbuffer, |
| 191 | + deserialize_from_flatbuffer_to_bundled_program, |
| 192 | +) |
| 193 | + |
| 194 | +serialized_bundled_program = serialize_from_bundled_program_to_flatbuffer(bundled_program) |
| 195 | +regenerate_bundled_program = deserialize_from_flatbuffer_to_bundled_program(serialized_bundled_program) |
| 196 | + |
| 197 | +``` |
| 198 | + |
| 199 | +## Runtime Stage |
| 200 | +This 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. |
| 201 | + |
| 202 | +### Get executorch program ptr from BundledProgram buffer |
| 203 | +We 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: |
| 204 | + ```c++ |
| 205 | + |
| 206 | +/** |
| 207 | + * Finds the serialized ExecuTorch program data in the provided file data. |
| 208 | + * |
| 209 | + * The returned buffer is appropriate for constructing a |
| 210 | + * torch::executor::Program. |
| 211 | + * |
| 212 | + * Calling this is only necessary if the file could be a bundled program. If the |
| 213 | + * file will only contain an unwrapped ExecuTorch program, callers can construct |
| 214 | + * torch::executor::Program with file_data directly. |
| 215 | + * |
| 216 | + * @param[in] file_data The contents of an ExecuTorch program or bundled program |
| 217 | + * file. |
| 218 | + * @param[in] file_data_len The length of file_data, in bytes. |
| 219 | + * @param[out] out_program_data The serialized Program data, if found. |
| 220 | + * @param[out] out_program_data_len The length of out_program_data, in bytes. |
| 221 | + * |
| 222 | + * @returns Error::Ok if the program was found, and |
| 223 | + * out_program_data/out_program_data_len point to the data. Other values |
| 224 | + * on failure. |
| 225 | + */ |
| 226 | +Error GetProgramData( |
| 227 | + void* file_data, |
| 228 | + size_t file_data_len, |
| 229 | + const void** out_program_data, |
| 230 | + size_t* out_program_data_len); |
| 231 | +``` |
| 232 | +
|
| 233 | +Here's an example of how to use the GetProgramData API: |
| 234 | +```c++ |
| 235 | + std::shared_ptr<char> buff_ptr; |
| 236 | + size_t buff_len; |
| 237 | +
|
| 238 | +// FILE_PATH here can be either BundledProgram or Program flatbuffer file. |
| 239 | + Error status = torch::executor::util::read_file_content( |
| 240 | + FILE_PATH, &buff_ptr, &buff_len); |
| 241 | + ET_CHECK_MSG( |
| 242 | + status == Error::Ok, |
| 243 | + "read_file_content() failed with status 0x%" PRIx32, |
| 244 | + status); |
| 245 | +
|
| 246 | + uint32_t prof_tok = EXECUTORCH_BEGIN_PROF("de-serialize model"); |
| 247 | +
|
| 248 | + const void* program_ptr; |
| 249 | + size_t program_len; |
| 250 | + status = torch::executor::util::GetProgramData( |
| 251 | + buff_ptr.get(), buff_len, &program_ptr, &program_len); |
| 252 | + ET_CHECK_MSG( |
| 253 | + status == Error::Ok, |
| 254 | + "GetProgramData() failed with status 0x%" PRIx32, |
| 255 | + status); |
| 256 | +``` |
| 257 | + |
| 258 | +### Load bundled input to ExecutionPlan |
| 259 | +To execute the program on the bundled input, we need to load the bundled input into the ExecutionPlan. Here we provided an API called `torch::executor::util::LoadBundledInput`: |
| 260 | + |
| 261 | +```c++ |
| 262 | + |
| 263 | +/** |
| 264 | + * Load testset_idx-th bundled input of method_idx-th Method test in |
| 265 | + * bundled_program_ptr to given Method. |
| 266 | + * |
| 267 | + * @param[in] method The Method to verify. |
| 268 | + * @param[in] bundled_program_ptr The bundled program contains expected output. |
| 269 | + * @param[in] method_name The name of the Method being verified. |
| 270 | + * @param[in] testset_idx The index of input needs to be set into given Method. |
| 271 | + * |
| 272 | + * @returns Return Error::Ok if load successfully, or the error happens during |
| 273 | + * execution. |
| 274 | + */ |
| 275 | +__ET_NODISCARD Error LoadBundledInput( |
| 276 | + Method& method, |
| 277 | + serialized_bundled_program* bundled_program_ptr, |
| 278 | + MemoryAllocator* memory_allocator, |
| 279 | + const char* method_name, |
| 280 | + size_t testset_idx); |
| 281 | +``` |
| 282 | +
|
| 283 | +### Verify the plan's output. |
| 284 | +We call `torch::executor::util::VerifyResultWithBundledExpectedOutput` to verify the method's output with bundled expected outputs. Here's the details of this API: |
| 285 | +
|
| 286 | +```c++ |
| 287 | +/** |
| 288 | + * Compare the Method's output with testset_idx-th bundled expected |
| 289 | + * output in method_idx-th Method test. |
| 290 | + * |
| 291 | + * @param[in] method The Method to extract outputs from. |
| 292 | + * @param[in] bundled_program_ptr The bundled program contains expected output. |
| 293 | + * @param[in] method_name The name of the Method being verified. |
| 294 | + * @param[in] testset_idx The index of expected output needs to be compared. |
| 295 | + * @param[in] rtol Relative tolerance used for data comparsion. |
| 296 | + * @param[in] atol Absolute tolerance used for data comparsion. |
| 297 | + * |
| 298 | + * @returns Return Error::Ok if two outputs match, or the error happens during |
| 299 | + * execution. |
| 300 | + */ |
| 301 | +__ET_NODISCARD Error VerifyResultWithBundledExpectedOutput( |
| 302 | + Method& method, |
| 303 | + serialized_bundled_program* bundled_program_ptr, |
| 304 | + MemoryAllocator* memory_allocator, |
| 305 | + const char* method_name, |
| 306 | + size_t testset_idx, |
| 307 | + double rtol = 1e-5, |
| 308 | + double atol = 1e-8); |
| 309 | +
|
| 310 | +``` |
| 311 | + |
| 312 | +### Example |
| 313 | + |
| 314 | +Here we provide an example about how to run the bundled program step by step. Most of the code are borrowed from "fbcode/executorch/sdk/runners/executor_runner.cpp" and please review that file if you need more info and context: |
| 315 | + |
| 316 | +```c++ |
| 317 | + // method_name is the name for the method we want to test |
| 318 | + // memory_manager is the executor::MemoryManager variable for executor memory allocation. |
| 319 | + // program is the executorch program. |
| 320 | + Result<Method> method = program->load_method(method_name, &memory_manager); |
| 321 | + EXECUTORCH_END_PROF(prof_tok); |
| 322 | + ET_CHECK_MSG( |
| 323 | + method.ok(), |
| 324 | + "load_method() failed with status 0x%" PRIx32, |
| 325 | + method.error()); |
| 326 | + |
| 327 | + // Load testset_idx-th input in the buffer to plan |
| 328 | + status = torch::executor::util::LoadBundledInput( |
| 329 | + *method, |
| 330 | + program_data.bundled_program_data(), |
| 331 | + &bundled_input_allocator, |
| 332 | + method_name, |
| 333 | + FLAGS_testset_idx); |
| 334 | + ET_CHECK_MSG( |
| 335 | + status == Error::Ok, |
| 336 | + "LoadBundledInput failed with status 0x%" PRIx32, |
| 337 | + status); |
| 338 | + |
| 339 | + // Execute the plan |
| 340 | + status = method->execute(); |
| 341 | + ET_CHECK_MSG( |
| 342 | + status == Error::Ok, |
| 343 | + "method->execute() failed with status 0x%" PRIx32, |
| 344 | + status); |
| 345 | + |
| 346 | + // Verify the result. |
| 347 | + status = torch::executor::util::VerifyResultWithBundledExpectedOutput( |
| 348 | + *method, |
| 349 | + program_data.bundled_program_data(), |
| 350 | + &bundled_input_allocator, |
| 351 | + method_name, |
| 352 | + FLAGS_testset_idx, |
| 353 | + FLAGS_rtol, |
| 354 | + FLAGS_atol); |
| 355 | + ET_CHECK_MSG( |
| 356 | + status == Error::Ok, |
| 357 | + "Bundle verification failed with status 0x%" PRIx32, |
| 358 | + status); |
| 359 | + |
| 360 | +``` |
0 commit comments