Skip to main content

HomOps Reference

HomMul

Multiply two encrypted values together.

MuArithmetic
RunsServer
Changes shapeYes: broadcast; keep_axis can retain axis_sum as size 1
Changes scaleYes: scales multiply
Levels spent1 by default, 0 with with_modswitch=False
RotatesNo
KeysSquare key

What it does

HomMul multiplies two ciphertexts element by element, then relinearizes the result back to a normal ciphertext. With axis_sum it becomes a dot product: multiply, then sum along one axis in the same step.
It takes two ciphertext inputs: both operands are encrypted values, with no plaintext weights. Two related operators cover common special cases: to square a single value (), HomSquare is HomMul of a value with itself; to multiply by a fixed weight or constant, HomConstMul multiplies a ciphertext by a plaintext and needs no key.
Multiplication is what drives your parameter budget: every multiply combines the scales of its two inputs, and that growth is what the modulus chain has to absorb. If you've read Choosing parameters, this is the operation that spends the budget that page is about.

Implicit HomMul: just write x * y

You don't have to spell out HomMul yourself. When you multiply two encrypted values with the * operator, the compiler infers a HomMul behind the scenes:
pipeline.pyPYTHON
y = x + pt        # HomConstAdd inferred (ciphertext + plaintext)
result = x * y    # HomMul inferred (ciphertext × ciphertext)
The inferred op uses the defaults shown in the signature below. Write HomMul(...) explicitly only when you need to set a parameter, for example axis_sum to turn the product into a dot product, or rows_budget to pin which rows automatic mod-switch may spend.

Signature

HomMul(
    axis_sum=None,
    keep_axis=False,
    with_modswitch=True,
    rows_budget=None,
)
HomMul takes no weights, so there is no set_data call; both operands are ciphertexts from earlier in the computation.

Parameters

ParameterTypeDefaultDescription
axis_sumintNoneIf set, sum the product along this axis. The axis is removed unless keep_axis=True. Turns an element-wise product into a dot product along that axis. Leave unset for a plain element-wise multiply.
keep_axisboolFalseWhen axis_sum is set, keep the summed axis as size 1 instead of removing it.
with_modswitchboolTrueA mod-switch runs after the multiply: it rescales the result and spends one level. Set to False to leave scale climbing and manage it yourself with HomModSwitch.
rows_budgetsequenceNoneRestricts which levels the mod-switch may spend, by absolute row index. Only relevant when with_modswitch=True. See rows_budget.
Rules that depend on the value
SettingRuleIf you break it
axis_sumMust be an axis of the tensor shape, and cannot be the n axis. That axis is packed across ciphertext slots and can't be summed away this way. Negative indices are allowed (-1 is the last axis).Compilation fails. To sum across slots, use a reduction op such as HomSumSlots.
rows_budgetAt least one listed row must still be droppable when the mod-switch runs.Compilation fails if none are eligible.

Requirements

  • Same n axis. Both inputs must share the same n axis to broadcast.
  • Compatible levels. The two inputs must sit on compatible modulus chains (one's active levels a subset of the other's). When they differ, the higher input is automatically brought down to match before the multiply; you don't call this. Chains that can't be reconciled fail at compile time.
These surface as compilation errors; see Compilation errors.

Shape effect: yes

The two inputs broadcast against each other the way array shapes do, on every axis except the n axis, which must already match. With axis_sum set, that axis is summed and removed unless keep_axis=True, which retains it with size 1.
The n axis is marked in bold in each shape below:
Input AInput Baxis_sumOutput
(128,)(128,)None(128,) element-wise
(4, 8)(4, 1)None(4, 8) with B broadcast across the second axis
(rows, dim)(rows, dim)1(rows,) one dot product per row over dim
(rows, dim)(rows, dim)1, keep_axis=True(rows, 1) retained summed axis
Note that in the dot-product row, axis_sum=1 sums over dim (not the n axis), which is why the n axis (rows) survives into the output.
The two streams merge at the multiply:
A(4, 8)@scale S
B(4, 1)@scale S
out(4, 8)@scale

broadcast shapes, multiplied scales

Shape and the n axis are covered on Concepts.

Scale effect: yes

The output scale is the product of the two input scales: two inputs at scale S give a result at . This is the growth the modulus chain must hold, and the reason a multiply is the costly step.
StageEffect on scale
After the multiplyscale_out = scale_a × scale_b
with_modswitch=TrueMultiplied by new_q / prev_q (i.e. divided by the dropped prime), spending one level. How far the scale comes down depends on the size of the prime that was dropped.
How scale moves through a pipeline is on Ciphertext state → Scale.

Level budget

ConfigurationLevels spent
with_modswitch=True (default)1 (drops one prime from the chain)
with_modswitch=False0 (but scale keeps climbing until you mod-switch yourself)
Aligning the two inputs to a common level before the multiply, and the relinearization afterward, are part of the operation and don't spend chain levels of their own. rows_budget limits which level the optional drop may touch. How primes are organized in the chain, and what exactly gets dropped, is explained on Level budget → The chain.

Keys

Square key (relinearization key). Multiplying two ciphertexts produces an oversized intermediate that must be reduced back to a normal ciphertext; the square key makes that reduction possible. It's generated for you during key generation; you never request it directly. Any pipeline containing a HomMul or HomSquare needs one.

Example

Compute result = x * (x + pt): an encrypted value gated by a shifted copy of itself. With operator inference you never write HomMul at all:
pipeline.pyPYTHON
# x: encrypted input
# pt: plaintext constant

y = x + pt        # HomConstAdd inferred
result = x * y    # HomMul inferred: element-wise, with_modswitch=True
To demonstrate axis_sum, add the flag to the same multiply. If x and y are both shaped (rows, dim) with the n axis on rows, summing over axis 1 turns the element-wise product into one dot product per row:
operators.pyPYTHON
from lattica_build.operators import HomMul

# same inputs as above, but contract over dim
dot = HomMul(axis_sum=1)(x, y)   # output shape: (rows,)

See also