Model Pruning for Low-Latency Trading Inference
Artikel DeepLOB kami mengakhiri bahagian deployment dengan tiga pilihan — ONNX bersama TensorRT, quantization INT8 dan FPGA — tanpa menghuraikan mana-mana daripadanya. Artikel ini mengisi masalah pertama di bawah ketiga-tiganya: model lebih besar daripada yang diperlukan. Neural network pruning membuang parameter berlebihan, dan dakwaan menarik dalam literatur bukan sekadar penjimatan memori, tetapi bahawa subnetwork yang memegang 10-20% weight boleh menyamai accuracy model dense.
Blog ini sudah menunjukkan mengapa latency penting — ZigBolt pada laluan messaging dan cukai IPC dengan matematik break-even — manakala spread modeling telah membincangkan pertukaran pantas-tetapi-sedikit-lebih-buruk berbanding perlahan-tetapi-lebih-baik, lengkap dengan jadual gradient boosting berbanding deep learning yang mempunyai baris inference latency. Tiada satu pun menerangkan bagaimana mengecilkan model tertentu. Dalam peringkat quoting loop, model inference ialah bahagian yang sepenuhnya di bawah kawalan kita; bahagian transport dengan angka p50/p95/p99 yang boleh diulang diterangkan dalam komunikasi data untuk algotrading.
Artikel ini ialah: matematik dan kod yang berfungsi untuk magnitude pruning, structured pruning, Iterative Magnitude Pruning, movement pruning, knowledge distillation dan semi-structured sparsity NVIDIA 2:4, yang diterapkan pada MLP dagangan.
Artikel ini bukan: hasil yang telah diukur. Setiap artikel empirikal di blog ini mempunyai baris provenance atau repo pendamping, dan artikel ini belum mempunyai kedua-duanya. Lengkung sparsity-versus-accuracy-versus-latency di bawah ialah eksperimen yang perlu dijalankan, bukan jadual untuk dipetik. Anggap semua kandungan di sini sebagai kaedah, manakala nombornya masih menunggu ukuran.
Apa yang Diberikan oleh Pruning

Kekangannya ialah saiz. Pertimbangkan model mid-frequency — MLP 4 lapisan dengan 2048 unit tersembunyi untuk feature order book:
Untuk , , , , jumlahnya kira-kira 12.6 juta parameter — sekitar 48 MB dalam float32. L2 biasanya 1-4 MB, jadi weight tidak muat dan perlu distrim dari memori lebih jauh pada setiap forward pass. Prune 95% daripadanya dan anda mendapat kira-kira 630K parameter berkesan dan 2.4 MB, yang memang muat.
Sama ada ia diterjemahkan kepada wall-clock time bergantung pada sama ada kernel memory-bound, dan itu soalan arithmetic intensity, bukan saiz. Tangga kelajuan backtest engine menggunakan roofline model (Williams, Waterman & Patterson) melalui contoh yang diukur, bukannya menganggap penalty factor; kerangka yang sama terpakai di sini, begitu juga disiplin yang sama: ukur bytes yang dipindahkan sebelum mendakwa speedup.
Asas Pruning

Unstructured Pruning
Pendekatan paling mudah ialah menetapkan weight individu kepada sifar berdasarkan magnitudnya. Diberi weight matrix , bina binary mask seperti berikut:
di mana ialah threshold yang dipilih untuk mencapai tahap sparsity yang dikehendaki:
Matrix yang dipruning ialah , dengan ialah hasil darab Hadamard. Intuisinya ialah weight yang hampir sifar hanya sedikit menyumbang kepada output layer.
Masalahnya, secara terus terang kerana nombor sparsity mudah disalah baca: unstructured sparsity tidak semestinya menghasilkan speedup pada hardware standard. Matrix dengan 90% sifar masih mengeluarkan bilangan multiply-accumulates yang sama kecuali anda menggunakan sparse kernels atau hardware yang menyokong sparsity. Apabila kod di bawah mencetak Sparsity: 90.0%, itu ialah kiraan sifar — bukan 10x apa-apa, dan pada dense CPU GEMM juga bukan 1.01x. Laluan yang benar-benar menjimatkan masa ialah structured pruning (matrix lebih kecil) dan semi-structured sparsity 2:4 (sokongan hardware), kedua-duanya diterangkan di bawah.
Structured Pruning
Structured pruning membuang seluruh neuron, channel atau attention head. Untuk linear layer dengan , membuang neuron menjadikan baris ke- bagi dan elemen ke- bagi sifar:
Neuron dengan -norm terkecil dibuang dahulu. Varian ini menghasilkan matrix yang benar-benar lebih kecil — tetapi hanya jika layer dibina semula. Menjadikan baris sifar sambil mengekalkan bentuk asal tensor tidak mengubah kiraan FLOP; langkah rebuild dalam bahagian implementasi menukar mask menjadi matrix .
Untuk convolutional layers, padanannya ialah filter pruning. Diberi , kepentingan output filter ialah:
Membuang filter menghapuskan seluruh output channel dan mengurangkan FLOP secara berkadar.
Hipotesis Lottery Ticket

Pada 2019, Frankle dan Carbin memperkenalkan Hipotesis Lottery Ticket (LTH): dalam dense network yang diinisialisasi secara rawak terdapat subnetwork sparse — "winning ticket" — yang, apabila dilatih daripada initialization asal, menyamai accuracy network penuh dalam bilangan iteration yang setanding.
Secara formal, pertimbangkan yang diinisialisasi dengan . Selepas latihan mencapai convergence, kita memperoleh dan membina pruning mask . LTH menyatakan wujud sedemikian rupa sehingga:
dengan . Eksperimen asal menggunakan MNIST dan CIFAR-10, dengan winning ticket mengekalkan 10-20% parameter. Tiada sebab untuk menganggap hasil itu berpindah kepada data order book — feature LOB tidak stationari dan label hampir seperti noise, iaitu regime yang berbeza daripada klasifikasi imej dalam perkara yang mungkin penting.
Iterative Magnitude Pruning (IMP)
Ticket ditemui melalui IMP:
- Inisialisasikan network dengan .
- Latih sehingga convergence untuk memperoleh .
- Prune weight yang mempunyai magnitud terkecil dan bina mask .
- Reset weight yang terselamat kepada nilainya dalam (rewinding).
- Ulangi dari langkah 2 dengan network yang mempunyai mask.
Setiap pusingan memangkas pecahan (biasanya 20%), maka selepas pusingan parameter masih kekal. Selepas 10 pusingan dengan , kira-kira 10.7% masih tinggal.
Tiga Hipotesis tentang Model Dagangan, Tiada Satu Pun Diuji
Mudah untuk berhujah bahawa LTH sepatutnya berfungsi dengan baik pada data pasaran. Tiga hujah muncul; kesemuanya hipotesis, dan menyatakannya sebagai fakta ialah kesilapan yang cuba dielakkan oleh blog ini.
- Signal kewangan adalah sparse. Kebanyakan snapshot order book ialah noise, jadi subnetwork sparse mungkin selari secara semula jadi dengan signal sparse. Boleh diuji: bandingkan IMP dengan random mask yang mempunyai sparsity sama; jika sparsity itu sendiri yang bekerja, random mask tidak sepatutnya jauh ketinggalan.
- Winning ticket membuat generalisasi merentas regime. Ini ialah dakwaan empirikal tentang pasaran tanpa citation, dan paling menarik antara tiga hujah. Ia boleh diuji terus terhadap label regime daripada pengesanan regime dengan HMM: cari ticket dalam regime A, latih semula dalam regime B, dan bandingkan dengan ticket yang ditemui secara native dalam B.
- Sparsity melakukan regularisasi. Kapasiti berkesan yang lebih rendah mungkin mengurangkan fitting kepada noise microstructure — yang akan muncul sebagai jurang out-of-sample model pruned yang lebih kecil daripada model dense, bukan sekadar setanding.
Implementasi: Pruning MLP Dagangan

