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

发布时间:2026/9/10 20:47:42

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/9/10 20:47:29

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/9/9 6:06:40

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/9/10 6:41:50

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/9/10 20:44:19

CANN/GE模型描述获取API

aclmdlGetDescFromFile 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、Ten…

2026/9/10 20:44:19

cann/ge 获取数据集缓冲区API

aclmdlGetDatasetBuffer 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、Te…

2026/9/10 20:39:18

Python实现轻量级日志监控与警报系统

1. 项目概述:日志监控与警报系统的核心价值日志监控是系统运维中最基础也最重要的环节之一。记得去年我们团队就遇到过一次线上事故——某核心服务突然崩溃,但由于缺乏实时日志监控,直到用户投诉才发现问题,整整耽误了40分钟。这件…

2026/9/10 16:39:38

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/10 11:16:38

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/10 0:00:55

目录对比去重实战:用哈希算法精准清理重复文件

我电脑里现在还有一块换了三次机的“数据墓地”硬盘,里面存着2016年以前所有旧笔记本的完整备份。平时不觉得有什么,直到前阵子想把它整理归档,发现同一个安装包、同一批照片、同一份论文草稿,在几个不同的备份目录里反复出现。更…

2026/9/10 0:00:55

Leaflet离线地图完整Demo合集:内网部署与坐标纠偏实战

简介:这是一份面向Web GIS开发者的LeafLet离线地图示例合集,帮助开发者快速掌握离线地图从搭建到交互的完整流程。压缩包共723个文件,大小14.06MB,以319个js脚本、175个html页面和29个css样式文件为主体,配合png/svg图…

2026/9/10 0:00:55

MATLAB读取Rinex 3.02观测文件:多系统GNSS数据解析实战

简介:基于MATLAB开发的Rinex3.02版观测文件(o文件)读取代码包,面向卫星定位导航方向的学习者与研究人员,用于解决新版观测文件的数据解析、历元提取与时间转换问题。压缩包共4个文件,包含两个m脚本、一个19…

2026/9/10 12:32:02

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/10 15:19:50

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/10 15:49:53

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

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

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

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