Repository navigation
Add a llava model type for fixed-resolution vision-language models - #2842
Open
Pierluigi Urru (stwykd) wants to merge 7 commits into
Open
Pierluigi Urru (stwykd) wants to merge 7 commits into
Pierluigi Urru (stwykd) wants to merge 7 commits into
Conversation
A prompt without images allocated the image features as [0, hidden] even when the vision and embedding models use [num_images, num_tokens, hidden], so ORT rejected the embedding run with an invalid rank. Only a negative batch size now drops the dimension. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: af407617-e477-4690-b7f6-38e5f88f661b
MultiModalPipelineState did not override SetActiveAdapter, so an adapter set through the generator went to the pipeline state's own run options, which no session uses, and was ignored. Forward it to the decoder state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: af407617-e477-4690-b7f6-38e5f88f661b
Runs the existing vision -> embedding -> decoder pipeline for models whose vision encoder emits a fixed number of features per image that replace an image placeholder token (LLaVA-style), with no image_sizes and 1D positions. - model.image_token_id names the placeholder; each one in the prompt is expanded to model.vision.num_visual_tokens copies, or to the static token dimension of the vision output when that is not set. - Image preprocessing is the ort-extensions pipeline in the processor config. - Tests use a tiny generated model and check generation against plain ONNX Runtime sessions, text-only prompts, token count errors and adapters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: af407617-e477-4690-b7f6-38e5f88f661b
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Non-default vision input names break rank-3 LLaVA generation by producing zero-sized feature buffers, while order-insensitive test fixtures can miss incorrect image-feature ordering.
2 open issues
What changed in this PR
Adds a config-driven llava model type for fixed-resolution vision-language models, reusing the existing vision → embedding → decoder pipeline.
Changes:
- Registers LLaVA support and adds image preprocessing, placeholder expansion, and configuration validation.
- Preserves rank-3 image features for text-only prompts and forwards active adapters to the decoder.
- Adds a small model fixture, its generator, and Python regression tests.
| File | Description |
|---|---|
test/python/models/test_llava_models.py |
Tests preprocessing, generation, validation, and adapters. |
test/python/create/create_dummy_llava_models.py |
Generates the small LLaVA test fixture. |
test/models/llava/tokenizer.json |
Defines the fixture vocabulary and image token. |
test/models/llava/tokenizer_config.json |
Configures fixture tokenizer metadata. |
test/models/llava/processor_config.json |
Defines fixed-resolution image transforms. |
test/models/llava/genai_config.json |
Configures the three-session test pipeline. |
src/models/preprocessing/multi_modal_processor.cpp |
Registers the LLaVA processor. |
src/models/preprocessing/llava_image_processor.h |
Declares the processor and token-expansion helper. |
src/models/preprocessing/llava_image_processor.cpp |
Implements preprocessing and placeholder expansion. |
src/models/multi_modal.h |
Declares multimodal adapter forwarding. |
src/models/multi_modal.cpp |
Forwards active adapters to the decoder. |
src/models/model_type.h |
Registers llava as a vision-language model. |
src/models/io/multi_modal_features.cpp |
Preserves feature rank for zero-image inputs. |
README.md |
Lists LLaVA-style model support. |
.gitignore |
Allows tracking the LLaVA test fixtures. |
Review effort: Balanced
The test decoder summed its input embeddings, so its logits didn't change when image features were swapped or moved, and the reference reused the processor's own input_ids and pixel_values. The decoder now weights each embedding by its absolute position (also in the KV cache), and the reference is built from the tokenized prompt expanded in the test and from each image preprocessed on its own. A new test checks that swapping the images changes the generated tokens. Adds a rank-2 variant of the test model, test/models/llava-rank2, whose vision model emits [total_tokens, hidden] features and sets num_visual_tokens, and checks it against the reference with one image, two images and text only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: af407617-e477-4690-b7f6-38e5f88f661b
The processors hand the pixels over under model.vision.inputs.pixel_values, but GetImageFeatureBatchSize only looked for the default pixel_values name. With a renamed vision input the image count fell through to 0, and the vision model failed with a pre-allocated [0, ...] image_features output. Match the configured name too. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: af407617-e477-4690-b7f6-38e5f88f661b
The pixels from the processor config were copied as float32 whatever their type, so a pipeline that left them uint8 (no Rescale, Normalize or Permute3D) read past the end of the buffer, and a channel-last output only failed inside the vision model with an ONNX Runtime shape error. Require float32 pixels, and check the channel, height and width dimensions against the vision input's static ones, with errors that point at the processor config. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: af407617-e477-4690-b7f6-38e5f88f661b
Contributor
|
/azp run |
|
Azure Pipelines: Successfully started running 2 pipeline(s). |
Author
|
Baiju Meswani (@baijumeswani) kunal-vaishnavi could you take a look when you get a chance? Sushanth suggested you. merged main in since the integration tests were failing on the older base, could someone /azp run again? |
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
Adds a config-driven
llavamodel type. It runs the existing vision -> embedding -> decoder pipeline(
MultiModalLanguageModel/MultiModalPipelineState) for VLMs whose fixed-resolution vision encoderemits a fixed number of features per image, and those features replace an image placeholder token that is
expanded to that many positions (LLaVA-style). There's no
image_sizes, no HD crops and no mRoPE; thedecoder is a regular
inputs_embedsdecoder with past/present KV (e.g. GroupQueryAttention).Today such an export has no type to use:
gemma3hardcodes its own image tokens and\n\nframing,phi3vneedsimage_sizes/num_img_tokensfromPhi3ImageTransform, and the Qwen-VL family assumes mRoPE.Config
image_token_idtoken per image. The processor tokenizes the prompt, checks thatcount against the number of images and expands each token to
vision.num_visual_tokenscopies.num_visual_tokens(an existing field) is optional: by default it's read from the static tokendimension of the vision output
[num_images, num_tokens, hidden]. If both are set they must agree.vision.config_filename, which must produce float32[N, C, H, W]. The processor checks that, and the channel, height and width against the vision input's staticdimensions, with an error pointing at the processor config (a pipeline left uint8 used to read past the buffer).
Pixels are cast to the vision input type when it's float32, float16 or bfloat16.
input_idsand the image features. It puts the feature rows atthe image-token positions, in order (HF LLaVA's masked scatter).
Fixes this needed (separate commits)
MultiModalFeaturesdropped the batchdimension, so
[num_images, num_tokens, hidden]became[0, hidden]and ORT failed with "Invalid rankfor input: image_features". The batch dimension is now dropped only for a negative batch size.
Every in-tree test model uses rank-2 features, so none of them changes.
Generator.set_active_adapteron multimodal pipelines.MultiModalPipelineStatedidn't overrideSetActiveAdapter. The adapter went to the pipeline state's own run options, which no session uses,so it was silently ignored for every VLM type. It's now forwarded to the decoder state. This covers the
models that run through
MultiModalPipelineState; pipelined decoder configs (decoder.pipeline) aren't changed.model.vision.inputs.pixel_values,but the image count only looked for the default
pixel_valuesname, so with a renamed input it fell to 0and the vision model failed on a
[0, ...]output. It now matches the configured name too (this also fixesa renamed rank-3
gemma3export).Tests
test/python/create/create_dummy_llava_models.pygenerates two tiny models, checked in astest/models/llavaandtest/models/llava-rank2(~14 KB each; the second emits[total_tokens, hidden]features and sets
num_visual_tokens). The generator reproduces them byte for byte. The decoder weightseach embedding by its absolute position, so its logits change when image features are swapped or moved.
test/python/models/test_llava_models.py(20 tests):in both orders, text-only, and 1/2/0 images on the rank-2 model). The reference's inputs are built
without the processor under test: the prompt tokenized and expanded in the test, and each image
preprocessed on its own. Swapping the two images changes the generated tokens;
num_visual_tokensmismatch, a missingimage_token_id,and a processor config that leaves the pixels uint8 or channel-last;
decoder-input-ids), the API, component-session, non-generative and adapter graph-capture tests, and
C++
unit_testsall pass on Linux x64 CPU.int8 KV cache, LoRA adapter). Greedy tokens are identical to a plain ONNX Runtime reference driving the
same graphs (CPU fp32, with and without the adapter, rerun after these changes).
Notes
cmake/deps.txtpin.transform. Companion PR: Add a PadToSquare image transform onnxruntime-extensions#1146 adds
PadToSquare; after acmake/deps.txtbump itcan go in
processor_config.json.This change is small and usable now.
cc kunal-vaishnavi Tianlei Wu (@tianleiwu)