Problem Description
During multimodal inference on an on‑premises server, a custom‑trained vision‑language model integrated with vLLM produces captions and answers that do not correspond to the supplied image. The generated text is either generic (e.g., “a person standing in a room”) or completely unrelated to the visual content. The failure is reproducible across different images and manifests in both image‑captioning and vision‑based question‑answering endpoints.
Typical symptoms observed in logs and API responses:
- API response contains only the textual part of the prompt, ignoring the image.
- Log entry:
vLLM.InferenceError: modality mismatch - Log entry:
RuntimeError: size mismatch, expected 768 but got 1024 - Log entry:
ValueError: Unexpected number of image channels (1)when grayscale images are supplied. - Performance metrics show normal token generation latency, indicating the text decoder runs but receives empty or corrupted visual embeddings.
Root Cause Analysis
The mismatch originates from three tightly coupled components:
- Image preprocessing pipeline – vLLM expects vision encoders (e.g., CLIP, BLIP) to receive images normalized with CLIP’s mean/std (
[0.48145466, 0.4578275, 0.40821073]and[0.26862954, 0.26130258, 0.27577711]) and resized to224×224(or model‑specific size). Using ImageNet normalization (mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) shifts the distribution of pixel values, producing embeddings that do not align with the text decoder’s learned space. This is documented in the vLLM Multimodal Inference Guide and matches the real incident where “image preprocessing used ImageNet mean/std while vLLM’s default expects CLIP normalization” (Evidence Package). - Vision encoder output dimensionality – The custom model’s vision head was exported to TorchScript with
float16tensors, while the text decoder expectsfloat32embeddings of size768. The mismatch triggers the runtime error “expected 768 but got 1024”, indicating the encoder’s output dimension diverges from the decoder’s projection layer (Evidence Package). - Request payload construction – Omitting the required
image_idfield or providing an unsupported image format forces vLLM to fall back to a cached text‑only prompt, yielding answers that ignore the visual input (Evidence Package).
Collectively, these issues break the alignment between visual embeddings and textual token embeddings, leading to the observed irrelevant outputs.
Investigation and Debugging
The following step‑by‑step process reproduces the diagnosis on a typical deployment.
1. Verify vLLM version and CUDA compatibility
python -c "import vllm, torch; print(vllm.__version__, torch.version.cuda)"
# Expected output, e.g.: 0.3.1 12.1
If the CUDA runtime reported by torch.version.cuda does not match the version used to build vLLM (see the On‑Premises Setup guide), rebuild vLLM with the correct CUDA toolkit.
2. Inspect the request payload
curl -X POST http://localhost:8000/v1/completions \\
-H "Content-Type: application/json" \\
-d '{
"model": "custom-blip",
"prompt": " Describe the scene.",
"max_tokens": 64
}'
Ensure the JSON contains a valid image_id that maps to a pre‑loaded image tensor. Missing this field triggers the “modality mismatch” error.
3. Check image preprocessing parameters
python - <<'PY'
from vllm.multimodal import MultimodalProcessor
proc = MultimodalProcessor.from_pretrained("custom-blip")
print("Mean:", proc.image_processor.mean)
print("Std :", proc.image_processor.std)
PY
Expected output (CLIP normalization):
Mean: [0.48145466, 0.4578275, 0.40821073]
Std : [0.26862954, 0.26130258, 0.27577711]
If the printed values differ, the processor was instantiated with the wrong configuration.
4. Validate vision encoder output shape and dtype
python - <<'PY'
import torch
from vllm.multimodal import VisionEncoder
encoder = VisionEncoder.from_pretrained("custom-blip-vision")
dummy = torch.randn(1, 3, 224, 224).to("cuda")
out = encoder(dummy)
print(out.shape, out.dtype)
PY
Expected: torch.Size([1, 768]) torch.float32. A shape of [1, 1024] or dtype float16 indicates a projection mismatch.
5. Examine server logs for preprocessing failures
journalctl -u vllm.service -f | grep -i "image"
# Sample log snippet:
2026-06-21 14:03:12,845 WARN vllm.multimodal.processor - Failed to decode image tensor: unsupported image format or corrupted file
2026-06-21 14:03:12,847 ERROR vllm.inference - vLLM.InferenceError: modality mismatch
6. Reproduce the issue with a minimal script
python - <<'PY'
import requests, base64, json, pathlib
img_path = pathlib.Path("sample.jpg").read_bytes()
payload = {
"model": "custom-blip",
"prompt": f" What is in the picture?",
"max_tokens": 32
}
r = requests.post("http://localhost:8000/v1/completions", json=payload)
print(r.json())
PY
If the response contains only the textual prompt without any visual grounding, the problem likely lies in preprocessing or encoder‑decoder alignment.
Resolution
Apply the following fixes in the order presented. Each block shows the configuration before the change and the corrected version after.
1. Align image normalization with CLIP defaults
Before (incorrect ImageNet normalization):
from torchvision import transforms
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
])
After (CLIP normalization as required by vLLM):
from torchvision import transforms
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.48145466, 0.4578275, 0.40821073],
std=[0.26862954, 0.26130258, 0.27577711]),
])
Re‑run the preprocessing pipeline and reload the model.
2. Ensure vision encoder output dimension matches text decoder
If the encoder was exported with an incompatible projection layer, re‑export using the same dtype and projection size as the decoder.
Re‑export script (TorchScript, float32, 768‑dim output):
import torch
from model import VisionEncoder
encoder = VisionEncoder()
encoder.eval()
example = torch.randn(1, 3, 224, 224)
traced = torch.jit.trace(encoder, example)
traced.save("vision_encoder_fp32.pt")
Replace the old checkpoint and restart the vLLM engine.
3. Correct request payload construction
Include the image_id field and ensure the image is base64‑encoded.
# Incorrect payload (missing image_id)
{
"model": "custom-blip",
"prompt": "Describe the image."
}
# Correct payload
{
"model": "custom-blip",
"prompt": " Describe the image."
}
4. Rebuild vLLM for the target CUDA version
If the environment uses CUDA 12.1 while the binary was compiled for CUDA 11.8, rebuild:
# From the vLLM source directory
export CUDA_HOME=/usr/local/cuda-12.1
pip install -e . # rebuilds native extensions against CUDA 12.1
Validation
After applying the fixes, perform the following checks:
- Functional test – Send a known image and verify the caption matches the content.
- Log inspection – No “modality mismatch” or size‑mismatch errors should appear.
- Tensor shape verification – Run the diagnostic script from the investigation section; it should print
torch.Size([1, 768]) torch.float32. - Performance sanity check – Latency should remain within the baseline (< 200 ms for 32 tokens on the test hardware).
curl -X POST http://localhost:8000/v1/completions \
-H "Content-Type: application/json" \
-d '{"model":"custom-blip","prompt":" Caption this image.","max_tokens":32}'
Expected output (example):
{
"id":"cmpl-...",
"choices":[{"text":"A red sports car parked in front of a modern building."}]
}
Prevention and Best Practices
- Standardize image preprocessing across all pipelines. Store the mean/std values in a configuration file and reference them in both training and inference scripts.
- Pin the vision encoder and text decoder to the same
dtypeand projection dimension. Include a CI test that assertsencoder.output_dim == decoder.hidden_size. - Validate request payloads with a schema validator (e.g.,
jsonschema) before forwarding to vLLM to catch missingimage_idfields early. - Maintain a reproducible build environment: use the same CUDA/cuDNN versions for training, exporting, and serving. Record these versions in
requirements.txtand container tags. - Enable vLLM’s built‑in monitoring (see the Multimodal Inference Guide) to emit metrics for
vision_encoder_latency_msandmodality_mismatch_errors. Alert on non‑zero counts.
Related Topic Hub: Model Serving Troubleshooting Hub
FAQ
- Why does the model generate correct text when I disable the image input?
Without an image, vLLM falls back to a text‑only prompt, so the decoder produces generic completions based solely on the language model. The visual pathway is simply bypassed. - Can I use ImageNet normalization if my model was trained with it?
Yes, but you must also train the text decoder on embeddings generated with the same normalization. Mixing CLIP‑normalized encoders with ImageNet‑normalized inputs will cause the alignment error described above. - What does the error “size mismatch, expected 768 but got 1024” indicate?
The vision encoder’s final projection layer outputs a vector of length 1024, while the text decoder’s cross‑attention expects a 768‑dimensional embedding. Re‑export the encoder with the correct projection size or add a linear adaptor to reshape the vector. - My images are grayscale; how should I handle them?
Convert them to three‑channel RGB before preprocessing. vLLM raisesValueError: Unexpected number of image channels (1)for single‑channel inputs. - Is it safe to run the vision encoder in float16 to save memory?
Only if the downstream text decoder also operates in float16 and the model was fine‑tuned with mixed‑precision. Mixing float16 encoder outputs with a float32 decoder leads to degraded embeddings and mismatched dimensions.