1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 05:45:22 +02:00

feat(gpu) EE-3191 Add GPU support for containers (#7146)

This commit is contained in:
congs 2022-07-18 11:02:14 +12:00 committed by GitHub
parent f0456cbf5f
commit 4997e9c7be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 758 additions and 10 deletions

View file

@ -0,0 +1,210 @@
import { useMemo } from 'react';
import { components } from 'react-select';
import { OnChangeValue } from 'react-select/dist/declarations/src/types';
import { OptionProps } from 'react-select/dist/declarations/src/components/Option';
import { Select } from '@@/form-components/ReactSelect';
import { Switch } from '@@/form-components/SwitchField/Switch';
import { Tooltip } from '@@/Tip/Tooltip';
interface Values {
enabled: boolean;
selectedGPUs: string[];
capabilities: string[];
}
interface GpuOption {
value: string;
label: string;
description?: string;
}
export interface GPU {
value: string;
name: string;
}
export interface Props {
values: Values;
onChange(values: Values): void;
gpus: GPU[];
usedGpus: string[];
usedAllGpus: boolean;
}
const NvidiaCapabilitiesOptions = [
// Taken from https://github.com/containerd/containerd/blob/master/contrib/nvidia/nvidia.go#L40
{
value: 'compute',
label: 'compute',
description: 'required for CUDA and OpenCL applications',
},
{
value: 'compat32',
label: 'compat32',
description: 'required for running 32-bit applications',
},
{
value: 'graphics',
label: 'graphics',
description: 'required for running OpenGL and Vulkan applications',
},
{
value: 'utility',
label: 'utility',
description: 'required for using nvidia-smi and NVML',
},
{
value: 'video',
label: 'video',
description: 'required for using the Video Codec SDK',
},
{
value: 'display',
label: 'display',
description: 'required for leveraging X11 display',
},
];
function Option(props: OptionProps<GpuOption, true>) {
const {
data: { value, description },
} = props;
return (
<div>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<components.Option {...props}>
{`${value} - ${description}`}
</components.Option>
</div>
);
}
export function Gpu({
values,
onChange,
gpus,
usedGpus = [],
usedAllGpus,
}: Props) {
const options = useMemo(() => {
const options = gpus.map((gpu) => ({
value: gpu.value,
label:
usedGpus.includes(gpu.value) || usedAllGpus
? `${gpu.name} (in use)`
: gpu.name,
}));
return options;
}, [gpus, usedGpus, usedAllGpus]);
function onChangeValues(key: string, newValue: boolean | string[]) {
const newValues = {
...values,
[key]: newValue,
};
onChange(newValues);
}
function toggleEnableGpu() {
onChangeValues('enabled', !values.enabled);
}
function onChangeSelectedGpus(newValue: OnChangeValue<GpuOption, true>) {
onChangeValues(
'selectedGPUs',
newValue.map((option) => option.value)
);
}
function onChangeSelectedCaps(newValue: OnChangeValue<GpuOption, true>) {
onChangeValues(
'capabilities',
newValue.map((option) => option.value)
);
}
const gpuCmd = useMemo(() => {
const devices = values.selectedGPUs.join(',');
const caps = values.capabilities.join(',');
return `--gpus 'device=${devices},"capabilities=${caps}"`;
}, [values.selectedGPUs, values.capabilities]);
const gpuValue = useMemo(
() =>
options.filter((option) => values.selectedGPUs.includes(option.value)),
[values.selectedGPUs, options]
);
const capValue = useMemo(
() =>
NvidiaCapabilitiesOptions.filter((option) =>
values.capabilities.includes(option.value)
),
[values.capabilities]
);
return (
<div>
<div className="form-group">
<div className="col-sm-3 col-lg-2 control-label text-left">
Enable GPU
<Switch
id="enabled"
name="enabled"
checked={values.enabled}
onChange={toggleEnableGpu}
className="ml-2"
/>
</div>
<div className="col-sm-9 col-lg-10 text-left">
<Select<GpuOption, true>
isMulti
closeMenuOnSelect
value={gpuValue}
isDisabled={!values.enabled}
onChange={onChangeSelectedGpus}
options={options}
/>
</div>
</div>
{values.enabled && (
<>
<div className="form-group">
<div className="col-sm-3 col-lg-2 control-label text-left">
Capabilities
<Tooltip
message={
"This is the generated equivalent of the '--gpus' docker CLI parameter based on your settings."
}
/>
</div>
<div className="col-sm-9 col-lg-10 text-left">
<Select<GpuOption, true>
isMulti
closeMenuOnSelect
value={capValue}
options={NvidiaCapabilitiesOptions}
components={{ Option }}
onChange={onChangeSelectedCaps}
/>
</div>
</div>
<div className="form-group">
<div className="col-sm-3 col-lg-2 control-label text-left">
Control
<Tooltip message="This is the generated equivalent of the '--gpus' docker CLI parameter based on your settings." />
</div>
<div className="col-sm-9 col-lg-10">
<code>{gpuCmd}</code>
</div>
</div>
</>
)}
</div>
);
}

View file

@ -1,8 +1,13 @@
import angular from 'angular';
import { Gpu } from 'Docker/react/views/gpu';
import { ItemView } from '@/react/docker/networks/ItemView';
import { r2a } from '@/react-tools/react2angular';
export const viewsModule = angular
.module('portainer.docker.react.views', [])
.component(
'gpu',
r2a(Gpu, ['values', 'onChange', 'gpus', 'usedGpus', 'usedAllGpus'])
)
.component('networkDetailsView', r2a(ItemView, [])).name;

View file

@ -1,4 +1,5 @@
angular.module('portainer.docker').controller('ContainersController', ContainersController);
import _ from 'lodash';
/* @ngInject */
function ContainersController($scope, ContainerService, Notifications, endpoint) {
@ -8,9 +9,42 @@ function ContainersController($scope, ContainerService, Notifications, endpoint)
$scope.getContainers = getContainers;
function getContainers() {
$scope.containers = null;
$scope.containers_t = null;
ContainerService.containers(1)
.then(function success(data) {
$scope.containers = data;
$scope.containers_t = data;
if ($scope.containers_t.length === 0) {
$scope.containers = $scope.containers_t;
return;
}
for (let item of $scope.containers_t) {
ContainerService.container(item.Id).then(function success(data) {
var Id = data.Id;
for (var i = 0; i < $scope.containers_t.length; i++) {
if (Id == $scope.containers_t[i].Id) {
const gpuOptions = _.find(data.HostConfig.DeviceRequests, function (o) {
return o.Driver === 'nvidia' || o.Capabilities[0][0] === 'gpu';
});
if (!gpuOptions) {
$scope.containers_t[i]['Gpus'] = 'none';
} else {
let gpuStr = 'all';
if (gpuOptions.Count !== -1) {
gpuStr = `id:${_.join(gpuOptions.DeviceIDs, ',')}`;
}
$scope.containers_t[i]['Gpus'] = `${gpuStr}`;
}
}
}
for (let item of $scope.containers_t) {
if (!Object.keys(item).includes('Gpus')) {
return;
}
}
$scope.containers = $scope.containers_t;
});
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve containers');

View file

@ -69,6 +69,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.containerWebhookFeature = FeatureId.CONTAINER_WEBHOOK;
$scope.formValues = {
alwaysPull: true,
GPU: {
enabled: false,
useSpecific: false,
selectedGPUs: [],
capabilities: ['compute', 'utility'],
},
Console: 'none',
Volumes: [],
NetworkContainer: null,
@ -149,6 +155,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
Runtime: null,
ExtraHosts: [],
Devices: [],
DeviceRequests: [],
CapAdd: [],
CapDrop: [],
Sysctls: {},
@ -199,6 +206,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.config.HostConfig.Devices.splice(index, 1);
};
$scope.onGpuChange = function (values) {
return $async(async () => {
$scope.formValues.GPU = values;
});
};
$scope.addSysctl = function () {
$scope.formValues.Sysctls.push({ name: '', value: '' });
};
@ -417,6 +430,36 @@ angular.module('portainer.docker').controller('CreateContainerController', [
config.HostConfig.CapDrop = notAllowed.map(getCapName);
}
function prepareGPUOptions(config) {
const driver = 'nvidia';
const gpuOptions = $scope.formValues.GPU;
const existingDeviceRequest = _.find($scope.config.HostConfig.DeviceRequests, function (o) {
return o.Driver === driver || o.Capabilities[0][0] === 'gpu';
});
if (existingDeviceRequest) {
_.pullAllBy(config.HostConfig.DeviceRequests, [existingDeviceRequest], 'Driver');
}
if (!gpuOptions.enabled) {
return;
}
const deviceRequest = existingDeviceRequest || {
Driver: driver,
Count: -1,
DeviceIDs: [], // must be empty if Count != 0 https://github.com/moby/moby/blob/master/daemon/nvidia_linux.go#L50
Capabilities: [], // array of ORed arrays of ANDed capabilites = [ [c1 AND c2] OR [c1 AND c3] ] : https://github.com/moby/moby/blob/master/api/types/container/host_config.go#L272
// Options: { property1: "string", property2: "string" }, // seems to never be evaluated/used in docker API ?
};
deviceRequest.DeviceIDs = gpuOptions.selectedGPUs;
deviceRequest.Count = 0;
deviceRequest.Capabilities = [gpuOptions.capabilities];
config.HostConfig.DeviceRequests.push(deviceRequest);
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareCmd(config);
@ -433,6 +476,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
prepareLogDriver(config);
prepareCapabilities(config);
prepareSysctls(config);
prepareGPUOptions(config);
return config;
}
@ -571,6 +615,24 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.config.HostConfig.Devices = path;
}
function loadFromContainerDeviceRequests() {
const deviceRequest = _.find($scope.config.HostConfig.DeviceRequests, function (o) {
return o.Driver === 'nvidia' || o.Capabilities[0][0] === 'gpu';
});
if (deviceRequest) {
$scope.formValues.GPU.enabled = true;
$scope.formValues.GPU.useSpecific = deviceRequest.Count !== -1;
$scope.formValues.GPU.selectedGPUs = deviceRequest.DeviceIDs || [];
if ($scope.formValues.GPU.useSpecific) {
$scope.formValues.GPU.selectedGPUs = deviceRequest.DeviceIDs;
}
// we only support a single set of capabilities for now
// UI needs to be reworked in order to support OR combinations of AND capabilities
$scope.formValues.GPU.capabilities = deviceRequest.Capabilities[0];
$scope.formValues.GPU = { ...$scope.formValues.GPU };
}
}
function loadFromContainerSysctls() {
for (var s in $scope.config.HostConfig.Sysctls) {
if ({}.hasOwnProperty.call($scope.config.HostConfig.Sysctls, s)) {
@ -651,6 +713,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
loadFromContainerLabels(d);
loadFromContainerConsole(d);
loadFromContainerDevices(d);
loadFromContainerDeviceRequests(d);
loadFromContainerImageConfig(d);
loadFromContainerResources(d);
loadFromContainerCapabilities(d);
@ -715,6 +778,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
function (d) {
var containers = d;
$scope.runningContainers = containers;
$scope.gpuUseAll = $scope.endpoint.Snapshots[0].GpuUseAll;
$scope.gpuUseList = $scope.endpoint.Snapshots[0].GpuUseList;
if ($transition$.params().from) {
loadFromContainerSpec();
} else {

View file

@ -693,6 +693,20 @@
<!-- !sysctls-input-list -->
</div>
<!-- !sysctls -->
<!-- #region GPU -->
<div class="col-sm-12 form-section-title"> GPU </div>
<gpu
ng-if="applicationState.endpoint.apiVersion >= 1.4"
values="formValues.GPU"
on-change="(onGpuChange)"
gpus="endpoint.Gpus"
used-gpus="gpuUseList"
used-all-gpus="gpuUseAll"
>
</gpu>
<!-- #endregion GPU -->
<div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
<div class="col-sm-12 form-section-title"> Resources </div>
<!-- memory-reservation-input -->

View file

@ -322,6 +322,10 @@
</table>
</td>
</tr>
<tr ng-if="container.HostConfig.DeviceRequests.length">
<td>GPUS</td>
<td>{{ computeDockerGPUCommand() }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>

View file

@ -77,6 +77,23 @@ angular.module('portainer.docker').controller('ContainerController', [
$state.reload();
};
$scope.computeDockerGPUCommand = () => {
const gpuOptions = _.find($scope.container.HostConfig.DeviceRequests, function (o) {
return o.Driver === 'nvidia' || o.Capabilities[0][0] === 'gpu';
});
if (!gpuOptions) {
return 'No GPU config found';
}
let gpuStr = 'all';
if (gpuOptions.Count !== -1) {
gpuStr = `"device=${_.join(gpuOptions.DeviceIDs, ',')}"`;
}
// we only support a single set of capabilities for now
// creation UI needs to be reworked in order to support OR combinations of AND capabilities
const capStr = `"capabilities=${_.join(gpuOptions.Capabilities[0], ',')}"`;
return `${gpuStr},${capStr}`;
};
var update = function () {
var nodeName = $transition$.params().nodeName;
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);

View file

@ -53,6 +53,10 @@
<td>URL</td>
<td>{{ endpoint.URL | stripprotocol }}</td>
</tr>
<tr>
<td>{{ endpoint.Gpus.length <= 1 ? 'GPU' : 'GPUs' }}</td>
<td>{{ gpuInfoStr }}</td>
</tr>
<tr>
<td>Tags</td>
<td>{{ endpointTags }}</td>
@ -97,5 +101,9 @@
<a ui-sref="docker.networks">
<dashboard-item icon="'fa-sitemap'" type="'Network'" value="networkCount"></dashboard-item>
</a>
<div>
<dashboard-item icon="'fa-digital-tachograph'" type="'GPU'" value="endpoint.Gpus.length"></dashboard-item>
</div>
</div>
</div>

View file

@ -44,6 +44,36 @@ angular.module('portainer.docker').controller('DashboardController', [
$scope.offlineMode = false;
$scope.showStacks = false;
$scope.buildGpusStr = function (gpuUseSet) {
var gpusAvailable = new Object();
for (let i = 0; i < $scope.endpoint.Gpus.length; i++) {
if (!gpuUseSet.has($scope.endpoint.Gpus[i].name)) {
var exist = false;
for (let gpuAvailable in gpusAvailable) {
if ($scope.endpoint.Gpus[i].value == gpuAvailable) {
gpusAvailable[gpuAvailable] += 1;
exist = true;
}
}
if (exist === false) {
gpusAvailable[$scope.endpoint.Gpus[i].value] = 1;
}
}
}
var retStr = Object.keys(gpusAvailable).length
? _.join(
_.map(Object.keys(gpusAvailable), (gpuAvailable) => {
var _str = gpusAvailable[gpuAvailable];
_str += ' x ';
_str += gpuAvailable;
return _str;
}),
' + '
)
: 'none';
return retStr;
};
async function initView() {
const endpointMode = $scope.applicationState.endpoint.mode;
$scope.endpoint = endpoint;
@ -72,6 +102,14 @@ angular.module('portainer.docker').controller('DashboardController', [
$scope.serviceCount = data.services.length;
$scope.stackCount = data.stacks.length;
$scope.info = data.info;
$scope.gpuInfoStr = $scope.buildGpusStr(new Set());
$scope.gpuUseAll = _.get($scope, 'endpoint.Snapshots[0].GpuUseAll', false);
$scope.gpuUseList = _.get($scope, 'endpoint.Snapshots[0].GpuUseList', []);
$scope.gpuFreeStr = 'all';
if ($scope.gpuUseAll == true) $scope.gpuFreeStr = 'none';
else $scope.gpuFreeStr = $scope.buildGpusStr(new Set($scope.gpuUseList));
$scope.endpointTags = endpoint.TagIds.length
? _.join(
_.filter(