Skip to main content

rusty_stack/benchmarks/
mod.rs

1//! Performance benchmark infrastructure for Rusty-Stack.
2//! Heavy lifting is delegated to a small embedded Python helper that runs
3//! ROCm-enabled PyTorch kernels to exercise the real hardware.
4
5use 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    // Single source (Tenet 3): inject the iGPU token list generated from the
33    // Rust `crate::gpu` consts so the benchmark Python filter cannot drift from
34    // the install-path filter.
35    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
58/// Build the Python iGPU-token list literal from the Rust `crate::gpu` consts
59/// (Tenet 3 single source — the benchmark Python filter derives from the same
60/// list as the install path, so it cannot drift).
61fn 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
323// ---------------------------------------------------------------------------
324// Embedded Python helper
325// ---------------------------------------------------------------------------
326const 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"#;