Studio: keep K and V equal when the flash-attention retry resets the V cache - #8827
Conversation
…V cache
llama-context.cpp rejects different K and V cache types on an MLA model,
and that check sits above the V-quantization one, so it decides first:
if ((model->hparams.is_mla() || model->arch == LLM_ARCH_DEEPSEEK4)
&& params.type_k != params.type_v) {
LLAMA_LOG_ERROR("model does not support different K (%s) and V (%s)
cache types");
return nullptr;
}
is_mla() covers DeepSeek V2/V3/R1, Kimi K2 and GLM-4.7/5.x, all of which
Studio already recognises through kv_lora_rank.
_reset_quantized_v_cache resets a quantized V cache to f16 and leaves K
quantized on purpose, because a quantized K needs no flash attention and
resetting it enlarges the cache for no reason. On an MLA model that leaves
K=q8_0 V=f16, which aborts for a different reason than the one being
avoided: the crash-recovery rung that exists to recover from a
flash-attention failure now fails on the K/V mismatch instead of
recovering. Someone on DeepSeek or Kimi with a quantized KV cache hits the
FA crash the rung was written for, and the recovery cannot start either.
Both axes now go to f16 together on an MLA model. Equal is what the rule
asks for, and f16 is the only value that also satisfies the V rule. Non-MLA
launches are untouched: the size argument for leaving K alone still holds
everywhere else, and the parameter defaults to the old behaviour.
The reachable path is the pre-existing one, _with_flash_attn_off. The
launch-site reset added recently shares the helper and is fixed by the same
change, though on a genuine binary it is close to unreachable: -fa landed
2024-04-30 and quantized KV a month later, so no upstream build has ever
accepted a quantized -ctv while lacking --flash-attn.
Also fixes the test harness this exposed. _flagless_v_cache_fixup bound the
class as `self`, so the block reading self._kv_lora_rank raised
AttributeError in the harness rather than failing on its merits. It now
binds a shim carrying the instance state and falling through to the class,
which is what the block actually runs against.
Tests: 10 new. 87 in the two suites that own this, 2159 across the
llama.cpp, flash-attention, cache-type, launch-flag and Metal suites. The
three video failures are in the origin/main baseline.
Not verified: no MLA GGUF was launched against a real llama-server. The
upstream rule is read from llama-context.cpp and the behaviour is verified
against the shipped code executed directly.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2b395dc843
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "--cache-type-k-draft", | ||
| "--spec-draft-type-k", | ||
| "-ctkd", |
There was a problem hiding this comment.
Keep non-MLA draft K caches quantized
When an MLA target uses a separate non-MLA drafter, such as the supported Gemma drafter path, mla is derived only from the target's _kv_lora_rank, but this list also rewrites the drafter's K flags. The K/V equality restriction is evaluated against the drafter's own model, so changing its quantized K cache to f16 is unnecessary and can substantially increase memory use during the flash-attention fallback, potentially turning the recovery into an OOM. Determine MLA status separately for the draft model or exclude draft K flags from the target-MLA rewrite.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct, and it is worse than stated in the other direction. Fixed in 9328343.
The target and the drafter are separate models with separate contexts and llama.cpp applies the restriction per context, so gating the draft flags on the target's answer was wrong both ways: an MLA target with a non-MLA drafter needlessly doubles the drafter's K cache, which is the OOM the size argument exists to avoid, and a non-MLA target with an MLA drafter leaves the draft K quantized against an f16 draft V and aborts the draft context outright.
Each side is now gated on its own metadata. Studio already reads the drafter's GGUF via _draft_backend_for for KV sizing, so the drafter's own signal was available. When the drafter cannot be read the draft signal is None and falls back to the target, which is the safe direction: an unnecessary reset costs memory, a missing one aborts.
| cmd = self._reset_quantized_v_cache( | ||
| cmd, | ||
| "this build has no --flash-attn", | ||
| mla = self._kv_lora_rank is not None, |
There was a problem hiding this comment.
Include DeepSeek4 in the symmetry signal
For a DeepSeek4 GGUF without attention.kv_lora_rank, this passes mla=False even though the upstream restriction quoted in this change explicitly applies when model->arch == LLM_ARCH_DEEPSEEK4 independently of is_mla(). A quantized-K/V launch that reaches the flash-attention fallback therefore still becomes K=q8_0/V=f16 and aborts instead of recovering. Include self._architecture == "deepseek4" when deriving this signal at every call site.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed against the upstream source and fixed in 9328343.
The arch term is not redundant with is_mla(). is_mla() is n_embd_head_k_mla_impl and n_embd_head_v_mla_impl both being non-zero (llama-hparams.cpp), and only deepseek2, deepseek32, glm-dsa, kimi-k3 and kimi-linear set those. DeepSeek4 has its own KV cache in llama-kv-cache-dsv4.cpp and sets neither, which is exactly why upstream needed the explicit arch clause.
The converter side confirms the kv_lora_rank probe cannot see it: DeepseekV4Model subclasses TextModel rather than DeepseekV2Model, and add_kv_lora_rank is only called in DeepseekV2Model. DeepSeek4 writes q_lora_rank and attention_output_lora_rank and no kv_lora_rank at all.
_requires_symmetric_kv now mirrors the upstream condition. The sweep carries a deepseek4 model and scores llama.cpp's ground truth separately from what Studio infers, and DeepSeek4 picks up the same 8064 fixed launches as the is_mla() models.
| self._with_flash_attn_off( | ||
| _last_spawn_cmd, mla = self._kv_lora_rank is not None |
There was a problem hiding this comment.
Preserve MLA symmetry for environment-only cache types
When an MLA launch inherits equal quantized K/V types through LLAMA_ARG_CACHE_TYPE_K and LLAMA_ARG_CACHE_TYPE_V, this call can only rewrite argv; the recovery block immediately afterward drops the quantized V environment variable while preserving K. The retry consequently runs with K=q8_0 and the default V=f16 and hits the same MLA mismatch abort this change is intended to prevent. On MLA retries, clear the corresponding quantized K environment variable whenever the V variable is cleared, including the draft pair and the flagless-build path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct. This was already fixed in 191c0c4, which landed after the commit this review is anchored to (2b395dc).
_drop_env_quantized_v_cache now takes the K vars down with the V ones, and the argv reset no longer makes the K rewrite conditional on having just reset V there, which covers the argv-K-only shape: -ctk q8_0 on the command line with LLAMA_ARG_CACHE_TYPE_V in the environment, where nothing quantized appears on argv for V so a V-triggered reset never fires. As of 9328343 the draft var is gated on the drafter's own model rather than the target's.
Found independently by the same sweep before the review landed, which is a good sign for both.
for more information, see https://pre-commit.ci
…-only paths
The first pass only lowered the K cache when it had just lowered a quantized V
on argv. A 43k-scenario sweep over [Windows, Linux, WSL, macOS] x [NVIDIA, AMD,
CPU] x [MLA, non-MLA] x KV combinations x flag spellings x env combinations x
main/draft found two shapes that still reached the "model does not support
different K and V cache types" abort:
- V quantized only through LLAMA_ARG_CACHE_TYPE_V with -ctk q8_0 on argv.
llama.cpp applies env before parsing argv, so nothing quantized appears on
argv for V and the V-triggered K reset never fires. The env drop then takes
V to f16 and leaves K at q8_0.
- A quantized K env var inherited by the flash-attn-off retry. The drop helper
deliberately preserved it, which is right off MLA (a quantized K runs fine
without flash attention and resetting it enlarges the cache) but reintroduces
the mismatch on MLA.
On this path V always ends up f16 regardless of where it came from, so on MLA the
K reset is no longer conditional on a V reset having happened, and the env drop
takes the K vars down with the V ones. Non-MLA behaviour is unchanged: the sweep
records zero non-MLA outcome differences across all 43008 scenarios.
Sweep result against the merge base: 8064 launches that aborted now start, zero
regressions, and zero remaining aborts where Studio itself introduced the
asymmetry (the base had 3024). The 1008 that still abort are all a user asking
for bf16 against f16 directly, which MLA rejects regardless of Studio.
Also corrects a test that asserted a lone quantized argv K should be preserved on
MLA. That config is not symmetric: V falls back to the f16 default, so llama.cpp
rejects it. Lowering K is what lets the retry start.
for more information, see https://pre-commit.ci
Two follow-ups from review, both confirmed against the upstream source. DeepSeek4 was missed. The restriction is `is_mla() || arch == LLM_ARCH_DEEPSEEK4` and the arch term is not redundant: is_mla() is n_embd_head_k_mla_impl and n_embd_head_v_mla_impl both being set, DeepSeek4 sets neither (it has its own KV cache in llama-kv-cache-dsv4.cpp), and DeepseekV4Model in the converter subclasses TextModel rather than DeepseekV2Model, so it writes q_lora_rank and attention_output_lora_rank but never kv_lora_rank. A kv_lora_rank probe therefore cannot see it, and a quantized-KV DeepSeek4 launch reaching the flash-attention fallback still aborted. _requires_symmetric_kv now mirrors the upstream condition. The target and the drafter are separate models with separate contexts, and llama.cpp applies the restriction per context, so each side is now gated on its own metadata via _draft_backend_for. Reusing the target's answer was wrong in both directions: an MLA target with a non-MLA drafter needlessly doubled the drafter's K cache, which is the OOM the size argument exists to avoid, and a non-MLA target with an MLA drafter left the draft K quantized against an f16 draft V and aborted the draft context. When the drafter's GGUF cannot be read the draft signal is None and falls back to the target, which is the safe direction: an unnecessary reset costs memory, a missing one aborts. The sweep now carries a deepseek4 model and scores llama.cpp's ground truth separately from what Studio infers, so the gap is measured rather than assumed. Across 64512 scenarios: 16128 launches that aborted now start (8064 MLA, 8064 DeepSeek4), zero regressions, zero non-MLA outcome differences, and zero remaining aborts where Studio introduced the asymmetry. The 2016 that still abort are a user asking for bf16 against f16 directly, which the rule rejects regardless.
|
@codex review |
1 similar comment
|
@codex review |
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fe2efa89d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| """ | ||
| target = self._target_kv_symmetry() | ||
| draft: Optional[bool] = None | ||
| drafter_path = getattr(self, "_mtp_draft_path", None) |
There was a problem hiding this comment.
Derive symmetry from the drafter that actually launches
When extra_args supplies its own --model-draft/--hf-repo-draft, that model is the drafter loaded by llama.cpp, but this lookup uses only self._mtp_draft_path, which is assigned from launch_mtp_draft_path and may be absent or point to an auto-resolved sidecar that _build_speculative_flags does not emit. Consequently, a non-MLA target with a user-supplied MLA drafter leaves the draft K cache quantized when V is reset and the recovery still aborts; the inverse combination can unnecessarily expand a non-MLA drafter's K cache. The new per-model gating is fresh evidence beyond the earlier comment, but its signal must be derived from the effective draft flag/path in the launch command rather than this stored managed path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct, and the stored path was wrong for a second reason you did not name. Fixed in 0334942.
self._mtp_draft_path is assigned at the end of the launch path, well after the launch-site reset runs, so at that call site it held whatever the previous load left, or nothing at all. So it was stale there even with no extra_args involved.
The signal now comes from the command being launched rather than any stored path, via the existing _extra_args_mtp_draft_source, which already resolves the local and HF draft flags last-wins and falls back to LLAMA_ARG_SPEC_DRAFT_MODEL. _build_speculative_flags emits --model-draft into that same command, so one source covers the managed sidecar and a user-supplied drafter alike, and every call site passes the command it is about to launch or retry. An HF repo id is not a local file whose metadata can be read, so it returns None and falls back to the target, as does an unreadable or absent drafter.
I removed _kv_symmetry_signals rather than leaving it beside the new path, since a second way to derive the same signal is how the stale source got used to begin with.
…mand The per-model gating read self._mtp_draft_path, which is the wrong source twice over. It only ever holds the managed sidecar, so an extra_args --model-draft or --hf-repo-draft, which is the drafter llama.cpp actually loads, was invisible to it. It is also not assigned until well after the launch-site reset has already run, so at that call site it held whatever the previous load left, or nothing. The signal now comes from the command being launched, via the existing _extra_args_mtp_draft_source, which already resolves the local and Hugging Face draft flags last-wins and falls back to LLAMA_ARG_SPEC_DRAFT_MODEL. The managed path emits --model-draft <sidecar> into that same command, so one source covers both the managed sidecar and a user-supplied drafter, and every call site now passes the command it is about to launch or retry. A Hugging Face repo id is not a local file whose metadata can be read, so it yields None and falls back to the target, as does an unreadable or absent drafter. _kv_symmetry_signals is removed rather than left beside the new path: a second way to derive the same signal is how the stale one got used in the first place. Sweep unchanged at 64512 scenarios: 16128 fixed, zero regressions, zero non-MLA outcome differences, zero remaining Studio-created asymmetries.
|
@codex review |
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c5f22931b8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| cmd, | ||
| "this build has no --flash-attn", | ||
| mla = self._target_kv_symmetry(), | ||
| draft_mla = self._draft_kv_symmetry(cmd, env), |
There was a problem hiding this comment.
Initialize the child environment before inspecting the drafter
When the capability probe authoritatively reports a build without --flash-attn, _flash_attn_known_off is true and this branch executes before env is assigned at line 15759. Because env is a local variable in load_model, evaluating this argument raises UnboundLocalError, so every load through the flagless-build compatibility path fails before spawning llama-server. Construct the child environment before this fixup or pass an already initialized environment view.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed in 7e23c97. This one was mine, introduced in 0334942.
Verified with an AST walk over load_model rather than by eye: exactly one Load of env sits before the first Store, at the line this comment is on. The apparent earlier binding a few lines up is a keyword argument (env = _mem_env inside a call), not an assignment, so there is no earlier binding to save it. On a build whose probe authoritatively reports no --flash-attn the branch does execute, so the flagless-build path would raise before llama-server was ever spawned.
The lookup no longer takes env there. os.environ is the correct source at that point anyway, since Studio never rewrites LLAMA_ARG_SPEC_DRAFT_MODEL and the child env is a copy of it plus loader paths.
Worth recording why the tests missed it: the block-extraction harness the flagless tests use seeds env into its exec scope, so it passed while the real function raised. I replaced that with a static check on load_model itself and confirmed it fails on the broken code and passes on the fix, rather than assuming a new test works.
| if draft_mla is None: | ||
| draft_mla = mla |
There was a problem hiding this comment.
Reset draft K conservatively when drafter metadata is unknown
When a non-MLA target launches an MLA drafter through --hf-repo-draft, or a local drafter whose metadata cannot be read, _draft_kv_symmetry returns None and this fallback converts it to the target's False. A flash-attention retry then resets the quantized draft V cache to f16 while leaving draft K quantized, causing the drafter context to abort on the K/V mismatch. Fresh evidence after the earlier thread is that the current implementation explicitly returns None for every remote drafter and still maps that unknown result to the target signal; unknown drafters should take the conservative symmetric path instead.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Agreed, fixed in 7e23c97. The implementation contradicted its own stated rationale: the docstring argued that an unnecessary reset only costs memory while a missing one aborts, and then mapped the unknown case to the target's possibly-False answer.
A named drafter whose metadata cannot be read now resolves to True, covering both the Hugging Face repo id and the unreadable local GGUF. None is returned only when no drafter is named at all, where there are no draft flags to reset and the value cannot matter.
This is consistent with the earlier thread rather than a reversal of it: that one was about a KNOWN non-MLA drafter, where the size argument applies and the draft K is left quantized. Only the unknown case takes the conservative path.
…ervative Two defects in the previous commit. The launch-site reset passed env to the drafter lookup, but the child environment is not built until several lines below, and env is a local of load_model. On a build whose capability probe authoritatively reports no --flash-attn the branch does execute, so every load through the flagless-build path would raise UnboundLocalError before llama-server was ever spawned. The lookup no longer takes env there and falls back to os.environ, which is the correct source at that point anyway since Studio never rewrites LLAMA_ARG_SPEC_DRAFT_MODEL. The block-extraction harness the flagless tests use seeds env into its exec scope, so it passed while the real function raised. The replacement is a static check on load_model itself, confirmed to fail on the broken code and pass on the fix. Second, an unknown drafter now takes the conservative symmetric path instead of the target's answer. A named drafter whose metadata cannot be read, a Hugging Face repo id or an unreadable GGUF, resolves to True. The two mistakes are not symmetric: resetting a non-MLA drafter's K needlessly costs memory, while failing to reset an MLA drafter's K leaves it quantized against an f16 draft V and aborts the draft context, which is the failure this path exists to prevent. None is now returned only when no drafter is named at all, where there are no draft flags to reset and the value cannot matter. Sweep unchanged: 64512 scenarios, 16128 fixed, zero regressions, zero non-MLA outcome differences.
|
@codex review |
1 similar comment
|
@codex review |
for more information, see https://pre-commit.ci
|
@codex review |

The rule
llama-context.cpprejects different K and V cache types on an MLA model, and that check sits above the V-quantization check, so it decides first:is_mla()covers DeepSeek V2/V3/R1, Kimi K2 and GLM-4.7/5.x, all of which Studio already recognises throughkv_lora_rank.What goes wrong
_reset_quantized_v_cacheresets a quantized V cache to f16 and leaves K quantized on purpose: a quantized K needs no flash attention, and resetting it enlarges the cache for nothing. That reasoning predates the MLA rule.On an MLA model the result is
K=q8_0 V=f16, which is a hard abort for a different reason than the one being avoided. The crash-recovery rung exists to recover from a flash-attention failure, and now it cannot start either. Someone on DeepSeek or Kimi with a quantized KV cache hits the FA crash the rung was written for, and the recovery attempt dies on the K/V mismatch.K=q8_0 V=f16(abort)K=f16 V=f16K=q8_0 V=f16Both axes go to f16 together on MLA. Equal is what the rule asks for, and f16 is the only value that also satisfies the V rule. Non-MLA is untouched, and the parameter defaults to the old behaviour so an un-updated caller keeps today's answer.
Reachability, ranked honestly
_with_flash_attn_off, pre-existing. This is the reachable one. It has always produced the asymmetry; the upstream rule turned it fatal.-falanded 2024-04-30 and quantized KV support a month later, so no upstream build has ever accepted a quantized-ctvwhile lacking--flash-attn. It still matters for a wrapper that hides-fafrom--help.So this is not a regression from #8710. It extended a helper whose K-untouched policy had become unsafe upstream.
A test-harness fix this exposed
_flagless_v_cache_fixupbound the class asself, so the block readingself._kv_lora_rankraisedAttributeErrorinside the harness rather than failing on its merits. It now binds a shim that carries instance state and falls through to the class. Any futureself.<attr>in that block will work rather than break the harness.Tests
10 new. 87 pass in the two suites that own this, 2159 across the llama.cpp, flash-attention, cache-type, launch-flag and Metal suites. Coverage includes the inline
=spelling, the draft pair, an unquantized KV (nothing moves), a quantized K alone (the MLA branch must not fire, since no V reset happened), and the non-MLA default.Three failures in
test_video_backend.pyandtest_video_prequant.pyare present onmainunchanged.Not verified
No MLA GGUF was launched against a real
llama-server. The upstream rule is read fromllama-context.cppand the behaviour is verified against the shipped code executed directly. Worth a real DeepSeek or Kimi launch before this is trusted end to end.