Series · NLP · Chapter 9

自然语言处理(九):大语言模型架构深度解析

拆解现代 LLM 的内部结构:Pre-norm + RMSNorm + SwiGLU + RoPE + GQA、KV Cache 机制、FlashAttention 的 IO 调度、稀疏 MoE,以及 INT8/INT4 量化。

2017 年的 Transformer 论文里画了一个 block。今天每一款生产级 LLM 还在沿用它的轮廓,但内部几乎每一个零件都换过:post-norm 换成了 pre-norm,LayerNorm 换成了 RMSNorm,GELU 换成了 SwiGLU,正弦位置换成了旋转位置(RoPE),多头注意力变成了分组查询注意力(GQA),稠密 FFN 在某些模型里被稀疏 MoE 替换。更重要的是,主导推理性能的那个数据结构——KV Cache——根本没出现在原论文里。

这篇文章按照"实现/训练/部署时真正起作用的顺序"来串这些改动:先讲现代 decoder block,再讲为长上下文买单的 KV Cache,然后讲位置怎么编(RoPE / ALiBi),讲让 cache 小下来的注意力布局(GQA / MQA),讲让注意力跑得快的 IO 友好内核(FlashAttention),最后讲怎么"长大但不增加每 token 计算量"(MoE)以及"压小但不掉精度"(量化)。

你将学到什么

  • 现代 block 布局:pre-norm + RMSNorm + SwiGLU + RoPE + GQA,每一项替换的动机。
  • KV Cache 机制:把前缀注意力从 $O(n^2)$ 重算变成每步 $O(n)$ 的代价是什么,内存代价又是什么。
  • 位置编码:sinusoidal、RoPE、ALiBi 三种方案的不同回答。
  • 注意力变种:MHA、MQA、GQA 在 70B 量级模型上具体差多少 cache。
  • FlashAttention:用 IO 感知调度让 tile 留在 SRAM、不再实例化 $n \times n$ 分数矩阵。
  • MoE:top-$k$ 稀疏路由让总参数变大但每 token FLOPs 不变。
  • 量化:FP16 → INT8 → INT4 的 GPTQ/AWQ 路线,以及精度与显存的取舍。

前置知识


三大家族:encoder-only、decoder-only、encoder-decoder

在进入现代 block 之前,先回顾一下为什么几乎所有通用 LLM 都收敛到了 decoder-only。三种架构的差别只在注意力 mask:

家族Mask预训练目标强项代表
Encoder-only双向掩码语言模型(MLM)语义理解BERT、RoBERTa、DeBERTa
Decoder-only因果(下三角)下一 token 预测(LM)生成、ICLGPT、LLaMA、Qwen、Mistral
Encoder-Decoder双向 encoder + 因果 decoder + cross-attn去噪 / span corruptionseq-to-seqT5、BART、FLAN-T5
1
2
3
4
5
from transformers import AutoModel, AutoModelForCausalLM, AutoModelForSeq2SeqLM

enc  = AutoModel.from_pretrained("bert-base-uncased")            # encoder-only
dec  = AutoModelForCausalLM.from_pretrained("gpt2")               # decoder-only
ed   = AutoModelForSeq2SeqLM.from_pretrained("t5-small")          # encoder-decoder

Decoder-only 之所以赢在 scaling 上有两个原因:第一,所有任务都可以转成"预测下一个 token",单一目标和数据格式可以无限扩展;第二,因果 mask 让前缀缓存(KV Cache)非常便宜——encoder-decoder 还要缓存 cross-attention,而 encoder-only 根本没法生成。今天说"LLM"几乎都默认指带后面这些改动的 decoder-only 模型。


现代 decoder block

图 1 — 现代 LLM decoder block

LLaMA 风格的 block(LLaMA、LLaMA-2、Mistral、Qwen、Yi、DeepSeek……)相对 2017 年的 block 改了五处。每一处单独看影响都不大,但叠加起来就得到一个"无须 warmup 调参也能稳定训练、能扩展到长上下文、推理更快"的模型。

1. Pre-norm 替代 post-norm。 原版是 Norm(x + Sublayer(x)),现代版是 x + Sublayer(Norm(x))。残差路径变成干净的恒等通路,深层堆叠时梯度尺度更稳,也省掉了著名的 Transformer 学习率 warmup。

2. RMSNorm 替代 LayerNorm。 LayerNorm 减去均值再除以标准差,RMSNorm 只除以均方根,没有均值,也没有 bias:

$$ \mathrm{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{d}\sum_i x_i^2 + \varepsilon}} \cdot g. $$

少一个归约、少一组参数,质量没有可测损失。

3. SwiGLU 替代 GELU。 标准 FFN 是 Linear → GELU → Linear,SwiGLU 多加一条 gating linear:

$$ \mathrm{SwiGLU}(x) = \big(\mathrm{Swish}(W_1 x) \odot (W_3 x)\big) W_2. $$

逐元素乘法给 FFN 注入了乘性交互,等参数量下 perplexity 通常改善 1–2%。为了维持参数预算,隐藏维度按 $2/3$ 缩。

4. RoPE 替代学习式绝对位置。 位置不再加在 embedding 上,而是在注意力时旋转 Q/K,下一节展开。

5. GQA / MQA 替代 MHA。 多个 query 头共享一个 KV 头,下下节展开。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import torch
import torch.nn as nn
import torch.nn.functional as F

class RMSNorm(nn.Module):
    def __init__(self, d, eps=1e-6):
        super().__init__()
        self.g = nn.Parameter(torch.ones(d))
        self.eps = eps
    def forward(self, x):
        rms = x.pow(2).mean(-1, keepdim=True).add(self.eps).rsqrt()
        return x * rms * self.g

class SwiGLU(nn.Module):
    def __init__(self, d_model, d_ff):
        super().__init__()
        self.w1 = nn.Linear(d_model, d_ff, bias=False)   # gate
        self.w3 = nn.Linear(d_model, d_ff, bias=False)   # value
        self.w2 = nn.Linear(d_ff, d_model, bias=False)   # down-proj
    def forward(self, x):
        return self.w2(F.silu(self.w1(x)) * self.w3(x))

class LlamaBlock(nn.Module):
    """Pre-norm + RMSNorm + SwiGLU;attention 由外部注入。"""
    def __init__(self, d_model, attn, d_ff):
        super().__init__()
        self.norm1 = RMSNorm(d_model)
        self.attn  = attn
        self.norm2 = RMSNorm(d_model)
        self.ffn   = SwiGLU(d_model, d_ff)
    def forward(self, x, **kw):
        x = x + self.attn(self.norm1(x), **kw)
        x = x + self.ffn(self.norm2(x))
        return x

KV Cache:为长上下文买单的数据结构

图 2 — KV Cache 把 O(n²) 前缀重算变成 O(n)

自回归解码每步只新增一个 token。朴素地实现,新位置的注意力需要"所有过去 token 的 K 和 V"——每步都从头投影一次的话,每步 $O(n)$,总共 $O(n^2)$。KV Cache 的关键观察是:这些投影只是过去 token 的确定函数,算一次存下来就行。

它能成立靠两个事实:

  • 模型是因果的,已写入 cache 的位置不会因后续 token 而需要修改;
  • K、V 投影对输入是线性的,缓存投影后张量与重算等价。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class KVCache:
    """每层 KV cache,layout: [B, H_kv, T, D_h]。"""
    def __init__(self, B, H_kv, D_h, max_T, device, dtype=torch.float16):
        self.k = torch.empty(B, H_kv, max_T, D_h, device=device, dtype=dtype)
        self.v = torch.empty(B, H_kv, max_T, D_h, device=device, dtype=dtype)
        self.t = 0                                     # 已写入长度

    def append(self, k_new, v_new):
        T_new = k_new.size(2)
        self.k[:, :, self.t : self.t + T_new] = k_new
        self.v[:, :, self.t : self.t + T_new] = v_new
        self.t += T_new
        return self.k[:, :, : self.t], self.v[:, :, : self.t]

def attention_step(q_new, cache: KVCache, k_new, v_new, scale):
    """一次 decode 步:q_new 形状 [B, H_q, 1, D_h]。"""
    K, V = cache.append(k_new, v_new)                  # 完整前缀
    scores = (q_new @ K.transpose(-2, -1)) * scale     # [B, H, 1, T]
    attn   = scores.softmax(-1)
    return attn @ V                                    # [B, H, 1, D_h]

代价并不便宜:cache 显存与序列长度线性增长,是 decode 阶段的主导内存项。以 LLaMA-2-70B 形状(80 层、64 KV 头、head_dim=128、fp16)的朴素 MHA 为例:

$$ 2 \cdot 80 \cdot 64 \cdot 128 \cdot 2\text{ B} = 2.6\text{ MB / token}, $$

32K 上下文光 cache 就要 84 GB——比权重还大。这正是 GQA、MQA 和 PagedAttention(vLLM)出现的根本原因。


位置编码:sinusoidal、RoPE、ALiBi

图 3 — 三种位置编码方案

自注意力本身没有顺序:把输入排列一下,输出也只是同样地排列。三种主流方案在三个不同的位置注入位置信息。

Sinusoidal 绝对位置(Vaswani 等,2017)。 在 embedding 层把固定的 $\sin/\cos$ 向量到 token 上。位置信息要靠后续每一层线性投影把它带过去,外推到训练长度之外效果很差。

RoPE — 旋转位置嵌入(Su 等,2021)。 不加在 embedding 上,而是在注意力时旋转 Q 和 K。把 $d$ 维 head 切成 $d/2$ 个二维平面,平面 $i$ 的频率为 $\theta_i = 10000^{-2i/d}$,位置 $m$ 把第 $i$ 个平面旋转角度 $m\theta_i$。关键恒等式:

$$ \langle R_m q,\; R_n k \rangle = \langle q,\; R_{n-m} k \rangle, $$

点积只依赖于相对偏移 $n - m$。这就是为什么 RoPE 能外推到比训练长度更长的上下文,也是为什么所有现代模型(LLaMA、Qwen、Mistral、Yi、DeepSeek、GPT-NeoX)都用它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def precompute_rope(d_head, max_T, base=10000.0, device="cpu"):
    half = d_head // 2
    freqs = 1.0 / (base ** (torch.arange(0, half, device=device).float() / half))
    t = torch.arange(max_T, device=device).float()
    angles = torch.outer(t, freqs)                     # [T, d_head/2]
    return torch.cos(angles), torch.sin(angles)

def apply_rope(x, cos, sin):
    """x: [B, H, T, D_h]。旋转 (i, i+half) 维度对。"""
    half = x.size(-1) // 2
    x1, x2 = x[..., :half], x[..., half:]
    cos = cos[: x.size(-2)].to(x.dtype)
    sin = sin[: x.size(-2)].to(x.dtype)
    rot1 = x1 * cos - x2 * sin
    rot2 = x1 * sin + x2 * cos
    return torch.cat([rot1, rot2], dim=-1)

ALiBi — 线性偏置注意力(Press 等,2021)。 直接不要位置嵌入,在 softmax 前的分数上加一个按头不同的线性惩罚 $-m_h \cdot |i - j|$。$m_h$ 小的头看全局,$m_h$ 大的头看局部。原论文里 ALiBi 比 RoPE 外推更远,但因为没注入相对相位,在知识密集的长上下文 benchmark 上通常输给 RoPE。BLOOM、MPT 用过它。

工程上的现实是:RoPE 配上 NTK-aware scaling、YaRN、position interpolation 这些后训练技巧,可以让 4K 训练的模型微调到 32K–128K,已经成为绝对主流。


注意力变种:MHA → GQA → MQA

图 4 — MHA / MQA / GQA 三种布局

