Closes the Dense placeholder from earlier commits. Now both backends
implement forward(); only SparseGqa supports streaming step()/KvCache,
which is the structural gap dense MHA can't bridge by design.
Dense path:
- src/dense.rs new — DenseHead wraps upstream dense_attention. Stores
causal flag and (cloned) config. forward() is a one-line delegation;
no GQA dispatch (dense_attention upstream requires q_heads == kv_heads).
- AetherTemporalHead::Dense changed from a unit variant to Dense(DenseHead).
Construction succeeds for any valid TemporalHeadConfig where backend
is Dense.
- AetherTemporalHead.step() returns BackendDoesNotSupportStreaming for
Dense — there is no dense-MHA-with-KV-cache equivalent and offering
one would silently swallow the ADR-096 §3.2 structural argument.
- AetherTemporalHead.make_cache() likewise — there's no cache to size
for a dense kernel.
Errors:
- New TemporalError::BackendDoesNotSupportStreaming variant covers
the Dense-step / Dense-make_cache cases. Specific so callers can
fall back to forward() instead of giving up entirely.
- TemporalError::DenseBackendNotImplemented retained for v0.1
back-compat (no consumers depend on it post-this-commit, but
removing a public variant is a hard break). Future work can
deprecate it once downstream callers move off.
Tests (19/19 passing):
- dense_backend_returns_typed_error → renamed and rewritten as
dense_backend_forward_runs_with_matching_shape: constructs a Dense
head, runs forward over (32, 4, 4, 16) Q/K/V, asserts output shape.
- New dense_backend_step_returns_streaming_error: constructs Dense,
attempts make_cache, expects BackendDoesNotSupportStreaming.
- All 8 weight blob, 2 blob e2e, 3 streaming, 5 other smoke tests
unchanged and still passing.
This commit completes the ADR-096 §5 A/B gate: callers can now run
the same Q/K/V through both backends and compare outputs / latency.
The §5 four-gate validation (contrastive loss within 1%, rank-1
within 1pp, Spearman ≥0.95, latency ≥5×) becomes a runnable
proposition, not a future task — though the actual gate run requires
trained AETHER weights, which is its own track.
Co-Authored-By: claude-flow <ruv@ruv.net>
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>