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:
parent
4ed40cb074
commit
785d446fc1
9 changed files with 212 additions and 15 deletions
|
|
@ -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) ============
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: "文档",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue