Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
185 changes: 185 additions & 0 deletions tests/pytorch/test_numerics.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import math
import os
from contextlib import nullcontext
from typing import Dict, List, Tuple, Optional
import pytest

Expand Down Expand Up @@ -906,6 +907,20 @@ def _checkpointed_linear_backward(body, use_reentrant, *layers):
assert torch.isfinite(layer.weight.grad).all()


def _assert_fp8_recompute_state_drained(*layers):
"""Check that each module consumed its stash and restored its live scale."""
recompute_buffer = FP8GlobalStateManager.quantization_state.fp8_tensors_recompute_buffer
assert len(recompute_buffer) == len(layers)
for layer in layers:
assert _FP8_RECOMPUTE_KEY in layer.fp8_meta
assert len(recompute_buffer[layer.fp8_meta[_FP8_RECOMPUTE_KEY]]) == 0
assert "updated_scale_fwd" in layer.fp8_meta
assert torch.equal(
layer.fp8_meta["scaling_fwd"].scale,
layer.fp8_meta["updated_scale_fwd"],
)


@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8)
@pytest.mark.parametrize("use_reentrant", all_boolean)
def test_checkpoint_inner_autocast_is_an_fp8_recompute_region(use_reentrant):
Expand Down Expand Up @@ -955,6 +970,176 @@ def body(value):
assert _FP8_RECOMPUTE_KEY in fp8_layer.fp8_meta


@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8)
@pytest.mark.parametrize("use_reentrant", all_boolean)
@pytest.mark.parametrize("recompute_training", all_boolean)
def test_checkpoint_eval_module_balances_fp8_recompute_state(recompute_training, use_reentrant):
"""An eval module must stash metadata for the recompute forward."""
FP8GlobalStateManager.reset()
fp8_recipe = recipe.DelayedScaling(fp8_format=recipe.Format.HYBRID)
layer = Linear(16, 16, bias=False, params_dtype=torch.float32).cuda().eval()

def body(value):
with autocast(enabled=True, recipe=fp8_recipe):
# Exercise an intermediate input whose grad state differs between the
# reentrant and non-reentrant checkpoint forward implementations.
return layer(value * 2)

inp = torch.randn(16, 16, device="cuda", dtype=torch.bfloat16, requires_grad=True)
with torch.autocast("cuda", dtype=torch.bfloat16):
loss = te_checkpoint(body, inp, use_reentrant=use_reentrant).float().sum()

layer.train(recompute_training)
loss.backward()
torch.cuda.synchronize()

assert inp.grad is not None and torch.isfinite(inp.grad).all()
assert layer.weight.grad is not None and torch.isfinite(layer.weight.grad).all()
_assert_fp8_recompute_state_drained(layer)


@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8)
def test_checkpoint_without_autograd_does_not_accumulate_recompute_stashes():
"""Checkpoint calls without autograd must not leave unreachable metadata."""
FP8GlobalStateManager.reset()
fp8_recipe = recipe.DelayedScaling(fp8_format=recipe.Format.HYBRID)
layer = Linear(16, 16, bias=False, params_dtype=torch.float32).cuda()
inp = torch.randn(16, 16, device="cuda", dtype=torch.bfloat16)
observed = []

def body(value):
with autocast(enabled=True, recipe=fp8_recipe):
observed.append(
(
is_fp8_activation_recompute_enabled(),
in_fp8_activation_recompute_phase(),
)
)
return layer(value)

with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
for _ in range(3):
out = te_checkpoint(body, inp)
assert torch.isfinite(out).all()

recompute_buffer = FP8GlobalStateManager.quantization_state.fp8_tensors_recompute_buffer
assert _FP8_RECOMPUTE_KEY not in layer.fp8_meta
assert all(len(stashed) == 0 for stashed in recompute_buffer)
assert observed == [(False, False)] * 3


@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8)
def test_reentrant_checkpoint_without_grad_input_does_not_stash_fp8_metadata():
"""A reentrant checkpoint that cannot receive backward must not save recompute state."""
FP8GlobalStateManager.reset()
fp8_recipe = recipe.DelayedScaling(fp8_format=recipe.Format.HYBRID)
layer = Linear(16, 16, bias=False, params_dtype=torch.float32).cuda()
observed = []

def body(value):
with autocast(enabled=True, recipe=fp8_recipe):
observed.append(
(
is_fp8_activation_recompute_enabled(),
in_fp8_activation_recompute_phase(),
)
)
return layer(value)

inp = torch.randn(16, 16, device="cuda", dtype=torch.bfloat16)
with torch.enable_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
for _ in range(3):
out = te_checkpoint(body, inp, use_reentrant=True)
assert torch.isfinite(out).all()
assert not out.requires_grad

assert _FP8_RECOMPUTE_KEY not in layer.fp8_meta
assert not FP8GlobalStateManager.quantization_state.fp8_tensors_recompute_buffer
assert observed == [(False, False)] * 3

valid_inp = torch.randn(
16,
16,
device="cuda",
dtype=torch.bfloat16,
requires_grad=True,
)
with torch.autocast("cuda", dtype=torch.bfloat16):
loss = te_checkpoint(body, valid_inp, use_reentrant=True).float().sum()
loss.backward()
torch.cuda.synchronize()

