Support v1 inline raw multimodal images - #3128
Open
eligotts wants to merge 44 commits into
Open
Conversation
/inference/v1/generate materializes every raw ref (no cache-only None branch); an unresolved ref is a hard error, not a silent None. Removes the cache-miss 409 path + helpers (_cache_only_mm_hashes/_is_missing_mm_cache_error/_missing_mm_cache_message). Bumps renderers/verifiers submodules to the matching cleanup commits. Deployment-agnostic; mm_hash encoder cache still skips re-encode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
renderers.mm_store dropped the split_mmraw_ref backcompat alias; use split_raw_mm_ref. Bump renderers submodule pin to the cleanup commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # deps/verifiers # src/prime_rl/orchestrator/orchestrator.py # src/prime_rl/trainer/batch.py # tests/unit/orchestrator/test_batch.py
# Conflicts: # packages/prime-rl-configs/src/prime_rl/configs/inference.py # packages/prime-rl-configs/src/prime_rl/configs/rl.py # src/prime_rl/entrypoints/rl.py
# Conflicts: # deps/verifiers
- Kimi adapter reads the actual processor layout instead of hard-equating it to the renderer constant; the fingerprint comparison is the single drift detector, matching the Qwen adapter. - Orchestrator validates every image ref's placeholder span lands on image-typed tokens before a sample ships, so offset drift anywhere upstream fails loudly instead of silently truncating wrong. - FakeDataLoader carries the mm stat counters; trainer metrics read them directly. Drop the duplicate apply_run_asset_env in the entrypoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
renderers: bridge sidecar aliasing fix, HF smart_resize import, full-hash asset filenames, layout parity tests. verifiers: ingress offload covers every image part shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # deps/renderers # deps/verifiers # packages/prime-rl-configs/src/prime_rl/configs/inference.py # packages/prime-rl-configs/src/prime_rl/configs/orchestrator.py # src/prime_rl/orchestrator/trajectories.py
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-pins to the commits where each companion branch merged its own origin/main (renderers e64cc58, verifiers 2b1627d03), ahead of merging main into this branch.
Reconciles the raw multimodal offload work with main's iter_trainable_branches dedup, mm_kwargs packing, MXFP8, and seq_lens contract: - trajectories.py: main's iter_trainable_branches() loop is authoritative; this branch's _validate_image_spans + mm_refs build spliced into it. - trainer/model.py: kept the ForwardPolicy generalization over main's image_grid_thw string check in forward() — Kimi K2.5 and Qwen VL need opposite position_ids behavior, which the key-presence check can't express. Restored the numpy import main's _routed_experts_row_size needs (auto-merge dropped it; caught by tests, not conflicts). - trainer/batch.py: took main's mm_kwargs bin-packing machinery, with a guard that raw-ref (mm_refs) samples never pack — with text or each other: their placeholder offsets are sample-relative and _materialize_bin carries only the first sample's refs, so packing would drop or misalign images. Rewrote main's packing test to assert this branch's contract. - trainer/rl/train.py: kept both mm_forward_policy threading (ours) and the [model.vlm] guard for multimodal samples (main's). - Bumped deps/renderers (e64cc58) and deps/verifiers (2b1627d03) to their own main-reconciled companion-PR heads and relocked. The verifiers bump crosses the interleaving-agents refactor (#2049), which renamed textarena's seat: migrated env.agent -> env.player in the two wordle rl.toml configs. Validation: uv lock --check clean; tests/unit minus GPU-only tests/unit/train: 393 passed; tests/unit/train collects (160 tests) without import errors.
The renderers library defaults multimodal_output to 'raw' (built for the RL offload path), but SFT's data path consumes processed pixel tensors straight from the renderer and has no raw-ref materializer — the library default would silently ship JSON descriptors into training. SFTConfig now defaults its renderer to 'processed' and rejects an explicit 'raw'.
Every request carries a raw ref for every image in its prompt (prior turns included), so a 20-turn rollout with 5 accumulated images paid ~100 materializations (read + sha256 + PIL decode + HF processor forward) where 5 would do — multiplied by group size hammering the same prompt images. vLLM's processor cache can't help: production happens in our handler before vLLM sees the request. Adds a byte-bounded, single-flight LRU in serving_tokens.py keyed by (raw_ref, expected_placeholder_length, processor_model_name) — content- addressed, so hits are sound and evicted entries can only go cold, never stale. _decode_raw_mm_kwargs now gathers all of a request's images concurrently instead of the sequential await loop, and single-flight collapses concurrent misses for one new image into one materialization. Failures are never cached; they propagate to all awaiters and the next request retries cleanly. One knob: PRIME_RL_MM_MATERIALIZE_CACHE_GB (default 2.0). 0 disables all bookkeeping and single-flight — byte-identical to the uncached path, and the kill switch. Logs hits/misses/bytes/evictions every 1000 lookups; monitor-run skill documents the signature.
…ions Post-merge audit findings (adversarial review of the main reconciliation): - forward(): the ForwardPolicy fallback inverted main's gate for callers that don't thread an adapter policy — SFT passes mm_kwargs without one, so Qwen-VL SFT got packed 1D position_ids and the model skipped its internal MRoPE construction. The no-policy default now reproduces the key-presence heuristic (image_grid_thw => model owns position ids); RL's explicit adapter policies are unaffected. - _MaterializedRefCache: the owner request's cancellation (client disconnect) set CancelledError on the shared single-flight future, poisoning every deduped awaiter — and a cancelled awaiter could cancel the future out from under the rest. Materialization now runs as a detached task and awaiters shield the shared future. - _is_multimodal_sample: also treat eager mm_kwargs samples as multimodal so main's packing compatibility machinery stays live-correct, not dead code guarded by prepare_sample's rejection alone. - multimodal_sample_error: raw mm_refs samples require mm_token_type_ids (the orchestrator always stamps them; trainer truncation and forward policies rely on them). - CI runs all of tests/unit on GPU runners (not just the CPU subset): adapted main's two mm_kwargs packer tests to the raw-ref contract (raw samples never pack, even within a run), added the missing seq_lens kwarg to the branch-only forward-policy test, extended the batch no-pack test to cover the mm+mm direction, and migrated main's new configs/ci/nightly-fft/wordle.toml to the verifiers textarena player seat. - Bumped verifiers for a docstring completeness fix.
Intermediate storage mode built off feat/v1-raw-mm-offload: the image processor stays out of the env worker (vLLM-front materialization + cache, trainer adapter re-materialization, renderer geometry math all kept), but images travel inline as base64 data URLs instead of offloaded file:// run assets. - Serving: materializes from the ref's inline payload (decode + hash verify); the materialize cache re-keys to (mm_hash, placeholder_len, model) so keys stay small now that the ref embeds the full image. - Trainer: RawImageMaterializer decodes inline bytes from the descriptor; MMImageRef drops the uri field (the data lives once, in the descriptor). - Removed the entire offload plumbing layer: [multimodal].offload_dir, MultimodalConfig, run_assets.py, VF_RENDERER_IMAGE_OFFLOAD_DIR launcher/SLURM exports, and missing_mm_image_policy + the adapters' synthesize_placeholder machinery — an inline image cannot go missing, so the zero-loss placeholder path has nothing to guard. - Rollout records and traces keep the inline base64 (documented in the monitor-run skill). - Pins deps/renderers and deps/verifiers to the inline companion branches. tests/unit minus GPU-kernel model tests: 482 passed.
Mirrors the offload branch's restoration of main's mm packing feature, which the merge had regressed to never-pack for raw-ref samples: can_add treats same-adapter-family raw-ref samples as pack-compatible (one family per micro batch is what the materializer enforces), and _materialize_bin merges refs across the bin's samples with placeholder offsets rebased to the packed token stream. Same run/LoRA gating, seq_lens boundaries, and modality alignment as main; the trainer-side adapter already materializes a list of refs into concatenated tensors. Packing tests restored to main's semantics (pack within run with rebased offsets, family mismatch splits, never across runs).
eligotts
marked this pull request as ready for review
July 27, 2026 23:42
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 e40dded. Configure here.
| expected_placeholder_length, | ||
| ) | ||
| except (TypeError, ValueError) as exc: | ||
| raise _MMImageRefError(str(exc)) from exc |
There was a problem hiding this comment.
Unknown family yields 500
Medium Severity
get_multimodal_adapter raises NotImplementedError for an unknown family, but _materialize_raw_image_ref_sync only maps TypeError/ValueError to _MMImageRefError. An unrecognized descriptor family therefore bypasses the invalid_mm_image_ref 400 path and surfaces as an unhandled 500 from serving.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e40dded. Configure here.
# Conflicts: # configs/basic/wordle/rl.toml # configs/ci/nightly-fft/wordle.toml # deps/verifiers # examples/basic/wordle/rl.toml
The renderer-side layout dataclasses are now the single canonical knob list per family, so the adapters stop re-listing the fields: fingerprints come from qwen_layout_from(image_processor).fingerprint() / kimi_layout_from(...).fingerprint(), and the kimi materialize path reads patch shapes through the same extractor. Deletes the hand-duplicated _processor_value / _processor_layout helper stacks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Catches this branch up ~23 commits, including the vLLM 0.26 move (serving_tokens imports from scale_out.token_in_token_out, mm_input takes a MultiModalKwargsItems wrapper, online_renderer rename) resolved the same way as the offload branch, with the inline raw_image_data decode path re-applied on top. Takes main's agent metrics rename (#3165) and drops tests main superseded. Submodule pins advance to the companion PR merges; uv.lock relocked with submodules at their merged pins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Render and materialize are bound to the same model.name, so materialize stops re-litigating layout policy: processor_fingerprint and the fingerprint comparisons are deleted from both materialize paths (vLLM front + trainer) along with the RawMMItem.layout_fingerprint field. The output-level grid and placeholder-length asserts stay — they compare processor output against the ref payload with values already in hand, and turn checkpoint skew into a clean per-request error instead of an engine crash or silent training corruption. Adapters read the knobs they still need straight off the live processor; the renderers-side layout extractors are config-JSON-only now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RawMMItem mirrors only what its consumers use now: family (adapter routing), the image source, and the adapter-owned payload. The modality, raw_ref, and vllm_modality mirror fields had zero readers — modality lives on the container key and the mmraw: ref, and vllm_modality is consumed at the renderers client. Drops the now-unused _optional_str helper and the dead envelope keys from test fixtures. deps/renderers advances to the matching envelope change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eligotts
force-pushed
the
feat/v1-inline-mm
branch
from
August 4, 2026 20:06
a677aff to
5608765
Compare
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.


