4625240437
* 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>
106 lines
4.8 KiB
Python
106 lines
4.8 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from collections.abc import Iterable
|
|
from typing import TYPE_CHECKING
|
|
|
|
import torch
|
|
|
|
if TYPE_CHECKING:
|
|
from torch import Tensor
|
|
|
|
from .base import ModelBase, TextModel, gguf
|
|
|
|
|
|
@ModelBase.register("K2HorizonForCausalLM")
|
|
@ModelBase.example("IFM/K2-Horizon-0.9B", "IFM/K2-Horizon-36B")
|
|
class K2HorizonModel(TextModel):
|
|
model_arch = gguf.MODEL_ARCH.K2HORIZON
|
|
|
|
_experts: list[dict[str, Tensor]] | None = None
|
|
|
|
def set_gguf_parameters(self):
|
|
super().set_gguf_parameters()
|
|
hparams = self.hparams
|
|
|
|
self.gguf_writer.add_group_norm_groups(int(hparams.get("layernorm_num_groups", 1)))
|
|
if (rope_head_dim := hparams.get("rope_head_dim")) is not None:
|
|
self.gguf_writer.add_rope_dimension_count(int(rope_head_dim))
|
|
|
|
if int(hparams.get("num_experts", 0)) > 0:
|
|
n_ff_exp = int(hparams["moe_intermediate_size"])
|
|
n_shared = int(hparams.get("num_shared_experts", 0))
|
|
|
|
# the leading dense layers are the prefix of mlp_only_layers, unless given explicitly
|
|
n_dense = hparams.get("num_dense_layers")
|
|
if n_dense is None:
|
|
mlp_only_layers = {int(il) for il in hparams.get("mlp_only_layers", [])}
|
|
n_dense = 0
|
|
while n_dense in mlp_only_layers:
|
|
n_dense += 1
|
|
|
|
self.gguf_writer.add_expert_feed_forward_length(n_ff_exp)
|
|
self.gguf_writer.add_leading_dense_block_count(n_dense)
|
|
self.gguf_writer.add_moe_every_n_layers(int(hparams.get("decoder_sparse_step", 1)))
|
|
self.gguf_writer.add_expert_shared_count(n_shared)
|
|
self.gguf_writer.add_expert_weights_norm(bool(hparams.get("norm_topk_prob", False)))
|
|
if n_shared > 0:
|
|
self.gguf_writer.add_expert_shared_feed_forward_length(n_ff_exp * n_shared)
|
|
if (router_scale := hparams.get("router_scaling_factor")) is not None:
|
|
self.gguf_writer.add_expert_weights_scale(float(router_scale))
|
|
|
|
# MoVA
|
|
n_value_expert = int(hparams.get("mova_num_experts", 0))
|
|
n_value_expert_used = int(hparams.get("mova_num_experts_per_tok", 0))
|
|
if n_value_expert > 0 and n_value_expert_used > 0:
|
|
assert n_value_expert_used <= n_value_expert
|
|
self.gguf_writer.add_attention_value_expert_count(n_value_expert)
|
|
self.gguf_writer.add_attention_value_expert_used_count(n_value_expert_used)
|
|
|
|
if (gate_func := hparams.get("attention_gate_func")) not in (None, "softplus"):
|
|
raise ValueError(f"Unsupported attention_gate_func: {gate_func!r}")
|
|
|
|
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
|
# the MoE router bias only selects experts
|
|
if name.endswith(".mlp.gate.bias"):
|
|
assert bid is not None
|
|
yield self.format_tensor_name(gguf.MODEL_TENSOR.FFN_EXP_PROBS_B, bid, ".bias"), data_torch
|
|
return
|
|
|
|
if re.fullmatch(r"model\.layers\.\d+\.mlp\.experts\.\d+\.(down|gate|up)_proj\.weight", name):
|
|
yield from self._stack_experts(data_torch, name, bid, int(self.hparams["num_experts"]),
|
|
"model.layers.{bid}.mlp.experts.{xid}.{w}.weight", ("down_proj", "gate_proj", "up_proj"))
|
|
return
|
|
|
|
if re.fullmatch(r"model\.layers\.\d+\.self_attn\.v_experts\.\d+\.weight", name):
|
|
yield from self._stack_experts(data_torch, name, bid, int(self.hparams["mova_num_experts"]),
|
|
"model.layers.{bid}.self_attn.v_experts.{xid}{w}.weight", ("",))
|
|
return
|
|
|
|
yield from super().modify_tensors(data_torch, name, bid)
|
|
|
|
# collect the per-expert weights of a layer, then emit one stacked 3D tensor per projection
|
|
def _stack_experts(self, data_torch: Tensor, name: str, bid: int | None, n_experts: int,
|
|
fmt: str, projs: tuple[str, ...]) -> Iterable[tuple[str, Tensor]]:
|
|
assert bid is not None
|
|
if self._experts is None:
|
|
self._experts = [{} for _ in range(self.block_count)]
|
|
self._experts[bid][name] = data_torch
|
|
|
|
names = {w: [fmt.format(bid=bid, xid=xid, w=w) for xid in range(n_experts)] for w in projs}
|
|
if not all(n in self._experts[bid] for ns in names.values() for n in ns):
|
|
return
|
|
|
|
for w, ns in names.items():
|
|
merged = torch.stack([self._experts[bid].pop(n) for n in ns], dim=0)
|
|
yield from super().modify_tensors(merged, fmt.replace(".{xid}", "").format(bid=bid, w=w), bid)
|
|
|
|
def prepare_tensors(self):
|
|
super().prepare_tensors()
|
|
|
|
if self._experts is not None:
|
|
# flatten the list of dicts
|
|
experts = [k for d in self._experts for k in d.keys()]
|
|
if len(experts) > 0:
|
|
raise ValueError(f"Unprocessed experts: {experts}")
|