1use std::cmp::max;
2use std::collections::BTreeMap;
3use std::ffi::CStr;
4use std::ptr::NonNull;
5use std::sync::{Arc, Mutex, MutexGuard};
6
7use qts_ggml::sys;
8
9use crate::Qwen3TtsError;
10
11#[cfg(all(feature = "metal", target_vendor = "apple"))]
13fn ggml_reg_mtl() -> &'static CStr {
14 c"MTL"
15}
16
17#[cfg(feature = "vulkan")]
19fn ggml_reg_vulkan() -> &'static CStr {
20 c"Vulkan"
21}
22
23#[cfg(any(all(feature = "metal", target_vendor = "apple"), feature = "vulkan"))]
24fn gpu_device_index() -> Result<usize, Qwen3TtsError> {
25 let Ok(s) = std::env::var("QWEN3_TTS_GPU_DEVICE") else {
26 return Ok(0);
27 };
28 let s = s.trim();
29 if s.is_empty() {
30 return Ok(0);
31 }
32 s.parse().map_err(|_| {
33 Qwen3TtsError::InvalidInput(format!(
34 "QWEN3_TTS_GPU_DEVICE must be a non-negative integer, got {s:?}"
35 ))
36 })
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42enum BackendChoice {
43 Auto,
44 Explicit(BackendPreference),
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48enum BackendPreference {
49 Cpu,
50 Metal,
51 Vulkan,
52}
53
54impl BackendPreference {
55 fn as_env_str(self) -> &'static str {
56 match self {
57 Self::Cpu => "cpu",
58 Self::Metal => "metal",
59 Self::Vulkan => "vulkan",
60 }
61 }
62}
63
64fn parse_qts_backend() -> Result<BackendChoice, Qwen3TtsError> {
65 let var = match std::env::var("QWEN3_TTS_BACKEND") {
66 Ok(s) if !s.trim().is_empty() => s,
67 _ => return Ok(BackendChoice::Auto),
68 };
69 match var.trim().to_ascii_lowercase().as_str() {
70 "auto" => Ok(BackendChoice::Auto),
71 "cpu" => Ok(BackendChoice::Explicit(BackendPreference::Cpu)),
72 "metal" => Ok(BackendChoice::Explicit(BackendPreference::Metal)),
73 "vulkan" => Ok(BackendChoice::Explicit(BackendPreference::Vulkan)),
74 other => Err(Qwen3TtsError::InvalidInput(format!(
75 "QWEN3_TTS_BACKEND: unknown value '{other}' (expected auto, cpu, metal, vulkan)"
76 ))),
77 }
78}
79
80fn default_auto_backend_order() -> Vec<BackendPreference> {
81 #[cfg(target_vendor = "apple")]
82 {
83 vec![
84 BackendPreference::Metal,
85 BackendPreference::Vulkan,
86 BackendPreference::Cpu,
87 ]
88 }
89 #[cfg(not(target_vendor = "apple"))]
90 {
91 vec![BackendPreference::Vulkan, BackendPreference::Cpu]
92 }
93}
94
95fn parse_auto_backend_order() -> Result<Vec<BackendPreference>, Qwen3TtsError> {
96 let var = match std::env::var("QWEN3_TTS_BACKEND_FALLBACK") {
97 Ok(s) if !s.trim().is_empty() => s,
98 _ => return Ok(default_auto_backend_order()),
99 };
100 let mut order = Vec::new();
101 for token in var.split(',') {
102 let value = token.trim().to_ascii_lowercase();
103 if value.is_empty() {
104 continue;
105 }
106 let pref = match value.as_str() {
107 "cpu" => BackendPreference::Cpu,
108 "metal" => BackendPreference::Metal,
109 "vulkan" => BackendPreference::Vulkan,
110 other => {
111 return Err(Qwen3TtsError::InvalidInput(format!(
112 "QWEN3_TTS_BACKEND_FALLBACK: unknown backend '{other}' (expected cpu, metal, vulkan)"
113 )));
114 }
115 };
116 if !order.contains(&pref) {
117 order.push(pref);
118 }
119 }
120 if order.is_empty() {
121 return Err(Qwen3TtsError::InvalidInput(
122 "QWEN3_TTS_BACKEND_FALLBACK must contain at least one backend".into(),
123 ));
124 }
125 Ok(order)
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum BackendKind {
130 Cpu,
131 #[cfg(all(feature = "metal", target_vendor = "apple"))]
132 Metal,
133 #[cfg(feature = "vulkan")]
134 Vulkan,
135}
136
137impl BackendKind {
138 pub fn as_str(self) -> &'static str {
139 match self {
140 Self::Cpu => "CPU",
141 #[cfg(all(feature = "metal", target_vendor = "apple"))]
142 Self::Metal => "Metal",
143 #[cfg(feature = "vulkan")]
144 Self::Vulkan => "Vulkan",
145 }
146 }
147}
148
149#[derive(Clone)]
150pub(crate) struct BackendSet(Arc<BackendSetInner>);
151
152unsafe impl Send for BackendSet {}
156unsafe impl Sync for BackendSet {}
157
158struct BackendSetInner {
159 primary: OwnedBackend,
160 primary_kind: BackendKind,
161 cpu_fallback: Option<OwnedBackend>,
162 primary_galloc: Mutex<OwnedGallocr>,
163}
164
165impl BackendSet {
166 pub(crate) fn new() -> Result<Self, Qwen3TtsError> {
167 unsafe {
168 sys::ggml_backend_load_all();
169 sys::ggml_cpu_init();
170 }
171
172 match parse_qts_backend()? {
173 BackendChoice::Auto => Self::auto_select_backend(&parse_auto_backend_order()?),
174 BackendChoice::Explicit(choice) => Self::require_backend(choice),
175 }
176 }
177
178 fn auto_select_backend(order: &[BackendPreference]) -> Result<Self, Qwen3TtsError> {
179 if backend_debug_enabled() {
180 let chain = order
181 .iter()
182 .map(|choice| choice.as_env_str())
183 .collect::<Vec<_>>()
184 .join(" -> ");
185 eprintln!("[backend-debug] auto fallback chain: {chain}");
186 }
187 for choice in order {
188 if let Some(backends) = Self::try_backend(*choice)? {
189 return Ok(backends);
190 }
191 }
192 Err(Qwen3TtsError::InvalidInput(
193 "QWEN3_TTS_BACKEND_FALLBACK did not contain a usable backend".into(),
194 ))
195 }
196
197 fn require_backend(choice: BackendPreference) -> Result<Self, Qwen3TtsError> {
198 match choice {
199 BackendPreference::Cpu => {
200 let primary = OwnedBackend::cpu()?;
201 if backend_debug_enabled() {
202 eprintln!("[backend-debug] selected {}", BackendKind::Cpu.as_str());
203 }
204 Self::with_primary(primary, BackendKind::Cpu, None)
205 }
206 BackendPreference::Metal => {
207 #[cfg(all(feature = "metal", target_vendor = "apple"))]
208 {
209 Self::require_reg_backend(ggml_reg_mtl(), BackendKind::Metal)
210 }
211 #[cfg(not(all(feature = "metal", target_vendor = "apple")))]
212 {
213 Err(Qwen3TtsError::InvalidInput(
214 "QWEN3_TTS_BACKEND=metal is only valid on Apple targets with the `metal` feature"
215 .into(),
216 ))
217 }
218 }
219 BackendPreference::Vulkan => {
220 #[cfg(feature = "vulkan")]
221 {
222 Self::require_reg_backend(ggml_reg_vulkan(), BackendKind::Vulkan)
223 }
224 #[cfg(not(feature = "vulkan"))]
225 {
226 Err(Qwen3TtsError::InvalidInput(
227 "QWEN3_TTS_BACKEND=vulkan requires building with --features vulkan".into(),
228 ))
229 }
230 }
231 }
232 }
233
234 fn try_backend(choice: BackendPreference) -> Result<Option<Self>, Qwen3TtsError> {
235 match choice {
236 BackendPreference::Cpu => {
237 let primary = OwnedBackend::cpu()?;
238 if backend_debug_enabled() {
239 eprintln!("[backend-debug] selected {}", BackendKind::Cpu.as_str());
240 }
241 Ok(Some(Self::with_primary(primary, BackendKind::Cpu, None)?))
242 }
243 BackendPreference::Metal => {
244 #[cfg(all(feature = "metal", target_vendor = "apple"))]
245 {
246 Self::try_optional_reg(ggml_reg_mtl(), BackendKind::Metal)
247 }
248 #[cfg(not(all(feature = "metal", target_vendor = "apple")))]
249 {
250 if backend_debug_enabled() {
251 eprintln!(
252 "[backend-debug] skipping metal (not supported by this build/target)"
253 );
254 }
255 Ok(None)
256 }
257 }
258 BackendPreference::Vulkan => {
259 #[cfg(feature = "vulkan")]
260 {
261 Self::try_optional_reg(ggml_reg_vulkan(), BackendKind::Vulkan)
262 }
263 #[cfg(not(feature = "vulkan"))]
264 {
265 if backend_debug_enabled() {
266 eprintln!("[backend-debug] skipping vulkan (not enabled in this build)");
267 }
268 Ok(None)
269 }
270 }
271 }
272 }
273
274 #[cfg(any(all(feature = "metal", target_vendor = "apple"), feature = "vulkan"))]
275 fn try_optional_reg(reg: &CStr, kind: BackendKind) -> Result<Option<Self>, Qwen3TtsError> {
276 let label = kind.as_str();
277 if let Some(primary) = OwnedBackend::init_from_reg(reg, label, false)? {
278 if backend_debug_enabled() {
279 eprintln!("[backend-debug] selected {label}");
280 }
281 return Ok(Some(Self::with_primary(
282 primary,
283 kind,
284 Some(OwnedBackend::cpu()?),
285 )?));
286 }
287
288 if backend_debug_enabled() {
289 eprintln!("[backend-debug] {label} unavailable, falling back");
290 }
291 Ok(None)
292 }
293
294 #[cfg(any(all(feature = "metal", target_vendor = "apple"), feature = "vulkan"))]
295 fn require_reg_backend(reg: &CStr, kind: BackendKind) -> Result<Self, Qwen3TtsError> {
296 let label = kind.as_str();
297 let primary = OwnedBackend::init_from_reg(reg, label, true)?.ok_or_else(|| {
298 Qwen3TtsError::InvalidInput(format!(
299 "QWEN3_TTS_BACKEND requested {label}, but GPU backend init failed (see QWEN3_TTS_GPU_DEVICE, drivers, or SDK)"
300 ))
301 })?;
302 if backend_debug_enabled() {
303 eprintln!("[backend-debug] selected {label} (QWEN3_TTS_BACKEND)");
304 }
305 Self::with_primary(primary, kind, Some(OwnedBackend::cpu()?))
306 }
307
308 #[allow(clippy::arc_with_non_send_sync)]
309 fn with_primary(
310 primary: OwnedBackend,
311 primary_kind: BackendKind,
312 cpu_fallback: Option<OwnedBackend>,
313 ) -> Result<Self, Qwen3TtsError> {
314 let primary_galloc = Mutex::new(OwnedGallocr::new(primary.as_ptr())?);
315 Ok(Self(Arc::new(BackendSetInner {
316 primary,
317 primary_kind,
318 cpu_fallback,
319 primary_galloc,
320 })))
321 }
322
323 pub(crate) fn primary_ptr(&self) -> sys::ggml_backend_t {
324 self.0.primary.as_ptr()
325 }
326
327 pub(crate) fn primary_kind(&self) -> BackendKind {
328 self.0.primary_kind
329 }
330
331 pub(crate) fn configure_threads(&self, thread_count: usize) {
332 self.0.primary.set_threads(thread_count);
333 if let Some(cpu_fallback) = &self.0.cpu_fallback {
334 cpu_fallback.set_threads(thread_count);
335 }
336 }
337
338 fn primary_galloc(&self) -> MutexGuard<'_, OwnedGallocr> {
339 self.0.primary_galloc.lock().unwrap()
340 }
341}
342
343struct OwnedBackend {
344 raw: NonNull<sys::ggml_backend>,
345 is_cpu: bool,
346}
347
348impl OwnedBackend {
349 fn cpu() -> Result<Self, Qwen3TtsError> {
350 let raw = unsafe { sys::ggml_backend_cpu_init() };
351 let raw = NonNull::new(raw).ok_or_else(|| {
352 Qwen3TtsError::InvalidInput("failed to initialize ggml CPU backend".into())
353 })?;
354 Ok(Self { raw, is_cpu: true })
355 }
356
357 #[cfg(any(all(feature = "metal", target_vendor = "apple"), feature = "vulkan"))]
362 fn init_from_reg(
363 reg: &CStr,
364 label: &str,
365 require: bool,
366 ) -> Result<Option<Self>, Qwen3TtsError> {
367 let device_index = gpu_device_index()?;
368
369 let reg_ptr = unsafe { sys::ggml_backend_reg_by_name(reg.as_ptr()) };
370 if reg_ptr.is_null() {
371 return Ok(None);
372 }
373
374 let n = unsafe { sys::ggml_backend_reg_dev_count(reg_ptr) };
375 if n == 0 {
376 return Ok(None);
377 }
378
379 if device_index >= n {
380 return Err(Qwen3TtsError::InvalidInput(format!(
381 "QWEN3_TTS_GPU_DEVICE={device_index} is out of range for {label} (available: 0..{})",
382 n.saturating_sub(1)
383 )));
384 }
385
386 let dev = unsafe { sys::ggml_backend_reg_dev_get(reg_ptr, device_index) };
387 if dev.is_null() {
388 return Ok(None);
389 }
390 let raw = unsafe { sys::ggml_backend_dev_init(dev, std::ptr::null()) };
391 let Some(raw) = NonNull::new(raw) else {
392 if require {
393 return Err(Qwen3TtsError::InvalidInput(format!(
394 "QWEN3_TTS_BACKEND requested {label}, but ggml_backend_dev_init failed for device index {device_index}"
395 )));
396 }
397 return Ok(None);
398 };
399
400 Ok(Some(Self { raw, is_cpu: false }))
401 }
402
403 fn as_ptr(&self) -> sys::ggml_backend_t {
404 self.raw.as_ptr()
405 }
406
407 fn set_threads(&self, thread_count: usize) {
408 if self.is_cpu {
409 unsafe {
410 sys::ggml_backend_cpu_set_n_threads(
411 self.raw.as_ptr(),
412 normalize_threads(thread_count),
413 );
414 }
415 }
416 }
417}
418
419impl Drop for OwnedBackend {
420 fn drop(&mut self) {
421 unsafe {
422 sys::ggml_backend_free(self.raw.as_ptr());
423 }
424 }
425}
426
427struct OwnedGallocr {
428 raw: NonNull<sys::ggml_gallocr>,
429}
430
431impl OwnedGallocr {
432 fn new(backend: sys::ggml_backend_t) -> Result<Self, Qwen3TtsError> {
433 let raw =
434 unsafe { sys::ggml_gallocr_new(sys::ggml_backend_get_default_buffer_type(backend)) };
435 let raw = NonNull::new(raw).ok_or_else(|| {
436 Qwen3TtsError::InvalidInput("failed to initialize ggml graph allocator".into())
437 })?;
438 Ok(Self { raw })
439 }
440}
441
442impl Drop for OwnedGallocr {
443 fn drop(&mut self) {
444 unsafe {
445 sys::ggml_gallocr_free(self.raw.as_ptr());
446 }
447 }
448}
449
450pub(crate) struct TensorUpload<'a> {
451 pub(crate) tensor: *mut sys::ggml_tensor,
452 pub(crate) bytes: &'a [u8],
453}
454
455pub(crate) struct TensorDownload<'a> {
456 pub(crate) tensor: *mut sys::ggml_tensor,
457 pub(crate) bytes: &'a mut [u8],
458}
459
460pub(crate) struct OwnedBuffer {
461 raw: NonNull<sys::ggml_backend_buffer>,
462}
463
464impl OwnedBuffer {
465 pub(crate) fn alloc(
466 ctx: *mut sys::ggml_context,
467 backend: sys::ggml_backend_t,
468 ) -> Result<Self, Qwen3TtsError> {
469 let raw = unsafe { sys::ggml_backend_alloc_ctx_tensors(ctx, backend) };
470 let raw = NonNull::new(raw).ok_or_else(|| {
471 Qwen3TtsError::InvalidInput("failed to allocate ggml backend tensor buffer".into())
472 })?;
473 Ok(Self { raw })
474 }
475}
476
477impl Drop for OwnedBuffer {
478 fn drop(&mut self) {
479 unsafe {
480 sys::ggml_backend_buffer_free(self.raw.as_ptr());
481 }
482 }
483}
484
485pub(crate) fn graph_metadata_mem_size(max_nodes: usize) -> usize {
486 let tensor_overhead = unsafe { sys::ggml_tensor_overhead() };
487 let graph_overhead = unsafe { sys::ggml_graph_overhead_custom(max_nodes, false) };
488 max(
489 1024 * 1024,
490 graph_overhead + tensor_overhead * max_nodes * 16,
491 )
492}
493
494pub(crate) fn slice_as_bytes<T>(slice: &[T]) -> &[u8] {
495 unsafe { std::slice::from_raw_parts(slice.as_ptr().cast::<u8>(), std::mem::size_of_val(slice)) }
496}
497
498pub(crate) fn slice_as_bytes_mut<T>(slice: &mut [T]) -> &mut [u8] {
499 unsafe {
500 std::slice::from_raw_parts_mut(
501 slice.as_mut_ptr().cast::<u8>(),
502 std::mem::size_of_val(slice),
503 )
504 }
505}
506
507pub(crate) fn packed_softmax_diag_mask_f32(ne: [i64; 4], n_past: i32) -> Vec<f32> {
510 let n0 = ne[0].max(0) as usize;
511 let n1 = ne[1].max(0) as usize;
512 let n2 = ne[2].max(0) as usize;
513 let n3 = ne[3].max(0) as usize;
514 let total = n0.saturating_mul(n1).saturating_mul(n2).saturating_mul(n3);
515 let mut out = vec![0.0f32; total];
516 let np = n_past.max(0) as usize;
517 for i3 in 0..n3 {
518 for i2 in 0..n2 {
519 for j in 0..n1 {
520 for i in 0..n0 {
521 let v = if i >= np && i > np.saturating_add(j) {
522 f32::NEG_INFINITY
523 } else {
524 0.0
525 };
526 let idx = i + n0 * (j + n1 * (i2 + n2 * i3));
527 if let Some(slot) = out.get_mut(idx) {
528 *slot = v;
529 }
530 }
531 }
532 }
533 }
534 out
535}
536
537pub(crate) unsafe fn ggml_soft_max_ext_with_diag_mask_cache(
539 ctx: *mut sys::ggml_context,
540 kq: *mut sys::ggml_tensor,
541 n_past: i32,
542 cache: &mut Option<(*mut sys::ggml_tensor, Vec<f32>)>,
543) -> *mut sys::ggml_tensor {
544 let mask_ptr = match cache {
545 None => {
546 let ne = (*kq).ne;
547 let data = packed_softmax_diag_mask_f32(ne, n_past);
548 let t = sys::ggml_new_tensor_4d(
549 ctx,
550 sys::ggml_type_GGML_TYPE_F32,
551 ne[0],
552 ne[1],
553 ne[2],
554 ne[3],
555 );
556 assert!(
557 !t.is_null(),
558 "ggml_new_tensor_4d failed for softmax causal mask"
559 );
560 *cache = Some((t, data));
561 t
562 }
563 Some((mask_t, _)) => *mask_t,
564 };
565 sys::ggml_soft_max_ext(ctx, kq, mask_ptr, 1.0, 0.0)
566}
567
568pub(crate) fn execute_graph(
569 backends: &BackendSet,
570 graph: NonNull<sys::ggml_cgraph>,
571 uploads: &[TensorUpload<'_>],
572 downloads: &mut [TensorDownload<'_>],
573 thread_count: usize,
574 error_message: &str,
575) -> Result<(), Qwen3TtsError> {
576 maybe_log_backend_support(backends, graph, error_message);
577 backends.configure_threads(thread_count);
578 let galloc = backends.primary_galloc();
579 let allocated = unsafe { sys::ggml_gallocr_alloc_graph(galloc.raw.as_ptr(), graph.as_ptr()) };
580 if !allocated {
581 return Err(Qwen3TtsError::InvalidInput(format!(
582 "failed to allocate backend graph for {error_message}"
583 )));
584 }
585 run_graph_impl(backends, graph, uploads, downloads, error_message)
586}
587
588fn run_graph_impl(
589 backends: &BackendSet,
590 graph: NonNull<sys::ggml_cgraph>,
591 uploads: &[TensorUpload<'_>],
592 downloads: &mut [TensorDownload<'_>],
593 error_message: &str,
594) -> Result<(), Qwen3TtsError> {
595 let _t0 = std::time::Instant::now();
596 for upload in uploads {
597 unsafe {
598 sys::ggml_backend_tensor_set(
599 upload.tensor,
600 upload.bytes.as_ptr().cast(),
601 0,
602 upload.bytes.len(),
603 );
604 }
605 }
606 let _t1 = std::time::Instant::now();
607 let status = unsafe { sys::ggml_backend_graph_compute(backends.primary_ptr(), graph.as_ptr()) };
608 if status != sys::ggml_status_GGML_STATUS_SUCCESS {
609 return Err(Qwen3TtsError::InvalidInput(error_message.into()));
610 }
611 let _t2 = std::time::Instant::now();
612 for download in downloads {
613 unsafe {
614 sys::ggml_backend_tensor_get(
615 download.tensor,
616 download.bytes.as_mut_ptr().cast(),
617 0,
618 download.bytes.len(),
619 );
620 }
621 }
622 let _t3 = std::time::Instant::now();
623 if backend_debug_enabled() {
624 eprintln!(
625 "[graph_impl] upload={:.2}ms compute={:.2}ms download={:.2}ms ({error_message})",
626 (_t1 - _t0).as_secs_f64() * 1000.0,
627 (_t2 - _t1).as_secs_f64() * 1000.0,
628 (_t3 - _t2).as_secs_f64() * 1000.0,
629 );
630 }
631 Ok(())
632}
633
634fn maybe_log_backend_support(backends: &BackendSet, graph: NonNull<sys::ggml_cgraph>, label: &str) {
635 if !backend_debug_enabled() {
636 return;
637 }
638
639 let n_nodes = unsafe { sys::ggml_graph_n_nodes(graph.as_ptr()) };
640 let mut supported = 0usize;
641 let mut offloaded = 0usize;
642 let mut unsupported_ops = BTreeMap::<String, usize>::new();
643 for idx in 0..n_nodes {
644 let node = unsafe { sys::ggml_graph_node(graph.as_ptr(), idx) };
645 if node.is_null() {
646 continue;
647 }
648 let is_supported = unsafe { sys::ggml_backend_supports_op(backends.primary_ptr(), node) };
649 let is_offloaded = unsafe { sys::ggml_backend_offload_op(backends.primary_ptr(), node) };
650 if is_supported {
651 supported += 1;
652 } else {
653 let op = unsafe {
654 let desc = sys::ggml_op_desc(node);
655 if desc.is_null() {
656 "<unknown>".to_string()
657 } else {
658 CStr::from_ptr(desc).to_string_lossy().into_owned()
659 }
660 };
661 *unsupported_ops.entry(op).or_default() += 1;
662 }
663 if is_offloaded {
664 offloaded += 1;
665 }
666 }
667
668 eprintln!(
669 "[backend-debug] {label}: nodes={n_nodes} supported={supported} offloaded={offloaded}"
670 );
671 for (op, count) in unsupported_ops.into_iter().take(12) {
672 eprintln!("[backend-debug] unsupported {op}: {count}");
673 }
674}
675
676fn backend_debug_enabled() -> bool {
677 std::env::var_os("QWEN3_TTS_DEBUG_BACKEND").is_some()
678}
679
680fn normalize_threads(thread_count: usize) -> i32 {
681 max(1, thread_count) as i32
682}