sketch_freq_directions.nx
buildroot/runtime/sketch_freq_directions.nx
about
sketch_freq_directions.nx -- Liberty 2013 Frequent Directions sketch.
Streaming low-rank matrix approximation. Given a stream of input
rows in R^d, maintain a sketch matrix B in R^(l x d) (l << n) such
that B^T B approximates A^T A to within an additive error bound
based on the tail singular values:
|| A^T A - B^T B ||_F <= || A - A_k ||_F^2 / (l - k)
where A_k is the best rank-k approximation of A. Direct
matrix-valued analog of Misra-Gries frequency counting (Liberty's
own framing in the 2013 STOC paper).
ALGORITHM:
For each input row x in R^d:
1. If any row of B is "empty" (all zeros) -> copy x into it.
2. Else -> shrink:
a. Compute SVD: B = U Σ V^T
b. delta := σ_l^2 (smallest singular value squared)
c. Shrunk singular values: σ_i'^2 := max(0, σ_i^2 - delta)
d. B := diag(sqrt(σ_i'^2)) V^T
-- by construction the last row has σ_l' = 0 (empty)
e. Copy x into the now-empty row.
SVD WITHOUT f64 (the technical chokepoint -- see queued roadmap
2026-05-10):
This reference impl uses POWER ITERATION with DEFLATION in Q14
fixed-point. Bounded to l <= 4, d <= 8 for this version --
convergence guarantee at small sizes is reliable; for larger
matrices the accumulated round-off in Q14 degrades the
shrink quality. v2 will swap in fixed-point Jacobi or
bidiagonal + QR for arbitrary size.
MATURITY = REFERENCE_IMPL with EXPLICIT size limits. Substrate
callers requesting larger l or d get NX_FD_ERR_TOO_BIG at alloc.
COMPOSITION:
- Complements sketch_kmeans1d (clustering family) for streaming
dimensionality reduction before clustering.
- Complements sketch_correlation for streaming low-rank
dependencies 3 imports · 3 importers
imports: syscalls.nxsketch_types.nxnx_vecmath.nx
imported by: nx_lowrank_kv_gate.nxsketch_freq_directions_test.nxsketch_freq_directions_vs_topnorm_bench.nx
structs
| 59 | struct FreqDir |
consts
| 49 | const NX_FD_Q14: i64 = 16384 |
| 50 | const NX_FD_L_MAX: i64 = 4 |
| 51 | const NX_FD_D_MAX: i64 = 8 |
| 52 | const NX_FD_POWER_ITERS: i64 = 32 |
| 53 | const NX_FD_POWER_TOL_Q14: i64 = 1 // 1/16384 ~ 6e-5 convergence |
| 55 | const NX_FD_OK: i64 = 0 |
| 56 | const NX_FD_ERR_TOO_BIG: i64 = -1 |
| 57 | const NX_FD_ERR_DIM_MISMATCH: i64 = -2 |
functions
| 73 | func nx_fd_alloc(l: i64, d: i64) -> *FreqDir |
| 105 | func nx_fd_isqrt(x: i64) -> i64 { return vm_isqrt(x) } |
| 107 | func nx_fd_abs(x: i64) -> i64 |
| 114 | func nx_fd_b_get(fd: *FreqDir, r: i64, c: i64) -> i64 |
| 118 | func nx_fd_b_set(fd: *FreqDir, r: i64, c: i64, v: i64) -> i64 |
| 123 | func nx_fd_row_norm_sq(fd: *FreqDir, r: i64) -> i64 |
| 134 | func nx_fd_empty_row_index(fd: *FreqDir) -> i64 |
| 149 | func nx_fd_dot(a: *i64, b: *i64, n: i64) -> i64 |
| 159 | func nx_fd_vnorm_sq(v: *i64, n: i64) -> i64 |
| 163 | func nx_fd_vscale(v: *i64, n: i64, factor_q14: i64) -> i64 |
| 172 | func nx_fd_vsub_scaled(target: *i64, source: *i64, n: i64, factor_q14: i64) -> i64 |
| 181 | func nx_fd_vnormalize(v: *i64, n: i64) -> i64 |
| 198 | func nx_fd_matvec_Bv(fd: *FreqDir, v: *i64, out: *i64) -> i64 |
| 213 | func nx_fd_matvec_Btw(fd: *FreqDir, w: *i64, out: *i64) -> i64 |
| 234 | func nx_fd_basis_get(fd: *FreqDir, idx: i64, c: i64) -> i64 |
| 238 | func nx_fd_basis_set(fd: *FreqDir, idx: i64, c: i64, v: i64) -> i64 |
| 244 | func nx_fd_deflate(fd: *FreqDir, v: *i64, found: i64) -> i64 |
| 268 | func nx_fd_power_one(fd: *FreqDir, found_idx: i64, init_idx: i64) -> i64 |
| 341 | func nx_fd_shrink(fd: *FreqDir) -> i64 |
| 378 | func nx_fd_add_row(fd: *FreqDir, row: *i64, n: i64) -> i64 |
| 421 | func nx_fd_top_sigma_sq(fd: *FreqDir) -> i64 |
| 435 | func nx_fd_total_rows(fd: *FreqDir) -> i64 |
| 439 | func nx_fd_memory_bytes(fd: *FreqDir) -> i64 |
| 450 | func nx_fd_rel_err_ppb(l: i64) -> i64 |
| 456 | func nx_fd_query_top(fd: *FreqDir) -> *ApproxI64 |