fix(inference): serialise the deferred weight move across threads - #1564
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #1564 +/- ##
=======================================
Coverage 90% 90%
=======================================
Files 136 136
Lines 18070 18088 +18
=======================================
+ Hits 16219 16236 +17
- Misses 1851 1852 +1 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The pre-lock device check can still let inference proceed while later parameters are moving.
Review effort: Balanced
Findings: 1
Open (3)
What changed in this PR
Serializes deferred model device moves to prevent concurrent weight corruption.
Changes:
- Adds locking and device re-checking around lazy device placement.
- Adds a concurrency regression test.
- Documents the fix in the changelog.
| File | Description |
|---|---|
src/rfdetr/detr.py |
Adds device-move synchronization. |
tests/inference/test_model_device_move.py |
Tests concurrent initial moves. |
CHANGELOG.md |
Records the concurrency fix. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…it for a move in progress
|
Addressed the Copilot review in 66e24f2:
|
|
GPU check of the fix (RTX 4070 Ti SUPER, torch 2.14 + CUDA 13.0,
Without the lock (same script on a branch that does not contain it), four threads left 11 of 573 tensors corrupted and every thread returned 0 detections. This is one run on one frame; I have not run the repo's GPU test suite. |
…zy-device-move # Conflicts: # CHANGELOG.md
[resolve No.8] Review by foundry:sw-engineer (PR roboflow#1564): "[LOW] Module-level lock plus fork() is an inherited-locked-mutex hazard ..." Challenge: evidence=VALID suggestion=VALID resolution=codex-direct --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: OpenAI Codex <codex@openai.com>
[resolve group] PR roboflow#1564 — items 9 10 --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
[resolve group] PR roboflow#1564 — items 4 5 6 7 --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Resolving the item-5 cherry-pick conflict (No.5, per-module WeakKeyDictionary lock) took the incoming declaration wholesale and discarded the fork-safety note item 8 had appended to the old single-lock declaration it replaced. Re-added to the new _MODULE_MOVE_LOCKS comment block, reworded for the per-module lock it now documents. --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
The origin/develop merge conflict resolution accidentally created a second `### Fixed` header under `## [Unreleased]` and duplicated the roboflow#1564 weight-move bullet. Collapse back to one `### Fixed` section with the new cuDNN bullet placed above the existing roboflow#1564 entry. --- Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>


Summary
The model's weights move from CPU to the accelerator on the first
predict(),inference()orexport()call (_move_model_context_to_device). That move takes no lock. When several threads share one model (for example one camera channel per thread) and reach that first call together, they run overlapping in-placenn.Module.to()calls on the same module.Repro on a cold segmentation model: four threads call
predict()at once. There is no exception and no warning, but 13 of the 573state_dicttensors come out corrupted (values around 1e24–1e28), and the model then returns no detections on every frame. The sequential baseline returns 23–25 detections per frame on the same inputs. I did not trace the byte-level mechanism, only the outcome.Changes
_move_model_context_to_devicetakes a module-level lock for the move and re-checks the device under it, so later callers see the move as done instead of repeating it.torch.inference_mode(False)guard around the move is unchanged.CHANGELOG.mdentry.Verification
TestMoveModelContextConcurrency::test_concurrent_first_calls_move_exactly_once: four threads race on a cold stand-in module whose.to()yields for 0.2 s. It failed before the fix (assert 4 == 1) and passes after.pytest src/ tests/ scripts/ -n 3 -m "not gpu and not coco17 and not integration and not xla and not tpu" ...: 6531 passed, 128 skipped.pre-commit run --all-files: passed.