云厂商集成(BYOC)
让 GPU 池在你自己的云账号里自动增减 GPU 节点,可以通过 Karpenter,也可以直接调用云 API
GPU 池可以在你自己的云账号里按需扩缩。本文说明池的三种节点供给模式、每种模式目前支持哪些云,以及各自怎么配置。
先说结论:在 AWS EKS 上使用 Karpenter 时,保持默认的 AutoSelect 模式,让 Karpenter NodePool 管理 GPU 节点。这是 Helm chart 的默认路径,也是端到端测试覆盖的路径。另外两种模式按你在池上设置的容量目标工作,目前属于预览能力,请先在测试集群验证。
节点供给模式
模式在每个池的 nodeManagerConfig.provisioningMode 中设置。
AutoSelect(默认) | Karpenter | Provisioned | |
|---|---|---|---|
| 谁创建节点 | TensorFusion 之外的任何方式:手工、节点组或 Karpenter NodePool | TensorFusion 创建 Karpenter NodeClaim | TensorFusion 调用云 API |
| 节点如何进入池 | 池的 nodeSelector 匹配节点标签 | 为该池创建 | 为该池创建 |
| 扩容触发条件 | TensorFusion Pod 因 GPU 容量不足而 Pending(要求节点归 Karpenter 管理) | 池容量低于 minResources,或空闲容量低于 warmResources;以及 Pod Pending | 同 Karpenter |
| 缩容 | TensorFusion 给空节点打标签,由 Karpenter 删除 | TensorFusion 删除节点 | TensorFusion 删除节点 |
| TensorFusion 是否需要云凭据 | 不需要 | 不需要 | 需要 |
| 目前支持 | 任意集群;自动扩容需要 Karpenter | 任意 Karpenter provider,已在 AWS 上测试 | 阿里云 ECS;AWS EC2 尚不完整,见下文 |
一个池只能用一种模式,同一集群里的不同池可以用不同模式。
前置条件
- 已安装 TensorFusion。参见 在 Kubernetes 安装。
- controller 带
-enable-auto-expander参数启动,默认命令里已包含。 - 两种 Karpenter 方案都需要:Karpenter 1.6 或更高版本,并开启
nodeOverlayfeature gate;一个能把 GPU 节点拉起并加入集群的EC2NodeClass(或你所用 provider 的 NodeClass)。 - GPU 节点带有
initialGpuNodeLabelSelector选中的标签,默认是nvidia.com/gpu.present=true。
方案一:AutoSelect + Karpenter NodePool(推荐)
GPU 节点归 Karpenter 管理。TensorFusion 把这些节点选进池,GPU 不够时向 Karpenter 要一台新节点,并标记可以回收的空节点。
第 1 步:先装 Karpenter,再装 TensorFusion
在 Karpenter 的 Helm values 中开启 nodeOverlay feature gate:
settings:
featureGates:
nodeOverlay: trueTensorFusion Pod 会申请名为 tensor-fusion.ai/index_* 的扩展资源。Karpenter 只有在认为某种机型能提供 Pod 申请的全部资源时,才会为它拉起节点。所以 TensorFusion chart 会创建一个名为 tensor-fusion-overlay 的 NodeOverlay 来声明这些资源,但前提是安装时集群里已经有 nodeoverlays.karpenter.sh 这个 CRD。
如果你先装了 TensorFusion,装完 Karpenter 后对 TensorFusion 的 release 再执行一次 helm upgrade,然后检查:
kubectl get nodeoverlay tensor-fusion-overlay第 2 步:为 GPU 节点创建 NodePool
给节点加上默认池所选择的标签,并把机型限定为 GPU 机型:
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: tf-gpu
spec:
template:
metadata:
labels:
nvidia.com/gpu.present: "true"
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: <your-ec2-node-class>
expireAfter: Never
requirements:
- key: node.kubernetes.io/instance-type
operator: In
values: ["g5.xlarge", "g5.12xlarge"]
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand", "spot"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: kubernetes.io/os
operator: In
values: ["linux"]
# 这个 NodePool 能拉起的资源上限
limits:
nvidia.com/gpu: 8
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 5mHelm chart 创建的默认池本来就是 AutoSelect 模式,并选择带 nvidia.com/gpu.present=true 的节点,所以不用修改 TensorFusionCluster。
第 3 步:验证
NodePool 拉起 GPU 节点后,TensorFusion 中会出现对应资源:
kubectl get nodeclaim,gpunode,gpu把现有 GPU 占满,再创建一个 TensorFusion Pod,观察新节点加入:
kubectl get events -A --field-selector reason=NodeExpansionCompleted
kubectl get nodeclaim -w节点加入集群、GPU 注册完成后,Pod 从 Pending 变为 Running。
工作方式
- 扩容。 调度器拒绝一个 TensorFusion Pod 后,controller 先判断是不是只缺 GPU 容量。如果是,它挑一个归 Karpenter
NodeClaim所有的现有 GPU 节点作模板,在同一个 NodePool 里创建新的NodeClaim。新节点遵守 NodePool 的 requirements,并按 Pod 的nodeSelector和节点亲和(仅机型、可用区、地域)进一步收窄。 - 并发。 同时供给的节点数不超过
dynamicConfig.maxInFlightNodes,默认 15。 - 缩容。 池的压缩循环(
nodeManagerConfig.nodeCompaction.period)给空节点打上标签tensor-fusion.ai/should-delete=true。TensorFusion 自己不删节点,由 Karpenter 的 consolidation 删除。创建不足 5 分钟的节点不会被标记;删掉后空闲容量会低于warmResources的节点也不会被标记。 - 容量上限。 这种模式下
capacityConfig.minResources和maxResources不驱动节点供给,规模上限请用 NodePool 的limits控制。
已知限制:
- 扩容需要模板节点:池里至少要有一台由 Karpenter 创建的 GPU 节点。托管节点组里的 GPU 节点不能当模板,此时 Pod 保持
Pending,并出现NodeExpansionFailed事件。 - GPU 节点数为 0 时,controller 把决定权交给 Karpenter。Karpenter 会拉起什么机型取决于你的 NodePool 配置,所以建议常驻一台 GPU 节点,或者在你的环境里先验证冷启动。
- Karpenter 的 consolidation 不知道 TensorFusion 在节点上已经分配了多少 GPU 容量。除非你用自己的负载验证过
WhenEmptyOrUnderutilized,否则使用consolidationPolicy: WhenEmpty。
方案二:Karpenter 模式
TensorFusion 根据池上的容量目标决定何时增减节点,Karpenter 只负责拉起和终止实例,不需要 NodePool。TensorFusion 先创建 GPUNodeClaim,再创建同名的 Karpenter NodeClaim。
预览能力,正式使用前请在测试集群验证。minResources 不为零时会立即拉起节点,建议先从很小的值开始。
在集群上设置 computingVendor,在池上设置 provisioner:
apiVersion: tensor-fusion.ai/v1
kind: TensorFusionCluster
metadata:
name: tensor-fusion
spec:
computingVendor:
name: karpenter
type: karpenter
authType: serviceAccountRole
enable: true
params:
defaultRegion: us-east-1
gpuPools:
- name: elastic
isDefault: false
specTemplate:
# componentConfig 和 schedulingConfigTemplate 从现有的池复制
capacityConfig:
minResources:
tflops: "65"
vram: "16Gi"
warmResources:
tflops: "0"
vram: "0"
maxResources:
tflops: "500"
vram: "128Gi"
nodeManagerConfig:
provisioningMode: Karpenter // [!code highlight]
nodeProvisioner:
karpenterNodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
version: v1
name: <your-ec2-node-class>
gpuRequirements:
- key: node.kubernetes.io/instance-type
operator: In
values: ["g4dn.xlarge", "g5.xlarge"]
- key: karpenter.sh/capacity-type
operator: In
values: ["OnDemand"]
gpuNodeLabels:
nvidia.com/gpu.present: "true"
# gpuTaints:
# - key: group
# value: gpu
# effect: NoSchedule
nodeCompaction:
period: 5m容易出错的地方:
type: karpenter不需要云凭据,controller 只访问 Kubernetes API。它会检查自己能否列出NodeClaim,结果写在集群的 conditionCloudVendorConnectionReady中。karpenterNodeClassRef必须指向已存在的 NodeClass,四个字段都要填。gpuNodeLabels必须包含initialGpuNodeLabelSelector选中的标签,否则 controller 不会监听新节点。- capacity type 的取值是
OnDemand、Spot、Reserved,TensorFusion 会转换成 Karpenter 的on-demand、spot、reserved。列表里有Spot时优先使用 Spot。 - 每个
NodeClaim都会申请nvidia.com/gpu。Karpenter 要等节点上报这个资源,所以节点上必须运行 NVIDIA device plugin。 - 这样创建的
NodeClaim带有karpenter.sh/do-not-disrupt: "true",Karpenter 不会对它们做 consolidation。节点变空、且warmResources不再需要它时,由 TensorFusion 删除。
computingVendor.params.extraParams 中的可选项:
| 键 | 作用 |
|---|---|
karpenter.gpuResource | NodeClaim 上申请的资源名(默认:nvidia.com/gpu) |
karpenter.nodeClaim.terminationGracePeriod | NodeClaim 的 terminationGracePeriod,例如 5m |
观察进度:
kubectl get tensorfusioncluster tensor-fusion -o jsonpath='{.status.conditions}'
kubectl get gpunodeclaim,nodeclaim,gpunode -wGPUNodeClaim 在对应的 NodeClaim 创建后从 Pending 变为 Creating,新节点上的 hypervisor 运行起来后变为 Bound。
TensorFusion 如何选择机型
在 Karpenter 和 Provisioned 模式下,TensorFusion 先算出 TFLOPS 和 VRAM 的容量缺口,再从内置的机型目录里选一种机型:补齐缺口所需的每小时总价最低的那种。只有下面这些 gpuRequirements 键会影响选择:
| 键 | 用途 |
|---|---|
node.kubernetes.io/instance-type | 允许的机型 |
kubernetes.io/arch | amd64 或 arm64 |
tensor-fusion.ai/gpu-vendor | 机型的 GPU 厂商 |
karpenter.sh/capacity-type | OnDemand、Spot 或 Reserved |
topology.kubernetes.io/region | 地域,取第一个值。默认使用 computingVendor.params.defaultRegion |
topology.kubernetes.io/zone | 可用区,每次扩容随机选一个 |
机型目录是静态的:里面是 AWS 的 GPU 机型和按需价格,价格不区分地域,Spot 也按按需价格计算。建议直接列出你想用的机型,不要依赖比价结果。
方案三:Provisioned 模式
TensorFusion 自己调用云的计算 API。扩缩容逻辑与 Karpenter 模式相同,区别在于由谁拉起实例,以及实例的规格要用 GPUNodeClass 描述。
预览能力,各云的实现程度不一,见下表。如果你的云有 Karpenter provider,优先使用方案一。
支持的云
CRD 中 computingVendor.type 可以填很多值,controller 实际实现的是这些:
type | 状态 | 认证方式 | 传给云 API 的内容 |
|---|---|---|---|
alibaba | 已实现 | 仅 authType: accessKey | 启动模板 ID、镜像 ID、安全组 ID、交换机 ID、数据盘、标签、user data,以及下文的额外参数。内置机型目录只有 ecs.gn6i-c8g1.2xlarge 和 ecs.gn6i-c16g1.4xlarge |
aws | 不完整 | AWS 默认凭据链,例如绑定在 controller ServiceAccount 上的 IAM 角色 | 只有 AMI ID、机型和标签。GPUNodeClass 里的子网、安全组、instance profile、块设备、user data 都不会生效,实例无法自行加入集群。在 AWS 上请用方案一或方案二 |
karpenter | 已实现 | 无 | 用于 Karpenter 模式,见方案二 |
gcp、azure 等其他取值 | 未实现 | controller 报 unsupported cloud provider |
配置阿里云
-
准备一组可以创建、查询、删除 ECS 实例的 AccessKey。chart 把它保存在 Secret
tf-cloud-vendor-credentials(键为ak和sk)中,并挂载到 controller 的/tmp/secret。首次安装时,传入
--set cloudVendorCredentials.accessKey=<access-key-id> --set cloudVendorCredentials.secretKey=<access-key-secret>。chart 只在这个 Secret 不存在时写入,所以已安装的环境要直接更新 Secret:kubectl -n tensor-fusion-sys patch secret tf-cloud-vendor-credentials --type merge \ -p '{"stringData":{"ak":"<access-key-id>","sk":"<access-key-secret>"}}' -
创建描述实例规格的
GPUNodeClass:apiVersion: tensor-fusion.ai/v1 kind: GPUNodeClass metadata: name: tf-node-class spec: osImageSelectorTerms: - id: <image-id> securityGroupSelectorTerms: - id: <security-group-id> subnetSelectorTerms: - id: <vswitch-id> blockDeviceMappings: - deviceName: data ebs: volumeSize: "200" volumeType: cloud_essd tags: team: ai-platform userData: | #!/bin/bash # 用你平时的引导命令把节点加入集群,并在注册时带上下面两个节点标签。 # tensor-fusion.ai/node-provisioner=__GPU_NODE_RESOURCE_NAME__ # nvidia.com/gpu.present=true各 selector 目前只按
id匹配。拉起实例前,controller 会把userData中的__GPU_NODE_RESOURCE_NAME__替换成GPUNodeClaim的名称。节点注册时必须带有标签tensor-fusion.ai/node-provisioner,值为这个名称。TensorFusion 靠这个标签把节点和它的 claim 对应起来。 -
让集群和池引用它们:
apiVersion: tensor-fusion.ai/v1 kind: TensorFusionCluster metadata: name: tensor-fusion spec: computingVendor: name: alibaba type: alibaba authType: accessKey enable: true params: defaultRegion: cn-hangzhou accessKeyPath: /tmp/secret/ak secretKeyPath: /tmp/secret/sk extraParams: systemDiskSize: "100" gpuPools: - name: elastic isDefault: false specTemplate: # componentConfig 和 schedulingConfigTemplate 从现有的池复制 capacityConfig: minResources: tflops: "65" vram: "15Gi" maxResources: tflops: "260" vram: "60Gi" nodeManagerConfig: provisioningMode: Provisioned // [!code highlight] nodeProvisioner: nodeClass: tf-node-class gpuRequirements: - key: node.kubernetes.io/instance-type operator: In values: ["ecs.gn6i-c8g1.2xlarge"] - key: topology.kubernetes.io/zone operator: In values: ["cn-hangzhou-h"] nodeCompaction: period: 5m
阿里云在 computingVendor.params.extraParams 中支持的额外参数:
| 键 | 作用 | 默认值 |
|---|---|---|
systemDiskCategory | 系统盘类型 | cloud_essd |
systemDiskSize | 系统盘大小,单位 GiB | 40 |
dataDiskPerformanceLevel | 数据盘性能等级 | PL0 |
keyPairName | 实例的 SSH 密钥对 | 无 |
spotPriceLimit | 抢占式实例的每小时最高出价。不设置时按市场价 | 无 |
spotDuration | 抢占式实例的保护时长,单位小时 | 0 |
spotInterruptionBehavior | 抢占式实例被回收时的处理方式 | 云的默认值 |
创建出来的实例带公网 IP,按流量计费,出方向带宽上限 100 Mbps。
容量设置
Karpenter 和 Provisioned 模式根据池的 capacityConfig 工作:
| 字段 | 作用 |
|---|---|
minResources | 池的 TFLOPS 和 VRAM 总量低于这些值时持续扩容 |
warmResources | 池进入 Running 后,空闲 TFLOPS 或 VRAM 低于这些值就扩容。空节点只有在删除后剩余空闲容量仍高于这些值时才会被回收 |
maxResources | TFLOPS 或 VRAM 总量达到这些值后不再加节点,池上会出现 MaxResourceConstraintReached 事件 |
其他需要了解的行为:
- 新节点在 5 分钟内不会被压缩回收。
- 带有标签
tensor-fusion.ai/do-not-disrupt=true的节点不会被压缩回收。 - CRD 中有
nodeProvisioner.budget,但当前 controller 并不执行预算限制。请用maxResources控制花费上限。
故障排查
| 现象 | 检查 |
|---|---|
CloudVendorConnectionReady 为 False | Karpenter 模式:确认已安装 Karpenter 的 CRD。Provisioned 模式:确认凭据和 defaultRegion 正确 |
GPUNodeClaim 一直是 Pending | 查看 controller 日志。常见原因:karpenterNodeClassRef 指向的 NodeClass 不存在,或 nodeClass 指向的 GPUNodeClass 不存在 |
节点已加入集群,但没有出现 GPUNode | 节点缺少 initialGpuNodeLabelSelector 选中的标签 |
AutoSelect 模式下 Pod 一直 Pending,也没有新节点 | 查看 Pod 的事件。NodeExpansionFailed 表示找不到归 Karpenter 管理的 GPU 节点作模板 |
| Karpenter 始终不为 TensorFusion Pod 拉起节点 | kubectl get nodeoverlay tensor-fusion-overlay 没有结果,或者没有开启 nodeOverlay feature gate |
kubectl logs -n tensor-fusion-sys deploy/tensor-fusion-sys-controller -c controller --tail=200下一步
- 生产级部署指南
- GPUPool 字段说明和 GPUNodeClass 字段说明
- TensorFusionCluster 字段说明,包含
computingVendor - Helm 安装 Values 配置
TensorFusion 文档