Knowledge Distillation: Compressing Trading Models for Low-Latency Deployment
ML-आधारित ट्रेडिंग में accuracy-vs-latency तनाव का इस ब्लॉग पर पहले से प्रकाशित उत्तर है। मशीन लर्निंग से स्प्रेड मॉडलिंग दो-चरणीय विभाजन सुझाता है: तेज gradient-boosting मॉडल latency-critical real-time quoting करता है, जबकि deep मॉडल asynchronous रूप से चलता है और उसे secondary signal देता है या उसके parameters समायोजित करता है। दो मॉडल, दो घड़ियां, एक सिस्टम।
Knowledge distillation इसी तनाव का अलग उत्तर है। धीमे मॉडल को तेज मॉडल के साथ चलाने के बजाय, आप उसे एक बार offline चलाकर तेज मॉडल को train करते हैं — student केवल hard labels नहीं सीखता, बल्कि outcomes पर teacher का पूरा probability distribution सीखता है, और फिर teacher hot path से पूरी तरह हट जाता है। Inference के समय एक मॉडल, कोई asynchronous coupling नहीं, कोई staleness window नहीं।
कौन-सा उत्तर जीतता है, यह empirical प्रश्न है और यह लेख अभी उसका उत्तर नहीं देता। आगे machinery के साथ उन measurements का स्पष्ट विवरण है जो फैसला करेंगे। यहां कुछ भी benchmark result नहीं है; जहां सामान्यतः कोई संख्या होनी चाहिए, वहां यह marker है कि क्या चलाना होगा।
शुरू में एक framing correction, DeepLOB और order book पर deep learning से: high classification accuracy अपने-आप profit में नहीं बदलती — predicted move को bid-ask spread पार करना होगा। इसलिए "teacher की directional accuracy बचाना" distillation setup को optimize करने का गलत लक्ष्य है।
Teacher-Student Framework

Hinton, Vinyals और Dean (2015) का मूल formulation सीधा है। आपके पास teacher मॉडल (बड़ा, धीमा, accurate) और student मॉडल (छोटा, तेज, जिसे train करना है) होता है। Student एक साथ दो signals से सीखता है:
- Hard targets: ground-truth labels (जैसे, कीमत ऊपर गई या नीचे)
- Soft targets: सभी classes पर teacher का output probability distribution
Student का loss function दोनों को मिलाता है:
जहां और teacher और student logits हैं, softmax function है, temperature parameter है, और दोनों loss components के बीच संतुलन नियंत्रित करता है।
ट्रेडिंग के लिए Soft Targets क्यों महत्वपूर्ण हैं
तीन-class up/stationary/down mid-price formulation, thresholding, और resulting imbalance जिसके कारण accuracy के बजाय weighted F1 report किया जाता है, सभी DeepLOB में setup हैं — यहां वही label scheme मानें। Distillation-specific बात यह है कि teacher argmax से पहले क्या emit करता है: hard "up" एक bit रखता है, जबकि 0.72/0.21/0.07 यह भी कहता है कि move रुक सकता है और लगभग निश्चित रूप से reverse नहीं होगा। Classes के बीच यह structure अतिरिक्त training signal है, और इसी कारण soft-target student केवल labels पर train किए गए उसी student से बेहतर generalize कर सकता है।
यह चेतावनी भी कि वह confidence क्या नहीं है। Softmax output calibrated uncertainty नहीं है, और 0.55 बनाम 0.85 को position-sizing input मानना वही shortcut है जिसे ट्रेडिंग के लिए conformal prediction स्वीकार करने से मना करता है — वह sizing interval width, edge ratio और तब no-trade filter से निकालता है जब interval zero को पार करता है; raw softmax इनमें से कुछ नहीं देता। यहां sizing claim कमाने के लिए student's calibration को teacher की calibration से (reliability diagram, ECE) मापना और दिखाना होगा कि distillation उसे बनाए रखती है। वह result अभी इस लेख में नहीं है।
Temperature और Soft Targets

