Inline raw image refs for multimodal rendering - #110
Conversation
Drop the cache-only None path. Every image (current and prior turns) carries its raw descriptor ref; _descriptor_only_mm_data no longer strips the pointer, so refs carry forward without a rebuild. Removes the now-orphaned materialize_image_refs / materialize_kimi_image_refs and the materialize_all_image_refs flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tale comments - Drop the render-time processor constructor arg from Qwen3VL/Qwen35/Kimi renderers: geometry is computed deterministically from config; no renderer runs the HF image processor at render. Remove Kimi dead _get_processor/_process_image/self._processor/_image_cache. - mm_store: remove all backcompat aliases (MMRAW_PREFIX, MM_RAW_PAYLOAD_KEY/VALUE, mmraw_ref, split_mmraw_ref, image_asset_dir) -- no consumers. - client.py: fix stale generate() docstring + comment that referenced the removed None/cache path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It only sized Kimi per-renderer image cache, which was deleted with the render-time processor path. No consumers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s-renderers # Conflicts: # renderers/configs.py # renderers/qwen3_vl.py
- Fix bridge merge mutating the caller's previous sidecar in place: shared merge_multi_modal_data helper copies inner lists, replacing the three per-renderer merge blocks; bridge test asserts no mutation. - Import Qwen's smart_resize from transformers (torch-free PIL-backend module) instead of maintaining a port. - Resolve and read each raw image asset once per layout describe. - mm_store: full sha256 content-addressed filenames; raise on undecodable base64 instead of silently passing the data URL through. - Drop the dead features/mm_data tuple plumbing in client.generate. - Add layout-math parity test against the real Qwen3-VL and Kimi-K2.5 image processors at rounding-boundary dimensions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s-renderers Weaves main's independent bridge-mutation fix and thinking-deviation skip guard into this branch's raw/processed offload restructuring: - kimi_k25.py/qwen35.py/qwen3_vl.py: kept our shared merge_multi_modal_data() helper over main's three duplicated inline fixes for the same bug (identical behavior, single implementation). - test_multimodal.py: re-injected main's _skip_for_disabled_thinking_deviation guard into this branch's parallel renderer_cases/processor_cases structure at all 4 call sites; took main's tuple-based prior_mm/prior_counts assertion over our redundant duplicate. - test_client.py: kept both additions (non-overlapping). Full suite: 2578 passed, 153 skipped, 1 xfailed.
Intermediate storage mode: the image processor stays out of the render path (raw geometry math + descriptors), but images travel inline as data:image/...;base64 URLs instead of offloaded file:// run assets. - mm_store: the mmraw ref envelope carries raw_image_data (the inline data URL) instead of raw_image_uri; offload_image_to_run_assets, run_image_dir, and VF_RENDERER_IMAGE_OFFLOAD_DIR are gone. New decode_data_image_url() is the one supported source decode. - qwen3_vl/qwen35/kimi_k25: raw layout describes decode the inline source once for dimensions + sha256 hash; descriptors embed it. - client: _build_vllm_mm_features serializes the inline source into every image slot's ref. Suite: 2153 passed, 121 skipped, 1 xfailed (parity deselection as usual).
The ref rides as a string inside a JSON request body, so serializing the payload as compact JSON costs only the escaping of its own quotes (~200 bytes). The base64 wrapper instead inflated the entire payload by a third — with an inline image source that was ~32 KiB per image, on every image slot of every request. Verified: a 75 KiB JPEG's ref goes 130.3 KiB -> 97.9 KiB on the wire (the 97.7 KiB data URL plus ~200 bytes), a 25% cut. The payload contains colons now, so refs parse with a single partition; the base64-alphabet guard on the segment is gone with the wrapper.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 50ce3c3. Configure here.
| "pillow>=12.2.0", | ||
| "torch>=2.11.0", | ||
| "torchvision>=0.26.0", | ||
| ] |
There was a problem hiding this comment.
Vision extra pulls unused torch
Medium Severity
The new vision optional extra installs torch and torchvision, but this package no longer imports either after moving client serialization to raw refs and using return_tensors="np" in processed mode. Default raw multimodal rendering only needs Pillow for dimension reads, so renderers[vision] currently forces a large unused dependency set.
Reviewed by Cursor Bugbot for commit 50ce3c3. Configure here.
ApprovabilityVerdict: Needs human review This PR introduces a new multimodal output mode system and changes how images are serialized for inference, switching from processed tensors to raw image refs. These are substantial architectural changes affecting core multimodal rendering behavior across multiple files. You can customize Macroscope's approvability policy. Learn more. |


