Announcing Concrete Numpy v0.8

October 18, 2022
  -  
The Zama Team

Today, Zama announces the release of a new version of Concrete-Numpy. In this blog post we will be walking you through all updates of each version from Concrete Numpy v0.6 to the latest Concrete Numpy v0.8.

Concrete Numpy v0.6

Indeed, Concrete Numpy was re-written from scratch for v0.6.0 in order to reduce technical debt.

We cleaned up old design decisions that were no longer relevant and improved the overall organization of our repository.

Along the way, several breaking changes have been introduced:

  • Official abbreviation of Concrete Numpy changed from `hnp` to `cnp`
  • `hnp.NPFHECompiler` renamed to `cnp.Compiler`
  • `hnp.NPFHECompiler.compile_on_inputset` renamed to `cnp.Compiler.compile`
  • `hnp.CompilationConfiguration` renamed to `cnp.Configuration`
  • `hnp.CompilationArtifacts` renamed to `cnp.DebugArtifacts`

Virtual Circuits

During prototyping, it may be desirable to ignore bit-width constraints to see what will be possible in the feature or to precisely determine the needs of the problem. For those cases, Virtual Circuits are introduced. When enabled, they disable bit-width constraints and revert execution to simulation. Thus, they are not meant for production, but exist to prototype and determine requirements.


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return x ** 2

inputset = [np.random.randint(0, 1001, size=()) for _ in range(10)]
circuit = function.compile(inputset, enable_unsafe_features=True, virtual=True)

assert circuit.encrypt_run_decrypt(1000) == 1_000_000

Library Compilation

Concrete Numpy compiles your circuits with Concrete Compiler, which converts its input to assembly. Previously, this assembly was kept in memory and execution happened like that (JIT Compilation). Now, library compilation, which is creating a shared object, is introduced and made the default. While not advised, the option to revert back to previous behavior exists, `jit=True`.

Integration with Client/Server Architectures

Deployment with Concrete Numpy wasn’t possible as the compiled circuit had all the functionalities (i.e., encryption, evaluation, decryption). Now, it’s possible to split the circuit into Client and Server components to separate those tasks. These constructs can be saved to disk, transferred, and work independently now. It’s a complicated system best described in the documentation.

PBS Error Probability Configuration

Each table lookup has a certain probability of error associated with the operation. Previously, this error probability was constant and assigned by the compiler. Now, it’s possible to set per Table Lookup error probability from Concrete Numpy. Increasing the probability of error will result in faster execution. So it’s a tradeoff you need to think about.


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return x // 2

inputset = [np.random.randint(0, 10, size=()) for _ in range(10)]
circuit = function.compile(inputset, p_error=0.01)

assert circuit.encrypt_run_decrypt(6) == 3

Univariate Extension

Univariate functions can be performed with a table lookup (as long as they are deterministic). Although it was possible, this required manual creation of the lookup table. Now, it’s much easier and much more readable.


import concrete.numpy as cnp
import numpy as np

def complex_univariate_function(x):

    def per_element(element):
        result = 0
        for i in range(element):
            result += i
        return result

    return np.vectorize(per_element)(x)

@cnp.compiler({"x": "encrypted"})
def function(x):
    return cnp.univariate(complex_univariate_function)(x)

inputset = [np.random.randint(0, 5, size=(3, 2)) for _ in range(10)]
circuit = function.compile(inputset)

sample = np.array([
    [0, 4],
    [2, 1],
    [3, 0],
])
assert np.array_equal(circuit.encrypt_run_decrypt(sample), complex_univariate_function(sample))

More NumPy functions


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return x + np.ones_like(x)

inputset = [np.random.randint(0, 10, size=(2,)) for _ in range(10)]
circuit = function.compile(inputset)

assert np.array_equal(circuit.encrypt_run_decrypt([1, 2]), [2, 3])

import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return x + np.zeros_like(x)

inputset = [np.random.randint(0, 10, size=(2,)) for _ in range(10)]
circuit = function.compile(inputset)

assert np.array_equal(circuit.encrypt_run_decrypt([1, 2]), [1, 2])

import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return np.around(x / 3).astype(np.int64)

inputset = [np.random.randint(0, 10, size=(4,)) for _ in range(10)]
circuit = function.compile(inputset)

assert np.array_equal(circuit.encrypt_run_decrypt([0, 1, 2, 3]), [0, 0, 1, 1])

Concrete Numpy v0.7

Signed Inputs

In the previous versions, Concrete Numpy supported signed intermediate values and signed outputs, but signed inputs were not supported for technical reasons. Those issues are now resolved and it’s possible to have signed inputs as show below.


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return np.abs(x)

inputset = [np.random.randint(-10, 10, size=()) for _ in range(10)]
circuit = function.compile(inputset)

assert circuit.encrypt_run_decrypt(-3) == 3

Zeros and Ones Extensions

It is very common to create tensors filled with 0s and 1s. Concrete Numpy now provides extensions to do that in the encrypted domain.


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return (x + cnp.one()) - cnp.zeros((2, 2))

inputset = [np.random.randint(0, 10, size=(2,)) for _ in range(10)]
circuit = function.compile(inputset)

assert np.array_equal(circuit.encrypt_run_decrypt([1, 2]), [[2, 3], [2, 3]])

Improved Error Messages

Concrete Numpy tries to give clear error messages wherever it can. Some new error messages for certain conditions are introduced with this release.


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return "foo"

inputset = [np.random.randint(0, 10, size=(2,)) for _ in range(10)]
circuit = function.compile(inputset)

This code will raise the following error:


ValueError: Function 'function' returned 'foo', which is not supported

Concrete Numpy v0.8.0

Python 3.7 and 3.10 Support

With the support for these Python versions, Concrete Numpy is now available in a large variety of services (e.g., Google Cloud, Kaggle).

Large Bit Width Support on Circuits without Table Lookups

For some applications, leveled operations and array manipulation operations are enough. In those cases, Concrete Numpy now supports up to 57-bits of precision. 


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return (x + 10000) * (2 ** 10)

inputset = [np.random.randint(2 ** 12, 2 ** 20, size=()) for _ in range(100)]
circuit = function.compile(inputset)

assert circuit.encrypt_run_decrypt(2 ** 16) == 77_348_864

Assignment to Tensors

Tensor manipulation with assignments is one of the most common features of NumPy, now it’s available in Concrete Numpy as well.


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    x[0, 0] = 10
    x[1] = [11, 12]
    x[:, 1] = [13, 14, 15]
    return x

inputset = [np.random.randint(0, 10, size=(3, 2)) for _ in range(10)]
circuit = function.compile(inputset)

sample_input = [[0, 1], [2, 3], [4, 5]]
expected_output = [[10, 13], [11, 14], [4, 15]]

assert np.array_equal(circuit.encrypt_run_decrypt(sample_input), expected_output)

Grouped 2D Convolutions

Extending our initial support, we now support grouped 2D convolutions.


import concrete.numpy as cnp
import concrete.onnx as connx
import numpy as np

weight = np.random.randint(0, 4, size=(6, 1, 2, 2))
bias = np.random.randint(0, 4, size=(6,))

@cnp.compiler({"x": "encrypted"})
def function(x):
    return connx.conv(x, weight, bias, group=6)

inputset = [np.random.randint(0, 4, size=(1, 6, 4, 4)) for _ in range(10)]
circuit = function.compile(inputset)

x = np.random.randint(0, 4, size=(1, 6, 4, 4))
circuit.encrypt_run_decrypt(x)

Native Subtraction Between Encrypted Values

In the previous versions of Concrete Numpy, subtraction was very limited, it’s not anymore.


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    a = x - 10
    b = 10 - x
    c = a - b
    return c

inputset = [np.random.randint(0, 10, size=()) for _ in range(10)]
circuit = function.compile(inputset)

assert circuit.encrypt_run_decrypt(5) == -10

Invalid Value Detection During Inputset Evaluation

Debugging in the presence of `Inf`, or `NaN` values was not straightforward before. Now, it’s baked into Concrete Numpy.


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return (1 / x).astype(np.int64)

inputset = [2, 1, 0, 3]
circuit = function.compile(inputset)

Will now result in the following error chain:


ValueError: An `Inf` value is tried to be converted to integer


The above exception was the direct cause of the following exception:

RuntimeError: Evaluation of the graph failed

%0 = 1                             # ClearScalar
%1 = input                         # EncryptedScalar
%2 = true_divide(%0, %1)           # EncryptedScalar
%3 = astype(%2, dtype=int_)        # EncryptedScalar
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of this node failed
return %3


The above exception was the direct cause of the following exception:

RuntimeError: Evaluation of the graph failed

%0 = x                   # EncryptedScalar
%1 = subgraph(%0)        # EncryptedScalar
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of this node failed
return %1

Subgraphs:

    %1 = subgraph(%0):

        %0 = 1                             # ClearScalar
        %1 = input                         # EncryptedScalar
        %2 = true_divide(%0, %1)           # EncryptedScalar
        %3 = astype(%2, dtype=int_)        # EncryptedScalar
        return %3


The above exception was the direct cause of the following exception:

RuntimeError: Bound measurement using inputset[2] failed

Array Extension

Creation of encrypted tensors during runtime is now easier than ever.


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return cnp.array([[x, 0], [1, x + 1]])

inputset = [np.random.randint(0, 10, size=()) for _ in range(10)]
circuit = function.compile(inputset)

assert np.array_equal(circuit.encrypt_run_decrypt(2), [[2, 0], [1, 3]])

Printing Optimizer Output

Concrete Numpy is using the Concrete Optimizer to select cryptographic parameters. Now, it’s possible to see the parameters selected by the Concrete Optimizer.


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return x // 2

inputset = [np.random.randint(0, 10, size=()) for _ in range(10)]
circuit = function.compile(inputset, show_optimizer=True)

This will print:


Optimizer
--------------------------------------------------------------------------------
--- Circuit
  4 bits integers
  0 manp (maxi log2 norm2)
  2ms to solve
--- Optimizer config
  6.334248e-05 error per pbs call
  nan error per circuit call
--- Complexity for the full circuit
  68 Millions Operations
--- Correctness for each Pbs call
  1/16285 errors (6.140437e-05)
--- Correctness for the full circuit
  1/16285 errors (6.140437e-05)
--- Parameters resolution
  2x glwe_dimension
  2**10 polynomial (1024)
  769 lwe dimension 
  keyswitch l,b=3,4
  blindrota l,b=1,23
  wopPbs : false
---
--------------------------------------------------------------------------------

More NumPy functions


import concrete.numpy as cnp
import numpy as np

@cnp.compiler({"x": "encrypted"})
def function(x):
    return np.broadcast_to(x, (3, 2))

inputset = [np.random.randint(0, 10, size=(2,)) for _ in range(10)]
circuit = function.compile(inputset)

assert np.array_equal(circuit.encrypt_run_decrypt([1, 2]), [[1, 2], [1, 2], [1, 2]])

Additional Links

- Release notes

- Github repo

- Documentation

- List of contributors

Read more related posts

Zama Product Announcement - October 2022

With these releases, Zama continues to build its suite of products to make fully homomorphic encryption accessible, easy, and fast

Read Article