Skip to content

Commit 72e60f5

Browse files
authored
mtmd: add chunks and fix preproc for qwen3a (ggml-org#23073)
* mtmd: add chunks and fix preproc for qwen3a * add attn_mask * limit mtmd_chunk size (avoid blow up memory) * correct audio tokens * re-order the set_input case * remove attn_mask
1 parent 8be1786 commit 72e60f5

7 files changed

Lines changed: 197 additions & 68 deletions

File tree

tools/mtmd/clip-graph.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
#define DEFAULT_INTERPOLATION_MODE (GGML_SCALE_MODE_BILINEAR | GGML_SCALE_FLAG_ANTIALIAS)
1313

14+
struct build_vit_opts {
15+
ggml_tensor * attn_mask = nullptr;
16+
};
17+
1418
struct clip_graph {
1519
const clip_model & model;
1620
const clip_hparams & hparams;
@@ -63,7 +67,8 @@ struct clip_graph {
6367
norm_type norm_t,
6468
ffn_op_type ffn_t,
6569
ggml_tensor * learned_pos_embd,
66-
std::function<ggml_tensor *(ggml_tensor *, const clip_layer &)> add_pos);
70+
std::function<ggml_tensor *(ggml_tensor *, const clip_layer &)> add_pos,
71+
const build_vit_opts & opts = {});
6772

6873
// build the input after conv2d (inp_raw --> patches)
6974
// returns tensor with shape [n_embd, n_patches]

tools/mtmd/clip.cpp

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,8 @@ ggml_tensor * clip_graph::build_vit(
300300
norm_type norm_t,
301301
ffn_op_type ffn_t,
302302
ggml_tensor * learned_pos_embd,
303-
std::function<ggml_tensor *(ggml_tensor *, const clip_layer &)> add_pos
303+
std::function<ggml_tensor *(ggml_tensor *, const clip_layer &)> add_pos,
304+
const build_vit_opts & opts
304305
) {
305306
if (learned_pos_embd) {
306307
inp = ggml_add(ctx0, inp, learned_pos_embd);
@@ -427,7 +428,7 @@ ggml_tensor * clip_graph::build_vit(
427428
}
428429

429430
cur = build_attn(layer.o_w, layer.o_b,
430-
Qcur, Kcur, Vcur, nullptr, kq_scale, il);
431+
Qcur, Kcur, Vcur, opts.attn_mask, kq_scale, il);
431432
cb(cur, "attn_out", il);
432433
}
433434

@@ -663,6 +664,9 @@ ggml_tensor * clip_graph::build_attn(
663664

664665
k = ggml_cast(ctx0, k, GGML_TYPE_F16);
665666
v = ggml_cast(ctx0, v, GGML_TYPE_F16);
667+
if (kq_mask) {
668+
kq_mask = ggml_cast(ctx0, kq_mask, GGML_TYPE_F16);
669+
}
666670

667671
cur = ggml_flash_attn_ext(ctx0, q, k, v, kq_mask, kq_scale, 0.0f, 0.0f);
668672
ggml_flash_attn_ext_set_prec(cur, GGML_PREC_F32);
@@ -3244,12 +3248,10 @@ int clip_n_output_tokens(const struct clip_ctx * ctx, struct clip_image_f32 * im
32443248
} break;
32453249
case PROJECTOR_TYPE_QWEN3A:
32463250
{
3247-
// 3x stride-2 conv2d: each step is floor((n-1)/2)+1
3248-
int n = img->nx;
3249-
n = (n - 1) / 2 + 1;
3250-
n = (n - 1) / 2 + 1;
3251-
n = (n - 1) / 2 + 1;
3252-
n_patches = n;
3251+
// chunk_size=100 frames --> 3x stride-2 conv2d --> 13 tokens per chunk
3252+
const int chunk_size = 100;
3253+
const int tokens_per_chunk = 13;
3254+
n_patches = (img->nx / chunk_size) * tokens_per_chunk;
32533255
} break;
32543256
case PROJECTOR_TYPE_GLMA:
32553257
{
@@ -4292,21 +4294,6 @@ bool clip_has_audio_encoder(const struct clip_ctx * ctx) {
42924294
return ctx->model.modality == CLIP_MODALITY_AUDIO;
42934295
}
42944296

4295-
bool clip_has_whisper_encoder(const struct clip_ctx * ctx) {
4296-
switch (ctx->proj_type()) {
4297-
case PROJECTOR_TYPE_ULTRAVOX:
4298-
case PROJECTOR_TYPE_QWEN2A:
4299-
case PROJECTOR_TYPE_QWEN3A:
4300-
case PROJECTOR_TYPE_GLMA:
4301-
case PROJECTOR_TYPE_VOXTRAL:
4302-
case PROJECTOR_TYPE_MERALION:
4303-
case PROJECTOR_TYPE_MUSIC_FLAMINGO:
4304-
return true;
4305-
default:
4306-
return false;
4307-
}
4308-
}
4309-
43104297
bool clip_encode_float_image (struct clip_ctx * ctx, int n_threads, float * img, int h, int w, float * vec) {
43114298
clip_image_f32 clip_img;
43124299
clip_img.buf.resize(h * w * 3);

tools/mtmd/clip.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ void clip_image_f32_batch_add_mel(struct clip_image_f32_batch * batch, int n_mel
115115

116116
bool clip_has_vision_encoder(const struct clip_ctx * ctx);
117117
bool clip_has_audio_encoder(const struct clip_ctx * ctx);
118-
bool clip_has_whisper_encoder(const struct clip_ctx * ctx);
119118

120119
struct clip_cap {
121120
bool has_vision;

tools/mtmd/models/qwen3a.cpp

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,88 @@
11
#include "models.h"
22

33
ggml_cgraph * clip_graph_qwen3a::build() {
4+
// Ref implementation: https://github.com/QwenLM/Qwen3-ASR/blob/main/qwen_asr/core/transformers_backend/modeling_qwen3_asr.py
5+
6+
// inp_raw: [n_frames, n_mel, 1] (nx=n_frames, ny=n_mel)
47
ggml_tensor * inp = build_inp_raw(1);
58

6-
// conv2d block
7-
// TODO: do we need to split by chunks of n_window each like on transformers impl?
8-
{
9-
inp = ggml_conv_2d(ctx0, model.conv2d_1_w, inp, 2, 2, 1, 1, 1, 1);
10-
inp = ggml_add(ctx0, inp, model.conv2d_1_b);
11-
inp = ggml_gelu_erf(ctx0, inp);
9+
const int64_t n_frames = inp->ne[0]; // total frames, padded to multiple of chunk_size
10+
const int64_t n_mel = inp->ne[1]; // 128
11+
const int64_t chunk_size = 100; // n_window * 2 (n_window=50 from model config)
12+
const int64_t n_chunks = n_frames / chunk_size;
1213

13-
inp = ggml_conv_2d(ctx0, model.conv2d_2_w, inp, 2, 2, 1, 1, 1, 1);
14-
inp = ggml_add(ctx0, inp, model.conv2d_2_b);
15-
inp = ggml_gelu_erf(ctx0, inp);
14+
GGML_ASSERT(n_frames % chunk_size == 0); // preprocessor should already pad the input
15+
GGML_ASSERT(inp->type == GGML_TYPE_F32);
1616

17-
inp = ggml_conv_2d(ctx0, model.conv2d_3_w, inp, 2, 2, 1, 1, 1, 1);
18-
inp = ggml_add(ctx0, inp, model.conv2d_3_b);
19-
inp = ggml_gelu_erf(ctx0, inp);
17+
// View mel spectrogram as batched 100-frame chunks: [chunk_size, n_mel, 1, n_chunks]
18+
inp = ggml_view_4d(ctx0, inp,
19+
chunk_size, n_mel, 1, n_chunks,
20+
n_frames * (int64_t)sizeof(float), // nb[1]: stride over mel bins
21+
chunk_size * (int64_t)sizeof(float), // nb[2]: stride for C=1 (unused)
22+
chunk_size * (int64_t)sizeof(float), // nb[3]: stride over chunks
23+
0);
24+
inp = ggml_cont(ctx0, inp);
25+
cb(inp, "inp_chunks", -1);
2026

21-
// inp [n_pos, n_mels/8, channels, 1] (W, H, C, N)
22-
cb(inp, "after_conv_blocks", -1);
27+
// 3 x conv2d + gelu
28+
{
29+
// conv output [OW, OH, C_out, n_chunks]
30+
auto conv_block = [&](ggml_tensor * x, ggml_tensor * w, ggml_tensor * b) {
31+
x = ggml_conv_2d(ctx0, w, x, 2, 2, 1, 1, 1, 1);
32+
if (b) {
33+
x = ggml_add(ctx0, x, ggml_reshape_4d(ctx0, b, 1, 1, x->ne[2], 1));
34+
}
35+
return ggml_gelu_erf(ctx0, x);
36+
};
2337

24-
const int64_t n_pos_after_conv = inp->ne[0];
25-
const int64_t n_mel_after_conv = inp->ne[1]; // 128/8 = 16
38+
inp = conv_block(inp, model.conv2d_1_w, model.conv2d_1_b);
39+
inp = conv_block(inp, model.conv2d_2_w, model.conv2d_2_b);
40+
inp = conv_block(inp, model.conv2d_3_w, model.conv2d_3_b);
41+
// inp: [OW=13, OH=16, OC=480, n_chunks]
42+
cb(inp, "after_conv_blocks", -1);
43+
}
2644

27-
inp = ggml_cont(ctx0, ggml_permute(ctx0, inp, 0, 2, 3, 1));
28-
inp = ggml_reshape_2d(ctx0, inp, n_pos_after_conv, n_mel_after_conv * inp->ne[3]); // [n_pos, 7680]
29-
inp = ggml_cont(ctx0, ggml_transpose(ctx0, inp)); // [7680, n_pos]
45+
// permute [OW=25, OH=16, OC=480, n_chunks] -> [OH=16, OC=480, OW=25, n_chunks]
46+
// reshape to [OH*OC=7680, OW*n_chunks]
47+
// feature index h+16*c = c*16+f (matches python code)
48+
inp = ggml_cont(ctx0, ggml_permute(ctx0, inp, 2, 0, 1, 3));
49+
inp = ggml_reshape_2d(ctx0, inp, inp->ne[0] * inp->ne[1], inp->ne[2] * inp->ne[3]);
3050

31-
// project to n_embd
32-
inp = ggml_mul_mat(ctx0, model.conv_out_w, inp);
33-
if (model.conv_out_b) {
34-
inp = ggml_add(ctx0, inp, model.conv_out_b);
35-
}
36-
cb(inp, "after_conv_out", -1);
51+
// Project to d_model: [d_model, 25*n_chunks]
52+
inp = ggml_mul_mat(ctx0, model.conv_out_w, inp);
53+
if (model.conv_out_b) {
54+
inp = ggml_add(ctx0, inp, model.conv_out_b);
3755
}
56+
cb(inp, "after_conv_out", -1);
3857

39-
auto n_pos = inp->ne[1];
58+
const int64_t n_pos = inp->ne[1]; // 25 * n_chunks
4059

41-
ggml_tensor * pos_embd_selected = ggml_view_2d(
42-
ctx0, model.position_embeddings,
43-
model.position_embeddings->ne[0], n_pos,
44-
model.position_embeddings->nb[1], 0
45-
);
46-
ggml_tensor * cur = build_vit(
47-
inp, n_pos,
48-
NORM_TYPE_NORMAL,
49-
hparams.ffn_op,
50-
pos_embd_selected,
51-
nullptr);
60+
// Per-chunk positional embeddings: repeat pos[0:13] for each chunk
61+
// (position indices reset 0..12 per chunk, not sequential across chunks)
62+
{
63+
const int64_t tokens_per_chunk = n_pos / n_chunks; // 13
64+
ggml_tensor * pos_tmp = ggml_view_2d(ctx0, model.position_embeddings,
65+
model.position_embeddings->ne[0], tokens_per_chunk,
66+
model.position_embeddings->nb[1], 0);
67+
ggml_tensor * tgt = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32,
68+
model.position_embeddings->ne[0], n_pos);
69+
inp = ggml_add(ctx0, inp, ggml_repeat(ctx0, pos_tmp, tgt));
70+
}
5271

72+
ggml_tensor * cur = build_vit(inp, n_pos,
73+
NORM_TYPE_NORMAL, hparams.ffn_op,
74+
nullptr, // pos embd already added above
75+
nullptr);
5376
cb(cur, "after_transformer", -1);
5477

55-
// projector
78+
// MLP projector
5679
cur = build_ffn(cur,
5780
model.mm_1_w, model.mm_1_b,
5881
nullptr, nullptr,
5982
model.mm_2_w, model.mm_2_b,
60-
FFN_GELU_ERF,
61-
-1);
62-
83+
FFN_GELU_ERF, -1);
6384
cb(cur, "projected", -1);
6485

6586
ggml_build_forward_expand(gf, cur);
66-
6787
return gf;
6888
}

tools/mtmd/mtmd-audio.cpp

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,110 @@ bool mtmd_audio_preprocessor_whisper::preprocess(const float * s
609609
return true;
610610
}
611611

612+
//
613+
// mtmd_audio_preprocessor_qwen3a
614+
//
615+
// Matches the Python WhisperFeatureExtractor called with truncation=False:
616+
// - reflection padding of n_fft/2 samples at each end (center=True)
617+
// - Whisper-style log10 + (max-8)/4 normalization applied to full audio
618+
// - output split into ≤30s (3000 mel frames) windows, each padded to a
619+
// multiple of 200 frames (n_window * 2) for the cgraph batch view
620+
//
621+
622+
void mtmd_audio_preprocessor_qwen3a::initialize() {
623+
cache.fill_sin_cos_table(hparams.audio_n_fft);
624+
cache.fill_hann_window(hparams.audio_window_len, true);
625+
cache.fill_mel_filterbank_matrix(hparams.n_mel_bins, hparams.audio_n_fft, hparams.audio_sample_rate);
626+
}
627+
628+
bool mtmd_audio_preprocessor_qwen3a::preprocess(const float * samples,
629+
size_t n_samples,
630+
std::vector<mtmd_audio_mel> & output) {
631+
if (n_samples == 0) {
632+
return false;
633+
}
634+
635+
GGML_ASSERT(!cache.sin_vals.empty());
636+
GGML_ASSERT(!cache.cos_vals.empty());
637+
GGML_ASSERT(!cache.filters.data.empty());
638+
639+
// Reflection-pad n_fft/2 samples at each end, matching WhisperFeatureExtractor center=True
640+
const int pad = hparams.audio_n_fft / 2; // = 200
641+
642+
std::vector<float> padded(n_samples + 2 * pad, 0.0f);
643+
// Reflect start: padded[0..pad-1] = samples[pad..1] (reversed)
644+
for (int i = 0; i < pad; i++) {
645+
int src = pad - i; // samples[pad], samples[pad-1], ..., samples[1]
646+
padded[i] = (src < (int)n_samples) ? samples[src] : 0.0f;
647+
}
648+
std::copy(samples, samples + n_samples, padded.begin() + pad);
649+
// Reflect end: padded[n+pad..n+2*pad-1] = samples[n-2..n-pad-1] (reversed)
650+
for (int i = 0; i < pad; i++) {
651+
int src = (int)n_samples - 2 - i; // samples[n-2], samples[n-3], ...
652+
padded[n_samples + pad + i] = (src >= 0) ? samples[src] : 0.0f;
653+
}
654+
655+
filter_params params;
656+
params.n_mel = hparams.n_mel_bins;
657+
params.n_fft_bins = 1 + (hparams.audio_n_fft / 2);
658+
params.hann_window_size = hparams.audio_window_len;
659+
params.hop_length = hparams.audio_hop_len;
660+
params.sample_rate = hparams.audio_sample_rate;
661+
params.no_padding = true; // reflection padding already applied above
662+
params.use_natural_log = false; // log10
663+
664+
mtmd_audio_mel mel_full;
665+
bool ok = log_mel_spectrogram(padded.data(), (int)padded.size(), 4, params, cache, mel_full);
666+
if (!ok) {
667+
return false;
668+
}
669+
670+
// Whisper-style normalization: clamp to (max - 8), scale to [-1, 1]
671+
{
672+
double mmax = -1e20;
673+
for (float v : mel_full.data) {
674+
if (v > mmax) mmax = v;
675+
}
676+
mmax -= 8.0;
677+
for (float & v : mel_full.data) {
678+
v = (std::max((double)v, mmax) + 4.0) / 4.0;
679+
}
680+
}
681+
682+
// The effective frame count: center-padded STFT gives ~n_samples/hop_length frames.
683+
// We take min(mel_full.n_len, n_samples/hop + 1) to avoid including excess frames.
684+
const int n_eff = std::min(mel_full.n_len,
685+
(int)(n_samples / hparams.audio_hop_len) + 1);
686+
687+
// Split into inference windows matching n_window_infer=800 from model config.
688+
// Each window is padded to the next multiple of chunk_size for the cgraph.
689+
// The mtmd caller loops over output entries, so long audio is handled automatically.
690+
const int chunk_size = 100; // conv sub-chunk size (n_window * 2, n_window=50)
691+
const int window_size = 800; // mel frames per forward pass (n_window_infer=800)
692+
693+
for (int off = 0; off < n_eff; off += window_size) {
694+
const int win_eff = std::min(window_size, n_eff - off);
695+
const int n_chunks = (win_eff + chunk_size - 1) / chunk_size;
696+
const int n_padded = n_chunks * chunk_size;
697+
698+
mtmd_audio_mel out;
699+
out.n_mel = mel_full.n_mel;
700+
out.n_len = n_padded;
701+
out.n_len_org = win_eff;
702+
out.data.assign(out.n_mel * out.n_len, 0.0f);
703+
for (int m = 0; m < out.n_mel; m++) {
704+
const int copy_len = std::min(win_eff, mel_full.n_len - off);
705+
if (copy_len > 0) {
706+
std::copy(mel_full.data.begin() + (size_t)m * mel_full.n_len + off,
707+
mel_full.data.begin() + (size_t)m * mel_full.n_len + off + copy_len,
708+
out.data.begin() + (size_t)m * out.n_len);
709+
}
710+
}
711+
output.push_back(std::move(out));
712+
}
713+
return true;
714+
}
715+
612716
//
613717
// mtmd_audio_preprocessor_conformer
614718
//

tools/mtmd/mtmd-audio.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ struct mtmd_audio_preprocessor_gemma4a : mtmd_audio_preprocessor {
9696
mtmd_audio_cache cache;
9797
};
9898

99+
struct mtmd_audio_preprocessor_qwen3a : mtmd_audio_preprocessor {
100+
mtmd_audio_preprocessor_qwen3a(const clip_ctx * ctx) : mtmd_audio_preprocessor(ctx) {}
101+
void initialize() override;
102+
bool preprocess(const float * samples, size_t n_samples, std::vector<mtmd_audio_mel> & output) override;
103+
104+
private:
105+
mtmd_audio_cache cache;
106+
};
107+
99108
//
100109
// streaming ISTFT - converts spectrogram frames back to audio one frame at a time
101110
//

tools/mtmd/mtmd.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,14 +515,19 @@ struct mtmd_context {
515515
// set preprocessor
516516
switch (proj) {
517517
case PROJECTOR_TYPE_QWEN2A:
518-
case PROJECTOR_TYPE_QWEN3A:
519518
case PROJECTOR_TYPE_QWEN25O:
520519
{
521520
// <|audio_bos|> ... (embeddings) ... <|audio_eos|>
522521
aud_beg = "<|audio_bos|>";
523522
aud_end = "<|audio_eos|>";
524523
audio_preproc = std::make_unique<mtmd_audio_preprocessor_whisper>(ctx_a);
525524
} break;
525+
case PROJECTOR_TYPE_QWEN3A:
526+
{
527+
aud_beg = "<|audio_start|>";
528+
aud_end = "<|audio_end|>";
529+
audio_preproc = std::make_unique<mtmd_audio_preprocessor_qwen3a>(ctx_a);
530+
} break;
526531
case PROJECTOR_TYPE_VOXTRAL:
527532
{
528533
// [BEGIN_AUDIO] ... (embeddings) ...

0 commit comments

Comments
 (0)