Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# vGPU scheduler for Kubernetes


## 目录

- [关于](#关于)
- [使用场景](#使用场景)
- [调度策略](#调度策略)
- [性能测试](#性能测试)
- [功能](#功能)
- [实验性功能](#实验性功能)
- [已知问题](#已知问题)
- [开发计划](#开发计划)
- [安装要求](#安装要求)
- [快速入门](#快速入门)
- [GPU节点准备](#GPU节点准备)
- [Kubernetes开启vGPU支持](#Kubernetes开启vGPU支持)
- [运行GPU任务](#运行GPU任务)
- [测试](#测试)
- [问题反馈及代码贡献](#问题反馈及代码贡献)

## 关于

**k8s vGPU scheduler** 基于4pd-k8s-device-plugin插件([4paradigm/k8s-device-plugin](https://github.com/4paradigm/k8s-device-plugin)),在保留原功能的基础上,添加了调度模块,以实现多个GPU节点间的负载均衡。k8s vGPU scheduler在原有显卡分配方式的基础上,可以进一步根据显存和算力来切分显卡。在k8s集群中,基于这些切分后的vGPU进行调度,使不同的容器可以安全的共享同一张物理GPU,提高GPU的利用率。此外,插件还可以对显存做虚拟化处理(使用到的显存可以超过物理上的显存),运行一些超大显存需求的任务,或提高共享的任务数,可参考[性能测试报告](#性能测试)。

## 使用场景

1. 需要定制GPU申请的场合,如申请特定大小的vGPU,每个vGPU使用特定比例的算力。
2. 在多个GPU节点组成的集群中,任务需要根据自身的显卡需求分配到合适的节点执行。
3. 显存、计算单元利用率低的情况,如在一张GPU卡上运行10个tf-serving。
4. 需要大量小显卡的情况,如教学场景把一张GPU提供给多个学生使用、云平台提供小GPU实例。
5. 物理显存不足的情况,可以开启虚拟显存,如大batch、大模型的训练。

## 调度策略

调度策略为,在保证显存和算力满足需求的GPU中,优先选择任务数最少的GPU执行任务,这样做可以使任务均匀分配到所有的GPU中

## 性能测试

见[k8s-device-plugin的性能测试部分](https://github.com/4paradigm/k8s-device-plugin/blob/master/README_cn.md#性能测试)

## 功能

- 指定每张物理GPU切分的最大vGPU的数量
- 限制vGPU的显存
- 限制vGPU的计算单元
- 对已有程序零改动

## 实验性功能

- 虚拟显存

vGPU的显存总和可以超过GPU实际的显存,这时候超过的部分会放到内存里,对性能有一定的影响。

## 已知问题

- 目前仅支持计算任务,不支持视频编解码处理。
- 暂时不支持MIG

## 开发计划

- 支持视频编解码处理
- 支持Multi-Instance GPUs (MIG)

## 安装要求

* NVIDIA drivers >= 384.81
* nvidia-docker version > 2.0
* docker已配置nvidia作为默认runtime
* Kubernetes version >= 1.10

## 快速入门

### GPU节点准备

以下步骤要在所有GPU节点执行。这份README文档假定GPU节点已经安装NVIDIA驱动和`nvidia-docker`套件。

注意你需要安装的是`nvidia-docker2`而非`nvidia-container-toolkit`。因为新的`--gpus`选项kubernetes尚不支持。安装步骤举例:

```
# 加入套件仓库
$ distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
$ curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
$ curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list

$ sudo apt-get update && sudo apt-get install -y nvidia-docker2
$ sudo systemctl restart docker
```

你需要在节点上将nvidia runtime做为你的docker runtime预设值。我们将编辑docker daemon的配置文件,此文件通常在`/etc/docker/daemon.json`路径:

```
{
"default-runtime": "nvidia",
"runtimes": {
"nvidia": {
"path": "/usr/bin/nvidia-container-runtime",
"runtimeArgs": []
}
}
}
```

> *如果 `runtimes` 字段没有出现, 前往的安装页面执行安装操作 [nvidia-docker](https://github.com/NVIDIA/nvidia-docker)*

### Kubernetes开启vGPU支持

当你在所有GPU节点完成前面提到的准备动作,如果Kubernetes有已经存在的NVIDIA装置插件,需要先将它移除。然后,你需要下载整个项目,并进入deployments文件夹

```
$ git clone https://gitlab.4pd.io/vgpu/k8s-vgpu.git
$ cd k8s-vgpu/deployments
```

在这个deployments文件中, 你可以在 `values.yaml/devicePlugin/extraArgs` 中使用以下的客制化参数:

* `device-split-count:`
整数类型,预设值是10。GPU的分割数,每一张GPU都不能分配超过其配置数目的任务。若其配置为N的话,每个GPU上最多可以同时存在N个任务。
* `device-memory-scaling:`
浮点数类型,预设值是1。NVIDIA装置显存使用比例,可以大于1(启用虚拟显存,实验功能)。对于有*M​*显存大小的NVIDIA GPU,如果我们配置`device-memory-scaling`参数为*S*,在部署了我们装置插件的Kubenetes集群中,这张GPU分出的vGPU将总共包含 *S \* M*显存。每张vGPU的显存大小也受`device-split-count`参数影响。在先前的例子中,如果`device-split-count`参数配置为*K*,那每一张vGPU最后会取得 *S \* M / K* 大小的显存。

除此之外,你可以在 `values.yaml/scheduler/extender/extraArgs` 中使用以下客制化参数:

* `default-mem:`
整数类型,预设值为5000,表示不配置显存时使用的默认显存大小,单位为MB

* `default-cores:`
整数类型(0-100),默认为0,表示不配置显卡使用比例时默认的使用比例。若设置为0,则代表任务可能会被分配到任一满足显存需求的GPU中,若设置为100,代表该任务独享整张显卡

配置完成后,随后使用helm安装整个chart

```
$ helm install vgpu 4pd-vgpu
```

通过kubectl get pods指令看到vgpu-4pd-vgpu-device-plugin与vgpu-4pd-vgpu-scheduler两个pod即为安装成功

```
$ kubectl get pods
```

### 运行GPU任务

NVIDIA vGPUs 现在能透过资源类型`nvidia.com/gpu`被容器请求:

```
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
containers:
- name: ubuntu-container
image: ubuntu:18.04
command: ["bash", "-c", "sleep 86400"]
resources:
limits:
nvidia.com/gpu: 2 # 请求2个vGPUs
nvidia.com/gpumem: 3000 # 每个vGPU申请3000m显存 (可选)
nvidia.com/gpucores: 30 # 每个vGPU的算力为30%实际显卡的算力 (可选)
```

现在你可以在容器执行`nvidia-smi`命令,然后比较vGPU和实际GPU显存大小的不同。

## 测试

- TensorFlow 1.14.0/2.4.1
- torch1.1.0
- mxnet 1.4.0
- mindspore 1.1.1

以上框架均通过测试。

## 反馈和参与

* bug、疑惑、修改欢迎提在 [Github Issues](https://github.com/4paradigm/k8s-device-plugin/issues/new)
* 想了解更多或者有想法可以参与到[Discussions](https://github.com/4paradigm/k8s-device-plugin/discussions)和[slack](https://join.slack.com/t/k8s-device-plugin/shared_invite/zt-oi9zkr5c-LsMzNmNs7UYg6usc0OiWKw)交流

126 changes: 64 additions & 62 deletions cmd/scheduler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,84 +16,86 @@
package main

import (
"4pd.io/k8s-vgpu/pkg/util"
"4pd.io/k8s-vgpu/pkg/version"
"net"
"net/http"
"net"
"net/http"

pb "4pd.io/k8s-vgpu/pkg/api"
"4pd.io/k8s-vgpu/pkg/scheduler"
"4pd.io/k8s-vgpu/pkg/scheduler/config"
"4pd.io/k8s-vgpu/pkg/scheduler/routes"
"github.com/julienschmidt/httprouter"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"k8s.io/klog/v2"
"4pd.io/k8s-vgpu/pkg/util"
"4pd.io/k8s-vgpu/pkg/version"

pb "4pd.io/k8s-vgpu/pkg/api"
"4pd.io/k8s-vgpu/pkg/scheduler"
"4pd.io/k8s-vgpu/pkg/scheduler/config"
"4pd.io/k8s-vgpu/pkg/scheduler/routes"
"github.com/julienschmidt/httprouter"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"k8s.io/klog/v2"
)

//var version string

var (
tlsKeyFile string
tlsCertFile string
rootCmd = &cobra.Command{
Use: "scheduler",
Short: "kubernetes vgpu scheduler",
Run: func(cmd *cobra.Command, args []string) {
start()
},
}
tlsKeyFile string
tlsCertFile string
rootCmd = &cobra.Command{
Use: "scheduler",
Short: "kubernetes vgpu scheduler",
Run: func(cmd *cobra.Command, args []string) {
start()
},
}
)

func init() {
rootCmd.Flags().SortFlags = false
rootCmd.PersistentFlags().SortFlags = false

rootCmd.Flags().StringVar(&config.GrpcBind, "grpc_bind", "127.0.0.1:9090", "grpc server bind address")
rootCmd.Flags().StringVar(&config.HttpBind, "http_bind", "127.0.0.1:8080", "http server bind address")
rootCmd.Flags().StringVar(&tlsCertFile, "cert_file", "", "tls cert file")
rootCmd.Flags().StringVar(&tlsKeyFile, "key_file", "", "tls key file")
rootCmd.Flags().StringVar(&config.SchedulerName, "scheduler-name", "", "the name to be added to pod.spec.schedulerName if not empty")
rootCmd.Flags().SortFlags = false
rootCmd.PersistentFlags().SortFlags = false

rootCmd.PersistentFlags().AddGoFlagSet(util.GlobalFlagSet())
rootCmd.AddCommand(version.VersionCmd)
rootCmd.Flags().StringVar(&config.GrpcBind, "grpc_bind", "127.0.0.1:9090", "grpc server bind address")
rootCmd.Flags().StringVar(&config.HttpBind, "http_bind", "127.0.0.1:8080", "http server bind address")
rootCmd.Flags().StringVar(&tlsCertFile, "cert_file", "", "tls cert file")
rootCmd.Flags().StringVar(&tlsKeyFile, "key_file", "", "tls key file")
rootCmd.Flags().StringVar(&config.SchedulerName, "scheduler-name", "", "the name to be added to pod.spec.schedulerName if not empty")
rootCmd.Flags().Int32Var(&config.DefaultMem, "default-mem", 5000, "default gpu device memory to allocate")
rootCmd.Flags().Int32Var(&config.DefaultCores, "default-cores", 0, "default gpu core percentage to allocate")
rootCmd.PersistentFlags().AddGoFlagSet(util.GlobalFlagSet())
rootCmd.AddCommand(version.VersionCmd)
}

func start() {
sher := scheduler.NewScheduler()
sher.Start()
defer sher.Stop()
sher := scheduler.NewScheduler()
sher.Start()
defer sher.Stop()

// start grpc server
lisGrpc, _ := net.Listen("tcp", config.GrpcBind)
defer lisGrpc.Close()
s := grpc.NewServer()
pb.RegisterDeviceServiceServer(s, sher)
go func() {
err := s.Serve(lisGrpc)
if err != nil {
klog.Fatal(err)
}
}()
// start grpc server
lisGrpc, _ := net.Listen("tcp", config.GrpcBind)
defer lisGrpc.Close()
s := grpc.NewServer()
pb.RegisterDeviceServiceServer(s, sher)
go func() {
err := s.Serve(lisGrpc)
if err != nil {
klog.Fatal(err)
}
}()

// start http server
router := httprouter.New()
router.POST("/filter", routes.PredicateRoute(sher))
router.POST("/webhook", routes.WebHookRoute())
klog.Info("listen on ", config.HttpBind)
if len(tlsCertFile) == 0 || len(tlsKeyFile) == 0 {
if err := http.ListenAndServe(config.HttpBind, router); err != nil {
klog.Fatal("Listen and Serve error, ", err)
}
} else {
if err := http.ListenAndServeTLS(config.HttpBind, tlsCertFile, tlsKeyFile, router); err != nil {
klog.Fatal("Listen and Serve error, ", err)
}
}
// start http server
router := httprouter.New()
router.POST("/filter", routes.PredicateRoute(sher))
router.POST("/webhook", routes.WebHookRoute())
klog.Info("listen on ", config.HttpBind)
if len(tlsCertFile) == 0 || len(tlsKeyFile) == 0 {
if err := http.ListenAndServe(config.HttpBind, router); err != nil {
klog.Fatal("Listen and Serve error, ", err)
}
} else {
if err := http.ListenAndServeTLS(config.HttpBind, tlsCertFile, tlsKeyFile, router); err != nil {
klog.Fatal("Listen and Serve error, ", err)
}
}
}

func main() {
if err := rootCmd.Execute(); err != nil {
klog.Fatal(err)
}
if err := rootCmd.Execute(); err != nil {
klog.Fatal(err)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "4pd-vgpu.device-plugin" . }}-admission
name: {{ include "4pd-vgpu.device-plugin" . }}-monitor
rules:
- apiGroups:
- ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ metadata:
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
#name: cluster-admin
name: {{ include "4pd-vgpu.device-plugin" . }}-monitor
subjects:
- kind: ServiceAccount
name: {{ include "4pd-vgpu.device-plugin" . }}
Expand Down
4 changes: 4 additions & 0 deletions deployments/4pd-vgpu/templates/scheduler/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ data:
managedResources:
- name: {{ .Values.resourceName }}
ignoredByScheduler: true
- name: {{ .Values.resourceMem }}
ignoredByScheduler: true
- name: {{ .Values.resourceCores }}
ignoredByScheduler: true
2 changes: 2 additions & 0 deletions deployments/4pd-vgpu/templates/scheduler/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ spec:
command:
- scheduler
- --resource-name={{ .Values.resourceName }}
- --resource-mem={{ .Values.resourceMem }}
- --resource-cores={{ .Values.resourceCores }}
- --http_bind=0.0.0.0:443
- --grpc_bind=0.0.0.0:1080
- --cert_file=/tls/tls.crt
Expand Down
6 changes: 5 additions & 1 deletion deployments/4pd-vgpu/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ fullnameOverride: ""
imagePullSecrets: []
#
resourceName: "nvidia.com/gpu"
resourceMem: "nvidia.com/gpumem"
resourceCores: "nvidia.com/gpucores"
schedulerName: "4pd-scheduler"

podSecurityPolicy:
Expand All @@ -26,6 +28,8 @@ scheduler:
imagePullPolicy: Always
extraArgs:
- --debug
- --default-mem=5000
- --default-cores=0
- -v=4
podAnnotations: {}
nodeSelector: {}
Expand All @@ -52,7 +56,7 @@ devicePlugin:
monitorctrPath: /tmp/vgpu/containers
imagePullPolicy: Always
extraArgs:
- --device-split-count=2
- --device-split-count=10
- -v=4

pluginPath: /var/lib/kubelet/device-plugins
Expand Down
Loading