推理服务的 GPU 资源池化管理:基于 CUDA MPS 的多模型混部与隔离策略
推理服务的 GPU 资源池化管理:基于 CUDA MPS 的多模型混部与隔离策略
一、三张 A100 跑 5 个模型,GPU 显存碎片导致总有模型加载失败
在一个多模型推理服务的部署中。需要在 3 张 A100(每张 80GB)上部署 5 个模型。模型的显存需求分别为 45GB、30GB、25GB、20GB、15GB(含 KV Cache)。单看总显存 240GB > 总需求 135GB。但实际部署时,显存碎片导致总有模型无法加载。
问题在于,默认的 GPU 分配策略是按整卡分配。每个模型独占一张或多张 GPU。45GB 模型需要独占一张 80GB 卡。30GB 和 25GB 模型无法共存于同一张卡。20GB 和 15GB 模型可以共享一张。3 张卡不够分。出现了一张卡上有大量空闲显存却无法利用的情况。
这就是 GPU 资源池化要解决的问题。通过 CUDA MPS(Multi-Process Service)或 MIG(Multi-Instance GPU)。让多个模型在一张 GPU 上共享计算资源和显存。实现细粒度的资源复用。
二、CUDA MPS 与 MIG 的架构对比与适用场景
两种 GPU 虚拟化技术有根本不同的设计目标。
graph TD A["GPU 资源池化策略"] -- > B["CUDA MPS<br/>多进程服务"] A -- > C["MIG<br/>多实例 GPU"] subgraph MPS 特性 B --> B1["逻辑空间共享"] B --> B2["显存共享,计算时分复用"] B --> B3["故障隔离弱:一个进程的 CUDA 错误影响全局"] B --> B4["性能:可通过 MPS 控制并发度"] end subgraph MIG 特性 C --> C1["物理切分 GPU"] C --> C2["显存和算力硬隔离"] C --> C3["故障隔离强:实例间完全独立"] C --> C4["性能:分配后不可动态调整"] end subgraph 适用场景 D1["MPS: 模型需要弹性资源<br/>同质化计算负载"] D2["MIG: 模型需要硬隔离<br/>多租户严格隔离"] end B --> D1 C --> D2CUDA MPS 是一种软件层面的多进程共享机制。多个 CUDA 进程的逻辑上下文通过 MPS Server 合并。MPS Server 将不同进程的 CUDA Kernel 在时间维度上交错执行。显存空间是共享的。没有硬性的显存隔离。
MIG 是硬件层面的切分(A100/A30/H100 支持)。将一张 GPU 物理切分为多个独立实例。每个实例有独立的显存、缓存和计算单元。实例间完全隔离,一个实例的崩溃不影响其他实例。
关键差异在于隔离性和灵活性。MPS 提供更高的资源利用率(因为共享更灵活)。但隔离性差。MIG 提供强隔离。但切分后不能动态调整。如果需要在一个 GPU 实例上同时跑大小差异大的模型。MPS 更合适。如果需要多租户严格隔离。MIG 是更好的选择。
三、基于 MPS 的资源调度实现
#!/bin/bash # GPU 资源池化管理脚本 # 目标:在有限 GPU 资源上部署多个模型 # ======================================== # 阶段一:启用 CUDA MPS # ======================================== # 设置 MPS 守护进程 export CUDA_VISIBLE_DEVICES=0 # 启动 MPS Control Daemon nvidia-cuda-mps-control -d # 验证 MPS 是否启动 echo "get_server_list" | nvidia-cuda-mps-control # 设置 MPS 的活跃线程百分比 # 为什么限制 80%?预留 20% 给操作系统和系统守护进程 echo "set_default_active_thread_percentage 80" | nvidia-cuda-mps-control # ======================================== # 阶段二:模型部署分配策略 # ======================================== # GPU 0 (80GB):部署两个模型 # Model A: 45GB + 4GB KV Cache = 49GB # Model B: 25GB + 2GB KV Cache = 27GB # Total: 76GB / 80GB,利用率 95% CUDA_VISIBLE_DEVICES=0 ollama serve --model llama3:70b-q4 & CUDA_VISIBLE_DEVICES=0 ollama serve --model qwen2:14b-q4 & # GPU 1 (80GB):部署两个模型 # Model C: 30GB + 3GB KV Cache = 33GB # Model D: 20GB + 2GB KV Cache = 22GB # Model E: 15GB + 2GB KV Cache = 17GB # Total: 72GB / 80GB,利用率 90% CUDA_VISIBLE_DEVICES=1 ollama serve --model mistral:7b-q8 & CUDA_VISIBLE_DEVICES=1 ollama serve --model qwen2:7b-q4 & CUDA_VISIBLE_DEVICES=1 ollama serve --model bge-large:fp16 & # ======================================== # 阶段三:显存监控与告警 # ======================================== # 持续监控脚本 monitor_gpu_memory() { while true; do echo "=== $(date) ===" nvidia-smi --query-gpu=index,name,memory.used,memory.free,utilization.gpu \ --format=csv,noheader # 检查显存使用是否超过阈值 USED=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits -i 0) if [ "$USED" -gt 76000 ]; then # 76GB / 80GB = 95% echo "警告:GPU 0 显存使用超过 95%!" # 触发告警:发送到监控系统 fi sleep 30 done } monitor_gpu_memory &/// GPU 资源管理器 /// /// 职责: /// 1. 追踪每张 GPU 的显存使用情况 /// 2. 为模型分配最优的 GPU /// 3. 在显存不足时触发降级或驱逐策略 use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; #[derive(Debug, Clone)] struct GpuInfo { gpu_id: usize, total_memory_mb: u64, free_memory_mb: u64, utilization_percent: u32, /// 此 GPU 上当前运行的模型 models: Vec<String>, } #[derive(Debug, Clone)] struct ModelRequirement { model_name: String, /// 模型所需显存(含 KV Cache) required_memory_mb: u64, /// 是否需要独占 GPU exclusive: bool, /// 优先级(数字越小优先级越高) priority: u32, } /// GPU 资源池管理器 struct GpuResourcePool { gpus: Arc<RwLock<Vec<GpuInfo>>>, /// 模型到 GPU 的映射 model_placement: Arc<RwLock<HashMap<String, usize>>>, } impl GpuResourcePool { /// 为模型分配 GPU /// /// 分配策略: /// 1. 优先查找已有模型的 GPU(提高利用率) /// 2. 使用 Best-Fit 算法减少碎片 /// 3. 独占模型跳过共享 GPU async fn allocate_gpu( &self, requirement: &ModelRequirement, ) -> Result<usize, String> { let gpus = self.gpus.read().await; if requirement.exclusive { // 独占模型:找一张空闲的 GPU for gpu in gpus.iter() { if gpu.models.is_empty() && gpu.free_memory_mb >= requirement.required_memory_mb { return Ok(gpu.gpu_id); } } return Err("无空闲 GPU 可用于独占模型".to_string()); } // 共享模型:Best-Fit 算法 // 优先选择已有模型的 GPU,提高资源利用率 let mut best_fit: Option<usize> = None; let mut best_waste = u64::MAX; for gpu in gpus.iter() { if gpu.free_memory_mb >= requirement.required_memory_mb { let waste = gpu.free_memory_mb - requirement.required_memory_mb; // 偏好:已有模型的 GPU > 空闲 GPU let score = if gpu.models.is_empty() { waste + 1024 // 惩罚空闲 GPU(鼓励共享) } else { waste }; if score < best_waste { best_waste = score; best_fit = Some(gpu.gpu_id); } } } best_fit.ok_or_else(|| { format!( "模型 {} 需要 {}MB 显存,所有 GPU 均无足够空间", requirement.model_name, requirement.required_memory_mb ) }) } /// 显存不足时的降级策略 /// /// 选项: /// 1. 减少低优先级模型的 KV Cache 大小 /// 2. 卸载低优先级模型(Swapping) /// 3. 拒绝新模型加载 async fn handle_oom(&self, gpu_id: usize, shortage_mb: u64) -> Result<(), String> { let mut gpus = self.gpus.write().await; let mut placements = self.model_placement.write().await; if let Some(gpu) = gpus.iter_mut().find(|g| g.gpu_id == gpu_id) { // 查找可降级的低优先级模型 // 此处简化:卸载第一个非独占模型 let evict_model = gpu.models.iter() .find(|name| !name.contains("high-priority")) .cloned(); if let Some(model) = evict_model { gpu.models.retain(|m| m != &model); placements.remove(&model); gpu.free_memory_mb += shortage_mb; // 简化计算 Ok(()) } else { Err("无可降级模型,GPU 显存不足".to_string()) } } else { Err("GPU 未找到".to_string()) } } }分配策略采用 Best-Fit 算法。在各种碎片化的显存空洞中选择"最紧凑"的那个。这减少了显存碎片的产生。惩罚系数鼓励模型共享 GPU。将多个小模型放在同一张卡上。为独占模型保留整张空闲卡。
OOM 处理中的降级策略是生产环境必需的容错机制。当显存不足时,优先减少低优先级模型的 KV Cache 或卸载。而不是让新部署直接失败。
四、MPS 的隔离缺陷与生产级注意事项
MPS 的资源共享带来了利用率的好处。但引入了隔离性的风险。
首先是故障传播。在 MPS 模式下,一个 CUDA 进程的致命错误(如非法内存访问、越界 Kernel)可能导致 MPS Server 崩溃。进而影响同一 GPU 上所有其他进程。需要额外的进程监控和自动重启机制。
其次是性能干扰。共享 GPU 的竞争可能导致表现不稳定的延迟抖动。当高优先级模型推理时。如果低优先级模型的 Kernel 正在执行。高优先级模型需要等待。MPS 不提供优先级调度。需要通过限制低优先级模型的并发度来间接控制。
第三是 MPS 和 MIG 不能同时使用。这是硬件层面的限制。需要在规划和设计阶段就做出选择。
最后是资源碎片随时间恶化。随着模型的加载和卸载。显存中产生不可用的小碎片。类似于操作系统的内存碎片问题。定期或在低峰期执行模型的卸载和重新加载。可以整理碎片。
五、总结
- CUDA MPS 通过软件层面多进程共享 GPU。实现显存和算力的细粒度复用。提高 GPU 利用率。
- MIG 通过硬件切分提供强隔离。适合多租户场景。但不能动态调整分配。
- GPU 池化分配采用 Best-Fit 算法减少显存碎片。并惩罚空闲 GPU 鼓励模型共享。
- MPS 的故障隔离性弱。一个 CUDA 进程的错误可能影响同 GPU 上所有进程。需要进程监控和自动重启。
- 定期模型重新加载可以整理显存碎片。建议在低峰期执行。避免影响在线服务。