系统级优化
📟 PagedAttention 如何通过分页管理减少显存碎片?¶
💡 PagedAttention 借鉴操作系统的虚拟内存分页思想,将 KV Cache 分割为固定大小的 block(页),允许这些 block 在物理显存中离散分布,通过页表(block table)维护逻辑序列到物理块的映射。这彻底消除了外部碎片,极大降低了内部碎片,使显存利用率从传统方案的 20-30% 跃升至接近 90%。
🔍 背景:传统 KV Cache 的内存痛点
-
在自回归生成中,每个请求都需要存储不断增长的 KV Cache。传统推理系统(如 FasterTransformer)会为每个请求预分配一块连续的最大长度显存(例如最大 2048 tokens)。
-
外部碎片:如果请求实际生成长度远小于最大长度,预留空间大部分被浪费,而且频繁分配和释放不同大小的连续块会导致显存出现大量小空隙,这些空隙无法合并成大块供其他请求使用,形成外部碎片。
-
内部碎片:即使按实际长度动态分配,不同请求的 KV Cache 大小不同,分配器也可能因对齐等产生内部碎片。更严重的是,无法精准共享相同前缀的 KV Cache,导致大量重复存储。
🧱 PagedAttention 的分页机制
-
固定大小的 Block:将 KV Cache 切分为固定大小的物理块(例如每个 block 包含 16 个 token 的 K 和 V)。块大小是配置参数,需权衡碎片率和管理开销。
-
逻辑块映射:每个请求的 KV Cache 由一系列逻辑块组成,逻辑块通过页表映射到物理块。物理块从全局内存池中按需分配,不要求连续。
-
内存池管理:系统初始化时预分配一个巨大的物理块池,后续请求从池中获取空闲块,用完归还。这避免了频繁的 cudaMalloc/cudaFree,同时物理块的离散分配天然无外部碎片。
✨ 减少碎片的体现
-
消除外部碎片:因为所有分配单位是大小相同的 block,所以不会出现“有足够总空闲但无连续大块”的问题。任何空闲 block 都可直接分配给任意请求。
-
控制内部碎片:每个序列的最后一个 block 可能未被完全占满,平均内部碎片约为半个 block 大小。通过选择合适的 block size(例如 16 或 32),可以将此控制在可接受范围。
-
前缀共享:多个请求共享相同的 system prompt 时,它们的逻辑页表可以指向相同的物理块(只读),从而避免为每个请求重复存储前缀 KV Cache,大幅节省显存。这也是分页带来的额外优势。
📊 实际效果(vLLM 论文数据)
-
在相同显存下,PagedAttention 使系统能够同时处理的请求数量提高 2-4 倍,显存利用率从不足 30% 提升到 80-90%。
-
允许动态调整 batch size,减少了排队延迟,同时支持更长的生成。
🔧 工程细节
-
块大小选择:小则碎片少,但页表更大,管理开销增加;大则管理简单,但内部碎片多。常用 16 或 32。
-
回收与重映射:当请求完成,所有物理块被标记为空闲,归还池中。由于使用逻辑-物理映射,无需移动数据。
-
Copy-on-Write 等扩展:某些框架支持对共享块进行写时复制,进一步优化多轮对话等场景。
✅ 总结:PagedAttention 通过分页离散分配打破了显存连续性的桎梏,将碎片降至极低,是 LLM 推理服务的核心内存管理创新。
🎛️ 推理服务中,如何实现多个 LoRA 适配器的显存共享?¶
💡 实现多 LoRA 适配器显存共享的核心思路是:基础模型权重常驻 GPU 且全局共享,多个 LoRA 适配器则以“热插拔”方式动态换入换出 GPU 内存池,仅同时保留活跃适配器于 GPU,其余放 CPU 或 NVMe,通过高效的 I/O 与分页管理使数千个适配器服务于海量用户而不爆显存。
🔌 基础模型自然共享
所有 LoRA 适配器都基于同一个基础模型,因此基础模型的权重(通常占显存 95% 以上)只需在 GPU 中保存一份,天然的共享。真正需要管理的是 LoRA 的 A 和 B 矩阵。
🗂️ 适配器权重的显存管理策略
-
集中存储 + 动态加载 系统维护一个中央适配器仓库(CPU 内存或 SSD)。当请求到达时,调度器根据请求所需的 LoRA ID,将对应适配器权重异步加载到 GPU 的一个预留内存池中。一旦该请求完成,且没有其他请求在使用该适配器,就可以将其卸载,释放 GPU 空间给其他适配器。
-
分页管理 LoRA 权重(类似 PagedAttention) S-LoRA 等先进系统将每个 LoRA 适配器的权重矩阵也划分为固定大小的块(tiles),存储在 GPU 内存池中。不同适配器的块可以在物理内存中交织存放,由统一的页表管理。每个 LoRA 矩阵的加载只需拷贝所需的块,而非整个矩阵。这种方式实现了:
- 细粒度共享:如果两个适配器部分相同(很少见),可共享块。
- 外部碎片消除:所有 LoRA 块大小相同,无外部碎片。
-
高利用率:GPU 内存池被众多适配器分时复用,整体显存占用接近活跃适配器总大小。
-
统一内存池 将基础模型的权重、KV Cache 和 LoRA 权重统一管理在一个分页内存池中(如 Punica、vLLM 的 LoRA 集成)。GPU 显存被划分为不同类型的内存块(KV block, LoRA block),按需分配,全局调度,最大化整体利用率。
-
批量计算与适配器分组 若多个请求使用不同 LoRA 适配器,需要批量执行
y = Wx + B_i A_i x。一些系统通过将同一批中相同适配器的请求分组,减少适配器切换开销。显存共享策略要确保热门的适配器常驻 GPU,冷门的则被换出。
📊 显存节省示例
- 假设一个 LoRA 适配器占 50 MB,1000 个适配器总大小 50 GB,远超单卡显存。通过动态加载,GPU 仅保留当前活跃请求所需的 10 个适配器(500 MB),其余暂存 CPU 内存(占用 50 GB 系统内存但 GPU 显存无压力)。这样一张 24 GB 显卡就能服务上千个定制模型。
⚠️ 挑战
-
切换延迟:从 CPU 拷贝适配器到 GPU 需要时间,必须预取和流水线化以避免增加请求延迟。
-
内存碎片:动态分配固定大小块可缓解,但需精心设计块大小和池容量。
✅ 因此,多 LoRA 适配器显存共享依赖统一的动态内存管理,通过分页与换入换出实现高密度部署,是“模型即服务”的关键支撑。
💾 模型卸载(offload)到 CPU/NVMe 的显存-速度权衡是怎样的?¶
💡 模型卸载将部分模型状态(权重、优化器、KV Cache)从 GPU 显存转移至 CPU 内存或 NVMe SSD,以大幅降低 GPU 显存占用,但代价是计算时将数据通过低带宽总线搬运,导致显著的速度下降。这是典型的“空间换时间”策略,带宽和延迟的鸿沟决定了下降程度。
🔄 卸载层次与带宽对比
| 存储层 | 典型带宽 | 与 GPU HBM 的差距 |
|---|---|---|
| GPU HBM (A100) | 2 TB/s | 基准 |
| CPU 内存 (DDR5) | 50-100 GB/s (通过 PCIe 4.0 x16) | ~20-40x |
| NVMe SSD (PCIe 4.0) | 7 GB/s | ~300x |
| NVMe SSD (PCIe 3.0) | 3.5 GB/s | ~600x |
当计算所需数据不在 GPU 时,必须等待数据搬运,GPU 空闲等待,吞吐大打折扣。
🔍 显存节省 vs 速度损失的具体表现
-
卸载优化器状态(训练场景) ZeRO-Offload 将 Adam 优化器状态(动量、方差)放在 CPU,GPU 仅保留参数和梯度。更新参数时,梯度传到 CPU,CPU 执行优化器步骤,再将更新后的参数传回 GPU。由于优化器状态大小是权重的 4 倍,显存节省巨大。但每步增加两次完整的参数传输(梯度→CPU,参数→GPU),加上 CPU 计算。通常导致 20-50% 的吞吐下降,但使单卡能训练数十 B 模型。
-
卸载模型参数(推理场景) 推理时可将不常用的层或整个模型权重放在 CPU,计算时逐层预取。例如 llama.cpp 支持部分层卸载到 CPU。每层需要传输权重矩阵,会严重降低生成速度,尤其对于小 batch。典型地,卸载一半层到 CPU 可能导致速度下降 3-5 倍。
-
卸载 KV Cache(推理场景) 长序列推理中,可将冷 KV Cache 块卸载到 CPU 内存,需要时换入。vLLM 等支持 swap 到 CPU。由于 KV Cache 访问频繁,即使部分卸载,延迟也会上升,但可处理更长上下文。
⚖️ 权衡决策
-
何时值得:当 GPU 显存绝对不足,无法运行目标模型时,卸载是唯一解法。例如在 24GB 卡上推理 70B 量化模型,必须部分卸载。
-
如何优化:通过异步预取(prefetch)和计算重叠隐藏传输延迟。DeepSpeed ZeRO-Infinity 可以在计算当前层时提前从 NVMe 预取下一层,实现部分隐藏。但硬件带宽底层限制仍在。
-
效果实例:使用 ZeRO-Infinity 在单张 V100 32GB 上训练 30B 模型,比纯 GPU 训练慢 3-5 倍;推理 175B 模型,生成速度降至数秒一个 token,但可行。
📊 速度-显存曲线(定性)
-
完全 GPU:速度 100%,显存 100%。
-
卸载优化器到 CPU:速度 70-80%,显存可降至 40%。
-
卸载参数到 CPU:速度 20-40%,显存可降至 10%。
-
卸载到 NVMe:速度 5-15%,显存可降至几乎无限(仅 GPU 保留极少工作集)。
✅ 总结:卸载是解决显存不足的终极手段,以大幅牺牲速度为代价换取模型运行能力。实际应用中,总是优先用量化、并行等其他方法,卸载作为保底方案。
🏊 内存池(memory pool)技术在大模型推理中的应用¶
💡 内存池是一种预分配一大块显存并自行管理分配与回收的技术,大模型推理中主要用于管理 KV Cache 和临时张量,以消除频繁的 cudaMalloc/cudaFree 开销,避免碎片,并实现动态批量处理。
🔧 内存池的实现方式
-
固定大小块池(Block Pool) 这是 PagedAttention 的核心。系统预先分配成千上万个固定大小的物理块(如每块容纳 16 个 token 的 K、V),这些块构成一个空闲池。请求到来时,从池中获取空闲块分配给该请求的 KV Cache;请求完成,块回收至池中。因为块大小统一,分配/回收仅为 O(1) 的链表操作,极快且无碎片。
-
分层内存池 一些系统(如 TensorRT-LLM)将不同生命周期的对象放入不同池:如权重池(静态,生命周期与应用同)、激活池(临时,每层复用)、KV Cache 池等。通过显式内存复用,可精确控制峰值显存。
-
PyTorch CUDA Caching Allocator PyTorch 自带的内存池机制:它拦截 cudaMalloc,维护一个缓存区。释放的张量空间不会立即归还给驱动,而是保留在缓存中,下次分配同样大小时直接复用,减少分配开销。但这只是通用池,缺乏语义层优化。
🚀 在大模型推理中的典型应用
-
vLLM 的 KV Cache 池: 初始化时根据可用显存创建尽可能多的 KV blocks,存入池中。调度器根据请求长度和当前占用,动态分配块。当总需求超过池容量时,可抢占低优先级请求,回收其块(swap 到 CPU 或丢弃)。这种池化设计保证了显存的有效利用和弹性调度。
-
多 LoRA 的权重池: S-LoRA 将 LoRA 权重也切块放入池,与 KV Cache 池统一管理。池的大小决定了可同时驻留的 LoRA 适配器数量和可支持的并发请求数。
-
激活内存池: 部分推理引擎(如 ONNX Runtime)会在模型初始化时分析计算图,预分配最大激活所需的一片内存,运行时通过 offset 重复使用,杜绝动态分配。
📊 内存池的优势
-
低碎片化:固定块或区域复用杜绝了外部碎片。
-
高吞吐:快速分配/回收使得能够高频调度大量请求,提升 GPU 利用率。
-
可预测性:预分配和池化使显存占用上限可预测,方便进行容量规划和限制。
-
支持弹性抢占:可基于池的占用情况做出换入换出决策,实现平滑的过载管理。
🔍 实际配置:vLLM 中,通过 --gpu-memory-utilization 设定池可使用的显存比例,剩下的留给权重和激活。例如设为 0.9,则 90% 的显存用于 KV 块池,10% 为其他。
✅ 因此,内存池是大模型推理系统中不可或缺的显存管理基石,它通过预分配和复用机制将显存利用率推至极致,支撑高并发服务。
☸️ 在 Kubernetes 集群中,如何设置 GPU 显存限制?¶
💡 Kubernetes 原生仅支持以整数卡数(nvidia.com/gpu)为粒度的 GPU 分配,不提供细粒度的显存限制。要限制单容器的 GPU 显存,必须借助第三方 GPU 共享方案、MIG(多实例 GPU)或硬件虚拟化技术,将物理 GPU 切分为更小的显存单元。
📊 常见方案对比
| 方案 | 原理 | 是否硬件隔离 | 显存限制方式 |
|---|---|---|---|
| NVIDIA MIG | 硬件分区(A100/A30/H100) | 是 | 每个 MIG 实例有固定显存和计算资源 |
| NVIDIA Time-Slicing | GPU Operator 时分复用 | 否(进程间无显存隔离) | 不限制显存,仅限制并发 |
| vGPU / GPU 虚拟化 | 软件虚拟化(如 NVIDIA vGPU, 腾讯云 cGPU) | 半隔离 | 可限制显存,显存和算力配额 |
| 开源 GPU 共享调度器 | 基于 CUDA API 劫持或自定义设备插件(如 Hami, Volcano) | 软隔离 | 在容器内设置显存使用上限 |
🔧 具体实施方式
- 使用 MIG(最硬隔离,推荐生产)
- 配置节点 MIG 模式,将一张 A100-80GB 划分为多个 GPU 实例(如 7 个 10GB 的 1g.10gb 实例)。
- K8s 中通过
nvidia.com/mig-1g.10gb这样的资源声明请求 MIG 实例。Pod 会绑定到特定的 MIG 实例,显存上限由该实例大小决定,绝对隔离。 -
优点:性能稳定,无相互干扰;缺点:仅部分 GPU 支持,切分不够灵活。
-
基于 Nvidia GPU Operator 的时分复用
- 配置
time-slicing可让多个 Pod 共享同一 GPU,但不会限制显存。每个容器仍可尝试使用全部显存,可能引发 OOM。 -
需要结合应用层的显存限制(如设置
PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb或框架内置的gpu-memory-utilization),但非强隔离。 -
GPU 共享调度器(软件限制)
- 部署如 Hami(原 kube-batch)、Volcano 等,它们提供自定义设备插件,暴露
nvidia.com/gpumem资源。 - 在 Pod 中设置
resources.limits.nvidia.com/gpumem:`` 8Gi,调度器会确保节点上所有 Pod 的显存声明之和不超过物理总量,并为容器注入环境变量或内核拦截来限制实际显存使用。 - 例如 Hami 通过修改 CUDA 库的显存分配函数,限制进程内可见显存大小,实现软件层面的显存限制。
-
优点:灵活,支持超售;缺点:软件限制可能被绕过,且存在性能干扰。
-
云厂商的 GPU 共享方案
- 阿里云 ACK 支持
aliyun.com/gpu-mem,腾讯云 TKE 支持tencent.com/vcuda-core和tencent.com/vcuda-memory,它们都是通过虚拟化驱动实现显存和算力的隔离。直接在 Pod 中设置此类扩展资源即可。
📝 示例 YAML(使用 Hami)
apiVersion: v1
kind: Pod
metadata:
name: gpu-test
spec:
containers:
- name: app
image: my-llm:latest
resources:
limits:
nvidia.com/gpu: 1 # 请求1卡
nvidia.com/gpumem: 8192 # 限制显存 8GiB
⚠️ 注意事项
-
软件显存限制通常依赖
LD_PRELOAD劫持 CUDA 库,要求容器内 CUDA 版本与宿主兼容。 -
显存超售(总和超过物理容量)可能导致 OOM 杀 Pod,需设置合适的调度策略。
-
监控和日志对排查显存瓶颈至关重要,可配合 Prometheus GPU Exporter。
✅ 总结:在 K8s 中实现 GPU 显存限制需借助 MIG(最优)或第三方软件方案(灵活)。选择方案需权衡隔离性、灵活性和硬件兼容性。