标准多头注意力(MHA)每个 query 头都有自己的 K、V 投影,因此 KV Cache 与 KV 头数成正比,正是上一节算出 84 GB 的元凶。两个折中方案:

  • MQA — 多查询注意力(Shazeer,2019): 所有 query 头共享一个 KV 头,cache 缩 $H$ 倍,但 K/V 投影失去头多样性,难任务上质量明显下降。
  • GQA — 分组查询注意力(Ainslie 等,2023): LLaMA-2-70B、Mistral 等当下普遍采用的中间方案。把 $H_q$ 个 query 头分成 $G$ 组,每组共享一个 KV 头。LLaMA-2-70B 用 $H_q = 64$、$G = 8$,cache 缩 8 倍而质量与 MHA 持平。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class GroupedQueryAttention(nn.Module):
    def __init__(self, d_model, n_q_heads, n_kv_heads):
        super().__init__()
        assert n_q_heads % n_kv_heads == 0
        self.h_q  = n_q_heads
        self.h_kv = n_kv_heads
        self.rep  = n_q_heads // n_kv_heads
        self.d_h  = d_model // n_q_heads
        self.wq = nn.Linear(d_model, n_q_heads  * self.d_h, bias=False)
        self.wk = nn.Linear(d_model, n_kv_heads * self.d_h, bias=False)
        self.wv = nn.Linear(d_model, n_kv_heads * self.d_h, bias=False)
        self.wo = nn.Linear(n_q_heads * self.d_h, d_model, bias=False)

    def forward(self, x, cos, sin, cache: KVCache | None = None):
        B, T, _ = x.shape
        q = self.wq(x).view(B, T, self.h_q,  self.d_h).transpose(1, 2)
        k = self.wk(x).view(B, T, self.h_kv, self.d_h).transpose(1, 2)
        v = self.wv(x).view(B, T, self.h_kv, self.d_h).transpose(1, 2)
        q = apply_rope(q, cos, sin)
        k = apply_rope(k, cos, sin)
        if cache is not None:
            k, v = cache.append(k, v)
        # 把每个 KV 头广播到对应的 query 组
        k = k.repeat_interleave(self.rep, dim=1)
        v = v.repeat_interleave(self.rep, dim=1)
        out = F.scaled_dot_product_attention(q, k, v, is_causal=cache is None)
        out = out.transpose(1, 2).contiguous().view(B, T, -1)
        return self.wo(out)

LLaMA-2-70B 形状下的具体数字(fp16,每 token):

  • MHA,64 KV 头 → 2.56 MB/token → 32K 上下文 80 GB;
  • GQA-8,8 KV 头 → 0.32 MB/token → 32K 上下文 10 GB;
  • MQA,1 KV 头 → 0.04 MB/token → 32K 上下文 1.25 GB。

GQA-8 几乎拿走了 MQA 的全部内存收益,而质量基本无损——这就是为什么近期开源权重模型默认都用它。


FlashAttention:相同的数学,IO 感知的调度

图 5 — FlashAttention 的 GPU 内存层级

朴素注意力内核会把 $S = QK^\top$ 整个 $n \times n$ 分数矩阵写进 HBM(GPU 主显存),在 HBM 上跑 softmax,再乘 $V$。$n=8192$、fp16 时,仅 $S$ 一个矩阵每个 head 每层就是 128 MB,绝大多数流量都被反复读写浪费掉。

FlashAttention(Dao 等,2022)做的是同一份数学、不同的调度:

  1. 把 $Q$、$K$、$V$ 切成行/列 tile,使其能放进每个 SM 上 ~192 KB 的 SRAM;
  2. 在 SRAM 内一次只算一个 $S$ tile;
  3. 在线 softmax:维护逐行的最大值 $m$ 和分母 $\ell$,每个 tile 只增量更新部分输出,整行 $S$ 永远不需要存在;
  4. 只把最终 $O$ 写回 HBM。完整的 $S$ 从未出现。

结果是数学上完全等价(仅浮点归约顺序不同)的注意力,HBM 流量从 $O(n^2)$ 降到 $O(n)$,$n \geq 2048$ 时墙钟提速 2–4 倍,显存最高省 8 倍。FlashAttention-2 进一步优化 warp 级任务划分,能跑到 A100 FP16 峰值的约 70%。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 现代 transformers/torch 里你不需要自己写 kernel,开关打开即可:
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    attn_implementation="flash_attention_2",   # 也可选 "sdpa" / "eager"
    torch_dtype=torch.float16,
    device_map="auto",
)
# 或者用 PyTorch 自带的 scaled_dot_product_attention,
# 它会自动派发到 flash / mem-efficient / math 三种内核。

记住:FlashAttention 不改变模型计算什么,只改变 GPU 上的算子顺序。序列长度超过 ~1K 时一律应该开。


混合专家:长大但不增加每 token 计算量

图 6 — 稀疏 MoE 的 top-k 路由

稠密 Transformer 的参数和 FLOPs 大头都在 FFN。MoE 把单个 FFN 换成 $N$ 个"专家"FFN,再加一个微型 router,对每个 token选 top-$k$ 个专家来执行:

$$ y = \sum_{i \in \mathrm{TopK}(W_g x)} g_i(x)\, E_i(x),\qquad g(x) = \mathrm{softmax}(W_g x). $$

每 token 的 FFN FLOPs 随 $k$(通常 2)增长,而非随 $N$ 增长。所以 8 专家模型的 FFN 参数大约是稠密模型的 8 倍,但 FFN 计算只多了 2 倍。Mixtral 8×7B 总参数 47B,每 token 只激活约 13B,得到 70B 量级的质量、13B 量级的推理成本。

老实说,代价在别的地方:

  • 显存。 所有专家都得常驻,即使只跑 $k$ 个。Mixtral 8×7B fp16 要 ~94 GB,比稠密 70B 还大;INT4 后才能塞进 24 GB。
  • 负载均衡。 没干预的 router 会塌缩到几个偏爱的专家。真实训练要加辅助负载均衡损失(Shazeer 2017、Switch Transformer)和小幅 router 噪声鼓励探索。
  • All-to-all 通信。 多卡训练时每个 token 要跑到目标专家上再跑回来,对互联拓扑非常敏感。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class TopKRouter(nn.Module):
    def __init__(self, d_model, n_experts, k=2):
        super().__init__()
        self.gate = nn.Linear(d_model, n_experts, bias=False)
        self.k = k

    def forward(self, x):
        logits = self.gate(x)                                   # [*, N]
        topv, topi = logits.topk(self.k, dim=-1)
        weights = topv.softmax(-1)                              # 重新归一
        return weights, topi                                    # [*, k], [*, k]

class SparseMoE(nn.Module):
    def __init__(self, d_model, d_ff, n_experts, k=2):
        super().__init__()
        self.experts = nn.ModuleList(
            [SwiGLU(d_model, d_ff) for _ in range(n_experts)]
        )
        self.router = TopKRouter(d_model, n_experts, k)

    def forward(self, x):
        B, T, D = x.shape
        flat = x.view(-1, D)
        w, idx = self.router(flat)                              # [BT, k]
        out = torch.zeros_like(flat)
        for slot in range(self.router.k):
            for e in range(len(self.experts)):
                mask = idx[:, slot] == e
                if mask.any():
                    out[mask] += w[mask, slot, None] * self.experts[e](flat[mask])
        return out.view(B, T, D)

(生产实现会用 grouped GEMM 和容量限制 bucket 并行调度 token 到专家,上面的循环只为了清晰。)


量化:每个权重少几比特

图 7 — 量化:FP16 → INT8 → INT4

现代 LLM 权重一般用 BF16 训练(每参数 2 字节),70B 模型 140 GB,单张 A100 80GB / H100 80GB 装不下。量化把每个权重换成低比特整数加一组 per-block scale,以小幅精度代价换显存。

对称 INT8。 选一个 per-tensor(或 per-channel)scale $s = \max|w| / 127$,存 $\hat w = \mathrm{round}(w / s) \in [-127, 127]$。计算时恢复 $w \approx s \cdot \hat w$。显存减半,INT8 tensor core 上吞吐大致也翻倍。

1
2
3
4
5
def quantize_int8_per_channel(W):
    """W: [out, in]。返回 int8 权重 + per-row fp16 scale。"""
    scale = W.abs().amax(dim=1, keepdim=True) / 127.0          # [out, 1]
    Wq = (W / scale).round().clamp(-127, 127).to(torch.int8)
    return Wq, scale.to(torch.float16)

INT4 + GPTQ(Frantar 等,2022)。 直接把每个权重独立量化到 4 bit 会精度暴跌。GPTQ 改为按列量化,每量完一列就用一小批校准数据估计的 Hessian 修正剩余未量化列以补偿当前列的舍入误差。结果是 7B 以上模型 4 bit 后 perplexity 损失 <1%。

INT4 + AWQ(Lin 等,2023)。 AWQ 观察到权重通道里只有约 1% 是"显著"的(由大激活值驱动),只要保护这部分通道(用 per-channel scaling,并不真的留 fp16)就能恢复绝大部分精度。AWQ 比 GPTQ 计算快很多,是当下许多量化开源 checkpoint 的默认方案。

精度字节/参数LLaMA-2-7B 权重LLaMA-2-70B 权重典型 PPL 增量
FP16 / BF162.013.5 GB140 GB参考基准
INT8(RTN)1.06.7 GB70 GB<0.5%
INT4(GPTQ / AWQ)0.53.4 GB35 GB<2%
INT3(高级算法)0.3752.5 GB26 GB3–6%
1
2
3
4
5
6
# 加载预量化的 AWQ checkpoint
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained(
    "TheBloke/Llama-2-7B-AWQ", device_map="auto", torch_dtype="auto",
)
tok = AutoTokenizer.from_pretrained("TheBloke/Llama-2-7B-AWQ")

激活通常仍保留 FP16/BF16,因为它们分布是重尾的(LLM.int8(),Dettmers 2022 发现少量"离群特征"承载了大部分激活能量,必须留高精度)。仅权重 INT4 是当下推理的最佳折中。


拼起来:高吞吐推理栈

这些组件是可组合的。一个现代的 vLLM 服务栈把以下东西凑在一起:

  • LLaMA 风格 decoder block(pre-norm + RMSNorm + SwiGLU + RoPE + GQA);
  • 每层 KV Cache,但按固定大小布局(PagedAttention),上下文增删不再产生碎片;
  • 预填和解码都跑 FlashAttention 内核;
  • 权重 INT4 量化(AWQ / GPTQ)省显存;
  • 连续批处理:某条请求结束后空出的 slot 立刻被新提示填上,而不是等批里最慢的请求。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from vllm import LLM, SamplingParams

llm = LLM(
    model="TheBloke/Llama-2-7B-AWQ",
    quantization="awq",
    dtype="float16",
    gpu_memory_utilization=0.90,
    max_model_len=8192,
)
params = SamplingParams(temperature=0.7, top_p=0.9, max_tokens=128)
for o in llm.generate(["用两句话解释 MoE。"], params):
    print(o.outputs[0].text)

实测下来,单张 A100 80GB 跑 7B AWQ 模型能做到 3000–5000 tokens/s 的聚合吞吐,单张 H100 跑 Mixtral 8×7B AWQ 能做到 >1000 tokens/s——这两个数字按 2017 年原版 block 来写是不可想象的。


常见问题

为什么是 decoder-only 而不是 encoder-decoder?

因果 decoder-only 模型只要换提示就能当分类器、翻译器、对话机;encoder-decoder 还要额外一套 cross-attention 参数和更复杂的训练流程,且生成时无法享受廉价的前缀缓存。decoder-only 也更适合扩展,因为语料里每个 token 都是训练信号。

RoPE 还是 ALiBi?

工程上几乎都选 RoPE:它给模型真正的相对相位信息,配合 NTK / YaRN 等后训练缩放可以把上下文从 4K 扩到 32K–128K。ALiBi 的吸引力是不微调也能外推,但代价是 in-distribution 质量更弱。当下主流开源权重一律 RoPE。

FlashAttention 会改变模型输出吗?

不会。FlashAttention 是精确注意力,仅浮点归约顺序不同,与朴素内核的数值差远低于训练噪声。

MoE 是不是总比稠密便宜?

计算上更便宜,显存上更贵。如果你被显存卡住(比如单张 24GB 卡推理),量化的稠密模型通常优于 MoE;如果你有多卡显存但 decode FLOPs 是瓶颈,MoE 胜出。

7B 模型 INT4 大约掉多少精度?

用 GPTQ 或 AWQ 配 ~128 条校准样本,perplexity 增量通常 <2%,绝大多数下游任务质量肉眼难辨。70B 模型损失通常 <1%。7B 以下时量化会明显更难。

为什么 KV Cache 是长上下文的瓶颈?

cache 大小是 $2 \cdot L \cdot H_{kv} \cdot d_h \cdot T \cdot \text{字节数}$。70B 模型 32K 上下文走 MHA 是 ~80 GB,比权重还大。GQA 把它降到 ~10 GB,PagedAttention 又把碎片率压到 <5%,两者合起来才让长上下文服务在普通硬件上跑得起来。


系列导航

部分主题链接
8模型微调与 PEFT<– 上一篇
9大语言模型架构深度解析(本文)
10RAG 与知识增强系统下一篇 –>

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub