ZhengyangZhang commited on
Commit
156f6c0
·
verified ·
1 Parent(s): 116f62a

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. lib/python3.12/site-packages/torch/distributed/__pycache__/_checkpointable.cpython-312.pyc +0 -0
  2. lib/python3.12/site-packages/torch/distributed/__pycache__/_composable_state.cpython-312.pyc +0 -0
  3. lib/python3.12/site-packages/torch/distributed/__pycache__/_functional_collectives.cpython-312.pyc +0 -0
  4. lib/python3.12/site-packages/torch/distributed/__pycache__/_functional_collectives_impl.cpython-312.pyc +0 -0
  5. lib/python3.12/site-packages/torch/distributed/__pycache__/c10d_logger.cpython-312.pyc +0 -0
  6. lib/python3.12/site-packages/torch/distributed/__pycache__/collective_utils.cpython-312.pyc +0 -0
  7. lib/python3.12/site-packages/torch/distributed/__pycache__/constants.cpython-312.pyc +0 -0
  8. lib/python3.12/site-packages/torch/distributed/__pycache__/device_mesh.cpython-312.pyc +0 -0
  9. lib/python3.12/site-packages/torch/distributed/__pycache__/rendezvous.cpython-312.pyc +0 -0
  10. lib/python3.12/site-packages/torch/distributed/_sharding_spec/__init__.py +22 -0
  11. lib/python3.12/site-packages/torch/distributed/_sharding_spec/__pycache__/__init__.cpython-312.pyc +0 -0
  12. lib/python3.12/site-packages/torch/distributed/_tools/__init__.py +12 -0
  13. lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/__init__.cpython-312.pyc +0 -0
  14. lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/fsdp2_mem_tracker.cpython-312.pyc +0 -0
  15. lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/ilp_utils.cpython-312.pyc +0 -0
  16. lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/mem_tracker.cpython-312.pyc +0 -0
  17. lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/memory_tracker.cpython-312.pyc +0 -0
  18. lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/mod_tracker.cpython-312.pyc +0 -0
  19. lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/runtime_estimator.cpython-312.pyc +0 -0
  20. lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/sac_estimator.cpython-312.pyc +0 -0
  21. lib/python3.12/site-packages/torch/distributed/_tools/__pycache__/sac_ilp.cpython-312.pyc +0 -0
  22. lib/python3.12/site-packages/torch/distributed/_tools/fsdp2_mem_tracker.py +610 -0
  23. lib/python3.12/site-packages/torch/distributed/_tools/ilp_utils.py +291 -0
  24. lib/python3.12/site-packages/torch/distributed/_tools/mem_tracker.py +943 -0
  25. lib/python3.12/site-packages/torch/distributed/_tools/memory_tracker.py +295 -0
  26. lib/python3.12/site-packages/torch/distributed/_tools/mod_tracker.py +238 -0
  27. lib/python3.12/site-packages/torch/distributed/_tools/runtime_estimator.py +527 -0
  28. lib/python3.12/site-packages/torch/distributed/_tools/sac_estimator.py +997 -0
  29. lib/python3.12/site-packages/torch/distributed/_tools/sac_ilp.py +295 -0
  30. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/__init__.cpython-312.pyc +0 -0
  31. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_checkpointer.cpython-312.pyc +0 -0
  32. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_dedup_save_plans.cpython-312.pyc +0 -0
  33. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_dedup_tensors.cpython-312.pyc +0 -0
  34. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_fsspec_filesystem.cpython-312.pyc +0 -0
  35. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_nested_dict.cpython-312.pyc +0 -0
  36. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_sharded_tensor_utils.cpython-312.pyc +0 -0
  37. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_storage_utils.cpython-312.pyc +0 -0
  38. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_traverse.cpython-312.pyc +0 -0
  39. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/_version.cpython-312.pyc +0 -0
  40. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/api.cpython-312.pyc +0 -0
  41. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/default_planner.cpython-312.pyc +0 -0
  42. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/filesystem.cpython-312.pyc +0 -0
  43. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/format_utils.cpython-312.pyc +0 -0
  44. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/logger.cpython-312.pyc +0 -0
  45. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/logging_handlers.cpython-312.pyc +0 -0
  46. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/metadata.cpython-312.pyc +0 -0
  47. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/optimizer.cpython-312.pyc +0 -0
  48. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/planner.cpython-312.pyc +0 -0
  49. lib/python3.12/site-packages/torch/distributed/checkpoint/__pycache__/planner_helpers.cpython-312.pyc +0 -0
  50. 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