Temperature parameter probability distribution की "softness" नियंत्रित करता है। Logits दिए हों तो temperature वाला softmax है:
जब (standard softmax) होता है, distribution peaky होता है — dominant class को probability mass का अधिकांश भाग मिलता है। जैसे-जैसे बढ़ता है, distribution flatten होता है और logits के relative magnitudes अधिक स्पष्ट दिखते हैं।
| Temperature | प्रभाव | उपयोग |
|---|---|---|
| Standard softmax, peaky | Normal inference | |
| Moderate softening | General distillation | |
| Heavy softening | जब teacher बहुत confident हो | |
| लगभग uniform | शायद ही उपयोगी, signal मिटा देता है |
एक plausible argument है कि trading models moderate temperature चाहते हैं: financial predictions image classification से बहुत कम confident होती हैं, इसलिए teacher 0.99/0.005/0.005 के बजाय 0.55/0.30/0.15 output कर सकता है, जिससे signal मिटने से पहले soften करने के लिए peakiness कम बचती है। यह argument है, finding नहीं — range real data पर sweep से आनी चाहिए, weighted F1 से score होनी चाहिए, और regime के अनुसार अलग हो सकती है।
KL divergence term में factor अधिक temperatures पर घटती gradient magnitudes की भरपाई करता है। इसके बिना बढ़ने पर distillation loss नगण्य रूप से छोटा हो जाता।
Grid Search से Temperature चुनना
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
Ensembles को एक मॉडल में Distill करना

एक quant ensemble अलग-अलग inductive biases मिलाता है: order-book features पर gradient-boosted tree, recent ticks पर 1D-CNN, multi-timeframe windows पर transformer, और macro factors पर linear model। Averaging किसी भी अकेले member से अधिक stable है, और चारों को चलाने से latency और cost कई गुना हो जाती है — यही स्थिति spread modeling with machine learning का two-stage split संभालता है, slow members को asynchronous side channel में भेजकर। Distillation इसके बजाय चारों को hot path में एक student में समेटता है।
Ensemble teacher का output उसके members के softmax outputs का average है:
जहां ensemble members की संख्या है। Student को इस averaged distribution पर train किया जाता है।
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)
Parameter-count asymmetry ही पूरी बात है: 64 hidden units वाला two-layer MLP, 60-feature और 3-class task के लिए लगभग 8,000 parameters का होता है, जबकि ensemble की combined count millions तक पहुंचती है।
Student क्या बचाता है और क्या खोता है
यह निर्णायक empirical प्रश्न है और लेख इसका उत्तर नहीं देता। Intuition यह है कि student in-distribution ensemble को track करता है और stressed regimes में पीछे रह जाता है, जहां ensemble की diversity काम कर रही होती है — लेकिन retention figure का अर्थ केवल real order-book data पर, regime के अनुसार विभाजित और weighted F1 के रूप में report किए गए measurement से है। जो student शांत दिनों में ठीक रहता है और liquidation cascade के दौरान collapse करता है, वह ऐसे student से अलग product है जो धीरे-धीरे degrade होता है; aggregate number दोनों को अलग नहीं कर सकता।
इस measurement के आधार पर जांचने योग्य तीन mitigations हैं, पहले से दावा करने योग्य नहीं:
- Stressed periods शामिल करें distillation set में, ताकि student उन regimes को देखे जहां gap खुलने की उम्मीद है।
- Feature-based distillation — केवल final outputs नहीं, intermediate representations को match करें।
- Student पर auxiliary regime head, जो shared trunk में regime-aware features को मजबूर करे।
Self-Distillation: जब Student Teacher बन जाता है

Self-distillation ऐसी तकनीक है जिसमें मॉडल खुद से knowledge distill करता है।
Born-Again Networks (BANs)
Teacher जैसी ही architecture वाला student train करें। "Born-again" student अक्सर original से बेहतर होता है, और प्रक्रिया दोहराई जाती है:
हर generation पिछले वाले से soft targets पर train होती है, और gains आम तौर पर कुछ generations के बाद saturate हो जाते हैं। Trading models के लिए architectural cost शून्य है — कोई नए features नहीं, कोई नया data नहीं, केवल अलग training procedure — इसलिए इसे test करना सस्ता है और बिना test किए report करने का कोई बहाना नहीं है।
Depth-Wise Self-Distillation
Intermediate layers पर auxiliary classifiers जोड़ें। सबसे deep exit shallower exits के लिए teacher का काम करता है। Inference पर exit चुनें: कम latency के लिए shallow, अधिकतम accuracy के लिए deep।
Trading system में सबसे अच्छा fit यही विचार है, क्योंकि exit depth runtime latency knob बन जाती है: training time पर एक architecture चुनने के बजाय एक trained network budgets की range को cover करता है। जब order book तेजी से बदल रहा हो तो shallow exit लें और खराब posterior स्वीकार करें; जब शांत हो तो full depth की कीमत दें। हर exit की accuracy-per-exit और latency-per-exit curves मापी जा सकती हैं, और उनका crossover तय करता है कि knob रखना सार्थक है या नहीं।
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
Inference Budget कहां से आता है

Distillation तभी महत्वपूर्ण है जब inference को hard budget के भीतर रहना हो, और पूरा tick-to-trade ladder — NIC-to-userspace, kernel bypass, sub-100 µs total और sub-10 µs tier जो FPGA और shared memory को मजबूर करता है — पहले से algorithmic trading में data और communication में दिया गया है। उस ladder की खुली row model inference है, और distillation उसी row को भरना चाहता है।
बाकी rows को model-class latency table से भरने से बचें। मशीन लर्निंग से spread modeling पहले ही GBM-vs-deep-learning comparison प्रकाशित करता है, साथ उस caveat के जो numbers से ज्यादा महत्वपूर्ण है: latency implementation-dependent है, और वही LightGBM model Python से प्रति row tens of microseconds लेता है, जबकि compiled predictor से कुछ microseconds। यहां latency claim को framework, core और batch size बताना होगा, वरना वह noise है।
GPU पर खास तौर पर: fixed per-launch overhead को पहले amortize करना पड़ता है, तभी device मदद करता है, और single-row inference roofline ridge से बहुत बाईं ओर होता है जहां वह कभी नहीं पहुंचता। GPU कब लाभ देता है batch sweep के साथ इस amortization curve को ठीक से मापता है, जिसमें यह भी शामिल है कि discrete PCIe card ridge को और दाईं ओर धकेलता है — memory से quote किए constant पर भरोसा करने के बजाय उसे पढ़ें।
Distillation के बाद Quantization
Distilled student को और compress किया जा सकता है: INT8 weights (AVX-512 VNNI के साथ CPU पर लगभग 2x), binary/ternary weights जो multiplies को adds में बदलते हैं, और pruning जो near-zero computation छोड़ देती है।
लुभावना दावा है कि distillation-then-quantization, quantization alone से अधिक accuracy बचाती है, क्योंकि student ने पहले ही compact representation सीख ली है। इसके आधार पर ship न करें। GPU precision trap reduced numeric precision पर ब्लॉग की स्थायी स्थिति है: उसने चुपचाप plausible-looking garbage लौटाया, और fast path को shippable बनाने वाली चीज quantified equivalence gate थी — fills shifted, PnL delta in bps — कोई assertion नहीं। INT8 student तब तक अलग model है जब तक उसे FP32 student के विरुद्ध उस gate से मापा न जाए।
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
FPGA Deployment: Distill-to-Bitstream Pipeline

FPGAs latency ladder में sub-10 µs tier हैं, और Tbricks/Broadridge review उन्हें kernel-bypass NICs के साथ production में cover करता है — deterministic latency, OS jitter नहीं, network stack के साथ co-located। इस ब्लॉग पर कहीं और यह नहीं बताया गया है कि distilled model इनमें से किसी एक तक कैसे पहुंचता है।
DeepLOB के production notes ONNX/TensorRT, INT8 quantization और FPGA deployment को तीन options के रूप में सूचीबद्ध करके रुक जाते हैं। तीसरा option यहां इस तरह खुलता है:
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
Binding constraint यह है कि model device के logic elements — LUTs, DSP slices, block RAM — में fit होना चाहिए। Measurement के बजाय order-of-magnitude budget के रूप में: 64 hidden units वाला 2-layer MLP और INT8 weights प्रति inference लगभग 8,000 multiply-accumulates और लगभग 16 KB weights का होता है, जो mid-range part का छोटा अंश है। यहीं distillation अपना लाभ कमाती है — ensemble teacher किसी budget में fit नहीं होता; student limit के पास भी नहीं है।
PyTorch/ONNX को synthesizable hardware में automate करने वाले tools में AMD/Xilinx Vitis AI, hls4ml (CERN से) और FINN (Xilinx Research से) शामिल हैं।
उदाहरण: hls4ml Conversion
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() किसी दिए गए model, board, precision और reuse factor के resource और latency numbers का एकमात्र credible source है — अकेले default_reuse_factor से ही figures काफी बदल जाते हैं। बिना चलाए "typical" synthesis table quote करना guessing है।
Practical Considerations

