← Zurück zu den Artikeln
August 14, 2026
5 min read

Model Pruning for Low-Latency Trading Inference

Model Pruning for Low-Latency Trading Inference
#model-compression
#pruning
#lottery-ticket
#latency
#deployment

Unser DeepLOB-Artikel beendet seinen Bereitstellungsabschnitt mit drei Aufzählungszeichen – ONNX plus TensorRT, INT8-Quantisierung, FPGA – und behandelt keine davon. Dieser Artikel ist die fehlende Behandlung des ersten Problems unter allen dreien: Das Modell ist größer als es sein muss. Durch die Bereinigung neuronaler Netzwerke werden redundante Parameter entfernt, und die interessante Behauptung in der Literatur lautet nicht, dass dadurch Speicher gespart wird, sondern dass ein Subnetzwerk, das 10–20 % der Gewichte enthält, mit der Genauigkeit des dichten Modells mithalten kann.

Dass die Latenz überhaupt eine Rolle spielt, hat der Blog bereits dargelegt – ZigBolt auf dem Messaging-Pfad und die IPC-Steuer mit der Break-Even-Arithmetik – und Spread Modeling besitzt bereits die Schnell-aber-etwas-schlechter versus langsam-aber-besserer Kompromiss, komplett mit einer Gradient-Boosting- versus Deep-Learning-Tabelle, die eine Inferenzlatenzzeile enthält. Was keiner von ihnen abdeckt, ist, wie man ein bestimmtes Modell verkleinert. Von den Phasen einer Zitatschleife unterliegt die Modellinferenz vollständig unserer Kontrolle; Die Transportstrecken werden mit reproduzierbaren p50/p95/p99-Nummern in Datenkommunikation für Algotrading abgedeckt.

Was dieser Artikel ist: der Mathematik- und Arbeitscode für Magnitude Pruning, Structured Pruning, Iterative Magnitude Pruning, Movement Pruning, Knowledge Destillation und NVIDIA 2:4 Semi-Structured Sparsity, angewendet auf ein Trading-MLP.

Was es nicht ist: ein gemessenes Ergebnis. Jeder empirische Artikel in diesem Blog trägt eine Provenienzlinie oder ein begleitendes Repo, und dieser trägt noch keines von beiden. Die Sparsity-versus-Genauigkeit-versus-Latenz-Kurve wird unten als Experiment zur Durchführung und nicht als Tabelle zum Zitieren angegeben. Behandeln Sie hier alles als Methode und die Zahlen als ausstehend.

Was Sie durch Beschneiden gewinnen

Kompaktes neuronales Netzwerk, das in eine schnelle Cache-Schicht passt

Die Einschränkung ist eine Größe eins. Betrachten Sie ein Mittelfrequenzmodell – ein 4-Schicht-MLP mit 2048 versteckten Einheiten über den Orderbuchfunktionen:

Parameters=dinh+h2(L1)+hdout\text{Parameters} = d_{\text{in}} \cdot h + h^2 \cdot (L - 1) + h \cdot d_{\text{out}}

Für din=100d_{\text{in}} = 100, h=2048h = 2048, L=4L = 4, dout=3d_{\text{out}} = 3, das sind ungefähr 12,6 Millionen Parameter – ungefähr 48 MB in float32. L2 ist normalerweise 1–4 MB groß, daher passen die Gewichtungen nicht; Sie werden bei jedem Vorwärtsdurchgang von weiter außen gestreamt. Wenn Sie 95 % davon bereinigen, haben Sie etwa 630 KB effektive Parameter und 2,4 MB, was durchaus passt.

Ob sich das auf die Arbeitszeit übertragen lässt, hängt davon ab, ob der Kernel speichergebunden ist, und das ist eher eine Frage der arithmetischen Intensität als eine Frage der Größe. Die Backtest-Motorgeschwindigkeitsleiter arbeitet das Dachlinienmodell (Williams, Waterman & Patterson) anhand eines gemessenen Beispiels, anstatt einen Straffaktor geltend zu machen; Hier gilt der gleiche Rahmen und die gleiche Disziplin: Messen Sie die verschobenen Bytes, bevor Sie die Beschleunigung in Anspruch nehmen.

Grundlagen des Beschneidens

Strukturiertes und unstrukturiertes Bereinigen über ein neuronales Gitter

Unstrukturierter Schnitt

Der einfachste Ansatz: Setzen Sie einzelne Gewichte basierend auf ihrer Größe auf Null. Gegeben sei eine Gewichtsmatrix WRm×n\mathbf{W} \in \mathbb{R}^{m \times n}, erstellen Sie eine Binärmaske M{0,1}m×n\mathbf{M} \in \{0, 1\}^{m \times n} so dass:

Mij={1if Wijτ0if Wij<τM_{ij} = \begin{cases} 1 & \text{if } |W_{ij}| \geq \tau \\ 0 & \text{if } |W_{ij}| < \tau \end{cases}

Wo τ\tau ist ein Schwellenwert, der gewählt wird, um das gewünschte Sparsity-Niveau zu erreichen ss:

s=1M0mns = 1 - \frac{\|\mathbf{M}\|_0}{m \cdot n}

Die beschnittene Matrix ist W=WM\mathbf{W}' = \mathbf{W} \odot \mathbf{M}, mit \odot das Hadamard-Produkt. Die Intuition ist, dass Gewichte nahe Null wenig zur Ausgabe der Ebene beitragen.

Das Problem wird klar formuliert, weil es leicht ist, die Sparsity-Zahl falsch zu interpretieren: Unstrukturierte Sparsity führt nicht zu einer Beschleunigung auf Standardhardware. Eine Matrix mit 90 % Nullen gibt immer noch die gleiche Anzahl von Multiplikations-Akkumulierungen aus, es sei denn, Sie wechseln zu Sparse-Kerneln oder Hardware mit Sparsity-Unterstützung. Wenn der folgende Code gedruckt wird Sparsity: 90.0%, das ist eine Zählung von Nullen – es ist kein 10-faches, und auf einem GEMM mit dichter CPU ist es auch kein 1,01-faches. Die Pfade, die Zeit sparen, sind strukturiertes Pruning (kleinere Matrizen) und 2:4 halbstrukturierte Sparsity (Hardware-Unterstützung), beide unten.

Strukturierter Schnitt

Beim strukturierten Beschneiden werden ganze Neuronen, Kanäle oder Aufmerksamkeitsköpfe entfernt. Für eine lineare Ebene y=Wx+b\mathbf{y} = \mathbf{W}\mathbf{x} + \mathbf{b} mit WRm×n\mathbf{W} \in \mathbb{R}^{m \times n}, Neuron entfernen jj setzt das auf Null jj-te Reihe von W\mathbf{W} und die jj-tes Element von b\mathbf{b}:

importance(j)=Wj,:2=k=1nWjk2\text{importance}(j) = \|\mathbf{W}_{j,:}\|_2 = \sqrt{\sum_{k=1}^{n} W_{jk}^2}

Neuronen mit den kleinsten 2\ell_2-Norm geht zuerst. Dies ist die Variante, die wirklich kleinere Matrizen erzeugt – allerdings nur, wenn man die Schichten tatsächlich neu aufbaut. Das Nullen von Zeilen und das Belassen des Tensors in seiner ursprünglichen Form ändert nichts an der FLOP-Anzahl; Der Neuerstellungsschritt im Implementierungsabschnitt konvertiert die Maske in eine 512×n512 \times n Matrix.

Für Faltungsschichten ist das Analogon die Filterbereinigung. Gegeben WRCout×Cin×k×k\mathbf{W} \in \mathbb{R}^{C_{\text{out}} \times C_{\text{in}} \times k \times k}, die Bedeutung des Ausgabefilters ii Ist:

importance(i)=Wi,:,:,:F=c,h,wWi,c,h,w2\text{importance}(i) = \|\mathbf{W}_{i,:,:,:}\|_F = \sqrt{\sum_{c,h,w} W_{i,c,h,w}^2}

Filter entfernen ii eliminiert einen gesamten Ausgangskanal und reduziert die FLOPs proportional.

Die Lottoschein-Hypothese

Kleines gewinnendes Subnetzwerk, isoliert in einem riesigen neuronalen Gitter

Im Jahr 2019 führten Frankle und Carbin die Lottery Ticket Hypothesis (LTH) ein: Innerhalb eines zufällig initialisierten dichten Netzwerks gibt es ein spärliches Teilnetzwerk – ein „Gewinnlos“ – das, trainiert von seiner ursprünglichen Initialisierung, in einer vergleichbaren Anzahl von Iterationen mit der Genauigkeit des gesamten Netzwerks übereinstimmt.

Überlegen Sie formal f(x;θ0)f(\mathbf{x}; \boldsymbol{\theta}_0) initialisiert mit θ0Dθ\boldsymbol{\theta}_0 \sim \mathcal{D}_\theta. Nach dem Training zur Konvergenz erhalten wir θ\boldsymbol{\theta}^* und leiten Sie eine Schnittmaske ab m\mathbf{m}. Das LTH gibt an, dass es existiert m\mathbf{m} so dass:

f(x;mθ0)trainθmwhereacc(θm)acc(θ)f(\mathbf{x}; \mathbf{m} \odot \boldsymbol{\theta}_0) \xrightarrow{\text{train}} \boldsymbol{\theta}_m^* \quad \text{where} \quad \text{acc}(\boldsymbol{\theta}_m^*) \geq \text{acc}(\boldsymbol{\theta}^*)

mit m0θ0\|\mathbf{m}\|_0 \ll |\boldsymbol{\theta}_0|. Die ursprünglichen Experimente erfolgten mit MNIST und CIFAR-10, wobei die Gewinnscheine 10–20 % der Parameter behielten. Nichts davon lässt sich aufgrund der Annahme auf Orderbuchdaten übertragen – LOB-Merkmale sind instationär und die Bezeichnung ist nahezu verrauscht, was sich in genau den Punkten, die wichtig sein könnten, von der Bildklassifizierung unterscheidet.

Iterative Magnitude Pruning (IMP)

Das Ticket wird von IMP gefunden:

  1. Initialisieren Sie das Netzwerk mit θ0\boldsymbol{\theta}_0.
  2. Konvergenz trainieren, erhalten θ\boldsymbol{\theta}^*.
  3. Beschneiden Sie die p%p\% von Gewichten mit kleinster Größe, wodurch eine Maske entsteht m\mathbf{m}.
  4. Setzen Sie die verbleibenden Gewichte auf ihre Werte in zurück θ0\boldsymbol{\theta}_0 (zurückspulen).
  5. Wiederholen Sie ab Schritt 2 mit dem maskierten Netzwerk.

Jede Runde schneidet einen Teil ab pp (normalerweise 20 %), also danach nn Runden (1p)n(1-p)^n der Parameter überleben. Nach 10 Runden bei p=0.2p = 0.2verbleiben etwa 10,7 %.

Drei Hypothesen über Handelsmodelle, keine davon getestet

Es ist verlockend zu argumentieren, dass LTH bei Marktdaten besonders gut funktionieren sollte. Es tauchen drei solcher Argumente auf; Bei allen dreien handelt es sich um Hypothesen, und sie als Fakten darzustellen, ist der Fehlermodus, den dieser Blog vermeiden möchte.

  1. Finanzsignale sind spärlich. Der größte Teil eines Orderbuch-Snapshots besteht aus Rauschen, daher kann ein spärliches Subnetzwerk auf natürliche Weise mit einem spärlichen Signal übereinstimmen. Testbar: IMP mit einer Zufallsmaske gleicher Sparsity vergleichen; Wenn Sparsity selbst die Arbeit erledigt, sollte die Zufallsmaske nicht weit dahinter liegen.
  2. Gewinnlose Tickets verallgemeinern sich über alle Regime hinweg. Dies ist eine empirische Behauptung über Märkte ohne Quellenangabe und die interessanteste der drei. Es ist direkt anhand der Regimebezeichnungen von Regimeerkennung mit HMMs testbar: Finden Sie das Ticket in Regime A, trainieren Sie es in Regime B neu und vergleichen Sie es mit einem Ticket, das nativ in B gefunden wurde.
  3. Sparsity reguliert. Eine geringere effektive Kapazität kann die Anpassung an Mikrostrukturrauschen verringern – was sich darin zeigen würde, dass die Out-of-Sample-Lücke des bereinigten Modells kleiner als die des dichten Modells und nicht nur vergleichbar ist.

Implementierung: Beschneiden eines Trading-MLP

Neuronale Schichten werden durch eine präzise, spärliche Maske transformiert

Das Basismodell

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():,}")

Unstrukturierter Größenschnitt

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%}")

Strukturierter Schnitt, mit dem Umbau, der ihn Wirklichkeit werden lässt

Das Maskieren von Zeilen ist die halbe Arbeit. Die Hälfte, die die Beschleunigung erzeugt, baut jede Schicht in ihrer reduzierten Form wieder auf – was bedeutet, dass sich die Entfernung nach vorne ausbreitet: Zeile fallen lassen jj der Schicht ii löscht auch die Spalte jj der Schicht i+1i+1 und Kanal jj von irgendjemandem BatchNorm1d zwischen ihnen.

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

Bevor Sie dem vertrauen, sollten Sie zwei Dinge überprüfen, ganz im Sinne der Äquivalenz-Gates, die auch im Rest des Blogs ausgeführt werden:

  • Formen. rebuilt sollte versteckte Dimensionen anzeigen (1fraction)h(1 - \text{fraction}) \cdot h — 512 für fraction=0.75, h=2048h = 2048 – und eine Parameteranzahl, die quadratisch gesunken ist, da beide Dimensionen der inneren Matrizen schrumpfen.
  • Ausgänge. In eval() Modus, rebuilt(x) muss mit dem maskierten Modell übereinstimmen rebuilt-freie Ausgabe mit Gleitkommatoleranz für denselben Stapel. Ist dies nicht der Fall, ist die Spaltenweitergabe falsch und jede Downstream-Zahl misst ein anderes Modell als Sie denken.

Beim Zeilenüberlebenstest wird davon ausgegangen, dass eine maskierte Zeile genau Null ist, eine aktive Zeile jedoch nicht. Das gilt für ln_structured Ausgabe; Dies würde nicht gelten, wenn ein anderes Verfahren ein echtes lebendes Neuron mit nur Nullen erzeugen würde. Stellen Sie daher die Anzahl der Überlebenden anhand des angeforderten Bruchteils fest, anstatt dem Normtest blind zu vertrauen.

Iterative Größenbeschneidung (Lottoscheinsuche)

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 ist das Rohmaterial für die Sparsitäts-Genauigkeits-Kurve, die Ihnen dieser Artikel schuldet. eval_fn muss bei bereinigten Splits tatsächlich außerhalb der Stichprobe liegen – ein in der Stichprobe erzielter IMP-Lauf zeigt eine schöne Kurve, die nichts bedeutet.

Messen

Abstrakte Effizienzgrenze, die Sparsity-Genauigkeit und Latenz in Einklang bringt

Die Latenz wird mit der gleichen Konvention wie im Rest des Blogs gemessen – Aufwärmen ausgeschlossen, Best-of-N, p50/p95/p99 statt eines Mittelwerts – und dieses Protokoll mit dem Code befindet sich in Polars vs Pandas. Drei Besonderheiten beim Beschneiden:

  • Vergleichen Sie das neu erstellte Modell, nicht das maskierte. Ein maskiertes Modell mit Losgröße 1 misst die dichte Form.
  • Geben Sie die Chargengröße an. Batch 1 (Quoting-Schleife) und Batch 256 (Recherche-Sweep) befinden sich auf unterschiedlichen Seiten der speichergebundenen/rechengebundenen Linie, und das Beschneiden hilft ihnen unterschiedlich.
  • Berichten Sie über die Genauigkeit für denselben Split, denselben Horizont und die angegebene Label-Definition. Eine Latenztabelle ohne die Spalte „Matching Accuracy“ ist ein Argument dafür, das Modell vollständig zu löschen.

Fortgeschrittene Techniken

Erweiterte Netzwerkkomprimierungspfade vereinen sich zu einem effizienten Modell

Beschneiden mit Wissensdestillation

Anstatt isoliert zu beschneiden und zu verfeinern, nutzen Sie das ursprüngliche dichte Modell als Lehrmittel. Der beschnittene Schüler minimiert eine Kombination aus Aufgabenverlust und der KL-Abweichung von der Ausgabeverteilung des Lehrers:

L=(1α)Ltask(y,y^s)+αT2DKL(σ(ztT)σ(zsT))\mathcal{L} = (1 - \alpha) \cdot \mathcal{L}_{\text{task}}(\mathbf{y}, \hat{\mathbf{y}}_s) + \alpha \cdot T^2 \cdot D_{\text{KL}}\left(\sigma\left(\frac{\mathbf{z}_t}{T}\right) \| \sigma\left(\frac{\mathbf{z}_s}{T}\right)\right)

Wo zt\mathbf{z}_t Und zs\mathbf{z}_s sind Lehrer- und Schülerlogs, TT ist die Temperatur und α\alpha gleicht die Ziele aus. Der T2T^2 Faktor skaliert die Destillationsgradienten neu, die sonst schrumpfen würden 1/T21/T^2.

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

Bewegungsbeschneidung

Anstatt nach der absoluten Größe zu beschneiden, werden beim Bewegungsbeschneiden (Sanh et al., 2020) Gewichte beschnitten, die sich während des Trainings in Richtung Null bewegen. Der Wichtigkeitswert akkumuliert das Gradienten-Gewichts-Produkt:

Sij(t)=Sij(t1)+Wij(t)Wij(t1)sign(Wij(t)LWij(t))S_{ij}^{(t)} = S_{ij}^{(t-1)} + \left| W_{ij}^{(t)} - W_{ij}^{(t-1)} \right| \cdot \text{sign}\left(W_{ij}^{(t)} \cdot \frac{\partial \mathcal{L}}{\partial W_{ij}^{(t)}}\right)

Gewichtungen mit negativen Werten werden bereinigt. Das Argument dafür gegenüber der Magnitudenbeschneidung bezieht sich insbesondere auf die Feinabstimmung: Wenn Sie ein vorab trainiertes Modell anpassen, wurde die Magnitudenverteilung durch die vorab trainierte Aufgabe geformt, sodass die Magnitude ein veraltetes Wichtigkeitssignal und die Bewegungsrichtung ein frischeres Signal ist. Bei einem Handelsmodell, das auf rollierende Fenster umgeschult wird, ist dies die häufigere Situation als das Training von Grund auf.

NVIDIA 2:4 Strukturierte Sparsity

Ampere- und neuere NVIDIA-GPUs unterstützen 2:4 strukturierte Sparsity in der Hardware: Von jeweils 4 zusammenhängenden Gewichtungen müssen genau 2 Null sein.

i,W[4i:4i+4]0=2\forall i, \quad \|\mathbf{W}_{[4i:4i+4]}\|_0 = 2

Dies ist die einzige Form der feinkörnigen Sparsität, die von der Hardware tatsächlich belohnt wird, weshalb sie wichtiger ist als die 90 %-Nullen-Zahl aus unstrukturiertem Pruning. Die Einschränkung ist eher lokal als global – es ist egal, welche von jeweils vier überleben – es handelt sich also um eine viel schwächere Einschränkung als die Festlegung einer globalen Maske, obwohl 50 % die einzige angebotene Sparsity-Stufe ist.

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()

Um die Beschleunigung zu realisieren, muss der Inferenzpfad die spärlichen Tensorkerne verwenden – einen ONNX-Export plus TensorRT-Build, oder torch.sparse.to_sparse_semi_structured. Wenn Sie ein 2:4-maskiertes Modell über eine dichte Laufzeit exportieren, erhalten Sie den Genauigkeitsaufwand, aber keinen Nutzen.

Produktionsbereitstellung

Schlankes Inferenzmodell, das in einen Hardware-Conduit mit geringer Latenz gelangt

Validierung

Ein beschnittenes Modell ist ein neues Modell, kein komprimiertes altes, und es durchläuft dasselbe Akzeptanztor wie jeder andere Kandidat: rollierendes Neutraining und Out-of-Sample-Revalidierung gemäß Walk-Forward-Optimierung mit der Auswahleffektkorrektur aus der deflationierten Sharpe-Ratio. Diese Korrektur ist hier nicht optional – IMP generiert eine Sequenz von Kandidatenmodellen, daher wurde bei der Suche der Sparsity-Level ausgewählt, der über zehn Runden am besten aussieht, und sein Sharpe muss anhand der effektiven Anzahl von Versuchen deflationiert werden. Eine pauschale Regel wie „Ablehnen, wenn Sharpe um mehr als 5 % sinkt“ übersteht diese Arithmetik nicht, weshalb Sie in diesem Artikel keine finden werden.

Quantisierungsstapelung

Beschneiden komponiert mit Quantisierung. Ein zu 90 % dünn besetztes und auf INT8 quantisiertes Modell hat ein Komprimierungsverhältnis von:

CR=1(1s)boriginalbquantized=10.1328=40×\text{CR} = \frac{1}{(1 - s)} \cdot \frac{b_{\text{original}}}{b_{\text{quantized}}} = \frac{1}{0.1} \cdot \frac{32}{8} = 40\times

Aus einem 48-MB-Modell werden 1,2 MB. Das ist ein Lageranspruch und nichts weiter. Ob das 1,2-MB-Modell zu denselben Entscheidungen führt, ist eine separate Frage mit eigener Antwort, und die GPU-Präzisionsfalle ist der Grund, sie zu stellen: In diesem Blog wurde gezeigt, dass fp32 allein in einer Backtest-Berechnung, die völlig vernünftig aussah, einen relativen Fehler von 211 erzeugte. INT8 ist eine weitaus aggressivere Reduzierung. Versenden Sie ein quantisiertes und bereinigtes Modell nur hinter einem quantifizierten Paritätstor gegenüber dem fp32-dichten Modell – Entscheidungsübereinstimmungsrate und PnL-Delta für einen angehaltenen Zeitraum, keine Zusicherung.

Überwachung

Beschnittene Modelle können empfindlicher auf Verteilungsverschiebungen reagieren. Sehenswert:

  • Aktivierungssparsität: Wenn überlebende Neuronen überwiegend Nullen aussenden, ist das effektive Modell kleiner als beabsichtigt und wahrscheinlich degradierend.
  • Gradientennormen während der Umschulung: Explodierende Gradienten deuten darauf hin, dass vom überlebenden Teilnetzwerk verlangt wird, zu aggressiv das zu kompensieren, was entfernt wurde.
  • Vorhersage-Entropie: Ein beschnittenes Modell, das bei verrauschten Mikrostrukturdaten zu selbstsicher wird, passt wahrscheinlich zum Trainingsprogramm.

Fazit

Kompaktes, effizientes neuronales Netzwerk, das dichte Unsicherheit auflöst

Die Methoden sind gut etabliert und das ist alles, was dieser Artikel behauptet, bis der Sweep läuft. Durch unstrukturiertes Beschneiden erhalten Sie eine spärliche Zahl und keine Geschwindigkeit. Durch strukturiertes Beschneiden erhalten Sie Geschwindigkeit, wenn – und nur dann – Sie die Ebenen neu aufbauen, anstatt sie zu maskieren. Die Lotterieschein-Hypothese legt nahe, dass das kompakte Modell bereits innerhalb des überparametrisierten Modells existiert, obwohl dies anhand von Bild-Benchmarks und nicht anhand von Orderbuchdaten nachgewiesen wurde, und die drei oben genannten Gründe, warum es bei Marktdaten funktionieren „sollte“, sind Hypothesen mit angehängten Experimenten, keine Erkenntnisse.

Die praktische Heuristik aus der Literatur besteht darin, groß zu trainieren und zu beschneiden, anstatt von Anfang an klein zu entwerfen: Das große Modell untersucht die Verlustlandschaft effektiver, und durch das Beschneiden bleiben die Pfade erhalten, die wichtig sind. Ob dies für ein Handelsmodell gilt, bei welcher Sparsität und bei welchen Genauigkeitskosten, ist nur ein IMP-Durchlauf entfernt – und dieser Artikel sollte nach diesem Durchlauf noch einmal gelesen werden, mit Zahlen darin.

blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

Dem Markt einen Schritt voraus

Abonniere unseren Newsletter für exklusive KI-Trading-Einblicke, Marktanalysen und Plattform-Updates.

Wir respektieren deine Privatsphäre. Jederzeit abbestellbar.