CoolFace
Modelpublic

webAI-Official/webAI-ColVec1.1-4b

sourceHugging Faceotherupdated 1mo agoView on Hugging Face
6likes1.6kdownloads
modeling_colqwen35_bidirection.py227 linesDownload Raw Back to root
1"""2ColQwen35Bidirection - Inheritance-based Qwen3.5 retrieval model.3 4Subclasses Qwen3_5ForConditionalGeneration directly and adds a projection5head for ColBERT-style multi-vector retrieval. Supports both causal (default)6and bidirectional attention modes via the `is_causal` parameter.7 8Key design decisions:9  - `is_causal` is popped in `from_pretrained` and NEVER forwarded to10    `super().from_pretrained(...)`. This prevents HuggingFace's11    PretrainedConfig catch-all from injecting `config.is_causal` onto12    the model config, which would alter Qwen 3.5's mask resolver path.13    With `is_causal=True`, the load path is bit-equivalent to14    ColQwen35Default (no config attribute mutation).15  - Bidirectional patches (`_configure_bidirectional_attention` on the16    config, `_patch_attention_is_causal` on attention modules) are17    applied around the parent load in `from_pretrained`, not forwarded18    through kwargs. The `__init__` retains idempotent guards for direct19    construction (e.g. tests).20  - No `self.is_causal` attribute is set before `super().__init__()`,21    avoiding shadowing of inherited attributes.22"""23 24from torch import nn25from transformers.models.qwen3_5 import Qwen3_5ForConditionalGeneration, Qwen3_5Config26from transformers.utils import is_torch_available, logging27 28if is_torch_available():29    import torch30 31logger = logging.get_logger(__name__)32 33 34class ColQwen35Bidirection(Qwen3_5ForConditionalGeneration):35    """36    Qwen 3.5 VLM adapted to return ColBERT-style token embeddings.37 38    Supports bidirectional attention (is_causal=False) for the 8 full39    attention layers while leaving the 24 GatedDeltaNet layers causal.40    When is_causal=True (default), the model uses Qwen 3.5's native41    attention without any modifications.42    """43 44    _checkpoint_conversion_mapping: dict[str, str] = {45        r"^base_model\.model\.embedding_proj_layer": "embedding_proj_layer",46    }47 48    def __init__(49        self,50        config: Qwen3_5Config,51        embedding_dim: int | None = None,52        is_causal: bool | None = None,53    ):54        if embedding_dim is None:55            embedding_dim = getattr(config, "embedding_dim", 128)56        if is_causal is None:57            is_causal = getattr(config, "is_causal", True)58 59        if not is_causal:60            self._configure_bidirectional_attention(config)61 62        super().__init__(config)63 64        if not is_causal:65            self._patch_attention_is_causal()66 67        self._is_causal_mode = is_causal68 69        hidden_size = getattr(self.config, "hidden_size", None)70        if hidden_size is None and hasattr(self.config, "text_config"):71            hidden_size = getattr(self.config.text_config, "hidden_size", None)72        if hidden_size is None:73            raise ValueError(74                f"Unable to determine text hidden size for {type(self.config).__name__}."75            )76 77        self.embedding_dim = embedding_dim78        self.config.embedding_dim = embedding_dim79        self.embedding_proj_layer = nn.Linear(hidden_size, self.embedding_dim)80        self.padding_side = "left"81 82        self.post_init()83        if getattr(self.config, "lm_head_removed", False):84            self.remove_lm_head()85 86    @staticmethod87    def _configure_bidirectional_attention(config: Qwen3_5Config) -> None:88        """89        Patch config for bidirectional mask generation.90 91        Sets is_causal=False on the config so that create_causal_mask()92        produces a padding-only mask instead of a lower-triangular causal mask.93        Must be called before super().__init__().94        """95        text_config = config.get_text_config()96        text_config.is_causal = False97        config.is_causal = False98 99    def _patch_attention_is_causal(self) -> None:100        """101        Patch is_causal on every Qwen3_5Attention module.102 103        Upstream Qwen3_5Attention.__init__ hardcodes self.is_causal = True.104        This flag is passed to Flash Attention and other attention backends,105        so it must be False for bidirectional behavior.106        """107        patched = 0108        for module in self.modules():109            if type(module).__name__ == "Qwen3_5Attention":110                module.is_causal = False111                patched += 1112        logger.info("Patched is_causal=False on %d Qwen3_5Attention modules", patched)113 114    @classmethod115    def from_pretrained(cls, *args, **kwargs):116        is_causal = kwargs.pop("is_causal", None)117        key_mapping = kwargs.pop("key_mapping", None)118        if key_mapping is None:119            key_mapping = dict(getattr(super(), "_checkpoint_conversion_mapping", {}))120            key_mapping.update(cls._checkpoint_conversion_mapping)121 122        if not is_causal:123            from transformers import AutoConfig124            config = kwargs.get("config")125            if config is None:126                path = args[0] if args else kwargs.get("pretrained_model_name_or_path")127                config = AutoConfig.from_pretrained(path)128                kwargs["config"] = config129            cls._configure_bidirectional_attention(config)130 131        instance = super().from_pretrained(132            *args, **kwargs, key_mapping=key_mapping133        )134 135        resolved_is_causal = (136            getattr(instance.config, "is_causal", True)137            if is_causal is None138            else is_causal139        )140        if not resolved_is_causal:141            instance._patch_attention_is_causal()142 143        instance._is_causal_mode = resolved_is_causal144        return instance145 146    def remove_lm_head(self) -> dict[str, int]:147        """Permanently remove the unused language-model output projection.148 149        The retrieval forward path calls ``self.model`` directly and consumes150        hidden states, so ``lm_head`` is never used to produce embeddings.151        Persisting ``lm_head_removed`` in the config ensures exported152        checkpoints reload with ``nn.Identity`` instead of recreating a large,153        randomly initialized output projection.154        """155        lm_head = getattr(self, "lm_head", None)156        if lm_head is None:157            raise AttributeError("Model has no lm_head attribute")158        if isinstance(lm_head, nn.Identity):159            return {"parameters": 0, "bytes": 0}160 161        parameters = {id(parameter): parameter for parameter in lm_head.parameters()}162        shared_elsewhere = {163            id(parameter)164            for name, parameter in self.named_parameters(remove_duplicate=False)165            if not name.startswith("lm_head.")166        }167        removed = {168            parameter_id: parameter169            for parameter_id, parameter in parameters.items()170            if parameter_id not in shared_elsewhere171        }172        removed_parameters = sum(parameter.numel() for parameter in removed.values())173        removed_bytes = sum(174            parameter.numel() * parameter.element_size()175            for parameter in removed.values()176        )177 178        self.lm_head = nn.Identity()179        self.config.lm_head_removed = True180        self.config.tie_word_embeddings = False181 182        tied_keys = getattr(self, "_tied_weights_keys", None)183        if isinstance(tied_keys, dict):184            self._tied_weights_keys = {185                key: value186                for key, value in tied_keys.items()187                if "lm_head" not in key and "lm_head" not in value188            }189        elif isinstance(tied_keys, (list, tuple, set)):190            self._tied_weights_keys = [191                key for key in tied_keys if "lm_head" not in key192            ]193 194        return {"parameters": removed_parameters, "bytes": removed_bytes}195 196    def forward(self, *args, **kwargs) -> torch.Tensor:197        inner = getattr(self.model, "model", self.model)198        if hasattr(inner, "rope_deltas"):199            inner.rope_deltas = None200 201        kwargs.pop("return_dict", None)202        kwargs.pop("output_hidden_states", None)203        kwargs.pop("use_cache", None)204 205        use_cache = not self.training206 207        last_hidden_state = (208            self.model.forward(209                *args, **kwargs,210                output_hidden_states=False,211                return_dict=True,212                use_cache=use_cache,213            )214            .last_hidden_state215        )216 217        proj_dtype = self.embedding_proj_layer.weight.dtype218        embeddings = self.embedding_proj_layer(last_hidden_state.to(proj_dtype))219 220        embeddings = embeddings / embeddings.norm(dim=-1, keepdim=True).clamp(min=1e-6)221        embeddings = embeddings * kwargs["attention_mask"].unsqueeze(-1)222 223        return embeddings224 225 226__all__ = ["ColQwen35Bidirection"]227