forked from hao-ai-lab/FastVideo
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathema.py
More file actions
159 lines (132 loc) · 4.8 KB
/
Copy pathema.py
File metadata and controls
159 lines (132 loc) · 4.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# SPDX-License-Identifier: Apache-2.0
"""EMA (Exponential Moving Average) callback.
Owns the full EMA lifecycle: creation, per-step updates, weight
swapping for validation, and checkpoint state. All EMA config
lives under ``callbacks.ema`` in the YAML file.
"""
from __future__ import annotations
import contextlib
from collections.abc import Generator
from typing import Any, TYPE_CHECKING
import torch
from fastvideo.logger import init_logger
from fastvideo.train.callbacks.callback import Callback
from fastvideo.training.training_utils import EMA_FSDP
if TYPE_CHECKING:
from fastvideo.train.methods.base import TrainingMethod
logger = init_logger(__name__)
class EMACallback(Callback):
"""Manage EMA shadow weights for the student transformer.
All configuration lives in the YAML ``callbacks.ema`` section:
.. code-block:: yaml
callbacks:
ema:
decay: 0.9999
start_iter: 0
The callback creates an ``EMA_FSDP`` instance at train start,
updates it after each optimizer step, and exposes an
``ema_context()`` context manager for temporarily swapping
EMA weights into the live model (used by validation).
"""
def __init__(
self,
*,
decay: float = 0.9999,
start_iter: int = 0,
update_interval: int = 1,
) -> None:
self._decay = float(decay)
self._start_iter = int(start_iter)
self._update_interval = max(1, int(update_interval))
self._ema_started = False
self.student_ema: EMA_FSDP | None = None
# ----------------------------------------------------------
# Hooks
# ----------------------------------------------------------
def on_train_start(
self,
method: TrainingMethod,
iteration: int = 0,
) -> None:
student = getattr(method, "student", None)
if student is None or student.transformer is None:
raise ValueError("No student transformer found on method, cannot initialize EMA")
logger.info(
"Initializing EMA (local_shard) with "
"decay=%s from student transformer",
self._decay,
)
self.student_ema = EMA_FSDP(
student.transformer,
decay=self._decay,
mode="local_shard",
)
logger.info(
"EMA callback enabled (decay=%s, "
"start_iter=%d, update_interval=%d).",
self._decay,
self._start_iter,
self._update_interval,
)
def on_training_step_end(
self,
method: TrainingMethod,
loss_dict: dict[str, Any],
iteration: int = 0,
) -> None:
if self.student_ema is None:
return
if iteration < self._start_iter:
return
if (iteration - self._start_iter) % self._update_interval != 0:
return
if not self._ema_started:
logger.info(
"Starting EMA updates at iteration %d "
"(re-initializing shadow from current "
"model).",
iteration,
)
self.student_ema._init_shadow(method.student.transformer, )
self._ema_started = True
self.student_ema.update(method.student.transformer, )
tracker = getattr(method, "tracker", None)
if tracker is not None:
tracker.log(
{"ema/decay": self.student_ema.decay},
iteration,
)
# ----------------------------------------------------------
# EMA context manager
# ----------------------------------------------------------
@contextlib.contextmanager
def ema_context(
self,
transformer: torch.nn.Module,
) -> Generator[torch.nn.Module, None, None]:
"""Temporarily swap EMA weights into *transformer*.
If EMA is not active, yields the transformer unchanged.
"""
if (self.student_ema is not None and self._ema_started):
with self.student_ema.apply_to_model(transformer, ):
yield transformer
else:
yield transformer
# ----------------------------------------------------------
# Checkpoint state
# ----------------------------------------------------------
def state_dict(self) -> dict[str, Any]:
if self.student_ema is None:
return {}
return {
"student_ema": self.student_ema.state_dict(),
"ema_started": self._ema_started,
}
def load_state_dict(
self,
state_dict: dict[str, Any],
) -> None:
ema_state = state_dict.get("student_ema")
if (ema_state is not None and self.student_ema is not None):
self.student_ema.load_state_dict(ema_state)
self._ema_started = bool(state_dict.get("ema_started", False), )