Skip to content

[PyTorch] Fix FP8 illegal memory access in single-process multi-GPU execution - #3469

Open
SuperGoodGame wants to merge 6 commits into
NVIDIA:mainfrom
SuperGoodGame:fix/single-process-multi-gpu-fp8
Open

[PyTorch] Fix FP8 illegal memory access in single-process multi-GPU execution#3469
SuperGoodGame wants to merge 6 commits into
NVIDIA:mainfrom
SuperGoodGame:fix/single-process-multi-gpu-fp8

Conversation

@SuperGoodGame

@SuperGoodGame SuperGoodGame commented Sep 3, 2026

Copy link
Copy Markdown

Summary

Fixes #3124.

Single-process multi-GPU execution (for example, accelerate.dispatch_model or plain device_map placement) can trigger a CUDA illegal memory access during FP8 training when a TE module is on cuda:1 but the ambient current device is cuda: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_forward now temporarily pins the current device to the input device when they differ. The guard is released by end_forward, and is also released if preparation itself raises. The guard stack keeps nested forwards and activation recomputation balanced.

B. Recipe-state placement

RecipeState.create previously defaulted to the ambient CUDA device. Persistent state such as scale and amax_history is now allocated from module-owned parameters/buffers. This applies to both the legacy TE module API and the pytorch.ops API, 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.cat and fused update is not always valid.

  • Without distributed reduction, entries are grouped and finalized locally on each owning device. No D2D copies or collectives are added.
  • With distributed reduction, entries are gathered in registration order to the first registered device, reduced with the existing single all_reduce, then scattered back before per-device updates.
  • global_amax_devices is rebuilt when registered tensors are replaced so the device index cannot become stale.

Ablation

The following was observed with a module on cuda:1 and ambient device cuda:0, enabling one fix at a time:

A B C Result
- - - IMA in the FP8 cast/quantize kernel
- - IMA moves to amax workspace placement
- - IMA remains in the cast kernel
- Forward completes; failure moves to amax finalization
Forward/backward and delayed-scaling state complete successfully

Testing

tests/pytorch/test_multi_device_fp8.py now contains seven focused tests:

  1. A module on a non-current device, including forward/backward state and output placement.
  2. Cleanup of the current-device guard when prepare_forward raises.
  3. Device-aware recipe state and forward/backward execution through pytorch.ops.BasicLinear.
  4. Two modules on different devices in one autocast context.
  5. Bitwise comparison of a single-device and split-device model.
  6. Ordered multi-device gather/reduce/scatter bookkeeping with a mocked collective.
  7. Sentinel-based verification that the distributed collective receives entries in logical registration order and applies a simulated MAX reduction.

Additional validation performed locally:

  • test_multi_device_fp8.py: 6 passed.
  • Relevant fusible-operation tests: 272 passed.
  • test_custom_recipe.py: 25 passed.
  • Real NCCL harness: tests/pytorch/distributed/run_multi_device_fp8.py runs 2 processes × 2 GPUs per process for 10 forward/backward iterations with one all-reduce per direction; both ranks produced the same final hash.
  • Pre-commit hooks: passed.

Run the real-NCCL check with:

CUDA_VISIBLE_DEVICES=0,1,2,3 torchrun --standalone --nproc_per_node=2 tests/pytorch/distributed/run_multi_device_fp8.py

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.

@github-actions github-actions Bot added the community-contribution PRs from external contributor outside the core maintainers, representing community-driven work. label Sep 3, 2026
@greptile-apps

greptile-apps Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR makes FP8 execution device-aware for single-process multi-GPU models and repairs cleanup after forward preparation fails.

  • Pins module and operation execution to the input or gradient device.
  • Allocates persistent recipe state on module-owned devices.
  • Finalizes global amax state per device while preserving distributed registration order.
  • Adds focused local and distributed multi-GPU validation.

Confidence Score: 5/5

The PR appears safe to merge.

The previously reported device-guard leak is fixed because prepare_forward now releases the entered guard on every preparation exception, while successful preparations remain paired with end_forward; no blocking failure remains.

Important Files Changed

Filename Overview
transformer_engine/pytorch/module/base.py Adds device-aware recipe-state placement and balanced CUDA-device cleanup for successful and exceptional forward preparation.
transformer_engine/pytorch/quantization.py Tracks registered amax devices and performs local or distributed delayed-scale finalization without combining tensors directly across devices.
transformer_engine/pytorch/ops/fuser.py Runs fused forward and backward work under the corresponding input or gradient CUDA device.
transformer_engine/pytorch/ops/op.py Allocates operation recipe state from operation-owned CUDA parameters or buffers and refreshes global device bookkeeping after state replacement.
tests/pytorch/test_multi_device_fp8.py Covers off-current-device execution, preparation failure cleanup, recipe-state placement, multi-device finalization, and distributed registration ordering.
tests/pytorch/distributed/run_multi_device_fp8.py Provides a real NCCL validation harness for two ranks with two local GPUs per process.

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
Loading

Reviews (4): Last reviewed commit: "[PyTorch] Validate mixed-device amax ord..." | Re-trigger Greptile

Comment thread transformer_engine/pytorch/module/base.py
SuperGoodGame and others added 5 commits September 3, 2026 16:52
…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>
Signed-off-by: SuperGoodGame <985236470@qq.com>
@SuperGoodGame
SuperGoodGame force-pushed the fix/single-process-multi-gpu-fp8 branch from da96593 to dc980e4 Compare September 3, 2026 08:52
Signed-off-by: SuperGoodGame <985236470@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution PRs from external contributor outside the core maintainers, representing community-driven work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-GPU FP8 (accelerate.dispatch_model + DelayedScaling HYBRID) — illegal memory access on H200 sm_90

1 participant