【Docker】使用Docker Client和Docker Go SDK為容器分配GPU資源

背景

深度學(xué)習(xí)的環(huán)境配置通常是一項(xiàng)比較麻煩的工作喻频,尤其是在多個(gè)用戶共享的服務(wù)器上。雖然conda集成了virtualenv這樣的工具用來(lái)隔離不同的依賴環(huán)境肘迎,但這種解決方案仍然沒(méi)辦法統(tǒng)一地分配計(jì)算資源∩拢現(xiàn)在,我們可以通過(guò)容器技術(shù)為每個(gè)用戶創(chuàng)建一個(gè)屬于他們自己的容器妓布,并為容器分配相應(yīng)的計(jì)算資源姻蚓。目前市面上基于容器的深度學(xué)習(xí)平臺(tái)產(chǎn)品已經(jīng)有很多了,比如超益集倫的AiMax匣沼。這款產(chǎn)品本身集成了非常多的功能狰挡,但如果你只是需要在容器內(nèi)調(diào)用一下GPU,可以參考下面的步驟释涛。

使用 Docker Client 調(diào)用 GPU

依賴安裝

docker run --gpu 命令依賴于 nvidia Linux 驅(qū)動(dòng)和 nvidia container toolkit加叁,如果你想查看安裝文檔請(qǐng)點(diǎn)擊這里,本節(jié)的下文只是安裝文檔的翻譯和提示唇撬。

在Linux服務(wù)器上安裝nvidia驅(qū)動(dòng)非常簡(jiǎn)單它匕,如果你安裝了圖形化界面的話直接在Ubuntu的“附加驅(qū)動(dòng)”應(yīng)用中安裝即可,在nvidia官網(wǎng)上也可以下載驅(qū)動(dòng)窖认。

接下來(lái)就是安裝nvidia container toolkit豫柬,我們的服務(wù)器需要滿足一些先決條件:

  • GNU/Linux x86_64 內(nèi)核版本 > 3.10

  • Docker >= 19.03 (注意不是Docker Desktop,如果你想在自己的臺(tái)式機(jī)上使用toolkit扑浸,請(qǐng)安裝Docker Engine而不是Docker Desktop烧给,因?yàn)镈esktop版本都是運(yùn)行在虛擬機(jī)之上的)

  • NVIDIA GPU 架構(gòu) >= Kepler (目前RTX20系顯卡是圖靈架構(gòu),RTX30系顯卡是安培架構(gòu))

  • NVIDIA Linux drivers >= 418.81.07

然后就可以正式地在Ubuntu或者Debian上安裝NVIDIA Container Toolkit喝噪,如果你想在 CentOS 上或者其他 Linux 發(fā)行版上安裝础嫡,請(qǐng)參考官方的安裝文檔

安裝 Docker

$ curl https://get.docker.com | sh \
  && sudo systemctl --now enable docker

當(dāng)然酝惧,這里安裝完成后請(qǐng)參考官方的安裝后需要執(zhí)行的一系列操作榴鼎。如果安裝遇到問(wèn)題涧尿,請(qǐng)參照官方的安裝文檔

安裝 NVIDIA Container Toolkit?

設(shè)置 Package Repository和GPG Key

$ distribution=$(. /etc/os-release;echo $ID$VERSION_ID) \
      && curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
      && curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \
            sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
            sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

請(qǐng)注意:如果你想安裝 NVIDIA Container Toolkit 1.6.0 之前的版本檬贰,你應(yīng)該使用 nvidia-docker repository 而不是上方的 libnvidia-container repositories。
如果遇到問(wèn)題請(qǐng)直接參考安裝手冊(cè)
安裝 nvidia-docker2 應(yīng)該會(huì)自動(dòng)安裝 libnvidia-container-tools libnvidia-container1 等依賴包缺亮,如果沒(méi)有安裝可以手動(dòng)安裝

完成前面步驟后安裝 nvidia-docker2

$ sudo apt update 
$ sudo apt install -y nvidia-docker2

重啟 Docker Daemon

$ sudo systemctl restart docker

接下來(lái)你就可以通過(guò)運(yùn)行一個(gè)CUDA容器測(cè)試下安裝是否正確翁涤。

docker run --rm --gpus all nvidia/cuda:11.0.3-base-ubuntu20.04 nvidia-smi

Shell 中顯示的應(yīng)該類似于下面的輸出:

+-----------------------------------------------------------------------------+
| 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 的話萌踱,它在安裝時(shí)就已經(jīng)在 Docker 中注冊(cè)了 NVIDIA Runtime葵礼。如果你安裝的是 nvidia-docker ,請(qǐng)根據(jù)官方文檔向Docker注冊(cè)運(yùn)行時(shí)并鸵。
如果你有任何疑問(wèn)鸳粉,請(qǐng)移步本節(jié)參考的文檔

可以使用以 Docker 開(kāi)頭的選項(xiàng)或使用環(huán)境變量將 GPU 指定給 Docker CLI。此變量控制在容器內(nèi)可訪問(wèn)哪些 GPU园担。

  • --gpus
  • NVIDIA_VISIBLE_DEVICES
可能的值 描述
0,1,2 或者 GPU-fef8089b 逗號(hào)分割的GPU UUID(s) 或者 GPU 索引
all 所有GPU都可被容器訪問(wèn)届谈,默認(rèn)值
none 不可訪問(wèn)GPU,但可以使用驅(qū)動(dòng)提供的功能
void或者 empty 或者 unset nvidia-container-runtime will have the same behavior as (i.e. neither GPUs nor capabilities are exposed)runc

使用該選項(xiàng)指定 GPU 時(shí)弯汰,應(yīng)使用該參數(shù)艰山。參數(shù)的格式應(yīng)封裝在單引號(hào)中,后跟要枚舉到容器的設(shè)備的雙引號(hào)咏闪。例如:將 GPU 2 和 3 枚舉到容器曙搬。--gpus '"device=2,3"'

使用 NVIDIA_VISIBLE_DEVICES 變量時(shí),可能需要設(shè)置--runtime nvidia除非已設(shè)置為默認(rèn)值鸽嫂。

  1. 設(shè)置一個(gè)啟用CUDA支持的容器

    $ docker run --rm --gpus all nvidia/cuda nvidia-smi
    
  2. 指定 nvidia 作為運(yùn)行時(shí)纵装,并指定變量 NVIDIA_VISIBLE_DEVICES

    $ docker run --rm --runtime=nvidia \
        -e NVIDIA_VISIBLE_DEVICES=all nvidia/cuda nvidia-smi
    
  3. 為啟動(dòng)的容器分配2個(gè)GPU

    $ docker run --rm --gpus 2 nvidia/cuda nvidia-smi
    
  4. 為容器指定使用索引為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
    
  5. 也可以使用 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
    
  6. 使用 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
    

關(guān)于在容器內(nèi)使用驅(qū)動(dòng)程序的功能的設(shè)置,以及其他設(shè)置請(qǐng)參閱這里据某。

使用 Docker Go SDK 為容器分配 GPU

使用 NVIDIA/go-nvml 獲取 GPU 信息

NVIDIA/go-nvml 提供NVIDIA Management Library API (NVML) 的Go語(yǔ)言綁定橡娄。目前僅支持Linux,倉(cāng)庫(kù)地址哗脖。

下面的演示代碼獲取了 GPU 的各種信息瀑踢,其他功能請(qǐng)參考 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) 

這個(gè) API 中需要很多用來(lái)指定配置的 struct才避, 其中用來(lái)請(qǐng)求 GPU 設(shè)備的是 container.HostConfig 這個(gè) struct 中的 Resources 橱夭,它的類型是 container.Resources ,而在它的里面保存的是 container.DeviceRequest 這個(gè)結(jié)構(gòu)體的切片桑逝,這個(gè)變量會(huì)被 GPU 設(shè)備的驅(qū)動(dòng)使用棘劣。

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 結(jié)構(gòu)體的定義

// DeviceRequest represents a request for devices from a device driver.
// Used by GPU device drivers.
type DeviceRequest struct {
    Driver       string            // 設(shè)備驅(qū)動(dòng)名稱 這里就填寫 "nvidia" 即可
    Count        int               // 請(qǐng)求設(shè)備的數(shù)量 (-1 = All)
    DeviceIDs    []string          // 可被設(shè)備驅(qū)動(dòng)識(shí)別的設(shè)備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 字段楞遏,就無(wú)法通過(guò) DeviceIDs 指定 GPU茬暇,它們是互斥的首昔。

接下來(lái)我們嘗試使用 Docker Go SDK 啟動(dòng)一個(gè) pytorch 容器。

首先我們編寫一個(gè) test.py 文件糙俗,讓它在容器內(nèi)運(yùn)行勒奇,檢查 CUDA 是否可用。

# test.py
import torch

print("cuda.is_available:", torch.cuda.is_available())

下面是實(shí)驗(yàn)代碼巧骚,啟動(dòng)一個(gè)名為 torch_test_1 的容器赊颠,并運(yùn)行 python3 /workspace/test.py 命令,然后從 stdoutstderr 獲取輸出劈彪。

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"},  // 這里填寫GPU index 或者 GPU UUID 都可以
                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)
}

可以看到竣蹦,程序輸出了創(chuàng)建的容器的 Contrainer ID 和 執(zhí)行命令的輸出。

$ go build main.go 
$ sudo ./main 
264535c7086391eab1d74ea48094f149ecda6d25709ac0c6c55c7693c349967b
cuda.is_available: True

接下來(lái)使用 docker ps 查看容器狀態(tài)沧奴。

$ docker ps 
CONTAINER ID   IMAGE             COMMAND   CREATED         STATUS             PORTS     NAMES
264535c70863   pytorch/pytorch   "bash"    2 minutes ago   Up 2 minutes                 torch_test_1

沒(méi)問(wèn)題痘括,Container ID 對(duì)得上。

擴(kuò)展閱讀:NVIDIA Multi-Instance GPU

多實(shí)例 GPU (MIG) 功能允許將基于 NVIDIA Ampere 架構(gòu)的 GPU(如 NVIDIA A100)安全地分區(qū)為多達(dá)七個(gè)單獨(dú)的 GPU 實(shí)例滔吠,用于 CUDA 應(yīng)用程序纲菌,為多個(gè)用戶提供單獨(dú)的 GPU 資源,以實(shí)現(xiàn)最佳的 GPU 利用率疮绷。此功能對(duì)于未使 GPU 的計(jì)算容量完全飽和的工作負(fù)載特別有用驰后,因此用戶可能希望并行運(yùn)行不同的工作負(fù)載以最大限度地提高利用率。

MIG 文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末矗愧,一起剝皮案震驚了整個(gè)濱河市灶芝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌唉韭,老刑警劉巖夜涕,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異属愤,居然都是意外死亡女器,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門住诸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)驾胆,“玉大人,你說(shuō)我怎么就攤上這事贱呐∩ヅ担” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵奄薇,是天一觀的道長(zhǎng)驳阎。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么呵晚? 我笑而不...
    開(kāi)封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任蜘腌,我火速辦了婚禮,結(jié)果婚禮上饵隙,老公的妹妹穿的比我還像新娘撮珠。我一直安慰自己,他們只是感情好金矛,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布劫瞳。 她就那樣靜靜地躺著,像睡著了一般绷柒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上涮因,一...
    開(kāi)封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天废睦,我揣著相機(jī)與錄音,去河邊找鬼养泡。 笑死嗜湃,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的澜掩。 我是一名探鬼主播购披,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼肩榕!你這毒婦竟也來(lái)了刚陡?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤株汉,失蹤者是張志新(化名)和其女友劉穎筐乳,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體乔妈,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蝙云,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了路召。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勃刨。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖股淡,靈堂內(nèi)的尸體忽然破棺而出身隐,到底是詐尸還是另有隱情,我是刑警寧澤唯灵,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布抡医,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏忌傻。R本人自食惡果不足惜大脉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望水孩。 院中可真熱鬧镰矿,春花似錦、人聲如沸俘种。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)宙刘。三九已至苍姜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間悬包,已是汗流浹背衙猪。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留布近,地道東北人垫释。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像撑瞧,于是被迫代替她去往敵國(guó)和親棵譬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容