From bb8bf201feaa3ede0bf7b8019c3555a4f5248b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Wed, 11 Jan 2023 21:58:30 +0100 Subject: [PATCH] Add GPU device request support (#84) * add gpus to api. * add gpus to gui. * get gpus all configuration. * correctly parse gpu device requests. --- OpenApi.yml | 5 ++ client/src/api/.openapi-generator/VERSION | 2 +- client/src/api/api.ts | 50 ++++++++------- client/src/api/base.ts | 5 +- client/src/api/common.ts | 45 +++++++++----- client/src/components/RoomInfo.vue | 1 + client/src/components/RoomsCreate.vue | 7 +++ go.mod | 1 + go.sum | 2 + internal/room/manager.go | 76 ++++++++++++++++++++++- internal/types/room.go | 9 +-- 11 files changed, 153 insertions(+), 50 deletions(-) diff --git a/OpenApi.yml b/OpenApi.yml index 42f3ea4..2a30332 100644 --- a/OpenApi.yml +++ b/OpenApi.yml @@ -349,6 +349,11 @@ components: memory: type: number description: in bytes + gpus: + type: array + items: + type: string + example: all RoomSettings: type: object diff --git a/client/src/api/.openapi-generator/VERSION b/client/src/api/.openapi-generator/VERSION index 4b448de..d6b4ec4 100644 --- a/client/src/api/.openapi-generator/VERSION +++ b/client/src/api/.openapi-generator/VERSION @@ -1 +1 @@ -5.3.0-SNAPSHOT \ No newline at end of file +6.3.0-SNAPSHOT \ No newline at end of file diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 2205155..56b01ad 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -13,13 +13,15 @@ */ -import { Configuration } from './configuration'; -import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import type { Configuration } from './configuration'; +import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; // Some imports not used depending on template conditions // @ts-ignore import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common'; +import type { RequestArgs } from './base'; // @ts-ignore -import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from './base'; +import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError } from './base'; /** * @@ -47,14 +49,12 @@ export interface BrowserPolicy { 'content'?: BrowserPolicyContent; } -/** - * @export - * @enum {string} - */ -export enum BrowserPolicyTypeEnum { - firefox = 'firefox', - chromium = 'chromium' -} +export const BrowserPolicyTypeEnum = { + firefox: 'firefox', + chromium: 'chromium' +} as const; + +export type BrowserPolicyTypeEnum = typeof BrowserPolicyTypeEnum[keyof typeof BrowserPolicyTypeEnum]; /** * @@ -249,7 +249,7 @@ export interface RoomEntry { */ 'is_outdated'?: boolean; /** - * + * 0 when using mux * @type {number} * @memberof RoomEntry */ @@ -330,16 +330,14 @@ export interface RoomMount { 'container_path'?: string; } -/** - * @export - * @enum {string} - */ -export enum RoomMountTypeEnum { - private = 'private', - template = 'template', - protected = 'protected', - public = 'public' -} +export const RoomMountTypeEnum = { + private: 'private', + template: 'template', + protected: 'protected', + public: 'public' +} as const; + +export type RoomMountTypeEnum = typeof RoomMountTypeEnum[keyof typeof RoomMountTypeEnum]; /** * @@ -371,6 +369,12 @@ export interface RoomResources { * @memberof RoomResources */ 'memory'?: number; + /** + * + * @type {Array} + * @memberof RoomResources + */ + 'gpus'?: Array; } /** * @@ -391,7 +395,7 @@ export interface RoomSettings { */ 'neko_image'?: string; /** - * + * 0 when using mux * @type {number} * @memberof RoomSettings */ diff --git a/client/src/api/base.ts b/client/src/api/base.ts index d88b3dc..68e8b0f 100644 --- a/client/src/api/base.ts +++ b/client/src/api/base.ts @@ -13,10 +13,11 @@ */ -import { Configuration } from "./configuration"; +import type { Configuration } from './configuration'; +import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; // Some imports not used depending on template conditions // @ts-ignore -import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; export const BASE_PATH = "https://virtserver.swaggerhub.com/m1k1o/neko-rooms/1.0.0".replace(/\/+$/, ""); diff --git a/client/src/api/common.ts b/client/src/api/common.ts index a5446ae..1c2451b 100644 --- a/client/src/api/common.ts +++ b/client/src/api/common.ts @@ -13,9 +13,10 @@ */ -import { Configuration } from "./configuration"; -import { RequiredError, RequestArgs } from "./base"; -import { AxiosInstance } from 'axios'; +import type { Configuration } from "./configuration"; +import type { RequestArgs } from "./base"; +import type { AxiosInstance, AxiosResponse } from 'axios'; +import { RequiredError } from "./base"; /** * @@ -83,24 +84,34 @@ export const setOAuthToObject = async function (object: any, name: string, scope } } +function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void { + if (typeof parameter === "object") { + if (Array.isArray(parameter)) { + (parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key)); + } + else { + Object.keys(parameter).forEach(currentKey => + setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`) + ); + } + } + else { + if (urlSearchParams.has(key)) { + urlSearchParams.append(key, parameter); + } + else { + urlSearchParams.set(key, parameter); + } + } +} + /** * * @export */ export const setSearchParams = function (url: URL, ...objects: any[]) { const searchParams = new URLSearchParams(url.search); - for (const object of objects) { - for (const key in object) { - if (Array.isArray(object[key])) { - searchParams.delete(key); - for (const item of object[key]) { - searchParams.append(key, item); - } - } else { - searchParams.set(key, object[key]); - } - } - } + setFlattenedQueryParams(searchParams, objects); url.search = searchParams.toString(); } @@ -131,8 +142,8 @@ export const toPathString = function (url: URL) { * @export */ export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { - return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url}; - return axios.request(axiosRequestArgs); + return axios.request(axiosRequestArgs); }; } diff --git a/client/src/components/RoomInfo.vue b/client/src/components/RoomInfo.vue index ba5eee6..35909c1 100644 --- a/client/src/components/RoomInfo.vue +++ b/client/src/components/RoomInfo.vue @@ -173,6 +173,7 @@ Memory {{ settings.resources.memory | memory }}--undefined-- CPU Shares {{ settings.resources.cpu_shares }}--undefined-- Shared memory {{ settings.resources.shm_size | memory }} + GPUs {{ settings.resources.gpus.join(",") }}--not used-- diff --git a/client/src/components/RoomsCreate.vue b/client/src/components/RoomsCreate.vue index 5ddb52c..7703027 100644 --- a/client/src/components/RoomsCreate.vue +++ b/client/src/components/RoomsCreate.vue @@ -358,6 +358,13 @@ + + + diff --git a/go.mod b/go.mod index 31aac8b..866af96 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( ) require ( + github.com/docker/cli v20.10.22+incompatible // indirect github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/go-units v0.4.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect diff --git a/go.sum b/go.sum index 33876aa..64184a7 100644 --- a/go.sum +++ b/go.sum @@ -104,6 +104,8 @@ github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v20.10.22+incompatible h1:0E7UqWPcn4SlvLImMHyh6xwyNRUGdPxhstpHeh0bFL0= +github.com/docker/cli v20.10.22+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= diff --git a/internal/room/manager.go b/internal/room/manager.go index 8ca36b4..5d7cc2a 100644 --- a/internal/room/manager.go +++ b/internal/room/manager.go @@ -1,7 +1,9 @@ package room import ( + "bytes" "context" + "encoding/csv" "encoding/json" "fmt" "os" @@ -9,6 +11,7 @@ import ( "path/filepath" "strings" + "github.com/docker/cli/opts" dockerTypes "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" dockerMount "github.com/docker/docker/api/types/mount" @@ -388,6 +391,31 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro paths[mount.ContainerPath] = true } + // + // Set container device requests + // + + var deviceRequests []container.DeviceRequest + + if len(settings.Resources.Gpus) > 0 { + gpuOpts := opts.GpuOpts{} + + // convert to csv + var buf bytes.Buffer + w := csv.NewWriter(&buf) + if err := w.Write(settings.Resources.Gpus); err != nil { + return "", err + } + w.Flush() + + // set GPU opts + if err := gpuOpts.Set(buf.String()); err != nil { + return "", err + } + + deviceRequests = append(deviceRequests, gpuOpts.Value()...) + } + // // Set container configs // @@ -430,9 +458,10 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro Mounts: mounts, // Resources contains container's resources (cgroups config, ulimits...) Resources: container.Resources{ - CPUShares: settings.Resources.CPUShares, - NanoCPUs: settings.Resources.NanoCPUs, - Memory: settings.Resources.Memory, + CPUShares: settings.Resources.CPUShares, + NanoCPUs: settings.Resources.NanoCPUs, + Memory: settings.Resources.Memory, + DeviceRequests: deviceRequests, }, // Privileged Privileged: isPrivilegedImage, @@ -556,11 +585,52 @@ func (manager *RoomManagerCtx) GetSettings(id string) (*types.RoomSettings, erro var roomResources types.RoomResources if container.HostConfig != nil { + gpus := []string{} + for _, req := range container.HostConfig.DeviceRequests { + var isGpu bool + var caps []string + for _, cc := range req.Capabilities { + for _, c := range cc { + if c == "gpu" { + isGpu = true + continue + } + caps = append(caps, c) + } + } + if !isGpu { + continue + } + + if req.Count > 1 { + gpus = append(gpus, fmt.Sprintf("count=%d", req.Count)) + } else if req.Count == -1 { + gpus = append(gpus, "all") + } + if req.Driver != "" { + gpus = append(gpus, fmt.Sprintf("driver=%s", req.Driver)) + } + if len(req.DeviceIDs) > 0 { + gpus = append(gpus, fmt.Sprintf("device=%s", strings.Join(req.DeviceIDs, ","))) + } + if len(caps) > 0 { + gpus = append(gpus, fmt.Sprintf("capabilities=%s", strings.Join(caps, ","))) + } + var opts []string + for key, val := range req.Options { + opts = append(opts, fmt.Sprintf("%s=%s", key, val)) + } + if len(opts) > 0 { + gpus = append(gpus, fmt.Sprintf("options=%s", strings.Join(opts, ","))) + } + } + roomResources = types.RoomResources{ CPUShares: container.HostConfig.CPUShares, NanoCPUs: container.HostConfig.NanoCPUs, ShmSize: container.HostConfig.ShmSize, Memory: container.HostConfig.Memory, + Gpus: gpus, } } diff --git a/internal/types/room.go b/internal/types/room.go index 2e7ab5a..a8b47fc 100644 --- a/internal/types/room.go +++ b/internal/types/room.go @@ -60,10 +60,11 @@ type RoomMount struct { } type RoomResources struct { - CPUShares int64 `json:"cpu_shares"` // relative weight vs. other containers - NanoCPUs int64 `json:"nano_cpus"` // in units of 10^-9 CPUs - ShmSize int64 `json:"shm_size"` // in bytes - Memory int64 `json:"memory"` // in bytes + CPUShares int64 `json:"cpu_shares"` // relative weight vs. other containers + NanoCPUs int64 `json:"nano_cpus"` // in units of 10^-9 CPUs + ShmSize int64 `json:"shm_size"` // in bytes + Memory int64 `json:"memory"` // in bytes + Gpus []string `json:"gpus"` // gpu opts } type RoomSettings struct {