Cifrado Homomórfico en la Práctica: TFHE, Concrete-ML e Inferencia ML sobre Datos Cifrados
Principios del Cifrado Homomórfico
La encriptación regular es como una caja fuerte cerrada. Puede guardar cosas y sacarlas, pero no es posible modificar el contenido sin abrirla primero. La encriptación homomórfica es como una caja fuerte con brazos manipuladores incorporados — es posible hacer computaciones sobre el contenido mientras sigue encerrado adentro.
Formalmente: dadas las encriptaciones E(a) y E(b), es posible computar E(a + b) y E(a × b) sin conocer a ni b. Eso es todo. Toda computación se puede descomponer en sumas y multiplicaciones, así que es Turing-completo. El catch es la performance.
TFHE: El Esquema Detrás de Concrete-ML
TFHE representa datos encriptados como polinomios sobre un toro. La innovación clave es el bootstrapping rápido — TFHE puede refrescar el ruido del ciphertext en ~10ms, comparado con minutos para esquemas más viejos como BGV/BFV.
El ruido crece con cada operación:
- Suma: El ruido crece linealmente (barato)
- Multiplicación: El ruido crece exponencialmente (caro)
- Bootstrapping: Resetea el ruido a un nivel fijo (~10ms pero habilita más computación)
Concrete-ML: FHE para Machine Learning
from concrete.ml.sklearn import LogisticRegression
import numpy as np
# Entrenar sobre datos plaintext (este paso es ML normal)
X_train = np.load("features_train.npy")
y_train = np.load("labels_train.npy")
model = LogisticRegression(n_bits=8) # Cuantizar a integers de 8 bits
model.fit(X_train, y_train)
# Evaluar en plaintext primero (sanity check)
plaintext_accuracy = model.score(X_test, y_test)
print(f"Accuracy plaintext: {plaintext_accuracy:.4f}")
# Compilar el modelo en un circuito FHE
fhe_circuit = model.compile(X_train)
# Ahora podemos hacer inferencia sobre datos encriptados
encrypted_input = fhe_circuit.encrypt(X_test[0:1])
encrypted_result = fhe_circuit.run(encrypted_input)
decrypted_result = fhe_circuit.decrypt(encrypted_result)
print(f"Predicción FHE: {decrypted_result}")
El parámetro n_bits=8 es crítico. Controla la cuantización — cuántos bits representan cada valor en el modelo:
| n_bits | Accuracy Plaintext | Tiempo Inferencia FHE | Speedup vs. 16-bit | |--------|--------------------|-----------------------|---------------------| | 4 | 0.8912 | 45ms | 18x | | 6 | 0.9156 | 95ms | 8.5x | | 8 | 0.9234 | 180ms | 4.5x | | 12 | 0.9241 | 520ms | 1.6x | | 16 | 0.9243 | 810ms | 1x |
De 16-bit a 8-bit, perdemos 0.09% de accuracy pero ganamos 4.5x de velocidad.
El Pipeline de Scoring Crediticio
[App Cliente] → [Encriptar features localmente] → [Mandar ciphertext a API]
↓
[Servidor Inferencia FHE (AWS)]
↓
[Resultado encriptado]
↓
[App Cliente] ← [Desencriptar resultado localmente] ← [Retornar predicción encriptada]
Los datos financieros del cliente nunca salen de su dispositivo en plaintext. Los pese es del modelo del banco están embebidos en el circuito FHE compilado, pero el cliente no puede extraerlos.
# Lado servidor
from concrete.ml.deployment import FHEModelServer
server = FHEModelServer(path_dir="./deployed_model")
@app.post("/predict")
async def predict(request: Request):
encrypted_input = await request.body()
encrypted_result = server.run(
serialized_encrypted_quantized_data=encrypted_input,
serialized_evaluation_keys=get_evaluation_keys(request.headers["client-id"]),
)
return Response(content=encrypted_result, media_type="application/octet-stream")
# Lado cliente
from concrete.ml.deployment import FHEModelClient
client = FHEModelClient(path_dir="./client_model", key_dir="./keys")
client.generate_private_and_evaluation_keys()
clear_input = np.array([[65000, 0.32, 7, 3, 720, 12000, 0, 1]])
encrypted_input = client.quantize_encrypt_serialize(clear_input)
evaluation_keys = client.get_serialized_evaluation_keys()
# ... mandar encrypted_input y evaluation_keys al servidor ...
encrypted_result = response.content
decrypted_prediction = client.deserialize_decrypt_dequantize(encrypted_result)
print(f"Categoría de credit score: {decrypted_prediction}")
Redes Neuronales Custom con Concrete-ML
import torch
from concrete.ml.torch.compile import compile_torch_model
class CreditNet(torch.nn.Module):
def __init__(self):
super().__init__()
self.fc1 = torch.nn.Linear(20, 64)
self.fc2 = torch.nn.Linear(64, 32)
self.fc3 = torch.nn.Linear(32, 2)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x
model = CreditNet()
# ... loop de entrenamiento ...
quantized_model = compile_torch_model(
model,
X_train,
n_bits=6,
rounding_threshold_bits=6,
)
La arquitectura del modelo importa enormemente para la performance FHE. Reglas derivadas de la experiencia:
- ReLU es caro — cada ReLU requiere una comparación, que en FHE significa una operación de programmable bootstrapping (~10ms cada una). Una red con 96 activaciones ReLU tiene ~960ms de overhead de bootstrapping.
- Profundidad sobre ancho — una red de 3 capas con 64 neuronas por capa es mucho más lenta que una de 2 capas con 128 neuronas, porque la profundidad agrega bootstrapping secuencial.
- Evitar max/min/argmax — requieren múltiples comparaciones. Utilizar average pooling en vez de max pooling.
- Batch normalization es gratis — se fusiona en la capa lineal precedente durante compilación.
La Realidad de Performance
Resultados en una AWS c6i.2xlarge (8 vCPUs, sin aceleración GPU):
| Modelo | Accuracy | Inferencia FHE | Plaintext | Slowdown | |--------|----------|----------------|-----------|----------| | Regresión Logística | 0.923 | 85ms | 0.02ms | 4,250x | | Árbol de Decisión (depth=5) | 0.941 | 210ms | 0.01ms | 21,000x | | XGBoost (10 árboles, depth=4) | 0.956 | 1,240ms | 0.08ms | 15,500x | | SVR Lineal | 0.917 | 72ms | 0.01ms | 7,200x | | NN Custom (2 capas, 64 units) | 0.948 | 3,400ms | 0.15ms | 22,667x |
Ese slowdown de 4,250x para regresión logística se traduce a 85ms — completamente viable para llamadas API real-time. El XGBoost en 1.2 segundos está bien para batch processing.
Lo Que NO Es Posible (Todavía)
Dejame ahorrarte semanas de esfuerzo tirado:
- Modelos de lenguaje grandes: Un modelo tamaño GPT-2 tomaría horas por token.
- Clasificación de imágenes con CNNs profundas: Inferencia de ResNet-50 serían ~45 minutos.
- Cualquier cosa con control flow dinámico: Los circuitos FHE son estáticos.
- Feature spaces grandes: 1000+ features significa 1000+ valores encriptados para procesar.
Tamaños de Claves y Transferencia de Red
FHE tiene un problema de expansión de datos. Un vector de 20 features que son 160 bytes en plaintext se convierte en:
Input encriptado: ~42 KB
Claves de evaluación: ~158 MB (se mandan una vez por cliente, cacheadas por el server)
Output encriptado: ~4 KB
Ese es 158 MB de clave de evaluación son el elefante en la habitación. Con compresión de claves se reduce a ~40 MB — sigue siendo grande, pero manejable.
La Arquitectura que Funciona
"""
1. Cliente genera claves una vez, almacena localmente
2. Claves de evaluación subidas al server una vez (cacheadas con TTL)
3. Cada request de inferencia manda solo el input encriptado (~42 KB)
4. Server retorna resultado encriptado (~4 KB)
5. Cliente desencripta localmente
Transferencia de datos total por request: ~46 KB
Budget de latencia:
- Red (input encriptado): 15ms
- Inferencia FHE (server): 340ms
- Red (resultado encriptado): 5ms
- Desencriptación cliente: 2ms
Total: ~362ms
"""
El tiempo de inferencia de 340ms es para el modelo XGBoost que el banco eligió — 15 árboles de profundidad 4, cuantizado a 6 bits, procesando 20 features financieros. La accuracy es 95.2% en su holdout set, comparado con 96.1% para el modelo plaintext. El banco aceptó ese tradeoff de 0.9% de accuracy porque FHE les permitió procesar datos de clientes de una institución partner sin acuerdos de compartición de datos, que hubieran tomado 8 meses de revisión legal.
Hacia Dónde Va Esto
La aceleración por hardware de TFHE viene en camino. Zama está construyendo ASICs custom que prometen 100-1000x de speedup sobre FHE en CPU. La librería HEXL de Intel ya provee 2-4x de speedup usando instrucciones AVX-512. Aceleración GPU a través de implementaciones TFHE basadas en CUDA muestra mejoras de 10-50x.
Cuando el hardware alcance — y va a alcanzar, porque la demanda de mercado es clara — una inferencia de 340ms va a ser 3ms. En ese punto, FHE se vuelve viable para aplicaciones real-time en general: búsqueda encriptada, sistemas de recomendación privados, diagnóstico médico confidencial.
Por ahora, el punto óptimo está claro: modelos pequeños a medianos (regresión logística, gradient boosting, redes shallow) sobre datos estructurados con menos de 100 features. Si tu caso de uso encaja en ese perfil y se necesita garantías de privacidad matemáticas más fuertes que "te prometemos que no miramos tus datos," la encriptación homomórfica está lista.