
AI 服务的 K8s 调度策略从资源争抢到智能编排云原生环境下的推理服务治理一、AI 推理服务的调度困境GPU 资源争抢与弹性伸缩的矛盾AI 推理服务在 Kubernetes 集群中的调度面临独特的挑战。与普通 Web 服务不同推理服务对 GPU 资源有强依赖而 GPU 是集群中最稀缺、最昂贵的资源。当多个推理服务共享 GPU 节点时资源争抢导致推理延迟抖动当推理服务独占 GPU 节点时资源利用率极低成本居高不下。更复杂的是推理服务的流量特征。大模型推理的请求耗时长数百毫秒到数秒内存占用高模型权重动辄数十 GB且不同模型的资源需求差异巨大。一个 7B 参数模型需要约 14GB 显存一个 70B 模型需要约 140GB 显存。传统的 K8s 调度器基于 CPU/内存的调度策略无法有效处理 GPU 资源的分配问题。弹性伸缩同样棘手。推理服务的冷启动耗时远超普通服务——加载模型权重需要数十秒到数分钟。当流量突增触发 HPA 扩容时新 Pod 在模型加载完成前无法服务导致请求排队超时。二、AI 推理服务的调度架构与资源模型AI 推理服务的调度需要在三个层面优化Pod 级别的 GPU 资源分配、节点级别的 GPU 共享策略、集群级别的弹性伸缩与调度。flowchart TD A[推理服务部署请求] -- B[调度决策层] B -- B1[GPU 资源需求评估] B -- B2[模型加载时间预估] B -- B3[流量预测与预热] B1 -- C{调度策略选择} B2 -- C B3 -- C C --|独占模式| D1[GPU 独占: 高优先级服务] C --|共享模式| D2[GPU 时间片: 低优先级服务] C --|MPS 模式| D3[多进程共享: 中等优先级] D1 -- E[Pod 调度与绑定] D2 -- E D3 -- E E -- F[模型预热与就绪检查] F -- G[流量接入] G -- H[运行时监控] H -- H1[GPU 利用率] H -- H2[推理延迟 P99] H -- H3[队列深度] H1 -- I{是否需要扩缩容?} H2 -- I H3 -- I I --|扩容| J[预热新 Pod 后接入流量] I --|缩容| K[流量排空后终止 Pod] style B fill:#e1f5fe style I fill:#fff3e02.1 GPU 资源模型与调度策略# gpu-resource-model.yaml — AI 推理服务的 GPU 资源定义 # 设计意图通过自定义资源定义精确描述 GPU 需求 # 支持独占、共享、MPS 三种模式为调度器提供决策依据 apiVersion: scheduling.ai/v1alpha1 kind: InferenceService metadata: name: llm-7b-service spec: model: name: llama-7b storageUri: s3://models/llama-7b framework: vllm resources: gpu: type: nvidia.com/gpu count: 1 # 请求的 GPU 数量 memory: 14Gi # 显存需求 sharing: mode: exclusive # exclusive | time-slicing | mps # time-slicing 配置GPU 时间片共享 # timeSlicing: # replicas: 4 # 将 1 个 GPU 切分为 4 个时间片 # mps 配置多进程服务 # mps: # activeThreadPercentage: 50 scheduling: priority: high # high | medium | low affinity: # 优先调度到已有模型缓存的节点减少冷启动时间 nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - preference: matchExpressions: - key: model-cache.ai/llama-7b operator: In values: [cached] weight: 100 scaling: minReplicas: 1 maxReplicas: 10 # 自定义指标基于推理队列深度扩缩容 metrics: - type: Pods pods: metric: name: inference_queue_depth target: type: AverageValue averageValue: 5 # 预热策略扩容时先加载模型再接入流量 warmup: enabled: true readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 10 periodSeconds: 52.2 自定义调度器实现// scheduler.go — AI 推理服务自定义调度器 // 设计意图基于 GPU 资源可用性和模型缓存状态进行调度决策 // 优先将 Pod 调度到已有模型缓存的节点减少冷启动时间 package scheduler import ( context fmt sort v1 k8s.io/api/core/v1 k8s.io/apimachinery/pkg/runtime k8s.io/kubernetes/pkg/scheduler/framework ) const Name AIInferenceScheduler type AIScheduler struct { handle framework.Handle } func New(obj runtime.Object, h framework.Handle) (framework.Plugin, error) { return AIScheduler{handle: h}, nil } func (s *AIScheduler) Name() string { return Name } // Score 打分为每个节点评分分数越高越优先调度 func (s *AIScheduler) Score( ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo, ) (int64, *framework.Status) { node : nodeInfo.Node() if node nil { return 0, framework.NewStatus(framework.Error, node not found) } var score int64 0 // 因子 1模型缓存命中权重 50 // 如果节点已缓存 Pod 需要的模型大幅加分 modelName : pod.Labels[model.ai/name] if modelName ! { cacheKey : fmt.Sprintf(model-cache.ai/%s, modelName) if val, ok : node.Labels[cacheKey]; ok val cached { score 50 } } // 因子 2GPU 资源可用性权重 30 // 优先调度到 GPU 资源充足的节点 gpuAllocatable : node.Status.Allocatable[nvidia.com/gpu] gpuRequested : nodeInfo.RequestedResource.ScalarResources[nvidia.com/gpu] gpuAvailable : gpuAllocatable.Value() - gpuRequested if gpuAvailable 0 { score 30 } // 因子 3节点负载权重 20 // 优先调度到负载较低的节点避免资源争抢 // 通过节点注解中的 GPU 利用率判断 if gpuUtilStr, ok : node.Annotations[gpu.ai/utilization]; ok { var util int fmt.Sscanf(gpuUtilStr, %d, util) // 利用率越低分数越高 score int64((100 - util) / 5) } return score, nil } func (s *AIScheduler) ScoreExtensions() framework.ScoreExtensions { return nil } // Filter 过滤排除不满足 GPU 需求的节点 func (s *AIScheduler) Filter( ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo, ) *framework.Status { node : nodeInfo.Node() if node nil { return framework.NewStatus(framework.Error, node not found) } // 检查节点是否有 GPU 资源 gpuAllocatable, ok : node.Status.Allocatable[nvidia.com/gpu] if !ok || gpuAllocatable.IsZero() { return framework.NewStatus(framework.Unschedulable, node has no GPU) } // 检查 GPU 显存是否满足需求 if memReq, ok : pod.Annotations[gpu.ai/memory-required]; ok { nodeGPUMem : node.Annotations[gpu.ai/memory-total] if nodeGPUMem memReq { return framework.NewStatus( framework.Unschedulable, fmt.Sprintf(GPU memory insufficient: need %s, have %s, memReq, nodeGPUMem), ) } } return nil }三、弹性伸缩与模型预热策略3.1 基于队列深度的 HPA// inference_hpa.go — 推理服务自定义 HPA 控制器 // 设计意图基于推理队列深度和 GPU 利用率进行扩缩容 // 扩容时先预热模型再接入流量避免冷启动导致的请求超时 package controller import ( context fmt time autoscalingv2 k8s.io/api/autoscaling/v2 v1 k8s.io/api/core/v1 metav1 k8s.io/apimachinery/pkg/apis/meta/v1 k8s.io/apimachinery/pkg/labels k8s.io/metrics/pkg/apis/external_metrics ) type InferenceHPAController struct { metricsClient MetricsClient kubeClient KubeClient } type ScaleDecision struct { TargetReplicas int32 Reason string } func (c *InferenceHPAController) CalculateScale( ctx context.Context, hpa *autoscalingv2.HorizontalPodAutoscaler, ) (*ScaleDecision, error) { // 获取当前副本数 currentReplicas : hpa.Status.CurrentReplicas // 采集推理队列深度指标 queueDepth, err : c.getQueueDepthMetric(ctx, hpa) if err ! nil { return nil, fmt.Errorf(获取队列深度失败: %w, err) } // 采集 GPU 利用率指标 gpuUtilization, err : c.getGPUUtilizationMetric(ctx, hpa) if err ! nil { return nil, fmt.Errorf(获取 GPU 利用率失败: %w, err) } // 扩缩容决策逻辑 var targetReplicas int32 currentReplicas var reason string queueThreshold : int32(5) // 队列深度阈值 gpuScaleUpThreshold : 0.8 // GPU 利用率扩容阈值 gpuScaleDownThreshold : 0.3 // GPU 利用率缩容阈值 if queueDepth queueThreshold || gpuUtilization gpuScaleUpThreshold { // 扩容队列深度或 GPU 利用率超过阈值 increment : int32(queueDepth / queueThreshold) if increment 1 { increment 1 } targetReplicas currentReplicas increment reason fmt.Sprintf( 队列深度 %d 超过阈值 %d, GPU 利用率 %.1f%%, queueDepth, queueThreshold, gpuUtilization*100, ) } else if queueDepth 0 gpuUtilization gpuScaleDownThreshold { // 缩容队列空闲且 GPU 利用率低 if currentReplicas *hpa.Spec.MinReplicas { targetReplicas currentReplicas - 1 reason fmt.Sprintf( 队列空闲, GPU 利用率 %.1f%% 低于阈值 %.1f%%, gpuUtilization*100, gpuScaleDownThreshold*100, ) } } // 限制最大副本数 if targetReplicas hpa.Spec.MaxReplicas { targetReplicas hpa.Spec.MaxReplicas } return ScaleDecision{ TargetReplicas: targetReplicas, Reason: reason, }, nil } func (c *InferenceHPAController) getQueueDepthMetric( ctx context.Context, hpa *autoscalingv2.HorizontalPodAutoscaler, ) (int32, error) { // 从 Prometheus 查询推理队列深度 selector, _ : labels.Parse(hpa.Spec.ScaleTargetRef.Name) metricValue, err : c.metricsClient.GetExternalMetric( ctx, inference_queue_depth, metav1.NamespaceDefault, selector, ) if err ! nil { return 0, err } return int32(metricValue), nil } func (c *InferenceHPAController) getGPUUtilizationMetric( ctx context.Context, hpa *autoscalingv2.HorizontalPodAutoscaler, ) (float64, error) { selector, _ : labels.Parse(hpa.Spec.ScaleTargetRef.Name) metricValue, err : c.metricsClient.GetExternalMetric( ctx, gpu_utilization, metav1.NamespaceDefault, selector, ) if err ! nil { return 0, err } return metricValue / 100.0, nil } // 模型预热扩容时等待新 Pod 模型加载完成 func (c *InferenceHPAController) WarmupAndWait( ctx context.Context, pod *v1.Pod, timeout time.Duration, ) error { deadline : time.Now().Add(timeout) for time.Now().Before(deadline) { // 检查 Pod 的就绪探针 ready : c.isPodReady(ctx, pod) if ready { return nil } select { case -ctx.Done(): return ctx.Err() case -time.After(5 * time.Second): continue } } return fmt.Errorf(Pod %s 模型预热超时, pod.Name) } func (c *InferenceHPAController) isPodReady(ctx context.Context, pod *v1.Pod) bool { for _, condition : range pod.Status.Conditions { if condition.Type v1.PodReady condition.Status v1.ConditionTrue { return true } } return false }四、边界分析与架构权衡GPU 共享的安全隔离时间片共享模式下多个推理服务在同一 GPU 上交替执行。一个服务的推理延迟会受到其他服务的影响无法提供稳定的 SLA。MPS 模式虽然支持并行执行但缺乏显存隔离——一个服务的显存泄漏会影响同一 GPU 上的所有服务。对延迟敏感的核心服务应使用独占模式对延迟容忍的批处理服务可使用共享模式。模型缓存的存储成本将模型缓存到节点本地磁盘可以加速冷启动但每个节点的缓存容量有限。一个 70B 模型的权重文件约 140GB如果集群有 10 个 GPU 节点全量缓存需要 1.4TB 存储。需要根据模型使用频率选择性缓存低频模型按需加载。扩容预热的流量损失扩容时新 Pod 需要预热模型这段时间内增加的流量仍由现有 Pod 承担。如果流量增长过快预热期间的排队超时不可避免。解决方案是预测性扩容——基于历史流量模式提前扩容而非等待队列深度超阈值后被动扩容。缩容的模型缓存浪费缩容终止 Pod 后节点上的模型缓存可能被清理。如果短时间内再次扩容又需要重新加载模型。可以通过延迟清理缓存和缩容冷却期来缓解但增加了资源占用时间。五、总结AI 推理服务的 K8s 调度需要在资源利用率和服务稳定性之间找到平衡。核心策略包括基于模型缓存的调度优化减少冷启动时间GPU 独占/共享/MPS 三种模式适配不同优先级的服务基于队列深度的弹性伸缩配合模型预热避免流量损失。落地建议核心推理服务使用 GPU 独占模式保证 SLA批处理服务使用共享模式提升利用率将模型缓存到节点本地磁盘加速冷启动但需管理缓存容量实现预测性扩容减少被动扩容的延迟缩容时设置冷却期避免频繁扩缩容导致的模型重复加载。