Skip to content

Add cold-eye cache and prefetch for CPU linear scan - #2351

Open
philsippl wants to merge 11 commits into
codex/cpu-linear-scanfrom
codex/cpu-linear-scan-cold-eye-cache
Open

Add cold-eye cache and prefetch for CPU linear scan#2351
philsippl wants to merge 11 commits into
codex/cpu-linear-scanfrom
codex/cpu-linear-scan-cold-eye-cache

Conversation

@philsippl

Copy link
Copy Markdown
Contributor

Summary

Adds the cold-eye data plane on top of the exact CPU linear-scan correctness baseline in #2347. This layer does not change matching thresholds, rotations, result semantics, or the resident-eye compute kernel.

How it works

  • Keeps only the configured full-scan eye resident for the process lifetime.
  • Keeps the most recent cold-eye records in a rolling LUC window.
  • Holds uncommitted mutations in a versioned FIFO overlay until the database commit is acknowledged.
  • Starts bounded cold-eye prefetch while the resident-eye scan is still running, so database reads overlap the first-stage scan.
  • Uses Moka's TinyLFU cache for older hot records such as recurring supermatchers (4,096 entries by default).
  • Keys cold-eye entries by exact (serial_id, version_id), invalidates them on mutation, and fails the request closed on a registry/database miss.
  • Emits cache hit/miss, prefetch capacity/latency, and database-miss metrics.

The prefetch list excludes candidates already consumed by the concurrent known-candidate stage before reservations are created, preventing stale reservations from accumulating across requests.

Validation

  • cargo check -p iris-mpc-bins --bin iris-mpc-linear-scan
  • cargo clippy -p iris-mpc-cpu -p iris-mpc -p iris-mpc-bins --all-targets -- -D warnings
  • Search/cascade tests: 6 passed on this layer.
  • Cold-eye cache/overlay/prefetch tests: 5 passed.
  • DB-backed cold-eye integration test compiles with db_dependent; the deterministic full-server GPU/CPU result comparison and real-server benchmark are documented in Fused mirror scan, packed pair kernel, and lane pipelining for the CPU linear scan #2348.

Review guide

  1. Start with the cache and prefetch state in iris-mpc-cpu/src/execution/hawk_main/iris_worker.rs.
  2. Review candidate scheduling and I/O overlap in iris-mpc-cpu/src/execution/hawk_main/search.rs.
  3. Review the side-only database loading API in iris-mpc-store.
  4. Finish with persistence acknowledgement in iris-mpc/src/server/mod.rs and the cache metrics/configuration.

Stack

philsippl and others added 5 commits August 19, 2026 15:26
Prefetch reservations
- Reservations are single-use and idempotent instead of use-counted. The
  same record was hinted by the known-candidate loop, by every first-eye
  chunk that matched it (reauth targets always do), and by both
  orientations, while stage two fetched it once after deduplication, so
  each reauth leaked a Ready entry until the 4096-record cap disabled
  prefetch for the rest of the process. The first fetch now consumes the
  reservation and promotes the value into the LFU, where a second consumer
  finds it.
- The actor releases leftover reservations once a batch has finished all
  of its scans (IrisWorkerPool::release_prefetched), so reservations never
  outlive the batch that made them, including on error paths.
- A failed prefetch read is logged and counted rather than latched for the
  next barrier. Its reservations are released, so the foreground fetch
  repeats the read and is the path that fails closed on a missing exact
  version; latching could fail a batch for a transient read the foreground
  would have served, or attribute an abandoned scan's error to the next
  batch. The db-backed test now asserts that contract.
- Count prefetch hits under their own metric instead of as LFU misses.

Rolling LUC window
- A non-dense startup window keeps its newest contiguous run, an empty
  window starts at the first serial ID it sees, and a skipped serial ID
  restarts the window, all with a warning/metric instead of a panic; the
  window is a cache in front of the exact-version overlay, LFU, and
  database. Startup missing-row check uses a set instead of O(n^2).

Configuration
- Reject disable_persistence in linear-scan mode: the cold-eye mutation
  overlay is only released by result persistence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAbAnQyfsSrWH6UGcRM1FF
dkales
dkales previously approved these changes Aug 21, 2026
Comment on lines +472 to +478
if search_mode == HawkSearchMode::LinearScan && disable_persistence {
bail!(
"exact CPU linear-scan mode requires persistence: the database-backed eye's \
mutation overlay is released only when results are persisted \
(SMPC__DISABLE_PERSISTENCE=false)"
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not a blocker for a potential shadow mode?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh great callout, @gryaele will it actually be deployed without persistence?

@gryaele gryaele Aug 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, we need to deploy it with persistence

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will run in shadow mode with hnsw db.


let mut db_size: usize = 0;
let mut cold_storage: Option<(Store, usize)> = None;
let mut cold_storage: Option<(Store, usize, usize, usize)> = None;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this could be a struct by now

A fresh Cargo.lock hash makes the release build and tests consume nearly
the whole 20-minute budget; the post-job cache upload is then cancelled,
so every subsequent run is cold again and fails the same way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAbAnQyfsSrWH6UGcRM1FF
A broad-matching query (prod batch 201: ~3.7k discovered candidates,
22.8s compute) exposed three compounding cold-eye weaknesses:

- Discovery enqueues one tiny prefetch command per full-scan chunk; with
  thousands of hits the 64-deep queue dropped most commands and the rest
  became single-row round trips, so almost nothing was prefetched. The
  worker now drains and merges everything queued into one reservation
  pass and one batched read per database round trip, and the queue holds
  4096 commands. Barriers drained mid-merge complete after that read,
  preserving their ordering contract.
- The candidates then stalled the scan on one giant foreground
  `id = ANY(...)` statement (~38 KiB TOASTed rows; Postgres logged
  multi-second slow statements). Large fetches, foreground and prefetch
  alike, are now split into 512-row sub-batches issued concurrently.
- That foreground read had no metric, which is why dashboards showed
  "DB time is short" while Postgres disagreed. It now records
  linear_scan_cold_foreground_db_duration/_batch_size/_records_total,
  and the worker records the coalesced batch size.

Also raise the cold-eye LFU capacity to 12288 records (~460 MiB, three
times the previous size) so recurring broad-matcher candidates stay
resident between requests instead of being refetched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants