Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 183 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ on:
description: Which runner(s) to target
type: choice
default: all
options: [all, mock, app-dev-gpu, strix-ubuntu, strix-windows]
options: [all, mock, app-dev-gpu, strix-ubuntu, strix-windows, strix-wsl]
# No tier input: the harness resolves pass/xfail/skip per scenario, so a
# platform runs one job covering everything applicable to it.
# Scenario-name regex forwarded to the cucumber harness
Expand Down Expand Up @@ -708,6 +708,187 @@ jobs:
name: e2e-report
path: tests/e2e-cucumber/results/

# WSL2 coverage on real hardware: the runner is an Ubuntu distro running under
# WSL2 on the Strix Halo Windows box, so this lane exercises the WSL host
# boundary AND whatever GPU access WSL exposes. Same suite as every other
# platform — scenarios the host cannot satisfy resolve to skip from the
# capability probe, so nothing is filtered out here. `wsl` in `runs-on`
# disambiguates it from the `native` Strix Linux runner. Non-blocking while
# GPU-on-WSL is proven out.
e2e-wsl:
name: E2E tests (Strix Halo, WSL2)
# 35min: matches the sibling collapsed GPU jobs — one job runs all serves
# plus per-scenario `install sdk`, and the cap must exceed the run so the job
# still writes platform.json.
timeout-minutes: 35
runs-on: [self-hosted, linux, strix-halo, wsl]
needs: [changes, build-and-test]
# See `e2e`: dispatch tolerates skipped build-and-test; strix-wsl.
if: >-
always()
&& needs.changes.result == 'success'
&& (
(github.event_name != 'workflow_dispatch'
&& needs.build-and-test.result == 'success'
&& needs.changes.outputs.heavy == 'true')
|| (github.event_name == 'workflow_dispatch'
&& (inputs.platform == 'all' || inputs.platform == 'strix-wsl'))
)
continue-on-error: true
env:
E2E_SERVE_TIMEOUT_SECS: "300"
# Match the other hardware lanes: opt into the platform-adaptive
# large-model scenario only when the manual include_nightly input is on.
E2E_INCLUDE_NIGHTLY: "${{ inputs.include_nightly && '1' || '' }}"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# Fail fast and loudly if this job ever lands on a native Linux runner:
# every WSL-tagged scenario would silently resolve to skip instead.
- name: Verify the host really is WSL2
run: |
if ! grep -qi microsoft /proc/sys/kernel/osrelease; then
echo "::error::runner is not a WSL host: $(uname -r)"
exit 1
fi
printf 'WSL kernel: %s\n' "$(uname -r)"

# The native Strix runners come pre-provisioned; a WSL distro often does
# not. Install the native build deps only when something is missing, and
# only if passwordless sudo is available — otherwise say what is missing
# instead of hanging on a password prompt.
- name: Ensure native build deps
run: |
missing=""
command -v pkg-config >/dev/null 2>&1 || missing="$missing pkg-config"
command -v cc >/dev/null 2>&1 || missing="$missing build-essential"
pkg-config --exists libcap 2>/dev/null || missing="$missing libcap-dev"
if [ -z "$missing" ]; then
echo "native build deps present"
elif sudo -n true 2>/dev/null; then
echo "installing:$missing"
sudo -n apt-get update
# shellcheck disable=SC2086
sudo -n apt-get install -y $missing
else
echo "::error::missing native build deps:$missing (no passwordless sudo to install them)"
exit 1
fi

- name: Reclaim GPU from stray E2E processes
run: |
# Reclaim from any serve leaked by a killed/timed-out prior run
# (see e2e-gpu). Scoped to e2e leftovers only.
pkill -f '/tmp/rocm-e2e.*llama-server' 2>/dev/null || true
pkill -f '/tmp/rocm-e2e.*vllm serve' 2>/dev/null || true
pkill -f 'e2e-shared.*llama-server' 2>/dev/null || true
pkill -f '__engine-serve-http.*rocm-e2e' 2>/dev/null || true
rm -rf /tmp/rocm-e2e-* 2>/dev/null || true
echo "reclaimed"

