Visual Document Retrieval
Transformers
Safetensors
multilingual
qwen3_5
feature-extraction
text
image
multimodal-embedding
vidore
colbert
colqwen3_5
multilingual-embedding
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:
# 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") - Notebooks
- Google Colab
- Kaggle
File size: 8,590 Bytes
e8491df | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | """
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()
@staticmethod
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)
@classmethod
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"]
|