Skip to content

Commit 86d7575

Browse files
xadupreCopilotCopilot
authored
Add OnnxDiscrepancyCheck speedup metric with default timing updates (microsoft#2502)
## Describe your changes Added speedup measurement for `OnnxDiscrepancyCheck` and updated behavior based on review feedback: - Changed `timing_iterations` default from `10` to `5`. - If `timing_iterations` is set to `0`, speedup measurement is skipped. - Added unit tests to validate the new default and the skip behavior. - Fixed test mocks to properly configure `device` attribute for `compare_generation` tests. ## Checklist before requesting a review - [x] Add unit tests for this change. - [x] Make sure all tests can pass. - [ ] Update documents if necessary. - [x] Lint and apply fixes to your code by running `lintrunner -a` - [ ] Is this a user-facing change? If yes, give a description of this change to be included in the release notes. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 3b9d03d commit 86d7575

2 files changed

Lines changed: 159 additions & 5 deletions

File tree

olive/passes/onnx/discrepancy_check.py

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
# Licensed under the MIT License.
44
# --------------------------------------------------------------------------
55
import logging
6+
import time
67
from typing import Optional
78

89
from olive.data.config import DataConfig
910
from olive.hardware import AcceleratorSpec
11+
from olive.hardware.accelerator import Device
1012
from olive.model import ONNXModelHandler
1113
from olive.passes import Pass
1214
from olive.passes.pass_config import BasePassConfig, PassConfigParam
@@ -61,6 +63,7 @@ class OnnxDiscrepancyCheck(Pass):
6163
- Maximum absolute error (MaxAE)
6264
- Number of elements where the absolute difference exceeds 0.1
6365
- Number of elements where the absolute difference exceeds 0.01
66+
- Inference speedup of ONNX over PyTorch on the target device (or CPU fallback)
6467
- Longest common token sequence from the beginning between transformers
6568
generate and ONNX Runtime GenAI generate (when enabled)
6669
@@ -107,6 +110,19 @@ def _default_config(cls, accelerator_spec: AcceleratorSpec) -> dict[str, PassCon
107110
"computes the longest common token sequence from the beginning of their outputs."
108111
),
109112
),
113+
"warmup_iterations": PassConfigParam(
114+
type_=int,
115+
default_value=3,
116+
description="Number of warmup iterations before timing inference for speedup measurement.",
117+
),
118+
"timing_iterations": PassConfigParam(
119+
type_=int,
120+
default_value=5,
121+
description=(
122+
"Number of timed iterations to measure inference speedup (ONNX vs PyTorch). "
123+
"Set to 0 to disable speedup measurement."
124+
),
125+
),
110126
"generate_prompt": PassConfigParam(
111127
type_=str,
112128
default_value="The capital of France is",
@@ -177,8 +193,28 @@ def _run_for_config(
177193
ref_model = AutoModelForCausalLM.from_pretrained(config.reference_model_path)
178194
ref_model.eval()
179195

180-
# Prepare ONNX session
181-
session = model.prepare_session()
196+
# Prepare ONNX session on the target device (fallback to CPU)
197+
device = self.accelerator_spec.accelerator_type if self.accelerator_spec else None
198+
execution_provider = self.accelerator_spec.execution_provider if self.accelerator_spec else None
199+
if device is None:
200+
device = Device.CPU
201+
elif not isinstance(device, Device):
202+
try:
203+
device = Device(str(device).lower())
204+
except ValueError:
205+
logger.warning("Unknown accelerator_type=%s; falling back to CPU.", device)
206+
device = Device.CPU
207+
208+
# Determine the torch device matching the accelerator spec
209+
torch_device = torch.device("cpu")
210+
if device == Device.GPU and torch.cuda.is_available():
211+
torch_device = torch.device("cuda")
212+
ref_model = ref_model.to(torch_device)
213+
214+
session = model.prepare_session(
215+
device=device,
216+
execution_providers=[execution_provider] if execution_provider else None,
217+
)
182218
io_config = model.io_config
183219

184220
# Run inference on both and compare
@@ -194,9 +230,9 @@ def _run_for_config(
194230

195231
# Run PyTorch inference
196232
if isinstance(input_data, dict):
197-
torch_inputs = {k: v.clone() for k, v in input_data.items()}
233+
torch_inputs = {k: v.clone().to(torch_device) for k, v in input_data.items()}
198234
else:
199-
torch_inputs = input_data
235+
torch_inputs = input_data.to(torch_device)
200236

201237
torch_output = ref_model(**torch_inputs)
202238
torch_logits = torch_output.logits.detach().cpu().numpy()
@@ -225,6 +261,23 @@ def _run_for_config(
225261
total_elements,
226262
)
227263

264+
# Measure inference speedup (ONNX vs PyTorch) on the target device
265+
if config.timing_iterations > 0:
266+
self._measure_speedup(
267+
ref_model,
268+
session,
269+
dataloader,
270+
io_config,
271+
torch_device,
272+
config.warmup_iterations,
273+
config.timing_iterations,
274+
)
275+
else:
276+
logger.info(
277+
"OnnxDiscrepancyCheck speedup measurement skipped because timing_iterations=%d.",
278+
config.timing_iterations,
279+
)
280+
228281
# Check thresholds
229282
failures = []
230283
if config.max_mae is not None and max_abs_error > config.max_mae:
@@ -254,6 +307,73 @@ def _run_for_config(
254307
# Return the model unchanged
255308
return model
256309

310+
def _measure_speedup(
311+
self, ref_model, session, dataloader, io_config, torch_device, warmup_iterations, timing_iterations
312+
):
313+
"""Measure inference speedup of ONNX over PyTorch on the target device."""
314+
if timing_iterations <= 0:
315+
logger.info(
316+
"OnnxDiscrepancyCheck speedup measurement skipped because timing_iterations=%d.",
317+
timing_iterations,
318+
)
319+
return None
320+
321+
import torch
322+
323+
from olive.common.utils import format_data
324+
325+
# Use the first batch for timing
326+
first_batch = next(iter(dataloader))
327+
input_data = first_batch[0] if isinstance(first_batch, (tuple, list)) else first_batch
328+
329+
if isinstance(input_data, dict):
330+
torch_inputs = {k: v.clone().to(torch_device) for k, v in input_data.items()}
331+
else:
332+
torch_inputs = input_data.to(torch_device)
333+
334+
onnx_input_feed = format_data(input_data, io_config)
335+
use_cuda_sync = torch_device.type == "cuda"
336+
337+
# Warmup PyTorch
338+
with torch.no_grad():
339+
for _ in range(warmup_iterations):
340+
ref_model(**torch_inputs)
341+
if use_cuda_sync:
342+
torch.cuda.synchronize()
343+
344+
# Time PyTorch
345+
with torch.no_grad():
346+
if use_cuda_sync:
347+
torch.cuda.synchronize()
348+
start = time.perf_counter()
349+
for _ in range(timing_iterations):
350+
ref_model(**torch_inputs)
351+
if use_cuda_sync:
352+
torch.cuda.synchronize()
353+
pytorch_time = (time.perf_counter() - start) / timing_iterations
354+
355+
# Warmup ONNX
356+
for _ in range(warmup_iterations):
357+
session.run(None, onnx_input_feed)
358+
359+
# Time ONNX
360+
start = time.perf_counter()
361+
for _ in range(timing_iterations):
362+
session.run(None, onnx_input_feed)
363+
onnx_time = (time.perf_counter() - start) / timing_iterations
364+
365+
speedup = pytorch_time / onnx_time if onnx_time > 0 else float("inf")
366+
367+
logger.info(
368+
"OnnxDiscrepancyCheck speedup: pytorch_avg=%.4fs, onnx_avg=%.4fs, speedup=%.2fx (device=%s)",
369+
pytorch_time,
370+
onnx_time,
371+
speedup,
372+
torch_device,
373+
)
374+
375+
return speedup
376+
257377
def compare_generation(self, config: type[BasePassConfig], ref_model) -> int:
258378
"""Run generation on both transformers and GenAI, return longest common token sequence length."""
259379
try:
@@ -266,6 +386,7 @@ def compare_generation(self, config: type[BasePassConfig], ref_model) -> int:
266386

267387
# Transformers generation
268388
input_ids = tokenizer(config.generate_prompt, return_tensors="pt").input_ids
389+
input_ids = input_ids.to(ref_model.device)
269390
import torch
270391

271392
with torch.no_grad():
@@ -274,7 +395,7 @@ def compare_generation(self, config: type[BasePassConfig], ref_model) -> int:
274395
max_new_tokens=config.generate_max_new_tokens,
275396
do_sample=False,
276397
)
277-
transformers_tokens = transformers_output[0].tolist()
398+
transformers_tokens = transformers_output[0].cpu().tolist()
278399

279400
# ONNX Runtime GenAI generation
280401
genai_model = og.Model(config.genai_model_path)

test/passes/onnx/test_discrepancy_check.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# Copyright (c) Microsoft Corporation. All rights reserved.
33
# Licensed under the MIT License.
44
# --------------------------------------------------------------------------
5+
# pylint: disable=protected-access
6+
57
import sys
68
from unittest.mock import MagicMock, patch
79

@@ -65,6 +67,7 @@ def test_compare_generation_returns_common_prefix_length(self):
6567

6668
# Transformers generates [1, 2, 3, 10, 11, 12, 13]
6769
mock_ref_model = MagicMock()
70+
mock_ref_model.device = torch.device("cpu")
6871
mock_ref_model.generate.return_value = torch.tensor([[1, 2, 3, 10, 11, 12, 13]])
6972

7073
# GenAI generates [1, 2, 3, 10, 11, 99, 99] (diverges at index 5)
@@ -122,6 +125,7 @@ def test_compare_generation_fully_matching(self):
122125
mock_tokenizer.return_value = MagicMock(input_ids=torch.tensor([[10, 20]]))
123126

124127
mock_ref_model = MagicMock()
128+
mock_ref_model.device = torch.device("cpu")
125129
mock_ref_model.generate.return_value = torch.tensor([[10, 20, 30, 40, 50]])
126130

127131
mock_og = MagicMock()
@@ -159,3 +163,32 @@ def get_next_tokens_side_effect():
159163
mock_generator.append_tokens.assert_called_once_with([[10, 20]])
160164
# All 5 tokens match
161165
assert result == 5
166+
167+
168+
class TestSpeedupSettings:
169+
def test_timing_iterations_default_is_5(self):
170+
from olive.passes.onnx.discrepancy_check import OnnxDiscrepancyCheck
171+
172+
pass_config = OnnxDiscrepancyCheck._default_config(None)
173+
assert pass_config["timing_iterations"].default_value == 5
174+
175+
def test_measure_speedup_skips_when_timing_iterations_is_zero(self):
176+
from olive.passes.onnx.discrepancy_check import OnnxDiscrepancyCheck
177+
178+
pass_instance = OnnxDiscrepancyCheck.__new__(OnnxDiscrepancyCheck)
179+
ref_model = MagicMock()
180+
session = MagicMock()
181+
182+
result = pass_instance._measure_speedup(
183+
ref_model=ref_model,
184+
session=session,
185+
dataloader=MagicMock(),
186+
io_config=MagicMock(),
187+
torch_device=MagicMock(),
188+
warmup_iterations=3,
189+
timing_iterations=0,
190+
)
191+
192+
assert result is None
193+
ref_model.assert_not_called()
194+
session.run.assert_not_called()

0 commit comments

Comments
 (0)