diff --git a/bench/check_diamond_io_csv_logs.py b/bench/check_diamond_io_csv_logs.py new file mode 100755 index 00000000..0e26d592 --- /dev/null +++ b/bench/check_diamond_io_csv_logs.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +"""Check DiamondIO CSV rows against their simulation and benchmark logs.""" + +from __future__ import annotations + +import argparse +import csv +import re +import sys +from decimal import Decimal, InvalidOperation +from pathlib import Path + + +BENCHMARK_FIELDS = [ + "obfuscate_latency", + "obfuscate_total_time_nanos", + "obfuscate_max_parallelism", + "eval_latency", + "eval_total_time_nanos", + "eval_max_parallelism", + "obfuscate_input_injection_latency_percent", + "obfuscate_input_injection_total_time_percent", + "eval_input_injection_latency_percent", + "eval_input_injection_total_time_percent", + "obfuscated_circuit_bytes", + "input_injection_bytes", +] + +CONFIG_CHECKS = [ + ("ring_dim_config", "ring_dim"), + ("min_log_ring_dim", "min_log_ring_dim"), + ("max_log_ring_dim", "max_log_ring_dim"), + ("input_size", "input_size"), + ("injector_batch_bits", "injector_batch_bits"), + ("output_size", "output_size"), + ("crt_bits", "crt_bits"), + ("base_bits", "base_bits"), + ("p_moduli_bits", "p_moduli_bits"), + ("max_unreduced_muls", "max_unreduced_muls"), + ("scale", "scale"), + ("min_crt_depth", "min_crt_depth"), + ("max_crt_depth", "max_crt_depth"), + ("security_bits", "security_bits"), + ("noise_refresh_cbd_n", "noise_refresh_cbd_n"), + ("error_sigma", "error_sigma"), + ("trapdoor_sigma", "trapdoor_sigma"), + ("d_secret", "d_secret"), +] + +SELECTED_CHECKS = [ + ("selected_crt_depth", "crt_depth"), + ("selected_log_ring_dim", "log_ring_dim"), + ("selected_ring_dim", "ring_dim"), + ("achieved_secpar_for_gauss_in_run", "achieved_secpar_for_gauss"), + ("achieved_secpar_for_cbd_in_run", "achieved_secpar_for_cbd"), + ("prf_mask_output_coeff_bits", "prf_mask_output_coeff_bits"), + ("noise_refresh_v_bits", "noise_refresh_v_bits"), + ("final_seed_bits", "final_seed_bits"), + ("noisy_plaintext_error_bits", "noisy_plaintext_error_bits"), + ("input_injection_error_bits", "input_injection_error_bits"), +] + +ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") +CONFIG_RE = re.compile(r"DiamondIOGpuBenchConfig \{([^}]*)\}") +KV_RE = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)=([^\s]+)") + + +def strip_ansi(text: str) -> str: + return ANSI_RE.sub("", text) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Check DiamondIO simulation and benchmark-estimation CSV values against logs." + ) + parser.add_argument( + "csv_path", + nargs="?", + default="bench/security_bits_100_diamond_io_simulation_parameters.csv", + help="CSV file to check", + ) + return parser.parse_args() + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[1] + + +def resolve_log_path(raw_path: str, root: Path) -> Path: + path = Path(raw_path) + if path.exists(): + return path + + workspace_prefix = "/workspace/logs/" + if raw_path.startswith(workspace_prefix): + local_path = root / "logs" / raw_path[len(workspace_prefix) :] + if local_path.exists(): + return local_path + + matches = sorted((root / "logs").rglob(path.name)) if (root / "logs").exists() else [] + if len(matches) == 1: + return matches[0] + + return path + + +def read_log(raw_path: str, root: Path) -> tuple[Path, str]: + path = resolve_log_path(raw_path, root) + if not path.exists(): + raise ValueError(f"log file not found: {raw_path}") + return path, strip_ansi(path.read_text(errors="replace")) + + +def last_line_with(text: str, needle: str) -> str: + matches = [line for line in text.splitlines() if needle in line] + if not matches: + raise ValueError(f"missing log line containing {needle!r}") + return matches[-1] + + +def parse_config(line: str) -> dict[str, str]: + match = CONFIG_RE.search(line) + if not match: + raise ValueError("missing DiamondIOGpuBenchConfig payload") + result: dict[str, str] = {} + for item in match.group(1).split(","): + key, value = item.strip().split(":", 1) + result[key.strip()] = value.strip() + return result + + +def parse_kv_line(line: str) -> dict[str, str]: + return {key: value.strip('"') for key, value in KV_RE.findall(line)} + + +def is_decimal(value: str) -> bool: + try: + Decimal(value) + return True + except (InvalidOperation, ValueError): + return False + + +def values_match(csv_value: str, log_value: str) -> bool: + left = csv_value.strip().strip('"') + right = log_value.strip().strip('"') + if left == "" or right == "": + return left == right + if left.lower() in {"true", "false"} or right.lower() in {"true", "false"}: + return left.lower() == right.lower() + if is_decimal(left) and is_decimal(right): + return Decimal(left) == Decimal(right) + return left == right + + +def require_match( + errors: list[str], + data_no: str, + source: str, + column: str, + csv_value: str, + log_key: str, + log_values: dict[str, str], +) -> None: + if log_key not in log_values: + if csv_value == "": + return + errors.append(f"row {data_no} {source}: log key {log_key!r} missing, CSV {column}={csv_value!r}") + return + + log_value = log_values[log_key] + if not values_match(csv_value, log_value): + errors.append( + f"row {data_no} {source}: {column}={csv_value!r} does not match " + f"log {log_key}={log_value!r}" + ) + + +def check_simulation(row: dict[str, str], root: Path, errors: list[str]) -> int: + data_no = row["data_no"] + _, text = read_log(row["simulation_log_file"], root) + checks = 0 + + config = parse_config(last_line_with(text, "DiamondIO GPU bench config:")) + for csv_column, log_key in CONFIG_CHECKS: + require_match(errors, data_no, "simulation config", csv_column, row[csv_column], log_key, config) + checks += 1 + require_match(errors, data_no, "simulation config", "bench_iterations", row["bench_iterations"], "bench_iterations", config) + require_match(errors, data_no, "simulation config", "search_only", row["search_only"], "search_only", config) + checks += 2 + + selected = parse_kv_line(last_line_with(text, "DiamondIO CRT-depth search selected parameters")) + for csv_column, log_key in SELECTED_CHECKS: + require_match(errors, data_no, "simulation selected", csv_column, row[csv_column], log_key, selected) + checks += 1 + + return checks + + +def check_benchmark(row: dict[str, str], root: Path, errors: list[str]) -> int: + data_no = row["data_no"] + _, text = read_log(row["benchmark_estimation_log_file"], root) + checks = 0 + + config = parse_config(last_line_with(text, "DiamondIO GPU bench config:")) + for csv_column, log_key in CONFIG_CHECKS: + require_match( + errors, + data_no, + "benchmark config", + csv_column, + row[csv_column], + log_key, + config, + ) + checks += 1 + require_match( + errors, + data_no, + "benchmark config", + "benchmark_estimation_bench_iterations", + row["benchmark_estimation_bench_iterations"], + "bench_iterations", + config, + ) + require_match(errors, data_no, "benchmark config", "expected_search_only", "false", "search_only", config) + checks += 2 + + selected = parse_kv_line( + last_line_with(text, "DiamondIO selected simulation parameters provided; skipping error simulation") + ) + for csv_column, log_key in SELECTED_CHECKS: + if csv_column.startswith("achieved_secpar_"): + continue + require_match(errors, data_no, "benchmark selected", csv_column, row[csv_column], log_key, selected) + checks += 1 + + estimate = parse_kv_line(last_line_with(text, "DiamondIO GPU benchmark estimate")) + for field in BENCHMARK_FIELDS: + csv_column = f"benchmark_estimation_{field}" + require_match(errors, data_no, "benchmark estimate", csv_column, row[csv_column], field, estimate) + checks += 1 + + return checks + + +def main() -> int: + args = parse_args() + root = repo_root() + csv_path = Path(args.csv_path) + if not csv_path.is_absolute(): + csv_path = root / csv_path + + with csv_path.open(newline="") as f: + rows = list(csv.DictReader(f)) + + errors: list[str] = [] + check_count = 0 + for row in rows: + try: + check_count += check_simulation(row, root, errors) + check_count += check_benchmark(row, root, errors) + except ValueError as exc: + errors.append(f"row {row.get('data_no', '')}: {exc}") + + if errors: + print("CSV/log consistency check failed:", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + return 1 + + print(f"CSV/log consistency check ok: {len(rows)} rows, {check_count} field checks") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bench/security_bits_100_diamond_io_simulation_parameters.csv b/bench/security_bits_100_diamond_io_simulation_parameters.csv new file mode 100644 index 00000000..3fe5018a --- /dev/null +++ b/bench/security_bits_100_diamond_io_simulation_parameters.csv @@ -0,0 +1,5 @@ +data_no,protocol,implementation,test_target,security_bits,input_count,input_size,injector_batch_bits,output_size,ring_dim_config,min_log_ring_dim,max_log_ring_dim,selected_log_ring_dim,selected_ring_dim,min_crt_depth,max_crt_depth,selected_crt_depth,crt_bits,base_bits,p_moduli_bits,max_unreduced_muls,scale,noise_refresh_cbd_n,error_sigma,trapdoor_sigma,d_secret,bench_iterations,search_only,lattice_estimator_check,lattice_reference_gauss_secpar,lattice_reference_cbd_secpar,lattice_reference_log_file,max_prf_mask_output_coeff_bits,achieved_secpar_for_gauss_in_run,achieved_secpar_for_cbd_in_run,prf_mask_output_coeff_bits,noise_refresh_v_bits,final_seed_bits,noisy_plaintext_error_bits,input_injection_error_bits,git_commit,git_worktree_dirty,simulation_log_file,benchmark_estimation_git_commit,benchmark_estimation_bench_iterations,benchmark_estimation_log_file,benchmark_estimation_obfuscate_latency,benchmark_estimation_obfuscate_total_time_nanos,benchmark_estimation_obfuscate_max_parallelism,benchmark_estimation_eval_latency,benchmark_estimation_eval_total_time_nanos,benchmark_estimation_eval_max_parallelism,benchmark_estimation_obfuscate_input_injection_latency_percent,benchmark_estimation_obfuscate_input_injection_total_time_percent,benchmark_estimation_eval_input_injection_latency_percent,benchmark_estimation_eval_input_injection_total_time_percent,benchmark_estimation_obfuscated_circuit_bytes,benchmark_estimation_input_injection_bytes +1,DiamondIO,tests/test_gpu_diamond_io.rs,test_gpu_diamond_io_error_search_and_bench_estimate,100,10,100,10,6,65536,15,17,16,65536,45,55,53,28,14,7,4,256,2,4.0,4.578,1,1,true,measured,120,120,/home/sora/.codex/worktrees/cedf/mxx/logs/test_gpu_diamond_io_input100_batch10_crt45_55_log15_17_sec100_sigma4p0_cbd2_release.log,1540,120,120,1474,756,149531758,1274,512,7bab63d0dcb8e75cd41cb9a140f49b19fb0a093f,true,/home/sora/.codex/worktrees/cedf/mxx/logs/test_gpu_diamond_io_input100_batch10_crt45_55_log15_17_sec100_sigma4p0_cbd2_release.log,d5ade121f9f09eafa4faa2f9cae1bb6e8dfbb7d8,5,/workspace/logs/diamond_io_sec100_h200_iter5_d5ade121_20260517T070013JST/mxx-h200-diamond-io-sec100-est-20260517_1gpu_d5ade121f9f0_20260517T070013JST_diamond_io_row1.log,6116.0162626537995,4147712288836272546518676036861137818135063392704860629750,24170928207324098131482568296628771565212991810,2081.6366215446014,350048464264823037952904676103997678563435289528,10760822874598810008199153238566502400,0.8956872261884774,1.0219173976262904e-57,2.6697915314711547,9.47236296456918e-55,7485461781551070670753632832559405143329441533839620640,718912573794748064 +2,DiamondIO,tests/test_gpu_diamond_io.rs,test_gpu_diamond_io_error_search_and_bench_estimate,100,8,80,10,6,65536,15,16,16,65536,45,53,50,28,14,7,4,256,2,4.0,4.578,1,1,true,measured,127,126,/home/sora/.codex/worktrees/cedf/mxx/logs/test_gpu_diamond_io_input80_batch10_crt45_53_log15_16_sec100_sigma4p0_cbd2_release.log,1484,127,126,1391,1364,209717927,1180,419,7bab63d0dcb8e75cd41cb9a140f49b19fb0a093f,true,/home/sora/.codex/worktrees/cedf/mxx/logs/test_gpu_diamond_io_input80_batch10_crt45_53_log15_16_sec100_sigma4p0_cbd2_release.log,d5ade121f9f09eafa4faa2f9cae1bb6e8dfbb7d8,5,/workspace/logs/diamond_io_sec100_h200_iter5_d5ade121_20260517T070013JST/mxx-h200-diamond-io-sec100-est-20260517_1gpu_d5ade121f9f0_20260517T070013JST_diamond_io_row2.log,4349.6224037724,3836841739399026528152604705114678011389011875781363121354,32587915154499688797348521447948292278419194160,1468.1229864235997,341613749629540640175700214725577743401185388204,14508039569129696930725625856000000000,1.1022438788346078,2.9360011137019488e-58,2.717653758176907,2.7019852806084382e-55,7624833700145137182585102700577477768380423660987821856,397407508418856352 +3,DiamondIO,tests/test_gpu_diamond_io.rs,test_gpu_diamond_io_error_search_and_bench_estimate,100,11,110,10,6,65536,16,16,16,65536,53,57,55,28,14,7,4,256,2,4.0,4.578,1,1,true,skipped_explicit_log_ring_dim,114,113,/home/sora/.codex/worktrees/cedf/mxx/logs/test_gpu_diamond_io_cbd_failure_signal_input110_crt55_log16_17_sec100_sigma4p0_cbd2_release_sage.log,1596,,,1529,1502,257431460,1322,559,7bab63d0dcb8e75cd41cb9a140f49b19fb0a093f,true,/home/sora/.codex/worktrees/cedf/mxx/logs/test_gpu_diamond_io_input110_batch10_crt53_57_log16_skip_lattice_sec100_sigma4p0_cbd2_release.log,d5ade121f9f09eafa4faa2f9cae1bb6e8dfbb7d8,5,/workspace/logs/diamond_io_sec100_h200_iter5_d5ade121_20260517T070013JST/mxx-h200-diamond-io-sec100-est-20260517_1gpu_d5ade121f9f0_20260517T070013JST_diamond_io_row3.log,7399.590372858398,11930376493455871470406567902825851008219256031267012708341,57793043386862822091653196282952132383198646606,2410.690358383399,981775308414456655348040561656065723072848751182,25729285111001160633634326891724800000,0.7935629742503815,4.696228574913229e-58,2.716362625862607,8.992567525973156e-55,20415755416907562105218484551666158918758500258495823776,961347918662682400 +4,DiamondIO,tests/test_gpu_diamond_io.rs,test_gpu_diamond_io_error_search_and_bench_estimate,100,9,90,10,6,65536,16,16,16,65536,45,53,51,28,14,7,4,256,2,4.0,4.578,1,1,true,skipped_explicit_log_ring_dim,127,126,/home/sora/.codex/worktrees/cedf/mxx/logs/test_gpu_diamond_io_input80_batch10_crt45_53_log15_16_sec100_sigma4p0_cbd2_release.log,1484,,,1418,2,33554433,1226,466,7bab63d0dcb8e75cd41cb9a140f49b19fb0a093f,true,/home/sora/.codex/worktrees/cedf/mxx/logs/test_gpu_diamond_io_input90_batch10_crt45_53_log16_skip_lattice_sec100_sigma4p0_cbd2_release.log,d5ade121f9f09eafa4faa2f9cae1bb6e8dfbb7d8,5,/workspace/logs/diamond_io_sec100_h200_iter5_d5ade121_20260517T070013JST/mxx-h200-diamond-io-sec100-est-20260517_1gpu_d5ade121f9f0_20260517T070013JST_diamond_io_row4.log,4894.2426108754,7639382038511751738891498139277151987051670594948741135,52756145950723562417943453342737232677474614,1728.1414876762003,670118290141931697811944482735822187500197972,23486873870902651209564816534405120,1.0182274534217755,1.967355999722658e-55,2.5708449391919452,3.468134372643018e-52,14713401305553361283670823025996689180981295518659744,525429407875401248 diff --git a/tests/test_gpu_lwe_nested_rns_ring_gsw_mul_bench.rs b/tests/test_gpu_lwe_nested_rns_ring_gsw_mul_bench.rs index 0584870f..1469d2a5 100644 --- a/tests/test_gpu_lwe_nested_rns_ring_gsw_mul_bench.rs +++ b/tests/test_gpu_lwe_nested_rns_ring_gsw_mul_bench.rs @@ -83,6 +83,7 @@ const DEFAULT_MAX_CRT_DEPTH: usize = 64; const DEFAULT_ERROR_SIGMA: f64 = 4.0; const DEFAULT_D_SECRET: usize = 1; const DEFAULT_BENCH_ITERATIONS: usize = 1; +const DEFAULT_MUL_DEPTH: usize = 1; const DEFAULT_BENCH_SEED: [u8; 32] = [0u8; 32]; const TRAPDOOR_SIGMA: f64 = 4.578; const ERROR_SIM_ACTIVE_LEVELS: usize = 1; @@ -113,6 +114,7 @@ struct RingGswMulBenchConfig { error_sigma: f64, d_secret: usize, bench_iterations: usize, + mul_depth: usize, active_levels_override: Option, dir_name_override: Option, } @@ -176,6 +178,8 @@ impl RingGswMulBenchConfig { "LWE_NESTED_RNS_RING_GSW_MUL_BENCH_BENCH_ITERATIONS", DEFAULT_BENCH_ITERATIONS, ); + let mul_depth = + env_or_parse_usize("LWE_NESTED_RNS_RING_GSW_MUL_BENCH_MUL_DEPTH", DEFAULT_MUL_DEPTH); let active_levels_override = env::var("LWE_NESTED_RNS_RING_GSW_MUL_BENCH_ACTIVE_LEVELS") .ok() .map(|value| value.trim().to_string()) @@ -208,6 +212,7 @@ impl RingGswMulBenchConfig { bench_iterations > 0, "LWE_NESTED_RNS_RING_GSW_MUL_BENCH_BENCH_ITERATIONS must be > 0" ); + assert!(mul_depth > 0, "LWE_NESTED_RNS_RING_GSW_MUL_BENCH_MUL_DEPTH must be > 0"); Self { ring_dim, @@ -220,6 +225,7 @@ impl RingGswMulBenchConfig { error_sigma, d_secret, bench_iterations, + mul_depth, active_levels_override, dir_name_override, } @@ -340,12 +346,16 @@ fn build_ring_gsw_mul_circuit( )); let lhs = RingGswCiphertext::input(ctx.clone(), None, &mut circuit); let rhs = RingGswCiphertext::input(ctx.clone(), None, &mut circuit); - let product = lhs.mul(&rhs, &mut circuit); + let mut product = lhs.mul(&rhs, &mut circuit); + for _ in 1..cfg.mul_depth { + product = product.mul(&rhs, &mut circuit); + } let reconstructed_outputs = product.reconstruct(&mut circuit); circuit.output(reconstructed_outputs); info!( - "ring_gsw mul circuit build elapsed_ms={:.3} encrypted_outputs=1", - build_start.elapsed().as_secs_f64() * 1000.0 + "ring_gsw mul circuit build elapsed_ms={:.3} mul_depth={} encrypted_outputs=1", + build_start.elapsed().as_secs_f64() * 1000.0, + cfg.mul_depth ); (circuit, ctx, vec![product]) } @@ -376,12 +386,16 @@ fn build_ring_gsw_mul_probe_circuit( )); let lhs = RingGswCiphertext::input(ctx.clone(), None, &mut circuit); let rhs = RingGswCiphertext::input(ctx.clone(), None, &mut circuit); - let product = lhs.mul(&rhs, &mut circuit); + let mut product = lhs.mul(&rhs, &mut circuit); + for _ in 1..cfg.mul_depth { + product = product.mul(&rhs, &mut circuit); + } let reconstructed_outputs = product.reconstruct(&mut circuit); circuit.output(reconstructed_outputs); info!( - "ring_gsw mul circuit build elapsed_ms={:.3} encrypted_outputs=1", - build_start.elapsed().as_secs_f64() * 1000.0 + "ring_gsw mul circuit build elapsed_ms={:.3} mul_depth={} encrypted_outputs=1", + build_start.elapsed().as_secs_f64() * 1000.0, + cfg.mul_depth ); (circuit, ctx) } @@ -711,6 +725,12 @@ async fn test_gpu_lwe_nested_rns_ring_gsw_mul_bench() { let (active_q_moduli, active_q, _) = active_q_moduli_and_modulus(¶ms, active_levels); let (circuit, ctx, encrypted_outputs) = build_ring_gsw_mul_circuit::(¶ms, &cfg, active_levels); + let final_product_fhe_decryption_error = encrypted_outputs + .last() + .expect("Ring-GSW multiplication circuit must produce a final product") + .estimate_decryption_error_norm(cfg.error_sigma) + .poly_norm + .norm; let max_selected_decryption_error = max_bigdecimal( encrypted_outputs .iter() @@ -723,6 +743,11 @@ async fn test_gpu_lwe_nested_rns_ring_gsw_mul_bench() { let ring_gsw_threshold = &ring_gsw_q / BigUint::from(2u64); let ring_gsw_threshold_bd = BigDecimal::from_biguint(ring_gsw_threshold.clone(), 0); let selected_decryption_ok = max_selected_decryption_error < ring_gsw_threshold_bd; + info!( + "ring_gsw_mul final_product_fhe_decryption_error={} final_product_fhe_decryption_error_bits={}", + final_product_fhe_decryption_error, + bigdecimal_bits_ceil(&final_product_fhe_decryption_error) + ); let gate_counts = circuit.count_gates_by_type_vec(); let total_lut_entries = circuit.total_registered_public_lut_entries(); let total_public_lut_gates = gate_counts.get(&PolyGateKind::PubLut).copied().unwrap_or(0); @@ -733,12 +758,13 @@ async fn test_gpu_lwe_nested_rns_ring_gsw_mul_bench() { single_gpu_id, detected_gpu_count, detected_gpu_ids ); info!( - "ring_gsw_mul selected crt_depth={} actual_crt_depth={} ring_dim={} num_slots={} active_levels={} crt_bits={} p_moduli_bits={} base_bits={} q_moduli={:?}", + "ring_gsw_mul selected crt_depth={} actual_crt_depth={} ring_dim={} num_slots={} active_levels={} mul_depth={} crt_bits={} p_moduli_bits={} base_bits={} q_moduli={:?}", crt_depth, actual_crt_depth, params.ring_dimension(), cfg.num_slots(), active_levels, + cfg.mul_depth, cfg.crt_bits, cfg.p_moduli_bits, cfg.base_bits,