← Insights

EN — English version

#11 · DeepSeek-V4-Flash auf 8x RTX 3090: eine Million Tokens, aber kein Tensor Parallelism

· Filip

Ein Modell kann auf acht GPUs laufen. Diese Aussage kann allerdings zwei sehr verschiedene Dinge bedeuten.

Im Idealfall bearbeiten alle acht Karten gemeinsam jeden Layer. Sie teilen die Arbeit innerhalb des Layers auf und bündeln ihre Speicherbandbreite; die Inferenz wird dadurch erheblich schneller. Im anderen Fall ist das Modell schlicht zu groß für eine einzelne GPU: Karte 0 berechnet ihre Layer und reicht den Hidden State an Karte 1 weiter, dann folgen Karte 2 bis Karte 7. Alle tragen ihren Teil bei, aber weitgehend nacheinander.

DeepSeek-V4-Flash läuft auf unserem 8×3090-Rig derzeit nach dem zweiten Muster. Genau deshalb gehört dieser Versuch zu den seltsameren Ergebnissen unserer lokalen Inferenzserie.

Wir haben einen 150,75 GiB großen Q8-GGUF auf 192 GB Consumer-VRAM geladen und den Server nacheinander für 512k, 768k und schließlich das vollständige Kontextfenster mit 1.048.576 Tokens konfiguriert. Alle drei Konfigurationen starteten erfolgreich. Der 1M-Server verarbeitete einen realen Prompt mit 120k Tokens ohne Out-of-Memory-Fehler. In einem separaten Lauf mit 500k aktivem Kontext erzeugte das Modell alle angeforderten 256 Ausgabetokens.

Das ist der erfreuliche Teil des Befunds:

Ein 150,75 GiB großer DeepSeek-V4-Flash-Q8 lässt sich auf acht Karten der RTX-3090-Klasse tatsächlich mit einem Kontextfenster von einer Million Tokens bereitstellen.

In den Speicher zu passen ist allerdings noch keine Parallelisierung. Gewichte und KV-Cache verteilen sich auf acht Karten, deshalb reicht die Gesamtkapazität. Der Durchsatz bleibt hinter dem zurück, was man von acht GPUs erwarten würde, weil für DeepSeek4 in diesem Aufbau weder in llama.cpp noch im regulären vLLM ein sauberer, validierter Tensor-Parallel-Pfad verfügbar ist.

Wir hatten damit unser etwas absurdes Kapazitätsziel erreicht, während jedes Token das Rig praktisch Etappe für Etappe durchlief. Erst ein spezialisierter Fork und MTP machten diesen seriellen Pfad deutlich weniger mühsam.

Die anschließende Integration als OpenCode-Werkzeugmodell führte uns zusätzlich auf einen unerwarteten Nebenschauplatz. Unsere Laguna-Coding-Agenten scheiterten wiederholt an den literalen <think>-Strings im Chat-Template und im Output-Parser. Die Fehlersuche legte sowohl einen Modelldefekt in Laguna als auch einen separaten Fehler im Reasoning-Parser von vLLM frei, durch den generierter Inhalt unbemerkt verworfen werden kann. Keiner von beiden ist ein Inferenzfehler von DeepSeek4. Wir fanden sie nur, weil die DeepSeek-Integration strukturelle Tokens direkt in den zu prüfenden Code brachte. Diese Untersuchung setzen wir in Beitrag #12: Die sechs Strings, die Laguna nicht schreiben kann fort.

Der Aufbau

Wir verwendeten dasselbe lokale LLM-Rig wie in den übrigen Artikeln der Serie:

  • 8x GPUs der RTX-3090-Klasse
  • 192 GB VRAM insgesamt
  • Ampere, SM86
  • kein NVLink
  • vier getrennte PCIe-Root-Complexes
  • CUDA-P2P bewusst deaktiviert

Als Modell diente:

  • DeepSeek-V4-Flash
  • der Unsloth-GGUF UD-Q8_K_XL
  • fünf Shards
  • 150,75 GiB Gesamtgröße

Wir entschieden uns bewusst für das große Q8-Ziel und gegen einen IQ2- oder Q2-Build. Eine stärker quantisierte Expertenvariante kann schneller sein und auf weniger Karten passen. Unser Versuch sollte eine andere Frage beantworten: Wie weit kommen acht 3090 mit dem großen Q8, wenn wir zugleich das enorme Kontextfenster behalten wollen?

Bei der Kapazität lautete die Antwort: weiter als erwartet. Beim reinen parallelen Durchsatz fiel sie nüchterner aus.

Der erste Erfolg: Das Modell lud

Die Konfiguration im regulären llama.cpp war überschaubar:

split mode:        layer
flash attention:   on
GPU layers:        all
KV cache:          q8_0 / q8_0
parallel slots:    1
batch:             1024
microbatch:        128

Mit 512k konfiguriertem Kontext lud das vollständige Modell sauber auf alle acht Karten. Bei kurzem Kontext erreichte der Decode:

  • 31,6 tok/s

