Summary
Installing a log callback via QwenLibrary.set_log_callback() (i.e. qt_log_set) and then constructing a second QwenTTS context in the same process reliably segfaults inside qt_init(), at the point where the native code invokes the registered callback. This reproduces with a single reused callback object kept alive for the whole process (not a GC/dangling-pointer issue on the Python side) and with a fresh callback per instance alike.
Without a log callback installed, repeated construction/close cycles are completely stable (control test below, 6/6 iterations clean).
Environment
qwentts-cpp-python 0.3.1 (also reproduces via realtimetts-qwen-native 0.2.0 / RealtimeTTS 0.8.5's QwenEngine, which triggers the same crash through QwenEngine's private _backend.library.set_log_callback)
- Python 3.12.14
- numpy 2.5.2
- Ubuntu 26.04 LTS, Linux 7.0.0-29-generic, x86_64
- 2x NVIDIA GeForce RTX 3090, driver 580.173.02
- Model:
Qwen/Qwen3-TTS-12Hz-0.6B-Base, quant Q8_0
Minimal repro
Only the public qwentts_cpp API - no other project code involved. Reference audio is a synthetic sine wave, not a real recording.
"""Minimal repro: installing a qt_log_set log callback, then constructing
a second QwenTTS context in the same process, segfaults - regardless of
whether the callback object is freshly created per instance or a single
object reused/kept alive for the whole process (both variants crash below).
"""
import gc
import numpy as np
from qwentts_cpp import QwenTTS
MODEL_ID = "Qwen/Qwen3-TTS-12Hz-0.6B-Base"
QUANT = "Q8_0"
# Any 24kHz mono float32 array works to exercise extract_voice_ref - it
# doesn't need to be real speech to trigger the crash.
SR = 24_000
_t = np.linspace(0, 2.0, SR * 2, endpoint=False, dtype=np.float32)
FAKE_REF_AUDIO = (0.2 * np.sin(2 * np.pi * 220.0 * _t)).astype(np.float32)
def _log_callback(level: int, message: str) -> None:
pass # a no-op callback triggers the same crash as one that prints
def make_and_use_engine(install_log_callback: bool) -> None:
engine = QwenTTS.from_pretrained(MODEL_ID, quant=QUANT)
if install_log_callback:
engine.library.set_log_callback(_log_callback)
voice_ref = engine.extract_voice_ref(FAKE_REF_AUDIO)
samples, sample_rate = engine.synthesize(
text="This is a short test sentence for reproduction purposes.",
ref_spk_emb=voice_ref.ref_spk_emb,
)
print(f"got {len(samples)} samples at {sample_rate} Hz")
engine.close()
for i in range(6):
print(f"=== iteration {i} ===", flush=True)
make_and_use_engine(install_log_callback=True)
gc.collect()
print("=== all iterations completed without crashing ===")
Result: segfaults during iteration 1 (the second QwenTTS.from_pretrained(...) call), every run.
Control (same script, install_log_callback=False): all 6 iterations complete cleanly, exit code 0. This isolates the crash to the callback registration itself, not to repeated construction/close in general.
Backtrace (gdb, main thread)
#0 0x000000000084909c in ffi_closure_unix64_inner ()
#1 0x000000000084a1e8 in ffi_closure_unix64 ()
#2 0x00007ffff46adffc in ?? () from qwentts_cpp/lib/libqwen.so
#3 0x00007ffff46b358f in qt_init () from qwentts_cpp/lib/libqwen.so
#4 0x000000000084a052 in ffi_call_unix64 ()
#5 0x0000000000848e66 in ffi_call_int ()
#6 0x0000000000848af0 in ffi_call ()
#7 0x0000000000760246 in _ctypes_callproc ()
#8 0x000000000075e905 in PyCFuncPtr_call ()
This shows qt_init() itself invoking the registered log callback through the ctypes/libffi trampoline (ffi_closure_unix64*) - i.e. qt_init on the second context logs something during initialization, and that call into the still-registered callback is what crashes.
What I've ruled out
- Not a Python-side GC/dangling-pointer issue: tried both (a) a fresh callback object per
QwenTTS instance, and (b) a single callback object created once and kept alive at module scope for the entire process, reused identically across every set_log_callback() call. Both crash, at the same point (second qt_init()), in the same number of iterations.
- Not general construct/close instability: the control run above (no callback) is fully stable across the same number of iterations.
Suspicion
Given qt_log_set is documented as installing a process-wide callback (mirroring llama.cpp-style logging), my guess is something in qt_init's own internal state (possibly GIL handling around the callback invocation, or state left over from the previous context's qt_free()) isn't safe across a second context's init while a callback from a prior context is still registered - even when that "prior context" callback is, on the Python side, the exact same live object being reused. Happy to help narrow this down further (e.g. testing a build with debug symbols, or testing whether calling set_log_callback(None) before qt_free()/before the next qt_init() avoids it) if useful.
Impact
Anyone constructing more than one QwenTTS/QwenEngine context in a single process (a pool, a warm-up-then-reload cycle, or simply more than one call to from_pretrained) cannot safely use set_log_callback at all today - the only way to avoid the crash we found is to never install a custom log callback, which means the native [Pipeline] .../[Perf] ... etc. stdout logging can't be suppressed or redirected.
Summary
Installing a log callback via
QwenLibrary.set_log_callback()(i.e.qt_log_set) and then constructing a secondQwenTTScontext in the same process reliably segfaults insideqt_init(), at the point where the native code invokes the registered callback. This reproduces with a single reused callback object kept alive for the whole process (not a GC/dangling-pointer issue on the Python side) and with a fresh callback per instance alike.Without a log callback installed, repeated construction/close cycles are completely stable (control test below, 6/6 iterations clean).
Environment
qwentts-cpp-python0.3.1 (also reproduces viarealtimetts-qwen-native0.2.0 /RealtimeTTS0.8.5'sQwenEngine, which triggers the same crash throughQwenEngine's private_backend.library.set_log_callback)Qwen/Qwen3-TTS-12Hz-0.6B-Base, quantQ8_0Minimal repro
Only the public
qwentts_cppAPI - no other project code involved. Reference audio is a synthetic sine wave, not a real recording.Result: segfaults during iteration 1 (the second
QwenTTS.from_pretrained(...)call), every run.Control (same script,
install_log_callback=False): all 6 iterations complete cleanly, exit code 0. This isolates the crash to the callback registration itself, not to repeated construction/close in general.Backtrace (gdb, main thread)
This shows
qt_init()itself invoking the registered log callback through the ctypes/libffi trampoline (ffi_closure_unix64*) - i.e.qt_initon the second context logs something during initialization, and that call into the still-registered callback is what crashes.What I've ruled out
QwenTTSinstance, and (b) a single callback object created once and kept alive at module scope for the entire process, reused identically across everyset_log_callback()call. Both crash, at the same point (secondqt_init()), in the same number of iterations.Suspicion
Given
qt_log_setis documented as installing a process-wide callback (mirroring llama.cpp-style logging), my guess is something inqt_init's own internal state (possibly GIL handling around the callback invocation, or state left over from the previous context'sqt_free()) isn't safe across a second context's init while a callback from a prior context is still registered - even when that "prior context" callback is, on the Python side, the exact same live object being reused. Happy to help narrow this down further (e.g. testing a build with debug symbols, or testing whether callingset_log_callback(None)beforeqt_free()/before the nextqt_init()avoids it) if useful.Impact
Anyone constructing more than one
QwenTTS/QwenEnginecontext in a single process (a pool, a warm-up-then-reload cycle, or simply more than one call tofrom_pretrained) cannot safely useset_log_callbackat all today - the only way to avoid the crash we found is to never install a custom log callback, which means the native[Pipeline] .../[Perf] ...etc. stdout logging can't be suppressed or redirected.