Skip to content

Feature/Qwen3vl (single commit) - #161

Merged
copybara-service[bot] merged 4 commits into
jax-ml:mainfrom
coder0143:main
Feb 19, 2026
Merged

Feature/Qwen3vl (single commit)#161
copybara-service[bot] merged 4 commits into
jax-ml:mainfrom
coder0143:main

Conversation

@coder0143

@coder0143 coder0143 commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Resolves #133

Reference

Adding qwen3vl model in single commit, btw do update the main readme with all the newer models post merging. To @chapman20j @jenriver

Checklist

  • I have read the Contribution Guidelines and used pre-commit hooks to format this commit.
  • I have added all the necessary unit tests for my change. (run_model.py for model usage, test_outputs.py and/or model_validation_colab.ipynb for quality).
  • (If using an LLM) I have carefully reviewed and removed all superfluous comments or unneeded, commented-out code. Only necessary and functional code remains.
  • I have signed the Contributor License Agreement (CLA).

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @coder0143, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request integrates the Qwen3-VL Vision Language Model into the JAX ecosystem, providing a complete and functional implementation. It focuses on enabling efficient and scalable inference through detailed sharding configurations and a robust mechanism for loading pretrained weights. The addition is thoroughly validated with tests ensuring numerical consistency and proper distributed behavior.

Highlights

  • New Model Implementation: Introduced a pure JAX implementation of the Qwen3-VL SOTA Vision Language Model, leveraging the Flax NNX API.
  • Sharding Support: Implemented comprehensive sharding configurations for both vision encoder and text decoder components, enabling distributed inference using FSDP and Tensor Parallelism.
  • Model Configurations: Added support for various Qwen3-VL model sizes (2B, 4B, 8B, 32B) and NVIDIA Cosmos-Reason2 models, with detailed configuration classes.
  • Weight Loading Utility: Developed a utility to load pretrained weights from safetensors files, including necessary key mapping and tensor transformations for compatibility with the JAX model structure.
  • Extensive Testing: Included comprehensive unit tests to verify numerical equivalence against the PyTorch implementation for both text-only and vision-text forward passes, as well as dedicated tests for sharding functionality.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • bonsai/models/qwen3_vl/README.md
    • Added a new README file detailing the Qwen3-VL JAX implementation, supported model configurations, running instructions, and contribution guidelines.
  • bonsai/models/qwen3_vl/modeling.py
    • Added the core Qwen3-VL model implementation, including vision and text encoder components, sharding configurations, attention mechanisms (with RoPE), MLP layers, RMSNorm, KV-caching, and modality merging logic.
  • bonsai/models/qwen3_vl/params.py
    • Added utilities for loading pretrained weights from safetensors files, including key mapping and tensor transformations to adapt PyTorch weights to the JAX model structure.
  • bonsai/models/qwen3_vl/tests/run_model.py
    • Added a script to demonstrate fast greedy generation for both text-only and vision-text inputs, with optional sharding configuration.
  • bonsai/models/qwen3_vl/tests/test_outputs_qwen3vl.py
    • Added unit tests to verify numerical equivalence between the JAX implementation and the original PyTorch model for various components and full forward passes, including vision and text modalities.
  • bonsai/models/qwen3_vl/tests/test_sharding_qwen3vl.py
    • Added unit tests to validate the sharding functionality, ensuring numerical equivalence between sharded and unsharded model components.
Activity
  • The pull request introduces the Qwen3-VL model implementation in a single commit.
  • The author linked this PR to resolve issue Feature/qwen3 vl #133.
  • The author confirmed adherence to contribution guidelines, including adding unit tests and signing the CLA.
  • The author requested a follow-up task to update the main README with the newly added models after merging.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a comprehensive JAX implementation of the Qwen3-VL model, including the model architecture, weight conversion utilities from PyTorch, and an extensive test suite. The code is well-structured and the addition is a valuable contribution to the repository. My review focuses on improving maintainability and fixing a minor documentation issue. I've identified a dependency on an internal JAX API that should be replaced, an unused variable that can be removed for code clarity, and a broken link in the README.

Comment thread bonsai/models/qwen3_vl/modeling.py Outdated
Comment on lines +880 to +883
if jax._src.config.abstract_mesh_context_manager.value is not None:
attn_weights = jnp.matmul(q, k.transpose(0, 1, 3, 2), out_sharding=self.shd_cfg.act_btd) * self.scale
else:
attn_weights = jnp.matmul(q, k.transpose(0, 1, 3, 2)) * self.scale

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.

high

The code uses an internal JAX API (jax._src.config.abstract_mesh_context_manager.value) to check if it's running within a mesh context. Relying on internal APIs is fragile and can break with future JAX updates. It's better to use the public jax.sharding.get_abstract_mesh() API for this check, similar to the pattern used in the ShardedLinear module.

Suggested change
if jax._src.config.abstract_mesh_context_manager.value is not None:
attn_weights = jnp.matmul(q, k.transpose(0, 1, 3, 2), out_sharding=self.shd_cfg.act_btd) * self.scale
else:
attn_weights = jnp.matmul(q, k.transpose(0, 1, 3, 2)) * self.scale
mesh = get_abstract_mesh()
if not mesh.empty and len(mesh.axis_names) > 0:
attn_weights = jnp.matmul(q, k.transpose(0, 1, 3, 2), out_sharding=self.shd_cfg.act_btd) * self.scale
else:
attn_weights = jnp.matmul(q, k.transpose(0, 1, 3, 2)) * self.scale

Comment thread bonsai/models/qwen3_vl/README.md Outdated

### Running this model

Run Qwen3 in action, implemented in [900 lines of code](bonsai/models/qwen3_vl/modeling.py) in JAX.

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.

medium

The relative link to modeling.py is incorrect. Since this README.md is in the same directory as modeling.py, the path should be relative to the current directory.

Suggested change
Run Qwen3 in action, implemented in [900 lines of code](bonsai/models/qwen3_vl/modeling.py) in JAX.
Run Qwen3 in action, implemented in [900 lines of code](modeling.py) in JAX.

Comment thread bonsai/models/qwen3_vl/params.py Outdated
key_mapping = _get_key_and_transform_mapping(config.text_config.tie_word_embeddings)

conversion_errors = []
loaded_keys = set()

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.

medium

The loaded_keys variable is initialized here and updated on line 369, but its value is never read or used. This unused variable should be removed to improve code clarity. The update on line 369 should also be removed.

@coder0143

Copy link
Copy Markdown
Contributor Author

Shall I make these changes?

@vfdev-5

vfdev-5 commented Feb 13, 2026

Copy link
Copy Markdown
Member

Seems like we have a large overlap with qwen3 model, maybe we can either put vl version to qwen3 or import qwen3 code here?

@coder0143

Copy link
Copy Markdown
Contributor Author

Hello @vfdev-5 , there is an overlap, but I think we should keep them separate. Only 3 things are common for the text decoder parts:

Qwen3Attention
Qwen3MLP
Qwen3RMSNorm

Some things are fundamentally different and changing them might break multimodal reasoning:

  1. RoPE -> 3D interleaved M-RoPE
  2. deeptack visual indices, vision masks, cross layer vision injection (decoder)

@coder0143

Copy link
Copy Markdown
Contributor Author

I will make the final changes along with testing and pack it all into a single commit

Comment thread bonsai/models/qwen3_vl/modeling.py Outdated

# --- Sharding Configuration --- #
@dataclass(slots=True, frozen=True)
class VisionShardingCfg:

@vfdev-5 vfdev-5 Feb 16, 2026

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.

Let's unify the sharding config names, recent update was to adopt this naming:

class VisionShardConfig:

Also let's implement no sharding option as no partition, same as

class VisionShardConfig:
attn_kernel: PartitionSpec | None = None
attn_bias: PartitionSpec | None = None
attn_qk_activation: PartitionSpec | None = None
fc1_kernel: PartitionSpec | None = None
fc1_bias: PartitionSpec | None = None
fc2_kernel: PartitionSpec | None = None
fc2_bias: PartitionSpec | None = None
activation: PartitionSpec | None = None
layer_norm: PartitionSpec | None = None
emb_patch_kernel: PartitionSpec | None = None
emb_patch_bias: PartitionSpec | None = None
emb_pos_kernel: PartitionSpec | None = None
@staticmethod
def no_sharding():
return VisionShardConfig()

