Skip to content

Commit 5ff3a59

Browse files
authored
Add chat-template hooks to LMEvalORTGenAIEvaluator (microsoft#2462)
## Describe your changes Implement `tokenizer_name` and `apply_chat_template` on `LMEvalORTGenAIEvaluator` so the backend supports `lm_eval.simple_evaluate(apply_chat_template=True)`. Without these, lm-eval raises `NotImplementedError` at task setup for any chat-formatted task. Parity with the HuggingFace backend in `lm_eval/models/huggingface.py`. The HF tokenizer is loaded lazily on the first `apply_chat_template` call, so model directories without HF tokenizer files still work for non-chat evaluation. Generation continues to go through `og.Tokenizer`. ## Checklist before requesting a review - [x] Add unit tests for this change. - [x] Make sure all tests can pass. - [x] Update documents if necessary. - [x] Lint and apply fixes to your code by running `lintrunner -a` - [x] Is this a user-facing change? Release note: *Enable `apply_chat_template=True` in lm-eval for ortgenai-backed evaluators.* ## (Optional) Issue link N/A
1 parent af26cc0 commit 5ff3a59

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

olive/evaluator/lmeval_ort.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,8 @@ def __init__(
549549
self.config.set_provider_option(ep, key, value)
550550
self.model = og.Model(self.config)
551551
self.tokenizer = og.Tokenizer(self.model)
552+
self._pretrained = str(pretrained)
553+
self._hf_tokenizer: AutoTokenizer | None = None
552554

553555
# consider adding auto batch sizes
554556
self.batch_size = int(batch_size)
@@ -572,6 +574,20 @@ def __init__(
572574
self.device = device
573575
self._returns_full_logits = self._detect_full_logits()
574576

577+
@property
578+
def tokenizer_name(self) -> str:
579+
return self._pretrained.replace("\\", "__").replace("/", "__")
580+
581+
def apply_chat_template(self, chat_history: list[dict], add_generation_prompt: bool = True) -> str:
582+
if self._hf_tokenizer is None:
583+
self._hf_tokenizer = AutoTokenizer.from_pretrained(self._pretrained)
584+
return self._hf_tokenizer.apply_chat_template(
585+
chat_history,
586+
tokenize=False,
587+
add_generation_prompt=add_generation_prompt,
588+
continue_final_message=not add_generation_prompt,
589+
)
590+
575591
def _detect_full_logits(self) -> bool:
576592
"""Check if the model returns logits for all input positions or only the last."""
577593
try:

test/evaluator/test_olive_evaluator.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,3 +510,51 @@ def test_lm_evaluator_dispatches_to_requested_backend(
510510
evaluator.evaluate(model, metrics=[], device=Device.CPU, execution_providers=["CPUExecutionProvider"])
511511

512512
get_model_mock.assert_called_once_with(model_class)
513+
514+
515+
@pytest.mark.skipif(
516+
importlib.util.find_spec("lm_eval") is None,
517+
reason="lm_eval not installed",
518+
)
519+
class TestLMEvalORTGenAIChatTemplate:
520+
def _bare_instance(self, pretrained: str):
521+
# pylint: disable=protected-access
522+
from olive.evaluator.lmeval_ort import LMEvalORTGenAIEvaluator
523+
524+
instance = object.__new__(LMEvalORTGenAIEvaluator)
525+
instance._pretrained = pretrained
526+
instance._hf_tokenizer = None
527+
return instance
528+
529+
@pytest.mark.parametrize(
530+
("pretrained", "expected"),
531+
[
532+
("/models/lfm2-350m", "__models__lfm2-350m"),
533+
("relative/path/model", "relative__path__model"),
534+
("C:\\models\\lfm2-350m", "C:__models__lfm2-350m"),
535+
],
536+
)
537+
def test_tokenizer_name_normalizes_separators(self, pretrained, expected):
538+
assert self._bare_instance(pretrained).tokenizer_name == expected
539+
540+
@patch("olive.evaluator.lmeval_ort.AutoTokenizer")
541+
def test_apply_chat_template_lazy_loads_hf_tokenizer(self, auto_tokenizer_mock):
542+
chat_history = [{"role": "user", "content": "hello"}]
543+
mock_tokenizer = MagicMock()
544+
mock_tokenizer.apply_chat_template.return_value = "rendered prompt"
545+
auto_tokenizer_mock.from_pretrained.return_value = mock_tokenizer
546+
547+
instance = self._bare_instance("/models/lfm2")
548+
549+
auto_tokenizer_mock.from_pretrained.assert_not_called()
550+
assert instance.apply_chat_template(chat_history) == "rendered prompt"
551+
auto_tokenizer_mock.from_pretrained.assert_called_once_with("/models/lfm2")
552+
553+
instance.apply_chat_template(chat_history, add_generation_prompt=False)
554+
auto_tokenizer_mock.from_pretrained.assert_called_once()
555+
mock_tokenizer.apply_chat_template.assert_called_with(
556+
chat_history,
557+
tokenize=False,
558+
add_generation_prompt=False,
559+
continue_final_message=True,
560+
)

0 commit comments

Comments
 (0)