| |
| |
|
|
| from __future__ import annotations |
|
|
| from abc import ABC, abstractmethod |
| from typing import List, Type |
|
|
| import torch.nn as nn |
|
|
|
|
| class DecoderAdapter(ABC): |
| """Maps a fine-tuned ``ForSequenceClassification`` inner model into a ``ForCausalLM`` backbone.""" |
|
|
| @staticmethod |
| @abstractmethod |
| def matches(model_name: str) -> bool: |
| """Return True if this adapter handles ``model_name`` (HF id or path).""" |
|
|
| @abstractmethod |
| def transfer_backbone( |
| self, |
| seq_cls_inner: nn.Module, |
| causal_lm: nn.Module, |
| ) -> None: |
| """ |
| Copy shared backbone weights from the classification model into ``causal_lm``. |
| |
| ``seq_cls_inner`` is typically ``GPTNeoXForSequenceClassification`` (possibly merged from PEFT). |
| ``causal_lm`` is ``GPTNeoXForCausalLM``. ``lm_head`` stays as loaded from ``from_pretrained``. |
| """ |
|
|
|
|
| class PythiaNeoXAdapter(DecoderAdapter): |
| """EleutherAI Pythia / GPT-NeoX sequence-classification -> causal LM.""" |
|
|
| _NEEDLE = "pythia" |
|
|
| @staticmethod |
| def matches(model_name: str) -> bool: |
| m = (model_name or "").lower() |
| return PythiaNeoXAdapter._NEEDLE in m or "gpt-neox" in m |
|
|
| def transfer_backbone(self, seq_cls_inner: nn.Module, causal_lm: nn.Module) -> None: |
| inner = seq_cls_inner |
| if hasattr(inner, "merge_and_unload"): |
| inner = inner.merge_and_unload() |
|
|
| src_sd = inner.state_dict() |
| dst_sd = causal_lm.state_dict() |
| to_load = {} |
| for k, v in src_sd.items(): |
| if not k.startswith("gpt_neox."): |
| continue |
| if k not in dst_sd or dst_sd[k].shape != v.shape: |
| continue |
| to_load[k] = v.to(device=dst_sd[k].device, dtype=dst_sd[k].dtype) |
|
|
| if not to_load: |
| raise RuntimeError( |
| "PythiaNeoXAdapter: no gpt_neox.* keys matched between SeqCLS and CausalLM. " |
| "Check model_name and transformers versions." |
| ) |
| causal_lm.load_state_dict(to_load, strict=False) |
|
|
|
|
| class Qwen2Adapter(DecoderAdapter): |
| """Qwen2 / Qwen2.5 sequence-classification -> causal LM (shared ``model.*`` backbone).""" |
|
|
| @staticmethod |
| def matches(model_name: str) -> bool: |
| m = (model_name or "").lower() |
| return "qwen2" in m |
|
|
| def transfer_backbone(self, seq_cls_inner: nn.Module, causal_lm: nn.Module) -> None: |
| inner = seq_cls_inner |
| if hasattr(inner, "merge_and_unload"): |
| inner = inner.merge_and_unload() |
|
|
| src_sd = inner.state_dict() |
| dst_sd = causal_lm.state_dict() |
| to_load = {} |
| for k, v in src_sd.items(): |
| if not k.startswith("model."): |
| continue |
| if k not in dst_sd or dst_sd[k].shape != v.shape: |
| continue |
| to_load[k] = v.to(device=dst_sd[k].device, dtype=dst_sd[k].dtype) |
|
|
| if not to_load: |
| raise RuntimeError( |
| "Qwen2Adapter: no model.* keys matched between SeqCLS and CausalLM. " |
| "Check model_name and transformers versions." |
| ) |
| causal_lm.load_state_dict(to_load, strict=False) |
|
|
|
|
| class LlamaAdapter(DecoderAdapter): |
| """Llama-family sequence-classification -> causal LM (shared ``model.*`` backbone). |
| |
| Covers meta-llama/Llama-3.2-*, TinyLlama, Sheared-LLaMA, MobileLLaMA — any HF id |
| containing "llama". Same state-dict layout as Qwen2 (``model.*`` prefix). |
| Tied embeddings (e.g. Llama-3.2-1B) are handled implicitly: transferring |
| ``model.embed_tokens.weight`` updates the tied ``lm_head`` as well. |
| """ |
|
|
| @staticmethod |
| def matches(model_name: str) -> bool: |
| m = (model_name or "").lower() |
| return "llama" in m |
|
|
| def transfer_backbone(self, seq_cls_inner: nn.Module, causal_lm: nn.Module) -> None: |
| inner = seq_cls_inner |
| if hasattr(inner, "merge_and_unload"): |
| inner = inner.merge_and_unload() |
|
|
| src_sd = inner.state_dict() |
| dst_sd = causal_lm.state_dict() |
| to_load = {} |
| for k, v in src_sd.items(): |
| if not k.startswith("model."): |
| continue |
| if k not in dst_sd or dst_sd[k].shape != v.shape: |
| continue |
| to_load[k] = v.to(device=dst_sd[k].device, dtype=dst_sd[k].dtype) |
|
|
| if not to_load: |
| raise RuntimeError( |
| "LlamaAdapter: no model.* keys matched between SeqCLS and CausalLM. " |
| "Check model_name and transformers versions." |
| ) |
| causal_lm.load_state_dict(to_load, strict=False) |
|
|
|
|
| |
| ADAPTER_REGISTRY: List[Type[DecoderAdapter]] = [ |
| Qwen2Adapter, |
| LlamaAdapter, |
| PythiaNeoXAdapter, |
| ] |
|
|
|
|
| def resolve_adapter(model_name: str) -> DecoderAdapter: |
| """ |
| Select an adapter for the given Hugging Face model id. |
| |
| Raises: |
| ValueError: If no registered adapter matches. |
| """ |
| for cls in ADAPTER_REGISTRY: |
| if cls.matches(model_name): |
| return cls() |
| registered = ", ".join(c.__name__ for c in ADAPTER_REGISTRY) |
| raise ValueError( |
| f"No DecoderAdapter registered for model_name={model_name!r}. " |
| f"Implement a new adapter class, append it to ADAPTER_REGISTRY in decoder_adapters.py, " |
| f"and implement transfer_backbone for that architecture. " |
| f"Currently registered: {registered}" |
| ) |
|
|