【Docker】使用Docker Client和Docker Go SDK為容器分配GPU資源
背景
深度學習的環境配置通常是一項比較麻煩的工作,尤其是在多個用戶共享的服務器上。雖然conda集成了virtualenv這樣的工具用來隔離不同的依賴環境,但這種解決方案仍然沒辦法統一地分配計算資源。現在,我們可以通過容器技術為每個用戶創建一個屬於他們自己的容器,並為容器分配相應的計算資源。目前市面上基於容器的深度學習平台產品已經有很多了,比如超益集倫的AiMax。這款產品本身集成了非常多的功能,但如果你只是需要在容器內調用一下GPU,可以參考下面的步驟。
使用 Docker Client 調用 GPU
依賴安裝
docker run --gpu
命令依賴於 nvidia Linux 驅動和 nvidia container toolkit,如果你想查看安裝文檔請點擊這裡,本節的下文只是安裝文檔的翻譯和提示。
在Linux服務器上安裝nvidia驅動非常簡單,如果你安裝了圖形化界面的話直接在Ubuntu的「附加驅動」應用中安裝即可,在nvidia官網上也可以下載驅動。
接下來就是安裝nvidia container toolkit,我們的服務器需要滿足一些先決條件:
-
GNU/Linux x86_64 內核版本 > 3.10
-
Docker >= 19.03 (注意不是Docker Desktop,如果你想在自己的台式機上使用toolkit,請安裝Docker Engine而不是Docker Desktop,因為Desktop版本都是運行在虛擬機之上的)
-
NVIDIA GPU 架構 >= Kepler (目前RTX20系顯卡是圖靈架構,RTX30系顯卡是安培架構)
-
NVIDIA Linux drivers >= 418.81.07
然後就可以正式地在Ubuntu或者Debian上安裝NVIDIA Container Toolkit,如果你想在 CentOS 上或者其他 Linux 發行版上安裝,請參考官方的安裝文檔。
安裝 Docker
$ curl //get.docker.com | sh \
&& sudo systemctl --now enable docker
當然,這裡安裝完成後請參考官方的安裝後需要執行的一系列操作。如果安裝遇到問題,請參照官方的安裝文檔。
安裝 NVIDIA Container Toolkit¶
設置 Package Repository和GPG Key
$ distribution=$(. /etc/os-release;echo $ID$VERSION_ID) \
&& curl -fsSL //nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
&& curl -s -L //nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \
sed 's#deb //#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] //#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
請注意:如果你想安裝 NVIDIA Container Toolkit 1.6.0 之前的版本,你應該使用 nvidia-docker repository 而不是上方的 libnvidia-container repositories。
如果遇到問題請直接參考安裝手冊
安裝 nvidia-docker2 應該會自動安裝libnvidia-container-tools
libnvidia-container1
等依賴包,如果沒有安裝可以手動安裝
完成前面步驟後安裝 nvidia-docker2
$ sudo apt update
$ sudo apt install -y nvidia-docker2
重啟 Docker Daemon
$ sudo systemctl restart docker
接下來你就可以通過運行一個CUDA容器測試下安裝是否正確。
docker run --rm --gpus all nvidia/cuda:11.0.3-base-ubuntu20.04 nvidia-smi
Shell 中顯示的應該類似於下面的輸出:
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 450.51.06 Driver Version: 450.51.06 CUDA Version: 11.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 Tesla T4 On | 00000000:00:1E.0 Off | 0 |
| N/A 34C P8 9W / 70W | 0MiB / 15109MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+
--gpus
用法
注意,如果你安裝的是 nvidia-docker2 的話,它在安裝時就已經在 Docker 中註冊了 NVIDIA Runtime。如果你安裝的是 nvidia-docker ,請根據官方文檔向Docker註冊運行時。
如果你有任何疑問,請移步本節參考的文檔
可以使用以 Docker 開頭的選項或使用環境變量將 GPU 指定給 Docker CLI。此變量控制在容器內可訪問哪些 GPU。
--gpus
NVIDIA_VISIBLE_DEVICES
可能的值 | 描述 |
---|---|
0,1,2 或者 GPU-fef8089b |
逗號分割的GPU UUID(s) 或者 GPU 索引 |
all |
所有GPU都可被容器訪問,默認值 |
none |
不可訪問GPU,但可以使用驅動提供的功能 |
void 或者 empty 或者 unset |
nvidia-container-runtime will have the same behavior as (i.e. neither GPUs nor capabilities are exposed)runc |
使用該選項指定 GPU 時,應使用該參數。參數的格式應封裝在單引號中,後跟要枚舉到容器的設備的雙引號。例如:將 GPU 2 和 3 枚舉到容器。
--gpus '"device=2,3"'
使用 NVIDIA_VISIBLE_DEVICES 變量時,可能需要設置
--runtime nvidia
除非已設置為默認值。
-
設置一個啟用CUDA支持的容器
$ docker run --rm --gpus all nvidia/cuda nvidia-smi
-
指定 nvidia 作為運行時,並指定變量
NVIDIA_VISIBLE_DEVICES
$ docker run --rm --runtime=nvidia \ -e NVIDIA_VISIBLE_DEVICES=all nvidia/cuda nvidia-smi
-
為啟動的容器分配2個GPU
$ docker run --rm --gpus 2 nvidia/cuda nvidia-smi
-
為容器指定使用索引為1和2的GPU
$ docker run --gpus '"device=1,2"' \ nvidia/cuda nvidia-smi --query-gpu=uuid --format=csv
uuid GPU-ad2367dd-a40e-6b86-6fc3-c44a2cc92c7e GPU-16a23983-e73e-0945-2095-cdeb50696982
-
也可以使用
NVIDIA_VISIBLE_DEVICES
$ docker run --rm --runtime=nvidia \ -e NVIDIA_VISIBLE_DEVICES=1,2 \ nvidia/cuda nvidia-smi --query-gpu=uuid --format=csv
uuid GPU-ad2367dd-a40e-6b86-6fc3-c44a2cc92c7e GPU-16a23983-e73e-0945-2095-cdeb50696982
-
使用
nvidia-smi
查詢 GPU UUID 然後將其指定給容器$ nvidia-smi -i 3 --query-gpu=uuid --format=csv
uuid GPU-18a3e86f-4c0e-cd9f-59c3-55488c4b0c24
docker run --gpus device=GPU-18a3e86f-4c0e-cd9f-59c3-55488c4b0c24 \ nvidia/cuda nvidia-smi
關於在容器內使用驅動程序的功能的設置,以及其他設置請參閱這裡。
使用 Docker Go SDK 為容器分配 GPU
使用 NVIDIA/go-nvml
獲取 GPU 信息
NVIDIA/go-nvml
提供NVIDIA Management Library API (NVML) 的Go語言綁定。目前僅支持Linux,倉庫地址。
下面的演示代碼獲取了 GPU 的各種信息,其他功能請參考 NVML 和 go-nvml 的官方文檔。
package main
import (
"fmt"
"github.com/NVIDIA/go-nvml/pkg/nvml"
"log"
)
func main() {
ret := nvml.Init()
if ret != nvml.SUCCESS {
log.Fatalf("Unable to initialize NVML: %v", nvml.ErrorString(ret))
}
defer func() {
ret := nvml.Shutdown()
if ret != nvml.SUCCESS {
log.Fatalf("Unable to shutdown NVML: %v", nvml.ErrorString(ret))
}
}()
count, ret := nvml.DeviceGetCount()
if ret != nvml.SUCCESS {
log.Fatalf("Unable to get device count: %v", nvml.ErrorString(ret))
}
for i := 0; i < count; i++ {
device, ret := nvml.DeviceGetHandleByIndex(i)
if ret != nvml.SUCCESS {
log.Fatalf("Unable to get device at index %d: %v", i, nvml.ErrorString(ret))
}
// 獲取 UUID
uuid, ret := device.GetUUID()
if ret != nvml.SUCCESS {
log.Fatalf("Unable to get uuid of device at index %d: %v", i, nvml.ErrorString(ret))
}
fmt.Printf("GPU UUID: %v\n", uuid)
name, ret := device.GetName()
if ret != nvml.SUCCESS {
log.Fatalf("Unable to get name of device at index %d: %v", i, nvml.ErrorString(ret))
}
fmt.Printf("GPU Name: %+v\n", name)
memoryInfo, _ := device.GetMemoryInfo()
fmt.Printf("Memory Info: %+v\n", memoryInfo)
powerUsage, _ := device.GetPowerUsage()
fmt.Printf("Power Usage: %+v\n", powerUsage)
powerState, _ := device.GetPowerState()
fmt.Printf("Power State: %+v\n", powerState)
managementDefaultLimit, _ := device.GetPowerManagementDefaultLimit()
fmt.Printf("Power Managment Default Limit: %+v\n", managementDefaultLimit)
version, _ := device.GetInforomImageVersion()
fmt.Printf("Info Image Version: %+v\n", version)
driverVersion, _ := nvml.SystemGetDriverVersion()
fmt.Printf("Driver Version: %+v\n", driverVersion)
cudaDriverVersion, _ := nvml.SystemGetCudaDriverVersion()
fmt.Printf("CUDA Driver Version: %+v\n", cudaDriverVersion)
computeRunningProcesses, _ := device.GetGraphicsRunningProcesses()
for _, proc := range computeRunningProcesses {
fmt.Printf("Proc: %+v\n", proc)
}
}
fmt.Println()
}
使用 Docker Go SDK 為容器分配 GPU
首先需要用的的是 ContainerCreate
API
// ContainerCreate creates a new container based in the given configuration.
// It can be associated with a name, but it's not mandatory.
func (cli *Client) ContainerCreate(
ctx context.Context,
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *specs.Platform,
containerName string) (container.ContainerCreateCreatedBody, error)
這個 API 中需要很多用來指定配置的 struct, 其中用來請求 GPU 設備的是 container.HostConfig
這個 struct 中的 Resources
,它的類型是 container.Resources
,而在它的裏面保存的是 container.DeviceRequest
這個結構體的切片,這個變量會被 GPU 設備的驅動使用。
cli.ContainerCreate API 需要 ---------> container.HostConfig{
Resources: container.Resources{
DeviceRequests: []container.DeviceRequest {
{
Driver: "nvidia",
Count: 0,
DeviceIDs: []string{"0"},
Capabilities: [][]string{{"gpu"}},
Options: nil,
}
}
}
}
下面是 container.DeviceRequest
結構體的定義
// DeviceRequest represents a request for devices from a device driver.
// Used by GPU device drivers.
type DeviceRequest struct {
Driver string // 設備驅動名稱 這裡就填寫 "nvidia" 即可
Count int // 請求設備的數量 (-1 = All)
DeviceIDs []string // 可被設備驅動識別的設備ID列表,可以是索引也可以是UUID
Capabilities [][]string // An OR list of AND lists of device capabilities (e.g. "gpu")
Options map[string]string // Options to pass onto the device driver
}
注意:如果指定了 Count
字段,就無法通過 DeviceIDs
指定 GPU,它們是互斥的。
接下來我們嘗試使用 Docker Go SDK 啟動一個 pytorch 容器。
首先我們編寫一個 test.py
文件,讓它在容器內運行,檢查 CUDA 是否可用。
# test.py
import torch
print("cuda.is_available:", torch.cuda.is_available())
下面是實驗代碼,啟動一個名為 torch_test_1
的容器,並運行 python3 /workspace/test.py
命令,然後從 stdout
和 stderr
獲取輸出。
package main
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"os"
)
var (
defaultHost = "unix:///var/run/docker.sock"
)
func main() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.WithHost(defaultHost), client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}
resp, err := cli.ContainerCreate(ctx,
&container.Config{
Image: "pytorch/pytorch",
Cmd: []string{},
OpenStdin: true,
Volumes: map[string]struct{}{},
Tty: true,
}, &container.HostConfig{
Binds: []string{`/home/joseph/workspace:/workspace`},
Resources: container.Resources{DeviceRequests: []container.DeviceRequest{{
Driver: "nvidia",
Count: 0,
DeviceIDs: []string{"0"},
Capabilities: [][]string{{"gpu"}},
Options: nil,
}}},
}, nil, nil, "torch_test_1")
if err != nil {
panic(err)
}
if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
panic(err)
}
fmt.Println(resp.ID)
execConf := types.ExecConfig{
User: "",
Privileged: false,
Tty: false,
AttachStdin: false,
AttachStderr: true,
AttachStdout: true,
Detach: true,
DetachKeys: "ctrl-p,q",
Env: nil,
WorkingDir: "/",
Cmd: []string{"python3", "/workspace/test.py"},
}
execCreate, err := cli.ContainerExecCreate(ctx, resp.ID, execConf)
if err != nil {
panic(err)
}
response, err := cli.ContainerExecAttach(ctx, execCreate.ID, types.ExecStartCheck{})
defer response.Close()
if err != nil {
fmt.Println(err)
}
// read the output
_, _ = stdcopy.StdCopy(os.Stdout, os.Stderr, response.Reader)
}
可以看到,程序輸出了創建的容器的 Contrainer ID 和 執行命令的輸出。
$ go build main.go
$ sudo ./main
264535c7086391eab1d74ea48094f149ecda6d25709ac0c6c55c7693c349967b
cuda.is_available: True
接下來使用 docker ps
查看容器狀態。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
264535c70863 pytorch/pytorch "bash" 2 minutes ago Up 2 minutes torch_test_1
沒問題,Container ID 對得上。