Stop ML jobs failing with CUDA out-of-memory when workers share one GPU - #162
Open
mihow wants to merge 3 commits into
Open
Stop ML jobs failing with CUDA out-of-memory when workers share one GPU#162mihow wants to merge 3 commits into
mihow wants to merge 3 commits into
Conversation
Analysis and mitigation plan for CUDA out-of-memory failures observed in a production deployment (August 2026) where several worker processes share one GPU. Establishes from code reading that the failures are a peak-working-set problem (forward passes sized by the API fetch batch) rather than a leak, and lays out the chunked-inference fix implemented in the follow-up commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HcXFHJRXMrsHPX7xz9ZifF
…ce, and per-job release The Antenna worker ran every GPU forward pass at the size of the API fetch batch (antenna_api_batch_size, default 24 full-resolution images) and stacked every detection crop from the batch into a single classifier call. The localization_batch_size and classification_batch_size settings were never applied on this path, so a GPU shared by several worker processes could be exhausted whenever more than one process was mid-batch. Observed in a production deployment (August 2026) as repeated CUDA out-of-memory failures on a 24 GiB GPU shared by four worker processes. - Apply localization_batch_size / classification_batch_size to the worker's models and slice every detector and classifier forward pass into chunks of that size (_predict_in_chunks), with a bounded halve-and-retry fallback when a chunk still hits a CUDA memory-allocation failure. - Release per-job GPU state in _process_job's finally block so a process idling between jobs returns its cached VRAM to co-tenant processes. - Default the CUDA allocator to expandable_segments:True to reduce fragmentation; operator-set allocator environment variables always take precedence. - Update the data-loading pipeline docs to match, and give the test settings fixtures real integers for the batch-size settings and for antenna_result_post_max_bytes (ResultPoster compares payload sizes against it, which a MagicMock attribute cannot support). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HcXFHJRXMrsHPX7xz9ZifF
…fault Finish the GPU memory bounding started in the previous commit, and remove the expandable_segments allocator default after it failed in production. - Add _classify_crops_in_chunks(): the binary filter and terminal classifier now slice, transform, stack, and classify detection crops one chunk at a time instead of building every crop tensor up front and only chunking the forward pass. With hundreds of detections per image, the up-front crop stack was itself an unbounded allocation. - Remove the expandable_segments:True allocator default. On NVIDIA vGPU (which does not expose the virtual-memory-management driver APIs) it raises "CUDA driver error: operation not supported" at CUDA initialization instead of falling back; observed identically on two production hosts. The worker now sets nothing and logs the effective PYTORCH_ALLOC_CONF / PYTORCH_CUDA_ALLOC_CONF values at startup, so an operator-set value remains visible and possible on supported hardware. - Clear the CUDA prefetcher's buffered next batch explicitly in _process_job's finally block; the buffer holds a full fetch batch of images on the GPU. - Add gpu_memory_bench.py, a multi-process GPU pressure benchmark that reproduces the shared-card exhaustion signature with the real worker code path and an in-process mock API, for before/after comparison on real hardware. - Correct the planning doc: the allocator-default item is withdrawn with the observed error string recorded, and the measured production results of the process-count/batch-size mitigation are added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HcXFHJRXMrsHPX7xz9ZifF
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When several worker processes share one GPU, ML jobs can fail with waves of
CUDA out of memoryerrors. The root cause is that the worker ran every GPU forward pass at the size of the API fetch batch (AMI_ANTENNA_API_BATCH_SIZE, default 24 full-resolution images) and stacked every detection crop from the batch into a single classifier call. TheAMI_LOCALIZATION_BATCH_SIZEandAMI_CLASSIFICATION_BATCH_SIZEsettings — which exist precisely to cap GPU memory — were silently ignored on the worker path. In a production deployment (August 2026), this appeared as a job crawling at 3.1 images/min with 512 out-of-memory errors before being killed as stale at 7% progress: the peak working set of a busy process was 13–17.4 GiB, on a 24 GiB card shared by four worker processes.This PR makes the batch-size settings actually govern the forward-pass size, chunks crop classification so dense images can't blow the peak, retries with smaller chunks under transient pressure from a co-tenant process, and releases GPU memory at the end of every job so idle processes stop holding VRAM their neighbours need.
Important framing: this is a peak working set problem, not a leak. The failing allocation was the same 426 MiB size, 504 times, at a stable 13.49 GiB allocated — a fixed peak colliding with a co-tenant on a shared card, not upward drift. A separate, unrelated defect (host RAM growing with work done in the DataLoader subprocesses, which never touch CUDA) is addressed independently — see PR #163 (worker drain-and-recycle, implementing #147) and PR #148. The two were conflated in earlier triage; they have different memory, different processes, different mechanisms, and different fixes.
List of Changes
AMI_LOCALIZATION_BATCH_SIZE/AMI_CLASSIFICATION_BATCH_SIZE(mirroring the synchronous API path), and a chunked-inference helper (_predict_in_chunks) slices every forward pass to that size.AMI_ANTENNA_API_BATCH_SIZEnow controls only fetch/transfer granularity._classify_crops_in_chunks) instead of every crop being built up front — the detector permits up to 500 detections per image, so the up-front stack was unbounded in practice.expandable_segments:True; deployed on NVIDIA vGPU (H100 24 GB vGPU profile) it raisesCUDA driver error: operation not supportedat CUDA initialization — PyTorch does not fall back. That default is removed; the worker now only logs the effectivePYTORCH_ALLOC_CONF/PYTORCH_CUDA_ALLOC_CONFvalues at startup so an operator opt-in stays visible.trapdata/antenna/gpu_memory_bench.py) runs N worker processes against an in-process mock API on one GPU to reproduce the shared-card exhaustion signature and compare configurations or revisions.trapdata/antenna/tests/test_gpu_memory.py).Measured results from production
The operational half of this mitigation shipped ahead of the code: one worker process per 24 GiB card instead of two, and fetch batch size 8. Measured outcome: a production job with ~3,200 large images completed at 88.3 images/min with zero CUDA errors (its previous run: 3.1 images/min, 512 OOM errors, killed at 7%); a ~400-image job completed at 103.4 images/min with zero errors. Throughput went up with half the processes — the second process was causing thrash, not adding capacity. The code changes in this PR bound the per-process peak so that process count and batch sizes have predictable headroom instead of relying on process count alone.
What still needs verification
nvidia-smiduring a real job.gpu_memory_bench.pyon real hardware.Test and lint results
trapdata/antenna/tests/test_gpu_memory.py: 15 passed.uv run pytest --import-mode=importlib): 60 passed, 1 skipped, 1 failed — the failure istrapdata/api/tests/test_models.py::TestSourceImageSchema::test_url, which live-fetches an image from Wikimedia and received an HTTP 400 from the external server; pre-existing environmental flake, unrelated to this branch.Base branch
Opened against
fix/result-post-chunking(PR #149) — this branch is stacked on it, so the diff here shows only this PR's work. Merge #149 first, then this PR can retargetmain.🤖 Generated with Claude Code
https://claude.ai/code/session_01HcXFHJRXMrsHPX7xz9ZifF