Skip to content

Commit 74ade81

Browse files
fix(export): Fix export to INT8_PTQ and INT8_ACQ (#3155)
* fix(export): set default value of dynamo to False in ExportMixin; Add onnxscript to the dependencies. * fix(export): Fixes INT8_ACQ * feat(export): Add max_drop parameter for INT8_ACQ quantization and default metric handling * test(export): Add unit tests for OpenVINO export with various compression types * Update src/anomalib/models/components/base/export_mixin.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Rajesh Gangireddy <rajesh.gangireddy@intel.com> * test(export): Enhance OpenVINO export tests with model file name assertions and CI skip conditions --------- Signed-off-by: Rajesh Gangireddy <rajesh.gangireddy@intel.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent cf83ed2 commit 74ade81

File tree

4 files changed

+506
-14
lines changed

4 files changed

+506
-14
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ dependencies = [
5858

5959
[project.optional-dependencies]
6060
# Model-specific optional dependencies
61-
openvino = ["openvino>=2024.0", "nncf>=2.10.0", "onnx>=1.16.0"]
61+
openvino = ["openvino>=2024.0", "nncf>=2.10.0", "onnx>=1.16.0", "onnxscript"]
6262
clip = [
6363
# NOTE: open-clip-torch throws the following error on v2.26.1
6464
# torch.onnx.errors.UnsupportedOperatorError: Exporting the operator

src/anomalib/engine/engine.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ def export(
742742
compression_type: CompressionType | None = None,
743743
datamodule: AnomalibDataModule | None = None,
744744
metric: Metric | str | None = None,
745+
max_drop: float = 0.01,
745746
ov_args: dict[str, Any] | None = None, # deprecated
746747
ov_kwargs: dict[str, Any] | None = None,
747748
onnx_kwargs: dict[str, Any] | None = None,
@@ -766,9 +767,14 @@ def export(
766767
(OpenVINO export only).
767768
Defaults to ``None``.
768769
metric (Metric | str | None, optional): Metric to measure quality loss when quantizing.
769-
Must be provided if ``CompressionType.INT8_ACQ`` is selected and must return higher value for better
770-
performance of the model (OpenVINO export only).
770+
Only used for ``CompressionType.INT8_ACQ`` (OpenVINO export only).
771+
If not provided for INT8_ACQ, defaults to F1Score at image level.
772+
Must return higher value for better performance of the model.
771773
Defaults to ``None``.
774+
max_drop (float, optional): Maximum acceptable accuracy drop during quantization.
775+
Only used for ``CompressionType.INT8_ACQ`` (OpenVINO export only).
776+
Value should be between 0 and 1 (e.g., 0.01 means 1% drop is acceptable).
777+
Defaults to ``0.01``.
772778
ov_args (dict[str, Any] | None, optional): Deprecated. Use ov_kwargs instead.
773779
This is optional and used only for OpenVINO's model optimizer.
774780
Defaults to None.
@@ -834,6 +840,25 @@ def export(
834840
if export_root is None:
835841
export_root = Path(self.trainer.default_root_dir)
836842

843+
# Warn if max_drop is provided but not used
844+
if max_drop != 0.01 and compression_type != CompressionType.INT8_ACQ:
845+
warnings.warn(
846+
f"max_drop parameter is only used for CompressionType.INT8_ACQ but got {compression_type}. "
847+
"The parameter will be ignored.",
848+
UserWarning,
849+
stacklevel=2,
850+
)
851+
852+
# Set default metric for INT8_ACQ if not provided
853+
if metric is None and compression_type == CompressionType.INT8_ACQ:
854+
from anomalib.metrics import F1Score
855+
856+
metric = F1Score(fields=["pred_label", "gt_label"])
857+
logger.info(
858+
"No metric provided for INT8_ACQ quantization. "
859+
"Using default: F1Score at image level (fields=['pred_label', 'gt_label']).",
860+
)
861+
837862
exported_model_path: Path | None = None
838863
if export_type == ExportType.TORCH:
839864
exported_model_path = model.to_torch(
@@ -855,6 +880,7 @@ def export(
855880
compression_type=compression_type,
856881
datamodule=datamodule,
857882
metric=metric,
883+
max_drop=max_drop,
858884
ov_kwargs=ov_kwargs,
859885
onnx_kwargs=onnx_kwargs,
860886
)

src/anomalib/models/components/base/export_mixin.py

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
from torchmetrics import Metric
5151

5252
from anomalib import TaskType
53-
from anomalib.data import AnomalibDataModule
53+
from anomalib.data import AnomalibDataModule, ImageBatch
5454
from anomalib.deploy.export import CompressionType, ExportType
5555

5656
if TYPE_CHECKING:
@@ -179,6 +179,7 @@ def to_onnx(
179179
dynamic_axes=kwargs.pop("dynamic_axes", dynamic_axes),
180180
input_names=kwargs.pop("input_names", ["input"]),
181181
output_names=kwargs.pop("output_names", output_names),
182+
dynamo=kwargs.pop("dynamo", False), # Dynamo is changed to True by default in torch 2.9
182183
**kwargs,
183184
)
184185

@@ -193,6 +194,7 @@ def to_openvino(
193194
datamodule: AnomalibDataModule | None = None,
194195
metric: Metric | None = None,
195196
task: TaskType | None = None,
197+
max_drop: float = 0.01,
196198
ov_kwargs: dict[str, Any] | None = None,
197199
onnx_kwargs: dict[str, Any] | None = None,
198200
) -> Path:
@@ -209,9 +211,14 @@ def to_openvino(
209211
datamodule (AnomalibDataModule | None): DataModule for quantization.
210212
Required for ``INT8_PTQ`` and ``INT8_ACQ``. Defaults to ``None``
211213
metric (Metric | None): Metric for accuracy-aware quantization.
212-
Required for ``INT8_ACQ``. Defaults to ``None``
214+
Used for ``INT8_ACQ``. If not provided, a default F1Score at image level
215+
will be used. Defaults to ``None``
213216
task (TaskType | None): Task type (classification/segmentation).
214217
Defaults to ``None``
218+
max_drop (float): Maximum acceptable accuracy drop during quantization.
219+
Only used for ``INT8_ACQ`` compression. Value should be between 0 and 1
220+
(e.g., 0.01 means 1% accuracy drop is acceptable).
221+
Defaults to ``0.01``
215222
ov_kwargs (dict[str, Any] | None): OpenVINO model optimizer arguments.
216223
Defaults to ``None``
217224
onnx_kwargs (dict[str, Any] | None): Additional arguments to pass to torch.onnx.export
@@ -257,7 +264,7 @@ def to_openvino(
257264

258265
model = ov.convert_model(model_path, **(ov_kwargs or {}))
259266
if compression_type and compression_type != CompressionType.FP16:
260-
model = self._compress_ov_model(model, compression_type, datamodule, metric, task)
267+
model = self._compress_ov_model(model, compression_type, datamodule, metric, task, max_drop)
261268

262269
# fp16 compression is enabled by default
263270
compress_to_fp16 = compression_type == CompressionType.FP16
@@ -272,6 +279,7 @@ def _compress_ov_model(
272279
datamodule: AnomalibDataModule | None = None,
273280
metric: Metric | None = None,
274281
task: TaskType | None = None,
282+
max_drop: float = 0.01,
275283
) -> "CompiledModel":
276284
"""Compress OpenVINO model using NNCF.
277285
@@ -285,6 +293,8 @@ def _compress_ov_model(
285293
Required for ``INT8_ACQ``. Defaults to ``None``
286294
task (TaskType | None): Task type (classification/segmentation).
287295
Defaults to ``None``
296+
max_drop (float): Maximum acceptable accuracy drop during quantization.
297+
Only used for ``INT8_ACQ``. Defaults to ``0.01``
288298
289299
Returns:
290300
CompiledModel: Compressed OpenVINO model
@@ -304,7 +314,7 @@ def _compress_ov_model(
304314
elif compression_type == CompressionType.INT8_PTQ:
305315
model = self._post_training_quantization_ov(model, datamodule)
306316
elif compression_type == CompressionType.INT8_ACQ:
307-
model = self._accuracy_control_quantization_ov(model, datamodule, metric, task)
317+
model = self._accuracy_control_quantization_ov(model, datamodule, metric, task, max_drop)
308318
else:
309319
msg = f"Unrecognized compression type: {compression_type}"
310320
raise ValueError(msg)
@@ -356,6 +366,7 @@ def _accuracy_control_quantization_ov(
356366
datamodule: AnomalibDataModule | None = None,
357367
metric: Metric | None = None,
358368
task: TaskType | None = None,
369+
max_drop: float = 0.01,
359370
) -> "CompiledModel":
360371
"""Apply accuracy-aware quantization to OpenVINO model.
361372
@@ -366,15 +377,19 @@ def _accuracy_control_quantization_ov(
366377
Defaults to ``None``
367378
metric (Metric | None): Metric to measure accuracy during quantization.
368379
Higher values should indicate better performance.
380+
If not provided, defaults to F1Score at image level.
369381
Defaults to ``None``
370382
task (TaskType | None): Task type (classification/segmentation).
371383
Defaults to ``None``
384+
max_drop (float): Maximum acceptable accuracy drop during quantization.
385+
Value should be between 0 and 1 (e.g., 0.01 means 1% drop is acceptable).
386+
Defaults to ``0.01``
372387
373388
Returns:
374389
CompiledModel: Quantized OpenVINO model
375390
376391
Raises:
377-
ValueError: If datamodule or metric is not provided
392+
ValueError: If datamodule is not provided, or if max_drop is out of valid range
378393
"""
379394
import nncf
380395

@@ -386,9 +401,25 @@ def _accuracy_control_quantization_ov(
386401
# if task is not provided, use the task from the datamodule
387402
task = task or datamodule.task
388403

389-
if metric is None:
390-
msg = "Metric must be provided for OpenVINO INT8_ACQ compression"
404+
# Validate max_drop parameter
405+
if not 0 <= max_drop <= 1:
406+
msg = f"max_drop must be between 0 and 1, got {max_drop}"
391407
raise ValueError(msg)
408+
if max_drop > 0.1:
409+
logger.warning(
410+
f"max_drop={max_drop} is a large value (>10%% accuracy drop). "
411+
"Typical values are in the 0.01-0.03 range (1-3%%).",
412+
)
413+
414+
# Set default metric if not provided
415+
if metric is None:
416+
from anomalib.metrics import F1Score
417+
418+
metric = F1Score(fields=["pred_label", "gt_label"])
419+
logger.info(
420+
"No metric provided for INT8_ACQ quantization. "
421+
"Using default: F1Score at image level (fields=['pred_label', 'gt_label']).",
422+
)
392423

393424
model_input = model.input(0)
394425

@@ -408,12 +439,28 @@ def _accuracy_control_quantization_ov(
408439
# validation function to evaluate the quality loss after quantization
409440
def val_fn(nncf_model: "CompiledModel", validation_data: Iterable) -> float:
410441
for batch in validation_data:
411-
preds = torch.from_numpy(nncf_model(batch["image"])[0])
412-
target = batch["label"] if task == TaskType.CLASSIFICATION else batch["mask"][:, None, :, :]
413-
metric.update(preds, target)
442+
ov_model_output = nncf_model(batch["image"])
443+
result_batch = ImageBatch(
444+
image=batch["image"],
445+
# pred_score must be same size as gt_label for metrics like AUROC
446+
pred_score=torch.from_numpy(ov_model_output["pred_score"]).squeeze(),
447+
pred_label=torch.from_numpy(ov_model_output["pred_label"]).squeeze(),
448+
gt_label=batch["gt_label"],
449+
anomaly_map=torch.from_numpy(ov_model_output["anomaly_map"]),
450+
pred_mask=torch.from_numpy(ov_model_output["pred_mask"]),
451+
gt_mask=batch["gt_mask"][:, None, :, :], # Make shape the same format as pred_mask
452+
)
453+
metric.update(result_batch)
454+
414455
return metric.compute()
415456

416-
return nncf.quantize_with_accuracy_control(model, calibration_dataset, validation_dataset, val_fn)
457+
return nncf.quantize_with_accuracy_control(
458+
model,
459+
calibration_dataset,
460+
validation_dataset,
461+
val_fn,
462+
max_drop=max_drop,
463+
)
417464

418465

419466
def _create_export_root(export_root: str | Path, export_type: ExportType) -> Path:

0 commit comments

Comments
 (0)