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