Skip to content

Commit c60a7bb

Browse files
authored
Merge pull request #20 from cwida/dp-sass-variable-m
Add variable-m DP-SAA and reproducible benchmarks
2 parents 008add9 + cd81a37 commit c60a7bb

252 files changed

Lines changed: 15688 additions & 106267 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,27 @@ test/python/__pycache__/
1515
*.parquet
1616

1717
# Generated benchmark result outputs
18+
benchmark/**/*.png
19+
benchmark/**/*.log
20+
benchmark/**/__pycache__/
21+
benchmark/results/*
22+
!benchmark/results/paper/
23+
!benchmark/results/paper/*.csv
24+
!benchmark/results/paper/README.md
25+
benchmark/dp/tmp/
1826
benchmark/tpch/dp_elastic_*.csv
1927
benchmark/tpch/dp_sass_*.csv
28+
benchmark/tpch/as_tpch_*.csv
2029
benchmark/clickbench/clickbench_*.csv
30+
benchmark/clickbench/as_clickbench_*_results.csv
2131
benchmark/clickbench/*.png
32+
benchmark/dp/*_results.csv
33+
benchmark/dp/*_summary.csv
34+
benchmark/dp/tpch_sf30_runtime_perfect_*.csv
2235
benchmark/dp/sqlstorm_stackoverflow_*stability*.csv
23-
benchmark/dp/jcch_*_results.csv
2436
benchmark/sqlstorm/sqlstorm_degradation_*.csv
2537
benchmark/sqlstorm/data/
26-
27-
# Generated query sets (produced by the stability/scan scripts)
28-
benchmark/dp/sqlstorm_stackoverflow_stable_queries/
29-
benchmark/dp/sqlstorm_stackoverflow_stable_aggregate_queries/
30-
benchmark/dp/uber_like_jcch/
38+
benchmark/tpch/*_paper_summary.csv
3139

3240
# Serialized privacy-metadata dumps
3341
pac_metadata_*.json

.gitmodules

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,3 @@
1212
[submodule ".claude/skills/shared"]
1313
path = .claude/skills/shared
1414
url = https://github.com/ila/duckdb-claude-skills.git
15-
[submodule "benchmark/jcch/dbgen.JCC-H"]
16-
path = benchmark/jcch/dbgen.JCC-H
17-
url = https://github.com/ldbc/dbgen.JCC-H

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Never execute git commands that could lose code. Always ask the user for permiss
2626

2727
- **`pac`** (default): PAC Privacy — empirical MIA resistance. Maintains 64 parallel counters per aggregate (one per "world" bit), adds noise calibrated to the query variance across sub-samples. Provides theoretical mutual-information bounds. Not differential privacy.
2828
- **`dp_standard`**: Global-sensitivity DP — pure ε-DP for **ungrouped** queries, (ε,δ)-DP for **grouped** ones (see partition selection below). Bounds each privacy unit's *total* contribution to a fixed constant (per-PU contribution clipping), so global sensitivity equals that constant — data-independent. On a single PU table this is per-row clipping (COUNT sensitivity = 1, SUM = `dp_sum_bound`); on a join it pre-aggregates per PU (grouping by the PU-adjacent FK), clips each PU's partial to `dp_sum_bound`/`dp_count_bound`, then sums — so a join with COUNT requires `dp_count_bound`. Laplace noise calibrated to `bound/epsilon`. **Grouped queries apply private partition selection by default** (noised `COUNT(DISTINCT pu) ≥ τ`, the same Wilson/Google mechanism as `dp_sass`): the released key set is data-dependent, so this reserves `ε_η = ε/(c+1)` and needs `δ_η`, making grouped `dp_standard` (ε,δ)-DP and **requiring `dp_delta`**; ungrouped `dp_standard` stays pure ε-DP.
29-
- **`dp_elastic`**: Elastic sensitivity DP — formal (ε,δ)-DP **at the row level** (FLEX/Chorus neighbor = change one tuple). Same Laplace pipeline as `dp_standard` but uses per-row clipping and the smoothed join-frequency envelope (`2·SES_β`, β derived from ε,δ) for sensitivity. Requires `dp_delta`. **Not user-level**: `∏mf` bounds a single-tuple change, not a whole PU's contribution — a user-level extension would need per-user statistics and a new sensitivity proof (future work). It follows FLEX's public/enumerated-partition model (no *DP* partition selection), unlike the user-level `dp_standard`/`dp_sass`. Its `privacy_min_group_count` gate is a **raw (un-noised) admin support filter, not DP partition selection** — under FLEX's public-partition assumption it's a utility convenience; making it row-level-DP-safe would need a noised τ with `C_u = 1` (Dandan's row-level branch), deferred.
29+
- **`dp_elastic`**: Elastic sensitivity DP — formal (ε,δ)-DP **at the row level** (FLEX/Chorus neighbor = change one tuple). Same Laplace pipeline as `dp_standard` but uses per-row clipping and the smoothed join-frequency envelope (`2·SES_β`, β derived from ε,δ) for sensitivity. Requires `dp_delta`. **Not user-level**: `∏mf` bounds a single-tuple change, not a whole PU's contribution — a user-level extension would need per-user statistics and a new sensitivity proof (future work). It follows FLEX's public/enumerated-partition model (no *DP* partition selection), unlike the user-level `dp_standard`/`dp_sass`. Its `privacy_min_group_count` gate is a **raw (un-noised) admin support filter, not DP partition selection** — under FLEX's public-partition assumption it's a utility convenience; making it row-level-DP-safe would need a noised τ with `C_u = 1` (the row-level branch), deferred.
3030
- **`dp_sass`**: Sample-and-aggregate. Computes 64 sample (lane) aggregates per privacy unit and releases either the **median** (`dp_sass_release='median'`, default; smooth-sensitivity Laplace, (ε,δ)-DP, requires `dp_delta`) or the **mean** (`dp_sass_release='average'`; GUPT-style, pure ε-DP, no `dp_delta`). Per-PU contribution is clipped (`dp_count_bound`/`dp_sum_bound`); the cross-group bound `dp_max_groups_contributed` (C_u) enters each aggregate's budget. **Grouped queries apply private partition selection automatically** (per the paper's Algorithm 2): a noised `COUNT(DISTINCT pu)` is released only when `≥ τ = 1 − C_u·log(2−2(1−δ_η)^{1/C_u})/ε_η`, reserving `ε_η = ε/(c+1)` and `δ_η = δ/(c+1)` so each of the c aggregates gets `ε/((c+1)·C_u)`. Grouped `dp_sass` therefore requires `dp_delta` (the pure-ε `average` release is only valid ungrouped). The released value is clamped to a public output domain (`dp_sass_count_output_bound`, `dp_sass_sum_output_bound`, `dp_sass_avg_lower/upper_bound`, `dp_sass_minmax_lower/upper_bound`); with `dp_sass_private_range=true` (average release only) a fraction `dp_sass_range_budget_fraction` of ε privately estimates that range (pure-ε exp-mechanism quantiles). Supports COUNT, COUNT(DISTINCT), SUM, AVG, MIN, MAX.
3131

3232
`dp_standard` and `dp_elastic` share one code path (`CompileDPLaplaceQuery` in `privacy_mechanisms.cpp`); they differ only in how contributions are clipped and how sensitivity is derived (`ClipAndComputeSensitivities`). All four modes share the same DDL (`PRIVACY_KEY`, `PRIVACY_LINK`, `PROTECTED`) and rewrite aggregate plans transparently — users write normal SQL.

CMakeLists.txt

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -106,23 +106,30 @@ if(NOT EMSCRIPTEN AND NOT WIN32)
106106
add_executable(as_clickbench_benchmark benchmark/clickbench/as_clickbench_benchmark.cpp)
107107
target_link_libraries(as_clickbench_benchmark ${PRIVACY_LINK_LIBS})
108108

109-
# TPCH Compiler benchmark runner (compares PAC vs regular TPC-H)
110-
add_executable(pac_tpch_compiler_benchmark benchmark/tpch/pac_tpch_compiler_benchmark.cpp)
111-
target_link_libraries(pac_tpch_compiler_benchmark ${PRIVACY_LINK_LIBS})
112-
113109
# Unified DP utility benchmark runner
110+
set(TPCH_STOCK_DP_QUERY_NAMES q01 q05 q06 q14 q19)
111+
set(TPCH_STOCK_DP_QUERY_FILES)
112+
foreach(QUERY_NAME IN LISTS TPCH_STOCK_DP_QUERY_NAMES)
113+
string(TOUPPER ${QUERY_NAME} QUERY_NAME_UPPER)
114+
set(QUERY_FILE "${CMAKE_CURRENT_SOURCE_DIR}/benchmark/dp/tpch_queries/${QUERY_NAME}.sql")
115+
list(APPEND TPCH_STOCK_DP_QUERY_FILES "${QUERY_FILE}")
116+
file(READ
117+
"${QUERY_FILE}"
118+
"TPCH_${QUERY_NAME_UPPER}_SQL")
119+
endforeach()
120+
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
121+
${TPCH_STOCK_DP_QUERY_FILES})
122+
set(TPCH_QUERY_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated")
123+
file(MAKE_DIRECTORY "${TPCH_QUERY_GENERATED_DIR}")
124+
configure_file(
125+
benchmark/include/tpch_stock_queries.hpp.in
126+
"${TPCH_QUERY_GENERATED_DIR}/tpch_stock_queries.hpp"
127+
@ONLY)
114128
add_executable(dp_benchmark_runner benchmark/dp/dp_benchmark_runner.cpp)
129+
target_include_directories(dp_benchmark_runner PRIVATE "${TPCH_QUERY_GENERATED_DIR}")
115130
target_link_libraries(dp_benchmark_runner ${PRIVACY_LINK_LIBS})
116131

117-
# ClickHouse/ClickBench benchmark runner
118-
add_executable(pac_clickhouse_benchmark benchmark/clickbench/pac_clickhouse_benchmark.cpp)
119-
target_link_libraries(pac_clickhouse_benchmark ${PRIVACY_LINK_LIBS})
120-
121132
# IMDb/JOB benchmark runner
122133
add_executable(imdb_benchmark benchmark/imdb/imdb_benchmark.cpp)
123134
target_link_libraries(imdb_benchmark ${PRIVACY_LINK_LIBS})
124-
125-
# SQLStorm benchmark runner
126-
add_executable(pac_sqlstorm_benchmark benchmark/sqlstorm/pac_sqlstorm_benchmark.cpp)
127-
target_link_libraries(pac_sqlstorm_benchmark ${PRIVACY_LINK_LIBS})
128135
endif()

README.md

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -250,27 +250,3 @@ ALTER PU TABLE t DROP PROTECTED (col2);
250250

251251
For implementation details, see the [docs/](docs/README.md) folder: \
252252
[Syntax](docs/syntax.md) | [SIMD aggregates](docs/simd-asa/aggregates.md) | [Query Operators](docs/pac/query_operators.md) | [DP mechanisms](docs/dp/dp_mechanisms.md) | [Runtime Checks](docs/pac/runtime_checks.md) | [Tests](docs/test/README.md) | [Benchmarks](docs/benchmark/README.md)
253-
254-
## Literature
255-
256-
I. Battiston, D. Yuan, X. Zhu, P. Boncz. [SIMD-PAC-DB: Pretty Performant PAC Privacy](https://arxiv.org/abs/2603.15023). 2026.
257-
258-
N. Johnson, J.M. Near, D. Song. [Towards Practical Differential Privacy for SQL Queries](https://arxiv.org/abs/1706.09479) (Flex / Elastic Sensitivity). VLDB 2018.
259-
260-
R. Wilson, C. Zhang, W. Lam, D. Desfontaines, D. Simmons-Marengo, B. Gipson. [Differentially Private SQL with Bounded User Contribution](https://arxiv.org/abs/1909.01917). PoPETs 2020.
261-
262-
```bibtex
263-
@misc{battiston2026simdpacdbprettyperformantpac,
264-
title={SIMD-PAC-DB: Pretty Performant PAC Privacy},
265-
author={Ilaria Battiston and Dandan Yuan and Xiaochen Zhu and Peter Boncz},
266-
year={2026},
267-
eprint={2603.15023},
268-
archivePrefix={arXiv},
269-
primaryClass={cs.DB},
270-
url={https://arxiv.org/abs/2603.15023},
271-
}
272-
```
273-
274-
## Maintainer
275-
276-
This extension is maintained by **@ila** (ilaria@cwi.nl).

attacks/clip_attack_results.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ threshold. Only the outlier level (1 contributor) is affected.
123123

124124
---
125125

126-
## Attack 6: Clip-after-filter vs clip-full-table (Dandan's hypothesis)
126+
## Attack 6: Clip-after-filter vs clip-full-table
127127

128-
Dandan's concern: clipping applied after filtering may leak more than clipping
128+
The concern is that clipping applied after filtering may leak more than clipping
129129
applied to the entire dataset, because the filter changes which users contribute
130130
to the bitmap, affecting which levels appear "supported."
131131

@@ -137,7 +137,7 @@ to the bitmap, affecting which levels appear "supported."
137137
| clip-after-filter (pac_clip_support=2) | **72.5%** | 613,511 | 122,026 | 5.0x |
138138
| clip-full-table (pre-clip to mu+3sigma) | **56.9%** | 180,457 | 124,641 | 1.4x |
139139

140-
**Finding: Dandan is correct.** Pre-clipping the full table then filtering gives
140+
**Finding: the concern is valid.** Pre-clipping the full table then filtering gives
141141
significantly better protection (56.9% vs 72.5%). The reasons:
142142

143143
1. **Full-table pre-clipping** clamps the billionaire to 13,661 BEFORE PAC sees it.
@@ -149,7 +149,7 @@ significantly better protection (56.9% vs 72.5%). The reasons:
149149
The attenuation is only ~16x (one level), leaving a 5x variance gap.
150150

151151
However, clip-after-filter is still much better than no clipping (72.5% vs 96%),
152-
confirming Dandan's second point: "this approach is still significantly better
152+
confirming the second point: "this approach is still significantly better
153153
than not applying clipping at all."
154154

155155
---
@@ -266,7 +266,7 @@ Best threshold accuracy: **100.0%**. Attack fully succeeds.
266266
"supported." The clipping mechanism assumes outlier levels have few contributors.
267267
Collusion (or any scenario with 2+ users at the same extreme level) defeats it.
268268

269-
### TEST 5: Dandan's filter probing
269+
### TEST 5: Filter probing
270270

271271
Attacker uses two queries with different filters to probe clipping behavior.
272272

@@ -276,7 +276,7 @@ Attacker uses two queries with different filters to probe clipping behavior.
276276
| Filter<=999 (wide) | 51.7% |
277277
| Cross-filter differential | **51.0%** |
278278

279-
**Dandan's concern is NOT exploitable with hard-zero.** The narrow query zeroes the
279+
**The filter-probing concern is NOT exploitable with hard-zero.** The narrow query zeroes the
280280
outlier level, giving identical counter distributions for in/out. The wide query
281281
has the outlier's level zeroed too (1 contributor < threshold). The cross-filter
282282
differential reveals nothing.
@@ -307,7 +307,7 @@ Even the minimum level-3 value is zeroed when it's the sole contributor.
307307
3. **Two colluding outliers defeat the clipping** by making their level "supported"
308308
(2 contributors >= threshold 2). Attack accuracy: 100%.
309309

310-
4. **Dandan's filter-probing concern does not apply with hard-zero.** The zeroed level
310+
4. **Filter probing does not apply with hard-zero.** The zeroed level
311311
contributes nothing regardless of filter, so different filters reveal no info.
312312

313313
5. **The pre-aggregation step remains essential** — 20K small items are correctly

attacks/clip_attack_test.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,14 @@ for CLIP in off 50 100; do
117117
run_scenario "atk5" 1000 999999 999 "$CLIP" 15
118118
done
119119

120-
# --- Attack 6: Clip after filter vs clip on full table (Dandan's concern) ---
120+
# --- Attack 6: Clip after filter vs clip on full table ---
121121
# pac_clip_sum clips AFTER filtering (only filtered rows enter the aggregate).
122122
# An adversary might exploit this: the clipping behavior differs depending on
123123
# which users are in the filter. Compare filter-then-clip (what pac_clip_sum does)
124124
# vs clip-all-then-filter (manual pre-clipping of the full table, then query).
125125
echo "## ATTACK 6: Clip-after-filter vs clip-full-table"
126126
echo "N=1000, target=999999, filter<=3, $NT trials"
127-
echo "Tests Dandan's hypothesis: clipping after filtering leaks more than"
127+
echo "Tests whether clipping after filtering leaks more than"
128128
echo "clipping the entire dataset. We compare pac_clip_sum (clips filtered rows)"
129129
echo "vs manual pre-clipping of all rows then querying without clip_support."
130130
echo ""

attacks/clip_hardzero_stress.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,9 @@ analyze "Two outliers (0 and -1), filt<=3" "$IN_F" "$OUT_F" "$FBG" 999999
209209
rm -f "$IN_F" "$OUT_F"
210210

211211
# ---------------------------------------------------------------
212-
# TEST 5: Dandan's filter probing attack
212+
# TEST 5: Filter probing attack
213213
# ---------------------------------------------------------------
214-
echo "## TEST 5: Dandan's filter probing"
214+
echo "## TEST 5: Filter probing"
215215
echo "Attacker tries different filters to see if clipping behavior changes."
216216
echo "If the outlier is present, the bitmap at level 3 has a bit set."
217217
echo "Query 1: filter<=3 (includes user 0 if present)"

benchmark/clickbench/README.md

Lines changed: 12 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,18 @@
1-
# ClickBench privacy benchmark
1+
# ClickBench AS Benchmark
22

3-
`pac_clickhouse_benchmark` runs the ClickHouse `hits` (ClickBench) queries through the
4-
`privacy` extension and measures runtime (and, where applicable, utility) for several
5-
privacy mechanisms against a non-private baseline.
6-
7-
## Running
3+
`as_clickbench_benchmark` compares plain DuckDB with handwritten stochastic-aggregation forms of the aggregate
4+
ClickBench queries. Queries without an aggregate are skipped.
85

96
```bash
10-
# build first: GEN=ninja make
11-
build/release/extension/privacy/pac_clickhouse_benchmark [options]
7+
cmake --build build/release --target as_clickbench_benchmark
8+
build/release/extension/privacy/as_clickbench_benchmark \
9+
--config benchmark/configs/utility/as_clickbench_graviton.json --dry-run
10+
build/release/extension/privacy/as_clickbench_benchmark \
11+
--config benchmark/configs/utility/as_clickbench_graviton.json
1212
```
1313

14-
Options:
15-
16-
| Flag | Meaning |
17-
|---|---|
18-
| `--micro` | Use a smaller dataset (`clickbench_micro.db`) for quick testing |
19-
| `--db <path>` | DuckDB database file |
20-
| `--queries <dir>` | Directory with `create.sql`, `load.sql`, `queries.sql`, `setup.sql` |
21-
| `--out <csv>` | Output CSV path (auto-named if omitted) |
22-
| `--modes <csv>` | Which mechanisms to measure: subset of `pac,dp_standard,dp_elastic,dp_sass` (default: all four) |
23-
| `--run-naive` | Also run the explicit sample-table-join "naive" variants for the sampling mechanisms in `--modes` |
24-
25-
The non-private **`baseline`** always runs regardless of `--modes`.
26-
27-
Each selected DP/PAC mechanism runs **once per query** at a single operating point —
28-
`dp_epsilon = 1.0` and the inferred "perfect" sensitivity bound (no epsilon/sensitivity sweep).
29-
The DP mechanisms use a common ClickBench support set for reporting; queries outside that
30-
intersection are recorded as failed for all DP modes instead of letting one mechanism report a
31-
larger successful-query set than another.
32-
33-
## Modes in the output `mode` column
34-
35-
- `baseline` — plain DuckDB, no privacy.
36-
- `PAC`, `dp_standard`, `dp_elastic`, `dp_sass` — the four mechanisms (vectorized rewrite path).
37-
- `naive_pac`, `naive_dp` — explicit-SQL sample-and-aggregate baselines (only with `--run-naive`).
38-
39-
## Naive queries
40-
41-
The naive variants live in:
42-
43-
- `clickbench_naive_pac_queries/qNN.sql`
44-
- `clickbench_naive_dp_queries/qNN.sql`
45-
46-
They exist only for the **privatizable aggregate** queries (`SUM`/`COUNT`/`AVG` over `hits`
47-
grouping on non-protected, non-`UserID` columns). Queries with no naive form — `COUNT(DISTINCT
48-
UserID)`, `MIN`/`MAX`, row dumps (`ORDER BY … LIMIT`), and `GROUP BY UserID` — have no file and
49-
are skipped automatically by the runner.
50-
51-
Both families share one skeleton and differ only in the terminal UDF:
52-
53-
- **PAC naive**: 128 random sub-samples per privacy unit (`WHERE random() < 0.5`), per-sub-sample
54-
`SUM`/`COUNT`/`AVG`, then `pac_aggregate(answers, counts, mi, k)` → mean-of-sub-samples + PAC noise.
55-
- **DP naive**: 64 lanes, each privacy unit assigned to ~8 of them via an **approximate** SQL hash
56-
(`hash(UserID, lane_id) % 64 < 8`), per-lane answers rescaled by `64/8`, then
57-
`dp_aggregate(answers, counts, eps, delta, lanes, lower, upper)` → clipped median of the 64
58-
lane answers + smooth-sensitivity Laplace noise (wraps `dp_smooth_median_noise`; the `counts`
59-
argument is present only for signature symmetry with `pac_aggregate` and is unused by the
60-
median path).
14+
If the configured database does not exist, the runner downloads `hits.parquet` and loads `hits`. It records the median
15+
of five hot runs in `query,mode,m,median_ms` format. Use `benchmark/configs/sanity/as_clickbench_m_sweep.json` for a
16+
focused `m=64,512` check.
6117

62-
> **The DP naive variant is a runtime / structural baseline, not a utility-faithful one.** Its
63-
> lane assignment is approximate (the real `DpSampleHash` 6-bit-chunk bitmask is C++-only and
64-
> cannot be reproduced in plain SQL), and its domain bounds are hand-set generous constants
65-
> rather than the per-query inferred bounds used by the real `dp_sass` rewrite. The runner records
66-
> only **timing + success** for naive rows; their numeric values are expected to be noise-dominated
67-
> and should not be compared to the vectorized path for accuracy.
18+
See [repro.md](../../repro.md) for the reported machine settings and plotting command.

0 commit comments

Comments
 (0)