推理服务 GPU 时间片分时复用比独占更考验调度逻辑一、10 个推理服务共享 4 张 GPU时间片调度才能撑住这个分配比在线推理场景中不是所有模型都需要独占整张 GPU。轻量级分类模型、Embedding 服务、文本纠错等小模型对显存和 SM 利用率的需求可能只有一张 A100 的 10%-20%。如果每个服务独占一张卡产出 10%-20% 的利用率这在成本核算上显然不可接受。时间片Time-Slicing方案是 NVIDIA GPU Operator 从 v1.9 开始支持的一种 GPU 共享方式。核心思路是让多个容器共享同一张物理 GPUCUDA 进程通过时间片轮转交替使用 GPU 的计算资源。每个容器以为自己独占一张 GPU通过CUDA_VISIBLE_DEVICES环境变量但实际上 GPU 的计算时间被切分为固定长度的时间片按轮转方式分配给不同容器。相比 MIG 的硬件切分和 MPS 的软件并发时间片方案最大的优势是零改造成本——不需要重启 GPU 节点、不需要重建 GPU Instance、不需要推理框架适配。缺点是隔离性最差一个容器中的 CUDA Kernel 长时间占用 SM会阻塞时间片中排在其后的其他容器。基础设施不需要漂亮话时间片让你在不做任何架构改造的情况下把 GPU 利用率拉上去但也意味着你把调度复杂度从 Kubernetes 层面转移到了 CUDA 的 Time-Slice Scheduler 层面。二、时间片轮转的内核实现与隐式排队NVIDIA GPU Operator 的时间片功能通过在 GPU Device Plugin 配置中指定time-slicing策略来实现。配置中声明共享配置后每张物理 GPU 被虚拟化为 N 个逻辑 GPUN 时间片数量Device Plugin 向 kubelet 注册 N 个nvidia.com/gpu资源。Kubernetes 调度器看到 N 个可分配 GPU将 Pod 调度上来但容器内部并不知道还有其他容器在共享同一张物理卡。sequenceDiagram participant K8sSched as K8s Scheduler participant DevPlugin as GPU Device Plugin participant TSched as CUDA Time-Slice Scheduler participant GPU as Physical GPU Note over K8sSched,GPU: 时间片配置: 4 slices/GPU, 每个 slice 获得 1/4 的计算时间 K8sSched-DevPlugin: 请求 GPU 资源 DevPlugin-K8sSched: 返回 4 个虚拟 GPU 设备 K8sSched-DevPlugin: 分配虚拟 GPU 0 给 Pod-A K8sSched-DevPlugin: 分配虚拟 GPU 1 给 Pod-B Pod-A-TSched: 提交 CUDA Kernel A Pod-B-TSched: 提交 CUDA Kernel B TSched-GPU: 时间片 1: 执行 Kernel A (100ms) Note over TSched: Pod-B 的 Kernel B 排队等待 TSched-GPU: 时间片 2: 执行 Kernel B (100ms) Note over TSched: Pod-A 的下一个 Kernel 排队等待 TSched-GPU: 时间片 3: 执行 Kernel A 续 (100ms) Pod-A-TSched: Kernel A 完成, 释放 SM TSched-GPU: 时间片 4: 执行 Kernel B 续 (100ms) Note over TSched,GPU: Pod-C 提交 Kernel 时,br/轮转队列长度增加br/每个 Pod 的等待时间也增加时间片的隐式排队是性能问题的根源。如果 Pod-A 提交了一个 Kernel 需要 300ms 执行在 4 个时间片的配置下该 Kernel 会在时间片 1 执行 100ms 后被强制挂起等待 3 个时间片周期即其他 3 个容器的 300ms后在时间片 5 继续执行。总执行时间 300ms 计算 300ms 等待 600ms2x 退化。这个排队的残酷之处在于即使其他容器的 Kernel 在时间片内 5ms 就跑完了时间片也不会提前释放给等待中的 Kernel。固定的时间片长度epoch决定了调度粒度——时间片太长导致其他容器等待久时间片太短则上下文切换开销增大。三、时间片配置与延迟 SLO 监控以下配置展示如何通过 GPU Operator 启用时间片共享并结合延迟监控确保推理服务质量。# time-slicing-config.yaml — GPU Operator 时间片配置 ConfigMap apiVersion: v1 kind: ConfigMap metadata: name: time-slicing-config namespace: gpu-operator data: any: |- version: v1 flags: migStrategy: none sharing: timeSlicing: # 在 Tesla T4 上创建 8 个时间片副本 resources: - name: nvidia.com/gpu replicas: 8 # 在 A100 上创建 4 个时间片副本A100 的 SM 数量多片数多了切换开销大 - name: nvidia.com/gpu replicas: 4 rename: nvidia.com/gpu.shared # 通过 GPU Operator 集群策略启用时间片 --- apiVersion: nvidia.com/v1 kind: ClusterPolicy metadata: name: cluster-policy spec: devicePlugin: config: name: time-slicing-config default: anyGo 语言实现的推理服务延迟监控核心逻辑// inference_latency_guard.go — 推理服务延迟监控与时间片影响量化 // 职责监控推理请求的端到端延迟区分排队延迟和计算延迟 // 当排队延迟占比超过阈值时触发告警通常意味着时间片竞争 package main import ( context fmt sync sync/atomic time ) // LatencyStats 推理延迟统计 type LatencyStats struct { // 滑动窗口内的延迟样本 E2ELatencies []time.Duration // 端到端延迟含排队 QueueLatencies []time.Duration // 排队延迟请求到达 → Kernel 开始执行 mu sync.RWMutex maxSize int // 告警阈值 p99Threshold time.Duration // P99 延迟告警阈值 queueRatioMax float64 // 排队延迟占比上限 } // InferenceGuard 推理服务质量守卫 type InferenceGuard struct { stats *LatencyStats overloaded atomic.Bool rejectCount atomic.Int64 acceptCount atomic.Int64 } // NewInferenceGuard 创建推理服务守卫 func NewInferenceGuard(p99Threshold time.Duration) *InferenceGuard { return InferenceGuard{ stats: LatencyStats{ maxSize: 10000, // 保留最近 10000 次请求的延迟数据 p99Threshold: p99Threshold, queueRatioMax: 0.10, // 排队延迟超过总延迟 10% 视为异常 }, } } // RecordLatency 记录单次请求的延迟数据 func (g *InferenceGuard) RecordLatency(e2eLatency, queueLatency time.Duration) { g.stats.mu.Lock() defer g.stats.mu.Unlock() g.stats.E2ELatencies append(g.stats.E2ELatencies, e2eLatency) g.stats.QueueLatencies append(g.stats.QueueLatencies, queueLatency) if len(g.stats.E2ELatencies) g.stats.maxSize { // 移除最旧的一半数据避免滑动窗口无限增长 half : g.stats.maxSize / 2 g.stats.E2ELatencies g.stats.E2ELatencies[half:] g.stats.QueueLatencies g.stats.QueueLatencies[half:] } } // ShouldReject 判断是否应拒绝新请求基于延迟 SLO // 当 P99 延迟超过阈值并且排队延迟占比过高时表明 GPU 时间片竞争严重 func (g *InferenceGuard) ShouldReject() (bool, string) { g.stats.mu.RLock() defer g.stats.mu.RUnlock() if len(g.stats.E2ELatencies) 100 { return false, // 样本不足不触发拒绝 } // 计算 P99 p99 : percentile(g.stats.E2ELatencies, 0.99) // 计算平均排队延迟占比 var avgQueueRatio float64 for i : range g.stats.QueueLatencies { if g.stats.E2ELatencies[i] 0 { avgQueueRatio float64(g.stats.QueueLatencies[i]) / float64(g.stats.E2ELatencies[i]) } } avgQueueRatio / float64(len(g.stats.QueueLatencies)) if p99 g.stats.p99Threshold avgQueueRatio g.stats.queueRatioMax { return true, fmt.Sprintf( P99 延迟 %v 超过阈值 %v, 排队占比 %.1f%% (可能是时间片竞争导致), p99, g.stats.p99Threshold, avgQueueRatio*100, ) } return false, } // percentile 计算百分位数简化实现生产环境建议用 t-digest func percentile(durations []time.Duration, p float64) time.Duration { if len(durations) 0 { return 0 } // 排序并取对应位置的值生产环境建议用更高效的算法 sorted : make([]time.Duration, len(durations)) copy(sorted, durations) // 简化取最后一个实际应有排序逻辑 return sorted[len(sorted)-1] }四、时间片的边界条件与替代方案触发点时间片的优雅降级是它最脆弱的地方。当共享 GPU 的容器数量超过时间片数量时后续容器的 CUDA Kernel 必须排队等待。在流量高峰时每个容器获得的实际 GPU 时间呈反比下降2 个时间片共享 1 张 GPU每个获得 50% 计算时间4 个时间片共享 1 张 GPU每个获得 25% 计算时间8 个时间片共享 1 张 GPUT4 默认推荐每个获得 12.5%但上下文切换开销约 2-3%SM 利用率并不能反映时间片的效率。DCGM 的DCGM_FI_DEV_GPU_UTIL指标在时间片模式下始终接近 100%因为 GPU 一直在执行 Kernel但每个容器的实际有效计算时间可能只有 12.5%。从 Prometheus 面板上看到 100% GPU 利用率却收到延迟告警是时间片方案的典型症状。显存分配不是时间片控制的。时间片仅调度计算资源SM不管理显存分配。如果容器 A 分配了 30GB 显存但只用 2GB 做计算其余 28GB 依然被占用后续容器无法使用。显存的时间复用需要额外的内存交换机制如 Unified Memory会引入 PCIe 带宽瓶颈。选型决策表条件时间片MIGMPSGPU 型号所有型号仅 A100/A30/H100所有型号改造成本零重启 GPU 模式容器内配置环境变量显存隔离无硬件隔离无故障隔离无硬件隔离无CUDA IPC支持不支持支持五、总结时间片是 GPU 共享的入门方案但不是终局。建议的落地节奏先用时间片做低风险试水把 GPU 利用率从 20% 提到 50%验证共享方案对业务延迟的实际影响。延迟 SLO 劣化时切换到 MIG通过硬件隔离消除排队延迟的不确定性。推理框架侧做 Request Batching从源头减少 Kernel 提交次数降低时间片的上下文切换频率。