Files
FunASR/tests/test_fun_asr_nano_repetition_penalty.py
Leoy b7b952d3aa fix: avoid CUDA crash from repetition_penalty in Fun-ASR-Nano vLLM prompt-embeds mode (#2948) (#2974)
The Fun-ASR-Nano vLLM serving paths run with enable_prompt_embeds=True, so
requests carry audio/text embeddings and have no prompt token IDs. vLLM applies
repetition_penalty by scattering over the prompt token IDs, so any value other
than 1.0 indexes an empty tensor and aborts the engine with a CUDA
"scatter gather index out of bounds" assertion (issue #2948).

The batch generate() entry already defaulted to 1.0, but the OpenAI/REST server
(_server_app), the pipeline path and the streaming path still hardcoded
repetition_penalty=1.3 and crashed. Centralize the rule in a dependency-free
helper (resolve_repetition_penalty) that forces the neutral value in
prompt-embeds mode and warns once, and route every Fun-ASR-Nano SamplingParams
through it. The pipeline and streaming paths now also honor a caller-supplied
repetition_penalty (sanitized) instead of ignoring it.

Adds tests/test_fun_asr_nano_repetition_penalty.py (no GPU or vLLM required).
2026-06-15 09:31:04 -07:00

75 lines
2.6 KiB
Python

"""Unit tests for Fun-ASR-Nano vLLM repetition-penalty handling.
Regression guard for issue #2948: a repetition penalty other than 1.0 is
incompatible with vLLM prompt-embeds mode and aborts the engine with a CUDA
"scatter gather index out of bounds" assertion. The serving paths must never
forward such a value to ``SamplingParams`` while ``enable_prompt_embeds=True``.
The helper is dependency-free, so these tests run without a GPU or vLLM.
"""
import logging
import unittest
from funasr.models.fun_asr_nano import vllm_utils
from funasr.models.fun_asr_nano.vllm_utils import (
NEUTRAL_REPETITION_PENALTY,
resolve_repetition_penalty,
)
class TestResolveRepetitionPenalty(unittest.TestCase):
def setUp(self):
# Reset the once-per-process warning flag so each test is independent.
vllm_utils._warned_prompt_embeds = False
def test_neutral_value_passes_through(self):
self.assertEqual(resolve_repetition_penalty(1.0), 1.0)
def test_none_maps_to_neutral(self):
self.assertEqual(
resolve_repetition_penalty(None), NEUTRAL_REPETITION_PENALTY
)
def test_nonneutral_is_clamped_in_prompt_embeds_mode(self):
# The exact value that triggers the #2948 crash.
self.assertEqual(
resolve_repetition_penalty(1.3, prompt_embeds=True),
NEUTRAL_REPETITION_PENALTY,
)
def test_nonneutral_preserved_for_token_prompts(self):
# Regular token-prompt decoding can safely apply the penalty.
self.assertEqual(
resolve_repetition_penalty(1.3, prompt_embeds=False), 1.3
)
def test_warns_once_in_prompt_embeds_mode(self):
with self.assertLogs(vllm_utils.logger, level=logging.WARNING) as cm:
resolve_repetition_penalty(1.3, prompt_embeds=True)
# Subsequent clamps must not emit additional warnings.
resolve_repetition_penalty(1.5, prompt_embeds=True)
self.assertEqual(len(cm.records), 1)
self.assertIn("2948", cm.output[0])
def test_no_warning_when_value_is_safe(self):
# Capture records directly (assertNoLogs is only available on 3.10+).
records = []
class _Collect(logging.Handler):
def emit(self, record):
records.append(record)
handler = _Collect(level=logging.WARNING)
vllm_utils.logger.addHandler(handler)
try:
resolve_repetition_penalty(1.0, prompt_embeds=True)
resolve_repetition_penalty(1.3, prompt_embeds=False)
finally:
vllm_utils.logger.removeHandler(handler)
self.assertEqual(records, [])
if __name__ == "__main__":
unittest.main()