Repository navigation
[PyTorch] Activation offloading refactor #1762
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 79 commits
1851c3a
657cbbe
44f6494
728b174
57e7869
ad68f91
001b23c
037a89c
8b00c6f
4c8a06d
4702364
35dbe1d
ba3a4e4
ec50bf0
346f35b
9809729
1cc72fd
9a63e22
ed0ee4c
d470da0
4b3865c
05f8e2f
644f204
1c04938
8440075
994bd46
9becb49
f181517
c281f70
713cec8
bd6b599
e6d9d86
55b2085
667fcaa
828088a
2ec4fc3
675bbbd
a150521
00382b9
4e775bd
665d321
8bf9e27
4138750
684320f
97d6619
b4f9d77
0aa6084
92007a7
8d51b9b
18c7f19
142bc56
2e79b9f
09eb633
40f4d82
c43658e
945872d
f7539b0
f154dc4
6129362
e9186c7
749be31
00a0a1e
d16fa99
20096bf
590b6ab
006257e
a5a92a1
5b5bd4f
6f99c57
2caf8d4
7e105f0
4860bce
ef8a5a1
d39a262
3ab009f
ca85b26
83e07c2
6e614ad
934570d
ee2f50c
6ed41be
7d6bb25
1290a33
a6d82a8
348d7fa
4356bfa
047883f
5a36f94
79ff5bc
6ddb662
f7b958c
f5a2d23
815bce6
180bbf1
6f4c04a
c727baf
366db40
9284ae6
0842562
e848cb7
57af48f
4e2e8b2
322d4aa
f93e270
f41cbce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| # Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # | ||
| # See LICENSE for license information. | ||
|
|
||
| import os | ||
| from contextlib import nullcontext | ||
| import pytest | ||
| import torch | ||
|
|
||
| import transformer_engine.pytorch as te | ||
| from transformer_engine.common import recipe | ||
| from transformer_engine.pytorch.fp8 import FP8GlobalStateManager | ||
| from transformer_engine.pytorch.attention.dot_product_attention import _attention_backends | ||
| from utils import ModelConfig, get_available_attention_backends | ||
|
|
||
| # Check if FP8 is supported | ||
| fp8_available, _ = FP8GlobalStateManager.is_fp8_available() | ||
|
|
||
| fp8_recipes = [None] | ||
| if fp8_available: | ||
| fp8_recipes.append(recipe.Float8CurrentScaling()) | ||
| fp8_recipes.append(recipe.DelayedScaling()) | ||
|
|
||
| model_config = { | ||
| "small": ModelConfig(8, 512, 8, 64, num_layers=5, eps=0.1), | ||
| } | ||
| SIZE = model_config["small"].hidden_size | ||
| NUM_HEADS = model_config["small"].num_heads | ||
| NUM_LAYERS = model_config["small"].num_layers | ||
| EPSILON = model_config["small"].eps | ||
|
|
||
| # Flash attention saves some internal tensor for the backward pass | ||
| # that cannot be offloaded to CPU. | ||
| assert os.getenv("NVTE_FLASH_ATTN") == "0" | ||
|
|
||
| assert os.environ["NVTE_CPU_OFFLOAD_LEGACY_CODE_PATH"] == "1" | ||
|
|
||
| # Offloading is supported for attention only for fused and flash attention backends, | ||
| # so the use of bfloat16 is required. | ||
| # | ||
| # For the TransformerLayer, activation offloading with dropout is not supported, | ||
| # so we set hidden_dropout to 0.0. | ||
| model_types = { | ||
| "linear": lambda: te.Linear(SIZE, SIZE, params_dtype=torch.bfloat16), | ||
| "layernorm_mlp": lambda: te.LayerNormMLP(SIZE, SIZE, params_dtype=torch.bfloat16), | ||
| "layernorm_linear": lambda: te.LayerNormLinear(SIZE, SIZE, params_dtype=torch.bfloat16), | ||
| "multihead_attention": lambda: te.MultiheadAttention( | ||
| SIZE, NUM_HEADS, params_dtype=torch.bfloat16 | ||
| ), | ||
| "transformer_layer": lambda: te.TransformerLayer( | ||
| SIZE, SIZE, NUM_HEADS, params_dtype=torch.bfloat16, hidden_dropout=0.0 | ||
| ), | ||
| } | ||
|
|
||
|
|
||
| def _get_input(): | ||
| return torch.empty((128, SIZE, SIZE), dtype=torch.bfloat16).cuda() | ||
|
|
||
|
|
||
| def _get_fp8_weight_cache_size(models, fp8_recipe): | ||
| """ | ||
| Calculate the total FP8 weight cache size (in MB) for a list of models. | ||
| """ | ||
| if fp8_recipe is None: | ||
| return 0 | ||
|
|
||
| params_bytes = 0 | ||
| for model in models: | ||
| for name, param in model.named_parameters(): | ||
| if "weight" in name: | ||
| params_bytes += param.numel() | ||
|
|
||
| # One byte for columnwise and one byte for rowwise, | ||
| # hence multiply by 2 and convert to MB | ||
| # there is 1 byte of scale per 32 elements in mxFP8 | ||
| factor_for_scale_inv_tensor = (1 + 1 / 32) if fp8_recipe.mxfp8() else 1 | ||
| return (2 * params_bytes * factor_for_scale_inv_tensor) / (1024**2) | ||
|
|
||
|
|
||
| def _measure_memory_between_forward_and_backward(models, fp8_recipe, cpu_offload): | ||
| tensor = _get_input() | ||
| if cpu_offload: | ||
| offload_context, sync_function = te.get_cpu_offload_context( | ||
| enabled=True, | ||
| num_layers=len(models) - 1, | ||
| model_layers=len(models), | ||
| offload_activations=True, | ||
| offload_weights=False, | ||
| ) | ||
| else: | ||
| offload_context = nullcontext() | ||
| sync_function = lambda x: x | ||
|
|
||
| for model in models: | ||
| with te.fp8_autocast( | ||
| enabled=fp8_recipe is not None, fp8_recipe=fp8_recipe | ||
| ), offload_context: | ||
| tensor = model(tensor) | ||
| tensor = sync_function(tensor) | ||
|
|
||
| max_mem_used = torch.cuda.memory_allocated() / (1024**2) | ||
| torch.cuda.synchronize() | ||
|
|
||
| tensor.sum().backward() | ||
|
|
||
| return max_mem_used | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("fp8_recipe", fp8_recipes) | ||
| @pytest.mark.parametrize("model_key", model_types.keys()) | ||
| def test_cpu_offload(fp8_recipe, model_key) -> None: | ||
| """ | ||
| We run three configurations: | ||
| (1) No offloading: All activations remain on the GPU between forward and backward passes. | ||
| (2) No offloading (one layer): Only the first layer's activations remain on the GPU between | ||
| forward and backward passes. | ||
| (3) With offloading (all layers): Only the last layer's activations remain on the GPU | ||
| between forward and backward passes, while all other layers are offloaded to the CPU. | ||
|
|
||
| We expect the memory consumption of configurations (2) and (3) to be similar, with | ||
| the difference being the size of the FP8 cache that is not offloaded to the CPU. | ||
| We also expect this memory consumption to be smaller than in scenario (1). | ||
| """ | ||
| import gc | ||
|
|
||
| gc.collect() | ||
|
|
||
| model_cls = model_types[model_key] | ||
| models_list = [model_cls() for _ in range(NUM_LAYERS)] | ||
|
|
||
| if model_key in ["multihead_attention", "transformer_layer"]: | ||
| available_backends, *_ = get_available_attention_backends( | ||
| model_config["small"], | ||
| qkv_dtype=torch.bfloat16, | ||
| qkv_layout="sbhd_sbhd_sbhd", | ||
| ) | ||
| _, fused_attn_supported, _ = available_backends | ||
| if not fused_attn_supported: | ||
| pytest.skip("Fused attention backend not available.") | ||
| os.environ["NVTE_FLASH_ATTN"] = "0" | ||
| _attention_backends["backend_selection_requires_update"] = True | ||
|
|
||
| without_offloading = _measure_memory_between_forward_and_backward( | ||
| models_list, fp8_recipe, False | ||
| ) | ||
| without_offloading_one_layer = _measure_memory_between_forward_and_backward( | ||
| models_list[:1], fp8_recipe, False | ||
| ) | ||
| with_offloading = _measure_memory_between_forward_and_backward(models_list, fp8_recipe, True) | ||
|
|
||
| assert with_offloading < without_offloading | ||
|
|
||
| # The only difference between the memory consumption of with_offloading | ||
| # and without_offloading_one_layer should be the size of the FP8 weights cache, | ||
| # which is not offloaded to the CPU. | ||
| memory_consumption_diff = abs(with_offloading - without_offloading_one_layer) | ||
| assert ( | ||
| memory_consumption_diff < _get_fp8_weight_cache_size(models_list[1:], fp8_recipe) + EPSILON | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,10 @@ | |
| ) | ||
| from transformer_engine.pytorch.attention.dot_product_attention.softmax import FusedScaleMaskSoftmax | ||
| from transformer_engine.pytorch.attention.inference import InferenceParams | ||
| from transformer_engine.pytorch.cpu_offload import ( | ||
| is_cpu_offload_enabled, | ||
| start_offload, | ||
| ) | ||
|
|
||
| # Import attention utils | ||
| import transformer_engine.pytorch.attention.dot_product_attention.utils as dpa_utils | ||
|
|
@@ -533,6 +537,9 @@ def forward( | |
| cp_size *= get_distributed_world_size(group) | ||
| context_parallel = cp_size > 1 | ||
|
|
||
| if is_cpu_offload_enabled(): | ||
| start_offload(query_layer, key_layer, value_layer, offload_base_tensor=True) | ||
|
|
||
| # get q_format and kv_format for training and inference | ||
| qkv_format, q_format, kv_format = dpa_utils.get_qkv_format(qkv_layout, inference_params) | ||
|
|
||
|
|
@@ -718,12 +725,12 @@ def forward( | |
| use_flash_attn_3=use_flash_attn_3, | ||
| ) | ||
| else: | ||
|
|
||
|
pggPL marked this conversation as resolved.
Resolved
|
||
| from transformer_engine.pytorch.cpu_offload import ( | ||
| CPUOffloadEnabled, | ||
| mark_activation_offload, | ||
| ) | ||
|
|
||
| if CPUOffloadEnabled: | ||
| if is_cpu_offload_enabled(): | ||
| mark_activation_offload( | ||
| query_layer, key_layer, value_layer, cu_seqlens_q, cu_seqlens_kv | ||
| ) | ||
|
|
@@ -960,6 +967,9 @@ def forward( | |
| # FP8 attn, is_output_fp8 = True: fake_dtype = torch.float8_e4m3fn | ||
| fake_dtype = q.dtype | ||
|
|
||
| if is_cpu_offload_enabled(): | ||
| start_offload(q, k, v, offload_base_tensor=True) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does offload base tensor do?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If multiple tensors are views on one big tensor, this will offload this big tensor once. |
||
|
|
||
| QKV_quantizer, O_quantizer, S_quantizer, dQKV_quantizer, dO_quantizer, dP_quantizer = ( | ||
| dpa_utils.get_attention_quantizers(fp8, quantizers, cp_specific_quantizers=False) | ||
| ) | ||
|
|
@@ -1096,11 +1106,11 @@ def forward( | |
| ctx.fp8 = fp8 and int(os.getenv("NVTE_FP8_DPA_BWD", "1")) | ||
|
|
||
| from transformer_engine.pytorch.cpu_offload import ( | ||
| CPUOffloadEnabled, | ||
| mark_activation_offload, | ||
| NVTE_CPU_OFFLOAD_LEGACY_CODE_PATH, | ||
| ) | ||
|
|
||
| if CPUOffloadEnabled: | ||
| if is_cpu_offload_enabled() and NVTE_CPU_OFFLOAD_LEGACY_CODE_PATH: | ||
| if ctx.fp8: | ||
| tensor_list = fp8_tensors | ||
| else: | ||
|
|
@@ -1113,6 +1123,7 @@ def forward( | |
| ctx.is_input_fp8 = is_input_fp8 | ||
| ctx.is_output_fp8 = is_output_fp8 | ||
| qkvo_tensors = (q, k, v, out_save) if not ctx.fp8 else (None, None, None, None) | ||
|
|
||
| tensors_to_save, tensor_objects = prepare_for_saving( | ||
| *fp8_tensors, | ||
| *qkvo_tensors, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,6 @@ | |
| import math | ||
| import os | ||
| from typing import Any, Callable, Dict, List, Optional, Tuple, Union | ||
| import warnings | ||
| import logging | ||
|
|
||
| import torch | ||
|
|
@@ -1171,14 +1170,6 @@ def forward( | |
| softmax_offset=softmax_offset, | ||
| ) | ||
|
|
||
| from transformer_engine.pytorch.cpu_offload import CPUOffloadEnabled | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed this warning, because it is not True and we do not want to expose old code path to new users and I think all current users make sure they do not use unfused attention. |
||
|
|
||
| if CPUOffloadEnabled: | ||
| warnings.warn( | ||
| "Attention activation Offloading is only implemented" | ||
| "with Flash Attention and Fused Attention!" | ||
| ) | ||
|
|
||
| if use_unfused_attention: | ||
| if checkpoint_core_attention: | ||
| return self._checkpointed_attention_forward( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should update this test with the changes in #2158. The old test didn't make the input with
requires_grad=True, so the memory estimates were incorrect. It turns out memory usage is not the same between the 1 layer case and the CPU offloading case because the input tensor caching is different between those two cases.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also,
_old_code_pathis a pretty bad name. Maybetest_cpu_offloading_v1.pyandtest_cpu_offloading_v2.py?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We plan to remove old code path at the end of the year - this is needed as a fallback option for DL training folks, if something will break with new code path. Since mcore imports from transformer_engine.pytorch.cpu_offloading, I think that we should use this name for new path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Temporary code has a tendency to stick around, and I'd rather eliminate the possibility of names like
_old_old_code_path. We can keeptest_cpu_offloading.py, but let's change old test totest_cpu_offloading_v1.py. Also, as long as we are keeping the old test we should keep the changes from #2158.