优化 GPU 效率
先量出资源池分配了多少、实际用了多少,再调整工作负载的资源配置、让负载排得更紧凑,并释放空闲节点
GPU 资源池的浪费主要有两种:工作负载申请的比实际用的多;已分配的资源分散在过多的节点上。本页先用 TensorFusion 已经采集的指标把这两种浪费量出来,再依次介绍四项对应的配置:工作负载自动伸缩、放置策略、节点合并和碎片整理。
前置条件
- 已安装 TensorFusion,至少有一个处于 Running 的
GPUPool,并且工作负载已经运行了一段时间,能看出用量规律。建议至少一天。 - 能访问 GreptimeDB,见查询指标和日志。
- 已打开 hypervisor 的 vector sidecar。实际用量(
tf_gpu_usage、tf_worker_usage)和工作负载自动伸缩都依赖它,见采集 hypervisor 指标和日志。 - 有权限修改
TensorFusionCluster和SchedulingConfigTemplate。
本页涉及的资源池配置都写在 TensorFusionCluster 的 spec.gpuPools[].specTemplate 下面。controller 会用这份模板覆盖派生出的 GPUPool。
步骤 1:测量分配量和使用量
按节点对比三个数:总量、分配出去多少、实际用了多少。
最近一天各节点的分配比例:
SELECT node, pool,
avg(allocated_tflops_percent) AS tflops_allocated_pct,
avg(allocated_vram_percent) AS vram_allocated_pct
FROM tf_node_metrics
WHERE greptime_timestamp >= now() - '1d'::INTERVAL
GROUP BY node, pool
ORDER BY tflops_allocated_pct;最近一天每张 GPU 的使用比例:
SELECT node, uuid,
avg(compute_percentage) AS compute_used_pct,
max(memory_percentage) AS vram_peak_pct
FROM tf_gpu_usage
WHERE greptime_timestamp >= now() - '1d'::INTERVAL
GROUP BY node, uuid
ORDER BY compute_used_pct;每个工作负载的申请量和使用量。分别执行两条查询,按 workload 对照:
SELECT workload,
avg(tflops_request) AS tflops_request,
avg(vram_bytes_request) / 1073741824 AS vram_request_gib
FROM tf_worker_resources
WHERE greptime_timestamp >= now() - '1d'::INTERVAL
GROUP BY workload;
SELECT workload,
avg(compute_tflops) AS tflops_avg,
max(compute_tflops) AS tflops_peak,
max(memory_bytes) / 1073741824 AS vram_peak_gib
FROM tf_worker_usage
WHERE greptime_timestamp >= now() - '1d'::INTERVAL
GROUP BY workload;结果这样解读:
| 现象 | 说明 | 对应步骤 |
|---|---|---|
| 分配比例高,使用比例低 | 工作负载申请的比需要的多 | 步骤 2 |
| 节点上每张 GPU 都有一点负载,大的请求哪里都放不下 | 负载铺得太散 | 步骤 3 |
| 有的节点完全没有分配 | 这些节点可以释放 | 步骤 4 |
| 很多节点都只分配了一点 | 容量在节点之间碎片化了 | 步骤 5 |
不想写 SQL 的话,kubectl get gpunode 和 kubectl get gpu 也能看到 TFLOPS 和显存的总量与可用量。内置告警 IdleGPU 和 EmptyGPU 会持续检查同样的情况,见配置告警。
步骤 2:调整工作负载的请求值
调度器按工作负载的请求值预留资源。请求值高于实际用量,多出来的部分就白白占着。可以根据步骤 1 的数字手动调低,也可以交给自动伸缩,让请求值跟着实际用量走。在 Pod 模板上加两条注解即可开启:
metadata:
annotations:
tensor-fusion.ai/autoscale: "true"
tensor-fusion.ai/autoscale-target: all # all、compute 或 vram之后 controller 会根据工作负载的历史用量算出推荐值,并应用到各个 worker。这需要 controller 带 -enable-auto-scale 启动参数,chart 默认已经加上。查看推荐值:
kubectl get tensorfusionworkload <workload> -n <namespace> -o jsonpath='{.status.recommendation}'百分位、上下界、定时伸缩和资源池级规则等配置,见配置自动扩缩容。
步骤 3:选择放置策略
放置策略决定新的工作负载落到哪张 GPU 上,在资源池引用的 SchedulingConfigTemplate 的 spec.placement.mode 里设置。
| 策略 | 行为 | 适用情况 |
|---|---|---|
NodeCompactGPULowLoad(默认) | 节点一个一个填满,节点内部把负载分散到各张 GPU | 想少用节点,又不想让单张 GPU 太挤 |
CompactFirst | 在放得下的 GPU 里选剩余容量最少的 | 成本优先,并且需要留出整张空闲 GPU 给大请求 |
LowLoadFirst | 选剩余容量最多的 GPU | 性能隔离比节点数量更重要 |
修改 chart 创建的模板:
kubectl patch schedulingconfigtemplate tf-scheduling-config --type=merge \
-p '{"spec":{"placement":{"mode":"CompactFirst"}}}'新策略只对之后调度的工作负载生效。运行中的工作负载留在原地,直到重启或被碎片整理挪走。
步骤 4:释放空闲节点
节点合并按固定周期检查资源池,把没有运行任何 TensorFusion 工作负载的 GPU 节点释放掉,前提是释放之后资源池的容量仍然够用。
capacityConfig:
minResources:
tflops: "20"
vram: 10Gi
warmResources:
tflops: "10"
vram: 5Gi
nodeManagerConfig:
nodeCompaction:
period: 30m| 配置 | 含义 |
|---|---|
nodeCompaction.period | 检查周期。chart 设置的是 30m |
capacityConfig.warmResources | 释放节点之后,资源池里必须保留的空闲容量 |
capacityConfig.minResources | 释放节点之后,资源池里必须保留的总容量 |
同时满足下面几个条件,节点才会被释放:节点上没有 TensorFusion worker;对应的 GPUNode 处于 Running 且创建超过 5 分钟;没有 tensor-fusion.ai/do-not-disrupt: "true" 标签;释放后不违反 warmResources 和 minResources。
“释放”具体做什么,取决于资源池的 provisioningMode:
| 模式 | 节点合并做的事 |
|---|---|
AutoSelect | 给 Kubernetes Node 打上 tensor-fusion.ai/should-delete=true 标签。TensorFusion 不会删除节点,要由节点自动伸缩组件来删,例如 Karpenter 的 consolidation |
Provisioned、Karpenter | 删除 TensorFusion 为该节点创建的 GPUNodeClaim,节点随之销毁。不是 TensorFusion 创建的节点会被跳过 |
不想等下一个周期,可以给资源池打一条值为当前时间的注解,立即触发一次检查。时间戳超过 3 秒 controller 就不认:
kubectl annotate gpupool <pool> --overwrite \
tensor-fusion.ai/manual-compaction="$(date -u +%Y-%m-%dT%H:%M:%SZ)"AutoSelect 模式的资源池可以这样查看哪些节点被标记了:
kubectl get nodes -l tensor-fusion.ai/should-delete=true步骤 5:整理低利用率节点
节点合并只能释放已经空了的节点,把节点腾空要靠碎片整理:它按 cron 定时,把在用 GPU 较少的节点上的 TensorFusion worker 驱逐掉,让调度器把它们放到更满的节点上,腾空的节点再交给节点合并释放。碎片整理默认关闭。
碎片整理会驱逐 Pod。被驱逐的工作负载在别的节点上重启,并重新加载模型,这不是热迁移。请安排在业务低峰期执行。
-
给每个允许被挪动的工作负载加上
PodDisruptionBudget。节点上只要有一个 TensorFusion worker Pod 没有被覆盖,这个节点就会被跳过。 -
在资源池上开启碎片整理。
timezone必填:nodeManagerConfig: nodeCompaction: period: 30m defrag: enabled: true schedule: "0 3 * * *" timezone: "UTC" maxDuration: 2h utilizationThresholdPercent: 40字段 默认值 含义 enabledfalse是否开启碎片整理 schedule0 3 * * *5 段 cron 表达式,不要加 TZ=前缀timezone无,必填 计算 schedule 用的 IANA 时区,例如 UTC、Asia/ShanghaimaxDuration2h单次执行的最长时间。创建时间短于这个值的 worker 也不会被驱逐 utilizationThresholdPercent40节点上至少承载一个 worker 的 GPU 占比不超过这个值时,节点成为候选。完全空闲的节点留给节点合并处理 -
到了设定时间之后,在资源池的事件里查看执行情况:
kubectl describe gpupool <pool>DefragStarted、DefragPodEvicted、DefragFinished表示一次正常的执行;DefragSkipMissingPDB会指出哪个 Pod 没有PodDisruptionBudget。
碎片整理的策略比较保守:一次只驱逐一个 Pod,并且先模拟调度,确认节点上所有 worker 都能放进利用率不低于当前节点的其他节点才动手。被设为不可调度的节点、带 tensor-fusion.ai/do-not-disrupt: "true" 标签的节点,以及运行着不经 TensorFusion 使用 GPU 的 Pod 的节点,都会被跳过。
验证
至少等一个节点合并周期;开启了碎片整理的话,再等它执行一次。然后重新执行步骤 1 的查询,对比前后变化:
- 留下来的节点分配比例更高了。
- 节点数量减少了:
kubectl get gpunode。 WorkerTFlopsThrottled告警没有开始触发。如果触发了,说明这些工作负载的请求值或限制值被调得太低。
限制
- 节点合并和碎片整理都不会挪动运行中的进程,靠的是 Pod 在别处重启。见 GPU 热迁移。
AutoSelect模式下 TensorFusion 只给节点打标签。没有节点自动伸缩组件来删除节点的话,什么都不会释放。SchedulingConfigTemplate里有一个reBalancer配置块,controller 目前不处理它。- chart 没有放置策略和碎片整理对应的 value。每次
helm upgrade之后请确认这两项配置还在。
TensorFusion 文档