[PyTorch] Fix FP8 illegal memory access in single-process multi-GPU execution - #3469
Open
SuperGoodGame wants to merge 6 commits into
Open
[PyTorch] Fix FP8 illegal memory access in single-process multi-GPU execution#3469SuperGoodGame wants to merge 6 commits into
SuperGoodGame wants to merge 6 commits into
Conversation
Contributor
Greptile SummaryThe PR makes FP8 execution device-aware for single-process multi-GPU models and repairs cleanup after forward preparation fails.
Confidence Score: 5/5The PR appears safe to merge. The previously reported device-guard leak is fixed because Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[prepare_forward receives input] --> B{Input device differs from current device?}
B -- Yes --> C[Enter CUDA device guard]
B -- No --> D[Push empty guard entry]
C --> E[Push guard entry]
D --> F[Prepare FP8 state]
E --> F
F --> G{Preparation succeeds?}
G -- No --> H[Pop NVTX if needed]
H --> I[Release device guard]
G -- Yes --> J[Run module forward]
J --> K[end_forward in finally]
K --> L[Restore recompute state]
L --> M[Pop NVTX range]
M --> I
Reviews (4): Last reviewed commit: "[PyTorch] Validate mixed-device amax ord..." | Re-trigger Greptile |
…s device Single-process multi-GPU execution (e.g. accelerate.dispatch_model) leaves the ambient current CUDA device different from a module's device. TE resolves kernel-launch context, RTC cache lookups and recipe-state allocation from the ambient device, so a module placed off the current device crashes with an illegal memory access, and RecipeState.create() without an explicit device allocates scale/amax_history buffers on the current device instead of the module's. Pin the input's device for the duration of the forward (prepare_forward / end_forward, a stack so nesting and recompute double-forwards stay balanced; zero cost when the module is already on the current device), and derive the recipe-state device from the module's own parameters/buffers. Fixes NVIDIA#3124 Signed-off-by: SuperGoodGame <985236470@qq.com>
global_amax_buffer collects amax tensors from every module registered under an autocast, so when modules live on several CUDA devices in one process the torch.cat over the buffer fails, and the fused update launches from the wrong device. Whether a buffer spans devices is now tracked at registration time (global_amax_devices set; O(1) to check at exit instead of an O(N) scan on every autocast exit). Single-device buffers take today's code path unchanged. Multi-device buffers without a distributed amax reduction (the common single-process case) are finalized per device group with local cat + fused update only -- no cross-device copies, no collectives. With a distributed reduction, each device's entries are gathered to the first-registered module's device (local cat, one D2D into a staging buffer, local index_copy_ at the original logical offsets), reduced by the same single collective over the same logical order as before, and scattered back (one D2D per device) before the per-device update. Collective count, order, size and semantics are unchanged; the cross-rank registration compatibility requirement is unchanged. Part of NVIDIA#3124 Signed-off-by: SuperGoodGame <985236470@qq.com>
Four tests on >=2 visible GPUs, never calling torch.cuda.set_device and never enabling peer access, matching accelerate.dispatch_model placement: module off the current device (NVIDIA#3124 case A), two modules on different devices in one autocast (case C), bit-identical 1-GPU vs 2-GPU numerics, and a mock-collective check that the multi-device gather/reduce/scatter orchestration (interleaved device registration order) matches the fully local path bit-for-bit while issuing exactly one stock-shaped collective per direction. Buffer invariants assert per-index device consistency, the amax row-0 view relationship, and that each module's registered position still refers to its own scale/history objects after the update. Part of NVIDIA#3124 Signed-off-by: SuperGoodGame <985236470@qq.com>
for more information, see https://pre-commit.ci
Signed-off-by: SuperGoodGame <985236470@qq.com>
SuperGoodGame
force-pushed
the
fix/single-process-multi-gpu-fp8
branch
from
September 3, 2026 08:52
da96593 to
dc980e4
Compare
Signed-off-by: SuperGoodGame <985236470@qq.com>
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
Fixes #3124.
Single-process multi-GPU execution (for example,
accelerate.dispatch_modelor plaindevice_mapplacement) can trigger a CUDA illegal memory access during FP8 training when a TE module is oncuda:1but the ambient current device iscuda:0. The same model works on a single GPU.This PR fixes the three independent device assumptions in TE's Python FP8 path.
Root causes and fixes
A. Execution-device mismatch
FP8 forward setup and runtime-compiled kernel lookup use the ambient CUDA device.
prepare_forwardnow temporarily pins the current device to the input device when they differ. The guard is released byend_forward, and is also released if preparation itself raises. The guard stack keeps nested forwards and activation recomputation balanced.B. Recipe-state placement
RecipeState.createpreviously defaulted to the ambient CUDA device. Persistent state such asscaleandamax_historyis now allocated from module-owned parameters/buffers. This applies to both the legacy TE module API and thepytorch.opsAPI, including quantized-weight initialization. Checkpoint restore placement is fixed for module-owned recipe states whose execution device can be derived from parameters or buffers; parameterless modules such as DPA remain a follow-up.The standalone operation fuser also runs forward and backward work under the input/gradient device context.
C. Global amax finalization
Registered amax tensors can belong to different CUDA devices, so a single
torch.catand fused update is not always valid.all_reduce, then scattered back before per-device updates.global_amax_devicesis rebuilt when registered tensors are replaced so the device index cannot become stale.Ablation
The following was observed with a module on
cuda:1and ambient devicecuda:0, enabling one fix at a time:Testing
tests/pytorch/test_multi_device_fp8.pynow contains seven focused tests:prepare_forwardraises.pytorch.ops.BasicLinear.Additional validation performed locally:
test_multi_device_fp8.py: 6 passed.test_custom_recipe.py: 25 passed.tests/pytorch/distributed/run_multi_device_fp8.pyruns 2 processes × 2 GPUs per process for 10 forward/backward iterations with one all-reduce per direction; both ranks produced the same final hash.Run the real-NCCL check with:
Performance
No stable performance regression was observed in a CUDA-event microbenchmark against the PR's original
HEAD. The normal TE module path only adds cleanup around exceptional paths; the ops path adds a conditional current-device check per fuser call. The measured variation was within run-to-run noise.Known environment limitation
The current test environment uses cuDNN 9.10.2. Some strict CUDA-graph attention comparisons require cuDNN 9.15.1 or newer and fail with small eager versus graph numerical differences. The representative failure reproduces on the unmodified base commit as well.
Related process-global caches whose keys do not include a device are outside this PR and should be handled separately.