推理引擎架构深度解析:从内存管理到计算调度的全栈优化设计

推理引擎架构深度解析:从内存管理到计算调度的全栈优化设计

一、推理引擎的性能瓶颈:框架开销不容忽视

大模型推理的性能,模型本身只占一部分,推理引擎的架构设计同样关键。拿 LLaMA-7B 来说,用原生 PyTorch 跑,延迟大概在 150ms/token;换成 llama.cpp 能压到 30ms/token;要是用 vLLM 在高并发下,吞吐量甚至是 PyTorch 的 10 倍。这些差距不是硬件或模型带来的,而是推理引擎在内存管理、计算调度和并发控制上的架构差异。

推理引擎的核心任务很明确:在 GPU 显存有限的情况下,既要最大化吞吐量(tokens/s),又要最小化首 token 延迟(TTFT)。但这俩目标经常打架------高吞吐需要大 batch size,大 batch 又意味着显存占用和计算延迟增加。架构设计的本质,就是在这些约束里找平衡点。

二、推理引擎的核心架构与数据流转

推理引擎可以拆成五个核心子系统,数据在它们之间流转的方式直接决定了端到端的性能。

flowchart TB A[请求调度器] --> B[KV Cache 管理器] B --> C[算子执行引擎] C --> D[内存分配器] D --> E[采样与解码器] subgraph 请求调度 A1[连续批处理: 动态合并请求] A2[优先级调度: 首token优先] A3[抢占机制: 显存不足时换出] end subgraph KV Cache管理 B1[PagedAttention: 分页存储KV] B2[前缀缓存: 共享系统提示] B3[淘汰策略: LRU释放长序列] end subgraph 算子执行 C1[FlashAttention: IO感知注意力] C2[算子融合: QKV+Softmax融合] C3[量化推理: INT8/INT4加速] end subgraph 内存管理 D1[显存池化: 预分配避免碎片] D2[CPU卸载: 溢出到主机内存] D3[张量并行: 跨GPU切分] end A --> A1 & A2 & A3 B --> B1 & B2 & B3 C --> C1 & C2 & C3 D --> D1 & D2 & D3

请求调度器是引擎的"大脑"。连续批处理(Continuous Batching)能把不同时间到的请求动态合到一个 batch 里,不用等所有请求都跑完再开下一轮。这能大幅提升 GPU 利用率------传统静态批处理里,短请求跑完了 GPU 还得等长请求,算力就浪费了。

KV Cache 管理器是引擎的"心脏"。自回归生成时,每生成一个 token 都得重新算前面所有 token 的 Key 和 Value,KV Cache 就是把这些中间结果存起来避免重算。但 KV Cache 占的显存会随序列长度线性增长,是显存瓶颈所在。PagedAttention(vLLM 的核心)把 KV Cache 分页存,按需分配释放,消除了预分配带来的浪费。

算子执行引擎负责张量计算。FlashAttention 用分块计算(Tiling)减少 HBM 访问,把注意力计算从内存带宽瓶颈转成计算瓶颈。算子融合把 QKV 投影、注意力计算和输出投影合到一个内核里,省去了中间张量的显存读写。

内存分配器管 GPU 显存生命周期。显存池化预分配大块显存,按需切分给不同请求,避免频繁 cudaMalloc/cudaFree 和显存碎片。CPU 卸载在显存不够时把不活跃的 KV Cache 挪到主机内存,需要时再拉回 GPU。

采样与解码器把模型输出的 logits 转成 token。支持多种采样策略(贪心、Top-K、Top-P、温度缩放),还要处理停止词检测和特殊 token。

三、推理引擎核心组件的代码实现

下面这段代码展示了连续批处理调度器和 PagedAttention 风格的 KV Cache 管理器的核心逻辑。

python 复制代码
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum
import time


class RequestStatus(Enum):
    """请求状态枚举"""
    WAITING = "waiting"       # 等待调度
    RUNNING = "running"       # 正在推理
    PREEMPTED = "preempted"   # 被抢占(显存不足)
    COMPLETED = "completed"   # 推理完成


@dataclass
class InferenceRequest:
    """推理请求"""
    request_id: int
    prompt_tokens: list[int]       # 输入 token 序列
    max_tokens: int                # 最大生成长度
    generated_tokens: list[int] = field(default_factory=list)
    status: RequestStatus = RequestStatus.WAITING
    arrival_time: float = 0.0      # 请求到达时间
    priority: int = 0              # 优先级(首 token 请求优先)


