diff --git a/swift/model/patcher.py b/swift/model/patcher.py index 57bd6ac4d7..57e88e2ed2 100644 --- a/swift/model/patcher.py +++ b/swift/model/patcher.py @@ -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') @@ -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, diff --git a/tests/general/test_model.py b/tests/general/test_model.py index 89825b597f..08b1e1977d 100644 --- a/tests/general/test_model.py +++ b/tests/general/test_model.py @@ -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()