-
Notifications
You must be signed in to change notification settings - Fork 620
[codex] Support raw image offload in v1 train client #1746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
173a518
de37650
e6b13dc
9430999
4a7b37a
0b1d73f
2d4969b
7ade0b2
0dc57a1
18b0fbe
22c7cf4
9b3e7ee
2c2824a
9bc3cc3
2b1627d
3d2068b
d9e79d6
ae516c1
0e41666
88c759b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,46 @@ def _decode_ndarray(d: dict) -> np.ndarray: | |
| return np.frombuffer(d["data"], dtype=np.dtype(d["dtype"])).reshape(d["shape"]) | ||
|
|
||
|
|
||
| _PROCESSED_MM_KEYS = frozenset({"pixel_values", "image_embeds", "image_features"}) | ||
|
|
||
|
|
||
| def _contains_processed_mm_key(value: Any) -> bool: | ||
| if isinstance(value, dict): | ||
| return bool(_PROCESSED_MM_KEYS.intersection(value)) or any( | ||
| _contains_processed_mm_key(v) for v in value.values() | ||
| ) | ||
| if isinstance(value, (list, tuple)): | ||
| return any(_contains_processed_mm_key(v) for v in value) | ||
| return False | ||
|
|
||
|
|
||
| def _validate_raw_mm_item(item: Any) -> dict[str, Any]: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium
🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||
| if not isinstance(item, dict): | ||
| raise TypeError( | ||
| "v1 multimodal sidecars must be raw image descriptor dicts, " | ||
| f"got {type(item).__name__}" | ||
| ) | ||
| if _contains_processed_mm_key(item): | ||
| raise TypeError( | ||
| "v1 multimodal sidecars must be raw image descriptors, " | ||
| "not processed multimodal payloads" | ||
| ) | ||
| if not isinstance(item.get("raw_image_id"), str) or not item["raw_image_id"]: | ||
| raise ValueError("v1 multimodal sidecars require raw_image_id") | ||
| return dict(item) | ||
|
|
||
|
|
||
| def _validate_raw_mm_data(mmd: MultiModalData) -> MultiModalData: | ||
| return MultiModalData( | ||
| mm_hashes={k: list(v) for k, v in mmd.mm_hashes.items()}, | ||
| mm_placeholders={k: list(v) for k, v in mmd.mm_placeholders.items()}, | ||
| mm_items={ | ||
| modality: [_validate_raw_mm_item(item) for item in items] | ||
| for modality, items in mmd.mm_items.items() | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| class MessageNode(StrictBaseModel): | ||
| """One message in the graph: a message plus the tokens it adds to the cumulative | ||
| sequence. Concatenating a root→leaf path's nodes reconstructs that branch's full token | ||
|
|
@@ -97,14 +137,16 @@ class MessageNode(StrictBaseModel): | |
| finish_reason: FinishReason = None | ||
| """The response's finish reason (assistant nodes only) — kept for truncation detection.""" | ||
| multi_modal_data: MultiModalData | None = None | ||
| """The renderer items for the images this message's content introduces (pixel tensors, | ||
| grids, hashes, placeholders) — the only carrier of the pixels from the env server to the | ||
| trainer. `Branch.multi_modal_data` concatenates them along the path into the training | ||
| `mm_kwargs`. Rides the wire as raw bytes (msgpack `bin`) since pydantic can't JSON the numpy; | ||
| kept off disk by the dump-site `exclude` in prime-rl (the tensors bloat the rollout jsonl).""" | ||
| usage: Usage | None = None | ||
| """Provider-reported token usage for this message's response (assistant nodes). Preserved | ||
| on the wire and on disk, including cache-read tokens when the provider reports them.""" | ||
| """The renderer items for images this message introduces. | ||
|
|
||
| With the raw-image path, items are lightweight descriptors (hashes, grid metadata, and | ||
| optional run-image refs), not image processor tensors. `Branch.multi_modal_data` concatenates | ||
| them along the path for the trainer. Old processed-payload sidecars are rejected. | ||
| """ | ||
| usage: Usage | None = Field(default=None, exclude=True) | ||
|
macroscopeapp[bot] marked this conversation as resolved.
Outdated
|
||
| """Provider-reported token usage for this message's response (assistant nodes). Transient | ||
| (excluded from wire/disk); lets the live dashboard show token counts even when the endpoint | ||
| returns no token ids (so `token_ids` is empty).""" | ||
| routed_experts: np.ndarray | None = None | ||
| """This node's slice of the MoE expert-routing array — uint8 `[len(token_ids), layers, | ||
| top_k]`, the expert ids inference selected for exactly this node's tokens. Attributed from | ||
|
|
@@ -116,46 +158,48 @@ class MessageNode(StrictBaseModel): | |
|
|
||
| @field_serializer("multi_modal_data") | ||
| def serialize_multi_modal_data(self, mmd: MultiModalData | None) -> dict | None: | ||
| """`MultiModalData` -> msgpack-safe dict so the pixel tensors ride the wire; numpy | ||
| `mm_items` values become raw-bytes `__nd__` dicts (every renderer emits `return_tensors="np"`).""" | ||
| """`MultiModalData` -> msgpack-safe raw descriptor dict.""" | ||
| if mmd is None: | ||
| return None | ||
| mmd = _validate_raw_mm_data(mmd) | ||
| return { | ||
| "mm_hashes": {k: list(v) for k, v in mmd.mm_hashes.items()}, | ||
| "mm_placeholders": { | ||
| modality: [{"offset": p.offset, "length": p.length} for p in ranges] | ||
| for modality, ranges in mmd.mm_placeholders.items() | ||
| }, | ||
| "mm_items": { | ||
| modality: [ | ||
| {k: _encode_ndarray(v) for k, v in item.items()} for item in items | ||
| ] | ||
| modality: [dict(item) for item in items] | ||
| for modality, items in mmd.mm_items.items() | ||
| }, | ||
| } | ||
|
|
||
| @field_validator("multi_modal_data", mode="before") | ||
| @classmethod | ||
| def deserialize_multi_modal_data(cls, value: Any) -> MultiModalData | None: | ||
| if value is None or isinstance(value, MultiModalData): | ||
| if value is None: | ||
| return value | ||
| if isinstance(value, MultiModalData): | ||
| return _validate_raw_mm_data(value) | ||
| if not isinstance(value, dict): | ||
| raise TypeError(f"cannot build MultiModalData from {type(value).__name__}") | ||
| return MultiModalData( | ||
| mm_hashes={k: list(v) for k, v in (value.get("mm_hashes") or {}).items()}, | ||
| mm_placeholders={ | ||
| modality: [ | ||
| PlaceholderRange(offset=p["offset"], length=p["length"]) | ||
| for p in ranges | ||
| ] | ||
| for modality, ranges in (value.get("mm_placeholders") or {}).items() | ||
| }, | ||
| mm_items={ | ||
| modality: [ | ||
| {k: _decode_ndarray(v) for k, v in item.items()} for item in items | ||
| ] | ||
| for modality, items in (value.get("mm_items") or {}).items() | ||
| }, | ||
| return _validate_raw_mm_data( | ||
| MultiModalData( | ||
| mm_hashes={ | ||
| k: list(v) for k, v in (value.get("mm_hashes") or {}).items() | ||
| }, | ||
| mm_placeholders={ | ||
| modality: [ | ||
| PlaceholderRange(offset=p["offset"], length=p["length"]) | ||
| for p in ranges | ||
| ] | ||
| for modality, ranges in (value.get("mm_placeholders") or {}).items() | ||
| }, | ||
| mm_items={ | ||
| modality: list(items) | ||
| for modality, items in (value.get("mm_items") or {}).items() | ||
| }, | ||
| ) | ||
| ) | ||
|
|
||
| @field_serializer("routed_experts") | ||
|
|
@@ -304,6 +348,23 @@ def prompt_message_spans( | |
| for span in tail_spans | ||
| ] | ||
|
|
||
| def previous_multi_modal_data(self) -> MultiModalData | None: | ||
| """Concatenate multimodal sidecars attached to the reusable prefix.""" | ||
| merged = MultiModalData() | ||
| found = False | ||
| for nid in self.prefix_node_ids: | ||
| mmd = self.trace.nodes[nid].multi_modal_data | ||
| if mmd is None or mmd.is_empty(): | ||
| continue | ||
| found = True | ||
| for modality, items in mmd.mm_items.items(): | ||
| merged.mm_items.setdefault(modality, []).extend(items) | ||
| for modality, hashes in mmd.mm_hashes.items(): | ||
| merged.mm_hashes.setdefault(modality, []).extend(hashes) | ||
| for modality, placeholders in mmd.mm_placeholders.items(): | ||
| merged.mm_placeholders.setdefault(modality, []).extend(placeholders) | ||
| return merged if found else None | ||
|
|
||
| def commit(self, response: Response) -> None: | ||
| _commit_turn(self, response) | ||
|
|
||
|
|
@@ -360,8 +421,9 @@ def _attribute_mm( | |
| renderer emits items per modality in prompt order (message order, then content-part order), | ||
| so we walk the path advancing a per-modality cursor over every message's media but write | ||
| only the nodes created this turn — `path[:num_reused]` is the reused prefix, already | ||
| attributed when first created. Item order is all training needs; placeholder offsets aren't | ||
| carried.""" | ||
| attributed when first created. Each node gets the hashes/items/placeholders for exactly the | ||
| media it introduced, preserving vLLM multimodal-list alignment when those node sidecars are | ||
| later merged for bridge or training.""" | ||
| if mmd is None or mmd.is_empty(): | ||
| return | ||
| cursors: dict[str, int] = {} | ||
|
|
@@ -371,6 +433,7 @@ def _attribute_mm( | |
| continue | ||
| node_items: dict[str, list] = {} | ||
| node_hashes: dict[str, list] = {} | ||
| node_placeholders: dict[str, list[PlaceholderRange]] = {} | ||
| for part in content: | ||
| modality = _part_modality(part) | ||
| if modality is None: | ||
|
|
@@ -382,13 +445,20 @@ def _attribute_mm( | |
| continue | ||
| items = mmd.mm_items.get(modality) or [] | ||
| hashes = mmd.mm_hashes.get(modality) or [] | ||
| placeholders = mmd.mm_placeholders.get(modality) or [] | ||
| if k < len(items): | ||
| node_items.setdefault(modality, []).append(items[k]) | ||
| if k < len(hashes): | ||
| node_hashes.setdefault(modality, []).append(hashes[k]) | ||
| if node_items: | ||
| trace.nodes[node_id].multi_modal_data = MultiModalData( | ||
| mm_items=node_items, mm_hashes=node_hashes | ||
| if k < len(placeholders): | ||
| node_placeholders.setdefault(modality, []).append(placeholders[k]) | ||
| if node_items or node_hashes or node_placeholders: | ||
| trace.nodes[node_id].multi_modal_data = _validate_raw_mm_data( | ||
| MultiModalData( | ||
| mm_items=node_items, | ||
| mm_hashes=node_hashes, | ||
| mm_placeholders=node_placeholders, | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.