Kubernetes 多卡GPU使用和分析

2019-09-01 16:36:06 浏览数 (1)

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和健康状态

代码语言:txt复制
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对象,如下

代码语言:txt复制
// 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()的实现

代码语言:txt复制
// 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

代码语言:txt复制
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()的实现

代码语言: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 {
		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。具体实现如下

代码语言:txt复制
// 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

0 人点赞