Modulus-chain depth
Example source: examples/03_eager_modulus_chain.py
This example derives several exact Q chains from one preset and compares their maximum public depth, modulus size, active RNS rows, selected execution dtype, and ciphertext storage.
Run the example
python examples/03_eager_modulus_chain.py \
--preset slots32768-scale40-depth34-int642
Select maximum depths:
python examples/03_eager_modulus_chain.py \
--preset slots32768-scale40-depth34-int64 \
--depths 16,24,332
3
1. Maximum depth follows Q-group count
A resolved configuration stores
config.q_depth_groups
config.max_depth2
as exact values. If the groups are
then max_depth == D. Public values use depths from zero through G_D is the ordinary terminal Q group.
The example's prefix_config function constructs a shorter exact parameter set by retaining the requested public-group prefix and the same terminal Q group:
CkksConfig(
default_scale=source.default_scale,
q_depth_groups=(
*source.q_depth_groups[:max_depth],
source.q_depth_groups[-1],
),
p_moduli=source.p_moduli,
logN=source.logN,
)2
3
4
5
6
7
8
9
No count override regenerates an existing configuration. Each constructed CkksConfig records its complete Q groups and P primes.
2. One depth may contain several RNS rows
One public rescale from depth
A one-prime group and a two-prime group both consume one depth. They have different active row counts and may select different native execution formats. Use config.rescale_divisor(d) for the group product and config.active_q_moduli(d) for the active prime sequence.
3. Q and P have different roles
- Q depth groups form the ciphertext-modulus chain.
- P contains special primes used temporarily by hybrid key switching.
- QP appends all P rows to the active Q basis without changing depth.
config.total_modulus_bits is the bit length of the exact complete QP product. When security-budget enforcement is enabled, it must not exceed config.maximum_modulus_bits.
4. Depth-zero values are largest
ciphertext = engine.encrypt_message([1, 2, 3, 4], depth=0)
print(ciphertext.data.nbytes)2
At depth zero, every Q group is active. Later depths contain fewer Q rows. For a two-component ciphertext, payload storage is approximately
where
5. Choose depth from the circuit
Count the public rescale transitions on the intended execution path and reserve the required margin. Then validate precision, message range, security budget, key storage, and latency with the exact groups:
- more public groups provide more transitions;
- more prime rows increase early-depth ciphertext and prepared-plaintext size;
- key switching and relinearization touch the active QP rows;
- group products determine the scale removed by each rescale.
Source
#!/usr/bin/env python3
"""Compare exact Q chains with different maximum public depths.
Run:
python examples/03_eager_modulus_chain.py \
--preset slots32768-scale40-depth34-int64
"""
from __future__ import annotations
import argparse
import math
import torch
from common import add_engine_args, format_bytes, parse_preset, print_table
from fhelium.config import CkksConfig
from fhelium.eager import Engine
def selected_depths(config: CkksConfig) -> list[int]:
"""Return representative maximum depths for one source parameter set."""
candidates = [
max(0, config.max_depth // 2),
max(0, (config.max_depth * 3) // 4),
config.max_depth,
]
return sorted(set(candidates))
def prefix_config(source: CkksConfig, max_depth: int) -> CkksConfig:
"""Retain the requested public Q-group prefix and terminal group."""
if not 0 <= max_depth <= source.max_depth:
raise ValueError(f"max_depth must be in [0, {source.max_depth}]")
return CkksConfig(
default_scale=source.default_scale,
q_depth_groups=(
*source.q_depth_groups[:max_depth],
source.q_depth_groups[-1],
),
p_moduli=source.p_moduli,
logN=source.logN,
sigma=source.sigma,
security_bits=source.security_bits,
enforce_security_budget=source.enforce_security_budget,
galois_generator=source.galois_generator,
)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
add_engine_args(parser, default_preset="slots32768-scale40-depth34-int64")
parser.add_argument(
"--depths",
default=None,
help="Comma-separated max_depth values. Default: middle and full variants.",
)
args = parser.parse_args()
torch.set_default_device(args.device)
source = CkksConfig.parse(parse_preset(args.preset))
depths = (
[int(item) for item in args.depths.split(",")]
if args.depths
else selected_depths(source)
)
rows = []
for max_depth in depths:
config = prefix_config(source, max_depth)
engine = Engine(config, ntt_backend=args.ntt_backend)
ciphertext = engine.encrypt_message([1, 2, 3, 4], depth=0)
rows.append(
[
max_depth,
config.num_q_primes,
config.num_p_primes,
config.total_modulus_bits,
config.maximum_modulus_bits,
str(engine.dtype).removeprefix("torch."),
format_bytes(ciphertext.data.nbytes),
]
)
print(
f"Source preset {args.preset}: "
f"log2(default_scale)={math.log2(source.default_scale):.3f}, "
f"terminal Q rows={len(source.q_depth_groups[-1])}."
)
print_table(
[
"max depth",
"Q rows",
"P rows",
"QP bits",
"security budget bits",
"RNS dtype",
"depth-0 CT size",
],
rows,
)
print(
"\nOne public rescale consumes one Q depth group. Ciphertext size and "
"native work follow the active prime-row count inside those groups."
)
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113