Skip to content

Commit c70d39e

Browse files
committed
Fix DP-SASS noise and contribution bounds
1 parent 087aaf0 commit c70d39e

7 files changed

Lines changed: 414 additions & 140 deletions

File tree

docs/dp/dp_mechanisms.md

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ loudly rather than releasing something unproven:
9494
| `dp_sum_bound` | per-contribution value clip for `SUM`/`AVG` | `L_i, Λ_i` (value range) |
9595
| `dp_count_bound` | per-PU row/contribution clip for `COUNT` over a join | contribution bound |
9696
| `dp_max_groups_contributed` | max output groups one PU may affect | **`C_u`** |
97-
| `dp_sample_lanes` | number of sample lanes a PU is hashed into (`m` is fixed at 64) | subsample assignment (`m`; see §11) |
97+
| `dp_sass_m` | number of SAA subsamples; one of `64`, `128`, `256`, or `512` | `m` |
98+
| `dp_sample_lanes` | number of subsamples a PU affects; `m > 64` requires the default `1` | per-PU subsample spread |
9899
| `dp_sass_release` | `'median'` (smooth-sensitivity, (ε,δ)) or `'average'` (GUPT mean, pure ε) | release rule |
99100
| `dp_sass_avg_method` | `dp_sass` AVG estimator: `'lane_average'` (default), `'nonempty_lane_average'`, or `'ratio'` (§13) ||
100101
| `dp_sass_sum_method` | `dp_sass` SUM estimator under average release: `'lane_average'` (default) or `'nonempty_lane_average'` (§13) ||
@@ -269,19 +270,21 @@ Song 2018) and its follow-up Chorus.
269270
**median** of the lane answers (smooth-sensitivity Laplace, (ε,δ)) or their **mean**
270271
(GUPT-style, pure ε).
271272

272-
> **Two "lanes" to keep apart.** There are always **`m = 64` lanes** (fixed) — the
273-
> release is the median/mean over these 64. The setting **`dp_sample_lanes`** (default
274-
> `1`) is *not* this count; it is how many of the 64 lanes each **PU** is placed into
275-
> (its per-PU spread). The name is misleading: `dp_sample_lanes = 1` still means 64
276-
> lanes, each PU in one of them.
273+
> **Two "lanes" to keep apart.** **`dp_sass_m`** controls the number of subsamples
274+
> (`64`, `128`, `256`, or `512`). The release is the median or mean over those `m`
275+
> answers. **`dp_sample_lanes`** instead controls how many subsamples each PU affects.
276+
> Its default is `1`, which gives disjoint subsamples. Values above `1` are supported
277+
> only by the `m = 64` mask path.
277278
278279
**Membership function (paper's `H(pu) mod m`).** With `dp_sample_lanes = 1` (the
279-
default) each PU is assigned to exactly one lane by a stable hash of its PU key —
280-
disjoint subsamples, as GUPT/the paper assume. The hash is computed after ordinary
281-
filters have been pushed to scans, and it is a pure function of the PU key, so
282-
adding/removing one PU does not renumber any other PU's sample lane.
283-
284-
> **Note — lanes are not disjoint when `dp_sample_lanes > 1`.** For `sample_lanes > 1`
280+
default), each PU is assigned to exactly one lane by a stable hash of its PU key.
281+
For `m = 64`, the lane is represented by one bit in a 64-bit mask. For larger `m`,
282+
the aggregate stores the lane identifier `H(pu) mod m` directly. Both paths form
283+
disjoint subsamples. The hash is computed after ordinary filters have been pushed
284+
to scans, and adding or removing one PU does not renumber any other PU's lane.
285+
286+
> **Note — lanes are not disjoint when `dp_sample_lanes > 1`.** This option applies
287+
> only to `m = 64`. For `sample_lanes > 1`
285288
> a PU is hashed into up to `sample_lanes` lanes by `DpSampleHash`
286289
> (`sample_hash |= 1 << ((key_hash >> 6i) & 63)`), so the subsamples **overlap** — a
287290
> PU appears in several lanes, and there is currently **no mechanism to force the
@@ -292,8 +295,8 @@ adding/removing one PU does not renumber any other PU's sample lane.
292295
> median — §12), but the disjoint-subsample assumption the SAA analysis relies on is
293296
> not enforced for `sample_lanes > 1`.
294297
295-
**`ss_noised` (median release; paper's subroutine).** Sort the 64 clipped lane
296-
answers, take `z_p` with `p = ⌈m/2⌉ = 32` (`values[31]`), compute the smooth
298+
**`ss_noised` (median release; paper's subroutine).** Sort the `m` clipped lane
299+
answers, take the lower median `z_p` with `p = ⌈m/2⌉`, compute the smooth
297300
sensitivity `S*` of the median (§12), and add `Laplace(2·S*/ε')` with
298301
`β = ε'/(2·ln(2/δ'))`. Cross-checked against Nissim–Raskhodnikova–Smith (2007).
299302

@@ -318,8 +321,8 @@ partition-selection count and give each aggregate cell
318321

319322
**Median (`dp_sass`).** Two code paths, in `src/aggregates/dp_laplace_noise.cpp`:
320323
- **Exact**`SmoothMedianSensitivityExact`: the Nissim–Raskhodnikova–Smith
321-
divide-and-conquer (`JList`) over the 64 sorted values bracketed by `[L, Λ]`
322-
sentinels, `O(m log m)`. Used on the bounded path (always, in practice).
324+
exact interval scan over the `m` sorted values bracketed by `[L, Λ]` sentinels,
325+
`O(m²)`. Used on the bounded path (always, in practice).
323326
- **Approximate**`SmoothMedianSensitivity`: a capped scan, used only on the legacy
324327
unbounded path.
325328

@@ -340,7 +343,7 @@ smooth sensitivity, same NRS `β`.
340343

341344
| aggregate | `dp_standard` / `dp_elastic` | `dp_sass` |
342345
|---|---|---|
343-
| `COUNT(*)` | clip per-PU row count (join: `dp_count_bound`), Laplace | per-lane count, median/mean of 64 |
346+
| `COUNT(*)` | clip per-PU row count (join: `dp_count_bound`), Laplace | per-lane count, median/mean of `m` answers |
344347
| `COUNT(DISTINCT x)` | per-PU distinct count, clip, sum, Laplace | per-PU distinct count, clip, sample-sum, median/mean; single DISTINCT may use the lane-mask specialization |
345348
| `SUM(x)` | clip `x` to `dp_sum_bound`, sum, Laplace | per-lane sum; average release supports fixed-lane or privately-counted non-empty-lane estimators |
346349
| `AVG(x)` | decomposed → `SUM(clip x)` + `COUNT`; released as `noised_sum / max(noised_count, 1)` (Google bounded-mean shape) | **three estimators** (`dp_sass_avg_method`): `lane_average` (default) fills empty lanes before releasing the lane mean; `nonempty_lane_average` divides a noisy shifted sum of populated-lane averages by their noisy count; `ratio` releases independent row SUM and COUNT SAA cells and divides them |
@@ -378,21 +381,22 @@ distinct values per group, and the top aggregate releases an `as_sample_sum` ove
378381
bounded per-PU counts. Single `COUNT(DISTINCT)` queries may use the optimized
379382
`RewriteDistinctCountToSampleMedian` lane-mask path: it groups by `[groups..., PU,
380383
distinct value]`, computes `bit_or(dp_sample_mask(pu_hash))`, applies the same caps, then
381-
merges masks by `[groups..., distinct value]` before releasing 64 lane counts.
384+
merges masks by `[groups..., distinct value]` before releasing 64 lane counts. The
385+
variable-`m` path represents lane assignments directly instead of using a bit mask.
382386

383387
---
384388

385389
## 14. NULL handling
386390

387391
### Empty-lane filling (`dp_sass`)
388392

389-
A `dp_sass` aggregate computes 64 lane answers and releases the median (or mean) of
390-
those 64. Some lanes can be **empty** — no PU was assigned to that lane, or everything
393+
A `dp_sass` aggregate computes `m` lane answers and releases their median or mean.
394+
Some lanes can be **empty** — no PU was assigned to that lane, or everything
391395
in it was filtered out. An empty lane has no natural answer: an empty `COUNT`/`SUM` is
392396
`0`, but an empty `AVG`/`MIN`/`MAX` is undefined (`NULL`, i.e. ±∞).
393397

394398
The danger is that the *number* of non-empty lanes is **data-dependent** — a group with
395-
3 PUs populates far fewer of the 64 lanes than a group with 3000. If we released "the
399+
3 PUs populates far fewer lanes than a group with 3000. If we released "the
396400
median over just the non-empty lanes," that effective sample size would itself be a
397401
signal about the group's support, and it is not covered by the noise calibration — a
398402
leak.
@@ -417,7 +421,7 @@ Because the fill depends only on the public bounds (`dp_sass_count_output_bound`
417421
`dp_sass_minmax_lower/upper_bound`, …), swapping one PU in or out cannot change what an
418422
empty lane receives — it leaks nothing. Were the fill taken from the data (e.g. the
419423
observed minimum) it *would* leak, which is why §5 lists "empty-lane fill is public" as
420-
a load-bearing assumption. The effect: a 3-PU group and a 3000-PU group both yield 64
424+
a load-bearing assumption. The effect: a 3-PU group and a 3000-PU group both yield `m`
421425
fully-defined lanes, so the support signal never reaches the released median. This is
422426
the GUPT "fixed number of blocks, bounded block outputs" property.
423427

@@ -480,6 +484,10 @@ releasing the raw value; `privacy_noise = false` zeroes noise for deterministic
480484
query that never named the PU table; now the lanes are derived from the linked table's
481485
FK column and the result matches the
482486
explicit join (§13).
487+
- **Configurable `m`.** `dp_sass_m` supports `64`, `128`, `256`, and `512`. The
488+
optimized `m = 64` path uses a machine-word membership mask. Larger values use
489+
direct lane identifiers and variable-size aggregate states while preserving the
490+
same disjoint assignment for `dp_sample_lanes = 1`.
483491

484492
### Still open
485493

@@ -489,16 +497,13 @@ releasing the raw value; `privacy_noise = false` zeroes noise for deterministic
489497
Bernoulli-`1/m` inclusion model, under which a PU appears in up to O(√m) subsamples
490498
(Nissim et al.) and that factor must enter the analysis. Confirm our hard-cap scaling is
491499
sound, or restrict `sample_lanes > 1`. An alternative derivation provides pseudocode for this case.
492-
2. **Configurable `m`.** `m` is fixed at 64 (SIMD/perf). SAA supports general `m`; `m ≤ 64`
493-
is achievable by using a subset of the 64-bit mask, `m > 64` needs a wider multi-word
494-
representation. Worth exposing `m` as a setting (64 the recommended default) — future work.
495-
3. **τ under very small δ.** A very small `δ_η` drives τ large enough to suppress all
500+
2. **τ under very small δ.** A very small `δ_η` drives τ large enough to suppress all
496501
groups. One possible mitigation is to give partition selection a relatively larger
497502
`δ_η` (an uneven δ split) so τ stays practical. Currently `δ_η = δ/(c+1)` (even split).
498-
4. **User-level FLEX.** Extending `dp_elastic` from row-level to user-level needs new
503+
3. **User-level FLEX.** Extending `dp_elastic` from row-level to user-level needs new
499504
per-user statistics + a sensitivity analysis and proof (future work).
500-
5. **`FILTER` on `COUNT(DISTINCT)`** (§3) — implementation gap, not a DP restriction: the
505+
4. **`FILTER` on `COUNT(DISTINCT)`** (§3) — implementation gap, not a DP restriction: the
501506
distinct rewrite already uses the aggregate's `FILTER` slot, so a user `FILTER` would
502507
have to be composed with it. Fails closed for now; plain aggregates support `FILTER`.
503-
6. **Fail-open side tables** — an unannotated private table is treated as public (§5.6);
508+
5. **Fail-open side tables** — an unannotated private table is treated as public (§5.6);
504509
this relies on analyst discipline.

0 commit comments

Comments
 (0)