把一座 GPU 集群想象成一家高级餐厅的后厨:每一张 GPU 卡就是一张料理台,上面有固定的“灶口”(算力)和“冷藏格”(显存);Kubernetes 就是那位眼观六路的大堂经理,负责把一桌桌客人(AI 训练任务)安排到合适的料理台前。可是后厨经常出现这样的怪象:有的料理台被一桌客人独占却只用了半个灶口,有的客人在门口排起长队,还有的包间明明空着,经理却不知道它能不能坐人、能不能拆成小桌。这篇文章要解决的,正是“如何把这个后厨管理得又快又满”:GPU 资源怎么声明、配额怎么控制、设备插件如何上报“哪张桌子能用”、MIG 怎样把大宴会厅切成雅间、拓扑感知调度怎么让传菜更近、GPU 共享如何拼桌错峰,以及跨云迁移时如何给“分店”做体检。掌握了这套调度逻辑,你就能让宝贵的 GPU 不再闲置、排队不再漫长。

岗位关联

学会这套调度知识,你将成为 AI 团队里能真正“榨干”GPU 资源的人——同样的预算跑更多的训练任务,这是平台工程师最值钱的能力之一。

目录
  1. 可视化演示
  2. GPU资源抽象:从“料理台”到“资源字段”
  3. 配额与限制:餐厅的包场额度
  4. 设备插件机制:点单登记员
  5. MIG灵活切片:把宴会厅隔成雅间
  6. 拓扑感知调度:让客人靠近传菜口
  7. 共享与超卖:拼桌与错峰用餐
  8. 多云迁移实践:去另一个城市开分店
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示一致性哈希在分布式系统中的工作原理:节点和键映射到哈希环上,每个键分配给顺时针方向最近的节点,支持动态增删节点观察数据迁移。

本节配有交互动效(进入视口后自动加载,约 3KB)。

一致性哈希环 节点和键均匀分布在0-2π的哈希环上 每个键分配给顺时针方向最近的节点 hash(key) → ring position → nearest node clockwise 增删节点只影响相邻区间的数据迁移

GPU资源抽象:从“料理台”到“资源字段”

餐厅后厨里,一张料理台本来就是一个整体:四个灶口、一个大冷藏柜,厨师来了就用,用完就走。但大堂经理要排班,就必须把“这张台能干什么”翻译成一句话——“能同时开四个火、冷藏 40 公斤食材”。在 Kubernetes 世界里,GPU 卡就是那张料理台,经理需要一个统一的“资源字段”来登记它:nvidia.com/gpu。

Kubernetes 本身不认识 GPU,它只把 GPU 当作一种“扩展资源”(Extended Resource)。Pod 在声明里写上 nvidia.com/gpu: 1,就等于告诉调度器:“我要订一张完整的料理台。”于是调度器在匹配节点时,会检查这台“餐厅分店”里还有几张台子可用。和 CPU、内存不同,GPU 的申请量必须是整数,你不能说“我要 0.5 张卡”——就像经理不能把一张料理台劈成两半给两位厨师用。

调度器做选择时,遵循一个朴素的账本逻辑:一张台子被订出去后,可分配数就减一,直到减到零为止。这个“减法账本”是理解一切 GPU 调度问题的起点。

回到餐厅的比喻:这一节讲的是“如何给料理台建立台账”。没有台账,经理不知道有几张台子、每张台子被谁占着;有了 nvidia.com/gpu 这个字段,Node 上每一张 GPU 卡都变得可数、可查、可分配——这是后面所有调度策略的地基。

AllocatableGPU=CapacityGPU−∑i=1nRequesti\text{Allocatable}_{\text{GPU}} = \text{Capacity}_{\text{GPU}} - \sum_{i=1}^{n} \text{Request}_{i}
节点当前可分配的 GPU 卡数 = 物理卡总数 − 所有已调度 Pod 的申请卡数之和。结果必须大于等于零,否则 Pod 会排队等待。
  1. Capacity(物理总量)节点上插着的 GPU 卡总数,比如一台机器有 8 张 A100,Capacity 就是 8,这是账本的“期初余额”。
    CapacityGPU=8\text{Capacity}_{\text{GPU}} = 8
  2. Request(单任务申请量)每个 Pod 在 resources.requests 里声明的 nvidia.com/gpu 数值,必须是 0、1、2 这样的整数,代表订走几张完整的卡。
    Requesti∈{0,1,2,… }\text{Request}_{i} \in \{0, 1, 2, \dots\}
  3. 求和(已订出的台数)把该节点上所有已调度 Pod 的申请量累加,得到已经“订出去”的料理台总量。
    ∑i=1nRequesti\sum_{i=1}^{n} \text{Request}_{i}
  4. Allocatable(可分配余量)物理总量减去已订出量,就是还能再接多少任务;如果结果为负,调度器就拒绝把 Pod 放到这个节点上,客人只能继续排队。
    AllocatableGPU≥0\text{Allocatable}_{\text{GPU}} \geq 0

配额与限制:餐厅的包场额度

高级餐厅常接待团体客户:某个公司年会包下整个大厅,但预算只够 20 桌。经理必须立规矩——大厅最多同时开 20 桌,超过的预约一律不接。Kubernetes 里的命名空间(Namespace)就是这座“大厅”,而 ResourceQuota 就是那块“包场额度”的告示牌。

在多人共用的 AI 集群里,最怕的不是资源不够,而是有人“占着茅坑不拉屎”:一个团队把 8 张卡全部申请走,却只跑了一个单卡就能训练的实验。ResourceQuota 可以限定某个命名空间最多能申请多少张 GPU,超出的 Pod 创建请求会被 API Server 直接拒绝。配合 LimitRange,还能规定每个 Pod 最少申请多少、最多申请多少——相当于给每桌设了“最低消费”和“最高预算”。

配额的本质是一个“封顶校验”:命名空间里所有 Pod 的申请量之和,必须小于等于配额上限。它不关心 GPU 用没用满,只关心“订出去多少”,这既是它的简单之处,也是它防止资源被滥占的关键。

这节的管理哲学,和餐厅经理拒绝超订的团体如出一辙:先定上限,再守红线。配额的真正价值不是提高利用率,而是防止个别团队滥占资源、挤压他人,让整座“大厅”的 GPU 分配公平有序。

Freequota=Quotans−∑p∈nsRequestp且Freequota≥0\text{Free}_{\text{quota}} = \text{Quota}_{\text{ns}} - \sum_{p \in \text{ns}} \text{Request}_{p} \quad \text{且} \quad \text{Free}_{\text{quota}} \geq 0
命名空间剩余配额 = 配额上限 − 该命名空间所有 Pod 的 GPU 申请之和。一旦剩余配额为负,新的 Pod 创建请求就会被拒绝。
  1. Quota(配额上限)管理员为命名空间设定的 GPU 申请上限,比如“最多 20 张卡”,这就是大厅的包场额度。
    Quotans=20\text{Quota}_{\text{ns}} = 20
  2. 求和(已有订桌量)累加该命名空间下所有 Pod 的 GPU 请求,无论是运行中还是等待中的,都算作“已订出的桌数”。
    ∑p∈nsRequestp\sum_{p \in \text{ns}} \text{Request}_{p}
  3. 校验(拒绝规则)计算剩余额度,若小于零,API Server 返回配额不足的错误,新来的客人连号都不取,直接请回。
    Freequota≥0\text{Free}_{\text{quota}} \geq 0

设备插件机制:点单登记员

客人坐下后,服务员要能随时回答:“3 号桌还能加人吗?”在 GPU 集群里,回答这个问题的不是调度器,而是一个叫 Device Plugin 的组件。它像一位专职“点单登记员”:每隔一段时间清点后厨的料理台——哪张能用、哪张在维修、一共几张,然后把台账报给 Kubelet。

这位登记员的工作有三步。第一步,注册:插件启动后向 Kubelet 注册自己管理的资源名,比如 nvidia.com/gpu,相当于在菜单上写下“本店供应 GPU 料理台”。第二步,上报:插件定期清点健康设备的数量,通过 ListAndWatch 接口告诉 Kubelet 当前有几张台子可用。第三步,分配与注入:当调度器决定把 Pod 放到某节点后,Kubelet 会请插件“安排一张具体的台子”,插件返回设备编号,Kubelet 再把设备文件(如 /dev/nvidia0)和对应的驱动库挂载进容器。

没有 Device Plugin,会发生什么?驱动装好了、卡也插上了,但 Kubelet 根本不知道“这家店有几张台子”,于是节点上报的 nvidia.com/gpu 永远是 0,调度器只能把任务拒之门外。插件上报的数量并非简单等于物理卡数,而是只统计健康的设备。

这位“登记员”的价值在于:它把“物理设备是否存在”翻译成了“调度器能读懂的资源数字”。没有它,GPU 就像后厨里一堆没写进菜单的食材——再新鲜,客人也点不了。

