char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Xuan-Son Nguyenmodel, mtmd: fix gemma4 vision handling (#28335)163a40796

char/aliceai-t5-moe
104.4 KiB2733 linesraw
1#include "clip.h"
2#include "clip-impl.h"
3#include "mtmd.h"
4#include "mtmd-internal.h"
5#include "mtmd-audio.h"
6#include "mtmd-image.h"
7#include "debug/mtmd-debug.h"
8
9#include "llama.h"
10
11// fix problem with std::min and std::max
12#if defined(_WIN32)
13#define WIN32_LEAN_AND_MEAN
14#ifndef NOMINMAX
15#   define NOMINMAX
16#endif
17#include <windows.h>
18#endif
19
20#include <algorithm>
21#include <cerrno>
22#include <cstdio>
23#include <cstdlib>
24#include <cstring>
25#include <climits>
26#include <type_traits>
27#include <vector>
28
29// remember to bump this if the serialization format changes
30#define MTMD_SERIALIZATION_VERSION 2
31
32struct mtmd_serialization {
33    // note: using 64-bit here for future-proofing
34    uint64_t version = MTMD_SERIALIZATION_VERSION;
35    std::vector<char> data;
36    size_t read_pos = 0; // cursor used when reading
37
38    // for writing
39    mtmd_serialization(uint64_t version) : version(version) {
40        write(version);
41    }
42
43    // for reading
44    mtmd_serialization(uint64_t version, const char * buf, size_t len) {
45        // copy buf to data
46        data.assign(buf, buf + len);
47        uint64_t ver_in = read<uint64_t>();
48        if (ver_in != version) {
49            throw std::runtime_error("version mismatch");
50        }
51        this->version = ver_in;
52    }
53
54    template <typename T>
55    void write(T value) {
56        static_assert(std::is_trivially_copyable<T>::value && !std::is_same<T, bool>::value,
57            "T must be trivially copyable and not bool");
58        const char * p = reinterpret_cast<const char *>(&value);
59        data.insert(data.end(), p, p + sizeof(T));
60    }
61
62    template <typename T>
63    T read() {
64        static_assert(std::is_trivially_copyable<T>::value && !std::is_same<T, bool>::value,
65            "T must be trivially copyable and not bool");
66        if (read_pos + sizeof(T) > data.size()) {
67            throw std::runtime_error("read OOB");
68        }
69        T value;
70        std::memcpy(&value, data.data() + read_pos, sizeof(T));
71        read_pos += sizeof(T);
72        return value;
73    }
74
75};
76
77template <>
78void mtmd_serialization::write<bool>(bool value) {
79    write<uint8_t>(value ? 1 : 0);
80}
81template <>
82bool mtmd_serialization::read<bool>() {
83    return read<uint8_t>() != 0;
84}
85
86template <>
87void mtmd_serialization::write<std::string>(std::string value) {
88    write<uint64_t>(value.size());
89    data.insert(data.end(), value.begin(), value.end());
90}
91template <>
92std::string mtmd_serialization::read<std::string>() {
93    uint64_t len = read<uint64_t>();
94    if (read_pos + len > data.size()) {
95        throw std::runtime_error("read_string OOB");
96    }
97    std::string str(data.data() + read_pos, len);
98    read_pos += len;
99    return str;
100}
101
102// only mtmd.cpp needs these, so they're implemented here rather than in clip-impl.h
103void clip_image_f32::serialize(mtmd_serialization & ser) const {
104    // remember to bump MTMD_SERIALIZATION_VERSION if this is changed
105    // note: buf is intentionally NOT serialized; the loaded clip_image_f32 will always be a placeholder
106    ser.write(add_viewsep);
107    ser.write(add_newline);
108    ser.write(lead_pad);
109    ser.write((int32_t)nx_);
110    ser.write((int32_t)ny_);
111}
112void clip_image_f32::deserialize(mtmd_serialization & ser) {
113    add_viewsep = ser.read<bool>();
114    add_newline = ser.read<bool>();
115    lead_pad = ser.read<int32_t>();
116    nx_ = ser.read<int32_t>();
117    ny_ = ser.read<int32_t>();
118    buf.clear(); // always a placeholder after loading
119}
120
121void clip_image_f32_batch::serialize(mtmd_serialization & ser) const {
122    // remember to bump MTMD_SERIALIZATION_VERSION if this is changed
123    ser.write(is_audio);
124    ser.write<uint64_t>(entries.size());
125    for (const auto & entry : entries) {
126        entry.serialize(ser);
127    }
128}
129void clip_image_f32_batch::deserialize(mtmd_serialization & ser) {
130    is_audio = ser.read<bool>();
131    uint64_t n = ser.read<uint64_t>();
132    constexpr size_t min_entry_bytes = sizeof(uint8_t) * 2 + sizeof(int32_t) * 2;
133    if (n > (ser.data.size() - ser.read_pos) / min_entry_bytes) {
134        throw std::runtime_error("entries count exceeds buffer size");
135    }
136    entries.clear();
137    entries.reserve(n);
138    for (uint64_t i = 0; i < n; i++) {
139        clip_image_f32 entry;
140        entry.deserialize(ser);
141        entries.push_back(std::move(entry));
142    }
143}
144
145// for still image data, layout is RGBRGBRGB...
146// length of data must be nx * ny * 3 bytes
147//
148// for audio bitmap: nx = sample count, ny = 1, layout is F32 F32 F32 ...
149// length of data must be nx * sizeof(float) bytes
150struct mtmd_bitmap {
151    uint32_t nx = 0;
152    uint32_t ny = 0;
153    std::string id; // optional user-defined id, for ex: can be set to image hash, useful for KV cache tracking
154    bool is_audio = false; // true if the bitmap is audio
155    bool mergeable = false; // [QWEN_VIDEO] set only on frames of the same video
156
157    // lazy-loaded bitmap
158    mtmd_bitmap_lazy_callback lazy_callback = nullptr;
159    void * lazy_user_data = nullptr;
160
161    mtmd_bitmap(const unsigned char * data, uint32_t nx, uint32_t ny)
162        : nx(nx), ny(ny), is_audio(false) {
163        if (data) {
164            size_t data_size = (size_t)nx * ny * 3;
165            this->data.resize(data_size);
166            std::memcpy(this->data.data(), data, data_size);
167        }
168    }
169
170    mtmd_bitmap(const unsigned char * data, uint32_t n_samples)
171        : nx(n_samples), ny(1), is_audio(true) {
172        if (data) {
173            size_t data_size = (size_t)nx * sizeof(float);
174            this->data.resize(data_size);
175            std::memcpy(this->data.data(), data, data_size);
176        }
177    }
178
179    const std::vector<unsigned char> & get_ro_buf() const {
180        return data;
181    }
182
183    bool is_placeholder() const {
184        return data.empty();
185    }
186
187    size_t n_bytes() const {
188        return data.size();
189    }
190
191    bool can_merge_with(const mtmd_bitmap & other) const {
192        // [QWEN_VIDEO] can (temporal) merge if both are images with same size
193        return mergeable && other.mergeable
194            && !is_audio && !other.is_audio
195            && nx == other.nx && ny == other.ny;
196    }
197
198  private:
199    std::vector<unsigned char> data;
200};
201
202// position indexing for decoder model
203enum mtmd_pos_type {
204    MTMD_POS_TYPE_NORMAL,    // number of positions equals to number of tokens
205    MTMD_POS_TYPE_MROPE,     // qwen-vl mrope style, each image takes max(t,h,w) position indexes
206    MTMD_POS_TYPE_HUNYUANVL, // HunyuanVL mrope + BOI/EOI/newline layout with XD-RoPE dim-3
207    MTMD_POS_TYPE_COUNT,     // for validation
208};
209
210struct mtmd_image_tokens {
211    uint32_t nx = 0; // number of tokens in x direction
212    uint32_t ny = 0; // number of tokens in y direction
213    mtmd_pos_type pos = MTMD_POS_TYPE_NORMAL;
214    uint32_t image_idx = 0; // 0-based position of this image among image chunks in the prompt(used by pos == MTMD_POS_TYPE_HUNYUANVL)
215    uint32_t n_temporal_merge = 1; // for qwen-vl style temporal merge
216    uint32_t n_tokens() const {
217        if (pos == MTMD_POS_TYPE_HUNYUANVL) {
218            // [BOI] [row0 tokens + newline] ... [row(ny-1) tokens + newline] [EOI]
219            return (nx + 1) * ny + 2;
220        }
221        uint32_t nz = batch_f32.entries.size();
222        if (n_temporal_merge > 1) {
223            // [QWEN_VIDEO] this logic is quite ugly, it's mostly to make qwen-vl temporal merge work, can be improved in the future
224            // TODO: simplify this by repeating the last frame until it fits the temporal merge
225            if (nz % n_temporal_merge != 0) {
226                nz = nz / n_temporal_merge + 1;
227            } else {
228                nz = nz / n_temporal_merge;
229            }
230        }
231        return nx * ny * nz;
232    }
233    clip_image_f32_batch batch_f32; // preprocessed image patches
234    std::string id; // optional user-defined ID, useful for KV cache tracking
235
236    // true if one of entries in batch_f32 is a placeholder
237    bool is_placeholder() const {
238        for (const auto & entry : batch_f32.entries) {
239            if (entry.is_placeholder()) {
240                return true;
241            }
242        }
243        return false;
244    }
245
246    bool can_batch_with(const mtmd_image_tokens & other) {
247        return nx == other.nx && ny == other.ny && pos == other.pos;
248    }
249
250    mtmd_image_tokens clone() {
251        return mtmd_image_tokens{
252            nx,
253            ny,
254            pos,
255            image_idx,
256            n_temporal_merge,
257            batch_f32.clone(),
258            id
259        };
260    }
261
262    void serialize(mtmd_serialization & ser) const {
263        // remember to bump MTMD_SERIALIZATION_VERSION if this is changed
264        ser.write(nx);
265        ser.write(ny);
266        ser.write((uint32_t)pos);
267        ser.write(image_idx);
268        ser.write(n_temporal_merge);
269        ser.write(id);
270        batch_f32.serialize(ser);
271    }
272    void deserialize(mtmd_serialization & ser) {
273        nx = ser.read<uint32_t>();
274        ny = ser.read<uint32_t>();
275        uint32_t pos_raw = ser.read<uint32_t>();
276        if (pos_raw >= MTMD_POS_TYPE_COUNT) {
277            throw std::runtime_error("invalid pos type");
278        }
279        pos = (mtmd_pos_type)pos_raw;
280        image_idx = ser.read<uint32_t>();
281        n_temporal_merge = ser.read<uint32_t>();
282        id = ser.read<std::string>();
283        batch_f32.deserialize(ser);
284    }
285};
286using mtmd_image_tokens_ptr = std::unique_ptr<mtmd_image_tokens>;
287
288struct mtmd_audio_tokens {
289    uint32_t n_tokens = 0; // number of tokens
290    clip_image_f32_batch batch_f32; // preprocessed image patches
291    std::string id; // optional user-defined ID, useful for KV cache tracking
292
293    // true if one of entries in batch_f32 is a placeholder
294    bool is_placeholder() const {
295        for (const auto & entry : batch_f32.entries) {
296            if (entry.is_placeholder()) {
297                return true;
298            }
299        }
300        return false;
301    }
302
303    mtmd_audio_tokens clone() {
304        return mtmd_audio_tokens{
305            n_tokens,
306            batch_f32.clone(),
307            id
308        };
309    }
310
311    void serialize(mtmd_serialization & ser) const {
312        // remember to bump MTMD_SERIALIZATION_VERSION if this is changed
313        ser.write(n_tokens);
314        ser.write(id);
315        batch_f32.serialize(ser);
316    }
317    void deserialize(mtmd_serialization & ser) {
318        n_tokens = ser.read<uint32_t>();
319        id = ser.read<std::string>();
320        batch_f32.deserialize(ser);
321    }
322};
323using mtmd_audio_tokens_ptr = std::unique_ptr<mtmd_audio_tokens>;
324
325struct mtmd_input_chunk {
326    mtmd_input_chunk_type type;
327    std::vector<llama_token> tokens_text;
328    mtmd_image_tokens_ptr tokens_image;
329    mtmd_audio_tokens_ptr tokens_audio;
330
331    bool can_batch_with(const mtmd_input_chunk & other) const {
332        if (type != other.type) {
333            return false;
334        }
335
336        if (tokens_image && other.tokens_image) {
337            return tokens_image->can_batch_with(*other.tokens_image);
338        }
339
340        // TODO: allow batching audio chunks of the same size
341
342        return false;
343    }
344
345    bool is_placeholder() const {
346        if (type == MTMD_INPUT_CHUNK_TYPE_IMAGE) {
347            return tokens_image && tokens_image->is_placeholder();
348        } else if (type == MTMD_INPUT_CHUNK_TYPE_AUDIO) {
349            return tokens_audio && tokens_audio->is_placeholder();
350        }
351        return false;
352    }
353
354    void serialize(mtmd_serialization & ser) const {
355        // remember to bump MTMD_SERIALIZATION_VERSION if this is changed
356        ser.write((uint32_t)type);
357
358        ser.write<uint64_t>(tokens_text.size());
359        for (llama_token tok : tokens_text) {
360            ser.write((int32_t)tok);
361        }
362
363        ser.write(tokens_image != nullptr);
364        if (tokens_image) {
365            tokens_image->serialize(ser);
366        }
367
368        ser.write(tokens_audio != nullptr);
369        if (tokens_audio) {
370            tokens_audio->serialize(ser);
371        }
372    }
373    void deserialize(mtmd_serialization & ser) {
374        uint32_t type_raw = ser.read<uint32_t>();
375        if (type_raw >= MTMD_INPUT_CHUNK_TYPE_COUNT) {
376            throw std::runtime_error("invalid chunk type");
377        }
378        type = (mtmd_input_chunk_type)type_raw;
379
380        uint64_t n_tokens_text = ser.read<uint64_t>();
381        // reject before resize() so a tiny corrupted/malicious buffer can't force a huge allocation
382        if (n_tokens_text > (ser.data.size() - ser.read_pos) / sizeof(int32_t)) {
383            throw std::runtime_error("tokens_text length exceeds buffer size");
384        }
385        tokens_text.resize(n_tokens_text);
386        for (uint64_t i = 0; i < n_tokens_text; i++) {
387            tokens_text[i] = (llama_token)ser.read<int32_t>();
388        }
389
390        if (ser.read<bool>()) {
391            tokens_image = std::make_unique<mtmd_image_tokens>();
392            tokens_image->deserialize(ser);
393        } else {
394            tokens_image.reset();
395        }
396
397        if (ser.read<bool>()) {
398            tokens_audio = std::make_unique<mtmd_audio_tokens>();
399            tokens_audio->deserialize(ser);
400        } else {
401            tokens_audio.reset();
402        }
403
404        // catch buffers where the declared type doesn't match which payload is actually present,
405        // so a mismatched chunk can't slip through and null-deref/abort later in an accessor
406        if (type == MTMD_INPUT_CHUNK_TYPE_IMAGE && !tokens_image) {
407            throw std::runtime_error("type is IMAGE but tokens_image is missing");
408        }
409        if (type == MTMD_INPUT_CHUNK_TYPE_AUDIO && !tokens_audio) {
410            throw std::runtime_error("type is AUDIO but tokens_audio is missing");
411        }
412    }
413};
414
415struct mtmd_input_chunks {
416    std::vector<mtmd_input_chunk> entries;
417};
418
419struct mtmd_batch {
420    mtmd_context * ctx;
421    std::vector<const mtmd_input_chunk *> entries;
422    std::vector<float> output_embd; // aggregated output embedding for the whole batch
423    mtmd_batch(mtmd_context * ctx): ctx(ctx) {}
424    int32_t n_tokens() const {
425        int32_t n = 0;
426        for (const auto * chunk : entries) {
427            n += mtmd_input_chunk_get_n_tokens(chunk);
428        }
429        return n;
430    }
431};
432
433// slice template, used by some llava-uhd models to correctly place the special tokens around image embeddings
434// models not having it (llava-1.6) will process embeddings without any special tokens in-between
435enum mtmd_slice_tmpl {
436    MTMD_SLICE_TMPL_NONE,
437    MTMD_SLICE_TMPL_MINICPMV_2_5,
438    MTMD_SLICE_TMPL_MINICPMV_2_6,
439    MTMD_SLICE_TMPL_LLAMA4,
440    MTMD_SLICE_TMPL_IDEFICS3,
441    MTMD_SLICE_TMPL_LFM2,
442    MTMD_SLICE_TMPL_STEP3VL,
443};
444
445const char * mtmd_default_marker() {
446    return "<__media__>";
447}
448
449static clip_flash_attn_type mtmd_get_clip_flash_attn_type(enum llama_flash_attn_type flash_attn_type) {
450    switch (flash_attn_type) {
451        case LLAMA_FLASH_ATTN_TYPE_AUTO:     return CLIP_FLASH_ATTN_TYPE_AUTO;
452        case LLAMA_FLASH_ATTN_TYPE_DISABLED: return CLIP_FLASH_ATTN_TYPE_DISABLED;
453        case LLAMA_FLASH_ATTN_TYPE_ENABLED:  return CLIP_FLASH_ATTN_TYPE_ENABLED;
454    }
455    return CLIP_FLASH_ATTN_TYPE_AUTO;
456}
457
458mtmd_context_params mtmd_context_params_default() {
459    mtmd_context_params params {
460        /* use_gpu           */ true,
461        /* device            */ nullptr,
462        /* print_timings     */ true,
463        /* n_threads         */ 4,
464        /* image_marker      */ nullptr,
465        /* media_marker      */ mtmd_default_marker(),
466        /* flash_attn_type   */ LLAMA_FLASH_ATTN_TYPE_AUTO,
467        /* warmup            */ true,
468        /* image_min_tokens  */ -1,
469        /* image_max_tokens  */ -1,
470        /* cb_eval           */ nullptr,
471        /* cb_eval_user_data */ nullptr,
472        /* batch_max_tokens  */ 1024,
473        /* progress_callback */ nullptr,
474        /* progress_callback_user_data */ nullptr,
475    };
476    return params;
477}
478
479struct mtmd_context {
480    struct clip_ctx * ctx_v; // vision
481    struct clip_ctx * ctx_a; // audio
482    std::vector<float> out_embd; // image embedding vector
483
484    // generation context
485    struct clip_ctx * ctx_gen_a; // audio
486    std::vector<int32_t> gen_out_codes; // this frame's 16 sampled codes (GEN_CODE)
487    std::vector<float>   gen_out_feats; // this frame's continuous features, if any (GEN_CODE)
488    std::vector<float>   gen_out_embd;  // next-step hidden state fed back to backbone (GEN_CODE)
489    std::vector<float>   gen_out_audio; // decoded PCM samples for the current frame (GEN_WAV)
490    std::vector<uint8_t> gen_out_state; // state to feed into the next GEN_WAV call
491
492    bool print_timings;
493    int n_threads;
494    std::string media_marker;
495    const int n_embd_text = -1; // -1 means llm context not provided, skip checking this
496    const llama_vocab * vocab = nullptr; // can be nullptr if text_model is not provided
497    mtmd_pos_type pos_type;
498
499    // these are not token, but strings used to mark the beginning and end of image/audio embeddings
500    std::string img_beg;
501    std::string img_end;
502    std::string aud_beg;
503    std::string aud_end;
504
505    // for llava-uhd style models, we need special tokens in-between slices
506    // minicpmv calls them "slices", llama 4 calls them "tiles"
507    mtmd_slice_tmpl slice_tmpl = MTMD_SLICE_TMPL_NONE;
508    std::vector<llama_token> tok_ov_img_start;  // overview image
509    std::vector<llama_token> tok_ov_img_end;    // overview image
510    std::vector<llama_token> tok_slices_start;  // start of all slices
511    std::vector<llama_token> tok_slices_end;    // end of all slices
512    std::vector<llama_token> tok_sli_img_start; // single slice start
513    std::vector<llama_token> tok_sli_img_end;   // single slice end
514    std::vector<llama_token> tok_sli_img_mid;   // between 2 slices
515    std::vector<llama_token> tok_row_end;       // end of row
516    bool tok_row_end_trail = false;
517    bool ov_img_first      = false;
518
519    // string template for slice image delimiters with row/col (idefics3)
520    std::string sli_img_start_tmpl;
521
522    std::unique_ptr<mtmd_audio_preprocessor> audio_preproc;
523    std::unique_ptr<mtmd_image_preprocessor> image_preproc;
524
525    // batching
526    int32_t batch_max_tokens;
527
528    // TODO @ngxson : add timings
529
530    mtmd_context(const char * mmproj_fname,
531                   const llama_model * text_model,
532                   const mtmd_context_params & ctx_params,
533                   bool no_alloc = false) :
534        print_timings   (ctx_params.print_timings),
535        n_threads       (ctx_params.n_threads),
536        media_marker    (ctx_params.media_marker),
537        n_embd_text     (text_model ? llama_model_n_embd_inp(text_model) : -1),
538        vocab           (text_model ? llama_model_get_vocab(text_model) : nullptr),
539        batch_max_tokens(ctx_params.batch_max_tokens)
540    {
541        if (ctx_params.image_marker != nullptr) {
542            throw std::runtime_error("custom image_marker is not supported anymore, use media_marker instead");
543        }
544
545        if (media_marker.empty()) {
546            throw std::runtime_error("media_marker must not be empty");
547        }
548
549        if (text_model) {
550            auto decoder_rope_type = llama_model_rope_type(text_model);
551            switch (decoder_rope_type) {
552                case LLAMA_ROPE_TYPE_NONE:
553                case LLAMA_ROPE_TYPE_NORM:
554                case LLAMA_ROPE_TYPE_NEOX:
555                    {
556                        pos_type = MTMD_POS_TYPE_NORMAL;
557                    } break;
558                case LLAMA_ROPE_TYPE_MROPE:
559                case LLAMA_ROPE_TYPE_IMROPE:
560                    {
561                        pos_type = MTMD_POS_TYPE_MROPE;
562                    } break;
563                default:
564                    throw std::runtime_error(string_format("unsupported decoder rope type: %d\n", decoder_rope_type));
565            }
566        }
567
568        clip_context_params ctx_clip_params {
569            /* use_gpu           */ ctx_params.use_gpu,
570            /* device            */ ctx_params.device,
571            /* flash_attn_type   */ mtmd_get_clip_flash_attn_type(ctx_params.flash_attn_type),
572            /* image_min_tokens  */ ctx_params.image_min_tokens,
573            /* image_max_tokens  */ ctx_params.image_max_tokens,
574            /* warmup            */ ctx_params.warmup,
575            /* cb_eval           */ ctx_params.cb_eval,
576            /* cb_eval_user_data */ ctx_params.cb_eval_user_data,
577            /* no_alloc          */ no_alloc,
578            /* progress_callback */ ctx_params.progress_callback,
579            /* progress_callback_user_data */ ctx_params.progress_callback_user_data,
580        };
581
582        auto res = clip_init(mmproj_fname, ctx_clip_params);
583        ctx_v = res.ctx_v;
584        ctx_a = res.ctx_a;
585        ctx_gen_a = res.ctx_gen_a;
586        if (!ctx_v && !ctx_a) {
587            throw std::runtime_error(string_format("Failed to load CLIP model from %s\n", mmproj_fname));
588        }
589
590        // if both vision and audio mmproj are present, we need to validate their n_embd
591        if (ctx_v && ctx_a) {
592            int n_embd_v = clip_n_mmproj_embd(ctx_v);
593            int n_embd_a = clip_n_mmproj_embd(ctx_a);
594            if (n_embd_v != n_embd_a) {
595                throw std::runtime_error(string_format(
596                    "mismatch between vision and audio mmproj (n_embd_v = %d, n_embd_a = %d)\n",
597                    n_embd_v, n_embd_a));
598            }
599        }
600
601        // since we already validate n_embd of vision and audio mmproj,
602        // we can safely assume that they are the same
603        int n_embd_clip = clip_n_mmproj_embd(ctx_v ? ctx_v : ctx_a);
604        if (n_embd_text > 0 && n_embd_text != n_embd_clip) {
605            throw std::runtime_error(string_format(
606                "mismatch between text model (n_embd = %d) and mmproj (n_embd = %d)\n"
607                "hint: you may be using wrong mmproj\n",
608                n_embd_text, n_embd_clip));
609        }
610        if (ctx_gen_a) {
611            int n_embd_gen = clip_n_mmproj_embd(ctx_gen_a);
612            if (n_embd_text > 0 && n_embd_text != n_embd_gen) {
613                throw std::runtime_error(string_format(
614                    "mismatch between text model (n_embd = %d) and gen-audio mmproj (n_embd = %d)\n"
615                    "hint: you may be using wrong mmproj\n",
616                    n_embd_text, n_embd_gen));
617            }
618        }
619        if (ctx_v) {
620            init_vision();
621        }
622        if (ctx_a) {
623            init_audio();
624        }
625    }
626
627    void init_vision() {
628        GGML_ASSERT(ctx_v != nullptr);
629        image_preproc.reset();
630
631        projector_type proj = clip_get_projector_type(ctx_v);
632
633        switch (proj) {
634            case PROJECTOR_TYPE_MLP:
635            case PROJECTOR_TYPE_MLP_NORM:
636            case PROJECTOR_TYPE_LDP:
637            case PROJECTOR_TYPE_LDPV2:
638            case PROJECTOR_TYPE_COGVLM:
639            case PROJECTOR_TYPE_JANUS_PRO:
640            case PROJECTOR_TYPE_GLM_EDGE:
641                {
642                    bool has_pinpoints = !clip_get_hparams(ctx_v)->image_res_candidates.empty();
643                    if (has_pinpoints) {
644                        image_preproc = std::make_unique<mtmd_image_preprocessor_llava_uhd>(ctx_v);
645                    } else {
646                        image_preproc = std::make_unique<mtmd_image_preprocessor_fixed_size>(ctx_v);
647                    }
648                } break;
649            case PROJECTOR_TYPE_MINICPMV:
650                {
651                    int minicpmv_version = clip_get_hparams(ctx_v)->minicpmv_version;
652                    if (minicpmv_version == 2) {
653                        // minicpmv 2.5 format:
654                        // <image> (overview) </image><slice><image> (slice) </image><image> (slice) </image>\n ... </slice>
655                        slice_tmpl        = MTMD_SLICE_TMPL_MINICPMV_2_5;
656                        tok_ov_img_start  = {lookup_token("<image>")};
657                        tok_ov_img_end    = {lookup_token("</image>")};
658                        tok_slices_start  = {lookup_token("<slice>")};
659                        tok_slices_end    = {lookup_token("</slice>")};
660                        tok_sli_img_start = tok_ov_img_start;
661                        tok_sli_img_end   = tok_ov_img_end;
662                        tok_row_end       = {lookup_token("\n")};
663                        tok_row_end_trail = false; // no trailing end-of-row token
664                        ov_img_first      = true;
665                    } else if (minicpmv_version == 3 || minicpmv_version == 4 || minicpmv_version == 5 || minicpmv_version == 6 || minicpmv_version == 100045) {
666                        // minicpmv 2.6 format:
667                        // <image> (overview) </image><slice> (slice) </slice><slice> (slice) </slice>\n ...
668                        slice_tmpl        = MTMD_SLICE_TMPL_MINICPMV_2_6;
669                        tok_ov_img_start  = {lookup_token("<image>")};
670                        tok_ov_img_end    = {lookup_token("</image>")};
671                        tok_sli_img_start = {lookup_token("<slice>")};
672                        tok_sli_img_end   = {lookup_token("</slice>")};
673                        tok_row_end       = {lookup_token("\n")};
674                        tok_row_end_trail = false; // no trailing end-of-row token
675                        ov_img_first      = true;
676
677                    } else if (minicpmv_version != 0) {
678                        throw std::runtime_error(string_format("unsupported minicpmv version: %d\n", minicpmv_version));
679                    }
680                    image_preproc = std::make_unique<mtmd_image_preprocessor_llava_uhd>(ctx_v);
681                } break;
682            case PROJECTOR_TYPE_MINICPMV4_6:
683                {
684                    slice_tmpl        = MTMD_SLICE_TMPL_MINICPMV_2_6;
685                    tok_ov_img_start  = {lookup_token("<image>")};
686                    tok_ov_img_end    = {lookup_token("</image>")};
687                    tok_sli_img_start = {lookup_token("<slice>")};
688                    tok_sli_img_end   = {lookup_token("</slice>")};
689                    tok_row_end       = {lookup_token("\n")};
690                    tok_row_end_trail = false; // no trailing end-of-row token
691                    ov_img_first      = true;
692                    image_preproc     = std::make_unique<mtmd_image_preprocessor_minicpmv>(ctx_v);
693                } break;
694            case PROJECTOR_TYPE_QWEN2VL:
695            case PROJECTOR_TYPE_QWEN25VL:
696            case PROJECTOR_TYPE_QWEN3VL:
697            case PROJECTOR_TYPE_MIMOVL:
698                {
699                    // <|vision_start|> ... (image embeddings) ... <|vision_end|>
700                    img_beg = "<|vision_start|>";
701                    img_end = "<|vision_end|>";
702                    image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
703                } break;
704            case PROJECTOR_TYPE_MINIMAX_M3:
705                {
706                    // ]<]start of image[>[ ... (image embeddings) ... ]<]end of image[>[
707                    img_beg = "]<]start of image[>[";
708                    img_end = "]<]end of image[>[";
709                    image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
710                } break;
711            case PROJECTOR_TYPE_MUSE_GLIMMER:
712                {
713                    img_beg = "<|image_start|>";
714                    img_end = "<|image_end|>";
715                    image_preproc = std::make_unique<mtmd_image_preprocessor_muse_glimmer>(ctx_v);
716                } break;
717            case PROJECTOR_TYPE_YOUTUVL:
718                {
719                    // <|vision_start|> ... (image embeddings) ... <|vision_end|>
720                    img_beg = "<|vision_start|>";
721                    img_end = "<|vision_end|>";
722                    image_preproc = std::make_unique<mtmd_image_preprocessor_youtuvl>(ctx_v);
723                } break;
724            case PROJECTOR_TYPE_YASA2:
725                {
726                    img_beg = "<image>";
727                    img_end = "</image>";
728                    // Currently only supprots single-tile preprocessing: any input is downscaled
729                    // to one image_size x image_size tile (64 output tokens via 8x8 adaptive avg
730                    // pool).
731                    // However, the model itself supports llava-uhd multi-tile tiling for high-res
732                    // images. This will be implemented in a future PR (dispatch on has_pinpoints
733                    // - see LDP/COGVLM branch above) and emit image_grid_pinpoints in the conversion
734                    // script.
735                    image_preproc = std::make_unique<mtmd_image_preprocessor_fixed_size>(ctx_v);
736                } break;
737            case PROJECTOR_TYPE_GEMMA3:
738            case PROJECTOR_TYPE_GEMMA3NV:
739                {
740                    // <start_of_image> ... (image embeddings) ... <end_of_image>
741                    img_beg = "<start_of_image>";
742                    img_end = "<end_of_image>";
743                    image_preproc = std::make_unique<mtmd_image_preprocessor_fixed_size>(ctx_v);
744                } break;
745            case PROJECTOR_TYPE_IDEFICS3:
746                {
747                    // https://github.com/huggingface/transformers/blob/a42ba80fa520c784c8f11a973ca9034e5f859b79/src/transformers/models/idefics3/processing_idefics3.py#L192-L215
748                    slice_tmpl         = MTMD_SLICE_TMPL_IDEFICS3;
749                    tok_ov_img_start   = {lookup_token("\n\n"), lookup_token("<fake_token_around_image>"), lookup_token("<global-img>")};
750                    tok_ov_img_end     = {lookup_token("<fake_token_around_image>")};
751                    tok_row_end        = {lookup_token("\n")};
752                    sli_img_start_tmpl = "<fake_token_around_image><row_%d_col_%d>";
753                    image_preproc = std::make_unique<mtmd_image_preprocessor_idefics3>(ctx_v);
754                } break;
755            case PROJECTOR_TYPE_PIXTRAL:
756                {
757                    // https://github.com/huggingface/transformers/blob/1cd110c6cb6a6237614130c470e9a902dbc1a4bd/docs/source/en/model_doc/pixtral.md
758                    img_end = "[IMG_END]";
759                    image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
760                } break;
761            case PROJECTOR_TYPE_PHI4:
762                {
763                    // Phi-4 uses media marker insertion only. Keep image boundary text empty.
764                    image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
765                } break;
766            case PROJECTOR_TYPE_LLAMA4:
767                {
768                    // (more details in mtmd_context constructor)
769                    img_beg = "<|image_start|>";
770                    img_end = "<|image_end|>";
771                    LOG_WRN("%s: llama 4 vision is known to have degraded quality:\n"
772                            "    https://github.com/ggml-org/llama.cpp/pull/13282\n", __func__);
773                    image_preproc = std::make_unique<mtmd_image_preprocessor_llava_uhd>(ctx_v);
774                    ov_img_first = false;
775                } break;
776            case PROJECTOR_TYPE_STEP3VL:
777                {
778                    // Step3 format:
779                    //   <patch_start> (patch) <patch_end> [<patch_newline>]
780                    //   ... (all patch rows)
781                    //   <im_start> (overview) <im_end>
782                    slice_tmpl        = MTMD_SLICE_TMPL_STEP3VL;
783                    tok_ov_img_start  = {lookup_token("<im_start>")};
784                    tok_ov_img_end    = {lookup_token("<im_end>")};
785                    tok_sli_img_start = {lookup_token("<patch_start>")};
786                    tok_sli_img_end   = {lookup_token("<patch_end>")};
787                    tok_row_end       = {lookup_token("<patch_newline>")};
788                    tok_row_end_trail = false;
789                    ov_img_first      = false; // patches first, overview last
790                    image_preproc = std::make_unique<mtmd_image_preprocessor_step3vl>(ctx_v);
791                } break;
792            case PROJECTOR_TYPE_INTERNVL:
793                {
794                    // <img> ... (image embeddings) ... </img>
795                    img_beg = "<img>";
796                    img_end = "</img>";
797                    image_preproc = std::make_unique<mtmd_image_preprocessor_internvl>(ctx_v);
798                    ov_img_first = false;
799                } break;
800            case PROJECTOR_TYPE_KIMIVL:
801                {
802                    // <|media_start|> ... (image embeddings) ... <|media_end|>
803                    img_beg = "<|media_start|>";
804                    img_end = "<|media_end|>";
805                    image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
806                } break;
807            case PROJECTOR_TYPE_KIMIK25:
808                {
809                    // GLM-5.2-V reuses the Kimi-K2.5 vision encoder and projector, but marks
810                    // images with its own tokens, so decide based on the text model vocab
811                    if (lookup_token("<|begin_of_image|>") != LLAMA_TOKEN_NULL) {
812                        // <|begin_of_image|> ... (image embeddings) ... <|end_of_image|>
813                        img_beg = "<|begin_of_image|>";
814                        img_end = "<|end_of_image|>";
815                    } else {
816                        // <|media_begin|> ... (image embeddings) ... <|media_end|>
817                        img_beg = "<|media_begin|>";
818                        img_end = "<|media_end|>";
819                    }
820                    image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
821                } break;
822            case PROJECTOR_TYPE_LIGHTONOCR:
823                {
824                    // <|im_start|> ... (image embeddings) ... <|im_end|>
825                    img_beg = "<|im_start|>";
826                    img_end = "<|im_end|>";
827                    image_preproc = std::make_unique<mtmd_image_preprocessor_longest_edge>(ctx_v);
828                } break;
829            case PROJECTOR_TYPE_DEEPSEEK4V:
830                {
831                    // no vocab tokens are added; the start/end/newline markers are learned embeddings emitted by the encoder
832                    image_preproc = std::make_unique<mtmd_image_preprocessor_deepseek4v>(ctx_v);
833                } break;
834            case PROJECTOR_TYPE_DOTS_OCR:
835            case PROJECTOR_TYPE_DOTS3NOTE_V:
836                {
837                    // <|img|> ... (image embeddings) ... <|endofimg|>
838                    img_beg = "<|img|>";
839                    img_end = "<|endofimg|>";
840                    image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
841                } break;
842            case PROJECTOR_TYPE_NEMOTRON_V2_VL:
843                {
844                    image_preproc = std::make_unique<mtmd_image_preprocessor_fixed_size>(ctx_v);
845                } break;
846            case PROJECTOR_TYPE_LFM2:
847                {
848                    // multi-tile:
849                    //   <|image_start|>
850                    //     <|img_row_1_col_1|> (tile) <|img_row_1_col_2|> (tile) ...
851                    //     <|img_thumbnail|> (thumbnail)
852                    //   <|image_end|>
853                    // single-tile:
854                    //   <|image_start|> (image) <|image_end|>
855                    img_beg            = "<|image_start|>";
856                    img_end            = "<|image_end|>";
857                    slice_tmpl         = MTMD_SLICE_TMPL_LFM2;
858                    sli_img_start_tmpl = "<|img_row_%d_col_%d|>";
859                    tok_ov_img_start   = {lookup_token("<|img_thumbnail|>")};
860                    ov_img_first       = false;
861                    image_preproc = std::make_unique<mtmd_image_preprocessor_lfm2>(ctx_v);
862                } break;
863            case PROJECTOR_TYPE_GLM4V:
864                {
865                    // <|begin_of_image|> ... (image embeddings) ... <|end_of_image|>
866                    img_beg = "<|begin_of_image|>";
867                    img_end = "<|end_of_image|>";
868                    image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
869                } break;
870            case PROJECTOR_TYPE_PADDLEOCR:
871                {
872                    // <|IMAGE_START|> ... (image embeddings) ... <|IMAGE_END|>
873                    img_beg = "<|IMAGE_START|>";
874                    img_end = "<|IMAGE_END|>";
875                    image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
876                } break;
877            case PROJECTOR_TYPE_GEMMA4V:
878            case PROJECTOR_TYPE_GEMMA4UV:
879                {
880                    // <|image> ... (image embeddings) ... <image|>
881                    img_beg = "<|image>";
882                    img_end = "<image|>";
883                    image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
884                } break;
885            case PROJECTOR_TYPE_DEEPSEEKOCR:
886            case PROJECTOR_TYPE_DEEPSEEKOCR2:
887                {
888                    img_end = "\n"; // prevent empty batch on llama-server
889                    image_preproc = std::make_unique<mtmd_image_preprocessor_deepseekocr>(ctx_v);
890                    ov_img_first = false;
891                } break;
892            case PROJECTOR_TYPE_HUNYUANVL:
893                {
894                    // note: these use fullwidth | (U+FF5C) and ▁ (U+2581) to match the tokenizer vocabulary
895                    img_beg = "<|hy_place▁holder▁no▁100|>";
896                    img_end = "<|hy_place▁holder▁no▁101|>";
897                    image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
898                } break;
899            case PROJECTOR_TYPE_EXAONE4_5:
900                {
901                    // <vision> ... (image embeddings) ... </vision>
902                    img_beg = "<vision>";
903                    img_end = "</vision>";
904                    image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
905                } break;
906            case PROJECTOR_TYPE_GRANITE4_VISION:
907                {
908                    // ... (image embeddings) \n ...
909                    img_beg = "";
910                    img_end = "\n";
911                    image_preproc = std::make_unique<mtmd_image_preprocessor_granite>(ctx_v);
912                } break;
913            default:
914                throw std::runtime_error(string_format("%s: unexpected vision projector type %d\n", __func__, proj));
915        }
916
917        GGML_ASSERT(image_preproc != nullptr);
918    }
919
920    void init_audio() {
921        GGML_ASSERT(ctx_a != nullptr);
922        audio_preproc.reset();
923
924        projector_type proj = clip_get_projector_type(ctx_a);
925
926        LOG_WRN("%s: audio input is in experimental stage and may have reduced quality:\n"
927                "    https://github.com/ggml-org/llama.cpp/discussions/13759\n", __func__);
928
929        // set preprocessor
930        switch (proj) {
931            case PROJECTOR_TYPE_QWEN2A:
932            case PROJECTOR_TYPE_QWEN25O:
933                {
934                    // <|audio_bos|> ... (embeddings) ... <|audio_eos|>
935                    aud_beg = "<|audio_bos|>";
936                    aud_end = "<|audio_eos|>";
937                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_whisper>(ctx_a);
938                } break;
939            case PROJECTOR_TYPE_QWEN3A:
940                {
941                    aud_beg = "<|audio_start|>";
942                    aud_end = "<|audio_end|>";
943                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_qwen3a>(ctx_a);
944                } break;
945            case PROJECTOR_TYPE_VOXTRAL:
946                {
947                    // [BEGIN_AUDIO] ... (embeddings) ...
948                    aud_beg = "[BEGIN_AUDIO]";
949                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_whisper>(ctx_a);
950                } break;
951            case PROJECTOR_TYPE_MUSIC_FLAMINGO:
952                {
953                    // <sound> ... (embeddings) ...
954                    aud_beg = "<sound>";
955                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_whisper>(ctx_a);
956                } break;
957            case PROJECTOR_TYPE_ULTRAVOX:
958            case PROJECTOR_TYPE_GLMA:
959            case PROJECTOR_TYPE_MERALION:
960                {
961                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_whisper>(ctx_a);
962                } break;
963            case PROJECTOR_TYPE_LFM2A:
964                {
965                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_conformer>(ctx_a);
966                } break;
967            case PROJECTOR_TYPE_GRANITE_SPEECH:
968                {
969                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_granite_speech>(ctx_a);
970                } break;
971            case PROJECTOR_TYPE_GEMMA4A:
972                {
973                    aud_beg = "<|audio>";
974                    aud_end = "<audio|>";
975                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_gemma4a>(ctx_a);
976                } break;
977            case PROJECTOR_TYPE_PARAKEET:
978                {
979                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_parakeet>(ctx_a);
980                } break;
981            case PROJECTOR_TYPE_GEMMA4UA:
982                {
983                    aud_beg = "<|audio>";
984                    aud_end = "<audio|>";
985                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_gemma4ua>(ctx_a);
986                } break;
987            case PROJECTOR_TYPE_DOTS3NOTE_A:
988                {
989                    // <|audio_comp_start|> ... (embeddings) ... <|audio_comp_end|>
990                    aud_beg = "<|audio_comp_start|>";
991                    aud_end = "<|audio_comp_end|>";
992                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_dots3note>(ctx_a);
993                } break;
994            case PROJECTOR_TYPE_MIMO_AUDIO:
995                {
996                    aud_beg = "<|mimo_audio_start|>";
997                    aud_end = "<|mimo_audio_end|>";
998                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_mimo_audio>(ctx_a);
999                } break;
1000            case PROJECTOR_TYPE_QWEN3TTS_SPKENC:
1001                {
1002                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_qwen3tts_spk>(ctx_a);
1003                } break;
1004            case PROJECTOR_TYPE_POCKETTTS_SPKENC:
1005                {
1006                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_pockettts>(ctx_a);
1007                } break;
1008            default:
1009                throw std::runtime_error(string_format("%s: unexpected audio projector type %d\n", __func__, proj));
1010        }
1011
1012        // initialize audio preprocessor
1013        GGML_ASSERT(audio_preproc != nullptr);
1014        audio_preproc->initialize();
1015    }
1016
1017    // get clip ctx based on chunk type
1018    clip_ctx * get_clip_ctx(const mtmd_input_chunk * chunk) const {
1019        if (chunk->type == MTMD_INPUT_CHUNK_TYPE_IMAGE) {
1020            return ctx_v;
1021        } else if (chunk->type == MTMD_INPUT_CHUNK_TYPE_AUDIO) {
1022            return ctx_a;
1023        }
1024        GGML_ABORT("unknown chunk type");
1025    }
1026
1027    projector_type proj_type_v() const {
1028        return ctx_v ? clip_get_projector_type(ctx_v) : PROJECTOR_TYPE_UNKNOWN;
1029    }
1030
1031    projector_type proj_type_a() const {
1032        return ctx_a ? clip_get_projector_type(ctx_a) : PROJECTOR_TYPE_UNKNOWN;
1033    }
1034
1035    int64_t n_embd_out() const {
1036        if (ctx_v) {
1037            return clip_n_mmproj_embd(ctx_v);
1038        } else if (ctx_a) {
1039            return clip_n_mmproj_embd(ctx_a);
1040        } else {
1041            throw std::runtime_error("no CLIP model loaded");
1042        }
1043    }
1044
1045    ~mtmd_context() {
1046        clip_free(ctx_a);
1047        clip_free(ctx_v);
1048        clip_free(ctx_gen_a);
1049    }
1050
1051private:
1052    llama_token lookup_token(const std::string & token_text) {
1053        if (vocab == nullptr) {
1054            // TODO @ngxson : this case is currently hit by mtmd_get_memory_usage
1055            // but we should reconsider this if this case is needed in other places in the future
1056            return LLAMA_TOKEN_NULL;
1057        }
1058        const int n_vocab = llama_vocab_n_tokens(vocab);
1059        for (int i = 0; i < n_vocab; i++) {
1060            if (token_to_piece(vocab, i, true) == token_text) {
1061                return i;
1062            }
1063        }
1064        return LLAMA_TOKEN_NULL;
1065    }
1066
1067    std::string token_to_piece(const llama_vocab * vocab, llama_token token, bool special) {
1068        if (vocab == nullptr) {
1069            throw std::runtime_error("llama_vocab is not provided");
1070        }
1071        std::string piece;
1072        piece.resize(piece.capacity());  // using string internal cache, 15 bytes + '\n'
1073        const int n_chars = llama_token_to_piece(vocab, token, &piece[0], piece.size(), 0, special);
1074        if (n_chars < 0) {
1075            piece.resize(-n_chars);
1076            int check = llama_token_to_piece(vocab, token, &piece[0], piece.size(), 0, special);
1077            GGML_ASSERT(check == -n_chars);
1078        } else {
1079            piece.resize(n_chars);
1080        }
1081        return piece;
1082    }
1083};
1084
1085mtmd_context * mtmd_init_from_file(const char * mmproj_fname,
1086        const struct llama_model * text_model,
1087        const struct mtmd_context_params ctx_params) {
1088    try {
1089        return new mtmd_context(mmproj_fname, text_model, ctx_params);
1090    } catch (const std::exception & e) {
1091        LOG_ERR("%s: error: %s\n", __func__, e.what());
1092        return nullptr;
1093    }
1094}
1095
1096void mtmd_free(mtmd_context * ctx) {
1097    delete ctx;
1098}
1099
1100std::vector<std::vector<const mtmd_bitmap *>> mtmd_group_mergeable_bitmaps(std::vector<mtmd_internal_part> & parts, int n_merge) {
1101    std::vector<std::vector<const mtmd_bitmap *>> output;
1102    for (size_t i = 0; i < parts.size(); i++) {
1103        if (parts[i].bitmap == nullptr) {
1104            continue; // text part
1105        }
1106        const bool has_next = n_merge > 1 && i + 1 < parts.size() && parts[i + 1].bitmap != nullptr;
1107        if (has_next && parts[i].bitmap->can_merge_with(*parts[i + 1].bitmap)) {
1108            LOG_DBG("%s: merging 2 frames at part index %zu and %zu\n", __func__, i, i + 1);
1109            output.push_back({parts[i].bitmap, parts[i + 1].bitmap});
1110            parts.erase(parts.begin() + i + 1);
1111            continue;
1112        }
1113        LOG_DBG("%s: no merging for part index %zu\n", __func__, i);
1114        output.push_back({parts[i].bitmap});
1115    }
1116    return output;
1117}
1118
1119struct mtmd_tokenizer {
1120    const mtmd_context * ctx;
1121
1122    std::string input_text; // note: can contain null bytes; do not use c_str()
1123    bool add_special;
1124    bool parse_special;
1125    const llama_vocab * vocab;
1126
1127    using part = mtmd_internal_part;
1128    std::vector<part> parts;
1129    // these will be freed when mtmd_tokenizer finishes
1130    std::vector<mtmd::bitmap> bm_from_lazy; // TODO @ngxson : refactor, free bm_from_lazy progressively
1131    std::vector<const char *> text_from_lazy;
1132
1133    mtmd_input_chunks cur;
1134    uint32_t n_images_added = 0; // 0-based index assigned to the next image chunk
1135
1136    ~mtmd_tokenizer() {
1137        // note: mtmd::bitmap is already RAII
1138        for (auto & str : text_from_lazy) {
1139            free((void *)str);
1140        }
1141    }
1142
1143    mtmd_tokenizer(const mtmd_context * ctx,
1144            const mtmd_input_text * text,
1145            const mtmd_bitmap * const * bmps,
1146            size_t n_bitmaps) : ctx(ctx) {
1147        add_special   = text->add_special;
1148        parse_special = text->parse_special;
1149        vocab         = ctx->vocab;
1150
1151        input_text.assign(text->text, text->text_len);
1152
1153        std::vector<const mtmd_bitmap *> bitmaps(bmps, bmps + n_bitmaps);
1154        auto parts_str = split_text(input_text, ctx->media_marker);
1155        size_t i_bm = 0;
1156        for (const auto & part : parts_str) {
1157            if (part == ctx->media_marker) {
1158                if (i_bm >= bitmaps.size()) {
1159                    throw std::runtime_error(string_format("number of media markers in text (%zu) exceeds number of bitmaps (%zu)", i_bm + 1, bitmaps.size()));
1160                }
1161                parts.push_back({"", bitmaps[i_bm++]});
1162            } else {
1163                parts.push_back({std::move(part), nullptr, parse_special});
1164            }
1165        }
1166
1167        size_t n_markers = 0;
1168        for (const auto & part : parts) {
1169            if (part.bitmap != nullptr) {
1170                n_markers++;
1171            }
1172        }
1173        if (n_markers != bitmaps.size()) {
1174            throw std::runtime_error(string_format("number of media markers in text (%zu) does not match number of bitmaps (%zu)", n_markers, bitmaps.size()));
1175        }
1176
1177        expand_lazy_bitmaps();
1178    }
1179
1180    mtmd_tokenizer(const mtmd_context * ctx,
1181            const mtmd_input_part * const * input_parts,
1182            size_t n_parts,
1183            bool add_special) : ctx(ctx) {
1184        this->add_special = add_special;
1185        parse_special = true; // only used for text returned by lazy bitmaps
1186        vocab         = ctx->vocab;
1187
1188        for (size_t i = 0; i < n_parts; i++) {
1189            const mtmd_input_part * p = input_parts[i];
1190            if (p->text != nullptr) {
1191                parts.push_back({std::string(p->text->text, p->text->text_len), nullptr, p->text->parse_special});
1192            } else {
1193                parts.push_back({"", p->bitmap});
1194            }
1195        }
1196
1197        expand_lazy_bitmaps();
1198    }
1199
1200    void expand_lazy_bitmaps() {
1201        std::vector<part> expanded;
1202        expanded.reserve(parts.size());
1203        for (auto & p : parts) {
1204            if (p.bitmap != nullptr && p.bitmap->lazy_callback) {
1205                LOG_DBG("%s: expanding lazy bitmap\n", __func__);
1206                for (size_t i = 0;; i++) {
1207                    char * out_str = nullptr;
1208                    mtmd_bitmap * out_bm = nullptr;
1209                    int res = p.bitmap->lazy_callback(i,
1210                                    p.bitmap->lazy_user_data,
1211                                    &out_bm,
1212                                    &out_str);
1213                    if (out_bm && out_str) {
1214                        throw std::runtime_error(string_format("lazy callback cannot return both bitmap and text"));
1215                    }
1216                    if (res == 0) {
1217                        // OK, append the returned chunk; lazy part is not yet added
1218                        if (out_bm) {
1219                            auto & ptr = bm_from_lazy.emplace_back(out_bm); // remember to free it later
1220                            expanded.push_back({"", ptr.ptr.get()});
1221                            LOG_DBG("%s: lazy callback returned bitmap with dimensions %d x %d\n", __func__, out_bm->nx, out_bm->ny);
1222                        } else if (out_str) {
1223                            auto & ptr = text_from_lazy.emplace_back(out_str); // remember to free it later
1224                            expanded.push_back({ptr, nullptr, parse_special});
1225                            LOG_DBG("%s: lazy callback returned text: %s\n", __func__, out_str);
1226                        }
1227                    } else if (res == -1) {
1228                        // EOF: lazy part removes itself (not added to expanded)
1229                        break;
1230                    } else if (res == -2) {
1231                        // error
1232                        throw std::runtime_error(string_format("lazy callback returned error"));
1233                    }
1234                }
1235            } else {
1236                expanded.push_back(std::move(p));
1237            }
1238        }
1239        parts = std::move(expanded);
1240    }
1241
1242    int32_t tokenize(mtmd_input_chunks * output) {
1243        cur.entries.clear();
1244
1245        // [QWEN_VIDEO] handle frame merging for models that support it (i.e. qwen-vl)
1246        int n_merge_frames = 1;
1247        if (ctx->ctx_v) {
1248            n_merge_frames = clip_model_n_temporal_merge(ctx->ctx_v);
1249            GGML_ASSERT(n_merge_frames <= 2 && "we only support merging maximum 2 images for now; open an issue if this model supports merging more");
1250        }
1251
1252        auto merged_bitmaps = mtmd_group_mergeable_bitmaps(parts, n_merge_frames);
1253
1254        size_t i_bm = 0;
1255        for (const auto & p : parts) {
1256            if (p.bitmap != nullptr) {
1257                if (i_bm >= merged_bitmaps.size()) {
1258                    LOG_ERR("%s: error: number of bitmaps (%zu) does not match number of markers (%zu)\n",
1259                            __func__, merged_bitmaps.size(), parts.size() - 1);
1260                    return 1;
1261                }
1262                auto bmps = merged_bitmaps[i_bm++];
1263                int32_t res = add_media(bmps);
1264                if (res != 0) {
1265                    return res;
1266                }
1267            } else {
1268                add_text(p.text, p.parse_special);
1269            }
1270        }
1271
1272        if (vocab != nullptr) {
1273            if (add_special && llama_vocab_get_add_bos(vocab)) {
1274                // if first chunk is text, we add BOS token to first text chunk
1275                // otherwise, create a new text chunk with BOS token
1276                if (!cur.entries.empty() && cur.entries[0].type == MTMD_INPUT_CHUNK_TYPE_TEXT) {
1277                    // add BOS token to the beginning of first text chunk
1278                    cur.entries[0].tokens_text.insert(cur.entries[0].tokens_text.begin(), llama_vocab_bos(vocab));
1279                } else {
1280                    // create a new text chunk with BOS token at the beginning
1281                    mtmd_input_chunk bos_chunk{
1282                        MTMD_INPUT_CHUNK_TYPE_TEXT,
1283                        {llama_vocab_bos(vocab)},
1284                        nullptr, // image tokens
1285                        nullptr, // audio tokens
1286                    };
1287                    cur.entries.insert(cur.entries.begin(), std::move(bos_chunk));
1288                }
1289            }
1290
1291            if (add_special && llama_vocab_get_add_eos(vocab)) {
1292                // if last chunk is text, we add EOS token to it
1293                add_text({llama_vocab_eos(vocab)});
1294            }
1295        }
1296
1297        if (i_bm != merged_bitmaps.size()) {
1298            LOG_ERR("%s: error: number of bitmaps (%zu) does not match number of markers (%zu)\n",
1299                    __func__, merged_bitmaps.size(), parts.size() - 1);
1300            return 1;
1301        }
1302
1303        *output = std::move(cur);
1304
1305        return 0;
1306    }
1307
1308    void add_text(const std::string & txt, bool parse_special) {
1309        if (vocab == nullptr) {
1310            throw std::runtime_error("llama_vocab is not provided");
1311        }
1312        LOG_DBG("%s: %s\n", __func__, txt.c_str());
1313        auto tokens = mtmd_tokenize_text_internal(vocab, txt, /* add_special */ false, parse_special);
1314        add_text(tokens);
1315    }
1316
1317    void add_text(const std::vector<llama_token> & tokens) {
1318        if (tokens.empty()) {
1319            return;
1320        }
1321        // if last entry is also a text chunk, add tokens to it instead of creating new chunk
1322        if (!cur.entries.empty() && cur.entries.back().type == MTMD_INPUT_CHUNK_TYPE_TEXT) {
1323            cur.entries.back().tokens_text.insert(
1324                                            cur.entries.back().tokens_text.end(),
1325                                            tokens.begin(),
1326                                            tokens.end());
1327        } else {
1328            mtmd_input_chunk chunk{
1329                MTMD_INPUT_CHUNK_TYPE_TEXT,
1330                tokens,
1331                nullptr, // image tokens
1332                nullptr, // audio tokens
1333            };
1334            cur.entries.emplace_back(std::move(chunk));
1335        }
1336    }
1337
1338    int32_t add_media(std::vector<const mtmd_bitmap *> & bitmaps) {
1339        GGML_ASSERT(!bitmaps.empty());
1340
1341        // note: only one type of media is supported per call, caller should enforce this
1342        const bool is_vision = !bitmaps[0]->is_audio;
1343
1344        if (is_vision) {
1345            // handle image
1346
1347            if (!ctx->ctx_v) {
1348                LOG_ERR("%s: error: model does not support vision input\n", __func__);
1349                return 2;
1350            }
1351
1352            if (!ctx->img_beg.empty()) {
1353                add_text(ctx->img_beg, true); // add image begin token
1354            }
1355
1356            // TODO @ngxson : this is quite hacky because preprocessor only support batch with one single element, that need to be fixed in the future (e.g. by changing the preprocessor interface always take single input)
1357
1358            mtmd_image_preproc_out preproc_out;
1359
1360            for (const auto * bmp : bitmaps) {
1361                // sanity check
1362                GGML_ASSERT(!bmp->is_audio);
1363                GGML_ASSERT(ctx->image_preproc != nullptr);
1364                if (bmp->nx <= 0 || bmp->ny <= 0) {
1365                    LOG_ERR("%s: error: invalid bitmap dimensions: nx = %d, ny = %d\n",
1366                            __func__, bmp->nx, bmp->ny);
1367                    return 2;
1368                }
1369
1370                // convert mtmd_bitmap to clip_image_u8
1371                clip_image_u8 img_u8;
1372                img_u8.set_size(
1373                    {(int)bmp->nx, (int)bmp->ny},
1374                    bmp->is_placeholder());
1375                img_u8.cpy_buf(bmp->get_ro_buf());
1376
1377                // preprocess image
1378                mtmd_image_preproc_out tmp_preproc_out = ctx->image_preproc->preprocess(img_u8);
1379
1380                // move entries and grid dimensions to the "global" preproc_out
1381                for (auto & entry : tmp_preproc_out.entries) {
1382                    preproc_out.entries.emplace_back(std::move(entry));
1383                }
1384
1385                // for llava-uhd style, we need to handle grid too
1386                // we don't care about overwriting these values for now because the case where bitmaps.size() > 1 is only for frame merging (qwen-vl), not supported by llava-uhd
1387                if ((tmp_preproc_out.grid_x > 0 && tmp_preproc_out.grid_y > 0)
1388                        || tmp_preproc_out.has_overview()) {
1389                    GGML_ASSERT(bitmaps.size() == 1);
1390                    preproc_out.grid_x = tmp_preproc_out.grid_x;
1391                    preproc_out.grid_y = tmp_preproc_out.grid_y;
1392                    preproc_out.overview = std::move(tmp_preproc_out.overview);
1393                }
1394            }
1395
1396            LOG_DBG("%s: preproc_out has %zu entries, grid_x = %d, grid_y = %d, has_overview = %d\n",
1397                    __func__, preproc_out.entries.size(), preproc_out.grid_x, preproc_out.grid_y,
1398                    preproc_out.has_overview() ? 1 : 0);
1399
1400            // handle llava-uhd style preprocessing
1401            // (output either a grid, or overview-only)
1402            const bool has_tiling_grid = (preproc_out.grid_x > 0 && preproc_out.grid_y > 0)
1403                || preproc_out.has_overview();
1404
1405            if (has_tiling_grid) {
1406                // [QWEN_VIDEO] we do not support "frame merging" for llama-uhd style, so no batching for now
1407                GGML_ASSERT(bitmaps.size() == 1);
1408
1409                const int n_col = preproc_out.grid_x;
1410                const int n_row = preproc_out.grid_y;
1411
1412                // split batch into chunks of single images
1413                auto chunks = split_batch_to_chunk(std::move(preproc_out), bitmaps[0]->id);
1414                GGML_ASSERT(chunks.size() > 0);
1415
1416                // NOTE: preproc_out is invalidated after this point, do not use it anymore
1417
1418                // split_batch_to_chunk must always put the overview image first
1419                auto ov_chunk = std::move(chunks.front());
1420                chunks.erase(chunks.begin());
1421
1422                // add overview image (first)
1423                if (ctx->ov_img_first) {
1424                    add_text(ctx->tok_ov_img_start);
1425                    cur.entries.emplace_back(std::move(ov_chunk));
1426                    add_text(ctx->tok_ov_img_end);
1427                }
1428
1429                // add slices (or tiles)
1430                if (!chunks.empty()) {
1431                    LOG_DBG("%s: adding %d slices (%d rows x %d cols)\n", __func__, (int)chunks.size(), n_row, n_col);
1432                    GGML_ASSERT((int)chunks.size() == n_row * n_col);
1433                    add_text(ctx->tok_slices_start);
1434                    for (int y = 0; y < n_row; y++) {
1435                        for (int x = 0; x < n_col; x++) {
1436                            const bool is_last_in_row = (x == n_col - 1);
1437                            if (!ctx->tok_sli_img_start.empty()) {
1438                                add_text(ctx->tok_sli_img_start);
1439                            } else if (!ctx->sli_img_start_tmpl.empty()) {
1440                                // If using a template to preceed a slice image
1441                                const size_t sz = std::snprintf(nullptr, 0, ctx->sli_img_start_tmpl.c_str(), y+1, x+1) + 1;
1442                                std::unique_ptr<char[]> buf(new char[sz]);
1443                                std::snprintf(buf.get(), sz, ctx->sli_img_start_tmpl.c_str(), y+1, x+1);
1444                                add_text(std::string(buf.get(), buf.get() + sz - 1), true);
1445                            }
1446
1447                            auto & curr_chunk = chunks[y * n_col + x];
1448                            auto & curr_batch = curr_chunk.tokens_image->batch_f32;
1449                            if (curr_batch.entries.size() != 1) {
1450                                throw std::runtime_error(string_format("%s: expect 1 image in batch_f32", __func__));
1451                            }
1452
1453                            LOG_DBG("%s: adding slice image at row %d col %d\n", __func__, y, x);
1454                            cur.entries.emplace_back(std::move(curr_chunk));
1455
1456                            add_text(ctx->tok_sli_img_end);
1457                            if (!is_last_in_row) {
1458                                add_text(ctx->tok_sli_img_mid);
1459                            }
1460                        }
1461                        if ((y != n_row - 1 || ctx->tok_row_end_trail)) {
1462                            add_text(ctx->tok_row_end);
1463                        }
1464                    }
1465                    add_text(ctx->tok_slices_end);
1466                }
1467
1468                // add overview image (last)
1469                if (!ctx->ov_img_first) {
1470                    add_text(ctx->tok_ov_img_start);
1471                    cur.entries.emplace_back(std::move(ov_chunk));
1472                    add_text(ctx->tok_ov_img_end);
1473                }
1474            } else {
1475
1476                if (preproc_out.entries.size() == 0) {
1477                    LOG_ERR("%s: no image tokens produced by preprocessor (ref: https://github.com/ggml-org/llama.cpp/pull/24769)\n", __func__);
1478                    return 2;
1479                }
1480
1481                if (ctx->proj_type_v() == PROJECTOR_TYPE_DEEPSEEK4V) {
1482                    // the text model perceives input in blocks of N tokens (N = COMPRESS_PAD_TO = 4, same as the CSA compress ratio)
1483                    // image need to be aligned to block size, while adding IMAGE_PAD embeddings to the beginning
1484                    // TODO @ngxson : maybe refactor this in the future
1485                    constexpr int32_t align = 4;
1486                    size_t n_past = 0;
1487                    for (const auto & e : cur.entries) {
1488                        n_past += mtmd_input_chunk_get_n_tokens(&e);
1489                    }
1490                    preproc_out.entries[0].lead_pad = align - 1 - (int32_t)(n_past % align);
1491                }
1492
1493                size_t n_tokens = 0;
1494                for (auto & e : preproc_out.entries) {
1495                    n_tokens += clip_n_output_tokens(ctx->ctx_v, &e);
1496                    if (clip_model_n_temporal_merge(ctx->ctx_v) == 2) {
1497                        // [QWEN_VIDEO] pair input is merged to the same embd, so only count as one image
1498                        break;
1499                    }
1500                }
1501
1502                mtmd_image_tokens_ptr image_tokens(new mtmd_image_tokens);
1503
1504                // [QWEN_VIDEO] improve this in the future
1505                image_tokens->n_temporal_merge = clip_model_n_temporal_merge(ctx->ctx_v);
1506
1507                if (mtmd_decode_use_mrope(ctx)) {
1508                    // for Qwen2VL, we need this information for M-RoPE decoding positions
1509                    image_tokens->nx = clip_n_output_tokens_x(ctx->ctx_v, &preproc_out.entries[0]);
1510                    image_tokens->ny = clip_n_output_tokens_y(ctx->ctx_v, &preproc_out.entries[0]);
1511                } else {
1512                    // other models, we only need the total number of tokens
1513                    image_tokens->nx = n_tokens;
1514                    image_tokens->ny = 1;
1515                }
1516                image_tokens->pos = ctx->pos_type;
1517                // HunyuanVL wraps the image grid with BOI/EOI and adds one newline per row,
1518                // and uses XD-RoPE (dim-3 = image index). Override the position type so that
1519                // n_tokens() and mtmd_image_tokens_get_decoder_pos pick the HunyuanVL layout.
1520                if (ctx->proj_type_v() == PROJECTOR_TYPE_HUNYUANVL) {
1521                    image_tokens->pos       = MTMD_POS_TYPE_HUNYUANVL;
1522                    image_tokens->image_idx = n_images_added;
1523                    GGML_ASSERT(n_tokens == (size_t)image_tokens->n_tokens());
1524                }
1525
1526                clip_image_f32_batch batch_f32;
1527                batch_f32.is_audio = false;
1528                batch_f32.entries = std::move(preproc_out.entries);
1529                // do NOT use preproc_out from this point on, it's moved
1530
1531                image_tokens->batch_f32 = std::move(batch_f32);
1532                image_tokens->id = bitmaps[0]->id; // optional
1533
1534                LOG_DBG("image_tokens->nx = %d\n", image_tokens->nx);
1535                LOG_DBG("image_tokens->ny = %d\n", image_tokens->ny);
1536                LOG_DBG("batch_f32 size = %d\n", (int)image_tokens->batch_f32.entries.size());
1537
1538                mtmd_input_chunk chunk{
1539                    MTMD_INPUT_CHUNK_TYPE_IMAGE,
1540                    {}, // text tokens
1541                    std::move(image_tokens),
1542                    nullptr, // audio tokens
1543                };
1544                cur.entries.emplace_back(std::move(chunk));
1545            }
1546
1547            if (!ctx->img_end.empty()) {
1548                add_text(ctx->img_end, true); // add image end token
1549            }
1550
1551            // advance image-chunk counter so the next image gets the next XD-RoPE dim-3 slot
1552            n_images_added++;
1553
1554        } else {
1555            // handle audio
1556
1557            GGML_ASSERT(bitmaps.size() == 1); // no batching support for now
1558            auto & bitmap = bitmaps[0];
1559
1560            if (!ctx->ctx_a) {
1561                LOG_ERR("%s: error: model does not support audio input\n", __func__);
1562                return 2;
1563            }
1564
1565            if (bitmap->nx == 0) {
1566                LOG_ERR("%s: error: empty audio data\n", __func__);
1567                return 2;
1568            }
1569
1570            if (!ctx->aud_beg.empty()) {
1571                add_text(ctx->aud_beg, true); // add audio begin token
1572            }
1573
1574            // sanity check
1575            GGML_ASSERT(ctx->audio_preproc != nullptr);
1576
1577            // preprocess audio
1578            std::vector<mtmd_audio_mel> mel_spec_chunks;
1579            {
1580                std::vector<float> dummy;
1581                const float * samples = nullptr;
1582                size_t n_samples = 0;
1583                if (bitmap->is_placeholder()) {
1584                    // TODO @ngxson : skip underlay processing if bitmap is placeholder
1585                    GGML_ASSERT(bitmap->ny == 1);
1586
1587                    dummy.resize(bitmap->nx);
1588                    samples = dummy.data();
1589                    n_samples = dummy.size();
1590                } else {
1591                    const auto & buf = bitmap->get_ro_buf();
1592                    GGML_ASSERT(buf.size() > sizeof(float));
1593                    GGML_ASSERT(buf.size() % sizeof(float) == 0);
1594
1595                    samples = (const float *)buf.data();
1596                    n_samples = buf.size() / sizeof(float);
1597                }
1598                bool ok = ctx->audio_preproc->preprocess(samples, n_samples, mel_spec_chunks);
1599                if (!ok) {
1600                    LOG_ERR("Unable to preprocess audio\n");
1601                    return 2;
1602                }
1603            }
1604
1605            // consider each mel_spec as a separate audio chunk
1606            // TODO: maybe support batching, but this may come with memory cost
1607            for (auto & mel_spec : mel_spec_chunks) {
1608                const bool is_placeholder = mel_spec.data.empty();
1609
1610                // Validate dimensions fit in clip_image_size (int)
1611                GGML_ASSERT(mel_spec.n_len <= INT32_MAX && mel_spec.n_len >= 0);
1612                GGML_ASSERT(mel_spec.n_mel <= INT32_MAX && mel_spec.n_mel >= 0);
1613                clip_image_f32 mel_f32;
1614                mel_f32.set_size(
1615                    {(int)mel_spec.n_len, (int)mel_spec.n_mel},
1616                    is_placeholder, /* is_audio */ true);
1617                mel_f32.cpy_buf(mel_spec.data);
1618
1619                size_t n_tokens = clip_n_output_tokens(ctx->ctx_a, &mel_f32);
1620
1621                clip_image_f32_batch batch_f32;
1622                batch_f32.is_audio = true;
1623                batch_f32.entries.push_back(std::move(mel_f32));
1624
1625                mtmd_audio_tokens_ptr audio_tokens(new mtmd_audio_tokens);
1626                audio_tokens->n_tokens = n_tokens;
1627                audio_tokens->batch_f32 = std::move(batch_f32);
1628                audio_tokens->id = bitmap->id; // optional
1629
1630                LOG_DBG("audio_tokens->n_tokens = %d\n", audio_tokens->n_tokens);
1631
1632                mtmd_input_chunk chunk{
1633                    MTMD_INPUT_CHUNK_TYPE_AUDIO,
1634                    {}, // text tokens
1635                    nullptr, // image tokens
1636                    std::move(audio_tokens),
1637                };
1638                cur.entries.emplace_back(std::move(chunk));
1639            }
1640
1641            if (!ctx->aud_end.empty()) {
1642                add_text(ctx->aud_end, true); // add audio end token
1643            }
1644        }
1645
1646        return 0;
1647    }
1648
1649    std::vector<mtmd_input_chunk> split_batch_to_chunk(mtmd_image_preproc_out && preproc_out, const std::string & id) {
1650        std::vector<mtmd_input_chunk> chunks;
1651
1652        auto process_chunk = [&](clip_image_f32 && img) {
1653            mtmd_image_tokens_ptr image_tokens(new mtmd_image_tokens);
1654            image_tokens->nx = clip_n_output_tokens(ctx->ctx_v, &img);
1655            image_tokens->ny = 1;
1656            image_tokens->batch_f32.entries.push_back(std::move(img));
1657            image_tokens->id = id;
1658
1659            GGML_ASSERT(image_tokens->nx > 0);
1660
1661            mtmd_input_chunk chunk{
1662                MTMD_INPUT_CHUNK_TYPE_IMAGE,
1663                {}, // text tokens
1664                std::move(image_tokens),
1665                nullptr, // audio tokens
1666            };
1667            chunks.emplace_back(std::move(chunk));
1668        };
1669
1670        // overview image first
1671        auto & overview = preproc_out.overview;
1672        if (overview.nx() == 0 || overview.ny() == 0) {
1673            throw std::runtime_error(string_format("%s: invalid overview image for llava-uhd style preprocessing\n", __func__));
1674        }
1675        process_chunk(std::move(preproc_out.overview));
1676
1677        // then, process slices
1678        for (auto & entry : preproc_out.entries) {
1679            if (entry.nx() == 0 || entry.ny() == 0) {
1680                throw std::runtime_error(string_format("%s: invalid image slice for llava-uhd style preprocessing\n", __func__));
1681            }
1682            process_chunk(std::move(entry));
1683        }
1684
1685        return chunks;
1686    }
1687
1688    // for example: "a <__media__> b <__media__> c" --> "a", "<__media__>", "b", "<__media__>", "c"
1689    static std::vector<std::string> split_text(const std::string & input, const std::string & delimiter) {
1690        std::vector<std::string> result;
1691        if (input.empty()) {
1692            return result;
1693        }
1694        size_t start = 0;
1695        size_t pos = 0;
1696        while ((pos = input.find(delimiter, start)) != std::string::npos) {
1697            if (pos > start) {
1698                result.push_back(input.substr(start, pos - start));
1699            }
1700            result.push_back(delimiter);
1701            start = pos + delimiter.length();
1702        }
1703        if (start < input.length()) {
1704            result.push_back(input.substr(start));
1705        }
1706        return result;
1707    }
1708
1709    // copied from common_tokenize
1710    static std::vector<llama_token> mtmd_tokenize_text_internal(
1711        const struct llama_vocab * vocab,
1712               const std::string & text,
1713                            bool   add_special,
1714                            bool   parse_special) {
1715        if (vocab == nullptr) {
1716            throw std::runtime_error("llama_vocab is not provided");
1717        }
1718        // upper limit for the number of tokens
1719        int n_tokens = text.length() + 2 * add_special;
1720        std::vector<llama_token> result(n_tokens);
1721        n_tokens = llama_tokenize(vocab, text.data(), text.length(), result.data(), result.size(), add_special, parse_special);
1722        if (n_tokens == std::numeric_limits<int32_t>::min()) {
1723            throw std::runtime_error("Tokenization failed: input text too large, tokenization result exceeds int32_t limit");
1724        }
1725        if (n_tokens < 0) {
1726            result.resize(-n_tokens);
1727            int check = llama_tokenize(vocab, text.data(), text.length(), result.data(), result.size(), add_special, parse_special);
1728            GGML_ASSERT(check == -n_tokens);
1729        } else {
1730            result.resize(n_tokens);
1731        }
1732        return result;
1733    }
1734};
1735
1736int32_t mtmd_tokenize(const mtmd_context * ctx,
1737            mtmd_input_chunks * output,
1738            const mtmd_input_text * text,
1739            const mtmd_bitmap * const * bitmaps,
1740            size_t n_bitmaps) {
1741    try {
1742        mtmd_tokenizer tokenizer(ctx, text, bitmaps, n_bitmaps);
1743        return tokenizer.tokenize(output);
1744    } catch (const std::exception & e) {
1745        LOG_ERR("%s: error: %s\n", __func__, e.what());
1746        return 2;
1747    }
1748}
1749
1750int32_t mtmd_tokenize_from_parts(const mtmd_context * ctx,
1751            mtmd_input_chunks * output,
1752            const mtmd_input_part * const * parts,
1753            size_t n_parts,
1754            bool add_special) {
1755    for (size_t i = 0; i < n_parts; i++) {
1756        if ((parts[i]->text == nullptr) == (parts[i]->bitmap == nullptr)) {
1757            LOG_ERR("%s: part %zu must have either text or bitmap set, not both\n", __func__, i);
1758            return 1;
1759        }
1760        if (parts[i]->text != nullptr && parts[i]->text->text == nullptr) {
1761            LOG_ERR("%s: part %zu has null text pointer\n", __func__, i);
1762            return 1;
1763        }
1764    }
1765    try {
1766        mtmd_tokenizer tokenizer(ctx, parts, n_parts, add_special);
1767        return tokenizer.tokenize(output);
1768    } catch (const std::exception & e) {
1769        LOG_ERR("%s: error: %s\n", __func__, e.what());
1770        return 2;
1771    }
1772}
1773
1774static int32_t mtmd_encode_impl(mtmd_context * ctx, const mtmd_image_tokens * image_tokens, std::vector<float> & out_embd) {
1775    clip_ctx * ctx_clip = ctx->ctx_v;
1776    if (!ctx_clip) {
1777        LOG_ERR("%s: this API does not support non-vision input, please use mtmd_encode_chunk instead\n", __func__);
1778        return 1;
1779    }
1780
1781    int n_embd_out = ctx->n_embd_out();
1782    auto n_tokens_out = image_tokens->n_tokens();
1783    out_embd.resize((size_t)n_embd_out * n_tokens_out);
1784
1785    if (image_tokens->is_placeholder()) {
1786        LOG_ERR("%s: image tokens batch is placeholder\n", __func__);
1787        return 1;
1788    }
1789
1790    bool ok = clip_image_batch_encode(
1791        ctx_clip,
1792        ctx->n_threads,
1793        &image_tokens->batch_f32,
1794        out_embd);
1795
1796    return ok ? 0 : 1;
1797}
1798
1799static int32_t mtmd_encode_chunk_impl(mtmd_context * ctx, const mtmd_input_chunk * chunk, std::vector<float> & out_embd) {
1800    if (chunk->type == MTMD_INPUT_CHUNK_TYPE_TEXT) {
1801        LOG_WRN("mtmd_encode_chunk has no effect for text chunks\n");
1802        return 0;
1803    } else if (chunk->type == MTMD_INPUT_CHUNK_TYPE_IMAGE) {
1804        if (!ctx->ctx_v) {
1805            LOG_ERR("%s: model does not support vision input\n", __func__);
1806            return 1;
1807        }
1808        if (chunk->tokens_image == nullptr) {
1809            LOG_ERR("%s: image tokens are null\n", __func__);
1810            return 1;
1811        }
1812        if (chunk->tokens_image->is_placeholder()) {
1813            LOG_ERR("%s: image tokens batch is placeholder\n", __func__);
1814            return 1;
1815        }
1816        return mtmd_encode_impl(ctx, chunk->tokens_image.get(), out_embd);
1817    } else if (chunk->type == MTMD_INPUT_CHUNK_TYPE_AUDIO) {
1818        if (!ctx->ctx_a) {
1819            LOG_ERR("%s: model does not support audio input\n", __func__);
1820            return 1;
1821        }
1822        if (chunk->tokens_audio == nullptr) {
1823            LOG_ERR("%s: audio tokens are null\n", __func__);
1824            return 1;
1825        }
1826        if (chunk->tokens_audio->is_placeholder()) {
1827            LOG_ERR("%s: audio tokens batch is placeholder\n", __func__);
1828            return 1;
1829        }
1830        int n_mmproj_embd = ctx->n_embd_out();
1831        out_embd.resize((size_t)chunk->tokens_audio->n_tokens * n_mmproj_embd);
1832        bool ok = clip_image_batch_encode(
1833            ctx->ctx_a,
1834            ctx->n_threads,
1835            &chunk->tokens_audio->batch_f32,
1836            out_embd);
1837        return ok ? 0 : 1;
1838    }
1839
1840    LOG_ERR("%s: unknown chunk type %d\n", __func__, (int)chunk->type);
1841    return 1;
1842}
1843
1844int32_t mtmd_encode_chunk(mtmd_context * ctx, const mtmd_input_chunk * chunk) {
1845    // this is the non-batching version
1846    try {
1847        return mtmd_encode_chunk_impl(ctx, chunk, ctx->out_embd);
1848    } catch (const std::exception & e) {
1849        LOG_ERR("%s: error: %s\n", __func__, e.what());
1850        return 1;
1851    }
1852}
1853
1854int32_t mtmd_encode(mtmd_context * ctx, const mtmd_image_tokens * image_tokens) {
1855    try {
1856        return mtmd_encode_impl(ctx, image_tokens, ctx->out_embd);
1857    } catch (const std::exception & e) {
1858        LOG_ERR("%s: error: %s\n", __func__, e.what());
1859        return 1;
1860    }
1861}
1862
1863float * mtmd_get_output_embd(mtmd_context * ctx) {
1864    return ctx->out_embd.data();
1865}
1866
1867//
1868// audio generation
1869//
1870
1871mtmd_gen_audio_info mtmd_gen_audio_get_info(const mtmd_context * ctx) {
1872    mtmd_gen_audio_info info{};
1873    info.model_variant = "";
1874    if (!ctx->ctx_gen_a) {
1875        info.type = MTMD_GEN_AUDIO_TYPE_NONE;
1876        return info;
1877    }
1878    info.model_variant = clip_get_hparams(ctx->ctx_gen_a)->gen_model_variant.c_str();
1879    switch (clip_get_projector_type(ctx->ctx_gen_a)) {
1880        case PROJECTOR_TYPE_QWEN3TTS_GEN:
1881            info.type = MTMD_GEN_AUDIO_TYPE_QWEN3TTS;
1882            info.sample_rate = 24000;
1883            break;
1884        case PROJECTOR_TYPE_POCKETTTS_GEN:
1885            info.type = MTMD_GEN_AUDIO_TYPE_POCKETTTS;
1886            info.sample_rate = 24000;
1887            break;
1888        default:
1889            info.type = MTMD_GEN_AUDIO_TYPE_NONE;
1890            break;
1891    }
1892    return info;
1893}
1894
1895mtmd_gen_inp mtmd_gen_inp_default(const mtmd_context * ctx) {
1896    mtmd_gen_inp inp{};
1897    inp.type = MTMD_GEN_PROCESS_TYPE_GEN_CODE;
1898    inp.seed = UINT32_MAX;
1899    if (!ctx->ctx_gen_a) {
1900        return inp;
1901    }
1902
1903    switch (clip_get_projector_type(ctx->ctx_gen_a)) {
1904        case PROJECTOR_TYPE_QWEN3TTS_GEN:
1905            // https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-Base/blob/main/generation_config.json
1906            inp.top_k = 50;
1907            inp.top_p = 1.0f;
1908            inp.temp  = 0.9f; // TODO: handle this on graph
1909            break;
1910        case PROJECTOR_TYPE_POCKETTTS_GEN:
1911            // https://github.com/kyutai-labs/pocket-tts/blob/main/pocket_tts/default_parameters.py
1912            inp.top_k = 50;
1913            inp.top_p = 1.0f;
1914            inp.temp  = 0.7f;
1915            break;
1916        default:
1917            break;
1918    }
1919    return inp;
1920}
1921
1922static int32_t mtmd_gen_audio_process_impl(mtmd_context * ctx, const mtmd_gen_inp * inp, mtmd_gen_out * out) {
1923    clip_ctx * ctx_clip = ctx->ctx_gen_a;
1924    if (!ctx_clip) {
1925        LOG_ERR("%s: model does not support audio generation\n", __func__);
1926        return 1;
1927    }
1928
1929    *out = {};
1930
1931    if (inp->type == MTMD_GEN_PROCESS_TYPE_GEN_CODE) {
1932        const size_t n_embd = (size_t) clip_n_mmproj_embd(ctx_clip);
1933
1934        clip_image_f32 hidden_state;
1935        hidden_state.set_size({(int) n_embd, 1}, false, true);
1936        hidden_state.cpy_buf(std::vector<float>(inp->embd, inp->embd + n_embd));
1937
1938        clip_image_f32_batch batch;
1939        batch.is_audio = true;
1940        batch.entries.push_back(std::move(hidden_state));
1941
1942        std::vector<float>   out_embd(n_embd);
1943        std::vector<int32_t> out_codes;
1944        std::vector<float>   out_feats;
1945        bool is_eos = false;
1946
1947        clip_encode_params params;
1948        params.imgs          = &batch;
1949        params.n_threads     = ctx->n_threads;
1950        params.gen_process   = CLIP_GEN_PROCESS_GEN_CODE;
1951        params.out_embd      = &out_embd;
1952        params.out_codes     = &out_codes;
1953        params.out_feats     = &out_feats;
1954        params.code0         = inp->code0;
1955        params.top_k         = inp->top_k;
1956        params.top_p         = inp->top_p;
1957        params.seed          = inp->seed;
1958        params.temp          = inp->temp;
1959        params.out_is_eos    = &is_eos;
1960
1961        if (!clip_encode(ctx_clip, &params)) {
1962            LOG_ERR("%s: clip_encode failed (gen_code)\n", __func__);
1963            return 1;
1964        }
1965
1966        ctx->gen_out_embd  = std::move(out_embd);
1967        ctx->gen_out_codes = std::move(out_codes);
1968        ctx->gen_out_feats = std::move(out_feats);
1969
1970        out->embd      = ctx->gen_out_embd.data();
1971        out->codes     = ctx->gen_out_codes.data();
1972        out->n_codes   = ctx->gen_out_codes.size();
1973        out->feats     = ctx->gen_out_feats.data();
1974        out->n_feats   = ctx->gen_out_feats.size();
1975        out->is_eos    = is_eos;
1976        return 0;
1977    }
1978
1979    // MTMD_GEN_PROCESS_TYPE_GEN_WAV
1980    const bool has_codes = inp->codes && inp->n_codes > 0;
1981    const bool has_feats = inp->feats && inp->n_feats > 0;
1982    if (has_codes == has_feats) {
1983        LOG_ERR("%s: gen_wav requires exactly one of codes or feats\n", __func__);
1984        return 1;
1985    }
1986    std::vector<int32_t> in_codes;
1987    std::vector<float>   in_feats;
1988    if (has_codes) {
1989        in_codes.assign(inp->codes, inp->codes + inp->n_codes);
1990    } else {
1991        in_feats.assign(inp->feats, inp->feats + inp->n_feats);
1992    }
1993    std::vector<uint8_t> in_state;
1994    if (inp->state_data) {
1995        in_state.assign(inp->state_data, inp->state_data + inp->state_size);
1996    }
1997
1998    // gen_wav has no hidden-state input, the batch entry is an unused placeholder
1999    // TODO @ngxson : some models in the future may require hidden-state input, need to update this code later
2000    clip_image_f32 dummy;
2001    dummy.set_size({1, 1}, false, true);
2002    dummy.cpy_buf(std::vector<float>(1, 0.0f));
2003
2004    clip_image_f32_batch batch;
2005    batch.is_audio = true;
2006    batch.entries.push_back(std::move(dummy));
2007
2008    clip_encode_params params;
2009    params.imgs        = &batch;
2010    params.n_threads   = ctx->n_threads;
2011    params.gen_process = CLIP_GEN_PROCESS_GEN_WAV;
2012    // gen_wav draws no randomness, but keep the seed so it does not reseed mid-generation
2013    params.seed        = inp->seed;
2014    params.codes       = has_codes ? &in_codes : nullptr;
2015    params.feats       = has_feats ? &in_feats : nullptr;
2016    params.out_audio   = &ctx->gen_out_audio;
2017    params.state_in    = inp->state_data ? &in_state : nullptr;
2018    params.state_out   = &ctx->gen_out_state;
2019
2020    if (!clip_encode(ctx_clip, &params)) {
2021        LOG_ERR("%s: clip_encode failed (code2wav)\n", __func__);
2022        return 1;
2023    }
2024
2025    out->audio      = ctx->gen_out_audio.data();
2026    out->n_samples  = ctx->gen_out_audio.size();
2027    out->state_data = (const char *) ctx->gen_out_state.data();
2028    out->state_size = ctx->gen_out_state.size();
2029
2030    return 0;
2031}
2032
2033int32_t mtmd_gen_audio_process(mtmd_context * ctx, const struct mtmd_gen_inp * inp, struct mtmd_gen_out * out) {
2034    try {
2035        return mtmd_gen_audio_process_impl(ctx, inp, out);
2036    } catch (const std::exception & e) {
2037        LOG_ERR("%s: error: %s\n", __func__, e.what());
2038        return 1;
2039    }
2040}
2041
2042mtmd_batch * mtmd_batch_init(mtmd_context * ctx) {
2043    return new mtmd_batch(ctx);
2044}
2045
2046void mtmd_batch_free(mtmd_batch * batch) {
2047    if (batch) {
2048        delete batch;
2049    }
2050}
2051
2052int32_t mtmd_batch_add_chunk(mtmd_batch * batch, const mtmd_input_chunk * chunk) {
2053    if (chunk->type == MTMD_INPUT_CHUNK_TYPE_TEXT) {
2054        LOG_ERR("%s: text chunk is not supported in batch\n", __func__);
2055        return 1;
2056    }
2057
2058    auto * ctx = batch->ctx->get_clip_ctx(chunk);
2059    if (!ctx) {
2060        LOG_ERR("%s: model does not support input chunk type %d\n", __func__, (int)chunk->type);
2061        return 1;
2062    }
2063
2064    if (batch->entries.empty()) {
2065        // batch must have at least one chunk
2066        batch->entries.push_back(chunk);
2067        return 0;
2068    }
2069
2070    if (!clip_support_batch(ctx)) {
2071        // if no batching support, batch can only have one single chunk
2072        return 2; // "batch too large" error code
2073    }
2074
2075    int32_t new_n_tokens = batch->n_tokens() + (int32_t)mtmd_input_chunk_get_n_tokens(chunk);
2076    if (new_n_tokens > batch->ctx->batch_max_tokens) {
2077        return 2; // "batch too large" error code
2078    }
2079
2080    auto & first_chunk = batch->entries[0];
2081    if (first_chunk->can_batch_with(*chunk)) {
2082        batch->entries.push_back(chunk);
2083        return 0;
2084    }
2085
2086    return 3; // "cannot batch" error code
2087}
2088
2089static int32_t mtmd_batch_encode_impl(mtmd_batch * batch) {
2090    if (batch->entries.empty()) {
2091        LOG_ERR("%s: batch is empty\n", __func__);
2092        return 1;
2093    }
2094    for (const auto * chunk : batch->entries) {
2095        if (chunk->is_placeholder()) {
2096            LOG_ERR("%s: chunk is placeholder\n", __func__);
2097            return 1;
2098        }
2099    }
2100
2101    // represent the whole batch as one single chunk
2102    mtmd::input_chunk_ptr batch_chunk(mtmd_input_chunk_copy(batch->entries[0]));
2103    if (batch_chunk->tokens_image) {
2104        auto & b0_f32 = batch_chunk->tokens_image->batch_f32;
2105        // copy all entries from other chunks into the first chunk's batch_f32
2106        // note: skip first entry because it's already in batch_chunk
2107        for (size_t ic = 1; ic < batch->entries.size(); ic++) {
2108            auto & chunk = batch->entries[ic];
2109            GGML_ASSERT(chunk->tokens_image);
2110            auto b1_f32 = chunk->tokens_image->batch_f32.clone();
2111            for (size_t i = 0; i < b1_f32.entries.size(); i++) {
2112                b0_f32.entries.push_back(std::move(b1_f32.entries[i]));
2113            }
2114        }
2115    } else if (batch_chunk->tokens_audio) {
2116        auto & b0_f32 = batch_chunk->tokens_audio->batch_f32;
2117        // copy all entries from other chunks into the first chunk's batch_f32
2118        // note: skip first entry because it's already in batch_chunk
2119        for (size_t ic = 1; ic < batch->entries.size(); ic++) {
2120            auto & chunk = batch->entries[ic];
2121            GGML_ASSERT(chunk->tokens_audio);
2122            auto b1_f32 = chunk->tokens_audio->batch_f32.clone();
2123            for (size_t i = 0; i < b1_f32.entries.size(); i++) {
2124                b0_f32.entries.push_back(std::move(b1_f32.entries[i]));
2125            }
2126        }
2127    } else {
2128        LOG_ERR("%s: unsupported chunk type\n", __func__);
2129        return 1;
2130    }
2131
2132    LOG_DBG("%s: encoding batch with %zu entries and total %zu tokens\n",
2133            __func__, batch->entries.size(), mtmd_input_chunk_get_n_tokens(batch_chunk.get()));
2134    int32_t res = mtmd_encode_chunk_impl(
2135        batch->ctx,
2136        batch_chunk.get(),
2137        batch->output_embd);
2138    return res;
2139}
2140
2141int32_t mtmd_batch_encode(mtmd_batch * batch) {
2142    try {
2143        return mtmd_batch_encode_impl(batch);
2144    } catch (const std::exception & e) {
2145        LOG_ERR("%s: error: %s\n", __func__, e.what());
2146        return 1;
2147    }
2148}
2149
2150float * mtmd_batch_get_output_embd(mtmd_batch * batch, const mtmd_input_chunk * chunk) {
2151    if (batch->output_embd.empty()) {
2152        LOG_ERR("%s: batch has not been encoded yet\n", __func__);
2153        return nullptr;
2154    }
2155    size_t offset = 0;
2156    const size_t n_embd = batch->ctx->n_embd_out();
2157    for (const auto * c : batch->entries) {
2158        size_t offset_prev = offset;
2159        size_t n_tokens = mtmd_input_chunk_get_n_tokens(c);
2160        offset += n_tokens * n_embd;
2161        GGML_ASSERT(offset_prev <  batch->output_embd.size());
2162        GGML_ASSERT(offset      <= batch->output_embd.size());
2163        if (c == chunk) {
2164            return &batch->output_embd.data()[offset_prev];
2165        }
2166    }
2167    return nullptr; // not found
2168}
2169
2170bool mtmd_decode_use_non_causal(const mtmd_context * ctx, const mtmd_input_chunk * chunk) {
2171    auto proj_type = ctx->proj_type_v();
2172    if (chunk && chunk->type == MTMD_INPUT_CHUNK_TYPE_AUDIO) {
2173        proj_type = ctx->proj_type_a();
2174    }
2175    switch (proj_type) {
2176        case PROJECTOR_TYPE_GEMMA4V:
2177            // E2B (n_embd = 1536) and E4B (n_embd = 2560) always use causal
2178            return ctx->n_embd_text != 1536 && ctx->n_embd_text != 2560;
2179        case PROJECTOR_TYPE_GEMMA4UV:
2180        case PROJECTOR_TYPE_GEMMA3:
2181        case PROJECTOR_TYPE_DEEPSEEK4V:
2182            return true;
2183        default:
2184            return false;
2185    }
2186}
2187
2188bool mtmd_decode_use_mrope(const mtmd_context * ctx) {
2189    return ctx->pos_type == MTMD_POS_TYPE_MROPE;
2190}
2191
2192bool mtmd_support_vision(const mtmd_context * ctx) {
2193    return ctx->ctx_v != nullptr;
2194}
2195
2196bool mtmd_support_audio(const mtmd_context * ctx) {
2197    return ctx->ctx_a != nullptr;
2198}
2199
2200int mtmd_get_audio_sample_rate(const mtmd_context * ctx) {
2201    if (!ctx->ctx_a) {
2202        return -1;
2203    }
2204    return clip_get_hparams(ctx->ctx_a)->audio_sample_rate;
2205}
2206
2207const char * mtmd_get_marker(const mtmd_context * ctx) {
2208    return ctx->media_marker.c_str();
2209}
2210
2211//
2212// public API functions
2213//
2214
2215// mtmd_bitmap
2216
2217mtmd_bitmap * mtmd_bitmap_init(uint32_t nx,
2218                               uint32_t ny,
2219                               const unsigned char * data) {
2220    mtmd_bitmap * bitmap = new mtmd_bitmap(data, nx, ny);
2221    return bitmap;
2222}
2223
2224mtmd_bitmap * mtmd_bitmap_init_from_audio(size_t n_samples,
2225                                          const float * data) {
2226    mtmd_bitmap * bitmap = new mtmd_bitmap((const unsigned char *)data, n_samples);
2227    GGML_ASSERT(bitmap->is_audio);
2228    if (!bitmap->is_placeholder()) {
2229        GGML_ASSERT(bitmap->get_ro_buf().size() == n_samples * sizeof(float));
2230    }
2231    return bitmap;
2232}
2233
2234uint32_t mtmd_bitmap_get_nx(const mtmd_bitmap * bitmap) {
2235    return bitmap->nx;
2236}
2237
2238uint32_t mtmd_bitmap_get_ny(const mtmd_bitmap * bitmap) {
2239    return bitmap->ny;
2240}
2241
2242const unsigned char * mtmd_bitmap_get_data(const mtmd_bitmap * bitmap) {
2243    if (bitmap->is_placeholder()) {
2244        return nullptr;
2245    }
2246    return bitmap->get_ro_buf().data();
2247}
2248
2249size_t mtmd_bitmap_get_n_bytes(const mtmd_bitmap * bitmap) {
2250    if (bitmap->is_placeholder()) {
2251        return 0;
2252    }
2253    return bitmap->get_ro_buf().size();
2254}
2255
2256bool mtmd_bitmap_is_audio(const mtmd_bitmap * bitmap) {
2257    return bitmap->is_audio;
2258}
2259
2260const char * mtmd_bitmap_get_id(const mtmd_bitmap * bitmap) {
2261    return bitmap->id.c_str();
2262}
2263
2264void mtmd_bitmap_set_id(mtmd_bitmap * bitmap, const char * id) {
2265    if (id) {
2266        bitmap->id = std::string(id);
2267    } else {
2268        bitmap->id.clear();
2269    }
2270}
2271
2272void mtmd_bitmap_set_mergeable(mtmd_bitmap * bitmap, bool mergeable) {
2273    bitmap->mergeable = mergeable;
2274}
2275
2276mtmd_bitmap * mtmd_bitmap_init_lazy(const mtmd_context * ctx,
2277                                    const char * id,
2278                                    void * user_data,
2279                                    mtmd_bitmap_lazy_callback callback) {
2280    GGML_UNUSED(ctx); // reserved for future use
2281    mtmd_bitmap * bitmap = new mtmd_bitmap(nullptr, 0, 0);
2282    bitmap->lazy_callback = callback;
2283    bitmap->lazy_user_data = user_data;
2284    mtmd_bitmap_set_id(bitmap, id);
2285    return bitmap;
2286}
2287
2288void mtmd_bitmap_free(mtmd_bitmap * bitmap) {
2289    if (bitmap) {
2290        delete bitmap;
2291    }
2292}
2293
2294// mtmd_input_chunks
2295
2296mtmd_input_chunks * mtmd_input_chunks_init() {
2297    return new mtmd_input_chunks;
2298}
2299
2300size_t mtmd_input_chunks_size(const mtmd_input_chunks * chunks) {
2301    return chunks->entries.size();
2302}
2303
2304const mtmd_input_chunk * mtmd_input_chunks_get(const mtmd_input_chunks * chunks, size_t idx) {
2305    if (idx >= chunks->entries.size()) {
2306        return nullptr;
2307    }
2308    return &chunks->entries[idx];
2309}
2310
2311void mtmd_input_chunks_free(mtmd_input_chunks * chunks) {
2312    if (chunks) {
2313        delete chunks;
2314    }
2315}
2316
2317// mtmd_input_chunk
2318
2319enum mtmd_input_chunk_type mtmd_input_chunk_get_type(const mtmd_input_chunk * chunk) {
2320    return chunk->type;
2321}
2322
2323const llama_token * mtmd_input_chunk_get_tokens_text(const mtmd_input_chunk * chunk, size_t * n_tokens_output) {
2324    if (chunk->type == MTMD_INPUT_CHUNK_TYPE_TEXT) {
2325        *n_tokens_output = chunk->tokens_text.size();
2326        return chunk->tokens_text.data();
2327    }
2328    *n_tokens_output = 0;
2329    return nullptr;
2330}
2331
2332const mtmd_image_tokens * mtmd_input_chunk_get_tokens_image(const mtmd_input_chunk * chunk) {
2333    if (chunk->type == MTMD_INPUT_CHUNK_TYPE_IMAGE) {
2334        return chunk->tokens_image.get();
2335    }
2336    return nullptr;
2337}
2338
2339size_t mtmd_input_chunk_get_n_tokens(const mtmd_input_chunk * chunk) {
2340    if (chunk->type == MTMD_INPUT_CHUNK_TYPE_TEXT) {
2341        return chunk->tokens_text.size();
2342    } else if (chunk->type == MTMD_INPUT_CHUNK_TYPE_IMAGE) {
2343        return mtmd_image_tokens_get_n_tokens(chunk->tokens_image.get());
2344    } else if (chunk->type == MTMD_INPUT_CHUNK_TYPE_AUDIO) {
2345        return chunk->tokens_audio->n_tokens;
2346    } else {
2347        GGML_ABORT("invalid chunk type");
2348    }
2349}
2350
2351llama_pos mtmd_input_chunk_get_n_pos(const mtmd_input_chunk * chunk) {
2352    if (chunk->type == MTMD_INPUT_CHUNK_TYPE_TEXT) {
2353        return chunk->tokens_text.size();
2354    } else if (chunk->type == MTMD_INPUT_CHUNK_TYPE_IMAGE) {
2355        return mtmd_image_tokens_get_n_pos(chunk->tokens_image.get());
2356    } else if (chunk->type == MTMD_INPUT_CHUNK_TYPE_AUDIO) {
2357        return chunk->tokens_audio->n_tokens;
2358    } else {
2359        GGML_ABORT("invalid chunk type");
2360    }
2361}
2362
2363const char * mtmd_input_chunk_get_id(const mtmd_input_chunk * chunk) {
2364    if (chunk->type == MTMD_INPUT_CHUNK_TYPE_IMAGE) {
2365        return chunk->tokens_image->id.c_str();
2366    } else if (chunk->type == MTMD_INPUT_CHUNK_TYPE_AUDIO) {
2367        return chunk->tokens_audio->id.c_str();
2368    }
2369    return nullptr;
2370}
2371
2372mtmd_input_chunk * mtmd_input_chunk_copy(const mtmd_input_chunk * chunk) {
2373    mtmd_input_chunk * copy = new mtmd_input_chunk{
2374        chunk->type,
2375        chunk->tokens_text,
2376        nullptr,
2377        nullptr,
2378    };
2379    if (chunk->tokens_image) {
2380        // copy the image tokens
2381        copy->tokens_image = mtmd_image_tokens_ptr(new mtmd_image_tokens());
2382        *copy->tokens_image = chunk->tokens_image->clone();
2383    }
2384    if (chunk->tokens_audio) {
2385        // copy the audio tokens
2386        copy->tokens_audio = mtmd_audio_tokens_ptr(new mtmd_audio_tokens());
2387        *copy->tokens_audio = chunk->tokens_audio->clone();
2388    }
2389    return copy;
2390}
2391
2392void mtmd_input_chunk_free(mtmd_input_chunk * chunk) {
2393    if (chunk) {
2394        delete chunk;
2395    }
2396}
2397
2398// returns 0 on success
2399static int32_t mtmd_input_chunk_save_impl(const mtmd_input_chunk * chunk, std::vector<char> & out_buf) {
2400    try {
2401        mtmd_serialization ser(MTMD_SERIALIZATION_VERSION);
2402        chunk->serialize(ser);
2403        out_buf = std::move(ser.data);
2404        return 0;
2405    } catch (const std::exception & e) {
2406        LOG_ERR("%s: %s\n", __func__, e.what());
2407        return -1;
2408    }
2409}
2410
2411mtmd_input_chunk * mtmd_input_chunk_get_placeholder(const mtmd_input_chunk * chunk) {
2412    // this is hacky, but still faster than copy the whole batch data
2413    std::vector<char> buf;
2414    if (mtmd_input_chunk_save_impl(chunk, buf) != 0) {
2415        return nullptr;
2416    }
2417    return mtmd_input_chunk_load(buf.data(), buf.size());
2418}
2419
2420int32_t mtmd_input_chunk_save(const mtmd_input_chunk * chunk, char * out_buf, size_t out_len, size_t * expected_out_len) {
2421    std::vector<char> buf;
2422    if (mtmd_input_chunk_save_impl(chunk, buf) != 0) {
2423        return -1;
2424    }
2425    if (expected_out_len) {
2426        *expected_out_len = buf.size();
2427    }
2428    if (!out_buf) {
2429        // caller is only querying the required size
2430        return 0;
2431    }
2432    if (out_len < buf.size()) {
2433        LOG_ERR("%s: out_buf is too small, need %zu bytes, got %zu\n", __func__, buf.size(), out_len);
2434        return -1;
2435    }
2436    std::memcpy(out_buf, buf.data(), buf.size());
2437    return 0;
2438}
2439
2440mtmd_input_chunk * mtmd_input_chunk_load(const char * buf, size_t len) {
2441    try {
2442        mtmd_serialization ser(MTMD_SERIALIZATION_VERSION, buf, len);
2443        mtmd::input_chunk_ptr chunk(new mtmd_input_chunk());
2444        chunk->deserialize(ser);
2445        return chunk.release();
2446    } catch (const std::exception & e) {
2447        LOG_ERR("%s: %s\n", __func__, e.what());
2448        return nullptr;
2449    }
2450}
2451
2452// mtmd_image_tokens
2453
2454size_t mtmd_image_tokens_get_n_tokens(const mtmd_image_tokens * image_tokens) {
2455    return image_tokens->n_tokens();
2456}
2457
2458size_t mtmd_image_tokens_get_nx(const mtmd_image_tokens * image_tokens) {
2459    return image_tokens->nx;
2460}
2461
2462size_t mtmd_image_tokens_get_ny(const mtmd_image_tokens * image_tokens) {
2463    return image_tokens->ny;
2464}
2465
2466mtmd_decoder_pos mtmd_image_tokens_get_decoder_pos(const mtmd_image_tokens * image_tokens, llama_pos pos_0, size_t i) {
2467    mtmd_decoder_pos pos;
2468    switch (image_tokens->pos) {
2469        case MTMD_POS_TYPE_MROPE:
2470            {
2471                pos.t = pos_0;
2472                pos.x = pos_0 + (i % image_tokens->nx);
2473                pos.y = pos_0 + (i / image_tokens->nx);
2474                pos.z = 0; // unused for now
2475            } break;
2476        case MTMD_POS_TYPE_NORMAL:
2477            {
2478                pos.t = pos_0 + i;
2479                pos.x = pos_0 + i;
2480                pos.y = pos_0 + i;
2481                pos.z = pos_0 + i;
2482            } break;
2483        case MTMD_POS_TYPE_HUNYUANVL:
2484            {
2485                // HunyuanVL layout: [BOI] [row0 tokens + newline] ... [row(ny-1) tokens + newline] [EOI]
2486                // Total = 1 + ny*(nx+1) + 1. BOI and EOI use sequential positions in every dim;
2487                // content and row-newline tokens use (row, col) with XD-RoPE dim-3 = image_idx.
2488                const uint32_t nx      = image_tokens->nx;
2489                const uint32_t n_total = image_tokens->n_tokens();
2490                if (i == 0) {
2491                    // BOI
2492                    pos.t = pos_0 + i;
2493                    pos.x = pos_0 + i;
2494                    pos.y = pos_0 + i;
2495                    pos.z = pos_0 + i;
2496                } else if (i == n_total - 1) {
2497                    // EOI
2498                    pos.t = pos_0 + i;
2499                    pos.x = pos_0 + i;
2500                    pos.y = pos_0 + i;
2501                    pos.z = pos_0 + i;
2502                } else {
2503                    // content token at (row, col), or the trailing newline of a row (col == nx)
2504                    //   section 0 = sequential, section 1 = w(col), section 2 = h(row), section 3 = image_count.
2505                    // set_position_mrope_2d writes .y -> section 1 and .x -> section 2
2506                    const uint32_t offset = (uint32_t)i - 1;
2507                    const uint32_t row    = offset / (nx + 1);
2508                    const uint32_t col    = offset % (nx + 1);
2509                    pos.t = pos_0 + i;
2510                    pos.x = row;
2511                    pos.y = col;
2512                    pos.z = image_tokens->image_idx;
2513                }
2514            } break;
2515        default:
2516            GGML_ABORT("invalid position type");
2517    }
2518    return pos;
2519}
2520
2521const char * mtmd_image_tokens_get_id(const mtmd_image_tokens * image_tokens) {
2522    return image_tokens->id.c_str();
2523}
2524
2525llama_pos mtmd_image_tokens_get_n_pos(const mtmd_image_tokens * image_tokens) {
2526    switch (image_tokens->pos) {
2527        case MTMD_POS_TYPE_MROPE:
2528            return std::max(image_tokens->nx, image_tokens->ny);
2529        case MTMD_POS_TYPE_NORMAL:
2530            return image_tokens->n_tokens();
2531        case MTMD_POS_TYPE_HUNYUANVL:
2532            // HunyuanVL: the sequential (dim-0) position advances by the full token count
2533            // (includes BOI/EOI and row newline tokens), not by max(nx, ny)
2534            return image_tokens->n_tokens();
2535        default:
2536            GGML_ABORT("invalid position type");
2537    }
2538}
2539
2540// test function
2541
2542mtmd_input_chunks * mtmd_test_create_input_chunks() {
2543    mtmd_input_chunks * chunks = mtmd_input_chunks_init();
2544    if (!chunks) {
2545        return nullptr;
2546    }
2547
2548    // create a text chunk
2549    std::vector<llama_token> tokens_text = { 1, 2, 3, 4, 5 };
2550    mtmd_input_chunk chunk_text{
2551        MTMD_INPUT_CHUNK_TYPE_TEXT,
2552        std::move(tokens_text),
2553        nullptr, // image tokens
2554        nullptr, // audio tokens
2555    };
2556    chunks->entries.emplace_back(std::move(chunk_text));
2557
2558    // create an image chunk
2559    mtmd_image_tokens_ptr image_tokens(new mtmd_image_tokens);
2560    image_tokens->nx = 4;
2561    image_tokens->ny = 4;
2562    image_tokens->batch_f32.entries.resize(16);
2563    image_tokens->id = "image_1";
2564    mtmd_input_chunk chunk_image{
2565        MTMD_INPUT_CHUNK_TYPE_IMAGE,
2566        {}, // text tokens
2567        std::move(image_tokens),
2568        nullptr, // audio tokens
2569    };
2570    chunks->entries.emplace_back(std::move(chunk_image));
2571
2572    return chunks;
2573}
2574
2575void mtmd_log_set(ggml_log_callback log_callback, void * user_data) {
2576    g_logger_state.log_callback = log_callback ? log_callback : clip_log_callback_default;
2577    g_logger_state.log_callback_user_data = user_data;
2578}
2579
2580struct mtmd_caps mtmd_get_cap_from_file(const char * fname) {
2581    try {
2582        auto tmp = clip_get_cap(fname);
2583        mtmd_caps cap;
2584        cap.inp_audio  = tmp.has_audio;
2585        cap.inp_vision = tmp.has_vision;
2586        return cap;
2587    } catch (const std::exception & e) {
2588        LOG_ERR("%s: failed to get capabilities from file '%s': %s\n", __func__, fname, e.what());
2589        return mtmd_caps{ false, false };
2590    }
2591}
2592
2593//
2594// Debugging API (NOT intended for public use)
2595//
2596
2597static void mtmd_debug_encode_impl(mtmd_context * ctx, clip_ctx * ctx_clip, clip_image_f32 & image) {
2598    clip_set_debug_output_embeddings(ctx_clip, true);
2599    int n_mmproj_embd = clip_n_mmproj_embd(ctx_clip);
2600    int n_tokens = clip_n_output_tokens(ctx_clip, &image);
2601    std::vector<float> embd_output(n_tokens * n_mmproj_embd, 0.0f);
2602    bool ok = clip_image_encode(
2603        ctx_clip,
2604        ctx->n_threads,
2605        &image,
2606        embd_output);
2607    if (!ok) {
2608        LOG_ERR("%s: failed to encode image\n", __func__);
2609    }
2610}
2611
2612void mtmd_debug_encode_image(mtmd_context * ctx, const std::vector<std::vector<float>> & image) {
2613    if (!ctx->ctx_v) {
2614        LOG_ERR("%s: model does not support vision input\n", __func__);
2615        return;
2616    }
2617    const int img_sz = (int)image.size();
2618    std::vector<float> img_buf;
2619    img_buf.reserve(img_sz * img_sz);
2620    for (const auto & row : image) {
2621        img_buf.insert(img_buf.end(), row.begin(), row.end());
2622    }
2623    clip_image_f32 inp_image;
2624    inp_image.set_size({img_sz, img_sz}, false, false);
2625    inp_image.cpy_buf(img_buf);
2626    LOG_INF("%s: created input image with nx=%d, ny=%d\n", __func__, img_sz, img_sz);
2627    mtmd_debug_encode_impl(ctx, ctx->ctx_v, inp_image);
2628}
2629
2630void mtmd_debug_encode_audio(mtmd_context * ctx, const std::vector<float> & input) {
2631    if (!ctx->ctx_a) {
2632        LOG_ERR("%s: model does not support audio input\n", __func__);
2633        return;
2634    }
2635    int n_mel = clip_get_hparams(ctx->ctx_a)->n_mel_bins;
2636    const int audio_nx = (int)input.size();
2637    std::vector<float> audio_buf(audio_nx * n_mel);
2638    for (int i = 0; i < audio_nx; i++) {
2639        for (int j = 0; j < n_mel; j++) {
2640            audio_buf[j * audio_nx + i] = input[i];
2641        }
2642    }
2643    clip_image_f32 inp_audio;
2644    inp_audio.set_size({audio_nx, n_mel}, false, true);
2645    inp_audio.cpy_buf(audio_buf);
2646    LOG_INF("%s: created input audio with nx=%d, ny=%d\n", __func__, audio_nx, n_mel);
2647    mtmd_debug_encode_impl(ctx, ctx->ctx_a, inp_audio);
2648}
2649
2650void mtmd_debug_preprocess_image(mtmd_context * ctx, const std::vector<uint8_t> & rgb_values, int nx, int ny) {
2651    if (!ctx->ctx_v) {
2652        LOG_ERR("%s: model does not support vision input\n", __func__);
2653        return;
2654    }
2655    clip_image_u8 img_u8;
2656    img_u8.set_size({nx, ny}, false);
2657    img_u8.cpy_buf(rgb_values);
2658    GGML_ASSERT(ctx->image_preproc != nullptr);
2659    mtmd_image_preproc_out preproc_out = ctx->image_preproc->preprocess(img_u8);
2660
2661    clip_image_f32_batch batch_f32;
2662    batch_f32.is_audio = false;
2663    for (auto & entry : preproc_out.entries) {
2664        batch_f32.entries.push_back(std::move(entry));
2665    }
2666
2667    LOG_INF("%s: preprocessed image to batch_f32 with %d entries\n", __func__, (int)batch_f32.entries.size());
2668    for (size_t i = 0; i < batch_f32.entries.size(); i++) {
2669        LOG_INF("%s: entry %zu has nx=%d, ny=%d\n", __func__, i, batch_f32.entries[i].nx(), batch_f32.entries[i].ny());
2670        // TODO: better way to dump entry content?
2671    }
2672}
2673
2674void mtmd_debug_preprocess_audio(mtmd_context * ctx, const std::vector<float> & samples) {
2675    if (!ctx->ctx_a) {
2676        LOG_ERR("%s: model does not support audio input\n", __func__);
2677        return;
2678    }
2679    std::vector<mtmd_audio_mel> mel_spec_chunks;
2680    bool ok = ctx->audio_preproc->preprocess(samples.data(), samples.size(), mel_spec_chunks);
2681    if (!ok) {
2682        LOG_ERR("%s: failed to preprocess audio\n", __func__);
2683        return;
2684    }
2685    LOG_INF("%s: preprocessed audio to %zu mel spec chunks\n", __func__, mel_spec_chunks.size());
2686    for (size_t i = 0; i < mel_spec_chunks.size(); i++) {
2687        LOG_INF("%s: mel spec chunk %zu has n_len=%d, n_mel=%d\n", __func__, i, mel_spec_chunks[i].n_len, mel_spec_chunks[i].n_mel);
2688
2689        // dump mel entries: data is stored as [n_mel][n_len] (mel-major)
2690        const auto & mel = mel_spec_chunks[i];
2691        for (int m = 0; m < mel.n_mel; m++) {
2692            for (int t = 0; t < mel.n_len; t++) {
2693                LOG_INF("mel[%zu][m=%d][t=%d] = %f\n", i, m, t, mel.data[m * mel.n_len + t]);
2694            }
2695        }
2696    }
2697}
2698
2699static void stub_log_callback(enum ggml_log_level, const char *, void *) {
2700    // do nothing
2701}
2702
2703std::map<ggml_backend_dev_t, size_t> mtmd_get_memory_usage(const char * mmproj_fname,
2704                                                            struct mtmd_context_params ctx_params) {
2705    mtmd::context_ptr ctx;
2706    auto saved_log_callback = g_logger_state.log_callback;
2707    auto saved_log_user_data = g_logger_state.log_callback_user_data;
2708
2709    ctx_params.progress_callback = nullptr;
2710
2711    try {
2712        mtmd_log_set(stub_log_callback, nullptr); // suppress logging
2713        ctx.reset(new mtmd_context(mmproj_fname, nullptr, ctx_params, true));
2714        mtmd_log_set(saved_log_callback, saved_log_user_data); // restore log callback
2715        std::map<ggml_backend_dev_t, size_t> total_mem;
2716        auto merge = [&](const struct clip_ctx * c) {
2717            for (auto & [dev, size] : clip_get_mem_usage(c)) {
2718                total_mem[dev] += size;
2719            }
2720        };
2721        if (ctx->ctx_v) {
2722            merge(ctx->ctx_v);
2723        }
2724        if (ctx->ctx_a) {
2725            merge(ctx->ctx_a);
2726        }
2727        return total_mem;
2728    } catch (const std::exception & e) {
2729        mtmd_log_set(saved_log_callback, saved_log_user_data); // restore log callback
2730        LOG_ERR("%s: error: %s\n", __func__, e.what());
2731        return {};
2732    }
2733}