put the KV levers on the model page, and state the block count both ways

Four settings decide whether a GGUF load fits in host RAM at all, and none of
them could be set from the model page — only by editing models.json by hand. The
placement check, how sliding-window layers reserve, the measured size the check
acts on, and the MTP-block repair are now on the page with the rest of the KV
cache, gated to GGUF models, and in the savable list that is where a field
quietly stops persisting. measured_kv_gb has been written by hand as a
{n_ctx: gb} map and is now journalled as a plain number, so the form renders
either shape instead of "[object Object]".

The MTP override now also sets nextn_predict_layers to 0 rather than correcting
block_count alone. Builds disagree on whether block_count includes the NextN
blocks: the one this was written against does not subtract them (it asked for
blk.64 of a 65/1 file), and a build that does would read a corrected 64 as 63 and
quietly leave a real layer out of the model. "64 blocks, none of them NextN" is
true under both readings.

The journal also takes its directory from platform_paths rather than inventing an
environment variable, so a deployment keeps one home.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
parent 16488fa5
......@@ -2981,6 +2981,11 @@ async def api_model_configure(request: Request, username: str = Depends(require_
"component_quantization", "output_crf", "force_vram_update",
"balanced_gpu_percent", "acceleration",
"cache_type_k", "cache_type_v", "kv_offload", "n_batch", "n_ubatch", "n_seq_max",
# The KV cache's size decides whether a load fits in host RAM at
# all, so the levers that govern it belong on the model page:
# whether the placement check runs, how sliding-window layers
# reserve, the measured size it acts on, and the MTP-block repair.
"kv_autoplan", "swa_full", "measured_kv_gb", "mtp_block_fix",
"embed_max_concurrency", "embed_max_backlog", "embed_min_interval_ms",
"gpu_split", "tensor_split", "split_strategy", "split_secondary_cap_gb",
"turboquant", "engine", "engine_fallback",
......
......@@ -1374,6 +1374,36 @@ window.__DEFAULT_WHISPER_SERVER_PATH__ = {{ default_whisper_server_path|tojson }
<option value="q4_0">q4_0 (smallest)</option>
</select>
</div>
<div class="form-row" style="margin:0" id="cfg-kv-autoplan-row">
<label class="form-label">KV placement check <span class="muted">(GGUF text)</span></label>
<select id="cfg-kv-autoplan" class="form-input">
<option value="">On (recommended)</option>
<option value="0">Off — load even if the KV cache cannot fit</option>
</select>
<span class="form-hint" style="font-size:11px">Before loading, checks the KV cache against free VRAM and host RAM and picks a placement that fits. A pinned <b>Keys</b>/<b>Values</b> type is never changed and a pinned <b>KV offload</b> is never moved — but if nothing fits, the load is <b>refused</b> rather than left to the kernel's OOM killer, which takes the whole engine and every other model on it. Turning this off restores the old behaviour.</span>
</div>
<div class="form-row" style="margin:0" id="cfg-swa-full-row">
<label class="form-label">Sliding-window KV <span class="muted">(GGUF; llama.cpp swa_full)</span></label>
<select id="cfg-swa-full" class="form-input">
<option value="">Auto (decided by the placement check)</option>
<option value="1">Full — sliding layers reserve the whole context</option>
<option value="0">Window only — smaller KV, no quality cost</option>
</select>
<span class="form-hint" style="font-size:11px">On a sliding-window model (Gemma's 1024-token window) the sliding layers can reserve the full context or just their window. <b>Window only</b> costs no quality — it limits context reuse between requests, not attention — so it is the first lever tried when the KV does not fit.</span>
</div>
<div class="form-row" style="margin:0" id="cfg-measured-kv-row">
<label class="form-label">Measured KV size <span class="muted">(GB, for this n_ctx)</span></label>
<input type="number" id="cfg-measured-kv" class="form-input" min="0" step="0.1" placeholder="auto — measured and remembered on load">
<span class="form-hint" style="font-size:11px">What the KV cache really costs at the configured context. Measured from llama.cpp's own report and remembered automatically, so this is normally blank; set it to override that. Predicting it from model metadata does not work (it was off 5× in both directions), which is why the placement check acts only on a measured number.</span>
</div>
<div class="form-row" style="margin:0" id="cfg-mtp-fix-row">
<label class="form-label">Repair dropped MTP blocks <span class="muted">(GGUF text)</span></label>
<select id="cfg-mtp-fix" class="form-input">
<option value="">On (recommended)</option>
<option value="0">Off — fail on the missing block</option>
</select>
<span class="form-hint" style="font-size:11px">Some quantizers drop a checkpoint's NextN (multi-token-prediction) blocks but leave <code>block_count</code> counting them, so llama.cpp asks for a block the file does not have and reports <code>missing tensor 'blk.N.attn_norm.weight'</code> — which looks like a corrupt download. When the file's shape proves that is what happened, the declared count is corrected and the model serves as its dense stack. A file missing blocks for any other reason still fails.</span>
</div>
<div class="form-row" style="margin:0" id="cfg-mmproj-row">
<label class="form-label">Multimodal projector <span class="muted">(mmproj → vision)</span></label>
<select id="cfg-mmproj" class="form-input" onchange="_onMmprojChange()">
......@@ -4160,6 +4190,10 @@ function _applyBackendCfgVisibility(m){
show('cfg-gpu-layers-row', isGguf);
show('cfg-kv-k-row', isGguf);
show('cfg-kv-v-row', isGguf);
show('cfg-kv-autoplan-row', isGguf);
show('cfg-swa-full-row', isGguf);
show('cfg-measured-kv-row', isGguf);
show('cfg-mtp-fix-row', isGguf);
// mmproj pairing only applies to GGUF text models on llama.cpp.
show('cfg-mmproj-row', isGguf);
}
......@@ -4369,6 +4403,19 @@ function openCfgModal(idx, cfgIdx){
document.getElementById('cfg-embed-min-interval-ms').value = s.embed_min_interval_ms != null ? s.embed_min_interval_ms : '';
document.getElementById('cfg-cache-type-k').value = s.cache_type_k || '';
document.getElementById('cfg-cache-type-v').value = s.cache_type_v || '';
document.getElementById('cfg-kv-autoplan').value = (s.kv_autoplan === false) ? '0' : '';
document.getElementById('cfg-swa-full').value =
(s.swa_full === true) ? '1' : (s.swa_full === false) ? '0' : '';
// Stored either as a plain number for the current n_ctx, or as a
// {n_ctx: gb} map from an older hand-written config; show the entry that
// applies rather than "[object Object]".
(function(mk, ctx){
var el = document.getElementById('cfg-measured-kv');
if (mk === null || mk === undefined) { el.value = ''; return; }
if (typeof mk === 'object') { el.value = (mk[String(ctx)] ?? mk[ctx] ?? ''); return; }
el.value = mk;
})(s.measured_kv_gb, s.n_ctx);
document.getElementById('cfg-mtp-fix').value = (s.mtp_block_fix === false) ? '0' : '';
_populateMmprojSelect(m, s);
document.getElementById('cfg-max-instances').value = s.max_instances != null ? s.max_instances : 1;
document.getElementById('cfg-preload-all-instances').checked = !!s.preload_all_instances;
......@@ -5482,6 +5529,12 @@ async function saveModelConfig(){
embed_min_interval_ms: document.getElementById('cfg-embed-min-interval-ms').value === '' ? null : parseInt(document.getElementById('cfg-embed-min-interval-ms').value),
cache_type_k: document.getElementById('cfg-cache-type-k').value || null,
cache_type_v: document.getElementById('cfg-cache-type-v').value || null,
kv_autoplan: document.getElementById('cfg-kv-autoplan').value === '0' ? false : null,
swa_full: (function(v){ return v === '' ? null : v === '1'; })(
document.getElementById('cfg-swa-full').value),
measured_kv_gb: document.getElementById('cfg-measured-kv').value === ''
? null : parseFloat(document.getElementById('cfg-measured-kv').value),
mtp_block_fix: document.getElementById('cfg-mtp-fix').value === '0' ? false : null,
mmproj: document.getElementById('cfg-mmproj').value || null,
auto_compact: document.getElementById('cfg-autocompact').checked,
auto_compact_pct: parseInt(document.getElementById('cfg-autocompact-pct').value) || 85,
......
......@@ -107,10 +107,19 @@ def read_layout(path) -> dict:
def block_count_override(path):
"""``{"<arch>.block_count": n}`` when a GGUF's NextN blocks were dropped.
Returns ``None`` whenever the file's shape does not prove that — including a
file that is simply missing blocks, which must keep failing.
"""Metadata overrides that make a de-MTP'd GGUF load as what it is.
Returns ``{"<arch>.block_count": n, "<arch>.nextn_predict_layers": 0}``, or
``None`` whenever the file's shape does not prove the NextN blocks were
dropped — including a file that is simply missing blocks, which must keep
failing.
Both keys are set, not just the count. Whether llama.cpp loads
``block_count`` blocks or ``block_count - nextn_predict_layers`` of them has
changed between builds: the build this was written against does the former
(it asked for ``blk.64`` of a 65/1 file), and a build doing the latter would
read a corrected count of 64 as 63 and quietly leave a real layer out of the
model. Saying "64 blocks, none of them NextN" is true under both.
"""
layout = read_layout(path)
if not layout or not layout.get('arch'):
......@@ -124,7 +133,8 @@ def block_count_override(path):
return None
if declared - present != nextn:
return None
return {f"{layout['arch']}.block_count": present}
return {f"{layout['arch']}.block_count": present,
f"{layout['arch']}.nextn_predict_layers": 0}
def explain(path) -> str:
......
......@@ -31,9 +31,14 @@ _PATH_OVERRIDE = None
def journal_path() -> str:
"""Next to the config it supplements, so a deployment keeps one home."""
if _PATH_OVERRIDE:
return _PATH_OVERRIDE
base = os.environ.get('CODERAI_HOME') or os.path.expanduser('~/.coderai')
try:
from codai.platform_paths import legacy_style_config_dir
base = str(legacy_style_config_dir())
except Exception:
base = os.path.expanduser('~/.coderai')
return os.path.join(base, 'cache', 'kv_sizes.json')
......
......@@ -76,7 +76,22 @@ def test_quant_that_dropped_the_nextn_block_is_corrected(tmpgguf):
'qwen35.block_count': 65,
'qwen35.nextn_predict_layers': 1},
['token_embd.weight'] + _blocks(0, 64))
assert gguf_mtp.block_count_override(p) == {'qwen35.block_count': 64}
assert gguf_mtp.block_count_override(p) == {'qwen35.block_count': 64,
'qwen35.nextn_predict_layers': 0}
def test_the_override_also_zeroes_nextn_so_the_count_is_read_the_same_way(tmpgguf):
"""Builds disagree on whether block_count includes the NextN blocks. A build
that subtracts them would read a corrected 64 as 63 and drop a real layer, so
the override states both halves."""
p = tmpgguf('mtp3.gguf',
{'general.architecture': 'qwen35',
'qwen35.block_count': 65,
'qwen35.nextn_predict_layers': 1},
['token_embd.weight'] + _blocks(0, 64))
over = gguf_mtp.block_count_override(p)
assert over['qwen35.nextn_predict_layers'] == 0
assert over['qwen35.block_count'] == 64
def test_intact_model_needs_no_override(tmpgguf):
......@@ -346,3 +361,53 @@ def test_modules_have_no_heavy_imports():
elif isinstance(node, ast.ImportFrom) and node.module:
names.add(node.module.split('.')[0])
assert not (names & {'torch', 'llama_cpp', 'transformers', 'diffusers'}), names
# ── 5. The knobs are reachable from the model page ───────────────────────────
#
# A lever the loader reads but the model page cannot set is a lever that only
# exists for whoever edits models.json by hand. All four of these decide whether
# a load fits in host RAM at all, so they belong in the UI — and the savable-list
# in the admin routes is where a field silently stops persisting.
ROUTES = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'codai', 'admin', 'routes.py')
MODELS_HTML = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'codai', 'admin', 'templates', 'models.html')
KV_FIELDS = ('kv_autoplan', 'swa_full', 'measured_kv_gb', 'mtp_block_fix')
@pytest.mark.parametrize('field', KV_FIELDS)
def test_field_is_savable(field):
assert f'"{field}"' in open(ROUTES).read(), f'{field} is dropped on save'
@pytest.mark.parametrize('field', KV_FIELDS)
def test_field_is_sent_by_the_form(field):
assert f'{field}:' in open(MODELS_HTML).read(), f'{field} is never posted'
@pytest.mark.parametrize('el', ('cfg-kv-autoplan', 'cfg-swa-full',
'cfg-measured-kv', 'cfg-mtp-fix'))
def test_control_exists_is_populated_and_is_gguf_only(el):
html = open(MODELS_HTML).read()
assert f'id="{el}"' in html, f'{el} has no control'
assert f"getElementById('{el}')" in html, f'{el} is never read or written'
assert f"show('{el}-row'" in html or f"show('{el.replace('cfg-', 'cfg-')}-row'" in html \
or f"'{el}-row'" in html, f'{el} row is not gated to GGUF models'
def test_the_loader_reads_each_field_it_offers():
"""The other half of the round trip: the UI must not offer a setting the
loader ignores."""
src = open(SRC).read()
for field in ('kv_autoplan', 'swa_full', 'measured_kv_gb', 'mtp_block_fix'):
assert f"'{field}'" in src, f'{field} is offered but never read'
def test_measured_kv_accepts_both_stored_shapes():
"""It has been written by hand as {n_ctx: gb} and is now journalled as a
plain number; the form must not render one of them as [object Object]."""
html = open(MODELS_HTML).read()
assert "typeof mk === 'object'" in html
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment