Fix iGPU-only Vulkan hosts budgeting GGUF against 0 GiB

The iGPU exclusion in ggufMemoryTotalGb is right when llama-server is
masked to an iGPU beside a dGPU torch can see, but it also catches the
plain APU case: a Strix Halo or 890M box on a Vulkan build enumerates one
integrated device and nothing else, so the budget collapses to 0 and the
Hub renders "0 GiB" on a machine llama-server reports 91 GiB for. Fit
still resolves via system RAM, so this is a labelling and fit-surface
defect rather than a wrong load, but it reads as OOM on every GGUF row.

Fall back to the torch total when the inventory is all-iGPU and torch sees
no device beyond the ones probed. The device-count check is what keeps the
masked-beside-a-dGPU host on 0, which is the case the exclusion exists for.

Tests: the shape was uncovered (the existing cases are all is_igpu False).
Added a backend test that an iGPU-only inventory is surfaced with its real
total and keeps picks supported, and a source contract guard on the
frontend rule, matching how the other frontend fixes here are pinned.

Verified: frontend typecheck clean (same 5 pre-existing missing-module
errors as main, none new), 211 passed across the vulkan probe, gguf
devices, gpu memory mode, chat-load and model-picker contract suites.
This commit is contained in:
Daniel Han 2026-07-25 01:52:16 -07:00
commit 71619891e0
3 changed files with 66 additions and 1 deletions

View file

@ -159,3 +159,39 @@ def test_non_vulkan_build_reports_no_gguf_inventory(main_module, monkeypatch):
if __name__ == "__main__":
raise SystemExit(pytest.main([__file__, "-v"]))
_IGPU_ONLY_INVENTORY = [
{
"index": 0,
"name": "AMD Radeon(TM) 8060S Graphics",
"free_mib": 89 * 1024,
"total_mib": 91 * 1024,
"is_igpu": True,
}
]
def test_igpu_only_vulkan_inventory_reports_the_igpu_and_its_total(
main_module, monkeypatch
):
"""An APU box on a Vulkan build (Strix Halo) enumerates one integrated device
and no discrete card. The backend must still surface it, flagged is_igpu with
its real total, so the frontend can tell this shape apart from a masked probe
and budget GGUF against the APU pool instead of labelling everything OOM.
"""
info = _gpu_info(
main_module, monkeypatch, is_vulkan = True, inventory = _IGPU_ONLY_INVENTORY
)
assert len(info["gguf_devices"]) == 1
dev = info["gguf_devices"][0]
assert dev["is_igpu"] is True
assert dev["index"] == 0 and dev["index_kind"] == "vulkan"
assert dev["name"] == "AMD Radeon(TM) 8060S Graphics"
# The total is reported, not zeroed: the zeroing is the frontend's budgeting
# decision, and it needs the real number to fall back to.
assert dev["memory_total_gb"] == 91.0
# A probed device means an ordinal exists, so picks stay supported.
assert info["gguf_gpu_ids_supported"] is True
assert info["gguf_backend_is_vulkan"] is True

View file

@ -136,6 +136,13 @@ function toGpuInfo(data: SystemInfoResponse | null): GpuInfo {
// see a dGPU llama-server never enumerated, and reusing that total would let
// fit checks pass against VRAM /load can't actually place.
const isVulkanBuild = gpuData?.gguf_backend_is_vulkan === true;
// An all-iGPU inventory that torch sees no extra device beyond means there is
// no discrete card anywhere (an APU box, e.g. Strix Halo on a Vulkan build).
// The exclusion above would budget 0 and label it "0 GiB"; the APU's pool is
// real and torch reports it, so use that. The device-count check keeps the
// masked-to-iGPU-beside-a-dGPU case on 0, which is what the exclusion is for.
const ggufIgpuOnly =
ggufDevices.length > 0 && ggufDevices.every((d) => d.is_igpu === true);
if (!gpuData?.available || !devices.length) {
// Torch sees no GPU (training stays CPU-bound / unavailable), but a Vulkan
// llama.cpp build may still drive GPUs for GGUF: surface that budget alone.
@ -151,7 +158,9 @@ function toGpuInfo(data: SystemInfoResponse | null): GpuInfo {
name: devices[0]?.name ?? "Unknown",
memoryTotalGb,
ggufMemoryTotalGb: ggufDevices.length
? ggufDeviceTotalGb
? ggufIgpuOnly && devices.length <= ggufDevices.length
? memoryTotalGb
: ggufDeviceTotalGb
: isVulkanBuild
? 0
: memoryTotalGb,

View file

@ -508,3 +508,23 @@ def test_legacy_migration_is_idempotent_and_non_destructive():
# Layer 3: non-overwriting merge skips an existing (or default) key, so even a
# forced re-run cannot duplicate or clobber a user's config.
assert "if (isDefaultConfig(migrated) || Object.hasOwn(map, key)) {" in src
def test_igpu_only_vulkan_budget_falls_back_to_the_torch_total():
"""An APU on a Vulkan build (Strix Halo, Radeon 890M) enumerates one iGPU and
nothing else. The iGPU exclusion in the GGUF budget would make that 0, so the
Hub renders "0 GiB" and every GGUF row labels OOM against a box whose pool is
real and reported by torch. Fall back to the torch total for that shape only;
a masked-to-iGPU-beside-a-dGPU host (torch sees more devices than the probe
enumerated) must stay on 0, which is what the exclusion exists for.
"""
src = " ".join(_read("hooks/use-gpu-info.ts").split())
# All-iGPU inventory detection, and the device-count guard that keeps the
# mixed masked host on the conservative 0.
assert (
"const ggufIgpuOnly = ggufDevices.length > 0 && "
"ggufDevices.every((d) => d.is_igpu === true);" in src
)
assert "ggufIgpuOnly && devices.length <= ggufDevices.length" in src
# Training/safetensors budgets must stay on the torch total regardless.
assert "memoryTotalGb," in src