大模型(LLM)推理优化技术全景总结

本博客单纯作为我想面试大模型实习岗进行临时抱佛脚的学习,如果有错误欢迎在评论区指正,我会修改的。

引言:为什么推理优化是 LLM 时代的"黄金技能"?

大语言模型(LLM)的参数量动辄百亿、千亿。在学术界,我们关注如何把模型训练出来;但在工业界,真正的挑战在于如何把模型便宜、高效地部署上线

当我们尝试运行一个 LLM 时,通常会遇到两座大山:

  1. Memory Wall(内存墙):大模型的自回归生成特性产生了庞大的 KV Cache,极易导致显存溢出(OOM)。GPU 的高带宽内存(HBM)容量成为了最大瓶颈。

  2. Compute-Bound vs. Memory-Bound:在 Prefill(预填充)阶段,计算是瓶颈;但在 Decode(解码)阶段,频繁的访存使得 GPU 的算力根本吃不满,沦为 Memory-Bound(访存瓶颈)。


核心技术一:显存优化(让 GPU 装下更多请求)

1. 空间换时间的艺术:KV Cache

LLM 生成文本是一个词一个词往外蹦的(Autoregressive)。为了生成第 N+1 个词,模型需要前面 N 个词的上下文。如果不做优化,每次生成新词都要把前面的词重新算一遍 Attention,计算复杂度是 O(N^2)。

KV Cache 的做法是:把历史 Token 的 Key 和 Value 矩阵缓存在显存中。新词只需要计算自己的 Query,并与缓存的 K、V 交互即可。计算复杂度瞬间降维到 O(N)。

痛点:算力省下来了,但随着并发请求增多,KV Cache 会占用海量显存。

2. 操作系统降维打击:PagedAttention (vLLM核心)

传统的 KV Cache 分配是连续的,假设系统预设最大长度为 2048,哪怕用户只生成了 10 个词,也会占用 2048 的显存空间,导致极大的内部碎片

PagedAttention 巧妙地借用了操作系统中的"虚拟内存分页"思想:

  • 将 KV Cache 切分成固定大小的 Block(块)。

  • 逻辑上连续的 Cache,在物理显存上是不连续的。

  • 按需分配:生成多少词,就分配几个 Block。

    通过这种方式,显存浪费率被压缩到了极低的水平,极大地提升了并发上限。


核心技术二:计算与访存优化(打破 Memory Wall)

1. 硬件级榨干性能:FlashAttention

在标准的 Attention 计算中,中间产生的注意力分数矩阵(如 Q * K^T)需要被写入 GPU 的 HBM,然后再读出来做 Softmax。这种极其频繁的 I/O 操作严重拖慢了推理速度。

FlashAttention 的核心逻辑是 Tiling(分块计算)。它将矩阵分块读入到 GPU 中容量极小但速度极快的 SRAM 中,一次性完成 Attention 的全部计算,再将最终结果写回 HBM。

  • 不保存庞大的中间注意力矩阵。

  • 大幅减少了 HBM 的读写次数(Memory Accesses)。

2. 告别 GPU 闲置:Continuous Batching (动态批处理)

在传统的 Static Batching 中,必须等待同批次中"最长"的那个句子生成完毕,GPU 才能处理下一批任务,导致短句子的计算资源被白白闲置。

Continuous Batching(或 In-flight Batching) 实现了在单个迭代步(Iteration)级别的动态调度:只要某个请求生成结束,立刻将其移出 Batch,并塞入新的用户请求。让 GPU 始终处于满负荷运转状态,吞吐量成倍提升。


核心技术三:模型压缩与加速(算法工程师的内功)

除了系统层面的调度,直接从模型结构入手,降低参数量和计算量,是算法优化的另一条主线。

1. 结构化剪枝 (Structural Pruning)

非结构化剪枝(把权重随机置零)对硬件加速非常不友好。在工业界,更倾向于结构化剪枝

