diff --git a/.changeset/milab-6382-gpu-support.md b/.changeset/milab-6382-gpu-support.md new file mode 100644 index 0000000..5f446d7 --- /dev/null +++ b/.changeset/milab-6382-gpu-support.md @@ -0,0 +1,21 @@ +--- +"@platforma-open/milaboratories.gpu-test.workflow": minor +"@platforma-open/milaboratories.gpu-test.gpu-info": minor +"@platforma-open/milaboratories.gpu-test.model": minor +"@platforma-open/milaboratories.gpu-test.ui": minor +--- + +MILAB-6382: dual-mode operation via `exec.hasGpu` gate + +- Workflow gates `.gpuMemory()` on `exec.hasGpu` and passes `--mode gpu` / + `--mode cpu` to the software. Calling `.gpuMemory()` on a backend without + GPU is a permanent runner error; the gate keeps the block runnable on + both GPU and CPU-only clusters. +- Software accepts the new `--mode {gpu,cpu}` flag. In `gpu` mode the + existing detection + benchmark runs; in `cpu` mode the GPU probes are + skipped and the report emits "GPU not available" fast (the block returns + successfully — it never errors when GPU is absent). +- Model: `GpuReport.mode` field added so the UI can render the active + branch. +- UI: summary banner shows "CPU branch (exec.hasGpu = false)" when running + in CPU mode. diff --git a/model/src/index.ts b/model/src/index.ts index ee07cc2..50482bf 100644 --- a/model/src/index.ts +++ b/model/src/index.ts @@ -27,6 +27,11 @@ export const model = BlockModel.create() export interface GpuReport { seed: number; + // 'gpu' = workflow ran on the GPU branch (exec.hasGpu was true) and the + // software ran the full detection + benchmark. + // 'cpu' = workflow ran on the CPU branch; probes were skipped and the + // block returned a fast "no GPU available" report without erroring. + mode: 'gpu' | 'cpu'; gpu_available: boolean; cupy: { available: boolean; diff --git a/software/gpu-info/src/main.py b/software/gpu-info/src/main.py index a0092dc..620be4b 100644 --- a/software/gpu-info/src/main.py +++ b/software/gpu-info/src/main.py @@ -293,6 +293,14 @@ def format_report(report): lines.append(f"GPU Detection Report (seed: {report.get('seed', 'N/A')})") lines.append("=" * 60) lines.append("") + mode = report.get("mode", "gpu") + lines.append(f"Mode: {mode.upper()}") + if mode == "cpu": + lines.append( + " Workflow ran on the CPU branch (exec.hasGpu = false). The block " + "skipped GPU probes and the benchmark." + ) + lines.append("") # CuPy (RAPIDS) cupy_info = report["cupy"] @@ -386,39 +394,81 @@ def format_report(report): # Summary gpu_available = cupy_info["available"] or torch_info["available"] or smi_info["available"] lines.append("=" * 60) - lines.append(f"SUMMARY: GPU {'AVAILABLE' if gpu_available else 'NOT AVAILABLE'}") + if mode == "cpu": + lines.append("SUMMARY: GPU NOT AVAILABLE (CPU branch, no probes attempted)") + else: + lines.append(f"SUMMARY: GPU {'AVAILABLE' if gpu_available else 'NOT AVAILABLE'}") lines.append("=" * 60) return "\n".join(lines) +def empty_probe_result(error_msg): + """Empty probe result used in --mode cpu (no detection attempted).""" + return { + "available": False, + "device_count": 0, + "devices": [], + "cuda_version": None, + "error": error_msg, + } + + def main(): import argparse parser = argparse.ArgumentParser(description="GPU detection and benchmark") + parser.add_argument( + "--mode", + choices=["gpu", "cpu"], + default="gpu", + help=( + "Execution mode (set by the workflow based on exec.hasGpu): " + "'gpu' runs CuPy/PyTorch/nvidia-smi probes and the GPU benchmark; " + "'cpu' skips probes and emits a fast 'no GPU available' report. " + "The block never errors when no GPU is present — the CPU path is " + "a first-class behaviour, not a fallback." + ), + ) parser.add_argument("--seed", type=int, default=0, help="Run seed for cache busting") parser.add_argument("--matrix-size", type=int, default=4000, help="Benchmark matrix size (NxN)") args = parser.parse_args() - report = {"seed": args.seed} - - report["cupy"] = detect_cupy() - report["torch_cuda"] = detect_torch_cuda() - report["nvidia_smi"] = detect_nvidia_smi() - report["environment"] = detect_env_vars() - - report["benchmark"] = run_benchmark( - report["cupy"]["available"], - report["torch_cuda"]["available"], - report["nvidia_smi"]["available"], - args.matrix_size, - ) + report = {"seed": args.seed, "mode": args.mode} + + if args.mode == "cpu": + # Workflow passed --mode cpu (exec.hasGpu = false). Skip every probe + # and emit a minimal but schema-compatible report so the UI renders. + cpu_reason = "workflow ran on the CPU branch (exec.hasGpu = false)" + report["cupy"] = empty_probe_result(cpu_reason) + report["torch_cuda"] = empty_probe_result(cpu_reason) + report["nvidia_smi"] = { + "available": False, + "driver_version": None, + "devices": [], + "error": cpu_reason, + } + report["environment"] = detect_env_vars() + report["benchmark"] = {"ran": False, "skipped": cpu_reason} + report["gpu_available"] = False + else: + report["cupy"] = detect_cupy() + report["torch_cuda"] = detect_torch_cuda() + report["nvidia_smi"] = detect_nvidia_smi() + report["environment"] = detect_env_vars() + + report["benchmark"] = run_benchmark( + report["cupy"]["available"], + report["torch_cuda"]["available"], + report["nvidia_smi"]["available"], + args.matrix_size, + ) - report["gpu_available"] = ( - report["cupy"]["available"] - or report["torch_cuda"]["available"] - or report["nvidia_smi"]["available"] - ) + report["gpu_available"] = ( + report["cupy"]["available"] + or report["torch_cuda"]["available"] + or report["nvidia_smi"]["available"] + ) # Write JSON output with open("gpu-info.json", "w") as f: diff --git a/ui/src/pages/GpuInfoPage.vue b/ui/src/pages/GpuInfoPage.vue index 2163d93..91a4530 100644 --- a/ui/src/pages/GpuInfoPage.vue +++ b/ui/src/pages/GpuInfoPage.vue @@ -112,6 +112,7 @@ function rerun() {