大模型 ASR 语音识别原理分析
Qwen3 在2026年初开源了ASR语音识别模型,据官方称是state-of-the-art效果,引起Redis作者antirez的注意,他用C语言重写了语音识别推理的代码[1],这个项目第三方依赖很少,通过它可以方便地理解大模型语音识别的过程。
下面是在CPU上测试一段12.76s的音频文件离线识别的耗时分布,可以大致了解各阶段的消耗占比。
| Phase | Time | % |
|---|---|---|
| Tokenizer load (vocab.json) | 76ms | 1.4% |
| Mel spectrogram (1276 frames) | 30ms | 0.5% |
| Encoder (166 tokens) | 2370ms | 42.6% |
| Prefill (181 tokens into KV cache) | 2352ms | 42.2% |
| Decode (21 tokens, 34.3ms/tok) | 720ms | 12.9% |
| Overhead | ~20ms | 0.4% |
Real-time factor: 5.57s / 12.76s = 0.44
涉及代码中的主要函数
qwen_transcribe_audio
transcribe_segment
qwen_mel_spectrogram
qwen_encoder_forward
qwen_decoder_prefill
qwen_decoder_forward
source: https://sourcegraph.com/r/github.com/antirez/qwen-asr@main/-/blob/qwen_asr.c?L590
下面将介绍以上运行过程,标题后面的百分比是耗时占比,有的细节没有展开说,只记录了要点,主要是我的学习笔记,重点讲解大模型 Transformer 架构。
1. 音频预处理(PCM变换为频谱) 1%
过程:预加重、分帧、加窗;短时FFT;Mel滤波。
分帧:每帧25ms,为防止割裂保持平滑性,取帧时与前后帧边界重叠, 窗口每次移动10ms,所以 100 frames/s。
短时FFT:重点, 将窗口内时域转为频域。
Mel刻度:音高与频率是对数关系,符合人耳听觉。来自心理学中的Weber-Fechner定律(也称对数定律),除了听觉音高,还适用于听觉响度、味觉、触觉等。
\[mel(f) = 2595 \space log_{10}{(1 + f/700)}\]1000以下近似线性,以上对数关系。
mel刻度
语谱图
FBank
取能量谱,经mel滤波器组滤波,将频率范围按对数方式划分为n段,人耳感觉到每段的音高差异相同,得到FBank。
\[E_k=\sum_{f}|X(f)|^2 H_k(f)\]其中 $X(f)$是频率点的幅度,$ H_k(f) $是三角滤波器权重,k是mel bin序号,从0到n-1。
Bin 0: 20 - 45 Hz (low bass)
Bin 1: 45 - 71 Hz
Bin 2: 71 - 98 Hz
...
Bin 60: 1000 - 1100 Hz (mid, speech fundamentals)
...
Bin 127: 7200 - 8000 Hz (high, sibilants like "s", "t")
2. Encode 43%
int enc_seq_len = 0;
float *enc_output = qwen_encoder_forward(ctx, mel, mel_frames, &enc_seq_len);
source: https://sourcegraph.com/r/github.com/antirez/qwen-asr@b00b789b17051aea61e9717458171100662318a4/-/blob/qwen_asr.c?L612
2.1 Conv2D 22%
将FBank下采样(压缩),减少数据量。
使用卷积kernel(也称channel/filter/mask)提取特征,一种kernel负责提取一种特征。
Why Compress:
The transformer’s attention is $O(n^2)$. Processing all 1,276 mel frames directly:
mel frame: 1,276 × 1,276 = 1,628,176 attention pairs ← expensive! token: 166 × 166 = 27,556 attention pairs ← 59× cheaper!
The Conv2D stem compresses 8× (1,276 → 166) while preserving the important speech information. The 3 Conv2D layers learn what to keep and what to discard — they keep phoneme boundaries, formant transitions, and pitch changes, while discarding redundant information between adjacent frames. [5]
\[\text{Edge detector} = \begin{bmatrix} -1 & -1 & -1 \\ -1 & 8 & -1 \\ -1 & -1 & -1 \end{bmatrix}\] \[\text{Blur} = \begin{bmatrix} \frac{1}{9} & \frac{1}{9} & \frac{1}{9} \\ \frac{1}{9} & \frac{1}{9} & \frac{1}{9} \\ \frac{1}{9} & \frac{1}{9} & \frac{1}{9} \end{bmatrix}\] \[\text{Sharpen} = \begin{bmatrix} 0 & -1 & 0 \\ -1 & 5 & -1 \\ 0 & -1 & 0 \end{bmatrix}\]考察1s音频:
- 时间轴:100 mel frame 经3次卷积变为13,每一次卷积减半后取上整。(12.76s是12*13+10=166个token)
- 频率轴:480 kernel;128 mel bin 经3次卷积变为 16。相乘是7680。将7680压缩至896:
// qwen_asr_encoder.c /* Project: [w3, 7680] -> [w3, d_model] (no bias) */ float *projected = x + (size_t)token_offset * d_model; qwen_linear_nobias(projected, reshaped, enc->conv_out_weight, w3, conv_proj_dim, d_model); // ↑ ↑ ↑ // 13 tokens 7680 input 896 output
enc->conv_out_weight is the [7680, 896] matrix, loaded from thinker.audio_tower.conv_out.weight in the safetensors file. These 6.8 million weights were learned during training.
考察单个token的变换如下(注意代码和论文中的向量是横向量,线性代数教科书中的向量通常是列向量)
\[\alpha_{1 \times 7680} A_{7680\times 896} = \beta_{ 1 \times 896}\]参数汇总:
- 1 window_infer = 8s
- 1 chunk/s
- 13 token/s
- 100 mel frame/s
- token 向量的维数$d_{model}$ = 896,重要
以上是音频相关的内容,token化得到 input embedding,接下来是LLM通用的步骤,也适用于图片、视频、文本token化之后的处理。
2.2 Transformer 21%
Transformer架构
Positional Encoding
transformer如何利用输入序列中token的前后位置信息?引入Sinusoidal正弦曲线编码位置信息。[6]
/* Add per-chunk sinusoidal position embeddings (starting from pos 0) */
float *pe = (float *)malloc(w3 * d_model * sizeof(float));
qwen_sinusoidal_pe(pe, w3, d_model);
qwen_add_inplace(projected, pe, w3 * d_model);
...
void qwen_sinusoidal_pe(float *pe, int n_pos, int d_model) {
int half = d_model / 2;
float log_timescale = logf(10000.0f) / (float)(half - 1);
for (int p = 0; p < n_pos; p++) {
float *row = pe + p * d_model;
for (int d = 0; d < half; d++) {
float inv_timescale = expf(-(float)d * log_timescale);
float angle = (float)p * inv_timescale;
row[d] = sinf(angle); /* first half: sin */
row[half + d] = cosf(angle); /* second half: cos */
}
}
}
考察1s音频:
\[\begin{aligned} PE_{(p, d)} &= \sin(p*0.0001^{d/(d_{model}/2-1)}) \approx \sin(p * 0.9796^d) \\ PE_{(p, d_{model}/2+d)} &= \cos(p*0.0001^{d/(d_{model}/2-1)}) \approx \cos(p * 0.9796^d) \end{aligned}\]前一半维度用sin,后一半用cos,称为 NeoX split-half 风格,使两个向量两两不同。
注:Attention论文中是偶数维用sin,奇数维用cos,类似于PCM的两种排列方式 interleaved 和 planar。与AI沟通,为什么使用split-half风格而不是奇偶交错呢?后面decode时有类似计算需要重用,对于SIMD友好,而encode阶段则无影响,这里可能是为了保持风格一致。
函数图像:
注意到d越大,三角函数的频率越低,最后将PE向量与token向量相加,得到最终的embedding。
Attention 7%
Pre-attention LayerNorm
数据处理技巧,将一个token向量标准化为均值0、方差1,避免数值间相差太大,w、b向量训练时得到,突出和抑制某些维度。
\[\text{LayerNorm}(x) = \frac{x-\mu}{\sigma} \odot w + b\]注:规范名称应是 Standardization(标准化) 或者 z-score,不是 Normalization (归一化,有 L1, L2, min-max, RMS 等) [2] ; $\odot$ 表示逐元素相乘,即 Hadamard 积
token预处理成Query/Key/Value向量
为降低理解门槛,考察单个token向量 x ,通过三个训练时得到的weight矩阵 $W_q$, $W_k$, $W_v$和三个bias向量(有的模型没有),分别得到 q, k, v 三个向量。
\[\begin{aligned} q &= x W_q + b_q \\ k &= x W_k + b_k \\ v &= x W_v + b_v \end{aligned}\]- $W_q$: 训练过程中,$W_q$ 会学习到输入中适合作为 “查询” 的特征: 比如x是代词 “它”,$W_q$ 会让 q 聚焦于 “查询前文指代的名词”; 比如x是动词,q 会聚焦于 “查询主语 / 宾语”。
- $W_k$: 给 token 打 “可被检索的标签”,表示“我包含什么”,提供给q查询。
- $W_v$: 掏出 token 的 “干货内容”,q、k匹配成功后,真正传给其他 token 的就是它。
打个比方,新生入学班上的同学们作自我介绍,大家都很好奇对方,比如你可能对某位女同学的长相感兴趣,你会多关注到她一些,介绍完后你就想了解她的详细情况,比如她的家庭住址、生日、爱好等等。
在这个例子中,我们看到几个重要过程:
- 第一个你的需求query,你关注长相,有的人关注性格,有的关注别人的学习成绩等等
- 第二个是其他同学的特征或者说标签,称为key,比如她有什么优点、特长?
- 第三个是同学的详细信息,称为value
你的需求和她的特点匹配上了,然后了解她的详情,就是这个过程。
再看下假如没有这个自我介绍会怎么样?大家坐一起干巴巴地,就在尬聊,互相认识的效果不太好。而这个自我介绍过程相当于对token作预处理,提取出token的query、key、value。
Without these projections, attention would just compare raw embeddings directly. The learned projections allow the model to learn what aspects of the input to compare (keys vs. queries) and what information to pass forward (values).
再举一个例子,以句子 “猫在睡觉,它很可爱” 为例:
- x=“它”,$W_q$ → q:我要查【前文被指代的名词】
- x=“猫”,$W_k$ → k:我是【可被指代的实体名词】→ 匹配成功,高分(后面将介绍计算方法)
- v:匹配到猫之后,携带【猫的所有实际语义:动物、在睡觉】
前面例子中把需求和标签配对,获取详情的过程,怎么用代码或者公式精确表达?下面就是transformer架构最核心的部分。
Attention
多个token预处理后写成矩阵形式 Q, K, V,每一行表示一个token预处理后的结果。
\[\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d}}\right)V\]$QK^T$的含义是两两token(包括自己和自己)的q向量与k向量求内积$\langle q, k \rangle=qk^T$,表示相似度。比如有3个token:
\[QK^T =\begin{pmatrix} q_1 \\ q_2 \\ q_3 \end{pmatrix} \begin{pmatrix} k_1 \\ k_2 \\ k_3 \end{pmatrix}^T =\begin{pmatrix} q_1 \\ q_2 \\ q_3 \end{pmatrix} \begin{pmatrix} k_1^T & k_2^T &k_3^T \end{pmatrix} = \begin{pmatrix} \langle q_1, k_1 \rangle & \langle q_1, k_2 \rangle & \langle q_1, k_3 \rangle \\ \langle q_2, k_1 \rangle & \langle q_2, k_2 \rangle & \langle q_2, k_3 \rangle \\ \langle q_3, k_1 \rangle & \langle q_3, k_2 \rangle & \langle q_3, k_3 \rangle \end{pmatrix}\]除以$\sqrt{d}$ 和 softmax 可认为是一种数据处理技巧,避免数值间相差太大。假设q、k向量的元素是独立随机变量,由前面的LayerNorm得知$\langle q, k \rangle$标准差是$\sqrt{d}$,除以它使标准化;softmax分别对矩阵的每一行处理,得到概率分布列,下面用$\langle q, k \rangle’$表示,与V加权求和得到数学期望。实现中使用更高效的online softmax。
\[\begin{pmatrix} \langle q_1, k_1 \rangle' & \langle q_1, k_2 \rangle' & \langle q_1, k_3 \rangle' \\ \langle q_2, k_1 \rangle' & \langle q_2, k_2 \rangle' & \langle q_2, k_3 \rangle' \\ \langle q_3, k_1 \rangle' & \langle q_3, k_2 \rangle' & \langle q_3, k_3 \rangle' \end{pmatrix} \begin{pmatrix} v_1 \\ v_2 \\ v_3 \end{pmatrix} =\begin{pmatrix} \sum \langle q_1, k_i \rangle' v_i \\ \sum \langle q_2, k_i \rangle' v_i \\ \sum \langle q_3, k_i \rangle' v_i \end{pmatrix}\]例如考查token 1与所有token的attention:
| 原始相似度 | $ \langle q_1, k_1 \rangle $ | $ \langle q_1, k_2 \rangle $ | $ \langle q_1, k_3 \rangle $ |
|---|---|---|---|
| 经缩放和softmax | $\langle q_1, k_1 \rangle’ $ | $ \langle q_1, k_2 \rangle’ $ | $ \langle q_1, k_3 \rangle’ $ |
| 示例值 | 11% | 2% | 87% |
概率分布列:
| value | $v_1$ | $v_2$ | $v_3$ |
|---|---|---|---|
| 概率 | 11% | 2% | 87% |
结果为数学期望 a=11%$v_1$ + 2%$v_2$ + 87%$v_3$
Multi-Head Attention
将$d_{model}$平均分成 h 份,每份维度$d_{k}$(Qwen3ASR 0.6B将896维划分为14份,每份64维)。每份就是head的意思。每份独立计算Attention,向量维度$d_{k}$,所有份完成后拼接回向量a,维度 $d_{model}$,最后线性变换a整合各份结果:
x += $a W_o + b_o$
分head,分window处理,虚线表示token
Feed-Forward Network (FFN) 14%
逐token独立处理,不涉及跨token处理。 首先 Pre-FFN LayerNorm,类似 Pre-Attention LayerNorm,只是w、b向量不同。
\[\text{FFN}(x)=\text{GeLU}(xW_1+b_1)W_2+b_2\]After attention gathers context, FFN processes it through a wider hidden layer:
x [166, 896] → fc1 [896, 3584] → GeLU → fc2 [3584, 896] → x [166, 896]
- 896 → 3584: “expand and look at the data from 3584 different angles”, 升维4倍
- GeLU: “keep useful signals, suppress noise”
- 3584 → 896: “compress back to original size with new understanding”
最后 x += FFN(x)
N层循环
How 18 Layers Build Understanding
Each layer adds more abstraction:
- Layer 1-3: Low-level: detect phonemes, formants, pitch changes
- Layer 4-8: Mid-level: recognize syllables, word boundaries
- Layer 9-14: High-level: understand words, short phrases
- Layer 15-18: Contextual: resolve ambiguity using surrounding context
Example for “今天天气不错” (nice weather today):
- After layer 3: tokens know: [jin] [tian] [tian] [qi] [bu] [cuo]
- After layer 10: tokens know: [jintian] [tianqi] [bucuo]
- After layer 18: tokens know: [今天] [天气] [不错] with full context
Adapt dimension for decoder
Final projection目的是为了适配decoder的维度,从896维到1024维:
\[\text{GeLU}(xW_1+b_1)W_2+b_2\]注:W, b与FFN的不同
| FFN (×18) | Final Projection (×1) | |
|---|---|---|
| Expands? | Yes: 896 → 3584 → 896 (4× wider) | No: 896 → 896 → 1024 |
| Returns to same dim? | Yes: 896 → 896 | No: 896 → 1024 (new size!) |
| Has residual? | Yes: x = x + ffn_out | No residual |
| Purpose | Learn nonlinear transformations | Change dimension for decoder |
| Repeated? | 18 times | Once, at the very end |
3. 准备 Prompt
Building the decoder’s input by combining text token embeddings with encoder audio embeddings into one flat array. The decoder expects input in a chat format. This code builds it as an embedding array input_embeds [total_seq, 1024]:
Token IDs:
| Token IDs | Section | Token Count |
|---|---|---|
[<im_start>] [system] [\n] |
prefix head | 3 tokens |
[optional system prompt tokens...] |
user prompt | 0 tokens default |
[<im_end>] [\n] [<im_start>] [user] [\n] [<audio_start>] |
prefix tail | 6 tokens |
[ENCODER_OUTPUT_0] [ENCODER_OUTPUT_1] ... [ENCODER_165] |
audio | 166 tokens |
[<audio_end>] [<im_end>] [\n] [<im_start>] [assistant] [\n] |
suffix | 6 tokens |
Total: 3 + 0 + 6 + 166 + 6 = 181 tokens
4. Prefill 42%
// 输入180个token,保留最后一个token用于decode阶段解码产生结果
int prefill_len = total_seq - 1; /* prefill all but last */
qwen_decoder_prefill(ctx, input_embeds, prefill_len);
source: https://sourcegraph.com/r/github.com/antirez/qwen-asr@b00b789b17051aea61e9717458171100662318a4/-/blob/qwen_asr.c?L706
RoPE
旋转位置编码 Rotary Position Embedding [7], 按原论文习惯q, k用列向量,经位置编码后计算两者的相似度:
\[\langle R(x)q, R(y)k\rangle = (R(x)q)^TR(y)k = q^TR(x)^TR(y)k=q^TR(x)^{-1}R(y)k = q^TR(y-x)k\]其中旋转矩阵R是正交矩阵,转置等于逆,逆表示向相反方向旋转,可以看到结果中只与两个token的相对位置y-x有关。向量维数是2时旋转矩阵如下(可以推广到更多维向量,见原论文)
\[R(x)= \begin{pmatrix} \cos x & -\sin x \\ \sin x & \cos x \end{pmatrix}\] \[x=p*0.000001^{d/(head\_dim/2)} \approx p * 0.8058^d\]注:RoPE论文中使用的底数是$10^{-4}$,Qwen3ASR代码中使用的是$10^{-6}$
计算好R的元素存储,便于后续计算复用。
另外一种推导相似度只与相对位置有关的方法是使用复数,与旋转矩阵的方法等价,向量维数是2时,它与一个复数一一对应,第一个分量对应复数的实部,第二个分量对应复数的虚部,把q, k看成复数,那么相似度计算变成
\[\langle e^{ix}q, e^{iy}k\rangle = \text{Re}[\overline{e^{ix}q} e^{iy}k] = \text{Re}[e^{-ix}\overline{q} e^{iy}k] = \text{Re}[\overline{q}e^{i(y-x)}k]\]可以看到结果也只与y-x有关,实际计算还是用矩阵的方式方便。
对比矩阵与复数两种方法:
\[R(x)q= \begin{pmatrix} \cos x & -\sin x \\ \sin x & \cos x \end{pmatrix} \begin{pmatrix} x_1 \\ x_2 \end{pmatrix} = \begin{pmatrix} x_1 \cos x - x_2 \sin x \\ x_1 \sin x +x_2 \cos x \end{pmatrix}\] \[e^{ix}q=(\cos x +i \sin x) (x_1 + i x_2)=x_1\cos x - x_2 \sin x +(x_1 \sin x + x_2 \cos x)i\]Attention
Pre-attention RMSNorm
\[\text{RMSNorm}(x) = \frac{x}{\text{RMS}(x)} \odot w\]$\frac{x}{\text{RMS}(x)}$ 的模固定是$\sqrt{n}=\sqrt{1024}=32$,向量的方向保持,大小归一化
token预处理成Query/Key/Value向量
| 特性 | MHA (Multi-head attention) | GQA (Grouped-query attention)[9] | MQA (Multi-query attention) |
|---|---|---|---|
| Q | H 个独立 | H 个独立 | H 个独立 |
| K, V | H 个独立 | G 个独立 (G<H) | 1 个共享 |
| KV 缓存大小 | O(H·L·d) | O(G·L·d) | O(1·L·d) |
| 性能 | 最高 | 接近 MHA | 最低 |
| 推理速度 | 最慢 | 中等 | 最快 |
| 通俗理解 | 每个研究员都带一套完整参考书 → 知识全面但成本高 | 几位研究员组成小组,每组共用一套参考书 → 成本适中且知识覆盖足够 | 所有研究员共用一套参考书 → 成本低但可能不够用 |
使用折中方案 GQA
参数汇总:
- H=16个head
- G=8个组(每组2个head)
- L=输入token数目180个
- d=head_dim 每个head 128维
- hidden=decoder token的维数1024维
Per-head Q, K RMSNorm
对每个token计算 $R(m\theta)q$, $R(n\theta)k$
此时将复用之前计算好的sin, cos值,可使用SIMD加速。
存储$RK$和$V$到缓存
/* Apply NeoX RoPE */
qwen_apply_rope_neox(q, rope_cos, rope_sin, seq_len, n_heads, head_dim);
qwen_apply_rope_neox(k, rope_cos, rope_sin, seq_len, n_kv_heads, head_dim);
/* Store K, V in cache */
for (int s = 0; s < seq_len; s++) {
memcpy(kv_cache_k_at(ctx, layer, start_pos + s),
k + s * kv_dim, kv_dim * sizeof(float));
memcpy(kv_cache_v_at(ctx, layer, start_pos + s),
v + s * kv_dim, kv_dim * sizeof(float));
}
/* Causal attention */
int total_seq = start_pos + seq_len;
float *full_k = kv_cache_k_at(ctx, layer, 0);
float *full_v = kv_cache_v_at(ctx, layer, 0);
qwen_causal_attention(attn_out, q, full_k, full_v,
seq_len, total_seq, n_heads, n_kv_heads,
head_dim, scale, start_pos);
source: https://sourcegraph.com/r/github.com/antirez/qwen-asr@main/-/blob/qwen_asr_decoder.c?L319
计算注意力分数
- Q [180 new queries, 16 heads, 128 dims]
- K [180 cached keys, 8 heads, 128 dims] (from KV cache)
- V [180 cached values, 8 heads, 128 dims]
Causal mask — token i only attends to positions ≤ i:
Q at pos 0: attends to K at [0]
Q at pos 5: attends to K at [0,1,2,3,4,5]
Q at pos 179: attends to K at [0,1,…,179]
GQA — every 2 Q heads share 1 KV head:
Q heads 0,1 use K/V head 0
Q heads 2,3 use K/V head 1
…
Q heads 14,15 use K/V head 7
Output: attn_out [180, 2048] (16 heads × 128 dims)
输出attention
x += $a W_o$
a 维度1$\times$2048, $W_o$ 维度 2048$\times$1024
Post-attention RMSNorm
Feed-Forward Network (FFN)
\(\text{FFN}(x)=\text{SwiGLU}(x_{1\times 1024}W_{1024\times 6144})W_{3072\times 1024}\)
SwiGLU:
gate_val = gate_up[2*i] // even positions = gate
up_val = gate_up[2*i + 1] // odd positions = up (content)
gate[i] = SiLU(gate_val) × up_val
最后 x += FFN(x)
N层循环
28 layers, 每层参数不同
5. Decode 13%
首先输入prefill时遗留的最后一个token,decode返回一个token。
/* First token from last prefill position */
float *last_embed = input_embeds + (size_t)prefill_len * dim;
int token = qwen_decoder_forward(ctx, last_embed);
free(input_embeds);
source: https://sourcegraph.com/r/github.com/antirez/qwen-asr@b00b789b17051aea61e9717458171100662318a4/-/blob/qwen_asr.c?L710
Attention
处理单个token,流程与之前类似。
存储本token的kv到kv cache中。计算与之前token的attention,即 causal attention。
FFN
处理单个token,流程与之前类似。
N层循环
处理单个token,流程与之前类似。
输出一个token
最后做一次RMSNorm,与词表(共151936条)中的每个向量(1024维)做点积比较,最大值的词即结果。
/* Final norm + streaming argmax (no logits buffer needed) */
qwen_rms_norm(x, x, dec->norm, 1, dim, eps);
return qwen_argmax_matvec_bf16(x, dec->tok_embeddings_bf16, dim, cfg->vocab_size);
识别到结束,即解码出 QWEN_TOKEN_ENDOFTEXT 或 QWEN_TOKEN_IM_END token,或者达到输出token数目上限结束。
后记
以上就是 LLM ASR 的识别过程,这里只讲了CPU上运行的基本流程,运行在GPU上的优化手段也值得讨论。回顾 2017 年 Transformer 架构推出后,RoPE、在线 Softmax、SwiGLU、GQA 等优化技术相继涌现,有的为了降成本,有的为了提升效果,很多是围绕着大矩阵乘法$QK^T$这个公式的优化,行业研究者不断迭代、丰富架构细节,共同搭建出当下主流的大模型框架。
参考资料:
[1] qwen-asr C语言版 https://github.com/antirez/qwen-asr
[2] Normalization v.s. Standardization https://stats.stackexchange.com/a/10298
[3] Attention Is All You Need https://arxiv.org/abs/1706.03762
[4] vLLM 推理加速 https://km.woa.com/articles/show/659449
[5] CNN kernel 通俗解释 https://www.bilibili.com/video/BV1dbokBXEiv
[6] 苏剑林 Sinusoidal位置编码追根溯源 https://kexue.fm/archives/8231
[7] 苏剑林等 RoPE位置编码 https://arxiv.org/abs/2104.09864
[8] 苏剑林 Attention论文解读 https://kexue.fm/archives/4765
[9] 苏剑林 Transformer位置编码 https://kexue.fm/archives/8130/comment-page-7#comment-29329
[10] GQA (Grouped-query attention) https://arxiv.org/abs/2305.13245