Threads, Processes and Copies¶
What the library keeps between calls, which objects several threads may
use at once, and what copy.copy, copy.deepcopy, pickle and
os.fork() do to it.
State lives in the objects that own it: a key generator keeps its master
key, the sharings it issued and its caches; a crypto object its keys and
key caches; a threshold server its weight key shares and their caches; a
precomputation its secret randomness. Process-wide state is limited to the
selection of the backend and the sampler, tables derived from public
parameters, the locks of the threshold stores, the OS random buffer and the
registries filled at import (Process-wide state).
tests/state/test_global_state.py keeps that list closed: a new module-level
cache, lock or registry fails it unless it is added there and here.
Selection: backend, threads, sampler¶
pyfe4ai.backend.set_backend()andPYFE4AI_BACKENDselect the backend for the process;with pyfe4ai.backend.backend(name):selects it for the current thread inside the block (athreading.local): threads started inside the block use the process-wide selection.pyfe4ai.backend.set_threads()andPYFE4AI_THREADSset how many threads one native call may use over the chunks of a tensor (Rust threads, with the GIL released), for the whole process. It does not limit how many Python threads call the library:PPython threads running native calls at once use up toPtimes that many threads.pyfe4ai.primitives.sampling.set_gaussian_sampler()andPYFE4AI_GAUSSIAN_SAMPLERselect the Gaussian sampler for the process;with gaussian_sampler(name):for the current context (acontextvars.ContextVar): threads started inside the block use the process-wide one. The schemes that record their sampler in their public parameters ignore the process-wide choice (scheme_sampler()).
Set the process-wide values once, before threads start: every call reads the current value, and changing it while other threads run changes their next calls.
Copies and pickles¶
pickle, copy.copy and copy.deepcopy use the same state
(__getstate__):
Caches that may hold native objects are left out and rebuilt on first use: the prepared master key of a MIFE / MCFE Ring-LWE key generator (
_native_msk), the encryption keys of the Ring-LWE crypto objects (_enc_key,_nkey) and a threshold server’s NTTs of its key shares (_hat_cache); aFESchemeWrapperleaves out its lock, its crypto objects and its servers (servers get their weight key shares again).A threshold server’s cache of the key shares callers passed in is left out (
mife/lwe_threshold,mcfe/lwe_threshold,mcfe/ring_lwe_threshold): the sums of replicated pieces for an enrolled set, with the key shares they were summed from (_enrolled_share_cache), are summed again on use. A pickle of an earlier version that holds the cache loads without it.Everything else is kept, secrets included: keys, the sharings a threshold key generator issued (
dict_weight_shares,dict_dk), a server’s weight key shares, and the caches the objects keep in plain Python values – the key row of a dense-LWE MCFE client (_dense_key_<backend>), the scalars of a ristretto255 MCFE key (_s_cache) andg^uof a MIFE-DDH key (_g_u). A pickle of a key generator, client or server is as secret as its keys.A shallow copy (
copy.copy) shares those stores and caches with the original (the same dictionaries): the process-wide store locks keep the two consistent, but they are one store. A deep copy or a pickle has its own.The threshold MCFE over Ring-LWE objects carry the version of their state; a state of another version is refused when loaded.
Never copied or pickled (
TypeError): one-time precomputations (a copy would be a second use of the same randomness), aPartialDecryptionBudget(a copy would count separately) and the BLS12-381 pairing group (it holds the extension module). Locks are never part of a pickle.
Processes and fork¶
The library registers one os.register_at_fork handler
(pyfe4ai.core.forksafe). In a forked child it
replaces the process-wide locks with new ones (
_XOF_LOCK,_contexts_lock,_SHARE_CACHE_LOCK,_STORE_LOCK): a lock that another thread of the parent held would otherwise stay held in the child;drops the buffered OS random bytes of the samplers (
_OS_RANDOM) and gives the buffer a new lock, so that the two processes never use the same bytes;marks every one-time precomputation used and drops its secret parts (the native ones also refuse to run in another process than the one that made them);
gives the copies of the objects that have a lock of their own a new one (
FESchemeWrapper,PartialDecryptionBudget): another thread of the parent may have held the old one.
A PartialDecryptionBudget counts in
the process that made it only: in a forked child its
check() and
consume() raise
FESchemeError, and so does a "kl"
server that counts against the copy. Otherwise the parent and the child
would each count from the copied used, together produce more partial
decryptions than max_partial_decryptions, and the KL flooding guarantee
would no longer hold. Make a server’s budget in the one process that
answers for it (a subclass may share and persist the count, see the class).
Nothing else changes in the child: it has a copy of every object of the
parent, keys and caches included, and the public tables. The native code
keeps no random generator state between calls (one DRBG per call), so a
fork duplicates none. Python warns about fork() in a process with
threads; the spawn and forkserver start methods of
multiprocessing avoid all of the above, and their workers rebuild every
cache.
Process-wide state¶
Every module- or class-level value that changes after import, or that the
library fills on first use (tests/state/test_global_state.py checks this
list). “Fork”: what a forked child does with it. None of it holds secrets
except the OS random buffer (random bytes not yet used).
Name (module) |
What, bound |
Fork |
|---|---|---|
|
the backend per thread and per process, the threads per native call |
kept |
|
test hooks: the native calls’ random bytes and DRBG seed, the native
sampler’s margin, the extension module; |
kept |
|
the two implementations; each keeps its Python rings per |
kept |
|
whether the missing-extension notice is still due (one entry, popped by the first fallback to Python without the extension) |
kept (a child whose parent gave the notice gives none) |
|
native ring contexts per |
lock renewed |
|
sampler tables per Gaussian width (LRU, 64 / 64 / 256 / 64) |
kept |
|
a module and the extension’s BLS12-381 functions, set on first use |
kept |
|
the Gaussian sampler per process and per context |
kept |
|
guards the block claims of every |
renewed |
|
OS random bytes read 64 KiB at a time, and its lock |
bytes dropped, lock renewed |
|
|
kept |
|
NTT tables, roots and rings per |
kept |
|
the public matrices of the seeded dense-LWE schemes, per seed, held weakly (dropped with the last object using them) |
kept |
|
the base-point table, baby-step tables (LRU, 16 / 32) and a constant |
kept |
|
set at import, by whether charm is installed |
kept |
|
guard the threshold stores, which are attributes of the key generators and servers; process-wide because a shallow copy shares its stores with the original |
renewed |
|
the config class of every scheme (filled when the classes are defined), constants and class defaults |
kept |
|
what a forked child runs, and the objects it notifies (held weakly) |
run |
The parameter cache on disk (PYFE4AI_CACHE_DIR, see
cache_dir()) is shared by every process that uses
the same directory.