Skip to content

perf(inference): avoid redundant eval-mode assignments in predict - #1419

Merged
Borda merged 2 commits into
roboflow:developfrom
JESUSROYETH:perf/skip-redundant-eval-mode-reassertion
Sep 3, 2026
Merged

Borda merged 2 commits into
roboflow:developfrom
JESUSROYETH:perf/skip-redundant-eval-mode-reassertion

Conversation

@JESUSROYETH

Copy link
Copy Markdown
Contributor

What

RFDETR.predict() calls model.eval() on every unoptimised inference call. nn.Module.eval() just calls train(False), and that reassigns training on every registered module through nn.Module.__setattr__. It does this even when the whole tree is already in eval mode, so most calls pay a recursive walk that changes nothing.

This keeps the same per-call check, but skips the recursive assignments when there is nothing to change:

if any(module.training for module in model.modules()):
    model.eval()

The scan still covers the root and every registered submodule. So a replacement model left in training mode after RFDETR.train() still triggers the existing recursive eval() call, and so does a single submodule toggled directly under an eval-mode root. The optimised inference path still returns before this code, that part doesn't change.

Why it is safe

The unconditional call was added on purpose in d0740fd (#1146). Before that, predict() only entered eval mode while it was emitting its once-only warning, so predict()train()predict() could leave dropout active on the second prediction. This change keeps that fix working: RFDETR.train() rebinds self.model.model to the module Lightning returns, that replacement reports training mode, and the scan finds it and calls eval() on it.

I also considered checking only the root's training flag, but that breaks a mixed-mode tree, for example when a child gets toggled directly. Scanning every module keeps the old behaviour and still skips the expensive recursive assignments for the common case, everything already in eval.

On the RTX 4060, the read-only scan costs 0.18–0.22 ms for Nano/Small against 0.61–0.72 ms for the unconditional eval(). So it saves 0.43–0.51 ms of host work on every call that finds the tree already in eval. A call that reaches a module still training pays that same recursive eval() cost plus the scan on top, in that case it doesn't get faster.

Results

Public RFDETR.predict() on one real COCO image, pinned to develop@ffc69fd. Each process reports the median of 60 timed calls after 30 warm-up ones; brackets are the full range across counterbalanced baseline/patched fresh processes.

hardware model batch processes Before (ms) After (ms) Change
RTX 4060 Nano 1 3 8.812 [8.766, 8.916] 8.387 [8.383, 8.558] -4.82%
RTX 4060 Nano 4 3 24.812 [24.711, 24.913] 24.562 [24.555, 24.600] -1.01%
RTX 4060 Small 1 3 14.176 [14.053, 14.246] 13.800 [13.627, 13.913] -2.65%
L4 Nano 1 5 19.996 [19.794, 20.176] 19.128 [18.650, 19.429] -4.34%
L4 Nano 4 5 25.641 [25.473, 25.836] 24.334 [23.653, 24.952] -5.10%
L4 Small 1 5 22.283 [21.857, 22.984] 21.018 [20.943, 21.208] -5.68%

All six baseline/patched envelopes are disjoint. Every run gave the same detection count and the same SHA-256 fingerprint over xyxy, confidence and class_id in both arms. The removed work is host-side Python, so how much you save in absolute terms depends on the host CPU and the torch build. Its share also depends on how much GPU work is left.

Tests

  • New regression test is red on pristine develop (call(False)) and green with the patch.
  • All 7 focused eval-mode tests pass: model rebind, mixed-mode child, redundant-assignment skip, and the optimised-inference early return.
  • tests/inference/ with the repo's pytest addopts and CPU marker selection: 300 passed, 5 skipped, 13 deselected.
  • The complete local CPU-CI marker surface, split into bounded shards: 4,486 passed, 78 skipped, 13 deselected. The only failure was the ONNX-notes doctest, which fails the same way on pristine develop.
  • The complete gpu and not e2e_tensorrt surface on the L4: 71 passed, including all 22 GPU benchmarks and all 17 pretrained COCO val2017 inference cases across detection, segmentation, keypoints, predict(), and PTL evaluation.
  • RF-DETR pre-commit: all 19 hooks pass, strict mypy included.
  • Same 7 focused tests pass on the L4 validation VM too.

Not covered

  • The 17 GPU COCO val2017 tests recomputed their metrics and all passed the repository's thresholds, but their successful pytest output does not print the exact mAP/F1 values.
  • The isolated scan and assignment costs were only measured on the RTX 4060 host. The end-to-end public path was measured on both the RTX 4060 and the L4.

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86%. Comparing base (d5cc00b) to head (0e19df2).

Additional details and impacted files
@@           Coverage Diff           @@
##           develop   #1419   +/-   ##
=======================================
  Coverage       86%     86%           
=======================================
  Files          114     114           
  Lines        15008   15009    +1     
=======================================
+ Hits         12961   12962    +1     
  Misses        2047    2047           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Borda
Borda requested a balanced review from Copilot September 3, 2026 12:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The focused optimization preserves eval-mode correctness and is adequately covered by regression tests.

Pull request overview

Optimizes unoptimized inference by avoiding redundant recursive eval-mode assignments while preserving mixed-mode correction.

Changes:

  • Scan the module tree before calling eval().
  • Add regression coverage for already-evaluated and mixed-mode trees.
  • Document measured inference improvements.
File summaries
File Description
src/rfdetr/detr.py Conditionally applies eval() only when needed.
tests/inference/test_predict_eval_mode.py Covers skipped assignments and mixed-mode restoration.
CHANGELOG.md Records behavior and benchmark results.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@Borda
Borda merged commit e74e2a0 into roboflow:develop Sep 3, 2026
41 checks passed
@Borda Borda added the enhancement New feature or request label Sep 3, 2026
@Borda Borda mentioned this pull request Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants