Skip to content

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:

uv sync --frozen

Use just for project commands:

just test
just typecheck
just docs build
just docs serve

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_bound is public metadata. It must be chosen independently of secret values.
  • Many CKKS properties, such as level, require an active CKKSContext.

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 -> backend model.
  • 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.