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-<sha>) 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 <danielhanchen@gmail.com>
Co-authored-by: Etherll <61019402+Etherll@users.noreply.github.com>
This commit is contained in:
oobabooga 2026-06-15 09:25:08 -03:00 committed by GitHub
commit 785d446fc1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 212 additions and 15 deletions

View file

@ -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) ============

View file

@ -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)

View file

@ -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-<commit>' suffix), so it's the fallback.
Last resort is ``b<build>`` 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()

View file

@ -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}
</code>
</SettingsRow>
{llamaCppVersion ? (
<SettingsRow label={t("settings.about.llamaCppVersion")}>
<code className="font-mono text-xs text-muted-foreground">
{llamaCppVersion}
</code>
</SettingsRow>
) : null}
</SettingsSection>
);
}

View file

@ -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() {
</p>
</header>
<StudioVersionSection />
{/* 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). */}
<StudioVersionSection llamaCppVersion={hw.llamaCpp} />
<SettingsSection title={t("settings.about.updates")}>
<div className="py-2">
@ -116,6 +120,40 @@ export function AboutTab() {
</div>
</SettingsSection>
{hw.gpus.length > 0 || hw.cuda || hw.rocm ? (
<SettingsSection title={t("settings.about.hardware")}>
{hw.gpus.map((gpu, i) => (
<SettingsRow
// Index key: device order from the backend is stable per request.
key={i}
label={
hw.gpus.length > 1
? `${t("settings.about.gpu")} ${i}`
: t("settings.about.gpu")
}
>
<code className="font-mono text-xs text-muted-foreground">
{gpu.name ?? "—"}
{gpu.vramTotalGb != null
? ` · ${Math.round(gpu.vramTotalGb)} GB`
: ""}
</code>
</SettingsRow>
))}
{hw.cuda || hw.rocm ? (
<SettingsRow
label={
hw.cuda ? t("settings.about.cuda") : t("settings.about.rocm")
}
>
<code className="font-mono text-xs text-muted-foreground">
{hw.cuda ?? hw.rocm}
</code>
</SettingsRow>
) : null}
</SettingsSection>
) : null}
<SettingsSection title={t("settings.about.help")}>
<SettingsRow label={t("settings.about.documentation")}>
<a

View file

@ -4,53 +4,99 @@
import { authFetch } from "@/features/auth";
import { useEffect, useState } from "react";
export interface GpuDevice {
name: string | null;
vramTotalGb: number | null;
}
interface ApiGpu {
name?: string | null;
vram_total_gb?: number | null;
}
export interface HardwareInfo {
gpuName: string | null;
vramTotalGb: number | null;
vramFreeGb: number | null;
gpus: GpuDevice[];
torch: string | null;
cuda: string | null;
rocm: string | null;
transformers: string | null;
unsloth: string | null;
llamaCpp: string | null;
}
const DEFAULT: HardwareInfo = {
gpuName: null,
vramTotalGb: null,
vramFreeGb: null,
gpus: [],
torch: null,
cuda: null,
rocm: null,
transformers: null,
unsloth: null,
llamaCpp: null,
};
// Module-level cache so multiple components share one fetch.
let cached: HardwareInfo | null = null;
let fetchPromise: Promise<HardwareInfo> | 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<HardwareInfo> {
invalidateHardwareInfo();
return fetchOnce();
}
async function fetchOnce(): Promise<HardwareInfo> {
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<HardwareInfo>(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;

View file

@ -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

View file

@ -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",

View file

@ -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: "文档",