Summary
Intermediate multimodal storage mode — the inline bundle. Keeps this repo's raw multimodal contract (renderer computes token layout via pure geometry math, no image processor at render time; refs + descriptors instead of processed tensors) but images travel inline as
data:image/...;base64URLs instead of offloadedfile://run assets.mm_store: themmrawref envelope carriesraw_image_data(the inline data URL) instead ofraw_image_uri.offload_image_to_run_assets,run_image_dir, andVF_RENDERER_IMAGE_OFFLOAD_DIRare gone. Newdecode_data_image_url()is the single supported source decode.client.generate()serializes the inline source into every image slot's ref — every request to/inference/v1/generatecarries the full inline payload for every image in the prompt (current and prior turns), and the prime-rl serving layer dedups processor work by content hash.Relationship to the offload bundle
This PR is cut from the offload branch (#89) and is the storage-layer intermediate: same processor-ownership architecture, no shared image directory. The offload bundle (renderers #89 / verifiers #1746 / prime-rl #2836) layers content-addressed
file://offload on top of this contract.Companion inline PRs: verifiers
feat/v1-inline-raw-images, prime-rlfeat/v1-inline-mm.Validation
2153 passed, 121 skipped, 1 xfailed(processor-parity tests included, run against the real Qwen3-VL-4B and pinned Kimi-K2.5 processors).Note
Inline raw image refs for multimodal rendering instead of processed tensors
"raw") across Qwen3-VL, Qwen3.5, and Kimi K2.5 renderers where images are emitted as raw run-image reference envelopes rather than processor-computed pixel tensors; processed tensors remain available viamultimodal_output="processed".raw_mm_item,raw_mm_ref,split_raw_mm_ref) for raw multimodal items shared by renderers and client feature serialization.describe_qwen_image_layoutanddescribe_kimi_image_layoutto compute image token counts and grid metadata deterministically without running an HF processor._build_mm_features/_build_qwen_vl_featuresin renderers/client.py with a renderer-agnostic_build_vllm_mm_featuresthat serializes raw reference strings per item across modalities.NotImplementedErrorat feature-build time;image_cache_maxis removed from all renderer configs; renderer constructors no longer accept an injectedprocessorargument.Macroscope summarized 50ce3c3.
Update: unwrapped the ref payload (
603118b)raw_mm_refserializes its payload as compact JSON instead of base64-wrapping it. The ref travels as a string inside a JSON request body, so the only encoding cost is escaping the payload's own quotes (~200 bytes) — while the base64 wrapper inflated the entire payload, image included, by a third.Measured on a 75 KiB JPEG: 130.3 KiB → 97.9 KiB per image slot (the 97.7 KiB data URL plus ~200 bytes), a 25% cut on every image slot of every request. Refs parse with a single
partition(":")now that the payload contains colons; the base64-alphabet guard retired with the wrapper.Note
High Risk
Changes the default
MultiModalData.mm_itemsshape and the vLLM generate request payload, affecting inference and any caller that assumed processed tensors; multimodal bridge and multi-turn image carry-forward behavior also shift.Overview
Multimodal rendering now defaults to lightweight raw image descriptors instead of running HuggingFace image processors at render time. A new
multimodal_outputsetting on renderer configs choosesraw(JSON-safe refs for inference) orprocessed(pixel_valuesfor SFT/training). Auto/default renderer resolution forwardsmultimodal_outputlikethinking_retention.New
renderers/mm_store.pydefines the wire contract: decode inlinedata:image/...;base64URLs, buildprime_raw_mm_itemenvelopes, and emit compactmmraw:refs (payload is JSON, not base64-wrapped).merge_multi_modal_datacentralizes bridge turn sidecar merging.Qwen3-VL, Qwen3.5, and Kimi K2.5 compute pad counts and grid metadata via ported layout math (
describe_*_image_layout) without materializing pixels in raw mode; processors load lazily only whenmultimodal_output='processed'. Constructorprocessor=injection and per-rendererimage_cache_maxconfig are removed.client.generate()no longer dispatches on renderer class or imports torch/vLLM to base64-encode HF batches._build_vllm_mm_featuresmaps raw envelopes to vLLMfeatures(hashes, placeholders, one ref per slot with inline image data). Prebuiltprompt_idscan pullmulti_modal_datafromprompt_attributionwhen omitted.Packaging/tests: optional
visionextra (pillow/torch/torchvision); multimodal parity tests use inline data URLs; new tests cover raw ref serialization, processed-mode payloads, and raw-vs-processor layout math.Reviewed by Cursor Bugbot for commit 50ce3c3. Bugbot is set up for automated code reviews on this repo. Configure here.