Merge commit '2fb989b9e79bf4da8159855e24892c8f4c20300f' into concedo_experimental

# Conflicts:
#	.github/actions/ccache-clear/action.yml
#	.github/workflows/build-cpu.yml
#	.github/workflows/make-release.yml
#	.github/workflows/release.yml
#	.pi/gg/SYSTEM.md
#	AGENTS.md
#	README.md
#	docs/build.md
#	ggml/src/ggml-opencl/CMakeLists.txt
#	ggml/src/ggml-opencl/ggml-opencl.cpp
#	ggml/src/ggml-opencl/kernels/moe_combine.cl
#	ggml/src/ggml-sycl/convert.cpp
#	ggml/src/ggml-sycl/dequantize.hpp
#	ggml/src/ggml-sycl/dmmv.cpp
#	ggml/src/ggml-sycl/esimd.hpp
#	ggml/src/ggml-sycl/ggml-sycl.cpp
#	ggml/src/ggml-sycl/mmvq.cpp
#	ggml/src/ggml-sycl/quants.hpp
#	ggml/src/ggml-sycl/vecdotq.hpp
#	src/CMakeLists.txt
#	tests/test-llama-archs.cpp
#	tools/llama-bench/llama-bench.cpp
#	tools/mtmd/CMakeLists.txt
This commit is contained in:
Concedo
2026-08-25 20:30:15 +08:00
41 changed files with 2181 additions and 136 deletions
+1
View File
@@ -33,6 +33,7 @@ int llama_fit_params(int argc, char ** argv) {
if (!params.fit_params_print) {
const common_params_fit_status status = common_fit_params(params.model.path.c_str(), &mparams, &cparams,
params.tensor_split, params.tensor_buft_overrides.data(), params.fit_params_target.data(), params.fit_params_min_ctx,
nullptr,
params.verbosity >= LOG_LEVEL_DEBUG ? GGML_LOG_LEVEL_DEBUG : GGML_LOG_LEVEL_ERROR);
if (status != COMMON_PARAMS_FIT_STATUS_SUCCESS) {
LOG_ERR("%s: failed to fit CLI arguments to free memory, exiting...\n", __func__);
+6
View File
@@ -120,6 +120,12 @@ struct clip_graph {
ffn_op_type type_op,
int il) const;
ggml_tensor * build_moe_ffn(
ggml_tensor * cur,
const clip_layer & layer,
ffn_op_type type_op,
int il) const;
ggml_tensor * build_attn(
ggml_tensor * wo,
ggml_tensor * wo_b,
+10 -1
View File
@@ -75,6 +75,7 @@
#define KEY_SAM_N_HEAD "clip.vision.sam.head_count"
#define KEY_SAM_N_BLOCK "clip.vision.sam.block_count"
#define KEY_SAM_N_EMBD "clip.vision.sam.embedding_length"
#define KEY_VISION_N_EXPERT_USED "clip.vision.expert_used_count"
// audio-specific
#define KEY_AUDIO_PROJ_TYPE "clip.audio.projector_type" // for models with mixed modalities
#define KEY_A_NUM_MEL_BINS "clip.audio.num_mel_bins"
@@ -119,7 +120,11 @@
#define TN_FFN_DOWN "%s.blk.%d.ffn_down.%s"
#define TN_FFN_GATE "%s.blk.%d.ffn_gate.%s"
#define TN_FFN_UP "%s.blk.%d.ffn_up.%s"
#define TN_FFN_GATE "%s.blk.%d.ffn_gate.%s"
#define TN_FFN_GATE_INP "%s.blk.%d.ffn_gate_inp.%s" // MoE router (dots3note)
#define TN_FFN_GATE_EXPS "%s.blk.%d.ffn_gate_exps.%s"
#define TN_FFN_UP_EXPS "%s.blk.%d.ffn_up_exps.%s"
#define TN_FFN_DOWN_EXPS "%s.blk.%d.ffn_down_exps.%s"
#define TN_FFN_EXP_PROBS_B "%s.blk.%d.exp_probs_b.%s"
#define TN_LN_1 "%s.blk.%d.ln1.%s" // layer norm
#define TN_LN_2 "%s.blk.%d.ln2.%s" // layer norm
#define TN_LS_1 "%s.blk.%d.ls1.%s" // layer scale
@@ -471,6 +476,8 @@ enum projector_type {
PROJECTOR_TYPE_COGVLM,
PROJECTOR_TYPE_JANUS_PRO,
PROJECTOR_TYPE_DOTS_OCR,
PROJECTOR_TYPE_DOTS3NOTE_V,
PROJECTOR_TYPE_DOTS3NOTE_A,
PROJECTOR_TYPE_DEEPSEEKOCR,
PROJECTOR_TYPE_DEEPSEEKOCR2,
PROJECTOR_TYPE_LFM2A,
@@ -533,6 +540,8 @@ static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = {
{ PROJECTOR_TYPE_COGVLM, "cogvlm"},
{ PROJECTOR_TYPE_JANUS_PRO, "janus_pro"},
{ PROJECTOR_TYPE_DOTS_OCR, "dots_ocr"},
{ PROJECTOR_TYPE_DOTS3NOTE_V, "dots3note_v"},
{ PROJECTOR_TYPE_DOTS3NOTE_A, "dots3note_a"},
{ PROJECTOR_TYPE_DEEPSEEKOCR, "deepseekocr"},
{ PROJECTOR_TYPE_DEEPSEEKOCR2, "deepseekocr2"},
{ PROJECTOR_TYPE_LFM2A, "lfm2a"},
+8
View File
@@ -93,6 +93,7 @@ struct clip_hparams {
float eps = 1e-6;
float rope_theta = 0.0;
int32_t n_expert_used = 0;
std::vector<int32_t> feature_layers;
int32_t attn_window_size = 0;
int32_t n_wa_pattern = 0;
@@ -259,6 +260,13 @@ struct clip_layer {
ggml_tensor * ff_down_w = nullptr;
ggml_tensor * ff_down_b = nullptr;
// MoE FFN (dots3note vision pyramid blocks)
ggml_tensor * ff_gate_inp_w = nullptr;
ggml_tensor * ff_gate_exps_w = nullptr;
ggml_tensor * ff_up_exps_w = nullptr;
ggml_tensor * ff_down_exps_w = nullptr;
ggml_tensor * ff_exp_probs_b = nullptr;
// layernorm 2 (or pre-FFN norm)
ggml_tensor * ln_2_w = nullptr;
ggml_tensor * ln_2_b = nullptr;
+123 -7
View File
@@ -44,6 +44,7 @@
#include "models/cogvlm.cpp"
#include "models/conformer.cpp"
#include "models/dotsocr.cpp"
#include "models/dots3note.cpp"
#include "models/exaone4_5.cpp"
#include "models/gemma4a.cpp"
#include "models/gemma4v.cpp"
@@ -571,11 +572,13 @@ ggml_tensor * clip_graph::build_vit(
cb(cur, "ffn_inp_normed", il);
// ffn
cur = build_ffn(cur,
layer.ff_up_w, layer.ff_up_b,
layer.ff_gate_w, layer.ff_gate_b,
layer.ff_down_w, layer.ff_down_b,
ffn_t, il);
cur = layer.ff_gate_exps_w
? build_moe_ffn(cur, layer, ffn_t, il)
: build_ffn(cur,
layer.ff_up_w, layer.ff_up_b,
layer.ff_gate_w, layer.ff_gate_b,
layer.ff_down_w, layer.ff_down_b,
ffn_t, il);
cb(cur, "ffn_out", il);
@@ -756,6 +759,50 @@ ggml_tensor * clip_graph::build_ffn(
return cur;
}
// MoE FFN with sigmoid router and normalized top-k weights (dots3note vision)
// the router runs in fp32; exp_probs_b only affects expert selection, not the weights
ggml_tensor * clip_graph::build_moe_ffn(ggml_tensor * cur, const clip_layer & layer, ffn_op_type type_op, int il) const {
const int64_t n_tokens = cur->ne[1];
const int64_t n_expert = layer.ff_gate_exps_w->ne[2];
const int64_t n_expert_used = std::min((int64_t) hparams.n_expert_used, n_expert);
GGML_ASSERT(n_expert_used > 0);
GGML_ASSERT(type_op == FFN_SILU);
ggml_tensor * probs = ggml_sigmoid(ctx0, build_mm(layer.ff_gate_inp_w, cur)); // [n_expert, n_tokens]
cb(probs, "ffn_moe_probs", il);
ggml_tensor * sel = layer.ff_exp_probs_b
? ggml_add(ctx0, probs, layer.ff_exp_probs_b)
: probs;
ggml_tensor * selected = ggml_top_k(ctx0, sel, n_expert_used); // [n_expert_used, n_tokens]
ggml_tensor * weights = ggml_get_rows(ctx0,
ggml_reshape_3d(ctx0, probs, 1, n_expert, n_tokens), selected);
weights = ggml_reshape_2d(ctx0, weights, n_expert_used, n_tokens);
weights = ggml_div(ctx0, weights, ggml_sum_rows(ctx0, weights));
weights = ggml_reshape_3d(ctx0, weights, 1, n_expert_used, n_tokens);
cb(weights, "ffn_moe_weights", il);
cur = ggml_reshape_3d(ctx0, cur, cur->ne[0], 1, n_tokens);
ggml_tensor * gate = ggml_mul_mat_id(ctx0, layer.ff_gate_exps_w, cur, selected); // [n_ff, n_expert_used, n_tokens]
ggml_tensor * up = ggml_mul_mat_id(ctx0, layer.ff_up_exps_w, cur, selected);
cur = ggml_mul(ctx0, ggml_silu(ctx0, gate), up);
cur = ggml_mul_mat_id(ctx0, layer.ff_down_exps_w, cur, selected); // [n_embd, n_expert_used, n_tokens]
cur = ggml_mul(ctx0, cur, weights);
// sum over the selected experts
ggml_tensor * out = nullptr;
for (int64_t i = 0; i < n_expert_used; i++) {
ggml_tensor * v = ggml_view_2d(ctx0, cur, cur->ne[0], n_tokens, cur->nb[2], i * cur->nb[1]);
out = out ? ggml_add(ctx0, out, v) : v;
}
if (n_expert_used == 1) {
out = ggml_cont(ctx0, out);
}
cb(out, "ffn_moe_out", il);
return out;
}
ggml_tensor * clip_graph::build_attn(
ggml_tensor * wo,
ggml_tensor * wo_b,
@@ -990,9 +1037,14 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const
builder = std::make_unique<clip_graph_pixtral>(ctx, img);
} break;
case PROJECTOR_TYPE_DOTS_OCR:
case PROJECTOR_TYPE_DOTS3NOTE_V: // same ViT + merger; pyramid MoE is handled by build_vit
{
builder = std::make_unique<clip_graph_dotsocr>(ctx, img);
} break;
case PROJECTOR_TYPE_DOTS3NOTE_A:
{
builder = std::make_unique<clip_graph_dots3note_a>(ctx, img);
} break;
case PROJECTOR_TYPE_QWEN2VL:
case PROJECTOR_TYPE_QWEN25VL:
{
@@ -1590,6 +1642,25 @@ struct clip_model_loader {
get_u32(KEY_IMAGE_MAX_PIXELS, hparams.image_max_pixels);
hparams.set_warmup_n_tokens(46*46); // avoid OOM on warmup
} break;
case PROJECTOR_TYPE_DOTS3NOTE_V:
{
hparams.rope_theta = 10000.0f;
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC_PILLOW;
get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge);
get_u32(KEY_IMAGE_MIN_PIXELS, hparams.image_min_pixels);
get_u32(KEY_IMAGE_MAX_PIXELS, hparams.image_max_pixels);
get_u32(KEY_VISION_N_EXPERT_USED, hparams.n_expert_used);
hparams.set_warmup_n_tokens(46*46); // avoid OOM on warmup
} break;
case PROJECTOR_TYPE_DOTS3NOTE_A:
{
hparams.rope_theta = 10000.0f;
hparams.audio_chunk_len = 60; // in seconds
hparams.audio_sample_rate = 16000;
hparams.audio_n_fft = 400;
hparams.audio_window_len = 400;
hparams.audio_hop_len = 160;
} break;
case PROJECTOR_TYPE_KIMIVL:
{
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
@@ -2280,12 +2351,20 @@ struct clip_model_loader {
layer.ln_1_b = get_tensor(string_format(TN_LN_1, prefix, il, "bias"), false);
layer.ln_2_b = get_tensor(string_format(TN_LN_2, prefix, il, "bias"), false);
// MoE ffn (dots3note vision pyramid blocks); replaces the dense ffn when present
layer.ff_gate_inp_w = get_tensor(string_format(TN_FFN_GATE_INP, prefix, il, "weight"), false);
layer.ff_gate_exps_w = get_tensor(string_format(TN_FFN_GATE_EXPS, prefix, il, "weight"), false);
layer.ff_up_exps_w = get_tensor(string_format(TN_FFN_UP_EXPS, prefix, il, "weight"), false);
layer.ff_down_exps_w = get_tensor(string_format(TN_FFN_DOWN_EXPS, prefix, il, "weight"), false);
layer.ff_exp_probs_b = get_tensor(string_format(TN_FFN_EXP_PROBS_B, prefix, il, "weight"), false);
const bool is_moe = layer.ff_gate_exps_w != nullptr;
// ffn
layer.ff_up_w = get_tensor(string_format(TN_FFN_UP, prefix, il, "weight"));
layer.ff_up_w = get_tensor(string_format(TN_FFN_UP, prefix, il, "weight"), !is_moe);
layer.ff_up_b = get_tensor(string_format(TN_FFN_UP, prefix, il, "bias"), false);
layer.ff_gate_w = get_tensor(string_format(TN_FFN_GATE, prefix, il, "weight"), false);
layer.ff_gate_b = get_tensor(string_format(TN_FFN_GATE, prefix, il, "bias"), false);
layer.ff_down_w = get_tensor(string_format(TN_FFN_DOWN, prefix, il, "weight"));
layer.ff_down_w = get_tensor(string_format(TN_FFN_DOWN, prefix, il, "weight"), !is_moe);
layer.ff_down_b = get_tensor(string_format(TN_FFN_DOWN, prefix, il, "bias"), false);
// mimovl per-head attention sink bias
@@ -2767,6 +2846,7 @@ struct clip_model_loader {
model.mm_patch_merger_w = get_tensor(string_format(TN_MM_PATCH_MERGER, "weight"), false);
} break;
case PROJECTOR_TYPE_DOTS_OCR:
case PROJECTOR_TYPE_DOTS3NOTE_V:
{
model.mm_0_w = get_tensor(string_format(TN_LLAVA_PROJ, 0, "weight"));
model.mm_0_b = get_tensor(string_format(TN_LLAVA_PROJ, 0, "bias"));
@@ -2777,6 +2857,23 @@ struct clip_model_loader {
// post_trunk_norm: applied after all ViT blocks, before the merger
model.post_ln_w = get_tensor(string_format(TN_MM_POST_NORM, "weight"));
} break;
case PROJECTOR_TYPE_DOTS3NOTE_A:
{
model.conv2d_1_w = get_tensor(string_format(TN_CONV2D, 1, "weight"));
model.conv2d_1_b = get_tensor(string_format(TN_CONV2D, 1, "bias"));
model.conv2d_2_w = get_tensor(string_format(TN_CONV2D, 2, "weight"));
model.conv2d_2_b = get_tensor(string_format(TN_CONV2D, 2, "bias"));
model.conv2d_3_w = get_tensor(string_format(TN_CONV2D, 3, "weight"));
model.conv2d_3_b = get_tensor(string_format(TN_CONV2D, 3, "bias"));
model.conv_out_w = get_tensor(string_format(TN_CONV_OUT, "weight")); // no bias
// adapter: LayerNorm -> Linear -> GELU -> Linear
model.mm_norm_pre_w = get_tensor(string_format(TN_MM_NORM_PRE, "weight"));
model.mm_norm_pre_b = get_tensor(string_format(TN_MM_NORM_PRE, "bias"));
model.mm_1_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 1, "weight"));
model.mm_1_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 1, "bias"));
model.mm_2_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 3, "weight"));
model.mm_2_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 3, "bias"));
} break;
case PROJECTOR_TYPE_ULTRAVOX:
{
model.conv1d_1_w = get_tensor(string_format(TN_CONV1D, 1, "weight"));
@@ -4165,12 +4262,18 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) {
} break;
case PROJECTOR_TYPE_PADDLEOCR:
case PROJECTOR_TYPE_DOTS_OCR:
case PROJECTOR_TYPE_DOTS3NOTE_V:
{
// dynamic size
int n_merge = ctx->model.hparams.n_merge;
int stride = n_merge * n_merge;
n_patches = CLIP_ALIGN(n_patches, stride) / stride;
} break;
case PROJECTOR_TYPE_DOTS3NOTE_A:
{
// 3x stride-2 conv2d over mel frames
n_patches = (img->nx() + 7) / 8;
} break;
case PROJECTOR_TYPE_PIXTRAL:
case PROJECTOR_TYPE_LIGHTONOCR:
{
@@ -4817,6 +4920,7 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
set_input_i32("minimax_pos_w", pos_w);
} break;
case PROJECTOR_TYPE_DOTS_OCR:
case PROJECTOR_TYPE_DOTS3NOTE_V:
{
const int pw = image_size_width / patch_size;
const int ph = image_size_height / patch_size;
@@ -5307,6 +5411,16 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
}
set_input_i32("pos_w", pos_data);
} break;
case PROJECTOR_TYPE_DOTS3NOTE_A:
{
GGML_ASSERT(imgs.entries.size() == 1);
const int n_pos = (imgs.entries.front().nx() + 7) / 8; // 3x stride-2 conv2d
std::vector<int32_t> positions(n_pos);
for (int i = 0; i < n_pos; i++) {
positions[i] = i;
}
set_input_i32("positions", positions);
} break;
case PROJECTOR_TYPE_GEMMA4A:
{
GGML_ASSERT(imgs.entries.size() == 1);
@@ -5803,6 +5917,8 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {
case PROJECTOR_TYPE_PIXTRAL:
case PROJECTOR_TYPE_LIGHTONOCR:
case PROJECTOR_TYPE_DOTS_OCR:
case PROJECTOR_TYPE_DOTS3NOTE_V:
case PROJECTOR_TYPE_DOTS3NOTE_A:
return ctx->model.mm_2_w->ne[1];
case PROJECTOR_TYPE_MLP_NORM:
return ctx->model.mm_3_b->ne[0];
+61
View File
@@ -0,0 +1,61 @@
#include "models.h"
ggml_cgraph * clip_graph_dots3note_a::build() {
// inp_raw: [n_frames, n_mel, 1], one 60s chunk, mel frames not padded
// the reference impl zero-masks conv inputs beyond the valid length at each stage;
// running on exactly the valid frames with the convs' zero padding is equivalent
ggml_tensor * inp = build_inp_raw(1);
GGML_ASSERT(inp->type == GGML_TYPE_F32);
// 3x conv2d (k=3, s=2, p=1) + gelu
{
auto conv_block = [&](ggml_tensor * x, ggml_tensor * w, ggml_tensor * b) {
x = ggml_conv_2d(ctx0, w, x, 2, 2, 1, 1, 1, 1);
x = ggml_add(ctx0, x, ggml_reshape_4d(ctx0, b, 1, 1, x->ne[2], 1));
return ggml_gelu_erf(ctx0, x);
};
inp = conv_block(inp, model.conv2d_1_w, model.conv2d_1_b);
inp = conv_block(inp, model.conv2d_2_w, model.conv2d_2_b);
inp = conv_block(inp, model.conv2d_3_w, model.conv2d_3_b);
// inp: [OW=n_frames/8, OH=n_mel/8, OC=480, 1]
cb(inp, "after_conv_stem", -1);
}
// [OW, OH, OC, 1] -> [OH*OC, OW], feature index f + OH*c (matches the reference permute+reshape)
inp = ggml_cont(ctx0, ggml_permute(ctx0, inp, 2, 0, 1, 3));
inp = ggml_reshape_2d(ctx0, inp, inp->ne[0] * inp->ne[1], inp->ne[2]);
// project to d_model (no bias)
inp = ggml_mul_mat(ctx0, model.conv_out_w, inp);
cb(inp, "after_conv_out", -1);
const int64_t n_pos = inp->ne[1];
ggml_tensor * positions = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_pos);
ggml_set_name(positions, "positions");
ggml_set_input(positions);
// partial rotary: first half of each head, NEOX style
auto add_pos = [&](ggml_tensor * cur, const clip_layer &) {
return ggml_rope_ext(ctx0, cur, positions, nullptr, d_head/2,
GGML_ROPE_TYPE_NEOX, 0, hparams.rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
};
ggml_tensor * cur = build_vit(inp, n_pos,
NORM_TYPE_RMS, hparams.ffn_op,
nullptr, add_pos);
cb(cur, "after_transformer", -1);
// adapter: LayerNorm -> Linear -> GELU -> Linear
cur = build_norm(cur, model.mm_norm_pre_w, model.mm_norm_pre_b, NORM_TYPE_NORMAL, 1e-5, -1);
cur = build_ffn(cur,
model.mm_1_w, model.mm_1_b,
nullptr, nullptr,
model.mm_2_w, model.mm_2_b,
FFN_GELU_ERF, -1);
cb(cur, "projected", -1);
ggml_build_forward_expand(gf, cur);
return gf;
}
+5
View File
@@ -119,6 +119,11 @@ struct clip_graph_dotsocr : clip_graph {
ggml_cgraph * build() override;
};
struct clip_graph_dots3note_a : clip_graph {
clip_graph_dots3note_a(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
ggml_cgraph * build() override;
};
struct clip_graph_cogvlm : clip_graph {
clip_graph_cogvlm(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
ggml_cgraph * build() override;
+94
View File
@@ -723,6 +723,100 @@ bool mtmd_audio_preprocessor_qwen3a::preprocess(const float * sa
return true;
}
//
// mtmd_audio_preprocessor_dots3note
//
// Matches Dots3NoteFeatureExtractor: the waveform is split into 60s chunks and each chunk gets
// its own whisper-style log-mel (center=True, log10 + (max-8)/4). Only sample_length//hop frames
// per chunk are valid; the reference masks everything beyond them, so we emit exactly that many.
//
void mtmd_audio_preprocessor_dots3note::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_dots3note::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());
const int pad = hparams.audio_n_fft / 2; // center=True padding
const int hop = hparams.audio_hop_len;
const size_t chunk_samples = (size_t) hparams.audio_chunk_len * hparams.audio_sample_rate;
for (size_t start = 0; start < n_samples; start += chunk_samples) {
const size_t n_chunk = std::min(chunk_samples, n_samples - start);
const float * chunk = samples + start;
const int64_t n_valid = n_chunk / hop;
if (n_valid == 0) {
continue; // sub-hop tail, contributes no frames
}
// reflect-pad the start; the reference zero-pads partial chunks to 60s before the STFT,
// so a partial chunk sees zeros past its end while a full chunk reflects its own tail
std::vector<float> padded(n_chunk + 2 * pad, 0.0f);
for (int i = 0; i < pad; i++) {
int src = pad - i;
padded[i] = (src < (int) n_chunk) ? chunk[src] : 0.0f;
}
std::copy(chunk, chunk + n_chunk, padded.begin() + pad);
if (n_chunk == chunk_samples) {
for (int i = 0; i < pad; i++) {
int src = (int) n_chunk - 2 - i;
padded[n_chunk + pad + i] = (src >= 0) ? chunk[src] : 0.0f;
}
}
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 = hop;
params.sample_rate = hparams.audio_sample_rate;
params.no_padding = true; // padding already applied above
params.use_natural_log = false;
mtmd_audio_mel mel_full;
if (!log_mel_spectrogram(padded.data(), (int) padded.size(), 4, params, cache, mel_full)) {
return false;
}
GGML_ASSERT(mel_full.n_len >= n_valid);
// per-chunk whisper-style normalization, then keep only the valid frames
mtmd_audio_mel out;
out.n_mel = mel_full.n_mel;
out.n_len = n_valid;
out.n_len_org = n_valid;
out.data.resize((size_t) out.n_mel * (size_t) out.n_len);
double mmax = -1e20;
for (int64_t m = 0; m < out.n_mel; m++) {
for (int64_t t = 0; t < n_valid; t++) {
mmax = std::max(mmax, (double) mel_full.data[(size_t) m * mel_full.n_len + t]);
}
}
mmax -= 8.0;
for (int64_t m = 0; m < out.n_mel; m++) {
for (int64_t t = 0; t < n_valid; t++) {
const double v = std::max((double) mel_full.data[(size_t) m * mel_full.n_len + t], mmax);
out.data[(size_t) m * n_valid + t] = (float) ((v + 4.0) / 4.0);
}
}
output.push_back(std::move(out));
}
return !output.empty();
}
//
// mtmd_audio_preprocessor_mimo_audio
//
+9
View File
@@ -111,6 +111,15 @@ struct mtmd_audio_preprocessor_qwen3a : mtmd_audio_preprocessor {
mtmd_audio_cache cache;
};
struct mtmd_audio_preprocessor_dots3note : mtmd_audio_preprocessor {
mtmd_audio_preprocessor_dots3note(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_mimo_audio : mtmd_audio_preprocessor {
mtmd_audio_preprocessor_mimo_audio(const clip_ctx * ctx) : mtmd_audio_preprocessor(ctx) {}
void initialize() override;
+49
View File
@@ -359,6 +359,15 @@ static bool decode_audio_from_buf(const unsigned char * buf_in, size_t len, int
} // namespace audio_helpers
static bool is_webp_file(const unsigned char * buf, size_t len) {
// WEBP ref: https://developers.google.com/speed/webp/docs/riff_container
return len >= 12 && memcmp(buf, "RIFF", 4) == 0 && memcmp(buf + 8, "WEBP", 4) == 0;
}
#ifdef MTMD_VIDEO
static mtmd_bitmap * decode_webp_with_ffmpeg(mtmd_context * mctx, const unsigned char * buf, size_t len, bool placeholder);
#endif
mtmd_helper_bitmap_wrapper mtmd_helper_bitmap_init_from_buf(mtmd_context * ctx, const unsigned char * buf, size_t len, bool placeholder) {
// calculate the hash if needed
std::string id;
@@ -398,6 +407,19 @@ mtmd_helper_bitmap_wrapper mtmd_helper_bitmap_init_from_buf(mtmd_context * ctx,
// otherwise, fallthrough to video decoding (if supported)
}
#ifdef MTMD_VIDEO
// stb_image does not support webp; decode it with ffmpeg as a single frame
if (!result && is_webp_file(buf, len)) {
result = decode_webp_with_ffmpeg(ctx, buf, len, placeholder);
if (!result) {
LOG_ERR("%s: failed to decode webp buffer\n", __func__);
return {nullptr, nullptr};
}
mtmd_bitmap_set_id(result, id.empty() ? nullptr : id.c_str());
return {result, nullptr};
}
#endif
// last try: load as video
#ifdef MTMD_VIDEO
if (!result) {
@@ -821,6 +843,33 @@ static std::string video_resolve_bin(const char * bin_dir, const char * name) {
return result;
}
#ifdef MTMD_VIDEO
static mtmd_bitmap * decode_webp_with_ffmpeg(mtmd_context * mctx, const unsigned char * buf, size_t len, bool placeholder) {
auto params = mtmd_helper_video_init_params_default();
mtmd_helper_video vctx;
vctx.mctx = mctx;
vctx.input_buf.assign(buf, buf + len);
vctx.ffmpeg_bin = video_resolve_bin(params.ffmpeg_bin_dir, "ffmpeg");
vctx.ffprobe_bin = video_resolve_bin(params.ffmpeg_bin_dir, "ffprobe");
if (!vctx.probe(0.0f)) {
return nullptr;
}
if (placeholder) {
return mtmd_bitmap_init(vctx.info.width, vctx.info.height, nullptr);
}
// still image: the fps filter would output no frame, so disable it
vctx.fps_target = 0.0f;
if (!vctx.start_ffmpeg(0.0f)) {
return nullptr;
}
mtmd_bitmap * frame = vctx.read_next_frame();
if (frame) {
mtmd_bitmap_set_mergeable(frame, false);
}
return frame;
}
#endif
mtmd_helper_video * mtmd_helper_video_init(
mtmd_context * mctx,
const char * path,
+1
View File
@@ -45,6 +45,7 @@ MTMD_API struct mtmd_helper_bitmap_wrapper mtmd_helper_bitmap_init_from_file(mtm
// helper function to construct a mtmd_bitmap from a buffer containing a file
// supported formats:
// image: formats supported by stb_image: jpg, png, bmp, gif, etc.
// webp is decoded via ffmpeg, requires MTMD_VIDEO build with ffmpeg in PATH
// audio: formats supported by miniaudio: wav, mp3, flac
// note:
// - for now, video input is only supported via C++ helper functions
+8
View File
@@ -825,6 +825,7 @@ struct mtmd_context {
image_preproc = std::make_unique<mtmd_image_preprocessor_longest_edge>(ctx_v);
} break;
case PROJECTOR_TYPE_DOTS_OCR:
case PROJECTOR_TYPE_DOTS3NOTE_V:
{
// <|img|> ... (image embeddings) ... <|endofimg|>
img_beg = "<|img|>";
@@ -976,6 +977,13 @@ struct mtmd_context {
aud_end = "<audio|>";
audio_preproc = std::make_unique<mtmd_audio_preprocessor_gemma4ua>(ctx_a);
} break;
case PROJECTOR_TYPE_DOTS3NOTE_A:
{
// <|audio_comp_start|> ... (embeddings) ... <|audio_comp_end|>
aud_beg = "<|audio_comp_start|>";
aud_end = "<|audio_comp_end|>";
audio_preproc = std::make_unique<mtmd_audio_preprocessor_dots3note>(ctx_a);
} break;
case PROJECTOR_TYPE_MIMO_AUDIO:
{
aud_beg = "<|mimo_audio_start|>";
+1 -56
View File
@@ -1040,62 +1040,7 @@ private:
}
}
// optionally reserve VRAM for the draft / MTP context before fitting the target model
if (params_base.fit_params) {
if (has_spec) {
// MTP draft context lives on the target model, only context+compute are new
bool measure_model_bytes = has_draft;
common_params params_dft = common_base_params_to_speculative(params_base);
auto mparams_dft = common_model_params_to_llama(params_dft);
auto cparams_dft = common_context_params_to_llama(params_dft);
if (spec_mtp) {
cparams_dft.ctx_type = LLAMA_CONTEXT_TYPE_MTP;
}
cparams_dft.n_rs_seq = 0;
std::vector<ggml_backend_dev_t> devs;
uint32_t hp_ngl = 0;
uint32_t hp_nct = 0;
uint32_t hp_nex = 0;
try {
auto dmd = common_get_device_memory_data(
params_dft.model.path.c_str(), &mparams_dft, &cparams_dft,
devs, hp_ngl, hp_nct, hp_nex, GGML_LOG_LEVEL_ERROR);
GGML_ASSERT(!params_base.fit_params_target.empty());
size_t total = 0;
std::vector<ggml_backend_dev_t> tgt_devices = params.devices;
if (tgt_devices.empty()) {
for(size_t i = 0; i < ggml_backend_dev_count(); ++i) {
tgt_devices.push_back(ggml_backend_dev_get(i));
}
}
for (size_t j = 0; j < devs.size(); ++j) {
const size_t bytes = (measure_model_bytes ? dmd[j].model : 0) + dmd[j].context + dmd[j].compute;
total += bytes;
for (size_t i = 0; i < tgt_devices.size(); i++) {
if (tgt_devices[i] == devs[j]) {
SRV_DBG("[spec] adding %.2f MiB to fit_params_target for device %s\n",
bytes / (1024.0 * 1024.0), ggml_backend_dev_name(devs[j]));
params_base.fit_params_target[i] += bytes;
break;
}
}
}
SRV_TRC("[spec] estimated memory usage of %s is %.2f MiB\n",
has_draft ? "draft model" : "MTP context",
total / (1024.0 * 1024.0));
} catch (const std::exception & e) {
SRV_WRN("[spec] failed to measure %s memory: %s\n",
has_draft ? "draft model" : "MTP context", e.what());
}
}
}
// note: the draft / MTP context is fitted together with the target model, see common_fit_extra_model
// attach a progress callback
{