Skip to content

Commit e106f02

Browse files
committed
feat(gpu): route device selection through driver config
Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent 009a6ee commit e106f02

22 files changed

Lines changed: 1081 additions & 276 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

architecture/compute-runtimes.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ but currently ignores them.
4242

4343
GPU requests enter the driver layer through
4444
`SandboxSpec.resource_requirements.gpu`. The compact interim shape supports a
45-
default GPU request, GPU count, and driver-specific device IDs.
45+
default GPU request and GPU count. Exact driver-native device selection is
46+
passed through the selected runtime's `driver_config` block; the gateway
47+
selects that block but does not interpret the nested driver schema. Drivers
48+
that support exact selection validate that the unique `gpu_device_ids` entry
49+
count matches the portable GPU count.
4650

4751
VM runtime state paths are derived only from driver-validated sandbox IDs
4852
matching `[A-Za-z0-9._-]{1,128}`. The gateway-owned VM driver socket uses a
@@ -81,9 +85,7 @@ users.
8185
Custom sandbox images must include the agent runtime and any system
8286
dependencies, but they should not need to include the gateway. GPU-capable
8387
images must include the user-space libraries required by the workload. The
84-
runtime still owns GPU device injection. GPU requests can include explicit
85-
driver-native device IDs or a requested count; the gateway validates the public
86-
request shape and each runtime enforces the GPU allocation modes it supports.
88+
runtime still owns GPU device injection.
8789

8890
## Deployment Shape
8991

crates/openshell-cli/src/main.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,8 +1215,8 @@ enum SandboxCommands {
12151215

12161216
/// Target a driver-specific GPU device. Docker and Podman use CDI device IDs
12171217
/// (for example "nvidia.com/gpu=0"); VM uses a PCI BDF or index.
1218-
/// Only valid with --gpu. When omitted with --gpu, the driver uses its default GPU selection.
1219-
#[arg(long, requires = "gpu", conflicts_with = "gpu_count")]
1218+
/// When omitted with --gpu, the driver uses its default GPU selection.
1219+
#[arg(long, conflicts_with = "gpu_count")]
12201220
gpu_device: Option<String>,
12211221

12221222
/// Request a specific number of GPUs. Mutually exclusive with --gpu-device.
@@ -4320,6 +4320,32 @@ mod tests {
43204320
);
43214321
}
43224322