Cgpu=∑d=1NI(Health(d)=1)C_{\text{gpu}} = \sum_{d=1}^{N} \mathbb{I}\left(\text{Health}(d) = 1\right)
节点对外可声明的 GPU 数量 = 对每一张物理卡做健康检查,健康记 1、不健康记 0,然后求和。只有健康的卡才会被登记员写进台账。
  1. N(物理设备总数)节点上安装的 GPU 卡总数,比如 8 张,这是清点的起点。
    N=8N = 8
  2. 指示函数(健康打分)对第 d 张卡检查驱动与设备状态,健康则取 1,异常则取 0,相当于登记员在台账上打勾或打叉。
    I(Health(d)=1)\mathbb{I}\left(\text{Health}(d) = 1\right)
  3. 求和(对外上报量)把所有健康标记累加,得到该节点可分配 GPU 总数,写入节点状态,供调度器读取。
    Cgpu=∑d=1NI(Health(d)=1)C_{\text{gpu}} = \sum_{d=1}^{N} \mathbb{I}\left(\text{Health}(d) = 1\right)

MIG灵活切片:把宴会厅隔成雅间

周末的宴会厅常常只坐两三桌,却占着整层楼;而门口还有一堆散客想用餐却订不到“小桌”。NVIDIA 的 MIG(Multi-Instance GPU)技术,就是给这张大宴会厅装上可移动隔断:一张 A100 或 H100 可以被切成多个硬件隔离的小实例,每个实例拥有独立的显存、计算核心和缓存。

为什么要切?因为大模型训练固然需要整卡,但推理服务、数据预处理、小模型微调往往只需要一小块算力。如果都为它们分配整卡,大量算力就被白白浪费。MIG 把“一张大卡”变成“多个小卡”,每个实例在硬件层面互相隔离——一个实例崩溃或被恶意任务打满,不会影响邻居,这比单纯用软件共享安全得多。

MIG 的划分可不是随心所欲的“平均分”,而是遵循硬件预设的 Profile 模板。切分时所有实例的显存之和必须不超过物理显存,实例数量也不能超过该卡支持的上限。调度器配合 MIG 时,需要认清节点上“台子”已经变成了“小隔间”,资源字段也要随之变化。

MIG 就像把冷清的大宴会厅改造成若干热闹的小雅间:整卡利用率上去了,散客也有了自己的包间,而且隔音(隔离性)一流。它是“大模型吃饱、小任务吃好”的经典解法。

∑k=1mMemoryslice,k≤Memoryphysical\sum_{k=1}^{m} \text{Memory}_{\text{slice}, k} \leq \text{Memory}_{\text{physical}}
所有 MIG 实例的显存之和,必须小于等于物理显存总量。切分方案还要受硬件 Profile 限制:比如 A100 40GB 只能切成 1g.5gb、2g.10gb 等固定规格,不能任意取整。
  1. Memory(单个实例显存)每个 MIG 实例分到的显存大小,如 5GB 或 10GB,由 Profile 模板决定。
    Memoryslice,k=10GB\text{Memory}_{\text{slice}, k} = 10\text{GB}
  2. 累加(占用的总显存)把 m 个实例的显存全部加起来,得到这宴会厅被“隔断”后实际占用的总面积。
    ∑k=1mMemoryslice,k\sum_{k=1}^{m} \text{Memory}_{\text{slice}, k}
  3. 比较(物理约束)累加值不能超过物理显存总量,否则切分方案不成立;同时实例数 m 不能超过 Profile 表允许的最大个数。
    ∑k=1mMemoryslice,k≤Memoryphysical\sum_{k=1}^{m} \text{Memory}_{\text{slice}, k} \leq \text{Memory}_{\text{physical}}

拓扑感知调度:让客人靠近传菜口

一桌客人点了八道菜,如果传菜口离得远,每道菜都要绕大半个餐厅,上菜慢、客人烦。多卡训练任务更夸张:模型参数在卡与卡之间每步都要同步,卡与卡之间的“传菜通道”带宽直接决定训练速度。NVLink 是后厨的专用传菜电梯,带宽极高;PCIe 是普通走廊,慢一个数量级;跨节点通信则要走“外卖通道”,最慢。

拓扑感知调度要做的事,就是让“一桌客人”尽量坐在同一楼层、靠近同一传菜口:同一个 Pod 申请的多张 GPU,尽量落在同一节点、甚至是 NVLink 直连的几张卡上。调度器在做放置决策时,会把机架、节点、NUMA 节点、GPU 之间的连接关系纳入考量,而不是随便找几张空闲卡塞进去。

