Initialisation du repository de Beta
This commit is contained in:
commit
14985f6dbb
9469 changed files with 1903273 additions and 0 deletions
|
|
@ -0,0 +1,12 @@
|
|||
# -------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
# --------------------------------------------------------------------------
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
|
||||
transformers_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
if transformers_dir not in sys.path:
|
||||
sys.path.append(transformers_dir)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,610 @@
|
|||
# -------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import datetime
|
||||
import gc
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import psutil
|
||||
import torch
|
||||
import whisper
|
||||
from benchmark_helper import measure_memory, setup_logger
|
||||
from onnxruntime_extensions import get_library_path
|
||||
from optimum.onnxruntime import ORTModelForSpeechSeq2Seq
|
||||
from torch.profiler import ProfilerActivity, profile, record_function
|
||||
from tqdm import trange
|
||||
from transformers import AutoModelForSpeechSeq2Seq, WhisperConfig, WhisperProcessor
|
||||
|
||||
import onnxruntime as ort
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_inputs(args: argparse.Namespace):
|
||||
if args.benchmark_type not in {"hf-pt-eager", "hf-pt-compile", "hf-ort", "ort"}:
|
||||
raise Exception("Unable to auto-detect inputs for provided model")
|
||||
|
||||
def load_via_ffmpeg():
|
||||
audio = whisper.load_audio(args.audio_path)
|
||||
audio = whisper.pad_or_trim(audio)
|
||||
return audio
|
||||
|
||||
def load_via_numpy():
|
||||
with open(args.audio_path, "rb") as f:
|
||||
audio = np.asarray(list(f.read()), dtype=np.uint8)
|
||||
audio = np.array([audio])
|
||||
return audio
|
||||
|
||||
inputs = {
|
||||
"max_length": args.max_length,
|
||||
"min_length": args.min_length,
|
||||
"num_beams": args.num_beams,
|
||||
"num_return_sequences": args.num_return_sequences,
|
||||
"length_penalty": args.length_penalty,
|
||||
"repetition_penalty": args.repetition_penalty,
|
||||
}
|
||||
if args.benchmark_type == "ort":
|
||||
# convert_to_onnx export or ONNX E2E solution created by Olive
|
||||
for k, v in inputs.items():
|
||||
inputs[k] = np.array([v], dtype=np.float32 if "penalty" in k else np.int32)
|
||||
if args.has_decoder_input_ids:
|
||||
inputs["decoder_input_ids"] = np.array([args.decoder_input_ids], dtype=np.int32)
|
||||
if args.has_logits_processor:
|
||||
inputs["logits_processor"] = np.array([args.logits_processor], dtype=np.int32)
|
||||
if args.has_temperature:
|
||||
inputs["temperature"] = np.array([args.temperature], dtype=np.float32)
|
||||
|
||||
# Measure time taken to load audio file
|
||||
logger.info(f"Load audio: {args.audio_path}")
|
||||
load_audio_fn = lambda onnx_e2e: load_via_numpy() if onnx_e2e else load_via_ffmpeg() # noqa: E731
|
||||
time_fn(args, load_audio_fn, args.has_audio_stream)
|
||||
audio_data = load_audio_fn(args.has_audio_stream)
|
||||
|
||||
if args.has_audio_stream:
|
||||
# ONNX E2E solution created by Olive
|
||||
inputs["audio_stream"] = audio_data
|
||||
return inputs
|
||||
|
||||
# Measure time taken to get input features
|
||||
logger.info("Feature extraction: ")
|
||||
return_type = "np" if args.benchmark_type == "ort" else "pt"
|
||||
processor_fn = lambda audio: args.processor.feature_extractor( # noqa: E731
|
||||
[audio], return_tensors=return_type, sampling_rate=args.sampling_rate
|
||||
).input_features
|
||||
time_fn(args, processor_fn, audio_data)
|
||||
input_features = processor_fn(audio_data)
|
||||
|
||||
if args.benchmark_type == "ort":
|
||||
# convert_to_onnx export
|
||||
inputs["input_features"] = input_features
|
||||
return inputs
|
||||
|
||||
inputs["inputs"] = input_features.to(
|
||||
dtype=torch.float16 if args.use_fp16 else torch.float32, device=args.target_device
|
||||
)
|
||||
inputs["no_repeat_ngram_size"] = args.no_repeat_ngram_size
|
||||
inputs["early_stopping"] = True
|
||||
inputs["use_cache"] = True
|
||||
|
||||
if args.decoder_input_ids:
|
||||
inputs["forced_decoder_ids"] = args.decoder_input_ids
|
||||
|
||||
return inputs
|
||||
|
||||
|
||||
def get_model(args: argparse.Namespace):
|
||||
model, sess_options = None, None
|
||||
start_time, end_time = None, None
|
||||
|
||||
# There are multiple sources that the model could come from:
|
||||
# 1) Benchmark Whisper from Hugging Face
|
||||
# 2) Benchmark Whisper ONNX model from Optimum export (without pre/post processing)
|
||||
# 3) Benchmark Whisper ONNX E2E model from Olive (with pre/post processing)
|
||||
|
||||
if args.benchmark_type in {"hf-pt-eager", "hf-pt-compile"}:
|
||||
source = args.hf_pt_model_path if args.hf_pt_model_path else args.model_name
|
||||
start_time = time.time()
|
||||
model = AutoModelForSpeechSeq2Seq.from_pretrained(
|
||||
source,
|
||||
torch_dtype=torch.float16 if args.use_fp16 else torch.float32,
|
||||
use_cache=True,
|
||||
).to(args.target_device)
|
||||
end_time = time.time()
|
||||
|
||||
if args.benchmark_type == "hf-pt-compile":
|
||||
model = torch.compile(model)
|
||||
|
||||
elif args.benchmark_type in {"hf-ort", "ort"}:
|
||||
sess_options = ort.SessionOptions()
|
||||
sess_options.enable_profiling = args.profile
|
||||
sess_options.register_custom_ops_library(get_library_path())
|
||||
if args.verbose:
|
||||
sess_options.log_verbosity_level = 1
|
||||
sess_options.log_severity_level = 1
|
||||
if args.tune:
|
||||
ort.set_default_logger_severity(0)
|
||||
ort.set_default_logger_verbosity(0)
|
||||
|
||||
else:
|
||||
raise Exception(f"Cannot recognize {args.benchmark_type}")
|
||||
|
||||
if args.benchmark_type == "hf-ort":
|
||||
# Optimum export
|
||||
provider = args.execution_provider[0] if type(args.execution_provider) is tuple else args.execution_provider
|
||||
provider_options = args.execution_provider[1] if type(args.execution_provider) is tuple else None
|
||||
|
||||
start_time = time.time()
|
||||
model = ORTModelForSpeechSeq2Seq.from_pretrained(
|
||||
args.hf_ort_dir_path,
|
||||
provider=provider,
|
||||
provider_options=provider_options,
|
||||
session_options=sess_options,
|
||||
use_io_binding=True, # Avoid memory copy overhead
|
||||
)
|
||||
end_time = time.time()
|
||||
|
||||
if args.benchmark_type == "ort":
|
||||
# convert_to_onnx.py export
|
||||
logger.info(f"Loading model from {args.ort_model_path}")
|
||||
start_time = time.time()
|
||||
model = ort.InferenceSession(
|
||||
args.ort_model_path,
|
||||
sess_options,
|
||||
providers=[args.execution_provider],
|
||||
)
|
||||
end_time = time.time()
|
||||
|
||||
logger.info(f"Loaded model in {end_time - start_time} s")
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def time_fn(args, fn, inputs):
|
||||
warmup_inputs = inputs[0] if type(inputs) is tuple else inputs
|
||||
benchmark_inputs = inputs[1] if type(inputs) is tuple else inputs
|
||||
torch_device = torch.device(args.target_device)
|
||||
|
||||
# Warm up
|
||||
warmup_range = (
|
||||
range(args.warmup_runs)
|
||||
if args.benchmark_type == "ort"
|
||||
else trange(args.warmup_runs, file=sys.stdout, desc="Warm up")
|
||||
)
|
||||
|
||||
if args.verbose:
|
||||
outputs = fn(warmup_inputs)
|
||||
logger.info(outputs)
|
||||
|
||||
for _ in warmup_range:
|
||||
fn(warmup_inputs)
|
||||
|
||||
# Benchmark
|
||||
if args.device != "cpu":
|
||||
torch.cuda.synchronize(torch_device)
|
||||
start_time = time.time()
|
||||
|
||||
bench_range = (
|
||||
range(args.num_runs)
|
||||
if args.benchmark_type == "ort"
|
||||
else trange(args.num_runs, file=sys.stdout, desc="Benchmark")
|
||||
)
|
||||
for _ in bench_range:
|
||||
fn(benchmark_inputs)
|
||||
|
||||
if args.device != "cpu":
|
||||
torch.cuda.synchronize(torch_device)
|
||||
end_time = time.time()
|
||||
|
||||
# Newline print after trange in order to print metrics on new lines without progress bar on same line
|
||||
if args.benchmark_type != "ort":
|
||||
logger.info("")
|
||||
|
||||
batch_size = 1
|
||||
latency = (end_time - start_time) / args.num_runs
|
||||
throughput = batch_size / latency
|
||||
|
||||
logger.info(f"Latency: {latency} s")
|
||||
logger.info(f"Throughput: {throughput} qps")
|
||||
return
|
||||
|
||||
|
||||
def profile_fn(args, fn, inputs, inputs_type):
|
||||
# Filename prefix format:
|
||||
# "<benchmark-type>-<precision>-<device>_<inference-step>_<inputs-type>_<current-time>"
|
||||
prefix = f"{args.benchmark_type.lower()}-{args.precision}-{args.device}_{fn.__name__.replace('_', '-')}_{inputs_type}_{datetime.datetime.now():%Y-%m-%d_%H:%M:%S}"
|
||||
filename = None
|
||||
|
||||
if args.benchmark_type in {"hf-pt-eager", "hf-pt-compile"}:
|
||||
# Profile PyTorch kernels
|
||||
with profile( # noqa: SIM117
|
||||
activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA], record_shapes=True, profile_memory=True
|
||||
) as prof:
|
||||
with record_function("model_inference"):
|
||||
fn(inputs)
|
||||
prof_data = prof.key_averages(group_by_stack_n=5).table(sort_by=args.pt_filter_by, row_limit=args.pt_num_rows)
|
||||
|
||||
filename = os.path.join(args.log_folder, f"{prefix}.log")
|
||||
with open(filename, "w") as f:
|
||||
f.write(prof_data)
|
||||
|
||||
else:
|
||||
# Profile ORT kernels
|
||||
fn(inputs)
|
||||
|
||||
# Set new log name for ORT profile log generated
|
||||
filename = f"{prefix}.json"
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def measure_fn(args, fn, inputs):
|
||||
# Measure CPU usage
|
||||
pid = os.getpid()
|
||||
process = psutil.Process(pid)
|
||||
process.cpu_percent(interval=0.1)
|
||||
|
||||
fn(inputs)
|
||||
logger.info(f"CPU usage: {process.cpu_percent(interval=None)}%")
|
||||
|
||||
# Measure memory usage
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
measure_memory(is_gpu=(args.device != "cpu"), func=lambda: fn(inputs), monitor_type=args.monitor_type)
|
||||
|
||||
# Flush output so memory usage is printed
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def run_hf_inference(args, inputs, model):
|
||||
# Inference steps to measure
|
||||
def get_pred_ids(inputs):
|
||||
# Inference pass with predicted token ids generation
|
||||
predicted_ids = model.generate(**inputs)
|
||||
return predicted_ids
|
||||
|
||||
def gen_and_dec(inputs):
|
||||
# Inference pass with generation and decoding
|
||||
predicted_ids = get_pred_ids(inputs)
|
||||
transcription = []
|
||||
for _ in range(args.num_return_sequences):
|
||||
transcription.append(args.processor.batch_decode(predicted_ids, skip_special_tokens=True)[0])
|
||||
return predicted_ids, transcription
|
||||
|
||||
# Examples of other inference steps that can be measured:
|
||||
# To use, uncomment the function and assign it to `generate_fn`
|
||||
|
||||
# def get_logits(inputs):
|
||||
# # Inference pass without decoding
|
||||
# outputs = model(**inputs)
|
||||
# return outputs
|
||||
|
||||
generate_fn = gen_and_dec
|
||||
|
||||
if args.benchmark_type == "hf-pt-compile":
|
||||
# Run forward pass once with each set of inputs to process through Dynamo
|
||||
generate_fn(inputs)
|
||||
|
||||
if args.profile:
|
||||
new_logname = profile_fn(args, generate_fn, inputs, "gen-and-dec")
|
||||
if args.benchmark_type == "hf-ort":
|
||||
# Rename log files per model component and turn profiling off to stop appending to log
|
||||
new_prefix = new_logname[: -len(".json")]
|
||||
|
||||
old_logname = model.encoder.session.end_profiling()
|
||||
new_logname = new_prefix + "-encoder.json"
|
||||
if os.path.isfile(old_logname):
|
||||
logger.warning(f"Renaming {old_logname} to {new_logname}")
|
||||
os.rename(old_logname, os.path.join(args.log_folder, new_logname))
|
||||
|
||||
old_logname = model.decoder.session.end_profiling()
|
||||
new_logname = new_prefix + "-decoder.json"
|
||||
if os.path.isfile(old_logname):
|
||||
logger.warning(f"Renaming {old_logname} to {new_logname}")
|
||||
os.rename(old_logname, os.path.join(args.log_folder, new_logname))
|
||||
|
||||
old_logname = model.decoder_with_past.session.end_profiling()
|
||||
new_logname = new_prefix + "-decoder-with-past.json"
|
||||
if os.path.isfile(old_logname):
|
||||
logger.warning(f"Renaming {old_logname} to {new_logname}")
|
||||
os.rename(old_logname, os.path.join(args.log_folder, new_logname))
|
||||
|
||||
return
|
||||
|
||||
# PyTorch evaluations
|
||||
logger.info("\nEvaluating PyTorch...")
|
||||
time_fn(args, generate_fn, inputs)
|
||||
predicted_ids, transcription = generate_fn(inputs)
|
||||
logger.info(f"Generated token length: {len(predicted_ids[0])} tokens")
|
||||
logger.info(f"Transcription: {transcription[0]}")
|
||||
measure_fn(args, generate_fn, inputs)
|
||||
|
||||
|
||||
def run_ort_inference(args, inputs, model):
|
||||
def prepare_ort_inputs(inputs, warmup=False):
|
||||
# Check that all model inputs will be provided
|
||||
model_inputs = {model_input.name for model_input in model.get_inputs()}
|
||||
user_inputs = set(inputs.keys())
|
||||
missing_inputs = model_inputs - user_inputs
|
||||
if len(missing_inputs):
|
||||
logger.error(f"The following model inputs are missing: {missing_inputs}")
|
||||
raise Exception("There are missing inputs to the model. Please add them and try again.")
|
||||
|
||||
if warmup and args.tune:
|
||||
inputs["min_length"] = inputs["max_length"]
|
||||
|
||||
# Remove unnecessary inputs from model inputs
|
||||
unnecessary_inputs = user_inputs - model_inputs
|
||||
if len(unnecessary_inputs):
|
||||
for unnecessary_input in unnecessary_inputs:
|
||||
logger.info(f"Removing unnecessary input '{unnecessary_input}' from user provided inputs")
|
||||
del inputs[unnecessary_input]
|
||||
|
||||
# Add IO bindings for non-CPU execution providers
|
||||
if args.device != "cpu":
|
||||
io_binding = model.io_binding()
|
||||
for k, v in inputs.items():
|
||||
io_binding.bind_cpu_input(k, v)
|
||||
for output in model.get_outputs():
|
||||
io_binding.bind_output(output.name, device_type=args.device, device_id=args.device_id)
|
||||
return io_binding
|
||||
|
||||
return inputs
|
||||
|
||||
def with_io_binding(io_binding):
|
||||
# Inference pass with IO binding
|
||||
model.run_with_iobinding(io_binding)
|
||||
return io_binding
|
||||
|
||||
def without_io_binding(inputs):
|
||||
# Inference pass without IO binding
|
||||
outputs = model.run(None, inputs)
|
||||
return outputs
|
||||
|
||||
def handle_output(output):
|
||||
if args.eos_token_id in output:
|
||||
first_end = np.where(output == args.eos_token_id)[0][0]
|
||||
return output[: first_end + 1]
|
||||
|
||||
return output
|
||||
|
||||
generate_fn = with_io_binding if args.device != "cpu" else without_io_binding
|
||||
ort_inputs = prepare_ort_inputs(inputs)
|
||||
|
||||
if args.profile:
|
||||
new_logname = profile_fn(args, generate_fn, ort_inputs, "e2e")
|
||||
|
||||
# Turn profiling off to stop appending to log file
|
||||
old_logname = model.end_profiling()
|
||||
logger.warning(f"Renaming {old_logname} to {new_logname}")
|
||||
os.rename(old_logname, os.path.join(args.log_folder, new_logname))
|
||||
|
||||
return
|
||||
|
||||
# ORT evaluation
|
||||
logger.info("\nEvaluating ONNX Runtime...")
|
||||
ort_evaluate_inputs = ort_inputs
|
||||
if args.tune:
|
||||
ort_warmup_inputs = prepare_ort_inputs(inputs, warmup=True)
|
||||
ort_evaluate_inputs = (ort_warmup_inputs, ort_inputs)
|
||||
|
||||
time_fn(args, generate_fn, ort_evaluate_inputs)
|
||||
ort_outputs = generate_fn(ort_inputs)
|
||||
if args.device != "cpu":
|
||||
ort_outputs = ort_outputs.copy_outputs_to_cpu()
|
||||
ort_outputs = ort_outputs[0]
|
||||
|
||||
if args.has_audio_stream:
|
||||
# ONNX E2E model from Olive produces transcribed output
|
||||
logger.info(f"Transcription: {ort_outputs[0][0]}")
|
||||
else:
|
||||
# convert_to_onnx model produces generated ids
|
||||
actual_output = handle_output(ort_outputs[0][0])
|
||||
logger.info(f"Generated token length: {len(actual_output)} tokens")
|
||||
transcription = args.processor.batch_decode(ort_outputs[0], skip_special_tokens=True)[0]
|
||||
# print to stdout as the output for comparison
|
||||
print(f"{transcription}")
|
||||
|
||||
measure_fn(args, generate_fn, ort_inputs)
|
||||
|
||||
|
||||
def run_inference(args, inputs, model):
|
||||
if args.benchmark_type in {"hf-pt-eager", "hf-pt-compile", "hf-ort"}:
|
||||
run_hf_inference(args, inputs, model)
|
||||
elif args.benchmark_type == "ort":
|
||||
run_ort_inference(args, inputs, model)
|
||||
else:
|
||||
raise Exception(f"Cannot recognize {args.benchmark_type}")
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
"-bt",
|
||||
"--benchmark-type",
|
||||
type=str,
|
||||
required=True,
|
||||
choices=["hf-pt-eager", "hf-pt-compile", "hf-ort", "ort"],
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
"--model-name",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Hugging Face name of model (e.g. 'openai/whisper-large-v2')",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--precision",
|
||||
type=str,
|
||||
required=True,
|
||||
default="fp32",
|
||||
choices=["int8", "fp16", "fp32"],
|
||||
help="Precision for model. For ONNX models, the model's precision should be set before running this script.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--hf-pt-model-path",
|
||||
type=str,
|
||||
default="",
|
||||
help="Path to directory containing all PyTorch files (e.g. tokenizer, PyTorch model)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hf-ort-dir-path",
|
||||
type=str,
|
||||
default="",
|
||||
help="Path to directory containing all ONNX files (e.g. tokenizer, encoder, decoder, decoder_with_past)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ort-model-path",
|
||||
type=str,
|
||||
default="",
|
||||
help="Path to ONNX model",
|
||||
)
|
||||
|
||||
# Args for running and evaluating the model
|
||||
parser.add_argument("-a", "--audio-path", type=str, required=True, help="Path to audio file for E2E evaluation")
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--device",
|
||||
type=str,
|
||||
default="cuda" if torch.cuda.is_available() else "cpu",
|
||||
choices=["cpu", "cuda", "rocm"],
|
||||
)
|
||||
parser.add_argument("-id", "--device-id", type=int, default=0)
|
||||
parser.add_argument("-w", "--warmup-runs", type=int, default=5)
|
||||
parser.add_argument("-n", "--num-runs", type=int, default=10)
|
||||
parser.add_argument("--seed", type=int, default=2)
|
||||
|
||||
# Optional args:
|
||||
parser.add_argument("--sampling-rate", type=int, default=16000, help="Sampling rate for audio (in Hz)")
|
||||
|
||||
# Args for decoding logic
|
||||
# Required args:
|
||||
parser.add_argument("--max-length", type=int, default=448)
|
||||
parser.add_argument("--min-length", type=int, default=0)
|
||||
parser.add_argument("--num-beams", type=int, default=1)
|
||||
parser.add_argument("--num-return-sequences", type=int, default=1)
|
||||
parser.add_argument("--length-penalty", type=float, default=1.0)
|
||||
parser.add_argument("--repetition-penalty", type=float, default=1.0)
|
||||
parser.add_argument("--no-repeat-ngram-size", type=int, default=3)
|
||||
|
||||
# Optional args for E2E solution:
|
||||
parser.add_argument(
|
||||
"--decoder-input-ids",
|
||||
type=str,
|
||||
default="[]",
|
||||
help="The forced decoder ids for generation. Format is [start token, timestamp token, language token, task token]. Default is [start token]. See `decoder_input_ids` in https://github.com/microsoft/Olive/tree/main/examples/whisper for details.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--logits-processor",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Whether to use timestamps logits processor or not (0 for false, 1 for true).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--temperature",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Temperature value for generation.",
|
||||
)
|
||||
|
||||
# Args for accessing detailed info
|
||||
parser.add_argument("--profile", default=False, action="store_true")
|
||||
parser.add_argument(
|
||||
"--pt-filter-by", type=str, default="self_cpu_time_total", help="What to filter PyTorch profiler by"
|
||||
)
|
||||
parser.add_argument("--pt-num-rows", type=int, default=1000, help="Number of rows for PyTorch profiler to display")
|
||||
parser.add_argument("--verbose", default=False, action="store_true")
|
||||
parser.add_argument("--log-folder", type=str, default=os.path.join("."), help="Folder to cache log files")
|
||||
parser.add_argument(
|
||||
"--tune",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Only used by ROCm EP, enable TunableOp tuning to select fastest kernel",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Set seed properties
|
||||
np.random.seed(args.seed)
|
||||
torch.manual_seed(args.seed)
|
||||
|
||||
args.monitor_type = args.device
|
||||
# Set runtime properties
|
||||
if "ort" in args.benchmark_type:
|
||||
args.execution_provider = f"{args.device.upper()}ExecutionProvider"
|
||||
if args.execution_provider == "CUDAExecutionProvider":
|
||||
args.execution_provider = (args.execution_provider, {"device_id": args.device_id})
|
||||
elif args.execution_provider == "ROCMExecutionProvider":
|
||||
args.execution_provider = (
|
||||
args.execution_provider,
|
||||
{
|
||||
"device_id": args.device_id,
|
||||
"tunable_op_enable": 1,
|
||||
"tunable_op_tuning_enable": 1 if args.tune else 0,
|
||||
},
|
||||
)
|
||||
args.device = "cuda"
|
||||
|
||||
# Check that model paths have been specified for any benchmarking with ORT
|
||||
if args.benchmark_type == "hf-ort":
|
||||
assert args.hf_ort_dir_path, "Please specify a path to `--hf-ort-dir-path`"
|
||||
if args.benchmark_type == "ort":
|
||||
assert args.ort_model_path, "Please specify a path to `--ort-model-path`"
|
||||
|
||||
# Convert decoder_input_ids string to list of ids
|
||||
# (e.g. "[1, 50257]" for Hugging Face or "[50257]" for ORT)
|
||||
args.decoder_input_ids = ast.literal_eval(args.decoder_input_ids)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
setup_logger(args.verbose)
|
||||
logger.info(args.__dict__)
|
||||
torch.backends.cudnn.benchmark = True
|
||||
|
||||
config = WhisperConfig.from_pretrained(args.model_name)
|
||||
processor = WhisperProcessor.from_pretrained(args.model_name)
|
||||
target_device = f"cuda:{args.device_id}" if args.device != "cpu" else args.device
|
||||
use_fp16 = args.precision == "fp16"
|
||||
|
||||
setattr(args, "processor", processor) # noqa: B010
|
||||
setattr(args, "target_device", target_device) # noqa: B010
|
||||
setattr(args, "use_fp16", use_fp16) # noqa: B010
|
||||
setattr(args, "has_audio_stream", False) # noqa: B010
|
||||
setattr(args, "eos_token_id", config.eos_token_id) # noqa: B010
|
||||
|
||||
logger.info(f"Forced decoder prompt ids: {args.decoder_input_ids}")
|
||||
|
||||
# Measure cost to transcribe audio
|
||||
model = get_model(args)
|
||||
if args.benchmark_type == "ort":
|
||||
# Check for optional inputs that could have been added during export
|
||||
ort_model_inputs = {model_input.name for model_input in model.get_inputs()}
|
||||
args.has_audio_stream = "audio_stream" in ort_model_inputs
|
||||
setattr(args, "has_decoder_input_ids", "decoder_input_ids" in ort_model_inputs) # noqa: B010
|
||||
setattr(args, "has_logits_processor", "logits_processor" in ort_model_inputs) # noqa: B010
|
||||
setattr(args, "has_temperature", "temperature" in ort_model_inputs) # noqa: B010
|
||||
|
||||
if args.decoder_input_ids == []:
|
||||
args.decoder_input_ids = [config.decoder_start_token_id]
|
||||
|
||||
inputs = get_inputs(args)
|
||||
run_inference(args, inputs, model)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,526 @@
|
|||
# -------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import librosa
|
||||
import torch
|
||||
from benchmark_helper import setup_logger
|
||||
from metrics import BenchmarkRecord
|
||||
from transformers import WhisperConfig, WhisperProcessor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--audio-path",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to folder of audio files for E2E evaluation",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--language",
|
||||
default=None,
|
||||
help="Language of audio file",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--task",
|
||||
default=None,
|
||||
choices=["transcribe", "translate"],
|
||||
help="Task to complete",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
"--warmup-runs",
|
||||
type=int,
|
||||
default=5,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--num-runs",
|
||||
type=int,
|
||||
default=10,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--hf-pt-eager",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Benchmark in PyTorch without `torch.compile`",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--hf-pt-compile",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Benchmark in PyTorch with `torch.compile`",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--hf-ort-dir-path",
|
||||
type=str,
|
||||
help="Path to folder containing ONNX models for Optimum + ORT benchmarking",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--ort-model-path",
|
||||
type=str,
|
||||
help="Path to ONNX model for ORT benchmarking",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--model-name",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Model name in Hugging Face (e.g. openai/whisper-large-v2)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--precision",
|
||||
type=str,
|
||||
required=True,
|
||||
choices=["int8", "fp16", "fp32"],
|
||||
help="Precision to run model",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--device",
|
||||
type=str,
|
||||
required=True,
|
||||
choices=["cpu", "cuda", "rocm"],
|
||||
help="Device to benchmark models",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--device-id",
|
||||
type=int,
|
||||
default=0,
|
||||
help="GPU device ID",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--verbose",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Print detailed logs",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Number of mins to attempt the benchmark before moving on",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--log-folder",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to folder to save logs and results",
|
||||
)
|
||||
|
||||
parser.add_argument("--tune", default=False, action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
setattr(args, "model_size", args.model_name.split("/")[-1].replace(".", "-")) # noqa: B010
|
||||
log_folder_name = f"./{args.model_size}-{args.precision}"
|
||||
if not args.log_folder:
|
||||
args.log_folder = log_folder_name
|
||||
os.makedirs(args.log_folder, exist_ok=True)
|
||||
|
||||
# Convert timeout value to secs
|
||||
args.timeout *= 60
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def process_log_file(device_id, log_file, base_results):
|
||||
entries = []
|
||||
|
||||
# Detect steps in speech pipeline
|
||||
step = None
|
||||
load_audio_pattern = "Load audio: "
|
||||
feat_ext_pattern = "Feature extraction: "
|
||||
pytorch_pattern = "Evaluating PyTorch..."
|
||||
onnxruntime_pattern = "Evaluating ONNX Runtime..."
|
||||
|
||||
load_audio_latency_s, load_audio_throughput_s = None, None
|
||||
feat_ext_latency_s, feat_ext_throughput_s = None, None
|
||||
token_length, latency_s, per_token_latency_s, per_token_latency_ms = None, None, None, None
|
||||
throughput, memory = None, None
|
||||
|
||||
# Detect metrics
|
||||
latency_pattern = "Latency: "
|
||||
throughput_pattern = "Throughput: "
|
||||
token_length_pattern = "Generated token length: "
|
||||
memory_pattern = "peak="
|
||||
|
||||
with open(log_file) as f:
|
||||
for input_line in f:
|
||||
line = input_line.replace("\n", "")
|
||||
|
||||
# Get step in speech recognition pipeline
|
||||
if load_audio_pattern in line:
|
||||
step = "load-audio"
|
||||
elif feat_ext_pattern in line:
|
||||
step = "feature-extraction"
|
||||
elif pytorch_pattern in line or onnxruntime_pattern in line:
|
||||
step = "process"
|
||||
|
||||
# Check metrics
|
||||
if latency_pattern in line:
|
||||
latency_s = float(line[len(latency_pattern) : line.rfind(" ")])
|
||||
elif throughput_pattern in line:
|
||||
throughput = float(line[len(throughput_pattern) : line.rfind(" ")])
|
||||
if step == "load-audio":
|
||||
load_audio_latency_s, load_audio_throughput_s = latency_s, throughput
|
||||
step = None
|
||||
if step == "feature-extraction":
|
||||
feat_ext_latency_s, feat_ext_throughput_s = latency_s, throughput
|
||||
step = None
|
||||
elif token_length_pattern in line:
|
||||
token_length = int(line[len(token_length_pattern) : line.rfind(" ")])
|
||||
per_token_latency_s = latency_s / token_length
|
||||
per_token_latency_ms = per_token_latency_s * 1000
|
||||
elif memory_pattern in line:
|
||||
if "CPU" in line:
|
||||
# Example format for log entry:
|
||||
# CPU memory usage: before=1000.0 MB, peak=2000.0 MB
|
||||
memory = float(line[line.rfind("=") + 1 : line.rfind(" MB")]) / 1000
|
||||
else:
|
||||
# Example format for log entry:
|
||||
# GPU memory usage: before=[{'device_id': 0, 'name': 'Tesla V100-PCIE-16GB', 'max_used_MB': 1638.875}, {'device_id': 1, 'name': 'Tesla V100-PCIE-16GB', 'max_used_MB': 236.875}, peak=[{'device_id': 0, 'name': 'Tesla V100-PCIE-16GB', 'max_used_MB': 1780.875}, {'device_id': 1, 'name': 'Tesla V100-PCIE-16GB', 'max_used_MB': 236.875}]
|
||||
peak = line[line.find(memory_pattern) + len(memory_pattern) :].replace("'", '"')
|
||||
usage = json.loads(peak)[device_id]["max_used_MB"]
|
||||
memory = float(usage) / 1000
|
||||
|
||||
# Calculate real-time factor (RTF):
|
||||
# RTF = total latency / audio duration
|
||||
total_latency = (
|
||||
(load_audio_latency_s if load_audio_latency_s else 0)
|
||||
+ (feat_ext_latency_s if feat_ext_latency_s else 0)
|
||||
+ (latency_s if latency_s else 0)
|
||||
)
|
||||
audio_duration = base_results[-1]
|
||||
rtf = (total_latency / audio_duration) if audio_duration else -1
|
||||
logger.info(f"Total latency: {total_latency} s")
|
||||
logger.info(f"Audio duration: {audio_duration} s")
|
||||
logger.info(f"Real-time factor: {rtf}")
|
||||
|
||||
# Append log entry to list of entries
|
||||
entry = base_results + [ # noqa: RUF005
|
||||
token_length,
|
||||
load_audio_latency_s,
|
||||
load_audio_throughput_s,
|
||||
feat_ext_latency_s if feat_ext_latency_s else -1,
|
||||
feat_ext_throughput_s if feat_ext_throughput_s else -1,
|
||||
latency_s,
|
||||
per_token_latency_ms,
|
||||
throughput,
|
||||
memory,
|
||||
rtf,
|
||||
]
|
||||
entries.append(entry)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def save_results(results, filename):
|
||||
import pandas as pd # noqa: PLC0415
|
||||
|
||||
df = pd.DataFrame(
|
||||
results,
|
||||
columns=[
|
||||
"Warmup Runs",
|
||||
"Measured Runs",
|
||||
"Model Name",
|
||||
"Engine",
|
||||
"Precision",
|
||||
"Device",
|
||||
"Audio File",
|
||||
"Duration (s)",
|
||||
"Token Length",
|
||||
"Load Audio Latency (s)",
|
||||
"Load Audio Throughput (qps)",
|
||||
"Feature Extractor Latency (s)",
|
||||
"Feature Extractor Throughput (qps)",
|
||||
"Latency (s)",
|
||||
"Per Token Latency (ms/token)",
|
||||
"Throughput (qps)",
|
||||
"Memory (GB)",
|
||||
"Real Time Factor (RTF)",
|
||||
],
|
||||
)
|
||||
|
||||
# Set column types
|
||||
df["Warmup Runs"] = df["Warmup Runs"].astype("int")
|
||||
df["Measured Runs"] = df["Measured Runs"].astype("int")
|
||||
df["Duration (s)"] = df["Duration (s)"].astype("float")
|
||||
df["Token Length"] = df["Token Length"].astype("int")
|
||||
df["Load Audio Latency (s)"] = df["Load Audio Latency (s)"].astype("float")
|
||||
df["Load Audio Throughput (qps)"] = df["Load Audio Throughput (qps)"].astype("float")
|
||||
df["Feature Extractor Latency (s)"] = df["Feature Extractor Latency (s)"].astype("float")
|
||||
df["Feature Extractor Throughput (qps)"] = df["Feature Extractor Throughput (qps)"].astype("float")
|
||||
df["Latency (s)"] = df["Latency (s)"].astype("float")
|
||||
df["Per Token Latency (ms/token)"] = df["Per Token Latency (ms/token)"].astype("float")
|
||||
df["Throughput (qps)"] = df["Throughput (qps)"].astype("float")
|
||||
df["Memory (GB)"] = df["Memory (GB)"].astype("float")
|
||||
df["Real Time Factor (RTF)"] = df["Real Time Factor (RTF)"].astype("float")
|
||||
|
||||
# get package name and version
|
||||
import pkg_resources # noqa: PLC0415
|
||||
|
||||
installed_packages = pkg_resources.working_set
|
||||
installed_packages_list = sorted(
|
||||
[f"{i.key}=={i.version}" for i in installed_packages if i.key in ["onnxruntime", "onnxruntime-gpu"]]
|
||||
)
|
||||
ort_pkg_name = ""
|
||||
ort_pkg_version = ""
|
||||
if installed_packages_list:
|
||||
ort_pkg_name = installed_packages_list[0].split("==")[0]
|
||||
ort_pkg_version = installed_packages_list[0].split("==")[1]
|
||||
|
||||
# Save results to csv with standard format
|
||||
records = []
|
||||
for _, row in df.iterrows():
|
||||
if row["Engine"] == "onnxruntime":
|
||||
record = BenchmarkRecord(
|
||||
row["Model Name"], row["Precision"], row["Engine"], row["Device"], ort_pkg_name, ort_pkg_version
|
||||
)
|
||||
else:
|
||||
record = BenchmarkRecord(
|
||||
row["Model Name"], row["Precision"], row["Engine"], row["Device"], torch.__name__, torch.__version__
|
||||
)
|
||||
record.config.customized["audio_file"] = row["Audio File"]
|
||||
record.config.warmup_runs = row["Warmup Runs"]
|
||||
record.config.measured_runs = row["Measured Runs"]
|
||||
|
||||
record.metrics.customized["duration"] = row["Duration (s)"]
|
||||
record.metrics.customized["token_length"] = row["Token Length"]
|
||||
record.metrics.customized["load_audio_latency"] = row["Load Audio Latency (s)"]
|
||||
record.metrics.customized["load_audio_throughput"] = row["Load Audio Throughput (qps)"]
|
||||
record.metrics.customized["feature_extractor_latency_s"] = row["Feature Extractor Latency (s)"]
|
||||
record.metrics.customized["feature_extractor_throughput_qps"] = row["Feature Extractor Throughput (qps)"]
|
||||
record.metrics.customized["per_token_latency_ms"] = row["Per Token Latency (ms/token)"]
|
||||
record.metrics.customized["rtf"] = row["Real Time Factor (RTF)"]
|
||||
|
||||
record.metrics.latency_ms_mean = row["Latency (s)"] * 1000
|
||||
record.metrics.throughput_qps = row["Throughput (qps)"]
|
||||
record.metrics.max_memory_usage_GB = row["Memory (GB)"]
|
||||
|
||||
records.append(record)
|
||||
|
||||
BenchmarkRecord.save_as_csv(filename, records)
|
||||
BenchmarkRecord.save_as_json(filename.replace(".csv", ".json"), records)
|
||||
logger.info(f"Results saved in {filename}!")
|
||||
|
||||
|
||||
def benchmark(args, benchmark_cmd, engine, audio_file, duration):
|
||||
log_filename = f"{engine}_{datetime.datetime.now():%Y-%m-%d_%H:%M:%S}.log"
|
||||
log_path = os.path.join(args.log_folder, log_filename)
|
||||
with open(log_path, "w") as log_file:
|
||||
process = subprocess.Popen(benchmark_cmd, stdout=log_file, stderr=log_file)
|
||||
try:
|
||||
process.wait(args.timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
|
||||
# Create entries for csv
|
||||
logger.info("Gathering data from log files...")
|
||||
base_results = [
|
||||
args.warmup_runs,
|
||||
args.num_runs,
|
||||
args.model_name,
|
||||
engine,
|
||||
args.precision,
|
||||
args.device,
|
||||
audio_file,
|
||||
duration,
|
||||
]
|
||||
results = process_log_file(args.device_id, log_path, base_results)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
setup_logger(args.verbose)
|
||||
logger.info(args.__dict__)
|
||||
torch.backends.cudnn.benchmark = True
|
||||
|
||||
config = WhisperConfig.from_pretrained(args.model_name)
|
||||
processor = WhisperProcessor.from_pretrained(args.model_name)
|
||||
|
||||
# Calculate forced decoder input ids
|
||||
hf_forced_decoder_ids = processor.get_decoder_prompt_ids(language=args.language, task=args.task)
|
||||
ort_forced_decoder_ids = [config.decoder_start_token_id] + [token_id[1] for token_id in hf_forced_decoder_ids]
|
||||
hf_decoder_input_ids_cmd = (
|
||||
["--decoder-input-ids", str(hf_forced_decoder_ids)] if args.language and args.task else []
|
||||
)
|
||||
ort_decoder_input_ids_cmd = (
|
||||
["--decoder-input-ids", str(ort_forced_decoder_ids)] if args.language and args.task else []
|
||||
)
|
||||
ort_tune_cmd = ["--tune"] if args.tune else []
|
||||
|
||||
all_results = []
|
||||
for audio_file in os.listdir(args.audio_path):
|
||||
audio_path = os.path.join(args.audio_path, audio_file)
|
||||
try:
|
||||
duration = librosa.get_duration(path=audio_path)
|
||||
except Exception as e:
|
||||
duration = -1
|
||||
logger.warning(f"An error occurred while trying to calculate the audio duration: {e}", exc_info=True)
|
||||
logger.warning(
|
||||
f"If you get an error that says:\n\tsoundfile.LibsndfileError: Error opening '{audio_file}': File contains data in an unknown format.\nyou may not have installed `ffmpeg` in addition to installing `librosa`."
|
||||
)
|
||||
logger.info(f"Testing {audio_path}...")
|
||||
|
||||
# Benchmark PyTorch without torch.compile
|
||||
if args.hf_pt_eager:
|
||||
benchmark_cmd = [ # noqa: RUF005
|
||||
"python",
|
||||
"-m",
|
||||
"models.whisper.benchmark",
|
||||
"--audio-path",
|
||||
audio_path,
|
||||
"--benchmark-type",
|
||||
"hf-pt-eager",
|
||||
"--model-name",
|
||||
args.model_name,
|
||||
"--precision",
|
||||
args.precision,
|
||||
"--device",
|
||||
args.device,
|
||||
"--device-id",
|
||||
str(args.device_id),
|
||||
"--warmup-runs",
|
||||
str(args.warmup_runs),
|
||||
"--num-runs",
|
||||
str(args.num_runs),
|
||||
"--log-folder",
|
||||
args.log_folder,
|
||||
] + hf_decoder_input_ids_cmd
|
||||
logger.info("Benchmark PyTorch without torch.compile")
|
||||
results = benchmark(args, benchmark_cmd, "pytorch-eager", audio_file, duration)
|
||||
all_results.extend(results)
|
||||
|
||||
# Benchmark PyTorch with torch.compile
|
||||
if args.hf_pt_compile:
|
||||
benchmark_cmd = [ # noqa: RUF005
|
||||
"python",
|
||||
"-m",
|
||||
"models.whisper.benchmark",
|
||||
"--audio-path",
|
||||
audio_path,
|
||||
"--benchmark-type",
|
||||
"hf-pt-compile",
|
||||
"--model-name",
|
||||
args.model_name,
|
||||
"--precision",
|
||||
args.precision,
|
||||
"--device",
|
||||
args.device,
|
||||
"--device-id",
|
||||
str(args.device_id),
|
||||
"--warmup-runs",
|
||||
str(args.warmup_runs),
|
||||
"--num-runs",
|
||||
str(args.num_runs),
|
||||
"--log-folder",
|
||||
args.log_folder,
|
||||
] + hf_decoder_input_ids_cmd
|
||||
logger.info("Benchmark PyTorch with torch.compile")
|
||||
results = benchmark(args, benchmark_cmd, "pytorch-compile", audio_file, duration)
|
||||
all_results.extend(results)
|
||||
|
||||
# Benchmark Optimum + ONNX Runtime
|
||||
if args.hf_ort_dir_path:
|
||||
benchmark_cmd = [ # noqa: RUF005
|
||||
"python",
|
||||
"-m",
|
||||
"models.whisper.benchmark",
|
||||
"--audio-path",
|
||||
audio_path,
|
||||
"--benchmark-type",
|
||||
"hf-ort",
|
||||
"--hf-ort-dir-path",
|
||||
args.hf_ort_dir_path,
|
||||
"--model-name",
|
||||
args.model_name,
|
||||
"--precision",
|
||||
args.precision,
|
||||
"--device",
|
||||
args.device,
|
||||
"--device-id",
|
||||
str(args.device_id),
|
||||
"--warmup-runs",
|
||||
str(args.warmup_runs),
|
||||
"--num-runs",
|
||||
str(args.num_runs),
|
||||
"--log-folder",
|
||||
args.log_folder,
|
||||
] + hf_decoder_input_ids_cmd
|
||||
logger.info("Benchmark Optimum + ONNX Runtime")
|
||||
results = benchmark(args, benchmark_cmd, "optimum-ort", audio_file, duration)
|
||||
all_results.extend(results)
|
||||
|
||||
# Benchmark ONNX Runtime
|
||||
if args.ort_model_path:
|
||||
benchmark_cmd = (
|
||||
[ # noqa: RUF005
|
||||
"python",
|
||||
"-m",
|
||||
"models.whisper.benchmark",
|
||||
"--audio-path",
|
||||
audio_path,
|
||||
"--benchmark-type",
|
||||
"ort",
|
||||
"--ort-model-path",
|
||||
args.ort_model_path,
|
||||
"--model-name",
|
||||
args.model_name,
|
||||
"--precision",
|
||||
args.precision,
|
||||
"--device",
|
||||
args.device,
|
||||
"--device-id",
|
||||
str(args.device_id),
|
||||
"--warmup-runs",
|
||||
str(args.warmup_runs),
|
||||
"--num-runs",
|
||||
str(args.num_runs),
|
||||
"--log-folder",
|
||||
args.log_folder,
|
||||
]
|
||||
+ ort_decoder_input_ids_cmd
|
||||
+ ort_tune_cmd
|
||||
)
|
||||
logger.info("Benchmark ONNX Runtime")
|
||||
results = benchmark(args, benchmark_cmd, "onnxruntime", audio_file, duration)
|
||||
all_results.extend(results)
|
||||
|
||||
csv_file = f"{args.model_size}-{args.precision}_{datetime.datetime.now():%Y-%m-%d_%H:%M:%S}.csv"
|
||||
save_results(all_results, os.path.join(args.log_folder, csv_file))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,573 @@
|
|||
# -------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
import torch
|
||||
from benchmark_helper import Precision, create_onnxruntime_session, prepare_environment, setup_logger
|
||||
from whisper_chain import chain_model
|
||||
from whisper_encoder import WhisperEncoder
|
||||
from whisper_helper import PRETRAINED_WHISPER_MODELS, WhisperHelper
|
||||
|
||||
from onnxruntime import quantization
|
||||
|
||||
logger = logging.getLogger("")
|
||||
|
||||
PROVIDERS = {
|
||||
"cpu": "CPUExecutionProvider",
|
||||
"cuda": "CUDAExecutionProvider",
|
||||
"rocm": "ROCMExecutionProvider",
|
||||
}
|
||||
|
||||
|
||||
def parse_arguments(argv=None):
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
conversion_args = parser.add_argument_group("Conversion Process Args")
|
||||
optional_inputs = parser.add_argument_group("Optional Inputs (for WhisperBeamSearch op)")
|
||||
optional_outputs = parser.add_argument_group("Optional Outputs (for WhisperBeamSearch op)")
|
||||
quant_args = parser.add_argument_group("INT8 Quantization Args")
|
||||
|
||||
#################################
|
||||
# Conversion options for Whisper
|
||||
#################################
|
||||
|
||||
conversion_args.add_argument(
|
||||
"-m",
|
||||
"--model_name_or_path",
|
||||
required=False,
|
||||
default=PRETRAINED_WHISPER_MODELS[0],
|
||||
type=str,
|
||||
help="Model path, or pretrained model name in the list: " + ", ".join(PRETRAINED_WHISPER_MODELS),
|
||||
)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"--model_impl",
|
||||
required=False,
|
||||
default="hf",
|
||||
choices=["hf", "openai"],
|
||||
type=str,
|
||||
help="Select implementation for export of encoder and decoder subgraphs",
|
||||
)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"--cache_dir",
|
||||
required=False,
|
||||
type=str,
|
||||
default=os.path.join(".", "cache_models"),
|
||||
help="Directory to cache pre-trained models",
|
||||
)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"--output",
|
||||
required=False,
|
||||
type=str,
|
||||
default=os.path.join(".", "onnx_models"),
|
||||
help="Output directory",
|
||||
)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"-o",
|
||||
"--optimize_onnx",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Use optimizer.py to optimize onnx model",
|
||||
)
|
||||
conversion_args.set_defaults(optimize_onnx=False)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"--use_gpu",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Use GPU for model inference",
|
||||
)
|
||||
conversion_args.set_defaults(use_gpu=False)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"-p",
|
||||
"--precision",
|
||||
required=False,
|
||||
type=Precision,
|
||||
default=Precision.FLOAT32,
|
||||
choices=[Precision.FLOAT32, Precision.FLOAT16, Precision.INT8],
|
||||
help="Precision of model to run. fp32 for full precision, fp16 for half precision, int8 for quantization",
|
||||
)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"--use_int64_inputs",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Use int64 instead of int32 for input_ids and attention_mask.",
|
||||
)
|
||||
conversion_args.set_defaults(use_int64_inputs=False)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"-r",
|
||||
"--provider",
|
||||
required=False,
|
||||
type=str,
|
||||
default="cpu",
|
||||
choices=list(PROVIDERS.keys()),
|
||||
help="Provider to benchmark. Default is CPUExecutionProvider.",
|
||||
)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"--verbose",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Enable verbose logging",
|
||||
)
|
||||
conversion_args.set_defaults(verbose=False)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"-e",
|
||||
"--use_external_data_format",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Save weights in external file. Necessary for 'small', 'medium', and 'large' models. Optional for 'tiny' and 'base' models.",
|
||||
)
|
||||
conversion_args.set_defaults(use_external_data_format=False)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"-w",
|
||||
"--overwrite",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Overwrite existing ONNX model",
|
||||
)
|
||||
conversion_args.set_defaults(overwrite=False)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"--separate_encoder_and_decoder_init",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Do not merge encoder and decoder init to initialize past KV caches. Output 3 instead of 2 ONNX models.",
|
||||
)
|
||||
conversion_args.set_defaults(separate_encoder_and_decoder_init=False)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"--no_beam_search_op",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Do not produce model with WhisperBeamSearch op, which chains encdecinit and decoder models into one op.",
|
||||
)
|
||||
conversion_args.set_defaults(no_beam_search_op=False)
|
||||
|
||||
conversion_args.add_argument(
|
||||
"--use_decoder_masked_mha",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Use DecoderMaskedMultiHeadAttention kernel for improved performance. This is currently an experimental feature.",
|
||||
)
|
||||
conversion_args.set_defaults(use_decoder_masked_mha=False)
|
||||
|
||||
#############################################################
|
||||
# Optional inputs for Whisper
|
||||
# (listed below in the order that WhisperBeamSearch expects)
|
||||
#############################################################
|
||||
|
||||
optional_inputs.add_argument(
|
||||
"-v",
|
||||
"--use_vocab_mask",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Use vocab_mask as an extra graph input to enable specific logits processing",
|
||||
)
|
||||
optional_inputs.set_defaults(use_vocab_mask=False)
|
||||
|
||||
optional_inputs.add_argument(
|
||||
"-u",
|
||||
"--use_prefix_vocab_mask",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Use prefix_vocab_mask as an extra graph input to enable specific logits processing",
|
||||
)
|
||||
optional_inputs.set_defaults(use_prefix_vocab_mask=False)
|
||||
|
||||
optional_inputs.add_argument(
|
||||
"-f",
|
||||
"--use_forced_decoder_ids",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Use decoder_input_ids as an extra graph input to the beam search op",
|
||||
)
|
||||
optional_inputs.set_defaults(use_forced_decoder_ids=False)
|
||||
|
||||
optional_inputs.add_argument(
|
||||
"-l",
|
||||
"--use_logits_processor",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Use logits_processor as an extra graph input to enable specific logits processing",
|
||||
)
|
||||
optional_inputs.set_defaults(use_specific_logits_processor=False)
|
||||
|
||||
optional_inputs.add_argument(
|
||||
"--collect_cross_qk",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Beam search model collect stacked cross QK.",
|
||||
)
|
||||
optional_inputs.set_defaults(collect_cross_qk=False)
|
||||
|
||||
optional_inputs.add_argument(
|
||||
"--extra_decoding_ids",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Need extra starting decoding ids for some feature like cross qk. Default if false.",
|
||||
)
|
||||
optional_inputs.set_defaults(extra_decoding_ids=False)
|
||||
|
||||
optional_inputs.add_argument(
|
||||
"-t",
|
||||
"--use_temperature",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Use temperature as an extra graph input for the WhisperBeamSearch op",
|
||||
)
|
||||
optional_inputs.set_defaults(use_temperature=False)
|
||||
|
||||
optional_inputs.add_argument(
|
||||
"--no_repeat_ngram_size",
|
||||
type=int,
|
||||
default=0,
|
||||
help="default to 0",
|
||||
)
|
||||
|
||||
#############################################################
|
||||
# Optional outputs for Whisper
|
||||
# (listed below in the order that WhisperBeamSearch expects)
|
||||
#############################################################
|
||||
|
||||
optional_outputs.add_argument(
|
||||
"--output_sequence_scores",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Beam search model output scores for each generated sequence.",
|
||||
)
|
||||
optional_outputs.set_defaults(output_sequence_scores=False)
|
||||
|
||||
optional_outputs.add_argument(
|
||||
"--output_scores",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Beam search model output scores over vocab per generated token.",
|
||||
)
|
||||
optional_outputs.set_defaults(output_scores=False)
|
||||
|
||||
optional_outputs.add_argument(
|
||||
"--output_cross_qk",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Beam search model output collected qk as output. Also hint collect_cross_qk",
|
||||
)
|
||||
optional_outputs.set_defaults(output_cross_qk=False)
|
||||
|
||||
optional_outputs.add_argument(
|
||||
"--cross_qk_onnx_model",
|
||||
required=False,
|
||||
type=str,
|
||||
default=None,
|
||||
help="The model which consumes cross_qk outputs.",
|
||||
)
|
||||
|
||||
optional_outputs.add_argument(
|
||||
"--output_no_speech_probs",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Beam search model output no speech probs which is computed from the encoder/context-decoder graph.",
|
||||
)
|
||||
optional_outputs.set_defaults(output_no_speech_probs=False)
|
||||
|
||||
###################################
|
||||
# Quantization options for Whisper
|
||||
###################################
|
||||
|
||||
quant_args.add_argument(
|
||||
"--quantize_embedding_layer",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Quantize MatMul, GEMM, and Gather.",
|
||||
)
|
||||
quant_args.set_defaults(quantize_embedding_layer=False)
|
||||
|
||||
quant_args.add_argument(
|
||||
"--quantize_per_channel",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Quantize weights per each channel.",
|
||||
)
|
||||
quant_args.set_defaults(quantize_per_channel=False)
|
||||
|
||||
quant_args.add_argument(
|
||||
"--quantize_reduce_range",
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Quantize weights with 7 bits.",
|
||||
)
|
||||
quant_args.set_defaults(quantize_reduce_range=False)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# Collect cross QKs if either flag is enabled
|
||||
args.collect_cross_qk = args.collect_cross_qk or args.output_cross_qk
|
||||
|
||||
# FP32 CPU can be supported here once the DMMHA CPU kernel bugs are fixed
|
||||
args.use_decoder_masked_mha = args.use_decoder_masked_mha and args.provider == "cuda"
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def export_onnx_models(
|
||||
model_name_or_path,
|
||||
model_impl,
|
||||
cache_dir,
|
||||
output_dir,
|
||||
use_gpu,
|
||||
use_external_data_format,
|
||||
optimize_onnx,
|
||||
precision,
|
||||
verbose,
|
||||
use_forced_decoder_ids: bool = False,
|
||||
merge_encoder_and_decoder_init: bool = True,
|
||||
no_beam_search_op: bool = False,
|
||||
use_decoder_masked_mha: bool = False,
|
||||
output_qk: bool = False,
|
||||
overwrite: bool = False,
|
||||
use_int32_inputs: bool = True,
|
||||
quantize_embedding_layer: bool = False,
|
||||
quantize_per_channel: bool = False,
|
||||
quantize_reduce_range: bool = False,
|
||||
provider: str = "cpu",
|
||||
):
|
||||
device = torch.device("cuda" if use_gpu else "cpu")
|
||||
|
||||
models = WhisperHelper.load_model(
|
||||
model_name_or_path,
|
||||
model_impl,
|
||||
cache_dir,
|
||||
device,
|
||||
torch.float16 if precision == Precision.FLOAT16 else torch.float32,
|
||||
merge_encoder_and_decoder_init,
|
||||
no_beam_search_op,
|
||||
output_qk,
|
||||
)
|
||||
config = models["decoder"].config
|
||||
|
||||
if (not use_external_data_format) and (config.num_hidden_layers > 24):
|
||||
logger.warning("You MUST pass `--use_external_data_format` because model size > 2GB")
|
||||
raise Exception("Please pass `--use_external_data_format` for this model.")
|
||||
|
||||
output_paths = []
|
||||
for name, model in models.items():
|
||||
print(f"========> Handling {name} model......")
|
||||
filename_suffix = "_" + name
|
||||
|
||||
onnx_path = WhisperHelper.get_onnx_path(
|
||||
output_dir,
|
||||
model_name_or_path,
|
||||
suffix=filename_suffix,
|
||||
new_folder=False,
|
||||
)
|
||||
|
||||
# Export to ONNX
|
||||
if overwrite or not os.path.exists(onnx_path):
|
||||
logger.info(f"Exporting ONNX model to {onnx_path}")
|
||||
WhisperHelper.export_onnx(
|
||||
model,
|
||||
onnx_path,
|
||||
PROVIDERS[provider],
|
||||
verbose,
|
||||
use_external_data_format,
|
||||
use_fp16_inputs=(precision == Precision.FLOAT16),
|
||||
use_int32_inputs=use_int32_inputs,
|
||||
use_encoder_hidden_states=(name == "decoder_init"),
|
||||
use_kv_cache_inputs=(name == "decoder"),
|
||||
)
|
||||
else:
|
||||
logger.info(f"Skip exporting: existing ONNX model {onnx_path}")
|
||||
|
||||
# Optimize ONNX model
|
||||
if optimize_onnx or precision != Precision.FLOAT32:
|
||||
output_path = WhisperHelper.get_onnx_path(
|
||||
output_dir,
|
||||
model_name_or_path,
|
||||
suffix=filename_suffix + "_" + str(precision),
|
||||
new_folder=False,
|
||||
)
|
||||
|
||||
if overwrite or not os.path.exists(output_path):
|
||||
if optimize_onnx:
|
||||
logger.info(f"Optimizing model to {output_path}")
|
||||
WhisperHelper.optimize_onnx(
|
||||
onnx_path,
|
||||
output_path,
|
||||
precision == Precision.FLOAT16,
|
||||
model.config.encoder_attention_heads,
|
||||
model.config.d_model,
|
||||
model.config.decoder_layers,
|
||||
use_external_data_format,
|
||||
use_gpu=use_gpu,
|
||||
provider=provider,
|
||||
is_decoder=(name == "decoder"),
|
||||
no_beam_search_op=no_beam_search_op,
|
||||
use_decoder_masked_mha=use_decoder_masked_mha,
|
||||
output_qk=output_qk,
|
||||
)
|
||||
# Remove old ONNX model and old data file
|
||||
if os.path.exists(onnx_path):
|
||||
os.remove(onnx_path)
|
||||
if os.path.exists(onnx_path + ".data"):
|
||||
os.remove(onnx_path + ".data")
|
||||
onnx_path = output_path
|
||||
|
||||
if isinstance(model, WhisperEncoder):
|
||||
model.verify_onnx(
|
||||
onnx_path,
|
||||
PROVIDERS[provider],
|
||||
use_fp16_inputs=(precision == Precision.FLOAT16),
|
||||
)
|
||||
else:
|
||||
model.verify_onnx(
|
||||
onnx_path,
|
||||
PROVIDERS[provider],
|
||||
use_fp16_inputs=(precision == Precision.FLOAT16),
|
||||
use_int32_inputs=use_int32_inputs,
|
||||
)
|
||||
|
||||
if precision == Precision.INT8:
|
||||
quantization.quantize_dynamic(
|
||||
onnx_path,
|
||||
output_path,
|
||||
op_types_to_quantize=(
|
||||
["MatMul", "Gemm", "Gather"] if quantize_embedding_layer else ["MatMul", "Gemm"]
|
||||
),
|
||||
use_external_data_format=use_external_data_format,
|
||||
per_channel=quantize_per_channel,
|
||||
reduce_range=quantize_reduce_range,
|
||||
extra_options={"MatMulConstBOnly": True},
|
||||
)
|
||||
else:
|
||||
logger.info(f"Skip optimizing: existing ONNX model {onnx_path}")
|
||||
else:
|
||||
output_path = onnx_path
|
||||
|
||||
output_paths.append(output_path)
|
||||
|
||||
return output_paths
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = parse_arguments(argv)
|
||||
|
||||
setup_logger(args.verbose)
|
||||
|
||||
logger.info(f"Arguments:{args}")
|
||||
|
||||
cache_dir = args.cache_dir
|
||||
output_dir = args.output if not args.output.endswith(".onnx") else os.path.dirname(args.output)
|
||||
prepare_environment(cache_dir, output_dir, args.use_gpu)
|
||||
|
||||
if args.precision == Precision.FLOAT16:
|
||||
assert args.use_gpu, "fp16 requires --use_gpu"
|
||||
|
||||
output_paths = export_onnx_models(
|
||||
args.model_name_or_path,
|
||||
args.model_impl,
|
||||
cache_dir,
|
||||
output_dir,
|
||||
args.use_gpu,
|
||||
args.use_external_data_format,
|
||||
args.optimize_onnx,
|
||||
args.precision,
|
||||
args.verbose,
|
||||
args.use_forced_decoder_ids,
|
||||
not args.separate_encoder_and_decoder_init,
|
||||
args.no_beam_search_op,
|
||||
args.use_decoder_masked_mha,
|
||||
args.output_cross_qk,
|
||||
args.overwrite,
|
||||
not args.use_int64_inputs,
|
||||
args.quantize_embedding_layer,
|
||||
args.quantize_per_channel,
|
||||
args.quantize_reduce_range,
|
||||
args.provider,
|
||||
)
|
||||
|
||||
max_diff = 0
|
||||
if not args.no_beam_search_op:
|
||||
logger.info("Chaining model ... :")
|
||||
args.beam_model_output_dir = WhisperHelper.get_onnx_path(
|
||||
output_dir,
|
||||
args.model_name_or_path,
|
||||
suffix="_beamsearch",
|
||||
new_folder=False,
|
||||
)
|
||||
for path in output_paths:
|
||||
if "encoder_decoder" in path or "encoder" in path:
|
||||
args.encoder_path = path
|
||||
elif "decoder" in path:
|
||||
args.decoder_path = path
|
||||
chain_model(args)
|
||||
output_paths.append(args.beam_model_output_dir)
|
||||
|
||||
# Check chained model
|
||||
ort_session = create_onnxruntime_session(
|
||||
args.beam_model_output_dir,
|
||||
use_gpu=args.use_gpu,
|
||||
provider=args.provider,
|
||||
)
|
||||
device = torch.device("cuda" if args.use_gpu else "cpu")
|
||||
|
||||
# Wrap parity check in try-except to allow export to continue in case this produces an error
|
||||
try:
|
||||
with torch.no_grad():
|
||||
# Verify batched decoding with prompts for OpenAI implementation
|
||||
if args.model_impl == "openai" and args.use_forced_decoder_ids:
|
||||
max_diff = WhisperHelper.verify_onnx(
|
||||
args.model_name_or_path, cache_dir, ort_session, device, batch_size=2, prompt_mode=True
|
||||
)
|
||||
else:
|
||||
max_diff = WhisperHelper.verify_onnx(args.model_name_or_path, cache_dir, ort_session, device)
|
||||
if max_diff > 1e-4:
|
||||
logger.warning("PyTorch and ONNX Runtime results are NOT close")
|
||||
else:
|
||||
logger.info("PyTorch and ONNX Runtime results are close")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"An error occurred while trying to verify parity between PyTorch and ONNX Runtime: {e}", exc_info=True
|
||||
)
|
||||
|
||||
# Remove extra ONNX models saved in output directory
|
||||
for _file in os.listdir(output_dir):
|
||||
if "_beamsearch" not in _file and "_jump_times" not in _file:
|
||||
path = os.path.join(output_dir, _file)
|
||||
os.remove(path)
|
||||
if path in output_paths:
|
||||
output_paths.remove(path)
|
||||
|
||||
else:
|
||||
# Create ancillary JSON files for ONNX Runtime GenAI and/or Hugging Face's Optimum
|
||||
WhisperHelper.save_processing(
|
||||
args.model_name_or_path,
|
||||
args.provider,
|
||||
args.separate_encoder_and_decoder_init,
|
||||
args.use_decoder_masked_mha,
|
||||
args.output_cross_qk,
|
||||
next(iter(filter(lambda path: "encoder" in path, output_paths))),
|
||||
next(iter(filter(lambda path: "decoder" in path, output_paths))),
|
||||
output_dir,
|
||||
cache_dir,
|
||||
)
|
||||
|
||||
logger.info(f"Done! Outputs: {output_paths}")
|
||||
return max_diff
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,331 @@
|
|||
# -------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import onnx
|
||||
from benchmark_helper import Precision
|
||||
from convert_generation import (
|
||||
get_shared_initializers,
|
||||
update_decoder_subgraph_output_cross_attention,
|
||||
update_decoder_subgraph_share_buffer_and_use_decoder_masked_mha,
|
||||
)
|
||||
from onnx import TensorProto, helper
|
||||
from transformers import WhisperConfig, WhisperTokenizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def verify_inputs(beam_inputs, graph_inputs):
|
||||
# Verify that ONNX graph's inputs match beam search op's inputs
|
||||
beam_required_inputs = list(filter(lambda beam_input: beam_input, beam_inputs))
|
||||
assert len(graph_inputs) == len(beam_required_inputs)
|
||||
for graph_input, beam_input in zip(graph_inputs, beam_required_inputs, strict=False):
|
||||
# Check if graph_input is in beam_input to handle beam_input names with the "_fp16" suffix
|
||||
assert graph_input.name in beam_input
|
||||
|
||||
|
||||
def clean_list(arr, remove_all_strings=True):
|
||||
if remove_all_strings:
|
||||
# Remove all empty strings in list
|
||||
return list(filter(lambda elm: elm != "", arr))
|
||||
|
||||
# Remove empty strings at end of list
|
||||
while len(arr) > 0:
|
||||
if arr[-1] == "":
|
||||
arr.pop()
|
||||
else:
|
||||
break
|
||||
return arr
|
||||
|
||||
|
||||
def chain_model(args):
|
||||
# Load encoder/decoder and insert necessary (but unused) graph inputs expected by WhisperBeamSearch op
|
||||
encoder_model = onnx.load_model(args.encoder_path, load_external_data=True)
|
||||
encoder_model.graph.name = "encoderdecoderinit subgraph"
|
||||
|
||||
decoder_model = onnx.load_model(args.decoder_path, load_external_data=True)
|
||||
decoder_model.graph.name = "decoder subgraph"
|
||||
|
||||
config = WhisperConfig.from_pretrained(args.model_name_or_path, cache_dir=args.cache_dir)
|
||||
tokenizer = WhisperTokenizer.from_pretrained(args.model_name_or_path, cache_dir=args.cache_dir)
|
||||
|
||||
# Create inputs/outputs for WhisperBeamSearch op
|
||||
temperature_name = "temperature_fp16" if args.precision == Precision.FLOAT16 else "temperature"
|
||||
beam_inputs = [
|
||||
"input_features_fp16" if args.precision == Precision.FLOAT16 else "input_features",
|
||||
"max_length",
|
||||
"min_length",
|
||||
"num_beams",
|
||||
"num_return_sequences",
|
||||
"length_penalty_fp16" if args.precision == Precision.FLOAT16 else "length_penalty",
|
||||
"repetition_penalty_fp16" if args.precision == Precision.FLOAT16 else "repetition_penalty",
|
||||
"vocab_mask" if args.use_vocab_mask else "",
|
||||
"prefix_vocab_mask" if args.use_prefix_vocab_mask else "",
|
||||
"", # attention mask
|
||||
"decoder_input_ids" if args.use_forced_decoder_ids else "",
|
||||
"logits_processor" if args.use_logits_processor else "",
|
||||
"cross_qk_layer_head" if args.collect_cross_qk else "",
|
||||
"extra_decoding_ids" if args.extra_decoding_ids else "",
|
||||
temperature_name if args.use_temperature else "",
|
||||
]
|
||||
|
||||
sequence_scores_name = "sequence_scores_fp16" if args.precision == Precision.FLOAT16 else "sequence_scores"
|
||||
scores_name = "scores_fp16" if args.precision == Precision.FLOAT16 else "scores"
|
||||
beam_outputs = [
|
||||
"sequences",
|
||||
sequence_scores_name if args.output_sequence_scores else "",
|
||||
scores_name if args.output_scores else "",
|
||||
"cross_qk" if args.collect_cross_qk else "",
|
||||
"no_speech_probs_beam" if args.output_no_speech_probs else "",
|
||||
]
|
||||
|
||||
graph_nodes = []
|
||||
if args.precision == Precision.FLOAT16:
|
||||
input_features_cast_node = helper.make_node(
|
||||
"Cast",
|
||||
inputs=["input_features"],
|
||||
outputs=["input_features_fp16"],
|
||||
name="CastInputFeaturesToFp16",
|
||||
to=TensorProto.FLOAT16,
|
||||
)
|
||||
len_pen_cast_node = helper.make_node(
|
||||
"Cast",
|
||||
inputs=["length_penalty"],
|
||||
outputs=["length_penalty_fp16"],
|
||||
name="CastLengthPenaltyToFp16",
|
||||
to=TensorProto.FLOAT16,
|
||||
)
|
||||
rep_pen_cast_node = helper.make_node(
|
||||
"Cast",
|
||||
inputs=["repetition_penalty"],
|
||||
outputs=["repetition_penalty_fp16"],
|
||||
name="CastRepetitionPenaltyToFp16",
|
||||
to=TensorProto.FLOAT16,
|
||||
)
|
||||
graph_nodes.extend([input_features_cast_node, len_pen_cast_node, rep_pen_cast_node])
|
||||
|
||||
if args.use_temperature:
|
||||
temp_cast_node = helper.make_node(
|
||||
"Cast",
|
||||
inputs=["temperature"],
|
||||
outputs=["temperature_fp16"],
|
||||
name="temperature_to_fp16",
|
||||
to=TensorProto.FLOAT16,
|
||||
)
|
||||
graph_nodes.append(temp_cast_node)
|
||||
|
||||
if args.output_sequence_scores:
|
||||
output_sequence_scores_cast_node = helper.make_node(
|
||||
"Cast",
|
||||
inputs=["sequence_scores_fp16"],
|
||||
outputs=["sequence_scores"],
|
||||
name="CastOutputSequenceScoresToFp32",
|
||||
to=TensorProto.FLOAT,
|
||||
)
|
||||
graph_nodes.append(output_sequence_scores_cast_node)
|
||||
|
||||
if args.output_scores:
|
||||
output_scores_cast_node = helper.make_node(
|
||||
"Cast",
|
||||
inputs=["scores_fp16"],
|
||||
outputs=["scores"],
|
||||
name="CastScoresToFp32",
|
||||
to=TensorProto.FLOAT,
|
||||
)
|
||||
graph_nodes.append(output_scores_cast_node)
|
||||
|
||||
# Create WhisperBeamSearch op
|
||||
beam_search_attrs = [
|
||||
helper.make_attribute("eos_token_id", config.eos_token_id),
|
||||
helper.make_attribute("pad_token_id", config.pad_token_id),
|
||||
helper.make_attribute(
|
||||
"decoder_start_token_id", config.decoder_start_token_id
|
||||
), # same as tokenizer.convert_tokens_to_ids(['<|startoftranscript|>'])[0]
|
||||
helper.make_attribute("translate_token_id", tokenizer.convert_tokens_to_ids(["<|translate|>"])[0]),
|
||||
helper.make_attribute("transcribe_token_id", tokenizer.convert_tokens_to_ids(["<|transcribe|>"])[0]),
|
||||
helper.make_attribute("start_of_lm_token_id", tokenizer.convert_tokens_to_ids(["<|startoflm|>"])[0]),
|
||||
(
|
||||
helper.make_attribute("no_speech_token_id", tokenizer.convert_tokens_to_ids(["<|nospeech|>"])[0])
|
||||
if args.output_no_speech_probs
|
||||
else ""
|
||||
),
|
||||
helper.make_attribute("no_timestamps_token_id", tokenizer.convert_tokens_to_ids(["<|notimestamps|>"])[0]),
|
||||
helper.make_attribute("beginning_timestamp_token_id", tokenizer.convert_tokens_to_ids(["<|0.00|>"])[0]),
|
||||
helper.make_attribute("no_repeat_ngram_size", args.no_repeat_ngram_size),
|
||||
helper.make_attribute("early_stopping", True),
|
||||
helper.make_attribute("model_type", 2),
|
||||
helper.make_attribute("decoder_output_cross_qk", 1) if args.collect_cross_qk else "",
|
||||
]
|
||||
node = helper.make_node(
|
||||
"WhisperBeamSearch",
|
||||
inputs=clean_list(beam_inputs, remove_all_strings=False),
|
||||
outputs=clean_list(beam_outputs, remove_all_strings=False),
|
||||
name="BeamSearch",
|
||||
domain="com.microsoft",
|
||||
)
|
||||
node.attribute.extend(clean_list(beam_search_attrs, remove_all_strings=True))
|
||||
|
||||
# Graph inputs
|
||||
input_features = helper.make_tensor_value_info(
|
||||
"input_features", TensorProto.FLOAT, ["batch_size", "feature_size", "sequence_length"]
|
||||
)
|
||||
max_length = helper.make_tensor_value_info("max_length", TensorProto.INT32, [1])
|
||||
min_length = helper.make_tensor_value_info("min_length", TensorProto.INT32, [1])
|
||||
num_beams = helper.make_tensor_value_info("num_beams", TensorProto.INT32, [1])
|
||||
num_return_sequences = helper.make_tensor_value_info("num_return_sequences", TensorProto.INT32, [1])
|
||||
length_penalty = helper.make_tensor_value_info("length_penalty", TensorProto.FLOAT, [1])
|
||||
repetition_penalty = helper.make_tensor_value_info("repetition_penalty", TensorProto.FLOAT, [1])
|
||||
vocab_mask = helper.make_tensor_value_info("vocab_mask", TensorProto.INT32, [config.vocab_size])
|
||||
prefix_vocab_mask = helper.make_tensor_value_info(
|
||||
"prefix_vocab_mask", TensorProto.INT32, ["batch_size", config.vocab_size]
|
||||
)
|
||||
decoder_input_ids = helper.make_tensor_value_info(
|
||||
"decoder_input_ids", TensorProto.INT32, ["batch_size", "initial_sequence_length"]
|
||||
)
|
||||
logits_processor = helper.make_tensor_value_info("logits_processor", TensorProto.INT32, [1])
|
||||
cross_qk_layer_head = helper.make_tensor_value_info("cross_qk_layer_head", TensorProto.INT32, ["num_layer_head", 2])
|
||||
extra_decoding_ids = helper.make_tensor_value_info(
|
||||
"extra_decoding_ids", TensorProto.INT32, ["batch_size", "extra_decoding_ids_len"]
|
||||
)
|
||||
temperature = helper.make_tensor_value_info("temperature", TensorProto.FLOAT, [1])
|
||||
|
||||
graph_inputs = clean_list(
|
||||
[
|
||||
input_features,
|
||||
max_length,
|
||||
min_length,
|
||||
num_beams,
|
||||
num_return_sequences,
|
||||
length_penalty,
|
||||
repetition_penalty,
|
||||
vocab_mask if args.use_vocab_mask else "",
|
||||
prefix_vocab_mask if args.use_prefix_vocab_mask else "",
|
||||
decoder_input_ids if args.use_forced_decoder_ids else "",
|
||||
logits_processor if args.use_logits_processor else "",
|
||||
cross_qk_layer_head if args.collect_cross_qk else "",
|
||||
extra_decoding_ids if args.extra_decoding_ids else "",
|
||||
temperature if args.use_temperature else "",
|
||||
]
|
||||
)
|
||||
|
||||
# Graph outputs
|
||||
sequences = helper.make_tensor_value_info(
|
||||
"sequences", TensorProto.INT32, ["batch_size", "num_return_sequences", "max_length"]
|
||||
)
|
||||
sequence_scores = helper.make_tensor_value_info("sequence_scores", TensorProto.FLOAT, ["batch_size"])
|
||||
scores = helper.make_tensor_value_info("scores", TensorProto.FLOAT, ["batch_size"])
|
||||
cross_qk = helper.make_tensor_value_info(
|
||||
"cross_qk",
|
||||
TensorProto.FLOAT,
|
||||
["batch_size", "num_return_sequences", "num_layer_head_cross_qk", "max_length", "frames"],
|
||||
)
|
||||
no_speech_probs = helper.make_tensor_value_info("no_speech_probs", TensorProto.FLOAT, ["batch_size"])
|
||||
|
||||
graph_outputs = clean_list(
|
||||
[
|
||||
sequences,
|
||||
sequence_scores if args.output_sequence_scores else "",
|
||||
scores if args.output_scores else "",
|
||||
cross_qk if args.output_cross_qk or (not args.cross_qk_onnx_model and args.collect_cross_qk) else "",
|
||||
no_speech_probs if args.output_no_speech_probs else "",
|
||||
]
|
||||
)
|
||||
|
||||
# Replace MultiHeadAttention with DecoderMaskedMultiHeadAttention for CUDA EP inference
|
||||
if hasattr(args, "use_gpu") and args.use_gpu:
|
||||
if update_decoder_subgraph_share_buffer_and_use_decoder_masked_mha(decoder_model.graph):
|
||||
logger.info("Updated whisper decoder subgraph to use DecoderMaskedMultiHeadAttention successfully!")
|
||||
else:
|
||||
logger.warning("DecoderMaskedMultiHeadAttention could not be applied to whisper decoder subgraph")
|
||||
if hasattr(args, "collect_cross_qk") and args.collect_cross_qk:
|
||||
update_decoder_subgraph_output_cross_attention(decoder_model.graph)
|
||||
|
||||
# Initializers/opsets
|
||||
# Delete shared data between decoder/encoder and move to larger graph initializers
|
||||
initializers = get_shared_initializers(encoder_model, decoder_model)
|
||||
node.attribute.extend(
|
||||
[
|
||||
helper.make_attribute("decoder", decoder_model.graph),
|
||||
helper.make_attribute("encoder", encoder_model.graph),
|
||||
]
|
||||
)
|
||||
|
||||
opset_import = [helper.make_opsetid(domain="com.microsoft", version=1), helper.make_opsetid(domain="", version=17)]
|
||||
|
||||
graph_nodes.append(node)
|
||||
if args.output_no_speech_probs:
|
||||
prob_cast_node = helper.make_node(
|
||||
"Cast",
|
||||
inputs=["no_speech_probs_beam"],
|
||||
outputs=["no_speech_probs"],
|
||||
name="no_speech_probs_cast_to_fp32",
|
||||
to=TensorProto.FLOAT,
|
||||
)
|
||||
graph_nodes.append(prob_cast_node)
|
||||
|
||||
# Make graph with WhisperBeamSearch op
|
||||
beam_graph = helper.make_graph(
|
||||
graph_nodes,
|
||||
name="WhisperBeamSearch Graph",
|
||||
inputs=graph_inputs,
|
||||
outputs=graph_outputs,
|
||||
initializer=initializers,
|
||||
)
|
||||
beam_graph_input_names = [gi.name for gi in graph_inputs]
|
||||
beam_graph_output_names = [go.name for go in graph_outputs]
|
||||
|
||||
if args.cross_qk_onnx_model:
|
||||
post_qk_model = onnx.load_model(args.cross_qk_onnx_model, load_external_data=True)
|
||||
post_qk_graph = post_qk_model.graph
|
||||
beam_graph.initializer.extend(post_qk_graph.initializer)
|
||||
beam_graph.node.extend(post_qk_graph.node)
|
||||
# If tensor from cross_qk_onnx_model has same name as tensor in beamsearch graph, treat them as same tensor.
|
||||
# User should notice this rule when provide cross_qk_onnx_model to append to the beamsearch node.
|
||||
for pgi in post_qk_graph.input:
|
||||
if (
|
||||
(pgi.name not in beam_graph_input_names)
|
||||
and (pgi.name not in beam_graph_output_names)
|
||||
and (pgi.name != "cross_qk")
|
||||
):
|
||||
beam_graph.input.extend([pgi])
|
||||
beam_graph.output.extend(post_qk_graph.output)
|
||||
|
||||
# Verify graph's inputs match beam search's inputs
|
||||
verify_inputs(beam_inputs, graph_inputs)
|
||||
|
||||
assert decoder_model.ir_version == encoder_model.ir_version
|
||||
logger.info(f"Using IR version {decoder_model.ir_version} for chained model")
|
||||
|
||||
# Set IR version of chained model to IR version of subgraphs in order to generate a working E2E model
|
||||
beam_model = helper.make_model_gen_version(
|
||||
beam_graph,
|
||||
producer_name="onnxruntime.transformers",
|
||||
opset_imports=opset_import,
|
||||
ir_version=decoder_model.ir_version,
|
||||
)
|
||||
|
||||
# Save WhisperBeamSearch graph and external data
|
||||
if os.path.isfile(args.beam_model_output_dir):
|
||||
logger.info(f"Overwriting {args.beam_model_output_dir} and {args.beam_model_output_dir + '.data'}")
|
||||
if os.path.exists(args.beam_model_output_dir):
|
||||
os.remove(args.beam_model_output_dir)
|
||||
if os.path.exists(args.beam_model_output_dir + ".data"):
|
||||
os.remove(args.beam_model_output_dir + ".data")
|
||||
|
||||
onnx.save(
|
||||
beam_model,
|
||||
args.beam_model_output_dir,
|
||||
save_as_external_data=args.use_external_data_format,
|
||||
all_tensors_to_one_file=True,
|
||||
convert_attribute=True,
|
||||
location=f"{os.path.basename(args.beam_model_output_dir)}.data",
|
||||
)
|
||||
try:
|
||||
onnx.checker.check_model(args.beam_model_output_dir, full_check=True)
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred while running the ONNX checker: {e}", exc_info=True) # noqa: G201
|
||||
|
|
@ -0,0 +1,464 @@
|
|||
# -------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import onnx
|
||||
import torch
|
||||
from float16 import convert_float_to_float16
|
||||
from google.protobuf.internal.containers import RepeatedCompositeFieldContainer
|
||||
from onnx import ModelProto, ValueInfoProto
|
||||
from onnx_model import OnnxModel
|
||||
from past_helper import PastKeyValuesHelper
|
||||
from transformers import WhisperConfig
|
||||
from whisper_inputs import (
|
||||
convert_inputs_for_ort,
|
||||
get_model_dynamic_axes,
|
||||
get_sample_decoder_inputs,
|
||||
group_past_key_values,
|
||||
)
|
||||
|
||||
from onnxruntime import InferenceSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WhisperDecoder(torch.nn.Module):
|
||||
"""A Whisper decoder with optional past key values"""
|
||||
|
||||
def __init__(self, config: WhisperConfig, model: torch.nn.Module, model_impl: str, no_beam_search_op: bool = False):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.device = model.device
|
||||
self.model_impl = model_impl
|
||||
self.no_beam_search_op = no_beam_search_op
|
||||
|
||||
self.decoder = None if model_impl == "openai" else model.model.decoder
|
||||
self.proj_out = None if model_impl == "openai" else model.proj_out
|
||||
self.model = model if model_impl == "openai" else None
|
||||
|
||||
self.max_source_positions = self.config.max_source_positions
|
||||
self.num_heads = self.config.decoder_attention_heads
|
||||
self.head_size = self.config.d_model // self.num_heads
|
||||
|
||||
def hf_forward(
|
||||
self,
|
||||
decoder_input_ids: torch.Tensor,
|
||||
encoder_hidden_states: torch.Tensor | None = None,
|
||||
past_key_values: list[tuple[torch.Tensor]] | None = None,
|
||||
):
|
||||
outputs = self.decoder(
|
||||
encoder_hidden_states=encoder_hidden_states,
|
||||
input_ids=decoder_input_ids,
|
||||
past_key_values=past_key_values,
|
||||
use_cache=True,
|
||||
)
|
||||
logits = self.proj_out(outputs.last_hidden_state)
|
||||
present_key_values = outputs.past_key_values
|
||||
|
||||
if past_key_values is None:
|
||||
# Return present_self_* and present_cross_* for decoder-init
|
||||
return logits, present_key_values
|
||||
|
||||
# Before: (past_key_self_0, past_value_self_0, past_key_cross_0, past_value_cross_0),
|
||||
# (past_key_self_1, past_value_self_1, past_key_cross_1, past_value_cross_1),
|
||||
# After: (past_key_self_0, past_value_self_0, past_key_self_1, past_value_self_1), ...,
|
||||
# (past_key_cross_0, past_value_cross_0, past_key_cross_1, past_value_cross_1), ...
|
||||
present_self, present_cross = PastKeyValuesHelper.group_by_self_and_cross(present_key_values)
|
||||
|
||||
# Return present_self_* for decoder-with-past since past_cross_* and present_cross_* are identical
|
||||
return logits, present_self
|
||||
|
||||
def oai_forward(
|
||||
self,
|
||||
decoder_input_ids: torch.Tensor,
|
||||
encoder_hidden_states: torch.Tensor | None = None,
|
||||
past_key_values: list[tuple[torch.Tensor]] | None = None,
|
||||
):
|
||||
past_kv_cache = {}
|
||||
if past_key_values is not None:
|
||||
# Convert past KV caches (BxNxSxH --> BxSxNxH --> BxSxD) for OpenAI's forward pass
|
||||
self_attn_kv_caches, cross_attn_kv_caches = group_past_key_values(past_key_values)
|
||||
self_attn_kv_caches = [past_kv.transpose(1, 2) for past_kv in self_attn_kv_caches]
|
||||
self_attn_kv_caches = [past_kv.reshape((*past_kv.shape[:2], -1)) for past_kv in self_attn_kv_caches]
|
||||
cross_attn_kv_caches = [past_kv.transpose(1, 2) for past_kv in cross_attn_kv_caches]
|
||||
cross_attn_kv_caches = [past_kv.reshape((*past_kv.shape[:2], -1)) for past_kv in cross_attn_kv_caches]
|
||||
|
||||
for idx, block in enumerate(self.model.decoder.blocks):
|
||||
past_kv_cache[block.attn.key] = self_attn_kv_caches[2 * idx]
|
||||
past_kv_cache[block.attn.value] = self_attn_kv_caches[2 * idx + 1]
|
||||
past_kv_cache[block.cross_attn.key] = cross_attn_kv_caches[2 * idx]
|
||||
past_kv_cache[block.cross_attn.value] = cross_attn_kv_caches[2 * idx + 1]
|
||||
|
||||
# Install OpenAI's hooks on the forward pass of each nn.Linear for key and value
|
||||
# since the hooks will capture the output of the key and value MatMuls, which
|
||||
# represent the current keys and values.
|
||||
#
|
||||
# For OpenAI's forward pass, the hook function will also perform the concat
|
||||
# operation (past_kv + curr_kv --> pres_kv) if needed. However, the ONNX model
|
||||
# will not contain this concat operation because the present KV caches aren't
|
||||
# returned by OpenAI's forward pass.
|
||||
kv_cache, hooks = self.model.install_kv_cache_hooks()
|
||||
|
||||
# Run forward pass
|
||||
# NOTE: There is a bug with openai-whisper==20240930 with the introduction of SDPA.
|
||||
# In the Whisper codebase, the following line
|
||||
#
|
||||
# is_causal = mask is not None and n_ctx > 1
|
||||
#
|
||||
# has been added where `mask` is a torch tensor. The right-hand side evaluates to `tensor(True/False)`
|
||||
# but `is_causal` only accepts the boolean value. The fix is to apply `.item()` after the right-hand
|
||||
# side has been evaluated. In other words, the line should be
|
||||
#
|
||||
# is_causal = (mask is not None and n_ctx > 1).item()
|
||||
#
|
||||
# instead.
|
||||
logits = self.model.decoder(x=decoder_input_ids, xa=encoder_hidden_states, kv_cache=past_kv_cache)
|
||||
|
||||
# Re-do concat operation on self attention KV caches for ONNX export (if past self attention KV caches exist)
|
||||
if past_key_values is not None:
|
||||
for block in self.model.decoder.blocks:
|
||||
kv_cache[block.attn.key] = torch.cat(
|
||||
[past_kv_cache[block.attn.key], kv_cache[block.attn.key]], dim=1
|
||||
).detach()
|
||||
kv_cache[block.attn.value] = torch.cat(
|
||||
[past_kv_cache[block.attn.value], kv_cache[block.attn.value]], dim=1
|
||||
).detach()
|
||||
|
||||
present_self, present_cross = [], []
|
||||
for block in self.model.decoder.blocks:
|
||||
# Group self and cross values
|
||||
present_self.append(kv_cache[block.attn.key])
|
||||
present_self.append(kv_cache[block.attn.value])
|
||||
if past_key_values is None:
|
||||
# Return present_self_* and present_cross_* for decoder-init
|
||||
present_cross.append(kv_cache[block.cross_attn.key])
|
||||
present_cross.append(kv_cache[block.cross_attn.value])
|
||||
|
||||
# Convert present KV caches (BxSxD --> BxSxNxH --> BxNxSxH) after OpenAI's forward pass
|
||||
present_self = [
|
||||
present_kv.reshape((*present_kv.shape[:2], -1, self.head_size)).transpose(1, 2)
|
||||
for present_kv in present_self
|
||||
]
|
||||
present_cross = [
|
||||
present_kv.reshape((*present_kv.shape[:2], -1, self.head_size)).transpose(1, 2)
|
||||
for present_kv in present_cross
|
||||
]
|
||||
|
||||
# Remove OpenAI's hooks since they can persist after this function completes
|
||||
for hook in hooks:
|
||||
hook.remove()
|
||||
|
||||
if past_key_values is None:
|
||||
# Return present_self_* and present_cross_* for decoder-init
|
||||
present_key_values = PastKeyValuesHelper.group_by_layer(
|
||||
present_self + present_cross, len(present_self) // 2
|
||||
)
|
||||
return logits, present_key_values
|
||||
|
||||
# Return present_self_* for decoder-with-past since past_cross_* and present_cross_* are identical
|
||||
return logits, present_self
|
||||
|
||||
def forward(
|
||||
self,
|
||||
decoder_input_ids: torch.Tensor,
|
||||
encoder_hidden_states: torch.Tensor | None = None,
|
||||
past_key_values: list[tuple[torch.Tensor]] | None = None,
|
||||
):
|
||||
if self.model_impl == "openai":
|
||||
return self.oai_forward(decoder_input_ids, encoder_hidden_states, past_key_values)
|
||||
return self.hf_forward(decoder_input_ids, encoder_hidden_states, past_key_values)
|
||||
|
||||
def input_names(self):
|
||||
if self.first_pass:
|
||||
input_names = ["input_ids", "encoder_hidden_states"]
|
||||
else:
|
||||
input_names = [
|
||||
"input_ids",
|
||||
"encoder_hidden_states",
|
||||
*list(
|
||||
chain.from_iterable(
|
||||
(f"past_key_self_{i}", f"past_value_self_{i}", f"past_key_cross_{i}", f"past_value_cross_{i}")
|
||||
for i in range(self.config.decoder_layers)
|
||||
)
|
||||
),
|
||||
]
|
||||
return input_names
|
||||
|
||||
def output_names(self):
|
||||
if self.first_pass:
|
||||
output_names = [
|
||||
"logits",
|
||||
*list(
|
||||
chain.from_iterable(
|
||||
(
|
||||
f"present_key_self_{i}",
|
||||
f"present_value_self_{i}",
|
||||
f"present_key_cross_{i}",
|
||||
f"present_value_cross_{i}",
|
||||
)
|
||||
for i in range(self.config.decoder_layers)
|
||||
)
|
||||
),
|
||||
]
|
||||
else:
|
||||
output_names = [
|
||||
"logits",
|
||||
*list(
|
||||
chain.from_iterable(
|
||||
(f"present_key_self_{i}", f"present_value_self_{i}") for i in range(self.config.decoder_layers)
|
||||
)
|
||||
),
|
||||
]
|
||||
return output_names
|
||||
|
||||
def dynamic_axes(self, input_names, output_names):
|
||||
dynamic_axes = get_model_dynamic_axes(self.config, input_names, output_names)
|
||||
if "input_ids" in dynamic_axes and not self.no_beam_search_op:
|
||||
# Set dynamic axes for `input_ids` when using beam search op to {0: "batch_size"} only
|
||||
del dynamic_axes["input_ids"][1]
|
||||
return dynamic_axes
|
||||
|
||||
def inputs(self, use_fp16_inputs: bool, use_int32_inputs: bool, return_dict: bool = False):
|
||||
inputs = get_sample_decoder_inputs(
|
||||
self.config,
|
||||
self.device,
|
||||
batch_size=2,
|
||||
past_sequence_length=(0 if self.first_pass else 6),
|
||||
sequence_length=(6 if self.first_pass else 1),
|
||||
use_fp16=use_fp16_inputs,
|
||||
use_int32=use_int32_inputs,
|
||||
)
|
||||
if return_dict:
|
||||
if self.first_pass:
|
||||
del inputs["past_key_values"]
|
||||
return inputs
|
||||
|
||||
if self.first_pass:
|
||||
return (
|
||||
inputs["decoder_input_ids"],
|
||||
inputs["encoder_hidden_states"],
|
||||
)
|
||||
return (
|
||||
inputs["decoder_input_ids"],
|
||||
inputs["encoder_hidden_states"],
|
||||
inputs["past_key_values"],
|
||||
)
|
||||
|
||||
def fix_key_value_cache_dims(self, io: ValueInfoProto, is_cross: bool = False, is_output: bool = False):
|
||||
# Shape should be (batch_size, num_heads, sequence_length, head_size) for self attention KV caches
|
||||
# and (batch_size, num_heads, num_frames // 2, head_size) for cross attention KV caches
|
||||
num_heads = io.type.tensor_type.shape.dim[1]
|
||||
if "_dim_" in num_heads.dim_param:
|
||||
num_heads.Clear()
|
||||
num_heads.dim_value = self.num_heads
|
||||
sequence_length = io.type.tensor_type.shape.dim[2]
|
||||
if "_dim_" in sequence_length.dim_param:
|
||||
sequence_length.Clear()
|
||||
if is_cross:
|
||||
sequence_length.dim_value = self.max_source_positions
|
||||
else:
|
||||
sequence_length.dim_param = "total_sequence_length" if is_output else "past_sequence_length"
|
||||
head_size = io.type.tensor_type.shape.dim[3]
|
||||
if "_dim_" in head_size.dim_param:
|
||||
head_size.Clear()
|
||||
head_size.dim_value = self.head_size
|
||||
return io
|
||||
|
||||
def fix_io(self, io_list: RepeatedCompositeFieldContainer, is_output: bool = False):
|
||||
# Fix order of inputs/outputs and each dim_value of input/output
|
||||
reordered_io = []
|
||||
self_attn_kv_caches = []
|
||||
cross_attn_kv_caches = []
|
||||
|
||||
for io in io_list:
|
||||
if "past" not in io.name and "present" not in io.name:
|
||||
reordered_io.append(io)
|
||||
elif "self" in io.name:
|
||||
# Self attention KV caches
|
||||
new_io = self.fix_key_value_cache_dims(io, is_cross=False, is_output=is_output)
|
||||
if self.no_beam_search_op:
|
||||
reordered_io.append(new_io)
|
||||
else:
|
||||
self_attn_kv_caches.append(new_io)
|
||||
else:
|
||||
# Cross attention KV caches
|
||||
new_io = self.fix_key_value_cache_dims(io, is_cross=True, is_output=is_output)
|
||||
if self.no_beam_search_op:
|
||||
reordered_io.append(new_io)
|
||||
else:
|
||||
cross_attn_kv_caches.append(new_io)
|
||||
|
||||
if not self.no_beam_search_op:
|
||||
reordered_io += self_attn_kv_caches + cross_attn_kv_caches
|
||||
return reordered_io
|
||||
|
||||
def fix_inputs_and_outputs(self, model: ModelProto):
|
||||
# ONNX exporter might mark dimensions like 'Transposepresent_value_self_1_dim_2' in shape inference.
|
||||
# We now change the dim_values to the correct one.
|
||||
reordered_inputs = self.fix_io(model.graph.input, is_output=False)
|
||||
while len(model.graph.input) > 0:
|
||||
model.graph.input.pop()
|
||||
model.graph.input.extend(reordered_inputs)
|
||||
|
||||
reordered_outputs = self.fix_io(model.graph.output, is_output=True)
|
||||
while len(model.graph.output) > 0:
|
||||
model.graph.output.pop()
|
||||
model.graph.output.extend(reordered_outputs)
|
||||
return model
|
||||
|
||||
def fix_layernorm_weights(self, model: ModelProto, use_fp16_inputs: bool):
|
||||
if self.model_impl == "openai" and use_fp16_inputs:
|
||||
# Cast ONNX model to float16 to ensure LayerNorm weights are converted from
|
||||
# float32 to float16 since exported model already has float16 weights everywhere
|
||||
# except for LayerNorm ops. This happens because OpenAI always upcasts to float32
|
||||
# when computing LayerNorm.
|
||||
#
|
||||
# Reference:
|
||||
# https://github.com/openai/whisper/blob/90db0de1896c23cbfaf0c58bc2d30665f709f170/whisper/model.py#L41
|
||||
model = convert_float_to_float16(model)
|
||||
return model
|
||||
|
||||
def export_onnx(
|
||||
self,
|
||||
onnx_model_path: str,
|
||||
provider: str,
|
||||
verbose: bool = True,
|
||||
use_external_data_format: bool = False,
|
||||
use_fp16_inputs: bool = False,
|
||||
use_int32_inputs: bool = True,
|
||||
use_encoder_hidden_states: bool = False,
|
||||
use_kv_cache_inputs: bool = True,
|
||||
):
|
||||
"""Export decoder to ONNX
|
||||
|
||||
Args:
|
||||
onnx_model_path (str): path to save ONNX model
|
||||
provider (str): provider to use for verifying parity on ONNX model
|
||||
verbose (bool, optional): print verbose information. Defaults to True.
|
||||
use_external_data_format (bool, optional): use external data format or not. Defaults to False.
|
||||
use_fp16_inputs (bool, optional): use float16 inputs for the KV caches. Defaults to False.
|
||||
use_int32_inputs (bool, optional): use int32 inputs for the decoder_input_ids. Defaults to True.
|
||||
use_encoder_hidden_states (bool, optional): use encoder_hidden_states as model input for decoder-init/decoder-without-past models. Defaults to False.
|
||||
use_kv_cache_inputs (bool, optional): use KV caches as model inputs for decoder-with-past models. Defaults to True.
|
||||
"""
|
||||
# Shape of decoder's tensors:
|
||||
# Required Inputs:
|
||||
# decoder_input_ids: (batch_size, sequence_length)
|
||||
# Optional Inputs:
|
||||
# encoder_hidden_states (comes from encoder's outputs): (batch_size, num_frames // 2, hidden_size)
|
||||
# past_{key/value}_self_* (past self attention KV caches): (batch_size, num_heads, past_sequence_length, head_size)
|
||||
# past_{key/value}_cross_* (past cross attention KV caches): (batch_size, num_heads, num_frames // 2, head_size)
|
||||
# Outputs:
|
||||
# logits: (batch_size, sequence_length, vocab_size)
|
||||
# present_{key/value}_self_* (present self attention KV caches): (batch_size, num_heads, past_sequence_length + sequence_length, head_size)
|
||||
# present_{key/value}_cross_* (present cross attention KV caches): (batch_size, num_heads, num_frames // 2, head_size)
|
||||
|
||||
# For the first pass through the decoder (i.e. decoder-init/decoder-without-past)
|
||||
self.first_pass = use_encoder_hidden_states and not use_kv_cache_inputs
|
||||
|
||||
# For subsequent passes through the decoder (i.e. decoder-with-past)
|
||||
self.later_pass = not use_encoder_hidden_states and use_kv_cache_inputs
|
||||
|
||||
assert self.first_pass or self.later_pass, (
|
||||
"Only one of `use_encoder_hidden_states` and `use_kv_cache_inputs` can be true at once."
|
||||
)
|
||||
|
||||
inputs = self.inputs(use_fp16_inputs=use_fp16_inputs, use_int32_inputs=use_int32_inputs)
|
||||
input_names = self.input_names()
|
||||
output_names = self.output_names()
|
||||
dynamic_axes = self.dynamic_axes(input_names, output_names)
|
||||
|
||||
Path(onnx_model_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory() as tmp_dir_name:
|
||||
temp_onnx_model_path = os.path.join(tmp_dir_name, "decoder.onnx")
|
||||
Path(temp_onnx_model_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path = temp_onnx_model_path if use_external_data_format else onnx_model_path
|
||||
|
||||
torch.onnx.export(
|
||||
self,
|
||||
args=inputs,
|
||||
f=out_path,
|
||||
export_params=True,
|
||||
input_names=input_names,
|
||||
output_names=output_names,
|
||||
dynamic_axes=dynamic_axes,
|
||||
opset_version=17,
|
||||
do_constant_folding=True,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
model = onnx.load_model(out_path, load_external_data=use_external_data_format)
|
||||
model = self.fix_inputs_and_outputs(model)
|
||||
model = self.fix_layernorm_weights(model, use_fp16_inputs)
|
||||
OnnxModel.save(
|
||||
model,
|
||||
onnx_model_path,
|
||||
save_as_external_data=use_external_data_format,
|
||||
all_tensors_to_one_file=True,
|
||||
)
|
||||
|
||||
self.verify_onnx(onnx_model_path, provider, use_fp16_inputs, use_int32_inputs)
|
||||
|
||||
def verify_onnx(
|
||||
self,
|
||||
onnx_model_path: str,
|
||||
provider: str,
|
||||
use_fp16_inputs: bool,
|
||||
use_int32_inputs: bool,
|
||||
):
|
||||
"""Verify ONNX model outputs and PyTorch model outputs match
|
||||
|
||||
Args:
|
||||
onnx_model_path (str): path to save ONNX model
|
||||
provider (str): execution provider for ONNX model
|
||||
use_fp16_inputs (bool, optional): use float16 inputs for the KV caches
|
||||
use_int32_inputs (bool, optional): use int32 inputs for the decoder_input_ids
|
||||
"""
|
||||
# Shape of decoder's tensors:
|
||||
# Required Inputs:
|
||||
# decoder_input_ids: (batch_size, sequence_length)
|
||||
# Optional Inputs:
|
||||
# encoder_hidden_states (comes from encoder's outputs): (batch_size, num_frames // 2, hidden_size)
|
||||
# past_{key/value}_self_* (past self attention KV caches): (batch_size, num_heads, past_sequence_length, head_size)
|
||||
# past_{key/value}_cross_* (past cross attention KV caches): (batch_size, num_heads, num_frames // 2, head_size)
|
||||
# Outputs:
|
||||
# logits: (batch_size, sequence_length, vocab_size)
|
||||
# present_{key/value}_self_* (present self attention KV caches): (batch_size, num_heads, past_sequence_length + sequence_length, head_size)
|
||||
# present_{key/value}_cross_* (present cross attention KV caches): (batch_size, num_heads, num_frames // 2, head_size)
|
||||
|
||||
# Run PyTorch model
|
||||
inputs = self.inputs(use_fp16_inputs=use_fp16_inputs, use_int32_inputs=use_int32_inputs, return_dict=True)
|
||||
pt_outputs = []
|
||||
if self.first_pass:
|
||||
out = self.forward(**inputs)
|
||||
pt_outputs.append(out[0].detach().cpu().numpy())
|
||||
for present_key_value_layer in out[1]:
|
||||
for present_key_value in present_key_value_layer:
|
||||
pt_outputs.append(present_key_value.detach().cpu().numpy())
|
||||
else:
|
||||
out = self.forward(**inputs)
|
||||
pt_outputs.append(out[0].detach().cpu().numpy())
|
||||
for present_self_key_value in out[1]:
|
||||
pt_outputs.append(present_self_key_value.detach().cpu().numpy())
|
||||
|
||||
# Run ONNX model
|
||||
sess = InferenceSession(onnx_model_path, providers=[provider])
|
||||
ort_outputs = sess.run(None, convert_inputs_for_ort(inputs, sess))
|
||||
|
||||
# Calculate output difference
|
||||
try:
|
||||
for i, output_name in enumerate(self.output_names()):
|
||||
diff = np.abs(pt_outputs[i] - ort_outputs[i])
|
||||
logger.warning(f"Comparing {output_name}...")
|
||||
logger.warning(f"Max diff: {np.max(diff)}")
|
||||
except: # noqa: E722
|
||||
pass
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
# -------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import onnx
|
||||
import torch
|
||||
from float16 import convert_float_to_float16
|
||||
from onnx import ModelProto
|
||||
from onnx_model import OnnxModel
|
||||
from transformers import WhisperConfig
|
||||
from whisper_inputs import get_model_dynamic_axes, get_sample_encoder_inputs
|
||||
|
||||
from onnxruntime import InferenceSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WhisperEncoder(torch.nn.Module):
|
||||
"""Whisper encoder component"""
|
||||
|
||||
def __init__(self, config: WhisperConfig, model: torch.nn.Module, model_impl: str):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.device = model.device
|
||||
self.model_impl = model_impl
|
||||
|
||||
self.encoder = model.encoder if model_impl == "openai" else model.model.encoder
|
||||
|
||||
def forward(self, audio_features: torch.Tensor):
|
||||
outputs = self.encoder(audio_features)
|
||||
return outputs if self.model_impl == "openai" else outputs.last_hidden_state
|
||||
|
||||
def input_names(self):
|
||||
input_names = ["audio_features"]
|
||||
return input_names
|
||||
|
||||
def output_names(self):
|
||||
output_names = ["encoder_hidden_states"]
|
||||
return output_names
|
||||
|
||||
def dynamic_axes(self, input_names, output_names):
|
||||
dynamic_axes = get_model_dynamic_axes(self.config, input_names, output_names)
|
||||
return dynamic_axes
|
||||
|
||||
def fix_layernorm_weights(self, model: ModelProto, use_fp16_inputs: bool):
|
||||
if self.model_impl == "openai" and use_fp16_inputs:
|
||||
# Cast ONNX model to float16 to ensure LayerNorm weights are converted from
|
||||
# float32 to float16 since exported model already has float16 weights everywhere
|
||||
# except for LayerNorm ops. This happens because OpenAI always upcasts to float32
|
||||
# when computing LayerNorm.
|
||||
#
|
||||
# Reference:
|
||||
# https://github.com/openai/whisper/blob/90db0de1896c23cbfaf0c58bc2d30665f709f170/whisper/model.py#L41
|
||||
model = convert_float_to_float16(model)
|
||||
return model
|
||||
|
||||
def export_onnx(
|
||||
self,
|
||||
onnx_model_path: str,
|
||||
provider: str,
|
||||
verbose: bool = True,
|
||||
use_external_data_format: bool = False,
|
||||
use_fp16_inputs: bool = False,
|
||||
):
|
||||
"""Export encoder to ONNX
|
||||
|
||||
Args:
|
||||
onnx_model_path (str): path to save ONNX model
|
||||
provider (str): provider to use for verifying parity on ONNX model
|
||||
verbose (bool, optional): print verbose information. Defaults to True.
|
||||
use_external_data_format (bool, optional): use external data format or not. Defaults to False.
|
||||
use_fp16_inputs (bool, optional): use float16 inputs for the audio_features. Defaults to False.
|
||||
"""
|
||||
# Shape of encoder's tensors:
|
||||
# Inputs:
|
||||
# audio_features: (batch_size, num_mels, num_frames)
|
||||
# Outputs:
|
||||
# encoder_hidden_states: (batch_size, num_frames // 2, hidden_size)
|
||||
|
||||
inputs = get_sample_encoder_inputs(
|
||||
self.config,
|
||||
self.device,
|
||||
batch_size=2,
|
||||
use_fp16=use_fp16_inputs,
|
||||
)
|
||||
|
||||
input_names = self.input_names()
|
||||
output_names = self.output_names()
|
||||
dynamic_axes = self.dynamic_axes(input_names, output_names)
|
||||
|
||||
Path(onnx_model_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory() as tmp_dir_name:
|
||||
temp_onnx_model_path = os.path.join(tmp_dir_name, "encoder.onnx")
|
||||
Path(temp_onnx_model_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path = temp_onnx_model_path if use_external_data_format else onnx_model_path
|
||||
|
||||
torch.onnx.export(
|
||||
self,
|
||||
args=(inputs["audio_features"]),
|
||||
f=out_path,
|
||||
export_params=True,
|
||||
input_names=input_names,
|
||||
output_names=output_names,
|
||||
dynamic_axes=dynamic_axes,
|
||||
opset_version=17,
|
||||
do_constant_folding=True,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
model = onnx.load_model(out_path, load_external_data=use_external_data_format)
|
||||
model = self.fix_layernorm_weights(model, use_fp16_inputs)
|
||||
OnnxModel.save(
|
||||
model,
|
||||
onnx_model_path,
|
||||
save_as_external_data=use_external_data_format,
|
||||
all_tensors_to_one_file=True,
|
||||
)
|
||||
|
||||
self.verify_onnx(onnx_model_path, provider, use_fp16_inputs)
|
||||
|
||||
def verify_onnx(
|
||||
self,
|
||||
onnx_model_path: str,
|
||||
provider: str,
|
||||
use_fp16_inputs: bool,
|
||||
):
|
||||
"""Verify ONNX model outputs and PyTorch model outputs match
|
||||
|
||||
Args:
|
||||
onnx_model_path (str): path to save ONNX model
|
||||
provider (str): execution provider for ONNX model
|
||||
use_fp16_inputs (bool, optional): use float16 inputs for the audio_features
|
||||
"""
|
||||
# Shape of encoder's tensors:
|
||||
# Inputs:
|
||||
# audio_features: (batch_size, num_mels, num_frames)
|
||||
# Outputs:
|
||||
# encoder_hidden_states: (batch_size, num_frames // 2, hidden_size)
|
||||
inputs = get_sample_encoder_inputs(
|
||||
self.config,
|
||||
self.device,
|
||||
batch_size=2,
|
||||
use_fp16=use_fp16_inputs,
|
||||
)
|
||||
|
||||
# Run PyTorch model
|
||||
pt_outputs = self.forward(inputs["audio_features"]).detach().cpu().numpy()
|
||||
|
||||
# Run ONNX model
|
||||
sess = InferenceSession(onnx_model_path, providers=[provider])
|
||||
ort_outputs = sess.run(None, {"audio_features": inputs["audio_features"].detach().cpu().numpy()})[0]
|
||||
|
||||
# Calculate output difference
|
||||
diff = np.abs(pt_outputs - ort_outputs)
|
||||
logger.warning("Comparing encoder_hidden_states...")
|
||||
logger.warning(f"Max diff: {np.max(diff)}")
|
||||
|
|
@ -0,0 +1,371 @@
|
|||
# -------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import onnx
|
||||
import torch
|
||||
from float16 import convert_float_to_float16
|
||||
from onnx import ModelProto, ValueInfoProto
|
||||
from onnx_model import OnnxModel
|
||||
from transformers import WhisperConfig
|
||||
from whisper_decoder import WhisperDecoder
|
||||
from whisper_encoder import WhisperEncoder
|
||||
from whisper_inputs import (
|
||||
convert_inputs_for_ort,
|
||||
get_model_dynamic_axes,
|
||||
get_sample_encoder_decoder_init_inputs,
|
||||
group_past_key_values,
|
||||
)
|
||||
|
||||
from onnxruntime import InferenceSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WhisperEncoderDecoderInit(torch.nn.Module):
|
||||
"""Whisper encoder component + first pass through Whisper decoder component to initialize KV caches"""
|
||||
|
||||
def __init__(self, config: WhisperConfig, model: torch.nn.Module, model_impl: str, no_beam_search_op: bool = False):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.device = model.device
|
||||
self.model_impl = model_impl
|
||||
self.no_beam_search_op = no_beam_search_op
|
||||
|
||||
self.encoder = WhisperEncoder(config, model, model_impl)
|
||||
self.decoder = WhisperDecoder(config, model, model_impl, no_beam_search_op)
|
||||
|
||||
self.max_source_positions = self.config.max_source_positions
|
||||
self.num_heads = self.config.decoder_attention_heads
|
||||
self.head_size = self.config.d_model // self.num_heads
|
||||
|
||||
def hf_forward_for_beam_search_op(self, audio_features: torch.Tensor, decoder_input_ids: torch.Tensor):
|
||||
encoder_hidden_states = self.encoder(audio_features)
|
||||
logits, present_key_values = self.decoder(decoder_input_ids, encoder_hidden_states)
|
||||
return logits, encoder_hidden_states, present_key_values
|
||||
|
||||
def hf_forward_for_no_beam_search_op(self, audio_features: torch.Tensor):
|
||||
encoder_hidden_states = self.encoder(audio_features)
|
||||
|
||||
# Get cross attention KV caches and return them for this model
|
||||
# We do this because these MatMuls are only run once before their outputs are being re-used in the decoder
|
||||
present_cross_attention_key_value_caches = []
|
||||
for layer in self.decoder.decoder.layers:
|
||||
cross_attn_key_cache = (
|
||||
layer.encoder_attn.k_proj(encoder_hidden_states)
|
||||
.view(-1, self.max_source_positions, self.num_heads, self.head_size)
|
||||
.transpose(1, 2)
|
||||
)
|
||||
cross_attn_value_cache = (
|
||||
layer.encoder_attn.v_proj(encoder_hidden_states)
|
||||
.view(-1, self.max_source_positions, self.num_heads, self.head_size)
|
||||
.transpose(1, 2)
|
||||
)
|
||||
present_cross_attention_key_value_caches.append(cross_attn_key_cache)
|
||||
present_cross_attention_key_value_caches.append(cross_attn_value_cache)
|
||||
|
||||
return encoder_hidden_states, present_cross_attention_key_value_caches
|
||||
|
||||
def oai_forward_for_beam_search_op(self, audio_features: torch.Tensor, decoder_input_ids: torch.Tensor):
|
||||
encoder_hidden_states = self.encoder(audio_features)
|
||||
logits, present_key_values = self.decoder(decoder_input_ids, encoder_hidden_states)
|
||||
return logits, encoder_hidden_states, present_key_values
|
||||
|
||||
def oai_forward_for_no_beam_search_op(self, audio_features: torch.Tensor):
|
||||
encoder_hidden_states = self.encoder(audio_features)
|
||||
|
||||
# Get cross attention KV caches and return them for this model
|
||||
# We do this because these MatMuls are only run once before their outputs are being re-used in the decoder
|
||||
present_cross_attention_key_value_caches = []
|
||||
for block in self.decoder.model.decoder.blocks:
|
||||
cross_attn_key_cache = (
|
||||
block.cross_attn.key(encoder_hidden_states)
|
||||
.view(-1, self.max_source_positions, self.num_heads, self.head_size)
|
||||
.transpose(1, 2)
|
||||
)
|
||||
cross_attn_value_cache = (
|
||||
block.cross_attn.value(encoder_hidden_states)
|
||||
.view(-1, self.max_source_positions, self.num_heads, self.head_size)
|
||||
.transpose(1, 2)
|
||||
)
|
||||
present_cross_attention_key_value_caches.append(cross_attn_key_cache)
|
||||
present_cross_attention_key_value_caches.append(cross_attn_value_cache)
|
||||
|
||||
return encoder_hidden_states, present_cross_attention_key_value_caches
|
||||
|
||||
def forward(self, audio_features: torch.Tensor, decoder_input_ids: torch.Tensor | None = None):
|
||||
if self.model_impl == "openai":
|
||||
if self.no_beam_search_op:
|
||||
return self.oai_forward_for_no_beam_search_op(audio_features)
|
||||
return self.oai_forward_for_beam_search_op(audio_features, decoder_input_ids)
|
||||
|
||||
# Hugging Face implementation
|
||||
if self.no_beam_search_op:
|
||||
return self.hf_forward_for_no_beam_search_op(audio_features)
|
||||
return self.hf_forward_for_beam_search_op(audio_features, decoder_input_ids)
|
||||
|
||||
def input_names(self):
|
||||
if self.no_beam_search_op:
|
||||
input_names = ["audio_features"]
|
||||
else:
|
||||
input_names = ["encoder_input_ids", "decoder_input_ids"]
|
||||
return input_names
|
||||
|
||||
def output_names(self):
|
||||
if self.no_beam_search_op:
|
||||
output_names = [
|
||||
"encoder_hidden_states",
|
||||
*list(
|
||||
chain.from_iterable(
|
||||
(f"present_key_cross_{i}", f"present_value_cross_{i}")
|
||||
for i in range(self.config.decoder_layers)
|
||||
)
|
||||
),
|
||||
]
|
||||
else:
|
||||
output_names = [
|
||||
"logits",
|
||||
"encoder_hidden_states",
|
||||
*list(
|
||||
chain.from_iterable(
|
||||
(
|
||||
f"present_key_self_{i}",
|
||||
f"present_value_self_{i}",
|
||||
f"present_key_cross_{i}",
|
||||
f"present_value_cross_{i}",
|
||||
)
|
||||
for i in range(self.config.decoder_layers)
|
||||
)
|
||||
),
|
||||
]
|
||||
return output_names
|
||||
|
||||
def dynamic_axes(self, input_names, output_names):
|
||||
dynamic_axes = get_model_dynamic_axes(self.config, input_names, output_names)
|
||||
return dynamic_axes
|
||||
|
||||
def inputs(self, use_fp16_inputs: bool, use_int32_inputs: bool, return_dict: bool = False):
|
||||
inputs = get_sample_encoder_decoder_init_inputs(
|
||||
self.config,
|
||||
self.device,
|
||||
batch_size=2,
|
||||
decoder_sequence_length=6,
|
||||
use_fp16=use_fp16_inputs,
|
||||
use_int32=use_int32_inputs,
|
||||
)
|
||||
if return_dict:
|
||||
if self.no_beam_search_op:
|
||||
del inputs["decoder_input_ids"]
|
||||
return inputs
|
||||
|
||||
if self.no_beam_search_op:
|
||||
return (inputs["audio_features"],)
|
||||
return (
|
||||
inputs["audio_features"],
|
||||
inputs["decoder_input_ids"],
|
||||
)
|
||||
|
||||
def fix_key_value_cache_dims(self, output: ValueInfoProto, is_cross: bool = False):
|
||||
# Shape should be (batch_size, num_heads, sequence_length, head_size) for self attention KV caches
|
||||
# and (batch_size, num_heads, num_frames // 2, head_size) for cross attention KV caches
|
||||
num_heads = output.type.tensor_type.shape.dim[1]
|
||||
if "_dim_" in num_heads.dim_param:
|
||||
num_heads.Clear()
|
||||
num_heads.dim_value = self.num_heads
|
||||
sequence_length = output.type.tensor_type.shape.dim[2]
|
||||
if "_dim_" in sequence_length.dim_param:
|
||||
sequence_length.Clear()
|
||||
if is_cross:
|
||||
sequence_length.dim_value = self.max_source_positions
|
||||
else:
|
||||
sequence_length.dim_param = "total_sequence_length"
|
||||
head_size = output.type.tensor_type.shape.dim[3]
|
||||
if "_dim_" in head_size.dim_param:
|
||||
head_size.Clear()
|
||||
head_size.dim_value = self.head_size
|
||||
return output
|
||||
|
||||
def fix_outputs(self, model: ModelProto):
|
||||
# ONNX exporter might mark dimensions like 'Transposepresent_value_self_1_dim_2' in shape inference.
|
||||
# We now change the dim_values to the correct one.
|
||||
reordered_outputs = []
|
||||
self_attn_kv_caches = []
|
||||
cross_attn_kv_caches = []
|
||||
|
||||
for output in model.graph.output:
|
||||
if "present" not in output.name:
|
||||
reordered_outputs.append(output)
|
||||
|
||||
elif "self" in output.name:
|
||||
# Self attention KV caches
|
||||
new_output = self.fix_key_value_cache_dims(output, is_cross=False)
|
||||
if self.no_beam_search_op:
|
||||
reordered_outputs.append(new_output)
|
||||
else:
|
||||
self_attn_kv_caches.append(new_output)
|
||||
else:
|
||||
# Cross attention KV caches
|
||||
new_output = self.fix_key_value_cache_dims(output, is_cross=True)
|
||||
if self.no_beam_search_op:
|
||||
reordered_outputs.append(new_output)
|
||||
else:
|
||||
cross_attn_kv_caches.append(new_output)
|
||||
|
||||
if not self.no_beam_search_op:
|
||||
reordered_outputs += self_attn_kv_caches + cross_attn_kv_caches
|
||||
|
||||
while len(model.graph.output) > 0:
|
||||
model.graph.output.pop()
|
||||
model.graph.output.extend(reordered_outputs)
|
||||
return model
|
||||
|
||||
def fix_layernorm_weights(self, model: ModelProto, use_fp16_inputs: bool):
|
||||
if self.model_impl == "openai" and use_fp16_inputs:
|
||||
# Cast ONNX model to float16 to ensure LayerNorm weights are converted from
|
||||
# float32 to float16 since exported model already has float16 weights everywhere
|
||||
# except for LayerNorm ops. This happens because OpenAI always upcasts to float32
|
||||
# when computing LayerNorm.
|
||||
#
|
||||
# Reference:
|
||||
# https://github.com/openai/whisper/blob/90db0de1896c23cbfaf0c58bc2d30665f709f170/whisper/model.py#L41
|
||||
model = convert_float_to_float16(model)
|
||||
return model
|
||||
|
||||
def export_onnx(
|
||||
self,
|
||||
onnx_model_path: str,
|
||||
provider: str,
|
||||
verbose: bool = True,
|
||||
use_external_data_format: bool = False,
|
||||
use_fp16_inputs: bool = False,
|
||||
use_int32_inputs: bool = True,
|
||||
):
|
||||
"""Export encoder-decoder-init to ONNX
|
||||
|
||||
Args:
|
||||
onnx_model_path (str): path to save ONNX model
|
||||
provider (str): provider to use for verifying parity on ONNX model
|
||||
verbose (bool, optional): print verbose information. Defaults to True.
|
||||
use_external_data_format (bool, optional): use external data format or not. Defaults to False.
|
||||
use_fp16_inputs (bool, optional): use float16 inputs for the audio_features. Defaults to False.
|
||||
use_int32_inputs (bool, optional): use int32 inputs for the decoder_input_ids. Defaults to True.
|
||||
"""
|
||||
# Shape of encoder's tensors:
|
||||
# Inputs:
|
||||
# audio_features: (batch_size, num_mels, num_frames)
|
||||
# Outputs:
|
||||
# encoder_hidden_states: (batch_size, num_frames // 2, hidden_size)
|
||||
|
||||
# Shape of decoder's tensors:
|
||||
# Inputs:
|
||||
# decoder_input_ids: (batch_size, sequence_length)
|
||||
# encoder_hidden_states (comes from encoder's outputs): (batch_size, num_frames // 2, hidden_size)
|
||||
# Outputs:
|
||||
# logits: (batch_size, sequence_length, vocab_size)
|
||||
# present_{key/value}_self_* (present self attention KV caches): (batch_size, num_heads, past_sequence_length + sequence_length, head_size)
|
||||
# present_{key/value}_cross_* (present cross attention KV caches): (batch_size, num_heads, num_frames // 2, head_size)
|
||||
|
||||
inputs = self.inputs(use_fp16_inputs=use_fp16_inputs, use_int32_inputs=use_int32_inputs)
|
||||
input_names = self.input_names()
|
||||
output_names = self.output_names()
|
||||
dynamic_axes = self.dynamic_axes(input_names, output_names)
|
||||
|
||||
Path(onnx_model_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory() as tmp_dir_name:
|
||||
temp_onnx_model_path = os.path.join(tmp_dir_name, "encoder_decoder_init.onnx")
|
||||
Path(temp_onnx_model_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path = temp_onnx_model_path if use_external_data_format else onnx_model_path
|
||||
|
||||
torch.onnx.export(
|
||||
self,
|
||||
args=inputs,
|
||||
f=out_path,
|
||||
export_params=True,
|
||||
input_names=input_names,
|
||||
output_names=output_names,
|
||||
dynamic_axes=dynamic_axes,
|
||||
opset_version=17,
|
||||
do_constant_folding=True,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
model = onnx.load_model(out_path, load_external_data=use_external_data_format)
|
||||
model = self.fix_outputs(model)
|
||||
model = self.fix_layernorm_weights(model, use_fp16_inputs)
|
||||
OnnxModel.save(
|
||||
model,
|
||||
onnx_model_path,
|
||||
save_as_external_data=use_external_data_format,
|
||||
all_tensors_to_one_file=True,
|
||||
)
|
||||
|
||||
self.verify_onnx(onnx_model_path, provider, use_fp16_inputs, use_int32_inputs)
|
||||
|
||||
def verify_onnx(
|
||||
self,
|
||||
onnx_model_path: str,
|
||||
provider: str,
|
||||
use_fp16_inputs: bool,
|
||||
use_int32_inputs: bool,
|
||||
):
|
||||
"""Verify ONNX model outputs and PyTorch model outputs match
|
||||
|
||||
Args:
|
||||
onnx_model_path (str): path to save ONNX model
|
||||
provider (str): execution provider for ONNX model
|
||||
use_fp16_inputs (bool, optional): use float16 inputs for the audio_features
|
||||
use_int32_inputs (bool, optional): use int32 inputs for the decoder_input_ids
|
||||
"""
|
||||
# Shape of encoder's tensors:
|
||||
# Inputs:
|
||||
# audio_features: (batch_size, num_mels, num_frames)
|
||||
# Outputs:
|
||||
# encoder_hidden_states: (batch_size, num_frames // 2, hidden_size)
|
||||
|
||||
# Shape of decoder's tensors:
|
||||
# Inputs:
|
||||
# decoder_input_ids: (batch_size, sequence_length)
|
||||
# encoder_hidden_states (comes from encoder's outputs): (batch_size, num_frames // 2, hidden_size)
|
||||
# Outputs:
|
||||
# logits: (batch_size, sequence_length, vocab_size)
|
||||
# present_{key/value}_self_* (present self attention KV caches): (batch_size, num_heads, past_sequence_length + sequence_length, head_size)
|
||||
# present_{key/value}_cross_* (present cross attention KV caches): (batch_size, num_heads, num_frames // 2, head_size)
|
||||
|
||||
inputs = self.inputs(use_fp16_inputs=use_fp16_inputs, use_int32_inputs=use_int32_inputs, return_dict=True)
|
||||
|
||||
# Run PyTorch model
|
||||
pt_outputs = []
|
||||
if self.no_beam_search_op:
|
||||
out = self.forward(**inputs)
|
||||
pt_outputs.append(out[0].detach().cpu().numpy())
|
||||
for present_cross_attn_cache in out[1]:
|
||||
pt_outputs.append(present_cross_attn_cache.detach().cpu().numpy())
|
||||
else:
|
||||
out = self.forward(**inputs)
|
||||
pt_outputs.append(out[0].detach().cpu().numpy())
|
||||
pt_outputs.append(out[1].detach().cpu().numpy())
|
||||
|
||||
(self_attn_kv_caches, cross_attn_kv_caches) = group_past_key_values(out[2])
|
||||
pt_outputs.extend([self_attn_kv_cache.detach().cpu().numpy() for self_attn_kv_cache in self_attn_kv_caches])
|
||||
pt_outputs.extend(
|
||||
[cross_attn_kv_cache.detach().cpu().numpy() for cross_attn_kv_cache in cross_attn_kv_caches]
|
||||
)
|
||||
|
||||
# Run ONNX model
|
||||
sess = InferenceSession(onnx_model_path, providers=[provider])
|
||||
ort_outputs = sess.run(None, convert_inputs_for_ort(inputs, sess))
|
||||
|
||||
# Calculate output difference
|
||||
for i, output_name in enumerate(self.output_names()):
|
||||
diff = np.abs(pt_outputs[i] - ort_outputs[i])
|
||||
logger.warning(f"Comparing {output_name}...")
|
||||
logger.warning(f"Max diff: {np.max(diff)}")
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,380 @@
|
|||
# -------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from transformers import WhisperConfig
|
||||
|
||||
from onnxruntime import InferenceSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Create audio_features for encoder
|
||||
# Shape is (batch_size, feature_size, sequence_length) = (batch_size, num_mel_filters, num_frames)
|
||||
# where num_mel_filters is a model attribute and num_frames = (chunk_length * sample_rate) // hop_length.
|
||||
#
|
||||
# Hard-coded audio hyperparameters:
|
||||
# SAMPLE_RATE = 16000
|
||||
# N_FFT = 400
|
||||
# HOP_LENGTH = 160
|
||||
# CHUNK_LENGTH = 30 (i.e. 30-second chunk of audio)
|
||||
# N_SAMPLES = CHUNK_LENGTH * SAMPLE_RATE = 30 * 16000 = 480000 (i.e. 480,000 samples in a 30-second chunk of audio)
|
||||
# N_FRAMES = N_SAMPLES // HOP_LENGTH = 480000 // 160 = 3000 (i.e. 3000 frames in a mel spectrogram input)
|
||||
#
|
||||
# N_SAMPLES_PER_TOKEN = HOP_LENGTH * 2 = 160 * 2 = 320
|
||||
# FRAMES_PER_TOKEN = SAMPLE_RATE // HOP_LENGTH = 16000 // 160 = 100 (i.e. 10 ms per audio frame)
|
||||
# TOKENS_PER_SECOND = SAMPLE_RATE // N_SAMPLES_PER_TOKEN = 16000 // 320 = 50 (i.e. 20 ms per audio token)
|
||||
def get_sample_audio_features(
|
||||
config: WhisperConfig,
|
||||
device: torch.device,
|
||||
batch_size: int,
|
||||
sequence_length: int = 3000,
|
||||
use_fp16: bool = False,
|
||||
):
|
||||
torch_dtype = torch.float16 if use_fp16 else torch.float32
|
||||
audio_features = torch.randn(batch_size, config.num_mel_bins, sequence_length, device=device, dtype=torch_dtype)
|
||||
return audio_features
|
||||
|
||||
|
||||
# Create input_ids for decoder
|
||||
# Shape is (batch_size, sequence_length) where sequence_length is the initial decoder sequence length
|
||||
def get_sample_decoder_input_ids(
|
||||
config: WhisperConfig,
|
||||
device: torch.device,
|
||||
batch_size: int,
|
||||
sequence_length: int,
|
||||
use_int32: bool = True,
|
||||
):
|
||||
torch_dtype = torch.int32 if use_int32 else torch.int64
|
||||
decoder_input_ids = torch.randint(
|
||||
low=0, high=config.vocab_size, size=(batch_size, sequence_length), device=device, dtype=torch_dtype
|
||||
)
|
||||
return decoder_input_ids
|
||||
|
||||
|
||||
# Create encoder_hidden_states for decoder-init
|
||||
# Shape is (batch_size, num_frames // 2, hidden_size)
|
||||
def get_sample_encoder_hidden_states(
|
||||
config: WhisperConfig,
|
||||
device: torch.device,
|
||||
batch_size: int,
|
||||
use_fp16: bool = False,
|
||||
):
|
||||
torch_dtype = torch.float16 if use_fp16 else torch.float32
|
||||
encoder_hidden_states = torch.randn(
|
||||
batch_size, config.max_source_positions, config.d_model, device=device, dtype=torch_dtype
|
||||
)
|
||||
return encoder_hidden_states
|
||||
|
||||
|
||||
# Create past_key_values
|
||||
# Self-attention KV caches are of shape (batch_size, num_heads, past_sequence_length, head_size)
|
||||
# Cross-attention KV caches are of shape (batch_size, num_heads, num_frames // 2, head_size)
|
||||
def get_sample_past_key_values(
|
||||
config: WhisperConfig,
|
||||
device: torch.device,
|
||||
batch_size: int,
|
||||
past_seq_len: int,
|
||||
use_fp16: bool = False,
|
||||
):
|
||||
num_heads = config.decoder_attention_heads
|
||||
head_size = config.d_model // num_heads
|
||||
max_source_positions = (
|
||||
config.max_source_positions
|
||||
) # equal to num_frames // 2 = encoder's sequence_length // 2 = 3000 // 2 = 1500
|
||||
torch_dtype = torch.float16 if use_fp16 else torch.float32
|
||||
self_attention_kv_caches = [
|
||||
(
|
||||
torch.rand(batch_size, num_heads, past_seq_len, head_size, device=device, dtype=torch_dtype),
|
||||
torch.rand(batch_size, num_heads, past_seq_len, head_size, device=device, dtype=torch_dtype),
|
||||
)
|
||||
for _ in range(config.decoder_layers)
|
||||
]
|
||||
cross_attention_kv_caches = [
|
||||
(
|
||||
torch.rand(batch_size, num_heads, max_source_positions, head_size, device=device, dtype=torch_dtype),
|
||||
torch.rand(batch_size, num_heads, max_source_positions, head_size, device=device, dtype=torch_dtype),
|
||||
)
|
||||
for _ in range(config.decoder_layers)
|
||||
]
|
||||
return flatten_past_key_values(self_attention_kv_caches, cross_attention_kv_caches)
|
||||
|
||||
|
||||
# Flatten KV caches into pairs-of-4 where each pair is defined as:
|
||||
# (self_attn_key_cache, self_attn_value_cache, cross_attn_key_cache, cross_attn_value_cache)
|
||||
def flatten_past_key_values(
|
||||
self_attn_kv_caches: list[tuple[torch.Tensor, torch.Tensor]],
|
||||
cross_attn_kv_caches: list[tuple[torch.Tensor, torch.Tensor]],
|
||||
):
|
||||
past_key_values = []
|
||||
for (self_k_cache, self_v_cache), (cross_k_cache, cross_v_cache) in zip(
|
||||
self_attn_kv_caches, cross_attn_kv_caches, strict=False
|
||||
):
|
||||
layer_kv_caches = (self_k_cache, self_v_cache, cross_k_cache, cross_v_cache)
|
||||
past_key_values.append(layer_kv_caches)
|
||||
return past_key_values
|
||||
|
||||
|
||||
# Group KV caches into two 1D lists where one list contains the self attention KV caches and
|
||||
# one list contains the cross attention KV caches
|
||||
def group_past_key_values(
|
||||
kv_caches: list[tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]],
|
||||
):
|
||||
self_attn_kv_caches, cross_attn_kv_caches = [], []
|
||||
for self_k_cache, self_v_cache, cross_k_cache, cross_v_cache in kv_caches:
|
||||
self_attn_kv_caches.append(self_k_cache)
|
||||
self_attn_kv_caches.append(self_v_cache)
|
||||
cross_attn_kv_caches.append(cross_k_cache)
|
||||
cross_attn_kv_caches.append(cross_v_cache)
|
||||
return self_attn_kv_caches, cross_attn_kv_caches
|
||||
|
||||
|
||||
# Create alignment heads for timestamps
|
||||
# Shape is (num_alignment_heads, 2)
|
||||
def get_sample_alignment_heads(
|
||||
config: WhisperConfig,
|
||||
device: torch.device,
|
||||
num_alignment_heads: int = 6,
|
||||
use_int32: bool = True,
|
||||
):
|
||||
torch_dtype = torch.int32 if use_int32 else torch.int64
|
||||
alignment_heads = torch.ones((num_alignment_heads, 2), device=device, dtype=torch_dtype)
|
||||
return alignment_heads
|
||||
|
||||
|
||||
# Create length of start-of-transcription sequence for timestamps
|
||||
# Shape is (1)
|
||||
def get_sample_sot_sequence_length(
|
||||
device: torch.device,
|
||||
sot_sequence_length: int,
|
||||
use_int32: bool = False,
|
||||
):
|
||||
torch_dtype = torch.int32 if use_int32 else torch.int64
|
||||
sot_length = torch.tensor([sot_sequence_length], device=device, dtype=torch_dtype)
|
||||
return sot_length
|
||||
|
||||
|
||||
# Create segment length for timestamps
|
||||
# Shape is (1)
|
||||
def get_sample_segment_length(
|
||||
device: torch.device,
|
||||
segment_length: int,
|
||||
use_int32: bool = False,
|
||||
):
|
||||
torch_dtype = torch.int32 if use_int32 else torch.int64
|
||||
segment_size = torch.tensor([segment_length], device=device, dtype=torch_dtype)
|
||||
return segment_size
|
||||
|
||||
|
||||
# Create QKs for timestamps
|
||||
# Shape is (batch_size, num_heads, sequence_length, num_frames // 2)
|
||||
def get_sample_QKs( # noqa: N802
|
||||
config: WhisperConfig,
|
||||
device: torch.device,
|
||||
batch_size: int,
|
||||
sequence_length: int,
|
||||
use_fp16: bool = False,
|
||||
):
|
||||
num_heads = config.decoder_attention_heads
|
||||
torch_dtype = torch.float16 if use_fp16 else torch.float32
|
||||
QKs = [ # noqa: N806
|
||||
torch.rand(
|
||||
batch_size, num_heads, sequence_length, config.max_source_positions, device=device, dtype=torch_dtype
|
||||
)
|
||||
for _ in range(config.decoder_layers)
|
||||
]
|
||||
return QKs
|
||||
|
||||
|
||||
# Create inputs for encoder component of Whisper
|
||||
def get_sample_encoder_inputs(
|
||||
config: WhisperConfig,
|
||||
device: torch.device,
|
||||
batch_size: int,
|
||||
sequence_length: int = 3000,
|
||||
use_fp16: bool = False,
|
||||
):
|
||||
audio_features = get_sample_audio_features(config, device, batch_size, sequence_length, use_fp16)
|
||||
return {"audio_features": audio_features}
|
||||
|
||||
|
||||
# Create inputs for encoder component + first pass through decoder component of Whisper
|
||||
def get_sample_encoder_decoder_init_inputs(
|
||||
config: WhisperConfig,
|
||||
device: torch.device,
|
||||
batch_size: int,
|
||||
decoder_sequence_length: int,
|
||||
encoder_sequence_length: int = 3000,
|
||||
use_fp16: bool = False,
|
||||
use_int32: bool = True,
|
||||
):
|
||||
audio_features = get_sample_audio_features(config, device, batch_size, encoder_sequence_length, use_fp16)
|
||||
decoder_input_ids = get_sample_decoder_input_ids(config, device, batch_size, decoder_sequence_length, use_int32)
|
||||
return {"audio_features": audio_features, "decoder_input_ids": decoder_input_ids}
|
||||
|
||||
|
||||
# Create inputs for decoder component of Whisper
|
||||
# Inputs for first pass through the decoder (i.e. decoder-init): decoder_input_ids, encoder_hidden_states
|
||||
# Inputs for subsequent passes through the decoder (i.e. decoder-with-past): decoder_input_ids, past_key_values
|
||||
def get_sample_decoder_inputs(
|
||||
config: WhisperConfig,
|
||||
device: torch.device,
|
||||
batch_size: int,
|
||||
past_sequence_length: int,
|
||||
sequence_length: int,
|
||||
use_fp16: bool = False,
|
||||
use_int32: bool = True,
|
||||
):
|
||||
decoder_input_ids = get_sample_decoder_input_ids(config, device, batch_size, sequence_length, use_int32)
|
||||
encoder_hidden_states = get_sample_encoder_hidden_states(config, device, batch_size, use_fp16)
|
||||
past_key_values = get_sample_past_key_values(config, device, batch_size, past_sequence_length, use_fp16)
|
||||
return {
|
||||
"decoder_input_ids": decoder_input_ids,
|
||||
"encoder_hidden_states": encoder_hidden_states,
|
||||
"past_key_values": past_key_values,
|
||||
}
|
||||
|
||||
|
||||
# Create inputs for timestamps component of Whisper
|
||||
def get_sample_jump_times_inputs(
|
||||
config: WhisperConfig,
|
||||
device: torch.device,
|
||||
batch_size: int,
|
||||
sequence_length: int,
|
||||
num_alignment_heads: int,
|
||||
sot_sequence_length: int,
|
||||
segment_length: int,
|
||||
use_fp16: bool = False,
|
||||
use_int32: bool = True,
|
||||
):
|
||||
alignment_heads = get_sample_alignment_heads(config, device, num_alignment_heads, use_int32)
|
||||
# lengths need to be int64 because subsequent 'Slice' ops only take int64 inputs
|
||||
sot_sequence_length = get_sample_sot_sequence_length(device, sot_sequence_length)
|
||||
segment_length = get_sample_segment_length(device, segment_length)
|
||||
QKs = get_sample_QKs(config, device, batch_size, sequence_length, use_fp16) # noqa: N806
|
||||
return {
|
||||
"alignment_heads": alignment_heads,
|
||||
"sot_sequence_length": sot_sequence_length,
|
||||
"segment_length": segment_length,
|
||||
"QKs": QKs,
|
||||
}
|
||||
|
||||
|
||||
# Convert PyTorch inputs to ONNX Runtime inputs
|
||||
def convert_inputs_for_ort(
|
||||
inputs: dict,
|
||||
model: InferenceSession,
|
||||
):
|
||||
self_attn_kv_caches, cross_attn_kv_caches = None, None
|
||||
batch_size, num_heads, past_seq_len, head_size = 0, 0, 0, 0
|
||||
num_beams, max_seq_len = 1, 448
|
||||
if "past_key_values" in inputs:
|
||||
(self_attn_kv_caches, cross_attn_kv_caches) = group_past_key_values(inputs["past_key_values"])
|
||||
batch_size, num_heads, past_seq_len, head_size = self_attn_kv_caches[0].shape
|
||||
|
||||
ort_inputs = {}
|
||||
model_inputs = list(map(lambda i: i.name, model.get_inputs())) # noqa: C417
|
||||
use_buffer_sharing = "cache_indirection" in model_inputs
|
||||
for name in model_inputs:
|
||||
if name in {"audio_features", "encoder_input_ids"}:
|
||||
# Encoder input
|
||||
ort_inputs[name] = inputs["audio_features"].detach().cpu().numpy()
|
||||
elif name == "encoder_hidden_states":
|
||||
# Encoder output
|
||||
ort_inputs[name] = inputs["encoder_hidden_states"].detach().cpu().numpy()
|
||||
elif name in {"decoder_input_ids", "input_ids"}:
|
||||
# Decoder input
|
||||
ort_inputs[name] = inputs["decoder_input_ids"].detach().cpu().numpy()
|
||||
elif "past_key_self" in name or "past_value_self" in name:
|
||||
# Decoder input
|
||||
orig_kv_cache = self_attn_kv_caches.pop(0).detach().cpu().numpy()
|
||||
if use_buffer_sharing:
|
||||
new_kv_cache = np.zeros((batch_size, num_heads, max_seq_len, head_size), dtype=orig_kv_cache.dtype)
|
||||
new_kv_cache[:batch_size, :num_heads, :past_seq_len, :head_size] = orig_kv_cache
|
||||
ort_inputs[name] = new_kv_cache
|
||||
else:
|
||||
ort_inputs[name] = orig_kv_cache
|
||||
elif "past_key_cross" in name or "past_value_cross" in name:
|
||||
# Decoder input
|
||||
orig_kv_cache = cross_attn_kv_caches.pop(0).detach().cpu().numpy()
|
||||
ort_inputs[name] = orig_kv_cache
|
||||
elif name == "past_sequence_length":
|
||||
# Decoder input
|
||||
ort_inputs[name] = np.array([past_seq_len], dtype=np.int32)
|
||||
elif name == "cache_indirection":
|
||||
# Decoder input
|
||||
ort_inputs[name] = np.zeros((batch_size, num_beams, max_seq_len), dtype=np.int32)
|
||||
elif name == "alignment_heads":
|
||||
# Jump times input
|
||||
ort_inputs[name] = inputs["alignment_heads"].detach().cpu().numpy()
|
||||
elif name == "sot_sequence_length":
|
||||
# Jump times input
|
||||
ort_inputs[name] = inputs["sot_sequence_length"].detach().cpu().numpy()
|
||||
elif name == "segment_length":
|
||||
# Jump times input
|
||||
ort_inputs[name] = inputs["segment_length"].detach().cpu().numpy()
|
||||
elif "cross_qk" in name:
|
||||
# Jump times input
|
||||
ort_inputs[name] = inputs["QKs"].pop(0).detach().cpu().numpy()
|
||||
else:
|
||||
raise ValueError(f"Unknown name not recognized: {name}")
|
||||
|
||||
return ort_inputs
|
||||
|
||||
|
||||
# Get dynamic axes for all inputs and outputs to the model
|
||||
def get_model_dynamic_axes(
|
||||
config: WhisperConfig,
|
||||
input_names: list[str],
|
||||
output_names: list[str],
|
||||
):
|
||||
dynamic_axes = {}
|
||||
for name in input_names + output_names:
|
||||
if name in {"audio_features", "encoder_input_ids"}:
|
||||
# shape is (batch_size, num_mels, num_frames)
|
||||
dynamic_axes[name] = {0: "batch_size"}
|
||||
elif name in {"input_ids", "decoder_input_ids"}:
|
||||
# shape is (batch_size, sequence_length)
|
||||
dynamic_axes[name] = {0: "batch_size", 1: "sequence_length"}
|
||||
elif name == "alignment_heads":
|
||||
# shape is (num_alignment_heads, 2)
|
||||
dynamic_axes[name] = {0: "num_alignment_heads"}
|
||||
elif name in {"sot_sequence_length", "segment_length"}:
|
||||
# shape is (1)
|
||||
pass
|
||||
elif name == "logits":
|
||||
# shape is (batch_size, sequence_length, vocab_size)
|
||||
dynamic_axes[name] = {0: "batch_size", 1: "sequence_length"}
|
||||
elif name == "encoder_hidden_states":
|
||||
# shape is (batch_size, num_frames // 2, hidden_size)
|
||||
dynamic_axes[name] = {0: "batch_size"}
|
||||
elif "past_key_self" in name or "past_value_self" in name:
|
||||
# shape is (batch_size, num_heads, past_sequence_length, head_size)
|
||||
dynamic_axes[name] = {0: "batch_size", 2: "past_sequence_length"}
|
||||
elif "present_key_self" in name or "present_value_self" in name:
|
||||
# shape is (batch_size, num_heads, past_sequence_length + sequence_length, head_size),
|
||||
# which is equal to (batch_size, num_heads, total_sequence_length, head_size)
|
||||
dynamic_axes[name] = {0: "batch_size", 2: "total_sequence_length"}
|
||||
elif (
|
||||
"past_key_cross" in name
|
||||
or "past_value_cross" in name
|
||||
or "present_key_cross" in name
|
||||
or "present_value_cross" in name
|
||||
):
|
||||
# shape is (batch_size, num_heads, num_frames // 2, head_size)
|
||||
dynamic_axes[name] = {0: "batch_size"}
|
||||
elif "cross_qk" in name:
|
||||
# shape is (batch_size, num_heads, source_sequence_length, target_sequence_length)
|
||||
dynamic_axes[name] = {0: "batch_size", 2: "sequence_length"}
|
||||
elif "jump_times" in name:
|
||||
# shape is (batch_size, max_length)
|
||||
dynamic_axes[name] = {0: "batch_size", 1: "max_length"}
|
||||
else:
|
||||
raise Exception(f"Unknown input or output name found: {name}")
|
||||
return dynamic_axes
|
||||
|
|
@ -0,0 +1,477 @@
|
|||
# -------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import onnx
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import torch.utils.cpp_extension
|
||||
from onnx_model import OnnxModel
|
||||
from transformers import WhisperConfig
|
||||
from whisper_inputs import convert_inputs_for_ort, get_model_dynamic_axes, get_sample_jump_times_inputs
|
||||
|
||||
from onnxruntime import InferenceSession
|
||||
from onnxruntime.tools import pytorch_export_contrib_ops
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
##################################################
|
||||
# Functions that have to be outside of the class
|
||||
# for torch.jit.script_if_tracing to work
|
||||
##################################################
|
||||
|
||||
|
||||
@torch.jit.script_if_tracing
|
||||
def index_QKs(alignment_heads: torch.Tensor, QKs: list[torch.Tensor]): # noqa: N802
|
||||
"""
|
||||
Compute the following to get stacked QK tensor that has been indexed for the desired attention heads:
|
||||
weights = torch.stack([QKs[_l][:, _h] for _l, _h in alignment_heads], dim=1)
|
||||
"""
|
||||
indexed_QKs = [] # noqa: N806
|
||||
for pair in alignment_heads:
|
||||
# Each QK is of shape (batch_size, num_heads, sequence_length, num_frames // 2)
|
||||
# The `QKs[_l]` selects the right QK from the list of QKs
|
||||
# The `QKs[_l][:, _h]` selects the right attention heads from the chosen QK. The `:` is to do this for the batch dim.
|
||||
#
|
||||
# PyTorch:
|
||||
# QKs[_l] is of shape (batch_size, num_heads, sequence_length, num_frames // 2)
|
||||
# QKs[_l][:, _h] is of shape (batch_size, sequence_length, num_frames // 2)
|
||||
#
|
||||
# ONNX:
|
||||
# QKs[_l] is of shape (batch_size, num_heads, sequence_length, num_frames // 2)
|
||||
# QKs[_l][:, _h] is of shape (batch_size, 1, sequence_length, num_frames // 2) because
|
||||
# the `[:, _h]` operation maps to a Gather op and that op does not reduce dimensions
|
||||
_l, _h = pair[0], pair[1]
|
||||
indexed_QKs.append(QKs[_l][:, _h])
|
||||
|
||||
# PyTorch:
|
||||
# torch.stack will return a tensor of shape (batch_size, num_alignment_heads, sequence_length, num_frames // 2).
|
||||
#
|
||||
# ONNX:
|
||||
# torch.stack will return a tensor of shape (batch_size, num_alignment_heads, 1, sequence_length, num_frames // 2)
|
||||
# because the Gather op does not reduce dimensions. To remove the unneeded dimension, torch.squeeze with a specified
|
||||
# dim (dim = 2) is added. The torch.squeeze op with a specified dim only runs if the specified dim has a size of 1.
|
||||
# Since the dim won't be of size 1 in the PyTorch tensor but it is of size 1 in the ONNX tensor, it will be a no-op
|
||||
# in PyTorch and an op in ONNX. Thus, the Squeeze op will only affect the ONNX model.
|
||||
weights = torch.stack(indexed_QKs, dim=1)
|
||||
weights = torch.squeeze(weights, dim=2)
|
||||
return weights
|
||||
|
||||
|
||||
def jump_timings(text_indices, time_indices):
|
||||
"""
|
||||
Calculate jump times from text_indices and time_indices where
|
||||
text_indices and time_indices are both 1d vectors
|
||||
"""
|
||||
TOKENS_PER_SECOND = 50.0 # noqa: N806
|
||||
diff = text_indices[1:] - text_indices[:-1]
|
||||
padding = torch.tensor([1], dtype=torch.int32)
|
||||
jumps = torch.cat((padding, diff)).to(torch.bool)
|
||||
jump_times = time_indices[jumps].to(torch.float) / TOKENS_PER_SECOND
|
||||
return jump_times
|
||||
|
||||
|
||||
def padded_jump_from_dtw(matrix_2d: torch.Tensor, max_length: torch.Tensor):
|
||||
"""
|
||||
Run Dynamic Time Warping (DTW) on batched tensor
|
||||
"""
|
||||
trace = torch.ops.onnxruntime.DynamicTimeWarping(matrix_2d)
|
||||
text_indices = trace[0, :]
|
||||
time_indices = trace[1, :]
|
||||
jump_times = jump_timings(text_indices, time_indices)
|
||||
return F.pad(jump_times, [0, int((max_length - jump_times.size(-1)).item())], mode="constant", value=-1.0)
|
||||
|
||||
|
||||
@torch.jit.script_if_tracing
|
||||
def batch_jump_times(matrix: torch.Tensor, max_decoded_length: torch.Tensor):
|
||||
"""
|
||||
Compute the following to calculate jump times for all batches:
|
||||
batched_jump_times = torch.stack([self.padded_jump_from_dtw(matrix[b], max_decoded_length) for b in range(matrix.size(0))])
|
||||
"""
|
||||
list_of_jump_times = []
|
||||
for b in range(matrix.size(0)):
|
||||
jump_times = padded_jump_from_dtw(matrix[b], max_decoded_length)
|
||||
list_of_jump_times.append(jump_times)
|
||||
batched_jump_times = torch.stack(list_of_jump_times)
|
||||
return batched_jump_times
|
||||
|
||||
|
||||
class WhisperJumpTimes(torch.nn.Module):
|
||||
"""Whisper jump times component"""
|
||||
|
||||
def __init__(self, config: WhisperConfig, device: torch.device, cache_dir: str | os.PathLike):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.device = device
|
||||
self.cache_dir = cache_dir
|
||||
|
||||
self.filter_width = 7
|
||||
self.qk_scale = 1.0
|
||||
|
||||
def median_filter(self, weights: torch.Tensor):
|
||||
"""
|
||||
Apply a median filter of width `filter_width` along the last dimension of `weights`
|
||||
"""
|
||||
pad_width = self.filter_width // 2
|
||||
x = F.pad(weights, (pad_width, pad_width, 0, 0), mode="reflect")
|
||||
x_unfolded = torch.ops.onnxruntime.UnfoldTensor(x, -1, self.filter_width, 1)
|
||||
result = torch.select(x_unfolded.sort()[0], dim=-1, index=pad_width)
|
||||
return result
|
||||
|
||||
def forward(
|
||||
self,
|
||||
alignment_heads: torch.Tensor,
|
||||
sot_sequence_length: torch.Tensor,
|
||||
segment_length: torch.Tensor,
|
||||
QKs: list[torch.Tensor],
|
||||
):
|
||||
# Get stacked QKs tensor
|
||||
weights = index_QKs(alignment_heads, QKs)
|
||||
weights = weights[:, :, : segment_length // 2]
|
||||
weights = weights.to(torch.float32)
|
||||
|
||||
weights = (weights * self.qk_scale).softmax(dim=-1)
|
||||
std, mean = torch.std_mean(weights, dim=-2, keepdim=True, unbiased=False)
|
||||
weights = (weights - mean) / std
|
||||
weights = self.median_filter(weights)
|
||||
|
||||
matrix = torch.mean(weights, 1)
|
||||
matrix = -matrix[:, sot_sequence_length:-1]
|
||||
|
||||
max_decoded_length = torch.tensor([matrix.size(1)], dtype=torch.int64)
|
||||
batched_jump_times = batch_jump_times(matrix, max_decoded_length)
|
||||
return batched_jump_times
|
||||
|
||||
def input_names(self):
|
||||
input_names = [
|
||||
"alignment_heads",
|
||||
"sot_sequence_length",
|
||||
"segment_length",
|
||||
*[f"cross_qk_{i}" for i in range(self.config.decoder_layers)],
|
||||
]
|
||||
return input_names
|
||||
|
||||
def output_names(self):
|
||||
output_names = ["jump_times"]
|
||||
return output_names
|
||||
|
||||
def inputs(self, use_fp16_inputs: bool, use_int32_inputs: bool, return_dict: bool = False):
|
||||
inputs = get_sample_jump_times_inputs(
|
||||
self.config,
|
||||
self.device,
|
||||
batch_size=2,
|
||||
sequence_length=8,
|
||||
num_alignment_heads=6,
|
||||
sot_sequence_length=3,
|
||||
segment_length=1332,
|
||||
use_fp16=use_fp16_inputs,
|
||||
use_int32=use_int32_inputs,
|
||||
)
|
||||
if return_dict:
|
||||
return inputs
|
||||
return (
|
||||
inputs["alignment_heads"],
|
||||
inputs["sot_sequence_length"],
|
||||
inputs["segment_length"],
|
||||
inputs["QKs"],
|
||||
)
|
||||
|
||||
def create_torch_ops(self):
|
||||
"""
|
||||
1) Create UnfoldTensor and DynamicTimeWarping as torch ops
|
||||
3) Provide a symbolic mapping from torch ops to ORT contrib ops
|
||||
|
||||
See https://pytorch.org/tutorials/advanced/torch_script_custom_ops.html#building-with-jit-compilation
|
||||
for more details on how this works.
|
||||
"""
|
||||
# Set torch extensions directory to cache directory
|
||||
os.environ["TORCH_EXTENSIONS_DIR"] = self.cache_dir
|
||||
|
||||
# Try to import `ninja` pip package
|
||||
try:
|
||||
assert torch.utils.cpp_extension.verify_ninja_availability()
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred while verifying `ninja` is available: {e}", exc_info=True) # noqa: G201
|
||||
install_cmd = "pip install ninja"
|
||||
logger.warning(f"Could not import `ninja`. Attempting to install `ninja` via `{install_cmd}`.")
|
||||
os.system(install_cmd)
|
||||
|
||||
# Create UnfoldTensor torch op
|
||||
unfold_op_source = textwrap.dedent("""\
|
||||
#include "torch/script.h"
|
||||
|
||||
torch::Tensor UnfoldTensor(torch::Tensor input, int64_t dim, int64_t size, int64_t step) {
|
||||
return input.unfold(dim, size, step);
|
||||
}
|
||||
|
||||
// namespace is onnxruntime
|
||||
static auto registry = torch::RegisterOperators("onnxruntime::UnfoldTensor", &UnfoldTensor);
|
||||
""")
|
||||
|
||||
torch.utils.cpp_extension.load_inline(
|
||||
name="UnfoldTensor",
|
||||
cpp_sources=unfold_op_source,
|
||||
is_python_module=False,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Create DynamicTimeWarping torch op
|
||||
dtw_op_source = textwrap.dedent("""\
|
||||
#include "torch/script.h"
|
||||
#include "torch/torch.h"
|
||||
#include <stdexcept>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
torch::Tensor Backtrace(torch::Tensor trace) {
|
||||
int64_t i = trace.size(0) - 1;
|
||||
int64_t j = trace.size(1) - 1;
|
||||
trace.index({0, torch::indexing::Slice()}) = 2;
|
||||
trace.index({torch::indexing::Slice(), 0}) = 1;
|
||||
|
||||
std::vector<int32_t> result_vec;
|
||||
while (i > 0 || j > 0) {
|
||||
result_vec.push_back(static_cast<int32_t>(i - 1));
|
||||
result_vec.push_back(static_cast<int32_t>(j - 1));
|
||||
int value = trace[i][j].item<int>();
|
||||
|
||||
if (value == 0) {
|
||||
i--;
|
||||
j--;
|
||||
} else if (value == 1) {
|
||||
i--;
|
||||
} else if (value == 2) {
|
||||
j--;
|
||||
} else {
|
||||
throw std::runtime_error("Unexpected trace[i, j]");
|
||||
}
|
||||
}
|
||||
|
||||
// Compute result[::-1, :].T
|
||||
torch::Tensor result = torch::from_blob(result_vec.data(), {static_cast<long int>(result_vec.size() / 2), 2}, torch::kInt32).clone();
|
||||
torch::Tensor reversed = result.flip(0); // result[::-1, :]
|
||||
torch::Tensor transposed = reversed.transpose(0, 1); // .T
|
||||
return transposed;
|
||||
}
|
||||
|
||||
torch::Tensor DynamicTimeWarping(torch::Tensor x) {
|
||||
int64_t N = x.size(0);
|
||||
int64_t M = x.size(1);
|
||||
torch::Tensor cost = torch::full({N + 1, M + 1}, std::numeric_limits<float>::infinity(), torch::dtype(torch::kFloat32));
|
||||
torch::Tensor trace = torch::full({N + 1, M + 1}, -1, torch::dtype(torch::kFloat32));
|
||||
|
||||
cost[0][0] = 0;
|
||||
for (int j = 1; j < M + 1; j++) {
|
||||
for (int i = 1; i < N + 1; i++) {
|
||||
float c0 = cost[i - 1][j - 1].item<float>();
|
||||
float c1 = cost[i - 1][j].item<float>();
|
||||
float c2 = cost[i][j - 1].item<float>();
|
||||
|
||||
float c = 0;
|
||||
float t = 0;
|
||||
|
||||
if (c0 < c1 && c0 < c2) {
|
||||
c = c0;
|
||||
t = 0;
|
||||
} else if (c1 < c0 && c1 < c2) {
|
||||
c = c1;
|
||||
t = 1;
|
||||
} else {
|
||||
c = c2;
|
||||
t = 2;
|
||||
}
|
||||
|
||||
cost[i][j] = x[i - 1][j - 1].item<float>() + c;
|
||||
trace[i][j] = t;
|
||||
}
|
||||
}
|
||||
|
||||
return Backtrace(trace);
|
||||
}
|
||||
|
||||
// namespace is onnxruntime
|
||||
static auto registry = torch::RegisterOperators("onnxruntime::DynamicTimeWarping", &DynamicTimeWarping);
|
||||
""")
|
||||
|
||||
torch.utils.cpp_extension.load_inline(
|
||||
name="DynamicTimeWarping",
|
||||
cpp_sources=dtw_op_source,
|
||||
is_python_module=False,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Create symbolic mapping from torch ops to ORT contrib ops
|
||||
pytorch_export_contrib_ops.register()
|
||||
|
||||
def export_onnx(
|
||||
self,
|
||||
onnx_model_path: str,
|
||||
provider: str,
|
||||
verbose: bool = True,
|
||||
use_external_data_format: bool = False,
|
||||
use_fp16_inputs: bool = False,
|
||||
use_int32_inputs: bool = True,
|
||||
):
|
||||
"""Export word-level timestamps to ONNX
|
||||
|
||||
Args:
|
||||
onnx_model_path (str): path to save ONNX model
|
||||
provider (str): provider to use for verifying parity on ONNX model
|
||||
verbose (bool, optional): print verbose information. Defaults to True.
|
||||
use_external_data_format (bool, optional): use external data format or not. Defaults to False.
|
||||
use_fp16_inputs (bool, optional): use float16 inputs for the audio_features. Defaults to False.
|
||||
use_int32_inputs (bool, optional): use int32 inputs for the decoder_input_ids. Defaults to True.
|
||||
"""
|
||||
# Shape of timestamps's tensors:
|
||||
# Inputs:
|
||||
# alignment_heads: (num_alignment_heads, 2)
|
||||
# sot_sequence_length: (1)
|
||||
# segment_length: (1)
|
||||
# cross_qk_*: (batch_size, num_heads, sequence_length, num_frames // 2)
|
||||
# Outputs:
|
||||
# jump_times: (batch_size, max_length)
|
||||
|
||||
# Definitions:
|
||||
# alignment_heads: the attention head indices where the Q*K values are highly correlated with word-level timestamps
|
||||
# (i.e. the alignment between audio and text tokens)
|
||||
# This is calculated as follows:
|
||||
#
|
||||
# ```
|
||||
# import base64
|
||||
# import gzip
|
||||
# import numpy as np
|
||||
# import torch
|
||||
#
|
||||
# # base85-encoded (n_layers, n_heads) boolean arrays indicating the cross-attention heads that are
|
||||
# # highly correlated to the word-level timing, i.e. the alignment between audio and text tokens.
|
||||
# _ALIGNMENT_HEADS = {
|
||||
# "tiny.en": b"ABzY8J1N>@0{>%R00Bk>$p{7v037`oCl~+#00",
|
||||
# "tiny": b"ABzY8bu8Lr0{>%RKn9Fp%m@SkK7Kt=7ytkO",
|
||||
# "base.en": b"ABzY8;40c<0{>%RzzG;p*o+Vo09|#PsxSZm00",
|
||||
# "base": b"ABzY8KQ!870{>%RzyTQH3`Q^yNP!>##QT-<FaQ7m",
|
||||
# "small.en": b"ABzY8>?_)10{>%RpeA61k&I|OI3I$65C{;;pbCHh0B{qLQ;+}v00",
|
||||
# "small": b"ABzY8DmU6=0{>%Rpa?J`kvJ6qF(V^F86#Xh7JUGMK}P<N0000",
|
||||
# "medium.en": b"ABzY8usPae0{>%R7<zz_OvQ{)4kMa0BMw6u5rT}kRKX;$NfYBv00*Hl@qhsU00",
|
||||
# "medium": b"ABzY8B0Jh+0{>%R7}kK1fFL7w6%<-Pf*t^=N)Qr&0RR9",
|
||||
# "large-v1": b"ABzY8r9j$a0{>%R7#4sLmoOs{s)o3~84-RPdcFk!JR<kSfC2yj",
|
||||
# "large-v2": b"ABzY8zd+h!0{>%R7=D0pU<_bnWW*tkYAhobTNnu$jnkEkXqp)j;w1Tzk)UH3X%SZd&fFZ2fC2yj",
|
||||
# "large-v3": b"ABzY8gWO1E0{>%R7(9S+Kn!D~%ngiGaR?*L!iJG9p-nab0JQ=-{D1-g00",
|
||||
# "large": b"ABzY8gWO1E0{>%R7(9S+Kn!D~%ngiGaR?*L!iJG9p-nab0JQ=-{D1-g00",
|
||||
# "large-v3-turbo": b"ABzY8j^C+e0{>%RARaKHP%t(lGR*)0g!tONPyhe`",
|
||||
# "turbo": b"ABzY8j^C+e0{>%RARaKHP%t(lGR*)0g!tONPyhe`",
|
||||
# }
|
||||
#
|
||||
# model_name = "large-v3-turbo"
|
||||
# array = np.frombuffer(
|
||||
# gzip.decompress(base64.b85decode(_ALIGNMENT_HEADS[model_name])), dtype=bool
|
||||
# ).copy()
|
||||
# mask = torch.from_numpy(array).reshape(
|
||||
# self.dims.n_text_layer, self.dims.n_text_head
|
||||
# )
|
||||
# self.alignment_heads = mask.to_sparse().indices().T
|
||||
# ```
|
||||
#
|
||||
# sot_sequence_length: the length of the start-of-transcription sequence before the first token is generated
|
||||
# Typically the start-of-transcription sequence is [<|startoftranscription|>, <|language_token|>, <|task_token|>]
|
||||
# so its length is 3.
|
||||
#
|
||||
# segment_length: the length (in frames) of the audio segment that is being transcribed
|
||||
#
|
||||
# cross_qk_*: the Q*K values for the cross-attention blocks in the decoder
|
||||
# Every decoder layer has a self-attention block and a cross-attention block so there are `n` cross-attention blocks
|
||||
# where `n` is the number of decoder layers.
|
||||
#
|
||||
# jump_times: the timings where jumps occur in speech
|
||||
# This allows us to detect when a word began to be spoken by the speaker (start_times) and when a word was finished
|
||||
# being spoken by the speaker (end_times).
|
||||
|
||||
inputs = self.inputs(use_fp16_inputs=use_fp16_inputs, use_int32_inputs=use_int32_inputs)
|
||||
input_names = self.input_names()
|
||||
output_names = self.output_names()
|
||||
dynamic_axes = get_model_dynamic_axes(self.config, input_names, output_names)
|
||||
|
||||
Path(onnx_model_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory() as tmp_dir_name:
|
||||
temp_onnx_model_path = os.path.join(tmp_dir_name, "encoder.onnx")
|
||||
Path(temp_onnx_model_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path = temp_onnx_model_path if use_external_data_format else onnx_model_path
|
||||
|
||||
# Create torch ops and map them to ORT contrib ops before export
|
||||
self.create_torch_ops()
|
||||
torch.onnx.export(
|
||||
self,
|
||||
args=inputs,
|
||||
f=out_path,
|
||||
export_params=True,
|
||||
input_names=input_names,
|
||||
output_names=output_names,
|
||||
dynamic_axes=dynamic_axes,
|
||||
opset_version=17,
|
||||
do_constant_folding=True,
|
||||
verbose=verbose,
|
||||
custom_opsets={"com.microsoft": 1},
|
||||
)
|
||||
|
||||
if use_external_data_format:
|
||||
model = onnx.load_model(out_path, load_external_data=use_external_data_format)
|
||||
OnnxModel.save(
|
||||
model,
|
||||
onnx_model_path,
|
||||
save_as_external_data=True,
|
||||
all_tensors_to_one_file=True,
|
||||
)
|
||||
|
||||
self.verify_onnx(onnx_model_path, provider, use_fp16_inputs, use_int32_inputs)
|
||||
|
||||
def verify_onnx(
|
||||
self,
|
||||
onnx_model_path: str,
|
||||
provider: str,
|
||||
use_fp16_inputs: bool,
|
||||
use_int32_inputs: bool,
|
||||
):
|
||||
"""Verify ONNX model outputs and PyTorch model outputs match
|
||||
|
||||
Args:
|
||||
onnx_model_path (str): path to save ONNX model
|
||||
provider (str): execution provider for ONNX model
|
||||
use_fp16_inputs (bool, optional): use float16 inputs for the cross_qk_{i}
|
||||
use_int32_inputs (bool, optional): use int32 inputs for the alignment_heads and sot_sequence_length
|
||||
"""
|
||||
# Shape of jump times's tensors:
|
||||
# Inputs:
|
||||
# alignment_heads: (num_alignment_heads, 2)
|
||||
# sot_sequence_length: (1)
|
||||
# segment_length: (1)
|
||||
# cross_qk_*: (batch_size, num_heads, sequence_length, num_frames // 2)
|
||||
# Outputs:
|
||||
# jump_times: (batch_size, max_length)
|
||||
inputs = self.inputs(use_fp16_inputs=use_fp16_inputs, use_int32_inputs=use_int32_inputs, return_dict=True)
|
||||
|
||||
# Run PyTorch model
|
||||
pt_outputs = (
|
||||
self.forward(
|
||||
inputs["alignment_heads"], inputs["sot_sequence_length"], inputs["segment_length"], inputs["QKs"]
|
||||
)
|
||||
.detach()
|
||||
.cpu()
|
||||
.numpy()
|
||||
)
|
||||
|
||||
# Run ONNX model
|
||||
sess = InferenceSession(onnx_model_path, providers=[provider])
|
||||
ort_outputs = sess.run(None, convert_inputs_for_ort(inputs, sess))
|
||||
|
||||
# Calculate output difference
|
||||
diff = np.abs(pt_outputs - ort_outputs)
|
||||
print("Comparing batched jump_times...", flush=True)
|
||||
print(f"Max diff: {np.max(diff)}", flush=True)
|
||||
Loading…
Add table
Add a link
Reference in a new issue