|
7 | 7 |
|
8 | 8 | import pytest |
9 | 9 | import torch |
| 10 | +from executorch.backends.arm.quantizer import ( |
| 11 | + get_symmetric_quantization_config, |
| 12 | + TOSAQuantizer, |
| 13 | +) |
10 | 14 | from executorch.backends.arm.scripts.neural_graphics_test_data import ( |
11 | 15 | _NSS_INPUT_CHANNELS, |
12 | 16 | iter_nss_test_calibration_samples, |
|
26 | 30 | TosaPipelineINT, |
27 | 31 | VgfPipeline, |
28 | 32 | ) |
| 33 | +from executorch.backends.arm.tosa import TosaSpecification |
| 34 | +from executorch.backends.transforms.duplicate_dynamic_quant_chain import ( |
| 35 | + DuplicateDynamicQuantChainPass, |
| 36 | +) |
29 | 37 |
|
30 | 38 | from huggingface_hub import hf_hub_download |
31 | 39 |
|
32 | 40 | from ng_model_gym.usecases.nss.model.model_blocks_v1 import ( # type: ignore[import-not-found,import-untyped] |
33 | 41 | AutoEncoderV1, |
34 | 42 | ) |
35 | 43 | from torch.export import Dim |
| 44 | +from torchao.quantization.pt2e import ( |
| 45 | + allow_exported_model_train_eval, |
| 46 | + FixedQParamsFakeQuantize, |
| 47 | + FixedQParamsObserver, |
| 48 | + move_exported_model_to_eval, |
| 49 | +) |
| 50 | +from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_qat_pt2e |
36 | 51 |
|
37 | 52 | input_t = Tuple[torch.Tensor] # Input x |
38 | 53 |
|
@@ -71,6 +86,62 @@ def nss() -> AutoEncoderV1: |
71 | 86 | return nss_model.auto_encoder |
72 | 87 |
|
73 | 88 |
|
| 89 | +def prequantized_nss(inputs: input_t) -> torch.fx.GraphModule: |
| 90 | + weights = hf_hub_download( # nosec B615 |
| 91 | + repo_id="Arm/neural-super-sampling", |
| 92 | + filename="nss_v1_0_1_high_int8.pt", |
| 93 | + revision="main", |
| 94 | + ) |
| 95 | + checkpoint = torch.load( |
| 96 | + weights, map_location=torch.device("cpu"), weights_only=True |
| 97 | + )["model_state_dict"] |
| 98 | + prefix = "autoencoder." |
| 99 | + assert all(key.startswith(prefix) for key in checkpoint) |
| 100 | + state_dict = {key.removeprefix(prefix): value for key, value in checkpoint.items()} |
| 101 | + |
| 102 | + exported = torch.export.export(nss().eval(), inputs, strict=True).module() |
| 103 | + quantizer = TOSAQuantizer(TosaSpecification.create_from_string("TOSA-1.0+INT")) |
| 104 | + quantizer.set_global( |
| 105 | + get_symmetric_quantization_config(is_per_channel=True, is_qat=True) |
| 106 | + ) |
| 107 | + prepared = prepare_qat_pt2e(exported, quantizer) |
| 108 | + |
| 109 | + fixed_observers = { |
| 110 | + key.removesuffix(".activation_post_process.scale") |
| 111 | + for key in state_dict |
| 112 | + if key.endswith(".activation_post_process.scale") |
| 113 | + } |
| 114 | + for name in fixed_observers: |
| 115 | + scale = state_dict[f"{name}.activation_post_process.scale"].item() |
| 116 | + zero_point = state_dict[f"{name}.activation_post_process.zero_point"].item() |
| 117 | + observer = FixedQParamsObserver.with_args( |
| 118 | + scale=scale, |
| 119 | + zero_point=zero_point, |
| 120 | + dtype=torch.int8, |
| 121 | + qscheme=torch.per_tensor_affine, |
| 122 | + quant_min=-127, |
| 123 | + quant_max=127, |
| 124 | + ) |
| 125 | + prepared.set_submodule(name, FixedQParamsFakeQuantize(observer=observer)) |
| 126 | + |
| 127 | + parameter_keys = list(dict(prepared.named_parameters())) |
| 128 | + lifted_parameter_keys = [ |
| 129 | + key for key in state_dict if key.startswith("_param_constant") |
| 130 | + ] |
| 131 | + assert len(parameter_keys) == len(lifted_parameter_keys) |
| 132 | + for lifted_key, parameter_key in zip( |
| 133 | + lifted_parameter_keys, parameter_keys, strict=True |
| 134 | + ): |
| 135 | + state_dict[parameter_key] = state_dict.pop(lifted_key) |
| 136 | + |
| 137 | + prepared.load_state_dict(state_dict, strict=True) |
| 138 | + move_exported_model_to_eval(prepared) |
| 139 | + converted = convert_pt2e(prepared) |
| 140 | + DuplicateDynamicQuantChainPass()(converted) |
| 141 | + allow_exported_model_train_eval(converted) |
| 142 | + return converted |
| 143 | + |
| 144 | + |
74 | 145 | def example_inputs(): |
75 | 146 | return load_nss_verification_inputs() |
76 | 147 |
|
@@ -132,6 +203,28 @@ def test_nss_tosa_INT(use_real_data, is_qat): |
132 | 203 | pipeline.run() |
133 | 204 |
|
134 | 205 |
|
| 206 | +@common.parametrize("use_real_data", input_test_data) |
| 207 | +def test_nss_prequantized_tosa_INT(use_real_data): |
| 208 | + inputs = example_inputs() if use_real_data else random_inputs() |
| 209 | + pipeline = TosaPipelineINT[input_t]( |
| 210 | + prequantized_nss(inputs), |
| 211 | + inputs, |
| 212 | + aten_op=[], |
| 213 | + exir_op=[], |
| 214 | + use_to_edge_transform_and_lower=True, |
| 215 | + qtol=12 if use_real_data else 8, |
| 216 | + ) |
| 217 | + pipeline.pop_stage("quantize") |
| 218 | + pipeline.pop_stage("check.quant_nodes") |
| 219 | + pipeline.add_stage_after( |
| 220 | + "export", |
| 221 | + pipeline.tester.check, |
| 222 | + ["torch.ops.quantized_decomposed.dequantize_per_tensor.default"], |
| 223 | + suffix="prequant_nodes", |
| 224 | + ) |
| 225 | + pipeline.run() |
| 226 | + |
| 227 | + |
135 | 228 | @pytest.mark.skip(reason="No support for aten_upsample_nearest2d_vec on U55") |
136 | 229 | @common.XfailIfNoCorstone300 |
137 | 230 | def test_nss_u55_INT(): |
@@ -202,3 +295,29 @@ def test_nss_vgf_INT(use_real_data, is_qat): |
202 | 295 | if use_real_data: |
203 | 296 | _set_nss_calibration_samples(pipeline) |
204 | 297 | pipeline.run() |
| 298 | + |
| 299 | + |
| 300 | +@common.SkipIfNoModelConverter |
| 301 | +@common.parametrize("use_real_data", input_test_data) |
| 302 | +def test_nss_prequantized_vgf_INT(use_real_data): |
| 303 | + inputs = example_inputs() if use_real_data else random_inputs() |
| 304 | + pipeline = VgfPipeline[input_t]( |
| 305 | + prequantized_nss(inputs), |
| 306 | + inputs, |
| 307 | + aten_op=[], |
| 308 | + exir_op=[], |
| 309 | + use_to_edge_transform_and_lower=True, |
| 310 | + run_on_vulkan_runtime=True, |
| 311 | + quantize=True, |
| 312 | + tosa_version="TOSA-1.0+INT", |
| 313 | + qtol=12 if use_real_data else 8, |
| 314 | + ) |
| 315 | + pipeline.pop_stage("quantize") |
| 316 | + pipeline.pop_stage("check.quant_nodes") |
| 317 | + pipeline.add_stage_after( |
| 318 | + "export", |
| 319 | + pipeline.tester.check, |
| 320 | + ["torch.ops.quantized_decomposed.dequantize_per_tensor.default"], |
| 321 | + suffix="prequant_nodes", |
| 322 | + ) |
| 323 | + pipeline.run() |
0 commit comments