Skip to content

Commit a226806

Browse files
authored
test(e2e): run gpu workloads from manifest (#1709)
* test(e2e): add workload manifest build flow Signed-off-by: Evan Lezar <elezar@nvidia.com> * test(e2e): add gpu workload validation tests Signed-off-by: Evan Lezar <elezar@nvidia.com> * ci(e2e): build gpu workloads before gpu e2e Signed-off-by: Evan Lezar <elezar@nvidia.com> --------- Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent a5161d0 commit a226806

11 files changed

Lines changed: 421 additions & 44 deletions

File tree

.github/workflows/e2e-gpu-test.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@ jobs:
4848
OPENSHELL_REGISTRY_NAMESPACE: nvidia/openshell
4949
OPENSHELL_REGISTRY_USERNAME: ${{ github.actor }}
5050
OPENSHELL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
51+
CONTAINER_ENGINE: docker
5152
OPENSHELL_E2E_DOCKER_GPU: "1"
5253
# NVIDIA-managed Ubuntu base used as the GPU probe target: it has the
5354
# filesystem layout CDI injection expects (ldconfig, populated /usr/bin)
5455
# which the distroless gateway runtime lacks. Consumed by the prereq
55-
# probe below and by the e2e tests in e2e/rust/tests/gpu_device_selection.rs.
56+
# probe below and by the e2e tests in e2e/rust/tests/gpu/device_selection.rs.
5657
OPENSHELL_E2E_GPU_PROBE_IMAGE: "nvcr.io/nvidia/base/ubuntu:noble-20251013"
5758
steps:
5859
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -65,5 +66,8 @@ jobs:
6566
docker info --format '{{json .CDISpecDirs}}'
6667
docker run --rm --device nvidia.com/gpu=all "${OPENSHELL_E2E_GPU_PROBE_IMAGE}" nvidia-smi -L
6768
69+
- name: Build GPU workload images
70+
run: mise run --no-deps --skip-deps e2e:workloads:build
71+
6872
- name: Run tests
6973
run: mise run --no-deps --skip-deps e2e:docker:gpu

e2e/gpu/README.md

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
# GPU workload images
55

6-
This directory defines workload test images for OpenShell GPU validation.
6+
This directory defines workload test images currently used by the OpenShell GPU
7+
e2e suite.
78

89
## Contract
910

@@ -22,11 +23,10 @@ Each workload image must:
2223
command explicitly.
2324

2425
OpenShell sandbox creation replaces the image entrypoint with the supervisor and
25-
does not run the OCI image `CMD`. When these images are used through OpenShell,
26-
the workload command from each manifest entry must be passed explicitly.
26+
does not run the OCI image `CMD`. E2e tests that use these images through
27+
OpenShell run the command from each manifest entry explicitly.
2728

28-
The image build task writes a local workload manifest. Each workload entry
29-
carries:
29+
The test harness is manifest-driven. Each workload entry carries:
3030

3131
- `name`
3232
- `image`
@@ -61,18 +61,17 @@ The build task uses `tasks/scripts/container-engine.sh`. Set
6161
`CONTAINER_ENGINE=docker` or `CONTAINER_ENGINE=podman` to choose an engine
6262
explicitly. When unset, the helper uses its existing auto-detection behavior.
6363

64-
Local tags use the current commit short SHA plus a short fingerprint of the
65-
external build inputs. Dirty local trees append `-dirty`. Set
66-
`OPENSHELL_GPU_WORKLOAD_IMAGE_TAG=<tag>` to override the tag.
64+
Local tags use a short SHA-256 fingerprint of the selected workload contexts
65+
and external build inputs. Set `OPENSHELL_GPU_WORKLOAD_IMAGE_TAG=<tag>` to
66+
override the tag.
6767

6868
The task writes the latest build refs to:
6969

7070
```text
7171
e2e/gpu/images/.build/latest.env
7272
```
7373

74-
The task also writes a local workload manifest for downstream tooling and
75-
future workload-runner integration:
74+
The task also writes the local workload manifest used by the Rust e2e runner:
7675

7776
```text
7877
e2e/gpu/images/.build/workloads.yaml
@@ -90,8 +89,7 @@ source e2e/gpu/images/.build/latest.env
9089
```
9190

9291
That env file exports `OPENSHELL_E2E_WORKLOAD_MANIFEST` pointing at the local
93-
manifest. The current checked-in Rust GPU e2e target does not consume this
94-
manifest yet. The per-image refs remain available as a convenience for direct
92+
manifest. The per-image refs remain available as a convenience for direct
9593
container-engine validation.
9694

9795
## Direct Validation
@@ -124,14 +122,63 @@ where Podman CDI is configured.
124122
Direct container-engine validation catches image, CDI, CUDA, and host GPU setup
125123
issues before OpenShell sandbox behavior is involved.
126124

127-
## OpenShell GPU E2E
125+
## Manifest-Driven Validation
128126

129-
The current Rust GPU validation target is:
127+
Run manifest-driven GPU validation through the e2e tasks so the workload
128+
images, manifest, gateway, and container-engine environment match CI:
130129

131130
```shell
132-
mise run e2e:gpu
131+
mise run e2e:workloads:build
132+
mise run e2e:docker:gpu
133+
```
134+
135+
For Podman GPU validation, build the manifest with
136+
`CONTAINER_ENGINE=podman mise run e2e:workloads:build`, then run
137+
`mise run e2e:podman:gpu`.
138+
139+
The workload validation path reads:
140+
141+
```text
142+
OPENSHELL_E2E_WORKLOAD_MANIFEST
143+
```
144+
145+
When that variable is unset, the runner uses the default local manifest path:
146+
147+
```text
148+
e2e/gpu/images/.build/workloads.yaml
149+
```
150+
151+
If neither path exists, the workload validation test prints a clear skip
152+
message telling you to run:
153+
154+
```shell
155+
mise run e2e:workloads:build
156+
```
157+
158+
or to set `OPENSHELL_E2E_WORKLOAD_MANIFEST` to an external manifest.
159+
160+
Each manifest entry supplies the sandbox image and command. OpenShell runs that
161+
command through `openshell sandbox create --gpu --from <image> -- <command>`.
162+
The test runner iterates all GPU-tagged workload entries and enforces each
163+
entry's declared expectation:
164+
165+
- `expect: pass` requires `OPENSHELL_GPU_WORKLOAD_SUCCESS`
166+
- `expect: fail` requires `OPENSHELL_GPU_WORKLOAD_FAILURE`
167+
168+
The current local manifest includes three workloads:
169+
170+
- `smoke-pass` expected to pass
171+
- `smoke-fail` expected to fail
172+
- `cuda-basic` expected to pass
173+
174+
## External Manifests
175+
176+
External workload catalogs can use the same schema. Point the runner at one
177+
with:
178+
179+
```shell
180+
export OPENSHELL_E2E_WORKLOAD_MANIFEST=/abs/path/to/workloads.yaml
133181
```
134182

135-
That target runs `gpu_device_selection`. It validates GPU request and device
136-
selection behavior against a Docker-backed gateway. It does not run the
137-
workload manifest generated by `mise run e2e:workloads:build`.
183+
That lets alternate workload manifests use the same test runner without
184+
introducing per-workload env vars.

e2e/rust/Cargo.lock

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

e2e/rust/Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ path = "tests/forward_proxy_jsonrpc_l7.rs"
103103
required-features = ["e2e-host-gateway"]
104104

105105
[[test]]
106-
name = "gpu_device_selection"
107-
path = "tests/gpu_device_selection.rs"
106+
name = "gpu"
107+
path = "tests/gpu.rs"
108108
required-features = ["e2e-gpu"]
109109

110110
[dependencies]
@@ -122,7 +122,12 @@ sha1 = "0.10"
122122
sha2 = "0.10"
123123
hex = "0.4"
124124
rand = "0.9"
125+
serde = { version = "1", features = ["derive"] }
125126
serde_json = "1"
127+
serde_yml = "0.0.12"
128+
129+
[dev-dependencies]
130+
serial_test = "3"
126131

127132
[lints.rust]
128133
unsafe_code = "warn"

e2e/rust/e2e-docker.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ set -euo pipefail
1111
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
1212
E2E_TEST="${OPENSHELL_E2E_DOCKER_TEST:-smoke}"
1313
E2E_FEATURES="${OPENSHELL_E2E_DOCKER_FEATURES:-e2e,e2e-docker}"
14+
DEFAULT_WORKLOAD_MANIFEST="${ROOT}/e2e/gpu/images/.build/workloads.yaml"
1415

1516
cargo build -p openshell-cli
1617

18+
if [ "${E2E_TEST}" = "gpu" ] && [ -z "${OPENSHELL_E2E_WORKLOAD_MANIFEST:-}" ] && [ ! -f "${DEFAULT_WORKLOAD_MANIFEST}" ]; then
19+
echo "note: running GPU e2e without a workload manifest; workload validation will log an explicit skip. Build one with 'mise run e2e:workloads:build' or set OPENSHELL_E2E_WORKLOAD_MANIFEST."
20+
fi
21+
1722
exec "${ROOT}/e2e/with-docker-gateway.sh" \
1823
cargo test --manifest-path "${ROOT}/e2e/rust/Cargo.toml" \
1924
--features "${E2E_FEATURES}" \

e2e/rust/e2e-podman.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ set -euo pipefail
1111
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
1212
E2E_TEST="${OPENSHELL_E2E_PODMAN_TEST:-}"
1313
E2E_FEATURES="${OPENSHELL_E2E_PODMAN_FEATURES:-e2e-podman}"
14+
DEFAULT_WORKLOAD_MANIFEST="${ROOT}/e2e/gpu/images/.build/workloads.yaml"
1415

1516
cargo build -p openshell-cli
1617

18+
if [ "${E2E_TEST}" = "gpu" ] && [ -z "${OPENSHELL_E2E_WORKLOAD_MANIFEST:-}" ] && [ ! -f "${DEFAULT_WORKLOAD_MANIFEST}" ]; then
19+
echo "note: running Podman GPU e2e without a workload manifest; workload validation will log an explicit skip. Build one with 'CONTAINER_ENGINE=podman mise run e2e:workloads:build' or set OPENSHELL_E2E_WORKLOAD_MANIFEST."
20+
fi
21+
1722
TEST_ARGS=(
1823
cargo test --manifest-path "${ROOT}/e2e/rust/Cargo.toml"
1924
--features "${E2E_FEATURES}"

e2e/rust/tests/gpu.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#![cfg(feature = "e2e-gpu")]
5+
6+
// GPU-consuming e2e tests use #[serial(gpu)] because common single-GPU hosts
7+
// cannot reliably provision multiple GPU sandboxes at the same time.
8+
9+
#[path = "gpu/device_selection.rs"]
10+
mod device_selection;
11+
#[path = "gpu/workloads.rs"]
12+
mod workloads;
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
#![cfg(feature = "e2e-gpu")]
5-
64
//! GPU device selection e2e tests.
75
//!
86
//! Requires a GPU-backed gateway and a sandbox image containing `nvidia-smi`.
@@ -15,6 +13,7 @@ use openshell_e2e::harness::container::{ContainerEngine, e2e_driver};
1513
use openshell_e2e::harness::output::strip_ansi;
1614
use openshell_e2e::harness::sandbox::SandboxGuard;
1715
use serde_json::{Map, Value};
16+
use serial_test::serial;
1817
use tokio::time::timeout;
1918

2019
const SANDBOX_CREATE_TIMEOUT: Duration = Duration::from_secs(600);
@@ -340,6 +339,7 @@ async fn sandbox_create_output(args: &[&str]) -> String {
340339
}
341340

342341
#[tokio::test]
342+
#[serial(gpu)]
343343
async fn gpu_request_without_device_matches_plain_default_gpu_container() {
344344
let device_ids = discovered_cdi_gpu_device_ids();
345345
let Some(default_gpu_device) =
@@ -359,6 +359,7 @@ async fn gpu_request_without_device_matches_plain_default_gpu_container() {
359359
}
360360

361361
#[tokio::test]
362+
#[serial(gpu)]
362363
async fn gpu_request_for_each_discovered_device_matches_plain_container() {
363364
let device_ids: Vec<_> = discovered_cdi_gpu_device_ids()
364365
.into_iter()
@@ -383,6 +384,7 @@ async fn gpu_request_for_each_discovered_device_matches_plain_container() {
383384
}
384385

385386
#[tokio::test]
387+
#[serial(gpu)]
386388
async fn gpu_all_device_request_matches_plain_all_gpu_container() {
387389
if !has_cdi_gpu_device(CDI_GPU_DEVICE_ALL) {
388390
eprintln!(
@@ -401,6 +403,7 @@ async fn gpu_all_device_request_matches_plain_all_gpu_container() {
401403
}
402404

403405
#[tokio::test]
406+
#[serial(gpu)]
404407
async fn gpu_invalid_device_request_fails() {
405408
let driver_config_json = cdi_devices_driver_config_json(&["nvidia.com/gpu=invalid"]);
406409
let args = vec![

0 commit comments

Comments
 (0)