Two issues related to maximum operation

Sorry to bother you, but I’ve encountered two issues related to maximum.
I guess their root causes may not be the same, and I think my usage should be correct?
The outputs are quite confusing.

Iusse 1:

When I use multiplication operations before maximum, the output results become incorrect.

from concrete import fhe
import numpy as np

@fhe.compiler({"x": "encrypted", "y": "encrypted", "z": "encrypted", "w": "encrypted"})
def debug_foo(x, y, z, w):
    res1 = x * y
    res2 = z * w
    final = np.maximum(res1, res2)
    return final

if __name__ == "__main__":
    x, y = -39, 74
    z, w = -38, 75
    
    expect_result = debug_foo(x, y, z, w)
    print(f"expect result: {expect_result}")

    inputset = fhe.inputset(fhe.int8, fhe.int8, fhe.int8, fhe.int8)
    circuit = debug_foo.compile(inputset)
    circuit.keygen()
    encrypted_x, encrypted_y, encrypted_z, encrypted_w = circuit.encrypt(x, y, z, w)
    encrypted_result = circuit.run(encrypted_x, encrypted_y, encrypted_z, encrypted_w)
    result = circuit.decrypt(encrypted_result)
    print(f"concrete result: {result}")

The output is:

expect result: -2850
concrete result: -7542

Issue 2

When I use multiplication operations after maximum, the output also becomes incorrect.

from concrete import fhe
import numpy as np
configuration = fhe.Configuration()

@fhe.compiler({"x": "encrypted", "y": "encrypted"})
def debug_foo(x, y):
    x = fhe.hint(x, bit_width=8)
    y = fhe.hint(y, bit_width=8)
    var2 = 14 + y
    var3 = var2 * 2
    var4 = 7
    var5 = np.minimum(var3, var4)
    var6 = var5 * -5
    var7 = np.maximum(var5, var6)
    var8 = var7 * -6
    return var8

if __name__ == "__main__":
    x, y = -12, 13
    
    expect_result = debug_foo(x, y)
    print(f"expect result: {expect_result}")

    inputset = fhe.inputset(fhe.int8, fhe.int8)
    circuit = debug_foo.compile(inputset, configuration=configuration)
    circuit.keygen()
    encrypted_x, encrypted_y = circuit.encrypt(x, y)
    encrypted_result = circuit.run(encrypted_x, encrypted_y)
    result = circuit.decrypt(encrypted_result)
    print(f"concrete result: {result}")

The output is:

expect result: -42
concrete result: -2562

Additionally, I checked the value of v7, and the output was correct. This may suggest that this issue might lie in the final multiplication operation, rather than being directly related to maximum as in Issue 1?

# output of v7
expect result: 7
concrete result: 7

Hey

My 2 cents: from Extensions | Concrete, could you use size = ... to have a larger set? I don’t know but maybe your input set is too small, take a big one maybe.

Also, you might want to print the circuit, to see if there are issues with the bitwidths

Hi! Thanks for your comments!

Then, I’ve used size=10000 for my programs.
For issue 1, I’ve checked the circuit, the bitwidths look correct:

%0 = x                       # EncryptedScalar<int8>         ∈ [-128, 127]
%1 = y                       # EncryptedScalar<int8>         ∈ [-128, 127]
%2 = z                       # EncryptedScalar<int8>         ∈ [-128, 127]
%3 = w                       # EncryptedScalar<int8>         ∈ [-128, 127]
%4 = multiply(%0, %1)        # EncryptedScalar<int15>        ∈ [-15750, 16256]
%5 = multiply(%2, %3)        # EncryptedScalar<int15>        ∈ [-16002, 15875]
%6 = maximum(%4, %5)         # EncryptedScalar<int15>        ∈ [-12322, 16256]

I’ve also tried a program like this:

def debug_foo(x, y):
    res1 = x * y
    res2 = x * y
    final = np.maximum(res1, res2)
    return final

And the circuit also looks correct:

%0 = x                       # EncryptedScalar<int8>         ∈ [-128, 127]
%1 = y                       # EncryptedScalar<int8>         ∈ [-128, 127]
%2 = multiply(%0, %1)        # EncryptedScalar<int15>        ∈ [-16256, 16129]
%3 = multiply(%0, %1)        # EncryptedScalar<int15>        ∈ [-16256, 16129]
%4 = maximum(%2, %3)         # EncryptedScalar<int15>        ∈ [-16256, 16129]
return %4

However, with the same input: x, y = -39, 74, the output is:

expect result: -2886
concrete result: -10001

