Visual Document Retrieval
Transformers
Safetensors
sentence-transformers
multilingual
qwen3_5
feature-extraction
text
image
multimodal-embedding
vidore
colbert
colqwen3_5
multilingual-embedding
multi-vector
custom_code
Instructions to use webAI-Official/webAI-ColVec1.1-4b with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use webAI-Official/webAI-ColVec1.1-4b with Transformers:
# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModel processor = AutoProcessor.from_pretrained("webAI-Official/webAI-ColVec1.1-4b", trust_remote_code=True) model = AutoModel.from_pretrained("webAI-Official/webAI-ColVec1.1-4b", trust_remote_code=True, device_map="auto") - sentence-transformers
How to use webAI-Official/webAI-ColVec1.1-4b with sentence-transformers:
from sentence_transformers import MultiVectorEncoder model = MultiVectorEncoder("webAI-Official/webAI-ColVec1.1-4b", trust_remote_code=True) queries = ["Which planet is known as the Red Planet?"] documents = [ "Venus is often called Earth's twin because of its similar size and proximity.", "Mars, known for its reddish appearance, is often referred to as the Red Planet.", "Jupiter, the largest planet in our solar system, has a prominent red spot.", ] query_embeddings = model.encode_query(queries) document_embeddings = model.encode_document(documents) similarities = model.similarity(query_embeddings, document_embeddings) print(similarities) - Notebooks
- Google Colab
- Kaggle
Download modeling_colqwen35_bidirection.py from webAI-Official/webAI-ColVec1.1-4b: direct link, hf CLI and curl.
- Browser
- Download file 8.59 kB
-
https://huggingface.co/webAI-Official/webAI-ColVec1.1-4b/resolve/main/modeling_colqwen35_bidirection.py
- Command line
-
hf download hf://webAI-Official/webAI-ColVec1.1-4b/modeling_colqwen35_bidirection.py
-
curl -L -o modeling_colqwen35_bidirection.py https://huggingface.co/webAI-Official/webAI-ColVec1.1-4b/resolve/main/modeling_colqwen35_bidirection.py
8.59 kB
| """ | |
| ColQwen35Bidirection - Inheritance-based Qwen3.5 retrieval model. | |
| Subclasses Qwen3_5ForConditionalGeneration directly and adds a projection | |
| head for ColBERT-style multi-vector retrieval. Supports both causal (default) | |
| and bidirectional attention modes via the `is_causal` parameter. | |
| Key design decisions: | |
| - `is_causal` is popped in `from_pretrained` and NEVER forwarded to | |
| `super().from_pretrained(...)`. This prevents HuggingFace's | |
| PretrainedConfig catch-all from injecting `config.is_causal` onto | |
| the model config, which would alter Qwen 3.5's mask resolver path. | |
| With `is_causal=True`, the load path is bit-equivalent to | |
| ColQwen35Default (no config attribute mutation). | |
| - Bidirectional patches (`_configure_bidirectional_attention` on the | |
| config, `_patch_attention_is_causal` on attention modules) are | |
| applied around the parent load in `from_pretrained`, not forwarded | |
| through kwargs. The `__init__` retains idempotent guards for direct | |
| construction (e.g. tests). | |
| - No `self.is_causal` attribute is set before `super().__init__()`, | |
| avoiding shadowing of inherited attributes. | |
| """ | |
| from torch import nn | |
| from transformers.models.qwen3_5 import Qwen3_5ForConditionalGeneration, Qwen3_5Config | |
| from transformers.utils import is_torch_available, logging | |
| if is_torch_available(): | |
| import torch | |
| logger = logging.get_logger(__name__) | |
| class ColQwen35Bidirection(Qwen3_5ForConditionalGeneration): | |
| """ | |
| Qwen 3.5 VLM adapted to return ColBERT-style token embeddings. | |
| Supports bidirectional attention (is_causal=False) for the 8 full | |
| attention layers while leaving the 24 GatedDeltaNet layers causal. | |
| When is_causal=True (default), the model uses Qwen 3.5's native | |
| attention without any modifications. | |
| """ | |
| _checkpoint_conversion_mapping: dict[str, str] = { | |
| r"^base_model\.model\.embedding_proj_layer": "embedding_proj_layer", | |
| } | |
| def __init__( | |
| self, | |
| config: Qwen3_5Config, | |
| embedding_dim: int | None = None, | |
| is_causal: bool | None = None, | |
| ): | |
| if embedding_dim is None: | |
| embedding_dim = getattr(config, "embedding_dim", 128) | |
| if is_causal is None: | |
| is_causal = getattr(config, "is_causal", True) | |
| if not is_causal: | |
| self._configure_bidirectional_attention(config) | |
| super().__init__(config) | |
| if not is_causal: | |
| self._patch_attention_is_causal() | |
| self._is_causal_mode = is_causal | |
| hidden_size = getattr(self.config, "hidden_size", None) | |
| if hidden_size is None and hasattr(self.config, "text_config"): | |
| hidden_size = getattr(self.config.text_config, "hidden_size", None) | |
| if hidden_size is None: | |
| raise ValueError( | |
| f"Unable to determine text hidden size for {type(self.config).__name__}." | |
| ) | |
| self.embedding_dim = embedding_dim | |
| self.config.embedding_dim = embedding_dim | |
| self.embedding_proj_layer = nn.Linear(hidden_size, self.embedding_dim) | |
| self.padding_side = "left" | |
| self.post_init() | |
| if getattr(self.config, "lm_head_removed", False): | |
| self.remove_lm_head() | |
| def _configure_bidirectional_attention(config: Qwen3_5Config) -> None: | |
| """ | |
| Patch config for bidirectional mask generation. | |
| Sets is_causal=False on the config so that create_causal_mask() | |
| produces a padding-only mask instead of a lower-triangular causal mask. | |
| Must be called before super().__init__(). | |
| """ | |
| text_config = config.get_text_config() | |
| text_config.is_causal = False | |
| config.is_causal = False | |
| def _patch_attention_is_causal(self) -> None: | |
| """ | |
| Patch is_causal on every Qwen3_5Attention module. | |
| Upstream Qwen3_5Attention.__init__ hardcodes self.is_causal = True. | |
| This flag is passed to Flash Attention and other attention backends, | |
| so it must be False for bidirectional behavior. | |
| """ | |
| patched = 0 | |
| for module in self.modules(): | |
| if type(module).__name__ == "Qwen3_5Attention": | |
| module.is_causal = False | |
| patched += 1 | |
| logger.info("Patched is_causal=False on %d Qwen3_5Attention modules", patched) | |
| def from_pretrained(cls, *args, **kwargs): | |
| is_causal = kwargs.pop("is_causal", None) | |
| key_mapping = kwargs.pop("key_mapping", None) | |
| if key_mapping is None: | |
| key_mapping = dict(getattr(super(), "_checkpoint_conversion_mapping", {})) | |
| key_mapping.update(cls._checkpoint_conversion_mapping) | |
| if not is_causal: | |
| from transformers import AutoConfig | |
| config = kwargs.get("config") | |
| if config is None: | |
| path = args[0] if args else kwargs.get("pretrained_model_name_or_path") | |
| config = AutoConfig.from_pretrained(path) | |
| kwargs["config"] = config | |
| cls._configure_bidirectional_attention(config) | |
| instance = super().from_pretrained( | |
| *args, **kwargs, key_mapping=key_mapping | |
| ) | |
| resolved_is_causal = ( | |
| getattr(instance.config, "is_causal", True) | |
| if is_causal is None | |
| else is_causal | |
| ) | |
| if not resolved_is_causal: | |
| instance._patch_attention_is_causal() | |
| instance._is_causal_mode = resolved_is_causal | |
| return instance | |
| def remove_lm_head(self) -> dict[str, int]: | |
| """Permanently remove the unused language-model output projection. | |
| The retrieval forward path calls ``self.model`` directly and consumes | |
| hidden states, so ``lm_head`` is never used to produce embeddings. | |
| Persisting ``lm_head_removed`` in the config ensures exported | |
| checkpoints reload with ``nn.Identity`` instead of recreating a large, | |
| randomly initialized output projection. | |
| """ | |
| lm_head = getattr(self, "lm_head", None) | |
| if lm_head is None: | |
| raise AttributeError("Model has no lm_head attribute") | |
| if isinstance(lm_head, nn.Identity): | |
| return {"parameters": 0, "bytes": 0} | |
| parameters = {id(parameter): parameter for parameter in lm_head.parameters()} | |
| shared_elsewhere = { | |
| id(parameter) | |
| for name, parameter in self.named_parameters(remove_duplicate=False) | |
| if not name.startswith("lm_head.") | |
| } | |
| removed = { | |
| parameter_id: parameter | |
| for parameter_id, parameter in parameters.items() | |
| if parameter_id not in shared_elsewhere | |
| } | |
| removed_parameters = sum(parameter.numel() for parameter in removed.values()) | |
| removed_bytes = sum( | |
| parameter.numel() * parameter.element_size() | |
| for parameter in removed.values() | |
| ) | |
| self.lm_head = nn.Identity() | |
| self.config.lm_head_removed = True | |
| self.config.tie_word_embeddings = False | |
| tied_keys = getattr(self, "_tied_weights_keys", None) | |
| if isinstance(tied_keys, dict): | |
| self._tied_weights_keys = { | |
| key: value | |
| for key, value in tied_keys.items() | |
| if "lm_head" not in key and "lm_head" not in value | |
| } | |
| elif isinstance(tied_keys, (list, tuple, set)): | |
| self._tied_weights_keys = [ | |
| key for key in tied_keys if "lm_head" not in key | |
| ] | |
| return {"parameters": removed_parameters, "bytes": removed_bytes} | |
| def forward(self, *args, **kwargs) -> torch.Tensor: | |
| inner = getattr(self.model, "model", self.model) | |
| if hasattr(inner, "rope_deltas"): | |
| inner.rope_deltas = None | |
| kwargs.pop("return_dict", None) | |
| kwargs.pop("output_hidden_states", None) | |
| kwargs.pop("use_cache", None) | |
| use_cache = not self.training | |
| last_hidden_state = ( | |
| self.model.forward( | |
| *args, **kwargs, | |
| output_hidden_states=False, | |
| return_dict=True, | |
| use_cache=use_cache, | |
| ) | |
| .last_hidden_state | |
| ) | |
| proj_dtype = self.embedding_proj_layer.weight.dtype | |
| embeddings = self.embedding_proj_layer(last_hidden_state.to(proj_dtype)) | |
| embeddings = embeddings / embeddings.norm(dim=-1, keepdim=True).clamp(min=1e-6) | |
| embeddings = embeddings * kwargs["attention_mask"].unsqueeze(-1) | |
| return embeddings | |
| __all__ = ["ColQwen35Bidirection"] | |