六种位置编码,其实只在争两个插入点

同一个 Qwen2.5-7B、同一段文本、同样是把窗口放大 4 倍:线性插值让 PPL 从 8.18 崩到 173.57,YaRN 只涨到 8.90。差别全在一张波长表——我手算出 YaRN 放过了 64 个维度对里转得最快的 30 对。附 llama.cpp 源码定位与三个 token 的置换等变手算。

一张「6 种 LLM 位置编码」的信息图摆在我面前,每种方法一条五步流水线,画得很漂亮。我没有照着讲六遍,而是把其中两种拿到本机的 Qwen2.5-7B 上跑了一遍困惑度。

同一段文本、同一个 512 的窗口、都是「把上下文放大 4 倍」这一个意图:线性插值把 PPL 从 8.18 干到 173.57,YaRN 只涨到 8.90。 一个 21 倍的崩塌,一个 8.8% 的代价。

这个 20 倍的差距不来自谁的算法更聪明。它来自一张表:128 维的注意力头拆成 64 个「维度对」,每对转得一快一慢。线性插值把 64 对全压了,YaRN 只压了后 34 对。我手算的 corr_dims = [30, 47],和 llama.cpp 里那个函数的注释严丝合缝。

所以这篇不讲「六种方法各有什么特点」。它只回答一个问题:为什么位置编码只有这么几种形状——因为 self-attention 压根看不见顺序,而你只有两个地方能把顺序塞回去。

名词速查

术语一句话解释
置换等变(permutation-equivariant)打乱输入的顺序,输出跟着同样地打乱,内容一个都不变——等于「这个函数看不见顺序」
causal mask因果掩码:把「看未来」的那些注意力分数置成 -\infty,softmax 之后权重变 0。详见站内旧文 Masked Softmax 到底在 mask 什么
维度对RoPE 把 128 维的注意力头两两配对成 64 个二维平面,每个平面按位置转一个角度。推导见 RoPE 为什么只是高中三角函数
θi\theta_i / 波长ii 个维度对每前进一个 token 转的角度;波长 = 转满一圈需要多少个 token
freq_baseRoPE 的底数,决定「快慢梯度」有多陡。Llama 2 是 10000,Qwen2.5-7B 是 1000000(本机 GGUF 实读)
freq_scale位置的缩放因子。把上下文放大 4 倍就是 freq_scale = 0.25,等于「把所有位置挤到原来的 1/4 处」
PPL(困惑度)模型对一段文本的平均意外程度,越低越好。8 和 170 的差别是「读得懂」和「读成乱码」
外推 / 插值超出训练长度时,让角度继续长(外推)还是把角度压回训练范围内(插值)

一、attention 真的看不见顺序:三个 token 的手算

先把地基打死。我用三个二维向量当 token,投影矩阵全取单位矩阵(这样读者能拿笔跟着算):

x1=[1,0],x2=[0,1],x3=[1,1]x_1 = [1, 0],\quad x_2 = [0, 1],\quad x_3 = [1, 1]

分数就是内积除以 2\sqrt{2},过 softmax,再加权求和。不加掩码时,输出是:

out = [[0.8022, 0.5989],
       [0.5989, 0.8022],
       [0.7517, 0.7517]]

现在把 x1x_1x2x_2 的位置互换再算一遍:

out(交换后) = [[0.5989, 0.8022],
               [0.8022, 0.5989],
               [0.7517, 0.7517]]

前两行原封不动地对调了,数值一个没变。 这就是置换等变:attention 只是一次「按相似度加权求和」,加权求和对求和顺序免疫。输入是一个集合,不是一个序列。

关键的转折在加上 causal mask 之后。同样两次计算,这次 P @ out ≠ out(交换后)

out(带掩码)     = [[1.0000, 0.0000],
                   [0.3302, 0.6698],
                   [0.7517, 0.7517]]
out(交换后带掩码) = [[0.0000, 1.0000],
                   [0.6698, 0.3302],
                   [0.7517, 0.7517]]

第二行从 [0.3302, 0.6698] 变成了 [0.6698, 0.3302]——不是行对调,是数值真的变了。因果掩码自己就是一种位置编码:第 1 个 token 只能看 1 个,第 2 个能看 2 个,可见集合的大小泄露了位置。这正是 NoPE(完全不加位置编码)唯一的本钱。

但请注意第三行:两次都是 [0.7517, 0.7517],一字不差。掩码只能告诉一个 token「我前面有几个、都是谁」,告诉不了它「他们谁在前」——在单层里,前缀仍然是个集合。顺序要靠多层堆叠才慢慢浮现出来,这是我认为 NoPE 只在小规模上追平显式编码的根本原因(这是我的判断,不是论文原话;NoPE 论文 的实验规模确实停在小模型)。

以上三段数字全部来自我本机的一个 20 行 numpy 脚本,读者可以逐个数复算。

二、只有两个地方能把顺序塞回去

上面的手算把约束逼出来了:token 向量进 attention 之前不带位置,attention 本身又对顺序免疫。那么整条通路上能动手的地方只剩两处——进 attention 前的那个向量,和 QK 打分那一步

flowchart TD
    X["token 向量 x_i<br>此刻没有任何位置信息"] --> ADD["插入点 ①:加向量<br>x_i + p_i"]
    ADD --> QKV["Q / K / V 投影"]
    QKV --> ROT["插入点 ②a:乘旋转<br>按位置转 Q 和 K"]
    ROT --> SCORE["打分 QK / sqrt d"]
    SCORE --> BIAS["插入点 ②b:加偏置<br>分数 + mask i j"]
    BIAS --> SM["softmax 后加权求和"]
    ADD -.-> M1["Sinusoidal 查固定表<br>Learned 查可训练表"]
    ROT -.-> M2["RoPE<br>YaRN"]
    BIAS -.-> M3["ALiBi 减去距离<br>causal mask 置负无穷"]
    SM -.-> M4["NoPE 两处都不做<br>顺序全靠 causal mask"]

这张图不是我画出来的分类法,它是 llama.cpp 的代码形状。整个仓库里,「乘旋转」这一路只有一个函数入口(以下源码位置均取自 ggml-org/llama.cpp master @ 41abbfd):

// src/models/llama4.cpp:163
Qcur = ggml_rope_ext(
        ctx0, Qcur, inp_pos, rope_factors,
        n_rot, rope_type, n_ctx_orig, freq_base_l, freq_scale_l,
        ext_factor, attn_factor, beta_fast, beta_slow
        );

RoPE 和 YaRN 不是两个函数,是同一次调用的两组参数。 后四个参数 ext_factor / attn_factor / beta_fast / beta_slow 全是 YaRN 专用;把 ext_factor 设成 0,这行就退化成纯 RoPE。六种方法里的四种,落到真实推理引擎里只是这一行的参数表加上两个 hparams 字段。

方法插入点参数/字段(llama.cpp)训练成本坏掉会怎样
Sinusoidal①加向量不走 rope 路径零参数长度超训练值时理论能外推,实测注意力糊掉
Learned①加向量一张 n_ctx × d 的权重4.7 亿参数(见下节)超出训练长度没有向量可查,不是变差是未定义
RoPE②a 乘旋转freq_base, freq_scale=1零参数超窗后角度绕圈,远距离分数变成噪声
ALiBi②b 加偏置hparams.use_alibi零参数远处 token 被线性压死,长程检索能力差
YaRN②a 乘旋转ext_factor, beta_fast/slow一次短微调配错 n_ctx_orig 时高低频分界线错位
NoPE都不做n_no_rope_layer_step零参数单层只知道「前面有几个」,顺序靠深度换

三、加法派:为什么 Learned 在 128K 时代直接出局

Learned 是最朴素的做法——给每个位置学一个向量,加到 token 向量上。它也是「反事实崩点」最干净的靶子。

拿本机这个模型算一笔账。Qwen2.5-7B 的 GGUF 元数据我实读如下(uv run --with gguf 读的,不是查文档):

qwen2.context_length     = 131072
qwen2.embedding_length   = 3584
qwen2.attention.head_count    = 28
qwen2.attention.head_count_kv = 4
qwen2.rope.freq_base     = 1000000.0

如果它用 Learned 绝对位置:

131072×3584=469,762,0484.7 亿参数131072 \times 3584 = 469{,}762{,}048 \approx 4.7\ \text{亿参数}

占 7.62B 总参数的 6.16%,fp16 下是 0.875 GiB——只为了记住「第几个」。而且这 4.7 亿花完之后,第 131073 个 token 依然没有向量可查:不是效果下降,是查表越界。

这就是崩点:Learned 的失效不是渐变的,是断崖。RoPE 在超窗时还能给出一个(错误的)角度,Learned 直接给不出数。这也解释了为什么 2021 年之后的开源大模型几乎没人再用它——不是效果不行,是它把「上下文长度」从一个推理期参数变成了一个权重形状

Sinusoidal(原始 Transformer 那一版,1706.03762)用固定的 sin/cos 公式替掉查表,从而在任意位置都有值可算。论文当年的说法是这样就能外推到更长序列;实际社区经验是外推质量很差(这是我读到的共识结论,未亲手复现 Sinusoidal 模型的外推实验)。原因在下一节的波长表里直接可见。

四、乘法派的钥匙:一张波长表

这一节是全文的枢纽。不看懂这张表,YaRN 就只能当魔法背;看懂了,YaRN 只剩三行。

RoPE 把 128 维的头拆成 64 个维度对,第 ii 对每前进一个 token 就转 θi\theta_i 弧度:

θi=base2i/d=106(i/64),λi=2πθi\theta_i = \text{base}^{-2i/d} = 10^{6 \cdot (-i/64)}, \qquad \lambda_i = \frac{2\pi}{\theta_i}

代入本机实读的 freq_base = 1000000head_dim = 128,我用一次性脚本算出(全部可复算):

维度对 ii波长 λi\lambda_i(token)在 131072 的窗口里转了几圈
06.320860.76
15160.1818.62
304080.232.12
46129026.81.016
635063255.80.026

两个数字值得盯着看。

第一,最后一对的波长是 506 万 token。 在 13 万的完整窗口里它只转了 2.6% 圈。也就是说 i=63i=63 这一对根本不是在「旋转」——它是一条单调的斜坡,编码的是绝对位置的粗略远近。而 i=0i=0 这一对在同样的窗口里转了两万多圈,它编码的是「上一个词还是上上个词」这种局部语序。RoPE 的 64 个维度对,是从「毫米尺」到「公里尺」的一整套量具。

第二,i=0i=0 这一对每个 token 转 1.000 弧度整(因为 base0=1\text{base}^0 = 1),也就是 57.3°。这个数马上要用到。

五、ALiBi 在 llama.cpp 里根本没有自己的代码

ALiBi(2108.12409)走的是插入点 ②b:不动 Q/K,直接在分数上减去一个和距离成正比的惩罚。信息图里画了五个步骤框,而在真实引擎里它是一行三目运算符

// src/llama-graph.cpp:442
data[idst + i0] = llama_cast<T>(hparams.use_alibi ? -std::abs(p0 - p1) : 0.0f);

p0p1 是两个 token 的位置。这一行填的就是 causal mask 那个张量。 不开 ALiBi 时这里写 0(对分数无影响,掩码的 -\infty 由别处写入);开了就写 ij-|i-j|

我第一次翻到这里是有点意外的:教科书把 ALiBi 讲成一个独立机制,但它在代码里和因果掩码共用同一块缓冲区、同一次遍历、同一个 kernel 参数(f_max_alibi_bias 作为斜率一路传到 ggml_flash_attn_ext,见 src/llama-graph.cpp:2632)。掩码本来就是「按位置改分数」,ALiBi 只是把里面的 0 换成了一个线性函数。 这是我理解这六种方法时最关键的一次视角转换:causal mask 不是位置编码的背景,它就是插入点 ②b 上已经躺着的那个位置编码。

顺着这个视角,ALiBi 的共同祖先也清楚了:它是把 CNN 的局部性先验(越近越重要)重新写回了 attention。Transformer 当年砍掉的就是这个假设(AI 顶级原理望远镜 里第 6 条「归纳偏置」的典型案例:CNN 假设局部、Transformer 假设万物关联),ALiBi 又把它以一个可控斜率的形式请回来。代价也在同一处:远距离信息被线性压制,长程精确检索是它公认的弱项。

六、YaRN:beta_fast / beta_slow 的单位是「圈」

现在回到波长表。要把窗口放大 4 倍,最朴素的做法是 Position Interpolation(2306.15595):把所有位置除以 4,即 freq_scale = 0.25

看一眼 i=0i=0 就知道要出事。 原本相邻两个 token 差 1.000 弧度(57.3°),压完只差 0.250 弧度(14.3°)。更要命的是:4×0.25=1.04 \times 0.25 = 1.0——在压缩后的模型眼里,「隔 4 个 token」长得和它原来见过的「隔 1 个 token」一模一样。 模型最精细的那把毫米尺,被当成公里尺用了。

