@@ -99,11 +99,12 @@ def _revision_kwargs(revision: str | None) -> dict[str, str]:
9999 "gemma2" : "gemma" ,
100100 "gemma4" : "gemma4" ,
101101 "gemma4_text" : "gemma4_text" ,
102- # gemma-4-12B "unified" (encoder-free) variant reuses the gemma4 ORT GenAI
103- # pipelines: the multimodal package (decoder taking inputs_embeds + vision
104- # embedder + embedding fusion) maps to "gemma4"; the standalone text
105- # backbone maps to "gemma4_text".
106- "gemma4_unified" : "gemma4" ,
102+ # gemma-4-12B "unified" (encoder-free) variant. The multimodal package
103+ # (decoder taking inputs_embeds + encoder-free vision/audio embedders +
104+ # embedding fusion) maps to the dedicated "gemma4_unified" ORT GenAI type,
105+ # which drives the unified processor (48px merged patches / raw 640-sample
106+ # audio frames). The standalone text backbone maps to "gemma4_text".
107+ "gemma4_unified" : "gemma4_unified" ,
107108 "gemma4_unified_text" : "gemma4_text" ,
108109 "mistral" : "mistral" ,
109110 "mistral3" : "mistral3" ,
@@ -194,12 +195,12 @@ class _DecoderAbi:
194195)
195196# Encoder-free gemma-4-12B "unified" variants. Their image/audio inputs are raw
196197# merged pixel patches (48px, 6912-dim) / raw waveform frames (640-dim), NOT the
197- # SigLIP 16px / 128-dim log-mel contract that the ort-extensions
198- # ``Gemma4ImageTransform`` / ``Gemma4LogMel `` ops implement. There is no
199- # genai-native transform for the unified contract, so we deliberately do NOT
200- # emit image_processor.json / audio_processor.json for these models — callers
201- # must preprocess with the HuggingFace processor and feed tensors via
202- # ``Generator.set_inputs`` (see examples/gemma4_unified_ort_genai.py ).
198+ # SigLIP 16px / 128-dim log-mel contract of the standard gemma4 (E2B/E4B) model.
199+ # These are produced natively by ort-extensions: ``Gemma4ImageTransform `` with
200+ # patch_size=48 / pooling_kernel_size=1 ( the 48px merged patch is identical to a
201+ # direct 48px patchify) and the ``Gemma4UnifiedAudioFrames`` raw-framing op. The
202+ # genai ``gemma4_unified`` processor consumes both (see the companion
203+ # onnxruntime-genai / onnxruntime-extensions support ).
203204_GEMMA4_UNIFIED_MODEL_TYPES = frozenset ({"gemma4_unified" , "gemma4_unified_text" })
204205_GLMASR_MODEL_TYPES = frozenset ({"glmasr" })
205206_MINICPM_MODEL_TYPES = frozenset ({"minicpmv4_6" })
@@ -937,9 +938,10 @@ def _write_vision_processor_config(
937938
938939 - **Gemma4** (``gemma4``, ``gemma4_text``): Writes ``image_processor.json``
939940 with a ``DecodeImage → Gemma4ImageTransform`` pipeline.
940- - **Gemma4 unified** (``gemma4_unified*``): Returns ``None`` — the
941- encoder-free model has no matching ort-extensions transform; callers feed
942- HF-preprocessed pixel_values via ``Generator.set_inputs``.
941+ - **Gemma4 unified** (``gemma4_unified*``): Writes ``image_processor.json``
942+ with a ``DecodeImage → Gemma4ImageTransform`` pipeline configured for the
943+ encoder-free 48px merged-patch contract (patch_size=48,
944+ pooling_kernel_size=1, patch_dim=6912).
943945 - **Gemma3** (``gemma3`` or ``gemma3_text``): Writes
944946 ``processor_config.json`` with a 5-step pipeline (DecodeImage →
945947 Resize[fixed] → Rescale → Normalize → Permute3D). Uses a
@@ -971,16 +973,6 @@ def _write_vision_processor_config(
971973 return None
972974
973975 model_type = getattr (config , "model_type" , "" )
974- if model_type in _GEMMA4_UNIFIED_MODEL_TYPES :
975- # Encoder-free unified model: no ort-extensions transform matches its
976- # raw merged-patch contract. Emit no image_processor.json; callers feed
977- # HF-preprocessed pixel_values via Generator.set_inputs.
978- logger .info (
979- "Skipping image_processor.json for encoder-free %s "
980- "(no native ort-extensions transform; use HF processor + set_inputs)" ,
981- model_type ,
982- )
983- return None
984976 if model_type in _MINICPM_MODEL_TYPES :
985977 # MiniCPM needs adaptive slicing and NaViT horizontal patch packing.
986978 # ort-extensions has no equivalent transform, so preserving the HF
@@ -1007,7 +999,49 @@ def _write_vision_processor_config(
1007999 vision_model_type = getattr (vision , "model_type" , None )
10081000 is_pixtral = vision_model_type == "pixtral" or model_type in _PIXTRAL_MODEL_TYPES
10091001
1010- if model_type in _GEMMA4_MODEL_TYPES :
1002+ if model_type in _GEMMA4_UNIFIED_MODEL_TYPES :
1003+ # Encoder-free unified model: it consumes 48px MERGED patches
1004+ # (patch_dim = 48*48*3 = 6912) directly, with no SigLIP tower to pool
1005+ # 3x3 teacher patches. HuggingFace produces these via
1006+ # (16px patchify -> 3x3 patches_merge), which is provably identical to a
1007+ # direct 48px patchify. So we reuse the same ort-extensions
1008+ # ``Gemma4ImageTransform`` op with the merged geometry: patch_size =
1009+ # patch_size * pooling_kernel_size (48) and pooling_kernel_size = 1.
1010+ max_soft_tokens = (
1011+ getattr (vision , "mm_tokens_per_image" , None )
1012+ or getattr (config , "mm_tokens_per_image" , None )
1013+ or 280
1014+ )
1015+ patch_size = getattr (vision , "patch_size" , None ) or 16
1016+ pooling = getattr (vision , "pooling_kernel_size" , None ) or 3
1017+ model_patch_size = patch_size * pooling # 16 * 3 = 48
1018+ processor_config : dict [str , Any ] = {
1019+ "processor" : {
1020+ "name" : "gemma_4_unified_image_processing" ,
1021+ "transforms" : [
1022+ {
1023+ "operation" : {
1024+ "name" : "decode_image" ,
1025+ "type" : "DecodeImage" ,
1026+ "attrs" : {"color_space" : "RGB" },
1027+ }
1028+ },
1029+ {
1030+ "operation" : {
1031+ "name" : "gemma4_image_transform" ,
1032+ "type" : "Gemma4ImageTransform" ,
1033+ "attrs" : {
1034+ "patch_size" : model_patch_size ,
1035+ "max_soft_tokens" : max_soft_tokens ,
1036+ "pooling_kernel_size" : 1 ,
1037+ },
1038+ }
1039+ },
1040+ ],
1041+ }
1042+ }
1043+ path = os .path .join (output_dir , "image_processor.json" )
1044+ elif model_type in _GEMMA4_MODEL_TYPES :
10111045 # Gemma4 needs an onnxruntime-extensions format processor config
10121046 # with a transforms pipeline (DecodeImage -> Gemma4ImageTransform).
10131047 max_soft_tokens = (
@@ -1017,7 +1051,7 @@ def _write_vision_processor_config(
10171051 )
10181052 patch_size = getattr (vision , "patch_size" , None ) or 16
10191053 pooling_kernel_size = getattr (vision , "pooling_kernel_size" , None ) or 3
1020- processor_config : dict [ str , Any ] = {
1054+ processor_config = {
10211055 "processor" : {
10221056 "name" : "gemma_4_image_processing" ,
10231057 "transforms" : [
@@ -1306,7 +1340,7 @@ def _write_audio_processor_config(
13061340 config : Any ,
13071341 output_dir : str ,
13081342) -> str | None :
1309- """Write audio_processor.json for models with audio encoders.
1343+ """Write the native audio processor config for models with audio encoders.
13101344
13111345 Returns the path if written, None otherwise.
13121346 """
@@ -1317,18 +1351,40 @@ def _write_audio_processor_config(
13171351 model_type = getattr (config , "model_type" , "" )
13181352
13191353 if model_type in _GEMMA4_UNIFIED_MODEL_TYPES :
1320- # Encoder-free unified model: raw 640-dim waveform frames, not the
1321- # 128-dim log-mel Gemma4LogMel contract. Emit no audio_processor.json;
1322- # callers feed HF-preprocessed input_features via Generator.set_inputs.
1323- logger .info (
1324- "Skipping audio_processor.json for encoder-free %s "
1325- "(no native ort-extensions transform; use HF processor + set_inputs)" ,
1326- model_type ,
1327- )
1328- return None
1329-
1330- if model_type in _GEMMA4_MODEL_TYPES :
1331- # Gemma4 USM-style 128-dim log-mel spectrogram.
1354+ # Encoder-free unified model: each audio soft token is a raw chunk of the
1355+ # 16 kHz waveform (audio_samples_per_token = audio_embed_dim, 640), not a
1356+ # 128-dim log-mel frame. Reproduced natively by the ort-extensions
1357+ # ``Gemma4Audio`` op with ``type="raw_frames"`` (pad to a whole number of
1358+ # frames, reshape to (num_tokens, 640)).
1359+ samples_per_token = getattr (audio , "hidden_size" , None ) or 640
1360+ processor = {
1361+ "feature_extraction" : {
1362+ "sequence" : [
1363+ {
1364+ "operation" : {
1365+ "name" : "audio_decoder" ,
1366+ "type" : "AudioDecoder" ,
1367+ }
1368+ },
1369+ {
1370+ "operation" : {
1371+ "name" : "gemma4_audio" ,
1372+ "type" : "Gemma4Audio" ,
1373+ "attrs" : {
1374+ "type" : "raw_frames" ,
1375+ "audio_samples_per_token" : samples_per_token ,
1376+ "sampling_rate" : 16000 ,
1377+ "padding_value" : 0.0 ,
1378+ },
1379+ }
1380+ },
1381+ ]
1382+ }
1383+ }
1384+ proc_filename = "audio_feature_extraction.json"
1385+ elif model_type in _GEMMA4_MODEL_TYPES :
1386+ # Gemma4 USM-style 128-dim log-mel spectrogram via the ort-extensions
1387+ # ``Gemma4Audio`` op with ``type="log_mel"``.
13321388 # OrtxCreateSpeechFeatureExtractor requires the feature_extraction.sequence format.
13331389 processor = {
13341390 "feature_extraction" : {
@@ -1341,9 +1397,10 @@ def _write_audio_processor_config(
13411397 },
13421398 {
13431399 "operation" : {
1344- "name" : "gemma4_log_mel " ,
1345- "type" : "Gemma4LogMel " ,
1400+ "name" : "gemma4_audio " ,
1401+ "type" : "Gemma4Audio " ,
13461402 "attrs" : {
1403+ "type" : "log_mel" ,
13471404 "feature_size" : 128 ,
13481405 "sampling_rate" : 16000 ,
13491406 "frame_length_ms" : 20.0 ,
0 commit comments