apocaliss92 commited on
Commit
2991ff3
·
verified ·
1 Parent(s): 570ec7f

source: D798 INT8 recipe (Detect head in float, MIXED, full bias correction); archive the 2026-10-08 convert_lib

Browse files
source/README.md CHANGED
@@ -89,11 +89,44 @@ INT8 files are skipped and the run says so.
89
  Until 2026-10-08 the calibration padded TOP-LEFT. On that date every INT8 IR
90
  was re-quantised centred from the hosted ONNX (`--reuse-onnx`, OpenVINO
91
  2026.2.1 + NNCF 3.2.0) and measured against the old one. 13 replaced the old
92
- IR; 8 measured worse and were kept, so they still carry the top-left
93
- calibration. `build-camstack-models.py` lists which is which. The kept ones are
94
- reproduced by [`archive/convert_lib-before-2026-10-08.py`](archive/convert_lib-before-2026-10-08.py),
95
- which rebuilds the hosted `yolov9m-640` INT8 IR bit for bit. The replaced old
96
- IRs are under `archive/2026-10-08-int8-topleft-calibration/` at the mirror root.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
  ## Licences
99
 
 
89
  Until 2026-10-08 the calibration padded TOP-LEFT. On that date every INT8 IR
90
  was re-quantised centred from the hosted ONNX (`--reuse-onnx`, OpenVINO
91
  2026.2.1 + NNCF 3.2.0) and measured against the old one. 13 replaced the old
92
+ IR, 8 measured worse and were kept. The replaced IRs are under
93
+ `archive/2026-10-08-int8-topleft-calibration/` at the mirror root.
94
+ [`archive/convert_lib-before-2026-10-08.py`](archive/convert_lib-before-2026-10-08.py)
95
+ reproduces the top-left ones; it rebuilds the hosted `yolov9m-640` INT8 IR bit
96
+ for bit.
97
+
98
+ ### INT8 recipe (since 2026-10-09)
99
+
100
+ `convert_lib.quantize_openvino_int8` keeps the YOLO Detect head in float.
101
+
102
+ - **YOLOv9:** the head's own ops stay float: the DFL softmax and the box
103
+ decode (anchor ± distance, ×stride, class sigmoid). The conv branches stay
104
+ INT8.
105
+ - **YOLO26:** the whole end-to-end head stays float.
106
+ - **Every model:** NNCF preset MIXED and full bias correction.
107
+
108
+ This follows the Ultralytics OpenVINO INT8 exporter and NNCF's YOLO26 example.
109
+ The previous recipe quantised the decode, which put box coordinates on a
110
+ 256-level grid.
111
+
112
+ **Result:**
113
+
114
+ - On all 8 YOLOv9 IRs, mAP50-95 went from 0.01–0.18 below float to within noise
115
+ of it, at NPU latency within 1 %.
116
+ - 14 IRs were replaced in place: every `yolov9*-int8`, `yolo26{n,s,l}-int8`,
117
+ `yolo26n-320-int8` and `yolo26{n,s}-256-int8`.
118
+ - The previous files are under `archive/2026-10-09-int8-before-head-float-recipe/`
119
+ at the mirror root.
120
+
121
+ Seven YOLO26 IRs were kept, because the float head cost 11–22 % latency on the
122
+ Intel iGPU:
123
+
124
+ - `yolo26{m,x}-int8` and `yolo26{m,l}-256-int8` (centred) are reproduced by
125
+ [`archive/convert_lib-before-2026-10-09.py`](archive/convert_lib-before-2026-10-09.py);
126
+ - `yolo26{s,m,l}-320-int8` (top-left) are reproduced by
127
+ `archive/convert_lib-before-2026-10-08.py`.
128
+
129
+ `build-camstack-models.py` lists which IR carries which recipe.
130
 
131
  ## Licences
132
 
source/archive/convert_lib-before-2026-10-09.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared model converter: ONNX -> OpenVINO IR (fp16/int8/CoreML) + validation.
2
+
3
+ Used by both the runtime model-convert cap (convert.py) and the offline
4
+ scripts/build-camstack-models.py. Conversion ALWAYS starts from ONNX via
5
+ ov.convert_model (never the Ultralytics OV exporter, which emits unnamed
6
+ tensors that break the runtime's named-output postprocess)."""
7
+ from __future__ import annotations
8
+ import os
9
+ from pathlib import Path
10
+
11
+ # OpenVINO sizes the pool that COMPILES a model to the machine's core count and
12
+ # TBB never gives those workers back. A container cgroup counts THREADS, and
13
+ # this one has been measured refusing forks against `pids.max=2048` — so a
14
+ # conversion running beside the live pools must not spend `nproc` of the budget
15
+ # just to validate an IR. Same knob, same env var and same default (4) as
16
+ # OV_COMPILATION_NUM_THREADS in
17
+ # packages/addon-pipeline/python/inference_pool.py, which carries the
18
+ # measurement; this process is short-lived, so the saving here is a spike, not
19
+ # the 214-thread accumulation that motivated the bound. 0 disables it.
20
+ _OV_COMPILATION_NUM_THREADS: int = 4
21
+ try:
22
+ _OV_COMPILATION_NUM_THREADS = int(
23
+ os.environ.get("CAMSTACK_OV_COMPILATION_THREADS", "4")
24
+ )
25
+ except ValueError:
26
+ _OV_COMPILATION_NUM_THREADS = 4
27
+
28
+
29
+ def export_openvino(onnx_path: str, out_xml: str, *, fp16: bool) -> None:
30
+ import openvino as ov
31
+ ov_model = ov.convert_model(onnx_path)
32
+ Path(out_xml).parent.mkdir(parents=True, exist_ok=True)
33
+ ov.save_model(ov_model, out_xml, compress_to_fp16=fp16)
34
+
35
+
36
+ def validate_openvino_ir(xml_path: str) -> None:
37
+ """Compile the IR and assert every input/output tensor carries a name.
38
+ This is the exact defect (unnamed tensors) that broke 14/26 catalog IRs."""
39
+ import openvino as ov
40
+ core = ov.Core()
41
+ model = core.read_model(xml_path)
42
+ # Bound the compilation pool (see _OV_COMPILATION_NUM_THREADS). A plugin
43
+ # REJECTS a property it does not know, and this compile is the VALIDATION —
44
+ # it must never fail for a reason that has nothing to do with the IR, so a
45
+ # refused property falls back to the unbounded compile rather than
46
+ # reporting a bad model.
47
+ if _OV_COMPILATION_NUM_THREADS > 0:
48
+ try:
49
+ compiled = core.compile_model(
50
+ model,
51
+ "CPU",
52
+ {"COMPILATION_NUM_THREADS": _OV_COMPILATION_NUM_THREADS},
53
+ )
54
+ except Exception:
55
+ compiled = core.compile_model(model, "CPU")
56
+ else:
57
+ compiled = core.compile_model(model, "CPU")
58
+ for port in list(compiled.inputs) + list(compiled.outputs):
59
+ try:
60
+ name = port.get_any_name()
61
+ except Exception as exc: # ov raises when a tensor has no names
62
+ raise ValueError(f"IR {xml_path} has an unnamed tensor: {exc}") from exc
63
+ if not name:
64
+ raise ValueError(f"IR {xml_path} has an empty-named tensor")
65
+
66
+
67
+ CALIBRATION_SUFFIXES = {".jpg", ".jpeg", ".png"}
68
+
69
+
70
+ def calibration_images(calib_dir: str, max_samples: int) -> "list[Path]":
71
+ """The first ``max_samples`` calibration images of ``calib_dir``, in path order.
72
+
73
+ A flat folder (COCO val2017) is read exactly as before: file-name order. A
74
+ folder is walked recursively, so the operator's own frames work as they
75
+ come. The hub's annotated export (a folder holding ``annotations.json``)
76
+ calibrates on its ``frames/`` only: its ``crops/`` are classifier inputs,
77
+ not what a root detector is fed.
78
+ """
79
+ root = Path(calib_dir)
80
+ if not root.is_dir():
81
+ raise ValueError(f"calibration folder {calib_dir} does not exist")
82
+ base = root / "frames" if (root / "annotations.json").is_file() else root
83
+ images = sorted(
84
+ (p for p in base.rglob("*") if p.is_file() and p.suffix.lower() in CALIBRATION_SUFFIXES),
85
+ key=lambda p: p.relative_to(base).as_posix(),
86
+ )[:max_samples]
87
+ if not images:
88
+ raise ValueError(f"no calibration images in {calib_dir}")
89
+ return images
90
+
91
+
92
+ def letterbox_for_calibration(rgb, imgsz: int):
93
+ """``rgb`` (HxWx3 uint8) → ``[1,3,imgsz,imgsz]`` float32 in [0,1], letterboxed
94
+ as the inference pool does (``inference_pool.letterbox_image``): scale by the
95
+ long side, truncate, CENTRE with grey 114 padding."""
96
+ import cv2
97
+ import numpy as np
98
+
99
+ h, w = rgb.shape[:2]
100
+ scale = imgsz / max(h, w)
101
+ nw, nh = int(w * scale), int(h * scale)
102
+ pad_x, pad_y = (imgsz - nw) // 2, (imgsz - nh) // 2
103
+ resized = rgb if (nw, nh) == (w, h) else cv2.resize(rgb, (nw, nh), interpolation=cv2.INTER_LINEAR)
104
+ canvas = np.full((imgsz, imgsz, 3), 114, dtype=np.uint8)
105
+ canvas[pad_y : pad_y + nh, pad_x : pad_x + nw] = resized
106
+ chw = canvas.transpose(2, 0, 1).astype(np.float32) / 255.0
107
+ return chw[np.newaxis, ...]
108
+
109
+
110
+ def quantize_openvino_int8(
111
+ onnx_path: str, out_xml: str, calib_dir: str, imgsz: int, max_samples: int = 300
112
+ ) -> None:
113
+ import cv2
114
+ import openvino as ov
115
+ import nncf
116
+
117
+ images = calibration_images(calib_dir, max_samples)
118
+
119
+ def preprocess(path: Path):
120
+ img = cv2.imread(str(path))
121
+ return letterbox_for_calibration(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), imgsz)
122
+
123
+ dataset = nncf.Dataset(images, lambda p: preprocess(p))
124
+ ov_model = ov.convert_model(onnx_path)
125
+ quantized = nncf.quantize(ov_model, dataset, subset_size=len(images))
126
+ Path(out_xml).parent.mkdir(parents=True, exist_ok=True)
127
+ ov.save_model(quantized, out_xml)
128
+
129
+
130
+ def export_coreml_from_onnx(
131
+ onnx_path: str,
132
+ out_pkg: str,
133
+ imgsz: int,
134
+ *,
135
+ input_shape: "tuple[int, ...] | None" = None,
136
+ fp32: bool = False,
137
+ ) -> None:
138
+ """ONNX -> CoreML mlpackage (darwin only).
139
+
140
+ Modern coremltools (>=6) dropped the ONNX frontend entirely — ``ct.convert``
141
+ only accepts TensorFlow / PyTorch / MIL sources, so feeding it an ``.onnx``
142
+ raises "Unable to determine the type of the model". We bridge through
143
+ PyTorch: ``onnx2torch`` rebuilds the ONNX graph as an ``nn.Module``, we trace
144
+ it with a representative image input, then coremltools converts the
145
+ TorchScript. Image models are NCHW ``(1, 3, imgsz, imgsz)``.
146
+
147
+ ``input_shape`` overrides that square-NCHW assumption for models whose input
148
+ is neither square nor channels-first — the plate recognizer is NHWC
149
+ ``(1, 64, 128, 3)``. Without it the trace runs on a wrongly-shaped tensor and
150
+ either throws deep inside the graph or, worse, produces a package whose
151
+ declared input silently disagrees with the real one. ``imgsz`` stays the
152
+ positional contract for every existing caller.
153
+
154
+ ``fp32`` keeps full precision instead of coremltools' default fp16 weights.
155
+ Detectors and embedders absorb fp16 — a box moves a fraction of a pixel, a
156
+ cosine moves 5e-5 — but a RECOGNIZER cannot: its output is an argmax plus a
157
+ confidence threshold, so a small shift in one softmax flips a stored
158
+ character or silences a real read. Measured on the plate recognizer, fp16
159
+ moved the weakest-character probability by up to 0.16 and changed the label
160
+ on a ground-truthed plate. The default stays fp16 so nothing already
161
+ published moves.
162
+
163
+ Dynamic-shape models (e.g. YOLO) emit ``Shape`` ops that coremltools' Torch
164
+ converter cannot lower ("Converter is not implemented … 'Shape'"). We first
165
+ run onnx-simplifier with the input shape pinned, which constant-folds those
166
+ Shape ops away, so the traced graph is static and convertible.
167
+ """
168
+ import coremltools as ct
169
+ import onnx
170
+ import torch
171
+ from onnx2torch import convert as onnx_to_torch
172
+ from onnxsim import simplify
173
+
174
+ shape = list(input_shape) if input_shape is not None else [1, 3, imgsz, imgsz]
175
+ onnx_model = onnx.load(onnx_path)
176
+ input_name = onnx_model.graph.input[0].name
177
+ simplified, ok = simplify(onnx_model, overwrite_input_shapes={input_name: shape})
178
+ if not ok:
179
+ simplified = onnx_model # fall back to the original if simplify bailed
180
+
181
+ model = onnx_to_torch(simplified).eval()
182
+ example = torch.rand(*shape)
183
+ with torch.no_grad():
184
+ traced = torch.jit.trace(model, example, strict=False)
185
+
186
+ mlmodel = ct.convert(
187
+ traced,
188
+ source="pytorch",
189
+ inputs=[ct.TensorType(name="input", shape=example.shape)],
190
+ minimum_deployment_target=ct.target.iOS16,
191
+ **({"compute_precision": ct.precision.FLOAT32} if fp32 else {}),
192
+ )
193
+ Path(out_pkg).parent.mkdir(parents=True, exist_ok=True)
194
+ mlmodel.save(out_pkg)
195
+
196
+
197
+ def validate_coreml(pkg_path: str) -> None:
198
+ import coremltools as ct
199
+ ct.models.MLModel(pkg_path) # load; raises if malformed
source/build-camstack-models.py CHANGED
@@ -45,7 +45,10 @@ Formats:
45
  int8 convert_lib.quantize_openvino_int8: NNCF post-training quantisation
46
  of the ONNX, calibrated on the first N images (sorted by path,
47
  recursively; an export folder's frames/ only) of --calib-dir
48
- (default $CAMSTACK_CALIB_DIR or ./calibration-images)
 
 
 
49
 
50
  INT8 calibration: the hosted INT8 IRs record subset_size 128 (YOLO26) and 300
51
  (YOLOv9), drawn from COCO; an IR does not record WHICH images. The documented
@@ -56,19 +59,22 @@ exactly as the CamStack runtime feeds the model (the padding sat top-left until
56
  2026-10-08, which the runtime never does). A different calibration set yields an
57
  equivalent but not bit-identical IR (NNCF picks ranges from the data).
58
 
59
- Which calibration each hosted INT8 IR carries (measured, D739 holdout +
60
- COCO; docs/benchmarks/2026-10-08-int8-recalibration.md). All were built from
61
- the HOSTED ONNX with OpenVINO 2026.2.1 + NNCF 3.2.0 on the first N COCO val2017
62
- images (reproduce with `--reuse-onnx`, the mirror's .onnx placed under --out):
63
- centred (this script, since 2026-10-08):
64
- yolov9{t,s,m}-320, yolov9s-640, yolo26n-320, yolo26{n,m,l,x} @640,
65
- yolo26{n,s,m,l}-256
66
- top-left (convert_lib as it was before 2026-10-08 — kept: re-quantising them
67
- centred measured WORSE, beyond noise):
68
- yolov9m-640, yolov9t-640, yolov9c-320, yolov9c-640, yolo26s @640,
 
 
 
69
  yolo26{s,m,l}-320
70
- The top-left padding (`canvas[:nh, :nw]`, sizes by round()) reproduces the
71
- hosted yolov9m-640 IR bit for bit.
72
 
73
  Toolchain: each recipe names the Ultralytics / torch / coremltools / OpenVINO
74
  versions its hosted files record. Other versions produce the same model with a
 
45
  int8 convert_lib.quantize_openvino_int8: NNCF post-training quantisation
46
  of the ONNX, calibrated on the first N images (sorted by path,
47
  recursively; an export folder's frames/ only) of --calib-dir
48
+ (default $CAMSTACK_CALIB_DIR or ./calibration-images). Recipe (D798):
49
+ preset MIXED, full bias correction, the Detect head in float —
50
+ its own ops (DFL + box decode) for a YOLOv9 head, all of it for
51
+ YOLO26's end-to-end head; every conv before it is INT8
52
 
53
  INT8 calibration: the hosted INT8 IRs record subset_size 128 (YOLO26) and 300
54
  (YOLOv9), drawn from COCO; an IR does not record WHICH images. The documented
 
59
  2026-10-08, which the runtime never does). A different calibration set yields an
60
  equivalent but not bit-identical IR (NNCF picks ranges from the data).
61
 
62
+ Which recipe each hosted INT8 IR carries. All were built from the HOSTED ONNX
63
+ with OpenVINO 2026.2.1 + NNCF 3.2.0 on the first N COCO val2017 images
64
+ (reproduce with `--reuse-onnx`, the mirror's .onnx placed under --out):
65
+ D798 recipe (this script, since 2026-10-09 — MIXED, full bias correction,
66
+ the Detect head in float; docs/benchmarks/2026-10-09-int8-recipe.md):
67
+ yolov9{t,s,m,c}-{320,640}, yolo26{n,s,l} @640, yolo26n-320,
68
+ yolo26{n,s}-256
69
+ NNCF defaults, centred calibration (convert_lib 2026-10-08 → 2026-10-09,
70
+ the mirror's source/archive/convert_lib-before-2026-10-09.py — kept: the D798
71
+ build was more accurate but 11-22 % slower on the Intel iGPU):
72
+ yolo26{m,x} @640, yolo26{m,l}-256
73
+ NNCF defaults, top-left calibration (convert_lib before 2026-10-08,
74
+ source/archive/convert_lib-before-2026-10-08.py — kept, same reason):
75
  yolo26{s,m,l}-320
76
+ The D798 build reproduces every replaced IR's .bin bit for bit
77
+ (docs/benchmarks/2026-10-09-int8-recipe.md).
78
 
79
  Toolchain: each recipe names the Ultralytics / torch / coremltools / OpenVINO
80
  versions its hosted files record. Other versions produce the same model with a
source/convert_lib.py CHANGED
@@ -6,7 +6,9 @@ ov.convert_model (never the Ultralytics OV exporter, which emits unnamed
6
  tensors that break the runtime's named-output postprocess)."""
7
  from __future__ import annotations
8
  import os
 
9
  from pathlib import Path
 
10
 
11
  # OpenVINO sizes the pool that COMPILES a model to the machine's core count and
12
  # TBB never gives those workers back. A container cgroup counts THREADS, and
@@ -107,9 +109,55 @@ def letterbox_for_calibration(rgb, imgsz: int):
107
  return chw[np.newaxis, ...]
108
 
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  def quantize_openvino_int8(
111
  onnx_path: str, out_xml: str, calib_dir: str, imgsz: int, max_samples: int = 300
112
  ) -> None:
 
 
 
 
 
 
113
  import cv2
114
  import openvino as ov
115
  import nncf
@@ -122,7 +170,18 @@ def quantize_openvino_int8(
122
 
123
  dataset = nncf.Dataset(images, lambda p: preprocess(p))
124
  ov_model = ov.convert_model(onnx_path)
125
- quantized = nncf.quantize(ov_model, dataset, subset_size=len(images))
 
 
 
 
 
 
 
 
 
 
 
126
  Path(out_xml).parent.mkdir(parents=True, exist_ok=True)
127
  ov.save_model(quantized, out_xml)
128
 
 
6
  tensors that break the runtime's named-output postprocess)."""
7
  from __future__ import annotations
8
  import os
9
+ import re
10
  from pathlib import Path
11
+ from typing import Iterable, Optional
12
 
13
  # OpenVINO sizes the pool that COMPILES a model to the machine's core count and
14
  # TBB never gives those workers back. A container cgroup counts THREADS, and
 
109
  return chw[np.newaxis, ...]
110
 
111
 
112
+ _HEAD_MODULE = re.compile(r"^(/model\.\d+)/")
113
+
114
+
115
+ def detect_head_prefix(ops: "Iterable[tuple[str, str]]") -> "Optional[str]":
116
+ """The Ultralytics Detect module (``/model.22`` for YOLOv9, ``/model.23``
117
+ for YOLO26): the module holding the graph's LAST Sigmoid, which is the
118
+ class-score sigmoid (the Ultralytics exporter finds its head the same way).
119
+ ``ops`` is ``(friendly_name, type_name)`` in graph order. None when the
120
+ model is not an Ultralytics export — it is then quantised whole."""
121
+ last = None
122
+ for name, kind in ops:
123
+ if kind == "Sigmoid":
124
+ last = name
125
+ match = _HEAD_MODULE.match(last) if last else None
126
+ return match.group(1) if match else None
127
+
128
+
129
+ def is_end_to_end_head(names: "Iterable[str]", head: str) -> bool:
130
+ """An NMS-free (end-to-end) head — YOLO26's — runs its ``one2one_*`` branches."""
131
+ return any(n.startswith(f"{head}/one2one") for n in names)
132
+
133
+
134
+ def int8_ignored_patterns(head: str, end_to_end: bool) -> "list[str]":
135
+ """NNCF ignored-scope patterns for the Detect head ``head``.
136
+
137
+ - DFL head (YOLOv9): the head's OWN ops stay float — the DFL and the box
138
+ decode (concat/split, anchor ± distance, ×stride, the class sigmoid). The
139
+ conv branches (``cv2.*``/``cv3.*``) stay INT8.
140
+ - End-to-end head (YOLO26): the whole head stays float, its ``one2one``
141
+ convs included, as NNCF's YOLO26 example keeps it; measured better than
142
+ the decode alone on YOLO26 (docs/benchmarks/2026-10-09-int8-recipe.md).
143
+
144
+ NNCF matches with ``re.match`` — a prefix match — so the own-op pattern is
145
+ anchored with ``$`` and every pattern ends at a module boundary."""
146
+ h = re.escape(head)
147
+ if end_to_end:
148
+ return [rf"{h}/"]
149
+ return [rf"{h}/[^/]+$", rf"{h}/dfl/"]
150
+
151
+
152
  def quantize_openvino_int8(
153
  onnx_path: str, out_xml: str, calib_dir: str, imgsz: int, max_samples: int = 300
154
  ) -> None:
155
+ """NNCF post-training INT8. For an Ultralytics detector (D798): preset MIXED
156
+ (asymmetric activations), full bias correction, and the Detect head in
157
+ float — its own ops for a DFL head, all of it for an end-to-end one
158
+ (`int8_ignored_patterns`). Any other model keeps NNCF's defaults. Measured in
159
+ docs/benchmarks/2026-10-09-int8-recipe.md: the decode in INT8 was where
160
+ the hosted IRs lost their box precision."""
161
  import cv2
162
  import openvino as ov
163
  import nncf
 
170
 
171
  dataset = nncf.Dataset(images, lambda p: preprocess(p))
172
  ov_model = ov.convert_model(onnx_path)
173
+ ops = [(op.get_friendly_name(), op.get_type_name()) for op in ov_model.get_ordered_ops()]
174
+ head = detect_head_prefix(ops)
175
+ options: dict = {}
176
+ if head:
177
+ end_to_end = is_end_to_end_head((name for name, _ in ops), head)
178
+ options = {
179
+ "preset": nncf.QuantizationPreset.MIXED,
180
+ "fast_bias_correction": False,
181
+ "ignored_scope": nncf.IgnoredScope(patterns=int8_ignored_patterns(head, end_to_end), validate=False),
182
+ }
183
+ # Not an Ultralytics detector: NNCF defaults, as before D798 (unmeasured otherwise).
184
+ quantized = nncf.quantize(ov_model, dataset, subset_size=len(images), **options)
185
  Path(out_xml).parent.mkdir(parents=True, exist_ok=True)
186
  ov.save_model(quantized, out_xml)
187