# GPU preflight, bounded like the native lanes — but ADVISORY here. GPU
# access under WSL is exactly what this lane is proving out, so an absent
# rocm-smi is a reported condition, not a job failure: the capability probe
# then resolves @requires-gpu scenarios to skip and the rest still runs. A
# GPU that IS present but stays held by a leftover serve still fails, since
# that would corrupt the serve scenarios' results.
- name: GPU preflight (advisory bounded wait)
run: |
MIN_FREE_GIB="${GPU_PREFLIGHT_MIN_FREE_GIB:-8}"
CEILING_SECS="${GPU_PREFLIGHT_CEILING_SECS:-90}"
if ! command -v rocm-smi >/dev/null 2>&1; then
echo "::warning::rocm-smi not found in this WSL distro — GPU scenarios will resolve to skip"
exit 0
fi
min_free=$(( MIN_FREE_GIB * 1024 * 1024 * 1024 ))
deadline=$(( SECONDS + CEILING_SECS ))
saw_vram=0
while [ "$SECONDS" -lt "$deadline" ]; do
out=$(timeout 15 rocm-smi --showmeminfo vram 2>/dev/null) || { sleep 5; continue; }
total=$(printf '%s\n' "$out" | grep -i 'VRAM Total Memory' | sed 's/.*: *//' | grep -oE '[0-9]+' | tail -1)
used=$(printf '%s\n' "$out" | grep -i 'VRAM Total Used Memory' | sed 's/.*: *//' | grep -oE '[0-9]+' | tail -1)
if [ -z "$total" ] || [ -z "$used" ]; then
sleep 5; continue
fi
saw_vram=1
free=$(( total - used ))
if [ "$free" -ge "$min_free" ]; then
echo "GPU ready: $(( free / 1024 / 1024 / 1024 )) GiB free (>= ${MIN_FREE_GIB} GiB)."
exit 0
fi
echo "waiting: $(( free / 1024 / 1024 / 1024 )) GiB free (< ${MIN_FREE_GIB} GiB)…"
sleep 5
done
if [ "$saw_vram" -eq 0 ]; then
echo "::warning::rocm-smi reported no VRAM figures under WSL — GPU scenarios will resolve to skip"
exit 0
fi
echo "::error::GPU preflight failed after ${CEILING_SECS}s: VRAM never dropped below the floor — a serve is likely still holding the GPU"
exit 1

# Bootstrap rustup with --no-modify-path so it never writes $HOME/.profile
# (setup-rust-toolchain doesn't expose that flag). rust-toolchain.toml pins
# the exact toolchain, installed on first cargo use. Idempotent.
- name: Ensure Rust toolchain
run: |
if ! command -v cargo >/dev/null 2>&1 && [ ! -x "$HOME/.cargo/bin/cargo" ]; then
curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs \
| sh -s -- -y --no-modify-path --default-toolchain none
fi
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"

- name: Run E2E tests on Strix Halo WSL2
run: |
export CARGO_TARGET_DIR="$RUNNER_WORKSPACE/e2e-target"
export E2E_SHARED_CACHE_DIR="$RUNNER_WORKSPACE/e2e-shared"
# Share ONE installed managed runtime across serve/chat scenarios and
# PRE-WARM it in place (mirrors e2e-gpu-strix-ubuntu). Required for
# correctness, not speed: `install sdk` bakes ABSOLUTE paths into the
# runtime manifest, so installing into a per-scenario temp dir leaves
# every later serve pointing at a deleted install root.
prewarm="$RUNNER_WORKSPACE/e2e-prewarm"
export E2E_SHARED_RUNTIMES_DIR="$prewarm/data/runtimes"

# Build the rocm binary once; reuse for pre-warm + suite so xtask
# doesn't rebuild.
cargo build --release -p rocm
export ROCM_CLI_BINARY="$CARGO_TARGET_DIR/release/rocm"

# Pre-warm once, serially, in place (no mv/symlink). Skipped once the
# tree is populated (persists across runs on RUNNER_WORKSPACE).
if [ ! -d "$E2E_SHARED_RUNTIMES_DIR/registry" ]; then
echo "pre-warming shared runtime (first run on this runner)…"
mkdir -p "$prewarm"/{data,config,cache}
ROCM_CLI_CONFIG_DIR="$prewarm/config" \
ROCM_CLI_DATA_DIR="$prewarm/data" \
ROCM_CLI_CACHE_DIR="$prewarm/cache" \
HF_HOME="$E2E_SHARED_CACHE_DIR/huggingface" \
"$ROCM_CLI_BINARY" install sdk
if [ -d "$E2E_SHARED_RUNTIMES_DIR/registry" ]; then
echo "shared runtime pre-warmed at $E2E_SHARED_RUNTIMES_DIR"
else
echo "pre-warm did not produce a runtimes registry; scenarios will install their own" >&2
fi
else
echo "shared runtime already present at $E2E_SHARED_RUNTIMES_DIR — skipping pre-warm"
fi

# Optional scenario-name filter for a scoped manual dispatch.
NAME_FILTER="${{ github.event.inputs.name_filter }}"
if [ -n "$NAME_FILTER" ]; then
echo "name filter active: $NAME_FILTER"
cargo xtask e2e -- --name "$NAME_FILTER"
else
cargo xtask e2e
fi

- name: Upload E2E report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-gpu-strix-wsl-report
path: tests/e2e-cucumber/results/

# app-dev MI300X (Instinct data-center GPU, `amd-gpu` label). Non-blocking.
# One job runs every applicable scenario (tiers collapsed). Cost drivers:
# `install sdk` re-running per scenario (isolated data dirs), several vLLM
Expand Down Expand Up @@ -1231,6 +1412,7 @@ jobs:
needs:
- changes
- e2e
- e2e-wsl
- e2e-gpu
- e2e-gpu-strix-ubuntu
- e2e-gpu-strix-windows
Expand Down
96 changes: 48 additions & 48 deletions docs/ci-hardware-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,59 @@ Copyright © Advanced Micro Devices, Inc., or its affiliates.
SPDX-License-Identifier: MIT
-->

# CI hardware (GPU / WSL) testing — planned
# CI hardware (GPU / WSL) testing

The hosted CI (`ubuntu-latest`, `windows-latest`) builds and unit-tests every
shipping target natively. By design it cannot exercise two things:
The E2E matrix covers native Linux, native Windows, and Ubuntu under WSL2.
GitHub-hosted runners have no AMD GPU, so every host-specific path — GPU
execution and the WSL boundary alike — runs on dedicated self-hosted runners.

- **Real AMD GPU execution** — GitHub-hosted runners have no AMD GPU.
- **Real WSL behaviour** — the Windows↔WSL interop path needs an actual WSL host.
## Platforms

A dedicated hardware-test layer covers exactly those gaps. It is **not** part of
the CI workflow yet; it will be introduced in a follow-up PR. This document
records the intended design so it can be reviewed and wired up as a unit.
Each job runs the same cucumber-rs suite. Capability tags resolve scenarios to
pass, expected failure, or not applicable for that host, and `e2e-report`
consolidates the resulting platform reports.

## Design

1. **Build once on hosted runners.** The hosted `build-and-test` (Linux) and
`windows-build-and-test` (Windows) jobs publish their per-OS binaries
(`rocm`, `rocmd`, `rocm-engine-*`) as workflow artifacts.
2. **Test on dedicated self-hosted runners.** Hardware-test jobs download those
exact artifacts and run only the checks hosted runners cannot: `rocm examine`
host/GPU detection, engine `detect`/`capabilities`, the no-CPU-fallback
smoke (`scripts/smoke_local.py --skip-build`), and the
`scripts/*_therock_gpu_test.py` end-to-end GPU harnesses.

### Targets

| Target | Runner | Notes |
| Job | Platform | Runner |
|---|---|---|
| Pure Windows 11 (gfx1151) | self-hosted Windows + AMD GPU | consumes the Windows artifact |
| AMD Instinct, bare-metal | self-hosted Linux + Instinct GPU | consumes the Linux artifact |
| Ubuntu on WSL (primary) | self-hosted Windows + WSL | consumes the Linux artifact in WSL |
| Fedora on WSL (secondary) | self-hosted Windows + WSL | builds in-WSL to match the distro's glibc |

### Guards

Each hardware-test job is:

- **Opt-in** via a repository/org variable (`ENABLE_WIN11_GPU_CI`,
`ENABLE_INSTINCT_CI`, `ENABLE_WSL_UBUNTU_CI`, `ENABLE_WSL_FEDORA_CI`) so it is
enabled per target as each runner is wired into the repo.
- **Non-blocking** (`continue-on-error: true`) — a hardware result never gates a PR.
- **Fork-safe** — self-hosted runners do not execute untrusted fork PRs.
| `e2e` | Mock (no GPU) | GitHub-hosted Ubuntu |
| `e2e-gpu` | MI300X | self-hosted Linux + AMD GPU |
| `e2e-gpu-strix-ubuntu` | Strix Halo (gfx1151) on Ubuntu | self-hosted Linux + AMD GPU |
| `e2e-gpu-strix-windows` | Strix Halo (gfx1151) on native Windows 11 | self-hosted Windows + AMD GPU |
| `e2e-wsl` | Strix Halo (gfx1151) on Ubuntu under WSL2 | self-hosted WSL2 on the Strix Halo box |

`e2e-wsl` runs on an Ubuntu distro hosted in WSL2 on the Strix Halo Windows
machine, targeted by the `wsl` runner label (the native Strix Linux runner
carries `native` instead). It builds the CLI in-distro and runs the same
black-box suite as every other platform, so it covers WSL host detection, the
Windows-to-WSL execution boundary, and whatever GPU access WSL exposes on that
box.

Nothing is filtered out by hand: the capability probe resolves each scenario
against the live host, so if GPU access is unavailable in the distro the
`@requires-gpu` scenarios report as not applicable rather than failing, and the
GPU preflight degrades to a warning instead of failing the job. Native-only
scenarios that the product deliberately routes around on WSL carry
`@requires-no-wsl`.

## Triggers and blocking behavior

The hardware jobs run automatically for heavy changes after the hosted build
succeeds. They can also be selected with `workflow_dispatch` using
`platform=app-dev-gpu`, `strix-ubuntu`, `strix-windows`, or `strix-wsl`.

Only the hosted Linux `e2e` job is blocking. `e2e-wsl` and the three GPU jobs
use `continue-on-error: true`, so they provide advisory platform coverage while
their failures remain visible in the consolidated report.

Self-hosted runners are not used for untrusted fork pull requests. Manual
workflow dispatch requires repository write access.

## Notes

- These jobs run **debug** binaries: they assert functional behaviour (device
detection, engine launch, policy enforcement), not performance, so the
debug-vs-release difference does not affect what they check. Release-fidelity,
optimized validation already lives in the nightly/release pipeline, which
builds `manylinux2014` (glibc 2.17) binaries.
- Linux artifacts are built on `ubuntu-latest`; a consuming runner on an older
distro must have a compatible (≥) glibc, otherwise it should build in-place
(as the Fedora-on-WSL target does).

The layer will be enabled end-to-end in a follow-up PR once the self-hosted
runners are connected to the repository and each path has been validated against
real hardware.
- E2E jobs build and run release binaries. They assert functional behavior such
as host detection, engine launch, and policy enforcement; they are not
performance benchmarks.
- WSL is its own platform slug (`strix-halo-wsl`, or plain `wsl` on a GPU-less
WSL host), so its results never collide with the native Strix Linux column.
- The WSL lane stays non-blocking until its distro provisioning and GPU access
have been validated end to end across several runs.
10 changes: 8 additions & 2 deletions tests/e2e-cucumber/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ Scenarios carry stable-id and capability tags:
|---|---|
| `@id:<slug>` | Stable scenario id. Keys the expectation matrix and the report grid; every scenario has one. |
| `@requires-gpu` | Needs a real AMD GPU. Resolves to **skip** (n/a) on a host with none (e.g. the mock job). |
| `@requires-wsl` | Needs a real WSL host. Resolves to **skip** on native Linux, native Windows, and other environments. |
| `@requires-no-wsl` | Needs native host behavior that WSL deliberately routes around. Resolves to **skip** on WSL. |
| `@requires-engine:<vllm\|lemonade>` | Pins the serve engine. Resolves to skip where that engine can't start (e.g. vLLM on a lemonade-only Strix host). |
| `@requires-os:<linux\|windows>` | Premise is OS-specific; skip on other OSes. |
| `@serve-timeout:<secs>` | Lengthen the serve-readiness wait for a genuinely slow serve (e.g. a large model). |
Expand All @@ -117,13 +119,17 @@ CI runs one job per platform, each executing the full suite:

