| |
| """기존 CAM++ extractor와 GPU extractor의 embedding 수치 차이와 latency를 비교한다.""" |
|
|
| import argparse |
| import random |
| import sys |
| import time |
| from pathlib import Path |
|
|
| import librosa |
| import torch |
| import torch.nn.functional as F |
|
|
| PROJECT_ROOT = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(PROJECT_ROOT / "src")) |
|
|
| from voxcpm.modules.locdit.onnx import CAMPLUS_ONNX_PATH, EmbeddingExtractor, EmbeddingExtractorGPU |
|
|
|
|
| def parse_args(): |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("--audio", type=str, default="", help="16 kHz로 로드할 검증용 wav/flac 경로") |
| parser.add_argument("--seed", type=int, default=0, help="10초 초과 audio crop 고정 seed") |
| parser.add_argument("--warmup", type=int, default=2, help="latency 측정 전 warmup 반복 횟수") |
| parser.add_argument("--iters", type=int, default=5, help="latency 측정 반복 횟수") |
| parser.add_argument("--max-abs-error", type=float, default=1e-3) |
| parser.add_argument("--mean-abs-error", type=float, default=1e-4) |
| parser.add_argument("--min-cosine", type=float, default=0.99999) |
| return parser.parse_args() |
|
|
|
|
| def resolve_audio_path(audio_arg: str) -> Path: |
| if audio_arg: |
| path = Path(audio_arg) |
| else: |
| path = PROJECT_ROOT / "seed-tts-eval" / "seedtts_testset" / "en" / "prompt-wavs" / "common_voice_en_2331.wav" |
| if not path.exists(): |
| raise FileNotFoundError(f"Audio file not found: {path}. Pass --audio explicitly.") |
| return path |
|
|
|
|
| def load_audio(path: Path, seed: int, max_len: int) -> torch.Tensor: |
| audio, _ = librosa.load(str(path), sr=16000, mono=True) |
| speech = torch.from_numpy(audio).float() |
| if speech.numel() > max_len: |
| rng = random.Random(seed) |
| start = rng.randint(0, speech.numel() - max_len) |
| speech = speech[start : start + max_len].contiguous() |
| return speech |
|
|
|
|
| def timed_call(fn, warmup: int, iters: int): |
| for _ in range(warmup): |
| fn() |
| if torch.cuda.is_available(): |
| torch.cuda.synchronize() |
| start = time.perf_counter() |
| result = None |
| for _ in range(iters): |
| result = fn() |
| if torch.cuda.is_available(): |
| torch.cuda.synchronize() |
| elapsed = (time.perf_counter() - start) / max(1, iters) |
| return result, elapsed |
|
|
|
|
| def main(): |
| args = parse_args() |
| if not torch.cuda.is_available(): |
| raise RuntimeError("GPU verification requires torch.cuda.is_available() == True") |
| if not CAMPLUS_ONNX_PATH.exists(): |
| raise FileNotFoundError(f"CAM++ ONNX model not found: {CAMPLUS_ONNX_PATH}") |
|
|
| audio_path = resolve_audio_path(args.audio) |
| speech = load_audio(audio_path, args.seed, max_len=10 * 16000) |
| speech_gpu = speech.to("cuda", non_blocking=True) |
|
|
| reference_extractor = EmbeddingExtractor(model_path=str(CAMPLUS_ONNX_PATH)) |
| gpu_extractor = EmbeddingExtractorGPU(model_path=str(CAMPLUS_ONNX_PATH)) |
|
|
| ref_embedding, ref_latency = timed_call( |
| lambda: reference_extractor.inference(speech), |
| warmup=args.warmup, |
| iters=args.iters, |
| ) |
| gpu_embedding, gpu_latency = timed_call( |
| lambda: gpu_extractor.inference(speech_gpu), |
| warmup=args.warmup, |
| iters=args.iters, |
| ) |
|
|
| ref = ref_embedding.detach().float().cpu().flatten() |
| gpu = gpu_embedding.detach().float().cpu().flatten() |
| diff = (ref - gpu).abs() |
| max_abs_error = diff.max().item() |
| mean_abs_error = diff.mean().item() |
| cosine = F.cosine_similarity(ref.unsqueeze(0), gpu.unsqueeze(0)).item() |
|
|
| print(f"audio: {audio_path}") |
| print(f"shape/reference: {tuple(ref.shape)}") |
| print(f"shape/gpu: {tuple(gpu.shape)}") |
| print(f"max_abs_error: {max_abs_error:.8g}") |
| print(f"mean_abs_error: {mean_abs_error:.8g}") |
| print(f"cosine: {cosine:.8g}") |
| print(f"latency/ref: {ref_latency * 1000:.3f} ms") |
| print(f"latency/gpu: {gpu_latency * 1000:.3f} ms") |
|
|
| passed = ( |
| ref.shape == gpu.shape == (192,) |
| and max_abs_error <= args.max_abs_error |
| and mean_abs_error <= args.mean_abs_error |
| and cosine >= args.min_cosine |
| ) |
| print("result: PASS" if passed else "result: FAIL") |
| if not passed: |
| raise SystemExit(1) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|