QoS 级别与计价
为工作负载指定 QoS 级别,并设置 TensorFusion 计算工作负载成本和 GPU 节点成本时使用的单价
TensorFusion 有四个 QoS 级别:low、medium、high、critical。工作负载的级别决定它的调度优先级,以及计算成本时使用的单价。本文介绍如何指定 QoS 级别、设置池的默认级别和单价、设置各 GPU 型号的每小时成本,以及如何查询成本数据。
这些成本数字用于内部成本分摊和展示。TensorFusion 只把它们记录为指标,不出账单,也不做预算管控。
前置条件
- 已安装 TensorFusion,且至少有一个 GPU 池处于
Running。参见 在 Kubernetes 安装。 - 有权限编辑
TensorFusionCluster资源。 - 查询成本数据需要能访问 GreptimeDB,参见查询指标和日志。
QoS 级别影响什么
| QoS | 什么情况下得到 | webhook 添加的 PriorityClass | 自动伸缩推荐的请求值 |
|---|---|---|---|
low | 显式指定 | 无 | 观测用量的下界 |
medium | 默认 | 无 | 观测用量的下界 |
high | 显式指定;或 TFLOPS、VRAM 的请求都等于限额且不为零时默认得到 | tensor-fusion-high(优先级 10000) | 观测用量的目标值 |
critical | 显式指定 | tensor-fusion-critical(优先级 100000) | 观测用量的上界 |
补充说明:
- 只有 Pod 自己没有设置
priorityClassName时,webhook 才会添加 PriorityClass。有了 PriorityClass,GPU 不够时调度器可以抢占低优先级的 Pod。low和medium的 Pod 沿用集群默认优先级。 - 每个级别可以有自己的单价,见下文“设置池的默认 QoS 和单价”。
为工作负载指定 QoS 级别
在 Pod 模板上设置注解 tensor-fusion.ai/qos:
apiVersion: apps/v1
kind: Deployment
# ...
spec:
template:
metadata:
labels:
tensor-fusion.ai/enabled: "true"
annotations:
tensor-fusion.ai/qos: "high"
tensor-fusion.ai/tflops-request: "100"
tensor-fusion.ai/tflops-limit: "200"
tensor-fusion.ai/vram-request: "8Gi"
tensor-fusion.ai/vram-limit: "8Gi"如果使用 WorkloadProfile,在它的 spec.qos 里设置。Pod 上的注解优先于 profile。
没有显式指定时,TensorFusion 按以下顺序决定级别:
- TFLOPS 和 VRAM 的请求都等于限额且不为零:
high。 - 池设置了
qosConfig.defaultQoS:取该值。 - 其余情况:
medium。
critical 不会被自动分配,因为它可以抢占其他工作负载。
在运行中的 Pod 上确认结果:
kubectl get pod <pod-name> -o jsonpath='{.metadata.annotations.tensor-fusion\.ai/qos}{"\n"}{.spec.priorityClassName}{"\n"}'保护新建的 Pod 不被抢占
带有注解 tensor-fusion.ai/eviction-protection 的 Pod,在创建后的指定时间内不会被抢占。取值是一段时长,例如 30m、2h:
metadata:
annotations:
tensor-fusion.ai/eviction-protection: "2h"保护期内,Pod 的成本会乘以池的 evictionProtectionPriceRatio。critical 的 Pod 不收这笔加价。
设置池的默认 QoS 和单价
QoS 配置和单价属于 GPU 池。在 TensorFusionCluster 中编辑对应池的 qosConfig:
kubectl edit tensorfusioncluster tensor-fusionapiVersion: tensor-fusion.ai/v1
kind: TensorFusionCluster
metadata:
name: tensor-fusion
spec:
gpuPools:
- name: shared
isDefault: true
specTemplate:
# ...
qosConfig:
defaultQoS: medium
evictionProtectionPriceRatio: "1.2"
pricing:
- qos: low
requests:
perFP16TFlopsPerHour: "0.0035"
perGBOfVRAMPerHour: "0.008"
limitsOverRequests: "0.5"
- qos: medium
requests:
perFP16TFlopsPerHour: "0.0069228"
perGBOfVRAMPerHour: "0.01548"
limitsOverRequests: "0.5"
- qos: high
requests:
perFP16TFlopsPerHour: "0.0069228"
perGBOfVRAMPerHour: "0.01548"
limitsOverRequests: "1"
- qos: critical
requests:
perFP16TFlopsPerHour: "0.0104"
perGBOfVRAMPerHour: "0.0232"
limitsOverRequests: "1"示例中的价格只是占位,请换成你自己的费率。
| 字段 | 含义 | CRD 默认值 |
|---|---|---|
defaultQoS | 工作负载没有指定级别、且请求与限额不相等时使用的 QoS 级别 | 无,回退为 medium |
pricing[].qos | 这条价格适用的 QoS 级别 | |
pricing[].requests.perFP16TFlopsPerHour | 每请求 1 TFLOPS、每小时的价格。可以带 $ 前缀,解析时会忽略 | $0.0069228 |
pricing[].requests.perGBOfVRAMPerHour | 每请求 1 GiB VRAM、每小时的价格 | $0.01548 |
pricing[].limitsOverRequests | 限额超出请求的那部分按什么比例计价。1 表示全价,0.5 表示半价 | 1 |
evictionProtectionPriceRatio | Pod 处于驱逐保护期时的成本倍数。小于 1 时按 1.2 处理 | 1.2 |
有三点需要注意:
- 没有
qosConfig的池,所有 worker 的成本都记为0。Helm chart 创建的默认池没有qosConfig,要用成本数据就得先补上。 pricing中没有对应条目的 QoS 级别,成本同样记为0。用到的每个级别都要配一条。qosConfig.definitions可以给每个级别写一个priority,但当前调度器不读取它。调度优先级来自上文的 PriorityClass。
worker 成本的计算方式
每个 worker、每小时、每张 GPU:
成本 = tflops 请求 × tflops 单价
+ vram 请求(GiB) × vram 单价
+ (tflops 限额 − tflops 请求) × tflops 单价 × limitsOverRequests
+ (vram 限额 − vram 请求)(GiB) × vram 单价 × limitsOverRequests结果再乘以 worker 使用的 GPU 数量;Pod 处于驱逐保护期时,还要乘以 evictionProtectionPriceRatio。
举例:一个 medium 的 worker 请求 100 TFLOPS 和 8 GiB,限额是 200 TFLOPS 和 8 GiB。按上面 medium 的单价,每小时成本为 100 × 0.0069228 + 8 × 0.01548 + 100 × 0.0069228 × 0.5 ≈ 1.16。
成本按 worker 持有的请求和限额计算,与实际用量无关。空闲的 worker 和满载的 worker 成本相同。
设置各 GPU 型号的每小时成本
节点成本根据各 GPU 型号的每小时成本计算。这些成本记录在对应厂商 ProviderConfig 的 spec.hardwareMetadata[].costPerHour 中。NVIDIA 的 ProviderConfig 由 Helm chart 根据 providerConfigs.nvidia.hardwareMetadata 创建,里面已经带有常见型号的参考价。
要换成自己的价格,先把 chart values.yaml 中的 hardwareMetadata 列表完整复制到你的 values 文件,再修改 costPerHour。Helm 覆盖列表是整体替换,只写一部分会把其他型号丢掉。
# my-values.yaml
providerConfigs:
nvidia:
hardwareMetadata:
# ... 从 values.yaml 复制的其他全部型号 ...
- model: T4
fullModelName: "Tesla T4"
costPerHour: "0.53"
fp16TFlops: "65"执行 helm upgrade 后检查资源:
kubectl get providerconfig nvidia-provider -o yaml | grep -B2 -A2 'Tesla T4'controller 会自动读取新价格,不用重启。
| 字段 | 含义 |
|---|---|
model | 型号简称 |
fullModelName | 驱动上报的型号全名。用 kubectl get gpu -o custom-columns=NAME:.metadata.name,MODEL:.status.gpuModel 查看准确写法 |
costPerHour | 单张 GPU 每小时的成本 |
fp16TFlops | 单张 GPU 的 FP16 算力,单位 TFLOPS。修改它会改变调度器可分配的容量,只想改价格时不要动它 |
旧版本从 ConfigMap tensor-fusion-sys-public-gpu-info 读取 GPU 价格。chart 仍然会安装这个 ConfigMap,但当前 controller 只从 ProviderConfig 取价格,修改这个 ConfigMap 不会影响成本。
查询成本数据
成本写入 GreptimeDB 的 raw_cost 列。每一行记录的是距上一行这段时间内的成本,所以按需要的时间段求和即可。
| 表 | 每行对应 | raw_cost 的含义 |
|---|---|---|
tf_worker_resources | 一个 worker 在一个采样时刻 | 该 worker 的成本,按池的 QoS 单价计算 |
tf_node_metrics | 一个 GPU 节点在一个采样时刻 | 该节点的成本,按节点上各 GPU 的 costPerHour 计算 |
最近 24 小时按命名空间和 QoS 级别统计成本:
SELECT namespace, qos, SUM(raw_cost) AS cost
FROM tf_worker_resources
WHERE greptime_timestamp >= now() - '24h'::INTERVAL
GROUP BY namespace, qos
ORDER BY cost DESC;同一时间段内按池统计 GPU 节点成本:
SELECT pool, SUM(raw_cost) AS cost
FROM tf_node_metrics
WHERE greptime_timestamp >= now() - '24h'::INTERVAL
GROUP BY pool;把两者放在一起对比,就能看出 GPU 的实际成本中有多少分摊到了工作负载上。
下一步
- 工作负载配置
- GPUPool 字段说明,包含
qosConfig的全部字段 - GPUResourceQuota 字段说明,用于限制每个命名空间的 GPU 资源
- 监控指标定义
TensorFusion 文档