File size: 5,640 Bytes
0ab2566
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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]