Skip to section
Foundationsfor rotation-free search
Section 33 of 5263% of course
Contents
Chapter 6 Section 6.2 72 min

Part IV · Approximate encrypted arithmetic

Scale, round, compute, unscale

Build fixed-point encoding from familiar decimal arithmetic.

Animated visual · ManimPrecision consumes range

Increasing the scale shrinks rounding error while growing the encoded integers and their products.

First, let's remove the mystery from fixed point

Think about how a price database often stores $12.34 as the integer 1234 cents. The database hasn't forgotten the decimal point; every reader just agrees that the stored unit is one hundredth of a dollar. CKKS encoding begins with the exact same idea, but uses a much, much finer unit. The scale Δ just says how many integer steps represent one real unit.

Build your encoding from ordinary rounding

Polynomial coefficients are integers modulo very large primes, while embeddings contain real-valued numbers. Choose a positive scale Δ. Multiply each real coordinate by Δ and round to the nearest integer.

encode(z)=round(Δz)    decode(m)=m/Δ
Interactive fixed-point encoder
0.3700× Δ=25694.720round95÷ Δ0.371094

absolute encoding error = 1.094e-3 · theoretical half-step ≤ 1.953e-3

Rounding to the nearest integer changes Δz by at most one half. After dividing by Δ, the encoding error is at most 1/(2Δ). A larger Δ preserves far more fractional detail but consumes more of your numeric headroom.

Worked example

Let's encode one coordinate by hand

Choose Δ=1000 and z=−0.1376. Then Δz=−137.6, which rounds to −138. Decoding gives −138/1000=−0.138. The error is −0.0004, whose magnitude is nicely below our promised half-step 1/(2Δ)=0.0005.

Multiplication multiplies your scales

If q is encoded near Δq and v near Δv, their product is near Δ²qv. That means a dot-product score extracted after one multiplication absolutely must be divided by Δ².

decoded score ≈ C[tj] / Δ²
def encode(value: float, scale: int) -> int:
    return round(value * scale)

q_hat = encode(0.25, 1000)       # 250
v_hat = encode(-0.40, 1000)      # -400
product_hat = q_hat * v_hat      # -100_000
score = product_hat / 1000**2    # -0.10

Worked example

A real production scale

Doolittle uses Δ=2²⁷. One single encoding step is approximately 7.45×10⁻⁹, and the half-step rounding error is approximately 3.73×10⁻⁹ per coordinate before we even introduce later noise sources.

Use the notebook as a scale microscope

Change the scale bits, then watch two quantities move in opposite directions: rounding error shrinks while the encoded integers and their products rapidly grow. We left the code visible because the whole point is to understand the construction, not to just operate a black-box slider.

Reactive Python laboratory · marimo + PyodideSpend a fixed-point precision budget

Change values and scale bits while tracking rounding error, product scale, integer growth, and remaining headroom.

Open full-screen lab ↗

Runs entirely in this browser. Python executes in Pyodide WebAssembly with no remote kernel. The construction code stays visible while reactive dependents recompute whenever you change an input.

Check your understanding

Two values encoded at scale Δ are multiplied once. What scale does the resulting product carry?

Section summary

  • Scale converts real values into approximate integers.
  • Rounding error falls drastically as Δ grows.
  • One single multiplication changes your scale from Δ to Δ².
Reactive Python laboratory · marimo + PyodideSpend a fixed-point precision budget

Change values and scale bits while tracking rounding error, product scale, integer growth, and remaining headroom.

Open full-screen lab ↗

Runs entirely in this browser. Python executes in Pyodide WebAssembly with no remote kernel. The construction code stays visible while reactive dependents recompute whenever you change an input.

Repository layer · second pass

How does fixed-point encoding turn real values into integers?

Choose scale Δ, multiply each real value by Δ, and round to an integer. Arithmetic then operates on encoded integers. Addition preserves scale; multiplying two encoded values produces scale Δ². Decoding divides by the current scale, not automatically by the original one.

Larger Δ reduces initial rounding error but consumes more modulus headroom. Scale is therefore an error-versus-range budget, not a free precision knob. Track it as metadata beside every intermediate value.

Reasoning chain

  1. 1

    Choose Δ from a bit budget.

  2. 2

    Encode m=round(Δx).

  3. 3

    Record rounding error.

  4. 4

    Propagate scales through operations.

  5. 5

    Decode with the accumulated scale.

  6. 6

    Check coefficient range against modulus headroom.

Worked trace

One multiplication changes scale

  1. x=.37,y=−.22,Δ=256.
  2. Encodings are 95 and −56.
  3. Product is −5320 at scale 65536.
  4. Decoding gives −.0811768 versus exact −.0814.

Result. The error comes from input rounding; dividing by 256 would be a scale bug.

Executable lens · Python

Make the hidden state visible

scale=256
x,y=.37,-.22
mx,my=round(scale*x),round(scale*y)
decoded=(mx*my)/(scale*scale)
assert abs(decoded-x*y) < .001

Retype this example, predict each intermediate value, and then change one input that touches a boundary.

Misconception clinic

Tempting mistakes

  • Adding scales during multiplication instead of multiplying them.
  • Selecting precision without checking overflow/noise headroom.

Retrieval and transfer

Close the book first

  1. Repeat with Δ=16 and compare error.
  2. Track scale through x*y+z when z starts at Δ.
  3. State why rescaling exists in deeper circuits.