Interleaved Rotary Position Embedding
RoPE:旋转位置编码
RoPE(Rotary Position Embedding)通过旋转 Q/K 特征,使 attention 内积包含相对位置信息。设旋转维度为偶数
-
对应 rope_theta,源码变量名为base,是生成频率序列的底数 -
当
时,频率以公比 递减: ,后续频率逐渐降低。增大 base会拉长各分量的周期 -
inv_freq[i]表示第对旋转特征每单位位置的相位增量,实际旋转角为 position_id * inv_freq[i]
{
"text_config": {
"hidden_size": 5120,
"num_attention_heads": 24,
"num_key_value_heads": 4,
"head_dim": 256,
"max_position_embeddings": 262144,
"rope_parameters": {
"rope_type": "default",
"rope_theta": 10000000,
"partial_rotary_factor": 0.25,
"mrope_interleaved": true,
"mrope_section": [11, 11, 10]
}
}
}
源码采用 [B,H,T,d_h] 布局:B 为 batch size,T 为当前输入长度,Q/K 的 head 数分别为 24/4;head_dim 为
Q / K [B, 24, T, 256] / [B, 4, T, 256]
position_ids [B, T]
inv_freq [32]
相位 Φ [B, T, 32]
cos / sin [B, T, 64] → unsqueeze(1) → [B, 1, T, 64]
先由 Qwen3_5TextRotaryEmbedding.compute_default_rope_parameters() 计算 32 个频率
base = config.rope_parameters["rope_theta"]
partial_rotary_factor = config.rope_parameters.get("partial_rotary_factor", 1.0)
head_dim = (
getattr(config, "head_dim", None)
or config.hidden_size // config.num_attention_heads
)
dim = int(head_dim * partial_rotary_factor)
attention_factor = 1.0
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float) / dim))
return inv_freq.to(device), attention_factor
arange(0,64,2)/64生成指数,得到 inv_freq为- 频率按旋转维度
64直接计算,而非从全维RoPE中截取
RoFormer §3.2.1 式(13)先给出二维形式。只写 Q 分支,以 Q、Q
-
从右向左计算:输入
先经 投影得到 ,再乘旋转矩阵得到 。角频率统一记为 ,对应某一通道对的 ; 是位置乘频率 -
对应到
Qwen3.5,取一个token、一个head传入RoPE的Q向量q(已完成投影和Q norm)。设位置, q[0]=1、q[32]=2;这对通道的频率,因此
- 结果已经是旋转后
Q的第 0、32 维。其余 31 对同样计算,再接回未旋转的后 192 维,得到完整的; 的完整矩阵见下文。 K同理,随后用旋转后的Q/K计算attention内积。同一token的各head复用cos/sin,分别旋转各自的Q/K数值
RoFormer 式(15)对全部
- 每个
块作用于Q的相邻分量 ,K同理;整体为 。式(14)定义 、 ;记投影结果为 ,由 得到相对位置内积
Qwen3.5 的 partial RoPE 是分块正交变换:前 64 维参与旋转,后 192 维由单位映射保留
- 令
、 ,其中 ,则
-
左上角的
旋转块记为 ,通过固定的通道置换与论文的块对角矩阵对应。旋转在attention内积之前分别作用于Q和K,只在各自的成对通道内作线性组合,保持向量范数,不含平移项;旋转通道与保留通道之间没有交叉混合 -
完整
head在添加mask前的attention score因而分解为
- 第一项通过
引入相对位置,第二项是保留通道的普通内积;两项共同构成attention score。完整的 256 维K均写入cache
对普通 RoPE,位置张量为 token 的位置编号。每个位置与 32 个频率分别相乘,得到该 token 的 32 个旋转角
-
保存旋转角,本例形状为[B,T,32]; 、 保存对应的cos/sin系数。方括号表示沿最后一维拼接,使 的形状成为[B,T,64]:第 、 个通道属于同一旋转对,因而需要相同的系数。unsqueeze(1)再将系数变为[B,1,T,64],供同一token的所有Q/K head复用 -
固定一个
token和一个head,记 。源码将二维旋转矩阵的乘法展开为逐元素乘加,第 、 个通道的输出为
rotate_half(q_rot)在这两个位置分别提供 和 ;乘以sin后,再加上q_rot * cos,就得到上式。cos、sin对应广播后的 、 ,K的计算相同
def rotate_half(x):
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return torch.cat((-x2, x1), dim=-1)
def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1):
cos = cos.unsqueeze(unsqueeze_dim)
sin = sin.unsqueeze(unsqueeze_dim)
# Keep half or full tensor for later concatenation
rotary_dim = cos.shape[-1]
q_rot, q_pass = q[..., :rotary_dim], q[..., rotary_dim:]
k_rot, k_pass = k[..., :rotary_dim], k[..., rotary_dim:]
# Apply rotary embeddings on the first half or full tensor
q_embed = (q_rot * cos) + (rotate_half(q_rot) * sin)
k_embed = (k_rot * cos) + (rotate_half(k_rot) * sin)
# Concatenate back to full shape
q_embed = torch.cat([q_embed, q_pass], dim=-1)
k_embed = torch.cat([k_embed, k_pass], dim=-1)
return q_embed, k_embed
交错多模态旋转位置编码(Interleaved MRoPE)流程
- 准备混合序列与
mm_tokens:处理器展开媒体占位,input_ids记录各槽位的词表编号,mm_token_type_ids标记模态;对应的媒体特征随后填入占位,与文本嵌入组成内容序列 - 生成三轴位置:
get_rope_index()根据模态类型、视觉网格和有效槽位生成T/H/W坐标;位置表的每一列与同一 token 的内容向量对应,提供其旋转所需的位置因子 - 计算三轴候选系数:三轴坐标分别乘共享频率
inv_freq,将位置因子转换为旋转角,再求cos/sin,为每对通道准备来自三个轴的候选系数 - 按通道对交错选轴:按
mrope_section规定的数量,沿频率下标将通道对交错分配给T/H/W;每对只保留对应轴的系数,最终得到 32 组确定的旋转系数 - 旋转 Q/K 的成对通道:将 32 组系数复制到 64 个旋转通道,使
(i,i+32)共用一个角度;各注意力头据此旋转投影、归一化后的Q/K,结果用于注意力内积
mm_tokens:混合序列与模态类型
-
媒体槽位与类型编号:
mm_tokens指混合序列中的媒体占位 token。用Qwen3-Omni的文本、图片、视频、音频四种输入模态举例,可以按0/1/2/3标记每个槽位类型编号 槽位内容 填入语言模型的向量 0文本、消息标记、媒体起止标记 token 的文本嵌入 1图片占位 图片编码器输出的特征 2视频占位 视频帧的视觉特征 3音频占位 音频编码器输出的特征 input_ids保存词表编号;类型编号只标记这个槽位属于哪种模态。例如一段文本后接图片区段,再接文本、视频和音频,类型可以写成0,0,1,1,1,1,0,2,2,0,3,3,3,0;连续的多个1表示同一图片区段中的多个特征槽位。Qwen3.5使用文本、图片、视频三类,类型为0/1/2,由mm_token_type_ids: [B,L]逐槽位传给get_rope_index() -
类型按占位编号生成:
create_mm_token_type_ids()先把整条序列设为0,再按图片、视频、音频占位的词表编号赋值生成多模态 Token 类型Transformers · GitHub tokenizer_input = np.array(tokenizer_input) mm_token_types = np.zeros_like(tokenizer_input) mm_token_types[np.isin(tokenizer_input, self.image_token_ids)] = 1 mm_token_types[np.isin(tokenizer_input, self.video_token_ids)] = 2 mm_token_types[np.isin(tokenizer_input, self.audio_token_ids)] = 3 -
处理器准备槽位与类型:
processor.apply_chat_template(messages, tokenize=True, return_dict=True, return_tensors="pt")按消息中的type渲染媒体标记,再由Qwen3VLProcessor按视觉特征数量展开占位。图片对应连续的<|image_pad|>;视频按时间片展开,每片带文本时间戳和连续的<|video_pad|>。B/L/D分别表示批大小、补齐后的序列长度和隐藏维度字段 结构 与位置表的关系 input_ids[B,L]确定混合序列的槽位顺序 mm_token_type_ids[B,L]文本 0、图片1、视频2,决定每段如何分配坐标image_grid_thw/video_grid_thw[媒体数量,3]提供视觉网格的时间、高度、宽度 attention_mask[B,L]标明有效槽位,排除批处理补齐位置 inputs_embeds[B,L,D]保存各槽位的文本嵌入或视觉特征 position_ids[3,B,L]为同一槽位提供 T/H/W三个坐标 -
区分媒体占位和补齐:
<|image_pad|>、<|video_pad|>是有效媒体槽位,不是 padding 或 EOS token,attention_mask为1。Qwen3.5-27B的 padding token 是<|endoftext|>,tokenizer 的 EOS 是<|im_end|>;生成配置把两者都列为停止 token。时间戳、消息标记和<|vision_start|>/<|vision_end|>的媒体类型均为0,按文本位置规则处理
get_rope_index():生成三维位置表
-
接口与输出:对
Qwen3_5ForConditionalGeneration实例model,调用model.model.get_rope_index();返回position_ids: [3,B,L]和rope_deltas: [B,1]。position_ids[:,b,l]是一个槽位的T/H/W三个坐标;函数只读取类型和网格,不读取像素调用三轴位置接口Transformers · 相关实现 · GitHub position_ids, rope_deltas = model.model.get_rope_index( input_ids=input_ids, mm_token_type_ids=mm_token_type_ids, image_grid_thw=image_grid_thw, video_grid_thw=video_grid_thw, attention_mask=attention_mask, ) -
准备网格与输出:视频已被时间戳分成多个区段,
repeat_interleave()按每段视频的时间网格长度复制网格,再把每行的时间长度设为1。图片、视频各有一个由整个批次共用的网格迭代器,按样本及媒体出现顺序消费;输出位置表先初始化为[3,B,L]展开视频帧网格Transformers · GitHub if video_grid_thw is not None: video_grid_thw = torch.repeat_interleave( video_grid_thw, video_grid_thw[:, 0], dim=0 ) video_grid_thw[:, 0] = 1 spatial_merge_size = self.config.vision_config.spatial_merge_size mrope_position_deltas = [] position_ids = torch.zeros( 3, input_ids.shape[0], input_ids.shape[1], dtype=input_ids.dtype, device=input_ids.device, ) grid_iters = { 1: iter(image_grid_thw) if image_grid_thw is not None else None, 2: iter(video_grid_thw) if video_grid_thw is not None else None, } -
过滤补齐并划分区段:对每个样本,用同一份
attention_mask同时过滤input_ids和mm_token_type_ids,保持逐位置对应。groupby()只合并连续相同的类型;两个图片区段即使都是1,中间的起止标记为0,仍会分开。start_index/end_index是过滤后序列的左闭右开区间按模态分组 TokenTransformers · GitHub for batch_idx, current_input_ids in enumerate(input_ids): input_token_type = mm_token_type_ids[batch_idx] if attention_mask is not None: current_input_ids = current_input_ids[attention_mask[batch_idx].bool()] input_token_type = input_token_type[attention_mask[batch_idx].bool()] input_type_group = [] for key, group in itertools.groupby( enumerate(input_token_type.tolist()), lambda x: x[1] ): group = list(group) start_index = group[0][0] end_index = group[-1][0] + 1 input_type_group.append((key, start_index, end_index)) -
推进位置:在上述批次循环内,为每个样本从
current_pos=0开始分配位置。start_idx/end_idx表示序列区间,current_pos表示旋转位置起点。类型0将连续编号复制到三行;类型1/2分别取下一个图片网格或视频时间片网格,生成[3,区段长度]坐标。视觉区段结束后,按合并网格的最大边长推进,不能用视觉token总数代替构造文本与视觉位置Transformers · GitHub current_pos = 0 llm_pos_ids_list = [] for modality_type, start_idx, end_idx in input_type_group: if modality_type == 0: text_len = end_idx - start_idx llm_pos_ids_list.append( torch.arange(text_len, device=input_ids.device) .view(1, -1) .expand(3, -1) + current_pos ) current_pos += text_len else: grid_thw = next(grid_iters[modality_type]) vision_position_ids = self.get_vision_position_ids( current_pos, grid_thw, 1, spatial_merge_size, device=input_ids.device, ) llm_pos_ids_list.append(vision_position_ids) current_pos += max(grid_thw[1], grid_thw[2]) // spatial_merge_size -
展平视觉网格:
grid_thw保存patch embedding后、空间合并前的网格,H/W先除以spatial_merge_size,与语言模型中的视觉槽位数量对齐。get_vision_position_ids()用meshgrid()为每个槽位分配(t,h,w),按宽度最快、高度次之、时间最慢的顺序展平;三行的同一列始终对应同一个视觉槽位。start_position将三个轴平移到当前区段起点生成视觉网格坐标Transformers · GitHub llm_grid_t, llm_grid_h, llm_grid_w = ( grid_thw[0].item() // temp_merge_size, grid_thw[1].item() // spatial_merge_size, grid_thw[2].item() // spatial_merge_size, ) position_temporal = ( torch.arange(llm_grid_t, device=device) * time_interval ).long() position_height = torch.arange(llm_grid_h, device=device) + start_position position_width = torch.arange(llm_grid_w, device=device) + start_position T_grid, H_grid, W_grid = torch.meshgrid( position_temporal, position_height, position_width, indexing="ij" ) vision_position_ids = torch.stack([T_grid, H_grid, W_grid], dim=0).reshape( 3, -1 ) vision_position_ids[0] += start_position return vision_position_ids -
视频时间片的位置:上述调用的
temp_merge_size=1、time_interval=1.0;单张图片和每个视频时间片的时间轴长度均为1,所以同一区段的T坐标相同。时间戳和起止标记按文本规则推进current_pos,下一个时间片使用新的起点;实际视频时间由时间戳文本表达 -
拼回整条序列:各段沿
dim=1拼接,得到当前样本去除补齐后的[3,有效长度]位置表;再用原始attention_mask将各列写回[3,B,L]的有效槽位。补齐位置保留初始化的0,由掩码排除。current_input_ids已经过滤,因此差值减去的是有效长度,不是补齐后的L写回三轴位置与偏移Transformers · GitHub llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1) if attention_mask is not None: position_ids[:, batch_idx, attention_mask[batch_idx].bool()] = ( llm_positions.to(position_ids.device) ) else: position_ids[:, batch_idx] = llm_positions.to(position_ids.device) mrope_position_deltas.append(llm_positions.max() + 1 - len(current_input_ids)) -
返回位置与差值:
position_ids: [3,B,L]提供每个 token 的三轴旋转坐标;rope_deltas: [B,1]保存下一个旋转位置 − 有效序列长度,用于后续生成时将序列编号对齐到 RoPE 位置,不是KV cache的存储索引。每条样本各保存一个差值返回位置表与偏移Transformers · GitHub mrope_position_deltas = torch.tensor( mrope_position_deltas, device=input_ids.device ).unsqueeze(1) return position_ids, mrope_position_deltas
三维位置表:单批次示例
-
对齐内容与位置:建立内容张量与三维坐标的对应关系。设混合序列的内容张量为
,对应inputs_embeds;位置张量为 ,对应position_ids。X[b,l,:]保存第b个样本、第l个 token 的内容向量,P[:,b,l]保存同一 token 的时间、高度、宽度坐标。视觉特征填入媒体槽位后,序列长度与槽位顺序保持不变;get_rope_index()根据模态类型和视觉网格,为这些槽位分配坐标。内容张量用于生成Q/K,位置张量用于生成旋转系数取
bs=1、L=10,单张图片的网格为[1,4,6],spatial_merge_size=2,空间合并后为[1,2,3],对应六个图片槽位。t0/t1各表示一个文本 token,S/E表示媒体起止标记,Ihw表示图片第h行、第w列的槽位。三轴位置表为槽位 l0 1 2 3 4 5 6 7 8 9 token t0SI00I01I02I10I11I12Et1T = P[0,0,l]0 1 2 2 2 2 2 2 5 6 H = P[1,0,l]0 1 2 2 2 3 3 3 5 6 W = P[2,0,l]0 1 2 3 4 2 3 4 5 6 图片区段的起点为
2。单张图片的时间坐标恒为2,高度、宽度坐标分别为区段起点加行号、列号,按宽度优先的顺序展平。区段结束后,旋转位置按最大网格边长3推进,因此E/t1的坐标为5/6。六个图片槽位的词表编号均为<|image_pad|>,各自对应不同的视觉特征和网格坐标。对I12,内容向量为X[0,7,:],位置列为P[:,0,7]=[2,3,4]。位置表中的坐标直接作为相位计算的位置因子
交错 MRoPE:按通道对选择 T/H/W 坐标
-
计算三轴候选角度:位置坐标与频率向量的外积。
Qwen3.5-27B的旋转维度为 ,对应 个二维旋转子空间;频率底数为 。轴下标 分别表示T/H/W,频率下标 对应通道对 ,inv_freq[i]记为 。每个轴的每个位置与全部频率相乘,构造候选相位张量Qwen3_5TextRotaryEmbedding.forward()将三轴共享的inv_freq扩展为[3,B,32,1],位置扩展为[3,B,1,L];相乘得到[3,B,32,L],交换最后两维得到freqs: [3,B,L,32],再求同形状的cos/sin(默认attention_scaling=1)。单样本示例中的相位与系数均为[3,1,10,32];I12的坐标[2,3,4]对应第i个子空间的三个候选角度 、 、 ,分别生成一组余弦、正弦系数 -
位置表提供旋转坐标:
position_ids是普通 RoPE 中位置 × 频率的位置输入;rope_theta决定频率底数。Qwen3.5为每个 token 保存T/H/W三个坐标,用同一组inv_freq分别计算三个轴的角度。Qwen3_5TextRotaryEmbedding.forward()中,相位与三角函数的核心语句为计算三轴旋转系数Transformers · GitHub inv_freq_expanded = ( self.inv_freq[None, None, :, None] .float() .expand(3, position_ids.shape[1], -1, 1) ) position_ids_expanded = position_ids[:, :, None, :].float() freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose( 2, 3 ) cos = freqs.cos() * self.attention_scaling sin = freqs.sin() * self.attention_scaling sin = self.recomposition_frequencies(sin) cos = self.recomposition_frequencies(cos) -
按通道对交错选轴:将三轴候选系数映射到旋转子空间。
mrope_section=[11,11,10]表示时间、高度、宽度轴分别分配11/11/10个旋转子空间。recomposition_frequencies()在频率下标0,3,6,…,30取时间轴,1,4,7,…,31取高度轴,2,5,8,…,29取宽度轴。对这组配置,定义轴选择映射 ,则每个通道对实际使用的相位与系数为选择后的
均为[B,L,32]。轴选择作用于频率维:每个二维子空间只采用一个轴的坐标,所有 token 共享同一轴分配规则。源码先计算三轴cos/sin,再按上述映射选取系数。对I12,选轴后最终采用的 32 个旋转角为2、3、4是I12的三轴位置坐标。在第i个二维子空间,选中的坐标充当一维 RoPE 中的位置因子 ,与该子空间的频率 相乘,得到实际旋转角 。例如频率下标i=2读取宽度坐标4,因此通道对(2,34)使用 和 。交错结构由频率索引到坐标轴的映射确定交错选择 T/H/W 频率Transformers · GitHub def recomposition_frequencies(self, freq): freqs_thw = freq[0] for dim, offset in enumerate((1, 2), start=1): length = self.mrope_section[dim] * 3 idx = slice(offset, length, 3) freqs_thw[..., idx] = freq[dim, ..., idx] return torch.cat((freqs_thw, freqs_thw), dim=-1)交错 MRoPE 的核心:同一 token 的 T/H/W 坐标沿频率下标交替作为位置因子。对于
I12,三轴坐标为[2,3,4],32 个子空间的位置因子依次为2、3、4、2、3、4、…;分别乘各自的频率,得到 这 32 个旋转角。每个角度生成一组cos/sin,作用于Q/K的一对通道(i,i+32),32 对共同完成 64 维特征的旋转文本 token 的三个轴共享同一位置因子。对于第
l个文本 token, ,因此任意子空间选轴后的旋转角均为 。在相同频率与通道配对下, ,与一维 RoPE 等价。 随 token 的序列位置变化, 随子空间变化 -
应用二维旋转:32 个角度作用于 64 个通道。取
I12的一个注意力头,通道从0编号;Q/K的 64 维旋转部分按(0,32)、(1,33)、…、(31,63)组成 32 对,每对使用一个旋转角。I12的位置因子按2、3、4、2、3、4、…交错排列,分别乘对应频率,得到 这 32 个角度。cos/sin各生成 32 个系数,再复制为 64 个,使配对通道共用同一角度具体取
(1,33)、(2,34)、(3,35):它们分别读取高度坐标3、宽度坐标4、时间坐标2,旋转角依次为 、 、 。Qwen3_5Attention.forward()将投影、归一化后的Q/K交给apply_rotary_pos_emb();rotate_half()将前后两半 变为 ,从而完成配对通道的乘加。以 表示Q的第j个通道,这三对通道的输出为其余 29 对通道分别代入各自的旋转角,完成整个 64 维部分的旋转;后
192维原样接回,V不参与旋转。各注意力头共享这些位置系数,分别处理自身的Q/K,旋转结果参与注意力内积。文本 token 的三轴坐标相同,例如t1为[6,6,6],则各子空间的旋转角为 ,与一维 RoPE 一致 -
交错的是坐标轴分配,配对方式仍是前后半区:选轴后剩下
[B,L,32];cat()将这组系数复制成[B,L,64],让通道i和i+32使用同一个角度,通过rotate_half()完成二维旋转。每个 token 都使用同一套坐标轴分配规则。文本的T/H/W坐标相同,得到普通一维 RoPE;图片和视频的坐标不同,各通道对分别编码时间、行、列位置
Qwen3.5:MRoPE 的接口调用
-
系数接口:
Qwen3_5TextModel.forward()→Qwen3_5TextRotaryEmbedding.forward()。文本模型接收三轴位置,通过self.rotary_emb(hidden_states, position_ids)返回position_embeddings=(cos,sin),两者均为[B,L,64],一次计算后传给各层。旋转模块用hidden_states确定设备类型与输出dtype,用position_ids和inv_freq计算角度。generate()首次准备位置时,将覆盖全部槽位的一维序列位置与三轴位置拼成[4,B,L];文本模型将首行拆为text_position_ids,用于注意力掩码构造,其余三行用于旋转系数计算。直接传入[3,B,L]时,三行均用于旋转,text_position_ids=None。核心传递语句为传递位置与旋转系数Transformers · GitHub if position_ids.ndim == 3 and position_ids.shape[0] == 4: text_position_ids = position_ids[0] position_ids = position_ids[1:] else: text_position_ids = None hidden_states = inputs_embeds position_embeddings = self.rotary_emb(hidden_states, position_ids) for i, decoder_layer in enumerate(self.layers[: self.config.num_hidden_layers]): hidden_states = decoder_layer( hidden_states, position_embeddings=position_embeddings, attention_mask=causal_mask_mapping[self.config.layer_types[i]], position_ids=text_position_ids, past_key_values=past_key_values, use_cache=use_cache, **kwargs, ) -
旋转接口:
Qwen3_5Attention.forward()→apply_rotary_pos_emb()。Qwen3_5DecoderLayer的full_attention分支将position_embeddings传给self.self_attn。注意力层先投影、归一化得到本层的Q/K,再解包系数并执行旋转;cos/sin从[B,L,64]扩展为[B,1,L,64],广播到各个注意力头,旋转后的Q/K用于注意力计算。linear_attention分支使用 Gated DeltaNet,不调用这组旋转接口。实际调用为旋转 Attention 的 Q/KTransformers · GitHub cos, sin = position_embeddings query_states, key_states = apply_rotary_pos_emb( query_states, key_states, cos, sin )
条件生成模型:输入与视觉特征填充
-
条件生成模型的入口:
Qwen3_5ForConditionalGeneration.forward()将混合输入和位置参数传给self.model,即Qwen3_5Model。该模型由self.visual视觉编码器与self.language_model文本模型组成,self.rope_deltas初始化为None,用于保存各样本的旋转位置差值。一次前向依次完成视觉编码、媒体槽位填充、位置构造与文本模型计算。入口调用为调用条件生成模型Transformers · GitHub 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, mm_token_type_ids=mm_token_type_ids, **kwargs, ) -
视觉编码:得到与媒体槽位等长的特征。
get_image_features()将像素张量转换为视觉编码器的dtype,连同grid_thw送入self.visual,取pooler_output作为合并后的视觉特征。每张图片对应的特征数为T×H×W / spatial_merge_size²,据此将特征拆成逐图片的张量。get_video_features()直接复用这一实现,传入视频像素与视频网格提取并拆分视觉特征Transformers · GitHub pixel_values = pixel_values.type(self.visual.dtype) vision_output = self.visual( pixel_values, grid_thw=image_grid_thw, return_dict=True, **kwargs ) image_embeds = vision_output.pooler_output split_sizes = ( image_grid_thw.prod(-1) // self.visual.spatial_merge_size**2 ).tolist() image_embeds = torch.split(image_embeds, split_sizes) vision_output.pooler_output = image_embeds return vision_output -
special_*_mask定位媒体槽位并校验特征数量:get_placeholder_mask()在input_ids路径中比较特殊 token 编号,先得到[B,L]布尔表,再扩展为[B,L,1],沿隐藏维广播。仅提供inputs_embeds时,则逐位置比较整条嵌入向量是否等于对应占位 token 的词嵌入。返回的两个掩码由调用方接收为image_mask/video_mask;填充前还会检查占位数 × 隐藏维度是否等于特征元素总数,不匹配即报错。图片分支的核心语句为定位与校验视觉占位符Transformers · GitHub special_image_mask = input_ids == self.config.image_token_id special_video_mask = input_ids == self.config.video_token_id n_image_tokens = special_image_mask.sum() special_image_mask = special_image_mask.unsqueeze(-1).to(inputs_embeds.device) special_video_mask = special_video_mask.unsqueeze(-1).to(inputs_embeds.device) if image_features is not None: torch_compilable_check( n_image_tokens * inputs_embeds.shape[-1] == image_features.numel(), f"Image features and image tokens do not match, " f"tokens: {n_image_tokens}, features: {image_features.shape[0]}", ) -
填入内容,保留槽位顺序:
forward()要求input_ids与inputs_embeds恰好提供一个;传入input_ids时先查词嵌入,再处理视觉分支。图片特征按原顺序拼接,并转换到inputs_embeds的设备与dtype,随后由masked_scatter()填入image_mask对应的槽位。视频分支同样填充video_mask;替换后L和 token 顺序不变,位置表仍与内容逐列对应写入图像与视频特征Transformers · GitHub if inputs_embeds is None: inputs_embeds = self.get_input_embeddings()(input_ids) if pixel_values is not None: image_outputs = self.get_image_features( pixel_values, image_grid_thw, return_dict=True, **kwargs ) image_embeds = image_outputs.pooler_output image_embeds = torch.cat(image_embeds, dim=0).to( inputs_embeds.device, inputs_embeds.dtype ) image_mask, _ = self.get_placeholder_mask( input_ids, inputs_embeds=inputs_embeds, image_features=image_embeds ) inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds) if pixel_values_videos is not None: video_outputs = self.get_video_features( pixel_values_videos, video_grid_thw, return_dict=True, **kwargs ) video_embeds = video_outputs.pooler_output video_embeds = torch.cat(video_embeds, dim=0).to( inputs_embeds.device, inputs_embeds.dtype ) _, video_mask = self.get_placeholder_mask( input_ids, inputs_embeds=inputs_embeds, video_features=video_embeds ) inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds) -
位置构造:
compute_3d_position_ids()。视觉特征填入后,未显式传入position_ids才进入位置构造;已有位置表则直接使用。传入input_ids与视觉网格时,必须同时提供mm_token_type_ids,否则抛出异常。调用为构造混合序列位置Transformers · GitHub if position_ids is None: position_ids = self.compute_3d_position_ids( input_ids=input_ids, image_grid_thw=image_grid_thw, video_grid_thw=video_grid_thw, inputs_embeds=inputs_embeds, attention_mask=attention_mask, past_key_values=past_key_values, mm_token_type_ids=mm_token_type_ids, )方法读取
past_key_values.get_seq_length(),按以下条件选择位置来源条件 位置处理 input_ids、mm_token_type_ids、视觉网格齐全,且缓存长度为0或rope_deltas尚未保存调用 get_rope_index()生成[3,B,L],将返回的差值保存到self.rope_deltas已保存 rope_deltas,且缓存长度大于0或未提供input_ids从序列编号恢复三轴位置,再加各样本的差值 其余情况 返回 None,由文本模型生成一维序列位置并扩展到三轴恢复位置时,有
attention_mask就用cumsum(-1)-1计算有效序列编号,并将补齐位置置0;否则从缓存长度开始构造arange。序列编号扩展为三行后,加上按批次对齐的rope_deltas,三轴继续使用相同的文本位置因子。显式传入位置表的调用会跳过这一方法,也不会在该路径中更新rope_deltas -
传入文本模型并返回状态:
self.language_model同时接收[B,L,D]内容张量与位置表。内容用于本层的特征投影,位置用于生成旋转系数;输出保留文本模型返回的隐藏状态与缓存,并附带rope_deltas调用文本模型并返回状态Transformers · GitHub outputs = self.language_model( input_ids=None, position_ids=position_ids, attention_mask=attention_mask, past_key_values=past_key_values, inputs_embeds=inputs_embeds, **kwargs, ) return Qwen3_5ModelOutputWithPast( **outputs, rope_deltas=self.rope_deltas, )