Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / data / omni / omni_transform.py: 25%
144 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""Omni transform lifecycle and the default AutoProcessor implementation."""
17import copy
18import os
19from collections.abc import Callable, Mapping
20from dataclasses import dataclass
21from typing import Any
23from transformers import AutoProcessor
25from hyper_parallel.data.constants import IGNORE_INDEX, OMNI_CP_TOKEN_FIELDS, ONLINE_SOURCE_PATH_KEY
28def build_auto_processor(
29 *,
30 pretrained_model_name_or_path: str,
31 trust_remote_code: bool = True,
32 **processor_kwargs: Any,
33) -> Any:
34 """Build the Transformers processor used by the default Omni transform.
36 Args:
37 pretrained_model_name_or_path: Model directory or Hub identifier.
38 trust_remote_code: Whether Transformers may load model-owned code.
39 **processor_kwargs: Additional ``AutoProcessor.from_pretrained`` options.
41 Returns:
42 Processor with an attached tokenizer and resolved chat template.
43 """
44 processor = AutoProcessor.from_pretrained(
45 pretrained_model_name_or_path,
46 trust_remote_code=trust_remote_code,
47 **processor_kwargs,
48 )
49 tokenizer = getattr(processor, "tokenizer", None)
50 if tokenizer is None:
51 raise ValueError("AutoProcessor must provide a tokenizer")
52 chat_template = getattr(processor, "chat_template", None)
53 if chat_template is None:
54 chat_template = getattr(tokenizer, "chat_template", None)
55 if chat_template is not None:
56 processor.chat_template = chat_template
58 return processor
61class OmniDataTransform:
62 """Define model-specific sample and batch encoding hooks.
64 Implement ``encode_sample`` for eager encoding, ``preencode_sample`` and
65 ``postencode_sample`` for deferred encoding, or only ``postencode_sample``
66 when source samples already contain offline packing metadata. The data
67 pipeline detects the implemented hook combination automatically.
69 Packing selection and concatenation belong to the optional packing path
70 rather than this transform.
71 """
73 def __init__(self, *, max_seq_len: int, processor: Any) -> None:
74 """Store the sequence limit and externally built processor."""
75 if max_seq_len <= 0:
76 raise ValueError("max_seq_len must be positive")
77 if processor is None:
78 raise ValueError("processor is required")
79 self.max_seq_len = max_seq_len
80 self.processor = processor
82 @staticmethod
83 def is_valid_sample(sample: Mapping[str, Any]) -> bool:
84 """Return whether one source record contains conversation messages."""
85 messages = sample.get("messages")
86 if not isinstance(messages, list):
87 raise ValueError("Omni sample messages must be a list")
88 return bool(messages)
90 def encode_sample(self, sample: dict[str, Any]) -> dict[str, Any]:
91 """Encode one source sample into one model-ready sample."""
92 raise NotImplementedError(f"{type(self).__name__} does not implement encode_sample")
94 def preencode_sample(self, sample: dict[str, Any]) -> dict[str, Any]:
95 """Prepare metadata used by a packing selector without full encoding."""
96 raise NotImplementedError(f"{type(self).__name__} does not implement preencode_sample")
98 def postencode_sample(self, sample: dict[str, Any]) -> dict[str, Any]:
99 """Encode selected online or offline metadata into a model-ready sample."""
100 raise NotImplementedError(f"{type(self).__name__} does not implement postencode_sample")
102 def encode_batch(self, batch: Any) -> Any:
103 """Apply an optional model-specific batch encoding stage."""
104 return batch
106 def prepare_messages(self, sample: dict[str, Any]) -> list[dict[str, Any]]:
107 """Copy messages and resolve media paths relative to their JSONL file."""
108 messages = sample.get("messages")
109 if not isinstance(messages, list) or not messages:
110 raise ValueError("Omni sample must contain non-empty messages")
112 prepared_messages = copy.deepcopy(messages)
113 source_path = sample.get(ONLINE_SOURCE_PATH_KEY)
114 if source_path is None:
115 return prepared_messages
117 source_directory = os.path.dirname(os.path.abspath(source_path))
118 for message in prepared_messages:
119 content = message.get("content")
120 if not isinstance(content, list):
121 continue
122 for content_block in content:
123 self._resolve_media_block(content_block, source_directory)
124 return prepared_messages
126 @classmethod
127 def _resolve_media_block(cls, content_block: Any, source_directory: str) -> None:
128 """Resolve path-bearing fields without changing the processor's message format."""
129 if not isinstance(content_block, dict):
130 return
132 block_type = content_block.get("type")
133 if block_type == "image_url":
134 image_url = content_block.get("image_url")
135 if isinstance(image_url, str):
136 content_block["image_url"] = cls._resolve_media_path(image_url, source_directory)
137 elif isinstance(image_url, dict) and isinstance(image_url.get("url"), str):
138 image_url["url"] = cls._resolve_media_path(image_url["url"], source_directory)
139 return
141 media_field = content_block.get(block_type)
142 if block_type in ("image", "video", "audio") and isinstance(media_field, str):
143 content_block[block_type] = cls._resolve_media_path(media_field, source_directory)
145 @staticmethod
146 def _resolve_media_path(media_path: str, source_directory: str) -> str:
147 """Keep remote and absolute references, and anchor local relative paths."""
148 if os.path.isabs(media_path) or media_path.startswith(("http://", "https://", "data:")):
149 return media_path
151 resolved_path = os.path.normpath(os.path.join(source_directory, media_path))
152 return resolved_path
155class AutoProcessorTransform(OmniDataTransform):
156 """Encode a conversation and supervise only its final assistant response."""
158 def encode_sample(self, sample: dict[str, Any]) -> dict[str, Any]:
159 """Build model inputs and assistant-only labels from processor messages."""
160 messages = self.prepare_messages(sample)
161 if len(messages) < 2 or messages[-1].get("role") != "assistant":
162 raise ValueError("Omni training sample must end with a final assistant message")
164 encoded_sample = dict(self.processor.apply_chat_template(
165 messages,
166 tokenize=True,
167 return_dict=True,
168 return_tensors="pt",
169 add_generation_prompt=False,
170 ))
171 prompt_sample = self.processor.apply_chat_template(
172 messages[:-1],
173 tokenize=True,
174 return_dict=True,
175 return_tensors="pt",
176 add_generation_prompt=True,
177 )
179 input_ids = encoded_sample["input_ids"].squeeze(0)
180 prompt_ids = prompt_sample["input_ids"].squeeze(0)
181 prompt_length = len(prompt_ids)
182 if prompt_length >= len(input_ids) or not input_ids[:prompt_length].equal(prompt_ids):
183 raise ValueError("Assistant prompt tokens must be a prefix of the full conversation")
185 labels = input_ids.clone()
186 labels[:prompt_length] = IGNORE_INDEX
187 if "mm_token_type_ids" in encoded_sample:
188 labels[encoded_sample["mm_token_type_ids"].squeeze(0) != 0] = IGNORE_INDEX
190 for field in OMNI_CP_TOKEN_FIELDS.intersection(encoded_sample):
191 encoded_sample[field] = encoded_sample[field].squeeze(0)
192 encoded_sample["labels"] = labels
193 return encoded_sample
196SampleEncoder = Callable[[Any], Any]
199@dataclass(frozen=True)
200class _OmniTransformStrategy:
201 """Resolve the encoding stages implemented by an Omni transform."""
203 sample_encoder: SampleEncoder | None
204 selected_sample_encoder: SampleEncoder | None
206 @classmethod
207 def from_transform(cls, transform: OmniDataTransform) -> "_OmniTransformStrategy":
208 """Build one strategy from Energon-style overridden hooks."""
209 encode_sample = cls._is_overridden(transform, "encode_sample")
210 preencode_sample = cls._is_overridden(transform, "preencode_sample")
211 postencode_sample = cls._is_overridden(transform, "postencode_sample")
213 if encode_sample and (preencode_sample or postencode_sample):
214 raise TypeError(
215 f"{type(transform).__name__} cannot combine encode_sample with "
216 "preencode_sample or postencode_sample"
217 )
218 if preencode_sample and not postencode_sample:
219 raise TypeError(
220 f"{type(transform).__name__} must pair preencode_sample with postencode_sample"
221 )
222 if not encode_sample and not postencode_sample:
223 raise TypeError(
224 f"{type(transform).__name__} must implement encode_sample or postencode_sample"
225 )
227 # encode_sample -> encode before candidate selection
228 # preencode_sample + postencode_sample -> prepare metadata, then defer full encoding
229 # postencode_sample only -> preserve offline metadata, then defer encoding
230 sample_encoder = None
231 if preencode_sample:
232 sample_encoder = transform.preencode_sample
233 elif encode_sample:
234 sample_encoder = transform.encode_sample
236 selected_sample_encoder = None
237 if postencode_sample:
238 selected_sample_encoder = transform.postencode_sample
240 transform_strategy = cls(
241 sample_encoder=sample_encoder,
242 selected_sample_encoder=selected_sample_encoder,
243 )
244 return transform_strategy
246 @staticmethod
247 def _is_overridden(transform: OmniDataTransform, method_name: str) -> bool:
248 """Return whether a transform hook differs from its base implementation."""
249 transform_method = getattr(type(transform), method_name)
250 base_method = getattr(OmniDataTransform, method_name)
251 method_overridden = transform_method is not base_method
252 return method_overridden
254 def prepare_sample(self, sample: Any) -> Any:
255 """Encode a sample or attach metadata before packing selection."""
256 prepared_sample = sample
257 if self.sample_encoder is not None:
258 prepared_sample = self.sample_encoder(sample)
259 return prepared_sample
261 def encode_selected_sample(self, sample: Any) -> Any:
262 """Finish encoding a sample after packing selection."""
263 encoded_sample = sample
264 if self.selected_sample_encoder is not None:
265 encoded_sample = self.selected_sample_encoder(sample)
266 return encoded_sample
268 def prepare_samples(self, raw_sample: Any) -> list[Any]:
269 """Return encoded, preencoded, or unchanged samples for candidate selection."""
270 prepared_sample = self.prepare_sample(raw_sample)
271 prepared_samples = [prepared_sample]
272 return prepared_samples
275__all__ = [
276 "build_auto_processor",
277 "OmniDataTransform",
278 "AutoProcessorTransform",
279]