它还需要照顾 CPU 亲和性:GPU 要频繁读写内存,如果 GPU 和 CPU 不在同一个 NUMA 节点上,数据搬运要多绕路。这就像不仅要把客人安排在传菜口附近,还得保证传菜员走的是最短那条走廊。调度的目标函数,就是让通信代价最小。

在餐厅里,聪明的经理会预判“哪桌客人互动最频繁”,把他们安排在传菜口附近;拓扑感知调度就是这个预判能力。它不增加任何硬件成本,却能让多卡训练的通信时间大幅缩短——这是性价比极高的调优手段。

Costcomm=∑i<jDij×Hopij\text{Cost}_{\text{comm}} = \sum_{i < j} D_{ij} \times \text{Hop}_{ij}
多卡任务的通信总代价 = 每对卡之间需要交换的数据量 × 它们之间的通信跳数(拓扑距离)的乘积之和。调度器优先选择使该代价最小的卡组合。
  1. D(数据交换量)卡 i 与卡 j 之间每步训练需要交换的梯度数据量,单位通常为 MB/步,由模型并行策略决定。
    Dij=梯度数据量ijD_{ij} = \text{梯度数据量}_{ij}
  2. Hop(拓扑跳数)卡 i 到卡 j 的通信路径要经过的“中转次数”:NVLink 直连记 1 跳,过 PCIe 记 2 跳,跨节点则更多。
    Hopij∈{1,2,3,… }\text{Hop}_{ij} \in \{1, 2, 3, \dots\}
  3. 乘积累加(总代价)把通信频率高的卡对放在跳数更小的位置,总代价就低;调度器遍历可行放置方案,选代价最小的一种。
    Costcomm=∑i<jDij×Hopij\text{Cost}_{\text{comm}} = \sum_{i < j} D_{ij} \times \text{Hop}_{ij}

共享与超卖:拼桌与错峰用餐

高峰期的餐厅永远有人排队,可有些四人桌只坐了一位客人。与其让空位闲着,不如安排互不相识的散客拼桌——当然,经理要提前说清楚:拼桌的客人错峰用筷子(算力),但各自的食物(显存)放在自己的盘子里。这就是 GPU 共享与超卖的核心思路。

共享方案主要有两类。一类是时间切片:多个 Pod 轮流使用同一张卡的算力,每个 Pod 跑一小段时间再切换,显存则是大家共存。它的优点是配置简单、能跑多个任务;缺点是切换有开销,而且某个任务吃满算力时会挤压邻居。另一类是显存复用 + 动态共享:借助 NVIDIA MPS 或第三方共享调度器,多个 Pod 同时提交计算,GPU 按优先级动态分配算力。这就像拼桌的客人可以同时动筷,但各自守着自己的“盘子”(显存配额)。

共享必然伴随超卖(overcommitting)风险:物理只有 40GB 显存,你却答应了两个各要 30GB 的任务。所以 QoS 保障至关重要——需要为每个租户设定算力上限和显存上限,必要时做抢占,否则一个失控任务会让整张卡 OOM。衡量共享效果,看的是“有效利用率”。

拼桌是一门平衡艺术:太保守,空位浪费;太激进,客人互相干扰。GPU 共享调优的终极目标,是在“捡空闲”和“保稳定”之间找到那个让有效利用率最高的甜蜜点。

Utileff=∑iUsedcompute,iCapacitytotal\text{Util}_{\text{eff}} = \frac{\sum_{i} \text{Used}_{\text{compute}, i}}{\text{Capacity}_{\text{total}}}
GPU 有效利用率 = 所有租户实际使用的算力量之和 ÷ GPU 总算力。共享的意义不是让单个任务更快,而是让整张卡的“空闲算力”被捡起来用。
  1. Used(单租户用量)每个 Pod 实际消耗的算力,时间切片模式下它随时间周期性波动,取一段时间内的平均值。
    Usedcompute,i=算力占用‾i\text{Used}_{\text{compute}, i} = \overline{\text{算力占用}}_i
  2. 求和(总消耗)把所有拼桌客人的实际用量相加,得到这张卡被“真正用起来”的算力总量。
    ∑iUsedcompute,i\sum_{i} \text{Used}_{\text{compute}, i}
  3. 求比(有效利用率)除以物理总算力,得到有效利用率。它越接近 100%,说明“占着不用”的浪费越少,调度策略越成功。
    Utileff=∑iUsediCapacitytotal\text{Util}_{\text{eff}} = \frac{\sum_{i} \text{Used}_{i}}{\text{Capacity}_{\text{total}}}

多云迁移实践:去另一个城市开分店

连锁餐厅开到新城市,不能把老店的厨房图纸照搬——当地的电压、燃气规格、消防标准可能全不一样。GPU 工作负载迁移到另一朵云,同样不是复制粘贴 YAML 就完事:不同云厂商的 GPU 实例类型、驱动版本、Device Plugin 实现、资源字段甚至调度器扩展都可能有差异。

迁移前要做一份“体检清单”。第一项是驱动与 CUDA 兼容矩阵:老集群用的是 A100 + CUDA 11.8,新云厂商默认给的是 H20 和更新的驱动,容器里的 CUDA 版本能不能兼容?第二项是设备插件差异:有的云厂商自己实现了 Device Plugin,资源字段可能不再是标准的 nvidia.com/gpu,而是 vendor.com/gpu;还有的云通过虚拟化方案暴露 GPU,上报机制完全不同。第三项是实例规格与显存:同叫“8 卡节点”,显存是 40GB 还是 80GB,直接影响 Pod 的资源声明是否还能满足。

迁移评估可以量化为一个风险分:把每一项兼容性按“是否匹配”打分,再按影响权重加权求和。分数越高,说明迁移的坑越多,越要谨慎。执行时则应采用灰度策略:先迁一个非核心推理服务,验证一切正常,再批量迁移训练任务,最后切换流量。

开分店不可怕,可怕的是不看当地消防规范就装修。多云迁移的智慧在于:把“想当然”变成“清单化评估”,把“一刀切”变成“灰度渐进”,让 GPU 工作负载在任何一朵云上都能稳稳跑起来。

Riskmigrate=∑fwf×(1−Compatf)\text{Risk}_{\text{migrate}} = \sum_{f} w_{f} \times \left(1 - \text{Compat}_{f}\right)
迁移风险分 = 对体检清单每一项,用权重 ×(1 − 是否兼容)求和。完全兼容记 0,完全不兼容记 1 倍权重;风险分越低,迁移越安全。
  1. f(检查项)体检清单里的每一项,包括驱动版本、Device Plugin、资源字段、实例规格、存储挂载方式等。
    f∈{驱动,插件,字段,规格,… }f \in \{\text{驱动}, \text{插件}, \text{字段}, \text{规格}, \dots\}
  2. Compat(兼容标记)该项在新云环境完全兼容记 1,不兼容记 0;不兼容则 1 − Compat 为 1,表示“这里会踩坑”。
    Compatf∈{0,1}\text{Compat}_{f} \in \{0, 1\}
  3. 加权求和(风险量化)每项坑的严重程度乘权重,得“越大越危险”的风险分,用来决定是直接迁移还是先做适配改造。
    Riskmigrate=∑fwf×(1−Compatf)\text{Risk}_{\text{migrate}} = \sum_{f} w_{f} \times \left(1 - \text{Compat}_{f}\right)

AI 应用场景

大模型训练平台的资源池治理

阶段:模型训练基础设施搭建与资源配置

解决的问题:多个算法团队共用一个 GPU 集群时,常有人一次性申领大量卡却长期闲置,导致真正要训练的任务排不上队。通过 ResourceQuota 为每个团队设置 GPU 上限、用 LimitRange 约束单任务最小申请量,再配合拓扑感知调度把多卡任务就近放置,可以把集群利用率从 30% 提升到 70% 以上。

具体案例:某大模型公司训练平台有 200 张 A100,三个算法组共享。平台组为每个组设定了配额(如 A 组 80 卡),并规定单任务至少申请 4 卡。结果“占坑不训练”的现象大幅减少,训练任务平均排队时间从 2 小时降到 20 分钟。

推理服务的 GPU 共享部署

阶段:模型部署与在线推理服务资源规划

解决的问题:多个轻量级推理模型如果各自独占一张 A100,显存用不到 30%;用 MIG 把大卡切成多个小实例,或用时间切片让多个推理 Pod 共享一张卡,能把单位显存的吞吐量提高 3-5 倍。

具体案例:某智能客服团队有 5 个不同的 BERT 推理服务,每个请求量都有明显波峰波谷。平台用 MIG 将一张 A100 切成 3 个实例,分别部署不同模型,高峰期再配合 GPU 共享调度器动态分配算力,整卡利用率从 25% 提升到 80%。

