mirror of
https://github.com/luxfhe/torus.git
synced 2026-10-30 17:07:26 +00:00
Forked from upstream concrete (BSD-3-Clause-Clear, © 2024 ZAMA) at HEAD
ecb729bb. Modifications copyright © 2026 Lux Industries Inc.
Rebranding scope:
- Python module concrete → torus (across frontends/torus-python)
- Rust module concrete-* → torus-* (across frontends/torus-rust)
- MLIR dialect concretelang → toruslang (across compilers/torus-compiler)
- Compiler optimizer concrete-optimizer → torus-optimizer
- Backend concrete-cpu / concrete-cuda → torus-cpu / torus-cuda
- Wire protocol concrete-protocol → torus-protocol
- Parameter-curves concrete-security-curves-* → torus-security-curves-*
- Public symbols concretelib → toruslib, ConcreteLang → TorusLang
- URLs github.com/zama-ai/concrete → github.com/luxfhe/torus
- URLs docs.zama.ai/concrete → docs.luxfhe.com/torus
Legal compliance:
- LICENSE.txt retains original Copyright © 2024 ZAMA verbatim (§1-§2)
- LICENSE.txt adds modifications copyright © 2026 Lux Industries Inc.
- §3 endorsement clause: Torus is marketed exclusively as a Lux product;
no Zama name used in README, docs, or marketing
- NOTICE file provides factual attribution per legal best practice
(not endorsement; required by upstream BSD-3-Clause-Clear)
README + NOTICE rewritten from scratch with Lux brand:
- Quick-start in Python (torus.fhe)
- Architecture diagram showing the gen-1 vtbl integration path
(luxcpp/gpu → lux-private/gpu-kernels for CUDA/Metal/WebGPU)
- Scheme coverage table: TFHE (luxfi/fhe), CKKS/BGV/BFV (luxfi/lattice)
- Lux FHE family table: Torus / TorusVM / TorusEVM / TorusML / TorusNet
Known limitations (phase-2A scope):
- PyPI package torus-fhe not yet published; the torus Python module
is renamed structurally but pip-installable distribution is a
follow-up (Phase 2A.1)
- MLIR dialect rename is text-level; the compiler-internal LLVM
rebuild (concrete-compiler-internal-llvm-project submodule) is
structural and works; tablegen files renamed concrete→torus but the
generated .inc files in the source tree will regenerate at build
time
- backends/torus-cuda/implementation/ is an upstream submodule
pointer to the tfhe-rs source (not fetched in the staging clone);
the CUDA backend body is delivered out-of-tree
- ~100 test-fixture variable names like concrete_encoded_sample
retain "concrete" as a math term (vs symbolic); these are inside
tests/, not the public API; cleanup is a follow-up commit
- 295 Python `from concrete import ...` residuals in luxfhe/ml-sdk
consumer remain; that repo's import update is Phase 2B
This commit is the seed for the luxfhe/torus repo. Push as the first
commit to a fresh GitHub repo; do not re-import upstream Zama git
history.
470 lines
12 KiB
Python
470 lines
12 KiB
Python
"""
|
|
Tests of execution of convolution operation.
|
|
"""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from torus import fhe
|
|
from torus.fhe.representation.node import Node
|
|
from torus.fhe.tracing.tracer import Tracer
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"input_shape,weight_shape, group",
|
|
[
|
|
pytest.param(
|
|
(1, 1, 4, 4),
|
|
(1, 1, 2, 2),
|
|
1,
|
|
),
|
|
pytest.param(
|
|
(4, 3, 4, 4),
|
|
(2, 3, 2, 2),
|
|
1,
|
|
),
|
|
pytest.param(
|
|
(1, 6, 4, 4),
|
|
(6, 1, 2, 2),
|
|
6,
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"strides",
|
|
[
|
|
(2, 2),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"dilations",
|
|
[
|
|
(1, 1),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"has_bias",
|
|
[
|
|
True,
|
|
False,
|
|
],
|
|
)
|
|
def test_conv2d(input_shape, weight_shape, group, strides, dilations, has_bias, helpers):
|
|
"""
|
|
Test conv2d.
|
|
"""
|
|
|
|
configuration = helpers.configuration()
|
|
|
|
weight = np.random.randint(0, 4, size=weight_shape)
|
|
bias = np.random.randint(0, 4, size=(weight_shape[0],)) if has_bias else None
|
|
|
|
@fhe.compiler({"x": "encrypted"})
|
|
def function(x):
|
|
return fhe.conv(x, weight, bias, strides=strides, dilations=dilations, group=group)
|
|
|
|
inputset = [np.random.randint(0, 4, size=input_shape) for i in range(100)]
|
|
circuit = function.compile(inputset, configuration)
|
|
|
|
sample = np.random.randint(0, 4, size=input_shape)
|
|
helpers.check_execution(circuit, function, sample)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"input_shape,weight_shape,bias_shape,pads,strides,dilations,kernel_shape,group,auto_pad,"
|
|
"expected_error,expected_message",
|
|
[
|
|
pytest.param(
|
|
(1, 1, 4, 4),
|
|
(1, 1, 2, 2),
|
|
(1,),
|
|
(0, 0, 0, 0),
|
|
(1, 1),
|
|
(1, 1),
|
|
None,
|
|
1,
|
|
"VALID",
|
|
ValueError,
|
|
"auto_pad should be in {'NOTSET'}, but got 'VALID'",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 1, 4),
|
|
(1, 1, 2, 2),
|
|
(1,),
|
|
(1, 0, 2, 0),
|
|
(1, 1),
|
|
(1, 1),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
RuntimeError,
|
|
"padding should be the same for the beginning of the dimension and its end, but got "
|
|
"1 in the beginning, and 2 at the end for dimension 0",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4),
|
|
(1, 1, 2),
|
|
(1,),
|
|
(),
|
|
(1,),
|
|
(1,),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"pads should be of form (D_begin_pad, D_end_pad) when performing 1D convolution, "
|
|
"but it's ()",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4),
|
|
(1, 1, 2),
|
|
(1,),
|
|
(0, 0),
|
|
(),
|
|
(1,),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"strides should be of form (D_stride,) when performing 1D convolution, but it's ()",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4),
|
|
(1, 1, 2),
|
|
(1,),
|
|
(0, 0),
|
|
(1,),
|
|
(),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"dilations should be of form (D_dilation,) when performing 1D "
|
|
"convolution, but it's ()",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4, 4),
|
|
(1, 1, 2, 2),
|
|
(1,),
|
|
(),
|
|
(1, 1),
|
|
(1, 1),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"pads should be of form (height_begin_pad, width_begin_pad, "
|
|
"height_end_pad, width_end_pad) when performing 2D convolution, "
|
|
"but it's ()",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4, 4),
|
|
(1, 1, 2, 2),
|
|
(1,),
|
|
(0, 0, 0, 0),
|
|
(),
|
|
(1, 1),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"strides should be of form (height_stride, width_stride) when performing 2D "
|
|
"convolution, but it's ()",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4, 4),
|
|
(1, 1, 2, 2),
|
|
(1,),
|
|
(0, 0, 0, 0),
|
|
(1, 1),
|
|
(),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"dilations should be of form (height_dilation, width_dilation) when performing 2D "
|
|
"convolution, but it's ()",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4, 4, 4),
|
|
(1, 1, 2, 2, 4),
|
|
(1,),
|
|
(),
|
|
(1, 1, 1),
|
|
(1, 1, 1),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"pads should be of form (D_begin_pad, height_begin_pad, width_begin_pad, "
|
|
"D_end_pad, height_end_pad, width_end_pad) when performing 3D convolution, "
|
|
"but it's ()",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4, 4, 4),
|
|
(1, 1, 2, 2, 2),
|
|
(1,),
|
|
(0, 0, 0, 0, 0, 0),
|
|
(),
|
|
(1, 1, 1),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"strides should be of form (D_stride, height_stride, width_stride) when performing 3D "
|
|
"convolution, but it's ()",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4, 4, 4),
|
|
(1, 1, 2, 2, 2),
|
|
(1,),
|
|
(0, 0, 0, 0, 0, 0),
|
|
(1, 1, 1),
|
|
(),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"dilations should be of form (D_dilation, height_dilation, width_dilation) when "
|
|
"performing 3D convolution, but it's ()",
|
|
),
|
|
pytest.param(
|
|
(),
|
|
(1, 1, 2, 2),
|
|
(1,),
|
|
(0, 0, 0, 0),
|
|
(1, 1),
|
|
(1, 1),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"expected input x to have at least 3 dimensions (N, C, D1, ...), but got 0",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4, 4),
|
|
(),
|
|
(1,),
|
|
(0, 0, 0, 0),
|
|
(1, 1),
|
|
(1, 1),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"expected weight to have at least 3 dimensions (F, C / group, K1, ...), but got 0",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4, 4),
|
|
(1, 1, 2, 2),
|
|
(),
|
|
(0, 0, 0, 0),
|
|
(1, 1),
|
|
(1, 1),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"expected bias to have a single dimension (F,), but got 0",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4, 4),
|
|
(1, 1, 2, 2),
|
|
(1,),
|
|
(0, 0, 0, 0),
|
|
(1, 1),
|
|
(1, 1),
|
|
(1, 2),
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"expected kernel_shape to be (2, 2), but got (1, 2)",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4, 4),
|
|
(1, 1, 2, 2),
|
|
(1,),
|
|
(0, 0, 0, 0),
|
|
(1, 1),
|
|
(1, 1),
|
|
None,
|
|
None,
|
|
"NOTSET",
|
|
ValueError,
|
|
"expected group to be an integer > 0, but got None",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4, 4),
|
|
(1, 2, 2, 2),
|
|
(1,),
|
|
(0, 0, 0, 0),
|
|
(1, 1),
|
|
(1, 1),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
ValueError,
|
|
"expected number of channel in weight to be 1.0 (C / group), but got 2",
|
|
),
|
|
pytest.param(
|
|
(1, 1, 4, 4, 4, 4),
|
|
(1, 1, 2, 2, 2, 2),
|
|
(1,),
|
|
(0, 0, 0, 0, 0, 0, 0, 0),
|
|
(1, 1, 1, 1),
|
|
(1, 1, 1, 1),
|
|
None,
|
|
1,
|
|
"NOTSET",
|
|
NotImplementedError,
|
|
"only 1D, 2D, and 3D convolutions are supported",
|
|
),
|
|
pytest.param(
|
|
(1, 2, 4, 4),
|
|
(1, 1, 2, 2),
|
|
(1,),
|
|
(0, 0, 0, 0),
|
|
(1, 1),
|
|
(1, 1),
|
|
None,
|
|
2,
|
|
"NOTSET",
|
|
ValueError,
|
|
"expected number of feature maps (1) to be a multiple of group (2)",
|
|
),
|
|
],
|
|
)
|
|
# pylint: disable=too-many-arguments
|
|
def test_bad_conv_compilation(
|
|
input_shape,
|
|
weight_shape,
|
|
bias_shape,
|
|
pads,
|
|
strides,
|
|
dilations,
|
|
kernel_shape,
|
|
group,
|
|
auto_pad,
|
|
expected_error,
|
|
expected_message,
|
|
helpers,
|
|
):
|
|
# pylint: enable=too-many-arguments
|
|
"""
|
|
Test conv with bad parameters.
|
|
"""
|
|
|
|
configuration = helpers.configuration()
|
|
|
|
weight = np.random.randint(0, 4, size=weight_shape)
|
|
bias = np.random.randint(0, 4, size=bias_shape) if bias_shape is not None else None
|
|
|
|
@fhe.compiler({"x": "encrypted"})
|
|
def function(x):
|
|
return fhe.conv(
|
|
x,
|
|
weight,
|
|
bias=bias,
|
|
pads=pads,
|
|
strides=strides,
|
|
dilations=dilations,
|
|
kernel_shape=kernel_shape,
|
|
group=group,
|
|
auto_pad=auto_pad,
|
|
)
|
|
|
|
inputset = [np.random.randint(0, 4, size=input_shape) for i in range(100)]
|
|
with pytest.raises(expected_error) as excinfo:
|
|
function.compile(inputset, configuration)
|
|
|
|
# Get the root cause error
|
|
current_error = excinfo.value
|
|
cause = current_error.__cause__
|
|
while cause:
|
|
current_error = cause
|
|
cause = current_error.__cause__
|
|
|
|
assert str(current_error) == expected_message
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"conv_func_name",
|
|
[
|
|
"conv",
|
|
223,
|
|
None,
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"func",
|
|
[
|
|
# pylint: disable=protected-access
|
|
fhe.extensions.convolution._evaluate_conv,
|
|
fhe.extensions.convolution._trace_conv,
|
|
# pylint: enable=protected-access
|
|
],
|
|
)
|
|
def test_bad_conv_func_name(conv_func_name, func):
|
|
"""
|
|
Test invalid conv function name.
|
|
"""
|
|
with pytest.raises(AssertionError) as excinfo:
|
|
func(None, None, None, None, None, None, None, conv_func_name)
|
|
assert (
|
|
str(excinfo.value) == f"expected conv_func to be one of ['conv1d', 'conv2d', 'conv3d'], "
|
|
f"but got {conv_func_name}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"x,weight,bias,expected_error,expected_message",
|
|
[
|
|
pytest.param(
|
|
np.array([1, 2, 3]),
|
|
"not same type as x",
|
|
None,
|
|
TypeError,
|
|
"expected weight to be of same type as x",
|
|
),
|
|
pytest.param(
|
|
np.array([1, 2, 3]),
|
|
np.array([1, 2, 3]),
|
|
"not same type as x",
|
|
TypeError,
|
|
"expected bias to be of same type as x",
|
|
),
|
|
pytest.param(
|
|
Tracer(Node.constant(np.array([1, 2, 3])), []),
|
|
"not same type as x",
|
|
None,
|
|
TypeError,
|
|
"expected weight to be of type Tracer or ndarray",
|
|
),
|
|
pytest.param(
|
|
Tracer(Node.constant(np.array([1, 2, 3])), []),
|
|
np.array([1, 2, 3]),
|
|
"not same type as x",
|
|
TypeError,
|
|
"expected bias to be of type Tracer or ndarray",
|
|
),
|
|
],
|
|
)
|
|
def test_inconsistent_input_types(
|
|
x,
|
|
weight,
|
|
bias,
|
|
expected_error,
|
|
expected_message,
|
|
):
|
|
"""
|
|
Test conv with inconsistent input types.
|
|
"""
|
|
with pytest.raises(expected_error) as excinfo:
|
|
fhe.conv(
|
|
x,
|
|
weight,
|
|
bias=bias,
|
|
)
|
|
|
|
assert str(excinfo.value) == expected_message
|