Model Asas
import torch
import torch.nn as nn
import torch.nn.utils.prune as prune
from copy import deepcopy
class TradingMLP(nn.Module):
"""MLP for mid-price direction prediction from order book features."""
def __init__(self, input_dim=100, hidden_dim=2048,
num_layers=4, output_dim=3):
super().__init__()
layers = []
dims = [input_dim] + [hidden_dim] * (num_layers - 1) + [output_dim]
for i in range(len(dims) - 1):
layers.append(nn.Linear(dims[i], dims[i + 1]))
if i < len(dims) - 2:
layers.append(nn.BatchNorm1d(dims[i + 1]))
layers.append(nn.ReLU())
layers.append(nn.Dropout(0.1))
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)
def count_parameters(self):
return sum(p.numel() for p in self.parameters())
model = TradingMLP(input_dim=100, hidden_dim=2048,
num_layers=4, output_dim=3)
print(f"Total parameters: {model.count_parameters():,}")
Unstructured Magnitude Pruning
def apply_unstructured_pruning(model, sparsity=0.9):
"""Apply global unstructured L1 pruning to all Linear layers."""
parameters_to_prune = []
for name, module in model.named_modules():
if isinstance(module, nn.Linear):
parameters_to_prune.append((module, 'weight'))
prune.global_unstructured(
parameters_to_prune,
pruning_method=prune.L1Unstructured,
amount=sparsity,
)
return model
def compute_sparsity(model):
"""Fraction of zero weights. Note: a *count*, not a speedup."""
total, zeros = 0, 0
for name, param in model.named_parameters():
if 'weight' in name:
total += param.numel()
zeros += (param == 0).sum().item()
return zeros / total if total > 0 else 0
pruned_model = apply_unstructured_pruning(deepcopy(model), sparsity=0.9)
print(f"Sparsity: {compute_sparsity(pruned_model):.1%}")
Structured Pruning dengan Rebuild yang Menjadikannya Nyata
Masking baris hanyalah separuh kerja. Separuh yang menghasilkan speedup ialah membina semula setiap layer dengan bentuk yang lebih kecil — ini bermaksud penyingkiran perlu disebarkan ke hadapan: membuang baris daripada layer turut membuang lajur daripada layer dan channel bagi mana-mana BatchNorm1d di antaranya.
def apply_structured_pruning(model, fraction=0.75):
"""Mask entire neurons by L2-norm of their weight rows."""
for name, module in model.named_modules():
if isinstance(module, nn.Linear) and module.out_features > 10:
prune.ln_structured(
module, name='weight', amount=fraction, n=2, dim=0
)
return model
def rebuild_pruned_mlp(model):
"""
Physically shrink a structurally pruned TradingMLP.
Walks the Sequential once. For each Linear: drop the input columns
the previous layer no longer emits, then drop its own dead output
rows. BatchNorm1d channels follow the preceding Linear's survivors.
"""
new_layers = []
keep_in = None # surviving output indices of the previous Linear
for layer in model.network:
if isinstance(layer, nn.Linear):
if prune.is_pruned(layer):
prune.remove(layer, 'weight')
W, b = layer.weight.data, layer.bias.data
keep_out = (W.norm(dim=1) > 0).nonzero(as_tuple=True)[0]
W = W[keep_out]
if keep_in is not None:
W = W[:, keep_in]
new = nn.Linear(W.shape[1], W.shape[0])
new.weight.data = W.clone()
new.bias.data = b[keep_out].clone()
new_layers.append(new)
keep_in = keep_out
elif isinstance(layer, nn.BatchNorm1d):
new = nn.BatchNorm1d(len(keep_in))
new.weight.data = layer.weight.data[keep_in].clone()
new.bias.data = layer.bias.data[keep_in].clone()
new.running_mean = layer.running_mean[keep_in].clone()
new.running_var = layer.running_var[keep_in].clone()
new.num_batches_tracked = layer.num_batches_tracked.clone()
new_layers.append(new)
else: # ReLU, Dropout -- shape-agnostic, reuse as is
new_layers.append(layer)
rebuilt = deepcopy(model)
rebuilt.network = nn.Sequential(*new_layers)
return rebuilt
Dua perkara perlu diperiksa sebelum mempercayainya, selaras dengan equivalence gate yang digunakan oleh bahagian lain blog ini:
- Shapes.
rebuiltsepatutnya menunjukkan dimensi hidden pada — 512 untukfraction=0.75, — dan bilangan parameter yang jatuh secara kuadratik kerana kedua-dua dimensi matrix dalaman mengecil. - Outputs. Dalam mod
eval(),rebuilt(x)mesti sepadan dengan output model bermask tanparebuiltdalam toleransi floating-point pada batch yang sama. Jika tidak, propagasi lajur salah dan setiap nombor downstream sedang mengukur model yang berbeza daripada sangkaan anda.
Ujian row-survival menganggap baris bermask betul-betul sifar dan baris aktif bukan sifar. Ini benar untuk output ln_structured; ia tidak semestinya benar jika prosedur lain menghasilkan neuron aktif yang benar-benar sifar, jadi sahkan bilangan yang terselamat terhadap pecahan yang diminta dan jangan mempercayai ujian norm secara membuta tuli.
Iterative Magnitude Pruning (Carian Lottery Ticket)
def lottery_ticket_search(model_cls, model_kwargs, train_fn, eval_fn,
rounds=10, prune_rate=0.2, device='cpu'):
"""
Iterative Magnitude Pruning to find a winning ticket.
Parameters
----------
model_cls : class -- model constructor
model_kwargs : dict -- constructor arguments
train_fn : callable -- train_fn(model) trains the model in-place
eval_fn : callable -- eval_fn(model) returns out-of-sample accuracy
rounds : int -- number of pruning rounds
prune_rate : float -- fraction of surviving weights pruned per round
"""
model_init = model_cls(**model_kwargs).to(device)
theta_0 = deepcopy(model_init.state_dict())
mask = {}
for name, param in model_init.named_parameters():
if 'weight' in name:
mask[name] = torch.ones_like(param, dtype=torch.bool)
results = []
for round_idx in range(rounds):
model = model_cls(**model_kwargs).to(device)
state = deepcopy(theta_0)
for name in mask:
state[name] = state[name] * mask[name].float()
model.load_state_dict(state)
train_fn(model)
acc = eval_fn(model)
surviving = sum(m.sum().item() for m in mask.values())
total = sum(m.numel() for m in mask.values())
sparsity = 1.0 - surviving / total
results.append({
'round': round_idx,
'accuracy': acc,
'sparsity': sparsity,
'surviving_params': int(surviving)
})
print(f"Round {round_idx}: acc={acc:.4f}, "
f"sparsity={sparsity:.1%}")
all_weights = []
for name, param in model.named_parameters():
if name in mask:
alive = param.data.abs()[mask[name]]
all_weights.append(alive.flatten())
all_weights = torch.cat(all_weights)
k = int(len(all_weights) * prune_rate)
if k == 0:
break
threshold = all_weights.kthvalue(k).values.item()
for name, param in model.named_parameters():
if name in mask:
mask[name] = mask[name] & (
param.data.abs() >= threshold
)
return results, mask
results ialah bahan mentah untuk lengkung sparsity-versus-accuracy yang perlu disediakan oleh artikel ini. eval_fn mesti benar-benar out-of-sample pada split yang telah dipurge — IMP yang diberi skor in-sample akan melaporkan lengkung cantik yang tidak bermakna.
Mengukurnya

Latency diukur dengan konvensyen harness yang sama seperti bahagian lain blog — warmup dikecualikan, best-of-N, dan p50/p95/p99 dilaporkan bukannya mean — dengan protokol dan kod dalam Polars berbanding pandas. Tiga perkara khusus untuk pruning:
- Benchmark model rebuilt, bukan model bermask. Model bermask pada batch size 1 mengukur bentuk dense.
- Laporkan batch size. Batch 1 (quoting loop) dan batch 256 (research sweep) berada di sisi berbeza garis memory-bound/compute-bound, dan pruning membantu kedua-duanya dengan cara berbeza.
- Laporkan accuracy pada split dan horizon yang sama, dengan definisi label dinyatakan. Jadual latency tanpa lajur accuracy sepadan ialah hujah untuk memadam model itu sepenuhnya.
Teknik Lanjutan

Pruning dengan Knowledge Distillation
Daripada menjalankan pruning dan fine-tuning secara berasingan, gunakan model dense asal sebagai teacher. Student yang dipruning meminimumkan gabungan task loss dan KL divergence daripada taburan output teacher:
di mana dan ialah logits teacher dan student, ialah temperature, dan mengimbangi objektif. Faktor menskala semula gradient distillation yang sebaliknya mengecil sebagai .
def distillation_loss(student_logits, teacher_logits, labels,
temperature=3.0, alpha=0.5):
"""Combined task + distillation loss."""
task_loss = nn.CrossEntropyLoss()(student_logits, labels)
soft_student = nn.functional.log_softmax(
student_logits / temperature, dim=-1
)
soft_teacher = nn.functional.softmax(
teacher_logits / temperature, dim=-1
)
kd_loss = nn.functional.kl_div(
soft_student, soft_teacher, reduction='batchmean'
)
return (1 - alpha) * task_loss + alpha * (temperature ** 2) * kd_loss
Movement Pruning
Daripada memangkas mengikut magnitud mutlak, movement pruning (Sanh et al., 2020) memangkas weight yang bergerak ke arah sifar semasa latihan. Skor kepentingan mengumpulkan hasil darab gradient-weight:
Weight dengan skor negatif dipruning. Hujah untuk memilihnya berbanding magnitude pruning khusus berkaitan fine-tuning: apabila menyesuaikan model pra-latih, taburan magnitud dibentuk oleh tugas pra-latihan, jadi magnitud ialah signal kepentingan yang lapuk dan arah pergerakan ialah signal yang lebih baharu. Untuk model dagangan yang dilatih semula pada rolling window, situasi itu lebih biasa daripada latihan dari awal.
Structured Sparsity NVIDIA 2:4
GPU NVIDIA Ampere dan yang lebih baharu menyokong structured sparsity 2:4 dalam hardware: daripada setiap 4 weight bersebelahan, tepat 2 mesti sifar.
Ini satu-satunya bentuk fine-grained sparsity yang benar-benar diberi ganjaran oleh hardware, sebab itu ia lebih penting daripada angka 90% sifar daripada unstructured pruning. Kekangannya bersifat lokal, bukan global — ia tidak peduli dua yang mana daripada setiap empat kekal — jadi sekatannya jauh lebih lemah daripada menetapkan global mask, walaupun 50% ialah satu-satunya tahap sparsity yang tersedia.
from torch.ao.pruning import WeightNormSparsifier
sparsifier = WeightNormSparsifier(
sparsity_level=0.5,
sparse_block_shape=(1, 4),
zeros_per_block=2,
)
sparsifier.prepare(
model, config=[{"tensor_fqn": "network.0.weight"}]
)
sparsifier.step()
sparsifier.squash_mask()
Untuk merealisasikan speedup, laluan inference mesti menggunakan sparse tensor cores — export ONNX bersama build TensorRT, atau torch.sparse.to_sparse_semi_structured. Mengeksport model bermask 2:4 melalui dense runtime memberi kos accuracy tanpa sebarang manfaat.
Deployment Production

