---
# System prepended metadata

title: 使用Karpenter通过时间切片管理GPU节点

---

# 使用Karpenter通过时间切片管理GPU节点

## 介绍

在机器学习领域，我们经常使用 GPU 来加速计算工作负载。但现在的企业和开发者都更热衷于“上云”。有了云计算，使用云服务，用多少付多少，也就能降低运营成本了。

当你拥有数十个在不同时间段要用到 GPU 的应用程序时，如何以更低成本在云服务器中调度资源，就会是一件非常重要的事情。

那么，如何优化云服务中 GPU 的使用成本呢？在使用虚拟机时，哪怕你不需要 7x24 的服务，你也必须持续为所有的设备付费。相比于虚拟机，容器拥有更高的资源使用效率，作为容器界的扛把子，kubernetes 提供了弹性的节点缩放方式。

因为我使用了 eks，所以本文选择 Karpenter 作为节点缩放器。Karpenter 是一个为 Kubernetes 构建的开源自动扩缩容项目，你可以通过此 [文档](https://karpenter.sh/) 了解更多关于 Karpenter 的信息。

想要管理多个 GPU 节点，还需要用到 NVIDIA 的 [k8s 插件](https://github.com/NVIDIA/k8s-device-plugin)。这是一个 Daemonset(守护进程)，提供了以下自动化的功能：

- 公开集群每个节点上的 GPU 数量
- 实时追踪 GPU 的运行状况
- 在 Kubernetes 集群中运行启用 GPU 的容器

它还支持 [时间切片](https://github.com/NVIDIA/k8s-device-plugin#shared-access-to-gpus-with-cuda-time-slicing)。因此，用户可以在 Pod （Kubernetes 的最小调度对象）之间共享 GPU，从而节省成本。

Karpenter 本身也为节点提供了自动缩放功能，也就是说，只有在需要算力时，才会创建 GPU 实例，并且可以根据实际需求修改应用实例的调度规则。除了降低成本之外，也能更灵活地把 GPU 资源调度到 kubernetes 集群中的应用程序。


## 架构

![vYVi0U.png](https://s1.ax1x.com/2022/08/12/vYVi0U.png)

<center>基础结构</center>

![vYVF7F.png](https://s1.ax1x.com/2022/08/12/vYVF7F.png)

<center>组件</center>

这个架构非常简单易懂：应用程序选择了一个带有选择器(selector)的  karpenter 制备器(provisioner)，接着，karpenter 制备器根据启动模板创建节点。

## 部署

接下来的问题是如何去部署它。有一些细节需要考虑清楚：

- 如何将 NVIDIA k8s 插件部署到仅有 GPU 的节点
- 如何配置共享的 GPU 节点以使用时间切片，而不影响其他节点
- 如何在启动模板中自动更新节点 AMI(Amazon Machine Image)，以便节点可以使用最新的镜像
- 如何设置 karpenter 制备器

下面我将逐一讲解。

首先，安装 karpenter，并使用 terraform 设置制备器。也可以参考官方文档，在 eks 中手动安装 karpenter。如果你已经有 eks 以及karpenter，可以跳过这一步。

不妨参考这个 [GitHub repo](https://github.com/tarrantro/terraform)。

### 制备器

```hcl
resource "kubectl_manifest" "karpenter_provisioner_gpu_shared" {
  yaml_body = <<-YAML
  apiVersion: karpenter.sh/v1alpha5
  kind: Provisioner
  metadata:
    name: gpu-shared
  spec:
    ttlSecondsAfterEmpty: 300
    labels:
      jina.ai/node-type: gpu-shared
      jina.ai/gpu-type: nvidia
      nvidia.com/device-plugin.config: shared_gpu
    requirements:
      - key: node.kubernetes.io/instance-type
        operator: In
        values: ["g4dn.xlarge", "g4dn.2xlarge", "g4dn.4xlarge", "g4dn.12xlarge"]
      - key: karpenter.sh/capacity-type
        operator: In
        values: ["spot", "on-demand"]
      - key: kubernetes.io/arch
        operator: In
        values: ["amd64"]
    taints:
      - key: nvidia.com/gpu-shared
        effect: "NoSchedule"
    limits:
      resources:
        cpu: 1000
    provider:
      launchTemplate: "karpenter-gpu-shared-${local.cluster_name}"
      subnetSelector:
        karpenter.sh/discovery: ${local.cluster_name}
      tags:
        karpenter.sh/discovery: ${local.cluster_name}
    ttlSecondsAfterEmpty: 30
  YAML

  depends_on = [
    helm_release.karpenter
  ]
}

resource "kubectl_manifest" "karpenter_provisioner_gpu" {
  yaml_body = <<-YAML
  apiVersion: karpenter.sh/v1alpha5
  kind: Provisioner
  metadata:
    name: gpu
  spec:
    ttlSecondsAfterEmpty: 300
    labels:
      jina.ai/node-type: gpu
      jina.ai/gpu-type: nvidia
    requirements:
      - key: node.kubernetes.io/instance-type
        operator: In
        values: ["g4dn.xlarge", "g4dn.2xlarge", "g4dn.4xlarge", "g4dn.12xlarge"]
      - key: karpenter.sh/capacity-type
        operator: In
        values: ["spot", "on-demand"]
      - key: kubernetes.io/arch
        operator: In
        values: ["amd64"]
    taints:
      - key: nvidia.com/gpu
        effect: "NoSchedule"
    limits:
      resources:
        cpu: 1000
    provider:
      launchTemplate: "karpenter-gpu-${local.cluster_name}"
      subnetSelector:
        karpenter.sh/discovery: ${local.cluster_name}
      tags:
        karpenter.sh/discovery: ${local.cluster_name}
    ttlSecondsAfterEmpty: 30
  YAML

  depends_on = [
    helm_release.karpenter
  ]
}
```
上述两份制备器（provisioner）配置使Kapenter通过启动模板（launch template）来初始化对应实例，并添加不同标签（labels）和污点（taints）。

### 启动模板（仅有GPU）

```hcl
resource "kubectl_manifest" "karpenter_provisioner_gpu" {
  yaml_body = <<-YAML
  apiVersion: karpenter.sh/v1alpha5
  kind: Provisioner
  metadata:
    name: gpu
  spec:
    ttlSecondsAfterEmpty: 300
    labels:
      jina.ai/node-type: gpu
      jina.ai/gpu-type: nvidia
    requirements:
      - key: node.kubernetes.io/instance-type
        operator: In
        values: ["g4dn.xlarge", "g4dn.2xlarge", "g4dn.4xlarge", "g4dn.12xlarge"]
      - key: karpenter.sh/capacity-type
        operator: In
        values: ["spot", "on-demand"]
      - key: kubernetes.io/arch
        operator: In
        values: ["amd64"]
    taints:
      - key: nvidia.com/gpu
        effect: "NoSchedule"
    limits:
      resources:
        cpu: 1000
    provider:
      launchTemplate: "karpenter-gpu-${local.cluster_name}"
      subnetSelector:
        karpenter.sh/discovery: ${local.cluster_name}
      tags:
        karpenter.sh/discovery: ${local.cluster_name}
    ttlSecondsAfterEmpty: 30
  YAML

  depends_on = [
    helm_release.karpenter
  ]
}
```

接下来，我们需要部署具有时间切片配置和默认配置的 NVIDIA k8s 插件，并设置节点选择器，以便 daemonset 仅在 GPU 实例上运行。

### nvdp.yml

```yaml
config:
  # ConfigMap name if pulling from an external ConfigMap
  name: ""
  # Set of named configs to build an integrated ConfigMap from
  map: 
    default: |-
      version: v1
      flags:
        migStrategy: "none"
        failOnInitError: true
        nvidiaDriverRoot: "/"
        plugin:
          passDeviceSpecs: false
          deviceListStrategy: envvar
          deviceIDStrategy: uuid
    shared_gpu: |-
      version: v1
      flags:
        migStrategy: "none"
        failOnInitError: true
        nvidiaDriverRoot: "/"
        plugin:
          passDeviceSpecs: false
          deviceListStrategy: envvar
          deviceIDStrategy: uuid
      sharing:
        timeSlicing:
          renameByDefault: false
          resources:
          - name: nvidia.com/gpu
            replicas: 10
nodeSelector: 
  jina.ai/gpu-type: nvidia
```

运行下述命令来安装 NVIDIA 的 k8s 插件：

```shell
$ helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
$ helm repo update
$ helm upgrade -i nvdp nvdp/nvidia-device-plugin \
  --namespace nvidia-device-plugin \
  --create-namespace -f nvdp.yaml
```

再之后，使用 nodeSelector 和 toleration 部署应用程序。

### gpu.yml

```yaml
kind: Deployment
apiVersion: apps/v1
metadata:
  name: test-gpu
  labels:
    app: gpu
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gpu
  template:
    metadata:
      labels:
        app: gpu
    spec:
      nodeSelector:
        jina.ai/node-type: gpu
        karpenter.sh/provisioner-name: gpu
      tolerations:
      - key: nvidia.com/gpu
        operator: Exists
        effect: NoSchedule
      containers:
      - name: gpu-container
        image: tensorflow/tensorflow:latest-gpu
        imagePullPolicy: Always
        command: ["python"]
        args: ["-u", "-c", "import tensorflow"]
        resources:
          limits:
            nvidia.com/gpu: 1
```

### gpu-shared.yml

```yaml
kind: Deployment
apiVersion: apps/v1
metadata:
  name: test-gpu-shared
  labels:
    app: gpu-shared
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gpu-shared
  template:
    metadata:
      labels:
        app: gpu-shared
    spec:
      nodeSelector:
        jina.ai/node-type: gpu-shared
        karpenter.sh/provisioner-name: gpu-shared
      tolerations:
      - key: nvidia.com/gpu-shared
        operator: Exists
        effect: NoSchedule
      containers:
      - name: gpu-container
        image: tensorflow/tensorflow:latest-gpu
        imagePullPolicy: Always
        command: ["python"]
        args: ["-u", "-c", "import tensorflow"]
        resources:
          limits:
            nvidia.com/gpu: 1
```

现在，如果部署两个 YAML 文件，你将在 AWS 控制台中看到制备的两个节点，或者通过 `kubectl get nodes — show-labels`命令查看。在每个节点上运行 nvidia-k8s-plugin 后，就可以在应用程序中进行测试。

## 参考资料

[Using Karpenter to manage GPU nodes with time-slicing](https://medium.com/jina-ai/using-karpenter-to-manage-gpu-nodes-with-time-slicing-129098a72cb6)（原文）

[Improving GPU Utilization in Kubernetes](https://developer.nvidia.com/blog/improving-gpu-utilization-in-kubernetes/)

[AWStip](https://awstip.com/)