I’ve also checked the circuit of Issue 2:

 %0 = y                        # EncryptedScalar<int8>          ∈ [-128, 127]
 %1 = 14                       # ClearScalar<uint4>             ∈ [14, 14]
 %2 = add(%1, %0)              # EncryptedScalar<int9>          ∈ [-114, 141]
 %3 = 2                        # ClearScalar<uint2>             ∈ [2, 2]
 %4 = multiply(%2, %3)         # EncryptedScalar<int10>         ∈ [-228, 282]
 %5 = 7                        # ClearScalar<uint3>             ∈ [7, 7]
 %6 = minimum(%4, %5)          # EncryptedScalar<int9>          ∈ [-228, 7]
 %7 = -5                       # ClearScalar<int4>              ∈ [-5, -5]
 %8 = multiply(%6, %7)         # EncryptedScalar<int12>         ∈ [-35, 1140]
 %9 = maximum(%6, %8)          # EncryptedScalar<uint11>        ∈ [0, 1140]
%10 = -6                       # ClearScalar<int4>              ∈ [-6, -6]
%11 = multiply(%9, %10)        # EncryptedScalar<int14>         ∈ [-6840, 0]
return %11

But the output is still strange:

expect result: -42
concrete result: 3774

So I’m guessing this might not be an issue with the bitwidth or the input set?

Yes it’s strange

Maybe I would ask:

  • does it work for some inputs, or does it always fail?
  • what happens if you specify the input set directly, without using fhe.inputset. Directly use some form of inputset = [(np.random.randint(-128, 127), np.random.randint(-128, 127)) for _ in range(100000)] and see

also,

  • does it work with simpler programs, like remove the multiplications and maximum, one by one, until it starts to work
  • does it work better if you only take unsigned integers?

I’ve tried your suggestion, and find that:

  1. They always fail.
  2. Even if I use inputset = [(np.random.randint(-128, 127), np.random.randint(-128, 127)) for _ in range(100000)], the programs of these two issues still fail.

For Issue 1

I checked this simpler program (as it only contains two variables):

def debug_foo(x, y):
    res1 = x * y
    res2 = x * y
    final = np.maximum(res1, res2)
    return final

I used inputset = [(np.random.randint(-128, 127), np.random.randint(-128, 127)) for _ in range(100000)], and run this program 3 times with randomly x, y, the input and output pairs are:
Test 1
Input 1:

x, y = -39, -123

Output 1:

expect result: 4797
concrete result: -24520

Circuit 1:

%0 = x                       # EncryptedScalar<int8>         ∈ [-128, 127]
%1 = y                       # EncryptedScalar<int8>         ∈ [-128, 127]
%2 = multiply(%0, %1)        # EncryptedScalar<int16>        ∈ [-16256, 16384]
%3 = multiply(%0, %1)        # EncryptedScalar<int16>        ∈ [-16256, 16384]
%4 = maximum(%2, %3)         # EncryptedScalar<int16>        ∈ [-16256, 16384]
return %4

Test 2
Input 2:

x, y = 103, 94

Output 2:

expect result: 9682
concrete result: 15988

Circuit 2:

%0 = x                       # EncryptedScalar<int8>         ∈ [-128, 127]
%1 = y                       # EncryptedScalar<int8>         ∈ [-128, 127]
%2 = multiply(%0, %1)        # EncryptedScalar<int16>        ∈ [-16256, 16384]
%3 = multiply(%0, %1)        # EncryptedScalar<int16>        ∈ [-16256, 16384]
%4 = maximum(%2, %3)         # EncryptedScalar<int16>        ∈ [-16256, 16384]
return %4

Test 3
Input 3:

x, y = 31, 99

Output 3:

expect result: 3069
concrete result: -35751

Circuit 3:

%0 = x                       # EncryptedScalar<int8>         ∈ [-128, 127]
%1 = y                       # EncryptedScalar<int8>         ∈ [-128, 127]
%2 = multiply(%0, %1)        # EncryptedScalar<int16>        ∈ [-16256, 16384]
%3 = multiply(%0, %1)        # EncryptedScalar<int16>        ∈ [-16256, 16384]
%4 = maximum(%2, %3)         # EncryptedScalar<int16>        ∈ [-16256, 16384]
return %4

For Issue 2
As I said in the first question, If I remove the last multiplication, then it works well, but if we keep that multiplication, then here are the input and output pairs for 3 times run with randomly x, y and inputset = [(np.random.randint(-128, 127), np.random.randint(-128, 127)) for _ in range(100000)]:

Test 1
Input 1:

x, y = -100, -3

Output 1:

expect result: -42
concrete result: -8897

Circuit 1:

 %0 = y                        # EncryptedScalar<int8>          ∈ [-128, 126]
 %1 = 14                       # ClearScalar<uint4>             ∈ [14, 14]
 %2 = add(%1, %0)              # EncryptedScalar<int9>          ∈ [-114, 140]
 %3 = 2                        # ClearScalar<uint2>             ∈ [2, 2]
 %4 = multiply(%2, %3)         # EncryptedScalar<int10>         ∈ [-228, 280]
 %5 = 7                        # ClearScalar<uint3>             ∈ [7, 7]
 %6 = minimum(%4, %5)          # EncryptedScalar<int9>          ∈ [-228, 7]
 %7 = -5                       # ClearScalar<int4>              ∈ [-5, -5]
 %8 = multiply(%6, %7)         # EncryptedScalar<int12>         ∈ [-35, 1140]
 %9 = maximum(%6, %8)          # EncryptedScalar<uint11>        ∈ [0, 1140]
%10 = -6                       # ClearScalar<int4>              ∈ [-6, -6]
%11 = multiply(%9, %10)        # EncryptedScalar<int14>         ∈ [-6840, 0]
return %11

Test 2
Input 2:

x, y = -44, -86

Output 2:

expect result: -4320
concrete result: -10801

Circuit 2:

 %0 = y                        # EncryptedScalar<int8>          ∈ [-128, 126]
 %1 = 14                       # ClearScalar<uint4>             ∈ [14, 14]
 %2 = add(%1, %0)              # EncryptedScalar<int9>          ∈ [-114, 140]
 %3 = 2                        # ClearScalar<uint2>             ∈ [2, 2]
 %4 = multiply(%2, %3)         # EncryptedScalar<int10>         ∈ [-228, 280]
 %5 = 7                        # ClearScalar<uint3>             ∈ [7, 7]
 %6 = minimum(%4, %5)          # EncryptedScalar<int9>          ∈ [-228, 7]
 %7 = -5                       # ClearScalar<int4>              ∈ [-5, -5]
 %8 = multiply(%6, %7)         # EncryptedScalar<int12>         ∈ [-35, 1140]
 %9 = maximum(%6, %8)          # EncryptedScalar<uint11>        ∈ [0, 1140]
%10 = -6                       # ClearScalar<int4>              ∈ [-6, -6]
%11 = multiply(%9, %10)        # EncryptedScalar<int14>         ∈ [-6840, 0]
return %11

Test 3
Input 3:

x, y = 101, 96

Output 3:

expect result: -42
concrete result: -2562

Circuit 3:

 %0 = y                        # EncryptedScalar<int8>          ∈ [-128, 126]
 %1 = 14                       # ClearScalar<uint4>             ∈ [14, 14]
 %2 = add(%1, %0)              # EncryptedScalar<int9>          ∈ [-114, 140]
 %3 = 2                        # ClearScalar<uint2>             ∈ [2, 2]
 %4 = multiply(%2, %3)         # EncryptedScalar<int10>         ∈ [-228, 280]
 %5 = 7                        # ClearScalar<uint3>             ∈ [7, 7]
 %6 = minimum(%4, %5)          # EncryptedScalar<int9>          ∈ [-228, 7]
 %7 = -5                       # ClearScalar<int4>              ∈ [-5, -5]
 %8 = multiply(%6, %7)         # EncryptedScalar<int12>         ∈ [-35, 1140]
 %9 = maximum(%6, %8)          # EncryptedScalar<uint11>        ∈ [0, 1140]
%10 = -6                       # ClearScalar<int4>              ∈ [-6, -6]
%11 = multiply(%9, %10)        # EncryptedScalar<int14>         ∈ [-6840, 0]
return %11

Well, I don’t really know, and the team who’s handling Concrete is busy with other things at the moment. Maybe I would try:

  • to explicitly add the failing input in the inputset, and see if it continues to fail
  • to try only with unsigned int

Not sure we can investigate nor fix now. If you are courageous, maybe that’s an excellent opportunity to look at Concrete code and debug it by yourself?

Thanks for your comments, I’ve tried to “explicitly add the failing input in the inputset” and “run only with unsigned int”.

I find that these two bugs only occur when the circuits use signed inputs; they do not occur when only unsigned inputs are used.

Since my project isn’t very urgent, maybe I can wait for Concrete’s good team to fix these bugs in the future :smiling_face_with_three_hearts:. However, if I have more time later, I’ll also try looking at Concrete’s code myself to attempt fixing these bugs :laughing:.

Sure! Let me create an issue for that: [Bug] misscomputations, might be related to the use if signed integers · Issue #1302 · zama-ai/concrete · GitHub.