Skip to content

mtmd : lfm2 tiles and layout match transformers - #30261

Merged
ngxson merged 4 commits into
ggml-org:masterfrom
ykhrustalev:ykhrustalev/lfm2-tiles-transformers
Oct 20, 2026
Merged

ngxson merged 4 commits into
ggml-org:masterfrom
ykhrustalev:ykhrustalev/lfm2-tiles-transformers

Conversation

@ykhrustalev

@ykhrustalev ykhrustalev commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Overview

LFM2-VL, LFM2.5-VL, d1-3B and d1-omni cut a large image into 512x512 tiles plus a small thumbnail. llama.cpp did this differently from the reference Python code (transformers Lfm2VlImageProcessor, d1-omni's vision.py) in two ways:

  • Tiles: Python stretches the image to fill the tile grid. llama.cpp kept the image's shape and filled the rest with black bars. This PR stretches for all LFM2 models, as llama.cpp already does for d1-omni (model : add LiquidAI/d1-omni-600M decision model #30114).
  • Layout: Python rounds halves to even and uses double precision when it picks the tile grid and the thumbnail size. llama.cpp used std::round and float, so 31,926 of the 16.8M image sizes up to 4096x4096 got another grid or thumbnail, and another number of tokens. For example, a 720x1600 image got 2x5 tiles instead of 2x4. This PR rounds as Python does, like mtmd: fix LFM2 image tiling threshold #27057 already did for should_tile.

Only LFM2 code changes; other models are not affected.

Additional information

Reproduction (prompt tokens: master 2812, this PR 2298, transformers 2298):

python3 -c "from PIL import Image; Image.new('RGB', (720, 1600), 'gray').save('img.png')"
llama-server -hf LiquidAI/LFM2.5-VL-450M-GGUF:F16 --port 8080 &
until curl -sf localhost:8080/health > /dev/null; do sleep 1; done
echo '{"max_tokens": 1, "messages": [{"role": "user", "content": [{"type": "image_url", "image_url": {"url": "data:image/png;base64,'$(base64 < img.png | tr -d '\n')'"}}]}]}' > req.json
curl -s localhost:8080/v1/chat/completions -H 'Content-Type: application/json' --data-binary @req.json | jq .usage.prompt_tokens
python3 -c "from PIL import Image; from transformers import AutoProcessor; p = AutoProcessor.from_pretrained('LiquidAI/LFM2.5-VL-450M'); t = p.apply_chat_template([{'role': 'user', 'content': [{'type': 'image'}]}], add_generation_prompt=True); print(len(p(images=[Image.open('img.png')], text=t)['input_ids'][0]))"

With a 400x600 image: master 258, this PR 239, transformers 239.

Accuracy: the largest probability difference to the Python reference (fp32; d1: over the answer options, LFM2-VL: over transformers' top-20 next tokens), and the number of items with another prompt length. Master 50e3e3e, Apple M3 Ultra (Metal), F16 models:

model, test set master this PR
LFM2.5-VL-450M, 52 images 0.193, 14 0.014, 0
LFM2-VL-450M, 52 images 0.503, 14, 2 top-1 flips 0.145, 0, 0
d1-omni-600M, 96 image questions 0.954, 42, 4 flipped answers 0.010, 0, 0
d1-3B, 96 image questions 0.356, 36 0.029, 6 (see below)
d1-3B, 450 POPE questions 0.056, 5 0.009, 0
  • LFM2-VL-450M stays at 0.145 because its published mmproj has no clip.vision.image_resize_algo, so llama.cpp resizes it with bilinear while transformers uses bicubic. With the key set to bicubic: 0.444 -> 0.003.
  • The new layout code gives the same result as the Python code for every image size up to 4096x4096. Where the layout changes, 27,834 sizes get fewer tokens (up to 2048 fewer) and 4,092 get more: most by under 100 tokens, 523 by 256 to 1024 tokens (shapes that sit exactly between two grids, where Python picks the bigger grid).
  • One case gets worse for d1-3B: its own runner shrinks images above 1 megapixel before tiling (720x1600 becomes 686x1526, 2x5 tiles). Master matched that by accident; this PR gives 2x4, as transformers does for the full image. 6 test items change: 3 worse (largest 0.019 -> 0.029), 3 better. Adding the shrink step is out of scope.

Speed follows the number of tokens. Whole-request time, this PR vs master 50e3e3e (F16, -np 1), on an H100 GPU (CUDA) and a 30-core Xeon 8358 (CPU):

image (prompt tokens, master -> this PR) CUDA (LFM2.5-VL / d1-3B) CPU (LFM2.5-VL / d1-3B)
504x1120 (2817 -> 761) -65.0% / -69.4% -73.6% / -72.6%
720x1600 (2817 -> 2303) -16.6% / -17.3% -16.3% / -17.8%
640x480, 1024x768, 2000x1500, 3024x4032 (unchanged) -2.8% to +0.8% within noise
1000x1200 (1797 -> 2568) +29.9% / +36.4% +43.0% / +42.9%
560x960 (771 -> 1799) +90.6% / +111% +124% / +131%

Stretching costs nothing: preparing a 1024x768 image takes 25.9 ms instead of 31.0 ms (LFM2.5-VL, CUDA build).

Tests: test-mtmd-impl gets 4 thumbnail-size cases and 4 grid cases with the values transformers gives; master's code fails 6 of the 13 checks. The stretch is a loader setting and has no unit test. For a 1024x768 image on LFM2.5-VL-450M, master's tiles differ from transformers in 97.5% of the pixel values (up to 255, the black bars); this PR's tiles are identical.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES , Claude is being used to run the investigation, implementation and the tests, human supervised every stage and directed the agent.

@ykhrustalev
ykhrustalev requested review from a team and ggerganov as code owners October 10, 2026 00:20
@github-actions github-actions Bot added testing Everything test related mtmd Related to multimodal functionality (video/image/audio) labels Oct 10, 2026
@ykhrustalev
ykhrustalev marked this pull request as draft October 13, 2026 14:31
@ykhrustalev
ykhrustalev marked this pull request as ready for review October 13, 2026 23:54
@ykhrustalev
ykhrustalev force-pushed the ykhrustalev/lfm2-tiles-transformers branch from af737d3 to 76b9ea3 Compare October 17, 2026 13:02

@ngxson ngxson left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please rebase this?

Lfm2VlImageProcessor.crop_image_to_patches resizes the image straight to
(512 * rows, 512 * cols) before it cuts the tiles. llama.cpp kept the aspect
ratio and added black bars for LFM2 (only d1-omni stretched), so the tiles of
d1-3B and every LFM2-VL / LFM2.5-VL model differed from transformers unless
the image had the aspect ratio of the grid. With PAD_NONE the tiles are the
same as in transformers.
Lfm2VlImageProcessor.smart_resize rounds with Python's round (half to even)
and computes beta and the tile grid aspect ratio in double. llama.cpp used
std::round and float, so 31926 of the 16.7M sizes in [1,4096]^2 got another
thumbnail size or grid than transformers (and d1-omni vision.py), e.g.
400x600 -> 416x608 instead of 384x608 and 720x1600 -> 2x5 tiles instead of
2x4. The lfm2 smart resize and grid choice now follow the Python code and
match it on every size of that range.
@ykhrustalev
ykhrustalev force-pushed the ykhrustalev/lfm2-tiles-transformers branch from 76b9ea3 to 61042fe Compare October 19, 2026 23:45
@ngxson
ngxson merged commit 4a45de4 into ggml-org:master Oct 20, 2026
13 checks passed
auRose94 pushed a commit to auRose94/llama.cpp that referenced this pull request Oct 28, 2026
* mtmd : lfm2 tiles stretch the image to the grid

Lfm2VlImageProcessor.crop_image_to_patches resizes the image straight to
(512 * rows, 512 * cols) before it cuts the tiles. llama.cpp kept the aspect
ratio and added black bars for LFM2 (only d1-omni stretched), so the tiles of
d1-3B and every LFM2-VL / LFM2.5-VL model differed from transformers unless
the image had the aspect ratio of the grid. With PAD_NONE the tiles are the
same as in transformers.

* mtmd : lfm2 layout with Python rounding in double precision

Lfm2VlImageProcessor.smart_resize rounds with Python's round (half to even)
and computes beta and the tile grid aspect ratio in double. llama.cpp used
std::round and float, so 31926 of the 16.7M sizes in [1,4096]^2 got another
thumbnail size or grid than transformers (and d1-omni vision.py), e.g.
400x600 -> 416x608 instead of 384x608 and 720x1600 -> 2x5 tiles instead of
2x4. The lfm2 smart resize and grid choice now follow the Python code and
match it on every size of that range.

* fix

---------

Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mtmd Related to multimodal functionality (video/image/audio) testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants