[codex] Support raw image refs for multimodal rendering - #89
Open
eligotts wants to merge 24 commits into
Open
Conversation
This was referenced Jun 18, 2026
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
eligotts
marked this pull request as ready for review
June 29, 2026 16:36
ApprovabilityVerdict: Needs human review This PR introduces a new multimodal rendering mode ('raw' vs 'processed') that changes the default behavior from shipping processed tensors to shipping raw image references. The scope spans new infrastructure (mm_store.py), client serialization changes, and modifications to multiple renderers - warranting human review for this runtime behavior change. You can customize Macroscope's approvability policy. Learn more. |
- 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.
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 e64cc58. Configure here.
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. The base64 wrapper inflated the whole payload by a third for no benefit. The saving is small in offload mode (the payload is a short file:// URI), but it keeps the ref format identical to the inline bundle, which carries the image source in the payload and saves ~32 KiB per image slot. The payload contains colons now, so refs parse with a single partition; the base64-alphabet guard on the segment is gone with the wrapper.
…s-renderers # Conflicts: # renderers/client.py
Bare filesystem paths were a second route the rest of the stack can't honor anyway — prime-rl's trainer-side file_uri_to_path already rejects anything without a file:// scheme, and verifiers ingress only ever emits as_uri(). Folds the two-step helper into one and drops the dead branch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Design update — inline/offload image storage
This PR now supports both raw image transport modes used by prime-rl:
offload: existing behavior, raw image bytes are written to run-scoped image assets and refs carry a file-backed image id.inline: data-image URIs remain inline and raw refs carry the inline source instead of requiringraw_image_id.This repo adds inline-capable
mmraw:v3refs while preservingmmraw:v2parsing, keeps Qwen image hashes aligned to the raw decoded bytes, and emits raw descriptor items with eitherraw_image_idorraw_uri.Validation after latest push:
uv run pytest tests/test_client.py -qpassed (14 passed).Design update — dropped the
None/cache-only image pathThis PR and its companions (prime-rl #2836 / verifiers #1746 / renderers #89) no longer use the "send
Nonefor already-cached images" mechanism. Every image carries its raw descriptor ref at every slot (current and prior turns);/inference/v1/generaterematerializes each ref from disk every request.Why: the
Nonepath coupled correctness to deployment (LRU cache present, single replica / DP-affinity, no eviction) and surfaced a miss as a hard vLLMEngineDeadError(qwen3-vl mrope dereferences aNoneimage_grid_thw) that the retry net couldn't catch across the engine→API IPC. Dropping it is deployment-agnostic (a miss is impossible) and non-hacky. vLLM'smm_hashencoder cache still skips the expensive GPU re-encode for free — we only forgo the cheap IPC/CPU-reprocess dedup.Validated: color-codeword (Qwen3-VL-4B) under DP=2, no affinity / no cache reliance: 0 crashes, 0
data=None, multi-turn accumulation correct, reward ~0.84. Also confirmed under TP.This repo: every image emits a raw descriptor ref at every slot.
_descriptor_only_mm_datano longer strips the pointer (pixel_valueswere never present in v1, so the strip was both stale and the root cause of the descriptor-only/rebuild churn). Removed thematerialize_all_image_refsflag and the now-orphanedmaterialize_image_refs/materialize_kimi_image_refs.Original description
Summary
mmraw:v2raw multimodal refs inrenderers.mm_store, parsed asRawMMRefobjects withfamily,fingerprint,modality, hash, asset id, and adapter-owned payloadprime_raw_mm_itemenvelopes instead of processed image payloads for Qwen-VL and Kimi K2.5 image renderingimage_grid_thwfor Qwen,grid_thws/media token metadata for Kimi)Companion PRs
Notes
file://.../assets/images/...refs before rendering.Validation
uvx ruff@0.15.18 check .passed.uvx ruff@0.15.18 format --check .passed.uvx 'ty<0.0.22' check .exited 0; remaining diagnostics are warning-level advisories under the repo config.PYTHONPATH=/home/ubuntu/renderers uv run --no-project --active pytest -q tests/test_client.pypassed:14 passed./home/ubuntu/renderers,/home/ubuntu/verifiers, and/home/ubuntu/prime-rl-v1-raw-mm-offloadcompleted inference, env rollouts, train batch creation, trainer step 0, and decoded strict trainer-bound raw image refs.Update: review hardening (
e3c12e9)render_completion_updatemutating the caller'sprevious_multi_modal_datain place (shallow dict copy +setdefault().extend()); the merge now lives in a sharedmerge_multi_modal_datahelper inbase.pythat copies inner lists, and the bridge test asserts the previous sidecar is unmutated.mm_storeuses full sha256 content-addressed filenames and raises on undecodable base64.test_raw_layout_math_matches_image_processor: parity against the real Qwen3-VL-4B and Kimi-K2.5 (pinned revision) processors at rounding-boundary dimensions. Full suite:2182 passed.Update: merged main (
e64cc58)Reconciled with renderers
main(12 commits: Hy3/PrimeQwen3/LagunaXS21 configs, kept-tokens parsing, SFT stop-token opt-in, a qwen thinking-wrapper fix, andbdb96b0— main's own independent fix for the same bridge-mutation bug this PR'smerge_multi_modal_data()already covers).kimi_k25.py/qwen35.py/qwen3_vl.py: kept this branch's sharedmerge_multi_modal_data()helper over main's three duplicated inline fixes for the identical bug — same behavior, one implementation instead of three.test_multimodal.py: re-injected main's_skip_for_disabled_thinking_deviationskip guard into this branch's parallelrenderer_cases/processor_casesstructure at all 4 call sites; took main's tuple-basedprior_mm/prior_countsassertion over this branch's redundant duplicate check.test_client.py,__init__.py,configs.py: non-overlapping additions on both sides, no semantic conflict.client.py: no textual conflict (main's fix landed near, not on, this branch's rewritten_build_vllm_mm_features) — manually verified the mergedgenerate()is consistent (prompt_attr/mm_datanaming intact).Full suite:
2578 passed, 153 skipped, 1 xfailed.Update: unwrapped the ref payload (
7c58fd6)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 wrapper's 33% inflation bought nothing. The saving is small in offload mode (the payload is a shortfile://URI), but it keeps the ref format identical to the inline bundle, where the payload carries the image source and the wrapper cost ~32 KiB per image slot. Refs parse with a singlepartition(":")now that the payload contains colons.Update: synced with main (
1af6589)Merged renderers
main(5 commits: vLLM token-serving import fix #115, GLM tool-name validation #114, setuptools bump, ruff-action pin).One conflict, in
client.py: main's #115 updated the vLLMmm_serdeimport path inside the processed-tensor encoder that this PR deletes by design. Kept this branch's raw-ref_build_vllm_mm_features; main's fix applies to code that no longer exists here.Full suite: 2622 passed, 125 skipped, 1 xfailed.
Update: single raw-mode image route (
66505d2)Raw mode now accepts exactly one image source: an offloaded
file://URL. The bare-filesystem-path fallback was a second route the rest of the stack couldn't honor anyway — prime-rl's trainer-sidefile_uri_to_pathalready rejects non-file://sources and verifiers ingress only ever emitsas_uri(). Anything else raises the existing "requires offloaded file:// image assets" error.Note
High Risk
Large multimodal contract change across inference client, bridge merging, and three VL renderers; constructor API removal and mandatory offloaded image assets can break downstream integrations.
Overview
Multimodal inference path switches from embedding processed
pixel_valuesin the sidecar to JSON-safe raw image descriptors (prime_raw_mm_item) with layout metadata andfile://URIs. Qwen-VL, Qwen3.5, and Kimi K2.5 compute placeholder counts via baked layout math (or transformerssmart_resizefor Qwen) without running the image processor in the default mode.Adds
multimodal_outputon renderer config (rawdefault,processedfor SFT/training),renderers/mm_store(offload, fingerprints,mmrawrefs), andmerge_multi_modal_dataso bridge turns concatenate prior media without mutating the caller’s sidecar.generate()now builds vLLM features from raw refs only—removing renderer-specific torch/vLLM encoding—and expects offloaded images at every slot.Breaking / API: VL renderer constructors no longer take
processor=; processors lazy-load only formultimodal_output="processed".image_cache_maxis removed from VL configs. New optionalvisionextra for Pillow/torch when using processed mode. Tests requirefile://image fixtures for raw render parity.Reviewed by Cursor Bugbot for commit a49e0fc. Bugbot is set up for automated code reviews on this repo. Configure here.