44from torch .distributed .tensor import DTensor
55
66from d9d .core .dist_context import DistributedContext
7+ from d9d .core .offload import Offloadable , OffloadContext , OnloadContext
78from d9d .internals .grad_sync import GradientSynchronizer
89from d9d .loop .config import GradientManagerConfig
910from d9d .metric .impl .aggregation import WeightedMeanMetric
1213from .model_stage_factory import TrackedModules
1314
1415
15- class GradientManager :
16+ class GradientManager ( Offloadable ) :
1617 """
1718 Manages the lifecycle of gradients during the training loop.
1819
@@ -53,6 +54,10 @@ def __init__(
5354 )
5455 self ._grads_to_scale : list [torch .Tensor ] | None = None
5556
57+ self ._installed = False
58+ self ._offloaded = False
59+ self ._in_flight_count = 0
60+
5661 def _setup_grad_dtype (self ):
5762 if self ._config .grad_dtype is None :
5863 return
@@ -85,6 +90,15 @@ def _scale_grads(self):
8590 if len (self ._grads_to_scale ) > 0 :
8691 torch ._foreach_mul_ (self ._grads_to_scale , scale_factor )
8792
93+ def _bind (self ):
94+ self ._setup_grad_dtype ()
95+ self ._grad_sync .bind ()
96+ self ._bind_grads_to_scale ()
97+
98+ def _unbind (self ):
99+ self ._unbind_grads_to_scale ()
100+ self ._grad_sync .unbind ()
101+
88102 @contextmanager
89103 def install (self ):
90104 """
@@ -95,12 +109,11 @@ def install(self):
95109 as the boundary for the accumulation phase.
96110 """
97111
98- self ._setup_grad_dtype ()
99- self ._grad_sync .bind ()
100- self ._bind_grads_to_scale ()
112+ self ._bind ()
113+ self ._installed = True
101114 yield
102- self ._unbind_grads_to_scale ()
103- self ._grad_sync . unbind ()
115+ self ._installed = False
116+ self ._unbind ()
104117
105118 def add_loss_with_weight (self , loss : torch .Tensor , loss_weight : torch .Tensor ):
106119 """
@@ -112,6 +125,7 @@ def add_loss_with_weight(self, loss: torch.Tensor, loss_weight: torch.Tensor):
112125 """
113126
114127 self ._loss .update (loss , loss_weight )
128+ self ._in_flight_count += 1
115129
116130 def sync_and_scale (self ):
117131 """
@@ -151,3 +165,62 @@ def zero_grad(self):
151165
152166 self ._grad_sync .zero_grad ()
153167 self ._loss .reset ()
168+ self ._in_flight_count = 0
169+
170+ @property
171+ def has_in_flight_gradients (self ) -> bool :
172+ """
173+ Checks whether a gradient accumulation is currently in flight.
174+
175+ The counter is raised by "add_loss_with_weight" and reset by "zero_grad". While it is
176+ non-zero, partial accumulation state lives in the synchronizer buckets, so offloading
177+ the gradient state would lose it.
178+ """
179+
180+ return self ._in_flight_count > 0
181+
182+ def offload (self , ctx : OffloadContext ) -> None :
183+ """
184+ Releases the GPU memory of the gradient state to host memory.
185+
186+ The synchronizer bucket buffers are released and the residual loss accumulator is reset.
187+
188+ Args:
189+ ctx: Context for this operation.
190+
191+ Raises:
192+ RuntimeError: If the gradient state is already offloaded, or if the manager has
193+ not been installed.
194+ """
195+
196+ if self ._offloaded :
197+ raise RuntimeError ("GradientManager is already offloaded." )
198+ if not self ._installed :
199+ raise RuntimeError ("GradientManager must be installed before it can be offloaded." )
200+
201+ self ._unbind ()
202+ self ._loss .reset ()
203+ self ._offloaded = True
204+
205+ def onload (self , ctx : OnloadContext ) -> None :
206+ """
207+ Restores GPU residency of the gradient state released by "offload".
208+
209+ Args:
210+ ctx: Context for this operation.
211+
212+ Raises:
213+ RuntimeError: If the gradient state is not offloaded.
214+ """
215+
216+ if not self ._offloaded :
217+ raise RuntimeError ("GradientManager is not offloaded." )
218+
219+ if self ._installed :
220+ self ._bind ()
221+
222+ self ._offloaded = False
223+
224+ def is_offloaded (self ) -> bool :
225+ """Reports whether the gradient state is currently on host memory."""
226+ return self ._offloaded
0 commit comments