Skip to main content

Qora-FL

Quorum-Oriented Robust Aggregation for Federated Learning

Crates.io PyPI DOI License

Problem

Federated learning systems are fragile under adversarial or faulty clients. Standard aggregation methods silently fail under model poisoning, gradient manipulation, or non-IID drift.

Qora-FL provides Rust-backed robust aggregation methods for federated learning. Each method has explicit assumptions and tolerance conditions, documented below. The included experiments evaluate selected attacks with up to 30% adversarial clients.

[!IMPORTANT] Qora-FL is an experimental robust-aggregation toolkit. Its algorithms are implemented and tested, but the project has not received an independent security review. Robustness depends on each method's stated assumptions, threat model, and correct configuration.

Currently implemented

Behavior available through tested public paths:

  • FedAvg baseline, with optional sample-count weighting
  • Coordinate-wise median
  • Coordinate-wise trimmed mean
  • Krum and Multi-Krum, with their preconditions enforced
  • Input validation: dimensions, non-finite rejection, client-ID alignment
  • Krum quorum enforcement (n >= 2f + 3)
  • Multi-Krum selection-bound enforcement (1 <= m <= n - 2f - 2)
  • Reputation tracking and participation gating
  • Optional norm-bound filtering, opt-in and off by default
  • Fail-closed behavior when filtering rejects every client
  • Reputation numeric invariants (scores finite and within [0, 1])
  • Rust API and Python bindings
  • Flower-compatible strategy adapter
  • Rust CI across three operating systems; Python CI including the adapter

Experimental or planned

Present in the codebase but not wired into the normal aggregation path, or not yet implemented:

  • Reputation-weighted robust aggregation (cubic influence weighting exists as a utility only)
  • Adaptive or percentile-derived norm bounds (today the caller picks a fixed bound)
  • Audit records in Python. The Rust API returns a typed, versioned record from aggregate_with_audit; the bindings do not expose it yet, so that a binding design is not frozen while the schema is still experimental.
  • Cross-platform bit-identical end-to-end aggregation
  • Independent security review
  • Real-world deployment validation
  • Additional attack and dataset evaluations

Architecture

Clients produce model updates
        │
        ▼
┌───────────────────────────┐
│   Qora Robust Aggregator  │
│                           │
│  ┌─────────────────────┐  │
│  │ Trimmed Mean        │  │
│  │ Median              │  │
│  │ Krum   (n ≥ 2f+3)  │  │
│  │ FedAvg  (baseline)  │  │
│  └─────────────────────┘  │
└─────────────┬─────────────┘
              │
   Deviation signal (not trust)
              │
              ▼
     ┌─────────────────┐
     │    Reputation    │
     │     Manager      │
     │                  │
     │  Scores / Gates  │
     │  / Weighting     │
     └────────┬────────┘
              │
              ▼
     Verified Global Update
flowchart LR
    C1[Client 1] --> U[Client Updates]
    C2[Client 2] --> U
    Cn[Client N] --> U

    U --> A[Qora Aggregator]

    A -->|Deviation Signal| R[Reputation Manager]
    R -->|Scores / Gates| A

    A --> G[Aggregated Model]

    style A fill:#f3f6ff,stroke:#4a6cf7,stroke-width:2px
    style R fill:#fff7e6,stroke:#f59e0b,stroke-width:2px

Determinism

Qora-FL uses integer arithmetic for Krum distance and score calculations after BFP-16 encoding (block floating-point, 16-bit mantissas, per-vector shared exponent). Identical BFP-encoded inputs therefore produce deterministic Krum scores and rankings.

Full cross-platform bit-identical aggregation is not currently guaranteed. The pipeline is only partly integer:

Stage Arithmetic
BFP-16 encoding floating-point (log2, scaling, rounding)
Krum distances and scores integer (i32 shifts, i64 accumulation, saturating)
Krum result the selected original f32 update
Multi-Krum result mean of selected original updates, in f32
Median, trimmed mean, FedAvg floating-point throughout

So the determinism claim covers the scoring stage, not the end-to-end result. Selection rankings are reproducible; the returned values are f32 and inherit whatever the encoding and averaging stages do.

Reputation

Reputation is not trust. It is a deviation-derived signal measuring how far a client's update sits from the aggregate that round.

What it does today:

  • Tracks scores across rounds, persisted via JSON serialization
  • Gates participation: clients below the configured threshold are excluded before aggregation
  • Fails closed: if every identified client is below the threshold, the round returns AllUpdatesRejected rather than reinstating them
  • Maintains numeric invariants: scores are finite and within [0, 1], and invalid mutation inputs are rejected rather than clamped

Norm-bound filtering does not feed reputation: a client excluded by the bound is left out of the round and its score is untouched.

What it does not do yet: cubic influence weighting (min(rep^3, 0.8)) and its cap are available as utilities, and a caller may use those values directly. The primary aggregation path does not currently consume them, so the cap does not provide protection for the default aggregation workflow. Reputation-weighted robust aggregation is roadmap work.

Details that affect configuration:

  • Unknown clients start at the default score of 0.5. A ban threshold above 0.5 therefore rejects every first-round participant -- which now fails the round rather than passing it through ungated.
  • Through the Flower adapter, reputation is computed from the complete flattened model, and each client receives at most one update per round. The adapter performs no gating of its own.

Quick Start (Python + Flower)

Flower is an optional extra. The core bindings work without it: import qora, ByzantineAggregator, and ReputationManager all function with Flower absent. Only qora.QoraStrategy requires it, and importing it without Flower installed raises an ImportError naming the extra to install.

# Quote the extra: some shells (zsh, fish) treat brackets as globs.
pip install "qora-fl[flower]"

Supported range flwr>=1.5,<2, upper-bounded because the adapter targets Flower's 1.x strategy interface. CI tests both ends explicitly: 1.5.0 and 1.32.1. Versions in between are covered by the declared promise but are not individually exercised.

import flwr as fl
from qora import QoraStrategy

strategy = QoraStrategy(
    aggregation_method="trimmed_mean",
    trim_fraction=0.2,
    min_fit_clients=5,
    # norm_bound=None by default: no filtering, no norm computation.
)

fl.server.start_server(
    server_address="0.0.0.0:8080",
    config=fl.server.ServerConfig(num_rounds=10),
    strategy=strategy,
)

QoraStrategy inherits from FedAvg, so standard Flower parameters (fraction_fit, min_fit_clients, initial_parameters, accept_failures, fit_metrics_aggregation_fn) are accepted. It is a compatibility adapter, not a behavioral replacement for every Flower built-in strategy.

What the adapter does:

  • One client result becomes one complete Rust update. Each client's model is flattened into a single update, aggregated in one call, then split back into the original layer shapes. Aggregating layer by layer would let a selection method pick a different client per layer and return a model no client submitted.
  • Validates the complete model structure against the first client as reference: layer count, per-layer shape, floating-point dtype, and finiteness. A malformed successful result raises ValueError naming the client and layer rather than being dropped -- discarding it would change the round's effective adversarial fraction.
  • Aggregates at float32. float64 input is accepted and converted; its extra precision is not preserved. Integer parameter arrays are rejected rather than silently coerced into trainable floats.
  • FedAvg weights by num_examples. Robust methods (median, trimmed mean, Krum, Multi-Krum) treat each accepted client update as one participant -- num_examples is never reinterpreted as an algorithmic weight there, since an attacker claiming a large sample count would otherwise gain proportional influence over a median.
  • Honors accept_failures. Reported failures refuse the round when it is False, returning (None, {}) without invoking Rust, moving reputation, or calling the metrics callback.
  • Aggregates metrics only through fit_metrics_aggregation_fn. With none configured it returns {}. Reputation is available through QoraStrategy.get_reputation(client_id).
  • Performs no gating of its own. Reputation gating and updates happen once, in the Rust aggregator.
  • Enables norm-bound filtering only when norm_bound is passed, and applies it to the complete flattened model rather than to individual layers.

Python (Standalone)

pip install qora-fl
import numpy as np
from qora import ByzantineAggregator

agg = ByzantineAggregator("trimmed_mean", 0.3)

# 7 honest clients + 3 Byzantine attackers
updates = [np.array([[1.0, 2.0]], dtype=np.float32) for _ in range(7)]
updates += [np.array([[100.0, 200.0]], dtype=np.float32) for _ in range(3)]

result = agg.aggregate(updates)
# Result is [1.0, 2.0] -- attackers rejected

Norm-bound filtering

Opt-in, and off by default. Without norm_bound, no norm is computed and behavior is unchanged.

from qora import ByzantineAggregator

agg = ByzantineAggregator("median", 0.0, norm_bound=10.0)
print(agg.norm_bound)   # 10.0, or None when disabled
  • None disables it. A supplied bound must be finite and strictly positive; zero, negative, NaN, and infinite bounds raise ValueError rather than being clamped.
  • The norm is computed in f64 and compared inclusively -- a norm exactly equal to the bound participates.
  • A rejected update takes its client ID and its FedAvg sample weight with it, so a rejected weight never reaches the numerator or the denominator.
  • A rejected update does not affect reputation. A large norm is a policy violation, not proof of malicious intent; this filter does not claim to detect Byzantine behavior.
  • If every update is rejected -- by reputation gating, by the bound, or by a mixture -- the round raises ValueError rather than aggregating a partial or best-effort result.
  • Method quorum is checked after filtering, so filtering can shrink a valid cohort into one Krum or Multi-Krum refuses. That is reported, not repaired.

Through the Flower adapter, pass norm_bound= to QoraStrategy. The bound then applies to each client's complete flattened model rather than to individual layers.

Audit records are Rust-only in 0.4.0

The Rust API returns a typed, versioned AggregationAuditEntry from aggregate_with_audit, recording one decision per submitted update with typed rejection reasons. The Python bindings do not expose it. Mirroring the schema as a hierarchy of Python classes would freeze a binding design while the schema is still experimental; a later release can return the serialized shape as a dictionary instead.

What Python does expose is the configuration (norm_bound) and the behavior it produces: an over-bound update is excluded, and a round that rejects everything raises ValueError. The per-client reasons behind that error are not available from Python yet.

Reputation Tracking

from qora import ReputationManager

rep = ReputationManager(ban_threshold=0.2)
rep.reward("hospital_A", 0.02)
rep.penalize("hospital_bad", 0.08)

print(rep.is_banned("hospital_bad"))  # True after enough penalties
print(rep.active_clients())           # Only non-banned clients

# Persist between server restarts
json_state = rep.to_json()
rep2 = ReputationManager.from_json(json_state, ban_threshold=0.2)

Rust

cargo add qora-fl
use qora_fl::{ByzantineAggregator, AggregationMethod};
use ndarray::array;

let mut agg = ByzantineAggregator::new(AggregationMethod::TrimmedMean, 0.3);

let updates = vec![
    array![[1.0, 2.0]],   // Honest
    array![[1.1, 2.1]],   // Honest
    array![[0.9, 1.9]],   // Honest
    array![[100.0, 200.0]], // Byzantine attacker
];

let result = agg.aggregate(&updates, None).unwrap();
// Result is close to [1.0, 2.0], attacker ignored

Individual Functions

use qora_fl::{trimmed_mean, median, fedavg};
use ndarray::array;

let updates = vec![array![[1.0]], array![[2.0]], array![[3.0]]];

let tm = trimmed_mean(&updates, 0.2).unwrap();
let med = median(&updates).unwrap();
let avg = fedavg(&updates, None).unwrap();

Aggregation methods and their assumptions

n is the number of accepted client updates; f is the configured Byzantine bound; m is the number of Multi-Krum candidates selected.

Method Enforced precondition Assumption for robustness
TrimmedMean 0.0 <= trim_fraction <= 0.5, and at least one value must survive trimming Depends on trim fraction, adversarial proportion, attack model, and the distribution of honest updates
Median none beyond shared input validation Strictly fewer than 50% adversarial values per coordinate
Krum n >= 2f + 3 Blanchard et al. (2017)
MultiKrum n >= 2f + 3 and 1 <= m <= n - 2f - 2 Blanchard et al. (2017)
FedAvg weights, when supplied, must be finite and non-negative with a positive total None. Conventional sample-weighted baseline; provides no Byzantine robustness by itself

Trimmed mean removes the configured fraction of the smallest and largest values independently in each coordinate. A trim fraction is not an attacker percentage: trim_fraction = 0.2 does not establish tolerance of 30% malicious clients, and no such universal guarantee is claimed here.

Median requires an honest majority per coordinate. Exactly 50% adversarial is not strictly fewer than half, so "tolerates 50%" would be wrong.

Krum rejects configurations violating n >= 2f + 3 with InsufficientQuorum. There is no best-effort path below quorum.

Multi-Krum rejects an explicit m outside 1..=n - 2f - 2 with InvalidMultiKrumSelection. When m is omitted, Qora-FL uses min(3, n - 2f - 2); explicit unsafe values are rejected rather than silently reduced.

Experiments

Qora-FL includes an attack-evaluation script for comparing aggregation methods across selected attacks and adversarial-client fractions. The repository does not currently publish verified benchmark results from a pinned dataset artifact and repeated multi-seed evaluation. Run the included experiment to evaluate the methods in your own environment.

See the root README for the full configuration and its limitations.

# Aggregation overhead (Python)
python examples/benchmark_overhead.py

# MNIST poisoning: FedAvg vs Qora-FL under 30% attack
pip install matplotlib scikit-learn
python examples/mnist_poisoning_demo.py

# Criterion benchmarks (Rust)
cargo bench
# Rust examples
cargo run --example quickstart
cargo run --example compare_methods

Validation status

Qora-FL is currently validated through:

  • the Rust unit and integration test suite
  • Python binding and Flower adapter tests
  • cross-platform Rust CI (Linux, macOS, Windows) and Python CI
  • Flower compatibility tests at both ends of the supported version range
  • algorithm preconditions enforced in code and covered by tests
  • the included examples and benchmarks, under their documented configurations

This is engineering validation. It is not an independent security review, and it does not establish robustness outside each algorithm's stated assumptions. See SECURITY.md for the boundaries.

Background

The aggregation algorithms in Qora-FL (trimmed mean, coordinate-wise median, Krum, Multi-Krum) are standard Byzantine-robust methods from the federated learning literature [1][2].

Project history

Qora-FL grew out of aggregation and distributed-trust experiments explored in the earlier QRES project (CavinKrenik/QRES_RaaS, historical work). Design choices here -- deviation-derived reputation, the BFP-16 scoring path, adaptive trim fraction, and the influence cap -- were informed by those earlier experiments and long-horizon simulations.

Qora-FL is a separate implementation with its own tests, experiments, and assumptions. Results or guarantees from QRES do not automatically apply to Qora-FL, and none of the evidence in this repository is inherited from it.

Design Philosophy

Qora-FL treats aggregation as a decision process, not a statistical convenience. The system favors explicit constraints, observable signals, and predictable failure modes over opaque optimization.

Roadmap

  • Weighted robust aggregation (reputation-scaled trimmed mean)
  • Adaptive trim selection based on detected attack intensity
  • TensorFlow Federated adapter
  • Formal verification experiments for the integer scoring path

License

Licensed under either of Apache License, Version 2.0 or MIT License at your option.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

qora_fl-0.4.0.tar.gz (195.4 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

qora_fl-0.4.0-cp38-abi3-win_amd64.whl (342.5 kB view details)

Uploaded CPython 3.8+Windows x86-64

qora_fl-0.4.0-cp38-abi3-manylinux_2_34_x86_64.whl (525.1 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.34+ x86-64

qora_fl-0.4.0-cp38-abi3-macosx_11_0_arm64.whl (453.9 kB view details)

Uploaded CPython 3.8+macOS 11.0+ ARM64

File details

Details for the file qora_fl-0.4.0.tar.gz.

File metadata

  • Download URL: qora_fl-0.4.0.tar.gz
  • Upload date:
  • Size: 195.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for qora_fl-0.4.0.tar.gz
Algorithm Hash digest
SHA256 203b9d378d6013afd087c916c4cc17b95e33f78ce59a83f5a395bca0af24ee23
MD5 3bcf58b8832602878bde436c1f0036eb
BLAKE2b-256 529a73d5d5bb4557ad7763a380094f3a4fcda045a7c46820d78bbbda7fa3797e

See more details on using hashes here.

Provenance

The following attestation bundles were made for qora_fl-0.4.0.tar.gz:

Publisher: release.yml on CavinKrenik/qora-fl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file qora_fl-0.4.0-cp38-abi3-win_amd64.whl.

File metadata

  • Download URL: qora_fl-0.4.0-cp38-abi3-win_amd64.whl
  • Upload date:
  • Size: 342.5 kB
  • Tags: CPython 3.8+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for qora_fl-0.4.0-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 366326eda7a655a37e8e4ea97f1867fb75434d67534ab467f7f4c5c7c41138a7
MD5 b68082a1ecec924d285566c6a1d0eb2d
BLAKE2b-256 f194cb76c7aba6658d5a7f290e35542e5a2b490ba49c099caec88ad9d837bc46

See more details on using hashes here.

Provenance

The following attestation bundles were made for qora_fl-0.4.0-cp38-abi3-win_amd64.whl:

Publisher: release.yml on CavinKrenik/qora-fl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file qora_fl-0.4.0-cp38-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for qora_fl-0.4.0-cp38-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 f53829105227a23349f0888188f7cf3bb64c98256fa42629a6c9bcde42e03b7c
MD5 5858c27dbbdb3b45d324c612f69071f4
BLAKE2b-256 4b2f45a9dc4bbc85af2fced767ba2eae30131b79b4c3a2e93df52d7e65cecaea

See more details on using hashes here.

Provenance

The following attestation bundles were made for qora_fl-0.4.0-cp38-abi3-manylinux_2_34_x86_64.whl:

Publisher: release.yml on CavinKrenik/qora-fl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file qora_fl-0.4.0-cp38-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for qora_fl-0.4.0-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 8215390b81cc18cc9942490ae7373737bb880557151ffd99c833a14e19ac2971
MD5 4824af9fe757abfa24e1617cc5d13f95
BLAKE2b-256 2dfefa8f9a0500f89b94b2ed90defec2c5a25a2d64f5b7bfb99d221f7103b997

See more details on using hashes here.

Provenance

The following attestation bundles were made for qora_fl-0.4.0-cp38-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on CavinKrenik/qora-fl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.4.0 This release

4 files

0.3.1

4 files

0.3.0

4 files

0.2.0

1 file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page