GPU 时间片调度与多租户推理隔离工程实践

当单个 H100 的价值超过 25,000 美元,推理集群的 GPU 利用率每提升 1% 就能为中型 AI 服务商每年节省数十万美元。本文深入剖析从硬件分区到软件时间片调度的多层 GPU 共享技术分析栈,覆盖 NVIDIA MPS、MIG、时间片轮转、多优先级队列抢占等关键机制,以及它们在真实推理部署中的工程权衡。


一、为什么 GPU 共享是推理系统的核心问题

AI 推理的 GPU 利用率困境是一个被严重低估的工程挑战。与训练任务长期满载不同,在线推理请求具有两个根本性特征:

脉冲式请求负载: 用户的推理请求到达时间不可预测,存在明显的潮汐效应。低峰时段 GPU 利用率经常低于 15%,而高峰期又面临 SLO 违约风险。

长尾延迟敏感: 生成式模型(LLM)的自回归特性使得请求服务时间高度不均匀——短查询可能几十毫秒完成,而长序列生成(如 4K token 输出)可能占用 GPU 数百毫秒甚至数秒。少数"慢请求"会拖累整个批次的调度效率。

NVIDIA 白皮书数据显示,典型在线推理集群的平均 GPU 利用率在 18-35% 之间,离线批处理场景可达 60-80%。而云服务商的定价模型中,GPU 实例按整机计费,因此将多个低优先级工作负载"塞"进同一块 GPU 的动机非常强烈。

GPU 共享技术栈从底向上可分为四层:

┌─────────────────────────────────────────────┐
│  Layer 4: 应用层调度                          │
│  (推理引擎批处理、请求路由、SLO 优先级队列)      │
├─────────────────────────────────────────────┤
│  Layer 3: 虚拟化层                            │
│  (MIG 硬件分区、SR-IOV 虚拟功能)              │
├─────────────────────────────────────────────┤
│  Layer 2: 运行时层                            │
│  (MPS 多进程共享、CUDA Time-Slice Scheduling) │
├─────────────────────────────────────────────┤
│  Layer 1: 硬件层                              │
│ (GPU 上下文切换、GMMU 地址空间隔离)             │
└─────────────────────────────────────────────┘

每一层都有不同的隔离保证和开销模型,实际生产系统往往是多层叠加使用。


二、NVIDIA MPS:轻量级多进程共享

2.1 原理与架构

NVIDIA Multi-Process Service (MPS) 是 CUDA 提供的用户态运行时组件,它允许不同进程的 CUDA 上下文在同一 GPU 上并发执行 kernel。与传统 CUDA 上下文串行执行(通过时间片轮转切换完整上下文)不同,MPS 的核心机制是:

单一上下文共享: MPS Server 作为特权进程维护一个"虚拟 GPU 上下文",所有 Client 进程通过 UNIX domain socket 连接到 MPS Server,将自己的 kernel 提交到共享流水线。

并发 kernel 执行: MPS Client 提交的 kernel 可以在 GPU SM(流式多处理器)上真正并行执行——前提是 SM 资源不冲突。这与传统模式下 kernel 严格串行化有本质区别。

内存隔离: 不同 Client 的显存分配通过 CUDA 虚拟地址管理保持隔离,一个进程无法访问其他进程的显存。

                    ┌──────────────┐
                    │  MPS Server   │
                    │ (特权上下文)   │
                    └──────┬───────┘
                           │ UNIX Socket
              ┌────────────┼────────────┐
              ▼            ▼            ▼
        ┌──────────┐ ┌──────────┐ ┌──────────┐
        │ Process A │ │ Process B │ │ Process C │
        │ (LLM 推理) │ │(Embedding)│ │ (批处理)  │
        └──────────┘ └──────────┘ └──────────┘

2.2 配置与调优

MPS 的配置主要通过环境变量控制:

# 启动 MPS 控制守护进程
export CUDA_VISIBLE_DEVICES=0
export CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mps
export CUDA_MPS_LOG_DIRECTORY=/tmp/nvidia-log
nvidia-cuda-mps-control -d

# 关键调优参数
# 1. 线程百分比:限制 MPS Client 可占用的 SM 比例
export CUDA_MPS_ACTIVE_THREAD_PERCENTAGE=50  # 最多使用 50% SM 资源

# 2. 限制并发 Client 数(通过连接数控制)
# 默认无明确限制,生产中建议配合 cgroup 限制 PID

2.3 MPS 的限制与陷阱

MPS 虽然在轻量级共享场景表现优秀,但有几个关键限制使其无法胜任所有多租户推理场景:

1. 单点故障风险: 如果 MPS Server 崩溃,所有依赖它的 Client 进程的 CUDA 上下文将全部失效。在一个 Client 触发 GPU 内存越界时,可能导致整个 MPS 实例挂掉。

2. 弱隔离性: MPS 没有显存使用量的硬限制。一个 Client 可以分配超过其"公平份额"的显存,导致其他 Client 的显存分配失败(OOM)。

3. 计算抢占粒度有限: MPS 的 Client 并发是协作式的——如果一个 Client 提交了大量长时间 kernel,其他 Client 的延迟 SLO 可能无法保证。

4. 仅支持同构 GPU: MPS 仅允许同一物理 GPU 上的进程共享,不支持跨设备共享。

适用场景:同一组优先级相近、信任边界明确的推理任务共享,如多个轻量级 Embedding 模型共享同一 GPU。


三、MIG 与硬件级隔离

3.1 MIG 分区机制

Multi-Instance GPU (MIG) 是 A100/H100 提供的硬件级分区功能,将物理 GPU 划分为最多 7 个相互隔离的 GPU Instance。

MIG 的关键特性是硬件级内存隔离:每个 GPU Instance 拥有独立的显存地址空间,一个分区的显存访问错误不会影响其他分区。同时 SM(计算单元)也是物理分区而非时间片共享。

3.2 MIG 的分区组合

H100 支持的分区配置(profile)包括:

Profile SM 数 显存(GB) 最大并行数
1g.10gb 14 10 7
2g.20gb 28 20 3
3g.40gb 42 40 2
4g.40gb 56 40 1
7g.80gb 112 (全卡) 80 1

选择合适的 MIG profile 是一个核心约束优化问题:GPU Instance 越大,单请求性能越好,但并发数越低。

3.3 MIG 的工程困境

实际部署中 MIG 面临的挑战超出预期:

分区重建代价高: 更改 MIG 配置需要先将 GPU 上所有进程停止,重新分区,然后重启服务。这导致 MIG 配置不适合需要弹性伸缩的场景。

显存碎片化: 一个 7B 参数的 FP16 模型需要 ~14GB 显存,只能运行在 2g.20gb 或更大分区上。如果集群需要同时运行 3 个这样的模型,至少需要 3 个 2g.20gb 分区,共分配 60GB 显存(实际只需 42GB),浪费率高达 30%。

调度复杂度高: Kubernetes 设备插件对 MIG 的 "Mixed Strategy" 支持有限。调度器需要同时考虑分区类型匹配和节点亲和性,间接增加了 bin-packing 问题的复杂度。


四、CUDA 时间片调度:GPU 上下文抢占

4.1 上下文切换机制

现代 GPU(Volta 架构起)支持 Preemption 级别从 Device-Level 到 Thread-Level 的演进,但 CUDA 运行时的默认行为触发了最粗粒度的抢占——当时间片耗尽时,当前运行的所有 block 被逐出 SM,上下文保存到显存。

时间片调度的关键参数:

┌─────────────────────────────────────────────────────────┐
│                GPU 时间片调度示意                         │
│                                                         │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐            │
│  │ Context A │   │ Context B │   │ Context C │           │
│  │ (高优先级) │   │ (中优先级) │   │ (低优先级) │          │
│  └────┬─────┘   └────┬─────┘   └────┬─────┘           │
│       │              │              │                   │
│  ═════╪══════════════╪══════════════╪═════════▶ 时间    │
│  ┌────┴────┐    ┌────┴────┐    ┌────┴────┐            │
│  │ QA: 2ms │    │ QB: 1ms │    │ QC: 3ms │             │
│  │ (TTFT)  │    │ (Gen)   │    │ (后台)  │              │
│  └─────────┘    └─────────┘    └─────────┘            │
│                                                         │
│  时间片长度:通常 10-100μs 级别(取决于 kernel 粒度)       │
└─────────────────────────────────────────────────────────┘

4.2 Stream Priority 与抢占控制

CUDA Stream 支持优先级调度,但没有标准 API 直接设定抢占时机。推理引擎通常采用以下模式:

// 创建不同优先级的 CUDA Stream
cudaStream_t high_priority_stream, low_priority_stream;
cudaStreamCreateWithPriority(&high_priority_stream, cudaStreamNonBlock, -1);  // 高优先级
cudaStreamCreateWithPriority(&low_priority_stream, cudaStreamNonBlock, 0);    // 默认优先级

// 高优先级推理请求走独立 stream
launch_llm_inference(high_priority_stream, input_tokens, output_tokens);

// 低优先级批处理/后台任务
launch_background_batch(low_priority_stream, batch_data);

// 同步:等待高优先级完成
cudaStreamSynchronize(high_priority_stream);

五、推理引擎级调度:SLO 感知的请求调度

5.1 两级调度架构

现代 LLM 推理引擎(如 vLLM、TensorRT-LLM)采用两级调度架构应对多租户 SLO。

5.2 Tag-Based Fairness 调度

这是 vLLM v0.4+ 引入的核心调度机制,解决"饥饿"问题:

class TagBasedScheduler:
    """基于 Tag 的公平调度器,防止某一租户独占 GPU"""
    
    def __init__(self, num_tenant_tags: int):
        self.tag_credits = {tag: MAX_TOKENS_PER_ROUND 
                           for tag in range(num_tenant_tags)}
        self.tag_budgets = {}
        
    def schedule(self, waiting_queue, running_batch):
        """
        每轮迭代的选择逻辑:
        1. 按 Tag 轮转,确保每个租户每轮都至少获得一次调度机会
        2. 同一 Tag 内的请求按 FIFO 处理
        3. 如果某个 Tag 请求为空/预算耗尽,跳过
        """
        scheduled = []
        
        # 按 Tag 分组
        tag_groups = defaultdict(list)
        for req in waiting_queue:
            tag_groups[req.tenant_tag].append(req)
        
        # 轮询 Tag 分配 token 配额
        for tag in sorted(tag_groups.keys()):
            credit = self.tag_credits[tag]
            if credit <= 0:
                continue  # 该 Tag 本已超用信用
            
            for req in tag_groups[tag]:
                req_tokens = req.prompt_len + req.max_new_tokens
                if credit >= req_tokens:
                    scheduled.append(req)
                    credit -= req_tokens
                    self.tag_credits[tag] = credit
                    
                if credit <= MIN_TOKEN_THRESHOLD:
                    break
        
        return scheduled

5.3 Preemption 与 KV Cache 驱逐

当 GPU 显存不足以容纳新请求时,推理引擎执行抢占:

Recomputation Preemption: 优先级低的请求直接丢弃,KV Cache 释放,请求重新排队。适用于短请求。

Swapping Preemption: 低优先级请求的 KV Cache 复制到 CPU 内存,释放 GPU 显存给高优先级请求。适用于长请求恢复成本低廉场景(但 LLM 的 KV Cache 体积巨大,swap 代价很高)。

抢占决策树:

新请求到达(P0 优先级)
         │
         ▼
   剩余显存足够? ──Yes──▶ 直接准入
         │
        No
         ▼
   查找最低优先级运行请求(P_n 最低)
         │
    P_n < P_new?
    /        \
  Yes         No
   │           │
   ▼           ▼
 Swap KV      加入等待队列
 到 CPU       (保留当前批)
   │
   ▼
 KV Cache 重新计算 vs 恢复
(L_prefill >> L_swap → 重算更优)

六、生产级多租户推理架构实战

6.1 SumoCity 的多层隔离模型

结合前述技术栈,一个典型的中型 AI 服务商多租户推理架构如下:

┌──────────────────────────────────────────────────────────────┐
│                     Ingress Network                           │
│            (TLS termination, Rate limiting)                  │
└──────────────────────┬───────────────────────────────────────┘
                       │
              ┌────────▼────────┐
              │  API Gateway     │
              │  (Tenant Auth,   │
              │   Quota Check)   │
              └────────┬────────┘
                       │
         ┌─────────────┼─────────────┐
         ▼             ▼             ▼
   ┌───────────┐ ┌───────────┐ ┌───────────┐
   │ Tenant A  │ │ Tenant B  │ │ Tenant C  │
   │ Gold Tier │ │Silver Tier│ │ Bronze    │
   │ P99<200ms │ │ P99<500ms │ │Best Effort│
   └─────┬─────┘ └─────┬─────┘ └─────┬─────┘
         │             │             │
   ┌─────▼─────────────▼─────────────▼─────┐
   │          Global Scheduler              │
   │  (Bin-packing + SLO feasibility check) │
   └─────────────────┬─────────────────────┘
                     │
    ┌────────────────┼────────────────┐
    ▼                ▼                ▼
┌────────┐    ┌────────┐    ┌────────┐
│ H100   │    │ H100   │    │ H100   │
│ GPU 0  │    │ GPU 1  │    │ GPU 2  │
│        │    │        │    │        │
│ ┌────┐ │    │ ┌────┐ │    │ ┌────┐ │
│ │MIG1│ │    │ │MPS │ │    │ │MPS │ │
│ │P0  │ │    │ │Share│    │ │Share│ │
│ └────┘ │    │ └────┘ │    │ └────┘ │
│ ┌────┐ │    │        │    │        │
│ │MIG2│ │    │        │    │        │
│ │P1  │ │    │        │    │        │
│ └────┘ │    │        │    │        │
└────────┘    └────────┘    └────────┘

6.2 关键 SLO 公式

在设计调度策略时,需要量化几个关键约束:

Little's Law 并发估算:

L = lambda_rate * W

# 其中:
# - L = GPU 中同时服务的平均请求数
# - lambda_rate = 请求到达率 (req/s)
# - W = 平均服务时间 (s)

# 示例:λ=100 req/s, W=0.5s → L=50 并发请求
# 单 H100 (184GB HBM,含 KV Cache ~14GB/并发请求)
# → 最多支持 13 并发请求 → 需 4 台 H100

抢占恢复开销模型:

# Recompute 模式:
T_recovery = T_kv_recompute
# T_kv_recompute = L × prefill_time_per_token
#                ≈ 0.3ms/token × L

# Swap 模式:
T_swap = KV_size / PCIe_bandwidth
#       ≈ (2 × L × d_model × 2bytes) / 32 GB/s
#       ≈ (2 × 4096 × 4096 × 2) / 32e9
#       ≈ 2ms (for L=4096)

# 结论:当已计算前缀 token 数 > 约 7000 时,Swap 优于 Recompute

6.3 KV Cache 水量监控

class KVCacheWatermarkMonitor:
    """实时监控 KV Cache 水位,触发抢占决策"""
    
    def __init__(self, gpu_memory_total, high_watermark=0.85, low_watermark=0.70):
        self.total = gpu_memory_total
        self.high_wm = int(gpu_memory_total * high_watermark)
        self.low_wm = int(gpu_memory_total * low_watermark)
        self.current_usage = 0
        self.preempting = False
    
    def on_allocate(self, size_bytes):
        """请求显存分配,返回是否允许"""
        if self.current_usage + size_bytes > self.high_wm:
            if not self.preempting:
                self._trigger_preemption()
            return False  # 拒绝分配,请求入等待队列
        self.current_usage += size_bytes
        return True
    
    def on_free(self, size_bytes):
        """请求结束,释放显存"""
        self.current_usage -= size_bytes
        if self.preempting and self.current_usage < self.low_wm:
            self._resume_waiting()
    
    def _trigger_preemption(self):
        """触发抢占:选择内存占用最大的低优先级请求驱逐"""
        self.preempting = True
        victim = self._select_victim()
        victim.preempt(preemption_mode=PreemptionMode.RECOMPUTE)
    
    def _select_victim(self):
        """选择抢占目标:优先级最低且 KV Cache 最大的请求"""
        candidates = [r for r in self.running_requests if r.priority < Priority.HIGH]
        return max(candidates, key=lambda r: r.kv_cache_size)

七、2026 年前沿趋势

7.1 Blackwell 架构的调度革新

NVIDIA Blackwell (B200/GB200) 引入了第五代 NVLink 和增强的引擎间通信,其 Fifth Gen Tensor Core 支持更细粒度的计算任务划分,理论上可实现推理级上下文切换延迟从毫秒级降至亚毫秒级别。

7.2 计算配额与 QoS 标准化的探索

AI 工作负载的 QoS 标准化仍在探索中。目前运营商侧主要依赖:

  • Kubernetes Extended Resource + 自定义调度器
  • NVIDIA Triton Inference Model Groups 的优先级设定
  • DRA (Dynamic Resource Allocation) API 的 Alpha 支持

7.3 异构推理的共存调度

CPU-offloading + GPU 联合推理场景的调度成为新热点——当 GPU 处理不了全量请求时,将部分 Layer offload 到 CPU,同时利用 GPU 处理高吞吐的 FFN 层。这种异构共存调度的目标是同时满足延迟 SLO 和吞吐最大化。


八、总结:GPU 共享技术选型决策树

开始
  │
  ▼
需要强隔离(安全/错误隔离)?
  ├── 是 → MIG 硬件分区
  │        │
  │        ▼
  │   分区大小是否可固定?
  │     ├── 是 → 静态 MIG Profile
  │     └── 否 → MIG + MPS 混合
  │
  └── 否 → 是否需要 SLO 保障?
           ├── 是 → MPS + Stream Priority
           │        + 推理引擎 Tag-Based Scheduler
           │
           └── 否 → 默认时间片轮转
                    + 应用层队列优先级

GPU 多租户共享不是一项单一技术的标签,而是一个从硬件到应用层的系统级工程挑战。MPS 提供了轻量级的进程并发但不保证 SLO;MIG 提供了强隔离但缺乏弹性;时间片调度提供了优先级但不提供内存隔离;推理引擎调度填补了应用层的公平性缺口。

核心原则:没有免费的午餐——每种共享技术在提升利用率的同时都在延迟确定性上做出让步。 工程设计者的任务是根据租户的 SLO 等级和信任边界,组合这些技术构建多层防御体系。


关键术语表:

  • SM (Streaming Multiprocessor):GPU 流式多处理器,包含 CUDA Core、Tensor Core、寄存器和 L1 Cache
  • TTFT (Time To First Token):首 token 延迟,衡量推理响应速度的核心指标
  • KV Cache:自回归推理中已计算 token 的 Key/Value 张量缓存,避免重复计算
  • Preemption:抢占,中断当前任务执行更高优先级任务的行为
  • Bin-packing:装箱问题,将不同大小的资源请求调度到有限节点上的组合优化问题
  • Little's Law:排队论基本定理 L=λW,关联系统中的平均项目数与吞吐/延迟
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部