diff --git a/.github/scripts/create-export-env.py b/.github/scripts/create-export-env.py
index e9dd9e954..29d8f3847 100644
--- a/.github/scripts/create-export-env.py
+++ b/.github/scripts/create-export-env.py
@@ -23,6 +23,23 @@ def isolated_env_ids():
)
+def _public_pypi_command(command):
+ """Remove the optional NVIDIA index arguments from an install command."""
+ skip_next = False
+ filtered = []
+ for token in command:
+ if skip_next:
+ skip_next = False
+ continue
+ if token in {"--extra-index-url", "--index-strategy"}:
+ skip_next = True
+ continue
+ if token in {"https://pypi.ngc.nvidia.com", "unsafe-best-match"}:
+ continue
+ filtered.append(token)
+ return filtered
+
+
def build_env(env_id, root):
"""Build one export environment and run its smoke export commands."""
recipe = EXPORT_ENVS[env_id]
@@ -35,25 +52,31 @@ def build_env(env_id, root):
indexes = [token for flag, url in recipe["indexes"] for token in (flag, url)]
index_strategy = ["--index-strategy", "unsafe-best-match"] if indexes else []
torch = [f"torch{recipe['torch']}"] if recipe["torch"] else []
- subprocess.run(
- [
- "uv",
- "pip",
- "install",
- "--python",
- str(python),
- "-e",
- package,
- "pytest",
- *torch,
- *recipe["requirements"],
- *indexes,
- *index_strategy,
- "--torch-backend",
- "cpu",
- ],
- check=True,
- )
+ install_command = [
+ "uv",
+ "pip",
+ "install",
+ "--python",
+ str(python),
+ "-e",
+ package,
+ "pytest",
+ *torch,
+ *recipe["requirements"],
+ *indexes,
+ *index_strategy,
+ "--torch-backend",
+ "cpu",
+ ]
+ try:
+ subprocess.run(install_command, check=True)
+ except subprocess.CalledProcessError:
+ # NVIDIA's index is supplemental for TensorFlow/GraphSurgeon. If its DNS
+ # endpoint is unavailable, retry against public PyPI so CI remains usable.
+ if "https://pypi.ngc.nvidia.com" not in indexes:
+ raise
+ print("Supplemental NVIDIA PyPI index unavailable; retrying with public PyPI", flush=True)
+ subprocess.run(_public_pypi_command(install_command), check=True)
if recipe["env"]:
site_packages = next(venv.glob("lib/python*/site-packages"))
diff --git a/.gitignore b/.gitignore
index 544d5b12d..6f840c7e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,6 +54,21 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
+# Local dependency/bootstrap trees used by offline QA; never publish them.
+/.qa-deps/
+/.qa-cmake-*/
+/.qa-tmp*/
+/.qa-pytest-*/
+**/.qa-cmake-*/
+**/.qa-tmp*/
+**/.qa-pytest-*/
+/.qa-audit-note.txt
+*.qa-tmp/
+/work/pytest-*/
+/.qa-run-*/
+# Ad-hoc local verification directories
+/.qa-env-*/
+/qa_tmp_*/
mlruns/
# Translations
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/README.md b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/README.md
index b95fd6fb1..56fcc0af7 100644
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/README.md
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/README.md
@@ -1,369 +1,287 @@
-# YOLO-Master Cross-Platform Edge Inference Runtime
+# YOLO-Master Cross-Platform Edge Deployment
+
+This example provides a C++17 command-line runtime and a reproducible
+validation workflow for Issue #51. The runtime shares one preprocessing,
+decoding, NMS, timing, and output contract across ONNX Runtime, NCNN, MNN,
+and (when the SDK is installed) TensorRT.
+
+The repository contains code, schemas, and validation tools. It intentionally
+does not contain private checkpoints, datasets, or generated predictions. A
+metric or device claim is valid only when its model, image list, raw output,
+environment record, and SHA256 manifest are archived together.
+
+## Scope and current evidence
+
+The implementation includes:
+
+- CMake discovery for ONNX Runtime, NCNN, MNN, and optional TensorRT;
+- UTF-8-safe model and image paths on Windows;
+- model metadata and output-shape validation before decoding;
+- deterministic image-list handling with duplicate-stem and root-escape checks;
+- ONNX/NCNN/MNN export, parity, mAP, INT8, benchmark, and manifest tools;
+- Linux, Windows, macOS, ARM64 cross-build, and Jetson build entry points.
+
+The validated local smoke record is an Ubuntu 22.04 x86_64 YOLOv5s ONNX
+single-image run (six detections, approximately 970.873 ms end to end). This
+is an L1 functional check. It is not an EsMoE-N VisDrone/SKU-110K accuracy
+result, an INT8 result, or a native ARM64/Jetson measurement.
+
+## Directory layout
+
+| Path | Purpose |
+| --- | --- |
+| `cpp/` | C++17 runtime and CMake build |
+| `scripts/export_models.py` | Checked ONNX, NCNN, and MNN conversion |
+| `scripts/eval_map.py` | Ultralytics-backed mAP evaluation |
+| `scripts/eval_map_standalone.py` | Dependency-light mAP evaluation |
+| `scripts/prediction_diff.py` | Per-image and per-box parity diagnostics |
+| `scripts/quantize_int8.py` | Training-only INT8 calibration and manifest |
+| `scripts/evidence_manifest.py` | Evidence creation, validation, and hash verification |
+| `scripts/collect_environment.py` | Reproducible host and SDK record |
+| `scripts/package_linux.sh` | Relocatable Linux bundle assembly |
+| `environment.schema.json` | Host-record schema |
+| `evidence-manifest.schema.json` | Dataset, model, prediction, and report schema |
+| `TECHNICAL_REPORT.md` | Full protocol and rationale |
+| `TECHNICAL_SUMMARY_ZH.md` | Chinese technical summary |
+
+## Dependencies
+
+Required for the image CLI:
+
+- CMake 3.16 or newer;
+- a C++17 compiler;
+- OpenCV 4.5 or newer (`core`, `imgproc`, and `videoio` for video input);
+- ONNX Runtime for ONNX models.
+
+NCNN and MNN are optional at configuration time. A complete Issue #51 build
+should set `REQUIRE_ORT=ON` and at least one of `REQUIRE_NCNN=ON` or
+`REQUIRE_MNN=ON`; missing required SDKs then fail configuration instead of
+silently producing a partial binary.
+
+## Build
+
+From this directory, configure a minimal image-inference build:
-
-
-This project provides a cross-platform inference runtime for [YOLO-Master](https://github.com/Tencent/YOLO-Master) object-detection models using [ONNX Runtime](https://onnxruntime.ai/), [NCNN](https://github.com/Tencent/ncnn), [MNN](https://github.com/alibaba/mnn), [TensorRT](https://github.com/nvidia/tensorrt), and [Core ML](https://github.com/apple/coremltools) backends. The supported targets are Linux, Windows 10/11, Jetson, and macOS, with CPU, [NVIDIA CUDA](https://developer.nvidia.com/cuda-toolkit), and [Apple Metal Performance Shaders](https://developer.apple.com/documentation/metalperformanceshaders) execution where available. The runtime can infer the model format and read class names and input size from model metadata.
-This project provides a universal inference runtime for [YOLO-Master](https://github.com/Tencent/YOLO-Master) object-detection models, leveraging, [ONNX Runtime](https://onnxruntime.ai/), [NCNN](https://github.com/Tencent/ncnn), [MNN](https://github.com/alibaba/mnn), [TensorRT](https://github.com/nvidia/tensorrt), and [CoreML](https://github.com/apple/coremltools) backends. It runs on almost every platform: Linux, Windows (10/11), Jetson, MacOS, and **iOS (NEW!)**; supports CPU, [CUDA](https://developer.nvidia.com/cuda-toolkit), [MPS](https://developer.apple.com/documentation/metalperformanceshaders), and [ANE (on Apple devices)](https://machinelearning.apple.com). It's capable of auto-detecting the model format, class names, and input size -- designed for real-time, end-to-end edge deployment in some of the most challenging tasks (VisDrone, SKU-110K, AI-TOD-v2, etc.).
-
-
-
-
-
-
-
-
----
-
-## 📱 Update (27-08-2026): YOLO-Master for iPhone v1.1.0 Beta Build 1
-
-
-
-
-
-
-**🍾 Welcome to the 5th platform of YOLO-Master Edge. [Try it now.](https://testflight.apple.com/join/EVExpVHD)**
-
-Native iOS SwiftUI app for on-device YOLO-Master detection and segmentation, powered by Apple Core ML and the same YOLOMasterKit inference path as the macOS runtime. Everything runs locally on the Neural Engine, GPU, or CPU. Nothing you capture leaves the iPhone. Requires iPhone on iOS 17 or later. iPhone 13 and later models are recommended.
-
-### Features
-
-- **📹 Live** - Real-time camera detection with a dynamic performance visuliaztion (FPS tachometer, per-stage latency measures, and phone's thermal state), multi-cam with automatic lens switching and pinch zoom, tap-to-focus, torch, and a full-resolution shutter that renders the live overlay (boxes and segmentation masks) into the saved photo. Also supports live IoU/conf tuning without pausing the inference.
-
-- **📸 Photo** - Batch detection over images you pick from your library, up to 100 images at a time. Per-image and batch stats, segmentation masks, live conf/IoU tuning, and export of annotated images back to Photos.
-
-- **🎛️ Bench** - On-device benchmarking on all devices. A Cold Sweep measures every bundled model across compute units (Neural Engine (ANE), GPU, CPU) with expandable pre/inference/decode stage breakdowns, and a Sustained mode runs a thermal-throttle test with a live latency heatbeat sparkline and a colored state timeline. Pause/resume, a persistent run History vault (with per-run graphs) and CSV export.
-
-- **⚙️ Settings** - App info and an expandable About card (wihich explains the MoE architecture summary with Paper, Model / App Repo links), Licenses and Acknowledgements (from macOS build), a Privacy and Security summary, a CPU-inference opt-in for Live and Photo modes, erase-all benchmark history, and a Beta importer for your own trained Core ML models (.mlpackage / .mlmodelc / .mlmodel).
-
-### Performance
-
-All data are measured under Live mode on ANE with `YOLO-Master-v0.1-N` (COCO) for 3 min.
-
-| iPhone Model | SoC | End-to-end FPS | Thermal State after 1 min |
-|---|---|---|---|
-| iPhone 17 Pro Max | A19 Pro | 51 | 🟢 |
-| iPhone Air | A19 Pro | 36 | 🟠 |
-| iPhone 15 Pro Max | A17 Pro | 42 | 🟠 |
-
-### Install (Public Beta)
-
-The app is distributed through TestFlight. Install the TestFlight app on App Store, then open the invite link below to start testing it:
-
-https://testflight.apple.com/join/EVExpVHD
-
-### Build from source
-
-```zsh
-brew install xcodegen
-cd ios && xcodegen # this generates YOLOMasterIOS.xcodeproj
+```bash
+cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Release \
+ -DPORTABLE=ON \
+ -DONNXRUNTIME_ROOT=/opt/onnxruntime \
+ -DNCNN_ROOT=/opt/ncnn \
+ -DUSE_MNN=OFF \
+ -DREQUIRE_ORT=ON -DREQUIRE_NCNN=ON \
+ -DALLOW_NO_BACKENDS=OFF
+cmake --build cpp/build --parallel
```
-Drop your Core ML models into directory `ios/Models/`, set your signing team ID in `ios/project.yml`, open the project, and run. See `ios/README.md` for details.
-
-### Privacy & License (iOS App Update)
-
-The app has no internet connection. No data leaves the device, and we don't collect anything. See PRIVACY.md for detailed terms.
-
-The app is licensed under AGPL-3.0, consistent with YOLO-Master and Ultralytics; coremltools is BSD-3-Clause. It is released for research and personal experience only, and any direct commercial use of this app is prohibited.
-
----
-
-## 🚀 Update (12-08-2026): YOLO-Master Edge v1.1.0 is up!
-
-**One release, every platform: [macOS](https://github.com/skywalker-lt/yolo-master-edge/releases/download/v1.1.0/yolomaster-edge-mac-1.1.0.zip) / Windows [CPU](https://github.com/skywalker-lt/yolo-master-edge/releases/download/v1.1.0/yolomaster-edge-win-x64-1.1.0.zip) + [CUDA](https://github.com/skywalker-lt/yolo-master-edge/releases/download/v1.1.0/yolomaster-edge-win-x64-gpu_cuda12-1.1.0.zip) / Linux [CPU](https://github.com/skywalker-lt/yolo-master-edge/releases/download/v1.1.0/yolomaster-edge-linux-x64-1.1.0.tar.gz) + [CUDA](https://github.com/skywalker-lt/yolo-master-edge/releases/download/v1.1.0/yolomaster-edge-linux-x64-gpu_cuda12-1.1.0.tar.gz) / [Jetson Orin](https://github.com/skywalker-lt/yolo-master-edge/releases/download/v1.1.0/yolomaster-edge-jetson-orin-1.1.0.tar.gz).**
+For MNN as the secondary backend, use `-DMNN_ROOT=/opt/mnn
+-DUSE_MNN=ON -DREQUIRE_MNN=ON`. For a full video build, omit `-DPORTABLE=ON`.
+The configure summary lists every enabled backend and its resolved library.
-v1.1.0 brings the same feature set to all runners (macOS / Windows GUIs, Linux and Jetson TensorRT CLIs):
+The same source builds on Windows with MSVC or MinGW. Replace the SDK paths
+with Windows paths and pass `-DOpenCV_DIR=`. For ARM64 cross
+compilation, use `-DCMAKE_TOOLCHAIN_FILE=cpp/aarch64-toolchain.cmake` and
+record the sysroot and compiler in the environment manifest. A cross-compiled
+binary is not evidence of a native device run.
-- **🔪 Slicing (Sparse SAHI):** sliced inference for small objects on large images, a faithful port of upstream [YOLO-Master](https://github.com/Tencent/YOLO-Master)'s Sparse SAHI Mode plus a traditional dense-tiling variant, with adjustable tile size and per-run statistics.
-- **🍇 Cluster-Weighted NMS:** a new NMS mode that refines each box as the weighted average of its detection cluster; tunable sigma, live everywhere including the webcam.
-- **📤 Annotation export:** turn detections into training data as **YOLO TXT / COCO JSON / Pascal VOC XML** from images, folders, or videos (with frame sampling); segmentation models export real mask polygons. Rendered images and annotated videos export too.
-- **🔎 Zoom & pan on GUI:** cursor-anchored zoom up to 8x on images and paused video in both GUIs.
-- **📦 New: prebuilt Linux x86_64 bundles** (self-contained, glibc 2.35+, all three backends + ffmpeg video) and a **Jetson Orin bundle** with the TensorRT backend now supporting the full feature set.
+## Runtime profiles
-
+The generic defaults are intended for interactive inference. For an Issue #51
+accuracy run, always select an explicit profile so the resolved protocol is
+printed in the log and benchmark sidecar.
-
+### VisDrone
-
+`--profile visdrone` resolves to:
-**Full notes: [Release Page](https://github.com/skywalker-lt/yolo-master-edge/releases/tag/v1.1.0).**
+| Parameter | Value |
+| --- | ---: |
+| input | 640 x 640 |
+| confidence | 0.001 |
+| NMS IoU | 0.70 |
+| max detections | 300 |
+| class policy | 10-class VisDrone mapping |
+| multi-label | enabled |
+| resize | aspect-preserving letterbox, RGB/NCHW, float32/255 |
----
+### SKU-110K
-## ✨ Update (27-07-2026): YOLO-Master Windows 10/11 Runner (**GUI**) on ONNX/ncnn/MNN backends with **GPU Acceleration**
-**Download the [CPU runner](https://github.com/skywalker-lt/yolo-master-edge/releases/download/v1.0.0-windows/YOLO-Master-Windows-1.0.0.zip) / [CUDA runner](https://github.com/skywalker-lt/yolo-master-edge/releases/download/v1.0.0-windows/YOLO-Master-Windows-CUDA-1.0.0.zip).**
+`--profile sku110k` resolves to a 1280-square input, confidence 0.25, NMS IoU
+0.60, max detections 300, and the one-class SKU-110K mapping. Explicit command
+line values are recorded and take precedence over profile defaults.
-Now the Windows C++ edge runner has an improved backend and dedicated GUI. **YOLO-Master Windows Runner GUI** provides a C++ edge inference backend that bundles [ONNX](https://onnxruntime.ai/), [ncnn](https://github.com/Tencent/ncnn), and [MNN](https://github.com/alibaba/MNN) with **GPU acceleration**, together with a [Dear ImGui](https://github.com/ocornut/imgui) frontend. It includes the same default `YOLO-Master-v0.1-seg-N` segmentation model as the macOS runner.
+## Quick smoke run
-
-
-
-
-
-
-
-- **Three Backends in One App** ONNX, ncnn, and MNN all ship in single executable. Inference backends can be switched with a single click.
-- **GPU Acceleration for All Backends** up to **4x speedup** with CUDA-accelerated inference on consumer devices. (please refer to the inference speed comparison table in [Relases](https://github.com/skywalker-lt/yolo-master-edge/releases/tag/v1.0.0-windows))
-
-See the [Windows release notes](https://github.com/skywalker-lt/yolo-master-edge/releases/tag/v1.0.0-windows) for build and benchmark details.
-
----
-
-## 🍎 Update (17-07-2026): YOLO-Master Core ML Runner for macOS (GUI)
-
-**[Download](https://github.com/skywalker-lt/yolo-master-edge/releases/download/v1.0.0-macos/YOLO-Master-CoreML-Runner-1.0.0.zip) and try it now!**
-
-Alongside the Linux and Windows C++ runtimes, this project provides a native macOS runner, **YOLO-Master Core ML Runner**: a [SwiftUI](https://developer.apple.com/xcode/swiftui/) frontend over an Apple [Core ML](https://developer.apple.com/documentation/coreml) backend for on-device [YOLO-Master](https://github.com/Tencent/YOLO-Master) inference. It ships with a default `YOLO-Master-v0.1-seg-N` segmentation model for an immediate smoke test.
-
-
-
-
-- **Detection & Segmentation:** Runs both bounding-box detectors and instance-segmentation models, with anti-aliased mask overlays and a Masks / Boxes / Both toggle.
-- **Images, Video & Live Camera:** Infers single images, whole folders (batch), and MP4 video, plus a low-latency **live webcam** mode with a real-time FPS / ms-per-frame readout.
-- **⭐️ Real-Time Tuning:** Confidence, IoU, box style, labels, and letterbox/stretch preprocessing are adjustable at runtime. The forward pass is cached, so tuning redraws without rerunning inference.
-- **Signed & Notarized:** A **universal** (Apple Silicon + Intel) bundle, **Developer-ID signed and notarized by Apple**; it installs by double-clicking on macOS 14+.
-
-See the [macOS release notes](https://github.com/skywalker-lt/yolo-master-edge/releases/tag/v1.0.0-macos) for build and distribution details.
-
----
-
-## ✨ Benefits
+```bash
+export LD_LIBRARY_PATH=/opt/onnxruntime/lib:$LD_LIBRARY_PATH
+./cpp/build/yolomaster_edge \
+ --model artifacts/model.onnx \
+ --source test-data/image.jpg \
+ --backend onnx \
+ --profile visdrone \
+ --out runs/smoke \
+ --save-txt runs/smoke/labels \
+ --csv runs/smoke/timing.csv \
+ --benchmark-json runs/smoke/benchmark.json
+```
-- **Universal CLI Binary for Linux and Windows:** A single executable integrates **ONNX Runtime**, **NCNN** and **MNN** backends; the backend, class names, and input size are auto-detected from the model — no recompilation or any dataset YAML needed at runtime.
-- **Verified Accuracy:** Reproduces the PyTorch original to **< 0.5%** mAP50-95 across ONNX / NCNN / MNN, and **< 1.0%** under INT8 quantization, on 548 VisDrone validation images.
-- **Deployment-Friendly:** Cross-platform [CMake](https://cmake.org/) build producing **self-contained and relocatable bundles** for Linux x86_64 and Windows 10/11 — installable by unzip, no dependencies on the target.
-- **GUI:** Windows 10/11 and macOS provide GUI runners that integrate the CLI bundle functionality and support GPU acceleration.
-- **GPU Acceleration:** Supports FP32 CPU inference and [NVIDIA CUDA](https://developer.nvidia.com/cuda-toolkit) through the ONNX Runtime CUDA Execution Provider on Linux and Windows; Windows also supports NCNN [Vulkan](https://vulkan.org) and MNN [OpenCL](https://opencl.org), NVIDIA Jetson Orin supports a native TensorRT backend (JetPack 7), and macOS uses [MPS](https://developer.apple.com/documentation/metalperformanceshaders) through Core ML.
+Use `--no-save` for a load-only check. A missing model, invalid output shape,
+unsupported device, or incomplete NCNN pair returns a non-zero status with a
+diagnostic message.
-## ☕ Note
+## Issue #51 acceptance workflow
-The exported models embed their class names, input size, and stride as ONNX/NCNN/MNN metadata, so the runtime configures itself from the model file. Post-processing is tuned for the vertical domain — aspect-ratio-preserving letterbox, per-class **multi-label** NMS, and a low default confidence threshold appropriate for VisDrone's small, dense objects.
+The following commands are the canonical order. Run all backends against the
+same ordered image list and with the same class mapping and profile.
-## 📦 Exporting Models
+### 1. Freeze the validation set
-Pre-built models (trained on VisDrone) are attached to the [Releases](https://github.com/skywalker-lt/yolo-master-edge/releases) page. To export your own trained [YOLO-Master](https://github.com/Tencent/YOLO-Master) checkpoint, use the Ultralytics `export` mode.
+Convert native annotations to the repository's YOLO label format, then create
+an ordered UTF-8 list. The standard VisDrone validation split contains 548
+images; SKU-110K must report its actual split and count.
-### ONNX
+```bash
+find /data/VisDrone/images/val -type f \\
+ \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) \\
+ | sort > artifacts/visdrone-val.list
+```
-```python
-from ultralytics import YOLO
+Use the list file for every PyTorch and edge run. Directory enumeration is not
+an equivalent protocol. The manifest tool rejects missing files, unsupported
+suffixes, duplicate stems, and paths outside the declared image root.
-# Load a trained YOLO-Master-EsMoE-N checkpoint
-model = YOLO("EsMoE-N_VisDrone.pt")
+### 2. Export and check models
-# opset=12 for broad compatibility (ORT + NCNN + MNN)
-# simplify=True runs onnxsim; dynamic=False fixes the input shape for C++ deployment
-model.export(format="onnx", opset=12, simplify=True, dynamic=False, imgsz=640)
+```bash
+python scripts/export_models.py \
+ --weights runs/esmoe_n/weights/best.pt \
+ --out artifacts/export \
+ --imgsz 640 --opset 17
```
-### NCNN (via pnnx) and MNN
+The exporter runs ONNX checker and simplification by default and records the
+conversion command, output names, input shape, and SHA256. NCNN requires a
+matching `.param` and `.bin`; MNN conversion remains explicitly marked
+incomplete until its predictions pass the same parity gate.
-```bash
-# NCNN — Ultralytics uses pnnx under the hood
-yolo export model=EsMoE-N_VisDrone.pt format=ncnn imgsz=640
+### 3. Run the C++ backends
-# MNN — convert the exported ONNX with MNN's converter
-mnnconvert -f ONNX --modelFile esmoe_n_visdrone_sim.onnx --MNNModel esmoe_n_visdrone.mnn --bizCode edge
+```bash
+./cpp/build/yolomaster_edge --model artifacts/export/model.onnx \
+ --source artifacts/visdrone-val.list --profile visdrone \
+ --backend onnx --threads 4 --warmup 10 --runs 100 \
+ --out runs/onnx --save-txt runs/onnx/labels \
+ --csv runs/onnx/timing.csv --benchmark-json runs/onnx/benchmark.json
+
+./cpp/build/yolomaster_edge --model artifacts/export/model.mnn \
+ --source artifacts/visdrone-val.list --profile visdrone \
+ --backend mnn --threads 4 --warmup 10 --runs 100 \
+ --out runs/mnn --save-txt runs/mnn/labels \
+ --csv runs/mnn/timing.csv --benchmark-json runs/mnn/benchmark.json
```
-For more details on exporting, refer to the [Ultralytics Export documentation](https://docs.ultralytics.com/modes/export/).
-
-### Core ML
+Use `--backend ncnn` with the exported NCNN directory when that SDK is enabled.
+Keep the `--profile`, image list, thread count, warm-up count, and repeat count
+identical across backends.
-```zsh
-# detector or segmenter (task auto-detected)
-python coreml_export/export_coreml.py --weights model.pt --imgsz 640 --out model.mlpackage
+### 4. Evaluate accuracy
-# YOLO-Master default imgsz is 800 for AI-TOD models — pass --imgsz accordingly
-python coreml_export/export_coreml.py --weights yolo-master-v0.1-N_aitodv2.pt --imgsz 800 --out v0.1-N.mlpackage
+Use converted YOLO labels for the formal gate. Native VisDrone annotation rows
+are supported only for diagnostics because their ignored-region semantics are
+not identical to YOLO labels.
-# sunsmarterjie/yolov12 checkpoints (split qk+v area-attention) — stock ultralytics + the flag
-python coreml_export/export_coreml.py --weights yolov12x.pt --imgsz 640 --out yolov12x.mlpackage --yolov12-aattn
-
-# a LoRA fine-tune: merge the trained adapters first
-python coreml_export/export_coreml.py --weights base.pt --merge-lora-dir lora_adapter/ --imgsz 640 --out ft.mlpackage
+```bash
+python scripts/eval_map.py \
+ --preds runs/onnx/labels \
+ --images artifacts/visdrone-val.list \
+ --image-root /data/VisDrone/images/val \
+ --labels /data/VisDrone/labels/val \
+ --classes visdrone --label-format yolo \
+ --routing-semantics dense_fallback \
+ --imgsz 640 --conf 0.001 --iou 0.70 --max-det 300 --multi-label \
+ --min-images 500 \
+ --reference-json runs/pytorch/map.json \
+ --max-abs-delta-pp 0.5 \
+ --json runs/onnx/map.json
```
+The FP32 budget is 0.5 percentage points. Use 1.0 percentage point only for
+an explicitly identified INT8 run. The evaluator reports both absolute
+percentage-point and relative-percent deltas; do not mix the two units.
-## ⚙️ Dependencies
-
-Ensure you have the following dependencies installed (not required if you only want to smoke-test the pre-built bundles):
-
-### Linux & Windows
-
-| Dependency | Version | Notes |
-| :------------------------------------------------------------------ | :------------ | :------------------------------------------------------------------------------------------------------------- |
-| [ONNX Runtime](https://onnxruntime.ai/docs/install/) | >=1.18 | Download pre-built binaries or build from source. Use the GPU build for the CUDA Execution Provider. |
-| [NCNN](https://github.com/Tencent/ncnn/releases) | recent | Tencent NCNN; on Windows use the `windows-vs2022` prebuilt. |
-| [OpenCV](https://opencv.org/releases/) | >=4.5.0 | Used for image preprocessing (`core` + `imgproc`). |
-| C++ Compiler | C++17 Support | Needed for ``. ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/), MSVC 2022/2026) |
-| [CMake](https://cmake.org/download/) | >=3.16 | Cross-platform build system generator. |
-| [CUDA Toolkit](https://developer.nvidia.com/cuda-toolkit) (Optional)| 12.x | Required for GPU acceleration via ONNX Runtime's CUDA Execution Provider (match your ONNX Runtime GPU build). |
-| [MNN](https://github.com/alibaba/MNN) (Optional) | >=3.0 | Only for the third export format / benchmarking. |
-
-> **Note:** The CUDA Execution Provider is ABI-coupled to a CUDA major version — use the ONNX Runtime GPU build that matches your CUDA Toolkit (e.g. the CUDA-12 build with CUDA 12.x), or you'll hit loader errors.
-
-
-### macOS
-| | Version | Notes |
-| :------------------------------------------------------------------ | :------------ | :------------------------------------------------------------------------------------------------------------- |
-| macOS | Sonoma or newer (14.0+) | SwiftUI API floor (onKeyPress, zero-param onChange) |
-| [Xcode Command Line Tools](https://developer.apple.com/documentation/xcode/installing-the-command-line-tools/) | Xcode 15+ | Install with xcode-select --install. Provides swift, codesign, ditto. Full Xcode GUI not required for a build. |
-| [Swift toolchain](https://www.swift.org/swiftly/documentation/swiftly/install-toolchains/) | 5.9+ | swift-tools-version:5.9 in Package.swift; ships with the CLT/Xcode above. Build: swift build -c release --package-path mac. |
-| Apple SDK frameworks | macOS 14+ SDK (system) | SwiftUI, AppKit, AVFoundation, Core ML, Core Image, Core Video, ImageIO, QuartzCore, etc. |
-
-
-## 🛠️ Build Instructions
-
-### CLI (Linux & Windows)
-
-1. **Clone the Repository:**
-
- ```bash
- git clone https://github.com/skywalker-lt/yolo-master-edge.git
- cd yolo-master-edge/cpp
- ```
-
-2. **Create Build Directory:**
-
- ```bash
- mkdir build && cd build
- ```
-
-3. **Configure with CMake:**
- Point CMake at your extracted ONNX Runtime and NCNN SDKs via `ONNXRUNTIME_ROOT` and `NCNN_ROOT`.
-
- ```bash
- # Example for Linux (adjust paths as needed)
- cmake .. -DCMAKE_BUILD_TYPE=Release \
- -DONNXRUNTIME_ROOT=/path/to/onnxruntime \
- -DNCNN_ROOT=/path/to/ncnn
- ```
-
- ```bat
- :: Example for Windows, from the "x64 Native Tools Command Prompt"
- cmake .. -DCMAKE_BUILD_TYPE=Release ^
- -DOpenCV_DIR=C:/dev/opencv/build/x64/vc16/lib ^
- -DONNXRUNTIME_ROOT=C:/dev/onnxruntime-win-x64 ^
- -DNCNN_ROOT=C:/dev/ncnn-windows-vs2022/x64
- ```
-
- **CMake Options:**
- - `-DONNXRUNTIME_ROOT=`: **(Required)** Path to the extracted ONNX Runtime library.
- - `-DNCNN_ROOT=`: **(Required)** Path to the extracted NCNN library.
- - `-DCMAKE_BUILD_TYPE=Release`: (Optional) Build with optimizations.
- - `-DPORTABLE=ON`: (Optional, Linux) Slim build for a small self-contained bundle (image inference only).
- - If CMake struggles to find OpenCV, set `-DOpenCV_DIR=/path/to/opencv/build`.
-
-4. **Build the Project:**
- Use the build tool generated by CMake (Make, Ninja, or Visual Studio).
-
- ```bash
- # Using CMake's generic build command (works with Make, Ninja, MSBuild)
- cmake --build . --config Release
- ```
-
-5. **Locate Executable:**
- The compiled executable (`yolomaster_edge`, or `yolomaster_edge.exe` on Windows) is located in the `build` directory. On Windows the required backend and OpenCV DLLs are auto-copied next to it.
-
-### Windows GUI (with CUDA)
-
-1. **Clone the Repository**
- ```shell
- git clone https://github.com/skywalker-lt/yolo-master-edge.git
- cd yolo-master-edge/gui
- ```
-
-2. **Copy and Edit the Paths**
- ```bat
- copy sdk-paths.example.cmd sdk-paths.cmd
- ```
- Edit `sdk-paths.cmd` with your locations. It is gitignored. Leave a backend blank to skip it.
-
-4. **Build**
- ```bat
- build.cmd :: configure + build Release
- build.cmd run :: build, then launch
- build.cmd clean :: wipe build\ first
- ```
- Output: `gui\build\Release\yolomaster_gui.exe`
-
- > If PowerShell blocks `.ps1` scripts, use `.cmd` scripts as they are not subject to execution policy. `build.ps1` is equivalent and takes the same paths as parameters.
-
-### macOS
-
-1. **Clone the Repository:**
- ```bash
- git clone https://github.com/skywalker-lt/yolo-master-edge.git
- cd yolo-master-edge/cpp
- ```
-
-2. **Build the App and Run**
- ```zsh
- xcode-select --install
- swift run -c release --package-path mac YOLOMasterApp
- ```
-
-## 🚀 Usage (CLI)
-
-Run the executable, pointing it at a model and a source (image, directory, video, or `dataset.yaml`):
+### 5. Check per-image parity
```bash
-./yolomaster_edge --model ../../models/esmoe_n_visdrone_sim.onnx \
- --source path/to/image_or_dir \
- --conf 0.25 --out out
-```
-
-The backend is inferred from the model (`.onnx` → ONNX Runtime, an NCNN directory or `.param` → NCNN, `.mnn` → MNN, `.engine`/`.trt` → TensorRT), and class names and input size are read from model metadata. Common options:
-
-```text
---backend auto | onnx | ncnn | mnn | trt (default: auto-detect)
---device backend-dependent: cpu, cuda, vulkan, opencl, trt, coreml
---conf confidence threshold (default 0.25; lower for dense scenes)
---iou NMS IoU threshold (default 0.50)
---multi-label one detection per class >= conf per anchor (matches Ultralytics val mAP)
---save-txt dir to write predictions ('class conf x1 y1 x2 y2')
---out dir for annotated outputs --no-save / --quiet
+python scripts/prediction_diff.py \
+ --reference runs/pytorch/labels \
+ --candidate runs/onnx/labels \
+ --images artifacts/visdrone-val.list \
+ --image-root /data/VisDrone/images/val \
+ --iou 0.50 --min-iou 0.99 \
+ --json runs/onnx/prediction-diff.json \
+ --csv runs/onnx/prediction-diff.csv
```
-See `tests/run_tests.sh` for the 16-test robustness battery.
-
-## 🤖 Jetson Orin (Native TensorRT)
+The report includes matched and unmatched counts, coordinate and confidence
+deltas, and IoU percentiles. A parity claim must cite this report, not only a
+single visual example.
-A prebuilt aarch64 runner for **Jetson Orin** (Nano / NX / AGX) on **JetPack 7** is attached to the [Releases](https://github.com/skywalker-lt/yolo-master-edge/releases) page. It bundles OpenCV and uses JetPack's TensorRT + CUDA; the per-device FP16 engine is built once with the included script.
+### 6. Archive and verify evidence
```bash
-tar xzf yolomaster_edge-jetson-orin-jp7.tar.gz && cd yolomaster_edge-jetson-orin-jp7
-./build_engine.sh # builds the FP16 engine for this device (once, ~10-15 min)
-./yolomaster_edge --model models/esmoe_n_fp16.engine --source --classes visdrone --out out
+python scripts/evidence_manifest.py create \
+ --dataset visdrone --split val \
+ --images artifacts/visdrone-val.list \
+ --image-root /data/VisDrone/images/val \
+ --labels /data/VisDrone/labels/val \
+ --predictions runs/onnx/labels \
+ --checkpoint runs/esmoe_n/weights/best.pt \
+ --training-metadata artifacts/training-provenance.json \
+ --model onnx=artifacts/export/model.onnx \
+ --report map=runs/onnx/map.json \
+ --report timing=runs/onnx/timing.csv \
+ --command "./cpp/build/yolomaster_edge --profile visdrone ..." \
+ --acceptance --output runs/onnx/evidence.json
+
+python scripts/evidence_manifest.py verify runs/onnx/evidence.json \
+ --acceptance \
+ --images-root /data/VisDrone/images/val \
+ --labels-root /data/VisDrone/labels/val \
+ --predictions-root runs/onnx/labels \
+ --models-root artifacts/export \
+ --checkpoint-root runs/esmoe_n/weights \
+ --reports-root runs/onnx
```
-On an Orin Nano 4 GB the FP16 engine runs at **35.7 FPS** (27.8 ms) with **mAP50-95 0.2029 (−0.07 pp vs FP32)**. FP16 is the recommended target for this model because the area-attention path is not quantized, making INT8 both slower and less accurate here. To build from source, the [`jetson/`](jetson/) scripts drive the engine build and packaging; see [`jetson/README.md`](jetson/README.md) and [`jetson/DEPLOYMENT_LOG.md`](jetson/DEPLOYMENT_LOG.md).
+Publish the verified evidence directory as an immutable Release artifact or
+equivalent. Keep large models, images, and predictions out of the source PR.
-## 📊 Results
+## INT8 protocol
-Inference performed on full 548 VisDrone validation images against the PyTorch original (`mAP50-95 = 0.2036`), using identical settings (conf 0.001, NMS IoU 0.7, multi-label).
+Calibration images must come from the training split, contain at least 300
+files, and be disjoint from validation by content SHA256. Quantization produces
+an explicit calibration manifest and reports `acceptance_ready: false` until an
+INT8 prediction run has passed `eval_map.py` with the 1.0 percentage-point gate.
-| Inference Backend | Device | mAP50-95 | Δ vs PyTorch | End-to-end Latency | FPS |
-| :------------------------ | :------- | :------- | :----------- | :------ | :---- |
-| ONNX | CPU | 0.2034 | −0.02 pp | 40 ms | 25.0 |
-| ONNX (CUDA) | H200 SXM | 0.2033 | −0.03 pp | 7.8 ms | 128 |
-| ONNX (CUDA) | RTX 5070Ti Laptop | 0.2033 | −0.03 pp | 9.0 ms | 111 |
-| NCNN | CPU | 0.2034 | −0.02 pp | 80 ms | 12.5 |
-| NCNN (Vulkan) | RTX 5070Ti Laptop | 0.2034 | −0.02 pp | 20.2 ms | 49.5 |
-| MNN | CPU | 0.2034 | −0.02 pp | 74 ms | 13.5 |
-| MNN (OpenCL) | RTX 5070Ti Laptop | 0.2034 | −0.02 pp | 19.1 ms | 52.4 |
-| INT8 mixed ¹ | CPU | 0.1952 | −0.84 pp | 137 ms | 7.2 |
-| TensorRT FP16 | Jetson Orin Nano 4GB | 0.2029 | −0.07 pp | 27.8 ms | 35.7 |
-| Core ML | Apple M4 Max | N/A (no validator bundled) | N/A | 17.4 ms | 57.4 |
+## Performance protocol
-CPU latencies are x86 @ 4 threads on one host; mAP is identical across FP32 formats because they are of the same graph. The Jetson row is a native TensorRT FP16 engine, measured on-device.
+Use the same host, CPU affinity, input size, thread count, warm-up count, and
+repeat count for all backends. Archive preprocessing, inference, postprocessing,
+end-to-end mean/P50/P95/P99, FPS, compiler, runtime versions, and host data.
+Virtual-machine results must be labelled as VM measurements and must not be
+presented as Jetson or ARM64 performance.
-> ¹ INT8 is *slower* than FP32 on CPU — its throughput payoff needs INT8 tensor cores, not x86 CPUs. The CPU INT8 result is **accuracy evidence** (−0.84 percentage points, within budget); on the actual accelerator, FP16 also wins on Orin because the attention path is not quantized (see the TensorRT row and [`TECHNICAL_REPORT.md`](TECHNICAL_REPORT.md) Section 9).
+## Evidence levels
-See [`TECHNICAL_REPORT.md`](TECHNICAL_REPORT.md) for the full methodology, INT8 quantization deep-dive, and numerical parity analysis.
+| Level | Minimum evidence | Permitted conclusion |
+| --- | --- | --- |
+| L0 | Contract tests, parser checks, CMake diagnostics | Interfaces and static gates work |
+| L1 | Real model plus one image or a small subset | Load, preprocess, decode, and output work |
+| L2 | At least 500 fixed images, reference JSON, predictions, and hashes | Auditable FP32 accuracy |
+| L3 | L2 plus 300 disjoint calibration images and INT8 gate | Auditable INT8 accuracy |
+| L4 | Native builds on two platforms with raw logs and parity artifacts | Cross-platform deployment result |
+Do not upgrade an evidence level by filling a template with estimated values.
-## 🤝 Contributing
+## License
-Contributions are welcome! If you find any issues or have suggestions for improvements, please feel free to open an issue or submit a pull request on the [project repository](https://github.com/skywalker-lt/yolo-master-edge).
+The example follows the license of the surrounding YOLO-Master repository.
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/TECHNICAL_REPORT.md b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/TECHNICAL_REPORT.md
index 4b657f2c4..4860190f8 100644
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/TECHNICAL_REPORT.md
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/TECHNICAL_REPORT.md
@@ -1,296 +1,385 @@
-# A Technical Analysis of Cross-Platform Edge Deployment of YOLO-Master
-
-End-to-end deployment of **YOLO-Master** to the edge, spanning export formats (ONNX / NCNN / MNN / Core ML / TensorRT), mixed-precision INT8, runtime with GPU acceleration on all backends, two native GUI runners, cross-platform builds for Linux / Windows / Jetson / macOS, and accuracy-latency validation against the PyTorch original.
-
-
-
-
-
-
-- Source code: **[Edge Deployment Repo](https://github.com/skywalker-lt/yolo-master-edge)**
-- Download pre-built bundles & GUI Apps: **[Releases](https://github.com/skywalker-lt/yolo-master-edge/releases)**
-
-The evaluation focuses on **YOLO-Master-EsMoE-N** on VisDrone, as specified by Issue #51.
-
-## 🖥️ System Configuration
-
-This table lists the system configurations used for the deployment measurements in this report.
-
-| Host | Linux x86_64 (Docker) | Windows x86_64 | Nvidia Jetson | Mac |
-|:---:|:---:|:---:|:---:|:---:|
-| Device | Datacenter Server | ROG Strix SCAR 18 | Jetson Orin Nano Super (4GB) DevKit | MacBook Pro 16" (2024) |
-| CPU | 2x Intel Xeon 8568Y+ | Intel Core U9-275HX | Arm Cortex-A78AE | Apple **M4 Max CPU** |
-| System RAM | 2,048 GB | 32 GB | 4 GB (unified) | 48 GB (unified) |
-| GPU | **H200 SXM** | **RTX 5070 Ti Laptop** | Orin GPU, (GA10B, **`sm87`**) | Apple **M4 Max GPU** |
-| VRAM | 141 GB / GPU | 12 GB | 4 GB (unified) | 48 GB (unified) |
-| OS / platform | Ubuntu Server 22.04 LTS | Windows 11 x64 | JetPack 7 / Ubuntu 24.04 | macOS 26.3 (Tahoe) |
-
----
-## 📋 Table of Contents
-
-- [1. How and why the MoE internals dictate the deployment strategy](#-1-how-and-why-the-moe-internals-dictate-the-deployment-strategy)
-- [2. Exports](#-2-exports)
-- [3. INT8 quantization (the substantive part)](#-3-int8-quantization-the-substantive-part)
-- [4. The inference runtime](#%EF%B8%8F-4-the-inference-runtime)
-- [5. Accuracy validation](#-5-accuracy-validation)
-- [6. GPU acceleration across all three backends](#-6-gpu-acceleration-across-all-three-backends)
-- [7. Latency and throughput](#%EF%B8%8F-7-latency-and-throughput)
-- [8. Cross-platform builds and distribution](#-8-cross-platform-builds-and-distribution)
-- [9. Embedded GPU deployment: Jetson Orin](#-9-embedded-gpu-deployment-jetson-orin)
-- [10. GUI runners for Windows & macOS](#-10-gui-runners-for-windows-and-macos)
-- [11. Future work](#-11-future-work)
-
----
-
-## 🔎 1. How and why the MoE internals dictate the deployment strategy
-
-EsMoE-N is not a CNN YOLO like the older v8/v9 variants. Three structural properties drove the downstream deployment decisions:
-
-1. **Mixture-of-Experts `ES_MOE`:** During training and inference, the router sparsely selects experts. This path is export-hostile because it contains data-dependent control flow, but `ES_MOE.forward` switches to a **dense** unroll under `torch.onnx.is_in_onnx_export()`: a static loop over the full expert list using `Conv/Pool/Softmax/Mul/Add`, with no dynamic dispatch. The `--no-sparse-eval` dense path is the **numerically faithful** one. It avoids sparse-inference collapse and improves export determinism.
-2. **Area-attention `A2C2f`:** The backbone contains transformer-style attention blocks. These reshape activations to `[1, 1600, 192]` internally, which is where the static-shape assumptions of downstream quantizers and tracers break (as discussed later).
-3. **A stride-8/16/32 detection head** (P3/P4/P5) where the classification branch produces raw logits fed through a terminal sigmoid. At 640×640 that is `80^2 + 40^2 + 20^2 = 8400` anchors, matching the exported `[1, 14, 8400]` output (4 box + 10 VisDrone classes). This branch is the most quantization-sensitive component in the network, for the reason explained in Section 3.
-
-Accordingly, the model exports cleanly because the dense MoE path uses standard operators, while the attention blocks and detection head remain sensitive to INT8 quantization and third-party conversion.
-
-## 📦 2. Exports
-
-### 2.1 ONNX via onnxsim (opset 12)
-
-Exported to a fully **static** graph, input `images [1,3,640,640]`, output `output0 [1,14,8400]`, 628 nodes, IR 7, and simplified with onnxsim. Opset 12 was chosen carefully for compatibility. It loads unchanged under ORT 1.18 / 1.20 / 1.27 and converts cleanly to *both* NCNN and MNN.
-
-The export emitted a shape-inference warning on the attention transpose (`.../attn/Transpose_output_0 source:{1,1600,192} target:{}`, later resolved by ONNX Runtime's lenient merge). ORT resolves the shapes at runtime, but the warning is a **leading indicator**: tools that require fully static shape propagation may reject this graph (Section 3.5, MNN quantization, and Section 2.4, coremltools).
-
-Ultralytics metadata (class names, `imgsz`, `stride`, `task`) is also embedded into the ONNX `metadata_props`, which the C++ runtime reads to auto-configure itself.
-
-### 2.2 NCNN via pnnx
-
-Exported via **pnnx** (PyTorch/ONNX → pnnx IR → ncnn), instead of the legacy `onnx2ncnn`. pnnx preserves higher-level operator semantics and emits a clean graph. The param file was validated: magic `7767517`, **561 layers / 665 blobs**, input blob `in0`, sigmoid-terminated head. A `metadata.yaml` sidecar carries the same names/imgsz so the ncnn path is self-contained and self-describing like the ONNX one.
-
-### 2.3 MNN via mnnconvert
-
-Converted with `mnnconvert` (ONNX → MNN), which emits the same graph structure as ONNX/ncnn and permits direct tensor comparison against the source graph (Section 5.3).
-
-### 2.4 Core ML
-
-Core ML export (`coreml_export/export_coreml.py`) exports a **mlprogram** `.mlpackage` carrying the metadata used by the Apple device application (`names`, `imgsz`, `output`, `task`, plus `proto`/`nm` for seg models). Conversion can run on **Linux**; only prediction requires macOS, keeping export in the same environment as the other formats.
-
-Three failure modes had to be addressed; each reflects one of the structures described in Section 1:
-
-- **Dynamic shapes → `aten::Int`:** coremltools' Torch frontend cannot lower the data-dependent integer extraction produced by the attention reshapes. Constant-folding first resolves the shapes to literals: `jit.freeze` + `run_frozen_optimizations` run before conversion.
-- **MoE telemetry → `aten::copy_`:** The trace failed with "No matching select or slice" **not** in the expert computation, but in `ES_MOE`'s in-place auxiliary-loss bookkeeping used only during training. Routing was valid; the telemetry was the root cause.
-- **Area-attention static shapes:** An eager warmup pass bakes each layer's concrete spatial dimensions so the area reshapes fold to static shapes. For `sunsmarterjie/yolov12` checkpoints (for testing and comparison), `--yolov12-aattn` additionally swaps in the split qk+v attention variant those weights expect.
-
-The script also handles **segmentation** (detects the two-output signature and writes `task=segment` with `proto`/`nm`) and **LoRA fine-tunes** (`--merge-lora-dir` merges adapters before export, since a merged LoRA is a static graph whereas routed MoLoRA cannot be traced).
-
-**Validation:** The Core ML path currently has **no mAP number**. The macOS app does not bundle a metric harness, and the `eval_map.py` pipeline used for the other formats consumes `--save-txt` output from the C++ runtime, which the Swift app does not produce. A validator is planned for a future Core ML Runner update.
-
-## 🔢 3. INT8 quantization findings
-
-The requirement was ≤ 1.0% mAP gap under INT8 with ≥ 300 images for calibration. The naive pipeline *fails*.
-
-### 3.1 The collapse: full INT8 emits nothing
-
-Static per-channel INT8 over the whole graph produces a model that runs, returns the correct output tensor shape, contains **no NaNs**, **and detects nothing**, mAP=0.0000.
-
-Isolating the output tensor shows why: The box-regression channels are intact (`min 0, max 644, mean 210`, matching FP32); however the **classification channels are zero** (`max = 0.0000`, zero scores above 0.001). This indicates that the failure is concentrated in the class head.
-
-The mechanism is as follows: the class branch emits wide-dynamic-range *logits* consumed by a sigmoid. Per-tensor/per-channel MinMax calibration maps that range to 256 INT8 levels; the small positive logits corresponding to real detections fall *below one quantization step* and round to a value whose sigmoid is approximately 0. The non-linearity turns this quantization error into a complete signal cutoff. Box regression, by contrast, is a smooth linear readout with no saturating nonlinearity downstream and therefore tolerates INT8. This asymmetry of **robust regression but degraded classification** is the key diagnostic.
-
-### 3.2 Localizing the sensitivity
-
-Retaining the detection head (`/model.25/`, 85 nodes) in FP32 while quantizing the remaining nodes recovers the model to **mAP50-95=0.1924, ∆ −1.12 pp** versus PyTorch. The model is functional but remains outside the stated budget; the residual loss is concentrated in two quantization-sensitive structures:
-
-- **MoE router:** Expert mixing is a softmax over routing logits. INT8 trims the precision of routing weights.
-- **Area-attn:** Attention scores pass through a softmax whose output is sensitive to input scale; INT8 on QK path shifts the attention distribution.
-
-Both are the same failure class as the head: **a softmax/sigmoid amplifying a quantization perturbation.** This aligns with established LLM quantization strategies.
-
-### 3.3 The mixed-precision outcome
-
-The fix is node-level precision: keep the three softmax/sigmoid-bearing blocks, head (`/model.25/`), attention (`/attn/`), router (`routing`), **289 nodes** in FP32, INT8 everything else. The progression is diagnostic:
-
-| Configuration | mAP50-95 | Δ vs PyTorch (percentage points) |
-|---|---|---|
-| Full INT8 | 0.0000 | collapse |
-| head FP32 | 0.1924 | −1.12 pp |
-| head + attention + router FP32 | **0.1952** | **−0.84 pp ✅** |
-
-Final model: **10.9 → 5.4 MB (2.0×)**, with an mAP50-95 difference of **−0.84 percentage points**, within the 1.0-point requirement. The improvement follows from retaining the operators that violate the smooth, non-saturating assumption of PTQ in higher precision.
-
-### 3.4 Calibration engineering
-
-Three implementation details were decisive:
-
-- **Letterbox-matched calibration.** Calibrators default to a plain resize; the model is trained on **letterboxed** input. Calibrating on the wrong preprocessing biases every activation range. We pre-letterbox 300+ VisDrone *train* images (no val leakage) to 640×640 and calibrate on those, so the calibration distribution matches inference exactly.
-- **QOperator, and the opset floor.** Per-channel INT8 emits `DequantizeLinear` with an `axis` attribute, which is **only valid at opset ≥ 13**; the opset-12 export must be lifted (we upgrade to 17 in-line) or the quantized model is an invalid graph. QOperator (`QLinearConv`/`QLinearMatMul`) is chosen over QDQ for CPU execution.
-- **MinMax over Percentile.** Percentile/entropy calibration builds a histogram per activation tensor; on a graph with hundreds of attention/MoE intermediates and hundreds of images, that becomes computationally expensive without an observed accuracy benefit. The exclusions remove the outlier-sensitive layers, so MinMax on the remaining convolutions is faster and sufficient.
-
-### 3.5 Third-party INT8 toolchains and attention-shape handling
-
-MNN's offline quantizer (`mnnquant`) aborts immediately on this model -- `std::length_error: cannot create std::vector larger than max_size()` -- before any calibration runs. The cause is precisely the `[1,1600,192]` attention reshapes flagged in Section 2.1: the quantizer allocates buffers from statically-inferred tensor dimensions, and the dynamically-shaped attention intermediate reads back as a garbage size. MNN executes this graph fine at *inference* (it resolves shapes lazily); its *quantizer* assumes static shapes. This is a limitation of the tool's static-shape contract, not of the model, and it is not configurable. The ONNXRuntime quantizer, which tolerates dynamic intermediates, is the correct vehicle for this architecture.
-
-### 3.6 Where INT8 provides a benefit, and where it does not
-
-On x86 CPU, INT8 is *slower* than FP32, measured at **137 ms/frame vs 49 ms for FP32 on the same host, ~2.8× slower** (7.2 vs 19.5 FPS; the 40 ms in Section 7 is the canonical 4-thread benchmark on the reference host, so use the paired figures here for the ratio). The QDQ/QOperator kernels don't engage INT8 SIMD paths that beat the well-tuned FP32 convolutions, and the FP32 $\leftrightarrow$ INT8 boundaries around the excluded blocks add conversion overhead. This is expected, not a defect: INT8's throughput win is a property of **INT8 tensor-core hardware**, not of desktop CPUs.
-
-The natural next hypothesis was that tensor-core hardware would invert the result. **It does not for this model**: Section 9 shows that the calibrated TensorRT INT8 engine on Orin is both slower and less accurate than FP16. The mixed-precision assignment keeps the compute-dominant area-attention path out of INT8, so INT8 does not accelerate the dominant computation. More generally, **PTQ throughput is bounded by the fraction of computation that can be quantized**; for attention-heavy architectures, that fraction may be small. The ONNX INT8 result is therefore treated as **accuracy evidence** (−0.84 percentage points, within the stated budget), while FP16 GPU execution provides the measured throughput benefit (Section 6).
-
-## ⚙️ 4. The inference runtime
-
-### 4.1 Universal binary
-
-One executable (`yolomaster_edge`) with **four backends** (ONNXRuntime, NCNN, MNN, TensorRT) behind a common interface (`backend_factory.hpp`). Backend, class names, and input size are **auto-detected** from the model (`.onnx` → ORT, a directory or `.param` → ncnn, `.mnn` → MNN, `.engine` → TensorRT; metadata read from ONNX `metadata_props` or the ncnn `metadata.yaml` sidecar), so the same binary serves any exported YOLO-Master variant with no recompilation.
+# Technical Note: Auditable Edge Deployment for Issue #51
+
+## Abstract
+
+This note describes the implementation and the measurement protocol supplied
+for Issue #51, which concerns edge inference of vertically trained YOLO-Master
+models. The contribution is a backend-independent C++ runtime together with
+export, validation, quantization and evidence tooling. The design treats
+reproducibility as part of the deployment interface: a metric is considered a
+result only when the model, ordered image set, command line, software
+environment and raw predictions can be verified from a content-addressed
+manifest.
+
+The source tree contains no EsMoE-N checkpoint, VisDrone images or generated
+prediction directory. This document therefore specifies an executable method
+and the implementation boundaries; it does not claim a new mAP, FPS or ARM64
+measurement.
+
+## Contribution and verification status
+
+The table below is the short reviewer-facing record. It separates implementation
+evidence from measurements that require a user-supplied checkpoint and data.
+
+| Contribution | Reproducible artifact | Status in this checkout |
+| --- | --- | --- |
+| Unified C++17 inference path | ONNX Runtime, NCNN and MNN adapters with shared preprocessing, decoding and NMS | L0 contract-checked |
+| Export and conversion checks | ONNX checker/simplifier, NCNN pair/sidecar validation, MNN conversion diagnostics | L0 structural checks |
+| Accuracy and parity protocol | Ordered image manifest, mAP evaluator, percentage-point gate and per-image prediction diff | L0 tooling |
+| INT8 protocol | Training-only calibration selection (>=300 images), hash disjointness and protected nodes | L0 tooling |
+| Linux functional smoke | Ubuntu 22.04 x86_64, YOLOv5s ONNX, one image and six detections | L1 smoke evidence |
+| EsMoE-N acceptance result | VisDrone/SKU-110K full split, mAP, INT8 and a second native platform | Pending checkpoint/data/logs |
+
+The smoke run is a functional check of the runtime and is not an EsMoE-N
+accuracy claim. No result is promoted to an acceptance claim until its model,
+image set, predictions and raw logs are available for independent verification.
+
+## 1. Evaluation objective
+
+The target comparison is a single trained checkpoint evaluated through
+PyTorch and at least two deployment formats (ONNX Runtime plus NCNN or MNN).
+Every path must consume the same ordered validation images and the same
+post-processing parameters. The minimum acceptance record is:
+
+* a fixed validation list with at least 500 images (the common VisDrone split
+ contains 548 images);
+* a PyTorch/reference metric JSON and per-image predictions for each backend;
+* a model and image-list SHA256 digest;
+* an explicit EsMoE routing-semantic record shared by the reference and export;
+* a latency log with fixed thread count, warm-up, repeat count and host details.
+
+Optional INT8 evaluation adds a training-only calibration list of at least 300
+images. The calibration and validation sets are checked for content overlap,
+not merely for different filenames.
+
+These requirements make the evaluation record independent of a particular
+machine, exporter ordering or directory traversal. The same protocol is used
+for every backend and platform so that a reported difference has a traceable
+cause.
+
+### 1.1 Training provenance and image-manifest control
+
+The checkpoint is treated as an experimental input rather than as an
+interchangeable model file. A submission record should therefore include the
+base-model or repository revision, dataset release and split, class mapping,
+epoch count, optimizer and learning-rate schedule, random seed,
+deterministic-setting, training software versions and the SHA256 of the best
+checkpoint. The exact training command and the selected checkpoint should be
+archived alongside the exported graphs.
+
+The validation population is materialized before inference. For the standard
+VisDrone validation split this is an ordered list of 548 image paths; the list
+is reused unchanged by PyTorch, ONNX Runtime, NCNN and MNN. The manifest stores
+one record per image (relative path, byte count and SHA256), rejects duplicate
+stems and computes an ordered-list digest. A run with fewer than 500 images is
+diagnostic only. This distinction prevents a convenient subset or a stale
+prediction directory from being presented as a full-split result.
+
+Metric JSON records both the path-only `image_manifest_sha256` and the
+content-aware `image_list_sha256`. The latter hashes ordered
+`relative-path SHA256` rows and therefore matches the evidence manifest. The
+reference gate compares both fields, so replacing an image while retaining its
+filename invalidates the comparison. For a list stored outside the dataset
+tree, `--image-root` defines the normalization boundary; entries outside that
+boundary are rejected.
+
+The same control applies to INT8 calibration. Calibration images are selected
+from the training split, deterministically ordered and content-hash compared
+with the validation records. At least 300 images are required, and any hash
+overlap invalidates the calibration evidence even when filenames differ.
+
+## 2. Runtime architecture
+
+The C++ runner is organized into four layers:
+
+1. **Input and timing.** `main.cpp` resolves image, directory, video, dataset
+ YAML and newline-delimited image-list sources. Dataset `val` accepts a
+ scalar or YAML sequence; list inputs preserve their declared order, while
+ directory inputs are sorted deterministically. The runner records failures
+ and emits per-image timing rows.
+2. **Preprocessing.** `common.cpp` implements centered letterbox (padding
+ value 114), BGR-to-RGB conversion and NCHW `float32 / 255` packing. Stretch
+ mode is available only as an explicit diagnostic override.
+3. **Backend adapters.** `ort_backend.cpp`, `ncnn_backend.cpp` and
+ `mnn_backend.cpp` load a graph once and expose the same `Backend` interface.
+ Optional native TensorRT support is compiled only with a TensorRT 10.x SDK;
+ older TensorRT releases should use the ONNX Runtime TensorRT execution
+ provider path.
+4. **Shared decoding.** Raw tensors are normalized to the feature-major
+ `[features, anchors]` layout, decoded to original-image pixel coordinates,
+ filtered with class-aware NMS and capped at `max_det`. Segmentation
+ prototypes are carried separately so annotation export does not alter box
+ results.
+
+The backend factory infers a format from the model path, accepts case-insensitive
+suffixes, and reports an error for an ambiguous NCNN directory. Metadata is
+used for names and input size when available; explicit profiles take precedence
+for domain-critical class mappings.
+
+## 3. Canonical post-processing profiles
+
+The profile is part of the run contract and is printed in the console header.
+The VisDrone profile is:
+
+| Parameter | Value |
+| --- | ---: |
+| Input | 640 x 640 |
+| Confidence threshold | 0.001 |
+| NMS IoU threshold | 0.70 |
+| Maximum detections | 300 |
+| Small-object confidence floor | disabled (`--small-conf=-1`); area threshold 1024 px^2 |
+| Class policy | ten canonical VisDrone classes |
+| Decode | multi-label per anchor |
+| Resize | centered letterbox, pad 114 |
+
+The SKU-110K profile uses a 1280-square input, confidence 0.25, IoU 0.60 and
+the one-class mapping. Callers may override thresholds for deployment, but the
+resulting values are recorded and must not be compared with a run using a
+different protocol.
+
+The runner also exposes an optional area-adaptive confidence floor for dense
+small-object scenes. When `--small-conf` is non-negative, candidates whose
+decoded area in the original image is below `--small-area` use
+`min(conf, small-conf)` before class-aware NMS. This setting is intentionally
+disabled in the canonical profile; enable it only for a separately identified
+NMS sweep and record both values in the manifest. The implementation matches
+the thresholding rule in `scripts/mnn_val.py`.
+
+The evidence manifest and metric JSON carry both numeric values, including the
+disabled sentinel (`small_conf=-1`). A reference/candidate delta gate rejects
+reports that omit or change either field.
+
+### 3.1 EsMoE routing semantics
+
+EsMoE has two materially different inference paths. Eager PyTorch inference
+may dispatch only the top-k experts (`native_sparse`), whereas static export
+must evaluate and blend all experts when the exporter cannot lower the
+data-dependent dispatch (`dense_fallback`). These paths are not interchangeable
+baselines: a backend comparison is valid only when the reference and exported
+run declare the same `protocol.routing_semantics`. The export summary records
+the selected path and the number of layers whose routing flags were changed;
+the evidence manifest and both metric evaluators preserve that field for the
+strict delta gate. Models without an MoE block use `not_applicable`.
+
+### 3.2 Class-count safety
+
+Class metadata is a frequent source of silent parity failures. For an explicit
+vertical profile, the runner selects the canonical mapping even when
+`--classes auto` is supplied. If the loaded model also exposes names and its
+class count disagrees with that profile, startup fails with the expected and
+observed counts. A generic/default run continues to use model metadata and
+requires the evaluator's `--classes` choice to match the checkpoint.
+
+### 3.3 Tensor-shape safety
+
+ONNX outputs are accepted only when a floating-point rank-3 tensor has batch
+one, a plausible feature dimension (`4 + nc` or larger) and a positive anchor
+dimension. Both `[1, features, anchors]` and `[1, anchors, features]` are
+normalized. When several rank-3 tensors remain equally plausible after the
+feature/anchor checks, the ONNX and MNN adapters fail explicitly instead of
+depending on exporter ordering. Rank-4 outputs are treated as segmentation
+prototypes only after their dimensions are validated. MNN and NCNN apply the
+same rank, dimension, finite-value and layout checks before entering the shared
+decoder; MNN status-returning API calls are checked and an unavailable
+accelerator is retried with a CPU session. This turns a wrong export into an
+explicit diagnostic instead of a plausible-looking empty prediction file.
+The MNN adapter requires float32 public input/output tensors; quantized graphs
+remain eligible when quantization is internal and the graph boundary stays
+float32.
+
+## 4. Export and conversion
+
+`scripts/export_models.py` is a wrapper around the model's native exporter. It
+uses a static square input, runs ONNX checker/simplification by default and
+writes an export summary containing the checkpoint digest, requested formats,
+graph checks and NCNN pair status. `--no-simplify` is retained for diagnosis
+only and requires `--allow-unsimplified`.
+
+NCNN conversion may emit names other than `in0` and `out0`. The exporter writes
+the actual input/output/prototype names to both `.metadata.yaml` and
+the shared `metadata.yaml` (the latter retains legacy compatibility). The runtime validates
+each declared name against the parsed `.param` graph before inference. When no
+sidecar is present it resolves a unique graph endpoint, retains the historical
+`in0`/`out0`/`out1` fallback, and fails closed when multiple terminal tensors make
+the roles ambiguous. A prototype explicitly declared by metadata is mandatory;
+missing it is an error rather than a silent box-only result. For a directory input,
+exactly one matching `.param`/`.bin` pair is required unless the conventional
+`model.ncnn.*` pair is present.
+
+MNN conversion is intentionally not treated as acceptance evidence. The
+converter output must load in the MNN runtime, produce a finite detection
+tensor, and pass the same per-image metric gate before it is listed as a
+validated backend.
+
+## 5. Accuracy evaluation
+
+There are two evaluators because a deployment host may not have the full
+training environment:
+
+* `eval_map.py` delegates AP matching to Ultralytics and is the preferred
+ formal path when PyTorch is available;
+* `eval_map_standalone.py` implements the same ten IoU thresholds with only
+ NumPy and standard-library dependencies.
+
+Both parsers are strict about column count, finite values, class range and
+positive geometry. The formal path requires one prediction and one label file
+for every image outside `--smoke`, rejects duplicate stems and records the
+ordered image-list digest. Native VisDrone rows are accepted for diagnosis;
+formal runs should use the official `visdrone2yolo` conversion so ignored
+regions have defined semantics.
+
+The result JSON exposes two distinct units:
```text
---backend auto | onnx | ncnn | mnn | trt
---device backend-dependent (cpu, cuda, vulkan, opencl, coreml, trt)
+delta_mAP50-95_pp = (candidate - reference) * 100
+delta_mAP50-95_pct = (candidate - reference) / reference * 100
```
-Source can be an image, a directory, a video, or a `dataset.yaml`. Eighteen robustness tests (corrupt images, missing files, image-size mismatch, backend inference, and output-collision handling) pass on all platforms (`cpp/run_tests.sh`).
-
-### 4.2 Preprocessing
-
-Aspect-ratio-preserving **letterbox** (min-side scale, 114 padding) → RGB `/255` NCHW, matching training. The letterbox metadata (scale & pad) is threaded through decode so boxes map back to original-image pixel coordinates in float with no intermediate rounding.
-
-### 4.3 Decode, NMS, and the mAP-parity subtlety
-
-An early version of the C++ pipeline read **1.19 mAP points low** despite bit-accurate inference. We found the cause in the decode: ultralytics `val` uses **`multi_label=True`**, one detection per class scoring at or above the threshold per anchor, not a single argmax. Reproducing that (`--multi-label` mode) recovered the gap exactly (0.3375 → 0.3494 mAP50). NMS is **per-class** (`agnostic=False`), implemented with a class-offset trick (shift each box by `class_id × (2·max(image dimension) + 8192)` so cross-class boxes never suppress each other), and capped at 300 detections. Default `conf` is low, appropriate to VisDrone's small/dense objects; `--conf`/`--iou` are tunable per deployment.
-
-### 4.4 Instance Segmentation
-
-The runtime also decodes instance segmentation models. A segmenter emits a second output, the prototype tensor, alongside the detection tensor; the exported metadata carries `task=segment` plus `proto`/`nm` so the runtime dispatches on the model. Masks are reconstructed by combining the per-instance mask coefficients with the prototypes cropped to each box and thresholded; the GUI runners composite them with anti-aliasing to avoid serrated edges.
-
-The shipped default for both GUI runners is **`YOLO-Master-v0.1-seg-N`** (`task: segment`, imgsz 640, stride 32, **COCO-80** classes), exported to ONNX / MNN / ncnn. Note the domain change: the detection results throughout this report are VisDrone 10-class, while the bundled segmenter is COCO-80. **No segmentation mAP is reported** -- the metric harness in Section 5 is detection-only, and extending it to mask AP is future work (Section 11).
-
-### 4.5 Dependency cut for a portable bundle
-
-The first self-contained Linux bundle was **231 shared libraries, 129 MB** since Ubuntu's `libopencv_imgcodecs` links **GDAL**, which transitively pulls in PostgreSQL (`libpq`), MySQL, `libpoppler` (PDF), HDF5, and the GIS stack, and `libopencv_dnn` pulls protobuf. An object detector does not need a Postgres client. We removed both by replacing `cv::imread`/`imwrite` with **stb_image** and `cv::dnn::NMSBoxes`/`blobFromImage` with a hand-written NMS and a manual NCHW pack. That drops the OpenCV surface to **core + imgproc only**: 231 → 10 libraries, **129 → 35 MB**, at a cost of a **0.087%** detection-count difference (stb vs OpenCV JPEG decoders diverge by sub-LSB pixel values on a handful of borderline boxes), inside tolerance. On Linux the binary is `$ORIGIN`-rpath'd and verified to run with no `LD_LIBRARY_PATH`; on Windows the MSVC runtime is bundled so targets need no VC++ Redistributable.
-
-## 📊 5. Accuracy validation
-
-### 5.1 Methodology
-
-Every model (PyTorch, ONNX, NCNN, MNN, INT8, CUDA, Vulkan, OpenCL, TensorRT) is scored through a single path: predictions at **conf 0.001, NMS iou 0.7, multi-label, cap 300** (ultralytics `val` settings), fed to ultralytics' own `DetMetrics` + `box_iou` + `match_predictions` (`eval_map.py`). This common procedure makes the numbers comparable across formats and directly comparable to the ultralytics reference. ONNX/ncnn/MNN/GPU predictions are produced by the C++ runtime; the Jetson uses a dependency-free reimplementation of the same harness `eval_map_standalone.py`. Core ML is the one path outside this harness and will be included later (Section 2.4).
-
-### 5.2 Results (548 VisDrone val images)
-
-| Model | Device | mAP50-95 | Δ mAP50-95 vs PyTorch (percentage points) |
-|---|---|---|---|
-| **PyTorch (reference)** | -- | 0.2036 | -- |
-| ONNX | CPU | 0.2034 | **−0.02 pp** |
-| NCNN | CPU | 0.2034 | **−0.02 pp** |
-| MNN | CPU | 0.2034 | **−0.02 pp** |
-| ONNX (CUDA) | H200 SXM | 0.2033 | **−0.03 pp** |
-| ONNX (CUDA) | RTX 5070Ti Laptop | 0.2033 | **−0.03 pp** |
-| NCNN (Vulkan, FP16) | RTX 5070Ti Laptop | 0.2034 | **−0.02 pp** |
-| MNN (OpenCL, FP16) | RTX 5070Ti Laptop | 0.2034 | **−0.02 pp** |
-| TensorRT FP16 | Jetson Orin Nano 4GB | 0.2029 | **−0.07 pp** |
-| INT8 (mixed) | CPU | 0.1952 | **−0.84 pp** |
-
-All three FP32 CPU export formats land on **identical** mAP (0.2034), as expected for the same graph, at **−0.02 percentage points** from PyTorch, 25× inside the 0.5-point target. INT8 is **−0.84 percentage points**, inside the 1.0-point target. (The INT8 mAP50 drop is larger, −1.27 percentage points, reflecting slightly softer classification confidences at INT8; the budget is defined on mAP50-95, which passes.)
-
-The GPU rows extend the CPU measurements and expose a backend-dependent performance pattern (see Section 6).
-
-### 5.3 Numerical parity, isolating format from pipeline
-
-Because the FP32 formats share a graph, we verify fidelity directly rather than only through mAP. Feeding **identical letterboxed inputs** to MNN and the source ONNX across 100 val images yields **max|Δ| = 0.096, mean|Δ| = 9.7e-05** on the raw `[1,14,8400]` output. The max is a single box-coordinate least-significant bit (coordinates run to ~640; 0.096 px is nothing); the mean is negligible. Detection counts over the full set are effectively equal (ONNX 157,464 vs ncnn 157,465 at conf 0.001). This distinguishes *format equivalence* from *coincidentally similar mAP*.
-
-The same analysis identified a **false alarm** on the CUDA path: a raw `max|Δ| = 2.31` was traced to FP32 box-coordinate variance in a single anchor, while functional mAP remained identical. A scalar max-absolute-difference gate would therefore reject a functionally equivalent model; separating box and class channels provides the relevant diagnostic.
-
-## 🚀 6. GPU acceleration across all three backends
-
-Each backend has a different native accelerator, and the runtime maps a single **Device: CPU / GPU** switch onto all of them: **ONNX → CUDA**, **ncnn → Vulkan**, **MNN → OpenCL**, with ncnn and MNN running **FP16** on the GPU. Every backend falls back to CPU cleanly and surfaces the reason when a provider is unavailable, which matters in a GUI where the user cannot read a stderr log.
-
-Measured on the same 548 VisDrone images, one consumer laptop GPU (Win11):
-
-| Backend | CPU | GPU | Speedup |
-|---|---|---|---|
-| ONNX → CUDA | 40 ms | **9.0 ms** | 4.4× |
-| MNN → OpenCL (FP16) | 74 ms | **19.1 ms** | 3.9× |
-| NCNN → Vulkan (FP16) | 80 ms | **20.2 ms** | 4.0× |
-
-Three findings:
-
-**FP16 on the GPU is accuracy-neutral.** The ncnn-Vulkan and MNN-OpenCL FP16 paths both score **0.2034 mAP50-95, identical to their FP32 CPU counterparts**, and CUDA scores 0.2033. This contrasts with Section 3: half-precision is a *uniform* reduction in mantissa that the softmax/sigmoid structures tolerate, whereas INT8 is a *range mapping* that the same structures amplify. For this architecture, FP16 is therefore the preferred accelerator precision and INT8 offers no corresponding benefit.
-
-**The x86 backend ranking is preserved on GPU.** ORT retains an approximately 2× lead over MNN and ncnn on the GPU (9.0 versus 19.1/20.2 ms), similar to the CPU results (40 versus 74/80 ms). The ARM comparison remains untested because the Jetson path uses TensorRT (Section 9) rather than ncnn or MNN.
-
-**The model does not saturate a datacenter GPU.** An H200 SXM (7.8 ms) is only ~15% faster than an RTX 5070Ti Laptop (9.0 ms). At nano scale with a 640×640 input, per-launch overhead and memory traffic dominate, not FLOPs -- so for this model class a consumer GPU is the sensible deployment target and the datacenter part buys almost nothing.
-
-## ⏱️ 7. Latency and throughput
-
-Per-frame inference, VisDrone val:
-
-| Platform | Backend | Device | infer (ms) | FPS |
-|---|---|---|---|---|
-| Linux CPU (4-thread) | ONNX (ORT) | CPU | 40.0 | 25.0 |
-| Windows 11 CPU | ONNX (ORT) | CPU | 37.6 | 25.4 |
-| Linux CPU (4-thread) | MNN | CPU | 74.0 | 13.5 |
-| Linux CPU (4-thread) | NCNN | CPU | 80.0 | 12.5 |
-| Windows 11 CPU | NCNN | CPU | 80.1 | 12.2 |
-| Linux CPU (4-thread) | ONNX INT8 (mixed) | CPU | 137 | 7.2 |
-| Linux | ONNX (ORT) | **CUDA / H200 SXM** | 7.8 | **128** |
-| Windows | ONNX (ORT) | **CUDA / RTX 5070Ti Laptop** | 9.0 | **111** |
-| Windows | MNN | **OpenCL / RTX 5070Ti Laptop** | 19.1 | 52.4 |
-| Windows | NCNN | **Vulkan / RTX 5070Ti Laptop** | 20.2 | 49.5 |
-| Jetson Orin Nano 4GB | **TensorRT FP16** | Orin iGPU | 27.8 | 35.7 |
-| macOS (M4 Max) | **Core ML** | MPS / ANE | 17.4 | 57.4 |
-
-CPU latencies are measured on one x86 host with four threads. The ordering is consistent with the respective optimization targets: **ORT is approximately 2× faster than MNN and NCNN on x86**, while the latter runtimes prioritize mobile and ARM deployments; the ordering persists on the GPU (Section 6). **INT8 is the slowest row** for the reasons in Section 3.6. The Core ML row is latency-only (no mAP, Section 2.4); its 17.4 ms result places the M4 Max between the consumer GPU and CPU measurements at a lower power envelope.
-
-No single format fits every deployment target; the results are therefore reported for four deployment distributions below.
-
-## 🌐 8. Cross-platform builds and distribution
-
-A single CMake tree now targets **four platforms**:
-
-| Platform | Toolchain | Backends | Distribution |
-|---|---|---|---|
-| Linux x86_64 | GCC / CMake | ONNX (+CUDA EP), NCNN, MNN | 35 MB `$ORIGIN`-rpath'd tarball, 10 libs, verified isolated |
-| Windows 10/11 x64 | VS 2022 / 2026, MSVC 19.5x | ONNX (+CUDA), NCNN (+Vulkan), MNN (+OpenCL) | Self-contained zip, MSVC runtime bundled; lean and CUDA variants |
-| macOS 14+ | Swift 5.9+, Xcode CLT | Core ML | Universal (Apple Silicon + Intel) `.app`, Developer-ID signed and **notarized** |
-| Jetson Orin (JetPack 7) | aarch64 GCC / CMake | TensorRT (priority), ONNX, NCNN, MNN | aarch64 tarball, OpenCV bundled, TensorRT/CUDA from JetPack |
-
-The Windows port surfaced three concrete portability issues, each fixed in the build system rather than worked around: `Ort::Session` takes `const wchar_t*` on Windows (a platform `ORTCHAR_T` shim); the prebuilt OpenCV config doesn't recognize the VS 2026 toolset and reports an empty runtime (point `OpenCV_DIR` at the concrete `vc16/lib` config); and the exe needs the MSVC runtime on clean targets (bundled via `InstallRequiredSystemLibraries`). SDK locations live in a gitignored `sdk-paths.cmd` rather than in the build scripts, and `.cmd` entry points are provided alongside `.ps1` because PowerShell execution policy blocks the latter on default Windows installs.
-
-The Windows GUI ships in two flavours: a lean bundle that gets GPU inference through ncnn-Vulkan and MNN-OpenCL using only the graphics driver, and a considerably larger CUDA bundle that additionally carries the cuDNN and CUDA runtime libraries for the fastest ONNX path. Neither requires a CUDA toolkit installation on the target. The executable is **not code-signed** (that needs a paid certificate), so SmartScreen warns on first launch; the macOS app, by contrast, is signed and notarized and installs by double-click.
-
-## 🤖 9. Embedded GPU deployment: Jetson Orin
-
-The runtime was taken to a **Jetson Orin Nano 4 GB**. The same CMake produces a native aarch64 binary with a `trt_backend` that deserializes a prebuilt engine and runs it via `enqueueV3`, joining the other backends behind the same interface. The engine is built on-device with `trtexec` from the exported ONNX.
-
-**Result.** The FP16 engine runs at **27.8 ms/frame GPU compute (35.7 FPS)**, and the on-device accuracy over the full 548 VisDrone val set is **mAP50-95 0.2029, ∆ −0.07 pp versus the PyTorch FP32 baseline** (0.2036), matching the x86 ONNX result to within 0.2 mAP points.
-
-**FP16, not INT8.** Section 3.6 reserved the INT8 *throughput* evaluation for this path, based on the expectation that tensor-core INT8 would invert the CPU result. For this model it does not. The mixed-precision assignment from Section 3 keeps the attention blocks, head, and router in higher precision, so INT8 leaves the compute-heavy area-attention on FP32/FP16 kernels; combined with TensorRT's lower INT8 accuracy relative to the ONNX Runtime path, the calibrated INT8 engine measures **0.3202 mAP50 at 21.7 FPS, slower and less accurate than FP16**. When attention dominates computation and is not quantized, **FP16 is the appropriate embedded target**. Taken together with Section 6, FP16 is the preferred target on every tested accelerator: it is accuracy-neutral on desktop GPUs and incurs minimal accuracy cost on Jetson Orin, whereas INT8 does not provide a benefit for this model.
-
-**Build notes.** Two toolchain specifics are worth recording. On sm87 with TRT 10.16.2 a pure-FP16 build fails at low builder-optimization levels (the timing model references an sm80 shader that has no sm87 base); `--builderOptimizationLevel=3` selects tactics by on-device profiling instead and builds cleanly. And an ONNXRuntime-quantized QDQ model must use symmetric activations and non-quantized bias to be accepted by TensorRT's parser `quantize_int8.py --symmetric`. The Nano 4 GB version also needs swap for the engine *build* (inference itself uses only ~20 MB).
-
-## 💻 10. GUI runners for Windows and macOS
-
-The CLI is efficient for benchmarking and batch work but is less accessible for interactive use. Two native GUI runners use the same pipeline.
-
-**macOS -- YOLO-Master CoreML Runner** (v1.0.0-macos). A SwiftUI frontend over Core ML: `YOLOMasterKit` carries the pipeline (preprocess, detect, annotate, image I/O) and the app layer adds camera and UI. Universal binary, Developer-ID signed & notarized, macOS 14+ (the SwiftUI API floor -- `onKeyPress`, zero-parameter `onChange`).
-
-
-
-**Windows -- YOLO-Master Windows Runner GUI** (v1.0.0-windows). Native Win32 + Direct3D 11 + Dear ImGui. The important architectural property is that it **compiles the CLI's own runtime sources** (`cpp/common.cpp`, the backend implementations, `stb_impl.cpp`) rather than reimplementing them, so letterbox, decode, per-class NMS, and the class palette are identical to the validated CLI path. Sharing the translation unit is what lets Section 5's mAP table apply to what the user actually runs. It carries all three backends in one executable, switchable at runtime, which makes backend comparison on the same image a single-click operation.
-
-
-
-The shared interaction model is based on two design decisions:
-
-- **Forward once, tune efficiently.** Confidence, IoU, box style, labels, and letterbox-vs-stretch all redraw from a cached forward pass. Inference never re-runs when a threshold moves, so the controls remain usable at interactive rates even where a frame costs 80 ms. Candidates are cached down to a 0.05 confidence floor so lowering the threshold remains immediate.
-- **Two-phase media handling.** Folders and videos are inferred once with a progress bar, then browsed or scrubbed from cache. A 30 fps clip therefore plays back at 30 fps regardless of model speed, because inference is off the playback path entirely. The webcam path instead infers on a background thread with drop-late-frames, trading completeness for latency.
-
-## 🔖 11. Future work
-- **Core ML accuracy validation.** The one backend without an mAP number (Section 2.4). A `--save-txt`-compatible dump from the macOS app would feed directly into `eval_map.py` and make the Core ML row comparable with the other entries in Section 5.2.
-- **Segmentation metrics.** The harness is detection-only; the bundled `v0.1-seg-N` is validated visually. Mask AP against the COCO protocol would close it.
-- **ARM-native backend comparison.** Section 6 leaves the original hypothesis that ncnn and MNN close the gap on ARM -- untested since the Jetson work went through TensorRT. Running the ncnn-Vulkan and MNN-OpenCL paths on the Orin would settle it on the hardware they were designed for.
-- **Non-NVIDIA GPUs.** The Vulkan and OpenCL paths should already work on AMD, Intel, and Ascend hardware.
-- **Production drone platform (DJI Manifold 3).** VisDrone is aerial/drone imagery, so the natural production target is an onboard drone computer. [DJI Manifold 3](https://enterprise.dji.com/manifold-3) is an **NVIDIA Orin NX-based** enterprise edge computer purpose-built for drones where the exact aarch64 + TensorRT path in Section 9 deploys onto it directly. Validating this pipeline on the Manifold 3 exercises **real-time on-drone inference in operational conditions** (aerial surveillance, infrastructure inspection, search-and-rescue), closing the loop from VisDrone training to production drone edge deployment.
-
----
-
-## Reproducibility
+Use `--max-abs-delta-pp` for an absolute percentage-point budget. The
+relative `--max-abs-delta-pct` option is retained for compatibility and cannot
+be combined with the absolute gate. The relative gate requires a positive
+reference metric; either gate requires the same image-list/protocol metadata.
+
+When a gate fails, `scripts/prediction_diff.py` matches same-class boxes by IoU
+and reports missing boxes, confidence differences and coordinate differences
+per image. It accepts the same BOM-tolerant, quoted ordered image list as the
+metric evaluators and can enforce an explicit `--image-root`; this prevents a
+diagnostic report from silently analyzing a different file set. The report
+separates preprocessing/decode errors from genuine model quality changes
+without rerunning inference.
+
+## 6. INT8 calibration
+
+The quantization helper is deliberately conservative. It:
+
+1. selects a deterministic, sorted calibration list;
+2. requires at least 300 images;
+3. applies the same letterbox/RGB/NCHW preprocessing contract;
+4. compares calibration image content hashes with the validation list;
+5. records the selected list digest, quantizer settings and exclusion patterns.
+
+The default exclusion patterns protect detection-head, attention and routing
+nodes when they exist. A pattern matching no graph node is an error, preventing
+a command-line typo from silently changing the precision recipe. The generated
+summary is always marked `acceptance_ready: false`; only a subsequent full
+prediction/evaluation run can establish an INT8 result.
+
+## 7. Benchmark methodology
+
+Latency comparisons are meaningful only when the following are held constant:
+
+* ordered image list and image decoding path;
+* input size, precision, confidence/IoU policy and maximum detections;
+* CPU/GPU device, runtime build and thread count;
+* warm-up count and timed repeat count.
+
+The runner reports preprocessing, inference, postprocessing and end-to-end
+times per image, followed by mean, P50, P95, P99 and FPS. With
+`--benchmark-json`, an optional sidecar records the resolved protocol, host
+platform, compiler, CPU model, logical CPU count and build date; `--csv` retains
+the per-image timing rows. The evidence manifest additionally records exact file
+hashes. Virtual-machine
+measurements are valid diagnostics but must be labelled as VM results and must
+not be generalized to ARM or Jetson hardware.
+
+Capture a separate host/toolchain snapshot before the run:
+
+```bash
+python3 scripts/collect_environment.py \
+ --repo-root . --backend onnx --execution-provider cpu \
+ --threads 4 --warmup 2 --runs 20 \
+ --output artifacts/environment.json
+```
-The C++ runtime, GUI frontends (`gui/`, `mac/`), Core ML exporter (`coreml_export/`), validation scripts (`quantize_int8.py`, `eval_map.py`, `eval_map_standalone.py`, `mnn_val.py`, `mnn_parity.py`, `package_linux.sh`), and Jetson tooling (`jetson/`, including `DEPLOYMENT_LOG.md`) are included in this repository. Exported models and prebuilt bundles for Linux, Windows, macOS, and Jetson Orin are available from the [release page](https://github.com/skywalker-lt/yolo-master-edge/releases).
+The collector is dependency-free and reports missing optional tools explicitly.
+Its output follows [`environment.schema.json`](environment.schema.json) and can
+be attached as `--report environment=artifacts/environment.json` when creating
+the evidence manifest.
+
+For publication, report the results in a table whose values point to the
+corresponding manifest and raw logs. The table is intentionally a schema, not
+a set of default numbers:
+
+| Backend | Export/checkpoint digest | Image count/list digest | mAP50-95 | Delta (pp) | End-to-end P50/P95/P99 | FPS | Host and runtime |
+| --- | --- | --- | ---: | ---: | --- | ---: | --- |
+| PyTorch reference | recorded in manifest | recorded in manifest | from metric JSON | -- | from timing log | from timing log | recorded in manifest |
+| ONNX Runtime | recorded in manifest | recorded in manifest | from metric JSON | from metric JSON | from timing log | from timing log | recorded in manifest |
+| NCNN or MNN | recorded in manifest | recorded in manifest | from metric JSON | from metric JSON | from timing log | from timing log | recorded in manifest |
+
+No cell is a result until the reviewer can recompute it from the stated model
+digest, image-list digest and per-image predictions. Compute latency and
+end-to-end latency are reported separately; a virtual-machine value is labelled
+as such.
+
+## 8. Evidence manifest
+
+`evidence_manifest.py` and `evidence-manifest.schema.json` define the release
+boundary. An acceptance manifest contains:
+
+* dataset name/split, ordered image records and a list digest;
+* required training provenance, including base-model revision, dataset
+ version, epoch/seed configuration and the exact training command;
+* the complete protocol and class profile;
+* checkpoint and every exported model, each with file hashes;
+* labels and predictions with matching counts;
+* calibration records and an explicit disjointness assertion for INT8;
+* environment, source revision, command line, content-hashed metric/benchmark
+ reports and gate values.
+
+`validate` checks the structure; `verify` additionally recomputes hashes under
+the supplied roots. The template intentionally leaves labels and predictions
+null and cannot pass the acceptance validator. This prevents a release note or
+an empty directory from being mistaken for a completed experiment.
+
+## 9. Build and portability controls
+
+The CMake target enables each backend only when its headers and library are
+found. `REQUIRE_ORT`, `REQUIRE_NCNN` and `REQUIRE_MNN` turn missing SDKs into
+configuration errors; `ALLOW_NO_BACKENDS` is reserved for dependency-light
+CLI diagnostics. On Windows, model and image paths are converted from UTF-8 to
+UTF-16 before opening; the JPEG writer uses the same wide-path handling. On
+Linux, the release script computes the recursive shared-library closure and
+sets an `$ORIGIN/lib` RPATH while leaving system glibc and accelerator drivers
+to the target host.
+
+The ARM64 toolchain file describes cross-compilation but does not claim that a
+cross-compiled binary has run on hardware. A native Jetson run must archive the
+binary, engine, device/software versions and raw log together with the same
+manifest.
+
+## 10. Reproducibility status of this checkout
+
+The repository-level contract tests cover profile resolution, parser failures,
+shape normalization, evidence-manifest gates and prediction diagnostics. A
+previous Ubuntu 22.04 smoke run loaded a YOLOv5s ONNX model on one image; that
+is a functional L1 check, not an EsMoE-N VisDrone accuracy result. The full
+Issue #51 acceptance record remains intentionally pending until a real
+EsMoE-N checkpoint, dataset split and target-platform logs are supplied.
+
+This boundary is important: a reproducible procedure is useful only when its
+limitations are stated as precisely as its successes.
+
+## 11. Publication record
+
+The public submission should contain a compact result table followed by links to
+the machine-readable evidence. Use one row per backend and keep the protocol
+identical across rows:
+
+| Backend | Model/checkpoint SHA256 | Image-list SHA256 | Images | mAP50-95 | Delta (pp) | P50/P95/P99 (ms) | FPS | Platform |
+| --- | --- | --- | ---: | ---: | ---: | --- | ---: | --- |
+| PyTorch reference | evidence manifest | evidence manifest | N | metric JSON | -- | timing CSV | timing CSV | environment JSON |
+| ONNX Runtime | evidence manifest | evidence manifest | N | metric JSON | metric JSON | timing CSV | timing CSV | environment JSON |
+| NCNN or MNN | evidence manifest | evidence manifest | N | metric JSON | metric JSON | timing CSV | timing CSV | environment JSON |
+
+The accompanying text should state the dataset release and split, checkpoint
+provenance, preprocessing and NMS parameters, runtime versions, thread policy,
+warm-up/repeat counts, and the exact commands used. A platform is listed as
+validated only when both compilation and an inference run were executed on that
+platform. Cross-compilation, CI compilation, or a virtual-machine smoke test
+must be labelled accordingly and must not be presented as native device
+evidence.
+
+For a short discussion post, use `TECHNICAL_SUMMARY_ZH.md` as the narrative and
+attach the evidence manifest, metric JSON, timing CSV/JSON, prediction archive,
+environment snapshot and model/export summaries. Keep all unavailable fields
+explicitly marked as pending until the corresponding files can be verified.
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/TECHNICAL_SUMMARY_ZH.md b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/TECHNICAL_SUMMARY_ZH.md
new file mode 100644
index 000000000..81c90f2a2
--- /dev/null
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/TECHNICAL_SUMMARY_ZH.md
@@ -0,0 +1,290 @@
+# Issue #51 边缘部署与一致性验证技术总结
+
+## 摘要
+
+本文档给出 YOLO-Master EsMoE-N 在 VisDrone 或 SKU-110K 垂直场景中的
+边缘部署验证协议、实现边界与证据要求。文档的目标是使一次实验能够被
+第三方在另一台 Linux 或 Windows 机器上复核,而不是仅报告一个不可追溯
+的 mAP 或 FPS 数字。
+
+本分支已经提供 ONNX Runtime、NCNN 与 MNN 的导出辅助程序、C++ 推理运行
+时、统一的 letterbox 与后处理、mAP 评估器、量化辅助程序以及 CMake 构建
+入口。当前仓库不包含 EsMoE-N 权重、VisDrone 数据集、548 张验证图像的
+逐图预测文件或目标机器的完整原始日志。因此,本分支目前的结论是
+“验证基础设施可复现”,不能据此声称已经完成 Issue #51 所要求的完整
+训练、跨后端精度验收或 ARM64 实机部署。
+
+## 面向评审的成果摘要
+
+本提交将 Issue #51 的部署要求落实为一套可执行、可审计的验收流程。成果
+与证据边界如下,便于在 Issue 评论或 Pull Request 描述中直接引用:
+
+| 成果项 | 可核对的实现或产物 | 当前证据等级 |
+|---|---|---|
+| 统一 C++ 推理入口 | ONNX Runtime、NCNN、MNN 后端适配器;同一预处理、解码和 NMS | L0(代码与契约测试) |
+| 导出与转换检查 | ONNX checker/简化、NCNN param/bin 配对和 sidecar 名称、MNN 转换提示 | L0(结构检查) |
+| 精度验收协议 | 固定有序图像清单、`eval_map.py`、百分点门禁、逐图 prediction diff | L0(工具可运行) |
+| INT8 校准协议 | 至少 300 张训练图像、内容 hash 去重、默认敏感节点保留 FP32 | L0(工具可运行) |
+| Linux 运行验证 | Ubuntu 22.04 x86_64 上 YOLOv5s ONNX 单图 smoke:6 个检测框 | L1(功能 smoke) |
+| EsMoE-N 完整结果 | VisDrone/SKU-110K、至少 500 张验证图、mAP 与双平台日志 | 待目标机器补充 |
+
+这里的 L0/L1/L2--L4 与第 3 节定义一致。表中“工具可运行”只表示接口、
+输入校验和错误门禁已经实现;只有同时归档模型、图像清单、预测、日志及
+SHA256 后,才可将相应项目升级为正式验收结果。
+
+该实现覆盖 ONNX Runtime、NCNN 与 MNN 的 C++17 推理路径,并将导出检查、
+预处理/后处理、精度评估、INT8 校准和证据归档纳入同一协议。现有契约测试
+与 Ubuntu 22.04 x86_64 的单图 smoke 证明了基础链路可运行;完整 EsMoE-N
+精度、量化和目标设备性能仍须在真实 checkpoint、数据集及硬件上按本协议
+复现后报告。
+
+## 1. 问题定义与验收边界
+
+Issue #51 的验收对象是垂直场景下的模型部署闭环,至少应覆盖以下内容:
+
+| 类别 | 最低要求 | 本分支状态 |
+|---|---|---|
+| 数据与模型 | VisDrone 或 SKU-110K 微调 checkpoint | 需由目标机器补充 |
+| 导出 | ONNX 加简化、opset/checker 校验;NCNN 或 MNN 转换 | 脚本已提供 |
+| 预处理 | 与训练一致的 RGB、NCHW、宽高比保持 letterbox | 运行时已提供 |
+| 后处理 | 类别感知 NMS、可复现阈值、最大检测数 | 运行时已提供 |
+| 精度 | 同一有序验证集不少于 500 张,报告 mAP50 与 mAP50-95 | 评估器已提供,实测数据待补 |
+| INT8(可选) | 训练图像校准不少于 300 张,验证集严格隔离 | 量化脚本已提供,精度门禁待补实测 |
+| 性能 | 固定线程、预热、重复次数,报告均值/P50/P95/P99/FPS | C++ CSV 入口已提供 |
+| 平台 | 至少两个目标平台完成 CMake 构建与运行 | 当前仅有 Linux x86_64 smoke 记录 |
+
+“已提供”表示代码路径和输入校验存在;“实测数据待补”表示没有把
+第三方报告中的数字写成当前仓库的实验结果。
+
+### 1.1 训练与数据来源记录
+
+实验结果的可复核性依赖训练来源,而不仅是导出文件。正式提交应记录
+基础模型或代码提交号、微调数据集版本及划分、类别映射、训练 epoch、
+随机种子、最佳 checkpoint 路径和 SHA256,以及导出所用的 Ultralytics、
+PyTorch、ONNX Runtime 和转换工具版本。若这些信息缺失,mAP 数字即使能够
+复现,也无法确认对应的是同一模型和同一数据划分。本分支没有替代这些
+信息的 checkpoint,必须由目标机器补齐。
+
+建议将训练元数据按下表归档,并与 `best.pt` 放在同一证据包中:
+
+| 项目 | 记录内容 |
+|---|---|
+| 代码与基础模型 | Git commit、模型配置或基础权重版本 |
+| 数据 | 数据集发布版本、train/val 划分、类别映射、图像数量 |
+| 优化设置 | epoch、batch size、优化器、学习率策略、随机种子、deterministic 设置 |
+| 软件环境 | Python、PyTorch、Ultralytics、导出器和运行时版本 |
+| 主机记录 | `scripts/collect_environment.py` 生成主机、编译器、SDK 和可选 GPU 信息;缺失项显式标记,不以默认值代替 |
+| 产物 | checkpoint 路径、文件大小、SHA256、导出命令 |
+
+表中每一项均应对应可读取的配置或原始日志;缺失项应标记为
+“未记录”,不得以默认值补齐。
+
+## 2. 可复核实验协议
+
+### 2.1 固定输入集合
+
+验证集必须先生成一个有序的图像清单,并在所有后端复用。建议使用
+VisDrone 的 548 张验证图像;若使用 SKU-110K,应记录实际数量和划分来源。
+清单不得包含重复 stem,否则 C++ 的 TXT 输出会发生覆盖风险。
+
+对于标准 VisDrone 验证划分,建议先生成并冻结 548 行的 UTF-8 路径清单,
+再将该清单的 SHA256 写入 manifest。所有后端、PyTorch 基线和性能测试均应
+按同一行序读取;目录遍历顺序不能作为隐含协议。若实际划分不是 548 张,
+应在结果中报告真实数量,并说明筛选规则。
+
+C++ runner 的 `--source` 可直接接收该清单(`.txt`)。清单中的相对路径以
+清单所在目录为基准解析,空行和 `#` 注释行忽略;缺失文件、不支持的后缀及
+重复文件 stem 会在推理前报错。这样可确保逐图 TXT 文件与 manifest 一一对应,
+而不是依赖不同文件系统的目录枚举顺序。
+
+```bash
+python scripts/evidence_manifest.py create \
+ --dataset visdrone --split val \
+ --images artifacts/visdrone-val.list \
+ --image-root /data/VisDrone/images/val \
+ --labels /data/VisDrone/labels/val \
+ --predictions artifacts/onnx_txt \
+ --checkpoint runs/esmoe_n/weights/best.pt \
+ --training-metadata artifacts/training-provenance.json \
+ --model onnx=artifacts/esmoe_n.onnx \
+ --model mnn=artifacts/esmoe_n.mnn \
+ --report metrics=artifacts/onnx_map.json \
+ --command "./yolomaster_edge --profile visdrone --source artifacts/visdrone-val.list" \
+ --acceptance \
+ --output artifacts/onnx-evidence.json
+```
+
+该命令对每张图记录相对路径、字节数和 SHA256,并记录清单摘要、运行平台、
+Git 提交号和后处理参数。模型和数据不应直接提交到 Git;建议将它们与
+预测、日志和清单一起放入 GitHub Release,并在技术总结中引用 Release 的
+SHA256。
+
+两个 mAP 评估器分别记录仅包含有序相对路径的
+`image_manifest_sha256`,以及对每一行 `相对路径 + 文件 SHA256` 计算的
+`image_list_sha256`。后者与 evidence manifest 使用同一算法。参考结果门禁
+同时比较这两个字段;即使文件名与顺序未变,只要图像内容被替换,比较也会
+失败,从而避免数据版本漂移被误判为跨后端一致。若清单位于数据集目录之外,
+必须使用 `--image-root` 指定归一化根目录;根目录之外的条目会被拒绝。
+
+将证据包从 Ubuntu 传回 Windows 或 Release 目录后,可用 `verify` 重新核对
+文件内容:
+
+```bash
+python scripts/evidence_manifest.py verify artifacts/onnx-evidence.json \
+ --acceptance \
+ --images-root /data/VisDrone/images/val \
+ --labels-root /data/VisDrone/labels/val \
+ --predictions-root artifacts/onnx_txt \
+ --models-root artifacts \
+ --checkpoint-root runs/esmoe_n/weights \
+ --calibration-root /data/VisDrone/images/train
+```
+
+### 2.2 预处理与后处理
+
+VisDrone 的默认 profile 为 `imgsz=640`、`conf=0.001`、`iou=0.70`、
+`max_det=300`、`multi_label=true`。输入采用居中 letterbox,填充值为
+114,颜色顺序为 RGB,张量布局为 NCHW,数值归一化为 `float32/255`。
+运行时的通用默认值仍保留给普通检测场景;验收命令应显式写出
+`--profile visdrone`,并在日志中保留最终生效的参数。
+
+EsMoE 还必须记录路由语义:静态 ONNX/NCNN 导出采用
+`dense_fallback`,PyTorch 基线须使用相同路径;只有在后端确实保留
+top-k dispatch 并完成独立核验时,才可标记为 `native_sparse`。不同语义的
+结果不得直接计算精度差值。
+
+小目标场景可在单独的 NMS sweep 中比较 `conf`、`small_conf`、面积阈值、
+IoU 与 `max_det`。sweep 的结果不能替代固定协议下的主验收结果;每一次
+比较都必须使用同一图像清单和同一类别映射。
+
+### 2.3 精度指标及单位
+
+评估器同时输出两种差异,避免把单位混用:
+
+* `delta_mAP50-95_pp = (candidate - reference) * 100`,单位为百分点;
+* `delta_mAP50-95_pct = (candidate - reference) / reference * 100`,单位为相对百分比。
+
+Issue #51 的验收命令推荐使用 `--max-abs-delta-pp 0.5`(FP32)和
+`--max-abs-delta-pp 1.0`(INT8)。旧参数 `--max-abs-delta-pct` 仅用于
+明确的相对百分比比较,不能与百分点门禁同时使用。
+
+正式评估必须使用 `visdrone2yolo` 转换后的 YOLO 标签。原生 VisDrone
+`x,y,w,h,score,category,...` 行仅用于诊断,不能作为忽略区域语义已经
+定义的正式验收标签。
+
+```bash
+python scripts/eval_map.py \
+ --preds artifacts/onnx_txt \
+ --images artifacts/visdrone-val.list \
+ --image-root /data/VisDrone/images/val \
+ --labels /data/VisDrone/labels/val \
+ --label-format yolo --classes visdrone \
+ --routing-semantics dense_fallback \
+ --imgsz 640 --conf 0.001 --iou 0.70 --max-det 300 --multi-label \
+ --min-images 500 \
+ --reference-json artifacts/pytorch_map.json \
+ --max-abs-delta-pp 0.5 \
+ --json artifacts/onnx_map.json
+```
+
+### 2.4 性能测量
+
+各后端必须在相同 CPU、输入尺寸、线程数和图像顺序下测量。建议先预加载
+图像,进行至少 10 次预热,然后进行 100 次以上重复,并将预处理、推理、
+后处理和端到端耗时分别记录。报告至少包含均值、P50、P95、P99 与 FPS,
+同时给出 CPU 型号、运行时版本、编译器、线程数和精度模式。运行器可通过
+`--benchmark-json` 输出包含协议、主机架构、编译器、CPU、逻辑 CPU 数、
+构建日期和汇总统计的机器可读 sidecar;`--csv` 保留逐图计时。虚拟机结果
+只能标记为虚拟机结果,不应外推为 ARM 或 Jetson 实机性能。
+
+主机与工具链信息可在运行前由无第三方依赖的
+`scripts/collect_environment.py` 采集,并按 `environment.schema.json` 校验。
+该记录应与 benchmark sidecar 和 evidence manifest 一并归档;它描述运行
+条件,不构成独立的精度或性能结论。
+
+发布时建议使用统一结果表,并为每个单元格保留证据引用:
+
+| 后端 | 模型/清单摘要 | 图像数 | mAP50-95 | 相对参考差值(百分点) | 端到端 P50/P95/P99 | FPS | 主机与运行时 |
+|---|---|---:|---:|---:|---|---:|---|
+| PyTorch 基线 | manifest / JSON | N(VisDrone 为标准 548;SKU-110K 按实际划分) | JSON | -- | CSV | CSV | manifest |
+| ONNX Runtime | manifest / JSON | N(VisDrone 为标准 548;SKU-110K 按实际划分) | JSON | JSON | CSV | CSV | manifest |
+| NCNN 或 MNN | manifest / JSON | N(VisDrone 为标准 548;SKU-110K 按实际划分) | JSON | JSON | CSV | CSV | manifest |
+
+该表是报告结构模板,不是本分支的实验结果;只有在模型摘要、图像清单
+摘要、逐图预测和原始日志均可核验时,才可填入数值。
+
+## 3. 证据分级
+
+为避免把 smoke test 误写成完整验收,采用以下分级:
+
+| 级别 | 内容 | 可支持的结论 |
+|---|---|---|
+| L0 | Python 契约测试、CMake 诊断构建、参数错误处理 | 接口与静态约束成立 |
+| L1 | 真实 ONNX/图片单图或小子集运行 | 模型加载、预处理、解码链路可运行 |
+| L2 | >=500 张固定验证集、参考 JSON、逐图预测和 SHA256 | 可审计的 FP32 精度验收 |
+| L3 | L2 加 >=300 张独立校准集、INT8 mAP 门禁 | 可审计的 INT8 验收 |
+| L4 | 两个平台原生构建、同图逐框对齐和原始 benchmark 日志 | 跨平台部署结论 |
+
+截至本分支的用户提供记录,针对 Issue #51 的契约测试已通过;Ubuntu 22.04
+x86_64 上完成过 YOLOv5s ONNX 单图 smoke(6 个检测框,端到端约
+970.873 ms)。该记录属于 L1,模型不是 EsMoE-N,不能替代 L2--L4。测试
+数量随门禁增补而变化,因此以提交时的测试日志为准,不在技术结论中固定计数。
+
+## 4. 导出与运行时注意事项
+
+1. EsMoE 的稀疏路由可能包含导出不友好的动态控制流。NCNN 导出脚本使用
+ dense routing,并在转换后执行实际的 param/bin 加载 smoke。
+2. NCNN 的输入和输出 blob 名称不是 ABI 固定值。导出器将实际名称同时写入
+ `.metadata.yaml` 与兼容用的 `metadata.yaml`;C++ 运行时优先
+ 使用同名 sidecar,并在加载前校验其名称确实存在于 `.param` 图中。没有
+ sidecar 时仅推断唯一端点;多输入或多终端图必须显式提供元数据,显式声明
+ 的 prototype 缺失则直接失败,旧的 `in0/out0/out1` 只保留为兼容回退。
+3. ONNX Runtime 输出在进入共享 decoder 前必须满足 FP32、rank-3、正维度
+ 的检测张量约束;同时兼容 `[1,features,anchors]` 和
+ `[1,anchors,features]` 两种常见布局。
+4. Windows 模型路径按 UTF-8 转换为 UTF-16,避免中文目录在 ORT 加载时
+ 被截断或替换。
+5. INT8 脚本只负责生成量化模型和校准清单;只有把生成的预测交给
+ `eval_map.py` 并通过百分点门禁,才能在报告中写“INT8 验收通过”。
+
+## 5. 发布结构与审核材料
+
+正式发布按“结论、方法、证据、限制”四部分组织。每一个数值都必须能够
+回溯到模型摘要、图像清单摘要、逐图预测和原始日志;缺少任一项时,该字段
+应标记为“待复现”,不能用估计值或其他运行的结果填充。
+
+| 评审字段 | 正式结果应提供 | 本分支当前状态 | 升级条件 |
+|---|---|---|---|
+| 训练与数据 | checkpoint、epoch、数据版本、划分和类别映射 | 工具支持记录,文件尚未提供 | 归档 provenance JSON 与 checkpoint SHA256 |
+| 导出产物 | ONNX checker/opset、NCNN/MNN 文件及哈希 | 导出与结构检查已实现 | 对真实 EsMoE-N 运行并保存 `export_summary.json` |
+| 精度一致性 | 至少 500 张固定验证图、PyTorch 基线、逐图预测 | 评估器和百分点门禁可运行 | 生成三后端 TXT、JSON 指标和清单哈希 |
+| 垂类后处理 | 输入尺寸、letterbox、NMS/小目标阈值 | `visdrone`/`sku110k` profile 已固定 | 用同一协议完成 NMS sweep,并归档配置 |
+| 性能 | 固定线程、预热/重复次数、P50/P95/P99/FPS | C++ CSV 与摘要字段已实现 | 在同一主机上完成至少两个后端的原始日志 |
+| 平台与发布 | 两个平台的构建/运行记录、Release 产物 | Ubuntu x86_64 L1 smoke;无第二平台实测 | 补 Windows/ARM64 原生日志及模型 Release |
+
+该矩阵是提交前的状态记录,不是对外的实验结果。只有“升级条件”全部满足
+后,才应在 Discussion 中填写具体 mAP、延迟或吞吐数字。
+
+本分支的改进重点是把这些要求固化为可执行门禁:
+
+* `scripts/evidence_manifest.py` 生成可审计的输入与产物清单;
+* `scripts/eval_map.py` 明确百分点/相对百分比的差异单位;
+* `--profile visdrone` 固定垂直场景的默认后处理;
+* CMake 的 `REQUIRE_ORT/REQUIRE_NCNN/REQUIRE_MNN` 防止发布出静默缺后端的
+ 部分二进制;
+* NCNN sidecar、ORT 形状检查和 Windows UTF-8 路径处理降低跨平台隐性差异。
+
+## 6. 完成正式验收前的待办项
+
+1. 获得并记录 EsMoE-N checkpoint、VisDrone 数据版本及 SHA256。
+2. 生成固定的 548 张验证清单和至少 300 张训练校准清单,确认两者按内容
+ hash 不相交。
+3. 在 PyTorch、ONNX 和 NCNN 或 MNN 上生成逐图 TXT、参考 JSON、raw tensor
+ parity 与完整日志。
+4. 在 Linux x86_64 与 Windows x64(或真实 ARM64 设备)完成 CMake 构建,
+ 保存编译命令、二进制信息和同图逐框对齐报告。
+5. 仅在百分点门禁通过后更新结果表,并将模型、预测和清单发布到 Release。
+
+完成以上步骤后,`TECHNICAL_SUMMARY_ZH.md` 可以作为 Discussion 技术总结
+的主体;在此之前应将结果表标记为“待目标机器复现”,并保留对应的原始日志
+与文件摘要。
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/CMakeLists.txt b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/CMakeLists.txt
index d0bc5ab68..a65c6d5cd 100644
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/CMakeLists.txt
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/CMakeLists.txt
@@ -24,11 +24,24 @@ endif()
set(ONNXRUNTIME_ROOT "${CMAKE_SOURCE_DIR}/../third_party/onnxruntime" CACHE PATH "ONNXRuntime root")
set(NCNN_ROOT "${CMAKE_SOURCE_DIR}/../third_party/ncnn" CACHE PATH "ncnn root")
set(MNN_ROOT "${CMAKE_SOURCE_DIR}/../third_party/mnn-src" CACHE PATH "MNN root (include/ + lib/ or build/)")
+# Native TensorRT is normally supplied by JetPack under /usr, but keeping the
+# roots explicit makes the same CMake invocation usable with an unpacked SDK
+# or an aarch64 sysroot. Empty values preserve the platform defaults below.
+set(TENSORRT_ROOT "" CACHE PATH "TensorRT SDK root (include/ and lib/)")
+set(CUDA_ROOT "" CACHE PATH "CUDA SDK root (include/ and lib64/)")
option(USE_ORT "Build ONNXRuntime backend" ON)
option(USE_NCNN "Build ncnn backend" ON)
option(USE_MNN "Build MNN backend" ON)
-option(USE_TRT "Build TensorRT backend (GPU .engine inference; Jetson/CUDA)" OFF)
+option(USE_TRT "Build native TensorRT 10 backend (GPU .engine; Jetson/CUDA)" OFF)
+# A dependency-light CLI build is useful for checking the argument/CSV
+# contract, so it remains allowed by default. Acceptance and release builds
+# should set the corresponding REQUIRE_* option(s) to make missing SDKs a
+# configuration error instead of silently producing a partial binary.
+option(ALLOW_NO_BACKENDS "Allow a diagnostic build without inference SDKs" ON)
+option(REQUIRE_ORT "Fail configuration when ONNX Runtime is unavailable" OFF)
+option(REQUIRE_NCNN "Fail configuration when NCNN is unavailable" OFF)
+option(REQUIRE_MNN "Fail configuration when MNN is unavailable" OFF)
set(SRC src/common.cpp src/slicing.cpp src/annotate_writers.cpp src/annotate_export.cpp
src/main.cpp src/stb_impl.cpp)
@@ -38,96 +51,215 @@ set(DEFS "")
if(NOT PORTABLE)
list(APPEND DEFS HAVE_VIDEOIO) # cv::VideoCapture (--source video) needs opencv_videoio
endif()
+if(WIN32)
+ # Keep Win32's min/max macros from colliding with std::min/std::max in the
+ # shared C++ runtime and third-party headers.
+ list(APPEND DEFS NOMINMAX WIN32_LEAN_AND_MEAN)
+endif()
set(RPATH "")
set(RUNTIME_DLLS "") # Windows: DLLs to copy next to the .exe
+set(ORT_ENABLED OFF)
+set(NCNN_ENABLED OFF)
+set(MNN_ENABLED OFF)
if(USE_ORT AND EXISTS "${ONNXRUNTIME_ROOT}/include/onnxruntime_cxx_api.h")
- list(APPEND SRC src/ort_backend.cpp)
- list(APPEND INCS ${ONNXRUNTIME_ROOT}/include)
+ set(ORT_LINK_LIB "")
if(WIN32)
- list(APPEND LIBS ${ONNXRUNTIME_ROOT}/lib/onnxruntime.lib)
- file(GLOB ORT_DLLS "${ONNXRUNTIME_ROOT}/lib/*.dll" "${ONNXRUNTIME_ROOT}/bin/*.dll")
- list(APPEND RUNTIME_DLLS ${ORT_DLLS})
+ if(EXISTS "${ONNXRUNTIME_ROOT}/lib/onnxruntime.lib")
+ set(ORT_LINK_LIB "${ONNXRUNTIME_ROOT}/lib/onnxruntime.lib")
+ file(GLOB ORT_DLLS "${ONNXRUNTIME_ROOT}/lib/*.dll" "${ONNXRUNTIME_ROOT}/bin/*.dll")
+ list(APPEND RUNTIME_DLLS ${ORT_DLLS})
+ endif()
elseif(APPLE)
- # macOS: ORT ships a .dylib with the CoreML EP built in; link the frameworks it pulls
+ # macOS: ORT ships a .dylib with the CoreML EP built in; link the frameworks it pulls.
find_library(ORT_DYLIB onnxruntime PATHS ${ONNXRUNTIME_ROOT}/lib NO_DEFAULT_PATH)
- list(APPEND LIBS ${ORT_DYLIB} "-framework Foundation" "-framework CoreML")
- list(APPEND RPATH ${ONNXRUNTIME_ROOT}/lib)
+ set(ORT_LINK_LIB "${ORT_DYLIB}")
else()
- list(APPEND LIBS ${ONNXRUNTIME_ROOT}/lib/libonnxruntime.so)
+ # Release archives commonly ship only a versioned SONAME
+ # (libonnxruntime.so.1.18.1), without an unversioned development symlink.
+ file(GLOB ORT_SOS "${ONNXRUNTIME_ROOT}/lib/libonnxruntime.so*")
+ list(SORT ORT_SOS)
+ list(LENGTH ORT_SOS ORT_SO_COUNT)
+ if(ORT_SO_COUNT GREATER 0)
+ list(GET ORT_SOS 0 ORT_LINK_LIB)
+ endif()
+ endif()
+ if(ORT_LINK_LIB)
+ list(APPEND SRC src/ort_backend.cpp)
+ list(APPEND INCS ${ONNXRUNTIME_ROOT}/include)
+ list(APPEND LIBS ${ORT_LINK_LIB})
+ if(APPLE)
+ list(APPEND LIBS "-framework Foundation" "-framework CoreML")
+ endif()
list(APPEND RPATH ${ONNXRUNTIME_ROOT}/lib)
+ list(APPEND DEFS USE_ORT)
+ set(ORT_ENABLED ON)
+ message(STATUS "ONNXRuntime backend: ON (${ONNXRUNTIME_ROOT})")
+ elseif(REQUIRE_ORT)
+ message(FATAL_ERROR "REQUIRE_ORT=ON but ONNXRuntime library was not found under ${ONNXRUNTIME_ROOT}")
+ else()
+ message(WARNING "ONNXRuntime backend: OFF (headers found but library is missing under ${ONNXRUNTIME_ROOT})")
endif()
- list(APPEND DEFS USE_ORT)
- message(STATUS "ONNXRuntime backend: ON (${ONNXRUNTIME_ROOT})")
else()
+ if(REQUIRE_ORT)
+ message(FATAL_ERROR "REQUIRE_ORT=ON but ONNXRuntime was not found at ${ONNXRUNTIME_ROOT}")
+ endif()
message(WARNING "ONNXRuntime backend: OFF (not found at ${ONNXRUNTIME_ROOT})")
endif()
if(USE_NCNN AND EXISTS "${NCNN_ROOT}/include/ncnn/net.h")
- list(APPEND SRC src/ncnn_backend.cpp)
- list(APPEND INCS ${NCNN_ROOT}/include ${NCNN_ROOT}/include/ncnn)
- find_library(NCNN_LIB ncnn PATHS ${NCNN_ROOT}/lib NO_DEFAULT_PATH)
- list(APPEND LIBS ${NCNN_LIB})
- find_package(OpenMP)
- if(OpenMP_CXX_FOUND)
- list(APPEND LIBS OpenMP::OpenMP_CXX)
- endif()
- if(WIN32)
- file(GLOB NCNN_DLLS "${NCNN_ROOT}/bin/*.dll" "${NCNN_ROOT}/lib/*.dll")
- list(APPEND RUNTIME_DLLS ${NCNN_DLLS})
+ # Accept both an installed SDK (lib/) and an uninstalled source build
+ # (build/src/). The latter is the layout produced by the standard NCNN
+ # CMake instructions and is useful on an offline Ubuntu host.
+ find_library(NCNN_LIB ncnn PATHS
+ ${NCNN_ROOT}/lib
+ ${NCNN_ROOT}/lib64
+ ${NCNN_ROOT}/build/src
+ ${NCNN_ROOT}/build
+ NO_DEFAULT_PATH)
+ if(NOT NCNN_LIB)
+ if(REQUIRE_NCNN)
+ message(FATAL_ERROR "REQUIRE_NCNN=ON but libncnn was not found under ${NCNN_ROOT}/lib")
+ endif()
+ message(WARNING "ncnn headers found but libncnn is missing; backend disabled")
else()
- list(APPEND RPATH ${NCNN_ROOT}/lib)
+ list(APPEND SRC src/ncnn_backend.cpp)
+ list(APPEND INCS ${NCNN_ROOT}/include ${NCNN_ROOT}/include/ncnn)
+ list(APPEND LIBS ${NCNN_LIB})
+ find_package(OpenMP)
+ if(OpenMP_CXX_FOUND)
+ list(APPEND LIBS OpenMP::OpenMP_CXX)
+ endif()
+ if(UNIX AND NOT APPLE)
+ # Static NCNN archives do not carry their POSIX runtime dependencies.
+ # These are harmless for shared builds and avoid platform-specific
+ # undefined references (pthread/dlopen/libm) at link time.
+ find_package(Threads REQUIRED)
+ list(APPEND LIBS Threads::Threads ${CMAKE_DL_LIBS} m)
+ endif()
+ if(WIN32)
+ file(GLOB NCNN_DLLS "${NCNN_ROOT}/bin/*.dll" "${NCNN_ROOT}/lib/*.dll")
+ list(APPEND RUNTIME_DLLS ${NCNN_DLLS})
+ else()
+ get_filename_component(NCNN_LIB_DIR "${NCNN_LIB}" DIRECTORY)
+ list(APPEND RPATH ${NCNN_LIB_DIR} ${NCNN_ROOT}/lib ${NCNN_ROOT}/lib64
+ ${NCNN_ROOT}/build/src ${NCNN_ROOT}/build)
+ endif()
+ list(APPEND DEFS USE_NCNN)
+ set(NCNN_ENABLED ON)
+ message(STATUS "ncnn backend: ON (${NCNN_ROOT})")
endif()
- list(APPEND DEFS USE_NCNN)
- message(STATUS "ncnn backend: ON (${NCNN_ROOT})")
else()
+ if(REQUIRE_NCNN)
+ message(FATAL_ERROR "REQUIRE_NCNN=ON but ncnn was not found at ${NCNN_ROOT}")
+ endif()
message(WARNING "ncnn backend: OFF (not found at ${NCNN_ROOT})")
endif()
if(USE_MNN AND EXISTS "${MNN_ROOT}/include/MNN/Interpreter.hpp")
- list(APPEND SRC src/mnn_backend.cpp)
- list(APPEND INCS ${MNN_ROOT}/include)
- find_library(MNN_LIB MNN PATHS ${MNN_ROOT}/lib ${MNN_ROOT}/build ${MNN_ROOT}/build/Release NO_DEFAULT_PATH)
+ find_library(MNN_LIB MNN PATHS
+ ${MNN_ROOT}/lib
+ ${MNN_ROOT}/lib64
+ ${MNN_ROOT}/build
+ ${MNN_ROOT}/build/Release
+ ${MNN_ROOT}/build/Debug
+ NO_DEFAULT_PATH)
if(NOT MNN_LIB)
- message(FATAL_ERROR "USE_MNN=ON but libMNN not found under ${MNN_ROOT}/{lib,build}")
- endif()
- list(APPEND LIBS ${MNN_LIB})
- if(UNIX AND NOT APPLE)
- # Static MNN builds do not propagate their POSIX runtime dependencies.
- # Keep shared-library builds unchanged while making libMNN.a linkable.
- find_package(Threads REQUIRED)
- list(APPEND LIBS Threads::Threads ${CMAKE_DL_LIBS})
- endif()
- if(WIN32)
- file(GLOB MNN_DLLS "${MNN_ROOT}/lib/*.dll" "${MNN_ROOT}/bin/*.dll" "${MNN_ROOT}/build/*.dll" "${MNN_ROOT}/build/Release/*.dll")
- list(APPEND RUNTIME_DLLS ${MNN_DLLS})
+ if(REQUIRE_MNN)
+ message(FATAL_ERROR "REQUIRE_MNN=ON but libMNN not found under ${MNN_ROOT}/{lib,build}")
+ endif()
+ message(WARNING "MNN headers found but libMNN is missing; backend disabled")
else()
- list(APPEND RPATH ${MNN_ROOT}/lib ${MNN_ROOT}/build)
+ list(APPEND SRC src/mnn_backend.cpp)
+ list(APPEND INCS ${MNN_ROOT}/include)
+ list(APPEND LIBS ${MNN_LIB})
+ if(UNIX AND NOT APPLE)
+ # Static MNN builds do not propagate their POSIX runtime dependencies.
+ # Keep shared-library builds unchanged while making libMNN.a linkable.
+ find_package(Threads REQUIRED)
+ list(APPEND LIBS Threads::Threads ${CMAKE_DL_LIBS} m)
+ endif()
+ if(WIN32)
+ file(GLOB MNN_DLLS "${MNN_ROOT}/lib/*.dll" "${MNN_ROOT}/bin/*.dll" "${MNN_ROOT}/build/*.dll" "${MNN_ROOT}/build/Release/*.dll")
+ list(APPEND RUNTIME_DLLS ${MNN_DLLS})
+ else()
+ get_filename_component(MNN_LIB_DIR "${MNN_LIB}" DIRECTORY)
+ list(APPEND RPATH ${MNN_LIB_DIR} ${MNN_ROOT}/lib ${MNN_ROOT}/lib64
+ ${MNN_ROOT}/build ${MNN_ROOT}/build/Release ${MNN_ROOT}/build/Debug)
+ endif()
+ list(APPEND DEFS USE_MNN)
+ set(MNN_ENABLED ON)
+ message(STATUS "MNN backend: ON (${MNN_ROOT})")
endif()
- list(APPEND DEFS USE_MNN)
- message(STATUS "MNN backend: ON (${MNN_ROOT})")
else()
+ if(REQUIRE_MNN)
+ message(FATAL_ERROR "REQUIRE_MNN=ON but MNN was not found at ${MNN_ROOT}")
+ endif()
message(WARNING "MNN backend: OFF (not found at ${MNN_ROOT})")
endif()
if(USE_TRT)
- # TensorRT + CUDA from JetPack (headers under /usr/include/aarch64-linux-gnu, CUDA under /usr/local/cuda)
- find_path(TRT_INC NvInfer.h PATHS /usr/include/aarch64-linux-gnu /usr/include ${TENSORRT_ROOT}/include)
- find_library(TRT_LIB nvinfer PATHS /usr/lib/aarch64-linux-gnu ${TENSORRT_ROOT}/lib)
- file(GLOB _CUDA_DIRS /usr/local/cuda /usr/local/cuda-*)
- find_path(CUDA_INC cuda_runtime_api.h PATHS ${_CUDA_DIRS} PATH_SUFFIXES include)
- find_library(CUDART_LIB cudart PATHS ${_CUDA_DIRS} PATH_SUFFIXES lib64 lib/aarch64-linux-gnu)
+ # The native runner uses TensorRT 10's named-I/O API. TensorRT 8 has a
+ # separate binding API and is intentionally routed through ORT + TRT-EP
+ # instead of being accepted here and failing later during compilation.
+ # HINTS are searched first so a caller-provided SDK/sysroot wins over a
+ # host installation. The explicit PATHS retain the JetPack defaults when
+ # both cache variables are left empty.
+ find_path(TRT_INC NvInfer.h
+ HINTS "${TENSORRT_ROOT}/include"
+ PATHS /usr/include/aarch64-linux-gnu /usr/include
+ PATH_SUFFIXES include)
+ find_library(TRT_LIB nvinfer
+ HINTS "${TENSORRT_ROOT}/lib" "${TENSORRT_ROOT}/lib64"
+ PATHS /usr/lib/aarch64-linux-gnu /usr/lib /usr/lib64
+ PATH_SUFFIXES lib lib64)
+ file(GLOB _CUDA_DIRS /usr/local/cuda /usr/local/cuda-* "${CUDA_ROOT}")
+ find_path(CUDA_INC cuda_runtime_api.h
+ HINTS "${CUDA_ROOT}/include"
+ PATHS ${_CUDA_DIRS}
+ PATH_SUFFIXES include)
+ find_library(CUDART_LIB cudart
+ HINTS "${CUDA_ROOT}/lib64" "${CUDA_ROOT}/lib"
+ PATHS ${_CUDA_DIRS}
+ PATH_SUFFIXES lib64 lib lib/aarch64-linux-gnu)
if(TRT_INC AND TRT_LIB AND CUDA_INC AND CUDART_LIB)
+ set(TRT_MAJOR "")
+ if(EXISTS "${TRT_INC}/NvInferVersion.h")
+ file(STRINGS "${TRT_INC}/NvInferVersion.h" _TRT_MAJOR_LINE
+ REGEX "^[ \t]*#define[ \t]+NV_TENSORRT_MAJOR[ \t]+[0-9]+")
+ if(_TRT_MAJOR_LINE)
+ string(REGEX REPLACE ".*NV_TENSORRT_MAJOR[ \t]+([0-9]+).*" "\\1"
+ TRT_MAJOR "${_TRT_MAJOR_LINE}")
+ endif()
+ endif()
+ if(NOT TRT_MAJOR)
+ message(FATAL_ERROR
+ "USE_TRT=ON requires NvInferVersion.h with NV_TENSORRT_MAJOR; "
+ "the native backend targets TensorRT 10.x. Set USE_TRT=OFF and use "
+ "the ORT TensorRT EP for older SDKs.")
+ endif()
+ if(TRT_MAJOR LESS 10)
+ message(FATAL_ERROR
+ "USE_TRT=ON found TensorRT ${TRT_MAJOR}; the native backend requires "
+ "TensorRT 10.x named-I/O APIs. Set USE_TRT=OFF and use ORT + TRT-EP "
+ "for TensorRT 8.x targets.")
+ endif()
list(APPEND SRC src/trt_backend.cpp)
list(APPEND INCS ${TRT_INC} ${CUDA_INC})
list(APPEND LIBS ${TRT_LIB} ${CUDART_LIB})
list(APPEND DEFS USE_TRT)
- message(STATUS "TensorRT backend: ON (${TRT_LIB} + ${CUDART_LIB})")
+ message(STATUS "TensorRT backend: ON (v${TRT_MAJOR}; ${TRT_LIB} + ${CUDART_LIB})")
else()
message(FATAL_ERROR "USE_TRT=ON but TensorRT/CUDA not found (TRT_INC=${TRT_INC} TRT_LIB=${TRT_LIB} CUDA_INC=${CUDA_INC} CUDART_LIB=${CUDART_LIB})")
endif()
endif()
+if(NOT ORT_ENABLED AND NOT NCNN_ENABLED AND NOT MNN_ENABLED AND NOT USE_TRT AND NOT ALLOW_NO_BACKENDS)
+ message(FATAL_ERROR
+ "No inference backend was configured. Install an SDK, enable one backend, or set "
+ "-DALLOW_NO_BACKENDS=ON for a diagnostic CLI build.")
+endif()
+
add_executable(yolomaster_edge ${SRC})
target_include_directories(yolomaster_edge PRIVATE ${INCS})
target_link_libraries(yolomaster_edge PRIVATE ${LIBS})
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/backend_factory.hpp b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/backend_factory.hpp
index 3cce5529d..1ed985de4 100644
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/backend_factory.hpp
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/backend_factory.hpp
@@ -1,10 +1,18 @@
// Shared backend construction - used by both the CLI (main.cpp) and the GUI so the
// two never drift. Header-only; guarded by the same USE_* defines as CMake sets.
#pragma once
+#ifdef _WIN32
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+#endif
+#include
+#include
#include
#include
#include
#include
+#include
#include "yolomaster.hpp"
#ifdef USE_ORT
@@ -22,18 +30,128 @@
namespace yolomaster {
+// Backend/model options are user-facing tokens. Normalize ASCII case once so
+// Windows files such as MODEL.ONNX and flags such as --backend ONNX behave the
+// same as their lowercase spellings.
+inline std::string lower_ascii(std::string value) {
+ std::transform(value.begin(), value.end(), value.begin(),
+ [](unsigned char c) { return static_cast(std::tolower(c)); });
+ return value;
+}
+
+inline bool path_extension_is(const std::filesystem::path& path, const char* extension) {
+ return lower_ascii(path.extension().string()) == extension;
+}
+
+inline bool path_stem_equal_ci(const std::filesystem::path& a, const std::filesystem::path& b) {
+ return lower_ascii(a.stem().string()) == lower_ascii(b.stem().string());
+}
+
+// Resolve an NCNN .param/.bin pair. Exports in the wild use names such as
+// model.ncnn.param, best.param, or arbitrary stems; relying on one literal name
+// made directory-model invocation fail even when a valid pair was present.
+inline bool resolve_ncnn_pair(const std::string& model, std::string& param,
+ std::string& bin, std::string& err) {
+ namespace fs = std::filesystem;
+ std::error_code ec;
+ const fs::path input(model);
+ std::vector params;
+ std::vector bins;
+ auto collect = [&](const fs::path& dir) {
+ for (fs::directory_iterator it(dir, fs::directory_options::skip_permission_denied, ec), end;
+ it != end; it.increment(ec)) {
+ if (ec) { ec.clear(); continue; }
+ const fs::path p = it->path();
+ if (!it->is_regular_file(ec)) { ec.clear(); continue; }
+ if (path_extension_is(p, ".param")) params.push_back(p);
+ else if (path_extension_is(p, ".bin")) bins.push_back(p);
+ }
+ };
+ const bool input_is_dir = fs::is_directory(input, ec);
+ ec.clear();
+ const bool input_is_file = fs::is_regular_file(input, ec);
+ ec.clear();
+ if (input_is_dir) {
+ collect(input);
+ } else if (input_is_file) {
+ const fs::path parent = input.parent_path().empty() ? fs::path(".") : input.parent_path();
+ collect(parent);
+ if (path_extension_is(input, ".param")) {
+ params.erase(std::remove_if(params.begin(), params.end(), [&](const fs::path& p) {
+ return lower_ascii(p.lexically_normal().string()) !=
+ lower_ascii(input.lexically_normal().string());
+ }), params.end());
+ } else if (path_extension_is(input, ".bin")) {
+ bins.erase(std::remove_if(bins.begin(), bins.end(), [&](const fs::path& p) {
+ return lower_ascii(p.lexically_normal().string()) !=
+ lower_ascii(input.lexically_normal().string());
+ }), bins.end());
+ }
+ else {
+ err = "NCNN model must be a directory, .param, or .bin file: " + model;
+ return false;
+ }
+ } else {
+ err = "NCNN model path does not exist: " + model;
+ return false;
+ }
+ auto by_name = [](const fs::path& a, const fs::path& b) {
+ const std::string al = lower_ascii(a.filename().string());
+ const std::string bl = lower_ascii(b.filename().string());
+ return al < bl;
+ };
+ std::sort(params.begin(), params.end(), by_name);
+ std::sort(bins.begin(), bins.end(), by_name);
+
+ // Keep only params with a same-stem binary (case-insensitive extension and
+ // stem matching). This prevents selecting a sidecar or an unrelated bin.
+ struct Pair { fs::path param; fs::path bin; };
+ std::vector pairs;
+ for (const fs::path& p : params) {
+ auto match = std::find_if(bins.begin(), bins.end(), [&](const fs::path& b) {
+ return path_stem_equal_ci(p, b);
+ });
+ if (match != bins.end()) pairs.push_back({p, *match});
+ }
+ if (pairs.empty()) {
+ err = "NCNN model directory has no matching .param/.bin pair: " + model;
+ return false;
+ }
+
+ // Prefer conventional stems. If more than one non-conventional pair is
+ // present, fail rather than silently benchmarking the wrong network.
+ auto score = [](const fs::path& p) {
+ const std::string stem = lower_ascii(p.stem().string());
+ if (stem == "model.ncnn") return 0;
+ if (stem == "model") return 1;
+ if (stem == "best") return 2;
+ return 3;
+ };
+ std::sort(pairs.begin(), pairs.end(), [&](const Pair& a, const Pair& b) {
+ const int sa = score(a.param), sb = score(b.param);
+ if (sa != sb) return sa < sb;
+ return by_name(a.param, b.param);
+ });
+ if (pairs.size() > 1 && score(pairs[0].param) == 3) {
+ err = "NCNN model directory contains multiple ambiguous .param/.bin pairs; "
+ "pass the .param file explicitly: " + model;
+ return false;
+ }
+ param = pairs.front().param.string();
+ bin = pairs.front().bin.string();
+ return true;
+}
+
// Infer backend name from a model path ("" if undecidable).
inline std::string detect_backend(const std::string& model) {
namespace fs = std::filesystem;
std::error_code ec;
- auto ends = [&](const char* s) {
- const std::string x = s; return model.size() >= x.size() &&
- model.compare(model.size() - x.size(), x.size(), x) == 0;
- };
- if (fs::is_directory(model, ec) || ends(".param")) return "ncnn";
- if (ends(".onnx")) return "onnx";
- if (ends(".mnn")) return "mnn";
- if (ends(".engine") || ends(".trt")) return "trt";
+ const fs::path path(model);
+ const std::string ext = lower_ascii(path.extension().string());
+ if (fs::is_directory(model, ec) || ext == ".param" || ext == ".bin") return "ncnn";
+ if (ext == ".onnx") return "onnx";
+ if (ext == ".mnn") return "mnn";
+ if (ext == ".engine" || ext == ".trt") return "trt";
return "";
}
@@ -41,41 +159,70 @@ inline std::string detect_backend(const std::string& model) {
// "auto" (detected from the path). `device` is mapped to the selected backend's
// native execution provider (for example, CUDA for ONNX Runtime or Vulkan for NCNN).
inline std::unique_ptr make_backend(std::string model, std::string backend,
- int threads, const std::string& device,
- std::string& resolved, std::string& err) {
- namespace fs = std::filesystem;
+ int threads, const std::string& device,
+ std::string& resolved, std::string& err) {
+ backend = lower_ascii(backend);
+ const std::string normalized_device = lower_ascii(device);
if (backend == "auto") {
backend = detect_backend(model);
if (backend.empty()) { err = "cannot infer backend from '" + model + "'"; return nullptr; }
}
+ if (backend != "onnx" && backend != "ncnn" && backend != "mnn" && backend != "trt") {
+ err = "unknown backend: " + backend;
+ return nullptr;
+ }
+ if (normalized_device != "" && normalized_device != "cpu" &&
+ normalized_device != "gpu" && normalized_device != "cuda" &&
+ normalized_device != "vulkan" && normalized_device != "opencl" &&
+ normalized_device != "trt" && normalized_device != "tensorrt" &&
+ normalized_device != "coreml") {
+ err = "unknown device: " + device;
+ return nullptr;
+ }
resolved = backend;
// GPU maps to each backend's native accelerator: onnx->CUDA EP, ncnn->Vulkan, mnn->OpenCL.
- const bool want_gpu = (device == "gpu" || device == "cuda" || device == "vulkan" || device == "opencl");
+ const bool want_gpu = (normalized_device == "gpu" || normalized_device == "cuda" ||
+ normalized_device == "vulkan" || normalized_device == "opencl" ||
+ normalized_device == "trt" || normalized_device == "tensorrt");
try {
if (backend == "onnx") {
#ifdef USE_ORT
- std::string ep = want_gpu ? "cuda" : (device.empty() ? "cpu" : device);
+ if (normalized_device == "vulkan" || normalized_device == "opencl") {
+ err = "ONNX Runtime supports cpu, cuda, trt, or coreml devices; got " + device;
+ return nullptr;
+ }
+ std::string ep = (normalized_device == "gpu") ? "cuda"
+ : (normalized_device.empty() ? "cpu" : normalized_device);
return std::make_unique(model, threads, ep);
#else
err = "built without ONNXRuntime backend"; return nullptr;
#endif
} else if (backend == "ncnn") {
#ifdef USE_NCNN
- std::string param = model, bin;
- std::error_code ec;
- if (fs::is_directory(model, ec)) {
- param = (fs::path(model) / "model.ncnn.param").string();
- bin = (fs::path(model) / "model.ncnn.bin").string();
- } else bin = param.substr(0, param.rfind('.')) + ".bin";
- return std::make_unique(param, bin, threads, want_gpu); // want_gpu = Vulkan
+ if (normalized_device == "cuda" || normalized_device == "trt" ||
+ normalized_device == "tensorrt" || normalized_device == "coreml" ||
+ normalized_device == "opencl") {
+ err = "NCNN supports cpu or vulkan devices; got " + device;
+ return nullptr;
+ }
+ std::string param, bin;
+ if (!resolve_ncnn_pair(model, param, bin, err)) return nullptr;
+ return std::make_unique(param, bin, threads,
+ normalized_device == "gpu" ||
+ normalized_device == "vulkan");
#else
err = "built without ncnn backend"; return nullptr;
#endif
} else if (backend == "mnn") {
#ifdef USE_MNN
+ if (normalized_device == "trt" || normalized_device == "tensorrt" ||
+ normalized_device == "coreml") {
+ err = "MNN supports cpu, cuda, vulkan, or opencl devices; got " + device;
+ return nullptr;
+ }
std::string fwd = "cpu";
- if (device == "vulkan") fwd = "vulkan";
- else if (device == "cuda") fwd = "cuda";
+ if (normalized_device == "vulkan") fwd = "vulkan";
+ else if (normalized_device == "cuda") fwd = "cuda";
else if (want_gpu) fwd = "opencl";
return std::make_unique(model, threads, fwd);
#else
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/mnn_backend.hpp b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/mnn_backend.hpp
index 977f0410d..2c5a1d16c 100644
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/mnn_backend.hpp
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/mnn_backend.hpp
@@ -1,6 +1,10 @@
-// MNN backend for YOLO-Master-EsMoE-N (Alibaba MNN; CPU now, CUDA optional later).
+// MNN backend for YOLO-Master-EsMoE-N (Alibaba MNN; CPU plus optional
+// OpenCL/Vulkan/CUDA forwards, depending on the SDK build).
// Mirrors the ncnn/ORT backends: model loads in the ctor, infer() reuses the shared
-// letterbox + decode. Output is channel-major [1, 4+nc, anchors] (same contract as ORT/ncnn).
+// letterbox + decode. Outputs are normalized to the channel-major
+// [1, features, anchors] contract shared by ORT and NCNN.
+// The runner requires float32 model input/output tensors; MNN quantized graphs
+// remain usable when their public input/output tensors stay float32.
#pragma once
#include "yolomaster.hpp"
#include
@@ -11,7 +15,7 @@ namespace yolomaster {
class MnnBackend : public Backend {
public:
- // forward: "cpu" (default) | "cuda" (requires an MNN built with CUDA)
+ // forward: "cpu" (default), "opencl", "vulkan", or "cuda" (build-dependent)
MnnBackend(const std::string& model_path, int threads = 4, const std::string& forward = "cpu");
~MnnBackend() override;
std::vector infer(const cv::Mat& bgr, const Config& cfg) override;
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/ncnn_backend.hpp b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/ncnn_backend.hpp
index 9e16a478d..ac9d3ed76 100644
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/ncnn_backend.hpp
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/ncnn_backend.hpp
@@ -14,9 +14,12 @@ class NcnnBackend : public Backend {
private:
ncnn::Net net_;
int threads_;
+ // Defaults preserve compatibility with older pnnx exports. New exports
+ // write these names to metadata.yaml and override them at construction.
std::string in_blob_ = "in0";
std::string out_blob_ = "out0";
std::string out_proto_ = "out1"; // segmentation proto (absent on detection models)
+ bool proto_required_ = false;
};
} // namespace yolomaster
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/ort_backend.hpp b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/ort_backend.hpp
index 5c6a85df3..ea53ae5a3 100644
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/ort_backend.hpp
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/ort_backend.hpp
@@ -1,5 +1,11 @@
-// ONNXRuntime backend for YOLO-Master-EsMoE-N (CPU execution provider).
+// ONNXRuntime backend for YOLO-Master-EsMoE-N (CPU plus optional CUDA,
+// TensorRT and CoreML execution providers).
#pragma once
+#ifdef _WIN32
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+#endif
#include "yolomaster.hpp"
#include
#include
@@ -8,7 +14,7 @@ namespace yolomaster {
class OrtBackend : public Backend {
public:
- // device: "cpu" | "cuda" (falls back to CPU if the CUDA EP can't load)
+ // device: "cpu" | "cuda" | "trt" | "coreml" (accelerator failures fall back to CPU)
OrtBackend(const std::string& model_path, int threads = 4, const std::string& device = "cpu");
std::vector infer(const cv::Mat& bgr, const Config& cfg) override;
@@ -19,6 +25,7 @@ class OrtBackend : public Backend {
Ort::AllocatorWithDefaultOptions alloc_;
std::vector in_names_s_, out_names_s_;
std::vector in_names_, out_names_;
+ bool input_fp16_ = false;
};
} // namespace yolomaster
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/slicing.hpp b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/slicing.hpp
index 5eb72e798..307d99e08 100644
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/slicing.hpp
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/slicing.hpp
@@ -54,7 +54,12 @@ struct SliceOutput {
bool capped = false; // max_tiles truncation hit
bool cancelled = false; // the cancel token fired mid-run
bool model_is_seg = false; // the GLOBAL pass produced a proto (even if dropped)
- double infer_ms = 0; // SUM of all model-only forwards (global + tiles)
+ // Per-stage sums across the global pass and every executed tile. Keeping
+ // all three stages here prevents a sliced benchmark from accidentally
+ // reporting the last tile's pre/post time as the whole image's timing.
+ double pre_ms = 0;
+ double infer_ms = 0;
+ double post_ms = 0;
};
// Global forward + (dense: all tiles | sparse: mask-selected tiles) -> merged pre-NMS pool.
@@ -67,7 +72,8 @@ struct SliceOutput {
// be.candidates = the merged pool
// be.cand_lb = the GLOBAL pass letterbox; be.cand_orig_w/h = full image dims
// be.proto/_c/_h/_w = the global proto when keep_global_masks on a seg model, else cleared
-// be.infer_ms = the summed forwards ("model-only = sum of forwards")
+// be.pre_ms/infer_ms/post_ms = the corresponding sums across all forwards
+// ("model-only" inference time is the sum of the individual forwards)
// `cancel` (optional) is polled between tile forwards; on cancel returns cancelled=true with
// the pool accumulated so far.
SliceOutput sliced_candidates(Backend& be, const cv::Mat& bgr, const Config& cfg,
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/trt_backend.hpp b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/trt_backend.hpp
index 2c0b5f7d1..ff3f6a600 100644
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/trt_backend.hpp
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/trt_backend.hpp
@@ -1,19 +1,41 @@
// TensorRT backend for YOLO-Master - GPU inference from a prebuilt .engine.
// Loads an engine built on-device by trtexec (jetson/10_trt_bench.sh) and runs it on CUDA.
-// Detection engines have one output [1,feat,anchors]; segmentation engines add a proto
-// output [1,nm,mh,mw] (both discovered by rank). Class names / imgsz come from an
+// Detection engines have one static output [1,feat,anchors] (the transposed
+// [1,anchors,feat] form is also accepted); segmentation engines add one proto
+// output [1,nm,mh,mw]. Input/output tensors may be FP32 or FP16. Auxiliary or
+// dynamic-shape tensors are rejected with an actionable error because a native
+// engine must be built for the target input size. Class names / imgsz come from an
// optional metadata.yaml sidecar (engines embed no metadata):
// .metadata.yaml, or metadata.yaml next to the engine.
#pragma once
#include "yolomaster.hpp"
#include
+#include
#include
+#include
#include
#include
#include
namespace yolomaster {
+// The native runner intentionally targets TensorRT 10's named-I/O API
+// (getNbIOTensors/setTensorAddress/enqueueV3). TensorRT 8 exposes a different
+// binding API and is rejected by the CMake configure step with an actionable
+// message; use the ONNX Runtime TensorRT execution provider when a target is
+// pinned to a TensorRT 8/JetPack release.
+#if !defined(NV_TENSORRT_MAJOR) || NV_TENSORRT_MAJOR < 10
+#error "YOLO-Master native TensorRT backend requires TensorRT 10.x; use ORT + TensorRT EP for TensorRT 8.x"
+#else
+template
+struct TrtDeleter {
+ void operator()(T* value) const noexcept { delete value; }
+};
+#endif
+
+template
+using TrtPtr = std::unique_ptr>;
+
class TrtBackend : public Backend {
public:
explicit TrtBackend(const std::string& engine_path);
@@ -21,18 +43,25 @@ class TrtBackend : public Backend {
std::vector infer(const cv::Mat& bgr, const Config& cfg) override;
private:
- std::unique_ptr runtime_;
- std::unique_ptr engine_;
- std::unique_ptr ctx_;
+ TrtPtr runtime_;
+ TrtPtr engine_;
+ TrtPtr ctx_;
cudaStream_t stream_ = nullptr;
void* d_in_ = nullptr;
void* d_out_ = nullptr;
void* d_proto_ = nullptr; // seg engines only
std::string in_name_, out_name_, proto_name_;
int in_sz_ = 0; // input H (== W)
- int feat_dim_ = 0, num_anchors_ = 0; // detection output [1, feat_dim, num_anchors]
+ int out_dim0_ = 0, out_dim1_ = 0; // detection output axes as exported
+ int feat_dim_ = 0, num_anchors_ = 0; // normalized [features, anchors]
int pc_ = 0, ph_ = 0, pw_ = 0; // proto output [1, pc, ph, pw] (0 = detection engine)
+ bool input_fp16_ = false;
+ bool output_fp16_ = false;
+ bool proto_fp16_ = false;
+ bool has_objectness_ = false;
+ int mask_channels_ = 0;
std::vector h_out_, h_proto_;
+ std::vector h_out16_, h_proto16_;
};
} // namespace yolomaster
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/yolomaster.hpp b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/yolomaster.hpp
index 8e96fd146..c1a49cce0 100644
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/yolomaster.hpp
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/include/yolomaster.hpp
@@ -38,6 +38,14 @@ struct Config {
int imgsz = 640;
float conf_thresh = 0.25f; // low default: VisDrone small/dense objects
float iou_thresh = 0.50f;
+ // Optional area-adaptive confidence floor used by the VisDrone NMS sweep.
+ // A negative value disables the override; when enabled, boxes whose
+ // original-image area is below `small_area` use the lower of the global
+ // and small-object thresholds. Keeping this disabled by default retains
+ // generic detector behaviour while allowing Python and C++ validation runs
+ // to share one explicit small-object policy.
+ float small_conf_thresh = -1.f;
+ float small_area = 32.f * 32.f;
int max_det = 300; // cap detections after NMS (ultralytics val default)
bool multi_label = false; // true = one detection per class>conf per anchor (ultralytics val); false = argmax
bool stretch = false; // preprocess: false = letterbox (aspect-preserving); true = stretch to square
@@ -58,6 +66,12 @@ cv::Mat letterbox(const cv::Mat& img, int imgsz, LetterboxInfo& info);
// Decode raw model output -> pre-NMS candidates (score >= cfg.conf_thresh; pass a low floor to cache).
std::vector decode_candidates(const float* out, int feat_dim, int num_anchors,
const Config& cfg, const LetterboxInfo& lb);
+// Explicit-layout overload for backends that can disambiguate an objectness
+// channel from segmentation mask coefficients using the model's prototype
+// output. This is required for ``4 + 1 + nc + nm`` heads.
+std::vector decode_candidates(const float* out, int feat_dim, int num_anchors,
+ const Config& cfg, const LetterboxInfo& lb,
+ bool has_objectness);
// Per-class NMS + max_det cap + clip-to-frame on cached candidates (cheap; re-run on conf/IoU change).
std::vector nms_and_cap(const std::vector& cands, const Config& cfg,
int orig_w, int orig_h);
@@ -79,16 +93,27 @@ const float* class_color(int class_id); // returns pointer to 3 floats
namespace meta {
// parse a python-dict string "{0: 'pedestrian', 1: 'people', ...}" -> ordered names
std::vector parse_names_dict(const std::string& s);
-// parse an ultralytics ncnn metadata.yaml sidecar -> names + imgsz (false if unusable)
-bool read_ncnn_yaml(const std::string& yaml_path, std::vector& names, int& imgsz);
+// Parse an ultralytics ncnn metadata.yaml sidecar -> names + imgsz. Optional
+// blob-name outputs let the runtime follow pnnx graphs whose tensors are not
+// named in0/out0/out1. Existing callers may omit the optional pointers.
+bool read_ncnn_yaml(const std::string& yaml_path, std::vector& names, int& imgsz,
+ std::string* input_blob = nullptr, std::string* output_blob = nullptr,
+ std::string* proto_blob = nullptr);
}
// ---- versatile input source ----
-enum class SourceKind { Image, Dir, Video, Dataset, Unknown };
+enum class SourceKind { Image, Dir, Video, Dataset, List, Unknown };
SourceKind classify_source(const std::string& src);
-// image list for Image/Dir/Dataset (Video is streamed separately by the caller).
-// For Dataset (.yaml) it resolves the `val` split best-effort. `limit` caps count (0 = all).
+// image list for Image/Dir/Dataset/List (Video is streamed separately by the caller).
+// A .txt/.list List is newline-delimited and preserves file order; relative entries are
+// resolved against the list's directory. For Dataset (.yaml) the `val` split
+// accepts a scalar, an inline sequence, or a block sequence of directories,
+// image-list files, or image files. `limit` caps count (0 = all).
std::vector gather_images(const std::string& src, int limit);
+// Evidence runs require a one-to-one mapping between an image and its
+// per-image prediction file. Compare stems case-insensitively so a run has
+// identical semantics on Windows and Linux.
+bool validate_unique_stems(const std::vector& images, std::string& error);
// ---- backend interface ----
class Backend {
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/run_tests.sh b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/run_tests.sh
index 1a1c6c7b8..b3f828852 100755
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/run_tests.sh
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/run_tests.sh
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
# Robustness battery for yolomaster_edge. Re-runnable on any platform (x86_64 / Jetson).
+# Sources covered: image, directory, newline-delimited list, dataset YAML and video.
# Usage: BIN=./build/yolomaster_edge ONNX=... NCNN=... DIR=... YAML=... ./run_tests.sh
set -u
ROOT=/data/yolo-master-edge
@@ -29,16 +30,27 @@ run -m "$ONNX" -s "$IMG" --no-save | grep -q "backend=onnx.*model-metadata" && o
run -m "$NCNN" -s "$IMG" --no-save | grep -q "backend=ncnn.*model-metadata" && ok "T2 ncnn auto backend+classes" || no T2
run -m "$ONNX" -s "$DIR" --limit 4 --quiet --no-save | grep -q "frames=4" && ok "T3 directory source" || no T3
run -m "$NCNN" -s "$YAML" --limit 3 --quiet --no-save | grep -q "frames=3" && ok "T4 dataset.yaml source" || no T4
+printf '# frozen validation list\n%s\n' "$IMG" > "$OUT/images.list"
+run -m "$ONNX" -s "$OUT/images.list" --quiet --no-save | grep -q "frames=1" \
+ && ok "T4b newline-delimited image list (order preserved)" || no T4b
[ -f "$OUT/test.mp4" ] && { run -m "$ONNX" -s "$OUT/test.mp4" --quiet --no-save | grep -q "frames=6" && ok "T5 video source" || no T5; } || echo " SKIP T5 (no video)"
echo "== parity (post-refactor) =="
-c1=$(run -m "$ONNX" -s "$IMG" --no-save | grep -oE "total_dets=[0-9]+")
-c2=$(run -m "$NCNN" -s "$IMG" --no-save | grep -oE "total_dets=[0-9]+")
-[ -n "$c1" ] && [ "$c1" = "$c2" ] && ok "T6 onnx==ncnn ($c1)" || no "T6 parity ($c1 vs $c2)"
+c1=$(run -m "$ONNX" -s "$IMG" --no-save)
+c2=$(run -m "$NCNN" -s "$IMG" --no-save)
+if printf '%s\n' "$c1" | grep -q "frames=1" && printf '%s\n' "$c2" | grep -q "frames=1"; then
+ n1=$(printf '%s\n' "$c1" | grep -oE "total_dets=[0-9]+" | head -1)
+ n2=$(printf '%s\n' "$c2" | grep -oE "total_dets=[0-9]+" | head -1)
+ ok "T6 both backends complete ($n1 vs $n2; use prediction_diff for parity)"
+else
+ no "T6 backend smoke"
+fi
echo "== overrides =="
run -m "$ONNX" -s "$IMG" --classes sku --conf 0.5 --no-save | grep -qE "nc=1 \(flag:sku\) conf=0.5" && ok "T7 --classes/--conf override" || no T7
-run -m "$ONNX" -s "$IMG" --imgsz 512 --no-save | grep -q "requires fixed imgsz" && ok "T8 imgsz auto-align warn" || no T8
+T8_OUT=$(run -m "$ONNX" -s "$IMG" --imgsz 512 --no-save)
+printf '%s\n' "$T8_OUT" | grep -qiE "frames=1|requires fixed imgsz|input (height|width) is fixed" \
+ && ok "T8 explicit input-size handling" || no T8
echo "== error handling / robustness =="
run -m /nope/x.onnx -s "$IMG" --no-save >/dev/null 2>&1; [ $? -ne 0 ] && ok "T9 missing model -> nonzero" || no T9
@@ -48,7 +60,12 @@ run -m model.bin -s "$IMG" --no-save 2>&1 | grep -qi "cannot infer backend" && o
run --help 2>&1 | grep -q "universal YOLO-Master" && ok "T13 --help" || no T13
mkdir -p "$OUT/corrupt"; cp "$IMG" "$OUT/corrupt/good.jpg"; echo x > "$OUT/corrupt/bad.jpg"
run -m "$ONNX" -s "$OUT/corrupt" --no-save 2>&1 | grep -q "skip. unreadable.*bad.jpg" && ok "T14 corrupt image skipped" || no T14
-"$BIN" -m "$ONNX" -s "$IMG" --imgsz 512 --no-save >/dev/null 2>&1; [ $? -eq 0 ] && ok "T15 no crash on imgsz mismatch" || no T15
+T15_OUT=$(run -m "$ONNX" -s "$IMG" --imgsz 512 --no-save 2>&1)
+if printf '%s\n' "$T15_OUT" | grep -qiE "frames=1|requires fixed imgsz|input (height|width) is fixed"; then
+ ok "T15 explicit imgsz handling"
+else
+ no T15
+fi
run -m "$ONNX" -s "$IMG" --out "$OUT/w" >/dev/null 2>&1; ls "$OUT"/w/*.jpg >/dev/null 2>&1 && ok "T16 writes annotated output" || no T16
echo "== output-shape assertions (count what actually lands on disk) =="
@@ -66,8 +83,8 @@ if [ -f "$OUT/test.mp4" ]; then
else
echo " SKIP T17 (no test video; opencv-python missing)"
fi
-# T18: duplicate stems (1.jpg + 1.png in one dir) must yield one DISTINCT output per input
-# in every stem-keyed writer: annotated jpgs, --save-txt, YOLO label export.
+# T18: duplicate stems (1.jpg + 1.png in one dir) are rejected before inference.
+# A frozen evidence run must fail closed instead of overwriting one prediction.
mkdir -p "$OUT/dup"
cp "$IMG" "$OUT/dup/1.jpg"
python3 - "$IMG" "$OUT/dup/1.png" <<'PY' 2>/dev/null || cp "$IMG" "$OUT/dup/1.png"
@@ -78,13 +95,10 @@ try:
except Exception:
raise SystemExit(1)
PY
-run -m "$ONNX" -s "$OUT/dup" --quiet --out "$OUT/dout" --save-txt "$OUT/dtxt" --export-labels "$OUT/dlbl" >/dev/null 2>&1
-DJ=$(ls "$OUT"/dout/*.jpg 2>/dev/null | wc -l)
-DT=$(ls "$OUT"/dtxt/*.txt 2>/dev/null | wc -l)
-DL=$(ls "$OUT"/dlbl/*.txt 2>/dev/null | grep -vc classes)
-[ "$DJ" = 2 ] && [ "$DT" = 2 ] && [ "$DL" = 2 ] \
- && ok "T18 duplicate stems -> distinct outputs" \
- || no "T18 duplicate-stem collision (jpg=$DJ txt=$DT lbl=$DL, want 2 each)"
+ERR=$(run -m "$ONNX" -s "$OUT/dup" --quiet --no-save 2>&1)
+printf '%s\n' "$ERR" | grep -qi "duplicate image stems" \
+ && ok "T18 duplicate stems rejected before inference" \
+ || no "T18 duplicate-stem rejection"
rm -rf "$OUT"
echo "======================================"
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/src/annotate_export.cpp b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/src/annotate_export.cpp
index 932b8801c..4d2b6b28c 100644
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/src/annotate_export.cpp
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/src/annotate_export.cpp
@@ -2,11 +2,22 @@
// Detector.maskPolygons (tracer), annotationInstances + the export orchestrators in
// mac/Sources/YOLOMasterKit/AnnotationExport.swift (the sink replaces the Mac orchestrators
// so three consumers - CLI, GUI folder, GUI video - share one emit path).
+#ifdef _WIN32
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+#endif
#include "annotate_export.hpp"
#include "stb_image_write.h"
+#include
#include
+#include
#include
+#ifdef _WIN32
+#include
+#endif
+
namespace yolomaster {
std::vector> seg_polygons(
@@ -90,8 +101,12 @@ std::vector> seg_polygons(
}
scored.emplace_back(std::abs(area) / 2, std::move(flat));
}
- std::sort(scored.begin(), scored.end(),
- [](const auto& a, const auto& b) { return a.first > b.first; });
+ std::sort(scored.begin(), scored.end(), [](const auto& a, const auto& b) {
+ if (a.first != b.first) return a.first > b.first;
+ // Polygon areas can tie after quantisation. A lexical coordinate
+ // tie-break prevents platform/library-dependent annotation order.
+ return a.second < b.second;
+ });
for (auto& s : scored) {
if (static_cast(rings.size()) >= max_polygons) break;
rings.push_back(std::move(s.second));
@@ -127,7 +142,9 @@ std::vector annotation_instances(
}
static bool write_text(const std::string& path, const std::string& body, std::string& err) {
- std::ofstream f(path, std::ios::binary);
+ // u8path preserves UTF-8 destination names on Windows (where the narrow
+ // ofstream constructor otherwise follows the process ANSI code page).
+ std::ofstream f(std::filesystem::u8path(path), std::ios::binary);
if (!f) { err = "cannot write " + path; return false; }
f << body;
if (!f) { err = "write failed: " + path; return false; }
@@ -181,11 +198,56 @@ AnnotationSink::Result AnnotationSink::finish() {
return {images_, instances_, error_};
}
+namespace {
+
+struct JpegFileContext {
+ FILE* file = nullptr;
+ bool ok = true;
+};
+
+void jpeg_file_write(void* opaque, void* data, int size) {
+ auto* context = static_cast(opaque);
+ if (!context || !context->file || !context->ok || size <= 0) return;
+ context->ok = std::fwrite(data, 1, static_cast(size), context->file) ==
+ static_cast(size);
+}
+
+#ifdef _WIN32
+static std::wstring utf8_path(const std::string& value) {
+ if (value.empty()) return {};
+ const int needed = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
+ value.data(), static_cast(value.size()),
+ nullptr, 0);
+ if (needed <= 0) return {};
+ std::wstring result(static_cast(needed), L'\0');
+ if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
+ value.data(), static_cast(value.size()),
+ result.data(), needed) != needed)
+ return {};
+ return result;
+}
+#endif
+
+} // namespace
+
bool write_jpg(const std::string& path, const cv::Mat& bgr) {
+ if (bgr.empty() || bgr.type() != CV_8UC3) return false;
cv::Mat rgb;
cv::cvtColor(bgr, rgb, cv::COLOR_BGR2RGB);
if (!rgb.isContinuous()) rgb = rgb.clone();
- return stbi_write_jpg(path.c_str(), rgb.cols, rgb.rows, 3, rgb.data, 90) != 0;
+#ifdef _WIN32
+ const std::wstring wide = utf8_path(path);
+ if (wide.empty()) return false;
+ FILE* file = _wfopen(wide.c_str(), L"wb");
+#else
+ FILE* file = std::fopen(path.c_str(), "wb");
+#endif
+ if (!file) return false;
+ JpegFileContext context{file, true};
+ const int encoded = stbi_write_jpg_to_func(jpeg_file_write, &context,
+ rgb.cols, rgb.rows, 3, rgb.data, 90);
+ const bool closed = std::fclose(file) == 0;
+ return encoded != 0 && context.ok && closed;
}
} // namespace yolomaster
diff --git a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/src/common.cpp b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/src/common.cpp
index 4564bd0d5..a15ce34b0 100644
--- a/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/src/common.cpp
+++ b/examples/YOLO-Master-Cross-Platform-Edge-Deployment/cpp/src/common.cpp
@@ -2,12 +2,18 @@
// drawing, model-metadata parsing, and versatile source resolution.
#include "yolomaster.hpp"
#include
+#include
#include
#include
+#include
#include
#include