Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions apps/ml-yolo/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand Down Expand Up @@ -130,21 +131,29 @@ 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 \
; do \
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/

Expand Down
10 changes: 9 additions & 1 deletion apps/ml-yolo/app/ml/archive_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions apps/ml-yolo/app/ml/train_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions apps/ml-yolo/app/ml/ultralytics_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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
7 changes: 3 additions & 4 deletions apps/ml-yolo/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
53 changes: 53 additions & 0 deletions apps/web/src/modules/model/components/AdvancedSettings/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
Loading