Run "on_stage_end" on all processes and save on only a single process - #2059
Conversation
|
This seems better to me. But in the current implementation any registered save hooks have expected to be called on the main process. I don't know if Checkpointer should wrap saving in Can saving still only run on the main process under FSDP, or do the other processes need to explicitly do something? Also, the intra-epoch checkpoint saving as currently implemented (based on time) is not compatible with this: speechbrain/speechbrain/core.py Lines 1130 to 1144 in 5d479fc This makes me realise that if we put run_on_main somewhere, then all processes must hit that code, so if the run_on_main goes into classes, we must ensure that all processes run that (and in the same order, i.e. if one process runs into save barrier and another into gradient sync barrier then it ends up in deadlock). Maybe we don't need the barrier inside run_on_main in many cases? There is already the if_main_process() (which really should be called is_main_process()), that might be enough in most cases. The other processes will wait at the next barrier, while main process saves or does whatever errands we need.
Note: Checkpointer should also wrap the checkpoint deletion in |
You've correctly deduced the reason why I put this in individual classes -- some (including FSDP) may want to run code on all processes. Perhaps a more backwards-compatible way of doing this would be to add some sort of argument (to
Perhaps by default we should call |
|
Continuing on the checkpoints with time intervals: basically, time cannot be relied on for multi-process synchronisation (almost by definition). If FSDP needs Checkpointer code to run on all nodes then I think that this checkpoint with time interval is not possible with FSDP. With the coming changes in SpeechBrain, I think we can afford to break some backwards compatibility and move to checkpoints every x steps. Some people also requested that anyway. |
|
I think the mark_as_saver decorator could indeed have an optional parameter (something like |
… process" This reverts most of commit 5d479fc. The only remaining changes are to "core.py"
Should that be done as part of this PR or could it be a separate one?
Implementation attempted here -- does this look reasonable? |
|
I think the two separated sets of default save hooks (main proc only vs. all procs) will lead to many errors. When searching for a default hook, the code looks through the class hierarchy, where it will often find e.g. torch.Module, and use that default hook. While we could change the order so that all_procs hooks are prioritized (as they are less common), I fear that this will lead to similar weird and hard to find bugs. I think we'd want to keep one set of save hooks. Perhaps the checkpoint registering could wrap the saver hook callable in a wrapper that only runs on the main process: from functools import wraps
def main_process_only(func):
@wraps(func)
def wrapper(*args, **kwargs):
if sb.utils.distributed.if_main_process():
func(*args, **kwargs)
return wrapperOr we could keep more information about those hooks, e.g. the hooks dict could map types to some Hook dataclass with entries like
It's something that breaks from this, so it could be done as part of this change or if we want to be tidy it could be a separate change implemented before this. |
This is a neat solution, I wish I'd thought of it first!
Okay, I've made the change in I was thinking of assuming there's 5 iterations per second (a tremendously inaccurate assumption) and converting the minutes to steps (* 60 * 5), but there's probably a better way to do this. |
Oof. Well, one idea is to keep the time based checkpointing, it is kind of useful and works in most cases (and now that you added step based checkpoints, keep those as well :D). We could also try to detect FSDP and raise an error, though I guess at the moment we cannot easily detect FSDP. Maybe a hack for now, something like checking if the name "FSDP" is in globals, I don't know. |
|
Otherwise, I think this could be a good implementation. You need to remember to wrap torch_recovery with main_process_only, and same for checkpoint deletion. Also, I wonder if many recipes do things like write some WER file in an on_stage_end, expecting this to happen only on main? |
Ah yes, I've added this for checkpoint deletion. However, for
These should be fixed: $ grep -r 'with open(self.hparams.wer_file' recipes/ | wc -l
33But there are some cases of saving that don't necessarily need to be fixed: $ grep -r 'torchaudio.save(' recipes/ | wc -l
63Looking at these, they mostly happen in places other than |
Oops yea sorry, I meant torch_save |
Gastron
left a comment
There was a problem hiding this comment.
Other than two very minor things I think this is ready to merge!
Co-authored-by: Aku Rouhe <akurouhe@hotmail.com>
Gastron
left a comment
There was a problem hiding this comment.
Alright thanks for the persistent work! Everything looks good to me now.
|
Side note: I was going to nitpick about using sys.exit to crash Brain in case of wrong arguments, but I noticed that this is done for some other options as well and you were using the same convention. I propose a separate PR to change all those to raising an |
|
There is an issue with this PR, as reported by @Adel-Moumen: "I trained a model with 3 GPUs and basically, I got [three] save folders" and "they all have: except one with the right |
Alternative proposal to #2053 .
In this, we just run
on_stage_endon all processes all the time, and take care of running things on a single process at the most granular level.There's likely still areas that need to be fixed here, like logging or something like this.
Does this seem better or worse to you @Gastron @Adel-Moumen or any others?