Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/torch/distributed/__pycache__/_checkpointable.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/__pycache__/_composable_state.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/__pycache__/_functional_collectives.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/__pycache__/_functional_collectives_impl.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/__pycache__/c10d_logger.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/__pycache__/collective_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/__pycache__/constants.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/__pycache__/device_mesh.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/__pycache__/rendezvous.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/_sharding_spec/__init__.py +22 -0
- lib/python3.12/site-packages/torch/distributed/_sharding_spec/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/_tools/__init__.py +12 -0
- lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/fsdp2_mem_tracker.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/ilp_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/mem_tracker.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/memory_tracker.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/mod_tracker.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/runtime_estimator.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/sac_estimator.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/sac_ilp.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/_tools/fsdp2_mem_tracker.py +610 -0
- lib/python3.12/site-packages/torch/distributed/_tools/ilp_utils.py +291 -0
- lib/python3.12/site-packages/torch/distributed/_tools/mem_tracker.py +943 -0
- lib/python3.12/site-packages/torch/distributed/_tools/memory_tracker.py +295 -0
- lib/python3.12/site-packages/torch/distributed/_tools/mod_tracker.py +238 -0
- lib/python3.12/site-packages/torch/distributed/_tools/runtime_estimator.py +527 -0
- lib/python3.12/site-packages/torch/distributed/_tools/sac_estimator.py +997 -0
- lib/python3.12/site-packages/torch/distributed/_tools/sac_ilp.py +295 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_checkpointer.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_dedup_save_plans.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_dedup_tensors.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_fsspec_filesystem.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_nested_dict.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_sharded_tensor_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_storage_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_traverse.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_version.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/api.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/default_planner.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/filesystem.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/format_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/logger.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/logging_handlers.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/metadata.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/optimizer.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/planner.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/planner_helpers.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/resharding.cpython-312.pyc +0 -0
lib/python3.12/site-packages/torch/distributed/__pycache__/_checkpointable.cpython-312.pyc
ADDED
|
Binary file (1.93 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/__pycache__/_composable_state.cpython-312.pyc
ADDED
|
Binary file (2.01 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/__pycache__/_functional_collectives.cpython-312.pyc
ADDED
|
Binary file (47.6 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/__pycache__/_functional_collectives_impl.cpython-312.pyc
ADDED
|
Binary file (4.47 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/__pycache__/c10d_logger.cpython-312.pyc
ADDED
|
Binary file (5.15 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/__pycache__/collective_utils.cpython-312.pyc
ADDED
|
Binary file (7.53 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/__pycache__/constants.cpython-312.pyc
ADDED
|
Binary file (649 Bytes). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/__pycache__/device_mesh.cpython-312.pyc
ADDED
|
Binary file (41.2 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/__pycache__/rendezvous.cpython-312.pyc
ADDED
|
Binary file (12.3 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/_sharding_spec/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Keep old package for BC purposes, this file should be removed once
|
| 2 |
+
# everything moves to the `torch.distributed._shard` package.
|
| 3 |
+
import sys
|
| 4 |
+
import warnings
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from torch.distributed._shard.sharding_spec import * # noqa: F403
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
with warnings.catch_warnings():
|
| 11 |
+
warnings.simplefilter("always")
|
| 12 |
+
warnings.warn(
|
| 13 |
+
"`torch.distributed._sharding_spec` will be deprecated, "
|
| 14 |
+
"use `torch.distributed._shard.sharding_spec` instead",
|
| 15 |
+
DeprecationWarning,
|
| 16 |
+
stacklevel=2,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
import torch.distributed._shard.sharding_spec as _sharding_spec
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
sys.modules["torch.distributed._sharding_spec"] = _sharding_spec
|
lib/python3.12/site-packages/torch/distributed/_sharding_spec/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (932 Bytes). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/_tools/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .fsdp2_mem_tracker import FSDPMemTracker
|
| 2 |
+
from .mem_tracker import MemTracker
|
| 3 |
+
from .memory_tracker import MemoryTracker
|
| 4 |
+
from .mod_tracker import ModTracker
|
| 5 |
+
from .runtime_estimator import RuntimeEstimator
|
| 6 |
+
from .sac_estimator import (
|
| 7 |
+
MSPS,
|
| 8 |
+
SACEstimator,
|
| 9 |
+
SACGreedyOrderMeta,
|
| 10 |
+
SACStats,
|
| 11 |
+
SACTradeOffStats,
|
| 12 |
+
)
|
lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (616 Bytes). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/fsdp2_mem_tracker.cpython-312.pyc
ADDED
|
Binary file (29.8 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/ilp_utils.cpython-312.pyc
ADDED
|
Binary file (11.9 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/mem_tracker.cpython-312.pyc
ADDED
|
Binary file (47.3 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/memory_tracker.cpython-312.pyc
ADDED
|
Binary file (16 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/mod_tracker.cpython-312.pyc
ADDED
|
Binary file (12.5 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/runtime_estimator.cpython-312.pyc
ADDED
|
Binary file (25 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/sac_estimator.cpython-312.pyc
ADDED
|
Binary file (47.6 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/sac_ilp.cpython-312.pyc
ADDED
|
Binary file (12.3 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/_tools/fsdp2_mem_tracker.py
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from copy import deepcopy
|
| 2 |
+
from datetime import timedelta
|
| 3 |
+
from functools import partial, wraps
|
| 4 |
+
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple, Type, Union
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.distributed as dist
|
| 8 |
+
from torch import nn, optim
|
| 9 |
+
from torch._guards import active_fake_mode
|
| 10 |
+
from torch.distributed._tools.mem_tracker import _RefType, _State, MemTracker
|
| 11 |
+
from torch.distributed.distributed_c10d import (
|
| 12 |
+
_IllegalWork,
|
| 13 |
+
ProcessGroup,
|
| 14 |
+
ReduceOp,
|
| 15 |
+
Work,
|
| 16 |
+
)
|
| 17 |
+
from torch.distributed.fsdp import FSDPModule
|
| 18 |
+
from torch.distributed.fsdp._fully_shard._fsdp_param_group import FSDPParamGroup
|
| 19 |
+
from torch.futures import Future
|
| 20 |
+
from torch.utils._python_dispatch import TorchDispatchMode
|
| 21 |
+
from torch.utils._pytree import tree_map_only
|
| 22 |
+
from torch.utils.weak import WeakIdKeyDictionary, weakref
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
_TOTAL_KEY = "Total"
|
| 26 |
+
|
| 27 |
+
__all__ = ["FSDPMemTracker"]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class _FSDPRefType(_RefType):
|
| 31 |
+
"""
|
| 32 |
+
Enumerates categories of memory usage in FSDP modules, including parameters, gradients, activations,
|
| 33 |
+
and optimizer states.
|
| 34 |
+
|
| 35 |
+
Attributes:
|
| 36 |
+
SHARDED_PARAM (str): Memory usage of sharded parameters.
|
| 37 |
+
UNSHARDED_PARAM (str): Memory usage of unsharded parameters.
|
| 38 |
+
SHARDED_GRAD (str): Memory usage of sharded gradients corresponding to the sharded parameters.
|
| 39 |
+
UNSHARDED_GRAD (str): Memory usage of unsharded gradients corresponding to the unsharded parameters.
|
| 40 |
+
ACT (str): Memory usage of activations and tensors from forward and AC recomputation.
|
| 41 |
+
TEMP (str): Memory usage of temporary tensors during the backward pass including gradients of activations.
|
| 42 |
+
ALL_GATHER (str): Memory usage of all_gather output tensor.
|
| 43 |
+
REDUCE_SCATTER (str): Memory usage of reduce_scatter input tensor.
|
| 44 |
+
OPT (str): Memory usage of tensors storing optimizer states.
|
| 45 |
+
INP (str): Memory usage of input tensors.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
SHARDED_PARAM = "Sharded Param"
|
| 49 |
+
UNSHARDED_PARAM = "Unsharded Param"
|
| 50 |
+
BUFFER = "Buffer"
|
| 51 |
+
SHARDED_GRAD = "Sharded Grad"
|
| 52 |
+
UNSHARDED_GRAD = "Unsharded Grad"
|
| 53 |
+
ACT = "Activation"
|
| 54 |
+
TEMP = "Temp"
|
| 55 |
+
ALL_GATHER = "All Gather"
|
| 56 |
+
REDUCE_SCATTER = "Reduce Scatter"
|
| 57 |
+
OPT = "OptState"
|
| 58 |
+
INP = "Inputs"
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class _SavedFSDPMethods(NamedTuple):
|
| 62 |
+
pre_backward: Callable
|
| 63 |
+
post_backward: Callable
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class _SavedCollectives(NamedTuple):
|
| 67 |
+
all_gather_into_tensor: Callable
|
| 68 |
+
reduce_scatter_tensor: Callable
|
| 69 |
+
all_reduce: Callable
|
| 70 |
+
barrier: Callable
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class _FSDPModState(_State):
|
| 74 |
+
"""
|
| 75 |
+
Enumerates the states of FSDP modules during the forward and backward passes.
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
BEF_PRE_FW = "Before Pre-Forward"
|
| 79 |
+
AFT_PRE_FW = "After Pre-Forward"
|
| 80 |
+
BEF_POST_FW = "Before Post-Forward"
|
| 81 |
+
AFT_POST_FW = "After Post-Forward"
|
| 82 |
+
BEF_PRE_BW = "Before Pre-Backward"
|
| 83 |
+
AFT_PRE_BW = "After Pre-Backward"
|
| 84 |
+
BEF_POST_BW = "Before Post-Backward"
|
| 85 |
+
AFT_POST_BW = "After Post-Backward"
|
| 86 |
+
PRE_FW_AC = "Pre-Forward AC"
|
| 87 |
+
POST_FW_AC = "Post-Forward AC"
|
| 88 |
+
PEAK_FW = "Peak Forward"
|
| 89 |
+
PEAK_BW = "Peak Backward"
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class _FSDPModMemStats:
|
| 93 |
+
"""
|
| 94 |
+
A class to store the memory statistics of an FSDP module.
|
| 95 |
+
|
| 96 |
+
Args:
|
| 97 |
+
mod_fqn (str): The fully qualified name of the FSDP module.
|
| 98 |
+
|
| 99 |
+
Attributes:
|
| 100 |
+
snapshots (Dict[_FSDPModState, Dict[torch.device, Dict[str, int]]]): A dictionary of memory snapshots
|
| 101 |
+
of the module at different states as defined by ``_FSDPModState``. Each key is a device, and
|
| 102 |
+
each value is another dictionary with keys as memory reference types defined by ``_FSDPRefType`` and
|
| 103 |
+
values as the memory consumed in bytes.
|
| 104 |
+
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
def __init__(self, mod_fqn: str) -> None:
|
| 108 |
+
self.mod_fqn = mod_fqn
|
| 109 |
+
self.local_peak: Dict[torch.device, int] = {}
|
| 110 |
+
self.snapshots: Dict[
|
| 111 |
+
_FSDPModState, List[Dict[torch.device, Dict[str, int]]]
|
| 112 |
+
] = {}
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class FSDPMemTracker(MemTracker):
|
| 116 |
+
"""
|
| 117 |
+
A ``TorchDispatchMode`` based context manager that extends ``torch.distributed._tools.mem_tracker.MemTracker`` to track
|
| 118 |
+
and categorize the peak memory and module-wise memory usage of FSDP modules.
|
| 119 |
+
|
| 120 |
+
It tracks the peak memory usage across all the devices of all the FSDP modules in the module tree and categorizes
|
| 121 |
+
the tensor memory usage as defined by ``_FSDPRefType``. Further, it captures memory `snapshots` at different stages of
|
| 122 |
+
the module execution defined by ``_FSDPModState``.
|
| 123 |
+
|
| 124 |
+
Attributes:
|
| 125 |
+
memory_tracking: A weakref key dictionary to store the memory statistics of each module. Each key is a reference
|
| 126 |
+
to a module, and each value is a ``_FSDPModMemStats`` object that stores the memory statistics of the module.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
mod (torch.nn.Module): The root FSDP module to be tracked.
|
| 130 |
+
optm (torch.optim.Optimizer, optional): The optimizer to be tracked.
|
| 131 |
+
|
| 132 |
+
Note: Please refer to ``torch.distributed._tools.mem_tracker.MemTracker`` to learn about the limitations.
|
| 133 |
+
|
| 134 |
+
Example usage
|
| 135 |
+
|
| 136 |
+
.. code-block:: python
|
| 137 |
+
|
| 138 |
+
module = ...
|
| 139 |
+
optimizer = ...
|
| 140 |
+
inp = ...
|
| 141 |
+
fmt = FSDPMemTracker(module, optimizer)
|
| 142 |
+
fmt.track_inputs((inp,))
|
| 143 |
+
with fmt:
|
| 144 |
+
optimizer.zero_grad()
|
| 145 |
+
loss = module(inp)
|
| 146 |
+
print("After Forward:")
|
| 147 |
+
fmt.display_snapshot("current")
|
| 148 |
+
loss.backward()
|
| 149 |
+
optimizer.step()
|
| 150 |
+
fmt.display_snapshot("peak")
|
| 151 |
+
fmt.display_modulewise_snapshots(depth = 3, units = "MB")
|
| 152 |
+
|
| 153 |
+
"""
|
| 154 |
+
|
| 155 |
+
def __init__(
|
| 156 |
+
self,
|
| 157 |
+
mod: torch.nn.Module,
|
| 158 |
+
optm: Optional[torch.optim.Optimizer] = None,
|
| 159 |
+
) -> None:
|
| 160 |
+
super().__init__()
|
| 161 |
+
assert isinstance(mod, FSDPModule), "FSDPMemTracker only supports FSDP modules"
|
| 162 |
+
self._root_mod = mod
|
| 163 |
+
self._optm = optm
|
| 164 |
+
self._in_fake_mode: bool = False
|
| 165 |
+
self._fsdp_mod_to_saved_methods: WeakIdKeyDictionary = WeakIdKeyDictionary()
|
| 166 |
+
self._saved_collectives: _SavedCollectives
|
| 167 |
+
self._ref_class: Type[_RefType] = _FSDPRefType
|
| 168 |
+
|
| 169 |
+
def _instrument_fsdp_sharded_params_grads(
|
| 170 |
+
self, fsdp_param_group: FSDPParamGroup
|
| 171 |
+
) -> None:
|
| 172 |
+
# Track sharded params and grads after initilization
|
| 173 |
+
for fsdp_param in fsdp_param_group.fsdp_params:
|
| 174 |
+
self._update_and_maybe_create_winfos(
|
| 175 |
+
fsdp_param.sharded_param,
|
| 176 |
+
_FSDPRefType.SHARDED_PARAM,
|
| 177 |
+
)
|
| 178 |
+
sharded_grad = fsdp_param.sharded_param.grad
|
| 179 |
+
if sharded_grad is not None:
|
| 180 |
+
self._update_and_maybe_create_winfos(
|
| 181 |
+
sharded_grad,
|
| 182 |
+
_FSDPRefType.SHARDED_GRAD,
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
def _fsdp_state_pre_forward(
|
| 186 |
+
self,
|
| 187 |
+
fsdp_mod: FSDPModule,
|
| 188 |
+
orig_fsdp_state_pre_fw: Callable,
|
| 189 |
+
) -> Callable:
|
| 190 |
+
# We capture memory snapshots before and after ``FSDPState._pre_forward`` to attribute the `unsharded` params
|
| 191 |
+
# and `all_gather` buffers. There are three cases:
|
| 192 |
+
# Case 1: If the module is not in the ``memory_tracking`` dictionary, create a new ``_FSDPModMemStats``
|
| 193 |
+
# instance for the module and add it to the ``memory_tracking`` dictionary.
|
| 194 |
+
# Case 2: If the module is already in the ``memory_tracking`` dictionary and we are in backward, this means
|
| 195 |
+
# we are in the AC region. We check if this is the top most module in the AC region. If it is,
|
| 196 |
+
# we store a weak reference and set the flag ``_in_ac`` to True.
|
| 197 |
+
# Case 3: If the module is already in the ``memory_tracking`` dictionary and we are in forward, this means
|
| 198 |
+
# this module is called for the second time. If it is a root module, that means we are in the next
|
| 199 |
+
# iteration and we error out. If it is not a root module, that means it's a submodule that is being
|
| 200 |
+
# used multiple times in the same iteration, which we allow and track.
|
| 201 |
+
# For Case 1 and 3, we also initialiaze the ``local_peak`` and ``PEAK_FW`` snapshot for the module.
|
| 202 |
+
# For Case 2 we only capture 1 snapshot after ``FSDPState._pre_forward`` runs because it is a no-op.
|
| 203 |
+
@wraps(orig_fsdp_state_pre_fw)
|
| 204 |
+
def inner(*args: Any, **kwargs: Any) -> Tuple[Tuple[Any, ...], Dict[str, Any]]:
|
| 205 |
+
mod_fqn = self._mod_tracker.get_known_fqn(fsdp_mod)
|
| 206 |
+
assert mod_fqn is not None
|
| 207 |
+
if fsdp_mod not in self.memory_tracking:
|
| 208 |
+
mod_stat = _FSDPModMemStats(mod_fqn)
|
| 209 |
+
self.memory_tracking[fsdp_mod] = mod_stat
|
| 210 |
+
snapshot = self.get_tracker_snapshot()
|
| 211 |
+
mod_stat.local_peak = {
|
| 212 |
+
dev: dev_snap[_TOTAL_KEY] for dev, dev_snap in snapshot.items()
|
| 213 |
+
}
|
| 214 |
+
mod_stat.snapshots.setdefault(_FSDPModState.PEAK_FW, []).append(
|
| 215 |
+
snapshot
|
| 216 |
+
)
|
| 217 |
+
mod_stat.snapshots.setdefault(_FSDPModState.BEF_PRE_FW, []).append(
|
| 218 |
+
deepcopy(snapshot)
|
| 219 |
+
)
|
| 220 |
+
elif not self._mod_tracker.is_bw:
|
| 221 |
+
parents = self._mod_tracker.parents - {mod_fqn}
|
| 222 |
+
if len(parents) == 1 and "Global" in parents:
|
| 223 |
+
raise NotImplementedError(
|
| 224 |
+
"FSDPMemTracker does not support memory tracking for multiple iterative calls."
|
| 225 |
+
" Either use ``reset_mod_stats`` to clear module memory stats for the previous iteration"
|
| 226 |
+
" or file a github issue if you need this feature."
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
args, kwargs = orig_fsdp_state_pre_fw(*args, **kwargs)
|
| 230 |
+
|
| 231 |
+
fsdp_state = fsdp_mod._get_fsdp_state()
|
| 232 |
+
if fsdp_param_group := fsdp_state._fsdp_param_group:
|
| 233 |
+
for fsdp_param in fsdp_param_group.fsdp_params:
|
| 234 |
+
self._update_and_maybe_create_winfos(
|
| 235 |
+
fsdp_param.unsharded_param,
|
| 236 |
+
_FSDPRefType.UNSHARDED_PARAM,
|
| 237 |
+
)
|
| 238 |
+
mod_stat = self.memory_tracking[fsdp_mod]
|
| 239 |
+
if self._mod_tracker.is_bw:
|
| 240 |
+
state = _FSDPModState.PRE_FW_AC
|
| 241 |
+
if self._ac_mod is None:
|
| 242 |
+
self._ac_mod = weakref.ref(fsdp_mod)
|
| 243 |
+
self._in_ac = True
|
| 244 |
+
else:
|
| 245 |
+
state = _FSDPModState.AFT_PRE_FW
|
| 246 |
+
mod_stat.snapshots.setdefault(state, []).append(self.get_tracker_snapshot())
|
| 247 |
+
return args, kwargs
|
| 248 |
+
|
| 249 |
+
return inner
|
| 250 |
+
|
| 251 |
+
def _fsdp_state_post_forward(
|
| 252 |
+
self,
|
| 253 |
+
fsdp_mod: FSDPModule,
|
| 254 |
+
orig_fsdp_state_post_fw: Callable,
|
| 255 |
+
) -> Callable:
|
| 256 |
+
# We capture memory snapshots before and after ``FSDPState._post_forward`` to capture the resharded state
|
| 257 |
+
# if ``reshard_after_forward`` is not ``False``. There are two cases:
|
| 258 |
+
# Case 1: This is called in backward, which means we are in the AC region. If this is the top most module
|
| 259 |
+
# in the AC region, we set the flag ``_in_ac`` to False.
|
| 260 |
+
# Case 2: This is called in forward.
|
| 261 |
+
@wraps(orig_fsdp_state_post_fw)
|
| 262 |
+
def inner(*args: Any, **kwargs: Any) -> Any:
|
| 263 |
+
mod_stat = self.memory_tracking[fsdp_mod]
|
| 264 |
+
if self._mod_tracker.is_bw:
|
| 265 |
+
state = _FSDPModState.POST_FW_AC
|
| 266 |
+
if self._ac_mod is not None and self._ac_mod() is fsdp_mod:
|
| 267 |
+
self._ac_mod = None
|
| 268 |
+
self._in_ac = False
|
| 269 |
+
else:
|
| 270 |
+
state = _FSDPModState.BEF_POST_FW
|
| 271 |
+
mod_stat.snapshots.setdefault(state, []).append(self.get_tracker_snapshot())
|
| 272 |
+
|
| 273 |
+
output = orig_fsdp_state_post_fw(*args, **kwargs)
|
| 274 |
+
|
| 275 |
+
if not self._mod_tracker.is_bw:
|
| 276 |
+
mod_stat.snapshots.setdefault(_FSDPModState.AFT_POST_FW, []).append(
|
| 277 |
+
self.get_tracker_snapshot()
|
| 278 |
+
)
|
| 279 |
+
return output
|
| 280 |
+
|
| 281 |
+
return inner
|
| 282 |
+
|
| 283 |
+
def _fsdp_param_group_pre_backward(
|
| 284 |
+
self,
|
| 285 |
+
fsdp_mod: FSDPModule,
|
| 286 |
+
orig_fsdp_param_group_pre_backward: Callable,
|
| 287 |
+
) -> Callable:
|
| 288 |
+
# We capture memory snapshots before and after ``FSDPParamGroup.pre_backward`` to capture the pre-fetching
|
| 289 |
+
# and unsharding of params. We also initialize ``local_peak`` and ``PEAK_BW`` snapshot for the module.
|
| 290 |
+
@wraps(orig_fsdp_param_group_pre_backward)
|
| 291 |
+
def inner(*args: Any, **kwargs: Any) -> None:
|
| 292 |
+
mod_stat = self.memory_tracking[fsdp_mod]
|
| 293 |
+
snapshot = self.get_tracker_snapshot()
|
| 294 |
+
mod_stat.local_peak = {
|
| 295 |
+
dev: dev_snap[_TOTAL_KEY] for dev, dev_snap in snapshot.items()
|
| 296 |
+
}
|
| 297 |
+
mod_stat.snapshots.setdefault(_FSDPModState.PEAK_BW, []).append(snapshot)
|
| 298 |
+
mod_stat.snapshots.setdefault(_FSDPModState.BEF_PRE_BW, []).append(
|
| 299 |
+
deepcopy(snapshot)
|
| 300 |
+
)
|
| 301 |
+
orig_fsdp_param_group_pre_backward(*args, **kwargs)
|
| 302 |
+
|
| 303 |
+
mod_stat.snapshots.setdefault(_FSDPModState.AFT_PRE_BW, []).append(
|
| 304 |
+
self.get_tracker_snapshot()
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
return inner
|
| 308 |
+
|
| 309 |
+
def _fsdp_param_group_post_backward(
|
| 310 |
+
self,
|
| 311 |
+
fsdp_mod: FSDPModule,
|
| 312 |
+
orig_fsdp_param_group_post_backward: Callable,
|
| 313 |
+
) -> Callable:
|
| 314 |
+
# We capture the memory snapshots before and after ``FSDPParamGroup.post_backward`` to track and attribute
|
| 315 |
+
# the `unsharded` grads before the post backward and then `sharded` grads and `reduce_scatter` buffers
|
| 316 |
+
# after the post backward.
|
| 317 |
+
@wraps(orig_fsdp_param_group_post_backward)
|
| 318 |
+
def inner(*args: Any, **kwargs: Any) -> None:
|
| 319 |
+
fsdp_state = fsdp_mod._get_fsdp_state()
|
| 320 |
+
if fsdp_param_group := fsdp_state._fsdp_param_group:
|
| 321 |
+
for fsdp_param in fsdp_param_group.fsdp_params:
|
| 322 |
+
unsharded_grad = fsdp_param._unsharded_param.grad
|
| 323 |
+
if unsharded_grad is not None:
|
| 324 |
+
self._update_and_maybe_create_winfos(
|
| 325 |
+
unsharded_grad,
|
| 326 |
+
_FSDPRefType.UNSHARDED_GRAD,
|
| 327 |
+
update_existing=True,
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
mod_stat = self.memory_tracking[fsdp_mod]
|
| 331 |
+
mod_stat.snapshots.setdefault(_FSDPModState.BEF_POST_BW, []).append(
|
| 332 |
+
self.get_tracker_snapshot()
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
orig_fsdp_param_group_post_backward(*args, **kwargs)
|
| 336 |
+
|
| 337 |
+
if fsdp_param_group := fsdp_state._fsdp_param_group:
|
| 338 |
+
for fsdp_param in fsdp_param_group.fsdp_params:
|
| 339 |
+
sharded_grad = fsdp_param.sharded_param.grad
|
| 340 |
+
if sharded_grad is not None:
|
| 341 |
+
self._update_and_maybe_create_winfos(
|
| 342 |
+
sharded_grad,
|
| 343 |
+
_FSDPRefType.SHARDED_GRAD,
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
mod_stat.snapshots.setdefault(_FSDPModState.AFT_POST_BW, []).append(
|
| 347 |
+
self.get_tracker_snapshot()
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
return inner
|
| 351 |
+
|
| 352 |
+
def _instrument_fsdp_module(self) -> None:
|
| 353 |
+
# We uninstall the existing `FSDPState._pre_forward` and `FSDPState._post_forward` hooks and install
|
| 354 |
+
# our own hooks that wrap them. We choose this over monkey-patching `FSDPParamGroup.pre_forward` and
|
| 355 |
+
# `FSDPParamGroup.post_forward` because during AC these won't be called.
|
| 356 |
+
# TODO(@sanketpurandare): This will need to be modified after this PR (https://github.com/pytorch/pytorch/pull/127786)
|
| 357 |
+
# lands. For backward we monkey-patch the `FSDPParamGroup.pre_backward` and `FSDPParamGroup.post_backward`.
|
| 358 |
+
for module in self._root_mod.modules():
|
| 359 |
+
if isinstance(module, FSDPModule):
|
| 360 |
+
fsdp_state = module._get_fsdp_state()
|
| 361 |
+
if fsdp_param_group := fsdp_state._fsdp_param_group:
|
| 362 |
+
self._instrument_fsdp_sharded_params_grads(fsdp_param_group)
|
| 363 |
+
fsdp_state._pre_forward_hook_handle.remove()
|
| 364 |
+
fsdp_state._post_forward_hook_handle.remove()
|
| 365 |
+
fsdp_state._pre_forward_hook_handle = (
|
| 366 |
+
module.register_forward_pre_hook(
|
| 367 |
+
self._fsdp_state_pre_forward(
|
| 368 |
+
module, fsdp_state._pre_forward
|
| 369 |
+
),
|
| 370 |
+
prepend=True,
|
| 371 |
+
with_kwargs=True,
|
| 372 |
+
)
|
| 373 |
+
)
|
| 374 |
+
fsdp_state._post_forward_hook_handle = module.register_forward_hook(
|
| 375 |
+
self._fsdp_state_post_forward(module, fsdp_state._post_forward),
|
| 376 |
+
prepend=False,
|
| 377 |
+
always_call=True,
|
| 378 |
+
)
|
| 379 |
+
self._fsdp_mod_to_saved_methods[module] = _SavedFSDPMethods(
|
| 380 |
+
fsdp_param_group.pre_backward,
|
| 381 |
+
fsdp_param_group.post_backward,
|
| 382 |
+
)
|
| 383 |
+
fsdp_param_group.pre_backward = self._fsdp_param_group_pre_backward( # type: ignore[assignment]
|
| 384 |
+
module, fsdp_param_group.pre_backward
|
| 385 |
+
)
|
| 386 |
+
fsdp_param_group.post_backward = ( # type: ignore[assignment]
|
| 387 |
+
self._fsdp_param_group_post_backward(
|
| 388 |
+
module, fsdp_param_group.post_backward
|
| 389 |
+
)
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
for buffer in self._root_mod.buffers():
|
| 393 |
+
self._update_and_maybe_create_winfos(
|
| 394 |
+
buffer,
|
| 395 |
+
_FSDPRefType.BUFFER,
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
def _instrument_optimizer(self) -> None:
|
| 399 |
+
# Register a hook on the optimizer step to track the optimizer states.
|
| 400 |
+
# The pre-hook is to set the flag ``_in_opt`` to True. The post-hook unsets the flag,
|
| 401 |
+
# and also tracks any optimizer states that are created during the optimizer step.
|
| 402 |
+
if self._optm is not None:
|
| 403 |
+
self._track_optimizer_states(_FSDPRefType.OPT, self._optm)
|
| 404 |
+
|
| 405 |
+
def _opt_step_pre_hook(
|
| 406 |
+
optimizer: optim.Optimizer, args: Any, kwargs: Any
|
| 407 |
+
) -> None:
|
| 408 |
+
self._in_opt = True
|
| 409 |
+
|
| 410 |
+
def _opt_step_post_hook(
|
| 411 |
+
optimizer: optim.Optimizer, args: Any, kwargs: Any
|
| 412 |
+
) -> None:
|
| 413 |
+
self._track_optimizer_states(_FSDPRefType.OPT, optimizer)
|
| 414 |
+
self._in_opt = False
|
| 415 |
+
|
| 416 |
+
self._optimizer_hook_handles = (
|
| 417 |
+
self._optm.register_step_pre_hook(_opt_step_pre_hook),
|
| 418 |
+
self._optm.register_step_post_hook(_opt_step_post_hook),
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
def _register_module_and_optimizer_hooks(self) -> None:
|
| 422 |
+
self._instrument_fsdp_module()
|
| 423 |
+
self._instrument_optimizer()
|
| 424 |
+
|
| 425 |
+
def _deregister_module_and_optimizer_hooks(self) -> None:
|
| 426 |
+
for (
|
| 427 |
+
fsdp_mod,
|
| 428 |
+
saved_methods,
|
| 429 |
+
) in self._fsdp_mod_to_saved_methods.items():
|
| 430 |
+
fsdp_state = fsdp_mod._get_fsdp_state()
|
| 431 |
+
fsdp_state._pre_forward_hook_handle.remove()
|
| 432 |
+
fsdp_state._post_forward_hook_handle.remove()
|
| 433 |
+
fsdp_state._pre_forward_hook_handle = fsdp_mod.register_forward_pre_hook(
|
| 434 |
+
fsdp_state._pre_forward, prepend=True, with_kwargs=True
|
| 435 |
+
)
|
| 436 |
+
fsdp_state._post_forward_hook_handle = fsdp_mod.register_forward_hook(
|
| 437 |
+
fsdp_state._post_forward, prepend=False
|
| 438 |
+
)
|
| 439 |
+
if fsdp_param_group := fsdp_state._fsdp_param_group:
|
| 440 |
+
fsdp_param_group.pre_backward = saved_methods.pre_backward
|
| 441 |
+
fsdp_param_group.post_backward = saved_methods.post_backward
|
| 442 |
+
self._fsdp_mod_to_saved_methods.clear()
|
| 443 |
+
|
| 444 |
+
if self._optimizer_hook_handles is not None:
|
| 445 |
+
for handle in self._optimizer_hook_handles:
|
| 446 |
+
handle.remove()
|
| 447 |
+
self._optimizer_hook_handles = None
|
| 448 |
+
|
| 449 |
+
def _instrument_and_maybe_bypass_collectives(self) -> None:
|
| 450 |
+
# Monkey-patching collectives is required because they do not work with `FakeTensorMode`
|
| 451 |
+
# It's also easier to track `all_gather` and `reduce_scatter` buffers faithfully.
|
| 452 |
+
self._saved_collectives = _SavedCollectives(
|
| 453 |
+
dist.all_gather_into_tensor,
|
| 454 |
+
dist.reduce_scatter_tensor,
|
| 455 |
+
dist.all_reduce,
|
| 456 |
+
dist.barrier,
|
| 457 |
+
)
|
| 458 |
+
|
| 459 |
+
class FakeWork(Work):
|
| 460 |
+
def __init__(self) -> None:
|
| 461 |
+
super().__init__()
|
| 462 |
+
|
| 463 |
+
def get_future(self) -> Future:
|
| 464 |
+
future: Future = Future()
|
| 465 |
+
future.set_result(None)
|
| 466 |
+
return future
|
| 467 |
+
|
| 468 |
+
def wait(self, timeout: Optional[timedelta] = None) -> bool:
|
| 469 |
+
return True
|
| 470 |
+
|
| 471 |
+
@wraps(dist.all_gather_into_tensor)
|
| 472 |
+
def all_gather_into_tensor(
|
| 473 |
+
output_tensor: torch.Tensor,
|
| 474 |
+
input_tensor: torch.Tensor,
|
| 475 |
+
group: Union[ProcessGroup, None] = None,
|
| 476 |
+
async_op: bool = False,
|
| 477 |
+
) -> Union[Work, _IllegalWork, None]:
|
| 478 |
+
self._update_and_maybe_create_winfos(
|
| 479 |
+
output_tensor,
|
| 480 |
+
_FSDPRefType.ALL_GATHER,
|
| 481 |
+
update_existing=True,
|
| 482 |
+
)
|
| 483 |
+
|
| 484 |
+
if self._in_fake_mode:
|
| 485 |
+
if async_op:
|
| 486 |
+
return FakeWork()
|
| 487 |
+
return None
|
| 488 |
+
else:
|
| 489 |
+
return self._saved_collectives.all_gather_into_tensor(
|
| 490 |
+
output_tensor, input_tensor, group, async_op
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
@wraps(dist.reduce_scatter_tensor)
|
| 494 |
+
def reduce_scatter_tensor(
|
| 495 |
+
output: torch.Tensor,
|
| 496 |
+
input: torch.Tensor,
|
| 497 |
+
op: ReduceOp.RedOpType = dist.ReduceOp.SUM,
|
| 498 |
+
group: Union[ProcessGroup, None] = None,
|
| 499 |
+
async_op: bool = False,
|
| 500 |
+
) -> Union[Work, _IllegalWork, None]:
|
| 501 |
+
self._update_and_maybe_create_winfos(
|
| 502 |
+
input,
|
| 503 |
+
_FSDPRefType.REDUCE_SCATTER,
|
| 504 |
+
update_existing=True,
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
if self._in_fake_mode:
|
| 508 |
+
if async_op:
|
| 509 |
+
return FakeWork()
|
| 510 |
+
return None
|
| 511 |
+
else:
|
| 512 |
+
return self._saved_collectives.reduce_scatter_tensor(
|
| 513 |
+
output, input, op, group, async_op
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
@wraps(dist.all_reduce)
|
| 517 |
+
def all_reduce(
|
| 518 |
+
tensor: torch.Tensor,
|
| 519 |
+
op: ReduceOp.RedOpType = dist.ReduceOp.SUM,
|
| 520 |
+
group: Union[ProcessGroup, None] = None,
|
| 521 |
+
async_op: bool = False,
|
| 522 |
+
) -> Union[Work, _IllegalWork, None]:
|
| 523 |
+
if self._in_fake_mode:
|
| 524 |
+
if async_op:
|
| 525 |
+
return FakeWork()
|
| 526 |
+
return None
|
| 527 |
+
else:
|
| 528 |
+
return self._saved_collectives.all_reduce(tensor, op, group, async_op)
|
| 529 |
+
|
| 530 |
+
@wraps(dist.barrier)
|
| 531 |
+
def barrier(
|
| 532 |
+
group: Union[ProcessGroup, None] = dist.GroupMember.WORLD,
|
| 533 |
+
async_op: bool = False,
|
| 534 |
+
device_ids: Union[List[int], None] = None,
|
| 535 |
+
) -> Union[Work, None]:
|
| 536 |
+
if self._in_fake_mode:
|
| 537 |
+
return None
|
| 538 |
+
else:
|
| 539 |
+
return self._saved_collectives.barrier(group, async_op, device_ids)
|
| 540 |
+
|
| 541 |
+
dist.all_gather_into_tensor = all_gather_into_tensor
|
| 542 |
+
dist.reduce_scatter_tensor = reduce_scatter_tensor
|
| 543 |
+
dist.all_reduce = all_reduce
|
| 544 |
+
dist.barrier = barrier
|
| 545 |
+
|
| 546 |
+
def _restore_collectives(self) -> None:
|
| 547 |
+
dist.all_gather_into_tensor = self._saved_collectives.all_gather_into_tensor
|
| 548 |
+
dist.reduce_scatter_tensor = self._saved_collectives.reduce_scatter_tensor
|
| 549 |
+
dist.all_reduce = self._saved_collectives.all_reduce
|
| 550 |
+
dist.barrier = self._saved_collectives.barrier
|
| 551 |
+
del self._saved_collectives
|
| 552 |
+
|
| 553 |
+
def track_inputs(self, inputs: Tuple[Any, ...]) -> None:
|
| 554 |
+
"""
|
| 555 |
+
This is used to track the input tensors to the model and annotate them as ``Inputs``.
|
| 556 |
+
Args:
|
| 557 |
+
inputs (Tuple[Any]): A tuple containing the input data. This can include tensors
|
| 558 |
+
as well as other data types. Only tensors will be tracked.
|
| 559 |
+
"""
|
| 560 |
+
|
| 561 |
+
def _track_inputs(t: torch.Tensor) -> None:
|
| 562 |
+
self._update_and_maybe_create_winfos(
|
| 563 |
+
t,
|
| 564 |
+
_FSDPRefType.INP,
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
tree_map_only(torch.Tensor, _track_inputs, inputs)
|
| 568 |
+
|
| 569 |
+
def track_external(
|
| 570 |
+
self, *external: Union[nn.Module, optim.Optimizer, torch.Tensor]
|
| 571 |
+
) -> None:
|
| 572 |
+
"""This is no-op for ``FSDPMemTracker``"""
|
| 573 |
+
|
| 574 |
+
def __enter__(self) -> "FSDPMemTracker":
|
| 575 |
+
self._in_fake_mode = True if active_fake_mode() else False
|
| 576 |
+
self._register_module_and_optimizer_hooks()
|
| 577 |
+
self._instrument_and_maybe_bypass_collectives()
|
| 578 |
+
self._track_resize()
|
| 579 |
+
self._peak_mem_snap = self.get_tracker_snapshot()
|
| 580 |
+
self._peak_mem = {
|
| 581 |
+
dev: dev_snap[_TOTAL_KEY] for dev, dev_snap in self._peak_mem_snap.items()
|
| 582 |
+
}
|
| 583 |
+
self._mod_tracker.__enter__()
|
| 584 |
+
TorchDispatchMode.__enter__(self)
|
| 585 |
+
return self
|
| 586 |
+
|
| 587 |
+
def __exit__(self, *args: Any) -> None:
|
| 588 |
+
self._deregister_module_and_optimizer_hooks()
|
| 589 |
+
self._restore_collectives()
|
| 590 |
+
self._restore_resize()
|
| 591 |
+
TorchDispatchMode.__exit__(self, *args)
|
| 592 |
+
self._mod_tracker.__exit__(*args)
|
| 593 |
+
|
| 594 |
+
def __torch_dispatch__(self, func, types, args=..., kwargs=None): # type: ignore[no-untyped-def]
|
| 595 |
+
res = func(*args, **kwargs or {})
|
| 596 |
+
# If we are tracking an optimizer state, we use the optimizer reference type.
|
| 597 |
+
# If we are in backward region and not in AC region, we use the backward reference type.
|
| 598 |
+
# Else we use the forward reference type.
|
| 599 |
+
if self._in_opt:
|
| 600 |
+
reftype = _FSDPRefType.OPT
|
| 601 |
+
elif self._mod_tracker.is_bw and not self._in_ac:
|
| 602 |
+
reftype = _FSDPRefType.TEMP
|
| 603 |
+
else:
|
| 604 |
+
reftype = _FSDPRefType.ACT
|
| 605 |
+
tree_map_only(torch.Tensor, partial(self._track, reftype), res)
|
| 606 |
+
peak_state = (
|
| 607 |
+
_FSDPModState.PEAK_BW if self._mod_tracker.is_bw else _FSDPModState.PEAK_FW
|
| 608 |
+
)
|
| 609 |
+
self._update_peak_stats(peak_state)
|
| 610 |
+
return res
|
lib/python3.12/site-packages/torch/distributed/_tools/ilp_utils.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
from typing import cast, Dict, List, OrderedDict, Tuple, TypedDict
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from torch.distributed._tools.mem_tracker import (
|
| 8 |
+
_MemRefType,
|
| 9 |
+
_ModMemStats,
|
| 10 |
+
_ModState,
|
| 11 |
+
MemTracker,
|
| 12 |
+
)
|
| 13 |
+
from torch.distributed._tools.runtime_estimator import RuntimeEstimator
|
| 14 |
+
from torch.distributed._tools.sac_estimator import SACEstimator, SACTradeOffStats
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ModOrder(TypedDict):
|
| 18 |
+
fw_pre_order: List[str]
|
| 19 |
+
bw_pre_order: List[str]
|
| 20 |
+
fw_post_order: List[str]
|
| 21 |
+
bw_post_order: List[str]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class ModRuntime(TypedDict):
|
| 25 |
+
fw: float
|
| 26 |
+
bw: float
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class ModStats(TypedDict):
|
| 30 |
+
fqn: str
|
| 31 |
+
# per-module params
|
| 32 |
+
param_per_module: int
|
| 33 |
+
# per-module grads
|
| 34 |
+
grad_per_module: int
|
| 35 |
+
# total accumulated gradients up to and including this module
|
| 36 |
+
grad_total: int
|
| 37 |
+
# per module fw activation size (excluding input and output)
|
| 38 |
+
act_fw_per_module: int
|
| 39 |
+
# per module bw activation size during peak_bw
|
| 40 |
+
act_bw_per_module: int
|
| 41 |
+
# per module activation grad size during peak_bw
|
| 42 |
+
act_grad_per_module: int
|
| 43 |
+
# total activation size up to but excluding the current module
|
| 44 |
+
# includes input of the current module (i.e., output of previous module)
|
| 45 |
+
act_total: int
|
| 46 |
+
# Inputs to the module
|
| 47 |
+
input_per_module: int
|
| 48 |
+
# Outputs of the module
|
| 49 |
+
output_per_module: int
|
| 50 |
+
# Total fw run-time of the module
|
| 51 |
+
fw_runtime_per_module: float
|
| 52 |
+
# Total bw run-time of the module
|
| 53 |
+
bw_runtime_per_module: float
|
| 54 |
+
# Is this module a leaf module
|
| 55 |
+
is_leaf: bool
|
| 56 |
+
# Total ac run-time of the module
|
| 57 |
+
sac_runtime: float
|
| 58 |
+
# Total ac_memory for the module
|
| 59 |
+
sac_memory: int
|
| 60 |
+
# Number of piecewise-linear functions used for approximating ac tradeoff curve
|
| 61 |
+
n_segments: int
|
| 62 |
+
# Slopes of the of piecewise-linear functions
|
| 63 |
+
slopes: List[float]
|
| 64 |
+
# Intercepts of the of piecewise-linear functions
|
| 65 |
+
intercepts: List[float]
|
| 66 |
+
# X breakpoints of the of piecewise-linear functions
|
| 67 |
+
breakpoints: List[float]
|
| 68 |
+
# Original trade-off curves
|
| 69 |
+
tradeoff_curve: OrderedDict[float, float]
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class ModuleInfo(TypedDict):
|
| 73 |
+
mod_order: ModOrder
|
| 74 |
+
mod_stats: List[ModStats]
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def aggregate_stats(
|
| 78 |
+
model: torch.nn.Module,
|
| 79 |
+
mem_tracker: MemTracker,
|
| 80 |
+
runtime_estimator: RuntimeEstimator,
|
| 81 |
+
sac_estimator: SACEstimator,
|
| 82 |
+
dev: torch.device,
|
| 83 |
+
) -> ModuleInfo:
|
| 84 |
+
"""
|
| 85 |
+
Collect modulewise stats for a given model, including memory, runtime, and AC tradeoff stats.
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
model: nn.Module object
|
| 89 |
+
runtime_estimator: RuntimeEstimator object with runtime stats
|
| 90 |
+
mem_tracker: MemTracker object with memory stats
|
| 91 |
+
sac_estimator: SACEstimator object with AC tradeoff stats
|
| 92 |
+
dev: device the model was run on (used to extract memory stats from MemTracker)
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
ModuleInfo: A dictionary with module order and module stats.
|
| 96 |
+
"""
|
| 97 |
+
|
| 98 |
+
# Memory stats
|
| 99 |
+
mod_mem_stats: Dict[torch.nn.Module, _ModMemStats] = dict(
|
| 100 |
+
copy.deepcopy(mem_tracker.memory_tracking)
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
# Runtime stats
|
| 104 |
+
mod_runtime_stats: Dict[str, ModRuntime] = {
|
| 105 |
+
fqn: {"fw": v["fw"], "bw": v["bw"]}
|
| 106 |
+
for fqn, v in runtime_estimator.mod_runtimes.items()
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
# Module order
|
| 110 |
+
mod_order: ModOrder = {
|
| 111 |
+
"fw_pre_order": list(runtime_estimator.mod_fw_pre_order),
|
| 112 |
+
"bw_pre_order": list(runtime_estimator.mod_bw_pre_order),
|
| 113 |
+
"fw_post_order": list(runtime_estimator.mod_fw_post_order),
|
| 114 |
+
"bw_post_order": list(runtime_estimator.mod_bw_post_order),
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
# Selective Activation Checkpointing stats
|
| 118 |
+
sac_estimator.pwlf_sac_tradeoff_curve()
|
| 119 |
+
mod_sac_tradeoff_stats: Dict[str, SACTradeOffStats] = copy.deepcopy(
|
| 120 |
+
sac_estimator.sac_mod_tradeoff_stats
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
module_info: ModuleInfo = {
|
| 124 |
+
"mod_order": mod_order,
|
| 125 |
+
"mod_stats": [],
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
for mod in model.modules():
|
| 129 |
+
if mod_mem_stat := mod_mem_stats.get(mod, None):
|
| 130 |
+
if tradeoff_stats := mod_sac_tradeoff_stats.get(mod_mem_stat.mod_fqn, None):
|
| 131 |
+
sac_runtime = tradeoff_stats.sac_runtime
|
| 132 |
+
sac_memory = tradeoff_stats.sac_memory
|
| 133 |
+
n_segments = tradeoff_stats.n_segments
|
| 134 |
+
slopes = tradeoff_stats.slopes
|
| 135 |
+
intercepts = tradeoff_stats.intercepts
|
| 136 |
+
breakpoints = tradeoff_stats.fit_breaks
|
| 137 |
+
tradeoff_curve = tradeoff_stats.tradeoff_curve
|
| 138 |
+
is_leaf = False
|
| 139 |
+
else:
|
| 140 |
+
sac_runtime = sac_memory = n_segments = 0
|
| 141 |
+
slopes = intercepts = breakpoints = []
|
| 142 |
+
tradeoff_curve: OrderedDict[float, float] = OrderedDict() # type: ignore[no-redef]
|
| 143 |
+
is_leaf = True
|
| 144 |
+
mod_stat: ModStats = {
|
| 145 |
+
"fqn": mod_mem_stat.mod_fqn,
|
| 146 |
+
"param_per_module": mod_mem_stat.parameter_mem,
|
| 147 |
+
"grad_per_module": mod_mem_stat.parameter_mem,
|
| 148 |
+
"grad_total": mod_mem_stat.snapshots[_ModState.PRE_BW][-1][dev][
|
| 149 |
+
_MemRefType.GRAD
|
| 150 |
+
],
|
| 151 |
+
"act_fw_per_module": max(
|
| 152 |
+
0,
|
| 153 |
+
mod_mem_stat.snapshots[_ModState.POST_FW][-1][dev][_MemRefType.ACT]
|
| 154 |
+
- mod_mem_stat.snapshots[_ModState.PRE_FW][-1][dev][_MemRefType.ACT]
|
| 155 |
+
- mod_mem_stat.output_mem,
|
| 156 |
+
),
|
| 157 |
+
"act_bw_per_module": max(
|
| 158 |
+
0,
|
| 159 |
+
mod_mem_stat.snapshots[_ModState.PEAK_BW][-1][dev][_MemRefType.ACT],
|
| 160 |
+
),
|
| 161 |
+
"act_grad_per_module": (
|
| 162 |
+
mod_mem_stat.snapshots[_ModState.PEAK_BW][-1][dev][_MemRefType.TEMP]
|
| 163 |
+
- mod_mem_stat.snapshots[_ModState.PRE_BW][-1][dev][
|
| 164 |
+
_MemRefType.TEMP
|
| 165 |
+
]
|
| 166 |
+
),
|
| 167 |
+
"act_total": mod_mem_stat.snapshots[_ModState.POST_FW][-1][dev][
|
| 168 |
+
_MemRefType.ACT
|
| 169 |
+
],
|
| 170 |
+
"input_per_module": mod_mem_stat.input_mem,
|
| 171 |
+
"output_per_module": mod_mem_stat.output_mem,
|
| 172 |
+
"fw_runtime_per_module": mod_runtime_stats[mod_mem_stat.mod_fqn]["fw"],
|
| 173 |
+
"bw_runtime_per_module": mod_runtime_stats[mod_mem_stat.mod_fqn]["bw"],
|
| 174 |
+
"is_leaf": is_leaf,
|
| 175 |
+
"sac_runtime": sac_runtime,
|
| 176 |
+
"sac_memory": sac_memory,
|
| 177 |
+
"n_segments": n_segments,
|
| 178 |
+
"slopes": slopes,
|
| 179 |
+
"intercepts": intercepts,
|
| 180 |
+
"breakpoints": breakpoints,
|
| 181 |
+
"tradeoff_curve": tradeoff_curve,
|
| 182 |
+
}
|
| 183 |
+
module_info["mod_stats"].append(mod_stat)
|
| 184 |
+
|
| 185 |
+
return module_info
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
class Node(ModStats):
|
| 189 |
+
index: int # index according to forward pre-order
|
| 190 |
+
pos_fw_post_order: int # index according to forward post-order
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
class Graph:
|
| 194 |
+
def __init__(self, n: int) -> None:
|
| 195 |
+
self.nodes: List[Node] = []
|
| 196 |
+
self.name2node: Dict[str, Node] = {}
|
| 197 |
+
self.ad_matrix = np.zeros((n, n))
|
| 198 |
+
self.fw_post_order: List[str] = []
|
| 199 |
+
|
| 200 |
+
def add_node(self, node: Node) -> None:
|
| 201 |
+
self.nodes.append(node)
|
| 202 |
+
self.name2node[node["fqn"]] = node
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def parse_module_info(module_info: ModuleInfo) -> Graph:
|
| 206 |
+
"""
|
| 207 |
+
Parse module info and create a graph (tree) of modules. The graph will be
|
| 208 |
+
used by MILP solver to find optimal SAC and/or FSDP configurations.
|
| 209 |
+
"""
|
| 210 |
+
mod_stats = module_info["mod_stats"]
|
| 211 |
+
fw_pre_order = module_info["mod_order"]["fw_pre_order"]
|
| 212 |
+
# assertion and number of nodes
|
| 213 |
+
assert len(mod_stats) == len(fw_pre_order)
|
| 214 |
+
n_nodes = len(mod_stats)
|
| 215 |
+
|
| 216 |
+
# create graph
|
| 217 |
+
g = Graph(n_nodes)
|
| 218 |
+
g.fw_post_order = module_info["mod_order"]["fw_post_order"]
|
| 219 |
+
|
| 220 |
+
# sort the modules by pre-order and add them to the graph
|
| 221 |
+
module_info["mod_stats"] = sorted(
|
| 222 |
+
mod_stats, key=lambda x: fw_pre_order.index(x["fqn"])
|
| 223 |
+
)
|
| 224 |
+
for i, one_mod_stats in enumerate(mod_stats):
|
| 225 |
+
node: Node = cast(Node, one_mod_stats)
|
| 226 |
+
node["index"] = i
|
| 227 |
+
node["pos_fw_post_order"] = g.fw_post_order.index(node["fqn"])
|
| 228 |
+
g.add_node(node)
|
| 229 |
+
|
| 230 |
+
# set up ancestor-descendant matrix
|
| 231 |
+
for i in range(n_nodes):
|
| 232 |
+
for j in range(i, n_nodes):
|
| 233 |
+
if is_self_or_submodule(g.nodes[j]["fqn"], g.nodes[i]["fqn"]):
|
| 234 |
+
g.ad_matrix[i][j] = 1
|
| 235 |
+
else:
|
| 236 |
+
break
|
| 237 |
+
|
| 238 |
+
return g
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def is_self_or_submodule(name_descendant: str, name_ancestor: str) -> bool:
|
| 242 |
+
"""
|
| 243 |
+
check if name_descendant is a submodule of name_ancestor, or if they are the same
|
| 244 |
+
"""
|
| 245 |
+
return name_descendant == name_ancestor or name_ancestor + "." in name_descendant
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def is_submodule(name_descendant: str, name_ancestor: str) -> bool:
|
| 249 |
+
"""
|
| 250 |
+
if name_descendant is a submodule of name_ancestor, but not the same
|
| 251 |
+
"""
|
| 252 |
+
return name_ancestor + "." in name_descendant
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def display_bytes(b: int, unit: str = "MiB") -> str:
|
| 256 |
+
"""
|
| 257 |
+
return a string that represent the number of bytes in a desired unit
|
| 258 |
+
"""
|
| 259 |
+
if unit == "KiB":
|
| 260 |
+
return f"{b/2**10:.2f} KiB"
|
| 261 |
+
if unit == "MiB":
|
| 262 |
+
return f"{b/2**20:.2f} MiB"
|
| 263 |
+
if unit == "GiB":
|
| 264 |
+
return f"{b/2**30:.2f} GiB"
|
| 265 |
+
return f"{b:.2f} bytes"
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def get_peak_memory_runtime_baseline(graph: Graph) -> Tuple[int, float]:
|
| 269 |
+
"""
|
| 270 |
+
Get the baseline peak memory and runtime.
|
| 271 |
+
Baseline here means there is no FSDP or AC.
|
| 272 |
+
Memory includes the parameters, gradients, activations, and activation gradients.
|
| 273 |
+
Memory does not include e.g., optimizer states, embedding tables, etc.
|
| 274 |
+
|
| 275 |
+
Returns:
|
| 276 |
+
int: peak memory in bytes
|
| 277 |
+
float: compute time in ms
|
| 278 |
+
"""
|
| 279 |
+
P_1 = graph.nodes[0]["param_per_module"]
|
| 280 |
+
num_nodes = len(graph.nodes)
|
| 281 |
+
peak_mem = 0
|
| 282 |
+
for i in range(num_nodes):
|
| 283 |
+
TG_i = graph.nodes[i]["grad_total"]
|
| 284 |
+
AG_i = graph.nodes[i]["act_grad_per_module"]
|
| 285 |
+
TA_i = graph.nodes[i]["act_total"]
|
| 286 |
+
peak_mem = max(peak_mem, P_1 + TG_i + AG_i + TA_i)
|
| 287 |
+
compute_time = (
|
| 288 |
+
graph.nodes[0]["fw_runtime_per_module"]
|
| 289 |
+
+ graph.nodes[0]["bw_runtime_per_module"]
|
| 290 |
+
)
|
| 291 |
+
return (peak_mem, compute_time)
|
lib/python3.12/site-packages/torch/distributed/_tools/mem_tracker.py
ADDED
|
@@ -0,0 +1,943 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
import warnings
|
| 5 |
+
from copy import deepcopy
|
| 6 |
+
from enum import auto, Enum
|
| 7 |
+
from functools import partial, wraps
|
| 8 |
+
from typing import (
|
| 9 |
+
Any,
|
| 10 |
+
Callable,
|
| 11 |
+
Dict,
|
| 12 |
+
List,
|
| 13 |
+
Optional,
|
| 14 |
+
Set,
|
| 15 |
+
Tuple,
|
| 16 |
+
Type,
|
| 17 |
+
TYPE_CHECKING,
|
| 18 |
+
Union,
|
| 19 |
+
)
|
| 20 |
+
from typing_extensions import Self
|
| 21 |
+
|
| 22 |
+
import torch
|
| 23 |
+
from torch import nn, optim
|
| 24 |
+
from torch.distributed._tools.mod_tracker import ModTracker
|
| 25 |
+
from torch.optim.optimizer import (
|
| 26 |
+
register_optimizer_step_post_hook,
|
| 27 |
+
register_optimizer_step_pre_hook,
|
| 28 |
+
)
|
| 29 |
+
from torch.utils._python_dispatch import (
|
| 30 |
+
is_traceable_wrapper_subclass,
|
| 31 |
+
TorchDispatchMode,
|
| 32 |
+
)
|
| 33 |
+
from torch.utils._pytree import tree_flatten, tree_map_only
|
| 34 |
+
from torch.utils.weak import WeakIdKeyDictionary, weakref
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
if TYPE_CHECKING:
|
| 38 |
+
from torch.utils.hooks import RemovableHandle
|
| 39 |
+
|
| 40 |
+
# This value is hard-coded here:
|
| 41 |
+
# https://github.com/pytorch/pytorch/blob/5fba5d83f0703ff8077ab65448a998e9ad6598fd/c10/cuda/CUDACachingAllocator.cpp#L117
|
| 42 |
+
_PYTORCH_MIN_ALLOCATE = (
|
| 43 |
+
2**9 if int(os.environ.get("PYTORCH_NO_CUDA_MEMORY_CACHING", 0)) == 0 else 1
|
| 44 |
+
)
|
| 45 |
+
_TOTAL_KEY = "Total"
|
| 46 |
+
|
| 47 |
+
__all__ = ["MemTracker"]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class _RefType(str, Enum):
|
| 51 |
+
"""Base Class for defining memory reference types, categorizing tensors based on their usage within a model."""
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class _State(str, Enum):
|
| 55 |
+
"""Base Class for defining module state to capture snapshots ."""
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class _MemRefType(_RefType):
|
| 59 |
+
"""
|
| 60 |
+
An enum to define memory reference types, categorizing tensors based on their usage within a model.
|
| 61 |
+
|
| 62 |
+
- PARAM: Tensors registered as nn.Parameter within modules.
|
| 63 |
+
- BUFFER: Tensors registered as nn.Buffer within modules.
|
| 64 |
+
- GRAD: Gradients associated with parameters.
|
| 65 |
+
- ACT: Tensors produced during the forward pass and recomputation in activation checkpointing.
|
| 66 |
+
- TMP: Temporary memory used during the backward pass, including gradients of activations.
|
| 67 |
+
- OPT: Tensors holding optimizer states.
|
| 68 |
+
- OTH: Tensors registered via `track_external` that do not fit the above categories.
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
PARAM = "Parameter"
|
| 72 |
+
BUFFER = "Buffer"
|
| 73 |
+
GRAD = "Gradient"
|
| 74 |
+
ACT = "Activation"
|
| 75 |
+
TEMP = "Temp"
|
| 76 |
+
OPT = "Optstate"
|
| 77 |
+
OTH = "Other"
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class _ModState(_State):
|
| 81 |
+
"""
|
| 82 |
+
An enum to define the state of a module.
|
| 83 |
+
|
| 84 |
+
- PRE_FW: The module is about to run the forward pass.
|
| 85 |
+
- POST_FW: The module has finished running the forward pass.
|
| 86 |
+
- PEAK_FW: The module has reached the peak memory usage during the forward pass.
|
| 87 |
+
- PRE_BW: The module is about to run the backward pass.
|
| 88 |
+
- PRE_FW_AC: The module is about to run the forward pass with activation checkpointing.
|
| 89 |
+
- POST_FW_AC: The module has finished running the forward pass with activation checkpointing.
|
| 90 |
+
- POST_BW: The module has finished running the backward pass.
|
| 91 |
+
- PEAK_BW: The module has reached the peak memory usage during the backward pass.
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
PRE_FW = "Pre-Forward"
|
| 95 |
+
POST_FW = "Post-Forward"
|
| 96 |
+
PEAK_FW = "Peak-Forward"
|
| 97 |
+
PRE_BW = "Pre-Backward"
|
| 98 |
+
PRE_FW_AC = "Pre-Forward-AC"
|
| 99 |
+
POST_FW_AC = "Post-Forward-AC"
|
| 100 |
+
POST_BW = "Post-Backward"
|
| 101 |
+
PEAK_BW = "Peak-Backward"
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class _ModMemStats:
|
| 105 |
+
"""
|
| 106 |
+
A class to store the memory statistics of a module.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
mod_fqn (str): The fully qualified name of the module.
|
| 110 |
+
Attributes:
|
| 111 |
+
mod_fqn (str): The fully qualified name of the module.
|
| 112 |
+
parameter_mem (int): The memory usage of the parameters of the module.
|
| 113 |
+
buffer_mem (int): The memory usage of the buffers of the module.
|
| 114 |
+
input_mem (int): The memory usage of the inputs to the module.
|
| 115 |
+
output_mem (int): The memory usage of the outputs from the module.
|
| 116 |
+
snapshots (Dict[_ModState, Dict[torch.device, Dict[str, int]]]): A dictionary of memory snapshots
|
| 117 |
+
of the module at different states defined by ``_ModState``.
|
| 118 |
+
Note:
|
| 119 |
+
The memory snapshot is stored as a dictionary - Dict[torch.device, Dict[str, int]], where each key is a device,
|
| 120 |
+
and each value is another dictionary with keys as memory reference types defined by `_MemRefType` and
|
| 121 |
+
values as the memory consumed in bytes.
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
def __init__(self, mod_fqn: str):
|
| 125 |
+
self.mod_fqn = mod_fqn
|
| 126 |
+
self.parameter_mem: int
|
| 127 |
+
self.buffer_mem: int
|
| 128 |
+
self.input_mem: int
|
| 129 |
+
self.output_mem: int
|
| 130 |
+
self.local_peak: Dict[torch.device, int] = {}
|
| 131 |
+
self.snapshots: Dict[_ModState, List[Dict[torch.device, Dict[str, int]]]] = {}
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class _WeakRefInfo:
|
| 135 |
+
"""
|
| 136 |
+
Manages memory statistics and device attributes for tensor storages.
|
| 137 |
+
"""
|
| 138 |
+
|
| 139 |
+
def __init__(
|
| 140 |
+
self, size: int, element_size: int, device: torch.device, reftype: _RefType
|
| 141 |
+
) -> None:
|
| 142 |
+
"""
|
| 143 |
+
Initializes the ``_WeakRefInfo`` object with tensor storage properties.
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
size (int): The number of elements in the tensor storage.
|
| 147 |
+
element_size (int): The size of each element in the tensor storage.
|
| 148 |
+
device (torch.device): The device on which the tensor is allocated.
|
| 149 |
+
reftype (_RefType): The reference type of the tensor.
|
| 150 |
+
"""
|
| 151 |
+
self.size = size
|
| 152 |
+
self.element_size = element_size
|
| 153 |
+
self.reftype = reftype
|
| 154 |
+
self.device = device
|
| 155 |
+
self.mem_consumed = self._calculate_mem_consumed()
|
| 156 |
+
|
| 157 |
+
def _calculate_mem_consumed(self) -> int:
|
| 158 |
+
"""
|
| 159 |
+
Calculates the memory consumed by the tensor storage, considering device-specific allocation rules.
|
| 160 |
+
|
| 161 |
+
Returns:
|
| 162 |
+
int: The memory consumed in bytes.
|
| 163 |
+
"""
|
| 164 |
+
mem = self.size * self.element_size
|
| 165 |
+
if self.device.type == "cuda":
|
| 166 |
+
return math.ceil((mem) / _PYTORCH_MIN_ALLOCATE) * _PYTORCH_MIN_ALLOCATE
|
| 167 |
+
return mem
|
| 168 |
+
|
| 169 |
+
def update_mem_consumed(self, st: torch.UntypedStorage) -> int:
|
| 170 |
+
"""
|
| 171 |
+
Updates and returns the memory consumed if the storage size has changed.
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
st (torch.UntypedStorage): The tensor storage to check for size updates.
|
| 175 |
+
|
| 176 |
+
Returns:
|
| 177 |
+
int: The updated memory consumed in bytes.
|
| 178 |
+
"""
|
| 179 |
+
if st.size() != self.size:
|
| 180 |
+
self.size = st.size()
|
| 181 |
+
self.mem_consumed = self._calculate_mem_consumed()
|
| 182 |
+
return self.mem_consumed
|
| 183 |
+
|
| 184 |
+
@staticmethod
|
| 185 |
+
def get_untyped_storages(t: torch.Tensor) -> Set[torch.UntypedStorage]:
|
| 186 |
+
"""
|
| 187 |
+
Recursively extracts untyped storages from a tensor or its subclasses.
|
| 188 |
+
|
| 189 |
+
Args:
|
| 190 |
+
t (torch.Tensor): The tensor to extract storages from.
|
| 191 |
+
|
| 192 |
+
Returns:
|
| 193 |
+
Set[torch.UntypedStorage]: A set of untyped storages.
|
| 194 |
+
"""
|
| 195 |
+
unflattened_tensors = [t]
|
| 196 |
+
flattened_tensor_storages = set()
|
| 197 |
+
while len(unflattened_tensors) > 0:
|
| 198 |
+
obj = unflattened_tensors.pop()
|
| 199 |
+
if is_traceable_wrapper_subclass(obj):
|
| 200 |
+
attrs, _ = obj.__tensor_flatten__() # type: ignore[attr-defined]
|
| 201 |
+
unflattened_tensors.extend([getattr(obj, attr) for attr in attrs])
|
| 202 |
+
else:
|
| 203 |
+
if not hasattr(obj, "untyped_storage"):
|
| 204 |
+
warnings.warn(
|
| 205 |
+
f"Expected a tensor or a traceable wrapper-subclass of tensor, but got {type(obj)}",
|
| 206 |
+
category=UserWarning,
|
| 207 |
+
stacklevel=2,
|
| 208 |
+
)
|
| 209 |
+
else:
|
| 210 |
+
flattened_tensor_storages.add(obj.untyped_storage())
|
| 211 |
+
return flattened_tensor_storages
|
| 212 |
+
|
| 213 |
+
@classmethod
|
| 214 |
+
def create_winfo(
|
| 215 |
+
cls,
|
| 216 |
+
st: torch.UntypedStorage,
|
| 217 |
+
device: torch.device,
|
| 218 |
+
reftype: _RefType,
|
| 219 |
+
callback: Optional[Callable[[Self, weakref.ref], Any]] = None,
|
| 220 |
+
) -> Tuple[Self, weakref.ref]:
|
| 221 |
+
"""
|
| 222 |
+
Creates a new ``_WeakRefInfo`` instance and a weak reference to a ``torch.UntypedStorage`` object,
|
| 223 |
+
optionally attaching a callback to the weak reference.
|
| 224 |
+
|
| 225 |
+
Args:
|
| 226 |
+
st (torch.UntypedStorage): The storage object for which to create the weak reference info.
|
| 227 |
+
device (torch.device): The device associated with the storage object.
|
| 228 |
+
reftype (_RefType): The type of reference, used to categorize the storage.
|
| 229 |
+
callback (Optional[Callable[[Self, weakref.ref]]]): A callback function that is called when
|
| 230 |
+
the storage object is about to be finalized (garbage collected). The callback function
|
| 231 |
+
should accept two arguments: the ``_WeakRefInfo`` instance and the weak reference to the storage.
|
| 232 |
+
Returns:
|
| 233 |
+
Tuple[Self, weakref.ref]: A tuple containing the newly created ``_WeakRefInfo`` instance and the
|
| 234 |
+
weak reference to the storage object. The weak reference may have an attached callback if provided.
|
| 235 |
+
"""
|
| 236 |
+
|
| 237 |
+
winfo = cls(st.size(), st.element_size(), device, reftype)
|
| 238 |
+
w_st = weakref.ref(st, partial(callback, winfo) if callback else None)
|
| 239 |
+
return winfo, w_st
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def _get_mem_divisor(units: str) -> int:
|
| 243 |
+
unit_dict = {"B": 1, "KiB": 2**10, "MiB": 2**20, "GiB": 2**30}
|
| 244 |
+
if units in unit_dict:
|
| 245 |
+
return unit_dict[units]
|
| 246 |
+
else:
|
| 247 |
+
raise ValueError(
|
| 248 |
+
f"Unsupported unit: {units}. Supported units are: {', '.join(unit_dict.keys())}"
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def _rounding_fn(value: int, divisor: int, precision: int) -> Union[float, int]:
|
| 253 |
+
return value if divisor == 1 else round(value / divisor, precision)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def _print_snapshot(snapshot: Dict[torch.device, Dict[str, int]], units: str) -> None:
|
| 257 |
+
if len(snapshot) == 0:
|
| 258 |
+
print("No memory tracked.")
|
| 259 |
+
return
|
| 260 |
+
divisor = _get_mem_divisor(units)
|
| 261 |
+
for dev, dev_snap in snapshot.items():
|
| 262 |
+
if _rounding_fn(dev_snap[_TOTAL_KEY], divisor, 2) <= 0:
|
| 263 |
+
continue
|
| 264 |
+
print(
|
| 265 |
+
f"Device: {dev}",
|
| 266 |
+
*(
|
| 267 |
+
f"\t{k}: {_rounding_fn(v, divisor, 2)} {units}"
|
| 268 |
+
for k, v in dev_snap.items()
|
| 269 |
+
),
|
| 270 |
+
sep="\n",
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def _print_snapshot_tabular(
|
| 275 |
+
snapshot: Dict[torch.device, Dict[str, int]], units: str
|
| 276 |
+
) -> None:
|
| 277 |
+
if len(snapshot) == 0:
|
| 278 |
+
print("No memory tracked.")
|
| 279 |
+
return
|
| 280 |
+
try:
|
| 281 |
+
from tabulate import tabulate
|
| 282 |
+
except ImportError as err:
|
| 283 |
+
raise ImportError(
|
| 284 |
+
"Please install tabulate to use the tabulate option."
|
| 285 |
+
) from err
|
| 286 |
+
divisor = _get_mem_divisor(units)
|
| 287 |
+
table_data = []
|
| 288 |
+
key_list = list(next(iter(snapshot.values())).keys())
|
| 289 |
+
headers = ["Device"] + [f"{key}" for key in key_list]
|
| 290 |
+
|
| 291 |
+
for dev, dev_snap in snapshot.items():
|
| 292 |
+
if _rounding_fn(dev_snap[_TOTAL_KEY], divisor, 2) <= 0:
|
| 293 |
+
continue
|
| 294 |
+
row = [str(dev)]
|
| 295 |
+
row.extend(f"{_rounding_fn(v, divisor, 2)} {units}" for v in dev_snap.values())
|
| 296 |
+
table_data.append(row)
|
| 297 |
+
print(tabulate(table_data, headers=headers, tablefmt="rst"))
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def _print_state_snapshots(
|
| 301 |
+
snapshots: Dict[_State, List[Dict[torch.device, Dict[str, int]]]], units: str
|
| 302 |
+
) -> None:
|
| 303 |
+
for state, snapshot_list in snapshots.items():
|
| 304 |
+
print(f"{state}")
|
| 305 |
+
for i, snapshot in enumerate(snapshot_list):
|
| 306 |
+
print(f"# {i + 1}:")
|
| 307 |
+
_print_snapshot(snapshot, units)
|
| 308 |
+
print()
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def _print_state_snapshots_tabular(
|
| 312 |
+
snapshots: Dict[_State, List[Dict[torch.device, Dict[str, int]]]], units: str
|
| 313 |
+
) -> None:
|
| 314 |
+
try:
|
| 315 |
+
from tabulate import tabulate
|
| 316 |
+
except ImportError as err:
|
| 317 |
+
raise ImportError(
|
| 318 |
+
"Please install tabulate to use the tabulate option."
|
| 319 |
+
) from err
|
| 320 |
+
|
| 321 |
+
table_data = []
|
| 322 |
+
last_state_call = None
|
| 323 |
+
divisor = _get_mem_divisor(units)
|
| 324 |
+
for state, snapshot_list in snapshots.items():
|
| 325 |
+
for i, snapshot in enumerate(snapshot_list):
|
| 326 |
+
state_call = f"{state} # {i + 1}"
|
| 327 |
+
for dev, dev_snap in snapshot.items():
|
| 328 |
+
if _rounding_fn(dev_snap[_TOTAL_KEY], divisor, 2) <= 0:
|
| 329 |
+
continue
|
| 330 |
+
row = {
|
| 331 |
+
"State & Call": (
|
| 332 |
+
state_call if state_call != last_state_call else ""
|
| 333 |
+
),
|
| 334 |
+
"Device": str(dev),
|
| 335 |
+
}
|
| 336 |
+
last_state_call = state_call
|
| 337 |
+
for k, v in dev_snap.items():
|
| 338 |
+
row[f"{k}"] = f"{_rounding_fn(v, divisor, 2)} {units}"
|
| 339 |
+
table_data.append(row)
|
| 340 |
+
print(tabulate(table_data, headers="keys", tablefmt="rst"))
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
class _UpdateType(Enum):
|
| 344 |
+
# These are used for tracking updates to the continuouly maintained memory snapshot.
|
| 345 |
+
# ADD - When a new tensor storage is tracked
|
| 346 |
+
# DEL - When a tensor storage is about to be finalized (garbage collected).
|
| 347 |
+
# REF - When a tensor reference is updated, for instance, the gradients are marked as
|
| 348 |
+
# generic backward reference types until the grad_hook categorizes them as gradients.
|
| 349 |
+
# SIZE - When a tensor's storage is resized.
|
| 350 |
+
ADD = auto()
|
| 351 |
+
DEL = auto()
|
| 352 |
+
REF = auto()
|
| 353 |
+
SIZE = auto()
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
class MemTracker(TorchDispatchMode):
|
| 357 |
+
"""
|
| 358 |
+
A TorchDispatchMode to track, categorize and attribute the tensor memory created or accessed within its context.
|
| 359 |
+
|
| 360 |
+
It categorizes the tracked tensors as parameters, buffers, activations, gradients, temporary memory and optimizer states
|
| 361 |
+
as defined by ``_MemRefType`` within its context. It captures memory `snapshots` for the modules, called within its context,
|
| 362 |
+
at various states defined by ``_ModState``.
|
| 363 |
+
|
| 364 |
+
Attributes:
|
| 365 |
+
memory_tracking: A weakref key dictionary to store the memory statistics of each module. Each key
|
| 366 |
+
is a reference to a module, and each value is a ``_ModMemStats`` object that stores the memory
|
| 367 |
+
statistics of the module.
|
| 368 |
+
|
| 369 |
+
Note:
|
| 370 |
+
The MemTracker should be used as a context manager. The modules, optimizers, and any other tensors created within
|
| 371 |
+
the context of MemTracker will be tracked by default. Any tensors or stateful objects such as modules, optimizers etc.
|
| 372 |
+
that need to be tracked but are created outside the MemTracker should be registered using the `track_external` method.
|
| 373 |
+
The `track_external` method should be called before the MemTracker is used. Any tensors created outside the ``MemTracker``
|
| 374 |
+
and not supplied to the `track_external` method will not be tracked by the ``MemTracker``.
|
| 375 |
+
|
| 376 |
+
Example usage:
|
| 377 |
+
|
| 378 |
+
.. code-block:: python
|
| 379 |
+
|
| 380 |
+
module = ...
|
| 381 |
+
optimizer = ...
|
| 382 |
+
inp = ...
|
| 383 |
+
mem_tracker = MemTracker()
|
| 384 |
+
mem_tracker.track_external(module, optimizer, inp)
|
| 385 |
+
with mem_tracker as mt:
|
| 386 |
+
loss = module(inp)
|
| 387 |
+
print("After Forward:")
|
| 388 |
+
mt.display_snapshot("current")
|
| 389 |
+
loss.backward()
|
| 390 |
+
optimizer.step()
|
| 391 |
+
optimizer.zero_grad()
|
| 392 |
+
mt.display_snapshot("peak")
|
| 393 |
+
mt.display_modulewise_snapshots(depth = 3, units = "MiB")
|
| 394 |
+
|
| 395 |
+
Known Limitations:
|
| 396 |
+
- The ``MemTracker`` does not track memory for tensors that bypass the ``TorchDispatchMode`` ex. under ``no_dispatch``.
|
| 397 |
+
- Resizing tensor storages directly by using non-Tensor methods other than using ``torch.Untyped_Storage.resize_``
|
| 398 |
+
is not tracked. File a Github issue if you have use-cases for this.
|
| 399 |
+
- If the tensors are not traceable or wrappable subclasses of ``torch.Tensor``, then the tracker does not know how to
|
| 400 |
+
track their storages. File a Github issue if you have use-cases for this.
|
| 401 |
+
- During AC in the backward pass there might be misattribution between activation and temp memory, but the peak memory
|
| 402 |
+
will be tracked accurately. This will be fixed in the next update by hooking intricately with ``torch.uitls.checkpoint``.
|
| 403 |
+
"""
|
| 404 |
+
|
| 405 |
+
def __init__(self) -> None:
|
| 406 |
+
self.memory_tracking = WeakIdKeyDictionary()
|
| 407 |
+
self._curr_mem_snap: Dict[torch.device, Dict[str, int]] = {}
|
| 408 |
+
self._peak_mem: Dict[torch.device, int] = {}
|
| 409 |
+
self._peak_mem_snap: Dict[torch.device, Dict[str, int]] = {}
|
| 410 |
+
self._param_to_grad_hook_handles = WeakIdKeyDictionary()
|
| 411 |
+
self._optimizer_hook_handles: Optional[
|
| 412 |
+
Tuple[RemovableHandle, RemovableHandle]
|
| 413 |
+
] = None
|
| 414 |
+
# Dictionary to store the ``_WeakRefInfo`` instances corresponding to each tensor's storage.
|
| 415 |
+
self._WINFO = WeakIdKeyDictionary()
|
| 416 |
+
self._mod_tracker = ModTracker()
|
| 417 |
+
# This is a general memory tracker which can be used with any ``_RefType`` subclass
|
| 418 |
+
self._ref_class: Type[_RefType] = _MemRefType
|
| 419 |
+
# Flags to track if we are in the AC region or optimizer step region
|
| 420 |
+
self._in_opt: bool = False
|
| 421 |
+
self._in_ac: bool = False
|
| 422 |
+
# Weak references to the topmost AC module currently active
|
| 423 |
+
self._ac_mod: Optional[weakref.ref] = None
|
| 424 |
+
self._orig_resize = torch.UntypedStorage.resize_
|
| 425 |
+
|
| 426 |
+
def _update_snap(
|
| 427 |
+
self,
|
| 428 |
+
u_type: _UpdateType,
|
| 429 |
+
winfo: _WeakRefInfo,
|
| 430 |
+
old_mem_consumed: Optional[int] = None,
|
| 431 |
+
old_reftype: Optional[_RefType] = None,
|
| 432 |
+
) -> None:
|
| 433 |
+
# Initialize a flag to track if the total memory might drop to zero after updates.
|
| 434 |
+
maybe_zero = False
|
| 435 |
+
# Ensure the device entry exists in the current memory snapshot, initializing if necessary.
|
| 436 |
+
dev_snap = self._curr_mem_snap.setdefault(
|
| 437 |
+
winfo.device, dict.fromkeys(self._ref_class, 0)
|
| 438 |
+
)
|
| 439 |
+
dev_snap.setdefault(_TOTAL_KEY, 0)
|
| 440 |
+
# Handle different types of updates based on the update type (`u_type`).
|
| 441 |
+
if u_type == _UpdateType.ADD:
|
| 442 |
+
# Increase the memory consumed for the specific reference type and update the total.
|
| 443 |
+
dev_snap[winfo.reftype] += winfo.mem_consumed
|
| 444 |
+
dev_snap[_TOTAL_KEY] += winfo.mem_consumed
|
| 445 |
+
elif u_type == _UpdateType.DEL:
|
| 446 |
+
# Decrease the memory consumed for the specific reference type and reduce the total.
|
| 447 |
+
dev_snap[winfo.reftype] -= winfo.mem_consumed
|
| 448 |
+
dev_snap[_TOTAL_KEY] -= winfo.mem_consumed
|
| 449 |
+
maybe_zero = True
|
| 450 |
+
elif u_type == _UpdateType.REF:
|
| 451 |
+
assert old_reftype is not None
|
| 452 |
+
# Adjust memory consumption between two reference types within the same device.
|
| 453 |
+
dev_snap[old_reftype] -= winfo.mem_consumed
|
| 454 |
+
dev_snap[winfo.reftype] += winfo.mem_consumed
|
| 455 |
+
elif u_type == _UpdateType.SIZE:
|
| 456 |
+
assert old_mem_consumed is not None
|
| 457 |
+
# Adjust the memory consumed for a reference type due to a change in size.
|
| 458 |
+
change = winfo.mem_consumed - old_mem_consumed
|
| 459 |
+
dev_snap[winfo.reftype] += change
|
| 460 |
+
dev_snap[_TOTAL_KEY] += change
|
| 461 |
+
maybe_zero = True
|
| 462 |
+
else:
|
| 463 |
+
raise ValueError(f"Invalid update type: {u_type}")
|
| 464 |
+
# Check if the total memory for the device has dropped to zero.
|
| 465 |
+
if maybe_zero:
|
| 466 |
+
if self._curr_mem_snap[winfo.device][_TOTAL_KEY] == 0:
|
| 467 |
+
# Remove the device entry from the memory snapshot if the total memory is zero.
|
| 468 |
+
del self._curr_mem_snap[winfo.device]
|
| 469 |
+
|
| 470 |
+
def _update_and_maybe_create_winfos(
|
| 471 |
+
self,
|
| 472 |
+
t: torch.Tensor,
|
| 473 |
+
reftype: _RefType,
|
| 474 |
+
update_existing: bool = False,
|
| 475 |
+
) -> Set[_WeakRefInfo]:
|
| 476 |
+
sts = _WeakRefInfo.get_untyped_storages(t)
|
| 477 |
+
winfos = set()
|
| 478 |
+
for st in sts:
|
| 479 |
+
# Attempt to retrieve existing ``_WeakRefInfo`` and its weak reference from the tracking dictionary.
|
| 480 |
+
winfo, _ = self._WINFO.get(st, (None, None))
|
| 481 |
+
if winfo is not None:
|
| 482 |
+
# If ``_WeakRefInfo`` exists, check if the reference type needs to be updated.
|
| 483 |
+
old_reftype = winfo.reftype
|
| 484 |
+
if old_reftype != reftype:
|
| 485 |
+
# Update the reference type and apply changes via ``_update_snap``.
|
| 486 |
+
winfo.reftype = reftype
|
| 487 |
+
self._update_snap(_UpdateType.REF, winfo, old_reftype=old_reftype)
|
| 488 |
+
winfos.add(winfo)
|
| 489 |
+
elif update_existing:
|
| 490 |
+
# If no existing ``_WeakRefInfo`` is found and update_existing is True, raise an error.
|
| 491 |
+
raise KeyError("No existing winfo found")
|
| 492 |
+
else:
|
| 493 |
+
# If no existing _WeakRefInfo is found and update_existing is False, create a new ``_WeakRefInfo``.
|
| 494 |
+
winfo, w_st = _WeakRefInfo.create_winfo(
|
| 495 |
+
st, t.device, reftype, self._delete_callback
|
| 496 |
+
)
|
| 497 |
+
# Store the new ``_WeakRefInfo`` and its weak reference in the tracking dictionary.
|
| 498 |
+
self._WINFO[st] = (winfo, w_st)
|
| 499 |
+
# Update the snapshot for the newly added ``_WeakRefInfo``.
|
| 500 |
+
if winfo.mem_consumed > 0:
|
| 501 |
+
self._update_snap(_UpdateType.ADD, winfo)
|
| 502 |
+
winfos.add(winfo)
|
| 503 |
+
return winfos
|
| 504 |
+
|
| 505 |
+
def _delete_callback(self, winfo: _WeakRefInfo, w_st: weakref.ref) -> None:
|
| 506 |
+
# Callback to be called when the storage object corresponding to the ``_WeakRefInfo``
|
| 507 |
+
# instance is about to be finalized.
|
| 508 |
+
if winfo.mem_consumed > 0:
|
| 509 |
+
self._update_snap(_UpdateType.DEL, winfo)
|
| 510 |
+
|
| 511 |
+
def _track_resize(self) -> None:
|
| 512 |
+
# Need to monkey-patch this because ``torch.UntypedStorage.resize_`` is not captured
|
| 513 |
+
# by ``TorchDispatchMode``.
|
| 514 |
+
@wraps(self._orig_resize)
|
| 515 |
+
def resize_(st: torch.UntypedStorage, size: int) -> None:
|
| 516 |
+
self._orig_resize(st, size)
|
| 517 |
+
winfo, _ = self._WINFO.get(st, (None, None))
|
| 518 |
+
if winfo is not None and winfo.size != st.size():
|
| 519 |
+
old_mem_consumed = winfo.mem_consumed
|
| 520 |
+
winfo.update_mem_consumed(st)
|
| 521 |
+
self._update_snap(
|
| 522 |
+
_UpdateType.SIZE, winfo, old_mem_consumed=old_mem_consumed
|
| 523 |
+
)
|
| 524 |
+
|
| 525 |
+
torch.UntypedStorage.resize_ = resize_ # type: ignore[method-assign, assignment]
|
| 526 |
+
|
| 527 |
+
def _restore_resize(self) -> None:
|
| 528 |
+
torch.UntypedStorage.resize_ = self._orig_resize # type: ignore[method-assign]
|
| 529 |
+
|
| 530 |
+
def _update_peak_stats(self, peak_state: _State) -> None:
|
| 531 |
+
# We first capture the current memory snapshot of the current tracker state then,
|
| 532 |
+
# We step through each of the modules we have tracked so far in ``memory_tracking``
|
| 533 |
+
# and check if it is currently active by querying ``_mod_tracker.parents``
|
| 534 |
+
# If it is active, we update the per device peak memory usage for the module
|
| 535 |
+
# corresponding to the ``_State`` which can be ``PEAK_FW`` or ``PEAK_BW``.
|
| 536 |
+
curr_snap = self._curr_mem_snap
|
| 537 |
+
|
| 538 |
+
for mod_stats in self.memory_tracking.values():
|
| 539 |
+
if mod_stats.mod_fqn in self._mod_tracker.parents:
|
| 540 |
+
if peak_state in mod_stats.snapshots:
|
| 541 |
+
for dev, dev_snap in curr_snap.items():
|
| 542 |
+
if mod_stats.local_peak.get(dev, 0) < dev_snap[_TOTAL_KEY]:
|
| 543 |
+
mod_stats.local_peak[dev] = dev_snap[_TOTAL_KEY]
|
| 544 |
+
mod_stats.snapshots[peak_state][-1][dev] = deepcopy(
|
| 545 |
+
dev_snap
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
for dev, dev_snap in curr_snap.items():
|
| 549 |
+
if self._peak_mem.get(dev, 0) < dev_snap[_TOTAL_KEY]:
|
| 550 |
+
self._peak_mem[dev] = dev_snap[_TOTAL_KEY]
|
| 551 |
+
self._peak_mem_snap[dev] = deepcopy(dev_snap)
|
| 552 |
+
|
| 553 |
+
def _track(self, reftype: _RefType, t: torch.Tensor) -> None:
|
| 554 |
+
# Get the storages of the tensor and check if we have already tracked them.
|
| 555 |
+
# If yes, then check if the storage size has changed and update the current snapshot.
|
| 556 |
+
# Else create a new ``_WeakRefInfo`` instance and add it to the dictionary.
|
| 557 |
+
sts = _WeakRefInfo.get_untyped_storages(t)
|
| 558 |
+
for st in sts:
|
| 559 |
+
winfo, _ = self._WINFO.get(st, (None, None))
|
| 560 |
+
if winfo is not None:
|
| 561 |
+
if winfo.size != st.size():
|
| 562 |
+
old_mem_consumed = winfo.mem_consumed
|
| 563 |
+
winfo.update_mem_consumed(st)
|
| 564 |
+
self._update_snap(
|
| 565 |
+
_UpdateType.SIZE, winfo, old_mem_consumed=old_mem_consumed
|
| 566 |
+
)
|
| 567 |
+
return
|
| 568 |
+
else:
|
| 569 |
+
winfo, w_st = _WeakRefInfo.create_winfo(
|
| 570 |
+
st, t.device, reftype, self._delete_callback
|
| 571 |
+
)
|
| 572 |
+
self._WINFO[st] = (winfo, w_st)
|
| 573 |
+
# Update the current snapshot for the newly added ``_WeakRefInfo``.
|
| 574 |
+
if winfo.mem_consumed > 0:
|
| 575 |
+
self._update_snap(_UpdateType.ADD, winfo)
|
| 576 |
+
|
| 577 |
+
def get_tracker_snapshot(
|
| 578 |
+
self, type: str = "current"
|
| 579 |
+
) -> Dict[torch.device, Dict[str, int]]:
|
| 580 |
+
"""
|
| 581 |
+
Capture a snapshot of the memory usage breakdown per device, based on the specified type.
|
| 582 |
+
|
| 583 |
+
Args:
|
| 584 |
+
type (str): The type of snapshot to capture. Can be "current" for the current memory usage or "peak" for the
|
| 585 |
+
peak memory usage. Defaults to "current".
|
| 586 |
+
Returns:
|
| 587 |
+
Dict[torch.device, Dict[str, int]]: A dictionary where each key is a torch.device, and each value is another
|
| 588 |
+
dictionary. This inner dictionary has keys representing memory reference
|
| 589 |
+
types as defined in ``_MemRefType`` and values representing the amount of
|
| 590 |
+
memory consumed in bytes.
|
| 591 |
+
Raises:
|
| 592 |
+
ValueError: If an invalid type is specified.
|
| 593 |
+
"""
|
| 594 |
+
if type == "current":
|
| 595 |
+
return deepcopy(self._curr_mem_snap)
|
| 596 |
+
elif type == "peak":
|
| 597 |
+
return deepcopy(self._peak_mem_snap)
|
| 598 |
+
else:
|
| 599 |
+
raise ValueError(f"Invalid type {type}")
|
| 600 |
+
|
| 601 |
+
def _track_module_params_and_buffers(
|
| 602 |
+
self, module: nn.Module, install_grad_hooks: bool = True
|
| 603 |
+
) -> Tuple[int, int]:
|
| 604 |
+
# Track the parameters and buffers of the module if not already tracked.
|
| 605 |
+
# If the parameters have gradients, track the gradients as well.
|
| 606 |
+
# If install_grad_hooks is True, install a gradient hook on the parameters
|
| 607 |
+
# to track the gradients, if it has not already been installed.
|
| 608 |
+
# Return the total memory consumed by the parameters and buffers.
|
| 609 |
+
def _grad_hook(grad: torch.Tensor) -> None:
|
| 610 |
+
self._update_and_maybe_create_winfos(
|
| 611 |
+
grad,
|
| 612 |
+
_MemRefType.GRAD,
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
param_memory = 0
|
| 616 |
+
for param in module.parameters():
|
| 617 |
+
winfos = self._update_and_maybe_create_winfos(
|
| 618 |
+
param,
|
| 619 |
+
_MemRefType.PARAM,
|
| 620 |
+
)
|
| 621 |
+
param_memory += sum(winfo.mem_consumed for winfo in winfos)
|
| 622 |
+
if param.grad is not None:
|
| 623 |
+
self._update_and_maybe_create_winfos(
|
| 624 |
+
param.grad,
|
| 625 |
+
_MemRefType.GRAD,
|
| 626 |
+
)
|
| 627 |
+
if (
|
| 628 |
+
self._param_to_grad_hook_handles.get(param, None) is None
|
| 629 |
+
and install_grad_hooks
|
| 630 |
+
):
|
| 631 |
+
grad_hook_handle = param.register_hook(_grad_hook)
|
| 632 |
+
post_acc_grad_hook_handle = param.register_post_accumulate_grad_hook(
|
| 633 |
+
lambda p: (_grad_hook(p.grad))
|
| 634 |
+
)
|
| 635 |
+
self._param_to_grad_hook_handles[param] = (
|
| 636 |
+
grad_hook_handle,
|
| 637 |
+
post_acc_grad_hook_handle,
|
| 638 |
+
)
|
| 639 |
+
buffer_memory = 0
|
| 640 |
+
for buffer in module.buffers():
|
| 641 |
+
winfos = self._update_and_maybe_create_winfos(
|
| 642 |
+
buffer,
|
| 643 |
+
_MemRefType.BUFFER,
|
| 644 |
+
)
|
| 645 |
+
buffer_memory += sum(winfo.mem_consumed for winfo in winfos)
|
| 646 |
+
return (param_memory, buffer_memory)
|
| 647 |
+
|
| 648 |
+
def _track_inputs_or_outputs(self, args: Any) -> int:
|
| 649 |
+
# Calculate the memory consumed by the inputs or outputs of the module.
|
| 650 |
+
input_or_output_memory = 0
|
| 651 |
+
|
| 652 |
+
def add_inps_or_outs(t: torch.Tensor) -> None:
|
| 653 |
+
nonlocal input_or_output_memory
|
| 654 |
+
sts = _WeakRefInfo.get_untyped_storages(t)
|
| 655 |
+
for st in sts:
|
| 656 |
+
winfo, _ = self._WINFO.get(st, (None, None))
|
| 657 |
+
if winfo is not None:
|
| 658 |
+
input_or_output_memory += winfo.mem_consumed
|
| 659 |
+
|
| 660 |
+
tree_map_only(torch.Tensor, add_inps_or_outs, args)
|
| 661 |
+
return input_or_output_memory
|
| 662 |
+
|
| 663 |
+
def _pre_fw_hook(self, module: nn.Module, inputs: Any) -> None:
|
| 664 |
+
# This is installed as a pre-fwd user hook with ``ModTracker.`` Based on the following cases we
|
| 665 |
+
# set the state and capture the memory snapshot for the module.
|
| 666 |
+
# Case 1: If the module is not in the ``memory_tracking`` dictionary, we track the parameters, buffers,
|
| 667 |
+
# input and output memory of the module. Create a new ``_ModMemStats`` instance for the module
|
| 668 |
+
# and add it to the ``memory_tracking`` dictionary.
|
| 669 |
+
# Case 2: If the module is already in the ``memory_tracking`` dictionary and we are in backward, this means
|
| 670 |
+
# we are in the AC region. We check if this is the top most module in the AC region. If it is,
|
| 671 |
+
# we store a weak reference and set the flag ``_in_ac`` to True.
|
| 672 |
+
# Case 3: If the module is already in the ``memory_tracking`` dictionary and we are in forward, this means
|
| 673 |
+
# this module is called for the second time. If it is a root module, that means we are in the next
|
| 674 |
+
# iteration and we error out. If it is not a root module, that means it's a submodule that is being
|
| 675 |
+
# used multiple times in the same iteration, which we allow and track.
|
| 676 |
+
# For Case 1 and 3, we also initialiaze the ``local_peak`` and ``PEAK_FW`` snapshot for the module.
|
| 677 |
+
mod_name = self._mod_tracker.get_known_fqn(module)
|
| 678 |
+
assert mod_name is not None
|
| 679 |
+
if module not in self.memory_tracking:
|
| 680 |
+
mod_stats = _ModMemStats(mod_name)
|
| 681 |
+
param_mem, buffer_mem = self._track_module_params_and_buffers(
|
| 682 |
+
module, install_grad_hooks=True
|
| 683 |
+
)
|
| 684 |
+
input_mem = self._track_inputs_or_outputs(inputs)
|
| 685 |
+
mod_stats.parameter_mem = param_mem
|
| 686 |
+
mod_stats.buffer_mem = buffer_mem
|
| 687 |
+
mod_stats.input_mem = input_mem
|
| 688 |
+
self.memory_tracking[module] = mod_stats
|
| 689 |
+
state = _ModState.PRE_FW
|
| 690 |
+
|
| 691 |
+
elif self._mod_tracker.is_bw:
|
| 692 |
+
mod_stats = self.memory_tracking[module]
|
| 693 |
+
state = _ModState.PRE_FW_AC
|
| 694 |
+
if self._ac_mod is None:
|
| 695 |
+
self._ac_mod = weakref.ref(module)
|
| 696 |
+
self._in_ac = True
|
| 697 |
+
else:
|
| 698 |
+
parents = set(self._mod_tracker.parents) - {mod_name}
|
| 699 |
+
if len(parents) == 1 and "Global" in parents:
|
| 700 |
+
raise NotImplementedError(
|
| 701 |
+
"MemTracker does not support memory tracking for multiple iterative calls."
|
| 702 |
+
" Either use ``reset_mod_stats`` to clear module memory stats for the previous iteration"
|
| 703 |
+
" or file a github issue if you need this feature."
|
| 704 |
+
)
|
| 705 |
+
mod_stats = self.memory_tracking[module]
|
| 706 |
+
state = _ModState.PRE_FW
|
| 707 |
+
input_mem = self._track_inputs_or_outputs(inputs)
|
| 708 |
+
mod_stats.input_mem = input_mem
|
| 709 |
+
|
| 710 |
+
mem_snapshot = self.get_tracker_snapshot()
|
| 711 |
+
if state == _ModState.PRE_FW:
|
| 712 |
+
mod_stats.local_peak = {
|
| 713 |
+
dev: dev_snap[_TOTAL_KEY] for dev, dev_snap in mem_snapshot.items()
|
| 714 |
+
}
|
| 715 |
+
mod_stats.snapshots.setdefault(_ModState.PEAK_FW, []).append(mem_snapshot)
|
| 716 |
+
mod_stats.snapshots.setdefault(state, []).append(deepcopy(mem_snapshot))
|
| 717 |
+
|
| 718 |
+
def _post_fw_hook(self, module: nn.Module, inputs: Any, outputs: Any) -> None:
|
| 719 |
+
# This is installed as a post-fwd user hook with ``ModTracker``. Based on the following cases we
|
| 720 |
+
# set the state and capture the memory snapshot for the module.
|
| 721 |
+
# Case 1: This is called in backward, which means we are in the AC region. If this is the top most module
|
| 722 |
+
# in the AC region, we set the flag ``_in_ac`` to False.
|
| 723 |
+
# Case 2: This is called in forward so we calculate the output memory
|
| 724 |
+
# of the module and update its mod_stats.
|
| 725 |
+
mod_stats = self.memory_tracking[module]
|
| 726 |
+
if self._mod_tracker.is_bw:
|
| 727 |
+
state = _ModState.POST_FW_AC
|
| 728 |
+
if self._ac_mod is not None and self._ac_mod() is module:
|
| 729 |
+
self._ac_mod = None
|
| 730 |
+
self._in_ac = False
|
| 731 |
+
else:
|
| 732 |
+
state = _ModState.POST_FW
|
| 733 |
+
output_mem = self._track_inputs_or_outputs(outputs)
|
| 734 |
+
mod_stats.output_mem = output_mem
|
| 735 |
+
mod_stats.snapshots.setdefault(state, []).append(self.get_tracker_snapshot())
|
| 736 |
+
|
| 737 |
+
def _pre_bw_hook(self, module: nn.Module, args: Any) -> None:
|
| 738 |
+
# This is installed as a pre-bwd user hook with ``ModTracker``. We set the state and capture the
|
| 739 |
+
# snapshot for the module. We also initialize the ``local_peak`` and ``PEAK_BW`` snapshot for it.
|
| 740 |
+
# If the module is None, we skip the hook.
|
| 741 |
+
# This can happen since this installed inside a multi-grad hook on the module's output tensors
|
| 742 |
+
# and the module itself may not be alive during backward.
|
| 743 |
+
if module is None:
|
| 744 |
+
warnings.warn("Module is None. Skipping PRE_BW hook.", stacklevel=2)
|
| 745 |
+
return
|
| 746 |
+
mod_stats = self.memory_tracking[module]
|
| 747 |
+
mem_snapshot = self.get_tracker_snapshot()
|
| 748 |
+
mod_stats.local_peak = {
|
| 749 |
+
dev: dev_snap[_TOTAL_KEY] for dev, dev_snap in mem_snapshot.items()
|
| 750 |
+
}
|
| 751 |
+
mod_stats.snapshots.setdefault(_ModState.PEAK_BW, []).append(mem_snapshot)
|
| 752 |
+
mod_stats.snapshots.setdefault(_ModState.PRE_BW, []).append(
|
| 753 |
+
deepcopy(mem_snapshot)
|
| 754 |
+
)
|
| 755 |
+
|
| 756 |
+
def _post_bw_hook(self, module: nn.Module, args: Any) -> None:
|
| 757 |
+
# This is installed as a post-bwd user hook with ``ModTracker``. We set the state and capture the
|
| 758 |
+
# snapshot for the module if it is not None.
|
| 759 |
+
# This can happen since this installed inside a multi-grad hook on the module's input tensors
|
| 760 |
+
# and the module itself may not be alive during backward.
|
| 761 |
+
if module is None:
|
| 762 |
+
warnings.warn("Module is None. Skipping POST_BW hook.", stacklevel=2)
|
| 763 |
+
return
|
| 764 |
+
mod_stats = self.memory_tracking[module]
|
| 765 |
+
mod_stats.snapshots.setdefault(_ModState.POST_BW, []).append(
|
| 766 |
+
self.get_tracker_snapshot()
|
| 767 |
+
)
|
| 768 |
+
|
| 769 |
+
def _track_optimizer_states(
|
| 770 |
+
self, reftype: _RefType, optimizer: optim.Optimizer
|
| 771 |
+
) -> None:
|
| 772 |
+
for states in optimizer.state.values():
|
| 773 |
+
for val in states.values():
|
| 774 |
+
if isinstance(val, torch.Tensor):
|
| 775 |
+
self._update_and_maybe_create_winfos(
|
| 776 |
+
val,
|
| 777 |
+
reftype,
|
| 778 |
+
)
|
| 779 |
+
|
| 780 |
+
def _register_global_optimizer_hook(self) -> None:
|
| 781 |
+
# Register a hook on the optimizer step to track the optimizer states.
|
| 782 |
+
# The pre-hook is to set the flag ``_in_opt`` to True. The post-hook unsets the flag,
|
| 783 |
+
# and also tracks any optimizer states that are created during the optimizer step.
|
| 784 |
+
def _opt_step_pre_hook(
|
| 785 |
+
optimizer: optim.Optimizer, args: Any, kwargs: Any
|
| 786 |
+
) -> None:
|
| 787 |
+
self._in_opt = True
|
| 788 |
+
|
| 789 |
+
def _opt_step_post_hook(
|
| 790 |
+
optimizer: optim.Optimizer, args: Any, kwargs: Any
|
| 791 |
+
) -> None:
|
| 792 |
+
self._track_optimizer_states(_MemRefType.OPT, optimizer)
|
| 793 |
+
self._in_opt = False
|
| 794 |
+
|
| 795 |
+
self._optimizer_hook_handles = (
|
| 796 |
+
register_optimizer_step_pre_hook(_opt_step_pre_hook),
|
| 797 |
+
register_optimizer_step_post_hook(_opt_step_post_hook),
|
| 798 |
+
)
|
| 799 |
+
|
| 800 |
+
def _deregister_param_and_optimizer_hooks(self) -> None:
|
| 801 |
+
for (
|
| 802 |
+
grad_hook_handle,
|
| 803 |
+
post_acc_grad_hook_handle,
|
| 804 |
+
) in self._param_to_grad_hook_handles.values():
|
| 805 |
+
grad_hook_handle.remove()
|
| 806 |
+
post_acc_grad_hook_handle.remove()
|
| 807 |
+
self._param_to_grad_hook_handles.clear()
|
| 808 |
+
|
| 809 |
+
if self._optimizer_hook_handles is not None:
|
| 810 |
+
for handle in self._optimizer_hook_handles:
|
| 811 |
+
handle.remove()
|
| 812 |
+
self._optimizer_hook_handles = None
|
| 813 |
+
|
| 814 |
+
def track_external(
|
| 815 |
+
self, *external: Union[nn.Module, optim.Optimizer, torch.Tensor]
|
| 816 |
+
) -> None:
|
| 817 |
+
"""
|
| 818 |
+
Track tensors and stateful objects like modules, optimizers etc. that are created outside the MemTracker.
|
| 819 |
+
|
| 820 |
+
This method should be called before the ``MemTracker`` is used. Any tensors that are not module parameters, buffers,
|
| 821 |
+
gradients activations, or optimizer states will be categorized as ``Other``. If you want them categorized with a
|
| 822 |
+
custom name, please file a GitHub issue. Any tensors created outside the MemTracker and not supplied to this
|
| 823 |
+
method will not be be tracked by ``MemTracker``.
|
| 824 |
+
|
| 825 |
+
Args:
|
| 826 |
+
*external (Union[nn.Module, optim.Optimizer, torch.Tensor]): The external modules, optimizers, and
|
| 827 |
+
tensors to be tracked.
|
| 828 |
+
"""
|
| 829 |
+
flat_external, _ = tree_flatten(external)
|
| 830 |
+
for obj in flat_external:
|
| 831 |
+
if isinstance(obj, torch.Tensor):
|
| 832 |
+
self._update_and_maybe_create_winfos(
|
| 833 |
+
obj,
|
| 834 |
+
_MemRefType.OTH,
|
| 835 |
+
)
|
| 836 |
+
elif isinstance(obj, torch.nn.Module):
|
| 837 |
+
self._track_module_params_and_buffers(obj, install_grad_hooks=False)
|
| 838 |
+
elif isinstance(obj, optim.Optimizer):
|
| 839 |
+
self._track_optimizer_states(_MemRefType.OPT, obj)
|
| 840 |
+
else:
|
| 841 |
+
raise TypeError(
|
| 842 |
+
f"Object of type {type(obj)} is not supported for tracking. "
|
| 843 |
+
f"Only stateful objects like modules, optimizers, and tensors are supported."
|
| 844 |
+
)
|
| 845 |
+
|
| 846 |
+
def display_snapshot(
|
| 847 |
+
self, type: str = "current", units: str = "B", tabulate: bool = False
|
| 848 |
+
) -> None:
|
| 849 |
+
"""
|
| 850 |
+
Display the memory usage breakdown snapshot of the tracker based on the specified type and units.
|
| 851 |
+
|
| 852 |
+
Keyword args:
|
| 853 |
+
type (str): The type of snapshot to display. Can be "current" for the current memory usage or "peak" for the
|
| 854 |
+
peak memory usage. Defaults to "current".
|
| 855 |
+
units (str): The units to use for displaying memory usage. Defaults to "B". Supports ["B", "KiB", "MiB", "GiB"].
|
| 856 |
+
tabulate (bool): Whether to display the snapshot in a tabular format. Defaults to False.
|
| 857 |
+
"""
|
| 858 |
+
snapshot = self.get_tracker_snapshot(type)
|
| 859 |
+
if tabulate:
|
| 860 |
+
_print_snapshot_tabular(snapshot, units)
|
| 861 |
+
else:
|
| 862 |
+
_print_snapshot(snapshot, units)
|
| 863 |
+
|
| 864 |
+
def display_modulewise_snapshots(
|
| 865 |
+
self, depth: int = 2, units: str = "B", tabulate: bool = False
|
| 866 |
+
) -> None:
|
| 867 |
+
"""
|
| 868 |
+
Print per device memory breakdown snapshot for each module called within MemTracker.
|
| 869 |
+
|
| 870 |
+
Snapshots are displayed for the states defined by ``_ModState``.
|
| 871 |
+
The module hierarchy is displayed up to the specified depth.
|
| 872 |
+
|
| 873 |
+
Keyword Args:
|
| 874 |
+
depth (int, optional): The depth of the module hierarchy to display. Defaults to 2.
|
| 875 |
+
units (str, optional): The units to use for memory tracking. Defaults to "B". Supports ["B", "KiB", "MiB", "GiB"].
|
| 876 |
+
tabulate (bool, optional): Whether to display the snapshot in a tabular format. Defaults to False.
|
| 877 |
+
"""
|
| 878 |
+
|
| 879 |
+
def natural_sort_key(s: str) -> List[Union[int, str]]:
|
| 880 |
+
return [
|
| 881 |
+
int(text) if text.isdigit() else text.lower()
|
| 882 |
+
for text in re.split("([0-9]+)", s)
|
| 883 |
+
]
|
| 884 |
+
|
| 885 |
+
for mod_stats in sorted(
|
| 886 |
+
self.memory_tracking.values(),
|
| 887 |
+
key=lambda m_stats: natural_sort_key(m_stats.mod_fqn),
|
| 888 |
+
):
|
| 889 |
+
mod_fqn = mod_stats.mod_fqn
|
| 890 |
+
mod_depth = mod_fqn.count(".") + 1
|
| 891 |
+
if mod_depth > depth:
|
| 892 |
+
continue
|
| 893 |
+
print(f"Module: {mod_fqn}")
|
| 894 |
+
if tabulate:
|
| 895 |
+
_print_state_snapshots_tabular(mod_stats.snapshots, units)
|
| 896 |
+
else:
|
| 897 |
+
_print_state_snapshots(mod_stats.snapshots, units)
|
| 898 |
+
|
| 899 |
+
def reset_mod_stats(self) -> None:
|
| 900 |
+
"""
|
| 901 |
+
Reset all the module memory stats. Clears ``memory_tracking`` dictionary.
|
| 902 |
+
"""
|
| 903 |
+
self.memory_tracking.clear()
|
| 904 |
+
|
| 905 |
+
def __enter__(self) -> "MemTracker":
|
| 906 |
+
self._register_global_optimizer_hook()
|
| 907 |
+
self._mod_tracker.register_user_hooks(
|
| 908 |
+
self._pre_fw_hook,
|
| 909 |
+
self._post_fw_hook,
|
| 910 |
+
self._pre_bw_hook,
|
| 911 |
+
self._post_bw_hook,
|
| 912 |
+
)
|
| 913 |
+
self._track_resize()
|
| 914 |
+
self._peak_mem_snap = self.get_tracker_snapshot()
|
| 915 |
+
self._peak_mem = {
|
| 916 |
+
dev: dev_snap[_TOTAL_KEY] for dev, dev_snap in self._peak_mem_snap.items()
|
| 917 |
+
}
|
| 918 |
+
self._mod_tracker.__enter__()
|
| 919 |
+
super().__enter__()
|
| 920 |
+
return self
|
| 921 |
+
|
| 922 |
+
def __exit__(self, *args: Any) -> None:
|
| 923 |
+
self._deregister_param_and_optimizer_hooks()
|
| 924 |
+
self._mod_tracker.clear_user_hooks()
|
| 925 |
+
self._restore_resize()
|
| 926 |
+
super().__exit__(*args)
|
| 927 |
+
self._mod_tracker.__exit__(*args)
|
| 928 |
+
|
| 929 |
+
def __torch_dispatch__(self, func, types, args=(), kwargs=None): # type: ignore[no-untyped-def]
|
| 930 |
+
res = func(*args, **kwargs or {})
|
| 931 |
+
# If we are tracking an optimizer state, we use the optimizer reference type.
|
| 932 |
+
# If we are in backward region and not in AC region, we use the backward reference type.
|
| 933 |
+
# Else we use the forward reference type.
|
| 934 |
+
if self._in_opt:
|
| 935 |
+
reftype = _MemRefType.OPT
|
| 936 |
+
elif self._mod_tracker.is_bw and not self._in_ac:
|
| 937 |
+
reftype = _MemRefType.TEMP
|
| 938 |
+
else:
|
| 939 |
+
reftype = _MemRefType.ACT
|
| 940 |
+
tree_map_only(torch.Tensor, partial(self._track, reftype), res)
|
| 941 |
+
peak_state = _ModState.PEAK_BW if self._mod_tracker.is_bw else _ModState.PEAK_FW
|
| 942 |
+
self._update_peak_stats(peak_state)
|
| 943 |
+
return res
|
lib/python3.12/site-packages/torch/distributed/_tools/memory_tracker.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: allow-untyped-defs
|
| 2 |
+
import operator
|
| 3 |
+
import pickle
|
| 4 |
+
from collections import defaultdict
|
| 5 |
+
from itertools import chain
|
| 6 |
+
from typing import Any, Callable, Dict, List, no_type_check, Sequence, TYPE_CHECKING
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
from torch.utils._python_dispatch import TorchDispatchMode
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
if TYPE_CHECKING:
|
| 14 |
+
from torch.utils.hooks import RemovableHandle
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
BYTES_PER_MB = 1024 * 1024.0
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class MemoryProfileDispatchMode(TorchDispatchMode):
|
| 21 |
+
"""Run in ``TorchDispatchMode`` to get memory stats at operator level."""
|
| 22 |
+
|
| 23 |
+
def __init__(self, memory_tracker) -> None:
|
| 24 |
+
self.memory_tracker = memory_tracker
|
| 25 |
+
|
| 26 |
+
def __torch_dispatch__(self, func, types, args=..., kwargs=None):
|
| 27 |
+
rs = func(*args, **kwargs)
|
| 28 |
+
if func == torch.ops.aten.detach.default:
|
| 29 |
+
return rs
|
| 30 |
+
func_name: str = (
|
| 31 |
+
self.memory_tracker._cur_module_name
|
| 32 |
+
+ "."
|
| 33 |
+
+ func.__name__
|
| 34 |
+
+ "_"
|
| 35 |
+
+ str(self.memory_tracker._operator_names[func.__name__])
|
| 36 |
+
)
|
| 37 |
+
self.memory_tracker._operator_names[func.__name__] = (
|
| 38 |
+
self.memory_tracker._operator_names[func.__name__] + 1
|
| 39 |
+
)
|
| 40 |
+
self.memory_tracker._record_memory_stats(func_name)
|
| 41 |
+
|
| 42 |
+
return rs
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class MemoryTracker:
|
| 46 |
+
"""
|
| 47 |
+
Collect and plot the memory stats at operator level.
|
| 48 |
+
|
| 49 |
+
Includes ``memories_allocated``, ``memories_active`` and ``memories_reserved``.
|
| 50 |
+
It also prints a summary for the top 20 operators that generate the most memories.
|
| 51 |
+
|
| 52 |
+
Example usage:
|
| 53 |
+
|
| 54 |
+
>>> # xdoctest: +SKIP(failing)
|
| 55 |
+
>>> net.cuda()
|
| 56 |
+
>>> input = input.cuda()
|
| 57 |
+
|
| 58 |
+
>>> mem_tracker = MemoryTracker()
|
| 59 |
+
>>> mem_tracker.start_monitor(net)
|
| 60 |
+
|
| 61 |
+
>>> net.zero_grad(True)
|
| 62 |
+
>>> loss = net(input)
|
| 63 |
+
>>> if isinstance(loss, dict):
|
| 64 |
+
>>> loss = loss['out']
|
| 65 |
+
>>> loss.sum().backward()
|
| 66 |
+
>>> net.zero_grad(set_to_none=True)
|
| 67 |
+
|
| 68 |
+
>>> mem_tracker.stop()
|
| 69 |
+
>>> mem_tracker.summary()
|
| 70 |
+
>>> mem_tracker.show_traces()
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
def __init__(self) -> None:
|
| 74 |
+
torch._C._log_api_usage_once("torch.distributed.memory_tracker")
|
| 75 |
+
self._hooks: List[RemovableHandle] = []
|
| 76 |
+
self._operator_names: Dict[str, int] = defaultdict(int)
|
| 77 |
+
self.memories_allocated: Dict[int, Dict[str, float]] = defaultdict()
|
| 78 |
+
self.memories_active: Dict[int, Dict[str, float]] = defaultdict()
|
| 79 |
+
self.memories_reserved: Dict[int, Dict[str, float]] = defaultdict()
|
| 80 |
+
self._markers: Dict[str, int] = defaultdict(int)
|
| 81 |
+
self._cur_module_name: str = ""
|
| 82 |
+
self._op_index: int = 0
|
| 83 |
+
self._num_cuda_retries: int = 0
|
| 84 |
+
|
| 85 |
+
@no_type_check
|
| 86 |
+
def start_monitor(self, root_module: nn.Module) -> None:
|
| 87 |
+
"""
|
| 88 |
+
Register module hooks and entering ``MemoryProfileDispatchMode``.
|
| 89 |
+
|
| 90 |
+
This enables operator level memory stats can be tracked during module runtime.
|
| 91 |
+
"""
|
| 92 |
+
self._clear_state()
|
| 93 |
+
root_module.__setattr__("_memory_tracker_is_root", True)
|
| 94 |
+
for name, m in root_module.named_modules():
|
| 95 |
+
if m is not root_module:
|
| 96 |
+
m.__setattr__("_memory_tracker_is_root", False)
|
| 97 |
+
# fused_proxy_group does not support hooks
|
| 98 |
+
if ".fused_proxy_grouped_embedding_bag" in name:
|
| 99 |
+
continue
|
| 100 |
+
# hook ordering with other hooks added by users is not managed, so
|
| 101 |
+
# the memory stats tracked here may not completely accurate.
|
| 102 |
+
h1 = m.register_forward_pre_hook(self._create_pre_forward_hook(name))
|
| 103 |
+
h2 = m.register_forward_hook(self._create_post_forward_hook(name))
|
| 104 |
+
# it does not work well with jagged tensor somehow, the root cause is not
|
| 105 |
+
# clear and remove it for now as it does not really capture important info.
|
| 106 |
+
# h3 = m.register_backward_hook(self._create_backward_hook(name))
|
| 107 |
+
self._hooks.extend([h1, h2])
|
| 108 |
+
torch.cuda.empty_cache()
|
| 109 |
+
assert getattr(self, "profile_mode", None) is None
|
| 110 |
+
self.profile_mode = MemoryProfileDispatchMode(self)
|
| 111 |
+
self.profile_mode.__enter__()
|
| 112 |
+
|
| 113 |
+
@no_type_check
|
| 114 |
+
def stop(self) -> None:
|
| 115 |
+
"""
|
| 116 |
+
Remove module hooks and exit ``MemoryProfileDispatchMode`` to stop tracking memory stats at operator level.
|
| 117 |
+
|
| 118 |
+
Get some aggregated stats when the memory_tracker() is enabled, like cuda ``num_alloc_retries``.
|
| 119 |
+
"""
|
| 120 |
+
self._num_cuda_retries = torch.cuda.memory_stats().get("num_alloc_retries", 0)
|
| 121 |
+
|
| 122 |
+
for h in self._hooks:
|
| 123 |
+
h.remove()
|
| 124 |
+
self._hooks.clear()
|
| 125 |
+
assert getattr(self, "profile_mode", None) is not None
|
| 126 |
+
self.profile_mode.__exit__(None, None, None)
|
| 127 |
+
self.profile_mode = None
|
| 128 |
+
|
| 129 |
+
@no_type_check
|
| 130 |
+
def summary(self, top: int = 20) -> None:
|
| 131 |
+
"""
|
| 132 |
+
Print out the top operators that generate the most memories.
|
| 133 |
+
|
| 134 |
+
The number of the top operators can be configured.
|
| 135 |
+
"""
|
| 136 |
+
op_diff: Dict[str, float] = defaultdict(float)
|
| 137 |
+
op_name, previous_allocated_memory = self.memories_allocated[0]
|
| 138 |
+
for i in range(1, self._op_index):
|
| 139 |
+
op_name, current_allocated_memory = self.memories_allocated[i]
|
| 140 |
+
op_diff[op_name] = current_allocated_memory - previous_allocated_memory
|
| 141 |
+
previous_allocated_memory = current_allocated_memory
|
| 142 |
+
|
| 143 |
+
print("------------------------------------------------")
|
| 144 |
+
print(f"The number of cuda retries are: {self._num_cuda_retries}")
|
| 145 |
+
print(f"Top {top} ops that generates memory are:")
|
| 146 |
+
for k, v in sorted(op_diff.items(), key=operator.itemgetter(1), reverse=True)[
|
| 147 |
+
:top
|
| 148 |
+
]:
|
| 149 |
+
print(f"{k}: {v}MB")
|
| 150 |
+
print("------------------------------------------------")
|
| 151 |
+
|
| 152 |
+
@no_type_check
|
| 153 |
+
def show_traces(self, path: str = "") -> None:
|
| 154 |
+
import matplotlib.pyplot as plt
|
| 155 |
+
|
| 156 |
+
def _plot_figure(x, y_values, labels):
|
| 157 |
+
min_val = min(list(chain(*y_values))) * 0.999
|
| 158 |
+
max_val = max(list(chain(*y_values))) * 1.001
|
| 159 |
+
plt.figure()
|
| 160 |
+
for y, label in zip(y_values, labels):
|
| 161 |
+
plt.plot(x, y, label=label)
|
| 162 |
+
plt.xlabel("# Operator Calls")
|
| 163 |
+
plt.ylabel("Memory (MB)")
|
| 164 |
+
plt.legend()
|
| 165 |
+
for marker_name, marker in self._markers.items():
|
| 166 |
+
if marker_name == "fw_bw_boundary":
|
| 167 |
+
plt.plot(
|
| 168 |
+
[marker, marker],
|
| 169 |
+
[min_val, max_val],
|
| 170 |
+
"r",
|
| 171 |
+
lw=2,
|
| 172 |
+
label=marker_name,
|
| 173 |
+
)
|
| 174 |
+
else:
|
| 175 |
+
plt.plot(
|
| 176 |
+
[marker, marker],
|
| 177 |
+
[min_val, max_val],
|
| 178 |
+
"k-",
|
| 179 |
+
lw=2,
|
| 180 |
+
label=marker_name,
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
if path != "":
|
| 184 |
+
self.load(path)
|
| 185 |
+
|
| 186 |
+
y_1 = [gb for (name, gb) in self.memories_allocated.values()]
|
| 187 |
+
y_2 = [gb for (name, gb) in self.memories_active.values()]
|
| 188 |
+
y_3 = [gb for (name, gb) in self.memories_reserved.values()]
|
| 189 |
+
x = list(range(len(y_1)))
|
| 190 |
+
# Split figures when there is big difference between
|
| 191 |
+
# "reserved_memory" and "allocated_memory" or "active_memory".
|
| 192 |
+
_plot_figure(
|
| 193 |
+
x,
|
| 194 |
+
[list(y_1), list(y_2), list(y_3)],
|
| 195 |
+
["allocated_memory", "active_memory", "reserved_memory"],
|
| 196 |
+
)
|
| 197 |
+
_plot_figure(x, [list(y_1)], ["allocated_memory"])
|
| 198 |
+
_plot_figure(x, [list(y_2)], ["active_memory"])
|
| 199 |
+
_plot_figure(x, [list(y_3)], ["reserved_memory"])
|
| 200 |
+
|
| 201 |
+
def save_stats(self, path: str) -> None:
|
| 202 |
+
"""Save the stats using pickle during runtime if users want to plot the traces in other places like notebook."""
|
| 203 |
+
stats = {
|
| 204 |
+
"memories_allocated": self.memories_allocated,
|
| 205 |
+
"memories_active": self.memories_active,
|
| 206 |
+
"memories_reserved": self.memories_reserved,
|
| 207 |
+
"markers": self._markers,
|
| 208 |
+
"num_alloc_retries": self._num_cuda_retries,
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
with open(path, "wb") as f:
|
| 212 |
+
pickle.dump(stats, f, pickle.HIGHEST_PROTOCOL)
|
| 213 |
+
|
| 214 |
+
def load(self, path: str) -> None:
|
| 215 |
+
"""Load the pickled memory stats to plot the traces or print the summary."""
|
| 216 |
+
with open(path, "rb") as f:
|
| 217 |
+
stats = pickle.load(f)
|
| 218 |
+
|
| 219 |
+
self.memories_allocated = stats["memories_allocated"]
|
| 220 |
+
self.memories_active = stats["memories_active"]
|
| 221 |
+
self.memories_reserved = stats["memories_reserved"]
|
| 222 |
+
self._markers = stats["markers"]
|
| 223 |
+
self._num_cuda_retries = stats["num_alloc_retries"]
|
| 224 |
+
|
| 225 |
+
def _create_pre_forward_hook(self, name: str) -> Callable:
|
| 226 |
+
"""Prefix operator name with current module and 'forward', and insert 'fw_start' marker at forward pass start."""
|
| 227 |
+
|
| 228 |
+
def _pre_forward_hook(module: nn.Module, inputs: Any) -> None:
|
| 229 |
+
self._cur_module_name = f"{name}.forward"
|
| 230 |
+
if (
|
| 231 |
+
hasattr(module, "_memory_tracker_is_root")
|
| 232 |
+
and module._memory_tracker_is_root
|
| 233 |
+
):
|
| 234 |
+
self._add_marker("fw_start")
|
| 235 |
+
|
| 236 |
+
return _pre_forward_hook
|
| 237 |
+
|
| 238 |
+
def _create_post_forward_hook(self, name: str) -> Callable:
|
| 239 |
+
"""Insert the marker 'fw_bw_boundary' at the boundary of forward and backward pass."""
|
| 240 |
+
|
| 241 |
+
def _post_forward_hook(
|
| 242 |
+
module: nn.Module,
|
| 243 |
+
inputs: Sequence[torch.Tensor],
|
| 244 |
+
outputs: Sequence[torch.Tensor],
|
| 245 |
+
) -> None:
|
| 246 |
+
if (
|
| 247 |
+
hasattr(module, "_memory_tracker_is_root")
|
| 248 |
+
and module._memory_tracker_is_root
|
| 249 |
+
):
|
| 250 |
+
self._add_marker("fw_bw_boundary")
|
| 251 |
+
|
| 252 |
+
return _post_forward_hook
|
| 253 |
+
|
| 254 |
+
def _create_backward_hook(self, name: str) -> Callable:
|
| 255 |
+
"""Insert the current module name with backward prefix for the operator name."""
|
| 256 |
+
|
| 257 |
+
def _backward_hook(
|
| 258 |
+
module: nn.Module, grad_input: torch.Tensor, grad_output: torch.Tensor
|
| 259 |
+
) -> None:
|
| 260 |
+
self._cur_module_name = f"{name}.backward"
|
| 261 |
+
|
| 262 |
+
return _backward_hook
|
| 263 |
+
|
| 264 |
+
@no_type_check
|
| 265 |
+
def _record_memory_stats(self, fn_name: str) -> None:
|
| 266 |
+
"""
|
| 267 |
+
Record current memory allocated, current memory active and current memory reserved.
|
| 268 |
+
|
| 269 |
+
The memory stats dict is indexed with ``self._op_index``.
|
| 270 |
+
"""
|
| 271 |
+
memory_allocated: float = torch.cuda.memory_allocated() / BYTES_PER_MB
|
| 272 |
+
memory_reserved: float = torch.cuda.memory_reserved() / BYTES_PER_MB
|
| 273 |
+
memory_active: float = (
|
| 274 |
+
torch.cuda.memory_stats().get("active_bytes.all.current", 0) / BYTES_PER_MB
|
| 275 |
+
)
|
| 276 |
+
self.memories_allocated[self._op_index] = (fn_name, memory_allocated)
|
| 277 |
+
self.memories_reserved[self._op_index] = (fn_name, memory_reserved)
|
| 278 |
+
self.memories_active[self._op_index] = (fn_name, memory_active)
|
| 279 |
+
self._op_index += 1
|
| 280 |
+
|
| 281 |
+
def _add_marker(self, marker_name: str) -> None:
|
| 282 |
+
"""Set the marker's x-axis value."""
|
| 283 |
+
marker_val = len(self.memories_allocated.values())
|
| 284 |
+
self._markers[marker_name] = marker_val
|
| 285 |
+
|
| 286 |
+
def _clear_state(self) -> None:
|
| 287 |
+
"""Clear states when start_monitor() is called."""
|
| 288 |
+
self._operator_names.clear()
|
| 289 |
+
self.memories_allocated.clear()
|
| 290 |
+
self.memories_active.clear()
|
| 291 |
+
self.memories_reserved.clear()
|
| 292 |
+
self._markers.clear()
|
| 293 |
+
self._cur_module_name = ""
|
| 294 |
+
self._op_index = 0
|
| 295 |
+
self._num_cuda_retries = 0
|
lib/python3.12/site-packages/torch/distributed/_tools/mod_tracker.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: allow-untyped-defs
|
| 2 |
+
import warnings
|
| 3 |
+
import weakref
|
| 4 |
+
from typing import Callable, Optional, Set
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from torch.autograd.graph import register_multi_grad_hook
|
| 8 |
+
from torch.nn.modules.module import (
|
| 9 |
+
register_module_forward_hook,
|
| 10 |
+
register_module_forward_pre_hook,
|
| 11 |
+
)
|
| 12 |
+
from torch.utils._pytree import tree_flatten
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
__all__ = ["ModTracker"]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ModTracker:
|
| 19 |
+
"""
|
| 20 |
+
``ModTracker`` is a context manager that tracks the nn.Module hierarchy during execution
|
| 21 |
+
so that other system can query which Module is currently being executed (or its backward is being
|
| 22 |
+
executed).
|
| 23 |
+
|
| 24 |
+
You can access the ``parents`` attribute on this context manager to get the set of all the
|
| 25 |
+
Modules currently being executed via their fqn (fully qualified name, also used as the key within
|
| 26 |
+
the state_dict).
|
| 27 |
+
You can access the ``is_bw`` attribute to know if you are currently running in backward or not.
|
| 28 |
+
|
| 29 |
+
Note that ``parents`` is never empty and always contains the "Global" key. The ``is_bw`` flag
|
| 30 |
+
will remain ``True`` after the forward until another Module is executed. If you need it to be
|
| 31 |
+
more accurate, please submit an issue requesting this. Adding a map from fqn to the module instance
|
| 32 |
+
is possible but not done yet, please submit an issue requesting this if you need it.
|
| 33 |
+
|
| 34 |
+
Example usage
|
| 35 |
+
|
| 36 |
+
.. code-block:: python
|
| 37 |
+
|
| 38 |
+
mod = torch.nn.Linear(2, 2)
|
| 39 |
+
|
| 40 |
+
with ModTracker() as tracker:
|
| 41 |
+
# Access anything during the forward pass
|
| 42 |
+
def my_linear(m1, m2, bias):
|
| 43 |
+
print(f"Current modules: {tracker.parents}")
|
| 44 |
+
return torch.mm(m1, m2.t()) + bias
|
| 45 |
+
torch.nn.functional.linear = my_linear
|
| 46 |
+
|
| 47 |
+
mod(torch.rand(2, 2))
|
| 48 |
+
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
parents: Set[str]
|
| 52 |
+
"""
|
| 53 |
+
A Set containing the fqn for each module currently running their forward
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
def __init__(self):
|
| 57 |
+
self.parents = {"Global"}
|
| 58 |
+
self._active_module_cnt = {}
|
| 59 |
+
self._known_modules: weakref.WeakKeyDictionary = weakref.WeakKeyDictionary()
|
| 60 |
+
self._seen_modules: weakref.WeakSet = weakref.WeakSet()
|
| 61 |
+
self._has_callback = False
|
| 62 |
+
self._user_pre_fw_hook = None
|
| 63 |
+
self._user_post_fw_hook = None
|
| 64 |
+
self._user_pre_bw_hook = None
|
| 65 |
+
self._user_post_bw_hook = None
|
| 66 |
+
|
| 67 |
+
def _maybe_set_engine_callback(self):
|
| 68 |
+
# This assumes no concurrent calls to backward
|
| 69 |
+
if self._has_callback:
|
| 70 |
+
return
|
| 71 |
+
|
| 72 |
+
def callback():
|
| 73 |
+
self.parents = {"Global"}
|
| 74 |
+
self._has_callback = False
|
| 75 |
+
|
| 76 |
+
torch.autograd.Variable._execution_engine.queue_callback(callback)
|
| 77 |
+
self._has_callback = True
|
| 78 |
+
|
| 79 |
+
@property
|
| 80 |
+
def is_bw(self):
|
| 81 |
+
"""
|
| 82 |
+
A boolean marking if this is currently running during the backward pass or not
|
| 83 |
+
"""
|
| 84 |
+
return torch._C._current_graph_task_id() != -1
|
| 85 |
+
|
| 86 |
+
def get_known_fqn(self, mod):
|
| 87 |
+
"""
|
| 88 |
+
Return the fqn for the given module if it is known to the ``ModTracker``, otherwise ``None``.
|
| 89 |
+
"""
|
| 90 |
+
return self._known_modules.get(mod, None)
|
| 91 |
+
|
| 92 |
+
def register_user_hooks(
|
| 93 |
+
self,
|
| 94 |
+
pre_fw_hook: Optional[Callable] = None,
|
| 95 |
+
post_fw_hook: Optional[Callable] = None,
|
| 96 |
+
pre_bw_hook: Optional[Callable] = None,
|
| 97 |
+
post_bw_hook: Optional[Callable] = None,
|
| 98 |
+
):
|
| 99 |
+
"""
|
| 100 |
+
Registers user-specified hooks to be called before/after the forward/backward pass for each
|
| 101 |
+
module tracked by the ``ModTracker``. One or more can be ``None``.
|
| 102 |
+
Args:
|
| 103 |
+
pre_fw_hook (Callable, optional): A hook to be called before the forward pass for the
|
| 104 |
+
module. It should have the following signature:
|
| 105 |
+
pre_fw_hook (module, input) -> None
|
| 106 |
+
post_fw_hook (Callable, optional): A hook to be called after the forward pass for the
|
| 107 |
+
module. It should have the following signature:
|
| 108 |
+
post_fw_hook (module, input, output) -> None
|
| 109 |
+
pre_bw_hook (Callable, optional): A multi-grad hook to be called on all the outputs of
|
| 110 |
+
the module that require gradients. It should have the following signature:
|
| 111 |
+
pre_bw_hook (module, grad_output) -> None
|
| 112 |
+
post_bw_hook (Callable, optional): A multi-grad hook to be called on all the inputs of
|
| 113 |
+
the module that require gradients. It should have the following signature:
|
| 114 |
+
post_bw_hook (module, grad_input) -> None
|
| 115 |
+
Raises:
|
| 116 |
+
AssertionError: If a new hook is provided when one is already registered.
|
| 117 |
+
Note:
|
| 118 |
+
If the module is not alive during the backward pass, the pre_bw_hook and post_bw_hook will
|
| 119 |
+
will receive None as the module argument.
|
| 120 |
+
The module fqn will be present in the ``parents`` attribute when each of the hooks is called.
|
| 121 |
+
Hooks are intended to be used as markers only not to modify the inputs/outputs.
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
def set_hook(hook, user_hook, hook_name):
|
| 125 |
+
if hook is not None and user_hook is not None:
|
| 126 |
+
raise AssertionError(
|
| 127 |
+
f"Only one {hook_name} can be registered at a time"
|
| 128 |
+
f" Clear the existing hook by calling ``clear_user_hooks`` before registering a new one"
|
| 129 |
+
)
|
| 130 |
+
return hook
|
| 131 |
+
|
| 132 |
+
self._user_pre_fw_hook = set_hook(
|
| 133 |
+
pre_fw_hook, self._user_pre_fw_hook, "pre_fw_hook"
|
| 134 |
+
)
|
| 135 |
+
self._user_post_fw_hook = set_hook(
|
| 136 |
+
post_fw_hook, self._user_post_fw_hook, "post_fw_hook"
|
| 137 |
+
)
|
| 138 |
+
self._user_pre_bw_hook = set_hook(
|
| 139 |
+
pre_bw_hook, self._user_pre_bw_hook, "pre_bw_hook"
|
| 140 |
+
)
|
| 141 |
+
self._user_post_bw_hook = set_hook(
|
| 142 |
+
post_bw_hook, self._user_post_bw_hook, "post_bw_hook"
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
def clear_user_hooks(self):
|
| 146 |
+
"""
|
| 147 |
+
Clears the user specified hooks registered with ``register_user_hooks``
|
| 148 |
+
"""
|
| 149 |
+
self._user_pre_fw_hook = None
|
| 150 |
+
self._user_post_fw_hook = None
|
| 151 |
+
self._user_pre_bw_hook = None
|
| 152 |
+
self._user_post_bw_hook = None
|
| 153 |
+
|
| 154 |
+
def _get_mod_name(self, mod):
|
| 155 |
+
if mod not in self._known_modules:
|
| 156 |
+
self._known_modules[mod] = type(mod).__name__
|
| 157 |
+
mod_name = self._known_modules[mod]
|
| 158 |
+
if mod not in self._seen_modules:
|
| 159 |
+
for name, submod in mod.named_children():
|
| 160 |
+
self._known_modules[submod] = f"{mod_name}.{name}"
|
| 161 |
+
self._get_mod_name(submod)
|
| 162 |
+
self._seen_modules.add(mod)
|
| 163 |
+
return mod_name
|
| 164 |
+
|
| 165 |
+
def _get_append_fn(self, w_mod, name, is_bw):
|
| 166 |
+
def fn(*args):
|
| 167 |
+
if is_bw:
|
| 168 |
+
self._maybe_set_engine_callback()
|
| 169 |
+
if name in self.parents and not self.is_bw:
|
| 170 |
+
|
| 171 |
+
def custom_formatwarning(msg, category, filename, lineno, line=None):
|
| 172 |
+
return f"{filename}:{lineno}: {category.__name__}: {msg} \n"
|
| 173 |
+
|
| 174 |
+
warnings.formatwarning = custom_formatwarning
|
| 175 |
+
warnings.warn(
|
| 176 |
+
"The module hierarchy tracking maybe be messed up."
|
| 177 |
+
" Please file a bug to PyTorch, if it is the case."
|
| 178 |
+
)
|
| 179 |
+
if name not in self.parents:
|
| 180 |
+
self._active_module_cnt[name] = 1
|
| 181 |
+
self.parents.add(name)
|
| 182 |
+
else:
|
| 183 |
+
self._active_module_cnt[name] += 1
|
| 184 |
+
|
| 185 |
+
if self._user_pre_bw_hook is not None and is_bw:
|
| 186 |
+
self._user_pre_bw_hook(w_mod(), args)
|
| 187 |
+
|
| 188 |
+
return fn
|
| 189 |
+
|
| 190 |
+
def _get_pop_fn(self, w_mod, name, is_bw):
|
| 191 |
+
def fn(*args):
|
| 192 |
+
if self._user_post_bw_hook is not None and is_bw:
|
| 193 |
+
self._user_post_bw_hook(w_mod(), args)
|
| 194 |
+
if name in self.parents:
|
| 195 |
+
self._active_module_cnt[name] -= 1
|
| 196 |
+
if self._active_module_cnt[name] == 0:
|
| 197 |
+
self.parents.remove(name)
|
| 198 |
+
elif not self.is_bw:
|
| 199 |
+
# Due to some input/output not requiring gradients, we cannot enforce
|
| 200 |
+
# proper nesting in backward
|
| 201 |
+
raise RuntimeError(
|
| 202 |
+
"The Module hierarchy tracking is wrong. Report a bug to PyTorch"
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
return fn
|
| 206 |
+
|
| 207 |
+
def _fw_pre_hook(self, mod, input):
|
| 208 |
+
name = self._get_mod_name(mod)
|
| 209 |
+
w_mod = weakref.ref(mod)
|
| 210 |
+
self._get_append_fn(w_mod, name, False)()
|
| 211 |
+
if self._user_pre_fw_hook is not None:
|
| 212 |
+
self._user_pre_fw_hook(mod, input)
|
| 213 |
+
args, _ = tree_flatten(input)
|
| 214 |
+
tensors = [a for a in args if isinstance(a, torch.Tensor) and a.requires_grad]
|
| 215 |
+
if not self.is_bw and tensors:
|
| 216 |
+
register_multi_grad_hook(tensors, self._get_pop_fn(w_mod, name, True))
|
| 217 |
+
|
| 218 |
+
def _fw_post_hook(self, mod, input, output):
|
| 219 |
+
name = self._get_mod_name(mod)
|
| 220 |
+
w_mod = weakref.ref(mod)
|
| 221 |
+
if self._user_post_fw_hook is not None:
|
| 222 |
+
self._user_post_fw_hook(mod, input, output)
|
| 223 |
+
self._get_pop_fn(w_mod, name, False)()
|
| 224 |
+
args, _ = tree_flatten(output)
|
| 225 |
+
tensors = [a for a in args if isinstance(a, torch.Tensor) and a.requires_grad]
|
| 226 |
+
if not self.is_bw and tensors:
|
| 227 |
+
register_multi_grad_hook(tensors, self._get_append_fn(w_mod, name, True))
|
| 228 |
+
|
| 229 |
+
def __enter__(self):
|
| 230 |
+
self._fw_pre_handle = register_module_forward_pre_hook(self._fw_pre_hook)
|
| 231 |
+
self._fw_post_handle = register_module_forward_hook(
|
| 232 |
+
self._fw_post_hook, always_call=True
|
| 233 |
+
)
|
| 234 |
+
return self
|
| 235 |
+
|
| 236 |
+
def __exit__(self, *args):
|
| 237 |
+
self._fw_pre_handle.remove()
|
| 238 |
+
self._fw_post_handle.remove()
|
lib/python3.12/site-packages/torch/distributed/_tools/runtime_estimator.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Owner(s): ["module: unknown"]
|
| 2 |
+
import math
|
| 3 |
+
import os
|
| 4 |
+
from collections import defaultdict
|
| 5 |
+
from typing import Any, Callable, Dict, List, Set, Tuple
|
| 6 |
+
from typing_extensions import Self
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.utils._pytree as pytree
|
| 10 |
+
from torch._guards import active_fake_mode
|
| 11 |
+
from torch._inductor.utils import get_device_tflops, get_gpu_dram_gbps
|
| 12 |
+
from torch._subclasses.fake_tensor import FakeTensorMode
|
| 13 |
+
from torch.distributed._tools.mod_tracker import ModTracker
|
| 14 |
+
from torch.utils._mode_utils import no_dispatch
|
| 15 |
+
from torch.utils._python_dispatch import TorchDispatchMode
|
| 16 |
+
from torch.utils.flop_counter import flop_registry
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
aten = torch.ops.aten
|
| 20 |
+
|
| 21 |
+
# This value is hard-coded here:
|
| 22 |
+
# https://github.com/pytorch/pytorch/blob/5fba5d83f0703ff8077ab65448a998e9ad6598fd/c10/cuda/CUDACachingAllocator.cpp#L117
|
| 23 |
+
_PYTORCH_MIN_ALLOCATE = (
|
| 24 |
+
2**9 if int(os.environ.get("PYTORCH_NO_CUDA_MEMORY_CACHING", 0)) == 0 else 1
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# No fall-back kernel needed/exists for view ops
|
| 28 |
+
_VIEW_OPS = {
|
| 29 |
+
aten.lift_fresh,
|
| 30 |
+
aten.t,
|
| 31 |
+
aten.transpose,
|
| 32 |
+
aten.view,
|
| 33 |
+
aten.detach,
|
| 34 |
+
aten._unsafe_view,
|
| 35 |
+
aten.split,
|
| 36 |
+
aten.adjoint,
|
| 37 |
+
aten.as_strided,
|
| 38 |
+
aten.diagonal,
|
| 39 |
+
aten.expand,
|
| 40 |
+
aten.expand_as,
|
| 41 |
+
aten.movedim,
|
| 42 |
+
aten.permute,
|
| 43 |
+
aten.select,
|
| 44 |
+
aten.squeeze,
|
| 45 |
+
aten.mT,
|
| 46 |
+
aten.mH,
|
| 47 |
+
aten.real,
|
| 48 |
+
aten.imag,
|
| 49 |
+
aten.view_as,
|
| 50 |
+
aten.unflatten,
|
| 51 |
+
aten.unfold,
|
| 52 |
+
aten.unbind,
|
| 53 |
+
aten.unsqueeze,
|
| 54 |
+
aten.vsplit,
|
| 55 |
+
aten.hsplit,
|
| 56 |
+
aten.split_with_sizes,
|
| 57 |
+
aten.swapaxes,
|
| 58 |
+
aten.swapdims,
|
| 59 |
+
aten.chunk,
|
| 60 |
+
}
|
| 61 |
+
# We can ignore benchmarking tensor create ops
|
| 62 |
+
_CREATE_OPS = {
|
| 63 |
+
aten.randint,
|
| 64 |
+
aten.randn,
|
| 65 |
+
aten.rand,
|
| 66 |
+
aten.randn_like,
|
| 67 |
+
aten.rand_like,
|
| 68 |
+
aten.randint_like,
|
| 69 |
+
aten.arange,
|
| 70 |
+
aten.ones_like,
|
| 71 |
+
aten.zeros_like,
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
_IGNORE_OPS = _VIEW_OPS | _CREATE_OPS
|
| 75 |
+
|
| 76 |
+
__all__ = ["RuntimeEstimator"]
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class RuntimeEstimator(TorchDispatchMode):
|
| 80 |
+
"""
|
| 81 |
+
Estimates the GPU runtime in milliseconds using various estimation methods under the ``FakeTensorMode``.
|
| 82 |
+
|
| 83 |
+
This class provides a ``TorchDispatchMode`` based context manager that can be used to estimate the eager
|
| 84 |
+
runtime of PyTorch functions. It supports two estimation modes, benchmarking (`operator-level-benchmark`) and
|
| 85 |
+
roofline cost modeling (`operator-level-cost-model`).
|
| 86 |
+
For modules executed under this context manager, it agggregates the forward and backward operation runtimes
|
| 87 |
+
and also records their execution orders.
|
| 88 |
+
|
| 89 |
+
Attributes:
|
| 90 |
+
mod_runtimes (Dict[str, Dict[str, float]]): A dictionary of module runtimes. The key to the outer dictionary
|
| 91 |
+
is the fully qualified name (FQN) of the module. For each module the forward and backward runtimes of the
|
| 92 |
+
operations are aggregated in the inner dictionary keyed by 'fw' and 'bw'.
|
| 93 |
+
mod_fw_pre_order (List[str]): List of module FQNs in pre-forward execution order.
|
| 94 |
+
mod_bw_pre_order (List[str]): List of module FQNs in pre-backward execution order.
|
| 95 |
+
mod_fw_post_order (List[str]): List of module FQNs in post-forward execution order.
|
| 96 |
+
mod_bw_post_order (List[str]): List of module FQNs in post-backward execution order.
|
| 97 |
+
total_runtime (float): The total estimated runtime in milliseconds.
|
| 98 |
+
|
| 99 |
+
Note:
|
| 100 |
+
1) The benchmarking estimate mode will execute kernels on GPU and assumes that every operation can run in
|
| 101 |
+
isolation without causing an OOM error. It is also designed to be used only under ``FakeTensorMode``.
|
| 102 |
+
2) Currently wrapper tensor sub-classes such as ``DTensor`` won't produce correct estimates. We plan to support
|
| 103 |
+
them in future PRs.
|
| 104 |
+
3) We only estimate the compute time, if your code has communication, it will not be considered. Again, we will
|
| 105 |
+
support this in future PRs.
|
| 106 |
+
|
| 107 |
+
Example usage:
|
| 108 |
+
|
| 109 |
+
.. code-block:: python
|
| 110 |
+
|
| 111 |
+
runtime_estimator = RuntimeEstimator()
|
| 112 |
+
with FakeTensorMode():
|
| 113 |
+
module = ...
|
| 114 |
+
optimizer = ...
|
| 115 |
+
inp = ...
|
| 116 |
+
with runtime_estimator(estimate_mode_type="operator-level-cost-model"):
|
| 117 |
+
loss = module(inp)
|
| 118 |
+
loss.backward()
|
| 119 |
+
optimizer.step()
|
| 120 |
+
optimizer.zero_grad()
|
| 121 |
+
runtime_estimator.display_modulewise_stats()
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
_float_types: Set[torch.dtype] = {
|
| 125 |
+
torch.float16,
|
| 126 |
+
torch.bfloat16,
|
| 127 |
+
torch.float32,
|
| 128 |
+
torch.float64,
|
| 129 |
+
}
|
| 130 |
+
_no_fallback_kernel: Set[torch._ops._OpNamespace] = set()
|
| 131 |
+
fake_mode: FakeTensorMode
|
| 132 |
+
|
| 133 |
+
def __init__(self) -> None:
|
| 134 |
+
super().__init__()
|
| 135 |
+
self._estimate: Callable
|
| 136 |
+
self._estimate_mode_type: str
|
| 137 |
+
self._mod_tracker = ModTracker()
|
| 138 |
+
self.mod_runtimes: Dict[str, Dict[str, float]] = defaultdict(
|
| 139 |
+
lambda: defaultdict(lambda: 0.0)
|
| 140 |
+
)
|
| 141 |
+
self.mod_fw_pre_order: List[str] = []
|
| 142 |
+
self.mod_bw_pre_order: List[str] = []
|
| 143 |
+
self.mod_fw_post_order: List[str] = []
|
| 144 |
+
self.mod_bw_post_order: List[str] = []
|
| 145 |
+
self.total_runtime: float = 0.0
|
| 146 |
+
|
| 147 |
+
# Adapted from: https://github.com/pytorch/pytorch/blob/9b902b3ee3bd608a19543362b66bf06c373dd374/torch/_subclasses/fake_tensor.py#L1969 # noqa: PGH004,B950
|
| 148 |
+
# NB: returns fake tensors
|
| 149 |
+
@classmethod
|
| 150 |
+
def _maybe_run_and_benchmark_fallback_kernel( # type: ignore[no-untyped-def]
|
| 151 |
+
cls,
|
| 152 |
+
func,
|
| 153 |
+
args,
|
| 154 |
+
kwargs,
|
| 155 |
+
orig_not_implemented_exception,
|
| 156 |
+
):
|
| 157 |
+
"""
|
| 158 |
+
Runs and benchmarks a fallback kernel for a given function.
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
func (Callable): The function to benchmark.
|
| 162 |
+
args (Tuple): The arguments to pass to the function.
|
| 163 |
+
kwargs (Dict[str, Any]): The keyword arguments to pass to the function.
|
| 164 |
+
orig_not_implemented_exception (Exception): The original exception to raise if the fallback kernel
|
| 165 |
+
is not implemented.
|
| 166 |
+
|
| 167 |
+
Returns:
|
| 168 |
+
Tuple[Any, float]: A tuple containing the result of the function and
|
| 169 |
+
the mean operation time in milliseconds.
|
| 170 |
+
"""
|
| 171 |
+
# these should all be supported, just to be safe
|
| 172 |
+
# avoid fallback for operators which inplace modify metadata
|
| 173 |
+
# because the input fake tensors would be umodified
|
| 174 |
+
if torch.Tag.inplace_view in func.tags: # type: ignore[attr-defined]
|
| 175 |
+
raise orig_not_implemented_exception
|
| 176 |
+
|
| 177 |
+
inp_impls = {}
|
| 178 |
+
flat_args, args_spec = pytree.tree_flatten((args, kwargs))
|
| 179 |
+
# Don't use in_kernel_invocation_manager(fake_mode) as we want to do
|
| 180 |
+
# REAL compute (not with meta device)
|
| 181 |
+
with no_dispatch():
|
| 182 |
+
|
| 183 |
+
def to_real_tensor(e): # type: ignore[no-untyped-def]
|
| 184 |
+
if cls.fake_mode.is_our_fake(e):
|
| 185 |
+
if e.dtype in cls._float_types:
|
| 186 |
+
out = torch.rand_like(e, device=e.fake_device)
|
| 187 |
+
else:
|
| 188 |
+
out = torch.ones_like(e, device=e.fake_device)
|
| 189 |
+
if e.is_sparse:
|
| 190 |
+
out._coalesced_(e.is_coalesced())
|
| 191 |
+
inp_impls[id(out)] = e
|
| 192 |
+
return out
|
| 193 |
+
return e
|
| 194 |
+
|
| 195 |
+
flat_args = [to_real_tensor(a) for a in flat_args]
|
| 196 |
+
args, kwargs = pytree.tree_unflatten(flat_args, args_spec)
|
| 197 |
+
r = func(*args, **kwargs)
|
| 198 |
+
warmup_iters, actual_iters = 2, 3
|
| 199 |
+
for _ in range(warmup_iters):
|
| 200 |
+
func(*args, **kwargs)
|
| 201 |
+
start_event = torch.cuda.Event(enable_timing=True)
|
| 202 |
+
end_event = torch.cuda.Event(enable_timing=True)
|
| 203 |
+
start_event.record(torch.cuda.current_stream())
|
| 204 |
+
for _ in range(actual_iters):
|
| 205 |
+
func(*args, **kwargs)
|
| 206 |
+
end_event.record(torch.cuda.current_stream())
|
| 207 |
+
torch.cuda.synchronize()
|
| 208 |
+
cuda_time = start_event.elapsed_time(end_event)
|
| 209 |
+
mean_op_time = cuda_time / actual_iters
|
| 210 |
+
|
| 211 |
+
storages = set()
|
| 212 |
+
|
| 213 |
+
for e in flat_args:
|
| 214 |
+
if isinstance(e, torch.Tensor):
|
| 215 |
+
if not e.is_sparse:
|
| 216 |
+
storages.add(e._typed_storage()._cdata)
|
| 217 |
+
|
| 218 |
+
# TODO: also check metadata change on inputs
|
| 219 |
+
# proper aliasing/metadata relationship between outputs and inputs will
|
| 220 |
+
# not be set up, bc of conversion to device, unless we can reuse an
|
| 221 |
+
# input impl
|
| 222 |
+
|
| 223 |
+
def map_out(e): # type: ignore[no-untyped-def]
|
| 224 |
+
if id(e) not in inp_impls and (
|
| 225 |
+
isinstance(e, torch.Tensor)
|
| 226 |
+
and not e.is_sparse
|
| 227 |
+
and e._typed_storage()._cdata in storages
|
| 228 |
+
):
|
| 229 |
+
raise orig_not_implemented_exception
|
| 230 |
+
|
| 231 |
+
if isinstance(e, torch.Tensor):
|
| 232 |
+
if id(e) in inp_impls:
|
| 233 |
+
return inp_impls[id(e)]
|
| 234 |
+
else:
|
| 235 |
+
return cls.fake_mode.fake_tensor_converter.from_real_tensor(
|
| 236 |
+
cls.fake_mode, e
|
| 237 |
+
)
|
| 238 |
+
else:
|
| 239 |
+
return e
|
| 240 |
+
|
| 241 |
+
return (pytree.tree_map(map_out, r), mean_op_time)
|
| 242 |
+
|
| 243 |
+
@classmethod
|
| 244 |
+
def _benchmark_estimate(cls, func, args, kwargs) -> Tuple[Any, float]: # type: ignore[no-untyped-def]
|
| 245 |
+
"""
|
| 246 |
+
Estimates the runtime of a function using benchmarking.
|
| 247 |
+
|
| 248 |
+
Args:
|
| 249 |
+
func: The function to estimate.
|
| 250 |
+
args: The arguments to pass to the function.
|
| 251 |
+
kwargs: The keyword arguments to pass to the function.
|
| 252 |
+
res: The result of the function.
|
| 253 |
+
|
| 254 |
+
Returns:
|
| 255 |
+
Tuple[Any, float]: A tuple containing the result of the function and
|
| 256 |
+
the mean operation time in milliseconds.
|
| 257 |
+
"""
|
| 258 |
+
assert isinstance(
|
| 259 |
+
cls.fake_mode, FakeTensorMode
|
| 260 |
+
), "Initialize/Assign FakeTensorMode before using this function"
|
| 261 |
+
mean_op_time = 0.0
|
| 262 |
+
if func._overloadpacket not in _VIEW_OPS:
|
| 263 |
+
try:
|
| 264 |
+
res, mean_op_time = cls._maybe_run_and_benchmark_fallback_kernel(
|
| 265 |
+
func,
|
| 266 |
+
args,
|
| 267 |
+
kwargs,
|
| 268 |
+
NotImplementedError,
|
| 269 |
+
)
|
| 270 |
+
return (res, mean_op_time)
|
| 271 |
+
except NotImplementedError:
|
| 272 |
+
cls._no_fallback_kernel.add(func._overloadpacket)
|
| 273 |
+
res = func(*args, **kwargs or {})
|
| 274 |
+
return (res, mean_op_time)
|
| 275 |
+
|
| 276 |
+
# Adapted from: https://github.com/pytorch/pytorch/blob/9b902b3ee3bd608a19543362b66bf06c373dd374/torch/_inductor/scheduler.py#L589 # noqa: PGH004,B950
|
| 277 |
+
@classmethod
|
| 278 |
+
def _roofline_estimate(cls, func, args, kwargs) -> Tuple[Any, float]: # type: ignore[no-untyped-def]
|
| 279 |
+
"""
|
| 280 |
+
Estimates the runtime of a function using a roofline cost model.
|
| 281 |
+
|
| 282 |
+
Args:
|
| 283 |
+
func: The function to estimate.
|
| 284 |
+
args: The arguments to pass to the function.
|
| 285 |
+
kwargs: The keyword arguments to pass to the function.
|
| 286 |
+
out: The output of the function.
|
| 287 |
+
|
| 288 |
+
Returns:
|
| 289 |
+
Tuple[Any, float]: A tuple containing the result of the function and
|
| 290 |
+
the mean operation time in milliseconds.
|
| 291 |
+
"""
|
| 292 |
+
assert (
|
| 293 |
+
torch.cuda.is_available()
|
| 294 |
+
), "Roofline estimation needs to access CUDA capabilities to make estimations"
|
| 295 |
+
|
| 296 |
+
def get_num_bytes(t: torch.Tensor) -> int:
|
| 297 |
+
"""
|
| 298 |
+
Calculates the memory consumption of a tensor.
|
| 299 |
+
|
| 300 |
+
Args:
|
| 301 |
+
t (torch.Tensor): The input tensor.
|
| 302 |
+
|
| 303 |
+
Returns:
|
| 304 |
+
int: The memory consumption of the tensor in bytes.
|
| 305 |
+
"""
|
| 306 |
+
num_bytes = t.untyped_storage().nbytes()
|
| 307 |
+
mem_consumed = (
|
| 308 |
+
math.ceil(num_bytes / _PYTORCH_MIN_ALLOCATE) * _PYTORCH_MIN_ALLOCATE
|
| 309 |
+
)
|
| 310 |
+
return mem_consumed
|
| 311 |
+
|
| 312 |
+
def get_compute_time(func_packet, args, kwargs, out, out_dtypes) -> float: # type: ignore[no-untyped-def]
|
| 313 |
+
"""
|
| 314 |
+
Estimates the compute time of an aten operator.
|
| 315 |
+
|
| 316 |
+
Args:
|
| 317 |
+
func_packet: The operator overload packet.
|
| 318 |
+
args: The arguments to the operator.
|
| 319 |
+
kwargs: The keyword arguments to the operator.
|
| 320 |
+
out: The output of the operator.
|
| 321 |
+
out_dtypes: The output data types.
|
| 322 |
+
|
| 323 |
+
Returns:
|
| 324 |
+
float: The estimated compute time in nanoseconds.
|
| 325 |
+
"""
|
| 326 |
+
if func_packet in flop_registry:
|
| 327 |
+
assert (
|
| 328 |
+
len(out_dtypes) == 1
|
| 329 |
+
), f"Only support single out dtype got {out_dtypes} for {func_packet}"
|
| 330 |
+
dtype = out_dtypes.pop()
|
| 331 |
+
# This actually gives peta-FLOPs/s hence multiply by 1e15 to get the FLOPs/s
|
| 332 |
+
peak_gpu_flops = get_device_tflops(dtype) * 1e15
|
| 333 |
+
# We can expect to achieve 75% of theoretical peak flops
|
| 334 |
+
factor = 0.75
|
| 335 |
+
peak_empirical_flops = factor * peak_gpu_flops
|
| 336 |
+
flop_count_func = flop_registry[func_packet]
|
| 337 |
+
# We divide by a factor of 2 to get the MACs (multiply and accumulate)
|
| 338 |
+
flop_count = flop_count_func(*args, **kwargs, out_val=out) / 2
|
| 339 |
+
# We multiply by 1e9 to get the time in nano seconds
|
| 340 |
+
compute_time = (flop_count / peak_empirical_flops) * 1e9
|
| 341 |
+
return compute_time
|
| 342 |
+
return 0.0
|
| 343 |
+
|
| 344 |
+
def get_transfer_time(flat_args_kwargs, flat_outs) -> float: # type: ignore[no-untyped-def]
|
| 345 |
+
"""
|
| 346 |
+
Estimates the memory transfer time of input and output tensors.
|
| 347 |
+
|
| 348 |
+
Args:
|
| 349 |
+
flat_args_kwargs (List[torch.Tensor]): The flat list of arguments and keyword arguments.
|
| 350 |
+
flat_outs (List[torch.Tensor]): The flat list of outputs.
|
| 351 |
+
|
| 352 |
+
Returns:
|
| 353 |
+
float: The estimated memory transfer time in nanoseconds.
|
| 354 |
+
"""
|
| 355 |
+
gpu_memory_bandwidth = get_gpu_dram_gbps()
|
| 356 |
+
read_bytes = sum(
|
| 357 |
+
get_num_bytes(t)
|
| 358 |
+
for t in flat_args_kwargs
|
| 359 |
+
if isinstance(t, torch.Tensor)
|
| 360 |
+
)
|
| 361 |
+
write_bytes = sum(
|
| 362 |
+
get_num_bytes(t) for t in flat_outs if isinstance(t, torch.Tensor)
|
| 363 |
+
)
|
| 364 |
+
counted_bytes = read_bytes + write_bytes
|
| 365 |
+
# The GPU memory bandwidth is in GB/s so the transfer time is in nanoseconds
|
| 366 |
+
transfer_time = counted_bytes / gpu_memory_bandwidth
|
| 367 |
+
return transfer_time
|
| 368 |
+
|
| 369 |
+
# Roofline Cost Model Explanation
|
| 370 |
+
|
| 371 |
+
# The roofline cost model estimates the execution time of an operator based on
|
| 372 |
+
# the device's empirical maximum FLOPs/sec (pi) and device DRAM bandwidth (beta).
|
| 373 |
+
|
| 374 |
+
# Variables:
|
| 375 |
+
# - pi: Maximum empirical FLOPs/sec of the device
|
| 376 |
+
# - beta: Maximum empirical device DRAM bandwidth (bytes/sec) of the device
|
| 377 |
+
# - I: Arithmetic intensity of the operator (FLOPs/bytes)
|
| 378 |
+
# - op_flops: FLOPs required by the operator
|
| 379 |
+
# - op_bytes: Bytes transferred to and from DRAM for the operator
|
| 380 |
+
|
| 381 |
+
# Calculation Steps:
|
| 382 |
+
# 1. Calculate arithmetic intensity: I = op_flops / op_bytes
|
| 383 |
+
# 2. Calculate estimated FLOPs/sec: est_flops_sec = min(pi, beta * I)
|
| 384 |
+
# 3. Calculate estimated operator time: estimated_op_time = op_flops / est_flops_sec
|
| 385 |
+
# This simplifies to: estimated_op_time = max(op_flops / pi, op_flops / (beta * I))
|
| 386 |
+
# Further simplifying: estimated_op_time = max(op_flops / pi, op_bytes / beta)
|
| 387 |
+
|
| 388 |
+
# Simplified Formulas:
|
| 389 |
+
# - compute_time = op_flops / pi
|
| 390 |
+
# - transfer_time = op_bytes / beta
|
| 391 |
+
# - estimated_op_time = max(compute_time, transfer_time)
|
| 392 |
+
|
| 393 |
+
kwargs = kwargs if kwargs else {}
|
| 394 |
+
out = func(*args, **kwargs)
|
| 395 |
+
op_time = 0.0
|
| 396 |
+
func_packet = func._overloadpacket
|
| 397 |
+
if func_packet not in _IGNORE_OPS:
|
| 398 |
+
flat_args_kwargs, args_spec = pytree.tree_flatten((args, kwargs))
|
| 399 |
+
flat_outs, out_spec = pytree.tree_flatten(out)
|
| 400 |
+
transfer_time = get_transfer_time(flat_args_kwargs, flat_outs)
|
| 401 |
+
|
| 402 |
+
out_dtypes = {
|
| 403 |
+
t.dtype
|
| 404 |
+
for t in flat_outs
|
| 405 |
+
if isinstance(t, torch.Tensor) and t.dtype in cls._float_types
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
args, kwargs = pytree.tree_unflatten(flat_args_kwargs, args_spec)
|
| 409 |
+
out = pytree.tree_unflatten(flat_outs, out_spec)
|
| 410 |
+
|
| 411 |
+
compute_time = get_compute_time(func_packet, args, kwargs, out, out_dtypes)
|
| 412 |
+
# We get the estimated time as the max of the transfer time and
|
| 413 |
+
# compute time. We divide by 1e6 to get the time in ms
|
| 414 |
+
op_time = max(transfer_time, compute_time) / 1e6
|
| 415 |
+
|
| 416 |
+
return (out, op_time)
|
| 417 |
+
|
| 418 |
+
def display_modulewise_stats(self, depth: int = 2) -> None:
|
| 419 |
+
"""
|
| 420 |
+
Displays module-wise statistics collected by ``RuntimeEstimator``.
|
| 421 |
+
|
| 422 |
+
Prints the pre-forward and pre-backward execution orders.
|
| 423 |
+
Displays the module-wise forward and backward runtimes in milliseconds.
|
| 424 |
+
|
| 425 |
+
Args:
|
| 426 |
+
depth (int): The maximum depth of module hierarchy to display (default to 2).
|
| 427 |
+
"""
|
| 428 |
+
print("Pre-Forward Execution Order: ")
|
| 429 |
+
for mod_fqn in self.mod_fw_pre_order:
|
| 430 |
+
mod_depth = mod_fqn.count(".") + 1
|
| 431 |
+
if mod_depth > depth:
|
| 432 |
+
continue
|
| 433 |
+
print(mod_fqn)
|
| 434 |
+
print("Pre-Backward Execution Order: ")
|
| 435 |
+
for mod_fqn in self.mod_bw_pre_order:
|
| 436 |
+
mod_depth = mod_fqn.count(".") + 1
|
| 437 |
+
if mod_depth > depth:
|
| 438 |
+
continue
|
| 439 |
+
print(mod_fqn)
|
| 440 |
+
for mod_fqn, runtimes in self.mod_runtimes.items():
|
| 441 |
+
mod_depth = mod_fqn.count(".") + 1
|
| 442 |
+
if mod_depth > depth:
|
| 443 |
+
continue
|
| 444 |
+
print(
|
| 445 |
+
f"{mod_fqn} fw: {runtimes.get('fw', 0.0):.3f}ms bw: {runtimes.get('bw', 0.0):.3f}ms"
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
def __torch_dispatch__(self, func, types, args=..., kwargs=None): # type: ignore[no-untyped-def]
|
| 449 |
+
# TODO: @sanketpurandare: Flatten tensors by desugaring the tensor subclasses
|
| 450 |
+
# TODO: @sanketpurandare: Add logic for incorporating communication time
|
| 451 |
+
res, op_time = self._estimate(func, args, kwargs)
|
| 452 |
+
for par in self._mod_tracker.parents:
|
| 453 |
+
if self._mod_tracker.is_bw:
|
| 454 |
+
self.mod_runtimes[par]["bw"] += op_time
|
| 455 |
+
else:
|
| 456 |
+
self.mod_runtimes[par]["fw"] += op_time
|
| 457 |
+
self.total_runtime += op_time
|
| 458 |
+
return res
|
| 459 |
+
|
| 460 |
+
def __call__(self, estimate_mode_type: str) -> Self:
|
| 461 |
+
"""
|
| 462 |
+
Sets the estimate mode type.
|
| 463 |
+
|
| 464 |
+
Currently supported modes:
|
| 465 |
+
- "operator-level-benchmark": Estimates runtime using operator benchmarking.
|
| 466 |
+
- "operator-level-cost-model": Estimates runtime using roofline cost model.
|
| 467 |
+
|
| 468 |
+
Args:
|
| 469 |
+
estimate_mode_type (str): The type of estimate mode to use.
|
| 470 |
+
|
| 471 |
+
Returns:
|
| 472 |
+
RuntimeEstimator: The runtime estimator instance.
|
| 473 |
+
|
| 474 |
+
Raises:
|
| 475 |
+
NotImplementedError: If the estimate mode type is not supported.
|
| 476 |
+
"""
|
| 477 |
+
if estimate_mode_type == "operator-level-benchmark":
|
| 478 |
+
self._estimate = RuntimeEstimator._benchmark_estimate
|
| 479 |
+
elif estimate_mode_type == "operator-level-cost-model":
|
| 480 |
+
self._estimate = RuntimeEstimator._roofline_estimate
|
| 481 |
+
else:
|
| 482 |
+
raise NotImplementedError(
|
| 483 |
+
f"estimate_mode_type {estimate_mode_type} not supported"
|
| 484 |
+
)
|
| 485 |
+
self._estimate_mode_type = estimate_mode_type
|
| 486 |
+
return self
|
| 487 |
+
|
| 488 |
+
def __enter__(self) -> Self:
|
| 489 |
+
fake_mode = active_fake_mode()
|
| 490 |
+
assert isinstance(
|
| 491 |
+
fake_mode, FakeTensorMode
|
| 492 |
+
), "No FakeTensorMode found, designed to used under FakeTensorMode"
|
| 493 |
+
RuntimeEstimator.fake_mode = fake_mode
|
| 494 |
+
self.total_runtime = 0.0
|
| 495 |
+
self.mod_runtimes = defaultdict(lambda: defaultdict(lambda: 0.0))
|
| 496 |
+
self.mod_fw_pre_order.clear()
|
| 497 |
+
self.mod_bw_pre_order.clear()
|
| 498 |
+
self.mod_fw_post_order.clear()
|
| 499 |
+
self.mod_bw_post_order.clear()
|
| 500 |
+
self._mod_tracker.register_user_hooks(
|
| 501 |
+
pre_fw_hook=lambda mod, inp: self.mod_fw_pre_order.append(
|
| 502 |
+
self._mod_tracker.get_known_fqn(mod)
|
| 503 |
+
),
|
| 504 |
+
pre_bw_hook=lambda mod, g_out: self.mod_bw_pre_order.append(
|
| 505 |
+
self._mod_tracker.get_known_fqn(mod)
|
| 506 |
+
),
|
| 507 |
+
post_fw_hook=lambda mod, inp, out: self.mod_fw_post_order.append(
|
| 508 |
+
self._mod_tracker.get_known_fqn(mod)
|
| 509 |
+
),
|
| 510 |
+
post_bw_hook=lambda mod, g_inp: self.mod_bw_post_order.append(
|
| 511 |
+
self._mod_tracker.get_known_fqn(mod)
|
| 512 |
+
),
|
| 513 |
+
)
|
| 514 |
+
self._mod_tracker.__enter__()
|
| 515 |
+
super().__enter__()
|
| 516 |
+
return self
|
| 517 |
+
|
| 518 |
+
def __exit__(self, *args: Any) -> None:
|
| 519 |
+
print(
|
| 520 |
+
f"Estimated ({self._estimate_mode_type})"
|
| 521 |
+
f"total_time: {self.total_runtime:.3f} ms"
|
| 522 |
+
)
|
| 523 |
+
if len(self._no_fallback_kernel) > 0:
|
| 524 |
+
print("no_fallback_kernel: ", list(self._no_fallback_kernel))
|
| 525 |
+
super().__exit__(*args)
|
| 526 |
+
self._mod_tracker.clear_user_hooks()
|
| 527 |
+
self._mod_tracker.__exit__()
|
lib/python3.12/site-packages/torch/distributed/_tools/sac_estimator.py
ADDED
|
@@ -0,0 +1,997 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import warnings
|
| 5 |
+
from collections import OrderedDict
|
| 6 |
+
from dataclasses import astuple, dataclass
|
| 7 |
+
from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple
|
| 8 |
+
from typing_extensions import Self
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch import nan, nn, UntypedStorage
|
| 12 |
+
from torch._guards import active_fake_mode
|
| 13 |
+
from torch._subclasses.fake_tensor import FakeTensorMode
|
| 14 |
+
from torch.distributed._tools.mod_tracker import ModTracker
|
| 15 |
+
from torch.distributed._tools.runtime_estimator import RuntimeEstimator
|
| 16 |
+
from torch.testing._internal.composite_compliance import (
|
| 17 |
+
is_inplace,
|
| 18 |
+
is_inplace_view_fn,
|
| 19 |
+
is_view_fn,
|
| 20 |
+
)
|
| 21 |
+
from torch.utils._python_dispatch import (
|
| 22 |
+
is_traceable_wrapper_subclass,
|
| 23 |
+
TorchDispatchMode,
|
| 24 |
+
)
|
| 25 |
+
from torch.utils._pytree import tree_flatten
|
| 26 |
+
from torch.utils.checkpoint import SAC_IGNORED_OPS
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
__all__ = ["SACEstimator", "SACStats", "MSPS", "SACTradeOffStats", "SACGreedyOrderMeta"]
|
| 30 |
+
aten = torch.ops.aten
|
| 31 |
+
|
| 32 |
+
_ADDITIONAL_IGNORED_OPS = {
|
| 33 |
+
aten.lift_fresh.default, # type: ignore[attr-defined]
|
| 34 |
+
torch.ops.profiler._record_function_exit._RecordFunction, # type: ignore[attr-defined]
|
| 35 |
+
aten.clone.default, # type: ignore[attr-defined] # seems needed for torch.compile
|
| 36 |
+
}
|
| 37 |
+
OPS_TO_ALWAYS_SKIP = SAC_IGNORED_OPS | _ADDITIONAL_IGNORED_OPS
|
| 38 |
+
# This value is hard-coded here:
|
| 39 |
+
# https://github.com/pytorch/pytorch/blob/5fba5d83f0703ff8077ab65448a998e9ad6598fd/c10/cuda/CUDACachingAllocator.cpp#L117
|
| 40 |
+
_PYTORCH_MIN_ALLOCATE = (
|
| 41 |
+
2**9 if int(os.environ.get("PYTORCH_NO_CUDA_MEMORY_CACHING", 0)) == 0 else 1
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _get_untyped_storages(t: torch.Tensor) -> Set[torch.UntypedStorage]:
|
| 46 |
+
"""
|
| 47 |
+
Retrieves untyped storages from a `torch.Tensor` or one of its traceable wrapper-subclass.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
t (torch.Tensor): Input `torch.Tensor` or traceable wrapper-subclass of `torch.Tensor`.
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
Set[torch.UntypedStorage]: Set of untyped storages.
|
| 54 |
+
|
| 55 |
+
Warns:
|
| 56 |
+
UserWarning: If the flattened input is not a tensor or traceable wrapper-subclass.
|
| 57 |
+
"""
|
| 58 |
+
unflattened_tensors = [t]
|
| 59 |
+
flattened_tensor_storages = set()
|
| 60 |
+
while len(unflattened_tensors) > 0:
|
| 61 |
+
obj = unflattened_tensors.pop()
|
| 62 |
+
if is_traceable_wrapper_subclass(obj):
|
| 63 |
+
attrs, _ = obj.__tensor_flatten__() # type: ignore[attr-defined]
|
| 64 |
+
unflattened_tensors.extend([getattr(obj, attr) for attr in attrs])
|
| 65 |
+
else:
|
| 66 |
+
if not hasattr(obj, "untyped_storage"):
|
| 67 |
+
warnings.warn(
|
| 68 |
+
f"Expected a tensor or a traceable wrapper-subclass of tensor, but got {type(obj)}",
|
| 69 |
+
category=UserWarning,
|
| 70 |
+
stacklevel=2,
|
| 71 |
+
)
|
| 72 |
+
else:
|
| 73 |
+
flattened_tensor_storages.add(obj.untyped_storage())
|
| 74 |
+
return flattened_tensor_storages
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _display_stats_tabular(headers: List[str], table_data: List[List[Any]]) -> None:
|
| 78 |
+
try:
|
| 79 |
+
from tabulate import tabulate
|
| 80 |
+
except ImportError as err:
|
| 81 |
+
raise ImportError("Please install tabulate.") from err
|
| 82 |
+
|
| 83 |
+
# Use tabulate to print the table
|
| 84 |
+
print(tabulate(table_data, headers=headers, tablefmt="rst"))
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# Based on:
|
| 88 |
+
# https://github.com/fairinternal/xformers/blob/0ded5697a2ea15711ce45131002d04e72053cc6d/xformers/checkpoint.py#L62
|
| 89 |
+
@dataclass
|
| 90 |
+
class _SACMetadata:
|
| 91 |
+
"""
|
| 92 |
+
Stores metadata for a single operator for SAC.
|
| 93 |
+
|
| 94 |
+
Attributes:
|
| 95 |
+
func (Any): The operator function.
|
| 96 |
+
time_taken (float): The time taken by the operator.
|
| 97 |
+
memory_used (float): The memory used by the operator.
|
| 98 |
+
curr_idx (int): The current operator index.
|
| 99 |
+
output_ids (Tuple[int, ...]): The storage IDs of the operator's outputs.
|
| 100 |
+
inplace_info (Tuple[int, ...]): Tuple of self and parent operator for in-place operator.
|
| 101 |
+
is_view_like (bool): Whether the operator is view-like.
|
| 102 |
+
is_rand_op (bool): Whether the operator is a random operator.
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
func: Any
|
| 106 |
+
time_taken: float
|
| 107 |
+
memory_used: float
|
| 108 |
+
curr_idx: int
|
| 109 |
+
output_ids: Tuple[int, ...]
|
| 110 |
+
inplace_info: Tuple[int, ...]
|
| 111 |
+
is_view_like: bool
|
| 112 |
+
is_rand_op: bool
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@dataclass
|
| 116 |
+
class _SACModMetadata:
|
| 117 |
+
"""
|
| 118 |
+
Stores metadata for a module for SAC.
|
| 119 |
+
|
| 120 |
+
Attributes:
|
| 121 |
+
start_idx (int): The starting index of the module's operators.
|
| 122 |
+
force_store_random (bool): Whether to force store random operators in the module.
|
| 123 |
+
sac_metadata (List[_SACMetadata]): List of metadata for each operator in the module.
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
start_idx: int
|
| 127 |
+
force_store_random: bool
|
| 128 |
+
sac_metadata: List[_SACMetadata]
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
@dataclass
|
| 132 |
+
class SACStats:
|
| 133 |
+
"""
|
| 134 |
+
A class for storing Activation Checkpointing statistics corresponding to a module.
|
| 135 |
+
|
| 136 |
+
Attributes:
|
| 137 |
+
func_names (List[str]): List of operator names.
|
| 138 |
+
runtimes (List[float]): List of operator runtimes in millliseconds.
|
| 139 |
+
memory (List[int]): List of operator memory usage in bytes.
|
| 140 |
+
view_like_ops (List[int]): Indices of view-like operators.
|
| 141 |
+
rand_ops (List[int]): Indices of random operators.
|
| 142 |
+
saved_autograd_ops (List[int]): Indices of operator results saved by autograd engine.
|
| 143 |
+
inplace_ops (List[Tuple[int, int]]): Tuple of indices of op and its first parent for Inplace operators.
|
| 144 |
+
force_store_random (bool): Whether to force store random operator results.
|
| 145 |
+
"""
|
| 146 |
+
|
| 147 |
+
func_names: List[str]
|
| 148 |
+
runtimes: List[float]
|
| 149 |
+
memory: List[int]
|
| 150 |
+
view_like_ops: List[int]
|
| 151 |
+
rand_ops: List[int]
|
| 152 |
+
saved_autograd_ops: List[int]
|
| 153 |
+
inplace_ops: List[Tuple[int, int]]
|
| 154 |
+
force_store_random: bool
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
class MSPS(NamedTuple):
|
| 158 |
+
"""
|
| 159 |
+
Represents Memory and Runtime Statistics for an operator/operator group.
|
| 160 |
+
|
| 161 |
+
Attributes:
|
| 162 |
+
func_names (Set[str]): Set of operator/operator group names.
|
| 163 |
+
op_idx (int): Operator index (group head index incase of operator groups).
|
| 164 |
+
memory (int): Memory usage in bytes.
|
| 165 |
+
runtime (float): Runtime in milliseconds.
|
| 166 |
+
msps (float): Memory per second calculated as memory/runtime.
|
| 167 |
+
"""
|
| 168 |
+
|
| 169 |
+
func_names: Set[str]
|
| 170 |
+
op_idx: int
|
| 171 |
+
memory: int
|
| 172 |
+
runtime: float
|
| 173 |
+
msps: float
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
@dataclass
|
| 177 |
+
class SACTradeOffStats:
|
| 178 |
+
"""
|
| 179 |
+
Stores statistics for activation-checkpointing trade-off.
|
| 180 |
+
|
| 181 |
+
Attributes:
|
| 182 |
+
n_segments (int): Number of piecewise linear segments fitted to the trade-off curve.
|
| 183 |
+
slopes (List[float]): Slopes of the pieces of linear segments fitted to the trade-off curve.
|
| 184 |
+
intercepts (List[float]): Intercepts of the of the pieces of linear segments fitted to the trade-off curve.
|
| 185 |
+
fit_breaks (List[float]): Breakpoints of the of the pieces of linear segments fitted to the trade-off curve.
|
| 186 |
+
tradeoff_curve (OrderedDict[float, float]): Trade-off curve data of memory discarded vs recomputation time.
|
| 187 |
+
sac_memory (int): Total memory of operations available for activation checkpointing in bytes.
|
| 188 |
+
sac_runtime (float): Total runtime of operations available for activation checkpointing in milliseconds.
|
| 189 |
+
"""
|
| 190 |
+
|
| 191 |
+
n_segments: int
|
| 192 |
+
slopes: List[float]
|
| 193 |
+
intercepts: List[float]
|
| 194 |
+
fit_breaks: List[float]
|
| 195 |
+
tradeoff_curve: OrderedDict[float, float]
|
| 196 |
+
sac_memory: int
|
| 197 |
+
sac_runtime: float
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
@dataclass
|
| 201 |
+
class SACGreedyOrderMeta:
|
| 202 |
+
"""
|
| 203 |
+
Stores metadata for Greedy-order SAC.
|
| 204 |
+
|
| 205 |
+
Attributes:
|
| 206 |
+
recomputed_ops (Set[int]): Set of operator indices to be recomputed.
|
| 207 |
+
stored_ops (Set[int]): Set of operator indices to be stored.
|
| 208 |
+
inplace_op_groups (Dict[int, Set[int]]): Dictionary of inplace operator groups from group-head to operators.
|
| 209 |
+
random_ops_group (Dict[int, Set[int]]): Dictionary of random op group head to random ops.
|
| 210 |
+
msps_meta (List[MSPS]): List of Memory and Runtime Statistics for operators.
|
| 211 |
+
"""
|
| 212 |
+
|
| 213 |
+
recomputed_ops: Set[int]
|
| 214 |
+
stored_ops: Set[int]
|
| 215 |
+
inplace_op_groups: Dict[int, Set[int]]
|
| 216 |
+
random_ops_group: Dict[int, Set[int]]
|
| 217 |
+
msps_meta: List[MSPS]
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
class SACEstimator(TorchDispatchMode):
|
| 221 |
+
"""
|
| 222 |
+
Estimates the memory and recomputation time trade-offs for applying Selective Activation Checkpointing (SAC).
|
| 223 |
+
|
| 224 |
+
This class provides a ``TorchDispatchMode`` based context manager that can be used to estimate the memory and
|
| 225 |
+
runtime trade-offs of functions or ``torch.nn.Module``s for Selective Activation Checkpointing (SAC). It provides
|
| 226 |
+
detailed statistics and metadata information for operators of each module and provides a greedy order for selecting
|
| 227 |
+
the operators to be recomputed/checkpointed. It also constructs the per-module trade-off graph of discarded memory
|
| 228 |
+
vs recomputation time for the obtained greedy order. Using ``RuntimeEstimator`` under the hood, it supports two
|
| 229 |
+
estimation modes, `operator-level-benchmark` and (`operator-level-cost-model` (roofline model).
|
| 230 |
+
|
| 231 |
+
Attributes:
|
| 232 |
+
sac_mod_stats (Dict[str, SACStats]): Dictionary from module FQN (fuly qualified name) to ``SACStats``.
|
| 233 |
+
sac_mod_tradeoff_stats (Dict[str, SACTradeOffStats]): Dictionary from module FQN to ``SACTradeOffStats``.
|
| 234 |
+
sac_mod_greedy_order_meta (Dict[str, SACGreedyOrderMeta]): Dictionary from module FQN to ``SACGreedyOrderMeta``.
|
| 235 |
+
|
| 236 |
+
Note:
|
| 237 |
+
1) This class is designed to be used under ``FakeTensorMode``.
|
| 238 |
+
2) Currently, it only supports estimation of compute time and memory usage, and does not consider communication.
|
| 239 |
+
|
| 240 |
+
Example usage:
|
| 241 |
+
|
| 242 |
+
.. code-block:: python
|
| 243 |
+
|
| 244 |
+
sac_estimator = SACEstimator()
|
| 245 |
+
with FakeTensorMode():
|
| 246 |
+
module = ...
|
| 247 |
+
inp = ...
|
| 248 |
+
with sac_estimator('operator-level-cost-model'):
|
| 249 |
+
output = module(inp)
|
| 250 |
+
sac_estimator.display_modulewise_sac_stats(depth=4, print_tabular=True)
|
| 251 |
+
"""
|
| 252 |
+
|
| 253 |
+
def __init__(self) -> None:
|
| 254 |
+
self.sac_mod_stats: Dict[str, SACStats] = {}
|
| 255 |
+
self.sac_mod_tradeoff_stats: Dict[str, SACTradeOffStats] = {}
|
| 256 |
+
self.sac_mod_greedy_order_meta: Dict[str, SACGreedyOrderMeta] = {}
|
| 257 |
+
self._mod_tracker = ModTracker()
|
| 258 |
+
self._sac_metadata: List[_SACMetadata] = []
|
| 259 |
+
self._sac_mod_metadata: Dict[str, _SACModMetadata] = {}
|
| 260 |
+
self._leaf_modules: Set[str] = set()
|
| 261 |
+
self._saved_tensor_hook_ctx = torch.autograd.graph.saved_tensors_hooks(
|
| 262 |
+
self._pack_hook, lambda x: x
|
| 263 |
+
)
|
| 264 |
+
self._saved_tensor_ids: Set[int] = set()
|
| 265 |
+
self._estimate_runtime = RuntimeEstimator._roofline_estimate
|
| 266 |
+
|
| 267 |
+
def _pack_hook(self, x: torch.Tensor) -> torch.Tensor:
|
| 268 |
+
# Hook function to track underlying storage IDs of tensors
|
| 269 |
+
# Updates the _saved_tensor_ids set with the IDs of the tensor's storages
|
| 270 |
+
# Used in conjunction with torch.autograd.graph.saved_tensors_hooks
|
| 271 |
+
untyped_storages = _get_untyped_storages(x)
|
| 272 |
+
storage_ids = (hash(st) for st in untyped_storages)
|
| 273 |
+
self._saved_tensor_ids.update(storage_ids)
|
| 274 |
+
return x
|
| 275 |
+
|
| 276 |
+
def _pre_fw_hook(self, mod: nn.Module, inputs: Any) -> None:
|
| 277 |
+
# Pre-forward hook function to prepare module metadata
|
| 278 |
+
# Tracks module FQN, force store random flag, and ``SACModMetadata``
|
| 279 |
+
# Initializes metadata for non-leaf modules, marks leaf modules
|
| 280 |
+
mod_fqn = self._mod_tracker.get_known_fqn(mod)
|
| 281 |
+
assert mod_fqn is not None
|
| 282 |
+
num_children = sum(1 for _ in mod.children())
|
| 283 |
+
if num_children > 0:
|
| 284 |
+
force_store_random = self._get_force_store_random(inputs)
|
| 285 |
+
self._sac_mod_metadata[mod_fqn] = _SACModMetadata(
|
| 286 |
+
start_idx=len(self._sac_metadata),
|
| 287 |
+
force_store_random=force_store_random,
|
| 288 |
+
sac_metadata=[],
|
| 289 |
+
)
|
| 290 |
+
else:
|
| 291 |
+
self._leaf_modules.add(mod_fqn)
|
| 292 |
+
|
| 293 |
+
def _post_fw_hook(self, mod: nn.Module, inputs: Any, outputs: Any) -> None:
|
| 294 |
+
# 1. Retrieves the module's FQN and checks if it's a leaf module
|
| 295 |
+
# 2. If not a leaf module, computes:
|
| 296 |
+
# - ``SACStats`` using the module's metadata and force store random flag
|
| 297 |
+
# - ``SACGreedyOrderMeta`` using the computed SAC statistics
|
| 298 |
+
mod_fqn = self._mod_tracker.get_known_fqn(mod)
|
| 299 |
+
assert mod_fqn is not None
|
| 300 |
+
if mod_fqn in self._leaf_modules:
|
| 301 |
+
return
|
| 302 |
+
else:
|
| 303 |
+
self.sac_mod_stats[mod_fqn] = self._get_sac_stats(
|
| 304 |
+
data=self._sac_mod_metadata[mod_fqn].sac_metadata,
|
| 305 |
+
force_store_random=self._sac_mod_metadata[mod_fqn].force_store_random,
|
| 306 |
+
)
|
| 307 |
+
self.sac_mod_greedy_order_meta[mod_fqn] = self._get_greedy_order_meta(
|
| 308 |
+
self.sac_mod_stats[mod_fqn]
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
def _get_force_store_random(self, inputs: Any) -> bool:
|
| 312 |
+
flat_inputs, _ = tree_flatten(inputs)
|
| 313 |
+
return all(not isinstance(x, torch.Tensor) for x in flat_inputs)
|
| 314 |
+
|
| 315 |
+
def _get_sac_stats(
|
| 316 |
+
self, data: List[_SACMetadata], force_store_random: bool
|
| 317 |
+
) -> SACStats:
|
| 318 |
+
# 1. Ignore the operations that should be skipped by SAC such as aten.detach.default because autograd
|
| 319 |
+
# inserts those during backward and it breaks the fwd-bwd alignment
|
| 320 |
+
filtered_data = [x for x in data if x.func not in OPS_TO_ALWAYS_SKIP]
|
| 321 |
+
|
| 322 |
+
(
|
| 323 |
+
ops,
|
| 324 |
+
runtimes_,
|
| 325 |
+
memory_,
|
| 326 |
+
new_ids,
|
| 327 |
+
output_ids,
|
| 328 |
+
inplace_ops_,
|
| 329 |
+
view_like_ops_,
|
| 330 |
+
rand_ops_,
|
| 331 |
+
) = zip(*[astuple(x) for x in filtered_data], strict=True)
|
| 332 |
+
|
| 333 |
+
# 2. Extract the metadata information
|
| 334 |
+
runtimes = list(runtimes_)
|
| 335 |
+
memory = list(memory_)
|
| 336 |
+
func_names = [op._overloadpacket.__name__ for op in ops]
|
| 337 |
+
view_like_ops = [i for i, x in enumerate(view_like_ops_) if x]
|
| 338 |
+
rand_ops = [i for i, x in enumerate(rand_ops_) if x]
|
| 339 |
+
saved_autograd_ops = [
|
| 340 |
+
i
|
| 341 |
+
for i, out_ids in enumerate(output_ids)
|
| 342 |
+
if set(out_ids).issubset(self._saved_tensor_ids)
|
| 343 |
+
]
|
| 344 |
+
|
| 345 |
+
# 3. Remap the inplace indices as we have removed OPS_TO_ALWAYS_SKIP
|
| 346 |
+
# FIXME @sanketpurandare: Fix this by changing the parent of the inplace-op
|
| 347 |
+
# to itself if the original parent is in OPS_TO_ALWAYS_SKIP.
|
| 348 |
+
try:
|
| 349 |
+
inplace_ops = [tuple(map(new_ids.index, x)) for x in inplace_ops_ if x]
|
| 350 |
+
except ValueError as err:
|
| 351 |
+
raise ValueError(
|
| 352 |
+
f"The remapping of inplace ops failed since one of the inplace op parents"
|
| 353 |
+
f" must have been present in {OPS_TO_ALWAYS_SKIP}"
|
| 354 |
+
) from err
|
| 355 |
+
|
| 356 |
+
# 4. The last operation is always stored as the output of the checkpoint
|
| 357 |
+
# block, so we can avoid recomputing it. We set the memory to zero
|
| 358 |
+
# instead of adding a new constraint because we want both the 0 and 1
|
| 359 |
+
# endpoints for memory_budget to be valid
|
| 360 |
+
# FIXME @sanketpurandare: this heuristic for finding the last non-view non-inplace op
|
| 361 |
+
# might not always be correct, which would yield suboptimal policies
|
| 362 |
+
last_op = len(ops) - 1
|
| 363 |
+
skip_ops_ = set(view_like_ops) | set({x[0] for x in inplace_ops})
|
| 364 |
+
reversed_skip_ops = sorted(skip_ops_, reverse=True)
|
| 365 |
+
for op in reversed_skip_ops:
|
| 366 |
+
if op == last_op:
|
| 367 |
+
last_op -= 1
|
| 368 |
+
|
| 369 |
+
memory[last_op] = 0
|
| 370 |
+
|
| 371 |
+
# 5. Create a single ``SACStats`` object for the entire block of ``_SACMetadata``.
|
| 372 |
+
return SACStats(
|
| 373 |
+
func_names=func_names,
|
| 374 |
+
runtimes=runtimes,
|
| 375 |
+
memory=memory,
|
| 376 |
+
view_like_ops=view_like_ops,
|
| 377 |
+
rand_ops=rand_ops,
|
| 378 |
+
saved_autograd_ops=saved_autograd_ops,
|
| 379 |
+
inplace_ops=inplace_ops, # type: ignore[arg-type]
|
| 380 |
+
force_store_random=force_store_random,
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
def _get_inplace_metadata(
|
| 384 |
+
self, func: Any, out_storages: Set[UntypedStorage]
|
| 385 |
+
) -> Tuple[int, Tuple[int, ...], Dict[str, Tuple[int, ...]]]:
|
| 386 |
+
# 1. Get the current index of the metadata obtained so far
|
| 387 |
+
curr_idx = len(self._sac_metadata)
|
| 388 |
+
# 2. Get the set of active modules that are not leaf
|
| 389 |
+
active_mod_fqns: Set[str] = {
|
| 390 |
+
par for par in self._mod_tracker.parents if par not in self._leaf_modules
|
| 391 |
+
}
|
| 392 |
+
# 3. Output ids are the identifies of the storage objects corresponding to the tensors
|
| 393 |
+
output_ids = tuple(hash(st) for st in out_storages)
|
| 394 |
+
# 4. If the function is not inplace, return
|
| 395 |
+
if not is_inplace(func):
|
| 396 |
+
return curr_idx, output_ids, {mod_fqn: () for mod_fqn in active_mod_fqns}
|
| 397 |
+
|
| 398 |
+
op_idx = curr_idx
|
| 399 |
+
# 5. Initialize the parent op ids of the inplace op for each of the active modules
|
| 400 |
+
mod_op_parent_idxs: Dict[str, int] = {
|
| 401 |
+
mod_fqn: -1 for mod_fqn in active_mod_fqns
|
| 402 |
+
}
|
| 403 |
+
for i, d in enumerate(self._sac_metadata):
|
| 404 |
+
# 6. Find the first occurence of a tensor corresponding to each module that
|
| 405 |
+
# shares the same storage as the current tensor
|
| 406 |
+
past_output_ids = d.output_ids
|
| 407 |
+
if set(output_ids).issubset(set(past_output_ids)):
|
| 408 |
+
for mod_fqn, op_parent_idx in mod_op_parent_idxs.items():
|
| 409 |
+
if op_parent_idx == -1:
|
| 410 |
+
if acm_stats := self._sac_mod_metadata.get(mod_fqn, None):
|
| 411 |
+
if i >= acm_stats.start_idx:
|
| 412 |
+
mod_op_parent_idxs[mod_fqn] = i
|
| 413 |
+
else:
|
| 414 |
+
assert mod_fqn == "Global"
|
| 415 |
+
mod_op_parent_idxs[mod_fqn] = i
|
| 416 |
+
# 7. If no parent tensor is found, then it's probably an inplace op on the arguments
|
| 417 |
+
# so one can just store the current-op idx as parent idx
|
| 418 |
+
for mod_fqn, op_parent_idx in mod_op_parent_idxs.items():
|
| 419 |
+
if op_parent_idx < 0:
|
| 420 |
+
mod_op_parent_idxs[mod_fqn] = op_idx
|
| 421 |
+
mod_inplace_info = {
|
| 422 |
+
mod_fqn: (op_idx, mod_op_parent_idxs[mod_fqn])
|
| 423 |
+
for mod_fqn in active_mod_fqns
|
| 424 |
+
}
|
| 425 |
+
return curr_idx, output_ids, mod_inplace_info # type: ignore[return-value]
|
| 426 |
+
|
| 427 |
+
def __torch_dispatch__( # type: ignore[no-untyped-def]
|
| 428 |
+
self, func, types, args=..., kwargs=None
|
| 429 |
+
):
|
| 430 |
+
# 1. Get the runtime estimate
|
| 431 |
+
out, op_time = self._estimate_runtime(func, args, kwargs)
|
| 432 |
+
flat_outs, _ = tree_flatten(out)
|
| 433 |
+
out_storages_cuda: Set[UntypedStorage] = set()
|
| 434 |
+
out_storages_cpu: Set[UntypedStorage] = set()
|
| 435 |
+
cuda_devices: Set[torch.device] = set()
|
| 436 |
+
for o in flat_outs:
|
| 437 |
+
if isinstance(o, torch.Tensor):
|
| 438 |
+
if o.device.type == "cuda":
|
| 439 |
+
out_storages_cuda.update(_get_untyped_storages(o))
|
| 440 |
+
cuda_devices.add(o.device)
|
| 441 |
+
else:
|
| 442 |
+
out_storages_cpu.update(_get_untyped_storages(o))
|
| 443 |
+
|
| 444 |
+
# Check if there's more than 1 CUDA device
|
| 445 |
+
assert (
|
| 446 |
+
len(cuda_devices) <= 1
|
| 447 |
+
), f"{func.__name__}'s output has more than 1 CUDA devices {cuda_devices}"
|
| 448 |
+
|
| 449 |
+
# 2. Get the memory consumed by output
|
| 450 |
+
nbytes_cuda = sum(
|
| 451 |
+
math.ceil(st.nbytes() / _PYTORCH_MIN_ALLOCATE) * _PYTORCH_MIN_ALLOCATE
|
| 452 |
+
for st in out_storages_cuda
|
| 453 |
+
)
|
| 454 |
+
nbytes_cpu = sum(st.nbytes() for st in out_storages_cpu)
|
| 455 |
+
nbytes = nbytes_cuda + nbytes_cpu
|
| 456 |
+
# 3. Get the current operator index, output storage identifiers and inplace metadata
|
| 457 |
+
out_storages = out_storages_cuda | out_storages_cpu
|
| 458 |
+
curr_idx, output_ids, mod_inplace_info = self._get_inplace_metadata(
|
| 459 |
+
func, out_storages
|
| 460 |
+
)
|
| 461 |
+
# 4. Determine if the function is in-place, random-op or a view-like
|
| 462 |
+
is_view_like = is_view_fn(func) or is_inplace_view_fn(func)
|
| 463 |
+
is_rand_op = torch.Tag.nondeterministic_seeded in func.tags
|
| 464 |
+
if is_view_like:
|
| 465 |
+
nbytes = 0
|
| 466 |
+
# sdpa has non-deterministic seed, but might be deterministic
|
| 467 |
+
# if no dropout is applied
|
| 468 |
+
if func.overloadpacket.__name__ == "_scaled_dot_product_flash_attention":
|
| 469 |
+
is_rand_op = kwargs.get("dropout_p", 0) != 0
|
| 470 |
+
# 5. Create metadata information per active non-leaf module
|
| 471 |
+
for mod_fqn in self._mod_tracker.parents:
|
| 472 |
+
if mod_fqn in self._leaf_modules:
|
| 473 |
+
continue
|
| 474 |
+
acm = _SACMetadata(
|
| 475 |
+
func=func,
|
| 476 |
+
time_taken=op_time,
|
| 477 |
+
memory_used=nbytes,
|
| 478 |
+
curr_idx=curr_idx,
|
| 479 |
+
output_ids=output_ids,
|
| 480 |
+
inplace_info=mod_inplace_info[mod_fqn],
|
| 481 |
+
is_view_like=is_view_like,
|
| 482 |
+
is_rand_op=is_rand_op,
|
| 483 |
+
)
|
| 484 |
+
if acm_stats := self._sac_mod_metadata.get(mod_fqn, None):
|
| 485 |
+
acm_stats.sac_metadata.append(acm)
|
| 486 |
+
else:
|
| 487 |
+
assert (
|
| 488 |
+
mod_fqn == "Global"
|
| 489 |
+
), f"Module {mod_fqn} not found in AC Mod Stats"
|
| 490 |
+
self._sac_metadata.append(acm)
|
| 491 |
+
|
| 492 |
+
return out
|
| 493 |
+
|
| 494 |
+
def _get_greedy_order_meta(self, sac_stats: SACStats) -> SACGreedyOrderMeta:
|
| 495 |
+
# An inplace-op group is a set of inplace-ops that operate on the same underlying tensor storage.
|
| 496 |
+
# 1. inplace_op_groups: A dictionary from the top-most parent of inplace-ops to the inplace-ops in the group
|
| 497 |
+
# The top-most op can itself be an inplace-op or can be a non-inplace op.
|
| 498 |
+
# 2. inplace_op_to_group_head: A dictionary that maps all the inplace-ops to their respective group heads.
|
| 499 |
+
inplace_op_groups: Dict[int, Set[int]] = {}
|
| 500 |
+
inplace_op_to_group_head: Dict[int, int] = dict(sac_stats.inplace_ops)
|
| 501 |
+
|
| 502 |
+
# Initialize inplace_op_groups using inplace_op_to_group_head
|
| 503 |
+
for op_idx, group_head_idx in inplace_op_to_group_head.items():
|
| 504 |
+
op_group = inplace_op_groups.setdefault(group_head_idx, {group_head_idx})
|
| 505 |
+
op_group.add(op_idx)
|
| 506 |
+
|
| 507 |
+
# Like inplace ops, all of the random ops in the function/module should all be either recomputed or saved
|
| 508 |
+
# as a group. This is because, they affect the ranom seed generator. If force_store_random is set True,
|
| 509 |
+
# all of the random ops will be stored by default. For easy of manageability, we store the top-most random op
|
| 510 |
+
# as the leader of the random_ops_group.
|
| 511 |
+
random_ops_group: Dict[int, Set[int]] = {}
|
| 512 |
+
random_group_head_idx = min(sac_stats.rand_ops, default=-1)
|
| 513 |
+
has_rand_ops = bool(sac_stats.rand_ops)
|
| 514 |
+
if has_rand_ops:
|
| 515 |
+
random_ops_group[random_group_head_idx] = set(sac_stats.rand_ops)
|
| 516 |
+
|
| 517 |
+
# 1. Random ops are stored if force_store_random is set
|
| 518 |
+
# 2. View-like ops are recomputed by default
|
| 519 |
+
# 3. For inplace_op_groups:
|
| 520 |
+
# a) If the head of this group is an inplace op, then we have to store the entire group.
|
| 521 |
+
# b) If any op in the group is random and force_store_random is set, then entire group will be stored.
|
| 522 |
+
# c) If none of ops in the group are random and the head of the group is not an in-place op, then
|
| 523 |
+
# this group can be considered for recomputation in its entireity
|
| 524 |
+
stored_ops: Set[int] = set()
|
| 525 |
+
recomputed_ops: Set[int] = set()
|
| 526 |
+
# Case 1:
|
| 527 |
+
if has_rand_ops and sac_stats.force_store_random:
|
| 528 |
+
stored_ops.add(random_group_head_idx)
|
| 529 |
+
# Case 2:
|
| 530 |
+
recomputed_ops.update(set(sac_stats.view_like_ops))
|
| 531 |
+
|
| 532 |
+
for group_head_idx, op_group in inplace_op_groups.items():
|
| 533 |
+
# Case 3a:
|
| 534 |
+
if group_head_idx in inplace_op_to_group_head:
|
| 535 |
+
stored_ops.add(group_head_idx)
|
| 536 |
+
# Case 3b:
|
| 537 |
+
if (
|
| 538 |
+
sac_stats.force_store_random & len(op_group & set(sac_stats.rand_ops))
|
| 539 |
+
> 0
|
| 540 |
+
):
|
| 541 |
+
stored_ops.add(group_head_idx)
|
| 542 |
+
|
| 543 |
+
# The potential recompute candidates are populated as:
|
| 544 |
+
recompute_candidates: Set[int] = set()
|
| 545 |
+
# 1) The random group head if it is not stored
|
| 546 |
+
if has_rand_ops and random_group_head_idx not in stored_ops:
|
| 547 |
+
recompute_candidates.add(random_group_head_idx)
|
| 548 |
+
# 2) The in-place op group heads that are not stored
|
| 549 |
+
recompute_candidates.update(set(inplace_op_groups.keys()) - stored_ops)
|
| 550 |
+
# 3) The non-inplace and non-random ops that are neither stored nor recomputed by default
|
| 551 |
+
recompute_candidates.update(
|
| 552 |
+
set(range(len(sac_stats.memory)))
|
| 553 |
+
- recomputed_ops
|
| 554 |
+
- stored_ops
|
| 555 |
+
- set(inplace_op_to_group_head.keys())
|
| 556 |
+
- set(sac_stats.rand_ops)
|
| 557 |
+
)
|
| 558 |
+
|
| 559 |
+
# We define msps for a recomp candidate as the ratio of memory/runtime aka memory savings per second
|
| 560 |
+
msps_meta: List[MSPS] = []
|
| 561 |
+
for cand_idx in recompute_candidates:
|
| 562 |
+
op_indices = {cand_idx}
|
| 563 |
+
if cand_idx in inplace_op_groups:
|
| 564 |
+
op_indices.update(inplace_op_groups[cand_idx])
|
| 565 |
+
if has_rand_ops and cand_idx == random_group_head_idx:
|
| 566 |
+
op_indices.update(sac_stats.rand_ops)
|
| 567 |
+
|
| 568 |
+
mem = sum(sac_stats.memory[op_idx] for op_idx in op_indices)
|
| 569 |
+
runtime = sum(sac_stats.runtimes[op_idx] for op_idx in op_indices)
|
| 570 |
+
func_names = {sac_stats.func_names[op_idx] for op_idx in op_indices}
|
| 571 |
+
msps = (mem / runtime) if runtime > 0 else sys.float_info.max
|
| 572 |
+
msps_meta.append(MSPS(func_names, cand_idx, mem, runtime, msps))
|
| 573 |
+
# We choose canidates to be recomputed based on increasing msps
|
| 574 |
+
msps_meta.sort(key=lambda x: x.msps, reverse=True)
|
| 575 |
+
return SACGreedyOrderMeta(
|
| 576 |
+
recomputed_ops, stored_ops, inplace_op_groups, random_ops_group, msps_meta
|
| 577 |
+
)
|
| 578 |
+
|
| 579 |
+
def _get_sac_tradeoff_pwlf_stats(
|
| 580 |
+
self,
|
| 581 |
+
sac_stats: SACStats,
|
| 582 |
+
greedy_order_meta: SACGreedyOrderMeta,
|
| 583 |
+
n_segments: int = 2,
|
| 584 |
+
save_tradeoff_graph: bool = False,
|
| 585 |
+
filename: str = "ac_tradeoff",
|
| 586 |
+
) -> SACTradeOffStats:
|
| 587 |
+
try:
|
| 588 |
+
import numpy as np # type: ignore[import-not-found]
|
| 589 |
+
import pwlf # type: ignore[import-untyped, import-not-found]
|
| 590 |
+
except ImportError as err:
|
| 591 |
+
raise ImportError("Please install pwlf and numpy package.") from err
|
| 592 |
+
|
| 593 |
+
stored_ops, recomputed_ops, inplace_op_groups, random_ops_group, msps_meta = (
|
| 594 |
+
greedy_order_meta.stored_ops,
|
| 595 |
+
greedy_order_meta.recomputed_ops,
|
| 596 |
+
greedy_order_meta.inplace_op_groups,
|
| 597 |
+
greedy_order_meta.random_ops_group,
|
| 598 |
+
greedy_order_meta.msps_meta,
|
| 599 |
+
)
|
| 600 |
+
# 1. Intitialize the discarded memory and recomputation runtime to sum of already chosen recomputed_ops
|
| 601 |
+
recomp_indices: Set[int] = set()
|
| 602 |
+
for r_idx in recomputed_ops:
|
| 603 |
+
recomp_indices.add(r_idx)
|
| 604 |
+
if r_idx in inplace_op_groups:
|
| 605 |
+
recomp_indices.update(inplace_op_groups[r_idx])
|
| 606 |
+
if r_idx in random_ops_group:
|
| 607 |
+
recomp_indices.update(random_ops_group[r_idx])
|
| 608 |
+
|
| 609 |
+
discarded_mem = sum(sac_stats.memory[op_idx] for op_idx in recomp_indices)
|
| 610 |
+
recomp_runtime = sum(sac_stats.runtimes[op_idx] for op_idx in recomp_indices)
|
| 611 |
+
# 2. Initialize the max recomputation time and total recomputation memory
|
| 612 |
+
sac_runtime = sum(sac_stats.runtimes)
|
| 613 |
+
sac_memory = sum(sac_stats.memory)
|
| 614 |
+
# 3. Tradeoff curve stores the KV pair of the dicarded memory to total memory and,
|
| 615 |
+
# recomputation time to total runtime incurred.
|
| 616 |
+
delta = 1e-2
|
| 617 |
+
tradeoff_curve = OrderedDict()
|
| 618 |
+
# 4. Initialize the trade-off curve with the stats of of already chosen recomputed_ops
|
| 619 |
+
tradeoff_curve[(discarded_mem / sac_memory) + delta] = (
|
| 620 |
+
recomp_runtime / sac_runtime
|
| 621 |
+
)
|
| 622 |
+
# 5. Update the trade-off curve with memory and runtime stats of SAC candidates in the
|
| 623 |
+
# greedy order of their ``MSPS``.
|
| 624 |
+
for cand in msps_meta:
|
| 625 |
+
discarded_mem += cand.memory
|
| 626 |
+
recomp_runtime += cand.runtime
|
| 627 |
+
tradeoff_curve[(discarded_mem / sac_memory) + delta] = (
|
| 628 |
+
recomp_runtime / sac_runtime
|
| 629 |
+
)
|
| 630 |
+
# 6. Finally, we add the memory and recomputation time of the always stored ops.
|
| 631 |
+
stored_indices: Set[int] = set()
|
| 632 |
+
for s_idx in stored_ops:
|
| 633 |
+
stored_indices.add(s_idx)
|
| 634 |
+
if s_idx in inplace_op_groups:
|
| 635 |
+
stored_indices.update(inplace_op_groups[s_idx])
|
| 636 |
+
if s_idx in random_ops_group:
|
| 637 |
+
stored_indices.update(random_ops_group[s_idx])
|
| 638 |
+
discarded_mem += sum(sac_stats.memory[op_idx] for op_idx in stored_indices)
|
| 639 |
+
recomp_runtime += sum(sac_stats.runtimes[op_idx] for op_idx in stored_indices)
|
| 640 |
+
tradeoff_curve[(discarded_mem / sac_memory) + delta] = (
|
| 641 |
+
recomp_runtime / sac_runtime
|
| 642 |
+
)
|
| 643 |
+
x_ = list(tradeoff_curve.keys())
|
| 644 |
+
y_ = list(tradeoff_curve.values())
|
| 645 |
+
# 7. We shift the y values to left and x values to right to upperbound the trade-off function
|
| 646 |
+
# TODO: Write a better explanation why this needs to be done
|
| 647 |
+
x = x_[: len(x_) - 1]
|
| 648 |
+
y = y_[1:]
|
| 649 |
+
tradeoff_pwlf = pwlf.PiecewiseLinFit(x, y)
|
| 650 |
+
# 8. Fit a piecewise linear function with the specified number of segments to the trade-off curve.
|
| 651 |
+
n_segments = max(min(len(x) - 2, n_segments), 1)
|
| 652 |
+
tradeoff_pwlf.fit(n_segments=n_segments)
|
| 653 |
+
|
| 654 |
+
# save prediction graph
|
| 655 |
+
def save_prediction_graph(
|
| 656 |
+
pwlf_: pwlf.PiecewiseLinFit, x: List[float], y: List[float], filename: str
|
| 657 |
+
) -> None:
|
| 658 |
+
try:
|
| 659 |
+
import matplotlib.pyplot as plt # type: ignore[import-not-found]
|
| 660 |
+
import numpy as np # type: ignore[import-not-found]
|
| 661 |
+
except ImportError as err:
|
| 662 |
+
raise ImportError(
|
| 663 |
+
"Install matplotlib and numpy using pip: pip install matplotlib numpy"
|
| 664 |
+
) from err
|
| 665 |
+
# predict for the determined points
|
| 666 |
+
xHat = np.linspace(min(x), max(x), num=10000)
|
| 667 |
+
yHat = pwlf_.predict(xHat)
|
| 668 |
+
|
| 669 |
+
# plot the results
|
| 670 |
+
plt.figure()
|
| 671 |
+
plt.plot(x, y, "o", label="Shifted")
|
| 672 |
+
plt.plot(xHat, yHat, "-", label="Predicted")
|
| 673 |
+
plt.plot(x_, y_, "x", label="Original")
|
| 674 |
+
plt.ylabel("Recomp time / Total recomp time")
|
| 675 |
+
plt.xlabel("Memory discarded / Total memory")
|
| 676 |
+
plt.legend()
|
| 677 |
+
plt.title(f"{filename}")
|
| 678 |
+
plt.suptitle(
|
| 679 |
+
f"Total Memory = {sac_memory} B Total Runtime = {sac_runtime:.4f} ms",
|
| 680 |
+
fontsize=10,
|
| 681 |
+
)
|
| 682 |
+
folder_name = "tradeoff_graphs"
|
| 683 |
+
if not os.path.exists(folder_name):
|
| 684 |
+
os.makedirs(folder_name)
|
| 685 |
+
# Save the plots in the folder
|
| 686 |
+
plt.savefig(os.path.join(folder_name, f"{filename}.png"))
|
| 687 |
+
|
| 688 |
+
if save_tradeoff_graph:
|
| 689 |
+
save_prediction_graph(tradeoff_pwlf, x, y, filename)
|
| 690 |
+
# 9. Obtain the slopes, intercepts and breakpoints of the fitted piecewise linear functions
|
| 691 |
+
slopes = tradeoff_pwlf.calc_slopes().tolist()
|
| 692 |
+
assert isinstance(tradeoff_pwlf.intercepts, np.ndarray) and isinstance(
|
| 693 |
+
tradeoff_pwlf.fit_breaks, np.ndarray
|
| 694 |
+
)
|
| 695 |
+
intercepts = tradeoff_pwlf.intercepts.tolist()
|
| 696 |
+
fit_breaks = tradeoff_pwlf.fit_breaks.tolist()
|
| 697 |
+
return SACTradeOffStats(
|
| 698 |
+
n_segments=n_segments,
|
| 699 |
+
slopes=slopes,
|
| 700 |
+
intercepts=intercepts,
|
| 701 |
+
fit_breaks=fit_breaks,
|
| 702 |
+
tradeoff_curve=tradeoff_curve,
|
| 703 |
+
sac_memory=sac_memory,
|
| 704 |
+
sac_runtime=sac_runtime,
|
| 705 |
+
)
|
| 706 |
+
|
| 707 |
+
def display_sac_stats(
|
| 708 |
+
self, sac_stats: SACStats, print_tabular: bool = False
|
| 709 |
+
) -> None:
|
| 710 |
+
"""
|
| 711 |
+
Displays the SAC statistics.
|
| 712 |
+
|
| 713 |
+
Args:
|
| 714 |
+
sac_stats (SACStats): The SAC statistics to display.
|
| 715 |
+
print_tabular (bool, optional): Whether to print the statistics in a tabular format. Defaults to False.
|
| 716 |
+
|
| 717 |
+
Prints:
|
| 718 |
+
1. Total Memory: The total memory usage in bytes.
|
| 719 |
+
2. Total Runtime: The total runtime in milliseconds.
|
| 720 |
+
3. Store Random: A flag indicating whether to force store random operator results.
|
| 721 |
+
|
| 722 |
+
Followed by a table with the following columns:
|
| 723 |
+
1. Op Idx: The operator index.
|
| 724 |
+
2. Op Name: The operator name.
|
| 725 |
+
3. Runtimes (ms): The operator runtime in milliseconds.
|
| 726 |
+
4. Memory (B): The operator memory usage in bytes.
|
| 727 |
+
5. View-like: A flag indicating whether the operator is view-like.
|
| 728 |
+
6. Random: A flag indicating whether the operator is random.
|
| 729 |
+
7. Saved Autograd: A flag indicating whether the operator's result is saved by autograd engine.
|
| 730 |
+
8. In-place: The index of the operator's first parent, or None if not in-place.
|
| 731 |
+
|
| 732 |
+
If print_tabular is True, the table is printed in a tabular format.
|
| 733 |
+
Otherwise, the table is printed in a plain text format.
|
| 734 |
+
"""
|
| 735 |
+
print(
|
| 736 |
+
f"Total Memory: {sum(sac_stats.memory)} B Total Runtime: {sum(sac_stats.runtimes)} ms"
|
| 737 |
+
f" Store Random: {sac_stats.force_store_random}"
|
| 738 |
+
)
|
| 739 |
+
table_data = []
|
| 740 |
+
op_parent = dict(sac_stats.inplace_ops)
|
| 741 |
+
for i, fn_name in enumerate(sac_stats.func_names):
|
| 742 |
+
row = [
|
| 743 |
+
str(i),
|
| 744 |
+
fn_name,
|
| 745 |
+
f"{sac_stats.runtimes[i]:.4f}",
|
| 746 |
+
str(sac_stats.memory[i]),
|
| 747 |
+
str(i in sac_stats.view_like_ops),
|
| 748 |
+
str(i in sac_stats.rand_ops),
|
| 749 |
+
str(i in sac_stats.saved_autograd_ops),
|
| 750 |
+
str(op_parent.get(i, None)),
|
| 751 |
+
]
|
| 752 |
+
table_data.append(row)
|
| 753 |
+
# Define headers
|
| 754 |
+
headers = [
|
| 755 |
+
"Op Idx",
|
| 756 |
+
"Op Name",
|
| 757 |
+
"Runtimes(ms)",
|
| 758 |
+
"Memory (B)",
|
| 759 |
+
"View-like",
|
| 760 |
+
"Random",
|
| 761 |
+
"Saved Autograd",
|
| 762 |
+
"In-place",
|
| 763 |
+
]
|
| 764 |
+
if print_tabular:
|
| 765 |
+
_display_stats_tabular(headers, table_data)
|
| 766 |
+
else:
|
| 767 |
+
max_widths = [0 for _ in range(len(headers))]
|
| 768 |
+
table_data.insert(0, headers)
|
| 769 |
+
for row in table_data:
|
| 770 |
+
for i, elem in enumerate(row):
|
| 771 |
+
max_widths[i] = max(max_widths[i], len(elem))
|
| 772 |
+
for row in table_data:
|
| 773 |
+
print(
|
| 774 |
+
"\t".join(
|
| 775 |
+
[f"{elem:<{max_widths[i]}}" for i, elem in enumerate(row)]
|
| 776 |
+
)
|
| 777 |
+
)
|
| 778 |
+
|
| 779 |
+
def display_sac_tradeoff_stats(
|
| 780 |
+
self,
|
| 781 |
+
greedy_order_meta: SACGreedyOrderMeta,
|
| 782 |
+
sac_stats: SACStats,
|
| 783 |
+
print_tabular: bool = False,
|
| 784 |
+
) -> None:
|
| 785 |
+
"""
|
| 786 |
+
Displays the SAC trade-off statistics.
|
| 787 |
+
|
| 788 |
+
Args:
|
| 789 |
+
greedy_order_meta (SACGreedyOrderMeta): The SAC greedy order metadata.
|
| 790 |
+
sac_stats (SACStats): The SAC statistics.
|
| 791 |
+
print_tabular (bool, optional): Whether to print the statistics in a tabular format. Defaults to False.
|
| 792 |
+
|
| 793 |
+
Prints:
|
| 794 |
+
A table with the following columns:
|
| 795 |
+
1. Op Id(s): The operator index(es).
|
| 796 |
+
2. Op Name(s): The operator name(s).
|
| 797 |
+
3. Discarded Mem (%): The percentage of discarded memory.
|
| 798 |
+
4. Discarded Mem (B): The discarded memory in bytes.
|
| 799 |
+
5. Recomp time (%): The percentage of recomputed time.
|
| 800 |
+
6. Recomp time (ms): The recomputed time in milliseconds.
|
| 801 |
+
7. MSPS: The memory per second.
|
| 802 |
+
8. Always Stored: A flag indicating whether the operator is always stored.
|
| 803 |
+
9. Always Recomputed: A flag indicating whether the operator is always recomputed.
|
| 804 |
+
|
| 805 |
+
If print_tabular is True, the table is printed in a tabular format.
|
| 806 |
+
Otherwise, the table is printed in a plain text format.
|
| 807 |
+
"""
|
| 808 |
+
table_data = []
|
| 809 |
+
total_memory, total_runtime = sum(sac_stats.memory), sum(sac_stats.runtimes)
|
| 810 |
+
discarded_mem: int = 0
|
| 811 |
+
recomp_runtime: float = 0.0
|
| 812 |
+
|
| 813 |
+
def append_row(
|
| 814 |
+
op_indices: Set[int],
|
| 815 |
+
func_names: Set[str],
|
| 816 |
+
msps: Optional[float] = None,
|
| 817 |
+
stored: Optional[bool] = False,
|
| 818 |
+
recomputed: Optional[bool] = False,
|
| 819 |
+
) -> None:
|
| 820 |
+
row = [
|
| 821 |
+
str(op_indices),
|
| 822 |
+
str(func_names),
|
| 823 |
+
f"{discarded_mem / total_memory:.4f}",
|
| 824 |
+
str(discarded_mem),
|
| 825 |
+
f"{recomp_runtime / total_runtime:.4f}",
|
| 826 |
+
str(recomp_runtime),
|
| 827 |
+
f"{msps:.2e}" if msps is not None else str(nan),
|
| 828 |
+
str(stored),
|
| 829 |
+
str(recomputed),
|
| 830 |
+
]
|
| 831 |
+
table_data.append(row)
|
| 832 |
+
|
| 833 |
+
stored_ops, recomputed_ops, inplace_op_groups, random_ops_group, msps_meta = (
|
| 834 |
+
greedy_order_meta.stored_ops,
|
| 835 |
+
greedy_order_meta.recomputed_ops,
|
| 836 |
+
greedy_order_meta.inplace_op_groups,
|
| 837 |
+
greedy_order_meta.random_ops_group,
|
| 838 |
+
greedy_order_meta.msps_meta,
|
| 839 |
+
)
|
| 840 |
+
|
| 841 |
+
for op_idx in recomputed_ops:
|
| 842 |
+
op_indices: Set[int] = {op_idx}
|
| 843 |
+
if op_idx in inplace_op_groups:
|
| 844 |
+
op_indices.update(inplace_op_groups[op_idx])
|
| 845 |
+
if op_idx in random_ops_group:
|
| 846 |
+
op_indices.update(random_ops_group[op_idx])
|
| 847 |
+
discarded_mem += sum(sac_stats.memory[i] for i in op_indices)
|
| 848 |
+
recomp_runtime += sum(sac_stats.runtimes[i] for i in op_indices)
|
| 849 |
+
func_names = {sac_stats.func_names[i] for i in op_indices}
|
| 850 |
+
append_row(op_indices, func_names, recomputed=True)
|
| 851 |
+
|
| 852 |
+
for cand in msps_meta:
|
| 853 |
+
discarded_mem += cand.memory
|
| 854 |
+
recomp_runtime += cand.runtime
|
| 855 |
+
op_indices = {cand.op_idx}
|
| 856 |
+
if cand.op_idx in inplace_op_groups:
|
| 857 |
+
op_indices.update(inplace_op_groups[cand.op_idx])
|
| 858 |
+
if cand.op_idx in random_ops_group:
|
| 859 |
+
op_indices.update(random_ops_group[cand.op_idx])
|
| 860 |
+
append_row(op_indices, cand.func_names, msps=cand.msps)
|
| 861 |
+
|
| 862 |
+
for op_idx in stored_ops:
|
| 863 |
+
op_indices = {op_idx}
|
| 864 |
+
if op_idx in inplace_op_groups:
|
| 865 |
+
op_indices.update(inplace_op_groups[op_idx])
|
| 866 |
+
if op_idx in random_ops_group:
|
| 867 |
+
op_indices.update(random_ops_group[op_idx])
|
| 868 |
+
discarded_mem += sum(sac_stats.memory[i] for i in op_indices)
|
| 869 |
+
recomp_runtime += sum(sac_stats.runtimes[i] for i in op_indices)
|
| 870 |
+
func_names = {sac_stats.func_names[i] for i in op_indices}
|
| 871 |
+
append_row(op_indices, func_names, stored=True)
|
| 872 |
+
|
| 873 |
+
headers = [
|
| 874 |
+
"Op Id(s)",
|
| 875 |
+
"Op Name(s)",
|
| 876 |
+
"Discarded Mem (%)",
|
| 877 |
+
"Discarded Mem (B)",
|
| 878 |
+
"Recomp time (%)",
|
| 879 |
+
"Recomp time (ms)",
|
| 880 |
+
"MSPS",
|
| 881 |
+
"Always Stored",
|
| 882 |
+
"Always Recomputed",
|
| 883 |
+
]
|
| 884 |
+
if print_tabular:
|
| 885 |
+
_display_stats_tabular(headers, table_data)
|
| 886 |
+
else:
|
| 887 |
+
max_widths = [0 for _ in range(len(headers))]
|
| 888 |
+
table_data.insert(0, headers)
|
| 889 |
+
for row in table_data:
|
| 890 |
+
for i, elem in enumerate(row):
|
| 891 |
+
max_widths[i] = max(max_widths[i], len(elem))
|
| 892 |
+
for row in table_data:
|
| 893 |
+
print(
|
| 894 |
+
"\t".join(
|
| 895 |
+
[f"{elem:<{max_widths[i]}}" for i, elem in enumerate(row)]
|
| 896 |
+
)
|
| 897 |
+
)
|
| 898 |
+
|
| 899 |
+
def pwlf_sac_tradeoff_curve(
|
| 900 |
+
self,
|
| 901 |
+
n_segments: int = 2,
|
| 902 |
+
save_tradeoff_graphs: bool = False,
|
| 903 |
+
) -> None:
|
| 904 |
+
"""
|
| 905 |
+
Fits a piecewise linear function with the specified sumber of segments to the SAC trade-off curve of
|
| 906 |
+
discarded memory vs recomputation time.
|
| 907 |
+
|
| 908 |
+
Args:
|
| 909 |
+
n_segments (int, optional): The number of segments to be used for fitting the piecewise linear function to
|
| 910 |
+
the trade-off curve. Defaults to 2.
|
| 911 |
+
save_tradeoff_graphs (bool, optional): Whether to save the trade-off graphs to file. Defaults to False.
|
| 912 |
+
|
| 913 |
+
If save_tradeoff_graphs is True, the trade-off graphs are saved to file using the module FQN as the filename.
|
| 914 |
+
"""
|
| 915 |
+
for mod_fqn, sac_stats in self.sac_mod_stats.items():
|
| 916 |
+
self.sac_mod_tradeoff_stats[mod_fqn] = self._get_sac_tradeoff_pwlf_stats(
|
| 917 |
+
sac_stats=sac_stats,
|
| 918 |
+
greedy_order_meta=self.sac_mod_greedy_order_meta[mod_fqn],
|
| 919 |
+
n_segments=n_segments,
|
| 920 |
+
save_tradeoff_graph=save_tradeoff_graphs,
|
| 921 |
+
filename=mod_fqn,
|
| 922 |
+
)
|
| 923 |
+
|
| 924 |
+
def display_modulewise_sac_stats(
|
| 925 |
+
self, depth: int = 2, print_tabular: bool = False
|
| 926 |
+
) -> None:
|
| 927 |
+
"""
|
| 928 |
+
Displays the SAC and trade-off statistics for each module.
|
| 929 |
+
|
| 930 |
+
Args:
|
| 931 |
+
depth (int, optional): The maximum depth of modules to display. Defaults to 2.
|
| 932 |
+
print_tabular (bool, optional): Whether to print the statistics in a tabular format. Defaults to False.
|
| 933 |
+
|
| 934 |
+
Prints:
|
| 935 |
+
For each module with depth less than or equal to the specified depth:
|
| 936 |
+
1. The SAC statistics for the module (using display_sac_stats).
|
| 937 |
+
2. The SAC trade-off statistics for the module (using display_sac_tradeoff_stats).
|
| 938 |
+
|
| 939 |
+
If print_tabular is True, the statistics are printed in a tabular format.
|
| 940 |
+
Otherwise, the statistics are printed in a plain text format.
|
| 941 |
+
"""
|
| 942 |
+
for mod_fqn, sac_stats in self.sac_mod_stats.items():
|
| 943 |
+
mod_depth = mod_fqn.count(".") + 1
|
| 944 |
+
if mod_depth > depth:
|
| 945 |
+
continue
|
| 946 |
+
print(f"Module: {mod_fqn}")
|
| 947 |
+
self.display_sac_stats(sac_stats, print_tabular)
|
| 948 |
+
print(f"AC Trade-off for Module: {mod_fqn} MSPS = Memory/Runtime")
|
| 949 |
+
self.display_sac_tradeoff_stats(
|
| 950 |
+
self.sac_mod_greedy_order_meta[mod_fqn], sac_stats, print_tabular
|
| 951 |
+
)
|
| 952 |
+
|
| 953 |
+
def __call__(self, estimate_mode_type: str) -> Self:
|
| 954 |
+
"""
|
| 955 |
+
Sets the estimate mode type.
|
| 956 |
+
|
| 957 |
+
Currently supported modes:
|
| 958 |
+
- "operator-level-benchmark": Estimates runtime using operator benchmarking.
|
| 959 |
+
- "operator-level-cost-model": Estimates runtime using roofline cost model.
|
| 960 |
+
|
| 961 |
+
Args:
|
| 962 |
+
estimate_mode_type (str): The type of estimate mode to use.
|
| 963 |
+
|
| 964 |
+
Returns:
|
| 965 |
+
SACEstimator: The SAC estimator instance.
|
| 966 |
+
|
| 967 |
+
Raises:
|
| 968 |
+
NotImplementedError: If the estimate mode type is not supported.
|
| 969 |
+
"""
|
| 970 |
+
if estimate_mode_type == "operator-level-benchmark":
|
| 971 |
+
self._estimate_runtime = RuntimeEstimator._benchmark_estimate
|
| 972 |
+
elif estimate_mode_type == "operator-level-cost-model":
|
| 973 |
+
self._estimate_runtime = RuntimeEstimator._roofline_estimate
|
| 974 |
+
else:
|
| 975 |
+
raise NotImplementedError(
|
| 976 |
+
f"estimate_mode_type {estimate_mode_type} not supported"
|
| 977 |
+
)
|
| 978 |
+
return self
|
| 979 |
+
|
| 980 |
+
def __enter__(self) -> Self: # type: ignore[no-untyped-def]
|
| 981 |
+
fake_mode = active_fake_mode()
|
| 982 |
+
assert isinstance(
|
| 983 |
+
fake_mode, FakeTensorMode
|
| 984 |
+
), "SAC Estimator should be called in FakeTensorMode"
|
| 985 |
+
RuntimeEstimator.fake_mode = fake_mode
|
| 986 |
+
self._mod_tracker.register_user_hooks(
|
| 987 |
+
pre_fw_hook=self._pre_fw_hook,
|
| 988 |
+
post_fw_hook=self._post_fw_hook,
|
| 989 |
+
)
|
| 990 |
+
self._mod_tracker.__enter__()
|
| 991 |
+
self._saved_tensor_hook_ctx.__enter__()
|
| 992 |
+
return super().__enter__()
|
| 993 |
+
|
| 994 |
+
def __exit__(self, *args: Any) -> None: # type: ignore[no-untyped-def]
|
| 995 |
+
self._saved_tensor_hook_ctx.__exit__()
|
| 996 |
+
self._mod_tracker.__exit__(*args)
|
| 997 |
+
super().__exit__(*args)
|
lib/python3.12/site-packages/torch/distributed/_tools/sac_ilp.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import math
|
| 3 |
+
from enum import IntEnum
|
| 4 |
+
from typing import Dict, List, Optional, Tuple
|
| 5 |
+
|
| 6 |
+
from torch.distributed._tools.ilp_utils import Graph, is_submodule
|
| 7 |
+
from torch.distributed._tools.sac_estimator import SACStats
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
try:
|
| 11 |
+
from pulp import ( # type: ignore[import-untyped,import-not-found]
|
| 12 |
+
lpDot,
|
| 13 |
+
LpInteger,
|
| 14 |
+
LpMaximize,
|
| 15 |
+
LpMinimize,
|
| 16 |
+
LpProblem,
|
| 17 |
+
LpStatus,
|
| 18 |
+
lpSum,
|
| 19 |
+
LpVariable,
|
| 20 |
+
PULP_CBC_CMD,
|
| 21 |
+
value,
|
| 22 |
+
)
|
| 23 |
+
except ImportError as err:
|
| 24 |
+
raise ImportError(
|
| 25 |
+
"Please install pulp package. See: https://github.com/coin-or/pulp."
|
| 26 |
+
) from err
|
| 27 |
+
|
| 28 |
+
# Create a logger object
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
# Set the logging level to INFO
|
| 32 |
+
logger.setLevel(logging.INFO)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def sac_milp(
|
| 36 |
+
graph: Graph,
|
| 37 |
+
memory_budget: float,
|
| 38 |
+
world_size: int = 1,
|
| 39 |
+
ac_units: Optional[List[str]] = None,
|
| 40 |
+
fsdp_units: Optional[List[str]] = None,
|
| 41 |
+
) -> Tuple[Dict[str, float], float, int]:
|
| 42 |
+
"""
|
| 43 |
+
MILP to decide which modules to AC and how much memory to discard.
|
| 44 |
+
The objective is to minimize recomputation time.
|
| 45 |
+
The constraint is to ensure peak memory is under budget.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
graph: graph representation of the model as a module submodule tree
|
| 49 |
+
where each node is a submodule with memory & runtime stats
|
| 50 |
+
memory_budget: memory budget in GiB
|
| 51 |
+
world_size: number of GPUs. In the case of FSDP, world_size will be
|
| 52 |
+
used to compute the amount of parameter and gradient memory on each rank
|
| 53 |
+
ac_units: a list of user-specified AC units.
|
| 54 |
+
fsdp_units: a list of FSDP units. AC units cannot be supermodules of FSDP units.
|
| 55 |
+
|
| 56 |
+
Returns:
|
| 57 |
+
Dict[str, float]: the optimal SAC solution, mapping from module fqn to
|
| 58 |
+
the percentage of activation memory to **discard**
|
| 59 |
+
float: the recomputation time of the optimal SAC solution
|
| 60 |
+
int: upper bound on the peak memory of the optimal SAC solution.
|
| 61 |
+
note that value of -1 means that the ILP solver failed to find a solution.
|
| 62 |
+
|
| 63 |
+
"""
|
| 64 |
+
num_nodes = len(graph.nodes)
|
| 65 |
+
M = 10**2 # note: numerical issue may occur if M is too big
|
| 66 |
+
MEM_MULTIPLIER = 2**30
|
| 67 |
+
|
| 68 |
+
# Create a MILP problem
|
| 69 |
+
prob = LpProblem("SAC", LpMinimize)
|
| 70 |
+
|
| 71 |
+
# Create decision variables
|
| 72 |
+
# y_i: indicator for if module i is AC'ed
|
| 73 |
+
y = LpVariable.matrix("y", list(range(num_nodes)), 0, 1, LpInteger)
|
| 74 |
+
# r_i: percentage of discarded activation memory
|
| 75 |
+
r = LpVariable.matrix("r", list(range(num_nodes)), 0, 1)
|
| 76 |
+
# d_i: discarded activation memory for module i
|
| 77 |
+
d = LpVariable.matrix("d", list(range(num_nodes)), 0)
|
| 78 |
+
# a_i: total activation memory at module i
|
| 79 |
+
a = LpVariable.matrix("a", list(range(num_nodes)), 0)
|
| 80 |
+
# m_i: memory at module i, combining parameters, gradients, and activations
|
| 81 |
+
m = LpVariable.matrix("m", list(range(num_nodes)), 0)
|
| 82 |
+
# rcp_i: percentage of recomputation time
|
| 83 |
+
rcp = LpVariable.matrix("rcp", list(range(num_nodes)), 0)
|
| 84 |
+
# rct_i: recomputation time for module i (in ms)
|
| 85 |
+
rct = LpVariable.matrix("rct", list(range(num_nodes)), 0)
|
| 86 |
+
# max_m: peak memory
|
| 87 |
+
max_m = LpVariable("max_m", 0)
|
| 88 |
+
|
| 89 |
+
# Add constraints
|
| 90 |
+
# [Constraint] User specified AC units
|
| 91 |
+
if ac_units:
|
| 92 |
+
ac_units_set = set(ac_units)
|
| 93 |
+
for i in range(num_nodes):
|
| 94 |
+
if graph.nodes[i]["fqn"] not in ac_units_set:
|
| 95 |
+
prob += y[i] == 0
|
| 96 |
+
|
| 97 |
+
# [Constraint] AC units cannot be supmodules of user specified FSDP units
|
| 98 |
+
if fsdp_units:
|
| 99 |
+
for i in range(num_nodes):
|
| 100 |
+
if any(
|
| 101 |
+
is_submodule(fsdp_unit, graph.nodes[i]["fqn"])
|
| 102 |
+
for fsdp_unit in fsdp_units
|
| 103 |
+
):
|
| 104 |
+
prob += y[i] == 0
|
| 105 |
+
|
| 106 |
+
# [Constraint] No nested AC units
|
| 107 |
+
for i in range(num_nodes):
|
| 108 |
+
for j in range(i + 1, num_nodes):
|
| 109 |
+
if graph.ad_matrix[i][j] == 1:
|
| 110 |
+
prob += y[i] + y[j] <= 1
|
| 111 |
+
|
| 112 |
+
# [Constraint] Do not AC leaf modules
|
| 113 |
+
for i in range(num_nodes):
|
| 114 |
+
if graph.nodes[i]["is_leaf"]:
|
| 115 |
+
prob += y[i] == 0
|
| 116 |
+
|
| 117 |
+
# [Constraint] Express amount of discarded activation memory
|
| 118 |
+
for i in range(num_nodes):
|
| 119 |
+
# There are two measures for activation memory: ACM and IA
|
| 120 |
+
# 1. IA is the activation memory saved when not using AC
|
| 121 |
+
# 2. ACM is the total activation memory, including those
|
| 122 |
+
# that are not typically saved when not using AC
|
| 123 |
+
# Note: ACM >= IA
|
| 124 |
+
if (not graph.nodes[i]["is_leaf"]) and graph.nodes[i][
|
| 125 |
+
"sac_memory"
|
| 126 |
+
] < graph.nodes[i]["act_fw_per_module"]:
|
| 127 |
+
logger.warning("For module {%s}: ", graph.nodes[i]["fqn"])
|
| 128 |
+
logger.warning(
|
| 129 |
+
"activation memory from memory tracker is {%d},",
|
| 130 |
+
graph.nodes[i]["act_fw_per_module"],
|
| 131 |
+
)
|
| 132 |
+
logger.warning(
|
| 133 |
+
"activation memory from SAC estimator is {%d}.",
|
| 134 |
+
graph.nodes[i]["sac_memory"],
|
| 135 |
+
)
|
| 136 |
+
logger.warning("Something is wrong. Please check!")
|
| 137 |
+
logger.warning("Overriding the latter with the former.")
|
| 138 |
+
graph.nodes[i]["sac_memory"] = graph.nodes[i]["act_fw_per_module"]
|
| 139 |
+
ACM_i = graph.nodes[i]["sac_memory"] / MEM_MULTIPLIER
|
| 140 |
+
IA_i = graph.nodes[i]["act_fw_per_module"] / MEM_MULTIPLIER
|
| 141 |
+
prob += d[i] == ACM_i * r[i] - (ACM_i - IA_i) * y[i]
|
| 142 |
+
|
| 143 |
+
# [Constraint] Ensure correctness of r_i
|
| 144 |
+
# There are two parts to its correctness
|
| 145 |
+
# 1. r_i > 0 only if y_i == 1 (discard only if it is an AC unit)
|
| 146 |
+
# 2. r_i needs to be large enough to cover the difference between
|
| 147 |
+
# ACM and IA. Otherwise, we are not saving any memory
|
| 148 |
+
for i in range(num_nodes):
|
| 149 |
+
prob += y[i] >= r[i]
|
| 150 |
+
if graph.nodes[i]["is_leaf"]:
|
| 151 |
+
continue
|
| 152 |
+
ACM_i = graph.nodes[i]["sac_memory"] / MEM_MULTIPLIER
|
| 153 |
+
IA_i = graph.nodes[i]["act_fw_per_module"] / MEM_MULTIPLIER
|
| 154 |
+
prob += r[i] >= (ACM_i - IA_i) / ACM_i * y[i]
|
| 155 |
+
|
| 156 |
+
# [Constraint] Express total activation memory in the backward pass
|
| 157 |
+
for i in range(num_nodes):
|
| 158 |
+
AG_i = graph.nodes[i]["act_grad_per_module"] / MEM_MULTIPLIER
|
| 159 |
+
TA_i = graph.nodes[i]["act_total"] / MEM_MULTIPLIER
|
| 160 |
+
# related to discarded amount of memory
|
| 161 |
+
pos = graph.nodes[i]["pos_fw_post_order"]
|
| 162 |
+
coeff = [0] * num_nodes
|
| 163 |
+
for p in range(pos):
|
| 164 |
+
j = graph.name2node[graph.fw_post_order[p]]["index"]
|
| 165 |
+
coeff[j] = 1
|
| 166 |
+
prob += a[i] == TA_i + AG_i - lpDot(coeff, d)
|
| 167 |
+
|
| 168 |
+
# [Constraint] Express the total amount of memory at each module
|
| 169 |
+
# Note that unsharded parameters and gradients are not included here
|
| 170 |
+
P_1 = graph.nodes[0]["param_per_module"] / MEM_MULTIPLIER
|
| 171 |
+
for i in range(num_nodes):
|
| 172 |
+
TG_i = graph.nodes[i]["grad_total"] / MEM_MULTIPLIER
|
| 173 |
+
prob += m[i] == a[i] + (P_1 + TG_i) / world_size
|
| 174 |
+
|
| 175 |
+
# [Constraint] Express peak memory
|
| 176 |
+
for i in range(num_nodes):
|
| 177 |
+
prob += max_m >= m[i]
|
| 178 |
+
|
| 179 |
+
# [Constraint] Express percentage of recomputation time
|
| 180 |
+
for i in range(num_nodes):
|
| 181 |
+
for s in range(graph.nodes[i]["n_segments"]):
|
| 182 |
+
slope = graph.nodes[i]["slopes"][s]
|
| 183 |
+
intercept = graph.nodes[i]["intercepts"][s]
|
| 184 |
+
prob += rcp[i] >= slope * r[i] + intercept
|
| 185 |
+
|
| 186 |
+
# [Constraint] Express recomputation time
|
| 187 |
+
# rct_i = (rcp_i * ACT_i) if y_i == 1 else 0
|
| 188 |
+
for i in range(num_nodes):
|
| 189 |
+
ACT_i = graph.nodes[i]["sac_runtime"]
|
| 190 |
+
prob += rct[i] <= M * y[i]
|
| 191 |
+
prob += rct[i] <= ACT_i * rcp[i]
|
| 192 |
+
prob += rct[i] >= ACT_i * rcp[i] - M * (1 - y[i])
|
| 193 |
+
|
| 194 |
+
# [Constraint] Peak memory should be below budget
|
| 195 |
+
prob += max_m <= memory_budget
|
| 196 |
+
|
| 197 |
+
# Set Objeictive
|
| 198 |
+
prob += lpSum(rct)
|
| 199 |
+
|
| 200 |
+
# Solve
|
| 201 |
+
solver = PULP_CBC_CMD(gapRel=0.05, timeLimit=180, msg=0)
|
| 202 |
+
status = prob.solve(solver)
|
| 203 |
+
|
| 204 |
+
# If solver fails, print status and return empty solution
|
| 205 |
+
if status != 1:
|
| 206 |
+
logger.error("Solver failed to find a solution: %s", LpStatus[status])
|
| 207 |
+
return {}, 0, -1
|
| 208 |
+
|
| 209 |
+
# Gather and return solution if optimal solution is found
|
| 210 |
+
ac_decisions = {}
|
| 211 |
+
for i in range(num_nodes):
|
| 212 |
+
if round(y[i].varValue) == 1:
|
| 213 |
+
ac_decisions[graph.nodes[i]["fqn"]] = round(r[i].varValue, 4)
|
| 214 |
+
recomputation_time = round(value(prob.objective), 2)
|
| 215 |
+
peak_mem = round(max_m.varValue * MEM_MULTIPLIER)
|
| 216 |
+
|
| 217 |
+
return ac_decisions, recomputation_time, peak_mem
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
class SACDecision(IntEnum):
|
| 221 |
+
RECOMPUTE = 0
|
| 222 |
+
SAVE = 1
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def get_optimal_checkpointing_policy_per_module(
|
| 226 |
+
sac_stats: SACStats, memory_budget: float
|
| 227 |
+
) -> List[int]:
|
| 228 |
+
"""
|
| 229 |
+
This is adapted from --
|
| 230 |
+
https://github.com/facebookresearch/xformers/blob/c6c0ac31f1b08542a0bc27278c6ed10f825f6963/xformers/checkpoint.py#L375
|
| 231 |
+
|
| 232 |
+
Given the SACStats of a module, including list of operators, their memory, runtimes, and metadata,
|
| 233 |
+
decide via MILP an optimal set of operators to checkpoint under a given ``memory_budget``.
|
| 234 |
+
|
| 235 |
+
Args:
|
| 236 |
+
sac_stats: the SACStats object of the module
|
| 237 |
+
memory_budget: a float between zero and one
|
| 238 |
+
|
| 239 |
+
Returns:
|
| 240 |
+
List[int]: the decision whether each operator should be saved (1) or recomptued (0).
|
| 241 |
+
"""
|
| 242 |
+
if not (0 <= memory_budget <= 1):
|
| 243 |
+
raise ValueError(
|
| 244 |
+
f"`memory_budget` must be a float between 0 and 1. Got {memory_budget}."
|
| 245 |
+
)
|
| 246 |
+
num_ops = len(sac_stats.func_names)
|
| 247 |
+
|
| 248 |
+
# Create a MILP problem
|
| 249 |
+
prob = LpProblem("SAC-per-module", LpMaximize)
|
| 250 |
+
|
| 251 |
+
# Create decision variables
|
| 252 |
+
# x[i] = 1 means the i-th operator should be saved, otherwise it should be recomputed
|
| 253 |
+
x = LpVariable.matrix("x", list(range(num_ops)), 0, 1, LpInteger)
|
| 254 |
+
|
| 255 |
+
# Add constraints
|
| 256 |
+
# [Constraint] random ops should be saved if ``force_store_random`` is True
|
| 257 |
+
# otherwise, random ops should either be all recomputed or all saved
|
| 258 |
+
if sac_stats.force_store_random:
|
| 259 |
+
for i in sac_stats.rand_ops:
|
| 260 |
+
prob += x[i] == SACDecision.SAVE.value
|
| 261 |
+
else:
|
| 262 |
+
for i1, i2 in zip(sac_stats.rand_ops[:-1], sac_stats.rand_ops[1:]):
|
| 263 |
+
prob += x[i1] == x[i2]
|
| 264 |
+
|
| 265 |
+
# [Constraint] view-like ops should always be recomputed
|
| 266 |
+
for i in sac_stats.view_like_ops:
|
| 267 |
+
prob += x[i] == SACDecision.RECOMPUTE.value
|
| 268 |
+
|
| 269 |
+
# [Constraint] inplace ops should always be done in conjunction with its parent op
|
| 270 |
+
for op, op_parent in sac_stats.inplace_ops:
|
| 271 |
+
if op != op_parent:
|
| 272 |
+
prob += x[op] == x[op_parent]
|
| 273 |
+
else:
|
| 274 |
+
prob += x[op] == SACDecision.SAVE.value
|
| 275 |
+
|
| 276 |
+
# [Constraint] saved memory should be under the ``memory_budget``
|
| 277 |
+
max_memory = math.ceil(memory_budget * sum(sac_stats.memory))
|
| 278 |
+
prob += lpDot(x, sac_stats.memory) <= max_memory
|
| 279 |
+
|
| 280 |
+
# [Objective] minimize recomputation time, note the ILP is a maximization problem
|
| 281 |
+
# because x[i] == 1 means the op is saved (not recomputed), and thus recomputation
|
| 282 |
+
# time is sum(sac_stats.runtimes) - lpDot(x, sac_stats.runtimes)
|
| 283 |
+
prob += lpDot(x, sac_stats.runtimes)
|
| 284 |
+
|
| 285 |
+
# Solve
|
| 286 |
+
solver = PULP_CBC_CMD(gapRel=0.05, timeLimit=10, msg=0)
|
| 287 |
+
status = prob.solve(solver)
|
| 288 |
+
|
| 289 |
+
# If solver fails, print status and return empty solution
|
| 290 |
+
if status != 1:
|
| 291 |
+
logger.error("Solver failed to find a solution: %s", LpStatus[status])
|
| 292 |
+
return []
|
| 293 |
+
|
| 294 |
+
# Gather and return solution if optimal solution is found
|
| 295 |
+
return [round(x[i].varValue) for i in range(num_ops)]
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (1.03 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_checkpointer.cpython-312.pyc
ADDED
|
Binary file (4.62 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_dedup_save_plans.cpython-312.pyc
ADDED
|
Binary file (2.69 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_dedup_tensors.cpython-312.pyc
ADDED
|
Binary file (2.93 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_fsspec_filesystem.cpython-312.pyc
ADDED
|
Binary file (7.41 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_nested_dict.cpython-312.pyc
ADDED
|
Binary file (2.44 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_sharded_tensor_utils.cpython-312.pyc
ADDED
|
Binary file (4.69 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_storage_utils.cpython-312.pyc
ADDED
|
Binary file (1.69 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_traverse.cpython-312.pyc
ADDED
|
Binary file (8.8 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_version.cpython-312.pyc
ADDED
|
Binary file (335 Bytes). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/api.cpython-312.pyc
ADDED
|
Binary file (2.91 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/default_planner.cpython-312.pyc
ADDED
|
Binary file (22.2 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/filesystem.cpython-312.pyc
ADDED
|
Binary file (40 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/format_utils.cpython-312.pyc
ADDED
|
Binary file (12.9 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/logger.cpython-312.pyc
ADDED
|
Binary file (4.21 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/logging_handlers.cpython-312.pyc
ADDED
|
Binary file (587 Bytes). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/metadata.cpython-312.pyc
ADDED
|
Binary file (6.85 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/optimizer.cpython-312.pyc
ADDED
|
Binary file (15.8 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/planner.cpython-312.pyc
ADDED
|
Binary file (18.7 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/planner_helpers.cpython-312.pyc
ADDED
|
Binary file (16.2 kB). View file
|
|
|
lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/resharding.cpython-312.pyc
ADDED
|
Binary file (2.42 kB). View file
|
|
|