Skip to content

Add Xiaomi-Lens-0 - #50028

Open
guarin wants to merge 37 commits into
huggingface:mainfrom
guarin:add-xiaomi-lens
Open

guarin wants to merge 37 commits into
huggingface:mainfrom
guarin:add-xiaomi-lens

Conversation

@guarin

@guarin guarin commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

🤗 Transformers CI

What does this PR do?

Fixes # (issue)

Code Agent Policy

The Transformers repo is currently being overwhelmed by a large number of PRs and issue comments written by
code agents. These often are low-quality, or fix extremely minor issues that occur rarely or never in practice.
As a result, we're instituting a rule that first-time contributors should not use code agents to submit PRs or issues.
We'd also ask autonomous "OpenClaw"-like agents not to open any PRs or issues.

Issues/PRs from first-time contributors that violate this rule will probably just be closed without review, and we
might block you, especially if you open more than one or appear to be deliberately ignoring this. We especially do not
want new contributors to jump in on random issues to contribute an agent-written fix. This creates lots of noise
for reviewers and other users and will almost certainly get you blocked.

For more information, please read CONTRIBUTING.md.

  • (First-time contributors only): I confirm that this PR description and code is not written by an LLM or code agent

Before submitting

  • This PR fixes a typo or improves the docs (you can dismiss the other checks if that's the case).
  • Did you read the contributor guideline and the
    Pull Request checks?
  • Was this discussed/approved via a Github issue or the forum? Please add a link
    to it if that's the case.
  • Did you make sure to update the documentation with your changes according to the guidelines?
  • Did you write any new necessary tests?

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.

🤖 mlinter review state

Comment on lines +67 to +84
image_features = model.get_image_features(
inputs.pop("pixel_values"), inputs.image_grid_thw, return_dict=True
)
generated = model.generate(
**inputs,
mm_encoder_outputs={"image": image_features},
return_dict_in_generate=True,
output_hidden_states=[model.config.text_config.num_hidden_layers - 1],
use_cache=True,
num_beams=1,
num_return_sequences=1,
max_new_tokens=1024,
)
hidden_states = torch.cat([step[-1] for step in generated.hidden_states], dim=1)
mask_logits, mask_token_indices = model.decode_masks(
generated.sequences, hidden_states, image_features.last_hidden_state,
inputs.image_grid_thw, ignore_thinking=True,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I had a very long plan to return image/video hidden states same way as we return encoder_hidden_states. It would be done auto-magically by routing model outputs directly into generation outputs so we dont have to manually infer/collect outputs per model

The person working on it left HF and the task got buried 😢

So the general plan i had was:

  • All multimodal models return same format mm_outputs, e.g. rn some models return only pooled images from VLM and some return a whole ModelOutput. IMO returning ModelOutput as in old blip models is the better way to allow flexibility
  • GenerationMixin split out the part of output collection into a smaller helper that mostly re-rereouts most fields returned from model to Mixin's output (we still dont return MoE router-logits and similar)
output = model.generate(
        **inputs,
        return_dict_in_generate=True,
        output_hidden_states=[model.config.text_config.num_hidden_layers - 1],
        max_new_tokens=1024,
    )
hidden_states = torch.cat([step[-1] for step in output.hidden_states], dim=1)
image_hidden_states = output.image_hidden_states[-1] # it is already only last one returned in list
mask_logits, mask_token_indices = model.decode_masks(
        output.sequences, hidden_states, image_hidden_states,
        inputs.image_grid_thw, ignore_thinking=True,
    )

@HuggingFaceDocBuilderDev

HuggingFaceDocBuilderDev commented Oct 29, 2026 •

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Pages changed in this PR:

@molbap molbap 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.

hey, thanks! left an initial review, looks clean so far, not sure about the tracing. I like your proposal @zucchini-nlp , worth tackling IMO

@require_vision
class XiaomiLens0IntegrationTest(MemoryCleanupMixin, unittest.TestCase):
# TODO: Update before merge
model_id = "guarin/Xiaomi-Lens-0" # "SeerRay-Lab/Xiaomi-Lens-0"

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.

do we need a hub PR as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, will open it once it is clear that no major changes are required

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.

ok! let's ping on slack as well

@molbap molbap 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.

few more generic comments

image_valid_mask = None if valid_targets_mask is None else valid_targets_mask[index]
num_masks += image_logits.shape[0] if image_valid_mask is None else image_valid_mask.flatten(1).any(-1).sum()

for start in range(0, image_logits.shape[0], chunk_size):

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.

IIUC: this chunking only lowers memory under no grad (eval loss), in training autograd keeps every chunk's fp32 tensors until backward anyway. the original also checkpoints each chunk, that's where it saves, so maybe we can we drop the loop and mask_loss_chunk_size?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fair, will drop it but then training will require a lot more compute.

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.

ah really? I hadn't measured such a big loss tbh (just 7% more memory), but if you think it's worse we keep it as it was

Comment on lines +165 to +167
elif labels is not None:
# Match the collective on ranks with mask supervision without connecting the decoder to the loss.
_normalize_mask_count(logits.new_zeros((), dtype=torch.float32))

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.

not too familiar with the DDP side, if I follow correctly, a rank that only gets text batches never calls the decoder, so its params get no grad there. and with gradient checkpointing I think the Trainer sets find_unused_parameters=False, which I think crashes?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Exactly, it would only work with find_unused_parameters=True, also with other trainers than hf

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.

ok so the docstring note is enough for me

@molbap molbap 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.

Nice work 🚀
Approving for the "standard" stuff, for the custom generate maybe worth exploring an initial PR that does the returning of img hidden states cleanly and stack this one on top of it? I think it would be preferred against having a standalone class that does just one thing that is mostly a wrapper on utils we have

Comment thread utils/modular_model_converter.py Resolved
Comment on lines +165 to +167
elif labels is not None:
# Match the collective on ranks with mask supervision without connecting the decoder to the loss.
_normalize_mask_count(logits.new_zeros((), dtype=torch.float32))

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.

ok so the docstring note is enough for me

@require_vision
class XiaomiLens0IntegrationTest(MemoryCleanupMixin, unittest.TestCase):
# TODO: Update before merge
model_id = "guarin/Xiaomi-Lens-0" # "SeerRay-Lab/Xiaomi-Lens-0"

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.

ok! let's ping on slack as well

@zucchini-nlp zucchini-nlp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

adding my 2 cents 😄

btw, i will be making proper model output in multmodals. I got a local branch tho it still waits for an update for 40 existing models 😭
Left a comment re naming of model output tho to make refactor easier

mask_logits = mask_embeddings @ pixel_embeddings.flatten(2) / self.config.mask_embed_dim**0.5
mask_logits = mask_logits.view(batch_size, -1, height, width)
# The fixed 4x upsampling brings the masks to the resolution of the image fed to the vision encoder
return F.interpolate(mask_logits, scale_factor=self.scale_factor, mode="bilinear", align_corners=False)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

vision_utils.get_interpolte_indices for export friendliness? 😄

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hmm don't think that is needed here as it is just a simple interpolate and all logits have the same size

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ah my bad, qwen ruined my life forever 😆

Comment thread src/transformers/conversion_mapping.py Resolved
Comment on lines +69 to +71
return XiaomiLens0GenerateOutput(**result, image_hidden_states=image_hidden_states)
return result

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

image_last_hidden_state pls since it is only the last one returned

Vision patch features before spatial merging, retaining gradients for mask supervision.
"""

image_hidden_states: torch.FloatTensor | None = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same here pls

@github-actions

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: auto, xiaomi_lens0

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Model linter — first pass

transformers-mlinter found 2 item(s) in the model files this PR touches. These are structural conventions a maintainer would otherwise flag by hand.

This is automated and advisory — it does not block merging.

rule count what it checks
TRF055 2 PreTrainedModel classes declare their config with a config: SomeConfig annotation, matched by __init__(config: SomeConfig); never config_class.

image_last_hidden_state: torch.Tensor | None = None


class XiaomiLens0Model(Qwen3_5Model):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

TRF055 — TRF055: generated modeling_xiaomi_lens0.py:76: XiaomiLens0Model.__init__ must annotate its config argument: config: XiaomiLens0Config, matching the config attribute it inherits or overrides.

Why this matters

PreTrainedModel derives config_class from the config annotation. An assignment or a hand-written config_class keeps or drifts from the parent's, and a mismatched __init__ builds the model from one config while it advertises another.

class Gemma4VisionModel(Gemma4PreTrainedModel):
     """The Gemma 4 Vision Encoder."""
-    config = Gemma4VisionConfig
+    config: Gemma4VisionConfig

-    def __init__(self, config):
+    def __init__(self, config: Gemma4VisionConfig):

Suppress with # trf-ignore: TRF055 if intentional.



@auto_docstring
class XiaomiLens0ForConditionalGeneration(Qwen3_5ForConditionalGeneration, XiaomiLens0GenerationMixin):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

TRF055 — TRF055: generated modeling_xiaomi_lens0.py:734: XiaomiLens0ForConditionalGeneration.__init__ must annotate its config argument: config: XiaomiLens0Config, matching the config attribute it inherits or overrides.

Why this matters

PreTrainedModel derives config_class from the config annotation. An assignment or a hand-written config_class keeps or drifts from the parent's, and a mismatched __init__ builds the model from one config while it advertises another.

class Gemma4VisionModel(Gemma4PreTrainedModel):
     """The Gemma 4 Vision Encoder."""
-    config = Gemma4VisionConfig
+    config: Gemma4VisionConfig

-    def __init__(self, config):
+    def __init__(self, config: Gemma4VisionConfig):

Suppress with # trf-ignore: TRF055 if intentional.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants