mirror of
https://github.com/ruvnet/RuView
synced 2026-08-07 20:01:43 +00:00
bfb3fdee13
Implements Phases 1-3 of the ADR-096 roadmap: Phase 1: workspace integration - Add `ruvllm_sparse_attention` as a path-vendored workspace dep against `vendor/ruvector/crates/ruvllm_sparse_attention`, default-features=false, features=["fp16"]. Mirrors the no_std posture ADR-095 will need on the firmware side so both consumers share a single feature set. - Register `wifi-densepose-temporal` as workspace member. Phase 2: AETHER temporal head - `AetherTemporalHead` facade dispatches to a `SparseGqa` backend wrapping `SubquadraticSparseAttention`. Selection rule from ADR-096 §4.4 enforced at forward(): MHA branch when q_heads == kv_heads, GQA branch otherwise. - `Dense` backend reserved (returns typed `DenseBackendNotImplemented`) so config-time validation fails loudly instead of at forward(). - `TemporalHeadConfig::default_aether()` matches the AETHER training default per ADR-096 §3.1 (window=32, block=16, q=4, kv=1 → MQA). - Token 0 always wired as a global anchor — preserves AETHER's contrastive "session-start reference" role per ADR-024. Phase 3: smoke tests (5/5 passing) - forward at AETHER default config, both MHA and GQA dispatch paths, rejected dense backend, rejected non-divisible GQA ratio, and the long-window roadmap target (N=1000, the 10s @ 100Hz case from ADR-096 §3.1 — proves the kernel runs at lengths where dense MHA costs 10⁶ edge ops vs sparse 10⁴). Streaming `step()` deferred — KvCache lifecycle ties to PoseTrack per ADR-096 §8.5 and lands when the firmware-side ABI does (Phase 4+). Co-Authored-By: claude-flow <ruv@ruv.net>
114 lines
3.6 KiB
Rust
114 lines
3.6 KiB
Rust
//! Smoke tests for the AETHER sparse-GQA temporal head (ADR-096 §5 gate is
|
|
//! a separate accuracy benchmark; this file just proves the wiring works).
|
|
|
|
use wifi_densepose_temporal::{
|
|
AetherTemporalHead, TemporalBackendKind, TemporalHeadConfig, TemporalError, Tensor3,
|
|
};
|
|
|
|
fn make_qkv(seq: usize, q_heads: usize, kv_heads: usize, dim: usize) -> (Tensor3, Tensor3, Tensor3) {
|
|
// Deterministic synthetic CSI-like activations so the test is
|
|
// reproducible across machines without bringing in `rand`.
|
|
let mut q = Tensor3::zeros(seq, q_heads, dim);
|
|
for s in 0..seq {
|
|
for h in 0..q_heads {
|
|
for d in 0..dim {
|
|
let v = ((s * 31 + h * 7 + d) as f32).sin() * 0.1;
|
|
q.set(s, h, d, v);
|
|
}
|
|
}
|
|
}
|
|
let mut k = Tensor3::zeros(seq, kv_heads, dim);
|
|
let mut v = Tensor3::zeros(seq, kv_heads, dim);
|
|
for s in 0..seq {
|
|
for h in 0..kv_heads {
|
|
for d in 0..dim {
|
|
let kv = (((s * 17 + h * 3 + d) as f32).cos()) * 0.1;
|
|
k.set(s, h, d, kv);
|
|
v.set(s, h, d, kv * 0.5);
|
|
}
|
|
}
|
|
}
|
|
(q, k, v)
|
|
}
|
|
|
|
#[test]
|
|
fn sparse_gqa_forward_runs_at_aether_default() {
|
|
let cfg = TemporalHeadConfig::default_aether();
|
|
let head = AetherTemporalHead::new(&cfg).expect("construct");
|
|
|
|
let (q, k, vt) = make_qkv(64, cfg.q_heads, cfg.kv_heads, cfg.head_dim);
|
|
let out = head.forward(&q, &k, &vt).expect("forward");
|
|
let (oseq, oh, od) = out.shape();
|
|
assert_eq!(oseq, 64);
|
|
assert_eq!(oh, cfg.q_heads);
|
|
assert_eq!(od, cfg.head_dim);
|
|
}
|
|
|
|
#[test]
|
|
fn sparse_mha_path_runs_when_qkv_heads_match() {
|
|
// q_heads == kv_heads forces the `forward` (non-GQA) branch.
|
|
let cfg = TemporalHeadConfig {
|
|
backend: TemporalBackendKind::SparseGqa,
|
|
q_heads: 2,
|
|
kv_heads: 2,
|
|
head_dim: 16,
|
|
window: 8,
|
|
block_size: 4,
|
|
causal: true,
|
|
};
|
|
let head = AetherTemporalHead::new(&cfg).expect("construct");
|
|
let (q, k, vt) = make_qkv(32, 2, 2, 16);
|
|
let out = head.forward(&q, &k, &vt).expect("forward");
|
|
assert_eq!(out.shape(), (32, 2, 16));
|
|
}
|
|
|
|
#[test]
|
|
fn dense_backend_returns_typed_error() {
|
|
let cfg = TemporalHeadConfig {
|
|
backend: TemporalBackendKind::Dense,
|
|
q_heads: 4,
|
|
kv_heads: 1,
|
|
head_dim: 32,
|
|
window: 32,
|
|
block_size: 16,
|
|
causal: true,
|
|
};
|
|
let err = AetherTemporalHead::new(&cfg).err().expect("dense rejected");
|
|
matches!(err, TemporalError::DenseBackendNotImplemented);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_gqa_ratio_rejected_at_construction() {
|
|
let cfg = TemporalHeadConfig {
|
|
backend: TemporalBackendKind::SparseGqa,
|
|
q_heads: 5,
|
|
kv_heads: 2, // 5 % 2 != 0
|
|
head_dim: 16,
|
|
window: 8,
|
|
block_size: 4,
|
|
causal: true,
|
|
};
|
|
let err = AetherTemporalHead::new(&cfg).err().expect("rejected");
|
|
matches!(err, TemporalError::InvalidConfig(_));
|
|
}
|
|
|
|
#[test]
|
|
fn long_window_at_aether_roadmap_target() {
|
|
// ADR-096 §3.1 roadmap target: 10 s @ 100 Hz = 1000 frames. Verify
|
|
// the kernel actually runs at this length so the long-window claim
|
|
// is more than aspirational.
|
|
let cfg = TemporalHeadConfig {
|
|
backend: TemporalBackendKind::SparseGqa,
|
|
q_heads: 4,
|
|
kv_heads: 1,
|
|
head_dim: 16,
|
|
window: 64,
|
|
block_size: 32,
|
|
causal: true,
|
|
};
|
|
let head = AetherTemporalHead::new(&cfg).expect("construct");
|
|
let (q, k, vt) = make_qkv(1000, 4, 1, 16);
|
|
let out = head.forward(&q, &k, &vt).expect("forward at N=1000");
|
|
assert_eq!(out.shape(), (1000, 4, 16));
|
|
}
|