apocaliss92 commited on
Commit
8d6abb9
·
verified ·
1 Parent(s): 63b51b7

source: every CoreML recipe exports FP16 (YOLOv9 packages replaced in place)

Browse files
Files changed (2) hide show
  1. source/README.md +12 -3
  2. 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 fp32-compute CoreML, IR fp16 twice + INT8.
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=False),
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=False),
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=False),
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 Core ML cannot load; empty when all is well."""
238
- return [
239
- f'{r.name}: CoreML export must be static (dynamic=False, batch=1) — a '
240
- f'batch-flexible {r.checkpoint} package aborts the process that loads it (D742)'
241
- for r in recipes
242
- if r.coreml is not None and r.coreml.dynamic
243
- and r.checkpoint.startswith(GATHER_ND_CHECKPOINT_PREFIX)
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=args.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