YaRN(2309.00071)的全部想法就是:别碰快的,只压慢的。 快维度对负责局部语序,反正它们在 4 倍窗口里也转得过来;慢维度对本来连一圈都没转完,压它们不丢分辨率。

那分界线画在哪?答案在 ggml 里那个带注释的函数:

// ggml/src/ggml.c:4468-4471
// Apparently solving `n_rot = 2pi * x * base^((2 * max_pos_emb) / n_dims)` for x, we get
// `corr_dim(n_rot) = n_dims * log(max_pos_emb / (n_rot * 2pi)) / (2 * log(base))`
static float ggml_rope_yarn_corr_dim(int n_dims, int n_ctx_orig, float n_rot, float base) {
    return n_dims * logf(n_ctx_orig / (n_rot * 2 * (float)M_PI)) / (2 * logf(base));
}

我把本机的参数代进去手算(n_dims=128, n_ctx_orig=131072, base=1e6beta_fast/slow 取 llama.cpp 默认的 32 和 1):

corr_dim(32)=30.018    =30,corr_dim(1)=46.073    =47\text{corr\_dim}(32) = 30.018 \;\to\; \lfloor\cdot\rfloor = 30, \qquad \text{corr\_dim}(1) = 46.073 \;\to\; \lceil\cdot\rceil = 47

然后我做了一个对账,结果让这两个超参一下子有了物理意义:

  • λ30=4080.2\lambda_{30} = 4080.2,在窗口里转 32.12 圈 —— 正好是 beta_fast = 32
  • λ46=129026.8\lambda_{46} = 129026.8,在窗口里转 1.016 圈 —— 正好是 beta_slow = 1

beta_fastbeta_slow 的单位是「在训练窗口里转了多少圈」。 beta_fast=32 的意思是「一个训练窗口内至少转满 32 圈的维度对,判定为局部尺,一律不动」;beta_slow=1 是「连一圈都没转完的,判定为绝对位置尺,可以放心压」。这句话我在任何文档里都没读到,是把注释里那条公式反解出来才对上的。

于是 64 个维度对被切成三段:

维度对范围数量YaRN 怎么处理
0 – 2930 对(46.9%)完全不动,原样外推
30 – 4617 对按 ramp 线性混合
47 – 6317 对完全按 freq_scale 插值

对应的实现是 20 行,混合权重就是一个斜坡:

// ggml/src/ggml-cpu/ops.cpp:5951-5972(有删减)
static float rope_yarn_ramp(const float low, const float high, const int i0) {
    const float y = (i0 / 2 - low) / MAX(0.001f, high - low);
    return 1 - MIN(1, MAX(0, y));
}

static void rope_yarn(float theta_extrap, float freq_scale, float corr_dims[2], int64_t i0,
                      float ext_factor, float mscale, float * cos_theta, float * sin_theta) {
    float theta_interp = freq_scale * theta_extrap;
    float theta = theta_interp;
    if (ext_factor != 0.0f) {
        float ramp_mix = rope_yarn_ramp(corr_dims[0], corr_dims[1], i0) * ext_factor;
        theta = theta_interp * (1 - ramp_mix) + theta_extrap * ramp_mix;
        mscale *= 1.0f + 0.1f * logf(1.0f / freq_scale);   // 注意力温度
    }
    *cos_theta = cosf(theta) * mscale;
    *sin_theta = sinf(theta) * mscale;
}

ramp_mix = 1theta = theta_extrap(原样不动),= 0theta = theta_interp(全压)。最后那个 mscale 就是信息图里画的「Scale Attention Temp」——放大窗口后 token 变多,softmax 前的分数需要整体抬一点,否则注意力被摊平。

我踩的坑--rope-scaling yarn 单独给是不够的,必须同时给 --rope-scale N;而且 ext_factor 到底取了多少,llama.cpp 在默认日志级别下一行都不打印。我是翻到 src/llama-context.cpp:172-173 才确认它在 YaRN 模式下默认取 1.0:

if (cparams.yarn_ext_factor < 0.0f) { // negative indicates 'not set'
    cparams.yarn_ext_factor = cparams.rope_scaling_type == LLAMA_ROPE_SCALING_TYPE_YARN ? 1.0f : 0.0f;
}

顺带说一个软处:llama.cpp 这里的 n_ctx_orig 默认取 GGUF 里的 context_length,也就是 131072。如果一个模型真实的预训练窗口更短(Qwen2.5 公开资料里的基座窗口是 32768,用 YaRN 扩到 128K),那分界线会整体左移——我用 32768 重算了一遍,corr_dims 变成 [23, 40],被完全保护的维度对从 30 对降到 23 对。这个参数配错不会报错,只会悄悄让分界线错位,是我认为 YaRN 最容易踩的坑。

七、实测:21 倍崩塌 vs 8.8% 代价

上面全是推理,现在上数字。这些都是我在本机跑的,不是引用论文。

设备是 M5 Pro / 24 GB 统一内存,模型 Qwen2.5-7B-Instruct-Q4_K_M,语料是 Project Gutenberg 的《傲慢与偏见》纯文本前 400 KB,窗口 512,100 个 chunk。命令形如:

llama-perplexity -m Qwen2.5-7B-Instruct-Q4_K_M.gguf -f ppl-input.txt \
  -c 512 --chunks 100 --no-warmup --rope-scaling yarn --rope-scale 4

注意一个关键设计:窗口固定在 512,全部在原生长度内。我测的不是「谁能撑到 128K」,而是「为了撑到 128K 所做的改动,在正常长度上先付了多少钱」。

配置PPL(100 chunk)相对原样
原样(freq_base=1e6, scale=1)8.1792 ± 0.1261.00×
线性插值 PI ×4173.5671 ± 3.27921.2×
YaRN ×48.8980 ± 0.1421.09×
线性插值 PI ×8278.1536 ± 5.31534.0×
YaRN ×89.7356 ± 0.1601.19×
freq_base 改成 1000011.3150 ± 0.1881.38×
冻结旋转(freq_base=1e30)322.8144 ± 6.03139.5×

三条读法:

第一,PI 的崩塌不是「长文本上表现差」,是当场变成乱码。 PPL 173 意味着模型在读一句正常英文散文时都严重迷路——而这段文本每个 chunk 只有 512 token,远在原生窗口内。第四节那个算式就是原因:所有 64 对被同比压缩,毫米尺当公里尺用,局部语序首先糊掉。

第二,YaRN ×4 只涨 8.8%,而且它连微调都没做过。 保住 30 对高频维度,就保住了绝大部分能力。这 8.8% 正是 YaRN 论文要用「一次短微调」抹掉的那部分。两个数字的对比(21.2× vs 1.09×)就是 YaRN 这篇论文全部的存在理由,而它们之间的因果链完全落在第四节那张波长表上。

第三,最后一行是我自己设计的一个近似,需要打折看。freq_base 设成 103010^{30} 会让所有 θi0\theta_i \to 0,旋转退化成恒等变换——等于在推理期把 RoPE 摘掉。结果 PPL 322.81,比 PI ×8 还糟。这个数只能说明「一个靠 RoPE 训出来的模型被抽掉 RoPE 会死」,它不能说明 NoPE 不行——NoPE 是从头就不用位置编码来训练,模型会把顺序信息压到别的通路上。拿这个数去否定 NoPE 是偷换概念,我自己一开始差点这么写。

顺便一个意外收获:freq_base 从 1e6 改成 10000(假装它是个 Llama 2 时代的模型)只让 PPL 涨 38%,远没有插值那么致命。底数选错是「量具刻度不对」,插值是「把量具本身压短了」。

八、NoPE 不是学术玩具:它是 Llama 4 里的一个整数

最后一种方法最容易被当成消融实验。但它已经在生产代码里了:

// src/models/llama4.cpp:145-146
const bool use_rope = hparams.n_no_rope_layer_step > 0 &&
                      (il + 1) % hparams.n_no_rope_layer_step != 0;

n_no_rope_layer_step 的默认值是 4(src/llama-hparams.h:252)。也就是说 Llama 4 每 4 层就有 1 层完全不加位置编码,整整 25% 的注意力层在 NoPE 模式下跑。

更有意思的是那一层改成做什么了:

if (use_rope) {
    Qcur = ggml_rope_ext(/* ... */);
    Kcur = ggml_rope_ext(/* ... */);
} else if (inp_attn_scale) {
    Qcur = ggml_mul(ctx0, Qcur, inp_attn_scale);   // 换成按长度缩放 Q
}

