Skip to content

Commit 492f6cd

Browse files
committed
Preserve requested GGUF GPU pools
1 parent b616bd2 commit 492f6cd

7 files changed

Lines changed: 61 additions & 7 deletions

File tree

studio/backend/models/inference.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,14 @@ class LoadResponse(BaseModel):
485485
)
486486
gpu_ids: Optional[List[int]] = Field(
487487
None,
488-
description = "Physical GPU indices the model is pinned to, or None for automatic selection.",
488+
description = "Effective GPU indices the model is using after fit-time narrowing, or None for automatic selection.",
489+
)
490+
requested_gpu_ids: Optional[List[int]] = Field(
491+
None,
492+
description = (
493+
"GPU placement pool requested by the user before fit-time narrowing, "
494+
"or None for automatic selection."
495+
),
489496
)
490497

491498

@@ -649,7 +656,14 @@ class InferenceStatusResponse(BaseModel):
649656
)
650657
gpu_ids: Optional[List[int]] = Field(
651658
None,
652-
description = "Physical GPU indices the model is pinned to, or None for automatic selection.",
659+
description = "Effective GPU indices the model is using after fit-time narrowing, or None for automatic selection.",
660+
)
661+
requested_gpu_ids: Optional[List[int]] = Field(
662+
None,
663+
description = (
664+
"GPU placement pool requested by the user before fit-time narrowing, "
665+
"or None for automatic selection."
666+
),
653667
)
654668
llama_cpp_supports_mtp: bool = Field(
655669
True,

studio/backend/routes/inference.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4425,6 +4425,7 @@ async def _load_model_impl(
44254425
n_layers = llama_backend.n_layers,
44264426
n_moe_layers = llama_backend.n_moe_layers,
44274427
gpu_ids = llama_backend.gpu_ids,
4428+
requested_gpu_ids = llama_backend.requested_gpu_ids,
44284429
)
44294430
else:
44304431
if (
@@ -4796,6 +4797,7 @@ async def _attempt_gguf_load(
47964797
n_layers = llama_backend.n_layers,
47974798
n_moe_layers = llama_backend.n_moe_layers,
47984799
gpu_ids = llama_backend.gpu_ids,
4800+
requested_gpu_ids = llama_backend.requested_gpu_ids,
47994801
)
48004802

48014803
# ── Standard path: load via Unsloth/transformers ──────────
@@ -5915,6 +5917,7 @@ async def get_status(current_subject: str = Depends(get_current_subject)):
59155917
n_layers = llama_backend.n_layers,
59165918
n_moe_layers = llama_backend.n_moe_layers,
59175919
gpu_ids = llama_backend.gpu_ids,
5920+
requested_gpu_ids = llama_backend.requested_gpu_ids,
59185921
llama_cpp_supports_mtp = _supports_mtp,
59195922
spec_fallback_reason = llama_backend.spec_fallback_reason,
59205923
llama_cpp_prebuilt_stale = _stale,

studio/backend/tests/test_gpu_memory_mode.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -591,10 +591,23 @@ def test_load_request_accepts_gpu_ids():
591591
@pytest.mark.parametrize("model_cls", [LoadResponse, InferenceStatusResponse])
592592
def test_response_models_emit_gpu_ids(model_cls):
593593
if model_cls is LoadResponse:
594-
obj = model_cls(status = "loaded", model = "m", display_name = "m", inference = {}, gpu_ids = [1])
594+
obj = model_cls(
595+
status = "loaded",
596+
model = "m",
597+
display_name = "m",
598+
inference = {},
599+
gpu_ids = [1],
600+
requested_gpu_ids = [1, 2],
601+
)
595602
else:
596-
obj = model_cls(gpu_ids = [1])
603+
obj = model_cls(gpu_ids = [1], requested_gpu_ids = [1, 2])
597604
assert obj.model_dump()["gpu_ids"] == [1]
605+
assert obj.model_dump()["requested_gpu_ids"] == [1, 2]
606+
607+
608+
def test_gguf_load_and_status_responses_include_requested_gpu_pool():
609+
route_src = (Path(_BACKEND_DIR) / "routes" / "inference.py").read_text(encoding = "utf-8")
610+
assert route_src.count("requested_gpu_ids = llama_backend.requested_gpu_ids") == 3
598611

599612

600613
def test_gpu_ids_property_default_and_reset():

studio/frontend/src/features/chat/lib/apply-inference-status-to-store.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,9 @@ export function applyActiveModelStatusToStore(
222222
incomingGpuMode === "manual" ? (status.n_cpu_moe ?? null) : null;
223223
const incomingSplit =
224224
incomingGpuMode === "manual" ? (status.tensor_split ?? null) : null;
225-
const incomingGpuIds = status.is_gguf ? (status.gpu_ids ?? null) : null;
225+
const incomingGpuIds = status.is_gguf
226+
? (status.requested_gpu_ids ?? status.gpu_ids ?? null)
227+
: null;
226228
const gpuStatusChanged =
227229
prevState.loadedGpuMemoryMode !== incomingGpuMode ||
228230
prevState.loadedGpuLayers !== incomingGpuLayers ||

studio/frontend/src/features/chat/stores/chat-runtime-store.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,7 @@ export function loadedGpuMemoryFields(resp: {
605605
n_layers?: number | null;
606606
n_moe_layers?: number;
607607
gpu_ids?: number[] | null;
608+
requested_gpu_ids?: number[] | null;
608609
}) {
609610
// GPU-memory state is meaningful only for a GGUF chat load. A non-GGUF response
610611
// still carries gpu_memory_mode (its default "auto" is serialized), so gate on
@@ -631,7 +632,9 @@ export function loadedGpuMemoryFields(resp: {
631632
};
632633
}
633634
const mode = resp.gpu_memory_mode ?? "auto";
634-
const gpuIds = resp.gpu_ids ?? null;
635+
// Keep the user's placement pool editable across status/load hydration.
636+
// gpu_ids remains the effective fitted subset for diagnostics.
637+
const gpuIds = resp.requested_gpu_ids ?? resp.gpu_ids ?? null;
635638
// Layer/MoE/split knobs apply (and are reported) only in manual mode; in auto
636639
// the server ignores them, so don't seed the loaded baseline or the editable
637640
// knobs with values it never applied. In manual, the server reports gpu_layers
@@ -669,7 +672,7 @@ export function loadedGpuMemoryFields(resp: {
669672
ggufLayerCount: resp.n_layers ?? null,
670673
// MoE expert-layer count: the n_cpu_moe slider max, and 0 hides the slider.
671674
moeLayerCount: resp.n_moe_layers ?? null,
672-
// The picker reflects what loaded (the request sent the user's pick).
675+
// The picker reflects the requested placement pool, not a fitted subset.
673676
selectedGpuIds: gpuIds,
674677
loadedGpuIds: gpuIds,
675678
...manualKnobs,

studio/frontend/src/features/chat/types/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,10 @@ export interface LoadModelResponse {
188188
n_layers?: number | null;
189189
/** Model's MoE expert-layer count (the n_cpu_moe ceiling); 0 if not MoE. */
190190
n_moe_layers?: number;
191+
/** Effective GPU placement after fit-time narrowing. */
191192
gpu_ids?: number[] | null;
193+
/** User-requested GPU placement pool before fit-time narrowing. */
194+
requested_gpu_ids?: number[] | null;
192195
}
193196

194197
export interface UnloadModelRequest {
@@ -240,7 +243,10 @@ export interface InferenceStatusResponse {
240243
/** n_ctx the active GGUF load was invoked with (0 = Auto); re-seeds a
241244
* Manual + Auto-layers context pin on hydration. Null for non-GGUF. */
242245
requested_context_length?: number | null;
246+
/** Effective GPU placement after fit-time narrowing. */
243247
gpu_ids?: number[] | null;
248+
/** User-requested GPU placement pool before fit-time narrowing. */
249+
requested_gpu_ids?: number[] | null;
244250
n_layers?: number | null;
245251
/** Model's MoE expert-layer count (the n_cpu_moe ceiling); 0 if not MoE. */
246252
n_moe_layers?: number;

tests/studio/test_model_picker_contracts.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,19 @@ def test_active_model_config_round_trips_gpu_fields():
151151
assert "export function gpuFieldsSignature" in shared
152152

153153

154+
def test_gpu_picker_round_trips_requested_pool_not_fitted_subset():
155+
"""A GGUF fit may narrow [0, 1] to [0], but load/status hydration must keep
156+
[0, 1] as the editable pool so a later reload can grow back onto GPU 1."""
157+
types = _read("features/chat/types/api.ts")
158+
assert types.count("requested_gpu_ids?: number[] | null") >= 2
159+
160+
store = _read("features/chat/stores/chat-runtime-store.ts")
161+
assert "resp.requested_gpu_ids ?? resp.gpu_ids ?? null" in store
162+
163+
status = _read("features/chat/lib/apply-inference-status-to-store.ts")
164+
assert "status.requested_gpu_ids ?? status.gpu_ids ?? null" in status
165+
166+
154167
def test_compare_load_uses_each_models_gpu_config():
155168
src = _read("features/chat/shared-composer.tsx")
156169
assert "ownConfig.gpuMemoryMode ?? compareLoadKnobs.gpuMemoryMode" in src

0 commit comments

Comments
 (0)