Skip to content

Commit 18a089e

Browse files
committed
feat(gpu): introduce GPU request spec
Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent 8a145eb commit 18a089e

16 files changed

Lines changed: 358 additions & 115 deletions

File tree

crates/openshell-cli/src/run.rs

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,16 @@ use openshell_core::proto::{
3737
GetClusterInferenceRequest, GetDraftHistoryRequest, GetDraftPolicyRequest,
3838
GetGatewayConfigRequest, GetProviderProfileRequest, GetProviderRequest,
3939
GetSandboxConfigRequest, GetSandboxLogsRequest, GetSandboxPolicyStatusRequest,
40-
GetSandboxRequest, GetServiceRequest, HealthRequest, ImportProviderProfilesRequest,
40+
GetSandboxRequest, GetServiceRequest, GpuSpec, HealthRequest, ImportProviderProfilesRequest,
4141
LintProviderProfilesRequest, ListProviderProfilesRequest, ListProvidersRequest,
4242
ListSandboxPoliciesRequest, ListSandboxProvidersRequest, ListSandboxesRequest,
4343
ListServicesRequest, PlatformEvent, PolicySource, PolicyStatus, Provider, ProviderProfile,
4444
ProviderProfileDiagnostic, ProviderProfileImportItem, RejectDraftChunkRequest,
45-
RevokeSshSessionRequest, Sandbox, SandboxPhase, SandboxPolicy, SandboxSpec, SandboxTemplate,
46-
ServiceEndpointResponse, SetClusterInferenceRequest, SettingScope, SettingValue,
47-
TcpForwardFrame, TcpForwardInit, TcpRelayTarget, UpdateConfigRequest, UpdateProviderRequest,
48-
WatchSandboxRequest, exec_sandbox_event, setting_value, tcp_forward_init,
45+
ResourceRequirements, RevokeSshSessionRequest, Sandbox, SandboxPhase, SandboxPolicy,
46+
SandboxSpec, SandboxTemplate, ServiceEndpointResponse, SetClusterInferenceRequest,
47+
SettingScope, SettingValue, TcpForwardFrame, TcpForwardInit, TcpRelayTarget,
48+
UpdateConfigRequest, UpdateProviderRequest, WatchSandboxRequest, exec_sandbox_event,
49+
setting_value, tcp_forward_init,
4950
};
5051
use openshell_core::settings::{self, SettingValueKind};
5152
use openshell_core::{ObjectId, ObjectName};
@@ -1692,8 +1693,7 @@ pub async fn sandbox_create(
16921693

16931694
let request = CreateSandboxRequest {
16941695
spec: Some(SandboxSpec {
1695-
gpu: requested_gpu,
1696-
gpu_device: gpu_device.unwrap_or_default().to_string(),
1696+
placement: resource_requirements_from_cli(requested_gpu, gpu_device),
16971697
policy,
16981698
providers: configured_providers,
16991699
template,
@@ -2083,6 +2083,20 @@ pub async fn sandbox_create(
20832083
}
20842084
}
20852085

2086+
fn resource_requirements_from_cli(
2087+
requested_gpu: bool,
2088+
gpu_device: Option<&str>,
2089+
) -> Option<ResourceRequirements> {
2090+
requested_gpu.then(|| ResourceRequirements {
2091+
gpu: Some(GpuSpec {
2092+
device_ids: gpu_device
2093+
.filter(|device_id| !device_id.is_empty())
2094+
.map(|device_id| vec![device_id.to_string()])
2095+
.unwrap_or_default(),
2096+
}),
2097+
})
2098+
}
2099+
20862100
/// Resolved source for the `--from` flag on `sandbox create`.
20872101
#[derive(Debug)]
20882102
enum ResolvedSource {
@@ -6281,7 +6295,8 @@ mod tests {
62816295
inferred_provider_type, package_managed_tls_dirs, parse_cli_setting_value,
62826296
parse_credential_pairs, plaintext_gateway_is_remote, progress_step_from_metadata,
62836297
provisioning_timeout_message, ready_false_condition_message, resolve_from,
6284-
sandbox_should_persist, service_expose_status_error, service_url_for_gateway,
6298+
resource_requirements_from_cli, sandbox_should_persist, service_expose_status_error,
6299+
service_url_for_gateway, source_requests_gpu,
62856300
};
62866301
use crate::TEST_ENV_LOCK;
62876302
use hyper::StatusCode;
@@ -6628,6 +6643,35 @@ mod tests {
66286643
}
66296644
}
66306645

6646+
#[test]
6647+
fn source_requests_gpu_detects_known_community_gpu_name() {
6648+
assert!(source_requests_gpu("nvidia-gpu"));
6649+
assert!(!source_requests_gpu("base"));
6650+
}
6651+
6652+
#[test]
6653+
fn resource_requirements_from_cli_uses_presence_with_empty_device_ids_for_default_gpu() {
6654+
let request = resource_requirements_from_cli(true, None)
6655+
.expect("resource requirements should be present");
6656+
let gpu = request.gpu.expect("gpu request should be present");
6657+
6658+
assert!(gpu.device_ids.is_empty());
6659+
}
6660+
6661+
#[test]
6662+
fn resource_requirements_from_cli_maps_gpu_device_to_one_device_id() {
6663+
let request = resource_requirements_from_cli(true, Some("0000:2d:00.0"))
6664+
.expect("resource requirements should be present");
6665+
let gpu = request.gpu.expect("gpu request should be present");
6666+
6667+
assert_eq!(gpu.device_ids, vec!["0000:2d:00.0"]);
6668+
}
6669+
6670+
#[test]
6671+
fn resource_requirements_from_cli_omits_placement_when_not_requested() {
6672+
assert!(resource_requirements_from_cli(false, Some("0")).is_none());
6673+
}
6674+
66316675
#[test]
66326676
fn resolve_from_classifies_existing_dockerfile_path() {
66336677
let temp = tempfile::tempdir().expect("failed to create tempdir");

crates/openshell-core/src/gpu.rs

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,19 @@
44
//! Shared GPU request helpers.
55
66
use crate::config::CDI_GPU_DEVICE_ALL;
7+
use crate::proto::compute::v1::GpuSpec;
78

8-
/// Resolve the existing GPU request fields into CDI device identifiers.
9+
/// Resolve a driver GPU request into CDI device identifiers.
910
///
10-
/// `None` means no GPU was requested. A GPU request with no explicit device
11-
/// ID uses the CDI all-GPU request; otherwise the driver-native ID passes
12-
/// through unchanged.
11+
/// `None` means no GPU was requested. Presence with no explicit device IDs
12+
/// uses the CDI all-GPU request; otherwise the driver-native IDs pass through.
1313
#[must_use]
14-
pub fn cdi_gpu_device_ids(gpu: bool, gpu_device: &str) -> Option<Vec<String>> {
15-
gpu.then(|| {
16-
if gpu_device.is_empty() {
17-
vec![CDI_GPU_DEVICE_ALL.to_string()]
18-
} else {
19-
vec![gpu_device.to_string()]
20-
}
21-
})
14+
pub fn cdi_gpu_device_ids(gpu: Option<&GpuSpec>) -> Option<Vec<String>> {
15+
match gpu {
16+
Some(gpu) if gpu.device_ids.is_empty() => Some(vec![CDI_GPU_DEVICE_ALL.to_string()]),
17+
Some(gpu) => Some(gpu.device_ids.clone()),
18+
None => None,
19+
}
2220
}
2321

2422
#[cfg(test)]
@@ -27,22 +25,46 @@ mod tests {
2725

2826
#[test]
2927
fn cdi_gpu_device_ids_returns_none_when_absent() {
30-
assert_eq!(cdi_gpu_device_ids(false, ""), None);
28+
assert_eq!(cdi_gpu_device_ids(None), None);
3129
}
3230

3331
#[test]
3432
fn cdi_gpu_device_ids_defaults_empty_request_to_all_gpus() {
33+
let request = GpuSpec { device_ids: vec![] };
34+
3535
assert_eq!(
36-
cdi_gpu_device_ids(true, ""),
36+
cdi_gpu_device_ids(Some(&request)),
3737
Some(vec![CDI_GPU_DEVICE_ALL.to_string()])
3838
);
3939
}
4040

4141
#[test]
42-
fn cdi_gpu_device_ids_passes_explicit_device_id_through() {
42+
fn cdi_gpu_device_ids_passes_single_device_id_through() {
43+
let request = GpuSpec {
44+
device_ids: vec!["nvidia.com/gpu=0".to_string()],
45+
};
46+
4347
assert_eq!(
44-
cdi_gpu_device_ids(true, "nvidia.com/gpu=0"),
48+
cdi_gpu_device_ids(Some(&request)),
4549
Some(vec!["nvidia.com/gpu=0".to_string()])
4650
);
4751
}
52+
53+
#[test]
54+
fn cdi_gpu_device_ids_passes_multiple_device_ids_through() {
55+
let request = GpuSpec {
56+
device_ids: vec![
57+
"nvidia.com/gpu=0".to_string(),
58+
"nvidia.com/gpu=1".to_string(),
59+
],
60+
};
61+
62+
assert_eq!(
63+
cdi_gpu_device_ids(Some(&request)),
64+
Some(vec![
65+
"nvidia.com/gpu=0".to_string(),
66+
"nvidia.com/gpu=1".to_string()
67+
])
68+
);
69+
}
4870
}

crates/openshell-driver-docker/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ contract:
3030
| `cap_add` | Grants supervisor-only capabilities required for namespace setup and process inspection. |
3131
| `apparmor=unconfined` | Avoids Docker's default profile blocking required mount operations. |
3232
| `restart_policy = unless-stopped` | Keeps managed sandboxes resumable across daemon or gateway restarts. |
33-
| CDI GPU request | Uses the sandbox `gpu_device` value when set; otherwise requests all NVIDIA GPUs when the sandbox spec asks for GPU support and daemon CDI support is detected. |
33+
| CDI GPU request | Uses explicit placement GPU device IDs when set; otherwise requests all NVIDIA GPUs for GPU placement requests when daemon CDI support is detected. |
3434

3535
The agent child process does not retain these supervisor privileges.
3636

crates/openshell-driver-docker/src/lib.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use openshell_core::proto::compute::v1::{
2424
CreateSandboxRequest, CreateSandboxResponse, DeleteSandboxRequest, DeleteSandboxResponse,
2525
DriverCondition, DriverSandbox, DriverSandboxStatus, DriverSandboxTemplate,
2626
GetCapabilitiesRequest, GetCapabilitiesResponse, GetSandboxRequest, GetSandboxResponse,
27-
ListSandboxesRequest, ListSandboxesResponse, StopSandboxRequest, StopSandboxResponse,
27+
GpuSpec, ListSandboxesRequest, ListSandboxesResponse, StopSandboxRequest, StopSandboxResponse,
2828
ValidateSandboxCreateRequest, ValidateSandboxCreateResponse, WatchSandboxesDeletedEvent,
2929
WatchSandboxesEvent, WatchSandboxesRequest, WatchSandboxesSandboxEvent,
3030
compute_driver_server::ComputeDriver, watch_sandboxes_event,
@@ -355,7 +355,12 @@ impl DockerComputeDriver {
355355
"docker sandboxes require a template image",
356356
));
357357
}
358-
Self::validate_gpu_request(spec.gpu, config.supports_gpu)?;
358+
Self::validate_gpu_request(
359+
spec.placement
360+
.as_ref()
361+
.and_then(|placement| placement.gpu.as_ref()),
362+
config.supports_gpu,
363+
)?;
359364
if !template.agent_socket_path.trim().is_empty() {
360365
return Err(Status::failed_precondition(
361366
"docker compute driver does not support template.agent_socket_path",
@@ -375,8 +380,8 @@ impl DockerComputeDriver {
375380
Ok(())
376381
}
377382

378-
fn validate_gpu_request(gpu: bool, supports_gpu: bool) -> Result<(), Status> {
379-
if gpu && !supports_gpu {
383+
fn validate_gpu_request(gpu: Option<&GpuSpec>, supports_gpu: bool) -> Result<(), Status> {
384+
if gpu.is_some() && !supports_gpu {
380385
return Err(Status::failed_precondition(
381386
"docker GPU sandboxes require Docker CDI support. Enable CDI on the Docker daemon, then restart the OpenShell gateway/server so GPU capability is detected.",
382387
));
@@ -996,8 +1001,8 @@ fn build_environment(sandbox: &DriverSandbox, config: &DockerDriverRuntimeConfig
9961001
.collect()
9971002
}
9981003

999-
fn docker_gpu_device_requests(gpu: bool, gpu_device: &str) -> Option<Vec<DeviceRequest>> {
1000-
cdi_gpu_device_ids(gpu, gpu_device).map(|device_ids| {
1004+
fn docker_gpu_device_requests(gpu: Option<&GpuSpec>) -> Option<Vec<DeviceRequest>> {
1005+
cdi_gpu_device_ids(gpu).map(|device_ids| {
10011006
vec![DeviceRequest {
10021007
driver: Some("cdi".to_string()),
10031008
device_ids: Some(device_ids),
@@ -1047,8 +1052,12 @@ fn build_container_create_body(
10471052
host_config: Some(HostConfig {
10481053
nano_cpus: resource_limits.nano_cpus,
10491054
memory: resource_limits.memory_bytes,
1050-
device_requests: docker_gpu_device_requests(spec.gpu, &spec.gpu_device),
1051-
binds: Some(build_binds(config)),
1055+
device_requests: docker_gpu_device_requests(
1056+
spec.placement
1057+
.as_ref()
1058+
.and_then(|placement| placement.gpu.as_ref()),
1059+
),
1060+
mounts: Some(build_mounts(config)),
10521061
restart_policy: Some(RestartPolicy {
10531062
name: Some(RestartPolicyNameEnum::UNLESS_STOPPED),
10541063
maximum_retry_count: None,

crates/openshell-driver-docker/src/tests.rs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
use super::*;
55
use openshell_core::config::{CDI_GPU_DEVICE_ALL, DEFAULT_SERVER_PORT};
66
use openshell_core::proto::compute::v1::{
7-
DriverResourceRequirements, DriverSandboxSpec, DriverSandboxTemplate,
7+
DriverResourceRequirements, DriverSandboxSpec, DriverSandboxTemplate, GpuSpec,
8+
ResourceRequirements,
89
};
910
use std::fs;
1011
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
@@ -31,8 +32,7 @@ fn test_sandbox() -> DriverSandbox {
3132
resources: None,
3233
platform_config: None,
3334
}),
34-
gpu: false,
35-
gpu_device: String::new(),
35+
placement: None,
3636
}),
3737
status: None,
3838
}
@@ -499,7 +499,9 @@ fn build_container_create_body_clears_inherited_cmd() {
499499
fn validate_sandbox_rejects_gpu_when_cdi_unavailable() {
500500
let config = runtime_config();
501501
let mut sandbox = test_sandbox();
502-
sandbox.spec.as_mut().unwrap().gpu = true;
502+
sandbox.spec.as_mut().unwrap().placement = Some(ResourceRequirements {
503+
gpu: Some(GpuSpec { device_ids: vec![] }),
504+
});
503505

504506
let err = DockerComputeDriver::validate_sandbox(&sandbox, &config).unwrap_err();
505507

@@ -512,7 +514,9 @@ fn build_container_create_body_maps_gpu_to_all_cdi_device() {
512514
let mut config = runtime_config();
513515
config.supports_gpu = true;
514516
let mut sandbox = test_sandbox();
515-
sandbox.spec.as_mut().unwrap().gpu = true;
517+
sandbox.spec.as_mut().unwrap().placement = Some(ResourceRequirements {
518+
gpu: Some(GpuSpec { device_ids: vec![] }),
519+
});
516520

517521
let create_body = build_container_create_body(&sandbox, &config).unwrap();
518522
let request = create_body
@@ -530,13 +534,18 @@ fn build_container_create_body_maps_gpu_to_all_cdi_device() {
530534
}
531535

532536
#[test]
533-
fn build_container_create_body_passes_explicit_cdi_device_id_through() {
537+
fn build_container_create_body_passes_explicit_cdi_device_ids_through() {
534538
let mut config = runtime_config();
535539
config.supports_gpu = true;
536540
let mut sandbox = test_sandbox();
537-
let spec = sandbox.spec.as_mut().unwrap();
538-
spec.gpu = true;
539-
spec.gpu_device = "nvidia.com/gpu=0".to_string();
541+
sandbox.spec.as_mut().unwrap().placement = Some(ResourceRequirements {
542+
gpu: Some(GpuSpec {
543+
device_ids: vec![
544+
"nvidia.com/gpu=0".to_string(),
545+
"nvidia.com/gpu=1".to_string(),
546+
],
547+
}),
548+
});
540549

541550
let create_body = build_container_create_body(&sandbox, &config).unwrap();
542551
let request = create_body
@@ -549,7 +558,10 @@ fn build_container_create_body_passes_explicit_cdi_device_id_through() {
549558
assert_eq!(request.driver.as_deref(), Some("cdi"));
550559
assert_eq!(
551560
request.device_ids.as_ref().unwrap(),
552-
&vec!["nvidia.com/gpu=0".to_string()]
561+
&vec![
562+
"nvidia.com/gpu=0".to_string(),
563+
"nvidia.com/gpu=1".to_string()
564+
]
553565
);
554566
}
555567

0 commit comments

Comments
 (0)