Skip to main content

rlx_cpu/
spd_kernels.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, version 3.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! CPU kernels for the core Riemannian / SPD-manifold ops (`Op::BiMap`,
17//! `Op::ReEig`, `Op::LogEig`, `Op::SpdBatchNorm`, `Op::SpdKarcherMean`
18//! and their backwards).
19//!
20//! These reuse the `CpuKernel` scaffold — the same typed-view contract
21//! as user custom ops — so the core ops ride the existing
22//! `Thunk::CustomOp` execution path (both the closure-compiled and the
23//! match-based executors) without bespoke `Thunk` variants. The compute
24//! bodies live in [`crate::spd`]; per-op parameters (`eps`/`iters`/`tol`)
25//! are baked into the kernel struct at compile time (F64, CPU-first).
26
27use crate::op_registry::{CpuKernel, CpuTensorMut, CpuTensorRef};
28use crate::spd;
29use rlx_ir::SpdMatFn;
30
31/// Read the `(rows, cols)` of an `[n,n]`-style square matrix input.
32fn dim(t: &CpuTensorRef<'_>, i: usize) -> usize {
33    t.shape().dim(i).unwrap_static()
34}
35
36/// BiMap forward: `Y = W · X · Wᵀ`. Inputs `[W [m,n], X [n,n]]`.
37pub struct BiMapKernel;
38impl CpuKernel for BiMapKernel {
39    fn name(&self) -> &str {
40        "core.bimap"
41    }
42    fn execute(
43        &self,
44        inputs: &[CpuTensorRef<'_>],
45        output: CpuTensorMut<'_>,
46        _attrs: &[u8],
47    ) -> Result<(), String> {
48        let w = inputs[0].expect_f64("bimap W")?;
49        let x = inputs[1].expect_f64("bimap X")?;
50        let (m, n) = (dim(&inputs[0], 0), dim(&inputs[0], 1));
51        let out = output.expect_f64_mut("bimap Y")?;
52        out.copy_from_slice(&spd::bimap(w, x, m, n));
53        Ok(())
54    }
55}
56
57/// ReEig forward: `Y = U max(ε, Σ) Uᵀ`. Input `[X [n,n]]`; output is
58/// the packed `[Y (n²), λ (n), U (n²)]` so the backward reuses the
59/// eigendecomposition. The builder narrows out `Y`.
60pub struct ReEigKernel {
61    pub eps: f64,
62}
63impl CpuKernel for ReEigKernel {
64    fn name(&self) -> &str {
65        "core.reeig"
66    }
67    fn execute(
68        &self,
69        inputs: &[CpuTensorRef<'_>],
70        output: CpuTensorMut<'_>,
71        _attrs: &[u8],
72    ) -> Result<(), String> {
73        let x = inputs[0].expect_f64("reeig X")?;
74        let n = dim(&inputs[0], 0);
75        let eps = self.eps;
76        let out = output.expect_f64_mut("reeig packed")?;
77        out.copy_from_slice(&spd::spectral_forward_packed(x, n, |l| l.max(eps)));
78        Ok(())
79    }
80}
81
82/// LogEig forward: `Y = logm(X)`. Packed `[Y, λ, U]` output (see [`ReEigKernel`]).
83pub struct LogEigKernel {
84    pub eps: f64,
85}
86impl CpuKernel for LogEigKernel {
87    fn name(&self) -> &str {
88        "core.logeig"
89    }
90    fn execute(
91        &self,
92        inputs: &[CpuTensorRef<'_>],
93        output: CpuTensorMut<'_>,
94        _attrs: &[u8],
95    ) -> Result<(), String> {
96        let x = inputs[0].expect_f64("logeig X")?;
97        let n = dim(&inputs[0], 0);
98        let eps = self.eps;
99        let out = output.expect_f64_mut("logeig packed")?;
100        out.copy_from_slice(&spd::spectral_forward_packed(x, n, |l| l.max(eps).ln()));
101        Ok(())
102    }
103}
104
105/// SPD batch-norm transport forward. Inputs `[X [B,n,n], mean [n,n], G [n,n]]`.
106pub struct SpdBatchNormKernel {
107    pub eps: f64,
108}
109impl CpuKernel for SpdBatchNormKernel {
110    fn name(&self) -> &str {
111        "core.spd_batch_norm"
112    }
113    fn execute(
114        &self,
115        inputs: &[CpuTensorRef<'_>],
116        output: CpuTensorMut<'_>,
117        _attrs: &[u8],
118    ) -> Result<(), String> {
119        let x = inputs[0].expect_f64("spd_bn X")?;
120        let mean = inputs[1].expect_f64("spd_bn mean")?;
121        let g = inputs[2].expect_f64("spd_bn G")?;
122        let (batch, n) = (dim(&inputs[0], 0), dim(&inputs[0], 1));
123        let out = output.expect_f64_mut("spd_bn Y")?;
124        out.copy_from_slice(&spd::spd_bn_transport(x, mean, g, batch, n, self.eps));
125        Ok(())
126    }
127}
128
129/// Batch Fréchet/Karcher mean. Input `[X [B,n,n]]` → `[n,n]`.
130pub struct SpdKarcherMeanKernel {
131    pub iters: usize,
132    pub tol: f64,
133}
134impl CpuKernel for SpdKarcherMeanKernel {
135    fn name(&self) -> &str {
136        "core.spd_karcher_mean"
137    }
138    fn execute(
139        &self,
140        inputs: &[CpuTensorRef<'_>],
141        output: CpuTensorMut<'_>,
142        _attrs: &[u8],
143    ) -> Result<(), String> {
144        let x = inputs[0].expect_f64("karcher X")?;
145        let (batch, n) = (dim(&inputs[0], 0), dim(&inputs[0], 1));
146        let covs: Vec<Vec<f64>> = (0..batch)
147            .map(|bi| x[bi * n * n..(bi + 1) * n * n].to_vec())
148            .collect();
149        let out = output.expect_f64_mut("karcher mean")?;
150        out.copy_from_slice(&spd::karcher_mean(&covs, n, self.iters, self.tol));
151        Ok(())
152    }
153}
154
155/// Weighted batch Fréchet/Karcher mean. Inputs `[X [B,n,n], w [B]]` → `[n,n]`.
156pub struct SpdKarcherMeanWeightedKernel {
157    pub iters: usize,
158    pub tol: f64,
159}
160impl CpuKernel for SpdKarcherMeanWeightedKernel {
161    fn name(&self) -> &str {
162        "core.spd_karcher_mean_weighted"
163    }
164    fn execute(
165        &self,
166        inputs: &[CpuTensorRef<'_>],
167        output: CpuTensorMut<'_>,
168        _attrs: &[u8],
169    ) -> Result<(), String> {
170        let x = inputs[0].expect_f64("karcher_w X")?;
171        let w = inputs[1].expect_f64("karcher_w weights")?;
172        let (batch, n) = (dim(&inputs[0], 0), dim(&inputs[0], 1));
173        let covs: Vec<Vec<f64>> = (0..batch)
174            .map(|bi| x[bi * n * n..(bi + 1) * n * n].to_vec())
175            .collect();
176        let out = output.expect_f64_mut("karcher_w mean")?;
177        out.copy_from_slice(&spd::karcher_mean_weighted(
178            &covs, w, n, self.iters, self.tol,
179        ));
180        Ok(())
181    }
182}
183
184/// AIRM log map at an arbitrary base. Inputs `[base [n,n], X [n,n]]` → `[n,n]`.
185pub struct SpdLogMapKernel;
186impl CpuKernel for SpdLogMapKernel {
187    fn name(&self) -> &str {
188        "core.spd_log_map"
189    }
190    fn execute(
191        &self,
192        inputs: &[CpuTensorRef<'_>],
193        output: CpuTensorMut<'_>,
194        _attrs: &[u8],
195    ) -> Result<(), String> {
196        let base = inputs[0].expect_f64("log_map base")?;
197        let x = inputs[1].expect_f64("log_map X")?;
198        let n = dim(&inputs[0], 0);
199        let out = output.expect_f64_mut("log_map V")?;
200        out.copy_from_slice(&spd::log_map(base, x, n));
201        Ok(())
202    }
203}
204
205/// AIRM exp map at an arbitrary base. Inputs `[base [n,n], V [n,n]]` → `[n,n]`.
206pub struct SpdExpMapKernel;
207impl CpuKernel for SpdExpMapKernel {
208    fn name(&self) -> &str {
209        "core.spd_exp_map"
210    }
211    fn execute(
212        &self,
213        inputs: &[CpuTensorRef<'_>],
214        output: CpuTensorMut<'_>,
215        _attrs: &[u8],
216    ) -> Result<(), String> {
217        let base = inputs[0].expect_f64("exp_map base")?;
218        let v = inputs[1].expect_f64("exp_map V")?;
219        let n = dim(&inputs[0], 0);
220        let out = output.expect_f64_mut("exp_map Y")?;
221        out.copy_from_slice(&spd::exp_map(base, v, n));
222        Ok(())
223    }
224}
225
226/// AIRM parallel transport. Inputs `[from [n,n], to [n,n], V [n,n]]` → `[n,n]`.
227pub struct SpdParallelTransportKernel;
228impl CpuKernel for SpdParallelTransportKernel {
229    fn name(&self) -> &str {
230        "core.spd_parallel_transport"
231    }
232    fn execute(
233        &self,
234        inputs: &[CpuTensorRef<'_>],
235        output: CpuTensorMut<'_>,
236        _attrs: &[u8],
237    ) -> Result<(), String> {
238        let from = inputs[0].expect_f64("transport from")?;
239        let to = inputs[1].expect_f64("transport to")?;
240        let v = inputs[2].expect_f64("transport V")?;
241        let n = dim(&inputs[0], 0);
242        let out = output.expect_f64_mut("transport out")?;
243        out.copy_from_slice(&spd::parallel_transport(from, to, v, n));
244        Ok(())
245    }
246}
247
248/// Batched SPD spectral matrix function. Input `[X [B,n,n]]` → `[B,n,n]`.
249pub struct SpdMatrixFnBatchKernel {
250    pub kind: SpdMatFn,
251}
252impl CpuKernel for SpdMatrixFnBatchKernel {
253    fn name(&self) -> &str {
254        "core.spd_matrix_fn_batch"
255    }
256    fn execute(
257        &self,
258        inputs: &[CpuTensorRef<'_>],
259        output: CpuTensorMut<'_>,
260        _attrs: &[u8],
261    ) -> Result<(), String> {
262        let x = inputs[0].expect_f64("spd_matfn X")?;
263        let (batch, n) = (dim(&inputs[0], 0), dim(&inputs[0], 1));
264        let covs: Vec<Vec<f64>> = (0..batch)
265            .map(|bi| x[bi * n * n..(bi + 1) * n * n].to_vec())
266            .collect();
267        let res = match self.kind {
268            SpdMatFn::Logm => spd::logm_batch(&covs, n),
269            SpdMatFn::Expm => spd::expm_batch(&covs, n),
270            SpdMatFn::Sqrtm => spd::sqrtm_batch(&covs, n),
271            SpdMatFn::Invsqrtm => spd::invsqrtm_batch(&covs, n),
272        };
273        let out = output.expect_f64_mut("spd_matfn Y")?;
274        out.copy_from_slice(&res.concat());
275        Ok(())
276    }
277}
278
279/// SpdLogMap backward. Inputs `[base [n,n], x [n,n], dY [n,n]]` →
280/// packed `[d_base ∥ d_x]` `[2,n,n]`.
281pub struct SpdLogMapBackwardKernel;
282impl CpuKernel for SpdLogMapBackwardKernel {
283    fn name(&self) -> &str {
284        "core.spd_log_map_backward"
285    }
286    fn execute(
287        &self,
288        inputs: &[CpuTensorRef<'_>],
289        output: CpuTensorMut<'_>,
290        _attrs: &[u8],
291    ) -> Result<(), String> {
292        let base = inputs[0].expect_f64("log_map_bwd base")?;
293        let x = inputs[1].expect_f64("log_map_bwd x")?;
294        let dy = inputs[2].expect_f64("log_map_bwd dY")?;
295        let n = dim(&inputs[0], 0);
296        let (d_base, d_x) = spd::log_map_backward(base, x, dy, n);
297        let out = output.expect_f64_mut("log_map_bwd packed")?;
298        out[..n * n].copy_from_slice(&d_base);
299        out[n * n..].copy_from_slice(&d_x);
300        Ok(())
301    }
302}
303
304/// SpdExpMap backward. Inputs `[base [n,n], v [n,n], dY [n,n]]` →
305/// packed `[d_base ∥ d_v]` `[2,n,n]`.
306pub struct SpdExpMapBackwardKernel;
307impl CpuKernel for SpdExpMapBackwardKernel {
308    fn name(&self) -> &str {
309        "core.spd_exp_map_backward"
310    }
311    fn execute(
312        &self,
313        inputs: &[CpuTensorRef<'_>],
314        output: CpuTensorMut<'_>,
315        _attrs: &[u8],
316    ) -> Result<(), String> {
317        let base = inputs[0].expect_f64("exp_map_bwd base")?;
318        let v = inputs[1].expect_f64("exp_map_bwd v")?;
319        let dy = inputs[2].expect_f64("exp_map_bwd dY")?;
320        let n = dim(&inputs[0], 0);
321        let (d_base, d_v) = spd::exp_map_backward(base, v, dy, n);
322        let out = output.expect_f64_mut("exp_map_bwd packed")?;
323        out[..n * n].copy_from_slice(&d_base);
324        out[n * n..].copy_from_slice(&d_v);
325        Ok(())
326    }
327}
328
329/// SpdParallelTransport backward. Inputs `[from, to, v, dY]` (each `[n,n]`) →
330/// packed `[d_from ∥ d_to ∥ d_v]` `[3,n,n]`.
331pub struct SpdParallelTransportBackwardKernel;
332impl CpuKernel for SpdParallelTransportBackwardKernel {
333    fn name(&self) -> &str {
334        "core.spd_parallel_transport_backward"
335    }
336    fn execute(
337        &self,
338        inputs: &[CpuTensorRef<'_>],
339        output: CpuTensorMut<'_>,
340        _attrs: &[u8],
341    ) -> Result<(), String> {
342        let from = inputs[0].expect_f64("transport_bwd from")?;
343        let to = inputs[1].expect_f64("transport_bwd to")?;
344        let v = inputs[2].expect_f64("transport_bwd v")?;
345        let dy = inputs[3].expect_f64("transport_bwd dY")?;
346        let n = dim(&inputs[0], 0);
347        let (d_from, d_to, d_v) = spd::parallel_transport_backward(from, to, v, dy, n);
348        let out = output.expect_f64_mut("transport_bwd packed")?;
349        out[..n * n].copy_from_slice(&d_from);
350        out[n * n..2 * n * n].copy_from_slice(&d_to);
351        out[2 * n * n..].copy_from_slice(&d_v);
352        Ok(())
353    }
354}
355
356/// SpdMatrixFnBatch backward. Inputs `[X [B,n,n], dY [B,n,n]]` → `dX [B,n,n]`.
357pub struct SpdMatrixFnBatchBackwardKernel {
358    pub kind: SpdMatFn,
359}
360impl CpuKernel for SpdMatrixFnBatchBackwardKernel {
361    fn name(&self) -> &str {
362        "core.spd_matrix_fn_batch_backward"
363    }
364    fn execute(
365        &self,
366        inputs: &[CpuTensorRef<'_>],
367        output: CpuTensorMut<'_>,
368        _attrs: &[u8],
369    ) -> Result<(), String> {
370        let x = inputs[0].expect_f64("spd_matfn_bwd X")?;
371        let dy = inputs[1].expect_f64("spd_matfn_bwd dY")?;
372        let n = dim(&inputs[0], 1);
373        // Same (f, f') as the batch forward (see spd::{logm,expm,sqrtm,invsqrtm}_batch).
374        let dx = match self.kind {
375            SpdMatFn::Logm => spd::matrix_fn_batch_backward(
376                x,
377                dy,
378                n,
379                |l| l.max(1e-12).ln(),
380                |l| 1.0 / l.max(1e-12),
381            ),
382            SpdMatFn::Expm => spd::matrix_fn_batch_backward(x, dy, n, |l| l.exp(), |l| l.exp()),
383            SpdMatFn::Sqrtm => spd::matrix_fn_batch_backward(
384                x,
385                dy,
386                n,
387                |l| l.max(0.0).sqrt(),
388                |l| 0.5 / l.max(1e-12).sqrt(),
389            ),
390            SpdMatFn::Invsqrtm => spd::matrix_fn_batch_backward(
391                x,
392                dy,
393                n,
394                |l| 1.0 / l.max(1e-12).sqrt(),
395                |l| -0.5 * l.max(1e-12).powf(-1.5),
396            ),
397        };
398        let out = output.expect_f64_mut("spd_matfn_bwd dX")?;
399        out.copy_from_slice(&dx);
400        Ok(())
401    }
402}
403
404/// `n` from a packed `[λ ∥ U]` length `L = n²+n` (⇒ `n = (√(1+4L)−1)/2`).
405fn n_from_packed(len: usize) -> usize {
406    (((1 + 4 * len) as f64).sqrt().round() as usize - 1) / 2
407}
408
409/// Symmetric eigendecomposition. Input `[A n,n]` → packed `[λ ∥ U]` `[n²+n]`.
410pub struct EighKernel;
411impl CpuKernel for EighKernel {
412    fn name(&self) -> &str {
413        "core.eigh"
414    }
415    fn execute(
416        &self,
417        inputs: &[CpuTensorRef<'_>],
418        output: CpuTensorMut<'_>,
419        _attrs: &[u8],
420    ) -> Result<(), String> {
421        let a = inputs[0].expect_f64("eigh A")?;
422        let n = dim(&inputs[0], 0);
423        let out = output.expect_f64_mut("eigh packed")?;
424        out.copy_from_slice(&spd::eigh_packed(a, n));
425        Ok(())
426    }
427}
428
429/// Eigh backward. Inputs `[fwd [n²+n], bar [n²+n]]` → `Ā [n,n]`.
430pub struct EighBackwardKernel;
431impl CpuKernel for EighBackwardKernel {
432    fn name(&self) -> &str {
433        "core.eigh_backward"
434    }
435    fn execute(
436        &self,
437        inputs: &[CpuTensorRef<'_>],
438        output: CpuTensorMut<'_>,
439        _attrs: &[u8],
440    ) -> Result<(), String> {
441        let fwd = inputs[0].expect_f64("eigh_bwd fwd")?;
442        let bar = inputs[1].expect_f64("eigh_bwd bar")?;
443        let n = n_from_packed(fwd.len());
444        let out = output.expect_f64_mut("eigh_bwd dA")?;
445        out.copy_from_slice(&spd::eigh_backward_packed(fwd, bar, n));
446        Ok(())
447    }
448}
449
450/// Batched eigh. Input `[A B,n,n]` → packed `[B, n²+n]`.
451pub struct EighBatchKernel;
452impl CpuKernel for EighBatchKernel {
453    fn name(&self) -> &str {
454        "core.eigh_batch"
455    }
456    fn execute(
457        &self,
458        inputs: &[CpuTensorRef<'_>],
459        output: CpuTensorMut<'_>,
460        _attrs: &[u8],
461    ) -> Result<(), String> {
462        let a = inputs[0].expect_f64("eigh_batch A")?;
463        let (b, n) = (dim(&inputs[0], 0), dim(&inputs[0], 1));
464        let covs: Vec<Vec<f64>> = (0..b)
465            .map(|bi| a[bi * n * n..(bi + 1) * n * n].to_vec())
466            .collect();
467        let out = output.expect_f64_mut("eigh_batch packed")?;
468        out.copy_from_slice(&spd::eigh_batch_packed(&covs, n).concat());
469        Ok(())
470    }
471}
472
473/// Batched eigh backward. Inputs `[fwd [B,n²+n], bar [B,n²+n]]` → `Ā [B,n,n]`.
474pub struct EighBatchBackwardKernel;
475impl CpuKernel for EighBatchBackwardKernel {
476    fn name(&self) -> &str {
477        "core.eigh_batch_backward"
478    }
479    fn execute(
480        &self,
481        inputs: &[CpuTensorRef<'_>],
482        output: CpuTensorMut<'_>,
483        _attrs: &[u8],
484    ) -> Result<(), String> {
485        use rayon::prelude::*;
486        let fwd = inputs[0].expect_f64("eigh_batch_bwd fwd")?;
487        let bar = inputs[1].expect_f64("eigh_batch_bwd bar")?;
488        let (b, packed) = (dim(&inputs[0], 0), dim(&inputs[0], 1));
489        let n = n_from_packed(packed);
490        let da: Vec<f64> = (0..b)
491            .into_par_iter()
492            .flat_map(|bi| {
493                let f = &fwd[bi * packed..(bi + 1) * packed];
494                let bb = &bar[bi * packed..(bi + 1) * packed];
495                spd::eigh_backward_packed(f, bb, n)
496            })
497            .collect();
498        let out = output.expect_f64_mut("eigh_batch_bwd dA")?;
499        out.copy_from_slice(&da);
500        Ok(())
501    }
502}
503
504/// ReEig backward. Inputs `[λ [n], U [n²], dY [n²]]` → `dX [n,n]`,
505/// reusing the forward eigendecomposition (no recompute).
506pub struct ReEigBackwardKernel {
507    pub eps: f64,
508}
509impl CpuKernel for ReEigBackwardKernel {
510    fn name(&self) -> &str {
511        "core.reeig_backward"
512    }
513    fn execute(
514        &self,
515        inputs: &[CpuTensorRef<'_>],
516        output: CpuTensorMut<'_>,
517        _attrs: &[u8],
518    ) -> Result<(), String> {
519        let lam = inputs[0].expect_f64("reeig_bwd λ")?;
520        let u = inputs[1].expect_f64("reeig_bwd U")?;
521        let dy = inputs[2].expect_f64("reeig_bwd dY")?;
522        let n = lam.len();
523        let out = output.expect_f64_mut("reeig_bwd dX")?;
524        out.copy_from_slice(&spd::reeig_backward_precomputed(lam, u, dy, n, self.eps));
525        Ok(())
526    }
527}
528
529/// LogEig backward. Inputs `[λ [n], U [n²], dY [n²]]` → `dX [n,n]`.
530pub struct LogEigBackwardKernel {
531    pub eps: f64,
532}
533impl CpuKernel for LogEigBackwardKernel {
534    fn name(&self) -> &str {
535        "core.logeig_backward"
536    }
537    fn execute(
538        &self,
539        inputs: &[CpuTensorRef<'_>],
540        output: CpuTensorMut<'_>,
541        _attrs: &[u8],
542    ) -> Result<(), String> {
543        let lam = inputs[0].expect_f64("logeig_bwd λ")?;
544        let u = inputs[1].expect_f64("logeig_bwd U")?;
545        let dy = inputs[2].expect_f64("logeig_bwd dY")?;
546        let n = lam.len();
547        let out = output.expect_f64_mut("logeig_bwd dX")?;
548        out.copy_from_slice(&spd::logeig_backward_precomputed(lam, u, dy, n, self.eps));
549        Ok(())
550    }
551}
552
553/// SPD batch-norm backward w.r.t. X. Inputs `[mean [n,n], G [n,n], dY [B,n,n]]`.
554pub struct SpdBnBackwardXKernel {
555    pub eps: f64,
556}
557impl CpuKernel for SpdBnBackwardXKernel {
558    fn name(&self) -> &str {
559        "core.spd_batch_norm_backward_x"
560    }
561    fn execute(
562        &self,
563        inputs: &[CpuTensorRef<'_>],
564        output: CpuTensorMut<'_>,
565        _attrs: &[u8],
566    ) -> Result<(), String> {
567        let mean = inputs[0].expect_f64("spd_bn_bwd_x mean")?;
568        let g = inputs[1].expect_f64("spd_bn_bwd_x G")?;
569        let dy = inputs[2].expect_f64("spd_bn_bwd_x dY")?;
570        let (batch, n) = (dim(&inputs[2], 0), dim(&inputs[0], 0));
571        let out = output.expect_f64_mut("spd_bn_bwd_x dX")?;
572        out.copy_from_slice(&spd::spd_bn_backward_x(mean, g, dy, batch, n, self.eps));
573        Ok(())
574    }
575}
576
577/// SPD batch-norm backward w.r.t. G. Inputs `[X [B,n,n], mean [n,n], G [n,n], dY [B,n,n]]`.
578pub struct SpdBnBackwardGKernel {
579    pub eps: f64,
580}
581impl CpuKernel for SpdBnBackwardGKernel {
582    fn name(&self) -> &str {
583        "core.spd_batch_norm_backward_g"
584    }
585    fn execute(
586        &self,
587        inputs: &[CpuTensorRef<'_>],
588        output: CpuTensorMut<'_>,
589        _attrs: &[u8],
590    ) -> Result<(), String> {
591        let x = inputs[0].expect_f64("spd_bn_bwd_g X")?;
592        let mean = inputs[1].expect_f64("spd_bn_bwd_g mean")?;
593        let g = inputs[2].expect_f64("spd_bn_bwd_g G")?;
594        let dy = inputs[3].expect_f64("spd_bn_bwd_g dY")?;
595        let (batch, n) = (dim(&inputs[0], 0), dim(&inputs[0], 1));
596        let out = output.expect_f64_mut("spd_bn_bwd_g dG")?;
597        out.copy_from_slice(&spd::spd_bn_backward_g(x, mean, g, dy, batch, n, self.eps));
598        Ok(())
599    }
600}