1use serde::{Deserialize, Serialize};
6use serde_json;
7use std::env;
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::process::Command;
11use std::time::Instant;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct BenchmarkResult {
15 pub name: String,
16 pub success: bool,
17 pub execution_time_ms: u128,
18 pub metrics: serde_json::Value,
19 pub errors: Vec<String>,
20}
21
22fn helper_dir_candidates() -> Vec<PathBuf> {
23 let mut dirs = Vec::new();
24 if let Some(home) = dirs::home_dir() {
25 dirs.push(home.join(".rusty-stack").join("tmp"));
26 }
27 dirs.push(PathBuf::from("/tmp").join("rusty-stack-tmp"));
28 dirs
29}
30
31fn ensure_helper_script() -> Result<PathBuf, String> {
32 let body = PY_HELPER
36 .trim_start()
37 .replace("__INTEGRATED_TOKENS__", &integrated_tokens_py());
38 let mut last_error = String::new();
39
40 for dir in helper_dir_candidates() {
41 if let Err(err) = fs::create_dir_all(&dir) {
42 last_error = format!("failed to create helper dir {}: {}", dir.display(), err);
43 continue;
44 }
45 let path = dir.join("rusty_bench.py");
46 match fs::write(&path, &body) {
47 Ok(_) => return Ok(path),
48 Err(err) => {
49 last_error = format!("failed to write helper script {}: {}", path.display(), err);
50 continue;
51 }
52 }
53 }
54
55 Err(last_error)
56}
57
58fn integrated_tokens_py() -> String {
62 let mut tokens: Vec<String> = crate::gpu::INTEGRATED_NAME_PATTERNS
63 .iter()
64 .map(|t| t.to_lowercase())
65 .collect();
66 tokens.extend(
67 crate::gpu::INTEGRATED_GFX_ARCHS
68 .iter()
69 .map(|t| t.to_lowercase()),
70 );
71 let mut s = String::from("[");
72 for (i, t) in tokens.iter().enumerate() {
73 if i > 0 {
74 s.push(',');
75 }
76 let escaped = t.replace('\\', "\\\\").replace('"', "\\\"");
77 s.push_str(&format!("\"{escaped}\""));
78 }
79 s.push(']');
80 s
81}
82
83fn resolve_benchmark_python() -> String {
84 for key in [
85 "MLSTACK_BENCHMARK_PYTHON",
86 "MLSTACK_PYTHON_BIN",
87 "UV_PYTHON",
88 ] {
89 if let Ok(value) = env::var(key) {
90 let value = value.trim();
91 if !value.is_empty() {
92 return value.to_string();
93 }
94 }
95 }
96
97 if let Some(value) = mlstack_env_python_bin() {
98 return value;
99 }
100
101 {
102 let candidate = "/usr/local/bin/python3";
103 let path = Path::new(candidate);
104 if path.exists() {
105 return candidate.to_string();
106 }
107 }
108
109 for candidate in ["python3", "python"] {
110 if Command::new(candidate).arg("--version").output().is_ok() {
111 return candidate.to_string();
112 }
113 }
114
115 "python3".to_string()
116}
117
118fn mlstack_env_python_bin() -> Option<String> {
119 let home = env::var("HOME").ok()?;
120 let contents = fs::read_to_string(Path::new(&home).join(".mlstack_env")).ok()?;
121 for line in contents.lines() {
122 let trimmed = line.trim();
123 let Some(value) = trimmed
124 .strip_prefix("export MLSTACK_PYTHON_BIN=")
125 .or_else(|| trimmed.strip_prefix("MLSTACK_PYTHON_BIN="))
126 else {
127 continue;
128 };
129 let value = value.trim().trim_matches('"').trim_matches('\'');
130 if !value.is_empty() {
131 return Some(value.to_string());
132 }
133 }
134 None
135}
136
137fn extract_helper_payload(stdout: &str) -> Option<serde_json::Value> {
138 let marker = "---BENCHMARK_RESULTS_START---";
139 let search = if let Some(pos) = stdout.rfind(marker) {
140 &stdout[pos + marker.len()..]
141 } else {
142 stdout
143 };
144
145 if let Some(start) = search.find('{') {
146 let mut depth = 0usize;
147 let mut in_string = false;
148 let mut escaped = false;
149 for (idx, ch) in search[start..].char_indices() {
150 if in_string {
151 if escaped {
152 escaped = false;
153 continue;
154 }
155 match ch {
156 '\\' => escaped = true,
157 '"' => in_string = false,
158 _ => {}
159 }
160 continue;
161 }
162
163 match ch {
164 '"' => in_string = true,
165 '{' => depth += 1,
166 '}' => {
167 if depth == 0 {
168 return None;
169 }
170 depth -= 1;
171 if depth == 0 {
172 let end = start + idx + 1;
173 return serde_json::from_str::<serde_json::Value>(&search[start..end]).ok();
174 }
175 }
176 _ => {}
177 }
178 }
179 }
180
181 None
182}
183
184fn run_python_benchmark(name: &str) -> BenchmarkResult {
185 let start = Instant::now();
186 let python_bin = resolve_benchmark_python();
187 let helper = match ensure_helper_script() {
188 Ok(path) => path,
189 Err(err) => {
190 return BenchmarkResult {
191 name: name.to_string(),
192 success: false,
193 execution_time_ms: start.elapsed().as_millis(),
194 metrics: serde_json::json!({}),
195 errors: vec![format!("Unable to create benchmark helper script: {}", err)],
196 };
197 }
198 };
199
200 let mut command = Command::new("bash");
205 command
206 .arg("-c")
207 .arg("source \"$HOME/.mlstack_env\" 2>/dev/null; exec \"$@\"")
208 .arg("_")
209 .arg(&python_bin)
210 .arg(&helper)
211 .arg(name)
212 .arg("--json");
213 let output = command.output();
214
215 let parse_payload_result = |parsed: serde_json::Value| BenchmarkResult {
216 name: parsed
217 .get("name")
218 .and_then(|v| v.as_str())
219 .unwrap_or(name)
220 .to_string(),
221 success: parsed
222 .get("success")
223 .and_then(|v| v.as_bool())
224 .unwrap_or(false),
225 execution_time_ms: parsed
226 .get("execution_time_ms")
227 .and_then(|v| v.as_u64())
228 .unwrap_or(start.elapsed().as_millis() as u64) as u128,
229 metrics: parsed.get("metrics").cloned().unwrap_or_default(),
230 errors: parsed
231 .get("errors")
232 .and_then(|v| v.as_array())
233 .map(|arr| {
234 arr.iter()
235 .filter_map(|e| e.as_str().map(|s| s.to_string()))
236 .collect()
237 })
238 .unwrap_or_default(),
239 };
240
241 match output {
242 Ok(out) => {
243 let stdout_str = String::from_utf8_lossy(&out.stdout);
244 if let Some(parsed) = extract_helper_payload(&stdout_str) {
245 let mut result = parse_payload_result(parsed);
246 if !result.success {
247 let stderr = String::from_utf8_lossy(&out.stderr);
248 let stderr_trim = stderr.trim();
249 if !stderr_trim.is_empty() {
250 let stderr_tail = stderr_trim
251 .lines()
252 .rev()
253 .take(24)
254 .collect::<Vec<_>>()
255 .into_iter()
256 .rev()
257 .collect::<Vec<_>>()
258 .join(" | ");
259 if !stderr_tail.is_empty() {
260 result
261 .errors
262 .push(format!("Helper stderr tail: {}", stderr_tail));
263 }
264 }
265 }
266 return result;
267 }
268
269 if out.status.success() {
270 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
271 let parse_error = if stderr.is_empty() {
272 format!("Failed to parse helper JSON output using {}", python_bin)
273 } else {
274 format!(
275 "Failed to parse helper JSON output using {} (stderr: {})",
276 python_bin, stderr
277 )
278 };
279 return BenchmarkResult {
280 name: name.to_string(),
281 success: false,
282 execution_time_ms: start.elapsed().as_millis(),
283 metrics: serde_json::json!({}),
284 errors: vec![parse_error],
285 };
286 }
287
288 BenchmarkResult {
289 name: name.to_string(),
290 success: false,
291 execution_time_ms: start.elapsed().as_millis(),
292 metrics: serde_json::json!({}),
293 errors: {
294 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
295 let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
296 let detail = if !stderr.is_empty() {
297 stderr
298 } else if !stdout.is_empty() {
299 stdout
300 } else {
301 "no output".to_string()
302 };
303 vec![format!(
304 "Helper failed with status {} using {}: {}",
305 out.status, python_bin, detail
306 )]
307 },
308 }
309 }
310 Err(err) => BenchmarkResult {
311 name: name.to_string(),
312 success: false,
313 execution_time_ms: start.elapsed().as_millis(),
314 metrics: serde_json::json!({}),
315 errors: vec![format!("Failed to invoke {}: {}", python_bin, err)],
316 },
317 }
318}
319
320pub fn run_gpu_capability_benchmark() -> BenchmarkResult {
321 run_python_benchmark("gpu-info")
322}
323pub fn run_memory_bandwidth_benchmark() -> BenchmarkResult {
324 run_python_benchmark("memory-bandwidth")
325}
326pub fn run_tensor_core_benchmark() -> BenchmarkResult {
327 run_python_benchmark("tensor-core")
328}
329pub fn run_gemm_benchmark() -> BenchmarkResult {
330 run_python_benchmark("gemm")
331}
332pub fn run_pytorch_benchmark() -> BenchmarkResult {
333 run_python_benchmark("pytorch")
334}
335pub fn run_llama_cpp_benchmark() -> BenchmarkResult {
336 if let Some(home) = dirs::home_dir() {
340 let _ = crate::installers::components::llama_cpp::ensure_default_gguf_model(
341 &home.to_string_lossy(),
342 );
343 }
344 run_python_benchmark("llama-cpp")
345}
346pub fn run_flash_attention_benchmark() -> BenchmarkResult {
347 run_python_benchmark("flash-attention")
348}
349pub fn run_flash_attention_ck_benchmark() -> BenchmarkResult {
350 run_python_benchmark("flash-attention-ck")
351}
352pub fn run_vllm_benchmark() -> BenchmarkResult {
353 run_python_benchmark("vllm")
354}
355pub fn run_deepspeed_benchmark() -> BenchmarkResult {
356 run_python_benchmark("deepspeed")
357}
358pub fn run_megatron_benchmark() -> BenchmarkResult {
359 run_python_benchmark("megatron")
360}
361pub fn run_onnx_benchmark() -> BenchmarkResult {
362 run_python_benchmark("onnx")
363}
364
365const PY_HELPER: &str = r#"
369import argparse
370import json
371import os
372import shutil
373import subprocess
374import sys
375import tempfile
376import time
377from glob import glob
378
379_GPU_RUNTIME_CACHE = None
380_DEFAULT_TINY_SAFETENSORS_MODEL = "HuggingFaceTB/SmolLM2-135M-Instruct"
381
382
383def _set_amd_gpu_id_table_env():
384 if os.environ.get("AMDGPU_ASIC_ID_TABLE_PATH"):
385 return
386 candidate = "/usr/share/libdrm/amdgpu.ids"
387 if os.path.isfile(candidate):
388 os.environ["AMDGPU_ASIC_ID_TABLE_PATH"] = candidate
389 os.environ.setdefault("AMDGPU_ASIC_ID_TABLE_PATHS", "/usr/share/libdrm")
390
391
392def _env_or_default(name, default):
393 value = os.environ.get(name)
394 if value is None:
395 return default
396 value = value.strip()
397 return value if value else default
398
399
400def _resolve_gguf_model_path(raw):
401 raw = (raw or "").strip()
402 if not raw:
403 return ""
404 expanded = os.path.expanduser(raw)
405 if os.path.isfile(expanded):
406 return expanded
407 if os.path.isdir(expanded):
408 for entry in sorted(os.listdir(expanded)):
409 if entry.lower().endswith(".gguf"):
410 return os.path.join(expanded, entry)
411 return ""
412
413
414def _ensure_cached_hf_model_weights(model_name):
415 try:
416 from huggingface_hub import hf_hub_download, try_to_load_from_cache
417 except Exception as exc:
418 return False, f"huggingface_hub unavailable for benchmark model download: {exc}"
419
420 weight_files = (
421 "model.safetensors",
422 "model.safetensors.index.json",
423 "pytorch_model.bin",
424 "pytorch_model.bin.index.json",
425 )
426 marker_files = ("config.json", "tokenizer.json", "tokenizer_config.json")
427
428 for filename in weight_files:
429 path = try_to_load_from_cache(model_name, filename)
430 if isinstance(path, str) and os.path.isfile(path):
431 return True, ""
432
433 try:
434 path = hf_hub_download(repo_id=model_name, filename="model.safetensors")
435 if isinstance(path, str) and os.path.isfile(path):
436 return True, ""
437 except Exception as exc:
438 cached_markers = []
439 for filename in marker_files:
440 path = try_to_load_from_cache(model_name, filename)
441 if isinstance(path, str) and os.path.isfile(path):
442 cached_markers.append(filename)
443 marker_msg = f" cached files: {', '.join(cached_markers)};" if cached_markers else ""
444 return False, (
445 f"could not download/cache model.safetensors for {model_name};"
446 f"{marker_msg} {exc}"
447 )
448
449 return False, f"downloaded model.safetensors for {model_name} but no local file was found"
450
451
452def _find_llama_cpp_binary():
453 candidates = [
454 os.path.expanduser("~/.mlstack/components/llama-cpp/bin/llama-bench"),
455 shutil.which("llama-bench"),
456 ]
457 for candidate in candidates:
458 if candidate and os.path.isfile(candidate) and os.access(candidate, os.X_OK):
459 return candidate
460 return ""
461
462
463def _find_gguf_model():
464 candidates = []
465 candidates.extend(sorted(glob(os.path.expanduser("~/.mlstack/models/*.gguf"))))
466 candidates.extend(sorted(glob(os.path.expanduser("~/.cache/huggingface/hub/*/snapshots/*/*.gguf"))))
467 candidates.extend(sorted(glob(os.path.expanduser("~/.cache/**/*.gguf"), recursive=True)))
468 for candidate in candidates:
469 if os.path.isfile(candidate):
470 return candidate
471 return ""
472
473
474def _gpu_info_from_env():
475 """GPU info from the canonical env (~/.mlstack_env) — the SINGLE source.
476 Carries ROCR_VISIBLE_DEVICES (discrete indices, iGPU filtered), GPU_ARCH,
477 MLSTACK_GPU_NAMES, MLSTACK_GPU_VRAM_GB, MLSTACK_GPU_CUS. No rocm-smi/rocminfo
478 here. Returns (indices, names, vram_gb_strings, cus_strings, arch)."""
479 rocr = os.environ.get("ROCR_VISIBLE_DEVICES", "").strip()
480 indices = []
481 for part in rocr.split(","):
482 part = part.strip()
483 if part.isdigit():
484 indices.append(int(part))
485 if not indices:
486 # Fail closed: do NOT invent device 0. On an APU+dGPU host where the env
487 # was not sourced (or lacks a valid ROCR_VISIBLE_DEVICES), guessing 0 can
488 # benchmark the unsupported iGPU, and on an unconfigured multi-dGPU host
489 # it silently benchmarks only one card while reporting incomplete
490 # results. Return empty so the benchmark's existing error path reports
491 # "no GPUs in ROCR_VISIBLE_DEVICES" instead of running on a wrong device.
492 return [], [], [], [], ""
493 names = [s.strip() for s in os.environ.get("MLSTACK_GPU_NAMES", "").split(",") if s.strip()]
494 vram = [s.strip() for s in os.environ.get("MLSTACK_GPU_VRAM_GB", "").split(",") if s.strip()]
495 cus = [s.strip() for s in os.environ.get("MLSTACK_GPU_CUS", "").split(",") if s.strip()]
496 arch = os.environ.get("GPU_ARCH", "").strip()
497 while len(names) < len(indices):
498 names.append("GPU {}".format(indices[len(names)]))
499 while len(vram) < len(indices):
500 vram.append("0")
501 while len(cus) < len(indices):
502 cus.append("0")
503 return indices, names, vram, cus, arch
504
505
506def _llama_cpp():
507 start = time.perf_counter()
508 bench = _find_llama_cpp_binary()
509 if not bench:
510 return False, {
511 "name": "llama-cpp",
512 "success": False,
513 "execution_time_ms": 0,
514 "metrics": {},
515 "errors": ["llama-cpp not installed"],
516 }, ["llama-cpp not installed"]
517
518 # llama-bench links libggml*.so from a sibling lib/ dir (bin/../lib). Add it
519 # to LD_LIBRARY_PATH or the loader can't start the binary (libs aren't beside
520 # it). Set once here so every subprocess.run below inherits it.
521 _bench_lib = os.path.join(os.path.dirname(os.path.dirname(bench)), "lib")
522 if os.path.isdir(_bench_lib):
523 _cur_ld = os.environ.get("LD_LIBRARY_PATH", "")
524 os.environ["LD_LIBRARY_PATH"] = _bench_lib + (":" + _cur_ld if _cur_ld else "")
525
526 model = _find_gguf_model()
527 if not model:
528 elapsed = int((time.perf_counter() - start) * 1000)
529 return False, {
530 "name": "llama-cpp",
531 "success": False,
532 "execution_time_ms": elapsed,
533 "metrics": {},
534 "errors": ["no GGUF model found"],
535 }, ["no GGUF model found"]
536
537 indices, gpu_names, gpu_vram, gpu_cus, gpu_arch = _gpu_info_from_env()
538 if not indices:
539 elapsed = int((time.perf_counter() - start) * 1000)
540 return False, {
541 "name": "llama-cpp",
542 "success": False,
543 "execution_time_ms": elapsed,
544 "metrics": {},
545 "errors": ["no GPUs in ROCR_VISIBLE_DEVICES (source ~/.mlstack_env)"],
546 }, ["no GPUs in ROCR_VISIBLE_DEVICES (source ~/.mlstack_env)"]
547
548 # Metadata (from the env — canonical GPU info) emitted alongside throughput
549 # so the results panel shows real GPU names/arch/VRAM + the model + ROCm ver.
550 def _has_wmma(a):
551 return a in ("gfx1100", "gfx1101", "gfx1102", "gfx1151", "gfx1200", "gfx1201")
552
553 metrics = {
554 "gpu_arch": gpu_arch,
555 "gpu_names": gpu_names,
556 "gpu_vram_gb": [float(v) for v in gpu_vram],
557 "model": os.path.basename(model) if model else "",
558 "rocm_version": os.environ.get("ROCM_VERSION", "").strip(),
559 # Full GPU inventory (model/vram/CUs/tensor_cores) so the "GPU Inventory"
560 # panel + HTML export show real devices, not "No GPU detected".
561 "gpus": [
562 {
563 "index": indices[i],
564 "gpu_model": gpu_names[i],
565 "vram_gb": float(gpu_vram[i]),
566 "compute_units": int(gpu_cus[i]) if gpu_cus[i].isdigit() else 0,
567 "tensor_cores": _has_wmma(gpu_arch),
568 }
569 for i in range(len(indices))
570 ],
571 }
572 errors = []
573 got_throughput = False
574 for gpu_idx in indices:
575 env = os.environ.copy()
576 # ROCR_VISIBLE_DEVICES (the ROCr runtime filter) — NOT the nonexistent
577 # ROCM_VISIBLE_DEVICES — isolates one discrete GPU per llama-bench run.
578 env["ROCR_VISIBLE_DEVICES"] = str(gpu_idx)
579 # Clear the alias masks so the inherited physical mask (e.g. "1,2" set
580 # by the sourced ~/.mlstack_env on an APU+dGPU host) can't re-filter the
581 # already-isolated GPU. ROCR renumbers the single visible GPU to logical
582 # 0, but a stale HIP_VISIBLE_DEVICES="1,2" / CUDA_VISIBLE_DEVICES="1,2"
583 # would then hide device 0 and the run reports no GPU. Same clearing the
584 # FastVideo per-device probe applies.
585 env.pop("HIP_VISIBLE_DEVICES", None)
586 env.pop("CUDA_VISIBLE_DEVICES", None)
587 env.pop("GPU_DEVICE_ORDINAL", None)
588 try:
589 proc = subprocess.run(
590 [bench, "-m", model, "-p", "512", "-p", "2048", "-p", "8192", "-p", "16384", "-p", "32768", "-n", "128", "-o", "json", "-r", "3"],
591 check=False,
592 capture_output=True,
593 text=True,
594 env=env,
595 )
596 raw = (proc.stdout or "").strip()
597 parsed = json.loads(raw) if raw else []
598 if isinstance(parsed, dict):
599 parsed = [parsed]
600 for entry in parsed:
601 if not isinstance(entry, dict):
602 continue
603 n_prompt = int(entry.get("n_prompt", 0) or 0)
604 n_gen = int(entry.get("n_gen", 0) or 0)
605 avg_ts = entry.get("avg_ts")
606 if avg_ts is None:
607 avg_ts = entry.get("avg_tps")
608 stddev_ts = entry.get("stddev_ts", 0.0)
609 prefix = "prefill" if n_prompt > 0 and n_gen == 0 else "decode" if n_prompt == 0 and n_gen > 0 else "other"
610 if prefix == "other" or avg_ts is None:
611 continue
612 context = n_prompt if prefix == "prefill" else n_gen
613 metrics[f"{prefix}_{context}_tps_gpu{gpu_idx}"] = float(avg_ts)
614 metrics[f"{prefix}_{context}_stddev_tps_gpu{gpu_idx}"] = float(stddev_ts or 0.0)
615 got_throughput = True
616 except Exception as exc:
617 errors.append(str(exc))
618
619 elapsed = int((time.perf_counter() - start) * 1000)
620 success = got_throughput and not errors
621 result = {
622 "name": "llama-cpp",
623 "success": success,
624 "execution_time_ms": elapsed,
625 "metrics": metrics,
626 "errors": errors,
627 }
628 print("---BENCHMARK_RESULTS_START---")
629 print(json.dumps(result, indent=2, sort_keys=True))
630 return success, result, errors
631
632
633def _resolve_vllm_model_candidates():
634 safetensors_model = _env_or_default(
635 "MLSTACK_BENCH_VLLM_SAFETENSORS_MODEL",
636 _DEFAULT_TINY_SAFETENSORS_MODEL,
637 )
638 gguf_hint = os.environ.get("MLSTACK_BENCH_VLLM_GGUF_MODEL_PATH", "")
639 gguf_tokenizer = _env_or_default("MLSTACK_BENCH_VLLM_GGUF_TOKENIZER", safetensors_model)
640 gguf_path = _resolve_gguf_model_path(gguf_hint)
641
642 candidates = []
643 if gguf_path:
644 candidates.append({
645 "format": "gguf",
646 "model": gguf_path,
647 "tokenizer": gguf_tokenizer,
648 })
649 candidates.append({
650 "format": "safetensors",
651 "model": safetensors_model,
652 })
653 return candidates
654
655
656def _probe_gpu_runtime():
657 global _GPU_RUNTIME_CACHE
658 if _GPU_RUNTIME_CACHE is not None:
659 return _GPU_RUNTIME_CACHE
660
661 _set_amd_gpu_id_table_env()
662 torch = _load_torch()
663 if isinstance(torch, tuple):
664 _, errors = torch
665 reason = "; ".join(errors) if errors else "unable to import torch"
666 _GPU_RUNTIME_CACHE = (False, reason, {"available": False})
667 return _GPU_RUNTIME_CACHE
668
669 if not torch.cuda.is_available():
670 _GPU_RUNTIME_CACHE = (False, "torch.cuda is not available", {"available": False})
671 return _GPU_RUNTIME_CACHE
672
673 arch = "unknown"
674 try:
675 props = torch.cuda.get_device_properties(torch.device("cuda:0"))
676 arch = getattr(props, "gcnArchName", "unknown")
677 except Exception:
678 pass
679
680 probe_code = (
681 "import torch;"
682 "x=torch.ones(1, device='cuda', dtype=torch.float32);"
683 "y=x+1;"
684 "torch.cuda.synchronize();"
685 "print(float(y.item()))"
686 )
687 try:
688 probe = subprocess.run(
689 [sys.executable, "-c", probe_code],
690 check=False,
691 capture_output=True,
692 text=True,
693 timeout=20,
694 )
695 if probe.returncode == 0:
696 _GPU_RUNTIME_CACHE = (True, "", {"available": True, "gcn_arch": arch})
697 return _GPU_RUNTIME_CACHE
698
699 detail = (probe.stderr or probe.stdout or "").strip()
700 if not detail:
701 detail = f"exit code {probe.returncode}"
702 if probe.returncode < 0:
703 detail = f"signal {-probe.returncode}: {detail}"
704 reason = f"GPU runtime probe failed on {arch}: {detail}"
705 _GPU_RUNTIME_CACHE = (False, reason, {"available": True, "gcn_arch": arch})
706 return _GPU_RUNTIME_CACHE
707 except subprocess.TimeoutExpired:
708 reason = f"GPU runtime probe timed out on {arch}"
709 _GPU_RUNTIME_CACHE = (False, reason, {"available": True, "gcn_arch": arch})
710 return _GPU_RUNTIME_CACHE
711 except Exception as exc:
712 reason = f"GPU runtime probe error on {arch}: {exc}"
713 _GPU_RUNTIME_CACHE = (False, reason, {"available": True, "gcn_arch": arch})
714 return _GPU_RUNTIME_CACHE
715
716
717def _degraded_metrics(name, reason, extra=None):
718 payload = {
719 "mode": "degraded",
720 "component": name,
721 "gpu_runtime_stable": False,
722 "reason": reason,
723 }
724 if isinstance(extra, dict):
725 payload.update(extra)
726 return payload
727
728
729def _load_torch():
730 try:
731 import torch # noqa: F401
732 return torch
733 except Exception as exc: # pragma: no cover
734 return None, [f"Unable to import torch: {exc}"]
735
736
737def _parse_visible_gpu_indices():
738 for key in ("HIP_VISIBLE_DEVICES", "ROCR_VISIBLE_DEVICES", "CUDA_VISIBLE_DEVICES"):
739 raw = os.environ.get(key, "").strip()
740 if not raw:
741 continue
742
743 values = []
744 valid = True
745 for token in raw.split(","):
746 token = token.strip()
747 if not token:
748 continue
749 if not token.isdigit():
750 valid = False
751 break
752 values.append(int(token))
753
754 if valid and values:
755 return values
756 return []
757
758
759def _coerce_smi_value(entry, key):
760 raw = entry.get(key)
761 if isinstance(raw, dict):
762 value = raw.get("value")
763 if value is None:
764 return None
765 return value
766 return raw
767
768
769def _discrete_hint_from_name(name):
770 if not name:
771 return None
772
773 lowered = str(name).lower()
774 if any(token in lowered for token in ("radeon rx", " rx ", "radeon pro", "instinct", "firepro")):
775 return True
776 if "ryzen" in lowered:
777 return False
778 if "radeon graphics" in lowered and "rx" not in lowered and "pro" not in lowered:
779 return False
780 if "integrated" in lowered or "igpu" in lowered or "apu" in lowered:
781 return False
782 return None
783
784
785def _is_integrated_name(name):
786 hint = _discrete_hint_from_name(name)
787 if hint is not None:
788 return not hint
789 n = str(name or "")
790 lowered = n.lower()
791 up = n.upper()
792 # Token set GENERATED from crate::gpu::INTEGRATED_NAME_PATTERNS at write
793 # time (single source — no hand-maintained copy that can drift from Rust).
794 for token in __INTEGRATED_TOKENS__:
795 if token in lowered:
796 return True
797 # "Ryzen" without "RX" => APU/iGPU (discrete cards never contain "Ryzen").
798 if "RYZEN" in up and "RX" not in up:
799 return True
800 return False
801
802
803def _gpu_info():
804 torch = _load_torch()
805 if isinstance(torch, tuple):
806 _, errors = torch
807 return False, {}, errors
808 if not torch.cuda.is_available():
809 return False, {}, ["torch.cuda is not available"]
810
811 visible_indices = _parse_visible_gpu_indices()
812 smi_available = shutil.which("rocm-smi") is not None
813 devices = []
814 for idx in range(torch.cuda.device_count()):
815 global_idx = visible_indices[idx] if idx < len(visible_indices) else idx
816 device = torch.device(f"cuda:{idx}")
817 props = torch.cuda.get_device_properties(device)
818 info = {
819 "index": global_idx,
820 "local_index": idx,
821 "gpu_model": getattr(props, "name", "Unknown"),
822 "gcn_arch": getattr(props, "gcnArchName", "unknown"),
823 "vram_gb": round(getattr(props, "total_memory", 0) / 1e9, 2),
824 "compute_units": getattr(props, "multi_processor_count", 0),
825 "max_clock_mhz": getattr(props, "max_frequency", 0) / 1e6,
826 "tensor_cores": bool(getattr(props, "gcnArchName", "").startswith("gfx11")),
827 }
828
829 smi_discrete_hint = None
830 if smi_available:
831 try:
832 smi = subprocess.run(
833 [
834 "rocm-smi",
835 "--showproductname",
836 "--showtemp",
837 "--showpower",
838 "--showuse",
839 "--showclocks",
840 "--json",
841 "-d",
842 str(global_idx),
843 ],
844 check=False,
845 capture_output=True,
846 text=True,
847 )
848 if smi.returncode == 0 and smi.stdout.strip().startswith("{"):
849 data = json.loads(smi.stdout)
850 first_key = next(iter(data.keys()))
851 entry = data.get(first_key, {})
852
853 smi_name = (
854 _coerce_smi_value(entry, "Card Series")
855 or _coerce_smi_value(entry, "Card series")
856 or _coerce_smi_value(entry, "Card Model")
857 or _coerce_smi_value(entry, "Card model")
858 )
859 if smi_name:
860 info["gpu_model"] = str(smi_name).strip()
861 smi_discrete_hint = _discrete_hint_from_name(smi_name)
862
863 temp = entry.get("Temperature (Sensor die)", {}).get("value")
864 power = entry.get("Average Graphics Package Power", {}).get("value")
865 if temp is not None:
866 info["temperature_c"] = float(temp)
867 if power is not None:
868 info["power_watts"] = float(power)
869
870 # capture additional metrics requested
871 util = entry.get("GPU use (%)", {}).get("value")
872 if util is not None:
873 info["utilization_percent"] = float(util)
874
875 mem_util = entry.get("GPU memory use (%)", {}).get("value")
876 if mem_util is not None:
877 info["memory_percent"] = float(mem_util)
878
879 sclk = entry.get("GFX Clock (MHz)", {}).get("value")
880 if sclk is not None:
881 info["sclk_mhz"] = int(float(sclk))
882
883 mclk = entry.get("Memory Clock (MHz)", {}).get("value")
884 if mclk is not None:
885 info["mclk_mhz"] = int(float(mclk))
886 except Exception:
887 pass
888
889 if smi_discrete_hint is False:
890 continue
891 if smi_discrete_hint is None and _is_integrated_name(info.get("gpu_model")):
892 continue
893 devices.append(info)
894
895 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
896 metrics = {"gpus": devices}
897 metrics.update({"gpu_runtime_stable": probe_ok})
898 if isinstance(probe_meta, dict):
899 metrics.update({f"probe_{k}": v for k, v in probe_meta.items()})
900 if probe_ok:
901 return True, metrics, []
902 return True, metrics, [probe_reason]
903
904
905def _time_fn(fn, warmup=1, repeat=5):
906 import torch
907
908 torch.cuda.synchronize()
909 for _ in range(warmup):
910 fn()
911 times = []
912 for _ in range(repeat):
913 torch.cuda.synchronize()
914 start = time.perf_counter()
915 fn()
916 torch.cuda.synchronize()
917 times.append(time.perf_counter() - start)
918 return sum(times) / len(times)
919
920
921def _memory_bandwidth():
922 torch = _load_torch()
923 if isinstance(torch, tuple):
924 _, errors = torch
925 return False, {}, errors
926 if not torch.cuda.is_available():
927 return False, {}, ["torch.cuda not available"]
928 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
929 if not probe_ok:
930 extra = {
931 "hbm_peak_gb_s": 0.0,
932 "system_peak_gb_s": 0.0,
933 "hbm_ratio": 0.0,
934 "hbm_samples_gbps": [],
935 "system_samples_gbps": [],
936 }
937 if isinstance(probe_meta, dict):
938 extra.update(probe_meta)
939 return False, _degraded_metrics("memory-bandwidth", probe_reason, extra), [probe_reason]
940
941 device = torch.device("cuda:0")
942 sizes_mb = [64, 128, 256, 512]
943 hbm_samples = []
944 system_samples = []
945
946 for size_mb in sizes_mb:
947 numel = size_mb * 1024 * 1024 // 4
948 a = torch.empty(numel, device=device, dtype=torch.float32)
949 def op():
950 a.add_(1.0)
951 t = _time_fn(op, warmup=1, repeat=4)
952 bytes_moved = a.numel() * a.element_size() * 2 # read + write
953 hbm_samples.append(bytes_moved / t / 1e9)
954
955 host = torch.empty_like(a.cpu())
956 def h2d():
957 _ = host.to(device, non_blocking=True)
958 t_h2d = _time_fn(h2d, warmup=1, repeat=3)
959 system_samples.append(bytes_moved / t_h2d / 1e9 if t_h2d > 0 else 0)
960
961 hbm_peak = max(hbm_samples) if hbm_samples else 0
962 sys_peak = max(system_samples) if system_samples else 0
963 hbm_ratio = hbm_peak / sys_peak if sys_peak > 0 else 0
964
965 return True, {
966 "hbm_peak_gb_s": round(hbm_peak, 2),
967 "system_peak_gb_s": round(sys_peak, 2),
968 "hbm_ratio": round(hbm_ratio, 2),
969 "hbm_samples_gbps": [round(x, 2) for x in hbm_samples],
970 "system_samples_gbps": [round(x, 2) for x in system_samples],
971 }, []
972
973
974def _tensor_core():
975 torch = _load_torch()
976 if isinstance(torch, tuple):
977 _, errors = torch
978 return False, {}, errors
979 if not torch.cuda.is_available():
980 return False, {}, ["torch.cuda not available"]
981 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
982 if not probe_ok:
983 extra = {
984 "fp16_tflops": 0.0,
985 "bf16_tflops": 0.0,
986 "tf32_tflops": 0.0,
987 "fp32_tflops": 0.0,
988 "fp16_samples_tflops": [],
989 }
990 if isinstance(probe_meta, dict):
991 extra.update(probe_meta)
992 return False, _degraded_metrics("tensor-core", probe_reason, extra), [probe_reason]
993
994 device = torch.device("cuda:0")
995 sizes = [512, 1024, 2048]
996
997 results = {}
998
999 # FP16
1000 fp16_samples = []
1001 for n in sizes:
1002 a = torch.randn((n, n), device=device, dtype=torch.float16)
1003 b = torch.randn((n, n), device=device, dtype=torch.float16)
1004 t = _time_fn(lambda: torch.matmul(a, b), warmup=2, repeat=3)
1005 flops = 2 * n * n * n
1006 fp16_samples.append(flops / t / 1e12)
1007 results["fp16_tflops"] = round(max(fp16_samples), 2)
1008 results["fp16_samples"] = [round(x, 2) for x in fp16_samples]
1009
1010 # BF16
1011 bf16_samples = []
1012 try:
1013 for n in sizes:
1014 a = torch.randn((n, n), device=device, dtype=torch.bfloat16)
1015 b = torch.randn((n, n), device=device, dtype=torch.bfloat16)
1016 t = _time_fn(lambda: torch.matmul(a, b), warmup=2, repeat=3)
1017 flops = 2 * n * n * n
1018 bf16_samples.append(flops / t / 1e12)
1019 results["bf16_tflops"] = round(max(bf16_samples), 2)
1020 except Exception:
1021 results["bf16_tflops"] = 0.0
1022
1023 # FP32
1024 fp32_samples = []
1025 for n in sizes:
1026 a = torch.randn((n, n), device=device, dtype=torch.float32)
1027 b = torch.randn((n, n), device=device, dtype=torch.float32)
1028 t = _time_fn(lambda: torch.matmul(a, b), warmup=2, repeat=3)
1029 flops = 2 * n * n * n
1030 fp32_samples.append(flops / t / 1e12)
1031 results["fp32_tflops"] = round(max(fp32_samples), 2)
1032
1033 # TF32 is NVIDIA specific, on AMD we'll report 0 or skip
1034 results["tf32_tflops"] = 0.0
1035
1036 return True, {
1037 "fp16_tflops": results["fp16_tflops"],
1038 "bf16_tflops": results["bf16_tflops"],
1039 "tf32_tflops": results["tf32_tflops"],
1040 "fp32_tflops": results["fp32_tflops"],
1041 "fp16_samples_tflops": results["fp16_samples"],
1042 }, []
1043
1044
1045def _gemm():
1046 torch = _load_torch()
1047 if isinstance(torch, tuple):
1048 _, errors = torch
1049 return False, {}, errors
1050 if not torch.cuda.is_available():
1051 return False, {}, ["torch.cuda not available"]
1052 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
1053 if not probe_ok:
1054 extra = {
1055 "fp16_peak_gflops": 0.0,
1056 "fp16_samples_gflops": [],
1057 }
1058 if isinstance(probe_meta, dict):
1059 extra.update(probe_meta)
1060 return False, _degraded_metrics("gemm", probe_reason, extra), [probe_reason]
1061
1062 device = torch.device("cuda:0")
1063 shapes = [(1024, 1024, 1024), (1536, 1536, 1536), (2048, 2048, 2048)]
1064 samples = []
1065 for m, k, n in shapes:
1066 a = torch.randn((m, k), device=device, dtype=torch.float16)
1067 b = torch.randn((k, n), device=device, dtype=torch.float16)
1068 def op():
1069 return torch.matmul(a, b)
1070 t = _time_fn(op, warmup=1, repeat=3)
1071 flops = 2 * m * n * k
1072 samples.append(flops / t / 1e9)
1073 peak = max(samples) if samples else 0
1074 return True, {
1075 "fp16_peak_gflops": round(peak, 2),
1076 "fp16_samples_gflops": [round(x, 2) for x in samples],
1077 }, []
1078
1079
1080def _pytorch():
1081 torch = _load_torch()
1082 if isinstance(torch, tuple):
1083 _, errors = torch
1084 return False, {}, errors
1085 if not torch.cuda.is_available():
1086 return False, {}, ["torch.cuda not available"]
1087 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
1088 if not probe_ok:
1089 extra = {
1090 "gemm_gflops": 0.0,
1091 "convolution_gflops": 0.0,
1092 "gemm_samples_gflops": [],
1093 "conv_samples_gflops": [],
1094 "autograd_overhead_percent": 0.0,
1095 }
1096 if isinstance(probe_meta, dict):
1097 extra.update(probe_meta)
1098 return False, _degraded_metrics("pytorch", probe_reason, extra), [probe_reason]
1099
1100 device = torch.device("cuda:0")
1101 m = k = n = 1024
1102 a = torch.randn((m, k), device=device, dtype=torch.float32, requires_grad=True)
1103 b = torch.randn((k, n), device=device, dtype=torch.float32, requires_grad=True)
1104 gemm_samples = []
1105 for _ in range(3):
1106 gemm_t = _time_fn(lambda: torch.matmul(a, b), warmup=1, repeat=1)
1107 gemm_samples.append(2 * m * n * k / gemm_t / 1e9)
1108 gemm_peak = max(gemm_samples) if gemm_samples else 0
1109
1110 conv = torch.nn.Conv2d(64, 64, kernel_size=3, padding=1).to(device)
1111 x = torch.randn((32, 64, 64, 64), device=device)
1112 conv_samples = []
1113 for _ in range(3):
1114 conv_t = _time_fn(lambda: conv(x), warmup=1, repeat=1)
1115 conv_ops = 2 * 32 * 64 * 64 * 64 * 64 * 3 * 3
1116 conv_samples.append(conv_ops / conv_t / 1e9)
1117 conv_peak = max(conv_samples) if conv_samples else 0
1118
1119 y = a @ b
1120 y.sum().backward()
1121 torch.cuda.synchronize()
1122 start = time.perf_counter()
1123 y = a @ b
1124 loss = y.sum()
1125 loss.backward()
1126 torch.cuda.synchronize()
1127 full = time.perf_counter() - start
1128
1129 start = time.perf_counter()
1130 _ = a @ b
1131 torch.cuda.synchronize()
1132 fwd = time.perf_counter() - start
1133 overhead = (full - fwd) / full * 100 if full > 0 else 0
1134
1135 return True, {
1136 "gemm_gflops": round(gemm_peak, 2),
1137 "convolution_gflops": round(conv_peak, 2),
1138 "gemm_samples_gflops": [round(x, 2) for x in gemm_samples],
1139 "conv_samples_gflops": [round(x, 2) for x in conv_samples],
1140 "autograd_overhead_percent": round(overhead, 2),
1141 }, []
1142
1143
1144def _flash_attention():
1145 torch = _load_torch()
1146 if isinstance(torch, tuple):
1147 _, errors = torch
1148 return False, {}, errors
1149 if not torch.cuda.is_available():
1150 return False, {}, ["torch.cuda not available"]
1151 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
1152 if not probe_ok:
1153 extra = {
1154 "standard_attention_speed": 0.0,
1155 "flash_attention_speed": 0.0,
1156 "standard_samples_tok_s": [],
1157 "flash_samples_tok_s": [],
1158 "speedup": 0.0,
1159 "memory_savings_gb": 0.0,
1160 }
1161 if isinstance(probe_meta, dict):
1162 extra.update(probe_meta)
1163 return False, _degraded_metrics("flash-attention", probe_reason, extra), [probe_reason]
1164
1165 try:
1166 from torch.backends.cuda import sdp_kernel
1167 except Exception as exc: # pragma: no cover
1168 return False, {}, [f"flash attention control unavailable: {exc}"]
1169
1170 device = torch.device("cuda:0")
1171 seqs = [128, 256, 512, 1024]
1172 bsz, heads, dim = 8, 8, 64
1173 flash_samples = []
1174 std_samples = []
1175 mem_std = 0
1176 mem_flash = 0
1177
1178 for seqlen in seqs:
1179 q = torch.randn((bsz, heads, seqlen, dim), device=device, dtype=torch.float16)
1180 k = torch.randn_like(q)
1181 v = torch.randn_like(q)
1182
1183 def run_flash():
1184 with sdp_kernel(enable_flash=True, enable_math=True, enable_mem_efficient=True):
1185 return torch.nn.functional.scaled_dot_product_attention(q, k, v, is_causal=False)
1186
1187 def run_standard():
1188 with sdp_kernel(enable_flash=False, enable_math=True, enable_mem_efficient=False):
1189 return torch.nn.functional.scaled_dot_product_attention(q, k, v, is_causal=False)
1190
1191 torch.cuda.reset_peak_memory_stats()
1192 _ = run_standard()
1193 mem_std = max(mem_std, torch.cuda.max_memory_allocated() / 1e9)
1194
1195 torch.cuda.reset_peak_memory_stats()
1196 _ = run_flash()
1197 mem_flash = max(mem_flash, torch.cuda.max_memory_allocated() / 1e9)
1198
1199 flash_t = _time_fn(run_flash, warmup=1, repeat=2)
1200 std_t = _time_fn(run_standard, warmup=1, repeat=2)
1201
1202 tokens = bsz * seqlen
1203 flash_samples.append(tokens / flash_t)
1204 std_samples.append(tokens / std_t)
1205
1206 flash_peak = max(flash_samples) if flash_samples else 0
1207 std_peak = max(std_samples) if std_samples else 0
1208
1209 return True, {
1210 "standard_attention_speed": round(std_peak, 2),
1211 "flash_attention_speed": round(flash_peak, 2),
1212 "standard_samples_tok_s": [round(x, 2) for x in std_samples],
1213 "flash_samples_tok_s": [round(x, 2) for x in flash_samples],
1214 "speedup": round(flash_peak / std_peak, 3) if std_peak > 0 else 0,
1215 "memory_savings_gb": round(max(0, mem_std - mem_flash), 4),
1216 }, []
1217
1218
1219def _flash_attention_ck():
1220 """Genuine-model forward benchmark for Flash Attention (CK) on RDNA3.
1221
1222 CK is forward-only on RDNA3 (no backward, ROCm/composable_kernel#1434), so
1223 this measures the SUPPORTED inference path: a real Llama-style decoder
1224 (RMSNorm + rotary + GQA via flash_attn_func + SwiGLU MLP) run forward under
1225 no_grad. No external model download: built in-torch, so it is reproducible
1226 on any box with torch+flash_attn.
1227 """
1228 torch = _load_torch()
1229 if isinstance(torch, tuple):
1230 _, errors = torch
1231 return False, {}, errors
1232 if not torch.cuda.is_available():
1233 return False, {}, ["torch.cuda not available"]
1234
1235 from pathlib import Path
1236 marker = Path.home() / ".mlstack" / "flash-attention" / ".backend"
1237 backend = marker.read_text().strip() if marker.exists() else ""
1238 if backend != "ck":
1239 return False, {}, [
1240 "flash-attention-ck requires backend marker 'ck' (found '"
1241 + backend
1242 + "'); install Flash Attention (CK) first"
1243 ]
1244
1245 try:
1246 import flash_attn
1247 from flash_attn import flash_attn_func
1248 except Exception as exc:
1249 return False, {}, ["flash_attn import failed: " + str(exc)]
1250
1251 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
1252 if not probe_ok:
1253 extra = {
1254 "backend": "ck",
1255 "forward_samples_ms": [],
1256 "throughput_samples_tok_s": [],
1257 "peak_throughput_tok_s": 0.0,
1258 }
1259 if isinstance(probe_meta, dict):
1260 extra.update(probe_meta)
1261 return False, _degraded_metrics("flash-attention-ck", probe_reason, extra), [probe_reason]
1262
1263 import torch.nn as nn
1264 import torch.nn.functional as F
1265
1266 device = torch.device("cuda:0")
1267 dtype = torch.bfloat16
1268
1269 def _rope(dim, max_seq, base=10000.0):
1270 inv = 1.0 / (base ** (torch.arange(0, dim, 2, device=device).float() / dim))
1271 seq = torch.arange(max_seq, device=device).float()
1272 freqs = torch.outer(seq, inv)
1273 return torch.cat([freqs, freqs], dim=-1)
1274
1275 def _apply_rope(x, table):
1276 s = x.shape[1]
1277 cos = table[:s].unsqueeze(1)
1278 sin = table[s:2 * s].unsqueeze(1)
1279 x1, x2 = x.float().chunk(2, dim=-1)
1280 rot = torch.cat([-x2, x1], dim=-1)
1281 return (x.float() * cos + rot * sin).to(x.dtype)
1282
1283 class _RMSNorm(nn.Module):
1284 def __init__(self, d, eps=1e-6):
1285 super().__init__()
1286 self.w = nn.Parameter(torch.ones(d))
1287 self.eps = eps
1288
1289 def forward(self, x):
1290 v = x.to(torch.float32).pow(2).mean(-1, keepdim=True)
1291 return (x * torch.rsqrt(v + self.eps)).to(x.dtype) * self.w
1292
1293 class _Block(nn.Module):
1294 def __init__(self, dim, n_heads, n_kv, rope_table):
1295 super().__init__()
1296 self.n_heads, self.n_kv, self.dim = n_heads, n_kv, dim
1297 self.head_dim = dim // n_heads
1298 self.wq = nn.Linear(dim, n_heads * self.head_dim, bias=False)
1299 self.wk = nn.Linear(dim, n_kv * self.head_dim, bias=False)
1300 self.wv = nn.Linear(dim, n_kv * self.head_dim, bias=False)
1301 self.wo = nn.Linear(n_heads * self.head_dim, dim, bias=False)
1302 hidden = int(8 * dim / 3)
1303 self.w1 = nn.Linear(dim, hidden, bias=False)
1304 self.w3 = nn.Linear(dim, hidden, bias=False)
1305 self.w2 = nn.Linear(hidden, dim, bias=False)
1306 self.n1, self.n2 = _RMSNorm(dim), _RMSNorm(dim)
1307 self.rope_table = rope_table
1308
1309 def forward(self, x):
1310 b, s, _ = x.shape
1311 q = self.wq(self.n1(x)).view(b, s, self.n_heads, self.head_dim)
1312 k = self.wk(self.n1(x)).view(b, s, self.n_kv, self.head_dim)
1313 v = self.wv(self.n1(x)).view(b, s, self.n_kv, self.head_dim)
1314 q = _apply_rope(q, self.rope_table)
1315 k = _apply_rope(k, self.rope_table)
1316 if self.n_kv != self.n_heads:
1317 rep = self.n_heads // self.n_kv
1318 k = k.repeat_interleave(rep, dim=2)
1319 v = v.repeat_interleave(rep, dim=2)
1320 attn = flash_attn_func(q, k, v, causal=True) # CK forward path
1321 attn = attn.reshape(b, s, -1)
1322 x = x + self.wo(attn)
1323 m = F.silu(self.w1(self.n2(x))) * self.w3(self.n2(x))
1324 return x + self.w2(m)
1325
1326 class _TransformerLM(nn.Module):
1327 def __init__(self, vocab, dim, n_layers, n_heads, n_kv, max_seq):
1328 super().__init__()
1329 rope_table = _rope(dim // n_heads, 2 * max_seq)
1330 self.embed = nn.Embedding(vocab, dim)
1331 self.layers = nn.ModuleList(
1332 [_Block(dim, n_heads, n_kv, rope_table) for _ in range(n_layers)]
1333 )
1334 self.norm = _RMSNorm(dim)
1335 self.head = nn.Linear(dim, vocab, bias=False)
1336
1337 def forward(self, ids):
1338 x = self.embed(ids)
1339 for blk in self.layers:
1340 x = blk(x)
1341 return self.head(self.norm(x))
1342
1343 vocab = 49152
1344 dim, n_layers, n_heads, n_kv, max_seq = 768, 12, 12, 4, 4096
1345 model = _TransformerLM(vocab, dim, n_layers, n_heads, n_kv, max_seq).to(device, dtype)
1346 model.eval()
1347 n_params = sum(p.numel() for p in model.parameters()) / 1e6
1348
1349 seq_lengths = [512, 1024, 2048, 4096]
1350 latencies_ms = []
1351 throughputs = []
1352 errors = []
1353 for seqlen in seq_lengths:
1354 ids = torch.randint(0, vocab, (1, seqlen), device=device)
1355 try:
1356 with torch.no_grad():
1357 fwd_t = _time_fn(lambda: model(ids), warmup=3, repeat=10)
1358 out = model(ids)
1359 if not torch.isfinite(out).all():
1360 errors.append("seq " + str(seqlen) + ": non-finite output")
1361 latencies_ms.append(round(fwd_t * 1000.0, 2))
1362 throughputs.append(round(seqlen / fwd_t, 0))
1363 except Exception as exc:
1364 errors.append("seq " + str(seqlen) + ": " + type(exc).__name__ + ": " + str(exc))
1365 latencies_ms.append(0.0)
1366 throughputs.append(0.0)
1367
1368 peak = max(throughputs) if throughputs else 0.0
1369 success = len(errors) == 0 and peak > 0
1370 metrics = {
1371 "backend": "ck",
1372 "flash_attn_version": getattr(flash_attn, "__version__", "unknown"),
1373 "model_params_m": round(n_params),
1374 "model_config": str(n_layers) + "L d" + str(dim) + " h" + str(n_heads)
1375 + " GQA-kv" + str(n_kv) + " (genuine Llama-style)",
1376 "seq_lengths": seq_lengths,
1377 "forward_samples_ms": latencies_ms,
1378 "throughput_samples_tok_s": throughputs,
1379 "peak_throughput_tok_s": peak,
1380 }
1381 return success, metrics, errors
1382
1383
1384def _vllm():
1385 target_device = _env_or_default("VLLM_TARGET_DEVICE", "rocm")
1386 if target_device not in {"rocm", "cuda", "cpu"}:
1387 target_device = "rocm"
1388
1389 def _visible_devices_from_env():
1390 visible = os.environ.get("HIP_VISIBLE_DEVICES") or os.environ.get("ROCR_VISIBLE_DEVICES") or os.environ.get("CUDA_VISIBLE_DEVICES") or ""
1391 candidates = []
1392 seen = set()
1393 for token in visible.split(","):
1394 item = token.strip()
1395 if not item.isdigit() or item in seen:
1396 continue
1397 seen.add(item)
1398 candidates.append(item)
1399 if not candidates:
1400 candidates = ["0"]
1401
1402 return ",".join(candidates), candidates[0]
1403
1404 visible_devices, primary_visible = _visible_devices_from_env()
1405
1406 candidates = _resolve_vllm_model_candidates()
1407 candidate_names = [f"{c.get('format')}:{c.get('model')}" for c in candidates]
1408 torch = _load_torch()
1409 if isinstance(torch, tuple):
1410 _, errors = torch
1411 reason = "; ".join(errors) if errors else "unable to import torch"
1412 return False, _degraded_metrics("vllm", reason, {
1413 "model": "vLLM (Unavailable)",
1414 "model_format": "unavailable",
1415 "throughput_tokens_per_sec": 0.0,
1416 "latency_ms": 0.0,
1417 "throughput_samples": [],
1418 "target_device": target_device,
1419 "candidate_models": candidate_names,
1420 }), [reason]
1421 runtime_probe_hint = ""
1422 if target_device in {"rocm", "cuda"}:
1423 try:
1424 if not torch.cuda.is_available():
1425 runtime_probe_hint = "ROCm torch reports no available HIP GPUs in benchmark environment"
1426 else:
1427 torch.cuda.get_device_properties(0)
1428 except Exception as exc:
1429 runtime_probe_hint = f"ROCm torch GPU initialization failed before vLLM load: {exc}"
1430 def _patch_amdsmi_for_vllm_arch_probe():
1431 try:
1432 import sys
1433 import types
1434
1435 if os.environ.get("MLSTACK_VLLM_DISABLE_AMDSMI_SHIM", "").strip() in {"1", "true", "TRUE"}:
1436 return
1437 try:
1438 import amdsmi as _real_amdsmi
1439 except Exception:
1440 _real_amdsmi = None
1441 if _real_amdsmi is not None:
1442 return
1443
1444 arch_hint = (
1445 os.environ.get("GPU_ARCH")
1446 or os.environ.get("PYTORCH_ROCM_ARCH")
1447 or os.environ.get("HSA_OVERRIDE_GFX_VERSION")
1448 or ""
1449 ).strip()
1450 if not arch_hint:
1451 return
1452 if not arch_hint.startswith("gfx") and "." in arch_hint:
1453 parts = [p for p in arch_hint.split(".") if p]
1454 if len(parts) >= 2:
1455 arch_hint = f"gfx{parts[0]}{parts[1]}"
1456
1457 shim = types.ModuleType("amdsmi")
1458
1459 class AmdSmiException(Exception):
1460 pass
1461
1462 def amdsmi_init():
1463 return None
1464
1465 def amdsmi_shut_down():
1466 return None
1467
1468 def amdsmi_get_processor_handles():
1469 return [0]
1470
1471 def amdsmi_get_gpu_asic_info(_handle):
1472 return {"target_graphics_version": arch_hint}
1473
1474 def amdsmi_topo_get_link_type(*_args, **_kwargs):
1475 return (0, 0)
1476
1477 shim.AmdSmiException = AmdSmiException
1478 shim.amdsmi_init = amdsmi_init
1479 shim.amdsmi_shut_down = amdsmi_shut_down
1480 shim.amdsmi_get_processor_handles = amdsmi_get_processor_handles
1481 shim.amdsmi_get_gpu_asic_info = amdsmi_get_gpu_asic_info
1482 shim.amdsmi_topo_get_link_type = amdsmi_topo_get_link_type
1483
1484 sys.modules["amdsmi"] = shim
1485 except Exception:
1486 # Best-effort compatibility shim.
1487 pass
1488
1489 _patch_amdsmi_for_vllm_arch_probe()
1490 try:
1491 import vllm
1492 from vllm import LLM, SamplingParams
1493 import logging
1494 import importlib
1495 # Suppress vLLM and related logging to avoid breaking JSON parsing
1496 logging.getLogger("vllm").setLevel(logging.ERROR)
1497 except Exception as exc:
1498 err = f"vLLM not available: {exc}"
1499 return False, _degraded_metrics("vllm", err, {
1500 "model": "vLLM (Unavailable)",
1501 "model_format": "unavailable",
1502 "throughput_tokens_per_sec": 0.0,
1503 "latency_ms": 0.0,
1504 "throughput_samples": [],
1505 "target_device": target_device,
1506 "candidate_models": candidate_names,
1507 }), [err]
1508 abi_errors = []
1509 for ext_name in ("vllm._C", "vllm._rocm_C"):
1510 try:
1511 importlib.import_module(ext_name)
1512 except Exception as exc:
1513 abi_errors.append(f"{ext_name} import failed: {exc}")
1514 if len(abi_errors) == 2:
1515 reason = "vLLM ROCm native extensions failed to load; " + "; ".join(abi_errors)
1516 return False, _degraded_metrics("vllm", reason, {
1517 "model": "vLLM (Unavailable)",
1518 "model_format": "unavailable",
1519 "throughput_tokens_per_sec": 0.0,
1520 "latency_ms": 0.0,
1521 "throughput_samples": [],
1522 "target_device": target_device,
1523 "candidate_models": candidate_names,
1524 }), [reason]
1525 platform_device_type = ""
1526 platform_class = "unknown"
1527 try:
1528 from vllm.platforms import current_platform
1529 platform_device_type = str(getattr(current_platform, "device_type", "") or "").strip()
1530 platform_class = current_platform.__class__.__name__
1531 except Exception:
1532 pass
1533 if not platform_device_type:
1534 platform_reason = (
1535 "vLLM platform detection returned an empty device_type (UnspecifiedPlatform). "
1536 "This commonly means amdsmi is missing, so ROCm platform detection failed. "
1537 "Install amdsmi in the benchmark/runtime Python environment and rerun."
1538 )
1539 return False, _degraded_metrics("vllm", platform_reason, {
1540 "model": "vLLM (Detected)",
1541 "model_format": "unavailable",
1542 "throughput_tokens_per_sec": 0.0,
1543 "latency_ms": 0.0,
1544 "throughput_samples": [],
1545 "target_device": target_device,
1546 "candidate_models": candidate_names,
1547 "vllm_platform_class": platform_class,
1548 "vllm_platform_device_type": platform_device_type,
1549 }), [platform_reason]
1550 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
1551 if not probe_ok:
1552 runtime_probe_hint = probe_reason if not runtime_probe_hint else f"{runtime_probe_hint}; {probe_reason}"
1553
1554 prompts = [
1555 "The future of AI is",
1556 "ROCm performance on AMD is",
1557 "vLLM is a fast",
1558 ]
1559 max_tokens = 24
1560 attempt_errors = []
1561
1562 def _run_vllm_attempt_subprocess(llm_kwargs, prompts, max_tokens):
1563 payload = {
1564 "llm_kwargs": llm_kwargs,
1565 "prompts": prompts,
1566 "max_tokens": int(max_tokens),
1567 }
1568 runner = r"""
1569import json
1570import logging
1571import os
1572import sys
1573import time
1574import traceback
1575
1576def _emit(obj):
1577 print(json.dumps(obj))
1578
1579def _main():
1580 try:
1581 payload_path = sys.argv[1] if len(sys.argv) > 1 else ""
1582 with open(payload_path, "r", encoding="utf-8") as payload_file:
1583 payload = json.load(payload_file)
1584 llm_kwargs = payload.get("llm_kwargs", {})
1585 prompts = payload.get("prompts", [])
1586 max_tokens = int(payload.get("max_tokens", 24))
1587 try:
1588 import types
1589 if os.environ.get("MLSTACK_VLLM_DISABLE_AMDSMI_SHIM", "").strip() not in {"1", "true", "TRUE"}:
1590 try:
1591 import amdsmi as _real_amdsmi
1592 except Exception:
1593 _real_amdsmi = None
1594 if _real_amdsmi is not None:
1595 raise RuntimeError("__MLSTACK_REAL_AMDSMI_PRESENT__")
1596 arch_hint = (
1597 os.environ.get("GPU_ARCH")
1598 or os.environ.get("PYTORCH_ROCM_ARCH")
1599 or os.environ.get("HSA_OVERRIDE_GFX_VERSION")
1600 or ""
1601 ).strip()
1602 if arch_hint and not arch_hint.startswith("gfx") and "." in arch_hint:
1603 parts = [p for p in arch_hint.split(".") if p]
1604 if len(parts) >= 2:
1605 arch_hint = f"gfx{parts[0]}{parts[1]}"
1606 if arch_hint:
1607 shim = types.ModuleType("amdsmi")
1608 class AmdSmiException(Exception):
1609 pass
1610 def amdsmi_init():
1611 return None
1612 def amdsmi_shut_down():
1613 return None
1614 def amdsmi_get_processor_handles():
1615 return [0]
1616 def amdsmi_get_gpu_asic_info(_handle):
1617 return {"target_graphics_version": arch_hint}
1618 def amdsmi_topo_get_link_type(*_args, **_kwargs):
1619 return (0, 0)
1620 shim.AmdSmiException = AmdSmiException
1621 shim.amdsmi_init = amdsmi_init
1622 shim.amdsmi_shut_down = amdsmi_shut_down
1623 shim.amdsmi_get_processor_handles = amdsmi_get_processor_handles
1624 shim.amdsmi_get_gpu_asic_info = amdsmi_get_gpu_asic_info
1625 shim.amdsmi_topo_get_link_type = amdsmi_topo_get_link_type
1626 sys.modules["amdsmi"] = shim
1627 except RuntimeError as exc:
1628 if str(exc) != "__MLSTACK_REAL_AMDSMI_PRESENT__":
1629 pass
1630 except Exception:
1631 pass
1632 # vLLM 0.25.0's ROCm V1 multiprocessing engine path can hang or fail
1633 # before the offline LLM benchmark reaches real generation. Keep the
1634 # benchmark environment sourced from ~/.mlstack_env, but force the local
1635 # benchmark process onto vLLM's in-process EngineCore path via vLLM's
1636 # lazy module setting instead of mutating os.environ.
1637 import vllm.envs as _vllm_envs
1638 _vllm_envs.environment_variables["VLLM_ENABLE_V1_MULTIPROCESSING"] = lambda: False
1639 if hasattr(_vllm_envs.__getattr__, "cache_clear"):
1640 _vllm_envs.__getattr__.cache_clear()
1641 import vllm
1642 from vllm import LLM, SamplingParams
1643 logging.getLogger("vllm").setLevel(logging.ERROR)
1644 sampling_params = SamplingParams(temperature=0.0, max_tokens=max_tokens)
1645 startup_start = time.perf_counter()
1646 _emit({"event": "startup_begin", "phase": "vllm_startup", "vllm_v1_multiprocessing": False})
1647 llm = LLM(**llm_kwargs)
1648 startup_ms = int((time.perf_counter() - startup_start) * 1000)
1649 _emit({"event": "startup_complete", "phase": "vllm_startup", "startup_ms": startup_ms})
1650
1651 generation_start = time.perf_counter()
1652 outputs = llm.generate(prompts, sampling_params)
1653 generation_elapsed = time.perf_counter() - generation_start
1654
1655 throughput_samples = []
1656 for prompt in prompts:
1657 sample_start = time.perf_counter()
1658 sample_out = llm.generate([prompt], sampling_params)
1659 sample_elapsed = time.perf_counter() - sample_start
1660 tokens = len(sample_out[0].outputs[0].token_ids)
1661 throughput_samples.append(tokens / sample_elapsed if sample_elapsed > 0 else 0.0)
1662
1663 total_tokens = sum(len(output.outputs[0].token_ids) for output in outputs)
1664 throughput = total_tokens / generation_elapsed if generation_elapsed > 0 else 0.0
1665 _emit({
1666 "ok": True,
1667 "startup_ms": startup_ms,
1668 "generation_ms": int(generation_elapsed * 1000),
1669 "throughput_tokens_per_sec": round(throughput, 2),
1670 "latency_ms": round((generation_elapsed / max(len(prompts), 1)) * 1000, 2),
1671 "throughput_samples": [round(x, 2) for x in throughput_samples],
1672 "vllm_v1_multiprocessing": False,
1673 })
1674 except Exception as exc:
1675 tb = traceback.format_exc().strip()
1676 tail_lines = [line.strip() for line in tb.splitlines()[-12:] if line.strip()]
1677 msg = str(exc).strip() or repr(exc)
1678 if tail_lines:
1679 msg = f"{msg} :: traceback_tail: {' | '.join(tail_lines)}"
1680 _emit({"ok": False, "error": msg})
1681
1682if __name__ == "__main__":
1683 _main()
1684"""
1685 payload_path = None
1686 runner_path = None
1687 timeout_seconds = 900
1688 attempt_started_at = None
1689 try:
1690 with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as payload_file:
1691 json.dump(payload, payload_file)
1692 payload_path = payload_file.name
1693 with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as runner_file:
1694 runner_file.write(runner)
1695 runner_path = runner_file.name
1696 attempt_started_at = time.perf_counter()
1697 proc = subprocess.run(
1698 [sys.executable, runner_path, payload_path],
1699 capture_output=True,
1700 text=True,
1701 timeout=timeout_seconds,
1702 )
1703 except subprocess.TimeoutExpired as exc:
1704 stdout = exc.stdout.decode("utf-8", "replace") if isinstance(exc.stdout, bytes) else (exc.stdout or "")
1705 stderr = exc.stderr.decode("utf-8", "replace") if isinstance(exc.stderr, bytes) else (exc.stderr or "")
1706 timeout_ms = int(((time.perf_counter() - attempt_started_at) * 1000) if attempt_started_at else (timeout_seconds * 1000))
1707 startup_complete = False
1708 startup_ms = 0
1709 for line in stdout.splitlines():
1710 text = line.strip()
1711 if not text.startswith("{") or not text.endswith("}"):
1712 continue
1713 try:
1714 event = json.loads(text)
1715 except Exception:
1716 continue
1717 if isinstance(event, dict) and event.get("event") == "startup_complete":
1718 startup_complete = True
1719 startup_ms = int(event.get("startup_ms") or 0)
1720 timeout_phase = "generation" if startup_complete else "startup"
1721 detail_parts = []
1722 for label, text in (("stdout_tail", stdout), ("stderr_tail", stderr)):
1723 tail = " | ".join([line.strip() for line in text.splitlines() if line.strip()][-20:])
1724 if tail:
1725 detail_parts.append(f"{label}: {tail[:3000]}")
1726 detail = "; ".join(detail_parts) if detail_parts else "no subprocess output captured"
1727 return {
1728 "ok": False,
1729 "error": f"vLLM {timeout_phase} timed out after {timeout_seconds} seconds; {detail}",
1730 "startup_ms": startup_ms,
1731 "startup_timed_out": not startup_complete,
1732 "generation_timed_out": startup_complete,
1733 "timeout_ms": timeout_ms,
1734 "timeout_limit_ms": timeout_seconds * 1000,
1735 "vllm_timeout_phase": timeout_phase,
1736 }
1737 except Exception as exc:
1738 return {"ok": False, "error": f"subprocess launch failed: {exc}"}
1739 finally:
1740 for temp_path in (payload_path, runner_path):
1741 if temp_path:
1742 try:
1743 os.unlink(temp_path)
1744 except OSError:
1745 pass
1746
1747 merged = (proc.stdout or "").splitlines()
1748 if proc.stderr:
1749 merged.extend(proc.stderr.splitlines())
1750 for line in reversed(merged):
1751 text = line.strip()
1752 if not text.startswith("{") or not text.endswith("}"):
1753 continue
1754 try:
1755 parsed = json.loads(text)
1756 except Exception:
1757 continue
1758 if isinstance(parsed, dict) and "ok" in parsed:
1759 if not parsed.get("ok", False):
1760 log_tail = []
1761 for raw in merged:
1762 msg = raw.strip()
1763 if not msg:
1764 continue
1765 if msg.startswith("{") and msg.endswith("}"):
1766 continue
1767 lower = msg.lower()
1768 if any(
1769 marker in lower
1770 for marker in (
1771 "error",
1772 "exception",
1773 "traceback",
1774 "runtimeerror",
1775 "valueerror",
1776 "fatal",
1777 "enginecore",
1778 "failed",
1779 )
1780 ):
1781 log_tail.append(msg)
1782 if log_tail:
1783 tail = " | ".join(log_tail[-12:])[:3000]
1784 base = str(parsed.get("error") or "unknown vLLM subprocess failure")
1785 if tail and tail not in base:
1786 parsed["error"] = f"{base}; engine_log_tail: {tail}"
1787 return parsed
1788
1789 detail = " ".join([line.strip() for line in merged if line.strip()])[:4000]
1790 if not detail:
1791 detail = f"exit code {proc.returncode}"
1792 return {"ok": False, "error": detail}
1793
1794 last_attempt_metrics = {}
1795 for candidate in candidates:
1796 model_name = candidate.get("model")
1797 model_format = candidate.get("format", "unknown")
1798 if not model_name:
1799 continue
1800
1801 model_cache_ms = 0
1802 llm_kwargs = {
1803 "model": model_name,
1804 "trust_remote_code": True,
1805 "enforce_eager": True,
1806 "gpu_memory_utilization": 0.65,
1807 "disable_log_stats": True,
1808 "tensor_parallel_size": 1,
1809 "dtype": "float16",
1810 }
1811 if model_format == "gguf":
1812 tokenizer_name = candidate.get("tokenizer")
1813 if tokenizer_name:
1814 llm_kwargs["tokenizer"] = tokenizer_name
1815 elif model_format == "safetensors":
1816 model_cache_start = time.perf_counter()
1817 weights_ready, weights_reason = _ensure_cached_hf_model_weights(model_name)
1818 model_cache_ms = int((time.perf_counter() - model_cache_start) * 1000)
1819 last_attempt_metrics = {"model_cache_ms": model_cache_ms}
1820 if not weights_ready:
1821 attempt_errors.append(f"{model_format} model {model_name} failed: {weights_reason}")
1822 continue
1823
1824 attempt = _run_vllm_attempt_subprocess(llm_kwargs, prompts, max_tokens)
1825 last_attempt_metrics = {"model_cache_ms": model_cache_ms}
1826 for key in (
1827 "startup_ms",
1828 "generation_ms",
1829 "startup_timed_out",
1830 "generation_timed_out",
1831 "timeout_ms",
1832 "timeout_limit_ms",
1833 "vllm_timeout_phase",
1834 "vllm_v1_multiprocessing",
1835 ):
1836 if key in attempt:
1837 last_attempt_metrics[key] = attempt[key]
1838 if attempt.get("ok"):
1839 return True, {
1840 "model": model_name,
1841 "model_format": model_format,
1842 "target_device": target_device,
1843 "visible_devices": visible_devices or primary_visible,
1844 "model_cache_ms": model_cache_ms,
1845 "startup_ms": int(attempt.get("startup_ms", 0)),
1846 "generation_ms": int(attempt.get("generation_ms", 0)),
1847 "throughput_tokens_per_sec": float(attempt.get("throughput_tokens_per_sec", 0.0)),
1848 "latency_ms": float(attempt.get("latency_ms", 0.0)),
1849 "throughput_samples": [
1850 float(x) for x in (attempt.get("throughput_samples") or [])
1851 ],
1852 "vllm_v1_multiprocessing": attempt.get("vllm_v1_multiprocessing"),
1853 "candidate_models": candidate_names,
1854 }, []
1855 err_msg = str(attempt.get("error") or "unknown vLLM execution error")
1856 if "Entry Not Found" in err_msg or "not found on the Hugging Face Hub" in err_msg:
1857 err_msg = (
1858 f"Model {model_name} not found. Download/copy it first or override "
1859 "MLSTACK_BENCH_VLLM_SAFETENSORS_MODEL / MLSTACK_BENCH_VLLM_GGUF_MODEL_PATH"
1860 )
1861 elif "authentication" in err_msg.lower():
1862 err_msg = "Hugging Face authentication required. Run 'huggingface-cli login' first."
1863 attempt_errors.append(f"{model_format} model {model_name} failed: {err_msg}")
1864
1865 failure_reason = "No tiny vLLM benchmark model could be loaded"
1866 if runtime_probe_hint:
1867 failure_reason = f"{failure_reason}; runtime_probe={runtime_probe_hint}"
1868 if attempt_errors:
1869 failure_reason = f"{failure_reason}; {attempt_errors[0]}"
1870
1871 failure_metrics = {
1872 "model": "vLLM (Detected)",
1873 "model_format": "unavailable",
1874 "target_device": target_device,
1875 "visible_devices": visible_devices or primary_visible,
1876 "throughput_tokens_per_sec": 0.0,
1877 "latency_ms": 0.0,
1878 "throughput_samples": [],
1879 "candidate_models": candidate_names,
1880 }
1881 failure_metrics.update(last_attempt_metrics)
1882 return False, _degraded_metrics("vllm", failure_reason, failure_metrics), attempt_errors or [failure_reason]
1883
1884
1885def _deepspeed():
1886 _set_amd_gpu_id_table_env()
1887 try:
1888 import deepspeed
1889 import torch
1890 import os
1891 except Exception as exc:
1892 err = f"DeepSpeed or Torch not available: {exc}"
1893 return False, _degraded_metrics("deepspeed", err, {
1894 "throughput_samples_per_sec": 0.0,
1895 "avg_latency_ms": 0.0,
1896 "stage": 1,
1897 "accelerator": "rocm",
1898 "samples": [],
1899 }), [err]
1900 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
1901 if not probe_ok:
1902 extra = {
1903 "throughput_samples_per_sec": 0.0,
1904 "avg_latency_ms": 0.0,
1905 "stage": 1,
1906 "accelerator": "rocm",
1907 "samples": [],
1908 }
1909 if isinstance(probe_meta, dict):
1910 extra.update(probe_meta)
1911 return False, _degraded_metrics("deepspeed", probe_reason, extra), [probe_reason]
1912
1913 # Ensure ROCm environment
1914 os.environ["DS_ACCELERATOR"] = "rocm"
1915
1916 try:
1917 from deepspeed.accelerator import get_accelerator
1918
1919 accelerator = get_accelerator()
1920 device_name = accelerator.device_name()
1921 device = torch.device(device_name if ":" in device_name else f"{device_name}:0")
1922
1923 class SimpleModel(torch.nn.Module):
1924 def __init__(self):
1925 super().__init__()
1926 self.net = torch.nn.Sequential(
1927 torch.nn.Linear(1024, 1024),
1928 torch.nn.ReLU(),
1929 torch.nn.Linear(1024, 1024),
1930 )
1931
1932 def forward(self, x):
1933 return self.net(x)
1934
1935 model = SimpleModel().to(device).half()
1936 batch_size = 4
1937 input_data = torch.randn(batch_size, 1024, device=device, dtype=torch.float16)
1938
1939 ds_config = {
1940 "train_batch_size": batch_size,
1941 "train_micro_batch_size_per_gpu": batch_size,
1942 "gradient_accumulation_steps": 1,
1943 "zero_optimization": {"stage": 0},
1944 "fp16": {"enabled": True},
1945 # DeepSpeed internally computes `step % steps_per_print`; keep this non-zero.
1946 "steps_per_print": 1,
1947 "wall_clock_breakdown": False,
1948 }
1949 optimizer = torch.optim.AdamW(model.parameters(), lr=0.001)
1950
1951 try:
1952 model_engine, _, _, _ = deepspeed.initialize(
1953 config=ds_config,
1954 model=model,
1955 model_parameters=model.parameters(),
1956 optimizer=optimizer,
1957 )
1958 mode = "train"
1959 except Exception as init_exc:
1960 init_err = str(init_exc)
1961 if "fused_adam" in init_err.lower() or "cuda_runtime_api.h" in init_err:
1962 model_engine = deepspeed.init_inference(
1963 model,
1964 mp_size=1,
1965 dtype=torch.float16,
1966 replace_with_kernel_inject=False,
1967 )
1968 mode = "inference"
1969 else:
1970 raise
1971
1972 times = []
1973 if mode == "train":
1974 for _ in range(2):
1975 out = model_engine(input_data)
1976 model_engine.backward(out.sum())
1977 model_engine.step()
1978
1979 for _ in range(5):
1980 accelerator.synchronize()
1981 start = time.perf_counter()
1982 out = model_engine(input_data)
1983 model_engine.backward(out.sum())
1984 model_engine.step()
1985 accelerator.synchronize()
1986 times.append(time.perf_counter() - start)
1987 else:
1988 model_engine.eval()
1989 for _ in range(2):
1990 with torch.no_grad():
1991 _ = model_engine(input_data)
1992 accelerator.synchronize()
1993
1994 for _ in range(5):
1995 accelerator.synchronize()
1996 start = time.perf_counter()
1997 with torch.no_grad():
1998 _ = model_engine(input_data)
1999 accelerator.synchronize()
2000 times.append(time.perf_counter() - start)
2001
2002 avg_time = sum(times) / len(times)
2003 throughput = batch_size / avg_time
2004
2005 return True, {
2006 "throughput_samples_per_sec": round(throughput, 2),
2007 "avg_latency_ms": round(avg_time * 1000, 2),
2008 "stage": 0 if mode == "train" else 0,
2009 "accelerator": "rocm",
2010 "mode": mode,
2011 "samples": [round(batch_size / t, 2) for t in times],
2012 }, []
2013 except Exception as exc:
2014 err = f"DeepSpeed benchmark failed: {exc}"
2015 return False, _degraded_metrics("deepspeed", err, {
2016 "throughput_samples_per_sec": 0.0,
2017 "avg_latency_ms": 0.0,
2018 "stage": 1,
2019 "accelerator": "rocm",
2020 "samples": [],
2021 }), [err]
2022
2023
2024def _megatron():
2025 try:
2026 import torch
2027 import megatron
2028 except Exception as exc:
2029 err = f"Megatron not available: {exc}"
2030 return False, _degraded_metrics("megatron", err, {
2031 "megatron_backend": "unavailable",
2032 "megatron_throughput_samples_per_sec": 0.0,
2033 "megatron_avg_latency_ms": 0.0,
2034 "megatron_samples": [],
2035 }), [err]
2036
2037 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
2038 if not probe_ok:
2039 extra = {
2040 "megatron_backend": "megatron-core",
2041 "megatron_throughput_samples_per_sec": 0.0,
2042 "megatron_avg_latency_ms": 0.0,
2043 "megatron_samples": [],
2044 }
2045 if isinstance(probe_meta, dict):
2046 extra.update(probe_meta)
2047 return False, _degraded_metrics("megatron", probe_reason, extra), [probe_reason]
2048
2049 try:
2050 device = torch.device("cuda:0")
2051 model = torch.nn.Sequential(
2052 torch.nn.Linear(4096, 4096),
2053 torch.nn.GELU(),
2054 torch.nn.Linear(4096, 4096),
2055 ).to(device).half()
2056
2057 input_data = torch.randn(8, 4096, device=device, dtype=torch.float16)
2058 samples = []
2059
2060 for _ in range(2):
2061 out = model(input_data)
2062 _ = out.sum()
2063 torch.cuda.synchronize()
2064
2065 for _ in range(5):
2066 torch.cuda.synchronize()
2067 start = time.perf_counter()
2068 out = model(input_data)
2069 _ = out.sum()
2070 torch.cuda.synchronize()
2071 elapsed = time.perf_counter() - start
2072 samples.append(8.0 / elapsed if elapsed > 0 else 0.0)
2073
2074 avg_tput = sum(samples) / len(samples) if samples else 0.0
2075 avg_latency_ms = (1000.0 / avg_tput * 8.0) if avg_tput > 0 else 0.0
2076 return True, {
2077 "megatron_backend": getattr(megatron, "__name__", "megatron"),
2078 "megatron_throughput_samples_per_sec": round(avg_tput, 2),
2079 "megatron_avg_latency_ms": round(avg_latency_ms, 2),
2080 "megatron_samples": [round(x, 2) for x in samples],
2081 }, []
2082 except Exception as exc:
2083 err = f"Megatron benchmark failed: {exc}"
2084 return False, _degraded_metrics("megatron", err, {
2085 "megatron_backend": "megatron-core",
2086 "megatron_throughput_samples_per_sec": 0.0,
2087 "megatron_avg_latency_ms": 0.0,
2088 "megatron_samples": [],
2089 }), [err]
2090
2091
2092def _onnx():
2093 import os
2094 try:
2095 import onnxruntime as ort
2096 except Exception as exc:
2097 err = f"onnxruntime not available: {exc}"
2098 return False, _degraded_metrics("onnx", err, {
2099 "ort_version": "unavailable",
2100 "provider": "none",
2101 "providers_available": [],
2102 "model_load_ms": 0.0,
2103 "session_create_ms": 0.0,
2104 "inference_latency_p50_ms": 0.0,
2105 "inference_latency_p95_ms": 0.0,
2106 "inference_latency_p99_ms": 0.0,
2107 "throughput_inf_per_sec": 0.0,
2108 "input_shape": [],
2109 "output_shape": [],
2110 "graph_opt_level": "unavailable",
2111 "inference_samples": [],
2112 }), [err]
2113
2114 amd_provider_order = ("MIGraphXExecutionProvider", "ROCMExecutionProvider")
2115 ort_version = getattr(ort, "__version__", None)
2116 if not ort_version or not hasattr(ort, "get_available_providers"):
2117 err = f"onnxruntime import is incomplete: {getattr(ort, '__file__', '<unknown>')}"
2118 return False, _degraded_metrics("onnx", err, {
2119 "ort_version": "incomplete",
2120 "provider": "none",
2121 "providers_available": [],
2122 "provider_priority": list(amd_provider_order),
2123 "model_load_ms": 0.0,
2124 "session_create_ms": 0.0,
2125 "inference_latency_p50_ms": 0.0,
2126 "inference_latency_p95_ms": 0.0,
2127 "inference_latency_p99_ms": 0.0,
2128 "throughput_inf_per_sec": 0.0,
2129 "input_shape": [],
2130 "output_shape": [],
2131 "graph_opt_level": "ORT_ENABLE_ALL",
2132 "inference_samples": [],
2133 }), [err]
2134
2135 all_providers = ort.get_available_providers()
2136 providers_available = list(all_providers)
2137
2138 provider = next((name for name in amd_provider_order if name in all_providers), None)
2139 if provider is None:
2140 err = (
2141 "ONNX Runtime AMD execution provider unavailable; expected "
2142 "MiGraphXExecutionProvider or legacy ROCMExecutionProvider; "
2143 f"available={providers_available}"
2144 )
2145 return False, _degraded_metrics("onnx", err, {
2146 "ort_version": ort_version,
2147 "provider": "none",
2148 "providers_available": providers_available,
2149 "provider_priority": list(amd_provider_order),
2150 "model_load_ms": 0.0,
2151 "session_create_ms": 0.0,
2152 "inference_latency_p50_ms": 0.0,
2153 "inference_latency_p95_ms": 0.0,
2154 "inference_latency_p99_ms": 0.0,
2155 "throughput_inf_per_sec": 0.0,
2156 "input_shape": [],
2157 "output_shape": [],
2158 "graph_opt_level": "ORT_ENABLE_ALL",
2159 "inference_samples": [],
2160 }), [err]
2161
2162 try:
2163 import numpy as np
2164 except Exception:
2165 err = "numpy not available for ONNX benchmark"
2166 return False, _degraded_metrics("onnx", err, {
2167 "ort_version": ort_version,
2168 "provider": provider,
2169 "providers_available": providers_available,
2170 "provider_priority": list(amd_provider_order),
2171 "model_load_ms": 0.0,
2172 "session_create_ms": 0.0,
2173 "inference_latency_p50_ms": 0.0,
2174 "inference_latency_p95_ms": 0.0,
2175 "inference_latency_p99_ms": 0.0,
2176 "throughput_inf_per_sec": 0.0,
2177 "input_shape": [],
2178 "output_shape": [],
2179 "graph_opt_level": "ORT_ENABLE_ALL",
2180 "inference_samples": [],
2181 }), [err]
2182
2183 try:
2184 import onnx
2185 from onnx import helper, TensorProto, numpy_helper
2186 except Exception:
2187 onnx = None
2188
2189 batch_size = 8
2190 hidden = 512
2191 input_shape = [batch_size, hidden]
2192
2193 def _build_test_model():
2194 X = helper.make_tensor_value_info("input", TensorProto.FLOAT, input_shape)
2195 Y = helper.make_tensor_value_info("output", TensorProto.FLOAT, input_shape)
2196 W_init = numpy_helper.from_array(
2197 np.random.randn(hidden, hidden).astype(np.float32) * 0.01, name="W"
2198 )
2199 B_init = numpy_helper.from_array(
2200 np.zeros(hidden, dtype=np.float32), name="B"
2201 )
2202 matmul = helper.make_node("MatMul", ["input", "W"], ["matmul_out"])
2203 add = helper.make_node("Add", ["matmul_out", "B"], ["add_out"])
2204 relu = helper.make_node("Relu", ["add_out"], ["output"])
2205 graph = helper.make_graph(
2206 [matmul, add, relu], "bench_graph", [X], [Y], initializer=[W_init, B_init]
2207 )
2208 model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 17)])
2209 model.ir_version = 8
2210 onnx.checker.validate = False
2211 return model.SerializeToString()
2212
2213 def _build_quantized_model():
2214 X = helper.make_tensor_value_info("input", TensorProto.FLOAT, input_shape)
2215 Y = helper.make_tensor_value_info("output", TensorProto.FLOAT, input_shape)
2216 W_quant = np.clip(
2217 np.round((np.random.randn(hidden, hidden).astype(np.float32) * 0.01) / 0.01) + 128,
2218 0,
2219 255,
2220 ).astype(np.uint8)
2221 W_init = numpy_helper.from_array(W_quant, name="W_quant")
2222 W_zp = numpy_helper.from_array(np.array(128, dtype=np.uint8), name="W_zp")
2223 B_init = numpy_helper.from_array(
2224 np.zeros(hidden, dtype=np.float32), name="B"
2225 )
2226 dq = helper.make_node(
2227 "DynamicQuantizeLinear", ["input"], ["input_quant", "input_scale", "input_zp"]
2228 )
2229 matmul_int = helper.make_node(
2230 "MatMulInteger", ["input_quant", "W_quant", "input_zp", "W_zp"], ["matmul_int_out"]
2231 )
2232 cast_out = helper.make_node(
2233 "Cast", ["matmul_int_out"], ["matmul_fp_out"], to=TensorProto.FLOAT
2234 )
2235 add = helper.make_node("Add", ["matmul_fp_out", "B"], ["output"])
2236 graph = helper.make_graph(
2237 [dq, matmul_int, cast_out, add], "quant_bench_graph",
2238 [X], [Y], initializer=[W_init, W_zp, B_init]
2239 )
2240 model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 17)])
2241 model.ir_version = 8
2242 onnx.checker.validate = False
2243 return model.SerializeToString()
2244
2245 import tempfile
2246 sess_opts = ort.SessionOptions()
2247 sess_opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
2248 sess_opts.log_severity_level = 3
2249
2250 results = {}
2251 errors = []
2252
2253 def _assert_amd_session(session):
2254 session_providers = list(session.get_providers())
2255 if (
2256 provider not in session_providers
2257 or not session_providers
2258 or session_providers[0] != provider
2259 ):
2260 raise RuntimeError(
2261 "ONNX session did not prioritize AMD provider; "
2262 f"selected={provider}; session_providers={session_providers}"
2263 )
2264 return session_providers
2265
2266 # --- FP32 model benchmark ---
2267 try:
2268 if onnx is not None:
2269 model_bytes = _build_test_model()
2270 else:
2271 model_bytes = None
2272
2273 if model_bytes is None:
2274 raise RuntimeError("onnx package required to build test model")
2275
2276 with tempfile.NamedTemporaryFile(suffix=".onnx", delete=False) as f:
2277 f.write(model_bytes)
2278 fp32_path = f.name
2279
2280 t0 = time.perf_counter()
2281 session = ort.InferenceSession(fp32_path, sess_opts, providers=[provider])
2282 session_create_ms = (time.perf_counter() - t0) * 1000.0
2283 session_providers = _assert_amd_session(session)
2284
2285 input_meta = session.get_inputs()[0]
2286 output_meta = session.get_outputs()[0]
2287 input_name = input_meta.name
2288 actual_input_shape = input_meta.shape
2289 output_shape = output_meta.shape
2290
2291 if isinstance(actual_input_shape[0], int):
2292 bench_batch = actual_input_shape[0]
2293 else:
2294 bench_batch = batch_size
2295 actual_input_shape_resolved = [bench_batch] + [
2296 d if isinstance(d, int) else hidden for d in actual_input_shape[1:]
2297 ]
2298
2299 feed = {input_name: np.random.randn(*actual_input_shape_resolved).astype(np.float32)}
2300
2301 for _ in range(3):
2302 session.run(None, feed)
2303
2304 num_runs = 20
2305 latencies = []
2306 for _ in range(num_runs):
2307 t_start = time.perf_counter()
2308 session.run(None, feed)
2309 latencies.append((time.perf_counter() - t_start) * 1000.0)
2310
2311 latencies.sort()
2312 p50 = latencies[len(latencies) // 2]
2313 p95 = latencies[int(len(latencies) * 0.95)]
2314 p99 = latencies[int(len(latencies) * 0.99)]
2315 avg_latency = sum(latencies) / len(latencies)
2316 throughput = bench_batch / (avg_latency / 1000.0) if avg_latency > 0 else 0.0
2317
2318 mem_rss_mb = 0.0
2319 try:
2320 import resource
2321 mem_rss_mb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024.0
2322 except Exception:
2323 pass
2324
2325 results.update({
2326 "fp32_session_create_ms": round(session_create_ms, 2),
2327 "fp32_session_providers": session_providers,
2328 "fp32_inference_latency_p50_ms": round(p50, 3),
2329 "fp32_inference_latency_p95_ms": round(p95, 3),
2330 "fp32_inference_latency_p99_ms": round(p99, 3),
2331 "fp32_avg_latency_ms": round(avg_latency, 3),
2332 "fp32_throughput_inf_per_sec": round(throughput, 2),
2333 "fp32_inference_samples": [round(l, 3) for l in latencies],
2334 "input_shape": actual_input_shape_resolved,
2335 "output_shape": list(output_shape) if output_shape else [],
2336 "input_dtype": str(input_meta.type),
2337 "output_dtype": str(output_meta.type),
2338 "peak_rss_mb": round(mem_rss_mb, 1),
2339 "num_warmup": 3,
2340 "num_timed_runs": num_runs,
2341 })
2342
2343 os.unlink(fp32_path)
2344 del session
2345 except Exception as exc:
2346 results["fp32_error"] = str(exc)
2347 errors.append(f"FP32 benchmark failed: {exc}")
2348
2349 # --- Quantized model benchmark ---
2350 quantized_ok = False
2351 try:
2352 if onnx is not None:
2353 q_model_bytes = _build_quantized_model()
2354 if q_model_bytes is not None:
2355 with tempfile.NamedTemporaryFile(suffix=".onnx", delete=False) as f:
2356 f.write(q_model_bytes)
2357 quant_path = f.name
2358
2359 t0 = time.perf_counter()
2360 q_session = ort.InferenceSession(
2361 quant_path, sess_opts, providers=[provider]
2362 )
2363 q_create_ms = (time.perf_counter() - t0) * 1000.0
2364 q_session_providers = _assert_amd_session(q_session)
2365
2366 q_input = q_session.get_inputs()[0]
2367 q_feed = {
2368 q_input.name: np.random.randn(
2369 *([batch_size] + [
2370 d if isinstance(d, int) else hidden
2371 for d in q_input.shape[1:]
2372 ])
2373 ).astype(np.float32)
2374 }
2375
2376 for _ in range(3):
2377 q_session.run(None, q_feed)
2378
2379 q_latencies = []
2380 for _ in range(20):
2381 t_start = time.perf_counter()
2382 q_session.run(None, q_feed)
2383 q_latencies.append((time.perf_counter() - t_start) * 1000.0)
2384
2385 q_latencies.sort()
2386 q_p50 = q_latencies[len(q_latencies) // 2]
2387 q_avg = sum(q_latencies) / len(q_latencies)
2388 q_throughput = batch_size / (q_avg / 1000.0) if q_avg > 0 else 0.0
2389
2390 results.update({
2391 "quantized_session_create_ms": round(q_create_ms, 2),
2392 "quantized_session_providers": q_session_providers,
2393 "quantized_inference_latency_p50_ms": round(q_p50, 3),
2394 "quantized_avg_latency_ms": round(q_avg, 3),
2395 "quantized_throughput_inf_per_sec": round(q_throughput, 2),
2396 "quantized_inference_samples": [round(l, 3) for l in q_latencies],
2397 "quantized_ops": ["DynamicQuantizeLinear", "MatMulInteger"],
2398 })
2399 quantized_ok = True
2400 os.unlink(quant_path)
2401 del q_session
2402 except Exception as exc:
2403 results["quantized_error"] = str(exc)
2404 errors.append(f"Quantized benchmark failed: {exc}")
2405
2406 p50_val = results.get("fp32_inference_latency_p50_ms", 0.0)
2407 p95_val = results.get("fp32_inference_latency_p95_ms", 0.0)
2408 p99_val = results.get("fp32_inference_latency_p99_ms", 0.0)
2409 tput_val = results.get("fp32_throughput_inf_per_sec", 0.0)
2410
2411 metrics = {
2412 "ort_version": ort_version,
2413 "provider": provider,
2414 "providers_available": providers_available,
2415 "provider_priority": list(amd_provider_order),
2416 "session_providers": results.get("fp32_session_providers", []),
2417 "graph_opt_level": "ORT_ENABLE_ALL",
2418 "model_load_ms": results.get("fp32_session_create_ms", 0.0),
2419 "session_create_ms": results.get("fp32_session_create_ms", 0.0),
2420 "inference_latency_p50_ms": p50_val,
2421 "inference_latency_p95_ms": p95_val,
2422 "inference_latency_p99_ms": p99_val,
2423 "throughput_inf_per_sec": tput_val,
2424 "input_shape": results.get("input_shape", input_shape),
2425 "output_shape": results.get("output_shape", []),
2426 "input_dtype": results.get("input_dtype", "tensor(float)"),
2427 "output_dtype": results.get("output_dtype", "tensor(float)"),
2428 "peak_rss_mb": results.get("peak_rss_mb", 0.0),
2429 "num_warmup": results.get("num_warmup", 0),
2430 "num_timed_runs": results.get("num_timed_runs", 0),
2431 "quantized_supported": quantized_ok,
2432 "inference_samples": results.get("fp32_inference_samples", []),
2433 }
2434
2435 for k in ("fp32_inference_samples", "fp32_session_create_ms", "fp32_inference_latency_p50_ms",
2436 "fp32_inference_latency_p95_ms", "fp32_inference_latency_p99_ms",
2437 "fp32_avg_latency_ms", "fp32_throughput_inf_per_sec", "fp32_session_providers",
2438 "quantized_session_create_ms", "quantized_session_providers", "quantized_inference_latency_p50_ms",
2439 "quantized_avg_latency_ms", "quantized_throughput_inf_per_sec",
2440 "quantized_inference_samples", "quantized_ops",
2441 "quantized_error", "fp32_error"):
2442 if k in results:
2443 metrics[k] = results[k]
2444
2445 success = not errors and "fp32_inference_samples" in results and quantized_ok
2446 return success, metrics, errors
2447
2448
2449BENCHES = {
2450 "gpu-info": _gpu_info,
2451 "memory-bandwidth": _memory_bandwidth,
2452 "tensor-core": _tensor_core,
2453 "gemm": _gemm,
2454 "pytorch": _pytorch,
2455 "flash-attention": _flash_attention,
2456 "flash-attention-ck": _flash_attention_ck,
2457 "vllm": _vllm,
2458 "deepspeed": _deepspeed,
2459 "megatron": _megatron,
2460 "onnx": _onnx,
2461 "llama-cpp": _llama_cpp,
2462}
2463
2464
2465def main():
2466 parser = argparse.ArgumentParser()
2467 parser.add_argument("bench", help=f"One of: {', '.join(BENCHES.keys())}")
2468 parser.add_argument("--json", action="store_true", help="Emit JSON")
2469 args = parser.parse_args()
2470
2471 fn = BENCHES.get(args.bench)
2472 if fn is None:
2473 payload = {
2474 "name": args.bench,
2475 "success": False,
2476 "execution_time_ms": 0,
2477 "metrics": {},
2478 "errors": [f"Unknown benchmark {args.bench}"],
2479 }
2480 print(json.dumps(payload, indent=2))
2481 sys.exit(1)
2482
2483 start = time.perf_counter()
2484 success, metrics, errors = fn()
2485 elapsed_ms = int((time.perf_counter() - start) * 1000)
2486 payload = {
2487 "name": args.bench,
2488 "success": success,
2489 "execution_time_ms": elapsed_ms,
2490 "metrics": metrics,
2491 "errors": errors,
2492 }
2493
2494 # Print a clear marker to help the Rust wrapper find the JSON block
2495 print("\n---BENCHMARK_RESULTS_START---")
2496 print(json.dumps(payload, indent=2))
2497 sys.exit(0 if success else 1)
2498
2499
2500if __name__ == "__main__":
2501 main()
2502"#;
2503
2504#[cfg(test)]
2505mod tests {
2506 use super::*;
2507 use std::sync::Mutex;
2508
2509 static ENV_LOCK: Mutex<()> = Mutex::new(());
2510
2511 #[test]
2512 fn resolve_benchmark_python_uses_mlstack_env_python_before_system_python() {
2513 let _global_env = crate::test_support::lock_env();
2514 let _guard = ENV_LOCK.lock().unwrap();
2515 let old_home = env::var("HOME").ok();
2516 let old_benchmark_python = env::var("MLSTACK_BENCHMARK_PYTHON").ok();
2517 let old_python_bin = env::var("MLSTACK_PYTHON_BIN").ok();
2518 let old_uv_python = env::var("UV_PYTHON").ok();
2519 let home = env::temp_dir().join(format!(
2520 "rusty-stack-benchmark-python-{}",
2521 std::process::id()
2522 ));
2523 let _ = fs::remove_dir_all(&home);
2524 fs::create_dir_all(&home).unwrap();
2525 fs::write(
2526 home.join(".mlstack_env"),
2527 "export MLSTACK_PYTHON_BIN=/tmp/rusty-managed-python\n",
2528 )
2529 .unwrap();
2530 env::set_var("HOME", &home);
2531 env::remove_var("MLSTACK_BENCHMARK_PYTHON");
2532 env::remove_var("MLSTACK_PYTHON_BIN");
2533 env::remove_var("UV_PYTHON");
2534
2535 let resolved = resolve_benchmark_python();
2536
2537 if let Some(home) = old_home {
2538 env::set_var("HOME", home);
2539 } else {
2540 env::remove_var("HOME");
2541 }
2542 if let Some(value) = old_benchmark_python {
2543 env::set_var("MLSTACK_BENCHMARK_PYTHON", value);
2544 } else {
2545 env::remove_var("MLSTACK_BENCHMARK_PYTHON");
2546 }
2547 if let Some(value) = old_python_bin {
2548 env::set_var("MLSTACK_PYTHON_BIN", value);
2549 } else {
2550 env::remove_var("MLSTACK_PYTHON_BIN");
2551 }
2552 if let Some(value) = old_uv_python {
2553 env::set_var("UV_PYTHON", value);
2554 } else {
2555 env::remove_var("UV_PYTHON");
2556 }
2557 let _ = fs::remove_dir_all(&home);
2558
2559 assert_eq!(resolved, "/tmp/rusty-managed-python");
2560 }
2561
2562 #[test]
2563 fn vllm_amdsmi_shim_preserves_real_amdsmi_when_available() {
2564 assert!(PY_HELPER.contains("import amdsmi as _real_amdsmi"));
2565 assert!(PY_HELPER.contains("if _real_amdsmi is not None:"));
2566 }
2567
2568 #[test]
2569 fn onnx_benchmark_uses_public_model_serialization() {
2570 assert!(!PY_HELPER.contains("onnx._serialize"));
2571 assert!(PY_HELPER.contains("model.SerializeToString()"));
2572 }
2573
2574 #[test]
2575 fn onnx_quantized_benchmark_uses_integer_weight_tensor() {
2576 assert!(PY_HELPER.contains("W_quant"));
2577 assert!(!PY_HELPER.contains(r#""MatMulInteger", ["input_quant", "W"]"#));
2578 }
2579
2580 #[test]
2581 fn onnx_benchmark_requires_amd_execution_provider() {
2582 assert!(PY_HELPER.contains(
2583 r#"amd_provider_order = ("MIGraphXExecutionProvider", "ROCMExecutionProvider")"#
2584 ));
2585 assert!(PY_HELPER.contains("onnxruntime import is incomplete"));
2586 assert!(PY_HELPER.contains("ONNX Runtime AMD execution provider unavailable"));
2587 assert!(PY_HELPER.contains("ONNX session did not prioritize AMD provider"));
2588 assert!(PY_HELPER.contains("success = not errors"));
2589 }
2590
2591 #[test]
2592 fn vllm_helper_uses_file_payload_without_env_overrides() {
2593 assert!(PY_HELPER.contains(r#"if __name__ == "__main__":"#));
2594 assert!(PY_HELPER.contains("NamedTemporaryFile"));
2595 assert!(!PY_HELPER.contains("env_overrides"));
2596 assert!(!PY_HELPER.contains("MLSTACK_VLLM_PAYLOAD_JSON"));
2597 assert!(!PY_HELPER.contains(r#"os.environ["VLLM_TARGET_DEVICE"]"#));
2598 assert!(!PY_HELPER.contains(r#"os.environ["HIP_VISIBLE_DEVICES"]"#));
2599 assert!(!PY_HELPER.contains(r#"os.environ["CUDA_VISIBLE_DEVICES"]"#));
2600 assert!(!PY_HELPER.contains(r#"os.environ["VLLM_ENABLE_V1_MULTIPROCESSING"]"#));
2601 }
2602
2603 #[test]
2604 fn vllm_benchmark_disables_v1_multiprocessing_without_env_mutation() {
2605 assert!(PY_HELPER.contains("import vllm.envs as _vllm_envs"));
2606 assert!(PY_HELPER.contains(
2607 r#"_vllm_envs.environment_variables["VLLM_ENABLE_V1_MULTIPROCESSING"] = lambda: False"#
2608 ));
2609 assert!(PY_HELPER.contains(r#""vllm_v1_multiprocessing": False"#));
2610 }
2611
2612 #[test]
2613 fn degraded_benchmarks_report_failure_not_success() {
2614 assert!(PY_HELPER.contains("return False, _degraded_metrics"));
2615 assert!(!PY_HELPER.contains("return True, _degraded_metrics"));
2616 }
2617
2618 #[test]
2619 fn vllm_benchmark_downloads_missing_hf_weights() {
2620 assert!(PY_HELPER.contains("def _ensure_cached_hf_model_weights"));
2621 assert!(PY_HELPER.contains("hf_hub_download"));
2622 assert!(PY_HELPER.contains(r#"filename="model.safetensors""#));
2623 }
2624
2625 #[test]
2626 fn vllm_timeout_reports_subprocess_output_tail() {
2627 assert!(PY_HELPER.contains("except subprocess.TimeoutExpired as exc"));
2628 assert!(PY_HELPER.contains("stdout_tail"));
2629 assert!(PY_HELPER.contains("stderr_tail"));
2630 assert!(PY_HELPER.contains("startup_timed_out"));
2631 assert!(PY_HELPER.contains("vllm_timeout_phase"));
2632 assert!(PY_HELPER.contains("\"event\": \"startup_begin\""));
2633 assert!(PY_HELPER.contains("\"event\": \"startup_complete\""));
2634 }
2635}