Skip to content

Commit c937a32

Browse files
authored
Merge pull request #394 from VectorInstitute/ft/state-checkpointing-refactor
State checkpointer refactor
2 parents 2444580 + 24581fb commit c937a32

28 files changed

Lines changed: 2330 additions & 1487 deletions

examples/nnunet_example/client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from os.path import exists, join
66
from pathlib import Path
77

8-
from fl4health.checkpointing.checkpointer import PerRoundStateCheckpointer
98
from fl4health.checkpointing.client_module import ClientCheckpointAndStateModule
9+
from fl4health.checkpointing.state_checkpointer import ClientStateCheckpointer
1010

1111
with warnings.catch_warnings():
1212
# Silence deprecation warnings from sentry sdk due to flwr and wandb
@@ -73,7 +73,7 @@ def main(
7373

7474
if intermediate_client_state_dir is not None:
7575
checkpoint_and_state_module = ClientCheckpointAndStateModule(
76-
state_checkpointer=PerRoundStateCheckpointer(Path(intermediate_client_state_dir))
76+
state_checkpointer=ClientStateCheckpointer(Path(intermediate_client_state_dir))
7777
)
7878
else:
7979
checkpoint_and_state_module = None
@@ -147,7 +147,7 @@ def main(
147147
even if the preprocessed data is found to already exist",
148148
)
149149
parser.add_argument(
150-
"--server_address",
150+
"--server-address",
151151
type=str,
152152
required=False,
153153
default="0.0.0.0:8080",

examples/nnunet_example/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from flwr.server.client_manager import SimpleClientManager
1313
from flwr.server.strategy import FedAvg
1414

15-
from fl4health.checkpointing.checkpointer import PerRoundStateCheckpointer
1615
from fl4health.checkpointing.server_module import NnUnetServerCheckpointAndStateModule
16+
from fl4health.checkpointing.state_checkpointer import NnUnetServerStateCheckpointer
1717
from fl4health.metrics.metric_aggregation import evaluate_metrics_aggregation_fn, fit_metrics_aggregation_fn
1818
from fl4health.parameter_exchange.full_exchanger import FullParameterExchanger
1919
from fl4health.servers.nnunet_server import NnunetServer
@@ -85,7 +85,7 @@ def main(
8585
)
8686

8787
state_checkpointer = (
88-
PerRoundStateCheckpointer(Path(intermediate_server_state_dir))
88+
NnUnetServerStateCheckpointer(Path(intermediate_server_state_dir))
8989
if intermediate_server_state_dir is not None
9090
else None
9191
)
@@ -132,7 +132,7 @@ def main(
132132
)
133133

134134
parser.add_argument(
135-
"--intermediate-server-state-dir",
135+
"--intermediate-server-state_dir",
136136
type=str,
137137
required=False,
138138
default=None,

fl4health/checkpointing/checkpointer.py

Lines changed: 0 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
from abc import ABC, abstractmethod
33
from collections.abc import Callable
44
from logging import ERROR, INFO, WARNING
5-
from pathlib import Path
6-
from typing import Any
75

86
import torch
97
import torch.nn as nn
@@ -276,7 +274,6 @@ def __init__(
276274
maximize: bool = False,
277275
) -> None:
278276
"""Checkpointer that checkpoints based on the value of a user defined metric.
279-
280277
Args:
281278
checkpoint_dir (str): Directory to which the model is saved. This directory should already exist. The
282279
checkpointer will not create it if it does not.
@@ -310,83 +307,3 @@ def metric_score_function(_: float, metrics: dict[str, Scalar]) -> float:
310307
checkpoint_score_name=metric,
311308
maximize=maximize,
312309
)
313-
314-
315-
class PerRoundStateCheckpointer:
316-
def __init__(self, checkpoint_dir: Path) -> None:
317-
"""
318-
Base class that provides a uniform interface for loading, saving and checking if checkpoints exists.
319-
320-
Args:
321-
checkpoint_dir (Path): Base directory to store checkpoints. This checkpoint directory MUST already exist.
322-
It will not be created by this state checkpointer.
323-
"""
324-
log(
325-
WARNING,
326-
"Creating PerRoundCheckpointer. Currently, this functionality is still experimental and only supported "
327-
"for BasicClient and NnunetClient, along with their associated servers.",
328-
)
329-
self.checkpoint_dir = checkpoint_dir
330-
331-
def save_checkpoint(self, checkpoint_name: str, checkpoint_dict: dict[str, Any]) -> None:
332-
"""
333-
Saves ``checkpoint_dict`` to checkpoint path form from this classes checkpointer dir and the provided
334-
checkpoint name.
335-
336-
Args:
337-
checkpoint_name (str): Name of the state checkpoint file.
338-
checkpoint_dict (dict[str, Any]): A dictionary with string keys and values of type Any representing the
339-
state to checkpoint.
340-
341-
Raises:
342-
e: Will throw an error if there is an issue saving the model. ``Torch.save`` seems to swallow errors in
343-
this context, so we explicitly surface the error with a try/except.
344-
"""
345-
346-
checkpoint_path = os.path.join(self.checkpoint_dir, checkpoint_name)
347-
try:
348-
log(INFO, f"Saving state as {checkpoint_path}")
349-
torch.save(checkpoint_dict, checkpoint_path)
350-
except Exception as e:
351-
log(ERROR, f"Encountered the following error while saving the checkpoint: {e}")
352-
raise e
353-
354-
def load_checkpoint(self, checkpoint_name: str) -> dict[str, Any]:
355-
"""
356-
Loads and returns the checkpoint stored in ``checkpoint_dir`` under the provided name if it exists. If it
357-
does not exist, an assertion error will be thrown.
358-
359-
Args:
360-
checkpoint_name (str): Name of the state checkpoint to be loaded.
361-
362-
Returns:
363-
dict[str, Any]: A dictionary representing the checkpointed state, as loaded by ``torch.load``.
364-
"""
365-
366-
assert self.checkpoint_exists(checkpoint_name)
367-
checkpoint_path = os.path.join(self.checkpoint_dir, checkpoint_name)
368-
log(INFO, f"Loading state from checkpoint at {checkpoint_path}")
369-
370-
return torch.load(checkpoint_path)
371-
372-
def checkpoint_exists(self, checkpoint_name: str, **kwargs: Any) -> bool:
373-
"""
374-
Checks if a checkpoint exists at the ``checkpoint_dir`` constructed at initialization + ``checkpoint_name``.
375-
376-
Args:
377-
checkpoint_name (str): Name of checkpoint for existence test. Directory of checkpoint is held internally
378-
as state by the checkpointer
379-
380-
Raises:
381-
ValueError: Previously this function supported sending a path, but now requires ``checkpoint_name``. Will
382-
raise an error is ``checkpoint_path`` provided.
383-
384-
Returns:
385-
bool: True if checkpoint exists, otherwise false.
386-
"""
387-
if "checkpoint_path" in kwargs:
388-
raise ValueError(
389-
"Previously this checkpoint supported sending a path, but it now requires a checkpoint_name only"
390-
)
391-
checkpoint_path = os.path.join(self.checkpoint_dir, checkpoint_name)
392-
return os.path.exists(checkpoint_path)

fl4health/checkpointing/client_module.py

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1+
from __future__ import annotations
2+
13
from collections.abc import Sequence
24
from enum import Enum
35
from logging import INFO
4-
from typing import Any
6+
from typing import TYPE_CHECKING
57

68
import torch.nn as nn
79
from flwr.common.logger import log
810
from flwr.common.typing import Scalar
911

10-
from fl4health.checkpointing.checkpointer import PerRoundStateCheckpointer, TorchModuleCheckpointer
12+
if TYPE_CHECKING:
13+
from fl4health.clients.basic_client import BasicClient
14+
15+
from fl4health.checkpointing.checkpointer import TorchModuleCheckpointer
16+
from fl4health.checkpointing.state_checkpointer import ClientStateCheckpointer
1117

1218
ModelCheckpointers = TorchModuleCheckpointer | Sequence[TorchModuleCheckpointer] | None
1319

@@ -18,11 +24,12 @@ class CheckpointMode(Enum):
1824

1925

2026
class ClientCheckpointAndStateModule:
27+
2128
def __init__(
2229
self,
2330
pre_aggregation: ModelCheckpointers = None,
2431
post_aggregation: ModelCheckpointers = None,
25-
state_checkpointer: PerRoundStateCheckpointer | None = None,
32+
state_checkpointer: ClientStateCheckpointer | None = None,
2633
) -> None:
2734
"""
2835
This module is meant to hold up three to major components that determine how clients handle model and state
@@ -44,9 +51,9 @@ def __init__(
4451
post_aggregation (ModelCheckpointers, optional): If defined, this checkpointer (or sequence
4552
of checkpointers) is used to checkpoint models based on their validation metrics/losses **AFTER**
4653
server-side aggregation. Defaults to None.
47-
state_checkpointer (PerRoundStateCheckpointer | None, optional): If defined, this checkpointer is used to
48-
preserve client state (not just models), in the event one wants to restart federated training.
49-
Defaults to None.
54+
state_checkpointer (ClientStateCheckpointer | None, optional): If defined, this checkpointer
55+
is used to preserve client state (not just models), in the event one wants to restart
56+
federated training. Defaults to None.
5057
"""
5158
self.pre_aggregation = (
5259
[pre_aggregation] if isinstance(pre_aggregation, TorchModuleCheckpointer) else pre_aggregation
@@ -119,52 +126,42 @@ def maybe_checkpoint(
119126
else:
120127
raise ValueError(f"Unrecognized mode for checkpointing: {str(mode)}")
121128

122-
def save_state(self, state_checkpoint_name: str, state: dict[str, Any]) -> None:
129+
def save_state(self, client: BasicClient) -> None:
123130
"""
124131
This function is meant to facilitate saving state required to restart an FL process on the client side. This
125-
function will simply save whatever information is passed in the state variable using the file name in
126-
``state_checkpoint_name``. This function should only be called if a ``state_checkpointer`` exists in this
127-
module
132+
function will simply save all the attributes stated in ``ClientStateCheckpointer.snapshot_attrs``.
133+
This function should only be called if a ``state_checkpointer`` exists in this module.
128134
129135
Args:
130-
state_checkpoint_name (str): Name of the state checkpoint file. The checkpointer itself will have a
131-
directory to which state will be saved.
132-
state (dict[str, Any]): State to be saved so that training might be resumed on the client if federated
133-
training is interrupted. For example, this might contain things like optimizer states, learning rate
134-
scheduler states, etc.
136+
client (BasicClient): The client object from which state will be saved.
135137
136138
Raises:
137139
ValueError: Throws an error if this function is called, but no state checkpointer has been provided
138140
"""
139141

140142
if self.state_checkpointer is not None:
141-
self.state_checkpointer.save_checkpoint(state_checkpoint_name, state)
143+
self.state_checkpointer.save_client_state(client)
142144
else:
143145
raise ValueError("Attempting to save state but no state checkpointer is specified")
144146

145-
def maybe_load_state(self, state_checkpoint_name: str) -> dict[str, Any] | None:
147+
def maybe_load_state(self, client: BasicClient) -> bool:
146148
"""
147-
This function facilitates loading of any pre-existing state (with the name ``state_checkpoint_name``) in the
148-
directory of the ``state_checkpointer``. If the state already exists at the proper path, the state is loaded
149-
and returned. If it doesn't exist, we return None.
149+
This function facilitates loading of any pre-existing state (with the name ``checkpoint_name``) in the
150+
directory of the ``checkpoint_dir``. If the state already exists at the proper path, the state is loaded
151+
and will be automatically saved into client's attributes. If it doesn't exist, we return False.
150152
151153
Args:
152-
state_checkpoint_name (str): Name of the state checkpoint file. The checkpointer itself will have a
153-
directory from which state will be loaded (if it exists).
154+
client (BasicClient): client object into which state will be loaded if a checkpoint exists
154155
155156
Raises:
156157
ValueError: Throws an error if this function is called, but no state checkpointer has been provided
157158
158159
Returns:
159-
dict[str, Any] | None: If the state checkpoint properly exists and is loaded correctly, this dictionary
160-
carries that state. Otherwise, we return a None (or throw an exception).
160+
bool : If the state checkpoint properly exists and is loaded correctly, client's attributes
161+
are set to the loaded values, and True is returned. Otherwise, we return False (or throw an exception).
161162
"""
162163

163164
if self.state_checkpointer is not None:
164-
if self.state_checkpointer.checkpoint_exists(state_checkpoint_name):
165-
return self.state_checkpointer.load_checkpoint(state_checkpoint_name)
166-
else:
167-
log(INFO, "State checkpointer is defined but no state checkpoint exists.")
168-
return None
165+
return self.state_checkpointer.maybe_load_client_state(client)
169166
else:
170167
raise ValueError("Attempting to load state, but no state checkpointer is specified")

0 commit comments

Comments
 (0)