Image-Text-to-Text
Transformers
Safetensors
qwen3_5
decision-model
typed-decisions
multimodal
conversational
Instructions to use inclusionAI/SingVL_S1_4B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use inclusionAI/SingVL_S1_4B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="inclusionAI/SingVL_S1_4B") 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)# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("inclusionAI/SingVL_S1_4B") model = AutoModelForMultimodalLM.from_pretrained("inclusionAI/SingVL_S1_4B", 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=256) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use inclusionAI/SingVL_S1_4B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "inclusionAI/SingVL_S1_4B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "inclusionAI/SingVL_S1_4B", "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/inclusionAI/SingVL_S1_4B
- SGLang
How to use inclusionAI/SingVL_S1_4B 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 "inclusionAI/SingVL_S1_4B" \ --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": "inclusionAI/SingVL_S1_4B", "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 "inclusionAI/SingVL_S1_4B" \ --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": "inclusionAI/SingVL_S1_4B", "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 inclusionAI/SingVL_S1_4B with Docker Model Runner:
docker model run hf.co/inclusionAI/SingVL_S1_4B
Download inference/model.py from inclusionAI/SingVL_S1_4B: direct link, hf CLI and curl.
- Browser
- Download file 13 kB
-
https://huggingface.co/inclusionAI/SingVL_S1_4B/resolve/main/inference/model.py
- Command line
-
hf download hf://inclusionAI/SingVL_S1_4B/inference/model.py
-
curl -L -o model.py https://huggingface.co/inclusionAI/SingVL_S1_4B/resolve/main/inference/model.py
13 kB
| """Native Transformers readout for the Sing VL S1 checkpoint.""" | |
| import hashlib | |
| import json | |
| import math | |
| import re | |
| from pathlib import Path | |
| from PIL import Image | |
| import torch | |
| import transformers | |
| from transformers import AutoProcessor, Qwen3_5ForConditionalGeneration | |
| MODEL_ID = "sing-vl-s1" | |
| TEXT_MAX_TOKENS = 65_536 | |
| VISION_MAX_TOKENS = 10_000 | |
| PUBLIC_IMAGE_MAX_PIXELS = 1_638_400 | |
| INSTRUCTION = "Solve the multiple-choice question and output ONLY the index token of the correct option.\n\n" | |
| ANSWER_PREFIX = "<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n" | |
| IMAGE_SLOT = "<|vision_start|><|image_pad|><|vision_end|>" | |
| TRAILING_IMAGES = re.compile(r"(?:\s*<image>\s*)+$") | |
| CONTROL_MARKER = re.compile(r"<video>|<\|(?:im_|vision_|image_pad|video_pad)") | |
| class Unsupported(ValueError): | |
| pass | |
| def as_text(value): | |
| return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, separators=(",", ":")) | |
| def questions_for_state(state, questions, image_count): | |
| if not isinstance(questions, dict) or not questions: | |
| raise ValueError("questions must be a nonempty object") | |
| state = "" if image_count and state == {} else as_text(state) | |
| if CONTROL_MARKER.search(state): | |
| raise Unsupported("state contains a reserved model-control marker") | |
| if image_count and "<image>" not in state: | |
| state = "<image>\n" * image_count + state | |
| if state.count("<image>") != image_count: | |
| raise Unsupported("the number of <image> placeholders must match the images") | |
| for question_id, question in questions.items(): | |
| if not isinstance(question, dict): | |
| raise ValueError("each question must be an object") | |
| kind = question.get("type") | |
| if kind not in ("choice", "noul", "score"): | |
| raise Unsupported("supported question types are choice, noul and score") | |
| criteria = question.get("criteria") | |
| if kind == "score": | |
| if not isinstance(criteria, list) or not 2 <= len(criteria) <= 10: | |
| raise Unsupported("score criteria must be an ordered array of 2..10 levels") | |
| criteria = {str(level): description for level, description in enumerate(criteria)} | |
| if kind == "noul" and (criteria is None or criteria == {}): | |
| criteria = {"true": "Yes", "false": "No"} | |
| if not isinstance(criteria, dict) or not 2 <= len(criteria) <= 256: | |
| raise Unsupported("supports 2..256 options per choice") | |
| if kind == "noul" and set(criteria) != {"true", "false"}: | |
| raise Unsupported("noul criteria must be true/false") | |
| instructions = as_text(question.get("instructions", "")) | |
| options = {key: key if value is None else as_text(value) for key, value in criteria.items()} | |
| if CONTROL_MARKER.search(instructions) or any(CONTROL_MARKER.search(value) for value in options.values()): | |
| raise Unsupported("question contains a reserved model-control marker") | |
| if "<image>" in instructions: | |
| raise Unsupported("place image markers in state") | |
| yield question_id, kind, state, instructions, options | |
| def reference_images(state, options): | |
| """Resolve option references to images already present in the state.""" | |
| for key, value in options.items(): | |
| if "<image>" not in value: | |
| continue | |
| needle = f"({key}) {value}" | |
| if state.count(needle) != 1: | |
| raise Unsupported(f"ambiguous image reference for option {key}") | |
| offset = state.index(needle) + len(f"({key}) ") | |
| index = state[:offset].count("<image>") + 1 | |
| references = iter(range(index, index + value.count("<image>"))) | |
| options[key] = re.sub(r"<image>", lambda _: f"[Image {next(references)} above]", value) | |
| indices = iter(range(1, state.count("<image>") + 1)) | |
| return re.sub(r"<image>", lambda _: f"[Image {next(indices)}] <image>", state) | |
| def render(state, instructions, options, labels): | |
| if any("<image>" in value for value in options.values()): | |
| state = reference_images(state, options) | |
| question = state + instructions | |
| trailing = TRAILING_IMAGES.search(question) | |
| if trailing and question[:trailing.start()].rstrip(): | |
| question = trailing.group().strip() + "\n" + question[:trailing.start()].rstrip() | |
| question = question.rstrip() | |
| if question.endswith("Choices:"): | |
| question = question[:-len("Choices:")].rstrip() | |
| lines = "\n".join(f"{label}: {value}" for label, value in zip(labels, options.values())) | |
| user = (INSTRUCTION + "### Question:\n" + question + "\n\n### Options:\n" + lines).strip() | |
| return "<|im_start|>user\n" + user.replace("<image>", IMAGE_SLOT) + ANSWER_PREFIX | |
| def resize_image(image, factor, min_pixels, max_pixels): | |
| if image.mode == "RGBA": | |
| rgb = Image.new("RGB", image.size, "white") | |
| rgb.paste(image, mask=image.getchannel("A")) | |
| else: | |
| rgb = image.convert("RGB") | |
| width, height = rgb.size | |
| if max(width, height) / min(width, height) > 200: | |
| raise Unsupported("image aspect ratio exceeds 200") | |
| w, h = (max(factor, round(d / factor) * factor) for d in (width, height)) | |
| if w * h > max_pixels: | |
| scale = math.sqrt(max_pixels / (width * height)) | |
| w, h = (max(factor, math.floor(d * scale / factor) * factor) for d in (width, height)) | |
| elif w * h < min_pixels: | |
| scale = math.sqrt(min_pixels / (width * height)) | |
| w, h = (math.ceil(d * scale / factor) * factor for d in (width, height)) | |
| return rgb.resize((w, h), Image.Resampling.BICUBIC) | |
| def bidirectional_prompt(_module, args, kwargs): | |
| """Supply the native Qwen3.5 per-layer masks for one complete prompt.""" | |
| attention = kwargs["attention_mask"] | |
| if (kwargs.get("use_cache") or kwargs.get("past_key_values") is not None | |
| or attention is None or attention.ndim != 2 or attention.shape[0] != 1 | |
| or not attention.bool().all().item()): | |
| raise ValueError("Sing VL S1 expects a single unpadded prompt without a cache") | |
| kwargs["attention_mask"] = {"full_attention": None, "linear_attention": None} | |
| return args, kwargs | |
| class SingVLS1: | |
| def __init__(self, model, device="cuda:0", image_max_pixels=PUBLIC_IMAGE_MAX_PIXELS): | |
| self.model_path = str(Path(model).resolve()) | |
| self.device = torch.device(device) | |
| self.processor = AutoProcessor.from_pretrained(self.model_path, local_files_only=True) | |
| self.tokenizer = self.processor.tokenizer | |
| self.model = Qwen3_5ForConditionalGeneration.from_pretrained( | |
| self.model_path, local_files_only=True, dtype=torch.bfloat16, attn_implementation="sdpa", | |
| ).to(self.device).eval() | |
| config = self.model.config | |
| if config.jev_prefix_attention_mode != "prefix_bidir" or config.jev_gdn_mode != "causal": | |
| raise ValueError("checkpoint does not declare the Sing VL S1 attention contract") | |
| readout = config.jev_soft_choice | |
| if readout["version"] != "soft-choice-full-vocab-v2" or readout["readout"] != "last-valid-input-position": | |
| raise ValueError("checkpoint does not declare the Sing VL S1 readout") | |
| self.choice_ids = readout["token_ids"] | |
| self.labels = [self.tokenizer.decode([idx], clean_up_tokenization_spaces=False) for idx in self.choice_ids] | |
| if (len(set(self.choice_ids)) != 256 or not all(re.fullmatch("[A-Z]{2}", label) for label in self.labels) | |
| or any(self.tokenizer.encode(label, add_special_tokens=False) != [idx] | |
| for idx, label in zip(self.choice_ids, self.labels))): | |
| raise ValueError("checkpoint's 256 choice tokens do not match the tokenizer") | |
| language = self.model.model.language_model | |
| for layer in language.layers: | |
| if hasattr(layer, "self_attn"): | |
| layer.self_attn.is_causal = False | |
| language.register_forward_pre_hook(bidirectional_prompt, with_kwargs=True) | |
| self.model.config.use_cache = language.config.use_cache = False | |
| self.image_id = config.image_token_id | |
| image_processor = self.processor.image_processor | |
| self.merge_size = image_processor.merge_size | |
| self.image_factor = image_processor.patch_size * self.merge_size | |
| self.min_pixels = int(config.jev_prefix_media["IMAGE_MIN_TOKEN_NUM"]) * self.image_factor ** 2 | |
| if image_max_pixels < self.min_pixels: | |
| raise ValueError("image_max_pixels is smaller than the checkpoint's minimum image area") | |
| self.image_max_pixels = int(image_max_pixels) | |
| self.max_text_tokens = min(TEXT_MAX_TOKENS, config.text_config.max_position_embeddings) | |
| def encode_images(self, images): | |
| if not images: | |
| return {} | |
| prepared = [resize_image(image, self.image_factor, self.min_pixels, self.image_max_pixels) for image in images] | |
| return dict(self.processor.image_processor(images=prepared, return_tensors="pt", do_resize=False)) | |
| def encode(self, prompt, image_features): | |
| ids = self.tokenizer.encode(prompt, add_special_tokens=False) | |
| if image_features: | |
| grid = image_features["image_grid_thw"] | |
| if ids.count(self.image_id) != len(grid): | |
| raise Unsupported("expanded media tokens do not match the images") | |
| counts = iter(grid.prod(dim=-1).tolist()) | |
| ids = [part for token in ids for part in ( | |
| [self.image_id] * (next(counts) // self.merge_size ** 2) if token == self.image_id else [token] | |
| )] | |
| limit = VISION_MAX_TOKENS if image_features else self.max_text_tokens | |
| if len(ids) > limit: | |
| raise Unsupported(f"maximum model length is {limit} tokens; got {len(ids)}") | |
| tokens = torch.tensor([ids], dtype=torch.long) | |
| batch = {"input_ids": tokens, "attention_mask": torch.ones_like(tokens), **image_features} | |
| if image_features: | |
| batch["mm_token_type_ids"] = (tokens == self.image_id).to(torch.int32) | |
| return batch, ids | |
| def predict(self, state, questions, images=()): | |
| if len(images) > 4: | |
| raise Unsupported("at most four images per request") | |
| requests = list(questions_for_state(state, questions, len(images))) | |
| image_features = self.encode_images(images) | |
| image_grid = image_features["image_grid_thw"].tolist() if image_features else None | |
| image_tokens = sum(math.prod(grid) // self.merge_size ** 2 for grid in image_grid or ()) | |
| answers, details = {}, {} | |
| for question_id, kind, state, instructions, options in requests: | |
| batch, ids = self.encode(render(state, instructions, options, self.labels), image_features) | |
| model_inputs = {name: value.to(self.device) for name, value in batch.items()} | |
| self.model.model.rope_deltas = None | |
| output = self.model(**model_inputs, use_cache=False, logits_to_keep=1) | |
| logits = output.logits[0, -1, self.choice_ids[:len(options)]].float() | |
| if not torch.isfinite(logits).all(): | |
| raise ValueError("nonfinite candidate logits") | |
| probabilities = dict(zip(options, logits.softmax(-1).tolist())) | |
| if kind == "noul": | |
| answers[question_id] = {"type": "noul", "noul": probabilities["true"]} | |
| elif kind == "score": | |
| total = math.fsum(probabilities.values()) | |
| probabilities = {level: probability / total for level, probability in probabilities.items()} | |
| answers[question_id] = { | |
| "type": "score", | |
| "score": math.fsum(int(level) * probability for level, probability in probabilities.items()), | |
| "probabilities": probabilities, | |
| "legend": dict(zip(probabilities, questions[question_id]["criteria"])), | |
| } | |
| else: | |
| answers[question_id] = { | |
| "type": "choice", "choice": max(probabilities, key=probabilities.get), | |
| "probabilities": probabilities, | |
| } | |
| details[question_id] = { | |
| "input_tokens": len(ids), | |
| "input_ids_sha256": hashlib.sha256(json.dumps([ids]).encode()).hexdigest(), | |
| "image_grid_thw": image_grid, | |
| "image_tokens": image_tokens, | |
| "candidate_logits": logits.tolist(), | |
| } | |
| usage = {"input_tokens": sum(item["input_tokens"] for item in details.values()), "output_tokens": 0} | |
| return {"model": MODEL_ID, "answers": answers, "usage": usage}, details | |
| def runtime(self): | |
| return { | |
| "torch": torch.__version__, "transformers": transformers.__version__, "device": str(self.device), | |
| "precision": "bfloat16", "attention": "bidirectional full attention, causal GDN, no KV cache", | |
| "image_max_pixels": self.image_max_pixels, "text_max_tokens": self.max_text_tokens, | |
| "vision_max_tokens": VISION_MAX_TOKENS, | |
| } | |