studio: use total multi-GPU VRAM for OOM checks, recommend smallest when all OOM

Two fixes for GGUF variant dropdown:

1. useGpuInfo now sums memory across all GPU devices instead of only
   reading devices[0]. This matches llama-server's multi-GPU allocation
   where models can be split across GPUs.

2. When the backend-recommended variant (e.g. UD-Q4_K_XL) exceeds total
   GPU VRAM, the frontend picks the largest variant that fits instead.
   If all variants are OOM, it recommends the smallest one (most likely
   to work with --fit).
This commit is contained in:
Daniel Han 2026-03-15 06:10:30 +00:00
commit ea45370ab8
2 changed files with 29 additions and 7 deletions

View file

@ -191,11 +191,32 @@ function GgufVariantExpander({
[repoId, onSelect],
);
// If the backend-recommended variant is OOM, pick the largest fitting
// variant instead; if all are OOM, recommend the smallest one.
const effectiveRecommended = useMemo(() => {
if (!variants || !gpuGb || gpuGb <= 0) return defaultVariant;
const isOom = (v: GgufVariantDetail) => {
const gb = v.size_bytes / (1024 ** 3);
return gb > 0 && checkVramFit(gb, gpuGb) === "exceeds";
};
const defaultV = variants.find((v) => v.quant === defaultVariant);
if (defaultV && !isOom(defaultV)) return defaultVariant;
// Default is OOM -- pick largest non-OOM variant (best quality that fits)
const fitting = variants.filter((v) => !isOom(v));
if (fitting.length > 0) {
fitting.sort((a, b) => b.size_bytes - a.size_bytes);
return fitting[0].quant;
}
// All OOM -- recommend smallest (most likely to run with --fit)
const sorted = [...variants].sort((a, b) => a.size_bytes - b.size_bytes);
return sorted[0].quant;
}, [variants, defaultVariant, gpuGb]);
const sortedVariants = useMemo(() => {
if (!variants) return variants;
return [...variants].sort((a, b) => {
const aIsRec = a.quant === defaultVariant;
const bIsRec = b.quant === defaultVariant;
const aIsRec = a.quant === effectiveRecommended;
const bIsRec = b.quant === effectiveRecommended;
if (aIsRec !== bIsRec) return aIsRec ? -1 : 1;
const aGb = a.size_bytes / (1024 ** 3);
@ -206,7 +227,7 @@ function GgufVariantExpander({
return b.size_bytes - a.size_bytes;
});
}, [variants, defaultVariant, gpuGb]);
}, [variants, effectiveRecommended, gpuGb]);
if (loading) {
return (
@ -257,7 +278,7 @@ function GgufVariantExpander({
>
<span className="min-w-0 flex-1 truncate font-mono text-xs">
{v.quant}
{v.quant === defaultVariant && (
{v.quant === effectiveRecommended && (
<span className="ml-1.5 text-[9px] font-sans font-medium text-primary/70">
recommended
</span>

View file

@ -30,11 +30,12 @@ async function fetchGpuOnce(): Promise<GpuInfo> {
const data = await res.json();
const gpuData = data?.gpu;
if (!gpuData?.available || !gpuData.devices?.length) return DEFAULT_GPU;
const dev = gpuData.devices[0];
const devices = gpuData.devices as Array<{ name?: string; memory_total_gb?: number }>;
const totalGb = devices.reduce((sum, d) => sum + (d.memory_total_gb ?? 0), 0);
const info: GpuInfo = {
available: true,
name: dev.name ?? "Unknown",
memoryTotalGb: dev.memory_total_gb ?? 0,
name: devices[0]?.name ?? "Unknown",
memoryTotalGb: totalGb,
};
cachedGpu = info;
return info;