Files
Live-streaming/tests/test_faster_qwen_worker.py
T

343 lines
10 KiB
Python

import logging
import os
import unittest
from unittest.mock import patch
from app.faster_qwen_worker import (
DEFAULT_CPU_AFFINITY_COUNT,
DEFAULT_CPU_THREADS,
MAX_NEW_TOKENS,
FasterQwenWorkerClient,
FasterQwenWorkerTimeout,
faster_qwen_worker_main,
_generation_kwargs,
_configure_worker_environment,
)
class _FakeConnection:
def __init__(self, responses):
self.responses = list(responses)
self.sent = []
self.closed = False
def poll(self, _timeout):
return bool(self.responses)
def recv(self):
return self.responses.pop(0)
def send(self, message):
self.sent.append(message)
def close(self):
self.closed = True
class _FakeChildConnection:
def close(self):
return None
class _FakeProcess:
_next_pid = 4000
def __init__(self):
type(self)._next_pid += 1
self.pid = type(self)._next_pid
self.alive = False
self.terminated = False
self.killed = False
def start(self):
self.alive = True
def is_alive(self):
return self.alive
def join(self, timeout=None):
return None
def terminate(self):
self.terminated = True
self.alive = False
def kill(self):
self.killed = True
self.alive = False
class _FakeContext:
def __init__(self, connections):
self.connections = list(connections)
self.processes = []
def Pipe(self, duplex=True):
self.assert_duplex = duplex
return self.connections.pop(0), _FakeChildConnection()
def Process(self, **_kwargs):
process = _FakeProcess()
self.processes.append(process)
return process
def _ready(pid):
return {
"type": "ready",
"pid": pid,
"load_ms": 100,
"warmup_ms": 50,
"max_new_tokens": MAX_NEW_TOKENS,
}
def _progress(stage):
return {
"type": "progress",
"stage": stage,
"pid": 4000,
}
def _probe_worker_main(connection, _settings):
connection.send({
"type": "ready",
"pid": os.getpid(),
"load_ms": 1,
"warmup_ms": 1,
"max_new_tokens": MAX_NEW_TOKENS,
})
while True:
message = connection.recv()
if message.get("command") == "stop":
break
if message.get("command") == "synthesize":
audio = b"RIFF-spawn-probe"
connection.send({
"type": "result",
"request_id": message["request_id"],
"audio": audio,
"duration_ms": 2,
"bytes": len(audio),
})
connection.close()
class FasterQwenWorkerTests(unittest.TestCase):
def test_worker_environment_limits_native_thread_pools(self):
names = (
"OMP_NUM_THREADS",
"MKL_NUM_THREADS",
"OPENBLAS_NUM_THREADS",
"NUMEXPR_NUM_THREADS",
"VECLIB_MAXIMUM_THREADS",
"BLIS_NUM_THREADS",
)
with patch.dict(os.environ, {}, clear=False):
threads = _configure_worker_environment({})
self.assertEqual(threads, DEFAULT_CPU_THREADS)
for name in names:
self.assertEqual(os.environ[name], str(DEFAULT_CPU_THREADS))
self.assertEqual(os.environ["TOKENIZERS_PARALLELISM"], "false")
def test_default_worker_resource_limits_are_four_threads_and_eight_cores(self):
self.assertEqual(DEFAULT_CPU_THREADS, 4)
self.assertEqual(DEFAULT_CPU_AFFINITY_COUNT, 8)
def test_worker_environment_accepts_bounded_override(self):
with patch.dict(os.environ, {}, clear=False):
self.assertEqual(_configure_worker_environment({"cpu_threads": 4}), 4)
self.assertEqual(_configure_worker_environment({"cpu_threads": 99}), 8)
def test_generation_is_hard_limited_to_384_tokens(self):
kwargs = _generation_kwargs(
{"language": "Chinese", "non_streaming_mode": True},
"测试",
)
self.assertEqual(MAX_NEW_TOKENS, 384)
self.assertEqual(kwargs["max_new_tokens"], 384)
def test_client_returns_worker_audio(self):
connection = _FakeConnection([
_progress("model_loading"),
_progress("warmup_started"),
_ready(4100),
{
"type": "result",
"request_id": "placeholder",
"audio": b"RIFF-audio",
"duration_ms": 1234,
"bytes": 10,
},
])
context = _FakeContext([connection])
client = FasterQwenWorkerClient(
{},
logging.getLogger("test-faster-qwen-worker"),
context=context,
)
client.ensure_ready()
request_id = "fixed-request-id"
connection.responses[0]["request_id"] = request_id
with patch("app.faster_qwen_worker.uuid.uuid4") as make_uuid:
make_uuid.return_value.hex = request_id
audio, metadata = client.synthesize("测试")
self.assertEqual(audio, b"RIFF-audio")
self.assertEqual(metadata["duration_ms"], 1234)
self.assertEqual(len(context.processes), 1)
client.close()
def test_client_default_startup_timeout_allows_slow_model_load(self):
client = FasterQwenWorkerClient({})
self.assertEqual(client.startup_timeout_seconds, 900.0)
client.close()
def test_worker_applies_process_limits_after_model_warmup(self):
order = []
class FakeModel:
def warmup(self):
order.append("warmup")
class WorkerConnection:
def __init__(self):
self.sent = []
self.closed = False
def send(self, message):
self.sent.append(message)
def recv(self):
return {"command": "stop"}
def close(self):
self.closed = True
connection = WorkerConnection()
with (
patch("app.faster_qwen_worker._configure_worker_environment", return_value=3),
patch(
"app.faster_qwen_worker._load_runtime",
side_effect=lambda _settings, progress=None: (
progress("from_pretrained_started") if progress else None,
order.append("load"),
{"model": FakeModel()},
)[-1],
),
patch(
"app.faster_qwen_worker._apply_worker_process_limits",
side_effect=lambda _settings: order.append("limits") or {
"cpu_affinity_count": 6,
"process_priority": "below_normal",
},
),
patch(
"app.faster_qwen_worker._synthesize_wav",
side_effect=lambda _runtime, _text: order.append("voice_clone") or b"wav",
),
):
faster_qwen_worker_main(connection, {})
self.assertEqual(order, ["load", "warmup", "voice_clone", "limits"])
progress_stages = [
item.get("stage")
for item in connection.sent
if item.get("type") == "progress"
]
self.assertEqual(
progress_stages,
[
"process_started",
"environment_ready",
"model_loading",
"from_pretrained_started",
"model_loaded",
"warmup_started",
"graph_warmup_complete",
"voice_clone_warmup_started",
"voice_clone_warmup_complete",
"warmup_complete",
],
)
self.assertEqual(connection.sent[-1]["type"], "ready")
def test_timeout_terminates_worker_and_starts_a_prewarmed_replacement(self):
first_connection = _FakeConnection([_ready(4200)])
replacement_connection = _FakeConnection([_ready(4300)])
context = _FakeContext([first_connection, replacement_connection])
client = FasterQwenWorkerClient(
{},
logging.getLogger("test-faster-qwen-timeout"),
synthesis_timeout_seconds=120,
context=context,
)
with self.assertRaises(FasterQwenWorkerTimeout):
client.synthesize("会超时的播报")
self.assertEqual(len(context.processes), 2)
self.assertTrue(context.processes[0].terminated)
self.assertTrue(context.processes[1].is_alive())
self.assertEqual(client.worker_pid, context.processes[1].pid)
client.close()
def test_real_spawned_worker_round_trip(self):
client = FasterQwenWorkerClient(
{},
logging.getLogger("test-faster-qwen-spawn"),
startup_timeout_seconds=20,
process_target=_probe_worker_main,
)
audio, metadata = client.synthesize("测试子进程")
self.assertEqual(audio, b"RIFF-spawn-probe")
self.assertEqual(metadata["duration_ms"], 2)
self.assertGreater(client.worker_pid, 0)
client.close()
def test_startup_failure_backoff_blocks_immediate_retry(self):
from app.faster_qwen_worker import (
STARTUP_FAILURE_BACKOFF_SECONDS,
FasterQwenWorkerError,
)
# 启动永远超时(poll 返回 False),触发启动失败
timeout_connection = _FakeConnection([])
timeout_connection.poll = lambda _timeout: False
context = _FakeContext([timeout_connection, _FakeConnection([_ready(4400)])])
client = FasterQwenWorkerClient(
{},
logging.getLogger("test-faster-qwen-backoff"),
startup_timeout_seconds=0.01,
context=context,
)
with self.assertRaises(FasterQwenWorkerError):
client.ensure_ready()
self.assertGreater(client._next_start_after, 0.0)
# 退避期内立即重试应直接报退避错误,且不创建新进程
with self.assertRaises(FasterQwenWorkerError) as ctx:
client.ensure_ready()
self.assertIn("退避", str(ctx.exception))
self.assertEqual(len(context.processes), 1)
# 退避期过后允许重新启动
client._next_start_after = 0.0
client.ensure_ready()
self.assertEqual(len(context.processes), 2)
self.assertEqual(client._next_start_after, 0.0)
self.assertGreater(STARTUP_FAILURE_BACKOFF_SECONDS, 0)
client.close()
if __name__ == "__main__":
unittest.main()