From 785d446fc1493232b7896e4089e2ba53d0809ab7 Mon Sep 17 00:00:00 2001
From: oobabooga
Date: Mon, 15 Jun 2026 09:25:08 -0300
Subject: [PATCH] Studio: show llama.cpp version and GPU specs in the About
panel (#6261)
* Studio: show llama.cpp version and GPU specs in the About panel
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Studio: run the hardware endpoint off the event loop
* Studio: show the full llama.cpp release tag (incl -mix-) in the About panel
* Studio: order About-panel GPUs by visible ordinal and skip the llama.cpp probe during updates
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Fix hardware info refresh and endpoint scope
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han
Co-authored-by: Etherll <61019402+Etherll@users.noreply.github.com>
---
studio/backend/main.py | 29 +++++++-
studio/backend/tests/test_llama_cpp_update.py | 27 ++++++++
studio/backend/utils/llama_cpp_update.py | 34 ++++++++++
.../components/studio-version-section.tsx | 17 ++++-
.../src/features/settings/tabs/about-tab.tsx | 40 ++++++++++-
.../frontend/src/hooks/use-hardware-info.ts | 68 ++++++++++++++++---
.../src/hooks/use-llama-update-check.ts | 2 +
studio/frontend/src/i18n/locales/en.ts | 5 ++
studio/frontend/src/i18n/locales/zh-CN.ts | 5 ++
9 files changed, 212 insertions(+), 15 deletions(-)
diff --git a/studio/backend/main.py b/studio/backend/main.py
index 2022074c08..2435efba21 100644
--- a/studio/backend/main.py
+++ b/studio/backend/main.py
@@ -250,7 +250,7 @@ if os.getenv("ENVIRONMENT_TYPE", "production") == "production":
# warnings.filterwarnings("ignore", category=DeprecationWarning)
# warnings.filterwarnings("ignore", module="triton.*")
-from fastapi import Depends, FastAPI, HTTPException, Request
+from fastapi import Depends, FastAPI, HTTPException, Query, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, HTMLResponse, Response
@@ -967,17 +967,40 @@ async def get_gpu_visibility(current_subject: str = Depends(get_current_subject)
@app.get("/api/system/hardware")
-async def get_hardware_info(current_subject: str = Depends(get_current_subject)):
+def get_hardware_info(
+ include_details: bool = Query(False), current_subject: str = Depends(get_current_subject)
+):
"""Return GPU name, total VRAM, and key ML package versions.
Gated behind auth alongside /api/system -- same fingerprinting concern.
/api/system/gpu-visibility is also auth-gated.
+
+ ``include_details`` is for About/diagnostics. The default response stays
+ cheap for callers that only need the primary GPU summary, like training
+ method auto-selection. Sync def (not async): hardware/detail probes can
+ shell out, and FastAPI runs sync endpoints in a threadpool.
"""
from utils.hardware import get_gpu_summary, get_package_versions
- return {
+
+ body = {
"gpu": get_gpu_summary(),
"versions": get_package_versions(),
}
+ if include_details:
+ from utils.llama_cpp_update import get_installed_llama_version
+
+ # All backend-visible GPUs (respects CUDA_VISIBLE_DEVICES), so multi-GPU
+ # hosts list every device -- get_gpu_summary alone reports only the primary.
+ # Sort by visible_ordinal: the nvidia-smi path returns rows in physical order,
+ # so under a reordering CUDA_VISIBLE_DEVICES (e.g. "5,3") labeling by array
+ # index would otherwise disagree with the GPU 0/1 the backend actually sees.
+ devices = get_backend_visible_gpu_info().get("devices", [])
+ body["gpus"] = [
+ {"name": d.get("name"), "vram_total_gb": d.get("memory_total_gb")}
+ for d in sorted(devices, key = lambda d: d.get("visible_ordinal", 0))
+ ]
+ body["llama_cpp"] = get_installed_llama_version()
+ return body
# ============ Serve Frontend (Optional) ============
diff --git a/studio/backend/tests/test_llama_cpp_update.py b/studio/backend/tests/test_llama_cpp_update.py
index 326b3dc6aa..c42c3e8a79 100644
--- a/studio/backend/tests/test_llama_cpp_update.py
+++ b/studio/backend/tests/test_llama_cpp_update.py
@@ -293,6 +293,33 @@ def test_status_source_build_skips_probe_while_job_runs(monkeypatch, tmp_path):
assert probes == {"resolve": 0, "version": 0}
+def test_installed_version_skips_probe_while_job_runs(monkeypatch, tmp_path):
+ # Markerless build: get_installed_llama_version falls back to exec'ing
+ # `llama-server --version`. While the updater swaps the tree that exec can
+ # fail the installer's os.replace on Windows, so the About-panel probe must
+ # be skipped (return None) exactly like get_update_status's source probe.
+ binary = tmp_path / "build" / "bin" / "llama-server"
+ binary.parent.mkdir(parents = True)
+ binary.write_text("stub") # markerless: no UNSLOTH_PREBUILT_INFO.json
+ monkeypatch.setattr(upd, "_find_binary", lambda: str(binary))
+ probed = {"n": 0}
+
+ def _count_version(b):
+ probed["n"] += 1
+ return 9585
+
+ monkeypatch.setattr(upd, "_installed_build_number", _count_version)
+
+ with upd._job_lock:
+ upd._job["state"] = upd._JOB_RUNNING
+ assert upd.get_installed_llama_version() is None
+ assert probed["n"] == 0 # never exec'd the binary mid-swap
+
+ upd._reset_job_for_tests() # back to idle -> probe runs
+ assert upd.get_installed_llama_version() == "b9585"
+ assert probed["n"] == 1
+
+
def test_status_update_available(monkeypatch, tmp_path):
binary = _write_install(tmp_path, "b9493")
monkeypatch.setattr(upd, "_find_binary", lambda: binary)
diff --git a/studio/backend/utils/llama_cpp_update.py b/studio/backend/utils/llama_cpp_update.py
index 3518247d7e..c2c6c67432 100644
--- a/studio/backend/utils/llama_cpp_update.py
+++ b/studio/backend/utils/llama_cpp_update.py
@@ -180,6 +180,40 @@ def _installed_build_number(binary: Optional[str]) -> Optional[int]:
return n if n > 1 else None
+def get_installed_llama_version() -> Optional[str]:
+ """Display string for the active llama.cpp install (e.g. 'b9585' or
+ 'b9601-mix-a0e2906'), or None.
+
+ Prefers the install marker's release_tag -- the full unsloth release
+ identity, the same field the update banner compares as installed (see
+ #6219) -- so a 'b9601-mix-a0e2906' build reads back in full rather than
+ collapsing to its base 'b9601'. The marker's bare ``tag`` is only the
+ upstream llama.cpp build (no '-mix-' suffix), so it's the fallback.
+ Last resort is ``b`` parsed from ``llama-server --version`` for
+ source/custom builds that have no marker.
+
+ Lightweight: reads the local marker and at most runs ``--version``. Does no
+ network or release-freshness work (unlike get_update_status), so it is safe
+ to call from latency-sensitive paths like the About panel.
+ """
+ binary = _find_binary()
+ marker = read_install_marker(binary)
+ if marker:
+ tag = marker.get("release_tag") or marker.get("tag")
+ if tag:
+ return tag
+ # Markerless/source build: the fallback execs ``llama-server --version``.
+ # Skip it while an update is swapping the tree -- on Windows that exec can
+ # make the installer's os.replace fail (the same race get_update_status's
+ # source-build probe guards against). The panel just omits the row.
+ with _job_lock:
+ job_running = _job["state"] == _JOB_RUNNING
+ if job_running:
+ return None
+ n = _installed_build_number(binary)
+ return f"b{n}" if n is not None else None
+
+
def _is_under(path: Path, root: Path) -> bool:
try:
p, r = path.resolve(), root.resolve()
diff --git a/studio/frontend/src/features/settings/components/studio-version-section.tsx b/studio/frontend/src/features/settings/components/studio-version-section.tsx
index 9ede7cd35d..9b0c513cbf 100644
--- a/studio/frontend/src/features/settings/components/studio-version-section.tsx
+++ b/studio/frontend/src/features/settings/components/studio-version-section.tsx
@@ -35,8 +35,14 @@ async function fetchStudioVersions(): Promise<{
}
}
-// Shared "Unsloth" version block, shown in both General and About.
-export function StudioVersionSection() {
+// Shared "Unsloth" version block, shown in both General and About. The About
+// tab passes llamaCppVersion to surface the installed llama.cpp build alongside
+// the version rows; General omits it, so the row only shows on About.
+export function StudioVersionSection({
+ llamaCppVersion,
+}: {
+ llamaCppVersion?: string | null;
+} = {}) {
const t = useT();
const [packageVersion, setPackageVersion] = useState("dev");
const [studioVersion, setStudioVersion] = useState("dev");
@@ -65,6 +71,13 @@ export function StudioVersionSection() {
{packageVersion}
+ {llamaCppVersion ? (
+
+
+ {llamaCppVersion}
+
+
+ ) : null}
);
}
diff --git a/studio/frontend/src/features/settings/tabs/about-tab.tsx b/studio/frontend/src/features/settings/tabs/about-tab.tsx
index 15684d9b3b..2394e2ad92 100644
--- a/studio/frontend/src/features/settings/tabs/about-tab.tsx
+++ b/studio/frontend/src/features/settings/tabs/about-tab.tsx
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
import { usePlatformStore } from "@/config/env";
import { getAuthToken } from "@/features/auth";
import { removeTrainingUnloadGuard } from "@/features/training";
+import { useHardwareInfo } from "@/hooks/use-hardware-info";
import { useT } from "@/i18n";
import { apiUrl, isTauri } from "@/lib/api-base";
import {
@@ -74,6 +75,7 @@ export function AboutTab() {
const t = useT();
const deviceType = usePlatformStore((s) => s.deviceType);
const defaultShell = deviceType === "windows" ? "windows" : "unix";
+ const hw = useHardwareInfo();
const [shutdownOpen, setShutdownOpen] = useState(false);
const [installSource, setInstallSource] = useState<
UpdateInstallSource | "loading"
@@ -104,7 +106,9 @@ export function AboutTab() {
-
+ {/* llama.cpp row lives in the shared version section so it sits with the
+ Unsloth/Package rows; the prop keeps it About-only (General passes none). */}
+
@@ -116,6 +120,40 @@ export function AboutTab() {
+ {hw.gpus.length > 0 || hw.cuda || hw.rocm ? (
+
+ {hw.gpus.map((gpu, i) => (
+ 1
+ ? `${t("settings.about.gpu")} ${i}`
+ : t("settings.about.gpu")
+ }
+ >
+
+ {gpu.name ?? "—"}
+ {gpu.vramTotalGb != null
+ ? ` · ${Math.round(gpu.vramTotalGb)} GB`
+ : ""}
+
+
+ ))}
+ {hw.cuda || hw.rocm ? (
+
+
+ {hw.cuda ?? hw.rocm}
+
+
+ ) : null}
+
+ ) : null}
+
| null = null;
+let cacheGeneration = 0;
+const listeners = new Set<(info: HardwareInfo) => void>();
+
+function notifyHardwareInfo(info: HardwareInfo) {
+ listeners.forEach((listener) => listener(info));
+}
+
+export function invalidateHardwareInfo() {
+ cacheGeneration += 1;
+ cached = null;
+ fetchPromise = null;
+}
+
+export async function refreshHardwareInfo(): Promise {
+ invalidateHardwareInfo();
+ return fetchOnce();
+}
async function fetchOnce(): Promise {
if (cached) return cached;
if (fetchPromise) return fetchPromise;
+ const generation = cacheGeneration;
fetchPromise = (async () => {
try {
- const res = await authFetch("/api/system/hardware");
+ const res = await authFetch("/api/system/hardware?include_details=true");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const info: HardwareInfo = {
gpuName: data?.gpu?.gpu_name ?? null,
vramTotalGb: data?.gpu?.vram_total_gb ?? null,
vramFreeGb: data?.gpu?.vram_free_gb ?? null,
+ gpus: Array.isArray(data?.gpus)
+ ? data.gpus.map((g: ApiGpu) => ({
+ name: g?.name ?? null,
+ vramTotalGb: g?.vram_total_gb ?? null,
+ }))
+ : [],
torch: data?.versions?.torch ?? null,
cuda: data?.versions?.cuda ?? null,
+ rocm: data?.versions?.rocm ?? null,
transformers: data?.versions?.transformers ?? null,
unsloth: data?.versions?.unsloth ?? null,
+ llamaCpp: data?.llama_cpp ?? null,
};
- cached = info;
- return info;
+ if (generation === cacheGeneration) {
+ cached = info;
+ notifyHardwareInfo(info);
+ return info;
+ }
+ return cached ?? DEFAULT;
} catch {
// Reset so subsequent calls retry (e.g. backend wasn't ready).
- fetchPromise = null;
+ if (generation === cacheGeneration) fetchPromise = null;
return DEFAULT;
}
})();
@@ -66,13 +112,17 @@ export function useHardwareInfo(): HardwareInfo {
const [info, setInfo] = useState(cached ?? DEFAULT);
useEffect(() => {
- if (cached) return;
-
let cancelled = false;
- fetchOnce().then((hw) => {
+ const listener = (hw: HardwareInfo) => {
if (!cancelled) setInfo(hw);
- });
- return () => { cancelled = true; };
+ };
+
+ listeners.add(listener);
+ if (!cached) fetchOnce().then(listener);
+ return () => {
+ cancelled = true;
+ listeners.delete(listener);
+ };
}, []);
return info;
diff --git a/studio/frontend/src/hooks/use-llama-update-check.ts b/studio/frontend/src/hooks/use-llama-update-check.ts
index 73e12f9bff..5fcacab521 100644
--- a/studio/frontend/src/hooks/use-llama-update-check.ts
+++ b/studio/frontend/src/hooks/use-llama-update-check.ts
@@ -2,6 +2,7 @@
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { authFetch, getAuthToken } from "@/features/auth";
+import { refreshHardwareInfo } from "@/hooks/use-hardware-info";
import { useCallback, useEffect, useRef, useState } from "react";
// First check shortly after load, then re-surface as an hourly reminder. The
@@ -120,6 +121,7 @@ export function useLlamaUpdateCheck({
setApplying(false);
if (s.job.state === "success") {
setVisible(false);
+ void refreshHardwareInfo();
onDone?.({ ok: true, tag: s.job.to_tag });
} else if (s.job.state === "error") {
// Leave the banner up so the user can retry; clearing applying drops
diff --git a/studio/frontend/src/i18n/locales/en.ts b/studio/frontend/src/i18n/locales/en.ts
index 08feda7300..9b32c22dde 100644
--- a/studio/frontend/src/i18n/locales/en.ts
+++ b/studio/frontend/src/i18n/locales/en.ts
@@ -329,6 +329,11 @@ export const en = {
"Docs, release notes, feedback, and build info.",
studioVersion: "Unsloth Version",
packageVersion: "Package Version",
+ llamaCppVersion: "llama.cpp Version",
+ hardware: "Hardware",
+ gpu: "GPU",
+ cuda: "CUDA",
+ rocm: "ROCm",
updates: "Update",
help: "Help",
documentation: "Documentation",
diff --git a/studio/frontend/src/i18n/locales/zh-CN.ts b/studio/frontend/src/i18n/locales/zh-CN.ts
index 9141a4c104..c98ff794bd 100644
--- a/studio/frontend/src/i18n/locales/zh-CN.ts
+++ b/studio/frontend/src/i18n/locales/zh-CN.ts
@@ -301,6 +301,11 @@ export const zhCN = {
description: "文档、发布说明、反馈和 Unsloth 构建信息。",
studioVersion: "Unsloth 版本",
packageVersion: "包版本",
+ llamaCppVersion: "llama.cpp 版本",
+ hardware: "硬件",
+ gpu: "GPU",
+ cuda: "CUDA",
+ rocm: "ROCm",
updates: "更新",
help: "帮助",
documentation: "文档",