Skip to content

Commit ec2b787

Browse files
bssrdfngxson
andauthored
mtmd: Add dynamic high-resolution image preprocessing for InternVL model (ggml-org#20847)
* added support for internvl's dynamic high-resolution (Qianfan-OCR needed) * add min/max dynamic patch to gguf meta * clean up * simplified handling min/max dynamic patch * reuse llava_uhd logic for slice images * provide default values for older models * flake8 * prevent writing 0 value to gguf * remove duplicated resolution candidates with a better algorithm * fix indentation * format * add protection from divide by zero * change to 0 to be safe --------- Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
1 parent d3ac030 commit ec2b787

7 files changed

Lines changed: 84 additions & 4 deletions

File tree

convert_hf_to_gguf.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4273,6 +4273,16 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter
42734273

42744274
@ModelBase.register("InternVisionModel")
42754275
class InternVisionModel(MmprojModel):
4276+
4277+
min_dynamic_tiles: int = 0
4278+
max_dynamic_tiles: int = 0
4279+
4280+
def __init__(self, *args, **kwargs):
4281+
super().__init__(*args, **kwargs)
4282+
assert self.hparams_vision is not None
4283+
self.min_dynamic_tiles = self.global_config.get("min_dynamic_patch", 0)
4284+
self.max_dynamic_tiles = self.global_config.get("max_dynamic_patch", 0)
4285+
42764286
def set_gguf_parameters(self):
42774287
assert self.hparams_vision is not None
42784288
if isinstance(self.hparams_vision['image_size'], list):
@@ -4295,6 +4305,11 @@ def set_gguf_parameters(self):
42954305
downsample_ratio = self.global_config.get("downsample_ratio")
42964306
assert downsample_ratio is not None
42974307
self.gguf_writer.add_vision_projector_scale_factor(int(1.0 / downsample_ratio))
4308+
# older models may not have min/max_dynamic_patch in config
4309+
if self.min_dynamic_tiles > 0:
4310+
self.gguf_writer.add_vision_preproc_min_tiles(self.min_dynamic_tiles)
4311+
if self.max_dynamic_tiles > 0:
4312+
self.gguf_writer.add_vision_preproc_max_tiles(self.max_dynamic_tiles)
42984313

42994314
def tensor_force_quant(self, name, new_name, bid, n_dims):
43004315
if ".position_embd." in new_name:

gguf-py/gguf/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,8 @@ class ClipVision:
301301
IMAGE_SIZE = "clip.vision.image_size"
302302
IMAGE_MIN_PIXELS = "clip.vision.image_min_pixels"
303303
IMAGE_MAX_PIXELS = "clip.vision.image_max_pixels"
304+
PREPROC_MIN_TILES = "clip.vision.preproc_min_tiles"
305+
PREPROC_MAX_TILES = "clip.vision.preproc_max_tiles"
304306
PREPROC_IMAGE_SIZE = "clip.vision.preproc_image_size"
305307
PATCH_SIZE = "clip.vision.patch_size"
306308
EMBEDDING_LENGTH = "clip.vision.embedding_length"

gguf-py/gguf/gguf_writer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,12 @@ def add_vision_max_pixels(self, value: int) -> None:
11561156
def add_vision_min_pixels(self, value: int) -> None:
11571157
self.add_uint32(Keys.ClipVision.IMAGE_MIN_PIXELS, value)
11581158

1159+
def add_vision_preproc_max_tiles(self, value: int) -> None:
1160+
self.add_uint32(Keys.ClipVision.PREPROC_MAX_TILES, value)
1161+
1162+
def add_vision_preproc_min_tiles(self, value: int) -> None:
1163+
self.add_uint32(Keys.ClipVision.PREPROC_MIN_TILES, value)
1164+
11591165
def add_vision_preproc_image_size(self, value: int) -> None:
11601166
self.add_uint32(Keys.ClipVision.PREPROC_IMAGE_SIZE, value)
11611167

tools/mtmd/clip-impl.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
#define KEY_IMAGE_SIZE "clip.vision.image_size"
3939
#define KEY_IMAGE_MIN_PIXELS "clip.vision.image_min_pixels"
4040
#define KEY_IMAGE_MAX_PIXELS "clip.vision.image_max_pixels"
41+
#define KEY_PREPROC_MIN_TILES "clip.vision.preproc_min_tiles"
42+
#define KEY_PREPROC_MAX_TILES "clip.vision.preproc_max_tiles"
4143
#define KEY_PREPROC_IMAGE_SIZE "clip.vision.preproc_image_size"
4244
#define KEY_PATCH_SIZE "clip.vision.patch_size"
4345
#define KEY_IMAGE_MEAN "clip.vision.image_mean"

tools/mtmd/clip-model.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ struct clip_hparams {
4242
int32_t image_max_pixels = -1;
4343
int32_t n_merge = 0; // number of patch merges **per-side**
4444

45+
int32_t preproc_min_tiles = 0;
46+
int32_t preproc_max_tiles = 0;
47+
4548
float image_mean[3];
4649
float image_std[3];
4750

tools/mtmd/clip.cpp

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,16 @@ struct clip_model_loader {
11381138
}
11391139
} break;
11401140
case PROJECTOR_TYPE_INTERNVL:
1141+
{
1142+
// older version of internvl doesn't have min/max tiles, we need to provide default values for them to avoid issues
1143+
hparams.preproc_min_tiles = 1;
1144+
hparams.preproc_max_tiles = 12;
1145+
get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);
1146+
get_u32(KEY_PREPROC_MIN_TILES, hparams.preproc_min_tiles, false);
1147+
get_u32(KEY_PREPROC_MAX_TILES, hparams.preproc_max_tiles, false);
1148+
GGML_ASSERT(hparams.preproc_min_tiles <= hparams.preproc_max_tiles && hparams.preproc_max_tiles < INT32_MAX);
1149+
set_internvl_dhr_res_candidates(model);
1150+
} break;
11411151
case PROJECTOR_TYPE_NEMOTRON_V2_VL:
11421152
{
11431153
get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);
@@ -2188,6 +2198,27 @@ struct clip_model_loader {
21882198
}
21892199
}
21902200
}
2201+
2202+
static void set_internvl_dhr_res_candidates(clip_model & model) {
2203+
auto & hparams = model.hparams;
2204+
int min_num = hparams.preproc_min_tiles;
2205+
int max_num = hparams.preproc_max_tiles;
2206+
if (min_num < 1) {
2207+
return; // avoid divide by 0
2208+
}
2209+
for (int a = min_num; a <= max_num; ++a) {
2210+
int b_lo = (min_num + a - 1) / a;
2211+
int b_hi = max_num / a;
2212+
b_lo = std::max(b_lo, min_num);
2213+
b_hi = std::min(b_hi, max_num);
2214+
for (int b = b_lo; b <= b_hi; ++b) {
2215+
hparams.image_res_candidates.push_back(clip_image_size {
2216+
a*hparams.image_size,
2217+
b*hparams.image_size,
2218+
});
2219+
}
2220+
}
2221+
}
21912222
};
21922223

