Support v1 raw multimodal image offload - #2836
Conversation
a574785 to
4e78f06
Compare
4e78f06 to
2f5eacc
Compare
2f5eacc to
034e83e
Compare
bfc45cc to
b7903f6
Compare
/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
| max_concurrent_runs: int = Field(1, ge=1) | ||
| """Maximum number of concurrent runs to allow. If 1, only one run may run at a time.""" | ||
|
|
||
| missing_mm_image_policy: MissingMMImagePolicy = "placeholder_zero_loss" |
There was a problem hiding this comment.
can we put this in the multi modal config ?
| def build_run_asset_env( | ||
| output_dir: Path, | ||
| multimodal: MultimodalConfig | None = None, | ||
| base: Mapping[str, str] | None = None, | ||
| ) -> dict[str, str]: | ||
| """Resolve the environment used by subprocesses that share run image assets. | ||
|
|
||
| Prime-RL config owns the multimodal image offload path. Env vars are only the | ||
| transport used by verifiers/renderers running in subprocesses. | ||
| """ | ||
|
|
||
| env = dict(os.environ if base is None else base) | ||
| config = multimodal or MultimodalConfig() | ||
|
|
||
| env[IMAGE_OFFLOAD_DIR_ENV] = str(resolve_image_offload_dir(output_dir, config, env)) | ||
|
|
||
| return env | ||
|
|
||
|
|
||
| def apply_run_asset_env(output_dir: Path, multimodal: MultimodalConfig) -> None: | ||
| if os.environ.get(IMAGE_OFFLOAD_DIR_ENV): | ||
| return | ||
| os.environ.update(build_run_asset_env(output_dir, multimodal=multimodal)) |
There was a problem hiding this comment.
not a fan of seting up env var, why can't we jsut read from the env var instead and dinamycally change it in the code if the env var is not present ?
| inherited_env = dict(os.environ) | ||
| writer_run_asset_env = build_run_asset_env(config.orchestrator.output_dir, multimodal=config.multimodal) |
- 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>
Tech Spec: Serving-Layer Materialization Cache for Raw Multimodal Image RefsStatus: approved design, ready to implement 1. Background: the raw multimodal offload architectureYou need this context to understand why the cache is needed and where it sits. Skim the
How images flow through the systemMultimodal RL rollouts (e.g. browser agents taking screenshots) used to ship processed
Key vocabulary:
2. The problemRead
None of vLLM's own caches can fix this, because the work happens in our handler 3. The three-cache model (why this cache is not redundant)There are three caches in the pipeline. Each one's hit avoids a different expensive step.
Produce → move → encode. In the old processed-tensor world, vLLM's processor cache Composition facts your implementation can rely on (verified against vLLM 0.24 in
4. Rejected alternatives (do not build these)
5. DesignAn in-process, byte-bounded, single-flight LRU in 5.1 Key
Note the key is deliberately stricter than 5.2 Value and byte accountingThe finished For sizing, reuse vLLM's own helper rather than writing tensor-walking code: from vllm.multimodal.cache import MultiModalCache
nbytes = MultiModalCache.get_item_size(item)(Verify this import against the installed vLLM at implementation time; it's present in 5.3 Structure and concurrency modelclass _MaterializedRefCache:
"""Byte-bounded LRU of materialized raw image refs, with single-flight misses.
All mutation happens on the event loop thread (materialization itself runs in a
worker thread via asyncio.to_thread, but insertion/eviction/lookup happen in the
async caller) — so no lock is needed. Do not touch this cache from sync code.
"""
def __init__(self, max_bytes: int) -> None:
self.max_bytes = max_bytes
self._items: OrderedDict[Key, tuple[Any, int]] = OrderedDict() # key -> (item, nbytes)
self._inflight: dict[Key, asyncio.Future] = {}
self._bytes = 0
self.hits = 0
self.misses = 0
self.evictions = 0
Eviction semantics (the part reviewers should verify): eviction is invisible. Keys 5.4 Aliasing guard (subtle, important)vLLM's injection path replaces elements of the kwargs lists we pass to
5.5 Concurrency within a requestReplace the sequential loop in 5.6 Configuration knobOne knob: max cache bytes, expressed in GiB.
5.7 Observability
6. Implementation planAll in
Do not touch: the adapters, 7. TestingRepo conventions (from Add, in rough priority order:
For sizing in tests, monkeypatch Run: 8. Validation and rollout
9. Conventions checklist (from
|
| What | Where |
|---|---|
| Target handler + current per-request materialization | src/prime_rl/inference/vllm/serving_tokens.py:218,274,362 |
| Family adapters (materialize_for_vllm, placeholder-length validation) | src/prime_rl/multimodal/adapters/{qwen_vl,kimi_k25}.py |
Ref format (mmraw: build/parse) |
deps/renderers/renderers/mm_store.py::raw_mm_ref,split_raw_mm_ref |
| Client that emits refs every request | deps/renderers/renderers/client.py::_build_vllm_mm_features |
| Ingress offload (content-addressed file store) | deps/verifiers/verifiers/utils/multimodal.py |
| DP-rank pinning (why one frontend cache serves all ranks) | src/prime_rl/utils/client.py:22-29,246 |
| vLLM item sizing helper | .venv/.../vllm/multimodal/cache.py::MultiModalCache.get_item_size |
| vLLM external-kwargs cache injection (P0→P1 dedup we compose with) | .venv/.../vllm/v1/engine/input_processor.py ("Inject pre-processed mm_kwargs…") |
| vLLM mm cache knobs + memory math | .venv/.../vllm/config/multimodal.py:123-137 |
| Existing test harness to extend | tests/unit/inference/test_serving_tokens.py::test_materialize_raw_image_ref_uses_generic_family_payload |
| Monitoring doc to update | skills/training/monitor-run/SKILL.md ("Multimodal image offload checks") |
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.
The main merge resolved the mm packing conflict too conservatively: raw-ref samples never packed, regressing main's feature where multimodal samples pack with text spans and compatible mm samples from the same run/LoRA. Restore it for the mm_refs representation: - can_add: raw-ref samples are pack-compatible when they share an adapter family (the materializer enforces one family per micro batch; grids and image sizes vary freely within a family) — the simpler analogue of the eager path's dtype/trailing-dim check. The run/LoRA gate applies as on main. - _materialize_bin: merges refs across the bin's samples, rebasing each sample's placeholder offsets by its start position in the packed token stream. mm_token_type_ids already concatenate with zero-fill and seq_lens already preserve boundaries; the trainer-side adapter already materializes a list of refs into concatenated tensors, so the packed batch reaches forward() shaped exactly like main's packed eager batches. - Restored main-shaped packing tests (pack within run with rebased offsets, family mismatch splits, never across runs). Also map adapter validation errors (fingerprint/grid/placeholder-length mismatches) to invalid_mm_image_ref 400s in serving_tokens instead of letting them escape as 500s — ported from the inline bundle's review.
| f"missing_uris={missing_uris or ['<unknown: disappeared during read>']}, " | ||
| f"uris={refs.uris})" | ||
| ) | ||
| return self._materialized_mm_parts(materialized) |
There was a problem hiding this comment.
Missing image zeros packed batch
Medium Severity
placeholder_zero_loss clears the entire microbatch loss_mask and advantages when any raw image file is missing. Raw-ref samples now pack with text and same-family peers, so one missing asset also drops loss for every other sample sharing that pack, including ones whose images are present.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 6a1a54b. Configure here.
Renderers no longer base64-wrap the ref payload (parity with the inline bundle, where the wrapper cost ~32 KiB per image slot). Adopts the inline bundle's materialize-cache key so both bundles key identically: digest the whole ref rather than trusting the caller's outer metadata, and include feature_modality and trust_remote_code. The digest matters less here (an offload ref is a short URI, so keying on the ref string was already sound) but trust_remote_code is a real gap — it selects a different image processor, so two values could previously alias one cache entry. Ports the inline anti-aliasing test. The test fixture now seeds image content explicitly: it previously colored images by filename length, so two same-length names produced byte-identical images and a forged-hash case couldn't be expressed.
The orchestrator subprocess inherited os.environ only as a side effect of build_run_asset_env returning dict(os.environ) plus the image dir — the same implicit coupling that broke inheritance outright in the inline bundle when that helper was removed. Spread inherited_env first, as the trainer and inference launches already do, so the image-dir helper only has to supply the var it owns.
| expected_placeholder_length, | ||
| ) | ||
| except (TypeError, ValueError) as exc: | ||
| raise _MMImageRefError(str(exc)) from exc |
There was a problem hiding this comment.
Unknown family returns server error
Medium Severity
get_multimodal_adapter raises NotImplementedError for an unknown family, but _materialize_raw_image_ref_sync only maps TypeError and ValueError to invalid_mm_image_ref 400s. An unrecognized family therefore escapes as a 500 instead of a client validation error.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit fd3f379. Configure here.
Picks up 18 main commits, notably the vLLM 0.26 bump (#3166), the task-centric orchestrator config restructure (train.source.env), RAE / role-conditioned advantage estimation (#3136), and the verifiers bump that adopts runtime-on-agent and the textarena player seat (#3137). Resolutions: - serving_tokens.py: vLLM 0.26 moved the disagg serving modules under scale_out.token_in_token_out and changed mm_input() to take MultiModalKwargsItems. Adopted both (the wrapper is an API change, not an eager-path detail) while leaving out main's decode_mm_kwargs_item / MultiModalKwargsItem imports, which only serve the processed-tensor path this branch replaces with raw-ref materialization. - wordle configs: main has since adopted the textarena `player` seat and restructured to [orchestrator.train.source.env], so main's versions supersede this branch's earlier env.agent -> env.player migration. - configs/orchestrator.py + rl.py: insertion collisions only — kept the multimodal block alongside main's rollout_transport field and main's removal of the deprecated [[env]]/[sampling] shim. - tests/unit/orchestrator/test_qwen3_vl_e2e.py: stays deleted; main only refreshed its vLLM import paths, but it round-trips the eager processed features payload that v1 rejects by design. Re-pins deps/renderers (1af6589) and deps/verifiers (0e4166608), both freshly merged with their own mains.
The merge brought a new deps/research-environments pin, but this worktree still had the previous commit checked out, so the regenerated lockfile resolved against a stale tree — dropping charxiv-v1 / mmmu-pro-v1 and downgrading bfcl-v3-v1, which failed CI's `uv sync --locked`. Also bumps deps/verifiers for the ruff 0.16 import sort in its multimodal client type test.
This branch's verifiers pin is ahead of the one prime-rl main tracks, and includes the runtime-on-agent work that renamed Trace.timing.generation to .agent (identical shape: duration + model/harness splits). The orchestrator's timing metrics still read .generation, which surfaced as 'Timing' object has no attribute 'generation' in the integration tests. Reads the renamed field but keeps the emitted metric names (time/generation, generation/model, generation/harness) unchanged so W&B dashboards and the monitor-run skill stay valid. PHASES did double duty as both metric names and trace field names — those have now diverged, so the trace-side lookup goes through an explicit TRACE_PHASES mapping.
| # a cancelled request (client disconnect) must neither poison the future | ||
| # for deduped awaiters nor cancel it out from under them. | ||
| asyncio.get_running_loop().create_task(_materialize_and_resolve()) | ||
| return await asyncio.shield(future) |
There was a problem hiding this comment.
Detached cache task unreferenced
Medium Severity
_MaterializedRefCache fires materialization with create_task and only keeps the shared Future in _inflight, not the Task. Under asyncio’s weak task refs, that task can be garbage-collected before it resolves, leaving awaiters blocked on a future that never completes.
Reviewed by Cursor Bugbot for commit 95afd1f. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 37ff96a. Configure here.
| mm_kwargs: dict[str, list[Any]] = {feature_modality: [] for feature_modality in features.mm_hashes} | ||
| for (feature_modality, _, _, _), item in zip(flat, decoded, strict=True): | ||
| mm_kwargs[feature_modality].append(item) | ||
| return mm_kwargs |
There was a problem hiding this comment.
Kimi modality key mismatch
High Severity
_decode_raw_mm_kwargs always inserts materialized items under the request feature_modality key, while KimiK25Adapter.materialize_for_vllm returns a vision_chunk-tagged MultiModalKwargsItem. vllm_modality is parsed on RawMMItem but never used to rekey mm_kwargs / hashes / placeholders, so Kimi requests either fail MultiModalKwargsItems construction or never reach the vision path.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 37ff96a. Configure here.


Summary
Wires Prime-RL v1 training onto the raw multimodal image offload contract used by the companion Renderers and Verifiers PRs.
Companion PRs:
Current design:
VF_RENDERER_IMAGE_OFFLOAD_DIR; they rewrite image parts to sharedfile://assets and emit JSON-safe raw descriptors.raw_image_urias the sole image locator plus adapter-owned metadata (family,layout_fingerprint,payload).raw_image_idwas removed to avoid two sources of truth.mmraw:<base64-json-payload>refs carryingraw_image_uri, hash, family, fingerprint, modality, and payload. Raw-mm ref and descriptor version markers were removed because this branch has no compatibility contract yet.VF_RENDERER_IMAGE_OFFLOAD_DIR.file://URIs throughMMRefs;build_mm_refsnow builds those URIs from the raw descriptors instead of rescanning messages.Noneslots, inline base64 storage, and root-plus-id lookup paths are not supported.Submodule pins:
a49e0fc(branch merged with renderersmainat5904fa2)9bc3cc36(branch merged with verifiersmainat5885ab9c)Latest merge (
7ea71a96f): merged prime-rlorigin/main(22 commits, incl. the dispatch-gate fix #2937, 1-indexed steps #2896, vLLM 0.24 #2921, and the ExperimentalConfig removal #2931 — resolved in favor of keeping[multimodal]without the placeholders). Both dep submodules were merged with their ownmainand re-pinned.Review hardening (latest push)
MMRefsrestructured from a modality-keyed descriptor dict + parallel lists toimages: list[MMImageRef](descriptor, hash, uri, placeholder range per image). Alignment is structural, so the count validators collapsed to one zip atbuild_mm_refs; truncation keeps a prefix of whole images and never splits a placeholder.build_mm_refsenforces sorted, non-overlapping placeholders; the orchestrator additionally validates every image ref's placeholder span lands on image-typed tokens (mm_token_type_ids == 1) before a sample ships, so offset drift anywhere upstream fails loudly.image_urlnested/direct,imagedirect, typed pydantic parts); renderers hard-requirefile://sources.Validation
uv run ruff check renderers/configs.py renderers/base.py renderers/client.py renderers/qwen3_vl.py renderers/qwen35.py renderers/kimi_k25.py renderers/mm_store.py renderers/__init__.py tests/test_renderer_config.py tests/test_multimodal_output_modes.py tests/test_client.pyuv run ruff format --check renderers/configs.py renderers/base.py renderers/client.py renderers/qwen3_vl.py renderers/qwen35.py renderers/kimi_k25.py renderers/mm_store.py renderers/__init__.py tests/test_renderer_config.py tests/test_multimodal_output_modes.py tests/test_client.pyuv run pytest tests/test_renderer_config.py tests/test_client.py::test_generate_serializes_raw_mm_refs tests/test_multimodal_output_modes.py -q->38 passedruff check,ruff format,ty (ci parity)passeduv run ruff check src/prime_rl/entrypoints/rl.py src/prime_rl/inference/server.py src/prime_rl/inference/vllm/serving_tokens.py src/prime_rl/multimodal/schema.py src/prime_rl/orchestrator/trajectories.py src/prime_rl/utils/mm.py tests/unit/inference/test_serving_tokens.py tests/unit/orchestrator/test_batch.py tests/unit/utils/test_mm.pyuv run pytest tests/unit/utils/test_mm.py tests/unit/inference/test_serving_tokens.py::test_materialize_raw_image_ref_uses_generic_family_payload tests/unit/orchestrator/test_batch.py::test_prepare_sample_rejects_overlong_raw_mm_refs->4 passeduv run pytest tests/unit/inference/test_serving_tokens.py::test_materialize_raw_image_ref_uses_generic_family_payload->1 passedgit diff --check,uv lock --check, anduv sync --all-extras --lockedpassed on the branch after mergingorigin/main.2182 passed(incl. Kimi-K2.5 parity/byte-parity against the pinned processor), verifiers839 passed(pre-existingtest_envs/test_opencode_rlm_envfailures excluded, reproduced on the base branch), prime-rltests/unitminus GPU-onlytests/unit/train410 passed.Latest sync:
/home/ubuntu/prime-rl-mainand merged latest prime-rlorigin/main(f19ba721a) into this branch.9b3e7ee, which contains both this PR's previous Verifiers pin (22c7cf4c) and current main's Verifiers pin (22d6333a). This resolves thedeps/verifiersmerge conflict and satisfieslean-v1'sverifiers>=0.1.15.dev402requirement.a7953b9, which contains main's renderers pin (a5efbb), the processed multimodal renderer output mode, and the cleaned lockfile diff.uv.lockfor the optional Renderersvisionextra metadata so CI'suv sync --all-extras --lockedpath is in sync.Update: merged main (
2119bdbd8)Reconciled with prime-rl
main(79 commits: MXFP8, FA3-default, CP for hybrid+VLM SFT, VLM SFT, Task/TaskData, NIXL/ModelExpress, theseq_lenspacked-sample contract,get_trace_pathrollout-save refactor, deployment-config restructure, v0.7.0). Both companion PRs were reconciled with their own mains first and re-pinned (rendererse64cc58, verifiers2b1627d03).Resolution decisions:
orchestrator/trajectories.py: main'siter_trainable_branches()(fork-dedup) loop is authoritative; this branch's_validate_image_spans+build_mm_refsspliced into it unchanged.trainer/model.py: kept this branch'sForwardPolicyadapter over main's"image_grid_thw" not in mm_kwargsstring check inforward()— Kimi K2.5 (pass_position_ids_with_mm=True) and Qwen VL (False, requires_mm_token_type_ids=True) need opposite behavior, which a key-presence check cannot express. Main's MXFP8/CP/quantization work merged around it untouched.trainer/batch.py: took main's new mm_kwargs bin-packing machinery, plus a guard that raw-ref (mm_refs) samples never pack — with text or each other — because their placeholder offsets are sample-relative and_materialize_bincarries only the first sample's refs; packing would silently drop or misalign images (main's own packing test packed a text sample into an mm bin, which would have dropped the refs on this branch's path). Rewrote that test to assert the raw-ref contract.numpyintrainer/batch.py(main's_routed_experts_row_sizerewrite) andFinishReason/Usagein verifiersgraph.py.env.agent→env.playerinconfigs/basic/wordle/rl.tomlandexamples/basic/wordle/rl.toml(prime-rl main will need the same migration at its next verifiers bump).seq_lens/mm-truncation interaction flagged in review scoping is safe:prepare_samplecomputesseq_lens=[len(input_ids)]after_truncate_mm_refs.Validation:
uv lock --checkclean;uv sync --all-packages --all-extrasclean;tests/unitminus GPU-onlytests/unit/train: 393 passed;tests/unit/traincollects (160 tests) with no import errors. Companion suites: renderers2578 passed, verifiers full suite passing (minusPRIME_API_KEY-gated e2e).Update: SFT renderer default + serving-layer materialize cache (
a2fdf6a8,76761553)multimodal_output='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.SFTConfignow defaults its renderer to'processed'and hard-rejects an explicit'raw'. RL keeps the library default of'raw', untouched.PrimeRlServingTokensbefore vLLM sees the request. Added a byte-bounded, single-flight LRU keyed by(raw_ref, expected_placeholder_length, processor_model_name)(content-addressed — hits are provably identical work; evicted entries go cold, never stale)._decode_raw_mm_kwargsnow gathers a request's images concurrently instead of sequentially. Failures are never cached. One knob:PRIME_RL_MM_MATERIALIZE_CACHE_GB(default 2.0,0disables — byte-identical to the uncached path, the kill switch). Logsmm materialize cache: hits/misses/hit_rate/bytes/evictionsevery 1000 lookups; the monitor-run skill documents the signature. Four unit tests: hit-skips-work, byte-budget eviction, failure-not-cached, single-flight.Validation: full
tests/unitminus GPU-onlytests/unit/train: 397 passed.Update: post-merge audit fixes (
e0c22ee5)An adversarial audit pass over the reconciliation (independent per-repo reviews diffing the merge against both parents, plus the first CI run) surfaced and fixed:
forward()'s no-policyForwardPolicy()fallback inverted main'simage_grid_thwgate — SFT callers don't thread an adapter policy, so multimodal SFT would have passed packed 1Dposition_idsand silently skipped the model's internal MRoPE construction. The fallback now reproduces the key-presence heuristic; RL's explicit adapter policies unaffected.CancelledErroron the shared single-flight future, spuriously cancelling every deduped awaiter. Materialization now runs as a detached task; awaiters shield the shared future.tests/uniton GPU runners, not the CPU subset validated locally: adapted main's two eager-mm_kwargspacker tests to the raw-ref contract (raw samples never pack, even within a run —_mm_samplenow carriesmm_refs), added the missingseq_lenskwarg to the branch-only forward-policy test, extended the batch no-pack test to the mm+mm direction, and migrated main's newconfigs/ci/nightly-fft/wordle.toml(added after the merge point; CI tests the PR merged with latest main) to theenv.playerseat. Merged main again (45488ca04) to pick that file up._is_multimodal_samplealso recognizes eagermm_kwargssamples (keeps main's packing-compatibility machinery live-correct);multimodal_sample_errorrequiresmm_token_type_idson rawmm_refssamples.Audit verdicts elsewhere: renderers merge CLEAN (independent re-verification of every conflict site, 182-test focused suite + full 2578 green); verifiers merge CLEAN (the
SkipJsonSchemaadoption was verified necessary — the plain annotation breaksTrace.model_json_schema()on the numpy field).Validation:
tests/unitminustests/unit/train/models: 484 passed locally (tests/unit/train/modelskernels segfault on the local Blackwell box in a mamba backward test untouched by this PR — green in CI).Update: restored main's multimodal packing (
6a1a54bc)The main merge had resolved the mm-packing conflict too conservatively — raw-ref samples never packed, regressing main's feature where multimodal samples pack with text spans and compatible mm samples from the same run/LoRA. Restored for the
mm_refsrepresentation:can_add: raw-ref samples are pack-compatible when they share an adapter family (the materializer enforces exactly one family per micro batch; grids/sizes vary freely within a family) — the simpler analogue of main's eager dtype/trailing-dim check. Run/LoRA gating,seq_lensboundaries, and cross-rank modality alignment as on main._materialize_bin: merges refs across the bin's samples, rebasing each sample's placeholder offsets by its start position in the packed token stream. The trainer-side adapter already materializes a list of refs into concatenated tensors, so packed batches reachforward()shaped exactly like main's packed eager batches.Also mapped adapter validation errors (fingerprint/grid/placeholder-length mismatches) to
invalid_mm_image_ref400s inserving_tokensinstead of escaping as 500s (ported from the inline bundle's review).Update: ref-format and cache-key parity with the inline bundle (
7c58fd6/fd3f379d)Three parity changes so the offload and inline bundles differ only in image storage:
raw_mm_refserializes compact JSON instead of base64-wrapping it. Worth little here (the payload is a shortfile://URI) but it keeps the ref format identical to the inline bundle, where the wrapper cost ~32 KiB per image slot.feature_modalityandtrust_remote_code. The digest matters less here (keying on a short content-addressed ref was already sound), buttrust_remote_codecloses a real gap: it selects a different image processor, so two values could previously alias one cache entry. Ported the anti-aliasing test.os.environonly as a side effect ofbuild_run_asset_envreturningdict(os.environ)— the same implicit coupling that broke inheritance outright in the inline bundle when that helper was removed.inherited_envis now spread first, as at the trainer and inference launches.Also fixed a test-fixture flaw found while porting:
_mm_featurescolored images by filename length, so two same-length names produced byte-identical images and a forged-hash case couldn't be expressed. It now seeds content explicitly.Update: synced with main (
2c3f6d6e8)All three repos in this bundle re-merged with their own mains.
prime-rl (18 commits): vLLM 0.26 bump (#3166), task-centric orchestrator config restructure (
train.source.env), RAE / role-conditioned advantage estimation (#3136), verifiers bump adopting runtime-on-agent + textarenaplayer(#3137).serving_tokens.py: vLLM 0.26 moved the disagg serving modules underscale_out.token_in_token_outand changedmm_input()to takeMultiModalKwargsItems. Adopted both — the wrapper is a genuine API change, not an eager-path detail — while leaving out main'sdecode_mm_kwargs_item/MultiModalKwargsItemimports, which only serve the processed-tensor path this branch replaces.playerseat and restructured to[orchestrator.train.source.env], so main's versions supersede this branch's earlierenv.agent→env.playermigration.configs/orchestrator.py+rl.py: insertion collisions only — keptmultimodalalongside main'srollout_transportand main's removal of the deprecated[[env]]/[sampling]shim.test_qwen3_vl_e2e.pystays deleted (main only refreshed its vLLM import paths; it round-trips the eager processed-features payload v1 rejects by design).Validation:
tests/unitminus GPU-only model kernels: 409 + 84 passed on vLLM 0.26;uv lock --checkclean; ruff clean.Note
High Risk
Touches the full RL multimodal path (transport, packing/truncation, inference materialization, trainer forward) and depends on companion renderer/verifier behavior; misaligned refs or offload paths can fail runs or silently mask loss via placeholders.
Overview
Prime-RL v1 multimodal RL now uses a raw image offload contract aligned with renderers/verifiers: images land once under a run-scoped directory, training samples carry
mm_refs(descriptor +file://URI + placeholder span) instead of encodedmm_kwargs, and processed pixel tensors are rejected on the v1 path.Config and launcher: Shared
[multimodal].offload_dirpropagates to trainer, orchestrator, and inference; the RL launcher setsVF_RENDERER_IMAGE_OFFLOAD_DIR(protected fromenv_vars) for the orchestrator and SLURM templates. Trainermissing_mm_image_policycan zero-loss placeholder when assets disappear; SFT defaults renderer output toprocessedand rejectsraw.Inference:
PrimeRlServingTokensmaterializes every raw ref (hash/fingerprint checks, Qwen VL + Kimi K2.5 adapters) before vLLM, with a byte-bounded LRU cache (PRIME_RL_MM_MATERIALIZE_CACHE_GB) andinvalid_mm_image_ref400s on validation failures.Trainer/orchestrator:
build_mm_refs+ placeholder-span validation before samples ship; truncation/packing respect whole-image boundaries and rebase offsets when packing same-family refs.RawImageMaterializerandForwardPolicydrive trainer-side materialization and correct position-id /mm_token_type_idsbehavior per family. Metrics addtime/mm_materializeand image materialize/placeholder counts; orchestrator timing maps verifiers’agentphase to existinggenerationmetrics.Reviewed by Cursor Bugbot for commit 37ff96a. Bugbot is set up for automated code reviews on this repo. Configure here.