Basic CKKS workflow
Example source: examples/01_basic_ckks_flow.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
engine = fh.CkksEngine(fh.Preset.slots8192_scale40_levels7_int64, device="cpu")2
3
A CkksEngine is process-local and owns one device. Distributed execution is expressed separately through fhelium.distributed collectives. Selecting device="cuda:0" dispatches the same engine operations and torch.ops schemas to CUDA; FHElium does not use backend-specific public methods or hidden transfers.
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 level, 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 level 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_level(engine.relinearize(product_triplet))2
3
4
FHElium deliberately does not hide rescale or relinearization. This makes the level, 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 an exact key
rotation_key = engine.rotation_key(1)
ct_rotated = engine.rotate_with_key(ct_x, rotation_key)2
A rotation key is bound to one canonical 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 an chosen numerical tolerance appropriate for the scale, depth, input range, and workload.
Complete runnable source
The source below is included directly from the tested repository example, so the tutorial does not maintain a second copy of the complete program.
#!/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("--level", 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, level=args.level)
ct_y = engine.encrypt_message(y, level=args.level)
# 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_level(
engine.relinearize(product_triplet)
)
ct_rotated = engine.rotate_with_key(ct_x, engine.rotation_key(1))
sync_if_cuda(engine.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), reference, slots)
rows.append(
[name, ct.level, f"{error['max_abs']:.3e}", f"{error['rms']:.3e}"]
)
print(engine)
print_table(
["operation", "output level", "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