Skip to content

Commit e37730d

Browse files
Studio: retry as one sequence when llama.cpp refuses a unified KV cache (#10371)
* Studio: retry as one sequence when llama.cpp refuses a unified KV cache Studio appends --kv-unified whenever it asks for more than one slot, so llama.cpp does not split -c into per-slot windows. Some architectures need one sequence per stream and refuse to build a context that way: llama_init_from_model: failed to initialize the context: glm5next: the pooled indexer needs one sequence per stream, so a unified KV cache is only supported with a single sequence The flag is Studio's, not the user's, so nothing they can change in the UI reaches it. A Strix Halo report shows GLM-5.3-Flash tried four times over two days after a 146 GB download and never once ran; the second attempt cut the context 15x, which could not have helped because it was never the context. Retry once with the geometry that would never have added the flag: one slot, no unified cache, the requested context untouched. Matched on llama.cpp's own wording rather than on an architecture name, so a second model with the same constraint needs no list entry. The rung sits ahead of the flash-attention retry, which only fires on a signal crash, and this is a clean refusal. Every --parallel occurrence is rewritten, not just the emitted one: extras are appended after Unsloth's flags and llama.cpp is last-wins. LLAMA_ARG_KV_UNIFIED is dropped from the child environment for the same reason the flash-attn retry drops LLAMA_ARG_FLASH_ATTN - llama.cpp applies the environment before argv, and there is no negated flag to emit against it. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: the single-sequence retry commits the one-slot geometry it launched * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Mark the single-sequence retry's slot clamp for PR #10371 * Keep the single-sequence retry ahead of the fit rungs and cover every slot alias * Tighten the comments on the single-sequence retry --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c2bb6e3 commit e37730d

4 files changed

Lines changed: 295 additions & 13 deletions

File tree

studio/backend/core/inference/llama_cpp.py

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
_GPU_LAYER_FLAGS,
7878
_LAYER_OFFLOAD_FLAGS,
7979
_MOE_OFFLOAD_FLAGS,
80+
_PARALLEL_FLAGS,
8081
_SPLIT_MODE_FLAGS,
8182
_TENSOR_SPLIT_FLAGS,
8283
_effective_tensor_parallel,
@@ -17693,6 +17694,79 @@ def _drop_env_flash_attn(env: MutableMapping[str, str]) -> bool:
1769317694
"""
1769417695
return env.pop("LLAMA_ARG_FLASH_ATTN", None) is not None
1769517696

17697+
# On the wording, not an architecture name, so the next model needs no list.
17698+
# glm5next spells it as the GGML_ASSERT of ggml-org/llama.cpp#27754.
17699+
_KV_UNIFIED_REFUSED_MARKERS = (
17700+
"a unified kv cache is only supported with a single sequence",
17701+
"needs one sequence per stream",
17702+
)
17703+
17704+
@staticmethod
17705+
def _is_kv_unified_refused(output: str) -> bool:
17706+
low = (output or "").lower()
17707+
return any(marker in low for marker in LlamaCppBackend._KV_UNIFIED_REFUSED_MARKERS)
17708+
17709+
@staticmethod
17710+
def _with_single_sequence(cmd: list[str]) -> Optional[list[str]]:
17711+
"""Return cmd re-run as one sequence, or None when it already is one.
17712+
17713+
Studio adds --kv-unified itself above one slot, so this reverses Studio's
17714+
choice, not the user's. Every alias goes, from _PARALLEL_FLAGS so it cannot
17715+
drift from the denylist; llama.cpp is last-wins.
17716+
"""
17717+
out: list[str] = []
17718+
saw_kv_unified = False
17719+
multi_slot = False
17720+
skip_value = False
17721+
for i, tok in enumerate(cmd):
17722+
if skip_value:
17723+
skip_value = False
17724+
continue
17725+
name = _flag_name(tok)
17726+
if name in ("--kv-unified", "-kvu"):
17727+
saw_kv_unified = True
17728+
# The flag is bare, but an inline "=1" or a separate "1" reach us.
17729+
if (
17730+
"=" not in tok
17731+
and cmd[i + 1 : i + 2]
17732+
# Case-sensitive, like llama.cpp's own bool parse.
17733+
and cmd[i + 1] in _LLAMA_ARG_TRUE_FALSE_AUTO_VALUES
17734+
):
17735+
skip_value = True
17736+
continue
17737+
if name in _PARALLEL_FLAGS:
17738+
if "=" in tok:
17739+
value = tok.partition("=")[2]
17740+
elif tok != name:
17741+
# The attached short, -np8, which _flag_name peels to -np.
17742+
value = tok[len(name) :]
17743+
else:
17744+
# Valueless --parallel: eating the next token would delete a flag.
17745+
value = cmd[i + 1] if i + 1 < len(cmd) else ""
17746+
skip_value = _flag_name(value) is None
17747+
try:
17748+
multi_slot = multi_slot or int(value.strip()) > 1
17749+
except (AttributeError, TypeError, ValueError):
17750+
pass
17751+
out.extend([name, "1"])
17752+
continue
17753+
out.append(tok)
17754+
if not saw_kv_unified and not multi_slot:
17755+
return None
17756+
if not any(_flag_name(tok) in _PARALLEL_FLAGS for tok in out):
17757+
out.extend(["--parallel", "1"])
17758+
return out
17759+
17760+
@staticmethod
17761+
def _drop_env_single_sequence(env: MutableMapping[str, str]) -> bool:
17762+
"""Drop inherited unified-cache and slot-count env before that retry.
17763+
17764+
llama.cpp reads its environment before argv. Dropped, not negated with
17765+
--no-kv-unified, which needs every build reaching here to know it.
17766+
"""
17767+
dropped = env.pop("LLAMA_ARG_KV_UNIFIED", None) is not None
17768+
return env.pop("LLAMA_ARG_N_PARALLEL", None) is not None or dropped
17769+
1769617770
@staticmethod
1769717771
def _strip_mmproj_args(cmd: list[str]) -> list[str]:
1769817772
"""Return cmd without the '--mmproj <path>' pair (text-only retry).
@@ -23692,6 +23766,11 @@ def _spawn_and_wait(run_cmd, *, label = ""):
2369223766
_startup_output
2369323767
) or self._is_tensor_quant_kv_unsupported(_startup_output)
2369423768
_hip_rocr_mismatch = self._is_bundled_hip_rocr_mismatch(_startup_output)
23769+
# No fit retry reaches it, and the rung below needs this
23770+
# launch's argv. Whole buffer: it arrives with a backtrace.
23771+
_capability_crash = _tensor_capability_crash or self._is_kv_unified_refused(
23772+
"\n".join(self._stdout_lines)
23773+
)
2369523774
if (
2369623775
not _did_rocm_retry
2369723776
and _startup_crashed
@@ -23722,7 +23801,7 @@ def _spawn_and_wait(run_cmd, *, label = ""):
2372223801
if (
2372323802
not _did_fit_retry
2372423803
and _startup_crashed
23725-
and not _tensor_capability_crash
23804+
and not _capability_crash
2372623805
and not _hip_rocr_mismatch
2372723806
):
2372823807
# A spill-planned launch that crashed on startup. The
@@ -23754,7 +23833,7 @@ def _spawn_and_wait(run_cmd, *, label = ""):
2375423833
not _did_fit_retry
2375523834
and fully_gpu_offloaded
2375623835
and _startup_crashed
23757-
and not _tensor_capability_crash
23836+
and not _capability_crash
2375823837
and not _hip_rocr_mismatch
2375923838
):
2376023839
# We forced --fit off because Unsloth's (conservative) VRAM
@@ -23822,7 +23901,7 @@ def _spawn_and_wait(run_cmd, *, label = ""):
2382223901
not _did_fit_retry
2382323902
and _fit_retry_allowed
2382423903
and _startup_crashed
23825-
and not _tensor_capability_crash
23904+
and not _capability_crash
2382623905
and not _hip_rocr_mismatch
2382723906
):
2382823907
logger.warning(
@@ -24517,6 +24596,34 @@ def _retry_is_oversized() -> bool:
2451724596
self._memory_state = resolve_effective_memory_state(cmd, env)
2451824597
healthy = _spawn_and_wait(cmd, label = "-archfallback")
2451924598

24599+
# Studio adds --kv-unified itself above one slot, so nothing the user
24600+
# changes reaches it: retry at one slot, context intact. It aborts, so
24601+
# this MUST stay ahead of the flash-attn rung, which takes any signal
24602+
# crash; on the message, not the exit, so Windows lands here too.
24603+
if not healthy and not _load_cancelled():
24604+
_kvu_cmd = (
24605+
self._with_single_sequence(_last_spawn_cmd)
24606+
if self._is_kv_unified_refused("\n".join(self._stdout_lines))
24607+
else None
24608+
)
24609+
if _kvu_cmd is not None:
24610+
logger.warning(
24611+
"llama-server refused a unified KV cache with more than "
24612+
"one sequence; retrying with one slot and --kv-unified "
24613+
"dropped. Concurrent requests will queue."
24614+
)
24615+
self._kill_process()
24616+
if self._drop_env_single_sequence(env):
24617+
logger.info(
24618+
"Dropped inherited LLAMA_ARG_KV_UNIFIED / "
24619+
"LLAMA_ARG_N_PARALLEL for the single-sequence retry."
24620+
)
24621+
cmd = _kvu_cmd
24622+
# Read by admission control; left as-is Studio over-admits.
24623+
n_parallel = 1 # allow-slot-clamp: llama-server refused more
24624+
kv_cache_unified = False
24625+
healthy = _spawn_and_wait(_kvu_cmd, label = "-single-seq")
24626+
2452024627
# Flash-attention kernels hard-crash at startup on some ROCm/GPU
2452124628
# builds (frequently inside the vision tower). Disabling FA keeps
2452224629
# both vision and MTP, so retry that way before dropping either.

studio/backend/core/inference/llama_server_args.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,16 @@
3535
CTX_CHECKPOINTS_MAX = 256
3636
CACHE_RAM_MAX_MIB = 1024 * 1024
3737

38+
# Slot-count aliases in one place: the denial below, its #9510 hint and the single-sequence retry must cover the same
39+
# set, or a spelling one of them misses reaches llama-server unnoticed.
40+
_PARALLEL_FLAGS: frozenset[str] = frozenset({"-np", "--parallel", "--n-parallel"})
41+
3842
# Each group = every alias (short + long) of one hard-denied flag. Extend the matching group when llama.cpp adds a new
3943
# alias.
4044
_DENYLIST_GROUPS: tuple[frozenset[str], ...] = (
4145
# Parallel slots: owned by typer --parallel and LoadRequest.n_parallel; a pass-through would desync the slot
4246
# bookkeeping from llama-server.
43-
frozenset({"-np", "--parallel", "--n-parallel"}),
47+
_PARALLEL_FLAGS,
4448
# Model identity: a second -m would load a different model than Unsloth thinks it loaded
4549
# Model identity: Unsloth resolves it from LoadRequest; a second -m would load a different model than Unsloth thinks
4650
# it loaded.
@@ -275,7 +279,7 @@ def validate_extra_args(args: Optional[Iterable[str]]) -> list[str]:
275279
# #9510: users reaching for `--parallel 1` hit this refusal with no pointer to the supported knob
276280
# Why (#9510): users reaching for `--parallel 1` to cap concurrent predictions on a local model hit this
277281
# refusal with no pointer to the supported knob; name it.
278-
if flag in {"-np", "--parallel", "--n-parallel"}:
282+
if flag in _PARALLEL_FLAGS:
279283
message += "; set n_parallel on the load request (parallel decode slots) instead"
280284
raise ValueError(message)
281285
if flag is None:
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# SPDX-License-Identifier: AGPL-3.0-only
2+
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
3+
4+
"""The single-sequence retry for architectures that refuse a unified KV cache.
5+
6+
Studio appends ``--kv-unified`` itself above one slot, so nothing the user changes
7+
in the UI reaches the flag that stops the model loading.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import os
13+
import sys
14+
15+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
16+
17+
from core.inference.llama_cpp import LlamaCppBackend
18+
19+
# Verbatim from a Strix Halo report: llama-server refusing GLM-5.3-Flash.
20+
_REFUSAL = (
21+
"0.56.012.623 E llama_init_from_model: failed to initialize the context: "
22+
"glm5next: the pooled indexer needs one sequence per stream, so a unified "
23+
"KV cache is only supported with a single sequence"
24+
)
25+
26+
27+
# What the binary prints: the full buffer is scanned, so the backtrace cannot bury it.
28+
_ASSERT = (
29+
"/build/llama.cpp/src/models/glm5next.cpp:1018: GGML_ASSERT(n_ps == 1 && "
30+
'"the per-cell pool view needs one sequence per stream") failed\n'
31+
"[New LWP 4242]\n#0 0x00007f0000000000 in abort ()"
32+
)
33+
34+
35+
def test_the_reported_refusal_is_recognised():
36+
assert LlamaCppBackend._is_kv_unified_refused(_REFUSAL)
37+
38+
39+
def test_the_assert_the_binary_actually_prints_is_recognised():
40+
assert LlamaCppBackend._is_kv_unified_refused(_ASSERT)
41+
42+
43+
def test_an_unrelated_failure_is_not():
44+
assert not LlamaCppBackend._is_kv_unified_refused(
45+
"error loading model: unknown model architecture: 'qwen4exp'"
46+
)
47+
assert not LlamaCppBackend._is_kv_unified_refused("")
48+
# minimax-m3 still loads: retrying it would cost three slots for nothing.
49+
assert not LlamaCppBackend._is_kv_unified_refused(
50+
"minimax_m3: unified KV cache with n_seq_max > 1; MSA needs per-sequence "
51+
"streams -> running DENSE attention. Drop --kv-unified to enable MSA."
52+
)
53+
54+
55+
def test_the_reported_launch_becomes_one_slot_with_no_unified_cache():
56+
cmd = [
57+
"llama-server",
58+
"-m",
59+
"GLM-5.3-Flash-UD-IQ4_XS-00001-of-00004.gguf",
60+
"--parallel",
61+
"4",
62+
"--flash-attn",
63+
"on",
64+
"--no-context-shift",
65+
"-c",
66+
"128000",
67+
"--gpu-layers",
68+
"47",
69+
"--fit",
70+
"off",
71+
"--kv-unified",
72+
"--jinja",
73+
]
74+
75+
out = LlamaCppBackend._with_single_sequence(cmd)
76+
77+
assert out is not None
78+
assert "--kv-unified" not in out
79+
assert out[out.index("--parallel") + 1] == "1"
80+
assert out[out.index("-c") + 1] == "128000"
81+
assert out[out.index("--gpu-layers") + 1] == "47"
82+
assert "--jinja" in out and "--no-context-shift" in out
83+
84+
85+
def test_a_parallel_surviving_in_the_extras_tail_is_rewritten_too():
86+
"""llama.cpp is last-wins and extras are appended after Unsloth's flags."""
87+
cmd = ["llama-server", "--parallel", "4", "--kv-unified", "-np", "8"]
88+
89+
out = LlamaCppBackend._with_single_sequence(cmd)
90+
91+
assert out == ["llama-server", "--parallel", "1", "-np", "1"]
92+
93+
94+
def test_every_spelling_of_the_two_flags_is_handled():
95+
cmd = ["llama-server", "--parallel=4", "-kvu", "--alias", "m"]
96+
assert LlamaCppBackend._with_single_sequence(cmd) == [
97+
"llama-server",
98+
"--parallel",
99+
"1",
100+
"--alias",
101+
"m",
102+
]
103+
104+
cmd = ["llama-server", "-np8", "--kv-unified", "1"]
105+
assert LlamaCppBackend._with_single_sequence(cmd) == ["llama-server", "-np", "1"]
106+
107+
108+
def test_every_alias_of_the_slot_count_is_rewritten():
109+
"""--n-parallel is in the denylist group too."""
110+
out = LlamaCppBackend._with_single_sequence(
111+
["llama-server", "--parallel", "4", "--kv-unified", "--n-parallel", "8"]
112+
)
113+
114+
assert out == ["llama-server", "--parallel", "1", "--n-parallel", "1"]
115+
116+
117+
def test_a_valueless_slot_flag_does_not_swallow_the_next_flag():
118+
"""Malformed, but eating the token behind it would delete --kv-unified."""
119+
out = LlamaCppBackend._with_single_sequence(["llama-server", "--parallel", "--kv-unified"])
120+
121+
assert out == ["llama-server", "--parallel", "1"]
122+
123+
124+
def test_a_command_already_running_one_sequence_has_nothing_to_retry():
125+
assert LlamaCppBackend._with_single_sequence(["llama-server", "-c", "8192"]) is None
126+
assert LlamaCppBackend._with_single_sequence(["llama-server", "--parallel", "1"]) is None
127+
128+
129+
def test_a_slot_count_is_added_when_the_command_carried_none():
130+
out = LlamaCppBackend._with_single_sequence(["llama-server", "--kv-unified"])
131+
132+
assert out == ["llama-server", "--parallel", "1"]
133+
134+
135+
def test_the_inherited_environment_is_dropped_so_it_cannot_undo_the_retry():
136+
"""llama.cpp applies its environment before parsing argv."""
137+
env = {"LLAMA_ARG_KV_UNIFIED": "1", "LLAMA_ARG_N_PARALLEL": "4", "PATH": "/usr/bin"}
138+
139+
assert LlamaCppBackend._drop_env_single_sequence(env) is True
140+
assert "LLAMA_ARG_KV_UNIFIED" not in env
141+
assert "LLAMA_ARG_N_PARALLEL" not in env
142+
assert env["PATH"] == "/usr/bin"
143+
144+
145+
def test_a_clean_environment_reports_nothing_dropped():
146+
env = {"PATH": "/usr/bin"}
147+
148+
assert LlamaCppBackend._drop_env_single_sequence(env) is False
149+
assert env == {"PATH": "/usr/bin"}
150+
151+
152+
def test_the_fit_recovery_rungs_stand_down_for_this_refusal():
153+
"""A startup crash, so the --fit rungs would take it first and waste a load."""
154+
import inspect
155+
156+
src = inspect.getsource(LlamaCppBackend.load_model)
157+
assert "_capability_crash = _tensor_capability_crash or self._is_kv_unified_refused" in src
158+
# Every --fit rung, and only those: the HIP rung keeps its narrower gate.
159+
assert src.count("and not _capability_crash") == 3
160+
assert src.count("and not _tensor_capability_crash") == 1
161+
162+
163+
def test_the_retry_commits_the_one_slot_geometry_it_launched():
164+
# Committed after the ladder, so the retry must overwrite what it reverses.
165+
import inspect
166+
167+
src = inspect.getsource(LlamaCppBackend.load_model)
168+
start = src.index('label = "-single-seq"')
169+
block = src[src.rindex("cmd = _kvu_cmd", 0, start) : start]
170+
assert "n_parallel = 1" in block
171+
assert "kv_cache_unified = False" in block

studio/backend/tests/test_tp_vision_regression.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -617,23 +617,23 @@ def test_fit_off_retry_skipped_on_a_tensor_capability_crash():
617617
"""The fit-independent --fit off retry is skipped on the split-axis marker, else
618618
the model crashes a second time before the latch records it (reviewer.py, #6659).
619619
620-
The same skip now also covers the pre-b9455 refusal of a quantized KV cache in
621-
tensor mode (ggml-org/llama.cpp#23792): both are capabilities the binary lacks,
622-
so a second spawn to let it offload cannot help and costs a full model load.
623-
Hence the guard's name is _tensor_capability_crash rather than the split-axis
624-
one it started as.
620+
It now also covers the pre-b9455 quantized-KV refusal in tensor mode
621+
(ggml-org/llama.cpp#23792) and the unified-cache refusal: no second spawn helps
622+
any of the three, and each costs a full model load. Hence _capability_crash,
623+
with _tensor_capability_crash left as the half the ROCm rung gates on.
625624
"""
626625
src = inspect.getsource(LlamaCppBackend.load_model)
627626
retry = src.find('run_cmd = [*run_cmd, "--fit", "off"]')
628627
assert retry != -1
629628
guard = src[max(0, retry - 1000) : retry]
630629
assert "_fit_retry_allowed" in guard and "_startup_crashed" in guard
631630
assert (
632-
"not _tensor_capability_crash" in guard
633-
), "the fit-off retry must be skipped when the crash is a tensor capability limit"
634-
# Both markers feed it, so neither can be dropped without this failing.
631+
"not _capability_crash" in guard
632+
), "the fit-off retry must be skipped when the crash is a capability limit"
633+
# All three markers feed it, so none can be dropped without this failing.
635634
assert "_is_tensor_split_assert" in src
636635
assert "_is_tensor_quant_kv_unsupported" in src
636+
assert "_is_kv_unified_refused" in src
637637

638638

639639
def test_is_abort_exit_recognizes_windows_crt_abort():

0 commit comments

Comments
 (0)