Skip to main content

Level budget & the modulus chain

On the Ciphertext state page we saw that every multiplication grows a ciphertext's scale, and that a mod-switch is what brings it back down. What we deferred until now: bringing scale down isn't free. Each mod-switch permanently spends a piece of the ciphertext's modulus chain, and once the chain is spent, no further multiplications are possible. Planning a pipeline means making sure the chain you configure is long enough for the multiplications you intend to do. That plan is your level budget, and this page is about how to make it.

The chain: rows and columns

The chain is configured once, at context creation, through full_q_list_precision. The digit recognizer on Build the Pipeline uses ((61,), (45,)). The illustration below adds extra columns so you can see both rows and columns:
params.pyPYTHON · CHAIN
full_q_list_precision = (
    (61,),        # row 0: one prime ≈ 2^61
    (61, 30),     # row 1: two primes ≈ 2^61 and 2^30
    (61,),        # row 2: one prime
)
Each entry is a row, and each number inside a row is a column, the bit-size of one prime. A fresh ciphertext starts with the entire chain active:
Column
Row0
61≈ 2^61
Row1
61≈ 2^61
30≈ 2^30
Row2
61≈ 2^61
A fresh ciphertext starts with the entire chain active: every row, every column.
Spending from the chain means dropping something, and there are two granularities:
01. Drop a column.

Removes one prime from within a row. The cheaper, finer-grained option: the row survives with a smaller modulus.

02. Drop a row.

Removes the entire row at once. Coarser, and what happens when a row has no spare columns left to give.

This is why HomModSwitch has a variant parameter (0 = row, 1 = column); you're choosing which of these two spends to make. Most of the time you won't choose manually; see Mod-switch below.

Rules when writing the tuple: every bit-size must be in 20–61, values within a row must be strictly descending, and the difference between adjacent values must itself fall in 20–61. Break any of these and the context refuses to initialize.

Multiplicative depth

So how long does your chain need to be? Count your multiplicative depth: the longest chain of multiplications any single value passes through from input to output. Only the deepest path matters; operators on parallel branches don't add up.
For budgeting purposes, every operator falls into one of three types:
01
Free
No levels consumed. Additions (HomAdd, HomConstAdd, etc.), slot sums (HomSumSlots), and everything client-only (HomRelu, HomReshape, etc.).
02
Fixed cost
A predefined number of levels, usually one. The multiply family (HomSquare, HomConstMul, etc.) each spend one level through their built-in mod-switch (with_modswitch, on by default).
03
Configurable cost
You choose the spend. For example, HomExpand and HomRunningSum consume roughly len(stage_sizes) / stages_per_level levels; see Level collapse below. Or, HomPolyEval consumes multiple levels depending on the polynomial degree you give it.

The exact level cost of every operator is stated in the Level budget field of its reference page. That field is the source of truth; the types above are how to think about it.

To budget a pipeline: walk your deepest path, classify each operator, and sum the costs.
Input
HomReshape
0
HomMul
1
HomSquare
1
HomAdd
0
HomExpand(2,2,2)
3
Output
0 + 1 + 1 + 0 + 3 =depth 5
Your chain needs at least that many levels beyond the one your ciphertext arrives on. When in doubt, print the compiled graph as described in Ciphertext state → Visualizing it yourself: each node shows q-list state (q=[…]), so you can watch the budget shrink op by op and see exactly where a too-short chain runs dry. You can also inspect each HomValue in the debugger for active rows and active columns on the q-list (remaining levels = their counts multiplied). See Inspecting the pipeline.
If your pipeline's depth exceeds any chain you can reasonably configure, that's what Bootstrap is for.

Mod-switch: automatic vs. explicit

You rarely place mod-switches yourself. Every operator that performs a mod-switch (HomMul, HomConstMul, HomSquare, etc.) takes with_modswitch=True by default: after the operation, the cheapest available spend is made for you: a spare column from the first row that has one, otherwise a whole row.
Reach for an explicit HomModSwitch when you want control over what gets dropped and when, for example, to spend a specific row before a section of your pipeline that needs the remaining ones intact.
Either way, the effect on state is the same and links back to what you already know: the ciphertext's pt_scale is multiplied by new_q / prev_q; that's the mechanism by which scale comes back down.

Level adjustment

There's one more way levels get spent, and it's implicit: when two ciphertexts meet at a HomAdd or HomMul, they must be on compatible chains: one's active rows and columns must be a subset of the other's. If they aren't, the higher-level ciphertext is automatically trimmed down to match the lower one before the operation runs. No parameter controls this; it simply means that combining a fresh ciphertext with a heavily-spent one costs the fresh one its advantage. Keep parallel branches at similar depths if you want to avoid paying for it.

Restricting the spend: rows_budget

Every operator that performs a mod-switch, including HomPolyEval, accepts a rows_budget parameter: a list of row indices that automatic mod-switch is allowed to spend from. Rows outside the budget are off-limits. Leave it unset unless you need to pin which rows may be spent.

Level collapse

A few operators (HomExpand, HomRunningSum) work through multiple internal rounds of multiplications, and would normally mod-switch between rounds. Their stage_sizes and stages_per_level parameters let you merge rounds:
Full depth (k=8)
stage_sizes = (2, 2, 2)
Round 1Round 2Round 3
3 rounds, 3 chances to mod-switch
Collapsed
stage_sizes = (8,)
Round 1
1 round, more rotations per round
The trade: fewer rounds means fewer levels spent, at the cost of more work (and more scale growth) per round. stages_per_level tunes the same trade from the other side: how many rounds to group together before each mod-switch. The exact knobs for each operator are on its reference page.

Next

Continue to Slots, rotations & stages for how slot operators spend rotations and levels. If your pipeline is deeper than any chain you can configure, continue to Bootstrap, refreshing levels mid-pipeline. Or jump to Choosing parameters to turn your depth count into an actual full_q_list_precision.