From 26dd90311cdb208bdd126f1df47a218368b090de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20P=C3=A9r=C3=A9?= Date: Mon, 26 Aug 2024 11:12:08 +0200 Subject: [PATCH] chore(optimizer): enhance optimizer errors --- .../src/dag/operator/location.rs | 2 +- .../dag/multi_parameters/analyze.rs | 50 +++++++------- .../dag/multi_parameters/optimize/mod.rs | 14 +++- .../multi_parameters/variance_constraint.rs | 3 +- .../src/optimization/mod.rs | 9 ++- docs/compilation/common_errors.md | 19 +++++- frontends/concrete-python/.pylintrc | 2 +- .../tests/compilation/test_modules.py | 18 +++-- .../compilation/test_optimizer_errors.py | 66 +++++++++++++++++++ 9 files changed, 141 insertions(+), 42 deletions(-) create mode 100644 frontends/concrete-python/tests/compilation/test_optimizer_errors.py diff --git a/compilers/concrete-optimizer/concrete-optimizer/src/dag/operator/location.rs b/compilers/concrete-optimizer/concrete-optimizer/src/dag/operator/location.rs index d0cfa76be..ee01eb352 100644 --- a/compilers/concrete-optimizer/concrete-optimizer/src/dag/operator/location.rs +++ b/compilers/concrete-optimizer/concrete-optimizer/src/dag/operator/location.rs @@ -11,7 +11,7 @@ pub enum Location { impl Display for Location { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Unknown => write!(f, "unknown location"), + Self::Unknown => write!(f, "unknown"), Self::File(file) => write!(f, "{}", file.file_name().unwrap().to_str().unwrap()), Self::Line(file, line) => { write!(f, "{}:{line}", file.file_name().unwrap().to_str().unwrap()) diff --git a/compilers/concrete-optimizer/concrete-optimizer/src/optimization/dag/multi_parameters/analyze.rs b/compilers/concrete-optimizer/concrete-optimizer/src/optimization/dag/multi_parameters/analyze.rs index 52df83e1b..94d271e1e 100644 --- a/compilers/concrete-optimizer/concrete-optimizer/src/optimization/dag/multi_parameters/analyze.rs +++ b/compilers/concrete-optimizer/concrete-optimizer/src/optimization/dag/multi_parameters/analyze.rs @@ -406,7 +406,7 @@ impl VariancedDag { .check_growing_input_noise() .map_err(|err| match err { Err::NotComposable(prev) => { - Err::NotComposable(format!("At {loc}: please add `fhe.refresh(...)` to guarantee the function composability.\n{prev}.")) + Err::NotComposable(format!("At location {loc}:\n{prev}.")) } _ => unreachable!(), }) @@ -912,7 +912,7 @@ pub mod tests { #[test] #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: NotComposable(\"At unknown location: please add `fhe.refresh(...)` to guarantee the function composability.\\nThe noise of the node 0 is contaminated by noise coming straight from the input (partition: 0, coeff: 1.21).\")" + expected = "called `Result::unwrap()` on an `Err` value: NotComposable(\"At location unknown:\\nThe noise of the node 0 is contaminated by noise coming straight from the input (partition: 0, coeff: 1.21).\")" )] fn test_composition_with_growing_inputs_panics() { let mut dag = unparametrized::Dag::new(); @@ -944,8 +944,8 @@ pub mod tests { .map(ToString::to_string) .collect::>(); let expected_constraint_strings = vec![ - "At location unknown location:\n1σ²Br[0] + 1σ²K[0] + 1σ²M[0] < (2²)**-5 (1bits partition:0 count:1, dom=10)", - "At location unknown location:\n1σ²Br[0] < (2²)**-6 (2bits partition:0 count:1, dom=12)", + "1σ²Br[0] + 1σ²K[0] + 1σ²M[0] < (2²)**-5 (1bits partition:0 count:1, dom=10)", + "1σ²Br[0] < (2²)**-6 (2bits partition:0 count:1, dom=12)", ]; assert!(actual_constraint_strings == expected_constraint_strings); } @@ -967,10 +967,10 @@ pub mod tests { .map(ToString::to_string) .collect::>(); let expected_constraint_strings = vec![ - "At location unknown location:\n1σ²Br[0] + 1σ²K[0] + 1σ²M[0] < (2²)**-7 (3bits partition:0 count:1, dom=14)", - "At location unknown location:\n1σ²Br[0] + 1σ²K[0→1] + 1σ²M[1] < (2²)**-10 (6bits partition:1 count:1, dom=20)", - "At location unknown location:\n1σ²Br[0] + 1σ²Br[1] + 1σ²FK[1→0] + 1σ²K[0] + 1σ²M[0] < (2²)**-7 (3bits partition:0 count:1, dom=14)", - "At location unknown location:\n1σ²Br[0] < (2²)**-7 (3bits partition:0 count:1, dom=14)", + "1σ²Br[0] + 1σ²K[0] + 1σ²M[0] < (2²)**-7 (3bits partition:0 count:1, dom=14)", + "1σ²Br[0] + 1σ²K[0→1] + 1σ²M[1] < (2²)**-10 (6bits partition:1 count:1, dom=20)", + "1σ²Br[0] + 1σ²Br[1] + 1σ²FK[1→0] + 1σ²K[0] + 1σ²M[0] < (2²)**-7 (3bits partition:0 count:1, dom=14)", + "1σ²Br[0] < (2²)**-7 (3bits partition:0 count:1, dom=14)", ]; assert_eq!(actual_constraint_strings, expected_constraint_strings); let partitions = vec![ @@ -1010,12 +1010,12 @@ pub mod tests { .map(ToString::to_string) .collect::>(); let expected_constraint_strings = vec![ - "At location unknown location:\n1σ²Br[0] + 1σ²FK[0→1] + 1σ²Br[2] + 1σ²FK[2→1] + 1σ²K[1→0] + 1σ²M[0] < (2²)**-7 (3bits partition:0 count:1, dom=14)", - "At location unknown location:\n1σ²Br[0] + 1σ²K[0→1] + 1σ²M[1] < (2²)**-10 (6bits partition:1 count:1, dom=20)", - "At location unknown location:\n1σ²Br[0] + 1σ²FK[0→1] + 1σ²Br[1] + 1σ²Br[2] + 1σ²FK[2→1] + 1σ²K[1→2] + 1σ²M[2] < (2²)**-17 (13bits partition:2 count:1, dom=34)", - "At location unknown location:\n1σ²Br[2] < (2²)**-7 (3bits partition:2 count:1, dom=14)", - "At location unknown location:\n1σ²Br[0] + 1σ²FK[0→1] + 1σ²Br[1] + 1σ²Br[2] + 1σ²FK[2→1] + 1σ²K[1→0] + 1σ²M[0] < (2²)**-7 (3bits partition:0 count:1, dom=14)", - "At location unknown location:\n1σ²Br[0] < (2²)**-7 (3bits partition:0 count:1, dom=14)", + "1σ²Br[0] + 1σ²FK[0→1] + 1σ²Br[2] + 1σ²FK[2→1] + 1σ²K[1→0] + 1σ²M[0] < (2²)**-7 (3bits partition:0 count:1, dom=14)", + "1σ²Br[0] + 1σ²K[0→1] + 1σ²M[1] < (2²)**-10 (6bits partition:1 count:1, dom=20)", + "1σ²Br[0] + 1σ²FK[0→1] + 1σ²Br[1] + 1σ²Br[2] + 1σ²FK[2→1] + 1σ²K[1→2] + 1σ²M[2] < (2²)**-17 (13bits partition:2 count:1, dom=34)", + "1σ²Br[2] < (2²)**-7 (3bits partition:2 count:1, dom=14)", + "1σ²Br[0] + 1σ²FK[0→1] + 1σ²Br[1] + 1σ²Br[2] + 1σ²FK[2→1] + 1σ²K[1→0] + 1σ²M[0] < (2²)**-7 (3bits partition:0 count:1, dom=14)", + "1σ²Br[0] < (2²)**-7 (3bits partition:0 count:1, dom=14)", ]; assert_eq!(actual_constraint_strings, expected_constraint_strings); let partitions = [1, 1, 0, 1, 1, 1, 2, 0] @@ -1272,17 +1272,17 @@ pub mod tests { .collect(); let expected_constraints = [ // First lut to force partition HIGH_PRECISION_PARTITION - "At location unknown location:\n1σ²In[1] + 1σ²K[1] + 1σ²M[1] < (2²)**-8 (4bits partition:1 count:1, dom=16)", + "1σ²In[1] + 1σ²K[1] + 1σ²M[1] < (2²)**-8 (4bits partition:1 count:1, dom=16)", // 16384(shift) = (2**7)², for Br[1] - "At location unknown location:\n16384σ²Br[1] + 16384σ²FK[1→0] + 1σ²K[0] + 1σ²M[0] < (2²)**-4 (0bits partition:0 count:1, dom=22)", + "16384σ²Br[1] + 16384σ²FK[1→0] + 1σ²K[0] + 1σ²M[0] < (2²)**-4 (0bits partition:0 count:1, dom=22)", // 4096(shift) = (2**6)², 1(due to 1 erase bit) for Br[0] and 1 for Br[1] - "At location unknown location:\n4096σ²Br[0] + 4096σ²Br[1] + 4096σ²FK[1→0] + 1σ²K[0] + 1σ²M[0] < (2²)**-4 (0bits partition:0 count:1, dom=20)", + "4096σ²Br[0] + 4096σ²Br[1] + 4096σ²FK[1→0] + 1σ²K[0] + 1σ²M[0] < (2²)**-4 (0bits partition:0 count:1, dom=20)", // 1024(shift) = (2**5)², 2(due to 2 erase bit for Br[0] and 1 for Br[1] - "At location unknown location:\n2048σ²Br[0] + 1024σ²Br[1] + 1024σ²FK[1→0] + 1σ²K[0] + 1σ²M[0] < (2²)**-4 (0bits partition:0 count:1, dom=19)", + "2048σ²Br[0] + 1024σ²Br[1] + 1024σ²FK[1→0] + 1σ²K[0] + 1σ²M[0] < (2²)**-4 (0bits partition:0 count:1, dom=19)", // 3(erase bit) Br[0] and 1 initial Br[1] - "At location unknown location:\n3σ²Br[0] + 1σ²Br[1] + 1σ²FK[1→0] + 1σ²K[0→1] + 1σ²M[1] < (2²)**-8 (4bits partition:1 count:1, dom=18)", + "3σ²Br[0] + 1σ²Br[1] + 1σ²FK[1→0] + 1σ²K[0→1] + 1σ²M[1] < (2²)**-8 (4bits partition:1 count:1, dom=18)", // Last lut to close the cycle - "At location unknown location:\n1σ²Br[1] < (2²)**-8 (4bits partition:1 count:1, dom=16)", + "1σ²Br[1] < (2²)**-8 (4bits partition:1 count:1, dom=16)", ]; for (c, ec) in constraints.iter().zip(expected_constraints) { assert!( @@ -1333,14 +1333,14 @@ pub mod tests { .collect(); let expected_constraints = [ // First lut to force partition HIGH_PRECISION_PARTITION - "At location unknown location:\n1σ²In[1] + 1σ²K[1] + 1σ²M[1] < (2²)**-8 (4bits partition:1 count:1, dom=16)", + "1σ²In[1] + 1σ²K[1] + 1σ²M[1] < (2²)**-8 (4bits partition:1 count:1, dom=16)", // 16384(shift) = (2**7)², for Br[1] - "At location unknown location:\n16384σ²Br[1] + 1σ²K[1→0] + 1σ²M[0] < (2²)**-4 (0bits partition:0 count:1, dom=22)", + "16384σ²Br[1] + 1σ²K[1→0] + 1σ²M[0] < (2²)**-4 (0bits partition:0 count:1, dom=22)", // 4096(shift) = (2**6)², 1(due to 1 erase bit) for Br[0] and 1 for Br[1] - "At location unknown location:\n4096σ²Br[0] + 4096σ²FK[0→1] + 4096σ²Br[1] + 1σ²K[1→0] + 1σ²M[0] < (2²)**-4 (0bits partition:0 count:1, dom=20)", + "4096σ²Br[0] + 4096σ²FK[0→1] + 4096σ²Br[1] + 1σ²K[1→0] + 1σ²M[0] < (2²)**-4 (0bits partition:0 count:1, dom=20)", // 1024(shift) = (2**5)², 2(due to 2 erase bit for Br[0] and 1 for Br[1] - "At location unknown location:\n2048σ²Br[0] + 2048σ²FK[0→1] + 1024σ²Br[1] + 1σ²K[1→0] + 1σ²M[0] < (2²)**-4 (0bits partition:0 count:1, dom=19)", - "At location unknown location:\n3σ²Br[0] + 3σ²FK[0→1] + 1σ²Br[1] + 1σ²K[1] + 1σ²M[1] < (2²)**-8 (4bits partition:1 count:1, dom=18)", + "2048σ²Br[0] + 2048σ²FK[0→1] + 1024σ²Br[1] + 1σ²K[1→0] + 1σ²M[0] < (2²)**-4 (0bits partition:0 count:1, dom=19)", + "3σ²Br[0] + 3σ²FK[0→1] + 1σ²Br[1] + 1σ²K[1] + 1σ²M[1] < (2²)**-8 (4bits partition:1 count:1, dom=18)", ]; for (c, ec) in constraints.iter().zip(expected_constraints) { assert!( diff --git a/compilers/concrete-optimizer/concrete-optimizer/src/optimization/dag/multi_parameters/optimize/mod.rs b/compilers/concrete-optimizer/concrete-optimizer/src/optimization/dag/multi_parameters/optimize/mod.rs index 5f4085992..2217fbfc5 100644 --- a/compilers/concrete-optimizer/concrete-optimizer/src/optimization/dag/multi_parameters/optimize/mod.rs +++ b/compilers/concrete-optimizer/concrete-optimizer/src/optimization/dag/multi_parameters/optimize/mod.rs @@ -21,7 +21,6 @@ use crate::optimization::dag::multi_parameters::feasible::Feasible; use crate::optimization::dag::multi_parameters::partition_cut::PartitionCut; use crate::optimization::dag::multi_parameters::partitions::PartitionIndex; use crate::optimization::dag::multi_parameters::{analyze, keys_spec}; -use crate::optimization::Err::NoParametersFound; use super::feasible::Feasibility; use super::keys_spec::InstructionKeys; @@ -1050,7 +1049,14 @@ pub fn optimize( fix_point = params.clone(); } if best_params.is_none() { - return Err(NoParametersFound); + match params.is_feasible { + Feasibility::Unfeasible(ref unfeasible_constraint) => { + return Err(optimization::Err::UnfeasibleVarianceConstraint(Box::new( + unfeasible_constraint.to_owned(), + ))); + } + _ => unreachable!(), + } } let best_params = best_params.unwrap(); sanity_check( @@ -1197,7 +1203,9 @@ pub fn optimize_to_circuit_solution( { return keys_spec::CircuitSolution::from_native_solution(sol, nb_instr); } - return keys_spec::CircuitSolution::no_solution(NoParametersFound.to_string()); + return keys_spec::CircuitSolution::no_solution( + optimization::Err::NoParametersFound.to_string(), + ); } let default_partition = PartitionIndex::FIRST; let dag_and_params = optimize( diff --git a/compilers/concrete-optimizer/concrete-optimizer/src/optimization/dag/multi_parameters/variance_constraint.rs b/compilers/concrete-optimizer/concrete-optimizer/src/optimization/dag/multi_parameters/variance_constraint.rs index 45d704875..c03b0f063 100644 --- a/compilers/concrete-optimizer/concrete-optimizer/src/optimization/dag/multi_parameters/variance_constraint.rs +++ b/compilers/concrete-optimizer/concrete-optimizer/src/optimization/dag/multi_parameters/variance_constraint.rs @@ -18,8 +18,7 @@ impl fmt::Display for VarianceConstraint { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "At location {}:\n{} < (2²)**{} ({}bits partition:{} count:{}, dom={})", - self.location, + "{} < (2²)**{} ({}bits partition:{} count:{}, dom={})", self.variance, self.safe_variance_bound.log2().round() / 2.0, self.precision, diff --git a/compilers/concrete-optimizer/concrete-optimizer/src/optimization/mod.rs b/compilers/concrete-optimizer/concrete-optimizer/src/optimization/mod.rs index 8f86410a3..4b77c1db3 100644 --- a/compilers/concrete-optimizer/concrete-optimizer/src/optimization/mod.rs +++ b/compilers/concrete-optimizer/concrete-optimizer/src/optimization/mod.rs @@ -16,10 +16,15 @@ pub enum Err { impl std::fmt::Display for Err { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - Self::NotComposable(details) => write!(f, "Program can not be composed: {details}"), + Self::NotComposable(details) => write!(f, "Program can not be composed (see https://docs.zama.ai/concrete/compilation/common_errors#id-8.-unfeasible-noise-constraint): {details}"), Self::NoParametersFound => write!(f, "No crypto parameters could be found"), Self::UnfeasibleVarianceConstraint(constraint) => { - write!(f, "Unfeasible noise constraint encountered: {constraint}") + write!( + f, + "Unfeasible noise constraint encountered (see https://docs.zama.ai/concrete/compilation/common_errors#id-9.-non-composable-circuit): At location {}:\n{}.", + constraint.location, + constraint + ) } } } diff --git a/docs/compilation/common_errors.md b/docs/compilation/common_errors.md index 9f954976d..beee0bd00 100644 --- a/docs/compilation/common_errors.md +++ b/docs/compilation/common_errors.md @@ -32,7 +32,7 @@ This document explains the most common errors and provides solutions to fix them **Possible solutions**: - Try to simplify your circuit. - Use smaller weights. -- Add intermediate PBS to reduce the noise, with identity function `fhe.univariate(lambda x: x)`. +- Add intermediate PBS to reduce the noise, with identity function `fhe.refresh(lambda x: x)`. ## 4. Too long inputs for table looup @@ -77,5 +77,22 @@ This document explains the most common errors and provides solutions to fix them - Change your program. - Consider using tricks to replace ternary-if, as `c ? t : f = f + c * (t-f)`. +## 8. Unfeasible noise constraint +**Error message**: `Unfeasible noise constraint encountered` +**Cause**: The optimizer can't find cryptographic parameters for the circuit that are both secure and correct. + +**Possible solutions**: +- Try to simplify your circuit. +- Use smaller weights. +- Add intermediate PBS to reduce the noise, with identity function `fhe.refresh(x)`. + +## 9. Non composable circuit + +**Error message**: `Program can not be composed` + +**Cause**: Some circuit outputs are contaminated by unrefreshed input noise. + +**Possible solutions**: +- Add intermediate PBS to refresh the noise with `fhe.refresh(x)`. diff --git a/frontends/concrete-python/.pylintrc b/frontends/concrete-python/.pylintrc index 6b83703de..bd89d3427 100644 --- a/frontends/concrete-python/.pylintrc +++ b/frontends/concrete-python/.pylintrc @@ -52,7 +52,7 @@ ignore=CVS # ignore-list. The regex matches against paths and can be in Posix or Windows # format. Because '\\' represents the directory delimiter on Windows systems, # it can't be used as an escape character. -ignore-paths= +ignore-paths=concrete/fhe/mlir/* # Files or directories matching the regular expression patterns are skipped. # The regex matches against base names, not paths. The default value ignores diff --git a/frontends/concrete-python/tests/compilation/test_modules.py b/frontends/concrete-python/tests/compilation/test_modules.py index b69f20b01..5ba68e715 100644 --- a/frontends/concrete-python/tests/compilation/test_modules.py +++ b/frontends/concrete-python/tests/compilation/test_modules.py @@ -3,7 +3,6 @@ Tests of everything related to modules. """ import inspect -import re import tempfile from pathlib import Path @@ -125,14 +124,19 @@ def test_non_composable_message(): def add(x, y): return x + y - line_of_add = inspect.currentframe().f_lineno - 2 - expected_message = f"""\ -Program can not be composed: At test_modules.py:{line_of_add}:0: please add `fhe.refresh(...)` to guarantee the function composability. -The noise of the node 0 is contaminated by noise coming straight from the input (partition: 0, coeff: 2.00).\ -""" - with pytest.raises(RuntimeError, match=re.escape(expected_message)): + line = inspect.currentframe().f_lineno - 2 + with pytest.raises(RuntimeError) as excinfo: Module.compile({"add": [(0, 0), (3, 3)]}) + assert ( + str(excinfo.value) + == f"""\ +Program can not be composed (see https://docs.zama.ai/concrete/compilation/common_errors#id-8.-unfeasible-noise-constraint): \ +At location test_modules.py:{line}:0:\nThe noise of the node 0 is contaminated by noise coming straight from the input \ +(partition: 0, coeff: 2.00).\ +""" + ) + def test_call_clear_circuits(): """ diff --git a/frontends/concrete-python/tests/compilation/test_optimizer_errors.py b/frontends/concrete-python/tests/compilation/test_optimizer_errors.py new file mode 100644 index 000000000..d7814e754 --- /dev/null +++ b/frontends/concrete-python/tests/compilation/test_optimizer_errors.py @@ -0,0 +1,66 @@ +""" +Tests errors returned by the compiler. +""" + +import inspect + +import numpy as np +import pytest + +from concrete import fhe + +# pylint: disable=missing-class-docstring, missing-function-docstring, no-self-argument, unused-variable, no-member, unused-argument, function-redefined, expression-not-assigned +# same disables for ruff: +# ruff: noqa: N805, E501, F841, ARG002, F811, B015, RUF001 + + +def test_non_composable(helpers): + """ + Test optimizer error for lack of refresh. + """ + + @fhe.compiler({"x": "encrypted"}) + def circuit(x): + return x * 2 + + line = inspect.currentframe().f_lineno - 2 + inputset = range(100) + config = helpers.configuration().fork(composable=True, parameter_selection_strategy="MULTI") + + with pytest.raises(RuntimeError) as excinfo: + circuit = circuit.compile(inputset, config) + + assert ( + str(excinfo.value) + == f"Program can not be composed (see https://docs.zama.ai/concrete/compilation/common_errors#id-8.-unfeasible-noise-constraint): \ +At location test_optimizer_errors.py:{line}:0:\nThe noise of the node 0 is contaminated by noise coming straight from the input \ +(partition: 0, coeff: 4.00)." + ) + + +def test_unfeasible(helpers): + """ + Test optimizer error for unfeasible circuit. + """ + + @fhe.module() + class Module: + @fhe.function({"x": "encrypted"}) + def a(x): + return fhe.refresh(x * 10) + + @fhe.function({"x": "encrypted"}) + def b(x): + return fhe.refresh(x * 1000) + + line = inspect.currentframe().f_lineno - 2 + inputset = [np.random.randint(1, 1000, size=()) for _ in range(100)] + + with pytest.raises(RuntimeError) as excinfo: + module = Module.compile({"a": inputset, "b": inputset}, p_error=0.000001) + + assert ( + str(excinfo.value) + == f"Unfeasible noise constraint encountered (see https://docs.zama.ai/concrete/compilation/common_errors#id-9.-non-composable-circuit): \ +At location test_optimizer_errors.py:{line}:0:\n21990232555520000000σ²Br[0] + 1σ²K[0] + 1σ²M[0] < (2²)**-4.5 (0bits partition:0 count:1, dom=73)." + )