21932224
struct clip_init_result clip_init(const char * fname, struct clip_context_params ctx_params) {
@@ -2734,17 +2765,22 @@ struct llava_uhd {
27342765
return res;
27352766
}
27362767

2737-
static std::vector<clip_image_u8_ptr> slice_image(const clip_image_u8 * img, const slice_instructions & inst) {
2768+
static std::vector<clip_image_u8_ptr> slice_image(const clip_image_u8 * img, const slice_instructions & inst, bool overview_first = true) {
27382769
std::vector<clip_image_u8_ptr> output;
27392770

27402771
// resize to overview size
27412772
clip_image_u8_ptr resized_img(clip_image_u8_init());
27422773
img_tool::resize(*img, *resized_img, inst.overview_size, inst.interpolation_overview,
27432774
inst.padding_overview, inst.pad_color_overview);
2744-
output.push_back(std::move(resized_img));
2775+
if (overview_first) {
2776+
output.push_back(std::move(resized_img));
2777+
}
27452778

27462779
if (inst.slices.empty()) {
27472780
// no slices, just return the resized image
2781+
if (!overview_first) {
2782+
output.push_back(std::move(resized_img));
2783+
}
27482784
return output;
27492785
}
27502786

@@ -2765,6 +2801,10 @@ struct llava_uhd {
27652801
output.push_back(std::move(img_slice));
27662802
}
27672803

2804+
if (!overview_first) {
2805+
output.push_back(std::move(resized_img));
2806+
}
2807+
27682808
return output;
27692809
}
27702810

@@ -3149,10 +3189,20 @@ bool clip_image_preprocess(struct clip_ctx * ctx, const clip_image_u8 * img, str
31493189
res_imgs->grid_x = instructions.grid_size.width;
31503190
res_imgs->grid_y = instructions.grid_size.height;
31513191
} break;
3192+
case PROJECTOR_TYPE_INTERNVL: // support dynamic high-resolution
3193+
{
3194+
GGML_ASSERT(!params.image_res_candidates.empty());
3195+
auto const inst = llava_uhd::get_slice_instructions(ctx, original_size);
3196+
std::vector<clip_image_u8_ptr> imgs = llava_uhd::slice_image(img, inst, false);
31523197

3198+
for (size_t i = 0; i < imgs.size(); ++i) {
3199+
clip_image_f32_ptr res(clip_image_f32_init());
3200+
normalize_image_u8_to_f32(*imgs[i], *res, params.image_mean, params.image_std);
3201+
res_imgs->entries.push_back(std::move(res));
3202+
}
3203+
} break;
31533204
case PROJECTOR_TYPE_GLM_EDGE:
31543205
case PROJECTOR_TYPE_GEMMA3:
3155-
case PROJECTOR_TYPE_INTERNVL: // TODO @ngxson : support dynamic resolution
31563206
case PROJECTOR_TYPE_NEMOTRON_V2_VL:
31573207
{
31583208
clip_image_u8 resized_image;

tools/mtmd/mtmd.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -851,13 +851,15 @@ int32_t mtmd_encode(mtmd_context * ctx, const mtmd_image_tokens * image_tokens)
851851
LOG_ERR("%s: this API does not support non-vision input, please use mtmd_encode_chunk instead\n", __func__);
852852
return 1;
853853
}
854+
auto proj_type = clip_get_projector_type(ctx_clip);
854855
int n_mmproj_embd = clip_n_mmproj_embd(ctx_clip);
855856
ctx->image_embd_v.resize(image_tokens->n_tokens() * n_mmproj_embd);
856857
bool ok = false;
857858

858859
if (clip_is_llava(ctx_clip)
859860
|| clip_is_minicpmv(ctx_clip)
860-
|| clip_is_glm(ctx_clip)) {
861+
|| clip_is_glm(ctx_clip)
862+
|| proj_type == PROJECTOR_TYPE_INTERNVL) {
861863
// TODO @ngxson : llava does not support batched encoding ; this should be fixed inside clip_image_batch_encode()
862864
const auto & entries = image_tokens->batch_f32.entries;
863865
for (size_t i = 0; i < entries.size(); i++) {

0 commit comments

Comments
 (0)