UEmbed-4B / src /models /qwen35_embedding.py
songtingyu's picture
Initial Commit.
1dc1186 verified
Raw
History Blame Contribute Delete
19.9 kB
import os
import json
import torch
import torch.nn.functional as F
import unicodedata
import numpy as np
import logging
from PIL import Image
from urllib.parse import urlparse
from dataclasses import dataclass
from typing import Optional, List, Union, Dict, Any
from transformers.modeling_outputs import ModelOutput
from transformers.processing_utils import Unpack
from transformers.utils import TransformersKwargs
from transformers.cache_utils import Cache
from transformers import AutoModel, AutoConfig, AutoProcessor
from qwen_vl_utils.vision_process import process_vision_info
from transformers.models.qwen3_5.modeling_qwen3_5 import (
Qwen3_5PreTrainedModel, Qwen3_5Model, Qwen3_5Config
)
from transformers.models.qwen3_vl.processing_qwen3_vl import Qwen3VLProcessor
from tokenizers import processors
logger = logging.getLogger(__name__)
import torch.distributed as dist
# Constants for configuration
MAX_LENGTH = 8192
IMAGE_BASE_FACTOR = 16
IMAGE_FACTOR = IMAGE_BASE_FACTOR * 2
MIN_PIXELS = 4 * IMAGE_FACTOR * IMAGE_FACTOR
MAX_PIXELS = 1800 * IMAGE_FACTOR * IMAGE_FACTOR
FPS = 1
MAX_FRAMES = 64
FRAME_MAX_PIXELS = 768 * IMAGE_FACTOR * IMAGE_FACTOR
MAX_TOTAL_PIXELS = 10 * FRAME_MAX_PIXELS
@dataclass
class Qwen3_5ForEmbeddingOutput(ModelOutput):
last_hidden_state: Optional[torch.FloatTensor] = None
attention_mask: Optional[torch.Tensor] = None
class Qwen3_5ForEmbedding(Qwen3_5PreTrainedModel):
"""Qwen3.5 模型的 Embedding 封装,使用 AutoModel 加载。"""
config_class = AutoConfig
_checkpoint_conversion_mapping = {}
accepts_loss_kwargs = False
def __init__(self, config: Qwen3_5Config):
super().__init__(config)
self.model = Qwen3_5Model(config)
self.post_init()
def enable_bidirectional_attention(self):
self.config.is_causal = False
if hasattr(self.config, 'text_config'):
self.config.text_config.is_causal = False
self.model.language_model.config.is_causal = False
for layer in self.model.language_model.layers:
if getattr(layer, 'layer_type', None) == "full_attention":
layer.self_attn.is_causal = False
logger.info("Bidirectional attention enabled")
def get_video_features(self, pixel_values_videos: torch.FloatTensor,
video_grid_thw: Optional[torch.LongTensor] = None, **kwargs):
return self.model.get_video_features(pixel_values_videos, video_grid_thw, **kwargs)
def get_image_features(self, pixel_values: torch.FloatTensor,
image_grid_thw: Optional[torch.LongTensor] = None, **kwargs):
return self.model.get_image_features(pixel_values, image_grid_thw, **kwargs)
def forward(
self,
input_ids: torch.LongTensor = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[Cache] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
pixel_values: Optional[torch.Tensor] = None,
pixel_values_videos: Optional[torch.FloatTensor] = None,
image_grid_thw: Optional[torch.LongTensor] = None,
video_grid_thw: Optional[torch.LongTensor] = None,
cache_position: Optional[torch.LongTensor] = None,
logits_to_keep: Union[int, torch.Tensor] = 0,
**kwargs: Unpack[TransformersKwargs],
) -> Union[tuple, Qwen3_5ForEmbeddingOutput]:
self.model.rope_deltas = None # this is set for inference cache
# print(f"Rank {dist.get_rank()}, device: {torch.cuda.current_device()}, value_device: {next(self.model.parameters()).device}")
# print(f"Rank {dist.get_rank()}, inputs shape: {input_ids.shape}, inputs device: {input_ids.device}")
outputs = self.model(
input_ids=input_ids,
pixel_values=pixel_values,
pixel_values_videos=pixel_values_videos,
image_grid_thw=image_grid_thw,
video_grid_thw=video_grid_thw,
position_ids=position_ids,
attention_mask=attention_mask,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
cache_position=cache_position,
**kwargs,
)
return Qwen3_5ForEmbeddingOutput(
last_hidden_state=outputs.last_hidden_state,
attention_mask=attention_mask,
)
def sample_frames(frames: List[Union[str, Image.Image]], max_segments: int) -> List[Union[str, Image.Image]]:
duration = len(frames)
if duration <= max_segments:
return frames
frame_id_array = np.linspace(0, duration - 1, max_segments, dtype=int)
return [frames[idx] for idx in frame_id_array.tolist()]
def is_image_path(path: str) -> bool:
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.svg'}
if path.startswith(('http://', 'https://')):
parsed_url = urlparse(path)
clean_path = parsed_url.path
else:
clean_path = path
_, ext = os.path.splitext(clean_path.lower())
return ext in image_extensions
def is_video_input(video) -> bool:
if isinstance(video, str):
return True
if isinstance(video, list) and len(video) > 0:
first_elem = video[0]
if isinstance(first_elem, Image.Image):
return True
if isinstance(first_elem, str):
return is_image_path(first_elem)
return False
class Qwen35Embedder:
"""Embedder for Qwen3.5 model with sparse and dense embedding support."""
# Supported pooling methods: pooling_string -> (pooling_type, is_sparse)
_POOLING_METHODS = {
'last.normal': ('dense', False),
'splade.last': ('sparse', True),
'splade.max': ('sparse', True),
}
def __init__(
self,
model_name_or_path: str,
pooling: str = "last.normal",
normalize: bool = True,
max_length: int = MAX_LENGTH,
min_pixels: int = MIN_PIXELS,
max_pixels: int = MAX_PIXELS,
total_pixels: int = MAX_TOTAL_PIXELS,
fps: float = FPS,
max_frames: int = MAX_FRAMES,
default_instruction: str = "Represent the user's input.",
attn_type: Optional[str] = None,
**kwargs
):
self.pooling = pooling
self.normalize = normalize
self.max_length = max_length
self.min_pixels = min_pixels
self.max_pixels = max_pixels
self.total_pixels = total_pixels
self.fps = fps
self.max_frames = max_frames
self.default_instruction = default_instruction
self.attn_type = attn_type
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Load model using AutoModel with trust_remote_code
self.model = Qwen3_5ForEmbedding.from_pretrained(
model_name_or_path, trust_remote_code=True, **kwargs
).to(self.device)
self.model.eval()
if self.attn_type == 'bi':
self.model.enable_bidirectional_attention()
# Load processor
self.processor = Qwen3VLProcessor.from_pretrained(
model_name_or_path, padding_side='right', trust_remote_code=True
)
# Load sparse config and weights
self._load_sparse_config(model_name_or_path)
self.update_processor()
def update_processor(self):
self.tokenizer = self.processor.tokenizer
if self.num_eos_tokens > 0:
# eos_token = self.tokenizer.eos_token
# eos_token = self.tokenizer.eos_token
# eos_id = self.tokenizer.eos_token_id
eos_token = '<|endoftext|>'
eos_id = self.tokenizer.convert_tokens_to_ids(eos_token)
eos_suffix = " " + eos_token
eos_single = eos_suffix * self.num_eos_tokens
eos_pair = eos_suffix * self.num_eos_tokens
multi_eos_tokens = " ".join([eos_token] * self.num_eos_tokens)
template = processors.TemplateProcessing(
single=f"$A {multi_eos_tokens}",
pair="$A" + eos_pair + " $B" + eos_pair,
special_tokens=[(eos_token, eos_id)]
)
# self.original_post_processor = self.tokenizer.backend_tokenizer.post_processor
self.processor.tokenizer._tokenizer.post_processor = template
self.tokenizer.padding_side = "right"
def _load_sparse_config(self, model_name_or_path: str):
"""加载 sparse 配置和权重。"""
self.num_eos_tokens = 0 # default: disabled
self.sparse_lm_heads = None
self.sparse_bias = None
print(f"Loading sparse info from {model_name_or_path}")
sparse_info_path = os.path.join(model_name_or_path, "sparse_info.json")
sparse_weights_path = os.path.join(model_name_or_path, "sparse_weights.pt")
if os.path.exists(sparse_info_path) and os.path.exists(sparse_weights_path):
try:
with open(sparse_info_path, 'r', encoding='utf-8') as f:
sparse_info = json.load(f)
self.num_eos_tokens = sparse_info.get("num_eos_tokens", 0)
sparse_weights = torch.load(sparse_weights_path, map_location='cpu')
self.sparse_lm_heads = torch.nn.ParameterList([
torch.nn.Parameter(head, requires_grad=False)
for head in sparse_weights["sparse_lm_heads"]
]).to(self.device)
self.sparse_bias = torch.nn.ParameterList([
torch.nn.Parameter(bias, requires_grad=False)
for bias in sparse_weights["sparse_bias"]
]).to(self.device)
logger.info(f"Loaded sparse config: num_eos_tokens={self.num_eos_tokens}")
except Exception as e:
logger.warning(f"Failed to load sparse config: {e}")
self.num_eos_tokens = 0
else:
logger.info("No sparse config found, sparse embedding disabled")
def _pooling_dense_last_normal(self, hidden_state: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
"""Dense pooling: last.normal"""
last_indices = (attention_mask.cumsum(dim=1) * attention_mask).argmax(dim=1)
target_indices = last_indices - self.num_eos_tokens
# target_indices = last_indices
batch_size = hidden_state.shape[0]
batch_indices = torch.arange(batch_size, device=hidden_state.device)
return hidden_state[batch_indices, target_indices]
def _pooling_sparse_splade_last(self, hidden_state: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
"""Sparse pooling: splade.last"""
if self.num_eos_tokens == 0:
raise ValueError(
"Sparse pooling 'splade.last' requires num_eos_tokens > 0, but got 0. "
"Please ensure sparse_info.json and sparse_weights.pt exist and are valid."
)
if self.sparse_lm_heads is None:
raise ValueError(
"Sparse pooling 'splade.last' requires sparse_lm_heads, but not loaded. "
"Please ensure sparse_weights.pt contains valid 'sparse_lm_heads' and 'sparse_bias'."
)
# Ensure sparse weights are on the same device as hidden_state
device = hidden_state.device
if self.sparse_lm_heads[0].device != device:
self.sparse_lm_heads = self.sparse_lm_heads.to(device)
self.sparse_bias = self.sparse_bias.to(device)
last_indices = (attention_mask.cumsum(dim=1) * attention_mask).argmax(dim=1)
batch_size = hidden_state.shape[0]
batch_indices = torch.arange(batch_size, device=device)
all_logits = []
for i in range(self.num_eos_tokens):
offset = (self.num_eos_tokens - 1) - i
target_indices = last_indices - offset
h_i = hidden_state[batch_indices, target_indices]
logits_i = F.linear(h_i, self.sparse_lm_heads[i], self.sparse_bias[i])
all_logits.append(logits_i)
logits = torch.cat(all_logits, dim=-1)
return torch.log1p(F.relu(logits))
def _pooling_sparse_splade_max(self, hidden_state: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
"""Sparse pooling: splade.max — max over all token positions."""
if self.sparse_lm_heads is None:
raise ValueError(
"Sparse pooling 'splade.max' requires sparse_lm_heads, but not loaded. "
"Please ensure sparse_weights.pt contains valid 'sparse_lm_heads' and 'sparse_bias'."
)
device = hidden_state.device
if self.sparse_lm_heads[0].device != device:
self.sparse_lm_heads = self.sparse_lm_heads.to(device)
self.sparse_bias = self.sparse_bias.to(device)
lm_head = self.sparse_lm_heads[0]
bias = self.sparse_bias[0]
logits = F.linear(hidden_state, lm_head, bias)
weights = torch.log1p(F.relu(logits))
weights = weights.masked_fill(
~attention_mask.unsqueeze(-1).bool(),
torch.finfo(weights.dtype).min
)
sparse_embeddings, _ = weights.max(dim=1)
return sparse_embeddings
def _do_pooling(self, hidden_state: torch.Tensor, attention_mask: torch.Tensor) -> tuple:
"""
根据 pooling 字符串路由到具体的 pooling 方法。
Returns:
tuple: (embeddings, is_sparse)
"""
if self.pooling not in self._POOLING_METHODS:
raise ValueError(
f"Unknown pooling method: '{self.pooling}'. "
f"Supported methods: {list(self._POOLING_METHODS.keys())}"
)
pooling_type, is_sparse = self._POOLING_METHODS[self.pooling]
method_name = f"_pooling_{pooling_type}_{self.pooling.replace('.', '_')}"
if not hasattr(self, method_name):
raise ValueError(f"Pooling method '{self.pooling}' is defined but implementation '{method_name}' not found.")
pooling_fn = getattr(self, method_name)
embeddings = pooling_fn(hidden_state, attention_mask)
return embeddings, is_sparse
def format_model_input(
self,
text: Optional[Union[List[str], str]] = None,
image: Optional[Union[List[Union[str, Image.Image]], str, Image.Image]] = None,
video: Optional[Union[List, str]] = None,
instruction: Optional[str] = None,
fps: Optional[float] = None,
max_frames: Optional[int] = None
) -> List[Dict]:
if instruction:
instruction = instruction.strip()
if instruction and not unicodedata.category(instruction[-1]).startswith('P'):
instruction = instruction + '.'
content = []
conversation = [
{"role": "system", "content": [{"type": "text", "text": instruction or self.default_instruction}]},
{"role": "user", "content": content}
]
texts = [text] if isinstance(text, str) else (text or [])
images = [image] if image and not isinstance(image, list) else (image or [])
videos = [video] if is_video_input(video) else (video or [])
if not texts and not images and not videos:
content.append({'type': 'text', 'text': "NULL"})
return conversation
for vid in videos:
if not vid:
continue
if isinstance(vid, list):
video_content = vid
if self.max_frames is not None:
video_content = sample_frames(video_content, self.max_frames)
video_content = [('file://' + ele if isinstance(ele, str) else ele) for ele in video_content]
video_kwargs = {'total_pixels': self.total_pixels}
elif isinstance(vid, str):
video_content = vid if vid.startswith(('http://', 'https://')) else 'file://' + vid
video_kwargs = {'fps': fps or self.fps, 'max_frames': max_frames or self.max_frames}
else:
raise TypeError(f"Unrecognized video type: {type(vid)}")
if video_content:
content.append({'type': 'video', 'video': video_content, **video_kwargs})
for img in images:
if not img:
continue
if isinstance(img, Image.Image):
image_content = img
elif isinstance(img, str):
image_content = img if img.startswith(('http://', 'https://')) else 'file://' + img
else:
raise TypeError(f"Unrecognized image type: {type(img)}")
if image_content:
content.append({
'type': 'image', 'image': image_content,
"min_pixels": self.min_pixels, "max_pixels": self.max_pixels
})
for txt in texts:
if txt:
content.append({'type': 'text', 'text': txt})
return conversation
def _preprocess_inputs(self, conversations: List[List[Dict]]) -> Dict[str, torch.Tensor]:
text = self.processor.apply_chat_template(
conversations, add_generation_prompt=True, tokenize=False
)
try:
images, video_inputs, video_kwargs = process_vision_info(
conversations, image_patch_size=16,
return_video_metadata=True, return_video_kwargs=True
)
except Exception as e:
logger.error(f"{conversations[0]}")
logger.error(f"Error in processing vision info: {e}")
logger.error(conversations)
images = None
video_inputs = None
video_kwargs = {'do_sample_frames': False}
text = self.processor.apply_chat_template(
[{'role': 'user', 'content': [{'type': 'text', 'text': 'NULL'}]}],
add_generation_prompt=True, tokenize=False
)
if video_inputs is not None:
videos, video_metadata = zip(*video_inputs)
videos, video_metadata = list(videos), list(video_metadata)
else:
videos, video_metadata = None, None
inputs = self.processor(
text=text, images=images, videos=videos, video_metadata=video_metadata,
truncation=True, max_length=self.max_length, padding=True,
do_resize=False, return_tensors='pt', **video_kwargs
)
return inputs
@torch.no_grad()
def forward(self, inputs: Dict[str, Any]) -> Dict[str, torch.Tensor]:
outputs = self.model(**inputs)
return {
'last_hidden_state': outputs.last_hidden_state,
'attention_mask': inputs.get('attention_mask')
}
def process(self, inputs: List[Dict[str, Any]], normalize: bool = None) -> torch.Tensor:
conversations = [self.format_model_input(
text=ele.get('text'),
image=ele.get('image'),
video=ele.get('video'),
instruction=ele.get('instruction'),
fps=ele.get('fps'),
max_frames=ele.get('max_frames')
) for ele in inputs]
processed_inputs = self._preprocess_inputs(conversations)
processed_inputs = {k: v.to(self.device) for k, v in processed_inputs.items()}
outputs = self.forward(processed_inputs)
hidden_state = outputs['last_hidden_state']
attention_mask = outputs['attention_mask']
embeddings, is_sparse = self._do_pooling(hidden_state, attention_mask)
# Normalize for dense embeddings
if not is_sparse:
if normalize is None:
normalize = self.normalize
if normalize:
embeddings = F.normalize(embeddings, p=2, dim=-1)
return embeddings