//! v0.9.0 — Núcleos SIMD com despacho em RUNTIME (crítica Qwen3.8 nº 4 + //! plano de ação itens 1/3; erratas e provas em r-34 §4/§9). //! //! **ERRATA-1 (da proposta original)**: `RUSTFLAGS="-C target-feature= //! +avx512f,..."` NÃO cabe no `[profile.release]` do Cargo.toml (só em //! `.cargo/config.toml` ou variável de ambiente) e, num repo PUBLICADO, //! produz binários que travam com SIGILL em toda CPU sem AVX-512. O padrão //! SOTA — o mesmo usado pelo crate `gemm` que o candle usa para matmul — é: //! detecção de feature EM RUNTIME (`is_x86_feature_detected!`) + kernel //! especializado `#[target_feature]` + fallback escalar. Portabilidade e //! velocidade simultâneas; a velocidade deixa de ser um compromisso com a //! corretude em outras máquinas. //! //! **ERRATA-2**: `#[repr(C, align(64))]` numa struct que CONTÉM `Vec` alinha //! a STRUCT (na pilha), não o buffer de HEAP do `Vec` — o buffer continua com //! o alinhamento do alocador global (glibc: 16 bytes). Para alinhar o heap é //! preciso alocação explícita com `Layout::from_size_align(.., 64)` — //! implementado em [`BufAlinhado64`] e verificado por endereço no teste //! `simd_enderecos`. //! //! **EXATIDÃO (Teo X-1, r-34 §4.3)**: os kernels "exatos" NÃO usam FMA nem //! aproximações (`rsqrt`/`rcp`) e preservam a ORDEM das operações do código //! escalar que substituem ⇒ saída BIT-A-BIT idêntica (IEEE-754: mesma //! sequência de operações ⇒ mesmo resultado; verificado por `to_bits()` nos //! testes `adamw_bit_a_bit`, `escalar_bit_a_bit`, `distancias_bit_a_bit`). //! O kernel `distancias4_rapidas` usa redução horizontal vetorizada //! (ordem de soma diferente) — documentado como ε-diferente, nunca usado //! onde a igualdade bit-a-bit é garantia. //! //! **Onde NÃO vetorizamos (Amdahl honesto)**: `norma_global` é uma REDUÇÃO — //! vetorizar muda a ordem de soma e quebraria a bit-identidade; fica escalar //! (custo marginal no passo). Reduções em f64 com associação diferente são a //! linha exata entre "otimização segura" e "mudança silenciosa de resultados". use std::alloc::{alloc, dealloc, Layout}; use std::sync::atomic::{AtomicUsize, Ordering}; // --------------------------------------------------------------------------- // Contadores de telemetria (painel_v090) // --------------------------------------------------------------------------- static CHAMADAS_AVX: AtomicUsize = AtomicUsize::new(0); static CHAMADAS_ESCALAR: AtomicUsize = AtomicUsize::new(0); /// Relatório do despacho SIMD (para telemetria/painel_v090). pub fn relatorio() -> serde_json::Value { serde_json::json!({ "avx512_disponivel": tem_avx512(), "chamadas_avx": CHAMADAS_AVX.load(Ordering::Relaxed), "chamadas_escalar": CHAMADAS_ESCALAR.load(Ordering::Relaxed), }) } /// AVX-512 (F+DQ+BW+VL) disponível nesta CPU? pub fn tem_avx512() -> bool { #[cfg(target_arch = "x86_64")] { std::arch::is_x86_feature_detected!("avx512f") && std::arch::is_x86_feature_detected!("avx512dq") && std::arch::is_x86_feature_detected!("avx512bw") && std::arch::is_x86_feature_detected!("avx512vl") } #[cfg(not(target_arch = "x86_64"))] { false } } // --------------------------------------------------------------------------- // Kernel 1: passo AdamW FUNDIDO (elemento-a-elemento, sem reduções) // --------------------------------------------------------------------------- /// Um passo AdamW sobre UM tensor achatado — réplica exata (ordem de ops) do /// loop escalar do `AdamWManual::passo` da v0.8.0: /// /// ```text /// m[j] = β₁·m[j] + (1−β₁)·g[j] /// v[j] = β₂·v[j] + ((1−β₂)·g[j])·g[j] // assoc. esquerda, como o escalar /// mh = m[j]/bc₁ ; vh = v[j]/bc₂ /// out[j] = w[j] − lr·(mh/(√vh+ε) + wd·w[j]) /// ``` /// /// Sem FMA, sem rcp/rsqrt — `add/mul/div/sqrt` apenas, todas exatas por /// IEEE-754 ⇒ **bit-a-bit idêntico** ao escalar (Teo X-1). pub fn passo_adamw( w: &[f32], g: &[f32], m: &mut [f32], v: &mut [f32], out: &mut [f32], lr: f64, beta1: f64, beta2: f64, eps: f64, wd: f64, bc1: f64, bc2: f64, ) { let n = w.len().min(g.len()).min(m.len()).min(v.len()).min(out.len()); let (lr, b1, b2, eps, wd, bc1, bc2) = ( lr as f32, beta1 as f32, beta2 as f32, eps as f32, wd as f32, bc1 as f32, bc2 as f32, ); // réplica EXATA do escalar v0.8.0: `(1.0 - self.beta1 as f32)` — a // subtração é em f32 (depois do cast), não em f64 — trocar isso quebra // a bit-identidade no último ulp. let omb1 = 1.0f32 - b1; let omb2 = 1.0f32 - b2; let usar_avx = tem_avx512() && n >= 64; if usar_avx { CHAMADAS_AVX.fetch_add(1, Ordering::Relaxed); unsafe { passo_adamw_avx(w, g, m, v, out, lr, b1, b2, eps, wd, bc1, bc2, omb1, omb2) }; } else { CHAMADAS_ESCALAR.fetch_add(1, Ordering::Relaxed); } // cauda + caminho escalar: MESMO código nas duas situações ⇒ bits idênticos let ini = if usar_avx { n & !15 } else { 0 }; for j in ini..n { m[j] = b1 * m[j] + omb1 * g[j]; v[j] = b2 * v[j] + omb2 * g[j] * g[j]; let mh = m[j] / bc1; let vh = v[j] / bc2; out[j] = w[j] - lr * (mh / (vh.sqrt() + eps) + wd * w[j]); } } /// Caminho 100% escalar (referência de bench A/B e de leitura) — mesmo corpo /// da cauda; SEM despacho. Exposto para medição honesta do ganho AVX-512. pub fn passo_adamw_escalar( w: &[f32], g: &[f32], m: &mut [f32], v: &mut [f32], out: &mut [f32], lr: f64, beta1: f64, beta2: f64, eps: f64, wd: f64, bc1: f64, bc2: f64, ) { let n = w.len().min(g.len()).min(m.len()).min(v.len()).min(out.len()); let (lrf, b1, b2, epsf, wdf, bc1f, bc2f) = ( lr as f32, beta1 as f32, beta2 as f32, eps as f32, wd as f32, bc1 as f32, bc2 as f32, ); let omb1 = 1.0f32 - b1; let omb2 = 1.0f32 - b2; for j in 0..n { m[j] = b1 * m[j] + omb1 * g[j]; v[j] = b2 * v[j] + omb2 * g[j] * g[j]; let mh = m[j] / bc1f; let vh = v[j] / bc2f; out[j] = w[j] - lrf * (mh / (vh.sqrt() + epsf) + wdf * w[j]); } } /// Kernel AVX-512 f32 (16 lanes). Processa `n & !15` elementos; a cauda fica /// para o loop escalar do chamador (mesma matemática ⇒ bits idênticos). /// /// # Segurança /// Requer AVX-512 (F) — checado por [`tem_avx512`] no chamador. Fatias de 16. #[target_feature(enable = "avx512f,avx512dq")] unsafe fn passo_adamw_avx( w: &[f32], g: &[f32], m: &mut [f32], v: &mut [f32], out: &mut [f32], lr: f32, b1: f32, b2: f32, eps: f32, wd: f32, bc1: f32, bc2: f32, omb1: f32, omb2: f32, ) { use std::arch::x86_64::*; let n = w.len().min(g.len()).min(m.len()).min(v.len()).min(out.len()); let v_lr = _mm512_set1_ps(lr); let v_b1 = _mm512_set1_ps(b1); let v_b2 = _mm512_set1_ps(b2); let v_eps = _mm512_set1_ps(eps); let v_wd = _mm512_set1_ps(wd); let v_bc1 = _mm512_set1_ps(bc1); let v_bc2 = _mm512_set1_ps(bc2); let v_omb1 = _mm512_set1_ps(omb1); let v_omb2 = _mm512_set1_ps(omb2); let mut j = 0usize; while j + 16 <= n { let vw = _mm512_loadu_ps(w.as_ptr().add(j)); let vg = _mm512_loadu_ps(g.as_ptr().add(j)); let vm = _mm512_loadu_ps(m.as_ptr().add(j)); let vv = _mm512_loadu_ps(v.as_ptr().add(j)); // m = b1*m + omb1*g ; v = b2*v + (omb2*g)*g [sem FMA — ordem exata] let t1 = _mm512_mul_ps(v_omb1, vg); let nm = _mm512_add_ps(_mm512_mul_ps(v_b1, vm), t1); let t2 = _mm512_mul_ps(v_omb2, vg); let nv = _mm512_add_ps(_mm512_mul_ps(v_b2, vv), _mm512_mul_ps(t2, vg)); // mh = m/bc1 ; vh = v/bc2 ; ajuste = mh/(sqrt(vh)+eps) + wd*w let mh = _mm512_div_ps(nm, v_bc1); let vh = _mm512_div_ps(nv, v_bc2); let raiz = _mm512_sqrt_ps(vh); // IEEE-exata (não é rsqrt aproximado) let denom = _mm512_add_ps(raiz, v_eps); let quoc = _mm512_div_ps(mh, denom); let decaimento = _mm512_mul_ps(v_wd, vw); let ajuste = _mm512_add_ps(quoc, decaimento); let passo = _mm512_mul_ps(v_lr, ajuste); let novo = _mm512_sub_ps(vw, passo); _mm512_storeu_ps(m.as_mut_ptr().add(j), nm); _mm512_storeu_ps(v.as_mut_ptr().add(j), nv); _mm512_storeu_ps(out.as_mut_ptr().add(j), novo); j += 16; } } // --------------------------------------------------------------------------- // Kernel 2: escala elemento-a-elemento (clip de gradiente) // --------------------------------------------------------------------------- /// `x[i] *= f` para todo i — elemento-a-elemento ⇒ bit-a-bit idêntico ao /// loop escalar do `AdamWManual::clipar` (uma única multiplicação por elemento). pub fn escalar_slice(xs: &mut [f32], f: f32) { let n = xs.len(); if tem_avx512() && n >= 64 { CHAMADAS_AVX.fetch_add(1, Ordering::Relaxed); unsafe { escalar_slice_avx(xs, f) }; let ini = n & !15; for x in xs[ini..].iter_mut() { *x *= f; } } else { CHAMADAS_ESCALAR.fetch_add(1, Ordering::Relaxed); for x in xs.iter_mut() { *x *= f; } } } #[target_feature(enable = "avx512f,avx512dq")] unsafe fn escalar_slice_avx(xs: &mut [f32], f: f32) { use std::arch::x86_64::*; let vf = _mm512_set1_ps(f); let n = xs.len(); let mut j = 0usize; while j + 16 <= n { let x = _mm512_loadu_ps(xs.as_ptr().add(j)); _mm512_storeu_ps(xs.as_mut_ptr().add(j), _mm512_mul_ps(x, vf)); j += 16; } } // --------------------------------------------------------------------------- // Kernel 3: distâncias² 4D em layout SoA (crítica Qwen3.8 B.3) // --------------------------------------------------------------------------- /// Layout SoA column-major: `soa[j*c + i]` = coordenada j do protótipo i /// (d = 4). Construído por [`soa_de_prototipos`]. /// /// **Teo X-2**: o termo (x_j − w_ij)² é computado por lane com sub/mul (sem /// FMA) e a soma por protótipo percorre j = 0,1,2,3 na MESMA ordem do /// `distancia2` escalar (fold esquerdo) ⇒ **bit-a-bit idêntico** ao caminho /// `Vec>` da v0.8.0. A diferença é só a localidade: 1 buffer /// contíguo (sem perseguição de ponteiro por protótipo) + 8 protótipos por /// instrução no kernel vetorial. pub fn soa_de_prototipos(ps: &[Vec]) -> (Vec, usize, usize) { let c = ps.len(); let d = ps.first().map(|p| p.len()).unwrap_or(0); let mut soa = vec![0.0f64; d * c]; for (i, p) in ps.iter().enumerate() { for (j, x) in p.iter().enumerate() { if j < d { soa[j * c + i] = *x; } } } (soa, c, d) } /// Distâncias² de `x` a todos os `c` protótipos (modo EXATO — bit-a-bit). /// `out` é preenchido com `distancia2(x, protótipo_i)` para todo i. pub fn distancias4_exatas(soa: &[f64], c: usize, x: &[f64], out: &mut [f64]) { debug_assert_eq!(soa.len(), 4 * c); let usar_avx = tem_avx512() && c >= 64 && x.len() == 4; if usar_avx { CHAMADAS_AVX.fetch_add(1, Ordering::Relaxed); unsafe { distancias4_exatas_avx(soa, c, x, out) }; let ini = c & !7; distancias4_escalar(soa, c, x, out, ini); } else { CHAMADAS_ESCALAR.fetch_add(1, Ordering::Relaxed); distancias4_escalar(soa, c, x, out, 0); } } /// Trecho escalar — a MESMA ordem de soma do `distancia2` (fold esquerdo em j). fn distancias4_escalar(soa: &[f64], c: usize, x: &[f64], out: &mut [f64], ini: usize) { let d = if c == 0 { 0 } else { soa.len() / c }; for i in ini..c { let mut s = 0.0f64; for j in 0..d { let dx = x[j] - soa[j * c + i]; s += dx * dx; } out[i] = s; } } /// Redução horizontal exata: acumuladores por j são ARMAZENADOS e somados por /// protótipo na ordem ((q0+q1)+q2)+q3 — a ordem do fold escalar. #[target_feature(enable = "avx512f,avx512dq")] unsafe fn distancias4_exatas_avx(soa: &[f64], c: usize, x: &[f64], out: &mut [f64]) { use std::arch::x86_64::*; let mut buf = [[0f64; 8]; 4]; let mut i = 0usize; while i + 8 <= c { let mut quads = [_mm512_setzero_pd(); 4]; for (j, q) in quads.iter_mut().enumerate() { let wj = _mm512_loadu_pd(soa.as_ptr().add(j * c + i)); let xj = _mm512_set1_pd(x[j]); let dx = _mm512_sub_pd(xj, wj); *q = _mm512_mul_pd(dx, dx); // sem FMA } for j in 0..4 { _mm512_storeu_pd(buf[j].as_mut_ptr(), quads[j]); } for k in 0..8 { out[i + k] = ((buf[0][k] + buf[1][k]) + buf[2][k]) + buf[3][k]; } i += 8; } } /// Modo RÁPIDO: redução horizontal vetorizada `(q0+q1)+(q2+q3)` — ordem de /// associação DIFERENTE do escalar ⇒ ε-diferença (≈ ulp), documentada; /// usar apenas onde igualdade bit-a-bit não é garantia (ex.: sondagem). pub fn distancias4_rapidas(soa: &[f64], c: usize, x: &[f64], out: &mut [f64]) { debug_assert_eq!(soa.len(), 4 * c); if tem_avx512() && c >= 64 && x.len() == 4 { CHAMADAS_AVX.fetch_add(1, Ordering::Relaxed); unsafe { use std::arch::x86_64::*; let mut i = 0usize; while i + 8 <= c { let mut quads = [_mm512_setzero_pd(); 4]; for (j, q) in quads.iter_mut().enumerate() { let wj = _mm512_loadu_pd(soa.as_ptr().add(j * c + i)); let xj = _mm512_set1_pd(x[j]); let dx = _mm512_sub_pd(xj, wj); *q = _mm512_mul_pd(dx, dx); } let r = _mm512_add_pd( _mm512_add_pd(quads[0], quads[1]), _mm512_add_pd(quads[2], quads[3]), ); _mm512_storeu_pd(out.as_mut_ptr().add(i), r); i += 8; } }; let ini = c & !7; distancias4_escalar(soa, c, x, out, ini); } else { CHAMADAS_ESCALAR.fetch_add(1, Ordering::Relaxed); distancias4_escalar(soa, c, x, out, 0); } } /// Layout ROW-MAJOR plano: `rm[i*d + j]` (protótipos contíguos, SEM /// perseguição de ponteiros) — v0.9.0 (r-34 §6.4): a medição mostrou que o /// SoA COLUMN-major (`soa[j*c+i]`) é MAIS LENTO que `Vec` em d=4 /// (strides de c·8 B entre coordenadas quebram a localidade), enquanto o /// row-major plano processa 2 protótipos por registrador ZMM (8 f64) com /// redução horizontal EXATA por par (mesma ordem de soma do escalar). pub fn rm_de_prototipos(ps: &[Vec]) -> (Vec, usize, usize) { let c = ps.len(); let d = ps.first().map(|p| p.len()).unwrap_or(0); let mut rm = vec![0.0f64; d * c]; for (i, p) in ps.iter().enumerate() { for (j, x) in p.iter().enumerate() { if j < d { rm[i * d + j] = *x; } } } (rm, c, d) } /// Distâncias² d=4 sobre row-major plano — bit-a-bit idêntico ao escalar /// (sub/mul por lane SEM FMA + redução por protótipo na ordem j=0..3). pub fn distancias4_rm(rm: &[f64], c: usize, x: &[f64; 4], out: &mut [f64]) { if tem_avx512() && c >= 64 { CHAMADAS_AVX.fetch_add(1, Ordering::Relaxed); unsafe { distancias4_rm_avx(rm, c, x, out) }; let ini = c & !1; distancias4_rm_escalar(rm, c, x, out, ini); } else { CHAMADAS_ESCALAR.fetch_add(1, Ordering::Relaxed); distancias4_rm_escalar(rm, c, x, out, 0); } } fn distancias4_rm_escalar(rm: &[f64], c: usize, x: &[f64; 4], out: &mut [f64], ini: usize) { for i in ini..c { let b = &rm[i * 4..i * 4 + 4]; let t0 = x[0] - b[0]; let t1 = x[1] - b[1]; let t2 = x[2] - b[2]; let t3 = x[3] - b[3]; out[i] = ((t0 * t0 + t1 * t1) + t2 * t2) + t3 * t3; } } /// 2 protótipos por ZMM: carga de 8 f64, x replicado [x0..x3, x0..x3], /// sub/mul por lane; redução por PAR na ordem exata do escalar. #[target_feature(enable = "avx512f,avx512dq")] unsafe fn distancias4_rm_avx(rm: &[f64], c: usize, x: &[f64; 4], out: &mut [f64]) { use std::arch::x86_64::*; let xv = _mm512_setr_pd(x[0], x[1], x[2], x[3], x[0], x[1], x[2], x[3]); let mut i = 0usize; let mut par = [0f64; 8]; while i + 2 <= c { let w = _mm512_loadu_pd(rm.as_ptr().add(i * 4)); let dif = _mm512_sub_pd(xv, w); let q = _mm512_mul_pd(dif, dif); // lanes: [q0p, q1p, q2p, q3p, q0q, ..., q3q] _mm512_storeu_pd(par.as_mut_ptr(), q); let (p, r) = (i, i + 1); // par JÁ contém os quadrados (q = dif*dif) — só SOMA, na ordem exata out[p] = ((par[0] + par[1]) + par[2]) + par[3]; out[r] = ((par[4] + par[5]) + par[6]) + par[7]; i += 2; } } /// Distâncias² row-major (despacho: d==4 → kernel AVX-512 2-protótipos/ZMM; /// senão escalar genérico com a mesma ordem de soma) — preenche `buf`. pub fn distancias_rm(rm: &[f64], c: usize, d: usize, x: &[f64], buf: &mut Vec) { buf.clear(); buf.resize(c, 0.0); if d == 4 && x.len() == 4 { let x4 = [x[0], x[1], x[2], x[3]]; distancias4_rm(rm, c, &x4, buf); } else { CHAMADAS_ESCALAR.fetch_add(1, Ordering::Relaxed); for i in 0..c { let mut s = 0.0; for j in 0..d { let dx = x[j] - rm[i * d + j]; s += dx * dx; } buf[i] = s; } } } /// BMU sobre row-major plano — equivalente bit-a-bit a `melhor_indice(ps, x)` /// (d=4; teste `distancias_bit_a_bit_rm`). pub fn melhor_indice_rm_buf(rm: &[f64], c: usize, x: &[f64], buf: &mut Vec) -> usize { if c == 0 { return 0; } buf.clear(); buf.resize(c, 0.0); if x.len() == 4 { let x4 = [x[0], x[1], x[2], x[3]]; distancias4_rm(rm, c, &x4, buf); } else { let d = if c == 0 { 0 } else { rm.len() / c }; for i in 0..c { let mut s = 0.0; for j in 0..d { let dx = x[j] - rm[i * d + j]; s += dx * dx; } buf[i] = s; } } argmin_primeiro(buf) } /// argmin com desempate pelo MENOR índice — semântica idêntica à do /// `melhor_indice` escalar (`dd < dm` estrito mantém o primeiro mínimo). pub fn argmin_primeiro(xs: &[f64]) -> usize { let mut melhor = 0usize; let mut dm = f64::INFINITY; for (i, x) in xs.iter().enumerate() { if *x < dm { dm = *x; melhor = i; } } melhor } /// Distâncias² para TODO d (despacho: d==4 → kernel AVX-512 exato; senão /// escalar com a MESMA ordem de soma do `distancia2`) — preenche `buf` /// (reutilizado entre chamadas: zero alocação por amostra). pub fn distancias_soa(soa: &[f64], c: usize, d: usize, x: &[f64], buf: &mut Vec) { buf.clear(); buf.resize(c, 0.0); if d == 4 { distancias4_exatas(soa, c, x, buf); } else { CHAMADAS_ESCALAR.fetch_add(1, Ordering::Relaxed); distancias4_escalar(soa, c, x, buf, 0); } } /// BMU sobre layout SoA — equivalente bit-a-bit a `melhor_indice(ps, x)` /// (Teo X-2; teste `distancias_bit_a_bit`). Versão com buffer reutilizado. pub fn melhor_indice_soa_buf(soa: &[f64], c: usize, d: usize, x: &[f64], buf: &mut Vec) -> usize { if c == 0 { return 0; } distancias_soa(soa, c, d, x, buf); argmin_primeiro(buf) } /// BMU sobre layout SoA — equivalente bit-a-bit a `melhor_indice(ps, x)` /// (Teo X-2; teste `distancias_bit_a_bit`). pub fn melhor_indice_soa(soa: &[f64], c: usize, d: usize, x: &[f64]) -> usize { let mut buf = Vec::with_capacity(c.max(1)); melhor_indice_soa_buf(soa, c, d, x, &mut buf) } // --------------------------------------------------------------------------- // Buffer de heap com alinhamento GARANTIDO de 64 B (errata-2 da proposta) // --------------------------------------------------------------------------- /// Buffer f64 com alinhamento de heap GARANTIDO em 64 bytes (linha de cache / // registrador ZMM). Esta é a forma CORRETA de obter o que a proposta /// `#[repr(align(64))]` em struct-com-Vec prometia (e não entrega: isso /// alinha a struct na pilha, não o buffer — ver teste `simd_enderecos`). pub struct BufAlinhado64 { ptr: *mut f64, len: usize, layout: Layout, } impl BufAlinhado64 { /// Aloca `len` f64 zerados, alinhados a 64 B. pub fn novo_zeros(len: usize) -> Self { let len_b = (len * std::mem::size_of::()).max(1); let layout = Layout::from_size_align(len_b, 64).expect("layout 64B inválido"); let ptr = unsafe { alloc(layout) }; assert!(!ptr.is_null(), "alocação alinhada falhou"); let buf = unsafe { std::slice::from_raw_parts_mut(ptr as *mut f64, len.max(1)) }; buf.fill(0.0); Self { ptr: ptr as *mut f64, len: len.max(1), layout } } /// Cópia alinhada de uma fatia. pub fn de_fatia(xs: &[f64]) -> Self { let mut b = Self::novo_zeros(xs.len()); b.as_mut_slice().copy_from_slice(xs); b } pub fn as_slice(&self) -> &[f64] { if self.len == 0 { &[] } else { unsafe { std::slice::from_raw_parts(self.ptr, self.len) } } } pub fn as_mut_slice(&mut self) -> &mut [f64] { if self.len == 0 { &mut [] } else { unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) } } } /// Endereço do buffer (para verificação de alinhamento em testes/bench). pub fn endereco(&self) -> usize { self.ptr as usize } } impl Drop for BufAlinhado64 { fn drop(&mut self) { unsafe { dealloc(self.ptr as *mut u8, self.layout) }; } } impl std::fmt::Debug for BufAlinhado64 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufAlinhado64") .field("len", &self.len) .field("endereco", &format!("{:#x}", self.endereco())) .finish() } } // O ponteiro é ownership exclusivo (sem compartilhamento); Send/Sync seguros. unsafe impl Send for BufAlinhado64 {} unsafe impl Sync for BufAlinhado64 {} // --------------------------------------------------------------------------- // Testes — falsificam: FMA escondido, ordem de soma trocada, alinhamento falso // --------------------------------------------------------------------------- #[cfg(test)] mod testes { use super::*; fn rng_vec(n: usize, semente: u64) -> Vec { let mut rng = crate::rng::RngDeterministico::novo(semente); (0..n).map(|_| rng.normal(0.0, 1.0) as f32).collect() } /// Teo X-1 (falsificação): qualquer FMA/rsqrt/ordem diferente quebra os bits. #[test] fn adamw_bit_a_bit() { let n = 1000usize; let w = rng_vec(n, 1); let g = rng_vec(n, 2); let mut m1 = rng_vec(n, 3); let mut v1 = rng_vec(n, 4); let mut m2 = m1.clone(); let mut v2 = v1.clone(); let mut o1 = vec![0f32; n]; let mut o2 = vec![0f32; n]; // referência: loop escalar IDÊNTICO ao que estava em treino.rs v0.8.0 // (atenção: `(1.0 - b1 as f32)` — subtração em f32 pós-cast) let (lr, b1, b2, eps, wd, bc1, bc2) = (3e-4f64, 0.9, 0.999, 1e-8, 0.01, 0.91, 0.001); let (lrf, b1f, b2f, epsf, wdf, bc1f, bc2f) = ( lr as f32, b1 as f32, b2 as f32, eps as f32, wd as f32, bc1 as f32, bc2 as f32, ); for j in 0..n { m1[j] = b1f * m1[j] + (1.0f32 - b1f) * g[j]; v1[j] = b2f * v1[j] + (1.0f32 - b2f) * g[j] * g[j]; let mh = m1[j] / bc1f; let vh = v1[j] / bc2f; o1[j] = w[j] - lrf * (mh / (vh.sqrt() + epsf) + wdf * w[j]); } // caminho SIMD (com cauda escalar interna) passo_adamw(&w, &g, &mut m2, &mut v2, &mut o2, lr, b1, b2, eps, wd, bc1, bc2); for j in 0..n { assert_eq!(o1[j].to_bits(), o2[j].to_bits(), "saída divergiu em {j}"); assert_eq!(m1[j].to_bits(), m2[j].to_bits(), "m divergiu em {j}"); assert_eq!(v1[j].to_bits(), v2[j].to_bits(), "v divergiu em {j}"); } } #[test] fn escalar_bit_a_bit() { let n = 500usize; let mut a = rng_vec(n, 5); let mut b = a.clone(); let f = 0.37f32; escalar_slice(&mut a, f); for x in b.iter_mut() { *x *= f; } for j in 0..n { assert_eq!(a[j].to_bits(), b[j].to_bits()); } } /// Teo X-2 (falsificação): ordem de soma ou FMA no kernel SoA quebra bits. #[test] fn distancias_bit_a_bit() { let (c, d) = (300usize, 4usize); let mut rng = crate::rng::RngDeterministico::novo(11); let ps: Vec> = (0..c) .map(|_| (0..d).map(|_| rng.normal(0.0, 2.0)).collect()) .collect(); let x: Vec = (0..d).map(|_| rng.normal(0.0, 2.0)).collect(); let (soa, c2, d2) = soa_de_prototipos(&ps); assert_eq!((c2, d2), (c, d)); // melhor_indice escalar da v0.8.0 (réplica) let dist_esc = |a: &[f64], b: &[f64]| -> f64 { a.iter().zip(b).map(|(p, q)| (p - q) * (p - q)).sum() }; let mut melhor = 0usize; let mut dm = f64::INFINITY; for (i, p) in ps.iter().enumerate() { let dd = dist_esc(&x, p); if dd < dm { dm = dd; melhor = i; } } // caminho SoA (AVX-512 quando disponível; cauda escalar) let obtido = melhor_indice_soa(&soa, c, d, &x); assert_eq!(obtido, melhor, "BMU divergiu"); // distâncias completas bit-a-bit let mut out = vec![0.0f64; c]; distancias4_exatas(&soa, c, &x, &mut out); for (i, p) in ps.iter().enumerate() { assert_eq!( out[i].to_bits(), dist_esc(&x, p).to_bits(), "distância divergiu no protótipo {i}" ); } // modo rápido: ε-próximo e mesmo BMU (ordem de associação diferente) let mut rap = vec![0.0f64; c]; distancias4_rapidas(&soa, c, &x, &mut rap); for i in 0..c { let esc = dist_esc(&x, &ps[i]); assert!( (rap[i] - esc).abs() <= 1e-9 * (1.0 + esc.abs()), "modo rápido longe demais: {} vs {}", rap[i], esc ); } assert_eq!(argmin_primeiro(&rap), melhor); } /// Teo X-2' (row-major): o kernel de 2-protótipos-por-ZMM preserva bits /// contra o escalar `distancia2`/`melhor_indice`. #[test] fn distancias_bit_a_bit_rm() { let (c, d) = (500usize, 4usize); let mut rng = crate::rng::RngDeterministico::novo(19); let ps: Vec> = (0..c) .map(|_| (0..d).map(|_| rng.normal(0.0, 2.0)).collect()) .collect(); let x: Vec = (0..d).map(|_| rng.normal(0.0, 2.0)).collect(); let (rm, c2, d2) = rm_de_prototipos(&ps); assert_eq!((c2, d2), (c, d)); let dist_esc = |a: &[f64], b: &[f64]| -> f64 { a.iter().zip(b).map(|(p, q)| (p - q) * (p - q)).sum() }; let mut melhor = 0usize; let mut dm = f64::INFINITY; for (i, p) in ps.iter().enumerate() { let dd = dist_esc(&x, p); if dd < dm { dm = dd; melhor = i; } } let mut buf = Vec::with_capacity(c); let obtido = melhor_indice_rm_buf(&rm, c, &x, &mut buf); assert_eq!(obtido, melhor, "BMU rm divergiu"); for (i, p) in ps.iter().enumerate() { assert_eq!(buf[i].to_bits(), dist_esc(&x, p).to_bits(), "rm dist divergiu em {i}"); } } /// Errata-2 empirica: o buffer dedicado É alinhado a 64; `Vec` comum NÃO /// tem essa garantia (apenas o alinhamento do tipo/allocador). #[test] fn simd_enderecos() { for len in [1usize, 7, 64, 257, 1000, 4096] { let b = BufAlinhado64::novo_zeros(len); assert_eq!(b.endereco() % 64, 0, "alinhamento 64B violado (len={len})"); assert_eq!(b.as_slice().len(), len.max(1)); } // cópia preserva conteúdo e alinhamento let orig: Vec = (0..100).map(|i| i as f64 * 0.5).collect(); let b = BufAlinhado64::de_fatia(&orig); assert_eq!(b.endereco() % 64, 0); assert_eq!(b.as_slice(), orig.as_slice()); // Vec do alocador global: SEM garantia de 64B (documenta a errata; // não afirmamos que sempre viola — afirmamos que NÃO é garantido) let mut violacoes = 0; for _ in 0..32 { let v: Vec = (0..1000).map(|i| i as f64).collect(); if v.as_ptr() as usize % 64 != 0 { violacoes += 1; } } // com glibc, alocações grandes frequentemente caem em páginas alinhadas // — o ponto é a GARANTIA, não a frequência; apenas registramos. eprintln!("[simd_enderecos] Vec não-alinhado em {violacoes}/32 alocações"); } #[test] fn despacho_conta_chamadas() { let antes = relatorio(); let b = BufAlinhado64::novo_zeros(128); let x = [0.1f64, 0.2, 0.3, 0.4]; let mut out = vec![0.0f64; 128]; distancias4_exatas(b.as_slice(), 128, &x, &mut out); let depois = relatorio(); let n0 = antes["chamadas_avx"].as_u64().unwrap_or(0) + antes["chamadas_escalar"].as_u64().unwrap_or(0); let n1 = depois["chamadas_avx"].as_u64().unwrap_or(0) + depois["chamadas_escalar"].as_u64().unwrap_or(0); assert!(n1 > n0, "contadores de despacho não incrementaram"); } }