0.8MB 跑通 Qwen|第 8-1 篇:大模型推理的自注意力数学——Q·K^T / softmax / V

简介: 本文精讲自注意力三步核心:Q·K^T打分、softmax归一化、加权V求和,逐行对照纯C引擎源码(vllm_transformer.c),剖析√d缩放、因果掩码与数值稳定性等工程地雷,聚焦Qwen3-VL系列在RK3588上的零依赖推理实现。(239字)

系列:《0.8MB 跑通 Qwen:从零实现 ARM 零依赖纯 C 推理引擎》(30 天 × 90 篇) | 适配模型:Qwen3-VL-8B-Instruct(千问3_VL_8B_Instruct)· Qwen3-VL-2B-Instruct · Qwen3-30B-A3B | 测试设备:RK3588(4×Cortex-A76 + 4×Cortex-A55,aarch64)

系列总纲:《0.8MB 跑通 Qwen》30 天 90 篇 · 总纲(阿里云社区)

上一篇:7-3《预热与测量抖动》 | 下一篇:8-2《在线 softmax:为什么不能先算完 e^x 再除》

源码精读篇:本文为源码/方法论精读,无独立实测;文中数字均引述仓库 docs 的板端实测记录

一句话导读:大模型推理的自注意力数学:Q·K^T、softmax、加权 V 三步公式,对照引擎朴素参考实现逐行对应因果掩码与√d 缩放,讲清每一步藏着的数值与工程地雷。

关键词:手搓 Qwen 推理引擎、千问大模型推理、Qwen3-VL、零依赖纯 C、自注意力、Q·K^T、softmax、KV cache

导语:回到大模型的心脏:自注意力。它只有 Q·K^T、softmax、加权 V 三步,却每一步都埋着数值与工程的地雷——为什么除以 √d、softmax 的"赢家通吃"意味着什么、V 为何只被挑不参与打分。这篇用手搓 Qwen 推理引擎里最朴素的参考实现把数学逐行讲明白。

量化、矩阵、线程都齐了,今天回到大模型的心脏:自注意力。它只有三步——Q·K^T、softmax、加权 V——但每一步都藏着数值与工程的地雷。先用最朴素的实现把数学读懂,再看引擎怎么在代码里落地。

1. 知识点:三步公式

对第 t 个位置的 query,注意力要"回看"前面所有已缓存的 token:

score(pos) = (Q_t · K_pos) / sqrt(d)    ① 点积打分(d = head_dim,防数值爆炸的缩放)
w(pos)     = softmax(score(0..t))[pos]  ② 归一化成权重
O_t        = Σ_pos w(pos) · V_pos        ③ 加权求和输出
  • ① 为什么除 √d:Q、K 各维度独立同分布时,点积的量级随 d 增长(约 √d)。不缩放,长维度时分数会越来越大,softmax 输入跑到 exp 的饱和/溢出区(8-2 专门讲);
  • ② softmax 是"赢家通吃":把分数变成 0–1 权重,让高分 token 主导输出;
  • ③ 加权 V:输出 = 缓存的 V 按注意力权重混合。V 不参与打分——它只被"挑"。

2. 对应代码:引擎里最朴素的参考实现

vllm_transformer.c 有一段"教科书式"的因果注意力(第 77–128 行),三步与公式一一对应:

/* Scaled Dot-Product Attention with Causal Mask & KV-Cache
 * For position `cur_pos` (the token we're generating):
 *   2. Compute scores = Q[cur_pos] @ K[0..cur_pos]^T / sqrt(head_dim) */
float scale = 1.0f / sqrtf((float)head_dim);
for (int h = 0; h < num_heads; h++) {
   
    const float *qh = q + h * head_dim;
    float max_score = -1e9f;
    for (int t = 0; t < seq_len; t++) {
   
        const float *kh = k_cache + t * kv_dim + h * head_dim;   /* ① K 缓存行 */
        float dot = 0.0f;
        for (int d = 0; d < head_dim; d++) dot += qh[d] * kh[d];
        scores[t] = dot * scale;                                  /* ÷√d */
        if (scores[t] > max_score) max_score = scores[t];
    }
    /* Softmax:先减 max 再 exp(数值稳定,8-2 细讲) */
    float sum_exp = 0.0f;
    for (int t = 0; t < seq_len; t++) {
    scores[t] = expf(scores[t] - max_score); sum_exp += scores[t]; }
    for (int t = 0; t < seq_len; t++) scores[t] /= sum_exp;      /* ② softmax */
    /* Weighted sum of V */
    for (int d = 0; d < head_dim; d++) {
   
        float val = 0.0f;
        for (int t = 0; t < seq_len; t++) {
   
            const float *vh = v_cache + t * kv_dim + h * head_dim;
            val += scores[t] * vh[d];                             /* ③ Σ w·V */
        }
        output[h * head_dim + d] = val;
    }
}

注意两个"又朴素又正确"的细节(它们就是后面所有优化版的"及格线"):

  1. 因果:只回看 t ≤ 当前(scores 数组长度 seq_len ≤ cur_pos+1),不偷看未来;
  2. 一次读一行 KV:k_cache + t*kv_dim + h*head_dim 表示"第 t 个 token、第 h 个头"的连续 head_dim 个元素——KV 是 token 优先(token-major)排布的,8-3 展开它的内存含义。

同样把这三步写在"paged 缓存"变体里的是 vllm_attention.c 的 exact_attention(第 44–94 行:逐块 kvcache_read → 打分 → softmax_normalize → 加权 V),数学完全一致,只是取 KV 走块表(block table)。

真实推理不会跑这个三重循环(太慢),会换成 q8 KV + NEON 单遍版(Day 9)。但参考实现的价值就是"慢而绝对对"——优化版全部以它为对拍对象(5-3 那把 1e-7 的尺子)。

3. 改动后果:去掉 √d 缩放,看分数漂哪去

数学上这一步最容易被"优化掉":把 dot * scale 的 scale 去掉,只留 dot。看起来"反正是线性缩放,softmax 里会被约掉"——对单个分数成立,对整条分数不成立:softmax 的分母是 Σ exp,去掉 scale 后 exp 的输入整体变大,逐步逼近 exp 的溢出阈值(8-2 的数据会给出:fp32 里 exp(89) 已经 inf)。维度越大(d=128 时点积量级 ~±20–30 甚至更高),越危险。

纪律:1/√d 是公式的一部分,不是可选的。删掉它,短上下文可能看不出问题,长上下文/大数值一上来就 NaN——这种 bug 只在压力测试里显形,最阴险。

4. 学员调试任务

  • A 档(板端动手):跑 --test-l3/--bench-mixed 之外的引擎自检前,先用纸笔算一个"2 头 × 4 token"的示例(自己给 Q/K/V 数字):手算 score 三行、softmax 权重四列、加权 O 一行,再对照代码第 98–126 行逐步核对。
  • B 档(纯读源码):读 vllm_transformer.c 第 77–128 行与 vllm_attention.c 第 44–94 行,标出两版各自对应的公式 ①②③ 行号,指出"因果在哪一行体现、√d 在哪一行乘"。

预期输出:你能脱稿写出三步公式,并指出引擎参考实现里"打分/归一化/加权 V"与"因果/KV 读取"各在哪几行。

收尾

  • 本篇源码点名:vllm_transformer.c(scaled dot-product attention 第 77–128 行)、vllm_attention.c(exact_attention 第 44–94 行)。
  • 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
  • 下篇预告:公式里那句"先减 max 再 exp"不是洁癖——下一篇 8-2 亲手把 e^x 算爆,看 naive softmax 怎么在大上下文里变 NaN,以及引擎的在线版本怎么绕开它。
