Bounds and costs¶
FHE circuits are compiled ahead of time. Concrete needs to know the possible shape and value range of encrypted inputs during compilation. In this package, those value ranges are called bounds.
Bounds are public application limits. They are not learned from encrypted data.
For example:
- if encrypted scores are percentages, use
min_value=0andmax_value=100; - if encrypted balances are stored as cents and can range from
-100_000to100_000, use those values as the bounds; - if a fixed-point angle is stored in degrees from 0 to 90, use
min_input=0,max_input=90, andinput_scale=1.
Runtime inputs must stay inside the bounds used to compile the circuit.
Common bound parameters¶
| Parameter | Used by | Meaning |
|---|---|---|
min_value, max_value |
integer operations, comparisons, GCD/LCM, parity, primality | Inclusive integer input range |
min_left, max_left, min_right, max_right |
two-input arithmetic | Inclusive range for each side |
min_numerator, max_numerator, min_denominator, max_denominator |
modulo/division predicates | Inclusive numerator and denominator ranges |
min_input, max_input |
fixed-point and special functions | Inclusive range of the encrypted scaled integer |
max_n |
factorial, Fibonacci, combinations, permutations | Encrypted index range 0..max_n |
scale |
floor, ceil, trunc, round | The encrypted integer x represents x / scale |
input_scale, output_scale |
special fixed-point functions | Input and output fixed-point scales |
numerator_width, denominator_width |
bit-level division | Unsigned scalar bit widths |
fractional_bits |
bit-level fixed-point division | Number of binary fractional output bits |
Choosing good bounds¶
Use the smallest public range that is correct for your application.
Good:
from concrete_fhe_toolkit import math as fhe_math
# Scores are known to be percentages.
circuit = fhe_math.is_prime(min_value=0, max_value=100)
Risky:
from concrete_fhe_toolkit import math as fhe_math
# This may be much more expensive than needed.
circuit = fhe_math.is_prime(min_value=0, max_value=1_000_000)
Wider bounds are more flexible, but they often increase compile time, key size, memory use, and execution time.
Native arithmetic vs lookup helpers¶
Some helpers compile as native Concrete arithmetic:
compile_addcompile_subtractcompile_multiplycompile_negatecompile_square- comparisons such as
compile_less
Other helpers use lookup tables over the declared domain:
absoluteclampmodulodivmodfactorialgcdlcmis_prime- fixed-point special functions such as
sin,log, andsqrt
Lookup helpers are convenient and predictable, but cost grows with domain size and output bit width.
Large lookup guards¶
Some lookup helpers can become expensive very quickly. The package estimates lookup pressure using:
- number of lookup entries;
- input index bit width;
- output bit width.
When the estimate is large enough, the helper emits FHECostWarning. When the
estimate is very large, it raises LookupResourceError unless you opt in:
from concrete_fhe_toolkit import math as fhe_math
gcd = fhe_math.gcd(
min_value=0,
max_value=31,
allow_large_lookup=True,
)
print(gcd.simulate(24, 18))
# 6
Opting in does not guarantee Concrete can find parameters for every machine and every bound. It only says you intentionally accept the possible resource cost.
Invalid mathematical inputs¶
Some functions have undefined inputs:
log,log2, andlog10are undefined for nonpositive values;log1pis undefined when1 + x <= 0;sqrtis undefined for negative values;- modulo and division need an explicit denominator-zero policy.
If your bounds include invalid values, pass an explicit fallback:
from concrete_fhe_toolkit import math as fhe_math
log2 = fhe_math.log2(
min_input=0,
max_input=16,
invalid_result=-1,
)
print(log2.encrypt_run_decrypt(0))
# -1
For modulo/division-like helpers:
from concrete_fhe_toolkit import math as fhe_math
modulo = fhe_math.modulo(
min_numerator=0,
max_numerator=20,
min_denominator=0,
max_denominator=10,
zero_result=-1,
)
print(modulo.encrypt_run_decrypt(17, 0))
# -1
Fixed-point scaling¶
Concrete works with integers. Fixed-point helpers represent real numbers as scaled integers.
If input_scale=100, the encrypted integer 314 represents 3.14.
If output_scale=1000, the output integer 841 represents 0.841.
Example:
from concrete_fhe_toolkit import math as fhe_math
sin = fhe_math.sin(
min_input=0,
max_input=90,
input_scale=1,
output_scale=1000,
angle_unit="degrees",
)
print(sin.encrypt_run_decrypt(30))
# 500, representing 0.500
Bit-level division bounds¶
unsigned_floor_divide and fixed_point_divide use bit widths instead of
lookup-table bounds.
from concrete_fhe_toolkit import math as fhe_math
floor_divide = fhe_math.unsigned_floor_divide(
numerator_width=8,
denominator_width=8,
zero_result=0,
)
print(floor_divide.encrypt_run_decrypt(100, 7))
# 14
The numerator must fit in numerator_width unsigned bits. The denominator must
fit in denominator_width unsigned bits. Division by zero returns
zero_result.
For fixed-point division:
from concrete_fhe_toolkit import math as fhe_math
divide = fhe_math.fixed_point_divide(
numerator_width=8,
denominator_width=8,
fractional_bits=8,
)
print(divide.encrypt_run_decrypt(1, 3))
# 85, representing floor((1 / 3) * 256)
The fixed-point result is an integer scaled by 2 ** fractional_bits.
Rounded lookups: trading accuracy for speed¶
Large, smooth tables (sigmoid, exp, sin) can be evaluated through Concrete's
rounded table lookups: make_unary_lookup(values, min_value, precision=N)
rounds the lookup index down to N bits before indexing, so the compiler
evaluates a 2**N-entry table instead of the full one. The input is
approximated to steps of 2**(input_bits - N), so reserve this knob for
functions where a coarser input grid is acceptable — never for exact
functions such as gcd or parity.
Estimating a model before compiling¶
ml.estimation.estimate_model_cost(model, min_feature=..., max_feature=...)
counts the encrypted comparisons, lookups, and multiplications a model will
compile into and reports the widest lookup domain in bits, mapped to the
same small/moderate/large/very-large levels as the lookup guardrails. Use
it to compare candidate models and to catch circuits that will not fit
(for example a k-NN whose argmin encoding needs 16+ bits) before paying
for a compile.