1use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
52
53#[derive(Debug, Clone, Copy)]
55pub struct InterchangeDecodeForward<'a> {
56 pub z: ArrayView2<'a, f64>,
57 pub weights: ArrayView2<'a, f64>,
58 pub gate: ArrayView1<'a, f64>,
59 pub bias: Option<ArrayView1<'a, f64>>,
60}
61
62#[derive(Debug, Clone, Copy)]
64pub struct InterchangeSwapForward<'a> {
65 pub z_a: ArrayView2<'a, f64>,
66 pub z_b: ArrayView2<'a, f64>,
67 pub mask: ArrayView1<'a, bool>,
68 pub weights: ArrayView2<'a, f64>,
69 pub gate: ArrayView1<'a, f64>,
70 pub bias: Option<ArrayView1<'a, f64>>,
71}
72
73#[derive(Debug, Clone)]
75pub struct InterchangeDecodeBackward {
76 pub grad_z: Array2<f64>,
77 pub grad_weights: Array2<f64>,
78 pub grad_gate: Array1<f64>,
79 pub grad_bias: Option<Array1<f64>>,
80}
81
82#[derive(Debug, Clone)]
84pub struct InterchangeSwapBackward {
85 pub grad_z_a: Array2<f64>,
86 pub grad_z_b: Array2<f64>,
87 pub grad_weights: Array2<f64>,
88 pub grad_gate: Array1<f64>,
89 pub grad_bias: Option<Array1<f64>>,
90}
91
92fn check_shapes_forward(
93 z_rows: usize,
94 z_cols: usize,
95 weights: ArrayView2<'_, f64>,
96 gate: ArrayView1<'_, f64>,
97 bias: Option<ArrayView1<'_, f64>>,
98) -> Result<(), String> {
99 let (d, f_weights) = weights.dim();
100 if f_weights != z_cols {
101 return Err(format!(
102 "interchange_decode: weights has F={f_weights}, expected {z_cols}"
103 ));
104 }
105 if gate.len() != z_cols {
106 return Err(format!(
107 "interchange_decode: gate has length {}, expected {z_cols}",
108 gate.len()
109 ));
110 }
111 if let Some(b) = bias
112 && b.len() != d
113 {
114 return Err(format!(
115 "interchange_decode: bias has length {}, expected D={d}",
116 b.len()
117 ));
118 }
119 if z_rows == 0 || z_cols == 0 {
120 return Err("interchange_decode: latent must be non-empty".to_string());
121 }
122 if !weights.iter().all(|v| v.is_finite()) {
123 return Err("interchange_decode: weights must be finite".to_string());
124 }
125 if !gate.iter().all(|v| v.is_finite()) {
126 return Err("interchange_decode: gate must be finite".to_string());
127 }
128 if let Some(b) = bias
129 && !b.iter().all(|v| v.is_finite())
130 {
131 return Err("interchange_decode: bias must be finite".to_string());
132 }
133 Ok(())
134}
135
136pub fn interchange_decode_forward(
138 inputs: InterchangeDecodeForward<'_>,
139) -> Result<Array2<f64>, String> {
140 let (b_rows, f) = inputs.z.dim();
141 check_shapes_forward(b_rows, f, inputs.weights, inputs.gate, inputs.bias)?;
142 if !inputs.z.iter().all(|v| v.is_finite()) {
143 return Err("interchange_decode: latent must be finite".to_string());
144 }
145
146 let d = inputs.weights.nrows();
147 let mut z_gated = Array2::<f64>::zeros((b_rows, f));
148 for i in 0..b_rows {
149 for j in 0..f {
150 z_gated[[i, j]] = inputs.z[[i, j]] * inputs.gate[j];
151 }
152 }
153 let mut out = z_gated.dot(&inputs.weights.t());
155 if let Some(bias) = inputs.bias {
156 for i in 0..b_rows {
157 for k in 0..d {
158 out[[i, k]] += bias[k];
159 }
160 }
161 }
162 Ok(out)
163}
164
165pub fn interchange_swap_forward(inputs: InterchangeSwapForward<'_>) -> Result<Array2<f64>, String> {
167 if inputs.z_a.dim() != inputs.z_b.dim() {
168 return Err(format!(
169 "interchange_swap: z_a {:?} and z_b {:?} must have the same shape",
170 inputs.z_a.dim(),
171 inputs.z_b.dim()
172 ));
173 }
174 let (b_rows, f) = inputs.z_a.dim();
175 if inputs.mask.len() != f {
176 return Err(format!(
177 "interchange_swap: mask length {} must equal F={f}",
178 inputs.mask.len()
179 ));
180 }
181 if !inputs.z_a.iter().all(|v| v.is_finite()) || !inputs.z_b.iter().all(|v| v.is_finite()) {
182 return Err("interchange_swap: latents must be finite".to_string());
183 }
184 let mut z_eff = Array2::<f64>::zeros((b_rows, f));
185 for j in 0..f {
186 let take_a = inputs.mask[j];
187 if take_a {
188 for i in 0..b_rows {
189 z_eff[[i, j]] = inputs.z_a[[i, j]];
190 }
191 } else {
192 for i in 0..b_rows {
193 z_eff[[i, j]] = inputs.z_b[[i, j]];
194 }
195 }
196 }
197 interchange_decode_forward(InterchangeDecodeForward {
198 z: z_eff.view(),
199 weights: inputs.weights,
200 gate: inputs.gate,
201 bias: inputs.bias,
202 })
203}
204
205pub fn interchange_decode_backward(
207 z: ArrayView2<'_, f64>,
208 weights: ArrayView2<'_, f64>,
209 gate: ArrayView1<'_, f64>,
210 grad_out: ArrayView2<'_, f64>,
211 with_bias: bool,
212) -> Result<InterchangeDecodeBackward, String> {
213 let (b_rows, f) = z.dim();
214 let (d, f_w) = weights.dim();
215 if f_w != f {
216 return Err(format!(
217 "interchange_decode_backward: weights has F={f_w}, expected {f}"
218 ));
219 }
220 if gate.len() != f {
221 return Err(format!(
222 "interchange_decode_backward: gate length {} != F={f}",
223 gate.len()
224 ));
225 }
226 if grad_out.dim() != (b_rows, d) {
227 return Err(format!(
228 "interchange_decode_backward: grad_out shape {:?} != ({b_rows}, {d})",
229 grad_out.dim()
230 ));
231 }
232
233 let g_mat = grad_out.dot(&weights); let mut grad_z = Array2::<f64>::zeros((b_rows, f));
238 for i in 0..b_rows {
239 for j in 0..f {
240 grad_z[[i, j]] = gate[j] * g_mat[[i, j]];
241 }
242 }
243
244 let mut grad_gate = Array1::<f64>::zeros(f);
246 for j in 0..f {
247 let mut acc = 0.0;
248 for i in 0..b_rows {
249 acc += z[[i, j]] * g_mat[[i, j]];
250 }
251 grad_gate[j] = acc;
252 }
253
254 let mut grad_weights = grad_out.t().dot(&z); for j in 0..f {
258 let scale = gate[j];
259 for k in 0..d {
260 grad_weights[[k, j]] *= scale;
261 }
262 }
263
264 let grad_bias = if with_bias {
265 let mut gb = Array1::<f64>::zeros(d);
266 for i in 0..b_rows {
267 for k in 0..d {
268 gb[k] += grad_out[[i, k]];
269 }
270 }
271 Some(gb)
272 } else {
273 None
274 };
275
276 Ok(InterchangeDecodeBackward {
277 grad_z,
278 grad_weights,
279 grad_gate,
280 grad_bias,
281 })
282}
283
284pub fn interchange_swap_backward(
286 z_a: ArrayView2<'_, f64>,
287 z_b: ArrayView2<'_, f64>,
288 mask: ArrayView1<'_, bool>,
289 weights: ArrayView2<'_, f64>,
290 gate: ArrayView1<'_, f64>,
291 grad_out: ArrayView2<'_, f64>,
292 with_bias: bool,
293) -> Result<InterchangeSwapBackward, String> {
294 if z_a.dim() != z_b.dim() {
295 return Err(format!(
296 "interchange_swap_backward: z_a {:?} and z_b {:?} must have the same shape",
297 z_a.dim(),
298 z_b.dim()
299 ));
300 }
301 let (b_rows, f) = z_a.dim();
302 if mask.len() != f {
303 return Err(format!(
304 "interchange_swap_backward: mask length {} != F={f}",
305 mask.len()
306 ));
307 }
308
309 let mut z_eff = Array2::<f64>::zeros((b_rows, f));
311 for j in 0..f {
312 let take_a = mask[j];
313 if take_a {
314 for i in 0..b_rows {
315 z_eff[[i, j]] = z_a[[i, j]];
316 }
317 } else {
318 for i in 0..b_rows {
319 z_eff[[i, j]] = z_b[[i, j]];
320 }
321 }
322 }
323 let inner = interchange_decode_backward(z_eff.view(), weights, gate, grad_out, with_bias)?;
324
325 let mut grad_z_a = Array2::<f64>::zeros((b_rows, f));
327 let mut grad_z_b = Array2::<f64>::zeros((b_rows, f));
328 for j in 0..f {
329 let take_a = mask[j];
330 if take_a {
331 for i in 0..b_rows {
332 grad_z_a[[i, j]] = inner.grad_z[[i, j]];
333 }
334 } else {
335 for i in 0..b_rows {
336 grad_z_b[[i, j]] = inner.grad_z[[i, j]];
337 }
338 }
339 }
340
341 Ok(InterchangeSwapBackward {
342 grad_z_a,
343 grad_z_b,
344 grad_weights: inner.grad_weights,
345 grad_gate: inner.grad_gate,
346 grad_bias: inner.grad_bias,
347 })
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use ndarray::{Array1, Array2, array};
354
355 fn approx_eq(a: &Array2<f64>, b: &Array2<f64>, tol: f64) -> bool {
356 if a.dim() != b.dim() {
357 return false;
358 }
359 a.iter().zip(b.iter()).all(|(x, y)| (x - y).abs() < tol)
360 }
361
362 #[test]
363 fn forward_matches_hand_recomputation() {
364 let z = array![[1.0, -2.0, 0.5], [0.0, 3.0, -1.0]];
365 let w = array![[0.1, 0.2, 0.3], [-0.4, 0.5, 0.6]];
366 let g = array![1.0, 0.5, -1.0];
367 let bias = array![0.01, -0.02];
368 let out = interchange_decode_forward(InterchangeDecodeForward {
369 z: z.view(),
370 weights: w.view(),
371 gate: g.view(),
372 bias: Some(bias.view()),
373 })
374 .unwrap();
375 let mut expected = Array2::<f64>::zeros((2, 2));
377 for i in 0..2 {
378 for k in 0..2 {
379 let mut acc = bias[k];
380 for j in 0..3 {
381 acc += g[j] * z[[i, j]] * w[[k, j]];
382 }
383 expected[[i, k]] = acc;
384 }
385 }
386 assert!(approx_eq(&out, &expected, 1e-12));
387 }
388
389 #[test]
390 fn swap_all_true_matches_z_a_forward() {
391 let z_a = array![[1.0, -2.0], [3.0, 0.5]];
392 let z_b = array![[10.0, 20.0], [-30.0, 40.0]];
393 let w = array![[0.1, 0.2], [0.3, -0.4], [0.5, 0.6]];
394 let g = array![0.7, -0.3];
395 let mask = Array1::from(vec![true, true]);
396 let swapped = interchange_swap_forward(InterchangeSwapForward {
397 z_a: z_a.view(),
398 z_b: z_b.view(),
399 mask: mask.view(),
400 weights: w.view(),
401 gate: g.view(),
402 bias: None,
403 })
404 .unwrap();
405 let plain = interchange_decode_forward(InterchangeDecodeForward {
406 z: z_a.view(),
407 weights: w.view(),
408 gate: g.view(),
409 bias: None,
410 })
411 .unwrap();
412 assert!(approx_eq(&swapped, &plain, 1e-12));
413 }
414
415 #[test]
416 fn swap_all_false_matches_z_b_forward() {
417 let z_a = array![[1.0, -2.0], [3.0, 0.5]];
418 let z_b = array![[10.0, 20.0], [-30.0, 40.0]];
419 let w = array![[0.1, 0.2], [0.3, -0.4]];
420 let g = array![0.7, -0.3];
421 let mask = Array1::from(vec![false, false]);
422 let swapped = interchange_swap_forward(InterchangeSwapForward {
423 z_a: z_a.view(),
424 z_b: z_b.view(),
425 mask: mask.view(),
426 weights: w.view(),
427 gate: g.view(),
428 bias: None,
429 })
430 .unwrap();
431 let plain = interchange_decode_forward(InterchangeDecodeForward {
432 z: z_b.view(),
433 weights: w.view(),
434 gate: g.view(),
435 bias: None,
436 })
437 .unwrap();
438 assert!(approx_eq(&swapped, &plain, 1e-12));
439 }
440
441 #[test]
442 fn backward_matches_finite_differences() {
443 let z = array![[0.4, -0.7, 1.1], [0.2, 0.8, -0.3]];
444 let w = array![[0.1, 0.2, 0.3], [-0.4, 0.5, 0.6]];
445 let g = array![0.6, -0.2, 1.3];
446 let bias = array![0.05, -0.01];
447 let grad_out = array![[1.0, -0.5], [0.3, 0.8]];
448
449 let an = interchange_decode_backward(z.view(), w.view(), g.view(), grad_out.view(), true)
450 .unwrap();
451
452 let eps = 1e-6;
455 for i in 0..z.nrows() {
456 for j in 0..z.ncols() {
457 let mut zp = z.clone();
458 let mut zm = z.clone();
459 zp[[i, j]] += eps;
460 zm[[i, j]] -= eps;
461 let fp = interchange_decode_forward(InterchangeDecodeForward {
462 z: zp.view(),
463 weights: w.view(),
464 gate: g.view(),
465 bias: Some(bias.view()),
466 })
467 .unwrap();
468 let fm = interchange_decode_forward(InterchangeDecodeForward {
469 z: zm.view(),
470 weights: w.view(),
471 gate: g.view(),
472 bias: Some(bias.view()),
473 })
474 .unwrap();
475 let lp: f64 = fp.iter().zip(grad_out.iter()).map(|(a, b)| a * b).sum();
476 let lm: f64 = fm.iter().zip(grad_out.iter()).map(|(a, b)| a * b).sum();
477 let fd = (lp - lm) / (2.0 * eps);
478 assert!(
479 (an.grad_z[[i, j]] - fd).abs() < 1e-7,
480 "grad_z mismatch at ({i},{j}): analytic {} vs fd {}",
481 an.grad_z[[i, j]],
482 fd
483 );
484 }
485 }
486 for j in 0..g.len() {
488 let mut gp = g.clone();
489 let mut gm = g.clone();
490 gp[j] += eps;
491 gm[j] -= eps;
492 let fp = interchange_decode_forward(InterchangeDecodeForward {
493 z: z.view(),
494 weights: w.view(),
495 gate: gp.view(),
496 bias: Some(bias.view()),
497 })
498 .unwrap();
499 let fm = interchange_decode_forward(InterchangeDecodeForward {
500 z: z.view(),
501 weights: w.view(),
502 gate: gm.view(),
503 bias: Some(bias.view()),
504 })
505 .unwrap();
506 let lp: f64 = fp.iter().zip(grad_out.iter()).map(|(a, b)| a * b).sum();
507 let lm: f64 = fm.iter().zip(grad_out.iter()).map(|(a, b)| a * b).sum();
508 let fd = (lp - lm) / (2.0 * eps);
509 assert!(
510 (an.grad_gate[j] - fd).abs() < 1e-7,
511 "grad_gate mismatch at {j}: analytic {} vs fd {}",
512 an.grad_gate[j],
513 fd
514 );
515 }
516 for d in 0..w.nrows() {
518 for j in 0..w.ncols() {
519 let mut wp = w.clone();
520 let mut wm = w.clone();
521 wp[[d, j]] += eps;
522 wm[[d, j]] -= eps;
523 let fp = interchange_decode_forward(InterchangeDecodeForward {
524 z: z.view(),
525 weights: wp.view(),
526 gate: g.view(),
527 bias: Some(bias.view()),
528 })
529 .unwrap();
530 let fm = interchange_decode_forward(InterchangeDecodeForward {
531 z: z.view(),
532 weights: wm.view(),
533 gate: g.view(),
534 bias: Some(bias.view()),
535 })
536 .unwrap();
537 let lp: f64 = fp.iter().zip(grad_out.iter()).map(|(a, b)| a * b).sum();
538 let lm: f64 = fm.iter().zip(grad_out.iter()).map(|(a, b)| a * b).sum();
539 let fd = (lp - lm) / (2.0 * eps);
540 assert!(
541 (an.grad_weights[[d, j]] - fd).abs() < 1e-7,
542 "grad_W mismatch at ({d},{j}): analytic {} vs fd {}",
543 an.grad_weights[[d, j]],
544 fd
545 );
546 }
547 }
548 let bias_grad = an.grad_bias.as_ref().unwrap();
550 for d in 0..bias.len() {
551 let mut bp = bias.clone();
552 let mut bm = bias.clone();
553 bp[d] += eps;
554 bm[d] -= eps;
555 let fp = interchange_decode_forward(InterchangeDecodeForward {
556 z: z.view(),
557 weights: w.view(),
558 gate: g.view(),
559 bias: Some(bp.view()),
560 })
561 .unwrap();
562 let fm = interchange_decode_forward(InterchangeDecodeForward {
563 z: z.view(),
564 weights: w.view(),
565 gate: g.view(),
566 bias: Some(bm.view()),
567 })
568 .unwrap();
569 let lp: f64 = fp.iter().zip(grad_out.iter()).map(|(a, b)| a * b).sum();
570 let lm: f64 = fm.iter().zip(grad_out.iter()).map(|(a, b)| a * b).sum();
571 let fd = (lp - lm) / (2.0 * eps);
572 assert!(
573 (bias_grad[d] - fd).abs() < 1e-7,
574 "grad_bias mismatch at {d}: analytic {} vs fd {}",
575 bias_grad[d],
576 fd
577 );
578 }
579 }
580
581 #[test]
582 fn swap_backward_routes_grad_through_mask() {
583 let z_a = array![[1.0, 2.0, 3.0]];
584 let z_b = array![[-1.0, -2.0, -3.0]];
585 let w = array![[0.5, 0.25, -0.1]];
586 let g = array![1.0, 0.5, -1.0];
587 let mask = Array1::from(vec![true, false, true]);
588 let grad_out = array![[1.0]];
589 let bk = interchange_swap_backward(
590 z_a.view(),
591 z_b.view(),
592 mask.view(),
593 w.view(),
594 g.view(),
595 grad_out.view(),
596 false,
597 )
598 .unwrap();
599 assert!((bk.grad_z_a[[0, 0]] - 1.0 * 0.5).abs() < 1e-12);
602 assert!((bk.grad_z_a[[0, 1]] - 0.0).abs() < 1e-12);
603 assert!((bk.grad_z_a[[0, 2]] - (-1.0) * (-0.1)).abs() < 1e-12);
604 assert!((bk.grad_z_b[[0, 0]] - 0.0).abs() < 1e-12);
605 assert!((bk.grad_z_b[[0, 1]] - 0.5 * 0.25).abs() < 1e-12);
606 assert!((bk.grad_z_b[[0, 2]] - 0.0).abs() < 1e-12);
607 }
608}