diff --git a/apps/ml-yolo/Dockerfile b/apps/ml-yolo/Dockerfile index 0eb23a6f..1f3b991f 100644 --- a/apps/ml-yolo/Dockerfile +++ b/apps/ml-yolo/Dockerfile @@ -51,7 +51,8 @@ RUN python3 -m venv /opt/tools-venv && \ /opt/tools-venv/bin/pip install --no-cache-dir --upgrade pip && \ /opt/tools-venv/bin/pip install --no-cache-dir --timeout=120 --retries=5 \ --index-url https://download.pytorch.org/whl/cpu \ - torch==2.8.0+cpu torchvision==0.23.0+cpu && \ + --extra-index-url https://pypi.org/simple \ + torch==2.8.0+cpu torchvision==0.23.0 && \ git clone https://github.com/luxonis/tools.git /tmp/luxonis-tools && \ cd /tmp/luxonis-tools && \ git checkout edbe7da1a7f75833a71d65caf1028036faa81061 && \ @@ -130,14 +131,15 @@ ENV IN_DOCKER=1 ENV ROBOPIPE_MODELCONVERTER_BIN=/opt/modelconverter-venv/bin/modelconverter ENV ROBOPIPE_SNPE_ROOT=/opt/snpe -# Pre-download all yolo11 detection + segmentation pretrained weights into the -# image. Ultralytics' YOLO(name) constructor calls safe_download() which has a -# known silent-failure path: if a retry deletes a partial file and the loop -# exits without raising, attempt_download_asset() returns a Path to a -# non-existent file and the next torch.load() raises FileNotFoundError. By -# baking the weights at /app (the WORKDIR), YOLO()'s first existence check -# (`Path(file).exists()` against cwd) hits and skips the download entirely. -# Pin to v8.3.0 — the release tag that hosts the yolo11 family weights. +# Pre-download pretrained weights into the image. Ultralytics' YOLO(name) +# constructor calls safe_download() which has a known silent-failure path: if +# a retry deletes a partial file and the loop exits without raising, +# attempt_download_asset() returns a Path to a non-existent file and the next +# torch.load() raises FileNotFoundError. By baking the weights at /app (the +# WORKDIR), YOLO()'s first existence check (`Path(file).exists()` against cwd) +# hits and skips the download entirely. +# YOLO11 weights at v8.3.0; YOLO26 weights at v8.4.0 (first release that +# ships the yolo26 family). RUN cd /app && for v in \ yolo11n yolo11s yolo11m yolo11l yolo11x \ yolo11n-seg yolo11s-seg yolo11m-seg yolo11l-seg yolo11x-seg \ @@ -145,6 +147,13 @@ RUN cd /app && for v in \ curl -fL --retry 3 -o ${v}.pt \ https://github.com/ultralytics/assets/releases/download/v8.3.0/${v}.pt; \ done +RUN cd /app && for v in \ + yolo26n yolo26s yolo26m yolo26l yolo26x \ + yolo26n-seg yolo26s-seg yolo26m-seg yolo26l-seg yolo26x-seg \ + ; do \ + curl -fL --retry 3 -o ${v}.pt \ + https://github.com/ultralytics/assets/releases/download/v8.4.0/${v}.pt; \ + done COPY app/ ./app/ diff --git a/apps/ml-yolo/app/ml/archive_patch.py b/apps/ml-yolo/app/ml/archive_patch.py index ee140af0..ddd9f9c8 100644 --- a/apps/ml-yolo/app/ml/archive_patch.py +++ b/apps/ml-yolo/app/ml/archive_patch.py @@ -24,6 +24,13 @@ from ..models.model_type import ModelType +def _yolo_subtype(model_variant: str) -> str: + stem = Path(model_variant).stem.lower() + if stem.startswith("yolo26"): + return "yolo26" + return "yolov8" + + # Pre-NMS thresholds the on-device DetectionParser uses. The dashboard # does its own confidence filtering on top # (sensor.dashboard_config.confidenceThreshold), so this is a coarse @@ -43,6 +50,7 @@ def patch_nn_archive_heads( archive_path: str | Path, model_type: ModelType, label_ids: list[int], + model_variant: str = "", ) -> None: """Mutate the NN archive at `archive_path` in-place to ensure it has a valid `heads` block. No-op when the file isn't an NN archive, when @@ -118,7 +126,7 @@ def patch_nn_archive_heads( "conf_threshold": _DEFAULT_CONF_THRESHOLD, "max_det": _DEFAULT_MAX_DET, "anchors": None, - "subtype": "yolov8", + "subtype": _yolo_subtype(model_variant), "yolo_outputs": output_names, }, "outputs": output_names, diff --git a/apps/ml-yolo/app/ml/train_model.py b/apps/ml-yolo/app/ml/train_model.py index 0e979a56..98e04870 100644 --- a/apps/ml-yolo/app/ml/train_model.py +++ b/apps/ml-yolo/app/ml/train_model.py @@ -396,6 +396,7 @@ def upload_for(t: ModelOutputType) -> OutputUpload: converted_path, config.type, config.training_config.dataset_config.label_ids, + model_variant=get_model_variant(config), ) conv_upload = upload_for(output_type) _upload_to_signed_url(conv_upload, converted_path) diff --git a/apps/ml-yolo/app/ml/ultralytics_config.py b/apps/ml-yolo/app/ml/ultralytics_config.py index 21fda86c..6610a386 100644 --- a/apps/ml-yolo/app/ml/ultralytics_config.py +++ b/apps/ml-yolo/app/ml/ultralytics_config.py @@ -10,6 +10,8 @@ dfl, hsv_h, hsv_s, hsv_v, degrees, translate, scale, shear, perspective, flipud, fliplr, mosaic, mixup, copy_paste, optimizer, cos_lr, patience, imgsz, workers, device, amp +Note: `dfl` is silently stripped for YOLO26 variants (DFL removed in YOLO26). +YOLO26 variants: yolo26[n|s|m|l|x][.pt] and yolo26[n|s|m|l|x]-seg[.pt] Plus two ml-yolo-specific keys stripped before passthrough: backend — consumed by the API dispatcher model_variant — pretrained weights filename (e.g. yolo11m.pt) @@ -35,6 +37,10 @@ } +def _is_yolo26(variant: str) -> bool: + return Path(variant).stem.lower().startswith("yolo26") + + def get_model_variant(config: ModelConfig) -> str: custom = config.training_config.custom_hyperparams or {} variant = custom.get("model_variant") @@ -102,4 +108,6 @@ def build_train_kwargs( # custom_hyperparams wins over defaults but not over the dispatch kwargs above. for key, value in custom.items(): kwargs[key] = value + if _is_yolo26(get_model_variant(config)): + kwargs.pop("dfl", None) return kwargs diff --git a/apps/ml-yolo/requirements.txt b/apps/ml-yolo/requirements.txt index 71664133..b63863d3 100644 --- a/apps/ml-yolo/requirements.txt +++ b/apps/ml-yolo/requirements.txt @@ -11,9 +11,8 @@ pydantic-settings==2.7.1 python-multipart==0.0.17 # ML training (no luxonis-train — this is the Ultralytics-only service). -# 8.3.160 decouples the confusion matrix update from args.plots (so the CM -# is populated even without plot files) and fixes the epoch-47 validation -# plot broadcast crash. Still numpy-1.x compatible. +# 8.4.44: first release series shipping yolo26*.pt weights (YOLO26 support). +# YOLO11 trains identically on 8.4.x — the 8.3→8.4 bump is additive only. # # torch is PINNED to 2.8.0 (not 2.9). In torch 2.9, `torch.onnx.export` # hard-imports `onnxscript` even on the legacy `dynamo=False` path, and @@ -23,7 +22,7 @@ python-multipart==0.0.17 # it triggers the crash. torch 2.8's legacy exporter is pure TorchScript # with zero onnxscript involvement, sidestepping the trap entirely. Bump # only when ultralytics or torch fix the upstream interaction. -ultralytics==8.3.160 +ultralytics==8.4.44 torch==2.8.0 torchvision==0.23.0 diff --git a/apps/web/src/modules/model/components/AdvancedSettings/presets.ts b/apps/web/src/modules/model/components/AdvancedSettings/presets.ts index c748a897..b36afc3d 100644 --- a/apps/web/src/modules/model/components/AdvancedSettings/presets.ts +++ b/apps/web/src/modules/model/components/AdvancedSettings/presets.ts @@ -110,6 +110,59 @@ const ULTRALYTICS_PRESETS: HyperparamsPreset[] = [ patience: 30, }, }, + { + id: "yolo26-fast", + name: "YOLO26 Fast", + description: "YOLO26 nano — NMS-free, faster CPU inference, quick iteration", + config: { + model_variant: "yolo26n", + imgsz: 640, + optimizer: "AdamW", + lr0: 0.001, + cos_lr: true, + patience: 20, + hsv_s: 0.15, + hsv_v: 0.2, + degrees: 15.0, + cls: 0.8, + }, + }, + { + id: "yolo26-accuracy", + name: "YOLO26 High Accuracy", + description: + "YOLO26 large at 1280px — NMS-free, edge-optimised, tuned for fine-grained classification", + config: { + model_variant: "yolo26l", + imgsz: 1280, + optimizer: "AdamW", + lr0: 0.001, + lrf: 0.01, + momentum: 0.937, + weight_decay: 0.0005, + cos_lr: true, + warmup_epochs: 5, + warmup_momentum: 0.8, + warmup_bias_lr: 0.1, + patience: 50, + box: 9.0, + cls: 0.8, + mosaic: 1.0, + close_mosaic: 25, + copy_paste: 0.2, + erasing: 0.4, + hsv_h: 0.01, + hsv_s: 0.15, + hsv_v: 0.2, + degrees: 15.0, + translate: 0.1, + scale: 0.5, + shear: 2.0, + perspective: 0.0005, + fliplr: 0.5, + amp: true, + }, + }, ]; export const getHyperparamsPresets = (