* Studio: account for mmproj VRAM in GGUF fit budget (#5825) Vision GGUFs load the mmproj projector onto the GPU via --mmproj alongside the weights, but the context auto-sizing / GPU-selection budget sized off _get_gguf_size_bytes(model_path), which counts only the weight file(s). The projector was never added, so the budget was too optimistic: context got mis-estimated and tight vision loads spilled to system RAM / OOM'd. Resolve the launch projector once before GPU selection and fold its size into the fit budget. The same resolved path feeds both the budget and the --mmproj launch flag, so the two cannot disagree. The summary log now reports the projector size separately, keeping "GGUF size" accurate. Adds _mmproj_vram_bytes() + unit tests (no GPU / network / subprocess). * Studio: simplify mmproj summary-log concatenation (#5825) Address review: the summary log mixed explicit `+` with implicit f-string concatenation. Extract the optional projector fragment into `mmproj_note` so the logger.info uses uniform implicit concatenation. No behavioral change. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: trim mmproj VRAM comments --------- Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Co-authored-by: imagineer99 <samleejackson0@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
7397c87843
commit
f033213c0b
2 changed files with 77 additions and 18 deletions
|
|
@ -2996,6 +2996,16 @@ class LlamaCppBackend:
|
|||
|
||||
return str(mmproj)
|
||||
|
||||
def _mmproj_vram_bytes(self, launch_mmproj_path: Optional[str]) -> int:
|
||||
"""Return resolved mmproj VRAM bytes, or 0 when absent/unreadable."""
|
||||
if not launch_mmproj_path:
|
||||
return 0
|
||||
try:
|
||||
return self._get_gguf_size_bytes(launch_mmproj_path)
|
||||
except OSError as e:
|
||||
logger.debug(f"Could not size mmproj {launch_mmproj_path}: {e}")
|
||||
return 0
|
||||
|
||||
def _resolve_launch_mtp_path(self, *, mtp_draft_path: Optional[str]) -> Optional[str]:
|
||||
"""Return mtp_draft_path iff it exists on disk, else None.
|
||||
|
||||
|
|
@ -3561,8 +3571,29 @@ class LlamaCppBackend:
|
|||
effective_ctx = requested_ctx if requested_ctx > 0 else (self._context_length or 0)
|
||||
max_available_ctx = self._context_length or effective_ctx
|
||||
gpus: list[tuple[int, int]] = []
|
||||
# Keep fit-budget and launch-flag mmproj resolution in sync.
|
||||
launch_mmproj_path = None
|
||||
if not extra_args_disable_mmproj(extra_args):
|
||||
launch_mmproj_path = self._resolve_launch_mmproj_path(
|
||||
model_path = model_path,
|
||||
mmproj_path = mmproj_path,
|
||||
)
|
||||
# Need both a resolved mmproj AND the config vision flag; a stray
|
||||
# mmproj passing the family-name heuristic must not flip a non-VLM
|
||||
# GGUF into vision mode.
|
||||
effective_is_vision = bool(launch_mmproj_path) and bool(is_vision)
|
||||
if is_vision and not effective_is_vision:
|
||||
logger.warning(
|
||||
"Vision-capable GGUF loaded without a usable mmproj; "
|
||||
"image input will be disabled for this session"
|
||||
)
|
||||
try:
|
||||
model_size = self._get_gguf_size_bytes(model_path)
|
||||
gguf_size = self._get_gguf_size_bytes(model_path)
|
||||
# Include GPU-loaded mmproj in the fit budget (#5825).
|
||||
mmproj_size = (
|
||||
self._mmproj_vram_bytes(launch_mmproj_path) if effective_is_vision else 0
|
||||
)
|
||||
model_size = gguf_size + mmproj_size
|
||||
gpus = self._get_gpu_free_memory()
|
||||
|
||||
# Resolve effective context: 0 means let llama-server use
|
||||
|
|
@ -3802,8 +3833,12 @@ class LlamaCppBackend:
|
|||
kv_cache_bytes = self._estimate_kv_cache_bytes(
|
||||
effective_ctx, cache_type_kv, n_parallel = n_parallel
|
||||
)
|
||||
mmproj_note = (
|
||||
f"mmproj: {mmproj_size / (1024**3):.1f} GB, " if mmproj_size else ""
|
||||
)
|
||||
logger.info(
|
||||
f"GGUF size: {model_size / (1024**3):.1f} GB, "
|
||||
f"GGUF size: {gguf_size / (1024**3):.1f} GB, "
|
||||
f"{mmproj_note}"
|
||||
f"est. KV cache: {kv_cache_bytes / (1024**3):.1f} GB, "
|
||||
f"context: {effective_ctx}, "
|
||||
f"GPUs free: {gpus}, selected: {gpu_indices}, fit: {use_fit}"
|
||||
|
|
@ -3814,22 +3849,6 @@ class LlamaCppBackend:
|
|||
tp_tensor_split = None
|
||||
effective_ctx = requested_ctx # fall back to original
|
||||
|
||||
launch_mmproj_path = None
|
||||
if not extra_args_disable_mmproj(extra_args):
|
||||
launch_mmproj_path = self._resolve_launch_mmproj_path(
|
||||
model_path = model_path,
|
||||
mmproj_path = mmproj_path,
|
||||
)
|
||||
# Need both a resolved mmproj AND the config vision flag; a stray
|
||||
# mmproj passing the family-name heuristic must not flip a non-VLM
|
||||
# GGUF into vision mode.
|
||||
effective_is_vision = bool(launch_mmproj_path) and bool(is_vision)
|
||||
if is_vision and not effective_is_vision:
|
||||
logger.warning(
|
||||
"Vision-capable GGUF loaded without a usable mmproj; "
|
||||
"image input will be disabled for this session"
|
||||
)
|
||||
|
||||
# Audio input straight from the mmproj (clip.has_audio_encoder),
|
||||
# independent of token names.
|
||||
self._mmproj_has_audio = False
|
||||
|
|
|
|||
40
studio/backend/tests/test_mmproj_vram_accounting.py
Normal file
40
studio/backend/tests/test_mmproj_vram_accounting.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""Tests for mmproj VRAM accounting in GGUF fit budgeting (#5825)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from core.inference.llama_cpp import LlamaCppBackend
|
||||
|
||||
|
||||
def _write(path: Path, n_bytes: int) -> Path:
|
||||
path.parent.mkdir(parents = True, exist_ok = True)
|
||||
path.write_bytes(b"\x00" * n_bytes)
|
||||
return path
|
||||
|
||||
|
||||
def _backend() -> LlamaCppBackend:
|
||||
return LlamaCppBackend.__new__(LlamaCppBackend)
|
||||
|
||||
|
||||
def test_counts_resolved_projector_size(tmp_path: Path):
|
||||
mmproj = _write(tmp_path / "Qwen3.5-9B-BF16-mmproj.gguf", 1024)
|
||||
|
||||
got = _backend()._mmproj_vram_bytes(str(mmproj))
|
||||
|
||||
assert got == 1024
|
||||
|
||||
|
||||
def test_zero_when_no_projector_resolved(tmp_path: Path):
|
||||
assert _backend()._mmproj_vram_bytes(None) == 0
|
||||
|
||||
|
||||
def test_zero_when_projector_missing_on_disk(tmp_path: Path):
|
||||
missing = tmp_path / "Qwen3.5-9B-BF16-mmproj.gguf" # never created
|
||||
|
||||
got = _backend()._mmproj_vram_bytes(str(missing))
|
||||
|
||||
assert got == 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue