fix(server): reuse Fun-ASR-Nano fallback after vLLM failure (#3402)

Closes #3401.
This commit is contained in:
zhifu gao
2026-07-24 01:33:53 +08:00
committed by GitHub
parent 8d9b34bebc
commit e0fbedfbac
2 changed files with 106 additions and 10 deletions
+6 -4
View File
@@ -150,7 +150,7 @@ def create_app(device: str = "cuda", preload_model: str = "auto", model_path: st
def _load_vllm_engine():
"""Load Fun-ASR-Nano vLLM engine. Falls back to AutoModel if vLLM unavailable."""
if app.state.engine is not None:
if app.state.engine is not None or "fun-asr-nano" in app.state.fallback_models:
return
try:
from funasr.models.fun_asr_nano.inference_vllm import FunASRNanoVLLM
@@ -162,7 +162,7 @@ def create_app(device: str = "cuda", preload_model: str = "auto", model_path: st
# cases, honor the server-level hub selection.
vllm_model = app.state.model_path if app.state.model_path else "FunAudioLLM/Fun-ASR-Nano-2512"
vllm_hub = app.state.hub
app.state.engine = FunASRNanoVLLM.from_pretrained(
engine = FunASRNanoVLLM.from_pretrained(
model=vllm_model,
hub=vllm_hub,
device=device,
@@ -171,10 +171,12 @@ def create_app(device: str = "cuda", preload_model: str = "auto", model_path: st
gpu_memory_utilization=0.5,
)
logger.info(f"vLLM engine ready in {time.time()-t0:.1f}s")
app.state.use_vllm = True
logger.info("Loading VAD model...")
app.state.vad_model = _AutoModel(model="fsmn-vad", device=device, disable_update=True)
vad_model = _AutoModel(model="fsmn-vad", device=device, disable_update=True)
app.state.engine = engine
app.state.vad_model = vad_model
app.state.use_vllm = True
logger.info("VAD ready.")
except Exception as e:
logger.warning(f"vLLM failed ({e}), falling back to AutoModel for fun-asr-nano")
+100 -6
View File
@@ -1,8 +1,11 @@
import asyncio
import importlib.util
import sys
import types
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
SERVICE_PATH = REPO_ROOT / "funasr" / "bin" / "_server_app.py"
@@ -12,12 +15,21 @@ def load_server_app(monkeypatch):
class DummyFastAPI:
def __init__(self, *args, **kwargs):
self.state = types.SimpleNamespace()
self.routes = {}
def post(self, *args, **kwargs):
return lambda func: func
def post(self, path, *args, **kwargs):
def decorator(func):
self.routes[("POST", path)] = func
return func
def get(self, *args, **kwargs):
return lambda func: func
return decorator
def get(self, path, *args, **kwargs):
def decorator(func):
self.routes[("GET", path)] = func
return func
return decorator
fastapi_stub = types.ModuleType("fastapi")
fastapi_stub.FastAPI = DummyFastAPI
@@ -41,14 +53,27 @@ def load_server_app(monkeypatch):
return module
def install_dummy_funasr(monkeypatch):
def install_dummy_funasr(monkeypatch, fail_once_for_models=()):
remaining_failures = {model: 1 for model in fail_once_for_models}
class DummyAutoModel:
instances = []
attempts = []
def __init__(self, **kwargs):
self.__class__.attempts.append(kwargs)
model = kwargs.get("model")
if remaining_failures.get(model, 0):
remaining_failures[model] -= 1
raise RuntimeError(f"{model} unavailable")
self.kwargs = kwargs
self.__class__.instances.append(kwargs)
def generate(self, **kwargs):
if self.kwargs.get("model") == "fsmn-vad":
return [{"value": [[0, 1000]]}]
return [{"text": "transcript"}]
funasr_stub = types.ModuleType("funasr")
funasr_stub.AutoModel = DummyAutoModel
monkeypatch.setitem(sys.modules, "funasr", funasr_stub)
@@ -64,7 +89,10 @@ def install_dummy_vllm(monkeypatch, raise_on_load=False):
cls.calls.append(kwargs)
if raise_on_load:
raise RuntimeError("vllm unavailable")
return object()
return cls()
def generate(self, inputs, **kwargs):
return [{"text": "transcript"} for _ in inputs]
monkeypatch.setitem(sys.modules, "funasr.models", types.ModuleType("funasr.models"))
monkeypatch.setitem(
@@ -76,6 +104,26 @@ def install_dummy_vllm(monkeypatch, raise_on_load=False):
return DummyVLLM
class DummyUpload:
filename = "audio.wav"
async def read(self):
return b"not-a-real-wave-file"
def transcribe_nano(app):
transcribe = app.routes[("POST", "/v1/audio/transcriptions")]
return asyncio.run(
transcribe(
file=DummyUpload(),
model="fun-asr-nano",
language=None,
response_format="json",
spk=False,
)
)
def test_fallback_segments_split_long_fun_asr_server_text(monkeypatch):
module = load_server_app(monkeypatch)
text = (
@@ -150,6 +198,52 @@ def test_default_fun_asr_nano_fallback_uses_requested_modelscope_hub(monkeypatch
assert fallback["hub"] == "ms"
def test_fun_asr_nano_reuses_fallback_after_vllm_failure(monkeypatch):
module = load_server_app(monkeypatch)
DummyAutoModel = install_dummy_funasr(monkeypatch)
DummyVLLM = install_dummy_vllm(monkeypatch, raise_on_load=True)
monkeypatch.setattr(module.sf, "info", lambda path: types.SimpleNamespace(duration=1.0))
app = module.create_app(device="cuda", preload_model="fun-asr-nano", hub="ms")
for _ in range(2):
assert transcribe_nano(app) == {"text": "transcript"}
nano_fallbacks = [
config
for config in DummyAutoModel.instances
if config.get("model") == "FunAudioLLM/Fun-ASR-Nano-2512"
]
assert len(DummyVLLM.calls) == 1
assert len(nano_fallbacks) == 1
def test_partial_vllm_setup_is_not_cached_after_fallback_failure(monkeypatch):
module = load_server_app(monkeypatch)
nano_model = "FunAudioLLM/Fun-ASR-Nano-2512"
DummyAutoModel = install_dummy_funasr(
monkeypatch,
fail_once_for_models=("fsmn-vad", nano_model),
)
DummyVLLM = install_dummy_vllm(monkeypatch)
monkeypatch.setattr(
module.sf,
"read",
lambda stream: (module.np.ones(16000, dtype=module.np.float32), 16000),
)
app = module.create_app(device="cuda", preload_model="sensevoice", hub="ms")
with pytest.raises(RuntimeError, match=f"{nano_model} unavailable"):
transcribe_nano(app)
assert app.state.engine is None
assert transcribe_nano(app) == {"text": "transcript"}
assert len(DummyVLLM.calls) == 2
assert len([attempt for attempt in DummyAutoModel.attempts if attempt.get("model") == "fsmn-vad"]) == 2
def test_custom_model_path_fallback_uses_empty_config_and_requested_hub(monkeypatch):
module = load_server_app(monkeypatch)
DummyAutoModel = install_dummy_funasr(monkeypatch)