Interleaved Rotary Position Embedding

RoPE:旋转位置编码

RoPE(Rotary Position Embedding)通过旋转 Q/K 特征,使 attention 内积包含相对位置信息。设旋转维度为偶数 ,频率底数为 ,位置为 ;第 个二维子空间的角频率与旋转角为

  • 对应 rope_theta,源码变量名为 base,是生成频率序列的底数

  • 当 时,频率以公比 递减:,后续频率逐渐降低。增大 base 会拉长 各分量的周期

  • inv_freq[i] 表示第 对旋转特征每单位位置的相位增量,实际旋转角为 position_id * inv_freq[i]

RoPE 配置示例Transformers · 配置定义 · GitHub
{
  "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 与旋转频率维度
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 个频率

生成旋转频率Transformers · GitHub
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)对全部 维旋转。令 、,以从 0 开始的索引写为

  • 每个 块作用于 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 的计算相同
旋转 Q/K 特征Transformers · GitHub
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)流程

  1. 准备混合序列与 mm_tokens:处理器展开媒体占位,input_ids 记录各槽位的词表编号,mm_token_type_ids 标记模态;对应的媒体特征随后填入占位,与文本嵌入组成内容序列
  2. 生成三轴位置:get_rope_index() 根据模态类型、视觉网格和有效槽位生成 T/H/W 坐标;位置表的每一列与同一 token 的内容向量对应,提供其旋转所需的位置因子
  3. 计算三轴候选系数:三轴坐标分别乘共享频率 inv_freq,将位置因子转换为旋转角,再求 cos/sin,为每对通道准备来自三个轴的候选系数
  4. 按通道对交错选轴:按 mrope_section 规定的数量,沿频率下标将通道对交错分配给 T/H/W;每对只保留对应轴的系数,最终得到 32 组确定的旋转系数
  5. 旋转 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 列的槽位。三轴位置表为

    槽位 l 0 1 2 3 4 5 6 7 8 9
    token t0 S I00 I01 I02 I10 I11 I12 E t1
    T = 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,
    )