Skip to content

Latest commit

 

History

History
428 lines (349 loc) · 16.2 KB

File metadata and controls

428 lines (349 loc) · 16.2 KB

GLM-TTS validation

This report covers the community GLM-TTS implementation, its prepared safetensors package, and a standalone mixed Q8/F16 GGUF. Measurements were captured per request, with deterministic Python parity completed before the normal sampled path was measured.

Implementation scope

  • Adds the community glm_tts loader and offline tts / clon session.
  • Runs the ChatGLM tokenizer, Llama speech-token generator, Whisper-VQ reference encoder, CAMPPlus speaker encoder, Flow/DiT mel generator, and HiFT vocoder natively through the shared framework.
  • Adds the glm_tts model-manager package, package spec, CLI/server registry entry, user documentation, converter, and validation probes.
  • Supports prepared safetensors packages and standalone multi-component GGUF files with embedded configs, tokenizer files, and package metadata.
  • Keeps reusable component runtimes resident, caches prepared references, reuses shape-compatible graphs, and restricts the Llama output projection to GLM-TTS audio-token rows.
  • Extends the shared OuteTTS Llama runtime and Whisper embedding surface. GLM-TTS's CAMPPlus partial-segment normalization is additive and explicitly enabled only by the GLM-TTS session; the established shared default remains unchanged for Chatterbox, IndexTTS2, and Seed-VC.

Test environment

Tests were run on 2026-07-24 with:

  • Windows 11 and MSVC 2022 14.43
  • NVIDIA RTX 3090 24 GB
  • CUDA toolkit 12.4.131
  • PyTorch 2.5.1+cu121 and Transformers 5.13.0
  • GLM-TTS branch based on audio.cpp main f8fb0c1

The official Python reference was checked out at:

..\_reference\GLM-TTS

The prepared package id and paths were:

model-manager package: glm_tts
safetensors package:   ..\models\GLM-TTS
standalone GGUF:       ..\models\GLM-TTS-Q8\GLM-TTS_Q8.gguf
reference WAV:         ..\SAMPLES\EN_2.wav
reference transcript:  If you actually care about security

Install the package with:

python tools\model_manager_deprecated.py install glm_tts --models-dir ..\models

The installer downloads zai-org/GLM-TTS, converts Flow and HiFT from the official PyTorch checkpoints to safetensors, exports the tokenizer resources, and installs matching CAMPPlus safetensors weights.

Exact build commands

CPU:

.\scripts\build_windows.ps1 `
  -Preset windows-cpu-release -ConfigureOnly

cmake -S . -B build\windows-cpu-release `
  -DENGINE_BUILD_WARMBENCH=ON

cmake --build build\windows-cpu-release --parallel 8 --target `
  audiocpp_cli audiocpp_server `
  campplus_shared_default_probe `
  glm_tts_warm_bench glm_tts_llama_probe `
  glm_tts_frontend_probe glm_tts_campplus_probe `
  glm_tts_flow_conditioned_probe

CUDA:

$cuda = "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4"
$env:CUDA_PATH = $cuda
$env:CUDAToolkit_ROOT = $cuda
$env:CUDA_TOOLKIT_ROOT_DIR = $cuda
$env:CUDACXX = Join-Path $cuda "bin\nvcc.exe"
$env:CMAKE_CUDA_COMPILER = $env:CUDACXX
$env:NVCC_PREPEND_FLAGS = "-allow-unsupported-compiler"

.\scripts\build_windows.ps1 `
  -Preset windows-cuda-release -ConfigureOnly

cmake -S . -B build\windows-cuda-release `
  -DENGINE_BUILD_WARMBENCH=ON `
  -DCMAKE_CUDA_COMPILER="$env:CUDACXX"

cmake --build build\windows-cuda-release --parallel 8 --target `
  audiocpp_cli audiocpp_server audiocpp_gguf `
  campplus_shared_default_probe `
  glm_tts_warm_bench glm_tts_llama_probe `
  glm_tts_frontend_probe glm_tts_campplus_probe `
  glm_tts_flow_conditioned_probe

Both clean builds completed. The loader catalog advertises glm_tts with offline tts and clon tasks and the /v1/audio/speech endpoint.

Standalone GGUF

The quality-tested package keeps the autoregressive Llama group in F16 and stores the speech tokenizer, Flow, HiFT, and CAMPPlus groups as Q8_0:

.\build\windows-cuda-release\bin\audiocpp_gguf.exe `
  --input llama_weights=..\models\GLM-TTS\llm\model.safetensors.index.json `
  --input speech_tokenizer_weights=..\models\GLM-TTS\speech_tokenizer\model.safetensors `
  --input flow_weights=..\models\GLM-TTS\flow\model.safetensors `
  --input hift_weights=..\models\GLM-TTS\hift\model.safetensors `
  --input campplus_weights=..\models\GLM-TTS\frontend\campplus.safetensors `
  --root ..\models\GLM-TTS `
  --family glm_tts `
  --model-spec .\model_specs\glm_tts.json `
  --type q8_0 `
  --keep-type "llama_weights/*=f16" `
  --overwrite `
  --output ..\models\GLM-TTS-Q8\GLM-TTS_Q8.gguf

Inspection:

.\build\windows-cuda-release\bin\audiocpp_gguf.exe `
  --inspect ..\models\GLM-TTS-Q8\GLM-TTS_Q8.gguf
tensors=2076
rank0_scalars=122
embedded_sidecars=true
embedded_model_spec=true
model_spec_family=glm_tts
namespace=campplus_weights
namespace=flow_weights
namespace=hift_weights
namespace=llama_weights
namespace=speech_tokenizer_weights

The file is 5,143,813,728 bytes and has SHA-256 28E2B66333FD5F051466520F6A8A5F26CBA7F3B768891006168A4560972107CB. It needs no external model spec, tokenizer, config, or weight file.

Exact run commands

Standalone CLI:

.\build\windows-cuda-release\bin\audiocpp_cli.exe `
  --task clon --family glm_tts `
  --model ..\models\GLM-TTS-Q8\GLM-TTS_Q8.gguf `
  --backend cuda `
  --voice-ref ..\SAMPLES\EN_2.wav `
  --reference-text "If you actually care about security" `
  --text "Hello from G L M T T S. This voice was generated locally." `
  --top-k 25 --top-p 0.8 --temperature 1.0 `
  --seed 0 --max-tokens 256 `
  --out ..\outputs\glm_tts_pr_validation\standalone_q8_cli.wav `
  --log

The one-file path was materialized to audio.cpp's temporary GGUF resource directory, produced 131 speech tokens, 262 mel frames, and 5.24 seconds of valid 24 kHz mono PCM. ffmpeg -v error -i ... -f null - completed without an error. The output SHA-256 is DF8ACFDCD308EA10FDBEF37D03581DAA4E2222922E2DC54A3DD417EEF0263450.

Server configuration:

{
  "host": "127.0.0.1",
  "port": 18083,
  "backend": "cuda",
  "device": 0,
  "threads": 8,
  "lazy_load": false,
  "models": [
    {
      "id": "glm-tts-q8",
      "family": "glm_tts",
      "path": "E:/models/GLM-TTS-Q8/GLM-TTS_Q8.gguf",
      "task": "clon",
      "mode": "offline",
      "default_voice_preset": {
        "voice_ref": "E:/samples/EN_2.wav",
        "reference_text": "If you actually care about security"
      }
    }
  ]
}
.\build\windows-cuda-release\bin\audiocpp_server.exe `
  --config ..\outputs\glm_tts_pr_validation\server.cuda.json `
  --log

curl.exe http://127.0.0.1:18083/v1/audio/speech `
  -H "Content-Type: application/json" `
  -o ..\outputs\glm_tts_pr_validation\server_q8.wav `
  -d '{\"model\":\"glm-tts-q8\",\"input\":\"Hello from the GLM TTS server.\",\"max_tokens\":256,\"seed\":0,\"temperature\":1.0,\"top_k\":25,\"top_p\":0.8}'

GET /health returned {"status":"ok","backend":"cuda","models":1}. The speech request produced a valid WAV with SHA-256 9C818AB9804FF92A04F0EC5AB22B06B45F6E9DA60704F1A851C1367787F5169C.

Deterministic Python parity

The official model uses RAS sampling with temperature 1.0, top-k 25, and top-p 0.8. Those defaults remain unchanged for normal synthesis. For logic parity only, the upstream RAS selection and audio.cpp sampling were changed to argmax. Native FP32 weights were used on both sides. This isolates prompt, token, duration, Flow, and vocoder logic from stochastic sampling drift.

Python:

python tests\glm_tts\reference_warm_bench.py `
  ..\_reference\GLM-TTS `
  --request-file tests\glm_tts\parity_greedy_requests.json `
  --voice-ref ..\SAMPLES\EN_2.wav `
  --reference-text "If you actually care about security" `
  --out-dir ..\outputs\glm_tts_pr_validation\python_f32_greedy `
  --greedy

audio.cpp:

.\build\windows-cuda-release\bin\glm_tts_warm_bench.exe `
  --model ..\models\GLM-TTS `
  --backend cuda --threads 8 `
  --voice-ref ..\SAMPLES\EN_2.wav `
  --reference-text "If you actually care about security" `
  --request-file tests\glm_tts\parity_greedy_requests.json `
  --iterations 1 `
  --session-option glm_tts.weight_type=f32 `
  --audio-out-dir ..\outputs\glm_tts_pr_validation\cpp_f32_greedy

Per-request results:

Request Speech tokens C++ / Python WAV frames C++ / Python WAV cosine Log-mel cosine C++ wall Python wall
short_cold 75 / 75, exact 72,000 / 72,000 0.150483 0.992423 2,684.2 ms 2,687.7 ms
short_repeat 75 / 75, exact 72,000 / 72,000 0.150483 0.992423 887.8 ms 1,929.9 ms
technical 139 / 139, exact 133,440 / 133,440 0.144861 0.991100 1,442.5 ms 3,208.2 ms
longform 305 / 305, exact 292,800 / 292,800 -0.012583 0.991946 3,161.7 ms 6,505.0 ms

The low waveform cosine is phase-sensitive and reflects independent HiFT excitation. Frame counts and token ids are exact, while log-mel similarity is 0.9911 to 0.9924. No parity-only trace dumping is enabled during normal requests.

Component boundaries

The component probes use fixed inputs and fixed Flow noise:

Component Compared boundary Result
Text tokenizer and prompt normalized strings and full prompt ids exact
Whisper-VQ reference speech-token ids 63 / 63 exact
Llama greedy generated speech-token ids exact for 75, 139, and 305-token requests
CAMPPlus 192-value speaker embedding cosine 0.999999999977; max abs 2.52e-05; RMSE 8.86e-06
CAMPPlus shared default deterministic 250 x 80 input against unmodified main byte-for-byte exact; max abs 0
Flow, 10 steps 114 x 80 mel cosine 0.999999999231; max abs 0.001515; RMSE 0.000279

CampplusEncoderConfig::normalize_partial_segment_by_full_length defaults to false. Only GLM-TTS sets it to true to match the published GLM-TTS CAMPPlus ONNX graph. Chatterbox, IndexTTS2, Seed-VC, and other shared callers continue through the previous pooling graph. The regression probe campplus_shared_default_probe was compiled once against unmodified main and once against this branch; both emitted the same 192 serialized values.

The request options flow_noise_path, hift_source_random_path, and hift_prior_noise_count expose stochastic boundaries for targeted parity tests without adding normal-request log spam.

Normal sampled-path parity

The normal official settings were then restored: RAS, temperature 1.0, top-k 25, top-p 0.8, and seed 0. Native FP32 and the mixed Q8/F16 GGUF were compared against the official FP32 output per request:

Request Native FP32 log-mel / frames Mixed GGUF log-mel / frames
short_cold 0.902897; 77,760 / 89,280 0.902589; 77,760 / 89,280
short_repeat 0.902897; 77,760 / 89,280 0.902589; 77,760 / 89,280
technical 0.929266; 150,720 / 147,840 0.857288; 135,840 / 147,840
longform 0.862186; 313,920 / 306,240 0.861948; 313,920 / 306,240

For the short sampled request, token ids matched Python through token 37 and then diverged stochastically. The deterministic tests above demonstrate that the model logic and frame accounting match when sampling variation is removed.

Per-request performance

The normal mixed Q8/F16 matrix was run in three fresh processes. Each request was executed twice inside one long-lived session. Iteration 1 can build a new shape-specific graph; iteration 2 measures reuse of that request shape.

