# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
"""
Export a YOLO PyTorch model to other formats. TensorFlow exports authored by https://github.com/zldrobit.

Format                  | `format=argument`         | Model
---                     | ---                       | ---
PyTorch                 | -                         | yolo26n.pt
TorchScript             | `torchscript`             | yolo26n.torchscript
ONNX                    | `onnx`                    | yolo26n.onnx
OpenVINO                | `openvino`                | yolo26n_openvino_model/
TensorRT                | `engine`                  | yolo26n.engine
CoreML                  | `coreml`                  | yolo26n.mlpackage
Apple Core AI           | `coreai`                  | yolo26n.aimodel
TensorFlow SavedModel   | `saved_model`             | yolo26n_saved_model/
TensorFlow GraphDef     | `pb`                      | yolo26n.pb
TensorFlow Edge TPU     | `edgetpu`                 | yolo26n_edgetpu.tflite
LiteRT                  | `litert`                  | yolo26n.tflite
PaddlePaddle            | `paddle`                  | yolo26n_paddle_model/
MNN                     | `mnn`                     | yolo26n.mnn
NCNN                    | `ncnn`                    | yolo26n_ncnn_model/
IMX                     | `imx`                     | yolo26n_imx_model/
RKNN                    | `rknn`                    | yolo26n_rknn_model/
ExecuTorch              | `executorch`              | yolo26n_executorch_model/
Axelera AI              | `axelera`                 | yolo26n_axelera_model/
DEEPX                   | `deepx`                   | yolo26n_deepx_model/
Qualcomm QNN            | `qnn`                     | yolo26n_qnn.onnx
Hailo                   | `hailo`                   | yolo26n_hailo_model/
Huawei Ascend           | `ascend`                  | yolo26n_ascend_model/
AMD Xilinx              | `xilinx`                  | yolo26n_xilinx_model/

Requirements:
    $ pip install "ultralytics[export]"

Python:
    from ultralytics import YOLO
    model = YOLO('yolo26n.pt')
    results = model.export(format='onnx')
    results = model.export(format='onnx', quantize=8, data='coco8.yaml')  # INT8 ONNX

CLI:
    $ yolo export model=yolo26n.pt format=onnx
    $ yolo export model=yolo26n.pt format=onnx quantize=8 data=coco8.yaml

Inference:
    $ yolo predict model=yolo26n.pt                 # PyTorch
                         yolo26n.torchscript        # TorchScript
                         yolo26n.onnx               # ONNX Runtime or OpenCV DNN with dnn=True
                         yolo26n_openvino_model     # OpenVINO
                         yolo26n.engine             # TensorRT
                         yolo26n.mlpackage          # CoreML (macOS-only)
                         yolo26n.aimodel            # Apple Core AI (export on macOS 26+ Apple silicon or x86_64 Linux)
                         yolo26n_saved_model        # TensorFlow SavedModel
                         yolo26n.pb                 # TensorFlow GraphDef
                         yolo26n_edgetpu.tflite     # TensorFlow Edge TPU
                         yolo26n.tflite             # LiteRT
                         yolo26n_paddle_model       # PaddlePaddle
                         yolo26n.mnn                # MNN
                         yolo26n_ncnn_model         # NCNN
                         yolo26n_imx_model          # IMX
                         yolo26n_rknn_model         # RKNN
                         yolo26n_executorch_model   # ExecuTorch
                         yolo26n_axelera_model      # Axelera AI
                         yolo26n_deepx_model        # DEEPX
                         yolo26n_qnn.onnx           # Qualcomm QNN
                         yolo26n_hailo_model        # Hailo
                         yolo26n_ascend_model       # Huawei Ascend
                         yolo26n_xilinx_model       # AMD Xilinx
"""

from __future__ import annotations

import json
import os
import shutil
import time
from copy import deepcopy
from datetime import datetime
from functools import partial
from pathlib import Path

import numpy as np
import torch

from ultralytics import __version__
from ultralytics.cfg import QUANTIZE_DOCS_URL, TASK2CALIBRATIONDATA, TASK2DATA, get_cfg
from ultralytics.data import build_dataloader, build_yolo_dataset
from ultralytics.data.dataset import ClassificationDataset
from ultralytics.data.utils import check_cls_dataset, check_det_dataset, get_split_fraction
from ultralytics.nn.autobackend import AutoBackend, check_class_names, default_class_names
from ultralytics.nn.modules import (
    OBB,
    OBB26,
    Attention,
    C2f,
    Classify,
    Depth,
    Detect,
    Pose,
    Pose26,
    RTDETRDecoder,
    Segment,
    Segment26,
    SemanticSegment,
)
from ultralytics.nn.tasks import ClassificationModel, DepthModel, DetectionModel, SegmentationModel, WorldModel
from ultralytics.utils import (
    ARM64,
    DEFAULT_CFG,
    IS_DOCKER,
    LINUX,
    LOGGER,
    MACOS,
    MACOS_VERSION,
    QNN_HTP_TARGETS,
    RKNN_CHIPS,
    SETTINGS,
    TORCH_VERSION,
    WINDOWS,
    XILINX_TARGETS,
    YAML,
    callbacks,
    colorstr,
    get_default_args,
    is_jetson,
)
from ultralytics.utils.checks import (
    IS_PYTHON_MINIMUM_3_9,
    IS_PYTHON_MINIMUM_3_13,
    check_data_portable,
    check_imgsz,
    check_python,
    check_requirements,
    check_version,
    is_intel,
    rocm_is_available,
)
from ultralytics.utils.export.axelera import AXELERA_SDK
from ultralytics.utils.files import file_size
from ultralytics.utils.metrics import batch_probiou
from ultralytics.utils.nms import TorchNMS
from ultralytics.utils.ops import Profile
from ultralytics.utils.patches import arange_patch
from ultralytics.utils.torch_utils import (
    TORCH_1_11,
    TORCH_1_13,
    TORCH_2_1,
    TORCH_2_3,
    TORCH_2_8,
    TORCH_2_9,
    is_qat,
    select_device,
)


def export_formats():
    """Return a dictionary of Ultralytics YOLO export formats."""
    #          Format, Argument, Suffix, CPU, GPU, Arguments, Env
    x = [
        ["PyTorch", "-", ".pt", True, True, [], "base"],
        [
            "TorchScript",
            "torchscript",
            ".torchscript",
            True,
            True,
            ["batch", "quantize", "nms", "dynamic"],
            "base",
        ],
        [
            "ONNX",
            "onnx",
            ".onnx",
            True,
            True,
            ["batch", "data", "dynamic", "quantize", "opset", "simplify", "nms", "fraction"],
            "base",
        ],
        [
            "OpenVINO",
            "openvino",
            "_openvino_model",
            True,
            False,
            ["batch", "data", "dynamic", "quantize", "nms", "fraction"],
            "base",
        ],
        [
            "TensorRT",
            "engine",
            ".engine",
            False,
            True,
            ["batch", "data", "dynamic", "quantize", "opset", "simplify", "workspace", "nms", "fraction"],
            "base",
        ],
        ["CoreML", "coreml", ".mlpackage", True, False, ["batch", "dynamic", "quantize", "nms"], "coreml"],
        [
            "Core AI",
            "coreai",
            ".aimodel",
            True,
            False,
            ["batch", "quantize"],
            "base",
        ],
        [
            "TensorFlow SavedModel",
            "saved_model",
            "_saved_model",
            True,
            True,
            ["batch", "data", "fraction", "quantize", "opset", "nms"],
            "tensorflow",
        ],
        ["TensorFlow GraphDef", "pb", ".pb", True, True, ["batch", "opset"], "tensorflow"],
        [
            "TensorFlow Edge TPU",
            "edgetpu",
            "_edgetpu.tflite",
            True,
            False,
            ["data", "fraction", "quantize", "opset"],
            "tensorflow",
        ],
        ["LiteRT", "litert", ".tflite", True, False, ["batch", "quantize", "data", "fraction"], "litert"],
        ["PaddlePaddle", "paddle", "_paddle_model", True, True, ["batch"], "base"],
        ["MNN", "mnn", ".mnn", True, True, ["batch", "dynamic", "quantize", "opset", "simplify", "nms"], "mnn"],
        ["NCNN", "ncnn", "_ncnn_model", True, True, ["batch", "quantize"], "ncnn"],
        ["IMX", "imx", "_imx_model", True, True, ["data", "quantize", "fraction", "nms"], "isolated-imx"],
        [
            "RKNN",
            "rknn",
            "_rknn_model",
            False,
            False,
            ["batch", "name", "quantize", "opset", "simplify", "data", "fraction"],
            "isolated-rknn",
        ],
        ["ExecuTorch", "executorch", "_executorch_model", True, False, ["batch"], "executorch"],
        [
            "Axelera AI",
            "axelera",
            "_axelera_model",
            False,
            False,
            ["batch", "quantize", "fraction", "data"],
            "isolated-axelera",
        ],
        [
            "DEEPX",
            "deepx",
            "_deepx_model",
            False,
            False,
            ["data", "quantize", "opset", "simplify", "optimize"],
            "isolated-deepx",
        ],
        [
            "Qualcomm QNN",
            "qnn",
            "_qnn.onnx",
            False,
            False,
            ["batch", "name", "quantize", "opset", "simplify", "fraction", "data"],
            "base",
        ],
        [
            "Hailo",
            "hailo",
            "_hailo_model",
            False,
            False,
            ["name", "quantize", "data", "fraction", "simplify", "conf", "iou"],
            "base",
        ],
        [
            "Huawei Ascend",
            "ascend",
            "_ascend_model",
            False,
            False,
            ["batch", "name", "quantize", "opset", "simplify", "nms"],
            "base",
        ],
        [
            "AMD Xilinx",
            "xilinx",
            "_xilinx_model",
            True,
            False,
            ["name", "quantize", "data", "fraction", "opset", "simplify"],
            "isolated-xilinx",
        ],
    ]
    return dict(zip(["Format", "Argument", "Suffix", "CPU", "GPU", "Arguments", "Env"], zip(*x)))


EXPORT_ENVS = {
    "base": {
        "python": None,
        "extras": ["export-base", "export-openvino"],
        "torch": None,
        "requirements": [],
        "indexes": [],
        "env": {},
        "smoke": [],
    },
    "tensorflow": {
        "python": "3.12",
        "extras": ["export-base", "export-tensorflow"],
        "torch": None,
        "requirements": [
            "onnx2tf>=1.26.3,<1.29.0",
            "tf_keras<=2.19.0",
            "sng4onnx>=1.0.1",
            "onnx_graphsurgeon>=0.3.26",
            "ai-edge-litert>=1.2.0",
            "onnxruntime",
            "protobuf>=5",
        ],
        "indexes": [],
        "env": {},
        "smoke": ["yolo export format=saved_model model=yolo26n.pt imgsz=32"],
    },
    "coreml": {
        "python": "3.13",
        "extras": ["export-base", "export-coreml"],
        "torch": ">=2.12",
        "requirements": [],
        "indexes": [],
        "env": {},
        "smoke": ["yolo export format=coreml model=yolo26n.pt imgsz=32"],
    },
    "mnn": {
        "python": "3.14",
        "extras": ["export-base"],
        "torch": None,
        "requirements": ["MNN>=2.9.6", "aliyun-log-python-sdk", "protobuf<6.0.0,>=3.20.3"],
        "indexes": [],
        "env": {},
        "smoke": ["yolo export format=mnn model=yolo26n.pt imgsz=32"],
    },
    "ncnn": {
        "python": "3.14",
        "extras": ["export-base"],
        "torch": None,
        "requirements": ["ncnn", "pnnx==20260526"],
        "indexes": [],
        "env": {},
        "smoke": ["yolo export format=ncnn model=yolo26n.pt imgsz=32"],
    },
    "executorch": {
        "python": "3.14",
        "extras": ["export-base", "export-executorch"],
        "torch": ">=2.12",
        "requirements": [],
        "indexes": [],
        "env": {},
        "smoke": ["yolo export format=executorch model=yolo26n.pt imgsz=32"],
    },
    "isolated-imx": {
        "python": "3.11",
        "extras": ["export-base"],
        "torch": ">=2.9,<2.12",
        "requirements": [
            "model-compression-toolkit>=2.4.1",
            "edge-mdt-cl<1.1.0",
            "edge-mdt-tpc>=1.2.0",
            "pydantic<2.12",
            "imx500-converter[pt]>=3.17.3",
        ],
        "indexes": [],
        "env": {},
        "smoke": ["yolo export format=imx model=yolo11n.pt imgsz=32 data=coco8.yaml"],
    },
    "isolated-rknn": {
        "python": "3.11",
        "extras": ["export-base"],
        "torch": "==2.4",
        "requirements": ["rknn-toolkit2>=2.3.2", "onnx>=1.16.1,<1.19.0", "setuptools<82"],
        "indexes": [],
        "env": {},
        "smoke": ["yolo export format=rknn model=yolo26n.pt imgsz=32 quantize=16"],
    },
    "isolated-axelera": {
        "python": "3.13",
        "extras": ["export-base"],
        # Axelera export requires 2.8.0 <= torch < 2.13.0.
        "torch": ">=2.8,<2.13",
        "requirements": [
            f"axelera-devkit=={AXELERA_SDK}",
            f"axelera-rt=={AXELERA_SDK}",
            "omnimalloc==0.5.0",
            "onnx>=1.12.0,<2.0.0",
            "onnxslim>=0.1.71",
        ],
        "indexes": [
            ("--extra-index-url", "https://software.axelera.ai/artifactory/api/pypi/axelera-pypi/simple"),
        ],
        # Use the Python protobuf runtime for Axelera compiler compatibility.
        "env": {"PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": "python"},
        "smoke": ["yolo export format=axelera model=yolo26n.pt imgsz=64 data=coco8.yaml"],
    },
    "isolated-deepx": {
        "python": "3.14",
        "extras": ["export-base", "export-deepx"],
        "torch": ">=2.8,<2.12",
        "requirements": [],
        "indexes": [],
        # DeepX export is only supported on non-aarch64 Linux.
        "env": {},
        "smoke": ["yolo export format=deepx model=yolo26n.pt imgsz=32 data=coco8.yaml"],
    },
    "isolated-xilinx": {
        "python": "3.12",
        "extras": ["export-base", "export-xilinx"],
        "torch": None,
        "requirements": [],
        "indexes": [],
        "env": {},
        "smoke": ["yolo export format=xilinx model=yolo26n.pt imgsz=64 data=coco8.yaml"],
    },
    "litert": {
        "python": "3.14",
        "extras": ["export-base", "export-litert"],
        "torch": None,
        "requirements": [],
        "indexes": [],
        "env": {},
        "smoke": ["yolo export format=litert model=yolo26n.pt imgsz=32"],
    },
}


# Export precision support per format. Unset/32 requests are FP32 except for formats listed in FP32_UNSUPPORTED_FORMATS.
FP16_FORMATS = frozenset(
    {"torchscript", "onnx", "openvino", "engine", "coreml", "coreai", "mnn", "ncnn", "rknn", "ascend"}
)
INT8_FORMATS = frozenset(
    {
        "onnx",
        "openvino",
        "engine",
        "coreml",
        "saved_model",
        "edgetpu",
        "litert",
        "mnn",
        "imx",
        "rknn",
        "axelera",
        "deepx",
        "hailo",
        "xilinx",
    }
)
W8A16_FORMATS = frozenset({"coreml", "litert", "qnn"})  # INT8 weights + 16-bit activations (FP16; INT16 on LiteRT)
W8A32_FORMATS = frozenset({"litert"})  # INT8 weights + FP32 activations (dynamic/weight-only INT8, no calibration)
FP32_UNSUPPORTED_FORMATS = frozenset({"edgetpu", "imx", "rknn", "axelera", "deepx", "qnn", "hailo", "ascend", "xilinx"})
# (label, supporting formats) per quantize precision, used to list valid options in errors. 32/None (FP32) is universal except FP32_UNSUPPORTED_FORMATS.
QUANTIZE_PRECISIONS = (
    ("16 (FP16)", FP16_FORMATS),
    ("8 (INT8)", INT8_FORMATS),
    ("'w8a16' (INT8 weights + INT16 activations)", W8A16_FORMATS),
    ("'w8a32' (dynamic INT8)", W8A32_FORMATS),
)


def validate_args(format, passed_args, valid_args):
    """Validate arguments based on the export format.

    Args:
        format (str): The export format.
        passed_args (SimpleNamespace): The arguments used during export.
        valid_args (list | None): List of valid arguments for the format.

    Raises:
        AssertionError: If an unsupported argument is used, or if the format lacks supported argument listings.
    """
    # Format-specific args from the export table; skip conf/iou/name, nms (handled by Exporter) and quantize (below)
    export_args = sorted(set().union(*export_formats()["Arguments"]) - {"conf", "iou", "name", "quantize", "nms"})

    assert valid_args is not None, f"ERROR ❌️ valid arguments for '{format}' not listed."
    custom = {"batch": 1, "data": None, "device": None}  # exporter defaults
    default_args = get_cfg(DEFAULT_CFG, custom)
    if passed_args.quantize is not None:  # 32/None (FP32) is universal except FP32_UNSUPPORTED_FORMATS
        options = [label for label, formats in QUANTIZE_PRECISIONS if format in formats]
        if format not in FP32_UNSUPPORTED_FORMATS:
            options.append("32 or None (FP32)")
        hint = f"format='{format}' supports quantize={', '.join(options)}. See {QUANTIZE_DOCS_URL}"
        if passed_args.quantize == 16:  # FP16
            assert format in FP16_FORMATS, f"ERROR ❌️ quantize=16 (FP16) is not supported; {hint}"
        elif passed_args.quantize == 8:  # INT8
            assert format in INT8_FORMATS, f"ERROR ❌️ quantize=8 (INT8) is not supported; {hint}"
        elif passed_args.quantize == "w8a16":  # INT8 weights + 16-bit activations (FP16; INT16 on LiteRT)
            assert format in W8A16_FORMATS, f"ERROR ❌️ quantize='w8a16' is not supported; {hint}"
        elif passed_args.quantize == "w8a32":  # INT8 weights + FP32 activations (dynamic/weight-only INT8)
            assert format in W8A32_FORMATS, f"ERROR ❌️ quantize='w8a32' is not supported; {hint}"
        elif passed_args.quantize == 32:  # FP32
            assert format not in FP32_UNSUPPORTED_FORMATS, f"ERROR ❌️ quantize=32 (FP32) is not supported; {hint}"
    for arg in export_args:
        not_default = getattr(passed_args, arg, getattr(default_args, arg, None)) != getattr(default_args, arg, None)
        if not_default:
            assert arg in valid_args, f"ERROR ❌️ argument '{arg}' is not supported for format='{format}'"


def try_export(inner_func):
    """YOLO export decorator, i.e. @try_export."""
    inner_args = get_default_args(inner_func)

    def outer_func(*args, **kwargs):
        """Export a model."""
        prefix = inner_args["prefix"]
        dt = 0.0
        try:
            with Profile() as dt:
                f = inner_func(*args, **kwargs)  # exported file/dir or tuple of (file/dir, *)
            path = f if isinstance(f, (str, Path)) else f[0]
            mb = file_size(path)
            assert mb > 0.1, f"{mb:.3f} MB output model too small (likely corrupt or unsupported ops)"
            LOGGER.info(f"{prefix} export success ✅ {dt.t:.1f}s, saved as '{path}' ({mb:.1f} MB)")
            return f
        except Exception as e:
            LOGGER.error(f"{prefix} export failure {dt.t:.1f}s: {e}")
            raise

    return outer_func


