@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / c0817e4d39
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 5, 16:00 UTC

[Model] Add support for Cheers multimodal model (#38788)

Signed-off-by: bsliu <1187291748@qq.com> Signed-off-by: 吴炳贤 <wubingxian24@mails.ucas.ac.cn>

bsliu committed Apr 2, 2026 at 21:01 UTC c0817e4d39c78335952fb4b6bfae3cd3e45ac4c3
11 files changed +996
docs/models/supported_models.md
+1
@@ -541,6 +541,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
541 | `BeeForConditionalGeneration` | Bee-8B | T + I<sup>E+</sup> | `Open-Bee/Bee-8B-RL`, `Open-Bee/Bee-8B-SFT` | | ✅︎ |
542 | `Blip2ForConditionalGeneration` | BLIP-2 | T + I<sup>E</sup> | `Salesforce/blip2-opt-2.7b`, `Salesforce/blip2-opt-6.7b`, etc. | ✅︎ | ✅︎ |
543 | `ChameleonForConditionalGeneration` | Chameleon | T + I | `facebook/chameleon-7b`, etc. | | ✅︎ |
544 +| `CheersForConditionalGeneration` | Cheers | T + I | `ai9stars/Cheers` | | ✅︎ |
545 | `Cohere2VisionForConditionalGeneration` | Command A Vision | T + I<sup>+</sup> | `CohereLabs/command-a-vision-07-2025`, etc. | | ✅︎ |
546 | `DeepseekVLV2ForCausalLM` | DeepSeek-VL2 | T + I<sup>+</sup> | `deepseek-ai/deepseek-vl2-tiny`, `deepseek-ai/deepseek-vl2-small`, `deepseek-ai/deepseek-vl2`, etc. | | ✅︎ |
547 | `DeepseekOCRForCausalLM` | DeepSeek-OCR | T + I<sup>+</sup> | `deepseek-ai/DeepSeek-OCR`, etc. | ✅︎ | ✅︎ |
examples/offline_inference/vision_language.py
+28
@@ -179,6 +179,33 @@ def run_chameleon(questions: list[str], modality: str) -> ModelRequestData:
179 )
180
181
182 +# Cheers
183 +def run_cheers(questions: list[str], modality: str) -> ModelRequestData:
184 + assert modality == "image"
185 + model_name = "ai9stars/Cheers"
186 +
187 + engine_args = EngineArgs(
188 + model=model_name,
189 + trust_remote_code=True,
190 + max_model_len=4096,
191 + limit_mm_per_prompt={modality: 1},
192 + )
193 +
194 + prompts = [
195 + (
196 + f"<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n"
197 + f"<|im_start|>user\n<|image_pad|>{question}<|im_end|>\n"
198 + f"<|im_start|>assistant\n"
199 + )
200 + for question in questions
201 + ]
202 +
203 + return ModelRequestData(
204 + engine_args=engine_args,
205 + prompts=prompts,
206 + )
207 +
208 +
209 def run_command_a_vision(questions: list[str], modality: str) -> ModelRequestData:
210 assert modality == "image"
211
@@ -2140,6 +2167,7 @@ model_example_map = {
2167 "aria": run_aria,
2168 "aya_vision": run_aya_vision,
2169 "bagel": run_bagel,
2170 + "cheers": run_cheers,
2171 "bee": run_bee,
2172 "blip-2": run_blip2,
2173 "chameleon": run_chameleon,
tests/models/registry.py
+8
@@ -766,6 +766,14 @@ _MULTIMODAL_EXAMPLE_MODELS = {
766 extras={"6b": "Salesforce/blip2-opt-6.7b"},
767 ),
768 "ChameleonForConditionalGeneration": _HfExamplesInfo("facebook/chameleon-7b"),
769 + "Cheers": _HfExamplesInfo(
770 + "ai9stars/Cheers",
771 + trust_remote_code=True,
772 + ),
773 + "CheersForConditionalGeneration": _HfExamplesInfo(
774 + "ai9stars/Cheers",
775 + trust_remote_code=True,
776 + ),
777 "Cohere2VisionForConditionalGeneration": _HfExamplesInfo(
778 "CohereLabs/command-a-vision-07-2025"
779 ),
vllm/config/model.py
+1
@@ -1203,6 +1203,7 @@ class ModelConfig:
1203 "gemma3",
1204 "molmo2",
1205 "paligemma",
1206 + "umm",
1207 )
1208 if not hasattr(self.hf_config, "model_type"):
1209 return False
vllm/model_executor/models/cheers.py new
+753
@@ -0,0 +1,753 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Inference-only Cheers (UMM) model compatible with HuggingFace weights.
4 +
5 +Cheers is a unified multimodal model for image understanding and generation.
6 +For vLLM, we focus on the image understanding (vision-to-text) capabilities.
7 +The image generation part (gen_projector, hi_gate, etc.) is not supported,
8 +but the VAE encoder + decoder projector are required for image understanding.
9 +"""
10 +
11 +import math
12 +from collections.abc import Iterable, Mapping, Sequence
13 +from typing import Any, Literal, TypeAlias
14 +
15 +import torch
16 +import torch.nn as nn
17 +import torch.nn.functional as F
18 +from einops import rearrange
19 +from transformers import BatchFeature
20 +
21 +from vllm.config import VllmConfig
22 +from vllm.config.multimodal import BaseDummyOptions
23 +from vllm.inputs import MultiModalDataDict
24 +from vllm.logger import init_logger
25 +from vllm.model_executor.layers.quantization import QuantizationConfig
26 +from vllm.multimodal import MULTIMODAL_REGISTRY
27 +from vllm.multimodal.inputs import (
28 + MultiModalFieldConfig,
29 + MultiModalKwargsItems,
30 +)
31 +from vllm.multimodal.parse import MultiModalDataItems
32 +from vllm.multimodal.processing import (
33 + BaseDummyInputsBuilder,
34 + BaseMultiModalProcessor,
35 + BaseProcessingInfo,
36 + PromptReplacement,
37 +)
38 +from vllm.sequence import IntermediateTensors
39 +from vllm.transformers_utils.processors.cheers import CheersProcessor
40 +from vllm.utils.tensor_schema import TensorSchema
41 +
42 +from .interfaces import (
43 + MultiModalEmbeddings,
44 + SupportsLoRA,
45 + SupportsMultiModal,
46 + SupportsPP,
47 +)
48 +from .siglip import SiglipVisionModel
49 +from .utils import (
50 + AutoWeightsLoader,
51 + WeightsMapper,
52 + init_vllm_registered_model,
53 + maybe_prefix,
54 +)
55 +
56 +logger = init_logger(__name__)
57 +
58 +
59 +# ── VAE components (needed for image understanding pipeline) ────────
60 +
61 +
62 +def _swish(x: torch.Tensor) -> torch.Tensor:
63 + return x * torch.sigmoid(x)
64 +
65 +
66 +class _AttnBlock(nn.Module):
67 + def __init__(self, in_channels: int):
68 + super().__init__()
69 + self.norm = nn.GroupNorm(32, in_channels, eps=1e-6, affine=True)
70 + self.q = nn.Conv2d(in_channels, in_channels, 1)
71 + self.k = nn.Conv2d(in_channels, in_channels, 1)
72 + self.v = nn.Conv2d(in_channels, in_channels, 1)
73 + self.proj_out = nn.Conv2d(in_channels, in_channels, 1)
74 +
75 + def forward(self, x: torch.Tensor) -> torch.Tensor:
76 + h_ = self.norm(x)
77 + q = self.q(h_)
78 + k = self.k(h_)
79 + v = self.v(h_)
80 + b, c, h, w = q.shape
81 + q = rearrange(q, "b c h w -> b 1 (h w) c").contiguous()
82 + k = rearrange(k, "b c h w -> b 1 (h w) c").contiguous()
83 + v = rearrange(v, "b c h w -> b 1 (h w) c").contiguous()
84 + h_ = F.scaled_dot_product_attention(q, k, v)
85 + h_ = rearrange(h_, "b 1 (h w) c -> b c h w", h=h, w=w, c=c, b=b)
86 + return x + self.proj_out(h_)
87 +
88 +
89 +class _ResnetBlock(nn.Module):
90 + def __init__(self, in_channels: int, out_channels: int):
91 + super().__init__()
92 + self.in_channels = in_channels
93 + self.out_channels = out_channels
94 + self.norm1 = nn.GroupNorm(32, in_channels, eps=1e-6, affine=True)
95 + self.conv1 = nn.Conv2d(in_channels, out_channels, 3, 1, 1)
96 + self.norm2 = nn.GroupNorm(32, out_channels, eps=1e-6, affine=True)
97 + self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1)
98 + if in_channels != out_channels:
99 + self.nin_shortcut = nn.Conv2d(in_channels, out_channels, 1)
100 +
101 + def forward(self, x: torch.Tensor) -> torch.Tensor:
102 + h = _swish(self.norm1(x))
103 + h = self.conv1(h)
104 + h = _swish(self.norm2(h))
105 + h = self.conv2(h)
106 + if self.in_channels != self.out_channels:
107 + x = self.nin_shortcut(x)
108 + return x + h
109 +
110 +
111 +class _Downsample(nn.Module):
112 + def __init__(self, in_channels: int):
113 + super().__init__()
114 + self.conv = nn.Conv2d(in_channels, in_channels, 3, stride=2, padding=0)
115 +
116 + def forward(self, x: torch.Tensor) -> torch.Tensor:
117 + x = F.pad(x, (0, 1, 0, 1), mode="constant", value=0)
118 + return self.conv(x)
119 +
120 +
121 +class _Upsample(nn.Module):
122 + def __init__(self, in_channels: int):
123 + super().__init__()
124 + self.conv = nn.Conv2d(in_channels, in_channels, 3, 1, 1)
125 +
126 + def forward(self, x: torch.Tensor) -> torch.Tensor:
127 + x = F.interpolate(x, scale_factor=2.0, mode="nearest")
128 + return self.conv(x)
129 +
130 +
131 +_VAE_ENCODER_DEFAULTS = {
132 + "in_channels": 3,
133 + "ch": 128,
134 + "ch_mult": [1, 2, 4, 4],
135 + "num_res_blocks": 2,
136 + "z_channels": 32,
137 +}
138 +_VAE_DECODER_DEFAULTS = {
139 + "in_channels": 3,
140 + "out_ch": 3,
141 + "ch": 128,
142 + "ch_mult": [1, 2, 4, 4],
143 + "num_res_blocks": 2,
144 + "z_channels": 32,
145 +}
146 +
147 +
148 +def _cfg(config, key, defaults=None):
149 + """Access config attribute whether it's a dict or namespace object."""
150 + if isinstance(config, dict):
151 + if key in config:
152 + return config[key]
153 + if defaults and key in defaults:
154 + return defaults[key]
155 + raise KeyError(f"Key '{key}' not found in config dict: {list(config.keys())}")
156 + return getattr(config, key)
157 +
158 +
159 +class CheersVAEEncoder(nn.Module):
160 + """VAE encoder from the Cheers/UMM model."""
161 +
162 + def __init__(self, config):
163 + super().__init__()
164 + d = _VAE_ENCODER_DEFAULTS
165 + ch = _cfg(config, "ch", d)
166 + ch_mult = _cfg(config, "ch_mult", d)
167 + num_res_blocks = _cfg(config, "num_res_blocks", d)
168 + z_channels = _cfg(config, "z_channels", d)
169 + in_channels = _cfg(config, "in_channels", d)
170 + num_resolutions = len(ch_mult)
171 +
172 + self.quant_conv = nn.Conv2d(2 * z_channels, 2 * z_channels, 1)
173 + self.conv_in = nn.Conv2d(in_channels, ch, 3, 1, 1)
174 +
175 + in_ch_mult = (1,) + tuple(ch_mult)
176 + self.down = nn.ModuleList()
177 + block_in = ch
178 + for i_level in range(num_resolutions):
179 + block = nn.ModuleList()
180 + attn = nn.ModuleList()
181 + block_in = ch * in_ch_mult[i_level]
182 + block_out = ch * ch_mult[i_level]
183 + for _ in range(num_res_blocks):
184 + block.append(_ResnetBlock(block_in, block_out))
185 + block_in = block_out
186 + down = nn.Module()
187 + down.block = block
188 + down.attn = attn
189 + if i_level != num_resolutions - 1:
190 + down.downsample = _Downsample(block_in)
191 + self.down.append(down)
192 +
193 + self.mid = nn.Module()
194 + self.mid.block_1 = _ResnetBlock(block_in, block_in)
195 + self.mid.attn_1 = _AttnBlock(block_in)
196 + self.mid.block_2 = _ResnetBlock(block_in, block_in)
197 +
198 + self.norm_out = nn.GroupNorm(32, block_in, eps=1e-6, affine=True)
199 + self.conv_out = nn.Conv2d(block_in, 2 * z_channels, 3, 1, 1)
200 + self._num_resolutions = num_resolutions
201 + self._num_res_blocks = num_res_blocks
202 +
203 + def forward(self, x: torch.Tensor) -> torch.Tensor:
204 + hs = [self.conv_in(x)]
205 + for i_level in range(self._num_resolutions):
206 + for i_block in range(self._num_res_blocks):
207 + h = self.down[i_level].block[i_block](hs[-1])
208 + if len(self.down[i_level].attn) > 0:
209 + h = self.down[i_level].attn[i_block](h)
210 + hs.append(h)
211 + if hasattr(self.down[i_level], "downsample"):
212 + hs.append(self.down[i_level].downsample(hs[-1]))
213 + h = hs[-1]
214 + h = self.mid.block_1(h)
215 + h = self.mid.attn_1(h)
216 + h = self.mid.block_2(h)
217 + h = _swish(self.norm_out(h))
218 + h = self.conv_out(h)
219 + h = self.quant_conv(h)
220 + return h
221 +
222 +
223 +class CheersVAEDecoder(nn.Module):
224 + """VAE decoder (used inside VAEDecoderProjector)."""
225 +
226 + def __init__(self, config):
227 + super().__init__()
228 + d = _VAE_DECODER_DEFAULTS
229 + ch = _cfg(config, "ch", d)
230 + ch_mult = _cfg(config, "ch_mult", d)
231 + num_res_blocks = _cfg(config, "num_res_blocks", d)
232 + z_channels = _cfg(config, "z_channels", d)
233 + out_ch = _cfg(config, "out_ch", d)
234 + num_resolutions = len(ch_mult)
235 +
236 + self.post_quant_conv = nn.Conv2d(z_channels, z_channels, 1)
237 + block_in = ch * ch_mult[num_resolutions - 1]
238 + self.conv_in = nn.Conv2d(z_channels, block_in, 3, 1, 1)
239 +
240 + self.mid = nn.Module()
241 + self.mid.block_1 = _ResnetBlock(block_in, block_in)
242 + self.mid.attn_1 = _AttnBlock(block_in)
243 + self.mid.block_2 = _ResnetBlock(block_in, block_in)
244 +
245 + self.up = nn.ModuleList()
246 + for i_level in reversed(range(num_resolutions)):
247 + block = nn.ModuleList()
248 + attn = nn.ModuleList()
249 + block_out = ch * ch_mult[i_level]
250 + for _ in range(num_res_blocks + 1):
251 + block.append(_ResnetBlock(block_in, block_out))
252 + block_in = block_out
253 + up = nn.Module()
254 + up.block = block
255 + up.attn = attn
256 + if i_level != 0:
257 + up.upsample = _Upsample(block_in)
258 + self.up.insert(0, up)
259 +
260 + self.norm_out = nn.GroupNorm(32, block_in, eps=1e-6, affine=True)
261 + self.conv_out = nn.Conv2d(block_in, out_ch, 3, 1, 1)
262 + self._num_resolutions = num_resolutions
263 + self._num_res_blocks = num_res_blocks
264 +
265 + def forward(self, z: torch.Tensor) -> torch.Tensor:
266 + z = self.post_quant_conv(z)
267 + upscale_dtype = next(self.up.parameters()).dtype
268 + h = self.conv_in(z)
269 + h = self.mid.block_1(h)
270 + h = self.mid.attn_1(h)
271 + h = self.mid.block_2(h)
272 + h = h.to(upscale_dtype)
273 + for i_level in reversed(range(self._num_resolutions)):
274 + for i_block in range(self._num_res_blocks + 1):
275 + h = self.up[i_level].block[i_block](h)
276 + if len(self.up[i_level].attn) > 0:
277 + h = self.up[i_level].attn[i_block](h)
278 + if i_level != 0:
279 + h = self.up[i_level].upsample(h)
280 + h = _swish(self.norm_out(h))
281 + return self.conv_out(h)
282 +
283 +
284 +class CheersVAEModel(nn.Module):
285 + """VAE model with encoder only (for image understanding)."""
286 +
287 + def __init__(self, config):
288 + super().__init__()
289 + enc_cfg = _cfg(config, "vae_encoder_config")
290 + self.encoder = CheersVAEEncoder(enc_cfg)
291 + self.ps = [2, 2]
292 + z_ch = _cfg(enc_cfg, "z_channels", _VAE_ENCODER_DEFAULTS)
293 + self.bn = nn.BatchNorm2d(
294 + math.prod(self.ps) * z_ch,
295 + eps=1e-4,
296 + momentum=0.1,
297 + affine=False,
298 + track_running_stats=True,
299 + )
300 +
301 + def encode(self, x: torch.Tensor) -> torch.Tensor:
302 + self.bn.eval()
303 + moments = self.encoder(x)
304 + mean = torch.chunk(moments, 2, dim=1)[0]
305 + z = rearrange(
306 + mean,
307 + "... c (i pi) (j pj) -> ... (c pi pj) i j",
308 + pi=self.ps[0],
309 + pj=self.ps[1],
310 + )
311 + return self.bn(z)
312 +
313 +
314 +class CheersVAEDecoderProjector(nn.Module):
315 + """VAE decoder projector that converts latent back to pixel-like space."""
316 +
317 + def __init__(self, config):
318 + super().__init__()
319 + dec_cfg = _cfg(config, "vae_decoder_config")
320 + enc_cfg = _cfg(config, "vae_encoder_config")
321 + self.decoder = CheersVAEDecoder(dec_cfg)
322 + self.ps = [2, 2]
323 + z_ch = _cfg(enc_cfg, "z_channels", _VAE_ENCODER_DEFAULTS)
324 + self.bn = nn.BatchNorm2d(
325 + math.prod(self.ps) * z_ch,
326 + eps=1e-4,
327 + momentum=0.1,
328 + affine=False,
329 + track_running_stats=True,
330 + )
331 +
332 + def forward(self, z: torch.Tensor) -> torch.Tensor:
333 + self.bn.eval()
334 + s = torch.sqrt(self.bn.running_var.view(1, -1, 1, 1) + 1e-4)
335 + m = self.bn.running_mean.view(1, -1, 1, 1)
336 + z = z * s + m
337 + z = rearrange(
338 + z,
339 + "... (c pi pj) i j -> ... c (i pi) (j pj)",
340 + pi=self.ps[0],
341 + pj=self.ps[1],
342 + )
343 + return self.decoder(z)
344 +
345 +
346 +class CheersImagePixelInputs(TensorSchema):
347 + """
348 + Dimensions:
349 + - bn: Batch size * number of images
350 + - c: Number of channels (3)
351 + - h: Height of each image
352 + - w: Width of each image
353 + """
354 +
355 + type: Literal["pixel_values"]
356 + pixel_values: torch.Tensor # Shape: (bn, 3, h, w)
357 +
358 +
359 +CheersImageInputs: TypeAlias = CheersImagePixelInputs
360 +
361 +
362 +class CheersUndProjector(nn.Module):
363 + """Understanding projector that maps vision features to LLM dimension
364 + with 2x2 spatial compression (4x token reduction)."""
365 +
366 + def __init__(
367 + self,
368 + image_embed_dim: int,
369 + text_embed_dim: int,
370 + compression_factor: tuple[int, int] = (2, 2),
371 + quant_config: QuantizationConfig | None = None,
372 + prefix: str = "",
373 + ):
374 + super().__init__()
375 + self.image_embed_dim = image_embed_dim
376 + self.text_embed_dim = text_embed_dim
377 + self.compression_factor = compression_factor
378 + self.layernorm = nn.LayerNorm(image_embed_dim)
379 + hidden_size = image_embed_dim * (compression_factor[0] * compression_factor[1])
380 + self.mlp = nn.Sequential(
381 + nn.Linear(hidden_size, hidden_size),
382 + nn.GELU(),
383 + nn.Linear(hidden_size, text_embed_dim),
384 + )
385 +
386 + def forward(self, x: torch.Tensor) -> torch.Tensor:
387 + x = self.layernorm(x)
388 + height = width = int(x.size(1) ** 0.5)
389 + x = x.permute(0, 2, 1).unflatten(-1, (height, width))
390 + batch_size, dim, height, width = x.shape
391 + unfolded = x.unfold(
392 + 2, self.compression_factor[0], self.compression_factor[0]
393 + ).unfold(3, self.compression_factor[1], self.compression_factor[1])
394 + unfolded = unfolded.contiguous().view(
395 + batch_size,
396 + dim,
397 + -1,
398 + self.compression_factor[0] * self.compression_factor[1],
399 + )
400 + unfolded = (
401 + unfolded.permute(0, 2, 3, 1)
402 + .contiguous()
403 + .view(
404 + batch_size,
405 + -1,
406 + dim * self.compression_factor[0] * self.compression_factor[1],
407 + )
408 + )
409 + return self.mlp(unfolded)
410 +
411 +
412 +class CheersProcessingInfo(BaseProcessingInfo):
413 + """Processing information for Cheers model."""
414 +
415 + def get_hf_processor(self, **kwargs: object) -> CheersProcessor:
416 + from vllm.transformers_utils.processor import cached_get_image_processor
417 +
418 + image_processor = cached_get_image_processor(
419 + self.ctx.model_config.model,
420 + revision=self.ctx.model_config.revision,
421 + trust_remote_code=self.ctx.model_config.trust_remote_code,
422 + )
423 +
424 + tokenizer = self.get_tokenizer()
425 +
426 + return CheersProcessor(
427 + image_processor=image_processor,
428 + tokenizer=tokenizer,
429 + **kwargs,
430 + )
431 +
432 + def get_supported_mm_limits(self) -> Mapping[str, int | None]:
433 + return {"image": None}
434 +
435 + def get_mm_max_tokens_per_item(
436 + self,
437 + seq_len: int,
438 + mm_counts: Mapping[str, int],
439 + ) -> Mapping[str, int]:
440 + hf_config = self.get_hf_config()
441 + vit_config = hf_config.vision_representation_config
442 + patch_size = vit_config.patch_size
443 + image_size = vit_config.image_size
444 + num_patches = (image_size // patch_size) ** 2
445 + # After 2x2 compression, tokens reduce by 4x
446 + num_tokens = num_patches // 4
447 + return {"image": num_tokens}
448 +
449 + def get_num_image_tokens(
450 + self,
451 + *,
452 + image_width: int,
453 + image_height: int,
454 + ) -> int:
455 + hf_config = self.get_hf_config()
456 + vit_config = hf_config.vision_representation_config
457 + patch_size = vit_config.patch_size
458 + image_size = vit_config.image_size
459 + num_patches = (image_size // patch_size) ** 2
460 + return num_patches // 4
461 +
462 +
463 +class CheersDummyInputsBuilder(BaseDummyInputsBuilder[CheersProcessingInfo]):
464 + """Build dummy inputs for Cheers model profiling."""
465 +
466 + def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
467 + num_images = mm_counts.get("image", 0)
468 + return "<|image_pad|>" * num_images
469 +
470 + def get_dummy_mm_data(
471 + self,
472 + seq_len: int,
473 + mm_counts: Mapping[str, int],
474 + mm_options: Mapping[str, BaseDummyOptions] | None = None,
475 + ) -> MultiModalDataDict:
476 + num_images = mm_counts.get("image", 0)
477 + hf_config = self.info.get_hf_config()
478 + vit_config = hf_config.vision_representation_config
479 + image_size = vit_config.image_size
480 + image_overrides = mm_options.get("image") if mm_options else None
481 +
482 + return {
483 + "image": self._get_dummy_images(
484 + width=image_size,
485 + height=image_size,
486 + num_images=num_images,
487 + overrides=image_overrides,
488 + ),
489 + }
490 +
491 +
492 +class CheersMultiModalProcessor(BaseMultiModalProcessor[CheersProcessingInfo]):
493 + """Multimodal processor for Cheers model."""
494 +
495 + def _call_hf_processor(
496 + self,
497 + prompt: str,
498 + mm_data: Mapping[str, object],
499 + mm_kwargs: Mapping[str, object],
500 + tok_kwargs: Mapping[str, object],
501 + ) -> BatchFeature:
502 + return super()._call_hf_processor(prompt, mm_data, mm_kwargs, tok_kwargs)
503 +
504 + def _hf_processor_applies_updates(
505 + self,
506 + prompt_text: str,
507 + mm_items: MultiModalDataItems,
508 + hf_processor_mm_kwargs: Mapping[str, object],
509 + tokenization_kwargs: Mapping[str, object],
510 + ) -> bool:
511 + return False
512 +
513 + def _get_prompt_updates(
514 + self,
515 + mm_items: MultiModalDataItems,
516 + hf_processor_mm_kwargs: Mapping[str, Any],
517 + out_mm_kwargs: MultiModalKwargsItems,
518 + ) -> Sequence[PromptReplacement]:
519 + hf_config = self.info.get_hf_config()
520 + vit_config = hf_config.vision_representation_config
521 + patch_size = vit_config.patch_size
522 + image_size = vit_config.image_size
523 +
524 + tokenizer = self.info.get_tokenizer()
525 + image_token_id = tokenizer.get_vocab().get("<|image_pad|>")
526 + if image_token_id is None:
527 + raise ValueError(
528 + "Image token '<|image_pad|>' not found in tokenizer vocabulary"
529 + )
530 +
531 + def get_replacement_cheers(item_idx: int):
532 + num_patches = (image_size // patch_size) ** 2
533 + num_tokens = num_patches // 4
534 + return [image_token_id] * num_tokens
535 +
536 + return [
537 + PromptReplacement(
538 + modality="image",
539 + target=[image_token_id],
540 + replacement=get_replacement_cheers,
541 + )
542 + ]
543 +
544 + def _get_mm_fields_config(
545 + self,
546 + hf_inputs: Any,
547 + hf_processor_mm_kwargs: Mapping[str, object],
548 + ) -> Mapping[str, MultiModalFieldConfig]:
549 + return {
550 + "pixel_values": MultiModalFieldConfig.batched("image"),
551 + }
552 +
553 +
554 +@MULTIMODAL_REGISTRY.register_processor(
555 + CheersMultiModalProcessor,
556 + info=CheersProcessingInfo,
557 + dummy_inputs=CheersDummyInputsBuilder,
558 +)
559 +class CheersForConditionalGeneration(
560 + nn.Module, SupportsMultiModal, SupportsLoRA, SupportsPP
561 +):
562 + """
563 + Cheers: A unified multimodal model for image understanding and generation.
564 +
565 + For vLLM, we focus on the image understanding (vision-to-text) capabilities.
566 + The image generation part is not supported in vLLM.
567 + """
568 +
569 + requires_raw_input_tokens = True
570 +
571 + hf_to_vllm_mapper = WeightsMapper(
572 + orig_to_new_prefix={
573 + "model.language_model.": "language_model.model.",
574 + "model.vision_representation.": "vision_representation.vision_model.",
575 + "model.und_projector.": "und_projector.",
576 + "model.vae_model.": "vae_model.",
577 + "model.vae_decoder_projector.": "vae_decoder_projector.",
578 + "lm_head.": "language_model.lm_head.",
579 + }
580 + )
581 +
582 + @classmethod
583 + def get_placeholder_str(cls, modality: str, i: int) -> str | None:
584 + if modality.startswith("image"):
585 + return "<|image_pad|>"
586 + raise ValueError("Only image modality is supported")
587 +
588 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
589 + super().__init__()
590 +
591 + config = vllm_config.model_config.hf_config
592 + quant_config = vllm_config.quant_config
593 + multimodal_config = vllm_config.model_config.multimodal_config
594 +
595 + if type(config).__name__ not in ("CheersConfig", "UMMConfig"):
596 + raise ValueError(
597 + f"Expected CheersConfig or UMMConfig, got {type(config).__name__}."
598 + )
599 +
600 + self.config = config
601 + self.multimodal_config = multimodal_config
602 +
603 + # The Cheers model's custom Qwen2Config defaults rope_theta to
604 + # 1_000_000, but this isn't stored in the JSON. vLLM's standard
605 + # Qwen2Config defaults to 10_000, causing a 100× mismatch.
606 + # We must patch BOTH the attribute AND rope_parameters (which
607 + # patch_rope_parameters may have already populated from the wrong
608 + # default before __init__ runs).
609 + _CHEERS_ROPE_THETA = 1_000_000.0
610 + tc = config.text_config
611 + old_theta = getattr(tc, "rope_theta", None)
612 + if old_theta != _CHEERS_ROPE_THETA:
613 + logger.info(
614 + "Overriding text_config.rope_theta from %s to %s",
615 + old_theta,
616 + _CHEERS_ROPE_THETA,
617 + )
618 + tc.rope_theta = _CHEERS_ROPE_THETA
619 + rp = getattr(tc, "rope_parameters", None)
620 + if rp is not None and rp.get("rope_theta") != _CHEERS_ROPE_THETA:
621 + logger.info(
622 + "Overriding rope_parameters.rope_theta from %s to %s",
623 + rp.get("rope_theta"),
624 + _CHEERS_ROPE_THETA,
625 + )
626 + rp["rope_theta"] = _CHEERS_ROPE_THETA
627 +
628 + with self._mark_language_model(vllm_config):
629 + self.language_model = init_vllm_registered_model(
630 + vllm_config=vllm_config,
631 + hf_config=config.text_config,
632 + prefix=maybe_prefix(prefix, "language_model"),
633 + architectures=["Qwen2ForCausalLM"],
634 + )
635 +
636 + vit_config = config.vision_representation_config
637 +
638 + with self._mark_tower_model(vllm_config, "image"):
639 + self.vae_model = CheersVAEModel(config)
640 + self.vae_decoder_projector = CheersVAEDecoderProjector(config)
641 +
642 + self.vision_representation = SiglipVisionModel(
643 + config=vit_config,
644 + quant_config=quant_config,
645 + prefix=maybe_prefix(prefix, "vision_representation"),
646 + )
647 +
648 + vit_hidden_size = vit_config.hidden_size
649 + llm_hidden_size = config.text_config.hidden_size
650 +
651 + self.und_projector = CheersUndProjector(
652 + image_embed_dim=vit_hidden_size,
653 + text_embed_dim=llm_hidden_size,
654 + compression_factor=(2, 2),
655 + quant_config=quant_config,
656 + prefix=maybe_prefix(prefix, "und_projector"),
657 + )
658 +
659 + self.make_empty_intermediate_tensors = (
660 + self.language_model.make_empty_intermediate_tensors
661 + )
662 +
663 + def _parse_and_validate_image_input(
664 + self, **kwargs: object
665 + ) -> CheersImageInputs | None:
666 + pixel_values = kwargs.pop("pixel_values", None)
667 + if pixel_values is None:
668 + return None
669 + return CheersImagePixelInputs(
670 + type="pixel_values",
671 + pixel_values=pixel_values,
672 + )
673 +
674 + def _process_image_input(
675 + self, image_input: CheersImageInputs
676 + ) -> tuple[torch.Tensor, ...]:
677 + """Process image inputs through VAE → SigLIP → projector pipeline.
678 +
679 + HF native path: pixel_values → VAE.encode(t=1.0) → vae_decoder_projector
680 + → SigLIP → und_projector → text-space embeddings
681 + """
682 + pixel_values = image_input["pixel_values"]
683 +
684 + if pixel_values.ndim == 5:
685 + batch_size, num_images, channels, height, width = pixel_values.shape
686 + pixel_values = pixel_values.reshape(
687 + batch_size * num_images, channels, height, width
688 + )
689 +
690 + with torch.no_grad():
691 + vae_dtype = next(self.vae_model.parameters()).dtype
692 + image_latent = self.vae_model.encode(pixel_values.to(dtype=vae_dtype))
693 + image_pixel_hat = self.vae_decoder_projector(image_latent)
694 +
695 + vision_features = self.vision_representation(image_pixel_hat)
696 + vision_embeds = self.und_projector(vision_features)
697 +
698 + return tuple(vision_embeds)
699 +
700 + def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
701 + image_input = self._parse_and_validate_image_input(**kwargs)
702 + if image_input is None:
703 + return []
704 + return self._process_image_input(image_input)
705 +
706 + def forward(
707 + self,
708 + input_ids: torch.Tensor | None,
709 + positions: torch.Tensor,
710 + intermediate_tensors: IntermediateTensors | None = None,
711 + inputs_embeds: torch.Tensor | None = None,
712 + **kwargs: object,
713 + ) -> torch.Tensor | IntermediateTensors:
714 + if intermediate_tensors is not None:
715 + inputs_embeds = None
716 +
717 + hidden_states = self.language_model.model(
718 + input_ids=input_ids,
719 + positions=positions,
720 + intermediate_tensors=intermediate_tensors,
721 + inputs_embeds=inputs_embeds,
722 + )
723 + return hidden_states
724 +
725 + def compute_logits(
726 + self,
727 + hidden_states: torch.Tensor,
728 + ) -> torch.Tensor | None:
729 + return self.language_model.compute_logits(hidden_states)
730 +
731 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
732 + """Load weights, keeping VAE encoder/decoder projector for understanding."""
733 + skip_prefixes = [
734 + "model.time_embed.",
735 + "model.gen_projector.",
736 + "model.hi_gate.",
737 + "model.hi_projector.",
738 + "model.vae_model.decoder.",
739 + ]
740 + skip_keywords = [
741 + "text_loss_fc",
742 + ]
743 +
744 + filtered_weights = []
745 + for name, tensor in weights:
746 + if any(name.startswith(p) for p in skip_prefixes):
747 + continue
748 + if any(kw in name for kw in skip_keywords):
749 + continue
750 + filtered_weights.append((name, tensor))
751 +
752 + loader = AutoWeightsLoader(self)
753 + return loader.load_weights(filtered_weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/registry.py
+2
@@ -350,6 +350,8 @@ _MULTIMODAL_MODELS = {
350 "chameleon",
351 "ChameleonForConditionalGeneration",
352 ),
353 + "Cheers": ("cheers", "CheersForConditionalGeneration"),
354 + "CheersForConditionalGeneration": ("cheers", "CheersForConditionalGeneration"),
355 "Cohere2VisionForConditionalGeneration": (
356 "cohere2_vision",
357 "Cohere2VisionForConditionalGeneration",
vllm/transformers_utils/config.py
+1
@@ -80,6 +80,7 @@ class LazyConfigDict(dict):
80 _CONFIG_REGISTRY: dict[str, type[PretrainedConfig]] = LazyConfigDict(
81 afmoe="AfmoeConfig",
82 bagel="BagelConfig",
83 + umm="CheersConfig",
84 chatglm="ChatGLMConfig",
85 colmodernvbert="ColModernVBertConfig",
86 colpali="ColPaliConfig",
vllm/transformers_utils/configs/__init__.py
+2
@@ -18,6 +18,7 @@ _CLASS_TO_MODULE: dict[str, str] = {
18 "AfmoeConfig": "vllm.transformers_utils.configs.afmoe",
19 "AXK1Config": "vllm.transformers_utils.configs.AXK1",
20 "BagelConfig": "vllm.transformers_utils.configs.bagel",
21 + "CheersConfig": "vllm.transformers_utils.configs.cheers",
22 "ChatGLMConfig": "vllm.transformers_utils.configs.chatglm",
23 "ColModernVBertConfig": "vllm.transformers_utils.configs.colmodernvbert",
24 "ColPaliConfig": "vllm.transformers_utils.configs.colpali",
@@ -75,6 +76,7 @@ __all__ = [
76 "AfmoeConfig",
77 "AXK1Config",
78 "BagelConfig",
79 + "CheersConfig",
80 "ChatGLMConfig",
81 "ColModernVBertConfig",
82 "ColPaliConfig",
vllm/transformers_utils/configs/cheers.py new
+109
@@ -0,0 +1,109 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from transformers import PretrainedConfig, SiglipVisionConfig
4 +from transformers.modeling_rope_utils import rope_config_validation
5 +
6 +
7 +class CheersTextConfig(PretrainedConfig):
8 + """Qwen2-based text config with Cheers-specific defaults."""
9 +
10 + model_type = "umm"
11 + base_config_key = "text_config"
12 +
13 + def __init__(
14 + self,
15 + vocab_size=152064,
16 + hidden_size=3584,
17 + intermediate_size=18944,
18 + num_hidden_layers=28,
19 + num_attention_heads=28,
20 + num_key_value_heads=4,
21 + hidden_act="silu",
22 + max_position_embeddings=131072,
23 + initializer_range=0.02,
24 + rms_norm_eps=1e-6,
25 + use_cache=True,
26 + tie_word_embeddings=False,
27 + rope_theta=1000000.0,
28 + rope_scaling=None,
29 + use_sliding_window=False,
30 + sliding_window=131072,
31 + max_window_layers=28,
32 + layer_types=None,
33 + attention_dropout=0.0,
34 + **kwargs,
35 + ):
36 + self.vocab_size = vocab_size
37 + self.max_position_embeddings = max_position_embeddings
38 + self.hidden_size = hidden_size
39 + self.intermediate_size = intermediate_size
40 + self.num_hidden_layers = num_hidden_layers
41 + self.num_attention_heads = num_attention_heads
42 + self.use_sliding_window = use_sliding_window
43 + self.sliding_window = sliding_window if self.use_sliding_window else None
44 + self.max_window_layers = max_window_layers
45 + if num_key_value_heads is None:
46 + num_key_value_heads = num_attention_heads
47 + self.num_key_value_heads = num_key_value_heads
48 + self.hidden_act = hidden_act
49 + self.initializer_range = initializer_range
50 + self.rms_norm_eps = rms_norm_eps
51 + self.use_cache = use_cache
52 + self.rope_theta = rope_theta
53 + self.rope_scaling = rope_scaling
54 + self.attention_dropout = attention_dropout
55 + if self.rope_scaling is not None and "type" in self.rope_scaling:
56 + self.rope_scaling["rope_type"] = self.rope_scaling["type"]
57 + rope_config_validation(self)
58 +
59 + self.layer_types = layer_types
60 + if self.layer_types is None:
61 + self.layer_types = [
62 + "sliding_attention"
63 + if self.sliding_window is not None and i >= self.max_window_layers
64 + else "full_attention"
65 + for i in range(self.num_hidden_layers)
66 + ]
67 +
68 + super().__init__(
69 + tie_word_embeddings=tie_word_embeddings,
70 + **kwargs,
71 + )
72 +
73 +
74 +class CheersConfig(PretrainedConfig):
75 + """Configuration class for Cheers (UMM) model."""
76 +
77 + model_type = "umm"
78 +
79 + def __init__(
80 + self,
81 + text_config: dict | CheersTextConfig | None = None,
82 + vision_representation_config: dict | SiglipVisionConfig | None = None,
83 + vae_encoder_config: dict | None = None,
84 + vae_decoder_config: dict | None = None,
85 + **kwargs,
86 + ):
87 + super().__init__(**kwargs)
88 +
89 + if isinstance(text_config, dict):
90 + self.text_config = CheersTextConfig(**text_config)
91 + else:
92 + self.text_config = text_config or CheersTextConfig()
93 +
94 + if isinstance(vision_representation_config, dict):
95 + self.vision_representation_config = SiglipVisionConfig(
96 + **vision_representation_config
97 + )
98 + else:
99 + self.vision_representation_config = (
100 + vision_representation_config or SiglipVisionConfig()
101 + )
102 +
103 + self.vae_encoder_config = vae_encoder_config or {"resolution": 512}
104 + self.vae_decoder_config = vae_decoder_config or {"resolution": 512}
105 +
106 + @property
107 + def hidden_size(self) -> int:
108 + """Return the hidden size of the language model."""
109 + return self.text_config.hidden_size
vllm/transformers_utils/processors/__init__.py
+2
@@ -12,6 +12,7 @@ import importlib
12
13 __all__ = [
14 "BagelProcessor",
15 + "CheersProcessor",
16 "CohereASRProcessor",
17 "DeepseekVLV2Processor",
18 "FireRedASR2Processor",
@@ -39,6 +40,7 @@ __all__ = [
40
41 _CLASS_TO_MODULE: dict[str, str] = {
42 "BagelProcessor": "vllm.transformers_utils.processors.bagel",
43 + "CheersProcessor": "vllm.transformers_utils.processors.cheers",
44 "CohereASRProcessor": "vllm.transformers_utils.processors.cohere_asr",
45 "DeepseekVLV2Processor": "vllm.transformers_utils.processors.deepseek_vl2",
46 "FireRedASR2Processor": "vllm.transformers_utils.processors.fireredasr2",
vllm/transformers_utils/processors/cheers.py new
+89
@@ -0,0 +1,89 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Cheers (UMM) processor for image and text inputs."""
4 +
5 +from transformers import AutoProcessor
6 +from transformers.feature_extraction_utils import BatchFeature
7 +from transformers.image_utils import ImageInput
8 +from transformers.processing_utils import ProcessingKwargs, ProcessorMixin, Unpack
9 +from transformers.tokenization_utils_base import PreTokenizedInput, TextInput
10 +
11 +
12 +class CheersProcessorKwargs(ProcessingKwargs, total=False): # type: ignore[call-arg]
13 + _defaults = {
14 + "images_kwargs": {
15 + "return_tensors": "pt",
16 + },
17 + }
18 +
19 +
20 +class CheersProcessor(ProcessorMixin):
21 + """
22 + Constructs a Cheers processor which wraps a
23 + SigLIP image processor and a Qwen2 tokenizer.
24 + """
25 +
26 + attributes = ["image_processor", "tokenizer"]
27 + image_processor_class = "AutoImageProcessor"
28 + tokenizer_class = "AutoTokenizer"
29 +
30 + def __call__(
31 + self,
32 + text: TextInput
33 + | PreTokenizedInput
34 + | list[TextInput]
35 + | list[PreTokenizedInput] = None,
36 + images: ImageInput = None,
37 + **kwargs: Unpack[CheersProcessorKwargs],
38 + ):
39 + output_kwargs = self._merge_kwargs(
40 + CheersProcessorKwargs,
41 + tokenizer_init_kwargs=self.tokenizer.init_kwargs,
42 + **kwargs,
43 + )
44 +
45 + if images is not None:
46 + import torch
47 +
48 + if isinstance(images, (list, tuple)):
49 + all_pv = []
50 + all_ghw = []
51 + for img in images:
52 + result = self.image_processor(img, **output_kwargs["images_kwargs"])
53 + all_pv.append(result["pixel_values"])
54 + if "grid_hws" in result:
55 + all_ghw.append(result["grid_hws"])
56 + pixel_values = {
57 + "pixel_values": torch.cat(all_pv, dim=0),
58 + }
59 + if all_ghw:
60 + pixel_values["grid_hws"] = torch.cat(all_ghw, dim=0)
61 + else:
62 + pixel_values = self.image_processor(
63 + images, **output_kwargs["images_kwargs"]
64 + )
65 + else:
66 + pixel_values = {}
67 +
68 + text_inputs = (
69 + self.tokenizer(text, **output_kwargs["text_kwargs"])
70 + if text is not None
71 + else {}
72 + )
73 +
74 + return BatchFeature(data={**pixel_values, **text_inputs})
75 +
76 + def batch_decode(self, *args, **kwargs):
77 + return self.tokenizer.batch_decode(*args, **kwargs)
78 +
79 + def decode(self, *args, **kwargs):
80 + return self.tokenizer.decode(*args, **kwargs)
81 +
82 + @property
83 + def model_input_names(self):
84 + tokenizer_input_names = self.tokenizer.model_input_names
85 + image_processor_input_names = self.image_processor.model_input_names
86 + return list(dict.fromkeys(tokenizer_input_names + image_processor_input_names))
87 +
88 +
89 +AutoProcessor.register("CheersProcessor", CheersProcessor)