相关文章
|
2天前
|
定位技术 Python
0.8MB 跑通 Qwen|第 3-2 篇:推理引擎的字节序与十六进制纪律——一个字节序错误,引擎当场翻脸
本文以RK3588真机实测为基础,深入剖析字节序陷阱:揭示VQF格式中小端落盘导致魔数“VQFW”在磁盘呈现为“FQFW”,并用篡改version字段的实验直观展示——小端机器误读大端数据会直接拒载(报错33554432≠2)。强调“先问字节序,再读数字”的十六进制读法铁律。(239字)
0.8MB 跑通 Qwen|第 3-2 篇:推理引擎的字节序与十六进制纪律——一个字节序错误,引擎当场翻脸
|
1天前
|
缓存 C语言 C++
0.8MB 跑通 Qwen|第 9-3 篇:推理引擎的 q8 KV 精度对照——量化进注意力,输出差多少
本系列《0.8MB跑通Qwen》手搓零依赖纯C推理引擎,适配Qwen3-VL多尺寸模型,在RK3588上实测q8 KV量化:输出误差仅≈0.01,端到端PPL损失<1%,带宽降为52%,精度与效率达成教科书级平衡。(239字)
|
1天前
|
JSON C语言 数据安全/隐私保护
0.8MB 跑通 Qwen|第 16-1 篇:VQF 的野心——推理引擎如何把量化与布局固化到文件
本系列手搓零依赖纯C推理引擎,仅0.8MB即可在RK3588上跑通Qwen3-VL多模态大模型。核心创新VQF格式将量化类型、内核布局与模型属性固化于2.17GB单文件中,实现mmap直挂、零转换加载,真正达成“加载即用”。
|
1天前
|
C++ 芯片
0.8MB 跑通 Qwen|第 6-2 篇:推理引擎的 8x8 布局重排——把"取数"提前到转换期
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型,在RK3588芯片实测落地。核心创新是Q8权重的8×8内存重排,使宽矩阵GEMM提速约1.7倍,且位级结果一致,兼顾性能与精度。(239字)
|
1天前
|
编解码 应用服务中间件 API
# 0.8MB 跑通 Qwen|第 19-3 篇:视频帧与媒体模块——推理引擎默认不启用的 H.264 独立模块
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型(2B/8B/30B),在RK3588上实测。视频支持分两层:帧序列路径已落地;MP4/H.264解码为独立未完成模块,API清晰、默认不构建、不演示未验证功能,体现严谨工程边界。(239字)
|
1天前
|
NoSQL 调度 C++
0.8MB 跑通 Qwen|第 14-3 篇:推理引擎的批处理正确性边界——margin 0.71 vs 0.032
本篇精读Qwen3-VL系列推理引擎的“位级一致性”本质:批量与串行输出大多逐字节一致,但因浮点累加顺序差异,在near-tie(近平局)场景下存在翻盘风险——margin(如0.71 vs 0.032)决定是否一致。这是工程结果,非数学承诺。
|
1天前
|
Java Linux 调度
0.8MB 跑通 Qwen|第 4-3 篇:核绑定实验——推理引擎在 ARM 大小核上为什么叮嘱"勿设 8 线程"
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实测RK3588(4×A76+4×A55)上适配Qwen3-VL-2B/8B及30B-A3B模型。通过三组真机实验揭示“核多≠快”本质:仅绑4大核最优,设8线程反降效12%,验证大小核架构下线程配置需严守硬件特性。(239字)
|
1天前
|
JSON 自然语言处理 算法
0.8MB 跑通 Qwen|第 18-1 篇:BPE 入门与 tokenizer.json——为什么推理引擎不直接跑 BPE
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多尺寸模型,在RK3588上实测通过。本文详解分词器工程近似方案:以“字节流最长前缀+空格标记”替代官方BPE merge,三方对拍15条语料达成9/15一致,并精准归因差异为“贪心vs排序”与“空格丢失”两类机制,践行技术诚实。(239字)
|
1天前
0.8MB 跑通 Qwen|第 13-1 篇:推理引擎的 decode 为什么慢——逐词、带宽、不可并行
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型,在RK3588上实测decode瓶颈——逐词生成、内存带宽受限、无法并行。直击每词92ms硬地板,为推测解码(speculative decode)铺路,实现“一次前向多产出”。
|
1天前
|
缓存 安全 API
0.8MB 跑通 Qwen|第 11-2 篇:大模型推理的前缀缓存键——怎么知道"两轮一样"?
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实测RK3588上高效运行Qwen3-VL多模态模型。本文详解前缀缓存核心机制:以token序列最长公共前缀(LCP)为键,实现KV复用;并揭示“完全相同反不复用”的安全设计——确保prefill刷新logits,杜绝空响应。

热门文章

最新文章