File size: 7,625 Bytes
97df2ae
e26f4cc
97df2ae
 
 
e26f4cc
 
97df2ae
e26f4cc
 
97df2ae
e26f4cc
97df2ae
 
e26f4cc
e4ee5c4
e26f4cc
 
e4ee5c4
e26f4cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97df2ae
e26f4cc
97df2ae
 
e26f4cc
 
 
 
 
 
97df2ae
 
e26f4cc
 
 
 
 
 
 
 
 
 
 
 
97df2ae
 
e26f4cc
 
 
 
97df2ae
 
 
 
 
 
 
 
 
 
 
 
 
 
e26f4cc
97df2ae
 
 
 
 
 
 
 
 
 
 
e26f4cc
 
97df2ae
 
 
 
 
 
 
e26f4cc
e4ee5c4
e26f4cc
 
 
 
 
 
 
 
 
 
 
97df2ae
e26f4cc
97df2ae
 
 
 
 
 
 
e26f4cc
97df2ae
 
e26f4cc
97df2ae
 
 
 
 
 
 
 
 
 
e26f4cc
 
 
 
 
 
97df2ae
 
e26f4cc
 
 
 
 
97df2ae
e26f4cc
 
97df2ae
 
e26f4cc
 
 
 
97df2ae
e26f4cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97df2ae
e26f4cc
97df2ae
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
//! Slot Mamba-3 (1-de-6 do tronco 5:1) — scan recorrente diagonal seletivo
//! (SSD) com adjunto EXATO via CustomOp2. Compatível com o Teo D.1(i):
//! fluxo estritamente esquerda→direita (a camada é causal por construção);
//! a bidirecionalidade intra-nível vem das camadas ADBA (H_D1 — r-04 §3).
//!
//! Recorrência: h_t = a_t ⊙ h_{t−1} + (1 − a_t) ⊙ x_t;  y_t = h_t
//! com a_t = σ(W_a·x_t + b_a) ∈ (0,1)^d (portas por canal).
//!
//! Adjunto (exato): dh_t = dy_t + a_{t+1} ⊙ dh_{t+1};
//! dx_t = (1 − a_t) ⊙ dh_t;  da_t = ⟨dh_t, h_{t−1} − x_t⟩ (escalar por canal).

use candle_core::{CpuStorage, CustomOp2, Device, Layout, Result, Shape, Tensor, Var};
use crate::camadas::{Linear, RmsNorm, Swiglu};

pub struct ScanDiagonal;

impl CustomOp2 for ScanDiagonal {
    fn name(&self) -> &'static str { "ssd_scan_diagonal" }

    fn cpu_fwd(&self, s1: &CpuStorage, l1: &Layout, s2: &CpuStorage, l2: &Layout) -> Result<(CpuStorage, Shape)> {
        if !(l1.is_contiguous() && l2.is_contiguous()) {
            return Err(candle_core::Error::Msg(
                "ssd_scan: entradas devem ser contíguas (chame .contiguous())".into(),
            ));
        }
        let x = s1.as_slice::<f32>()?; // (n, d)
        let a = s2.as_slice::<f32>()?; // (n, d) em (0,1)
        let (n, d) = (l1.shape().dims()[0], l1.shape().dims()[1]);
        let mut y = vec![0f32; n * d];
        for j in 0..d {
            let mut h = 0f32;
            for i in 0..n {
                let ai = a[i * d + j];
                h = ai * h + (1.0 - ai) * x[i * d + j];
                y[i * d + j] = h;
            }
        }
        Ok((CpuStorage::F32(y), (n, d).into()))
    }

    fn bwd(&self, arg1: &Tensor, arg2: &Tensor, _res: &Tensor, grad_res: &Tensor) -> Result<(Option<Tensor>, Option<Tensor>)> {
        // recomputa h (forward sem gradiente) e percorre reverso
        let (x, a) = (arg1, arg2);
        let h = Tensor::apply_op2(x, a, ScanDiagonal)?.detach();
        let x_v = x.flatten_all()?.to_vec1::<f32>()?;
        let a_v = a.flatten_all()?.to_vec1::<f32>()?;
        let h_v = h.flatten_all()?.to_vec1::<f32>()?;
        let dy_v = grad_res.flatten_all()?.to_vec1::<f32>()?;
        let (n, d) = (x.dim(0)?, x.dim(1)?);
        let mut dx = vec![0f32; n * d];
        let mut da = vec![0f32; n * d];
        let mut dh = vec![0f32; d];
        for i in (0..n).rev() {
            for j in 0..d {
                let idx = i * d + j;
                let g = dy_v[idx] + dh[j];
                dx[idx] = (1.0 - a_v[idx]) * g;
                let h_ant = if i > 0 { h_v[(i - 1) * d + j] } else { 0.0 };
                da[idx] = g * (h_ant - x_v[idx]);
                dh[j] = a_v[idx] * g;
            }
        }
        let dev = x.device();
        let gx = Tensor::from_vec(dx, (n, d), dev)?;
        let ga = Tensor::from_vec(da, (n, d), dev)?;
        Ok((Some(gx), Some(ga)))
    }
}

/// Bloco SSD (a camada recorrente do tronco): portas por canal + scan + projeção.
pub struct BlocoSsd {
    pub norm1: RmsNorm,
    pub porta_a: Linear, // d → d (logitas do decaimento)
    pub proj_x: Linear,  // d → d
    pub proj_o: Linear,  // d → d
    pub norm2: RmsNorm,
    pub ffn: Swiglu,
}