Bei ungefähr 100k aktivem Kontext blieben davon:

  • 23,9 tok/s

Spektakulär waren diese Zahlen nicht. Sie waren jedoch stabil und in sich schlüssig. Der Quant war nicht beschädigt, das Kontextfenster war real und der Server funktionierte. Also erhöhten wir die Reservierung weiter.

Konfigurierter Serverkontext Geladen? Freier VRAM auf der knappsten GPU
524.288 ja ungefähr 1,58 GiB
786.432 ja ungefähr 1,18 GiB
1.048.576 ja ungefähr 0,77 GiB

Beim 1M-Server blieb es nicht bei einem bestandenen Healthcheck. An die 768k- und die 1M-Konfiguration schickten wir dieselbe Last mit 120.021 Tokens:

Serverkontext Aktiver Prompt Prefill Decode Gesamtdauer
786.432 120.021 301,52 tok/s 22,38 tok/s 409,7 s
1.048.576 120.021 290,68 tok/s 22,27 tok/s 424,6 s

Aus diesem Vergleich folgen zwei getrennte Aussagen.

Bei 120k aktivem Kontext verursachte die Konfiguration für 1M keinen nennenswerten Decode-Verlust: 22,27 statt 22,38 tok/s auf dem 768k-Server. Der Prefill sank von 301,52 auf 290,68 tok/s, die größere Reservierung ist also nicht kostenlos. Für den Decode zählt der tatsächlich aktive Kontext trotzdem erheblich stärker als der Wert in --ctx-size.

Zugleich besaß der Q8 genug VRAM-Reserve, um mit der vollständigen 1M-Allokation betriebsfähig zu bleiben. Wir mussten weder auf Q4-KV noch auf CPU-Offload oder einen kleineren Zielquant ausweichen. Das war der klarste Kapazitätserfolg des gesamten Versuchs.

Eine Million konfiguriert bedeutet nicht eine Million verstanden

Wie schon bei unserem Laguna-1M-Test trennen wir die technische Allokation von der inhaltlichen Leistung. Nachgewiesen haben wir Folgendes:

1M server context allocatable:                  yes
1M-configured server processes a 120k prompt:  yes
500k-active prompt processable:                yes
server remains healthy and returns 256 tokens: yes

Nicht nachgewiesen sind diese Punkte:

full 1M active prompt processed end to end:    not tested
1M needle retrieval:                           not tested
1M multi-hop repo reasoning:                   not tested
same semantic quality at 1M as at 262k:        unknown

Der Befund ist folglich keine weitere 1M-Kontextbehauptung, die sich allein auf einen einmal grün gewordenen Healthcheck stützt. Er ist dennoch wesentlich: Q8-Modell, KV-Allokation, Compute-Buffer und vollständiger Serving-Stack passten gemeinsam auf acht Karten mit je 24 GB, während der Server auf das gesamte trainierte Kontextfenster eingestellt war. Ohne diese Voraussetzung kann kein anspruchsvollerer 1M-Test überhaupt beginnen.

Der 500k-Lauf: Hier wird serieller Decode schmerzhaft

Unser anspruchsvollster abgeschlossener Lauf mit aktivem Langkontext verwendete:

  • 500.021 aktive Tokens
  • 256 erzeugte Ausgabetokens
  • Wiederverwendung des warmen Präfixes bei cache_n=500017

Der Decode erreichte:

  • 12,20 tok/s

Über die unterbrochene und später fortgesetzte Sequenz summierte sich der Prefill auf ungefähr 78 Minuten. Schönreden lässt sich das nicht.

Entscheidend ist die Art der Grenze. Weder ging dem Modell der VRAM aus, noch stürzte es ab. Der Server blieb funktionsfähig und lieferte alle 256 angeforderten Tokens. Wir haben diese Ausgabe nicht auf Retrieval oder semantische Qualität hin geprüft. Der Lauf belegt daher die betriebliche Funktionsfähigkeit bei 500k, keine verlässliche inhaltliche Leistung bei 500k. Gemessen haben wir vor allem die Rechen- und Attention-Kosten eines enormen aktiven Kontexts.

Die Kurve des Mainline-Pfads sah so aus:

Aktiver Kontext Decode-Rate
kurz 31,6 tok/s
ungefähr 100k 23,9 tok/s
120k auf dem 1M-Server 22,27 tok/s
ungefähr 500k 12,20 tok/s

Hier zeigt sich der Preis des langen Kontexts in nahezu reiner Form. Jedes neue Token arbeitet gegen eine immer größere aktive Historie. Die derzeitige Implementierung macht aus den acht Karten dabei keinen gemeinsamen Bandbreiten-Pool. Die betriebliche Bedeutung dieser Unterscheidung haben wir bereits bei MiniMax unter anhaltender Agentenlast gesehen; bei DeepSeek4 lässt sich zusätzlich sehr genau zeigen, woher sie im Multi-GPU-Pfad stammt.

Warum acht GPUs trotzdem weitgehend seriell arbeiten

