Knowledge Distillation: Compressing Trading Models for Low-Latency Deployment
La tensión entre precisión y latencia en el comercio impulsado por ML ya tiene una respuesta publicada en este blog. Modelado extendido con aprendizaje automático recomienda una división en dos etapas: un modelo rápido de aumento de gradiente realiza las cotizaciones en tiempo real críticas para la latencia, mientras que un modelo profundo se ejecuta de forma asincrónica y le envía una señal secundaria o ajusta sus parámetros. Dos modelos, dos relojes, un sistema.
La destilación del conocimiento es una respuesta diferente a la misma tensión. En lugar de ejecutar el modelo lento junto con el rápido, se usa una vez, fuera de línea, para entrenar el rápido: el estudiante aprende la distribución de probabilidad completa del maestro sobre los resultados, no solo las etiquetas estrictas, y luego el maestro abandona el camino caliente por completo. Un modelo en el momento de la inferencia, sin acoplamiento asincrónico, sin ventana de estancamiento.
Cuál respuesta gana es empírica y este artículo aún no la responde. Lo que sigue es la maquinaria, más una exposición explícita de las medidas que lo decidirían. Nada aquí es un resultado de referencia; donde normalmente iría un número, hay un marcador que dice lo que se debe ejecutar.
Una corrección de encuadre desde el principio, de DeepLOB y aprendizaje profundo en el libro de pedidos: una alta precisión de clasificación no se traduce automáticamente en ganancias: el movimiento previsto debe eliminar el diferencial entre oferta y demanda. Por lo tanto, "preservar la precisión direccional del profesor" no es lo correcto para optimizar una configuración de destilación.
El marco profesor-alumno

La formulación original de Hinton, Vinyals y Dean (2015) es sencilla. Tienes un modelo de profesor. (grande, lento, preciso) y un modelo de estudiante (pequeño, rápido, para entrenar). El alumno aprende de dos señales simultáneamente:
- Objetivos difíciles: las etiquetas de verdad sobre el terreno (por ejemplo, el precio subió o bajó)
- Objetivos blandos: distribución de probabilidad de producción del profesor sobre todas las clases
La función de pérdida de estudiante combina ambos:
dónde y son los logits de profesor y alumno, es la función softmax, es el parámetro de temperatura, y controla el equilibrio entre los dos componentes de la pérdida.
Por qué los objetivos blandos son importantes para el trading
La formulación de tres clases de precio medio arriba/estacionario/baja, el el umbral y por qué el desequilibrio resultante significa que informa F1 ponderado en lugar de precisión están configurados en DeepLOB; asuma ese esquema de etiquetas aquí. El punto específico de la destilación es lo que el profesor emite antes del argmax: un "arriba" fuerte lleva un bit, mientras que 0.72/0.21/0.07 también dice que el movimiento puede estancarse y casi con seguridad no se revertirá. Esa estructura entre clases es la señal de entrenamiento adicional, y es la razón por la cual un estudiante con objetivos suaves puede generalizar mejor que el mismo estudiante entrenado únicamente con etiquetas.
Una advertencia sobre lo que no es esa confianza. La salida de Softmax no es una incertidumbre calibrada, y tratar 0,55 frente a 0,85 como una entrada de tamaño de posición es el atajo que predicción conforme para el comercio existe para rechazar: deriva el tamaño del ancho del intervalo, una relación de borde y un filtro de no comercio cuando el intervalo se extiende a cero, nada de lo cual un softmax sin procesar le proporciona. Obtener el reclamo de tamaño aquí significa comparar la calibración del estudiante con la del maestro (diagrama de confiabilidad, ECE) y demostrar que la destilación la preserva. Ese resultado aún no está en este artículo.
Temperatura y objetivos blandos

El parámetro de temperatura. controla la "suavidad" de la distribución de probabilidad. logits dados , el softmax con temperatura es:
Cuando (softmax estándar), la distribución es máxima: la clase dominante obtiene la mayor parte de la masa de probabilidad. Como aumenta, la distribución se aplana, revelando más claramente las magnitudes relativas de los logits.
| Temperatura | Efecto | Caso de uso |
|---|---|---|
| Softmax estándar, pico | Inferencia normal | |
| Ablandamiento moderado | Destilación general | |
| Ablandamiento intenso | Cuando el maestro tiene mucha confianza | |
| Casi uniforme | Rara vez útil, elimina la señal |
Existe un argumento plausible de que los modelos comerciales quieren una temperatura moderada: las predicciones financieras son mucho menos fiables que la clasificación de imágenes, por lo que un profesor puede generar 0,55/0,30/0,15 en lugar de 0,99/0,005/0,005, dejando menos picos para suavizar antes de que la señal desaparezca. Se trata de un argumento, no de una conclusión: el rango tiene que proceder de un análisis de datos reales, puntuados mediante F1 ponderado, y puede diferir según el régimen.
El El factor en el término de divergencia KL compensa las magnitudes de gradiente reducidas a temperaturas más altas. Sin él, la pérdida por destilación sería insignificante ya que aumenta.
Elección de la temperatura mediante la búsqueda en cuadrícula
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from sklearn.metrics import f1_score
def distillation_loss(
student_logits: torch.Tensor,
teacher_logits: torch.Tensor,
labels: torch.Tensor,
temperature: float,
alpha: float,
) -> torch.Tensor:
"""Combined hard-target + soft-target distillation loss."""
hard_loss = F.cross_entropy(student_logits, labels)
soft_teacher = F.log_softmax(teacher_logits / temperature, dim=-1)
soft_student = F.log_softmax(student_logits / temperature, dim=-1)
soft_loss = F.kl_div(
soft_student,
soft_teacher,
log_target=True,
reduction="batchmean",
)
return alpha * hard_loss + (1.0 - alpha) * (temperature ** 2) * soft_loss
def search_temperature(
teacher: nn.Module,
student_factory, # callable returning a fresh student
train_loader: DataLoader,
val_loader: DataLoader,
temperatures: list[float] = [1, 2, 3, 5, 8, 12],
alpha: float = 0.3,
epochs: int = 30,
lr: float = 1e-3,
device: str = "cuda",
):
"""Grid search over temperature, scored by weighted F1 (not accuracy:
the up/flat/down label scheme is heavily imbalanced toward flat)."""
best_f1, best_T, best_student = 0.0, 1.0, None
for T in temperatures:
student = student_factory().to(device)
optimizer = torch.optim.AdamW(student.parameters(), lr=lr)
for epoch in range(epochs):
student.train()
for X, y in train_loader:
X, y = X.to(device), y.to(device)
with torch.no_grad():
teacher_logits = teacher(X)
student_logits = student(X)
loss = distillation_loss(
student_logits, teacher_logits, y, T, alpha
)
optimizer.zero_grad()
loss.backward()
optimizer.step()
student.eval()
preds, targets = [], []
with torch.no_grad():
for X, y in val_loader:
preds.append(student(X.to(device)).argmax(dim=-1).cpu())
targets.append(y)
f1 = f1_score(
torch.cat(targets), torch.cat(preds), average="weighted"
)
print(f"T={T:>4.1f} val_weighted_f1={f1:.4f}")
if f1 > best_f1:
best_f1, best_T, best_student = f1, T, student
print(f"\nBest temperature: T={best_T}, val_weighted_f1={best_f1:.4f}")
return best_T, best_student
Destilando conjuntos en un solo modelo

