Compare commits

...
23 Commits
Author SHA1 Message Date
Pascal 55467b6b66 fix build 2026-07-31 10:08:09 +02:00
Pascal b29d0f3fae missing file 2026-07-31 09:03:47 +02:00
Pascal 707c79c72b tts: add Qwen3-TTS end to end example
tools/tts/tts-qwen3.cpp drives the whole pipeline: talker as a plain
llama model, each frame through mtmd_gen_audio, hidden state feedback
as the next input embedding, PCM out as wav.

Three things the model needs that tokens alone cannot express:
- the prompt is the sum of two aligned streams (projected text plus
  codec specials), so positions are assembled from embedding rows read
  straight out of the talker GGUF
- the talker rides the qwen3vl interleaved mrope, batch positions
  carry four sections laid out [section * n_tokens + i]
- the text keeps streaming during generation: the input after frame k
  adds trailing text row k on top of the codes embedding, then
  tts_eos, then tts_pad, which is what makes EOS land naturally

mtmd keeps the code2wav left-context ring internally (extended from
each frame's codes, cleared by mtmd_gen_audio_reset), so the decode
window gets real history with no API burden on the caller.
2026-07-31 09:02:07 +02:00
Pascal 6cfa726879 mtmd: qwen3-tts code2wav left-context window
The stateless per-frame decode zero-padded every causal conv and ran
the pre_transformer on a single position, so each hop came out as if
it opened the utterance, glitching at every frame boundary.

Feed the last C2W_CTX_FRAMES frames of codes as a graph input, decode
the whole window, emit only the newest hop. quant_decode and the
pre_transformer generalize over T; the conv modules already were.
out_codes / ctx_codes in clip_encode_params carry the history ring.

Cost: the window re-decodes W = 24 frames per emitted hop. Amortize by
emitting C hops per call (overhead (L + C) / C), or remove it entirely
by carrying conv and KV state across calls.
2026-07-30 22:23:10 +02:00
Xuan Son Nguyen 84559b46a0 wire up in/out 2026-07-30 19:40:21 +02:00
Xuan Son Nguyen ff6b289261 code2wav graph ok 2026-07-30 19:25:50 +02:00
Xuan Son Nguyen bf0e641fda code2wav convert to gguf 2026-07-30 18:28:00 +02:00
Xuan Son Nguyen e68884dcf0 Merge branch 'master' into xsn/qwen3-tts 2026-07-30 17:04:49 +02:00
Xuan Son NguyenandPascal d98264a564 code gen cgraph init version
Co-authored-by: Pascal <[email protected]>
2026-07-30 12:50:22 +02:00
Xuan Son Nguyen 2e9e8bbe34 wire up 2026-07-30 00:12:08 +02:00
Xuan Son Nguyen 4712dce74f add clip_encode 2026-07-29 23:26:33 +02:00
Xuan Son Nguyen bd38cb5cc2 load gen_code model ok 2026-07-29 21:38:24 +02:00
Xuan Son Nguyen ed0b0503d6 convert code_predictor to gguf 2026-07-29 21:10:44 +02:00
Xuan Son Nguyen eea7c8b3db poc new mtmd gen api 2026-07-29 21:08:30 +02:00
Xuan Son Nguyen 85af600c4f add suppress_tokens 2026-07-29 20:27:09 +02:00
Xuan Son Nguyen ebd17cc1cb Merge branch 'master' into xsn/qwen3-tts 2026-07-29 20:07:12 +02:00
Xuan Son Nguyen 894ac82b19 adapt vocab for backbone (with some tricks) 2026-07-29 14:24:55 +02:00
Xuan Son Nguyen b98aff2605 Merge branch 'master' into xsn/qwen3-tts 2026-07-29 01:53:15 +02:00
Xuan Son Nguyen 4da8d180c3 speaker enc graph 2026-07-29 01:49:19 +02:00
Xuan Son Nguyen 39b9c2d5a0 speaker encoder loading ok 2026-07-29 01:32:01 +02:00
Xuan Son Nguyen de0ac58c1f convert encoder ok 2026-07-29 01:02:45 +02:00
Xuan Son Nguyen 8892b6c60b main model load ok 2026-07-29 00:36:36 +02:00
Xuan Son Nguyen ae7663e829 convert text model 2026-07-28 23:29:44 +02:00
24 changed files with 2766 additions and 18 deletions
+2
View File
@@ -209,6 +209,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
"Qwen3MoeForCausalLM": "qwen",
"Qwen3NextForCausalLM": "qwen",
"Qwen3OmniMoeForConditionalGeneration": "qwen3vl",
"Qwen3TTSForConditionalGeneration": "qwen3tts",
"Qwen3VLForConditionalGeneration": "qwen3vl",
"Qwen3VLMoeForConditionalGeneration": "qwen3vl",
"Qwen3_5ForCausalLM": "qwen",
@@ -303,6 +304,7 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
"Qwen2_5_VLForConditionalGeneration": "qwenvl",
"Qwen3ASRForConditionalGeneration": "qwen3vl",
"Qwen3OmniMoeForConditionalGeneration": "qwen3vl",
"Qwen3TTSForConditionalGeneration": "qwen3tts",
"Qwen3VLForConditionalGeneration": "qwen3vl",
"Qwen3VLMoeForConditionalGeneration": "qwen3vl",
"Qwen3_5ForConditionalGeneration": "qwen3vl",
+479
View File
@@ -0,0 +1,479 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Callable, Iterable, TYPE_CHECKING
import torch
import torch.nn.functional as F
if TYPE_CHECKING:
from torch import Tensor
from .base import ModelBase, MmprojModel, TextModel, gguf, logger
# Tricks being used to support this model via existing llama.cpp code paths:
# - Text projection MLP is folded into the embedding table
# - codec_embedding is concat to the text embedding table, vocab is extended
# example: codec_bos_id(2149) --> "<|codec_bos|>"
# codec_eos_token_id(2150) --> "<|codec_eos_token|>"
# codec_language_id.chinese(2055) --> "<|codec_language_chinese|>"
# other rows --> "<|codec_0|>", "<|codec_1|>", ..., "<|codec_1023|>"
# - output tensor codec_head is smaller than vocab, so logits will be padded at inference time
# - suppress_tokens is used to limit the backbone to only sample either semantic or EOS (stop) token
# torch activation functions used by Qwen3TTSTalkerResizeMLP (config's hidden_act)
_ACT2FN = {
"silu": F.silu,
"gelu": F.gelu,
"relu": F.relu,
}
# TODO: figure out the correct template
DEFAULT_TEMPLATE = """{% for m in messages %}{{m['content']}}{% endfor %}"""
@ModelBase.register("Qwen3TTSForConditionalGeneration")
class Qwen3TTSTalkerModel(TextModel):
model_arch = gguf.MODEL_ARCH.QWEN3TTS
_TEXT_PROJ_KEYS = (
"model.text_embedding.weight",
"text_projection.linear_fc1.weight",
"text_projection.linear_fc1.bias",
"text_projection.linear_fc2.weight",
"text_projection.linear_fc2.bias",
)
_text_proj_buffer: dict[str, Tensor]
_folded_text_embed: Tensor | None
_codec_embed: Tensor | None
def __init__(self, dir_model: Path, *args, **kwargs):
hparams = kwargs.pop("hparams", None)
if hparams is None:
hparams = ModelBase.load_hparams(dir_model, is_mistral_format=False)
raw_talker_config = dict(hparams["talker_config"])
self._talker_config = raw_talker_config
self.n_codec_vocab = raw_talker_config["vocab_size"]
talker_config = dict(raw_talker_config)
talker_config["vocab_size"] = talker_config["text_vocab_size"]
hparams["text_config"] = talker_config
super().__init__(dir_model, *args, hparams=hparams, **kwargs)
self._text_proj_buffer = {}
self._folded_text_embed = None
self._codec_embed = None
def _codec_token_names(self) -> list[str]:
# start every row with a generic name, then override the ones with a
# known meaning (bos/eos/language/etc, derived from the *_id fields
# of talker_config) with a more descriptive one
names = [f"<|codec_{i}|>" for i in range(self.n_codec_vocab)]
for key, val in self._talker_config.items():
if not key.endswith("_id"):
continue
prefix = key[:-len("_id")]
if isinstance(val, int):
names[val] = f"<|{prefix}|>"
elif isinstance(val, dict):
for subkey, subval in val.items():
names[subval] = f"<|{prefix}_{subkey}|>"
return names
def set_vocab(self):
codec_tokens = self._codec_token_names()
codec_toktypes = [gguf.TokenType.CONTROL] * len(codec_tokens)
try:
tokens, scores, toktypes = self._create_vocab_sentencepiece()
self.gguf_writer.add_tokenizer_model("llama")
self.gguf_writer.add_tokenizer_pre("default")
tokens += [t.encode("utf-8") for t in codec_tokens]
scores += [0.0] * len(codec_tokens)
toktypes += codec_toktypes
self.gguf_writer.add_token_list(tokens)
self.gguf_writer.add_token_scores(scores)
self.gguf_writer.add_token_types(toktypes)
special_vocab = gguf.SpecialVocab(self.dir_model, n_vocab=len(tokens))
special_vocab.add_to_gguf(self.gguf_writer)
return
except FileNotFoundError:
pass
tokens, toktypes, tokpre = self.get_vocab_base()
tokens += codec_tokens
toktypes += codec_toktypes
self.gguf_writer.add_tokenizer_model("gpt2")
self.gguf_writer.add_tokenizer_pre(tokpre)
self.gguf_writer.add_token_list(tokens)
self.gguf_writer.add_token_types(toktypes)
special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
special_vocab.add_to_gguf(self.gguf_writer)
def set_gguf_parameters(self):
super().set_gguf_parameters()
self.gguf_writer.add_chat_template(DEFAULT_TEMPLATE)
# note: final vocab layout is [text_vocab | codec_vocab], with text_vocab is actually padded with -inf in cgraph
# for codec_vocab, only first 2048 rows can be sampled for semantic code
# plus codec_eos_token_id that used for signaling end of generation
# ref: https://github.com/QwenLM/Qwen3-TTS/blob/022e286b98fbec7e1e916cb940cdf532cd9f488e/qwen_tts/core/models/modeling_qwen3_tts.py#L2059-L2063
vocab_size = self.hparams["vocab_size"] + self.n_codec_vocab
codec_eos_token_id = self.hparams["vocab_size"] + self._talker_config["codec_eos_token_id"]
self.gguf_writer.add_suppress_tokens([
i for i in range(vocab_size - 1024, vocab_size)
if i != codec_eos_token_id
])
self.gguf_writer.add_eos_token_id(codec_eos_token_id)
self.gguf_writer.add_add_eos_token(False)
@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
name, gen = item
if not name.startswith("talker.") or name.startswith("talker.code_predictor."):
return None
name = name[len("talker."):]
return super().filter_tensors((name, gen))
def _maybe_emit_token_embd(self) -> Iterable[tuple[str, Tensor]]:
if self._folded_text_embed is None or self._codec_embed is None:
return
combined = torch.cat([self._folded_text_embed, self._codec_embed], dim=0)
yield (self.format_tensor_name(gguf.MODEL_TENSOR.TOKEN_EMBD), combined)
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
# codec_embedding rows are appended after the text vocab, extending the embedding table
if name == "model.codec_embedding.weight":
self._codec_embed = data_torch
yield from self._maybe_emit_token_embd()
return
# codec_head is the output head for the (smaller) codec vocab; logits get padded to
# the extended vocab size at inference time
if name == "codec_head.weight":
yield (self.format_tensor_name(gguf.MODEL_TENSOR.OUTPUT), data_torch)
return
if name in self._TEXT_PROJ_KEYS:
self._text_proj_buffer[name] = data_torch
if len(self._text_proj_buffer) < len(self._TEXT_PROJ_KEYS):
return
# fold MLP into the embedding table at conversion time, MLP won't be used at inference time anyway
act_fn = _ACT2FN[self.hparams["hidden_act"]]
embed = self._text_proj_buffer["model.text_embedding.weight"]
hidden = act_fn(F.linear(embed,
self._text_proj_buffer["text_projection.linear_fc1.weight"],
self._text_proj_buffer["text_projection.linear_fc1.bias"]))
folded = F.linear(hidden,
self._text_proj_buffer["text_projection.linear_fc2.weight"],
self._text_proj_buffer["text_projection.linear_fc2.bias"])
self._folded_text_embed = folded
yield from self._maybe_emit_token_embd()
return
yield from super().modify_tensors(data_torch, name, bid)
@ModelBase.register("Qwen3TTSForConditionalGeneration")
class Qwen3TTSSpeakerEncoderModel(MmprojModel):
has_vision_encoder = False
has_audio_encoder = True
# talker.code_predictor.model.layers.{bid}.<key> -> A_GEN_CODE_*
# bypass tensor_mapping.py for now to make it simple
_CODE_LAYER_TENSOR_MAP = {
"input_layernorm": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_NORM,
"self_attn.q_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_Q,
"self_attn.q_norm": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_Q_NORM,
"self_attn.k_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_K,
"self_attn.k_norm": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_K_NORM,
"self_attn.v_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_V,
"self_attn.o_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_OUT,
"post_attention_layernorm": gguf.MODEL_TENSOR.A_GEN_CODE_FFN_NORM,
"mlp.gate_proj": gguf.MODEL_TENSOR.A_GEN_CODE_FFN_GATE,
"mlp.up_proj": gguf.MODEL_TENSOR.A_GEN_CODE_FFN_UP,
"mlp.down_proj": gguf.MODEL_TENSOR.A_GEN_CODE_FFN_DOWN,
}
# note: codebook pages will be stacked to 3D
_CODE_GEN_N_CODEBOOKS = 15
_code_embed_buffer: dict[int, Tensor] = {}
_code_head_buffer: dict[int, Tensor] = {}
_wav_config_cache: dict[str, Any] | None = None
def __init__(self, dir_model: Path, *args, **kwargs):
hparams = kwargs.pop("hparams", None)
if hparams is None:
hparams = ModelBase.load_hparams(dir_model, is_mistral_format=False)
hparams["text_config"] = {"hidden_size": hparams["talker_config"]["hidden_size"]}
# ECAPA-TDNN has a fixed 4-stage backbone, not a configurable transformer depth;
# MmprojModel.__init__ still needs one of the n_block_keys to build its tensor map
hparams["speaker_encoder_config"]["n_layers"] = 4
super().__init__(dir_model, *args, hparams=hparams, **kwargs)
self._wav_config_cache = None
def get_audio_config(self) -> dict[str, Any] | None:
return self.global_config.get("speaker_encoder_config")
def set_gguf_parameters(self):
self.gguf_writer.add_file_type(self.ftype)
self.gguf_writer.add_clip_has_audio_encoder(True)
self.gguf_writer.add_clip_audio_projector_type(gguf.VisionProjectorType.QWEN3TTS_SPKENC)
# handle speaker encoder config
self.gguf_writer.add_audio_projection_dim(self.n_embd_text)
# mel_spectrogram() front-end: sr=24000, n_fft=1024, hop=256, n_mels=128, fmin=0, fmax=12000 (=sr/2, the clip.cpp default)
self.gguf_writer.add_audio_num_mel_bins(128)
# the 3 SE-Res2Net stages (blocks 1-3); the stem conv, mfa, asp and fc are singletons, not part of this count
self.gguf_writer.add_audio_block_count(3)
# ECAPA-TDNN has no attention/FFN, these are dummy to allow clip.cpp to load it
self.gguf_writer.add_audio_embedding_length(1536)
self.gguf_writer.add_audio_head_count(1)
self.gguf_writer.add_audio_feed_forward_length(1536)
self.gguf_writer.add_audio_attention_layernorm_eps(1e-5)
# handle code predictor config
self.gguf_writer.add_clip_has_gen_audio_encoder(True)
self.gguf_writer.add_clip_gen_audio_projector_type(gguf.VisionProjectorType.QWEN3TTS_GEN)
code_predictor_config = self.global_config["talker_config"]["code_predictor_config"]
self.gguf_writer.add_gen_audio_projection_dim(self.n_embd_text)
self.gguf_writer.add_gen_audio_embedding_length(code_predictor_config["hidden_size"])
self.gguf_writer.add_gen_audio_feed_forward_length(code_predictor_config["intermediate_size"])
self.gguf_writer.add_gen_audio_block_count(code_predictor_config["num_hidden_layers"])
self.gguf_writer.add_gen_audio_head_count(code_predictor_config["num_attention_heads"])
self.gguf_writer.add_gen_audio_head_count_kv(code_predictor_config["num_key_value_heads"])
self.gguf_writer.add_gen_audio_attention_layernorm_eps(code_predictor_config["rms_norm_eps"])
# note: code2wav hparams are hardcoded on the mtmd/clip.cpp side for now, not written here
def _wav_decoder_config(self) -> dict[str, Any]:
# code2wav (RVQ codes -> raw PCM) lives in its own checkpoint dir, sibling to
# the main safetensors, with its own config.json
if self._wav_config_cache is None:
path = self.dir_model / "speech_tokenizer" / "config.json"
with open(path, "r", encoding="utf-8") as f:
cfg = json.load(f)
self._wav_config_cache = cfg["decoder_config"]
return self._wav_config_cache
def tensor_force_quant(self, name, new_name, bid, n_dims):
# regular (non-transpose) conv1d/conv1d_dw weights must be F16, never BF16:
# ggml_conv_1d(_dw) pairs the kernel as mul_mat's src1 against an F32 im2col
# src0, and the CPU backend only accepts src1 in F32 -- BF16 kernels can't be
# scheduled.
if new_name.endswith(".weight") and (
new_name in ("a.gen.wav.pre_conv.weight", "a.gen.wav.dac.entry.weight", "a.gen.wav.dac.post_conv.weight")
or (".up.blk." in new_name and new_name.endswith(".dwconv.weight"))
or (".dac.blk." in new_name and (new_name.endswith(".conv1.weight") or new_name.endswith(".conv2.weight")))
):
return gguf.GGMLQuantizationType.F16
# causal ConvTranspose1d weights: ggml_compute_forward_conv_transpose_1d
# only implements F16/F32 kernels, never BF16
if new_name.endswith(".conv.weight") and (".up.blk." in new_name or ".dac.blk." in new_name):
return gguf.GGMLQuantizationType.F32
return super().tensor_force_quant(name, new_name, bid, n_dims)
@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
name, gen = item
if not (
name.startswith("speaker_encoder.")
or name.startswith("talker.code_predictor.")
or name == "talker.model.codec_embedding.weight"
):
return None
return super().filter_tensors((name, gen))
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
# code2wav tensors come from generate_extra_tensors() already renamed to their
# final gguf name (bypassing tensor_mapping.py, same as the code_predictor
# tensors below); prepare_tensors() re-runs modify_tensors() on them too, so
# pass them through untouched instead of falling into map_tensor_name()
if name.startswith("a.gen.wav."):
yield (name, data_torch)
return
# codebook-0 embedding: fed back into the talker backbone once a codec token is
# generated, the counterpart of code_predictor's codec_embedding.{0..14} for codebooks 1-15
if name == "talker.model.codec_embedding.weight":
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_GEN_CODE_OUT_EMBD), data_torch)
return
if name == "talker.code_predictor.model.norm.weight":
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_GEN_CODE_OUTPUT_NORM), data_torch)
return
if name.startswith("talker.code_predictor.small_to_mtp_projection."):
suffix = "." + name.rsplit(".", 1)[1]
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_GEN_CODE_PROJ_IN, suffix=suffix), data_torch)
return
if name.startswith("talker.code_predictor.model.codec_embedding."):
idx = int(name.split("codec_embedding.")[1].split(".")[0])
self._code_embed_buffer[idx] = data_torch
if len(self._code_embed_buffer) < self._CODE_GEN_N_CODEBOOKS:
return
stacked = torch.stack([self._code_embed_buffer.pop(i) for i in range(self._CODE_GEN_N_CODEBOOKS)], dim=0)
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_GEN_CODE_EMBD), stacked)
return
if name.startswith("talker.code_predictor.lm_head."):
idx = int(name.split("lm_head.")[1].split(".")[0])
self._code_head_buffer[idx] = data_torch
if len(self._code_head_buffer) < self._CODE_GEN_N_CODEBOOKS:
return
stacked = torch.stack([self._code_head_buffer.pop(i) for i in range(self._CODE_GEN_N_CODEBOOKS)], dim=0)
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_GEN_CODE_HEAD), stacked)
return
if name.startswith("talker.code_predictor.model.layers."):
rest = name.split("model.layers.")[1] # "{bid}.<key>.weight"
_, key_with_suffix = rest.split(".", 1) # "<key>.weight"
key = key_with_suffix.rsplit(".", 1)[0] # "<key>"
tensor = self._CODE_LAYER_TENSOR_MAP.get(key)
if tensor is not None:
yield (self.format_tensor_name(tensor, bid), data_torch)
return
if "res2net_block.blocks." in name:
assert bid is not None # the outer stage index, picked up from the tensor name automatically
xid = int(name.split("res2net_block.blocks.")[1].split(".")[0])
suffix = "." + name.rsplit(".", 1)[1]
new_name = gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.A_ENC_CONV_RES2].format(bid=bid, xid=xid) + suffix
yield (new_name, data_torch)
return
yield from super().modify_tensors(data_torch, name, bid)
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
yield from self._generate_code2wav_tensors()
def _generate_code2wav_tensors(self) -> Iterable[tuple[str, Tensor]]:
# code2wav lives in its own checkpoint dir (speech_tokenizer/model.safetensors),
# not the main safetensors this ModelBase was constructed from
from safetensors.torch import load_file
wav_config = self._wav_decoder_config()
state_dict = load_file(self.dir_model / "speech_tokenizer" / "model.safetensors")
def get(name: str) -> Tensor:
return state_dict[name]
def snake_fold(alpha: Tensor, beta: Tensor) -> tuple[Tensor, Tensor]:
# SnakeBeta activation folds its exp()/reciprocal at conversion time, so the
# runtime graph is only mul -> sin -> sqr -> mul -> add
return torch.exp(alpha), 1.0 / (torch.exp(beta) + 1e-9)
def rvq_codebook(prefix: str, n_layers: int) -> Tensor:
# the checkpoint stores EMA training accumulators, not a ready embedding
# table: codebook[i] = embedding_sum[i] / cluster_usage[i]
books = []
for i in range(n_layers):
embedding_sum = get(f"{prefix}.vq.layers.{i}._codebook.embedding_sum")
cluster_usage = get(f"{prefix}.vq.layers.{i}._codebook.cluster_usage")
books.append(embedding_sum / cluster_usage.clamp_min(1e-5).unsqueeze(-1))
return torch.stack(books, dim=0) if n_layers > 1 else books[0]
T = gguf.MODEL_TENSOR
# --- quantizer: RVQ codebook decode ---
yield (self.format_tensor_name(T.A_GEN_WAV_QUANT_FIRST_IN), get("decoder.quantizer.rvq_first.input_proj.weight").squeeze(-1))
yield (self.format_tensor_name(T.A_GEN_WAV_QUANT_FIRST_OUT), get("decoder.quantizer.rvq_first.output_proj.weight").squeeze(-1))
yield (self.format_tensor_name(T.A_GEN_WAV_QUANT_FIRST_CB), rvq_codebook("decoder.quantizer.rvq_first", 1))
yield (self.format_tensor_name(T.A_GEN_WAV_QUANT_REST_IN), get("decoder.quantizer.rvq_rest.input_proj.weight").squeeze(-1))
yield (self.format_tensor_name(T.A_GEN_WAV_QUANT_REST_OUT), get("decoder.quantizer.rvq_rest.output_proj.weight").squeeze(-1))
yield (self.format_tensor_name(T.A_GEN_WAV_QUANT_REST_CB), rvq_codebook("decoder.quantizer.rvq_rest", self._CODE_GEN_N_CODEBOOKS))
# --- pre_conv ---
yield (self.format_tensor_name(T.A_GEN_WAV_PRE_CONV, suffix=".weight"), get("decoder.pre_conv.conv.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_PRE_CONV, suffix=".bias"), get("decoder.pre_conv.conv.bias"))
# --- pre_transformer ---
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_IN_PROJ, suffix=".weight"), get("decoder.pre_transformer.input_proj.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_IN_PROJ, suffix=".bias"), get("decoder.pre_transformer.input_proj.bias"))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_OUT_PROJ, suffix=".weight"), get("decoder.pre_transformer.output_proj.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_OUT_PROJ, suffix=".bias"), get("decoder.pre_transformer.output_proj.bias"))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_OUTPUT_NORM), get("decoder.pre_transformer.norm.weight"))
tfm_layer_map = {
"input_layernorm.weight": T.A_GEN_WAV_TFM_ATTN_NORM,
"self_attn.q_proj.weight": T.A_GEN_WAV_TFM_ATTN_Q,
"self_attn.k_proj.weight": T.A_GEN_WAV_TFM_ATTN_K,
"self_attn.v_proj.weight": T.A_GEN_WAV_TFM_ATTN_V,
"self_attn.o_proj.weight": T.A_GEN_WAV_TFM_ATTN_OUT,
"self_attn_layer_scale.scale": T.A_GEN_WAV_TFM_ATTN_SCALE,
"post_attention_layernorm.weight": T.A_GEN_WAV_TFM_FFN_NORM,
"mlp.gate_proj.weight": T.A_GEN_WAV_TFM_FFN_GATE,
"mlp.up_proj.weight": T.A_GEN_WAV_TFM_FFN_UP,
"mlp.down_proj.weight": T.A_GEN_WAV_TFM_FFN_DOWN,
"mlp_layer_scale.scale": T.A_GEN_WAV_TFM_FFN_SCALE,
}
for bid in range(wav_config["num_hidden_layers"]):
for key, tensor_id in tfm_layer_map.items():
yield (self.format_tensor_name(tensor_id, bid), get(f"decoder.pre_transformer.layers.{bid}.{key}"))
# --- upsample: 2x (causal ConvTranspose1d + ConvNeXt block) ---
up_map = {
"0.conv.weight": (T.A_GEN_WAV_UP_CONV, ".weight"),
"0.conv.bias": (T.A_GEN_WAV_UP_CONV, ".bias"),
"1.dwconv.conv.weight": (T.A_GEN_WAV_UP_DWCONV, ".weight"),
"1.dwconv.conv.bias": (T.A_GEN_WAV_UP_DWCONV, ".bias"),
"1.norm.weight": (T.A_GEN_WAV_UP_NORM, ".weight"),
"1.norm.bias": (T.A_GEN_WAV_UP_NORM, ".bias"),
"1.pwconv1.weight": (T.A_GEN_WAV_UP_PW1, ".weight"),
"1.pwconv1.bias": (T.A_GEN_WAV_UP_PW1, ".bias"),
"1.pwconv2.weight": (T.A_GEN_WAV_UP_PW2, ".weight"),
"1.pwconv2.bias": (T.A_GEN_WAV_UP_PW2, ".bias"),
"1.gamma": (T.A_GEN_WAV_UP_GAMMA, ""),
}
for bid in range(len(wav_config["upsampling_ratios"])):
for key, (tensor_id, suffix) in up_map.items():
yield (self.format_tensor_name(tensor_id, bid, suffix=suffix), get(f"decoder.upsample.{bid}.{key}"))
# --- DAC decoder ---
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_ENTRY, suffix=".weight"), get("decoder.decoder.0.conv.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_ENTRY, suffix=".bias"), get("decoder.decoder.0.conv.bias"))
n_dac_blocks = len(wav_config["upsample_rates"])
for bid in range(n_dac_blocks):
py = bid + 1 # decoder.decoder.0 is the entry conv, blocks start at 1
a, b = snake_fold(get(f"decoder.decoder.{py}.block.0.alpha"), get(f"decoder.decoder.{py}.block.0.beta"))
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_UP_SNAKE, bid, suffix=".alpha"), a)
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_UP_SNAKE, bid, suffix=".beta"), b)
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_UP_CONV, bid, suffix=".weight"), get(f"decoder.decoder.{py}.block.1.conv.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_UP_CONV, bid, suffix=".bias"), get(f"decoder.decoder.{py}.block.1.conv.bias"))
for xid in range(3):
ridx = xid + 2 # block.2/3/4 are the 3 residual units
a1, b1 = snake_fold(get(f"decoder.decoder.{py}.block.{ridx}.act1.alpha"), get(f"decoder.decoder.{py}.block.{ridx}.act1.beta"))
name1 = gguf.TENSOR_NAMES[T.A_GEN_WAV_DAC_RES_ACT1].format(bid=bid, xid=xid)
yield (name1 + ".alpha", a1)
yield (name1 + ".beta", b1)
name_conv1 = gguf.TENSOR_NAMES[T.A_GEN_WAV_DAC_RES_CONV1].format(bid=bid, xid=xid)
yield (name_conv1 + ".weight", get(f"decoder.decoder.{py}.block.{ridx}.conv1.conv.weight"))
yield (name_conv1 + ".bias", get(f"decoder.decoder.{py}.block.{ridx}.conv1.conv.bias"))
a2, b2 = snake_fold(get(f"decoder.decoder.{py}.block.{ridx}.act2.alpha"), get(f"decoder.decoder.{py}.block.{ridx}.act2.beta"))
name2 = gguf.TENSOR_NAMES[T.A_GEN_WAV_DAC_RES_ACT2].format(bid=bid, xid=xid)
yield (name2 + ".alpha", a2)
yield (name2 + ".beta", b2)
name_conv2 = gguf.TENSOR_NAMES[T.A_GEN_WAV_DAC_RES_CONV2].format(bid=bid, xid=xid)
yield (name_conv2 + ".weight", get(f"decoder.decoder.{py}.block.{ridx}.conv2.conv.weight"))
yield (name_conv2 + ".bias", get(f"decoder.decoder.{py}.block.{ridx}.conv2.conv.bias"))
a5, b5 = snake_fold(get("decoder.decoder.5.alpha"), get("decoder.decoder.5.beta"))
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_POST_SNAKE, suffix=".alpha"), a5)
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_POST_SNAKE, suffix=".beta"), b5)
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_POST_CONV, suffix=".weight"), get("decoder.decoder.6.conv.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_POST_CONV, suffix=".bias"), get("decoder.decoder.6.conv.bias"))
+207
View File
@@ -322,6 +322,7 @@ class Keys:
PROJECTOR_TYPE = "clip.projector_type"
HAS_VISION_ENCODER = "clip.has_vision_encoder"
HAS_AUDIO_ENCODER = "clip.has_audio_encoder"
HAS_GEN_AUDIO_ENCODER = "clip.has_gen_audio_encoder"
HAS_LLAVA_PROJECTOR = "clip.has_llava_projector"
class ClipVision:
@@ -395,6 +396,18 @@ class Keys:
DOWNSAMPLE_RATE = "clip.audio.projector.downsample_rate"
HEAD_COUNT = "clip.audio.projector.head_count"
class ClipGenAudio:
PROJECTOR_TYPE = "clip.gen.audio.projector_type" # for mixed modality models
EMBEDDING_LENGTH = "clip.gen.audio.embedding_length"
FEED_FORWARD_LENGTH = "clip.gen.audio.feed_forward_length"
BLOCK_COUNT = "clip.gen.audio.block_count"
PROJECTION_DIM = "clip.gen.audio.projection_dim"
class Attention:
HEAD_COUNT = "clip.gen.audio.attention.head_count"
HEAD_COUNT_KV = "clip.gen.audio.attention.head_count_kv"
LAYERNORM_EPS = "clip.gen.audio.attention.layer_norm_epsilon"
class Diffusion:
SHIFT_LOGITS = "diffusion.shift_logits"
@@ -556,6 +569,7 @@ class MODEL_ARCH(IntEnum):
TALKIE = auto()
MELLUM = auto()
NANBEIGE = auto()
QWEN3TTS = auto()
class VISION_PROJECTOR_TYPE(IntEnum):
@@ -956,6 +970,66 @@ class MODEL_TENSOR(IntEnum):
A_ENC_DOWNSAMPLE_CONV = auto() # mimo-audio-tokenizer: post-transformer downsample conv
A_ENC_DOWNSAMPLE_NORM = auto() # mimo-audio-tokenizer: post-transformer downsample norm
A_ENC_RVQ_CODEBOOK = auto() # mimo-audio-tokenizer: residual vector quantizer codebook, per quantizer index
A_ENC_CONV_RES2 = auto() # qwen3tts
A_ENC_SE_CONV1 = auto() # qwen3tts
A_ENC_SE_CONV2 = auto() # qwen3tts
A_ENC_ASP_ATTN = auto() # qwen3tts
A_ENC_ASP_TDNN = auto() # qwen3tts
# qwen3tts code_predictor: autoregressively predicts the remaining RVQ codebooks
A_GEN_CODE_PROJ_IN = auto() # small_to_mtp_projection
A_GEN_CODE_EMBD = auto() # per-codebook embedding table, merged 3D [n_codebooks, vocab, dim]
A_GEN_CODE_HEAD = auto() # per-codebook output head, merged 3D [n_codebooks, vocab, dim]
A_GEN_CODE_OUT_EMBD = auto() # codebook-0 embedding, re-fed into the talker backbone (talker.model.codec_embedding)
A_GEN_CODE_ATTN_NORM = auto()
A_GEN_CODE_ATTN_Q = auto()
A_GEN_CODE_ATTN_Q_NORM = auto()
A_GEN_CODE_ATTN_K = auto()
A_GEN_CODE_ATTN_K_NORM = auto()
A_GEN_CODE_ATTN_V = auto()
A_GEN_CODE_ATTN_OUT = auto()
A_GEN_CODE_FFN_NORM = auto()
A_GEN_CODE_FFN_GATE = auto()
A_GEN_CODE_FFN_UP = auto()
A_GEN_CODE_FFN_DOWN = auto()
A_GEN_CODE_OUTPUT_NORM = auto()
# qwen3tts code2wav: RVQ codes -> raw PCM (quantizer decode + pre_conv +
# pre_transformer + ConvNeXt upsample + DAC decoder)
A_GEN_WAV_QUANT_FIRST_IN = auto() # semantic RVQ, in_proj (1x1 conv, loaded as 2D)
A_GEN_WAV_QUANT_FIRST_OUT = auto() # semantic RVQ, out_proj
A_GEN_WAV_QUANT_FIRST_CB = auto() # semantic RVQ codebook (1 layer), folded from embedding_sum/cluster_usage
A_GEN_WAV_QUANT_REST_IN = auto() # acoustic RVQ, in_proj
A_GEN_WAV_QUANT_REST_OUT = auto() # acoustic RVQ, out_proj
A_GEN_WAV_QUANT_REST_CB = auto() # acoustic RVQ codebooks, merged 3D [15, vocab, dim]
A_GEN_WAV_PRE_CONV = auto()
A_GEN_WAV_TFM_IN_PROJ = auto()
A_GEN_WAV_TFM_OUT_PROJ = auto()
A_GEN_WAV_TFM_OUTPUT_NORM = auto()
A_GEN_WAV_TFM_ATTN_NORM = auto()
A_GEN_WAV_TFM_ATTN_Q = auto()
A_GEN_WAV_TFM_ATTN_K = auto()
A_GEN_WAV_TFM_ATTN_V = auto()
A_GEN_WAV_TFM_ATTN_OUT = auto()
A_GEN_WAV_TFM_ATTN_SCALE = auto() # layer scale (gamma) on the attn output
A_GEN_WAV_TFM_FFN_NORM = auto()
A_GEN_WAV_TFM_FFN_GATE = auto()
A_GEN_WAV_TFM_FFN_UP = auto()
A_GEN_WAV_TFM_FFN_DOWN = auto()
A_GEN_WAV_TFM_FFN_SCALE = auto() # layer scale (gamma) on the FFN output
A_GEN_WAV_UP_CONV = auto() # causal ConvTranspose1d, 2x upsample
A_GEN_WAV_UP_DWCONV = auto() # ConvNeXt depthwise conv
A_GEN_WAV_UP_NORM = auto() # ConvNeXt LayerNorm
A_GEN_WAV_UP_PW1 = auto() # ConvNeXt pointwise conv 1 (expand)
A_GEN_WAV_UP_PW2 = auto() # ConvNeXt pointwise conv 2 (project)
A_GEN_WAV_UP_GAMMA = auto() # ConvNeXt layer scale
A_GEN_WAV_DAC_ENTRY = auto() # DAC conv_pre
A_GEN_WAV_DAC_UP_SNAKE = auto() # DAC per-block SnakeBeta before the upsample conv
A_GEN_WAV_DAC_UP_CONV = auto() # DAC per-block causal ConvTranspose1d
A_GEN_WAV_DAC_RES_ACT1 = auto() # DAC residual unit, SnakeBeta before conv1
A_GEN_WAV_DAC_RES_CONV1 = auto() # DAC residual unit, dilated causal conv
A_GEN_WAV_DAC_RES_ACT2 = auto() # DAC residual unit, SnakeBeta before conv2
A_GEN_WAV_DAC_RES_CONV2 = auto() # DAC residual unit, pointwise causal conv
A_GEN_WAV_DAC_POST_SNAKE = auto() # DAC final SnakeBeta
A_GEN_WAV_DAC_POST_CONV = auto() # DAC conv_post -> 1-channel PCM
A_MMPROJ = auto()
A_MMPROJ_FC = auto()
A_MM_NORM_PRE = auto()
@@ -1168,6 +1242,7 @@ MODEL_ARCH_NAMES: dict[MODEL_ARCH, str] = {
MODEL_ARCH.TALKIE: "talkie",
MODEL_ARCH.MELLUM: "mellum",
MODEL_ARCH.NANBEIGE: "nanbeige",
MODEL_ARCH.QWEN3TTS: "qwen3tts",
}
VISION_PROJECTOR_TYPE_NAMES: dict[VISION_PROJECTOR_TYPE, str] = {
@@ -1565,6 +1640,63 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
MODEL_TENSOR.A_ENC_DOWNSAMPLE_CONV: "a.downsample.conv",
MODEL_TENSOR.A_ENC_DOWNSAMPLE_NORM: "a.downsample.norm",
MODEL_TENSOR.A_ENC_RVQ_CODEBOOK: "a.rvq.codebook",
MODEL_TENSOR.A_ENC_CONV_RES2: "a.blk.{bid}.res2.{xid}",
MODEL_TENSOR.A_ENC_SE_CONV1: "a.blk.{bid}.se_conv1",
MODEL_TENSOR.A_ENC_SE_CONV2: "a.blk.{bid}.se_conv2",
MODEL_TENSOR.A_ENC_ASP_ATTN: "a.asp_attn",
MODEL_TENSOR.A_ENC_ASP_TDNN: "a.asp_tdnn",
MODEL_TENSOR.A_GEN_CODE_PROJ_IN: "a.gen.code.proj_in",
MODEL_TENSOR.A_GEN_CODE_EMBD: "a.gen.code.embd",
MODEL_TENSOR.A_GEN_CODE_HEAD: "a.gen.code.head",
MODEL_TENSOR.A_GEN_CODE_OUT_EMBD: "a.gen.code.out_embd",
MODEL_TENSOR.A_GEN_CODE_ATTN_NORM: "a.gen.code.blk.{bid}.ln1", # reuses the generic clip.cpp block loader (TN_LN_1)
MODEL_TENSOR.A_GEN_CODE_ATTN_Q: "a.gen.code.blk.{bid}.attn_q",
MODEL_TENSOR.A_GEN_CODE_ATTN_Q_NORM: "a.gen.code.blk.{bid}.attn_q_norm",
MODEL_TENSOR.A_GEN_CODE_ATTN_K: "a.gen.code.blk.{bid}.attn_k",
MODEL_TENSOR.A_GEN_CODE_ATTN_K_NORM: "a.gen.code.blk.{bid}.attn_k_norm",
MODEL_TENSOR.A_GEN_CODE_ATTN_V: "a.gen.code.blk.{bid}.attn_v",
MODEL_TENSOR.A_GEN_CODE_ATTN_OUT: "a.gen.code.blk.{bid}.attn_out",
MODEL_TENSOR.A_GEN_CODE_FFN_NORM: "a.gen.code.blk.{bid}.ln2", # reuses the generic clip.cpp block loader (TN_LN_2)
MODEL_TENSOR.A_GEN_CODE_FFN_GATE: "a.gen.code.blk.{bid}.ffn_gate",
MODEL_TENSOR.A_GEN_CODE_FFN_UP: "a.gen.code.blk.{bid}.ffn_up",
MODEL_TENSOR.A_GEN_CODE_FFN_DOWN: "a.gen.code.blk.{bid}.ffn_down",
MODEL_TENSOR.A_GEN_CODE_OUTPUT_NORM: "a.gen.code.output_norm",
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_IN: "a.gen.wav.quant.first.in_proj",
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_OUT: "a.gen.wav.quant.first.out_proj",
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_CB: "a.gen.wav.quant.first.codebook",
MODEL_TENSOR.A_GEN_WAV_QUANT_REST_IN: "a.gen.wav.quant.rest.in_proj",
MODEL_TENSOR.A_GEN_WAV_QUANT_REST_OUT: "a.gen.wav.quant.rest.out_proj",
MODEL_TENSOR.A_GEN_WAV_QUANT_REST_CB: "a.gen.wav.quant.rest.codebook",
MODEL_TENSOR.A_GEN_WAV_PRE_CONV: "a.gen.wav.pre_conv",
MODEL_TENSOR.A_GEN_WAV_TFM_IN_PROJ: "a.gen.wav.tfm.in_proj",
MODEL_TENSOR.A_GEN_WAV_TFM_OUT_PROJ: "a.gen.wav.tfm.out_proj",
MODEL_TENSOR.A_GEN_WAV_TFM_OUTPUT_NORM: "a.gen.wav.tfm.output_norm",
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_NORM: "a.gen.wav.tfm.blk.{bid}.ln1",
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_Q: "a.gen.wav.tfm.blk.{bid}.attn_q",
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_K: "a.gen.wav.tfm.blk.{bid}.attn_k",
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_V: "a.gen.wav.tfm.blk.{bid}.attn_v",
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_OUT: "a.gen.wav.tfm.blk.{bid}.attn_out",
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_SCALE: "a.gen.wav.tfm.blk.{bid}.ls1",
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_NORM: "a.gen.wav.tfm.blk.{bid}.ln2",
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_GATE: "a.gen.wav.tfm.blk.{bid}.ffn_gate",
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_UP: "a.gen.wav.tfm.blk.{bid}.ffn_up",
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_DOWN: "a.gen.wav.tfm.blk.{bid}.ffn_down",
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_SCALE: "a.gen.wav.tfm.blk.{bid}.ls2",
MODEL_TENSOR.A_GEN_WAV_UP_CONV: "a.gen.wav.up.blk.{bid}.conv",
MODEL_TENSOR.A_GEN_WAV_UP_DWCONV: "a.gen.wav.up.blk.{bid}.dwconv",
MODEL_TENSOR.A_GEN_WAV_UP_NORM: "a.gen.wav.up.blk.{bid}.norm",
MODEL_TENSOR.A_GEN_WAV_UP_PW1: "a.gen.wav.up.blk.{bid}.pw1",
MODEL_TENSOR.A_GEN_WAV_UP_PW2: "a.gen.wav.up.blk.{bid}.pw2",
MODEL_TENSOR.A_GEN_WAV_UP_GAMMA: "a.gen.wav.up.blk.{bid}.gamma",
MODEL_TENSOR.A_GEN_WAV_DAC_ENTRY: "a.gen.wav.dac.entry",
MODEL_TENSOR.A_GEN_WAV_DAC_UP_SNAKE: "a.gen.wav.dac.blk.{bid}.snake",
MODEL_TENSOR.A_GEN_WAV_DAC_UP_CONV: "a.gen.wav.dac.blk.{bid}.conv",
MODEL_TENSOR.A_GEN_WAV_DAC_RES_ACT1: "a.gen.wav.dac.blk.{bid}.res.{xid}.act1",
MODEL_TENSOR.A_GEN_WAV_DAC_RES_CONV1: "a.gen.wav.dac.blk.{bid}.res.{xid}.conv1",
MODEL_TENSOR.A_GEN_WAV_DAC_RES_ACT2: "a.gen.wav.dac.blk.{bid}.res.{xid}.act2",
MODEL_TENSOR.A_GEN_WAV_DAC_RES_CONV2: "a.gen.wav.dac.blk.{bid}.res.{xid}.conv2",
MODEL_TENSOR.A_GEN_WAV_DAC_POST_SNAKE: "a.gen.wav.dac.post_snake",
MODEL_TENSOR.A_GEN_WAV_DAC_POST_CONV: "a.gen.wav.dac.post_conv",
MODEL_TENSOR.A_MMPROJ: "mm.a.mlp.{bid}",
MODEL_TENSOR.A_MMPROJ_FC: "mm.a.fc",
MODEL_TENSOR.A_MM_NORM_PRE: "mm.a.norm_pre",
@@ -1819,6 +1951,63 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
MODEL_TENSOR.A_ENC_CONV_NORM,
MODEL_TENSOR.A_ENC_CONV_PW1,
MODEL_TENSOR.A_ENC_CONV_PW2,
MODEL_TENSOR.A_ENC_CONV_RES2,
MODEL_TENSOR.A_ENC_SE_CONV1,
MODEL_TENSOR.A_ENC_SE_CONV2,
MODEL_TENSOR.A_ENC_ASP_ATTN,
MODEL_TENSOR.A_ENC_ASP_TDNN,
MODEL_TENSOR.A_GEN_CODE_PROJ_IN,
MODEL_TENSOR.A_GEN_CODE_EMBD,
MODEL_TENSOR.A_GEN_CODE_HEAD,
MODEL_TENSOR.A_GEN_CODE_OUT_EMBD,
MODEL_TENSOR.A_GEN_CODE_ATTN_NORM,
MODEL_TENSOR.A_GEN_CODE_ATTN_Q,
MODEL_TENSOR.A_GEN_CODE_ATTN_Q_NORM,
MODEL_TENSOR.A_GEN_CODE_ATTN_K,
MODEL_TENSOR.A_GEN_CODE_ATTN_K_NORM,
MODEL_TENSOR.A_GEN_CODE_ATTN_V,
MODEL_TENSOR.A_GEN_CODE_ATTN_OUT,
MODEL_TENSOR.A_GEN_CODE_FFN_NORM,
MODEL_TENSOR.A_GEN_CODE_FFN_GATE,
MODEL_TENSOR.A_GEN_CODE_FFN_UP,
MODEL_TENSOR.A_GEN_CODE_FFN_DOWN,
MODEL_TENSOR.A_GEN_CODE_OUTPUT_NORM,
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_IN,
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_OUT,
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_CB,
MODEL_TENSOR.A_GEN_WAV_QUANT_REST_IN,
MODEL_TENSOR.A_GEN_WAV_QUANT_REST_OUT,
MODEL_TENSOR.A_GEN_WAV_QUANT_REST_CB,
MODEL_TENSOR.A_GEN_WAV_PRE_CONV,
MODEL_TENSOR.A_GEN_WAV_TFM_IN_PROJ,
MODEL_TENSOR.A_GEN_WAV_TFM_OUT_PROJ,
MODEL_TENSOR.A_GEN_WAV_TFM_OUTPUT_NORM,
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_NORM,
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_Q,
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_K,
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_V,
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_OUT,
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_SCALE,
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_NORM,
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_GATE,
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_UP,
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_DOWN,
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_SCALE,
MODEL_TENSOR.A_GEN_WAV_UP_CONV,
MODEL_TENSOR.A_GEN_WAV_UP_DWCONV,
MODEL_TENSOR.A_GEN_WAV_UP_NORM,
MODEL_TENSOR.A_GEN_WAV_UP_PW1,
MODEL_TENSOR.A_GEN_WAV_UP_PW2,
MODEL_TENSOR.A_GEN_WAV_UP_GAMMA,
MODEL_TENSOR.A_GEN_WAV_DAC_ENTRY,
MODEL_TENSOR.A_GEN_WAV_DAC_UP_SNAKE,
MODEL_TENSOR.A_GEN_WAV_DAC_UP_CONV,
MODEL_TENSOR.A_GEN_WAV_DAC_RES_ACT1,
MODEL_TENSOR.A_GEN_WAV_DAC_RES_CONV1,
MODEL_TENSOR.A_GEN_WAV_DAC_RES_ACT2,
MODEL_TENSOR.A_GEN_WAV_DAC_RES_CONV2,
MODEL_TENSOR.A_GEN_WAV_DAC_POST_SNAKE,
MODEL_TENSOR.A_GEN_WAV_DAC_POST_CONV,
MODEL_TENSOR.A_ENC_CONV_NORM_MEAN,
MODEL_TENSOR.A_ENC_CONV_NORM_VAR,
MODEL_TENSOR.A_ENC_MEL_FILTERS,
@@ -4602,6 +4791,22 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
MODEL_TENSOR.FFN_DOWN,
MODEL_TENSOR.FFN_UP,
],
MODEL_ARCH.QWEN3TTS: [
MODEL_TENSOR.TOKEN_EMBD,
MODEL_TENSOR.OUTPUT_NORM,
MODEL_TENSOR.OUTPUT,
MODEL_TENSOR.ATTN_NORM,
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.FFN_NORM,
MODEL_TENSOR.FFN_GATE,
MODEL_TENSOR.FFN_DOWN,
MODEL_TENSOR.FFN_UP,
],
}
# tensors that will not be serialized
@@ -4876,6 +5081,8 @@ class VisionProjectorType:
GLM4V = "glm4v"
YOUTUVL = "youtuvl"
NEMOTRON_V2_VL = "nemotron_v2_vl"
QWEN3TTS_SPKENC = "qwen3tts_spkenc" # audio: ECAPA-TDNN speaker encoder
QWEN3TTS_GEN = "qwen3tts_gen" # audio generation: code_predictor
HUNYUANVL = "hunyuanvl"
PARAKEET = "parakeet" # audio
MINIMAXM3 = "minimax_m3"
+30
View File
@@ -1199,6 +1199,9 @@ class GGUFWriter:
def add_clip_has_audio_encoder(self, value: bool) -> None:
self.add_bool(Keys.Clip.HAS_AUDIO_ENCODER, value)
def add_clip_has_gen_audio_encoder(self, value: bool) -> None:
self.add_bool(Keys.Clip.HAS_GEN_AUDIO_ENCODER, value)
def add_clip_projector_type(self, value: str) -> None:
self.add_string(Keys.Clip.PROJECTOR_TYPE, value)
@@ -1398,6 +1401,33 @@ class GGUFWriter:
def add_audio_projector_head_count(self, value: int) -> None:
self.add_uint32(Keys.ClipAudio.Projector.HEAD_COUNT, value)
# audio generation (mmproj)
def add_clip_gen_audio_projector_type(self, value: str) -> None:
self.add_string(Keys.ClipGenAudio.PROJECTOR_TYPE, value)
def add_gen_audio_projection_dim(self, value: int) -> None:
self.add_uint32(Keys.ClipGenAudio.PROJECTION_DIM, value)
def add_gen_audio_embedding_length(self, value: int) -> None:
self.add_uint32(Keys.ClipGenAudio.EMBEDDING_LENGTH, value)
def add_gen_audio_feed_forward_length(self, value: int) -> None:
self.add_uint32(Keys.ClipGenAudio.FEED_FORWARD_LENGTH, value)
def add_gen_audio_block_count(self, value: int) -> None:
self.add_uint32(Keys.ClipGenAudio.BLOCK_COUNT, value)
def add_gen_audio_head_count(self, value: int) -> None:
self.add_uint32(Keys.ClipGenAudio.Attention.HEAD_COUNT, value)
def add_gen_audio_head_count_kv(self, value: int) -> None:
self.add_uint32(Keys.ClipGenAudio.Attention.HEAD_COUNT_KV, value)
def add_gen_audio_attention_layernorm_eps(self, value: float) -> None:
self.add_float32(Keys.ClipGenAudio.Attention.LAYERNORM_EPS, value)
def add_xielu_alpha_p(self, values: Sequence[float]):
self.add_array(Keys.xIELU.ALPHA_P, values)
+22 -1
View File
@@ -2109,6 +2109,7 @@ class TensorNameMap:
"conformer.subsample_conv_projection.layer{bid}.conv", # gemma4
"sound_encoder.encoder.subsampling.layers.{bid}", # parakeet
"encoder.conv{bid}", # mimo-audio-tokenizer
"speaker_encoder.blocks.{bid}.conv", # qwen3tts speaker encoder (only bid=0, the stem TDNN)
),
MODEL_TENSOR.A_ENC_CONV1D_NORM: (
@@ -2126,6 +2127,7 @@ class TensorNameMap:
MODEL_TENSOR.A_ENC_CONV_OUT: (
"audio_tower.conv_out", # qwen3omni
"speaker_encoder.mfa.conv", # qwen3tts speaker encoder: multi-layer feature aggregation
),
MODEL_TENSOR.A_PRE_NORM: (),
@@ -2336,7 +2338,8 @@ class TensorNameMap:
MODEL_TENSOR.A_MMPROJ_FC: (
"audio.multi_modal_projector.linear", # qwen2audio
"audio_tower.proj", # qwen2omni
"model.audio_tower.output_proj" # gemma4
"model.audio_tower.output_proj", # gemma4
"speaker_encoder.fc", # qwen3tts speaker encoder: final speaker embedding projection
),
MODEL_TENSOR.A_MM_NORM_PRE: (
@@ -2411,6 +2414,7 @@ class TensorNameMap:
"conformer.layers.{bid}.lconv1d.linear_start", # gemma3n
"sound_encoder.encoder.layers.{bid}.conv.pointwise_conv1", # parakeet
"encoder.layers.{bid}.conv.up_conv", # granite_speech
"speaker_encoder.blocks.{bid}.tdnn1.conv", # qwen3tts speaker encoder
),
MODEL_TENSOR.A_ENC_CONV_PW2: (
@@ -2418,6 +2422,23 @@ class TensorNameMap:
"conformer.layers.{bid}.lconv1d.linear_end", # gemma3n
"sound_encoder.encoder.layers.{bid}.conv.pointwise_conv2", # parakeet
"encoder.layers.{bid}.conv.down_conv", # granite_speech
"speaker_encoder.blocks.{bid}.tdnn2.conv", # qwen3tts speaker encoder
),
MODEL_TENSOR.A_ENC_SE_CONV1: (
"speaker_encoder.blocks.{bid}.se_block.conv1", # qwen3tts
),
MODEL_TENSOR.A_ENC_SE_CONV2: (
"speaker_encoder.blocks.{bid}.se_block.conv2", # qwen3tts
),
MODEL_TENSOR.A_ENC_ASP_ATTN: (
"speaker_encoder.asp.conv", # qwen3tts
),
MODEL_TENSOR.A_ENC_ASP_TDNN: (
"speaker_encoder.asp.tdnn.conv", # qwen3tts
),
MODEL_TENSOR.A_ENC_NORM_CONV: (
+1
View File
@@ -144,6 +144,7 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
{ LLM_ARCH_TALKIE, "talkie" },
{ LLM_ARCH_MELLUM, "mellum" },
{ LLM_ARCH_NANBEIGE, "nanbeige" },
{ LLM_ARCH_QWEN3TTS, "qwen3tts" },
{ LLM_ARCH_UNKNOWN, "(unknown)" },
};
+1
View File
@@ -149,6 +149,7 @@ enum llm_arch {
LLM_ARCH_MINIMAX_M3,
LLM_ARCH_DFLASH,
LLM_ARCH_NANBEIGE,
LLM_ARCH_QWEN3TTS,
LLM_ARCH_UNKNOWN,
};
+5
View File
@@ -111,6 +111,10 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params
return new llama_model_qwen3vl(params);
case LLM_ARCH_QWEN3VLMOE:
return new llama_model_qwen3vlmoe(params);
case LLM_ARCH_QWEN3TTS:
// Qwen3-TTS talker backbone: identical tensor layout and interleaved
// mrope to qwen3vl, just without vision/deepstack tensors (n_deepstack_layers is 0)
return new llama_model_qwen3vl(params);
case LLM_ARCH_PHI2:
return new llama_model_phi2(params);
case LLM_ARCH_PHI3:
@@ -2628,6 +2632,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
case LLM_ARCH_QWEN3VLMOE:
case LLM_ARCH_QWEN35:
case LLM_ARCH_QWEN35MOE:
case LLM_ARCH_QWEN3TTS:
return LLAMA_ROPE_TYPE_IMROPE;
case LLM_ARCH_GLM4:
+24 -1
View File
@@ -16,11 +16,16 @@ void llama_model_qwen3vl::load_arch_hparams(llama_model_loader & ml) {
void llama_model_qwen3vl::load_arch_tensors(llama_model_loader &) {
LLAMA_LOAD_LOCALS;
int64_t n_vocab_out = n_vocab;
if (arch == LLM_ARCH_QWEN3TTS) {
n_vocab_out = 3072;
}
tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
// output
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);
output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab_out}, TENSOR_NOT_REQUIRED);
// if output is NULL, init from the input tok embed
if (output == NULL) {
output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
@@ -166,6 +171,24 @@ llama_model_qwen3vl::graph::graph(const llama_model & model, const llm_graph_par
// lm_head
cur = build_lora_mm(model.output, cur, model.output_s);
int64_t n_vocab_in = model.tok_embd->ne[1];
int64_t n_vocab_out = model.output->ne[1];
if (n_vocab_in > n_vocab_out) {
// case: Qwen3TTS model with codec_head as output
GGML_ASSERT(model.output_norm);
int64_t pad = n_vocab_in - n_vocab_out;
// using this trick to get a scalar -inf tensor to pad the output
ggml_tensor * neg_inf = ggml_scale_bias(ctx0,
ggml_view_1d(ctx0, model.output_norm, 1, 0),
0.0f, -INFINITY);
neg_inf = ggml_repeat_4d(ctx0, neg_inf, pad, cur->ne[1], 1, 1);
cur = ggml_concat(ctx0, neg_inf, cur, 0); // [padded .. n_vocab_out, n_stream]
} else if (n_vocab_in < n_vocab_out) {
GGML_ASSERT("invalid case");
}
cb(cur, "result_output", -1);
res->t_logits = cur;
+2
View File
@@ -52,6 +52,8 @@ add_library(mtmd
models/mimovl.cpp
models/qwen3a.cpp
models/mimo-audio.cpp
models/qwen3tts-spkenc.cpp
models/qwen3tts-gen.cpp
models/step3vl.cpp
models/siglip.cpp
models/whisper-enc.cpp
+3
View File
@@ -54,6 +54,9 @@ struct clip_graph {
clip_graph(clip_ctx * ctx, const clip_image_f32 & img);
// build sub-graph, reuse buf from parent
clip_graph(const clip_graph & parent);
virtual ~clip_graph() = default;
virtual ggml_cgraph * build() = 0;
+49
View File
@@ -32,6 +32,7 @@
#define KEY_PROJ_TYPE "clip.projector_type"
#define KEY_HAS_AUDIO_ENC "clip.has_audio_encoder"
#define KEY_HAS_VISION_ENC "clip.has_vision_encoder"
#define KEY_HAS_GEN_AUDIO_ENC "clip.has_gen_audio_encoder"
#define KEY_USE_GELU "clip.use_gelu"
#define KEY_USE_SILU "clip.use_silu"
@@ -88,6 +89,8 @@
#define KEY_A_ATTN_WINDOW_SIZE "clip.audio.window_size" // mimo-audio-tokenizer: sliding-window radius
#define KEY_A_LOCAL_BLOCK_COUNT "clip.audio.local_block_count" // mimo-v2.5: input_local_transformer layer count
#define KEY_A_LOCAL_GROUP_SIZE "clip.audio.local_group_size" // mimo-v2.5: input_local_transformer grouping size
// audio generation (gen-audio)-specific
#define KEY_GEN_AUDIO_PROJ_TYPE "clip.gen.audio.projector_type" // for models with mixed modalities
#define KEY_AUDIO_SUBSAMPLING_FACTOR "clip.audio.subsampling_factor"
//
@@ -200,6 +203,48 @@
#define TN_MM_A_LOCAL_LN2 "mm.a.local_blk.%d.ln2.%s"
#define TN_MM_A_LOCAL_NORM "mm.a.local_norm.%s"
// qwen3tts speaker encoder (ECAPA-TDNN)
#define TN_A_SE_CONV1 "a.blk.%d.se_conv1.%s"
#define TN_A_SE_CONV2 "a.blk.%d.se_conv2.%s"
#define TN_A_CONV_RES2 "a.blk.%d.res2.%d.%s"
#define TN_A_ASP_ATTN "a.asp_attn.%s"
#define TN_A_ASP_TDNN "a.asp_tdnn.%s"
// qwen3tts code_predictor
#define TN_A_GEN_CODE_PROJ_IN "a.gen.code.proj_in.%s"
#define TN_A_GEN_CODE_EMBD "a.gen.code.embd.%s"
#define TN_A_GEN_CODE_HEAD "a.gen.code.head.%s"
#define TN_A_GEN_CODE_OUT_EMBD "a.gen.code.out_embd.%s"
#define TN_A_GEN_CODE_NORM "a.gen.code.output_norm.%s"
// qwen3tts code2wav (RVQ codes -> raw PCM)
// pre_transformer per-layer tensors are loaded through the generic TN_ATTN_*/TN_FFN_*/TN_LN_*/TN_LS_* macros with prefix "a.gen.wav.tfm"
#define TN_A_GEN_WAV_QUANT_FIRST_IN "a.gen.wav.quant.first.in_proj.%s"
#define TN_A_GEN_WAV_QUANT_FIRST_OUT "a.gen.wav.quant.first.out_proj.%s"
#define TN_A_GEN_WAV_QUANT_FIRST_CB "a.gen.wav.quant.first.codebook.%s"
#define TN_A_GEN_WAV_QUANT_REST_IN "a.gen.wav.quant.rest.in_proj.%s"
#define TN_A_GEN_WAV_QUANT_REST_OUT "a.gen.wav.quant.rest.out_proj.%s"
#define TN_A_GEN_WAV_QUANT_REST_CB "a.gen.wav.quant.rest.codebook.%s"
#define TN_A_GEN_WAV_PRE_CONV "a.gen.wav.pre_conv.%s"
#define TN_A_GEN_WAV_TFM_IN_PROJ "a.gen.wav.tfm.in_proj.%s"
#define TN_A_GEN_WAV_TFM_OUT_PROJ "a.gen.wav.tfm.out_proj.%s"
#define TN_A_GEN_WAV_TFM_OUT_NORM "a.gen.wav.tfm.output_norm.%s"
#define TN_A_GEN_WAV_UP_CONV "a.gen.wav.up.blk.%d.conv.%s"
#define TN_A_GEN_WAV_UP_DWCONV "a.gen.wav.up.blk.%d.dwconv.%s"
#define TN_A_GEN_WAV_UP_NORM "a.gen.wav.up.blk.%d.norm.%s"
#define TN_A_GEN_WAV_UP_PW1 "a.gen.wav.up.blk.%d.pw1.%s"
#define TN_A_GEN_WAV_UP_PW2 "a.gen.wav.up.blk.%d.pw2.%s"
#define TN_A_GEN_WAV_UP_GAMMA "a.gen.wav.up.blk.%d.gamma"
#define TN_A_GEN_WAV_DAC_ENTRY "a.gen.wav.dac.entry.%s"
#define TN_A_GEN_WAV_DAC_SNAKE "a.gen.wav.dac.blk.%d.snake.%s"
#define TN_A_GEN_WAV_DAC_CONV "a.gen.wav.dac.blk.%d.conv.%s"
#define TN_A_GEN_WAV_DAC_RES_ACT1 "a.gen.wav.dac.blk.%d.res.%d.act1.%s"
#define TN_A_GEN_WAV_DAC_RES_CONV1 "a.gen.wav.dac.blk.%d.res.%d.conv1.%s"
#define TN_A_GEN_WAV_DAC_RES_ACT2 "a.gen.wav.dac.blk.%d.res.%d.act2.%s"
#define TN_A_GEN_WAV_DAC_RES_CONV2 "a.gen.wav.dac.blk.%d.res.%d.conv2.%s"
#define TN_A_GEN_WAV_DAC_POST_SNAKE "a.gen.wav.dac.post_snake.%s"
#define TN_A_GEN_WAV_DAC_POST_CONV "a.gen.wav.dac.post_conv.%s"
// cogvlm
#define TN_MM_POST_FC_NORM "mm.post_fc_norm.%s"
#define TN_MM_H_TO_4H "mm.up.%s"
@@ -407,6 +452,8 @@ enum projector_type {
PROJECTOR_TYPE_MINIMAX_M3,
PROJECTOR_TYPE_GRANITE4_VISION,
PROJECTOR_TYPE_MIMO_AUDIO,
PROJECTOR_TYPE_QWEN3TTS_SPKENC,
PROJECTOR_TYPE_QWEN3TTS_GEN,
PROJECTOR_TYPE_UNKNOWN,
};
@@ -464,6 +511,8 @@ static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = {
{ PROJECTOR_TYPE_GRANITE4_VISION, "granite4_vision"},
{ PROJECTOR_TYPE_MIMO_AUDIO, "mimo_audio"},
{ PROJECTOR_TYPE_PARAKEET, "parakeet"},
{ PROJECTOR_TYPE_QWEN3TTS_SPKENC, "qwen3tts_spkenc"},
{ PROJECTOR_TYPE_QWEN3TTS_GEN, "qwen3tts_gen"},
};
static projector_type clip_projector_type_from_string(const std::string & str) {
+109
View File
@@ -134,6 +134,18 @@ struct clip_hparams {
int32_t rvq_num_quantizers = 0;
std::vector<int32_t> rvq_codebook_size; // per-quantizer bin count (ragged, e.g. 1024/1024/256/128x17)
// qwen3tts code2wav
int32_t wav_tfm_n_layer = 0;
int32_t wav_tfm_n_embd = 0;
int32_t wav_tfm_n_ff = 0;
int32_t wav_tfm_n_head = 0;
int32_t wav_tfm_n_head_kv = 0;
float wav_tfm_eps = 1e-5f;
float wav_tfm_rope_theta = 10000.0f;
int32_t wav_upsample_n_block = 0;
int32_t wav_dac_n_block = 0;
int32_t wav_dac_n_res = 0;
// mimo-v2.5: LLM-side connector (input_local_transformer)
int32_t audio_local_n_layer = 0;
int32_t audio_local_group_size = 0;
@@ -284,6 +296,15 @@ struct clip_layer {
ggml_tensor * cross_attn_norm_w = nullptr;
ggml_tensor * cross_attn_norm_b = nullptr;
// qwen3tts speaker encoder: SE-Res2Net block (conv_pw1_w/b and conv_pw2_w/b
// above are reused for this block's tdnn1/tdnn2 bottleneck convs)
ggml_tensor * se_conv1_w = nullptr;
ggml_tensor * se_conv1_b = nullptr;
ggml_tensor * se_conv2_w = nullptr;
ggml_tensor * se_conv2_b = nullptr;
std::vector<ggml_tensor *> res2_conv_w; // Res2Net hierarchical branches
std::vector<ggml_tensor *> res2_conv_b;
bool has_deepstack() const {
return deepstack_fc1_w != nullptr;
}
@@ -363,6 +384,75 @@ struct qf_block {
std::vector<clip_layer> qf_proj_layers;
};
// qwen3tts code2wav: RVQ codes -> raw PCM
struct clip_code2wav {
// one ConvNeXt block + its preceding causal ConvTranspose1d
// (the "upsample" stage between pre_transformer and the DAC decoder)
struct upsample_block {
ggml_tensor * conv_w = nullptr; // causal ConvTranspose1d, 2x
ggml_tensor * conv_b = nullptr;
ggml_tensor * dwconv_w = nullptr; // depthwise causal conv, k=7
ggml_tensor * dwconv_b = nullptr;
ggml_tensor * norm_w = nullptr; // LayerNorm
ggml_tensor * norm_b = nullptr;
ggml_tensor * pw1_w = nullptr; // pointwise expand
ggml_tensor * pw1_b = nullptr;
ggml_tensor * pw2_w = nullptr; // pointwise project
ggml_tensor * pw2_b = nullptr;
ggml_tensor * gamma = nullptr; // layer scale
};
// one DAC residual unit
// (SnakeBeta -> dilated causal conv -> SnakeBeta -> pointwise causal conv)
struct dac_res {
ggml_tensor * act1_alpha = nullptr;
ggml_tensor * act1_beta = nullptr;
ggml_tensor * conv1_w = nullptr;
ggml_tensor * conv1_b = nullptr;
ggml_tensor * act2_alpha = nullptr;
ggml_tensor * act2_beta = nullptr;
ggml_tensor * conv2_w = nullptr;
ggml_tensor * conv2_b = nullptr;
};
// one DAC upsample block (SnakeBeta -> causal ConvTranspose1d -> 3 residual units)
struct dac_block {
ggml_tensor * snake_alpha = nullptr;
ggml_tensor * snake_beta = nullptr;
ggml_tensor * conv_w = nullptr; // causal ConvTranspose1d
ggml_tensor * conv_b = nullptr;
std::vector<dac_res> res;
};
// quantizer: RVQ codebook decode
ggml_tensor * quant_first_in_w = nullptr; // semantic RVQ, in_proj (1x1 conv, loaded as 2D)
ggml_tensor * quant_first_out_w = nullptr;
ggml_tensor * quant_first_cb_w = nullptr; // codebook (1 layer)
ggml_tensor * quant_rest_in_w = nullptr; // acoustic RVQ
ggml_tensor * quant_rest_out_w = nullptr;
ggml_tensor * quant_rest_cb_w = nullptr; // codebooks, merged 3D [15, vocab, dim]
ggml_tensor * pre_conv_w = nullptr;
ggml_tensor * pre_conv_b = nullptr;
ggml_tensor * tfm_in_proj_w = nullptr;
ggml_tensor * tfm_in_proj_b = nullptr;
ggml_tensor * tfm_out_proj_w = nullptr;
ggml_tensor * tfm_out_proj_b = nullptr;
ggml_tensor * tfm_output_norm_w = nullptr;
std::vector<clip_layer> tfm_layers; // reuses the generic block fields (ln_1/attn/ln_2/ffn/ls_1/ls_2)
std::vector<upsample_block> upsample;
ggml_tensor * dac_entry_w = nullptr;
ggml_tensor * dac_entry_b = nullptr;
std::vector<dac_block> dac;
ggml_tensor * dac_post_snake_alpha = nullptr;
ggml_tensor * dac_post_snake_beta = nullptr;
ggml_tensor * dac_post_conv_w = nullptr;
ggml_tensor * dac_post_conv_b = nullptr;
};
struct clip_model {
clip_modality modality = CLIP_MODALITY_VISION;
projector_type proj_type = PROJECTOR_TYPE_MLP;
@@ -575,6 +665,25 @@ struct clip_model {
ggml_tensor * conv2d_3_w = nullptr;
ggml_tensor * conv2d_3_b = nullptr;
// qwen3tts speaker encoder (ECAPA-TDNN): the stem conv (block 0) reuses
// conv1d_1_w/b, the multi-layer feature aggregation conv reuses conv_out_w/b,
// and the final speaker embedding projection reuses mm_fc_w/b
ggml_tensor * spk_asp_attn_w = nullptr;
ggml_tensor * spk_asp_attn_b = nullptr;
ggml_tensor * spk_asp_tdnn_w = nullptr;
ggml_tensor * spk_asp_tdnn_b = nullptr;
// qwen3tts code_predictor
ggml_tensor * gen_code_proj_in_w = nullptr; // small_to_mtp_projection
ggml_tensor * gen_code_proj_in_b = nullptr;
ggml_tensor * gen_code_embd_w = nullptr; // per-codebook embedding, merged 3D
ggml_tensor * gen_code_head_w = nullptr; // per-codebook output head, merged 3D
ggml_tensor * gen_code_out_embd_w = nullptr; // codebook-0 embedding, fed back into the talker
ggml_tensor * gen_code_norm_w = nullptr; // final norm
// qwen3tts code2wav: RVQ codes -> raw PCM
clip_code2wav c2w;
// cogvlm
ggml_tensor * mm_post_fc_norm_w = nullptr;
ggml_tensor * mm_post_fc_norm_b = nullptr;
+333 -16
View File
@@ -17,6 +17,7 @@
#include <cstring>
#include <fstream>
#include <map>
#include <random>
#include <stdexcept>
#include <unordered_set>
#include <vector>
@@ -269,6 +270,29 @@ clip_graph::clip_graph(clip_ctx * ctx, const clip_image_f32 & img) :
gf = ggml_new_graph_custom(ctx0, ctx->max_nodes, false);
}
clip_graph::clip_graph(const clip_graph & parent) :
model(parent.model),
hparams(parent.hparams),
proj_type(parent.proj_type),
img(parent.img),
patch_size(parent.patch_size),
n_patches_x(parent.n_patches_x),
n_patches_y(parent.n_patches_y),
n_patches(parent.n_patches),
n_embd(parent.n_embd),
n_head(parent.n_head),
n_head_kv(parent.n_head_kv),
d_head(parent.d_head),
n_layer(parent.n_layer),
n_mmproj_embd(parent.n_mmproj_embd),
eps(parent.eps),
kq_scale(parent.kq_scale),
flash_attn_type(parent.flash_attn_type) {
// reuse from parent
ctx0 = parent.ctx0;
gf = parent.gf;
}
ggml_tensor * clip_graph::build_mm(ggml_tensor * w, ggml_tensor * x) const {
return ggml_mul_mat(ctx0, w, x);
}
@@ -873,7 +897,8 @@ ggml_tensor * clip_graph::build_patch_merge_permute(ggml_tensor * cur, int scale
return cur;
}
static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const clip_image_f32_batch & imgs) {
static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const clip_image_f32_batch & imgs,
const clip_encode_params * params = nullptr) {
const clip_image_f32 & img = imgs.entries[0];
std::unique_ptr<clip_graph> builder;
@@ -1025,6 +1050,16 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const
{
builder = std::make_unique<clip_graph_mimo_audio>(ctx, img);
} break;
case PROJECTOR_TYPE_QWEN3TTS_SPKENC:
{
builder = std::make_unique<clip_graph_qwen3tts_spkenc>(ctx, img);
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
{
const int top_k = params ? params->top_k : 50;
const float top_p = params ? params->top_p : 1.0f;
builder = std::make_unique<clip_graph_qwen3tts_gen>(ctx, img, top_k, top_p);
} break;
case PROJECTOR_TYPE_YOUTUVL:
{
builder = std::make_unique<clip_graph_youtuvl>(ctx, img);
@@ -1065,8 +1100,9 @@ struct clip_model_loader {
size_t model_size = 0; // in bytes
bool has_vision = false;
bool has_audio = false;
bool has_vision = false;
bool has_audio = false;
bool has_gen_audio = false;
mtmd_progress_callback progress_callback = nullptr;
void * progress_callback_user_data = nullptr;
@@ -1112,8 +1148,9 @@ struct clip_model_loader {
// modalities
{
get_bool(KEY_HAS_VISION_ENC, has_vision, false);
get_bool(KEY_HAS_AUDIO_ENC, has_audio, false);
get_bool(KEY_HAS_VISION_ENC, has_vision, false);
get_bool(KEY_HAS_AUDIO_ENC, has_audio, false);
get_bool(KEY_HAS_GEN_AUDIO_ENC, has_gen_audio, false);
if (has_vision) {
LOG_INF("%s: has vision encoder\n", __func__);
@@ -1121,6 +1158,9 @@ struct clip_model_loader {
if (has_audio) {
LOG_INF("%s: has audio encoder\n", __func__);
}
if (has_gen_audio) {
LOG_INF("%s: has audio generation (gen) encoder\n", __func__);
}
}
// tensors
@@ -1147,6 +1187,8 @@ struct clip_model_loader {
GGML_ASSERT(has_vision);
} else if (modality == CLIP_MODALITY_AUDIO) {
GGML_ASSERT(has_audio);
} else if (modality == CLIP_MODALITY_GEN_AUDIO) {
GGML_ASSERT(has_gen_audio);
}
model.modality = modality;
@@ -1163,6 +1205,8 @@ struct clip_model_loader {
get_string(KEY_VISION_PROJ_TYPE, proj_type, false);
} else if (modality == CLIP_MODALITY_AUDIO) {
get_string(KEY_AUDIO_PROJ_TYPE, proj_type, false);
} else if (modality == CLIP_MODALITY_GEN_AUDIO) {
get_string(KEY_GEN_AUDIO_PROJ_TYPE, proj_type, false);
} else {
GGML_ABORT("unknown modality");
}
@@ -1182,12 +1226,13 @@ struct clip_model_loader {
}
}
const bool is_vision = model.modality == CLIP_MODALITY_VISION;
const bool is_audio = model.modality == CLIP_MODALITY_AUDIO;
const bool is_vision = model.modality == CLIP_MODALITY_VISION;
const bool is_audio = model.modality == CLIP_MODALITY_AUDIO;
const bool is_gen_audio = model.modality == CLIP_MODALITY_GEN_AUDIO;
// other hparams
{
const char * prefix = is_vision ? "vision" : "audio";
const char * prefix = is_vision ? "vision" : (is_audio ? "audio" : "gen.audio");
get_u32(string_format(KEY_N_EMBD, prefix), hparams.n_embd);
get_u32(string_format(KEY_N_HEAD, prefix), hparams.n_head);
get_u32(string_format(KEY_N_FF, prefix), hparams.n_ff);
@@ -1197,6 +1242,7 @@ struct clip_model_loader {
// n_head_kv is optional (for GQA), default to n_head
hparams.n_head_kv = hparams.n_head;
get_u32(string_format(KEY_N_HEAD_KV, prefix), hparams.n_head_kv, false);
if (is_vision) {
get_u32(KEY_IMAGE_SIZE, hparams.image_size);
@@ -1225,6 +1271,11 @@ struct clip_model_loader {
hparams.image_size = 0;
hparams.patch_size = 1;
} else if (is_gen_audio) {
// these are unused, but still need to be set to avoid issues
hparams.image_size = 0;
hparams.patch_size = 1;
} else {
GGML_ASSERT(false && "unknown modality");
}
@@ -1645,6 +1696,33 @@ struct clip_model_loader {
"%s: mimo_audio: %s must be > 0\n", __func__, KEY_A_LOCAL_GROUP_SIZE));
}
} break;
case PROJECTOR_TYPE_QWEN3TTS_SPKENC:
{
// ECAPA-TDNN speaker/voice encoder; mel_spectrogram() front-end
// matches the Slaney mel default (fmin=0, fmax=sample_rate/2)
hparams.audio_sample_rate = 24000;
hparams.audio_n_fft = 1024;
hparams.audio_window_len = 1024;
hparams.audio_hop_len = 256;
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
{
// discrete-token autoregressive predictor, no mel-frontend needed
// TODO: hardcoded for now, read from code_predictor_config instead
hparams.rope_theta = 1000000.0f;
// code2wav params
hparams.wav_tfm_n_layer = 8;
hparams.wav_tfm_n_embd = 512;
hparams.wav_tfm_n_ff = 1024;
hparams.wav_tfm_n_head = 16;
hparams.wav_tfm_n_head_kv = 16;
hparams.wav_tfm_eps = 1e-5f;
hparams.wav_tfm_rope_theta = 10000.0f;
hparams.wav_upsample_n_block = 2;
hparams.wav_dac_n_block = 4;
hparams.wav_dac_n_res = 3;
} break;
case PROJECTOR_TYPE_PADDLEOCR:
{
hparams.n_merge = 2;
@@ -1869,7 +1947,9 @@ struct clip_model_loader {
}
// TODO @ngxson : support both audio and video in the future
const char * prefix = model.modality == CLIP_MODALITY_AUDIO ? "a" : "v";
const char * prefix = model.modality == CLIP_MODALITY_AUDIO ? "a"
: model.modality == CLIP_MODALITY_GEN_AUDIO ? "a.gen.code"
: "v";
// get offsets
for (int64_t i = 0; i < gguf_get_n_tensors(ctx_gguf.get()); ++i) {
@@ -1971,7 +2051,8 @@ struct clip_model_loader {
model.position_embeddings = get_tensor(string_format(TN_POS_EMBD, prefix), false);
const bool has_standard_layers = (
model.proj_type != PROJECTOR_TYPE_GEMMA3NV);
model.proj_type != PROJECTOR_TYPE_GEMMA3NV &&
model.proj_type != PROJECTOR_TYPE_QWEN3TTS_SPKENC);
// layers
const int n_layers_to_load = has_standard_layers ? hparams.n_layer : 0;
@@ -2592,6 +2673,146 @@ struct clip_model_loader {
model.mm_1_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 1, "weight"));
model.mm_2_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 2, "weight"));
} break;
case PROJECTOR_TYPE_QWEN3TTS_SPKENC:
{
// stem TDNN (block 0)
model.conv1d_1_w = get_tensor(string_format(TN_CONV1D, 0, "weight"));
model.conv1d_1_b = get_tensor(string_format(TN_CONV1D, 0, "bias"));
// SE-Res2Net blocks (GGUF bid 1..3, one per hparams.n_layer)
model.layers.resize(hparams.n_layer);
for (int il = 0; il < hparams.n_layer; il++) {
auto & layer = model.layers[il];
int bid = il + 1;
layer.conv_pw1_w = get_tensor(string_format(TN_CONV_PW1, prefix, bid, "weight"));
layer.conv_pw1_b = get_tensor(string_format(TN_CONV_PW1, prefix, bid, "bias"));
layer.conv_pw2_w = get_tensor(string_format(TN_CONV_PW2, prefix, bid, "weight"));
layer.conv_pw2_b = get_tensor(string_format(TN_CONV_PW2, prefix, bid, "bias"));
layer.se_conv1_w = get_tensor(string_format(TN_A_SE_CONV1, bid, "weight"));
layer.se_conv1_b = get_tensor(string_format(TN_A_SE_CONV1, bid, "bias"));
layer.se_conv2_w = get_tensor(string_format(TN_A_SE_CONV2, bid, "weight"));
layer.se_conv2_b = get_tensor(string_format(TN_A_SE_CONV2, bid, "bias"));
layer.res2_conv_w.resize(7);
layer.res2_conv_b.resize(7);
for (int xid = 0; xid < 7; xid++) {
layer.res2_conv_w[xid] = get_tensor(string_format(TN_A_CONV_RES2, bid, xid, "weight"));
layer.res2_conv_b[xid] = get_tensor(string_format(TN_A_CONV_RES2, bid, xid, "bias"));
}
}
// multi-layer feature aggregation
model.conv_out_w = get_tensor(string_format(TN_CONV_OUT, "weight"));
model.conv_out_b = get_tensor(string_format(TN_CONV_OUT, "bias"));
// attentive statistics pooling
model.spk_asp_attn_w = get_tensor(string_format(TN_A_ASP_ATTN, "weight"));
model.spk_asp_attn_b = get_tensor(string_format(TN_A_ASP_ATTN, "bias"));
model.spk_asp_tdnn_w = get_tensor(string_format(TN_A_ASP_TDNN, "weight"));
model.spk_asp_tdnn_b = get_tensor(string_format(TN_A_ASP_TDNN, "bias"));
// final speaker embedding projection
model.mm_fc_w = get_tensor(string_format(TN_MM_AUDIO_FC, "weight"));
model.mm_fc_b = get_tensor(string_format(TN_MM_AUDIO_FC, "bias"));
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
{
// code_predictor
model.gen_code_proj_in_w = get_tensor(string_format(TN_A_GEN_CODE_PROJ_IN, "weight"));
model.gen_code_proj_in_b = get_tensor(string_format(TN_A_GEN_CODE_PROJ_IN, "bias"));
model.gen_code_embd_w = get_tensor(string_format(TN_A_GEN_CODE_EMBD, "weight"));
model.gen_code_head_w = get_tensor(string_format(TN_A_GEN_CODE_HEAD, "weight"));
model.gen_code_out_embd_w = get_tensor(string_format(TN_A_GEN_CODE_OUT_EMBD, "weight"));
model.gen_code_norm_w = get_tensor(string_format(TN_A_GEN_CODE_NORM, "weight"));
// code2wav: RVQ codes -> raw PCM, lives in the same ctx as code_predictor
{
auto & c2w = model.c2w;
c2w.quant_first_in_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_FIRST_IN, "weight"));
c2w.quant_first_out_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_FIRST_OUT, "weight"));
c2w.quant_first_cb_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_FIRST_CB, "weight"));
c2w.quant_rest_in_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_REST_IN, "weight"));
c2w.quant_rest_out_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_REST_OUT, "weight"));
c2w.quant_rest_cb_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_REST_CB, "weight"));
c2w.pre_conv_w = get_tensor(string_format(TN_A_GEN_WAV_PRE_CONV, "weight"));
c2w.pre_conv_b = get_tensor(string_format(TN_A_GEN_WAV_PRE_CONV, "bias"));
c2w.tfm_in_proj_w = get_tensor(string_format(TN_A_GEN_WAV_TFM_IN_PROJ, "weight"));
c2w.tfm_in_proj_b = get_tensor(string_format(TN_A_GEN_WAV_TFM_IN_PROJ, "bias"));
c2w.tfm_out_proj_w = get_tensor(string_format(TN_A_GEN_WAV_TFM_OUT_PROJ, "weight"));
c2w.tfm_out_proj_b = get_tensor(string_format(TN_A_GEN_WAV_TFM_OUT_PROJ, "bias"));
c2w.tfm_output_norm_w = get_tensor(string_format(TN_A_GEN_WAV_TFM_OUT_NORM, "weight"));
// pre_transformer layers: own prefix/layer-count, so loaded manually
// rather than through the generic model.layers loop (already claimed
// by code_predictor's 5 layers)
c2w.tfm_layers.resize(hparams.wav_tfm_n_layer);
for (int il = 0; il < hparams.wav_tfm_n_layer; il++) {
auto & layer = c2w.tfm_layers[il];
const char * p = "a.gen.wav.tfm";
layer.q_w = get_tensor(string_format(TN_ATTN_Q, p, il, "weight"));
layer.k_w = get_tensor(string_format(TN_ATTN_K, p, il, "weight"));
layer.v_w = get_tensor(string_format(TN_ATTN_V, p, il, "weight"));
layer.o_w = get_tensor(string_format(TN_ATTN_OUTPUT, p, il, "weight"));
layer.ln_1_w = get_tensor(string_format(TN_LN_1, p, il, "weight"));
layer.ln_2_w = get_tensor(string_format(TN_LN_2, p, il, "weight"));
layer.ls_1_w = get_tensor(string_format(TN_LS_1, p, il, "weight"));
layer.ls_2_w = get_tensor(string_format(TN_LS_2, p, il, "weight"));
layer.ff_gate_w = get_tensor(string_format(TN_FFN_GATE, p, il, "weight"));
layer.ff_up_w = get_tensor(string_format(TN_FFN_UP, p, il, "weight"));
layer.ff_down_w = get_tensor(string_format(TN_FFN_DOWN, p, il, "weight"));
}
// upsample: 2x (causal ConvTranspose1d + ConvNeXt block)
c2w.upsample.resize(hparams.wav_upsample_n_block);
for (int il = 0; il < hparams.wav_upsample_n_block; il++) {
auto & up = c2w.upsample[il];
up.conv_w = get_tensor(string_format(TN_A_GEN_WAV_UP_CONV, il, "weight"));
up.conv_b = get_tensor(string_format(TN_A_GEN_WAV_UP_CONV, il, "bias"));
up.dwconv_w = get_tensor(string_format(TN_A_GEN_WAV_UP_DWCONV, il, "weight"));
up.dwconv_b = get_tensor(string_format(TN_A_GEN_WAV_UP_DWCONV, il, "bias"));
up.norm_w = get_tensor(string_format(TN_A_GEN_WAV_UP_NORM, il, "weight"));
up.norm_b = get_tensor(string_format(TN_A_GEN_WAV_UP_NORM, il, "bias"));
up.pw1_w = get_tensor(string_format(TN_A_GEN_WAV_UP_PW1, il, "weight"));
up.pw1_b = get_tensor(string_format(TN_A_GEN_WAV_UP_PW1, il, "bias"));
up.pw2_w = get_tensor(string_format(TN_A_GEN_WAV_UP_PW2, il, "weight"));
up.pw2_b = get_tensor(string_format(TN_A_GEN_WAV_UP_PW2, il, "bias"));
up.gamma = get_tensor(string_format(TN_A_GEN_WAV_UP_GAMMA, il));
}
// DAC decoder: conv_pre + n upsample blocks (each with n_res residual units) + conv_post
c2w.dac_entry_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_ENTRY, "weight"));
c2w.dac_entry_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_ENTRY, "bias"));
c2w.dac.resize(hparams.wav_dac_n_block);
for (int il = 0; il < hparams.wav_dac_n_block; il++) {
auto & blk = c2w.dac[il];
blk.snake_alpha = get_tensor(string_format(TN_A_GEN_WAV_DAC_SNAKE, il, "alpha"));
blk.snake_beta = get_tensor(string_format(TN_A_GEN_WAV_DAC_SNAKE, il, "beta"));
blk.conv_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_CONV, il, "weight"));
blk.conv_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_CONV, il, "bias"));
blk.res.resize(hparams.wav_dac_n_res);
for (int ir = 0; ir < hparams.wav_dac_n_res; ir++) {
auto & res = blk.res[ir];
res.act1_alpha = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_ACT1, il, ir, "alpha"));
res.act1_beta = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_ACT1, il, ir, "beta"));
res.conv1_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_CONV1, il, ir, "weight"));
res.conv1_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_CONV1, il, ir, "bias"));
res.act2_alpha = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_ACT2, il, ir, "alpha"));
res.act2_beta = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_ACT2, il, ir, "beta"));
res.conv2_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_CONV2, il, ir, "weight"));
res.conv2_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_CONV2, il, ir, "bias"));
}
}
c2w.dac_post_snake_alpha = get_tensor(string_format(TN_A_GEN_WAV_DAC_POST_SNAKE, "alpha"));
c2w.dac_post_snake_beta = get_tensor(string_format(TN_A_GEN_WAV_DAC_POST_SNAKE, "beta"));
c2w.dac_post_conv_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_POST_CONV, "weight"));
c2w.dac_post_conv_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_POST_CONV, "bias"));
}
} break;
case PROJECTOR_TYPE_VOXTRAL:
{
model.conv1d_1_w = get_tensor(string_format(TN_CONV1D, 1, "weight"));
@@ -3420,6 +3641,7 @@ struct clip_model_loader {
struct clip_init_result clip_init(const char * fname, struct clip_context_params ctx_params) {
clip_ctx * ctx_vision = nullptr;
clip_ctx * ctx_audio = nullptr;
clip_ctx * ctx_gen_audio = nullptr;
try {
clip_model_loader loader(fname,
@@ -3452,16 +3674,25 @@ struct clip_init_result clip_init(const char * fname, struct clip_context_params
}
}
if (loader.has_gen_audio) {
ctx_gen_audio = new clip_ctx(ctx_params);
loader.load_hparams(ctx_gen_audio->model, CLIP_MODALITY_GEN_AUDIO);
loader.load_tensors(*ctx_gen_audio);
// TODO: fix warmup
ctx_gen_audio->buf_compute_meta.resize(ctx_gen_audio->max_nodes * ggml_tensor_overhead() + ggml_graph_overhead());
}
} catch (const std::exception & e) {
LOG_ERR("%s: failed to load model '%s': %s\n", __func__, fname, e.what());
delete ctx_vision;
delete ctx_audio;
delete ctx_gen_audio;
return {nullptr, nullptr};
return {nullptr, nullptr, nullptr};
}
return {ctx_vision, ctx_audio};
return {ctx_vision, ctx_audio, ctx_gen_audio};
}
struct clip_cap clip_get_cap(const char * fname) {
@@ -3778,6 +4009,17 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) {
const int ds = ctx->model.hparams.audio_proj_downsample_rate;
n_patches = ((img->nx() + ws - 1) / ws) * (ws / ds);
} break;
case PROJECTOR_TYPE_QWEN3TTS_SPKENC:
{
// attentive statistics pooling collapses the whole clip into
// a single speaker embedding vector, regardless of its length
n_patches = 1;
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
{
// one hidden-state vector fed back to the talker per call
n_patches = 1;
} break;
case PROJECTOR_TYPE_GRANITE4_VISION:
{
// Per-tile output token count: each projector block outputs
@@ -3811,7 +4053,16 @@ bool clip_image_encode(struct clip_ctx * ctx, int n_threads, const clip_image_f3
}
bool clip_image_batch_encode(clip_ctx * ctx, int n_threads, const clip_image_f32_batch * imgs_c_ptr, std::vector<float> & out_batch_embd) {
const clip_image_f32_batch & imgs = *imgs_c_ptr;
clip_encode_params params;
params.imgs = imgs_c_ptr;
params.n_threads = n_threads;
params.out_embd = &out_batch_embd;
return clip_encode(ctx, &params);
}
bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
const clip_image_f32_batch & imgs = *params->imgs;
int n_batch_cur = imgs.entries.size();
// [QWEN_VIDEO] for video models, the batch dimension is used as temporal dimension for merged frames
@@ -3822,12 +4073,12 @@ bool clip_image_batch_encode(clip_ctx * ctx, int n_threads, const clip_image_f32
// if buffers are not allocated, we need to do a warmup run to allocate them
if (!ctx->is_allocated) {
clip_model_loader::warmup(*ctx, *imgs_c_ptr);
clip_model_loader::warmup(*ctx, *params->imgs);
}
// build the inference graph
ggml_backend_sched_reset(ctx->sched.get());
ggml_cgraph * gf = clip_get_graph_builder(ctx, imgs)->build();
ggml_cgraph * gf = clip_get_graph_builder(ctx, imgs, params)->build();
ggml_backend_sched_alloc_graph(ctx->sched.get(), gf);
// set inputs
@@ -4465,9 +4716,48 @@ bool clip_image_batch_encode(clip_ctx * ctx, int n_threads, const clip_image_f32
case PROJECTOR_TYPE_COGVLM:
case PROJECTOR_TYPE_YASA2:
case PROJECTOR_TYPE_GEMMA4UA:
case PROJECTOR_TYPE_QWEN3TTS_SPKENC:
{
// do nothing
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
{
std::vector<int32_t> code0 = { params->code0 };
set_input_i32("inp_code0", code0);
// one uniform(0,1) draw per codebook, consumed by do_sampling()'s
// inverse-CDF token selection (inp_rand_0 .. inp_rand_{n_acoustic-1})
static std::mt19937 rng{ std::random_device{}() };
std::uniform_real_distribution<float> dist(0.0f, 1.0f);
const int64_t n_acoustic = model.gen_code_head_w->ne[2];
for (int64_t g = 0; g < n_acoustic; g++) {
std::vector<float> r = { dist(rng) };
set_input_f32(("inp_rand_" + std::to_string(g)).c_str(), r);
}
// code2wav left-context window: repack the caller's frame
// major history (oldest first) into the group major layout
// of inp_ctx_codes, front-padded with code 0
{
ggml_tensor * t = get_inp_tensor("inp_ctx_codes");
const int64_t T_ctx = t->ne[0];
const int64_t n_codes = t->ne[1];
std::vector<int32_t> buf((size_t) (T_ctx * n_codes), 0);
if (params->ctx_codes) {
const auto & hist = *params->ctx_codes;
const int64_t n_frames = (int64_t) hist.size() / n_codes;
const int64_t n_use = std::min(n_frames, T_ctx);
const int64_t dst0 = T_ctx - n_use; // front padding
const int64_t src0 = n_frames - n_use; // newest frames
for (int64_t f = 0; f < n_use; f++) {
for (int64_t g = 0; g < n_codes; g++) {
buf[(size_t) (g * T_ctx + dst0 + f)] = hist[(size_t) ((src0 + f) * n_codes + g)];
}
}
}
set_input_i32("inp_ctx_codes", buf);
}
} break;
case PROJECTOR_TYPE_HUNYUANVL:
{
// Compute the HunyuanVL 2D position embedding on CPU (with the
@@ -4873,7 +5163,7 @@ bool clip_image_batch_encode(clip_ctx * ctx, int n_threads, const clip_image_f32
if (reg) {
auto ggml_backend_set_n_threads_fn = (ggml_backend_set_n_threads_t) ggml_backend_reg_get_proc_address(reg, "ggml_backend_set_n_threads");
if (ggml_backend_set_n_threads_fn) {
ggml_backend_set_n_threads_fn(ctx->backend_cpu, n_threads);
ggml_backend_set_n_threads_fn(ctx->backend_cpu, params->n_threads);
}
}
@@ -4899,6 +5189,7 @@ bool clip_image_batch_encode(clip_ctx * ctx, int n_threads, const clip_image_f32
// copy output to user buffer if provided
// if output is empty, skip the copy
auto & out_batch_embd = *params->out_embd;
if (!out_batch_embd.empty()) {
if (out_batch_embd.size() != (size_t)ggml_nelements(embeddings)) {
LOG_ERR("%s: output buffer has %zu elements but expected %zu\n", __func__, out_batch_embd.size(), (size_t)ggml_nelements(embeddings));
@@ -4909,6 +5200,28 @@ bool clip_image_batch_encode(clip_ctx * ctx, int n_threads, const clip_image_f32
LOG_WRN("%s: output buffer is empty, skipping copy\n", __func__);
}
// for audio gen: also copy out the decoded PCM samples
// auto-sized to whatever the graph produced (fixed per model, but not known up-front)
if (params->out_codes != nullptr) {
ggml_tensor * codes = ggml_graph_get_tensor(gf, "out_codes");
if (codes == nullptr) {
GGML_ABORT("out_codes requested but graph has no \"out_codes\" tensor");
}
auto & out_codes = *params->out_codes;
out_codes.resize(ggml_nelements(codes));
ggml_backend_tensor_get(codes, out_codes.data(), 0, ggml_nbytes(codes));
}
if (params->out_audio != nullptr) {
ggml_tensor * audio = ggml_graph_get_tensor(gf, "out_audio");
if (audio == nullptr) {
GGML_ABORT("out_audio requested but graph has no \"out_audio\" tensor");
}
auto & out_audio = *params->out_audio;
out_audio.resize(ggml_nelements(audio));
ggml_backend_tensor_get(audio, out_audio.data(), 0, ggml_nbytes(audio));
}
// Debug: dump final embeddings if MTMD_DEBUG_EMBEDDINGS is set
if (ctx->debug_output_embeddings) {
const int64_t n_embd = embeddings->ne[0];
@@ -5037,6 +5350,10 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {
return ctx->model.mm_ffn_down_w->ne[1];
case PROJECTOR_TYPE_MIMO_AUDIO:
return ctx->model.mm_2_w->ne[1];
case PROJECTOR_TYPE_QWEN3TTS_SPKENC:
return ctx->model.mm_fc_w->ne[2];
case PROJECTOR_TYPE_QWEN3TTS_GEN:
return ctx->model.gen_code_out_embd_w->ne[0];
case PROJECTOR_TYPE_PARAKEET:
return ctx->model.mm_1_w->ne[1];
default:
+25
View File
@@ -37,6 +37,7 @@ struct clip_image_f32_batch;
enum clip_modality {
CLIP_MODALITY_VISION,
CLIP_MODALITY_AUDIO,
CLIP_MODALITY_GEN_AUDIO,
};
enum clip_flash_attn_type {
@@ -61,6 +62,7 @@ struct clip_context_params {
struct clip_init_result {
struct clip_ctx * ctx_v; // vision context
struct clip_ctx * ctx_a; // audio context
struct clip_ctx * ctx_gen_a; // audio generation context
};
struct clip_init_result clip_init(const char * fname, struct clip_context_params ctx_params);
@@ -84,6 +86,29 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx);
bool clip_image_encode (struct clip_ctx * ctx, int n_threads, const clip_image_f32 * img, std::vector<float> & out_vec);
bool clip_image_batch_encode(struct clip_ctx * ctx, int n_threads, const struct clip_image_f32_batch * imgs, std::vector<float> & out_batch_embd);
struct clip_encode_params {
int n_threads = 1;
const clip_image_f32_batch * imgs = nullptr;
std::vector<float> * out_embd = nullptr;
// note: for audio gen, imgs has expectly one entry of size (n_text_embd, 1), it's the hidden state from backbone
// code0 is the sampled semantic code from backbone
// out_embd holds the embd to be fed back to backbone
// out_audio holds the generated audio samples (PCM float32)
int32_t code0 = 0;
int32_t top_k = 50;
float top_p = 1.0f;
std::vector<float> * out_audio = nullptr;
// past codes feeding the code2wav left-context window: flattened
// frames * 16, frame major, oldest first. May hold fewer frames than
// the window (the front is padded with code 0); only the newest
// frames are used. out_codes receives this frame's 16 codes so the
// caller can extend its history after each call.
const std::vector<int32_t> * ctx_codes = nullptr;
std::vector<int32_t> * out_codes = nullptr;
};
bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params);
bool clip_is_llava(const struct clip_ctx * ctx);
// note for contributor: this clip_is_(model) pattern is deprecated
// do NOT add new functions like this
+85
View File
@@ -215,6 +215,91 @@ struct clip_graph_mimo_audio : clip_graph {
ggml_cgraph * build() override;
};
struct clip_graph_qwen3tts_spkenc : clip_graph {
clip_graph_qwen3tts_spkenc(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
ggml_cgraph * build() override;
ggml_tensor * conv1d_same(ggml_tensor * x, ggml_tensor * w, ggml_tensor * b, int dilation) const;
ggml_tensor * res2net(ggml_tensor * x, const clip_layer & layer, int dilation, int scale) const;
ggml_tensor * se_block(ggml_tensor * x, const clip_layer & layer) const;
ggml_tensor * se_res2net_block(ggml_tensor * x, const clip_layer & layer, int dilation, int scale) const;
ggml_tensor * attentive_stats_pool(ggml_tensor * x) const;
};
struct clip_graph_qwen3tts_gen : clip_graph {
clip_graph_qwen3tts_gen(clip_ctx * ctx, const clip_image_f32 & img, int top_k, float top_p)
: clip_graph(ctx, img), top_k(top_k), top_p(top_p) {}
ggml_cgraph * build() override;
// sampling params, fixed at graph-build time
int top_k;
float top_p;
ggml_tensor * cache_set(ggml_tensor * cache, int row_idx, ggml_tensor * value) const;
ggml_tensor * do_sampling(ggml_tensor * logits, ggml_tensor * inp_rand, int top_k, float top_p) const;
ggml_tensor * const_i32(ggml_tensor * anchor, float value) const;
ggml_tensor * causal_mask_row(int64_t n_kv_pad, int pos) const;
ggml_tensor * project_in(ggml_tensor * cur) const;
ggml_tensor * layer_forward(
ggml_tensor * cur,
const clip_layer & layer,
ggml_tensor * inp_pos,
ggml_tensor * kq_mask,
ggml_tensor *& k_cache_layer,
ggml_tensor *& v_cache_layer,
int64_t n_kv_pad,
int pos,
int il) const;
void prefill(
std::vector<ggml_tensor *> & k_cache,
std::vector<ggml_tensor *> & v_cache,
ggml_tensor *& out_code_cache,
ggml_tensor * h_state,
ggml_tensor * code0_embd,
ggml_tensor * inp_rand,
int top_k,
float top_p) const;
ggml_tensor * step(
std::vector<ggml_tensor *> & k_cache,
std::vector<ggml_tensor *> & v_cache,
ggml_tensor * out_code_cache,
ggml_tensor * inp_rand,
int step_idx,
int top_k,
float top_p) const;
//
// code2wav: RVQ codes -> raw PCM (quantizer + pre_conv + pre_transformer + upsample + DAC).
// Stateless left-context window: every frame re-decodes the last
// C2W_CTX_FRAMES frames of codes in front of the current one and only the
// newest hop of samples is emitted, so the conv stack and the transformer
// see real history instead of zero padding.
//
struct code2wav : clip_graph {
code2wav(const clip_graph & parent) : clip_graph(parent) {}
ggml_cgraph * build() override { GGML_ABORT("call decode() instead"); }
ggml_tensor * causal_conv1d(ggml_tensor * x, ggml_tensor * w, ggml_tensor * b, int dilation) const;
ggml_tensor * causal_conv1d_dw(ggml_tensor * x, ggml_tensor * w, ggml_tensor * b) const;
ggml_tensor * causal_conv_transpose1d(ggml_tensor * x, ggml_tensor * w, ggml_tensor * b, int stride) const;
ggml_tensor * snake(ggml_tensor * x, ggml_tensor * alpha, ggml_tensor * beta) const;
ggml_tensor * quant_decode(ggml_tensor * out_code_cache, ggml_tensor * ctx_codes) const;
ggml_tensor * tfm_layer_forward(ggml_tensor * cur, const clip_layer & layer, ggml_tensor * pos, ggml_tensor * mask) const;
ggml_tensor * convnext_block(ggml_tensor * x, const clip_code2wav::upsample_block & blk) const;
ggml_tensor * dac_res_unit(ggml_tensor * x, const clip_code2wav::dac_res & res, int dilation) const;
// out_code_cache: [1, n_codes] I32 (as produced by prefill()/step()).
// ctx_codes: [C2W_CTX_FRAMES, n_codes] I32, oldest frame first.
// returns this frame's audio samples, [hop] F32, clamped to [-1, 1].
ggml_tensor * decode(ggml_tensor * out_code_cache, ggml_tensor * ctx_codes) const;
};
};
struct clip_graph_kimik25 : clip_graph {
clip_graph_kimik25(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
ggml_cgraph * build() override;
+643
View File
@@ -0,0 +1,643 @@
#include "models.h"
#include <string>
// on-device sampling: top-k, top-p, then a random draw
ggml_tensor * clip_graph_qwen3tts_gen::do_sampling(ggml_tensor * logits, ggml_tensor * inp_rand, int top_k, float top_p) const {
logits = ggml_reshape_1d(ctx0, logits, ggml_nelements(logits));
const int64_t n_vocab = logits->ne[0];
// sort a's rows by idx
auto sort_by = [this](ggml_tensor * a, ggml_tensor * idx) {
ggml_tensor * a2d = ggml_reshape_2d(ctx0, a, 1, a->ne[0]);
return ggml_reshape_1d(ctx0, ggml_get_rows(ctx0, a2d, idx), idx->ne[0]);
};
ggml_tensor * cur = logits;
ggml_tensor * candidates = nullptr; // maps row index back to vocab id
if (top_k > 0 && top_k < n_vocab) {
ggml_tensor * idx = ggml_top_k(ctx0, cur, top_k);
candidates = idx;
cur = sort_by(cur, idx);
cb(cur, "sample_top_k_logits", -1);
}
if (top_p < 1.0f) {
ggml_tensor * sorted_idx = ggml_argsort(ctx0, cur, GGML_SORT_ORDER_DESC);
ggml_tensor * sorted_logits = sort_by(cur, sorted_idx);
candidates = candidates ? sort_by(candidates, sorted_idx) : sorted_idx;
ggml_tensor * probs = ggml_soft_max(ctx0, sorted_logits);
ggml_tensor * cdf = ggml_cumsum(ctx0, probs);
// keep_mask[i] = 1 once cdf[i] crosses top_p
ggml_tensor * cdf_scaled = ggml_scale_bias(ctx0, cdf, -1.0f, top_p);
ggml_tensor * keep_mask = ggml_step(ctx0, cdf_scaled);
ggml_tensor * idxf = ggml_sum(ctx0, keep_mask);
idxf = ggml_clamp(ctx0, idxf, 0.0f, (float) keep_mask->ne[0] - 1);
ggml_tensor * ones = ggml_scale_bias(ctx0, idxf, 0.0f, 1.0f);
// top-p must include the crossing element, so force it to 1
ggml_tensor * keep_mask_2d = ggml_reshape_2d(ctx0, keep_mask, 1, keep_mask->ne[0]);
keep_mask_2d = ggml_set_rows(ctx0, keep_mask_2d, ones, ggml_cast(ctx0, idxf, GGML_TYPE_I32));
keep_mask = ggml_reshape_1d(ctx0, keep_mask_2d, keep_mask->ne[0]);
// log(1) = 0 (keep), log(0) = -inf (drop)
ggml_tensor * bias = ggml_log(ctx0, keep_mask);
cur = ggml_add(ctx0, sorted_logits, bias);
cb(cur, "sample_top_p_logits", -1);
}
// draw one token: find where the cdf crosses inp_rand
ggml_tensor * probs = ggml_soft_max(ctx0, cur);
ggml_tensor * cumsum = ggml_cumsum(ctx0, probs);
ggml_tensor * diff = ggml_sub(ctx0, cumsum, inp_rand);
ggml_tensor * cross_mask = ggml_step(ctx0, diff);
ggml_tensor * idxf = ggml_sum(ctx0, cross_mask);
ggml_tensor * idx = ggml_cast(ctx0, ggml_scale_bias(ctx0, idxf, -1.0f, (float) cross_mask->ne[0]), GGML_TYPE_I32);
if (candidates) {
ggml_tensor * cand_2d = ggml_reshape_2d(ctx0, candidates, 1, candidates->ne[0]);
idx = ggml_get_rows(ctx0, cand_2d, idx);
}
cb(idx, "sample_token_id", -1);
return idx;
}
// returns a new cache with row row_idx set to value
ggml_tensor * clip_graph_qwen3tts_gen::cache_set(ggml_tensor * cache, int row_idx, ggml_tensor * value) const {
const int64_t n_embd = cache->ne[0];
const int64_t n_cache = cache->ne[1];
GGML_ASSERT(row_idx >= 0 && row_idx < n_cache);
// append value as the last row, then gather it back into place
ggml_tensor * value_2d = ggml_reshape_2d(ctx0, value, n_embd, 1);
ggml_tensor * cache_ext = ggml_concat(ctx0, cache, value_2d, 1); // [n_embd, n_cache + 1]
// gather indices [0..row_idx-1, n_cache, row_idx+1..n_cache-1]: row_idx is a
// compile-time int, so this is built via concat rather than ggml_set_rows
// (which requires an F32/F16 value, not usable for an I32 index array)
ggml_tensor * idx = const_i32(cache, (float) n_cache);
if (row_idx > 0) {
ggml_tensor * prefix = ggml_cast(ctx0, ggml_arange(ctx0, 0.0f, (float) row_idx, 1.0f), GGML_TYPE_I32);
idx = ggml_concat(ctx0, prefix, idx, 0);
}
if (row_idx < n_cache - 1) {
ggml_tensor * suffix = ggml_cast(ctx0, ggml_arange(ctx0, (float) (row_idx + 1), (float) n_cache, 1.0f), GGML_TYPE_I32);
idx = ggml_concat(ctx0, idx, suffix, 0);
}
ggml_tensor * result = ggml_get_rows(ctx0, cache_ext, idx);
cb(result, "cache_set_out", -1);
return result;
}
// builds a const i32 value with no host upload: view any tensor, cast to
// f32 (ggml_scale only supports f32), scale it to 0, add the value, cast to i32
ggml_tensor * clip_graph_qwen3tts_gen::const_i32(ggml_tensor * anchor, float value) const {
ggml_tensor * v = ggml_view_1d(ctx0, anchor, 1, 0);
if (v->type != GGML_TYPE_F32) {
v = ggml_cast(ctx0, v, GGML_TYPE_F32);
}
return ggml_cast(ctx0, ggml_scale_bias(ctx0, v, 0.0f, value), GGML_TYPE_I32);
}
// causal keep-mask row for a query at position pos, window size n_kv_pad
ggml_tensor * clip_graph_qwen3tts_gen::causal_mask_row(int64_t n_kv_pad, int pos) const {
ggml_tensor * ones = ggml_fill(ctx0, ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_kv_pad, n_kv_pad), 1.0f);
ggml_tensor * keep = ggml_tri(ctx0, ones, GGML_TRI_TYPE_LOWER_DIAG);
ggml_tensor * row = ggml_view_1d(ctx0, keep, n_kv_pad, (size_t) pos * keep->nb[1]);
ggml_tensor * mask = ggml_log(ctx0, row); // 0 = keep, -inf = masked
return ggml_reshape_4d(ctx0, mask, n_kv_pad, 1, 1, 1);
}
// talker hidden size -> predictor hidden size (small_to_mtp_projection)
ggml_tensor * clip_graph_qwen3tts_gen::project_in(ggml_tensor * cur) const {
if (!model.gen_code_proj_in_w) {
return cur;
}
cur = ggml_mul_mat(ctx0, model.gen_code_proj_in_w, cur);
if (model.gen_code_proj_in_b) {
cur = ggml_add(ctx0, cur, model.gen_code_proj_in_b);
}
return cur;
}
// one transformer layer at a single new position pos; writes k/v into
// k_cache_layer/v_cache_layer at row pos
ggml_tensor * clip_graph_qwen3tts_gen::layer_forward(
ggml_tensor * cur,
const clip_layer & layer,
ggml_tensor * inp_pos,
ggml_tensor * kq_mask,
ggml_tensor *& k_cache_layer,
ggml_tensor *& v_cache_layer,
int64_t n_kv_pad,
int pos,
int il) const {
const int n_head = hparams.n_head;
const int n_head_kv = hparams.n_head_kv;
const int64_t d_head = layer.q_w->ne[1] / n_head; // real head_dim, not n_embd / n_head
const float kq_scale = 1.0f / sqrtf((float) d_head);
ggml_tensor * residual = cur;
ggml_tensor * h = ggml_rms_norm(ctx0, cur, hparams.eps);
h = ggml_mul(ctx0, h, layer.ln_1_w);
ggml_tensor * q = ggml_mul_mat(ctx0, layer.q_w, h);
ggml_tensor * k = ggml_mul_mat(ctx0, layer.k_w, h);
ggml_tensor * v = ggml_mul_mat(ctx0, layer.v_w, h);
q = ggml_reshape_3d(ctx0, q, d_head, n_head, 1);
k = ggml_reshape_3d(ctx0, k, d_head, n_head_kv, 1);
q = ggml_rms_norm(ctx0, q, hparams.eps);
q = ggml_mul(ctx0, q, layer.q_norm);
k = ggml_rms_norm(ctx0, k, hparams.eps);
k = ggml_mul(ctx0, k, layer.k_norm);
q = ggml_rope_ext(ctx0, q, inp_pos, nullptr, (int) d_head, GGML_ROPE_TYPE_NEOX, 0,
hparams.rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
k = ggml_rope_ext(ctx0, k, inp_pos, nullptr, (int) d_head, GGML_ROPE_TYPE_NEOX, 0,
hparams.rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
// write k/v into the cache at row pos, flat layout
ggml_tensor * k_flat = ggml_reshape_1d(ctx0, k, d_head * n_head_kv);
k_cache_layer = cache_set(k_cache_layer, pos, k_flat);
v_cache_layer = cache_set(v_cache_layer, pos, v);
ggml_tensor * q_cur = ggml_reshape_4d(ctx0, q, d_head, n_head, 1, 1);
ggml_tensor * k_cur = ggml_reshape_4d(ctx0, k_cache_layer, d_head, n_head_kv, n_kv_pad, 1);
ggml_tensor * v_cur = ggml_reshape_4d(ctx0, v_cache_layer, d_head, n_head_kv, n_kv_pad, 1);
ggml_tensor * attn_out = build_attn(layer.o_w, layer.o_b, q_cur, k_cur, v_cur, kq_mask, kq_scale, il);
cur = ggml_add(ctx0, residual, attn_out);
ggml_tensor * h2 = ggml_rms_norm(ctx0, cur, hparams.eps);
h2 = ggml_mul(ctx0, h2, layer.ln_2_w);
ggml_tensor * gate = ggml_mul_mat(ctx0, layer.ff_gate_w, h2);
ggml_tensor * up = ggml_mul_mat(ctx0, layer.ff_up_w, h2);
ggml_tensor * gu = ggml_swiglu_split(ctx0, gate, up);
ggml_tensor * down = ggml_mul_mat(ctx0, layer.ff_down_w, gu);
return ggml_add(ctx0, cur, down);
}
// position 0: hidden bridge, no sampling, only seeds the k/v cache.
// position 1: embed(code0) via the talker's out_embd table, sample with
// lm_head[0], write out_code_cache[1].
void clip_graph_qwen3tts_gen::prefill(
std::vector<ggml_tensor *> & k_cache,
std::vector<ggml_tensor *> & v_cache,
ggml_tensor *& out_code_cache,
ggml_tensor * h_state,
ggml_tensor * code0_embd,
ggml_tensor * inp_rand,
int top_k,
float top_p) const {
const int64_t n_kv_pad = k_cache[0]->ne[1];
{
ggml_tensor * cur = project_in(h_state);
ggml_tensor * kq_mask = causal_mask_row(n_kv_pad, 0);
ggml_tensor * inp_pos = const_i32(k_cache[0], 0.0f);
for (size_t il = 0; il < model.layers.size(); il++) {
cur = layer_forward(cur, model.layers[il], inp_pos, kq_mask, k_cache[il], v_cache[il], n_kv_pad, 0, (int) il);
}
// position 0's own output is not used further, it only seeded the cache
}
{
ggml_tensor * cur = project_in(code0_embd);
ggml_tensor * kq_mask = causal_mask_row(n_kv_pad, 1);
ggml_tensor * inp_pos = const_i32(k_cache[0], 1.0f);
for (size_t il = 0; il < model.layers.size(); il++) {
cur = layer_forward(cur, model.layers[il], inp_pos, kq_mask, k_cache[il], v_cache[il], n_kv_pad, 1, (int) il);
}
cur = ggml_rms_norm(ctx0, cur, hparams.eps);
cur = ggml_mul(ctx0, cur, model.gen_code_norm_w);
ggml_tensor * head_w = model.gen_code_head_w;
ggml_tensor * head_g = ggml_view_2d(ctx0, head_w, head_w->ne[0], head_w->ne[1], head_w->nb[1], 0); // lm_head[0]
ggml_tensor * logits = ggml_mul_mat(ctx0, head_g, cur);
ggml_tensor * sampled = do_sampling(logits, inp_rand, top_k, top_p);
out_code_cache = cache_set(out_code_cache, 1, sampled);
}
}
// one decode step of the 5-layer code_predictor.
// at step_idx g: read code from out_code_cache[g], embed it with codebook
// table g-1, write the new k/v at cache row g+1, sample with lm_head[g],
// write the result to out_code_cache[g+1].
//
// k_cache/v_cache: per layer, [d_head * n_head_kv, n_kv_pad].
// out_code_cache: [1, n_codes] I32. inp_rand: [1] F32 draw for this step.
// Create all input tensors in build(), not here.
// step_idx range: [1, n_acoustic - 1]. Returns the new out_code_cache.
ggml_tensor * clip_graph_qwen3tts_gen::step(
std::vector<ggml_tensor *> & k_cache,
std::vector<ggml_tensor *> & v_cache,
ggml_tensor * out_code_cache,
ggml_tensor * inp_rand,
int step_idx,
int top_k,
float top_p) const {
const int64_t n_acoustic = model.gen_code_head_w->ne[2];
GGML_ASSERT(step_idx >= 1 && step_idx < n_acoustic);
GGML_ASSERT(k_cache.size() == model.layers.size());
GGML_ASSERT(v_cache.size() == model.layers.size());
const int64_t n_kv_pad = k_cache[0]->ne[1];
const int pos = step_idx + 1; // new cache row and RoPE position
// embed the previous code through this step's codebook table
// (out_code_cache has ne[0] == 1, so one row is already a single scalar)
ggml_tensor * code_in = ggml_view_1d(ctx0, out_code_cache, 1, (size_t) step_idx * out_code_cache->nb[1]);
ggml_tensor * embd_w = model.gen_code_embd_w; // [n_embd_talker, vocab, n_acoustic]
ggml_tensor * embd_g = ggml_view_2d(ctx0, embd_w, embd_w->ne[0], embd_w->ne[1], embd_w->nb[1],
(size_t) (step_idx - 1) * embd_w->nb[2]);
ggml_tensor * cur = ggml_get_rows(ctx0, embd_g, code_in);
cur = ggml_reshape_1d(ctx0, cur, cur->ne[0]);
cb(cur, "step_embd_in", step_idx);
cur = project_in(cur);
cb(cur, "step_proj_in", step_idx);
ggml_tensor * kq_mask = causal_mask_row(n_kv_pad, pos);
ggml_tensor * inp_pos = const_i32(k_cache[0], (float) pos);
for (size_t il = 0; il < model.layers.size(); il++) {
cur = layer_forward(cur, model.layers[il], inp_pos, kq_mask, k_cache[il], v_cache[il], n_kv_pad, pos, (int) il);
cb(cur, "step_layer_out", (int) il);
}
// final norm, this step's lm_head, sample, write the result
cur = ggml_rms_norm(ctx0, cur, hparams.eps);
cur = ggml_mul(ctx0, cur, model.gen_code_norm_w);
ggml_tensor * head_w = model.gen_code_head_w; // [n_embd_pred, vocab, n_acoustic]
ggml_tensor * head_g = ggml_view_2d(ctx0, head_w, head_w->ne[0], head_w->ne[1], head_w->nb[1],
(size_t) step_idx * head_w->nb[2]);
ggml_tensor * logits = ggml_mul_mat(ctx0, head_g, cur);
cb(logits, "step_logits", step_idx);
ggml_tensor * sampled = do_sampling(logits, inp_rand, top_k, top_p);
cb(sampled, "step_sampled", step_idx);
return cache_set(out_code_cache, pos, sampled);
}
// left-context window of the stateless code2wav: every frame re-decodes
// this many past frames in front of the current one and emits only the
// newest hop. At the start of an utterance the missing history is padded
// with code 0, an imperfect warmup that fades once real frames fill in.
static constexpr int C2W_CTX_FRAMES = 23;
// causal conv1d, stride 1: left-pad (K-1)*dilation zeros, then a plain conv.
// x: [T, IC] (T-first, matches ggml_conv_1d's native layout). w: [K, IC, OC].
// returns [T, OC].
ggml_tensor * clip_graph_qwen3tts_gen::code2wav::causal_conv1d(ggml_tensor * x, ggml_tensor * w, ggml_tensor * b, int dilation) const {
const int K = (int) w->ne[0];
const int pad = (K - 1) * dilation;
ggml_tensor * x_pad = pad > 0 ? ggml_pad_ext(ctx0, x, pad, 0, 0, 0, 0, 0, 0, 0) : x;
ggml_tensor * y = ggml_conv_1d(ctx0, w, x_pad, 1, 0, dilation); // [T, OC, 1]
y = ggml_reshape_2d(ctx0, y, y->ne[0], y->ne[1]);
if (b) {
y = ggml_add(ctx0, y, ggml_reshape_2d(ctx0, b, 1, b->ne[0]));
}
return y;
}
// causal depthwise conv1d, stride 1, dilation 1, kernel from w's shape.
// x: [T, C]. w: [K, 1, C]. returns [T, C].
ggml_tensor * clip_graph_qwen3tts_gen::code2wav::causal_conv1d_dw(ggml_tensor * x, ggml_tensor * w, ggml_tensor * b) const {
const int K = (int) w->ne[0];
const int pad = K - 1;
ggml_tensor * x_pad = pad > 0 ? ggml_pad_ext(ctx0, x, pad, 0, 0, 0, 0, 0, 0, 0) : x;
ggml_tensor * y = ggml_conv_1d_dw(ctx0, w, x_pad, 1, 0, 1); // [T, C, 1]
y = ggml_reshape_2d(ctx0, y, y->ne[0], y->ne[1]);
if (b) {
y = ggml_add(ctx0, y, ggml_reshape_2d(ctx0, b, 1, b->ne[0]));
}
return y;
}
// causal ConvTranspose1d: full (non-causal) transpose conv, then trim the
// right (kernel - stride) frames that would otherwise leak future context.
// x: [T, IC] (plain matrix). w: [K, OC, IC]. returns [T * stride, OC].
ggml_tensor * clip_graph_qwen3tts_gen::code2wav::causal_conv_transpose1d(ggml_tensor * x, ggml_tensor * w, ggml_tensor * b, int stride) const {
const int K = (int) w->ne[0];
const int trim = K - stride;
ggml_tensor * y = ggml_conv_transpose_1d(ctx0, w, x, stride, 0, 1); // [T*stride + trim, OC, 1, 1]
y = ggml_reshape_2d(ctx0, y, y->ne[0], y->ne[1]);
if (trim > 0) {
y = ggml_cont(ctx0, ggml_view_2d(ctx0, y, y->ne[0] - trim, y->ne[1], y->nb[1], 0));
}
if (b) {
y = ggml_add(ctx0, y, ggml_reshape_2d(ctx0, b, 1, b->ne[0]));
}
return y;
}
// SnakeBeta activation: y = x + sin(alpha*x)^2 * inv_beta (alpha/inv_beta
// already folded with exp()/reciprocal at conversion time).
// x: [T, C]. alpha/beta: [C]. broadcasts over T.
ggml_tensor * clip_graph_qwen3tts_gen::code2wav::snake(ggml_tensor * x, ggml_tensor * alpha, ggml_tensor * beta) const {
ggml_tensor * a = ggml_reshape_2d(ctx0, alpha, 1, alpha->ne[0]);
ggml_tensor * b = ggml_reshape_2d(ctx0, beta, 1, beta->ne[0]);
ggml_tensor * s = ggml_sin(ctx0, ggml_mul(ctx0, x, a));
s = ggml_sqr(ctx0, s);
s = ggml_mul(ctx0, s, b);
return ggml_add(ctx0, x, s);
}
// RVQ codebook decode: 16 codes -> 512-dim hidden (C-first, [512, 1]).
// codebook 0 (semantic) and 1..15 (acoustic) are summed within their own
// group, projected out_proj'd separately, then the two projections added.
ggml_tensor * clip_graph_qwen3tts_gen::code2wav::quant_decode(ggml_tensor * out_code_cache, ggml_tensor * ctx_codes) const {
const auto & c2w = model.c2w;
const int64_t T_ctx = ctx_codes->ne[0];
// ids for group g over the whole window: T_ctx past codes then the
// current frame's code, [T_ctx + 1] I32
auto group_ids = [&](int g) {
ggml_tensor * past = ggml_view_1d(ctx0, ctx_codes, T_ctx, (size_t) g * ctx_codes->nb[1]);
ggml_tensor * cur = ggml_view_1d(ctx0, out_code_cache, 1, (size_t) g * out_code_cache->nb[1]);
return ggml_concat(ctx0, past, cur, 0);
};
ggml_tensor * sem = ggml_get_rows(ctx0, c2w.quant_first_cb_w, group_ids(0)); // [256, W]
ggml_tensor * sem_out = ggml_mul_mat(ctx0, c2w.quant_first_out_w, sem); // [512, W]
ggml_tensor * acc = nullptr;
const int64_t n_acoustic = c2w.quant_rest_cb_w->ne[2];
for (int g = 1; g <= n_acoustic; g++) {
ggml_tensor * cb_g = ggml_view_2d(ctx0, c2w.quant_rest_cb_w, c2w.quant_rest_cb_w->ne[0], c2w.quant_rest_cb_w->ne[1],
c2w.quant_rest_cb_w->nb[1], (size_t) (g - 1) * c2w.quant_rest_cb_w->nb[2]);
ggml_tensor * embd = ggml_get_rows(ctx0, cb_g, group_ids(g)); // [256, W]
acc = acc ? ggml_add(ctx0, acc, embd) : embd;
}
ggml_tensor * ac_out = ggml_mul_mat(ctx0, c2w.quant_rest_out_w, acc); // [512, W]
ggml_tensor * hidden = ggml_add(ctx0, sem_out, ac_out);
cb(hidden, "wav_quant_hidden", -1);
return hidden;
}
// one pre_transformer layer. Single position only: pos0/mask are shared
// constants across all layers/calls (self-attention on one token always
// has softmax weight 1, but the ops are still built out in full so this
// slots into a real KV cache later without restructuring).
ggml_tensor * clip_graph_qwen3tts_gen::code2wav::tfm_layer_forward(ggml_tensor * cur, const clip_layer & layer, ggml_tensor * pos, ggml_tensor * mask) const {
const int n_head = hparams.wav_tfm_n_head;
const int n_head_kv = hparams.wav_tfm_n_head_kv;
const int64_t d_head = layer.q_w->ne[1] / n_head;
const float kq_scale = 1.0f / sqrtf((float) d_head);
const int64_t T = cur->ne[1];
ggml_tensor * residual = cur;
ggml_tensor * h = ggml_rms_norm(ctx0, cur, hparams.wav_tfm_eps);
h = ggml_mul(ctx0, h, layer.ln_1_w);
ggml_tensor * q = ggml_mul_mat(ctx0, layer.q_w, h);
ggml_tensor * k = ggml_mul_mat(ctx0, layer.k_w, h);
ggml_tensor * v = ggml_mul_mat(ctx0, layer.v_w, h);
q = ggml_reshape_3d(ctx0, q, d_head, n_head, T);
k = ggml_reshape_3d(ctx0, k, d_head, n_head_kv, T);
q = ggml_rope_ext(ctx0, q, pos, nullptr, (int) d_head, GGML_ROPE_TYPE_NEOX, 0,
hparams.wav_tfm_rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
k = ggml_rope_ext(ctx0, k, pos, nullptr, (int) d_head, GGML_ROPE_TYPE_NEOX, 0,
hparams.wav_tfm_rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
ggml_tensor * q_cur = ggml_reshape_4d(ctx0, q, d_head, n_head, T, 1);
ggml_tensor * k_cur = ggml_reshape_4d(ctx0, k, d_head, n_head_kv, T, 1);
ggml_tensor * v_cur = ggml_reshape_4d(ctx0, v, d_head, n_head_kv, T, 1);
ggml_tensor * attn_out = build_attn(layer.o_w, layer.o_b, q_cur, k_cur, v_cur, mask, kq_scale, 0);
if (layer.ls_1_w) {
attn_out = ggml_mul(ctx0, attn_out, layer.ls_1_w);
}
cur = ggml_add(ctx0, residual, attn_out);
ggml_tensor * residual2 = cur;
ggml_tensor * h2 = ggml_rms_norm(ctx0, cur, hparams.wav_tfm_eps);
h2 = ggml_mul(ctx0, h2, layer.ln_2_w);
ggml_tensor * gate = ggml_mul_mat(ctx0, layer.ff_gate_w, h2);
ggml_tensor * up = ggml_mul_mat(ctx0, layer.ff_up_w, h2);
ggml_tensor * gu = ggml_swiglu_split(ctx0, gate, up);
ggml_tensor * down = ggml_mul_mat(ctx0, layer.ff_down_w, gu);
if (layer.ls_2_w) {
down = ggml_mul(ctx0, down, layer.ls_2_w);
}
return ggml_add(ctx0, residual2, down);
}
// dwconv -> LayerNorm -> pwconv1 -> GELU -> pwconv2 -> layer scale -> residual.
// x: [T, C] T-first; LayerNorm/pwconv need C on ne0, so this transposes in
// and back out around them.
ggml_tensor * clip_graph_qwen3tts_gen::code2wav::convnext_block(ggml_tensor * x, const clip_code2wav::upsample_block & blk) const {
ggml_tensor * residual = x;
ggml_tensor * h = causal_conv1d_dw(x, blk.dwconv_w, blk.dwconv_b); // [T, C]
ggml_tensor * hc = ggml_cont(ctx0, ggml_transpose(ctx0, h)); // [C, T]
hc = ggml_norm(ctx0, hc, 1e-6f);
hc = ggml_mul(ctx0, hc, blk.norm_w);
hc = ggml_add(ctx0, hc, blk.norm_b);
ggml_tensor * g = ggml_mul_mat(ctx0, blk.pw1_w, hc);
g = ggml_add(ctx0, g, blk.pw1_b);
g = ggml_gelu(ctx0, g);
g = ggml_mul_mat(ctx0, blk.pw2_w, g);
g = ggml_add(ctx0, g, blk.pw2_b);
g = ggml_mul(ctx0, g, blk.gamma);
ggml_tensor * g_t = ggml_cont(ctx0, ggml_transpose(ctx0, g)); // back to [T, C]
return ggml_add(ctx0, residual, g_t);
}
// SnakeBeta -> dilated causal conv (k=7) -> SnakeBeta -> pointwise causal conv (k=1) -> residual.
// x: [T, C]. returns [T, C].
ggml_tensor * clip_graph_qwen3tts_gen::code2wav::dac_res_unit(ggml_tensor * x, const clip_code2wav::dac_res & res, int dilation) const {
ggml_tensor * residual = x;
ggml_tensor * h = snake(x, res.act1_alpha, res.act1_beta);
h = causal_conv1d(h, res.conv1_w, res.conv1_b, dilation);
h = snake(h, res.act2_alpha, res.act2_beta);
h = causal_conv1d(h, res.conv2_w, res.conv2_b, 1);
return ggml_add(ctx0, residual, h);
}
// RVQ codes -> raw PCM over the left-context window: the whole window
// decodes through the stack and only the newest hop of samples returns.
ggml_tensor * clip_graph_qwen3tts_gen::code2wav::decode(ggml_tensor * out_code_cache, ggml_tensor * ctx_codes) const {
const auto & c2w = model.c2w;
const int64_t W = ctx_codes->ne[0] + 1;
// 1. quantizer decode: 16 codes per frame -> [512, W] (C-first)
ggml_tensor * hidden = quant_decode(out_code_cache, ctx_codes);
// 2. pre_conv: [512, W] -> T-first [W, 512] -> causal conv k=3 -> [W, 1024]
ggml_tensor * x = ggml_cont(ctx0, ggml_transpose(ctx0, hidden)); // [W, 512]
x = causal_conv1d(x, c2w.pre_conv_w, c2w.pre_conv_b, 1); // [W, 1024]
cb(x, "wav_pre_conv_out", -1);
// 3. pre_transformer: back to C-first [1024, W], project down to
// hidden_size, run the layers causal over the window, project back up.
// W stays below the model's attention window, so full causal attention
// equals the sliding window here.
ggml_tensor * cur = ggml_cont(ctx0, ggml_transpose(ctx0, x)); // [1024, W]
cur = ggml_mul_mat(ctx0, c2w.tfm_in_proj_w, cur);
cur = ggml_add(ctx0, cur, c2w.tfm_in_proj_b); // [512 (tfm hidden), W]
ggml_tensor * pos = ggml_cast(ctx0, ggml_arange(ctx0, 0.0f, (float) W, 1.0f), GGML_TYPE_I32);
ggml_tensor * tri = ggml_tri(ctx0, ggml_fill(ctx0, ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, W, W), 1.0f),
GGML_TRI_TYPE_LOWER_DIAG);
ggml_tensor * mask = ggml_reshape_4d(ctx0, ggml_log(ctx0, tri), W, W, 1, 1);
for (int il = 0; il < hparams.wav_tfm_n_layer; il++) {
cur = tfm_layer_forward(cur, c2w.tfm_layers[il], pos, mask);
}
cur = ggml_rms_norm(ctx0, cur, hparams.wav_tfm_eps);
cur = ggml_mul(ctx0, cur, c2w.tfm_output_norm_w);
cur = ggml_mul_mat(ctx0, c2w.tfm_out_proj_w, cur);
cur = ggml_add(ctx0, cur, c2w.tfm_out_proj_b); // [1024, 1]
cb(cur, "wav_tfm_out", -1);
// 4. upsample: 2x (causal ConvTranspose1d, stride 2 + ConvNeXt block), back to T-first
x = ggml_cont(ctx0, ggml_transpose(ctx0, cur)); // [1, 1024]
for (size_t il = 0; il < c2w.upsample.size(); il++) {
const auto & up = c2w.upsample[il];
x = causal_conv_transpose1d(x, up.conv_w, up.conv_b, 2);
x = convnext_block(x, up);
cb(x, "wav_upsample_out", (int) il);
}
// 5. DAC decoder: conv_pre -> n blocks (SnakeBeta -> ConvTranspose1d -> 3 res units) -> conv_post
static constexpr int DAC_STRIDES[4] = { 8, 5, 4, 3 };
static constexpr int DAC_DILATIONS[3] = { 1, 3, 9 };
x = causal_conv1d(x, c2w.dac_entry_w, c2w.dac_entry_b, 1);
cb(x, "wav_dac_entry_out", -1);
for (size_t il = 0; il < c2w.dac.size(); il++) {
const auto & blk = c2w.dac[il];
x = snake(x, blk.snake_alpha, blk.snake_beta);
x = causal_conv_transpose1d(x, blk.conv_w, blk.conv_b, DAC_STRIDES[il]);
for (size_t ir = 0; ir < blk.res.size(); ir++) {
x = dac_res_unit(x, blk.res[ir], DAC_DILATIONS[ir]);
}
cb(x, "wav_dac_block_out", (int) il);
}
x = snake(x, c2w.dac_post_snake_alpha, c2w.dac_post_snake_beta);
x = causal_conv1d(x, c2w.dac_post_conv_w, c2w.dac_post_conv_b, 1); // [W * hop, 1]
x = ggml_clamp(ctx0, x, -1.0f, 1.0f);
x = ggml_reshape_1d(ctx0, x, x->ne[0]);
// emit only the newest frame's samples, the rest was context
const int64_t hop = x->ne[0] / W;
x = ggml_cont(ctx0, ggml_view_1d(ctx0, x, hop, (size_t) (W - 1) * (size_t) hop * sizeof(float)));
cb(x, "wav_audio_out", -1);
return x;
}
ggml_cgraph * clip_graph_qwen3tts_gen::build() {
GGML_ASSERT(n_batch == 1); // this module only ever processes one frame at a time
ggml_tensor * h_state = build_inp_raw(1);
h_state = ggml_reshape_1d(ctx0, h_state, h_state->ne[0]);
cb(h_state, "inp_h_state", -1);
ggml_tensor * code0 = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, 1);
ggml_set_name(code0, "inp_code0");
ggml_set_input(code0);
ggml_tensor * code0_embd = ggml_get_rows(ctx0, model.gen_code_out_embd_w, code0);
code0_embd = ggml_reshape_1d(ctx0, code0_embd, code0_embd->ne[0]);
cb(code0_embd, "code0_embd", -1);
const int64_t n_acoustic = model.gen_code_head_w->ne[2]; // 15
const int n_codes = (int) n_acoustic + 1; // 16
const int64_t n_kv_pad = n_codes;
const int n_layer = (int) model.layers.size();
const int n_head = hparams.n_head;
const int n_head_kv = hparams.n_head_kv;
const int64_t d_head = model.layers[0].q_w->ne[1] / n_head;
// zero-filled per layer k/v caches, so masked-out rows can't hold garbage
std::vector<ggml_tensor *> k_cache(n_layer), v_cache(n_layer);
for (int il = 0; il < n_layer; il++) {
k_cache[il] = ggml_fill(ctx0, ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, d_head * n_head_kv, n_kv_pad), 0.0f);
v_cache[il] = ggml_fill(ctx0, ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, d_head * n_head_kv, n_kv_pad), 0.0f);
}
ggml_tensor * out_code_cache = ggml_new_tensor_2d(ctx0, GGML_TYPE_I32, 1, n_codes);
out_code_cache = cache_set(out_code_cache, 0, code0);
ggml_tensor * inp_rand0 = ggml_new_tensor_1d(ctx0, GGML_TYPE_F32, 1);
ggml_set_name(inp_rand0, "inp_rand_0");
ggml_set_input(inp_rand0);
prefill(k_cache, v_cache, out_code_cache, h_state, code0_embd, inp_rand0, top_k, top_p);
for (int g = 1; g < n_acoustic; g++) {
ggml_tensor * inp_rand = ggml_new_tensor_1d(ctx0, GGML_TYPE_F32, 1);
ggml_set_name(inp_rand, ("inp_rand_" + std::to_string(g)).c_str());
ggml_set_input(inp_rand);
out_code_cache = step(k_cache, v_cache, out_code_cache, inp_rand, g, top_k, top_p);
}
// past codes feeding the code2wav left-context window, oldest first
ggml_tensor * ctx_codes = ggml_new_tensor_2d(ctx0, GGML_TYPE_I32, C2W_CTX_FRAMES, n_codes);
ggml_set_name(ctx_codes, "inp_ctx_codes");
ggml_set_input(ctx_codes);
// output 1: raw PCM audio for this frame, decoded from the 16 sampled
// codes with the context window in front
ggml_tensor * out_audio = code2wav(*this).decode(out_code_cache, ctx_codes);
ggml_set_name(out_audio, "out_audio");
ggml_set_output(out_audio);
ggml_build_forward_expand(gf, out_audio);
// output 2: this frame's 16 codes, for the caller's context ring
ggml_tensor * out_codes = ggml_cont(ctx0, out_code_cache);
ggml_set_name(out_codes, "out_codes");
ggml_set_output(out_codes);
ggml_build_forward_expand(gf, out_codes);
// output 3 (last node, read by clip_encode()): the sum of all 16
// codebook embeddings, fed back to the talker backbone for the next frame
ggml_tensor * out_embd = code0_embd;
for (int g = 1; g <= n_acoustic; g++) {
ggml_tensor * code_g = ggml_view_1d(ctx0, out_code_cache, 1, (size_t) g * out_code_cache->nb[1]);
ggml_tensor * embd_g = ggml_view_2d(ctx0, model.gen_code_embd_w, model.gen_code_embd_w->ne[0], model.gen_code_embd_w->ne[1],
model.gen_code_embd_w->nb[1], (size_t) (g - 1) * model.gen_code_embd_w->nb[2]);
ggml_tensor * e = ggml_get_rows(ctx0, embd_g, code_g);
e = ggml_reshape_1d(ctx0, e, e->ne[0]);
out_embd = ggml_add(ctx0, out_embd, e);
}
out_embd = ggml_reshape_2d(ctx0, out_embd, out_embd->ne[0], 1);
cb(out_embd, "gen_audio_out", -1);
ggml_build_forward_expand(gf, out_embd);
return gf;
}
+201
View File
@@ -0,0 +1,201 @@
#include "models.h"
static constexpr int SPK_RES2NET_SCALE = 8; // enc_res2net_scale
static constexpr int SPK_DILATIONS[3] = { 2, 3, 4 }; // enc_dilations[1..3]
// Conv1d, kernel K, padding "same" (reflect), dilation d.
// x: [C, T] (ne[0]=C, ne[1]=T) -> [out_c, T]
ggml_tensor * clip_graph_qwen3tts_spkenc::conv1d_same(ggml_tensor * x, ggml_tensor * w, ggml_tensor * b, int dilation) const {
const int K = (int) w->ne[0];
const int IC = (int) w->ne[1];
const int OC = (int) w->ne[2];
const int pad = ((K - 1) * dilation) / 2;
// ggml_pad_reflect_1d pads ne[0], so bring T onto ne[0] first; im2col
// below expects the same [T, IC] layout.
ggml_tensor * x_t = ggml_cont(ctx0, ggml_transpose(ctx0, x)); // [T, IC]
if (pad > 0) {
x_t = ggml_pad_reflect_1d(ctx0, x_t, pad, pad); // [T + 2*pad, IC]
}
ggml_tensor * x4d = ggml_reshape_4d(ctx0, x_t, x_t->ne[0], IC, 1, 1);
// Dummy F32 kernel: im2col only reads its shape (K, IC), never its data,
// so this avoids a type assert when w is quantized.
ggml_tensor * dummy = ggml_new_tensor_4d(ctx0, GGML_TYPE_F32, K, IC, 1, 1);
ggml_tensor * col = ggml_im2col(ctx0, dummy, x4d, 1, 1, 0, 0, dilation, 1, false, GGML_TYPE_F32);
const int64_t T_out = col->ne[1];
col = ggml_reshape_2d(ctx0, col, (int64_t) K * IC, T_out);
ggml_tensor * w2d = ggml_reshape_2d(ctx0, w, (int64_t) K * IC, OC);
ggml_tensor * y = ggml_mul_mat(ctx0, w2d, col); // [OC, T_out]
ggml_mul_mat_set_prec(y, GGML_PREC_F32);
ggml_tensor * b2d = ggml_reshape_2d(ctx0, b, OC, 1);
y = ggml_add(ctx0, y, b2d);
return y;
}
// Res2Net: split channel axis into `scale` chunks, chain dilated conv1d
// branches. x: [C, T] -> [C, T]
ggml_tensor * clip_graph_qwen3tts_spkenc::res2net(ggml_tensor * x, const clip_layer & layer, int dilation, int scale) const {
const int64_t C = x->ne[0];
const int64_t T = x->ne[1];
const int64_t Cs = C / scale;
std::vector<ggml_tensor *> outs;
outs.reserve(scale);
auto chunk = [&](int i) -> ggml_tensor * {
return ggml_view_2d(ctx0, x, Cs, T, x->nb[1], (size_t) i * Cs * x->nb[0]);
};
ggml_tensor * prev = nullptr;
for (int i = 0; i < scale; i++) {
ggml_tensor * c = ggml_cont(ctx0, chunk(i));
if (i == 0) {
outs.push_back(c);
continue;
}
ggml_tensor * inp = (i >= 2) ? ggml_add(ctx0, c, prev) : c;
ggml_tensor * y = conv1d_same(inp, layer.res2_conv_w[i - 1], layer.res2_conv_b[i - 1], dilation);
y = ggml_relu(ctx0, y);
outs.push_back(y);
prev = y;
}
ggml_tensor * acc = outs[0];
for (int i = 1; i < scale; i++) {
acc = ggml_concat(ctx0, acc, outs[i], 0);
}
return acc;
}
// Squeeze-and-excitation gate. x: [C, T] -> [C, T]
ggml_tensor * clip_graph_qwen3tts_spkenc::se_block(ggml_tensor * x, const clip_layer & layer) const {
// temporal mean, keepdim: transpose so T is on ne[0], reduce, transpose back
ggml_tensor * x_t = ggml_cont(ctx0, ggml_transpose(ctx0, x)); // [T, C]
ggml_tensor * mean = ggml_mean(ctx0, x_t); // [1, C]
mean = ggml_cont(ctx0, ggml_transpose(ctx0, mean)); // [C, 1]
ggml_tensor * h = conv1d_same(mean, layer.se_conv1_w, layer.se_conv1_b, 1);
h = ggml_relu(ctx0, h);
h = conv1d_same(h, layer.se_conv2_w, layer.se_conv2_b, 1);
h = ggml_sigmoid(ctx0, h); // [C, 1]
return ggml_mul(ctx0, x, h); // broadcast gate over T
}
// tdnn1 -> res2net -> tdnn2 -> se, plus residual. x: [C, T] -> [C, T]
ggml_tensor * clip_graph_qwen3tts_spkenc::se_res2net_block(ggml_tensor * x, const clip_layer & layer, int dilation, int scale) const {
ggml_tensor * residual = x;
ggml_tensor * h = conv1d_same(x, layer.conv_pw1_w, layer.conv_pw1_b, 1); // tdnn1
h = ggml_relu(ctx0, h);
h = res2net(h, layer, dilation, scale);
h = conv1d_same(h, layer.conv_pw2_w, layer.conv_pw2_b, 1); // tdnn2
h = ggml_relu(ctx0, h);
h = se_block(h, layer);
return ggml_add(ctx0, h, residual);
}
// Attentive statistics pooling. x: [C, T] -> [2*C, 1]
ggml_tensor * clip_graph_qwen3tts_spkenc::attentive_stats_pool(ggml_tensor * x) const {
const int64_t T = x->ne[1];
// mean over T: [C, 1]
ggml_tensor * x_t = ggml_cont(ctx0, ggml_transpose(ctx0, x));
ggml_tensor * mean = ggml_mean(ctx0, x_t);
mean = ggml_cont(ctx0, ggml_transpose(ctx0, mean));
// std over T: sqrt(clamp(mean((x - mean)^2), eps))
ggml_tensor * mean_rep = ggml_repeat(ctx0, mean, x);
ggml_tensor * centered = ggml_sub(ctx0, x, mean_rep);
ggml_tensor * var_t = ggml_cont(ctx0, ggml_transpose(ctx0, ggml_sqr(ctx0, centered)));
ggml_tensor * var = ggml_mean(ctx0, var_t);
var = ggml_cont(ctx0, ggml_transpose(ctx0, var));
var = ggml_scale_bias(ctx0, var, 1.0f, 1e-12f);
ggml_tensor * std = ggml_sqrt(ctx0, var);
// attention input: cat([x, mean, std]) along channel axis -> [3C, T]
ggml_tensor * std_rep = ggml_repeat(ctx0, std, x);
ggml_tensor * cat = ggml_concat(ctx0, x, mean_rep, 0);
cat = ggml_concat(ctx0, cat, std_rep, 0);
// attention TDNN (3C -> attn_c) + ReLU, tanh, then 1x1 conv (attn_c -> C)
ggml_tensor * a = conv1d_same(cat, model.spk_asp_tdnn_w, model.spk_asp_tdnn_b, 1);
a = ggml_relu(ctx0, a);
a = ggml_tanh(ctx0, a);
a = conv1d_same(a, model.spk_asp_attn_w, model.spk_asp_attn_b, 1);
// softmax over T
ggml_tensor * a_t = ggml_cont(ctx0, ggml_transpose(ctx0, a)); // [T, C]
ggml_tensor * w_t = ggml_soft_max(ctx0, a_t);
ggml_tensor * w = ggml_cont(ctx0, ggml_transpose(ctx0, w_t)); // [C, T]
// weighted mean: sum(w * x) over T (w already sums to 1 over T,
// ggml_mean gives 1/T scaling so multiply back by T to undo it)
ggml_tensor * wx = ggml_mul(ctx0, w, x);
ggml_tensor * wx_t = ggml_cont(ctx0, ggml_transpose(ctx0, wx));
ggml_tensor * w_mean = ggml_mean(ctx0, wx_t);
w_mean = ggml_scale(ctx0, w_mean, (float) T);
w_mean = ggml_cont(ctx0, ggml_transpose(ctx0, w_mean)); // [C, 1]
// weighted std: sum(w * (x - w_mean)^2) over T
ggml_tensor * w_mean_rep = ggml_repeat(ctx0, w_mean, x);
ggml_tensor * dev = ggml_sub(ctx0, x, w_mean_rep);
ggml_tensor * w_var_in = ggml_mul(ctx0, w, ggml_sqr(ctx0, dev));
ggml_tensor * w_var_t = ggml_cont(ctx0, ggml_transpose(ctx0, w_var_in));
ggml_tensor * w_var = ggml_mean(ctx0, w_var_t);
w_var = ggml_scale(ctx0, w_var, (float) T);
w_var = ggml_cont(ctx0, ggml_transpose(ctx0, w_var));
w_var = ggml_scale_bias(ctx0, w_var, 1.0f, 1e-12f);
ggml_tensor * w_std = ggml_sqrt(ctx0, w_var);
return ggml_concat(ctx0, w_mean, w_std, 0); // [2C, 1]
}
ggml_cgraph * clip_graph_qwen3tts_spkenc::build() {
// inp_raw: [T, n_mel, 1, 1] (nx=T frames, ny=n_mel bins), from the
// preprocessor's mel_spectrogram() output (mtmd_audio_preprocessor_qwen3tts_spk)
ggml_tensor * inp = build_inp_raw(1);
inp = ggml_reshape_2d(ctx0, inp, inp->ne[0], inp->ne[1]);
// this file's convention is [C, T]; the preprocessor delivers [T, C]
ggml_tensor * mel = ggml_cont(ctx0, ggml_transpose(ctx0, inp)); // [n_mel, T]
cb(mel, "mel", -1);
// frontend conv0 TDNN k=5, dilation=1: 128 -> 512
ggml_tensor * cur = conv1d_same(mel, model.conv1d_1_w, model.conv1d_1_b, 1);
cur = ggml_relu(ctx0, cur);
cb(cur, "frontend", -1);
// 3 SE-Res2Net blocks at dilations 2, 3, 4
GGML_ASSERT((int) model.layers.size() == 3);
std::vector<ggml_tensor *> blk_out(3);
for (int il = 0; il < 3; il++) {
cur = se_res2net_block(cur, model.layers[il], SPK_DILATIONS[il], SPK_RES2NET_SCALE);
blk_out[il] = cur;
cb(cur, "block_out", il);
}
// multi-layer feature aggregation: cat blk[0..2] then TDNN k=1 + ReLU
ggml_tensor * cat = ggml_concat(ctx0, blk_out[0], blk_out[1], 0);
cat = ggml_concat(ctx0, cat, blk_out[2], 0); // [1536, T]
ggml_tensor * mfa = conv1d_same(cat, model.conv_out_w, model.conv_out_b, 1);
mfa = ggml_relu(ctx0, mfa);
cb(mfa, "mfa", -1);
// attentive statistics pooling: [1536, T] -> [3072, 1]
ggml_tensor * stats = attentive_stats_pool(mfa);
cb(stats, "asp", -1);
// final FC k=1: [3072, 1] -> [enc_dim, 1]
ggml_tensor * emb = conv1d_same(stats, model.mm_fc_w, model.mm_fc_b, 1);
emb = ggml_reshape_1d(ctx0, emb, emb->ne[0]);
emb = ggml_cont(ctx0, emb);
cb(emb, "spk_embedding", -1);
ggml_build_forward_expand(gf, emb);
return gf;
}
+64
View File
@@ -791,6 +791,70 @@ bool mtmd_audio_preprocessor_mimo_audio::preprocess(const float *
return true;
}
//
// mtmd_audio_preprocessor_qwen3tts_spk
//
// Mirrors qwen_tts.core.models.modeling_qwen3_tts.mel_spectrogram():
// pad reflect by (n_fft - hop) / 2, STFT (n_fft, hop, win=n_fft, hann
// periodic, center=False), mel = slaney_mel_basis @ |STFT|, log(max(mel, 1e-5)).
// Unlike Whisper-style encoders the whole clip is consumed in a single
// forward pass by the ECAPA-TDNN body, so there's no 30s/3000-frame chunking
// or Whisper (max-8)/4 normalization here.
//
void mtmd_audio_preprocessor_qwen3tts_spk::initialize() {
cache.fill_sin_cos_table(hparams.audio_n_fft);
cache.fill_hann_window(hparams.audio_window_len, true);
cache.fill_mel_filterbank_matrix(hparams.n_mel_bins, hparams.audio_n_fft, hparams.audio_sample_rate);
}
bool mtmd_audio_preprocessor_qwen3tts_spk::preprocess(const float * samples,
size_t n_samples,
std::vector<mtmd_audio_mel> & output) {
if (n_samples == 0) {
return false;
}
GGML_ASSERT(!cache.sin_vals.empty());
GGML_ASSERT(!cache.cos_vals.empty());
GGML_ASSERT(!cache.filters.data.empty());
// reflect pad by (n_fft - hop) / 2 = 384, matching center=False STFT framing
const int pad = (hparams.audio_n_fft - hparams.audio_hop_len) / 2;
if ((int) n_samples < pad + 1) {
return false;
}
std::vector<float> padded(n_samples + 2 * pad, 0.0f);
for (int i = 0; i < pad; i++) {
padded[i] = samples[pad - i];
}
std::copy(samples, samples + n_samples, padded.begin() + pad);
for (int i = 0; i < pad; i++) {
padded[n_samples + pad + i] = samples[n_samples - 2 - i];
}
filter_params params;
params.n_mel = hparams.n_mel_bins;
params.n_fft_bins = 1 + (hparams.audio_n_fft / 2);
params.hann_window_size = hparams.audio_window_len;
params.hop_length = hparams.audio_hop_len;
params.sample_rate = hparams.audio_sample_rate;
params.no_padding = true; // reflect padding already applied above
params.use_natural_log = true;
params.use_magnitude = true;
params.mel_floor = 1e-5f;
mtmd_audio_mel out;
bool ok = log_mel_spectrogram(padded.data(), (int) padded.size(), 4, params, cache, out);
if (!ok) {
return false;
}
output.push_back(std::move(out));
return true;
}
//
// mtmd_audio_preprocessor_conformer
//
+9
View File
@@ -120,6 +120,15 @@ struct mtmd_audio_preprocessor_mimo_audio : mtmd_audio_preprocessor {
mtmd_audio_cache cache;
};
struct mtmd_audio_preprocessor_qwen3tts_spk : mtmd_audio_preprocessor {
mtmd_audio_preprocessor_qwen3tts_spk(const clip_ctx * ctx) : mtmd_audio_preprocessor(ctx) {}
void initialize() override;
bool preprocess(const float * samples, size_t n_samples, std::vector<mtmd_audio_mel> & output) override;
private:
mtmd_audio_cache cache;
};
struct mtmd_audio_preprocessor_parakeet : mtmd_audio_preprocessor {
mtmd_audio_preprocessor_parakeet(clip_ctx * ctx) : mtmd_audio_preprocessor(ctx) { }
void initialize() override;
+104
View File
@@ -262,6 +262,15 @@ struct mtmd_context {
struct clip_ctx * ctx_a; // audio
std::vector<float> out_embd; // image embedding vector
// generation context
struct clip_ctx * ctx_gen_a; // audio
std::vector<float> gen_out_audio;
// code2wav left-context ring: past frames' codes, frame major,
// oldest first, fed to the decode window and extended after every
// generated frame
std::vector<int32_t> gen_ctx_codes;
std::vector<int32_t> gen_out_codes; // decoded PCM samples for the current frame
bool print_timings;
int n_threads;
std::string media_marker;
@@ -354,6 +363,7 @@ struct mtmd_context {
auto res = clip_init(mmproj_fname, ctx_clip_params);
ctx_v = res.ctx_v;
ctx_a = res.ctx_a;
ctx_gen_a = res.ctx_gen_a;
if (!ctx_v && !ctx_a) {
throw std::runtime_error(string_format("Failed to load CLIP model from %s\n", mmproj_fname));
}
@@ -740,6 +750,10 @@ struct mtmd_context {
aud_end = "<|mimo_audio_end|>";
audio_preproc = std::make_unique<mtmd_audio_preprocessor_mimo_audio>(ctx_a);
} break;
case PROJECTOR_TYPE_QWEN3TTS_SPKENC:
{
audio_preproc = std::make_unique<mtmd_audio_preprocessor_qwen3tts_spk>(ctx_a);
} break;
default:
throw std::runtime_error(string_format("%s: unexpected audio projector type %d\n", __func__, proj));
}
@@ -780,6 +794,7 @@ struct mtmd_context {
~mtmd_context() {
clip_free(ctx_a);
clip_free(ctx_v);
clip_free(ctx_gen_a);
}
private:
@@ -1553,6 +1568,95 @@ float * mtmd_get_output_embd(mtmd_context * ctx) {
return ctx->out_embd.data();
}
//
// audio generation
//
mtmd_gen_audio_type mtmd_gen_audio_get_type(const mtmd_context * ctx) {
if (!ctx->ctx_gen_a) {
return MTMD_GEN_AUDIO_TYPE_NONE;
}
switch (clip_get_projector_type(ctx->ctx_gen_a)) {
case PROJECTOR_TYPE_QWEN3TTS_GEN:
return MTMD_GEN_AUDIO_TYPE_MTP;
default:
return MTMD_GEN_AUDIO_TYPE_NONE;
}
}
static int32_t mtmd_gen_audio_impl(mtmd_context * ctx, const mtmd_gen_inp * inp, mtmd_gen_out * out) {
clip_ctx * ctx_clip = ctx->ctx_gen_a;
if (!ctx_clip) {
LOG_ERR("%s: model does not support audio generation\n", __func__);
return 1;
}
const size_t n_embd = (size_t) clip_n_mmproj_embd(ctx_clip);
if (inp->n_embd != n_embd) {
LOG_ERR("%s: n_embd mismatch: model expects %zu, got %zu\n", __func__, n_embd, inp->n_embd);
return 1;
}
clip_image_f32 hidden_state;
hidden_state.set_size({(int) n_embd, 1}, false, true);
hidden_state.cpy_buf(std::vector<float>(inp->embd, inp->embd + n_embd));
clip_image_f32_batch batch;
batch.is_audio = true;
batch.entries.push_back(std::move(hidden_state));
std::vector<float> out_embd(n_embd);
ctx->gen_out_audio.clear();
clip_encode_params params;
params.imgs = &batch;
params.n_threads = ctx->n_threads;
params.out_embd = &out_embd;
params.out_audio = &ctx->gen_out_audio;
params.code0 = inp->code0;
params.top_k = inp->top_k;
params.top_p = inp->top_p;
params.ctx_codes = &ctx->gen_ctx_codes;
params.out_codes = &ctx->gen_out_codes;
if (!clip_encode(ctx_clip, &params)) {
LOG_ERR("%s: clip_encode failed\n", __func__);
return 1;
}
if (!out->embd || out->n_embd != out_embd.size()) {
LOG_ERR("%s: output buffer size mismatch: expected %zu, got %zu\n", __func__, out_embd.size(), out->n_embd);
return 1;
}
std::copy(out_embd.begin(), out_embd.end(), out->embd);
// extend the context ring with this frame's codes, capped to more
// history than the decode window ever reads
ctx->gen_ctx_codes.insert(ctx->gen_ctx_codes.end(), ctx->gen_out_codes.begin(), ctx->gen_out_codes.end());
const size_t max_hist = 64 * ctx->gen_out_codes.size();
if (max_hist > 0 && ctx->gen_ctx_codes.size() > max_hist) {
ctx->gen_ctx_codes.erase(ctx->gen_ctx_codes.begin(),
ctx->gen_ctx_codes.end() - (ptrdiff_t) max_hist);
}
out->audio = ctx->gen_out_audio.data();
out->n_samples = ctx->gen_out_audio.size();
return 0;
}
void mtmd_gen_audio_reset(mtmd_context * ctx) {
ctx->gen_ctx_codes.clear();
}
int32_t mtmd_gen_audio(mtmd_context * ctx, const struct mtmd_gen_inp * inp, struct mtmd_gen_out * out) {
try {
return mtmd_gen_audio_impl(ctx, inp, out);
} catch (const std::exception & e) {
LOG_ERR("%s: error: %s\n", __func__, e.what());
return 1;
}
}
mtmd_batch * mtmd_batch_init(mtmd_context * ctx) {
return new mtmd_batch(ctx);
}
+34
View File
@@ -327,6 +327,40 @@ struct mtmd_caps {
};
MTMD_API struct mtmd_caps mtmd_get_cap_from_file(const char * mmproj_fname);
/////////////////////////////////////////
// EXPERIMENTAL API for audio generation, subjected to breaking changes
enum mtmd_gen_audio_type {
MTMD_GEN_AUDIO_TYPE_NONE, // not supported
MTMD_GEN_AUDIO_TYPE_MTP, // qwen3tts style, with MTP-like generation head
};
MTMD_API enum mtmd_gen_audio_type mtmd_gen_audio_get_type(const mtmd_context * ctx);
struct mtmd_gen_inp {
int32_t code0; // the sampled codebook 0 entry from backbone
float * embd; // the hidden state from backbone, size = n_embd * n_pos
size_t n_embd; // only for validation
// sampling params
int32_t top_k;
float top_p;
};
struct mtmd_gen_out {
float * embd; // the generated hidden state, to be fed back to backbone
size_t n_embd; // only for validation
// out: raw PCM samples (F32) decoded for this frame; owned by mtmd_context,
// valid until the next mtmd_gen_audio() call, caller does not allocate this
const float * audio;
size_t n_samples;
};
MTMD_API int32_t mtmd_gen_audio(mtmd_context * ctx,
const struct mtmd_gen_inp * inp,
struct mtmd_gen_out * out);
// clear the code2wav left-context ring, call before starting a new utterance
MTMD_API void mtmd_gen_audio_reset(mtmd_context * ctx);
/////////////////////////////////////////
// test function, to be used in test-mtmd-c-api.c
+6
View File
@@ -6,3 +6,9 @@ target_compile_features(${TARGET} PRIVATE cxx_std_17)
if(LLAMA_TOOLS_INSTALL)
install(TARGETS ${TARGET} RUNTIME)
endif()
set(TARGET llama-tts-qwen3)
add_executable(${TARGET} tts-qwen3.cpp)
install(TARGETS ${TARGET} RUNTIME)
target_link_libraries(${TARGET} PRIVATE llama mtmd llama-common ${CMAKE_THREAD_LIBS_INIT})
target_compile_features(${TARGET} PRIVATE cxx_std_17)
+328
View File
@@ -0,0 +1,328 @@
// Qwen3-TTS end to end driver: the talker runs as a plain llama model
// (codec rows live in the extended vocab), each generated frame goes
// through mtmd_gen_audio (code predictor + code2wav) and its hidden
// state feedback returns as the next input embedding.
//
// The prompt is the sum of two aligned streams (projected text + codec
// specials); tokens can only pick one embedding row, so the prompt is
// assembled as raw embeddings from rows read straight out of the
// talker GGUF:
//
// role text(<|im_start|>assistant\n) 3 vecs
// prefill tts_pad x4 + tts_bos
// + codec(think, think_bos, lang, think_eos, codec_pad)
// trailing text(utterance) + tts_eos, + codec_pad each
// handoff tts_pad + codec(codec_bos) 1 vec
#include "llama.h"
#include "mtmd.h"
#include "common.h"
#include "log.h"
#include "ggml.h"
#include "gguf.h"
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
// Read one dequantized row of a 2D tensor from a GGUF file.
struct gguf_row_reader {
struct gguf_context * gguf = nullptr;
struct ggml_context * meta = nullptr;
FILE * f = nullptr;
size_t data_off = 0;
bool open(const char * path) {
struct ggml_init_params ip = { 0, nullptr, true };
struct gguf_init_params gp = { true, &meta };
gguf = gguf_init_from_file(path, gp);
if (!gguf) {
return false;
}
data_off = gguf_get_data_offset(gguf);
f = fopen(path, "rb");
return f != nullptr;
}
bool read_row(const char * tensor_name, int64_t row, std::vector<float> & out) {
const int64_t idx = gguf_find_tensor(gguf, tensor_name);
if (idx < 0) {
return false;
}
struct ggml_tensor * t = ggml_get_tensor(meta, tensor_name);
if (!t || row < 0 || row >= t->ne[1]) {
return false;
}
const size_t row_bytes = ggml_row_size(t->type, t->ne[0]);
std::vector<uint8_t> raw(row_bytes);
if (fseek(f, (long) (data_off + gguf_get_tensor_offset(gguf, idx) + (size_t) row * row_bytes), SEEK_SET) != 0) {
return false;
}
if (fread(raw.data(), 1, row_bytes, f) != row_bytes) {
return false;
}
out.resize((size_t) t->ne[0]);
if (t->type == GGML_TYPE_F32) {
memcpy(out.data(), raw.data(), row_bytes);
} else {
const auto * traits = ggml_get_type_traits(t->type);
traits->to_float(raw.data(), out.data(), t->ne[0]);
}
return true;
}
~gguf_row_reader() {
if (f) fclose(f);
if (gguf) gguf_free(gguf);
if (meta) ggml_free(meta);
}
};
static llama_token find_token(const llama_vocab * vocab, const std::string & piece) {
const int32_t n = llama_vocab_n_tokens(vocab);
for (llama_token t = 0; t < n; t++) {
if (piece == llama_vocab_get_text(vocab, t)) {
return t;
}
}
return LLAMA_TOKEN_NULL;
}
static void save_wav16(const char * path, const std::vector<float> & pcm, int rate) {
FILE * f = fopen(path, "wb");
if (!f) {
LOG_ERR("failed to open %s\n", path);
return;
}
const uint32_t data_sz = (uint32_t) (pcm.size() * 2);
const uint32_t riff_sz = 36 + data_sz;
const uint32_t fmt_sz = 16, byte_rate = (uint32_t) rate * 2;
const uint16_t fmt = 1, ch = 1, align = 2, bits = 16;
const uint32_t rate32 = (uint32_t) rate;
fwrite("RIFF", 1, 4, f); fwrite(&riff_sz, 4, 1, f); fwrite("WAVE", 1, 4, f);
fwrite("fmt ", 1, 4, f); fwrite(&fmt_sz, 4, 1, f);
fwrite(&fmt, 2, 1, f); fwrite(&ch, 2, 1, f); fwrite(&rate32, 4, 1, f);
fwrite(&byte_rate, 4, 1, f); fwrite(&align, 2, 1, f); fwrite(&bits, 2, 1, f);
fwrite("data", 1, 4, f); fwrite(&data_sz, 4, 1, f);
for (float v : pcm) {
int16_t s = (int16_t) (std::max(-1.0f, std::min(1.0f, v)) * 32767.0f);
fwrite(&s, 2, 1, f);
}
fclose(f);
}
int main(int argc, char ** argv) {
const char * model_path = nullptr;
const char * mmproj_path = nullptr;
const char * out_path = "output.wav";
std::string text;
std::string lang = "english";
int max_new = 512;
int n_gpu = 999;
for (int i = 1; i < argc; i++) {
auto next = [&](const char * flag) -> const char * {
if (i + 1 >= argc) { fprintf(stderr, "missing value for %s\n", flag); exit(1); }
return argv[++i];
};
if (!strcmp(argv[i], "-m")) model_path = next("-m");
else if (!strcmp(argv[i], "--mmproj")) mmproj_path = next("--mmproj");
else if (!strcmp(argv[i], "-p")) text = next("-p");
else if (!strcmp(argv[i], "-o")) out_path = next("-o");
else if (!strcmp(argv[i], "--lang")) lang = next("--lang");
else if (!strcmp(argv[i], "--max-new")) max_new = atoi(next("--max-new"));
else if (!strcmp(argv[i], "-ngl")) n_gpu = atoi(next("-ngl"));
else {
fprintf(stderr,
"usage: %s -m talker.gguf --mmproj tts.gguf -p \"text\" [-o out.wav] [--lang english] "
"[--max-new n] [-ngl n]\n", argv[0]);
return 1;
}
}
if (!model_path || !mmproj_path || text.empty()) {
fprintf(stderr, "need -m, --mmproj and -p\n");
return 1;
}
llama_backend_init();
llama_model_params mparams = llama_model_default_params();
mparams.n_gpu_layers = n_gpu;
llama_model * model = llama_model_load_from_file(model_path, mparams);
if (!model) { LOG_ERR("failed to load %s\n", model_path); return 1; }
const llama_vocab * vocab = llama_model_get_vocab(model);
const int n_embd = llama_model_n_embd(model);
llama_context_params cparams = llama_context_default_params();
cparams.n_ctx = 4096;
cparams.n_batch = 4096;
cparams.embeddings = true;
llama_context * lctx = llama_init_from_model(model, cparams);
if (!lctx) { LOG_ERR("failed to create context\n"); return 1; }
mtmd_context_params mtmd_params = mtmd_context_params_default();
mtmd_context * mctx = mtmd_init_from_file(mmproj_path, model, mtmd_params);
if (!mctx) { LOG_ERR("failed to load %s\n", mmproj_path); return 1; }
if (mtmd_gen_audio_get_type(mctx) == MTMD_GEN_AUDIO_TYPE_NONE) {
LOG_ERR("mmproj does not support audio generation\n");
return 1;
}
// vocab landmarks: the codec rows sit after the text vocab
const llama_token codec_0 = find_token(vocab, "<|codec_0|>");
const llama_token codec_bos = find_token(vocab, "<|codec_bos|>");
const llama_token codec_eos = find_token(vocab, "<|codec_eos_token|>");
const llama_token codec_pad = find_token(vocab, "<|codec_pad|>");
const llama_token c_think = find_token(vocab, "<|codec_think|>");
const llama_token c_think_b = find_token(vocab, "<|codec_think_bos|>");
const llama_token c_think_e = find_token(vocab, "<|codec_think_eos|>");
const llama_token c_lang = find_token(vocab, ("<|codec_language_" + lang + "|>").c_str());
const llama_token tts_pad = find_token(vocab, "<tts_pad>");
const llama_token tts_bos = find_token(vocab, "<tts_text_bos>");
const llama_token tts_eos = find_token(vocab, "<tts_text_eod>");
for (llama_token t : { codec_0, codec_bos, codec_eos, codec_pad, c_think, c_think_b, c_think_e, c_lang,
tts_pad, tts_bos, tts_eos }) {
if (t == LLAMA_TOKEN_NULL) {
LOG_ERR("missing special token in vocab (lang '%s'?)\n", lang.c_str());
return 1;
}
}
// embedding rows straight from the gguf: the prompt sums two rows
// per position, which tokens cannot express
gguf_row_reader rows;
if (!rows.open(model_path)) { LOG_ERR("failed to open %s for row reads\n", model_path); return 1; }
const char * EMBD = "token_embd.weight";
auto row = [&](llama_token t) {
std::vector<float> v;
if (!rows.read_row(EMBD, t, v)) { LOG_ERR("row read failed for token %d\n", t); exit(1); }
return v;
};
auto sum_row = [&](llama_token a, llama_token b) {
std::vector<float> va = row(a), vb = row(b);
for (size_t i = 0; i < va.size(); i++) va[i] += vb[i];
return va;
};
// upstream wrap, then slices: [0:3] role, [3:-5] utterance body
const std::string full = "<|im_start|>assistant\n" + text + "<|im_end|>\n<|im_start|>assistant\n";
std::vector<llama_token> ids(full.size() + 16);
int n_ids = llama_tokenize(vocab, full.c_str(), (int32_t) full.size(), ids.data(), (int32_t) ids.size(),
false, true);
if (n_ids < 8) { LOG_ERR("tokenization failed\n"); return 1; }
ids.resize((size_t) n_ids);
std::vector<std::vector<float>> prompt;
for (int i = 0; i < 3; i++) prompt.push_back(row(ids[(size_t) i]));
prompt.push_back(sum_row(tts_pad, c_think));
prompt.push_back(sum_row(tts_pad, c_think_b));
prompt.push_back(sum_row(tts_pad, c_lang));
prompt.push_back(sum_row(tts_pad, c_think_e));
prompt.push_back(sum_row(tts_bos, codec_pad));
for (int i = 3; i < n_ids - 5; i++) prompt.push_back(sum_row(ids[(size_t) i], codec_pad));
prompt.push_back(sum_row(tts_eos, codec_pad));
prompt.push_back(sum_row(tts_pad, codec_bos));
const int n_prompt = (int) prompt.size();
LOG_INF("prompt: %d positions (%d text tokens)\n", n_prompt, n_ids);
// the talker rides the qwen3vl interleaved mrope: positions carry
// n_pos_per_embd sections laid out [section * n_tokens + i], all
// equal for a pure text/codec stream
const bool mrope = llama_model_rope_type(model) == LLAMA_ROPE_TYPE_MROPE ||
llama_model_rope_type(model) == LLAMA_ROPE_TYPE_IMROPE;
const int n_pos_sec = mrope ? 4 : 1;
std::vector<llama_pos> pos_buf((size_t) n_pos_sec * (size_t) n_prompt);
// prefill as one embd batch, logits on the last position
std::vector<float> embd_buf((size_t) n_prompt * (size_t) n_embd);
for (int i = 0; i < n_prompt; i++) {
memcpy(embd_buf.data() + (size_t) i * n_embd, prompt[(size_t) i].data(), (size_t) n_embd * sizeof(float));
}
llama_batch batch = llama_batch_init(n_prompt, n_embd, 1);
batch.n_tokens = n_prompt;
batch.pos = pos_buf.data();
memcpy(batch.embd, embd_buf.data(), embd_buf.size() * sizeof(float));
for (int i = 0; i < n_prompt; i++) {
for (int sec = 0; sec < n_pos_sec; sec++) {
pos_buf[(size_t) sec * n_prompt + (size_t) i] = i;
}
batch.n_seq_id[i] = 1;
batch.seq_id[i][0] = 0;
batch.logits[i] = (int8_t) (i == n_prompt - 1);
}
if (llama_decode(lctx, batch) != 0) { LOG_ERR("prefill decode failed\n"); return 1; }
// the text stream keeps flowing during generation: the input after
// frame k adds trailing text row k on top of the codes embedding,
// then tts_eos, then tts_pad once the utterance is spent
std::vector<std::vector<float>> overlay;
for (int i = 3; i < n_ids - 5; i++) overlay.push_back(row(ids[(size_t) i]));
overlay.push_back(row(tts_eos));
overlay.push_back(row(tts_pad));
// AR loop: sample c0 among the semantic codec rows plus eos, hand
// the hidden state to the generator, feed its embedding back
mtmd_gen_audio_reset(mctx);
std::vector<float> audio;
std::vector<float> h((size_t) n_embd), fb((size_t) n_embd);
int n_frames = 0;
int pos = n_prompt;
for (; n_frames < max_new; n_frames++) {
const float * logits = llama_get_logits_ith(lctx, -1);
llama_token best = codec_eos;
float bestv = logits[codec_eos];
for (llama_token t = codec_0; t < codec_0 + 2048; t++) {
if (logits[t] > bestv) { bestv = logits[t]; best = t; }
}
if (best == codec_eos) {
break;
}
const float * he = llama_get_embeddings_ith(lctx, -1);
memcpy(h.data(), he, (size_t) n_embd * sizeof(float));
mtmd_gen_inp inp = {};
inp.code0 = best - codec_0;
inp.embd = h.data();
inp.n_embd = (size_t) n_embd;
inp.top_k = 50;
inp.top_p = 1.0f;
mtmd_gen_out out = {};
out.embd = fb.data();
out.n_embd = (size_t) n_embd;
if (mtmd_gen_audio(mctx, &inp, &out) != 0) { LOG_ERR("mtmd_gen_audio failed\n"); return 1; }
audio.insert(audio.end(), out.audio, out.audio + out.n_samples);
const auto & ov = overlay[std::min((size_t) n_frames, overlay.size() - 1)];
for (int i = 0; i < n_embd; i++) {
fb[(size_t) i] += ov[(size_t) i];
}
batch.n_tokens = 1;
memcpy(batch.embd, fb.data(), (size_t) n_embd * sizeof(float));
for (int sec = 0; sec < n_pos_sec; sec++) {
pos_buf[(size_t) sec] = pos;
}
pos++;
batch.n_seq_id[0] = 1;
batch.seq_id[0][0] = 0;
batch.logits[0] = 1;
if (llama_decode(lctx, batch) != 0) { LOG_ERR("decode failed at frame %d\n", n_frames); return 1; }
}
LOG_INF("generated %d frames, %zu samples (%.2f s)\n", n_frames, audio.size(), (double) audio.size() / 24000.0);
save_wav16(out_path, audio, 24000);
LOG_INF("wrote %s\n", out_path);
batch.pos = nullptr;
llama_batch_free(batch);
mtmd_free(mctx);
llama_free(lctx);
llama_model_free(model);
llama_backend_free();
return 0;
}