@dataclass
class KVBlock:
    """KV Cache 分页块:固定大小的存储单元"""
    block_id: int
    size: int = 16                 # 每块存储 16 个 token 的 KV
    ref_count: int = 0             # 引用计数(支持前缀共享)


class PagedKVCache:
    """
    PagedAttention 风格的 KV Cache 管理器
    将 KV Cache 分页存储,按需分配和释放,消除预分配浪费
    """

    def __init__(self, total_blocks: int, block_size: int = 16):
        self.block_size = block_size
        # 空闲块池:所有可分配的 KV 块
        self.free_blocks: list[KVBlock] = [
            KVBlock(block_id=i, size=block_size) for i in range(total_blocks)
        ]
        # 每个请求占用的块列表
        self.request_blocks: dict[int, list[KVBlock]] = {}
        # 物理块表:块ID → 实际数据存储位置
        self.block_table: dict[int, Optional[bytes]] = {}

    def allocate(self, request_id: int, num_tokens: int) -> bool:
        """
        为请求分配 KV Cache 块
        返回 True 表示分配成功,False 表示显存不足
        """
        num_blocks_needed = (num_tokens + self.block_size - 1) // self.block_size
        num_blocks_existing = len(self.request_blocks.get(request_id, []))
        num_blocks_to_alloc = num_blocks_needed - num_blocks_existing

        if num_blocks_to_alloc <= 0:
            return True  # 已有足够块

        if len(self.free_blocks) < num_blocks_to_alloc:
            return False  # 显存不足

        # 从空闲池中取出所需块数
        if request_id not in self.request_blocks:
            self.request_blocks[request_id] = []

        for _ in range(num_blocks_to_alloc):
            block = self.free_blocks.pop()
            block.ref_count += 1
            self.request_blocks[request_id].append(block)

        return True

    def free(self, request_id: int) -> int:
        """
        释放请求占用的所有 KV Cache 块
        返回释放的块数量
        """
        if request_id not in self.request_blocks:
            return 0

        freed = 0
        for block in self.request_blocks[request_id]:
            block.ref_count -= 1
            if block.ref_count == 0:
                # 引用计数归零,块可被复用
                self.free_blocks.append(block)
                freed += 1

        del self.request_blocks[request_id]
        return freed

    def get_num_free_blocks(self) -> int:
        return len(self.free_blocks)


class ContinuousBatchScheduler:
    """
    连续批处理调度器
    动态合并请求到同一 batch,短请求完成后立即释放资源
    """

    def __init__(self, kv_cache: PagedKVCache, max_batch_size: int = 32):
        self.kv_cache = kv_cache
        self.max_batch_size = max_batch_size
        self.waiting_queue: list[InferenceRequest] = []
        self.running_batch: list[InferenceRequest] = []

    def add_request(self, request: InferenceRequest) -> None:
        """添加新请求到等待队列"""
        request.arrival_time = time.time()
        request.priority = 1  # 新请求优先调度以降低首 token 延迟
        self.waiting_queue.append(request)

    def schedule(self) -> list[InferenceRequest]:
        """
        调度一轮推理:从等待队列中选择请求加入运行批次
        考虑显存容量和 batch size 限制
        """
        # 先处理已完成的请求,释放 KV Cache
        completed = [r for r in self.running_batch
                     if len(r.generated_tokens) >= r.max_tokens]
        for req in completed:
            req.status = RequestStatus.COMPLETED
            self.kv_cache.free(req.request_id)

        self.running_batch = [r for r in self.running_batch
                              if r.status == RequestStatus.RUNNING]

        # 从等待队列中补充请求
        still_waiting = []
        for req in self.waiting_queue:
            if len(self.running_batch) >= self.max_batch_size:
                still_waiting.append(req)
                continue

            # 尝试为新请求分配 KV Cache
            total_tokens = len(req.prompt_tokens) + 1  # 至少生成1个token
            if self.kv_cache.allocate(req.request_id, total_tokens):
                req.status = RequestStatus.RUNNING
                self.running_batch.append(req)
            else:
                # 显存不足,请求保留在等待队列
                still_waiting.append(req)

        self.waiting_queue = still_waiting
        return self.running_batch

    def preempt_if_needed(self) -> Optional[InferenceRequest]:
        """
        抢占机制:当显存严重不足时,换出最低优先级的运行请求
        被抢占的请求的 KV Cache 被释放,请求回到等待队列
        """
        if self.kv_cache.get_num_free_blocks() > 0:
            return None

        # 找到优先级最低的运行请求(已生成最多 token 的请求)
        victim = min(self.running_batch,
                     key=lambda r: r.priority * 1000 - len(r.generated_tokens))

        # 释放其 KV Cache
        self.kv_cache.free(victim.request_id)
        victim.status = RequestStatus.PREEMPTED
        self.running_batch.remove(victim)

        # 重新加入等待队列,降低优先级防止饥饿
        victim.priority = 0
        self.waiting_queue.append(victim)
        return victim

代码里 PagedKVCache 实现了分页 KV Cache 管理,按需分配释放显存块,用引用计数支持前缀共享。ContinuousBatchScheduler 实现了连续批处理调度,动态合并请求,显存不够时触发抢占。

四、推理引擎架构的权衡与适用场景

PagedAttention 的碎片开销:分页存储引入了块表查找的间接开销,块大小选得不好会影响内部碎片率。块太小块表太大、查找慢;块太大内部碎片又严重。vLLM 实验发现,块大小设为 16 个 token 时大多数场景下平衡最好。

连续批处理的延迟抖动:新请求加入 batch 会增加单步推理的计算量,正在跑的请求延迟就会增加。对延迟敏感的场景(比如对话 AI),得限制最大 batch size 或者设个延迟预算。

CPU 卸载的带宽瓶颈:KV Cache 在 GPU 和 CPU 之间传,受 PCIe 带宽限制。PCIe 4.0 x16 理论带宽大概 32GB/s,一个 7B 模型的 KV Cache 可能有数 GB,卸载加载的延迟能到几百毫秒。CPU 卸载只适合吞吐优先、对延迟不敏感的场景。

适用边界:这架构适合大语言模型的在线推理服务,尤其是多用户并发场景。单用户离线推理(比如批量文本生成)用简单的顺序推理更高效,不需要复杂的调度和显存管理。小模型(< 1B 参数)KV Cache 显存占用不是瓶颈,PagedAttention 收益有限。

五、总结

推理引擎架构的核心是在显存约束下最大化吞吐量并控制延迟。PagedAttention 解决了 KV Cache 的显存浪费,连续批处理提升了 GPU 利用率,抢占机制保证了显存不足时的系统稳定性。落地建议:优先实现连续批处理调度,这是吞吐量提升最明显的优化;KV Cache 管理用分页方案,块大小设 16 个 token;显存不足时通过抢占释放资源,别直接拒绝新请求。架构选择得看实际场景的延迟和吞吐需求,没有通用的最优方案。

相关推荐
A8ai_napiai几秒前
GPT-5.6三档定档7月7日+GEO市场爆发+Anthropic最严封禁:模型商的价格战与AI搜索的新战场
人工智能·gpt
IT_陈寒36 分钟前
React的setState竟然不是立刻生效的,害我调试半天
前端·人工智能·后端
腾讯云大数据41 分钟前
腾讯云大数据计算智能:从结构化 SQL 到多模态 AI Workload 的融合范式
大数据·人工智能·腾讯云
AI职业加油站1 小时前
大数据采集工程师:技术栈全景图与实战路径
大数据·人工智能·数据分析
code 旭1 小时前
不会编程也能做AI 量化交易工具:基于 MCP 协议的自然语言交易思路不会编程也能做AI 量化交易工具:基于 MCP 协议的自然语言交易思路
人工智能·ai·量化交易·mcp
AI服务老曹1 小时前
视觉算法模型管理完整流程:多版本上线、灰度发布与回滚的落地实践
人工智能·docker·音视频
机器之心2 小时前
Anthropic发现Claude「类意识工作台」!神秘J空间藏着没说出口的想法
人工智能·openai
AI实践录2 小时前
大模型架构:理解大模型预测输出文本的底层逻辑
人工智能
Lifangyun_WD2 小时前
昇腾910B vs NVIDIA A100/H100:国产GPU算力租赁选型与迁移实践
人工智能·gpu算力·ai算力·算力租赁·gpu云主机
卡卡罗特AI2 小时前
GPT5.6 今晚全量开发?Codex上线史上最强Coding模型!
人工智能·ai编程