跨云 GPU 容灾与迁移

阶段:多云基础设施管理与容灾切换

解决的问题:单一云厂商的 GPU 配额可能不够,或某地域实例价格暴涨。通过体检清单评估驱动、插件、字段兼容性,再用灰度策略把训练和推理任务平滑迁移到备选云,降低单一云依赖风险。

具体案例:某自动驾驶公司主力训练集群在云 A,为防资源挤兑,在云 B 预置了兼容环境。平台组编写了迁移检查脚本,先迁移 2 个数据预处理任务验证通过,再分三批迁移全部训练任务,全程业务无感知。

Python 演示

python56 行
class GpuNode:    def __init__(self, name, gpu_total, mem_total):        self.name = name        self.gpu_total = gpu_total        self.mem_total = mem_total        self.gpu_used = 0        self.mem_used = 0     def can_fit(self, need_gpu, need_mem):        return (self.gpu_used + need_gpu <= self.gpu_total and                self.mem_used + need_mem <= self.mem_total)     def allocate(self, need_gpu, need_mem):        self.gpu_used += need_gpu        self.mem_used += need_mem  def best_fit_schedule(pods, nodes):    result, pending = {}, []    for pod in sorted(pods, key=lambda p: p["gpu"], reverse=True):        best = None        for node in nodes:            if node.can_fit(pod["gpu"], pod["mem"]):                if best is None or node.gpu_used < best.gpu_used:                    best = node        if best is not None:            best.allocate(pod["gpu"], pod["mem"])            result[pod["name"]] = best.name        else:            pending.append(pod["name"])    return result, pending  def main():    nodes = [        GpuNode("node-a", 8, 640),        GpuNode("node-b", 8, 640),        GpuNode("node-c", 4, 320),    ]    pods = [        {"name": "llm-1", "gpu": 4, "mem": 320},        {"name": "llm-2", "gpu": 4, "mem": 320},        {"name": "vision-1", "gpu": 2, "mem": 160},        {"name": "vision-2", "gpu": 2, "mem": 160},        {"name": "infer-1", "gpu": 1, "mem": 80},        {"name": "infer-2", "gpu": 1, "mem": 80},    ]    placed, pending = best_fit_schedule(pods, nodes)    for name in placed:        print("{} -> {}".format(name, placed[name]))    if pending:        print("pending: " + ", ".join(pending))  if __name__ == "__main__":    main()
目的:模拟 K8s 调度器的“最佳适应”策略——让大任务优先、尽量挑剩余资源最富余的节点,把 6 个 GPU 训练/推理任务放进 3 个节点,并输出无法放置的排队任务。
  1. def can_fit(self, need_gpu, need_mem):检查节点剩余 GPU 和显存能否同时容纳新任务,相当于调度器的“这台料理台还坐不坐得下”。
  2. return (self.gpu_used + need_gpu <= self.gpu_total and ...必须 GPU 卡数和显存两个条件同时满足才算可放置,缺一不可。
  3. for pod in sorted(pods, key=lambda p: p["gpu"], reverse=True):把大任务排到前面先调度,避免大任务最后无处可放的“碎片化”。
  4. if best is None or node.gpu_used < best.gpu_used:在所有能放下的节点里选择当前占用率最低的一个,实现负载均衡。
  5. pending.append(pod["name"])没有节点能容纳的任务进入等待队列,模拟真实 K8s 里 Pod 的 Pending 状态。

常见误区

误区CPU 可以设置 `requests: 500m`(0.5 核),GPU 是不是也能写 `nvidia.com/gpu: 0.5`?这样一张卡就能给两个任务用,多省钱啊。

正解GPU 是“整张占用”的离散资源,nvidia.com/gpu 只接受非负整数。写 0.5 会被 API Server 直接拒绝。想一卡多用,要靠 MIG 切片或 GPU 共享方案,而不是在资源字段里写小数——就像餐厅经理不会把一张料理台从中间劈开,而是用隔断或排班来复用。

为何常见:人们习惯了 CPU 可压缩、可分片的弹性模型,想当然地把 GPU 也套进同一套资源体系,忽略了 GPU 独占设备文件的物理特性。

误区只要在节点上装好 NVIDIA 驱动,Kubernetes 就能“看见”GPU,Pod 申请 `nvidia.com/gpu` 就能被调度到这台机器。

正解驱动只是让操作系统能用 GPU,Kubernetes 的 Kubelet 并不知道设备存在。必须部署 Device Plugin,由它向 Kubelet 注册扩展资源、上报健康设备数、并在 Pod 启动时把设备注入容器。没有插件,节点上报的 GPU 数量永远是 0,任务只能永远 Pending。这就像后厨买了高级烤箱,但菜单上没写、服务员也不登记,客人自然点不到。

为何常见:人眼能看到“这台机器有 8 张卡”,于是默认系统也能看到;但 K8s 的资源模型完全依赖组件上报,看不到就是不存在。

误区MIG 就是把 80GB 显存平均切成 8 份、每份 10GB,想切几份切几份,甚至可以 3.5 份。

正解MIG 的划分受硬件 Profile 严格限制。比如 A100 40GB 只支持 1g.5gb、2g.10gb、3g.20gb 等固定模板,且所有实例的组合必须满足约束表;A100 80GB 的切法又不一样。切分前要查官方模板,否则配置会被拒绝。这就像宴会厅的隔断只能放在预设的滑轨上,不能随便拉一道墙。

为何常见:MIG 这个名字让人联想到“内存切片”,大家按内存平均分的直觉去理解,忽略了 GPU 计算核心、缓存也要一起分组的硬件约束。

误区既然都是 Kubernetes,把 Deployment、Pod 的 YAML 从 AWS 复制到阿里云,GPU 任务就能原样跑起来。

正解云厂商的 GPU 实例类型、驱动版本、Device Plugin 实现、资源字段命名都可能不同。比如有的云用 aliyun.com/gpu 而不是 nvidia.com/gpu,有的云用虚拟化 GPU 需要额外 scheduler-extender。不评估直接迁,轻则 Pod 调度失败,重则驱动不兼容导致容器崩溃。迁移前必须做兼容矩阵体检,再灰度切换。

为何常见:K8s 的“标准化”给人很强的安全感,让人忽略它只标准化了调度框架,而设备接入层(Device Plugin)和资源字段是各家自由实现的。

误区让多个 Pod 共享一张 GPU 是纯赚——利用率上去了,任务还都能跑,何乐而不为?

正解时间切片意味着算力分时复用,上下文切换有开销,且多个任务会互相争抢算力,一个任务跑满会让邻居明显变慢;显存虽共享但要防 OOM。生产环境必须配 QoS 限制和监控告警。共享是“捡回空闲”,不是“凭空变多”,它用稳定性换利用率。

为何常见:看到利用率数字大幅上涨就以为万事大吉,忽略了共享引入的干扰和超卖风险,等任务变慢、OOM 频发时才发现问题。

面试题

Q1. 在 Kubernetes 中,Pod 申领一块 GPU 使用的资源字段是?

单选题 · easy

  • A.cpu: "1" CPU 核数(9)
  • B.memory: "16Gi" 内存(10)
  • C.nvidia.com/gpu: 1 扩展资源(13)← 正确答案
  • D.gpu: "shared" 共享模式(11)

答案:C。GPU 在 K8s 中属于扩展资源,字段名形如 nvidia.com/gpu,由 Device Plugin 注册后再使用。A 是 CPU、B 是内存,都属于内置资源;D 不是合法字段。这就像点餐要报菜谱上真实存在的菜名,nvidia.com/gpu 就是 GPU 这道菜的官方菜名。

Q2. Device Plugin 在集群中的核心职责是?

单选题 · easy

  • A.负责 Pod 到节点的调度决策(12)
  • B.向 kubelet 上报并分配设备(12)← 正确答案
  • C.监控 Pod 的 CPU 使用率(11)
  • D.自动升级显卡驱动版本(11)

答案:B。Device Plugin 负责向 Kubelet 注册扩展资源、上报健康设备数量,并在 Pod 创建时分配具体设备注入容器。调度决策是 kube-scheduler 的事,CPU 监控是 metrics 系统的事,驱动升级也不归插件管。它好比点单登记员,只负责“记台账、派台子”。

Q3. 命名空间 GPU 配额用尽后,继续创建 Pod 会?

单选题 · mid

  • A.创建请求被 API 直接拒绝(12)← 正确答案
  • B.Pod 先运行再被强制杀死(12)
  • C.节点自动扩容 GPU 资源(12)
  • D.集群自动删除历史 Pod(12)

答案:A。ResourceQuota 的校验发生在准入阶段,请求超限时 API Server 直接返回 403 错误,Pod 根本不会创建,更谈不上运行、扩容或删除旧 Pod。就像餐厅包场额度满了,新客人连号都不取就被请回,而不是先坐进来再被赶走。

Q4. 提升集群 GPU 利用率的有效手段包括?

多选题 · mid

  • A.配额管理防止任务占坑(11)← 正确答案
  • B.MIG 切片切分空闲大卡(11)← 正确答案
  • C.拓扑就近放置多卡任务(11)← 正确答案
  • D.取消所有调度资源校验(11)

答案:ABC。配额防占坑、MIG 切分大卡碎片、拓扑感知降低通信开销,都是实打实的调优手段。取消资源校验会让任务随意放置、互相争抢,利用率只会更糟。调优的目标是“有秩序地挤”,不是“无秩序地乱”。

Q5. 多卡训练任务做拓扑感知调度,核心目标是?

单选题 · hard

  • A.让任务散落在多个宿主机(12)
  • B.减少跨卡通信路径代价(12)← 正确答案
  • C.优先抢占低负载机器(11)
  • D.增加 GPU 显存总容量(10)

答案:B。拓扑感知调度把多张卡放到 NVLink 直连、跳数最少的位置,从而降低梯度同步的通信代价。散落多宿主机会增加网络开销;抢占低负载机器和增加显存容量都不是拓扑调度的目标。它解决的是“传菜距离”问题,而不是“菜量”问题。

Q6. 相比整卡共享,MIG 切片最大的收益是?

单选题 · mid

  • A.显存容量成倍自动增长(11)
  • B.硬件级计算与显存隔离(11)← 正确答案
  • C.完全消除调度器开销(10)
  • D.无需安装任何 GPU 驱动(11)

答案:B。MIG 在硬件层面隔离计算单元、显存和缓存,一个实例故障或打满不会影响邻居,隔离性是它相对软件共享的最大优势。显存不会增长,调度开销依然存在,驱动也照样要装。它像加了硬隔断的雅间,比“软帘子”更安全。

Q7. 跨云迁移 GPU 工作负载前,需要重点评估?

多选题 · hard

  • A.驱动版本与 CUDA 兼容矩阵(12)← 正确答案
  • B.设备插件与资源字段差异(12)← 正确答案
  • C.实例规格与显存容量差异(12)← 正确答案
  • D.团队成员开发语言偏好(11)

答案:ABC。迁移的核心风险在于驱动/CUDA 兼容性、Device Plugin 实现和资源字段命名、以及实例规格显存是否满足原声明。开发语言偏好与技术无关,不在评估清单里。就像开分店要检查当地电压和燃气规范,而不是问厨师喜欢用哪口锅。

Q8. GPU 时间切片共享时,多个 Pod 同时运行会?

单选题 · easy

  • A.每份显存容量都会翻倍(11)
  • B.算力分时交替且共享显存(12)← 正确答案
  • C.网络带宽自动获得加成(11)
  • D.训练精度必然完全一致(11)

答案:B。时间切片让多个 Pod 轮流使用算力,显存则是同时共存,因此 B 正确。显存不会翻倍,网络带宽与共享无关,精度也可能因算力抢占而波动。它像拼桌的客人错峰动筷,但菜还是自己那份。

Q9. 关于 GPU 资源请求的正确表述是?

单选题 · mid

  • A.支持像 CPU 一样设置 0.5(12)
  • B.必须为整数且不支持超订(12)← 正确答案
  • C.可以只设 limits 不设请求(12)
  • D.与 CPU 一样可弹性伸缩(12)

答案:B。GPU 扩展资源必须是正整数,表示整卡占用,不支持小数请求,也不允许只设 limits 而省略 requests(扩展资源要求两者一致),更不能弹性伸缩。要用满一张卡,只能靠 MIG 或共享调度器在应用层解决。

Q10. 某团队把 8 卡 A100 节点全部用 MIG 切成最小实例,再叠加时间切片共享,可能产生的问题有?

多选题 · extreme

  • A.单实例显存过小导致训练 OOM(13)← 正确答案
  • B.过度切片使算力碎片化加剧(13)← 正确答案
  • C.共享叠加使切换开销增大(12)← 正确答案
  • D.合理方案完全无任何代价(12)

答案:ABC。最小 MIG 实例显存很小,大模型放不下会 OOM;切片过多把算力切碎,单实例算力不足;再叠加时间切片,切换开销进一步放大。D 明显错误,任何共享方案都有代价。调优要按任务实际需求选切片规格,而不是“越小越好”。