Let's set ShardMode names instead of hardcoded "fsdp", "tp":
class ShardMode(Enum):
FSDP = "fsdp"
TP = "tp"

Ideally, these names should be even configurable, but this is for another PR.

@coder0143

Copy link
Copy Markdown
Contributor Author

Cool, on it!

@coder0143

Copy link
Copy Markdown
Contributor Author

Final changes based on all the reviews along with testing done, the code can be merged @jenriver @chapman20j , @vfdev-5 , thanks for reviewing my PR

@copybara-service
copybara-service Bot merged commit d3d3899 into jax-ml:main Feb 19, 2026
5 checks passed
cache: Cache,
input_ids: Array,
pixel_values: Array,
image_grid_thw: Array,

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.

@coder0143 What is the structure of image_grid_thw? I saw in run_model it was reported as [[1, 64, 64]]. In the code 3 values only used :

        grid_h, grid_w = int(grid_thw[0, 1]), int(grid_thw[0, 2])
        grid_t = int(grid_thw[0, 0])

Should not it be static tuple of int instead of being a jax array? And to confirm, we are not missing the batch dim?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@vfdev-5 yup you are correct, I'm making changes on my fork, will you review the changes, or you can open a PR yourself to support jit for forward_vision.

@vfdev-5 vfdev-5 Feb 20, 2026

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 think there is a bug in Qwen3VLVisionModel, the line:

hidden_states = hidden_states + pos_embeds[:seq_len]

The error message:

E     TypeError: add got incompatible shapes for broadcasting: (784, 64), (196, 64).

if we preprocess a message with 4 chat messages (dicts), we will get the following shapes for pixel values and thw grid:

>>> inputs = processor.apply_chat_template(messages, tokenize=True, add_generation_prompt=True, return_dict=True, return_tensors="pt")                                                                   
>>> inputs["pixel_values"].shape                                                                                                                                                                         
torch.Size([1024, 1536]) 
>>> inputs["image_grid_thw"]
tensor([[ 1, 16, 16],
        [ 1, 16, 16],
        [ 1, 16, 16],
        [ 1, 16, 16]]) 
>>> inputs["image_grid_thw"].shape
torch.Size([4, 3])

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.

Here is the test I would like to run in test_sharding_qwen3vl.py

    def test_full(self):
        """Test full model can be created with sharding enabled."""
        rngs = nnx.Rngs(0)
        fsdp = modeling.ShardMode.FSDP.value
        model = modeling.Qwen3VLForConditionalGeneration(self.cfg_sharded, rngs=rngs)
        config = model.config

        # TODO: enable the test once fixed the bug in Qwen3VLVisionModel when batch size > 1
        batch_size = 4  # should be evenly divisible to num devices for fsdp axis
        num_tokens = 128
        key = jax.random.key(0)
        patch_size = config.vision_config.patch_size
        img_size = patch_size * patch_size
        n_img = jax.random.uniform(
            key,
            (batch_size * img_size, img_size * 3 * 2),
            dtype=jnp.float32,
            minval=-1,
            maxval=1,
            out_sharding=P(fsdp),
        )
        n_text = jax.device_put(
            np.arange(batch_size * num_tokens).reshape(batch_size, -1),
            device=P(fsdp),
        )
        token_type_ids = np.zeros((batch_size, num_tokens), dtype=int)
        token_type_ids[:, 12:98] = 1
        n_tti = jax.device_put(
            token_type_ids,
            device=P(fsdp),
        )
        image_grid_thw = jnp.asarray([[1, patch_size, patch_size]] * batch_size)
        cache = modeling.init_cache(config, batch_size, num_tokens, 1, jnp.float32)

        out = model(n_text, n_img, image_grid_thw, cache=cache, token_type_ids=n_tti)

        assert isinstance(out.sharding, NamedSharding)
        assert out.sharding.spec == config.text_config.shd_cfg.act_btd

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants