Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/board/STATUS_BOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ Docs #1756; lab crate `drf-focus-probe` #1762–#1771 (workspace-excluded, no CI
|---|---|---|---|
| **D-DRF-1** | reference semantics: geometry, focus lease, lookahead prefetch, foreign-key cache exception, dirty-row writeback, learning overlay | Shipped, docs only (#1756 `06adcbce`) | falsifier matrix pre-registered; run so far (lab): G1/G3/G4/G6, L1–L3, P1–P2, W1–W4, W6–W8. NOT RUN: G2/G5/G7, L4, P3, M1–M2, W5, R1–R7 |
| **DRF-P1** | two-focus lazy load / lease / prefetch / last-reader eviction | Shipped, lab (#1762 `62d6c70f`; key gains `facet`, #1769 `94d254a1`) | L1–L3, G1/G3/G4/G6, P1 green; `entries/2026-10-24-drf-p1-focus-lease-probe.md`, `entries/2026-10-25-drf-focuskey-facet.md` |
| **DRF-P2** | 64-thought foreign-table A/B/C (direct+prefetch vs gather+reuse vs gather+reuse+writeback) | Partly measured, lab (#1763 `e8aabd95`, #1764 `2bfb2908`) | break-even 1–3 folds warm and cold; A1/A3 NOT RUN, so the exception's lapse rule is not decided; `entries/2026-10-24-drf-p2-breakeven.md` |
| **DRF-P2** | 64-thought foreign-table A/B/C (direct+prefetch vs gather+reuse vs gather+reuse+writeback) | Partly measured, lab (#1763 `e8aabd95`, #1764 `2bfb2908`) | break-even 1–3 folds warm and cold; A1/A3 NOT RUN (they need a real workload), so the exception's lapse rule is not decided; gather cost rises with distinct pages at constant cache lines and balanced offsets (flat to 2 048 pages, 2.5-3x by 4 096), but the original 1 024 vs 65 536 gap also changed the line count and is not decomposed; 2 MiB-page control and counters not run; `entries/2026-10-24-drf-p2-breakeven.md`, `entries/2026-10-25-drf-tlb-probe.md` |
| **DRF-W** | writeback W1–W4, W6–W8: per-row coalesce, conflict refusal, handoff to the real CycleWriter, lost-ack resubmit, no-change cycle, eviction budget | Shipped, lab (#1765–#1768, #1770, #1771; `22bac9d3`…`332b420c`) | W5 (descriptor-only cast) NOT RUN; fake WAL stands in for Lance; `entries/2026-10-25-drf-w1-writeback.md`, `-w3-ordered-apply`, `-w4-handoff`, `-w8-budget` |
| **DRF-CHURN** | eviction victim order under churn (LRU vs MRU) | In PR (#1772) | measured: neither wins both cyclic and phased; default unchanged; `entries/2026-10-25-drf-churn-policy.md` (on #1772) |
| **DRF-CHURN** | eviction victim order under churn (LRU vs MRU) | Shipped, lab (#1772) | measured: neither wins both cyclic and phased; default unchanged; `entries/2026-10-25-drf-churn-policy.md` |
| **DRF-P3** | relationship cube / slices and typed dirty-row reduction oracle | Proposed | R1–R3, W1–W3; no Bag/NULL/evidence loss |
| **DRF-P4** | optional plasticity overlay + ONNX candidate stream | Proposed | R4–R7; base DR revision bit-identical |

Expand Down
5 changes: 5 additions & 0 deletions .claude/board/entries/2026-10-24-drf-p2-breakeven.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,8 @@ Fold cost: before ndarray #376, `masked_sum_i32` walked set bits one by one; ful
OPEN: the warm 65 536-row build (23 µs vs 3.8 µs at 1024 rows, same cache-line footprint)
suggests TLB misses across 32 MiB of 4 KiB pages; not measured. Also open: focus churn under
a memory budget, and dirty-row writeback (A1, W1–W8).

Correction (2026-10-25, see `2026-10-25-drf-tlb-probe.md`): the "same cache-line footprint" and
"whatever the row count" statements above are wrong for the 1 024-row random case (about 1 005
distinct lines against about 3 970 at 65 536 rows). The 3.8 vs 23 µs gap is not attributable to
TLB alone.
71 changes: 71 additions & 0 deletions .claude/board/entries/2026-10-25-drf-tlb-probe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# D-DRF-TLB — gather cost against distinct pages touched (lab, reported not gated)

**Status:** MEASURED (lab), PARTLY EXPLAINED. **Probe:** `crates/drf-focus-probe/examples/tlb_probe.rs`
(`cargo run --release --example tlb_probe`). Narrows, does not close, the OPEN point in
`2026-10-24-drf-p2-breakeven.md` (warm 65 536-row build 23 µs vs 3.8 µs at 1 024 rows): that
comparison was not at constant cache lines (see Correction), so this probe shows a locality
effect at constant lines, not the full cause of the original gap.
Host: Xeon 2.1 GHz, L2 8 MiB (4 instances), L3 260 MiB, THP `madvise` (so the 32 MiB row
buffer was read back from `/proc/self/smaps` at run time: KernelPageSize 4 kB, AnonHugePages
0 kB, so the policy line alone is not the evidence; no 2 MiB control was run). Page, line and
within-page offset counts come from the real row addresses, and the probe prints the 8-bin
within-page offset histogram (512 per bin for `balanced` and `shuffled`; all 4096 in one bin
for `skewed` at P = 4096).

Pre-registered in the file header before the first run: H_TLB (build rises with distinct pages
past the second-level TLB reach) against H_other (flat in pages; cache lines set the cost).
Slots fixed at 4 096. For P >= 512 the distinct cache-line count is constant at 4 096.

## Two generators (added after review)

The first generator, `skewed`, read row `(i % P) * 8 + (i / P) % 8`. That makes the number of
distinct within-page offsets fall with P (8 at 512 pages, 4 at 1 024, 2 at 2 048, 1 at 4 096),
so cache-set coverage changed with P along with the page count. The `balanced` generator
rotates the offset by page (`(visit + page % 8) % 8`), so all 8 offsets are used at every P.

Warm build ns (median of 401; `balanced` over three native runs and one `x86-64-v3` run):

| pages | skewed (1 run) | balanced |
|---|---|---|
| 512 | 12 200 | 8 500-11 100 |
| 1024 | 17 700 | 8 800-10 400 |
| 1536 | 39 800 (outlier) | 9 000-9 700 |
| 2048 | 18 100 | 9 900-13 900 |
| 3072 | 29 700 | 22 400-25 200 |
| 4096 | 27 600 | 26 700-32 100 |

Cold builds (one run per row, `balanced`) rise 63 000 -> 150 000 ns over 512 -> 4 096 pages.

- **H_other rejected.** At constant 4 096 lines and balanced offsets the warm build is about
9-10 µs up to 2 048 pages and 22-32 µs at 3 072-4 096: 2.5-3x. Pages matter on their own.
- **The 512 -> 1 024 step depends on the generator.** It is gone under `balanced` (9 µs ->
9-10 µs); with `skewed` the 1 024-page build was 17.7 µs. The change alters which lines are
selected, not only their set coverage, so this is consistent with cache-set conflict but does
not prove it.
- **Shape under `balanced`:** flat to 2 048 pages, rise between 2 048 and 3 072. That is
consistent with second-level TLB reach, but no counter or huge-page control confirms it.
- **Visit order does not create the shape.** A `shuffled` arm (same ordinals as `balanced`,
seeded shuffle, one run) gives 9.7 µs at 2 048 pages, 15.3 µs at 3 072 and 25.3 µs at
4 096: the same flat-then-rise shape.
- **Not shown:** that huge pages remove the effect (the 2 MiB arm needs an aligned `madvise`
allocation); any hardware-counter evidence (perf is not usable here, `perf_event_paranoid` =
2 and no `perf`); the cause of the cold-build rise. The cost-model quantity may be the
translation working set (page reuse distance), not the page count; that is a hypothesis, not
a result, and no optimization rule follows from it.

## Correction to `2026-10-24-drf-p2-breakeven.md`

That entry says a focus touches "one cache line per distinct ordinal: 248-256 KiB here,
whatever the row count" and compares 1 024 and 65 536 rows as "same cache-line footprint".
Distinct rows drawn by 4 096 random slots from N rows is about N(1 - e^(-4096/N)): about 1 005
at N = 1 024 (about 63 KiB of lines) and about 3 970 at N = 65 536 (about 248 KiB). So the
original 3.8 µs vs 23 µs comparison also changed the line count roughly fourfold, and the
duplicates at 1 024 rows were served from lines already loaded. The gap combines more distinct
lines, fewer repeated addresses and more page dispersion; this probe isolates only the last.

Consequence for the cost model: the focus build is not a function of slot count alone, and
a clustered map is cheaper than a scattered one with the same slots. How much is pages and how
much is lines in the original gap is not separated.

OPEN: the 2 MiB-page control (two-factor with visit order); hardware counters; page reuse
distance vs page count; the original 1 024 vs 65 536 gap decomposed into lines vs pages.
3 changes: 2 additions & 1 deletion .claude/board/entries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately
opposite directions; the stranding this convention prevents shows up in
exactly one of them, never both.

472 entries, 2026-08-06 .. 2026-10-25.
473 entries, 2026-08-06 .. 2026-10-25.

| date | entry id | finding | file |
|---|---|---|---|
| 2026-10-25 | `D-DRF-W8` | | [2026-10-25-drf-w8-budget.md](2026-10-25-drf-w8-budget.md) |
| 2026-10-25 | `D-DRF-W4` | | [2026-10-25-drf-w4-handoff.md](2026-10-25-drf-w4-handoff.md) |
| 2026-10-25 | `D-DRF-W3` | | [2026-10-25-drf-w3-ordered-apply.md](2026-10-25-drf-w3-ordered-apply.md) |
| 2026-10-25 | `D-DRF-W1` | | [2026-10-25-drf-w1-writeback.md](2026-10-25-drf-w1-writeback.md) |
| 2026-10-25 | `D-DRF-TLB` | | [2026-10-25-drf-tlb-probe.md](2026-10-25-drf-tlb-probe.md) |
| 2026-10-25 | `D-DRF-G-FACET` | | [2026-10-25-drf-focuskey-facet.md](2026-10-25-drf-focuskey-facet.md) |
| 2026-10-25 | `D-DRF-CHURN` | | [2026-10-25-drf-churn-policy.md](2026-10-25-drf-churn-policy.md) |
| 2026-10-24 | `tanh-precision-symmetric-i8` | | [2026-10-24-tanh-precision-symmetric-i8.md](2026-10-24-tanh-precision-symmetric-i8.md) |
Expand Down
181 changes: 181 additions & 0 deletions crates/drf-focus-probe/examples/tlb_probe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
//! D-DRF-P2 follow-up: does the 65 536-row build cost track distinct 4 KiB pages touched?
//!
//! `p2_breakeven` measured the warm gather at 3.8 µs for 1 024 rows and 23 µs for 65 536 rows
//! with the same 4 096 slots. This probe holds the slot count fixed, builds maps that touch a
//! chosen number P of distinct pages, and reports the gather cost against P. Reported, not gated.
//!
//! PRE-REGISTERED (before the first run), per build on a cold manager, warm caches:
//! H_TLB build ns rises with P once P passes the second-level TLB reach (about 1.5-2 K
//! entries on this class of host), so the P = 4096 build is clearly above the
//! P = 512 build, even though both touch 4096 distinct cache lines.
//! H_other build ns is flat in P over 512..4096 (cache lines, not pages, set the cost).
//! Within P >= 512 the distinct cache-line count is constant at 4096, so that range isolates
//! the page effect. Below 512 the line count also falls; those rows are shown but do not
//! decide. Page counts are computed from the real row addresses, not assumed from the row size.
//! cargo run --release --example tlb_probe
use drf_focus_probe::*;
use std::collections::HashSet;
use std::hint::black_box;
use std::sync::Arc;
use std::time::Instant;

const OFF: usize = 16;
const NROWS: usize = 65536;
const PAGE: usize = 4096;

fn median_ns(reps: usize, mut f: impl FnMut()) -> f64 {
let mut v: Vec<f64> = (0..reps)
.map(|_| {
let t = Instant::now();
f();
t.elapsed().as_nanos() as f64
})
.collect();
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
v[reps / 2]
}

fn evict(buf: &mut [u64]) {
let mut acc = 0u64;
for (i, v) in buf.iter_mut().enumerate() {
*v = v.wrapping_add(i as u64);
acc ^= *v;
}
black_box(acc);
}

fn median_cold_ns(reps: usize, buf: &mut [u64], mut f: impl FnMut()) -> f64 {
let mut v: Vec<f64> = (0..reps)
.map(|_| {
evict(buf);
let t = Instant::now();
f();
t.elapsed().as_nanos() as f64
})
.collect();
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
v[reps / 2]
}

/// Kernel page size and huge-page bytes for the mapping holding `addr`, read from smaps. Policy
/// (`transparent_hugepage/enabled`) does not say how a given buffer is actually backed.
fn backing(addr: usize) -> String {
let Ok(text) = std::fs::read_to_string("/proc/self/smaps") else {
return "smaps unreadable".into();
};
let mut hit = false;
let mut out = String::from("mapping not found");
for line in text.lines() {
if let Some((range, _)) = line.split_once(' ')
&& let Some((lo, hi)) = range.split_once('-')
&& let (Ok(lo), Ok(hi)) = (usize::from_str_radix(lo, 16), usize::from_str_radix(hi, 16))
{
hit = (lo..hi).contains(&addr);
continue;
}
if hit && (line.starts_with("KernelPageSize") || line.starts_with("AnonHugePages")) {
if out == "mapping not found" {
out.clear();
}
out.push_str(&line.split_whitespace().collect::<Vec<_>>().join(" "));
out.push_str("; ");
}
}
out
}

fn main() {
let thp = std::fs::read_to_string("/sys/kernel/mm/transparent_hugepage/enabled")
.unwrap_or_else(|_| "unreadable".into());
println!("transparent_hugepage: {}", thp.trim());
let mut evict_buf = vec![0u64; 64 << 20];
let rows: Vec<Row> = (0..NROWS)
.map(|i| {
let mut r = [0u8; ROW];
r[OFF..OFF + 4].copy_from_slice(&((i as i32) * 3 - 7).to_le_bytes());
r
})
.collect();
println!("row buffer backing: {}", backing(rows.as_ptr() as usize));
let soa = Soa {
rows: &rows,
id: SourceId {
dataset: 1,
branch: 1,
version: 1,
index_seal: 7,
},
};
let facet = Facet::new(OFF).unwrap();
println!(
"slots={SLOTS} rows={NROWS} ({} MiB) facet=i32@{OFF}",
(NROWS * ROW) >> 20
);
println!(
"{:>6} {:>8} {:>7} {:>9} {:>9} {:>9} {:>9}",
"P_req", "pages", "lines", "build_w", "build_c", "ns/slot_w", "ns/page_w"
);
// Two generators over the same P. "skewed": slot i reads row (i % P) * 8 + (i / P) % 8, so the
// number of distinct within-page offsets shrinks as P grows (8 at P=512, 1 at P=4096) and
// cache-set coverage changes with P. "balanced": the offset is rotated by page, so every
// offset is used and the offsets-per-page spread no longer depends on P.
for arm in ["skewed", "balanced", "shuffled"] {
println!("-- {arm}");
println!(
"{:>6} {:>7} {:>7} {:>5} {:>9} {:>9} {:>9}",
"P_req", "pages", "lines", "offs", "build_w", "build_c", "ns/slot_w"
);
for p_req in [64usize, 128, 256, 512, 1024, 1536, 2048, 3072, 4096] {
let mut ord = Box::new([0u16; SLOTS]);
for (i, o) in ord.iter_mut().enumerate() {
let (page, visit) = (i % p_req, i / p_req);
let off = if arm == "skewed" {
visit % 8
} else {
(visit + page % 8) % 8
};
*o = ((page * 8 + off) % NROWS) as u16;
}
if arm == "shuffled" {
// Same set of ordinals as `balanced`, seeded Fisher-Yates visit order.
let mut x = 0x9E37_79B9_7F4A_7C15u64 ^ p_req as u64;
for i in (1..SLOTS).rev() {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
ord.swap(i, (x % (i as u64 + 1)) as usize);
}
}
let addr = |o: u16| rows.as_ptr() as usize + o as usize * ROW + OFF;
let pages: HashSet<usize> = ord.iter().map(|&o| addr(o) / PAGE).collect();
let lines: HashSet<usize> = ord.iter().map(|&o| addr(o) / 64).collect();
let offs: HashSet<usize> = ord.iter().map(|&o| (addr(o) % PAGE) / 64).collect();
let mut hist = [0usize; 8];
for &o in ord.iter() {
hist[(addr(o) % PAGE) / 512] += 1;
}
let map = Arc::new(SealedMap::new(MapId(p_req as u64), 7, ord));
let warm = median_ns(401, || {
let mgr = FocusManager::new();
black_box(mgr.load(&soa, &map, Scope(1), facet).unwrap());
});
let cold = median_cold_ns(21, &mut evict_buf, || {
let mgr = FocusManager::new();
black_box(mgr.load(&soa, &map, Scope(1), facet).unwrap());
});
println!(
"{:>6} {:>7} {:>7} {:>5} {:>9.0} {:>9.0} {:>9.2}",
p_req,
pages.len(),
lines.len(),
offs.len(),
warm,
cold,
warm / SLOTS as f64
);
if p_req == 4096 || p_req == 512 {
println!(" offset-bin histogram (8 x 512 B): {hist:?}");
}
}
}
}
Loading