Add Molmo2#43451
Conversation
Adds AllenAI Molmo2 multimodal VLM to transformers, supporting: - Molmo2ForConditionalGeneration (image+video+text → text) - Molmo2TextModel / Molmo2TextForCausalLM (text-only) - Molmo2ImageProcessor and Molmo2VideoProcessor - Molmo2Processor Key implementation details: - Uses is_first_iteration (v5 API) for prepare_inputs_for_generation - Custom Molmo2Embedding with embedding + new_embedding parameters - Vision backbone with pooling adapter and multi-layer ViT features - Dynamic full cache support for generation Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…odel_prefix - Replace einops.rearrange with native numpy reshape+transpose+reshape - Add @strict decorator to all 4 config classes (Molmo2VitConfig, Molmo2AdapterConfig, Molmo2TextConfig, Molmo2Config) to satisfy TRF010 - Set Molmo2Model.base_model_prefix = "model" (was empty, violating TRF002) - Fix image_mean/image_std mutable shared list (copy constants on init) - Fix test_image_processing: use image_processing_class instead of image_processor_list; skip CHW torch and 4-channel unsupported tests Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Re-sort _toctree.yml to place Molmo2 after mllama alphabetically - Add None guard in test_video_processor_from_dict_with_kwargs to skip when fast_video_processing_class is not defined Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Molmo2TextModel is an internal sub-component used by Molmo2Model and Molmo2ForConditionalGeneration and is tested implicitly through those. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
requests is not part of the standard library and caused ImportError in minimal environments (e.g. HuggingFace Jobs). Use urllib.request instead. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Molmo2's processor has several behaviors that are incompatible with the default ProcessorTesterMixin assumptions: - Chat template enforces strict user/assistant alternation (no system role) - Processor inserts BOS token, shifting sequence length by 1 - Image processor patchifies output, so rescale_factor passthrough fails - Video processor requires FPS metadata not provided by base tests - Hub processor_config.json contains auto_map not preserved in save/load Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Add @auto_docstring(checkpoint="allenai/Molmo2-8B") decorator to Molmo2TextConfig and Molmo2Config with custom_args for documenting non-standard parameters. This fixes check_config_docstrings CI check. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
… date Add parameter docstrings to Molmo2TextConfig and Molmo2Config __init__ methods so @strict-wrapped classes pass config docstring CI checks. Update model doc date to 2026-03-28. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Move top-level `import torch` and `import torchvision.transforms` behind `is_torch_available()` / `is_torchvision_available()` guards in both image and video processors to prevent ModuleNotFoundError when torchvision is not installed. Also skip test_kwargs_overrides_default_image_processor_kwargs since Molmo2's patchifying image processor doesn't support rescale_factor passthrough. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Convert all absolute imports (from transformers.xxx) to relative imports (from ...xxx) in image_processing, video_processing, and processing modules to match the convention used by all other in-library models. Remove register_for_auto_class() calls which are only needed for custom hub models and were causing dynamic_module_utils to incorrectly scan local files for relative imports during save_pretrained. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…n_available The processor's top-level imports from image_processing_molmo2 and video_processing_molmo2 pull in PILImageResampling which requires PIL. Guard these imports with is_vision_available() so `from transformers import *` works when only torch is installed (no PIL/torchvision). Co-Authored-By: Claude Opus 4.6 <[email protected]>
…L imports Move Molmo2ImagesKwargs and Molmo2VideosKwargs definitions directly into processing_molmo2.py instead of importing them from image/video processor modules which require PIL. Also remove Molmo2ImageProcessor/VideoProcessor type hints from __init__ to avoid NameError when vision is unavailable. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
@molbap Hi I am still working on it since I have to make some example visualizer for this and (most of the code is generated by Claude code). However, you can start review this with brief level of code review! cc. @merveenoyan |
Add integration tests for Molmo2-8B covering: - Image generation with exact expected text verification - Video QA (penguin identification) - Video pointing (coordinate output) - Multi-image comparison All expected values derived from actual model inference on A10G. Co-Authored-By: Claude Opus 4.6 <[email protected]>
zucchini-nlp
left a comment
There was a problem hiding this comment.
Hey @SangbumChoi
Great model to add to Transformers. After reviewing I see that using modular would be much better since a lot of part are copy-paste from different models. I left comments on each class about where it can be copied from. Apart from that, there are a few places where we need to clean up and align API with the rest of VLMs for consistency
If you have q, ping me on slack. I will unsubscribe myself from this PR to not get notif about each commit, so when you want another review ping me again by @
zucchini-nlp
left a comment
There was a problem hiding this comment.
Hey @SangbumChoi
Great model to add to Transformers. After reviewing I see that using modular would be much better since a lot of part are copy-paste from different models. I left comments on each class about where it can be copied from. Apart from that, there are a few places where we need to clean up and align API with the rest of VLMs for consistency
If you have q, ping me on slack. I will unsubscribe myself from this PR to not get notif about each commit, so when you want another review ping me again by @
Per @molbap: the skip was only needed for the base test's assertIsInstance(get_input_embeddings(), nn.Embedding) -- Molmo2 uses a custom Molmo2Embedding (concatenated base + extra-vocab tables), not nn.Embedding. Override the test to relax that one type assertion to nn.Module while still verifying the get/set round-trip (the behavior that actually matters). Confirmed passing on a tiny model. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014Z1uph57N8WrnxSL69bYL4
|
I assume #43451 (review) this is becuase resize logic with similar aspec_ratio or shape is more efficient? is there any blog for this? |
Addresses two of guarin's image-processor review comments (the image_num_crops device fix was done separately by the maintainer): 1. do_rescale / float images: resize_and_normalize_image raised on any non-uint8 input and gated rescale on uint8, so float32 images were rejected and never rescaled. Route rescale/normalize through the base TorchvisionBackend rescale_and_normalize, which honors do_rescale/do_normalize for any dtype (we trust the user's do_rescale for their float scale) and builds the mean/std on the input device. The uint8 path is numerically unchanged. 2. device placement: resize_idx / patch_idx / image_grid were created on CPU while pixel_values live on the input image's device. Create them on image_chw.device so the returned tensors are device-consistent. The group-images-by-shape batching from the same review is a larger refactor, handled separately.
make fix-repo side-effect: the 'remove unnecessary slicing' change was applied to modular_molmo2.py but modeling_molmo2.py still had enumerate(self.blocks[: self.config.num_hidden_layers]). Regenerated so the decoder loop matches the modular (self.blocks already holds exactly num_hidden_layers layers).
Implements guarin's optimization: flatten the images, group_images_by_shape, process each unique shape as one [N,C,H,W] batch (tiling + all patch/pooling indices are shape-dependent only, computed once; resize/unfold vectorized over N), then reorder_images back to the original order. Semantically identical to the former per-image loop (when grouping is disabled each group is N=1). build_resized_image / _build_overlapping_crops are now batch-native; the video _build_frame_patches passes a 1-frame batch.
`get_placeholder_mask` returns a [batch, seq, 1] mask, but Molmo2 adds the image features onto the placeholder-token embeddings via `inputs_embeds[special_image_mask] + image_features`. Direct boolean indexing does not broadcast, so a [batch, seq, 1] mask cannot reach the hidden dim and raises `IndexError: shape of the mask [.., .., 1] at index 2 does not match the indexed tensor [.., .., hidden]` on every image/video input (text-only inputs skip this path, which is why they worked). Expand the mask to the hidden dim before the masked_scatter, restoring the residual-add semantics of the original Molmo2 implementation. Fixes 29 of the fast Molmo2ModelTest failures (test_model, save/load, all generate tests).
- test_tied_weights_keys: set `_tied_weights_keys = None` (Molmo2 ties no weights); an empty list broke the dict-based tied-weights handling. Removes the skip. - test_model_outputs_equivalence: un-skip; the old "shape mismatch" was the image-merge bug (placeholder mask not expanded), already fixed. - test_generate_from_inputs_embeds: allow image features to merge into a provided `inputs_embeds` instead of forbidding it, so the multimodal greedy path and a new text-only override (greedy + beam) run. Multimodal beam search stays skipped (it would need the flat-concatenated crops/pooling offsets expanded by beam width). - test_resize_tokens_embeddings / _untied: keep skipped but document the real reason (Molmo2Embedding's two-table layout with fixed special-token ids makes standard resize ill-defined), rather than the vague "custom embedding" note. Fast Molmo2ModelTest: 126 passed, 119 skipped (3 pre-existing assisted-decoding failures are unrelated).
`token_type_ids_mask_function` only clamped `kv_idx` against the length of `mm_token_type_ids` (which covers just the original prompt), but indexed `token_type_ids[batch_idx, q_idx]` directly. When generation verifies several new tokens in one forward (e.g. assisted/speculative decoding), the query length exceeds the prompt length and `q_idx` runs out of bounds: `IndexError: index N is out of bounds for dimension 1 with size N`. Newly generated positions are always text, never image patches, so clamp the `q_idx` lookup the same way as `kv_idx` and treat out-of-range positions as token-type 0. Fixes test_assisted_decoding_sample and test_assisted_decoding_matches_greedy_search. Fast Molmo2ModelTest is now fully green (129 passed, 119 skipped).
|
@molbap @merveenoyan @guarin gentle ping |
molbap
left a comment
There was a problem hiding this comment.
Hey @SangbumChoi , thanks a lot for iterating on this.
The modular implementation is muuuch cleaner. processor is also looking good. There's no more numpy, einsum, tests are leaner, there's not much missing.
The PR has been open for a long time, and we need to merge it before main drifts away, we get new API changes, and so on. What's left is a small, well-understood set of items that have been through two review rounds already, so rather than start a third one I'll finish them myself directly on this branch.
One ask: please don't push to this branch, so we don't overwrite each other. Your commits and authorship stay exactly as they are and if you spot something you'd like changed, review the PR and I'll include it. I'll ping you when it's ready to merge. Thanks again for carrying the model this far!
| from transformers import Molmo2ForConditionalGeneration, Molmo2Processor | ||
| import torch | ||
|
|
||
| processor = Molmo2Processor.from_pretrained("allenai/Molmo2-8B") | ||
| model = Molmo2ForConditionalGeneration.from_pretrained( | ||
| "allenai/Molmo2-8B", |
There was a problem hiding this comment.
these should work with AutoModel/AutoProcessor
| from transformers import Molmo2ForConditionalGeneration, Molmo2Processor | ||
| from transformers.video_utils import load_video |
| ("mistral3", "PixtralProcessor"), | ||
| ("mm-grounding-dino", "GroundingDinoProcessor"), | ||
| ("modernvbert", "Idefics3Processor"), | ||
| ("molmo2", "Molmo2Processor"), |
There was a problem hiding this comment.
not needed here AFAIK
| ("molmo2", "Molmo2Processor"), |
| add_generation_prompt=True, | ||
| return_tensors="pt", | ||
| return_dict=True, | ||
| processor_kwargs={"video_metadata": [metadata]}, |
There was a problem hiding this comment.
i don't think we should need to pass explicitly processor_kwargs here... we should be able to send video_metadata directly imo
| image_num_pos: int = 577 | ||
| attention_dropout: float = 0.0 | ||
| residual_dropout: float = 0.0 | ||
| float32_attention: bool = True |
There was a problem hiding this comment.
if you upcast inputs just before the dispatched call then sdpa is computed in whatever dtype the inputs are in, that should be enough
| def __post_init__(self, **kwargs): | ||
| if self.attn_implementation is not None: | ||
| kwargs["attn_implementation"] = self.attn_implementation | ||
| super().__post_init__(**kwargs) |
| def __post_init__(self, **kwargs): | ||
| if self.attn_implementation is not None: | ||
| kwargs["attn_implementation"] = self.attn_implementation | ||
| super().__post_init__(**kwargs) |
| def tearDown(self): | ||
| cleanup(torch_device, gc_collect=True) |
There was a problem hiding this comment.
these should call super.teardown
| if self.config._attn_implementation == "eager": | ||
| key_states = repeat_kv(key_states, self.num_key_value_groups) | ||
| value_states = repeat_kv(value_states, self.num_key_value_groups) | ||
| attn_weights = torch.matmul(query_states / math.sqrt(query_states.size(-1)), key_states.transpose(2, 3)) | ||
| if attention_mask is not None: | ||
| # The image pooling adapter passes a boolean keep-mask (valid patches); exclude the | ||
| # invalid ones with -inf to match the SDPA path. A float mask is already additive. | ||
| if attention_mask.dtype == torch.bool: | ||
| attn_weights = attn_weights.masked_fill(~attention_mask, torch.finfo(attn_weights.dtype).min) | ||
| else: | ||
| attn_weights = attn_weights + attention_mask | ||
|
|
||
| attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query_states.dtype) | ||
| attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=self.training) | ||
| attn_output = torch.matmul(attn_weights.to(value_states.dtype), value_states) | ||
| attn_output = attn_output.transpose(1, 2).contiguous() | ||
| elif self.config._attn_implementation == "sdpa": |
There was a problem hiding this comment.
this conditional branching, as said, only reason is the upcast, which can done before the call, and casted back after output.
|
@molbap Got it. From now on I will just leave it as a comment only |
|
Thanks @SangbumChoi ! I also am trying to help narrow down AI-assisted PRs scope to boost contribution. I am trying out a "Self-review" skill to run on your end on your PRs, which should cover the recent API changes/alignment and boost a bit. If you have feedback on it it'd be very helpful! |
|
[For maintainers] Suggested jobs to run (before merge) run-slow: auto, molmo2 |
CI recapDashboard: View test results in Grafana |
What does this PR do?
Fixes # (issue)
Before submitting
Pull Request section?
to it if that's the case.
documentation guidelines, and
here are tips on formatting docstrings.
Who can review?
Anyone in the community is free to review the PR once the tests have passed. Feel free to tag
members/contributors who may be interested in your PR.