4323+
#[test]
4324+
fn sandbox_create_gpu_device_parses_without_gpu_flag() {
4325+
let cli = Cli::try_parse_from([
4326+
"openshell",
4327+
"sandbox",
4328+
"create",
4329+
"--gpu-device",
4330+
"nvidia.com/gpu=0",
4331+
])
4332+
.expect("sandbox create --gpu-device should parse without --gpu");
4333+
4334+
match cli.command {
4335+
Some(Commands::Sandbox {
4336+
command:
4337+
Some(SandboxCommands::Create {
4338+
gpu, gpu_device, ..
4339+
}),
4340+
..
4341+
}) => {
4342+
assert!(!gpu);
4343+
assert_eq!(gpu_device.as_deref(), Some("nvidia.com/gpu=0"));
4344+
}
4345+
other => panic!("expected SandboxCommands::Create, got: {other:?}"),
4346+
}
4347+
}
4348+
43234349
#[test]
43244350
fn sandbox_create_gpu_count_conflicts_with_gpu_device() {
43254351
let result = Cli::try_parse_from([

crates/openshell-cli/src/run.rs

Lines changed: 104 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,8 +1734,10 @@ pub async fn sandbox_create(
17341734
}
17351735
None => None,
17361736
};
1737+
let gpu_device_ids = gpu_device_ids_from_cli(gpu_device);
1738+
let effective_gpu_count = gpu_count_from_cli(gpu_count, &gpu_device_ids);
17371739
let requested_gpu =
1738-
gpu || gpu_count.is_some() || image.as_deref().is_some_and(image_requests_gpu);
1740+
gpu || effective_gpu_count.is_some() || image.as_deref().is_some_and(image_requests_gpu);
17391741

17401742
let providers_v2_enabled = gateway_providers_v2_enabled(&mut client).await?;
17411743
let inferred_types: Vec<String> = if providers_v2_enabled {
@@ -1753,11 +1755,13 @@ pub async fn sandbox_create(
17531755

17541756
let policy = load_sandbox_policy(policy)?;
17551757
let resource_limits = build_sandbox_resource_limits(cpu, memory)?;
1758+
let driver_config = gpu_driver_config_from_cli(&gpu_device_ids);
17561759

1757-
let template = if image.is_some() || resource_limits.is_some() {
1760+
let template = if image.is_some() || resource_limits.is_some() || driver_config.is_some() {
17581761
Some(SandboxTemplate {
17591762
image: image.unwrap_or_default(),
17601763
resources: resource_limits,
1764+
driver_config,
17611765
..SandboxTemplate::default()
17621766
})
17631767
} else {
@@ -1768,8 +1772,7 @@ pub async fn sandbox_create(
17681772
spec: Some(SandboxSpec {
17691773
resource_requirements: resource_requirements_from_cli(
17701774
requested_gpu,
1771-
gpu_device,
1772-
gpu_count,
1775+
effective_gpu_count,
17731776
),
17741777
policy,
17751778
providers: configured_providers,
@@ -2197,17 +2200,69 @@ pub async fn sandbox_create(
21972200

21982201
fn resource_requirements_from_cli(
21992202
requested_gpu: bool,
2200-
gpu_device: Option<&str>,
22012203
gpu_count: Option<u32>,
22022204
) -> Option<SandboxResourceRequirements> {
2203-
requested_gpu.then(|| SandboxResourceRequirements {
2204-
gpu: Some(GpuResourceRequirement {
2205-
device_ids: gpu_device
2206-
.filter(|device_id| !device_id.is_empty())
2207-
.map(|device_id| vec![device_id.to_string()])
2208-
.unwrap_or_default(),
2209-
count: gpu_count,
2210-
}),
2205+
requested_gpu.then_some(SandboxResourceRequirements {
2206+
gpu: Some(GpuResourceRequirement { count: gpu_count }),
2207+
})
2208+
}
2209+
2210+
fn gpu_device_ids_from_cli(gpu_device: Option<&str>) -> Vec<String> {
2211+
gpu_device
2212+
.map(str::trim)
2213+
.filter(|device_id| !device_id.is_empty())
2214+
.map(|device_id| vec![device_id.to_string()])
2215+
.unwrap_or_default()
2216+
}
2217+
2218+
fn gpu_count_from_cli(gpu_count: Option<u32>, gpu_device_ids: &[String]) -> Option<u32> {
2219+
if gpu_device_ids.is_empty() {
2220+
gpu_count
2221+
} else {
2222+
u32::try_from(gpu_device_ids.len()).ok()
2223+
}
2224+
}
2225+
2226+
fn gpu_driver_config_from_cli(gpu_device_ids: &[String]) -> Option<prost_types::Struct> {
2227+
use prost_types::{ListValue, Struct, Value, value::Kind};
2228+
2229+
fn string_value(value: &str) -> Value {
2230+
Value {
2231+
kind: Some(Kind::StringValue(value.to_string())),
2232+
}
2233+
}
2234+
2235+
fn driver_block(gpu_device_ids: &[String]) -> Value {
2236+
Value {
2237+
kind: Some(Kind::StructValue(Struct {
2238+
fields: std::iter::once((
2239+
"gpu_device_ids".to_string(),
2240+
Value {
2241+
kind: Some(Kind::ListValue(ListValue {
2242+
values: gpu_device_ids
2243+
.iter()
2244+
.map(|device_id| string_value(device_id))
2245+
.collect(),
2246+
})),
2247+
},
2248+
))
2249+
.collect(),
2250+
})),
2251+
}
2252+
}
2253+
2254+
if gpu_device_ids.is_empty() {
2255+
return None;
2256+
}
2257+
2258+
Some(Struct {
2259+
fields: [
2260+
("docker".to_string(), driver_block(gpu_device_ids)),
2261+
("podman".to_string(), driver_block(gpu_device_ids)),
2262+
("vm".to_string(), driver_block(gpu_device_ids)),
2263+
]
2264+
.into_iter()
2265+
.collect(),
22112266
})
22122267
}
22132268

@@ -7460,7 +7515,8 @@ mod tests {
74607515
dockerfile_sources_supported_for_gateway, format_endpoint, format_gateway_select_header,
74617516
format_gateway_select_items, format_provider_attachment_table, gateway_add,
74627517
gateway_auth_label, gateway_env_override_warning, gateway_select_with, gateway_type_label,
7463-
git_sync_files, http_health_check, image_requests_gpu, import_local_package_mtls_bundle,
7518+
git_sync_files, gpu_count_from_cli, gpu_device_ids_from_cli, gpu_driver_config_from_cli,
7519+
http_health_check, image_requests_gpu, import_local_package_mtls_bundle,
74647520
inferred_provider_type, package_managed_tls_dirs, parse_cli_setting_value,
74657521
parse_credential_expiry_cli_value, parse_credential_expiry_pairs, parse_credential_pairs,
74667522
plaintext_gateway_is_remote, progress_step_from_metadata,
@@ -7946,49 +8002,65 @@ mod tests {
79468002
}
79478003
}
79488004

8005+
#[test]
8006+
fn gpu_device_ids_from_cli_trims_gpu_device() {
8007+
assert_eq!(
8008+
gpu_device_ids_from_cli(Some(" nvidia.com/gpu=0 ")),
8009+
vec!["nvidia.com/gpu=0".to_string()]
8010+
);
8011+
}
8012+
8013+
#[test]
8014+
fn gpu_device_ids_from_cli_omits_empty_device() {
8015+
assert!(gpu_device_ids_from_cli(Some(" ")).is_empty());
8016+
assert!(gpu_device_ids_from_cli(None).is_empty());
8017+
}
8018+
8019+
#[test]
8020+
fn gpu_count_from_cli_uses_gpu_device_id_count() {
8021+
let device_ids = gpu_device_ids_from_cli(Some("nvidia.com/gpu=0"));
8022+
8023+
assert_eq!(gpu_count_from_cli(None, &device_ids), Some(1));
8024+
assert_eq!(gpu_count_from_cli(Some(2), &device_ids), Some(1));
8025+
}
8026+
79498027
#[test]
79508028
fn resource_requirements_from_cli_uses_presence_for_default_gpu() {
7951-
let requirements = resource_requirements_from_cli(true, None, None)
8029+
let requirements = resource_requirements_from_cli(true, None)
79528030
.expect("resource requirements should be present");
79538031
let gpu = requirements.gpu.expect("GPU requirement should be present");
79548032

7955-
assert!(gpu.device_ids.is_empty());
79568033
assert_eq!(gpu.count, None);
79578034
}
79588035

79598036
#[test]
7960-
fn resource_requirements_from_cli_maps_gpu_device_to_one_device_id() {
7961-
let requirements = resource_requirements_from_cli(true, Some("0000:2d:00.0"), None)
7962-
.expect("resource requirements should be present");
7963-
let gpu = requirements.gpu.expect("GPU requirement should be present");
8037+
fn gpu_driver_config_from_cli_maps_gpu_device_to_driver_blocks() {
8038+
let device_ids = gpu_device_ids_from_cli(Some("nvidia.com/gpu=0"));
8039+
let config =
8040+
gpu_driver_config_from_cli(&device_ids).expect("driver config should be present");
79648041

7965-
assert_eq!(gpu.device_ids, vec!["0000:2d:00.0"]);
7966-
assert_eq!(gpu.count, None);
8042+
assert!(config.fields.contains_key("docker"));
8043+
assert!(config.fields.contains_key("podman"));
8044+
assert!(config.fields.contains_key("vm"));
79678045
}
79688046

79698047
#[test]
79708048
fn resource_requirements_from_cli_maps_gpu_count() {
79718049
let requirements =
7972-
resource_requirements_from_cli(true, None, Some(2)).expect("requirements should exist");
8050+
resource_requirements_from_cli(true, Some(2)).expect("requirements should exist");
79738051
let gpu = requirements.gpu.expect("GPU requirement should be present");
79748052

7975-
assert!(gpu.device_ids.is_empty());
79768053
assert_eq!(gpu.count, Some(2));
79778054
}
79788055

79798056
#[test]
7980-
fn resource_requirements_from_cli_preserves_device_and_gpu_count_for_gateway_validation() {
7981-
let requirements = resource_requirements_from_cli(true, Some("nvidia.com/gpu=0"), Some(2))
7982-
.expect("requirements should exist");
7983-
let gpu = requirements.gpu.expect("GPU requirement should be present");
7984-
7985-
assert_eq!(gpu.device_ids, vec!["nvidia.com/gpu=0"]);
7986-
assert_eq!(gpu.count, Some(2));
8057+
fn gpu_driver_config_from_cli_omits_empty_device() {
8058+
assert!(gpu_driver_config_from_cli(&[]).is_none());
79878059
}
79888060

79898061
#[test]
79908062
fn resource_requirements_from_cli_omits_gpu_request_when_not_requested() {
7991-
assert!(resource_requirements_from_cli(false, Some("0"), None).is_none());
8063+
assert!(resource_requirements_from_cli(false, None).is_none());
79928064
}
79938065

79948066
#[test]

crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -922,15 +922,23 @@ async fn sandbox_create_sends_gpu_count_request() {
922922
.expect("sandbox create should succeed");
923923

924924
let requests = create_requests(&server).await;
925-
let gpu = requests[0]
925+
let spec = requests[0]
926926
.spec
927927
.as_ref()
928-
.and_then(|spec| spec.resource_requirements.as_ref())
928+
.expect("sandbox spec should be sent");
929+
let gpu = spec
930+
.resource_requirements
931+
.as_ref()
929932
.and_then(|requirements| requirements.gpu.as_ref())
930933
.expect("GPU request should be sent");
931934

932-
assert!(gpu.device_ids.is_empty());
933935
assert_eq!(gpu.count, Some(2));
936+
assert!(
937+
spec.template
938+
.as_ref()
939+
.and_then(|template| template.driver_config.as_ref())
940+
.is_none()
941+
);
934942
}
935943

936944
#[tokio::test]

0 commit comments

Comments
 (0)