Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3816a26
[PyTorch] Preserve CUDA RNG state during checkpoint recompute
losewayy Nov 8, 2026
613a449
[PyTorch] Test CUDA RNG restoration after checkpoint backward
losewayy Nov 8, 2026
26a46cc
[PyTorch] Restore captured checkpoint RNG state on device
pggPL Nov 10, 2026
e5d2d08
[PyTorch] Use public Philox API for captured checkpoint RNG
pggPL Nov 10, 2026
1149f52
[PyTorch] Guard captured stochastic NVFP4 quantization
pggPL Nov 10, 2026
24f7ce5
Trim redundant checkpoint RNG test coverage
pggPL Nov 10, 2026
8afc77a
Expose custom op tags to checkpoint RNG guards
pggPL Nov 10, 2026
5fa09af
Clarify the missing Philox state sentinel
pggPL Nov 10, 2026
41a4afc
Restore captured RNG bases and CPU offsets separately
pggPL Nov 10, 2026
52f1895
Remove the RNG tracker snapshot note
pggPL Nov 10, 2026
6dab9c7
Remove the custom op RNG tag test
pggPL Nov 10, 2026
65ab410
Restore the unrelated NVFP4 guard change
pggPL Nov 10, 2026
82c6e72
Remove the NVFP4 stochastic rounding property guard
pggPL Nov 10, 2026
1e8baf3
Integrate checkpoint RNG guard and snapshot simplifications
pggPL Nov 10, 2026
9a72905
Remove the missing Philox API checkpoint tests
pggPL Nov 10, 2026
d0192ef
Merge main into checkpoint RNG fixes
pggPL Nov 13, 2026
f4c9a29
Remove unused custom op tags parameter
pggPL Nov 13, 2026
4ecc533
Keep deprecated graph-safe RNG helpers
pggPL Nov 13, 2026
369376b
Keep dropout implementation selection for p=0
pggPL Nov 13, 2026
2a3d1d3
Accept preserve_rng_state in checkpoint during capture
pggPL Nov 13, 2026
b34da49
Drop cosmetic edits unrelated to checkpoint RNG
pggPL Nov 13, 2026
cd2fa8b
Restore checkpoint RNG handles after a failed value restore
pggPL Nov 13, 2026
26645f8
Test the checkpoint RNG guard without Philox state access
pggPL Nov 13, 2026
5ea84ec
Revert "Test the checkpoint RNG guard without Philox state access"
pggPL Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions tests/pytorch/test_numerics.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn import Parameter
import transformer_engine.pytorch as te

from transformer_engine.pytorch.quantization import (
FP8GlobalStateManager,
Expand Down Expand Up @@ -45,6 +47,7 @@
)
from transformer_engine.pytorch import checkpoint as te_checkpoint
from transformer_engine.pytorch.distributed import (
CudaRNGStatesTracker,
is_fp8_activation_recompute_enabled,
in_fp8_activation_recompute_phase,
)
Expand Down Expand Up @@ -1132,6 +1135,184 @@ def outer_body(value):
_assert_fp8_recompute_state_drained(inner, tail)


@pytest.mark.parametrize("use_reentrant", all_boolean)
@pytest.mark.parametrize("rng_source", ("default", "tracker", "shared"))
def test_checkpoint_recompute_preserves_tracker_rng_state(use_reentrant, rng_source):
"""Recompute must match forward and preserve the live RNG states."""

def run(checkpointed):
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
tracker = CudaRNGStatesTracker()
tracker.add("model-parallel-rng", 4321)
tracker_state = tracker.get_states()["model-parallel-rng"]
inp = torch.ones(4096, device="cuda", requires_grad=True)
draws = []

def get_tracked_rng_state():
state = tracker.get_states()["model-parallel-rng"]
return state.get_state() if isinstance(state, torch.Generator) else state.clone()

def body(value):
if rng_source == "tracker":
with tracker.fork():
out = F.dropout(value, p=0.5, training=True)
else:
out = F.dropout(value, p=0.5, training=True)
if rng_source == "shared":
with tracker.fork():
out = out + 2 * F.dropout(value, p=0.5, training=True)
draws.append(out.detach().clone())
return out

# Leave the fork before backward so the checkpoint must restore the
# sharing between the default generator and the tracker for recompute.
with tracker.fork() if rng_source == "shared" else nullcontext():
if checkpointed:
out = te_checkpoint(
body,
inp,
use_reentrant=use_reentrant,
get_rng_state_tracker=lambda: tracker,
distribute_saved_activations=False,
tp_group=None,
)
else:
out = body(inp)

# Backward must preserve the RNG positions after unrelated draws.
torch.rand(8, device=inp.device)
with tracker.fork():
torch.rand(8, device=inp.device)
default_rng_before = torch.cuda.get_rng_state(inp.device)
tracked_rng_before = get_tracked_rng_state()
out.sum().backward()
assert torch.equal(
torch.cuda.get_rng_state(inp.device), default_rng_before
), "Default RNG changed during backward"
assert torch.equal(
get_tracked_rng_state(), tracked_rng_before
), "Tracked RNG changed during backward"
return out.detach(), inp.grad.detach(), draws, tracker, tracker_state
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved

ref_out, ref_grad, _, _, _ = run(checkpointed=False)
out, grad, draws, tracker, tracker_state = run(checkpointed=True)
assert torch.equal(out, ref_out)
assert len(draws) == 2
assert torch.equal(draws[0], draws[1])
assert torch.equal(grad, ref_grad)
if isinstance(tracker_state, torch.Generator):
assert tracker.get_states()["model-parallel-rng"] is tracker_state


