Moving a navigation model to a server can expose more than a map. Laser observations, a goal direction, and the state accumulated along a route can all reveal information about the robot's surroundings. Keeping those inputs encrypted turns an ordinary prediction into a computation that needs to keep going across control cycles.

This experiment is about local navigation: predict linear and angular velocity for the next control period from laser readings, a relative goal, and the action currently being held. The part I wanted to understand was whether a policy with memory could run continuously while its hidden state stayed encrypted.

The candidate discussed here is K2 recovered / original trial1, which completed 40 genuine FHE simulation episodes. “Genuine” means that inference performed ciphertext arithmetic and bootstrapping. The environment was still IR-SIM, with client and server tested on the same machine. A physical robot, ROS, and a cross-machine service were not part of this experiment.

Start with the privacy boundary

Homomorphic encryption lets an evaluator compute on ciphertexts and lets the private-key holder decrypt the result. CKKS supports approximate numerical arithmetic, including encrypted addition, multiplication, and precision management. The original CKKS paper provides the background.

The logical client holds the private key, encrypts observations and initial state, and decrypts the resulting action. The server has plaintext model parameters and evaluation material. It maintains the encrypted hidden state and memory projection without decrypting them during inference.

text
Laser / goal / currently held action
    -> client preprocessing and encryption
    -> encrypted server policy + recurrent ciphertext state
    -> client action decryption
    -> execution at the next control boundary

Preprocessing and action execution remain on the client side. The benchmark also reads plaintext observations to run an independent reference and check numerical agreement. That is validation infrastructure, not a production server's privacy boundary. Protecting model weights and authenticating a malicious server's computation are outside the current implementation.

The input includes the action already running

Each observation has 41 dimensions:

InputDimensionsMeaning
Laser sectors36Clip 180 rays to 0–7 meters, take the minimum of each group of five, then divide by 7
Relative goal distance1min(distance / 10, 1)
Relative goal direction2cos(angle) and sin(angle)
Held action2The normalized linear and angular commands currently being executed

The final two dimensions matter. While a new action is being calculated, the environment continues executing the old command. The input must describe the action actually held during that interval. Substituting a newly predicted action that has not executed would change the causal order.

The network produces two normalized action values. The client clips them and maps them to velocities:

text
a0, a1 = clip(action, -1, 1)
linear_velocity  = 0.25 * (a0 + 1)  m/s
angular_velocity = a1               rad/s

The default ranges are 0–0.5 m/s and −1–1 rad/s. Clipping happens on the execution side, not inside the encrypted circuit. Ciphertext correctness checks compare the output before clipping, so clipping cannot conceal a circuit error.

Fast observations, slower memory

A recurrent network carries work from one frame into the next. This candidate uses a small two-rate network: a 41-to-32 encoder, a 16-dimensional GRU hidden state, a 32-dimensional action-head intermediate, and two output values. There are 5,378 neural parameters, including 1,024 in the fast path.

The public period is K=2, with frames numbered from zero. Every frame encodes a fresh observation. Even frames update the GRU and memory projection; intervening frames reuse them:

text
e_t = P_enc(W_enc x_t + b_enc)

if t mod 2 == 0:
    h_t = PolyGRU(e_t, h_previous)
    m_t = W_head h_t + b_head
else:
    h_t = h_previous
    m_t = m_previous

a_t = P_out(W_out P_head(m_t + V e_t) + b_out)

Here h is the recurrent state and m is its projection into the action head. The fast path V e_t reads every new observation, so an intervening frame can still change the action. It defers the memory update rather than repeating the previous command.

At a 2 Hz action rate, memory updates at 1 Hz. Increasing K changes the timing of information reaching the policy and generally requires retraining and validation. It is not a performance switch that leaves the model unchanged. This candidate also does not retain an intervening frame's encoding for the next GRU update; two-frame fusion is a separate implementation with separate results.

Match the GRU convention first

The implementation follows PyTorch's reset-after convention. In the candidate state, the reset gate multiplies the hidden projection and its bias:

text
n_t = P_n(W_in e_t + b_in
          + r_t ⊙ (W_hn h_previous + b_hn))

Moving the gate to r_t ⊙ h_previous before that projection would produce a different network. Native PyTorch, a handwritten forward pass, and the independent NumPy reference need to agree before encrypted execution enters the picture.

Translate the network into ciphertext arithmetic

Replace activations with fixed polynomials

The circuit replaces Tanh and Sigmoid with frozen Chebyshev polynomials. Coefficients and domain radii are stored together in configuration:

text
P(s) = Σ c_j T_j(s / R)
LocationDegreeCost in this packed evaluator
Encoder, r/z gates, candidate, output315 limbs
Head154 limbs

Those costs belong to this balanced Chebyshev evaluation plan, including plaintext coefficient multiplications at its leaves. They are not a universal formula for polynomial circuits. Each activation also has its own coefficients and radius.

Normalization constants W/R and b/R are folded into the plaintext affine parameters beforehand. The domain remains a model constraint: the server cannot inspect an encrypted value to detect an excursion, and there is no online plaintext clipping layer. No excursions on sampled trajectories would not establish a bound for every possible input.

Logical slots and ring dimension are different

The Actor uses 64 logical slots with ring dimension 65,536. The former describes this packing layout; the latter is a cryptographic parameter.

Observations occupy the first 41 slots, encoding and memory projection the first 32, and hidden state the first 16. The r/z gates occupy separate windows and use vector coefficients in one packed polynomial call. A 32-slot rotation then aligns z. This layout determines the rotations, projections, and evaluation material the application needs.

Matrix projections use bsgs_hoisted with baby step 8. Several projections of the same encoding can share baby rotations and decomposition work, while still performing their distinct output matrix multiplications. These details determine how small a small neural network really is under encryption.

Refresh the state where recurrence can continue

A continuous policy needs more than a valid single-frame output: it must leave usable ciphertext state for the next update. On an update frame, this circuit refreshes one hidden ciphertext after the complete GRU, then computes the memory projection from it.

Bootstrapping restores capacity for further encrypted computation without giving the evaluator the private key. The OpenFHE CKKS bootstrapping example illustrates the basic flow. The numbers below belong to this experiment's backend: cur_limbs is the current limb count, not multiplication depth already consumed.

StageOutput limbs
Observation input21
After input affine and encoder polynomial15
After the full GRU update2
After hidden-state bootstrapping and alignment15
Memory projection / fast-path projection14
After the head polynomial10
After final affine and output polynomial4

The refreshed h15 serves both the next recurrence and the current action head. Intervening frames retain h15 and m14, computing a fresh encoding and action. Action computation creates temporary ciphertexts instead of writing lower-level results back to those caches. Reading the cache therefore does not consume its level on every frame.

The two frame types also perform different amounts of work. These counts exclude the operations inside bootstrapping:

OperationUpdate frameIntervening frame
MatVec83
Ciphertext × ciphertext8848
Ciphertexts bootstrapped10

Intervening frames avoid the GRU and refresh, but still run the encoder and action head. Total cost does not simply halve.

A half-second period needs a real clock

Physics advances at 20 Hz. Every ten physics steps produce a control boundary, giving a 2 Hz control rate. After frame t is sampled, the robot keeps the old command; a timely prediction starts executing after the next boundary.

text
0.5t seconds: sample an observation and compute a_t
    -> continue executing the old command this period
0.5(t+1) seconds: check whether a_t arrived by the deadline
    -> execute it, or hold the old command and record a miss

The initial held action is [-1, 0], which maps to a stop. The worker accepts only commands with the same stream_id, a source frame equal to the current frame minus one, and a receive time no later than the deadline. Expired actions are not applied at some later boundary.

An accelerated plaintext loop can simulate this causal order. The wall-clock test additionally uses an independent simulation process and time.monotonic() to check deadlines. A miss is determined by when the worker actually receives an action, not merely when the parent sends it.

Holding the old action is a timing rule. A physical robot still needs a separate policy for slowing, stopping, and recovering during network loss or repeated missed deadlines.

Advancing state needs a commit boundary

The encrypted Actor should not be reconstructed for every frame. A stream starts with encrypted initial state and advances through monotonically increasing frame numbers:

python
actor.reset_stream(stream_id, encrypted_h0)
result = actor.step(
    encrypted_observation,
    stream_id=stream_id,
    frame_id=0,
)
encrypted_action = result["action"]

This illustrates the call relationship and omits context and key preparation. Even an all-zero initial hidden state must be supplied as a valid encrypted h0. Subsequent frames reuse the Actor instead of resetting it.

An update frame first computes candidate hidden state and memory projection locally. Only after the full action head succeeds does it commit hidden, memory_head, and next_frame together. An output-layer exception leaves persistent state and the frame number unchanged, allowing that frame to be retried.

GPU work already performed is not rolled back. After step() succeeds, a client decoding or message-delivery failure does not automatically restore old state or obtain a cached response to a duplicate request. Internal commitment, result delivery, and action execution are distinct events.

What the 40 episodes establish

The candidate archive records 40 genuine FHE episodes, 1,362 frames, and 690 refreshes. Refreshes are counted as ceil(T/K) per episode and then summed, rather than by dividing the total frame count by two.

Local processing timeMean
All frames129.55 ms
Update frames187.22 ms
Intervening frames70.34 ms

Local processing time adds observation encryption, the complete encrypted Actor call, and action decryption/extraction. It excludes initialization, warmup, and reference checks, and is not cross-machine network latency. This set of 2 Hz episodes had no missed action deadlines. A roughly 130 ms mean still does not qualify a higher control rate: changing the rate changes policy timing and environmental feedback too.

Completing 40 test episodes also does not mean all 40 reached the goal. Navigation quality needs arrival, collision, timeout, clearance, and path-cost evidence. Ciphertext runtime or numerical agreement cannot supply a success rate.

Keep validation in separate layers

ComparisonQuestion
PyTorch, handwritten forward, NumPyDo gate order, biases, and recurrent state agree?
Exact activations and plaintext polynomialsHow does approximation change actions, state, and closed-loop behavior?
FHE and the same-weight, same-coefficient referenceAre arithmetic, packing, and level planning correct?
Frame ordering, reset, and failure retriesDo persistent state and caches advance correctly?
Full episodes and the actual clockAre navigation quality, action deadlines, and physics timing acceptable?

Warmup checks action and hidden state using a nonzero private h0. The normal closed loop decrypts only actions and compares against a polynomial reference on that episode's own observation sequence. The reference does not take over control, and policies are not required to remain frame-for-frame identical after their trajectories diverge.

The next work is at the system boundary

This is a continuous experimental system with specific dimensions, packing, and timing rules. Changing weights, polynomial degrees, hidden dimensions, or K requires renewed numerical, level, trajectory, and wall-clock checks. Replacing a file does not establish equivalence.

The final lattice-security estimate is not complete, so these parameters should not be presented as a fully audited 128-bit deployment configuration. A cross-machine ciphertext protocol, multi-session scheduling, ROS integration, and state synchronization after network failure also remain future work.

What I want to retain from this experiment is the whole chain: how the model accumulates memory, how the circuit leaves encrypted state usable for further computation, and when the execution side adopts an action. Aligning those three is a useful start toward a privacy-computing application that can keep running.