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
- Start exo with a vision-capable model
- Send an API request with a 6×4 RGB image (e.g. pixel values [0..71])
- Send a second request with a 4×6 RGB image whose raw pixel bytes are identical (same values, different spatial arrangement)
- 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.
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
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:
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.