跳转至

推理服务中的显存管理

⚙️ 推理服务启动时,如何预分配显存?什么是 block preallocation?

推理服务启动时的显存预分配是为了避免运行时动态分配带来的碎片和延迟。主流框架(如 vLLM、TensorRT-LLM)的做法是:

  • 先加载模型权重,占据一部分显存。

  • 剩下的可用显存几乎全部划为一个KVCache 块池,按固定大小的 block 预先分配好。

  • 后续请求的 KV Cache 直接从池中取用空闲 block,用完归还。

什么是 block preallocation?

Block preallocation 就是上述的“块预分配”:把显存切分成许多同等大小的物理块(如每块 16 个 token 的 K/V),形成一个空闲块池。每个请求的逻辑 KV Cache 由若干逻辑块组成,通过页表映射到物理块。

  • 优势:消除外部碎片、分配/回收 O(1)、支持共享前缀。

  • 配置:可通过 --gpu-memory-utilization 控制池大小(例如 vLLM 中设为 0.9 表示用剩余显存的 90% 建池),剩下的保留给激活和临时缓冲区。


🧩 动态批处理中,不同请求的序列长度不同,如何避免显存碎片?

核心方案:PagedAttention 的分块管理

传统做法为每个请求预留最长序列的连续显存,导致严重内部碎片和外部碎片。动态批处理下,多个请求长度各异,PagedAttention 将 KV Cache 分割为固定大小的 block:

  • 每个请求只需按需分配若干 block,最后一个 block 会有少许内部碎片(平均半块),但外部碎片彻底消失,因为所有分配单位大小相同。

  • 请求结束后,其占用的 block 被整体回收到空闲池,供后续请求使用,不会产生不可合并的孔洞。

辅助措施:

  • 使用内存池的统一分配器,避免直接 cudaMalloc。

  • 动态调整 batch 成员,尽早移除已完成的长请求,释放 block。

  • 结合 prefix caching 共享相同前缀的 block,减少重复分配。


🔗 前缀缓存(Prefix Caching)如何识别相同前缀并复用 KV Cache?

识别机制:

  • 推理框架(如 vLLM)在接收到请求时,会对 prompt 计算哈希(例如 token 序列的前缀哈希)。

  • 系统维护一个 前缀哈希表,记录每个前缀对应的 KV 块列表(即物理块 ID 序列)。

  • 当新请求的前缀哈希命中时,直接将该前缀的物理块映射到新请求的逻辑页表中,不需要重新计算。

复用过程:

  1. 新请求到达,调度器提取 prompt token。

  2. 逐 token 或逐块计算哈希,查找是否有相同前缀的现成 KV 块。

  3. 如果命中,就直接共享那些物理块,并设置只读标志;新请求从该前缀之后开始计算。

  4. 对于不匹配的后缀,分配新块继续生成。

写时复制(Copy-on-Write):

如果某个请求要修改共享的 KV(理论上生成阶段不会修改已有的 K/V,但某些场景可能需要),可使用写时复制保护共享块,但常见的自回归推理中共享块是只读的,无需复制,直接复用即可。


💬 多轮对话中,何时清理或压缩 KV Cache?

多轮对话中,KV Cache 随对话历史不断增长,必须在适当时机清理或压缩,否则会耗尽显存。

  1. 达到最大上下文长度时 当总 token 数超过模型最大长度(或预设的 max_model_len),框架可以:

  2. 滑动窗口丢弃:只保留最近的 token,剔除最早的 KV 块(如 Mistral 的 sliding window attention)。

  3. 截断历史:重置对话,仅保留系统提示的 KV Cache,并重新编码最近的几轮。

  4. 显存压力触发 swap 或 eviction

  5. vLLM 支持将不活跃的对话 KV Cache 块交换到 CPU 内存,释放 GPU 块给新请求。

  6. 当 GPU 块池空闲率低于阈值时,可选择暂停最旧的对话,将其 KV 块换出,待 GPU 有空时再换回继续生成。

  7. 对话轮次或 token 限制

  8. 主动设定每对话最多保留 N 轮历史,超过则删除最早轮次的 KV。

  9. 或限制总 token 数,达到上限后清除所有历史(除 system prompt 外)重新开始。

  10. 请求完成后立即回收

对话结束时,该请求的所有 KV 块被归还池中,显存瞬间释放。


🧑‍🤝‍🧑 如何为每个用户或会话分配独立的 KV Cache 空间?

逻辑独立,物理共享池

实际不会为每个用户预先划分一块物理显存,那样会浪费且不灵活。正确做法是:

  • 所有用户的 KV Cache 统一从一个全局 block 池 中分配。

  • 每个会话(请求)维护自己的逻辑页表,指向池中的物理块。

  • 会话之间通过逻辑隔离,无法访问对方的物理块。

分配策略:

  • 当用户请求到达,调度器从空闲池中取出 block 分配给它,并记入该用户的页表。

  • 可以设置每个会话的最大块数限制(例如依据 max_tokens 计算),防止某个会话垄断资源。

  • 对于优先级高的用户,可赋予更多 block 配额或可抢占低优会话的物理块(通过 swap)。

多租户场景:

利用 PagedAttention 的分页特性,可以在保障隔离的同时实现极高的物理内存利用率,不存在物理分区碎片。


🚦 当显存不足时,推理服务如何优雅降级?

目标:避免服务崩溃,尽可能响应更多请求,或以降级方式继续服务。

策略:

  1. 请求排队与限流:当空闲 block 数低于安全阈值,拒绝接受新的请求,放入等待队列,返回重试信号(HTTP 429)。

  2. 抢占与交换:暂停执行中的低优先级请求,将其 KV Cache 换出到 CPU 内存,腾出 GPU 块给新请求或继续执行高优请求。完成后可选择换回。

  3. 缩短生成长度:动态减少 max_new_tokens,限制请求的最终 KV Cache 尺寸。

  4. 早期截断与摘要:对于超长 prompt,只保留其摘要或后半部分,牺牲质量换取空间。

  5. 卸载模型副本:如果启用多 LoRA 或多模型,卸载不活跃的适配器或模型到 CPU。

  6. 返回部分结果:若某请求生成中被抢占,可选择将已生成的内容返回,标记未完成。

实现:vLLM 具有自动 swap 和抢占机制;配合调度器,可在显存紧张时保持可用性。


📈 并发请求不断增加,显存分配失败怎么办?

分配失败通常意味空闲块耗尽,系统必须做出反应:

  1. 立即拒绝新请求,返回错误码,防止 OOM 扩散。

  2. 触发 swap:将一些等待中的请求或低优先级请求的 KV 块移到 CPU,腾出物理块。

  3. 杀死最旧或最低优先级请求:释放其全部块,让高优请求继续。

  4. 动态降低 max_num_seqs:限制并发数,使已接受的请求能顺利完成。

  5. 扩容:如果是集群,自动向调度器申请更多 GPU 节点,迁移部分负载(需要模型多实例支持)。

预防机制:

  • 合理设置 gpu_memory_utilization 留有余地。

  • 监控 num_free_blocks,提前告警和限流。

  • 使用请求超时策略,避免无效长请求占着资源。


🚧 为什么推理服务需要限制最大并发数?

如果不限制最大并发数:

  • KV Cache 池耗尽,无法为新请求分配 block,导致大量请求失败或 OOM。

  • 调度开销陡增:动态批处理中,并发过多使得每步要处理大量请求,增加延迟和 GPU 内核启动开销。

  • 显存碎片风险:虽然分页减轻碎片,但极高并发下块池频繁分配回收,可能引起性能抖动。

  • 公平性问题:少数用户可能提交大量请求,挤占其他用户资源。

设置方法:

  • --max-num-seqs(vLLM)硬限制同时进行的序列数,超过排队。

  • 根据显存大小和模型结构估算最大可服务 token 数,换算为并发数。


🏊 推理框架中的显存池(memory pool)是如何设计的?

设计思路:

  • 预分配大块:启动时根据剩余显存一次性分配一个超大连续区域(或几大块)。

  • 分块管理:将该区域划分为固定大小的 block(如 16 token 的 KV 块),通过位图、链表或空闲栈管理。

  • 统一调度:不同用途的 block(KV Cache、LoRA 权重、激活)可能位于同一个池的不同分区,或分别建池。

典型实现(vLLM):

  • 使用 C++ 写的缓存管理器,维护 block 的分配和释放。

  • 每个 block 有物理 ID,逻辑块通过页表映射。

  • 分配时 O(1) 从空闲栈取出,回收时 push 回栈,无碎片。

  • 支持 block 复制、swap in/out。

优势:避免 cudaMalloc 开销,提供可预测的延迟,支撑高并发。


🔌 推理时,多 LoRA 适配器切换如何做到零显存开销?

“零显存开销”指 GPU 显存中同时只驻留当前活跃的 LoRA 权重,切换时不增加额外永久占用。实现原理:

  1. 基础模型常驻,LoRA 按需加载 GPU 显存中始终只保留基础模型权重。LoRA 适配器存在 CPU 内存或 SSD 上。

  2. 分页 LoRA 权重管理(S-LoRA) 类似 KV 块池,将 LoRA 矩阵也切成固定大小块,统一放在一个 LoRA 块池中。GPU 只为当前需要执行的 LoRA 请求加载对应的块,其他适配器的块保留在 CPU。切换就是换入换出块,不占额外显存。

  3. 预取与流水线 调度器根据请求队列提前将即将使用的 LoRA 块从 CPU 异步预取到 GPU 的空闲块中,完成计算后如果不再需要,该块可被覆盖。这样 GPU 上 LoRA 块的总占用保持恒定(等于池大小),不会随适配器数量增长。

  4. 合并计算 对于同一适配器的请求,尽量批量执行,减少块换入换出频率。

因此,只要 LoRA 池大小经过合理设置,就可以支持成千上万个适配器而不额外增加显存占用。切换时仅在池内替换块,消耗几乎为零。


☸️ 在 Kubernetes 中,如何为推理 Pod 设置 GPU 显存 limit?

💡 核心矛盾:Kubernetes 原生仅支持以整卡数 (nvidia.com/gpu) 作为资源请求和限制,无法直接设置显存容量。必须借助 GPU 共享方案 或 MIG 来实现细粒度的显存隔离和限制。

🔧 主流方案对比:

方案 原理 如何设置显存限制 隔离性
NVIDIA MIG 硬件级分区,将单卡划为多个独立实例 通过 MIG 预划分固定显存大小的实例,Pod 请求特定实例类型 nvidia.com/mig-1g.10gb 强隔离,无干扰
HAMi (原 kube-batch) 基于 CUDA API 劫持,软件限制显存 设置资源 nvidia.com/gpumem: 8192(单位 MiB),配合 nvidia.com/gpucores 限制算力 软隔离,可超售
NVIDIA GPU Operator (Time-slicing) 时分复用,无显存限制,仅限制并发 不限制显存,只划分时间片,无法防止 OOM 无隔离
云厂商 vGPU (如腾讯云 cGPU, 阿里云 cGPU) 虚拟化驱动实现显存和算力隔离 直接设置 tke.cloud.tencent.com/qgpu-memory 或 aliyun.com/gpu-memory 硬/半隔离

📌 推荐方案及 YAML 示例:

方案一:使用 MIG(生产推荐,强隔离)

apiVersion: v1
kind: Pod
metadata:
  name: mig-inference
spec:
  containers:
  - name: llm
    image: my-vllm:latest
    resources:
      limits:
        nvidia.com/mig-1g.10gb: 1   # 请求一个 10GB MIG 实例

需提前将 GPU 配置为 MIG 模式,并部署 NVIDIA K8s Device Plugin。

方案二:使用 HAMi(灵活,可超售)

apiVersion: v1
kind: Pod
metadata:
  name: hami-inference
spec:
  containers:
  - name: llm
    image: my-vllm:latest
    resources:
      limits:
        nvidia.com/gpu: 1            # 请求1卡
        nvidia.com/gpumem: 8192      # 限制显存 8GB
        nvidia.com/gpucores: 30      # 限制30%算力(可选)

HAMi 通过 LD_PRELOAD 注入自定义 CUDA 库,监控并限制显存分配,若超限则返回 OOM。适合非关键业务或开发环境。

⚠️ 注意事项:软件限制可能被绕过,且存在性能干扰;MIG 的分配粒度有限,可能造成资源浪费。


🧱 使用 GPU MIG 如何隔离显存?

💡 MIG(Multi-Instance GPU)从硬件层面将一张物理 GPU 切分为多个独立的 GPU 实例,每个实例拥有专用的显存、缓存和计算核心,彼此完全隔离,就像多张小 GPU。

🔍 隔离机制:

  • 显存专用:每个 MIG 实例的显存被物理分区,无法访问其他实例的显存。例如 A100-80GB 可划分为 7 个 10GB 的 1g.10gb 实例 + 剩余空闲。

  • 错误隔离:一个实例的故障不影响其他实例。

  • 带宽与缓存隔离:L2 缓存、DRAM 带宽也被划分,保证服务质量。

📋 配置步骤示例(节点上):

# 启用 MIG 模式
sudo nvidia-smi -i 0 -mig 1
# 创建 MIG 实例 (以 A100 为例)
sudo nvidia-smi mig -cgi 5,5,5,5,5,5,5 -C

之后重启 NVIDIA K8s Device Plugin,会自动发现 MIG 资源并暴露为 nvidia.com/mig-...

🔗 在 Pod 中使用:

resources:
  limits:
    nvidia.com/mig-1g.10gb: 1

这样该 Pod 就只能看到那 10GB 的实例,即使模型尝试分配更多也会 OOM,但不会影响其他 MIG 实例。

✅ 适用场景:需要严格性能隔离的多租户推理服务,或模型大小恰好与 MIG 实例匹配时效率最高。


📊 推理服务中,如何监控每个请求的显存消耗?

💡 推理框架通常以 block 为单位管理 KV Cache,无法直接给出单请求字节数,但可以通过每个请求分配的逻辑块数量与块大小换算。框架暴露的指标是关键。

🔧 具体方法:

  1. 利用框架内置 Metrics
  2. vLLM 的 /metrics 端点提供 vllm:kv_cache_usage_perc 和每个请求的 vllm:request_prompt_tokensvllm:request_generation_tokens
  3. 可通过公式估算:单请求KV Cache ≈ (prompt_len + generation_len) × 每token_kv_bytes
  4. TGI 有 tgi_request_duration_seconds 等,也可用 token 数估算。

  5. 自定义调度器回调 在 vLLM 中,可以注册 StatLogger 或通过调度器 API 获取每个序列块数。例如在请求完成时记录 seq.get_num_blocks() 乘以 block_size 得到占用。

  6. 运行时显存采样 若需精确到字节,可在请求前后调用 torch.cuda.memory_allocated() 记录差值(不包含缓存池碎片)。但推理中动态分配频繁,此法粗糙。

  7. Prometheus + Grafana 仪表盘 将框架指标和 GPU 显存指标结合,可在 Grafana 中按请求聚合或绘制分布图。例如,使用 histogram_quantile 计算 P99 请求显存。

📊 估算示例:

假设 7B 模型,FP16,每 token KV 大小 0.5MB。某请求 prompt 500 token,生成 200 token,则 KV Cache ≈ 700 × 0.5 = 350MB。

✅ 最佳实践:利用 token 数量结合每 token 成本进行监控,可快速定位高消耗请求。


🎯 负载均衡时,如何根据显存使用情况分配请求?

💡 实现“显存感知”的负载均衡,需要每个推理实例上报其剩余容量(如空闲 KV 块数或剩余显存),然后由智能网关或客户端 SDK 将请求路由到最空闲的实例。

🔧 实现方案:

  1. 实例侧暴露自定义健康/容量接口 在推理服务内部添加一个轻量 HTTP 端点 /health/capacity,返回 JSON 如 {"free_blocks": 4500, "total_blocks": 5000}。vLLM 可通过内部 API 获取 scheduler.block_manager.get_num_free_blocks()

  2. 负载均衡器侧策略

  3. Envoy / Nginx Plus:通过自定义健康检查或从外部脚本动态获取指标,使用加权最小连接算法。
  4. 自研 Gateway:维护所有实例的剩余容量表,每次请求到来选择容量最大的实例。
  5. 基于 gRPC 的服务网格:利用 envoy 的 load reporting 和 consistent hashing。

  6. 利用 Prometheus 指标动态调整权重 使用 Prometheus Adapter 将空闲块数转换为自定义 metric,结合 K8s Service 的 pod 权重,但实现复杂。更简单的是在应用层(如 Python 客户端)实现轮询 + 随机选优:客户端从服务发现获取所有 pod IP,并发探测 /capacity,选择最大空闲者。

📊 效果:防止热点实例过载 OOM,同时充分利用所有 GPU 显存,提高整体吞吐。


📈 推理服务的水平自动伸缩(HPA)如何与显存指标关联?

💡 通过 Prometheus Adapter 将 GPU 显存指标或框架的排队指标暴露为 K8s Custom Metric,然后创建 HPA 基于这些指标进行弹性伸缩。

🔧 步骤:

  1. 采集显存指标
  2. 使用 NVIDIA DCGM Exporter 收集 DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_FREE
  3. 或者直接使用推理框架的 Prometheus 指标,例如 vLLM 的 vllm:num_requests_waiting(排队数)更能反映压力。

  4. 部署 Prometheus Adapter 配置 Rules 将指标映射为 HPA 可用的 metric。例如:

rules:
- seriesQuery: 'DCGM_FI_DEV_FB_USED{namespace="inference"}'
  resources:
    overrides:
      namespace: {resource: "namespace"}
      pod: {resource: "pod"}
  name:
    matches: "DCGM_FI_DEV_FB_USED"
    as: "gpu_memory_used_bytes"
  1. 创建 HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: llm-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: vllm-deploy
  metrics:
  - type: Pods
    pods:
      metric:
        name: gpu_memory_used_bytes
      target:
        type: AverageValue
        averageValue: 70Gi   # 当平均显存使用超过70G时扩容
  1. 更合适的指标 显存使用率高不一定需要扩容(可能都是长请求占用),更好的做法是基于 num_requests_waiting 或请求延迟进行伸缩,间接反映显存压力。

✅ 注意:扩容时需确保新 Pod 有足够 GPU 资源,冷启动时间要短(模型预热)。


🌊 流式输出对显存管理有什么特殊要求?

💡 流式输出(streaming)下,生成 token 是逐步产生的,每个新 token 都会追加 KV Cache,导致显存占用随时间动态增长。管理上需要动态分配 block,并防范“长生成”耗尽资源。

🔍 特殊要求与对策:

  1. 动态分配与回收 必须使用像 PagedAttention 这样的分页机制,按需从池中取 block,而不是预先分配连续空间。生成结束或中断时立即回收全部 block。

  2. 限制最大生成长度 必须设置 max_tokens 硬限制,防止单个请求无限增长撑爆显存。vLLM/TGI 都支持。

  3. 抢占与交换 由于流式请求可能长时间占用 GPU,若期间无新 block 可用,调度器需要能抢占低优先级流式请求,将其 KV Cache swap 到 CPU,或直接中止并返回部分结果。

  4. 公平调度 长流式输出不应阻塞短请求。可以使用时间片轮转或多优先级队列,确保短请求也能及时完成。

  5. 内存压力下的 early stopping 当系统检测到空闲 block 过低时,可主动终止某些流式请求(尤其是接近最大长度的),返回已生成内容并说明原因。

✅ 结论:流式输出的显存管理核心是“弹性分配 + 严格的长度限制 + 抢占”,以保证系统稳定。


♻️ 如何实现推理时显存的“即时回收”?

💡 即时回收是指当请求完成、取消或超时后,其占用的 KV Cache 块能在微秒级内归还到空闲池,立即可以被新请求使用。这得益于 PagedAttention 的块管理器和引用计数机制。

🔧 实现原理:

  • 每个序列(请求)持有若干物理块的引用。当调度器决定移除该序列时(正常结束、中止、抢占),其逻辑页表被销毁,物理块的引用计数递减。

  • 一旦引用计数归零(对于非共享块),块立即被加入空闲列表,无需数据擦除,只需标记为空闲。

  • 对于共享前缀块,需要引用计数,当所有引用序列都结束才回收。

  • vLLM 中,BlockSpaceManagerfree 方法直接操作 C++ 缓存管理器,回收块是 O(1) 的。

📊 回收时机:

  • 正常结束:生成 EOS token 或达到 max_tokens 时。

  • 客户端断开:检测到 gRPC/HTTP 连接断开,立即终止序列。

  • 超时:请求级别超时,强制中止。

  • 抢占:被交换到 CPU 或直接丢弃时(取决于抢占模式)。

✅ 优势:无碎片、无延迟,实现显存的即时复用,支撑高吞吐。


🧪 在推理服务中,如何测试显存上限以制定容量规划?

💡 通过压力测试工具模拟真实负载,逐步增加并发数、序列长度和输出长度,观察服务吞吐和失败率,找到即将 OOM 的临界点,从而确定单实例的最大容量。

🔧 步骤:

  1. 准备测试脚本 使用 vllm bench serve 或自定义 Python 脚本(如 locust)发送不同配置的请求。需模拟业务分布,如 P50/P95 输入长度、生成长度。

  2. 固定生成长度,增加并发 逐步提高并发用户数,监控服务端 num_free_blocks 和请求延迟。当 num_free_blocks 接近零且开始出现请求拒绝或 swap 时,记录当前的吞吐和并发数,即为安全上限。

  3. 测试极端序列长度 单独发送超长 prompt 或要求生成极长的序列,观察是否会触发 OOM 或提前截断,验证 max_model_len 配置有效性。

  4. 记录 GPU 利用率与显存占用 用 nvidia-smi dmon 和 Prometheus 采集指标,绘制并发-吞吐-显存曲线,找到吞吐拐点(即显存成为瓶颈处)。

  5. 容量规划 根据单实例最大并发数 Cmax 和期望承载的总并发数,计算所需实例数 = 总并发 / Cmax。考虑冗余,增加 20-30% 余量。

📊 示例结果:单卡 80G A100 运行 LLaMA-7B FP16,最大生成长度 512,测得最大稳定并发为 32 个请求。若业务需支持 128 并发,则至少需要 4 个实例。


🆚 不同推理框架(vLLM, TGI, TensorRT-LLM)的显存管理策略有何异同?

💡 三大框架都采用了分页 KV Cache 机制,但在实现细节、预分配策略、offload 支持和优化重点上存在差异。

📊 对比表格:

特性 vLLM TGI (HuggingFace) TensorRT-LLM
KV Cache 管理 PagedAttention,固定大小 block,块池预分配 PagedAttention 类似,称为“paged attention” Paged KV Cache,也使用 block 池,集成在 TensorRT 内存池中
显存利用率控制 --gpu-memory-utilization (默认0.9) --max-total-tokens 直接限制池内 token 总数 通过 max_tokens_in_paged_kv_cache 和 builder 配置
碎片处理 无外部碎片,内部碎片取决于 block size 同,通过 block 消除外部碎片 基于 TensorRT 的统一内存分配器,碎片极低
Prefix Caching 自动,基于哈希 支持,需开启 --prefix-caching 实验性支持,可通过配置启用
Swap/Offload 支持 swap 到 CPU 内存 (vLLM 的 --swap-space) 不支持显式的 KV swap,但支持模型 offload 支持 KV Cache 卸载到 CPU 或 NVMe(通过 multi-stream)
多模型/LoRA 支持 LoRA 热插拔,分页 LoRA 块 支持 LoRA,未分页,但可同时加载多个适配器 LoRA 支持,集成到 TRT 引擎中
内存池设计 自行管理 C++ 块分配器 基于 PyTorch 分配器 + 自定义管理 完全使用 TensorRT 的内存池,与引擎优化结合
性能调优 易用,较少手动调参 自动调优较少,需手动指定 --max-batch-size 等 需要构建引擎,但提供极致优化

🔍 详细比较:

  • vLLM 是最早提出 PagedAttention 的,显存管理成熟,社区活跃,支持 continuous batching 和 prefix caching,swap 功能使得在大流量下能弹性处理。

  • TGI 由 HuggingFace 推出,与 transformers 生态集成紧密,同样支持 paged attention 和 continuous batching,但 swap 功能缺失,在极端显存压力下可能不如 vLLM 灵活。

  • TensorRT-LLM 基于 NVIDIA TensorRT,显存管理完全由 TensorRT 优化,内存池效率极高,且针对 GPU 架构深度优化,可以做到极低的延迟和高吞吐,但使用门槛较高,需要构建引擎。

✅ 选择建议:追求易用和快速部署选 vLLM;需与 HuggingFace 生态无缝结合选 TGI;追求极致性能和成本优化(尤其使用 NVIDIA 硬件)选 TensorRT-LLM。


⚖️ 推理服务启动时,如何选择最优的显存分配比例(如权重 vs KV Cache)?

💡 没有固定的“黄金比例”,最优分配取决于模型大小、序列长度、并发数。核心原则是:在保证能容纳模型权重和必要的激活内存后,将剩余显存尽可能多地划给 KV Cache 池,然后通过合理设置 max_model_len 和并发数来匹配业务需求。

🔍 决策依据与步骤:

  1. 测定固定开销
  2. 模型权重:根据精度计算。FP16: 参数量×2,INT8: ×1,INT4: ×0.5。例如 LLaMA-7B FP16 占 14 GB。
  3. 激活与临时缓冲:推理时 Prefill 阶段需要临时存储注意力中间结果、算子工作区。通常预留 1–3 GB(取决于框架和 batch size)。
  4. 其他常驻:CUDA context、通信缓冲(多卡)等 ~0.5–1 GB。

  5. 计算可用显存 剩余显存 = 总显存 - 权重 - 固定开销。例如 24 GB 卡,7B FP16 权重 14 GB,固定开销 2 GB,剩余 8 GB。

  6. 确定 KV Cache 块大小与池容量

  7. 每个 token 的 KV Cache 大小 = 2 × 层数 × KV 头数 × 头维度 × 精度字节
  8. 如果使用 GQA,KV 头数远小于 Query 头数。
  9. 例如 7B 模型(无 GQA):每 token 约 0.5 MB(FP16)。
  10. 剩余 8 GB 可容纳约 16,000 token 的 KV Cache。

  11. 分配比例的权衡

  12. 显存优先分配给权重是刚性的,无法妥协。
  13. KV Cache 池的大小直接决定了系统能同时处理的总 token 数(并发 × 平均序列长度)。
  14. 通常框架如 vLLM 通过 --gpu-memory-utilization(默认 0.9)来设定,即用剩余显存的 90% 建立 KV 块池,剩下的 10% 作为安全缓冲。这个比例可微调:
    • 如果经常遇到 Prefill 阶段大激活峰值,可降低到 0.85。
    • 如果追求极致吞吐,且激活开销稳定,可提高到 0.95。
  15. 最佳实践:先加载模型,打印剩余显存,再乘以 0.9 作为池大小,然后根据业务最大序列长度反推能支持的最大并发数。如果不满足,则考虑量化权重或使用多卡。

  16. 动态权衡

  17. 有些系统(如 TensorRT-LLM)可以在构建引擎时预先分配 KV Cache 块数量,而不是比例。
  18. 需要估算业务场景:如果大多数请求是短文本,可设置较大的 KV 池和较短的 max_model_len,从而支持极高并发;如果经常有长文档,需留出足够单请求 token 容量,同时减少并发数。

✅ 因此,最优分配是:先满足硬性权重需求,再将剩余显存的大部分划给 KV Cache,并根据目标序列长度和并发数微调比例,确保不 OOM 且吞吐最大。


🔥 模型热更新时,显存如何平稳过渡?

💡 模型热更新(不停服更新模型版本)的显存过渡需要采用新旧模型共存、流量逐步切换、显存复用或精细卸载的策略,避免瞬时显存峰值导致 OOM。

🔧 平稳过渡方案:

  1. 双模型共存 + 流量切换
  2. 在同一个推理服务进程中,加载新模型到另一部分显存(前提是总显存足够容纳两个模型)。
  3. 新模型加载完成后,将新请求路由到新模型,旧模型继续服务正在进行的请求。
  4. 待旧模型所有请求处理完毕,卸载旧模型,释放其占用的显存。
  5. 显存挑战:需要至少能同时装下两个模型的权重和部分 KV Cache,对大模型不现实。因此需结合 offload。

  6. 单模型 + 优雅重启

  7. 使用 K8s 的滚动更新,先启动新 Pod(新模型),旧 Pod 逐渐终止。但滚动更新期间新旧 Pod 共存,资源翻倍。
  8. 可配合显存超售?不可靠。更稳妥的是额外节点。

  9. 动态卸载与重载(同进程内热切换)

  10. 将旧模型参数先卸载到 CPU 内存,释放 GPU 显存。
  11. 然后加载新模型到 GPU(从 CPU 预加载或直接读取)。
  12. 切换期间服务中断但短暂,适用于允许秒级中断的场景。
  13. 显存过渡平稳,因为 GPU 显存只同时保存一个模型权重。

  14. 使用 PEFT/LoRA 的热插拔

  15. 如果仅更新适配器而非基础模型,只需切换 LoRA 权重(几十 MB),GPU 显存几乎无波动,可瞬间完成。

  16. 显存池的复用

  17. 如果框架支持,可在旧模型释放显存后,不归还给 OS 而是保留在内存池中,立即分配给新模型,减少碎片和分配开销。

📊 推荐方案:

  • 生产环境:优先使用 K8s 滚动更新并配合充裕资源,或使用多模型同时加载 + 流量切换在有 48GB+ 显存的卡上对 7B 模型进行无感更新。

  • 资源受限:用卸载-重载方案,接受几秒服务降级。

✅ 关键:显存过渡平稳的核心是“先建后拆”或“先卸后装”,避免同时持有两份完整权重,以及利用 LoRA 热插拔。


🖼️ 如果推理请求中包含多张图片(多模态),显存如何预估?

💡 多模态大模型(如 LLaVA)的显存占用 =文本 LLM 部分 + 视觉编码器权重 + 图像 token 的 KV Cache + 图像编码中间激活。多张图片会导致图像 token 数量成倍增长,KV Cache 和激活峰值随之上升。

🔍 预估方法:

  1. 视觉编码器权重
  2. 例如 CLIP ViT-L/14 约 0.3B 参数,FP16 约 0.6 GB。这部分常驻显存,不随图片数量变化。

  3. 单张图片产生的图像 token 数

  4. 图像经过视觉编码器后,通常通过一个投影层转换成若干个“图像 token”(例如 LLaVA-1.5 中每张图 576 个 token)。
  5. 这些 token 会拼接到文本序列中,参与自回归生成。因此每张图贡献 L_img 个 token 的 KV Cache。

  6. 文本部分 KV Cache

  7. 包括系统提示、用户文本,以及所有图像 token 和生成的文本 token。
  8. 总 token 数 = 文本 token 数 + 图片数 × L_img + 生成 token 数
  9. 每 token KV Cache 大小计算同纯文本模型。

  10. 中间激活峰值

  11. 图像编码阶段:视觉编码器的一次前向,激活大小与图像分辨率相关,但通常几百 MB,可控。
  12. 多模态融合阶段:所有图像 token 一次性输入 LLM,序列长度较长,attention 的 Prefill 需要临时空间,与总 token 数成平方关系(若无 FlashAttention)。因此必须用 FlashAttention。

  13. 显存总预估公式 总显存 ≈ 文本LLM权重 + 视觉编码器权重 + (总token数) × 每token_KV字节 + 激活开销 其中总 token 数取决于图片数量。例如:

  14. 7B LLaVA:文本 LLM 权重 14 GB,视觉编码器 0.6 GB。
  15. 每 token KV = 0.5 MB。
  16. 输入:文本 200 token + 4 张图片 × 576 = 2504 token,生成 500 token,总计 3004 token → KV Cache ≈ 1.5 GB。
  17. 激活约 2 GB。总显存约 18.1 GB,单卡 24GB 可运行。
  18. 若 8 张图片,总 token 达 5800,KV Cache 约 2.9 GB,总 ~19.5 GB,仍然安全。但若图片极大导致编码激活增大,需额外预留。

⚙️ 注意事项:

  • 部分模型支持图片特征压缩(如 TokenPacker),可减少每张图的 token 数。

  • 多图片对话往往需要 batch 处理,若同时处理多个请求,总 token 数加倍。

  • 视觉编码器可以单独卸载到 CPU(如 LLaVA 的 device_map),节约 GPU 显存。

✅ 因此,多模态推理显存预估的关键是准确估计图像 token 数量并计入 KV Cache**,同时留意视觉编码器的权重和临时激活。


🧹 在长连接下,KV Cache 的累积如何设计淘汰策略?

💡 长连接(如 WebSocket 对话)中,对话历史不断增长,KV Cache 必须淘汰以限制资源。淘汰策略需在保留上下文质量和释放显存之间权衡,常见方法包括滑动窗口、LRU、基于 token 数限制、主动摘要等。

🔧 策略设计:

  1. 滑动窗口(Sliding Window)
  2. 仅保留最近的 W 个 token 的 KV Cache,丢弃旧的。适用于模型本身支持窗口注意力(如 Mistral 的 sliding window)。
  3. 实现:在逻辑页表中维护窗口,旧块直接回收。简单高效,但会丢失远期上下文。

  4. 基于 token 总数的硬性限制

  5. 设定一个最大历史 token 数(如 4096),当总长度超过时,丢弃最早的 token(可能丢失重要信息)。
  6. 可以结合轮次:保留最近 N 轮完整对话,超出部分删除。

  7. LRU(最近最少使用)

  8. 如果同时管理多个长连接,显存不足时淘汰最不活跃会话的 KV Cache,将其 swap 到 CPU,待需要时再换回。
  9. 更适合多会话并发场景,通过全局淘汰释放显存。

  10. 主动摘要(Summarization)

  11. 当对话达到一定长度,调用模型本身对历史进行摘要,用摘要代替原始历史 KV Cache,大幅减少 token 数。但需额外调用,增加延迟。

  12. 分层淘汰(优先丢弃 Value 等)

  13. 有些方法只保留 Key 而丢弃 Value?不常见。但可对 KV Cache 压缩,保留重要部分。

  14. 用户可控的“重置”

  15. 提供客户端接口重置对话历史,清空 KV Cache。

📊 推荐实践:

  • 对于单个长连接,使用 token 数硬限制 + 滑动窗口,保证显存可预测。

  • 对于服务端多连接,结合 全局 LRU 淘汰 + swap,如 vLLM 将冷会话换出到 CPU,热会话保留 GPU。

  • 设置告警:当空闲块低于阈值,主动淘汰低优先级或长连接的旧历史。

✅ 长连接淘汰策略核心是“设定上限,有序丢弃”,并利用 swap 柔性处理突发,避免 OOM。


🕵️ 推理服务中,发现显存占用越来越高但不释放,可能是什么原因?

💡 这通常是显存泄漏或KV Cache 未及时回收所致。具体原因可能包括:请求完成后块未归还、共享前缀引用计数错误、长连接不断累积、PyTorch 缓存碎片、框架 bug 等。

🔍 排查路径:

  1. KV Cache 块未释放(最常见)
  2. 检查是否有请求异常终止(如客户端断开但服务端未清理),导致序列的物理块仍被占用。
  3. 验证框架的引用计数机制:前缀共享时,引用计数未减到零就无法回收。可能因为逻辑错误导致计数泄漏。
  4. 查看 num_free_blocks 是否单调递减。

  5. 长连接累积

  6. 多轮对话中,如果未设置淘汰策略或最大长度,KV Cache 随对话无限增长。
  7. 解决:设置 max_model_len 或主动清理历史。

  8. PyTorch 缓存分配器碎片

  9. torch.cuda.memory_reserved() 持续上升但不回落,而 allocated 稳定。这是因为分配器保留了未使用的缓存块,不归还系统。虽然不代表泄漏,但会导致 nvidia-smi 显示占用高。使用 PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True 可缓解。

  10. 框架/应用层内存泄漏

  11. 某些张量(如 attention mask)被重复创建且未释放。
  12. LoRA 适配器未正确卸载。
  13. 使用 PyTorch Profiler 的 Memory Snapshot 检查未释放的张量。

  14. 多模型或多 LoRA 残留

  15. 动态加载的模型或适配器未卸载,长期积累。
  16. 检查显存中是否存在多个模型的副本。

🔧 诊断工具:

  • 监控 vllm:num_free_blockstorch.cuda.memory_allocated()

  • 开启 PyTorch memory history 并导出快照分析。

  • 使用 gdbpy-spy 查看进程状态。

✅ 发现持续增长时,优先检查 KV 块回收和长连接累积,然后排查碎片和泄漏,利用框架指标快速定位。


📊 如何使用 Prometheus + Grafana 搭建显存监控面板?

💡 搭建流程:部署 NVIDIA DCGM Exporter 采集 GPU 显存指标 → Prometheus 抓取 → Grafana 可视化。同时结合推理框架自带的 Metrics(如 vLLM 的 /metrics)展示 KV Cache 利用率、请求排队等。

🛠️ 步骤:

  1. 部署 DCGM Exporter
helm repo add gpu-helm-charts https://nvidia.github.io/dcgm-exporter/helm-charts
helm install dcgm-exporter gpu-helm-charts/dcgm-exporter

它会在每个 GPU 节点上以 DaemonSet 形式运行,暴露 :9400/metrics,包含 DCGM_FI_DEV_FB_USEDDCGM_FI_DEV_FB_TOTAL 等。

  1. 配置 Prometheus 在 prometheus.yml 中添加 job:
- job_name: 'dcgm'
  kubernetes_sd_configs:
    - role: pod
  relabel_configs:
    - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
      action: keep
      regex: true

或者直接指定 targets。确认指标能查询。

  1. 集成推理框架指标 vLLM 默认在 :8000/metrics 暴露指标,包括 vllm:kv_cache_usage_percvllm:num_requests_running 等。同样加入 Prometheus 抓取。

  2. Grafana 面板设计

  3. 实时显存用量:DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTALpod 区分,使用 Timeseries 或 Stat 面板。
  4. KV Cache 利用率:vllm:kv_cache_usage_perc 折线图,设置告警阈值(如 >90%)。
  5. 并发请求数:vllm:num_requests_running
  6. 空闲块数趋势:若框架暴露 free_blocks 指标,绘制以观察碎片。
  7. 单请求显存估算:通过 request_prompt_tokens + generation_tokens 乘以每 token 大小计算,可在 Grafana 中用表达式。

  8. 告警规则

- alert: GPU memory high
  expr: DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL > 0.95
  for: 2m
  labels:
    severity: warning
  annotations:
    summary: "GPU memory usage above 95%"
  1. 导入现成 Dashboard Grafana 社区有 DCGM 仪表盘(如 ID: 12239),可直接导入再添加推理框架指标。

✅ 最终效果:在一个 Grafana 面板上同时看到显存总量、KV Cache 占用、请求负载和告警信息,实现全方位显存可观测性。