Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions swift/model/patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,29 @@ def transformers_seq_cls_forward(self, *args, origin_forward, padding_side=None,
)


def _seq_cls_architectures(arch_list):
"""Rewrite architectures to the matching ``*ForSequenceClassification`` class.

Used by :func:`_patch_sequence_classification` so that the on-disk
``config.json`` produced by ``PreTrainedModel.save_pretrained`` references
the seq_cls architecture (e.g. ``Qwen3VLForSequenceClassification``) instead
of the generation architecture that the model class is actually an instance
of. Without this, downstream vLLM deployment fails because the checkpoint
advertises ``Qwen3VLForConditionalGeneration`` while shipping a
``score`` head and ``num_labels`` / ``problem_type`` fields — see #9704.
"""
if not arch_list:
return arch_list
res = []
for arch in arch_list:
if arch.endswith('ForConditionalGeneration'):
arch = arch[:-len('ForConditionalGeneration')] + 'ForSequenceClassification'
elif arch.endswith('ForCausalLM'):
arch = arch[:-len('ForCausalLM')] + 'ForSequenceClassification'
res.append(arch)
return res


def _patch_sequence_classification(model, model_meta):
hidden_size = HfConfigFactory.get_config_attr(model.config, 'hidden_size')
initializer_range = HfConfigFactory.get_config_attr(model.config, 'initializer_range')
Expand All @@ -274,6 +297,12 @@ def new_forward(self, *args, **kwargs):

lm_head_model.forward = MethodType(new_forward, lm_head_model)

# Align the on-disk `architectures` with the task. PreTrainedModel.save_pretrained
# writes `model.__class__.__name__` for `architectures`, but the seq_cls patcher
# monkey-patches a `score` head onto the generation class without swapping it,
# so the saved checkpoint would otherwise advertise the wrong class (see #9704).
model.config.architectures = _seq_cls_architectures(getattr(model.config, 'architectures', None))


@contextmanager
def patch_automodel_for_sequence_classification(model_info=None,
Expand Down
47 changes: 47 additions & 0 deletions tests/general/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,53 @@ def test_registration(self):
self.assertIn(TemplateType.molmo2, TEMPLATE_MAPPING)


class TestSeqClsArchitecturesRewrite(unittest.TestCase):
"""Regression coverage for #9704.

The seq_cls / reranker patcher monkey-patches a ``score`` head onto the
generation model class without swapping the class itself, so
``PreTrainedModel.save_pretrained`` would otherwise write the wrong
``architectures`` to ``config.json`` and break downstream vLLM deployment.
"""

def test_seq_cls_architectures_rewrite(self):
from swift.model.patcher import _seq_cls_architectures

# Generation -> SequenceClassification
self.assertEqual(_seq_cls_architectures(['Qwen2ForCausalLM']), ['Qwen2ForSequenceClassification'])
self.assertEqual(
_seq_cls_architectures(['Qwen3VLForConditionalGeneration']),
['Qwen3VLForSequenceClassification'])
self.assertEqual(_seq_cls_architectures(['LlamaForCausalLM']), ['LlamaForSequenceClassification'])
self.assertEqual(
_seq_cls_architectures(['Qwen2VLForConditionalGeneration']),
['Qwen2VLForSequenceClassification'])

# Already-seq_cls class is preserved (idempotent).
self.assertEqual(
_seq_cls_architectures(['BertForSequenceClassification']),
['BertForSequenceClassification'])

# Multi-arch list: only the matching suffix is rewritten.
self.assertEqual(
_seq_cls_architectures(['FooForCausalLM', 'BarForConditionalGeneration']),
['FooForSequenceClassification', 'BarForSequenceClassification'])

# Empty / None inputs are returned unchanged.
self.assertEqual(_seq_cls_architectures([]), [])
self.assertIsNone(_seq_cls_architectures(None))

def test_seq_cls_architectures_unknown_suffix(self):
"""Unknown suffixes (custom architectures) are left alone — we don't
silently invent a class name that may not exist in transformers."""
from swift.model.patcher import _seq_cls_architectures

self.assertEqual(_seq_cls_architectures(['MyCustomLMHeadModel']), ['MyCustomLMHeadModel'])
self.assertEqual(
_seq_cls_architectures(['MyCustomLMHeadModel', 'LlamaForCausalLM']),
['MyCustomLMHeadModel', 'LlamaForSequenceClassification'])


if __name__ == '__main__':
test_qwen2()
# test_modelscope_hub()
Loading