Repository: NVIDIA/NVFlare

Issue:
Tenseal dependency for HE is not available on ARM aarch64
The tenseal dependency is not available for the ARM aarch64 platform, causing installation to fail.  This has been reported for local development on Mac M1 and will affect other non-x86 architectures, Jetson, Clara AGX, IBM POWER, etc..

The tenseal dependency is only required when using the HEBuilder module, and it looks like all other functionality could be used without this dependency.  Can tenseal be made optional, with the caveat that HE is not available without tenseal?

One option would be providing an alternate install, a `requirements-no-tenseal.txt` that includes everything but tenseal.  For example, I generated this file in a clean venv on my linux machine using:
```
pip download nvflare -d /tmp -v \
    | grep Collecting \
    | awk '{print $2}' \
    | tr '[:upper:]' '[:lower:]' \
    | grep -v tenseal \
    | tee requirements-no-tenseal.txt
```
and verified that I can install nvflare and all deps except tenseal by copying to an aarch64 system (in this case a Jetson TX2) with:
```
python3 -m pip install --no-deps -r requirements-no-tenseal.txt
```

This is a pretty awkward solution.  It would be much cleaner to remove the tenseal dependency in the default packaging, since HE is optional, and note in the docs that tenseal must be installed when using HE.


Discussion:
It's possible to make homomorphic encryption support an [optional feature](https://setuptools.pypa.io/en/latest/userguide/dependency_management.html#optional-dependencies), with its dependencies only installed if specified explicitly using e.g. `pip install nvflare[he]`.

Resolving patch:
diff --git a/nvflare/lighter/impl/he.py b/nvflare/lighter/impl/he.py
--- a/nvflare/lighter/impl/he.py
+++ b/nvflare/lighter/impl/he.py
@@ -14,7 +14,10 @@
 
 import os
 
-import tenseal as ts
+try:
+    import tenseal as ts
+except ImportError:
+    ts = None
 
 from nvflare.lighter.spec import Builder
 
@@ -38,6 +41,11 @@ def __init__(
             scale_bits: defaults to 40.
             scheme: defaults to "CKKS".
         """
+        if ts is None:
+            print(
+                "\n *** tenseal is not installed.  HEBuilder is ignored and no tenseal files will be generated. ***\n"
+            )
+            return
         self._context = None
         self.scheme_type_mapping = {
             "CKKS": ts.SCHEME_TYPE.CKKS,
@@ -52,6 +60,8 @@ def __init__(
         self.serialized = None
 
     def initialize(self, ctx):
+        if ts is None:
+            return
         self._context = ts.context(
             self.scheme_type,
             poly_modulus_degree=self.poly_modulus_degree,
@@ -64,6 +74,8 @@ def initialize(self, ctx):
         self._context.global_scale = 2 ** self.scale_bits
 
     def build(self, study, ctx):
+        if ts is None:
+            return
         server = study.get_participants_by_type("server")
         dest_dir = self.get_kit_dir(server, ctx)
         with open(os.path.join(dest_dir, "server_context.tenseal"), "wb") as f:
@@ -74,6 +86,8 @@ def build(self, study, ctx):
                 f.write(self.get_serialized_context(is_client=True))
 
     def get_serialized_context(self, is_client=False):
+        if ts is None:
+            return
         _serialized_context = self._context.serialize(
             save_public_key=is_client,
             save_secret_key=is_client,
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -76,15 +76,8 @@
     long_description_content_type="text/markdown",
     python_requires=">=3.7",
     # install_requires=list(pkutils.parse_requirements("requirements.txt")),
-    install_requires=[
-        "PyYAML",
-        "psutil",
-        "numpy",
-        "grpcio",
-        "google-api-python-client",
-        "cryptography",
-        "tenseal==0.3.0",
-    ],
+    install_requires=["PyYAML", "psutil", "numpy", "grpcio", "google-api-python-client", "cryptography"],
+    extras_require={"HE": ["tenseal==0.3.0"]},
     entry_points={
         "console_scripts": [
             "provision=nvflare.lighter.provision:main",


Test patch: