| import argparse |
| import os |
| import torch |
| from torch.nn.attention import sdpa_kernel, SDPBackend |
| from transformers import AutoConfig, AutoModelForSpeechSeq2Seq, AutoModelForMultimodalLM, AutoModelForCTC, AutoProcessor, MODEL_FOR_MULTIMODAL_LM_MAPPING, MODEL_FOR_SPEECH_SEQ_2_SEQ_MAPPING, MODEL_FOR_CTC_MAPPING, CompileConfig |
| import evaluate |
| from normalizer import data_utils |
| from normalizer.eval_utils import normalize_compound_pairs |
| from tqdm import tqdm |
| from datasets import load_dataset, Audio |
| import random |
| import numpy as np |
|
|
| wer_metric = evaluate.load("wer") |
| torch.set_float32_matmul_precision('high') |
|
|
|
|
| def main(args): |
|
|
| |
| seed = 42 |
| random.seed(seed) |
| np.random.seed(seed) |
| torch.manual_seed(seed) |
| torch.cuda.manual_seed_all(seed) |
| torch.backends.cudnn.deterministic = True |
|
|
| torch_dtype = getattr(torch, args.dtype) |
|
|
| config = AutoConfig.from_pretrained(args.model_id) |
| if type(config) in MODEL_FOR_SPEECH_SEQ_2_SEQ_MAPPING: |
| cls_model = AutoModelForSpeechSeq2Seq |
| elif type(config) in MODEL_FOR_MULTIMODAL_LM_MAPPING: |
| cls_model = AutoModelForMultimodalLM |
| elif type(config) in MODEL_FOR_CTC_MAPPING: |
| cls_model = AutoModelForCTC |
| else: |
| raise ValueError(f"Model config of type {type(config)} not recognized in Transformers mappings.") |
| is_ctc = cls_model == AutoModelForCTC |
|
|
| model = cls_model.from_pretrained( |
| args.model_id, |
| dtype=torch_dtype, |
| attn_implementation=args.attn_implementation, |
| ) |
| model.to(args.device) |
| model.eval() |
| print(f"Model size: {sum(p.numel() for p in model.parameters()) / 1e9:.2f}B parameters") |
| processor = AutoProcessor.from_pretrained(args.model_id) |
| has_transcription_processor = hasattr(processor, "apply_transcription_request") |
|
|
| |
| if hasattr(processor, "feature_extractor") and processor.feature_extractor is not None: |
| sampling_rate = processor.feature_extractor.sampling_rate |
| elif hasattr(processor, "audio_processor") and processor.audio_processor is not None: |
| sampling_rate = processor.audio_processor.sampling_rate |
| else: |
| sampling_rate = 16_000 |
|
|
| |
| if model.can_generate(): |
| gen_kwargs = {} |
| if args.max_new_tokens is not None: |
| gen_kwargs["max_new_tokens"] = args.max_new_tokens |
|
|
| |
| if getattr(model.generation_config, "is_multilingual", False): |
| gen_kwargs["task"] = "transcribe" |
| if args.language is not None: |
| gen_kwargs["language"] = args.language |
| elif args.max_new_tokens: |
| raise ValueError("`max_new_tokens` should only be set for auto-regressive models, but got a CTC model.") |
|
|
| CONFIG_NAME = args.config_name |
| SPLIT_NAME = args.split |
|
|
| |
| if args.language is not None: |
| norm_language = args.language |
| else: |
| try: |
| norm_language = CONFIG_NAME.split("_", 1)[1] |
| except IndexError: |
| norm_language = "en" |
| print(f"Language not specified, extracted '{norm_language}' from config_name '{CONFIG_NAME}'") |
|
|
| if args.torch_compile is not None: |
| if model.can_generate(): |
| gen_kwargs["compile_config"] = CompileConfig(mode=args.torch_compile, fullgraph=args.compile_fullgraph) |
| model.generation_config.cache_implementation = "static" |
| else: |
| model = torch.compile(model, mode=args.torch_compile, fullgraph=args.compile_fullgraph) |
|
|
| |
| if args.warmup_steps is None or args.warmup_steps < 1: |
| print("`--torch_compile` is enabled; forcing `--warmup_steps=10` to trigger compilation before timed runs.") |
| args.warmup_steps = 10 |
|
|
| |
| print(f"Loading dataset: {args.dataset} with config: {CONFIG_NAME}") |
| dataset = load_dataset( |
| args.dataset, |
| CONFIG_NAME, |
| split=SPLIT_NAME, |
| streaming=args.streaming, |
| token=True, |
| ) |
| dataset = dataset.cast_column("audio", Audio(sampling_rate=sampling_rate)) |
|
|
| if args.max_eval_samples is not None and args.max_eval_samples > 0: |
| print(f"Subsampling dataset to first {args.max_eval_samples} samples!") |
| if args.streaming: |
| dataset = dataset.take(args.max_eval_samples) |
| else: |
| dataset = dataset.select(range(min(args.max_eval_samples, len(dataset)))) |
|
|
| def benchmark(batch, min_new_tokens=None): |
| audios = [audio["array"] for audio in batch["audio"]] |
| minibatch_size = len(audios) |
| sampling_rate = batch["audio"][0]["sampling_rate"] |
| batch["audio_length_s"] = [len(audio) / sampling_rate for audio in audios] |
| batch["audio_filepath"] = data_utils.extract_audio_filepaths_from_batch(batch, minibatch_size) |
|
|
| |
| torch.cuda.synchronize(device=args.device) |
| start_event = torch.cuda.Event(enable_timing=True) |
| end_event = torch.cuda.Event(enable_timing=True) |
| start_event.record() |
|
|
| |
| |
| padding_size = None |
| if minibatch_size != args.batch_size and args.torch_compile is not None: |
| padding_size = args.batch_size - minibatch_size |
| padding_audios = [audios[-1] for _ in range(padding_size)] |
| audios.extend(padding_audios) |
|
|
| if has_transcription_processor: |
| if "voxtral" in args.model_id.lower(): |
| inputs = processor.apply_transcription_request( |
| language=args.language, |
| audio=audios, |
| model_id=args.model_id, |
| sampling_rate=sampling_rate, |
| format=["wav"] * len(audios), |
| ) |
| else: |
| inputs = processor.apply_transcription_request(audios) |
| prompt_len = inputs["input_ids"].shape[1] |
| elif not model.can_generate(): |
| |
| inputs = processor( |
| audios, |
| sampling_rate=sampling_rate, |
| truncation=False, |
| padding="longest", |
| return_tensors="pt", |
| return_attention_mask=True, |
| ) |
| else: |
| |
| inputs = processor( |
| audios, |
| sampling_rate=sampling_rate, |
| return_tensors="pt", |
| padding="longest", |
| return_attention_mask=True, |
| device=args.device, |
| ) |
|
|
| inputs = inputs.to(args.device, dtype=torch_dtype) |
|
|
| |
| if args.torch_compile is not None: |
| sdpa_backends = [SDPBackend.MATH] |
| else: |
| sdpa_backends = [SDPBackend.FLASH_ATTENTION, SDPBackend.EFFICIENT_ATTENTION, SDPBackend.MATH] |
| with sdpa_kernel(sdpa_backends): |
| if model.can_generate(): |
| pred_ids = model.generate(**inputs, **gen_kwargs, min_new_tokens=min_new_tokens) |
| else: |
| |
| with torch.no_grad(): |
| logits = model(**inputs).logits |
| pred_ids = logits.argmax(-1) |
|
|
| |
| |
| if padding_size is not None: |
| pred_ids = pred_ids[:-padding_size, ...] |
|
|
| |
| if has_transcription_processor: |
| pred_text = processor.batch_decode(pred_ids[:, prompt_len:], skip_special_tokens=True) |
| elif is_ctc: |
| |
| pred_text = processor.batch_decode(pred_ids) |
| else: |
| pred_text = processor.batch_decode(pred_ids, skip_special_tokens=True) |
|
|
| |
| end_event.record() |
| torch.cuda.synchronize(device=args.device) |
| runtime = start_event.elapsed_time(end_event) / 1000.0 |
|
|
| batch["transcription_time_s"] = minibatch_size * [runtime / minibatch_size] |
|
|
| |
| batch["predictions"] = [data_utils.ml_normalizer(pred, lang=norm_language) for pred in pred_text] |
| batch["references"] = [data_utils.ml_normalizer(ref, lang=norm_language) for ref in batch["text"]] |
|
|
| return batch |
|
|
| if args.warmup_steps is not None and args.warmup_steps > 0: |
| print(f"Running {args.warmup_steps} warmup steps...") |
| num_warmup_samples = args.warmup_steps * args.batch_size |
| if args.streaming: |
| warmup_dataset = dataset.take(num_warmup_samples) |
| else: |
| warmup_dataset = dataset.select(range(min(num_warmup_samples, len(dataset)))) |
| warmup_dataset = iter(warmup_dataset.map( |
| benchmark, batch_size=args.batch_size, batched=True, |
| fn_kwargs={"min_new_tokens": args.max_new_tokens} |
| )) |
| for _ in tqdm(warmup_dataset, desc="Warming up..."): |
| continue |
|
|
| |
| dataset = load_dataset( |
| args.dataset, |
| CONFIG_NAME, |
| split=SPLIT_NAME, |
| streaming=args.streaming, |
| token=True, |
| ) |
| dataset = dataset.cast_column("audio", Audio(sampling_rate=sampling_rate)) |
|
|
| if args.max_eval_samples is not None and args.max_eval_samples > 0: |
| if args.streaming: |
| dataset = dataset.take(args.max_eval_samples) |
| else: |
| dataset = dataset.select(range(min(args.max_eval_samples, len(dataset)))) |
|
|
| dataset = dataset.map( |
| benchmark, batch_size=args.batch_size, batched=True, remove_columns=["audio"], |
| ) |
|
|
| all_results = { |
| "audio_length_s": [], |
| "transcription_time_s": [], |
| "predictions": [], |
| "references": [], |
| "audio_filepath": [], |
| } |
|
|
| result_iter = iter(dataset) |
| for result in tqdm(result_iter, desc="Samples..."): |
| for key in all_results: |
| all_results[key].append(result[key]) |
|
|
| |
| filtered = [ |
| (ref, pred, dur, time_s, fpath) |
| for ref, pred, dur, time_s, fpath in zip( |
| all_results["references"], all_results["predictions"], |
| all_results["audio_length_s"], all_results["transcription_time_s"], |
| all_results["audio_filepath"] |
| ) |
| if data_utils.is_target_text_in_range(ref) |
| ] |
| if filtered: |
| all_results["references"], all_results["predictions"], all_results["audio_length_s"], all_results["transcription_time_s"], all_results["audio_filepath"] = zip(*filtered) |
| all_results = {k: list(v) for k, v in all_results.items()} |
|
|
| |
| manifest_path = data_utils.write_manifest( |
| all_results["references"], |
| all_results["predictions"], |
| args.model_id, |
| args.dataset, |
| CONFIG_NAME, |
| args.split, |
| audio_length=all_results["audio_length_s"], |
| transcription_time=all_results["transcription_time_s"], |
| audio_filepaths=all_results["audio_filepath"], |
| ) |
| print("Results saved at path:", os.path.abspath(manifest_path)) |
|
|
| wer_refs, wer_preds = normalize_compound_pairs(all_results["references"], all_results["predictions"]) |
| wer = wer_metric.compute(references=wer_refs, predictions=wer_preds) |
| wer = round(100 * wer, 2) |
| rtfx = round(sum(all_results["audio_length_s"]) / sum(all_results["transcription_time_s"]), 2) |
| print("WER:", wer, "%", "RTFx:", rtfx) |
|
|
|
|
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser() |
|
|
| parser.add_argument( |
| "--model_id", |
| type=str, |
| required=True, |
| help="Model identifier. Should be loadable with Transformers", |
| ) |
| parser.add_argument( |
| "--dataset", |
| type=str, |
| required=True, |
| help="Dataset name. E.g. 'nithinraok/asr-leaderboard-datasets'", |
| ) |
| parser.add_argument( |
| "--config_name", |
| type=str, |
| required=True, |
| help="Config name for the dataset. E.g. 'fleurs_de' for German FLEURS.", |
| ) |
| parser.add_argument( |
| "--language", |
| type=str, |
| default=None, |
| help="Language code, e.g. 'de' for German. If not set, the model will auto-detect the language.", |
| ) |
| parser.add_argument( |
| "--split", |
| type=str, |
| default="test", |
| help="Split of the dataset.", |
| ) |
| parser.add_argument( |
| "--device", |
| type=int, |
| default=-1, |
| help="The device to run the pipeline on. -1 for CPU (default), 0 for the first GPU and so on.", |
| ) |
| parser.add_argument( |
| "--batch_size", |
| type=int, |
| default=64, |
| help="Number of samples to go through each batch.", |
| ) |
| parser.add_argument( |
| "--max_eval_samples", |
| type=int, |
| default=None, |
| help="Number of samples to be evaluated. Put a lower number e.g. 64 for testing this script.", |
| ) |
| parser.add_argument( |
| "--streaming", |
| action="store_true", |
| help="Stream the dataset lazily over the network instead of downloading it in full before the evaluation. Off by default for reproducible benchmark timings.", |
| ) |
| parser.add_argument( |
| "--max_new_tokens", |
| type=int, |
| default=None, |
| help="Maximum number of tokens to generate.", |
| ) |
| parser.add_argument( |
| "--torch_compile", |
| type=str, |
| default=None, |
| help="Mode for torch compiling model forward pass. Can be either 'default', 'reduce-overhead', 'max-autotune' or 'max-autotune-no-cudagraphs'.", |
| ) |
| parser.add_argument( |
| "--compile_fullgraph", |
| action="store_true", |
| help="Whether to do full graph compilation.", |
| ) |
| parser.add_argument( |
| "--dtype", |
| type=str, |
| default="bfloat16", |
| help="The dtype to use for model loading and inference. E.g. 'bfloat16', 'float16', 'float32'.", |
| ) |
| parser.add_argument( |
| "--attn_implementation", |
| type=str, |
| default="sdpa", |
| help="Attention implementation to use for model loading (e.g. 'sdpa', 'eager', 'flash_attention_2').", |
| ) |
| parser.add_argument( |
| "--warmup_steps", |
| type=int, |
| default=10, |
| help="Number of warm-up steps to run before launching the timed runs.", |
| ) |
| args = parser.parse_args() |
|
|
| main(args) |
|
|