„Seriell“ bedeutet nicht, dass sieben GPUs unbenutzt bleiben. Alle acht Karten halten Gewichte und führen die ihnen zugewiesene Arbeit aus. In unserem getesteten Aufbau liefern sie gemeinsam die Kapazität für Modell, Compute-Buffer und großen Kontext. Das Problem liegt im Zeitpunkt, zu dem sie rechnen.

Layer-Split: zuerst die Kapazität

Beim Layer-Split werden vollständige Transformer-Layer einzelnen GPUs zugewiesen; alternativ erhält jede Karte eine Gruppe von Layern. Der vereinfachte Weg eines erzeugten Tokens sieht damit so aus:

token state
  -> GPU 0 computes its layers
  -> GPU 1 computes its layers
  -> GPU 2 computes its layers
  -> GPU 3 computes its layers
  -> GPU 4 computes its layers
  -> GPU 5 computes its layers
  -> GPU 6 computes its layers
  -> GPU 7 computes its layers
  -> final logits

Jeder Layer benötigt die Ausgabe seines Vorgängers, und das nächste Token hängt vom Ergebnis des aktuellen Tokens ab. Bei autoregressivem Batch-One-Decode wird diese Abhängigkeitskette zum bestimmenden Faktor.

Der Layer-Split liefert uns:

  • die addierte VRAM-Kapazität,
  • kleine Hidden-State-Transfers zwischen den Etappen,
  • vergleichsweise wenig kollektive Kommunikation,
  • gute Verträglichkeit mit einer schwachen Consumer-PCIe-Topologie.

Er stellt jedoch nicht bei jedem Layer die addierte Speicherbandbreite aller acht GPUs gleichzeitig bereit. Wenn GPU 3 die Gewichte eines Layers liest, können GPU 0, 1, 2, 4, 5, 6 und 7 den nächsten abhängigen Layer noch nicht unabhängig berechnen. Sie warten auf den Hidden State. Deshalb kann ein Modell mit 150 GiB komfortabel in den Speicher passen und trotzdem nur 31 tok/s decodieren.

Tensor Parallelism: zuerst die Bandbreite

Echtes Tensor Parallelism teilt die Arbeit innerhalb eines Layers auf mehrere GPUs auf:

one layer
  -> GPU 0 computes shard 0
  -> GPU 1 computes shard 1
  -> GPU 2 computes shard 2
  -> ... simultaneously
  -> collective reduction combines the result
  -> next layer

Mehrere Karten lesen nun gleichzeitig unterschiedliche Teile desselben Layers. Erst dadurch kann ein einzelnes Token die addierte VRAM-Bandbreite nutzen.

Der Preis dafür ist Kommunikation. Jeder aufgeteilte Layer braucht Reduktionen oder andere Collectives. Auf A100- oder H100-Systemen mit NVLink beziehungsweise NVSwitch kann dieser Aufwand vertretbar sein. Bei acht GeForce-Karten über vier PCIe-Root-Complexes kann er zum eigentlichen Problem werden.

Trotzdem wäre ein korrekter TP-Pfad der naheliegende Weg, um aus acht Karten, die Kapazität liefern, acht Karten zu machen, die Single-Stream-Geschwindigkeit liefern. Eine weitere Option wäre ein korrekter Graph-Splitter, der ausgewählte Operationen über kleinere GPU-Gruppen parallelisiert. Für DeepSeek4 steht uns heute in einem verantwortbar einsetzbaren Stack keiner dieser Wege zur Verfügung.

llama.cpp: Die TP-Flags gibt es, den DeepSeek4-Pfad noch nicht

Hier wartete die erste Falle. llama.cpp kennt mehrere Split-Modi. Die bloße Existenz eines Flags bedeutet allerdings nicht, dass jede Architektur es verwenden kann.

--split-mode tensor

Der aktuelle Mainline-Pfad lehnt den Tensor-Modus für DeepSeek4 ab. Das lässt sich weder mit einer anderen VRAM-Verteilung noch mit einem besseren Tensor-Split-Verhältnis beheben. Im produktiven Pfad fehlen Graph und Kernel, die DeepSeek4 korrekt aufteilen müssten.

--split-mode row

Auch der historische Row-Split bot keinen Ausweg. Das CUDA-Backend meldete, dass das Gerät in diesem Pfad die erforderlichen Split-Buffer nicht unterstützt.

ik_llama, Graph-Modus und --max-gpu

Der interessanteste theoretische Kompromiss für Consumer-PCIe steckt in ik_llama.cpp:

  • Graph-Split über das vollständige Modell,
  • Begrenzung jeder Operation auf eine kleinere GPU-Gruppe mit --max-gpu,
  • Verteilung späterer Graphbereiche auf die übrigen Karten.

Konzeptionell könnte so innerhalb eines Bereichs eine TP4-ähnliche Verarbeitung entstehen, während die Bereiche über acht GPUs eher wie Pipeline Parallelism verteilt werden. Genau diese Form hätten wir gern getestet.

Unser älteres lokales ik_llama-Image erkannte die Architektur noch nicht:

unknown model architecture: 'deepseek4'

Neuere Stände kennen DeepSeek4, doch der Graph-Splitter fällt für diese Architektur weiterhin auf Layer-Split zurück. Ab diesem Fallback steuert --max-gpu keinen echten Graph-Split mehr und verliert für unser Single-Stream-Problem seine Wirkung. Das interessante Flag existiert; der dafür benötigte DeepSeek4-Pfad ist noch nicht fertig.

Der experimentelle Tensor-Split-PR

Für DeepSeek4 gibt es einen experimentellen Tensor-Split-Proof-of-Concept in llama.cpp. Auf einem ähnlichen Q8-Aufbau mit acht 3090 wurden ungefähr folgende Werte gemeldet:

  • 35 bis 37 tok/s bei kurzem Kontext
  • ungefähr 32 tok/s bei rund 40k Kontext

Die Zahlen sind interessant, produktionsreif ist dieser Stand aber nicht. Im geprüften Pfad werden Tensoren falsch zugewiesen, KV-Zustände dupliziert und die Qualitätsvalidierung reicht nicht aus, um einem 150-GiB-Reasoning-Modell zu vertrauen. Gegenüber unserem späteren Q8+MTP-Ergebnis im Layer-Split ist der Vorsprung zudem zu klein, um dieses Korrektheitsrisiko zu rechtfertigen. Ein experimenteller Graph mit einer plausiblen Tokenrate ergibt eben noch keine korrekte TP-Runtime.

vLLM: schnell, wenn Hardware und Kernel zueinander passen

Nach den Grenzen des GGUF-Pfads lag die Frage nahe, ob wir den Kampf mit llama.cpp beenden und zu vLLM wechseln sollten. Für DeepSeek-V4-Flash auf Ampere ist der reguläre vLLM-Stack jedoch nicht die Abkürzung, nach der er klingt.

Die offiziellen Modellpfade konzentrieren sich auf FP4- und FP8-Formate sowie spezialisierte Attention- und MoE-Kernel für neuere Datacenter-Architekturen. Auf SM86 ist das Ergebnis nicht bloß etwas langsamer: Die erwarteten Kernel und Zahlenformate fehlen häufig vollständig. Für acht RTX 3090 bietet stock vLLM derzeit keinen überzeugenden, sauberen DeepSeek-V4-Flash-TP8-Pfad.

Mit spezialisierten Forks wird das Bild differenzierter:

Hardware Runtime-Pfad Gemeldetes Ergebnis
8x RTX 3080 20 GB Lasimeri-Ampere-vLLM-Fork, starker CPU-Offload 4,3 bis 4,4 tok/s
8x A800 80 GB gepatchter vLLM-Prototyp 40,6 tok/s
8x A100 80 GB gepatchtes vLLM 85,5 tok/s
8x A100 80 GB gepatchtes vLLM plus DSpark 170 tok/s

Diese Berichte zeigen, dass DeepSeek4 schnell werden kann, wenn Runtime, Quant, Kernel, Speicherkapazität und Interconnect gemeinsam entworfen sind. Sie belegen keine saubere Übertragbarkeit desselben Pfads auf acht SM86-Karten mit 24 GB und ohne P2P. A100- und A800-Systeme sind keine bloß größeren 3090; unterstützte Datentypen, Speicherkapazität und Interconnect verändern die gesamte Rechnung.

Warum Expert Parallelism uns nicht rettete

Als Mixture-of-Experts-Modell legt DeepSeek Expert Parallelism zunächst nahe: Verschiedene Experten liegen auf verschiedenen GPUs, und jedes Token wird nur zu den Karten mit den ausgewählten Experten geschickt.

Damit entsteht allerdings viel All-to-all-Kommunikation. Tokens und Expertenergebnisse müssen häufig in jedem MoE-Layer zwischen den Ranks wechseln. Auf einem schnellen Datacenter-Fabric kann sich das lohnen. Über vier Consumer-PCIe-Root-Complexes ohne nutzbares P2P ist dieser Tausch riskant.

Ein öffentliches DeepSeek4-Experiment machte diesen Effekt sichtbar:

  • Der Prefill wurde schneller.
  • Der Decode sank von 36,34 auf 23,82 tok/s.

Eine EP-to-Layer-Variante kann das Decode-Verhalten wieder verbessern, setzt jedoch erneut einen funktionierenden schnellen Kommunikationspfad voraus und hilft vor allem dem Prefill. Unsere eigentliche Engstelle war Single-Stream-Decode bei großem aktivem Kontext. Eine Optimierung, die den Prefill beschleunigt und den Decode zugleich um 34 Prozent verlangsamt, löst dieses Problem nicht.

AppMana: der ehrgeizigste Pfad für Consumer-GPUs

AppMana ist das stärkste Gegenbeispiel zur pauschalen Behauptung, Consumer-Karten könnten DeepSeek4 nicht sinnvoll betreiben. Der angepasste Pfad kombiniert:

  • eigene INT4-/INT8-Quantisierung,
  • Pipeline Parallelism,
  • Sparse-MLA-Kernel,
  • spekulativen Decode mit DSpark,
  • eigene Arbeiten an NCCL und Runtime.

Der veröffentlichte Consumer-Aufbau verwendete elf oder zwölf Karten der RTX-30-Klasse mit je 24 GB. Einschließlich DSpark umfasst der Quant ungefähr 170,06 GiB. Gemeldet wurden:

  • 33,3 tok/s ohne DSpark
  • 56,1 tok/s mit DSpark

Das ist ein echter Fortschritt. Für unsere aktuelle TP-Frage bleibt das Ergebnis jedoch nur begrenzt übertragbar: AppMana nutzt mehr Karten sowie eine andere Kombination aus Quant und Runtime in einem stark angepassten Pipeline-Aufbau. Mit zehn lokalen 3090 wäre ein PP10-Versuch rechnerisch denkbar. Einen validierten PP10-Lauf zum Nachbauen gibt es nicht, und der betriebliche Aufwand liegt deutlich über dem Start eines GGUF-Servers.

Wir beobachten diesen Ansatz weiter. Als direkt nutzbarer TP8-Pfad für unseren Q8 scheidet er derzeit aus.

SGLang, KTransformers, TensorSharp und der Rest des Feldes

Unsere Suche endete nicht bei llama.cpp und vLLM.

SGLang plus KTransformers

Ein funktionierender Ampere-Aufbau aus SGLang und KTransformers nutzte:

  • 2x RTX A6000 48 GB
  • zwei Xeon Gold 6530
  • 768 GB DDR5
  • CPU-Offload der Experten per AMXINT4

Gemeldet wurden 30 tok/s ohne MTP und 36 tok/s mit MTP.

Technisch ist das beachtlich. Der Aufbau liegt trotzdem fast genau auf unserem Kurzkontext-Durchsatz, benötigt ein gewaltiges CPU- und RAM-Subsystem, verwendet eine andere Gewichtskonvertierung und liefert keinen vergleichbaren Messpunkt bei 100k. Für unsere Maschine wäre er kein Upgrade.

TensorSharp

TensorSharp bleibt die Alternative, die wir noch praktisch testen würden. Die Engine liest den Unsloth-DeepSeek-V4-GGUF direkt; für vier A40 mit Q8 und DSpark liegt ein verifiziertes Ergebnis vor:

  • 26,4 tok/s als Baseline
  • 37,1 tok/s mit DSpark
  • 69 Prozent spekulative Akzeptanz

Dabei gelten vier Einschränkungen:

  • Der veröffentlichte Prompt lag bei ungefähr 15k, nicht bei 100k.
  • Die Gewichte werden weiterhin layerweise platziert; echtes TP ist das nicht.
  • Der Lauf braucht einen separaten DSpark-Support-GGUF mit ungefähr 6 GB.
  • Ein relevanter Vorteil gegenüber unseren 32,39 tok/s bei 100k wurde noch nicht gezeigt.

TensorSharp ist damit ein sinnvoller späterer Versuch, aber kein Beleg für ein bereits gelöstes TP-Problem.

Transformers

Transformers erkennt die Architektur. Da die FP4-/FP8-Pfade auf SM86 nicht unterstützt werden, wachsen die Gewichte jedoch auf 567 GiB in BF16 an. Das ist keine Serving-Konfiguration, sondern eine Bestellung für eine andere Maschine.

TensorRT-LLM, LMDeploy, stock SGLang und ExLlamaV3

Die Kurzfassung:

Engine DeepSeek4-Situation auf unserem Rig
TensorRT-LLM relevanter Pfad auf Blackwell ausgerichtet
LMDeploy DeepSeek4-Beitrag auf Hopper ausgerichtet
stock SGLang durch FP8-, FlashMLA- und MoE-Ampere-Pfade blockiert
ExLlamaV3-Fork experimentell, separater Quant, keine belastbaren Leistungsdaten

Wir haben also keine heimlich ausgereifte Engine übersehen. Für DeepSeek4 mit Tensor Parallelism auf acht Consumer-Ampere-Karten ist das Ökosystem schlicht noch nicht so weit.

Die Wende: Ein Fork optimiert den seriellen Pfad

An dieser Stelle hätte der Artikel mit dem Befund enden können, dass das Modell passt, aber seriell und langsam läuft. Das wäre richtig gewesen und hätte zugleich das beste Ergebnis des Versuchs unterschlagen.

Der Branch ds4-longctx von alesha-pro/llama.cpp setzt an einer anderen Stelle an. Er behauptet nicht, bereits einen korrekten TP-Pfad zu besitzen, sondern beschleunigt den vorhandenen Layer-Pfad.

Der Fork enthält:

  • Decode-Graphen mit konstanter Form,
  • Sparse Top-k Flash Attention,
  • Union-Listen für Sparse-Attention-Arbeit,
  • Causal Skipping im Lightning Indexer,
  • fusionierte und resident gehaltene MoE-Pfade,
  • GLU-Fusion,
  • einen fusionierten Decode-Indexer,
  • Radix-Select Top-k für den Decode,
  • Batched Radix Top-k für Langkontext-Prefill,
  • spekulativen K=1-MTP-Decode innerhalb des Graphen.

Für langen Kontext ist vor allem der Sparse-Attention-Pfad wichtig. Der reguläre DeepSeek4-Code kann Top-k als Maske anwenden und trotzdem einen erheblichen Teil der dichten Attention-Arbeit über den aktiven Kontext ausführen. Der spezialisierte Fork sammelt für die Sparse-Chunks nur jene KV-Positionen ein, die der Indexer tatsächlich ausgewählt hat.

Der Aufbau bleibt ein Layer-Split, doch jede serielle Etappe verrichtet weniger nutzlose Arbeit. Außerdem konnten wir den Pfad mit unserem vorhandenen Q8 prüfen, ohne einen experimentellen TP-Quant dazwischenzuschieben.

Der passende MTP-Sidecar

Das Zielmodell blieb unverändert:

  • Unsloth UD-Q8_K_XL
  • 150,75 GiB

Hinzu kam der passende K=1-MTP-Sidecar für die ursprüngliche Flash-Version:

DeepSeek-V4-Flash-MTP-Q4K-Q8_0-F32.gguf
size: 3.545 GiB
SHA-256: afd481ee689dce9037f70f39085fcdae5a5b096d521cdad43b19fa52bf8f4083

Der MTP-Layer schlägt jeweils ein Kandidatentoken vor. Das vollständige Q8-Zielmodell prüft diesen Vorschlag. Akzeptierte Kandidaten sparen Arbeit, verworfene gelangen nicht in die Ausgabe. Die kleinere Draft-Komponente hat damit nie das letzte Wort und kann die Antwortverteilung nicht ungeprüft verändern.

Die optimierten Ergebnisse

Unser Messprotokoll war bewusst unspektakulär:

  • ein Warmup,
  • drei gemessene Requests,
  • jeweils 256 erzwungene Ausgabetokens,
  • Temperatur null,
  • dasselbe Q8-Ziel,
  • ein kurzer Prompt und ungefähr 100k aktiver Kontext.

Die Ergebnisse:

Kontext Mainline-Layer-Split optimierter Q8+MTP-Pfad Gewinn
kurz 31,6 tok/s 35,98 tok/s +13,9 %
ungefähr 100k 23,9 tok/s 32,39 tok/s +35,5 %

Die einzelnen optimierten Läufe:

Kontext Warmup Gemessene Läufe Mittelwert MTP-Akzeptanz
kurz 35,84 35,97 / 35,99 / 35,98 35,98 78,87 %
100.021 32,80 32,53 / 32,92 / 31,71 32,39 89,77 %

Ein frischer 100k-Prefill erreichte:

  • 409,59 tok/s

Im normalen Chat-Smoke-Test beantwortete das Modell 17 * 23 korrekt mit 391. Die Serverlogs bestätigten, dass alle vorgesehenen Optimierungspfade aktiv waren.

Damit änderte sich unsere praktische Bewertung. Der Gewinn bei kurzem Kontext ist willkommen; ausschlaggebend sind die 100k. Ein Anstieg von 23,9 auf 32,39 tok/s verwandelt Layer-Split nicht nachträglich in TP. Er nimmt DeepSeek jedoch einen erheblichen Teil seiner Zähigkeit, und zwar genau in dem Kontextbereich, in dem das Modell für Agentenarbeit interessant wird.

Der Preis: FP16-Cache und kaum Reserve

Der optimierte Pfad hat eine wesentliche Einschränkung. Der Fork erzwingt für den komprimierten beziehungsweise Indexer-Cache von DeepSeek4 FP16 und ignoriert dort die angeforderte Q8-KV-Einstellung. Nach dem 100k-Test blieben auf der knappsten GPU nur:

  • 157 MiB frei

Mit 157 MiB Reserve arbeitet diese Konfiguration ohne jeden Puffer. Die gemessene 100k-Last lief stabil. Für größere aktive Kontexte braucht der optimierte MTP-Fork einen besseren Layer-Split, kleinere Batch- und Microbatch-Werte oder eine weitere Cache-Verbesserung, bevor wir ihn als sicher einstufen können.

Damit erklären sich auch die beiden Maximalergebnisse des Artikels: Sie stammen aus unterschiedlichen Konfigurationen.

  • Mainline-Q8: sehr große Kontextkapazität bis zum Start eines 1M-Servers
  • optimierter Q8+MTP-Fork: wesentlich besserer Durchsatz bei 100k, aber höherer Druck durch den FP16-Cache

Ehrlicherweise vereint keine einzelne Konfiguration sämtliche Höchstwerte dieses Artikels. Genau dafür sind Benchmarks da: Sie sollen Zielkonflikte offenlegen, statt sie in einer Marketingzeile zu verstecken.

Was sich aus den Messungen ableiten lässt

Die pauschale Aussage „DeepSeek-V4-Flash ist auf RTX 3090 langsam“ würde den Befund verfehlen. Präziser ist: Für einen einzelnen Decode-Stream fehlt DeepSeek-V4-Flash derzeit ein sauberer und validierter Pfad, der acht RTX 3090 parallel auslastet.

Der Q8-Quant funktioniert. Er passt in den Speicher, erzeugt Ausgaben und erlaubt außergewöhnliche Kontextreservierungen. Der optimierte Pfad bestand außerdem einen einfachen arithmetischen Kohärenztest. Sparse Attention und MTP erhöhen den Durchsatz deutlich. Die fehlende Komponente ist die Geometrie der Runtime.

Derzeit stehen mehrere unvollkommene Formen zur Wahl:

  • Layer-Split: korrekt, einfach und mit großer Kapazität, aber serieller Decode
  • experimenteller Tensor-Split: stärker parallelisiert, jedoch mit Korrektheits- und KV-Platzierungsproblemen
  • Expert Parallelism: kommunikationsintensiv und im veröffentlichten Versuch beim Decode langsamer
  • angepasste vLLM- und AppMana-Forks: vielversprechend, aber mit anderen Quants, mehr Hardware und ohne sauberen TP8-Pfad für dieses Rig
  • CPU-Offload der Experten: auf anderen Systemen funktionsfähig, benötigt aber ein sehr großes CPU- und RAM-Subsystem

Keine Variante von „einfach --tensor-parallel-size 8 setzen und fertig“ ersetzt derzeit den fehlenden Pfad.

Was an diesem Versuch trotzdem bemerkenswert ist

Der fehlende Tensor-Parallel-Pfad darf den Kapazitätsbefund nicht verdecken. Wir haben einen 150,75 GiB großen DeepSeek-V4-Flash-Q8 über acht Ampere-Karten mit je 24 GB verteilt und erfolgreich mit folgenden Kontextgrößen gestartet:

  • 512k konfigurierter Kontext
  • 768k konfigurierter Kontext
  • 1.048.576 Tokens konfigurierter Kontext

Der 1M-Server blieb betriebsfähig und verarbeitete eine reale Last mit 120k Tokens. In einem separaten Lauf bewältigte das Modell 500k aktiven Kontext, erzeugte alle 256 Ausgabetokens und erreichte dabei 12,20 tok/s.

Wir brauchten keinen CPU-Offload für die Gewichte, keinen Rückzug auf einen winzigen Expertenquant und keinen Kontextwert, der beim ersten echten Prompt sofort im OOM endet. Für Consumer-Ampere ist das ein starkes Kapazitätsergebnis.

Anschließend behielten wir dasselbe Q8-Ziel bei, ergänzten den passenden MTP-Head sowie den spezialisierten DeepSeek4-Fork und steigerten den Decode bei 100k von 23,9 auf 32,39 tok/s. Die acht GPUs reichen ein Token weiterhin etappenweise durch das Modell. Dabei tragen sie allerdings ein sehr großes Modell durch einen sehr großen aktiven Kontext, und die gemessenen Lasten laufen stabil zu Ende.

Fazit: Das Modell passt früher als die Parallelisierung

DeepSeek-V4-Flash auf acht 3090 zeigt ungewöhnlich deutlich den Unterschied zwischen Speicherskalierung und Rechenskalierung. Acht GPUs lösten unser Speicherproblem. Den Decode beschleunigten sie nicht automatisch im selben Verhältnis.

Der Layer-Split machte aus 192 GB getrenntem VRAM genug Kapazität für einen 150,75-GiB-Q8 und die Allokation eines Kontextfensters mit einer Million Tokens. Jedes autoregressive Token durchläuft die Layergruppen trotzdem in ihrer Abhängigkeitsreihenfolge. Ohne korrekten Tensor- oder Graph-Split innerhalb der Layer kann der größte Teil der addierten Bandbreite nicht gleichzeitig am selben Layer arbeiten.

Der spezialisierte ds4-longctx-Fork zeigte, wie viel Spielraum selbst im seriellen Pfad noch steckt. Sparse Top-k Attention, fusionierte MoE- und Indexer-Arbeit, konstante Graphen, Radix Top-k und K=1-MTP brachten bei 100k 35,5 Prozent mehr Durchsatz, ohne das Q8-Ziel auszutauschen. Diese Konfiguration würden wir heute für unsere gemessene 100k-Last einsetzen.

Das eigentliche Ziel bleibt ein korrekter Tensor- oder Graph-Parallel-Pfad für DeepSeek4, der alle acht 3090 gleichzeitig nutzt, ohne beschädigte Transfers, duplizierten KV-Zustand oder eine Kette nicht unterstützter Ampere-Kernel. Wir beobachten deshalb die Entwicklungen in llama.cpp, ik_llama, vLLM, SGLang, TensorSharp, AppMana und den kleineren Ampere-Forks.

Sobald ein belastbarer TP- oder Graph-Pfad für diese Hardwareklasse verfügbar ist, testen wir ihn mit demselben Q8, demselben Kurz-/100k-Protokoll und derselben sauberen Trennung zwischen konfiguriertem und tatsächlich geprüftem Langkontext. Das Modell passt bereits. Jetzt muss die Software lernen, alle acht GPUs wirklich gemeinsam rechnen zu lassen.

Nerd-Anhang: der exakte optimierte Pfad

Der validierte optimierte Build:

fork:   alesha-pro/llama.cpp
branch: ds4-longctx
commit: 393c388a
image:  llama-ds4-longctx:393c388a-sm86-runtime

Zielmodell:

/bigData/llama-models/unsloth/DeepSeek-V4-Flash-GGUF/UD-Q8_K_XL/
DeepSeek-V4-Flash-UD-Q8_K_XL-00001-of-00005.gguf

MTP-Sidecar:

/bigData/llama-models/unsloth/DeepSeek-V4-Flash-GGUF/
DeepSeek-V4-Flash-MTP-Q4K-Q8_0-F32.gguf

Die Q8-kompatible Optimierungsumgebung:

DSV4_CONSTANT_SHAPE=1
DSV4_SPARSE_FA=1
DSV4_FA_UNION=1
DSV4_IDX_SKIP=1
DSV4_MOE_TILE=1
DSV4_MOE_RESIDENT=1
DSV4_GLU_FUSE=1
DSV4_MOE_FUSE=1
DSV4_DECODE_FUSED_IDX=1
DSV4_DECODE_RADIX_TOPK=1
DSV4_PREFILL_RADIX_TOPK=1
DSV4_MTP_SPEC=1
DSV4_MTP_GGUF=/models/DeepSeek-V4-Flash-MTP-Q4K-Q8_0-F32.gguf

Zwei Optionen fehlen bewusst:

  • Kein GGML_CUDA_P2P=1. P2P über mehrere Root-Complexes erzeugte auf diesem Rig zuvor beschädigte Nulltransfers und bleibt deaktiviert.
  • Kein DSV4_MMVQ_SMALLK=1. Diese Optimierung zielt auf die veröffentlichten Expertentensoren in IQ2_XXS beziehungsweise Q2_K, nicht auf unseren Q8.

Der entsprechende zentrale Serverstart lautet:

docker rm -f deepseek_v4_flash 2>/dev/null || true
docker run -d --name deepseek_v4_flash \
  --entrypoint /app/llama-server \
  --gpus all --ipc host --shm-size 64g \
  -p 8009:8080 \
  -e DSV4_CONSTANT_SHAPE=1 \
  -e DSV4_SPARSE_FA=1 \
  -e DSV4_FA_UNION=1 \
  -e DSV4_IDX_SKIP=1 \
  -e DSV4_MOE_TILE=1 \
  -e DSV4_MOE_RESIDENT=1 \
  -e DSV4_GLU_FUSE=1 \
  -e DSV4_MOE_FUSE=1 \
  -e DSV4_DECODE_FUSED_IDX=1 \
  -e DSV4_DECODE_RADIX_TOPK=1 \
  -e DSV4_PREFILL_RADIX_TOPK=1 \
  -e DSV4_MTP_SPEC=1 \
  -e DSV4_MTP_GGUF=/models/DeepSeek-V4-Flash-MTP-Q4K-Q8_0-F32.gguf \
  -v /bigData/llama-models/unsloth/DeepSeek-V4-Flash-GGUF:/models:ro \
  llama-ds4-longctx:393c388a-sm86-runtime \
  -m /models/UD-Q8_K_XL/DeepSeek-V4-Flash-UD-Q8_K_XL-00001-of-00005.gguf \
  --alias deepseek-v4-flash-q8-mtp \
  --host 0.0.0.0 --port 8080 \
  -ngl 999 --split-mode layer --flash-attn on --no-repack \
  --ctx-size 262144 --batch-size 4096 --ubatch-size 512 \
  --spec-type dsv4-mtp --parallel 1 \
  --jinja --reasoning on --reasoning-format deepseek

Die aktive optimierte Konfiguration nutzt die automatische Layer-Platzierung. Vor einem Einsatz oberhalb der gemessenen 100k-Last würden wir den Layer-Split manuell ausbalancieren oder den Batch-Druck reduzieren. Nach dem Test waren auf der knappsten Karte nur 157 MiB frei.

Der Mainline-Aufbau für den Kapazitätstest verwendete kleinere Batches:

--ctx-size 524288 --parallel 1 \
--flash-attn on --n-gpu-layers 999 --split-mode layer \
--cache-type-k q8_0 --cache-type-v q8_0 \
--batch-size 1024 --ubatch-size 128

Für die 768k- und 1M-Läufe änderten wir ausschließlich den konfigurierten Kontext. Aus diesen Mainline-Versuchen stammen der Start des 1M-Servers und die Messung mit 120k aktivem Kontext auf diesem Server. Sie sind ausdrücklich nicht mit der engeren, auf 262k konfigurierten MTP-Variante gleichzusetzen.

Kommentar schreiben

Deine E-Mail-Adresse wird nicht veröffentlicht. Der erste Kommentar wird manuell freigegeben.