diff --git a/studio/backend/main.py b/studio/backend/main.py index 1d1b3ce2c6..ffebf17a94 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -1241,10 +1241,15 @@ def _get_cached_system_gpu_info(logger) -> dict[str, Any]: try: from core.inference.llama_cpp import LlamaCppBackend from utils.hardware import DeviceType, get_device - if get_device() == DeviceType.XPU: - gpu_ids_supported = False - elif LlamaCppBackend._is_vulkan_backend(): + # Check the Vulkan build first: its picks live in ggml's own ordinal + # space (--device Vulkan) and don't rely on torch-xpu ordinals, so + # they're valid even on an Intel/XPU host. Only fall through to the + # XPU ban for a non-Vulkan build (where a pick would need torch-xpu + # ordinals no visibility mask can speak). + if LlamaCppBackend._is_vulkan_backend(): gpu_ids_supported = bool(gguf_devices) + elif get_device() == DeviceType.XPU: + gpu_ids_supported = False else: gpu_ids_supported = True except Exception as e: diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index a0a6685cd6..d83350a470 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -4443,18 +4443,12 @@ async def _load_model_impl( from utils.hardware import DeviceType, get_device from utils.hardware.hardware import resolve_requested_gpu_ids - if get_device() == DeviceType.XPU: - raise HTTPException( - status_code = 400, - detail = ( - "GPU selection (gpu_ids) is not supported on Intel XPU. " - "Omit gpu_ids to use all devices." - ), - ) - # A Vulkan-only build validates against ggml's own Vulkan ordinals - # instead: /api/system reports gguf_devices in that space, load_model - # pins the pick with --device Vulkan, so probe, picker, and pin - # share one index space (physical ids are never involved). + # A Vulkan build validates against ggml's own Vulkan ordinals: + # /api/system reports gguf_devices in that space, load_model pins the + # pick with --device Vulkan, so probe, picker, and pin share one + # index space (physical/torch ids are never involved). Check it + # BEFORE the XPU ban -- a Vulkan pick on an Intel/XPU host does not + # rely on torch-xpu ordinals, so the ban must not reject it. if LlamaCppBackend._is_vulkan_backend(): # Diffusion GGUFs bypass llama-server: the diffusion runner # forwards gpu_ids[0] as a CUDA/DG_GPU device token, NOT @@ -4474,10 +4468,24 @@ async def _load_model_impl( "runner cannot map ggml Vulkan ordinals. Omit gpu_ids." ), ) + # validate_vulkan_gpu_ids may spawn the Vulkan device probe + # (blocking subprocess.run). Run it off the event loop so a + # stalled driver/probe can't freeze status/progress/unload for + # up to the probe timeout. try: - LlamaCppBackend.validate_vulkan_gpu_ids(effective_gpu_ids) + await asyncio.to_thread( + LlamaCppBackend.validate_vulkan_gpu_ids, effective_gpu_ids + ) except ValueError as exc: raise HTTPException(status_code = 400, detail = str(exc)) from exc + elif get_device() == DeviceType.XPU: + raise HTTPException( + status_code = 400, + detail = ( + "GPU selection (gpu_ids) is not supported on Intel XPU. " + "Omit gpu_ids to use all devices." + ), + ) else: try: resolve_requested_gpu_ids(effective_gpu_ids) @@ -5082,21 +5090,15 @@ async def validate_model( from utils.hardware import DeviceType, get_device from utils.hardware.hardware import resolve_requested_gpu_ids - if get_device() == DeviceType.XPU: - raise HTTPException( - status_code = 400, - detail = ( - "GPU selection (gpu_ids) is not supported on Intel XPU. " - "Omit gpu_ids to use all devices." - ), - ) # Mirror /load: a Vulkan build validates the pick in ggml's own # Vulkan ordinal space (the space the --device pin uses), and rejects # picks for CONFIRMED diffusion GGUFs (their runner takes a CUDA/DG_GPU # token, not --device Vulkan, so an ordinal targets the wrong card). # `None` (uncached, unclassifiable) is allowed through so first-time # remote GGUF loads still work; the spawn-time backstop catches an - # uncached model that turns out to be diffusion after download. + # uncached model that turns out to be diffusion after download. Check + # the Vulkan path BEFORE the XPU ban: a Vulkan pick on an XPU host + # uses ggml ordinals, not torch-xpu ones, so the ban must not hide it. if LlamaCppBackend._is_vulkan_backend(): if _classify_diffusion_gguf(config) is True: raise HTTPException( @@ -5107,10 +5109,22 @@ async def validate_model( "runner cannot map ggml Vulkan ordinals. Omit gpu_ids." ), ) + # Off-loop: validate_vulkan_gpu_ids may spawn the blocking Vulkan + # probe subprocess (see /load). try: - LlamaCppBackend.validate_vulkan_gpu_ids(effective_gpu_ids) + await asyncio.to_thread( + LlamaCppBackend.validate_vulkan_gpu_ids, effective_gpu_ids + ) except ValueError as exc: raise HTTPException(status_code = 400, detail = str(exc)) from exc + elif get_device() == DeviceType.XPU: + raise HTTPException( + status_code = 400, + detail = ( + "GPU selection (gpu_ids) is not supported on Intel XPU. " + "Omit gpu_ids to use all devices." + ), + ) else: try: resolve_requested_gpu_ids(effective_gpu_ids) diff --git a/studio/frontend/src/features/model-picker/hooks/use-active-model-config.ts b/studio/frontend/src/features/model-picker/hooks/use-active-model-config.ts index 9d09ee6897..aa7df32675 100644 --- a/studio/frontend/src/features/model-picker/hooks/use-active-model-config.ts +++ b/studio/frontend/src/features/model-picker/hooks/use-active-model-config.ts @@ -2,6 +2,7 @@ // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 import { isExternalModelId, useChatRuntimeStore } from "@/features/chat"; +import { useCurrentGpuIndexKind } from "@/hooks/use-gpu-info"; import { useMemo } from "react"; import type { PerModelConfig } from "../model-config/per-model-config"; @@ -28,6 +29,13 @@ export function useActiveModelConfig(): ActiveModelConfigState { const gpuLayers = useChatRuntimeStore((s) => s.gpuLayers); const nCpuMoe = useChatRuntimeStore((s) => s.nCpuMoe); const selectedGpuIds = useChatRuntimeStore((s) => s.selectedGpuIds); + const selectedGpuIdsKind = useChatRuntimeStore((s) => s.selectedGpuIdsKind); + // The active model is running under the current backend, so its pick is in + // the current index space. Prefer the store's stamp, but fall back to the + // current kind (reactive, so it fills in once the cache warms) -- else a + // /status hydration before the cache warmed leaves no stamp, and a later + // Reload/Remember would treat the live Vulkan pick as a legacy physical one. + const currentKind = useCurrentGpuIndexKind(); const isGguf = activeGgufVariant != null || @@ -56,6 +64,10 @@ export function useActiveModelConfig(): ActiveModelConfigState { gpuLayers, nCpuMoe, selectedGpuIds, + selectedGpuIdsIndexKind: + selectedGpuIds == null + ? undefined + : ((selectedGpuIdsKind ?? currentKind) ?? undefined), }; }, [ checkpoint, @@ -71,6 +83,8 @@ export function useActiveModelConfig(): ActiveModelConfigState { gpuLayers, nCpuMoe, selectedGpuIds, + selectedGpuIdsKind, + currentKind, ]); return { checkpoint, isGguf, config }; diff --git a/studio/frontend/src/features/model-picker/model-config/apply-per-model-config.ts b/studio/frontend/src/features/model-picker/model-config/apply-per-model-config.ts index f893e737dc..3094a50f8c 100644 --- a/studio/frontend/src/features/model-picker/model-config/apply-per-model-config.ts +++ b/studio/frontend/src/features/model-picker/model-config/apply-per-model-config.ts @@ -10,7 +10,7 @@ import { reconcilePersistedGpuIds, useChatRuntimeStore, } from "@/features/chat"; -import type { GpuIndexKind } from "@/hooks/use-gpu-info"; +import { type GpuIndexKind, currentGpuIndexKind } from "@/hooks/use-gpu-info"; import { DEFAULT_PER_MODEL_CONFIG, type PerModelConfig, @@ -103,8 +103,14 @@ export function currentRuntimePerModelConfig( selectedGpuIds: s.selectedGpuIds, // Carry the index space the live pick is in so a save/restore round-trip // (and a cancel-restore of this snapshot) can drop it after a backend swap. + // Fall back to the current kind when the store stamp is missing (a /status + // hydration before the GPU cache warmed leaves it null): a live/active pick + // is by definition in the current backend's space, so this is the right + // stamp and avoids persisting it as an unstamped (legacy physical) pick. selectedGpuIdsIndexKind: - s.selectedGpuIds == null ? undefined : (s.selectedGpuIdsKind ?? undefined), + s.selectedGpuIds == null + ? undefined + : ((s.selectedGpuIdsKind ?? currentGpuIndexKind()) ?? undefined), }; } diff --git a/studio/frontend/src/hooks/use-gpu-info.ts b/studio/frontend/src/hooks/use-gpu-info.ts index a759cf0081..004a3c0af0 100644 --- a/studio/frontend/src/hooks/use-gpu-info.ts +++ b/studio/frontend/src/hooks/use-gpu-info.ts @@ -206,6 +206,25 @@ export function useGpuDevices(): SystemGpuDevice[] { return devices; } +/** Reactive currentGpuIndexKind(): re-renders when /api/system loads, so a + * component reading a pick's index space (e.g. the active model's) updates + * once the cache warms instead of being stuck at the cold-cache null. */ +export function useCurrentGpuIndexKind(): GpuIndexKind | null { + const [kind, setKind] = useState(() => + systemGpuIndexKind(cachedSystem), + ); + useEffect(() => { + let cancelled = false; + fetchSystemOnce().then((d) => { + if (!cancelled) setKind(systemGpuIndexKind(d)); + }); + return () => { + cancelled = true; + }; + }, []); + return kind; +} + /** * Await the shared /api/system fetch so cachedPinnableGpuIndices (and the * store's reconcilePersistedGpuIds) can validate a persisted pick before a