Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
c21d55b
Benchmark the mcq medeical dataset
mina5rovic Apr 16, 2026
3014cf5
lint error correction
mina5rovic Apr 17, 2026
e785cf6
add val dataset path param
mina5rovic Apr 19, 2026
c93631c
add working local train
mina5rovic Apr 20, 2026
2e34a37
add model saving to disk arg and more debug lines
mina5rovic Apr 22, 2026
4c9c84c
add debug commands
mina5rovic Apr 24, 2026
23c733d
add cnahges to federated approach
mina5rovic Apr 24, 2026
096393c
change round 0 payload null handling
mina5rovic Apr 24, 2026
1ef7d85
chnage server max payload limit to higher number
mina5rovic Apr 26, 2026
417bfa5
fix memory reads and wait for all clients to begin
mina5rovic Apr 26, 2026
77ce9a9
add server debug logs to see why ws close session
mina5rovic Apr 26, 2026
7aef628
cover whole dataset and split data to clients
mina5rovic Apr 27, 2026
d65fb4a
add validation dataset loading changes
May 4, 2026
fe7c51a
change gpt config to use whole dadataset
mina5rovic May 4, 2026
0dc32aa
add arg for model saving location, cnahge save to saveLog, change lin…
mina5rovic May 5, 2026
5c03f40
add training optimizations
mina5rovic May 5, 2026
4ee6096
change onnx converter to be able to convert different context len
mina5rovic May 6, 2026
5b5f88c
aggregate inside of an epoch for llms
mina5rovic May 11, 2026
8d409b9
fix mem leak
mina5rovic May 11, 2026
144b751
change ligs
mina5rovic May 11, 2026
c046e9e
change model to 256
mina5rovic May 12, 2026
e89b9e0
back to 512
mina5rovic May 12, 2026
fbcca59
fix end of training
mina5rovic May 12, 2026
dfe388e
fix mem script and add logs
mina5rovic May 14, 2026
04a4769
debug memorization script
mina5rovic May 15, 2026
e2d2361
change benchmark
mina5rovic May 16, 2026
25fcf28
add client id on debug logs, and final aggregation wait between clients
mina5rovic May 23, 2026
3f6755c
Implement goldfish loss and make benchmark and memorizarion script ju…
mina5rovic May 28, 2026
d15da84
add def task for local finetuning
mina5rovic Jun 1, 2026
05cf239
fix bug
mina5rovic Jun 1, 2026
c0c41a1
bug fix
mina5rovic Jun 1, 2026
95197f7
add validation before and after aggreagtion
mina5rovic Jun 1, 2026
3e05567
fix eval script
mina5rovic Jun 2, 2026
6374b54
add save-checkpoints flag
mina5rovic Jun 2, 2026
7a815a9
decrease lr
mina5rovic Jun 2, 2026
718027c
decrease lr with flag
mina5rovic Jun 2, 2026
f829347
fix lr
mina5rovic Jun 4, 2026
7e16a96
set doSample true
mina5rovic Jun 4, 2026
aa5d8be
optimize eval script
mina5rovic Jun 4, 2026
240c6cd
add hellaswag-like eval for medMCQ gpt2
mina5rovic Jun 4, 2026
d2ae092
add top k in mem script and debug fed client
mina5rovic Jun 12, 2026
f37f5de
fix mem leak mean aggr and serialization
mina5rovic Jun 12, 2026
153d5aa
add mem fix debug logs
mina5rovic Jun 13, 2026
b46fd9b
fix server error
mina5rovic Jun 13, 2026
d73e5ad
fix leak in mem
mina5rovic Jun 13, 2026
95952cb
patch DP
mina5rovic Jun 13, 2026
d46614a
patch DP mem leak
mina5rovic Jun 13, 2026
b2fbd1e
fix unwanted dispose
mina5rovic Jun 13, 2026
1ed4bee
clean lint
mina5rovic Jul 6, 2026
2a4aa76
failing test fix
mina5rovic Jul 6, 2026
cddb271
trainer fix deadlock
mina5rovic Jul 6, 2026
2bec409
fix dispose
mina5rovic Jul 6, 2026
3061d94
fix debug line
mina5rovic Jul 6, 2026
9b40fb7
Merge branch 'develop' into gpt2-training
mina5rovic Jul 8, 2026
4ad761b
continue merge
mina5rovic Jul 8, 2026
6c53b6f
fix formatting
mina5rovic Jul 8, 2026
39f9689
fix lint
mina5rovic Jul 8, 2026
a9dfeef
fix formatting
mina5rovic Jul 8, 2026
3446d0d
fix formatting
mina5rovic Jul 8, 2026
34e16c8
add part
mina5rovic Jul 8, 2026
434a018
Update discojs/src/training/trainer.ts
mina5rovic Jul 19, 2026
b861f55
Update discojs/src/training/trainer.ts
mina5rovic Jul 19, 2026
f1d486b
Update discojs/src/training/trainer.ts
mina5rovic Jul 19, 2026
c34face
Update discojs/src/training/trainer.ts
mina5rovic Jul 19, 2026
4207877
clean debug logs
mina5rovic Jul 19, 2026
ff62115
add hash comment and fix refactor
mina5rovic Jul 19, 2026
3a921b9
make changes after first part of code review
mina5rovic Aug 27, 2026
757dab1
add avg aggregator change
mina5rovic Aug 27, 2026
3d205c4
fix: memory leak
JulienVig Aug 28, 2026
5fc85dc
fix: catch potential serialization errors
JulienVig Aug 28, 2026
6cc4fb9
fix: outdated command
JulienVig Aug 28, 2026
202a3f2
fix: only log when metric is defined
JulienVig Aug 28, 2026
526faec
fix: rm console.log
JulienVig Aug 31, 2026
b0aa7a5
fix: rm unreachable if
JulienVig Aug 31, 2026
fdd41de
fix: add validationMode arg to cli
JulienVig Aug 31, 2026
a883657
fix: propagate encoding error in catch
JulienVig Aug 31, 2026
248b928
fix: change console.log to debug
JulienVig Aug 31, 2026
adbc444
fix: remove debug statements
JulienVig Aug 31, 2026
5c92349
fix: throw error if validation path is set for non-text task
JulienVig Aug 31, 2026
8dbd156
fix: outdated doc
JulienVig Aug 31, 2026
4c4fe6e
test: aggregator tensor disposal
JulienVig Aug 31, 2026
66bce64
test: tensor disposal
JulienVig Aug 31, 2026
18d7eda
fix: rm debug statements
JulienVig Aug 31, 2026
403f61b
doc: roundIterations takes precedence over roundDuration
JulienVig Aug 31, 2026
99855ea
fix: throw early if model doesn't support training by iteration
JulienVig Aug 31, 2026
f0c9ee7
refactor: rename privacyrun to goldfish
JulienVig Aug 31, 2026
bf4c446
fix: don't fail silently
JulienVig Aug 31, 2026
f8ba291
doc: update README cli args
JulienVig Aug 31, 2026
07d4f87
fix: rm centralized fine-tuning
JulienVig Aug 31, 2026
f860805
merge: main
JulienVig Sep 1, 2026
9411190
fix: pin pnpm version
JulienVig Sep 1, 2026
4199dea
test: new event timeline
JulienVig Sep 2, 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
5 changes: 4 additions & 1 deletion .knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"entry": [
"src/benchmark_gpt.ts",
"src/hellaswag_gpt.ts",
"src/train_gpt.ts"
"src/train_gpt.ts",
"src/evaluate_finetuned_gpt2_full_answer.ts",
"src/evaluate_finetuned_gpt2.ts",
"src/measure_memorization_gpt2.ts"
]
},
"onnx-converter": {
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# TODO freeze to 22 until tfjs#8425 is merged
FROM node:22 AS builder

RUN npm i -g pnpm
RUN npm i -g pnpm@11.11.0

WORKDIR /disco

Expand Down
28 changes: 24 additions & 4 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,34 @@ Non-mandatory fields will automatically use values from the task specification.
- `testID`: (mandatory) arbitrary test ID defined by the user for the test run
- `task`: (mandatory) pre-defined task (adding a new task is described in the next section)
- `numberOfUsers`: number of users participating in the learning round
- `save`: whether to save the logs of the test run
- `host`: URL of the server to connect to, defaults to `http://localhost:8080`
- `outputPath`: path to save logs and models, defaults to `./<testID>`
- `saveLogs`: whether to save the logs of the test run
- `saveModel`: whether to save the trained model to disk
- `saveCheckpoints`: whether to save each client model after every completed round/aggregation

### Dataset arguments

- `datasetPath`: path to the training dataset
- `validationDatasetPath`: path to a separate validation dataset shared by all clients, takes precedence over `validationSplit`

### Learning hyperparameters

- `epochs`: total number of training epochs
- `roundDuration`: number of epochs per round
- `roundDuration`: number of epochs per round, ignored if `roundIterations` is set.
- `roundIterations`: number of iterations per round, takes precedence over `roundDuration`
- `batchSize`: batch size
- `validationSplit`: ratio of the validation set used for evaluation
- `validationSplit`: fraction of each client's training data used for validation, ignored when `validationDatasetPath` is set; 0 disables split-based validation
- `validationFrequency`: how often to validate. Validate the first aggregation round and every N rounds after it; defaults to every round, 0 disables validation metrics
- `validationMode`: when to run the validation: `before` model aggregation (default), `after`, or `both`
- `learningRate`: override the learning rate (GPT text tasks only)

### Goldfish loss parameters (GPT text tasks only)

- `goldfishLoss`: train with the [goldfish loss](https://arxiv.org/abs/2406.10209), which drops a subset of target tokens from the loss to mitigate memorization
- `goldfishK`: drop modulus k, a target token is dropped if hash(context) mod k == 0
- `goldfishH`: localized hash context length
- `goldfishPadTokenId`: (optional) padding token id to exclude from the goldfish loss denominator

### Aggregator parameters

Expand All @@ -57,7 +77,7 @@ Non-mandatory fields will automatically use values from the task specification.

## Adding new tasks

The CLI can be used on several pre-defined tasks: titanic, lus_covid and CIFAR10. In order
The CLI can be used on several pre-defined tasks: `cifar10`, `lus_covid`, `mnist`, `simple_face`, `tinder_dog`, `titanic` and `goldfish` (GPT-2 fine-tuning). In order
to understand how to add a new task have a look at [TASK.md](../docs/TASK.md).

Once a new task has been defined in `discojs`, it can be loaded in [data.ts](./src/data.ts) as it is already implemented for current tasks. There are currently [multiple classes](../discojs-node/src/loaders) you can use to load data using Node.js and preprocess data: loadImagesInDir, loadCSV and loadText.
Expand Down
4 changes: 4 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"benchmark_gpt": "pnpm run build && node dist/benchmark_gpt.js",
"train_gpt": "pnpm run build && node dist/train_gpt.js",
"hellaswag_gpt": "pnpm run build && node dist/hellaswag_gpt.js",
"eval_finetuned_gpt2": "pnpm run build && node dist/evaluate_finetuned_gpt2.js",
"measure_memorization_gpt2": "pnpm run build && node dist/measure_memorization_gpt2.js",
"build": "tsc --build",
"test": ": nothing"
},
Expand All @@ -17,7 +19,9 @@
"dependencies": {
"@epfml/discojs": "workspace:",
"@epfml/discojs-node": "workspace:",
"@tensorflow/tfjs": "catalog:",
"@tensorflow/tfjs-node": "catalog:",
"debug": "catalog:",
"immutable": "catalog:",
"server": "workspace:"
},
Expand Down
209 changes: 197 additions & 12 deletions cli/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { parse } from "ts-command-line-args";
import { Map, Set } from "immutable";

import type { DataType, Network, TaskProvider } from "@epfml/discojs";
import { defaultTasks } from "@epfml/discojs";
import { defaultTasks, GPT } from "@epfml/discojs";

type AggregationStrategy = "mean" | "byzantine" | "secure";

Expand All @@ -11,14 +11,56 @@ function parseAggregator(raw: string): AggregationStrategy {
else throw new Error(`Aggregator ${raw} is not supported.`);
}

type ValidationMode = "before" | "after" | "both";

function parseValidationMode(raw: string): ValidationMode {
if (raw === "before" || raw === "after" || raw === "both") return raw;
else
throw new Error(
`Validation mode ${raw} is not supported, expected "before", "after" or "both".`,
);
}

export interface BenchmarkArguments {
provider: TaskProvider<DataType, Network>;
testID: string;
numberOfUsers: number;
epochs: number;
roundDuration: number;
roundIterations?: number;
batchSize: number;
/**
* Fraction of each client's training dataset reserved for validation.
* Ignored when `validationDatasetPath` is set. A value of 0 leaves the
* client without validation data unless `validationDatasetPath` is set.
*/
validationSplit: number;
/**
* Validate the first aggregation round and every N rounds after it. If
* omitted, validation runs every round; 0 disables validation metrics. This
* only controls when validation runs, not whether its data comes from
* `validationSplit` or `validationDatasetPath`.
*/
validationFrequency?: number;
/**
* When to run validation relative to weight aggregation: "before" (on the
* local model), "after" (on the freshly aggregated global model), or "both".
* Defaults to "before".
*/
validationMode?: ValidationMode;
datasetPath?: string;
/**
* Path to a separate validation dataset. When set, this dataset is shared
* by all clients and takes precedence over `validationSplit`, including
* when `validationSplit` is non-zero.
*/
validationDatasetPath?: string;
Comment thread
mina5rovic marked this conversation as resolved.
outputPath?: string;
goldfishLoss: boolean;
goldfishK: number;
goldfishH: number;
goldfishPadTokenId?: number;
learningRate?: number;

// DP
epsilon?: number;
Expand All @@ -33,24 +75,24 @@ export interface BenchmarkArguments {
// Secure aggregator
maxShareValue?: number;

save: boolean;
saveLogs: boolean;
saveModel: boolean;
saveCheckpoints: boolean;
host: URL;
}

type BenchmarkUnsafeArguments = Omit<BenchmarkArguments, "provider"> & {
task: string;
datasetPath?: string;
validationDatasetPath?: string;
help?: boolean;
};

const argExample = "e.g. pnpm start -u 2 -e 3 # runs 2 users for 3 epochs";

const unsafeArgs = parse<BenchmarkUnsafeArguments>(
{
testID: {
type: String,
alias: "i",
description: "ID of the testcase",
},
testID: { type: String, alias: "i", description: "ID of the testcase" },
task: {
type: String,
alias: "t",
Expand All @@ -75,6 +117,12 @@ const unsafeArgs = parse<BenchmarkUnsafeArguments>(
description: "Round duration (in epochs)",
defaultValue: 2,
},
roundIterations: {
type: Number,
description:
"For GPT text tasks, aggregate every N training batches without rewinding the dataset",
optional: true,
},
batchSize: {
type: Number,
alias: "b",
Expand All @@ -84,15 +132,87 @@ const unsafeArgs = parse<BenchmarkUnsafeArguments>(
validationSplit: {
type: Number,
alias: "v",
description: "Validation dataset ratio",
description:
"Fraction of each client's training data used for validation. Ignored when --validationDatasetPath is set; 0 disables split-based validation.",
defaultValue: 0.2,
},
save: {
validationFrequency: {
type: Number,
description:
"Validate the first aggregation round and every N rounds after it. Defaults to every round; use 0 to disable validation metrics.",
optional: true,
},
validationMode: {
type: parseValidationMode,
typeLabel: "before|after|both",
description:
"When to run validation relative to weight aggregation: before (local model), after (aggregated global model), or both. Defaults to before.",
optional: true,
},
datasetPath: {
type: String,
alias: "d",
description: "Path to the dataset",
optional: true,
},
validationDatasetPath: {
type: String,
alias: "V",
description:
"Path to a separate validation dataset shared by all clients. Takes precedence over --validationSplit.",
optional: true,
},
outputPath: {
type: String,
alias: "o",
description: "Path to save logs and models. Defaults to ./<testID>",
optional: true,
},
goldfishLoss: {
type: Boolean,
description: "Use Goldfish loss for GPT text tasks",
defaultValue: false,
},
goldfishK: {
type: Number,
description:
"Goldfish loss drop modulus k. Drops target if hash(context) mod k == 0",
defaultValue: 4,
},
goldfishH: {
type: Number,
description: "Goldfish loss localized hash context length",
defaultValue: 13,
},
goldfishPadTokenId: {
type: Number,
description:
"Optional padding token id to exclude from Goldfish loss denominator",
optional: true,
},
learningRate: {
type: Number,
description: "Override learning rate for GPT text tasks",
optional: true,
},
saveLogs: {
type: Boolean,
alias: "s",
description: "Save logs of benchmark",
defaultValue: false,
},
saveModel: {
type: Boolean,
alias: "m",
description: "Save trained model to disk",
defaultValue: false,
},
saveCheckpoints: {
type: Boolean,
description:
"Save each client model after every completed round/aggregation",
defaultValue: false,
},
host: {
type: (raw: string) => new URL(raw),
typeLabel: "URL",
Expand Down Expand Up @@ -172,17 +292,18 @@ const unsafeArgs = parse<BenchmarkUnsafeArguments>(

const supportedTasks = Map(
await Promise.all(
Set.of<TaskProvider<"image" | "tabular", Network>>(
Set.of<TaskProvider<"image" | "tabular" | "text", Network>>(
defaultTasks.cifar10,
defaultTasks.lusCovid,
defaultTasks.titanic,
defaultTasks.tinderDog,
defaultTasks.mnist,
defaultTasks.goldfish,
).map(
async (t) =>
[(await t.getTask()).id, t] as [
string,
TaskProvider<"image" | "tabular", Network>,
TaskProvider<"image" | "tabular" | "text", Network>,
],
),
),
Expand All @@ -204,6 +325,46 @@ export const args: BenchmarkArguments = {
task.trainingInformation.roundDuration = unsafeArgs.roundDuration;
task.trainingInformation.epochs = unsafeArgs.epochs;
task.trainingInformation.validationSplit = unsafeArgs.validationSplit;
task.trainingInformation.roundIterations = unsafeArgs.roundIterations;
task.trainingInformation.validationFrequency =
unsafeArgs.validationFrequency;
task.trainingInformation.validationMode = unsafeArgs.validationMode;

if (unsafeArgs.goldfishLoss) {
if (
task.dataType !== "text" ||
task.trainingInformation.tensorBackend !== "gpt"
)
throw new Error("Goldfish loss is only supported for GPT text tasks");
if (!Number.isInteger(unsafeArgs.goldfishK) || unsafeArgs.goldfishK < 1)
throw new Error("goldfishK must be a positive integer");
if (!Number.isInteger(unsafeArgs.goldfishH) || unsafeArgs.goldfishH < 1)
throw new Error("goldfishH must be a positive integer");

task.trainingInformation.goldfishLoss = {
enabled: true,
k: unsafeArgs.goldfishK,
h: unsafeArgs.goldfishH,
padTokenId: unsafeArgs.goldfishPadTokenId,
};
}

if (unsafeArgs.learningRate !== undefined) {
if (
task.dataType !== "text" ||
task.trainingInformation.tensorBackend !== "gpt"
)
throw new Error(
"learningRate override is only supported for GPT text tasks",
);
if (
!Number.isFinite(unsafeArgs.learningRate) ||
unsafeArgs.learningRate <= 0
)
throw new Error("learningRate must be a positive finite number");

task.trainingInformation.learningRate = unsafeArgs.learningRate;
}

const { aggregator, clippingRadius, maxIterations, beta, maxShareValue } =
unsafeArgs;
Expand Down Expand Up @@ -277,6 +438,30 @@ export const args: BenchmarkArguments = {

return task;
},
modelCard: provider.modelCard,
modelCard: {
card: provider.modelCard.card,
async getModel() {
const model = await provider.modelCard.getModel();

if (unsafeArgs.learningRate !== undefined) {
if (!(model instanceof GPT))
throw new Error(
"learningRate override is only supported for GPT models",
);
if (
!Number.isFinite(unsafeArgs.learningRate) ||
unsafeArgs.learningRate <= 0
)
throw new Error("learningRate must be a positive finite number");

model.setLearningRate(unsafeArgs.learningRate);
console.log(
`Overriding GPT learning rate to ${unsafeArgs.learningRate}`,
);
}

return model;
},
},
},
};
Loading