Skip to content

Commit 4f39d2b

Browse files
committed
test(e2e): wait for GPU VRAM to drain before starting a new serve
ensure_serve_port_free freed the shared port but not device memory: a killed vLLM releases its VRAM only as the process fully exits, which lags the socket close. After a large model (Qwen3.6-27B, ~54 GiB) the residue dropped free VRAM below the next serve's gpu-memory-utilization request, so the following scenario died with "Free memory on device cuda:0 ... less than desired GPU memory utilization" (engine core init failed) — observed on app-dev where the 27B serve starved the next Qwen2.5-1.5B serve. Poll amd-smi (then rocm-smi) after the port frees and wait until enough VRAM is free before returning. Best-effort: if no smi tool exists (mock/local, no ROCm) it returns immediately, so non-GPU runs are unaffected. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
1 parent 2075ec3 commit 4f39d2b

1 file changed

Lines changed: 77 additions & 1 deletion

File tree

tests/e2e-cucumber/tests/e2e/serving_steps.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,88 @@ async fn ensure_serve_port_free() {
109109
.await
110110
.is_err();
111111
if free || Instant::now() >= deadline {
112-
return;
112+
break;
113113
}
114114
kill_listeners_on_port(SERVE_PORT);
115115
kill_listeners_on_port(ASSISTANT_PORT);
116116
tokio::time::sleep(Duration::from_secs(2)).await;
117117
}
118+
// The port closing does NOT mean the prior serve's VRAM is back: a killed
119+
// vLLM releases ~tens of GiB of device memory only as the process fully
120+
// exits, which lags the socket close. The next `rocm serve` reads free VRAM
121+
// at startup and demands `gpu-memory-utilization` of the TOTAL — after a large
122+
// model (e.g. 27B ~54 GiB) the residue can drop free memory below that
123+
// request, so the next serve dies with "Free memory ... less than desired GPU
124+
// memory utilization" (engine core init failed). Wait for the device to
125+
// actually drain before returning.
126+
wait_for_free_vram().await;
127+
}
128+
129+
/// Minimum free device VRAM (MiB) required before starting a new serve. Sized so
130+
/// the largest single scenario model can allocate its `gpu-memory-utilization`
131+
/// share without tripping vLLM's startup memory check. A generous floor: the
132+
/// suite's biggest model (Qwen3.6-27B, ~54 GiB) plus vLLM's ~0.8-of-total KV
133+
/// reservation needs the card mostly clear.
134+
const MIN_FREE_VRAM_MIB: u64 = 150_000;
135+
136+
/// Best-effort: wait until the GPU reports at least [`MIN_FREE_VRAM_MIB`] free,
137+
/// so a just-killed serve's memory is actually reclaimed before the next serve
138+
/// starts. Queries `amd-smi` then `rocm-smi`; if neither is present (mock/local,
139+
/// no ROCm), returns immediately so non-GPU runs are unaffected.
140+
async fn wait_for_free_vram() {
141+
// No GPU tooling → nothing to wait on (mock/local). Probe once up front.
142+
if free_vram_mib().is_none() {
143+
return;
144+
}
145+
let deadline = Instant::now() + Duration::from_secs(120);
146+
loop {
147+
match free_vram_mib() {
148+
Some(free) if free >= MIN_FREE_VRAM_MIB => return,
149+
_ if Instant::now() >= deadline => return,
150+
_ => tokio::time::sleep(Duration::from_secs(3)).await,
151+
}
152+
}
153+
}
154+
155+
/// Free device VRAM in MiB for GPU 0, via `amd-smi` (then `rocm-smi`). `None`
156+
/// when no such tool exists or its output can't be parsed.
157+
fn free_vram_mib() -> Option<u64> {
158+
use std::process::Command;
159+
// amd-smi: a line like " FREE_VRAM: 196309 MB"
160+
if let Ok(out) = Command::new("amd-smi").args(["metric", "-m"]).output()
161+
&& out.status.success()
162+
{
163+
let text = String::from_utf8_lossy(&out.stdout);
164+
if let Some(mib) = text
165+
.lines()
166+
.find_map(|l| l.trim().strip_prefix("FREE_VRAM:"))
167+
.and_then(|v| v.split_whitespace().next())
168+
.and_then(|n| n.parse::<u64>().ok())
169+
{
170+
return Some(mib);
171+
}
172+
}
173+
// rocm-smi fallback: `--showmeminfo vram --json` → vram total/used per card.
174+
if let Ok(out) = Command::new("rocm-smi")
175+
.args(["--showmeminfo", "vram", "--csv"])
176+
.output()
177+
&& out.status.success()
178+
{
179+
let text = String::from_utf8_lossy(&out.stdout);
180+
// CSV columns include "VRAM Total Memory (B)" and "VRAM Total Used Memory (B)".
181+
// Parse the first data row's total and used to derive free.
182+
let mut lines = text.lines();
183+
let header = lines.next()?;
184+
let cols: Vec<&str> = header.split(',').collect();
185+
let total_idx = cols.iter().position(|c| c.contains("Total Memory"))?;
186+
let used_idx = cols.iter().position(|c| c.contains("Total Used Memory"))?;
187+
let row = lines.next()?;
188+
let vals: Vec<&str> = row.split(',').collect();
189+
let total: u64 = vals.get(total_idx)?.trim().parse().ok()?;
190+
let used: u64 = vals.get(used_idx)?.trim().parse().ok()?;
191+
return Some(total.saturating_sub(used) / (1024 * 1024));
192+
}
193+
None
118194
}
119195

120196
/// Kill whatever process is listening on `port`. Best-effort and

0 commit comments

Comments
 (0)