Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
290 changes: 279 additions & 11 deletions studio/backend/core/inference/llama_cpp.py

Large diffs are not rendered by default.

16 changes: 10 additions & 6 deletions studio/backend/models/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,15 @@ def normalize_blank_chat_template_override(cls, value: Optional[str]) -> Optiona
tensor_split: Optional[List[float]] = Field(
None,
description = (
"Manual mode only: relative share of the model per GPU (--tensor-split), "
"in the order of the GPUs in use, e.g. [2, 1] for 2:1. Omit it to let "
"llama.cpp use its default, which splits by free VRAM. Any list given is "
"passed through as-is, so send [1, 1] to force an even split. Ignored "
"unless gpu_memory_mode is 'manual' with gpu_layers >= 0."
"Relative share of the model per GPU (--tensor-split), in the order of "
"the GPUs in use, e.g. [2, 1] for 2:1. Omit it to let llama.cpp use its "
"default, which splits by free VRAM. Values are relative, so [1, 1] "
"forces an even split and [3, 1] and [75, 25] mean the same thing. "
"In manual mode (gpu_layers >= 0) the list is passed through as-is. In "
"auto mode it applies only with tensor_parallel, and only when the "
"placement planner did not size the split itself; a ratio that does not "
"fit the planner's per-GPU budget is dropped rather than forwarded. "
"Ignored entirely when gpu_memory_mode is 'manual' with gpu_layers < 0."
),
)

Expand Down Expand Up @@ -1274,7 +1278,7 @@ class _InferenceRuntimeFields(BaseModel):
)
tensor_split: Optional[List[float]] = Field(
None,
description = "Manual mode: relative model share per GPU (--tensor-split); None = default (split by free VRAM).",
description = "Relative model share per GPU (--tensor-split) used by the active load; None = default (split by free VRAM).",
)
n_layers: Optional[int] = Field(
None,
Expand Down
130 changes: 130 additions & 0 deletions studio/backend/tests/test_llama_cpp_placement.py
Original file line number Diff line number Diff line change
Expand Up @@ -1790,6 +1790,136 @@ def test_an_explicit_tensor_split_leaves_the_shared_heap_uncredited(tmp_path, mo
)


def test_auto_tensor_parallel_honors_user_tensor_split_when_planner_returns_none(tmp_path):
"""When auto tensor planning decides an even split is safe, the user's
per-GPU ratio must still be emitted instead of being silently ignored.
Regression for unslothai/unsloth#10355."""
backend, gguf = _backend_non_vulkan(
tmp_path,
memory = [(0, 24_000, 24_000), (1, 24_000, 24_000)],
)
backend._can_estimate_kv = lambda: True
backend._estimate_kv_cache_bytes = lambda *args, **kwargs: 0
backend._compute_buffer_ctx_bytes = lambda *args, **kwargs: 0
backend._get_gguf_size_bytes = lambda _path: 1 * 1024**3
backend._TENSOR_PARALLEL_BUFFER_RESERVE_MIB = 256

cmd = _launch(
backend,
gguf,
gpu_memory_mode = "auto",
tensor_parallel = True,
tensor_split = [3, 1],
gpu_ids = [0, 1],
n_ctx = 4096,
)["cmd"]

assert backend.tensor_parallel is True
assert "--split-mode" in cmd
assert cmd[cmd.index("--split-mode") + 1] == "tensor"
assert "--tensor-split" in cmd
assert cmd[cmd.index("--tensor-split") + 1] == "3,1"


def test_auto_tensor_parallel_drops_user_split_when_it_exceeds_budget(tmp_path):
"""A user ratio that overshoots a GPU's usable budget must not be forwarded
in auto mode just because an even split fits."""
backend, gguf = _backend_non_vulkan(
tmp_path,
memory = [(0, 16_000, 16_000), (1, 16_000, 16_000)],
)
backend._can_estimate_kv = lambda: True
backend._estimate_kv_cache_bytes = lambda *args, **kwargs: 0
backend._compute_buffer_ctx_bytes = lambda *args, **kwargs: 0
backend._get_gguf_size_bytes = lambda _path: 25 * 1024**3
backend._TENSOR_PARALLEL_BUFFER_RESERVE_MIB = 256

cmd = _launch(
backend,
gguf,
gpu_memory_mode = "auto",
tensor_parallel = True,
tensor_split = [3, 1],
gpu_ids = [0, 1],
n_ctx = 4096,
)["cmd"]

assert backend.tensor_parallel is True
assert cmd[cmd.index("--split-mode") + 1] == "tensor"
assert "--tensor-split" not in cmd


def test_auto_tensor_parallel_records_split_for_reload_matching(tmp_path):
"""An auto tensor-parallel load with a concrete ratio must not be reused
when a later request asks for a different ratio.

What is recorded is the ratio the load was ASKED for, normalized, not the
list that was emitted: the planner's own split is in per-device MiB and a
declined ratio emits nothing, so recording the emitted value would
mismatch every identical repeat and reload forever. See
``_auto_split_fingerprint``.
"""
backend, gguf = _backend_non_vulkan(
tmp_path,
memory = [(0, 24_000, 24_000), (1, 24_000, 24_000)],
)
backend._can_estimate_kv = lambda: True
backend._estimate_kv_cache_bytes = lambda *args, **kwargs: 0
backend._compute_buffer_ctx_bytes = lambda *args, **kwargs: 0
backend._get_gguf_size_bytes = lambda _path: 1 * 1024**3
backend._TENSOR_PARALLEL_BUFFER_RESERVE_MIB = 256

_launch(
backend,
gguf,
gpu_memory_mode = "auto",
tensor_parallel = True,
tensor_split = [3, 1],
gpu_ids = [0, 1],
n_ctx = 4096,
)

assert backend._auto_tensor_split == (0.75, 0.25)
# /status should also surface the normalized ratio that was emitted.
assert backend.tensor_split == [0.75, 0.25]

# The same ratio written differently is the same instruction to llama.cpp,
# so it reuses; a different one reloads.
def _intent(split):
return GgufLoadIntent(
gguf_path = str(gguf),
model_identifier = "test",
gpu_memory_mode = "auto",
tensor_parallel = True,
tensor_split = split,
gpu_ids = [0, 1],
n_ctx = 4096,
)

assert backend.adopt_load_intent_if_matched(_intent([3, 1])) is True
assert backend.adopt_load_intent_if_matched(_intent([6, 2])) is True
assert backend.adopt_load_intent_if_matched(_intent([1, 3])) is False

intent_same = GgufLoadIntent(
model_identifier = "test",
gpu_memory_mode = "auto",
tensor_parallel = True,
tensor_split = (3, 1),
gpu_ids = (0, 1),
n_ctx = 4096,
)
intent_changed = GgufLoadIntent(
model_identifier = "test",
gpu_memory_mode = "auto",
tensor_parallel = True,
tensor_split = (1, 3),
gpu_ids = (0, 1),
n_ctx = 4096,
)
assert backend._runtime_matches_intent(intent_same, None) is True
assert backend._runtime_matches_intent(intent_changed, None) is False


def _mixed_vulkan(tmp_path, monkeypatch, memory):
"""A 30 GiB GGUF on a host with 4 GiB of RAM left, full manual offload."""
backend, gguf = _backend(tmp_path, vulkan = True, memory = memory)
Expand Down
Loading
Loading