Image-Text-to-Text
Transformers
Safetensors
qwen3_5
vllm
video
multimodal
reinforcement-learning
temporal-grounding
object-tracking
video-segmentation
visual-question-answering
spatial-reasoning
qwen3.5
conversational
Instructions to use OraRL/Video-ORA-9B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use OraRL/Video-ORA-9B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="OraRL/Video-ORA-9B") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("OraRL/Video-ORA-9B") model = AutoModelForMultimodalLM.from_pretrained("OraRL/Video-ORA-9B", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use OraRL/Video-ORA-9B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "OraRL/Video-ORA-9B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OraRL/Video-ORA-9B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/OraRL/Video-ORA-9B
- SGLang
How to use OraRL/Video-ORA-9B with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "OraRL/Video-ORA-9B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OraRL/Video-ORA-9B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "OraRL/Video-ORA-9B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OraRL/Video-ORA-9B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use OraRL/Video-ORA-9B with Docker Model Runner:
docker model run hf.co/OraRL/Video-ORA-9B
| # Copyright 2024 The Qwen team, Alibaba Group and the HuggingFace Inc. team | |
| # Copyright 2024 Bytedance Ltd. and/or its affiliates | |
| # Based on: | |
| # https://github.com/huggingface/transformers/blob/v5.3.0/src/transformers/models/qwen3_5/modeling_qwen3_5.py | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| import itertools | |
| from typing import Optional | |
| import torch | |
| from transformers.models.qwen3_5.modeling_qwen3_5 import ( | |
| Qwen3_5CausalLMOutputWithPast, | |
| Qwen3_5ForConditionalGeneration, | |
| Qwen3_5Model, | |
| Qwen3_5ModelOutputWithPast, | |
| ) | |
| def get_rope_index( | |
| processor, | |
| input_ids: torch.Tensor, | |
| image_grid_thw: Optional[torch.Tensor] = None, | |
| video_grid_thw: Optional[torch.Tensor] = None, | |
| attention_mask: Optional[torch.Tensor] = None, | |
| **kwargs, | |
| ) -> torch.Tensor: | |
| """ | |
| Gets the position ids for Qwen3.5, it should be generated before sharding the sequence. | |
| The batch dim has been removed and the input_ids should be a 1D tensor representing a single example. | |
| Qwen3.5 uses mm_token_type_ids to distinguish modalities (0=text, 1=image, 2=video) | |
| and interleaved MRoPE with sections=[11,11,10]. | |
| Returns position_ids of shape (3, seq_length) — [temporal, height, width]. | |
| """ | |
| spatial_merge_size = processor.image_processor.merge_size | |
| image_token_id = processor.image_token_id | |
| video_token_id = processor.video_token_id | |
| # Since we use timestamps to separate videos, | |
| # like <t1> <vision_start> <frame1> <vision_end> <t2> <vision_start> <frame2> <vision_end>, | |
| # the video_grid_thw should also be split | |
| if video_grid_thw is not None: | |
| video_grid_thw = torch.repeat_interleave(video_grid_thw, video_grid_thw[:, 0], dim=0) | |
| video_grid_thw[:, 0] = 1 | |
| if input_ids is not None and (image_grid_thw is not None or video_grid_thw is not None): | |
| if attention_mask is None: | |
| attention_mask = torch.ones_like(input_ids) | |
| position_ids = torch.ones(3, input_ids.shape[0], dtype=input_ids.dtype, device=input_ids.device) | |
| attention_mask = attention_mask.to(input_ids.device) | |
| input_ids_masked = input_ids[attention_mask == 1] | |
| # Build mm_token_type_ids: 0=text, 1=image, 2=video | |
| mm_token_type_ids = torch.zeros_like(input_ids_masked) | |
| mm_token_type_ids[input_ids_masked == image_token_id] = 1 | |
| mm_token_type_ids[input_ids_masked == video_token_id] = 2 | |
| # Group tokens by modality type using itertools.groupby | |
| image_iter = iter(image_grid_thw) if image_grid_thw is not None else iter([]) | |
| video_iter = iter(video_grid_thw) if video_grid_thw is not None else iter([]) | |
| input_type_group = [] | |
| for key, group in itertools.groupby(enumerate(mm_token_type_ids.tolist()), lambda x: x[1]): | |
| group = list(group) | |
| start_index = group[0][0] | |
| end_index = group[-1][0] + 1 | |
| input_type_group.append((key, start_index, end_index)) | |
| current_pos = 0 | |
| llm_pos_ids_list = [] | |
| for modality_type, start_idx, end_idx in input_type_group: | |
| if modality_type == 0: | |
| # Text tokens: standard 1D position IDs expanded to 3D | |
| text_len = end_idx - start_idx | |
| llm_pos_ids_list.append( | |
| torch.arange(text_len, device=input_ids.device).view(1, -1).expand(3, -1) + current_pos | |
| ) | |
| current_pos += text_len | |
| else: | |
| # Vision tokens (image=1, video=2): compute 3D position IDs | |
| if modality_type == 1: | |
| grid_thw = next(image_iter) | |
| else: | |
| grid_thw = next(video_iter) | |
| # Compute vision position IDs (same logic as Qwen3_5Model.get_vision_position_ids) | |
| llm_grid_t = grid_thw[0].item() // 1 # temp_merge_size=1 | |
| llm_grid_h = grid_thw[1].item() // spatial_merge_size | |
| llm_grid_w = grid_thw[2].item() // spatial_merge_size | |
| image_seq_length = llm_grid_h * llm_grid_w * llm_grid_t | |
| position_width = torch.arange( | |
| current_pos, current_pos + llm_grid_w, device=input_ids.device | |
| ).repeat(llm_grid_h * llm_grid_t) | |
| position_height = torch.arange( | |
| current_pos, current_pos + llm_grid_h, device=input_ids.device | |
| ).repeat_interleave(llm_grid_w * llm_grid_t) | |
| position_temporal = torch.full( | |
| (image_seq_length,), current_pos, device=input_ids.device, dtype=torch.long | |
| ) | |
| vision_position_ids = torch.stack([position_temporal, position_height, position_width], dim=0) | |
| llm_pos_ids_list.append(vision_position_ids) | |
| # Advance current_pos by max(h, w) // spatial_merge_size (Qwen3.5 convention) | |
| current_pos += max(grid_thw[1].item(), grid_thw[2].item()) // spatial_merge_size | |
| llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1) | |
| position_ids[..., attention_mask == 1] = llm_positions.to(position_ids.device) | |
| else: | |
| if attention_mask is not None: | |
| position_ids = attention_mask.long().cumsum(-1) - 1 | |
| position_ids.masked_fill_(attention_mask == 0, 1) | |
| position_ids = position_ids.unsqueeze(0).expand(3, -1).to(attention_mask.device) | |
| else: | |
| position_ids = torch.arange(input_ids.shape[0], device=input_ids.device).view(1, -1).expand(3, -1) | |
| return position_ids | |
| def _get_input_embeds( | |
| model: "Qwen3_5Model", | |
| input_ids: torch.LongTensor, | |
| attention_mask: Optional[torch.Tensor] = None, | |
| pixel_values: Optional[torch.FloatTensor] = None, | |
| pixel_values_videos: Optional[torch.FloatTensor] = None, | |
| image_grid_thw: Optional[torch.LongTensor] = None, | |
| video_grid_thw: Optional[torch.LongTensor] = None, | |
| ): | |
| """ | |
| Compute input embeddings with visual features injected for Qwen3.5. | |
| Qwen3.5 uses model.visual (Qwen3_5VisionModel) which returns | |
| BaseModelOutputWithPooling with pooler_output containing split image embeds. | |
| No deepstack_visual_indexes in Qwen3.5-2B (empty list). | |
| """ | |
| inputs_embeds = model.get_input_embeddings()(input_ids) | |
| if pixel_values is not None: | |
| pixel_values = pixel_values.type(model.visual.dtype) | |
| image_outputs = model.visual(pixel_values, grid_thw=image_grid_thw, return_dict=True) | |
| image_embeds = image_outputs.pooler_output | |
| # pooler_output is already split by image; cat them back | |
| split_sizes = (image_grid_thw.prod(-1) // model.visual.spatial_merge_size**2).tolist() | |
| image_embeds = torch.split(image_embeds, split_sizes) | |
| image_embeds = torch.cat(image_embeds, dim=0) | |
| n_image_tokens = (input_ids == model.config.image_token_id).sum().item() | |
| n_image_features = image_embeds.shape[0] | |
| if n_image_tokens != n_image_features: | |
| raise ValueError( | |
| f"Image features and image tokens do not match: tokens: {n_image_tokens}, features {n_image_features}" | |
| ) | |
| mask = input_ids == model.config.image_token_id | |
| mask_unsqueezed = mask.unsqueeze(-1) | |
| mask_expanded = mask_unsqueezed.expand_as(inputs_embeds) | |
| image_mask = mask_expanded.to(inputs_embeds.device) | |
| image_embeds = image_embeds.to(inputs_embeds.device, inputs_embeds.dtype) | |
| inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds) | |
| else: | |
| image_mask = None | |
| if pixel_values_videos is not None: | |
| pixel_values_videos = pixel_values_videos.type(model.visual.dtype) | |
| video_outputs = model.visual(pixel_values_videos, grid_thw=video_grid_thw, return_dict=True) | |
| video_embeds = video_outputs.pooler_output | |
| split_sizes = (video_grid_thw.prod(-1) // model.visual.spatial_merge_size**2).tolist() | |
| video_embeds = torch.split(video_embeds, split_sizes) | |
| video_embeds = torch.cat(video_embeds, dim=0) | |
| n_video_tokens = (input_ids == model.config.video_token_id).sum().item() | |
| n_video_features = video_embeds.shape[0] | |
| if n_video_tokens != n_video_features: | |
| raise ValueError( | |
| f"Video features and video tokens do not match: tokens: {n_video_tokens}, features {n_video_features}" | |
| ) | |
| mask = input_ids == model.config.video_token_id | |
| mask_unsqueezed = mask.unsqueeze(-1) | |
| mask_expanded = mask_unsqueezed.expand_as(inputs_embeds) | |
| video_mask = mask_expanded.to(inputs_embeds.device) | |
| video_embeds = video_embeds.to(inputs_embeds.device, inputs_embeds.dtype) | |
| inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds) | |
| else: | |
| video_mask = None | |
| # Dummy gradient flow: when neither images nor videos are present, | |
| # run a dummy forward through the visual encoder to maintain gradient flow | |
| if pixel_values is None and pixel_values_videos is None: | |
| config = model.config.vision_config | |
| patch_dim = config.in_channels * config.temporal_patch_size * config.patch_size**2 | |
| dummy_pixels = torch.zeros((16, patch_dim), dtype=inputs_embeds.dtype, device=inputs_embeds.device) | |
| dummy_grid_thw = torch.tensor([[1, 4, 4]], dtype=torch.long, device=inputs_embeds.device) | |
| dummy_outputs = model.visual(dummy_pixels, grid_thw=dummy_grid_thw, return_dict=True) | |
| dummy_embeds = dummy_outputs.pooler_output | |
| if isinstance(dummy_embeds, (list, tuple)): | |
| for emb in dummy_embeds: | |
| inputs_embeds += 0.0 * emb.mean() | |
| else: | |
| inputs_embeds += 0.0 * dummy_embeds.mean() | |
| # Dummy gradient flow for image-video mixed training: | |
| # when only images present, create dummy video input | |
| if pixel_values is not None and pixel_values_videos is None: | |
| config = model.config.vision_config | |
| patch_dim = config.in_channels * config.temporal_patch_size * config.patch_size**2 | |
| _video_grid_thw = video_grid_thw if (video_grid_thw is not None) \ | |
| else torch.tensor([[2, 4, 4]], dtype=torch.long, device=inputs_embeds.device) | |
| _T, _H, _W = _video_grid_thw[0].tolist() | |
| _n_tokens = int(_T * _H * _W) | |
| _dummy_video_pixels = torch.zeros((_n_tokens, patch_dim), | |
| dtype=inputs_embeds.dtype, device=inputs_embeds.device) | |
| _video_outputs = model.visual(_dummy_video_pixels, grid_thw=_video_grid_thw, return_dict=True) | |
| _video_embeds = _video_outputs.pooler_output | |
| if isinstance(_video_embeds, (list, tuple)): | |
| for emb in _video_embeds: | |
| inputs_embeds = inputs_embeds + 0.0 * emb.mean() | |
| else: | |
| inputs_embeds = inputs_embeds + 0.0 * _video_embeds.mean() | |
| # Dummy gradient flow: when only videos present, create dummy image input | |
| if pixel_values is None and pixel_values_videos is not None: | |
| config = model.config.vision_config | |
| patch_dim = config.in_channels * config.temporal_patch_size * config.patch_size**2 | |
| _image_grid_thw = image_grid_thw if (image_grid_thw is not None) \ | |
| else torch.tensor([[1, 4, 4]], dtype=torch.long, device=inputs_embeds.device) | |
| _T, _H, _W = _image_grid_thw[0].tolist() | |
| _n_tokens = int(_T * _H * _W) | |
| _dummy_image_pixels = torch.zeros((_n_tokens, patch_dim), | |
| dtype=inputs_embeds.dtype, device=inputs_embeds.device) | |
| _image_outputs = model.visual(_dummy_image_pixels, grid_thw=_image_grid_thw, return_dict=True) | |
| _image_embeds = _image_outputs.pooler_output | |
| if isinstance(_image_embeds, (list, tuple)): | |
| for emb in _image_embeds: | |
| inputs_embeds = inputs_embeds + 0.0 * emb.mean() | |
| else: | |
| inputs_embeds = inputs_embeds + 0.0 * _image_embeds.mean() | |
| if attention_mask is not None: | |
| attention_mask = attention_mask.to(inputs_embeds.device) | |
| return { | |
| "inputs_embeds": inputs_embeds, | |
| "attention_mask": attention_mask, | |
| } | |
| def qwen3_5_base_forward( | |
| self: "Qwen3_5Model", | |
| input_ids: torch.LongTensor, | |
| attention_mask: Optional[torch.Tensor] = None, | |
| pixel_values: Optional[torch.FloatTensor] = None, | |
| pixel_values_videos: Optional[torch.FloatTensor] = None, | |
| image_grid_thw: Optional[torch.LongTensor] = None, | |
| video_grid_thw: Optional[torch.LongTensor] = None, | |
| **kwargs, | |
| ): | |
| """ | |
| Override Qwen3_5Model.forward for RL training with padding-free sequence packing. | |
| This replaces the original forward to: | |
| 1. Handle visual embedding injection with dummy gradient flow | |
| 2. Support the verl padding-free training pipeline | |
| """ | |
| position_ids = kwargs.get("position_ids") | |
| if isinstance(position_ids, torch.Tensor) and position_ids.ndim not in (2, 3): | |
| raise ValueError("position_ids should have shape (batch_size, seq_length) or (3, batch_size, seq_length).") | |
| input_kwargs = _get_input_embeds( | |
| self, | |
| input_ids, | |
| attention_mask, | |
| pixel_values, | |
| pixel_values_videos, | |
| image_grid_thw, | |
| video_grid_thw, | |
| ) | |
| kwargs.update(input_kwargs) # avoid lora module to have multiple keyword arguments | |
| outputs = self.language_model(input_ids=None, **kwargs) | |
| return Qwen3_5ModelOutputWithPast(last_hidden_state=outputs.last_hidden_state) | |
| def qwen3_5_model_forward( | |
| self: "Qwen3_5ForConditionalGeneration", | |
| input_ids: torch.LongTensor, | |
| labels: Optional[torch.LongTensor] = None, | |
| **kwargs, | |
| ) -> "Qwen3_5CausalLMOutputWithPast": | |
| """ | |
| Override Qwen3_5ForConditionalGeneration.forward for RL training. | |
| Calls self.model (Qwen3_5Model) then self.lm_head to get logits. | |
| """ | |
| outputs = self.model(input_ids=input_ids, **kwargs) | |
| hidden_states = outputs[0] | |
| logits = self.lm_head(hidden_states) | |
| return Qwen3_5CausalLMOutputWithPast(logits=logits) | |