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)             │
└─────────────────────────────────────────────────────┘

这个架构的关键设计是:

  1. Prefill Worker Pool:专做首 token 预填充(计算密集,需要高算力)
  2. Decode Worker Pool:专做逐 token 解码生成(访存密集,需要大显存带宽)
  3. KV Cache 统一内存池:脱离 GPU 显存,实现跨实例共享
  4. 推理路由器:根据模型名称、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);
            }
        }
    }
}

这个设计的关键优势:

  1. Prefix Caching:相同 System Prompt 的请求(如都带相同 System Prompt 的对话),直接复用已计算 KV Cache,Prefill 时间降低 80%+
  2. 自动分层:热数据自动升到 GPU 显存,冷数据落盘,对上层模型透明
  3. 按需分配:不再预留最大长度,按实际使用增量分配

五、多模型混合推理的编排调度

当集群同时运行多个模型时,调度问题变得极其复杂。以下是关键策略:

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 推理服务网格正在从"手工作坊"走向工业化生产。核心设计原则可以总结为:

  1. 计算与状态分离:Prefill/Decode 解耦,KV Cache 统一管理,最大化 GPU 利用率
  2. 多维度感知路由:KV Cache 命中率、GPU 算力、网络拓扑三维协同调度
  3. 分级存储 + 智能预取:GPU→CPU→NVMe 三级缓存,按需流动
  4. 预算化限流:基于计算量的自适应限流替代固定 QPS 限流
  5. 灰度热更新:模型版本管理是企业级推理平台的必备能力

未来趋势看,推理服务网格还将进一步融合:与 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
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部