前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Kubernetes 多卡GPU使用和分析

Kubernetes 多卡GPU使用和分析

原创
作者头像
langwu 吴英文
发布2019-09-01 16:36:06
9.7K1
发布2019-09-01 16:36:06
举报
文章被收录于专栏:lang的专栏

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Kubernetes中GPU使用
  • nvidia-device-plugin实现分析
  • GPU分卡问题
  • GPU资源合理分配问题
  • GPU虚拟化简单实现
  • 参考
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档