Skip to content

Fused mirror scan, packed pair kernel, and lane pipelining for the CPU linear scan - #2348

Open
philsippl wants to merge 8 commits into
codex/cpu-linear-scan-cold-eye-cachefrom
codex/cpu-linear-scan-fused-mirror
Open

Fused mirror scan, packed pair kernel, and lane pipelining for the CPU linear scan#2348
philsippl wants to merge 8 commits into
codex/cpu-linear-scan-cold-eye-cachefrom
codex/cpu-linear-scan-fused-mirror

Conversation

@philsippl

@philsippl philsippl commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the compute, memory-layout, networking, and scheduling optimizations for the exact CPU linear scan. It is stacked on the correctness baseline (#2347) and cold-eye cache/prefetch layer (#2351); it does not change thresholds, rotations, MPC result semantics, or the public server contract.

Actual server benchmark

Measured with the real iris-mpc-linear-scan service and production client across 3 × AWS r8g.24xlarge: TLS MPC networking, S3/SNS/SQS ingestion through Moto, persistence, normal + mirror scans, batch size 1, 31 rotations, and a 1,048,576-record seeded database.

Build Synchronized comparisons/s Wall/request
Production-complete pre-follow-up build (12 measured requests; one warm-up excluded) 4.983 M/s median (4.950–5.028 M/s) 421 ms median
Intermediate doubled-row/UMMLA build 5.65 M/s 371 ms
This PR, paired/fused pipeline (tokio=14) 6.33 M/s 331.5 ms
Plus AMPC threshold-buffer reuse (companion revision) 6.47 M/s 324.5 ms

“Logical comparisons” counts normal and mirror work once rather than once per MPC party. In the 4.983 M/s run, median harness end-to-end throughput was 1.492 M comparisons/s because Moto SNS/SQS fan-out added about 985 ms; synchronized scan time is the production compute/network measurement. At 6.33 M/s, an 18M-record normal + mirror scan extrapolates to about 5.7 seconds, excluding ingress and persistence.

How it works

  • Stores the resident eye in a contiguous ARM mixed-plane layout and scans 4,096-record chunks across all 31 rotations.
  • Represents each query with doubled rows, turning rotations into aligned windows and reducing the prerotated query footprint from about 1.19 MiB to 153.6 KiB.
  • Uses NEON/UMMLA cross-packed pair kernels, including padded-tail handling and byte-identical scalar/reference parity checks.
  • Evaluates normal and mirror orientations while streaming the resident eye once; the per-orientation MPC sessions and transcripts remain independent.
  • Overlaps dot-product work with threshold MPC through bounded per-lane and global lookahead queues, and uses multiple physical TLS flows to avoid AWS's per-flow bandwidth ceiling.
  • Preserves the Add cold-eye cache and prefetch for CPU linear scan #2351 rolling LUC window, mutation overlay, asynchronous cold-eye prefetch, and TinyLFU cache. Known candidates are excluded before prefetch reservation, preventing unconsumed slots from accumulating.
  • Emits per-eye comparisons/s, scan duration, candidate fraction, cache/prefetch, and networking metrics suitable for comparison with the server benchmark.

Capacity estimate: the resident mixed-plane eye costs about 38.55 kB per record, including container, allocator, registry, and 20% store-reserve overhead. The 501-record rolling LUC window, 4,096-entry TinyLFU, and one 4,096-record prefetch chunk add about 0.32 GiB. On an r8g.24xlarge with the configured 700 GiB pod limit, the recommended operational maximum is 18–18.5M records (about 646.5–664.5 GiB); the roughly 19.49M mathematical ceiling leaves no safe runtime margin.

Deployment

  1. Merge/publish worldcoin/ampc-common#138, keep all four AMPC pins on the same revision, and build ghcr.io/worldcoin/iris-mpc-linear-scan using .github/workflows/build-and-push-linear-scan-server.yaml. Pin production to an immutable image digest.
  2. Provision one dedicated on-demand r8g.24xlarge per party and use the supplied stage/prod common values plus party overlays. They request 92 CPUs without a CPU limit and select/tolerate the dedicated linear-scan nodes.
  3. Apply deploy/aws/r8g-24xlarge-linear-scan-user-data.mime through the node class, launch template, or baked AMI. It keeps ENA interrupts on CPUs 0–10 and reserves CPUs 11–95 for dot-product workers; verify IRQ placement under load.
  4. Preserve hostNetwork: true, allow direct cross-party TCP ports 4000–4002, and keep 16 physical connections so traffic is not capped by a single AWS 5-Gbit/s flow.
  5. Verify SMPC__MAX_BATCH_SIZE=1, the same SMPC__FULL_SCAN_SIDE on every party, SMPC__FULL_SCAN_SIDE_SWITCHING_ENABLED=false, all three /ready endpoints, a real request, and the scan/cache metrics.
  6. Drain/stop all three GPU consumers first, then deploy all three CPU parties together with Recreate. Never run mixed images or both queue consumers concurrently. Alternate the resident eye only by changing Left/Right on all parties and performing another coordinated restart.

Review guide

  1. Review paired cascade scheduling and bounded lookahead in iris-mpc-cpu/src/execution/hawk_main/search.rs.
  2. Review resident layout and cache/prefetch interaction in worker_pool_initializer.rs and iris_worker.rs.
  3. Review doubled-query and UMMLA kernels in iris-mpc-cpu/src/protocol/ops.rs plus fused MPC primitives in ampc-common.
  4. Review server/network tuning and the real-server benchmark tooling.
  5. Finish with packed-kernel parity, paired-vs-independent cascade, GPU parity, cold-eye, and deterministic full-server tests.

Stack

@philsippl
philsippl requested a review from a team as a code owner August 19, 2026 11:19
@philsippl
philsippl force-pushed the codex/cpu-linear-scan-fused-mirror branch from 5e2b019 to 8ef678f Compare August 19, 2026 11:20
@philsippl
philsippl changed the base branch from codex/cpu-linear-scan to codex/cpu-linear-scan-cold-eye-cache August 19, 2026 11:21
Comment on lines +35 to 40
run: |
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
sudo apt-get install acl
sudo setfacl --modify user:$USER:rw /var/run/docker.sock

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.

Semgrep identified an issue in your code:

The release workflow executes the live response from get.docker.com with sh, allowing a compromised download to control the CI runner, Docker daemon, build context, and published image.

More details about this

The Install Docker step downloads https://get.docker.com and immediately executes its response with sh:

curl -fsSL https://get.docker.com | sh

Because the downloaded bytes are executed before they are inspected, anyone who can compromise get.docker.com, its delivery path, or the content returned to this runner can run arbitrary commands as the GitHub Actions runner. This job runs on an arm64 Ubuntu runner for releases, has packages: write, contents: read, attestations: write, and id-token: write permissions, and later uses GITHUB_TOKEN to publish an image, making the runner a valuable target.

A plausible attack is:

  1. An attacker compromises the Docker install endpoint or causes the runner to receive a modified response for get.docker.com.
  2. The modified response is consumed by sh in the Install Docker step, so it can read the checked-out repository, alter the Docker build context, and inspect available runner credentials or environment data.
  3. The script can replace files used by the later Build and push step, causing a malicious image to be published to ${REGISTRY}/${IMAGE_NAME}. It can also tamper with the workflow workspace or wait for the later docker/login-action step to expose the registry token, then use that credential to push unauthorized packages.
  4. The subsequent sudo usermod and setfacl commands give the current user access to /var/run/docker.sock; code executed by the downloaded script can therefore control the Docker daemon and start privileged containers on the runner.

To resolve this comment:

✨ Commit fix suggestion
  1. Replace the Install Docker run: step with the pinned Docker setup action:
    - name: Install Docker
      uses: docker/setup-docker-action@e43656e248c0bd0647d3f5c195d116aacf6fcaf4
      with:
        version: v29.7.2
  2. Remove the curl, usermod, apt-get, and setfacl commands. The setup action installs Docker without piping remote content directly into a shell and configures it for subsequent workflow steps.
💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by gha-curl-pipe-shell.

You can view more details about this finding in the Semgrep AppSec Platform.

philsippl and others added 6 commits August 19, 2026 14:57
- Port the GPU linear-scan bench to the resident-layout pool constructor;
  it still called the pre-layout LocalIrisWorkerPool::new_local and did
  not compile under --all-features --all-targets.
- Restore the empty-request early return in search() and its test; the
  identity-update no-match search runs every batch and does not need a
  session per eye (the linear-scan no-match shortcut in per_session is
  back through the merge).
- Lookahead dot tasks abort when their handle is dropped, so a lane error
  or a sibling lane failing try_join! cancels the in-flight chunk instead
  of leaving it running on the dot workers; the distance-mode and center
  rotation checks are made before spawning rather than inside the task.
- Port the restored db-backed cold-eye test to the layout-aware pool API.

Restored through the merge of the updated base: LinearScan-only gating of
the full_face_mirror_* uniqueness result fields (HNSW keeps serializing
null) and the dead linear-scan branch removal in per_session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAbAnQyfsSrWH6UGcRM1FF
The fused dot pass dispatches both orientations' queries through the
normal-orientation store. That resolves the mirror query only because
every session of an eye shares that eye's worker pool, into which
HawkRequest::cache_into caches normal and mirror queries alike. Check the
invariant per lane instead of relying on it silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAbAnQyfsSrWH6UGcRM1FF
The HNSW-style windowed dot products read resident records through
ResidentIris::to_arc, which rebuilds a u16 iris per target when the
resident layout is the mixed-plane scan layout. The exact scan streams
planes directly and never takes that path, so production pools now refuse
it instead of running a silent per-target reconstruction if a future
caller reaches it. The cross-kernel parity tests that compare both kernels
on the same mixed-plane data opt in explicitly.

Test actors pick the resident layout by search mode exactly like the
server does (HNSW keeps u16, the exact scan uses the CPU's scan layout),
through a shared HawkActor::resident_layout_for.

Also take the cached live VectorId list from the registry in the paired
cascade, matching the single-orientation path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAbAnQyfsSrWH6UGcRM1FF
Comment on lines +138 to +139
/// `id` of the mask share (party_id + 1), preserved for reconstruction.
mask_id: usize,

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.

This is never out of sync or am I missing something? why are we storing this twice?

/// A spawned task handle that aborts the task when dropped, so a lookahead
/// chunk cannot keep running detached after the lane that requested it has
/// failed or been cancelled.
pub struct AbortOnDropHandle<T>(tokio::task::JoinHandle<T>);

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.

}
}

#[cfg(target_arch = "aarch64")]

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.

These will not run in the CI pipeline I guess?

/// independent dot products.
#[cfg(target_arch = "aarch64")]
#[inline(always)]
fn dot_product_1x4_u16(query: &[u16], targets: [&[u16]; 4]) -> [u16; 4] {

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.

These could all be one const-generic function, no? I would hope the compiler can unroll this and produce equivalent code.

let mut store = iris_store.data.write().await;
for (vector_id, iris) in resolved {
store.insert(vector_id, iris);
store.insert(vector_id, ResidentIris::from_arc(iris, layout));

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.

The from_arc does the conversion under the write lock here. I guess this is not really that much of an issue though

row: usize,
rotation: usize,
) -> *const u8 {
let amount = PrerotatedQueryRowMajorView::<ROTATIONS>::ROTATION_AMOUNTS[rotation];

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.

This could use a const assert that this is a multiple of 4.

}
target_idx += 4;
} else {
// Missing vectors are uncommon in a full scan. Preserve their

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.

I guess this is true, but maybe this is worth a metric emmission since this probably tanks performance if it happens often.

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.

2 participants