|
4 | 4 |
|
5 | 5 | import math |
6 | 6 | import os |
7 | | -from contextlib import contextmanager |
| 7 | +from contextlib import contextmanager, nullcontext |
8 | 8 | from typing import Dict, List, Tuple, Optional |
9 | 9 | import pytest |
10 | 10 |
|
@@ -928,6 +928,20 @@ def _checkpointed_linear_backward(body, use_reentrant, *layers): |
928 | 928 | assert torch.isfinite(layer.weight.grad).all() |
929 | 929 |
|
930 | 930 |
|
| 931 | +def _assert_fp8_recompute_state_drained(*layers): |
| 932 | + """Check that each module consumed its stash and restored its live scale.""" |
| 933 | + recompute_buffer = FP8GlobalStateManager.quantization_state.fp8_tensors_recompute_buffer |
| 934 | + assert len(recompute_buffer) == len(layers) |
| 935 | + for layer in layers: |
| 936 | + assert _FP8_RECOMPUTE_KEY in layer.fp8_meta |
| 937 | + assert len(recompute_buffer[layer.fp8_meta[_FP8_RECOMPUTE_KEY]]) == 0 |
| 938 | + assert "updated_scale_fwd" in layer.fp8_meta |
| 939 | + assert torch.equal( |
| 940 | + layer.fp8_meta["scaling_fwd"].scale, |
| 941 | + layer.fp8_meta["updated_scale_fwd"], |
| 942 | + ) |
| 943 | + |
| 944 | + |
931 | 945 | @pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8) |
932 | 946 | @pytest.mark.parametrize("use_reentrant", all_boolean) |
933 | 947 | def test_checkpoint_inner_autocast_is_an_fp8_recompute_region(use_reentrant): |
@@ -977,6 +991,176 @@ def body(value): |
977 | 991 | assert _FP8_RECOMPUTE_KEY in fp8_layer.fp8_meta |
978 | 992 |
|
979 | 993 |
|
| 994 | +@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8) |
| 995 | +@pytest.mark.parametrize("use_reentrant", all_boolean) |
| 996 | +@pytest.mark.parametrize("recompute_training", all_boolean) |
| 997 | +def test_checkpoint_eval_module_balances_fp8_recompute_state(recompute_training, use_reentrant): |
| 998 | + """An eval module must stash metadata for the recompute forward.""" |
| 999 | + FP8GlobalStateManager.reset() |
| 1000 | + fp8_recipe = recipe.DelayedScaling(fp8_format=recipe.Format.HYBRID) |
| 1001 | + layer = Linear(16, 16, bias=False, params_dtype=torch.float32).cuda().eval() |
| 1002 | + |
| 1003 | + def body(value): |
| 1004 | + with autocast(enabled=True, recipe=fp8_recipe): |
| 1005 | + # Exercise an intermediate input whose grad state differs between the |
| 1006 | + # reentrant and non-reentrant checkpoint forward implementations. |
| 1007 | + return layer(value * 2) |
| 1008 | + |
| 1009 | + inp = torch.randn(16, 16, device="cuda", dtype=torch.bfloat16, requires_grad=True) |
| 1010 | + with torch.autocast("cuda", dtype=torch.bfloat16): |
| 1011 | + loss = te_checkpoint(body, inp, use_reentrant=use_reentrant).float().sum() |
| 1012 | + |
| 1013 | + layer.train(recompute_training) |
| 1014 | + loss.backward() |
| 1015 | + torch.cuda.synchronize() |
| 1016 | + |
| 1017 | + assert inp.grad is not None and torch.isfinite(inp.grad).all() |
| 1018 | + assert layer.weight.grad is not None and torch.isfinite(layer.weight.grad).all() |
| 1019 | + _assert_fp8_recompute_state_drained(layer) |
| 1020 | + |
| 1021 | + |
| 1022 | +@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8) |
| 1023 | +def test_checkpoint_without_autograd_does_not_accumulate_recompute_stashes(): |
| 1024 | + """Checkpoint calls without autograd must not leave unreachable metadata.""" |
| 1025 | + FP8GlobalStateManager.reset() |
| 1026 | + fp8_recipe = recipe.DelayedScaling(fp8_format=recipe.Format.HYBRID) |
| 1027 | + layer = Linear(16, 16, bias=False, params_dtype=torch.float32).cuda() |
| 1028 | + inp = torch.randn(16, 16, device="cuda", dtype=torch.bfloat16) |
| 1029 | + observed = [] |
| 1030 | + |
| 1031 | + def body(value): |
| 1032 | + with autocast(enabled=True, recipe=fp8_recipe): |
| 1033 | + observed.append( |
| 1034 | + ( |
| 1035 | + is_fp8_activation_recompute_enabled(), |
| 1036 | + in_fp8_activation_recompute_phase(), |
| 1037 | + ) |
| 1038 | + ) |
| 1039 | + return layer(value) |
| 1040 | + |
| 1041 | + with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16): |
| 1042 | + for _ in range(3): |
| 1043 | + out = te_checkpoint(body, inp) |
| 1044 | + assert torch.isfinite(out).all() |
| 1045 | + |
| 1046 | + recompute_buffer = FP8GlobalStateManager.quantization_state.fp8_tensors_recompute_buffer |
| 1047 | + assert _FP8_RECOMPUTE_KEY not in layer.fp8_meta |
| 1048 | + assert all(len(stashed) == 0 for stashed in recompute_buffer) |
| 1049 | + assert observed == [(False, False)] * 3 |
| 1050 | + |
| 1051 | + |
| 1052 | +@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8) |
| 1053 | +def test_reentrant_checkpoint_without_grad_input_does_not_stash_fp8_metadata(): |
| 1054 | + """A reentrant checkpoint that cannot receive backward must not save recompute state.""" |
| 1055 | + FP8GlobalStateManager.reset() |
| 1056 | + fp8_recipe = recipe.DelayedScaling(fp8_format=recipe.Format.HYBRID) |
| 1057 | + layer = Linear(16, 16, bias=False, params_dtype=torch.float32).cuda() |
| 1058 | + observed = [] |
| 1059 | + |
| 1060 | + def body(value): |
| 1061 | + with autocast(enabled=True, recipe=fp8_recipe): |
| 1062 | + observed.append( |
| 1063 | + ( |
| 1064 | + is_fp8_activation_recompute_enabled(), |
| 1065 | + in_fp8_activation_recompute_phase(), |
| 1066 | + ) |
| 1067 | + ) |
| 1068 | + return layer(value) |
| 1069 | + |
| 1070 | + inp = torch.randn(16, 16, device="cuda", dtype=torch.bfloat16) |
| 1071 | + with torch.enable_grad(), torch.autocast("cuda", dtype=torch.bfloat16): |
| 1072 | + for _ in range(3): |
| 1073 | + out = te_checkpoint(body, inp, use_reentrant=True) |
| 1074 | + assert torch.isfinite(out).all() |
| 1075 | + assert not out.requires_grad |
| 1076 | + |
| 1077 | + assert _FP8_RECOMPUTE_KEY not in layer.fp8_meta |
| 1078 | + assert not FP8GlobalStateManager.quantization_state.fp8_tensors_recompute_buffer |
| 1079 | + assert observed == [(False, False)] * 3 |
| 1080 | + |
| 1081 | + valid_inp = torch.randn( |
| 1082 | + 16, |
| 1083 | + 16, |
| 1084 | + device="cuda", |
| 1085 | + dtype=torch.bfloat16, |
| 1086 | + requires_grad=True, |
| 1087 | + ) |
| 1088 | + with torch.autocast("cuda", dtype=torch.bfloat16): |
| 1089 | + loss = te_checkpoint(body, valid_inp, use_reentrant=True).float().sum() |
| 1090 | + loss.backward() |
| 1091 | + torch.cuda.synchronize() |
| 1092 | + |
| 1093 | + assert valid_inp.grad is not None and torch.isfinite(valid_inp.grad).all() |
| 1094 | + assert layer.weight.grad is not None and torch.isfinite(layer.weight.grad).all() |
| 1095 | + assert observed[-2:] == [(True, False), (True, True)] |
| 1096 | + _assert_fp8_recompute_state_drained(layer) |
| 1097 | + |
| 1098 | + |
| 1099 | +@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8) |
| 1100 | +def test_ineligible_reentrant_checkpoint_preserves_outer_recompute_context(): |
| 1101 | + """An inner checkpoint without grad inputs must inherit its outer recompute phase.""" |
| 1102 | + FP8GlobalStateManager.reset() |
| 1103 | + fp8_recipe = recipe.DelayedScaling(fp8_format=recipe.Format.HYBRID) |
| 1104 | + inner = Linear(16, 16, bias=False, params_dtype=torch.float32).cuda() |
| 1105 | + tail = Linear(16, 16, bias=False, params_dtype=torch.float32).cuda() |
| 1106 | + observed = [] |
| 1107 | + |
| 1108 | + def inner_body(value): |
| 1109 | + observed.append( |
| 1110 | + ( |
| 1111 | + is_fp8_activation_recompute_enabled(), |
| 1112 | + in_fp8_activation_recompute_phase(), |
| 1113 | + ) |
| 1114 | + ) |
| 1115 | + return inner(value) |
| 1116 | + |
| 1117 | + def outer_body(value): |
| 1118 | + with autocast(enabled=True, recipe=fp8_recipe): |
| 1119 | + inner_out = te_checkpoint(inner_body, value.detach(), use_reentrant=True) |
| 1120 | + return tail(value + inner_out) |
| 1121 | + |
| 1122 | + inp = torch.randn(16, 16, device="cuda", dtype=torch.bfloat16, requires_grad=True) |
| 1123 | + with torch.autocast("cuda", dtype=torch.bfloat16): |
| 1124 | + loss = te_checkpoint(outer_body, inp, use_reentrant=False).float().sum() |
| 1125 | + loss.backward() |
| 1126 | + torch.cuda.synchronize() |
| 1127 | + |
| 1128 | + assert observed == [(True, False), (True, True)] |
| 1129 | + assert inp.grad is not None and torch.isfinite(inp.grad).all() |
| 1130 | + assert inner.weight.grad is None |
| 1131 | + assert tail.weight.grad is not None and torch.isfinite(tail.weight.grad).all() |
| 1132 | + _assert_fp8_recompute_state_drained(inner, tail) |
| 1133 | + |
| 1134 | + |
| 1135 | +@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8) |
| 1136 | +def test_checkpoint_without_autograd_preserves_forward_context(): |
| 1137 | + """The direct path must preserve a context that explicitly enables gradients.""" |
| 1138 | + FP8GlobalStateManager.reset() |
| 1139 | + fp8_recipe = recipe.DelayedScaling(fp8_format=recipe.Format.HYBRID) |
| 1140 | + layer = Linear(16, 16, bias=False, params_dtype=torch.float32).cuda().eval() |
| 1141 | + inp = torch.randn(16, 16, device="cuda", dtype=torch.bfloat16, requires_grad=True) |
| 1142 | + |
| 1143 | + def body(value): |
| 1144 | + with autocast(enabled=True, recipe=fp8_recipe): |
| 1145 | + return layer(value) |
| 1146 | + |
| 1147 | + with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16): |
| 1148 | + out = te_checkpoint( |
| 1149 | + body, |
| 1150 | + inp, |
| 1151 | + context_fn=lambda: (torch.enable_grad(), nullcontext()), |
| 1152 | + ) |
| 1153 | + |
| 1154 | + assert out.requires_grad |
| 1155 | + out.float().sum().backward() |
| 1156 | + torch.cuda.synchronize() |
| 1157 | + assert inp.grad is not None and torch.isfinite(inp.grad).all() |
| 1158 | + assert layer.weight.grad is not None and torch.isfinite(layer.weight.grad).all() |
| 1159 | + recompute_buffer = FP8GlobalStateManager.quantization_state.fp8_tensors_recompute_buffer |
| 1160 | + assert _FP8_RECOMPUTE_KEY not in layer.fp8_meta |
| 1161 | + assert all(len(stashed) == 0 for stashed in recompute_buffer) |
| 1162 | + |
| 1163 | + |
980 | 1164 | def _test_e2e_checkpointing_get_model(config, dtype): |
981 | 1165 | sigma = 0.023 |
982 | 1166 | init_method = init_method_normal(sigma) |
|
0 commit comments