Un conjunto cuantitativo mezcla sesgos inductivos: un árbol potenciado por gradiente en las características del libro de pedidos, un 1D-CNN en ticks recientes, un transformador en ventanas de múltiples marcos temporales, un modelo lineal en factores macro. El promedio es más estable que cualquier miembro por sí solo, y ejecutar los cuatro multiplica la latencia y el costo: la situación que la división en dos etapas de modelado extendido con aprendizaje automático maneja degradando miembros lentos a un canal lateral asincrónico. En cambio, la destilación colapsa a los cuatro en un solo estudiante en el camino caliente.
La producción del profesor del conjunto es el promedio de la producción softmax de sus miembros:
dónde es el número de miembros del conjunto. El estudiante es entrenado contra esta distribución promediada.
class EnsembleTeacher(nn.Module):
"""Wraps K models, returns averaged logits for distillation."""
def __init__(self, models: list[nn.Module]):
super().__init__()
self.models = nn.ModuleList(models)
@torch.no_grad()
def forward(self, x: torch.Tensor) -> torch.Tensor:
logits = torch.stack([m(x) for m in self.models], dim=0)
return logits.mean(dim=0) # average logits, not softmax
class TradingStudent(nn.Module):
"""Lightweight MLP for sub-millisecond inference."""
def __init__(self, input_dim: int, hidden: int = 64, n_classes: int = 3):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, hidden),
nn.ReLU(),
nn.BatchNorm1d(hidden),
nn.Linear(hidden, hidden),
nn.ReLU(),
nn.BatchNorm1d(hidden),
nn.Linear(hidden, n_classes),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x)
La asimetría del recuento de parámetros es el punto central: un MLP de dos capas con 64 unidades ocultas es del orden de 8.000 parámetros para una tarea de 60 funciones y 3 clases, frente a un conjunto cuyo recuento combinado asciende a millones.
Lo que el estudiante retiene y lo que pierde
Ésta es la pregunta empírica más importante y el artículo no la responde. La intuición es que el estudiante sigue la distribución del conjunto y cae en regímenes estresados, donde la diversidad del conjunto hace el trabajo, pero una cifra de retención sólo significa algo medido con datos reales del libro de pedidos, dividido por régimen y reportado como F1 ponderado. Un estudiante que aguanta en días tranquilos y colapsa durante una cascada de liquidaciones es un producto diferente de uno que se degrada con gracia, y un número agregado no puede distinguirlos.
Vale la pena probar tres mitigaciones con respecto a esa medición en lugar de afirmarlas de antemano:
- Incluir períodos estresados en el conjunto de destilación, para que el estudiante vea los regímenes en los que se espera que se abra la brecha.
- Destilación basada en características: combine representaciones intermedias, no solo resultados finales.
- Cabeza de régimen auxiliar sobre el estudiante, forzando que las características conscientes del régimen entren en el tronco compartido.
Autodestilación: cuando el alumno se convierte en maestro

La autodestilación es una técnica en la que un modelo destila conocimiento de sí mismo.
Redes nacidas de nuevo (BAN)
Formar a un alumno con una arquitectura idéntica a la del profesor. El estudiante "nacido de nuevo" a menudo supera al original y el proceso se repite:
Cada generación se entrena con objetivos fáciles de la anterior, y las ganancias normalmente se saturan después de unas pocas generaciones. Para los modelos comerciales, esto no cuesta nada desde el punto de vista arquitectónico (ni características nuevas, ni datos nuevos, solo un procedimiento de entrenamiento diferente), lo que también significa que es barato probarlo y no hay excusa para informar que no se ha probado.
Autodestilación en profundidad
Adjunte clasificadores auxiliares en las capas intermedias. La salida más profunda sirve de maestra a las más superficiales. En la inferencia, eliges una salida: superficial para una latencia más baja, profunda para una máxima precisión.
Esta es la idea que mejor se adapta a un sistema comercial, porque la profundidad de salida se convierte en un control de latencia en tiempo de ejecución: una red entrenada cubre una variedad de presupuestos en lugar de comprometerse con una única arquitectura en el momento del entrenamiento. Cuando el libro se mueve rápido, tomas la salida poco profunda y aceptas un trasero peor; cuando hay silencio, pagas por la profundidad total. Tanto la curva de precisión por salida como la de latencia por salida son mensurables, y su cruce decide si vale la pena tener la perilla.
class SelfDistillingNet(nn.Module):
"""Network with early-exit classifiers for variable-latency inference."""
def __init__(self, input_dim: int, n_classes: int = 3):
super().__init__()
self.block1 = nn.Sequential(
nn.Linear(input_dim, 128), nn.ReLU(), nn.BatchNorm1d(128)
)
self.block2 = nn.Sequential(
nn.Linear(128, 64), nn.ReLU(), nn.BatchNorm1d(64)
)
self.block3 = nn.Sequential(
nn.Linear(64, 32), nn.ReLU(), nn.BatchNorm1d(32)
)
self.exit1 = nn.Linear(128, n_classes)
self.exit2 = nn.Linear(64, n_classes)
self.exit3 = nn.Linear(32, n_classes) # final exit
def forward(
self, x: torch.Tensor, exit_layer: int = 3
) -> torch.Tensor:
h1 = self.block1(x)
if exit_layer == 1:
return self.exit1(h1)
h2 = self.block2(h1)
if exit_layer == 2:
return self.exit2(h2)
h3 = self.block3(h2)
return self.exit3(h3)
def forward_all_exits(self, x: torch.Tensor):
"""Return logits from all exits (for self-distillation training)."""
h1 = self.block1(x)
h2 = self.block2(h1)
h3 = self.block3(h2)
return self.exit1(h1), self.exit2(h2), self.exit3(h3)
def self_distillation_step(
model: SelfDistillingNet,
x: torch.Tensor,
y: torch.Tensor,
temperature: float = 4.0,
alpha: float = 0.5,
) -> torch.Tensor:
"""One training step with self-distillation from deepest exit."""
logits_1, logits_2, logits_3 = model.forward_all_exits(x)
loss_hard = F.cross_entropy(logits_3, y)
loss_distill_1 = distillation_loss(
logits_1, logits_3.detach(), y, temperature, alpha
)
loss_distill_2 = distillation_loss(
logits_2, logits_3.detach(), y, temperature, alpha
)
return loss_hard + 0.5 * loss_distill_1 + 0.5 * loss_distill_2
De dónde proviene el presupuesto de inferencia

La destilación solo importa si la inferencia tiene que estar dentro de un presupuesto estricto, y la escala completa de tick-to-trade (NIC a espacio de usuario, bypass del kernel, el total de menos de 100 µs y el nivel de menos de 10 µs que fuerza FPGA y memoria compartida) ya está establecido en datos y comunicación en el comercio algorítmico. La fila que la escalera deja abierta es la inferencia del modelo, y esa es la fila que la destilación está tratando de llenar.
Resista llenar las otras filas con una tabla de latencia de clase de modelo. Modelado extendido con aprendizaje automático ya publica la comparación GBM-vs-deep-learning más la advertencia que importa más que los números: la latencia depende de la implementación, y el mismo modelo LightGBM toma decenas de microsegundos por fila de Python pero unos pocos microsegundos de un predictor compilado. Cualquier reclamo de latencia aquí debe nombrar el marco, el núcleo y el tamaño del lote, o será ruido.
Específicamente en las GPU: la sobrecarga fija por lanzamiento debe amortizarse antes de que un dispositivo ayude, y la inferencia de una sola fila se encuentra muy a la izquierda de la cresta de la línea del techo, donde nunca está. Cuando la GPU da sus frutos mide esa curva de amortización correctamente con un barrido por lotes, incluido cómo una tarjeta PCIe discreta empuja la cresta más hacia la derecha; léalo en lugar de confiar en una constante citada de la memoria.
Cuantización después de la destilación
Un estudiante destilado comprime aún más: pesos INT8 (aproximadamente 2x en CPU con AVX-512 VNNI), pesos binarios/ternarios que convierten multiplicaciones en sumas y poda para omitir cálculos cercanos a cero.
La afirmación tentadora es que la destilación y luego la cuantificación preservan más precisión que la cuantificación sola, dado que el estudiante ya aprendió una representación compacta. No realice envíos en él. La trampa de precisión de GPU es la posición firme del blog sobre la precisión numérica reducida: devolvió silenciosamente basura de apariencia plausible, y lo que hizo que la ruta rápida se pudiera enviar fue una puerta de equivalencia cuantificada (rellenos desplazados, delta de PnL en bps), no una afirmación. Un estudiante INT8 es un modelo diferente hasta que esa puerta se compara con el estudiante FP32.
import torch.quantization as quant
def quantize_student(student: nn.Module, calibration_loader: DataLoader):
"""Post-training static quantization for CPU deployment."""
student.cpu()
student.eval()
student.qconfig = quant.get_default_qconfig("x86")
student_prepared = quant.prepare(student)
with torch.no_grad():
for X, _ in calibration_loader:
student_prepared(X)
student_quantized = quant.convert(student_prepared)
return student_quantized
Implementación de FPGA: el canal de destilación a Bitstream

Los FPGA son el nivel inferior a 10 µs en la escalera de latencia, y la revisión de Tbricks/Broadridge los cubre en producción junto con las NIC de derivación del kernel: latencia determinista, sin fluctuaciones del sistema operativo, ubicadas junto con la pila de red. Lo que no se trata en ninguna parte de este blog es cómo un modelo destilado llega a uno.
Notas de producción de DeepLOB enumeran ONNX/TensorRT, la cuantificación de INT8 y la implementación de FPGA como tres opciones y deténgase allí. Esto es en lo que se expande el tercero:
1. Train ensemble teacher (offline, GPU cluster, hours/days)
|
2. Distill to small MLP student (offline, single GPU, minutes)
|
3. Quantize student to INT8 / fixed-point (offline, CPU)
|
4. Convert to HLS (High-Level Synthesis) or RTL
|
5. Synthesize FPGA bitstream (offline, hours)
|
6. Deploy to FPGA card in production server
|
7. Inference: market data -> FPGA -> trading signal
La restricción vinculante es que el modelo debe caber en los elementos lógicos del dispositivo: LUT, segmentos DSP, bloque de RAM. Como un presupuesto de orden de magnitud en lugar de una medición: un MLP de 2 capas con 64 unidades ocultas y pesos INT8 es del orden de 8000 acumulaciones múltiples por inferencia y ~16 KB de pesos, una pequeña fracción de una parte de rango medio. Aquí es donde la destilación se gana la vida: el profesor de conjunto no cabe en ningún presupuesto; el estudiante no está ni cerca del límite.
Las herramientas que automatizan PyTorch/ONNX en hardware sintetizable incluyen AMD/Xilinx Vitis AI, hls4ml (del CERN) y FINN (de Xilinx Research).
Ejemplo: Conversión hls4ml
import hls4ml
import onnx
dummy_input = torch.randn(1, 60) # 60 input features
torch.onnx.export(student, dummy_input, "student.onnx", opset_version=13)
hls_config = hls4ml.utils.config_from_onnx_model(
onnx.load("student.onnx"),
granularity="name",
default_precision="ap_fixed<16,8>",
default_reuse_factor=1, # full parallelism
)
hls_model = hls4ml.converters.convert_from_onnx_model(
"student.onnx",
hls_config=hls_config,
output_dir="hls_student",
backend="VivadoAccelerator",
board="alveo-u250",
)
hls_model.compile()
hls_model.build(csim=True, synth=True)
hls_model.report()
hls_model.report() es la única fuente creíble de cifras de recursos y latencia para un modelo, placa, precisión y factor de reutilización determinados: las cifras cambian sustancialmente con default_reuse_factor solo. Citar una tabla de síntesis "típica" sin ejecutarla es adivinar.
Consideraciones prácticas

Logits de profesores previos al cálculo
La destilación necesita predicciones de los profesores sobre todo el conjunto de capacitación: un costo único fuera de línea que vale la pena pagar deliberadamente: ejecutar el conjunto una vez, persistir los logits, capacitar a los estudiantes contra el caché. Los barridos de temperatura y las búsquedas de arquitectura no cuestan nada adicional en los pases de avance del maestro, que es lo que hace que los barridos anteriores sean prácticos.
El único monitor específico para la destilación
La higiene de la canalización de funciones, la normalización continua debido a la deriva de los parámetros de puntuación z, el monitoreo del cambio de distribución de entrada y el reentrenamiento activado por el régimen se tratan en la sección de producción de DeepLOB y se aplican sin cambios aquí.
El monitor específico de la destilación es divergencia de KL entre profesor y estudiante en datos en vivo. El profesor todavía existe fuera de línea; ejecútelo en una muestra de entradas en vivo y compare distribuciones. El aumento de KL significa que la aproximación del estudiante se está degradando en regímenes en los que no fue destilada, y se activa antes que la precisión, porque no espera etiquetas. El umbral de reentrenamiento debe calibrarse en función del KL observado en períodos conocidos y degradados; elegido a priori es arbitrario.
Cuándo no destilar
- El maestro ya es pequeño (un modelo lineal, un GBM poco profundo): la destilación agrega una etapa de tubería sin compresión.
- La latencia no es una limitación (reequilibrio diario, señales de fin de día): despliegue al profesor.
- La interpretabilidad supera la velocidad: una red destilada es más difícil de explicar que el conjunto de árboles que reemplazó.
- La división en dos etapas ya funciona: si el modelo lento asíncrono en la arquitectura de modelado extendido está funcionando, la destilación tiene que superarlo en una comparación mesurada antes de que justifique el reemplazo de un sistema en funcionamiento.
Resumen

La destilación es una alternativa coherente a la división rápida/lenta en dos etapas: capacite al mejor profesor que pueda permitirse sin conexión, transfiera su estructura de objetivo suave a un estudiante lo suficientemente pequeño para el camino activo, cuantice, implemente en CPU o FPGA. La variante en profundidad va más allá y hace que la latencia sea una elección de tiempo de ejecución en lugar de tiempo de entrenamiento.
Lo que este artículo deliberadamente no afirma es que algo supere lo que el blog ya publica. Ese veredicto necesita tres mediciones sobre datos reales del libro de pedidos: la curva de retención F1 ponderada de estudiante versus conjunto dividida por régimen, el barrido de temperatura y una puerta de paridad INT8 al estilo de [la trampa de precisión de GPU] (/en/blog/post/gpu-precision-trap-fp32-backtest). Hasta que existan, esta es una descripción de una técnica, no una recomendación para implementarla.
Referencias
-
Hinton, G., Vinyals, O. y Dean, J. (2015). Destilando el conocimiento en una red neuronal. arXiv:1503.02531
-
Furlanello, T., Lipton, ZC, Tschannen, M., Itti, L. y Anandkumar, A. (2018). Redes neuronales nacidas de nuevo. ICML. arXiv:1805.04770
-
Zhang, L., Song, J., Gao, A., Chen, J., Bao, C. y Ma, K. (2019). Sea su propio maestro: mejore el rendimiento de las redes neuronales convolucionales mediante la autodestilación. ICCV. arXiv:1905.08094
-
Romero, A., Ballas, N., Kahou, S. E., Chassang, A., Gatta, C. y Bengio, Y. (2015). FitNets: Consejos para redes delgadas y profundas. ICLR. arXiv:1412.6550
-
Gou, J., Yu, B., Maybank, SJ y Tao, D. (2021). Destilación del conocimiento: una encuesta. Revista Internacional de Visión por Computadora, 129, 1789-1819. arXiv:2006.05525
-
Duarte, J., et al. (2018). Inferencia rápida de redes neuronales profundas en FPGA para física de partículas (hls4ml). Revista de Instrumentación, 13, P07027. arXiv:1804.06913
-
Umuroglu, Y., et al. (2017). FINN: Un marco para la inferencia de redes neuronales binarizadas rápida y escalable. FPGA '17. arXiv:1612.07119
-
Zhang, Z., Zohren, S. y Roberts, S. (2019). DeepLOB: Redes neuronales convolucionales profundas para libros de órdenes limitadas. Transacciones IEEE sobre procesamiento de señales, 67(11), 3001-3012. arXiv:1808.03668
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.