v0.9.0: análise/prova/teste das críticas Qwen3.8 lote 2 (r-34) — SIMD com despacho em runtime bit-a-bit (X-1), oráculo candle_nn::optim::AdamW (nosso 1,53× mais rápido), Thompson Sampling (regret 0,0019 vs UCB1 0,0156), GNG por entropia com piso (T-G1'), JEPA de fração adaptativa (bit-a-bit com 1 braço), configs tipadas, entrada numpy (2008× na fronteira), avaliação com dados reais — 104/104 gates, 111/111 cargo
d444358 verified Download src/simd.rs from PowerMachine/khtst-rust: direct link, hf CLI and curl.
- Browser
- Download file 30.3 kB
-
https://huggingface.co/PowerMachine/khtst-rust/resolve/main/src/simd.rs
- Command line
-
hf download hf://PowerMachine/khtst-rust/src/simd.rs
-
curl -L -o simd.rs https://huggingface.co/PowerMachine/khtst-rust/resolve/main/src/simd.rs
30.3 kB
| //! 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 { | |
| { | |
| 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") | |
| } | |
| { | |
| 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. | |
| 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; | |
| } | |
| } | |
| } | |
| 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<Vec<f64>>` 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<f64>]) -> (Vec<f64>, 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. | |
| 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<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<f64>]) -> (Vec<f64>, 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. | |
| 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<f64>) { | |
| 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<f64>) -> 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<f64>) { | |
| 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<f64>) -> 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::<f64>()).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 | |
| // --------------------------------------------------------------------------- | |
| mod testes { | |
| use super::*; | |
| fn rng_vec(n: usize, semente: u64) -> Vec<f32> { | |
| 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. | |
| 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}"); | |
| } | |
| } | |
| 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. | |
| fn distancias_bit_a_bit() { | |
| let (c, d) = (300usize, 4usize); | |
| let mut rng = crate::rng::RngDeterministico::novo(11); | |
| let ps: Vec<Vec<f64>> = (0..c) | |
| .map(|_| (0..d).map(|_| rng.normal(0.0, 2.0)).collect()) | |
| .collect(); | |
| let x: Vec<f64> = (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`. | |
| fn distancias_bit_a_bit_rm() { | |
| let (c, d) = (500usize, 4usize); | |
| let mut rng = crate::rng::RngDeterministico::novo(19); | |
| let ps: Vec<Vec<f64>> = (0..c) | |
| .map(|_| (0..d).map(|_| rng.normal(0.0, 2.0)).collect()) | |
| .collect(); | |
| let x: Vec<f64> = (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). | |
| 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<f64> = (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<f64> 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<f64> = (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<f64> não-alinhado em {violacoes}/32 alocações"); | |
| } | |
| 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"); | |
| } | |
| } | |