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 {
98 let candidate = "/usr/local/bin/python3";
99 let path = Path::new(candidate);
100 if path.exists() {
101 return candidate.to_string();
102 }
103 }
104
105 for candidate in ["python3", "python"] {
106 if Command::new(candidate).arg("--version").output().is_ok() {
107 return candidate.to_string();
108 }
109 }
110
111 "python3".to_string()
112}
113
114fn extract_helper_payload(stdout: &str) -> Option<serde_json::Value> {
115 let marker = "---BENCHMARK_RESULTS_START---";
116 let search = if let Some(pos) = stdout.rfind(marker) {
117 &stdout[pos + marker.len()..]
118 } else {
119 stdout
120 };
121
122 if let Some(start) = search.find('{') {
123 let mut depth = 0usize;
124 let mut in_string = false;
125 let mut escaped = false;
126 for (idx, ch) in search[start..].char_indices() {
127 if in_string {
128 if escaped {
129 escaped = false;
130 continue;
131 }
132 match ch {
133 '\\' => escaped = true,
134 '"' => in_string = false,
135 _ => {}
136 }
137 continue;
138 }
139
140 match ch {
141 '"' => in_string = true,
142 '{' => depth += 1,
143 '}' => {
144 if depth == 0 {
145 return None;
146 }
147 depth -= 1;
148 if depth == 0 {
149 let end = start + idx + 1;
150 return serde_json::from_str::<serde_json::Value>(&search[start..end]).ok();
151 }
152 }
153 _ => {}
154 }
155 }
156 }
157
158 None
159}
160
161fn run_python_benchmark(name: &str) -> BenchmarkResult {
162 let start = Instant::now();
163 let python_bin = resolve_benchmark_python();
164 let helper = match ensure_helper_script() {
165 Ok(path) => path,
166 Err(err) => {
167 return BenchmarkResult {
168 name: name.to_string(),
169 success: false,
170 execution_time_ms: start.elapsed().as_millis(),
171 metrics: serde_json::json!({}),
172 errors: vec![format!("Unable to create benchmark helper script: {}", err)],
173 };
174 }
175 };
176
177 let mut command = Command::new(&python_bin);
178 command.arg(helper).arg(name).arg("--json");
179 if env::var_os("PYTHONHASHSEED").is_none() {
180 command.env("PYTHONHASHSEED", "0");
181 }
182 let output = command.output();
183
184 let parse_payload_result = |parsed: serde_json::Value| BenchmarkResult {
185 name: parsed
186 .get("name")
187 .and_then(|v| v.as_str())
188 .unwrap_or(name)
189 .to_string(),
190 success: parsed
191 .get("success")
192 .and_then(|v| v.as_bool())
193 .unwrap_or(false),
194 execution_time_ms: parsed
195 .get("execution_time_ms")
196 .and_then(|v| v.as_u64())
197 .unwrap_or(start.elapsed().as_millis() as u64) as u128,
198 metrics: parsed.get("metrics").cloned().unwrap_or_default(),
199 errors: parsed
200 .get("errors")
201 .and_then(|v| v.as_array())
202 .map(|arr| {
203 arr.iter()
204 .filter_map(|e| e.as_str().map(|s| s.to_string()))
205 .collect()
206 })
207 .unwrap_or_default(),
208 };
209
210 match output {
211 Ok(out) => {
212 let stdout_str = String::from_utf8_lossy(&out.stdout);
213 if let Some(parsed) = extract_helper_payload(&stdout_str) {
214 let mut result = parse_payload_result(parsed);
215 if !result.success {
216 let stderr = String::from_utf8_lossy(&out.stderr);
217 let stderr_trim = stderr.trim();
218 if !stderr_trim.is_empty() {
219 let stderr_tail = stderr_trim
220 .lines()
221 .rev()
222 .take(24)
223 .collect::<Vec<_>>()
224 .into_iter()
225 .rev()
226 .collect::<Vec<_>>()
227 .join(" | ");
228 if !stderr_tail.is_empty() {
229 result
230 .errors
231 .push(format!("Helper stderr tail: {}", stderr_tail));
232 }
233 }
234 }
235 return result;
236 }
237
238 if out.status.success() {
239 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
240 let parse_error = if stderr.is_empty() {
241 format!("Failed to parse helper JSON output using {}", python_bin)
242 } else {
243 format!(
244 "Failed to parse helper JSON output using {} (stderr: {})",
245 python_bin, stderr
246 )
247 };
248 return BenchmarkResult {
249 name: name.to_string(),
250 success: false,
251 execution_time_ms: start.elapsed().as_millis(),
252 metrics: serde_json::json!({}),
253 errors: vec![parse_error],
254 };
255 }
256
257 BenchmarkResult {
258 name: name.to_string(),
259 success: false,
260 execution_time_ms: start.elapsed().as_millis(),
261 metrics: serde_json::json!({}),
262 errors: {
263 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
264 let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
265 let detail = if !stderr.is_empty() {
266 stderr
267 } else if !stdout.is_empty() {
268 stdout
269 } else {
270 "no output".to_string()
271 };
272 vec![format!(
273 "Helper failed with status {} using {}: {}",
274 out.status, python_bin, detail
275 )]
276 },
277 }
278 }
279 Err(err) => 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![format!("Failed to invoke {}: {}", python_bin, err)],
285 },
286 }
287}
288
289pub fn run_gpu_capability_benchmark() -> BenchmarkResult {
290 run_python_benchmark("gpu-info")
291}
292pub fn run_memory_bandwidth_benchmark() -> BenchmarkResult {
293 run_python_benchmark("memory-bandwidth")
294}
295pub fn run_tensor_core_benchmark() -> BenchmarkResult {
296 run_python_benchmark("tensor-core")
297}
298pub fn run_gemm_benchmark() -> BenchmarkResult {
299 run_python_benchmark("gemm")
300}
301pub fn run_pytorch_benchmark() -> BenchmarkResult {
302 run_python_benchmark("pytorch")
303}
304pub fn run_llama_cpp_benchmark() -> BenchmarkResult {
305 run_python_benchmark("llama-cpp")
306}
307pub fn run_flash_attention_benchmark() -> BenchmarkResult {
308 run_python_benchmark("flash-attention")
309}
310pub fn run_vllm_benchmark() -> BenchmarkResult {
311 run_python_benchmark("vllm")
312}
313pub fn run_deepspeed_benchmark() -> BenchmarkResult {
314 run_python_benchmark("deepspeed")
315}
316pub fn run_megatron_benchmark() -> BenchmarkResult {
317 run_python_benchmark("megatron")
318}
319pub fn run_onnx_benchmark() -> BenchmarkResult {
320 run_python_benchmark("onnx")
321}
322
323const PY_HELPER: &str = r#"
327import argparse
328import json
329import os
330import shutil
331import subprocess
332import sys
333import time
334from glob import glob
335
336_GPU_RUNTIME_CACHE = None
337_DEFAULT_TINY_SAFETENSORS_MODEL = "HuggingFaceTB/SmolLM2-135M-Instruct"
338
339
340def _set_amd_gpu_id_table_env():
341 if os.environ.get("AMDGPU_ASIC_ID_TABLE_PATH"):
342 return
343 candidate = "/usr/share/libdrm/amdgpu.ids"
344 if os.path.isfile(candidate):
345 os.environ["AMDGPU_ASIC_ID_TABLE_PATH"] = candidate
346 os.environ.setdefault("AMDGPU_ASIC_ID_TABLE_PATHS", "/usr/share/libdrm")
347
348
349def _env_or_default(name, default):
350 value = os.environ.get(name)
351 if value is None:
352 return default
353 value = value.strip()
354 return value if value else default
355
356
357def _resolve_gguf_model_path(raw):
358 raw = (raw or "").strip()
359 if not raw:
360 return ""
361 expanded = os.path.expanduser(raw)
362 if os.path.isfile(expanded):
363 return expanded
364 if os.path.isdir(expanded):
365 for entry in sorted(os.listdir(expanded)):
366 if entry.lower().endswith(".gguf"):
367 return os.path.join(expanded, entry)
368 return ""
369
370
371def _find_llama_cpp_binary():
372 candidates = [
373 os.path.expanduser("~/.mlstack/components/llama-cpp/bin/llama-bench"),
374 shutil.which("llama-bench"),
375 ]
376 for candidate in candidates:
377 if candidate and os.path.isfile(candidate) and os.access(candidate, os.X_OK):
378 return candidate
379 return ""
380
381
382def _find_gguf_model():
383 candidates = []
384 candidates.extend(sorted(glob(os.path.expanduser("~/.mlstack/models/*.gguf"))))
385 candidates.extend(sorted(glob(os.path.expanduser("~/.cache/huggingface/hub/*/snapshots/*/*.gguf"))))
386 candidates.extend(sorted(glob(os.path.expanduser("~/.cache/**/*.gguf"), recursive=True)))
387 for candidate in candidates:
388 if os.path.isfile(candidate):
389 return candidate
390 return ""
391
392
393def _detect_llama_cpp_gpus():
394 gpu_names = []
395 try:
396 proc = subprocess.run(
397 ["rocm-smi", "--showproductname"],
398 check=False,
399 capture_output=True,
400 text=True,
401 )
402 output = (proc.stdout or "") + "\n" + (proc.stderr or "")
403 for line in output.splitlines():
404 line = line.strip()
405 if not line:
406 continue
407 lowered = line.lower()
408 if "gfx" in lowered or "amd" in lowered or "radeon" in lowered or "instinct" in lowered:
409 gpu_names.append(line)
410 except Exception:
411 pass
412 if gpu_names:
413 return gpu_names
414
415 try:
416 proc = subprocess.run(
417 "rocminfo | grep gfx",
418 shell=True,
419 check=False,
420 capture_output=True,
421 text=True,
422 )
423 output = (proc.stdout or "") + "\n" + (proc.stderr or "")
424 for line in output.splitlines():
425 line = line.strip()
426 if "gfx" in line.lower():
427 gpu_names.append(line)
428 except Exception:
429 pass
430 return gpu_names
431
432
433def _llama_cpp():
434 start = time.perf_counter()
435 bench = _find_llama_cpp_binary()
436 if not bench:
437 return False, {
438 "name": "llama-cpp",
439 "success": False,
440 "execution_time_ms": 0,
441 "metrics": {},
442 "errors": ["llama-cpp not installed"],
443 }, ["llama-cpp not installed"]
444
445 model = _find_gguf_model()
446 if not model:
447 elapsed = int((time.perf_counter() - start) * 1000)
448 return False, {
449 "name": "llama-cpp",
450 "success": False,
451 "execution_time_ms": elapsed,
452 "metrics": {},
453 "errors": ["no GGUF model found"],
454 }, ["no GGUF model found"]
455
456 gpus = _detect_llama_cpp_gpus()
457 if not gpus:
458 elapsed = int((time.perf_counter() - start) * 1000)
459 return False, {
460 "name": "llama-cpp",
461 "success": False,
462 "execution_time_ms": elapsed,
463 "metrics": {},
464 "errors": ["no ROCm GPUs detected"],
465 }, ["no ROCm GPUs detected"]
466
467 metrics = {}
468 errors = []
469 for gpu_idx, _ in enumerate(gpus):
470 env = os.environ.copy()
471 env["ROCM_VISIBLE_DEVICES"] = str(gpu_idx)
472 try:
473 proc = subprocess.run(
474 [bench, "-m", model, "-p", "512", "-p", "2048", "-p", "8192", "-n", "128", "-o", "json", "-r", "3"],
475 check=False,
476 capture_output=True,
477 text=True,
478 env=env,
479 )
480 raw = (proc.stdout or "").strip()
481 parsed = json.loads(raw) if raw else []
482 if isinstance(parsed, dict):
483 parsed = [parsed]
484 for entry in parsed:
485 if not isinstance(entry, dict):
486 continue
487 n_prompt = int(entry.get("n_prompt", 0) or 0)
488 n_gen = int(entry.get("n_gen", 0) or 0)
489 avg_ts = entry.get("avg_ts")
490 if avg_ts is None:
491 avg_ts = entry.get("avg_tps")
492 stddev_ts = entry.get("stddev_ts", 0.0)
493 prefix = "prefill" if n_prompt > 0 and n_gen == 0 else "decode" if n_prompt == 0 and n_gen > 0 else "other"
494 if prefix == "other" or avg_ts is None:
495 continue
496 context = n_prompt if prefix == "prefill" else n_gen
497 metrics[f"{prefix}_{context}_tps_gpu{gpu_idx}"] = float(avg_ts)
498 metrics[f"{prefix}_{context}_stddev_tps_gpu{gpu_idx}"] = float(stddev_ts or 0.0)
499 except Exception as exc:
500 errors.append(str(exc))
501
502 elapsed = int((time.perf_counter() - start) * 1000)
503 success = bool(metrics) and not errors
504 result = {
505 "name": "llama-cpp",
506 "success": success,
507 "execution_time_ms": elapsed,
508 "metrics": metrics,
509 "errors": errors,
510 }
511 print("---BENCHMARK_RESULTS_START---")
512 print(json.dumps(result, indent=2, sort_keys=True))
513 return success, result, errors
514
515
516def _resolve_vllm_model_candidates():
517 safetensors_model = _env_or_default(
518 "MLSTACK_BENCH_VLLM_SAFETENSORS_MODEL",
519 _DEFAULT_TINY_SAFETENSORS_MODEL,
520 )
521 gguf_hint = os.environ.get("MLSTACK_BENCH_VLLM_GGUF_MODEL_PATH", "")
522 gguf_tokenizer = _env_or_default("MLSTACK_BENCH_VLLM_GGUF_TOKENIZER", safetensors_model)
523 gguf_path = _resolve_gguf_model_path(gguf_hint)
524
525 candidates = []
526 if gguf_path:
527 candidates.append({
528 "format": "gguf",
529 "model": gguf_path,
530 "tokenizer": gguf_tokenizer,
531 })
532 candidates.append({
533 "format": "safetensors",
534 "model": safetensors_model,
535 })
536 return candidates
537
538
539def _probe_gpu_runtime():
540 global _GPU_RUNTIME_CACHE
541 if _GPU_RUNTIME_CACHE is not None:
542 return _GPU_RUNTIME_CACHE
543
544 _set_amd_gpu_id_table_env()
545 torch = _load_torch()
546 if isinstance(torch, tuple):
547 _, errors = torch
548 reason = "; ".join(errors) if errors else "unable to import torch"
549 _GPU_RUNTIME_CACHE = (False, reason, {"available": False})
550 return _GPU_RUNTIME_CACHE
551
552 if not torch.cuda.is_available():
553 _GPU_RUNTIME_CACHE = (False, "torch.cuda is not available", {"available": False})
554 return _GPU_RUNTIME_CACHE
555
556 arch = "unknown"
557 try:
558 props = torch.cuda.get_device_properties(torch.device("cuda:0"))
559 arch = getattr(props, "gcnArchName", "unknown")
560 except Exception:
561 pass
562
563 probe_code = (
564 "import torch;"
565 "x=torch.ones(1, device='cuda', dtype=torch.float32);"
566 "y=x+1;"
567 "torch.cuda.synchronize();"
568 "print(float(y.item()))"
569 )
570 try:
571 probe = subprocess.run(
572 [sys.executable, "-c", probe_code],
573 check=False,
574 capture_output=True,
575 text=True,
576 timeout=20,
577 )
578 if probe.returncode == 0:
579 _GPU_RUNTIME_CACHE = (True, "", {"available": True, "gcn_arch": arch})
580 return _GPU_RUNTIME_CACHE
581
582 detail = (probe.stderr or probe.stdout or "").strip()
583 if not detail:
584 detail = f"exit code {probe.returncode}"
585 if probe.returncode < 0:
586 detail = f"signal {-probe.returncode}: {detail}"
587 reason = f"GPU runtime probe failed on {arch}: {detail}"
588 _GPU_RUNTIME_CACHE = (False, reason, {"available": True, "gcn_arch": arch})
589 return _GPU_RUNTIME_CACHE
590 except subprocess.TimeoutExpired:
591 reason = f"GPU runtime probe timed out on {arch}"
592 _GPU_RUNTIME_CACHE = (False, reason, {"available": True, "gcn_arch": arch})
593 return _GPU_RUNTIME_CACHE
594 except Exception as exc:
595 reason = f"GPU runtime probe error on {arch}: {exc}"
596 _GPU_RUNTIME_CACHE = (False, reason, {"available": True, "gcn_arch": arch})
597 return _GPU_RUNTIME_CACHE
598
599
600def _degraded_metrics(name, reason, extra=None):
601 payload = {
602 "mode": "degraded",
603 "component": name,
604 "gpu_runtime_stable": False,
605 "reason": reason,
606 }
607 if isinstance(extra, dict):
608 payload.update(extra)
609 return payload
610
611
612def _load_torch():
613 try:
614 import torch # noqa: F401
615 return torch
616 except Exception as exc: # pragma: no cover
617 return None, [f"Unable to import torch: {exc}"]
618
619
620def _parse_visible_gpu_indices():
621 for key in ("HIP_VISIBLE_DEVICES", "ROCR_VISIBLE_DEVICES", "CUDA_VISIBLE_DEVICES"):
622 raw = os.environ.get(key, "").strip()
623 if not raw:
624 continue
625
626 values = []
627 valid = True
628 for token in raw.split(","):
629 token = token.strip()
630 if not token:
631 continue
632 if not token.isdigit():
633 valid = False
634 break
635 values.append(int(token))
636
637 if valid and values:
638 return values
639 return []
640
641
642def _coerce_smi_value(entry, key):
643 raw = entry.get(key)
644 if isinstance(raw, dict):
645 value = raw.get("value")
646 if value is None:
647 return None
648 return value
649 return raw
650
651
652def _discrete_hint_from_name(name):
653 if not name:
654 return None
655
656 lowered = str(name).lower()
657 if any(token in lowered for token in ("radeon rx", " rx ", "radeon pro", "instinct", "firepro")):
658 return True
659 if "ryzen" in lowered:
660 return False
661 if "radeon graphics" in lowered and "rx" not in lowered and "pro" not in lowered:
662 return False
663 if "integrated" in lowered or "igpu" in lowered or "apu" in lowered:
664 return False
665 return None
666
667
668def _is_integrated_name(name):
669 hint = _discrete_hint_from_name(name)
670 if hint is not None:
671 return not hint
672 n = str(name or "")
673 lowered = n.lower()
674 up = n.upper()
675 # Token set GENERATED from crate::gpu::INTEGRATED_NAME_PATTERNS at write
676 # time (single source — no hand-maintained copy that can drift from Rust).
677 for token in __INTEGRATED_TOKENS__:
678 if token in lowered:
679 return True
680 # "Ryzen" without "RX" => APU/iGPU (discrete cards never contain "Ryzen").
681 if "RYZEN" in up and "RX" not in up:
682 return True
683 return False
684
685
686def _gpu_info():
687 torch = _load_torch()
688 if isinstance(torch, tuple):
689 _, errors = torch
690 return False, {}, errors
691 if not torch.cuda.is_available():
692 return False, {}, ["torch.cuda is not available"]
693
694 visible_indices = _parse_visible_gpu_indices()
695 smi_available = shutil.which("rocm-smi") is not None
696 devices = []
697 for idx in range(torch.cuda.device_count()):
698 global_idx = visible_indices[idx] if idx < len(visible_indices) else idx
699 device = torch.device(f"cuda:{idx}")
700 props = torch.cuda.get_device_properties(device)
701 info = {
702 "index": global_idx,
703 "local_index": idx,
704 "gpu_model": getattr(props, "name", "Unknown"),
705 "gcn_arch": getattr(props, "gcnArchName", "unknown"),
706 "vram_gb": round(getattr(props, "total_memory", 0) / 1e9, 2),
707 "compute_units": getattr(props, "multi_processor_count", 0),
708 "max_clock_mhz": getattr(props, "max_frequency", 0) / 1e6,
709 "tensor_cores": bool(getattr(props, "gcnArchName", "").startswith("gfx11")),
710 }
711
712 smi_discrete_hint = None
713 if smi_available:
714 try:
715 smi = subprocess.run(
716 [
717 "rocm-smi",
718 "--showproductname",
719 "--showtemp",
720 "--showpower",
721 "--showuse",
722 "--showclocks",
723 "--json",
724 "-d",
725 str(global_idx),
726 ],
727 check=False,
728 capture_output=True,
729 text=True,
730 )
731 if smi.returncode == 0 and smi.stdout.strip().startswith("{"):
732 data = json.loads(smi.stdout)
733 first_key = next(iter(data.keys()))
734 entry = data.get(first_key, {})
735
736 smi_name = (
737 _coerce_smi_value(entry, "Card Series")
738 or _coerce_smi_value(entry, "Card series")
739 or _coerce_smi_value(entry, "Card Model")
740 or _coerce_smi_value(entry, "Card model")
741 )
742 if smi_name:
743 info["gpu_model"] = str(smi_name).strip()
744 smi_discrete_hint = _discrete_hint_from_name(smi_name)
745
746 temp = entry.get("Temperature (Sensor die)", {}).get("value")
747 power = entry.get("Average Graphics Package Power", {}).get("value")
748 if temp is not None:
749 info["temperature_c"] = float(temp)
750 if power is not None:
751 info["power_watts"] = float(power)
752
753 # capture additional metrics requested
754 util = entry.get("GPU use (%)", {}).get("value")
755 if util is not None:
756 info["utilization_percent"] = float(util)
757
758 mem_util = entry.get("GPU memory use (%)", {}).get("value")
759 if mem_util is not None:
760 info["memory_percent"] = float(mem_util)
761
762 sclk = entry.get("GFX Clock (MHz)", {}).get("value")
763 if sclk is not None:
764 info["sclk_mhz"] = int(float(sclk))
765
766 mclk = entry.get("Memory Clock (MHz)", {}).get("value")
767 if mclk is not None:
768 info["mclk_mhz"] = int(float(mclk))
769 except Exception:
770 pass
771
772 if smi_discrete_hint is False:
773 continue
774 if smi_discrete_hint is None and _is_integrated_name(info.get("gpu_model")):
775 continue
776 devices.append(info)
777
778 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
779 metrics = {"gpus": devices}
780 metrics.update({"gpu_runtime_stable": probe_ok})
781 if isinstance(probe_meta, dict):
782 metrics.update({f"probe_{k}": v for k, v in probe_meta.items()})
783 if probe_ok:
784 return True, metrics, []
785 return True, metrics, [probe_reason]
786
787
788def _time_fn(fn, warmup=1, repeat=5):
789 import torch
790
791 torch.cuda.synchronize()
792 for _ in range(warmup):
793 fn()
794 times = []
795 for _ in range(repeat):
796 torch.cuda.synchronize()
797 start = time.perf_counter()
798 fn()
799 torch.cuda.synchronize()
800 times.append(time.perf_counter() - start)
801 return sum(times) / len(times)
802
803
804def _memory_bandwidth():
805 torch = _load_torch()
806 if isinstance(torch, tuple):
807 _, errors = torch
808 return False, {}, errors
809 if not torch.cuda.is_available():
810 return False, {}, ["torch.cuda not available"]
811 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
812 if not probe_ok:
813 extra = {
814 "hbm_peak_gb_s": 0.0,
815 "system_peak_gb_s": 0.0,
816 "hbm_ratio": 0.0,
817 "hbm_samples_gbps": [],
818 "system_samples_gbps": [],
819 }
820 if isinstance(probe_meta, dict):
821 extra.update(probe_meta)
822 return True, _degraded_metrics("memory-bandwidth", probe_reason, extra), [probe_reason]
823
824 device = torch.device("cuda:0")
825 sizes_mb = [64, 128, 256, 512]
826 hbm_samples = []
827 system_samples = []
828
829 for size_mb in sizes_mb:
830 numel = size_mb * 1024 * 1024 // 4
831 a = torch.empty(numel, device=device, dtype=torch.float32)
832 def op():
833 a.add_(1.0)
834 t = _time_fn(op, warmup=1, repeat=4)
835 bytes_moved = a.numel() * a.element_size() * 2 # read + write
836 hbm_samples.append(bytes_moved / t / 1e9)
837
838 host = torch.empty_like(a.cpu())
839 def h2d():
840 _ = host.to(device, non_blocking=True)
841 t_h2d = _time_fn(h2d, warmup=1, repeat=3)
842 system_samples.append(bytes_moved / t_h2d / 1e9 if t_h2d > 0 else 0)
843
844 hbm_peak = max(hbm_samples) if hbm_samples else 0
845 sys_peak = max(system_samples) if system_samples else 0
846 hbm_ratio = hbm_peak / sys_peak if sys_peak > 0 else 0
847
848 return True, {
849 "hbm_peak_gb_s": round(hbm_peak, 2),
850 "system_peak_gb_s": round(sys_peak, 2),
851 "hbm_ratio": round(hbm_ratio, 2),
852 "hbm_samples_gbps": [round(x, 2) for x in hbm_samples],
853 "system_samples_gbps": [round(x, 2) for x in system_samples],
854 }, []
855
856
857def _tensor_core():
858 torch = _load_torch()
859 if isinstance(torch, tuple):
860 _, errors = torch
861 return False, {}, errors
862 if not torch.cuda.is_available():
863 return False, {}, ["torch.cuda not available"]
864 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
865 if not probe_ok:
866 extra = {
867 "fp16_tflops": 0.0,
868 "bf16_tflops": 0.0,
869 "tf32_tflops": 0.0,
870 "fp32_tflops": 0.0,
871 "fp16_samples_tflops": [],
872 }
873 if isinstance(probe_meta, dict):
874 extra.update(probe_meta)
875 return True, _degraded_metrics("tensor-core", probe_reason, extra), [probe_reason]
876
877 device = torch.device("cuda:0")
878 sizes = [512, 1024, 2048]
879
880 results = {}
881
882 # FP16
883 fp16_samples = []
884 for n in sizes:
885 a = torch.randn((n, n), device=device, dtype=torch.float16)
886 b = torch.randn((n, n), device=device, dtype=torch.float16)
887 t = _time_fn(lambda: torch.matmul(a, b), warmup=2, repeat=3)
888 flops = 2 * n * n * n
889 fp16_samples.append(flops / t / 1e12)
890 results["fp16_tflops"] = round(max(fp16_samples), 2)
891 results["fp16_samples"] = [round(x, 2) for x in fp16_samples]
892
893 # BF16
894 bf16_samples = []
895 try:
896 for n in sizes:
897 a = torch.randn((n, n), device=device, dtype=torch.bfloat16)
898 b = torch.randn((n, n), device=device, dtype=torch.bfloat16)
899 t = _time_fn(lambda: torch.matmul(a, b), warmup=2, repeat=3)
900 flops = 2 * n * n * n
901 bf16_samples.append(flops / t / 1e12)
902 results["bf16_tflops"] = round(max(bf16_samples), 2)
903 except Exception:
904 results["bf16_tflops"] = 0.0
905
906 # FP32
907 fp32_samples = []
908 for n in sizes:
909 a = torch.randn((n, n), device=device, dtype=torch.float32)
910 b = torch.randn((n, n), device=device, dtype=torch.float32)
911 t = _time_fn(lambda: torch.matmul(a, b), warmup=2, repeat=3)
912 flops = 2 * n * n * n
913 fp32_samples.append(flops / t / 1e12)
914 results["fp32_tflops"] = round(max(fp32_samples), 2)
915
916 # TF32 is NVIDIA specific, on AMD we'll report 0 or skip
917 results["tf32_tflops"] = 0.0
918
919 return True, {
920 "fp16_tflops": results["fp16_tflops"],
921 "bf16_tflops": results["bf16_tflops"],
922 "tf32_tflops": results["tf32_tflops"],
923 "fp32_tflops": results["fp32_tflops"],
924 "fp16_samples_tflops": results["fp16_samples"],
925 }, []
926
927
928def _gemm():
929 torch = _load_torch()
930 if isinstance(torch, tuple):
931 _, errors = torch
932 return False, {}, errors
933 if not torch.cuda.is_available():
934 return False, {}, ["torch.cuda not available"]
935 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
936 if not probe_ok:
937 extra = {
938 "fp16_peak_gflops": 0.0,
939 "fp16_samples_gflops": [],
940 }
941 if isinstance(probe_meta, dict):
942 extra.update(probe_meta)
943 return True, _degraded_metrics("gemm", probe_reason, extra), [probe_reason]
944
945 device = torch.device("cuda:0")
946 shapes = [(1024, 1024, 1024), (1536, 1536, 1536), (2048, 2048, 2048)]
947 samples = []
948 for m, k, n in shapes:
949 a = torch.randn((m, k), device=device, dtype=torch.float16)
950 b = torch.randn((k, n), device=device, dtype=torch.float16)
951 def op():
952 return torch.matmul(a, b)
953 t = _time_fn(op, warmup=1, repeat=3)
954 flops = 2 * m * n * k
955 samples.append(flops / t / 1e9)
956 peak = max(samples) if samples else 0
957 return True, {
958 "fp16_peak_gflops": round(peak, 2),
959 "fp16_samples_gflops": [round(x, 2) for x in samples],
960 }, []
961
962
963def _pytorch():
964 torch = _load_torch()
965 if isinstance(torch, tuple):
966 _, errors = torch
967 return False, {}, errors
968 if not torch.cuda.is_available():
969 return False, {}, ["torch.cuda not available"]
970 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
971 if not probe_ok:
972 extra = {
973 "gemm_gflops": 0.0,
974 "convolution_gflops": 0.0,
975 "gemm_samples_gflops": [],
976 "conv_samples_gflops": [],
977 "autograd_overhead_percent": 0.0,
978 }
979 if isinstance(probe_meta, dict):
980 extra.update(probe_meta)
981 return True, _degraded_metrics("pytorch", probe_reason, extra), [probe_reason]
982
983 device = torch.device("cuda:0")
984 m = k = n = 1024
985 a = torch.randn((m, k), device=device, dtype=torch.float32, requires_grad=True)
986 b = torch.randn((k, n), device=device, dtype=torch.float32, requires_grad=True)
987 gemm_samples = []
988 for _ in range(3):
989 gemm_t = _time_fn(lambda: torch.matmul(a, b), warmup=1, repeat=1)
990 gemm_samples.append(2 * m * n * k / gemm_t / 1e9)
991 gemm_peak = max(gemm_samples) if gemm_samples else 0
992
993 conv = torch.nn.Conv2d(64, 64, kernel_size=3, padding=1).to(device)
994 x = torch.randn((32, 64, 64, 64), device=device)
995 conv_samples = []
996 for _ in range(3):
997 conv_t = _time_fn(lambda: conv(x), warmup=1, repeat=1)
998 conv_ops = 2 * 32 * 64 * 64 * 64 * 64 * 3 * 3
999 conv_samples.append(conv_ops / conv_t / 1e9)
1000 conv_peak = max(conv_samples) if conv_samples else 0
1001
1002 y = a @ b
1003 y.sum().backward()
1004 torch.cuda.synchronize()
1005 start = time.perf_counter()
1006 y = a @ b
1007 loss = y.sum()
1008 loss.backward()
1009 torch.cuda.synchronize()
1010 full = time.perf_counter() - start
1011
1012 start = time.perf_counter()
1013 _ = a @ b
1014 torch.cuda.synchronize()
1015 fwd = time.perf_counter() - start
1016 overhead = (full - fwd) / full * 100 if full > 0 else 0
1017
1018 return True, {
1019 "gemm_gflops": round(gemm_peak, 2),
1020 "convolution_gflops": round(conv_peak, 2),
1021 "gemm_samples_gflops": [round(x, 2) for x in gemm_samples],
1022 "conv_samples_gflops": [round(x, 2) for x in conv_samples],
1023 "autograd_overhead_percent": round(overhead, 2),
1024 }, []
1025
1026
1027def _flash_attention():
1028 torch = _load_torch()
1029 if isinstance(torch, tuple):
1030 _, errors = torch
1031 return False, {}, errors
1032 if not torch.cuda.is_available():
1033 return False, {}, ["torch.cuda not available"]
1034 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
1035 if not probe_ok:
1036 extra = {
1037 "standard_attention_speed": 0.0,
1038 "flash_attention_speed": 0.0,
1039 "standard_samples_tok_s": [],
1040 "flash_samples_tok_s": [],
1041 "speedup": 0.0,
1042 "memory_savings_gb": 0.0,
1043 }
1044 if isinstance(probe_meta, dict):
1045 extra.update(probe_meta)
1046 return True, _degraded_metrics("flash-attention", probe_reason, extra), [probe_reason]
1047
1048 try:
1049 from torch.backends.cuda import sdp_kernel
1050 except Exception as exc: # pragma: no cover
1051 return False, {}, [f"flash attention control unavailable: {exc}"]
1052
1053 device = torch.device("cuda:0")
1054 seqs = [128, 256, 512, 1024]
1055 bsz, heads, dim = 8, 8, 64
1056 flash_samples = []
1057 std_samples = []
1058 mem_std = 0
1059 mem_flash = 0
1060
1061 for seqlen in seqs:
1062 q = torch.randn((bsz, heads, seqlen, dim), device=device, dtype=torch.float16)
1063 k = torch.randn_like(q)
1064 v = torch.randn_like(q)
1065
1066 def run_flash():
1067 with sdp_kernel(enable_flash=True, enable_math=True, enable_mem_efficient=True):
1068 return torch.nn.functional.scaled_dot_product_attention(q, k, v, is_causal=False)
1069
1070 def run_standard():
1071 with sdp_kernel(enable_flash=False, enable_math=True, enable_mem_efficient=False):
1072 return torch.nn.functional.scaled_dot_product_attention(q, k, v, is_causal=False)
1073
1074 torch.cuda.reset_peak_memory_stats()
1075 _ = run_standard()
1076 mem_std = max(mem_std, torch.cuda.max_memory_allocated() / 1e9)
1077
1078 torch.cuda.reset_peak_memory_stats()
1079 _ = run_flash()
1080 mem_flash = max(mem_flash, torch.cuda.max_memory_allocated() / 1e9)
1081
1082 flash_t = _time_fn(run_flash, warmup=1, repeat=2)
1083 std_t = _time_fn(run_standard, warmup=1, repeat=2)
1084
1085 tokens = bsz * seqlen
1086 flash_samples.append(tokens / flash_t)
1087 std_samples.append(tokens / std_t)
1088
1089 flash_peak = max(flash_samples) if flash_samples else 0
1090 std_peak = max(std_samples) if std_samples else 0
1091
1092 return True, {
1093 "standard_attention_speed": round(std_peak, 2),
1094 "flash_attention_speed": round(flash_peak, 2),
1095 "standard_samples_tok_s": [round(x, 2) for x in std_samples],
1096 "flash_samples_tok_s": [round(x, 2) for x in flash_samples],
1097 "speedup": round(flash_peak / std_peak, 3) if std_peak > 0 else 0,
1098 "memory_savings_gb": round(max(0, mem_std - mem_flash), 4),
1099 }, []
1100
1101
1102def _vllm():
1103 # Some shell setups can propagate an empty/whitespace-only target device.
1104 # vLLM rejects empty device strings, so normalize aggressively before import.
1105 target_device = _env_or_default("VLLM_TARGET_DEVICE", "rocm")
1106 if target_device not in {"rocm", "cuda", "cpu"}:
1107 target_device = "rocm"
1108 os.environ["VLLM_TARGET_DEVICE"] = target_device
1109
1110 def _ensure_writable_triton_cache_env():
1111 import tempfile
1112
1113 def _writable_dir(path):
1114 if not path:
1115 return False
1116 try:
1117 os.makedirs(path, exist_ok=True)
1118 probe = os.path.join(path, f".mlstack_probe_{os.getpid()}")
1119 with open(probe, "w", encoding="utf-8") as fp:
1120 fp.write("ok")
1121 os.remove(probe)
1122 return True
1123 except Exception:
1124 return False
1125
1126 triton_cache = (os.environ.get("TRITON_CACHE_DIR") or "").strip()
1127 if triton_cache and _writable_dir(triton_cache):
1128 triton_home = (os.environ.get("TRITON_HOME") or os.path.dirname(triton_cache)).strip()
1129 os.environ.setdefault("TRITON_HOME", triton_home or os.path.expanduser("~/.cache/mlstack/triton"))
1130 os.environ.setdefault("TRITON_DUMP_DIR", os.path.join(os.environ["TRITON_HOME"], "dump"))
1131 os.environ.setdefault("TRITON_OVERRIDE_DIR", os.path.join(os.environ["TRITON_HOME"], "override"))
1132 _writable_dir(os.environ.get("TRITON_DUMP_DIR", ""))
1133 _writable_dir(os.environ.get("TRITON_OVERRIDE_DIR", ""))
1134 return
1135
1136 home = os.path.expanduser("~")
1137 candidates = [
1138 os.environ.get("MLSTACK_TRITON_HOME", "").strip(),
1139 os.environ.get("TRITON_HOME", "").strip(),
1140 os.path.join(home, ".cache", "mlstack", "triton"),
1141 os.path.join(tempfile.gettempdir(), f"mlstack-triton-{os.getuid()}"),
1142 ]
1143 for root in candidates:
1144 if not root:
1145 continue
1146 cache_dir = os.path.join(root, "cache")
1147 dump_dir = os.path.join(root, "dump")
1148 override_dir = os.path.join(root, "override")
1149 if _writable_dir(cache_dir) and _writable_dir(dump_dir) and _writable_dir(override_dir):
1150 os.environ["MLSTACK_TRITON_HOME"] = root
1151 os.environ["TRITON_HOME"] = root
1152 os.environ["TRITON_CACHE_DIR"] = cache_dir
1153 os.environ["TRITON_DUMP_DIR"] = dump_dir
1154 os.environ["TRITON_OVERRIDE_DIR"] = override_dir
1155 return
1156
1157 _ensure_writable_triton_cache_env()
1158
1159 def _normalize_visible_devices():
1160 visible = os.environ.get("HIP_VISIBLE_DEVICES") or os.environ.get("CUDA_VISIBLE_DEVICES") or ""
1161 candidates = []
1162 seen = set()
1163 for token in visible.split(","):
1164 item = token.strip()
1165 if not item.isdigit() or item in seen:
1166 continue
1167 seen.add(item)
1168 candidates.append(item)
1169 if not candidates:
1170 candidates = ["0"]
1171
1172 normalized = ",".join(candidates)
1173 primary = candidates[0]
1174 os.environ["HIP_VISIBLE_DEVICES"] = normalized
1175 os.environ["CUDA_VISIBLE_DEVICES"] = normalized
1176 if not str(os.environ.get("PYTORCH_ROCM_DEVICE", "")).strip():
1177 os.environ["PYTORCH_ROCM_DEVICE"] = primary
1178 return normalized, primary
1179
1180 # Preserve the full discrete-GPU set. Do not collapse to a single GPU.
1181 visible_devices, primary_visible = _normalize_visible_devices()
1182 if target_device == "rocm":
1183 os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
1184
1185 candidates = _resolve_vllm_model_candidates()
1186 candidate_names = [f"{c.get('format')}:{c.get('model')}" for c in candidates]
1187 torch = _load_torch()
1188 if isinstance(torch, tuple):
1189 _, errors = torch
1190 reason = "; ".join(errors) if errors else "unable to import torch"
1191 return True, _degraded_metrics("vllm", reason, {
1192 "model": "vLLM (Unavailable)",
1193 "model_format": "unavailable",
1194 "throughput_tokens_per_sec": 0.0,
1195 "latency_ms": 0.0,
1196 "throughput_samples": [],
1197 "target_device": target_device,
1198 "candidate_models": candidate_names,
1199 }), [reason]
1200 runtime_probe_hint = ""
1201 if target_device in {"rocm", "cuda"}:
1202 try:
1203 if not torch.cuda.is_available():
1204 runtime_probe_hint = "ROCm torch reports no available HIP GPUs in benchmark environment"
1205 else:
1206 torch.cuda.get_device_properties(0)
1207 except Exception as exc:
1208 runtime_probe_hint = f"ROCm torch GPU initialization failed before vLLM load: {exc}"
1209 def _patch_amdsmi_for_vllm_arch_probe():
1210 try:
1211 import sys
1212 import types
1213
1214 if os.environ.get("MLSTACK_VLLM_DISABLE_AMDSMI_SHIM", "").strip() in {"1", "true", "TRUE"}:
1215 return
1216
1217 arch_hint = (
1218 os.environ.get("GPU_ARCH")
1219 or os.environ.get("PYTORCH_ROCM_ARCH")
1220 or os.environ.get("HSA_OVERRIDE_GFX_VERSION")
1221 or ""
1222 ).strip()
1223 if not arch_hint:
1224 return
1225 if not arch_hint.startswith("gfx") and "." in arch_hint:
1226 parts = [p for p in arch_hint.split(".") if p]
1227 if len(parts) >= 2:
1228 arch_hint = f"gfx{parts[0]}{parts[1]}"
1229
1230 shim = types.ModuleType("amdsmi")
1231
1232 class AmdSmiException(Exception):
1233 pass
1234
1235 def amdsmi_init():
1236 return None
1237
1238 def amdsmi_shut_down():
1239 return None
1240
1241 def amdsmi_get_processor_handles():
1242 return [0]
1243
1244 def amdsmi_get_gpu_asic_info(_handle):
1245 return {"target_graphics_version": arch_hint}
1246
1247 def amdsmi_topo_get_link_type(*_args, **_kwargs):
1248 return (0, 0)
1249
1250 shim.AmdSmiException = AmdSmiException
1251 shim.amdsmi_init = amdsmi_init
1252 shim.amdsmi_shut_down = amdsmi_shut_down
1253 shim.amdsmi_get_processor_handles = amdsmi_get_processor_handles
1254 shim.amdsmi_get_gpu_asic_info = amdsmi_get_gpu_asic_info
1255 shim.amdsmi_topo_get_link_type = amdsmi_topo_get_link_type
1256
1257 sys.modules["amdsmi"] = shim
1258 except Exception:
1259 # Best-effort compatibility shim.
1260 pass
1261
1262 _patch_amdsmi_for_vllm_arch_probe()
1263 try:
1264 import vllm
1265 from vllm import LLM, SamplingParams
1266 import logging
1267 import importlib
1268 # Suppress vLLM and related logging to avoid breaking JSON parsing
1269 logging.getLogger("vllm").setLevel(logging.ERROR)
1270 except Exception as exc:
1271 err = f"vLLM not available: {exc}"
1272 return True, _degraded_metrics("vllm", err, {
1273 "model": "vLLM (Unavailable)",
1274 "model_format": "unavailable",
1275 "throughput_tokens_per_sec": 0.0,
1276 "latency_ms": 0.0,
1277 "throughput_samples": [],
1278 "target_device": target_device,
1279 "candidate_models": candidate_names,
1280 }), [err]
1281 abi_errors = []
1282 for ext_name in ("vllm._C", "vllm._rocm_C"):
1283 try:
1284 importlib.import_module(ext_name)
1285 except Exception as exc:
1286 abi_errors.append(f"{ext_name} import failed: {exc}")
1287 if len(abi_errors) == 2:
1288 reason = "vLLM ROCm native extensions failed to load; " + "; ".join(abi_errors)
1289 return True, _degraded_metrics("vllm", reason, {
1290 "model": "vLLM (Unavailable)",
1291 "model_format": "unavailable",
1292 "throughput_tokens_per_sec": 0.0,
1293 "latency_ms": 0.0,
1294 "throughput_samples": [],
1295 "target_device": target_device,
1296 "candidate_models": candidate_names,
1297 }), [reason]
1298 platform_device_type = ""
1299 platform_class = "unknown"
1300 try:
1301 from vllm.platforms import current_platform
1302 platform_device_type = str(getattr(current_platform, "device_type", "") or "").strip()
1303 platform_class = current_platform.__class__.__name__
1304 except Exception:
1305 pass
1306 if not platform_device_type:
1307 platform_reason = (
1308 "vLLM platform detection returned an empty device_type (UnspecifiedPlatform). "
1309 "This commonly means amdsmi is missing, so ROCm platform detection failed. "
1310 "Install amdsmi in the benchmark/runtime Python environment and rerun."
1311 )
1312 return True, _degraded_metrics("vllm", platform_reason, {
1313 "model": "vLLM (Detected)",
1314 "model_format": "unavailable",
1315 "throughput_tokens_per_sec": 0.0,
1316 "latency_ms": 0.0,
1317 "throughput_samples": [],
1318 "target_device": target_device,
1319 "candidate_models": candidate_names,
1320 "vllm_platform_class": platform_class,
1321 "vllm_platform_device_type": platform_device_type,
1322 }), [platform_reason]
1323 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
1324 if not probe_ok:
1325 runtime_probe_hint = probe_reason if not runtime_probe_hint else f"{runtime_probe_hint}; {probe_reason}"
1326
1327 prompts = [
1328 "The future of AI is",
1329 "ROCm performance on AMD is",
1330 "vLLM is a fast",
1331 ]
1332 max_tokens = 24
1333 attempt_errors = []
1334
1335 def _run_vllm_attempt_subprocess(llm_kwargs, prompts, max_tokens, env_overrides=None):
1336 payload = {
1337 "llm_kwargs": llm_kwargs,
1338 "prompts": prompts,
1339 "max_tokens": int(max_tokens),
1340 }
1341 runner = r"""
1342import json
1343import logging
1344import os
1345import sys
1346import time
1347import traceback
1348
1349def _emit(obj):
1350 print(json.dumps(obj))
1351
1352try:
1353 payload = json.loads(sys.stdin.read())
1354 llm_kwargs = payload.get("llm_kwargs", {})
1355 prompts = payload.get("prompts", [])
1356 max_tokens = int(payload.get("max_tokens", 24))
1357 try:
1358 import types
1359 if os.environ.get("MLSTACK_VLLM_DISABLE_AMDSMI_SHIM", "").strip() not in {"1", "true", "TRUE"}:
1360 arch_hint = (
1361 os.environ.get("GPU_ARCH")
1362 or os.environ.get("PYTORCH_ROCM_ARCH")
1363 or os.environ.get("HSA_OVERRIDE_GFX_VERSION")
1364 or ""
1365 ).strip()
1366 if arch_hint and not arch_hint.startswith("gfx") and "." in arch_hint:
1367 parts = [p for p in arch_hint.split(".") if p]
1368 if len(parts) >= 2:
1369 arch_hint = f"gfx{parts[0]}{parts[1]}"
1370 if arch_hint:
1371 shim = types.ModuleType("amdsmi")
1372 class AmdSmiException(Exception):
1373 pass
1374 def amdsmi_init():
1375 return None
1376 def amdsmi_shut_down():
1377 return None
1378 def amdsmi_get_processor_handles():
1379 return [0]
1380 def amdsmi_get_gpu_asic_info(_handle):
1381 return {"target_graphics_version": arch_hint}
1382 def amdsmi_topo_get_link_type(*_args, **_kwargs):
1383 return (0, 0)
1384 shim.AmdSmiException = AmdSmiException
1385 shim.amdsmi_init = amdsmi_init
1386 shim.amdsmi_shut_down = amdsmi_shut_down
1387 shim.amdsmi_get_processor_handles = amdsmi_get_processor_handles
1388 shim.amdsmi_get_gpu_asic_info = amdsmi_get_gpu_asic_info
1389 shim.amdsmi_topo_get_link_type = amdsmi_topo_get_link_type
1390 sys.modules["amdsmi"] = shim
1391 except Exception:
1392 pass
1393 import vllm
1394 from vllm import LLM, SamplingParams
1395 logging.getLogger("vllm").setLevel(logging.ERROR)
1396 sampling_params = SamplingParams(temperature=0.0, max_tokens=max_tokens)
1397 llm = LLM(**llm_kwargs)
1398 start = time.perf_counter()
1399 outputs = llm.generate(prompts, sampling_params)
1400 elapsed = time.perf_counter() - start
1401
1402 throughput_samples = []
1403 for prompt in prompts:
1404 sample_start = time.perf_counter()
1405 sample_out = llm.generate([prompt], sampling_params)
1406 sample_elapsed = time.perf_counter() - sample_start
1407 tokens = len(sample_out[0].outputs[0].token_ids)
1408 throughput_samples.append(tokens / sample_elapsed if sample_elapsed > 0 else 0.0)
1409
1410 total_tokens = sum(len(output.outputs[0].token_ids) for output in outputs)
1411 throughput = total_tokens / elapsed if elapsed > 0 else 0.0
1412 _emit({
1413 "ok": True,
1414 "throughput_tokens_per_sec": round(throughput, 2),
1415 "latency_ms": round((elapsed / max(len(prompts), 1)) * 1000, 2),
1416 "throughput_samples": [round(x, 2) for x in throughput_samples],
1417 })
1418except Exception as exc:
1419 tb = traceback.format_exc().strip()
1420 tail_lines = [line.strip() for line in tb.splitlines()[-12:] if line.strip()]
1421 msg = str(exc).strip() or repr(exc)
1422 if tail_lines:
1423 msg = f"{msg} :: traceback_tail: {' | '.join(tail_lines)}"
1424 _emit({"ok": False, "error": msg})
1425"""
1426 env = os.environ.copy()
1427 if isinstance(env_overrides, dict):
1428 for key, value in env_overrides.items():
1429 if value is None:
1430 env.pop(key, None)
1431 else:
1432 env[key] = str(value)
1433 try:
1434 proc = subprocess.run(
1435 [sys.executable, "-c", runner],
1436 input=json.dumps(payload),
1437 capture_output=True,
1438 text=True,
1439 timeout=240,
1440 env=env,
1441 )
1442 except Exception as exc:
1443 return {"ok": False, "error": f"subprocess launch failed: {exc}"}
1444
1445 merged = (proc.stdout or "").splitlines()
1446 if proc.stderr:
1447 merged.extend(proc.stderr.splitlines())
1448 for line in reversed(merged):
1449 text = line.strip()
1450 if not text.startswith("{") or not text.endswith("}"):
1451 continue
1452 try:
1453 parsed = json.loads(text)
1454 except Exception:
1455 continue
1456 if isinstance(parsed, dict) and "ok" in parsed:
1457 if not parsed.get("ok", False):
1458 log_tail = []
1459 for raw in merged:
1460 msg = raw.strip()
1461 if not msg:
1462 continue
1463 if msg.startswith("{") and msg.endswith("}"):
1464 continue
1465 lower = msg.lower()
1466 if any(
1467 marker in lower
1468 for marker in (
1469 "error",
1470 "exception",
1471 "traceback",
1472 "runtimeerror",
1473 "valueerror",
1474 "fatal",
1475 "enginecore",
1476 "failed",
1477 )
1478 ):
1479 log_tail.append(msg)
1480 if log_tail:
1481 tail = " | ".join(log_tail[-12:])[:3000]
1482 base = str(parsed.get("error") or "unknown vLLM subprocess failure")
1483 if tail and tail not in base:
1484 parsed["error"] = f"{base}; engine_log_tail: {tail}"
1485 return parsed
1486
1487 detail = " ".join([line.strip() for line in merged if line.strip()])[:4000]
1488 if not detail:
1489 detail = f"exit code {proc.returncode}"
1490 return {"ok": False, "error": detail}
1491
1492 for candidate in candidates:
1493 model_name = candidate.get("model")
1494 model_format = candidate.get("format", "unknown")
1495 if not model_name:
1496 continue
1497
1498 llm_kwargs = {
1499 "model": model_name,
1500 "trust_remote_code": True,
1501 "enforce_eager": True,
1502 "gpu_memory_utilization": 0.65,
1503 "disable_log_stats": True,
1504 "tensor_parallel_size": 1,
1505 "dtype": "float16",
1506 }
1507 if model_format == "gguf":
1508 tokenizer_name = candidate.get("tokenizer")
1509 if tokenizer_name:
1510 llm_kwargs["tokenizer"] = tokenizer_name
1511
1512 attempt = _run_vllm_attempt_subprocess(llm_kwargs, prompts, max_tokens)
1513 if attempt.get("ok"):
1514 return True, {
1515 "model": model_name,
1516 "model_format": model_format,
1517 "target_device": target_device,
1518 "visible_devices": os.environ.get("HIP_VISIBLE_DEVICES", primary_visible),
1519 "throughput_tokens_per_sec": float(attempt.get("throughput_tokens_per_sec", 0.0)),
1520 "latency_ms": float(attempt.get("latency_ms", 0.0)),
1521 "throughput_samples": [
1522 float(x) for x in (attempt.get("throughput_samples") or [])
1523 ],
1524 "candidate_models": candidate_names,
1525 }, []
1526 err_msg = str(attempt.get("error") or "unknown vLLM execution error")
1527 engine_core_failure = (
1528 "Engine core initialization failed" in err_msg
1529 or "Failed core proc" in err_msg
1530 or "No HIP GPUs are available" in err_msg
1531 or "torch.cuda is not available" in err_msg
1532 )
1533 # Defensive fallback for intermittent vLLM device/runtime failures.
1534 # Preserve full visible GPU list for multi-GPU hosts.
1535 if "Device string must not be empty" in err_msg or engine_core_failure:
1536 retry_profiles = [
1537 (
1538 "spawn-v1-default",
1539 {
1540 "VLLM_WORKER_MULTIPROC_METHOD": "spawn",
1541 "VLLM_ENABLE_V1_MULTIPROCESSING": "1",
1542 "VLLM_USE_V1": "1",
1543 },
1544 ),
1545 (
1546 "fork-v1-default",
1547 {
1548 "VLLM_WORKER_MULTIPROC_METHOD": "fork",
1549 "VLLM_ENABLE_V1_MULTIPROCESSING": "1",
1550 "VLLM_USE_V1": "1",
1551 },
1552 ),
1553 (
1554 "spawn-v0-singleproc",
1555 {
1556 "VLLM_WORKER_MULTIPROC_METHOD": "spawn",
1557 "VLLM_ENABLE_V1_MULTIPROCESSING": "0",
1558 "VLLM_USE_V1": "0",
1559 },
1560 ),
1561 (
1562 "spawn-v0-no-aiter",
1563 {
1564 "VLLM_WORKER_MULTIPROC_METHOD": "spawn",
1565 "VLLM_ENABLE_V1_MULTIPROCESSING": "0",
1566 "VLLM_USE_V1": "0",
1567 "VLLM_ROCM_USE_AITER": "0",
1568 },
1569 ),
1570 ]
1571 retry_failures = []
1572 for profile_name, profile_overrides in retry_profiles:
1573 retry_overrides = {
1574 "HIP_VISIBLE_DEVICES": visible_devices,
1575 "CUDA_VISIBLE_DEVICES": visible_devices,
1576 "PYTORCH_ROCM_DEVICE": primary_visible,
1577 "VLLM_TARGET_DEVICE": target_device,
1578 }
1579 retry_overrides.update(profile_overrides)
1580 retry = _run_vllm_attempt_subprocess(
1581 llm_kwargs,
1582 prompts,
1583 max_tokens,
1584 env_overrides=retry_overrides,
1585 )
1586 if retry.get("ok"):
1587 return True, {
1588 "model": model_name,
1589 "model_format": model_format,
1590 "target_device": target_device,
1591 "throughput_tokens_per_sec": float(retry.get("throughput_tokens_per_sec", 0.0)),
1592 "latency_ms": float(retry.get("latency_ms", 0.0)),
1593 "throughput_samples": [float(x) for x in (retry.get("throughput_samples") or [])],
1594 "candidate_models": candidate_names,
1595 "visible_devices_after_retry": visible_devices,
1596 "fallback_mode": profile_name,
1597 }, []
1598 retry_failures.append(f"{profile_name}: {retry.get('error', 'unknown retry failure')}")
1599 if retry_failures:
1600 err_msg = f"{err_msg}; retry_profiles_failed: {' || '.join(retry_failures)}"
1601 if "Entry Not Found" in err_msg or "not found on the Hugging Face Hub" in err_msg:
1602 err_msg = (
1603 f"Model {model_name} not found. Download/copy it first or override "
1604 "MLSTACK_BENCH_VLLM_SAFETENSORS_MODEL / MLSTACK_BENCH_VLLM_GGUF_MODEL_PATH"
1605 )
1606 elif "authentication" in err_msg.lower():
1607 err_msg = "Hugging Face authentication required. Run 'huggingface-cli login' first."
1608 attempt_errors.append(f"{model_format} model {model_name} failed: {err_msg}")
1609
1610 failure_reason = "No tiny vLLM benchmark model could be loaded"
1611 if runtime_probe_hint:
1612 failure_reason = f"{failure_reason}; runtime_probe={runtime_probe_hint}"
1613 if attempt_errors:
1614 failure_reason = f"{failure_reason}; {attempt_errors[0]}"
1615
1616 return True, _degraded_metrics("vllm", failure_reason, {
1617 "model": "vLLM (Detected)",
1618 "model_format": "unavailable",
1619 "target_device": target_device,
1620 "visible_devices": os.environ.get("HIP_VISIBLE_DEVICES", primary_visible),
1621 "throughput_tokens_per_sec": 0.0,
1622 "latency_ms": 0.0,
1623 "throughput_samples": [],
1624 "candidate_models": candidate_names,
1625 }), attempt_errors or [failure_reason]
1626
1627
1628def _deepspeed():
1629 _set_amd_gpu_id_table_env()
1630 try:
1631 import deepspeed
1632 import torch
1633 import os
1634 except Exception as exc:
1635 err = f"DeepSpeed or Torch not available: {exc}"
1636 return True, _degraded_metrics("deepspeed", err, {
1637 "throughput_samples_per_sec": 0.0,
1638 "avg_latency_ms": 0.0,
1639 "stage": 1,
1640 "accelerator": "rocm",
1641 "samples": [],
1642 }), [err]
1643 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
1644 if not probe_ok:
1645 extra = {
1646 "throughput_samples_per_sec": 0.0,
1647 "avg_latency_ms": 0.0,
1648 "stage": 1,
1649 "accelerator": "rocm",
1650 "samples": [],
1651 }
1652 if isinstance(probe_meta, dict):
1653 extra.update(probe_meta)
1654 return True, _degraded_metrics("deepspeed", probe_reason, extra), [probe_reason]
1655
1656 # Ensure ROCm environment
1657 os.environ["DS_ACCELERATOR"] = "rocm"
1658
1659 try:
1660 from deepspeed.accelerator import get_accelerator
1661
1662 accelerator = get_accelerator()
1663 device_name = accelerator.device_name()
1664 device = torch.device(device_name if ":" in device_name else f"{device_name}:0")
1665
1666 class SimpleModel(torch.nn.Module):
1667 def __init__(self):
1668 super().__init__()
1669 self.net = torch.nn.Sequential(
1670 torch.nn.Linear(1024, 1024),
1671 torch.nn.ReLU(),
1672 torch.nn.Linear(1024, 1024),
1673 )
1674
1675 def forward(self, x):
1676 return self.net(x)
1677
1678 model = SimpleModel().to(device).half()
1679 batch_size = 4
1680 input_data = torch.randn(batch_size, 1024, device=device, dtype=torch.float16)
1681
1682 ds_config = {
1683 "train_batch_size": batch_size,
1684 "train_micro_batch_size_per_gpu": batch_size,
1685 "gradient_accumulation_steps": 1,
1686 "zero_optimization": {"stage": 0},
1687 "fp16": {"enabled": True},
1688 # DeepSpeed internally computes `step % steps_per_print`; keep this non-zero.
1689 "steps_per_print": 1,
1690 "wall_clock_breakdown": False,
1691 }
1692 optimizer = torch.optim.AdamW(model.parameters(), lr=0.001)
1693
1694 try:
1695 model_engine, _, _, _ = deepspeed.initialize(
1696 config=ds_config,
1697 model=model,
1698 model_parameters=model.parameters(),
1699 optimizer=optimizer,
1700 )
1701 mode = "train"
1702 except Exception as init_exc:
1703 init_err = str(init_exc)
1704 if "fused_adam" in init_err.lower() or "cuda_runtime_api.h" in init_err:
1705 model_engine = deepspeed.init_inference(
1706 model,
1707 mp_size=1,
1708 dtype=torch.float16,
1709 replace_with_kernel_inject=False,
1710 )
1711 mode = "inference"
1712 else:
1713 raise
1714
1715 times = []
1716 if mode == "train":
1717 for _ in range(2):
1718 out = model_engine(input_data)
1719 model_engine.backward(out.sum())
1720 model_engine.step()
1721
1722 for _ in range(5):
1723 accelerator.synchronize()
1724 start = time.perf_counter()
1725 out = model_engine(input_data)
1726 model_engine.backward(out.sum())
1727 model_engine.step()
1728 accelerator.synchronize()
1729 times.append(time.perf_counter() - start)
1730 else:
1731 model_engine.eval()
1732 for _ in range(2):
1733 with torch.no_grad():
1734 _ = model_engine(input_data)
1735 accelerator.synchronize()
1736
1737 for _ in range(5):
1738 accelerator.synchronize()
1739 start = time.perf_counter()
1740 with torch.no_grad():
1741 _ = model_engine(input_data)
1742 accelerator.synchronize()
1743 times.append(time.perf_counter() - start)
1744
1745 avg_time = sum(times) / len(times)
1746 throughput = batch_size / avg_time
1747
1748 return True, {
1749 "throughput_samples_per_sec": round(throughput, 2),
1750 "avg_latency_ms": round(avg_time * 1000, 2),
1751 "stage": 0 if mode == "train" else 0,
1752 "accelerator": "rocm",
1753 "mode": mode,
1754 "samples": [round(batch_size / t, 2) for t in times],
1755 }, []
1756 except Exception as exc:
1757 err = f"DeepSpeed benchmark failed: {exc}"
1758 return True, _degraded_metrics("deepspeed", err, {
1759 "throughput_samples_per_sec": 0.0,
1760 "avg_latency_ms": 0.0,
1761 "stage": 1,
1762 "accelerator": "rocm",
1763 "samples": [],
1764 }), [err]
1765
1766
1767def _megatron():
1768 try:
1769 import torch
1770 import megatron
1771 except Exception as exc:
1772 err = f"Megatron not available: {exc}"
1773 return True, _degraded_metrics("megatron", err, {
1774 "megatron_backend": "unavailable",
1775 "megatron_throughput_samples_per_sec": 0.0,
1776 "megatron_avg_latency_ms": 0.0,
1777 "megatron_samples": [],
1778 }), [err]
1779
1780 probe_ok, probe_reason, probe_meta = _probe_gpu_runtime()
1781 if not probe_ok:
1782 extra = {
1783 "megatron_backend": "megatron-core",
1784 "megatron_throughput_samples_per_sec": 0.0,
1785 "megatron_avg_latency_ms": 0.0,
1786 "megatron_samples": [],
1787 }
1788 if isinstance(probe_meta, dict):
1789 extra.update(probe_meta)
1790 return True, _degraded_metrics("megatron", probe_reason, extra), [probe_reason]
1791
1792 try:
1793 device = torch.device("cuda:0")
1794 model = torch.nn.Sequential(
1795 torch.nn.Linear(4096, 4096),
1796 torch.nn.GELU(),
1797 torch.nn.Linear(4096, 4096),
1798 ).to(device).half()
1799
1800 input_data = torch.randn(8, 4096, device=device, dtype=torch.float16)
1801 samples = []
1802
1803 for _ in range(2):
1804 out = model(input_data)
1805 _ = out.sum()
1806 torch.cuda.synchronize()
1807
1808 for _ in range(5):
1809 torch.cuda.synchronize()
1810 start = time.perf_counter()
1811 out = model(input_data)
1812 _ = out.sum()
1813 torch.cuda.synchronize()
1814 elapsed = time.perf_counter() - start
1815 samples.append(8.0 / elapsed if elapsed > 0 else 0.0)
1816
1817 avg_tput = sum(samples) / len(samples) if samples else 0.0
1818 avg_latency_ms = (1000.0 / avg_tput * 8.0) if avg_tput > 0 else 0.0
1819 return True, {
1820 "megatron_backend": getattr(megatron, "__name__", "megatron"),
1821 "megatron_throughput_samples_per_sec": round(avg_tput, 2),
1822 "megatron_avg_latency_ms": round(avg_latency_ms, 2),
1823 "megatron_samples": [round(x, 2) for x in samples],
1824 }, []
1825 except Exception as exc:
1826 err = f"Megatron benchmark failed: {exc}"
1827 return True, _degraded_metrics("megatron", err, {
1828 "megatron_backend": "megatron-core",
1829 "megatron_throughput_samples_per_sec": 0.0,
1830 "megatron_avg_latency_ms": 0.0,
1831 "megatron_samples": [],
1832 }), [err]
1833
1834
1835def _onnx():
1836 import os
1837 try:
1838 import onnxruntime as ort
1839 except Exception as exc:
1840 err = f"onnxruntime not available: {exc}"
1841 return True, _degraded_metrics("onnx", err, {
1842 "ort_version": "unavailable",
1843 "provider": "none",
1844 "providers_available": [],
1845 "model_load_ms": 0.0,
1846 "session_create_ms": 0.0,
1847 "inference_latency_p50_ms": 0.0,
1848 "inference_latency_p95_ms": 0.0,
1849 "inference_latency_p99_ms": 0.0,
1850 "throughput_inf_per_sec": 0.0,
1851 "input_shape": [],
1852 "output_shape": [],
1853 "graph_opt_level": "unavailable",
1854 "inference_samples": [],
1855 }), [err]
1856
1857 ort_version = ort.__version__
1858 all_providers = ort.get_available_providers()
1859 providers_available = list(all_providers)
1860
1861 if "MIGraphXExecutionProvider" in all_providers:
1862 provider = "MIGraphXExecutionProvider"
1863 elif "DmlExecutionProvider" in all_providers:
1864 provider = "DmlExecutionProvider"
1865 elif "ROCMExecutionProvider" in all_providers:
1866 provider = "ROCMExecutionProvider"
1867 elif "CUDAExecutionProvider" in all_providers:
1868 provider = "CUDAExecutionProvider"
1869 else:
1870 provider = "CPUExecutionProvider"
1871
1872 try:
1873 import numpy as np
1874 except Exception:
1875 err = "numpy not available for ONNX benchmark"
1876 return True, _degraded_metrics("onnx", err, {
1877 "ort_version": ort_version,
1878 "provider": provider,
1879 "providers_available": providers_available,
1880 "model_load_ms": 0.0,
1881 "session_create_ms": 0.0,
1882 "inference_latency_p50_ms": 0.0,
1883 "inference_latency_p95_ms": 0.0,
1884 "inference_latency_p99_ms": 0.0,
1885 "throughput_inf_per_sec": 0.0,
1886 "input_shape": [],
1887 "output_shape": [],
1888 "graph_opt_level": "ORT_ENABLE_ALL",
1889 "inference_samples": [],
1890 }), [err]
1891
1892 try:
1893 import onnx
1894 from onnx import helper, TensorProto, numpy_helper
1895 except Exception:
1896 onnx = None
1897
1898 batch_size = 8
1899 hidden = 512
1900 input_shape = [batch_size, hidden]
1901
1902 def _build_test_model():
1903 X = helper.make_tensor_value_info("input", TensorProto.FLOAT, input_shape)
1904 Y = helper.make_tensor_value_info("output", TensorProto.FLOAT, input_shape)
1905 W_init = numpy_helper.from_array(
1906 np.random.randn(hidden, hidden).astype(np.float32) * 0.01, name="W"
1907 )
1908 B_init = numpy_helper.from_array(
1909 np.zeros(hidden, dtype=np.float32), name="B"
1910 )
1911 matmul = helper.make_node("MatMul", ["input", "W"], ["matmul_out"])
1912 add = helper.make_node("Add", ["matmul_out", "B"], ["add_out"])
1913 relu = helper.make_node("Relu", ["add_out"], ["output"])
1914 graph = helper.make_graph(
1915 [matmul, add, relu], "bench_graph", [X], [Y], initializer=[W_init, B_init]
1916 )
1917 model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 17)])
1918 model.ir_version = 8
1919 onnx.checker.validate = False
1920 return onnx._serialize(model)
1921
1922 def _build_quantized_model():
1923 X = helper.make_tensor_value_info("input", TensorProto.FLOAT, input_shape)
1924 Y = helper.make_tensor_value_info("output", TensorProto.FLOAT, input_shape)
1925 W_init = numpy_helper.from_array(
1926 np.random.randn(hidden, hidden).astype(np.float32) * 0.01, name="W"
1927 )
1928 B_init = numpy_helper.from_array(
1929 np.zeros(hidden, dtype=np.float32), name="B"
1930 )
1931 dq = helper.make_node(
1932 "DynamicQuantizeLinear", ["input"], ["input_quant", "input_scale", "input_zp"]
1933 )
1934 matmul_int = helper.make_node(
1935 "MatMulInteger", ["input_quant", "W"], ["matmul_int_out"]
1936 )
1937 cast_out = helper.make_node(
1938 "Cast", ["matmul_int_out"], ["matmul_fp_out"], to=TensorProto.FLOAT
1939 )
1940 add = helper.make_node("Add", ["matmul_fp_out", "B"], ["output"])
1941 graph = helper.make_graph(
1942 [dq, matmul_int, cast_out, add], "quant_bench_graph",
1943 [X], [Y], initializer=[W_init, B_init]
1944 )
1945 model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 17)])
1946 model.ir_version = 8
1947 onnx.checker.validate = False
1948 return onnx._serialize(model)
1949
1950 import tempfile
1951 sess_opts = ort.SessionOptions()
1952 sess_opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
1953 sess_opts.log_severity_level = 3
1954
1955 results = {}
1956 errors = []
1957
1958 # --- FP32 model benchmark ---
1959 try:
1960 if onnx is not None:
1961 model_bytes = _build_test_model()
1962 else:
1963 model_bytes = None
1964
1965 if model_bytes is None:
1966 raise RuntimeError("onnx package required to build test model")
1967
1968 with tempfile.NamedTemporaryFile(suffix=".onnx", delete=False) as f:
1969 f.write(model_bytes)
1970 fp32_path = f.name
1971
1972 t0 = time.perf_counter()
1973 session = ort.InferenceSession(fp32_path, sess_opts, providers=[provider])
1974 session_create_ms = (time.perf_counter() - t0) * 1000.0
1975
1976 input_meta = session.get_inputs()[0]
1977 output_meta = session.get_outputs()[0]
1978 input_name = input_meta.name
1979 actual_input_shape = input_meta.shape
1980 output_shape = output_meta.shape
1981
1982 if isinstance(actual_input_shape[0], int):
1983 bench_batch = actual_input_shape[0]
1984 else:
1985 bench_batch = batch_size
1986 actual_input_shape_resolved = [bench_batch] + [
1987 d if isinstance(d, int) else hidden for d in actual_input_shape[1:]
1988 ]
1989
1990 feed = {input_name: np.random.randn(*actual_input_shape_resolved).astype(np.float32)}
1991
1992 for _ in range(3):
1993 session.run(None, feed)
1994
1995 num_runs = 20
1996 latencies = []
1997 for _ in range(num_runs):
1998 t_start = time.perf_counter()
1999 session.run(None, feed)
2000 latencies.append((time.perf_counter() - t_start) * 1000.0)
2001
2002 latencies.sort()
2003 p50 = latencies[len(latencies) // 2]
2004 p95 = latencies[int(len(latencies) * 0.95)]
2005 p99 = latencies[int(len(latencies) * 0.99)]
2006 avg_latency = sum(latencies) / len(latencies)
2007 throughput = bench_batch / (avg_latency / 1000.0) if avg_latency > 0 else 0.0
2008
2009 mem_rss_mb = 0.0
2010 try:
2011 import resource
2012 mem_rss_mb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024.0
2013 except Exception:
2014 pass
2015
2016 results.update({
2017 "fp32_session_create_ms": round(session_create_ms, 2),
2018 "fp32_inference_latency_p50_ms": round(p50, 3),
2019 "fp32_inference_latency_p95_ms": round(p95, 3),
2020 "fp32_inference_latency_p99_ms": round(p99, 3),
2021 "fp32_avg_latency_ms": round(avg_latency, 3),
2022 "fp32_throughput_inf_per_sec": round(throughput, 2),
2023 "fp32_inference_samples": [round(l, 3) for l in latencies],
2024 "input_shape": actual_input_shape_resolved,
2025 "output_shape": list(output_shape) if output_shape else [],
2026 "input_dtype": str(input_meta.type),
2027 "output_dtype": str(output_meta.type),
2028 "peak_rss_mb": round(mem_rss_mb, 1),
2029 "num_warmup": 3,
2030 "num_timed_runs": num_runs,
2031 })
2032
2033 os.unlink(fp32_path)
2034 del session
2035 except Exception as exc:
2036 results["fp32_error"] = str(exc)
2037 errors.append(f"FP32 benchmark failed: {exc}")
2038
2039 # --- Quantized model benchmark ---
2040 quantized_ok = False
2041 try:
2042 if onnx is not None:
2043 q_model_bytes = _build_quantized_model()
2044 if q_model_bytes is not None:
2045 with tempfile.NamedTemporaryFile(suffix=".onnx", delete=False) as f:
2046 f.write(q_model_bytes)
2047 quant_path = f.name
2048
2049 t0 = time.perf_counter()
2050 q_session = ort.InferenceSession(
2051 quant_path, sess_opts, providers=[provider]
2052 )
2053 q_create_ms = (time.perf_counter() - t0) * 1000.0
2054
2055 q_input = q_session.get_inputs()[0]
2056 q_feed = {
2057 q_input.name: np.random.randn(
2058 *([batch_size] + [
2059 d if isinstance(d, int) else hidden
2060 for d in q_input.shape[1:]
2061 ])
2062 ).astype(np.float32)
2063 }
2064
2065 for _ in range(3):
2066 q_session.run(None, q_feed)
2067
2068 q_latencies = []
2069 for _ in range(20):
2070 t_start = time.perf_counter()
2071 q_session.run(None, q_feed)
2072 q_latencies.append((time.perf_counter() - t_start) * 1000.0)
2073
2074 q_latencies.sort()
2075 q_p50 = q_latencies[len(q_latencies) // 2]
2076 q_avg = sum(q_latencies) / len(q_latencies)
2077 q_throughput = batch_size / (q_avg / 1000.0) if q_avg > 0 else 0.0
2078
2079 results.update({
2080 "quantized_session_create_ms": round(q_create_ms, 2),
2081 "quantized_inference_latency_p50_ms": round(q_p50, 3),
2082 "quantized_avg_latency_ms": round(q_avg, 3),
2083 "quantized_throughput_inf_per_sec": round(q_throughput, 2),
2084 "quantized_inference_samples": [round(l, 3) for l in q_latencies],
2085 "quantized_ops": ["DynamicQuantizeLinear", "MatMulInteger"],
2086 })
2087 quantized_ok = True
2088 os.unlink(quant_path)
2089 del q_session
2090 except Exception as exc:
2091 results["quantized_error"] = str(exc)
2092 errors.append(f"Quantized benchmark failed: {exc}")
2093
2094 p50_val = results.get("fp32_inference_latency_p50_ms", 0.0)
2095 p95_val = results.get("fp32_inference_latency_p95_ms", 0.0)
2096 p99_val = results.get("fp32_inference_latency_p99_ms", 0.0)
2097 tput_val = results.get("fp32_throughput_inf_per_sec", 0.0)
2098
2099 metrics = {
2100 "ort_version": ort_version,
2101 "provider": provider,
2102 "providers_available": providers_available,
2103 "graph_opt_level": "ORT_ENABLE_ALL",
2104 "model_load_ms": results.get("fp32_session_create_ms", 0.0),
2105 "session_create_ms": results.get("fp32_session_create_ms", 0.0),
2106 "inference_latency_p50_ms": p50_val,
2107 "inference_latency_p95_ms": p95_val,
2108 "inference_latency_p99_ms": p99_val,
2109 "throughput_inf_per_sec": tput_val,
2110 "input_shape": results.get("input_shape", input_shape),
2111 "output_shape": results.get("output_shape", []),
2112 "input_dtype": results.get("input_dtype", "tensor(float)"),
2113 "output_dtype": results.get("output_dtype", "tensor(float)"),
2114 "peak_rss_mb": results.get("peak_rss_mb", 0.0),
2115 "num_warmup": results.get("num_warmup", 0),
2116 "num_timed_runs": results.get("num_timed_runs", 0),
2117 "quantized_supported": quantized_ok,
2118 "inference_samples": results.get("fp32_inference_samples", []),
2119 }
2120
2121 for k in ("fp32_inference_samples", "fp32_session_create_ms", "fp32_inference_latency_p50_ms",
2122 "fp32_inference_latency_p95_ms", "fp32_inference_latency_p99_ms",
2123 "fp32_avg_latency_ms", "fp32_throughput_inf_per_sec",
2124 "quantized_session_create_ms", "quantized_inference_latency_p50_ms",
2125 "quantized_avg_latency_ms", "quantized_throughput_inf_per_sec",
2126 "quantized_inference_samples", "quantized_ops",
2127 "quantized_error", "fp32_error"):
2128 if k in results:
2129 metrics[k] = results[k]
2130
2131 success = True
2132 return success, metrics, errors
2133
2134
2135BENCHES = {
2136 "gpu-info": _gpu_info,
2137 "memory-bandwidth": _memory_bandwidth,
2138 "tensor-core": _tensor_core,
2139 "gemm": _gemm,
2140 "pytorch": _pytorch,
2141 "flash-attention": _flash_attention,
2142 "vllm": _vllm,
2143 "deepspeed": _deepspeed,
2144 "megatron": _megatron,
2145 "onnx": _onnx,
2146 "llama-cpp": _llama_cpp,
2147}
2148
2149
2150def main():
2151 parser = argparse.ArgumentParser()
2152 parser.add_argument("bench", help=f"One of: {', '.join(BENCHES.keys())}")
2153 parser.add_argument("--json", action="store_true", help="Emit JSON")
2154 args = parser.parse_args()
2155
2156 fn = BENCHES.get(args.bench)
2157 if fn is None:
2158 payload = {
2159 "name": args.bench,
2160 "success": False,
2161 "execution_time_ms": 0,
2162 "metrics": {},
2163 "errors": [f"Unknown benchmark {args.bench}"],
2164 }
2165 print(json.dumps(payload, indent=2))
2166 sys.exit(1)
2167
2168 start = time.perf_counter()
2169 success, metrics, errors = fn()
2170 elapsed_ms = int((time.perf_counter() - start) * 1000)
2171 payload = {
2172 "name": args.bench,
2173 "success": success,
2174 "execution_time_ms": elapsed_ms,
2175 "metrics": metrics,
2176 "errors": errors,
2177 }
2178
2179 # Print a clear marker to help the Rust wrapper find the JSON block
2180 print("\n---BENCHMARK_RESULTS_START---")
2181 print(json.dumps(payload, indent=2))
2182 sys.exit(0 if success else 1)
2183
2184
2185if __name__ == "__main__":
2186 main()
2187"#;