Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
6e5b8b7
gguf: add qwen4exp (Qwen3.8-Flash-Next) arch and converter
danielhanchen Aug 25, 2026
331fa7e
llama: load qwen4exp (Qwen3.8-Flash-Next) hparams and tensors
danielhanchen Aug 25, 2026
058b7a1
qwen4exp: shorten comments
danielhanchen Aug 26, 2026
c9ad86f
llama: qwen4exp text graph with hyper-connections, GDN and MoE
danielhanchen Aug 25, 2026
ad4fa3f
llama: qwen4exp PLE n-gram hash embedding
danielhanchen Aug 25, 2026
c89e67b
llama: carry the qwen4exp PLE conv state across ubatches
danielhanchen Aug 25, 2026
ddf0980
llama: fix the qwen4exp PLE conv state and unblock test-llama-archs
danielhanchen Aug 25, 2026
ff24f38
llama: optional indexer key cache in llama_memory_hybrid
danielhanchen Aug 25, 2026
c88c916
llama: QSA sparse attention for qwen4exp
danielhanchen Aug 25, 2026
1b59910
llama: give the qwen4exp indexer cache the attention cache's slots
danielhanchen Aug 25, 2026
dd89627
tests: record what the qwen4exp arch-test skip actually observes
danielhanchen Aug 25, 2026
40e9041
tests: cover qwen4exp in test-llama-archs
danielhanchen Aug 25, 2026
53c0f62
convert: stream the qwen4exp PLE table instead of concatenating it
danielhanchen Aug 25, 2026
570c26d
quantize: fall back to F16 for 32-block types with an odd ncols
danielhanchen Aug 25, 2026
4597a86
quantize: let --tensor-type name per_layer_token_embd
danielhanchen Aug 25, 2026
25aa77a
quantize: size the output buffer exactly instead of nelements * 4
danielhanchen Aug 26, 2026
eb95d12
qwen4exp: hash the image placeholder for multimodal batches
danielhanchen Aug 26, 2026
bea3b12
qwen4exp: support a non-unified KV cache in QSA
Aug 26, 2026
7fee670
llama: keep the qwen4exp top-k attention mask arch-local
danielhanchen Aug 26, 2026
035e227
llama: hold the qwen4exp indexer cache in a new llama_memory_hybrid_idx
danielhanchen Aug 26, 2026
cfbdc0a
llama: save and restore the qwen4exp indexer KV cache
danielhanchen Aug 26, 2026
d22d2be
llama: make the qwen4exp PLE n-gram history per context and serialise it
danielhanchen Aug 26, 2026
d4a943f
qwen4exp: tidy comments and simplify image token read
Aug 26, 2026
0ac4b18
qwen4exp: support a quantized KV cache in the QSA attention path
Aug 26, 2026
c52ed2a
llama: give qwen4exp a large-graph node budget
Aug 26, 2026
6a69a0c
qwen4exp: drop an unused variable that breaks -Werror builds
Aug 27, 2026
ef9fa1b
quantize: dequantize and quantize large tensors in row bands
Aug 26, 2026
5674c73
llama: segment the qwen4exp fused QKV for tensor split
danielhanchen Aug 27, 2026
24ea62d
llama: fix the qwen4exp PLE history seq_rm(-1) iterator invalidation …
danielhanchen Aug 27, 2026
0b19188
qwen4exp: include llama-impl.h explicitly for llama_mul_mat_hadamard
Aug 27, 2026
213df58
convert: fix the qwen4exp lint and type-check failures
danielhanchen Aug 27, 2026
fbe1773
llama: give the qwen4exp indexer cache its own tensor names
Aug 27, 2026
6c5afc8
qwen4exp: double the Q split granularity for tensor parallelism
Aug 27, 2026
8b3ed0a
qwen4exp: keep the indexer cache in step across server slots
danielhanchen Aug 27, 2026
f43070a
kv-cache: clear the cache once when restoring a whole context
danielhanchen Aug 27, 2026
d1fe507
kv-cache: check the mirrored slot layout on a whole-context restore too
danielhanchen Aug 27, 2026
bf53960
qwen4exp: give the PLE conv history its own mirrored recurrent row
danielhanchen Aug 27, 2026
b5b5fc3
Merge remote-tracking branch 'origin/master' into qwen4exp/qwen3.8-fl…
ngxson Aug 27, 2026
5eeb97d
no more ple_hist (use master version)
ngxson Aug 27, 2026
1caf6c5
llama: give the qwen4exp full memory context its indexer cache
danielhanchen Aug 27, 2026
c325977
qwen4exp: shrink the PLE hparams storage
danielhanchen Aug 27, 2026
95da4ba
llama: opt-in random-access mmap advice for host-resident gather tables
Aug 27, 2026
5832557
llama: narrow the random-access mmap advice to the gather table
Aug 27, 2026
b8bdf73
llama: fold the random-access prefetch into its own feature flag
danielhanchen Aug 27, 2026
69a1269
Merge commit 'refs/pull/27742/head' of github.com:ggerganov/llama.cpp…
ngxson Aug 27, 2026
e5227ab
FACP (Fewer Acronym Classes Please)
CISC Aug 27, 2026
ef68766
qwen4exp: bias the QSA selection per block, not per cell
danielhanchen Aug 27, 2026
5b34ce2
clean up code comments
ngxson Aug 27, 2026
56672bf
Merge commit 'refs/pull/27742/head' of github.com:ggerganov/llama.cpp…
ngxson Aug 27, 2026
2439147
clean up new comments
ngxson Aug 27, 2026
41b4b70
revert LLAMA_MMAP_RANDOM
ngxson Aug 27, 2026
539c35e
Merge remote-tracking branch 'origin/master' into qwen4exp/qwen3.8-fl…
ngxson Aug 27, 2026
af1ffaf
nits
ngxson Aug 27, 2026
250b614
replace some changes with #27795
ngxson Aug 27, 2026
fbc5135
improve the m-rope image for get_prev_tokens
ngxson Aug 27, 2026
a510c82
LazyChunkedTensor
ngxson Aug 27, 2026
3d7baae
Merge remote-tracking branch 'origin/master' into qwen4exp/qwen3.8-fl…
ngxson Aug 27, 2026
072f519
fix lint
ngxson Aug 27, 2026
d7ea031
add some validations
ngxson Aug 27, 2026
e361303
reduce input nodes
ngxson Aug 27, 2026
dfb5c13
trim output tokens
ngxson Aug 27, 2026
2014107
nits
ngxson Aug 27, 2026
61b62d8
some more sanity checks
ngxson Aug 27, 2026
d807f04
fix llm_graph_input_ple reuse
ngxson Aug 27, 2026
eaf9376
exclude from webgpu test
ngxson Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions conversion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@
"Qwen3_5ForConditionalGeneration": "qwen",
"Qwen3_5MoeForCausalLM": "qwen",
"Qwen3_5MoeForConditionalGeneration": "qwen",
"Qwen4ExpForCausalLM": "qwen4exp",
"Qwen4ExpForConditionalGeneration": "qwen4exp",
"RND1": "qwen",
"RWForCausalLM": "falcon",
"RWKV6Qwen2ForCausalLM": "rwkv",
Expand Down Expand Up @@ -332,6 +334,7 @@
"Qwen3VLMoeForConditionalGeneration": "qwen3vl",
"Qwen3_5ForConditionalGeneration": "qwen3vl",
"Qwen3_5MoeForConditionalGeneration": "qwen3vl",
"Qwen4ExpForConditionalGeneration": "qwen4exp",
"RADIOModel": "nemotron",
"Sarashina2VisionForCausalLM": "sarashina2",
"SmolVLMForConditionalGeneration": "smolvlm",
Expand Down
8 changes: 6 additions & 2 deletions conversion/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1006,12 +1006,16 @@
else:
raise ValueError(f"Unknown file type: {self.ftype.name}")

# a chunked tensor quantizes as one chunk at a time, while it is written
quantize = data.quantize if isinstance(data, gguf.LazyChunkedTensor) else (
lambda qtype, d=data: gguf.quants.quantize(d, qtype))

try:
data = gguf.quants.quantize(data, data_qtype)
data = quantize(data_qtype)
except gguf.QuantError as e:
logger.warning("%s, %s", e, "falling back to F16")
data_qtype = gguf.GGMLQuantizationType.F16
data = gguf.quants.quantize(data, data_qtype)
data = quantize(data_qtype)

shape = gguf.quant_shape_from_byte_shape(data.shape, data_qtype) if data.dtype == np.uint8 else data.shape

Expand Down Expand Up @@ -1418,15 +1422,15 @@

from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(self.dir_model)
vocab_size = self.hparams.get("vocab_size", len(tokenizer.vocab)) # ty: ignore[unresolved-attribute]

Check warning on line 1425 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1425:76: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
assert max(tokenizer.vocab.values()) < vocab_size # ty: ignore[unresolved-attribute]

Check warning on line 1426 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1426:60: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment

tokpre = self.get_vocab_base_pre(tokenizer)

reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()} # ty: ignore[unresolved-attribute]

Check warning on line 1430 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1430:93: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
added_vocab = tokenizer.get_added_vocab() # ty: ignore[unresolved-attribute]

