feat(xla): derive image context floors for LLaVA and Qwen2-VL - #1280
Merged
Conversation
`xla_image_context_floor` only had a formula for Molmo2, so LLaVA and Qwen2-VL fell through the startup guard silently and still hit the failure it exists to prevent: run the whole vision tower, then reject at admission. Both derivations mirror what this codebase's preprocessors actually emit rather than upstream behavior, because the guard has to predict the token count the runtime will produce. LLaVA's block carries no markers, separator or per-row token, so one image contributes exactly the projector's count and there is no shape to maximize over. That count is `mm_tokens_per_image` when stated, otherwise `(image_size / patch_size)^2`, matching `load_llava_host_preprocessor`. No anyres grid is applied, because that loader applies none either, including for `llava_next` checkpoints which route to the same path. Qwen2-VL has no fixed grid. `smart_resize` rounds both edges to `patch_size * spatial_merge_size` and caps the area, and the token count is `(h / merge) * (w / merge)`, so both reduce to `pixels / (patch * merge)^2` and the maximum is that quotient at the pixel cap. The cap comes from the processor constructor, not `preprocessor_config.json`: `qwen_vl_processor` calls `Qwen2VLProcessor::new`, which never reads that file, so a `max_pixels` key in a checkpoint does not affect what this path admits. The bounds are now named constants shared with the derivation so the two cannot drift. The guard's message is rewritten because the Qwen2-VL floor exposed a flaw in it. Real expansion on that family spans 64 tokens for a 224x224 image to 16384 for one at the pixel cap, a 256x range, and the old text told the operator to set the capacity to the maximum "to serve images". That is advice nobody can follow at 16384, and it is also wrong: a smaller capacity serves every image that fits it. The message now asks for the largest expansion the operator intends to serve and states what a smaller value does, including that oversized images are still rejected only after their vision tower has run. Verified on five real checkpoints: llava-1.5-7b (576), llava-interleave-0.5b (729), llava-next-mistral-7b (576), and qwen2-vl-2b with its 4bit variant (16384). The Qwen2-VL check runs the real processor over extreme shapes rather than re-evaluating the same formula, and asserts the worst observed count equals the floor, so the bound is shown to be tight and not merely safe.
Recorded before the merge, per the TECHNICAL_REPORTS/.keep-reports contract, so the report lands inside the squash merge rather than trailing it.
This was referenced Aug 22, 2026
inureyes
added a commit
that referenced
this pull request
Aug 22, 2026
clippy 1.97 flags .err().expect() as err_expect, and the workspace gate runs with -D warnings, so a clean main fails local clippy since #1280. The two load_xla_image_preprocessor sites keep the .err().expect() shape: their Ok type does not implement Debug, so expect_err does not apply and the lint does not fire there.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Derive the worst-case image context floor for LLaVA and Qwen2-VL, so the OpenXLA startup guard stops falling through for two families that are qualified for the image path. Only Molmo2 had a formula, so LLaVA and Qwen2-VL still hit the failure the guard exists to prevent: run the whole vision tower, then reject at admission.
Both derivations follow this codebase, not the upstream families
Worth stating up front, because the divergence will look like a bug to anyone comparing against HF behavior:
load_llava_host_preprocessorcomputesmm_tokens_per_image.unwrap_or((image_size / patch_size)^2), andllava_token_block_infosetsuse_boi_eoi: falsewith empty prefix and suffix lists, so one image contributes exactly that count with no framing tokens and no shape dependence.llava_nextcheckpoints route to the same loader unless the text backbone is Granite, so they get the same formula.max_pixelsfrompreprocessor_config.json.qwen_vl_processorbuilds the processor withQwen2VLProcessor::new, which sets the pixel bounds from constructor defaults and never opens that file. Amax_pixelskey in a checkpoint has no effect on what this path admits, so a floor derived from one would be a number the runtime does not honor.The guard has to predict what the runtime will emit, so both follow the code. The pixel bounds are now named constants in the processor module, shared with the derivation, so the two cannot drift apart.
The Qwen2-VL floor exposed a flaw in the guard's message
Qwen2-VL expansion depends on image shape across a 256x range:
The old message told the operator to set the capacity to the floor "to serve images". At Molmo2's 1834 that is followable. At 16384 it is not, given that capacity is the sequence length every decode step attends over and measured decode already falls from 3.18 to 1.41 tok/s between 256 and 2048.
It was also wrong on its own terms. A capacity below the floor serves every image whose expansion fits it, which on this family is most real images. The old text implied such a configuration could serve none.
The message now asks for the largest expansion the operator intends to serve, says what a smaller value does, and notes that oversized images are still rejected only after their vision tower has run. It carries no issue numbers, because a GitHub reference is not useful to someone reading a server startup failure.
This edits
ensure_xla_image_context_capacity, which the issue placed out of scope. That scope line was written before the 256x range was known, and shipping a correct number attached to impossible advice would have been worse than the gap it closes.Validation
Unit tests: 26 passed in
host_preprocessor, existing Qwen2-VL processor test still green.Five real checkpoints, floors derived from each one's own
config.json:The Qwen2-VL check runs the real
smart_resizeandcompute_grid_thwover extreme shapes rather than re-evaluating the derivation, which would only have proved the formula agrees with itself. The worst observed count equals the floor exactly, so the bound is tight rather than a safe overestimate. That direction matters: a floor above the true maximum only costs a larger graph, while one below reinstates the late-admission failure.The real-checkpoint test is
#[ignore]and readsMLXCEL_FLOOR_MODEL, so it does not require checkpoints to be present, and it loads no weights.Recorded caveat
If the LLaVA host preprocessor ever implements an anyres grid, the LLaVA floor stops being a fixed per-image count and has to be revisited. The derivation is correct for the loader as it exists, not for the family in general. This is noted in the code rather than only here.
Closes #1272