Conversation
Changes: - Guard LightweightDataset's annotation_refs and cpp_cache with a mutex held by every public method, so concurrent Python threads can no longer corrupt them. - Return get_cpp_annotations by value instead of by reference; the cached entry it exposed can be erased by clear_cache_entry from another thread, and every caller already copied the result. - Add a private get_cpp_annotations_locked helper so get_cpp_instances takes one lock for its whole sweep rather than one per image/category pair. - Add move construction and assignment that transfer the containers under the source's lock and leave each object its own mutex, restoring the movability the pickle __setstate__ factory needs; copying is now explicitly deleted. - Declare pybind11::mod_gil_not_used() on faster_eval_api_cpp and mask_api_new_cpp. - Raise the pybind11 build requirement to >=2.13.0, the first release exposing mod_gil_not_used. - Advertise Python 3.14 in the classifiers. - Add TestDatasetConcurrency exercising concurrent readers and reads interleaved with cache eviction. Rationale: - An extension that does not declare Py_MOD_GIL_NOT_USED makes CPython re-enable the GIL for the whole process when it is imported, silently disabling free-threading for every library in that process. The pinned cibuildwheel builds cp314t wheels by default and no skip pattern excludes them, so those wheels would ship undeclared. The declaration is a thread-safety promise, so the dataset cache had to be guarded before making it. - mask_api needed no locking change: its only process-wide mutable state is ParallelPermitPool, already guarded by a mutex and condition variable because those APIs release the GIL and are entered concurrently. Impact: - Measured on a 2,000 image x 300 detection synthetic bbox workload, maxDets=[1,10,500], interleaved A/B over frozen builds, 6 rounds each, medians: evaluate 0.85495s -> 0.85492s (-0.00%); _prepare 0.32861s -> 0.33304s (+1.35%, within a median absolute deviation of 0.0067 and 0.0045). - Evaluator output digest over precision, recall, scores and summary stats is unchanged. Verification: - Full suite: 204 passed, 2 skipped. - Pickle round trip exercises the new move constructor and preserves both stored annotations and their parsed form. - ruff check and ruff format clean. Residual limits: - pybind11 compiles the declaration under #ifdef Py_GIL_DISABLED, so on a GIL-enabled interpreter it is inert and its runtime effect could not be exercised here; verifying it needs a free-threaded build, which this host does not have. - The concurrency tests run under the GIL, so they demonstrate absence of deadlock and correct results rather than absence of data races. - Locking is per dataset instance; append_ref takes the lock once per annotation, which is the source of the small _prepare change. --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Mutex-protected Python callbacks can deadlock, cache invalidation is incomplete, and the claimed pickle path lacks coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Declares both native modules compatible with free-threaded CPython and synchronizes shared dataset state.
Changes:
- Adds
mod_gil_not_useddeclarations and Python 3.14 metadata. - Adds dataset locking, safe cache returns, and explicit move operations.
- Adds concurrency regression tests and raises the pybind11 minimum version.
File summaries
| File | Description |
|---|---|
csrc/faster_eval_api/coco_eval/dataset.cpp |
Implements synchronization and move behavior. |
csrc/faster_eval_api/coco_eval/dataset.h |
Defines mutex-backed dataset API. |
csrc/faster_eval_api/faster_eval_api.cpp |
Declares free-threading support. |
csrc/mask_api/mask_api.cpp |
Declares free-threading support. |
pyproject.toml |
Raises pybind11 build requirement. |
setup.py |
Adds Python 3.14 classifier. |
tests/test_cpp_safety_regressions.py |
Adds concurrent cache tests. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 6
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Changes: - Refactor native Dataset reads, parsing, cleaning, and tuple loading to snapshot or swap state under the mutex while performing Python conversion and reference destruction after unlock; invalidate per-key parsed cache entries and version cache publication after append. - Remove unused Dataset move assignment while retaining move construction for pickle restore. - Add spawned-process, barrier-synchronized regressions for finalizer and conversion re-entry, concurrent cache publication, append invalidation, and public pickle round trips. - Add a non-publishing CPython 3.14t CI job that asserts no-GIL mode, imports both extensions with warnings as errors, installs from the sdist, and runs focused Dataset concurrency tests. - Correct the native free-threading comment to describe CPython warning and GIL re-enable behavior. Impact: - Prevents self-deadlocks and stale native annotation results when Python callbacks or writers interact with Dataset. - Makes deadlock regressions externally bounded and exercises the pybind pickle move path. - Adds executable CI coverage for the declared free-threaded compatibility without publishing artifacts. Verification: - Native extension build passed; 9 Dataset concurrency tests and 33 Dataset/safety tests passed. - Full suite passed: 211 passed, 2 skipped. - pre-commit run --all-files passed, including Ruff, docformatter, mdformat, and clang-format; git diff --check passed. Residual limits: - The hosted CPython 3.14t job and TSan have not executed; local runtime tests used a GIL-enabled interpreter. - Independent QA/challenger provenance remains unverified because current host events do not satisfy the installed validator. --- Co-authored-by: Codex <codex@openai.com>
There was a problem hiding this comment.
🟡 Changes recommended
Move assignment is missing, the promised single-lock sweep is absent, and merged annotations incur an avoidable extra copy.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
csrc/faster_eval_api/coco_eval/dataset.cpp:299
- This makes the returned vector
constand then copies everyInstanceAnnotationintomerged. Sinceget_cpp_annotationsnow returns an owned vector, this branch can still move its elements as before; otherwise the API change adds a second O(number of annotations) copy for every merged category sweep.
const std::vector<InstanceAnnotation> anns =
get_cpp_annotations(img_id, cat_id);
merged.insert(merged.end(), anns.begin(),
anns.end());
- Files reviewed: 8/8 changed files
- Comments generated: 2
- Review effort level: Balanced
Changes: - Extend standard sdist CI through Python 3.14 and run the free-threaded job on both 3.13t and 3.14t. - Verify native modules load from the installed sdist outside the checkout and do not re-enable the GIL. - Remove the unrelated Python 3.14 classifier and evaluator-source formatting churn while preserving the minimal mod_gil_not_used declaration. Impact: - Separate ordinary Python 3.14 compatibility failures from no-GIL-specific failures across the hosted platforms. - Prevent checkout shadowing from producing false-positive free-threaded results and keep the PR review focused on required runtime safety. Verification: - Final workflow passed check-yaml; the staged diff passed git diff --cached --check. - CPython 3.14.2t kept the GIL disabled after both native imports and passed 16 safety regressions. - CPython 3.10.11 full suite passed 211 tests with 2 skips. Residual limits: - Hosted standard 3.14 and free-threaded 3.13t/3.14t jobs have not run on this local revision. - The paste-ready PR description remains an ignored local artifact under .plans/active. --- Co-authored-by: Codex <codex@openai.com>
There was a problem hiding this comment.
🟡 Changes recommended
The pybind11 cap excludes Python 3.14 support, and free-threaded CI does not exercise concurrent evaluator or mask operations.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 2
- Review effort level: Balanced
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Changes: - Add a subprocess-timeout regression that releases evaluator and mask operations concurrently and asserts their concrete outputs. - Run that regression in the free-threaded native stress-test selection. Impact: - A native deadlock in concurrent evaluator or mask entry points now fails within the existing 15-second process bound instead of hanging CI. - CPython 3.13t/3.14t CI exercises both no-GIL module surfaces, not only Dataset cache contention. Verification: - pytest -p no:rerunfailures -q tests/test_cpp_safety_regressions.py: 17 passed. - pre-commit run --files .github/workflows/unittest.yml tests/test_cpp_safety_regressions.py: passed. - git diff --check: passed. Residual limits: - Hosted CPython 3.13t/3.14t CI remains required for direct free-threaded runtime validation. --- Co-authored-by: Codex <codex@openai.com>
espressolee
left a comment
There was a problem hiding this comment.
I independently validated exact base 123cc73a1da42420d77e230c9d02cba8b4d83ac3 against exact head ae7242631b374075c6c7f2a134a8c79f9e18095a on macOS arm64 with CPython 3.14.6 and 3.14.6t.
The primary compatibility result checks out: the base extension re-enables the GIL on 3.14t, while this head imports both native modules with the GIL still disabled. The head passed tests/test_cpp_safety_regressions.py (17/17) and the full local suite (177 passed, 28 skipped) on both regular and free-threaded 3.14.6. As positive controls, the two new stale-cache/append-invalidation tests fail against the base and pass against the head; the exact-head hosted matrix is also green.
I found one blocking liveness regression in the cache-publication retry. annotation_version is global to the dataset, so an append to an unrelated key invalidates the snapshot for the key currently being converted. Because conversion intentionally runs arbitrary Python hooks outside the mutex, a conversion hook that appends to a different key can invalidate every attempt and make get_cpp_annotations() retry forever.
A subprocess reproducer against the exact head timed out in 5/5 runs on both 3.14.6 and 3.14.6t. The same scenario returned in 5/5 runs on the base, and a one-shot mutation control returned in 5/5 runs on the head, so this is repeated global invalidation rather than ordinary conversion latency. Minimal shape:
dataset = _eval.Dataset()
class Area:
def __float__(self):
dataset.append_ref(
999, 1, {"id": 2, "area": 1.0, "iscrowd": 0}
)
return 10.0
dataset.append_ref(
1, 1, {"id": 1, "area": Area(), "iscrowd": 0}
)
dataset.get_cpp_annotations(1, 1) # does not return at this headThe retry is at csrc/faster_eval_api/coco_eval/dataset.cpp:250: it compares the snapshot against a dataset-wide generation that every append_ref() increments.
A scratch proof using a global replacement epoch for clean/load_tuple plus per-key append versions makes both the repeated-hook reproducer and the one-shot control return, while retaining 17/17 focused tests and 177 passed / 28 skipped in both interpreter modes. An equivalent target-key identity/version check would also address the issue.
Continuous mutation of the requested key may reasonably require retry/abstention semantics; mutation of an unrelated key should not be able to starve this read. My local scope was macOS arm64; the PR's exact-head hosted cross-platform checks are green.
|
|
||
| - name: Run synchronized native stress regressions | ||
| working-directory: ./tests | ||
| run: python -m pytest -q test_cpp_safety_regressions.py -k "TestDatasetConcurrency or test_concurrent_evaluator_and_mask_operations" |
There was a problem hiding this comment.
| run: python -m pytest -q test_cpp_safety_regressions.py -k "TestDatasetConcurrency or test_concurrent_evaluator_and_mask_operations" | |
| run: python -m pytest -q test_cpp_safety_regressions.py -k "TestDatasetConcurrency" |
| "Cython", | ||
| "pybind11>=2.12.0, <3", | ||
| # 3.0.0 is the first release supporting CPython 3.14 and 3.14t. | ||
| "pybind11>=3.0.0, <4", |
There was a problem hiding this comment.
| "pybind11>=3.0.0, <4", | |
| "pybind11>=2.13.0; python_version<'3.14'", | |
| "pybind11>=3.0.0; python_version>='3.14'" |
| for (const auto& kv : annotations) { | ||
| auto key = kv.first; | ||
| auto ann_list = kv.second; | ||
|
|
There was a problem hiding this comment.
| const auto& key = kv.first; | |
| const auto& ann_list = kv.second; |
Summary
Enable
faster_coco_evalto load on free-threaded CPython without re-enabling the GIL for the entire process.Datasetcache and avoid exposing cache references after their lock is released.mod_gil_not_used.Why this matters
Before this change, importing either native extension on CPython 3.14t warns and enables the GIL process-wide. Evaluation results remain correct, but other Python threads lose free-threaded execution. With this change, the GIL remains disabled after both extensions are imported.
The expected benefit is concurrent throughput, not lower latency for one COCO evaluation.
Python 3.14t benchmark
CPython 3.14.2 free-threaded on macOS arm64. Each row uses two warmups and 15 alternating fresh-process base/PR pairs. The bbox workload contains 200 images and 10,000 detections with
maxDets=[1, 10, 50]; each worker owns an independent evaluator.Four concurrent evaluations complete in 0.13760 seconds with the PR versus 0.52048 seconds on the base revision. Every run produced the same exact result digest.
Standard CPython 3.13.15 also produced the same digest. An extended alternating A/B run measured a 0.91% total-time change and a -0.03% peak-RSS change, so the synchronization cost remained below the predeclared 3% regression guard.
Benchmark revisions:
123cc73a1da42420d77e230c9d02cba8b4d83ac3b67e5d163c0ee350051962e8f1351188a0ddeec0Safety and reliability
Datasetcontainers and cache versions are mutex-protected.Verification
16 passedintest_cpp_safety_regressions.py.211 passed, 2 skipped.Claim boundary
This PR does not make one evaluation materially faster and does not claim universal linear scaling, shared-mutable-
Datasetscaling, or cross-platform equivalence. The measured 1.61-3.76x gain applies to 2-4 independent evaluators on one synthetic bbox workload and one macOS arm64 host.