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 Bytedance Ltd. and/or its affiliates | |
| # | |
| # 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 argparse | |
| import os | |
| import re | |
| import shutil | |
| from concurrent.futures import ThreadPoolExecutor | |
| import numpy as np | |
| import torch | |
| from torch.distributed._tensor import DTensor, Placement, Shard | |
| from transformers import ( | |
| AutoConfig, | |
| AutoModelForCausalLM, | |
| AutoModelForImageTextToText, | |
| AutoModelForTokenClassification, | |
| PretrainedConfig, | |
| PreTrainedModel, | |
| ) | |
| def merge_by_placement(tensors: list[torch.Tensor], placement: Placement): | |
| if placement.is_replicate(): | |
| return tensors[0] | |
| elif placement.is_partial(): | |
| raise NotImplementedError("Partial placement is not supported yet") | |
| elif placement.is_shard(): | |
| return torch.cat(tensors, dim=placement.dim).contiguous() | |
| else: | |
| raise ValueError(f"Unsupported placement: {placement}") | |
| def upload_model_to_huggingface(local_path: str, remote_path: str): | |
| # Push to hugging face | |
| from huggingface_hub import HfApi | |
| api = HfApi() | |
| api.create_repo(repo_id=remote_path, private=False, exist_ok=True) | |
| api.upload_folder(repo_id=remote_path, folder_path=local_path, repo_type="model") | |
| def _load_missing_keys_from_base( | |
| base_model_path: str, | |
| missing_keys: set[str], | |
| ) -> dict[str, torch.Tensor]: | |
| """Stream the base model's safetensors shards and pick out only the | |
| parameters listed in ``missing_keys``. Used to backfill frozen modules | |
| (e.g. ``visual.*`` when freeze_vision_tower=True) that never end up in | |
| the FSDP per-rank state_dict.""" | |
| from safetensors.torch import load_file as _safe_load | |
| if not missing_keys: | |
| return {} | |
| shard_paths = sorted( | |
| os.path.join(base_model_path, name) | |
| for name in os.listdir(base_model_path) | |
| if name.endswith(".safetensors") | |
| ) | |
| if not shard_paths: | |
| raise FileNotFoundError( | |
| f"--base_model_path={base_model_path} has no .safetensors files; " | |
| "cannot backfill frozen weights." | |
| ) | |
| found: dict[str, torch.Tensor] = {} | |
| remaining = set(missing_keys) | |
| for path in shard_paths: | |
| if not remaining: | |
| break | |
| shard = _safe_load(path, device="cpu") | |
| hit = remaining & shard.keys() | |
| for k in hit: | |
| found[k] = shard[k].to(torch.bfloat16).contiguous() | |
| remaining -= hit | |
| del shard | |
| if remaining: | |
| print( | |
| f"[merger] WARNING: {len(remaining)} keys still missing after " | |
| f"scanning base model. First 5: {sorted(list(remaining))[:5]}" | |
| ) | |
| print(f"[merger] backfilled {len(found)} keys from base model.") | |
| return found | |
| def _copy_hf_metadata_from_base(base_model_path: str, hf_path: str) -> None: | |
| """Copy non-weight HF files needed to load config/tokenizer/processor. | |
| Some thinned FSDP checkpoints keep only ``model_world_size_*`` shards and may | |
| not preserve ``actor/huggingface/config.json``. The merger still needs those | |
| metadata files before it can instantiate the architecture and write the | |
| merged weights. | |
| """ | |
| os.makedirs(hf_path, exist_ok=True) | |
| for name in os.listdir(base_model_path): | |
| # Do not copy base weights or stale shard indexes into the output dir. | |
| if ( | |
| name.endswith((".safetensors", ".bin", ".pt")) | |
| or name.startswith("model-") | |
| or name == "model.safetensors.index.json" | |
| ): | |
| continue | |
| src = os.path.join(base_model_path, name) | |
| dst = os.path.join(hf_path, name) | |
| if os.path.isdir(src): | |
| shutil.copytree(src, dst, dirs_exist_ok=True) | |
| elif os.path.isfile(src): | |
| shutil.copy2(src, dst) | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--local_dir", required=True, type=str, help="The path for your saved model") | |
| parser.add_argument("--hf_upload_path", default=False, type=str, help="The path of the huggingface repo to upload") | |
| parser.add_argument( | |
| "--base_model_path", | |
| default=None, | |
| type=str, | |
| help="Optional path to the original HF base model. If provided, any " | |
| "architecture parameters missing from the FSDP shards (e.g. the " | |
| "vision tower when freeze_vision_tower=True) are pulled from here. " | |
| "If omitted, the merger auto-discovers it from " | |
| "<local_dir>/huggingface/config.json's `_name_or_path`.", | |
| ) | |
| args = parser.parse_args() | |
| local_dir: str = args.local_dir | |
| assert not local_dir.endswith("huggingface"), "The local_dir should not end with huggingface." | |
| # copy rank zero to find the shape of (dp, fsdp) | |
| rank = 0 | |
| world_size = 0 | |
| for filename in os.listdir(local_dir): | |
| match = re.match(r"model_world_size_(\d+)_rank_0\.pt", filename) | |
| if match: | |
| world_size = match.group(1) | |
| break | |
| assert world_size, "No model file with the proper format." | |
| rank0_weight_path = os.path.join(local_dir, f"model_world_size_{world_size}_rank_{rank}.pt") | |
| state_dict = torch.load(rank0_weight_path, map_location="cpu", weights_only=False) | |
| pivot_key = sorted(state_dict.keys())[0] | |
| weight = state_dict[pivot_key] | |
| if isinstance(weight, DTensor): | |
| # get sharding info | |
| device_mesh = weight.device_mesh | |
| mesh = device_mesh.mesh | |
| mesh_dim_names = device_mesh.mesh_dim_names | |
| else: | |
| # for non-DTensor | |
| mesh = np.array([int(world_size)], dtype=np.int64) | |
| mesh_dim_names = ("fsdp",) | |
| print(f"Got device mesh {mesh}, mesh_dim_names {mesh_dim_names}") | |
| assert mesh_dim_names in (("fsdp",), ("ddp", "fsdp")), f"Unsupported mesh_dim_names {mesh_dim_names}." | |
| if "tp" in mesh_dim_names: | |
| # fsdp * tp | |
| total_shards = mesh.shape[-1] * mesh.shape[-2] | |
| mesh_shape = (mesh.shape[-2], mesh.shape[-1]) | |
| else: | |
| # fsdp | |
| total_shards = mesh.shape[-1] | |
| mesh_shape = (mesh.shape[-1],) | |
| print(f"Processing {total_shards} model shards in total.") | |
| model_state_dict_lst = [] | |
| model_state_dict_lst.append(state_dict) | |
| model_state_dict_lst.extend([""] * (total_shards - 1)) | |
| def process_one_shard(rank, model_state_dict_lst): | |
| model_path = os.path.join(local_dir, f"model_world_size_{world_size}_rank_{rank}.pt") | |
| state_dict = torch.load(model_path, map_location="cpu", weights_only=False) | |
| model_state_dict_lst[rank] = state_dict | |
| return state_dict | |
| with ThreadPoolExecutor(max_workers=min(32, os.cpu_count())) as executor: | |
| for rank in range(1, total_shards): | |
| executor.submit(process_one_shard, rank, model_state_dict_lst) | |
| state_dict: dict[str, list[torch.Tensor]] = {} | |
| param_placements: dict[str, list[Placement]] = {} | |
| keys = set(model_state_dict_lst[0].keys()) | |
| for key in keys: | |
| state_dict[key] = [] | |
| for model_state_dict in model_state_dict_lst: | |
| try: | |
| tensor = model_state_dict.pop(key) | |
| except Exception: | |
| print(f"Cannot find key {key} in rank {rank}.") | |
| if isinstance(tensor, DTensor): | |
| state_dict[key].append(tensor._local_tensor.bfloat16()) | |
| placements = tuple(tensor.placements) | |
| # replicated placement at ddp dimension can be discarded | |
| if mesh_dim_names[0] == "ddp": | |
| placements = placements[1:] | |
| if key not in param_placements: | |
| param_placements[key] = placements | |
| else: | |
| assert param_placements[key] == placements | |
| else: | |
| state_dict[key].append(tensor.bfloat16()) | |
| del model_state_dict_lst | |
| for key in sorted(state_dict): | |
| if not isinstance(state_dict[key], list): | |
| print(f"No need to merge key {key}") | |
| continue | |
| if key in param_placements: | |
| # merge shards | |
| placements: tuple[Shard] = param_placements[key] | |
| if len(mesh_shape) == 1: | |
| # 1-D list, FSDP without TP | |
| assert len(placements) == 1 | |
| shards = state_dict[key] | |
| state_dict[key] = merge_by_placement(shards, placements[0]) | |
| else: | |
| # 2-D list, FSDP + TP | |
| raise NotImplementedError("FSDP + TP is not supported yet.") | |
| else: | |
| state_dict[key] = torch.cat(state_dict[key], dim=0) | |
| print("Merge completed.") | |
| hf_path = os.path.join(local_dir, "huggingface") | |
| if not os.path.isfile(os.path.join(hf_path, "config.json")): | |
| if args.base_model_path and os.path.isdir(args.base_model_path): | |
| print( | |
| f"[merger] {hf_path}/config.json missing; copying HF metadata " | |
| f"from base model {args.base_model_path}" | |
| ) | |
| _copy_hf_metadata_from_base(args.base_model_path, hf_path) | |
| else: | |
| raise FileNotFoundError( | |
| f"{hf_path}/config.json is missing. Pass " | |
| f"--base_model_path /path/to/base/hf/model so the merger can " | |
| f"copy config/tokenizer/processor metadata before loading." | |
| ) | |
| config: PretrainedConfig = AutoConfig.from_pretrained(hf_path) | |
| architectures: list[str] = getattr(config, "architectures", ["Unknown"]) | |
| if "ForTokenClassification" in architectures[0]: | |
| AutoClass = AutoModelForTokenClassification | |
| elif "ForConditionalGeneration" in architectures[0]: | |
| AutoClass = AutoModelForImageTextToText | |
| elif "ForCausalLM" in architectures[0]: | |
| AutoClass = AutoModelForCausalLM | |
| else: | |
| raise NotImplementedError(f"Unknown architecture {architectures}.") | |
| # Backfill from the base model any architecture keys that didn't make it | |
| # into the FSDP shards (typically the frozen vision tower when | |
| # freeze_vision_tower=True). We compute the architecture's expected key | |
| # set on a meta model (free), then pull only the diff from the base | |
| # safetensors. vLLM rejects checkpoints with uninitialized weights, so | |
| # this must run before save. | |
| with torch.device("meta"): | |
| meta_model: PreTrainedModel = AutoClass.from_config(config, torch_dtype=torch.bfloat16) | |
| arch_keys = set(meta_model.state_dict().keys()) | |
| del meta_model | |
| missing_keys = arch_keys - set(state_dict.keys()) | |
| if missing_keys: | |
| base_model_path = args.base_model_path | |
| if base_model_path is None: | |
| base_model_path = getattr(config, "_name_or_path", None) | |
| if not base_model_path or not os.path.isdir(base_model_path): | |
| raise RuntimeError( | |
| f"[merger] {len(missing_keys)} architecture keys are missing from " | |
| f"the FSDP shards (e.g. {sorted(missing_keys)[:3]}). " | |
| f"Pass --base_model_path /path/to/base/hf/model to backfill " | |
| f"them (or set _name_or_path in config.json to a valid base " | |
| f"model directory)." | |
| ) | |
| print( | |
| f"[merger] {len(missing_keys)} keys missing from FSDP shards; " | |
| f"backfilling from base model at {base_model_path}" | |
| ) | |
| backfill = _load_missing_keys_from_base(base_model_path, missing_keys) | |
| state_dict.update(backfill) | |
| del backfill | |
| # Sanity check: every key has a real (non-empty) tensor. | |
| extra_keys = set(state_dict.keys()) - arch_keys | |
| if extra_keys: | |
| print( | |
| f"[merger] dropping {len(extra_keys)} keys not declared by the " | |
| f"architecture (likely optimizer/training-only state). " | |
| f"Examples: {sorted(extra_keys)[:3]}" | |
| ) | |
| for k in extra_keys: | |
| state_dict.pop(k, None) | |
| # Write the safetensors file ourselves to avoid HF `save_pretrained`'s | |
| # implicit key remapping. On nested VL architectures (Qwen3.5-VL etc.) | |
| # save_pretrained(state_dict=…) silently renames `model.visual.*` to | |
| # `model.language_model.visual.*`, producing a file that vLLM refuses to | |
| # load. Writing the merged dict directly preserves the exact same key | |
| # layout as the base HF model, which vLLM already knows how to map. | |
| from safetensors.torch import save_file as _safe_save | |
| # save_pretrained would also have written config.json + generation_config | |
| # + processor + tokenizer, but those files are already present in | |
| # hf_path (verl copies them at training-init time), so we only need to | |
| # (re)write the weight file and refresh the safetensors index if any. | |
| hf_weights_path = os.path.join(hf_path, "model.safetensors") | |
| print(f"Writing merged weights to {hf_weights_path} ({len(state_dict)} keys)") | |
| # safetensors requires contiguous tensors. | |
| save_dict: dict[str, torch.Tensor] = {} | |
| for k, v in state_dict.items(): | |
| t = v if isinstance(v, torch.Tensor) else torch.as_tensor(v) | |
| if t.dtype != torch.bfloat16: | |
| t = t.to(torch.bfloat16) | |
| save_dict[k] = t.contiguous() | |
| _safe_save(save_dict, hf_weights_path, metadata={"format": "pt"}) | |
| del state_dict, save_dict | |
| # Drop any stale sharded index that would mislead the HF loader; the | |
| # weights now live in a single model.safetensors. | |
| index_path = os.path.join(hf_path, "model.safetensors.index.json") | |
| if os.path.exists(index_path): | |
| print(f"Removing stale {index_path}") | |
| os.remove(index_path) | |
| for stale_shard in os.listdir(hf_path): | |
| if stale_shard.startswith("model-") and stale_shard.endswith(".safetensors"): | |
| os.remove(os.path.join(hf_path, stale_shard)) | |
| # We no longer call save_pretrained, so generation_config.json / | |
| # tokenizer / processor configs are not touched and don't need a backup. | |
| if args.hf_upload_path: | |
| upload_model_to_huggingface(hf_path, args.hf_upload_path) | |