训练推理混合调度同一个集群的 GPU 怎么分才不打架一、训练要吞吐、推理要延迟混在一起两败俱伤GPU 集群中训练和推理的资源使用模式本质对立。训练追求高吞吐——一批数据处理完立即接下一批GPU 利用率接近 100%运行时长以小时甚至天为单位。推理追求低延迟——请求到达后 200ms 内必须返回GPU 负载呈脉冲状大量时间在等待请求而非计算。混部时典型的冲突表现训练任务的持续高压让同一节点的推理 Pod 无法获得足够 GPU 时间片P99 延迟从毫秒级飙升到秒级推理的突发流量抢占 GPU 内存训练进程 OOM 崩溃NCCL 通信环中混入推理 Pod 的干扰流量训练迭代时间大幅波动。混合调度的目标不是消除冲突——这是不可能的——而是在保证推理 SLO 的前提下最大化训练吞吐。核心手段是物理隔离 优先级抢占 动态资源调节。二、分池与弹性混合调度架构资源池划分遵循 3:5 的比例——推理独占 24 张 GPU、训练和弹性共享 40 张。这个比例不是固定的而是基于历史推理负载的 P95 峰值来确定的。弹性共享池的工作原理正常状态下40 张卡由训练任务使用低优任务占大部分。当推理负载上升通过 Prometheus 监控 GPU 利用率和请求队列深度检测训练 Pod 被逐步驱逐——先驱逐可中断的低优训练已做 Checkpoint 保存再考虑抢占常规训练。抢占通过 Pod Priority 实现推理 Pod 的 Priority 设得比训练 Pod 更高资源紧张时调度器自动驱逐低优先级的训练 Pod。三、Go 实现的训练推理混合调度控制器package scheduler import ( context fmt sync time promapi github.com/prometheus/client_golang/api promv1 github.com/prometheus/client_golang/api/prometheus/v1 ) // PoolType 资源池类型 type PoolType string const ( PoolInference PoolType inference // 推理专属池 PoolShared PoolType shared // 弹性共享池 ) // GPUNode 集群 GPU 节点信息 type GPUNode struct { Name string TotalGPU int UsedGPU int Pool PoolType GPUUtil float64 // 当前 GPU 利用率 MemUtil float64 // 当前显存利用率 } // MixedScheduler 训练推理混合调度器 type MixedScheduler struct { mu sync.RWMutex nodes map[string]*GPUNode inferPoolSize int sharedPoolSize int // 推理扩容阈值 inferExpandThreshold float64 // 默认 0.80 // 训练缩容阈值 trainShrinkThreshold float64 // 默认 0.85 // Pod 驱逐器接口 evictor PodEvictor // Prometheus 查询客户端 promClient promv1.API } // PodEvictor 定义 Pod 驱逐操作 type PodEvictor interface { EvictPods(ctx context.Context, namespace string, labels map[string]string, maxCount int) (int, error) ListPodsByPriority(ctx context.Context, namespace string, labels map[string]string) ([]PodInfo, error) } type PodInfo struct { Name string Namespace string Priority int Labels map[string]string } // Reconcile 核心调度调和循环 func (s *MixedScheduler) Reconcile(ctx context.Context) error { s.mu.Lock() defer s.mu.Unlock() // 1. 收集当前 GPU 集群状态 if err : s.collectNodeMetrics(ctx); err ! nil { return fmt.Errorf(收集节点指标失败: %w, err) } // 2. 计算推理池的当前利用率 inferUtil : s.calculatePoolUtilization(PoolInference) sharedUtil : s.calculatePoolUtilization(PoolShared) // 3. 推理池扩容决策 if inferUtil s.inferExpandThreshold { needGPUs : s.calculateExpansionGPUs(inferUtil) fmt.Printf(推理利用率 %.2f%% 超过阈值需要从共享池获取 %d 张 GPU\n, inferUtil*100, needGPUs) if err : s.expandInferencePool(ctx, needGPUs); err ! nil { return fmt.Errorf(推理池扩容失败: %w, err) } } // 4. 推理池缩容决策利用率回落时归还 GPU if inferUtil 0.40 sharedUtil 0.60 { releaseGPUs : s.calculateReleaseGPUs(inferUtil) fmt.Printf(推理利用率回落至 %.2f%%释放 %d 张 GPU 回共享池\n, inferUtil*100, releaseGPUs) if err : s.shrinkInferencePool(ctx, releaseGPUs); err ! nil { return fmt.Errorf(推理池缩容失败: %w, err) } } // 5. 共享池内训练任务调度 if err : s.scheduleTrainingJobs(ctx); err ! nil { return fmt.Errorf(训练任务调度失败: %w, err) } return nil } // expandInferencePool 从共享池腾出 GPU 给推理使用 func (s *MixedScheduler) expandInferencePool(ctx context.Context, needGPUs int) error { // 逐级驱逐策略 // 第1步驱逐 P2 离线实验可中断有 Checkpoint evicted, err : s.evictor.EvictPods(ctx, training, map[string]string{priority: p2-offline}, needGPUs) if err ! nil { return err } remaining : needGPUs - evicted if remaining 0 { return nil } // 第2步驱逐 P1 常规训练可能丢失少量进度 evicted, err s.evictor.EvictPods(ctx, training, map[string]string{priority: p1-normal}, remaining) if err ! nil { return err } remaining - evicted if remaining 0 { return nil } // 第3步所有可驱逐 Pod 都已处理仍不足——告警 if remaining 0 { return fmt.Errorf(无法满足推理扩容需求仍缺 %d 张 GPU, remaining) } return nil } // shrinkInferencePool 推理负载回落时释放 GPU func (s *MixedScheduler) shrinkInferencePool(ctx context.Context, releaseGPUs int) error { // 逐步缩容推理 Pod让共享池恢复训练 for _, node : range s.nodes { if releaseGPUs 0 { break } if node.Pool PoolInference node.UsedGPU 0 { node.Pool PoolShared node.TotalGPU - 1 releaseGPUs-- // 此处的节点标记更新会触发训练调度器感知新增可用资源 } } return nil } // calculatePoolUtilization 计算指定资源池的 GPU 平均利用率 func (s *MixedScheduler) calculatePoolUtilization(pool PoolType) float64 { var totalUtil float64 count : 0 for _, node : range s.nodes { if node.Pool pool node.UsedGPU 0 { totalUtil node.GPUUtil count } } if count 0 { return 0 } return totalUtil / float64(count) } // calculateExpansionGPUs 计算推理池需要额外多少 GPU func (s *MixedScheduler) calculateExpansionGPUs(currentUtil float64) int { // 目标将利用率降至 70% targetUtil : 0.70 currentEffective : currentUtil * float64(s.inferPoolSize) targetTotal : currentEffective / targetUtil need : int(targetTotal) - s.inferPoolSize if need 1 { need 1 } // 不超过共享池可用 GPU maxBorrow : s.sharedPoolSize if need maxBorrow { need maxBorrow } return need } // calculateReleaseGPUs 计算可以释放回共享池的 GPU 数量 func (s *MixedScheduler) calculateReleaseGPUs(currentUtil float64) int { // 利用率低于 40% 时逐步释放空闲 GPU idleFraction : 0.40 - currentUtil if idleFraction 0 { return 0 } release : int(idleFraction * float64(s.inferPoolSize)) if release 1 { release 1 } return release } // scheduleTrainingJobs 在共享池内调度训练任务 func (s *MixedScheduler) scheduleTrainingJobs(ctx context.Context) error { // 按优先级顺序尝试调度训练 PodP0 P1 P2 priorities : []string{p0-urgent, p1-normal, p2-offline} for _, priority : range priorities { pods, err : s.evictor.ListPodsByPriority(ctx, training, map[string]string{priority: priority}) if err ! nil { continue } for _, pod : range pods { // 检查共享池是否有可用 GPU if s.sharedPoolAvailable() 0 { return nil } // 调度逻辑此处省略具体实现 _ pod } } return nil } func (s *MixedScheduler) sharedPoolAvailable() int { available : 0 for _, node : range s.nodes { if node.Pool PoolShared { available node.TotalGPU - node.UsedGPU } } return available } func (s *MixedScheduler) collectNodeMetrics(ctx context.Context) error { // 从 Prometheus 拉取 GPU 节点指标 // 查询示例: avg by(node) (DCGM_FI_DEV_GPU_UTIL) query : avg by(node) (DCGM_FI_DEV_GPU_UTIL{clusterprod}) result, _, err : s.promClient.Query(ctx, query, time.Now()) if err ! nil { return fmt.Errorf(Prometheus 查询失败: %w, err) } // 解析 result 并更新 s.nodes 中的 GPU 利用率 _ result return nil }四、混合调度的核心权衡Checkpoint 与驱逐的时序对齐驱逐训练 Pod 之前必须确认最近的 Checkpoint 已经保存完成。如果驱逐和 Checkpoint 保存之间的窗口过大被驱逐的训练任务会丢失大量计算进度。建议在驱逐前调用训练框架的 Checkpoint API如 PyTorch 的torch.save等待确认完成后再执行驱逐。弹性池的收敛稳定性推理负载是波动的。如果推理利用率在阈值边缘反复横跳80% → 85% → 79% → 82%会导致训练 Pod 被反复驱逐和恢复——这就是震荡问题。需要通过引入迟滞带来解决扩容阈值设为 80%缩容阈值设为 40%避免在临界点频繁切换。GPU 内存碎片训练 Pod 被驱逐后释放的 GPU 可能分布在不同的节点上形成碎片化的空闲资源。新的训练任务可能因为需要的 GPU 数量不够跨节点 NCCL 通信性能差而无法调度。需要定期运行 GPU 碎片整理任务通过 Pod 迁移聚居空闲 GPU。五、总结训练推理混合调度的核心策略是池化隔离 弹性抢占。推理独占池保障 SLO弹性共享池在推理低负载时回收 GPU 给训练使用。调度的关键指标是推理池利用率的扩缩容阈值和迟滞带大小这两个参数直接影响系统的稳定性和 GPU 利用率。任何单一集群同时承载训练和推理的方案都必须面对这两个场景在资源使用模式上的根本冲突。