[Bugfix][TTS] Make CosyVoice3 TRT plan publication concurrency-safe (#6955)

Signed-off-by: BANANASJIM <bananasjim1@gmail.com>
This commit is contained in:
Jim Ban authored and GitHub committed 2026-09-03 06:44:42 +00:00
1 parent 2b47faf601
commit 711daacabf
2 files changed
+148 -5

No files matched your search

@@ -0,0 +1,125 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import threading
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import pytest
from vllm_omni.model_executor.models.cosyvoice3 import flow_estimator_trt
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _temporary_plans(plan_path: Path) -> list[Path]:
return list(plan_path.parent.glob(f"{plan_path.name}.tmp.*"))
def test_write_plan_cleans_up_after_replace_failure(tmp_path, monkeypatch):
plan_path = tmp_path / "flow.plan"
plan_path.write_bytes(b"existing plan")
replace_error = OSError("replace failed")
def fail_replace(source, destination):
raise replace_error
monkeypatch.setattr(flow_estimator_trt.os, "replace", fail_replace)
with pytest.raises(OSError) as exc_info:
flow_estimator_trt._write_plan_atomically(b"new plan", str(plan_path))
assert exc_info.value is replace_error
assert plan_path.read_bytes() == b"existing plan"
assert _temporary_plans(plan_path) == []
def test_write_plan_preserves_replace_error_when_cleanup_fails(tmp_path, monkeypatch):
plan_path = tmp_path / "flow.plan"
replace_error = OSError("replace failed")
def fail_replace(source, destination):
raise replace_error
def fail_unlink(path):
raise PermissionError("cleanup failed")
monkeypatch.setattr(flow_estimator_trt.os, "replace", fail_replace)
monkeypatch.setattr(flow_estimator_trt.os, "unlink", fail_unlink)
with pytest.raises(OSError) as exc_info:
flow_estimator_trt._write_plan_atomically(b"new plan", str(plan_path))
assert exc_info.value is replace_error
assert len(_temporary_plans(plan_path)) == 1
def test_write_plan_cleans_up_after_write_failure(tmp_path, monkeypatch):
plan_path = tmp_path / "flow.plan"
write_error = OSError("write failed")
real_open = open
class FailingWriter:
def __init__(self, path, mode):
self.file = real_open(path, mode)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.file.close()
def write(self, data):
self.file.write(data[:1])
raise write_error
monkeypatch.setattr(flow_estimator_trt, "open", FailingWriter, raising=False)
with pytest.raises(OSError) as exc_info:
flow_estimator_trt._write_plan_atomically(b"new plan", str(plan_path))
assert exc_info.value is write_error
assert not plan_path.exists()
assert _temporary_plans(plan_path) == []
def test_write_plan_does_not_remove_a_colliding_temporary_file(tmp_path, monkeypatch):
plan_path = tmp_path / "flow.plan"
token = "0" * 32
temporary_path = Path(f"{plan_path}.tmp.{flow_estimator_trt.os.getpid()}.{token}")
temporary_path.write_bytes(b"another writer")
monkeypatch.setattr(flow_estimator_trt.uuid, "uuid4", lambda: flow_estimator_trt.uuid.UUID(hex=token))
with pytest.raises(FileExistsError):
flow_estimator_trt._write_plan_atomically(b"new plan", str(plan_path))
assert temporary_path.read_bytes() == b"another writer"
assert not plan_path.exists()
def test_write_plan_supports_concurrent_publication(tmp_path, monkeypatch):
plan_path = tmp_path / "flow.plan"
payloads = (b"a" * 4096, b"b" * 4096)
barrier = threading.Barrier(len(payloads))
source_paths = []
source_paths_lock = threading.Lock()
real_replace = flow_estimator_trt.os.replace
def synchronized_replace(source, destination):
with source_paths_lock:
source_paths.append(Path(source))
barrier.wait(timeout=5)
real_replace(source, destination)
monkeypatch.setattr(flow_estimator_trt.os, "replace", synchronized_replace)
with ThreadPoolExecutor(max_workers=len(payloads)) as executor:
futures = [
executor.submit(flow_estimator_trt._write_plan_atomically, payload, str(plan_path)) for payload in payloads
]
for future in futures:
future.result(timeout=10)
assert len(set(source_paths)) == len(payloads)
assert plan_path.read_bytes() in payloads
assert _temporary_plans(plan_path) == []
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""TensorRT engine for the CosyVoice3 flow-decoder (CFM) DiT estimator.
The estimator is the per-step network the conditional flow-matching ODE solver
@@ -26,6 +26,7 @@ from __future__ import annotations
import os
import queue
import uuid
import torch
from vllm.logger import init_logger
@@ -52,6 +53,26 @@ def _is_fp16_onnx(onnx_path: str) -> bool:
return "fp16" in name or "autocast" in name
def _write_plan_atomically(engine_bytes, plan_path: str) -> None:
tmp = f"{plan_path}.tmp.{os.getpid()}.{uuid.uuid4().hex}"
tmp_created = False
try:
with open(tmp, "xb") as f:
tmp_created = True
f.write(engine_bytes)
os.replace(tmp, plan_path)
tmp_created = False
except BaseException:
if tmp_created:
try:
os.unlink(tmp)
except FileNotFoundError:
pass
except OSError:
logger.warning("Failed to remove temporary TensorRT plan %s", tmp, exc_info=True)
raise
def _convert_onnx_to_trt(onnx_path: str, plan_path: str, strongly_typed: bool) -> None:
import tensorrt as trt
@@ -96,10 +117,7 @@ def _convert_onnx_to_trt(onnx_path: str, plan_path: str, strongly_typed: bool) -
engine_bytes = builder.build_serialized_network(network, config)
if engine_bytes is None:
raise RuntimeError(f"TensorRT failed to build flow-estimator engine from {onnx_path}")
tmp = plan_path + ".tmp"
with open(tmp, "wb") as f:
f.write(engine_bytes)
os.replace(tmp, plan_path)
_write_plan_atomically(engine_bytes, plan_path)
logger.info("Wrote flow-estimator TensorRT engine to %s", plan_path)