Qwen3.8-27B 的 RoPE 维度 64 是怎么来的:一个乘法和一次求和
Qwen3.8-27B 的 RoPE 作用在 64 个维度上。这个数字在讨论中出现得很频繁,但如果你打开 config.json 去搜「64」,找不到任何一个字段直接写着它。
这不是配置文件写漏了。64 是算出来的,而且能从两条互相独立的路径算出来——两条路都通向同一个数。
这篇把这两条路走一遍。
这篇的依据
事实来源两处,采集时间 2026-08-24:
- Hugging Face 上
Qwen/Qwen3.8-27B的config.json - llama.cpp 主干仓库
conversion/qwen.py中的相关注释
我们没有加载过模型、没有验证过实际的位置编码行为。 下面是配置字段之间的算术关系,以及框架源码里的对应说明。
先看那组配置
text_config.rope_parameters 的完整内容是:
{
"mrope_interleaved": true,
"mrope_section": [11, 11, 10],
"partial_rotary_factor": 0.25,
"rope_theta": 10000000,
"rope_type": "default"
}
五个字段,没有一个叫 rope_dim 之类的东西。但其中两个字段各自藏着答案。
路径一:部分旋转因子乘以头维度
关键字段是 partial_rotary_factor: 0.25。
「部分旋转」的意思是:RoPE 不作用于注意力头的全部维度,只作用于其中一部分。 这个因子说的就是那一部分占多大比例。
Qwen3.8-27B 的 head_dim 是 256(这个值以及 24 个 Q 头配 4 个 KV 头的结构,见 GQA 那篇)。乘起来:
256 × 0.25 = 64
这就是第一条路径。 每个注意力头有 256 个维度,其中 64 个参与旋转位置编码,剩下 192 个不参与。
这种设计不算罕见——把一部分维度留给位置信息、另一部分留给纯内容表示,是位置编码的一种常见取舍。但它带来的一个直接后果是:你没法从「head_dim 是多少」直接推出「RoPE 作用在多少维上」,必须把这个因子一起考虑进去。
如果之前你按「RoPE 维度等于 head_dim」的默认假设去理解这个模型,那么得到的会是 256,而不是 64。这大概就是这个数字容易让人困惑的原因。
路径二:多维分段之和的两倍
第二条路径走的是 mrope_section: [11, 11, 10]。
MRoPE 是为多模态设计的位置编码——文本只有一个序列位置维度,而图像有高和宽、视频还多一个时间维度。MRoPE 把可用的旋转维度切成几段,分给不同的位置维度。
这三段加起来:
11 + 11 + 10 = 32
RoPE 的旋转是按维度对进行的——每两个维度组成一对,一起做一次二维旋转。所以 32 个维度对,对应的实际维度数是:
32 × 2 = 64
又是 64。
两条路径用的是完全不同的字段(一个用 partial_rotary_factor 和 head_dim,另一个用 mrope_section),走的是完全不同的算法(一个是乘法,一个是求和再翻倍),结果却完全一致。
这种交叉印证很有说服力。 单看任何一条,你都可能怀疑自己算错了;两条对上,基本可以确认 64 这个数没问题。
llama.cpp 那边的旁证
第三个来源也指向同一处。llama.cpp 的转换代码里有段注释:
Qwen3.5 always applies interleaved MRoPE (see Qwen3_5RotaryEmbedding in transformers); the upstream default mrope_section is [11, 11, 10]…
两点对得上:
「always applies interleaved MRoPE」 对应配置里的 mrope_interleaved: true。交错式的意思是几个位置维度的旋转在维度上交错排列,而不是分块堆叠。
「upstream default mrope_section is [11, 11, 10]」 与配置文件里的值完全一致。
不过 llama.cpp 实际写进 GGUF 的是四段 [11, 11, 10, 0]——它的加载器把这个字段当必填,而且要求四段,所以补了个 0(这段处理的细节见 llama.cpp 转 GGUF 时改了什么)。
补的那一段是 0,所以不影响总和,算出来还是 64。三个来源,一个数。
顺带解决另一个疑问:为什么不是 YaRN
rope_parameters 里还有一个字段值得单独说:
"rope_type": "default"
写的是 default,不是 yarn。
这个模型的 YaRN 相关说法一直有点乱——YaRN 三处配置对不上 那篇梳理过这个问题。现在从 rope_type 这个字段能得到一条明确的信息:在这份默认配置里,RoPE 走的是标准路径,没有启用 YaRN 缩放。
这与 model card 的说法并不矛盾。YaRN 通常是扩展上下文时才启用的可选项——你要跑超出训练长度的上下文,才需要手动改配置把它打开。默认配置里不开,是很常规的做法。
所以正确的理解是:默认不开 YaRN,需要时自己开。 而不是「这个模型不支持 YaRN」或者「YaRN 一直开着」。
另外那个 rope_theta: 10000000(一千万)也值得留意。这个基数比早期模型常见的 10000 大了三个数量级——大基数是长上下文模型的典型特征,它让位置编码在很长的序列上仍然能保持区分度。这与这个模型 262144 的 最大位置长度 是配套的。
为什么部分旋转值得单独理解
partial_rotary_factor 这个字段容易被跳过,但它对理解模型行为其实挺关键的。
回想 RoPE 的作用:它通过旋转来给向量注入位置信息,让模型知道两个 token 之间隔了多远。如果所有维度都参与旋转,那么整个表示都被位置「染色」了。
部分旋转的做法是把维度分成两拨:一拨参与旋转,携带位置相关的信息;另一拨完全不动,保留纯粹的内容表示。对 Qwen3.8-27B 来说,256 维里 64 维旋转、192 维不动,旋转部分只占四分之一。
这个比例的意义可以从两头看。旋转维度少意味着位置信息占的比重小,剩下更多容量留给内容语义。不旋转的维度多意味着有大量表示是位置无关的——同一个 token 出现在序列开头还是结尾,这部分表示是一样的。
这两种取舍谁更好,不是本文能回答的问题,需要实测和消融实验,我们没做。但知道这个比例的存在,至少能让你在读到「这个模型的 RoPE 维度」时,多问一句「说的是参与旋转的那部分,还是 head_dim 全部」。
另外这也解释了一件事:为什么单看 head_dim = 256 会觉得这个模型的注意力头「特别宽」。256 确实比常见的 128 大一倍,但其中只有 64 维参与位置编码——从位置编码的角度看,它反而是偏窄的那一类。 两个视角得出的印象正好相反,取决于你在看哪个数。
这些数字之间的关系
把配置里这几个字段的关系理一遍:
head_dim 256 是每个注意力头的总维度。乘以 partial_rotary_factor 0.25,得到 64——参与旋转的维度数。这 64 维按对折半得到 32 个维度对,被 mrope_section 切成 11、11、10 三段,分给不同的位置维度。mrope_interleaved 说这三段在维度上是交错排布的。rope_theta 一千万决定旋转的频率基数,rope_type 为 default 说明不做额外缩放。
六个字段,描述的是同一套机制的六个侧面。 单看任何一个都不完整,合起来才是这个模型位置编码的全貌。
小结
config.json里没有任何字段直接写着 64,它是算出来的- 路径一:
head_dim256 ×partial_rotary_factor0.25 = 64 - 路径二:
mrope_section三段和 32,按维度对翻倍 = 64 - llama.cpp 的注释是第三个旁证,它写四段是因为自家加载器要求补零,总和不变
rope_type是default不是yarn,说明默认不启用 YaRN,需要时手动开rope_theta一千万是长上下文模型的典型配置
最值得带走的一条:看到「某某模型 RoPE 是多少维」这类说法,别默认它等于 head_dim。先查有没有 partial_rotary_factor——有这个字段的话,实际维度是两者相乘的结果,可能只有你以为的四分之一。
更多拆解在 Qwen3.8-27B 专题。