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.
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 Δ².
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.10Worked 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.
Change values and scale bits while tracking rounding error, product scale, integer growth, and remaining headroom.
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 Δ².
Change values and scale bits while tracking rounding error, product scale, integer growth, and remaining headroom.
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
Choose Δ from a bit budget.
- 2
Encode m=round(Δx).
- 3
Record rounding error.
- 4
Propagate scales through operations.
- 5
Decode with the accumulated scale.
- 6
Check coefficient range against modulus headroom.
Worked trace
One multiplication changes scale
- x=.37,y=−.22,Δ=256.
- Encodings are 95 and −56.
- Product is −5320 at scale 65536.
- 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) < .001Retype 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
- Repeat with Δ=16 and compare error.
- Track scale through x*y+z when z starts at Δ.
- State why rescaling exists in deeper circuits.