impl BlocoSsd {
    pub fn novo(d: usize, d_ff: usize, dev: &Device) -> Result<Self> {
        let desvio = 1.0 / ((d * 2) as f64).sqrt();
        Ok(BlocoSsd {
            norm1: RmsNorm::novo(d, 1e-5, dev)?,
            porta_a: Linear::novo(d, d, desvio, dev)?,
            proj_x: Linear::sem_vies(d, d, desvio, dev)?,
            proj_o: Linear::sem_vies(d, d, desvio, dev)?,
            norm2: RmsNorm::novo(d, 1e-5, dev)?,
            ffn: Swiglu::novo(d, d_ff, dev)?,
        })
    }

    /// x (b, n, d) → (b, n, d). O scan é aplicado por elemento do lote.
    /// `cache_estado`: estado h anterior (b, d) para streaming incremental.
    pub fn fwd(&self, x: &Tensor, cache_estado: &mut Option<Tensor>) -> Result<Tensor> {
        let b = x.dim(0)?;
        let n = x.dim(1)?;
        let d = x.dim(2)?;
        let xb = self.norm1.fwd(x)?;
        let a = candle_nn::ops::sigmoid(&self.porta_a.fwd(&xb)?)?;
        let xp = self.proj_x.fwd(&xb)?;
        // achata (b·n, d), aplica o scan por lote e recompõe
        let xf = xp.reshape((b * n, d))?.contiguous()?;
        let af = a.reshape((b * n, d))?.contiguous()?;
        // pré-estado: se houver cache, subtrai efeito — simplificação: scan
        // independente por segmento com estado inicial zero (documentado; o
        // estado cruzado entre segmentos é ponto de extensão do streaming).
        let _ = cache_estado;
        let mut saidas = Vec::with_capacity(b);
        for i in 0..b {
            let xi = xf.narrow(0, i * n, n)?;
            let ai = af.narrow(0, i * n, n)?;
            let yi = Tensor::apply_op2(&xi, &ai, ScanDiagonal)?;
            saidas.push(yi);
        }
        let y = Tensor::cat(&saidas, 0)?.reshape((b, n, d))?;
        let y = self.proj_o.fwd(&y)?;
        let x2 = (x + &y)?;
        let f = self.ffn.fwd(&self.norm2.fwd(&x2)?)?;
        (&x2 + &f)
    }

    pub fn vars(&self) -> Vec<Var> {
        [
            self.norm1.vars(), self.porta_a.vars(), self.proj_x.vars(),
            self.proj_o.vars(), self.norm2.vars(), self.ffn.vars(),
        ].concat()
    }
}

#[cfg(test)]
mod testes {
    use super::*;

    #[test]
    fn adjunto_do_scan_vs_finitas_diferencas() -> Result<()> {
        let dev = Device::Cpu;
        let n = 9;
        let d = 5;
        let xv = Var::from_tensor(&Tensor::randn(0f32, 1f32, (n, d), &dev)?)?;
        let av = Var::from_tensor(&candle_nn::ops::sigmoid(&Tensor::randn(0f32, 1f32, (n, d), &dev)?)?)?;
        let pesos = Tensor::randn(0f32, 1f32, (n, d), &dev)?;
        let y = Tensor::apply_op2(xv.as_tensor(), &av.as_tensor(), ScanDiagonal)?;
        let perda = (&y * &pesos)?.sum_all()?;
        let gs = perda.backward()?;
        let gx = gs.get(&xv).ok_or_else(|| candle_core::Error::Msg("sem grad x".into()))?;
        let ga = gs.get(&av).ok_or_else(|| candle_core::Error::Msg("sem grad a".into()))?;
        let gxv = gx.flatten_all()?.to_vec1::<f32>()?;
        let gav = ga.flatten_all()?.to_vec1::<f32>()?;
        // FD
        let eps = 1e-3f32;
        let f = |x: &Tensor, a: &Tensor| -> Result<f64> {
            let y = Tensor::apply_op2(x, a, ScanDiagonal)?;
            Ok((&y * &pesos)?.sum_all()?.to_scalar::<f32>()? as f64)
        };
        let x0 = xv.as_tensor();
        let a0 = av.as_tensor();
        let x_plano = x0.flatten_all()?.to_vec1::<f32>()?;
        let a_plano = a0.flatten_all()?.to_vec1::<f32>()?;
        let mut erro = 0f64;
        for idx in [3usize, 11, 17, 24, 30] {
            let mut xp = x_plano.clone();
            let mut xm = x_plano.clone();
            xp[idx] += eps;
            xm[idx] -= eps;
            let tp = Tensor::from_vec(xp, (n, d), &dev)?;
            let tm = Tensor::from_vec(xm, (n, d), &dev)?;
            let fd = (f(&tp, a0)? - f(&tm, a0)?) / (2.0 * eps as f64);
            erro = erro.max((fd - gxv[idx] as f64).abs());
            // porta a
            let mut ap = a_plano.clone();
            let mut am = a_plano.clone();
            ap[idx] += eps;
            am[idx] -= eps;
            let tap = Tensor::from_vec(ap, (n, d), &dev)?;
            let tam = Tensor::from_vec(am, (n, d), &dev)?;
            let fda = (f(x0, &tap)? - f(x0, &tam)?) / (2.0 * eps as f64);
            erro = erro.max((fda - gav[idx] as f64).abs());
        }
        assert!(erro < 5e-3, "erro adjunto SSD = {erro}");
        Ok(())
    }
}