| Job | Platform | Blocking |
|---|---|---|
| `e2e` | Mock (no GPU, GitHub-hosted) | yes |
| `e2e` | Mock (no GPU, GitHub-hosted Linux) | yes |
| `e2e-gpu` | MI300X (self-hosted) | no |
| `e2e-gpu-strix-ubuntu` | Strix Halo / Ubuntu (self-hosted) | no |
| `e2e-gpu-strix-windows` | Strix Halo / Windows (self-hosted) | no |
| `e2e-wsl` | Strix Halo / Ubuntu under WSL2 (self-hosted) | no |

The blocking mock job passes when every applicable scenario is pass-or-xfail with
no XPASS or unexpected failure; the GPU jobs are non-blocking. The `e2e-report`
no XPASS or unexpected failure. The GPU and WSL jobs are non-blocking. The WSL
job runs in an Ubuntu distro under WSL2 on the Strix Halo box and executes the
same full suite, validating the WSL host boundary, the in-distro build, and CLI
behavior; scenarios the host cannot satisfy resolve to skip. The `e2e-report`
job consolidates all platforms' results into one cross-platform report.

The nightly workflow runs three non-blocking jobs — the existing MI300X job and
Expand Down
14 changes: 8 additions & 6 deletions tests/e2e-cucumber/features/diagnose.feature
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ Feature: Diagnosing failures and listing fixes
# remediations. Both are black-box and GPU-independent (no serve, no download,
# no mutation), so every scenario here runs on the mock lane / per-PR tier.
#
# The catalog is OS-gated (the checkers only run on linux/windows), so these
# scenarios do NOT assert a specific fix-id — the top match is environment-
# dependent. They assert the SHAPE of a diagnosis (a scored match with an id
# and a plan) and the query/refusal contracts.
# The catalog is OS-gated (the checkers only run on native linux/windows), so
# match-shape scenarios are not applicable on WSL, where diagnose deliberately
# routes to platform-specific guidance without scoring the bare-metal catalog.
# On applicable hosts they do NOT assert a specific fix-id — the top match is
# environment-dependent. They assert the SHAPE of a diagnosis (a scored match
# with an id and a plan) and the query/refusal contracts.

@id:diagnose-matches-known-symptom
@id:diagnose-matches-known-symptom @requires-no-wsl
Scenario: 1 - Diagnosing a recognised failure reports a likely cause and a fix
Given a user who hit a known ROCm failure
When the user asks the CLI to diagnose that symptom
Expand All @@ -22,7 +24,7 @@ Feature: Diagnosing failures and listing fixes
When the user asks the CLI to diagnose that symptom in machine-readable form
Then the CLI always points to somewhere the problem can be reported

@id:diagnose-json-has-match-flag
@id:diagnose-json-has-match-flag @requires-no-wsl
Scenario: 3 - A diagnosis is available in machine-readable form for tooling
Given a user who hit a known ROCm failure
When the user asks the CLI to diagnose that symptom in machine-readable form
Expand Down
7 changes: 7 additions & 0 deletions tests/e2e-cucumber/features/examine.feature
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ Feature: GPU detection and system inspection
When the user inspects the system
Then the inspection reports the install as pre-existing
And the inspection suggests setting up a CLI-managed install

@id:examine-detects-wsl @requires-wsl
Scenario: 6 - System inspection recognizes a WSL host
Given the CLI is running in WSL
When the user inspects the system
Then the inspection reports Linux as the operating system
And the inspection reports that the host is WSL
Loading
Loading