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.
Laser / goal / currently held action
-> client preprocessing and encryption
-> encrypted server policy + recurrent ciphertext state
-> client action decryption
-> execution at the next control boundaryPreprocessing 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:
| Input | Dimensions | Meaning |
|---|---|---|
| Laser sectors | 36 | Clip 180 rays to 0–7 meters, take the minimum of each group of five, then divide by 7 |
| Relative goal distance | 1 | min(distance / 10, 1) |
| Relative goal direction | 2 | cos(angle) and sin(angle) |
| Held action | 2 | The 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:
a0, a1 = clip(action, -1, 1)
linear_velocity = 0.25 * (a0 + 1) m/s
angular_velocity = a1 rad/sThe 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:
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:
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:
P(s) = Σ c_j T_j(s / R)| Location | Degree | Cost in this packed evaluator |
|---|---|---|
| Encoder, r/z gates, candidate, output | 31 | 5 limbs |
| Head | 15 | 4 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.
| Stage | Output limbs |
|---|---|
| Observation input | 21 |
| After input affine and encoder polynomial | 15 |
| After the full GRU update | 2 |
| After hidden-state bootstrapping and alignment | 15 |
| Memory projection / fast-path projection | 14 |
| After the head polynomial | 10 |
| After final affine and output polynomial | 4 |
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:
| Operation | Update frame | Intervening frame |
|---|---|---|
| MatVec | 8 | 3 |
| Ciphertext × ciphertext | 88 | 48 |
| Ciphertexts bootstrapped | 1 | 0 |
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.
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 missThe 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:
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 time | Mean |
|---|---|
| All frames | 129.55 ms |
| Update frames | 187.22 ms |
| Intervening frames | 70.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
| Comparison | Question |
|---|---|
| PyTorch, handwritten forward, NumPy | Do gate order, biases, and recurrent state agree? |
| Exact activations and plaintext polynomials | How does approximation change actions, state, and closed-loop behavior? |
| FHE and the same-weight, same-coefficient reference | Are arithmetic, packing, and level planning correct? |
| Frame ordering, reset, and failure retries | Do persistent state and caches advance correctly? |
| Full episodes and the actual clock | Are 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.