Skip to content

fix(model): preserve seq-cls metadata across save round trips - #10228

Open
Excelius-Wang wants to merge 5 commits into
modelscope:mainfrom
Excelius-Wang:fix/seq-cls-save-roundtrip
Open

Excelius-Wang wants to merge 5 commits into
modelscope:mainfrom
Excelius-Wang:fix/seq-cls-save-roundtrip

Conversation

@Excelius-Wang

@Excelius-Wang Excelius-Wang commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

PR type

  • Bug Fix

PR information

A patched sequence classifier can still save generation metadata after a non-main save, or after a pickle round trip to a fresh interpreter. This follow-up preserves classifier metadata in those cases and restores it when saving fails before config serialization.

Dependency: #10063. This branch retains RerankerGuo's three original commits through c121140 unchanged, followed by the incremental save fix and its test compatibility follow-up. Please review the follow-up commits separately while #10063 is pending; this follow-up is not intended to replace that PR or duplicate its credit. I can rebase onto main once the dependency lands.

Reproduced cases at c121140

  • model.save_pretrained(first_dir, is_main_process=False) followed by an ordinary save writes Qwen2ForCausalLM into the second config. Transformers resets architectures but the non-main call skips the restoring config callback.
  • Pickle to a fresh interpreter, unpickle, then save: the instance marker survives but the class hook does not. The existing same-process pickle test cannot detect this.
  • A custom-code export failure after the architecture reset but before config saving leaves the in-memory architecture changed. The new regression checks cleanup and a successful retry.

Bind a module-level wrapper to the model instance with functools.partial, retaining the original save implementation. Restore metadata before config serialization and in finally. Normalize bound methods before retaining them so pickle cannot resolve the original method name to the replacement wrapper. This uses ordinary Python serialization and does not modify the model class or reimplement checkpoint writing.

Five added tests cover non-main then main saving, fresh-process pickle and saved score weights, failures during/before config saving, and an existing save wrapper with repeated installation. Existing deepcopy, custom architecture, missing architecture and unrelated-instance tests remain covered.

Experiment results

  • Before: non-main and fresh-process pickle regressions fail on c121140; the permanent pre-config export-failure test also fails against that source.
  • After: 14 tests and 3 subtests pass on Transformers 4.57.6, 5.12.1 and 5.16.1, with two unrelated model-download tests deselected:
    python -m pytest tests/general/test_model.py -q -k 'not test_qwen2 and not test_modelscope_hub'.
  • pre-commit run --all-files: all 10 hooks passed. git diff --check passed.
  • Additional CPU checks: tiny-model Trainer training/save/reload/continued training, Swift trainer save path, tiny Qwen3VL 20-label save, PEFT merge, sharded checkpoint reload, explicit state_dict, and nested save-wrapper pickle/deepcopy.

These are save-API edge cases. The training smoke deliberately injects the non-main call and pickle transport; it does not establish that ordinary Swift/DDP saving naturally triggers them. GPU/BF16 training, FSDP/DeepSpeed, full serialized-model forward semantics, and vLLM loading/inference are not validated. This follow-up does not claim to resolve the complete 20-label vLLM deployment problem in #9704.

The first NPU CI run used Transformers 5.16.1 and exposed a test setup issue: is_remote_code() reads the class _auto_class, while the test set the instance attribute. The follow-up patches the class attribute in a restoring context. The original failure is reproduced on CPU with 5.16.1, the corrected test still detects the pre-fix defect, and all three local version selections pass. Updated NPU CI results are pending.

RerankerGuo and others added 4 commits September 8, 2026 13:11
…on (modelscope#9704)

When fine-tuning a VLM or LM with task_type=seq_cls (or reranker),
the seq_cls patcher monkey-patches a 'score' head onto the generation
model class without swapping the class itself. transformers'
PreTrainedModel.save_pretrained writes 'model.__class__.__name__'
into config.json['architectures'], so the on-disk checkpoint
advertises the generation architecture (e.g. Qwen3VLForConditionalGeneration)
while shipping a score head, num_labels, id2label, and problem_type.

Downstream vLLM deployment reads architectures to pick the model
class, finds the generation class, and rejects the checkpoint —
even though inference through PtEngine works correctly. The fix
rewrites model.config.architectures in-place to the matching
*ForSequenceClassification class at the same place the score head
is attached, so every save path (trainer, save_checkpoint, export,
peft merge) writes the right value.

A new helper _seq_cls_architectures() handles the suffix rewrite
idempotently and leaves unknown/custom architectures untouched.
Unit tests cover the rewrite, idempotence, multi-arch lists, and
empty/None inputs.

Refs: modelscope#9704
PreTrainedModel.save_pretrained() resets config.architectures to the model
class name right before serializing the config, so the seq_cls name assigned
in _patch_sequence_classification never reached config.json. Restore it in
PretrainedConfig.save_pretrained, the last hook before the file is written,
and cover the save path with a regression test that reads config.json back.

Also fix the yapf formatting that failed the lint job.
The seq_cls architectures hook was bound onto the model instance as a bare
closure capturing the original model and config, so copy.deepcopy(model)
silently saved the original weights and pickle.dumps(model) failed outright.
Install the wrapper on the model class instead, gated by an instance flag, so
deep copies and pickled models keep saving their own weights.

Also replace the == [class_name] check with a pre-save snapshot comparison:
the restore now only undoes a change made during the save, so a value
assigned after patching is kept, and a config without architectures no
longer gets null written into config.json.

Covered by regression tests for deepcopy, pickle, the architectures=None
case, and post-patch assignment.
Follow up on modelscope#10063: bind a pickle-safe instance save wrapper and restore architecture metadata even when config saving is skipped or fails. Cover non-main saves, fresh-process pickle, failure cleanup and existing save wrappers.
@Excelius-Wang
Excelius-Wang marked this pull request as ready for review September 22, 2026 14:18
Transformers 5.16 checks is_remote_code through a classmethod. Patch the class _auto_class attribute temporarily so the pre-config failure regression exercises the export path on both old and new Transformers versions.

This branch has not been deployed

No deployments
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.

2 participants