Basic CKKS workflow
Example source: examples/01_eager_basics.py
This example encrypts dense tensor messages, evaluates independent addition, multiplication, and rotation branches, then decrypts and checks each result. The tutorial explains the CKKS state transitions in that baseline workflow.
1. Create a local engine
import fhelium as fh
from fhelium.eager import Engine
engine = Engine(fh.Preset.slots8192_scale40_depth7_int64)2
3
4
An eager Engine is process-local and creates device-specific arithmetic resources when they are first needed. Source and material factories use PyTorch's default device unless the call supplies device=. Distributed execution is expressed separately through fhelium.distributed collectives. Passing device="cuda:0" to a boundary or factory call places that result on CUDA; ordinary homomorphic operations dispatch from their operands. Key material must already be on the operation device unless the Engine was created with allow_automatic_key_replication=True.
2. Encrypt messages
import torch
x = torch.linspace(-0.05, 0.05, engine.num_slots, dtype=torch.float64)
y = torch.linspace(0.02, -0.02, engine.num_slots, dtype=torch.float64)
ct_x = engine.encrypt_message(x)
ct_y = engine.encrypt_message(y)2
3
4
5
6
7
The returned Ciphertext carries its depth, scale, prime IDs, polynomial domain, modulus basis, and residue representation alongside one dense tensor.
3. Evaluate an operation that preserves state
ct_sum = engine.add(ct_x, ct_y)add is out of place and requires compatible ciphertext layouts. It does not change the depth or scale.
4. Prepare and multiply ciphertexts
mul_x = engine.coefficient_domain_to_ntt_domain(ct_x)
mul_y = engine.coefficient_domain_to_ntt_domain(ct_y)
product_triplet = engine.multiply(mul_x, mul_y)
ct_product = engine.rescale_to_next_depth(engine.relinearize(product_triplet))2
3
4
FHElium deliberately does not hide rescale or relinearization. This makes the depth, representation, and key-switch transitions visible to algorithms that reuse NTT-domain operands or delay relinearization. With default-scale inputs, the product carries scale
5. Rotate with a key
rotation_key = engine.rotation_key(1)
ct_rotated = engine.rotate_with_key(ct_x, rotation_key)2
A rotation key carries one normalized signed step. Applications choose which keys exist and where they reside.
6. Decrypt and check approximation error
sum_clear = engine.decrypt_message(ct_sum)[: engine.num_slots]
torch.testing.assert_close(sum_clear, x + y, atol=2e-5, rtol=0)2
CKKS is approximate. Validate results with a chosen numerical tolerance appropriate for the scale, depth, input range, and workload.
Source
#!/usr/bin/env python3
"""Basic dense-tensor CKKS encrypt/decrypt and arithmetic flow."""
from __future__ import annotations
import argparse
import torch
from common import (
add_engine_args,
error_stats,
make_engine,
print_table,
small_complex_vector,
sync_if_cuda,
)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
add_engine_args(parser)
parser.add_argument("--depth", type=int, default=0)
args = parser.parse_args()
engine = make_engine(args)
slots = engine.num_slots
x = small_complex_vector(slots, seed=1)
y = small_complex_vector(slots, seed=2)
ct_x = engine.encrypt_message(x, depth=args.depth)
ct_y = engine.encrypt_message(y, depth=args.depth)
# These are three independent branches. add is out-of-place and does
# not feed the multiplication or rotation below.
ct_sum = engine.add(ct_x, ct_y)
# Direct CKKS operands enter multiplication at the ordinary scale. The
# pending square scale is consumed after relinearization.
mul_x = engine.coefficient_domain_to_ntt_domain(ct_x)
mul_y = engine.coefficient_domain_to_ntt_domain(ct_y)
product_triplet = engine.multiply(mul_x, mul_y)
ct_product = engine.rescale_to_next_depth(
engine.relinearize(product_triplet)
)
ct_rotated = engine.rotate_with_key(ct_x, engine.rotation_key(1))
sync_if_cuda(torch.get_default_device())
rows = []
for name, ct, reference in [
("add", ct_sum, x + y),
(
"NTT + multiplication + relinearization + rescale",
ct_product,
x * y,
),
("rotate(+1)", ct_rotated, torch.roll(x, shifts=1, dims=0)),
]:
error = error_stats(engine.decrypt_message(ct).cpu(), reference, slots)
rows.append(
[name, ct.depth, f"{error['max_abs']:.3e}", f"{error['rms']:.3e}"]
)
print(engine)
print_table(
["operation", "output depth", "max abs error", "rms error"], rows
)
print(
"Unbatched ciphertext layout: "
f"[component, limb, coeff]={tuple(ct_x.data.shape)}, "
f"batch_shape={tuple(ct_x.batch_shape)}, "
f"prime_ids={ct_x.prime_ids}, bytes={ct_x.data.nbytes}"
)
if __name__ == "__main__":
main()2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77