Skip to content

Commit 3c13f57

Browse files
authored
Support HY-MT evaluation workflows (microsoft#2482)
## Summary - Update lm-eval integration to preserve recipe bootstrap settings. - Improve ORT GenAI evaluation provider handling and runtime GenAI search options. - Keep model-load-only Hugging Face kwargs out of generation config loading. - Add accelerator normalization coverage for WebGPU export-only and runtime-required paths. ## Validation - Ran py_compile for olive/evaluator/lmeval_ort.py, olive/evaluator/olive_evaluator.py, olive/model/handler/mixin/hf.py, and test/hardware/test_accelerator.py. - Ran git diff --check for the touched Olive files. - VS Code diagnostics reported no errors for touched accelerator/evaluator files. - Targeted pytest was attempted but blocked by missing pydantic in this environment.
1 parent c10e5bc commit 3c13f57

4 files changed

Lines changed: 81 additions & 10 deletions

File tree

olive/evaluator/lmeval_ort.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import logging
99
from abc import abstractmethod
1010
from pathlib import Path
11+
from typing import ClassVar
1112

1213
import torch
1314
import torch.nn.functional as F
@@ -515,6 +516,23 @@ def initialize_buffers(self, batch_size: int, max_length: int):
515516
class LMEvalORTGenAIEvaluator(LMEvalOnnxBase):
516517
"""Evaluate a model using ONNX Runtime GenAI."""
517518

519+
_ORT_GENAI_PROVIDER_NAMES: ClassVar[dict[str, str]] = {
520+
"cpu": "CPU",
521+
"cuda": "cuda",
522+
"dml": "DML",
523+
"openvino": "OpenVINO",
524+
"qnn": "QNN",
525+
"vitisai": "VitisAI",
526+
"webgpu": "WebGPU",
527+
"nvtensorrtrtx": "NvTensorRtRtx",
528+
}
529+
530+
@classmethod
531+
def _normalize_provider_name(cls, ep: str) -> tuple[str, str]:
532+
"""Return the normalized Olive EP key and the provider name expected by ORT GenAI."""
533+
normalized_ep = str(ep).lower().replace("executionprovider", "")
534+
return normalized_ep, cls._ORT_GENAI_PROVIDER_NAMES.get(normalized_ep, normalized_ep)
535+
518536
def __init__(
519537
self,
520538
pretrained: str,
@@ -541,12 +559,14 @@ def __init__(
541559

542560
self.config = og.Config(pretrained)
543561
if ep != "follow_config":
544-
ep = ep.lower().replace("executionprovider", "")
562+
# Accept Olive-style EP names from recipes while calling ORT GenAI with
563+
# the provider names used in genai_config.json/session options.
564+
ep, provider_name = self._normalize_provider_name(ep)
545565
self.config.clear_providers()
546566
if ep != "cpu":
547-
self.config.append_provider(ep)
567+
self.config.append_provider(provider_name)
548568
for key, value in (ep_options or {}).items():
549-
self.config.set_provider_option(ep, key, value)
569+
self.config.set_provider_option(provider_name, key, value)
550570
self.model = og.Model(self.config)
551571
self.tokenizer = og.Tokenizer(self.model)
552572
self._pretrained = str(pretrained)
@@ -568,10 +588,16 @@ def __init__(
568588
# and first/scalar for loglikelihood (TemplateLM.eot_token_id expects int).
569589
self._eos_token_ids = list(eot) if isinstance(eot, list) else [eot]
570590
self._eot_token_id = self._eos_token_ids[0]
591+
# Mirror the exported GenAI cache-sharing setting when creating GeneratorParams.
592+
# Artifacts with shared past/present buffers require the same search option at runtime.
593+
self._past_present_share_buffer = genai_config["search"].get("past_present_share_buffer", False)
571594
self.params = og.GeneratorParams(self.model)
572-
self.params.set_search_options(max_length=self.max_length, past_present_share_buffer=False)
595+
self.params.set_search_options(
596+
max_length=self.max_length,
597+
past_present_share_buffer=self._past_present_share_buffer,
598+
)
573599

574-
self.device = device
600+
self._device = device
575601
self._returns_full_logits = self._detect_full_logits()
576602

577603
@property
@@ -593,7 +619,11 @@ def _detect_full_logits(self) -> bool:
593619
try:
594620
dummy_len = 3
595621
params = og.GeneratorParams(self.model)
596-
params.set_search_options(max_length=self.max_length, past_present_share_buffer=False, batch_size=1)
622+
params.set_search_options(
623+
max_length=self.max_length,
624+
past_present_share_buffer=self._past_present_share_buffer,
625+
batch_size=1,
626+
)
597627
generator = og.Generator(self.model, params)
598628
dummy_ids = [[self._eot_token_id] * dummy_len]
599629
generator.append_tokens(dummy_ids)
@@ -633,10 +663,10 @@ def model_call(self, input_ids: torch.Tensor, cont_len: int = 0) -> torch.Tensor
633663
n_logits = max(cont_len, 1)
634664
prefix_len = seq_len - n_logits
635665
generator.append_tokens(input_ids[:, : prefix_len + 1].tolist())
636-
all_logits = [torch.from_numpy(generator.get_logits()).to(self.device)]
666+
all_logits = [torch.from_numpy(generator.get_logits()).to(self._device)]
637667
for i in range(prefix_len + 1, seq_len):
638668
generator.append_tokens(input_ids[:, i : i + 1].tolist())
639-
all_logits.append(torch.from_numpy(generator.get_logits()).to(self.device))
669+
all_logits.append(torch.from_numpy(generator.get_logits()).to(self._device))
640670

641671
# No need to pad to [batch, seq_len, vocab]. The slicing in _loglikelihood_tokens computes
642672
# ctx_len = inplen + (logits.shape[0] - padding_len_inp), which adjusts for the shorter
@@ -671,7 +701,7 @@ def generate_until(self, requests, disable_tqdm: bool = False) -> list[str]:
671701
params = og.GeneratorParams(self.model)
672702
params.set_search_options(
673703
max_length=len(input_ids) + max_new_tokens,
674-
past_present_share_buffer=False,
704+
past_present_share_buffer=self._past_present_share_buffer,
675705
batch_size=1,
676706
)
677707
if gen_kwargs.get("temperature", 0.0) == 0.0:

olive/evaluator/olive_evaluator.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1900,6 +1900,9 @@ def __init__(self, tasks: list[str], **kwargs):
19001900
self.model_class = kwargs.get("model_class")
19011901
self.batch_size = kwargs.get("batch_size", 1)
19021902
self.max_length = kwargs.get("max_length")
1903+
# Preserve lm-eval bootstrap control from recipe configs. Some generation metrics disable
1904+
# bootstrap stderr resampling to avoid extra post-processing work on large test sets.
1905+
self.bootstrap_iters = kwargs.get("bootstrap_iters", 100000)
19031906
self.ep = kwargs.get("execution_provider")
19041907
self.ep_options = kwargs.get("provider_options")
19051908
self.device = kwargs.get("device")
@@ -1982,6 +1985,8 @@ def evaluate(
19821985
batch_size=self.batch_size,
19831986
device=device,
19841987
limit=self.limit,
1988+
# Forward the configured value instead of letting lm-eval silently use its default.
1989+
bootstrap_iters=self.bootstrap_iters,
19851990
)
19861991

19871992
for task_name in sorted(results["results"].keys()):

olive/model/handler/mixin/hf.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ def get_hf_generation_config(self, exclude_load_keys: Optional[list[str]] = None
5151
:param exclude_load_keys: list of keys to exclude from load_kwargs
5252
:return: generation config or None
5353
"""
54-
return get_generation_config(self.model_path, **self.get_load_kwargs(exclude_load_keys))
54+
# Generation config loading should not receive model-loading-only kwargs such as
55+
# dtype, device placement, or quantization settings.
56+
generation_config_exclude_keys = {"torch_dtype", "dtype", "device_map", "max_memory", "quantization_config"}
57+
if exclude_load_keys:
58+
generation_config_exclude_keys.update(exclude_load_keys)
59+
return get_generation_config(self.model_path, **self.get_load_kwargs(list(generation_config_exclude_keys)))
5560

5661
def get_hf_tokenizer(self) -> Union["PreTrainedTokenizer", "PreTrainedTokenizerFast"]:
5762
"""Get tokenizer for the model."""

test/hardware/test_accelerator.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,17 @@ def test_normalize_accelerators(
429429
},
430430
("npu", [ExecutionProvider.QNNExecutionProvider], 1e9),
431431
),
432+
(
433+
{
434+
"type": "LocalSystem",
435+
"config": {
436+
"accelerators": [
437+
{"device": "gpu", "execution_providers": [ExecutionProvider.WebGpuExecutionProvider]}
438+
]
439+
},
440+
},
441+
("gpu", [ExecutionProvider.WebGpuExecutionProvider]),
442+
),
432443
],
433444
)
434445
def test_normalize_accelerators_skip_ep_check(system_config, expected_acc):
@@ -440,6 +451,26 @@ def test_normalize_accelerators_skip_ep_check(system_config, expected_acc):
440451
assert normalized_accs.config.accelerators[0].memory == expected_acc[2]
441452

442453

454+
@patch("olive.systems.local.get_ort_available_providers")
455+
def test_normalize_accelerators_requires_runtime_webgpu_when_target_used(get_available_providers_mock):
456+
system_config = validate_config(
457+
{
458+
"type": "LocalSystem",
459+
"config": {
460+
"accelerators": [{"device": "gpu", "execution_providers": [ExecutionProvider.WebGpuExecutionProvider]}]
461+
},
462+
},
463+
SystemConfig,
464+
)
465+
get_available_providers_mock.return_value = [
466+
ExecutionProvider.CUDAExecutionProvider,
467+
ExecutionProvider.CPUExecutionProvider,
468+
]
469+
470+
with pytest.raises(ValueError, match="None of the execution providers"):
471+
AcceleratorNormalizer(system_config, skip_supported_eps_check=False).normalize()
472+
473+
443474
@pytest.mark.parametrize(
444475
("system_config", "available_providers", "exception", "error_message"),
445476
[

0 commit comments

Comments
 (0)