class Exporter:
    """A class for exporting YOLO models to various formats.

    This class provides functionality to export YOLO models to different formats including ONNX, TensorRT, CoreML,
    TensorFlow, and others. It handles format validation, device selection, model preparation, and the actual export
    process for each supported format.

    Attributes:
        args (SimpleNamespace): Configuration arguments for the exporter.
        callbacks (dict): Dictionary of callback functions for different export events.
        im (torch.Tensor): Input tensor for model inference during export.
        model (torch.nn.Module): The YOLO model to be exported.
        file (Path): Path to the model file being exported.
        output_shape (tuple): Shape of the model output tensor(s).
        pretty_name (str): Formatted model name for display purposes.
        metadata (dict): Model metadata including description, author, version, etc.
        device (torch.device): Device on which the model is loaded.
        dla (str | None): TensorRT DLA core ('0' or '1') when exporting with device='dla:N', otherwise None.
        qat (bool): Whether the model is a quantization-aware trained (QAT) model.
        imgsz (list): Input image size for the model.

    Methods:
        __call__: Main export method that handles the export process.
        get_int8_calibration_dataloader: Build dataloader for INT8 calibration.
        export_torchscript: Export model to TorchScript format.
        export_onnx: Export model to ONNX format.
        export_openvino: Export model to OpenVINO format.
        export_paddle: Export model to PaddlePaddle format.
        export_litert: Export model to LiteRT format.
        export_mnn: Export model to MNN format.
        export_ncnn: Export model to NCNN format.
        export_coreml: Export model to CoreML format.
        export_engine: Export model to TensorRT format.
        export_saved_model: Export model to TensorFlow SavedModel format.
        export_pb: Export model to TensorFlow GraphDef format.
        export_edgetpu: Export model to Edge TPU format.
        export_rknn: Export model to RKNN format.
        export_imx: Export model to IMX format.
        export_executorch: Export model to ExecuTorch format.
        export_coreai: Export model to Apple Core AI format.
        export_axelera: Export model to Axelera format.
        export_deepx: Export model to DEEPX format.
        export_qnn: Export model to Qualcomm QNN format.
        export_hailo: Export model to Hailo HEF format.
        export_ascend: Export model to Huawei Ascend format.
        export_xilinx: Export model to AMD Xilinx Vitis AI format.

    Examples:
        Export a YOLO26 model to TorchScript format
        >>> from ultralytics import YOLO
        >>> from ultralytics.engine.exporter import Exporter
        >>> exporter = Exporter()
        >>> exporter(model=YOLO("yolo26n.pt").model)  # exports to yolo26n.torchscript

        Export with specific arguments
        >>> args = {"format": "onnx", "dynamic": True, "quantize": 8, "data": "coco8.yaml"}
        >>> exporter = Exporter(overrides=args)
        >>> exporter(model=YOLO("yolo26n.pt").model)
    """

    def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks: dict | None = None):
        """Initialize the Exporter class.

        Args:
            cfg (str | Path | dict | SimpleNamespace, optional): Configuration file path or configuration object.
            overrides (dict, optional): Configuration overrides.
            _callbacks (dict, optional): Dictionary of callback functions.
        """
        self.args = get_cfg(cfg, overrides)
        self.callbacks = _callbacks or callbacks.get_default_callbacks()
        callbacks.add_integration_callbacks(self)

    def __call__(self, model: torch.nn.Module) -> str:
        """Export a model and return the final exported path as a string.

        Args:
            model (torch.nn.Module): The YOLO PyTorch model to export.

        Returns:
            (str): Path to the exported file or directory (the last export artifact).

        Raises:
            ValueError: If the export format is invalid or the model/arguments are unsupported for the format.
        """
        t = time.time()
        fmt = self.args.format = self.args.format.lower()  # to lowercase
        if fmt in {"tensorrt", "trt"}:  # 'engine' aliases
            fmt = self.args.format = "engine"
        if fmt in {"mlmodel", "mlpackage", "mlprogram", "apple", "ios", "coreml"}:  # 'coreml' aliases
            fmt = "coreml"
        if fmt in {"huawei", "cann", "om"}:  # 'ascend' aliases
            fmt = self.args.format = "ascend"
        if fmt in {"vitis", "vitisai", "versal"}:  # 'xilinx' aliases
            fmt = self.args.format = "xilinx"
        if fmt in {"tflite", "tfjs"}:  # deprecated formats, replaced by the unified Google LiteRT export
            LOGGER.warning(
                f"format='{fmt}' is deprecated as of 8.4.83 and has been replaced by the unified Google LiteRT "
                f"format. Exporting format='litert' instead. See https://docs.ultralytics.com/integrations/litert"
            )
            fmt = self.args.format = "litert"
        fmts_dict = export_formats()
        fmts = tuple(fmts_dict["Argument"][1:])  # available export formats
        if fmt not in fmts:
            import difflib

            # Get the closest match if format is invalid
            matches = difflib.get_close_matches(fmt, fmts, n=1, cutoff=0.6)  # 60% similarity required to match
            if not matches:
                msg = "Model is already in PyTorch format." if fmt == "pt" else f"Invalid export format='{fmt}'."
                raise ValueError(f"{msg} Valid formats are {fmts}")
            LOGGER.warning(f"Invalid export format='{fmt}', updating to format='{matches[0]}'")
            fmt = self.args.format = matches[0]
        is_tf_format = fmt in {"saved_model", "pb", "edgetpu"}

        # Device
        self.dla = None
        if fmt == "engine" and self.args.device is None:
            LOGGER.warning("TensorRT requires GPU export, automatically assigning device=0")
            self.args.device = "0"
        if fmt == "engine" and "dla" in str(self.args.device):  # convert int/list to str first
            device_str = str(self.args.device)
            self.dla = device_str.rsplit(":", 1)[-1]
            self.args.device = "0"  # update device to "0"
            assert self.dla in {"0", "1"}, f"Expected device 'dla:0' or 'dla:1', but got {device_str}."
        if fmt == "imx" and self.args.device is None and torch.cuda.is_available():
            LOGGER.warning("Exporting on CPU while CUDA is available, setting device=0 for faster export on GPU.")
            self.args.device = "0"  # update device to "0"
        self.device = select_device("cpu" if self.args.device is None else self.args.device)

        # Argument compatibility checks
        fmt_keys = dict(zip(fmts_dict["Argument"], fmts_dict["Arguments"]))[fmt]
        validate_args(fmt, self.args, fmt_keys)
        if fmt in {"deepx", "axelera", "imx", "edgetpu", "qnn", "hailo", "xilinx"} and self.args.quantize not in {
            8,
            "w8a16",
        }:
            LOGGER.warning(f"{fmt} export requires INT8 quantization, enabling it.")
            self.args.quantize = "w8a16" if fmt == "qnn" else 8
        if fmt in {"axelera", "hailo", "xilinx"} and not self.args.data:
            self.args.data = TASK2CALIBRATIONDATA.get(model.task)
        if fmt == "hailo":
            assert LINUX and not ARM64, "Hailo export is only supported on Linux x86_64."
            blocks = {str(x[2]) for x in model.yaml.get("backbone", []) + model.yaml.get("head", [])}
            family = Path(getattr(model, "yaml_file", None) or model.yaml.get("yaml_file", "")).stem.lower() or (
                "yolov8" if "C2f" in blocks else "yolo11" if {"C3k2", "C2PSA"} <= blocks else ""
            )
            task26 = {Segment26: "segmentation", Pose26: "pose", OBB26: "OBB"}.get(type(model.model[-1]))
            if task26:
                raise ValueError(f"Hailo export does not currently support YOLO26 {task26} models.")
            if (
                model.task not in {"detect", "segment", "pose", "obb", "classify", "semantic", "depth"}
                or type(model.model[-1]) not in {Detect, Segment, Pose, OBB, Classify, SemanticSegment, Depth}
                or not family.startswith(("yolov8", "yolo11", "yolo26"))
            ):
                raise ValueError(
                    "Hailo export currently supports YOLOv8/YOLO11/YOLO26 detection and classification models, "
                    "YOLOv8/YOLO11 segmentation, pose, and OBB models, and YOLO26 semantic segmentation and depth "
                    "models."
                )
            if model.task in {"semantic", "depth"} and not family.startswith("yolo26"):
                raise ValueError(f"Hailo export supports {model.task} models only for YOLO26.")
            self.args.name = str(self.args.name or "hailo8l").lower()
            hailo_archs = ("hailo8", "hailo8l", "hailo10h", "hailo15h", "hailo15l")
            if self.args.name not in hailo_archs:
                raise ValueError(f"Invalid Hailo architecture '{self.args.name}'. Valid names are {hailo_archs}.")
        if fmt == "axelera" and model.task == "segment" and any(isinstance(m, Segment26) for m in model.modules()):
            raise ValueError("Axelera export does not currently support YOLO26 segmentation models.")
        if fmt == "imx":
            if model.task == "depth":
                raise ValueError("IMX export is not supported for depth models.")
            if not self.args.nms and model.task in {"detect", "pose", "segment"}:
                LOGGER.warning("IMX export requires nms=True, setting nms=True.")
                self.args.nms = True
            if model.task not in {"detect", "pose", "classify", "segment"}:
                raise ValueError(
                    "IMX export only supported for detection, pose estimation, classification, and segmentation models."
                )
        memo = {}
        if isinstance(model, WorldModel):
            LOGGER.warning(
                "YOLOWorld (original version) export is not supported to any format. "
                "YOLOWorldv2 models (i.e. 'yolov8s-worldv2.pt') only support export to "
                "(torchscript, onnx, openvino, engine, coreml) formats. "
                "See https://docs.ultralytics.com/models/yolo-world for details."
            )
            # Keep a cached CLIP encoder out of the export copy: https://github.com/ultralytics/ultralytics/pull/18445
            memo[id(getattr(model, "clip_model", None))] = None
        model = deepcopy(model, memo).to(self.device)  # copy before the head and names writes below
        if not getattr(model, "names", None):  # missing, None or empty on legacy and foreign checkpoints
            head = model.model[-1]  # name the head's own classes so the metadata matches the output layer
            nc = head.linear.out_features if isinstance(head, Classify) else getattr(head, "nc", 999)
            model.names = default_class_names(nc=nc)
        model.names = check_class_names(model.names)
        if hasattr(model, "end2end"):
            model.end2end = self.args.nms is False
        if getattr(model, "end2end", False):
            if fmt in {"rknn", "ncnn", "executorch", "paddle", "imx", "edgetpu", "qnn"}:
                # Disable the end2end branch for formats without top-k support
                model.end2end = False
                LOGGER.warning("This export format does not support end2end models, disabling the end2end branch.")
            if fmt == "litert" and self.args.quantize in {8, "w8a16"}:
                # Static activation quantization collapses the end2end class-index output; export raw and run NMS later
                model.end2end = False
                LOGGER.warning("LiteRT INT8 export does not support end2end models, disabling end2end branch.")
            if fmt == "engine":
                try:
                    import tensorrt as trt

                    if check_version(trt.__version__, "<8.5.0"):
                        # https://github.com/ultralytics/ultralytics/issues/24607
                        model.end2end = False
                        LOGGER.warning(
                            "TensorRT versions earlier than 8.5.0 do not support the Mod operator in end-to-end models, disabling the end2end branch. "
                            "Please upgrade TensorRT to 8.5.0 or later to enable end2end export."
                        )

                    if (
                        self.args.quantize == 8
                        and check_version(trt.__version__, ">=10.3.0,<10.4.0")  # JetPack 6 builds report 10.3.0.x
                        and is_jetson(jetpack=6)
                    ):
                        # https://github.com/ultralytics/ultralytics/issues/23841
                        model.end2end = False
                        LOGGER.warning(
                            "TensorRT 10.3.0 on JetPack 6 with INT8 cannot build NMS-free exports; selecting one-to-many outputs. "
                            "For a fix, see https://docs.ultralytics.com/guides/nvidia-jetson#why-does-my-tensorrt-int8-export-disable-nms-free-inference-on-jetpack-6"
                        )
                except ImportError:
                    pass
        if self.args.quantize == 16 and fmt == "torchscript" and self.device.type == "cpu":
            raise ValueError("FP16 TorchScript export is only supported on GPU, i.e. use device=0.")
        self.imgsz = check_imgsz(self.args.imgsz, stride=model.stride, min_dim=2)  # check image size
        if fmt == "axelera" and min(self.imgsz) < 64:
            raise ValueError(f"Axelera export requires imgsz>=64, but got imgsz={self.imgsz}.")
        if fmt == "rknn":
            if not self.args.name:
                LOGGER.warning(
                    "Rockchip RKNN export requires a missing 'name' arg for processor type. "
                    "Using default name='rk3588'."
                )
                self.args.name = "rk3588"
            self.args.name = self.args.name.lower()
            assert self.args.name in RKNN_CHIPS, (
                f"Invalid processor name '{self.args.name}' for Rockchip RKNN export. Valid names are {RKNN_CHIPS}."
            )
            if self.args.name in {"rv1103", "rv1106", "rv1103b", "rv1106b"} and self.args.quantize != 8:
                if self.args.quantize not in {None, 8}:
                    raise ValueError(
                        f"Rockchip target '{self.args.name}' only supports INT8, but got quantize={self.args.quantize}. "
                        f"See {QUANTIZE_DOCS_URL}"
                    )
                LOGGER.warning(f"Rockchip target '{self.args.name}' requires INT8 quantization, enabling it.")
                self.args.quantize = 8
            elif self.args.quantize is None:
                self.args.quantize = 16
            if self.args.quantize == 8 and model.task != "detect":
                raise ValueError("Rockchip RKNN INT8 export is only supported for detection models.")
        if fmt == "ascend":
            # No SoC allowlist: valid --soc_version values depend on which Ascend-cann-kernels-* packages are
            # installed, so a hardcoded list would reject valid targets. ATC reports an unknown SoC itself.
            if not self.args.name:
                LOGGER.warning(
                    "Huawei Ascend export requires a missing 'name' arg for the target SoC. "
                    "Using default name='Ascend310B4'."
                )
                self.args.name = "Ascend310B4"
            if not str(self.args.name).startswith("Ascend"):
                raise ValueError(
                    f"Invalid Ascend SoC name='{self.args.name}'. Expected a CANN --soc_version such as "
                    f"'Ascend310P3' or 'Ascend310B4'. See https://docs.ultralytics.com/integrations/ascend"
                )
            if self.args.quantize is None:
                self.args.quantize = 16  # Ascend AI Core convolutions accept only FP16/INT8 inputs, never FP32
        if fmt == "qnn":
            if not self.args.name:
                LOGGER.warning(
                    "Qualcomm QNN export requires a missing 'name' arg for the target Hexagon HTP architecture. "
                    "Using default name='73' (Snapdragon 8 Gen 2)."
                )
                self.args.name = "73"
            self.args.name = str(self.args.name).lower().lstrip("v")  # accept '73', 'v73', or a supported SoC
            assert self.args.name in QNN_HTP_TARGETS, (
                f"Invalid Qualcomm QNN target '{self.args.name}'. Valid targets are {tuple(QNN_HTP_TARGETS)}."
            )
        if fmt == "xilinx":
            self.args.name = str(self.args.name or XILINX_TARGETS[0]).lower()
            assert self.args.name in XILINX_TARGETS, (
                f"Invalid AMD Xilinx device '{self.args.name}'. Valid devices are {XILINX_TARGETS}."
            )
        if self.args.nms and "nms" not in fmt_keys:
            LOGGER.warning(f"format={fmt} does not support embedded NMS; exporting native outputs for external NMS.")
            self.args.nms = None
        if self.args.nms and model.task in {"semantic", "depth"}:
            LOGGER.warning(f"'nms=True' is not valid for {model.task} models. Forcing 'nms=None'.")
            self.args.nms = None
        if fmt == "coreml" and self.args.nms and model.task not in {"detect", "segment", "pose"}:
            LOGGER.warning(
                "CoreML 'nms=True' is only supported for detect, segment and pose models. Forcing 'nms=None'."
            )
            self.args.nms = None
        if self.args.nms:
            assert not isinstance(model, ClassificationModel), "'nms=True' is not valid for classification models."
            assert not is_tf_format or TORCH_1_13, "TensorFlow exports with NMS require torch>=1.13"
            assert fmt != "onnx" or TORCH_1_13, "ONNX export with NMS requires torch>=1.13"
            # A previously fused checkpoint may only have its one-to-one branch remaining.
            if getattr(model, "end2end", False) or isinstance(model.model[-1], RTDETRDecoder):
                LOGGER.warning("'nms=True' is not available for end2end models. Forcing 'nms=None'.")
                self.args.nms = None
            self.args.conf = 0.25 if self.args.conf is None else self.args.conf  # set conf default for nms export
        if fmt == "mnn" and self.args.nms:
            if self.args.dynamic:
                raise ValueError("Alibaba MNN export does not support combining 'dynamic=True' with 'nms=True'.")
            if model.task not in {"detect", "pose"}:
                raise ValueError("Alibaba MNN export with 'nms=True' only supports detect and pose models.")
        if fmt == "coreml":
            if self.args.nms and model.task != "detect" and self.args.quantize == 32:
                # CoreML evaluates NMSModel's data-dependent shapes only on its FP16 path, silently dropping
                # detections from an FP32 ML Program. Detect is unaffected, its NMS is an Apple pipeline stage.
                LOGGER.warning(f"CoreML 'nms=True' requires FP16 for {model.task} models. Forcing 'quantize=16'.")
                self.args.quantize = 16
            if self.args.batch > 1:
                assert self.args.dynamic, (
                    "batch sizes > 1 are not supported without 'dynamic=True' for CoreML export. Please retry at 'dynamic=True'."
                )
            if self.args.dynamic:
                assert not self.args.nms, (
                    "'nms=True' cannot be used together with 'dynamic=True' for CoreML export. Please disable one of them."
                )
                assert model.task != "classify" and not isinstance(model.model[-1], RTDETRDecoder), (
                    "'dynamic=True' is not supported for CoreML classification or RT-DETR models."
                )
        if (fmt in {"engine", "coreml"} or self.args.nms) and self.args.dynamic and self.args.batch == 1:
            LOGGER.warning("'dynamic=True' export requires a maximum batch size, e.g. 'batch=16'.")
        if fmt == "edgetpu" and (not LINUX or ARM64):
            raise SystemError(
                "Edge TPU export only supported on non-aarch64 Linux. See https://coral.ai/docs/edgetpu/compiler"
            )
        self.qat = is_qat(model)  # quantization-aware trained model: ranges are baked in, calibration is a no-op
        if self.qat:
            assert fmt in {"onnx", "engine"}, (
                f"format='{fmt}' cannot export a QAT model: its Q/DQ ranges are only read by the 'onnx' and "
                f"'engine' backends. Export a non-QAT checkpoint to this format instead."
            )
            assert self.args.quantize in {None, 8}, (
                f"a QAT model exports INT8, but got quantize={self.args.quantize}. Export a non-QAT checkpoint for "
                f"other precisions."
            )
            self.args.quantize = 8  # the graph carries Q/DQ nodes whether or not INT8 was requested
        if self.args.quantize in {8, "w8a16"} and not self.args.data and not self.qat and "data" in fmt_keys:
            self.args.data = DEFAULT_CFG.data or TASK2DATA[getattr(model, "task", "detect")]  # assign default data
            LOGGER.warning(
                f"INT8 export requires a missing 'data' arg for calibration. Using default 'data={self.args.data}'."
            )
        # Recommend OpenVINO if export and Intel CPU
        if SETTINGS.get("openvino_msg"):
            if is_intel():
                LOGGER.info(
                    "💡 ProTip: Export to OpenVINO format for best performance on Intel hardware."
                    " Learn more at https://docs.ultralytics.com/integrations/openvino"
                )
            SETTINGS["openvino_msg"] = False

        # Input
        im = torch.zeros(self.args.batch, model.yaml.get("channels", 3), *self.imgsz).to(self.device)
        file = Path(
            getattr(model, "pt_path", None) or getattr(model, "yaml_file", None) or model.yaml.get("yaml_file", "")
        )
        if file.suffix in {".yaml", ".yml"}:
            file = Path(file.name)

        # Update model
        for p in model.parameters():
            p.requires_grad = False
        model.eval()
        model.float()
        model = model.fuse(imgsz=self.imgsz)

        if fmt == "imx":
            from ultralytics.utils.export.imx import FXModel

            model = FXModel(model, self.imgsz)
        if fmt == "edgetpu":
            from ultralytics.utils.export.tensorflow import tf_wrapper

            model = tf_wrapper(model)
        if fmt == "executorch":
            from ultralytics.utils.export.executorch import executorch_wrapper

            model = executorch_wrapper(model)
        for m in model.modules():
            if isinstance(m, Attention) and fmt == "coreml" and self.args.format != "mlmodel":
                m.format = fmt
            if isinstance(m, (Classify, SemanticSegment, Depth)):
                m.export = True
                m.format = self.args.format
                # Semantic argmax bake needs an integer graph output; TensorRT supports uint8 outputs only on TRT>=10
                # (Jetson TRT 8.x rejects them). Read the version from the package name to avoid importing tensorrt here.
                if isinstance(m, SemanticSegment) and fmt == "engine":
                    cuda_major = (torch.version.cuda or "12").split(".")[0]
                    m.bake_argmax = check_version(f"tensorrt-cu{cuda_major}", ">=10.0.0") or check_version(
                        "tensorrt", ">=10.0.0"
                    )
            if isinstance(m, (Detect, RTDETRDecoder)):  # includes all Detect subclasses like Segment, Pose, OBB
                m.dynamic = self.args.dynamic
                m.export = True
                m.format = self.args.format
                # Clamp max_det to available queries/anchors (required for TensorRT compatibility)
                available = (
                    m.num_queries
                    if isinstance(m, RTDETRDecoder)
                    else sum(int(self.imgsz[0] / s) * int(self.imgsz[1] / s) for s in model.stride.tolist())
                )
                m.max_det = min(self.args.max_det, available)
                m.agnostic_nms = self.args.agnostic_nms
                # CoreML detect keeps IOSDetectModel's own xywh handling; segment/pose route through
                # NMSModel like every other format, which expects xyxy boxes.
                m.xyxy = self.args.nms and (fmt != "coreml" or model.task != "detect")
                m.shape = None  # reset cached shape for new export input size
                if hasattr(model, "pe") and hasattr(m, "fuse") and not hasattr(m, "lrpc"):  # for YOLOE models
                    m.fuse(model.pe.to(self.device))
            elif isinstance(m, C2f) and not is_tf_format:
                # EdgeTPU does not support FlexSplitV while split provides cleaner ONNX graph
                m.forward = m.forward_split

        if model.task == "semantic" and fmt in {"qnn", "coreml", "ascend"}:
            # NPU-targeted semantic exports ship a compact uint8 class map instead of float logits: emitting logits
            # forces consumers to dequantize and argmax ~8M floats at 640px (~20M at 1024px) on the CPU every frame
            # (measured erratic 123-1065 ms on Hexagon at 1024px). Not applied to LiteRT, where the GPU delegate cannot compile ArgMax (int64
            # indices) and a whole-graph CPU fallback is slower than GPU logits + consumer-side argmax. Python
            # predict/val accept both forms.
            model = ClassMapModel(model)

        y = NMSModel(model, self.args)(im) if self.args.nms and fmt not in {"coreml", "imx"} else model(im)  # dry run
        if self.args.quantize == 16 and fmt in {"onnx", "torchscript"} and self.device.type != "cpu":
            im, model = im.half(), model.half()  # to FP16

        # Assign
        self.im = im
        self.model = model
        self.file = file
        self.output_shape = (
            tuple(y.shape)
            if isinstance(y, torch.Tensor)
            else tuple(tuple(x.shape if isinstance(x, torch.Tensor) else []) for x in y)
        )
        self.pretty_name = Path(self.model.yaml.get("yaml_file", self.file)).stem.replace("yolo", "YOLO")
        data = model.args["data"] if hasattr(model, "args") and isinstance(model.args, dict) else ""
        description = f"Ultralytics {self.pretty_name} model {f'trained on {data}' if data else ''}"
        self.metadata = {
            "description": description,
            "author": "Ultralytics",
            "date": datetime.now().astimezone().isoformat(),
            "version": __version__,
            "license": "AGPL-3.0 License (https://ultralytics.com/license)",
            "docs": "https://docs.ultralytics.com",
            "stride": int(max(model.stride)),
            "task": model.task,
            "head": type(model.model[-1]).__name__,
            "batch": self.args.batch,
            "imgsz": self.imgsz,
            "names": model.names,
            "args": {k: str(v) if isinstance(v, Path) else v for k, v in self.args if k in fmt_keys},
            "channels": model.yaml.get("channels", 3),
            "end2end": getattr(model, "end2end", False),
        }  # model metadata
        if self.dla is not None:
            self.metadata["dla"] = self.dla  # make sure `AutoBackend` uses correct dla device if it has one
        if model.task == "pose":
            self.metadata["kpt_shape"] = model.model[-1].kpt_shape
            if hasattr(model, "kpt_names"):
                self.metadata["kpt_names"] = model.kpt_names

        LOGGER.info(
            f"\n{colorstr('PyTorch:')} starting from '{file}' with input shape {tuple(im.shape)} BCHW and "
            f"output shape(s) {self.output_shape} ({file_size(file):.1f} MB)"
        )
        self.run_callbacks("on_export_start")

        # Export
        if is_tf_format:
            f, keras_model = self.export_saved_model()
            if fmt == "pb":
                f = self.export_pb(keras_model=keras_model)
            if fmt == "edgetpu":
                f = self.export_edgetpu(tflite_model=Path(f) / f"{self.file.stem}_full_integer_quant.tflite")
        else:
            f = getattr(self, f"export_{fmt}")()

        # Finish
        if f:
            square = self.imgsz[0] == self.imgsz[1]
            s = (
                ""
                if square
                else f"WARNING ⚠️ non-PyTorch val requires square images, 'imgsz={self.imgsz}' will not "
                f"work. Use export 'imgsz={max(self.imgsz)}' if val is required."
            )
            imgsz = self.imgsz[0] if square else str(self.imgsz)[1:-1].replace(" ", "")
            q = "quantize=16" if self.args.quantize == 16 else ""  # FP16 inference flag for the val/predict hint
            d = f"data={data}" if check_data_portable(data) else ""  # omit host paths that would not run here
            inference_commands = (
                f"\nPredict:         yolo predict task={model.task} model={f} imgsz={imgsz} {q}"
                f"\nValidate:        yolo val task={model.task} model={f} imgsz={imgsz} "
                f"{d} {q} {s}"
                if fmt in AutoBackend._BACKEND_MAP
                else ""
            )
            LOGGER.info(
                f"\nExport complete ({time.time() - t:.1f}s)"
                f"\nResults saved to {colorstr('bold', Path(f).resolve())}"
                f"{inference_commands}"
                f"\nVisualize:       https://netron.app"
            )

        self.run_callbacks("on_export_end")
        return f  # path to final export artifact

    def get_int8_calibration_dataloader(self, prefix=""):
        """Build and return a dataloader for calibration of INT8 models."""
        LOGGER.info(f"{prefix} collecting INT8 calibration images from 'data={self.args.data}'")
        cfg = deepcopy(self.args)
        cfg.imgsz = max(self.imgsz)
        split = self.args.split or "val"
        if self.model.task == "classify":
            import torchvision.transforms as T  # scope for faster 'import ultralytics'

            data = check_cls_dataset(self.args.data, split=split)
            if not isinstance(cfg.fraction, list):
                cfg.fraction = [cfg.fraction] * 3
            dataset = ClassificationDataset(data[split], args=cfg, augment=False, prefix=split)
            # INT8 backends divide images by 255, so emit uint8 [0, 255] center-cropped like classify inference
            dataset.torch_transforms = T.Compose([T.Resize(cfg.imgsz), T.CenterCrop(cfg.imgsz), T.PILToTensor()])
        else:
            data = check_det_dataset(self.args.data, split=split)
            cfg.fraction = get_split_fraction(cfg.fraction, split) if isinstance(cfg.fraction, list) else cfg.fraction
            dataset = build_yolo_dataset(
                cfg,
                data[split],
                self.args.batch,
                data,
                mode="val",
                fraction=cfg.fraction,
            )
        if hasattr(dataset, "transforms") and hasattr(dataset.transforms.transforms[0], "new_shape"):
            dataset.transforms.transforms[0].new_shape = self.imgsz  # LetterBox with non-square imgsz
        n = len(dataset)
        if n < 1:
            raise ValueError(f"The calibration dataset must have at least 1 image, but found {n} images.")
        batch = min(self.args.batch, n)
        if n < self.args.batch:
            LOGGER.warning(
                f"{prefix} calibration dataset has only {n} images, reducing calibration batch size to {batch}."
            )
        if self.args.format == "axelera" and n < 100:
            LOGGER.warning(f"{prefix} >100 images required for Axelera calibration, found {n} images.")
        elif self.args.format != "axelera" and n < 300:
            LOGGER.warning(f"{prefix} >300 images recommended for INT8 calibration, found {n} images.")
        return build_dataloader(dataset, batch=batch, workers=0, drop_last=True)  # required for batch loading

    def _int8_calibration_images(self, prefix=""):
        """Collect calibration batches directly into one BHWC float32 array."""
        loader = self.get_int8_calibration_dataloader(prefix)
        images = np.empty((len(loader) * loader.batch_size, *self.imgsz, self.im.shape[1]), dtype=np.float32)
        for i, batch in enumerate(loader):
            images[i * loader.batch_size : (i + 1) * loader.batch_size] = (
                torch.nn.functional.interpolate(batch["img"].float(), size=self.imgsz).permute(0, 2, 3, 1).numpy()
            )
        return images

    @try_export
    def export_torchscript(self, prefix=colorstr("TorchScript:")):  # noqa: B008
        """Export YOLO model to TorchScript format."""
        from ultralytics.utils.export.torchscript import torch2torchscript

        return torch2torchscript(
            model=NMSModel(self.model, self.args) if self.args.nms else self.model,
            im=self.im,
            output_file=self.file.with_suffix(".torchscript"),
            metadata=self.metadata,
            prefix=prefix,
        )

    @try_export
    def export_onnx(self, prefix=colorstr("ONNX:")):  # noqa: B008
        """Export YOLO model to ONNX format."""
        requirements = ["onnx>=1.16.1,<1.19.0" if self.args.format == "rknn" else "onnx>=1.12.0,<2.0.0"]
        if self.args.simplify or (self.args.format == "onnx" and self.args.quantize == 8 and not self.qat):
            # Pass onnxruntime variants as interchangeable candidates so AutoUpdate keeps an installed build
            # (e.g. onnxruntime-qnn for QNN export) instead of reinstalling stable onnxruntime and breaking its ABI.
            # ROCm gets stock onnxruntime, the base the MIGraphX EP plugin installs onto at inference.
            ort = "onnxruntime-gpu" if "cuda" in self.device.type and not rocm_is_available() else "onnxruntime"
            requirements += [(ort, "onnxruntime", "onnxruntime-gpu", "onnxruntime-qnn", "onnxruntime-vitisai")]
        if self.args.simplify:
            requirements += ["onnxslim>=0.1.82"]
        check_requirements(requirements)
        import onnx

        from ultralytics.utils.export.engine import best_onnx_opset, torch2onnx

        opset = self.args.opset or best_onnx_opset(onnx)
        # nearest Upsample needs 9, NMS and depth align_corners Resize need 11, RT-DETR GridSample needs 16
        min_opset = {"classify": 7, "depth": 11}.get(self.model.task, 11 if self.args.nms else 9)
        if isinstance(self.model.model[-1], RTDETRDecoder):
            min_opset = 16
        assert opset >= min_opset, f"ONNX export requires opset>={min_opset} for this model, but got opset={opset}"
        LOGGER.info(f"\n{prefix} starting export with onnx {onnx.__version__} opset {opset}...")
        if self.args.nms:
            assert TORCH_1_13, f"'nms=True' ONNX export requires torch>=1.13 (found torch=={TORCH_VERSION})"

        f = str(self.file.with_suffix(".onnx"))
        output_names = ["output0", "output1"] if self.model.task == "segment" else ["output0"]
        dynamic = self.args.dynamic
        if dynamic:
            dynamic = {"images": {0: "batch", 2: "height", 3: "width"}}  # shape(1,3,640,640)
            if isinstance(self.model, SegmentationModel):
                dynamic["output0"] = {0: "batch", 2: "anchors"}  # shape(1, 116, 8400)
                dynamic["output1"] = {0: "batch", 2: "mask_height", 3: "mask_width"}  # shape(1,32,160,160)
            elif isinstance(self.model, DepthModel):
                dynamic["output0"] = {0: "batch", 2: "height", 3: "width"}  # shape(1,1,640,640) dense map, not anchors
            elif isinstance(self.model, DetectionModel):
                dynamic["output0"] = {0: "batch", 2: "anchors"}  # shape(1, 84, 8400)
            if self.args.nms:
                dynamic["output0"].pop(2)
        if self.args.nms and self.model.task == "obb":
            self.args.opset = opset  # for NMSModel
            self.args.simplify = True  # fix OBB runtime error related to topk

        model = NMSModel(self.model, self.args) if self.args.nms else self.model
        # Normalize coordinates by input size so RKNN's per-tensor INT8 scale preserves class scores.
        if (
            self.args.format == "rknn"
            and self.args.quantize == 8
            and self.model.task in {"detect", "segment", "pose", "obb"}
            and not self.metadata["end2end"]
            and self.metadata["head"] != "RTDETRDecoder"
        ):
            from ultralytics.utils.export.engine import _NormalizeCoords

            model = _NormalizeCoords(
                model,
                int(self.im.shape[2]),
                int(self.im.shape[3]),
                self.model.task,
                len(self.metadata["names"]),
                self.metadata.get("kpt_shape"),
            )

        with arange_patch(dynamic=bool(dynamic), quantize=self.args.quantize, fmt=self.args.format):
            torch2onnx(
                model.cpu() if self.qat else model,
                self.im.cpu() if self.qat else self.im,
                f,
                opset=opset,
                input_names=["images"],
                output_names=output_names,
                dynamic=dynamic or None,
            )

        # Checks
        model_onnx = onnx.load(f)  # load onnx model

        # Simplify
        if self.args.simplify:
            try:
                import onnxslim

                LOGGER.info(f"{prefix} slimming with onnxslim {onnxslim.__version__}...")
                model_onnx = onnxslim.slim(model_onnx)

            except Exception as e:
                LOGGER.warning(f"{prefix} simplifier failure: {e}")

        # CANN requires the optional score-threshold input on ONNX NonMaxSuppression nodes. Scores were already
        # filtered by args.conf in NMSModel, so zero preserves the graph's semantics.
        if self.args.format == "ascend":
            for i, node in enumerate(model_onnx.graph.node):
                if node.op_type == "NonMaxSuppression" and len(node.input) == 4:
                    threshold_name = f"ascend_nms_score_threshold_{i}"
                    node.input.append(threshold_name)
                    model_onnx.graph.initializer.append(
                        onnx.helper.make_tensor(threshold_name, onnx.TensorProto.FLOAT, [1], [0.0])
                    )

        # Metadata
        for k, v in self.metadata.items():
            meta = model_onnx.metadata_props.add()
            meta.key, meta.value = k, str(v)

        # IR version
        if getattr(model_onnx, "ir_version", 0) > 10:
            LOGGER.info(f"{prefix} limiting IR version {model_onnx.ir_version} to 10 for ONNXRuntime compatibility...")
            model_onnx.ir_version = 10

        # FP16 conversion for CPU export (GPU exports are already FP16 from model.half() during tracing)
        if self.args.quantize == 16 and self.args.format == "onnx" and self.device.type == "cpu":
            try:
                from onnxruntime.transformers import float16

                LOGGER.info(f"{prefix} converting to FP16...")
                model_onnx = float16.convert_float_to_float16(model_onnx, keep_io_types=True)
            except Exception as e:
                LOGGER.warning(f"{prefix} FP16 conversion failure: {e}")

        onnx.save(model_onnx, f)
        del model_onnx
        if self.args.quantize == 8 and self.args.format == "onnx" and not self.qat:  # QAT exports Q/DQ directly
            from ultralytics.utils.export.onnx import onnx_int8_quantize

            source = Path(f)
            f_int8 = str(source.with_name(f"{source.stem}_int8{source.suffix}"))
            f = onnx_int8_quantize(
                source,
                f_int8,
                self.get_int8_calibration_dataloader(prefix),
                self._transform_fn,
                prefix=prefix,
            )
            source.unlink(missing_ok=True)
        return f

    @try_export
    def export_openvino(self, prefix=colorstr("OpenVINO:")):  # noqa: B008
        """Export YOLO model to OpenVINO format."""
        from ultralytics.utils.export.openvino import torch2openvino

        # OpenVINO <= 2025.1.0 error on macOS 15.4+: https://github.com/openvinotoolkit/openvino/issues/30023
        check_requirements("openvino>=2025.2.0" if MACOS and MACOS_VERSION >= "15.4" else "openvino>=2024.0.0")
        import openvino as ov

        assert TORCH_2_1, f"OpenVINO export requires torch>=2.1 but torch=={TORCH_VERSION} is installed"

        def serialize(ov_model, file):
            """Set RT info, serialize, and save metadata YAML."""
            ov_model.set_rt_info("YOLO", ["model_info", "model_type"])
            ov_model.set_rt_info(True, ["model_info", "reverse_input_channels"])
            ov_model.set_rt_info(114, ["model_info", "pad_value"])
            ov_model.set_rt_info([255.0], ["model_info", "scale_values"])
            ov_model.set_rt_info(self.args.iou, ["model_info", "iou_threshold"])
            ov_model.set_rt_info([v.replace(" ", "_") for v in self.model.names.values()], ["model_info", "labels"])
            if self.model.task != "classify":
                ov_model.set_rt_info("fit_to_window_letterbox", ["model_info", "resize_type"])

            ov.save_model(ov_model, file, compress_to_fp16=self.args.quantize == 16)
            YAML.save(Path(file).parent / "metadata.yaml", self.metadata)  # add metadata.yaml

        calibration_dataset = None
        if self.args.quantize == 8:
            check_requirements("packaging>=23.2")  # must be installed first to build nncf wheel
            check_requirements("nncf>=2.14.0,<3.0.0" if not TORCH_2_3 else "nncf>=2.14.0")
            import nncf

            calibration_dataset = nncf.Dataset(self.get_int8_calibration_dataloader(prefix), self._transform_fn)

        ov_model = torch2openvino(
            model=NMSModel(self.model, self.args) if self.args.nms else self.model,
            im=self.im,
            dynamic=self.args.dynamic,
            quantize=self.args.quantize,
            calibration_dataset=calibration_dataset,
            int8_detect=isinstance(self.model.model[-1], (Detect, RTDETRDecoder)),
            prefix=prefix,
        )

        suffix = f"_{'int8_' if self.args.quantize == 8 else ''}openvino_model{os.sep}"
        f = str(self.file).replace(self.file.suffix, suffix)
        f_ov = str(Path(f) / self.file.with_suffix(".xml").name)

        serialize(ov_model, f_ov)
        return f

    @try_export
    def export_paddle(self, prefix=colorstr("PaddlePaddle:")):  # noqa: B008
        """Export YOLO model to PaddlePaddle format."""
        from ultralytics.utils.export.paddle import torch2paddle

        return torch2paddle(
            model=self.model,
            im=self.im,
            output_dir=str(self.file).replace(self.file.suffix, f"_paddle_model{os.sep}"),
            metadata=self.metadata,
            prefix=prefix,
        )

    @try_export
    def export_litert(self, prefix=colorstr("LiteRT:")):  # noqa: B008
        """Export YOLO model to LiteRT format using litert_torch with optional INT8 quantization.

        Supports ``quantize=8`` (static INT8, int8 weights + int8 activations, requires calibration ``data``),
        ``quantize='w8a16'`` (static, int8 weights + int16 activations, requires calibration ``data``) and
        ``quantize='w8a32'`` (dynamic/weight-only INT8, int8 weights + FP32 activations, no calibration needed).
        """
        assert MACOS or (LINUX and not ARM64), "LiteRT export only supported on Linux x86 and macOS"
        from ultralytics.utils.export.litert import torch2litert

        return torch2litert(
            self.model,
            self.im,
            self.file,
            quantize=self.args.quantize,
            calibration_dataset=self.get_int8_calibration_dataloader(prefix)
            if self.args.quantize in {8, "w8a16"}
            else None,
            metadata=self.metadata,
            prefix=prefix,
        )

    @try_export
    def export_mnn(self, prefix=colorstr("MNN:")):  # noqa: B008
        """Export YOLO model to MNN format using MNN https://github.com/alibaba/MNN."""
        from ultralytics.utils.export.mnn import onnx2mnn

        return onnx2mnn(
            onnx_file=self.export_onnx(),
            output_file=self.file.with_suffix(".mnn"),
            quantize=self.args.quantize,
            metadata=self.metadata,
            prefix=prefix,
        )

    @try_export
    def export_ncnn(self, prefix=colorstr("NCNN:")):  # noqa: B008
        """Export YOLO model to NCNN format using PNNX https://github.com/pnnx/pnnx."""
        from ultralytics.utils.export.ncnn import torch2ncnn

        return torch2ncnn(
            model=self.model,
            im=self.im,
            output_dir=str(self.file).replace(self.file.suffix, "_ncnn_model/"),
            quantize=self.args.quantize,
            metadata=self.metadata,
            device=self.device,
            prefix=prefix,
        )

    @try_export
    def export_coreml(self, prefix=colorstr("CoreML:")):  # noqa: B008
        """Export YOLO model to CoreML format."""
        mlmodel = self.args.format == "mlmodel"  # legacy *.mlmodel export format requested
        from ultralytics.utils.export.coreml import IOSDetectModel, pipeline_coreml, torch2coreml

        # numpy 2.4.x breaks coremltools CoreML export https://github.com/apple/coremltools/issues/2633
        check_requirements(["coremltools>=9.0", "numpy>=1.14.5,<=2.3.5"])
        import coremltools as ct

        assert not WINDOWS, "CoreML export is not supported on Windows, please run on macOS or Linux."
        assert TORCH_1_11, "CoreML export requires torch>=1.11"
        f = self.file.with_suffix(".mlmodel" if mlmodel else ".mlpackage")

        if self.args.nms and self.model.task == "detect":
            model = IOSDetectModel(self.model, self.im, mlprogram=not mlmodel)
        elif self.args.nms:  # segment, pose: NMS baked into the trace instead of Apple's NMS pipeline,
            model = NMSModel(self.model, self.args)  # which can't carry masks/keypoints through suppression
        else:
            model = self.model

        if self.args.dynamic:
            h, w = self.imgsz
            lb_h = lb_w = 32
            if getattr(self.model, "end2end", False):
                # end2end graphs bake TopK k=max_det, so the smallest declared input must still supply >= k anchors
                # or CoreML rejects the model at load; shrink the range proportionally from the traced default size
                stride = int(self.model.stride.max())
                r = self.model.model[-1].max_det / sum(int(h / s) * int(w / s) for s in self.model.stride.tolist())
                lb_h = max(lb_h, int(np.ceil(h * r**0.5 / stride)) * stride)
                lb_w = max(lb_w, int(np.ceil(w * r**0.5 / stride)) * stride)
            input_shape = ct.Shape(
                shape=(
                    ct.RangeDim(lower_bound=1, upper_bound=self.args.batch, default=1),
                    self.im.shape[1],
                    ct.RangeDim(lower_bound=lb_h, upper_bound=h * 2, default=h),
                    ct.RangeDim(lower_bound=lb_w, upper_bound=w * 2, default=w),
                )
            )
            inputs = [ct.TensorType("image", shape=input_shape)]
        else:
            inputs = [ct.ImageType("image", shape=self.im.shape, scale=1 / 255, bias=[0.0, 0.0, 0.0])]

        quantize = 16 if self.args.nms and not mlmodel and self.args.quantize is None else self.args.quantize
        self.metadata["args"]["quantize"] = quantize
        ct_model = torch2coreml(
            model=model,
            inputs=inputs,
            im=self.im,
            classifier_names=list(self.model.names.values()) if self.model.task == "classify" else None,
            mlmodel=mlmodel,
            quantize=quantize,
            metadata=self.metadata,
            prefix=prefix,
        )

        if self.args.nms and self.model.task == "detect":
            ct_model = pipeline_coreml(
                ct_model,
                self.output_shape,
                weights_dir=None if mlmodel else ct_model.weights_dir,
                metadata=self.metadata,
                mlmodel=mlmodel,
                iou=self.args.iou,
                conf=self.args.conf,
                agnostic_nms=self.args.agnostic_nms,
                prefix=prefix,
            )

        if self.model.task == "classify":
            ct_model.user_defined_metadata.update({"com.apple.coreml.model.preview.type": "imageClassifier"})

        ct_model.save(str(f))  # save *.mlpackage or *.mlmodel
        return f

    @try_export
    def export_engine(self, prefix=colorstr("TensorRT:")):  # noqa: B008
        """Export YOLO model to TensorRT format https://developer.nvidia.com/tensorrt."""
        assert self.im.device.type != "cpu", "export running on CPU but must be on GPU, i.e. use 'device=0'"
        f_onnx = self.export_onnx()  # run before TRT import https://github.com/ultralytics/ultralytics/issues/7016
        from ultralytics.utils.export.engine import onnx2engine

        assert Path(f_onnx).exists(), f"failed to export ONNX file: {f_onnx}"
        f = self.file.with_suffix(".engine")  # TensorRT engine file
        onnx2engine(
            f_onnx,
            f,
            self.args.workspace,
            self.args.quantize,
            self.args.dynamic,
            self.im.shape,
            dla=self.dla,
            dataset=self.get_int8_calibration_dataloader(prefix) if self.args.quantize == 8 and not self.qat else None,
            metadata=self.metadata,
            verbose=self.args.verbose,
            prefix=prefix,
        )

        return f

    @try_export
    def export_saved_model(self, prefix=colorstr("TensorFlow SavedModel:")):  # noqa: B008
        """Export YOLO model to TensorFlow SavedModel format."""
        assert not (MACOS and IS_PYTHON_MINIMUM_3_13), (
            "TensorFlow exports not supported on macOS with Python>=3.13: the ai-edge-litert macOS wheel fails to load "
            "(missing libpywrap_litert_common.dylib). TensorFlow export works on Linux Python 3.13."
        )
        from ultralytics.utils.export.tensorflow import onnx2saved_model

        f = Path(str(self.file).replace(self.file.suffix, "_saved_model"))
        if f.is_dir():
            shutil.rmtree(f)  # delete output folder

        # Export to ONNX
        if isinstance(self.model.model[-1], RTDETRDecoder):
            self.args.opset = self.args.opset or 19
            assert self.args.opset <= 19, "RTDETR TensorFlow export requires opset<=19"
        self.args.simplify = True
        f_onnx = self.export_onnx()  # ensure ONNX is available
        keras_model = onnx2saved_model(
            f_onnx,
            f,
            quantize=self.args.quantize,
            # built inline as a temporary so onnx2saved_model's `del images` frees it before the conversion phase
            images=self._int8_calibration_images(prefix) if self.args.quantize == 8 and self.args.data else None,
            disable_group_convolution=self.args.format == "edgetpu",
            cuda=self.device.type == "cuda" and not rocm_is_available(),  # TensorFlow and onnxruntime-gpu are CUDA-only
            prefix=prefix,
        )
        YAML.save(f / "metadata.yaml", self.metadata)  # add metadata.yaml
        # Add TFLite metadata
        for file in f.rglob("*.tflite"):
            file.unlink() if "quant_with_int16_act.tflite" in str(file) else self._add_tflite_metadata(file)

        return str(f), keras_model  # or keras_model = tf.saved_model.load(f, tags=None, options=None)

    @try_export
    def export_pb(self, keras_model, prefix=colorstr("TensorFlow GraphDef:")):  # noqa: B008
        """Export YOLO model to TensorFlow GraphDef *.pb format https://github.com/leimao/Frozen-Graph-TensorFlow."""
        from ultralytics.utils.export.tensorflow import keras2pb

        return keras2pb(keras_model, output_file=self.file.with_suffix(".pb"), prefix=prefix)

    @try_export
    def export_axelera(self, prefix=colorstr("Axelera:")):  # noqa: B008
        """Export YOLO model to Axelera format."""
        assert LINUX and not (ARM64 and IS_DOCKER), (
            "export is only supported on Linux and is not supported on ARM64 Docker."
        )
        assert TORCH_2_8, "export requires torch>=2.8.0."

        from ultralytics.utils.export.axelera import torch2axelera

        output_dir = self.file.parent / f"{self.file.stem}_axelera_model"
        return torch2axelera(
            model=self.model,
            output_dir=output_dir,
            calibration_dataset=self.get_int8_calibration_dataloader(prefix),
            transform_fn=self._transform_fn,
            model_name=self.file.stem,
            metadata=self.metadata,
            prefix=prefix,
        )

    @try_export
    def export_executorch(self, prefix=colorstr("ExecuTorch:")):  # noqa: B008
        """Export YOLO model to ExecuTorch *.pte format."""
        assert TORCH_2_9, f"ExecuTorch requires torch>=2.9.0 but torch=={TORCH_VERSION} is installed"
        from ultralytics.utils.export.executorch import torch2executorch

        return torch2executorch(
            model=self.model,
            im=self.im,
            output_dir=str(self.file).replace(self.file.suffix, "_executorch_model/"),
            metadata=self.metadata,
            prefix=prefix,
        )

    @try_export
    def export_coreai(self, prefix=colorstr("Core AI:")):  # noqa: B008
        """Export YOLO model to Apple Core AI *.aimodel format."""
        assert (MACOS and ARM64 and MACOS_VERSION >= "26.0") or (LINUX and not ARM64), (
            "Core AI export requires macOS>=26 on Apple silicon or x86_64 Linux, the platforms coreai-core publishes."
        )
        assert TORCH_2_8, f"Core AI export requires torch>=2.8.0 but torch=={TORCH_VERSION} is installed"
        from ultralytics.utils.export.coreai import torch2coreai

        return torch2coreai(
            model=self.model,
            im=self.im,
            output_file=self.file.with_suffix(".aimodel"),
            quantize=self.args.quantize,
            metadata=self.metadata,
            prefix=prefix,
        )

    @try_export
    def export_edgetpu(self, tflite_model: Path, prefix=colorstr("Edge TPU:")):  # noqa: B008
        """Export YOLO model to Edge TPU format https://coral.ai/docs/edgetpu/models-intro/."""
        from ultralytics.utils.export.tensorflow import tflite2edgetpu

        output_file = tflite2edgetpu(tflite_file=tflite_model, output_dir=tflite_model.parent, prefix=prefix)
        self._add_tflite_metadata(output_file)
        return output_file

    @try_export
    def export_rknn(self, prefix=colorstr("RKNN:")):  # noqa: B008
        """Export YOLO model to RKNN format with optional INT8 quantization."""
        from ultralytics.utils.export.rknn import onnx2rknn

        if self.args.opset and self.args.opset > 19:
            LOGGER.warning(f"{prefix} rknn-toolkit2 requires opset<=19, setting opset=19.")
        self.args.opset = min(self.args.opset or 19, 19)  # rknn-toolkit expects opset<=19
        self.im = self.im[:1]  # RKNN Toolkit expands the batch after calibrating the batch-1 ONNX model
        f_onnx = self.export_onnx()
        output_dir = Path(str(self.file).replace(self.file.suffix, f"_rknn_model{os.sep}"))
        rknn_dataset = None
        if self.args.quantize == 8:
            dataloader = self.get_int8_calibration_dataloader(prefix)
            image_paths = getattr(dataloader.dataset, "im_files", None)
            if image_paths is None and hasattr(dataloader.dataset, "samples"):
                image_paths = [x[0] for x in dataloader.dataset.samples]
            if not image_paths:
                raise ValueError("RKNN INT8 export requires a calibration dataset with image file paths.")
            output_dir.mkdir(parents=True, exist_ok=True)
            rknn_dataset = output_dir / "dataset.txt"
            rknn_dataset.write_text("\n".join(str(Path(x).resolve()) for x in image_paths) + "\n")
        try:
            return onnx2rknn(
                onnx_file=f_onnx,
                output_dir=output_dir,
                name=self.args.name,
                quantize=self.args.quantize,
                batch=self.args.batch,
                dataset=rknn_dataset,
                metadata=self.metadata,
                prefix=prefix,
            )
        finally:
            if self.args.quantize == 8:  # INT8 graphs hold normalized coordinates, so they are not reusable
                Path(f_onnx).unlink(missing_ok=True)

    @try_export
    def export_ascend(self, prefix=colorstr("Ascend:")):  # noqa: B008
        """Export YOLO model to Huawei Ascend offline model (.om) format."""
        from ultralytics.utils.export.ascend import _check_atc, onnx2ascend

        _check_atc()  # before the ONNX trace, so a missing toolchain does not cost a full export first
        if self.args.opset and self.args.opset > 17:
            LOGGER.warning(f"{prefix} the CANN ONNX parser requires opset<=17, setting opset=17.")
        self.args.opset = min(self.args.opset or 17, 17)
        return onnx2ascend(
            onnx_file=self.export_onnx(),
            output_dir=self.file.parent / f"{self.file.stem}_ascend_model",
            name=self.args.name,
            imgsz=self.imgsz,
            batch=self.args.batch,
            channels=self.im.shape[1],
            metadata=self.metadata,
            prefix=prefix,
        )

    @try_export
    def export_imx(self, prefix=colorstr("IMX:")):  # noqa: B008
        """Export YOLO model to IMX format."""
        assert LINUX, (
            "Export only supported on Linux. "
            "See https://developer.aitrios.sony-semicon.com/en/docs/raspberry-pi-ai-camera/imx500-converter?version=3.17.3&progLang="
        )
        assert IS_PYTHON_MINIMUM_3_9, "IMX export is only supported on Python 3.9 or above."

        from ultralytics.utils.export.imx import torch2imx

        return torch2imx(
            model=self.model,
            output_dir=str(self.file).replace(self.file.suffix, "_imx_model/"),
            conf=self.args.conf,
            iou=self.args.iou,
            max_det=self.args.max_det,
            metadata=self.metadata,
            dataset=partial(self.get_int8_calibration_dataloader, prefix),
            prefix=prefix,
        )

    @try_export
    def export_deepx(self, prefix=colorstr("DEEPX:")):  # noqa: B008
        """Export YOLO model to DEEPX format."""
        assert LINUX and not ARM64, "DEEPX export only supported on non-aarch64 Linux"
        from ultralytics.utils.export.deepx import onnx2deepx

        f = self.export_onnx()
        return onnx2deepx(
            onnx_file=f,
            imgsz=self.imgsz,
            dataset=self.get_int8_calibration_dataloader(prefix),
            metadata=self.metadata,
            optimize=self.args.optimize,
            prefix=prefix,
        )

    @try_export
    def export_qnn(self, prefix=colorstr("Qualcomm QNN:")):  # noqa: B008
        """Export YOLO model to a Qualcomm QNN context binary using ONNX Runtime QNN."""
        from ultralytics.utils.export.qnn import onnx2qnn

        # Wrap for Hexagon-friendly I/O: channel-last input (the class-map wrap for semantic is format-agnostic)
        model, im = self.model, self.im
        try:
            self.model, self.im = QNNModel(model), im.permute(0, 2, 3, 1)
            f_onnx = self.export_onnx()
        finally:
            self.model, self.im = model, im
        return onnx2qnn(
            onnx_file=f_onnx,
            output_file=str(self.file.with_name(f"{self.file.stem}_qnn.onnx")),
            dataset=self.get_int8_calibration_dataloader(prefix),
            transform_fn=self._transform_fn,
            name=self.args.name,
            metadata=self.metadata,
            prefix=prefix,
        )

    @try_export
    def export_xilinx(self, prefix=colorstr("AMD Xilinx:")):  # noqa: B008
        """Export YOLO model to AMD Xilinx Vitis AI format for Versal AI Edge Series Gen 2 NPUs."""
        assert LINUX, "AMD Xilinx export is only supported on Linux."
        check_python(">=3.11,<3.14")  # AMD Quark wheels, checked before the ONNX trace
        from ultralytics.utils.export.xilinx import onnx2xilinx

        self.args.opset = self.args.opset or 17  # opset validated by AMD for YOLO on Vitis AI
        return onnx2xilinx(
            onnx_file=self.export_onnx(),
            output_dir=self.file.parent / f"{self.file.stem}_xilinx_model",
            dataset=self.get_int8_calibration_dataloader(prefix),
            transform_fn=self._transform_fn,
            name=self.args.name,
            prefix=prefix,
        )

    @try_export
    def export_hailo(self, prefix=colorstr("Hailo:")):  # noqa: B008
        """Export a YOLO model to Hailo Executable Format (HEF)."""
        try:
            from hailo_sdk_client import ClientRunner
        except ImportError as e:
            raise ImportError("Hailo export requires the Hailo Dataflow Compiler.") from e

        calibration_images = self._int8_calibration_images(prefix)  # fixed set, every DFC pass must see the same images
        calibration_size = len(calibration_images)
        LOGGER.warning(
            f"\nHailo level-2 optimization will use {calibration_size} calibration images. "
            "Hailo recommends at least 1,024 representative images for best accuracy. "
            'Pass data="path/to/dataset.yaml". '
            "See https://docs.ultralytics.com/integrations/hailo#export-a-hailo-hef-model"
        )
        head_index = len(self.model.model) - 1
        head = self.model.model[head_index]
        one2one = getattr(self.model, "end2end", False)
        task = self.model.task
        raw_detect = task == "detect" and head.reg_max == 1
        if task == "classify":
            # The Classify head ends in Gemm -> Softmax; cut at the Softmax so the HEF returns the same
            # (1, nc) probabilities as the PyTorch model. The DFC translates the softmax to a native layer.
            end_nodes = [f"/model.{head_index}/Softmax"]
        elif task == "semantic":
            # Multi-class Hailo-15/10 (DFC 5.x) heads compile the bilinear upsample and ArgMax on chip. Hailo-8/8L
            # (DFC 3.x) cannot compile the Resize, and single-class heads use a threshold instead of ArgMax, so both
            # cut at the classifier logits and run the reduction on the host.
            head.bake_argmax = head.nc > 1 and self.args.name in {"hailo10h", "hailo15h", "hailo15l"}
            end_nodes = [
                f"/model.{head_index}/ArgMax"
                if head.bake_argmax
                else f"/model.{head_index}/classifier/classifier.1/Conv"
            ]
        elif task == "depth":
            # The Depth head ends in clamp/exp, log-affine calibration, and a 4x upsample. Cut at the final logit
            # conv (head.3, the last layer of the dense decoder) so the a16 HEF carries the raw logit and the host
            # mirrors Depth.forward. Same string convention as detect's `.2/Conv`; no new head attribute.
            end_nodes = [f"/model.{head_index}/head/head.3/Conv"]
        else:
            scales = range(len(head.one2one_cv2 if one2one else head.cv2))
            if raw_detect:
                branch_prefix = "one2one_" if one2one else ""
                end_nodes = [
                    f"/model.{head_index}/{branch_prefix}cv{branch}.{i}/{branch_prefix}cv{branch}.{i}.2/Conv"
                    for branch in (2, 3)
                    for i in scales
                ]
            elif task in {"segment", "pose", "obb"}:
                # reg/cls/extra triple per scale (extra = mask coeffs, keypoints, or angle); segment adds prototypes.
                end_nodes = [
                    f"/model.{head_index}/cv{branch}.{i}/cv{branch}.{i}.2/Conv" for i in scales for branch in (2, 3, 4)
                ]
                if task == "segment":
                    end_nodes.append(f"/model.{head_index}/proto/cv3/act/Mul")
            else:
                end_nodes = [
                    f"/model.{head_index}/cv{branch}.{i}/cv{branch}.{i}.2/Conv" for i in scales for branch in (2, 3)
                ]
        self.args.opset = 11
        f_onnx = Path(self.export_onnx())
        output_dir = self.file.parent / f"{self.file.stem}_hailo_model"
        if output_dir.exists():
            shutil.rmtree(output_dir)
        output_dir.mkdir(parents=True, exist_ok=True)
        try:
            runner = ClientRunner(hw_arch=self.args.name)
            runner.translate_onnx_model(str(f_onnx), self.file.stem, end_node_names=end_nodes)
            model_script = [
                "normalization1 = normalization([0, 0, 0], [255, 255, 255])",
                "model_optimization_flavor(optimization_level=2)",
                f"post_quantization_optimization(finetune, policy=enabled, dataset_size={calibration_size})",
            ]
            if raw_detect or task == "depth":
                # a16 on the output(s): the DFL-free detect logits and the single dense depth logit both need the
                # wider activation to keep their range (a8 collapses the depth map; validated on Hailo-8L).
                outputs = ", ".join(f"output_layer{i + 1}" for i in range(len(end_nodes)))
                model_script.append(f"quantization_param([{outputs}], precision_mode=a16_w16)")
            elif task in {"classify", "semantic"}:
                pass  # softmax/class-map is already the graph output; no NMS or activation changes needed
            else:
                outputs = [layer.inputs[0].rsplit("/", 1)[-1] for layer in runner.get_hn_model().get_output_layers()]
                if task in {"segment", "pose", "obb"}:
                    # Bake sigmoid into the class convs only (position 1 of each per-scale reg/cls/extra triple).
                    # Mask coeffs, prototypes, keypoints and angles stay raw and are decoded on the host.
                    model_script.extend(
                        f"change_output_activation({outputs[i]}, sigmoid)" for i in range(1, 3 * len(scales), 3)
                    )
                else:
                    nms_config = output_dir / "nms_config.json"
                    nms_config.write_text(
                        json.dumps(
                            {
                                "nms_scores_th": self.args.conf if self.args.conf is not None else 0.25,
                                "nms_iou_th": self.args.iou,
                                "image_dims": self.imgsz,
                                "max_proposals_per_class": 100,
                                "classes": len(self.model.names),
                                "regression_length": 16,
                                "background_removal": False,
                                "background_removal_index": 0,
                                "bbox_decoders": [
                                    {
                                        "name": f"bbox_decoder_{stride}",
                                        "stride": stride,
                                        "reg_layer": outputs[i * 2],
                                        "cls_layer": outputs[i * 2 + 1],
                                    }
                                    for i, stride in enumerate(int(x) for x in head.stride)
                                ],
                            },
                            indent=2,
                        )
                    )
                    model_script.extend(
                        f"change_output_activation({outputs[i]}, sigmoid)" for i in range(1, len(outputs), 2)
                    )
                    model_script.append(f'nms_postprocess("{nms_config}", meta_arch=yolov8, engine=cpu)')
                    model_script.append("allocator_param(width_splitter_defuse=disabled)")
            runner.load_model_script("\n".join(model_script))

            runner.optimize(calibration_images)
            (output_dir / f"{self.file.stem}.hef").write_bytes(runner.compile())
            YAML.save(
                output_dir / "metadata.yaml",
                {
                    **self.metadata,
                    "hailo_arch": self.args.name,
                    "nms": task == "detect" and not raw_detect,
                    "semantic_baked": task == "semantic" and head.bake_argmax,
                    # Depth's learned log-affine calibration is applied on the host, so it must ride in metadata.
                    **({"cal_a": float(head.cal_a), "cal_b": float(head.cal_b)} if task == "depth" else {}),
                },
            )
            return str(output_dir)
        finally:
            f_onnx.unlink(missing_ok=True)

    def _add_tflite_metadata(self, file):
        """Add metadata to *.tflite models per https://ai.google.dev/edge/litert/models/metadata."""
        import zipfile

        with zipfile.ZipFile(file, "a", zipfile.ZIP_DEFLATED) as zf:
            zf.writestr("metadata.json", json.dumps(self.metadata, indent=2))

    def _transform_fn(self, data_item) -> np.ndarray:
        """Quantization preprocessing transform for INT8 calibration (Axelera, OpenVINO, ONNX, QNN).

        Calibration datasets smaller than the export batch yield undersized batches that static-batch graphs reject, so
        images are tiled up to exactly the export batch.
        """
        data_item: torch.Tensor = data_item["img"] if isinstance(data_item, dict) else data_item
        assert data_item.dtype == torch.uint8, "Input image must be uint8 for the quantization preprocessing"
        im = data_item.numpy().astype(np.float32) / 255.0  # uint8 to float32 and 0 - 255 to 0.0 - 1.0
        im = im[None] if im.ndim == 3 else im
        if not self.args.dynamic and len(im) < self.args.batch:  # tile up to the static batch dimension
            im = np.tile(im, (-(-self.args.batch // len(im)), 1, 1, 1))[: self.args.batch]
        return im

    def add_callback(self, event: str, callback):
        """Append the given callback to the specified event."""
        self.callbacks[event].append(callback)

    def run_callbacks(self, event: str):
        """Execute all callbacks for a given event."""
        for callback in self.callbacks.get(event, []):
            callback(self)


class ExportWrapper(torch.nn.Module):
    """Base for export-time model wrappers: stores the wrapped model and forwards attribute lookups.

    Subclasses adapt a fused YOLO model's inference I/O for a specific deployment contract (layout, output
    reduction) while the exporter keeps interacting with the wrapper as if it were the model itself.
    """

    def __init__(self, model):
        """Wrap a fused YOLO `model` prepared for export."""
        super().__init__()
        # Stored under a private name so attribute forwarding resolves `wrapper.model` to the wrapped model's own
        # `model` (its nn.Sequential), keeping exporter code like `self.model.model[-1]` working unchanged.
        self._model = model
        self.task = model.task

    def __getattr__(self, name):
        """Forward attribute lookups (model, names, stride, yaml, args, ...) to the wrapped model."""
        try:
            return super().__getattr__(name)
        except AttributeError:
            return getattr(self._model, name)


class QNNModel(ExportWrapper):
    """Wraps a YOLO model with channel-last inference input for Qualcomm QNN export.

    Traced by the standard ONNX export (`export_qnn` swaps it in with a channel-last dummy input). The graph takes `[N,
    H, W, C]` images - the Hexagon HTP's native layout and what camera pipelines produce - so ONNX Runtime's layout
    transformer folds the wrapper's Transpose into the NPU partition during context generation, and neither the NPU
    (boundary transpose) nor the consuming app (CPU-side permute) pays a per-inference layout cost.

    Attributes:
        task (str): The wrapped model's task, forwarded for the ONNX export plumbing.
    """

    def forward(self, x):
        """Run inference on channel-last `[N, H, W, C]` input normalized to [0, 1]."""
        return self._model(x.permute(0, 3, 1, 2))  # the wrapped model is NCHW; the transpose folds into the NPU graph


class ClassMapModel(ExportWrapper):
    """Reduces semantic-segmentation logits to a compact integer class map for export.

    Applied to QNN, Core ML and Ascend semantic exports, where the argmax runs on the NPU: deployment consumers want
    per-pixel class indices, and shipping float logits instead forces a dequantize + argmax over large tensors (~8M
    values at the standard 640px mobile input) on the consumer's CPU every frame - measured as both slow and highly
    variable on mobile NPUs. The argmax cannot live in the model's own forward because it is non-differentiable
    (training needs logits), so it is attached here at export time, mirroring how `NMSModel` adds suppression only for
    export.

    Attributes:
        task (str): The wrapped model's task ("semantic").
        dtype (torch.dtype): Class-index dtype; uint8 unless the model has more than 256 classes.
    """

    def __init__(self, model):
        """Wrap a fused semantic `model` so export emits class indices instead of logits."""
        super().__init__(model)
        # uint8 quarters the NPU->CPU output transfer vs int32 and Core ML promotes it to int32 in-spec;
        # int32 only when more than 256 classes make uint8 indices ambiguous.
        self.dtype = torch.uint8 if len(model.names) <= 256 else torch.int32

    def forward(self, x):
        """Run the wrapped model and return a `[N, H, W]` integer class map instead of float logits."""
        y = self._model(x)
        y = y[0] if isinstance(y, (list, tuple)) else y
        # Single-channel (binary) models threshold the logit, matching predict/val semantics for nc == 1
        return (y.argmax(1) if y.shape[1] > 1 else y[:, 0].gt(0)).to(self.dtype)


class NMSModel(torch.nn.Module):
    """Model wrapper with embedded NMS for Detect, Segment, Pose and OBB."""

    def __init__(self, model, args):
        """Initialize the NMSModel.

        Args:
            model (torch.nn.Module): The model to wrap with NMS postprocessing.
            args (SimpleNamespace): The export arguments.
        """
        super().__init__()
        self.model = model
        self.args = args
        self.obb = model.task == "obb"
        self.is_tf = self.args.format == "saved_model"

    def forward(self, x):
        """Perform inference with NMS post-processing. Supports Detect, Segment, OBB and Pose.

        Args:
            x (torch.Tensor): The preprocessed tensor with shape (B, C, H, W).

        Returns:
            (torch.Tensor | tuple): Tensor of shape (B, max_det, 4 + 2 + extra_shape) where B is the batch size, or a
                tuple of (detections, proto) for segmentation models.
        """
        from torchvision.ops import nms

        preds = self.model(x)
        pred = preds[0] if isinstance(preds, tuple) else preds
        bs = pred.shape[0]
        pred = pred.transpose(-1, -2)  # shape(1,84,6300) to shape(1,6300,84)
        extra_shape = pred.shape[-1] - (4 + len(self.model.names))  # extras from Segment, OBB, Pose
        if self.args.dynamic and self.args.batch > 1:  # batch size needs to always be same due to loop unroll
            pad = pred.new_zeros((self.args.batch - torch._shape_as_tensor(pred)[0]).clamp(min=0), *pred.shape[1:])
            pred = torch.cat((pred, pad))
        if self.args.dynamic and self.args.format == "onnx" and self.obb:
            pred = torch.cat((pred, pred.new_zeros(pred.shape[0], self.args.max_det * 5, pred.shape[2])), dim=1)
        boxes, scores, extras = pred.split([4, len(self.model.names), extra_shape], dim=2)
        scores, classes = scores.max(dim=-1)
        # (N, max_det, 4 coords + 1 class score + 1 class label + extra_shape).
        out = pred.new_zeros(pred.shape[0], self.args.max_det, boxes.shape[-1] + 2 + extra_shape)
        for i in range(bs):
            box, cls, score, extra = boxes[i], classes[i], scores[i], extras[i]
            mask = score > self.args.conf
            if self.is_tf or (self.args.format == "onnx" and self.obb):
                # TFLite GatherND error if mask is empty
                score *= mask
                # Explicit length otherwise reshape error, hardcoded to `self.args.max_det * 5`
                mask = score.topk(min(self.args.max_det * 5, score.shape[0])).indices
            box, score, cls, extra = box[mask], score[mask], cls[mask], extra[mask]
            nmsbox = box.clone()
            # `8` is the minimum value experimented to get correct NMS results for obb
            multiplier = 8 if self.obb else 1 / max(len(self.model.names), 1)
            # Normalize boxes for NMS since large values for class offset causes issue with int8 quantization
            nmsbox = multiplier * (nmsbox / torch._shape_as_tensor(x)[2:].max().to(nmsbox.dtype))
            if not self.args.agnostic_nms:  # class-wise NMS
                end = 2 if self.obb else 4
                # fully explicit expansion otherwise reshape error
                cls_offset = cls.view(cls.shape[0], 1).expand(cls.shape[0], end)
                offbox = nmsbox[:, :end] + cls_offset * multiplier
                nmsbox = torch.cat((offbox, nmsbox[:, end:]), dim=-1)
            nms_fn = (
                partial(
                    TorchNMS.fast_nms,
                    use_triu=not (
                        self.is_tf
                        or (self.args.opset or 14) < 14
                        or (self.args.format == "openvino" and self.args.quantize == 8)  # OpenVINO INT8 error with triu
                    ),
                    iou_func=batch_probiou,
                    exit_early=False,
                )
                if self.obb
                else nms
            )
            keep = nms_fn(
                torch.cat([nmsbox, extra], dim=-1) if self.obb else nmsbox,
                score,
                self.args.iou,
            )[: self.args.max_det]
            dets = torch.cat(
                [box[keep], score[keep].view(-1, 1), cls[keep].view(-1, 1).to(out.dtype), extra[keep]], dim=-1
            )
            # Zero-pad to max_det size to avoid reshape error. Padded on a flattened 1D view (rather than
            # dim 0 of the 2D tensor) since CoreML's MIL conversion only supports dynamic padding on rank-1 tensors.
            c = dets.shape[-1]
            pad = (0, (self.args.max_det - dets.shape[0]) * c)
            out[i] = torch.nn.functional.pad(dets.reshape(-1), pad).reshape(self.args.max_det, c)
        return (out[:bs], preds[1]) if self.model.task == "segment" else out[:bs]
