From 56d25d80d81cb35ac6a75c901bb8c1e9d1b6f502 Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Mon, 24 Aug 2026 20:31:50 -0700 Subject: [PATCH 1/3] Generate graph-driven ORT GenAI decoder configs Derive generic decoder inputs, outputs, and cache topology from the optimized ONNX graph instead of architecture-name registration. Preserve runtime-specific behavior, fail closed on unsupported state layouts, and publish released-version compatibility metadata.\n\nValidate the exact SmolLM GGUF route with deterministic generation on ORT GenAI 0.14.1 and 0.15.2.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Justin Chu --- docs/cli_reference.md | 38 ++ .../integrations/gguf/_runtime_package.py | 9 +- .../integrations/ort_genai/auto_export.py | 340 ++++++++++++++++-- .../ort_genai/auto_export_test.py | 311 ++++++++++++++-- .../integrations/ort_genai/genai_config.py | 22 +- ...uf_small_model_runtime_integration_test.py | 76 ++++ 6 files changed, 737 insertions(+), 59 deletions(-) diff --git a/docs/cli_reference.md b/docs/cli_reference.md index c93de71fa..ca307ef2f 100644 --- a/docs/cli_reference.md +++ b/docs/cli_reference.md @@ -161,6 +161,44 @@ tokenizer files to the output directory: - With `--config` (local directory): tokenizer files are copied from that directory. +For a single-model decoder-only text graph, Mobius emits the architecture-neutral +`model.type: "decoder"` contract supported by onnxruntime-genai 0.14.0 and newer. +The graph determines the exact semantic input names, output names, cache templates, +and global cache indices, so dense, MoE, tied-weight, quantized, and unknown +architecture names do not need a runtime registry entry. + +Architecture-specific types remain only where the runtime selects different +behavior. `lfm2` uses its legacy convolution-cache implementation. `gpt2` selects +`Gpt_Model`, but Mobius's separate rank-4 key/value cache ABI does not match that +runtime's rank-5 combined-cache contract, so config generation currently fails +closed. `phi3`, `phimoe`, and `phi3small` retain their names because the released +generator uses them to recompute LongRoPE caches when generation crosses the +short-context threshold. Multimodal, audio, encoder-decoder, special-position-ID, +and split pipeline packages remain outside the generic path and require their +dedicated types and schemas. These exceptions follow the +[v0.15.2 runtime model factory](https://github.com/microsoft/onnxruntime-genai/blob/v0.15.2/src/models/model.cpp#L874-L907), +which selects `Gpt_Model`, `LFM2_Model`, `WhisperModel`, `MarianModel`, +`MultiModalLanguageModel`, and `DecoderOnlyPipelineModel` separately from +`DecoderOnly_Model`; Qwen-VL's special position handling is likewise implemented in +its [dedicated runtime model](https://github.com/microsoft/onnxruntime-genai/blob/v0.15.2/src/models/qwen_vl_model.cpp). +The Phi-3 LongRoPE threshold dispatch is in the released +[`Generator`](https://github.com/microsoft/onnxruntime-genai/blob/v0.15.2/src/generators.cpp). + +Released generic recurrent state is enabled only when the optimized graph exposes +matching `conv_state` and `recurrent_state` names derived from the same cache +template. Mobius rejects static-cache names and heterogeneous state layouts that +the released config schema cannot represent rather than emitting a misleading +dense cache. The deferred state-manifest work is tracked by +[#605](https://github.com/onnxruntime/mobius/issues/605). + +Each export also writes `runtime_compatibility.json`. Generic decoder metadata +records the minimum runtime version and the released versions exercised by Mobius +(0.14.1 and 0.15.2); it never emits the unreleased `decoder.state_groups` field. +Generic config availability does not promote a GGUF runtime verdict: the only +runtime-supported GGUF route remains the exact pinned SmolLM F16/CPU package, while +SmolLM2 remains rejected because its GGUF padding-token metadata conflicts with the +official pinned tokenizer. + #### Example ```bash diff --git a/src/mobius/integrations/gguf/_runtime_package.py b/src/mobius/integrations/gguf/_runtime_package.py index 91755d51b..fd1ececba 100644 --- a/src/mobius/integrations/gguf/_runtime_package.py +++ b/src/mobius/integrations/gguf/_runtime_package.py @@ -280,7 +280,14 @@ def write_gguf_runtime_package( "ORT GenAI runtime packaging requires an explicit evidenced execution " "provider: cpu, cuda, or dml." ) - artifacts.update(write_ort_genai_config(pkg, str(stage), ep=execution_provider)) + artifacts.update( + write_ort_genai_config( + pkg, + str(stage), + ep=execution_provider, + runtime_version=runtime_version, + ) + ) else: from mobius.integrations.onnx_genai import write_onnx_genai_config diff --git a/src/mobius/integrations/ort_genai/auto_export.py b/src/mobius/integrations/ort_genai/auto_export.py index 9e1626612..222458702 100644 --- a/src/mobius/integrations/ort_genai/auto_export.py +++ b/src/mobius/integrations/ort_genai/auto_export.py @@ -51,7 +51,9 @@ import json import logging import os +import re import shutil +from dataclasses import dataclass from typing import TYPE_CHECKING, Any from mobius.upstream_patches import apply_asset_patches @@ -135,6 +137,42 @@ def _revision_kwargs(revision: str | None) -> dict[str, str]: "minicpmv4_6": "phi3v", } +# These text types select runtime implementations with semantics that are not +# described by the ordinary decoder graph ABI. All other compatible, single- +# model decoder packages use ORT GenAI's released generic DecoderOnly_Model. +_ARCHITECTURE_SPECIFIC_TEXT_TYPES = { + "gpt2": "gpt2", + "lfm2": "lfm2", + "lfm2_vl": "lfm2", + "phi3": "phi3", + "phi3small": "phi3small", + "phimoe": "phimoe", +} +_GENERIC_DECODER_MIN_VERSION = (0, 14, 0) +_GENERIC_DECODER_TESTED_VERSIONS = ("0.14.1", "0.15.2") +_DECODER_SEMANTIC_INPUTS = frozenset( + { + "input_ids", + "attention_mask", + "position_ids", + "past_sequence_length", + "current_sequence_length", + } +) +_CACHE_NAME = re.compile( + r"^(?P.+\.)(?P[0-9]+)\.(?P" + r"key|value|conv_state|recurrent_state|ssm_state)$" +) + + +@dataclass(frozen=True) +class _DecoderAbi: + inputs: dict[str, str] + outputs: dict[str, str] + cache_slots: int + has_recurrent_state: bool + + _GEMMA4_MODEL_TYPES = frozenset( {"gemma4", "gemma4_text", "gemma4_unified", "gemma4_unified_text"} ) @@ -234,24 +272,215 @@ def _select_ort_model_type( ) -> str: """Choose the ORT-GenAI model type for an exported package. - Decoder-only packages prefer the built package's ``config.model_type`` so - text-only / overridden builds (e.g. ``gemma4_unified -> gemma4_unified_text``) - resolve to the decoder-only ORT type. Multimodal packages keep the HF - parent ``model_type``: ``build()`` unwraps composite configs to their text - sub-config, so ``config.model_type`` would otherwise be the text type even - for a full multimodal export. - - The ``config.model_type`` preference only applies when it resolves to a - *known* ORT-GenAI type (a key in :data:`_ORT_GENAI_MODEL_TYPE`). An - unrecognised ``config.model_type`` would otherwise pass straight through as - an invalid ORT type and mask a valid HF-derived mapping, so in that case we - fall back to ``hf_model_type``. + Released ORT GenAI 0.14+ dispatches ``decoder`` to its generic + ``DecoderOnly_Model``. Decoder-only packages therefore use that type unless + the runtime has genuinely different behavior: ``gpt2`` selects ``Gpt_Model``, + ``lfm2`` selects ``LFM2_Model``/``LFM2Cache``, and Phi-3 family names enable + LongRoPE cache recomputation after the short-context threshold. + + Multimodal and encoder-decoder packages retain their architecture-specific + type because those values select distinct runtime pipelines and position-ID + semantics. """ - if is_decoder_only and config_model_type in _ORT_GENAI_MODEL_TYPE: - return _ORT_GENAI_MODEL_TYPE[config_model_type] + if is_decoder_only: + for source_type in (config_model_type, hf_model_type): + resolved = _resolve_ort_genai_model_type(source_type or "unknown") + if resolved in _ARCHITECTURE_SPECIFIC_TEXT_TYPES: + return _ARCHITECTURE_SPECIFIC_TEXT_TYPES[resolved] + return "decoder" return _resolve_ort_genai_model_type(hf_model_type or "unknown") +def _cache_names(names: list[str]) -> dict[str, dict[int, str]]: + result: dict[str, dict[int, str]] = {} + for name in names: + match = _CACHE_NAME.fullmatch(name) + if match is None: + continue + result.setdefault(match["kind"], {})[int(match["index"])] = name + return result + + +def _name_template(names: dict[int, str], *, label: str) -> str: + templates: set[str] = set() + for name in names.values(): + match = _CACHE_NAME.fullmatch(name) + if match is None: + raise ValueError(f"Invalid {label} cache name {name!r}") + templates.add(f"{match['prefix']}%d.{match['kind']}") + if len(templates) != 1: + raise ValueError(f"ORT GenAI requires one consistent {label} name template") + return templates.pop() + + +def _is_single_model_decoder_package(pkg: ModelPackage) -> bool: + if set(pkg) != {"model"}: + return False + model = pkg.get("model") + if model is None: + return False + input_names = {value.name for value in model.graph.inputs} + output_names = {value.name for value in model.graph.outputs} + return "input_ids" in input_names and "logits" in output_names + + +def _inspect_decoder_abi(model: ir.Model, *, model_type: str) -> _DecoderAbi: + """Validate and describe the released ORT GenAI decoder graph contract.""" + input_names = [value.name for value in model.graph.inputs if value.name is not None] + output_names = [value.name for value in model.graph.outputs if value.name is not None] + if "input_ids" not in input_names: + raise ValueError("Generic ORT GenAI decoder graphs must expose an input_ids input") + if "logits" not in output_names: + raise ValueError("Generic ORT GenAI decoder graphs must expose a logits output") + if model_type == "gpt2": + raise ValueError( + "ORT GenAI's gpt2 runtime requires one rank-5 combined KV-cache tensor per " + "layer, but Mobius GPT-2 graphs expose separate key/value tensors; refusing " + "to emit an incompatible specialized-runtime config" + ) + + input_cache = _cache_names(input_names) + output_cache = _cache_names(output_names) + cache_input_names = {name for values in input_cache.values() for name in values.values()} + unknown_inputs = set(input_names) - cache_input_names - _DECODER_SEMANTIC_INPUTS + if unknown_inputs: + raise ValueError( + "ORT GenAI cannot automatically supply decoder graph inputs " + f"{sorted(unknown_inputs)}; use a specialized runtime pipeline" + ) + has_current_length = "current_sequence_length" in input_names + has_past_length = "past_sequence_length" in input_names + if has_current_length != has_past_length: + raise ValueError( + "ORT GenAI supplies current_sequence_length and past_sequence_length only " + "as a pair" + ) + unsupported_kinds = (set(input_cache) | set(output_cache)) - { + "key", + "value", + "conv_state", + "recurrent_state", + } + if unsupported_kinds: + raise ValueError( + "ORT GenAI released config cannot represent decoder state kinds " + f"{sorted(unsupported_kinds)}; heterogeneous state manifests are deferred to #605" + ) + + key_indices = set(input_cache.get("key", {})) + value_indices = set(input_cache.get("value", {})) + present_key_indices = set(output_cache.get("key", {})) + present_value_indices = set(output_cache.get("value", {})) + if not key_indices or key_indices != value_indices: + raise ValueError("ORT GenAI decoder graphs require paired key/value cache inputs") + if key_indices != present_key_indices or key_indices != present_value_indices: + raise ValueError( + "ORT GenAI decoder cache outputs must match the graph's key/value inputs" + ) + + recurrent_indices = set(input_cache.get("conv_state", {})) + has_recurrent_state = bool(input_cache.get("recurrent_state")) + if model_type == "lfm2": + if has_recurrent_state: + raise ValueError( + "LFM2's legacy runtime contract does not accept recurrent_state tensors" + ) + if recurrent_indices != set(output_cache.get("conv_state", {})): + raise ValueError("LFM2 conv_state outputs must match its conv_state inputs") + elif recurrent_indices or has_recurrent_state: + expected = set(input_cache.get("recurrent_state", {})) + if not recurrent_indices or recurrent_indices != expected: + raise ValueError( + "Generic recurrent state requires paired conv_state/recurrent_state inputs" + ) + if recurrent_indices != set( + output_cache.get("conv_state", {}) + ) or recurrent_indices != set(output_cache.get("recurrent_state", {})): + raise ValueError( + "Generic recurrent state outputs must match conv_state/recurrent_state inputs" + ) + + decoder_inputs = {name: name for name in input_names if name in _DECODER_SEMANTIC_INPUTS} + decoder_inputs["past_key_names"] = _name_template(input_cache["key"], label="past-key") + decoder_inputs["past_value_names"] = _name_template( + input_cache["value"], label="past-value" + ) + decoder_outputs = { + "logits": "logits", + "present_key_names": _name_template(output_cache["key"], label="present-key"), + "present_value_names": _name_template(output_cache["value"], label="present-value"), + } + if model_type == "lfm2" and recurrent_indices: + decoder_inputs["past_conv_names"] = _name_template( + input_cache["conv_state"], label="past-convolution" + ) + decoder_outputs["present_conv_names"] = _name_template( + output_cache["conv_state"], label="present-convolution" + ) + elif recurrent_indices: + expected_input_prefix = decoder_inputs["past_key_names"].rsplit(".", 1)[0] + expected_output_prefix = decoder_outputs["present_key_names"].rsplit(".", 1)[0] + recurrent_templates = { + _name_template(input_cache["conv_state"], label="past-convolution"), + _name_template(input_cache["recurrent_state"], label="past-recurrent"), + } + present_templates = { + _name_template(output_cache["conv_state"], label="present-convolution"), + _name_template(output_cache["recurrent_state"], label="present-recurrent"), + } + if {template.rsplit(".", 1)[0] for template in recurrent_templates} != { + expected_input_prefix + } or {template.rsplit(".", 1)[0] for template in present_templates} != { + expected_output_prefix + }: + raise ValueError( + "Released ORT GenAI derives recurrent state names from the key-cache " + "templates; graph prefixes must match exactly" + ) + all_indices = key_indices | recurrent_indices + return _DecoderAbi( + inputs=decoder_inputs, + outputs=decoder_outputs, + cache_slots=max(all_indices) + 1, + has_recurrent_state=has_recurrent_state, + ) + + +def _runtime_version_tuple(version: str) -> tuple[int, int, int]: + match = re.match(r"^([0-9]+)\.([0-9]+)\.([0-9]+)", version) + if match is None: + raise ValueError( + f"Invalid onnxruntime-genai version {version!r}; expected MAJOR.MINOR.PATCH" + ) + return tuple(int(part) for part in match.groups()) + + +def _write_runtime_compatibility( + output_dir: str, *, model_type: str, runtime_version: str | None +) -> str: + if model_type == "decoder" and runtime_version is not None: + if _runtime_version_tuple(runtime_version) < _GENERIC_DECODER_MIN_VERSION: + raise ValueError( + "Generic ORT GenAI decoder packages require onnxruntime-genai >= 0.14.0; " + f"requested {runtime_version}" + ) + metadata = { + "runtime": "onnxruntime-genai", + "model_type": model_type, + "minimum_version": "0.14.0" if model_type == "decoder" else None, + "tested_versions": ( + list(_GENERIC_DECODER_TESTED_VERSIONS) if model_type == "decoder" else [] + ), + "uses_main_only_state_groups": False, + "heterogeneous_state_manifest": "deferred: https://github.com/onnxruntime/mobius/issues/605", + } + path = os.path.join(output_dir, "runtime_compatibility.json") + with open(path, "w", encoding="utf-8") as handle: + json.dump(metadata, handle, indent=2) + handle.write("\n") + return path + + def _load_generation_config(model_id: str): """Load optional Hugging Face generation settings without requiring the file.""" import transformers @@ -1197,11 +1426,21 @@ def _write_genai_config( # --- Discover decoder inputs from the ONNX graph --- decoder_key = "decoder" if "decoder" in pkg else "model" - decoder_inputs = _introspect_inputs(pkg, decoder_key) - if decoder_inputs is not None: - # KV cache entries are template-based, not per-input - decoder_inputs["past_key_names"] = "past_key_values.%d.key" - decoder_inputs["past_value_names"] = "past_key_values.%d.value" + decoder_model = pkg.get(decoder_key) + decoder_abi: _DecoderAbi | None = None + if _is_single_model_decoder_package(pkg): + if decoder_model is None: + raise ValueError("ORT GenAI text packages require a decoder ONNX graph") + decoder_abi = _inspect_decoder_abi(decoder_model, model_type=ort_model_type) + decoder_inputs = decoder_abi.inputs + decoder_outputs = decoder_abi.outputs + else: + decoder_inputs = _introspect_inputs(pkg, decoder_key) + decoder_outputs = None + if decoder_inputs is not None: + # Multimodal runtime types retain their architecture-specific cache contract. + decoder_inputs["past_key_names"] = "past_key_values.%d.key" + decoder_inputs["past_value_names"] = "past_key_values.%d.value" # Derive decoder filename from the actual package key decoder_filename = ( @@ -1236,7 +1475,6 @@ def _write_genai_config( # mismatch rather than at load time. Rather than silently emit a broken # config, raise a clear error so the caller picks an EP/dtype combination # (e.g. fp32 on CPU) that lowers full attention to GQA. - decoder_model = pkg.get(decoder_key) supports_in_place_kv_cache: bool | None = None if decoder_model is not None: has_gqa = any( @@ -1269,6 +1507,27 @@ def _write_genai_config( ) supports_in_place_kv_cache = has_gqa or has_recurrent_state + sliding_window = None + window_size = getattr(config, "sliding_window", None) + if isinstance(window_size, int) and window_size > 0: + layer_types = getattr(config, "layer_types", None) + local_types = {"local", "sliding_attention", "window_attention"} + layers = ( + [ + index + for index, layer_type in enumerate(layer_types) + if layer_type in local_types + ] + if layer_types + else list(range(config.num_hidden_layers)) + ) + sliding_window = { + "window_size": window_size, + "slide_key_value_cache": False, + "slide_inputs": False, + "layers": layers, + } + generator = GenaiConfigGenerator.from_config( config, ort_model_type, @@ -1278,9 +1537,15 @@ def _write_genai_config( eos_token_id=eos_token_id, pad_token_id=pad_token_id, decoder_inputs=decoder_inputs, + decoder_outputs=decoder_outputs, decoder_filename=decoder_filename, supports_in_place_kv_cache=supports_in_place_kv_cache, - num_cache_layer_slots=_count_cache_layer_slots(decoder_model), + num_cache_layer_slots=( + decoder_abi.cache_slots + if decoder_abi is not None + else _count_cache_layer_slots(decoder_model) + ), + sliding_window=sliding_window, ) generator.with_special_tokens( **_special_token_ids_from_tokenizer_config(output_dir, config.vocab_size) @@ -1499,6 +1764,7 @@ def write_ort_genai_config( context_length: int = 4096, local_config_dir: str | None = None, trust_remote_code: bool = False, + runtime_version: str | None = None, ) -> dict[str, str]: """Generate ORT-GenAI config artifacts for an already-built ModelPackage. @@ -1537,6 +1803,8 @@ def write_ort_genai_config( resolving token IDs and model type. revision: Optional immutable HuggingFace revision used for every remote configuration, tokenizer, and processor request. + runtime_version: Optional onnxruntime-genai version that will consume the + package. Generic decoder packages reject versions older than 0.14.0. Returns: Dict mapping artifact name to file path, e.g.:: @@ -1580,12 +1848,11 @@ def write_ort_genai_config( pad_token_id: int | None = None ort_model_type: str - # Detect multimodal capabilities from the package keys. Needed before - # resolving the ORT model type so decoder-only (text-only) packages can - # prefer their own config.model_type (see below). + # Generic decoder dispatch is intentionally limited to one-graph text packages. + # Auxiliary encoder, pipeline, and sidecar graphs require their own runtime contract. is_vlm = "vision_encoder" in pkg and "embedding" in pkg has_speech = "audio_encoder" in pkg - is_decoder_only = not is_vlm and not has_speech + is_decoder_only = _is_single_model_decoder_package(pkg) if hf_model_id is not None: import transformers @@ -1635,7 +1902,9 @@ def write_ort_genai_config( # does not bind, so borrowing that type would mis-wire the graph. ort_model_type = "gemma3n" else: - ort_model_type = _resolve_ort_genai_model_type(raw_type) + ort_model_type = _select_ort_model_type( + raw_type, raw_type, is_decoder_only=is_decoder_only + ) if ort_model_type == "unknown": logger.warning( "Could not determine ORT-GenAI model type: pkg.config.model_type " @@ -1672,6 +1941,15 @@ def write_ort_genai_config( # Override to 'phi4mm' so ORT-GenAI loads the correct pipeline. if ort_model_type == "phi" and has_speech: ort_model_type = "phi4mm" + if ( + ort_model_type == "decoder" + and runtime_version is not None + and _runtime_version_tuple(runtime_version) < _GENERIC_DECODER_MIN_VERSION + ): + raise ValueError( + "Generic ORT GenAI decoder packages require onnxruntime-genai >= 0.14.0; " + f"requested {runtime_version}" + ) result: dict[str, str] = {} @@ -1746,6 +2024,12 @@ def write_ort_genai_config( has_speech=has_speech, ) result["genai_config"] = genai_path + compatibility_path = _write_runtime_compatibility( + directory, + model_type=ort_model_type, + runtime_version=runtime_version, + ) + result["runtime_compatibility"] = compatibility_path # Write processor config for VLMs processor_path = _write_vision_processor_config( @@ -1791,6 +2075,7 @@ def export_package( context_length: int = 4096, local_config_dir: str | None = None, trust_remote_code: bool = False, + runtime_version: str | None = None, external_data: str = "onnx", progress_bar: bool = True, ) -> dict[str, str]: @@ -1834,6 +2119,8 @@ def export_package( resolving token IDs and model type. revision: Optional immutable HuggingFace revision used for remote configuration, tokenizer, and processor requests. + runtime_version: Optional onnxruntime-genai version that will consume + the package. external_data: External-data format passed to :meth:`ModelPackage.save` (``"onnx"`` or ``"safetensors"``). progress_bar: Whether to show the save progress bar. @@ -1892,6 +2179,7 @@ def export_package( context_length=context_length, local_config_dir=local_config_dir, trust_remote_code=trust_remote_code, + runtime_version=runtime_version, ) # 3. Add ONNX paths to the manifest diff --git a/src/mobius/integrations/ort_genai/auto_export_test.py b/src/mobius/integrations/ort_genai/auto_export_test.py index 65416e7a9..d284b14e4 100644 --- a/src/mobius/integrations/ort_genai/auto_export_test.py +++ b/src/mobius/integrations/ort_genai/auto_export_test.py @@ -22,7 +22,9 @@ _fix_chat_template, _fix_tokenizer_config, _graph_input_names, + _inspect_decoder_abi, _introspect_outputs, + _is_single_model_decoder_package, _resolve_ort_genai_model_type, _select_ort_model_type, _write_audio_processor_config, @@ -57,6 +59,19 @@ def _mock_model_with_outputs(names: list[str]) -> ir.Model: return _mock_model(outputs=names) +def _mock_decoder_model( + *, + semantic_inputs: list[str] | None = None, + layer_indices: tuple[int, ...] = (0, 1), +) -> ir.Model: + inputs = list(semantic_inputs or ["input_ids", "attention_mask", "position_ids"]) + outputs = ["logits"] + for index in layer_indices: + inputs.extend([f"past_key_values.{index}.key", f"past_key_values.{index}.value"]) + outputs.extend([f"present.{index}.key", f"present.{index}.value"]) + return _mock_model(inputs=inputs, outputs=outputs) + + def test_moonshine_native_runtime_is_rejected(tmp_path): from mobius._model_package import ModelPackage @@ -94,7 +109,7 @@ class FakeConfig: max_position_embeddings: int = 128 return ModelPackage( - {"model": _mock_model()}, + {"model": _mock_decoder_model()}, config=FakeConfig(model_type=model_type), ) @@ -148,14 +163,12 @@ def test_gemma4_unified_model_types(self): class TestSelectOrtModelType: """Text-only / multimodal ORT model type selection (PR: text_only export).""" - def test_decoder_only_prefers_config_type(self): - # Text-only gemma-4-12B: package config carries the text sibling, HF - # reports the multimodal type. Decoder-only -> follow the package. + def test_decoder_only_uses_generic_decoder(self): assert ( _select_ort_model_type( "gemma4_unified_text", "gemma4_unified", is_decoder_only=True ) - == "gemma4_text" + == "decoder" ) def test_multimodal_keeps_hf_type(self): @@ -170,24 +183,33 @@ def test_multimodal_keeps_hf_type(self): ) def test_decoder_only_falls_back_to_hf_when_config_missing(self): - assert _select_ort_model_type(None, "qwen3", is_decoder_only=True) == "qwen2" + assert _select_ort_model_type(None, "qwen3", is_decoder_only=True) == "decoder" - def test_decoder_only_qwen3_moe_resolves_to_supported_type(self): - # hf_model_id mode: both the package config and the HF config report - # "qwen3_moe"; the decoder-only preference must still resolve through - # the alias rather than emitting the unsupported HF type. + def test_decoder_only_unknown_config_uses_generic_decoder(self): assert ( - _select_ort_model_type("qwen3_moe", "qwen3_moe", is_decoder_only=True) == "qwen3" + _select_ort_model_type("not_a_real_type", "qwen3", is_decoder_only=True) + == "decoder" ) - def test_decoder_only_unknown_config_falls_back_to_hf(self): - # An unrecognised config.model_type (not in _ORT_GENAI_MODEL_TYPE) must - # not pass straight through as an invalid ORT type; fall back to the - # known HF-derived mapping instead. + def test_decoder_only_preserves_specialized_hf_fallback(self): assert ( - _select_ort_model_type("not_a_real_type", "qwen3", is_decoder_only=True) == "qwen2" + _select_ort_model_type("not_a_real_type", "gpt2", is_decoder_only=True) == "gpt2" ) + @pytest.mark.parametrize( + ("model_type", "expected"), + [ + ("gpt2", "gpt2"), + ("lfm2", "lfm2"), + ("lfm2_vl", "lfm2"), + ("phi3", "phi3"), + ("phi3small", "phi3small"), + ("phimoe", "phimoe"), + ], + ) + def test_specialized_decoder_type_is_preserved(self, model_type, expected): + assert _select_ort_model_type(model_type, model_type, is_decoder_only=True) == expected + class TestWriteProcessorConfig: def test_no_vision_returns_none(self, tmp_path): @@ -1136,7 +1158,9 @@ def test_genai_config_json_is_written(self, tmp_path): with open(result["genai_config"]) as f: data = json.load(f) assert "model" in data - assert data["model"]["type"] == "qwen2" + assert data["model"]["type"] == "decoder" + assert data["model"]["decoder"]["inputs"]["position_ids"] == "position_ids" + assert (tmp_path / "runtime_compatibility.json").is_file() def test_rejects_generic_vision_encoder_decoder_package(self, tmp_path): import dataclasses @@ -1644,7 +1668,7 @@ class FakeConfig: pkg = ModelPackage( { - "model": _mock_model(), + "model": _mock_decoder_model(), "vision": _mock_model(), "embedding": _mock_model(), }, @@ -1754,7 +1778,7 @@ class FakeConfig: num_key_value_heads: int = 1 head_dim: int = 256 - pkg = ModelPackage({"model": _mock_model()}, config=FakeConfig()) + pkg = ModelPackage({"model": _mock_decoder_model()}, config=FakeConfig()) result = write_ort_genai_config(pkg, str(tmp_path)) assert "audio_processor" not in result @@ -2110,13 +2134,12 @@ class FakeConfig: head_dim: int = 16 max_position_embeddings: int = 128 - pkg = ModelPackage({"model": _mock_model()}, config=FakeConfig()) + pkg = ModelPackage({"model": _mock_decoder_model()}, config=FakeConfig()) result = write_ort_genai_config(pkg, str(tmp_path), hf_model_id=None) with open(result["genai_config"]) as f: data = json.load(f) - # "gemma2" maps to "gemma" in _ORT_GENAI_MODEL_TYPE - assert data["model"]["type"] == "gemma" + assert data["model"]["type"] == "decoder" def test_config_mode_qwen3_moe_emits_supported_decoder_type(self, tmp_path): """Qwen3-MoE --config exports must not emit the unsupported HF type. @@ -2450,7 +2473,7 @@ class FakeConfig: eos_token_id: int = 2 pad_token_id: int = 0 - pkg = ModelPackage({"model": _mock_model()}, config=FakeConfig()) + pkg = ModelPackage({"model": _mock_decoder_model()}, config=FakeConfig()) result = write_ort_genai_config(pkg, str(tmp_path), hf_model_id=None) with open(result["genai_config"]) as f: @@ -2668,7 +2691,7 @@ class FakeConfig: eos_token_id: list = dataclasses.field(default_factory=lambda: [1, 106]) pad_token_id: int = 0 - pkg = ModelPackage({"model": _mock_model()}, config=FakeConfig()) + pkg = ModelPackage({"model": _mock_decoder_model()}, config=FakeConfig()) result = write_ort_genai_config(pkg, str(tmp_path), hf_model_id=None) with open(result["genai_config"]) as f: @@ -3037,9 +3060,29 @@ class FakeConfig: ir.Node(op_type=op_type, domain=domain, inputs=[], num_outputs=1) for op_type, domain in node_op_types ] + inputs = [ + ir.Value(name=name) + for name in ( + "input_ids", + "past_key_values.0.key", + "past_key_values.0.value", + "past_key_values.1.conv_state", + "past_key_values.1.recurrent_state", + ) + ] + outputs = [ + ir.Value(name=name) + for name in ( + "logits", + "present.0.key", + "present.0.value", + "present.1.conv_state", + "present.1.recurrent_state", + ) + ] graph = ir.Graph( - inputs=[ir.Value(name="input_ids")], - outputs=[ir.Value(name="logits")], + inputs=inputs, + outputs=outputs, nodes=nodes, name="decoder", ) @@ -3235,6 +3278,212 @@ def test_returns_none_for_missing_model(self): assert _count_cache_layer_slots(None) is None +class TestGenericDecoderAbi: + def test_requires_a_single_decoder_graph(self): + from mobius._model_package import ModelPackage + + decoder = _mock_decoder_model() + assert _is_single_model_decoder_package(ModelPackage({"model": decoder})) + assert not _is_single_model_decoder_package( + ModelPackage({"model": decoder, "encoder": _mock_model()}) + ) + + def test_sparse_cache_indices_preserve_global_slots_and_exact_names(self): + model = _mock_model( + inputs=[ + "input_ids", + "attention_mask", + "cache.1.k", + "cache.1.v", + "cache.3.k", + "cache.3.v", + ], + outputs=[ + "logits", + "next.1.k", + "next.1.v", + "next.3.k", + "next.3.v", + ], + ) + # Rename suffixes to the released semantic key/value vocabulary while + # retaining non-default prefixes. + for value in model.graph.inputs: + if value.name is not None: + value.name = value.name.replace(".k", ".key").replace(".v", ".value") + for value in model.graph.outputs: + if value.name is not None: + value.name = value.name.replace(".k", ".key").replace(".v", ".value") + + abi = _inspect_decoder_abi(model, model_type="decoder") + + assert abi.cache_slots == 4 + assert abi.inputs["past_key_names"] == "cache.%d.key" + assert abi.outputs["present_value_names"] == "next.%d.value" + + def test_omits_optimized_away_optional_inputs(self): + abi = _inspect_decoder_abi( + _mock_decoder_model(semantic_inputs=["input_ids"]), model_type="decoder" + ) + assert set(abi.inputs) == {"input_ids", "past_key_names", "past_value_names"} + + def test_accepts_released_recurrent_state_pair(self): + model = _mock_decoder_model(layer_indices=(1,)) + model.graph.inputs.extend( + [ + ir.Value(name="past_key_values.0.conv_state"), + ir.Value(name="past_key_values.0.recurrent_state"), + ] + ) + model.graph.outputs.extend( + [ + ir.Value(name="present.0.conv_state"), + ir.Value(name="present.0.recurrent_state"), + ] + ) + abi = _inspect_decoder_abi(model, model_type="decoder") + assert abi.has_recurrent_state + assert abi.cache_slots == 2 + + def test_accepts_paired_sequence_length_inputs(self): + abi = _inspect_decoder_abi( + _mock_decoder_model( + semantic_inputs=[ + "input_ids", + "current_sequence_length", + "past_sequence_length", + ] + ), + model_type="decoder", + ) + assert abi.inputs["current_sequence_length"] == "current_sequence_length" + assert abi.inputs["past_sequence_length"] == "past_sequence_length" + + def test_rejects_unpaired_sequence_length_input(self): + with pytest.raises(ValueError, match="only as a pair"): + _inspect_decoder_abi( + _mock_decoder_model(semantic_inputs=["input_ids", "past_sequence_length"]), + model_type="decoder", + ) + + def test_rejects_mobius_gpt2_separate_cache_abi(self): + with pytest.raises(ValueError, match="rank-5 combined KV-cache"): + _inspect_decoder_abi(_mock_decoder_model(), model_type="gpt2") + + @pytest.mark.parametrize( + "inputs,outputs,message", + [ + ( + ["input_ids", "key_cache.0", "value_cache.0"], + ["logits", "present_key_cache.0", "present_value_cache.0"], + "cannot automatically supply", + ), + ( + [ + "input_ids", + "past_key_values.0.key", + "past_key_values.0.value", + "past_key_values.1.ssm_state", + ], + ["logits", "present.0.key", "present.0.value", "present.1.ssm_state"], + "state kinds", + ), + ], + ) + def test_rejects_unreleased_state_topologies(self, inputs, outputs, message): + with pytest.raises(ValueError, match=message): + _inspect_decoder_abi( + _mock_model(inputs=inputs, outputs=outputs), model_type="decoder" + ) + + +def test_generic_decoder_runtime_compatibility_metadata(tmp_path): + result = write_ort_genai_config( + _make_fake_llm_pkg("unknown_architecture"), + str(tmp_path), + runtime_version="0.15.2", + ) + with open(result["runtime_compatibility"], encoding="utf-8") as handle: + metadata = json.load(handle) + assert metadata == { + "runtime": "onnxruntime-genai", + "model_type": "decoder", + "minimum_version": "0.14.0", + "tested_versions": ["0.14.1", "0.15.2"], + "uses_main_only_state_groups": False, + "heterogeneous_state_manifest": ( + "deferred: https://github.com/onnxruntime/mobius/issues/605" + ), + } + + +@pytest.mark.parametrize( + ("model_type", "config_overrides"), + [ + pytest.param("llama", {}, id="dense"), + pytest.param("qwen3_moe", {}, id="moe"), + pytest.param("gemma", {"tie_word_embeddings": True}, id="tied"), + pytest.param( + "qwen2", + {"quantization_config": {"quant_method": "matmul_nbits"}}, + id="quantized", + ), + ], +) +def test_generic_decoder_schema_is_architecture_and_weight_agnostic( + tmp_path, model_type, config_overrides +): + pkg = _make_fake_llm_pkg(model_type) + for name, value in config_overrides.items(): + setattr(pkg.config, name, value) + + result = write_ort_genai_config(pkg, str(tmp_path)) + with open(result["genai_config"], encoding="utf-8") as handle: + config = json.load(handle) + + assert config["model"]["type"] == "decoder" + assert config["model"]["vocab_size"] == 256 + assert config["model"]["context_length"] == 4096 + assert config["model"]["decoder"]["num_hidden_layers"] == 2 + assert config["model"]["decoder"]["inputs"] == { + "input_ids": "input_ids", + "attention_mask": "attention_mask", + "position_ids": "position_ids", + "past_key_names": "past_key_values.%d.key", + "past_value_names": "past_key_values.%d.value", + } + assert config["model"]["decoder"]["outputs"] == { + "logits": "logits", + "present_key_names": "present.%d.key", + "present_value_names": "present.%d.value", + } + assert config["search"]["past_present_share_buffer"] is False + + +def test_generic_decoder_rejects_pre_014_runtime(tmp_path): + with pytest.raises(ValueError, match=r">= 0\.14\.0"): + write_ort_genai_config( + _make_fake_llm_pkg("qwen2"), + str(tmp_path), + runtime_version="0.13.0", + ) + assert not (tmp_path / "genai_config.json").exists() + + +def test_gpt2_specialized_runtime_rejects_separate_cache_graph(tmp_path): + with pytest.raises(ValueError, match="rank-5 combined KV-cache"): + write_ort_genai_config(_make_fake_llm_pkg("gpt2"), str(tmp_path)) + assert not (tmp_path / "genai_config.json").exists() + + +@pytest.mark.parametrize("model_type", ["phi3", "phi3small", "phimoe"]) +def test_phi3_longrope_runtime_type_is_preserved(tmp_path, model_type): + result = write_ort_genai_config(_make_fake_llm_pkg(model_type), str(tmp_path)) + with open(result["genai_config"], encoding="utf-8") as handle: + config = json.load(handle) + assert config["model"]["type"] == model_type + + class TestGemma4RealModel: """Build a real tiny Gemma4 model and verify genai config inputs.""" @@ -3401,13 +3650,17 @@ def test_text_only_genai_config_is_decoder_only(self, tmp_path): with open(result["genai_config"]) as f: data = json.load(f) - # ORT-GenAI type resolved from pkg.config.model_type (gemma4_text), - # NOT the multimodal HF gemma4_unified -> gemma4. - assert data["model"]["type"] == "gemma4_text" + assert data["model"]["type"] == "decoder" # Decoder-only: input_ids decoder, no multimodal sections. assert "vision" not in data["model"] assert "audio" not in data["model"] assert "input_ids" in data["model"]["decoder"]["inputs"] + assert data["model"]["decoder"]["sliding_window"] == { + "window_size": 8, + "slide_key_value_cache": False, + "slide_inputs": False, + "layers": [0], + } # No multimodal processor artifacts. assert "processor_config" not in result assert "audio_processor" not in result diff --git a/src/mobius/integrations/ort_genai/genai_config.py b/src/mobius/integrations/ort_genai/genai_config.py index 0a75294d4..b231d13b5 100644 --- a/src/mobius/integrations/ort_genai/genai_config.py +++ b/src/mobius/integrations/ort_genai/genai_config.py @@ -161,6 +161,8 @@ class GenaiConfigGenerator: :func:`_default_decoder_inputs`. Must already include KV cache template entries (``past_key_names``, ``past_value_names``). + decoder_outputs: Explicit decoder output mapping derived from the + graph, including logits and present-cache templates. """ def __init__( @@ -179,11 +181,13 @@ def __init__( eos_token_id: int | list[int] | None = None, pad_token_id: int | None = None, decoder_inputs: dict[str, str] | None = None, + decoder_outputs: dict[str, str] | None = None, decoder_filename: str | None = None, supports_in_place_kv_cache: bool | None = None, decoder_graph_capture: bool | None = None, layer_types: list[str] | None = None, conv_cache_size: int | None = None, + sliding_window: dict[str, Any] | None = None, ): self.model_type = model_type self.vocab_size = vocab_size @@ -200,6 +204,7 @@ def __init__( # Explicit decoder inputs (from graph introspection); None -> use defaults self._decoder_inputs = decoder_inputs + self._decoder_outputs = decoder_outputs # Explicit decoder filename; None -> use "model.onnx" self._decoder_filename = decoder_filename # Whether the exported decoder ONNX graph supports in-place KV-cache @@ -211,6 +216,7 @@ def __init__( self._decoder_graph_capture = decoder_graph_capture self._layer_types = layer_types self._conv_cache_size = conv_cache_size + self._sliding_window = sliding_window # Optional VLM fields (set via with_vision()) self._vision: dict[str, Any] | None = None @@ -236,9 +242,11 @@ def from_config( eos_token_id: int | list[int] | None = None, pad_token_id: int | None = None, decoder_inputs: dict[str, str] | None = None, + decoder_outputs: dict[str, str] | None = None, decoder_filename: str | None = None, supports_in_place_kv_cache: bool | None = None, num_cache_layer_slots: int | None = None, + sliding_window: dict[str, Any] | None = None, ) -> GenaiConfigGenerator: """Create a generator from a BaseModelConfig-like dataclass. @@ -284,6 +292,7 @@ def from_config( eos_token_id=eos_token_id, pad_token_id=pad, decoder_inputs=decoder_inputs, + decoder_outputs=decoder_outputs, decoder_filename=decoder_filename, supports_in_place_kv_cache=supports_in_place_kv_cache, layer_types=getattr(config, "layer_types", None), @@ -292,6 +301,7 @@ def from_config( if hasattr(config, "short_conv_kernel") else None ), + sliding_window=sliding_window, ) def with_vision( @@ -531,7 +541,11 @@ def generate(self) -> dict[str, Any]: "head_size": self.head_dim, "hidden_size": self.hidden_size, "inputs": decoder_inputs, - "outputs": _default_decoder_outputs(), + "outputs": ( + dict(self._decoder_outputs) + if self._decoder_outputs is not None + else _default_decoder_outputs() + ), "num_attention_heads": self.num_attention_heads, "num_hidden_layers": self.num_hidden_layers, "num_key_value_heads": self.num_key_value_heads, @@ -541,8 +555,10 @@ def generate(self) -> dict[str, Any]: decoder["conv_cache_size"] = ( self._conv_cache_size if self._conv_cache_size is not None else 3 ) - decoder["inputs"]["past_conv_names"] = "past_key_values.%d.conv_state" - decoder["outputs"]["present_conv_names"] = "present.%d.conv_state" + decoder["inputs"].setdefault("past_conv_names", "past_key_values.%d.conv_state") + decoder["outputs"].setdefault("present_conv_names", "present.%d.conv_state") + if self._sliding_window is not None: + decoder["sliding_window"] = self._sliding_window # Model section model: dict[str, Any] = { diff --git a/tests/gguf_small_model_runtime_integration_test.py b/tests/gguf_small_model_runtime_integration_test.py index 93cd2f887..c13075ad9 100644 --- a/tests/gguf_small_model_runtime_integration_test.py +++ b/tests/gguf_small_model_runtime_integration_test.py @@ -629,3 +629,79 @@ def capture_save(package: ModelPackage, *args: object, **kwargs: object) -> None np.testing.assert_allclose(ort_logits, reference_logits, rtol=1e-4, atol=2e-4) np.testing.assert_array_equal(generated, case.generated_tokens) + + +@pytest.mark.integration +@pytest.mark.integration_slow +def test_smollm_generic_ort_genai_generation(tmp_path: Path) -> None: + """The one evidenced GGUF route loads through ORT GenAI's generic decoder.""" + from importlib.metadata import version + + ort_genai = pytest.importorskip("onnxruntime_genai") + from mobius.integrations.ort_genai import write_ort_genai_config + + case = _CASES[0] + gguf_path = Path( + hf_hub_download( + repo_id=case.gguf_repository, + revision=case.gguf_revision, + filename=case.gguf_filename, + ) + ) + output_dir = tmp_path / "smollm-ort-genai" + captured: list[ModelPackage] = [] + original_save = ModelPackage.save + + def capture_save(package: ModelPackage, *args: object, **kwargs: object) -> None: + captured.append(package) + original_save(package, *args, **kwargs) + + with mock.patch.object(ModelPackage, "save", capture_save): + main( + [ + "build-gguf", + str(gguf_path), + "--output", + str(output_dir), + "--dtype", + "f32", + "--execution-provider", + "cpu", + "--runtime", + "onnx-genai", + "--runtime-version", + "1.29.0", + "--tokenizer-repository", + case.tokenizer_repository, + "--tokenizer-revision", + case.tokenizer_revision, + "--local-files-only", + ] + ) + + assert len(captured) == 1 + write_ort_genai_config( + captured[0], + str(output_dir), + runtime_version=version("onnxruntime-genai"), + ) + config = json.loads((output_dir / "genai_config.json").read_text()) + assert config["model"]["type"] == "decoder" + + model = ort_genai.Model(str(output_dir)) + tokenizer = ort_genai.Tokenizer(model) + prompt_ids = tokenizer.encode(case.prompt) + params = ort_genai.GeneratorParams(model) + params.set_search_options( + max_length=len(prompt_ids) + len(case.generated_tokens), + do_sample=False, + ) + generator = ort_genai.Generator(model, params) + generator.append_tokens(prompt_ids) + + generated: list[int] = [] + for _ in case.generated_tokens: + generator.generate_next_token() + generated.append(generator.get_next_tokens()[0]) + + assert generated == list(case.generated_tokens) From 7e3e4d5204fd638385c2ab12182728015a8917a2 Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Mon, 24 Aug 2026 20:32:21 -0700 Subject: [PATCH 2/3] Record generic decoder implementation attribution Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Justin Chu From f43a31fe23522f7c4ad234781d4b465926f3057b Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Tue, 25 Aug 2026 12:31:43 -0700 Subject: [PATCH 3/3] Validate generic decoder configs on latest OGA Limit recorded runtime coverage to onnxruntime-genai 0.15.2 and keep the real SmolLM generation test independent of the ONNX GenAI runtime-evidence packaging path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Justin Chu --- docs/cli_reference.md | 4 ++-- src/mobius/integrations/ort_genai/auto_export.py | 4 +--- .../integrations/ort_genai/auto_export_test.py | 16 ++++------------ .../gguf_small_model_runtime_integration_test.py | 11 ++--------- 4 files changed, 9 insertions(+), 26 deletions(-) diff --git a/docs/cli_reference.md b/docs/cli_reference.md index ca307ef2f..a1e5bd589 100644 --- a/docs/cli_reference.md +++ b/docs/cli_reference.md @@ -192,8 +192,8 @@ dense cache. The deferred state-manifest work is tracked by [#605](https://github.com/onnxruntime/mobius/issues/605). Each export also writes `runtime_compatibility.json`. Generic decoder metadata -records the minimum runtime version and the released versions exercised by Mobius -(0.14.1 and 0.15.2); it never emits the unreleased `decoder.state_groups` field. +records the minimum runtime version and the latest stable release exercised by +Mobius (0.15.2); it never emits the unreleased `decoder.state_groups` field. Generic config availability does not promote a GGUF runtime verdict: the only runtime-supported GGUF route remains the exact pinned SmolLM F16/CPU package, while SmolLM2 remains rejected because its GGUF padding-token metadata conflicts with the diff --git a/src/mobius/integrations/ort_genai/auto_export.py b/src/mobius/integrations/ort_genai/auto_export.py index 222458702..da8d0fc4b 100644 --- a/src/mobius/integrations/ort_genai/auto_export.py +++ b/src/mobius/integrations/ort_genai/auto_export.py @@ -149,7 +149,7 @@ def _revision_kwargs(revision: str | None) -> dict[str, str]: "phimoe": "phimoe", } _GENERIC_DECODER_MIN_VERSION = (0, 14, 0) -_GENERIC_DECODER_TESTED_VERSIONS = ("0.14.1", "0.15.2") +_GENERIC_DECODER_TESTED_VERSIONS = ("0.15.2",) _DECODER_SEMANTIC_INPUTS = frozenset( { "input_ids", @@ -1801,8 +1801,6 @@ def write_ort_genai_config( directory rather than a HuggingFace model ID. trust_remote_code: Allow custom HuggingFace configuration code when resolving token IDs and model type. - revision: Optional immutable HuggingFace revision used for every remote - configuration, tokenizer, and processor request. runtime_version: Optional onnxruntime-genai version that will consume the package. Generic decoder packages reject versions older than 0.14.0. diff --git a/src/mobius/integrations/ort_genai/auto_export_test.py b/src/mobius/integrations/ort_genai/auto_export_test.py index d284b14e4..1fc301542 100644 --- a/src/mobius/integrations/ort_genai/auto_export_test.py +++ b/src/mobius/integrations/ort_genai/auto_export_test.py @@ -2141,16 +2141,8 @@ class FakeConfig: data = json.load(f) assert data["model"]["type"] == "decoder" - def test_config_mode_qwen3_moe_emits_supported_decoder_type(self, tmp_path): - """Qwen3-MoE --config exports must not emit the unsupported HF type. - - Regression: a decoder-only Qwen3-MoE package wrote - ``"type": "qwen3_moe"`` into genai_config.json, and loading it raised - ``RuntimeError: Unsupported model_type in config.json: qwen3_moe`` - because ORT GenAI's LLM registry has no such type. The emitted type is - literally ``"qwen3"``: ORT GenAI's tokenizer tag fallback keys the - Qwen3 reasoning-token IDs off that name. - """ + def test_config_mode_qwen3_moe_emits_generic_decoder_type(self, tmp_path): + """Qwen3-MoE uses the graph-driven generic decoder contract.""" from mobius.integrations.ort_genai.auto_export import write_ort_genai_config pkg = _make_fake_llm_pkg("qwen3_moe") @@ -2158,7 +2150,7 @@ def test_config_mode_qwen3_moe_emits_supported_decoder_type(self, tmp_path): with open(result["genai_config"]) as f: data = json.load(f) - assert data["model"]["type"] == "qwen3" + assert data["model"]["type"] == "decoder" def test_config_mode_gemma3_text_vlm_uses_multimodal_model_type(self, tmp_path): """Gemma3 VLM --config exports use ORT's multimodal gemma3 type.""" @@ -3409,7 +3401,7 @@ def test_generic_decoder_runtime_compatibility_metadata(tmp_path): "runtime": "onnxruntime-genai", "model_type": "decoder", "minimum_version": "0.14.0", - "tested_versions": ["0.14.1", "0.15.2"], + "tested_versions": ["0.15.2"], "uses_main_only_state_groups": False, "heterogeneous_state_manifest": ( "deferred: https://github.com/onnxruntime/mobius/issues/605" diff --git a/tests/gguf_small_model_runtime_integration_test.py b/tests/gguf_small_model_runtime_integration_test.py index c13075ad9..e78be7ea2 100644 --- a/tests/gguf_small_model_runtime_integration_test.py +++ b/tests/gguf_small_model_runtime_integration_test.py @@ -667,15 +667,6 @@ def capture_save(package: ModelPackage, *args: object, **kwargs: object) -> None "f32", "--execution-provider", "cpu", - "--runtime", - "onnx-genai", - "--runtime-version", - "1.29.0", - "--tokenizer-repository", - case.tokenizer_repository, - "--tokenizer-revision", - case.tokenizer_revision, - "--local-files-only", ] ) @@ -683,6 +674,8 @@ def capture_save(package: ModelPackage, *args: object, **kwargs: object) -> None write_ort_genai_config( captured[0], str(output_dir), + hf_model_id=case.tokenizer_repository, + revision=case.tokenizer_revision, runtime_version=version("onnxruntime-genai"), ) config = json.loads((output_dir / "genai_config.json").read_text())