Skip to content

model : add K2 Horizon dense and MoVA support - #29535

Merged
CISC merged 28 commits into
ggml-org:masterfrom
bitalov:k2-horizon-all-fixes-20260927
Oct 6, 2026
Merged

CISC merged 28 commits into
ggml-org:masterfrom
bitalov:k2-horizon-all-fixes-20260927

Conversation

@bitalov

@bitalov bitalov commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds support for K2 Horizon, both the dense and MoVA variants. It covers HF-to-GGUF conversion, tokenizer support, model loading, and the inference graph. Everything is built on existing GGML operators, so there are no new kernels.

Along the way, a few supporting changes were needed:

  • Numeric indexing in selectattr and rejectattr. The K2 chat template processes tool schemas with dict | items | rejectattr('0', 'equalto', '$ref'), which needs to reach the first element of each key/value pair. The existing filters couldn't do that, so I added support for it.

  • A dedicated K2 chat parser. The auto-parser figures out reasoning and tool-call formats by rendering sample conversations. Some of those samples include assistant turns without reasoning_content, and the K2 template rejects them, so discovery fails. The dedicated parser handles K2's reasoning and tool calls, its end-of-turn marker, and the case where tool_choice=required jumps straight to a tool call.

  • Template capability detection in common/jinja/caps.cpp. The same missing field can make capability probes fail, which then wrongly reports that tools, object arguments, or parallel calls aren't supported. Now, if a probe fails, it retries once with an empty reasoning_content added to any assistant turns that lack it. This does touch shared capability detection, but when I compared results across 75 templates, only the K2 templates changed.

  • Multi-GPU support with -sm tensor. Value-expert weights follow the same split rule as the value weights. Q/K normalization weights are loaded per head, and the MoVA expert sum uses 3D views so the tensor-parallel backend can keep track of the split. None of this requires a change to the GGUF format.

  • MoVA save/reload support. The saver now writes the value-expert metadata needed to load saved models back in.

AI usage disclosure: YES - Codex reviewed the changes, made targeted fixes, and ran additional validation.

rayendito and others added 14 commits August 29, 2026 14:34
The K2-Horizon regex had no arm in unicode_regex_split_custom and fell through to the
general std::regex fallback, which fails two ways.

On MSVC std::regex rejects \p{...}, so no K2-Horizon GGUF loads on Windows at all:
llama-quantize, llama-imatrix and llama-perplexity all abort with
regex_error(error_escape) before a token is produced.

Where the fallback does compile it is still wrong. unicode_regex_split collapses each
codepoint to a single byte naming its Unicode category before matching, and U+200C/U+200D
are category Control, which has no entry in k_ucat_cpt, so both become the 0xD0 fallback
byte. The literal ‌ and ‍ alternatives in K2's regex can then never match and
every ZWNJ or ZWJ ends a letter run.

The splitter is the existing llama3 one with a single rule widened, since K2's regex
differs from llama3's only in that a letter run also takes marks, ZWNJ and ZWJ.

tests/test-unicode.cpp gains a case for this: it fails before the change with
[Amy] [ZWNJ khaham] and passes after with the run intact.
…ed-tests

tests: expand K2 Horizon unicode splitter coverage
…nizer

unicode : add the K2-Horizon pre-tokenizer splitter
Includes the K2 Horizon implementation from ifm-ai/llama.cpp with converter, tensor-parallel and model save/reload fixes.

Assisted-by: Codex
@github-actions github-actions Bot added model Model specific testing Everything test related jinja parser Issues related to the jinja parser conversion labels Sep 27, 2026
@ggml-gh-bot

ggml-gh-bot Bot commented Sep 27, 2026

Copy link
Copy Markdown

Hi @bitalov, thanks for your contribution!

Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:

  • Large PR: Large changes require prior discussion (e.g. an issue or RFC) and maintainers may not be able to review this PR as-is. Consider splitting it into smaller, focused PRs.

Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below.

@bitalov bitalov closed this Sep 27, 2026
@bitalov bitalov reopened this Sep 27, 2026
@bitalov bitalov closed this Sep 27, 2026
@bitalov bitalov reopened this Sep 27, 2026
Constrain final JSON after reasoning, accept flexible JSON tool envelopes,
enforce XML dialects, and handle repeated or alternate thinking markers.
Load YaRN beta metadata instead of retaining the default values.

Add schema, streaming, continuation, and model reload regressions. Validate
CUDA and CPU builds and 0.9B, 4B, and MoVA conversation/tool round trips.

Assisted-by: Codex
@bitalov
bitalov marked this pull request as ready for review September 30, 2026 12:22
@bitalov

bitalov commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

I think I found what's going on (with help from Claude). K2's group norm starts with a reshape, so the view it creates still points to the embeddings, which are on the CPU. That view then ends up in a Meta split, and since the backend expects a Meta buffer there, it asserts.
Adding a ggml_cont() before the reshape fixes it ofc, but I'm not sure it's the right fix. I might be wrong, but I'd expect the scheduler or the Meta backend to handle the transfer and the view, rather than the model code having to deal with it explicitly.
CC @CISC

@CISC

CISC commented Oct 6, 2026

Copy link
Copy Markdown
Member

I think I found what's going on (with help from Claude). K2's group norm starts with a reshape, so the view it creates still points to the embeddings, which are on the CPU. That view then ends up in a Meta split, and since the backend expects a Meta buffer there, it asserts. Adding a ggml_cont() before the reshape fixes it ofc, but I'm not sure it's the right fix. I might be wrong, but I'd expect the scheduler or the Meta backend to handle the transfer and the view, rather than the model code having to deal with it explicitly.

Yeah, this is not the right fix, though, not sure what would be right myself, @ggerganov?

@CISC CISC removed the jinja parser Issues related to the jinja parser label Oct 6, 2026
@ggerganov

Copy link
Copy Markdown
Member

Which part of the graph is that?

@bitalov

bitalov commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Which part of the graph is that?

It happens right at the start of the first layer, before the Q/K/V projections even run. On line 241, the embeddings get passed into the group RMS norm:

cur = k2_horizon_group_rms_norm(ctx0, inpL, layer.attn_norm, hparams.n_norm_groups, hparams.f_norm_rms_eps);

The first thing that helper does is this reshape on line 130:

cur = ggml_reshape_3d(ctx, cur, n_embd / n_groups, n_groups, n_tokens);

@ggerganov

Copy link
Copy Markdown
Member

Hm, not sure. @JohannesGaessler how should we handle CPU buffers reaching the meta backend?

@JohannesGaessler

Copy link
Copy Markdown
Contributor

I would expect this to be a bug in the ggml backend scheduler. It should not be possible to reach that assert with a tensor allocated on a buffer for which ggml_backend_supports_buft evaluates to false. And ggml_backend_meta_device_supports_buft is implemented in such a way that it should pretty clearly return false for a CPU buffer.

In any case, for this PR I would suggest that you simply add the new arch to llm_arch_supports_sm_tensor in llama-arch.cpp. It seems unlikely to me that this is a bug introduced with the new model arch and the fix should be in a standalone PR.

@github-actions github-actions Bot added the jinja parser Issues related to the jinja parser label Oct 6, 2026
@CISC CISC added the highlight Changes that will be highlighted in the next release notes label Oct 6, 2026
@CISC
CISC merged commit 4625240 into ggml-org:master Oct 6, 2026
31 of 37 checks passed
danielhanchen added a commit to danielhanchen/llama.cpp that referenced this pull request Oct 7, 2026
Resolves the conflicts with upstream ggml-org#29535 (K2 Horizon) and ggml-org#29442
(cuBLAS ldc and F16/BF16 src0 chunking):

- Inkling and K2 Horizon both register: parser decl, parser source,
  model mapping case, EOG token, output tensor name. model.unembed
  moves a few lines up the OUTPUT tuple, away from upstream's
  embedding_projection, so pins that carry that line still merge.
- LLAMA_VOCAB_PRE_TYPE_INKLING moves to 62, since upstream claimed 61
  for K2_HORIZON. The id is internal; the GGUF carries
  tokenizer.ggml.pre as a string.
- ggml_cuda_mul_mat_cublas_impl keeps the F32_PEDANTIC GemmEx path and
  passes upstream's ldc to both F32 branches; the pedantic compute-type
  override stays ahead of upstream's F16/BF16 src0 chunking, which it
  cannot overlap (pedantic only applies to F32 src0).
@ggerganov ggerganov mentioned this pull request Oct 8, 2026
3 tasks done
tschnorpfeil added a commit to tschnorpfeil/pippa that referenced this pull request Oct 8, 2026
b11146 predates ggml-org/llama.cpp#29535 (K2 Horizon dense and MoVA, merged
2026-10-06 as 4625240). b11503 is 49 commits ahead of that merge (GitHub
compare: ahead/0 behind). Asset sizes and SHA256 come from the release's
GitHub digests; the macOS arm64 archive was downloaded and re-hashed.

llama-server --help of that archive still lists every flag Pippa passes
(--jinja, --reasoning, --swa-full, --cache-type-k/-v, --chat-template-kwargs,
--ctx-checkpoints, --cache-ram, --alias, --slot-save-path, --no-webui,
sampling flags, LLAMA_API_KEY). The dylib set build-app.sh walks via @rpath
is unchanged (ggml 0.26.0, llama 0.6.0), the LICENSE file is identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EmeraldBitTwizzler pushed a commit to EmeraldBitTwizzler/unsloth-llama.cpp that referenced this pull request Oct 9, 2026
…at merge past b11453, drop unslothai#247, pin unslothai#251

Upstream K2 Horizon (ggml-org#29535), the glm5-next gather removal (ggml-org#30042) and the
GLM5-Next MTP graph (ggml-org#29928) broke the Inkling, unslothai#243 and unslothai#241 pins. Each
branch now carries a merge of upstream master. unslothai#247 is in every tag from
b11452 on. unslothai#251 adds --moe-cache-mib auto on top of ggml-org#29887.
adderek pushed a commit to adderek/llama.cpp that referenced this pull request Oct 21, 2026
276 upstream commits. Conflicts and how they were resolved:

- K2-Horizon: upstream added its own port of the same ifm-ai code (ggml-org#29535),
  with the chat parser, the unicode splitter and tests. Upstream's model,
  converter and names are taken; GGUF names are identical, so existing
  K2-Horizon GGUFs still load. The fork keeps only the K2-Horizon-36B
  tokenizer hash, which upstream dropped. LLAMA_VOCAB_PRE_TYPE_K2_HORIZON
  moves from 60 to 61 to follow upstream (it is not stored in GGUFs).
- MoE: upstream moved the selective expert copy out of the scheduler into a
  llama_context copy callback (ggml-org#29943) and added a GPU cache for host
  experts (ggml-org#29887). The fork's partial mul_mat_id (id < 0 rows, used by
  LLAMA_MOE_HOT) now skips negative ids in that callback. build_lora_mm_id
  takes both the fork's partial/ids_s and upstream's MoE cache slots;
  build_moe_ffn_exps passes the slots on the untiered path only, so the MoE
  cache and LLAMA_MOE_HOT tiering do not stack on one layer.
- mmvq: upstream's shared-expert fusion and the fork's negative-id early
  return combined; the shared-expert block never takes the early return.
- server: upstream's rejection sampling and checkpoint replay paths gained
  the fork's on_sample hook, so speculative logprobs cover them too.
- qwen4exp / build_attn: turbo Q pre-rotation kept on top of upstream's
  reworked QSA mask and the new training path.
- top-k.cu: upstream's CCCL >= 3.4.3 gate; the fork's hipcub arm was dead
  since HIP stopped defining GGML_CUDA_USE_CUB.
- ggml-cuda init_tensor: upstream's async memset on owned buffers, the
  fork's host-side padding clear on externally owned ones.
- tests/test-turbo-kv.cpp, tests/test-moe-tier.cpp: moved to common_batch
  and llama_process, as upstream removed common_batch_add.

Tested on gfx1100: ctest 76/78 (the two failures are environmental: the
ggml-vocabs clone has git-lfs pointers, and system python has no jinja2;
test-jinja-py passes with it). test-llama-archs 506/507; the qwen2moe
"Meta" failure is a seed-dependent flake that the pre-merge build shows
too. Server speculative/logprobs pytests 18/18. Needle at 12k tokens with
turbo4 KV: K2-Horizon-36B, Qwen3.8-Flash-Next, Qwen3.8-27B (plus MTP).
auRose94 pushed a commit to auRose94/llama.cpp that referenced this pull request Oct 28, 2026
* model: K2 Horizon gguf conversion code

* model: loading hparams and tensors in k2-horizon.cpp

* model: K2 Horizon compute graph

* model: K2 Horizon compute graph adjustment and registering tokenizers

* model: K2 Horizon chat template and accomodate safetensors naming

* unicode : add the K2-Horizon pre-tokenizer splitter

The K2-Horizon regex had no arm in unicode_regex_split_custom and fell through to the
general std::regex fallback, which fails two ways.

On MSVC std::regex rejects \p{...}, so no K2-Horizon GGUF loads on Windows at all:
llama-quantize, llama-imatrix and llama-perplexity all abort with
regex_error(error_escape) before a token is produced.

Where the fallback does compile it is still wrong. unicode_regex_split collapses each
codepoint to a single byte naming its Unicode category before matching, and U+200C/U+200D
are category Control, which has no entry in k_ucat_cpt, so both become the 0xD0 fallback
byte. The literal ‌ and ‍ alternatives in K2's regex can then never match and
every ZWNJ or ZWJ ends a letter run.

The splitter is the existing llama3 one with a single rule widened, since K2's regex
differs from llama3's only in that a letter run also takes marks, ZWNJ and ZWJ.

tests/test-unicode.cpp gains a case for this: it fails before the change with
[Amy] [ZWNJ khaham] and passes after with the run intact.

* tests: expand K2 Horizon unicode splitter coverage

* unicode: handle K2 Horizon case folding and empty input

Assisted-by: Codex

* jinja : support sequence indices in selectattr and rejectattr

Assisted-by: Codex

* model : add K2 Horizon dense and MoVA support

Includes the K2 Horizon implementation from ifm-ai/llama.cpp with converter, tensor-parallel and model save/reload fixes.

Assisted-by: Codex

* chat : support K2 Horizon reasoning and tool calls

Assisted-by: Codex

* conversion: remove obsolete K2 Aurora alias

Assisted-by: Codex

* k2-horizon: enforce response schemas and load YaRN betas

Constrain final JSON after reasoning, accept flexible JSON tool envelopes,
enforce XML dialects, and handle repeated or alternate thinking markers.
Load YaRN beta metadata instead of retaining the default values.

Add schema, streaming, continuation, and model reload regressions. Validate
CUDA and CPU builds and 0.9B, 4B, and MoVA conversation/tool round trips.

Assisted-by: Codex

* renaming template fixture

* adressing cisc follows ups

* desloppify the parser / adress aldehir comments

* clean test-chat

* remove fallback : model trained mostly on high anyway

* fix k2 attn_v_exp tn splitting and metal fusion baseline

* k2-horizon : forward expand views before sums

* k2-horizon: copy embds before group norm to fix TP

* disable tesnor parallelism

---------

Co-authored-by: Ryandito Diandaru <ryandito.diandaru@mbzuai.ac.ae>
Co-authored-by: WestWaters <mario.papaleo2013@gmail.com>
Co-authored-by: Natani L. Mayday <71436458+TaskPuppyNatani@users.noreply.github.com>
Co-authored-by: West <100190545+WestWaters@users.noreply.github.com>
Co-authored-by: aaryamonvikram <aaryamonvikram@gmail.com>
Co-authored-by: aaryamonvikram <96529820+aaryamonvikram@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conversion highlight Changes that will be highlighted in the next release notes jinja parser Issues related to the jinja parser model Model specific testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add support for K2 Horizon (0.9B, 3.7B, 7B, 32B, 36B MoVA) Eval bug: K2-Horizon models fail to load

9 participants