Model Pruning for Low-Latency Trading Inference
DeepLOB мақаламыз deployment section-ін үш тармақпен аяқтайды — ONNX және TensorRT, INT8 quantization, FPGA — бірақ олардың ешқайсысын ашпайды. Бұл мақала сол үшеуінің астарындағы бірінші мәселені толтырады: model қажет болғаннан үлкен. Neural network pruning redundant parameter-лерді алып тастайды, ал literature-дегі қызықты claim memory үнемдеу емес, weight-тердің 10-20%-ын сақтайтын subnetwork dense model accuracy-сіне жете алатыны.
Latency-дің маңызды екені блогта әлдеқашан көрсетілген — messaging path-тағы ZigBolt және break-even arithmetic-і бар IPC tax — ал spread modeling inference-latency row-ы бар gradient-boosting пен deep-learning кестесімен fast-but-slightly-worse және slow-but-better trade-off-ін қамтиды. Бірақ олардың ешқайсысы берілген model-ді қалай кішірейту керегін түсіндірмейді. Quoting loop кезеңдерінің ішінде model inference толық біздің бақылауымызда; transport legs reproducible p50/p95/p99 сандарымен algotrading үшін data communication мақаласында қамтылған.
Бұл мақала не туралы: trading MLP-ге қолданылған magnitude pruning, structured pruning, Iterative Magnitude Pruning, movement pruning, knowledge distillation және NVIDIA 2:4 semi-structured sparsity үшін math пен жұмыс істейтін code.
Бұл не емес: өлшенген result емес. Блогтағы әрбір empirical article provenance line немесе companion repo алып жүреді, ал бұл мақалада әзірге екеуі де жоқ. Sparsity-versus-accuracy-versus-latency curve төменде келтіру үшін дайын table емес, іске қосылатын experiment ретінде беріледі. Мұндағының бәрін method, ал сандарды pending деп қабылдаңыз.
Pruning не береді

Мұндағы constraint — size. Order book feature-лері үшін 2048 hidden unit-і бар 4-layer MLP, яғни mid-frequency model-ді қарастырайық:
, , , болғанда бұл шамамен 12.6 million parameter — float32-та 48 MB. L2 әдетте 1-4 MB, сондықтан weight-тер сыймайды және әр forward pass кезінде алысырақтан stream жасалады. Олардың 95%-ын prune етсеңіз, шамамен 630K effective parameter және 2.4 MB қалады, бұл L2-ге сыяды.
Оның wall-clock time-ға айналуы kernel memory-bound па, соған байланысты, ал бұл size емес, arithmetic-intensity сұрағы. Backtest engine speed ladder roofline model-ін (Williams, Waterman & Patterson) penalty factor-ін жай айтпай, өлшенген example арқылы көрсетеді; мұнда да сол framing және сол тәртіп керек: speedup claim жасамас бұрын жылжытылған byte санын өлшеңіз.
Pruning негіздері

Құрылымсыз pruning
Ең қарапайым тәсіл: жеке weight-терді magnitude-іне қарай нөлге қою. weight matrix-і берілсе, мынадай binary mask жасаңыз:
— қажетті sparsity level -ке жету үшін таңдалған threshold:
Pruned matrix — , мұнда — Hadamard product. Интуициясы: нөлге жақын weight-тер layer output-ына аз үлес қосады.
Мәселе, sparsity санын қате түсіну оңай болғандықтан ашық айтайық: unstructured sparsity стандарт hardware-да speedup бермейді. 90% нөлі бар matrix sparse kernel-ге немесе sparsity қолдайтын hardware-ға ауыспасаңыз, дәл сол multiply-accumulate санын орындайды. Төмендегі code Sparsity: 90.0% шығарса, бұл нөлдердің саны ғана — ол ешқандай 10x емес, dense CPU GEMM-де 1.01x та емес. Уақыт үнемдейтін жолдар — structured pruning (кішірек matrix-тер) және 2:4 semi-structured sparsity (hardware support); екеуі де төменде.
Құрылымды pruning
Structured pruning толық neuron, channel немесе attention head-терді алып тастайды. linear layer-інде, болғанда, neuron-ін алып тастау -қатарды -тен және элементті -тен нөлдейді:
Ең кіші -norm neuron-дер алдымен алынады. Бұл шынымен кішірек matrix жасайтын variant — бірақ layer-лерді шын мәнінде rebuild етсеңіз ғана. Қатарларды нөлдеп, tensor-ді бастапқы shape-інде қалдыру FLOP count-ты өзгертпейді; implementation section-дегі rebuild step mask-ті matrix-ке айналдырады.
Convolutional layer-лердегі аналог — filter pruning. берілсе, output filter маңыздылығы:
Filter -ді алып тастау толық output channel-ді жояды және FLOP-ты пропорционалды азайтады.
Lottery Ticket Hypothesis

2019 жылы Frankle және Carbin Lottery Ticket Hypothesis (LTH) ұсынды: random initialized dense network ішінде бастапқы initialization-ынан train жасалғанда, iteration саны ұқсас болса да full network accuracy-сіне жететін sparse subnetwork — "winning ticket" — бар.
Formal түрде , мұнда арқылы initialized жасалған, model-ді қарастырыңыз. Convergence-ке дейін train жасағаннан кейін аламыз және pruning mask шығарамыз. LTH мынадай бар дейді:
мұнда . Бастапқы experiment MNIST және CIFAR-10-да жасалды, онда winning ticket-тер parameter-лердің 10-20%-ын сақтады. Мұны order book data-ға автоматты түрде көшіруге болмайды — LOB feature-лері non-stationary, ал label noise-қа жақын; бұл image classification-нан маңызды болуы мүмкін барлық тұрғыда басқа regime.
Итеративті magnitude pruning (IMP)
Ticket IMP арқылы табылады:
- Network-ті арқылы initialize жасаңыз.
- Convergence-ке дейін train жасап, алыңыз.
- Magnitude-і ең кіші weight-тердің -ын prune жасап, mask-ін жасаңыз.
- Surviving weight-терді мәндеріне reset жасаңыз (rewinding).
- Mask салынған network-пен 2-қадамнан қайталаңыз.
Әр round fraction-ін (әдетте 20%) prune етеді, сондықтан round-тан кейін parameter-лердің бөлігі қалады. болған 10 round-тан кейін шамамен 10.7% қалады.
Trading model-дер туралы үш hypothesis, ешқайсысы тексерілмеген
LTH market data-да ерекше жақсы жұмыс істеуі тиіс деп айту қызықты. Мұндай үш argument бар; үшеуі де hypothesis, оларды fact ретінде айту — блог болдырмауға тырысатын failure mode.
- Financial signal-дар sparse. Order book snapshot-ының көбі noise, сондықтан sparse subnetwork sparse signal-мен табиғи түрде aligned болуы мүмкін. Testable: IMP-ті дәл сондай sparsity-і бар random mask-пен салыстырыңыз; егер жұмысты sparsity өзі істесе, random mask қатты артта қалмауы тиіс.
- Winning ticket-тер regime-дер арасында generalize етеді. Бұл citation-ы жоқ нарық туралы empirical claim және үшеуінің ішіндегі ең қызықтысы. Оны HMM арқылы regime detection мақаласындағы regime label-дермен тікелей тексеруге болады: ticket-ті A regime-де табыңыз, B regime-де қайта train жасап, B-де native табылған ticket-пен салыстырыңыз.
- Sparsity regularize етеді. Төмен effective capacity microstructure noise-ына fit-ті азайтуы мүмкін — бұл pruned model-дің out-of-sample gap-і dense model-дікімен жай ғана салыстырмалы емес, кіші болуынан көрінеді.
Іске асыру: trading MLP-ін pruning жасау

Бастапқы model
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():,}")
Құрылымсыз 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%}")
Құрылымды pruning және оны шынайы ететін rebuild
Қатарларды mask жасау — жұмыстың жартысы. Speedup беретін жартысы әр layer-ді кішірейтілген shape-імен rebuild ету — яғни алып тастауды алға propagation жасау: row-ын layer-інен тастау column-ды layer-інен және олардың арасындағы кез келген BatchNorm1d-дің channel-ін де тастайды.
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
Бұған сенбес бұрын, блогтың қалған бөлігіндегі equivalence gate-тер рухында екі нәрсені тексеріңіз:
- Shapes.
rebuiltдеңгейіндегі hidden dimension-дерді көрсетуі керек —fraction=0.75, үшін 512 — әрі ішкі matrix-тердің екі өлшемі де кішірейетіндіктен, parameter count quadratic түрде азаюы тиіс. - Outputs.
eval()mode-ындаrebuilt(x)сол batch-тегі masked model-дің rebuild-ке дейінгі output-ымен floating-point tolerance шегінде сәйкес болуы керек. Сәйкес болмаса, column propagation қате және downstream сандарының бәрі сіз ойлағаннан басқа model-ді өлшеп тұр.
Row-survival test masked row дәл нөл, ал live row нөл емес деп ұйғарады. Бұл ln_structured output-ы үшін орындалады; басқа procedure шынымен нөлдік live neuron жасаса, орындалмас еді. Сондықтан norm test-ке соқыр сенбей, survivor count-ты сұралған fraction-мен assert етіңіз.
Итеративті magnitude pruning (Lottery Ticket Search)
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 — осы мақала ұсынуға тиіс sparsity-versus-accuracy curve-інің шикі материалы. eval_fn purged split-терде шын мәнінде out-of-sample болуы керек — in-sample бағаланған IMP run ештеңе білдірмейтін әдемі curve көрсетеді.
Оны өлшеу

Latency блогтың қалған бөлігімен бірдей harness convention арқылы өлшенеді — warmup есепке алынбайды, best-of-N, mean орнына p50/p95/p99 хабарланады — code-ы бар protocol Polars vs pandas мақаласында. Pruning-ге тән үш тармақ:
- Rebuilt model-ді benchmark жасаңыз, masked model-ді емес. Batch size 1 кезіндегі masked model dense shape-ті өлшейді.
- Batch size-ті хабарлаңыз. Batch 1 (quoting loop) және batch 256 (research sweep) memory-bound/compute-bound сызығының екі жағында орналасады, pruning оларға әртүрлі көмектеседі.
- Accuracy-ді сол split-те, сол horizon-да, label definition-ін көрсетіп хабарлаңыз. Сәйкес accuracy column-ы жоқ latency table model-ді толық жоюға дәлел болады.
Жетілдірілген тәсілдер

Білімді дистилляциялау арқылы pruning
Pruning пен fine-tuning-ді бөлек жасаудың орнына, бастапқы dense model-ді teacher ретінде пайдаланыңыз. Pruned student task loss пен teacher output distribution-ынан KL divergence комбинациясын минимизациялайды:
мұнда және teacher мен student logit-тері, — temperature, мақсаттар балансын басқарады. factor distillation gradient-терін қайта масштабтайды, онсыз олар ретінде кішірейеді.
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
Absolute magnitude бойынша prune жасаудың орнына, movement pruning (Sanh et al., 2020) training кезінде нөлге қарай жылжып жатқан weight-терді prune етеді. Importance score gradient-weight product-ін жинақтайды:
Negative score-і бар weight-тер prune жасалады. Оның magnitude pruning-нен артықшылығы fine-tuning-ге қатысты: pre-trained model-ді бейімдегенде magnitude distribution pre-training task арқылы қалыптасқан, сондықтан magnitude — ескірген importance signal, ал қозғалыс бағыты — жаңарақ signal. Rolling window-дерде қайта train жасалатын trading model үшін бұл scratch-тен training-ге қарағанда жиірек кездесетін жағдай.
NVIDIA 2:4 құрылымды sparsity
Ampere және кейінгі NVIDIA GPU-лары hardware деңгейінде 2:4 structured sparsity-ді қолдайды: қатар тұрған әр 4 weight-тің дәл 2-еуі нөл болуы тиіс.
Бұл hardware шын мәнінде сыйақы беретін fine-grained sparsity-дің жалғыз түрі, сондықтан unstructured pruning-дегі 90%-zeros санынан маңыздырақ. Constraint global емес, local — әр төрттіктің қай екеуі қалатыны маңызды емес — сондықтан global mask бекітуден әлдеқайда әлсіз шектеу, бірақ ұсынылатын жалғыз sparsity level — 50%.
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()
Speedup-ты іске асыру үшін inference path sparse tensor core-ларды пайдалануы керек — ONNX export пен TensorRT build немесе torch.sparse.to_sparse_semi_structured. 2:4-masked model-ді dense runtime арқылы export ету accuracy cost-ін береді, бірақ пайда бермейді.
Production-ға енгізу

