Skip to content

Qwen3-VL-2B (VLM)

Overview

This guide is intended for users who are already familiar with the basics of optimum-rbln, and introduces two key techniques for using a Vision-Language Model (VLM) effectively on a single Rebel100™ (RBLN-CR13) NPU.

  • Use bucketing to efficiently handle variable input resolutions and decoder batch sizes within a single compilation.
  • Configure rbln_config per submodule to control behavior at each level.

The running example is Qwen/Qwen3-VL-2B-Instruct. All default values used in the snippets below match the Rebel100™ example in the RBLN Model Zoo (compile.py and inference.py under REBEL/huggingface/transformers/image-text-to-text/qwen3-vl/qwen3-vl-2b/).

Serving on Rebel100™

On Rebel100™, use RBLN vLLM (vllm serve) as the main way to serve VLMs. It compiles the model and exposes an OpenAI-compatible API in one step; see the Qwen3-VL-2B-Instruct recipe. This guide covers the optimum-rbln Python API, which is useful for checking compilation and running inference directly.

Note

Rebel100™ does not support RSD (see the version matrix), and every Rebel100™ example in the Model Zoo compiles for one NPU, so num_devices is set to 1 for both the visual submodule and the LM in this guide.

Note

Debugging error conditions (for example Failed to create RBLN runtime or No memory blocks are available) is out of scope for this guide. For memory- or runtime-creation-related errors, refer to Troubleshooting Multi-Module Models.


Model structure with submodules

A VLM is a model made up of several neural-network components such as a vision encoder and a language model. optimum-rbln places the principal component as the top-level model and declares the rest as submodules, compiling each into its own graph and serving each with its own runtime. With this structure, the desired settings can be specified independently for each submodule.

The structure of Qwen3-VL (Qwen3VLForConditionalGeneration) is as follows.

RBLNQwen3VLForConditionalGeneration   ← top-level (causal LM)
└── visual: RBLNQwen3VLVisionModel    ← submodule (Vision Transformer)

The top-level model is a causal language model (LM), and it contains a Vision Transformer that encodes images and video frames as a single submodule named visual. The roles of each component are as follows.

  • Top-level LM: consumes the image embeddings produced by visual and generates text tokens auto-regressively.
  • visual submodule: a Vision Transformer, executed per image or per video frame.

This tree structure maps directly onto the key structure of the rbln_config passed at compile time: top-level keys apply to the LM, while keys nested under "visual" apply to the visual submodule.

rbln_config = {
    "visual": {  # ← submodule level (Vision Transformer)
        "max_seq_len": 16384,
        "num_devices": 1,
        "create_runtimes": False,
    },
    "num_devices": 1,  # ← top level (LM)
    "kvcache_partition_len": 8_192,
    "max_seq_len": 262_144,
    "create_runtimes": False,
}

Tip

Each field is defined in the RBLN config file of the corresponding model. For example, the fields available on Qwen3-VL's visual submodule are defined in RBLNQwen3VLVisionModelConfig.

Note

Which component is the top-level model and which are submodules varies across models. For example, in LLaVA and Gemma3, the top-level is a Conditional Generation wrapper and both vision_tower and language_model are submodules; in Idefics3, vision_model and text_model are declared as submodules together. To check the structure of a specific model, look at the submodules = [...] declaration in its RBLN config file (configuration_<model>.py).


Using a submodule's rbln_config

This section covers common patterns for configuring optimum-rbln's VLMs with rbln_config on Rebel100™: compiling and loading both submodules on one NPU, bucketing along two axes, and handling other memory and workflow scenarios.

The rbln_config fields most commonly used with Qwen3-VL are summarized below.

Field Level Purpose
batch_size shared Number of concurrent sequences for the whole model; cannot be set per submodule
visual.max_seq_len submodule (visual) Maximum number of patches per image or frame accepted by the ViT graph. Accepts either a single int or a List[int] (bucketing)
visual.num_devices submodule (visual) Number of devices for visual (1 on Rebel100™)
visual.device submodule (visual) Device on which the visual runtime is placed
visual.create_runtimes submodule (visual) Whether to create the visual runtime at compile time
num_devices top (LM) Number of devices for the LM (1 on Rebel100™)
kvcache_partition_len top (LM) Flash attention partition size; must divide max_seq_len evenly
max_seq_len top (LM) Maximum position embedding for the LM; must be a multiple of kvcache_partition_len
decoder_batch_sizes top (LM) List of decoder batch sizes for batch-size bucketing; every value must be ≤ batch_size
device top (LM) Device on which the LM runtime is placed
create_runtimes top (LM) Whether to create the LM runtime at compile time

Caution

Notes for keys that may appear at both levels:

  • num_devices can be set per submodule, but on Rebel100™ it is 1 at both levels, and each submodule's device is a single device index (for example 0).
  • batch_size is a single value that applies to the whole model and cannot be set per submodule. visual and the LM process batches in different ways internally, but optimum-rbln reconciles this difference.

Note

The flash attention limits on Rebel100™ are half of those on ATOM™: kvcache_partition_len must be between 1,024 and 16,384 (ATOM™ allows up to 32,768), and its default is 8,192 (ATOM™: 16,384). max_seq_len must be a multiple of kvcache_partition_len and at least twice its value. The Model Zoo values (kvcache_partition_len=8_192, max_seq_len=262_144, 32 partitions) satisfy these limits.

Compiling and loading both submodules on one NPU

On Rebel100™, visual and the LM each run on a single NPU. The Model Zoo example sets create_runtimes to False at both levels, so compilation only produces and saves the compiled artifacts without creating runtimes.

import os

from optimum.rbln import RBLNAutoModelForImageTextToText

model_id = "Qwen/Qwen3-VL-2B-Instruct"
model = RBLNAutoModelForImageTextToText.from_pretrained(
    model_id,
    export=True,
    rbln_config={
        "visual": {
            "max_seq_len": 16384,
            "num_devices": 1,
            "create_runtimes": False,  # do not create the visual runtime at compile time
        },
        "num_devices": 1,
        "kvcache_partition_len": 8_192,
        "max_seq_len": 262_144,
        "create_runtimes": False,  # do not create the LM runtime at compile time
    },
)
model.save_pretrained(os.path.basename(model_id))

At load time, the device field in rbln_config places each runtime. The Model Zoo inference.py places both visual and the LM on device 0.

import os

from optimum.rbln import RBLNAutoModelForImageTextToText

model_id = "Qwen/Qwen3-VL-2B-Instruct"
model = RBLNAutoModelForImageTextToText.from_pretrained(
    os.path.basename(model_id),
    export=False,
    rbln_config={
        "visual": {
            "device": 0,  # visual on device 0
        },
        "device": 0,  # LM on device 0
    },
)

With this layout, the ViT graphs, the LM graphs, and the KV cache must all fit in the memory of one NPU. If runtime creation fails as the batch size or context length grows, reduce the workload as described in Other scenarios below.

Bucketing the ViT input: visual.max_seq_len

Qwen3-VL's ViT runs on one image (or video frame) at a time, and its graph shape is fixed at compile time. A ViT compiled with max_seq_len=16384 always performs work proportional to 16,384 patches, regardless of whether the actual input contains 1,024 patches or 16,384 patches. In real serving workloads, a single request often contains images of different sizes or videos with different numbers of frames. If a single max_seq_len is sized for the largest expected input, smaller inputs still consume the full capacity, which increases latency and wastes memory.

optimum-rbln addresses this with a multi-size bucketing strategy. visual.max_seq_len accepts not only a single int but also a List[int], and the behavior is as follows.

  • When a list is provided, optimum-rbln compiles a separate ViT graph for each length in a single compilation.
  • At inference time, the smallest bucket that can accommodate the actual patch count is selected automatically.
  • If an input exceeds every bucket in the list, an error is raised. Choose the largest value so that it covers the upper bound of your workload.

The example below is a modified version of the Model Zoo compile.py, where the single value (max_seq_len: 16384) is replaced with a list of three buckets compiled together.

import os

from optimum.rbln import RBLNAutoModelForImageTextToText

model_id = "Qwen/Qwen3-VL-2B-Instruct"
model = RBLNAutoModelForImageTextToText.from_pretrained(
    model_id,
    export=True,
    rbln_config={
        "visual": {
            "max_seq_len": [1024, 3136, 16384],  # ← compile three buckets together
            "num_devices": 1,
            "create_runtimes": False,
        },
        "num_devices": 1,
        "kvcache_partition_len": 8_192,
        "max_seq_len": 262_144,
        "create_runtimes": False,
    },
)
model.save_pretrained(os.path.basename(model_id))

At runtime, the compiled model behaves as follows.

Input patch count Selected bucket
800 1024
2000 3136
5000 16384
20000 error: exceeds all buckets

Note

Each value in max_seq_len is the upper bound on the number of patches the ViT processes for a single image (or one temporal patch, 2 frames, of a video). The ViT counts patches before the spatial_merge_size=2 merger combines them, so with Qwen3-VL's patch_size=16 the patch count for an H × W image is:

patches = (H / 16) × (W / 16)

H and W are the size after the processor resizes the image, with each side rounded to the nearest multiple of 32. For example, 512×512 yields 1,024 patches, 896×896 yields 3,136, and 2048×2048 yields 16,384. The bucket values in the example above (1024, 3136, 16384) cover up to those three resolutions, respectively. The Model Zoo inference.py creates the processor with max_pixels=2048 * 2048, so a resized image never exceeds 16,384 patches, which matches the largest bucket.

Caution

The more buckets you add, the longer compilation takes and the more device memory the compiled model uses. On Rebel100™, all ViT buckets share one NPU with the LM and its KV cache, so selecting too many can exhaust device memory. Two to four buckets chosen from the actual resolution distribution of your traffic are typically sufficient.

Tip

The general mechanics, trade-offs, and standalone examples of bucketing are covered in the Bucketing guide. ViT input-length bucketing applies only to models that explicitly support it; check the model's RBLN config file for a max_seq_len field on the vision submodule declared as Union[int, List[int]]. The LM-side max_seq_len is declared as plain int and does not accept a list.

Bucketing the decoder batch size: decoder_batch_sizes

Qwen3-VL's language-model decoder is also a graph whose shape is fixed at compile time. A decoder compiled with batch_size=4 always performs work proportional to 4 slots, whether the actual batch is 3 or 4. In real serving (continuous batching, in-flight batching), requests finish at different times and the active batch frequently shrinks below the compiled maximum, so the compute spent on unused slots is wasted.

decoder_batch_sizes addresses this with bucketing at the decoder graph level. It is a top-level (LM-side) field defined on RBLNDecoderOnlyModelConfig and accepts a List[int]. The behavior is as follows.

  • When a list is provided, optimum-rbln compiles a separate decoder graph (decoder_batch_{N}) for each batch size.
  • Every value must be ≤ batch_size, and the maximum value should equal batch_size. If it is smaller, optimum-rbln automatically appends batch_size and emits a warning.
  • At inference time, the engine selects the most appropriate decoder graph for the active batch size during the decoding phase.

The configuration below adds batch_size=4 to the Model Zoo compile.py settings and compiles three decoder graphs (1, 2, 4). It also lowers max_seq_len to 32,768: at batch_size=4 the Model Zoo's 262,144-token context does not fit on one Rebel100™ together with the ViT, and loading fails with Failed to create RBLN runtime ... Lack of device memory. With 32,768, both submodules load on one NPU. To keep the full context at a larger batch, place visual on a second NPU (see Troubleshooting Multi-Module Models).

rbln_config = {
    "visual": {
        "max_seq_len": 16384,
        "num_devices": 1,
        "create_runtimes": False,
    },
    "num_devices": 1,
    "kvcache_partition_len": 8_192,
    "max_seq_len": 32_768,  # ← 262_144 at batch_size=4 does not fit one Rebel100™
    "batch_size": 4,
    "decoder_batch_sizes": [1, 2, 4],  # ← compile three decoder graphs
    "create_runtimes": False,
}

At runtime, the compiled model selects a decoder graph based on the active batch size.

Active batch Selected decoder graph
1 decoder_batch_1
2 decoder_batch_2
3–4 decoder_batch_4

Note

The decoder graph is picked at each decoding step by the serving engine. RBLN vLLM uses these graphs as its decode batch buckets. With optimum-rbln alone, Qwen3-VL's generate() runs the batch_size graph and expects exactly batch_size sequences; a smaller batch fails with Cache position size mismatch.

Caution

More decoder graphs mean longer compilation time and higher device memory usage. A common pattern is 1, batch_size//2, batch_size (two to four buckets), adjusted to your traffic profile. On Rebel100™, a larger batch_size also enlarges the KV cache on the same NPU; if runtime creation fails, see Other scenarios.

Tip

ViT input-length bucketing (visual.max_seq_len) and decoder batch-size bucketing (decoder_batch_sizes) are independent and can be combined within a single compilation.

Other scenarios

Specific memory and device configurations, as well as compile/inference workflows, are covered step-by-step in Troubleshooting Multi-Module Models. Frequently encountered scenarios and their corresponding guides:

Scenario Guide
Separate compilation from runtime creation (create_runtimes: False) Step 1
KV cache block pool exhaustion at large batches (tune kvcache_num_blocks) Step 3
Cap the LM context length to the actual workload Step 4
Right-size the ViT input for the target resolution Step 5

Note

The troubleshooting guide shows Rebel100™ and ATOM™ settings in separate tabs. Select the Rebel100™ tab when applying them.


See also