diff --git a/convert_hf_to_gguf.py b/convert_hf_to_gguf.py index 90c2b7094c71..12623c43a4e5 100755 --- a/convert_hf_to_gguf.py +++ b/convert_hf_to_gguf.py @@ -9199,6 +9199,479 @@ def prepare_tensors(self): raise ValueError(f"Unprocessed experts: {experts}") +@ModelBase.register("DeepseekV4ForCausalLM") +class DeepseekV4Model(TextModel): + """DeepSeek-V4-Flash converter (PR1 BF16 path). + + Reference: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/inference/model.py + Dequant patterns mirror inference/convert.py and inference/kernel.py. + """ + model_arch = gguf.MODEL_ARCH.DEEPSEEK_V4 + + # FP4 e2m1 dequant lookup table (matches inference/convert.py:11-14) + _FP4_TABLE = torch.tensor([ + 0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, + 0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0, + ], dtype=torch.float32) + + # FP8 dequant block size (matches config.json: weight_block_size=[128,128]) + _FP8_BLOCK = 128 + # FP4 dequant block size along K (matches inference/convert.py:26) + _FP4_BLOCK = 32 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # MTP block joins the regular block array at index num_hidden_layers. + n_main = self.hparams["num_hidden_layers"] + n_mtp = self.hparams.get("num_nextn_predict_layers", 0) + self.block_count = n_main + n_mtp + self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count) + self._mtp_bid = n_main # MTP block lands at this bid + + # Buffers for paired weight/.scale streaming dequant. + self._fp_weight_buf: dict[str, Tensor] = {} + self._fp_scale_buf: dict[str, Tensor] = {} + + # Buffer for stacking 256 routed experts per layer into 3D tensors. + # _experts[bid][param_name] = stacked Tensor once all experts arrived. + self._exp_weight_buf: dict[int, dict[str, list[Tensor | None]]] = {} + self._exp_scale_buf: dict[int, dict[str, list[Tensor | None]]] = {} + + # Chat-mode Jinja template equivalent to encoding_dsv4.encode_messages + # (thinking_mode="chat"). BOS is emitted by the template (V4's + # tokenizer_config sets add_bos_token=False, so the tokenizer does not + # auto-prepend it). Detection on the C++ side keys on the four markers + # <|User|>, <|Assistant|>, <|end▁of▁sentence|>, . + _V4_CHAT_TEMPLATE = ( + "{{- '<|begin▁of▁sentence|>' -}}" + "{%- for message in messages -%}" + "{%- if message['role'] == 'system' -%}" + "{{- message['content'] -}}" + "{%- elif message['role'] == 'user' or message['role'] == 'developer' -%}" + "{{- '<|User|>' + message['content'] -}}" + "{%- if not loop.last and messages[loop.index]['role'] == 'assistant' -%}" + "{{- '<|Assistant|>' -}}" + "{%- endif -%}" + "{%- elif message['role'] == 'assistant' -%}" + "{{- message['content'] + '<|end▁of▁sentence|>' -}}" + "{%- endif -%}" + "{%- endfor -%}" + "{%- if add_generation_prompt -%}" + "{{- '<|Assistant|>' -}}" + "{%- endif -%}" + ) + + def set_vocab(self): + # tokenizer.json BPE + self._set_vocab_gpt2() + # Emit a Jinja chat template that matches encoding_dsv4 chat-mode output. + # The C++ side auto-detects DEEPSEEK_V4 by the four markers in this string. + self.gguf_writer.add_chat_template(self._V4_CHAT_TEMPLATE) + + def set_gguf_parameters(self): + h = self.hparams + gw = self.gguf_writer + + # standard + gw.add_block_count(self.block_count) + gw.add_embedding_length(h["hidden_size"]) + gw.add_feed_forward_length(h["moe_intermediate_size"]) # dense FFN feature unused + gw.add_head_count(h["num_attention_heads"]) + gw.add_head_count_kv(h["num_key_value_heads"]) # =1 (MQA) + gw.add_layer_norm_rms_eps(h["rms_norm_eps"]) + gw.add_vocab_size(h["vocab_size"]) + gw.add_context_length(h["max_position_embeddings"]) + + # RoPE + gw.add_rope_freq_base(h["rope_theta"]) # 10000 (SWA layers) + gw.add_rope_dimension_count(h["qk_rope_head_dim"]) # 64 + gw.add_key_length(h["head_dim"]) # 512 + gw.add_value_length(h["head_dim"]) + + # YaRN (used on CSA/HCA layers; SWA layers ignore it via compress_ratio==0) + rs = h.get("rope_scaling") or {} + if rs.get("type") == "yarn": + gw.add_rope_scaling_type(gguf.RopeScalingType.YARN) + gw.add_rope_scaling_factor(rs["factor"]) + gw.add_rope_scaling_orig_ctx_len(rs["original_max_position_embeddings"]) + if "beta_fast" in rs: + gw.add_rope_scaling_yarn_beta_fast(rs["beta_fast"]) + if "beta_slow" in rs: + gw.add_rope_scaling_yarn_beta_slow(rs["beta_slow"]) + + # V4 attention + gw.add_q_lora_rank(h["q_lora_rank"]) + gw.add_o_lora_rank(h["o_lora_rank"]) + gw.add_o_groups(h["o_groups"]) + gw.add_sliding_window(h["sliding_window"]) # 128 + gw.add_compress_ratios(h["compress_ratios"]) # length 44 (43 + MTP) + gw.add_compress_rope_freq_base(h["compress_rope_theta"]) # 160000 + + # Indexer (CSA layers only) + gw.add_indexer_head_count(h["index_n_heads"]) + gw.add_indexer_key_length(h["index_head_dim"]) + gw.add_indexer_top_k(h["index_topk"]) + + # mHC (manifold-constrained Hyper-Connections) + gw.add_hc_mult(h["hc_mult"]) + gw.add_hc_sinkhorn_iters(h["hc_sinkhorn_iters"]) + gw.add_hc_eps(h["hc_eps"]) + gw.add_n_hash_layers(h["num_hash_layers"]) + + # MTP / NextN + gw.add_nextn_predict_layers(h.get("num_nextn_predict_layers", 0)) + + # MoE + gw.add_expert_feed_forward_length(h["moe_intermediate_size"]) + gw.add_expert_count(h["n_routed_experts"]) + gw.add_expert_used_count(h["num_experts_per_tok"]) + gw.add_expert_shared_count(h["n_shared_experts"]) + gw.add_expert_weights_scale(h["routed_scaling_factor"]) + if h.get("norm_topk_prob"): + gw.add_expert_weights_norm(True) + # 4 = LLAMA_EXPERT_GATING_FUNC_TYPE_SQRT_SOFTPLUS + gw.add_expert_gating_func(gguf.ExpertGatingFuncType.SQRT_SOFTPLUS) + + # SwiGLU clamp (= swiglu_limit) on routed AND shared experts + # (per inference/model.py:600-602 and SGLang #23776). Per-layer array + # of length block_count so the C++ side reads via get_key_or_arr. + sw = float(h["swiglu_limit"]) + gw.add_swiglu_clamp_exp([sw] * self.block_count) + gw.add_swiglu_clamp_shexp([sw] * self.block_count) + + # --- helpers ------------------------------------------------------------- + + def _fp8_dequant(self, weight_f32: Tensor, scale_f32: Tensor) -> Tensor: + """Block-wise FP8 e4m3fn × UE8M0 scale → F32. + + Mirrors inference/convert.py:140 and inference/kernel.py for fp8_gemm. + weight: (M, K) already-decoded FP8 values (loader cast FP8 → FP32). + scale: (M/128, K/128) already-decoded UE8M0 values. + + Returns F32; the GGUF writer downcasts to the requested ftype (BF16 by + default for --outtype bf16) at write time. + """ + b = self._FP8_BLOCK + M, K = weight_f32.shape + assert M % b == 0 and K % b == 0, f"FP8 dequant: shape {weight_f32.shape} not divisible by {b}" + Mb, Kb = M // b, K // b + assert scale_f32.shape == (Mb, Kb), \ + f"FP8 scale shape {tuple(scale_f32.shape)} != ({Mb}, {Kb})" + out = (weight_f32.view(Mb, b, Kb, b) + * scale_f32.view(Mb, 1, Kb, 1)).reshape(M, K) + return out.contiguous() + + def _fp4_dequant(self, packed_f32: Tensor, scale_f32: Tensor) -> Tensor: + """Block-wise FP4 e2m1 packed (2 nibbles / byte) × UE8M0 scale → F32. + + Mirrors inference/convert.py:cast_e2m1fn_to_e4m3fn (the fp8 path) but + emits F32 directly (PR1 BF16 routed-expert path; native MXFP4 + passthrough is PR3). The writer downcasts F32 → BF16 at write time. + packed: (M, K_packed) where K_packed = K/2; loader cast int8 → FP32 so + each value is a signed byte in [-128, 127]. + scale: (M, K/32) UE8M0 → already-decoded scale floats. + """ + b = self._FP4_BLOCK + M, Kp = packed_f32.shape + K = Kp * 2 + assert K % b == 0, f"FP4 dequant: K={K} not divisible by {b}" + # recover unsigned bytes from the signed-int8-cast-to-float32 + u8 = (packed_f32.to(torch.int32) & 0xFF) + low = u8 & 0x0F + high = (u8 >> 4) & 0x0F + # Stack [low, high] → (M, Kp, 2) → flatten to (M, K) + # The on-disk layout matches inference/convert.py:30-33: low nibble at + # even index, high nibble at odd index. + nibbles = torch.stack([low, high], dim=-1).flatten(1) # (M, K) int32 + decoded = self._FP4_TABLE[nibbles.long()] # (M, K) fp32 + # Scale per block of 32 along K + Kb = K // b + assert scale_f32.shape == (M, Kb), \ + f"FP4 scale shape {tuple(scale_f32.shape)} != ({M}, {Kb})" + out = decoded.view(M, Kb, b) * scale_f32.view(M, Kb, 1) + return out.reshape(M, K).contiguous() + + # Source-name suffixes that come with a sibling `.scale` and need FP8 dequant. + # Determined by inspection of the V4-Flash safetensors index. Routed-expert + # weights are FP4 (handled by _consume_expert) and are NOT in this set. + _FP8_PACKED_SUFFIXES = ( + "attn.wq_a", "attn.wq_b", "attn.wkv", "attn.wo_a", "attn.wo_b", + "attn.indexer.wq_b", + "ffn.shared_experts.w1", "ffn.shared_experts.w2", "ffn.shared_experts.w3", + "e_proj", "h_proj", + ) + + def _is_fp8_packed(self, name: str) -> bool: + if not name.endswith(".weight"): + return False + base = name[:-len(".weight")] + return any(base.endswith(suffix) for suffix in self._FP8_PACKED_SUFFIXES) + + def _maybe_pair(self, name: str, data: Tensor): + """For FP8-packed tensors only: buffer the .weight or .scale half and + return the completed (weight, scale) pair when both have arrived. + Non-FP8 tensors return (None, None) so the caller emits them directly.""" + if name.endswith(".scale"): + base = name[:-len(".scale")] + self._fp_scale_buf[base] = data + elif name.endswith(".weight"): + if not self._is_fp8_packed(name): + # not paired -- caller treats as a single tensor. + return None, None + base = name[:-len(".weight")] + self._fp_weight_buf[base] = data + else: + return None, None + if base in self._fp_weight_buf and base in self._fp_scale_buf: + return self._fp_weight_buf.pop(base), self._fp_scale_buf.pop(base) + return None, None + + def _v4_logical_name(self, source: str, bid: int | None) -> str | None: + """Map V4 source-name → llama.cpp logical name. Returns None if the + tensor should be silently dropped (e.g. .scale once consumed).""" + # mtp.0.* → block index = num_hidden_layers + if source.startswith("mtp.0."): + assert bid == 0 + bid = self._mtp_bid + tail = source[len("mtp.0."):] + return self._v4_block_name(tail, bid, mtp=True) + + if source.startswith("layers.") and bid is not None: + tail = source[len(f"layers.{bid}."):] + return self._v4_block_name(tail, bid, mtp=False) + + # Top-level + m = { + "embed.weight": "token_embd.weight", + "norm.weight": "output_norm.weight", + "head.weight": "output.weight", + # Model-level hc_head (matches gguf-py constants.py: output.hc_head.{fn,base,scale}). + "hc_head_fn": "output.hc_head.fn.weight", + "hc_head_base": "output.hc_head.base.weight", + "hc_head_scale": "output.hc_head.scale.weight", + } + if source in m: + return m[source] + return None + + def _v4_block_name(self, tail: str, bid: int, mtp: bool) -> str | None: + """Translate a per-block tail like 'attn.wq_a.weight' to logical name. + + For MTP (bid == self._mtp_bid), the standard attention / FFN tensors share + the same names as a regular block at bid=N so they slot into the existing + per-layer table; only the MTP-specific extras (e_proj, h_proj, enorm, + hnorm, norm, hc_head_*) carry the `.mtp.` infix. + """ + prefix = f"blk.{bid}." + + # mHC residual mapping (per-block; same names for MTP and main blocks) + m = { + "hc_attn_fn": prefix + "hc_attn.fn.weight", + "hc_attn_base": prefix + "hc_attn.base.weight", + "hc_attn_scale": prefix + "hc_attn.scale.weight", + "hc_ffn_fn": prefix + "hc_ffn.fn.weight", + "hc_ffn_base": prefix + "hc_ffn.base.weight", + "hc_ffn_scale": prefix + "hc_ffn.scale.weight", + } + if tail in m: + return m[tail] + + # MTP-only mappings + if mtp: + mtp_m = { + "hc_head_fn": prefix + "mtp.hc_head.fn.weight", + "hc_head_base": prefix + "mtp.hc_head.base.weight", + "hc_head_scale": prefix + "mtp.hc_head.scale.weight", + "e_proj.weight": prefix + "mtp.e_proj.weight", + "h_proj.weight": prefix + "mtp.h_proj.weight", + "enorm.weight": prefix + "mtp.enorm.weight", + "hnorm.weight": prefix + "mtp.hnorm.weight", + "norm.weight": prefix + "mtp.norm.weight", + } + if tail in mtp_m: + return mtp_m[tail] + + # attn / ffn norms + if tail == "attn_norm.weight": return prefix + "attn_norm.weight" + if tail == "ffn_norm.weight": return prefix + "ffn_norm.weight" + + # attention parameters + a = "attn." + if tail == a + "attn_sink": return prefix + "attn_sinks.weight" + if tail == a + "wq_a.weight": return prefix + "attn_q_a.weight" + if tail == a + "wq_b.weight": return prefix + "attn_q_b.weight" + if tail == a + "q_norm.weight": return prefix + "attn_q_a_norm.weight" + if tail == a + "wkv.weight": return prefix + "attn_kv.weight" + if tail == a + "kv_norm.weight": return prefix + "attn_kv_norm.weight" + if tail == a + "wo_a.weight": return prefix + "attn_o_a.weight" + if tail == a + "wo_b.weight": return prefix + "attn_o_b.weight" + + # compressor (ratio>0 layers only) + c = a + "compressor." + if tail == c + "wkv.weight": return prefix + "compressor.wkv.weight" + if tail == c + "wgate.weight": return prefix + "compressor.wgate.weight" + if tail == c + "ape": return prefix + "compressor.ape.weight" + if tail == c + "norm.weight": return prefix + "compressor.norm.weight" + + # indexer (ratio==4 layers only) + i = a + "indexer." + if tail == i + "wq_b.weight": return prefix + "indexer.attn_q_b.weight" + if tail == i + "weights_proj.weight": return prefix + "indexer.weights_proj.weight" + ic = i + "compressor." + if tail == ic + "wkv.weight": return prefix + "indexer.compressor.wkv.weight" + if tail == ic + "wgate.weight": return prefix + "indexer.compressor.wgate.weight" + if tail == ic + "ape": return prefix + "indexer.compressor.ape.weight" + if tail == ic + "norm.weight": return prefix + "indexer.compressor.norm.weight" + + # FFN router + if tail == "ffn.gate.weight": return prefix + "ffn_gate_inp.weight" + if tail == "ffn.gate.bias": return prefix + "exp_probs_b.bias" + if tail == "ffn.gate.tid2eid": return prefix + "ffn_gate_tid2eid.weight" + + # Shared expert + if tail == "ffn.shared_experts.w1.weight": return prefix + "ffn_gate_shexp.weight" + if tail == "ffn.shared_experts.w2.weight": return prefix + "ffn_down_shexp.weight" + if tail == "ffn.shared_experts.w3.weight": return prefix + "ffn_up_shexp.weight" + + # Routed experts -- handled by stacking path in modify_tensors + return None + + # --- main hook ----------------------------------------------------------- + + def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]: + # === routed-expert stacking path === + # Source: layers.{bid}.ffn.experts.{xid}.{w1|w2|w3}.{weight|scale} + m = re.match(r"^(?:layers\.(\d+)|mtp\.0)\.ffn\.experts\.(\d+)\.(w[123])\.(weight|scale)$", name) + if m: + return self._consume_expert(m, name, data_torch) + + # === everything else: detect FP8 weight/scale pairing === + weight, scale = self._maybe_pair(name, data_torch) + if name.endswith(".scale"): + if weight is None: + # scale arrived without its weight — buffer; nothing to emit yet + return [] + # both present: dequant FP8 → BF16 + deq = self._fp8_dequant(weight, scale) + return list(self._emit_single(name, deq, bid, dequant=True)) + if name.endswith(".weight") and self._is_fp8_packed(name): + # FP8-packed weight: buffered until scale arrives. Nothing to emit yet. + if weight is None: + return [] + deq = self._fp8_dequant(weight, scale) + return list(self._emit_single(name, deq, bid, dequant=True)) + # Plain non-paired tensor (BF16/F32 norm, attn_sink, hc_*, ape, tid2eid, etc.) + return list(self._emit_single(name, data_torch, bid)) + + def _emit_single(self, source_name: str, data: Tensor, bid: int | None, dequant: bool = False) -> Iterable[tuple[str, Tensor]]: + """Emit a single non-expert tensor with its logical name + dtype fixups. + + All weights are emitted as F32; the GGUF writer's tensor_force_quant + path downcasts to BF16 (or other ftype) at write time. The single + exception is ffn_gate_tid2eid which is written directly as I32 via + gguf_writer.add_tensor with raw_dtype=I32. + """ + # If the source name ends with .weight, build the logical name from the + # original .weight name (so dequant doesn't change which entry we map). + # If it ends with .scale, use the .weight entry instead. + lookup = source_name + if source_name.endswith(".scale"): + lookup = source_name[:-len(".scale")] + ".weight" + + logical = self._v4_logical_name(lookup, bid) + if logical is None: + # Unmapped — error loudly so we notice silent drops. + raise ValueError(f"DeepseekV4Model: unmapped source tensor {source_name!r} (bid={bid})") + + # tid2eid: write as I32 directly via gguf_writer.add_tensor, return []. + # (loader cast int64 → fp32 at line 783-784 of the prepare_tensors loop; + # we materialize to a real numpy array of int32, then write as raw I32.) + if logical.endswith("ffn_gate_tid2eid.weight"): + eager = LazyTorchTensor.to_eager(data) + arr = eager.to(torch.int32).contiguous().numpy() + self.gguf_writer.add_tensor(logical, arr, raw_dtype=gguf.GGMLQuantizationType.I32) + return + + # wo_a: reshape (n_groups*o_lora_rank, dim_per_group) + # → (n_groups, o_lora_rank, dim_per_group) for grouped einsum on C++ side. + if logical.endswith("attn_o_a.weight"): + n_groups = self.hparams["o_groups"] + o_lora = self.hparams["o_lora_rank"] + assert data.shape[0] == n_groups * o_lora, \ + f"attn_o_a expected first dim {n_groups*o_lora}, got {tuple(data.shape)}" + data = data.view(n_groups, o_lora, data.shape[1]).contiguous() + + # Ensure F32 for the rest of the pipeline (writer handles BF16 cast). + if data.dtype != torch.float32: + data = data.to(torch.float32) + + yield logical, data + + def _consume_expert(self, m: re.Match, source_name: str, data: Tensor) -> Iterable[tuple[str, Tensor]]: + """Buffer expert tensors per (block, w_name); when all 256 experts arrive + for a (block, w_name, kind) triple, dequant FP4 → BF16 and stack.""" + layer_grp, xid_str, wname, kind = m.groups() + if layer_grp is None: + bid = self._mtp_bid + else: + bid = int(layer_grp) + xid = int(xid_str) + n_experts = self.hparams["n_routed_experts"] + + if kind == "weight": + buf = self._exp_weight_buf.setdefault(bid, {}) + else: + buf = self._exp_scale_buf.setdefault(bid, {}) + slots = buf.setdefault(wname, [None] * n_experts) + slots[xid] = data + + # Emit only when all 256 experts have BOTH weight and scale. + wbuf = self._exp_weight_buf.get(bid, {}).get(wname, []) + sbuf = self._exp_scale_buf.get(bid, {}).get(wname, []) + if len(wbuf) != n_experts or len(sbuf) != n_experts: + return [] + if any(t is None for t in wbuf) or any(t is None for t in sbuf): + return [] + + # All 256 experts ready. Dequant each FP4 → BF16, stack into 3D. + deq_list = [self._fp4_dequant(wbuf[i], sbuf[i]) for i in range(n_experts)] + # PR2 graph expects per-expert shape (out_features, in_features); stack on dim 0. + stacked = torch.stack(deq_list, dim=0) # (n_experts, out, in) + + # Free the buffer + del self._exp_weight_buf[bid][wname] + del self._exp_scale_buf[bid][wname] + + # Map w1/w2/w3 → ffn_{gate,down,up}_exps + # inference/model.py:Expert: w1=gate, w2=down, w3=up; gate(x).silu * up(x), then down + wmap = {"w1": "ffn_gate_exps", "w2": "ffn_down_exps", "w3": "ffn_up_exps"} + suffix = wmap[wname] + logical = f"blk.{bid}.{suffix}.weight" + return [(logical, stacked)] + + def prepare_tensors(self): + super().prepare_tensors() + # Unprocessed buffers indicate a name our converter didn't map. + leftover_w = {k for k in self._fp_weight_buf if not k.endswith("scale")} + leftover_s = list(self._fp_scale_buf.keys()) + if leftover_w or leftover_s: + raise ValueError( + f"DeepseekV4Model: unmatched FP8 weight/scale pairs:\n" + f" weights without scales: {sorted(leftover_w)}\n" + f" scales without weights: {sorted(leftover_s)}" + ) + for bid, by_w in self._exp_weight_buf.items(): + for wname, slots in by_w.items(): + missing = [i for i, t in enumerate(slots) if t is None] + if missing: + raise ValueError(f"layer {bid} {wname}.weight: missing experts {missing[:8]}...") + for bid, by_w in self._exp_scale_buf.items(): + for wname, slots in by_w.items(): + missing = [i for i, t in enumerate(slots) if t is None] + if missing: + raise ValueError(f"layer {bid} {wname}.scale: missing experts {missing[:8]}...") + + @ModelBase.register( "Mistral3ForConditionalGeneration", "Ministral3ForCausalLM", @@ -13229,6 +13702,8 @@ class LazyTorchTensor(gguf.LazyBase): torch.float16: np.float16, torch.float32: np.float32, torch.uint8: np.uint8, + # DeepSeek-V4-Flash hash-routing LUT (ffn_gate_tid2eid) is stored as I32. + torch.int32: np.int32, } # only used when byteswapping data. Only correct size is needed @@ -13248,6 +13723,7 @@ class LazyTorchTensor(gguf.LazyBase): torch.bool: np.uint8, torch.float8_e4m3fn: np.uint8, torch.float8_e5m2: np.uint8, + torch.float8_e8m0fnu: np.uint8, } # used for safetensors slices @@ -13269,6 +13745,9 @@ class LazyTorchTensor(gguf.LazyBase): "BOOL": torch.bool, "F8_E4M3": torch.float8_e4m3fn, "F8_E5M2": torch.float8_e5m2, + # DeepSeek-V4-Flash uses UE8M0 scale tensors alongside FP8 e4m3 / FP4 e2m1 + # weights. ref: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/config.json + "F8_E8M0": torch.float8_e8m0fnu, } def numpy(self) -> gguf.LazyNumpyTensor: diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py index 83ae51ce9ce3..e51affb2979f 100644 --- a/gguf-py/gguf/constants.py +++ b/gguf-py/gguf/constants.py @@ -152,6 +152,11 @@ class LLM: SWIGLU_CLAMP_SHEXP = "{arch}.swiglu_clamp_shexp" DENSE_FEAT_IN_SIZE = "{arch}.{dense}_feat_in" DENSE_FEAT_OUT_SIZE = "{arch}.{dense}_feat_out" + # DeepSeek-V4 mHC + hash routing + N_HASH_LAYERS = "{arch}.n_hash_layers" + HC_MULT = "{arch}.hyperconnections.mult" + HC_SINKHORN_ITERS = "{arch}.hyperconnections.sinkhorn_iters" + HC_EPS = "{arch}.hyperconnections.eps" class Attention: HEAD_COUNT = "{arch}.attention.head_count" @@ -183,6 +188,11 @@ class Attention: SHARED_KV_LAYERS = "{arch}.attention.shared_kv_layers" SLIDING_WINDOW_PATTERN = "{arch}.attention.sliding_window_pattern" TEMPERATURE_SCALE = "{arch}.attention.temperature_scale" + # DeepSeek-V4 + O_LORA_RANK = "{arch}.attention.o_lora_rank" + O_GROUPS = "{arch}.attention.o_groups" + COMPRESS_RATIOS = "{arch}.attention.compress_ratios" + COMPRESS_ROPE_FREQ_BASE = "{arch}.attention.compress_rope_freq_base" class Indexer: HEAD_COUNT = "{arch}.attention.indexer.head_count" @@ -442,6 +452,7 @@ class MODEL_ARCH(IntEnum): DEEPSEEK = auto() DEEPSEEK2 = auto() DEEPSEEK2OCR = auto() + DEEPSEEK_V4 = auto() CHATGLM = auto() GLM4 = auto() GLM4_MOE = auto() @@ -844,6 +855,37 @@ class MODEL_TENSOR(IntEnum): NEXTN_HNORM = auto() NEXTN_SHARED_HEAD_HEAD = auto() NEXTN_SHARED_HEAD_NORM = auto() + # DeepSeek-V4-Flash specific + ATTN_KV = auto() # single shared K=V projection (MQA) + ATTN_KV_NORM = auto() # RMSNorm on shared KV + ATTN_O_A = auto() # grouped low-rank wo_a [n_groups, o_lora_rank, ...] + ATTN_O_B = auto() # wo_b + COMPRESSOR_WKV = auto() + COMPRESSOR_WGATE = auto() + COMPRESSOR_APE = auto() + COMPRESSOR_NORM = auto() + INDEXER_COMPRESSOR_WKV = auto() + INDEXER_COMPRESSOR_WGATE = auto() + INDEXER_COMPRESSOR_APE = auto() + INDEXER_COMPRESSOR_NORM = auto() + HC_ATTN_FN = auto() + HC_ATTN_BASE = auto() + HC_ATTN_SCALE = auto() + HC_FFN_FN = auto() + HC_FFN_BASE = auto() + HC_FFN_SCALE = auto() + HC_HEAD_FN = auto() + HC_HEAD_BASE = auto() + HC_HEAD_SCALE = auto() + FFN_GATE_TID2EID = auto() + MTP_E_PROJ = auto() + MTP_H_PROJ = auto() + MTP_ENORM = auto() + MTP_HNORM = auto() + MTP_NORM = auto() + MTP_HC_HEAD_FN = auto() + MTP_HC_HEAD_BASE = auto() + MTP_HC_HEAD_SCALE = auto() # lfm2 audio A_ENC_NORM_CONV = auto() A_ENC_LINEAR_POS = auto() @@ -928,6 +970,7 @@ class MODEL_TENSOR(IntEnum): MODEL_ARCH.DEEPSEEK: "deepseek", MODEL_ARCH.DEEPSEEK2: "deepseek2", MODEL_ARCH.DEEPSEEK2OCR: "deepseek2-ocr", + MODEL_ARCH.DEEPSEEK_V4: "deepseek-v4", MODEL_ARCH.CHATGLM: "chatglm", MODEL_ARCH.GLM4: "glm4", MODEL_ARCH.GLM4_MOE: "glm4moe", @@ -1340,6 +1383,37 @@ class MODEL_TENSOR(IntEnum): MODEL_TENSOR.NEXTN_HNORM: "blk.{bid}.nextn.hnorm", MODEL_TENSOR.NEXTN_SHARED_HEAD_HEAD: "blk.{bid}.nextn.shared_head_head", MODEL_TENSOR.NEXTN_SHARED_HEAD_NORM: "blk.{bid}.nextn.shared_head_norm", + # DeepSeek-V4-Flash + MODEL_TENSOR.ATTN_KV: "blk.{bid}.attn_kv", + MODEL_TENSOR.ATTN_KV_NORM: "blk.{bid}.attn_kv_norm", + MODEL_TENSOR.ATTN_O_A: "blk.{bid}.attn_o_a", + MODEL_TENSOR.ATTN_O_B: "blk.{bid}.attn_o_b", + MODEL_TENSOR.COMPRESSOR_WKV: "blk.{bid}.compressor.wkv", + MODEL_TENSOR.COMPRESSOR_WGATE: "blk.{bid}.compressor.wgate", + MODEL_TENSOR.COMPRESSOR_APE: "blk.{bid}.compressor.ape", + MODEL_TENSOR.COMPRESSOR_NORM: "blk.{bid}.compressor.norm", + MODEL_TENSOR.INDEXER_COMPRESSOR_WKV: "blk.{bid}.indexer.compressor.wkv", + MODEL_TENSOR.INDEXER_COMPRESSOR_WGATE: "blk.{bid}.indexer.compressor.wgate", + MODEL_TENSOR.INDEXER_COMPRESSOR_APE: "blk.{bid}.indexer.compressor.ape", + MODEL_TENSOR.INDEXER_COMPRESSOR_NORM: "blk.{bid}.indexer.compressor.norm", + MODEL_TENSOR.HC_ATTN_FN: "blk.{bid}.hc_attn.fn", + MODEL_TENSOR.HC_ATTN_BASE: "blk.{bid}.hc_attn.base", + MODEL_TENSOR.HC_ATTN_SCALE: "blk.{bid}.hc_attn.scale", + MODEL_TENSOR.HC_FFN_FN: "blk.{bid}.hc_ffn.fn", + MODEL_TENSOR.HC_FFN_BASE: "blk.{bid}.hc_ffn.base", + MODEL_TENSOR.HC_FFN_SCALE: "blk.{bid}.hc_ffn.scale", + MODEL_TENSOR.HC_HEAD_FN: "output.hc_head.fn", + MODEL_TENSOR.HC_HEAD_BASE: "output.hc_head.base", + MODEL_TENSOR.HC_HEAD_SCALE: "output.hc_head.scale", + MODEL_TENSOR.FFN_GATE_TID2EID: "blk.{bid}.ffn_gate_tid2eid", + MODEL_TENSOR.MTP_E_PROJ: "blk.{bid}.mtp.e_proj", + MODEL_TENSOR.MTP_H_PROJ: "blk.{bid}.mtp.h_proj", + MODEL_TENSOR.MTP_ENORM: "blk.{bid}.mtp.enorm", + MODEL_TENSOR.MTP_HNORM: "blk.{bid}.mtp.hnorm", + MODEL_TENSOR.MTP_NORM: "blk.{bid}.mtp.norm", + MODEL_TENSOR.MTP_HC_HEAD_FN: "blk.{bid}.mtp.hc_head.fn", + MODEL_TENSOR.MTP_HC_HEAD_BASE: "blk.{bid}.mtp.hc_head.base", + MODEL_TENSOR.MTP_HC_HEAD_SCALE: "blk.{bid}.mtp.hc_head.scale", } MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = { @@ -2816,6 +2890,64 @@ class MODEL_TENSOR(IntEnum): MODEL_TENSOR.FFN_UP_SHEXP, MODEL_TENSOR.FFN_EXP_PROBS_B, ], + MODEL_ARCH.DEEPSEEK_V4: [ + # Top-level + MODEL_TENSOR.TOKEN_EMBD, + MODEL_TENSOR.OUTPUT_NORM, + MODEL_TENSOR.OUTPUT, + MODEL_TENSOR.HC_HEAD_FN, + MODEL_TENSOR.HC_HEAD_BASE, + MODEL_TENSOR.HC_HEAD_SCALE, + # Per-block (43 main + 1 MTP) + MODEL_TENSOR.ATTN_NORM, + MODEL_TENSOR.ATTN_Q_A, + MODEL_TENSOR.ATTN_Q_A_NORM, + MODEL_TENSOR.ATTN_Q_B, + MODEL_TENSOR.ATTN_KV, + MODEL_TENSOR.ATTN_KV_NORM, + MODEL_TENSOR.ATTN_O_A, + MODEL_TENSOR.ATTN_O_B, + MODEL_TENSOR.ATTN_SINKS, + # Compressor (CSA + HCA layers only; declared globally, loader skips on SWA layers) + MODEL_TENSOR.COMPRESSOR_WKV, + MODEL_TENSOR.COMPRESSOR_WGATE, + MODEL_TENSOR.COMPRESSOR_APE, + MODEL_TENSOR.COMPRESSOR_NORM, + # Indexer (CSA layers only; loaded but unused in MVP graph - dense fallback) + MODEL_TENSOR.INDEXER_ATTN_Q_B, + MODEL_TENSOR.INDEXER_PROJ, + MODEL_TENSOR.INDEXER_COMPRESSOR_WKV, + MODEL_TENSOR.INDEXER_COMPRESSOR_WGATE, + MODEL_TENSOR.INDEXER_COMPRESSOR_APE, + MODEL_TENSOR.INDEXER_COMPRESSOR_NORM, + # mHC per-block residual mapping + MODEL_TENSOR.HC_ATTN_FN, + MODEL_TENSOR.HC_ATTN_BASE, + MODEL_TENSOR.HC_ATTN_SCALE, + MODEL_TENSOR.HC_FFN_FN, + MODEL_TENSOR.HC_FFN_BASE, + MODEL_TENSOR.HC_FFN_SCALE, + # MoE + MODEL_TENSOR.FFN_NORM, + MODEL_TENSOR.FFN_GATE_INP, + MODEL_TENSOR.FFN_EXP_PROBS_B, # router bias on layers 3+ + MODEL_TENSOR.FFN_GATE_TID2EID, # hash routing LUT on first n_hash_layers + MODEL_TENSOR.FFN_GATE_EXP, + MODEL_TENSOR.FFN_DOWN_EXP, + MODEL_TENSOR.FFN_UP_EXP, + MODEL_TENSOR.FFN_GATE_SHEXP, + MODEL_TENSOR.FFN_DOWN_SHEXP, + MODEL_TENSOR.FFN_UP_SHEXP, + # MTP (last block only; loaded but unused in MVP graph) + MODEL_TENSOR.MTP_E_PROJ, + MODEL_TENSOR.MTP_H_PROJ, + MODEL_TENSOR.MTP_ENORM, + MODEL_TENSOR.MTP_HNORM, + MODEL_TENSOR.MTP_NORM, + MODEL_TENSOR.MTP_HC_HEAD_FN, + MODEL_TENSOR.MTP_HC_HEAD_BASE, + MODEL_TENSOR.MTP_HC_HEAD_SCALE, + ], MODEL_ARCH.ERNIE4_5_MOE: [ MODEL_TENSOR.TOKEN_EMBD, MODEL_TENSOR.OUTPUT_NORM, @@ -4028,8 +4160,10 @@ class GGMLQuantizationType(IntEnum): class ExpertGatingFuncType(IntEnum): - SOFTMAX = 1 - SIGMOID = 2 + SOFTMAX = 1 + SIGMOID = 2 + SOFTMAX_WEIGHT = 3 # applied to the router weights instead of the logits + SQRT_SOFTPLUS = 4 # DeepSeek-V4-Flash: probs = sqrt(softplus(logits)) # TODO: add GGMLFileType from ggml_ftype in ggml.h diff --git a/gguf-py/gguf/gguf_writer.py b/gguf-py/gguf/gguf_writer.py index 6a81ca37d8c4..a18ad4f401dd 100644 --- a/gguf-py/gguf/gguf_writer.py +++ b/gguf-py/gguf/gguf_writer.py @@ -913,6 +913,31 @@ def add_causal_attention(self, value: bool) -> None: def add_q_lora_rank(self, length: int) -> None: self.add_uint32(Keys.Attention.Q_LORA_RANK.format(arch=self.arch), length) + # DeepSeek-V4-Flash specific + def add_o_lora_rank(self, length: int) -> None: + self.add_uint32(Keys.Attention.O_LORA_RANK.format(arch=self.arch), length) + + def add_o_groups(self, count: int) -> None: + self.add_uint32(Keys.Attention.O_GROUPS.format(arch=self.arch), count) + + def add_compress_ratios(self, ratios: Sequence[int]) -> None: + self.add_array(Keys.Attention.COMPRESS_RATIOS.format(arch=self.arch), list(ratios)) + + def add_compress_rope_freq_base(self, base: float) -> None: + self.add_float32(Keys.Attention.COMPRESS_ROPE_FREQ_BASE.format(arch=self.arch), base) + + def add_n_hash_layers(self, n: int) -> None: + self.add_uint32(Keys.LLM.N_HASH_LAYERS.format(arch=self.arch), n) + + def add_hc_mult(self, n: int) -> None: + self.add_uint32(Keys.LLM.HC_MULT.format(arch=self.arch), n) + + def add_hc_sinkhorn_iters(self, n: int) -> None: + self.add_uint32(Keys.LLM.HC_SINKHORN_ITERS.format(arch=self.arch), n) + + def add_hc_eps(self, eps: float) -> None: + self.add_float32(Keys.LLM.HC_EPS.format(arch=self.arch), eps) + def add_kv_lora_rank(self, length: int) -> None: self.add_uint32(Keys.Attention.KV_LORA_RANK.format(arch=self.arch), length) diff --git a/src/llama-arch.cpp b/src/llama-arch.cpp index 633a66fc6651..e06b31ebe6ab 100644 --- a/src/llama-arch.cpp +++ b/src/llama-arch.cpp @@ -75,6 +75,7 @@ static const std::map LLM_ARCH_NAMES = { { LLM_ARCH_DEEPSEEK, "deepseek" }, { LLM_ARCH_DEEPSEEK2, "deepseek2" }, { LLM_ARCH_DEEPSEEK2OCR, "deepseek2-ocr" }, + { LLM_ARCH_DEEPSEEK_V4, "deepseek-v4" }, { LLM_ARCH_CHATGLM, "chatglm" }, { LLM_ARCH_GLM4, "glm4" }, { LLM_ARCH_GLM4_MOE, "glm4moe" }, @@ -241,6 +242,14 @@ static const std::map LLM_KV_NAMES = { { LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, "%s.attention.indexer.head_count" }, { LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, "%s.attention.indexer.key_length" }, { LLM_KV_ATTENTION_INDEXER_TOP_K, "%s.attention.indexer.top_k" }, + { LLM_KV_ATTENTION_O_LORA_RANK, "%s.attention.o_lora_rank" }, + { LLM_KV_ATTENTION_O_GROUPS, "%s.attention.o_groups" }, + { LLM_KV_ATTENTION_COMPRESS_RATIOS, "%s.attention.compress_ratios" }, + { LLM_KV_ATTENTION_COMPRESS_ROPE_FREQ_BASE, "%s.attention.compress_rope_freq_base" }, + { LLM_KV_HC_MULT, "%s.hyperconnections.mult" }, + { LLM_KV_HC_SINKHORN_ITERS, "%s.hyperconnections.sinkhorn_iters" }, + { LLM_KV_HC_EPS, "%s.hyperconnections.eps" }, + { LLM_KV_N_HASH_LAYERS, "%s.n_hash_layers" }, { LLM_KV_ATTENTION_SHARED_KV_LAYERS, "%s.attention.shared_kv_layers" }, { LLM_KV_ROPE_DIMENSION_COUNT, "%s.rope.dimension_count" }, @@ -547,6 +556,37 @@ static const std::map LLM_TENSOR_NAMES = { { LLM_TENSOR_INDEXER_PROJ, "blk.%d.indexer.proj" }, { LLM_TENSOR_INDEXER_ATTN_K, "blk.%d.indexer.attn_k" }, { LLM_TENSOR_INDEXER_ATTN_Q_B, "blk.%d.indexer.attn_q_b" }, + // DeepSeek-V4-Flash specific tensors + { LLM_TENSOR_ATTN_KV, "blk.%d.attn_kv" }, + { LLM_TENSOR_ATTN_KV_NORM, "blk.%d.attn_kv_norm" }, + { LLM_TENSOR_ATTN_O_A, "blk.%d.attn_o_a" }, + { LLM_TENSOR_ATTN_O_B, "blk.%d.attn_o_b" }, + { LLM_TENSOR_COMPRESSOR_WKV, "blk.%d.compressor.wkv" }, + { LLM_TENSOR_COMPRESSOR_WGATE, "blk.%d.compressor.wgate" }, + { LLM_TENSOR_COMPRESSOR_APE, "blk.%d.compressor.ape" }, + { LLM_TENSOR_COMPRESSOR_NORM, "blk.%d.compressor.norm" }, + { LLM_TENSOR_INDEXER_COMPRESSOR_WKV, "blk.%d.indexer.compressor.wkv" }, + { LLM_TENSOR_INDEXER_COMPRESSOR_WGATE, "blk.%d.indexer.compressor.wgate" }, + { LLM_TENSOR_INDEXER_COMPRESSOR_APE, "blk.%d.indexer.compressor.ape" }, + { LLM_TENSOR_INDEXER_COMPRESSOR_NORM, "blk.%d.indexer.compressor.norm" }, + { LLM_TENSOR_HC_ATTN_FN, "blk.%d.hc_attn.fn" }, + { LLM_TENSOR_HC_ATTN_BASE, "blk.%d.hc_attn.base" }, + { LLM_TENSOR_HC_ATTN_SCALE, "blk.%d.hc_attn.scale" }, + { LLM_TENSOR_HC_FFN_FN, "blk.%d.hc_ffn.fn" }, + { LLM_TENSOR_HC_FFN_BASE, "blk.%d.hc_ffn.base" }, + { LLM_TENSOR_HC_FFN_SCALE, "blk.%d.hc_ffn.scale" }, + { LLM_TENSOR_HC_HEAD_FN, "output.hc_head.fn" }, + { LLM_TENSOR_HC_HEAD_BASE, "output.hc_head.base" }, + { LLM_TENSOR_HC_HEAD_SCALE, "output.hc_head.scale" }, + { LLM_TENSOR_FFN_GATE_TID2EID, "blk.%d.ffn_gate_tid2eid" }, + { LLM_TENSOR_MTP_E_PROJ, "blk.%d.mtp.e_proj" }, + { LLM_TENSOR_MTP_H_PROJ, "blk.%d.mtp.h_proj" }, + { LLM_TENSOR_MTP_ENORM, "blk.%d.mtp.enorm" }, + { LLM_TENSOR_MTP_HNORM, "blk.%d.mtp.hnorm" }, + { LLM_TENSOR_MTP_NORM, "blk.%d.mtp.norm" }, + { LLM_TENSOR_MTP_HC_HEAD_FN, "blk.%d.mtp.hc_head.fn" }, + { LLM_TENSOR_MTP_HC_HEAD_BASE, "blk.%d.mtp.hc_head.base" }, + { LLM_TENSOR_MTP_HC_HEAD_SCALE, "blk.%d.mtp.hc_head.scale" }, }; // declare information about the model weight tensors: @@ -767,6 +807,37 @@ static const std::map LLM_TENSOR_INFOS = { // Nemotron 3 Super {LLM_TENSOR_FFN_LATENT_DOWN, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}}, {LLM_TENSOR_FFN_LATENT_UP, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}}, + // DeepSeek-V4-Flash + {LLM_TENSOR_ATTN_KV, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_ATTN_KV_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}}, + {LLM_TENSOR_ATTN_O_A, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_ATTN_O_B, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_COMPRESSOR_WKV, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_COMPRESSOR_WGATE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_COMPRESSOR_APE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_ADD}}, + {LLM_TENSOR_COMPRESSOR_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}}, + {LLM_TENSOR_INDEXER_COMPRESSOR_WKV, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_INDEXER_COMPRESSOR_WGATE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_INDEXER_COMPRESSOR_APE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_ADD}}, + {LLM_TENSOR_INDEXER_COMPRESSOR_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}}, + {LLM_TENSOR_HC_ATTN_FN, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_HC_ATTN_BASE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_ADD}}, + {LLM_TENSOR_HC_ATTN_SCALE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}}, + {LLM_TENSOR_HC_FFN_FN, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_HC_FFN_BASE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_ADD}}, + {LLM_TENSOR_HC_FFN_SCALE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}}, + {LLM_TENSOR_HC_HEAD_FN, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_HC_HEAD_BASE, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_ADD}}, + {LLM_TENSOR_HC_HEAD_SCALE, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL}}, + {LLM_TENSOR_FFN_GATE_TID2EID, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_GET_ROWS}}, + {LLM_TENSOR_MTP_E_PROJ, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_MTP_H_PROJ, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_MTP_ENORM, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL}}, + {LLM_TENSOR_MTP_HNORM, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL}}, + {LLM_TENSOR_MTP_NORM, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL}}, + {LLM_TENSOR_MTP_HC_HEAD_FN, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}}, + {LLM_TENSOR_MTP_HC_HEAD_BASE, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_ADD}}, + {LLM_TENSOR_MTP_HC_HEAD_SCALE, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL}}, }; LLM_KV::LLM_KV(llm_arch arch, const char * suffix) : arch(arch), suffix(suffix) {} @@ -890,6 +961,7 @@ bool llm_arch_supports_sm_tensor(const llm_arch & arch) { case LLM_ARCH_OLMO2: case LLM_ARCH_OLMOE: case LLM_ARCH_DEEPSEEK2: + case LLM_ARCH_DEEPSEEK_V4: case LLM_ARCH_GLM_DSA: case LLM_ARCH_BITNET: case LLM_ARCH_T5: diff --git a/src/llama-arch.h b/src/llama-arch.h index 8f335f5c7b3e..3829fe828ac7 100644 --- a/src/llama-arch.h +++ b/src/llama-arch.h @@ -79,6 +79,7 @@ enum llm_arch { LLM_ARCH_DEEPSEEK, LLM_ARCH_DEEPSEEK2, LLM_ARCH_DEEPSEEK2OCR, + LLM_ARCH_DEEPSEEK_V4, LLM_ARCH_CHATGLM, LLM_ARCH_GLM4, LLM_ARCH_GLM4_MOE, @@ -246,6 +247,14 @@ enum llm_kv { LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, LLM_KV_ATTENTION_INDEXER_TOP_K, LLM_KV_ATTENTION_SHARED_KV_LAYERS, + LLM_KV_ATTENTION_O_LORA_RANK, + LLM_KV_ATTENTION_O_GROUPS, + LLM_KV_ATTENTION_COMPRESS_RATIOS, + LLM_KV_ATTENTION_COMPRESS_ROPE_FREQ_BASE, + LLM_KV_HC_MULT, + LLM_KV_HC_SINKHORN_ITERS, + LLM_KV_HC_EPS, + LLM_KV_N_HASH_LAYERS, LLM_KV_ROPE_DIMENSION_COUNT, LLM_KV_ROPE_DIMENSION_COUNT_SWA, @@ -554,6 +563,37 @@ enum llm_tensor { LLM_TENSOR_NEXTN_HNORM, LLM_TENSOR_NEXTN_SHARED_HEAD_HEAD, LLM_TENSOR_NEXTN_SHARED_HEAD_NORM, + // DeepSeek-V4-Flash specific tensors + LLM_TENSOR_ATTN_KV, // V4 single shared K=V projection (MQA), distinct from KV_A_MQA + KV_B + LLM_TENSOR_ATTN_KV_NORM, // V4 RMSNorm on shared KV, distinct from KV_A_NORM (MLA two-stage) + LLM_TENSOR_ATTN_O_A, // V4 grouped low-rank wo_a [n_groups, o_lora_rank, n_heads*head_dim/n_groups] + LLM_TENSOR_ATTN_O_B, // V4 wo_b [n_groups*o_lora_rank, n_embd] + LLM_TENSOR_COMPRESSOR_WKV, // V4 KV compression linear (CSA + HCA) + LLM_TENSOR_COMPRESSOR_WGATE, // V4 KV compression gate + LLM_TENSOR_COMPRESSOR_APE, // V4 additive positional embedding for compression + LLM_TENSOR_COMPRESSOR_NORM, // V4 RMSNorm post-compression + LLM_TENSOR_INDEXER_COMPRESSOR_WKV, // V4 indexer compressor (separate state from main compressor) + LLM_TENSOR_INDEXER_COMPRESSOR_WGATE, + LLM_TENSOR_INDEXER_COMPRESSOR_APE, + LLM_TENSOR_INDEXER_COMPRESSOR_NORM, + LLM_TENSOR_HC_ATTN_FN, // mHC linear projection for attn block split + LLM_TENSOR_HC_ATTN_BASE, + LLM_TENSOR_HC_ATTN_SCALE, + LLM_TENSOR_HC_FFN_FN, + LLM_TENSOR_HC_FFN_BASE, + LLM_TENSOR_HC_FFN_SCALE, + LLM_TENSOR_HC_HEAD_FN, // model-level mHC head projection (sigmoid+eps reduce only) + LLM_TENSOR_HC_HEAD_BASE, + LLM_TENSOR_HC_HEAD_SCALE, + LLM_TENSOR_FFN_GATE_TID2EID, // hash routing LUT for first n_hash_layers + LLM_TENSOR_MTP_E_PROJ, // MTP next-token-embedding projection + LLM_TENSOR_MTP_H_PROJ, // MTP hidden-state projection + LLM_TENSOR_MTP_ENORM, // MTP RMSNorm on embedded next-token + LLM_TENSOR_MTP_HNORM, // MTP RMSNorm on hidden state + LLM_TENSOR_MTP_NORM, // MTP final norm before head + LLM_TENSOR_MTP_HC_HEAD_FN, // MTP-specific hc_head projection + LLM_TENSOR_MTP_HC_HEAD_BASE, + LLM_TENSOR_MTP_HC_HEAD_SCALE, }; enum llm_tensor_layer { diff --git a/src/llama-chat.cpp b/src/llama-chat.cpp index 6554a89b28a6..b047f495e7d1 100644 --- a/src/llama-chat.cpp +++ b/src/llama-chat.cpp @@ -49,6 +49,7 @@ static const std::map LLM_CHAT_TEMPLATES = { { "deepseek", LLM_CHAT_TEMPLATE_DEEPSEEK }, { "deepseek2", LLM_CHAT_TEMPLATE_DEEPSEEK_2 }, { "deepseek3", LLM_CHAT_TEMPLATE_DEEPSEEK_3 }, + { "deepseek-v4", LLM_CHAT_TEMPLATE_DEEPSEEK_V4 }, { "deepseek-ocr", LLM_CHAT_TEMPLATE_DEEPSEEK_OCR }, { "command-r", LLM_CHAT_TEMPLATE_COMMAND_R }, { "llama3", LLM_CHAT_TEMPLATE_LLAMA_3 }, @@ -181,6 +182,11 @@ llm_chat_template llm_chat_detect_template(const std::string & tmpl) { return LLM_CHAT_TEMPLATE_MINICPM; } else if (tmpl_contains("'Assistant: ' + message['content'] + eos_token")) { return LLM_CHAT_TEMPLATE_DEEPSEEK_2; + } else if (tmpl_contains(LU8("<|Assistant|>")) && tmpl_contains(LU8("<|User|>")) && tmpl_contains(LU8("<|end▁of▁sentence|>")) && tmpl_contains("")) { + // DeepSeek-V4-Flash: chat-mode appends after <|Assistant|>; + // distinguished from V3 (which has no in its template). + // ref: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/encoding/encoding_dsv4.py + return LLM_CHAT_TEMPLATE_DEEPSEEK_V4; } else if (tmpl_contains(LU8("<|Assistant|>")) && tmpl_contains(LU8("<|User|>")) && tmpl_contains(LU8("<|end▁of▁sentence|>"))) { return LLM_CHAT_TEMPLATE_DEEPSEEK_3; } else if (tmpl_contains("[|system|]") && tmpl_contains("[|assistant|]") && tmpl_contains("[|endofturn|]")) { @@ -556,6 +562,54 @@ int32_t llm_chat_apply_template( if (add_ass) { ss << LU8("<|Assistant|>"); } + } else if (tmpl == LLM_CHAT_TEMPLATE_DEEPSEEK_V4) { + // DeepSeek-V4-Flash, chat mode (encoding_dsv4.encode_messages with thinking_mode="chat"). + // + // Layout: + // <|begin▁of▁sentence|>{system}<|User|>{user1}<|Assistant|>{assistant1}<|end▁of▁sentence|>...<|Assistant|> + // + // Key differences from V3: + // - BOS is emitted by the template (V4's tokenizer_config sets add_bos_token=False, + // so the tokenizer does not prepend it; encoding_dsv4 lines 17, 546 prepend it + // explicitly). + // - assistant content has NO <|Assistant|> prefix (the prefix is emitted as + // a turn-transition suffix after every user/developer message, then closed + // with for chat mode). + // - system_msg_template is "{content}" with no separator. + // - The sentinel after <|Assistant|> tells the model to skip + // thinking-mode reasoning and emit the chat-mode response directly. + // + // Refs: + // - encoding/encoding_dsv4.py:render_message (handles "system", "user", "assistant", task suffixes) + // - encoding/encoding_dsv4.py:encode_messages (BOS handling, multi-turn) + ss << LU8("<|begin▁of▁sentence|>"); + const size_t n = chat.size(); + for (size_t i = 0; i < n; ++i) { + const auto * message = chat[i]; + std::string role(message->role); + if (role == "system") { + ss << message->content; + } else if (role == "user") { + ss << LU8("<|User|>") << message->content; + // Mid-conversation user→assistant turn transition: emit Assistant + + // before the next assistant content (matches encoding_dsv4 line 384-392 chat-mode path). + if (i + 1 < n && std::string(chat[i + 1]->role) == "assistant") { + ss << LU8("<|Assistant|>") << ""; + } + } else if (role == "developer") { + // V4 "developer" role: rendered as a user message prefixed with <|User|> + // (encoding_dsv4 line 272-283, chat-mode subset, no tools/response_format). + ss << LU8("<|User|>") << message->content; + if (i + 1 < n && std::string(chat[i + 1]->role) == "assistant") { + ss << LU8("<|Assistant|>") << ""; + } + } else if (role == "assistant") { + ss << message->content << LU8("<|end▁of▁sentence|>"); + } + } + if (add_ass) { + ss << LU8("<|Assistant|>") << ""; + } } else if (tmpl == LLM_CHAT_TEMPLATE_DEEPSEEK_OCR) { for (auto message : chat) { // no template diff --git a/src/llama-chat.h b/src/llama-chat.h index 13f936a946c4..b1349dc2ea5d 100644 --- a/src/llama-chat.h +++ b/src/llama-chat.h @@ -28,6 +28,7 @@ enum llm_chat_template { LLM_CHAT_TEMPLATE_DEEPSEEK, LLM_CHAT_TEMPLATE_DEEPSEEK_2, LLM_CHAT_TEMPLATE_DEEPSEEK_3, + LLM_CHAT_TEMPLATE_DEEPSEEK_V4, LLM_CHAT_TEMPLATE_DEEPSEEK_OCR, LLM_CHAT_TEMPLATE_COMMAND_R, LLM_CHAT_TEMPLATE_LLAMA_3, diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp index 2ff23f87cf44..9b438db893c1 100644 --- a/src/llama-graph.cpp +++ b/src/llama-graph.cpp @@ -1269,6 +1269,25 @@ ggml_tensor * llm_graph_context::build_ffn( cur = ggml_reglu(ctx0, cur); cb(cur, "ffn_reglu", il); } break; + case LLM_FFN_SWIGLU_CLAMPED_PRE: + { + // DeepSeek-V4-Flash: gate.clamp(-INF, lim) → silu, up.clamp(±lim), gate*up. + // Limit per-layer is read from hparams.swiglu_clamp_{exp,shexp}; the + // shared-expert path uses swiglu_clamp_shexp, the (rare) dense FFN path + // uses swiglu_clamp_exp[0]. Routed-expert path is handled inside build_moe_ffn. + // ref: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/inference/model.py:600-602 + GGML_ASSERT(gate != nullptr && type_gate == LLM_FFN_PAR && "SWIGLU_CLAMPED_PRE expects parallel gate/up"); + const float limit = (il >= 0) ? hparams.swiglu_clamp_shexp[il] : hparams.swiglu_clamp_exp[0]; + ggml_tensor * gate_clamped = ggml_clamp(ctx0, cur, -INFINITY, limit); + cb(gate_clamped, "ffn_gate_clamped", il); + gate_clamped = ggml_silu(ctx0, gate_clamped); + cb(gate_clamped, "ffn_gate_silu", il); + ggml_tensor * up_clamped = ggml_clamp(ctx0, tmp, -limit, limit); + cb(up_clamped, "ffn_up_clamped", il); + cur = ggml_mul(ctx0, gate_clamped, up_clamped); + cb(cur, "ffn_swiglu_clamped_pre", il); + type_gate = LLM_FFN_SEQ; + } break; default: GGML_ABORT("fatal error"); } @@ -1400,6 +1419,13 @@ ggml_tensor * llm_graph_context::build_moe_ffn( { probs = logits; // [n_expert, n_tokens] } break; + case LLAMA_EXPERT_GATING_FUNC_TYPE_SQRT_SOFTPLUS: + { + // DeepSeek-V4-Flash: probs = sqrt(softplus(logits)) + // ref: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/inference/model.py:565,571 + ggml_tensor * sp = ggml_softplus(ctx0, logits); + probs = ggml_sqrt(ctx0, sp); + } break; default: GGML_ABORT("fatal error"); } @@ -1639,6 +1665,22 @@ ggml_tensor * llm_graph_context::build_moe_ffn( cur = ggml_sqr(ctx0, cur); cb(cur, "ffn_moe_relu_sqr", il); } break; + case LLM_FFN_SWIGLU_CLAMPED_PRE: + { + // DeepSeek-V4-Flash routed experts: clamp(gate, -inf, lim) → silu, clamp(up, ±lim), gate*up. + // Note: unlike the post-silu-clamp variant above (Step35-style) which clamps AFTER silu, + // V4 clamps BEFORE silu — see SGLang #23776 / inference/model.py:600-602. + GGML_ASSERT(has_gate); + const float limit = hparams.swiglu_clamp_exp[il]; + ggml_tensor * gate_clamped = ggml_clamp(ctx0, cur, -INFINITY, limit); + cb(gate_clamped, "ffn_moe_gate_clamped", il); + gate_clamped = ggml_silu(ctx0, gate_clamped); + cb(gate_clamped, "ffn_moe_gate_silu", il); + ggml_tensor * up_clamped = ggml_clamp(ctx0, up, -limit, limit); + cb(up_clamped, "ffn_moe_up_clamped", il); + cur = ggml_mul(ctx0, gate_clamped, up_clamped); + cb(cur, "ffn_moe_swiglu_clamped_pre", il); + } break; default: GGML_ABORT("fatal error"); } diff --git a/src/llama-graph.h b/src/llama-graph.h index 5cb1756c6a97..3f701d726a85 100644 --- a/src/llama-graph.h +++ b/src/llama-graph.h @@ -43,6 +43,10 @@ enum llm_ffn_op_type { LLM_FFN_GEGLU, LLM_FFN_REGLU, LLM_FFN_SWIGLU_OAI_MOE, + // DeepSeek-V4-Flash: gate.clamp(-INF, lim) → silu, up.clamp(±lim), gate*up. + // Limit per-layer is read from hparams.swiglu_clamp_{exp,shexp}. + // ref: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/inference/model.py:600-602 + LLM_FFN_SWIGLU_CLAMPED_PRE, }; enum llm_ffn_gate_type { diff --git a/src/llama-hparams.h b/src/llama-hparams.h index ac7f9ee86502..524e3062cc08 100644 --- a/src/llama-hparams.h +++ b/src/llama-hparams.h @@ -14,6 +14,7 @@ enum llama_expert_gating_func_type { LLAMA_EXPERT_GATING_FUNC_TYPE_SOFTMAX = 1, LLAMA_EXPERT_GATING_FUNC_TYPE_SIGMOID = 2, LLAMA_EXPERT_GATING_FUNC_TYPE_SOFTMAX_WEIGHT = 3, // applied to the router weights instead of the logits + LLAMA_EXPERT_GATING_FUNC_TYPE_SQRT_SOFTPLUS = 4, // DeepSeek-V4: probs = sqrt(softplus(logits)) }; enum llama_swa_type { @@ -207,6 +208,16 @@ struct llama_hparams { uint32_t indexer_head_size = 0; uint32_t indexer_top_k = 0; + // DeepSeek-V4-Flash specific + uint32_t v4_o_lora_rank = 0; // wo_a low-rank dim per group + uint32_t v4_o_groups = 0; // grouped low-rank wo (n_groups) + float v4_compress_rope_freq_base = 0.0f; // distinct from rope_freq_base; CSA/HCA layers use this + uint32_t v4_hc_mult = 0; // mHC stream count (4 for V4-Flash) + uint32_t v4_hc_sinkhorn_iters = 0; + float v4_hc_eps = 1e-6f; + uint32_t v4_n_hash_layers = 0; // first N layers use tid2eid hash routing + std::array v4_compress_ratios; // 0=SWA, 4=CSA, 128=HCA + // qwen3vl deepstack uint32_t n_deepstack_layers = 0; diff --git a/src/llama-model-loader.cpp b/src/llama-model-loader.cpp index 4e65a45a50d8..cde2ce9a1c0b 100644 --- a/src/llama-model-loader.cpp +++ b/src/llama-model-loader.cpp @@ -393,6 +393,8 @@ namespace GGUFMeta { } template bool llama_model_loader::get_arr>(enum llm_kv kid, std::vector & result, bool required); + // DeepSeek-V4-Flash compress_ratios array (uint32, length up to LLAMA_MAX_LAYERS). + template bool llama_model_loader::get_arr>(enum llm_kv kid, std::array & result, bool required); template bool llama_model_loader::get_key(const std::string & key, T & result, bool required) { diff --git a/src/llama-model.cpp b/src/llama-model.cpp index 9e2a13cbd43e..30cc771fce9e 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -2014,6 +2014,58 @@ void llama_model::load_hparams(llama_model_loader & ml) { default: type = LLM_TYPE_UNKNOWN; } } break; + case LLM_ARCH_DEEPSEEK_V4: + { + // DeepSeek-V4-Flash: hybrid SWA/CSA/HCA attention, mHC residuals, + // hash-routed MoE, MQA-style single shared K=V. + // ref: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/inference/model.py + ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps); + ml.get_key(LLM_KV_LEADING_DENSE_BLOCK_COUNT, hparams.n_layer_dense_lead, false); + ml.get_key(LLM_KV_ATTENTION_Q_LORA_RANK, hparams.n_lora_q); + ml.get_key(LLM_KV_ATTENTION_O_LORA_RANK, hparams.v4_o_lora_rank); + ml.get_key(LLM_KV_ATTENTION_O_GROUPS, hparams.v4_o_groups); + ml.get_key(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp); + ml.get_key(LLM_KV_EXPERT_SHARED_COUNT, hparams.n_expert_shared); + ml.get_key(LLM_KV_EXPERT_WEIGHTS_SCALE, hparams.expert_weights_scale, false); + ml.get_key(LLM_KV_EXPERT_WEIGHTS_NORM, hparams.expert_weights_norm, false); + ml.get_key(LLM_KV_EXPERT_GATING_FUNC, hparams.expert_gating_func, false); + ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa, false); + hparams.swa_type = LLAMA_SWA_TYPE_STANDARD; + + // V4 indexer (used in CSA layers; loaded but unused in MVP graph - dense fallback) + ml.get_key(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, hparams.indexer_n_head, false); + ml.get_key(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, hparams.indexer_head_size, false); + ml.get_key(LLM_KV_ATTENTION_INDEXER_TOP_K, hparams.indexer_top_k, false); + + // mHC + V4-specific + ml.get_key(LLM_KV_HC_MULT, hparams.v4_hc_mult); + ml.get_key(LLM_KV_HC_SINKHORN_ITERS, hparams.v4_hc_sinkhorn_iters); + ml.get_key(LLM_KV_HC_EPS, hparams.v4_hc_eps, false); + ml.get_key(LLM_KV_N_HASH_LAYERS, hparams.v4_n_hash_layers, false); + ml.get_key(LLM_KV_NEXTN_PREDICT_LAYERS, hparams.nextn_predict_layers, false); + ml.get_key(LLM_KV_ATTENTION_COMPRESS_ROPE_FREQ_BASE, hparams.v4_compress_rope_freq_base, false); + + // compress_ratios is a uint32 array of length (n_layer + nextn_predict_layers) + { + std::array ratios_u32{}; + if (ml.get_arr(LLM_KV_ATTENTION_COMPRESS_RATIOS, ratios_u32, false)) { + const uint32_t n = hparams.n_layer + hparams.nextn_predict_layers; + for (uint32_t i = 0; i < n && i < ratios_u32.size(); ++i) { + hparams.v4_compress_ratios[i] = (uint8_t) ratios_u32[i]; + } + } + } + + // swiglu_limit applied to both routed and shared experts (per SGLang #23776). + // Writer emits an array of length n_layer so the value is read via get_key_or_arr. + ml.get_key_or_arr(LLM_KV_SWIGLU_CLAMP_EXP, hparams.swiglu_clamp_exp, hparams.n_layer, false); + ml.get_key_or_arr(LLM_KV_SWIGLU_CLAMP_SHEXP, hparams.swiglu_clamp_shexp, hparams.n_layer, false); + + switch (hparams.n_layer) { + case 43: type = LLM_TYPE_236B; break; // V4-Flash variant + default: type = LLM_TYPE_UNKNOWN; + } + } break; case LLM_ARCH_DEEPSEEK2OCR: { // similar to deepseek2, but without MLA @@ -5360,6 +5412,131 @@ bool llama_model::load_tensors(llama_model_loader & ml) { } } } break; + case LLM_ARCH_DEEPSEEK_V4: + { + // DeepSeek-V4-Flash. PR1: full per-layer tensor declaration so the + // converter's GGUF loads end-to-end; the stub graph still uses only + // token_embd + output_norm + output (junk logits). All per-layer + // tensors are TENSOR_NOT_REQUIRED so partial / pre-PR2 GGUFs still load. + // + // ref: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/inference/model.py + const int64_t q_lora_rank = hparams.n_lora_q; + const int64_t o_lora_rank = hparams.v4_o_lora_rank; + const int64_t n_groups = hparams.v4_o_groups; + const int64_t head_dim = (int64_t) hparams.n_embd_head_k(0); // 512 + const int64_t rope_dim = (int64_t) hparams.n_rot(0); // 64 + (void) rope_dim; // unused in PR1 loader + const int64_t hc_mult = hparams.v4_hc_mult; // 4 + const int64_t idx_n_heads = hparams.indexer_n_head; // 64 + const int64_t idx_head_dim = hparams.indexer_head_size;// 128 + const int64_t n_layer_main = hparams.n_layer - hparams.nextn_predict_layers; + const int64_t n_hash_layers = (int64_t) hparams.v4_n_hash_layers; + // Shadowed-as-int64 for clean brace-list deductions in {ne[0], ne[1], ...}. + const int64_t n_ff_exp = (int64_t) hparams.n_ff_exp; + const int64_t n_expert_shared = (int64_t) hparams.n_expert_shared; + + tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0); + output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0); + output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, TENSOR_NOT_REQUIRED); + if (!output) { + output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED); + } + + // Model-level mHC head (collapses hc_mult streams to 1 before lm_head). + v4_hc_head_fn = create_tensor(tn(LLM_TENSOR_HC_HEAD_FN, "weight"), {hc_mult * n_embd, hc_mult}, TENSOR_NOT_REQUIRED); + v4_hc_head_base = create_tensor(tn(LLM_TENSOR_HC_HEAD_BASE, "weight"), {hc_mult}, TENSOR_NOT_REQUIRED); + v4_hc_head_scale = create_tensor(tn(LLM_TENSOR_HC_HEAD_SCALE, "weight"), {1}, TENSOR_NOT_REQUIRED); + + for (int i = 0; i < (int) hparams.n_layer; ++i) { + auto & layer = layers[i]; + const bool is_mtp = (i >= n_layer_main); + const uint8_t cr = hparams.v4_compress_ratios[i]; + const bool has_compressor = (cr != 0); + const bool has_indexer = (cr == 4); + // First n_hash_layers are hash-routed (tid2eid LUT, no router bias). + const bool is_hash_layer = (i < n_hash_layers); + + // Norms (attn_norm, ffn_norm, attn_q_a_norm, attn_kv_norm). + layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, TENSOR_NOT_REQUIRED); + layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, TENSOR_NOT_REQUIRED); + layer.attn_q_a_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_A_NORM, "weight", i), {q_lora_rank}, TENSOR_NOT_REQUIRED); + layer.v4_attn_kv_norm = create_tensor(tn(LLM_TENSOR_ATTN_KV_NORM, "weight", i), {head_dim}, TENSOR_NOT_REQUIRED); + + // Attention parameters. + layer.attn_sinks = create_tensor(tn(LLM_TENSOR_ATTN_SINKS, "weight", i), {n_head}, TENSOR_NOT_REQUIRED); + layer.wq_a = create_tensor(tn(LLM_TENSOR_ATTN_Q_A, "weight", i), {n_embd, q_lora_rank}, TENSOR_NOT_REQUIRED); + layer.wq_b = create_tensor(tn(LLM_TENSOR_ATTN_Q_B, "weight", i), {q_lora_rank, n_head * head_dim}, TENSOR_NOT_REQUIRED); + layer.v4_attn_kv = create_tensor(tn(LLM_TENSOR_ATTN_KV, "weight", i), {n_embd, head_dim}, TENSOR_NOT_REQUIRED); + // attn_o_a is stored as a 3D tensor [dim_per_group, o_lora_rank, n_groups] + // so the C++ side can use mul_mat_id on the n_groups axis. + layer.v4_attn_o_a = create_tensor(tn(LLM_TENSOR_ATTN_O_A, "weight", i), + {(n_head * head_dim) / n_groups, o_lora_rank, n_groups}, TENSOR_NOT_REQUIRED); + layer.v4_attn_o_b = create_tensor(tn(LLM_TENSOR_ATTN_O_B, "weight", i), + {n_groups * o_lora_rank, n_embd}, TENSOR_NOT_REQUIRED); + + // mHC (per-block). + // shape note: hc_attn_fn comes from inference/model.py:660-663 + // nn.Linear(hc_mult*n_embd, (2+hc_mult)*hc_mult) → weight [(2+hc_mult)*hc_mult, hc_mult*n_embd] + layer.v4_hc_attn_fn = create_tensor(tn(LLM_TENSOR_HC_ATTN_FN, "weight", i), + {hc_mult * n_embd, (2 + hc_mult) * hc_mult}, TENSOR_NOT_REQUIRED); + layer.v4_hc_attn_base = create_tensor(tn(LLM_TENSOR_HC_ATTN_BASE, "weight", i), {(2 + hc_mult) * hc_mult}, TENSOR_NOT_REQUIRED); + layer.v4_hc_attn_scale = create_tensor(tn(LLM_TENSOR_HC_ATTN_SCALE, "weight", i), {3}, TENSOR_NOT_REQUIRED); + layer.v4_hc_ffn_fn = create_tensor(tn(LLM_TENSOR_HC_FFN_FN, "weight", i), + {hc_mult * n_embd, (2 + hc_mult) * hc_mult}, TENSOR_NOT_REQUIRED); + layer.v4_hc_ffn_base = create_tensor(tn(LLM_TENSOR_HC_FFN_BASE, "weight", i), {(2 + hc_mult) * hc_mult}, TENSOR_NOT_REQUIRED); + layer.v4_hc_ffn_scale = create_tensor(tn(LLM_TENSOR_HC_FFN_SCALE, "weight", i), {3}, TENSOR_NOT_REQUIRED); + + // Compressor (CSA + HCA layers). coff=2 if ratio==4 (overlap_transform), else 1. + if (has_compressor) { + const int64_t coff = (cr == 4) ? 2 : 1; + layer.v4_compressor_wkv = create_tensor(tn(LLM_TENSOR_COMPRESSOR_WKV, "weight", i), {n_embd, coff * head_dim}, TENSOR_NOT_REQUIRED); + layer.v4_compressor_wgate = create_tensor(tn(LLM_TENSOR_COMPRESSOR_WGATE, "weight", i), {n_embd, coff * head_dim}, TENSOR_NOT_REQUIRED); + layer.v4_compressor_ape = create_tensor(tn(LLM_TENSOR_COMPRESSOR_APE, "weight", i), {coff * head_dim, (int64_t) cr}, TENSOR_NOT_REQUIRED); + layer.v4_compressor_norm = create_tensor(tn(LLM_TENSOR_COMPRESSOR_NORM, "weight", i), {head_dim}, TENSOR_NOT_REQUIRED); + } + + // Indexer (CSA layers only). Indexer's compressor uses ratio==4 (coff=2). + if (has_indexer) { + layer.indexer_attn_q_b = create_tensor(tn(LLM_TENSOR_INDEXER_ATTN_Q_B, "weight", i), {q_lora_rank, idx_n_heads * idx_head_dim}, TENSOR_NOT_REQUIRED); + layer.indexer_proj = create_tensor(tn(LLM_TENSOR_INDEXER_PROJ, "weight", i), {n_embd, idx_n_heads}, TENSOR_NOT_REQUIRED); + const int64_t idx_coff = 2; + layer.v4_idx_compressor_wkv = create_tensor(tn(LLM_TENSOR_INDEXER_COMPRESSOR_WKV, "weight", i), {n_embd, idx_coff * idx_head_dim}, TENSOR_NOT_REQUIRED); + layer.v4_idx_compressor_wgate = create_tensor(tn(LLM_TENSOR_INDEXER_COMPRESSOR_WGATE, "weight", i), {n_embd, idx_coff * idx_head_dim}, TENSOR_NOT_REQUIRED); + layer.v4_idx_compressor_ape = create_tensor(tn(LLM_TENSOR_INDEXER_COMPRESSOR_APE, "weight", i), {idx_coff * idx_head_dim, 4}, TENSOR_NOT_REQUIRED); + layer.v4_idx_compressor_norm = create_tensor(tn(LLM_TENSOR_INDEXER_COMPRESSOR_NORM, "weight", i), {idx_head_dim}, TENSOR_NOT_REQUIRED); + } + + // MoE: gate_inp + (hash routing OR router bias) + experts + shared expert. + layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", i), {n_embd, n_expert}, TENSOR_NOT_REQUIRED); + if (is_hash_layer) { + // hash-route layers: tid2eid LUT [vocab_size, n_expert_used], stored as I32. + layer.v4_ffn_gate_tid2eid = create_tensor(tn(LLM_TENSOR_FFN_GATE_TID2EID, "weight", i), {(int64_t) n_expert_used, n_vocab}, TENSOR_NOT_REQUIRED); + } else { + // score-routed layers: have a router bias for top-k selection only. + layer.ffn_exp_probs_b = create_tensor(tn(LLM_TENSOR_FFN_EXP_PROBS_B, "bias", i), {n_expert}, TENSOR_NOT_REQUIRED); + } + // Stacked routed experts (shape [moe_inter, n_embd, n_expert] etc., matching DeepSeek-V2 conv). + layer.ffn_gate_exps = create_tensor(tn(LLM_TENSOR_FFN_GATE_EXP, "weight", i), {n_embd, n_ff_exp, n_expert}, TENSOR_NOT_REQUIRED); + layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXP, "weight", i), {n_ff_exp, n_embd, n_expert}, TENSOR_NOT_REQUIRED); + layer.ffn_up_exps = create_tensor(tn(LLM_TENSOR_FFN_UP_EXP, "weight", i), {n_embd, n_ff_exp, n_expert}, TENSOR_NOT_REQUIRED); + // Shared expert (n_expert_shared==1 for V4-Flash). + layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", i), {n_embd, n_ff_exp * n_expert_shared}, TENSOR_NOT_REQUIRED); + layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", i), {n_ff_exp * n_expert_shared, n_embd}, TENSOR_NOT_REQUIRED); + layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", i), {n_embd, n_ff_exp * n_expert_shared}, TENSOR_NOT_REQUIRED); + + // MTP block extras. + if (is_mtp) { + layer.v4_mtp_e_proj = create_tensor(tn(LLM_TENSOR_MTP_E_PROJ, "weight", i), {n_embd, n_embd}, TENSOR_NOT_REQUIRED); + layer.v4_mtp_h_proj = create_tensor(tn(LLM_TENSOR_MTP_H_PROJ, "weight", i), {n_embd, n_embd}, TENSOR_NOT_REQUIRED); + layer.v4_mtp_enorm = create_tensor(tn(LLM_TENSOR_MTP_ENORM, "weight", i), {n_embd}, TENSOR_NOT_REQUIRED); + layer.v4_mtp_hnorm = create_tensor(tn(LLM_TENSOR_MTP_HNORM, "weight", i), {n_embd}, TENSOR_NOT_REQUIRED); + layer.v4_mtp_norm = create_tensor(tn(LLM_TENSOR_MTP_NORM, "weight", i), {n_embd}, TENSOR_NOT_REQUIRED); + layer.v4_mtp_hc_head_fn = create_tensor(tn(LLM_TENSOR_MTP_HC_HEAD_FN, "weight", i), {hc_mult * n_embd, hc_mult}, TENSOR_NOT_REQUIRED); + layer.v4_mtp_hc_head_base = create_tensor(tn(LLM_TENSOR_MTP_HC_HEAD_BASE, "weight", i), {hc_mult}, TENSOR_NOT_REQUIRED); + layer.v4_mtp_hc_head_scale = create_tensor(tn(LLM_TENSOR_MTP_HC_HEAD_SCALE, "weight", i), {1}, TENSOR_NOT_REQUIRED); + } + } + } break; case LLM_ARCH_DEEPSEEK2OCR: { // similar to deepseek2, but without MLA @@ -8840,6 +9017,10 @@ ggml_cgraph * llama_model::build_graph(const llm_graph_params & params) const { { llm = std::make_unique(*this, params); } break; + case LLM_ARCH_DEEPSEEK_V4: + { + llm = std::make_unique(*this, params); + } break; case LLM_ARCH_CHATGLM: { llm = std::make_unique(*this, params); @@ -9236,6 +9417,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) { case LLM_ARCH_DEEPSEEK: case LLM_ARCH_DEEPSEEK2: case LLM_ARCH_DEEPSEEK2OCR: + case LLM_ARCH_DEEPSEEK_V4: case LLM_ARCH_PLM: case LLM_ARCH_CHATGLM: case LLM_ARCH_GRANITE: diff --git a/src/llama-model.h b/src/llama-model.h index 5f101bd63745..bd7d2da473b3 100644 --- a/src/llama-model.h +++ b/src/llama-model.h @@ -484,6 +484,45 @@ struct llama_layer { struct ggml_tensor * indexer_attn_k = nullptr; struct ggml_tensor * indexer_attn_q_b = nullptr; // note: for lora a/b, not bias + // DeepSeek-V4-Flash. ref: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/inference/model.py + struct ggml_tensor * v4_attn_kv = nullptr; // single shared K=V matmul (MQA) + struct ggml_tensor * v4_attn_kv_norm = nullptr; // RMSNorm over head_dim after wkv + struct ggml_tensor * v4_attn_o_a = nullptr; // grouped low-rank: 3D [n_heads*head_dim/n_groups, o_lora_rank, n_groups] + struct ggml_tensor * v4_attn_o_b = nullptr; // [n_groups*o_lora_rank, n_embd] + + // mHC residual mapping (per-block, plus separate model-level + per-MTP). + struct ggml_tensor * v4_hc_attn_fn = nullptr; // F32 [(2+hc)*hc, hc*n_embd] + struct ggml_tensor * v4_hc_attn_base = nullptr; // F32 [(2+hc)*hc] + struct ggml_tensor * v4_hc_attn_scale = nullptr; // F32 [3] + struct ggml_tensor * v4_hc_ffn_fn = nullptr; + struct ggml_tensor * v4_hc_ffn_base = nullptr; + struct ggml_tensor * v4_hc_ffn_scale = nullptr; + + // Compressor (CSA + HCA layers). For HCA ratio==128 coff=1; for CSA ratio==4 coff=2. + struct ggml_tensor * v4_compressor_wkv = nullptr; // [coff*head_dim, n_embd] + struct ggml_tensor * v4_compressor_wgate = nullptr; + struct ggml_tensor * v4_compressor_ape = nullptr; // F32 [coff*head_dim, ratio] + struct ggml_tensor * v4_compressor_norm = nullptr; // [head_dim] + + // Indexer (CSA layers only). Loaded but unused by MVP graph (dense fallback). + struct ggml_tensor * v4_idx_compressor_wkv = nullptr; + struct ggml_tensor * v4_idx_compressor_wgate = nullptr; + struct ggml_tensor * v4_idx_compressor_ape = nullptr; + struct ggml_tensor * v4_idx_compressor_norm = nullptr; + + // Hash-routing LUT (first n_hash_layers only). I32 [vocab_size, n_expert_used]. + struct ggml_tensor * v4_ffn_gate_tid2eid = nullptr; + + // MTP block (last layer only). + struct ggml_tensor * v4_mtp_e_proj = nullptr; + struct ggml_tensor * v4_mtp_h_proj = nullptr; + struct ggml_tensor * v4_mtp_enorm = nullptr; + struct ggml_tensor * v4_mtp_hnorm = nullptr; + struct ggml_tensor * v4_mtp_norm = nullptr; + struct ggml_tensor * v4_mtp_hc_head_fn = nullptr; + struct ggml_tensor * v4_mtp_hc_head_base = nullptr; + struct ggml_tensor * v4_mtp_hc_head_scale = nullptr; + // gemma4 layer output scale struct ggml_tensor * out_scale = nullptr; @@ -550,6 +589,11 @@ struct llama_model { struct ggml_tensor * per_layer_model_proj = nullptr; struct ggml_tensor * per_layer_proj_norm = nullptr; + // DeepSeek-V4-Flash model-level mHC head (collapses hc_mult streams to 1 before lm_head). + struct ggml_tensor * v4_hc_head_fn = nullptr; // F32 [hc_mult, hc_mult * n_embd] + struct ggml_tensor * v4_hc_head_base = nullptr; // F32 [hc_mult] + struct ggml_tensor * v4_hc_head_scale = nullptr; // F32 [1] + std::vector layers; //Dense linear projections for SentenceTransformers models like embeddinggemma diff --git a/src/models/deepseek-v4.cpp b/src/models/deepseek-v4.cpp new file mode 100644 index 000000000000..3300b314b564 --- /dev/null +++ b/src/models/deepseek-v4.cpp @@ -0,0 +1,30 @@ +#include "models.h" + +// DeepSeek-V4-Flash forward graph. +// +// PR1: STUB GRAPH. The full mHC + Compressor + dense-Indexer + grouped-output +// attention + sqrt-softplus / hash-routed MoE forward will be implemented in a +// follow-up commit. This stub only embeds + RMSNorms + projects to logits so +// the model loads end-to-end and llama-server boots; output is junk. +// +// Reference: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/inference/model.py +llm_build_deepseek_v4::llm_build_deepseek_v4(const llama_model & model, const llm_graph_params & params) : + llm_graph_context(params) { + + ggml_tensor * cur; + ggml_tensor * inpL; + + // input embedding + inpL = build_inp_embd(model.tok_embd); + + // final RMSNorm (model.py:787) + cur = build_norm(inpL, model.output_norm, /*mb=*/nullptr, LLM_NORM_RMS, -1); + cb(cur, "result_norm", -1); + + // lm_head (model.py:788, 715) + cur = build_lora_mm(model.output, cur); + cb(cur, "result_output", -1); + res->t_logits = cur; + + ggml_build_forward_expand(gf, cur); +} diff --git a/src/models/models.h b/src/models/models.h index 94991c55fe87..133f1eddfdf4 100644 --- a/src/models/models.h +++ b/src/models/models.h @@ -194,6 +194,10 @@ struct llm_build_deepseek : public llm_graph_context { llm_build_deepseek(const llama_model & model, const llm_graph_params & params); }; +struct llm_build_deepseek_v4 : public llm_graph_context { + llm_build_deepseek_v4(const llama_model & model, const llm_graph_params & params); +}; + struct llm_build_dots1 : public llm_graph_context { llm_build_dots1(const llama_model & model, const llm_graph_params & params); }; diff --git a/tests/test-chat-template.cpp b/tests/test-chat-template.cpp index bf45d737c832..c0345f2726f9 100644 --- a/tests/test-chat-template.cpp +++ b/tests/test-chat-template.cpp @@ -519,6 +519,16 @@ int main_automated_tests(void) { /* .bos_token= */ "", /* .eos_token= */ "<|end▁of▁sentence|>", }, + { + // DeepSeek-V4-Flash chat-mode template (encoding_dsv4.encode_messages, thinking_mode="chat") + // ref: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/encoding/encoding_dsv4.py + /* .name= */ "deepseek-ai/DeepSeek-V4-Flash (chat mode)", + /* .template_str= */ U8C("{{- '<|begin▁of▁sentence|>' -}}{%- for message in messages -%}{%- if message['role'] == 'system' -%}{{- message['content'] -}}{%- elif message['role'] == 'user' or message['role'] == 'developer' -%}{{- '<|User|>' + message['content'] -}}{%- if not loop.last and messages[loop.index]['role'] == 'assistant' -%}{{- '<|Assistant|>' -}}{%- endif -%}{%- elif message['role'] == 'assistant' -%}{{- message['content'] + '<|end▁of▁sentence|>' -}}{%- endif -%}{%- endfor -%}{%- if add_generation_prompt -%}{{- '<|Assistant|>' -}}{%- endif -%}"), + /* .expected_output= */ U8C("<|begin▁of▁sentence|>You are a helpful assistant<|User|>Hello<|Assistant|>Hi there<|end▁of▁sentence|><|User|>Who are you<|Assistant|> I am an assistant <|end▁of▁sentence|><|User|>Another question<|Assistant|>"), + /* .expected_output_jinja= */ "", + /* .bos_token= */ "", + /* .eos_token= */ U8C("<|end▁of▁sentence|>"), + }, { /* .name= */ "ibm-granite/granite-3.0-8b-instruct", /* .template_str= */ "{%- if tools %}\n {{- '<|start_of_role|>available_tools<|end_of_role|>\n' }}\n {%- for tool in tools %}\n {{- tool | tojson(indent=4) }}\n {%- if not loop.last %}\n {{- '\n\n' }}\n {%- endif %}\n {%- endfor %}\n {{- '<|end_of_text|>\n' }}\n{%- endif %}\n{%- for message in messages %}\n {%- if message['role'] == 'system' %}\n {{- '<|start_of_role|>system<|end_of_role|>' + message['content'] + '<|end_of_text|>\n' }}\n {%- elif message['role'] == 'user' %}\n {{- '<|start_of_role|>user<|end_of_role|>' + message['content'] + '<|end_of_text|>\n' }}\n {%- elif message['role'] == 'assistant' %}\n {{- '<|start_of_role|>assistant<|end_of_role|>' + message['content'] + '<|end_of_text|>\n' }}\n {%- elif message['role'] == 'assistant_tool_call' %}\n {{- '<|start_of_role|>assistant<|end_of_role|><|tool_call|>' + message['content'] + '<|end_of_text|>\n' }}\n {%- elif message['role'] == 'tool_response' %}\n {{- '<|start_of_role|>tool_response<|end_of_role|>' + message['content'] + '<|end_of_text|>\n' }}\n {%- endif %}\n {%- if loop.last and add_generation_prompt %}\n {{- '<|start_of_role|>assistant<|end_of_role|>' }}\n {%- endif %}\n{%- endfor %}",