|
| 1 | +// Unit tests for Qwen3VL multi-tile grid selection (tools/mtmd/mtmd-image.cpp). |
| 2 | +// Exercises mtmd_image_preprocessor_qwen3vl::preprocess() directly via the |
| 3 | +// test-only hparams constructor, without loading a real GGUF model. |
| 4 | + |
| 5 | +#include "clip-model.h" |
| 6 | +#include "mtmd-image.h" |
| 7 | + |
| 8 | +#include <cstdio> |
| 9 | +#include <cstdlib> |
| 10 | + |
| 11 | +// plain assert() is compiled out under -DNDEBUG (this project's default Release |
| 12 | +// build), which would make every check below a silent no-op; use an explicit |
| 13 | +// check that always runs instead. |
| 14 | +#define CHECK(cond) \ |
| 15 | + do { \ |
| 16 | + if (!(cond)) { \ |
| 17 | + fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #cond); \ |
| 18 | + abort(); \ |
| 19 | + } \ |
| 20 | + } while (0) |
| 21 | + |
| 22 | +static clip_hparams make_hparams(int image_size, int patch_size) { |
| 23 | + clip_hparams hp; |
| 24 | + hp.image_size = image_size; |
| 25 | + hp.patch_size = patch_size; |
| 26 | + hp.n_merge = 2; // matches PROJECTOR_TYPE_QWEN3VL default in clip.cpp |
| 27 | + // dyn_size fallback needs these set; mirrors clip.cpp's set_limit_image_tokens(8, 4096) for Qwen-VL |
| 28 | + hp.set_limit_image_tokens(8, 4096); |
| 29 | + return hp; |
| 30 | +} |
| 31 | + |
| 32 | +static clip_image_u8 make_image(int nx, int ny) { |
| 33 | + clip_image_u8 img; |
| 34 | + img.nx = nx; |
| 35 | + img.ny = ny; |
| 36 | + img.buf.resize((size_t) nx * ny * 3); |
| 37 | + return img; |
| 38 | +} |
| 39 | + |
| 40 | +// max_tiles == 1 excludes every grid but 1x1, which is itself excluded as |
| 41 | +// "equivalent to dyn_size" -> the candidate set is always empty -> dyn_size fallback. |
| 42 | +static void test_max_tiles_one_falls_back_to_dyn_size() { |
| 43 | + clip_hparams hp = make_hparams(/*image_size*/ 768, /*patch_size*/ 16); |
| 44 | + mtmd_image_preprocessor_qwen3vl pre(hp, /*max_tiles*/ 1); |
| 45 | + |
| 46 | + clip_image_u8 img = make_image(2000, 1000); |
| 47 | + clip_image_f32_batch out; |
| 48 | + bool ok = pre.preprocess(img, out); |
| 49 | + |
| 50 | + CHECK(ok); |
| 51 | + CHECK(out.grid_x == 0 && out.grid_y == 0); |
| 52 | + CHECK(out.entries.size() == 1); |
| 53 | + printf("test_max_tiles_one_falls_back_to_dyn_size: OK\n"); |
| 54 | +} |
| 55 | + |
| 56 | +// An image smaller than one tile in both dimensions has no downscaling grid -> |
| 57 | +// empty candidate set -> dyn_size fallback (same outcome as max_tiles=1, different cause). |
| 58 | +static void test_empty_candidate_set_falls_back_to_dyn_size() { |
| 59 | + clip_hparams hp = make_hparams(/*image_size*/ 768, /*patch_size*/ 16); |
| 60 | + mtmd_image_preprocessor_qwen3vl pre(hp, /*max_tiles*/ 256); |
| 61 | + |
| 62 | + clip_image_u8 img = make_image(100, 100); |
| 63 | + clip_image_f32_batch out; |
| 64 | + bool ok = pre.preprocess(img, out); |
| 65 | + |
| 66 | + CHECK(ok); |
| 67 | + CHECK(out.grid_x == 0 && out.grid_y == 0); |
| 68 | + CHECK(out.entries.size() == 1); |
| 69 | + printf("test_empty_candidate_set_falls_back_to_dyn_size: OK\n"); |
| 70 | +} |
| 71 | + |
| 72 | +// A large, well-fitting image at the max_tiles=256 ceiling should tile, and the |
| 73 | +// selected grid must respect the tile budget: grid_x*grid_y <= max_tiles. |
| 74 | +static void test_max_tiles_ceiling_selects_valid_grid() { |
| 75 | + clip_hparams hp = make_hparams(/*image_size*/ 768, /*patch_size*/ 16); |
| 76 | + mtmd_image_preprocessor_qwen3vl pre(hp, /*max_tiles*/ 256); |
| 77 | + |
| 78 | + clip_image_u8 img = make_image(768 * 16, 768 * 16); // square, downscales cleanly at many grids |
| 79 | + clip_image_f32_batch out; |
| 80 | + bool ok = pre.preprocess(img, out); |
| 81 | + |
| 82 | + CHECK(ok); |
| 83 | + CHECK(out.grid_x > 0 && out.grid_y > 0); |
| 84 | + CHECK(out.grid_x * out.grid_y <= 256); |
| 85 | + CHECK(out.entries.size() == (size_t)(out.grid_x * out.grid_y + 1)); // +1 overview thumbnail |
| 86 | + printf("test_max_tiles_ceiling_selects_valid_grid: OK (grid=%dx%d)\n", out.grid_x, out.grid_y); |
| 87 | +} |
| 88 | + |
| 89 | +// A zero-dimension image must fail this one preprocess() call gracefully |
| 90 | +// (return false) rather than aborting the whole process via GGML_ASSERT. |
| 91 | +static void test_zero_dimension_image_fails_gracefully() { |
| 92 | + clip_hparams hp = make_hparams(/*image_size*/ 768, /*patch_size*/ 16); |
| 93 | + mtmd_image_preprocessor_qwen3vl pre(hp, /*max_tiles*/ 4); |
| 94 | + |
| 95 | + clip_image_u8 img = make_image(0, 100); |
| 96 | + clip_image_f32_batch out; |
| 97 | + bool ok = pre.preprocess(img, out); |
| 98 | + |
| 99 | + CHECK(!ok); |
| 100 | + printf("test_zero_dimension_image_fails_gracefully: OK\n"); |
| 101 | +} |
| 102 | + |
| 103 | +int main(void) { |
| 104 | + test_max_tiles_one_falls_back_to_dyn_size(); |
| 105 | + test_empty_candidate_set_falls_back_to_dyn_size(); |
| 106 | + test_max_tiles_ceiling_selects_valid_grid(); |
| 107 | + test_zero_dimension_image_fails_gracefully(); |
| 108 | + printf("\nAll qwen3vl grid selection tests passed.\n"); |
| 109 | + return 0; |
| 110 | +} |
0 commit comments