assert valid_inp.grad is not None and torch.isfinite(valid_inp.grad).all()
assert layer.weight.grad is not None and torch.isfinite(layer.weight.grad).all()
assert observed[-2:] == [(True, False), (True, True)]
_assert_fp8_recompute_state_drained(layer)


@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8)
def test_ineligible_reentrant_checkpoint_preserves_outer_recompute_context():
"""An inner checkpoint without grad inputs must inherit its outer recompute phase."""
FP8GlobalStateManager.reset()
fp8_recipe = recipe.DelayedScaling(fp8_format=recipe.Format.HYBRID)
inner = Linear(16, 16, bias=False, params_dtype=torch.float32).cuda()
tail = Linear(16, 16, bias=False, params_dtype=torch.float32).cuda()
observed = []

def inner_body(value):
observed.append(
(
is_fp8_activation_recompute_enabled(),
in_fp8_activation_recompute_phase(),
)
)
return inner(value)

def outer_body(value):
with autocast(enabled=True, recipe=fp8_recipe):
inner_out = te_checkpoint(inner_body, value.detach(), use_reentrant=True)
return tail(value + inner_out)

inp = torch.randn(16, 16, device="cuda", dtype=torch.bfloat16, requires_grad=True)
with torch.autocast("cuda", dtype=torch.bfloat16):
loss = te_checkpoint(outer_body, inp, use_reentrant=False).float().sum()
loss.backward()
torch.cuda.synchronize()

assert observed == [(True, False), (True, True)]
assert inp.grad is not None and torch.isfinite(inp.grad).all()
assert inner.weight.grad is None
assert tail.weight.grad is not None and torch.isfinite(tail.weight.grad).all()
_assert_fp8_recompute_state_drained(inner, tail)


@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8)
def test_checkpoint_without_autograd_preserves_forward_context():
"""The direct path must preserve a context that explicitly enables gradients."""
FP8GlobalStateManager.reset()
fp8_recipe = recipe.DelayedScaling(fp8_format=recipe.Format.HYBRID)
layer = Linear(16, 16, bias=False, params_dtype=torch.float32).cuda().eval()
inp = torch.randn(16, 16, device="cuda", dtype=torch.bfloat16, requires_grad=True)

def body(value):
with autocast(enabled=True, recipe=fp8_recipe):
return layer(value)

with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
out = te_checkpoint(
body,
inp,
context_fn=lambda: (torch.enable_grad(), nullcontext()),
)

assert out.requires_grad
out.float().sum().backward()
torch.cuda.synchronize()
assert inp.grad is not None and torch.isfinite(inp.grad).all()
assert layer.weight.grad is not None and torch.isfinite(layer.weight.grad).all()
recompute_buffer = FP8GlobalStateManager.quantization_state.fp8_tensors_recompute_buffer
assert _FP8_RECOMPUTE_KEY not in layer.fp8_meta
assert all(len(stashed) == 0 for stashed in recompute_buffer)


Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do you think we need so much tests for one line fix? My agent says yes, but I'm sceptical about it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I reduced this to six focused cases covering eval pairing with and without a mode transition across both checkpoint paths, no grad entry, and forward context preservation.

def _test_e2e_checkpointing_get_model(config, dtype):
sigma = 0.023
init_method = init_method_normal(sigma)
Expand Down
18 changes: 15 additions & 3 deletions transformer_engine/pytorch/distributed.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,14 @@ def forward(
# Preserve torch autocast context for the backward pass
torch_gpu_amp_ctx, torch_cpu_amp_ctx = _get_active_autocast_contexts()

with torch.no_grad(), forward_ctx:
with activation_recompute_forward(activation_recompute=True, recompute_phase=False):
outputs = run_function(*args, **kwargs)
# An ineligible nested checkpoint must inherit an enclosing recompute phase.
activation_recompute_ctx = (
activation_recompute_forward(activation_recompute=True, recompute_phase=False)
if any(ctx.needs_input_grad)
else nullcontext()
)
with torch.no_grad(), forward_ctx, activation_recompute_ctx:
outputs = run_function(*args, **kwargs)

# Divide hidden states across model parallel group and only keep
# the chunk corresponding to the current rank.
Expand Down Expand Up @@ -740,6 +745,13 @@ def checkpoint(
**kwargs,
)

# When checkpoint is entered with autograd disabled, run the forward directly
# to avoid unreachable FP8 recompute state. Preserve the forward context.
if not torch.is_grad_enabled():
forward_ctx, _ = context_fn()
with forward_ctx:
return function(*args, **kwargs)

from .module.base import TransformerEngineBaseModule

if isinstance(function, TransformerEngineBaseModule):
Expand Down
2 changes: 1 addition & 1 deletion transformer_engine/pytorch/module/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1632,7 +1632,7 @@ def prepare_forward(
FP8GlobalStateManager.add_fp8_tensors_to_global_buffer(self.fp8_meta)

# Activation recomputation is used and this is the first forward phase.
if self.training and is_fp8_activation_recompute_enabled():
if is_fp8_activation_recompute_enabled():
FP8GlobalStateManager.copy_forward_fp8_meta_tensors_for_recompute(self.fp8_meta)

nvtx_range_push(self.__class__.__name__ + " forward")
Expand Down
Loading