简介
类似社区 GPU Operator,TKE 计划通过一个 Addon 将驱动安装、device-plugin部署、gpu监控、qgpu模块等能力集成到一起,并从将GPU流程和集群、节点池、节点流程解耦。
Addon会向集群中部署CRD表示 集群GPU 的配置,该CRD同样会作为和上层Gateway的交互界面。
我们会包含两个CRD:
GPUConfig 集群唯一,表示集群维度的GPU配置,GPUConfig中并没有集群维度的默认设置,因为驱动和机型强绑定,集群中可能会添加不同机型,但我们限制节点池只能添加单一的GPU机型,因此只有节点池维度和OrphanNode的配置。
GPUShadow 每节点一个,表示单节点的配置和驱动状态
CRD定义如下(临时定义,可以修改):
代码解释
代码改写
// 集群唯一的GPU配置
type GPUConfig struct {
metav1.TypeMeta
metav1.ObjectMeta
Spec GPUConfigSpec
Status GPUConfigStatus
}
// 以下的创建包括控制台、TF等,所有可以创建节点池、节点的方式
// 也是Addon和Gateway唯二需要交互的位置(另一个可能需要修改配置)
// 另外GPUConfig并没有集群维度设置,因为设置是和机型强绑定的
type GPUConfigSpec struct {
// 节点池维度配置,通过Selector和node匹配
// 在创建节点池时,需要在数组中创建一项
// 在删除节点池时,尽量将节点池从数组中删掉,避免对象过大
NodePoolConfigs []NodePoolConfig
// 节点维度配置,通过NodeName来和Node匹配
// 在创建节点时,需要在相同配置的Nodes数组中添加nodename
// 如果没有相同配置就新建个数组项
// 需要考虑下会不会有控制台创建100个独立node的问题
OrphanNodeConfigs []OrphanNodeConfig
}
type NodePoolConfig struct {
// selector 用来匹配节点池
Selector *metav1.LabelSelector
GPUDriverInfo GPUDriverInfo
GPUFeatures GPUFeatures
}
type OrphanNodeConfig struct {
Nodes []string
GPUDriverInfo GPUDriverInfo
GPUFeatures GPUFeatures
}
// GPUDriverInfo 主要用来描述节点初始化时的一些版本信息
// 在不支持GPU驱动升级的场景下,这些字段的修改不会对存量生效
type GPUDriverInfo struct {
Driver string
Cuda string
Cudnn string
FabricManager bool
CustomDriverUrl string
}
// GPUFeatures 用来表示要开启哪些特性,这些修改可以对存量生效。
// 之后会提到两个方案,在纯DaemonSet的模式下,如果对存量生效可能会有大波动。
type GPUFeatures struct {
// GPUPlugin tells which device plugin should be used for nodes and node pools, Nvidia, QGPU or SHARED.
GPUPlugin string
// GPUExporter tells which monitor should be used. NONE, QGPU or DCGM.
GPUExporter string
}
// GPUShadow 每节点一个,会和节点同名
// 在metadata中会通过OwnerRef指向所属节点
type GPUShadow struct {
metav1.TypeMeta
metav1.ObjectMeta
Spec GPUShadowSpec
Status GPUShadowStatus
}
// 这里是否还需要保留驱动和Feature有待考虑
// 如果GPUConfig中OrphanNode数组不好解决问题,也可以挪到这里
type GPUShadowSpec struct {
GPUDriverInfo *GPUDriverInfo
GPUFeatures *GPUFeatures
}
// 表示节点上实际的驱动版本、组件版本等信息
type GPUShadowStatus struct {
GPUDriverInfo GPUDriverInfo
GPUFeatures GPUFeatures
// 表示GPU安装的进展、状态和失败的原因
// 目前这里还没细化,等写完下面两种方式后细化
Conditions []Condition
// TODO 根据DS部署还是Controller部署需要考虑是否加入更多组件版本信息
}
目前组件的部署方式有两种,纯DaemonSet模式和Controller模式
纯DaemonSet模式
方案介绍
纯DaemonSet方式中,Addon仅包含DaemonSet(有多DaemonSet模式和依赖黑科技的单DaemonSet方案),无需部署Controller。
优点:不存在节点从0到1的问题,理想情况下,一个DaemonSet搞定所有安装、DevicePlugin、监控的工作
缺点:依赖黑科技,也许要牺牲稳定性,没有Controller在中间控制,之后某些升级、切换DevicePlugin需求可能会遇到并发问题;另外需要DevicePlugin来适配我们(单DaemonSet模式下),如qgpu DevicePlugin其实是两个容器,我们需要他合并成一个容器,并修改EntryPoint
多DaemonSet模式
Addon中包含:
负责安装GPU驱动的 DaemonSet和相关配置
DevicePlugin、Exporter的DaemonSet等
我们模拟下新节点创建的流程
在添加GPU节点时,仍依赖 Dashboard 或 Cloud Gateway 给Node打Label 用于驱动安装Pod调度(我们假设驱动安装Pod为Player)
当Player调度后,会去集群中获取GPUConfig,找到当前节点的GPUDriverInfo
Player创建GPUShadow并尝试安装驱动,安装驱动可能有多重结果:
安装成功,Controller设置GPUShadow中的状态,并继续往下
安装失败,因为节点上已经存在驱动,需要DriverPlayer检测驱动版本,写到GPUShadow上(Controller或Player写)
其他原因安装失败,需要将状态和错误原因写到GPU Shadow上
当驱动安装成功后,DaemonSet修改GPUShadow的状态,添加Exporter、DevicePlugin相关的label
Player 安装驱动后就无其他作用了,这里可以选择摘掉自己的Label;或者Sleep来保证支持通过GPUConfig来切换DevicePlugin或Exporter;或和监控做到同一个Pod中
单DaemonSet模式
上面的模式中存在多个DaemonSet,并且无法像Controller模式一样Lazy Apply DaemonSet的配置。因此我们通过原地更新Image能力来实现单DaemonSet模式。
单Daemonset模式中,我们将DaemonSet称之为GPU kit,其中包含3个Container,主容器 player、device plugin容器 plugin、监控容器exporter。
在DaemonSet的PodTemplate中 或 Pod刚创建时,plugin和exporter的Image都是空壳,仅能保证容器能被Command启动并不会被Probe干掉。
当添加GPU节点时,我们依赖Dashboard给节点打标保证我们调度,然后我们会走到如下流程:
-
此时只有Player正常工作,Player创建GPUShadow,并获取GPUConfig中对当前节点的配置
-
Player安装GPU驱动,成功或失败都同上,将结果同步到GPU Shadow
-
驱动安装成功后,Player根据配置选择性将plugin、exporter的镜像修改成NVIDIA Device Plugin或QGPU
-
kubelet发现容器的镜像变化,会重启plugin、exporter容器,Player容器不会被重启
-
等plugin、exporter容器启动后再次将结果同步到GPU Shadow
因为只有pod.container.image可变,当前方案强依赖全部的Plugin和Exporter都需要有相同的配置(特权、环境变量、容器数量、启动命令、Probe)
当客户在GPUConfig中关闭了Exporter的特性时,Player将容器Exporter的镜像再次切回到空壳即可
(当然,多准备几个壳容器也行,大不了就6/6)
版本管理
我们会将所有的CRD、DaemonSet、镜像版本都交给Addon来管理,之后更新只需要更新Addon即可。
Player的镜像设置在Addon的DaemonSet中,Player在安装驱动时可能要根据驱动版本去某处Head一个脚本(避免上线新版驱动、机型需要发布Addon的尴尬,当然Addon中的DaemonSet如果是OnDelete应该也行,需要验证下OnDelete的DaemonSet更新了,会不会动这种修改过container image的Pod)
Plugin、Exporter的镜像作为环境变量设置在Player容器里,这样每次更新Addon时只需要更新同一个DaemonSet即可,假设我们发布QGPU的DevicePlugin只需要更新下Addon中的DaemonSet即可:Pod重新创建时Player会检查节点的GPU驱动,并根据环境变量替换镜像 + 重启容器
存量兼容
单DaemonSet模式兼容新集群比较简单,这里跳过;兼容存量集群较为麻烦,需要客户协作。
这里兼容分为几种情况,存量节点和新节点:
-
存量节点上没有Label,GPU kit不会调度到存量节点。在手动打上Label的情况下,Player需要检测到节点上的DevicePlugin(考虑是否需要再GPUConfig中配置DevicePlugin的名称,避免客户自定义DevicePlugin名称我们识别不到),并跳过更新Plugin容器的镜像,同时在GPUShadow中说明原因。
-
新节点有Label,新建的OrphanNode或新建节点池的节点,我们都会带上Label,GPU Kit可以调度上去,Player负责安装驱动,之后同上识别到节点上有DevicePlugin就不启动Plugin容器,当客户删除掉DevicePlugin后,我们自动启动Plugin容器。
-
新节点无Label,这里需要考虑如何兼容,如下
这里有个坑是存量GPU节点池创建出来的节点是没有Label的,这种情况可能会导致不安装GPU驱动。有两种方式,也影响上面情况1存量节点如何兼容:
-
我们可以考虑这种节点池扩出来的节点继续沿用之前安装驱动的方式,相对柔和,新节点无Label会变为情况1,存量节点无Label
-
或者让GPU Kit的Selector直接用之前NVIDIA、QGPU的label(或关系),这种情况原本安装GPU驱动的逻辑可以完全下线,GPU Kit也会部署到存量节点上。
这两个方案需要trade off下
Controller模式
方案介绍
Controller 模式更接近于常规的GPU Operator
优点:稳定,直观,黑科技较少,和存量兼容更简单
缺点:依赖Controller需要占用metacluster资源或部署在客户集群牺牲一点稳定性
Addon中包含:
-
部署 Controller 用的 Deployment 和相关的配置
-
部署 NvidiaDevicePlugin 用的 DaemonSet
-
部署 QGPUNvidiaDevicePlugin 用的 DaemonSet
-
部署监控用的DaemonSet
… (可能会有GPU Share等)
集群添加第一个GPU节点或节点池时,自动安装当前Addon。
我们模拟下新节点创建的流程
Controller watch到新节点创建,若发现新节点可以匹配到 GPUConfig 中某个 NodePool 或 OrphanNode 的配置、则会执行如下步骤:
-
创建 GPUShadow并和当前节点绑定,Shadow中明确说明发现了GPU节点,要探测或安装驱动
-
创建 pod DriverPlayer,指定调度到新节点上,同时将GPUConfig中的配置通过环境变量传给主进程
-
等待 pod DriverPlayer执行结束,此时有三种情况:
-
安装成功,Controller设置GPUShadow中的状态,并继续往下
-
安装失败,因为节点上已经存在驱动,需要DriverPlayer检测驱动版本,写到GPUShadow上(Controller或Player写)
-
其他原因安装失败,需要将状态和错误原因写到GPU Shadow上
-
-
在驱动安装成功的情况下,Controller会根据GPUConfig中的配置给节点上打标,其中包含DevicePlugin或QGPU、Exporter等
-
打标后DevicePlugin即可调度到节点上
从0到1的问题
其中Controller可以部署在客户集群或meta cluster中,唯一要求是能连接api server创建pod和给node打label。若部署在客户集群内,Controller也无需考虑从0到1启动问题:假设第一个弹出节点即为GPU节点,此时节点可以不安装GPU驱动作为普通节点初始化,Controller可以正常启动;在Controller启动后,发现当前节点可以匹配GPUConfig中的字段,则会为当前节点安装驱动、初始化DevicePlugin等。
方案优化
我们还要考虑下,是否需要一开始就往节点上投放那么多DaemonSet,如果一个只用到DevicePlugin的普通集群直接就拿到了4个DaemonSet(甚至更多),会把客户吓到。
DaemonSet使用Controller Lazy部署,将yaml link到Controller的代码里或设置在ConfigMap上,等到Controller发现有节点、节点池开启相应的特性再去创建DaemonSet
版本管理
我们会将所有的CRD、DaemonSet、镜像版本都交给Addon来管理,之后更新只需要更新Addon即可。
DaemonSet 如DevicePlugin的更新策略可以设置成RollingUpdate或OnDelete(根据Addon更新策略决定)。
最经常发生变化的应该是Player容器的镜像(可能新版本驱动、新版本镜像,自定义驱动等需要适配),最简单的方案是将Player镜像会作为Env或ConfigMap配置到Controller的Deployment中,因此更新镜像时只需要更新Controller即可。但每次发新的驱动都要去客户集群中更新组件并不合理,因此我们可以给不同的驱动、CUDA版本组合单独的镜像,Controller尝试去Head是否存在该组合的镜像,尝试从镜像仓库中搜索,从而找到适配的最佳镜像。
存量兼容
Controller做存量兼容相对比较简单,存量集群添加GPU节点时,我们会部署Addon;
部署Addon时,Addon不会强制覆盖集群中的DaemonSet,尽量以客户的DaemonSet为主(客户可能会用自己的DS,因此也许需要在GPUConfig中开放一些自定义设置来指定DevicePlugin、Exporter、Label等名称)
Dashboard 和 Cloud Gateway 中给 node 标记 Label的逻辑会被删除,但由于节点池可能会给Node打上Label,因此Node上可能存在label和DevicePlugin,Controller需要做适配。也即当Controller等待pod Player执行结束后,检查节点Label和DevicePlugin。
More
当然DCGM Exporter可能也需要ConfigMap、CRD可能定义的还不够复杂,这些问题都需要考虑;最好找个集群,分别把各种DevicePlugin、QGPU Exporter、DCGM Exporter部署上去,然后对比下哪种方案最好
more more
AS 中是有 CloudInit 脚本的,脚本里面可能有安装GPU驱动的部分,这里要考虑好是存量刷新还是按照节点上有驱动来处理
7月18日更新controller部署模式:
原生节点需要:
新建GPU原生节点池时,安装 GPU Operator 的 addon,同时修改 crd GPUConfig 中 NodePool 的设置,此时不要再给MachineSet设置GPUParams。
当MS创建Machine进入到machine lifecycle controller的逻辑时,根据GPUParams是否是空来判断走新逻辑还是旧逻辑(新逻辑GPU Operator,旧逻辑脚本)。
新逻辑:等node注册成功时,GPU Operator发现node能被GPUConfig的NodePool match上,开始安装驱动
旧逻辑:此时MachineSet创建出的node不能被GPUConfig的NodePool匹配上,GPU Operator会跳过这个node的处理,不走ClusterConfig的兜底逻辑
普通节点需要:
增量节点池,修改 crd GPUConfig 中 NodePool 的设置
存量节点池新节点,逻辑和之前保持一致,如果控制台能提供修改驱动入口,在修改后会切换到GPU Operator上
优化查询CloudInit逻辑,对于裸GPU + 设置了GPU驱动版本的节点,设置到OrphanNode上(谁来回收?)
下线CloudInit后,需要发布Dashboard,支持对于裸GPU节点,没设置GPU驱动(走兜底逻辑的),需要打Label。GPU Operator遇到带有Label的并且不能匹配NodePool、OrphanNode的节点,则走CloudInit的默认逻辑。
搞个类似的label tke.cloud.tencent.com/gpu-enable: true,