Skip to content

Commit 5969085

Browse files
committed
model: add Qwen3-Omni Thinker support (qwen3omnimoe)
Add support for Qwen3-Omni Thinker, a 48-layer MoE model with 128 experts (8 active per token) and optional shared expert. This enables text-only inference as the foundation for full multimodal support. Key changes: - New architecture: LLM_ARCH_QWEN3OMNIMOE - GGUF conversion with nested thinker_config handling - IMRoPE (Interleaved M-RoPE) with sections [24, 20, 20, 0] - Shared expert support in qwen3vl-moe graph builder - Reuses llm_build_qwen3vlmoe for graph construction
1 parent a4bf358 commit 5969085

6 files changed

Lines changed: 165 additions & 1 deletion

File tree

convert_hf_to_gguf.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4239,6 +4239,44 @@ def set_vocab(self):
42394239
super().set_vocab()
42404240

42414241

4242+
@ModelBase.register("Qwen3OmniMoeForConditionalGeneration")
4243+
class Qwen3OmniThinkerModel(Qwen3MoeModel):
4244+
model_arch = gguf.MODEL_ARCH.QWEN3OMNIMOE
4245+
4246+
def __init__(self, *args, **kwargs):
4247+
super().__init__(*args, **kwargs)
4248+
# Qwen3-Omni has nested thinker_config - merge into hparams
4249+
import copy
4250+
self.global_config = copy.deepcopy(self.hparams)
4251+
if "thinker_config" in self.global_config:
4252+
thinker_config = self.global_config["thinker_config"]
4253+
for key, value in thinker_config.items():
4254+
if key not in ["audio_config", "vision_config"]:
4255+
self.hparams[key] = value
4256+
4257+
def set_gguf_parameters(self):
4258+
super().set_gguf_parameters()
4259+
# M-RoPE dimension sections [temporal, spatial_y, spatial_x, padding]
4260+
self.gguf_writer.add_rope_dimension_sections([24, 20, 20, 0])
4261+
4262+
def set_vocab(self):
4263+
super().set_vocab()
4264+
# Fix EOS/BOS/PAD - Qwen3-Omni tokenizer_config.json has null for bos
4265+
self.gguf_writer.add_eos_token_id(151645) # <|im_end|>
4266+
self.gguf_writer.add_bos_token_id(151643) # <|endoftext|>
4267+
self.gguf_writer.add_pad_token_id(151643)
4268+
self.gguf_writer.add_add_bos_token(False)
4269+
4270+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
4271+
# Strip thinker prefix
4272+
if name.startswith("thinker."):
4273+
name = name.replace("thinker.", "", 1)
4274+
# Filter non-Thinker components
4275+
if name.startswith(("audio_tower.", "visual.", "talker.", "code2wav.")):
4276+
return []
4277+
return super().modify_tensors(data_torch, name, bid)
4278+
4279+
42424280
@ModelBase.register("Qwen3NextForCausalLM")
42434281
class Qwen3NextModel(Qwen2MoeModel):
42444282
model_arch = gguf.MODEL_ARCH.QWEN3NEXT

