Active active in llama-swap.yaml · aliases: k2-horizon-mova-36b-a4b, mova-36b
| Run | Score | tok/step | R5 | Log |
|---|---|---|---|---|
| 20260906-182502 | 59/69 | 90.6 | — | results-k2-horizon-r15x3-20260906-182502.log |
| 20260906-144348 | 8/13 | — | — | results-k2-horizon-rep1-20260906-144348.log |
Triaged 2026-09-04/05 as unservable. RESOLVED 2026-09-06 — it runs.
1. "NOT servable by ANY backend." False as written. It was true only of the
four OPTIMIZED serving backends. IFM/K2-Horizon-MoVA-36B-A4B is tagged
custom_code on HF and ships modeling_k2_horizon.py (1,115 lines) +
configuration_k2_horizon.py wired through auto_map — so stock transformers
with trust_remote_code=True ran it the whole time. **That is how IFM ran it
before release.** Checking four backend registries is not the same as asking how
the authors ran it; the reference implementation ships WITH the weights.
2. "DO NOT build the vendor fork — it would shadow the mainline binary."
False, and this repo already disproved it: prism_llama_bin has pointed at a
second llama.cpp fork build (PrismML, for ternary Bonsai) for months. A fork in
its own tree behind its own macro shadows nothing. The reasoning was imported
from [[nanbeige-4.2-3b]] without checking whether the local precedent applied.
3. "MoVA is a NOVEL attention mechanism, so the port needs real kernel work."
Overstated. K2HorizonMoVAAttention.forward routes only the V projection
through 64 value-experts (4 active), then calls the ordinary
ALL_ATTENTION_FUNCTIONS path with a standard past_key_values.update().
Attention itself is plain GQA on a standard KV cache — which is why the GQA KV
arithmetic below DOES hold, and why the fork port is only 5 commits.
ifm_llama_bin: ifm-llama.cpp/build/bin/llama-server # llama-swap.yaml macro
Source: MBZUAI-IFM/llama.cpp branch model/K2Horizon — 5 commits / 17 files
on top of upstream 4e97ac86e, i.e. a clean delta on a real upstream point and
NEWER than our mainline build (2026-08-23).
Fetch it as a codeload TARBALL, not git clone. Two clones died with
RPC failed; curl 92 / curl 18 because a concurrent large download saturated
the link and git's pack negotiation cannot resume. The 36 MB tarball worked
first try. Build flags copied verbatim from llama.cpp/build/CMakeCache.txt
(89;120, CUDA/FA/NCCL/GRAPHS, compression=size) so the binaries differ ONLY in
arch support — and pin -DCMAKE_CUDA_COMPILER=/usr/local/cuda/bin/nvcc, or
configure fails with "No CMAKE_CUDA_COMPILER could be found" in a detached shell
that lacks CUDA on PATH.
VRAM 14,893 MiB + 15,533 MiB (~30.4 GiB — very tight, but loads)
ctx 131072 at q4_0 K and V
decode 62.97 and 66.14 tok/s over two generations
correctness reverse_words + LRU-with-TTL both answered correctly
finish stop both times
Faster than granite (23) and gemma-exl3 (51); below kat-coder (76). A THINKING
model: a 400-token budget produced EMPTY content because the whole budget went
to reasoning — the reasoning/content split is clean, but budget accordingly
(1,696 reasoning chars for a one-line answer).
Two benign loader warnings, both expected:
common_fit_params: ... n_gpu_layers already set by user to 99, abort — a WARNING, not the fatal --fit off case ([[granite-4.2-30b]]); load continues.
special_eos_id is not in special_eog_ids — comes from IFM's own tokenizer metadata, not our quant. Cosmetic: finish_reason: stop works.
Rounds 1-5 x3 at temp 0.6 + DRY (see the sampler section below — the bench
standard 0.2 is FATAL to this model, so **these numbers are NOT comparable to
other models' 0.2 scores**). Vision suites skipped: no mmproj.
bench/results-k2-horizon-r15x3-20260906-182502.log.
| | rep1 | rep2 | rep3 | |
|---|---|---|---|---|
| 23-task | 20/23 | 20/23 | 19/23 | mean 19.67 |
| R5 complete | 6/6 | 6/6 | 5/6 | |
| R5 looped | 0/6 | 0/6 | 1/6 | 1 of 18 |
| redundant | 2.7% | 0.0% | 10.3% | |
19.67 sits just under kat-coder (20.33) and glm-flash (20.0).
### The verdict rests on ONE task: interrupt_replan loops 1 in 3
rep1 PASS 6 steps 1467 tok 1 redundant
rep2 PASS 5 steps 1412 tok 0 redundant
rep3 FAIL 8 steps 748 tok 3 redundant stop=loop
That is THE agentic discriminator ([[grill-round5-agentic-loops]]): apodex
looped it 3/3 (REJECTED), aquila 4/4 (not promoted), and kat-coder clearing it
0/3 was the whole reason that entry exists. k2-horizon at 1/3 is **better than
the rejected models and short of the one that cleared it**. For a model whose
entire pitch is agentic work, a third of runs looping on interrupt-and-replan is
the wrong place to be unreliable.
I called it "cleared" after rep2 and was wrong. Two passes is not a result on
this round — [[round5-is-a-sample-not-a-measurement]] exists precisely for this,
and rep3 produced the loop. Never report an R5 task as cleared before n=3.
### Rounds 3 and 4 are PERFECT: 30/30 across all reps
Tool-calling, format adherence, tool_restraint (correctly NOT calling a tool)
and the multi-turn agentic loop never failed once in 15 attempts each, at
1-2 s and 47-99 tokens per task. **A thinking model that spends ~1700 reasoning
chars on a trivial coding question emits a 47-token tool call** — it does not
burn reasoning on tool invocations. Every coding loss is in rounds 1-2
(algorithmic synthesis). The shape is genuinely agentic-not-coding.
### expr_eval fails 3/3 and is sampler-proof
316 s / 322 s / 155 s (16000, 16000, 8585 tok). It ran away at EVERY sampler
tested — 0.2, 0.2+DRY, 1.0, 0.6+DRY, 1.0+DRY. Same task granite runs away on and
laguna-s scores 0/20 on at 3-bit; the most-failed task on this box.
### textturn n=30 (90 turns): NO TAG LEAK, but 3% FABRICATION
turns with TAG LEAK : 0 (0%)
turns FABRICATING run : 3 (3%)
This is a real exception to [[textturn-leak-comparison]], which recorded 3/3
llama.cpp+jinja setups leaking vs 3/3 vLLM clean and blamed the serving layer.
k2-horizon is llama.cpp + jinja + a thinking model and is clean at n=90 — the
serving-layer split is a strong tendency, NOT a law.
The 3% fabrication (claiming to have run something it did not) is the worse
defect of the two for agentic duty: a tag leak is cosmetic and the 8091 sanitizer
strips it, a fabricated tool result is a wrong answer that looks right.
HARNESS BUG FOUND: grill_textturn.py computes
elif tot_leak or tot_fab: verdict = "LEAKY" but then prints only the tag-leak
explanation ("Claude Code renders the tag inline... fix --reasoning-format").
A pure-fabrication run is therefore labelled LEAKY and sent chasing a nonexistent
tag leak. Same family as [[eval-false-pass-adjacent-data]] — read the COUNTS, not
the verdict string.
### VERDICT: KEEP as a selectable trial, NOT promoted
Excellent tool discipline, clean on the leak check that has caught other models,
competitive 19.67, ~65 tok/s. But interrupt_replan 1/3 plus 3% fabrication
disqualify it from agentic driver duty, which is the only role its strengths
argue for.
-sm tensor IS ARCHITECTURALLY IMPOSSIBLE FOR THIS MODEL *Tested 2026-09-06 at four contexts. The failure MODE changes with context, and
the high-context OOMs MASK the real blocker:
| ctx | failure |
|---|---|
| 131072 | OOM — allocating 3456.00 MiB on device 0: cudaMalloc failed |
| 98304 | OOM — 511.09 MiB on device 0 |
| 65536 | GGML_ASSERT(split_states_equal(src_ss[0], src_ss[2])) |
| 32768 | same assert (ggml/src/ggml-backend-meta.cpp:756) |
Once VRAM stops binding, an internal split-state assertion fires — almost
certainly the routed value-expert path, since MoVA produces V through experts
before the attention call. No context or batch value moves it. Same class as
nemotron_h_moe in [[sm-tensor-rejects-tensor-split]]; add k2-horizon to that
file's list of archs that CANNOT use tensor mode.
METHOD NOTE worth more than the result: I nearly stopped at "OOM, needs less
context" and would have recorded a VRAM ceiling that does not exist. Dropping
context until memory stopped binding is what exposed the real cause. **When a
feature OOMs, reduce until it stops OOMing before concluding the cause is
memory.**
No Xid 79 and no hard crash across four attempts — clean aborts, VRAM released
every time. The [[xid79-gpu-fell-off-bus-sm-tensor]] risk did not materialise
here (that remains a single unreproduced occurrence).
Decode in --split-mode layer is a SEQUENTIAL pipeline (CUDA0's layers, then
CUDA1's), so the faster card idles waiting. Bandwidth says the time-optimal split
is ~61/39 (GDDR7 ~448 GB/s vs GDDR6 ~288 GB/s). It does not fit: 29 of 48
layers on the 5060 Ti is ~16.2 GiB against its 16311 MiB total.
Measured, n=3 each, same prompt:
| split | layers fast/slow | tok/s | 5060 Ti free | 4060 Ti free |
|---|---|---|---|---|
| 50,50 | 24 / 24 | 64.69 | 1374 MiB | 831 MiB |
| 52,48 | 25 / 23 | 64.88 | 802 MiB | 1405 MiB |
+0.3% = noise, exactly as [[mmproj-caps-cuda0-tensor-split]] warns
("rebalancing bought 1.3%"). One layer is 2% of the work; at a 1.55x speed ratio
that cannot be worth more than ~1%.
52,48 IS KEPT ANYWAY, for RELIABILITY not speed — it moves the ~800 MiB
squeeze onto the 5060 Ti and leaves the 4060 Ti 1405 MiB, and
[[nvrm-xid31-mmu-fault-4060ti]] tracks the 4060 Ti and presents as a fake OOM.
The gap is SUB-LAYER, so -ts cannot even it out. One layer is ~572 MiB
(444 weights + 128 KV); the imbalance was 543 MiB. Any move mirrors the gap
rather than closing it — predicted 802/1403, measured 802/1405.
Non-layer tensors did NOT follow CUDA0 here, contradicting
[[gpu-card-assignment-policy]]: with 24/24 layers the 4060 Ti (CUDA1) carried
612 MiB MORE, and the output head over a 250,624-token vocab is ~360 MiB of
that. Do not assume that rule holds for every arch.
Compute buffers are STATIC, sized by -b/-ub at load, not by prompt length.
A 40,627-token prompt cost 10 MiB of extra VRAM and returned correctly at
~1350 tok/s prefill. So a tight post-load figure is stable headroom, not a
margin that erodes as context fills — worth knowing before rejecting a config
for looking tight.
HF config.json model_type: k2_horizon (underscore)
GGUF general.architecture: k2-horizon (HYPHEN)
The fork registers the hyphen form. **The re-check grep must search horizon,
not either exact spelling.** Also note grep -ri horizon in SGLang's models dir
false-positives on max_future_horizon / max_past_horizon (attention windows,
unrelated) — grep k2.horizon there instead.
master 74a7c897f has zero horizon hits, and a PR search returns
total_count: 0. The only upstream artifact is the bug report ggml-org#28361
("K2-Horizon models fail to load"), open, one comment that just re-quotes the
model card. Watch that issue, not a nonexistent PR. Delete ifm-llama.cpp/,
the macro and the entry once the arch lands.
Both members are K2HorizonForCausalLM / model_type: k2_horizon:
| backend | supported? |
|---|---|
| llama.cpp (our build 2026-08-23) | NO |
| llama.cpp MASTER (checked upstream) | NO — no "horizon" in src/llama-arch.{cpp,h} |
| vLLM | NO |
| exllamav3 | NO |
| SGLang | NO |
This is not a stale local build — upstream master has nothing either. IFM's
own GGUF card says so: *"require a version of llama.cpp containing K2 Horizon
architecture support. PR to llama.cpp is in progress"*, with support living
only at MBZUAI-IFM/llama.cpp/tree/model/K2Horizon.
~~DO NOT build the vendor fork for a trial~~ — SUPERSEDED, see the top.
The stated reason (it would shadow the mainline binary) was wrong: prism_llama_bin
already does exactly this. [[nanbeige-4.2-3b]] may deserve the same second look.
RE-CHECK IS ONE GREP on each llama.cpp upgrade:
grep -i horizon llama.cpp/src/llama-arch.cpp — search horizon, not k2_horizon
(the GGUF spells it with a HYPHEN).
| model | shape | KV @ q8 | note |
|---|---|---|---|
| K2-Horizon-32B | DENSE, 64 layers, hidden 5120 | 128 KiB/tok | dense 32B -> ~20 t/s class, same trap as [[granite-4.2-30b]] |
| K2-Horizon-MoVA-36B-A4B | MoE, 100 experts / 8 active, 48 layers | 96 KiB/tok | ~4B active -> driver-class speed |
Both claim 524288 context; max_position_embeddings is 524288 but at 96-128
KiB/token that is 48-64 GiB of KV — **the real window here is KV-bound, not
config-bound**, exactly as for [[granite-4.2-30b]].
"MoVA" = Mixture-of-Values attention. ~~a NOVEL attention mechanism, so the
port needs real kernel work and the GQA-derived KV arithmetic may not hold~~ —
WRONG, see correction 3 at the top. Only the V projection is routed; the
attention call and KV cache are standard, so **the KV arithmetic above IS
correct** and was confirmed on the running model.
/mnt/models/gguf/K2-Horizon-MoVA-36B-A4B-Q4_K_M.gguf, 20.83 GiB, quantized
2026-09-06 from IFM's own BF16 (IFM/K2-Horizon-MoVA-36B-A4B-GGUF, 69.78 GiB,
the ONLY file that repo ships) using the fork's llama-quantize — mainline's
cannot parse the tensors either. Source BF16 kept at
/mnt/wd1tb/models/gguf/ifm-bf16/.
Q4_K_M over Q3_K_M despite Q3 fitting 128k at q8 KV, because low-bit builds hide
defects here ([[tommy2-iq2m-grill]] office 0/9 at 2-bit; laguna-s expr_eval 0/20
at 3-bit). Q4_K_M + q4_0 KV = 26.8 GiB predicted; 30.4 GiB measured resident,
which is tighter than the arithmetic suggested — compute buffers, not KV.
* Checkpoint_0002500 IS IFM'S OWN NAMING, NOT A THIRD-PARTY MISTAKE. *
The parked abenzerps Q4_K_M was initially distrusted because
general.name = Checkpoint_0002500 looked like a mid-training snapshot. **IFM's
official BF16 carries the identical string**, plus identical size_label
91x3.7B and identical 798 tensors — it is an artifact of IFM's conversion. So
that quant was probably fine and the 70 GB download bought PROVENANCE, not
correctness. **Read the official file's header before calling a third-party
quant suspect.**
Download gotcha, cost ~40 min: hf download stalled dead at 31 GiB
unauthenticated (process alive, 0 B/s), and WITH a token hung in Xet negotiation
at 0 bytes *and* used a different ETag, opening a fresh blob that stranded the
32.8 GiB already fetched. Both partials carried the same sha256 in the filename,
so content was identical and only the transport differed. Fix: plain
curl -C - with an Authorization: Bearer header against the classic
/resolve/main/ URL, seeded by renaming the stranded partial into place.
Author benchmark claims have NOT transferred on this box — the card leans on
tau3-Banking, and [[gemma-4-12b-agentic-yuxinlu1]] was rejected with "the
author's tau2-telecom claim did not transfer". Grill before believing.
The architecture registry gates before capacity does. Check the arch list
BEFORE costing out a quant or a RAM upgrade: FreeToken rejecting
Qwen3NextForCausalLM ([[freetoken-ram-ceiling]]), exllamav3 lacking
Glm4MoeLite ([[exl3-tabbyapi-backend]]), SGLang having no fast MLA kernel
([[sglang-tp2-mixed-arch-norm-kernel]]), and this.
# NO ttl: POLICY 2026-09-09 -- models STAY LOADED until something else
# evicts them. This entry used to carry `ttl: 300` and was the only
# reason VRAM went empty ~5 min after a remote omp/Claude-Code session
# ("Unloading model, TTL of 300s reached"). All 45 entries are now
# uniform: no ttl anywhere. Re-add one ONLY for an entry that must free
# VRAM on idle, and say so here -- an idle unload costs a full cold load
# on the next request (this model: ~30 s, prefill 94 t/s cold vs 236 warm).
"k2-horizon":
aliases: [k2-horizon-mova-36b-a4b, mova-36b]
# *** THE ONLY ENTRY ON ${ifm_llama_bin} — a SECOND llama.cpp fork build. ***
# IFM/MBZUAI K2-Horizon-MoVA-36B-A4B. Mainline llama.cpp CANNOT load this:
# `unknown model architecture: 'k2-horizon'`. Upstream master has no such
# arch and there is STILL NO PR (verified 2026-09-06 against master
# 74a7c897f; only the bug report ggml-org#28361 exists). Support lives in
# MBZUAI-IFM/llama.cpp branch model/K2Horizon = 5 commits / 17 files on top
# of upstream 4e97ac86e, built side-by-side in ifm-llama.cpp/ exactly like
# the PrismML fork. See auto-memory k2-horizon-arch-gate.
#
# SHAPE: MoE, 100 experts / 8 active + 1 shared, 48 layers, hidden 2560,
# 32 attn heads, 8 KV heads, head_dim 128, vocab 250624.
# "MoVA" = Mixture-of-Values Attention: 64 VALUE experts, 4 active, routed
# per token. It is NOT a novel attention KERNEL -- modeling_k2_horizon.py
# routes only the V projection through experts and then calls the ordinary
# attention path with a standard KV cache, which is why the port needed
# 5 commits and why the GQA KV arithmetic below holds.
#
# *** KV IS 96 KiB/token AT q8_0 -- q4_0 IS MANDATORY FOR A USEFUL WINDOW.
# 2 * 48 layers * 8 kv_heads * 128 head_dim:
# ctx q8_0 q4_0
# 65536 6.00 GiB 3.00 GiB
# 131072 12.00 GiB 6.00 GiB
# 262144 24.00 GiB 12.00 GiB
# Q4_K_M weights ~21 GiB against ~30.3 GiB usable, so q8_0 @131072 needs
# 33 GiB and does NOT fit; q4_0 @131072 lands at ~27 GiB and does.
# This is a DELIBERATE exception to [[vram-spend-priority]] ("upgrade KV
# q4->q8 before chasing tok/s"), same call as granite-4.2-30b and
# coder-agentic: q4_0 is the only depth that reaches the required window.
#
# CONTEXT: config declares max_position_embeddings 524288 and the GGUF
# repeats it, but at 96 KiB/tok that is 48 GiB of KV. The real window here
# is KV-BOUND, not config-bound -- 131072 is what the box funds. Same trap
# as nemotron-3-nano's fake 1M and granite's "512K".
#
# WEIGHTS: quantized HERE from IFM's OWN BF16 GGUF (69.78 GiB,
# IFM/K2-Horizon-MoVA-36B-A4B-GGUF, the only file that repo ships) with the
# FORK's llama-quantize -- mainline llama-quantize cannot parse the tensors
# either. Deliberately NOT the third-party abenzerps Q4_K_M: its
# general.name is "Checkpoint_0002500", i.e. an intermediate training
# checkpoint, and grilling that would produce a verdict about the wrong
# artifact. That file stays parked on /mnt/wd1tb.
#
# *** SAMPLER IS LOAD-BEARING: temp 0.2 IS FATAL. ***
# IFM ship NO sampler defaults (generation_config.json has only bos/eos), so
# every value here is measured, not inherited. At temp 0.2 the model falls
# into a low-entropy attractor and repeats a sentence until it burns the
# whole budget. Measured on lru_ttl, 16000-token cap:
# temp 0.2 0/5 clean (deterministic, 313 s per runaway)
# temp 0.2 + DRY 0/2 (DRY verified applied; no effect at all)
# temp 0.6 1/2
# temp 1.0 4/5
# temp 0.6 + DRY 5/5 clean <- CHOSEN, and tokens TIGHTEN across reps
# DRY does nothing at 0.2, so its value is only in combination and is not
# isolated; the PAIRING is what was tested, so the pairing is what ships.
# A runaway returns NO fenced code block, so a grader that exec()s the
# response runs the model's PROSE and reports a SyntaxError — which reads as
# "wrote prose instead of code" and is really "never stopped".
#
# *** `expr_eval` RUNS AWAY AT EVERY SAMPLER TRIED *** — 0.2, 0.2+DRY,
# 1.0 (2/3) and 0.6+DRY (2/2), always a 313 s / 16000-token cap-hit
# reproducible to ~1 s. That is a genuine defect, not a config problem, and
# it is the most-failed task on this box (granite runs away on it too).
#
# --override-kv eot_token_id: the FORK ported the ARCH but not the VOCAB.
# Every turn in the chat template ends with `<|ifm|im_end|>` (token 250019)
# and generation_config lists it as an EOS, but llama-vocab.cpp matches the
# literal string `<|im_end|>` and so never registers the ifm-prefixed one.
# 250019 is a CONTROL token, so a per-request "stop" string CANNOT catch it;
# the override is the only lever. NOTE: this did NOT fix the runaways (they
# are mid-reasoning, before any stop token) but it matters for MULTI-TURN
# TOOL LOOPS, where every tool result is also wrapped in `<|ifm|im_end|>`.
#
# UNGRILLED TRIAL as of 2026-09-06 -- wired and load-tested only. The card
# leans on tau3-Banking; author benchmark claims have NOT transferred on this
# box before ([[gemma-4-12b-agentic-yuxinlu1]]). Grill before believing.
# -sm tensor is deliberately NOT set on the first wiring: it needs --fit off
# and is implicated in the one Xid 79 this box has seen. One variable first.
cmd: |
/usr/bin/env
CUDA_DEVICE_ORDER=PCI_BUS_ID
${ifm_llama_bin}
-m /mnt/models/gguf/K2-Horizon-MoVA-36B-A4B-Q4_K_M.gguf
--alias k2-horizon
--jinja
-ngl 99 -c 131072 -fa on
--override-kv tokenizer.ggml.eot_token_id=int:250019
--tensor-split 52,48
-b 2048 -ub 512
--temp 0.6
--dry-multiplier 0.8 --dry-base 1.75 --dry-allowed-length 2
--cache-type-k q4_0 --cache-type-v q4_0
--host 127.0.0.1 --port 9193 --parallel 1
proxy: http://127.0.0.1:9193