mirror of
https://github.com/modelscope/FunASR.git
synced 2026-10-12 02:19:18 +00:00
282 lines
12 KiB
Python
282 lines
12 KiB
Python
"""CPU-only regressions using real AutoModel construction and inference orchestration.
|
|
|
|
Accelerator availability is mocked only to exercise configuration labels. Stand-in
|
|
models retain CPU weights; these tests do not claim CUDA/MPS hardware execution.
|
|
"""
|
|
|
|
import copy
|
|
import logging
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
|
|
from funasr.auto.auto_model import AutoModel
|
|
from funasr.register import tables
|
|
|
|
|
|
class RecordingModel(torch.nn.Module):
|
|
def __init__(self, model, **kwargs):
|
|
super().__init__()
|
|
self.weight = torch.nn.Parameter(torch.zeros(1))
|
|
self.role = model
|
|
self.calls = []
|
|
|
|
def to(self, device):
|
|
self.placement = str(device)
|
|
return self # Configuration probe only: weights always stay on CPU.
|
|
|
|
def inference(self, data_in, key=None, **kwargs):
|
|
if kwargs["device"] != self.placement:
|
|
raise AssertionError(f"{self.role}: feature device differs from model placement")
|
|
if kwargs["device"] == "cpu":
|
|
features = torch.zeros(1, device=kwargs["device"])
|
|
if features.device != self.weight.device:
|
|
raise AssertionError("CPU feature/weight mismatch")
|
|
self.calls.append(copy.deepcopy(kwargs))
|
|
results = []
|
|
for index, _ in enumerate(data_in):
|
|
if self.role == "device-test-vad":
|
|
result = {"key": key[index], "value": [[0, 1000]]}
|
|
elif self.role == "device-test-spk":
|
|
result = {"spk_embedding": torch.ones(1, 4)}
|
|
elif self.role == "device-test-punc":
|
|
result = {"text": "hello.", "punc_array": [2]}
|
|
else:
|
|
result = {"text": "hello", "timestamp": [[0, 1000]]}
|
|
results.append(result)
|
|
return results, {"batch_data_time": 1}
|
|
|
|
|
|
class TestSubmodelDevice(unittest.TestCase):
|
|
def setUp(self):
|
|
self.old_threads = torch.get_num_threads()
|
|
self.addCleanup(torch.set_num_threads, self.old_threads)
|
|
registry = {f"device-test-{role}": RecordingModel for role in ("asr", "vad", "punc", "spk")}
|
|
self.start_patch(patch.dict(tables.model_classes, registry))
|
|
self.start_patch(patch("torch.cuda.is_available", return_value=True))
|
|
self.start_patch(patch("funasr.auto.auto_model.ClusterBackend", create=True))
|
|
# Reject accidental model-hub access. model_conf={} uses the local registry.
|
|
self.start_patch(
|
|
patch("funasr.auto.auto_model.download_model", side_effect=AssertionError("network"))
|
|
)
|
|
self.audio = np.zeros(16000, dtype=np.float32)
|
|
|
|
def start_patch(self, patcher):
|
|
value = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
return value
|
|
|
|
def make_model(self, with_vad=True, device="cuda:0", configs=None):
|
|
configs = (
|
|
configs
|
|
if configs is not None
|
|
else {
|
|
role: {"model_conf": {}, "device": "cpu", f"{role}_only": "kept"}
|
|
for role in ("vad", "punc", "spk")
|
|
}
|
|
)
|
|
kwargs = dict(
|
|
model="device-test-asr",
|
|
model_conf={},
|
|
device=device,
|
|
ncpu=1,
|
|
disable_update=True,
|
|
disable_pbar=True,
|
|
return_spk_res=False,
|
|
)
|
|
for role in ("vad", "punc", "spk") if with_vad else ("punc",):
|
|
kwargs[f"{role}_model"] = f"device-test-{role}"
|
|
kwargs[f"{role}_kwargs"] = configs[role]
|
|
return AutoModel(**kwargs)
|
|
|
|
def test_explicit_devices_and_caller_configs_survive_two_calls(self):
|
|
configs = {
|
|
role: {"model_conf": {"nested": [1]}, "device": "cpu", f"{role}_only": "kept"}
|
|
for role in ("vad", "punc", "spk")
|
|
}
|
|
original = copy.deepcopy(configs)
|
|
model = self.make_model(configs=configs)
|
|
self.assertEqual(configs, original)
|
|
first = model.generate(self.audio, device="cuda:7", hotword="runtime", batch_size_s=2)
|
|
second = model.generate(self.audio)
|
|
self.assertEqual(first[0]["text"], "hello.")
|
|
self.assertEqual(second[0]["text"], "hello.")
|
|
for role in ("vad", "punc", "spk"):
|
|
child = getattr(model, f"{role}_model")
|
|
self.assertEqual([call["device"] for call in child.calls], ["cpu", "cpu"])
|
|
self.assertEqual(child.calls[0][f"{role}_only"], "kept")
|
|
self.assertEqual(child.calls[0]["hotword"], "runtime")
|
|
self.assertEqual(child.calls[0]["batch_size_s"], 2)
|
|
self.assertNotIn("hotword", child.calls[1])
|
|
self.assertNotIn("batch_size_s", child.calls[1])
|
|
self.assertEqual(getattr(model, f"{role}_kwargs")["device"], "cpu")
|
|
self.assertEqual([call["device"] for call in model.model.calls], ["cuda:0", "cuda:0"])
|
|
self.assertEqual(configs, original)
|
|
|
|
def test_punctuation_without_vad_preserves_placement(self):
|
|
model = self.make_model(with_vad=False)
|
|
model.generate(self.audio, device="cuda:7", hotword="runtime")
|
|
model.generate(self.audio)
|
|
self.assertEqual([call["device"] for call in model.punc_model.calls], ["cpu", "cpu"])
|
|
self.assertEqual(model.punc_model.calls[0]["hotword"], "runtime")
|
|
self.assertNotIn("hotword", model.punc_model.calls[1])
|
|
|
|
def test_omitted_devices_inherit_resolved_asr_device(self):
|
|
for available, expected in ((True, "cuda:0"), (False, "cpu")):
|
|
with self.subTest(available=available), patch(
|
|
"torch.cuda.is_available", return_value=available
|
|
):
|
|
configs = {role: {"model_conf": {}} for role in ("vad", "punc", "spk")}
|
|
model = self.make_model(configs=configs)
|
|
model.generate(self.audio, device="cuda:7")
|
|
model.generate(self.audio)
|
|
for role in ("vad", "punc", "spk"):
|
|
child = getattr(model, f"{role}_model")
|
|
self.assertEqual(child.placement, expected)
|
|
self.assertEqual([call["device"] for call in child.calls], [expected, expected])
|
|
self.assertEqual(configs, {role: {"model_conf": {}} for role in configs})
|
|
|
|
def test_unavailable_explicit_submodel_devices_fall_back_to_cpu(self):
|
|
with patch("torch.cuda.is_available", return_value=False):
|
|
configs = {
|
|
role: {"model_conf": {}, "device": "cuda:0"} for role in ("vad", "punc", "spk")
|
|
}
|
|
model = self.make_model(device="cpu", configs=configs)
|
|
model.generate(self.audio, device="cuda:7")
|
|
model.generate(self.audio)
|
|
for role in configs:
|
|
self.assertEqual(getattr(model, f"{role}_model").placement, "cpu")
|
|
self.assertEqual(
|
|
[call["device"] for call in getattr(model, f"{role}_model").calls], ["cpu", "cpu"]
|
|
)
|
|
self.assertEqual(configs[role]["device"], "cuda:0")
|
|
|
|
|
|
@pytest.fixture
|
|
def fallback_model(monkeypatch):
|
|
from funasr.auto.auto_model import _current_blas_threads, _limit_blas_threads
|
|
|
|
original_threads = torch.get_num_threads()
|
|
original_blas = _current_blas_threads()
|
|
for role in ("asr", "vad", "punc", "spk"):
|
|
monkeypatch.setitem(tables.model_classes, f"device-test-{role}", RecordingModel)
|
|
|
|
def reject_download(**kwargs):
|
|
raise AssertionError("device tests must not access model hubs")
|
|
|
|
monkeypatch.setattr("funasr.auto.auto_model.download_model", reject_download)
|
|
with patch("funasr.auto.auto_model.ClusterBackend", create=True):
|
|
def build(**kwargs):
|
|
return AutoModel(
|
|
model="device-test-asr",
|
|
model_conf={},
|
|
ncpu=1,
|
|
disable_update=True,
|
|
disable_pbar=True,
|
|
**kwargs,
|
|
)
|
|
|
|
yield build
|
|
if original_blas is not None:
|
|
_limit_blas_threads(original_blas)
|
|
torch.set_num_threads(original_threads)
|
|
|
|
|
|
_ACCELERATOR_PROBES = [
|
|
("cuda:0", "torch.cuda.is_available"),
|
|
("xpu:0", "torch.xpu.is_available"),
|
|
("mps", "torch.backends.mps.is_available"),
|
|
("npu:0", "funasr.auto.auto_model.is_npu_available"),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("device,probe", _ACCELERATOR_PROBES)
|
|
def test_unavailable_accelerator_warns_and_preserves_cpu_fallback(
|
|
fallback_model, caplog, device, probe
|
|
):
|
|
with patch(probe, return_value=False), caplog.at_level(logging.WARNING):
|
|
model = fallback_model(device=device, batch_size=8)
|
|
assert model.kwargs["device"] == model.model.placement == "cpu"
|
|
assert model.kwargs["batch_size"] == 1
|
|
assert len(caplog.records) == 1
|
|
assert caplog.records[0].levelno == logging.WARNING
|
|
message = caplog.records[0].getMessage()
|
|
assert device in message
|
|
assert "device-test-asr" in message
|
|
assert "falling back to CPU" in message
|
|
assert "PyTorch" in message
|
|
|
|
|
|
@pytest.mark.parametrize("device,probe", _ACCELERATOR_PROBES)
|
|
def test_available_accelerator_does_not_warn(fallback_model, caplog, device, probe):
|
|
with patch(probe, return_value=True), caplog.at_level(logging.WARNING):
|
|
model = fallback_model(device=device, batch_size=8)
|
|
assert model.kwargs["device"] == model.model.placement == device
|
|
assert model.kwargs["batch_size"] == 8
|
|
assert not caplog.records
|
|
|
|
|
|
@pytest.mark.parametrize("device,probe", _ACCELERATOR_PROBES)
|
|
@pytest.mark.parametrize("available", [False, True])
|
|
def test_ngpu_zero_is_intentional_cpu_use(
|
|
fallback_model, caplog, device, probe, available
|
|
):
|
|
with patch(probe, return_value=available), caplog.at_level(logging.WARNING):
|
|
model = fallback_model(device=device, ngpu=0, batch_size=8)
|
|
assert model.kwargs["device"] == model.model.placement == "cpu"
|
|
assert model.kwargs["batch_size"] == 1
|
|
assert not caplog.records
|
|
|
|
|
|
def test_explicit_cpu_does_not_warn(fallback_model, caplog):
|
|
with caplog.at_level(logging.WARNING):
|
|
model = fallback_model(device="cpu", batch_size=8)
|
|
assert model.kwargs["device"] == model.model.placement == "cpu"
|
|
assert model.kwargs["batch_size"] == 8
|
|
assert not caplog.records
|
|
|
|
|
|
def test_default_cuda_fallback_warns_once_with_inherited_submodels(fallback_model, caplog):
|
|
configs = {
|
|
f"{role}_{kind}": f"device-test-{role}" if kind == "model" else {"model_conf": {}}
|
|
for role in ("vad", "punc", "spk")
|
|
for kind in ("model", "kwargs")
|
|
}
|
|
original = copy.deepcopy(configs)
|
|
with patch("torch.cuda.is_available", return_value=False), caplog.at_level(logging.WARNING):
|
|
model = fallback_model(**configs)
|
|
assert model.kwargs["device"] == "cpu"
|
|
for role in ("vad", "punc", "spk"):
|
|
assert getattr(model, f"{role}_kwargs")["device"] == "cpu"
|
|
assert getattr(model, f"{role}_model").placement == "cpu"
|
|
assert configs == original
|
|
assert len(caplog.records) == 1
|
|
assert "cuda" in caplog.records[0].getMessage()
|
|
|
|
|
|
def test_explicit_submodel_fallback_identifies_the_submodel(fallback_model, caplog):
|
|
config = {"model_conf": {}, "device": "cuda:0"}
|
|
original = copy.deepcopy(config)
|
|
with patch("torch.cuda.is_available", return_value=False), caplog.at_level(logging.WARNING):
|
|
model = fallback_model(
|
|
device="cpu", punc_model="device-test-punc", punc_kwargs=config
|
|
)
|
|
assert model.punc_kwargs["device"] == model.punc_model.placement == "cpu"
|
|
assert config == original
|
|
assert len(caplog.records) == 1
|
|
assert "device-test-punc" in caplog.records[0].getMessage()
|
|
|
|
|
|
def test_fallback_respects_application_logging_level(fallback_model, caplog):
|
|
with patch("torch.cuda.is_available", return_value=False), caplog.at_level(logging.ERROR):
|
|
model = fallback_model(device="cuda:0")
|
|
assert model.kwargs["device"] == "cpu"
|
|
assert not caplog.records
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|