Skip to content

Exclude Sigmoid from TensorRT INT8 quantization - #25020

Merged
glenn-jocher merged 8 commits into
ultralytics:mainfrom
davidnichols-ops:fix/yolo26-int8-confidence-compression-24668
Jul 9, 2026
Merged

glenn-jocher merged 8 commits into
ultralytics:mainfrom
davidnichols-ops:fix/yolo26-int8-confidence-compression-24668

Conversation

@davidnichols-ops

@davidnichols-ops davidnichols-ops commented Jul 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

YOLO26's one-to-one detection head produces moderate logits in Sigmoid's steep region, where INT8 quantization error compresses confidence scores and degrades F1 calibration.

This keeps Sigmoid at higher precision in both TensorRT export paths:

  • TensorRT 11+ ModelOpt Q/DQ: excludes the Sigmoid ONNX op from INT8 quantization.
  • TensorRT 8.2–10 implicit quantization: applies per-layer FP32 precision with OBEY_PRECISION_CONSTRAINTS.
  • TensorRT 7–8.1 implicit quantization: applies the same per-layer FP32 precision with legacy STRICT_TYPES.

The patch also bumps Ultralytics from 8.4.90 to 8.4.91 and adds a real static INT8 detect export to the existing GPU matrix.

Deletion/relocation cannot combine the two implementation sites because TensorRT 7–10 configures parsed builder layers while TensorRT 11+ quantizes the ONNX graph through ModelOpt.

Deleted: the speculative Softmax exclusion, its standalone documentation block, and the silent OBEY_PRECISION_CONSTRAINTS-availability guard from prior PR revisions; the guidance now lives in the existing INT8 drawbacks section and legacy builders select STRICT_TYPES instead of skipping the fix.

Fixes #24668

T4 validation

Model INT8 mAP50 FP16 mAP50 Retention INT8 mAP50-95 FP16 mAP50-95 Retention
YOLO26s detect 0.5916 0.6360 93.0% 0.4324 0.4714 91.7%
YOLO11s detect 0.6164 0.6283 98.1% 0.4486 0.4601 97.5%

INT8 inference remained comparable to FP16 (3.9 ms vs 4.0 ms), with 27–31% smaller engines and no YOLO11s regression.

Environment: Kaggle Tesla T4 (SM 7.5), TensorRT 10.7.0, PyTorch 2.10.0+cu128, 500 COCO validation calibration images, full 5,000-image COCO validation, batch 1, 640 px.

Test plan

  • Focused ModelOpt exclusion tests: 2 passed.
  • Ruff check and format.
  • Pinned Prettier 3.6.2 check.
  • Test collection: 110 CUDA tests collected, including the static legacy INT8 case.
  • Two fresh independent adversarial reviews; final round: LGTM.
  • GitHub CI on the final head.

@github-actions

github-actions Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

All Contributors have signed the CLA. ✅
Posted by the CLA Assistant Lite bot.

@UltralyticsAssistant UltralyticsAssistant added bug Something isn't working as intended in the official Ultralytics package. documentation Improvements or additions to documentation exports Model exports (ONNX, TensorRT, TFLite, etc.) labels Jul 2, 2026
@UltralyticsAssistant

Copy link
Copy Markdown
Member

👋 Hello @davidnichols-ops, thank you for submitting a ultralytics/ultralytics 🚀 PR! This automated message is here to help with the initial review process, and an Ultralytics engineer will assist further. Your PR appears to address YOLO26 INT8 confidence calibration, TensorRT export behavior, end2end=False score handling, tests, and related documentation updates.

  • ✅ Define a Purpose: Clearly explain the purpose of your fix or feature in your PR description, and link to any relevant issues. Ensure your commit messages are clear, concise, and adhere to the project's conventions.
  • ✅ Synchronize with Source: Confirm your PR is synchronized with the ultralytics/ultralytics main branch. If it's behind, update it by clicking the 'Update branch' button or by running git pull and git merge main locally.
  • ✅ Ensure CI Checks Pass: Verify all Ultralytics Continuous Integration (CI) checks are passing. If any checks fail, please address the issues.
  • ✅ Update Documentation: Update the relevant documentation for any new or modified features.
  • ✅ Add Tests: If applicable, include or update tests to cover your changes, and confirm that all tests are passing.
  • ✅ Sign the CLA: Please ensure you have signed our Contributor License Agreement if this is your first Ultralytics PR by writing "I have read the CLA Document and I sign the CLA" in a new message.
  • ✅ Minimize Changes: Limit your changes to the minimum necessary for your bug fix or feature addition. "It is not daily increase but daily decrease, hack away the unessential. The closer to the source, the less wastage there is." — Bruce Lee

For more guidance, please refer to our Contributing Guide. Don't hesitate to leave a comment if you have any questions. Thank you for contributing to Ultralytics! 🚀

@UltralyticsAssistant UltralyticsAssistant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 PR Review

Made with ❤️ by Ultralytics Actions

Found two actionable issues: a misleading exporter warning when end2end is left as the default None, and a likely failure path for fused end-to-end models with end2end=False due to the one-to-one-head selection guard.

💬 Posted 2 inline comments

Comment thread ultralytics/nn/modules/head.py Resolved
Comment thread ultralytics/engine/exporter.py Resolved

@Y-T-G Y-T-G left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR.

Can this be resolved with Fix 2 and Fix 3 alone?

Fix 1 and Fix 4 are regressive. Fix 4 defeats the point of end2end=False.

The PR is also too large and verbose for a fix. Please scope it and reduce verbosity.

@davidnichols-ops davidnichols-ops changed the title Fix YOLO26 INT8 confidence compression and end2end=False score calibration Exclude Sigmoid from TensorRT INT8 quantization for YOLO26 confidence preservation Jul 3, 2026
@davidnichols-ops
davidnichols-ops marked this pull request as ready for review July 3, 2026 15:02
@codecov

codecov Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 7.69231% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
ultralytics/utils/export/engine.py 0.00% 12 Missing ⚠️

📢 Thoughts on this report? Let us know!

@davidnichols-ops davidnichols-ops changed the title Exclude Sigmoid from TensorRT INT8 quantization for YOLO26 confidence preservation Exclude Sigmoid from TensorRT INT8 quantization (#24668) Jul 3, 2026
@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

@Y-T-G Yes — the PR now contains only Fix 2 and Fix 3. Fix 1 (exporter warning for end2end=False) and Fix 4 (one-to-one head selection for end2end=False) have been removed entirely.

The squashed commit (5cfc713) touches only:

  • ultralytics/utils/export/engine.py — Sigmoid/Softmax exclusion from INT8 quantization (TRT 10 per-layer FP32 constraint + TRT 11+ ModelOpt op_types_to_exclude)
  • tests/test_exports.py — tests for the above
  • docs/en/integrations/tensorrt.md — short doc note
  • tests/bench_int8_retention.py — benchmark script

No changes to head.py or exporter.py's end2end logic remain. The stale inline comments from UltralyticsAssistant on those files have been resolved.

@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

Latest changes — what I found and fixed

While testing the Softmax generalization, I ran into a few issues with the TRT 10 path and the test suite. Here's what happened and how I dealt with each:

1. Softmax wasn't actually being preserved on TRT 10 (real bug)

In TensorRT, Softmax is a separate ISoftMaxLayer with LayerType.SOFTMAX — it's not an IActivationLayer. My original loop only checked layer.type == trt.LayerType.ACTIVATION, so Softmax layers were filtered out before the detection helper ever ran. trt.ActivationType also has no SOFTMAX member, so the typed-cast path was dead code for Softmax too. The Sigmoid fix worked fine (Sigmoid is an activation layer), but the Softmax generalization was advertised without actually being implemented.

Fixed by adding an explicit LayerType.SOFTMAX branch in _constrain_sensitive_activations_fp32:

softmax_type = getattr(trt.LayerType, "SOFTMAX", None)
...
if layer.type == trt.LayerType.ACTIVATION and _is_precision_sensitive_activation(layer, trt):
    # constrain Sigmoid activations
elif softmax_type is not None and layer.type == softmax_type:
    # constrain Softmax layers

The execution test now includes a Softmax layer with LayerType.SOFTMAX and asserts it's constrained (count == 2: Sigmoid + Softmax). For the reported YOLO26 detect issue only Sigmoid matters, so the fix was already working for its stated purpose — but the Softmax generalization is now actually implemented rather than just advertised.

2. The call-site test wasn't actually testing the call site

The old test_trt10_sigmoid_fp32_precision_constraint just called the helper directly with an empty network and checked the return type. It didn't verify that onnx2engine actually invokes the helper at the right time. Replaced it with test_trt10_constrain_sensitive_activations_call_site that builds a full fake tensorrt module (Builder, Config, Network, Parser, Logger, IInt8Calibrator, all flags/enums), creates a minimal valid ONNX file, and calls onnx2engine directly. It verifies the helper is:

  • Called for INT8 on TRT 10
  • NOT called for FP16 on TRT 10
  • NOT called for INT8 on TRT 11 (ModelOpt handles it there)

3. Comment said FP32, but it's actually FP16

The comment on the ModelOpt path said excluded nonlinearities "run in FP32 on dequantized logits," but ModelOpt defaults to high_precision_dtype='fp16', so they actually run in FP16. Fixed the comment to say so, and noted that OpenVINO's IgnoredScope is stricter (keeps ops in FP32) but the accuracy impact is negligible for 0-3 logits.

4. Test could trigger a network install

The ModelOpt test was calling check_requirements("nvidia-modelopt[onnx]>=0.44") which, if modelopt is absent and autoinstall is on, would try to pip install it. Safe in CI (where YOLO_AUTOINSTALL=false), but not hermetic. Added a monkeypatch for check_requirements so the test is self-contained.

5. Removed a test that's no longer relevant

Dropped test_end2end_false_export_smoke — it tested end2end=False export behavior, which was part of the original PR but was removed per the earlier review feedback to scope down to Fix 2 and Fix 3 only.

6. OpenVINO parity test — kept as-is

The OpenVINO test uses source inspection (searching the wrapped function's source for IgnoredScope and Sigmoid). It's not as strong as a behavior test, but a behavior-driven test would require installing OpenVINO + NNCF, which is too heavy for CI. The closure-cell iteration was improved to search for the first callable cell rather than indexing a fixed slot, so it survives ordering changes if try_export is refactored.

Verification:

$ ruff check ultralytics/utils/export/engine.py tests/test_exports.py
All checks passed!
$ pytest tests/test_exports.py — 27 passed, 5 failed (pre-existing missing optional deps), 4 skipped

@glenn-jocher

Copy link
Copy Markdown
Member

Thanks, this rescope addresses the original concern and the TRT 10 Softmax branch/test coverage makes sense. One remaining cleanup to keep the PR minimal: please drop the standalone tests/bench_int8_retention.py benchmark script from the committed diff and keep the benchmark/reproduction details in the PR description or docs instead; the focused engine.py, test_exports.py, and TensorRT docs changes should be enough.

YOLO26's one-to-one detection head produces moderate logits (0-3) in the
sigmoid's steep region, making confidence scores sensitive to INT8
quantization error. Exclude precision-sensitive nonlinearities (Sigmoid,
Softmax) from INT8 quantization to preserve score calibration, mirroring
the OpenVINO INT8 exporter's IgnoredScope(types=["Sigmoid"]).

TRT 11+ (ModelOpt Q/DQ path):
  Pass op_types_to_exclude=["Sigmoid", "Softmax"] to modelopt.quantize()
  so the nonlinearities run in FP32 on dequantized inputs.

TRT 10 (implicit quantization path):
  Force Sigmoid/Softmax activation layers to FP32 via setPrecision /
  setOutputType + OBEY_PRECISION_CONSTRAINTS. Detection uses a typed
  IActivationLayer cast first (TRT 10.3-), falling back to layer name
  matching (TRT 10.7+ bindings don't expose activation_type on ILayer.

T4 GPU validation (TensorRT 10.7, full COCO val, 500 cal images):
  YOLO26s INT8 retains 92.9% mAP50 / 91.5% mAP50-95 vs FP16
  YOLO11s INT8 retains 98.0% mAP50 / 97.2% mAP50-95 vs FP16 (no regression)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
EOF
)
@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

Done — dropped tests/bench_int8_retention.py from the diff. The committed changes are now just the three files:

  • ultralytics/utils/export/engine.py — Sigmoid/Softmax exclusion logic
  • tests/test_exports.py — tests for the above
  • docs/en/integrations/tensorrt.md — doc note

Benchmark/reproduction details remain in the PR description (collapsed sections) and the Kaggle kernel link.

@glenn-jocher

Copy link
Copy Markdown
Member

Thanks, that cleanup is exactly what I was looking for. With the benchmark script removed, the PR is now focused on the TensorRT INT8 precision handling plus tests/docs; we can proceed with the normal CI/review path from here.

@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

Thanks, Glenn. I appreciate the feedback. I’ll leave it as-is and wait for CI approval and the normal review process!

@glenn-jocher glenn-jocher changed the title Exclude Sigmoid from TensorRT INT8 quantization (#24668) Exclude Sigmoid/Softmax from TensorRT INT8 quantization Jul 5, 2026
UltralyticsAssistant and others added 2 commits July 5, 2026 21:49
…ns_fp32` docstring

Fixes failing Publish Docs CI check on PR #25020, which requires every
parameter to have a type in the signature or `(type)` in the docstring
Args section. `network`, `config`, `trt`, and `prefix` were untyped.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

I have updated the PR to include the requested type annotations in _constrain_sensitive_activations_fp32 to resolve the documentation CI gap.

@glenn-jocher

Copy link
Copy Markdown
Member

Thanks, the type annotations on _constrain_sensitive_activations_fp32 are the right cleanup for the docs CI issue. Let’s let CI rerun on the updated diff and continue through the normal review path.

Fold the TRT 10 FP32 constraint loop into the existing INT8 calibrator
branch, drop the speculative IActivationLayer cast (not constructible in
TRT 10 Python bindings; name matching is what ran in the T4 validation),
and inline the op list at both call sites.

Deleted: _FP32_PRESERVED_ACTIVATIONS, _FP32_PRESERVED_OPS,
_is_precision_sensitive_activation, _is_sensitive_by_name,
_constrain_sensitive_activations_fp32, four mock-heavy tests
(call-site, execution, binding-branches, OpenVINO source inspection),
and the YOLO26 docs subsection referencing private constants.
@glenn-jocher

glenn-jocher commented Jul 7, 2026 •

Copy link
Copy Markdown
Member

Thanks @davidnichols-ops, the diagnosis and T4 validation here are excellent and the approach (mirroring OpenVINO's IgnoredScope) is exactly right. 🙏

I've pushed a simplification to your branch that preserves the validated behavior while reducing the diff to +35/−4:

  • The trt.IActivationLayer(layer) typed cast isn't constructible in the TRT 10 Python bindings (it raises TypeError on every version, not just 10.7+), so the name-matching fallback was the code path that actually ran in your T4 benchmarks. The revision keeps just that path: Softmax by LayerType.SOFTMAX, Sigmoid by ONNX node name — folded into the existing use_int8 and not is_trt11 calibrator branch.
  • op_types_to_exclude is now inlined at both call sites instead of the paired module constants.
  • Kept one focused unit test guarding the ModelOpt kwarg; your hardware validation covers the TRT 10 loop end-to-end.
  • Trimmed the docs to a short tip (docs shouldn't point users at private constants).

@glenn-jocher

Copy link
Copy Markdown
Member

Should this be quantize=w8a32 just like LiteRT?

@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

Good question — I looked into this, and I don’t think w8a32 applies directly to this particular path.

From what I found, ModelOpt’s ONNX quantization flow does not currently expose a weight-only mode. The available modes are INT8 static, INT4, and FP8. For the INT8 path this PR is using, excluded ops are kept in high_precision_dtype (default FP16), so op_types_to_exclude appears to be the intended mechanism for keeping numerically sensitive ops like Sigmoid/Softmax out of INT8.

The TRT 10 path also seems limited here since it uses activation + weight INT8 through IInt8Calibrator; I couldn’t find a weight-only builder option for this flow. LiteRT can provide w8a32 because its quantizer has a separate weight-only recipe, but I don’t see an equivalent exposed through ModelOpt/TRT ONNX currently.

I also ran a quick T4 comparison using ONNX Runtime quantize_dynamic (INT8 weights + FP32 activations) as a w8a32 proxy alongside the current TRT INT8 path (ModelOpt quantize=8 with Sigmoid/Softmax exclusion). The weight-only proxy did preserve confidence values better in my test, which is expected since activations remain higher precision.

Since ModelOpt doesn’t currently expose a weight-only mode, would it be worth considering whether an ONNX Runtime-based weight-only path is something worth exploring, or would the added complexity outweigh the accuracy benefit compared with the current ModelOpt/TensorRT approach?

I can share the benchmark details if useful.


Conclusion for this PR: I would keep the existing quantize=8 path with Sigmoid/Softmax exclusion rather than switching to w8a32, since ModelOpt/TensorRT does not currently provide an equivalent weight-only mode.

@glenn-jocher

Copy link
Copy Markdown
Member

Agreed — let’s keep this PR on the current TensorRT quantize=8 path with Sigmoid/Softmax exclusion. w8a32 is currently a LiteRT-specific dynamic/weight-only export mode in our export precision matrix, while TensorRT INT8 export remains calibration-based as described in the TensorRT docs. An ONNX Runtime weight-only route would be a separate feature/design PR if we decide it’s worth supporting, not something to add here.

@glenn-jocher glenn-jocher left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The final patch is scoped to the validated Sigmoid issue, removes the speculative Softmax policy and silent compatibility guard, covers TensorRT 7–11 through the correct APIs, includes the 8.4.91 patch bump, and is green across the full final-head CI matrix.

@glenn-jocher
glenn-jocher merged commit 8fc958e into ultralytics:main Jul 9, 2026
17 checks passed
@glenn-jocher glenn-jocher changed the title Exclude Sigmoid/Softmax from TensorRT INT8 quantization Exclude Sigmoid from TensorRT INT8 quantization Jul 9, 2026
@davidnichols-ops

Copy link
Copy Markdown
Contributor Author

Thank you Glenn for the help in getting this to the finish line. Nothing like green CI's in realtime.

@glenn-jocher

Copy link
Copy Markdown
Member

Thanks @davidnichols-ops — your careful diagnosis, T4 validation, and responsive cleanup made this a strong community contribution. Glad we got it merged with green CI!

@davidnichols-ops

davidnichols-ops commented Jul 10, 2026 via email

Copy link
Copy Markdown
Contributor Author

@glenn-jocher

Copy link
Copy Markdown
Member

Thanks again @davidnichols-ops—the careful validation and focused iteration benefited the whole YOLO community. We appreciate the contribution and look forward to your future PRs.

glenn-jocher added a commit to vedantparnaik/ultralytics that referenced this pull request Jul 13, 2026
…ics#25020)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ultralytics Assistant <[email protected]>
Co-authored-by: Glenn Jocher <[email protected]>
Laughing-q added a commit that referenced this pull request Jul 14, 2026
Softmax was intentionally left quantized in #25020 (its docs describe
Sigmoid-only exclusion); restore that behavior.

This reverts commit 2aab46546b0b1a93e40a81f828e7b72b34d45fc1.
glenn-jocher added a commit that referenced this pull request Jul 15, 2026
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ultralytics Assistant <[email protected]>
Co-authored-by: Glenn Jocher <[email protected]>
glenn-jocher added a commit that referenced this pull request Sep 30, 2026
- Restore region-shape ObjectCounter polygon direction so one lane never splits IN/OUT (#25104)
- Plot per-frame counts in Analytics line charts, not 30-frame sums (#25567)
- Read gray-palette semantic masks as gray-level class ids (#26426)
- Use args.conf for confusion matrices again (#25677, reverting #25320's fix)
- Stop deleting classify splits on every run, which wiped cache=disk files (#26423)
- Keep segment and semantic label caches from invalidating each other (#26207)
- Ignore *.yml files so they never shadow data.yaml (#26323)
- Skip the OS font rescan that costs 8s on macOS (#26256)
- Stop the false slow-access warning on tiny-file datasets (#25025)
- Scope TensorRT INT8 FP32 layers to the head for nms=True exports (#25020)
- Follow kpt_shape in RKNN and LiteRT pose keypoint scaling, skip RT-DETR in RKNN INT8 normalization (#25524)
- Document the shared dataset cache version
- Delete mock tests (#25106, #25720, #25875)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working as intended in the official Ultralytics package. documentation Improvements or additions to documentation exports Model exports (ONNX, TensorRT, TFLite, etc.)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Yolo26 int8 F1 curve is narrow (conf compression) than Yolo11 int8 F1 curve (e2e false during export and val)

4 participants