Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
25673e9
Initial DPBuiltinCPInstruction and RDPAccountant
Jun 26, 2026
52b87c8
Add dp_gaussian and dp_laplace operations
Jun 26, 2026
01b938a
Fix test package name.
Jul 2, 2026
643ec89
Fix Laplace accountant
Jul 6, 2026
6eecbea
Add unit tests
Jul 6, 2026
7d3e25e
Rename RDPAccountant to DPBudgetAccountant
Jul 6, 2026
77db192
Delete RDPAccountant
Jul 6, 2026
af7b13a
Fix parseInstruction() comment
Jul 6, 2026
9dcbb08
Integration tests
Jul 6, 2026
7334f4a
Update test comments
Jul 6, 2026
e2a70a9
Fix integration test to compare clean to noisy data
Jul 7, 2026
14df339
Make integration test more readable
Jul 7, 2026
e6d87e6
Comment fix
Jul 7, 2026
33a15fa
Fix L1 L2 sensitivity parameter documentation
Jul 7, 2026
67d3b1f
junit output format
Jul 7, 2026
b75d85b
dp_laplace and dp_gaussian now take the original matrix and build a t…
Jul 9, 2026
467144c
Remove 4th and 5th parameters and instead get them in a loop, accordi…
Jul 13, 2026
74d10b9
Add dp_set_budget(epsilon, delta) built-in
Jul 14, 2026
dc0dbf2
Change DPBudgetAccountant defaults to be constants
Jul 14, 2026
a62a1e5
Cleanup comments
Jul 14, 2026
57c59aa
Differential Privacy Benchmark
Jul 14, 2026
dda4a53
Checkstyle fix
Jul 21, 2026
15eff33
Add missing licenses
Jul 21, 2026
520b432
More checkstyle fixes
Jul 22, 2026
e5651ff
Add missing break. Fixes test
Jul 22, 2026
a328bd7
Change ParameterizedBuiltinOp.compare() so that two syntactically ide…
Jul 28, 2026
1a43391
Address David's review comments: unite utility methods, reorder alpha…
Jul 28, 2026
da825c7
Route dp_laplace/dp_gaussian through the named-parameter builtin path
Jul 28, 2026
ef27332
Code review comments: remove special characters
Jul 29, 2026
fcf4338
Unite tests
Jul 31, 2026
1e8f5d9
Remove unneeded pom.xml change
Jul 31, 2026
1c35622
Use getRandomMatrix in DPBuiltinDMLTest and call it in setup instead …
Aug 1, 2026
5f660cf
Removed python requirements file
Aug 1, 2026
3e2f01e
Reorder Gauss and Laplace cases alphabetically
Aug 1, 2026
cd0844b
Add parsing tests
Aug 2, 2026
1103eb6
Use computeGaussianSigma
Aug 2, 2026
8774f3f
Change gaussian noise generation to use MatrixBlock randOperation, an…
Aug 2, 2026
6149776
Checkstyle
Aug 2, 2026
baf15c0
use an existing ParameterizedBuiltin type, instead of a new instructi…
Aug 3, 2026
d021e92
Comment readability
Aug 4, 2026
8b2e967
Return DPBuiltinCPInstruction, but now it extends ParameterizedBuilti…
Aug 4, 2026
20a640f
Comment cleanups after review fixes
Aug 5, 2026
d47883b
Move DP benchmark under dp folder and add a README
Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions benchmark/dp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<!--
{% comment %}
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to you under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
{% end comment %}
-->

# DP-FedAvg Benchmark

Benchmarks the `dp_gaussian` built-in by sweeping the privacy budget ε for
federated logistic regression (DP-FedAvg) on the UCI Adult dataset, and
plotting the accuracy/privacy trade-off.

## Setup

Run these commands from the repository root. They create a virtual
environment named `python_venv` and install the Python dependencies listed
in [scripts/requirements.txt](scripts/requirements.txt):

```bash
python3 -m venv benchmark/dp/scripts/python_venv
source benchmark/dp/scripts/python_venv/bin/activate
pip install -r benchmark/dp/scripts/requirements.txt
```

Build SystemDS before running the benchmark, if you haven't already:

```bash
mvn clean package -DskipTests
```

## Running

With `python_venv` activated, run the benchmark from the repository root:

```bash
bash benchmark/dp/scripts/run_benchmark.sh
```

This prepares the dataset, starts the federated workers, sweeps
epsilon over {0.5, 1, 4, 8} plus a non-private baseline, stops the workers, and
generates the plots.

## Outputs

- Prepared dataset and per-worker federated shards: [benchmark/dp/data/](data/)
- Trained models, accuracy logs, and results table: [benchmark/dp/results/](results/)
- Accuracy vs. epsilon plot: [benchmark/dp/results/accuracy_vs_epsilon.png](results/accuracy_vs_epsilon.png)
- Utility cost of privacy plot: [benchmark/dp/results/privacy_cost.png](results/privacy_cost.png)

Deactivate the virtual environment when done:

```bash
deactivate
```
30 changes: 30 additions & 0 deletions benchmark/dp/scripts/benchmark_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#-------------------------------------------------------------
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#-------------------------------------------------------------


"""
Common constants
"""
import pathlib

RESULTS_DIR = pathlib.Path("benchmark", "dp", "results")
DATA_DIR = pathlib.Path("benchmark", "dp", "data")

62 changes: 62 additions & 0 deletions benchmark/dp/scripts/collect_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#-------------------------------------------------------------
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#-------------------------------------------------------------

"""
Parse per-run accuracy files into a single results.csv.

Output columns: label, epsilon, private, accuracy
"""
import pathlib, csv

from benchmark_utilities import RESULTS_DIR

rows = []

def parse_acc(path: pathlib.Path) -> float:
txt = path.read_text().strip()
# SystemDS writes a bare float.
return float(txt)

# Non-private baseline.
baseline_path = RESULTS_DIR / "acc_baseline.txt"
if baseline_path.exists():
rows.append(dict(label="baseline", epsilon="inf",
private=0, accuracy=parse_acc(baseline_path)))

# DP runs.
for eps in [0.5, 1, 4, 8]:
p = RESULTS_DIR / f"acc_eps_{eps}.txt"
if p.exists():
rows.append(dict(label=f"epsilon={eps}", epsilon=eps,
private=1, accuracy=parse_acc(p)))
else:
print(f"Warning: {p} not found - skipping")

out = RESULTS_DIR / "results.csv"
with open(out, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=["label","epsilon","private","accuracy"])
w.writeheader()
w.writerows(rows)

print(f"Wrote {out}")
for r in rows:
print(f" {r['label']:12s} acc={r['accuracy']:.4f}")

43 changes: 43 additions & 0 deletions benchmark/dp/scripts/eval.dml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#-------------------------------------------------------------
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#-------------------------------------------------------------

# eval.dml — compute binary classification accuracy on held-out test set.
# Arguments: data_dir, model_path, out_acc
data_dir = $data_dir;
model_path = $model_path;
out_acc = $out_acc;

X_test = read(data_dir + "/X_test.csv",
data_type="matrix", value_type="double", format="csv");
y_test = read(data_dir + "/y_test.csv",
data_type="matrix", value_type="double", format="csv");
w = read(model_path,
data_type="matrix", value_type="double", format="csv");

scores = X_test %*% w;
preds = (scores > 0.0); # threshold at 0 (log-odds)
correct = sum(preds == y_test);
n_test = nrow(y_test);
accuracy = correct / n_test;

print("Accuracy: " + accuracy);
write(accuracy, out_acc, format="csv");

151 changes: 151 additions & 0 deletions benchmark/dp/scripts/fedavg_dp.dml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#-------------------------------------------------------------
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#-------------------------------------------------------------

# ── fedavg_dp.dml ────────────────────────────────────────────────────────────
# Arguments (passed via -nvargs):
# data_dir : path to benchmark/data/
# epsilon : DP privacy budget ε (use 9999 for non-private baseline)
# delta : DP delta (1e-5 can be used)
# clip_norm : per-example gradient L2-norm clip bound (default 4.0) —
# sensitivity = clip_norm / n follows from this, since
# clipping each record's gradient contribution to clip_norm
# is what makes that bound actually hold.
# n_rounds : number of FedAvg rounds (default 50)
# lr : learning rate (default 0.1)
# w1_rows : rows in worker 1 shard
# w2_rows : rows in worker 2 shard
# w3_rows : rows in worker 3 shard
# w4_rows : rows in worker 4 shard
# n_features : number of features
# out : path to write final weights
# private : 1 = apply DP noise (default), 0 = non-private baseline

data_dir = $data_dir;
epsilon = $epsilon;
delta = $delta;
clip_norm = ifdef($clip_norm, 4.0);
n_rounds = ifdef($n_rounds, 50);
lr = ifdef($lr, 0.1);
private = ifdef($private, 1);
out_path = $out;

# Per-round epsilon: dp_gaussian is called once per round, and
# DPBudgetAccountant composes the cost of every release against the single
# total budget set by dp_set_budget(). Spending the full epsilon on every
# round would exhaust the budget almost immediately, so split it evenly
# across rounds instead.
round_epsilon = epsilon / n_rounds;

w1r = $w1_rows;
w2r = $w2_rows;
w3r = $w3_rows;
w4r = $w4_rows;
d = $n_features;
n = w1r + w2r + w3r + w4r;

# Sensitivity of the released (mean) gradient to a single record changing:
# with each record's contribution clipped to L2-norm <= clip_norm below,
# the sum can move by at most clip_norm, so the average moves by clip_norm/n.
sensitivity = clip_norm / n;

# ── Build federated matrix from 4 local workers ───────────────────────────
# Row ranges are 0-based [begin, end) for each partition.
r1s=0; r1e=w1r;
r2s=w1r; r2e=w1r+w2r;
r3s=w1r+w2r; r3e=w1r+w2r+w3r;
r4s=w1r+w2r+w3r; r4e=n;

X = federated(
addresses=list(
"localhost:8301/" + data_dir + "/worker1/X_train.csv",
"localhost:8302/" + data_dir + "/worker2/X_train.csv",
"localhost:8303/" + data_dir + "/worker3/X_train.csv",
"localhost:8304/" + data_dir + "/worker4/X_train.csv"),
ranges=list(
list(r1s, 0), list(r1e, d),
list(r2s, 0), list(r2e, d),
list(r3s, 0), list(r3e, d),
list(r4s, 0), list(r4e, d)));

y = federated(
addresses=list(
"localhost:8301/" + data_dir + "/worker1/y_train.csv",
"localhost:8302/" + data_dir + "/worker2/y_train.csv",
"localhost:8303/" + data_dir + "/worker3/y_train.csv",
"localhost:8304/" + data_dir + "/worker4/y_train.csv"),
ranges=list(
list(r1s, 0), list(r1e, 1),
list(r2s, 0), list(r2e, 1),
list(r3s, 0), list(r3e, 1),
list(r4s, 0), list(r4e, 1)));

# ── Initialise weights ────────────────────────────────────────────────────
w = matrix(0, rows=d, cols=1);

if (private == 1) {
eps = dp_set_budget($epsilon, $delta);
}

# ── Training rounds ───────────────────────────────────────────────────────
for (round in 1:n_rounds) {

# Forward pass — executes on federated workers.
scores = X %*% w; # (n × 1), federated
probs = 1.0 / (1.0 + exp(-scores)); # (n × 1), federated

residuals = probs - y; # (n × 1), federated

# DP noise injection — only when private=1.
if (private == 1) {
# Per-example L2-norm clipping: each record's raw gradient
# contribution is X[i,:] * residual_i, with norm
# ||X[i,:]||_2 * |residual_i|. Scaling it down to clip_norm
# whenever it exceeds that bound is what makes
# sensitivity = clip_norm / n (set above) an actual, provable
# bound instead of an arbitrary constant.
row_norms = sqrt(rowSums(X^2)); # (n × 1) ||X[i,:]||_2
contrib_norms = abs(residuals) * row_norms; # (n × 1) ||X[i,:]*residual_i||_2
clip_scale = clip_norm / pmax(contrib_norms, clip_norm); # (n × 1), in (0,1]
clipped_residuals = residuals * clip_scale; # (n × 1)

# Gradient aggregation — t(X) %*% clipped_residuals is a (d × 1)
# local sum that the coordinator collects in one federated
# aggregate instruction.
grad = t(X) %*% clipped_residuals / n; # (d × 1), LOCAL after agg, clipped

noisy_grad = dp_gaussian(grad,
"identity",
sensitivity=sensitivity,
epsilon=round_epsilon,
delta=delta);
w = w - lr * noisy_grad;
} else {
# Gradient aggregation — t(X) %*% residuals is an (d × 1) local sum
# that the coordinator collects in one federated aggregate instruction.
grad = t(X) %*% residuals / n; # (d × 1), LOCAL after agg
w = w - lr * grad;
}
}

# ── Write model weights ───────────────────────────────────────────────────
write(w, out_path, format="csv");
print("FedAvg done. epsilon=" + epsilon + " rounds=" + n_rounds);

Loading
Loading