Summary
Intermediate multimodal storage mode — the inline bundle. Keeps the load-bearing architecture of the raw multimodal redesign — the image processor runs nowhere near the env worker: renderers compute token layout via pure geometry math,
PrimeRlServingTokensmaterializes pixels in front of vLLM (with the byte-bounded single-flight materialize cache), and the trainer re-materializes from the same source via family adapters — but images travel inline as base64 data URLs through messages, traces, transport, and refs. No shared image directory, no offload.Companion inline PRs:
Design:
mmraw:refs embed the inline image source; materialization decodes and sha256-verifies the inline payload. Expected descriptor failures—including hash, fingerprint, grid, and placeholder-length mismatches—returninvalid_mm_image_ref400s. The byte-bounded single-flight cache keys on a digest of the complete ref plus its modality, outer hash, placeholder length, processor model, and trust setting, so a distinct or stale descriptor cannot alias a validated entry. Cache size is controlled byPRIME_RL_MM_MATERIALIZE_CACHE_GB.RawImageMaterializerdecodes inline bytes from the descriptor.MMImageRefdrops theurifield — the image lives once, in the descriptor.[multimodal].offload_dir/MultimodalConfig,utils/run_assets.py,VF_RENDERER_IMAGE_OFFLOAD_DIRlauncher + SLURM plumbing, andmissing_mm_image_policy+ the adapters'synthesize_placeholdermachinery — an inline image cannot disappear, so the zero-loss placeholder path has nothing to guard.multimodal_output='processed'with a hard guard against'raw'(storage-independent).results.jsonlto grow with turn count).PATHand credentials.Relationship to the offload bundle
This branch is cut from
feat/v1-raw-mm-offload(#2836) and targets main independently. The offload bundle (renderers #89 / verifiers #1746 / prime-rl #2836) is the follow-on that layers content-addressedfile://storage on top: ingress offload walker, offload-dir plumbing, missing-image policy, and disk-backed refs.Validation
tests/unitminus GPU-kernel model tests — 482 passed (398non-train +84train non-models);uv lock --checkclean; ruff check + format clean.uv run pytest -q tests/unit/inference/test_serving_tokens.py— 21 passed; touched-file ruff check and format check clean.Update: hardening + restored main's multimodal packing (
7d85b170,fb6ef306)7d85b170): subprocess env inheritance restored inrl_local's orchestrator launch; the materialize-cache key now digests the full ref ((sha256(raw_ref), modality, mm_hash, placeholder_len, model, trust_remote_code)) so a distinct descriptor can never alias an already-validated entry; adapter validation errors map toinvalid_mm_image_ref400s.fb6ef306): the merge had left raw-ref multimodal samples never packing, regressing main's feature. Same-family raw-ref samples now pack with each other and with text from the same run/LoRA;_materialize_binmerges refs with placeholder offsets rebased to the packed stream. Mirrors the offload branch's fix; tests restored to main's semantics.Validation:
tests/unitminus GPU-kernel model tests: 484 passed.Update: unwrapped the ref payload (
603118b/e40dded5)raw_mm_refno longer base64-wraps its payload — it serializes compact JSON directly. 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); the base64 wrapper instead inflated the whole payload by a third, and in inline mode the payload contains the image.Measured on a 75 KiB JPEG: 130.3 KiB → 97.9 KiB per image slot on the wire (the 97.7 KiB data URL plus ~200 bytes of metadata/escapes) — a 25% cut, applied to every image slot of every request. Refs now parse with a single
partition(":")since the payload contains colons; the base64-alphabet guard retired with the wrapper.Update: merged main — vLLM 0.26 + agent metrics (
0722181f3)Merged prime-rl main (~23 commits), catching this branch up to the offload bundle's baseline: the vLLM 0.26 move (
serving_tokensimports fromscale_out.token_in_token_out,mm_inputtakes aMultiModalKwargsItemswrapper,online_rendererrename) resolved identically to #2836 with the inlineraw_image_datadecode path re-applied on top, plus main's agent metrics rename (#3165). Submodule pins advance to the companion PR merges (renderers #11027fe3c6, verifiers #212010da86af0);uv.lockrelocked with submodules at their merged pins. Unit suite: 411 passed.Update: trust the train-time checkpoint contract at materialize (
8e9c8c75a)Render and materialize are bound to the same
model.name, so materialize stops re-litigating layout policy (companion to renderersce7078a):processor_fingerprintand the fingerprint comparisons are deleted from both materialize paths (vLLM front + trainer), along withRawMMItem.layout_fingerprintand the adapter-protocol method.invalid_mm_image_referror / trainer exception instead of an engine crash or silent training corruption.merge_size,patch_size) straight off the live processor; the renderers-side extractors are config-JSON-only now.Materialize is now: read image → verify content hash →
image_processor(images=…)→ grid/length assert → pack. Unit suite: 411 passed.Update: pruned
RawMMItemto what materialize reads (56087658b)Companion to renderers
20b3f2d:RawMMItemkeepsfamily(adapter routing), the image source, and the adapter-ownedpayload; the unreadmodality/raw_ref/vllm_modalitymirror fields and the now-orphaned_optional_strhelper are gone. Unit suite: 411 passed.Note
High Risk
Large cross-cutting change to multimodal data paths (inference serving, orchestrator transport, trainer batching/forward) with strict validation assumptions; regressions would surface as training failures or silent image/token misalignment rather than isolated UI bugs.
Overview
This PR switches v1 multimodal RL to inline base64 image descriptors end to end instead of shipping preprocessed pixel tensors through transport. Orchestrator samples carry lightweight
mm_refs(descriptor + hash + placeholder span); inference and trainer each decode and verify bytes locally through family adapters (qwen_vl,kimi_k25).Inference materializes every raw ref in
PrimeRlServingTokensbefore vLLM sees the request, with a byte-bounded LRU cache (PRIME_RL_MM_MATERIALIZE_CACHE_GB, default 2 GiB), single-flight dedup, andinvalid_mm_image_ref400s on hash/grid/placeholder mismatches. Trainer usesRawImageMaterializerat batch load; packing groups same-family raw-ref samples and rebases image offsets in packed streams. Truncation cuts only at whole-image boundaries; orchestrator validates placeholder spans againstmm_token_type_idsbefore samples ship.Config/docs: SFT rejects
multimodal_output='raw'and defaults to'processed'; RL launcher preserves inherited subprocess env for orchestrator/trainer/inference. Docs and monitor-run skill cover inline payloads, cache metrics, and larger rollout artifacts.Reviewed by Cursor Bugbot for commit 8e9c8c7. Bugbot is set up for automated code reviews on this repo. Configure here.
Update: current-main integration (
6353659ae)Merged prime-rl
mainatf50ec5c4c. Resolved the currentplayer.harness/player.runtimeconfig shape, regenerated the two stale lockfile metadata lines, and pinned the companion PR heads exactly: renderers #110 at50ce3c3e, verifiers #2120 at1e35f768b.Validation after the merge:
uv lock --check, touched-file Ruff, and format checks passed; 48 targeted multimodal serving, batch, raw-ref packing, and schema tests passed. One unrelated trainer progress test could not import in the local CPU-only environment becauseflash_attnis not installed; GitHub CPU/GPU checks cover the full environment.Update: fingerprints derived from renderer layout specs (
d453dfa82)Companion to renderers
9208b40: the renderer-side layout dataclasses are now the single canonical knob list per family, so the adapters stop re-listing the fields.processor_fingerprintis one line per family —qwen_layout_from(image_processor).fingerprint()/kimi_layout_from(...).fingerprint()— and the kimi materialize path reads patch shapes through the same extractor. Deletes the hand-duplicated_processor_value/_processor_layouthelper stacks that previously had to be kept in sync with the renderers by hand. Fingerprint values are unchanged. Unit suite: 405 passed.