系列 · NLP 技术前沿 · 第 9 篇

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

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

2017 年的 Transformer 论文提出了一种模块,如今所有生产环境中的大语言模型(LLM)依然沿用其整体框架,但内部几乎所有组件都经历了彻底替换:后置归一化(post-norm)被前置归一化(pre-norm)取代,LayerNorm 被 RMSNorm 替换,GELU 激活函数变成了 SwiGLU,正弦位置编码也被旋转位置编码(RoPE)所替代。多头注意力机制(Multi-head Attention)演进为分组查询注意力(Grouped-Query Attention, GQA),稠密前馈网络(FFN)在某些模型中甚至被稀疏专家混合模型(Mixture of Experts, MoE)所取代;更重要的是,推理过程中最核心的数据结构——KV 缓存(KV Cache)——在原始论文中完全没有提及。

本文将按照这些改动在模型实现、训练和部署时的实际重要性顺序展开讲解:从现代解码器模块入手,介绍支撑长上下文的关键数据结构——KV 缓存;探讨位置信息编码(如 RoPE 和 ALiBi),以及通过注意力布局优化(如 GQA 和 MQA)来降低缓存开销;分析使注意力计算更快的 IO 优化内核(FlashAttention);最后讨论如何在不增加每 token 计算量的情况下扩展模型规模(MoE),或在不损失精度的前提下压缩模型(量化)。

自然语言处理(九):大语言模型架构深度解析 — 章节概览图


你将学到什么#

自然语言处理(九):大语言模型架构深度解析 — 章节小结图

  • 现代模块设计:为什么 pre-norm、RMSNorm、SwiGLU、RoPE 和 GQA 成为主流选择,每一项改进背后的原因是什么。
  • KV Cache 工作原理:如何通过优化将注意力计算的复杂度从 $O(n^2)$ 降低到 $O(n)$ 的摊销成本,以及这种优化对内存占用的具体影响。
  • 位置编码方法: sinusoidal、RoPE 和 ALiBi 这三种技术如何分别回答“当前 token 的位置在哪里”这个问题。
  • 注意力机制的变体: MHA、MQA 和 GQA 在头部多样性与缓存大小之间的权衡,结合一个 70B 参数量级的模型,用实际数据说明它们的差异。
  • FlashAttention 算法:一种高效的注意力实现方式,通过 IO 感知调度策略,将数据块驻留在 SRAM 中,从而避免显式生成 $n \times n$ 的分数矩阵。
  • MoE (混合专家模型):利用稀疏 top-$k$ 路由,在不增加每个 token 计算量(FLOPs)的情况下扩展模型的总参数规模。
  • 量化技术:从 FP16 到 INT8 再到 INT4,使用 GPTQ 和 AWQ 方法进行模型压缩时,精度和显存的实际表现如何。

前置知识#


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

在深入探讨现代架构之前,不妨先思考一下,为什么几乎所有通用的大语言模型(LLM)最终都选择了仅解码器的架构。其实,这三种架构的核心区别就在于注意力掩码的设计:

家族掩码类型预训练目标擅长领域示例
仅编码器双向掩码语言模型(MLM)语义理解BERT、 RoBERTa、 DeBERTa
仅解码器因果(下三角)下一词预测(LM)文本生成、上下文学习GPT、 LLaMA、 Qwen、 Mistral
编码器-解码器双向编码器 + 因果解码器 + 跨注意力去噪 / Span Corruption序列到序列任务T5、 BART、 FLAN-T5
1
2
3
4
5
from transformers import AutoModel, AutoModelForCausalLM, AutoModelForSeq2SeqLM

enc  = AutoModel.from_pretrained("bert-base-uncased")            # 仅编码器
dec  = AutoModelForCausalLM.from_pretrained("gpt2")               # 仅解码器
ed   = AutoModelForSeq2SeqLM.from_pretrained("t5-small")          # 编码器-解码器

仅解码器架构之所以能在规模化竞赛中脱颖而出,主要有两个原因:几乎所有任务都可以转化为“预测下一个词”的问题,这种单一的目标和统一的数据格式让扩展变得非常高效且清晰;其次,因果掩码使得前缀缓存(KV Cache)的实现成本极低——相比之下,编码器-解码器架构还需要额外缓存跨注意力机制,而仅编码器架构则完全无法独立完成生成任务。如今,提到“LLM”,几乎默认指的就是后面会提到改进版的仅解码器模型。

