Skip to content

Commit fc780b1

Browse files
committed
feat(pt): add ckpt_keep_ratio to set max_ckpt_keep automatically
1 parent a7c1635 commit fc780b1

6 files changed

Lines changed: 114 additions & 0 deletions

File tree

deepmd/pt/train/training.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
clip_grad_norm_,
7878
latest_checkpoint_path,
7979
resolve_best_checkpoint_dir,
80+
resolve_keep_ckpt_count,
8081
scoped_env_defaults,
8182
)
8283
from deepmd.pt.train.validation import (
@@ -216,6 +217,7 @@ def __init__(
216217
self.save_dir.mkdir(parents=True, exist_ok=True)
217218
self.save_freq = training_params.get("save_freq", 1000)
218219
self.max_ckpt_keep = training_params.get("max_ckpt_keep", 5)
220+
self.ckpt_keep_ratio = training_params.get("ckpt_keep_ratio")
219221
self.enable_ema = bool(training_params.get("enable_ema", False))
220222
self.ema_decay = float(training_params.get("ema_decay", 0.999))
221223
self.ema_ckpt_keep = int(training_params.get("ema_ckpt_keep", 3))
@@ -729,6 +731,24 @@ def get_lr(lr_params: dict[str, Any]) -> BaseLR:
729731
rank=self.rank,
730732
)
731733

734+
# === Derive checkpoint retention from ckpt_keep_ratio ===
735+
# num_steps is final here (including when derived from num_epoch), so the
736+
# ratio can be converted into an absolute keep count once.
737+
keep_ckpt_count = resolve_keep_ckpt_count(
738+
self.ckpt_keep_ratio, self.num_steps, self.save_freq
739+
)
740+
if keep_ckpt_count is not None:
741+
self.max_ckpt_keep = keep_ckpt_count
742+
self.ema_ckpt_keep = keep_ckpt_count
743+
log.info(
744+
"Resolved checkpoint retention to %d from ckpt_keep_ratio=%s "
745+
"(num_steps=%d, save_freq=%d).",
746+
keep_ckpt_count,
747+
self.ckpt_keep_ratio,
748+
self.num_steps,
749+
self.save_freq,
750+
)
751+
732752
# Learning rate
733753
self.gradient_max_norm = training_params.get("gradient_max_norm", 0.0)
734754
self.nonfinite_grad_guard = NonFiniteGradGuard()

deepmd/pt/train/utils.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
from contextlib import (
1010
contextmanager,
1111
)
12+
from math import (
13+
ceil,
14+
)
1215
from pathlib import (
1316
Path,
1417
)
@@ -244,3 +247,38 @@ def resolve_best_checkpoint_dir(
244247
if save_best_dir:
245248
return Path(save_best_dir)
246249
return Path(save_ckpt).parent
250+
251+
252+
def resolve_keep_ckpt_count(
253+
ckpt_keep_ratio: float | None, num_steps: int, save_freq: int
254+
) -> int | None:
255+
"""
256+
Convert a checkpoint-retention ratio into a sliding-window keep count.
257+
258+
Periodic checkpoints are saved every ``save_freq`` steps, so a run of
259+
``num_steps`` produces about ``num_steps // save_freq`` of them. Keeping the
260+
most recent ``ceil(ratio * total)`` of those is equivalent to retaining the
261+
final ``ratio`` fraction of the run by step, without the caller computing
262+
the count by hand.
263+
264+
Parameters
265+
----------
266+
ckpt_keep_ratio : float or None
267+
The fraction of the training run, by step, whose periodic checkpoints
268+
are retained. ``None`` leaves the keep count unchanged.
269+
num_steps : int
270+
The total number of training steps, already resolved (including when
271+
derived from ``numb_epoch``).
272+
save_freq : int
273+
The checkpoint saving frequency in steps.
274+
275+
Returns
276+
-------
277+
int or None
278+
The number of most recent checkpoints to keep (at least one), or
279+
``None`` when ``ckpt_keep_ratio`` is not set.
280+
"""
281+
if ckpt_keep_ratio is None:
282+
return None
283+
total_periodic_ckpts = max(1, num_steps // save_freq)
284+
return max(1, ceil(ckpt_keep_ratio * total_periodic_ckpts))

deepmd/utils/argcheck.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4994,6 +4994,12 @@ def training_args(
49944994
"The oldest checkpoints will be deleted once the number of checkpoints exceeds max_ckpt_keep. "
49954995
"Defaults to 5."
49964996
)
4997+
doc_ckpt_keep_ratio = (
4998+
"An alternative to `max_ckpt_keep` that sets the number of retained "
4999+
"checkpoints as a fraction in (0, 1) of the run: the most recent "
5000+
"`ceil(ckpt_keep_ratio * numb_steps / save_freq)` checkpoints are kept. "
5001+
"When set, it overrides `max_ckpt_keep` and `ema_ckpt_keep`."
5002+
)
49975003
doc_enable_ema = (
49985004
"Whether to maintain an exponential moving average (EMA) of model "
49995005
"parameters during training and save periodic EMA checkpoints with an "
@@ -5129,6 +5135,15 @@ def training_args(
51295135
"save_ckpt", str, optional=True, default="model.ckpt", doc=doc_save_ckpt
51305136
),
51315137
Argument("max_ckpt_keep", int, optional=True, default=5, doc=doc_max_ckpt_keep),
5138+
Argument(
5139+
"ckpt_keep_ratio",
5140+
[float, None],
5141+
optional=True,
5142+
default=None,
5143+
doc=doc_only_pt_supported + doc_ckpt_keep_ratio,
5144+
extra_check=lambda x: x is None or 0.0 < x < 1.0,
5145+
extra_check_errmsg="must be a fraction in the open interval (0, 1)",
5146+
),
51325147
Argument(
51335148
"enable_ema",
51345149
bool,

doc/train/training-advanced.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ Other keys in the {ref}`training <training>` section are explained below:
104104
- {ref}`disp_freq <training/disp_freq>` The frequency of printing learning curve. Set in the unit of training steps
105105
- {ref}`save_freq <training/save_freq>` The frequency of saving checkpoint.
106106
- {ref}`save_dir <training/save_dir>` The directory where periodic checkpoints are written (PyTorch backend). It is created recursively if missing, while the `model.ckpt.pt` symlinks and the `checkpoint` pointer file stay in the working directory. Defaults to the working directory.
107+
- {ref}`ckpt_keep_ratio <training/ckpt_keep_ratio>` An alternative to `max_ckpt_keep` (PyTorch backend) that keeps a sliding window of `ceil(ckpt_keep_ratio * numb_steps / save_freq)` most recent checkpoints, i.e. the final `ckpt_keep_ratio` fraction of the run by step. It overrides `max_ckpt_keep` (and `ema_ckpt_keep`) when set, and works the same whether the run length is given by `numb_steps` or `numb_epoch`.
107108

108109
## Options and environment variables
109110

source/tests/pt/test_train_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from deepmd.pt.train.utils import (
77
NonFiniteGradGuard,
88
clip_grad_norm_,
9+
resolve_keep_ckpt_count,
910
)
1011

1112

@@ -111,5 +112,22 @@ def test_resets_after_check(self) -> None:
111112
guard.raise_if_nonfinite(self._named(1.0))
112113

113114

115+
class TestResolveKeepCkptCount(unittest.TestCase):
116+
def test_none_ratio_leaves_count_unchanged(self) -> None:
117+
self.assertIsNone(resolve_keep_ckpt_count(None, 1000, 10))
118+
119+
def test_ratio_maps_to_recent_window_count(self) -> None:
120+
# 1000 / 10 = 100 periodic checkpoints; 40% keeps the most recent 40.
121+
self.assertEqual(resolve_keep_ckpt_count(0.4, 1000, 10), 40)
122+
123+
def test_ratio_rounds_up(self) -> None:
124+
# 4 periodic checkpoints; ceil(0.4 * 4) = ceil(1.6) = 2.
125+
self.assertEqual(resolve_keep_ckpt_count(0.4, 4, 1), 2)
126+
127+
def test_keeps_at_least_one(self) -> None:
128+
# save_freq larger than num_steps yields a single (final) checkpoint.
129+
self.assertEqual(resolve_keep_ckpt_count(0.4, 5, 100), 1)
130+
131+
114132
if __name__ == "__main__":
115133
unittest.main()

source/tests/pt/test_training.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,28 @@ def tearDown(self) -> None:
11801180
os.chdir(self._cwd)
11811181
self._tmpdir.cleanup()
11821182

1183+
@TRAINING_TEST_TIMEOUT
1184+
def test_ckpt_keep_ratio_overrides_keep_counts(self) -> None:
1185+
config = deepcopy(self.config)
1186+
config["training"]["ckpt_keep_ratio"] = 0.5
1187+
trainer = get_trainer(config)
1188+
# 4 periodic checkpoints; ceil(0.5 * 4) = 2 overrides both the regular
1189+
# and EMA keep counts.
1190+
self.assertEqual(trainer.max_ckpt_keep, 2)
1191+
self.assertEqual(trainer.ema_ckpt_keep, 2)
1192+
save_ckpt = trainer.save_ckpt
1193+
ema_save_ckpt = trainer.ema_save_ckpt
1194+
trainer.run()
1195+
1196+
self.assertEqual(
1197+
sorted(path.name for path in Path(".").glob(f"{save_ckpt}-*.pt")),
1198+
[f"{save_ckpt}-3.pt", f"{save_ckpt}-4.pt"],
1199+
)
1200+
self.assertEqual(
1201+
sorted(path.name for path in Path(".").glob(f"{ema_save_ckpt}-*.pt")),
1202+
[f"{ema_save_ckpt}-3.pt", f"{ema_save_ckpt}-4.pt"],
1203+
)
1204+
11831205
@TRAINING_TEST_TIMEOUT
11841206
def test_save_dir_redirects_checkpoints_with_local_symlinks(self) -> None:
11851207
config = deepcopy(self.config)

0 commit comments

Comments
 (0)