Check warning on line 1431 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1431:52: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment

added_tokens_decoder = tokenizer.added_tokens_decoder # ty: ignore[unresolved-attribute]

Check warning on line 1433 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1433:64: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment

for i in range(vocab_size):
if i not in reverse_vocab:
Expand All @@ -1439,7 +1443,7 @@
# To avoid unexpected issues - we make sure to normalize non-normalized tokens
if not added_tokens_decoder[i].normalized:
previous_token = token
token = tokenizer.decode(tokenizer.encode(token, add_special_tokens=False)) # ty: ignore[unresolved-attribute, invalid-assignment]

Check warning on line 1446 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1446:102: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
if previous_token != token:
logger.info(f"{repr(previous_token)} is encoded and decoded back to {repr(token)} using AutoTokenizer")

Expand Down Expand Up @@ -1806,14 +1810,14 @@
def _set_vocab_hybriddna(self):
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(self.dir_model, trust_remote_code=True)
vocab_size = self.hparams.get("vocab_size", len(tokenizer.vocab)) # ty: ignore[unresolved-attribute]

Check warning on line 1813 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1813:76: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
assert max(tokenizer.vocab.values()) < vocab_size # ty: ignore[unresolved-attribute]

Check warning on line 1814 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1814:60: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment

reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()} # ty: ignore[unresolved-attribute]

Check warning on line 1816 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1816:93: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
# k-mers can share text with a base-vocab BPE token (e.g. CCCCCC) and get
# dropped by get_vocab(); a reserved marker suffix (U+E000) keeps each
# k-mer's own id (llama.cpp strips it on detokenization)
for kmer in tokenizer.kmers: # ty: ignore[unresolved-attribute]

Check warning on line 1820 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1820:39: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
reverse_vocab[tokenizer.dna_token_to_id[kmer]] = kmer + "\ue000" # ty: ignore[unresolved-attribute]
added_vocab = tokenizer.get_added_vocab() # ty: ignore[unresolved-attribute]
added_tokens_decoder = tokenizer.added_tokens_decoder # ty: ignore[unresolved-attribute]
Expand Down
195 changes: 195 additions & 0 deletions conversion/qwen4exp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
from __future__ import annotations

from typing import Iterable, cast

import torch
from torch import Tensor

import gguf
import numpy as np

from .base import ModelBase
from .qwen import _LinearAttentionVReorderBase, _Qwen35MRopeMixin
from .qwen3vl import Qwen3VLVisionModel


@ModelBase.register("Qwen4ExpForConditionalGeneration", "Qwen4ExpForCausalLM")
@ModelBase.example("Qwen/Qwen3.8-Flash-Next")
class Qwen4ExpTextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase):
"""Qwen3.8-Flash-Next.
Shares the Qwen3.5 gated delta net and interleaved mrope, and adds three things:
hyper-connections in place of every layer norm, QSA sparse attention on the full
attention layers, and PLE n-gram hash embeddings on a single layer.
"""

model_arch = gguf.MODEL_ARCH.QWEN4EXP

# the MTP block is a separate draft head; vLLM drops it too
supports_mtp_export = False
no_mtp = True

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# only the shard names, so the table itself is never held
self._ple_shards: dict[int, str] = {}
self._ple_row_dim: int | None = None

def _read_hash_constants(self, suffix: str) -> list[int]:
"""Read an int64 PLE constant straight from the checkpoint.
prepare_tensors() casts every non-float dtype to float32 before
modify_tensors() sees it (base.py), which would silently round these
45-bit multipliers. Reading the lazy tensor here bypasses that.
"""
for name, gen in self.model_tensors.items():
if name.endswith(suffix):
t = gen()
if t.dtype != torch.int64:
t = t.to(torch.int64)
return [int(x) for x in t.tolist()]
raise ValueError(f"PLE constant {suffix!r} missing from the checkpoint")

