Skip to content

Commit cd4b679

Browse files
jiafatomCopilot
andauthored
Fix genai_config.json discovery for nested multi-component model layouts (microsoft#2529)
## Description The evaluator previously assumed `genai_config.json` is in the same directory as the ONNX model file (`Path(model.model_path).parent`). For nested multi-component model layouts (e.g., `models/decoder/model.onnx` where `genai_config.json` lives at `models/`), this caused evaluation to fail because the config file was not found. This is the case for models exported by `MobiusBuilder` (gemma4, etc.), which creates a nested layout: ``` models/ ├── genai_config.json ├── decoder/model.onnx ├── vision_encoder/model.onnx ├── audio_encoder/model.onnx └── embedding/model.onnx ``` When the evaluator loads the decoder (`models/decoder/model.onnx`), `Path(model.model_path).parent` resolves to `models/decoder/`, missing the `genai_config.json` at `models/`. ## Changes - Added module-level `_find_genai_config()` helper that searches upward (up to 3 levels) from the ONNX file's parent directory to find `genai_config.json` - Added module-level `_get_genai_model_dir()` helper that returns the root model directory containing `genai_config.json` (falls back to ONNX file's parent if not found) - `OnnxEvaluator` static methods wrap these for backward compatibility - Updated all usages across `OnnxEvaluator`, `LMEvaluator`, and `MTEBEvaluator` ## Motivation This fix enables evaluation of multimodal models with nested component layouts (e.g., gemma4-E2B exported by MobiusBuilder) using Olive's built-in evaluator. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: jiafatom <jiafatom@users.noreply.github.com>
1 parent 8452622 commit cd4b679

3 files changed

Lines changed: 137 additions & 13 deletions

File tree

.azure_pipelines/job_templates/olive-test-linux-gpu-template.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ parameters:
88
base_image: 'mcr.microsoft.com/mirror/nvcr/nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04'
99
trt_version: '10.5.0.18-1+cuda12.6'
1010
python_version: '3.12'
11-
onnxruntime: 'onnxruntime-gpu'
11+
onnxruntime: 'onnxruntime-gpu<1.27'
1212
torch: 'torch'
1313
requirements_file: 'requirements-test-gpu.txt'
1414
test_script: 'run_test.sh'
@@ -66,8 +66,8 @@ jobs:
6666
-v $(Build.SourcesDirectory)/logs:/logs \
6767
${{ parameters.docker_image }} \
6868
bash .azure_pipelines/scripts/${{ parameters.test_script }} \
69-
${{ parameters.torch }} \
70-
${{ parameters.onnxruntime }} \
69+
"${{ parameters.torch }}" \
70+
"${{ parameters.onnxruntime }}" \
7171
${{ parameters.onnxruntime_nightly }} \
7272
test/${{ parameters.requirements_file }} \
7373
${{ parameters.test_path }} \

olive/evaluator/olive_evaluator.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,35 @@ def get_inference_settings(metric: Metric, model: ONNXModelHandler) -> dict[str,
482482
return inference_settings
483483

484484

485+
def _find_genai_config(model: ONNXModelHandler) -> Optional[Path]:
486+
"""Find genai_config.json by searching upward from the ONNX file's parent directory.
487+
488+
Returns the Path to genai_config.json if found, or None. Searches at most
489+
3 levels up to avoid traversing unrelated directories.
490+
"""
491+
candidate = Path(model.model_path).parent
492+
for _ in range(3):
493+
genai_path = candidate / "genai_config.json"
494+
if genai_path.is_file():
495+
return genai_path
496+
parent = candidate.parent
497+
if parent == candidate:
498+
break
499+
candidate = parent
500+
return None
501+
502+
503+
def _get_genai_model_dir(model: ONNXModelHandler) -> str:
504+
"""Get the ORT GenAI model root directory (where genai_config.json lives).
505+
506+
Falls back to the ONNX file's parent directory if genai_config.json is not found.
507+
"""
508+
genai_config_path = _find_genai_config(model)
509+
if genai_config_path is not None:
510+
return str(genai_config_path.parent)
511+
return str(Path(model.model_path).parent)
512+
513+
485514
@Registry.register(str(Framework.ONNX))
486515
@Registry.register("OnnxEvaluator")
487516
class OnnxEvaluator(_OliveEvaluator, OnnxEvaluatorMixin):
@@ -584,9 +613,14 @@ def _inference(
584613

585614
@staticmethod
586615
def _load_genai_config(model: ONNXModelHandler) -> Optional[dict]:
587-
"""Load genai_config.json from the model directory, or return None if not found."""
588-
genai_config_path = Path(model.model_path).parent / "genai_config.json"
589-
if not genai_config_path.exists():
616+
"""Load genai_config.json from the model directory, or return None if not found.
617+
618+
Searches upward from the ONNX file's parent directory to support nested
619+
multi-component model layouts (e.g. ``models/decoder/model.onnx`` where
620+
``genai_config.json`` lives at ``models/``).
621+
"""
622+
genai_config_path = _find_genai_config(model)
623+
if genai_config_path is None:
590624
return None
591625
import json
592626

@@ -845,7 +879,7 @@ def _inference_vision_genai(
845879
except ImportError as e:
846880
raise ImportError("Pillow is required for vision evaluation. Install it with: pip install Pillow") from e
847881

848-
model_dir = str(Path(model.model_path).parent)
882+
model_dir = _get_genai_model_dir(model)
849883

850884
# Default max_length; can be overridden per-sample from the data config.
851885
default_max_length = 4096
@@ -918,6 +952,11 @@ def _inference_vision_genai(
918952
prompt = tokenizer.apply_chat_template(messages_json, add_generation_prompt=True)
919953
inputs = processor(prompt, images=images)
920954

955+
# Remove audio_features if present but not needed (vision-only inference)
956+
# to avoid "Model output was not found: audio_features" errors
957+
if "audio_features" in inputs:
958+
del inputs["audio_features"]
959+
921960
params = og.GeneratorParams(og_model)
922961
params.set_search_options(max_length=max_length, do_sample=False)
923962

@@ -991,7 +1030,7 @@ def _inference_text_genai(
9911030

9921031
import soundfile as sf
9931032

994-
model_dir = str(Path(model.model_path).parent)
1033+
model_dir = _get_genai_model_dir(model)
9951034

9961035
# Read genai_config to determine model properties
9971036
with (Path(model_dir) / "genai_config.json").open() as f:
@@ -1125,7 +1164,7 @@ def _inference_text_genai_streaming(
11251164

11261165
import json
11271166

1128-
model_dir = str(Path(model.model_path).parent)
1167+
model_dir = _get_genai_model_dir(model)
11291168

11301169
with (Path(model_dir) / "genai_config.json").open() as f:
11311170
genai_config = json.load(f)
@@ -1947,7 +1986,7 @@ def evaluate(
19471986
}
19481987
elif self.model_class == "ortgenai":
19491988
init_args = {
1950-
"pretrained": str(Path(model.model_path).parent),
1989+
"pretrained": _get_genai_model_dir(model),
19511990
"ep": self.ep or execution_providers,
19521991
"ep_options": self.ep_options,
19531992
"device": device,
@@ -2062,8 +2101,8 @@ def evaluate(
20622101
model_class = "hf"
20632102
elif isinstance(model, ONNXModelHandler):
20642103
# ModelBuilder outputs ONNXModelHandler but with genai_config.json
2065-
genai_config = Path(model.model_path).parent / "genai_config.json"
2066-
model_class = "ortgenai" if genai_config.exists() else "ort"
2104+
genai_config_path = _find_genai_config(model)
2105+
model_class = "ortgenai" if genai_config_path is not None else "ort"
20672106
else:
20682107
raise ValueError(
20692108
"Unable to auto-detect model_class for MTEBEvaluator from model handler "
@@ -2098,7 +2137,7 @@ def evaluate(
20982137
)
20992138
elif model_class == "ortgenai":
21002139
mteb_model = MTEBORTGenAIEvaluator(
2101-
pretrained=str(Path(model.model_path).parent),
2140+
pretrained=_get_genai_model_dir(model),
21022141
batch_size=self.batch_size,
21032142
max_length=self.max_length,
21042143
ep=self.ep

test/evaluator/test_olive_evaluator.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,3 +784,88 @@ def test_standard_vision_when_no_genai_config(self, tmp_path):
784784

785785
mock_vision.assert_called_once()
786786
mock_genai.assert_not_called()
787+
788+
789+
class TestFindGenaiConfig:
790+
"""Tests for _find_genai_config upward search behavior."""
791+
792+
def test_find_genai_config_same_directory(self, tmp_path):
793+
"""Find genai_config.json in the same directory as the ONNX file."""
794+
import json
795+
796+
from olive.evaluator.olive_evaluator import _find_genai_config
797+
from olive.model.handler.onnx import ONNXModelHandler
798+
799+
model_dir = tmp_path / "models"
800+
model_dir.mkdir()
801+
onnx_file = model_dir / "model.onnx"
802+
onnx_file.write_text("")
803+
config_path = model_dir / "genai_config.json"
804+
config_path.write_text(json.dumps({"model": {"type": "test"}}))
805+
806+
model = MagicMock(spec=ONNXModelHandler)
807+
model.model_path = str(onnx_file)
808+
809+
result = _find_genai_config(model)
810+
assert result == config_path
811+
812+
def test_find_genai_config_parent_directory(self, tmp_path):
813+
"""Find genai_config.json one level up (nested model layout)."""
814+
import json
815+
816+
from olive.evaluator.olive_evaluator import _find_genai_config
817+
from olive.model.handler.onnx import ONNXModelHandler
818+
819+
model_dir = tmp_path / "models"
820+
model_dir.mkdir()
821+
decoder_dir = model_dir / "decoder"
822+
decoder_dir.mkdir()
823+
onnx_file = decoder_dir / "model.onnx"
824+
onnx_file.write_text("")
825+
config_path = model_dir / "genai_config.json"
826+
config_path.write_text(json.dumps({"model": {"type": "gemma4"}}))
827+
828+
model = MagicMock(spec=ONNXModelHandler)
829+
model.model_path = str(onnx_file)
830+
831+
result = _find_genai_config(model)
832+
assert result == config_path
833+
834+
def test_find_genai_config_not_found(self, tmp_path):
835+
"""Return None when genai_config.json does not exist."""
836+
from olive.evaluator.olive_evaluator import _find_genai_config
837+
from olive.model.handler.onnx import ONNXModelHandler
838+
839+
decoder_dir = tmp_path / "models" / "decoder"
840+
decoder_dir.mkdir(parents=True)
841+
onnx_file = decoder_dir / "model.onnx"
842+
onnx_file.write_text("")
843+
844+
model = MagicMock(spec=ONNXModelHandler)
845+
model.model_path = str(onnx_file)
846+
847+
result = _find_genai_config(model)
848+
assert result is None
849+
850+
def test_find_genai_config_ignores_directory(self, tmp_path):
851+
"""Ignore a directory named genai_config.json."""
852+
from olive.evaluator.olive_evaluator import _find_genai_config
853+
from olive.model.handler.onnx import ONNXModelHandler
854+
855+
model_dir = tmp_path / "models"
856+
model_dir.mkdir()
857+
# Create a directory (not file) named genai_config.json
858+
fake_dir = model_dir / "genai_config.json"
859+
fake_dir.mkdir()
860+
861+
decoder_dir = model_dir / "decoder"
862+
decoder_dir.mkdir()
863+
onnx_file = decoder_dir / "model.onnx"
864+
onnx_file.write_text("")
865+
866+
model = MagicMock(spec=ONNXModelHandler)
867+
model.model_path = str(onnx_file)
868+
869+
# Should not find the directory, should return None
870+
result = _find_genai_config(model)
871+
assert result is None

0 commit comments

Comments
 (0)