现代 decoder block#

图 1 — 现代 LLM 解码器模块

LLaMA 风格的模块(如 LLaMA、LLaMA-2、Mistral、Qwen、Yi、DeepSeek 等)与 2017 年的经典 Transformer 模块相比,主要在五个方面进行了改进。虽然每个改动单独来看影响有限,但它们共同作用,使得模型在训练时更加稳定,无需学习率预热(warmup)调参,能够更好地扩展到更长的上下文,并且推理速度更快。

1. 前置归一化(Pre-norm)取代后置归一化(Post-norm)。
原始设计是先进行残差加和,再对结果进行归一化(x + Sublayer(x) 后 norm),而现代设计则是在进入子层之前先进行归一化(x + Sublayer(Norm(x)))。前置归一化保留了残差路径的恒等映射特性,这在深层网络中能有效保持梯度尺度的稳定性,同时避免了 Transformer 中常见的学习率 warmup 需求。

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

这种简化不仅每层减少了一次归约操作和一组参数,而且在实际效果上几乎没有损失。

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

逐元素乘法为 FFN 引入了乘性交互机制,在相同参数量下,困惑度通常能降低 1–2%。为了控制参数预算,隐藏层维度被压缩了 $2/3$

4. RoPE 取代可学习绝对位置编码。
不再将位置信息直接加到嵌入向量中,而是在注意力计算时通过对 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):
    """前置归一化 + RMSNorm + SwiGLU;注意力模块从外部注入。"""
    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 决定的确定性函数,那只需要算一次并保存下来即可。

KV Cache 能够奏效的关键在于两点:

  • 模型是因果的,这意味着已经写入 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 缓存,布局为 [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):
    """单步解码: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]
$$2 \cdot 80 \cdot 64 \cdot 128 \cdot 2\text{ B} = 2.6\text{ MB / token},$$

对于长度为 32K 的上下文,仅 KV Cache 就需要占用 84 GB 显存——甚至超过了模型权重本身的大小。正是这一巨大的压力催生了 GQA、 MQA 以及 PagedAttention (vLLM)等优化方案。

位置编码: sinusoidal、 RoPE、 ALiBi#

图 3 — 三种位置编码方法对比

自注意力机制天生对顺序没有感知能力:输入的顺序打乱,输出也会随之改变。为了解决这个问题,目前主流的三种方法分别在不同的环节注入位置信息。

Sinusoidal 绝对位置编码(Vaswani 等, 2017)。 在输入层将一个固定的 $\sin/\cos$ 向量直接叠加到 token 的 embedding 上。由于位置信息需要通过每一层的线性变换传递,这种方法在训练分布内的表现尚可,但一旦超出训练长度,外推能力就显得捉襟见肘。

$$\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,但由于缺乏相对相位信息,在知识密集型的长上下文任务中通常表现不如 RoPE。 BLOOM 和 MPT 是采用该方法的典型代表。

在实际工程中, RoPE 结合 NTK-aware scaling、 YaRN 和 position interpolation 等技术手段,可以让训练长度为 4K 的模型通过少量微调扩展到 32K–128K,这已经成为当前处理长上下文任务的主流方法。

注意力变种: MHA → GQA → MQA#

图 4 — MHA、MQA 和 GQA 的对比

标准多头注意力(MHA)为每个查询头(query head)分别维护独立的键(K)和值(V)投影。 KV 缓存的开销与 KV 头的数量直接相关,这也是前面提到的 84 GB 内存占用的主要来源,尤其是在处理长上下文时。为了优化内存使用,研究者提出了两种折中方案:

  • MQA — 多查询注意力(Shazeer, 2019): 所有查询头共享一个键值头(KV head)。这种方式让 KV 缓存的大小缩小了 $H$ 倍,但由于键值投影失去了多样性,在复杂任务上的性能会有明显下降。
  • GQA — 分组查询注意力(Ainslie 等, 2023): 这是 LLaMA-2-70B、 Mistral 以及大多数最新模型采用的折中方案。它将 $H_q$ 个查询头分成 $G$ 组,每组共享一个键值头。以 LLaMA-2-70B 为例,当 $H_q = 64$$G = 8$ 时, KV 缓存缩小了 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 头广播到对应的查询组
        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 头 → 每 token 占用 2.56 MB → 在 32K 上下文时需要 80 GB;
  • GQA-8, 8 个 KV 头 → 每 token 占用 0.32 MB → 在 32K 上下文时需要 10 GB;
  • MQA, 1 个 KV 头 → 每 token 占用 0.04 MB → 在 32K 上下文时需要 1.25 GB。

GQA-8 几乎实现了 MQA 的全部内存节省优势,同时性能几乎没有损失。正因如此,近期的开源权重模型几乎都默认采用了这一方案。

FlashAttention:数学不变,调度优化#

图 5 — FlashAttention 的 GPU 内存层级

传统的注意力核函数会先计算 $S = QK^\top$ ,然后将完整的 $n \times n$ 分数矩阵存储到 HBM (GPU 主显存)中,在 HBM 上执行 softmax 操作,最后再与 $V$ 相乘。以 $n=8192$ 、 fp16 精度为例,单是 $S$ 矩阵每个注意力头每层就占用 128 MB 显存,而这些数据在计算过程中会被反复读写,导致大量带宽被浪费。

FlashAttention (Dao 等, 2022)采用相同的数学公式,但通过优化调度方式显著提升了效率:

  1. $Q$$K$$V$ 切分为适合片上 SRAM 的小块(tile),确保它们能装入 A100/H100 每个 SM 中约 192 KB 的 SRAM。
  2. 每次只在 SRAM 中计算一个 $S$ 小块。
  3. 使用在线 softmax 技巧:动态维护最大值 $m$ 和分母 $\ell$ ,使得每个小块可以直接更新部分输出,而无需加载整行数据。
  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 的场景,务必启用它以获得最佳性能。

混合专家:容量提升,计算量不增#

图 6 — 稀疏 MoE 的 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 的计算复杂度与 $k$ 成正比(通常 $k=2$ ),而不是与专家总数 $N$ 相关。因此,一个包含 8 个专家的模型,其 FFN 参数量大约是稠密模型的 8 倍,但计算量仅增加到原来的 2 倍。以 Mixtral 8×7B 为例,该模型总参数量为 47B,但每 token 只激活约 13B 参数,推理成本接近 13B 模型,却能实现接近 70B 模型的质量。

然而,这种设计并非没有代价,主要体现在以下几个方面:

  • 显存占用: 尽管每次只运行 $k$ 个专家,但所有专家都需要常驻显存。例如, Mixtral 8×7B 在 fp16 精度下需要约 94 GB 显存,甚至超过了稠密 70B 模型;不过,通过 INT4 量化,显存占用可以降至 24 GB 以下。
  • 负载均衡: 如果不加以干预,路由器可能会倾向于选择少数几个“热门”专家,导致负载分布不均。为了解决这一问题,实际训练中通常会引入辅助的负载均衡损失(Shazeer 2017、 Switch Transformer),并在路由决策中加入少量噪声,以鼓励探索更多专家。
  • 全互联通信: 在多 GPU 训练场景中,每个 token 需要被发送到目标专家所在的设备并返回结果,因此 MoE 对网络拓扑结构的性能非常敏感。
 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)

(在生产环境中, MoE 的实现通常会利用 grouped GEMM 和容量限制的分桶机制,并行调度 token 到对应的专家。上述代码中的循环只是为了清晰展示逻辑,并未考虑性能优化。)

量化:每个权重少几比特#

图 7 — 量化:FP16 → INT8 → INT4

现代大语言模型(LLM)的权重通常使用 BF16 格式训练,每参数占用 2 字节。一个 70B 参数的模型需要 140 GB 显存,这远远超出了单张 A100 80GB 或 H100 80GB 的显存容量。量化技术通过将每个权重替换为低比特整数,并结合每个块的缩放因子(scale),在精度损失极小的情况下显著减少了模型的显存占用。

对称 INT8 量化: 首先为每个张量(或通道)选择一个缩放因子 $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 权重 + 每行的 fp16 缩放因子。"""
    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 比特,模型精度会大幅下降。 GPTQ 提出了一种更聪明的方法:按列逐步量化,并在每次量化完一列后,利用从小批量校准数据中估计的 Hessian 矩阵调整剩余未量化的列,以补偿当前列的舍入误差。最终结果是,对于 7B 参数以上的模型,量化到 4 比特后困惑度(perplexity)的损失不到 1%。

