Download code/multi_vlm2vec_train/train_cot/qwen3_vl_cot_embedding.py from WaitXian/liqi: direct link, hf CLI and curl.
- Browser
- Download file 5.64 kB
-
https://huggingface.co/WaitXian/liqi/resolve/main/code/multi_vlm2vec_train/train_cot/qwen3_vl_cot_embedding.py
- Command line
-
hf download hf://WaitXian/liqi/code/multi_vlm2vec_train/train_cot/qwen3_vl_cot_embedding.py
-
curl -L -o qwen3_vl_cot_embedding.py https://huggingface.co/WaitXian/liqi/resolve/main/code/multi_vlm2vec_train/train_cot/qwen3_vl_cot_embedding.py
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 | |
| 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] | |