liqi / code /multi_vlm2vec_train /train_cot /qwen3_vl_cot_embedding.py
WaitXian's picture
Upload liqi files
0ab2566 verified
Raw History Blame Contribute Delete
5.64 kB
import logging
from dataclasses import dataclass
from typing import Optional, Union
import torch
import torch.nn.functional as F
from transformers.cache_utils import Cache
from transformers.modeling_outputs import ModelOutput
from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLForConditionalGeneration
logger = logging.getLogger(__name__)
IGNORE_INDEX = -100
@dataclass
class Qwen3VLCoTEmbeddingOutput(ModelOutput):
"""CoT embedding 模型的前向输出。
last_hidden_state / attention_mask 用于 last-token pooling 取 embedding;
lm_loss 为 CoT 段上的 cross entropy,只在传入 labels 时计算;
logits / past_key_values 只在生成(return_logits=True)时填充。
"""
lm_loss: Optional[torch.FloatTensor] = None
num_cot_tokens: Optional[torch.Tensor] = None
last_hidden_state: Optional[torch.FloatTensor] = None
attention_mask: Optional[torch.Tensor] = None
logits: Optional[torch.FloatTensor] = None
past_key_values: Optional[Cache] = None
class Qwen3VLForCoTEmbedding(Qwen3VLForConditionalGeneration):
"""在 Qwen3-VL 上同时输出 CoT 生成 logits 与检索 embedding 的模型。
继承 Qwen3VLForConditionalGeneration 以复用 lm_head、权重 tie 逻辑以及
generate(),因此同一份权重既能做 CoT 生成(推理阶段 query 先出 CoT)
也能做 embedding(对 last_hidden_state 做 last-token pooling)。
query 侧的输入由 collator 拼成 `prefix + cot + <|im_end|>` 的 teacher forcing
形式,labels 只在 CoT 段有效;doc 侧不传 labels,只取 embedding。
"""
accepts_loss_kwargs = False
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,
labels: Optional[torch.LongTensor] = None,
return_logits: bool = False,
logits_to_keep: Union[int, torch.Tensor] = 0,
**kwargs,
) -> Qwen3VLCoTEmbeddingOutput:
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,
)
hidden_states = outputs.last_hidden_state
lm_loss, num_cot_tokens = None, None
if labels is not None:
lm_loss, num_cot_tokens = self.cot_lm_loss(hidden_states, labels)
# 训练时不需要全序列 logits(显存开销 = B*L*vocab),只有生成时才算
logits = None
if return_logits:
slice_indices = (
slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) and logits_to_keep > 0
else slice(None)
)
logits = self.lm_head(hidden_states[:, slice_indices, :])
return Qwen3VLCoTEmbeddingOutput(
lm_loss=lm_loss,
num_cot_tokens=num_cot_tokens,
last_hidden_state=hidden_states,
attention_mask=attention_mask,
logits=logits,
past_key_values=outputs.past_key_values,
)
def cot_lm_loss(self, hidden_states: torch.Tensor, labels: torch.Tensor):
"""只在 labels != -100 的位置上算 next-token cross entropy。
先按 label mask 把需要预测的 hidden state 挑出来再过 lm_head,
避免对整条序列展开 (B, L, vocab_size) 的 logits。
"""
shift_hidden = hidden_states[:, :-1, :]
shift_labels = labels[:, 1:]
keep = shift_labels != IGNORE_INDEX
num_cot_tokens = keep.sum()
if num_cot_tokens == 0:
# 整个 batch 都没有 CoT 监督(例如全部被截断),返回挂在计算图上的 0
logger.warning("No CoT label token in this batch, cot loss is skipped.")
return hidden_states.sum() * 0.0, num_cot_tokens
selected_hidden = shift_hidden[keep] # (M, H)
targets = shift_labels[keep].to(torch.long) # (M,)
selected_logits = self.lm_head(selected_hidden).float() # (M, vocab_size)
return F.cross_entropy(selected_logits, targets), num_cot_tokens
def prepare_inputs_for_generation(self, *args, **kwargs):
# generate() 依赖 outputs.logits,因此生成路径上显式打开 logits 计算
model_inputs = super().prepare_inputs_for_generation(*args, **kwargs)
model_inputs["return_logits"] = True
return model_inputs
def pooling_last(hidden_state: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
"""取每条序列最后一个有效 token 的 hidden state 作为 embedding。"""
flipped = attention_mask.flip(dims=[1])
last_positions = flipped.argmax(dim=1)
col = attention_mask.shape[1] - last_positions - 1
row = torch.arange(hidden_state.shape[0], device=hidden_state.device)
return hidden_state[row, col]