NVIDIA / NVIDIA/TransformerEngine

[PyTorch] FusedAdam/multi_tensor_apply: OOB metadata writes when a zero-numel tensor lands in the last TensorListMetadata slot (CUDA IMA / misaligned address; silent corruption below threshold)

Open
#3,202 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
3.5k
Forks
831
Avg merge
3d 11h
Merged PRs (30d)
65

Description

Describe the bug

multi_tensor_apply() (transformer_engine/common/multi_tensor/multi_tensor_apply.cuh)
gives every tensor a slot in the fixed-size kernel-arg struct
TensorListMetadata (36 slots for the depth-4 Adam kernel, 30 for the
depth-5 master-weights kernel). The tensors_full flush condition is only
evaluated inside the per-chunk loop. A zero-numel tensor occupies a
slot and increments loc_tensor_info, but contributes zero chunks, so the
chunk-loop body never runs for it. If the zero-numel tensor lands in the
last slot, loc_tensor_info reaches max_tensors without a flush, and
every subsequent tensor writes tl.sizes[loc_tensor_info] and
tl.addresses[d][loc_tensor_info] out of bounds of the struct arrays:
sizes[] writes overwrite block_to_tensor[], and the last addresses row
sprays raw pointer bytes over sizes[] and block_to_tensor[]. The launched
kernel then reads garbage tensor indices/sizes/addresses.

Consequences, depending on how many tensors follow the boundary-slot empty:

  • ≥ ~16 trailing tensors (depth 5; ~20 for depth 4): CUDA fault —
    illegal memory access or misaligned address depending on the garbage
    bytes (we observed both signatures in production).
  • Fewer trailing tensors: the overflow stays inside adjacent struct
    fields — no fault, silently wrong kernel inputs, i.e. corrupted
    optimizer updates with no error signal. This is arguably the worse case.

This input is not exotic: PyTorch FSDP2 (fully_shard) shards every
parameter along dim-0, so any parameter with dim-0 < shard-group size leaves
zero-numel local shards on tail ranks (e.g. [1, hidden] gates/scales,
class/position embeddings). Whether a training run crashes, silently
corrupts, or works is then decided by the numel-sequence-dependent slot
position of the empty shard — which is why the failure pattern looks
spurious (tail ranks only, first step only, only when small params are
trainable, "fixed" by unrelated param-count changes). We hit it
deterministically on 4-GPU and 128-GPU FSDP2 runs (VLM with trainable vision
tower; torch.optim AdamW on the identical population trains fine).

Steps/Code to reproduce the bug

Single GPU, no distributed setup, default kwargs:

import torch
from transformer_engine.pytorch.optimizers import FusedAdam

# 35 normal tensors, then a zero-numel tensor in the LAST metadata slot
# (36 slots for the depth-4 kernel), then enough tensors to push the OOB
# writes into block_to_tensor[].
params = [torch.nn.Parameter(torch.randn(8, device="cuda")) for _ in range(35)]
params.append(torch.nn.Parameter(torch.empty(0, device="cuda")))
params += [torch.nn.Parameter(torch.randn(8, device="cuda")) for _ in range(20)]
for p in params:
    p.grad = torch.randn_like(p)

opt = FusedAdam(params)  # plain kwargs; master-weights kwargs fault too (30-slot depth-5 kernel)
opt.step()               # CUDA illegal memory access / misaligned address
torch.cuda.synchronize()

Run with CUDA_LAUNCH_BLOCKING=1 to pin the fault to
multi_tensor_apply.cuh:92. For the depth-5 master-weights kernel use
[8]*29 + [0] + [8]*16 with
master_weights=True, master_weight_dtype=torch.float32, store_param_remainders=True, exp_avg_dtype=torch.bfloat16, exp_avg_sq_dtype=torch.float32.

Expected behavior

Zero-numel tensors are either skipped when building the tensor lists /
metadata (their update is an exact no-op — under FSDP2 every element lives on
other ranks; upstream apex skips empties in the analogous code), or rejected
with a clear Python-side error. The flush condition should in any case be
correct independent of chunk count (e.g. checked outside the chunk loop) so
that a slot-boundary empty cannot silently corrupt the metadata of subsequent
tensors.

Environment
  • TE 2.15.0+42b84005 (source build), torch 2.12.0a0+nv26.04 (NGC 26.06),
    CUDA 13.2, H100. Fault verified deterministic single-GPU with the snippet
    above; also reproduced in real 4-GPU and 128-GPU FSDP2 trainings.
  • The metadata-overflow structure is unchanged on current main (no
    numel()==0 guard; flush only inside the chunk loop).
Suggested fix

Skip numel() == 0 tensors when building TensorListMetadata (as upstream
apex does), and/or evaluate the tensors_full flush outside the per-chunk
loop; document that empty tensors are supported. Happy to test a fix.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with transformer_engine/common/multi_tensor/multi_tensor_apply.cuh, especially the metadata construction around line 92, and run the provided single-GPU FusedAdam reproducer with CUDA_LAUNCH_BLOCKING=1. Compare the depth-4 and depth-5 slot boundaries and verify that zero-numel tensors no longer cause out-of-bounds metadata writes or CUDA faults, while normal optimizer updates remain correct.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python, pytorch
Domain
backend, machine-learning, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.