Fix: torch.compile errors in threaded multi-GPU runs - #22
Closed
carshadi wants to merge 4 commits into
Closed
Conversation
Make torch.compile safer in the multi-GPU threaded inference pipeline by avoiding CUDA graph modes, serializing first-use compilation per worker, and keeping compiled input shapes constant. PyTorch compile modes such as reduce-overhead and max-autotune enable CUDA graphs, which can fail in this threaded multi-GPU worker model with Inductor cudagraph tree/thread-local assertions. Default compile mode is now "default", and multi-CUDA compiled runs automatically downgrade unsafe modes: reduce-overhead -> default and max-autotune -> max-autotune-no-cudagraphs. Move compiled model first-use warmup into GpuWorker setup. Each copied model is moved to its assigned CUDA device, wrapped with torch.compile, and immediately run once with a dummy input on that device before worker threads start consuming real batches. This prevents concurrent lazy Dynamo/FX tracing from occurring in gpu-* threads. Pad PrepWorker tail batches to the configured batch_size so compiled models always see a constant input shape. Writers continue to use starts_in_block and valid_sizes, so padded rows are ignored downstream. This avoids runtime recompiles caused by smaller final batches. Also update compile configuration and CLI behavior: - add explicit compile mode typing - expose max-autotune-no-cudagraphs - replace the old boolean dynamic flag with --compile-dynamic auto|true|false - default dynamic behavior to auto for one static-specialized graph when shapes remain constant Add regression coverage for multi-CUDA compile mode downgrades and padded tail batch behavior. Tests: - conda run --no-capture-output -n compression pytest tests
Resolve add/add conflict in tests/test_workers.py: keep both the PrepWorker tail-batch padding test (this branch) and the WriterWorker output-channel mismatch test (main); combine imports. The torch.compile stabilization (constant input shape via padded tail batches, per-worker warmup, multi-CUDA compile-mode downgrade) composes with main's multi-output writer support: GpuWorker sizes host_out to the actual model output and writers only index starts_in_block, so padded rows are ignored. All 13 tests pass under the compression env. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- not all models are compile-friendly, e.g., the proteomics shared encoder model
- Padding the tail batch to a fixed size can introduce a very slight performance hit when not using compilation.
Member
Author
|
Closing to reopen against |
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.
Resolves #20
Summary
The inference pipeline (
run.py) parallelizes across GPUs by launching oneGpuWorkerthread per device, each holding adeepcopyof the model and optionally wrapping it withtorch.compile. This combination — threads × multiple GPUs ×torch.compile— exposed several failure modes, ranging from hard crashes to a model that simply cannot be compiled.This branch:
torch.compilerecompile/threading crash by keeping model input shapes constant and warming the compiled model synchronously.torch.compile.Problems addressed and how they're fixed
1. Crash: FX symbolic trace of a dynamo-optimized function
Running the multi-GPU denoising pipeline with
torch.compileand static shapes produced runtime recompile warnings followed by a hard crash in one of the GPU threads:Root cause. With dynamic shapes disabled, every distinct input shape forces a fresh compilation. Because each block's patch count rarely divides evenly by
batch_size, the last batch of every block is smaller (expected 32, actual 4), so dynamo recompiled at runtime — and once pastrecompile_limit (8)it fell back to eager for new shapes. Critically, those recompiles happened inside the concurrentgpu-*threads. Dynamo's compile path runs an FX/proxy-tensor trace that monkeypatchestorch.nn.Module.__call__process-wide, so while one thread was mid-compile, another thread's ordinaryforwardcall was intercepted by that tracer — producing the FX symbolically trace a dynamo-optimized function" error, killing the thread, and stalling the blocks whose batches it was holding.Fixes (complementary):
PrepWorkernow pads the final (short) batch of each block up tobatch_sizewith zeros when compiling, so the model only ever sees one input shape. With shapes constant, no runtime recompiles happen at all, which removes the recompile stalls and the concurrent-compile race. Padded rows are ignored downstream:WriterWorkeronly reads rows present instarts_in_block.GpuWorker._warmup_compiled_modelruns one forward pass at the canonical shape during construction (the workers are built sequentially on the main thread). This forces the initial compilation to happen serially rather than racing lazily across worker threads on their first batch.Together these guarantee that by the time the worker threads start, every model is already compiled, and no further compilation is ever triggered.
2. CUDA-graph compile modes are unsafe under threaded multi-GPU
reduce-overheadandmax-autotuneenable CUDA graphs, which are fragile when capture/replay happens from multiple threads, and which return outputs backed by static buffers that the next replay overwrites — hazardous given thatGpuWorker.runhands the output off to an asynchronous D2H copy on a separate stream while the next batch begins.Fix.
InferenceConfignow downgrades CUDA-graph-enabling modes to their non-graph equivalents when more than one CUDA device is configured, with aRuntimeWarning:reduce-overhead→defaultmax-autotune→max-autotune-no-cudagraphs3. Crash: a model
torch.compilecannot trace (proteomics MAE encoder)The proteomics pipeline uses an MAE ViT encoder with RoPE positional embeddings. Compiling it broke first with a (non-fatal) graph break and then a fatal guard assertion:
Root cause. The encoder does host-side numpy/Python work inside
forwardthat dynamo cannot trace as tensor ops:float(self.resolution_z)converts a tensor to a Python scalar (the graph break), andself.grid_sizeis a numpy array (mae_encoder.py:80) that gets indexed and fed into the RoPE module. Dynamo auto-wraps the numpy array viatorch.from_numpy(...)and installs a guard on it; that guard trips a known dynamo bug (dispatch-key-set mismatch onthe same frame that created it), aborting compilation. This is intrinsic to the model and independent of the threading/shape fixes above — the model is simply not traceable as written.
Fix.
GpuWorker._compile_modelnow wraps both thetorch.compile()call and the warmup forward (where lazy compilation actually executes) in atry/except. On any failure it logs a warning and restores the original eagermodule, so the run proceeds in eager mode instead of crashing:
Because warmup runs on the main thread, the failure and fallback both happen there, before any worker thread executes — so a failed compile never reintroduces the threading hazard.
Dynamic-shape defaults
The handling of
torch.compile(dynamic=...)was reworked:dynamic=Truetodynamic=None(automatic). With inputs now shape-constant, automatic dynamic compiles a single static, shape-specialized graph at warmup (fastest kernels) and would only promote to a dynamic graph if an unexpected shape ever appeared — the most graceful degradation of the three options.compile_dynamicis nowOptional[bool](None= auto /True= force dynamic /False= recompile per shape).compile_modechanged fromreduce-overheadtodefault(the former silently enabled the unsafe CUDA-graph path under multi-GPU).Behavioral / breaking changes
--no-compile-dynamicis removed. Use--compile-dynamic falsefor the old behavior, or the new default--compile-dynamic auto. Any caller still passing--no-compile-dynamicwill get an argparse error.
compile_modeis nowdefault(wasreduce-overhead).compile_dynamicis nowauto/None(wasTrue).runs eager (one warning + traceback per GPU worker).
Benchmarks
Run on a g6.12xlarge (4X L4 GPUs)
Test script - baseline with no compile
2026-06-24 23:45:47,258 - main - INFO - Total time: 608.65s
2026-06-24 23:45:47,258 - main - INFO - Throughput: 127.02MB/s
With compile (uncomment above options):
2026-06-24 22:01:54,000 - main - INFO - Total time: 498.90s
2026-06-24 22:01:54,000 - main - INFO - Throughput: 154.97MB/s
So a ~22% boost using the default options.