Skip to content

Commit 3e706dd

Browse files
ngxsonsfallah
andauthored
mtmd: deepseek-ocr v1 multi-tile (#24717)
* mtmd: deepseek-ocr v1 multi-tile dynamic resolution + unified image-preprocessors for both versions (ds-ocr v1 and v2) * remove hacky API * fuse row into a long image * almost working * adapt to new preprocessor api * rm debugging printf * improve * mtmd: dsocr-tiles fixes (#25481) * ds-ocr img-preproc fuse_row tile-drop fix for multi rows and columns images * mtmd drop the duplicate redundant img_end * deepseekocr graph simplify CLS broadcast cleanup * test-deepseek-ocr: relax v1 single-view tolerance; drop trailing prompt space; make DRY opt-in and n_predict model-specific (#25486) --------- Co-authored-by: Saba Fallah <10401143+sfallah@users.noreply.github.com> Co-authored-by: Saba Fallah <sabafallah@gmail.com>
1 parent 07d9378 commit 3e706dd

10 files changed

Lines changed: 240 additions & 120 deletions

File tree

tools/mtmd/clip-graph.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ struct clip_graph {
2020
const clip_hparams & hparams;
2121
projector_type proj_type;
2222

23-
// we only support single image per batch
24-
const clip_image_f32 & img;
23+
const clip_image_f32 & img; // for backward compat
24+
const clip_image_f32_batch * img_batch = nullptr;
2525

2626
const int patch_size;
2727
const int n_patches_x;
@@ -63,6 +63,12 @@ struct clip_graph {
6363
//
6464
void cb(ggml_tensor * cur0, const char * name, int il) const;
6565

66+
const clip_image_f32 & get_img(size_t idx) const {
67+
GGML_ASSERT(img_batch);
68+
GGML_ASSERT(idx < img_batch->entries.size());
69+
return img_batch->entries[idx];
70+
}
71+
6672
// siglip2 naflex
6773
ggml_tensor * resize_position_embeddings(uint32_t interpolation_mode = DEFAULT_INTERPOLATION_MODE);
6874

tools/mtmd/clip-model.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ struct clip_hparams {
6969
std::vector<clip_image_size> image_res_candidates;
7070
int32_t preproc_min_tiles = 0;
7171
int32_t preproc_max_tiles = 0;
72+
int32_t preproc_tile_size = 0; // local tile size (deepseek-ocr)
7273
resize_algo image_resize_algo_rf = RESIZE_ALGO_BICUBIC;
7374
resize_algo image_resize_algo_ov = RESIZE_ALGO_BILINEAR;
7475
pad_style image_pad_rf = PAD_CEIL; // padding style for the refined image (e.g. llava-1.6)

tools/mtmd/clip.cpp

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,8 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const
10241024
GGML_ABORT("missing cgraph builder");
10251025
}
10261026

1027+
builder->img_batch = &imgs;
1028+
10271029
// TODO [QWEN_VIDEO]: improve this in the future
10281030
builder->n_batch = imgs.entries.size();
10291031

@@ -1580,7 +1582,16 @@ struct clip_model_loader {
15801582
get_u32(KEY_SAM_N_HEAD, hparams.sam_n_head, true);
15811583
get_u32(KEY_SAM_N_EMBD, hparams.sam_n_embd, true);
15821584
get_u32(KEY_ATTN_WINDOW_SIZE, hparams.attn_window_size, true);
1585+
hparams.preproc_min_tiles = 2;
1586+
if (model.proj_type == PROJECTOR_TYPE_DEEPSEEKOCR) {
1587+
hparams.preproc_max_tiles = 9;
1588+
hparams.preproc_tile_size = 640;
1589+
// the CLIP/ViT body runs its layernorms at 1e-5 (the SAM stage uses 1e-6)
1590+
hparams.eps = 1e-5f;
1591+
}
15831592
if (model.proj_type == PROJECTOR_TYPE_DEEPSEEKOCR2) {
1593+
hparams.preproc_max_tiles = 6;
1594+
hparams.preproc_tile_size = 768;
15841595
// qwen2 encoder is GQA, requires KEY_N_HEAD_KV
15851596
get_u32(string_format(KEY_N_HEAD_KV, "vision"), hparams.n_head_kv);
15861597
}
@@ -3251,6 +3262,9 @@ int clip_n_output_tokens_x(const clip_ctx * ctx, const clip_image_f32 * img) {
32513262
return (img->nx() / params.patch_size) / 2;
32523263
case PROJECTOR_TYPE_STEP3VL:
32533264
return img->nx() / (params.patch_size * params.n_merge);
3265+
case PROJECTOR_TYPE_DEEPSEEKOCR:
3266+
case PROJECTOR_TYPE_DEEPSEEKOCR2:
3267+
return (img->nx() / params.patch_size) / 4;
32543268
default:
32553269
break;
32563270
}
@@ -3460,10 +3474,17 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) {
34603474
// E.g., 64x64 -> 16x16 patches
34613475
n_patches /= 16;
34623476

3463-
// build_global_local_features adds image newlines and view separator
3464-
// Formula: h*(w+1) + 1 where h = w = sqrt(n_patches)
3465-
int h = static_cast<int>(std::sqrt(static_cast<float>(n_patches)));
3466-
n_patches = h * (h + 1) + 1;
3477+
if (img->add_viewsep) {
3478+
// global view: one image-newline per token-row + trailing view separator
3479+
const int h = static_cast<int>(std::sqrt(static_cast<float>(n_patches)));
3480+
n_patches = h * (h + 1) + 1;
3481+
} else if (img->ny() >= img->nx() && img->ny() % img->nx() == 0) {
3482+
// tile row: one image-newline per token-row
3483+
const int grid_w = img->ny() / img->nx();
3484+
const int tile_patches = img->nx() / (patch_size * 4); // patches per tile side (SAM divides by 4)
3485+
const int h = tile_patches;
3486+
n_patches = (tile_patches * grid_w + 1) * h;
3487+
}
34673488
} break;
34683489
case PROJECTOR_TYPE_HUNYUANVL:
34693490
{
@@ -4103,7 +4124,10 @@ bool clip_image_batch_encode(clip_ctx * ctx, int n_threads, const clip_image_f32
41034124
case PROJECTOR_TYPE_DEEPSEEKOCR:
41044125
case PROJECTOR_TYPE_DEEPSEEKOCR2:
41054126
{
4106-
GGML_ASSERT(pos_w == pos_h);
4127+
GGML_ASSERT(
4128+
(pos_w == pos_h) // overview image
4129+
|| (pos_h >= pos_w && pos_h % pos_w == 0) // tile images
4130+
);
41074131

41084132
const int window = hparams.attn_window_size;
41094133
const int pos = pos_w;

tools/mtmd/models/deepseekocr.cpp

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ ggml_tensor * clip_graph_deepseekocr::build_sam(ggml_tensor * inp_raw) {
9696
const int n_heads = hparams.sam_n_head;
9797
const int d_heads = n_embd / n_heads;
9898
const int window = hparams.attn_window_size;
99+
// SAM stage runs its layernorms at 1e-6
100+
const float sam_eps = 1e-6f;
99101

100102
ggml_tensor * inpL;
101103

@@ -134,7 +136,7 @@ ggml_tensor * clip_graph_deepseekocr::build_sam(ggml_tensor * inp_raw) {
134136
ggml_tensor * shortcut = cur;
135137

136138
// layernorm1
137-
cur = build_norm(cur, layer.ln_1_w, layer.ln_1_b, NORM_TYPE_NORMAL, eps, il);
139+
cur = build_norm(cur, layer.ln_1_w, layer.ln_1_b, NORM_TYPE_NORMAL, sam_eps, il);
138140

139141
const int64_t w0 = cur->ne[1];
140142
const int64_t h0 = cur->ne[2];
@@ -214,7 +216,7 @@ ggml_tensor * clip_graph_deepseekocr::build_sam(ggml_tensor * inp_raw) {
214216
ggml_tensor * inpFF = cur;
215217

216218
// layernorm2
217-
cur = build_norm(inpFF, layer.ln_2_w, layer.ln_2_b, NORM_TYPE_NORMAL, eps, il);
219+
cur = build_norm(inpFF, layer.ln_2_w, layer.ln_2_b, NORM_TYPE_NORMAL, sam_eps, il);
218220

219221
// ffn
220222
cur = build_ffn(cur, layer.ff_up_w, layer.ff_up_b, nullptr, nullptr, layer.ff_down_w, layer.ff_down_b,
@@ -229,12 +231,12 @@ ggml_tensor * clip_graph_deepseekocr::build_sam(ggml_tensor * inp_raw) {
229231

230232
cur = ggml_conv_2d(ctx0, model.neck_0_w, cur, 1, 1, 0, 0, 1, 1);
231233
cur = ggml_cont(ctx0, ggml_permute(ctx0, cur, 1, 2, 0, 3));
232-
cur = build_norm(cur, model.neck_1_w, model.neck_1_b, NORM_TYPE_NORMAL, hparams.eps, -1);
234+
cur = build_norm(cur, model.neck_1_w, model.neck_1_b, NORM_TYPE_NORMAL, sam_eps, -1);
233235
cur = ggml_cont(ctx0, ggml_permute(ctx0, cur, 2, 0, 1, 3));
234236

235237
cur = ggml_conv_2d(ctx0, model.neck_2_w, cur, 1, 1, 1, 1, 1, 1);
236238
cur = ggml_cont(ctx0, ggml_permute(ctx0, cur, 1, 2, 0, 3));
237-
cur = build_norm(cur, model.neck_3_w, model.neck_3_b, NORM_TYPE_NORMAL, hparams.eps, -1);
239+
cur = build_norm(cur, model.neck_3_w, model.neck_3_b, NORM_TYPE_NORMAL, sam_eps, -1);
238240
cur = ggml_cont(ctx0, ggml_permute(ctx0, cur, 2, 0, 1, 3));
239241

240242
cur = ggml_conv_2d(ctx0, model.net_2, cur, 2, 2, 1, 1, 1, 1);
@@ -248,16 +250,50 @@ ggml_tensor * clip_graph_deepseekocr::build_sam(ggml_tensor * inp_raw) {
248250
ggml_cgraph * clip_graph_deepseekocr::build() {
249251
// patch embedding
250252
ggml_tensor * inp_raw = build_inp_raw();
253+
254+
bool is_overview = img.add_viewsep;
255+
int n_tiles_per_row = 0;
256+
257+
// note: we expect either a batch of rows or a batch of overviews, but not a mix of both
258+
259+
if (!is_overview) {
260+
// handle the case where we have a batch of rows
261+
// sanity check
262+
for (auto & entry : img_batch->entries) {
263+
if (entry.add_viewsep) {
264+
throw std::runtime_error("DeepSeek-OCR: mixed overview and non-overview images in batch");
265+
}
266+
if (entry.nx() != img.nx() || entry.ny() != img.ny()) {
267+
throw std::runtime_error("DeepSeek-OCR: mixed image sizes in batch");
268+
}
269+
}
270+
271+
GGML_ASSERT(img.ny() >= img.nx());
272+
GGML_ASSERT(img.ny() % img.nx() == 0);
273+
n_tiles_per_row = img.ny() / img.nx();
274+
275+
// input shape: [tile_size, tile_size * n_tiles_per_row, 3]
276+
// we want to reshape it to [tile_size, tile_size, 3, n_tiles_per_row]
277+
inp_raw = ggml_reshape_4d(ctx0, inp_raw, img.nx(), img.nx(), n_tiles_per_row, 3);
278+
inp_raw = ggml_cont(ctx0, ggml_permute(ctx0, inp_raw, 0, 1, 3, 2));
279+
}
280+
251281
ggml_tensor * sam_out = build_sam(inp_raw);
252282

283+
if (!is_overview) {
284+
n_batch = n_tiles_per_row;
285+
}
286+
253287
const int clip_n_patches = sam_out->ne[0] * sam_out->ne[1];
254288

255289
ggml_tensor * clip_out;
256290
// Building DS-OCR CLIP
257291
{
258292
ggml_tensor * inp;
259293

260-
inp = ggml_reshape_2d(ctx0, sam_out, clip_n_patches, sam_out->ne[2]);
294+
// sam_out: [patch_h, patch_w, n_embd, n_batch]
295+
// -> [n_embd, clip_n_patches, n_batch]
296+
inp = ggml_reshape_3d(ctx0, sam_out, clip_n_patches, sam_out->ne[2], sam_out->ne[3]);
261297
inp = ggml_cont(ctx0, ggml_permute(ctx0, inp, 1, 0, 2, 3));
262298

263299
ggml_tensor * new_pos_embd = model.position_embeddings;
@@ -281,8 +317,11 @@ ggml_cgraph * clip_graph_deepseekocr::build() {
281317
n_pos = tgt_size * tgt_size + 1;
282318
}
283319

284-
// add CLS token
285-
inp = ggml_concat(ctx0, model.class_embedding, inp, 1);
320+
// add CLS token per batch item
321+
// inp: [n_embd, clip_n_patches, n_batch]
322+
// class_embedding: [n_embd] -> [n_embd, 1, n_batch]
323+
ggml_tensor * cls_embd = ggml_repeat_4d(ctx0, model.class_embedding, n_embd, 1, n_batch, 1);
324+
inp = ggml_concat(ctx0, cls_embd, inp, 1);
286325

287326
// for selecting learned pos embd, used by ViT
288327
ggml_tensor * positions = ggml_cast(ctx0, ggml_arange(ctx0, 0, n_pos, 1), GGML_TYPE_I32);
@@ -294,25 +333,56 @@ ggml_cgraph * clip_graph_deepseekocr::build() {
294333
clip_out = cur;
295334
}
296335

336+
// sam_out: [patch_h, patch_w, n_embd, n_batch]
337+
// -> [n_embd, clip_n_patches, n_batch]
297338
sam_out = ggml_cont(ctx0, ggml_permute(ctx0, sam_out, 1, 2, 0, 3));
298-
sam_out = ggml_reshape_2d(ctx0, sam_out, sam_out->ne[0], clip_n_patches);
299-
clip_out = ggml_view_2d(ctx0, clip_out, n_embd, clip_n_patches, clip_out->nb[1], clip_out->nb[1]);
339+
sam_out = ggml_reshape_3d(ctx0, sam_out, sam_out->ne[0], clip_n_patches, n_batch);
340+
341+
// clip_out: [n_embd, n_pos, n_batch] where n_pos = clip_n_patches + 1 (CLS)
342+
// strip CLS token: skip first position, view only the patch tokens
343+
clip_out = ggml_view_3d(ctx0, clip_out, n_embd, clip_n_patches, n_batch,
344+
clip_out->nb[1], clip_out->nb[2], clip_out->nb[1]);
300345

301346
ggml_tensor * cur;
302347
cur = ggml_concat(ctx0, clip_out, sam_out, 0);
303348
cur = ggml_mul_mat(ctx0, model.mm_fc_w, cur);
304349
cur = ggml_add(ctx0, cur, model.mm_fc_b);
305350

306-
const auto h = static_cast<int>(std::sqrt(static_cast<float>(cur->ne[1])));
307-
const auto w = h;
308-
const auto n_dim = cur->ne[0];
351+
if (is_overview) {
352+
// global view: weave one newline per row + trailing view separator
353+
const auto h = static_cast<int>(std::sqrt(static_cast<float>(cur->ne[1])));
354+
const auto w = h;
355+
const auto n_dim = cur->ne[0];
356+
357+
ggml_tensor * imgnl = ggml_repeat_4d(ctx0, model.image_newline, n_dim, 1, h, 1);
358+
cur = ggml_reshape_3d(ctx0, cur, n_dim, w, h);
359+
cur = ggml_reshape_2d(ctx0, ggml_concat(ctx0, cur, imgnl, 1), n_dim, (w + 1) * h);
360+
cur = ggml_concat(ctx0, cur, model.view_seperator, 1); // (n_dim, h*(w+1) + 1)
361+
} else {
362+
// tile row: interleave tiles within each row, add newline per row
363+
const int grid_x = static_cast<int>(std::sqrt(static_cast<float>(clip_n_patches)));
364+
const int grid_y = grid_x;
365+
const auto n_dim = cur->ne[0];
309366

310-
ggml_tensor * imgnl;
367+
// (n_dim, clip_n_patches, n_batch) -> (n_dim, grid_x, grid_y, n_batch)
368+
cur = ggml_reshape_4d(ctx0, cur, n_dim, grid_x, grid_y, n_batch);
311369

312-
imgnl = ggml_repeat_4d(ctx0, model.image_newline, n_dim, 1, h, 1);
313-
cur = ggml_reshape_3d(ctx0, cur, n_dim, w, h);
314-
cur = ggml_reshape_2d(ctx0, ggml_concat(ctx0, cur, imgnl, 1), n_dim, (w + 1) * h);
315-
cur = ggml_concat(ctx0, cur, model.view_seperator, 1); // (n_dim, h*(w+1) + 1)
370+
// tiles: re-order from A.row0 A.row1 B.row0 B.row1 ...
371+
// to A.row0 B.row0 A.row1 B.row1 ...
372+
// then add nl: A.row0 B.row0 [nl] A.row1 B.row1 [nl] ...
373+
// interleave tiles: (n_dim, grid_x, grid_y, n_batch) -> (n_dim, grid_x, n_batch, grid_y)
374+
cur = ggml_cont(ctx0, ggml_permute(ctx0, cur, 0, 1, 3, 2));
375+
376+
// merge: (n_dim, grid_x, n_batch, grid_y) -> (n_dim, grid_x*n_batch, grid_y, 1)
377+
cur = ggml_reshape_4d(ctx0, cur, n_dim, grid_x * n_batch, grid_y, 1);
378+
379+
// append newline per row: (n_dim, grid_x*n_batch+1, grid_y, 1)
380+
ggml_tensor * imgnl = ggml_repeat_4d(ctx0, model.image_newline, n_dim, 1, grid_y, 1);
381+
cur = ggml_concat(ctx0, cur, imgnl, 1);
382+
383+
// flatten: (n_dim, (grid_x*n_batch+1)*grid_y)
384+
cur = ggml_reshape_2d(ctx0, cur, n_dim, (grid_x * n_batch + 1) * grid_y);
385+
}
316386

317387
cb(cur, "dsocr_output", -1);
318388

tools/mtmd/models/models.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ struct clip_graph_deepseekocr : clip_graph {
127127
clip_graph_deepseekocr(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
128128
ggml_cgraph * build() override;
129129
ggml_tensor * build_sam(ggml_tensor * inp); // build the SAM model
130+
// bool support_batch() const override { return true; } // TODO: support batch for DeepSeek-OCR v1
130131
};
131132

132133
struct clip_graph_deepseekocr2 : clip_graph_deepseekocr {

0 commit comments

Comments
 (0)