A complete example, start to finish
One script that builds a small MNIST classifier, deploys it, creates a query token, generates keys, and runs encrypted queries against it. Copy it, run it, see the result. Then change things and watch what happens.
What the script does
Before you run it
Python 3.11 or newer. The example uses the current studio and query packages.
Install Lattica Studio. pip install lattica-studio. It includes the build and query clients, the packaged mnist_fc pipeline, and the lightweight mnist data helper used below. See Installation & Setup.
Get your License. Sign up and take your License from the console. Set LICENSE_KEY in the script, or load it from LATTICA_LICENSE_KEY. Guide
Have some credits. Running the worker uses credits. Add them in the console if your balance is low. Pricing
Network access to the Lattica backend (default https://api.lattica.ai). Override with LATTICA_BE_URL if you point at another environment. The data helper caches the MNIST test set on first use. Pipeline weights come from lattica_build.examples.advanced.mnist_fc. You do not copy a checkpoint by hand.
The script
mnist_e2e.py. Set LICENSE_KEY or export LATTICA_LICENSE_KEY, then run python mnist_e2e.py. The flags at the top let you rerun only deploy, only token + key registration, or only queries. Token + key registration does not start a worker; the query flag owns the worker session. Running deploy again with the same MODEL_NAME redeploys into the existing model.import os import mnist import torch from lattica_build.examples.advanced import mnist_fc from lattica_studio import LatticaStudio from lattica_query import QueryClient # Set explicitly, or keep empty to read LATTICA_LICENSE_KEY from the environment. LICENSE_KEY = "" if not LICENSE_KEY: LICENSE_KEY = os.getenv("LATTICA_LICENSE_KEY", "") if not LICENSE_KEY: raise ValueError("Set LICENSE_KEY or LATTICA_LICENSE_KEY before running this script") MODEL_NAME = "MY_MNIST_MODEL" # Run stages selectively during development. RUN_DEPLOY_AND_COMPILE = True RUN_CREATE_QUERY_TOKEN_AND_GENERATE_KEYS = True RUN_ENCRYPTED_QUERY = True def load_mnist_test_data() -> tuple[torch.Tensor, torch.Tensor]: mnist.datasets_url = "https://raw.githubusercontent.com/fgnt/mnist/master/" images = mnist.test_images() labels = mnist.test_labels() x = torch.tensor(images, dtype=torch.float32).reshape((-1, *mnist_fc.INPUT_SHAPE)) x = ((x / 255.0) - 0.1307) / 0.3081 y = torch.tensor(labels, dtype=torch.long).reshape((-1, mnist_fc.BATCH)) return x, y def main() -> None: x, y = load_mnist_test_data() studio = LatticaStudio(LICENSE_KEY) if RUN_DEPLOY_AND_COMPILE: model_id = studio.deploy_pipeline( mnist_fc.build_pipeline(), mnist_fc.build_params(), MODEL_NAME, display_graph=True, ) else: model_id = studio.models.get_id_by_name(MODEL_NAME) if RUN_CREATE_QUERY_TOKEN_AND_GENERATE_KEYS: token = studio.tokens.create(model_id, name=MODEL_NAME, save=True) with QueryClient(token) as client: client.keys.ensure() # registers the EVK without a worker if RUN_ENCRYPTED_QUERY: token = studio.tokens.load(MODEL_NAME) with studio.workers.running(model_id, stop_on_exit=True): with QueryClient(token) as client: key = client.keys.ensure() for i in range(3): result = client.query.encrypted(x[i], key=key) prediction = result.argmax(dim=-1) accuracy = (prediction == y[i]).sum().item() / mnist_fc.BATCH print(f"Accuracy: {accuracy * 100:.1f}%") if __name__ == "__main__": main()
[━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━] LATTICA BUILDER │ registering model ✓ └─ duration: 1.61s └─ Model 'MY_MNIST_MODEL' already exists, redeploying into model_id='4b5b220e-4955-4da5-a9b6-3737a874476c' └─ stopping all active workers of model 'MY_MNIST_MODEL' [━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━] LATTICA BUILDER │ uploading model ✓ └─ duration: 2.22s └─ model size: 170.7 KB └─ upload status: Success [━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━] LATTICA BUILDER │ compiling model ✓ └─ duration: 6.05s └─ model ID: 4b5b220e-4955-4da5-a9b6-3737a874476c [━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━] LATTICA BUILDER │ generate token ✓ └─ duration: 971ms [━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━] LATTICA QUERY │ retrieving initialization data ✓ └─ duration: 913ms └─ Timing summary • get_user_init_data Step Time Percent ──────── ────────── ──────── download 788.0 ms 86.4% instance 100.0 ms 11.0% logic 20.0 ms 2.2% upload 4.0 ms 0.4% worker 0.0 ms 0.0% ──────── ────────── ──────── Total 912.0 ms 100.0% [━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━] LATTICA QUERY │ generating FHE keys ✓ └─ duration: 8ms └─ key artifacts will be stored at /home/rotem/.lattica/keys/query_key_artifacts_fe53624a-0d7a-4389-9d0f-616f08ba29e0.json └─ saved key to /home/rotem/.lattica/keys/query_key_artifacts_fe53624a-0d7a-4389-9d0f-616f08ba29e0.json [━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━] LATTICA QUERY │ registering evaluation key ✓ └─ duration: 2.94s └─ evaluation key size: 465.1 KB └─ upload complete: Success └─ no worker running; the key will be loaded when a worker starts [━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━] LATTICA BUILDER │ starting worker ✓ └─ duration: 3.78s └─ worker status: {'workerSessionId': '514ee064-ad99-4566-a5e6-990bc3679dbc', 'status': 'UP'} [━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━] LATTICA QUERY │ loading local key artifacts ✓ └─ duration: 1ms └─ loading key from /home/rotem/.lattica/keys/query_key_artifacts_fe53624a-0d7a-4389-9d0f-616f08ba29e0.json └─ loaded cached evaluation key; skipping online registration [━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━] LATTICA QUERY │ running encrypted query ✓ └─ duration: 2.77s └─ ciphertext 6.1 MB └─ Timing summary • apply_hom_pipeline Step Time Percent ──────── ────────── ──────── upload 1.269 s 47.9% download 1.250 s 47.2% instance 93.0 ms 3.5% logic 32.0 ms 1.2% worker 7.0 ms 0.3% ──────── ────────── ──────── Total 2.651 s 100.0% Accuracy: 96.0% [━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━] LATTICA BUILDER │ stop worker ✓ └─ duration: 2.01s
Things to try
Change the parameters
Adjust n, the scale, or the precision levels and redeploy. See what still compiles and how results shift.
Swap an operator
Try a different activation or layer in the body and see how it affects compilation and accuracy.
Build your own MNIST
Write the pipeline yourself. If it does not compile, compare it against this one to find the difference.
Start a different model
Once the flow makes sense, swap in your own weights and a body that matches your network.
These are examples. The script is a sandbox, change what you want and rerun.
When it clicks
Build your own pipeline
Once this example feels clear, you are ready to build a pipeline for your own model. Start from the operators and the structure.