Validation
Model yang dipruning ialah model baharu, bukan model lama yang dimampatkan, dan ia melalui acceptance gate yang sama seperti calon lain: rolling retraining dan out-of-sample revalidation mengikut walk-forward optimization, dengan pembetulan selection effect daripada deflated Sharpe ratio. Pembetulan itu tidak boleh diketepikan — IMP menghasilkan urutan model calon, jadi tahap sparsity yang kelihatan terbaik merentas sepuluh pusingan dipilih melalui carian, dan Sharpe-nya perlu dideflate mengikut bilangan percubaan berkesan. Peraturan rata seperti "tolak jika Sharpe jatuh lebih 5%" tidak bertahan dengan matematik itu, sebab itu anda tidak akan menemuinya dalam artikel ini.
Stacking Quantization
Pruning boleh digabungkan dengan quantization. Model 90% sparse yang diquantize kepada INT8 mempunyai nisbah pemampatan:
Model 48 MB menjadi 1.2 MB. Itu dakwaan tentang storan sahaja dan tidak lebih. Sama ada model 1.2 MB menghasilkan keputusan yang sama ialah soalan berasingan, dan perangkap precision GPU ialah sebab untuk bertanya: blog ini telah menunjukkan fp32 sahaja boleh menghasilkan relative error 211 dalam pengiraan backtest yang kelihatan munasabah. INT8 ialah pengurangan yang jauh lebih agresif. Hanya deploy model yang diquantize dan dipruning selepas lulus parity gate berangka berbanding model dense fp32 — kadar keputusan sepadan dan delta PnL pada tempoh held-out, bukan jaminan.
Monitoring
Model yang dipruning mungkin lebih sensitif terhadap distribution shift. Perkara yang wajar dipantau:
- Activation sparsity: jika neuron yang terselamat kebanyakannya mengeluarkan sifar, model berkesan lebih kecil daripada yang dirancang dan mungkin sedang merosot.
- Gradient norms semasa retraining: gradient yang meletup menunjukkan subnetwork yang terselamat diminta mengimbangi bahagian yang dibuang secara terlalu agresif.
- Prediction entropy: model pruned yang menjadi terlalu yakin pada data microstructure ber-noise mungkin sedang overfit kepada training regime.
Kesimpulan

Kaedah-kaedah ini sudah kukuh, dan sehingga sweep dijalankan, itulah sahaja dakwaan artikel ini. Unstructured pruning memberi angka sparsity tetapi tiada speed. Structured pruning memberi speed jika — dan hanya jika — layer dibina semula, bukan sekadar dimask. Hipotesis Lottery Ticket mencadangkan model kompak sudah wujud dalam model berparameter berlebihan, walaupun ia dibuktikan pada benchmark imej dan bukan data order book; tiga sebab bahawa ia "sepatutnya" berfungsi pada data pasaran ialah hipotesis bersama eksperimen, bukan dapatan.
Heuristik praktikal daripada literatur ialah melatih model besar kemudian memangkasnya, bukannya mereka bentuk model kecil sejak awal: model besar meneroka loss landscape dengan lebih berkesan, dan pruning mengekalkan laluan yang penting. Sama ada ini terpakai pada model dagangan, pada tahap sparsity berapa dan dengan kos accuracy berapa, hanya memerlukan satu IMP sweep — dan artikel ini patut dibaca semula selepas sweep itu, dengan nombor sebenar.
Pengarang
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.