不加旋转的那一层,改成给 Q 乘一个随长度变化的缩放因子。同一个插入点 ②a,一层用「转多少度」编码位置,另一层用「分数放大多少」应对长度——两种手段在同一个乘法槽位里轮换。

架构侧还有两个字段值得记:rope_patternsrc/llama-hparams.h:168,逐层的 RoPE 开关数组,注释写着「默认所有层都用 RoPE」)和 has_rope(il)src/llama-hparams.cpp:333,逐层查询)。「这一层要不要位置编码」已经是一个逐层可配的布尔量了,不再是模型级的全局选择。这是我翻这遍源码最大的收获:六种方法在工程上不是六选一,是可以按层混搭的。

小结

  • 根本约束:self-attention 的加权求和对输入顺序置换等变(我用三个 token 手算验过),所以顺序必须从外面注入;而整条通路上只有两个注入口——进 attention 前的向量、和 QK 打分那一步。六种方法就是这两个口的全部玩法:①加向量(Sinusoidal / Learned)、②a 乘旋转(RoPE / YaRN)、②b 加偏置(ALiBi / causal mask)、或者都不做(NoPE)。如果这个约束不存在(换成 RNN 或 CNN,顺序天然写在结构里),位置编码这个概念会整个消失——RNN 时代确实没人做它。
  • 崩点:最朴素的 Learned 绝对位置,在 Qwen2.5-7B 的 131072 窗口下要花 4.7 亿参数(总参数的 6.16%),而且第 131073 个 token 直接查表越界;RoPE 超窗只是角度绕圈,Learned 是未定义。
  • 可带走的动作:下次看到任何位置编码新方法,先问「它动的是哪个插入点、动的是快维度还是慢维度」。我实测线性插值 ×4 让 PPL 从 8.18 涨到 173.57,YaRN ×4 只到 8.90,两者唯一的区别就是后者放过了 64 个维度对里转得最快的 30 对。

通俗总结

把注意力机制想象成一场圆桌会议,所有人同时发言,而且会议记录不记录发言顺序——你只知道谁说了什么、谁和谁观点接近,但翻遍记录都找不到「谁先开口」。这是 Transformer 刻意的设计(放弃顺序假设,换来任意两个词直接对话的能力),代价就是顺序得另找地方补回来。

补的地方只有两个:要么在每个人进门时给他别一枚号码牌(这是加法派:固定号码牌叫 Sinusoidal,可训练的号码牌叫 Learned),要么在记录两人对话热度时按座位远近打折(这是打分派:把两人的发言方向按座位各转一个角度,叫 RoPE;直接按距离扣分,叫 ALiBi)。

现在会议室要从 100 人扩到 400 人。有两个改法:

  • 笨办法:把所有人的座位号除以 4,硬塞进原来的编号范围。问题是隔壁的人和隔了三个座位的人,编号差变得几乎一样——连「谁挨着我」都分不清了。这就是那个 21 倍的崩塌。
  • YaRN 的办法:会议室里既有「分辨邻座」的细刻度,也有「分辨对面还是隔壁桌」的粗刻度。它只压粗刻度,细刻度一动不动。代价 8.8%,还能靠一次短培训抹掉。

一句能复述给同事的话:扩上下文不能把尺子整根压短,只能压掉那些本来就没用满的粗刻度——我在本机实测过,压全部尺子的困惑度是压对的那种的 19 倍。

参考来源

arXiv 论文(ID 与标题均在本次写作时逐个回查核实)

工程实践(源码位置取自 ggml-org/llama.cpp master @ 41abbfd

  • ggml/src/ggml.c:4468-4481 —— ggml_rope_yarn_corr_dimcorr_dims 的计算
  • ggml/src/ggml-cpu/ops.cpp:5951-5972 —— rope_yarn_ramp / rope_yarn 的 20 行实现
  • src/llama-graph.cpp:442 —— ALiBi 与 causal mask 共用一个张量的那一行
  • src/llama-context.cpp:164-213 —— ext_factor / attn_factor 的默认值推导
  • src/llama-hparams.h:168,212,227,252src/llama-hparams.cpp:333 —— rope_pattern / use_alibi / n_no_rope_layer_step / has_rope
  • src/models/llama4.cpp:145-175 —— NoPE 层的逐层开关与 Q 缩放替代路径

站内相关