Skip to content

Adding jit support to Qwen3VL, readme fixes - #170

Open
coder0143 wants to merge 3 commits into
jax-ml:mainfrom
coder0143:main
Open

Adding jit support to Qwen3VL, readme fixes#170
coder0143 wants to merge 3 commits into
jax-ml:mainfrom
coder0143:main

Conversation

@coder0143

Copy link
Copy Markdown
Contributor

Adds jit support to Qwen3VL model and fixes some readme files

Based on @vfdev-5 's suggestions, jit support and batched input support added for Qwen3VL model, the tests can be seen here: https://www.kaggle.com/code/prathamshahmldlds/test-bonsai-qwen3-vl (on T4x2 with mesh (1,2)).

@chapman20j and @jenriver do check and merge.

Fixes #161

Checklist

  • I have read the Contribution Guidelines and used pre-commit hooks to format and squash so that this PR has 1 commit typically.
  • I have added all the necessary unit tests for my change (run_model.py, test_outputs.py, and/or model_validation_colab.ipynb).
  • (Code Quality) I have reviewed the code and removed all superfluous comments or LLM-generated boilerplate. Only functional, necessary 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 introduces Just-In-Time (JIT) compilation support for the Qwen3VL model's vision component, enhancing its performance and enabling more efficient batched processing of visual inputs. The core vision embedding logic has been refactored to dynamically handle variable image grid dimensions within a JIT-compatible framework. Additionally, the changes include minor documentation updates to reflect the improved model capabilities and status.

Highlights

  • JIT Compilation for Qwen3VL Vision: Enabled JIT compilation for the Qwen3VL model's vision forward pass, significantly improving performance and efficiency for visual input processing.
  • Batched Input Support: Refactored the vision encoder's position embedding and rotary position embedding calculations to efficiently handle batched inputs by processing individual images and then concatenating results.
  • Static Argument for JIT: Introduced a GridTHW type alias (tuple of tuples) for image_grid_thw and marked it as a static argument for JIT compilation, allowing the JAX compiler to optimize for varying image grid dimensions.
  • Documentation Updates: Updated the main README.md to reflect the status of newly supported models (Dinov3, VJEPA-2, Qwen3VL) and made a minor fix in the Qwen3VL specific README.md.
  • Test Adaptations: Adjusted existing Qwen3VL test files (run_model.py, test_outputs_qwen3vl.py) to align with the new image_grid_thw input type, ensuring compatibility with the JIT-compiled vision component.

🧠 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
  • README.md
    • Updated the status of Dinov3, VJEPA-2, and Qwen3VL models in the main model table.
  • bonsai/models/qwen3_vl/README.md
    • Removed an extraneous empty line from the model support table.
  • bonsai/models/qwen3_vl/modeling.py
    • Added functools import and defined GridTHW type alias for image grid dimensions.
    • Refactored _fast_pos_embed_interpolate into _fast_pos_embed_interpolate_single and a new _fast_pos_embed_interpolate to handle single and batched image grids respectively.
    • Refactored _rot_pos_emb into _rot_pos_emb_single and a new _rot_pos_emb to support batched rotary position embeddings.
    • Updated type hints for image_grid_thw to GridTHW in __call__ and forward_vision.
    • Applied jax.jit decorator to forward_vision, marking image_grid_thw as a static argument.
  • bonsai/models/qwen3_vl/tests/run_model.py
    • Modified the conversion of image_grid_thw from a NumPy array to a tuple of tuples to match the new GridTHW type.
  • bonsai/models/qwen3_vl/tests/test_outputs_qwen3vl.py
    • Updated all relevant test cases to convert image_grid_thw to a tuple of tuples (grid_thw_tuple) before passing it to the Flax model's vision components.
Activity
  • JIT support and batched input for the Qwen3VL model were implemented based on suggestions from @vfdev-5.
  • Tests for the changes are available on Kaggle, demonstrating functionality on T4x2 with mesh (1,2).
  • This pull request addresses and fixes issue Feature/Qwen3vl (single commit) #161.
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 successfully adds JIT compilation support to the Qwen3VL model's vision processing path, which is a great performance enhancement. The core change involves making the image grid dimensions a static argument by changing its type from a JAX array to a tuple of integers. This required some thoughtful refactoring to support batched inputs, which has been done well. The tests and documentation have been updated accordingly. I have a couple of minor suggestions to improve code conciseness.

Comment thread bonsai/models/qwen3_vl/modeling.py Outdated
Comment on lines +651 to +653
results = []
for grid_t, grid_h, grid_w in grid_thw:
results.append(self._fast_pos_embed_interpolate_single(grid_t, grid_h, grid_w))

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

For conciseness and better readability, this for-loop can be replaced with a list comprehension. This is a common Pythonic pattern.

Suggested change
results = []
for grid_t, grid_h, grid_w in grid_thw:
results.append(self._fast_pos_embed_interpolate_single(grid_t, grid_h, grid_w))
results = [self._fast_pos_embed_interpolate_single(grid_t, grid_h, grid_w) for grid_t, grid_h, grid_w in grid_thw]

Comment thread bonsai/models/qwen3_vl/modeling.py Outdated
Comment on lines +702 to +704
embs = []
for grid_t, grid_h, grid_w in grid_thw:
embs.append(self._rot_pos_emb_single(grid_t, grid_h, grid_w))

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

Similar to the other comment, this loop can be simplified into a more concise list comprehension, which is generally preferred in Python for creating lists.

Suggested change
embs = []
for grid_t, grid_h, grid_w in grid_thw:
embs.append(self._rot_pos_emb_single(grid_t, grid_h, grid_w))
embs = [self._rot_pos_emb_single(grid_t, grid_h, grid_w) for grid_t, grid_h, grid_w in grid_thw]

grid_thw_np = np.array([[1, 16, 16]], dtype=np.int64)
grid_thw_pt = torch.tensor(grid_thw_np)
grid_thw_jax = jnp.array(grid_thw_np)
grid_thw_tuple = ((1, 16, 16),)

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 we can rewrite these 3 lines as following:

grid_thw_tuple = ((1, 16, 16),)
grid_thw_pt = torch.tensor(grid_thw_tuple, dtype=torch.long)

and remove grid_thw_np as unused

grid_thw_np = np.array([[grid_t, grid_h, grid_w]], dtype=np.int64)
grid_thw_pt = torch.tensor(grid_thw_np)
grid_thw_jax = jnp.array(grid_thw_np)
grid_thw_tuple = ((grid_t, grid_h, grid_w),)

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 idea here

@vfdev-5

vfdev-5 commented Feb 21, 2026

Copy link
Copy Markdown
Member

@coder0143 can you try to run the following sharding test to make sure the batched input issue is fixed. Thanks!

@unittest.skipIf(jax.device_count() < 8, "Atleast 8 devices required")
class TestSharding(absltest.TestCase):
    """Test sharding with simulated 8-device mesh."""

    @classmethod
    def setUpClass(cls):
        cls.mesh = jax.make_mesh((4, 2), ("fsdp", "tp"), axis_types=(AxisType.Explicit, AxisType.Explicit))
        jax.set_mesh(cls.mesh)

        cls.cfg_unsharded = get_test_config(use_fsdp=False, use_tp=False)
        cls.cfg_sharded = get_test_config(use_fsdp=True, use_tp=True)

    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

        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 =((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

@coder0143

Copy link
Copy Markdown
Contributor Author

@vfdev-5 The fsdp test added and some small rewrites for unused vars done!

self.assertIsInstance(out.sharding, NamedSharding)
expected_logit_shd = P(cfg.text_config.shd_cfg.act_btd[0], None, None)
self.assertEqual(out.sharding.spec, expected_logit_shd)
print(f"Full vision+text forward: shape={out.shape}, sharding={out.sharding.spec} ✓")

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.

Please remove all prints


def test_full_vision_text_forward(self):
"""Test full vision+text forward pass with sharding (FSDP=4, TP=2)."""
mesh = jax.make_mesh((4, 2), ("fsdp", "tp"), axis_types=(AxisType.Explicit, AxisType.Explicit))

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.

we can set the mesh once in the class setUp

@coder0143

Copy link
Copy Markdown
Contributor Author

On it!

@coder0143

Copy link
Copy Markdown
Contributor Author

@vfdev-5 Done, sharding file has been updated, removed prints, setup mesh of (4,2) across the file.

@coder0143

coder0143 commented Feb 24, 2026

Copy link
Copy Markdown
Contributor Author

@jenriver , @vfdev-5 The changes are done here, do check and merge, thanks!

@vfdev-5 vfdev-5 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.

Thanks for the PR @coder0143
I'll run locally the sharding test_full to make sure there is nothing missing.
I left a minor comment

inputs = self._create_dummy_image_input()
grid_thw_pt = inputs["image_grid_thw"]
grid_thw_jax = jnp.array(grid_thw_pt.numpy())
grid_thw_tuple = tuple((int(x[0]), int(x[1]), int(x[2])) for x in grid_thw_pt)

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.

Can't we do grid_thw_tuple = tuple(grid_thw_pt.long().tolist()) instead?

@coder0143

Copy link
Copy Markdown
Contributor Author

@vfdev-5 Fixed everywhere for grid_thw_tuple

@coder0143 coder0143 mentioned this pull request Mar 2, 2026
@coder0143

Copy link
Copy Markdown
Contributor Author

@vfdev-5 , @jenriver Do check and merge, thanks! I need it for research work and would like to directly use bonsai rather than my fork. The earlier implementation which I merged didn't support full jit and has some readme issues.

pixel_values = jnp.array(inputs_vision["pixel_values"].numpy())
image_grid_thw = jnp.array(inputs_vision["image_grid_thw"].numpy())
image_grid_thw_raw = inputs_vision["image_grid_thw"].numpy()
image_grid_thw = tuple((int(row[0]), int(row[1]), int(row[2])) for row in image_grid_thw_raw)

@vfdev-5 vfdev-5 Mar 2, 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.

And here we can't use the previous trick (tuple(grid_thw_pt.long().tolist()))?

You do not need to define image_grid_thw_raw neither

@coder0143

Copy link
Copy Markdown
Contributor Author

I sincerely apologize for being impatient and rushing the maintainers. From now on I will double check and ping only when absolutely necessary, thankyou.

@vfdev-5

vfdev-5 commented Mar 3, 2026

Copy link
Copy Markdown
Member

@coder0143 please squash all commits into 1.

I can confirm sharding tests are passing locally.

@coder0143

coder0143 commented Mar 3, 2026

Copy link
Copy Markdown
Contributor Author

I have combined all commits into a single commit, I am not touching the main Readme file (earlier I added a few newly added models to it), do check if any more changes have to be made @vfdev-5 , thanks!

@coder0143 coder0143 reopened this Mar 3, 2026
if "pixel_values" in inputs_vision:
pixel_values = jnp.array(inputs_vision["pixel_values"].numpy())
image_grid_thw = jnp.array(inputs_vision["image_grid_thw"].numpy())
image_grid_thw = tuple(tuple(row) for row in inputs_vision["image_grid_thw"].long().tolist())

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 see that finally we could not make the original code simple as we still need to iterate over its structure. I wonder if we can accept the type of image_grid_thw as list[list[int]] instead of tuple[tuple[int, int, int]] ? Can this work with jit and the type checker etc?

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.

No, list[list[int]] won't work directly with static_argnums because lists are not hashable — JAX needs to hash static arguments for its compilation cache, and list raises TypeError: unhashable type: 'list'.

To make it simple we can do something like:

@functools.partial(jax.jit, static_argnums=(4,))
def _forward_vision_jit(model, cache, input_ids, pixel_values, image_grid_thw, token_type_ids):
    logits = model(input_ids, cache, pixel_values, image_grid_thw, token_type_ids)
    return logits[:, -1, :], cache

def forward_vision(model, cache, input_ids, pixel_values, image_grid_thw, token_type_ids):
    """Accepts list[list[int]] or tuple — converts to hashable tuple for JIT."""
    grid_thw = tuple(tuple(row) for row in image_grid_thw)
    return _forward_vision_jit(model, cache, input_ids, pixel_values, grid_thw, token_type_ids)

Which makes the callers simple:

image_grid_thw = inputs["image_grid_thw"].long().tolist()  # [[1, 64, 64]]
logits, cache = forward_vision(model, cache, ids, pixels, image_grid_thw, tti)

@vfdev-5 Do tell if you want me to make the changes.

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.

Finally, I think it's fine to keep it everywhere as tuple of tuples. We have to do the conversion in tests only so it's ok

@coder0143

Copy link
Copy Markdown
Contributor Author

Just notifying, I have started working on Qwen3.5 and will make a PR soon

@vfdev-5 vfdev-5 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.

LGTM

@coder0143

Copy link
Copy Markdown
Contributor Author

@jenriver Please do check and merge, thanks for reviewing the final changes @vfdev-5

@coder0143

Copy link
Copy Markdown
Contributor Author

Just a reminder..

@coder0143

Copy link
Copy Markdown
Contributor Author

@vfdev-5 Please do merge if possible

cgarciae
cgarciae previously approved these changes Apr 13, 2026
@vfdev-5

vfdev-5 commented Apr 14, 2026

Copy link
Copy Markdown
Member

@coder0143 PR needs rebasing as this branch has conflicts that must be resolved

@coder0143

coder0143 commented Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

@vfdev-5 Yup, the earlier code was mine and was merged by chapman20j (is working on another team) earlier, the next code has been pushed here by me, I have resolved the conflicts by accepting the current(newer) changes which were verified via the tests. Thankyou for approving the changes @cgarciae !
On selecting current changes in merge conflicts, the earlier review might have gotten dismissed.

@cgarciae

Copy link
Copy Markdown
Collaborator

@coder0143 seems that pre-commit is failing, see test.

@coder0143

Copy link
Copy Markdown
Contributor Author

@cgarciae Thanks for reviewing, fixed the cache bug, pre-commit is running now and the rest of the tests are passing as well

@coder0143

Copy link
Copy Markdown
Contributor Author

@cgarciae , @vfdev-5 Please do rerun the selective tests whenever possible, i have fixed the single line bug as the pr was old.

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.

3 participants