Skip to content

Commit b7b952d

Browse files
authored
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).
1 parent 5a37a31 commit b7b952d

6 files changed

Lines changed: 155 additions & 6 deletions

File tree

funasr/bin/_server_app.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,10 @@ def _process_vllm(audio_data, sr, language=None, hotwords=None, use_spk=False):
132132
if not seg_audios:
133133
return {"text": "", "segments": [], "duration": len(audio_data)/sr}
134134

135-
# vLLM generate with repetition_penalty
136-
gen_kwargs = {"max_new_tokens": 500, "repetition_penalty": 1.3}
135+
# repetition_penalty is left at the neutral 1.0: the Fun-ASR-Nano vLLM
136+
# engine runs in prompt-embeds mode, where any other value crashes the
137+
# CUDA kernel (see issue #2948 and fun_asr_nano.vllm_utils).
138+
gen_kwargs = {"max_new_tokens": 500, "repetition_penalty": 1.0}
137139
if language:
138140
gen_kwargs["language"] = language
139141
if hotwords:

funasr/models/fun_asr_nano/inference_vllm.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,8 @@ def generate(
527527
except ImportError:
528528
from vllm.inputs.data import EmbedsPrompt
529529

530+
from funasr.models.fun_asr_nano.vllm_utils import resolve_repetition_penalty
531+
530532
if isinstance(inputs, (str, np.ndarray, torch.Tensor)):
531533
inputs = [inputs]
532534

@@ -535,7 +537,8 @@ def generate(
535537
temperature=temperature,
536538
top_p=top_p,
537539
top_k=top_k if top_k > 0 else -1,
538-
repetition_penalty=repetition_penalty,
540+
# Prompt-embeds mode has no token IDs to penalize; see #2948.
541+
repetition_penalty=resolve_repetition_penalty(repetition_penalty),
539542
skip_special_tokens=True,
540543
)
541544

funasr/models/fun_asr_nano/inference_vllm_pipeline.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ def _process_one(self, audio_path, **kwargs):
204204
except ImportError:
205205
from vllm.inputs.data import EmbedsPrompt
206206

207+
from funasr.models.fun_asr_nano.vllm_utils import resolve_repetition_penalty
208+
207209
prompts = []
208210
for seg_audio in segment_audios:
209211
seg_tensor = torch.from_numpy(seg_audio).float()
@@ -219,7 +221,10 @@ def _process_one(self, audio_path, **kwargs):
219221
params = SamplingParams(
220222
max_tokens=kwargs.get("max_new_tokens", 512),
221223
temperature=0.0,
222-
repetition_penalty=1.3,
224+
# Prompt-embeds mode has no token IDs to penalize; see #2948.
225+
repetition_penalty=resolve_repetition_penalty(
226+
kwargs.get("repetition_penalty", 1.0)
227+
),
223228
skip_special_tokens=True,
224229
)
225230

funasr/models/fun_asr_nano/inference_vllm_streaming.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,15 @@ def streaming_generate(self, audio_input, chunk_ms=None, rollback_chars=None,
234234
chunk_samples = int(self.sample_rate * chunk_ms / 1000)
235235
num_chunks = (total_samples + chunk_samples - 1) // chunk_samples
236236

237-
params = SamplingParams(max_tokens=max_new_tokens, temperature=temperature,
238-
repetition_penalty=1.3, skip_special_tokens=True)
237+
from funasr.models.fun_asr_nano.vllm_utils import resolve_repetition_penalty
238+
239+
# Prompt-embeds mode has no token IDs to penalize; see #2948.
240+
params = SamplingParams(
241+
max_tokens=max_new_tokens, temperature=temperature,
242+
repetition_penalty=resolve_repetition_penalty(
243+
kwargs.get("repetition_penalty", 1.0)
244+
),
245+
skip_special_tokens=True)
239246

240247
# Two-stage approach for long audio:
241248
# Stage 1: batch first N chunks fresh (no prev_text) to find stable output
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Helpers shared by the Fun-ASR-Nano vLLM serving paths.
2+
3+
Kept dependency-free (standard library only) so it can be imported and unit
4+
tested without a CUDA device or a vLLM installation.
5+
"""
6+
7+
import logging
8+
9+
logger = logging.getLogger("funasr.fun_asr_nano.vllm")
10+
11+
# A repetition penalty of 1.0 is the identity value, i.e. "no penalty".
12+
NEUTRAL_REPETITION_PENALTY = 1.0
13+
14+
# Warn only once per process so streaming/batch loops do not spam the log.
15+
_warned_prompt_embeds = False
16+
17+
18+
def resolve_repetition_penalty(repetition_penalty, *, prompt_embeds=True):
19+
"""Return a repetition penalty that is safe for the requested vLLM mode.
20+
21+
Fun-ASR-Nano feeds vLLM precomputed audio/text *embeddings* with
22+
``enable_prompt_embeds=True``. In that mode a request carries no prompt
23+
token IDs. vLLM applies ``repetition_penalty`` by scattering over the
24+
prompt's token IDs, so any value other than 1.0 indexes an empty token-id
25+
tensor and aborts the engine with a CUDA
26+
``scatter gather kernel index out of bounds`` assertion (issue #2948).
27+
28+
When ``prompt_embeds`` is True we therefore force the penalty back to the
29+
neutral value and warn once. With ``prompt_embeds=False`` (regular
30+
token-prompt decoding) the requested value is passed through unchanged.
31+
32+
Args:
33+
repetition_penalty: Penalty requested by the caller. ``None`` is
34+
treated as "unset" and maps to the neutral value.
35+
prompt_embeds: Whether the request runs in vLLM prompt-embeds mode.
36+
37+
Returns:
38+
A repetition penalty that will not crash the engine.
39+
"""
40+
global _warned_prompt_embeds
41+
42+
if repetition_penalty is None:
43+
return NEUTRAL_REPETITION_PENALTY
44+
45+
if prompt_embeds and repetition_penalty != NEUTRAL_REPETITION_PENALTY:
46+
if not _warned_prompt_embeds:
47+
logger.warning(
48+
"repetition_penalty=%s is not supported in vLLM prompt-embeds "
49+
"mode (no prompt token IDs to penalize) and would trigger a CUDA "
50+
"scatter index-out-of-bounds crash; using repetition_penalty=%s "
51+
"instead. See https://github.com/modelscope/FunASR/issues/2948.",
52+
repetition_penalty,
53+
NEUTRAL_REPETITION_PENALTY,
54+
)
55+
_warned_prompt_embeds = True
56+
return NEUTRAL_REPETITION_PENALTY
57+
58+
return repetition_penalty
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Unit tests for Fun-ASR-Nano vLLM repetition-penalty handling.
2+
3+
Regression guard for issue #2948: a repetition penalty other than 1.0 is
4+
incompatible with vLLM prompt-embeds mode and aborts the engine with a CUDA
5+
"scatter gather index out of bounds" assertion. The serving paths must never
6+
forward such a value to ``SamplingParams`` while ``enable_prompt_embeds=True``.
7+
8+
The helper is dependency-free, so these tests run without a GPU or vLLM.
9+
"""
10+
11+
import logging
12+
import unittest
13+
14+
from funasr.models.fun_asr_nano import vllm_utils
15+
from funasr.models.fun_asr_nano.vllm_utils import (
16+
NEUTRAL_REPETITION_PENALTY,
17+
resolve_repetition_penalty,
18+
)
19+
20+
21+
class TestResolveRepetitionPenalty(unittest.TestCase):
22+
def setUp(self):
23+
# Reset the once-per-process warning flag so each test is independent.
24+
vllm_utils._warned_prompt_embeds = False
25+
26+
def test_neutral_value_passes_through(self):
27+
self.assertEqual(resolve_repetition_penalty(1.0), 1.0)
28+
29+
def test_none_maps_to_neutral(self):
30+
self.assertEqual(
31+
resolve_repetition_penalty(None), NEUTRAL_REPETITION_PENALTY
32+
)
33+
34+
def test_nonneutral_is_clamped_in_prompt_embeds_mode(self):
35+
# The exact value that triggers the #2948 crash.
36+
self.assertEqual(
37+
resolve_repetition_penalty(1.3, prompt_embeds=True),
38+
NEUTRAL_REPETITION_PENALTY,
39+
)
40+
41+
def test_nonneutral_preserved_for_token_prompts(self):
42+
# Regular token-prompt decoding can safely apply the penalty.
43+
self.assertEqual(
44+
resolve_repetition_penalty(1.3, prompt_embeds=False), 1.3
45+
)
46+
47+
def test_warns_once_in_prompt_embeds_mode(self):
48+
with self.assertLogs(vllm_utils.logger, level=logging.WARNING) as cm:
49+
resolve_repetition_penalty(1.3, prompt_embeds=True)
50+
# Subsequent clamps must not emit additional warnings.
51+
resolve_repetition_penalty(1.5, prompt_embeds=True)
52+
self.assertEqual(len(cm.records), 1)
53+
self.assertIn("2948", cm.output[0])
54+
55+
def test_no_warning_when_value_is_safe(self):
56+
# Capture records directly (assertNoLogs is only available on 3.10+).
57+
records = []
58+
59+
class _Collect(logging.Handler):
60+
def emit(self, record):
61+
records.append(record)
62+
63+
handler = _Collect(level=logging.WARNING)
64+
vllm_utils.logger.addHandler(handler)
65+
try:
66+
resolve_repetition_penalty(1.0, prompt_embeds=True)
67+
resolve_repetition_penalty(1.3, prompt_embeds=False)
68+
finally:
69+
vllm_utils.logger.removeHandler(handler)
70+
self.assertEqual(records, [])
71+
72+
73+
if __name__ == "__main__":
74+
unittest.main()

0 commit comments

Comments
 (0)