Skip to content

Commit e0fbedf

Browse files
authored
fix(server): reuse Fun-ASR-Nano fallback after vLLM failure (#3402)
Closes #3401.
1 parent 8d9b34b commit e0fbedf

2 files changed

Lines changed: 106 additions & 10 deletions

File tree

funasr/bin/_server_app.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def create_app(device: str = "cuda", preload_model: str = "auto", model_path: st
150150

151151
def _load_vllm_engine():
152152
"""Load Fun-ASR-Nano vLLM engine. Falls back to AutoModel if vLLM unavailable."""
153-
if app.state.engine is not None:
153+
if app.state.engine is not None or "fun-asr-nano" in app.state.fallback_models:
154154
return
155155
try:
156156
from funasr.models.fun_asr_nano.inference_vllm import FunASRNanoVLLM
@@ -162,7 +162,7 @@ def _load_vllm_engine():
162162
# cases, honor the server-level hub selection.
163163
vllm_model = app.state.model_path if app.state.model_path else "FunAudioLLM/Fun-ASR-Nano-2512"
164164
vllm_hub = app.state.hub
165-
app.state.engine = FunASRNanoVLLM.from_pretrained(
165+
engine = FunASRNanoVLLM.from_pretrained(
166166
model=vllm_model,
167167
hub=vllm_hub,
168168
device=device,
@@ -171,10 +171,12 @@ def _load_vllm_engine():
171171
gpu_memory_utilization=0.5,
172172
)
173173
logger.info(f"vLLM engine ready in {time.time()-t0:.1f}s")
174-
app.state.use_vllm = True
175174

176175
logger.info("Loading VAD model...")
177-
app.state.vad_model = _AutoModel(model="fsmn-vad", device=device, disable_update=True)
176+
vad_model = _AutoModel(model="fsmn-vad", device=device, disable_update=True)
177+
app.state.engine = engine
178+
app.state.vad_model = vad_model
179+
app.state.use_vllm = True
178180
logger.info("VAD ready.")
179181
except Exception as e:
180182
logger.warning(f"vLLM failed ({e}), falling back to AutoModel for fun-asr-nano")

tests/test_server_app_openai_segments.py

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import asyncio
12
import importlib.util
23
import sys
34
import types
45
from pathlib import Path
56

7+
import pytest
8+
69

710
REPO_ROOT = Path(__file__).resolve().parents[1]
811
SERVICE_PATH = REPO_ROOT / "funasr" / "bin" / "_server_app.py"
@@ -12,12 +15,21 @@ def load_server_app(monkeypatch):
1215
class DummyFastAPI:
1316
def __init__(self, *args, **kwargs):
1417
self.state = types.SimpleNamespace()
18+
self.routes = {}
19+
20+
def post(self, path, *args, **kwargs):
21+
def decorator(func):
22+
self.routes[("POST", path)] = func
23+
return func
1524

16-
def post(self, *args, **kwargs):
17-
return lambda func: func
25+
return decorator
1826

19-
def get(self, *args, **kwargs):
20-
return lambda func: func
27+
def get(self, path, *args, **kwargs):
28+
def decorator(func):
29+
self.routes[("GET", path)] = func
30+
return func
31+
32+
return decorator
2133

2234
fastapi_stub = types.ModuleType("fastapi")
2335
fastapi_stub.FastAPI = DummyFastAPI
@@ -41,14 +53,27 @@ def get(self, *args, **kwargs):
4153
return module
4254

4355

44-
def install_dummy_funasr(monkeypatch):
56+
def install_dummy_funasr(monkeypatch, fail_once_for_models=()):
57+
remaining_failures = {model: 1 for model in fail_once_for_models}
58+
4559
class DummyAutoModel:
4660
instances = []
61+
attempts = []
4762

4863
def __init__(self, **kwargs):
64+
self.__class__.attempts.append(kwargs)
65+
model = kwargs.get("model")
66+
if remaining_failures.get(model, 0):
67+
remaining_failures[model] -= 1
68+
raise RuntimeError(f"{model} unavailable")
4969
self.kwargs = kwargs
5070
self.__class__.instances.append(kwargs)
5171

72+
def generate(self, **kwargs):
73+
if self.kwargs.get("model") == "fsmn-vad":
74+
return [{"value": [[0, 1000]]}]
75+
return [{"text": "transcript"}]
76+
5277
funasr_stub = types.ModuleType("funasr")
5378
funasr_stub.AutoModel = DummyAutoModel
5479
monkeypatch.setitem(sys.modules, "funasr", funasr_stub)
@@ -64,7 +89,10 @@ def from_pretrained(cls, **kwargs):
6489
cls.calls.append(kwargs)
6590
if raise_on_load:
6691
raise RuntimeError("vllm unavailable")
67-
return object()
92+
return cls()
93+
94+
def generate(self, inputs, **kwargs):
95+
return [{"text": "transcript"} for _ in inputs]
6896

6997
monkeypatch.setitem(sys.modules, "funasr.models", types.ModuleType("funasr.models"))
7098
monkeypatch.setitem(
@@ -76,6 +104,26 @@ def from_pretrained(cls, **kwargs):
76104
return DummyVLLM
77105

78106

107+
class DummyUpload:
108+
filename = "audio.wav"
109+
110+
async def read(self):
111+
return b"not-a-real-wave-file"
112+
113+
114+
def transcribe_nano(app):
115+
transcribe = app.routes[("POST", "/v1/audio/transcriptions")]
116+
return asyncio.run(
117+
transcribe(
118+
file=DummyUpload(),
119+
model="fun-asr-nano",
120+
language=None,
121+
response_format="json",
122+
spk=False,
123+
)
124+
)
125+
126+
79127
def test_fallback_segments_split_long_fun_asr_server_text(monkeypatch):
80128
module = load_server_app(monkeypatch)
81129
text = (
@@ -150,6 +198,52 @@ def test_default_fun_asr_nano_fallback_uses_requested_modelscope_hub(monkeypatch
150198
assert fallback["hub"] == "ms"
151199

152200

201+
def test_fun_asr_nano_reuses_fallback_after_vllm_failure(monkeypatch):
202+
module = load_server_app(monkeypatch)
203+
DummyAutoModel = install_dummy_funasr(monkeypatch)
204+
DummyVLLM = install_dummy_vllm(monkeypatch, raise_on_load=True)
205+
monkeypatch.setattr(module.sf, "info", lambda path: types.SimpleNamespace(duration=1.0))
206+
207+
app = module.create_app(device="cuda", preload_model="fun-asr-nano", hub="ms")
208+
209+
for _ in range(2):
210+
assert transcribe_nano(app) == {"text": "transcript"}
211+
212+
nano_fallbacks = [
213+
config
214+
for config in DummyAutoModel.instances
215+
if config.get("model") == "FunAudioLLM/Fun-ASR-Nano-2512"
216+
]
217+
assert len(DummyVLLM.calls) == 1
218+
assert len(nano_fallbacks) == 1
219+
220+
221+
def test_partial_vllm_setup_is_not_cached_after_fallback_failure(monkeypatch):
222+
module = load_server_app(monkeypatch)
223+
nano_model = "FunAudioLLM/Fun-ASR-Nano-2512"
224+
DummyAutoModel = install_dummy_funasr(
225+
monkeypatch,
226+
fail_once_for_models=("fsmn-vad", nano_model),
227+
)
228+
DummyVLLM = install_dummy_vllm(monkeypatch)
229+
monkeypatch.setattr(
230+
module.sf,
231+
"read",
232+
lambda stream: (module.np.ones(16000, dtype=module.np.float32), 16000),
233+
)
234+
235+
app = module.create_app(device="cuda", preload_model="sensevoice", hub="ms")
236+
237+
with pytest.raises(RuntimeError, match=f"{nano_model} unavailable"):
238+
transcribe_nano(app)
239+
240+
assert app.state.engine is None
241+
242+
assert transcribe_nano(app) == {"text": "transcript"}
243+
assert len(DummyVLLM.calls) == 2
244+
assert len([attempt for attempt in DummyAutoModel.attempts if attempt.get("model") == "fsmn-vad"]) == 2
245+
246+
153247
def test_custom_model_path_fallback_uses_empty_config_and_requested_hub(monkeypatch):
154248
module = load_server_app(monkeypatch)
155249
DummyAutoModel = install_dummy_funasr(monkeypatch)

0 commit comments

Comments
 (0)