GLM-TTS validation

July 31, 2026 · View on GitHub

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:

RequestSpeech tokens C++ / PythonWAV frames C++ / PythonWAV cosineLog-mel cosineC++ wallPython wall
short_cold75 / 75, exact72,000 / 72,0000.1504830.9924232,684.2 ms2,687.7 ms
short_repeat75 / 75, exact72,000 / 72,0000.1504830.992423887.8 ms1,929.9 ms
technical139 / 139, exact133,440 / 133,4400.1448610.9911001,442.5 ms3,208.2 ms
longform305 / 305, exact292,800 / 292,800-0.0125830.9919463,161.7 ms6,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:

ComponentCompared boundaryResult
Text tokenizer and promptnormalized strings and full prompt idsexact
Whisper-VQreference speech-token ids63 / 63 exact
Llama greedygenerated speech-token idsexact for 75, 139, and 305-token requests
CAMPPlus192-value speaker embeddingcosine 0.999999999977; max abs 2.52e-05; RMSE 8.86e-06
CAMPPlus shared defaultdeterministic 250 x 80 input against unmodified mainbyte-for-byte exact; max abs 0
Flow, 10 steps114 x 80 melcosine 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:

RequestNative FP32 log-mel / framesMixed GGUF log-mel / frames
short_cold0.902897; 77,760 / 89,2800.902589; 77,760 / 89,280
short_repeat0.902897; 77,760 / 89,2800.902589; 77,760 / 89,280
technical0.929266; 150,720 / 147,8400.857288; 135,840 / 147,840
longform0.862186; 313,920 / 306,2400.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:

RequestIterationAudioWall mean ± SDRTF mean ± SD
short_cold1, reference miss3.24 s2,469.5 ± 637.1 ms0.7622 ± 0.1966
short_cold23.24 s626.2 ± 2.4 ms0.1933 ± 0.0008
short_repeat13.24 s617.1 ± 2.4 ms0.1905 ± 0.0007
short_repeat23.24 s618.1 ± 2.2 ms0.1908 ± 0.0007
technical15.66 s1,023.2 ± 2.6 ms0.1808 ± 0.0005
technical25.66 s1,002.8 ± 2.6 ms0.1772 ± 0.0005
longform113.08 s2,231.0 ± 3.6 ms0.1706 ± 0.0003
longform213.08 s2,189.1 ± 6.1 ms0.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:

PathPeak GPU memoryWarm repeated short request
Native FP32 audio.cpp8,846 MiB887.8 ms
Official Python FP328,628 to 8,787 MiB1,929.9 ms
Mixed GGUF, resident5,320 MiB608.6 ms
Mixed GGUF, balanced glm_tts.mem_saver=true3,878 MiB622.3 ms
Mixed GGUF, glm_tts.aggressive_mem_saver=true3,372 MiB3,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.