-
Notifications
You must be signed in to change notification settings - Fork 23
Add tests to validate externalization workflow with graph mode #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
pkmandke
wants to merge
14
commits into
apple:main
Choose a base branch
from
pkmandke:dev/extern
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
11618af
use extern API
pkmandke 9e66ae1
inspect
pkmandke eb03e2c
subexport_and_restore
pkmandke 6200c57
output
pkmandke 26f7def
update to latest extern API and cleanup tests
pkmandke 15431ee
merge main
pkmandke 82eaa4f
remove notebook diff and refactor quant cfg util
pkmandke ee9107c
index select for composite input quantization and refactoring
pkmandke 2e94595
cleanup
pkmandke 5c3e15f
composite ops boundary quantization coverage
pkmandke 0f51025
add doc page
pkmandke 8d66ed1
update flowchart
pkmandke 7ec5d63
address review comments
pkmandke 95d1822
nit
pkmandke File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| # Quantizing Models with Core AI Composite Ops in Graph Mode | ||
|
|
||
| Core AI recognizes certain well-known building blocks, such as SDPA or RMSNorm, as _composite ops_ and applies optimized implementations for them. | ||
| `coreai-torch` establishes those boundaries through _externalization_. | ||
| Refer to the [Externalization](https://apple.github.io/coreai-torch/main/guides/externalization.html) guide for details. | ||
| Here, we will discuss the steps required to quantize a model in `graph` mode using `coreai-opt`. | ||
|
|
||
| `graph`-mode quantization invokes `torch.export.export` under the hood, which decomposes a submodule's `forward` into aten ops. | ||
| In order to preserve the composite op structure during this process for externalization, the following APIs are provided: | ||
|
|
||
| - `_patch_model_for_externalization`: Patch the model **before** `quantizer.prepare`, so that the composite op call sites survive export and all subsequent quantization passes as opaque nodes. | ||
| - `_subexport_and_restore`: The submodule bodies of the composite ops themselves are then exported and restored before lowering to `CoreAI`. | ||
|
|
||
| Quantization treats each composite op as opaque: no fake-quantize op is placed inside the composite body. | ||
| The composite's input and output boundary can still be quantized, see [Quantizing the composite op boundary](#quantizing-the-composite-op-boundary) below for details. | ||
|
|
||
| :::{warning} | ||
| The externalization APIs used below, `_patch_model_for_externalization` and `_subexport_and_restore` in `coreai-torch` are currently experimental. | ||
| ::: | ||
|
|
||
| ```mermaid | ||
| --- | ||
| title: "Graph mode Quantization Workflow with Externalization" | ||
| --- | ||
| flowchart LR | ||
| model["Full Precision<br>Model"] --> patch["Patch Model for<br>Externalization"] | ||
| patch --> prepare["Prepare and<br>Calibrate"] | ||
| prepare --> qfin["Finalize and<br>Export"] | ||
| qfin --> sub["Sub-export<br>and Restore"] | ||
| sub --> convert["Convert to<br>Core AI"] | ||
| style model fill:#f9f9f9,stroke:#999 | ||
| style patch fill:#e8f0fe,stroke:#4285f4 | ||
| style sub fill:#e8f0fe,stroke:#4285f4 | ||
| ``` | ||
|
|
||
| ## Step 1: Patch the model before prepare | ||
|
|
||
| `_patch_model_for_externalization` replaces the `forward` of every matching submodule in the model with a `torch.library.custom_op`, in place. | ||
| Call it before constructing the `Quantizer`. | ||
| The example below uses the same `RMSNormComposite` module as an example, however, the same process applies for all composite ops with their respective `ExternalizeSpec`s. | ||
|
|
||
| ```python | ||
| import torch | ||
| import torch.nn as nn | ||
| from coreai_torch import ExternalizeSpec, _patch_model_for_externalization | ||
|
|
||
|
|
||
| # The composite op | ||
| class RMSNormComposite(nn.Module): | ||
| def __init__(self, axes=-1, eps=1e-5, version=1): | ||
| super().__init__() | ||
| self.axes = axes | ||
| self.eps = eps | ||
| self.version = version | ||
|
|
||
| def forward(self, input: torch.Tensor, scale: torch.Tensor) -> torch.Tensor: | ||
| x_f32 = input.to(torch.float32) | ||
| inv_rms = torch.rsqrt((x_f32 * x_f32).mean(self.axes, keepdim=True) + self.eps) | ||
| return (input * inv_rms).to(input.dtype) * scale | ||
|
|
||
|
|
||
| # A model that uses the composite op | ||
| class Model(nn.Module): | ||
| def __init__(self, dim=32): | ||
| super().__init__() | ||
| self.proj = nn.Linear(dim, dim) | ||
| self.norm = RMSNormComposite() | ||
| self.norm_weight = nn.Parameter(torch.ones(dim)) | ||
| self.out = nn.Linear(dim, dim) | ||
|
|
||
| def forward(self, x): | ||
| return self.out(self.norm(self.proj(x), self.norm_weight)) | ||
|
|
||
|
|
||
| model = Model().eval() | ||
| example_inputs = (torch.randn(1, 32),) | ||
|
|
||
| # Patch the model in-place | ||
| # to externalize the RMSNormComposite | ||
| _patch_model_for_externalization( | ||
| model, | ||
| targets=[ | ||
| ExternalizeSpec( | ||
| target_class=RMSNormComposite, | ||
| composite_op_name="rms_norm", | ||
| composite_attrs=["axes", "eps", "version"], | ||
| ) | ||
| ], | ||
| ) | ||
| ``` | ||
|
|
||
| ## Step 2: Prepare, calibrate and finalize | ||
|
|
||
| Nothing about the quantizer configuration or the calibration workflow changes. | ||
| The composite op holds no weights of its own here, so weight quantization applies to the surrounding `Linear` layers only. | ||
|
|
||
| ```python | ||
| import coreai_opt as opt | ||
| from coreai_opt.quantization import ModuleQuantizerConfig, Quantizer, QuantizerConfig | ||
| from coreai_opt.quantization.spec import ( | ||
| default_activation_quantization_spec, | ||
| default_weight_quantization_spec, | ||
| ) | ||
|
|
||
| global_config = ModuleQuantizerConfig( | ||
| op_state_spec={"weight": default_weight_quantization_spec()}, | ||
| op_input_spec={"*": default_activation_quantization_spec()}, | ||
| op_output_spec={"*": default_activation_quantization_spec()}, | ||
| ) | ||
| quant_config = QuantizerConfig(global_config=global_config) | ||
|
|
||
| quantizer = Quantizer(model, quant_config) | ||
| prepared_model = quantizer.prepare(example_inputs) | ||
|
|
||
| with quantizer.calibration_mode(): | ||
| for batch in calibration_dataloader: | ||
| prepared_model(batch) | ||
|
|
||
| final_model = quantizer.finalize(backend=opt.ExportBackend.CoreAI) | ||
| ``` | ||
|
|
||
| ## Step 3: Export and convert to Core AI | ||
|
|
||
| After quantization is complete and the model is finalized, `_subexport_and_restore` API exports each patched composite op and restores the original `forward` method in the model. | ||
| Note that the first argument to `_subexport_and_restore` is the original module that was patched in Step 1, not the finalized `GraphModule`. | ||
|
|
||
| ```python | ||
| import coreai_torch | ||
| from coreai_torch import TorchConverter, _subexport_and_restore | ||
|
|
||
| exported_program = torch.export.export(final_model, example_inputs).run_decompositions( | ||
| coreai_torch.get_decomp_table() | ||
| ) | ||
| externalized = _subexport_and_restore(model, exported_program) | ||
|
|
||
| coreai_program = ( | ||
| TorchConverter() | ||
| .add_exported_program( | ||
| exported_program, _externalized_exported_programs=externalized | ||
| ) | ||
| .to_coreai() | ||
| ) | ||
| ``` | ||
|
|
||
| In the Core AI graph, the composite op is emitted as a separate private graph that `@main` reaches through `coreai.invoke`: | ||
|
|
||
| ```text | ||
| coreai.graph private noinline @norm_57e2d4a8(%arg0: tensor<1x32xf32> {coreai.name = "input"}, %arg1: tensor<32xf32> {coreai.name = "scale"}) -> (tensor<1x32xf32>) attributes {composite_decl = ...} { | ||
| %2 = coreai.decomposable.broadcasting_mul %0, %1 : (tensor<1x32xf32>, tensor<1x32xf32>) -> tensor<1x32xf32> | ||
| %4 = coreai.reduce_mean %2, %3 : (tensor<1x32xf32>, tensor<1xsi32>) -> tensor<1x1xf32> | ||
| %8 = coreai.decomposable.broadcasting_add %6, %7 : (tensor<1x1xf32>, tensor<f32>) -> tensor<1x1xf32> | ||
| %9 = coreai.rsqrt %8 : tensor<1x1xf32> -> tensor<1x1xf32> | ||
| %12 = coreai.decomposable.broadcasting_mul %10, %11 : (tensor<1x32xf32>, tensor<1x1xf32>) -> tensor<1x32xf32> | ||
| %15 = coreai.decomposable.broadcasting_mul %13, %14 : (tensor<1x32xf32>, tensor<32xf32>) -> tensor<1x32xf32> | ||
| coreai.output %15 : tensor<1x32xf32> | ||
| } | ||
|
|
||
| coreai.graph @main(%arg0: tensor<1x32xf32> {coreai.name = "x"}) -> (tensor<1x32xf32>) { | ||
| %44 = coreai.decomposable.broadcasting_add %43, %2 : (tensor<1x32xf32>, tensor<32xf32>) -> tensor<1x32xf32> | ||
| %53 = coreai.quantize %44, ... : (tensor<1x32xf32>, ...) -> tensor<1x32xsi8> | ||
| %62 = coreai.dequantize %53, ... : (tensor<1x32xsi8>, ...) -> tensor<1x32xf32> | ||
| %63 = coreai.invoke @norm_57e2d4a8(%62, %0) : (tensor<1x32xf32>, tensor<32xf32>) -> tensor<1x32xf32> | ||
| %72 = coreai.quantize %63, ... : (tensor<1x32xf32>, ...) -> tensor<1x32xsi8> | ||
| %81 = coreai.dequantize %72, ... : (tensor<1x32xsi8>, ...) -> tensor<1x32xf32> | ||
| %84 = coreai.decomposable.broadcasting_batch_matmul %81, %83 : (tensor<1x32xf32>, tensor<32x32xf32>) -> tensor<1x32xf32> | ||
| } | ||
| ``` | ||
|
|
||
| (`coreai.cast`, `coreai.constant` and `coreai.reshape` ops omitted above for brevity.) | ||
|
|
||
| The composite body carries no `coreai.quantize` or `coreai.dequantize` op and stays in full precision. | ||
|
|
||
| ## Quantizing the composite op boundary | ||
|
|
||
| The `coreai.quantize` pairs surrounding the `coreai.invoke` above come from the global config. They are the output quantizer of the preceding `Linear` and the input quantizer of the following one. | ||
| The composite op boundary itself is not targeted by a global config. | ||
|
|
||
| To target the boundary specifically, use `module_input_spec` and `module_output_spec` on a {class}`~coreai_opt.quantization.config.ModuleQuantizerConfig` scoped by `module_type_configs` or `module_name_configs`. | ||
|
|
||
| To see this in isolation, the following example uses a model with the composite op alone and specifies a module level spec to quantize it's boundary. | ||
|
|
||
| ```python | ||
| from coreai_opt.quantization.spec import ( | ||
| PerTensorGranularity, | ||
| QuantizationScheme, | ||
| QuantizationSpec, | ||
| ) | ||
|
|
||
|
|
||
| class RMSNormOnly(nn.Module): | ||
| def __init__(self, dim=32): | ||
| super().__init__() | ||
| self.norm = RMSNormComposite() | ||
| self.norm_weight = nn.Parameter(torch.ones(dim)) | ||
|
|
||
| def forward(self, x): | ||
| return self.norm(x, self.norm_weight) | ||
|
|
||
|
|
||
| boundary_spec = QuantizationSpec( | ||
| dtype=torch.int8, | ||
| qscheme=QuantizationScheme.SYMMETRIC, | ||
| granularity=PerTensorGranularity(), | ||
| ) | ||
| quant_config = QuantizerConfig( | ||
| module_type_configs={ | ||
| RMSNormComposite: ModuleQuantizerConfig( | ||
| module_input_spec={"*": boundary_spec}, | ||
| module_output_spec={"*": boundary_spec}, | ||
| ) | ||
| }, | ||
| ) | ||
| ``` | ||
|
|
||
| Running the same patch, prepare, calibrate, finalize and convert steps as above, gives a `@main` graph containing just the boundary quantization and the composite call. | ||
|
|
||
| ```text | ||
| coreai.graph private noinline @norm_20ea9665(%arg0: tensor<1x32xf32> {coreai.name = "input"}, %arg1: tensor<32xf32> {coreai.name = "scale"}) -> (tensor<1x32xf32>) attributes {composite_decl = ...} { | ||
| %2 = coreai.decomposable.broadcasting_mul %0, %1 : (tensor<1x32xf32>, tensor<1x32xf32>) -> tensor<1x32xf32> | ||
| %4 = coreai.reduce_mean %2, %3 : (tensor<1x32xf32>, tensor<1xsi32>) -> tensor<1x1xf32> | ||
| %8 = coreai.decomposable.broadcasting_add %6, %7 : (tensor<1x1xf32>, tensor<f32>) -> tensor<1x1xf32> | ||
| %9 = coreai.rsqrt %8 : tensor<1x1xf32> -> tensor<1x1xf32> | ||
| %12 = coreai.decomposable.broadcasting_mul %10, %11 : (tensor<1x32xf32>, tensor<1x1xf32>) -> tensor<1x32xf32> | ||
| %15 = coreai.decomposable.broadcasting_mul %13, %14 : (tensor<1x32xf32>, tensor<32xf32>) -> tensor<1x32xf32> | ||
| coreai.output %15 : tensor<1x32xf32> | ||
| } | ||
|
|
||
| coreai.graph @main(%arg0: tensor<1x32xf32> {coreai.name = "x"}) -> (tensor<1x32xf32>) { | ||
| %13 = coreai.quantize %arg0, ... : (tensor<1x32xf32>, ...) -> tensor<1x32xsi8> | ||
| %22 = coreai.dequantize %13, ... : (tensor<1x32xsi8>, ...) -> tensor<1x32xf32> | ||
| %23 = coreai.invoke @norm_20ea9665(%22, %0) : (tensor<1x32xf32>, tensor<32xf32>) -> tensor<1x32xf32> | ||
| %32 = coreai.quantize %23, ... : (tensor<1x32xf32>, ...) -> tensor<1x32xsi8> | ||
| %41 = coreai.dequantize %32, ... : (tensor<1x32xsi8>, ...) -> tensor<1x32xf32> | ||
| coreai.output %41 : tensor<1x32xf32> | ||
| } | ||
| ``` | ||
|
|
||
| (`coreai.cast`, `coreai.constant` and `coreai.reshape` ops omitted above for brevity.) | ||
|
|
||
| ## Notes | ||
|
|
||
| - The same set of APIs and steps apply for Quantization Aware Training in `graph` mode as well. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.