source: every CoreML recipe exports FP16 (YOLOv9 packages replaced in place)
Browse files- source/README.md +12 -3
- source/build-camstack-models.py +39 -17
source/README.md
CHANGED
|
@@ -38,10 +38,10 @@ What the recipes encode (the hosted files record each of these):
|
|
| 38 |
|
| 39 |
| Folder | Files | ONNX | CoreML | OpenVINO IR |
|
| 40 |
| --- | --- | --- | --- | --- |
|
| 41 |
-
| `objectDetection/yolo26` | `camstack-yolo26{n,s,m,l,x}` @640 | static, opset 17 | static, batch 1 | FP16 as `<stem>.xml` and `<stem>-fp16.xml`; INT8 `<stem>-int8.xml` (128 images) |
|
| 42 |
| `objectDetection/yolo26` | `camstack-yolo26{n,s,m,l}-{320,256}` | dynamic axes, opset 17 | static, batch 1, FP16 | FP32 `<stem>.xml`; INT8 `<stem>-int8.xml` (128 images) |
|
| 43 |
-
| `objectDetection/yolov9` | `camstack-yolov9{t,s,m,c}-{320,640}` | static, opset 13 | static, batch 1 | FP16 `<stem>-fp16.xml`; INT8 `<stem>-int8.xml` (300 images) |
|
| 44 |
-
| `segmentation/yolo26-seg` | `camstack-yolo26{n,s,m}-seg` @640 | static, opset 17 | static, batch 1 | FP16 `<stem>.xml` |
|
| 45 |
|
| 46 |
Recorded tool versions (Ultralytics / PyTorch / coremltools / OpenVINO):
|
| 47 |
YOLO26 @640 and YOLO26-seg 8.4.24 / 2.10.0 / 9.0 / 2026.2.1; YOLO26 @320 and
|
|
@@ -58,6 +58,15 @@ flexible batch. They are now exported with a static batch of 1 and were
|
|
| 58 |
replaced in place, under the same names. The script refuses a batch-flexible
|
| 59 |
CoreML recipe for a YOLO26 checkpoint.
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
### INT8 calibration
|
| 62 |
|
| 63 |
The INT8 IRs are NNCF post-training quantisation of the ONNX
|
|
|
|
| 38 |
|
| 39 |
| Folder | Files | ONNX | CoreML | OpenVINO IR |
|
| 40 |
| --- | --- | --- | --- | --- |
|
| 41 |
+
| `objectDetection/yolo26` | `camstack-yolo26{n,s,m,l,x}` @640 | static, opset 17 | static, batch 1, FP16 | FP16 as `<stem>.xml` and `<stem>-fp16.xml`; INT8 `<stem>-int8.xml` (128 images) |
|
| 42 |
| `objectDetection/yolo26` | `camstack-yolo26{n,s,m,l}-{320,256}` | dynamic axes, opset 17 | static, batch 1, FP16 | FP32 `<stem>.xml`; INT8 `<stem>-int8.xml` (128 images) |
|
| 43 |
+
| `objectDetection/yolov9` | `camstack-yolov9{t,s,m,c}-{320,640}` | static, opset 13 | static, batch 1, FP16 | FP16 `<stem>-fp16.xml`; INT8 `<stem>-int8.xml` (300 images) |
|
| 44 |
+
| `segmentation/yolo26-seg` | `camstack-yolo26{n,s,m}-seg` @640 | static, opset 17 | static, batch 1, FP16 | FP16 `<stem>.xml` |
|
| 45 |
|
| 46 |
Recorded tool versions (Ultralytics / PyTorch / coremltools / OpenVINO):
|
| 47 |
YOLO26 @640 and YOLO26-seg 8.4.24 / 2.10.0 / 9.0 / 2026.2.1; YOLO26 @320 and
|
|
|
|
| 58 |
replaced in place, under the same names. The script refuses a batch-flexible
|
| 59 |
CoreML recipe for a YOLO26 checkpoint.
|
| 60 |
|
| 61 |
+
Every CoreML package computes in FP16, the only precision the Apple Neural
|
| 62 |
+
Engine runs. An FP32 package runs on the GPU or the CPU instead, 2–4× slower.
|
| 63 |
+
The YOLOv9 packages were FP32 until 2026-10-07: Ultralytics 8.4.98 pins FP32
|
| 64 |
+
unless `half=True` is passed. They were re-exported FP16 and replaced in place,
|
| 65 |
+
under the same names. The YOLO26 @640 and YOLO26-seg packages record
|
| 66 |
+
`half: False` but are FP16, because Ultralytics 8.4.24 left Core ML's FP16
|
| 67 |
+
default in place; their recipes now pass `half=True` so a current Ultralytics
|
| 68 |
+
gives the same precision. The script refuses an FP32 CoreML recipe.
|
| 69 |
+
|
| 70 |
### INT8 calibration
|
| 71 |
|
| 72 |
The INT8 IRs are NNCF post-training quantisation of the ONNX
|
source/build-camstack-models.py
CHANGED
|
@@ -33,7 +33,8 @@ e.g. exported-models/objectDetection/yolo26/openvino/camstack-yolo26n-320-int8.x
|
|
| 33 |
|
| 34 |
Formats:
|
| 35 |
onnx Ultralytics export(format="onnx") from the upstream .pt
|
| 36 |
-
coreml Ultralytics export(format="coreml") from the upstream .pt (macOS only)
|
|
|
|
| 37 |
openvino convert_lib.export_openvino: ov.convert_model(ONNX) + ov.save_model
|
| 38 |
(compress_to_fp16 per precision) — never the Ultralytics OV exporter
|
| 39 |
int8 convert_lib.quantize_openvino_int8: NNCF post-training quantisation
|
|
@@ -160,16 +161,19 @@ FP16_IR = OpenvinoOutput('', 'fp16')
|
|
| 160 |
|
| 161 |
|
| 162 |
def _yolo26_full(tier: str) -> Recipe:
|
| 163 |
-
"""yolo26<tier> @640: static ONNX, static
|
| 164 |
|
| 165 |
The unsuffixed IR and `-fp16` IR are the same compressed-to-FP16 weights;
|
| 166 |
-
the catalog addresses both names.
|
|
|
|
|
|
|
|
|
|
| 167 |
"""
|
| 168 |
return Recipe(
|
| 169 |
name=f'camstack-yolo26{tier}', folder='objectDetection/yolo26', imgsz=640,
|
| 170 |
toolchain=TC_2604, checkpoint=f'yolo26{tier}',
|
| 171 |
onnx=OnnxExport(dynamic=False, opset=17),
|
| 172 |
-
coreml=CoremlExport(dynamic=False, batch=1, half=
|
| 173 |
openvino=(FP16_IR, OpenvinoOutput('-fp16', 'fp16'), OpenvinoOutput('-int8', 'int8')),
|
| 174 |
calib_samples=128,
|
| 175 |
)
|
|
@@ -199,12 +203,19 @@ def _yolo26_reduced(tier: str, res: int) -> Recipe:
|
|
| 199 |
|
| 200 |
|
| 201 |
def _yolov9(tier: str, res: int) -> Recipe:
|
| 202 |
-
"""yolov9<tier>-<res>: static opset-13 ONNX, static CoreML, IR fp16 + INT8.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
return Recipe(
|
| 204 |
name=f'camstack-yolov9{tier}-{res}', folder='objectDetection/yolov9', imgsz=res,
|
| 205 |
toolchain=TC_2607_YOLOV9, checkpoint=f'yolov9{tier}',
|
| 206 |
onnx=OnnxExport(dynamic=False, opset=13),
|
| 207 |
-
coreml=CoremlExport(dynamic=False, batch=1, half=
|
| 208 |
openvino=(OpenvinoOutput('-fp16', 'fp16'), OpenvinoOutput('-int8', 'int8')),
|
| 209 |
calib_samples=300,
|
| 210 |
)
|
|
@@ -215,7 +226,7 @@ def _yolo26_seg(tier: str) -> Recipe:
|
|
| 215 |
name=f'camstack-yolo26{tier}-seg', folder='segmentation/yolo26-seg', imgsz=640,
|
| 216 |
toolchain=TC_2604, checkpoint=f'yolo26{tier}-seg',
|
| 217 |
onnx=OnnxExport(dynamic=False, opset=17),
|
| 218 |
-
coreml=CoremlExport(dynamic=False, batch=1, half=
|
| 219 |
openvino=(FP16_IR,),
|
| 220 |
)
|
| 221 |
|
|
@@ -234,14 +245,26 @@ GATHER_ND_CHECKPOINT_PREFIX = 'yolo26'
|
|
| 234 |
|
| 235 |
|
| 236 |
def check_recipes(recipes: tuple[Recipe, ...]) -> list[str]:
|
| 237 |
-
"""Recipes whose CoreML export
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
|
| 247 |
FAMILIES = {
|
|
@@ -478,7 +501,7 @@ def adhoc_recipe(args: argparse.Namespace) -> Recipe:
|
|
| 478 |
name=name, folder=args.folder, imgsz=args.imgsz,
|
| 479 |
toolchain=Toolchain('any', 'any', 'any', 'any'), checkpoint=args.model,
|
| 480 |
onnx=OnnxExport(dynamic=args.dynamic, opset=args.opset),
|
| 481 |
-
coreml=CoremlExport(dynamic=args.dynamic, batch=16 if args.dynamic else 1, half=
|
| 482 |
openvino=tuple(OpenvinoOutput(f'-{p}', p) for p in precisions),
|
| 483 |
calib_samples=args.calib_samples or 300,
|
| 484 |
)
|
|
@@ -556,7 +579,6 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
| 556 |
adhoc.add_argument('--folder', default='custom')
|
| 557 |
adhoc.add_argument('--dynamic', action='store_true', help='dynamic axes (ONNX) / flexible batch (CoreML)')
|
| 558 |
adhoc.add_argument('--opset', type=int, default=17)
|
| 559 |
-
adhoc.add_argument('--half', action='store_true', help='FP16 CoreML')
|
| 560 |
adhoc.add_argument('--openvino', help='IR precisions, comma-separated: fp32,fp16,int8')
|
| 561 |
return p.parse_args(argv)
|
| 562 |
|
|
|
|
| 33 |
|
| 34 |
Formats:
|
| 35 |
onnx Ultralytics export(format="onnx") from the upstream .pt
|
| 36 |
+
coreml Ultralytics export(format="coreml") from the upstream .pt (macOS only),
|
| 37 |
+
always FP16: an FP32 package never runs on the Neural Engine
|
| 38 |
openvino convert_lib.export_openvino: ov.convert_model(ONNX) + ov.save_model
|
| 39 |
(compress_to_fp16 per precision) — never the Ultralytics OV exporter
|
| 40 |
int8 convert_lib.quantize_openvino_int8: NNCF post-training quantisation
|
|
|
|
| 161 |
|
| 162 |
|
| 163 |
def _yolo26_full(tier: str) -> Recipe:
|
| 164 |
+
"""yolo26<tier> @640: static ONNX, static FP16 CoreML, IR fp16 twice + INT8.
|
| 165 |
|
| 166 |
The unsuffixed IR and `-fp16` IR are the same compressed-to-FP16 weights;
|
| 167 |
+
the catalog addresses both names. The hosted CoreML packages record
|
| 168 |
+
`half: False` and still compute in FP16: Ultralytics 8.4.24 left Core ML's
|
| 169 |
+
FP16 default in place. Later Ultralytics pins FP32 unless FP16 is asked
|
| 170 |
+
for, so the recipe asks (`check_recipes`, D743).
|
| 171 |
"""
|
| 172 |
return Recipe(
|
| 173 |
name=f'camstack-yolo26{tier}', folder='objectDetection/yolo26', imgsz=640,
|
| 174 |
toolchain=TC_2604, checkpoint=f'yolo26{tier}',
|
| 175 |
onnx=OnnxExport(dynamic=False, opset=17),
|
| 176 |
+
coreml=CoremlExport(dynamic=False, batch=1, half=True),
|
| 177 |
openvino=(FP16_IR, OpenvinoOutput('-fp16', 'fp16'), OpenvinoOutput('-int8', 'int8')),
|
| 178 |
calib_samples=128,
|
| 179 |
)
|
|
|
|
| 203 |
|
| 204 |
|
| 205 |
def _yolov9(tier: str, res: int) -> Recipe:
|
| 206 |
+
"""yolov9<tier>-<res>: static opset-13 ONNX, static FP16 CoreML, IR fp16 + INT8.
|
| 207 |
+
|
| 208 |
+
The CoreML packages were FP32 until 2026-10-07: `half=False` under
|
| 209 |
+
Ultralytics 8.4.98 pins Core ML's compute precision to FP32. The Neural
|
| 210 |
+
Engine computes in FP16 only, so Core ML ran them on the GPU (`ALL`) or the
|
| 211 |
+
CPU (`CPU_AND_NE`). The FP16 export runs on the Neural Engine and replaced
|
| 212 |
+
the FP32 one in place (D743).
|
| 213 |
+
"""
|
| 214 |
return Recipe(
|
| 215 |
name=f'camstack-yolov9{tier}-{res}', folder='objectDetection/yolov9', imgsz=res,
|
| 216 |
toolchain=TC_2607_YOLOV9, checkpoint=f'yolov9{tier}',
|
| 217 |
onnx=OnnxExport(dynamic=False, opset=13),
|
| 218 |
+
coreml=CoremlExport(dynamic=False, batch=1, half=True),
|
| 219 |
openvino=(OpenvinoOutput('-fp16', 'fp16'), OpenvinoOutput('-int8', 'int8')),
|
| 220 |
calib_samples=300,
|
| 221 |
)
|
|
|
|
| 226 |
name=f'camstack-yolo26{tier}-seg', folder='segmentation/yolo26-seg', imgsz=640,
|
| 227 |
toolchain=TC_2604, checkpoint=f'yolo26{tier}-seg',
|
| 228 |
onnx=OnnxExport(dynamic=False, opset=17),
|
| 229 |
+
coreml=CoremlExport(dynamic=False, batch=1, half=True),
|
| 230 |
openvino=(FP16_IR,),
|
| 231 |
)
|
| 232 |
|
|
|
|
| 245 |
|
| 246 |
|
| 247 |
def check_recipes(recipes: tuple[Recipe, ...]) -> list[str]:
|
| 248 |
+
"""Recipes whose CoreML export is unusable on a Mac; empty when all is well.
|
| 249 |
+
|
| 250 |
+
- A batch-flexible YOLO26 package aborts the process that loads it (D742).
|
| 251 |
+
- An FP32 package never runs on the Neural Engine, which computes in FP16
|
| 252 |
+
only: Core ML puts it on the GPU (`ALL`) or the CPU (`CPU_AND_NE`), 2-4x
|
| 253 |
+
slower than the FP16 package of the same model on the Neural Engine
|
| 254 |
+
(D743). Ultralytics pins FP32 unless `half` is set, so every CoreML
|
| 255 |
+
recipe sets it.
|
| 256 |
+
"""
|
| 257 |
+
bad: list[str] = []
|
| 258 |
+
for r in recipes:
|
| 259 |
+
if r.coreml is None:
|
| 260 |
+
continue
|
| 261 |
+
if r.coreml.dynamic and r.checkpoint.startswith(GATHER_ND_CHECKPOINT_PREFIX):
|
| 262 |
+
bad.append(f'{r.name}: CoreML export must be static (dynamic=False, batch=1) — a '
|
| 263 |
+
f'batch-flexible {r.checkpoint} package aborts the process that loads it (D742)')
|
| 264 |
+
if not r.coreml.half:
|
| 265 |
+
bad.append(f'{r.name}: CoreML export must be FP16 (half=True) — an FP32 package never '
|
| 266 |
+
f'runs on the Neural Engine (D743)')
|
| 267 |
+
return bad
|
| 268 |
|
| 269 |
|
| 270 |
FAMILIES = {
|
|
|
|
| 501 |
name=name, folder=args.folder, imgsz=args.imgsz,
|
| 502 |
toolchain=Toolchain('any', 'any', 'any', 'any'), checkpoint=args.model,
|
| 503 |
onnx=OnnxExport(dynamic=args.dynamic, opset=args.opset),
|
| 504 |
+
coreml=CoremlExport(dynamic=args.dynamic, batch=16 if args.dynamic else 1, half=True),
|
| 505 |
openvino=tuple(OpenvinoOutput(f'-{p}', p) for p in precisions),
|
| 506 |
calib_samples=args.calib_samples or 300,
|
| 507 |
)
|
|
|
|
| 579 |
adhoc.add_argument('--folder', default='custom')
|
| 580 |
adhoc.add_argument('--dynamic', action='store_true', help='dynamic axes (ONNX) / flexible batch (CoreML)')
|
| 581 |
adhoc.add_argument('--opset', type=int, default=17)
|
|
|
|
| 582 |
adhoc.add_argument('--openvino', help='IR precisions, comma-separated: fp32,fp16,int8')
|
| 583 |
return p.parse_args(argv)
|
| 584 |
|