Kubernetes中GPU使用
Kubernetes中通过device plugin将GPU作为一种resource来使用,因此需要先创建一个device plugin将GPU信息注册到Kubernetes中。NVIDIA官方提供了一个GPU device plugin,详情可见https://github.com/NVIDIA/k8s-device-plugin。
先执行kubectl create -f nvidia-device-plugin.yaml
创建daemonset对象,等pod跑起来后,用kubectl describe node
查看下所在node是否获取到GPU信息
如上,可看到nvidia.com/gpu
信息,说明GPU信息已经注册到Kubernetes中。
业务pod中使用GPU资源跟使用CPU一样,配置下containers.[*].resources.limits.nvidia.com/gpu
即可,如下
nvidia-device-plugin实现分析
接下来分析下nvidia-device-plugin的实现,看是如何将GPU信息注册到Kubernetes中的。详细代码可见:https://github.com/NVIDIA/k8s-device-plugin
nvidia-device-plugin会先通过getDevices()
方法获取当前的GPU卡信息,并为每个GPU卡创建一个pluginapi.Device
对象。该对象包含设备ID和健康状态
func getDevices() []*pluginapi.Device {
n, err := nvml.GetDeviceCount()
check(err)
var devs []*pluginapi.Device
for i := uint(0); i < n; i {
d, err := nvml.NewDeviceLite(i)
check(err)
devs = append(devs, &pluginapi.Device{
ID: d.UUID,
Health: pluginapi.Healthy,
})
}
return devs
}
拿到GPU信息后,会创建一个NvidiaDevicePlugin
对象,如下
// NewNvidiaDevicePlugin returns an initialized NvidiaDevicePlugin
func NewNvidiaDevicePlugin() *NvidiaDevicePlugin {
return &NvidiaDevicePlugin{
devs: getDevices(),
socket: serverSock,
stop: make(chan interface{}),
health: make(chan *pluginapi.Device),
}
}
该对象实现了Kubernetes device plugin API,对kubelet提供ListAndWatch()
,Allocate()
等方法。先看下ListAndWatch()
的实现
// ListAndWatch lists devices and update that list according to the health status
func (m *NvidiaDevicePlugin) ListAndWatch(e *pluginapi.Empty, s pluginapi.DevicePlugin_ListAndWatchServer) error {
s.Send(&pluginapi.ListAndWatchResponse{Devices: m.devs})
for {
select {
case <-m.stop:
return nil
case d := <-m.health:
// FIXME: there is no way to recover from the Unhealthy state.
d.Health = pluginapi.Unhealthy
s.Send(&pluginapi.ListAndWatchResponse{Devices: m.devs})
}
}
}
nvidia-device-plugin在向kubelet注册GPU信息后,kubelet会调用ListAndWatch()
方法。该方法在初始调用时,会先将所有的devices上报给kubelet,当检测到某个device状态异常时,会再次上报。
当kubelet要创建容器时,如果检测到pod要使用GPU resource,会调用Allocate()
方法,该方法入参是kubelet申请使用的GPU设备ID
type AllocateRequest struct {
ContainerRequests []*ContainerAllocateRequest `protobuf:"bytes,1,rep,name=container_requests,json=containerRequests" json:"container_requests,omitempty"`
}
type ContainerAllocateRequest struct {
DevicesIDs []string `protobuf:"bytes,1,rep,name=devicesIDs" json:"devicesIDs,omitempty"`
}
看下Allocate()
的实现
// Allocate which return list of devices.
func (m *NvidiaDevicePlugin) Allocate(ctx context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
devs := m.devs
responses := pluginapi.AllocateResponse{}
for _, req := range reqs.ContainerRequests {
response := pluginapi.ContainerAllocateResponse{
Envs: map[string]string{
//将kubelet请求的DeviceID封装到NVIDIA_VISIBLE_DEVICES环境变量,再返回给kubelet
"NVIDIA_VISIBLE_DEVICES": strings.Join(req.DevicesIDs, ","),
},
}
for _, id := range req.DevicesIDs {
if !deviceExists(devs, id) {
return nil, fmt.Errorf("invalid allocation request: unknown device: %s", id)
}
}
responses.ContainerResponses = append(responses.ContainerResponses, &response)
}
return &responses, nil
}
Allocate()
方法接收到请求的DevicesIDs后,会返回NVIDIA_VISIBLE_DEVICES
环境变量给kubelet。该变量是NVIDIA docker用来设置容器可使用哪些GPU卡。关于NVIDIA docker容器如何支持使用GPU,可见NVIDIA Docker CUDA容器化原理分析。
nvidia-device-plugin是使用环境变量来操作容器,Kubernetes device plugin API 共提供了以下几种方式来设置容器
代码语言:txt复制type ContainerAllocateResponse struct {
// List of environment variable to be set in the container to access one of more devices.
Envs map[string]string `protobuf:"bytes,1,rep,name=envs" json:"envs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
// Mounts for the container.
Mounts []*Mount `protobuf:"bytes,2,rep,name=mounts" json:"mounts,omitempty"`
// Devices for the container.
Devices []*DeviceSpec `protobuf:"bytes,3,rep,name=devices" json:"devices,omitempty"`
// Container annotations to pass to the container runtime
Annotations map[string]string `protobuf:"bytes,4,rep,name=annotations" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
其中Envs表示环境变量,如nvidia-device-plugin就是通过这个来指定容器可运行的GPU卡。Devices则对应容器的—device
,可通过这个来设置容器访问host上的/dev。
GPU分卡问题
从上面可知,Kubernetes其实就是通过docker的NVIDIA_VISIBLE_DEVICES
来管理pod可运行的GPU。当在使用中,会发现没法正确分卡,所有的容器都跑在了GPU 0卡上。主要原因有以下几种:
1)当pod通过resource管理机制使用GPU资源(resources.limits.nvidia.com/gpu: 1)的同时,又给container配置了NVIDIA_VISIBLE_DEVICES环境变量,会导致NVIDIA_VISIBLE_DEVICES值冲突
如上配置创建了一个pod,使用docker inspect CONTAINERID查看相应容器配置
NVIDIA_VISIBLE_DEVICES=all覆盖了上一个NVIDIA_VISIBLE_DEVICES的值,导致没法正确分卡。
2)特权模式下,docker的NVIDIA_VISIBLE_DEVICES会失效,所有GPU卡对容器皆可见,这时容器默认会运行在第0张卡,这会导致Kubernetes没法实现分卡功能。
3)需要注意的是,目前nvidia-device-plugin是通过NVIDIA_VISIBLE_DEVICES来控制容器可使用的GPU卡,但docker-ce 19.03版本之后不再支持该参数,而是引入 DeviceRequests,所以在kubernetes升级docker时需要特别注意下。
GPU资源合理分配问题
通过Kubernetes的resource管理机制,我们可以为pod分配要运行的GPU资源。不过因为对扩展资源,Kubernetes只能分配整数资源,所以如果一个node上只有2张GPU卡,那意味着最多只能运行2个pod。如下,在一台只有2张GPU卡的机子上,运行一个deployment,4个实例只有2个能运行成功。
要解决这个问题有一种方法是绕过Kubernetes的resource机制,即不要在containers.[*].resources.limits
中声明使用GPU资源,然后在containers.[*].env
中配置NVIDIA_VISIBLE_DEVICES
环境变量来指定容器可使用的GPU,这样就可以解决运行GPU类型pod的数量。
不过该方法有个问题,就是没办法实现自动对GPU资源合理分配。比如一个机子上有多张GPU卡,那使用该方法时,如配置NVIDIA_VISIBLE_DEVICES
为all,默认下所有的pod都会运行在第0张GPU卡上,这会导致其他GPU卡浪费。当然如果不嫌麻烦的话,可以手动为每个pod配置不同的NVIDIA_VISIBLE_DEVICES
,这意味着需要人工处理GPU资源分配问题。
GPU虚拟化简单实现
要想解决GPU资源合理分配问题,业界有提出GPU虚拟化技术,这里就先不展开了。不过如果只是想解决运行pod数量的问题,且保证每张GPU卡都能被使用到,那有一种简单的方式,在上报DeviceID给kubelet之前先将设备虚拟化,也就是上报给kubelet的实际是虚拟的DeviceID,然后在kubelet调用Allocate()
请求获取某个DeviceID信息时再将该DeviceID转换为对应的实际DeviceID。具体实现如下
// ListAndWatch lists devices and update that list according to the health status
func (m *NvidiaDevicePlugin) ListAndWatch(e *pluginapi.Empty, s pluginapi.DevicePlugin_ListAndWatchServer) error {
//将GPU设备虚拟化后再上报
s.Send(&pluginapi.ListAndWatchResponse{Devices: virtualDevices(m.devs)})
for {
select {
case <-m.stop:
return nil
case d := <-m.health:
// FIXME: there is no way to recover from the Unhealthy state.
d.Health = pluginapi.Unhealthy
s.Send(&pluginapi.ListAndWatchResponse{Devices: virtualDevices(m.devs)})
}
}
}
//虚拟化设备,对每个设备按10倍虚拟化
func virtualDevices(devs []*pluginapi.Device)[]*pluginapi.Device {
var virtualDevs []*pluginapi.Device
for i := 0; i < 10; i {
for _, dev := range devs {
virtualDev := *dev
virtualDev.ID = fmt.Sprintf("-%d", i)
virtualDevs = append(virtualDevs, &virtualDev)
}
}
return virtualDevs
}
代码语言:txt复制// Allocate which return list of devices.
func (m *NvidiaDevicePlugin) Allocate(ctx context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
devs := m.devs
responses := pluginapi.AllocateResponse{}
for _, req := range reqs.ContainerRequests {
//获取虚拟设备ID对应的实际设备ID
var deviceIds []string
inMap := make(map[string]bool)
for _, deviceId := range req.DevicesIDs {
devId := deviceId[:strings.LastIndex(deviceId, "-")]
if _,ok := inMap[devId]; ok {
continue
}
deviceIds = append(deviceIds, devId)
inMap[devId] = true
}
response := pluginapi.ContainerAllocateResponse{
Envs: map[string]string{
"NVIDIA_VISIBLE_DEVICES": strings.Join(deviceIds, ","),
},
}
for _, id := range deviceIds {
if !deviceExists(devs, id) {
return nil, fmt.Errorf("invalid allocation request: unknown device: %s", id)
}
}
responses.ContainerResponses = append(responses.ContainerResponses, &response)
}
return &responses, nil
}
在只有2张GPU卡的机子上创建4个pod实例,查看如下
4个pod实例都创建成功,并且分布在2张GPU卡上。
参考
https://github.com/NVIDIA/k8s-device-plugin
https://github.com/kubernetes/community/blob/master/contributors/design-proposals/resource-management/device-plugin.md