Валидация
Pruned model — compressed old model емес, жаңа model және кез келген басқа candidate сияқты acceptance gate-тен өтеді: walk-forward optimization бойынша rolling retraining және out-of-sample revalidation, deflated Sharpe ratio арқылы selection-effect correction. Бұл correction мұнда optional емес — IMP candidate model-дердің sequence-ін жасайды, сондықтан он round ішінде ең жақсы көрінген sparsity level search астында таңдалған, оның Sharpe-ін effective trial санына қарай deflate ету керек. "Sharpe 5%-дан көп түссе reject ет" сияқты flat rule бұл arithmetic-тен өтпейді, сондықтан оны бұл мақаладан таппайсыз.
Quantization-ды қабаттастыру
Pruning quantization-мен бірге қолданылады. 90% sparse және INT8-ге quantized жасалған model-дің compression ratio-сы:
48 MB model 1.2 MB болады. Бұл тек storage claim, басқа ештеңе емес. 1.2 MB model дәл сондай decision шығара ма — өз жауабы бар бөлек сұрақ. Оны қоюға GPU precision trap себеп: осы блогта fp32-тің өзі толық орынды көрінген backtest computation-да 211 relative error берген. INT8 одан да агрессивті reduction. Quantized-and-pruned model-ді fp32 dense model-ге қатысты quantified parity gate артында ғана ship етіңіз — held-out period-тегі decision agreement rate және PnL delta арқылы, assurance арқылы емес.
Мониторинг
Pruned model-дер distribution shift-ке сезімтал болуы мүмкін. Бақылауға тұрарлық нәрселер:
- Activation sparsity: surviving neuron-дер негізінен нөл шығарса, effective model ойлағаннан кішірек және нашарлап жатқан болуы мүмкін.
- Retraining кезіндегі gradient norm-дар: exploding gradient-тер surviving subnetwork-тен алып тасталған нәрсені тым агрессивті өтеу талап етіліп жатқанын көрсетеді.
- Prediction entropy: noisy microstructure data-да overconfident болып кеткен pruned model training regime-ге fit жасап жатқан болуы ықтимал.
Қорытынды

Бұл әдістер жақсы қалыптасқан, ал sweep іске қосылғанша мақала осыны ғана claim етеді. Unstructured pruning sparsity санын береді, speed бермейді. Structured pruning layer-лерді mask етпей, rebuild етсеңіз ғана speed береді. Lottery Ticket Hypothesis compact model overparameterized model ішінде әлдеқашан бар деп болжайды, бірақ бұл image benchmark-терінде көрсетілген, order book data-да емес; жоғарыда келтірілген оның market data-да "жұмыс істеуі тиіс" деген үш себебі — experiment-термен бекітілген hypothesis-тер, finding-тер емес.
Literature-дегі practical heuristic — басынан small model design етудің орнына, үлкен model-ді train жасап, кейін prune ету: үлкен model loss landscape-ті тиімдірек зерттейді, ал pruning маңызды болған жолдарды сақтайды. Бұл trading model үшін, қандай sparsity-де және қандай accuracy cost-пен орындалатыны бір IMP sweep қашықтықта — осы мақаланы сол sweep-тен кейін, ішіндегі сандармен қайта оқу керек.
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.