This commit: + Adds support for a protocol which enables inter-op between concrete, tfhe-rs and potentially other contributors to the fhe ecosystem. + Gets rid of hand-made serialization in the compiler, and client/server libs. + Refactors client/server libs to allow more pre/post processing of circuit inputs/outputs. The protocol is supported by a definition in the shape of a capnp file, which defines different types of objects among which: + ProgramInfo object, which is a precise description of a set of fhe circuit coming from the same compilation (understand function type information), and the associated key set. + *Key objects, which represent secret/public keys used to encrypt/execute fhe circuits. + Value object, which represent values that can be transferred between client and server to support calls to fhe circuits. The hand-rolled serialization that was previously used is completely dropped in favor of capnp in the whole codebase. The client/server libs, are refactored to introduce a modular design for pre-post processing. Reading the ProgramInfo file associated with a compilation, the client and server libs assemble a pipeline of transformers (functions) for pre and post processing of values coming in and out of a circuit. This design properly decouples various aspects of the processing, and allows these capabilities to be safely extended. In practice this commit includes the following: + Defines the specification in a concreteprotocol package + Integrate the compilation of this package as a compiler dependency via cmake + Modify the compiler to use the Encodings objects defined in the protocol + Modify the compiler to emit ProgramInfo files as compilation artifact, and gets rid of the bloated ClientParameters. + Introduces a new Common library containing the functionalities shared between the compiler and the client/server libs. + Introduces a functional pre-post processing pipeline to this common library + Modify the client/server libs to support loading ProgramInfo objects, and calling circuits using Value messages. + Drops support of JIT. + Drops support of C-api. + Drops support of Rust bindings. Co-authored-by: Nikita Frolov <nf@mkmks.org>
3.8 KiB
Calling from other languages
After doing a compilation, we endup with a couple of artifacts, including crypto parameters and a binary file containing the executable circuit. In order to be able to encrypt and run the circuit properly, we need to know how to interpret these artifacts, and there are a couple of utility functions to load them. These utility functions can be accessed through a variety of languages, including Python and C++.
Demo
We will use a really simple example for a demo, but the same steps can be done for any other circuit. example.mlir will contain the MLIR below:
func.func @main(%arg0: tensor<4x4x!FHE.eint<6>>, %arg1: tensor<4x2xi7>) -> tensor<4x2x!FHE.eint<6>> {
%0 = "FHELinalg.matmul_eint_int"(%arg0, %arg1): (tensor<4x4x!FHE.eint<6>>, tensor<4x2xi7>) -> (tensor<4x2x!FHE.eint<6>>)
%tlu = arith.constant dense<[40, 13, 20, 62, 47, 41, 46, 30, 59, 58, 17, 4, 34, 44, 49, 5, 10, 63, 18, 21, 33, 45, 7, 14, 24, 53, 56, 3, 22, 29, 1, 39, 48, 32, 38, 28, 15, 12, 52, 35, 42, 11, 6, 43, 0, 16, 27, 9, 31, 51, 36, 37, 55, 57, 54, 2, 8, 25, 50, 23, 61, 60, 26, 19]> : tensor<64xi64>
%result = "FHELinalg.apply_lookup_table"(%0, %tlu): (tensor<4x2x!FHE.eint<6>>, tensor<64xi64>) -> (tensor<4x2x!FHE.eint<6>>)
return %result: tensor<4x2x!FHE.eint<6>>
}
You can use the concretecompiler binary to compile this MLIR program. Same can be done with concrete-python, as we only need the compilation artifacts at the end.
$ concretecompiler --action=compile -o python-demo example.mlir
You should be able to see artifacts listed in the python-demo directory
$ ls python-demo/
client_parameters.concrete.params.json compilation_feedback.json fhecircuit-client.h sharedlib.so staticlib.a
Now we want to use the Python bindings in order to call the compiled circuit.
from concrete.compiler import (ClientSupport, LambdaArgument, LibrarySupport)
The main struct to manage compilation artifacts is LibrarySupport. You will have to create one with the path you used during compilation, then load the result of the compilation
lib_support = LibrarySupport.new("/path/to/your/python-demo/")
compilation_result = lib_support.reload()
Using the compilation result, you can load the server lambda (the entrypoint to the executable compiled circuit) as well as the client parameters (containing crypto parameters)
server_lambda = lib_support.load_server_lambda(compilation_result)
client_params = lib_support.load_client_parameters(compilation_result)
The client parameters will serve the client to generate keys and encrypt arguments for the circuit
client_support = ClientSupport.new()
key_set = client_support.key_set(client_params)
args = [
LambdaArgument.from_tensor_u8([1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], [4, 4]),
LambdaArgument.from_tensor_u8([1, 2, 1, 2, 1, 2, 1, 2], [4, 2])
]
encrypted_args = client_support.encrypt_arguments(client_params, key_set, args)
Only evaluation keys are required for the execution of the circuit. You can execute the circuit on the encrypted arguments via server_lambda_call
eval_keys = key_set.get_evaluation_keys()
encrypted_result = lib_support.server_call(server_lambda, encrypted_args, eval_keys)
At this point you have the encrypted result and can decrypt it using the keyset which holds the secret key
result_arg = client_support.decrypt_result(client_params, key_set, encrypted_result)
print("result tensor dims: {}".format(result_arg.n_values()))
print("result tensor data: {}".format(result_arg.get_values()))
There is also a couple of tests in test_compilation.py that can show how to both compile and run a circuit between a client and server using serialization.