Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches: [main]
pull_request:
branches: [main, cmd_line]
branches: [main, cmd_line, tests_and_CI]
workflow_dispatch:

concurrency:
Expand Down
5 changes: 4 additions & 1 deletion flcore/models/xgb/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,11 @@ def fit(
}

# If multiclass objective, ensure num_class is correctly set from local data
n_classes = len(np.unique(self.local_data['y_train']))
if self.xgb_params.get("objective") == "binary:logistic" and n_classes > 2:
self.xgb_params["objective"] = "multi:softmax"
self.xgb_params["eval_metric"] = "mlogloss"
if self.xgb_params.get("objective", "").startswith("multi"):
n_classes = len(np.unique(self.local_data['y_train']))
if n_classes >= 2:
self.xgb_params["num_class"] = n_classes
print(f"[Client] Multiclass detected: num_class={n_classes}")
Expand Down
41 changes: 37 additions & 4 deletions flcore/models/xgb/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import xgboost as xgb

from flwr.common import (
FitIns,
ArrayRecord,
ConfigRecord,
Message,
Expand Down Expand Up @@ -174,6 +175,34 @@ def initialize_parameters(self, client_manager):
empty = np.frombuffer(b"", dtype=np.uint8)
return ndarrays_to_parameters([empty])

# ------------------------------------------------------
# CONFIGURE FIT
# ------------------------------------------------------

def configure_fit(self, server_round, parameters, client_manager):
"""Send each client its final learning rate: eta / number of clients."""
# Without scaling, the training becomes unstable with more clients
instructions = super().configure_fit(server_round, parameters, client_manager)
num_clients = len(instructions)
if num_clients == 0:
return instructions

base_eta = float(self.xgb_params.get("eta", 0.1))
final_eta = base_eta / num_clients
print(
f"[Round {server_round}] eta: {base_eta} / {num_clients} clients "
f"= {final_eta}"
)

# One FitIns per client, each carrying its own final eta
return [
(
client,
FitIns(fit_ins.parameters, {**fit_ins.config, "eta": final_eta}),
)
for client, fit_ins in instructions
]

# ------------------------------------------------------
# AGGREGATE FIT (CRITICAL FIX)
# ------------------------------------------------------
Expand Down Expand Up @@ -233,8 +262,12 @@ def aggregate_fit(
total_examples = sum(fit_res.num_examples for _, fit_res in results)

for _, fit_res in results:
if "n_out" in fit_res.metrics:
self.xgb_params["num_class"] = int(fit_res.metrics["n_out"])
n_out = int(fit_res.metrics.get("n_out", 0))
if self.xgb_params["objective"] == "binary:logistic" and n_out > 2:
self.xgb_params["objective"] = "multi:softmax"
self.xgb_params["eval_metric"] = "mlogloss"
if self.xgb_params["objective"].startswith("multi"):
self.xgb_params["num_class"] = n_out

for key, value in fit_res.metrics.items():
if not isinstance(value, (int, float)):
Expand Down Expand Up @@ -361,7 +394,7 @@ def get_server_and_strategy(config: dict) -> FedXgbFullyFederated:
"colsample_bytree": 0.8,
}

if task == "binary":
if task in ("binary", "classification"):
xgb_params["objective"] = "binary:logistic"
xgb_params["eval_metric"] = "auc"

Expand All @@ -377,7 +410,7 @@ def get_server_and_strategy(config: dict) -> FedXgbFullyFederated:
xgb_params["eval_metric"] = "rmse"

train_method = xgb_config.get("train_method", "bagging") # 'bagging' or 'cyclic'
num_local_rounds = xgb_config.get("tree_num", 100) // config.get("num_rounds", 10) # Trees per round
num_local_rounds = 1 # Always train one tree per round


print("\n" + "=" * 60)
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ scikit-learn==1.3.2
torch==2.3.1
torchmetrics==1.4.0
tqdm==4.65.0
xgboost==1.7.5
xgboost==2.1.4
pdfkit==1.0.0
scikit-survival==0.22.2
fpboost==0.1.0
Expand Down
3 changes: 1 addition & 2 deletions tests/test_federated_training.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ def case(model, scenario, client_extra=(), id_suffix="", bug=None):
"lsvc/svm, but SGDClassifier needs one intercept per class (multiclass)"
)
SVR_KERNEL_BUG = "SVR with a non-linear kernel has no fit_intercept / coef_"
XGB_BUG = "xgb server needs iteration_indptr (XGBoost >= 2.0, 1.7.5 is pinned)"
SURVIVAL_TIME_BUG = "survival client fit() uses `time` without importing it"

CASES = []
Expand Down Expand Up @@ -98,7 +97,7 @@ def case(model, scenario, client_extra=(), id_suffix="", bug=None):
case("weighted_random_forest", "binary", bug=wrf_bug),
case("weighted_random_forest", "multiclass", bug=wrf_bug),
]
CASES += [case("xgb", s, bug=XGB_BUG) for s in ("binary", "multiclass", "regression")]
CASES += [case("xgb", s) for s in ("binary", "multiclass", "regression")]
CASES += [case("nn", s) for s in ("binary", "multiclass", "regression")]

# Survival
Expand Down
Loading