From 0dd8a71bca6b636756c9406560d3ca45dc01a722 Mon Sep 17 00:00:00 2001 From: Varalakshmi Bayanagari Date: Wed, 15 Jul 2026 21:05:12 +0000 Subject: [PATCH 1/6] Make Whisper mel magnitudes contiguous to avoid slow strided matmul `stft[..., :-1]` produces a non-contiguous view, and squaring its magnitude keeps it non-contiguous. On some CPU backends (observed on a ROCm PyTorch CPU build) the subsequent `mel_filters.T @ magnitudes` matmul falls onto a pathological strided-GEMM path that runs ~8x slower (~43 ms vs ~3 ms for a 30 s audio window), dominating Whisper feature extraction. Forcing `magnitudes` contiguous costs ~0.18 ms but restores the fast matmul path, cutting per-request audio preprocessing substantially with no change in output. Signed-off-by: Varalakshmi Bayanagari --- .../models/whisper/feature_extraction_whisper.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/transformers/models/whisper/feature_extraction_whisper.py b/src/transformers/models/whisper/feature_extraction_whisper.py index 4151a3824dfd..8adbf8fa20e7 100644 --- a/src/transformers/models/whisper/feature_extraction_whisper.py +++ b/src/transformers/models/whisper/feature_extraction_whisper.py @@ -147,7 +147,11 @@ def _torch_extract_fbank_features(self, waveform: np.ndarray, device: str = "cpu waveform += self.dither * torch.randn(waveform.shape, dtype=waveform.dtype, device=waveform.device) stft = torch.stft(waveform, self.n_fft, self.hop_length, window=window, return_complex=True) - magnitudes = stft[..., :-1].abs() ** 2 + # `stft[..., :-1]` is a non-contiguous view; on some CPU backends the + # downstream `mel_filters.T @ magnitudes` matmul hits a slow strided + # path (observed ~8x slower). Forcing contiguity here is cheap and + # keeps the matmul on the fast path. + magnitudes = (stft[..., :-1].abs() ** 2).contiguous() mel_filters = torch.from_numpy(self.mel_filters).to(device, torch.float32) mel_spec = mel_filters.T @ magnitudes From bf2f2a838dd3a4bb817ed2f6aa1af87aaa7e4ce8 Mon Sep 17 00:00:00 2001 From: vara lakshmi bayanagari Date: Fri, 17 Jul 2026 21:33:19 +0000 Subject: [PATCH 2/6] Added test case to assert and benchmark --- .../test_feature_extraction_whisper.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/models/whisper/test_feature_extraction_whisper.py b/tests/models/whisper/test_feature_extraction_whisper.py index 65f150cc1295..13f44fe00de1 100644 --- a/tests/models/whisper/test_feature_extraction_whisper.py +++ b/tests/models/whisper/test_feature_extraction_whisper.py @@ -16,6 +16,7 @@ import itertools import os import tempfile +import time import unittest import numpy as np @@ -268,6 +269,67 @@ def test_double_precision_pad(self): pt_processed = feature_extractor.pad([{"input_features": inputs}], return_tensors="pt") self.assertTrue(pt_processed.input_features.dtype == torch.float32) + @require_torch + def test_torch_extract_fbank_features_contiguous_magnitudes(self): + """Guards the mel-magnitude contiguity fix. + + `stft[..., :-1]` is a non-contiguous view, and squaring it keeps that + layout. On some CPU backends the downstream `mel_filters.T @ magnitudes` + matmul then falls onto a slow strided-GEMM path (observed ~8x slower on + a ROCm PyTorch build, ~43 ms vs ~3 ms for a 30 s window). Forcing + `magnitudes` contiguous restores the fast path with no change in output. + + Correctness (and the non-contiguity of the raw view) is asserted so this + stays stable in CI; the per-call timings are printed for reference + (run with `-s`) but not asserted, since the size of the speedup is + backend-dependent. + """ + torch.manual_seed(0) + feature_extractor = WhisperFeatureExtractor() + n_fft = feature_extractor.n_fft + hop_length = feature_extractor.hop_length + # Mirror `_torch_extract_fbank_features`: the window is built from n_fft. + window = torch.hann_window(n_fft) + # One full Whisper window of audio (chunk_length * sampling_rate samples). + waveform = torch.randn(feature_extractor.n_samples, dtype=torch.float32) + + stft = torch.stft(waveform, n_fft, hop_length, window=window, return_complex=True) + magnitudes_non_contiguous = stft[..., :-1].abs() ** 2 + magnitudes_contiguous = magnitudes_non_contiguous.contiguous() + + # Root cause of the slow path: the sliced view is non-contiguous. + self.assertFalse(magnitudes_non_contiguous.is_contiguous()) + self.assertTrue(magnitudes_contiguous.is_contiguous()) + + mel_filters = torch.from_numpy(feature_extractor.mel_filters).to(torch.float32) + + # The fix must not change the result, only the memory layout. + torch.testing.assert_close( + mel_filters.T @ magnitudes_non_contiguous, + mel_filters.T @ magnitudes_contiguous, + ) + + def _median_ms(magnitudes, iters=20, warmup=5): + # for _ in range(warmup): + # mel_filters.T @ magnitudes + timings = [] + for _ in range(iters): + start = time.perf_counter() + mel_filters.T @ magnitudes + timings.append((time.perf_counter() - start) * 1e3) + timings.sort() + return np.mean(timings) #timings[len(timings) // 2] + + with torch.inference_mode(): + non_contiguous_ms = _median_ms(magnitudes_non_contiguous) + contiguous_ms = _median_ms(magnitudes_contiguous) + + print( + f"\n[whisper mel matmul] non-contiguous={non_contiguous_ms:.3f} ms " + f"contiguous={contiguous_ms:.3f} ms " + f"speedup={non_contiguous_ms / contiguous_ms:.2f}x" + ) + def _load_datasamples(self, num_samples): ds = load_dataset("hf-internal-testing/librispeech_asr_dummy", "clean", split="validation") # automatic decoding with librispeech From e6debd245ca47f0f81027e06ec1342498f00f405 Mon Sep 17 00:00:00 2001 From: vara lakshmi bayanagari Date: Fri, 17 Jul 2026 22:26:10 +0000 Subject: [PATCH 3/6] enabled warmup in test case --- tests/models/whisper/test_feature_extraction_whisper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/models/whisper/test_feature_extraction_whisper.py b/tests/models/whisper/test_feature_extraction_whisper.py index 13f44fe00de1..aed88b40a22f 100644 --- a/tests/models/whisper/test_feature_extraction_whisper.py +++ b/tests/models/whisper/test_feature_extraction_whisper.py @@ -310,8 +310,8 @@ def test_torch_extract_fbank_features_contiguous_magnitudes(self): ) def _median_ms(magnitudes, iters=20, warmup=5): - # for _ in range(warmup): - # mel_filters.T @ magnitudes + for _ in range(warmup): + mel_filters.T @ magnitudes timings = [] for _ in range(iters): start = time.perf_counter() From dce485bf53f3e0f45ecc827a46b8d4e6d3aa969d Mon Sep 17 00:00:00 2001 From: vbayanag <147658931+vbayanag@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:10:35 -0700 Subject: [PATCH 4/6] Remove median timing function from tests Removed median timing measurement for mel_filters matrix multiplication. --- .../test_feature_extraction_whisper.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/tests/models/whisper/test_feature_extraction_whisper.py b/tests/models/whisper/test_feature_extraction_whisper.py index aed88b40a22f..8942b16e4697 100644 --- a/tests/models/whisper/test_feature_extraction_whisper.py +++ b/tests/models/whisper/test_feature_extraction_whisper.py @@ -309,26 +309,6 @@ def test_torch_extract_fbank_features_contiguous_magnitudes(self): mel_filters.T @ magnitudes_contiguous, ) - def _median_ms(magnitudes, iters=20, warmup=5): - for _ in range(warmup): - mel_filters.T @ magnitudes - timings = [] - for _ in range(iters): - start = time.perf_counter() - mel_filters.T @ magnitudes - timings.append((time.perf_counter() - start) * 1e3) - timings.sort() - return np.mean(timings) #timings[len(timings) // 2] - - with torch.inference_mode(): - non_contiguous_ms = _median_ms(magnitudes_non_contiguous) - contiguous_ms = _median_ms(magnitudes_contiguous) - - print( - f"\n[whisper mel matmul] non-contiguous={non_contiguous_ms:.3f} ms " - f"contiguous={contiguous_ms:.3f} ms " - f"speedup={non_contiguous_ms / contiguous_ms:.2f}x" - ) def _load_datasamples(self, num_samples): ds = load_dataset("hf-internal-testing/librispeech_asr_dummy", "clean", split="validation") From bcba305cb7e6d74a2786e5b8ace0782e8cbfe16b Mon Sep 17 00:00:00 2001 From: vara lakshmi bayanagari Date: Tue, 21 Jul 2026 20:13:31 +0000 Subject: [PATCH 5/6] Apply make style (ruff) Signed-off-by: vara lakshmi bayanagari --- tests/models/whisper/test_feature_extraction_whisper.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/models/whisper/test_feature_extraction_whisper.py b/tests/models/whisper/test_feature_extraction_whisper.py index 8942b16e4697..1747c440071e 100644 --- a/tests/models/whisper/test_feature_extraction_whisper.py +++ b/tests/models/whisper/test_feature_extraction_whisper.py @@ -16,7 +16,6 @@ import itertools import os import tempfile -import time import unittest import numpy as np @@ -309,7 +308,6 @@ def test_torch_extract_fbank_features_contiguous_magnitudes(self): mel_filters.T @ magnitudes_contiguous, ) - def _load_datasamples(self, num_samples): ds = load_dataset("hf-internal-testing/librispeech_asr_dummy", "clean", split="validation") # automatic decoding with librispeech From d7561669aeb062474accaf4ed9b33febb4cb880e Mon Sep 17 00:00:00 2001 From: vara lakshmi bayanagari Date: Wed, 22 Jul 2026 16:00:03 +0000 Subject: [PATCH 6/6] Reference PR #47351 in test docstring; drop stale -s note Signed-off-by: vara lakshmi bayanagari --- tests/models/whisper/test_feature_extraction_whisper.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/models/whisper/test_feature_extraction_whisper.py b/tests/models/whisper/test_feature_extraction_whisper.py index 1747c440071e..f7a9d590619e 100644 --- a/tests/models/whisper/test_feature_extraction_whisper.py +++ b/tests/models/whisper/test_feature_extraction_whisper.py @@ -279,9 +279,8 @@ def test_torch_extract_fbank_features_contiguous_magnitudes(self): `magnitudes` contiguous restores the fast path with no change in output. Correctness (and the non-contiguity of the raw view) is asserted so this - stays stable in CI; the per-call timings are printed for reference - (run with `-s`) but not asserted, since the size of the speedup is - backend-dependent. + stays stable in CI. See https://github.com/huggingface/transformers/pull/47351 + for the benchmark script and the measured (backend-dependent) speedups. """ torch.manual_seed(0) feature_extractor = WhisperFeatureExtractor()