Kube-Scheduler 扩展:自定义 GPU 调度插件实现业务感知

发布时间:2026/10/31 15:35:11

Kube-Scheduler 扩展:自定义 GPU 调度插件实现业务感知 Kube-Scheduler 扩展自定义 GPU 调度插件实现业务感知一、默认调度器不知道你的 GPU 在干什么Kubernetes 默认调度器对 GPU 的理解停留在有没有卡、几块卡的层面。Node 上有nvidia.com/gpu: 8Pod 请求nvidia.com/gpu: 1调度器找到一个满足资源需求的节点就算完成。但在实际业务中有 GPU和能用 GPU之间有巨大的鸿沟。同一个节点上的 8 张 A100可能其中 3 张正在跑模型训练持续高负载2 张在跑在线推理延迟敏感剩下 3 张空闲。默认调度器只知道有 3 张可用但不知道这 3 张是 NVLink 拓扑最优的、还是已经产生了大量 GPU 内存碎片的。真实的痛点包括跨 NUMA 节点分配 GPU 导致 PCIe 带宽瓶颈推理和训练混合部署时的显存碎片化某张卡温度过高需要主动避让同一业务的多张卡需要 NVLink 直连以降低通信开销。这些问题默认调度器一个也解决不了。二、Kube-Scheduler 插件框架与 GPU 拓扑感知调度Kubernetes 1.19 引入的调度框架Scheduling Framework允许在调度的多个阶段插入自定义逻辑。GPU 调度插件主要在 Filter、Score 和 Reserve 三个阶段发挥作用。Filter 阶段不只是检查 GPU 数量。插件从 Prometheus 拉取实时指标GPU 利用率、显存使用率、温度过滤掉超温或显存碎片过高的节点。同时检查 NUMA 亲和性——如果 Pod 请求 4 张 GPU只保留 4 张卡在同一 NUMA 节点上的候选。Score 阶段不是简单的空闲越多分越高。评分策略包含多个维度显存连续性优先选碎片少的、NVLink 直连跳数优先选拓扑最近的、负载均衡优先选已有业务少的、同业务亲和同一模型的 Pod 尽量调度到同一节点利用 NVLink。Reserve 阶段预留具体的 GPU 索引如GPU-0,GPU-1,GPU-3,GPU-4而不是简单标记已占用 4 张。这避免了两个 Pod 同时调度时争抢同一张卡的问题。三、GPU 感知调度插件实现以下是基于调度框架的 GPU Filter Score 插件核心代码package gpuscheduler import ( context fmt sync time v1 k8s.io/api/core/v1 k8s.io/apimachinery/pkg/runtime k8s.io/kubernetes/pkg/scheduler/framework k8s.io/kubernetes/pkg/scheduler/framework/plugins/names ) const ( Name GPUTopology gpuKey nvidia.com/gpu // 避免过热的温度阈值 tempThreshold 80.0 // 显存碎片阈值剩余显存 / 总显存 fragThreshold 0.3 ) // GPUTopologyPlugin 拓扑感知的 GPU 调度插件 type GPUTopologyPlugin struct { handle framework.Handle gpuState *GPUStateCache cacheTTL time.Duration mu sync.RWMutex } type GPUStateCache struct { mu sync.RWMutex nodes map[string]*NodeGPUInfo lastSync time.Time } type NodeGPUInfo struct { TotalGPUs int FreeGPUs []int // 空闲 GPU 索引列表 UsedGPUs map[int]string // GPU索引 - Pod名称 Temperature map[int]float64 MemoryFree map[int]uint64 // 剩余显存MiB MemoryTotal map[int]uint64 NVLinkTopo [][]int // NVLink 连通矩阵 NUMA map[int]int // GPU索引 - NUMA节点 PodCount int } var _ framework.FilterPlugin GPUTopologyPlugin{} var _ framework.ScorePlugin GPUTopologyPlugin{} var _ framework.ReservePlugin GPUTopologyPlugin{} // Name 返回插件名称 func (p *GPUTopologyPlugin) Name() string { return Name } // Filter 过滤不满足 GPU 调度条件的节点 func (p *GPUTopologyPlugin) Filter( ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo, ) *framework.Status { gpuRequested : getGPURequest(pod) if gpuRequested 0 { return nil // 非 GPU Pod放行 } node : nodeInfo.Node() if node nil { return framework.NewStatus(framework.Unschedulable, node not found) } nodeName : node.Name info, err : p.getNodeGPUInfo(nodeName) if err ! nil { return framework.NewStatus(framework.UnschedulableAndUnresolvable, fmt.Sprintf(failed to get GPU info for node %s: %v, nodeName, err)) } // 检查1温度阈值 — 有高温卡则排除当前节点 for gpuIdx, temp : range info.Temperature { if temp tempThreshold { return framework.NewStatus(framework.Unschedulable, fmt.Sprintf(GPU-%d on node %s temperature %.1f exceeds threshold %.1f, gpuIdx, nodeName, temp, tempThreshold)) } } // 检查2显存碎片 — 剩余显存过低的不算可用 freeCount : 0 for _, gpuIdx : range info.FreeGPUs { memFree : info.MemoryFree[gpuIdx] memTotal : info.MemoryTotal[gpuIdx] if memTotal 0 float64(memFree)/float64(memTotal) fragThreshold { continue // 显存碎片严重不算可用 } freeCount } if freeCount gpuRequested { return framework.NewStatus(framework.Unschedulable, fmt.Sprintf(node %s has %d usable GPUs (excluding fragmented), need %d, nodeName, freeCount, gpuRequested)) } // 检查3NUMA 亲和性 — 多卡请求需要同一 NUMA 节点 if gpuRequested 1 { if !hasNUMAAffinity(info, gpuRequested) { return framework.NewStatus(framework.Unschedulable, fmt.Sprintf(node %s cannot allocate %d GPUs in same NUMA node, nodeName, gpuRequested)) } } return nil } // Score 对通过 Filter 的节点打分 func (p *GPUTopologyPlugin) Score( ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string, ) (int64, *framework.Status) { gpuRequested : getGPURequest(pod) if gpuRequested 0 { return 0, nil } info, err : p.getNodeGPUInfo(nodeName) if err ! nil { return 0, framework.NewStatus(framework.Error, err.Error()) } score : int64(0) // 维度1显存连续性可用 GPU 的显存剩余率均值 var totalFragScore float64 count : 0 for _, gpuIdx : range info.FreeGPUs { if info.MemoryTotal[gpuIdx] 0 { totalFragScore float64(info.MemoryFree[gpuIdx]) / float64(info.MemoryTotal[gpuIdx]) count } } if count 0 { score int64((totalFragScore / float64(count)) * 40) // 40 分权重 } // 维度2业务亲和性 — 同 Deployment 的 Pod 已在节点上运行时加分 if deploymentName, ok : pod.Labels[app]; ok { for _, usedBy : range info.UsedGPUs { if usedBy deploymentName { score 30 // 30 分权重 break } } } // 维度3负载均衡 — 当前 Pod 数越少分越高 maxPodFraction : 50.0 currentPods : float64(info.PodCount) if cap : float64(info.TotalGPUs); cap 0 { score int64((1.0 - currentPods/cap) * maxPodFraction) } return score, nil } // ScoreExtensions 返回评分归一化范围 func (p *GPUTopologyPlugin) ScoreExtensions() framework.ScoreExtensions { return p } func (p *GPUTopologyPlugin) NormalizeScore( _ context.Context, _ *framework.CycleState, _ *v1.Pod, scores framework.NodeScoreList, ) *framework.Status { // 不做归一化分数差异本身就反映了节点优劣 return nil } // Reserve 预留具体 GPU 索引 func (p *GPUTopologyPlugin) Reserve( ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string, ) *framework.Status { gpuRequested : getGPURequest(pod) if gpuRequested 0 { return nil } info, err : p.getNodeGPUInfo(nodeName) if err ! nil { return framework.NewStatus(framework.Error, err.Error()) } // 按显存剩余量和 NVLink 跳数选择最优 GPU 组合 selected, err : selectBestGPUs(info, gpuRequested) if err ! nil { return framework.NewStatus(framework.Unschedulable, err.Error()) } // 写入 CycleState 供后续 Bind 阶段使用 state.Write(reserved-gpus, ReservedGPU{ NodeName: nodeName, GPUs: selected, }) return nil } // Unreserve 调度失败时释放预留 func (p *GPUTopologyPlugin) Unreserve( ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string, ) { // 清理 Reserved 状态 state.Delete(reserved-gpus) } type ReservedGPU struct { NodeName string GPUs []int } // selectBestGPUs 从可用 GPU 中选择 NVLink 拓扑最优的组合 func selectBestGPUs(info *NodeGPUInfo, n int) ([]int, error) { if len(info.FreeGPUs) n { return nil, fmt.Errorf(only %d free GPUs, need %d, len(info.FreeGPUs), n) } // 贪心策略按显存剩余量排序取前 n 个 // 生产环境应结合 NVLink 连通矩阵做最小跳数选择 candidates : make([]int, len(info.FreeGPUs)) copy(candidates, info.FreeGPUs) // 按剩余显存降序排列 for i : 0; i len(candidates); i { for j : i 1; j len(candidates); j { if info.MemoryFree[candidates[i]] info.MemoryFree[candidates[j]] { candidates[i], candidates[j] candidates[j], candidates[i] } } } return candidates[:n], nil } func hasNUMAAffinity(info *NodeGPUInfo, required int) bool { numaCounts : make(map[int]int) for _, gpuIdx : range info.FreeGPUs { numa : info.NUMA[gpuIdx] memFree : info.MemoryFree[gpuIdx] memTotal : info.MemoryTotal[gpuIdx] if memTotal 0 float64(memFree)/float64(memTotal) fragThreshold { numaCounts[numa] } } for _, count : range numaCounts { if count required { return true } } return false } func getGPURequest(pod *v1.Pod) int { total : 0 for _, container : range pod.Spec.Containers { if qty, ok : container.Resources.Requests[gpuKey]; ok { total int(qty.Value()) } } return total } func (p *GPUTopologyPlugin) getNodeGPUInfo(nodeName string) (*NodeGPUInfo, error) { p.gpuState.mu.RLock() info, ok : p.gpuState.nodes[nodeName] p.gpuState.mu.RUnlock() if !ok || time.Since(p.gpuState.lastSync) p.cacheTTL { // 触发刷新 return p.refreshNodeGPUInfo(nodeName) } return info, nil } func (p *GPUTopologyPlugin) refreshNodeGPUInfo(nodeName string) (*NodeGPUInfo, error) { // 实际实现应从 DCGM Exporter / Prometheus 拉取数据 // 此处省略 Prometheus 查询和 NVLink 拓扑解析逻辑 return nil, fmt.Errorf(refresh not implemented) }调度器配置中的插件注册apiVersion: kubescheduler.config.k8s.io/v1 kind: KubeSchedulerConfiguration profiles: - schedulerName: gpu-topology-scheduler plugins: filter: enabled: - name: GPUTopology score: enabled: - name: GPUTopology reserve: enabled: - name: GPUTopology四、调度插件的边界与取舍Filter 开销每个 GPU 节点的 Filter 调用都需要查询外部数据源Prometheus/DCGM这会增加调度延迟。必须设置缓存 TTL建议 5-10 秒避免每次调度都发起网络请求。缓存的代价是可能基于过时数据做出决策需要在准确性和性能之间权衡。Score 策略复杂度评分维度越多越容易产生非预期的交互效应。比如负载均衡少 Pod 高分和业务亲和性同业务 Pod 高分在某些场景下会矛盾。建议针对业务特点固定 3-4 个评分维度过多维度反而降低可解释性。GPU 碎片化Reserve 阶段预留了具体 GPU 索引后如果有 Pod 被抢占或失败需要可靠的清理机制更新 GPU 状态。否则会出现幽灵占用——GPU 实际空闲但状态标记为已用。五、总结Kube-Scheduler 扩展框架为 GPU 调度提供了灵活的定制空间。Filter 阶段基于实时指标过滤不合规节点Score 阶段综合显存连续性、业务亲和性和负载均衡打分Reserve 阶段预留具体 GPU 索引避免竞态。实现过程中最大的挑战不是代码本身而是 GPU 状态数据的准确性和实时性——调度决策的质量取决于你对集群 GPU 状态的感知精度。
延伸阅读

更多相关文章

2026/10/24 4:56:05

Conclave:基于CRDT和WebRTC的实时协作编辑器完整指南

Conclave:基于CRDT和WebRTC的实时协作编辑器完整指南 【免费下载链接】conclave CRDT and WebRTC based real-time, peer-to-peer, collaborative text editor 项目地址: https://gitcode.com/gh_mirrors/co/conclave Conclave是一款开源的实时协作文本编辑器…

2026/10/25 19:13:56

Rust Rosetta Code算法解析:10个排序算法的Rust实现对比

Rust Rosetta Code算法解析:10个排序算法的Rust实现对比 【免费下载链接】rust-rosetta Implementing Rosetta Code problems in Rust. 项目地址: https://gitcode.com/gh_mirrors/ru/rust-rosetta 在计算机科学领域,排序算法是基础且核心的内容。…

2026/10/24 4:56:12

Wonka与TypeScript完美结合:类型安全的流处理最佳实践

Wonka与TypeScript完美结合:类型安全的流处理最佳实践 【免费下载链接】wonka 🎩 A tiny but capable push & pull stream library for TypeScript and Flow 项目地址: https://gitcode.com/gh_mirrors/wo/wonka Wonka 是一个轻量级但功能强大…

2026/10/31 15:34:26

DV-HOP节点定位精度优化:多通信半径与跳距加权

简介:面向无线传感器网络定位研究者的DV-Hop改进算法仿真材料,围绕多通信半径与跳距加权策略降低误差传播、提升节点定位精度,适合WSN课程设计、算法复现与参数对比等场景的高年级学生和开发人员参考。压缩包共5个文件,约66KB&…

2026/10/31 15:34:26

Calibre 3Dstack connect命令详解与IC验证实践

1. Calibre 3Dstack命令解析:connect功能深度剖析在IC验证领域,Calibre作为业界标准的物理验证工具,其3Dstack模块专门用于处理先进封装设计的复杂验证需求。今天我们要重点解析的是3Dstack模块中的connect命令——这个看似简单的指令在实际工…

2026/10/31 15:34:26

ponytail 轻量级任务编排与插件化扩展实战指南

1. 从“ponytail”这个标题说起:它到底是什么第一次看到“ponytail”这个词,很多人脑子里蹦出来的画面是扎在脑后的马尾辫。但在技术圈和工具链语境里,它早就不是发型那么简单了。我最早接触到这个词,是在一个前端工程化的讨论群里…

2026/10/31 15:34:26

ponytail 工具实战:轻量级信息聚合与快捷操作工作流搭建指南

1. 从“ponytail”这个标题说起:它到底是什么第一次看到“ponytail”这个词,很多人脑子里蹦出来的画面是扎起来的马尾辫。但在技术圈和效率工具圈里,ponytail 早就不是发型的意思了。它是一类轻量级信息聚合与快捷操作工具的代称,…

2026/10/31 15:34:26

Calibre 3Dstack工具链与connect命令在3D IC验证中的应用

1. Calibre 3Dstack工具链概述Calibre 3Dstack是Mentor Graphics(现为Siemens EDA)推出的针对3D IC设计的物理验证解决方案。作为Calibre工具家族的重要成员,它专门处理芯片堆叠设计中的复杂互连验证问题。在先进封装技术如HBM、Chiplet等架构…

2026/10/31 15:29:26

语音社交产品冷启动策略与内容增长引擎设计

1. 语音社交产品的冷启动困境与破局点2023年全球语音社交市场规模已达47.8亿美元,但新产品的90日留存率普遍低于15%。这个数据背后反映的是语音社交产品面临的典型增长困境:用户获取成本高、内容沉淀难、社交关系链脆弱。我在操盘多款语音社交产品时发现…

2026/10/30 20:55:10

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/30 20:55:10

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/30 20:55:09

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/31 0:02:41

猫狗目标检测数据集:VOC/COCO/YOLO三格式一体化实战包

简介:本资源是一套面向计算机视觉初学者与YOLO目标检测实践者的猫狗图像数据集及配套开发工具包,专为快速上手目标检测模型训练而设计。资源包含1000张真实场景高清猫狗图片,标注质量高,同时提供VOC(XML)、…

2026/10/31 0:02:41

HDU2017多校标程复现:算法竞赛工程化真题沙盒

简介:本资源为2017年HDU多校联合训练第一场官方配套资料,面向ACM程序设计竞赛备赛学生、算法爱好者及高校编程集训队成员,旨在提供权威解题参考与完整测试验证环境。压缩包共40个文件,含15份C标程(覆盖全部赛题&#x…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