INT4 + AWQ (Lin 等, 2023)。 AWQ 的研究发现,权重通道中仅有约 1% 是“显著”的——这些通道由较大的激活值驱动。通过仅保护这些显著通道(使用 per-channel scaling,而非保留为 fp16),可以恢复绝大部分模型精度。相比 GPTQ, AWQ 的计算速度更快,因此已经成为许多量化开源 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 的解码器模块(pre-norm + RMSNorm + SwiGLU + RoPE + GQA),兼顾性能与效果。
  • 每层的 KV 缓存采用固定大小的分页机制(PagedAttention),动态调整上下文长度时不会引发内存碎片问题。
  • 使用 FlashAttention 内核优化预填充和解码阶段的计算效率。
  • 权重通过 INT4 量化(AWQ / GPTQ)显著降低显存占用。
  • 支持连续批处理:当某个请求完成后,其占用的资源会立即被下一个请求接管,而无需等待整个批次中最慢的序列完成。
 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 年的技术水平来实现,这样的性能表现简直是天方夜谭。

常见问题#

为什么选择 decoder-only 而不是 encoder-decoder?#

一个训练用来预测下一个 token 的因果 decoder-only 模型,只需调整提示词,就可以轻松转换为分类器、翻译工具或聊天机器人。而 encoder-decoder 模型则需要额外的交叉注意力参数,并且训练流程更复杂,生成时也无法利用前缀缓存来节省计算开销。此外, decoder-only 的扩展性更强,因为语料中的每个 token 都能直接作为有效的训练信号。

RoPE 和 ALiBi,哪个在实际中更有优势?#

绝大多数情况下, RoPE 是更好的选择:它能够为模型提供真实的相对相位信息,并且通过 NTK/YaRN 缩放技术可以无缝扩展上下文长度(从 4K 扩展到 32K–128K)。 ALiBi 的优点在于无需微调即可实现外推,但其分布内性能会稍逊一筹。目前所有主流开源大语言模型(LLM)都采用了 RoPE。

FlashAttention 是否会影响模型输出?#

不会。 FlashAttention 实现的是精确的注意力计算,只是浮点数归约顺序略有不同。与朴素实现相比,数值差异远低于训练噪声,完全可以忽略不计。

MoE 是否一定比稠密模型更高效?#

计算效率更高,但显存占用更多。如果显存是瓶颈(例如在单张 24GB 显卡上推理),量化的稠密模型通常表现更好。如果显存充足,但在解码阶段受限于 FLOPs, MoE 则更具优势。

对于 7B 参数规模的模型, INT4 量化会损失多少精度?#

使用 GPTQ 或 AWQ 在约 128 条样本的校准集上进行量化后,困惑度(perplexity)通常只会增加不到 2%,大多数下游任务的质量几乎察觉不到变化。对于 70B 参数规模的模型,精度损失一般小于 1%。但如果模型规模小于 7B,量化带来的负面影响会更加显著。

为什么长上下文场景下 KV Cache 成为瓶颈?#

KV Cache 的大小公式为 $2 \cdot L \cdot H_{kv} \cdot d_h \cdot T \cdot \text{字节数}$ 。以 70B 规模的模型为例,在 32K 上下文长度下使用 MHA, KV Cache 占用约 80GB,甚至超过了模型权重本身的大小。 GQA 将其降至约 10GB,而 PagedAttention 则将内存碎片率控制在 5% 以内,两者结合才使得普通硬件也能支持长上下文推理。

本系列

NLP 技术前沿 12 篇

  1. 01 自然语言处理(一):NLP 入门与文本预处理
  2. 02 自然语言处理(二):词向量与语言模型
  3. 03 自然语言处理(三):RNN 与序列建模
  4. 04 自然语言处理(四):注意力机制与 Transformer
  5. 05 自然语言处理(五):BERT 与预训练模型
  6. 06 自然语言处理(六):GPT 与生成式语言模型
  7. 07 自然语言处理(七):提示工程与 In-Context Learning
  8. 08 自然语言处理(八):模型微调与 PEFT
  9. 09 自然语言处理(九):大语言模型架构深度解析 当前
  10. 10 自然语言处理(十):RAG 与知识增强系统
  11. 11 自然语言处理(十一):多模态大模型
  12. 12 自然语言处理(十二):前沿技术与实战应用

读有所得?

GitHub 关注我 → 新文周更

GitHub