Skip to content

fix(safetensors): drop the previous export when re-saving a checkpoint - #258

Open
li-lizhe wants to merge 1 commit into
modelscope:mainfrom
li-lizhe:fix/stale-shards-on-resave
Open

li-lizhe wants to merge 1 commit into
modelscope:mainfrom
li-lizhe:fix/stale-shards-on-resave

Conversation

@li-lizhe

@li-lizhe li-lizhe commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

What

StreamingSafetensorSaver now removes the files of an earlier export in the same directory when they are not part of the new one, and renames shards with os.replace.

Fixes #257.

Why

finalize() writes the shards it just produced and never looks at what a previous export left behind, so re-saving into the same output directory kept stale files:

  • a single-file re-export kept model.safetensors.index.json, and SafetensorLazyLoader reads the index in preference to model.safetensors - reloading the directory returned the old tensors under the same keys, with no error;
  • a smaller shard count kept the surplus shard files of the larger export;
  • os.rename raised FileExistsError when the target name of the previous export still existed, which aborted the save.

How

__init__ records the model files present when the saver starts (model*.safetensors plus model.safetensors.index.json). finalize() knows the exact set the new export produces, deletes the difference before the renames so the rename targets are free, and uses os.replace for the rename itself. PEFT exports are untouched: they write adapter_model.safetensors and leave the model files alone.

self._previous_export_files = set(Path(save_dir).glob('model*.safetensors'))

Tests

tests/test_safetensors_shard_cleanup.py covers four cases:

case previous code this PR
single-file re-export after a 2-shard export (dir contents + reloaded values) fail pass
2-shard re-export after a 4-shard export (surplus shards removed) fail pass
exporting twice into the same directory (files just written are kept) fail pass
PEFT export next to model weights (model files kept) pass pass

Run against the module source with a stub mcore_bridge.utils.env (the package __init__ needs megatron and peft), so the sharding and cleanup logic under test is the real code. flake8 with the repo's setup.cfg selection, isort and yapf are clean on both files.

One caveat on the environment: the shard filename contains a literal ????? placeholder, which cannot be created on Windows, so the multi-shard cases were exercised with that placeholder rewritten to a legal token in the module copy - the rename and cleanup paths themselves are unchanged. Full pytest on Linux was not run; pre-commit and CI were not run either.

`StreamingSafetensorSaver` writes every shard under a temporary
`model-NNNNN-of-?????.safetensors` name and only renames them in `finalize()`.
Nothing removed the files an earlier export left in the same directory, so a
second export that needs fewer shards inherited the old ones:

* a 2-shard export followed by a single-file export kept
  `model.safetensors.index.json`, and `SafetensorLazyLoader` prefers the index
  over `model.safetensors`, so reloading the directory returned the previous
  weights under the same keys;
* a 4-shard export followed by a 2-shard export kept the old shard files;
* `os.rename` also raises `FileExistsError` when the target shard of the
  previous export is still there, which aborted the save.

Record the model files present when the saver starts and delete the ones the
new export does not produce, before the renames so the targets are free.
`os.replace` keeps the rename working if a file appears in between. PEFT
exports (`adapter_model.safetensors`) keep their files untouched.

Tests cover the single-file case, the fewer-shards case, an export repeated
twice and the PEFT path; the first three fail on the previous code.

Signed-off-by: li-lizhe <147392333@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StreamingSafetensorSaver keeps the files of a previous export, so reloading returns the old weights

1 participant