Knowledge Distillation: Compressing Trading Models for Low-Latency Deployment
La tensione tra accuratezza e latenza nel trading basato sul machine learning ha già una risposta pubblicata su questo blog. Spread modeling con machine learning consiglia una divisione in due fasi: un modello di potenziamento del gradiente rapido esegue la quotazione in tempo reale critica per la latenza, mentre un modello profondo viene eseguito in modo asincrono e gli fornisce un segnale secondario o ne regola i parametri. Due modelli, due orologi, un sistema.
La distillazione della conoscenza è una risposta diversa alla stessa tensione. Invece di eseguire il modello lento insieme a quello veloce, lo usi una volta, offline, per addestrare quello veloce: lo studente apprende l'intera distribuzione di probabilità dell'insegnante sui risultati, non solo le etichette rigide, e l'insegnante poi abbandona completamente il percorso caldo. Un modello al momento dell'inferenza, nessun accoppiamento asincrono, nessuna finestra di stallo.
Quale risposta vince è empirica e questo articolo non fornisce ancora una risposta. Quello che segue è il macchinario, più una dichiarazione esplicita delle misurazioni che lo deciderebbero. Niente qui è un risultato di riferimento; dove normalmente andrebbe un numero, c'è un indicatore che dice cosa deve essere eseguito.
Una correzione di framing in anticipo, da DeepLOB e deep learning sul book degli ordini: l'elevata precisione della classificazione non si traduce automaticamente in profitto: la mossa prevista deve eliminare lo spread bid-ask. "Preservare la precisione direzionale dell'insegnante" è quindi la cosa sbagliata contro cui ottimizzare una configurazione di distillazione.
Il quadro insegnante-studente

La formulazione originale di Hinton, Vinyals e Dean (2015) è semplice. Hai un modello di insegnante (grande, lento, accurato) e un modello studentesco (piccolo, veloce, da addestrare). Lo studente impara da due segnali contemporaneamente:
- Obiettivi difficili: le etichette della verità (ad esempio, il prezzo è aumentato o diminuito)
- Obiettivi soft: distribuzione della probabilità di rendimento dell'insegnante su tutte le classi
La funzione di perdita dello studente combina entrambi:
Dove E sono i registri dell'insegnante e dello studente, è la funzione softmax, è il parametro della temperatura e controlla l’equilibrio tra le due componenti di perdita.
Perché i soft target sono importanti per il trading
La formulazione del prezzo medio a tre classi su/stazionario/giù, la la soglia e il motivo per cui lo squilibrio risultante significa che riporti F1 ponderato anziché l'accuratezza sono tutti impostati in DeepLOB - presupponi questo schema di etichette qui. Il punto specifico della distillazione è ciò che l'insegnante emette prima dell'argmax: un hard "up" porta un bit, mentre 0.72/0.21/0.07 dice anche che la mossa potrebbe bloccarsi e quasi certamente non si invertirà. Questa struttura tra classi è il segnale di formazione in più, ed è il motivo per cui uno studente con target soft può generalizzare meglio dello stesso studente formato solo sulle etichette.
Un avvertimento su cosa non è quella fiducia. L'output di Softmax non è un'incertezza calibrata e trattare 0,55 contro 0,85 come input per il dimensionamento della posizione è la scorciatoia che previsione conforme per il trading esiste per rifiutare: deriva il dimensionamento dall'ampiezza dell'intervallo, un rapporto edge e un filtro no-trade quando l'intervallo è a cavallo dello zero, nessuno dei quali fornisce un softmax grezzo. Guadagnare la richiesta di dimensionamento qui significa misurare la calibrazione dello studente rispetto a quella dell'insegnante (diagramma di affidabilità, ECE) e mostrare che la distillazione la preserva. Questo risultato non è ancora presente in questo articolo.
Temperatura e obiettivi morbidi