Teacher Logits को पहले से निकालना
Distillation को पूरे training set पर teacher predictions चाहिए — एक बार की offline cost जिसे जानबूझकर देना उचित है: ensemble को एक बार चलाएं, logits persist करें, और cache के विरुद्ध students train करें। इसके बाद temperature sweeps और architecture searches में teacher forward passes की कोई अतिरिक्त cost नहीं होती, इसी से ऊपर के sweeps व्यावहारिक बनते हैं।
Distillation-Specific एक Monitor
Feature-pipeline hygiene, rolling normalization क्योंकि z-score parameters drift करते हैं, input distribution-shift monitoring और regime-triggered retraining सभी DeepLOB के production section में covered हैं और यहां भी बिना बदलाव लागू होते हैं।
Distillation-specific monitor live data पर teacher-student KL divergence है। Teacher offline अभी भी मौजूद है; live inputs के sample पर उसे चलाएं और distributions की तुलना करें। बढ़ता KL बताता है कि जिन regimes पर distill नहीं किया गया उनमें student का approximation degrade हो रहा है — और यह accuracy से पहले fire करता है, क्योंकि labels का इंतजार नहीं करता। Retraining threshold known-good और known-degraded periods में observed KL के आधार पर calibrate होना चाहिए; a priori चुना गया threshold arbitrary है।
कब Distill न करें
- Teacher पहले ही छोटा है (linear model, shallow GBM): compression के बिना distillation एक pipeline stage जोड़ती है।
- Latency constraint नहीं है (daily rebalancing, end-of-day signals): teacher को deploy करें।
- Interpretability speed से ऊपर है: distilled network को उस tree ensemble से समझाना कठिन है जिसे उसने replace किया।
- Two-stage split पहले ही काम करता है: यदि spread-modeling architecture का asynchronous slow model परिणाम दे रहा है, तो working system को बदलने को उचित ठहराने से पहले distillation को measured comparison में उसे हराना होगा।
Summary

Distillation two-stage fast/slow split का coherent alternative है: offline जितना अच्छा teacher afford कर सकें train करें, उसके soft-target structure को hot path के लिए पर्याप्त छोटे student में transfer करें, quantize करें, और CPU या FPGA पर deploy करें। Depth-wise variant इससे आगे जाता है और latency को training-time choice के बजाय runtime choice बनाता है।
यह लेख जानबूझकर यह दावा नहीं करता कि इनमें से कुछ भी ब्लॉग में पहले से प्रकाशित उपायों से बेहतर है। उस verdict के लिए real order-book data पर तीन measurements चाहिए: regime के अनुसार split की गई student-vs-ensemble weighted F1 retention curve, temperature sweep और GPU precision trap की शैली में INT8 parity gate। जब तक ये मौजूद नहीं हैं, यह technique का description है, deployment की recommendation नहीं।
संदर्भ
-
Hinton, G., Vinyals, O., & Dean, J. (2015). एक न्यूरल नेटवर्क में ज्ञान का डिस्टिलेशन। arXiv:1503.02531
-
Furlanello, T., Lipton, Z. C., Tschannen, M., Itti, L., & Anandkumar, A. (2018). Born-Again न्यूरल नेटवर्क। ICML. arXiv:1805.04770
-
Zhang, L., Song, J., Gao, A., Chen, J., Bao, C., & Ma, K. (2019). अपने शिक्षक स्वयं बनें: Self Distillation से Convolutional Neural Networks का प्रदर्शन सुधारना। ICCV. arXiv:1905.08094
-
Romero, A., Ballas, N., Kahou, S. E., Chassang, A., Gatta, C., & Bengio, Y. (2015). FitNets: पतले Deep Nets के लिए संकेत। ICLR. arXiv:1412.6550
-
Gou, J., Yu, B., Maybank, S. J., & Tao, D. (2021). Knowledge Distillation: एक सर्वेक्षण। International Journal of Computer Vision, 129, 1789-1819. arXiv:2006.05525
-
Duarte, J., et al. (2018). Particle Physics के लिए FPGAs में Deep Neural Networks का तेज Inference (hls4ml)। Journal of Instrumentation, 13, P07027. arXiv:1804.06913
-
Umuroglu, Y., et al. (2017). FINN: तेज, scalable Binarized Neural Network Inference का framework। FPGA '17. arXiv:1612.07119
-
Zhang, Z., Zohren, S., & Roberts, S. (2019). DeepLOB: Limit Order Books के लिए Deep Convolutional Neural Networks। IEEE Transactions on Signal Processing, 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.