例如,我们可以参考类似 Sheared-LLaMA 的方案,直接对庞大的预训练模型进行层数(Depth)或隐藏层维度(Width)的物理裁剪。裁剪后,利用高质量的开源数据集(例如 RedPajama)进行持续预训练(Continual Pre-training),以此来恢复模型的生成能力。这种物理瘦身能带来最直接的推理加速。

2. 模型量化 (Quantization)

将模型原本的 FP16(16位浮点数)权重,压缩为 INT8 甚至 INT4(4位整数)。常见的方案如 PTQ(训练后量化)、AWQ(激活感知权重压缩)等。不仅大幅降低了模型加载的显存门槛,还能利用 Tensor Core 的整数运算单元加速计算。


Python 实战:纯手工撸一个极简版 KV Cache 管理器

为了加深理解,我们用纯 Python + PyTorch 实现一个极其简化的 KV Cache 缓存更新逻辑。

python 复制代码
import torch # 导入 PyTorch 核心库
import torch.nn as nn # 导入 PyTorch 的神经网络模块

class MinimalKVCache(nn.Module): # 继承 nn.Module,定义一个极简版的 KV Cache 模块
    def __init__(self, max_batch, max_seq_length, hidden_dimension): # 初始化函数,需要预设最大批次大小、最大序列长度和隐藏层维度
        super().__init__() # 调用父类的初始化方法
        
        # 【核心技巧】使用 register_buffer 在 GPU 上预先分配一块固定大小的静态显存。
        # 为什么要这么做?因为如果在每次生成新词时都使用 torch.cat 动态拼接张量,
        # 会不断触发底层的显存分配和释放,带来极大的推理延迟(Overhead)。
        self.register_buffer(
            "k_cache", # 注册名为 k_cache 的显存块
            # 初始化一个形状为 [批大小, 序列长度, 隐藏维度] 的全零张量
            # 指定数据类型为 FP16(半精度浮点数)以节省显存,并直接将其加载到 GPU (.cuda())
            torch.zeros(max_batch, max_seq_length, hidden_dimension, dtype=torch.float16).cuda() 
        )
        self.register_buffer(
            "v_cache", # 注册名为 v_cache 的显存块
            # 同理,为 Value 矩阵初始化对应形状的全零 FP16 张量并放入 GPU
            torch.zeros(max_batch, max_seq_length, hidden_dimension, dtype=torch.float16).cuda()
        )
        self.current_length = 0 # 初始化当前已缓存的序列长度指针,起步为 0

    def update_and_retrieve(self, new_k, new_v): # 定义更新缓存并获取完整历史记录的方法,传入新生成的 K 和 V
        """
        将最新生成的 Key/Value 状态追加写入到预先分配好的显存池中,
        并返回截至当前所有的上下文历史记录,供 Attention 计算使用。
        """
        batch_sz, seq_len, dim = new_k.shape # 获取新传入的 K 矩阵的形状:当前批次大小,新增词的长度,隐藏层维度
        start_pos = self.current_length # 确定本次写入的起始位置(即上一次写到了哪里)
        end_pos = start_pos + seq_len # 确定本次写入的结束位置

        # 【原位更新】将新生成的 K 和 V 状态,直接通过索引切片赋值,塞进我们预先分配好的显存块中
        self.k_cache[:batch_sz, start_pos:end_pos, :] = new_k # 把新的 K 贴进去
        self.v_cache[:batch_sz, start_pos:end_pos, :] = new_v # 把新的 V 贴进去
        
        # 更新当前序列长度的指针,向前推进 seq_len 个位置
        self.current_length += seq_len 

        # 切片提取并返回从索引 0 到当前最新长度 (current_length) 的完整上下文
        # 这里的提取操作并不会发生物理显存的拷贝,只是创建了一个逻辑视图(View),非常高效
        return self.k_cache[:batch_sz, :self.current_length, :], \
               self.v_cache[:batch_sz, :self.current_length, :]

大厂实习面试高频 Q&A (这是我让AI生成的)

面试官考察 LLM 优化,往往是从基础概念切入,逐步深挖到底层数据结构。以下是几个经典连环问:

Q1: 为什么 LLM 推理需要 KV Cache?如果不加会怎么样?

答: 因为大模型的生成是自回归的。如果不使用 KV Cache,每次生成第 N 个词时,前面的 N-1 个词都要重新参与计算 Attention。这会导致时间复杂度随着序列长度的增加呈 O(N^2) 爆炸。加上 KV Cache 后,我们缓存了历史的 K 和 V 矩阵,每次只需计算当前新词的 Q,时间复杂度退化为 O(N)。

Q2: 既然 KV Cache 这么好,那它有什么缺点?如何解决?

答: 缺点是极度消耗显存。随着 Batch Size 和上下文长度增加,KV Cache 会占用几十 GB 甚至更多的显存,导致模型哪怕能算得过来,也存不下数据(Out of Memory)。

解决思路主要有:

  1. 系统层面:使用 PagedAttention,将 KV Cache 存放在非连续的显存块中,消除显存碎片。

  2. 算法层面:使用 MQA (Multi-Query Attention)GQA (Grouped-Query Attention),让多个 Query 共享同一组 K 和 V,从物理上缩小 KV Cache 的体积。

Q3: FlashAttention 优化了时间复杂度还是空间复杂度?

答:这是一个常见的陷阱题。 FlashAttention 并没有改变标准 Attention 的数学计算逻辑,所以它的计算时间复杂度依然是 O(N^2),并没有减少乘加操作的数量。

它的核心是降低了空间复杂度 (从 O(N^2) 降到了 O(N),无需实例化庞大的中间分数矩阵),并且通过 Tiling 技术大幅减少了 GPU SRAM 与 HBM 之间的内存读写次数(IO 复杂度),从而打破了访存瓶颈,实现了物理时间上的加速。

Q4: 在做模型压缩(比如结构化剪枝或蒸馏)时,你是如何评估优化效果的?

答: 在工业界做模型级优化时,不能单纯只看某一项特定任务的准确率,更应避免使用局限性较强的指标(例如 TPR 或 FPR 等分类指标),因为这无法反映大模型的泛化能力。

我们会更多地考察跨领域的通用评估指标(Cross-domain indicators)。具体包括:

  1. 生成质量:在不同领域(代码、数学、通识)的数据集上测量困惑度(Perplexity, PPL)的变化。

  2. 系统效率:测量优化前后的吞吐量(Throughput, Tokens/s)以及显存占用效率(Memory Efficiency),这才是衡量工程落地价值的硬核标准。


结语 :更加详细的可以去看这篇知乎的文章万字长文!大模型(LLM)推理优化技术总结(非常详细) - 知乎

相关推荐
Fleshy数模1 小时前
基于 LangChain 实现 PDF 文档检索:从加载到向量检索全流程
人工智能·数据挖掘·langchain·大模型
平行侠1 小时前
A10 恶劣环境传感器信号仿真与统计检验台
算法
洛水水2 小时前
【力扣100题】34.二叉搜索树中第K小的元素
c++·算法·leetcode
_深海凉_2 小时前
LeetCode热题100-翻转二叉树
算法·leetcode·职场和发展
吃好睡好便好2 小时前
在Matlab中绘制抛物三维曲面图
开发语言·人工智能·学习·算法·matlab·信息可视化
Li_yizYa2 小时前
【大模型篇】谈谈对于Function Calling、MCP、Skill的理解
ai·大模型
伯远医学2 小时前
Nat. Methods | 邻近标记技术:活细胞中捕捉分子互作的新利器
java·开发语言·前端·javascript·人工智能·算法·eclipse
SeatuneWrite2 小时前
动态漫软件2026推荐,助力高效创作体验
人工智能·python
AC赳赳老秦2 小时前
文案策划提效:OpenClaw批量生成活动文案、宣传海报配文,适配不同渠道调性
java·大数据·服务器·人工智能·python·deepseek·openclaw