Encrypted inference means running a model on an input that the server never sees in the clear. There are three main ways to do it. A trusted execution environment decrypts inside attested hardware. Homomorphic encryption computes on ciphertexts. Secure multi-party computation (MPC) splits the data into random-looking shares held by parties who must not collude. The site's secure inference overview compares the three, the CKKS article covers homomorphic encryption and the confidential compute article covers attested GPUs.
This article goes inside the MPC option, because it is the one most engineers have never seen work. We build two-party secret-shared inference from first principles: additive shares over a 64-bit ring, fixed-point numbers, Beaver triples for multiplication, and truncation with its small chance of failure. We run a tested layer in Python, then scale the cost up to a transformer attention head. Finally we look at why softmax, GELU and LayerNorm dominate the bill, what MPC still leaks, and when it is worth using.
Additive shares and fixed point
Work in the ring of integers modulo 2^64, which is what a 64-bit CPU register does when it overflows. To share a value v between two parties, pick a uniformly random r, give r to party 0 and v - r to party 1. Each share on its own is uniformly random and says nothing about v. Adding the shares gives v back.
Addition is free. If both parties hold shares of x and y, each adds its two shares locally and the results are shares of x + y, with no communication. Multiplying by a public constant is also local. Adding a public constant is local too, but only one party may add it, otherwise it is counted twice. That rule comes back when we multiply.
Neural networks need real numbers, so use fixed point. Multiply each real by 2^16 and round, giving 16 fractional bits. Negative numbers wrap round to the top half of the ring, as in two's complement. The catch is that a product of two encoded values has 32 fractional bits, so after every multiplication you must shift right by 16. In secret-shared form that shift is the hard part.
Multiplication with Beaver triples
Multiplying two shared values needs interaction. Beaver's trick, from 1991, uses a precomputed random triple (a, b, c) with c = a times b, secret-shared between the parties. To multiply shared x and y:
- Each party computes its share of e = x - a and f = y - b, and both parties open e and f. Because a and b are uniformly random and used once, e and f reveal nothing about x or y.
- Each party computes its share of z = c + e*b + f*a + e*f. Expanding shows z = x*y.
- Only party 0 adds the public term e*f. If both add it, the result is off by e*f. This is the most common bug in first implementations.
The cost of one multiplication is one round of communication, two ring elements sent per party, and one triple used up. Triples do not depend on the input, so they can be made in an offline phase before the request arrives. Who makes them matters. A trusted dealer is simple and many frameworks offer it for testing, but it must not collude with either server. Production protocols generate triples between the two parties with oblivious transfer or homomorphic encryption, which is where much of the real cost goes.
Truncation and its failure probability
After a multiplication the shared product carries 32 fractional bits. The simple fix, from SecureML, is local truncation: party 0 shifts its share right by 16, and party 1 shifts the negation of its share and negates again. The result is correct up to one unit in the last place, with one exception. If the random share happens to sit near the point where the ring wraps, the reconstructed value is wildly wrong. The chance is about 2^(k + 1 - 64), where k is the bit length of the true value. With 16 fractional bits and values in the thousands it is around one in 10^11 per truncation. That is rare, but a large model performs a great many truncations. Production systems either budget for it, use a wider ring, or use an interactive truncation protocol that costs another round.
A working two-party layer
Here is a complete two-party layer. Both servers hold shares of the input and the weights, compute a dot product, and apply a quadratic activation of the kind MPC-friendly models use in place of GELU. Python integers are used so there is no overflow trap.
import secrets
MOD = 1 << 64
FRAC = 16
SCALE = 1 << FRAC
def encode(x): return round(x * SCALE) % MOD
def decode(v): return (v - MOD if v >= MOD // 2 else v) / SCALE
def share(v):
r = secrets.randbelow(MOD)
return r, (v - r) % MOD
def reveal(s): return (s[0] + s[1]) % MOD
def add(x, y): return (x[0] + y[0]) % MOD, (x[1] + y[1]) % MOD
def add_public(x, k): return (x[0] + k) % MOD, x[1] # party 0 only
def dealer_triple():
a, b = secrets.randbelow(MOD), secrets.randbelow(MOD)
return share(a), share(b), share(a * b % MOD)
def beaver_mul(x, y, triple):
(a0, a1), (b0, b1), (c0, c1) = triple
e = reveal(((x[0] - a0) % MOD, (x[1] - a1) % MOD)) # opens x - a
f = reveal(((y[0] - b0) % MOD, (y[1] - b1) % MOD)) # opens y - b
z0 = (c0 + e * b0 + f * a0 + e * f) % MOD # e*f: party 0 only
z1 = (c1 + e * b1 + f * a1) % MOD
return z0, z1
def truncate(z):
# Local truncation: off by at most 1 ulp, but fails with
# probability about 2**(bits(x) + 1 - 64).
t0 = z[0] >> FRAC
t1 = (MOD - ((MOD - z[1]) % MOD >> FRAC)) % MOD
return t0, t1
def mul(x, y): return truncate(beaver_mul(x, y, dealer_triple()))
def mul_public(x, k): return truncate(((x[0] * encode(k)) % MOD, (x[1] * encode(k)) % MOD))
def dot(xs, ws):
acc = (0, 0)
for x, w in zip(xs, ws):
acc = add(acc, mul(x, w))
return acc
def quad_act(x): # 0.125x^2 + 0.25x + 0.5, an MPC-friendly GELU stand-in
return add_public(add(mul_public(mul(x, x), 0.125), mul_public(x, 0.25)), encode(0.5))
x, w = [0.5, -1.25, 2.0], [0.8, 0.3, -0.4]
h = dot([share(encode(v)) for v in x], [share(encode(v)) for v in w])
y = quad_act(h)
print(decode(reveal(h)), decode(reveal(y))) # about -0.775 and 0.38133The plaintext answers are -0.775 and 0.381328. The shared version matches to about four decimal places, and the small error comes from the fixed-point rounding at each truncation. In a real deployment the two tuples in each pair live on different machines, and every call to reveal is a network message.
Worked example: cost of one attention head
Scale this up to one attention head of a BERT-base-sized encoder: sequence length 128, head dimension 64. The score matrix Q times K-transpose needs 128 x 128 x 64 = 1,048,576 scalar multiplications. With one triple per scalar, each party sends two 8-byte values per multiplication, which is about 16.8 MB per party for one head of one layer.
Matrix triples fix most of that. Share random matrices A and B and C = AB, open E = Q - A and F = K-transpose - B once, and compute the shares of C + EB + AF + EF locally, with EF added by one party. Now each party sends E and F: 128 x 64 + 64 x 128 = 16,384 values, or 131 KB. That is 128 times less. Linear layers are therefore not the bottleneck in well-built systems. Most of the remaining traffic and rounds go elsewhere.
Why the non-linear layers dominate
Additions and multiplications are cheap in MPC. Comparisons, exponentials, division and square roots are not, and a transformer uses all of them:
- Softmax needs the row maximum, which takes a tree of secure comparisons with many rounds, then an exponential, often approximated by repeated squaring of (1 + x / 2^n), then a reciprocal, usually by a few Newton iterations.
- GELU has no cheap exact form. Systems use piecewise polynomials, which need comparisons to choose the piece, or simpler stand-ins.
- LayerNorm needs an inverse square root, again by Newton iteration, each step costing several multiplications.
Published work attacks this from two sides. Protocol papers for the client-server setting, where the client holds the input and the server the model, include Iron (NeurIPS 2022), BOLT (IEEE Security and Privacy 2024) and BumbleBee (NDSS 2025). They combine homomorphic encryption for the linear layers with specialised protocols for the non-linear ones, and each reports large cuts in communication over its predecessors. From the model side, MPCFormer (ICLR 2023) replaces GELU and softmax with quadratic approximations and uses knowledge distillation to recover accuracy. The cost of a single input is still measured in seconds to minutes and in large amounts of network traffic, so check each paper for its exact network setting before you compare numbers.
Generation makes it worse. An autoregressive model runs the full protocol once per output token, with the KV cache kept in shared form, so the cost grows with the length of the reply. Most published evaluations use encoders or small decoders for this reason.
What MPC still leaks
Secret sharing hides values, not everything:
- Shapes and timing. Sequence length, the number of generated tokens and the time per step are visible to both servers. Pad inputs to fixed buckets if length is sensitive.
- The architecture. In client-server protocols the client learns the layer structure, and in most designs the activation approximations too.
- The output. The client sees the result, so a curious client can probe the model, and repeated queries support attacks such as membership inference. Rate limits and output policies still apply.
- Collusion and dishonesty. Everything here assumes semi-honest servers that follow the protocol and do not pool their shares. Protection against a server that deviates needs maliciously secure protocols, which cost more. Non-collusion is a contract and governance question as much as a cryptographic one.
Failure modes
Typical failures in MPC inference projects:
- Both parties add the public term e*f, or both add a public bias, so results are off by a constant.
- A triple is reused across two multiplications, which lets an observer subtract the openings and learn x1 - x2.
- Too few fractional bits, so deep networks drift; too many, so truncation failure becomes likely.
- The two servers run in the same cloud account under one administrator, which voids non-collusion.
- Accuracy checked in plaintext only. Approximated softmax and GELU change outputs, so evaluate the MPC model.
Trade-offs
| Approach | Trust assumption | Cost | Good fit |
|---|---|---|---|
| TEE with attestation | Hardware vendor and attestation chain | Small overhead | Large LLMs today |
| Homomorphic encryption | Cryptography only | Very high for deep models | Small models, retrieval scoring |
| Two-server MPC | Servers do not collude | High; network-bound | Small models where hardware trust is unacceptable |
| Client-server 2PC (HE plus OT) | Cryptography; semi-honest server | High; seconds to minutes per input | Classification on sensitive inputs |
What to do next
- Run the code above, then break it on purpose: make both parties add e*f and watch the result go wrong.
- Replace the per-scalar dot product with a matrix triple and count the values each party sends.
- Write down your threat model: who runs each server, and why they would not collude.
- Prototype a small classifier in an MPC framework and measure latency on your real network, not on localhost.
- Compare accuracy with MPC-friendly activations against the original model on your own evaluation set.
- If the target is a large generative model, cost a TEE design from confidential computing for LLM inference alongside MPC before deciding.