Getting started¶
Start from a fresh checkout. You will run Ren locally, try a CKKS computation, inspect a small polynomial graph, and see one JIT replay.
If your first work is CKKS usage, run the setup and CKKS sections first, then come back to the polynomial graph when you need the underlying execution model.
Local setup¶
Install the project dependencies with uv:
Use just for project commands:
CUDA tests require a CUDA machine. On a laptop or CPU-only checkout, use the Python backend and CPU-safe tests first.
First CKKS computation¶
CKKS code sits on top of the same polynomial engine. The example below uses CKKSParams.python_test(), which is a small, non-secure parameter set intended for local examples and tests.
import numpy as np
from ren.schemes.ckks import CKKSParams, decode, decrypt, encode, encrypt, keygen
params = CKKSParams.python_test()
ctx = keygen(params)
values = np.array([0.25, 0.5, 0.75, 1.0])
with ctx:
pt = encode(values, msg_bound=1.0)
ct = encrypt(pt)
result = ct + 1
level = result.level
decoded = decode(decrypt(result), length=len(values))
print("level:", level)
print("slots:", [round(z.real, 3) for z in decoded])
level: 5
slots: [1.25, 1.5, 1.75, 2.0]
The example decodes the first four slots: [0.25, 0.5, 0.75, 1.0] becomes [1.25, 1.5, 1.75, 2.0]. Sequence encoding pads the remaining CKKS slots with zeros, and scalar constants apply to every slot, including that padded tail. The printed level stays at the fresh-encryption level because adding an integer scalar does not rescale.
Track two details from the first run:
msg_boundis public metadata. It must be chosen independently of secret values.- Many CKKS properties, such as
level, require an activeCKKSContext.
First polynomial graph¶
Poly operations are lazy. They build an IR graph and execute when you realize or export the value.
from ren.poly import Poly
from ren.ring import RingSpec
ring = RingSpec(n=8, moduli=(17, 97))
a = Poly([1, 2, 3, 4, 0, 0, 0, 0], ring)
b = Poly([4, 3, 2, 1, 0, 0, 0, 0], ring)
result = (a + b) * 2
print("coefficients:", result.tolist())
print("node:", result.node)
coefficients: [10, 10, 10, 10, 0, 0, 0, 0]
node: 0 UNIQUE 0
1 DEVICE python
2 BUFFER [0, 1] size=8
3 TO_RNS [2]
4 UNIQUE 1
5 BUFFER [4, 1] size=8
6 TO_RNS [5]
7 ADD [3, 6]
8 CONST [1] 2
9 AS_RING [8] coeff (n=8 q=<11b> moduli=2 dtype=u64)
10 MUL [7, 9]
====== Summary ======
UNIQUE 2
BUFFER 2
TO_RNS 2
DEVICE 1
ADD 1
CONST 1
AS_RING 1
MUL 1
The printed node trace is the graph Ren will schedule. The inputs are buffers, the arithmetic stays symbolic, and the scalar 2 appears as a constant node.
First JIT replay¶
ren.jit works on arguments and return values that contain Poly values directly or inside ordinary containers and dataclasses. CKKS Ciphertext and Plaintext objects work because they carry Poly fields inside dataclasses.
import ren
from ren.schemes.ckks import CKKSParams, Ciphertext, decode, decrypt, encode, encrypt, keygen
params = CKKSParams.python_test()
ctx = keygen(params)
captures = 0
@ren.jit
def affine(x: Ciphertext) -> Ciphertext:
global captures
captures += 1
return (x + 1) * 0.5
with ctx:
x1 = encrypt(encode([1.0, 2.0, 3.0], msg_bound=4))
y1 = affine(x1)
print("captures after first call:", captures)
print("first output:", [round(z.real, 3) for z in decode(decrypt(y1), length=3)])
x2 = encrypt(encode([2.0, 3.0, 4.0], msg_bound=5))
y2 = affine(x2)
print("captures after second call:", captures)
print("second output:", [round(z.real, 3) for z in decode(decrypt(y2), length=3)])
captures after first call: 1
first output: [1.0, 1.5, 2.0]
captures after second call: 1
second output: [1.5, 2.0, 2.5]
The first matching call captures a replay plan for the contained polynomial buffers. The second call reuses that plan because the ciphertext components have the same ring, size, dtype, device, and domain, so the capture counter stays at one.
Next¶
- Read Build a CKKS circuit for the first reusable encrypted computation.
- Read CKKS workflow for the practical encrypted-computation workflow.
- Read CKKS concepts for levels, scales, message bounds, and keys.
- Read Engine overview for the core
Poly -> Node -> schedule -> backendmodel. - Read Polynomial engine for ring, RNS, domain, node, constant, and view semantics.
- Read Execution pipeline to connect lazy user code to scheduled backend work.
- Read Scheduling when you need to understand why a graph turns into a particular set of kernels.
- Read Runtime when you need to reason about memory planning, backends, copies, or JIT replay.