Image-Text-to-Text
Transformers
Safetensors
qwen3_vl
vision-language
image-classification
calibrated-probabilities
structured-outputs
typed-questions
jev-inspired
qwen3-vl
conversational
Instructions to use MeerDevelopment/Qevi-2B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use MeerDevelopment/Qevi-2B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="MeerDevelopment/Qevi-2B") 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("MeerDevelopment/Qevi-2B") model = AutoModelForMultimodalLM.from_pretrained("MeerDevelopment/Qevi-2B", 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 MeerDevelopment/Qevi-2B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "MeerDevelopment/Qevi-2B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "MeerDevelopment/Qevi-2B", "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/MeerDevelopment/Qevi-2B
- SGLang
How to use MeerDevelopment/Qevi-2B 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 "MeerDevelopment/Qevi-2B" \ --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": "MeerDevelopment/Qevi-2B", "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 "MeerDevelopment/Qevi-2B" \ --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": "MeerDevelopment/Qevi-2B", "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 MeerDevelopment/Qevi-2B with Docker Model Runner:
docker model run hf.co/MeerDevelopment/Qevi-2B
Bundle the qevi inference engine in-repo
Browse files- qevi/pack.py +173 -0
qevi/pack.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Packed multi-question sequence construction for the Qevi architecture spike.
|
| 2 |
+
|
| 3 |
+
Builds, from an image token count and a list of typed questions, the packed sequence layout
|
| 4 |
+
specified in plan/02-ARCHITECTURE.md section 3: one image prefix followed by one context plus
|
| 5 |
+
readout segment group per question, under a block-diagonal attention mask so that questions
|
| 6 |
+
cannot see each other and options within one question cannot see each other.
|
| 7 |
+
|
| 8 |
+
Model-agnostic. Produces a 4D additive attention bias and a position id tensor that any
|
| 9 |
+
transformers causal LM accepts as ordinary keyword arguments.
|
| 10 |
+
|
| 11 |
+
SCOPE LIMIT, current as of 19 September 2026: position_ids here is a single scalar stream,
|
| 12 |
+
correct for plain 1D-RoPE decoders. Qwen3-VL-2B-Instruct, the settled primary base model, uses
|
| 13 |
+
3D multimodal RoPE (three streams: temporal, height, width; see
|
| 14 |
+
research/18-qwen3-vl-2b-verification.md section 6). For text segments this scalar stream
|
| 15 |
+
transfers directly, since Qwen3-VL gives all three axes the identical value for text tokens; a
|
| 16 |
+
caller targeting Qwen3-VL broadcasts this module's scalar position_ids across all three axes for
|
| 17 |
+
text positions. For the image segment itself, this module's arange(image_len) placeholder does
|
| 18 |
+
NOT match Qwen3-VL's real 2D grid convention and must be replaced with the model's own
|
| 19 |
+
get_vision_position_ids output before a real forward pass on that model. Not yet done here.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from dataclasses import dataclass, field
|
| 23 |
+
from typing import Literal
|
| 24 |
+
|
| 25 |
+
import torch
|
| 26 |
+
|
| 27 |
+
NEG_INF = torch.finfo(torch.float32).min
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class Segment:
|
| 32 |
+
name: str
|
| 33 |
+
start: int
|
| 34 |
+
end: int # exclusive
|
| 35 |
+
kind: Literal["image", "context", "readout"]
|
| 36 |
+
question_idx: int | None = None
|
| 37 |
+
readout_idx: int | None = None
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def length(self) -> int:
|
| 41 |
+
return self.end - self.start
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def sentinel_pos(self) -> int:
|
| 45 |
+
"""Absolute index of the last token in this segment, where the typed head reads."""
|
| 46 |
+
return self.end - 1
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@dataclass
|
| 50 |
+
class Question:
|
| 51 |
+
qtype: Literal["noul", "choice", "score"]
|
| 52 |
+
context_len: int
|
| 53 |
+
readout_lens: list[int] = field(default_factory=lambda: [1])
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@dataclass
|
| 57 |
+
class PackedBatch:
|
| 58 |
+
seq_len: int
|
| 59 |
+
segments: list[Segment]
|
| 60 |
+
attn_bias: torch.Tensor # (1, 1, seq_len, seq_len) additive, 0 = allowed, NEG_INF = blocked
|
| 61 |
+
position_ids: torch.Tensor # (1, seq_len)
|
| 62 |
+
sentinel_positions: dict # (question_idx, readout_idx) -> absolute position
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _build_segments(image_len: int, questions: list[Question]) -> list[Segment]:
|
| 66 |
+
segments = [Segment("V", 0, image_len, kind="image")]
|
| 67 |
+
cursor = image_len
|
| 68 |
+
for qi, q in enumerate(questions):
|
| 69 |
+
c_start = cursor
|
| 70 |
+
c_end = c_start + q.context_len
|
| 71 |
+
segments.append(Segment(f"C{qi}", c_start, c_end, kind="context", question_idx=qi))
|
| 72 |
+
cursor = c_end
|
| 73 |
+
for ri, rlen in enumerate(q.readout_lens):
|
| 74 |
+
r_start = cursor
|
| 75 |
+
r_end = r_start + rlen
|
| 76 |
+
segments.append(
|
| 77 |
+
Segment(f"R{qi}_{ri}", r_start, r_end, kind="readout", question_idx=qi, readout_idx=ri)
|
| 78 |
+
)
|
| 79 |
+
cursor = r_end
|
| 80 |
+
return segments
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _group_key(seg: Segment):
|
| 84 |
+
"""Two positions may attend to each other (subject to causal order) only if they share a
|
| 85 |
+
group. A readout's group includes its own context segment, which is why a readout sees all
|
| 86 |
+
of C_i without needing a separate 'full attention to context' rule: C_i is already earlier
|
| 87 |
+
in sequence order, so ordinary intra-group causal masking gives it full visibility."""
|
| 88 |
+
if seg.kind == "image":
|
| 89 |
+
return ("V",)
|
| 90 |
+
if seg.kind == "context":
|
| 91 |
+
return ("Q", seg.question_idx)
|
| 92 |
+
return ("Q", seg.question_idx, seg.readout_idx)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _segment_at(segments: list[Segment], pos: int) -> Segment:
|
| 96 |
+
for seg in segments:
|
| 97 |
+
if seg.start <= pos < seg.end:
|
| 98 |
+
return seg
|
| 99 |
+
raise IndexError(pos)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def build_packed_batch(
|
| 103 |
+
image_len: int,
|
| 104 |
+
questions: list[Question],
|
| 105 |
+
position_scheme: Literal["reset", "monotonic"] = "reset",
|
| 106 |
+
text_start_offset: int | None = None,
|
| 107 |
+
) -> PackedBatch:
|
| 108 |
+
"""text_start_offset is the position value each question's context segment resets to under
|
| 109 |
+
the 'reset' scheme. Defaults to image_len, correct for a plain 1D-RoPE decoder where the
|
| 110 |
+
image occupies image_len sequential positions. It is WRONG for M-RoPE models such as
|
| 111 |
+
Qwen3-VL, whose own convention resumes text at max(grid_h, grid_w) // spatial_merge_size,
|
| 112 |
+
not the raw image token count; see research/18-qwen3-vl-2b-verification.md section 6. Callers
|
| 113 |
+
targeting such a model must compute the correct value themselves (typically via the model's
|
| 114 |
+
own get_vision_position_ids or equivalent) and pass it here explicitly."""
|
| 115 |
+
if text_start_offset is None:
|
| 116 |
+
text_start_offset = image_len
|
| 117 |
+
segments = _build_segments(image_len, questions)
|
| 118 |
+
seq_len = segments[-1].end
|
| 119 |
+
|
| 120 |
+
bias = torch.full((seq_len, seq_len), NEG_INF, dtype=torch.float32)
|
| 121 |
+
for i in range(seq_len):
|
| 122 |
+
seg_i = _segment_at(segments, i)
|
| 123 |
+
group_i = _group_key(seg_i)
|
| 124 |
+
for seg_j in segments:
|
| 125 |
+
group_j = _group_key(seg_j)
|
| 126 |
+
same_group = group_i == group_j
|
| 127 |
+
is_image = seg_j.kind == "image"
|
| 128 |
+
# A readout's group is (Q, qi, ri); its context segment's group is (Q, qi).
|
| 129 |
+
# Give the readout visibility into its own context by matching on question index.
|
| 130 |
+
same_question_context = (
|
| 131 |
+
seg_i.kind == "readout" and seg_j.kind == "context" and seg_i.question_idx == seg_j.question_idx
|
| 132 |
+
)
|
| 133 |
+
if not (same_group or is_image or same_question_context):
|
| 134 |
+
continue
|
| 135 |
+
j_start = seg_j.start
|
| 136 |
+
j_end = min(seg_j.end, i + 1) # causal: never attend beyond position i
|
| 137 |
+
if j_end > j_start:
|
| 138 |
+
bias[i, j_start:j_end] = 0.0
|
| 139 |
+
|
| 140 |
+
attn_bias = bias.unsqueeze(0).unsqueeze(0) # (1, 1, seq_len, seq_len)
|
| 141 |
+
|
| 142 |
+
position_ids = torch.zeros(seq_len, dtype=torch.long)
|
| 143 |
+
if position_scheme == "monotonic":
|
| 144 |
+
position_ids[:] = torch.arange(seq_len)
|
| 145 |
+
else:
|
| 146 |
+
position_ids[: image_len] = torch.arange(image_len)
|
| 147 |
+
for qi, q in enumerate(questions):
|
| 148 |
+
c_seg = next(s for s in segments if s.kind == "context" and s.question_idx == qi)
|
| 149 |
+
local_len = c_seg.length
|
| 150 |
+
position_ids[c_seg.start : c_seg.end] = text_start_offset + torch.arange(local_len)
|
| 151 |
+
cursor = text_start_offset + local_len
|
| 152 |
+
for ri, seg in enumerate(s for s in segments if s.kind == "readout" and s.question_idx == qi):
|
| 153 |
+
rlen = seg.length
|
| 154 |
+
position_ids[seg.start : seg.end] = cursor + torch.arange(rlen)
|
| 155 |
+
cursor += rlen
|
| 156 |
+
|
| 157 |
+
sentinel_positions = {
|
| 158 |
+
(seg.question_idx, seg.readout_idx): seg.sentinel_pos
|
| 159 |
+
for seg in segments
|
| 160 |
+
if seg.kind == "readout"
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
return PackedBatch(
|
| 164 |
+
seq_len=seq_len,
|
| 165 |
+
segments=segments,
|
| 166 |
+
attn_bias=attn_bias,
|
| 167 |
+
position_ids=position_ids.unsqueeze(0),
|
| 168 |
+
sentinel_positions=sentinel_positions,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def permute_questions(questions: list[Question], order: list[int]) -> list[Question]:
|
| 173 |
+
return [questions[i] for i in order]
|