Build the pipeline
This is the part you write. You compose a pipeline from HomOps, load your weights, and set the encryption parameters. The result is one object you hand to Deploy. Everything here runs on your machine.
What you produce
HomomorphicPipeline): your computation, built from operators, with weights loaded.Parameters (
HomParams): the encryption settings the pipeline runs under.The three parts of a pipeline
HomomorphicPipeline.A worked example: digit recognition
example_mnist_fc. Below is the same pipeline written out, so you can see how to assemble it yourself.import os import torch from lattica_build.base_classes.hom_pipeline import HomomorphicPipeline from lattica_build.operators.composite.sequential import SequentialHomOp from lattica_build.client_ops import Softmax from lattica_build.operators.ml.h_linear import HomLinear from lattica_build.operators.shape.h_reshape import HomReshape from lattica_build.operators.polynomials.h_square import HomSquare BATCH = 100 INPUT_SHAPE = (BATCH, 1, 1, 28 * 28) # pretrained MNIST classifier · same weights as example_mnist_fc model_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "model.pt") model = torch.load(model_path, weights_only=True, map_location="cpu") fc1_weight = model["l1.weight"] fc2_weight = model["l2.weight"] pipeline = HomomorphicPipeline( client_pre=[HomReshape(INPUT_SHAPE)], # preprocess hom=SequentialHomOp( # body (on the worker) HomLinear(fc1_weight.shape, bias=False, with_modswitch=False), HomSquare(with_modswitch=False), HomLinear(fc2_weight.shape, bias=False, with_modswitch=False), ), client_post=[Softmax(-1)], # postprocess n_axis=0, input_shape=INPUT_SHAPE, ) # load the trained weights into the body operators, by position pipeline.set_data(0, fc1_weight) # first HomLinear pipeline.set_data(2, fc2_weight) # second HomLinear
Why HomSquare and not HomReLU? Both run encrypted. HomSquare is a cheap quadratic (HomMul of a value with itself). HomReLU is an encrypted composite via HomSign and costs more levels. Simple networks often use squaring when the model can train against it; use ReLU when you need that activation and can afford the Sign depth.
Before you deploy, print the compiled graph to see every operator, shape, and scale, still on your machine. How: Inspecting the pipeline.
Loading weights
set_data, addressing each operator by its position in the SequentialHomOp. pipeline = HomomorphicPipeline( hom=SequentialHomOp( HomLinear((50, 784), bias=False, with_modswitch=False), # index 0: takes weights HomSquare(with_modswitch=False), # index 1: takes nothing, still counts HomLinear((10, 50), bias=False, with_modswitch=False), # index 2: takes weights ), input_shape=(100, 1, 1, 28 * 28), ) pipeline.set_data(0, fc1_weight) # first HomLinear pipeline.set_data(2, fc2_weight) # second HomLinear
One call per operator
pipeline.set_data(0, fc1_weight) loads the operator at index 0. There is no bulk load; each operator that holds data gets its own call. The arguments after the index are whatever that operator expects.
Count every position
HomSquare at index 1 holds no data and takes no call, but it still occupies its position. The next load is index 2, not 1. Count positions, not just the layers you load.
Not every operator takes data. Some operators require data through set_data; most transform the ciphertext itself and take none. Each operator's page in the HomOps Reference states whether it takes set_data and what arguments it expects.
Forgot one? A missing set_data doesn't fail here. The pipeline compiles at Deploy, and that's where an unloaded operator raises an error. If Deploy complains about missing data, check your indices first - miscounting past a no-data operator is the usual cause.
The operators you can use
Setting the parameters
from lattica_build.params import DecompositionType, HomParams params = HomParams( full_q_list_precision=((61,), (45,)), n=2 ** 8, pt_scale=2 ** 20, decomposition_type=DecompositionType.BV, )
Parameters depend on your pipeline. The number of levels in full_q_list_precision, the ring size n, and the scale relate to how much computation the body does. Deeper pipelines need more. The demos set these per workload; start from one close to yours and adjust.
Hand it to Deploy
from lattica_studio import LatticaStudio studio = LatticaStudio(license_key) model_id = studio.deploy_pipeline( pipeline, params, "MY_MNIST_MODEL", display_graph=True, )
deploy_pipeline (register, upload, compile) is the next stage. Access token creation and key generation come after that, on the same page.Next
Deploy the Pipeline
Upload and compile, then create a token and generate keys.