.\build\windows-cuda-release\bin\glm_tts_warm_bench.exe `
  --model ..\models\GLM-TTS-Q8\GLM-TTS_Q8.gguf `
  --backend cuda --threads 8 `
  --voice-ref ..\SAMPLES\EN_2.wav `
  --reference-text "If you actually care about security" `
  --request-file tests\glm_tts\warm_bench_requests.json `
  --audio-out-dir ..\outputs\glm_tts_pr_validation\q8_run1\audio `
  --log-file ..\outputs\glm_tts_pr_validation\q8_run1\trace.log

Mean and sample standard deviation across the three processes:

Request Iteration Audio Wall mean ± SD RTF mean ± SD
short_cold 1, reference miss 3.24 s 2,469.5 ± 637.1 ms 0.7622 ± 0.1966
short_cold 2 3.24 s 626.2 ± 2.4 ms 0.1933 ± 0.0008
short_repeat 1 3.24 s 617.1 ± 2.4 ms 0.1905 ± 0.0007
short_repeat 2 3.24 s 618.1 ± 2.2 ms 0.1908 ± 0.0007
technical 1 5.66 s 1,023.2 ± 2.6 ms 0.1808 ± 0.0005
technical 2 5.66 s 1,002.8 ± 2.6 ms 0.1772 ± 0.0005
longform 1 13.08 s 2,231.0 ± 3.6 ms 0.1706 ± 0.0003
longform 2 13.08 s 2,189.1 ± 6.1 ms 0.1674 ± 0.0005

The four Python sampled requests took 3,314.2, 2,429.6, 3,808.6, and 7,151.9 ms respectively. Using the comparable reference-cached audio.cpp requests, the mixed GGUF path was approximately 3.2x to 3.9x faster than the official Python reference.

All 24 WAVs from the three mixed-GGUF runs reduced to three hashes: one shared by the two identical short cases, one for technical, and one for longform. The repeated outputs are byte-identical across processes.

Memory and mem-saver tradeoff

Memory was sampled every 200 ms with nvidia-smi:

Path Peak GPU memory Warm repeated short request
Native FP32 audio.cpp 8,846 MiB 887.8 ms
Official Python FP32 8,628 to 8,787 MiB 1,929.9 ms
Mixed GGUF, resident 5,320 MiB 608.6 ms
Mixed GGUF, balanced glm_tts.mem_saver=true 3,878 MiB 622.3 ms
Mixed GGUF, glm_tts.aggressive_mem_saver=true 3,372 MiB 3,761.7 ms

Balanced mem-saver releases the approximately 1.58 GiB reference-only Whisper-VQ and CAMPPlus path after its output is cached, but keeps Llama, Flow, and HiFT warm. Across three warm repeats it saved 1,442 MiB (27.1%) versus resident mode while adding 13.7 ms (2.3%) mean latency. A reference-cache miss releases the generation path before reconstructing the reference encoders so the groups do not overlap in VRAM.

Aggressive mem-saver preserves the former unload-every-stage policy. It saved another 506 MiB, but rebuilding Llama, Flow, and HiFT made warm requests about 6.0x slower than balanced mode. All balanced, aggressive, and resident outputs reduced to the same SHA-256 hash: 72672D49755B1B3987318EC264FBEC0DBB0AA0175461818AAC56BD1404F56E88.

Windows reported peak working set around 25.0 GiB and private bytes between 33.8 and 44.5 GiB while loading this model. These counters include mapped GGUF pages, converted host tensors, and backend virtual reservations; private bytes must not be interpreted as committed physical RAM.

CPU path

A full short request was executed with the CPU build:

.\build\windows-cpu-release\bin\glm_tts_warm_bench.exe `
  --model ..\models\GLM-TTS-Q8\GLM-TTS_Q8.gguf `
  --backend cpu --threads 16 `
  --voice-ref ..\SAMPLES\EN_2.wav `
  --reference-text "If you actually care about security" `
  --text "Hello." --iterations 1 `
  --audio-out-dir ..\outputs\glm_tts_pr_validation\cpu_short\audio

It produced 0.64 seconds of valid audio in 4,963.3 ms, RTF 7.755. CPU is functional, but CUDA is the practical tested backend for interactive use.

Generated artifacts

..\outputs\glm_tts_pr_validation\standalone_q8_cli.wav
..\outputs\glm_tts_pr_validation\server_q8.wav
..\outputs\glm_tts_pr_validation\python_f32
..\outputs\glm_tts_pr_validation\python_f32_greedy
..\outputs\glm_tts_pr_validation\cpp_f32
..\outputs\glm_tts_pr_validation\cpp_f32_greedy
..\outputs\glm_tts_pr_validation\q8_run1
..\outputs\glm_tts_pr_validation\q8_run2
..\outputs\glm_tts_pr_validation\q8_run3
..\outputs\glm_tts_pr_validation\components
..\outputs\glm_tts_pr_validation\cpu_short
..\models\GLM-TTS-Q8\GLM-TTS_Q8.gguf

Known limitations

  • GLM-TTS supports Chinese and English.
  • Both advertised task names use the same zero-shot reference-conditioned path. A clean reference WAV and its exact transcript are required.
  • Normal sampled runs can diverge from Python because small numeric differences alter autoregressive multinomial choices. Greedy FP32 token and frame parity is exact.
  • Independent HiFT excitation makes waveform cosine phase-sensitive even when token ids and frame counts match; log-mel similarity is the useful metric.
  • The mixed GGUF keeps Llama in F16 because a fully Q8 autoregressive head had a larger quality and token-selection penalty.
  • CUDA and CPU were tested. Vulkan and Metal were not tested.
  • The upstream WeText normalizer was unavailable in the Windows Python environment. Parity uses English text that the identity fallback does not alter.