ML Adapter¶
pyfe4ai.ml runs SIFE and MCFE schemes for encrypted linear inference
and federated gradient aggregation behind one wrapper:
from pyfe4ai.ml import FESchemeWrapper, aggregate_gradients, encrypt_gradient
The functions and classes below are importable from pyfe4ai.ml; they
are defined in its submodules wrapper (the wrapper; its schemes are
those of pyfe4ai.core.registry), serialization (the encrypted containers), gradients
(federated aggregation), threshold (the decryption servers of the
threshold MCFE) and inference (encrypted linear inference).
Threshold MCFE¶
For mcfe/ring_lwe_threshold the wrapper also runs the decryption servers
(one trust domain, for experiments). prepare_aggregation()
and aggregate_gradients() take servers=
(default: the first t of the public parameters’ lst_sid) and give each
enrolled server its label key share for the round and, once per weights, its
weight key share. The messages of a deployment are those of the separate
steps: partial_decrypt_gradients() at each
server and combine_partial_decryptions() at
the combiner, exchanging ThresholdPartial
objects:
prepared = prepare_aggregation(wrapper, weights, shape=(3, 7), label="round-a",
servers=["sid_1", "sid_2"])
received = {nid: EncryptedGradient.from_dict(d) for nid, d in client_messages.items()}
wire = {sid: partial_decrypt_gradients(received, wrapper, prepared, server=sid).to_dict()
for sid in prepared.servers}
total = combine_partial_decryptions(
{sid: ThresholdPartial.from_dict(d) for sid, d in wire.items()}, wrapper, prepared)
Every partial decryption carries the sharing_id of the server’s key
share. Keep it when partials (or key shares) are stored or transmitted by
other means: the combiner refuses partials of two sharings of one key
(MixedSharingsError; prepare
the round again), but partials without an id combine only with each other,
so dropping it on every side would let two sharings be mixed unnoticed.
examples/mcfe_ring_lwe_threshold_demo.py runs both the scheme classes and
the adapter.
API¶
ML framework adapter layer for PyFE4AI.
Provides two high-level APIs that hide FE complexity from ML engineers:
- FL aggregation (
eta=1, ndarray helpers): Encrypt per-element gradients from n clients, aggregate with weights.
>>> keygen_cfg = {"eta": 1, "n": 2, "lst_nid": ["c0", "c1"], "bound_x": 1000, "bound_y": 10} >>> wrapper = FESchemeWrapper("mcfe", "lwe", keygen_cfg, precision=2) >>> enc0 = encrypt_gradient(grad0, wrapper, nid="c0", label="round-1") >>> enc1 = encrypt_gradient(grad1, wrapper, nid="c1", label="round-1") >>> result = aggregate_gradients( ... {"c0": enc0, "c1": enc1}, wrapper, {"c0": 1, "c1": 2}, label="round-1")
Extra entries of the crypto objects’ config go to
crypto_options, e.g.FESchemeWrapper("mcfe", "ring_lwe", keygen_cfg, crypto_options={"ct_format": "binary"}).- Encrypted inference (
eta=feature_dim, vector mode): Encrypt a feature vector, compute
⟨x, w⟩without revealing x.>>> wrapper = FESchemeWrapper("sife", "lwe", {"eta": 3, "bound_x": 10, "bound_y": 10}) >>> enc = encrypt_features(feature_vec, wrapper) >>> score = compute_linear(enc, wrapper, model_weights)
PyTorch is optional — functions accept both numpy.ndarray and
torch.Tensor inputs.
Threshold MCFE (mcfe/ring_lwe_threshold): the wrapper also runs the
s decryption servers. prepare_aggregation() (or
aggregate_gradients() without prepared) gives each enrolled server
its label key share of the round and, once per weights, its weight key share
(the scheme’s split key shares, its default); aggregate_gradients()
then has the servers decrypt partially and combines their answers. The two
steps are also separate functions – partial_decrypt_gradients() per
server, combine_partial_decryptions() at the combiner – whose
ThresholdPartial serialises like EncryptedGradient
(binary ciphertexts and partial decryptions, the scheme’s default format, as
base64).
Single trust domain. A FESchemeWrapper runs the key generator
itself, so it holds the master secret key and plays every party (clients,
key authority, decryptor). It is meant for experiments and for code that
already trusts one process with everything; a deployment keeps the master
key away from the aggregator. The wrapper applies a default policy that a
key holder would otherwise have to enforce: aggregate_gradients()
needs a ciphertext and a non-zero weight for every enrolled client (pass
allow_partial=True to relax this), each client encrypts at most once per
label, and each label is aggregated with one weight vector only. These
checks stop the obvious ways of isolating one client, not all of them: a
single aggregate with very unequal weights (e.g. (W, 1) with W above
twice the largest gradient entry) separates two clients’ gradients, as with
any inner-product FE; choosing the weights is the key holder’s
responsibility, and FESchemeWrapper.get_decryption_keys() issues keys
for any weights.
The functions and classes are defined in the submodules wrapper,
serialization, gradients, threshold and inference, and
imported from here; pyfe4ai.ml.quantization holds the float /
integer conversion.
- class pyfe4ai.ml.FESchemeWrapper(scheme_type, variant, keygen_config, *, precision=0, qmode='decimal', crypto_options=None)[source]¶
Bases:
objectUnified wrapper around any PyFE4AI scheme.
Manages the KeyGenerator lifecycle (setup + key distribution) and provides a consistent encrypt / decrypt surface regardless of the underlying scheme family. The wrapper runs the key generator itself and so forms a single trust domain (see
pyfe4ai.ml).Every registered
(scheme_type, variant)pair is accepted: the schemes ofpyfe4ai.core.registry(for example("sife", "ddh"),("mcfe", "lwe_threshold")), with their review status instatus. The FL helpers are narrower:encrypt_gradient()andaggregate_gradients()refuse MIFE (no labels, so a key holder could combine a client’s ciphertexts across elements or rounds), andaggregate_gradients()andcompute_linear()refuse the*_decentralizedand*_thresholdvariants, whose decryption is a multi-party protocol; use the scheme classes directly for those. The exception ismcfe/ring_lwe_threshold(capabilityml_servers), whosesdecryption servers the wrapper runs (see the module docstring):aggregate_gradients()andprepare_aggregation()take it, with the enrolledservers.A wrapper can be pickled and copied (
multiprocessing, Ray): the copy carries the(nid, label)pairs used so far and refuses them as the original does. Copies do not learn of labels used later by the original or by other copies, so encrypt each client’s gradient for a label through one wrapper. A pickled wrapper carries everything it holds, the master secret key and every client’s key included: do not ship it to workers that should only encrypt. Give such a worker its client’s own crypto object instead – the scheme class built fromwrapper.ppandwrapper.kg.get_private_keys(nid)(e.g.MCFERingLWE({"id": nid, "precision": p, "keys": {"pp": pp, "sk": sk}})formcfe/ring_lwe), whoseencrypt_lst_ndarraythe worker calls.- Parameters:
scheme_type (
str) –"sife","mife","mcfe"or"quadratic".variant (
str) – Variant name within the family, e.g."ddh","lwe","damgard","paillier","ring_lwe"or"lwe_threshold"(seepyfe4ai.list_schemes).keygen_config (
dict) – Configuration dict for the KeyGenerator (sec_param,eta,n,bound_x,security_profile, etc.).group="ristretto255"selects the elliptic-curve instantiation of theddhvariants (ddh_ristretto255).precision (
int, default:0) – Quantization precision (0: no quantization).qmode (
str, default:'decimal') – Quantization mode,"decimal"or"binary".crypto_options (
dict|None, default:None) – Extra entries of the crypto objects’ config, e.g.{"ct_format": "binary"}for the Ring-LWE MIFE / MCFE.
- Raises:
ValueError – If
(scheme_type, variant)is not registered.
- status¶
Review status of the scheme (see
pyfe4ai.core.status).
- class pyfe4ai.ml.PreparedAggregation(label, sid, fusion_weight, weight_scale, n_chunks, dk, allow_partial, servers=None)[source]¶
Bases:
objectA functional key derived before the round (
prepare_aggregation()); for a threshold MCFE the key shares of the enrolledservers(dk = {sid: key share}) andsidthe combiner.dkis key material (a functional key, or the servers’ label key shares with their MAC and noise keys): keep it where the wrapper is, and do not send, log or store it elsewhere.- Parameters:
- class pyfe4ai.ml.EncryptedGradient(ciphertexts, shape, precision, mode, nid)[source]¶
Bases:
objectEncrypted gradient array for FL aggregation.
Produced by
encrypt_gradient().- ciphertexts: list¶
One entry per layer, an object array of per-element ciphertexts or, for the packed Ring-LWE MCFE schemes, a dict
{"shape", "first_chunk", "chunks"}(seepyfe4ai.schemes.mcfe.ring_lwe.MCFERingLWE.encrypt_lst_ndarray()).
- to_dict()[source]¶
Serialise for network transfer (e.g. federated learning).
A per-element layer is written as
{"shape", "data"}with one entry per index, a packed layer as{"packed": {"shape", "first_chunk", "chunks"}}. Chunk fields holding bytes (binary ciphertexts, e.g. the default format ofmcfe/ring_lwe_threshold) are written as base64 strings and named in the layer’s"base64"list;from_dict()decodes them.
- MAX_ELEMENTS = 10000000¶
Largest number of elements
from_dict()accepts per array.
- MODES = ('decimal', 'binary')¶
The quantization modes of a serialised gradient (
QuantizationConfig).
- classmethod from_dict(d)[source]¶
Deserialise from
to_dict(), checking the field types and the layout (the scheme checks the ciphertexts when decrypting).- Raises:
ValueError – Unless d has exactly the keys
nid(a client id: astror anint,is_id(); a numpy integer, as an in-memory gradient may carry, is read as theintit equals),shape(a list of non-negative ints),precision(a non-negative int),mode("decimal"or"binary") andciphertexts(a non-empty list of layers); if an array’s shape is invalid or has more thanMAX_ELEMENTSelements; if a per-element layer is not{"shape", "data"}with one ciphertext (a dict) under each index keyto_dictwrites,"(i, j, ...)", and nothing else; if a packed layer is not a valid{"shape", "first_chunk", "chunks"}layout; or if packed and per-element layers are mixed.- Parameters:
d (
dict)- Return type:
- class pyfe4ai.ml.EncryptedFeatures(ciphertext, dim, precision, mode)[source]¶
Bases:
objectEncrypted feature vector for inference.
Produced by
encrypt_features().- classmethod from_dict(d)[source]¶
Deserialise from
to_dict(), checking the field types (the scheme checks the ciphertext when decrypting).- Raises:
ValueError – Unless d has exactly the keys
ciphertext(the scheme’s ciphertext, a dict),dim(a positive int),precision(a non-negative int) andmode("decimal"or"binary").- Parameters:
d (
dict)- Return type:
- class pyfe4ai.ml.ThresholdPartial(sid, label, layers)[source]¶
Bases:
objectOne server’s partial decryptions of a round’s gradients (threshold MCFE), from
partial_decrypt_gradients()forcombine_partial_decryptions().- layers: list¶
compute_lst_ndarray_ct()of the clients’ gradients: per layer{"shape", "parts"}, one partial decryption per chunk (wandc_ybytes for binary public parameters, lists of decimal strings for JSON ones;sharing_idthat of the server’s key share, orNone).to_dict()keeps thesharing_id; a transport of its own must keep it as well, or the combiner can no longer tell partials of two sharings apart.
- to_dict()[source]¶
Serialise for transfer:
{"sid", "label", "format", "layers"}, binarywandc_y(format = "binary") as base64 strings.
- classmethod from_dict(d)[source]¶
Deserialise from
to_dict(), checking the layout (the combiner checks the values).- Raises:
ValueError – For another layout, a
sidorlst_sid_enrolledentry that is not a server id (astror anint,is_id(); a numpy integer is read as theintit equals), a partial decryption of another server, orw/c_ynot of the format (strict base64 for"binary", lists of strings for"json").- Parameters:
d (
dict)- Return type:
- pyfe4ai.ml.precompute_gradient(shape, wrapper, *, nid='nid_default', label)[source]¶
Offline phase of
encrypt_gradient()for a gradient of shape.For
mcfe/ring_lwe(seeprecompute()): label pads, Gaussian samples and NTT products are computed before the gradient exists;encrypt_gradient(..., precomputed=...)then only adds the plaintext term. Formcfe/ddh,mcfe/damgardandmcfe/paillier(and their decentralized variants;pyfe4ai.schemes.mcfe.offline) the label masks of every element are computed. The result is secret and one-time; the ciphertexts are those of the plain call.- Raises:
ValueError – If the scheme has no offline phase, or nid already encrypted under label.
- Parameters:
wrapper (
FESchemeWrapper)nid (
str, default:'nid_default')label (
str)
- Return type:
- pyfe4ai.ml.encrypt_gradient(gradient, wrapper, *, nid='nid_default', label=None, precomputed=None)[source]¶
Encrypt a gradient array for federated aggregation.
- Parameters:
gradient (
ndarray|Any) – Gradient array (numpy array ortorch.Tensor).wrapper (
FESchemeWrapper) – FE scheme wrapper instance.nid (
str, default:'nid_default') – Node identifier.label (
str|None, default:None) – Round label; MCFE allows one ciphertext per client and label, so a label this wrapper already used for nid is refused.precomputed (
Any, default:None) – The result ofprecompute_gradient()for this client, label and shape (online phase).
- Return type:
- pyfe4ai.ml.prepare_aggregation(wrapper, weights, *, shape, sid='sid_agg', label=None, allow_partial=False, servers=None)[source]¶
Derive the functional key of an aggregation before the ciphertexts arrive.
The key depends on the weights, the label and (for the packed Ring-LWE MCFE) the number of chunks of a gradient of shape, not on the ciphertexts;
aggregate_gradients()withprepared=then only decrypts. The same policy checks apply, with the clients of weights as those whose ciphertexts will be aggregated.For
mcfe/ring_lwe_thresholdthe key shares of the enrolled servers (default: the firsttof the public parameters’lst_sid): their label key shares of the round, each server given its weight key share once per weights (again where a copy of the wrapper or the server’s store lacks it when the round is decrypted); sid is the combiner. The result holds key material (seePreparedAggregation). weights is keyed by client ids (numpy integer keys are read as ints; another key:ValueError).
- pyfe4ai.ml.aggregate_gradients(encrypted, wrapper, weights=None, *, sid='sid_agg', label=None, allow_partial=False, prepared=None, servers=None)[source]¶
Decrypt and aggregate encrypted gradients from multiple clients.
- Parameters:
encrypted (
dict[str,EncryptedGradient]) – Encrypted data container.wrapper (
FESchemeWrapper) – FE scheme wrapper instance.weights (
dict[str,float|int|list] |None, default:None) – Weight per client (a number or a one-element list); fractional weights are quantised with the wrapper’s precision and the result is scaled back.sid (
str, default:'sid_agg') – Session / decryption-key identifier.label (
str|None, default:None) – Round label (MCFE); each label is aggregated with one weight vector only.allow_partial (
bool, default:False) – Allow zero weights and a subset of the enrolled clients. Off by default: the wrapper holds the master key, and such weights would decrypt a single client’s gradient (so would very unequal weights; seepyfe4ai.ml).prepared (
PreparedAggregation|None, default:None) – A key fromprepare_aggregation()(online phase); weights, if given, and label must match it.servers (
list|None, default:None) –mcfe/ring_lwe_threshold: the servers whose partial decryptions are combined (default: those of prepared, or the firsttoflst_sid); the wrapper runs them (partial_decrypt_gradients(),combine_partial_decryptions()) and sid is the combiner. If their label key shares turn out to be of different sharings (MixedSharingsError), all servers fetch them again and decrypt once more.
- Raises:
ValueError – If the policy above is violated, SIFE is given more than one client, encrypted or weights is keyed by a value that is no client id (numpy integers are read as ints; see
pyfe4ai.core.ids), or a gradient is filed under another client than itsnid.- Return type:
- pyfe4ai.ml.partial_decrypt_gradients(encrypted, wrapper, prepared, *, server)[source]¶
Server server’s partial decryptions of the clients’ gradients for a prepared threshold aggregation (
mcfe/ring_lwe_threshold; the wrapper runs the server, with its key share of prepared, seeprepare_aggregation()).- Raises:
ValueError – If server is not enrolled in prepared, the clients are not those of its weights, encrypted is keyed by a value that is no client id (numpy integers are read as ints), or a gradient is filed under another client than its
nid.- Parameters:
encrypted (
dict[str,EncryptedGradient])wrapper (
FESchemeWrapper)prepared (
PreparedAggregation)server (
str)
- Return type:
- pyfe4ai.ml.combine_partial_decryptions(partials, wrapper, prepared)[source]¶
Combine the enrolled servers’ partial decryptions (
partial_decrypt_gradients()) into the weighted sum of the gradients, asaggregate_gradients()returns it.- Raises:
ValueError – Unless partials are those of the prepared servers for its label.
FESchemeError – If they do not combine (see
decrypt_lst_ndarray_ct(); servers that decrypted different ciphertexts:DISAGREEING_PARTIALS);MixedSharingsErrorif their label parts are of different sharings (the key generator shared the label again between the servers’ fetches): prepare the round again and have all servers decrypt with the new key shares (aggregate_gradients()does this once by itself).
- Parameters:
partials (
dict[str,ThresholdPartial])wrapper (
FESchemeWrapper)prepared (
PreparedAggregation)
- Return type:
- pyfe4ai.ml.encrypt_features(features, wrapper, *, nid='nid_default')[source]¶
Encrypt a feature vector for privacy-preserving inference.
- Parameters:
features (
ndarray|Any) – Feature vector (numpy array ortorch.Tensor), flattened and quantized with the wrapper’s precision.wrapper (
FESchemeWrapper) – FE scheme wrapper instance.nid (
str, default:'nid_default') – Node identifier.
- Return type:
- pyfe4ai.ml.compute_linear(encrypted, wrapper, weights, *, sid='sid_infer')[source]¶
Compute ⟨features, weights⟩ on encrypted features.
- Parameters:
encrypted (
EncryptedFeatures) – Encrypted data container.wrapper (
FESchemeWrapper) – FE scheme wrapper instance.sid (
str, default:'sid_infer') – Session / decryption-key identifier.
- Return type:
Quantization¶
Precision management toolkit for functional encryption on real-valued data.
FE schemes operate on integers, but AI/ML workloads use floats. This module provides three tools to bridge the gap:
QuantizationConfig— float↔int conversion with configurable scaling (decimalorbinarymode).estimate_bounds()— auto-computebound_x,bound_yand discrete-log table requirements from sample data.analyze_precision_loss()— report quantization error metrics.
- class pyfe4ai.ml.quantization.QuantizationConfig(precision=3, mode='decimal')[source]¶
Bases:
objectImmutable quantization strategy for float↔int conversion.
- Parameters:
precision (
int, default:3) – Number of fractional digits (decimal) or bits (binary) to preserve.mode (
str, default:'decimal') –"decimal"scales by10**precision(compatible with existing PyFE4AI ndarray helpers)."binary"scales by2**precision(avoids base-10 rounding artefacts, better for ML weights).
- Raises:
ValueError – If precision is negative or mode is unknown.
- pyfe4ai.ml.quantization.estimate_bounds(x_data, y_weights, precision, eta=None, mode='decimal')[source]¶
Auto-compute safe FE bounds from sample data.
- Parameters:
x_data (
ndarray) – Sample plaintext data (floats).y_weights (
ndarray) – Sample weights (floats).precision (
int) – Quantization precision.eta (
int|None, default:None) – Inner-product vector dimension (default: the smaller sample size).mode (
str, default:'decimal') – Quantization mode,"decimal"or"binary".
- Return type:
- pyfe4ai.ml.quantization.validate_bounds(bound_x, bound_y, eta, precision)[source]¶
Check whether configured bounds are safe for FE operations.