-
Notifications
You must be signed in to change notification settings - Fork 25
Fused mirror scan, packed pair kernel, and lane pipelining for the CPU linear scan #2348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
philsippl
wants to merge
8
commits into
codex/cpu-linear-scan-cold-eye-cache
from
codex/cpu-linear-scan-fused-mirror
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b56b88b
perf(linear-scan): add compute and pipeline optimizations
philsippl 8ef678f
Prevent stale cold-eye prefetch reservations
philsippl c476115
Keep ground-truth scheduling consistent
philsippl 16198cd
Merge codex/cpu-linear-scan-cold-eye-cache into fused mirror scan
philsippl 6294c90
Address fused-scan review findings
philsippl 05a07ce
Assert shared worker pool in the paired linear scan
philsippl 8639fa0
Merge codex/cpu-linear-scan-cold-eye-cache into fused mirror scan
philsippl cca4fbd
Refuse windowed dot products on mixed-plane residents
philsippl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| use iris_mpc_common::VectorId; | ||
| use std::{ | ||
| collections::HashSet, | ||
| env, | ||
| hint::black_box, | ||
| mem::size_of, | ||
| time::{Duration, Instant}, | ||
| }; | ||
|
|
||
| fn env_usize(name: &str, default: usize) -> usize { | ||
| env::var(name) | ||
| .ok() | ||
| .and_then(|value| value.parse().ok()) | ||
| .unwrap_or(default) | ||
| } | ||
|
|
||
| fn main() { | ||
| let records = env_usize("IRIS_MPC_CANDIDATE_BENCH_RECORDS", 2_000_000); | ||
| let candidate_count = env_usize("IRIS_MPC_CANDIDATE_BENCH_CANDIDATES", 128); | ||
| let repetitions = env_usize("IRIS_MPC_CANDIDATE_BENCH_REPETITIONS", 5); | ||
| assert!(records > 0 && records <= u32::MAX as usize); | ||
| assert!(repetitions > 0); | ||
|
|
||
| let live_ids = (0..records) | ||
| .map(|index| VectorId::from_0_index(index as u32)) | ||
| .collect::<Vec<_>>(); | ||
| let candidates = (0..candidate_count) | ||
| .map(|index| { | ||
| let record = index * records / candidate_count.max(1); | ||
| VectorId::from_0_index(record.min(records - 1) as u32) | ||
| }) | ||
| .collect::<Vec<_>>(); | ||
|
|
||
| let mut hash_build = Duration::ZERO; | ||
| let mut hash_lookup = Duration::ZERO; | ||
| let mut hash_capacity = 0usize; | ||
| for _ in 0..repetitions { | ||
| let started = Instant::now(); | ||
| let live_ids_set = live_ids.iter().copied().collect::<HashSet<_>>(); | ||
| hash_build += started.elapsed(); | ||
| hash_capacity = live_ids_set.capacity(); | ||
|
|
||
| let started = Instant::now(); | ||
| let found = candidates | ||
| .iter() | ||
| .filter(|id| live_ids_set.contains(id)) | ||
| .count(); | ||
| hash_lookup += started.elapsed(); | ||
| black_box(found); | ||
| } | ||
|
|
||
| let mut binary_lookup = Duration::ZERO; | ||
| for _ in 0..repetitions { | ||
| let started = Instant::now(); | ||
| let found = candidates | ||
| .iter() | ||
| .filter(|id| live_ids.binary_search(id).is_ok()) | ||
| .count(); | ||
| binary_lookup += started.elapsed(); | ||
| black_box(found); | ||
| } | ||
|
|
||
| let divisor = repetitions as f64; | ||
| println!( | ||
| "LINEAR_SCAN_CANDIDATE_RESULT records={records} candidates={} repetitions={repetitions} \ | ||
| vector_id_bytes={} hash_capacity={hash_capacity} hash_table_lower_bound_bytes={} \ | ||
| hash_build_seconds={:.9} hash_lookup_seconds={:.9} binary_lookup_seconds={:.9}", | ||
| candidates.len(), | ||
| size_of::<VectorId>(), | ||
| hash_capacity * size_of::<VectorId>(), | ||
| hash_build.as_secs_f64() / divisor, | ||
| hash_lookup.as_secs_f64() / divisor, | ||
| binary_lookup.as_secs_f64() / divisor, | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| use eyre::{ensure, Result}; | ||
| use iris_mpc_common::{VectorId, IRIS_CODE_LENGTH, MASK_CODE_LENGTH, ROTATIONS}; | ||
| use iris_mpc_cpu::{ | ||
| execution::hawk_main::iris_worker::{ | ||
| init_workers, IrisWorkerPool, LocalIrisWorkerPool, QueryId, QuerySpec, | ||
| }, | ||
| hawkers::{aby3::aby3_store::DistanceMode, shared_irises::SharedIrises}, | ||
| protocol::shared_iris::{ArcIris, GaloisRingSharedIris}, | ||
| }; | ||
| use rayon::prelude::*; | ||
| use std::{ | ||
| collections::HashMap, | ||
| env, | ||
| hint::black_box, | ||
| sync::Arc, | ||
| time::{Duration, Instant}, | ||
| }; | ||
|
|
||
| const DEFAULT_DB_SIZE: usize = 1_572_864; | ||
| const DEFAULT_WARMUP_RUNS: usize = 1; | ||
| const DEFAULT_MEASURED_RUNS: usize = 3; | ||
| const DEFAULT_TOKIO_CORES: usize = 8; | ||
|
|
||
| fn env_usize(name: &str, default: usize) -> Result<usize> { | ||
| match env::var(name) { | ||
| Ok(value) => Ok(value.parse()?), | ||
| Err(env::VarError::NotPresent) => Ok(default), | ||
| Err(err) => Err(err.into()), | ||
| } | ||
| } | ||
|
|
||
| fn build_pool(db_size: usize, numa_shard: usize) -> (LocalIrisWorkerPool, Vec<VectorId>, ArcIris) { | ||
| let query = Arc::new(GaloisRingSharedIris::default_for_party(0)); | ||
| println!( | ||
| "BENCH_LOADING db_size={db_size} payload_gib={:.3}", | ||
| db_size as f64 * 2.0 * (IRIS_CODE_LENGTH + MASK_CODE_LENGTH) as f64 | ||
| / (1024.0 * 1024.0 * 1024.0), | ||
| ); | ||
| let load_started = Instant::now(); | ||
|
|
||
| // Deep-clone every record so the scan reads a production-sized working | ||
| // set instead of repeatedly hitting one shared cache-resident allocation. | ||
| let irises = (0..db_size) | ||
| .into_par_iter() | ||
| .map(|_| Arc::new((*query).clone())) | ||
| .collect::<Vec<_>>(); | ||
| let layout = iris_mpc_cpu::protocol::shared_iris::preferred_scan_layout(); | ||
| let mut store = SharedIrises::new( | ||
| HashMap::new(), | ||
| iris_mpc_cpu::protocol::shared_iris::ResidentIris::from_arc( | ||
| Arc::new(GaloisRingSharedIris::default_for_party(0)), | ||
| layout, | ||
| ), | ||
| ); | ||
| store.reserve(db_size); | ||
| let mut vector_ids = Vec::with_capacity(db_size); | ||
| for iris in irises { | ||
| vector_ids.push( | ||
| store.append(iris_mpc_cpu::protocol::shared_iris::ResidentIris::from_arc( | ||
| iris, layout, | ||
| )), | ||
| ); | ||
| } | ||
| println!( | ||
| "BENCH_LOADED seconds={:.3}", | ||
| load_started.elapsed().as_secs_f64() | ||
| ); | ||
|
|
||
| let store = store.to_arc(); | ||
| let workers = init_workers(numa_shard, store.clone(), true, layout); | ||
| ( | ||
| LocalIrisWorkerPool::new(workers, store, layout, DistanceMode::MinRotation, 0), | ||
| vector_ids, | ||
| query, | ||
| ) | ||
| } | ||
|
|
||
| fn median(samples: &[Duration]) -> Duration { | ||
| let mut sorted = samples.to_vec(); | ||
| sorted.sort_unstable(); | ||
| sorted[sorted.len() / 2] | ||
| } | ||
|
|
||
| fn report(backend: &str, db_size: usize, orientations: usize, samples: &[Duration]) { | ||
| let elapsed = median(samples).as_secs_f64(); | ||
| let comparisons = db_size * orientations; | ||
| println!( | ||
| "BENCH_RESULT backend={backend} db_size={db_size} orientations={orientations} \ | ||
| median_seconds={elapsed:.6} logical_comparisons_per_second={:.3} \ | ||
| full_scan_equivalent_seconds={:.6}", | ||
| comparisons as f64 / elapsed, | ||
| elapsed, | ||
| ); | ||
| } | ||
|
|
||
| fn main() -> Result<()> { | ||
| let db_size = env_usize("IRIS_MPC_DOT_BENCH_DB_SIZE", DEFAULT_DB_SIZE)?; | ||
| let numa_shard = env_usize("IRIS_MPC_DOT_BENCH_NUMA_SHARD", 0)?; | ||
| let warmup_runs = env_usize("IRIS_MPC_DOT_BENCH_WARMUP", DEFAULT_WARMUP_RUNS)?; | ||
| let measured_runs = env_usize("IRIS_MPC_DOT_BENCH_RUNS", DEFAULT_MEASURED_RUNS)?; | ||
| let tokio_cores = env_usize("IRIS_MPC_DOT_BENCH_TOKIO_CORES", DEFAULT_TOKIO_CORES)?; | ||
| let tokio_cores = (tokio_cores > 0).then_some(tokio_cores); | ||
| ensure!(db_size > 0, "DB size must be positive"); | ||
| ensure!(measured_runs > 0, "measured run count must be positive"); | ||
| iris_mpc_common::helpers::numactl::init(tokio_cores); | ||
| println!( | ||
| "BENCH_CONFIG batch_size=1 db_size={db_size} numa_shard={numa_shard} \ | ||
| warmup_runs={warmup_runs} measured_runs={measured_runs} rotations={ROTATIONS} \ | ||
| tokio_cores={} dot_cores={}", | ||
| tokio_cores.unwrap_or_else(iris_mpc_common::helpers::numactl::get_tokio_worker_threads), | ||
| iris_mpc_cpu::execution::hawk_main::iris_worker::select_core_ids(numa_shard).len(), | ||
| ); | ||
|
|
||
| let (pool, vector_ids, query) = build_pool(db_size, numa_shard); | ||
| iris_mpc_common::helpers::numactl::restrict_tokio_runtime(); | ||
| let runtime = tokio::runtime::Builder::new_multi_thread() | ||
| .worker_threads(iris_mpc_common::helpers::numactl::get_tokio_worker_threads()) | ||
| .on_thread_start(iris_mpc_common::helpers::numactl::restrict_tokio_runtime) | ||
| .enable_all() | ||
| .build()?; | ||
| let normal_id = QueryId::new(); | ||
| let mirror_id = QueryId::new(); | ||
| runtime.block_on(pool.cache_queries(vec![(normal_id, query.clone()), (mirror_id, query)]))?; | ||
| let normal = QuerySpec::new(normal_id); | ||
| let mirror = QuerySpec::new(mirror_id); | ||
| let expected_len = 2 * ROTATIONS * db_size; | ||
|
|
||
| let run_single = || -> Result<Duration> { | ||
| let started = Instant::now(); | ||
| let output = runtime | ||
| .block_on(pool.compute_dot_products_full_rotations(normal, vector_ids.clone()))?; | ||
| let elapsed = started.elapsed(); | ||
| ensure!(output.len() == expected_len, "unexpected result length"); | ||
| black_box(output); | ||
| Ok(elapsed) | ||
| }; | ||
| for _ in 0..warmup_runs { | ||
| black_box(run_single()?); | ||
| } | ||
| let mut single_samples = Vec::with_capacity(measured_runs); | ||
| for run in 0..measured_runs { | ||
| let elapsed = run_single()?; | ||
| println!( | ||
| "BENCH_SAMPLE backend=cpu_single_orientation run={run} seconds={:.6}", | ||
| elapsed.as_secs_f64() | ||
| ); | ||
| single_samples.push(elapsed); | ||
| } | ||
|
|
||
| let run_mirror = || -> Result<Duration> { | ||
| let started = Instant::now(); | ||
| let (normal_output, mirror_output) = runtime.block_on(async { | ||
| tokio::join!( | ||
| pool.compute_dot_products_full_rotations(normal, vector_ids.clone()), | ||
| pool.compute_dot_products_full_rotations(mirror, vector_ids.clone()), | ||
| ) | ||
| }); | ||
| let normal_output = normal_output?; | ||
| let mirror_output = mirror_output?; | ||
| let elapsed = started.elapsed(); | ||
| ensure!( | ||
| normal_output.len() == expected_len && mirror_output.len() == expected_len, | ||
| "unexpected mirror result length" | ||
| ); | ||
| black_box((normal_output, mirror_output)); | ||
| Ok(elapsed) | ||
| }; | ||
| for _ in 0..warmup_runs { | ||
| black_box(run_mirror()?); | ||
| } | ||
| let mut mirror_samples = Vec::with_capacity(measured_runs); | ||
| for run in 0..measured_runs { | ||
| let elapsed = run_mirror()?; | ||
| println!( | ||
| "BENCH_SAMPLE backend=cpu_normal_and_mirror run={run} seconds={:.6}", | ||
| elapsed.as_secs_f64() | ||
| ); | ||
| mirror_samples.push(elapsed); | ||
| } | ||
|
|
||
| let run_fused = || -> Result<Duration> { | ||
| let started = Instant::now(); | ||
| let [normal_output, mirror_output] = runtime.block_on( | ||
| pool.compute_dot_products_full_rotations_pair([normal, mirror], vector_ids.clone()), | ||
| )?; | ||
| let elapsed = started.elapsed(); | ||
| ensure!( | ||
| normal_output.len() == expected_len && mirror_output.len() == expected_len, | ||
| "unexpected fused result length" | ||
| ); | ||
| black_box((normal_output, mirror_output)); | ||
| Ok(elapsed) | ||
| }; | ||
| for _ in 0..warmup_runs { | ||
| black_box(run_fused()?); | ||
| } | ||
| let mut fused_samples = Vec::with_capacity(measured_runs); | ||
| for run in 0..measured_runs { | ||
| let elapsed = run_fused()?; | ||
| println!( | ||
| "BENCH_SAMPLE backend=cpu_fused_pair run={run} seconds={:.6}", | ||
| elapsed.as_secs_f64() | ||
| ); | ||
| fused_samples.push(elapsed); | ||
| } | ||
|
|
||
| report("cpu_single_orientation", db_size, 1, &single_samples); | ||
| report("cpu_normal_and_mirror", db_size, 2, &mirror_samples); | ||
| report("cpu_fused_pair", db_size, 2, &fused_samples); | ||
| println!( | ||
| "BENCH_MIRROR_RATIO wall_time_ratio={:.3}", | ||
| median(&mirror_samples).as_secs_f64() / median(&single_samples).as_secs_f64() | ||
| ); | ||
| println!( | ||
| "BENCH_FUSED_RATIO wall_time_ratio={:.3}", | ||
| median(&fused_samples).as_secs_f64() / median(&single_samples).as_secs_f64() | ||
| ); | ||
| Ok(()) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.comwithsh, allowing a compromised download to control the CI runner, Docker daemon, build context, and published image.More details about this
The
Install Dockerstep downloadshttps://get.docker.comand immediately executes its response withsh:curl -fsSL https://get.docker.com | shBecause 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, haspackages: write,contents: read,attestations: write, andid-token: writepermissions, and later usesGITHUB_TOKENto publish an image, making the runner a valuable target.A plausible attack is:
get.docker.com.shin theInstall Dockerstep, so it can read the checked-out repository, alter the Docker build context, and inspect available runner credentials or environment data.Build and pushstep, causing a malicious image to be published to${REGISTRY}/${IMAGE_NAME}. It can also tamper with the workflow workspace or wait for the laterdocker/login-actionstep to expose the registry token, then use that credential to push unauthorized packages.sudo usermodandsetfaclcommands 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
Install Dockerrun:step with the pinned Docker setup action:curl,usermod,apt-get, andsetfaclcommands. 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 reasonsAlternatively, 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.