Il parametro della temperatura controlla la "morbidezza" della distribuzione di probabilità. Dati i logit , il softmax con la temperatura è:
Quando (softmax standard), la distribuzione è elevata: la classe dominante ottiene la maggior parte della massa di probabilità. COME aumenta, la distribuzione si appiattisce, rivelando più chiaramente le grandezze relative dei logit.
| Temperatura | Effetto | Caso d'uso |
|---|---|---|
| Softmax standard, picco | Inferenza normale | |
| Ammorbidimento moderato | Distillazione generale | |
| Ammorbidimento pesante | Quando l'insegnante è molto fiducioso | |
| Quasi uniforme | Raramente utile, cancella il segnale |
Esiste un argomento plausibile secondo cui i modelli di trading vogliono una temperatura moderata: le previsioni finanziarie sono molto meno sicure della classificazione delle immagini, quindi un insegnante può produrre 0,55/0,30/0,15 anziché 0,99/0,005/0,005, lasciando che i picchi inferiori si attenuino prima che il segnale svanisca. Questo è un argomento, non una scoperta: l’intervallo deve provenire da un’analisi dei dati reali, valutati in base alla F1 ponderata, e può differire in base al regime.
IL Il fattore nel termine di divergenza KL compensa la riduzione dell'entità del gradiente a temperature più elevate. Senza di esso, la perdita di distillazione diventerebbe trascurabilmente piccola aumenta.
Scelta della temperatura tramite la ricerca nella griglia
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
Distillazione degli insiemi in un unico modello

Un insieme quantistico mescola pregiudizi induttivi: un albero potenziato dal gradiente sulle caratteristiche del portafoglio ordini, una 1D-CNN sui tick recenti, un trasformatore su finestre multi-timeframe, un modello lineare su fattori macro. La media è più stabile di qualsiasi membro preso singolarmente e l'esecuzione di tutti e quattro moltiplica la latenza e i costi: la situazione che la divisione in due fasi da spread modeling con machine learning gestisce retrocedendo i membri lenti a un canale laterale asincrono. La distillazione invece fa collassare tutti e quattro in uno studente nel percorso caldo.
Il rendimento dell'insegnante dell'insieme è la media dei risultati softmax dei suoi membri:
Dove è il numero dei membri dell'insieme. Lo studente viene addestrato rispetto a questa distribuzione media.
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)
Il punto centrale è l'asimmetria nel conteggio dei parametri: un MLP a due strati con 64 unità nascoste è dell'ordine di 8.000 parametri per un compito di 60 funzioni e 3 classi, contro un insieme il cui conteggio combinato ammonta a milioni.
Cosa conserva e cosa perde lo studente
Questa è la domanda empirica portante e l’articolo non risponde. L'intuizione è che lo studente tiene traccia dell'insieme nella distribuzione e cade in regimi stressati, dove la diversità dell'insieme fa il lavoro - ma una cifra di fidelizzazione significa solo qualcosa misurato su dati reali del registro degli ordini, suddivisi per regime e riportati come F1 ponderato. Uno studente che resiste nei giorni tranquilli e crolla durante una cascata di liquidazioni è un prodotto diverso da uno che si degrada con grazia, e un numero aggregato non può distinguerli.
Vale la pena testare tre mitigazioni rispetto a tale misurazione piuttosto che affermarle in anticipo:
- Includere periodi stressanti nel set di distillazione, in modo che lo studente veda i regimi in cui si prevede che si apra il divario.
- Distillazione basata sulle funzionalità: corrisponde alle rappresentazioni intermedie, non solo ai risultati finali.
- Testa del regime ausiliario sullo studente, forzando le funzionalità sensibili al regime nel bagagliaio condiviso.
Autodistillazione: quando lo studente diventa insegnante

L'auto-distillazione è una tecnica in cui un modello distilla la conoscenza da se stesso.
Reti Born-Again (BAN)
Formare uno studente con un'architettura identica a quella dell'insegnante. Lo studente "nato di nuovo" spesso supera l'originale e il processo si ripete:
Ogni generazione si allena su obiettivi facili rispetto alla precedente, con guadagni che generalmente si saturano dopo poche generazioni. Per i modelli di trading questo non costa nulla dal punto di vista architettonico – nessuna nuova funzionalità, nessun nuovo dato, solo una diversa procedura di formazione – il che significa anche che è economico testarlo e non ci sono scuse per segnalarlo non testato.
Autodistillazione profonda
Allega classificatori ausiliari ai livelli intermedi. L'uscita più profonda serve da insegnante per quelle meno profonde. Durante l'inferenza si sceglie un'uscita: superficiale per una latenza inferiore, profonda per la massima precisione.
Questa è l'idea che si adatta meglio a un sistema di trading, perché la profondità di uscita diventa una manopola della latenza di runtime: una rete addestrata copre una gamma di budget invece di impegnarsi in una singola architettura al momento dell'addestramento. Quando il libro si muove velocemente, prendi l'uscita superficiale e accetti un posteriore peggiore; quando è tranquillo paghi per tutta la profondità. Sia la curva di precisione per uscita che quella di latenza per uscita sono misurabili e il loro crossover decide se vale la pena avere la manopola.
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
Da dove proviene il budget dell'inferenza

La distillazione è importante solo se l'inferenza deve rientrare in un budget rigido e l'intera scala tick-to-trade (da NIC a spazio utente, bypass del kernel, il totale inferiore a 100 µs e il livello inferiore a 10 µs che forza FPGA e memoria condivisa) è già illustrata in [dati e comunicazione nel trading algoritmico] (/en/blog/post/data-communication-algotrading). La riga che la scala lascia aperta è l'inferenza del modello, e questa è la riga che la distillazione sta cercando di riempire.
Resistere a riempire le altre righe con una tabella di latenza della classe modello. Spread modeling with machine learning pubblica già il confronto GBM-vs-deep-learning oltre all'avvertenza che conta più dei numeri: la latenza dipende dall'implementazione e lo stesso modello LightGBM richiede decine di microsecondi per riga da Python ma pochi microsecondi da un predittore compilato. Qualsiasi dichiarazione di latenza qui deve menzionare il framework, il core e la dimensione del batch, altrimenti è rumore.
Sulle GPU in particolare: il sovraccarico fisso per il lancio deve essere ammortizzato prima che un dispositivo possa essere d'aiuto, e l'inferenza su riga singola si trova molto a sinistra della cresta della linea del tetto dove non lo è mai. Quando la GPU ripaga misura correttamente la curva di ammortamento con una scansione batch, incluso il modo in cui una scheda PCIe discreta spinge la cresta più a destra: leggilo invece di fidarti di una costante citata dalla memoria.
Quantizzazione dopo la distillazione
Uno studente distillato comprime ulteriormente: pesi INT8 (circa 2x su CPU con AVX-512 VNNI), pesi binari/ternari che trasformano le moltiplicazioni in addizioni e potatura per saltare calcoli vicini allo zero.
L'affermazione allettante è che la distillazione e poi la quantizzazione preservano una maggiore precisione rispetto alla sola quantizzazione, poiché lo studente ha già imparato una rappresentazione compatta. Non spedire su di esso. La trappola di precisione della GPU è la posizione del blog sulla precisione numerica ridotta: ha restituito silenziosamente spazzatura dall'aspetto plausibile e ciò che ha reso spedibile il percorso veloce era un gate di equivalenza quantificato - riempimenti spostati, delta PnL in bps - non un'asserzione. Uno studente INT8 è un modello diverso finché quel cancello non viene confrontato con lo studente 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
Distribuzione FPGA: la pipeline da distill a bitstream

Gli FPGA sono il livello inferiore a 10 µs nella ladder di latenza e la revisione di Tbricks/Broadridge li copre in produzione insieme alle NIC con bypass del kernel: latenza deterministica, nessun jitter del sistema operativo, co-localizzati con lo stack di rete. Ciò che non viene trattato da nessuna parte in questo blog è il modo in cui un modello distillato arriva a uno di essi.
Le note di produzione di DeepLOB elencano ONNX/TensorRT, quantizzazione INT8 e distribuzione FPGA come tre opzioni e si fermano qui. Questo è ciò in cui si espande il terzo:
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
Il vincolo vincolante è che il modello deve adattarsi agli elementi logici del dispositivo: LUT, sezioni DSP, blocco RAM. Come budget di ordine di grandezza piuttosto che come misura: un MLP a 2 strati con 64 unità nascoste e pesi INT8 è dell'ordine di 8.000 moltiplicazioni per inferenza e ~ 16 KB di pesi, una piccola frazione di una parte di fascia media. È qui che la distillazione si guadagna da vivere: l'insegnante di ensemble non si adatta a nessun budget; lo studente non è neanche lontanamente vicino al limite.
Gli strumenti che automatizzano PyTorch/ONNX in hardware sintetizzabile includono AMD/Xilinx Vitis AI, hls4ml (del CERN) e FINN (di Xilinx Research).
Esempio: conversione 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() è l'unica fonte credibile di numeri di risorse e latenza per un dato modello, scheda, precisione e fattore di riutilizzo: le cifre cambiano sostanzialmente con default_reuse_factor solo. Citare una tabella di sintesi "tipica" senza eseguirla è un'ipotesi.
Considerazioni pratiche

Precalcolo dei registri degli insegnanti
La distillazione ha bisogno delle previsioni degli insegnanti sull'intero set di formazione: un costo offline una tantum che vale la pena pagare deliberatamente: eseguire l'insieme una volta, persistere i logit, addestrare gli studenti contro la cache. I controlli della temperatura e le ricerche sull'architettura non costano quindi nulla in più nei passaggi in avanti dell'insegnante, il che è ciò che rende pratici i controlli di cui sopra.
L'unico monitor specifico per la distillazione
L'igiene della pipeline delle funzionalità, la normalizzazione progressiva a causa della deriva dei parametri del punteggio z, il monitoraggio dello spostamento della distribuzione degli input e la riqualificazione attivata dal regime sono tutti trattati nella sezione di produzione di DeepLOB e si applicano invariati qui.
Il monitoraggio specifico della distillazione è la divergenza KL insegnante-studente sui dati in tempo reale. L'insegnante esiste ancora offline; eseguilo su un campione di input live e confronta le distribuzioni. L’aumento del KL significa che l’approssimazione dello studente si sta degradando nei regimi su cui non è stata distillata – e si attiva prima che lo faccia l’accuratezza, perché non attende le etichette. La soglia di riqualificazione deve essere calibrata rispetto al KL osservato in periodi notoriamente buoni e notoriamente degradati; scelto a priori è arbitrario.
Quando non distillare
- L'insegnante è già piccolo (un modello lineare, un GBM poco profondo): la distillazione aggiunge una fase di pipeline per nessuna compressione.
- La latenza non è un vincolo (ribilanciamento giornaliero, segnali di fine giornata): schierare l'insegnante.
- L'interpretabilità supera la velocità: una rete distillata è più difficile da spiegare rispetto all'insieme di alberi che ha sostituito.
- La suddivisione in due fasi funziona già: se il modello asincrono lento nell'architettura di modellazione diffusa funziona, la distillazione deve batterlo su un confronto misurato prima di giustificare la sostituzione di un sistema funzionante.
Riepilogo

La distillazione è un'alternativa coerente alla divisione in due fasi veloce/lento: forma il miglior insegnante che ti puoi permettere offline, trasferisci la sua struttura soft-target in uno studente abbastanza piccolo per il percorso caldo, quantizza, distribuisci su CPU o FPGA. La variante basata sulla profondità va oltre e rende la latenza una scelta di runtime piuttosto che una scelta di training.
Ciò che questo articolo deliberatamente non afferma è che nessuno di essi batte ciò che il blog già pubblica. Questo verdetto richiede tre misurazioni sui dati reali degli ordini: la curva di ritenzione F1 ponderata per studenti rispetto a gruppi divisa per regime, lo spostamento della temperatura e un gate di parità INT8 nello stile della trappola di precisione della GPU. Fino a quando non esisteranno, questa è la descrizione di una tecnica, non una raccomandazione per implementarla.
Riferimenti
-
Hinton, G., Vinyals, O. e Dean, J. (2015). Distillare la conoscenza in una rete neurale. arXiv:1503.02531
-
Furlanello, T., Lipton, Z. C., Tschannen, M., Itti, L., & Anandkumar, A. (2018). Reti neurali rinate. ICML. arXiv:1805.04770
-
Zhang, L., Song, J., Gao, A., Chen, J., Bao, C., & Ma, K. (2019). Sii il tuo insegnante: migliora le prestazioni delle reti neurali convoluzionali tramite l'autodistillazione. ICCV. arXiv:1905.08094
-
Romero, A., Ballas, N., Kahou, S.E., Chassang, A., Gatta, C., & Bengio, Y. (2015). FitNets: suggerimenti per reti sottili e profonde. ICLR. arXiv:1412.6550
-
Gou, J., Yu, B., Maybank, S. J. e Tao, D. (2021). Distillazione della conoscenza: un'indagine. Giornale internazionale di visione artificiale, 129, 1789-1819. arXiv:2006.05525
-
Duarte, J., et al. (2018). Inferenza rapida di reti neurali profonde in FPGA per la fisica delle particelle (hls4ml). Giornale di strumentazione, 13, P07027. arXiv:1804.06913
-
Umuroglu, Y., et al. (2017). FINN: A Framework for Fast, Scalable Binarized Neural Network Inference. FPGA '17. arXiv:1612.07119
-
Zhang, Z., Zohren, S. e Roberts, S. (2019). DeepLOB: reti neurali convoluzionali profonde per libri con ordini limitati. Transazioni IEEE sull'elaborazione del segnale, 67(11), 3001-3012. arXiv:1808.03668
Autori
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.