@pytest.mark.parametrize("fork_tracker", all_boolean)
def test_checkpoint_preserves_captured_rng_state(fork_tracker):
"""Graph replay must still advance its registered RNG after eager recompute."""
tracker = CudaRNGStatesTracker()
tracker.add("model-parallel-rng", 4321)
rng_context = tracker.fork if fork_tracker else nullcontext
generator = (
tracker.get_states()["model-parallel-rng"]
if fork_tracker
else torch.cuda.default_generators[torch.cuda.current_device()]
)
sample = torch.empty(128, device="cuda")
with rng_context():
sample.uniform_()
graph = torch.cuda.CUDAGraph()
if fork_tracker:
graph.register_generator_state(generator)
with torch.cuda.graph(graph), rng_context():
sample.uniform_()

offset = generator.get_offset()
graph.replay()
increment = generator.get_offset() - offset
assert increment > 0

def body(value):
with rng_context():
return F.dropout(value, p=0.5, training=True)

inp = torch.ones(4096, device="cuda", requires_grad=True)
out = te_checkpoint(body, inp, use_reentrant=True, get_rng_state_tracker=lambda: tracker)
out.sum().backward()
assert torch.equal(inp.grad, out)

offset = generator.get_offset()
graph.replay()
assert generator.get_offset() - offset == increment


@pytest.mark.parametrize("use_reentrant", all_boolean)
@pytest.mark.parametrize("rng_source", ("default", "tracker", "mixed", "shared", "no_tracker"))
@pytest.mark.parametrize("capture_mode", ("graphed", "manual"))
@pytest.mark.skipif(
not hasattr(torch.Generator, "philox_state"), reason="Captured RNG requires PyTorch 2.14+"
)
def test_checkpoint_dropout_gradients_during_graph_replay(use_reentrant, rng_source, capture_mode):
"""Each captured recompute must reproduce its corresponding forward masks."""
torch.manual_seed(seed)
tracker = CudaRNGStatesTracker()
tracker.add("model-parallel-rng", 4321)

def body(value):
if rng_source == "tracker":
with tracker.fork():
return F.dropout(value, p=0.5, training=True)
out = F.dropout(value, p=0.5, training=True)
if rng_source in ("mixed", "shared"):
with tracker.fork():
out = out + 2 * F.dropout(value, p=0.5, training=True)
return out

class Model(torch.nn.Module):
def forward(self, value):
with tracker.fork() if rng_source == "shared" else nullcontext():
torch.rand(8, device=value.device)
return te_checkpoint(
body,
value,
use_reentrant=use_reentrant,
get_rng_state_tracker=None if rng_source == "no_tracker" else lambda: tracker,
)

sample = torch.ones(4096, device="cuda", requires_grad=True)
if capture_mode == "manual":
model = Model()
stream = torch.cuda.Stream()
stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(stream):
for _ in range(3):
sample.grad = None
model(sample).sum().backward()
torch.cuda.current_stream().wait_stream(stream)
sample.grad = None
graph = torch.cuda.CUDAGraph()
graph.register_generator_state(tracker.get_states()["model-parallel-rng"])
with torch.cuda.graph(graph):
output = model(sample)
output.sum().backward()
else:
model = te.make_graphed_callables(Model(), (sample,), num_warmup_iters=3)
previous = None
for _ in range(3):
if capture_mode == "manual":
sample.grad.zero_()
graph.replay()
grad = sample.grad
else:
value = torch.ones_like(sample, requires_grad=True)
output = model(value)
output.sum().backward()
grad = value.grad
forward_output = output.detach().clone()
assert torch.equal(grad, forward_output), "Recompute used a different dropout mask"
if previous is not None:
assert not torch.equal(forward_output, previous), "Graph replay did not advance RNG"
previous = forward_output


@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Older builds lose test coverage

Removing test_checkpoint_graph_rng_without_philox_api and test_checkpoint_native_attention_requires_philox_api removes the only tests of capture without philox_state. The remaining graph-replay test skips builds without that API, so regressions in the guards or cleanup after rejected capture can pass unnoticed.

This is non-blocking, but please keep these tests or replace them. They check that random operations raise, deterministic capture works, and rejected capture restores the changed state.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

def test_checkpoint_without_autograd_preserves_forward_context():
"""The direct path must preserve a context that explicitly enables gradients."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
META_QKV,
)
from transformer_engine.pytorch.quantization import get_fp8_torch_dtype, FP8GlobalStateManager
from transformer_engine.pytorch.distributed import get_distributed_world_size
from transformer_engine.pytorch.distributed import get_distributed_world_size, _check_checkpoint_rng
from transformer_engine.pytorch.attention.custom_ops import (
convert_bshd_to_thd,
convert_thd_to_bshd,
Expand Down Expand Up @@ -1047,6 +1047,9 @@ def forward(
) -> torch.Tensor:
"""flash-attn fprop"""

if self.training and self.attention_dropout > 0:
_check_checkpoint_rng()

assert all(
x.dtype in [torch.float16, torch.bfloat16] or isinstance(x, Float8Tensor)
for x in [query_layer, key_layer, value_layer]
Expand Down Expand Up @@ -2724,6 +2727,10 @@ def forward(
load_balancing_strategy=CPLoadBalancingStrategy.DUAL_CHUNK_SWAP,
) -> torch.Tensor:
"""fused attention fprop"""

if self.training and self.attention_dropout > 0:
_check_checkpoint_rng()

assert (
fused_attention_backend != FusedAttnBackend["No_Backend"]
), "No fused attention backend supports this input combination!"
Expand Down
Loading
Loading