33# Licensed under the MIT License.
44# --------------------------------------------------------------------------
55import logging
6+ import time
67from typing import Optional
78
89from olive .data .config import DataConfig
910from olive .hardware import AcceleratorSpec
11+ from olive .hardware .accelerator import Device
1012from olive .model import ONNXModelHandler
1113from olive .passes import Pass
1214from 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 )
0 commit comments