AI 推理服务网格:多模型混合推理的架构设计与工程实践
在大模型从实验走向生产的过程中,推理基础设施正经历从"单模型单实例"到"多模型服务网格"的架构跃迁。本文深入拆解推理服务网格的核心组件——从 Prefill/Decode 分离部署、KV Cache 全局池化、自适应路由策略,到多模型混合推理的编排调度,结合生产级代码示例,呈现一套完整的推理服务治理方案。
一、推理基础设施的演进困境
2024 年之前,部署一个大模型意味着启动一个 vLLM 实例,绑定一张 GPU,模型权重从磁盘加载到显存,等待请求到来。这种模式在 demo 场景下运转良好,但当企业同时部署 Llama-70B、Qwen-72B、DeepSeek-V3、各种 LoRA 微调适配器,还要满足 99.9% 的 SLA 时,单体架构迅速崩溃。
核心矛盾有以下几个:
- 显存碎片化:每个模型独占 GPU,显存利用率通常低于 40%,大量 KV Cache 空间被预留但闲置
- 流量异构:不同模型的请求模式差异巨大(prefill-heavy vs decode-heavy),同一套调度策略无法同时最优
- GPU 异构集群:从 A100 80GB 到 H100、H200、B200 混部,内存容量、算力、带宽差异大,简单轮询必然导致资源浪费
- 版本热更新:模型迭代频繁,权重热切换不能停服,如何做到无损滚动升级
解决这些问题的关键思路是将计算(GPU)与状态(KV Cache)分离,引入推理服务网格(Inference Service Mesh)来统一编排多模型流量、显存资源和 GPU 算力。
二、推理服务网格的核心架构
一个典型的推理服务网格包含以下层次:
┌─────────────────────────────────────────────────────┐
│ API Gateway │
│ (认证、限流、请求标准化、Prompt 缓存) │
├─────────────────────────────────────────────────────┤
│ Inference Router │
│ (模型亲和路由、KV Cache 感知、负载均衡) │
├──────────┬──────────┬──────────┬───────────────────┤
│ Prefill │ Prefill │ Decode │ Decode Workers │
│ Pool │ Pool │ Pool │ │
│ (GPU-1) │ (GPU-2) │ (GPU-3) │ (GPU-N) │
├──────────┴──────────┴──────────┴───────────────────┤
│ KV Cache Unified Memory Pool │
│ (CPU Memory / NVMe / Remote RDMA) │
├─────────────────────────────────────────────────────┤
│ GPU Resource Scheduler │
│ (MIG Profile、Time-Slice、Bin-Packing) │
└─────────────────────────────────────────────────────┘
这个架构的关键设计是:
- Prefill Worker Pool:专做首 token 预填充(计算密集,需要高算力)
- Decode Worker Pool:专做逐 token 解码生成(访存密集,需要大显存带宽)
- KV Cache 统一内存池:脱离 GPU 显存,实现跨实例共享
- 推理路由器:根据模型名称、KV Cache 命中率、GPU 负载智能调度
这类设计在 DistServe(OSDI'24)、Mooncake(ASPLOS'25)、NVIDIA 的 application note 以及 SGLang 的架构中都有体现。
三、Prefill/Decode 分离部署工程实现
Prefill 阶段和 Decode 阶段的资源需求完全不同。Prefill 计算 Token embedding + Attention,是 compute-bound;Decode 是自回归生成每 token,每次只处理一个 token 但同时需要读取全部 KV Cache,是 memory-bandwidth-bound。
将两者混部在同一 GPU 上会导致严重的资源争用:Prefill 的 GEMM kernel 打满 SM,Decode 的 kernel 在等待显存带宽,互相干扰。
以下是一个简化的 Prefill/Decode 分离推理服务器的 Rust 实现框架:
// prefill_worker.rs
use tokio::sync::mpsc;
use candle_core::{Tensor, Device};
pub struct PrefillWorker {
model: Arc<LlamaModel>,
device: Device,
kv_cache_pool: Arc<KvCachePool>,
// 接收推理请求的通道
request_rx: mpsc::Receiver<InferRequest>,
// 将 Prefill 完成后的 KV 传递给 Decode Worker 的通道
kv_tx: mpsc::Sender<PrefilledBatch>,
}
impl PrefillWorker {
pub async fn run(mut self) -> Result<()> {
while let Some(req) = self.request_rx.recv().await {
// 1. Tokenize
let input_ids = tokenize(&req.prompt);
// 2. 从 KV Cache Pool 分配空间
let kv_slot = self.kv_cache_pool.allocate(
req.request_id,
input_ids.len() + req.max_tokens
).await?;
// 3. Prefill 计算(GEMM 密集)
let kv_tensors = self.model.forward_prefill(
&input_ids,
kv_slot.offset,
)?;
// 4. 将 KV Cache 写入统一内存池
self.kv_cache_pool.write(kv_slot.id, &kv_tensors).await?;
// 5. 通知 Decode Worker 开始生成
self.kv_tx.send(PrefilledBatch {
request_id: req.request_id,
kv_slot_id: kv_slot.id,
prompt_len: input_ids.len(),
max_tokens: req.max_tokens,
sampling_params: req.sampling_params,
}).await?;
}
Ok(())
}
}
// decode_worker.rs
pub struct DecodeWorker {
model: Arc<LlamaModel>,
device: Device,
kv_cache_pool: Arc<KvCachePool>,
rx: mpsc::Receiver<PrefilledBatch>,
}
impl DecodeWorker {
pub async fn run(mut self) -> Result<()> {
let mut running_batch: HashMap<RequestId, DecodeState> = HashMap::new();
loop {
tokio::select! {
// 接收新的 Prefill 完成请求
Some(batch) = self.rx.recv() => {
running_batch.insert(batch.request_id, DecodeState::new(batch));
}
// 每个 step 生成一个 token
_ = tokio::time::interval(Duration::from_micros(200)) => {
if running_batch.is_empty() { continue; }
// 批量 Decode(最大化显存带宽利用率)
let batch_tokens = self.decode_step(&running_batch).await?;
// 输出完成的请求,释放 KV Cache
for (req_id, token) in batch_tokens {
if token.is_eos() {
let state = running_batch.remove(&req_id).unwrap();
self.kv_cache_pool.free(state.kv_slot_id).await?;
}
// 将 token 流式返回给客户端
}
}
}
}
}
async fn decode_step(&self, batch: &HashMap<RequestId, DecodeState>)
-> Result<Vec<(RequestId, Token)>> {
// 批量 gather KV Cache → 一次 Attention
// 这是显存带宽瓶颈,batch size 越大利用率越高
...
}
}
核心思想:Prefill Worker 和 Decode Worker 各自满载运行,互不干扰。KV Cache 在统一内存池中管理,支持跨 GPU 甚至跨节点共享。
四、KV Cache 统一内存池设计
KV Cache 是推理中最大的内存开销,约占模型推理总显存的 60%-80%。在 131072 上下文的 Llama-70B 场景中,单个请求的 KV Cache 可达数 GB。
传统方案中 KV Cache 和 GPU 显存绑定,导致严重的碎片化(每个请求必须预留最大长度的 KV 空间,但实际使用往往远小于预留)。统一内存池的设计思想是:三层存储架构。
// kv_cache_pool.rs
pub struct KvCachePool {
// L1: GPU 显存 — 最热数据(当前正在生成的 token 对应的 KV)
gpu_slots: GpuCacheSlots,
// L2: CPU DRAM — 温数据(最近请求完成,可能复用的 KV)
cpu_pool: CpuCachePool,
// L3: NVMe SSD — 冷数据(超长上下文的底座)
disk_store: DiskCacheStore,
// 全局索引:Prompt Hash → KV Cache 位置(用于 Prompt Cache 命中)
prompt_index: PromptHashIndex,
}
impl KvCachePool {
/// 根据 Prompt 前缀计算 Hash,尝试命中已有 KV Cache
pub async fn lookup_prefix(&self, prompt: &[Token]) -> Option<KvCacheRef> {
// Radix Tree 前缀匹配:长 prompt 的前缀很可能已经被缓存
let prefix_hash = radix_hash(prompt);
self.prompt_index.find(prefix_hash).await
}
pub async fn allocate(&self, req_id: RequestId, max_len: usize) -> Result<KvSlot> {
// 尝试 GPU 显存
if let Some(slot) = self.gpu_slots.allocate(max_len) {
return Ok(slot);
}
// 降级到 CPU 内存
if let Some(slot) = self.cpu_pool.allocate(max_len).await {
return Ok(slot.of_level(CacheLevel::Cpu));
}
// 最后选择 NVMe
self.disk_store.allocate(req_id, max_len).await
}
/// 基于 LRU + KV Cache 命中频率的自适应淘汰
pub async fn evict(&self) {
let stats = self.access_stats.read().await;
let victim = stats.lfu_order()
.filter(|s| s.pin_count == 0)
.next();
if let Some(slot) = victim {
// 仅当 L3 已有副本时才从 L1/L2 驱逐
if self.disk_store.contains(slot.hash).await {
self.gpu_slots.free(slot.id);
}
}
}
}
这个设计的关键优势:
- Prefix Caching:相同 System Prompt 的请求(如都带相同 System Prompt 的对话),直接复用已计算 KV Cache,Prefill 时间降低 80%+
- 自动分层:热数据自动升到 GPU 显存,冷数据落盘,对上层模型透明
- 按需分配:不再预留最大长度,按实际使用增量分配
五、多模型混合推理的编排调度
当集群同时运行多个模型时,调度问题变得极其复杂。以下是关键策略:
5.1 模型感知的负载均衡
// inference_router.rs
pub struct InferenceRouter {
model_registry: Arc<ModelRegistry>,
metrics: Arc<RouterMetrics>,
}
impl InferenceRouter {
pub async fn route(&self, request: &InferRequest) -> Result<Endpoint> {
let model_name = &request.model;
let candidates = self.model_registry.endpoints(model_name).await;
// 多维度评分
let scored: Vec<(f64, Endpoint)> = candidates.iter().map(|ep| {
let kv_hit_rate = self.metrics.kv_cache_hit_rate(ep, &request.prompt);
let queue_depth = self.metrics.queue_depth(ep);
let gpu_util = self.metrics.gpu_utilization(ep);
// 加权评分:命中率权重最高,其次是队列深度
let score = kv_hit_rate * 0.5
+ (1.0 / (1.0 + queue_depth as f64)) * 0.3
+ (1.0 - gpu_util) * 0.2;
(score, ep.clone())
}).collect();
// Softmax 采样引入随机性,避免惊群效应
let probs = softmax(scored.iter().map(|(s, _)| *s));
sample_softmax(&scored, &probs)
}
}
5.2 多 LoRA 适配器并行推理
单个基座模型加载多个 LoRA 适配器是常见场景(一个基座 + N 个微调变体),关键挑战是显存和计算效率:
pub struct MultiLoraEngine {
base_model: Arc<LlamaModel>,
lora_adapters: HashMap<String, LoraAdapter>,
// 将多个 LoRA 请求合并到同一个 batch 的不同 slot
batch_sorter: BatchSorter,
}
impl MultiLoraEngine {
pub fn group_by_base_model(
&self, requests: Vec<InferRequest>
) -> Vec<HomogeneousBatch> {
// 按 LoRA adapter 分组,同组请求可以共享 base model 计算
let mut groups: HashMap<String, Vec<InferRequest>> = HashMap::new();
for req in requests {
groups.entry(req.lora_id.clone())
.or_default()
.push(req);
}
// 每个组内做 padded batch,组间可并行(stream 并行)
groups.into_iter()
.map(|(lora_id, reqs)| {
let adapter = self.lora_adapters.get(&lora_id).unwrap();
HomogeneousBatch::new(reqs, adapter.clone())
})
.collect()
}
/// 同时执行多个 LoRA 推理 stream
pub async fn execute_parallel(
&self, batches: Vec<HomogeneousBatch>
) -> Vec<TokenStream> {
// 每个 batch 在一个 CUDA stream 上并行执行
let streams: Vec<_> = batches.iter().map(|b| {
let model = self.base_model.clone();
let adapter = b.adapter.clone();
async move {
// LoRA forward:W' = W + BA 低秩分解
model.forward_with_lora(&b.input_ids, &adapter).await
}
}).collect();
join_all(streams).await
}
}
六、可观测性与 SLO 保障体系
推理服务的可观测性与传统微服务有本质延迟目标——P99 TTFT(Time to First Token)和 TBT(Time Between Tokens)是核心指标。
6.1 推理请求全链路追踪
# tracing.py - 使用 OpenTelemetry 追踪推理请求全链路
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
tracer = trace.get_tracer("inference.mesh")
class InferenceRequestTracer:
"""追踪一个请求从网关到 Prefill 到 Decode 的完整生命周期"""
def start_request(self, request_id: str, model: str, prompt_len: int):
with tracer.start_as_current_span("inference_request") as span:
span.set_attribute("request.id", request_id)
span.set_attribute("model.name", model)
span.set_attribute("prompt.length", prompt_len)
# 记录 Prefill 阶段耗时
with tracer.start_span("prefill") as prefill_span:
# Prefill Worker 实际执行处调用
pass
# 记录 KV Cache 同步延迟
with tracer.start_span("kv_transfer") as kv_span:
# Prefill → Decode KV Cache 搬运处调用
pass
# 记录 Decode 阶段
with tracer.start_span("decode_loop") as decode_span:
for token_idx in range(max_tokens):
with tracer.start_span("decode_step") as step_span:
# 每步 decode 调用
pass
def record_cache_metrics(self, request_id: str,
kv_hit: bool, cache_level: str):
"""记录 KV Cache 命中率和层级命中分布"""
with tracer.start_as_current_span("kv_cache_lookup") as span:
span.set_attribute("kv.hit", kv_hit)
span.set_attribute("kv.cache_level", cache_level) # GPU / CPU / Disk
6.2 自适应限流与降级
推理服务的限流不能简单基于 QPS——长上下文请求的 Prefill 时间可达短上下文的 10 倍,按 QPS 限流不合理。应该基于"计算量"限流:
// adaptive_limiter.rs
pub struct ComputeBudgetLimiter {
/// GPU 每秒可提供的最大 prefill token 数(基于 profiling 标定)
prefill_budget: AtomicU64, // tokens/second
decode_budget: AtomicU64, // tokens/second
current_usage: AtomicU64,
}
impl ComputeBudgetLimiter {
pub fn try_admit(&self, prompt_len: usize, max_tokens: usize) -> bool {
// 估算消耗的计算预算
let estimated_cost = self.estimate_compute_cost(prompt_len, max_tokens);
let current = self.current_usage.load(Ordering::Relaxed);
if current + estimated_cost <= self.prefill_budget.load(Ordering::Relaxed) {
self.current_usage.fetch_add(estimated_cost, Ordering::Relaxed);
true
} else {
false
}
}
fn estimate_compute_cost(&self, prompt_len: usize, max_tokens: usize) -> u64 {
// Prefill: O(n²) attention;Decode: O(n) per token
let prefill_cost = (prompt_len * prompt_len) as u64;
let decode_cost = (prompt_len * max_tokens) as u64;
// 加权:Prefill Compute 成本更高,乘 2.0 系数
prefill_cost * 2 + decode_cost
}
}
七、生产级模型热更新
推理服务网格必须支持模型权重热切换,不能停服。以下是实现方案:
// hot_reload.rs
pub struct ModelVersionManager {
versions: RwHashMap<String, ModelVersion>,
current_active: AtomicU64, // 当前活跃版本号
draining_versions: Vec<ModelVersion>,
}
impl ModelVersionManager {
/// 加载新模型版本,逐步切换流量
pub async fn rollout(&self, new_version: ModelVersion) -> Result<()> {
// 1. 后台加载权重到 GPU Memory Pool
let loaded = self.load_weights_background(&new_version).await?;
// 2. Canary 发布:5% 流量切入新版本
self.traffic_split(TrafficSplit {
stable: 0.95,
canary: 0.05,
}).await;
// 3. 观察 5 分钟,检查错误率、延迟指标
let healthy = self.observe_canary(Duration::from_secs(300)).await;
if healthy {
// 4. 逐步扩大新版本流量比例
for ratio in [0.25, 0.5, 0.75, 1.0] {
self.traffic_split(TrafficSplit {
stable: 1.0 - ratio,
canary: ratio,
}).await;
tokio::time::sleep(Duration::from_secs(120)).await;
}
// 5. 旧版本进入 draining 状态,等待存量请求完成
self.drain_old_versions().await;
} else {
// 6. 自动回滚
self.traffic_split(TrafficSplit {
stable: 1.0,
canary: 0.0,
}).await;
self.unload_version(&new_version.id).await?;
}
Ok(())
}
}
八、GPU 资源池化与显存超配
在推理网格中,GPU 显存是核心瓶颈。通过 MIG(Multi-Instance GPU)和显存超配(Memory Overcommit)技术,可以实现更高的 GPU 利用率:
- MIG Profile 动态调整:白天推理高峰期分配大 Profile(7g.80gb),夜间批处理迁移到小 Profile 并发执行
- vGPU Time-Slice:非实时任务(Embedding 模型)共享同一 GPU,通过 CUDA Stream 优先级控制保证在线推理的延迟 SLO
- 显存超配配合 KV Cache 分级:当 GPU 显存超配时,超出部分自动降级到 CPU DRAM 和 NVMe,通过预取流水线隐藏延迟
实际效果:在 8xH100 集群上部署 Llama-70B 时,上述混合调度方案可将单卡 QPS 提升 2-3 倍,同时 P99 TTFT 控制在 500ms 以内。
九、总结与展望
AI 推理服务网格正在从"手工作坊"走向工业化生产。核心设计原则可以总结为:
- 计算与状态分离:Prefill/Decode 解耦,KV Cache 统一管理,最大化 GPU 利用率
- 多维度感知路由:KV Cache 命中率、GPU 算力、网络拓扑三维协同调度
- 分级存储 + 智能预取:GPU→CPU→NVMe 三级缓存,按需流动
- 预算化限流:基于计算量的自适应限流替代固定 QPS 限流
- 灰度热更新:模型版本管理是企业级推理平台的必备能力
未来趋势看,推理服务网格还将进一步融合:与 Kubernetes Device Plugin 深度集成实现 GPU 池化、支持跨数据中心推理的 Geo-Distributed KV Cache、以及和 vLLM/SGLang/TensorRT-LLM 等推理引擎的标准化接口对接。
推理基础设施的大幕才刚刚拉开。
参考资料:
- DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving (OSDI 2024)
- Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving (ASPLOS 2025)
- SGLang: Efficient Execution of Structured Language Model Programs (2024)
- vLLM: Efficient Memory Management for Large Language Model Serving with PagedAttention (SOSP 2023)
- NVIDIA TensorRT-LLM: A TensorRT Toolbox for Optimized Large Language Model Inference

发表评论 取消回复