Skip to content

Commit 9506f16

Browse files
committed
feat(gpu)!: add resource requirements
BREAKING CHANGE: SandboxSpec.gpu and DriverSandboxSpec.gpu were replaced with resource_requirements.gpu. This intentionally reuses protobuf field 9, changing it from a bool to a message for both public and driver APIs. Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent d64542f commit 9506f16

20 files changed

Lines changed: 444 additions & 153 deletions

File tree

architecture/compute-runtimes.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ users.
9898
Custom sandbox images must include the agent runtime and any system
9999
dependencies, but they should not need to include the gateway. GPU-capable
100100
images must include the user-space libraries required by the workload. The
101-
runtime still owns GPU device injection.
101+
runtime still owns GPU device injection. GPU requests are explicit, and can be
102+
refined with a driver-native device identifier; the gateway validates the
103+
request shape and each runtime enforces the GPU allocation modes it supports.
102104

103105
## Deployment Shape
104106

crates/openshell-cli/src/main.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use openshell_bootstrap::{
1919
use openshell_cli::completers;
2020
use openshell_cli::run;
2121
use openshell_cli::tls::TlsOptions;
22+
use openshell_core::proto::GpuResourceRequirements;
2223

2324
/// Resolved gateway context: name + gateway endpoint.
2425
struct GatewayContext {
@@ -2625,6 +2626,7 @@ async fn main() -> Result<()> {
26252626
.map(|s| openshell_core::forward::ForwardSpec::parse(&s))
26262627
.transpose()?;
26272628
let keep = keep || !no_keep || editor.is_some() || forward.is_some();
2629+
let gpu_requirements = gpu.then_some(GpuResourceRequirements {});
26282630

26292631
let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
26302632
let endpoint = &ctx.endpoint;
@@ -2637,7 +2639,7 @@ async fn main() -> Result<()> {
26372639
&ctx.name,
26382640
&upload_specs,
26392641
keep,
2640-
gpu,
2642+
gpu_requirements,
26412643
cpu.as_deref(),
26422644
memory.as_deref(),
26432645
driver_config_json.as_deref(),
@@ -4495,6 +4497,39 @@ mod tests {
44954497
}
44964498
}
44974499

4500+
#[test]
4501+
fn sandbox_create_gpu_parses_driver_default() {
4502+
let cli = Cli::try_parse_from(["openshell", "sandbox", "create", "--gpu"])
4503+
.expect("sandbox create --gpu should parse");
4504+
4505+
match cli.command {
4506+
Some(Commands::Sandbox {
4507+
command: Some(SandboxCommands::Create { gpu, .. }),
4508+
..
4509+
}) => {
4510+
assert!(gpu);
4511+
}
4512+
other => panic!("expected SandboxCommands::Create, got: {other:?}"),
4513+
}
4514+
}
4515+
4516+
#[test]
4517+
fn sandbox_create_gpu_driver_default_allows_trailing_command() {
4518+
let cli = Cli::try_parse_from(["openshell", "sandbox", "create", "--gpu", "--", "claude"])
4519+
.expect("sandbox create --gpu -- claude should parse");
4520+
4521+
match cli.command {
4522+
Some(Commands::Sandbox {
4523+
command: Some(SandboxCommands::Create { gpu, command, .. }),
4524+
..
4525+
}) => {
4526+
assert!(gpu);
4527+
assert_eq!(command, vec!["claude".to_string()]);
4528+
}
4529+
other => panic!("expected SandboxCommands::Create, got: {other:?}"),
4530+
}
4531+
}
4532+
44984533
#[test]
44994534
fn service_expose_accepts_positional_target_port_and_service() {
45004535
let cli = Cli::try_parse_from([

crates/openshell-cli/src/run.rs

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,13 @@ use openshell_core::proto::{
4242
GetClusterInferenceRequest, GetDraftHistoryRequest, GetDraftPolicyRequest,
4343
GetGatewayConfigRequest, GetProviderProfileRequest, GetProviderRefreshStatusRequest,
4444
GetProviderRequest, GetSandboxConfigRequest, GetSandboxLogsRequest,
45-
GetSandboxPolicyStatusRequest, GetSandboxRequest, GetServiceRequest, HealthRequest,
46-
ImportProviderProfilesRequest, LintProviderProfilesRequest, ListProviderProfilesRequest,
47-
ListProvidersRequest, ListSandboxPoliciesRequest, ListSandboxProvidersRequest,
48-
ListSandboxesRequest, ListServicesRequest, PlatformEvent, PolicySource, PolicyStatus, Provider,
49-
ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, ProviderProfile,
50-
ProviderProfileDiagnostic, ProviderProfileImportItem, RejectDraftChunkRequest,
45+
GetSandboxPolicyStatusRequest, GetSandboxRequest, GetServiceRequest, GpuResourceRequirements,
46+
HealthRequest, ImportProviderProfilesRequest, LintProviderProfilesRequest,
47+
ListProviderProfilesRequest, ListProvidersRequest, ListSandboxPoliciesRequest,
48+
ListSandboxProvidersRequest, ListSandboxesRequest, ListServicesRequest, PlatformEvent,
49+
PolicySource, PolicyStatus, Provider, ProviderCredentialRefreshStatus,
50+
ProviderCredentialRefreshStrategy, ProviderProfile, ProviderProfileDiagnostic,
51+
ProviderProfileImportItem, RejectDraftChunkRequest, ResourceRequirements,
5152
RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox, SandboxPhase, SandboxPolicy,
5253
SandboxSpec, SandboxTemplate, ServiceEndpointResponse, SetClusterInferenceRequest,
5354
SettingScope, SettingValue, TcpForwardFrame, TcpForwardInit, TcpRelayTarget,
@@ -123,7 +124,7 @@ fn ready_false_condition_message(
123124

124125
fn provisioning_timeout_message(
125126
timeout_secs: u64,
126-
requested_gpu: bool,
127+
resource_requirements: Option<&ResourceRequirements>,
127128
condition_message: Option<&str>,
128129
) -> String {
129130
let mut message = format!("sandbox provisioning timed out after {timeout_secs}s");
@@ -133,7 +134,7 @@ fn provisioning_timeout_message(
133134
message.push_str(condition_message);
134135
}
135136

136-
if requested_gpu {
137+
if resource_requirements.is_some_and(|requirements| requirements.gpu.is_some()) {
137138
message.push_str(
138139
". Hint: this may be because the available GPU is already in use by another sandbox.",
139140
);
@@ -1753,7 +1754,7 @@ pub async fn sandbox_create(
17531754
gateway_name: &str,
17541755
uploads: &[(String, Option<String>, bool)],
17551756
keep: bool,
1756-
gpu: bool,
1757+
gpu_requirements: Option<GpuResourceRequirements>,
17571758
cpu: Option<&str>,
17581759
memory: Option<&str>,
17591760
driver_config_json: Option<&str>,
@@ -1809,8 +1810,6 @@ pub async fn sandbox_create(
18091810
}
18101811
None => None,
18111812
};
1812-
let requested_gpu = gpu;
1813-
18141813
let providers_v2_enabled = gateway_providers_v2_enabled(&mut client).await?;
18151814
let inferred_types: Vec<String> = if providers_v2_enabled {
18161815
Vec::new()
@@ -1842,9 +1841,11 @@ pub async fn sandbox_create(
18421841
None
18431842
};
18441843

1844+
let resource_requirements = gpu_requirements.map(|gpu| ResourceRequirements { gpu: Some(gpu) });
1845+
18451846
let request = CreateSandboxRequest {
18461847
spec: Some(SandboxSpec {
1847-
gpu: requested_gpu,
1848+
resource_requirements,
18481849
environment: environment.clone(),
18491850
policy,
18501851
providers: configured_providers,
@@ -1989,7 +1990,7 @@ pub async fn sandbox_create(
19891990
if remaining.is_zero() {
19901991
let timeout_message = provisioning_timeout_message(
19911992
provision_timeout.as_secs(),
1992-
requested_gpu,
1993+
resource_requirements.as_ref(),
19931994
last_condition_message.as_deref(),
19941995
);
19951996
if let Some(d) = display.as_mut() {
@@ -2008,7 +2009,7 @@ pub async fn sandbox_create(
20082009
// Timeout fired — the stream was idle for too long.
20092010
let timeout_message = provisioning_timeout_message(
20102011
provision_timeout.as_secs(),
2011-
requested_gpu,
2012+
resource_requirements.as_ref(),
20122013
last_condition_message.as_deref(),
20132014
);
20142015
if let Some(d) = display.as_mut() {
@@ -7686,9 +7687,10 @@ mod tests {
76867687
PROGRESS_STEP_STARTING_SANDBOX,
76877688
};
76887689
use openshell_core::proto::{
7689-
Provider, ProviderCredentialRefresh, ProviderCredentialRefreshStatus,
7690-
ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrant, ProviderProfile,
7691-
ProviderProfileCredential, SandboxCondition, SandboxStatus, datamodel::v1::ObjectMeta,
7690+
GpuResourceRequirements, Provider, ProviderCredentialRefresh,
7691+
ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy,
7692+
ProviderCredentialTokenGrant, ProviderProfile, ProviderProfileCredential,
7693+
ResourceRequirements, SandboxCondition, SandboxStatus, datamodel::v1::ObjectMeta,
76927694
};
76937695

76947696
struct EnvVarGuard {
@@ -8392,9 +8394,12 @@ mod tests {
83928394

83938395
#[test]
83948396
fn provisioning_timeout_message_includes_condition_and_gpu_hint() {
8397+
let resource_requirements = ResourceRequirements {
8398+
gpu: Some(GpuResourceRequirements {}),
8399+
};
83958400
let message = provisioning_timeout_message(
83968401
120,
8397-
true,
8402+
Some(&resource_requirements),
83988403
Some("DependenciesNotReady: Pod exists with phase: Pending; Service Exists"),
83998404
);
84008405

@@ -8405,7 +8410,15 @@ mod tests {
84058410

84068411
#[test]
84078412
fn provisioning_timeout_message_omits_gpu_hint_for_non_gpu_requests() {
8408-
let message = provisioning_timeout_message(120, false, None);
8413+
let message = provisioning_timeout_message(120, None, None);
8414+
8415+
assert_eq!(message, "sandbox provisioning timed out after 120s");
8416+
}
8417+
8418+
#[test]
8419+
fn provisioning_timeout_message_omits_gpu_hint_without_gpu_requirements() {
8420+
let resource_requirements = ResourceRequirements { gpu: None };
8421+
let message = provisioning_timeout_message(120, Some(&resource_requirements), None);
84098422

84108423
assert_eq!(message, "sandbox provisioning timed out after 120s");
84118424
}

0 commit comments

Comments
 (0)