#单纯 GPU 推理成本高,纯 CPU 推理延迟大。在实际工程中,一台服务器往往同时配备大量 CPU 核心和少量 GPU,如何让二者协同工作,最大化整机算力利用率,是推理架构设计的核心问题之一。
本篇聚焦 CPU+GPU 混合部署的架构模式与任务调度策略,从原理到工程实现全面覆盖。
1 混合部署的核心动机
1.1 资源互补
| 资源 | CPU | GPU |
|---|---|---|
| 算力特点 | 串行/少并行,擅长复杂逻辑 | 大规模并行,擅长矩阵运算 |
| 内存容量 | 大(数百 GB 系统内存) | 小(通常 16-80 GB 显存) |
| 带宽特点 | 较低(百 GB/s 量级) | 高(TB/s 量级 HBM) |
| 成本 | 低 | 高 |
混合部署的本质是:用 CPU 的大内存承载模型权重溢出,用 GPU 的高算力加速热路径计算。
1.2 典型场景
- 超大模型部署:单 GPU 显存不足以容纳全部模型权重,将部分层卸载到 CPU
- 成本敏感场景:用少量 GPU 处理高优先级/短 token 请求,CPU 处理低优先级/批处理请求
- 峰谷调度:GPU 满载时,溢出请求由 CPU 承接,保证服务可用性
2 架构模式一:层间分割(Layer-wise Offloading)
将 Transformer 模型的不同层分别部署在 CPU 和 GPU 上,数据在层间传递时跨设备流动。
2.1 工作原理
输入 Tokens
↓
[Embedding Layer] ← CPU 或 GPU
↓
[Layer 1-16] ← GPU(显存充足的层)
↓ (数据传输: GPU→CPU)
[Layer 17-32] ← CPU(溢出层)
↓ (数据传输: CPU→GPU)
[Output Head] ← GPU
↓
输出 Tokens
瓶颈:层间数据传输(PCIe 带宽约 16-64 GB/s)会成为性能瓶颈,需要最小化跨设备传输次数。
2.2 llama.cpp 的 GPU 层卸载
llama.cpp 通过 -ngl(Number of GPU Layers)参数控制卸载层数:
bash
# 安装 CUDA 支持版本
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release -j $(nproc)
# 将前 28 层卸载到 GPU,剩余层在 CPU 上运行
./build/bin/llama-cli \
-m llama-3-70b.Q4_K_M.gguf \
-t 16 \
-ngl 28 \
-c 4096 \
-p "你好"
# 完全卸载到 GPU(所有层)
./build/bin/llama-cli -m model.gguf -ngl 999
# 纯 CPU 推理
./build/bin/llama-cli -m model.gguf -ngl 0
调优策略 :逐步增加 -ngl 的值,直到 GPU 显存接近上限(留 10% 余量),此时吞吐量通常最优。
3 架构模式二:请求级分流(Request-level Routing)
不同请求根据优先级、长度或资源预算,路由到不同的计算后端:
┌─────────────────┐
请求入口 ──────────► │ 路由调度器 │
└────────┬────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
[短序列/高优] [中等序列] [长序列/批处理]
│ │ │
GPU 后端 CPU 后端 CPU 后端
(低延迟) (中等延迟) (高吞吐)
3.1 调度器实现(Python 示例)
python
import asyncio
from dataclasses import dataclass
from enum import Enum
class Backend(Enum):
GPU = "gpu"
CPU = "cpu"
@dataclass
class InferenceRequest:
prompt: str
max_tokens: int
priority: int # 1=高优, 2=普通, 3=低优
class HybridScheduler:
def __init__(self):
self.gpu_queue = asyncio.Queue(maxsize=32)
self.cpu_queue = asyncio.Queue(maxsize=256)
# 路由规则
self.GPU_MAX_TOKENS = 512 # GPU 处理短生成
self.HIGH_PRIORITY_THRESHOLD = 1
def route(self, request: InferenceRequest) -> Backend:
"""决定请求路由到哪个后端"""
# 规则 1:高优先级请求走 GPU
if request.priority == self.HIGH_PRIORITY_THRESHOLD:
return Backend.GPU
# 规则 2:短生成请求走 GPU(延迟敏感)
if request.max_tokens <= self.GPU_MAX_TOKENS:
return Backend.GPU
# 规则 3:GPU 队列已满,降级到 CPU
if self.gpu_queue.full():
return Backend.CPU
return Backend.CPU
async def submit(self, request: InferenceRequest):
backend = self.route(request)
if backend == Backend.GPU:
await self.gpu_queue.put(request)
else:
await self.cpu_queue.put(request)
return backend
4 架构模式三:流水线并行(Pipeline Parallelism)
将一个请求的处理拆分为多个阶段,CPU 和 GPU 同时处理不同阶段的不同请求,形成流水线:
时间线:
GPU: [请求A推理] [请求B推理] [请求C推理]
CPU: [请求A后处理] [请求B预处理] [请求C后处理]
↑同时进行↑
CPU 负责:tokenization、prompt 格式化、后处理、结果序列化
GPU 负责:模型前向推理
这种模式下,CPU 与 GPU 几乎不存在等待,整体吞吐量接近纯 GPU 推理的上限。
5 PCIe 数据传输优化
混合部署的隐性瓶颈是 CPU-GPU 之间的数据传输。
5.1 使用锁页内存(Pinned Memory)
python
import torch
# 普通内存(需要先拷贝到锁页内存再传输)
cpu_tensor = torch.randn(1000, 1000)
# 锁页内存(直接走 DMA,传输速度提升 2-3x)
pinned_tensor = torch.randn(1000, 1000).pin_memory()
# 异步传输到 GPU(不阻塞 CPU 计算)
gpu_tensor = pinned_tensor.to("cuda", non_blocking=True)
# CPU 和 GPU 可以同时工作
do_cpu_work()
# 同步点:确保传输完成
torch.cuda.synchronize()
5.2 重叠传输与计算
python
import torch
stream_compute = torch.cuda.Stream()
stream_transfer = torch.cuda.Stream()
for batch in dataloader:
# 在传输流中异步上传下一批数据
with torch.cuda.stream(stream_transfer):
next_batch_gpu = batch.pin_memory().to("cuda", non_blocking=True)
# 在计算流中处理当前批次
with torch.cuda.stream(stream_compute):
output = model(current_batch_gpu)
# 等待传输完成后切换
stream_compute.wait_stream(stream_transfer)
current_batch_gpu = next_batch_gpu
6 显存不足时的动态卸载策略
当推理过程中 GPU 显存耗尽时,需要动态将部分数据卸载到 CPU:
python
import torch
class MemoryAwareModel:
def __init__(self, model, cpu_offload_threshold=0.9):
self.model = model
self.threshold = cpu_offload_threshold
def get_gpu_memory_ratio(self) -> float:
"""获取 GPU 显存使用率"""
if not torch.cuda.is_available():
return 0.0
used = torch.cuda.memory_allocated()
total = torch.cuda.get_device_properties(0).total_memory
return used / total
def maybe_offload(self):
"""显存使用率超阈值时,将 KV Cache 卸载到 CPU"""
if self.get_gpu_memory_ratio() > self.threshold:
torch.cuda.empty_cache()
# 将历史 KV Cache 移到 CPU
for layer in self.model.layers:
if hasattr(layer, "kv_cache") and layer.kv_cache is not None:
layer.kv_cache = layer.kv_cache.cpu()
def forward(self, input_ids):
self.maybe_offload()
return self.model(input_ids)
7 总结与选型建议
| 场景 | 推荐架构 |
|---|---|
| 70B+ 模型,显存不足 | 层间分割(llama.cpp -ngl 调参) |
| 高并发,请求特征差异大 | 请求级分流(GPU 短请求 + CPU 长请求) |
| 吞吐量最大化,延迟要求宽松 | 流水线并行 |
| GPU 显存充足,CPU 做预处理 | 锁页内存 + 异步传输 |
核心原则:
- 最小化 PCIe 传输次数,将跨设备数据传输与计算重叠
- GPU 做计算密集操作,CPU 做逻辑密集操作(路由、格式化、后处理)
- 监控 GPU 显存利用率和 PCIe 带宽占用,这两个是混合部署的核心瓶颈指标