[Bugfix][Audio] Restore soundfile-first automatic decoding (#55642)

Signed-off-by: Andreas Karatzas <Andreas.Karatzas@amd.com>
This commit is contained in:
Andreas Karatzas
2026-09-07 06:21:02 +00:00
committed by GitHub
parent ed29dfae6e
commit f7f060d253
3 changed files with 38 additions and 29 deletions
+9 -7
View File
@@ -1210,7 +1210,7 @@ vLLM decodes audio bytes into waveforms using a selectable decoding backend:
| Backend | Description |
| --- | --- |
| `auto` (default) | torchcodec, falling back to soundfile, then PyAV |
| `auto` (default) | soundfile, falling back to torchcodec, then PyAV |
| `soundfile` | libsndfile only, no fallback |
| `pyav` | PyAV (FFmpeg) only, no fallback |
| `torchcodec` | TorchCodec (PyTorch-native) only, no fallback |
@@ -1226,15 +1226,17 @@ vllm serve mistralai/Voxtral-Mini-3B-2507 \
`pyav` drives FFmpeg through a per-frame Python generator, so under
concurrency the Python/C crossings contend on the GIL. `torchcodec`
decodes each stream in a single call that releases the GIL for its whole
duration, which is why `auto` prefers it when many requests decode audio
concurrently, such as when audio tracks are extracted from video.
duration. Select it explicitly for concurrent decoding workloads.
`auto` prefers soundfile for supported formats to preserve their existing
decoding behavior, including encoder padding. Audio extracted from formats
that soundfile cannot read, such as video containers, can use torchcodec
through the fallback chain.
!!! note
`torchcodec` ships as a requirement on CUDA, CPU and XPU builds. On other
platforms (e.g. ROCm, TPU) `auto` falls back to the soundfile → PyAV
chain unless you install it manually. torchcodec also links against a
system FFmpeg installation; if FFmpeg is missing, `auto` falls back the
same way.
platforms (e.g. ROCm, TPU), install it manually to enable that backend.
torchcodec also links against a system FFmpeg installation. If the package
or FFmpeg is unavailable, `auto` uses the soundfile → PyAV chain.
### Embedding Inputs
+8
View File
@@ -193,6 +193,14 @@ def test_load_audio_backend_matches_default(backend, dummy_audio_bytes):
np.testing.assert_allclose(ref_audio[:n], audio[:n], atol=1e-4)
def test_load_audio_default_preserves_vorbis_length(dummy_audio_bytes):
"""Default decoding must not append Vorbis padding to the speech waveform."""
expected, expected_sr = load_audio_soundfile(BytesIO(dummy_audio_bytes), sr=None)
audio, sr = load_audio(BytesIO(dummy_audio_bytes), sr=None)
assert sr == expected_sr
assert audio.shape == expected.shape
def test_load_audio_unknown_backend_rejected(dummy_audio_bytes):
"""An unknown backend must fail loudly instead of silently degrading."""
with pytest.raises(ValueError, match="Unknown audio backend"):
+21 -22
View File
@@ -60,11 +60,11 @@ _BAD_SF_CODES = {0, 1, 3, 4}
# Audio decoding backends selectable via `load_audio(backend=...)` or
# `--media-io-kwargs '{"audio": {"audio_backend": ...}}'`.
# "auto" tries torchcodec, then soundfile, then PyAV.
# "auto" tries soundfile, then torchcodec, then PyAV.
AUDIO_BACKENDS = ("auto", "soundfile", "pyav", "torchcodec")
# Raised as ImportError when the torchcodec backend cannot be used, so
# `load_audio(backend="auto")` falls back to the soundfile → PyAV chain.
# `load_audio(backend="auto")` can continue to PyAV.
_TORCHCODEC_UNAVAILABLE_MSG = (
"torchcodec audio backend is unavailable (requires the torchcodec "
"package and a system ffmpeg installation)"
@@ -413,8 +413,8 @@ def load_audio(
Args:
backend: One of ``AUDIO_BACKENDS``. ``None`` (default) selects
``"auto"``, which tries torchcodec, then falls back to the
soundfile → PyAV chain; the other values select a single
``"auto"``, which tries soundfile, then torchcodec, then PyAV;
the other values select a single
backend with no fallback.
"""
backend = backend or "auto"
@@ -433,27 +433,11 @@ def load_audio(
max_decode_bytes=max_decode_bytes,
)
# "auto": torchcodec → soundfile → PyAV. Each backend is called by its
# Keep soundfile first to preserve decoding and padding of supported formats.
# "auto": soundfile → torchcodec → PyAV. Each backend is called by its
# bare name so tests that monkeypatch it take effect. Only an ImportError
# (backend missing) or, for soundfile, an unsupported-format error defers
# to the next; any other error (decode failure, guard ValueError) raises.
if isinstance(path, BytesIO):
path.seek(0)
try:
return load_audio_torchcodec(
path,
sr=sr,
mono=mono,
max_duration_s=max_duration_s,
max_decode_bytes=max_decode_bytes,
)
except ImportError as exc:
# Decode errors don't defer: torchcodec is FFmpeg-based like PyAV, so
# retrying would just fail the same way.
logger.warning(
"torchcodec unavailable (%r); falling back to soundfile/PyAV.", exc
)
if isinstance(path, BytesIO):
path.seek(0)
try:
@@ -477,6 +461,21 @@ def load_audio(
if exc.code not in _BAD_SF_CODES:
raise
if isinstance(path, BytesIO):
path.seek(0)
try:
return load_audio_torchcodec(
path,
sr=sr,
mono=mono,
max_duration_s=max_duration_s,
max_decode_bytes=max_decode_bytes,
)
except ImportError as exc:
# Decode errors don't defer: torchcodec is FFmpeg-based like PyAV, so
# retrying would just fail the same way.
logger.warning("torchcodec unavailable (%r); falling back to PyAV.", exc)
# PyAV is terminal: nothing left to fall back to. Normalize an FFmpeg
# failure to ValueError; let a missing soundfile/PyAV surface its
# "install vllm[audio]" ImportError.