gguf-py/gguf/constants.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ class MODEL_ARCH(IntEnum):
372372
QWEN3NEXT = auto()
373373
QWEN3VL = auto()
374374
QWEN3VLMOE = auto()
375+
QWEN3OMNIMOE = auto()
375376
PHI2 = auto()
376377
PHI3 = auto()
377378
PHIMOE = auto()
@@ -768,6 +769,7 @@ class MODEL_TENSOR(IntEnum):
768769
MODEL_ARCH.QWEN3NEXT: "qwen3next",
769770
MODEL_ARCH.QWEN3VL: "qwen3vl",
770771
MODEL_ARCH.QWEN3VLMOE: "qwen3vlmoe",
772+
MODEL_ARCH.QWEN3OMNIMOE: "qwen3omnimoe",
771773
MODEL_ARCH.PHI2: "phi2",
772774
MODEL_ARCH.PHI3: "phi3",
773775
MODEL_ARCH.PHIMOE: "phimoe",
@@ -1718,6 +1720,26 @@ class MODEL_TENSOR(IntEnum):
17181720
MODEL_TENSOR.FFN_DOWN_EXP,
17191721
MODEL_TENSOR.FFN_UP_EXP,
17201722
],
1723+
MODEL_ARCH.QWEN3OMNIMOE: [
1724+
MODEL_TENSOR.TOKEN_EMBD,
1725+
MODEL_TENSOR.OUTPUT_NORM,
1726+
MODEL_TENSOR.OUTPUT,
1727+
MODEL_TENSOR.ATTN_NORM,
1728+
MODEL_TENSOR.ATTN_Q,
1729+
MODEL_TENSOR.ATTN_Q_NORM,
1730+
MODEL_TENSOR.ATTN_K,
1731+
MODEL_TENSOR.ATTN_K_NORM,
1732+
MODEL_TENSOR.ATTN_V,
1733+
MODEL_TENSOR.ATTN_OUT,
1734+
MODEL_TENSOR.FFN_NORM,
1735+
MODEL_TENSOR.FFN_GATE_INP,
1736+
MODEL_TENSOR.FFN_GATE_EXP,
1737+
MODEL_TENSOR.FFN_DOWN_EXP,
1738+
MODEL_TENSOR.FFN_UP_EXP,
1739+
MODEL_TENSOR.FFN_GATE_SHEXP,
1740+
MODEL_TENSOR.FFN_DOWN_SHEXP,
1741+
MODEL_TENSOR.FFN_UP_SHEXP,
1742+
],
17211743
MODEL_ARCH.PLAMO: [
17221744
MODEL_TENSOR.TOKEN_EMBD,
17231745
MODEL_TENSOR.OUTPUT_NORM,

src/llama-arch.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
3737
{ LLM_ARCH_QWEN3NEXT, "qwen3next" },
3838
{ LLM_ARCH_QWEN3VL, "qwen3vl" },
3939
{ LLM_ARCH_QWEN3VLMOE, "qwen3vlmoe" },
40+
{ LLM_ARCH_QWEN3OMNIMOE, "qwen3omnimoe" },
4041
{ LLM_ARCH_PHI2, "phi2" },
4142
{ LLM_ARCH_PHI3, "phi3" },
4243
{ LLM_ARCH_PHIMOE, "phimoe" },
@@ -934,6 +935,27 @@ static std::set<llm_tensor> llm_get_tensor_names(llm_arch arch) {
934935
LLM_TENSOR_FFN_DOWN_EXPS,
935936
LLM_TENSOR_FFN_UP_EXPS,
936937
};
938+
case LLM_ARCH_QWEN3OMNIMOE:
939+
return {
940+
LLM_TENSOR_TOKEN_EMBD,
941+
LLM_TENSOR_OUTPUT_NORM,
942+
LLM_TENSOR_OUTPUT,
943+
LLM_TENSOR_ATTN_NORM,
944+
LLM_TENSOR_ATTN_Q,
945+
LLM_TENSOR_ATTN_Q_NORM,
946+
LLM_TENSOR_ATTN_K,
947+
LLM_TENSOR_ATTN_K_NORM,
948+
LLM_TENSOR_ATTN_V,
949+
LLM_TENSOR_ATTN_OUT,
950+
LLM_TENSOR_FFN_NORM,
951+
LLM_TENSOR_FFN_GATE_INP,
952+
LLM_TENSOR_FFN_GATE_EXPS,
953+
LLM_TENSOR_FFN_DOWN_EXPS,
954+
LLM_TENSOR_FFN_UP_EXPS,
955+
LLM_TENSOR_FFN_GATE_SHEXP,
956+
LLM_TENSOR_FFN_DOWN_SHEXP,
957+
LLM_TENSOR_FFN_UP_SHEXP,
958+
};
937959
case LLM_ARCH_QWEN3NEXT:
938960
return {
939961
LLM_TENSOR_TOKEN_EMBD,

src/llama-arch.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ enum llm_arch {
4141
LLM_ARCH_QWEN3NEXT,
4242
LLM_ARCH_QWEN3VL,
4343
LLM_ARCH_QWEN3VLMOE,
44+
LLM_ARCH_QWEN3OMNIMOE,
4445
LLM_ARCH_PHI2,
4546
LLM_ARCH_PHI3,
4647
LLM_ARCH_PHIMOE,

src/llama-model.cpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,18 @@ void llama_model::load_hparams(llama_model_loader & ml) {
11441144
default: type = LLM_TYPE_UNKNOWN;
11451145
}
11461146
} break;
1147+
case LLM_ARCH_QWEN3OMNIMOE:
1148+
{
1149+
// Qwen3-Omni Thinker: Qwen3MOE with M-RoPE and shared experts
1150+
ml.get_key_or_arr(LLM_KV_ROPE_DIMENSION_SECTIONS, hparams.rope_sections, 4, true);
1151+
ml.get_key(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp, false);
1152+
ml.get_key(LLM_KV_EXPERT_SHARED_FEED_FORWARD_LENGTH, hparams.n_ff_shexp, false);
1153+
ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps);
1154+
switch (hparams.n_layer) {
1155+
case 48: type = LLM_TYPE_30B; break;
1156+
default: type = LLM_TYPE_UNKNOWN;
1157+
}
1158+
} break;
11471159
case LLM_ARCH_PHI2:
11481160
{
11491161
ml.get_key(LLM_KV_ATTENTION_LAYERNORM_EPS, hparams.f_norm_eps);
@@ -3622,6 +3634,55 @@ bool llama_model::load_tensors(llama_model_loader & ml) {
36223634
layer.ffn_up_exps = create_tensor(tn(LLM_TENSOR_FFN_UP_EXPS, "weight", i), { n_embd, n_ff_exp, n_expert}, 0);
36233635
}
36243636
} break;
3637+
case LLM_ARCH_QWEN3OMNIMOE:
3638+
{
3639+
// Qwen3-Omni Thinker: Qwen3MOE with M-RoPE and shared experts
3640+
tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
3641+
3642+
output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0);
3643+
output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, TENSOR_NOT_REQUIRED);
3644+
if (output == NULL) {
3645+
output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
3646+
}
3647+
3648+
for (int i = 0; i < n_layer; ++i) {
3649+
auto & layer = layers[i];
3650+
3651+
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
3652+
3653+
layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", i), {n_embd, n_embd_head_k * n_head}, 0);
3654+
layer.wk = create_tensor(tn(LLM_TENSOR_ATTN_K, "weight", i), {n_embd, n_embd_gqa}, 0);
3655+
layer.wv = create_tensor(tn(LLM_TENSOR_ATTN_V, "weight", i), {n_embd, n_embd_gqa}, 0);
3656+
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_head_k * n_head, n_embd}, 0);
3657+
3658+
layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", i), {n_embd_head_k}, 0);
3659+
layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", i), {n_embd_head_k}, 0);
3660+
3661+
layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
3662+
3663+
layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", i), {n_embd, n_expert}, 0);
3664+
3665+
if (n_expert == 0) {
3666+
throw std::runtime_error("n_expert must be > 0 for QWEN3OMNIMOE");
3667+
}
3668+
if (n_expert_used == 0) {
3669+
throw std::runtime_error("n_expert_used must be > 0 for QWEN3OMNIMOE");
3670+
}
3671+
3672+
// MoE experts
3673+
const int64_t n_ff_exp = hparams.n_ff_exp ? hparams.n_ff_exp : n_ff / n_expert_used;
3674+
layer.ffn_gate_exps = create_tensor(tn(LLM_TENSOR_FFN_GATE_EXPS, "weight", i), { n_embd, n_ff_exp, n_expert}, 0);
3675+
layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", i), {n_ff_exp, n_embd, n_expert}, 0);
3676+
layer.ffn_up_exps = create_tensor(tn(LLM_TENSOR_FFN_UP_EXPS, "weight", i), { n_embd, n_ff_exp, n_expert}, 0);
3677+
3678+
// Shared expert (optional)
3679+
if (hparams.n_ff_shexp > 0) {
3680+
layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", i), {n_embd, hparams.n_ff_shexp}, 0);
3681+
layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", i), {hparams.n_ff_shexp, n_embd}, 0);
3682+
layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", i), {n_embd, hparams.n_ff_shexp}, 0);
3683+
}
3684+
}
3685+
} break;
36253686
case LLM_ARCH_PHI2:
36263687
{
36273688
tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
@@ -7452,6 +7513,11 @@ ggml_cgraph * llama_model::build_graph(const llm_graph_params & params) const {
74527513
{
74537514
llm = std::make_unique<llm_build_qwen3vlmoe>(*this, params);
74547515
} break;
7516+
case LLM_ARCH_QWEN3OMNIMOE:
7517+
{
7518+
// Reuse Qwen3VLMOE builder - supports M-RoPE and shared experts
7519+
llm = std::make_unique<llm_build_qwen3vlmoe>(*this, params);
7520+
} break;
74557521
case LLM_ARCH_PHI2:
74567522
{
74577523
llm = std::make_unique<llm_build_phi2>(*this, params);
@@ -8014,6 +8080,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
80148080
return LLAMA_ROPE_TYPE_MROPE;
80158081
case LLM_ARCH_QWEN3VL:
80168082
case LLM_ARCH_QWEN3VLMOE:
8083+
case LLM_ARCH_QWEN3OMNIMOE:
80178084
return LLAMA_ROPE_TYPE_IMROPE;
80188085

80198086
case LLM_ARCH_GLM4:

src/models/qwen3vl-moe.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,21 @@ llm_build_qwen3vlmoe::llm_build_qwen3vlmoe(const llama_model & model, const llm_
113113
LLAMA_EXPERT_GATING_FUNC_TYPE_SOFTMAX,
114114
il);
115115
cb(moe_out, "ffn_moe_out", il);
116-
cur = moe_out;
116+
117+
// Shared expert (optional, for QWEN3OMNIMOE)
118+
if (model.layers[il].ffn_up_shexp != nullptr) {
119+
ggml_tensor * ffn_shexp =
120+
build_ffn(cur,
121+
model.layers[il].ffn_up_shexp, nullptr, nullptr,
122+
model.layers[il].ffn_gate_shexp, nullptr, nullptr,
123+
model.layers[il].ffn_down_shexp, nullptr, nullptr,
124+
nullptr, LLM_FFN_SILU, LLM_FFN_PAR, il);
125+
cb(ffn_shexp, "ffn_shexp", il);
126+
cur = ggml_add(ctx0, moe_out, ffn_shexp);
127+
cb(cur, "ffn_out", il);
128+
} else {
129+
cur = moe_out;
130+
}
117131

118132
cur = ggml_add(ctx0, cur, ffn_inp);
119133

0 commit comments

Comments
 (0)