Skip to content

[BUG] Vision feature cache key omits image dimensions, causing incorrect inference on shape-colliding images #2151

Description

@3em0

Describe the bug

_image_cache_key() and content_hash in src/exo/worker/engines/mlx/vision.py compute SHA-256 over PIL.Image.tobytes() after convert("RGB"). tobytes() returns raw pixel bytes without any dimension information. Two RGB images with different
width×height but identical pixel byte sequences (e.g. 6×4 vs 4×6 with the same pixel data) will produce the same hash, causing the feature cache and prefix cache to return features computed for the wrong image.

This is a deterministic collision — no brute-force required. The vision encoder preprocesses images by resizing according to aspect ratio, so a 6×4 and a 4×6 image produce semantically different features despite having identical tobytes() output.

To Reproduce

  1. Start exo with a vision-capable model
  2. Send an API request with a 6×4 RGB image (e.g. pixel values [0..71])
  3. Send a second request with a 4×6 RGB image whose raw pixel bytes are identical (same values, different spatial arrangement)
  4. The second request hits the feature cache and returns features computed from the 6×4 image

Minimal PoC:

from PIL import Image
import hashlib

pixels = bytes(range(72)) # 643 = 72 bytes RGB

img_a = Image.frombytes("RGB", (6, 4), pixels)
img_b = Image.frombytes("RGB", (4, 6), pixels)

hash_a = hashlib.sha256(img_a.tobytes()).hexdigest()
hash_b = hashlib.sha256(img_b.tobytes()).hexdigest()

assert hash_a == hash_b # ← passes, but images are visually different
assert img_a.size != img_b.size

Expected behavior

Images with different dimensions should produce different cache keys, even if their raw pixel byte sequences are identical. The hash input should include width and height

Actual behavior

The feature cache (_feature_cache) and prefix cache (content_hash in MediaRegion) treat the two images as identical, returning stale/incorrect vision features for the second request

Affected locations:

  • src/exo/worker/engines/mlx/vision.py:712 — content_hash for prefix cache validation
  • src/exo/worker/engines/mlx/vision.py:737-742 — _image_cache_key for feature cache lookup

Environment

Not hardware-specific. Affects any exo deployment using vision models via the MLX engine.

Additional context

Suggested fix — mix dimensions into the hash:

_image_cache_key (line 737-742)

def _image_cache_key(self, images: list[Base64Image]) -> str:
h = hashlib.sha256()
for img in images:
pil = decode_base64_image(img)
h.update(f"{pil.width}x{pil.height}".encode())
h.update(pil.tobytes())
return h.hexdigest()

content_hash (line 711-712)

img = decode_base64_image(images[i])
region.content_hash = hashlib.sha256(
f"{img.width}x{img.height}".encode() + img.tobytes()
).hexdigest()

When exo serves multiple API clients, the process-level _feature_cache is shared across all requests. A shape collision would cause one client's inference to silently use vision features computed from a different client's image.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions