fix: guard tokenizer save with rank 0 to avoid NFS deadlock - #3504
Merged
Conversation
Contributor
Author
|
/ok to test 27de939 |
Contributor
Author
|
@yfw can you help review it? |
dafu-wu
force-pushed
the
fix/tokenizer-save-rank0-guard
branch
2 times, most recently
from
August 6, 2026 04:40
5ce70c4 to
27de939
Compare
Contributor
Author
|
/ok to test 27de939 |
yuki-97
reviewed
Aug 17, 2026
yuki-97
left a comment
Contributor
There was a problem hiding this comment.
@jinglinglingling to review
jinglinglingling
approved these changes
Aug 18, 2026
jinglinglingling
left a comment
Contributor
There was a problem hiding this comment.
LGTM. No concerns from my side.
dafu-wu
force-pushed
the
fix/tokenizer-save-rank0-guard
branch
from
August 18, 2026 17:33
27de939 to
38b62d3
Compare
Contributor
Author
|
@jinglinglingling Thanks for your review! Could you also approve and run the four pending workflows? |
Contributor
Author
|
@jinglinglingling CI quality check is still stuck at Expected — Waiting for status to be reported. All currently triggered checks have completed, but I don't see a CI quality check run under the Checks tab. Could you help trigger it or check whether the required status check is stale? |
Contributor
|
/ok to test 38b62d3 |
When saving a checkpoint, every rank called `tokenizer.save_pretrained()` on the same `tokenizer_path`. Unlike model/optimizer state, the tokenizer is replicated rather than sharded, and `save_pretrained` writes rank-independent filenames, so all ranks raced on the same files. `save_pretrained` is not atomic: it opens with `O_TRUNC`, writes, and may read files back. Concurrent writers therefore contend on the inode lock. On a `hard`-mounted NFS share this deadlocks: one rank blocks in `do_truncate -> nfs_setattr` holding the inode write lock while the others block in `nfs_start_io_read -> down_read`, all in uninterruptible disk sleep. The ranks never return, so the driver waits forever in `ray.get()` and the whole job hangs at checkpoint time with no error. Observed on an 8-node/32-rank DTensor v2 SFT run: 31 ranks finished the save while 4 ranks on one node stayed in `D` state for 35+ hours, all holding `tokenizer_config.json`. Add `save_tokenizer_on_rank0()` and use it from both save paths. This mirrors the rank-0 guard nemo_automodel already applies to the same artifacts in `ConsolidatedHFAddon.pre_save`; passing `tokenizer_path` bypasses that addon, which is how the guard went missing. The guard deliberately covers only the tokenizer. Model and optimizer saves go through `dcp.save`, which is collective and already writes rank-disjoint files, so guarding those would deadlock rank 0 in `all_gather` and drop every non-zero rank's shard. The DTensor v2 value worker reuses `AutomodelCheckpointManager` and is covered by the same change. Signed-off-by: dafu-wu <wuchengyi2006@163.com>
dafu-wu
force-pushed
the
fix/tokenizer-save-rank0-guard
branch
from
August 21, 2026 17:14
38b62d3 to
8f38084
Compare
Contributor
|
/ok to test 8f38084 |
yuki-97
approved these changes
Aug 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do ?
Adds a rank-0 guard around tokenizer saving so that only one process writes the tokenizer directory during checkpointing.
Every rank previously called
tokenizer.save_pretrained()on the sametokenizer_path. Unlike model/optimizer state, the tokenizer is replicated rather than sharded, andsave_pretrainedwrites rank-independent filenames (tokenizer_config.json, ...), so all ranks raced on the same files.save_pretrainedis not atomic: it opens withO_TRUNC, writes, and may read files back. Concurrent writers therefore contend on the inode lock. On ahard-mounted NFS share this deadlocks: one rank blocks indo_truncate -> nfs_setattrholding the inode write lock while the others block innfs_start_io_read -> down_read, all in uninterruptible disk sleep (Dstate). Those ranks never return, so the driver waits forever inray.get()and the whole job hangs at checkpoint time with no error and no timeout.Observed failure
On an 8-node / 32-rank DTensor v2 SFT run (gemma-4-31B-it), training went silent right after
Saving checkpoint for step 500...for 35+ hours. 31 ranks had finished and returned to Ray'smain_loop; 4 ranks on a single node were stuck:Kernel stacks (
/proc/PID/stack) show the writer/reader interlock, all four ontokenizer_config.json:dmesgconfirms:rwsem_down_read_slowpath,blocked for more than 245 seconds.How the guard went missing
nemo_automodelalready guards these artifacts on rank 0 inConsolidatedHFAddon.pre_save. ButAutomodelCheckpointManager.save_checkpointpassestokenizer=tokenizer if tokenizer_path is None else Nonetosave_model(), and callers (e.g.sft.py) always passtokenizer_path. So the addon receivestokenizer=None, its rank-0 branch never runs, and control falls through to a locally implemented, unguardedsave_pretrained().Scope of the guard
Deliberately tokenizer-only. Model and optimizer saves go through
dcp.save, which is collective and already writes rank-disjoint files (shard-000NN-*,__N_0.distcp). Guarding those would deadlock rank 0 inall_gatherand drop every non-zero rank's shard.The DTensor v2 value worker reuses
AutomodelCheckpointManagerand is covered by the same change. The v1 path innative_checkpoint.save_checkpointhad the same bug and is fixed too.Issues
Fixes a silent checkpoint hang on shared NFS storage. No linked issue.
Usage
No API or config change. Existing checkpointing calls are unaffected; only the number of processes writing the tokenizer directory changes (N to 1).
Before your PR is "Ready for review"
dcp.save)Additional Information
Tests added:
tests/unit/utils/test_native_checkpoint.py::TestSaveTokenizerOnRank0covers: non-distributed writes; rank 0 writes and barriers; ranks 1/7/31 skip the write but still barrier;save_checkpoint()keepsdcp.saverunning on a non-zero rank while skipping the tokenizer.tests/unit/models/automodel/test_automodel_checkpoint.py::test_save_with_tokenizer_skipped_on_non_zero_rankpairs with the existing rank-0 test to cover theAutomodelCheckpointManagerpath.Validation performed:
AutomodelCheckpointManageron ranks 0/5/31)ruff checkandruff format --checkNote on local test runs:
uv runcurrently fails to resolve dependencies in my environment for a reason unrelated to this change (nemo-rl[mcore]needsmegatron-bridge, which pinstransformers<=5.3.0, whilenemo-rlrequires>=5.5.0). The tests above were therefore executed against the real modules with a minimal stub for the optionaltransformersimport. Please run the full suite in CI.