def set_gguf_parameters(self):
super().set_gguf_parameters()
hp = self.hparams

self.gguf_writer.add_hyper_connection_count(hp["hc_count"])
self.gguf_writer.add_hyper_connection_low_rank(hp["hc_lowrank"])

n_layer = hp["num_hidden_layers"]
self.gguf_writer.add_indexer_head_count(hp["indexer_n_heads"])
self.gguf_writer.add_indexer_key_length(hp["indexer_head_dim"])
self.gguf_writer.add_indexer_top_k(hp["indexer_budget"])
ratio = hp["indexer_compress_ratio"]
layer_types = hp["layer_types"]
self.gguf_writer.add_attention_compress_ratios(
[ratio if layer_types[i] == "full_attention" else 0 for i in range(n_layer)]
)

# ple_layer_ids is 1-based in the HF config; empty means no n-gram table,
# so emit no PLE keys rather than optional ones
ple_layers = [i - 1 for i in hp["ple_layer_ids"]]
if not ple_layers:
return
self.gguf_writer.add_ple_layers(ple_layers)
self.gguf_writer.add_ple_ngram_size(hp["ngram_size"])
self.gguf_writer.add_ple_heads_per_ngram(hp["heads_per_ngram"])
self.gguf_writer.add_ple_conv_kernel(hp["ple_conv_kernel_size"])
self.gguf_writer.add_ple_eos_token_id(self._eos_token_id())
# an image is decoded as an embeddings-only batch, so the graph has no placeholder
# ids to hash; carry the id and let it stand in for those positions
_img = self._image_token_id()
if _img is not None:
self.gguf_writer.add_ple_image_token_id(int(_img))
if self._ple_row_dim is not None:
self.gguf_writer.add_embedding_length_per_layer_input(self._ple_row_dim)

self.gguf_writer.add_ple_layer_multipliers(
self._read_hash_constants("ple_embedding.layer_multipliers"))
self.gguf_writer.add_ple_head_offsets(
self._read_hash_constants("ple_embedding.ngram_heads_offsets"))
self.gguf_writer.add_ple_head_vocab_sizes(
self._read_hash_constants("ple_embedding.ngram_heads_vocab_sizes"))

def _image_token_id(self) -> int | None:
img = self.hparams.get("image_token_id")
return None if img is None else int(img)
Comment on lines +95 to +97

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems to be redundant: text_config is merged with global config, not replace it completely:

llama.cpp/conversion/base.py

Lines 1186 to 1188 in bf94216

if "text_config" in self.hparams:
# move the text_config to the root level
self.hparams = {**self.hparams, **self.hparams["text_config"]}

so in theory you should be able to read image_token_id directly from self.hparams


def _eos_token_id(self) -> int:
eos = self.hparams.get("eos_token_id")
if isinstance(eos, list):
# the PLE hash resets n-grams on the primary EOS
return int(eos[-1])
if eos is None:
raise ValueError("eos_token_id is required: the PLE hash resets its n-grams on it")
return int(eos)

def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
# int64 hash constants must stay exact; 1-D tensors force F32, so use KV
if name.endswith("ple_embedding.layer_multipliers"):
self._ple_multipliers = [int(x) for x in data_torch.tolist()]
return []
if name.endswith("ple_embedding.ngram_heads_offsets"):
self._ple_head_offsets = [int(x) for x in data_torch.tolist()]
return []
if name.endswith("ple_embedding.ngram_heads_vocab_sizes"):
self._ple_head_vocab_sizes = [int(x) for x in data_torch.tolist()]
return []

if ".ngram_embedding.shard_" in name:
return self._place_ple_shard(data_torch, name)

# one projection feeds indexer q and k; split it, as minimax-m3 does
if ".indexer.index_qk_proj.weight" in name:
n_q = self.hparams["indexer_n_heads"] * self.hparams["indexer_head_dim"]
q = data_torch[:n_q]
k = data_torch[n_q:]
return [
(self.format_tensor_name(gguf.MODEL_TENSOR.INDEXER_Q_PROJ, bid, ".weight"), q),
(self.format_tensor_name(gguf.MODEL_TENSOR.INDEXER_K_PROJ, bid, ".weight"), k),
]

# Gemma zero-centred gammas the inherited norm.weight rule misses
if name.endswith((".ple.norm_key.weight", ".ple.norm_query.weight", ".ple.norm_conv.weight",
".indexer.q_layernorm.weight", ".indexer.k_layernorm.weight")):
return [(self.map_tensor_name(name), data_torch + 1)]

if name.endswith(".ple.conv1d.weight"):
return [(self.map_tensor_name(name), data_torch.squeeze())]

return super().modify_tensors(data_torch, name, bid)

# the shards concatenate into a tensor of well over 100 GB
# use LazyChunkedTensor here, a single shard resident at a time
def _place_ple_shard(self, data_torch: Tensor, name: str) -> Iterable[tuple[str, Tensor]]:

idx = int(name.rpartition(".shard_")[2].partition(".")[0])
n_parts = self.hparams["split_ngram_parts"]

self._ple_shards[idx] = name
self._ple_row_dim = int(data_torch.shape[-1])

if len(self._ple_shards) < n_parts:
return []

# the checkpoint may yield the shards in any order, the row order is by index
shards = [self._ple_shards[i] for i in sorted(self._ple_shards)]
rows = 0
for shard in shards:
shape = self.model_tensors[shard]().shape
if int(shape[-1]) != self._ple_row_dim:
raise ValueError(
f"PLE shard {shard} has row dim {int(shape[-1])}, expected {self._ple_row_dim}")
rows += int(shape[0])

table = gguf.LazyChunkedTensor(
[self._load_ple_shard(shard) for shard in shards],
shape=(rows, self._ple_row_dim),
dtype=np.float32,
)
gguf_name = gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.PER_LAYER_TOKEN_EMBD]
return [(gguf_name + ".weight", cast(Tensor, table))]

def _load_ple_shard(self, name: str):
def load() -> np.ndarray:
from .base import LazyTorchTensor

# a fresh lazy tensor every call, or to_eager() memoizes every shard
eager = LazyTorchTensor.to_eager(self.model_tensors[name]())
return eager.to(torch.float32).contiguous().numpy()
return load

def prepare_tensors(self):
super().prepare_tensors()
n_parts = self.hparams.get("split_ngram_parts", 0)
if self._ple_shards and len(self._ple_shards) != n_parts:
raise ValueError(
f"got {len(self._ple_shards)} PLE embedding shards, expected {n_parts}"
)


@ModelBase.register("Qwen4ExpForConditionalGeneration")
@ModelBase.example("Qwen/Qwen3.8-Flash-Next")
class Qwen4ExpVisionModel(Qwen3VLVisionModel):
"""The vision tower is an unmodified Qwen3-VL ViT."""
101 changes: 101 additions & 0 deletions gguf-py/gguf/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,19 @@ class HyperConnection:
COUNT = "{arch}.hyper_connection.count"
SINKHORN_ITERATIONS = "{arch}.hyper_connection.sinkhorn_iterations"
EPSILON = "{arch}.hyper_connection.epsilon"
# absent means the mix projection is full rank (DeepSeek-V4 behaviour)
LOW_RANK = "{arch}.hyper_connection.low_rank"

class PerLayerEmbedding:
LAYERS = "{arch}.ple.layers"
NGRAM_SIZE = "{arch}.ple.ngram_size"
HEADS_PER_NGRAM = "{arch}.ple.heads_per_ngram"
CONV_KERNEL = "{arch}.ple.conv_kernel"
LAYER_MULTIPLIERS = "{arch}.ple.layer_multipliers"
HEAD_OFFSETS = "{arch}.ple.head_offsets"
HEAD_VOCAB_SIZES = "{arch}.ple.head_vocab_sizes"
EOS_TOKEN_ID = "{arch}.ple.eos_token_id"
IMAGE_TOKEN_ID = "{arch}.ple.image_token_id"

class Rope:
DIMENSION_COUNT = "{arch}.rope.dimension_count"
Expand Down Expand Up @@ -494,6 +507,7 @@ class MODEL_ARCH(IntEnum):
QWEN3VLMOE = auto()
QWEN35 = auto()
QWEN35MOE = auto()
QWEN4EXP = auto()
PHI2 = auto()
PHI3 = auto()
PHIMOE = auto()
Expand Down Expand Up @@ -636,6 +650,9 @@ class MODEL_TENSOR(IntEnum):
HC_HEAD_FN = auto()
HC_HEAD_BASE = auto()
HC_HEAD_SCALE = auto()
HC_HEAD_NORM = auto() # qwen4exp
HC_HEAD_DOWN = auto() # qwen4exp
HC_HEAD_UP = auto() # qwen4exp
ROPE_FREQS = auto()
ROPE_FACTORS_LONG = auto()
ROPE_FACTORS_SHORT = auto()
Expand Down Expand Up @@ -780,6 +797,20 @@ class MODEL_TENSOR(IntEnum):
HC_FFN_FN = auto()
HC_FFN_BASE = auto()
HC_FFN_SCALE = auto()
HC_ATTN_NORM = auto() # qwen4exp
HC_ATTN_DOWN = auto() # qwen4exp
HC_ATTN_UP = auto() # qwen4exp
HC_ATTN_INJECT = auto() # qwen4exp
HC_FFN_NORM = auto() # qwen4exp
HC_FFN_DOWN = auto() # qwen4exp
HC_FFN_UP = auto() # qwen4exp
HC_FFN_INJECT = auto() # qwen4exp
PLE_KEY = auto() # qwen4exp
PLE_VALUE = auto() # qwen4exp
PLE_NORM_KEY = auto() # qwen4exp
PLE_NORM_QUERY = auto() # qwen4exp
PLE_NORM_CONV = auto() # qwen4exp
PLE_CONV1D = auto() # qwen4exp
ATTN_COMPRESSOR_WKV = auto()
ATTN_COMPRESSOR_WGATE = auto()
ATTN_COMPRESSOR_APE = auto()
Expand Down Expand Up @@ -1217,6 +1248,7 @@ class MODEL_TENSOR(IntEnum):
MODEL_ARCH.QWEN3VLMOE: "qwen3vlmoe",
MODEL_ARCH.QWEN35: "qwen35",
MODEL_ARCH.QWEN35MOE: "qwen35moe",
MODEL_ARCH.QWEN4EXP: "qwen4exp",
MODEL_ARCH.PHI2: "phi2",
MODEL_ARCH.PHI3: "phi3",
MODEL_ARCH.PHIMOE: "phimoe",
Expand Down Expand Up @@ -1358,6 +1390,9 @@ class MODEL_TENSOR(IntEnum):
MODEL_TENSOR.HC_HEAD_FN: "output_hc_fn",
MODEL_TENSOR.HC_HEAD_BASE: "output_hc_base",
MODEL_TENSOR.HC_HEAD_SCALE: "output_hc_scale",
MODEL_TENSOR.HC_HEAD_NORM: "output_hc_norm", # qwen4exp
MODEL_TENSOR.HC_HEAD_DOWN: "output_hc_down", # qwen4exp
MODEL_TENSOR.HC_HEAD_UP: "output_hc_up", # qwen4exp
MODEL_TENSOR.ROPE_FREQS: "rope_freqs",
MODEL_TENSOR.ROPE_FACTORS_LONG: "rope_factors_long",
MODEL_TENSOR.ROPE_FACTORS_SHORT: "rope_factors_short",
Expand Down Expand Up @@ -1502,6 +1537,20 @@ class MODEL_TENSOR(IntEnum):
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_ATTN_NORM: "blk.{bid}.hc_attn_norm", # qwen4exp
MODEL_TENSOR.HC_ATTN_DOWN: "blk.{bid}.hc_attn_down", # qwen4exp
MODEL_TENSOR.HC_ATTN_UP: "blk.{bid}.hc_attn_up", # qwen4exp
MODEL_TENSOR.HC_ATTN_INJECT: "blk.{bid}.hc_attn_inject", # qwen4exp
MODEL_TENSOR.HC_FFN_NORM: "blk.{bid}.hc_ffn_norm", # qwen4exp
MODEL_TENSOR.HC_FFN_DOWN: "blk.{bid}.hc_ffn_down", # qwen4exp
MODEL_TENSOR.HC_FFN_UP: "blk.{bid}.hc_ffn_up", # qwen4exp
MODEL_TENSOR.HC_FFN_INJECT: "blk.{bid}.hc_ffn_inject", # qwen4exp
MODEL_TENSOR.PLE_KEY: "blk.{bid}.ple_key", # qwen4exp
MODEL_TENSOR.PLE_VALUE: "blk.{bid}.ple_value", # qwen4exp
MODEL_TENSOR.PLE_NORM_KEY: "blk.{bid}.ple_norm_key", # qwen4exp
MODEL_TENSOR.PLE_NORM_QUERY: "blk.{bid}.ple_norm_query", # qwen4exp
MODEL_TENSOR.PLE_NORM_CONV: "blk.{bid}.ple_norm_conv", # qwen4exp
MODEL_TENSOR.PLE_CONV1D: "blk.{bid}.ple_conv1d", # qwen4exp
MODEL_TENSOR.ATTN_COMPRESSOR_WKV: "blk.{bid}.attn_compressor_kv",
MODEL_TENSOR.ATTN_COMPRESSOR_WGATE: "blk.{bid}.attn_compressor_gate",
MODEL_TENSOR.ATTN_COMPRESSOR_APE: "blk.{bid}.attn_compressor_ape",
Expand Down Expand Up @@ -2795,6 +2844,58 @@ class MODEL_TENSOR(IntEnum):
MODEL_TENSOR.NEXTN_SHARED_HEAD_HEAD,
MODEL_TENSOR.NEXTN_SHARED_HEAD_NORM,
],
MODEL_ARCH.QWEN4EXP: [
MODEL_TENSOR.TOKEN_EMBD,
MODEL_TENSOR.OUTPUT,
# no OUTPUT_NORM / ATTN_NORM / ATTN_POST_NORM: hyper-connections replace every layer norm
MODEL_TENSOR.HC_HEAD_NORM,
MODEL_TENSOR.HC_HEAD_DOWN,
MODEL_TENSOR.HC_HEAD_UP,
MODEL_TENSOR.HC_ATTN_NORM,
MODEL_TENSOR.HC_ATTN_DOWN,
MODEL_TENSOR.HC_ATTN_UP,
MODEL_TENSOR.HC_ATTN_INJECT,
MODEL_TENSOR.HC_FFN_NORM,
MODEL_TENSOR.HC_FFN_DOWN,
MODEL_TENSOR.HC_FFN_UP,
MODEL_TENSOR.HC_FFN_INJECT,
# full attention layers: ATTN_Q holds [q|gate] interleaved per head
MODEL_TENSOR.ATTN_Q,
MODEL_TENSOR.ATTN_Q_NORM,
MODEL_TENSOR.ATTN_K,
MODEL_TENSOR.ATTN_K_NORM,
MODEL_TENSOR.ATTN_V,
MODEL_TENSOR.ATTN_OUT,
MODEL_TENSOR.INDEXER_Q_PROJ,
MODEL_TENSOR.INDEXER_K_PROJ,
MODEL_TENSOR.INDEXER_Q_NORM,
MODEL_TENSOR.INDEXER_K_NORM,
MODEL_TENSOR.ATTN_QKV,
MODEL_TENSOR.ATTN_GATE,
MODEL_TENSOR.SSM_A,
MODEL_TENSOR.SSM_CONV1D,
MODEL_TENSOR.SSM_DT,
MODEL_TENSOR.SSM_NORM,
MODEL_TENSOR.SSM_BETA,
MODEL_TENSOR.SSM_ALPHA,
MODEL_TENSOR.SSM_OUT,
MODEL_TENSOR.FFN_GATE_INP,
MODEL_TENSOR.FFN_GATE_INP_SHEXP,
MODEL_TENSOR.FFN_UP_SHEXP,
MODEL_TENSOR.FFN_DOWN_SHEXP,
MODEL_TENSOR.FFN_GATE_SHEXP,
MODEL_TENSOR.FFN_DOWN_EXP,
MODEL_TENSOR.FFN_UP_EXP,
MODEL_TENSOR.FFN_GATE_EXP,
MODEL_TENSOR.FFN_GATE_UP_EXP,
MODEL_TENSOR.PER_LAYER_TOKEN_EMBD,
MODEL_TENSOR.PLE_KEY,
MODEL_TENSOR.PLE_VALUE,
MODEL_TENSOR.PLE_NORM_KEY,
MODEL_TENSOR.PLE_NORM_QUERY,
MODEL_TENSOR.PLE_NORM_CONV,
MODEL_TENSOR.PLE_CONV1D,
],
MODEL_ARCH.PLAMO: [
MODEL_TENSOR.TOKEN_EMBD,
MODEL_TENSOR.OUTPUT_NORM,
Expand Down
Loading
Loading