[Edge][WP5] Codec stream diagnostics: per-request output/codes counters, client saves received codes, offline parity diag

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
tzhouamandClaude Fable 5.1 committed 2026-09-09 16:02:02 +08:00
1 parent 15bb6133a3
commit b8d1e44ebe
3 files changed
+98

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Offline parity diagnosis for the codec-token stream (CPU only).
Given a codec_stream_e2e.py output directory (client_<i>.codes.npy, client_<i>.wav,
full_<i>.wav) decode the *received* codes with the exact (non-chunked) decoder and
compare (a) against the client's chunked decode (tests the client's chunking) and
(b) against the full server's PCM (tests token equality + the server's decode).
Reports lag-aligned SNR and correlation.
python benchmarks/tts/codec_parity_diag.py --dir benchmarks/tts/edge_results/codec_stream/<ts>
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
import soundfile as sf
import torch
def aligned_snr(ref: np.ndarray, test: np.ndarray, max_lag: int = 4000) -> tuple[float, float, int]:
n = min(ref.size, test.size)
ref, test = ref[:n].astype(np.float64), test[:n].astype(np.float64)
if n < 3 * max_lag:
lag = 0
else:
c = np.correlate(ref[max_lag:-max_lag], test, mode="valid")
lag = int(np.argmax(c)) - max_lag
if lag >= 0:
a, b = test[lag:], ref[: n - lag]
else:
a, b = test[: n + lag], ref[-lag:]
m = min(a.size, b.size)
a, b = a[:m], b[:m]
snr = 10 * np.log10((b**2).sum() / (((b - a) ** 2).sum() + 1e-12))
corr = float(np.corrcoef(a, b)[0, 1]) if m > 1 else 0.0
return float(snr), corr, lag
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--dir", required=True)
p.add_argument("--model", default="Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice")
args = p.parse_args(argv)
from vllm_omni.edge.decoder_export import load_code2wav_decoder
dec = load_code2wav_decoder(args.model)
d = Path(args.dir)
report = []
for codes_path in sorted(d.glob("client_*.codes.npy")):
i = codes_path.name.split("_")[1].split(".")[0]
codes = np.load(codes_path)
if codes.size == 0:
continue
with torch.inference_mode():
exact = dec._forward_exact(torch.as_tensor(codes.T[None], dtype=torch.long)).reshape(-1).numpy()
client, _ = sf.read(d / f"client_{i}.wav", dtype="float32")
full, _ = sf.read(d / f"full_{i}.wav", dtype="float32")
s1, c1, l1 = aligned_snr(exact, client.reshape(-1))
s2, c2, l2 = aligned_snr(full.reshape(-1), exact)
row = {
"i": int(i),
"frames": int(codes.shape[0]),
"exact_vs_client_chunked": {"snr_db": s1, "corr": c1, "lag": l1},
"full_server_vs_exact": {"snr_db": s2, "corr": c2, "lag": l2},
}
report.append(row)
print(json.dumps(row))
(d / "parity_diag.json").write_text(json.dumps(report, indent=1))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -199,6 +199,7 @@ async def run(args: argparse.Namespace) -> dict[str, Any]:
if val is not None:
config[key] = val
pcm_parts: list[np.ndarray] = []
codes_all: list[np.ndarray] = []
chunk_log: list[dict[str, Any]] = []
pending = np.zeros((0, decoder.num_quantizers), dtype=np.int64)
chunk_idx = 0
@@ -222,6 +223,7 @@ async def run(args: argparse.Namespace) -> dict[str, Any]:
total_frames += n
if n:
pending = np.concatenate([pending, arr.astype(np.int64)], axis=0)
codes_all.append(arr.astype(np.int64))
eos = bool(flags & FLAG_EOS)
target = schedule[min(chunk_idx, len(schedule) - 1)]
while pending.shape[0] >= target or (eos and pending.shape[0] > 0):
@@ -283,6 +285,10 @@ async def run(args: argparse.Namespace) -> dict[str, Any]:
sf.write(args.out, audio, sr, format="WAV")
result["wav"] = args.out
# Raw received codec rows (frames x codebooks) for offline parity checks.
codes_path = str(Path(args.out).with_suffix(".codes.npy"))
np.save(codes_path, np.concatenate(codes_all, axis=0) if codes_all else np.zeros((0, 0), dtype=np.int64))
result["codes"] = codes_path
if args.json:
Path(args.json).write_text(json.dumps(result, indent=1))
print(json.dumps({k: v for k, v in result.items() if k != "chunks"}, indent=1))
@@ -3293,8 +3293,12 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
token_ids: list[int] = []
sent = 0
finished = False
n_outputs = 0
n_with_codes = 0
first_codes_at = -1
try:
async for res in generator:
n_outputs += 1
if usage_acc is not None:
usage_acc.observe(res)
outputs = getattr(res, "outputs", None)
@@ -3308,6 +3312,9 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
codes_cum = self._extract_codec_output(res)
done = bool(getattr(res, "finished", False))
if codes_cum is not None:
n_with_codes += 1
if first_codes_at < 0:
first_codes_at = n_outputs
aligned = align_codec_rows(codes_cum, token_ids, codebook_size)
delta = aligned[sent:]
if delta.shape[0] >= min_frames or (done and delta.shape[0] > 0):
@@ -3319,6 +3326,14 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
finished = True
yield None, True
finally:
logger.info(
"[codec_stream] req=%s engine outputs=%d with codes=%d first codes at output #%d rows sent=%d",
request_id,
n_outputs,
n_with_codes,
first_codes_at,
sent,
)
if usage_acc is not None and tts_params is not None and finished:
self._validate_tts_generation(tts_params, usage_acc)