iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
Kubernetes

異世界生存戰記:30天煉成GKE大師系列 第 27

Day27 模型神速啟動!Local SSD 究極奧義,開啟 AI 推論的超次元通道

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20241011/201690178M8SLJrgIO.png

前言

在深度學習應用中,模型的載入速度和推理效率至關重要,尤其是在大規模部署和高併發場景下。直接從雲端儲存(例如 Google Cloud Storage,GCS)載入模型往往會成為效能瓶頸。

本文將探討如何利用 Google Kubernetes Engine (GKE) 上的本地 NVMe SSD 和 HostPath,搭配 RAID 0 配置,構建一個高效能的深度學習推理環境。

我們將逐步講解如何創建一個配備 MIG H100 GPU 的節點池,如何使用本地卷靜態預配工具設定 RAID 並格式化磁碟,以及如何利用 DaemonSet 自動將模型自動從 GCS 同步到節點的本地 NVMe SSD。最後,我們將部署一個大規模的推理任務,並分析這種方法相比直接從 GCS 載入模型的優勢,例如降低延遲、減少網路頻寬消耗、簡化 Pod 設定以及更好的資源利用。

創建 Node Pool

使用 Day3 範例創建的Cluster,再建立一個 MIG H100x8(a3-highgpu-8g)切分為1g.10gb 的 Node Pool,MIG GPU 可以看 Day25 的介紹,其中將 local_nvme_ssd_count 設定為 16,這將會自動預配本地固態硬盤容量共 16顆各 375GB 的 local ssd 硬碟。

### node-pool-variables.tf
module "gke" {
  node_pools = [
    var.h100-partition-7.config,
  ]

  node_pools_labels = {
    "${var.h100-partition-7.config.name}" = var.h100-partition-7.kubernetes_label
  }

  node_pools_taints = {
	  "${var.h100-partition-7.config.name}" = var.h100-partition-7.taints
  }

  node_pools_resource_labels = {
    "${var.h100-partition-7.config.name}" = var.h100-partition-7.node_pools_resource_labels
  }
}
### Node pool
variable "h100-partition-7" {
  default = {
    config = {
      name               = "h100-partition-7"
      machine_type       = "a3-highgpu-8g"
      accelerator_type   = "nvidia-h100-80gb"
      accelerator_count  = "8"
	    # 將每張 H100 切分成 7 份
      gpu_partition_size = "1g.10gb"
      gpu_driver_version = "LATEST"
      node_locations     = "us-central1-c"
      # 每個 Node 可以部署 256 個 Pods
      max_pods_per_node  = 256
      autoscaling        = false
      node_count         = 1
      local_ssd_count    = 0
      disk_size_gb       = 2000
      local_nvme_ssd_count = 16
      spot               = true
      disk_type          = "pd-ssd"
      image_type         = "COS_CONTAINERD"
      enable_gcfs        = false
      enable_gvnic       = false
      logging_variant    = "DEFAULT"
      auto_repair        = true
      auto_upgrade       = true
      preemptible        = false
    }
    kubernetes_label = {
      role = "h100-mig"
    }
    taints = [
          key    = "nvidia/share"
          value  = "nvidia-mig"
          effect = "NO_SCHEDULE"
    ]
  }
}

檢查磁碟分區

進入 Node VM 中使用指令 sudo fdisk -l 查看硬碟和分割區,以及它們的大小和類型。可以看到有 16 個 375 GiB 的硬碟 (nvme0~15) 以及 1.94 GiB 的開機硬碟。

$ sudo fdisk -l

### ...以上省略...

Disk /dev/nvme15n1: 375 GiB, 402653184000 bytes, 98304000 sectors
Disk model: nvme_card14
Units: sectors of 1 * 4096 = 4096 bytes
Sector size (logical/physical): 4096 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes

Disk /dev/mapper/vroot: 1.94 GiB, 2087714816 bytes, 509696 sectors
Units: sectors of 1 * 4096 = 4096 bytes
Sector size (logical/physical): 4096 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes

運行本地卷靜態預配工具

可以使用本地卷靜態預配工具自動為本地 SSD 建立 PersistentVolume。 預配工具是一個 DaemonSet,用於管理每個節點上的本地固態硬碟磁碟、為它們創建和刪除 PersistentVolume 以及在釋放 PersistentVolume 時清除本地固態硬碟磁碟上的數據。

要執行本地卷靜態預配程式,請執行以下操作:

  1. 使用 DaemonSet 設定 RAID 並對磁碟進行格式化:

    部署RAID磁碟DaemonSet。 DaemonSet 會在所有本地 SSD 磁碟上設置 RAID 0 陣列,並將設備格式化為 ext4 檔案系統。

kubectl create -f gke-daemonset-raid-disks.yaml

# gke-daemonset-raid-disks.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: gke-raid-disks
  namespace: default
  labels:
    k8s-app: gke-raid-disks
spec:
  selector:
    matchLabels:
      name: gke-raid-disks
  template:
    metadata:
      labels:
        name: gke-raid-disks
    spec:
      nodeSelector:
        cloud.google.com/gke-local-nvme-ssd: "true"
      hostPID: true
      containers:
      - name: startup-script
        image: gcr.io/google-containers/startup-script:v1
        securityContext:
          privileged: true
        env:
        - name: STARTUP_SCRIPT
          value: |
            set -o errexit
            set -o nounset
            set -o pipefail

            devices=()
            for ssd in /dev/disk/by-id/google-local-ssd-block*; do
              if [ -e "${ssd}" ]; then
                devices+=("${ssd}")
              fi
            done
            if [ "${#devices[@]}" -eq 0 ]; then
              echo "No Local NVMe SSD disks found."
              exit 0
            fi

            seen_arrays=(/dev/md/*)
            device=${seen_arrays[0]}
            echo "Setting RAID array with Local SSDs on device ${device}"
            if [ ! -e "$device" ]; then
              device="/dev/md/0"
              echo "y" | mdadm --create "${device}" --level=0 --force --raid-devices=${#devices[@]} "${devices[@]}"
            fi

            if ! tune2fs -l "${device}" ; then
              echo "Formatting '${device}'"
              mkfs.ext4 -F "${device}"
            fi

            mountpoint=/mnt/disks/raid/0
            mkdir -p "${mountpoint}"
            echo "Mounting '${device}' at '${mountpoint}'"
            mount -o discard,defaults "${device}" "${mountpoint}"
            chmod a+w "${mountpoint}"
      tolerations:
        - operator: "Exists"
          effect: NoSchedule

查看 gke-raid-disks Pod Log (kubectl logs $gke-raid-disks-Pod-Name)

$ kubectl logs $gke-raid-disks-Pod-Name

Setting RAID array with Local SSDs on device /dev/md/*
mdadm: Defaulting to version 1.2 metadata
mdadm: array /dev/md/0 started.
tune2fs: Bad magic number in super-block while trying to open /dev/md/0
tune2fs 1.47.0 (5-Feb-2023)
Formatting '/dev/md/0'
mke2fs 1.47.0 (5-Feb-2023)
Discarding device blocks: done                            
Creating filesystem with 1572335616 4k blocks and 196542464 inodes
Filesystem UUID: 7797058c-7483-45e6-85c4-c3746cfb23dc
Superblock backups stored on blocks: 
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 
        4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968, 
        102400000, 214990848, 512000000, 550731776, 644972544

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (262144 blocks): done
Writing superblocks and filesystem accounting information: done       

Mounting '/dev/md/0' at '/mnt/disks/raid/0'
!!! startup-script succeeded!

確認 Pod 中的 Shell Script 執行完成後,使用 sudo fdisk -l 指令查看磁碟分區,會發現 16 個 375 Gi 的硬碟 Raid0 後總共組成了 5.86 TiB。

$ sudo fdisk -l

### ...以上省略...

Disk /dev/md0: 5.86 TiB, 6440286683136 bytes, 1572335616 sectors
Units: sectors of 1 * 4096 = 4096 bytes
Sector size (logical/physical): 4096 bytes / 4096 bytes
I/O size (minimum/optimal): 524288 bytes / 8388608 bytes

使用 DaemonSet Auto Sync Model 到 Node HostPath

這個 DaemonSet 的目的是將 GCS 存儲桶中的 Model 定期同步到集群中每個 GPU 節點的本地 NVMe SSD RAID 設備(hostPath)的 /mnt/disks/raid/0 目錄中,每當增加新的 GPU 節點時,都會自動部署此 DaemonSet Auto Sync 的 Pod,自動將所有 GPU 節點的 hostPath 都同步 Model 上去,如下圖。以下 Model 及 GCS 使用 Day23 訓練出來的 MNIST Model

https://ithelp.ithome.com.tw/upload/images/20241011/20169017O7OGB6xK9E.png

sync-model.yaml 有以下幾個重點:

  • $K8s_Service_Account 變數改成具有存取 Model GCS 權限的 Service Account。
  • $PROJECT_ID 變數改成 Model GCS 所在的專案 ID。
  • $GCS_BUCKET_NAME 變數改成 Model GCS 名稱。
  • 需要具有主機權限,因此要加入 SYS_ADMIN 參數

之後執行 kubectl apply -f sync-model.yaml 指令

# sync-model.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: model-sync
  namespace: ai
spec:
  selector:
    matchLabels:
      app: model-sync
  template:
    metadata:
      labels:
        app: model-sync
    spec:
      # 填入剛剛創建的 K8s SA 具有 GCS 物件擁有者權限
      serviceAccountName: "$K8s_Service_Account"
      initContainers:
        - name: init
          image: busybox:1.28
          command: ['sh', '-c', 'echo "Suspend for 60 seconds to wait for the RAID device to be ready." && sleep 60']
      containers:
        - name: model-sync
          image: google/cloud-sdk:stable
          imagePullPolicy: IfNotPresent
          securityContext:
            privileged: true
            capabilities:
              add:
                - SYS_ADMIN
          resources:
            requests:
              cpu: "500m"
              memory: "1Gi"
            limits:
              cpu: "1"
              memory: "2Gi"
          env:
            - name: PROJECT_ID
              value: "$PROJECT_ID"
            - name: GCS_BUCKET_NAME
              value: "$GCS_BUCKET_NAME"
            - name: DESTINATION
              value: "/model-sync-volume"
          command:
            - /bin/bash
            - -c
            - |
              while :
              do 
                if [ ! -d $DESTINATION ]; then
                    mkdir -p $DESTINATION
                fi
                echo "Starts download model weights..."

                gcloud config set project $PROJECT_ID
                mkdir -p $DESTINATION
                gcloud storage rsync --recursive gs://$GCS_BUCKET_NAME $DESTINATION

                echo "Process finished"
                sleep 600
              done
          volumeMounts:
          - name: model-sync-volume
            mountPropagation: "Bidirectional"
            mountPath: "/model-sync-volume"
      volumes:
        - name: model-sync-volume
          hostPath:
            path: /mnt/disks/raid/0
            type: DirectoryOrCreate
      nodeSelector:
        cloud.google.com/gke-local-nvme-ssd: "true"
      tolerations:
        - operator: "Exists"
          effect: NoSchedule

SSH 進入 GPU 的 Node 中查看 /mnt/disks/raid/0 目錄下是否存在同步的 Model

gke-demo-ai-cluster-h100-partition-7 /tmp $ cd /mnt/disks/raid/0

gke-demo-ai-cluster-h100-partition-7 /mnt/disks/raid/0 $ ls -al
total 32
drwxrwxrwx 6 root root  4096 Oct  8 11:47 .
drwxr-xr-x 3 root root    60 Oct  7 11:48 ..
drwx------ 2 root root 16384 Oct  7 11:48 lost+found
drwxr-xr-x 2 root root  4096 Oct  8 11:47 mnist_predict
drwxr-xr-x 2 root root  4096 Oct  8 11:47 mnist_saved_model
drwxr-xr-x 4 root root  4096 Oct  8 11:47 tensorflow-mnist-example

部署大量推論 Workload Job

使用 Day23MNIST 數據部署推理 Job,將以下值改寫:

  • spec.parallelism 改成 60,最多可以同時執行 60 個 Pod
  • cloud.google.com/gke-accelerator: nvidia-h100-80gb 使用我們創建的 MIG H100 Node
  • volumes將剛剛 Model Sync 的 hostPath 掛載進來
  • volumeMounts將掛載進來的 hostPath 下的全部目錄掛載到 Pod 內的 /data 目錄下
  • spec.serviceAccountName替換成剛剛創建的 K8s SA 具有 GCS 物件擁有者權限

使用指令kubectl apply -f mnist-inference-job.yaml 部署 mnist-inference-job

# mnist-inference-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: mnist-inference-job
  namespace: ai
spec:
  ttlSecondsAfterFinished: 3600 # Job 將在 3600 秒後刪除
  parallelism: 60 # 同時執行 60 個副本
  template:
    metadata:
      name: mnist
    spec:
      # 替換成剛剛創建的 K8s SA 具有 GCS 物件擁有者權限
      serviceAccountName: $Workload_Identity_ServiceAccount
      nodeSelector: # 改成所使用的機器標籤,這裡使用 nvidia-h100-80gb
        cloud.google.com/gke-accelerator: nvidia-h100-80gb
      tolerations:
      - key: "nvidia.com/gpu"
        operator: "Exists"
        effect: "NoSchedule"
      containers:
      - name: tensorflow
        image: tensorflow/tensorflow:latest-gpu
        command: ["/bin/bash", "-c", "--"]
        args: ["cd /data/tensorflow-mnist-example; pip install -r requirements.txt; python tensorflow_mnist_batch_predict.py"]
        resources:
          limits:
            cpu: "1"
            memory: 3Gi
            nvidia.com/gpu: "1"
        volumeMounts:
        # 將掛載進來的 hostPath 下的全部目錄掛載到 Pod 內的 /data 目錄下
          - name: hostpath
            mountPath: /data
            readOnly: true
      volumes:
      # 將剛剛 Model Sync 的 hostPath 掛載進來
        - name: hostpath
          hostPath:
            path: /mnt/disks/raid/0
            type: DirectoryOrCreate
      restartPolicy: "Never"

因為我們將每張 H100 都切成 7 份,一台 a3-highgpu-8g 總共有 8 張 H100,所以可以同時部署 7*8=56 個 Pods。

至於此種方法比起 Day23 將 GCS 直接掛到 Pod中又有哪些優點呢?

  1. 性能提升:
  • 降低延遲: 本地 NVMe SSD 的讀取速度遠高於從 GCS 下載,這顯著減少了模型載入時間和推理延遲,尤其是在高併發推理場景下,性能提升非常明顯。
  • GCS 掛載 Pods 的數量上限**:**當有多個節點時,掛載同一個 GCS Bucket 的 Pod 數量上百時,Pod 會開始發生無法掛載 GCS 的清況。
  • 減少網路頻寬消耗: 避免了每個 Pod 都從 GCS 下載模型,大大減少了網路頻寬消耗,降低了網路瓶頸的風險,也節省了 GCS 的 egress 流量費用。
  1. 簡化 Pod 設定:
  • 無需 GCS 權限: Pod 不需要配置 GCS 訪問憑據,簡化了 Pod 的配置和部署,也提高了安全性。
  1. 更好的資源利用:
  • 並行讀取: RAID 0 配置可以提高讀取性能,允許多個 Pod 並行讀取模型數據,進一步提升推理效率。

總結

筆者之所以會寫出這篇文章,也是因為在工作中部署大量的推論用 Ai Model 時遇到的痛點,剛開始服務的 Pod 還不多時可以使用 GCS Sidecar 掛載到每個 Pod 中部署,但是當 GKE 的規模越來越大時,同樣 Model 的 Pod 上百上千時,開始遇到 GCS 網路流量限制的問題,Pod 啟動非常緩慢,甚至有些 Pod 無法成功掛載 GCS,導致服務中斷。

因此,才開始研究如何將 Model 同步到本地硬碟中,並使用 HostPath 掛載到 Pod 中,解決了 GCS 網路流量限制的問題,也大幅提升了 Pod 啟動速度和推理效率。

總而言之,使用本地 NVMe SSD RAID 和 HostPath 掛載模型數據是一種有效的優化方案,可以顯著提升推理性能。 但是,也需要仔細考慮其引入的挑戰,並根據實際情況選擇合適的解決方案。 隨著技術的發展,未來會有更多更完善的方案出現,進一步簡化模型部署和管理。

參考文件


上一篇
Day26 GPU 軟體層級切割魔法 Time-Slicing & MPS
下一篇
Day28 在 GKE TPU 異世界中轉職為詠唱師:Gemma LLM 部署實戰
系列文
異世界生存戰記:30天煉成GKE大師30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言