Skip to main content

qualia_core_db/wgsl_forge/audio/
mel.rs

1//! Mel-filterbank apply as a certified forge kernel.
2//!
3//! Projects a row-major power spectrum (`n_frames × n_bins`) onto a triangular mel
4//! filterbank (`n_mel × n_bins`), producing mel energies (`n_frames × n_mel`). The
5//! operation is the matrix contraction
6//! `mel_out[f, m] = Σ_b spectrum[f, b] · mel_fb[m, b]`.
7//!
8//! Embeds [`shaders/audio_mel.wgsl`](../../../shaders/audio_mel.wgsl) via `include_str!`
9//! (single source of truth), grades it against the exact CPU oracle [`mel_apply_cpu`],
10//! and runs it on the auxiliary GPU circuit via [`mel_apply_forge`]. The public entry
11//! point [`mel_apply`] prefers the GPU when one is present and otherwise uses the CPU
12//! floor, so the call is never broken.
13//!
14//! Circuit placement: the forge kernel runs on the **auxiliary circuit (the iGPU when
15//! present)** so the primary/discrete GPU stays free for the LLM. Device selection goes
16//! through [`crate::gpu_context::device_registry::try_auxiliary_gpu`], which falls back
17//! auxiliary → primary → `None`; on `None` the forge path returns
18//! [`ForgeError::GpuUnavailable`] and [`mel_apply`] drops to the CPU floor — i.e. the
19//! effective placement chain is auxiliary → primary → CPU.
20
21use crate::wgsl_forge::ForgeError;
22
23/// The mel-apply kernel source (embedded from the canonical `.wgsl`).
24pub const MEL_APPLY_WGSL: &str = include_str!("../../shaders/audio_mel.wgsl");
25/// Entry-point name of [`MEL_APPLY_WGSL`].
26pub const MEL_APPLY_ENTRY: &str = "mel_apply";
27
28/// Exact CPU oracle for the mel-filterbank apply. Mirrors the WGSL scalar-for-scalar:
29/// for each output element `(frame, m)`, accumulates `spectrum[frame, b] · mel_fb[m, b]`
30/// over `b` in increasing order. `spectrum` is row-major `n_frames × n_bins`, `mel_fb`
31/// is row-major `n_mel × n_bins`; the result is row-major `n_frames × n_mel`.
32pub fn mel_apply_cpu(
33    spectrum: &[f32],
34    mel_fb: &[f32],
35    n_frames: usize,
36    n_bins: usize,
37    n_mel: usize,
38) -> Vec<f32> {
39    let mut out = vec![0.0f32; n_frames * n_mel];
40    for frame in 0..n_frames {
41        let spec_base = frame * n_bins;
42        for m in 0..n_mel {
43            let fb_base = m * n_bins;
44            let mut acc = 0.0f32;
45            for b in 0..n_bins {
46                acc += spectrum[spec_base + b] * mel_fb[fb_base + b];
47            }
48            out[frame * n_mel + m] = acc;
49        }
50    }
51    out
52}
53
54/// Run the mel-filterbank apply on the GPU and read back the result. Runs on the
55/// **auxiliary GPU circuit (the iGPU when present)** to keep the primary/discrete GPU
56/// free for the LLM: the device is taken from
57/// [`crate::gpu_context::device_registry::try_auxiliary_gpu`] (falls back
58/// auxiliary → primary → `None`) and the compute context is built with
59/// [`WgpuComputeContext::from_device`] on that shared device, rather than requesting its
60/// own HighPerformance adapter.
61///
62/// Uploads `spectrum` (binding 0, read), `mel_fb` (binding 1, read), a zeroed output
63/// (binding 2, read_write) and `params = [n_frames, n_bins, n_mel]` (binding 3, read),
64/// dispatches one invocation per output element, and reads back the `n_frames × n_mel`
65/// result. Returns [`ForgeError::GpuUnavailable`] when no GPU circuit is available (so
66/// [`mel_apply`] falls back auxiliary → primary → CPU), and [`ForgeError::GpuValidation`]
67/// on a shape/length mismatch.
68pub fn mel_apply_forge(
69    spectrum: &[f32],
70    mel_fb: &[f32],
71    n_frames: usize,
72    n_bins: usize,
73    n_mel: usize,
74) -> Result<Vec<f32>, ForgeError> {
75    use crate::wgsl_forge::execute::{
76        BindingUsage, QualiaCompute, WgpuComputeContext, WgpuPipeline,
77    };
78    use crate::wgsl_forge::Schedule;
79
80    if n_frames == 0 || n_bins == 0 || n_mel == 0 {
81        return Err(ForgeError::GpuValidation(format!(
82            "mel_apply_forge: dimensions must be non-zero (n_frames={n_frames}, n_bins={n_bins}, n_mel={n_mel})"
83        )));
84    }
85    if spectrum.len() != n_frames * n_bins {
86        return Err(ForgeError::GpuValidation(format!(
87            "mel_apply_forge: spectrum length {} != n_frames*n_bins {}",
88            spectrum.len(),
89            n_frames * n_bins
90        )));
91    }
92    if mel_fb.len() != n_mel * n_bins {
93        return Err(ForgeError::GpuValidation(format!(
94            "mel_apply_forge: mel_fb length {} != n_mel*n_bins {}",
95            mel_fb.len(),
96            n_mel * n_bins
97        )));
98    }
99
100    let out_len = n_frames * n_mel;
101    let total_floats = spectrum.len() + mel_fb.len() + out_len + 4;
102    let capacity = (total_floats * 4).max(4 << 20);
103    // Take the device on the auxiliary circuit (iGPU when present), falling back
104    // auxiliary → primary → None inside `try_auxiliary_gpu`. On `None` there is no GPU
105    // at all: return `GpuUnavailable` so the public `mel_apply` drops to the CPU floor.
106    let shared = crate::gpu_context::device_registry::try_auxiliary_gpu().ok_or_else(|| {
107        ForgeError::GpuUnavailable(
108            "mel_apply_forge: no GPU circuit available (auxiliary→primary both absent)".to_string(),
109        )
110    })?;
111    let mut ctx = WgpuComputeContext::from_device(
112        shared.device.clone(),
113        shared.queue.clone(),
114        &shared.adapter_caps,
115        capacity,
116    )?;
117
118    let view_spectrum = ctx.allocate_and_write(
119        bytemuck::cast_slice(spectrum),
120        0,
121        0,
122        BindingUsage::StorageRead,
123    )?;
124    let view_fb = ctx.allocate_and_write(
125        bytemuck::cast_slice(mel_fb),
126        1,
127        0,
128        BindingUsage::StorageRead,
129    )?;
130    let zeros = vec![0.0f32; out_len];
131    let view_out = ctx.allocate_and_write(
132        bytemuck::cast_slice(&zeros),
133        2,
134        0,
135        BindingUsage::StorageReadWrite,
136    )?;
137    let params = [n_frames as f32, n_bins as f32, n_mel as f32];
138    let view_params = ctx.allocate_and_write(
139        bytemuck::cast_slice(&params),
140        3,
141        0,
142        BindingUsage::StorageRead,
143    )?;
144
145    let buffers = vec![view_spectrum, view_fb, view_out, view_params];
146    let pipeline = WgpuPipeline::compile(&ctx, MEL_APPLY_WGSL, MEL_APPLY_ENTRY)?;
147    let schedule = Schedule {
148        workgroup_size: 64,
149        ..Default::default()
150    };
151    pipeline.dispatch(&buffers, &schedule, out_len)?;
152    let mut out = ctx.read_buffer_f32(&view_out)?;
153    out.truncate(out_len);
154    Ok(out)
155}
156
157/// Public entry point: run the mel-filterbank apply on the best path available on this
158/// machine. If a wgpu adapter is present ([`caps().wgpu`]), try [`mel_apply_forge`] and
159/// return its result on success; otherwise (no adapter, or a runtime GPU failure) fall
160/// back to the exact CPU oracle [`mel_apply_cpu`], so the call is never broken.
161///
162/// [`caps().wgpu`]: crate::wgsl_forge::dispatch::caps
163pub fn mel_apply(
164    spectrum: &[f32],
165    mel_fb: &[f32],
166    n_frames: usize,
167    n_bins: usize,
168    n_mel: usize,
169) -> Vec<f32> {
170    if crate::wgsl_forge::dispatch::caps().wgpu {
171        if let Ok(out) = mel_apply_forge(spectrum, mel_fb, n_frames, n_bins, n_mel) {
172            return out;
173        }
174        // Forge path was eligible but failed at runtime — fall through to the CPU floor
175        // rather than propagating, so the call is never broken.
176    }
177    mel_apply_cpu(spectrum, mel_fb, n_frames, n_bins, n_mel)
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::wgsl_forge::validate::validate_wgsl;
184
185    /// The mel-apply kernel must naga-validate under naga 30 and expose the `mel_apply`
186    /// entry point. Runs without a GPU — the always-on proof the shader is valid WGSL.
187    #[test]
188    fn mel_apply_wgsl_validates() {
189        let report = validate_wgsl(MEL_APPLY_WGSL).expect("mel-apply WGSL must naga-validate");
190        assert!(
191            report.entry_points.iter().any(|e| e == MEL_APPLY_ENTRY),
192            "validated module must expose {MEL_APPLY_ENTRY}; got {:?}",
193            report.entry_points
194        );
195    }
196
197    /// Hand-computed small case: n_frames=2, n_bins=4, n_mel=2 with a known filterbank.
198    /// Two triangular-ish bands, each covering two of the four bins.
199    #[test]
200    fn mel_apply_cpu_matches_reference() {
201        // spectrum: 2 frames × 4 bins.
202        //   frame 0: [1, 2, 3, 4]
203        //   frame 1: [5, 6, 7, 8]
204        let spectrum = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
205        // mel_fb: 2 mel bands × 4 bins.
206        //   band 0: [1, 1, 0, 0]  (low bins)
207        //   band 1: [0, 0, 1, 1]  (high bins)
208        let mel_fb = vec![1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0];
209
210        let out = mel_apply_cpu(&spectrum, &mel_fb, 2, 4, 2);
211
212        // frame 0: band0 = 1+2 = 3 ; band1 = 3+4 = 7
213        // frame 1: band0 = 5+6 = 11; band1 = 7+8 = 15
214        assert_eq!(out, vec![3.0, 7.0, 11.0, 15.0]);
215    }
216
217    /// The public entry point must agree with the CPU oracle. Works on GPU-less boxes
218    /// via the fallback (always runs) and on GPU boxes via the forge path.
219    #[test]
220    fn mel_apply_public_matches_cpu() {
221        let (n_frames, n_bins, n_mel) = (3usize, 5usize, 4usize);
222        let spectrum: Vec<f32> = (0..n_frames * n_bins)
223            .map(|k| (k as f32) * 0.5 - 3.0)
224            .collect();
225        // A deterministic overlapping-triangle-ish filterbank.
226        let mut mel_fb = vec![0.0f32; n_mel * n_bins];
227        for m in 0..n_mel {
228            for b in 0..n_bins {
229                mel_fb[m * n_bins + b] = ((m + b) as f32 % 3.0) * 0.25;
230            }
231        }
232        let public = mel_apply(&spectrum, &mel_fb, n_frames, n_bins, n_mel);
233        let oracle = mel_apply_cpu(&spectrum, &mel_fb, n_frames, n_bins, n_mel);
234        assert_eq!(public.len(), oracle.len());
235        for (p, o) in public.iter().zip(oracle.iter()) {
236            let tol = 1e-3 * o.abs().max(1.0);
237            assert!((p - o).abs() <= tol, "public/CPU mismatch: {p} vs {o}");
238        }
239    }
240
241    /// GPU certify: the kernel on a real adapter must match the CPU oracle within f32
242    /// tolerance over a deterministic multi-frame scene. Skips cleanly with no device.
243    #[test]
244    #[serial_test::serial(gpu)]
245    fn mel_gpu_matches_oracle() {
246        if !crate::wgsl_forge::test_gpu_available() {
247            return;
248        }
249        // Report which circuit/adapter the forge path resolved to (cheap, non-asserting:
250        // CI may expose only one GPU, so this is diagnostic only).
251        if let Some(shared) = crate::gpu_context::device_registry::try_auxiliary_gpu() {
252            let caps = &shared.adapter_caps;
253            eprintln!(
254                "mel_gpu_matches_oracle: forge on adapter '{}' ({:?}, {:?})",
255                caps.name, caps.device_type, caps.backend
256            );
257        }
258        let (n_frames, n_bins, n_mel) = (17usize, 33usize, 12usize);
259        let spectrum: Vec<f32> = (0..n_frames * n_bins)
260            .map(|k| ((k as f32) * 0.017).sin().abs() + 0.001)
261            .collect();
262        // Deterministic triangular filterbank: band m peaks around bin proportional to m.
263        let mut mel_fb = vec![0.0f32; n_mel * n_bins];
264        for m in 0..n_mel {
265            let centre = (m as f32 + 1.0) * (n_bins as f32) / (n_mel as f32 + 1.0);
266            for b in 0..n_bins {
267                let w = 1.0 - ((b as f32 - centre).abs() / 3.0);
268                mel_fb[m * n_bins + b] = w.max(0.0);
269            }
270        }
271        let expected = mel_apply_cpu(&spectrum, &mel_fb, n_frames, n_bins, n_mel);
272        let gpu = mel_apply_forge(&spectrum, &mel_fb, n_frames, n_bins, n_mel)
273            .expect("mel_apply_forge on an available device");
274        assert_eq!(gpu.len(), expected.len());
275        for (g, e) in gpu.iter().zip(expected.iter()) {
276            let tol = 1e-3 * e.abs().max(1.0);
277            assert!((g - e).abs() <= tol, "GPU/CPU mismatch: {g} vs {e}");
278        }
279    }
280}