UE5 Heterogeneous Multi-GPU: Unterschied zwischen den Versionen

Aus UnrealWiki
Elaina (Diskussion | Beiträge)
Projektstand 05.09.2026: asynchroner 128x96-Texture-Job, Pending/Ready und persistentes Target
Elaina (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
Zeile 1: Zeile 1:
{{Hinweis|Projektstand: 5. September 2026. Die beschriebenen Ergebnisse sind ein experimenteller Proof of Concept für Unreal Engine 5.8 unter Windows und Direct3D 12.}}
{{Hinweis|Projektstand: 6. September 2026, Commit <code>cbf56c2</code>. Die beschriebenen Ergebnisse sind ein experimenteller Proof of Concept für Unreal Engine 5.8 unter Windows und Direct3D 12.}}


'''UE5 Heterogeneous Multi-GPU''' ist ein experimentelles Projekt zur gemeinsamen Nutzung unterschiedlicher Grafikkarten in [[Unreal Engine 5]]. Eine leistungsfähige GPU übernimmt das normale Rendering. Weitere NVIDIA-, AMD- oder Intel-GPUs sollen als unabhängige Worker klar abgegrenzte Aufgaben bearbeiten.
'''UE5 Heterogeneous Multi-GPU''' ist ein experimentelles Projekt zur gemeinsamen Nutzung unterschiedlicher Grafikkarten in [[Unreal Engine 5]]. Eine leistungsfähige GPU übernimmt das normale Rendering. Weitere NVIDIA-, AMD- oder Intel-GPUs sollen als unabhängige Worker klar abgegrenzte Aufgaben bearbeiten.
Zeile 21: Zeile 21:
|-
|-
! Git-Stand
! Git-Stand
| Commit <code>30202f7</code> auf Branch <code>mgpu-experimental</code>
| Commit <code>cbf56c2</code> auf Branch <code>mgpu-experimental</code>
|-
|-
! Windows/D3D12-Proof-of-Concept
! Windows/D3D12-Proof-of-Concept
| Asynchroner 128×96-Worker-Texture-Job, GPU-Transfer, Pending/Ready-Ausgabe und persistente UE-Textur nachgewiesen
| Sichtbarer 128×96-Worker-Texture-Stream im TPS, persistenter Single Runtime Slot, stabile UTexture-Bridge und Benchmark-Diagnostik nachgewiesen
|-
|-
! Gesamtvision
! Gesamtvision
Zeile 34: Zeile 34:
<pre>
<pre>
Intel Worker GPU
Intel Worker GPU
   -> MainTextureCS
   -> MainTextureCS + PatternPhase
   -> lokale Worker-Textur
   -> persistente Worker-Textur
   -> Shared Cross-Adapter Buffer
   -> persistenter Shared Cross-Adapter Buffer
   -> Shared Fence / Queue Wait
   -> monotone Shared Fence / Queue Wait
   -> UE-eigene FTextureRHIRef auf der Primary
   -> persistente UE-eigene FTextureRHIRef
  -> stabile UExperimentalMultiGPUTexture


RTX Primary GPU
RTX Primary GPU
   -> normales UE5-Rendering
   -> normales UE5-Rendering
   -> übernimmt und verwendet das Worker-Ergebnis
   -> TPS-Material zeigt das Worker-Ergebnis sichtbar an
</pre>
</pre>
Pro Worker ist maximal ein Runtime-Job gleichzeitig in flight. Backpressure lehnt weitere Requests als <code>Busy</code> ab, bis der Slot nach der Primary-Completion wieder sicher verwendbar ist.


Jeder Worker besitzt ein eigenes <code>ID3D12Device</code>, eigene Queues, Command Lists und Ressourcen. Ein späterer Scheduler soll Rechenleistung, Fähigkeiten, Transferkosten und gewünschte Aktualisierungsrate bewerten. Vorgesehene Aufgaben sind beispielsweise SceneCapture2D, Spiegel, CCTV, Minimap, Compute oder Niagara.
Jeder Worker besitzt ein eigenes <code>ID3D12Device</code>, eigene Queues, Command Lists und Ressourcen. Ein späterer Scheduler soll Rechenleistung, Fähigkeiten, Transferkosten und gewünschte Aktualisierungsrate bewerten. Vorgesehene Aufgaben sind beispielsweise SceneCapture2D, Spiegel, CCTV, Minimap, Compute oder Niagara.
Zeile 53: Zeile 56:
Ein echter, von Unreal kompilierter Global Compute Shader läuft auf der Intel UHD, während Unreal regulär über die RTX rendert. Der Test verdoppelt 64 Ganzzahlen auf dem Worker; '''64 von 64 Ergebnissen wurden korrekt verifiziert'''. Damit ist echte GPU-Arbeit auf einem zweiten, herstellerfremden Device nachgewiesen.
Ein echter, von Unreal kompilierter Global Compute Shader läuft auf der Intel UHD, während Unreal regulär über die RTX rendert. Der Test verdoppelt 64 Ganzzahlen auf dem Worker; '''64 von 64 Ergebnissen wurden korrekt verifiziert'''. Damit ist echte GPU-Arbeit auf einem zweiten, herstellerfremden Device nachgewiesen.


Zusätzlich läuft <code>MainTextureCS</code> auf der Intel-GPU. Der synchrone Debugtest erzeugt eine 64 × 64 Pixel große RGBA8-Textur; '''alle 4096 shader-generierten Pixel wurden korrekt verifiziert'''. Der normale Runtime-Pfad verwendet inzwischen einen asynchronen 128×96-Test. Der Shader liest seine Dimensionen mit <code>GetDimensions()</code>, schützt überhängende Threads durch einen Bounds-Check und wird mit <code>Dispatch(16,12,1)</code> ausgeführt. Ein fester 64×64-Divisor ist damit aus dem produktiven Testpfad entfernt.
Zusätzlich läuft <code>MainTextureCS</code> auf der Intel-GPU. Der synchrone Debugtest erzeugt eine 64 × 64 Pixel große RGBA8-Textur; '''alle 4096 shader-generierten Pixel wurden korrekt verifiziert'''. Der normale Runtime-Pfad verwendet inzwischen einen asynchronen 128×96-Test. Der Shader liest seine Dimensionen mit <code>GetDimensions()</code>, schützt überhängende Threads durch einen Bounds-Check und wird mit <code>Dispatch(16,12,1)</code> ausgeführt. <code>PatternPhase</code> erzeugt für jeden akzeptierten Submit deterministisch einen neuen sichtbaren Inhalt. Abgewiesene Busy-Requests erhöhen die Phase nicht und erzeugen daher keine Lücken.


=== Cross-Adapter-Speicher und Synchronisation ===
=== Cross-Adapter-Speicher und Synchronisation ===
Zeile 59: Zeile 62:
* RTX und Intel öffnen denselben D3D12 Cross-Adapter Heap.
* RTX und Intel öffnen denselben D3D12 Cross-Adapter Heap.
* Ein gemeinsamer Buffer wurde mit 64 Testwerten erfolgreich geprüft.
* Ein gemeinsamer Buffer wurde mit 64 Testwerten erfolgreich geprüft.
* Ein Shared Fence synchronisiert die Queues direkt von GPU zu GPU.
* Ein persistenter Shared Fence synchronisiert die Queues direkt von GPU zu GPU. Seine Werte steigen pro Job monoton: WorkerReady/PrimaryComplete verwenden die Paare 1/2, 3/4, 5/6 und so weiter.
* Die CPU vermittelt die GPU-Abhängigkeit nicht; Worker- und Primary-Queue verwenden <code>Signal</code> und <code>Wait</code> direkt auf der GPU.
* Die CPU vermittelt die GPU-Abhängigkeit nicht; Worker- und Primary-Queue verwenden <code>Signal</code> und <code>Wait</code> direkt auf der GPU.
* Der separate Readback-Verifikationstest darf weiterhin blockieren. Der normale Runtime-Pfad wartet dagegen nicht auf GPU-Leerlauf.
* Der separate Readback-Verifikationstest darf weiterhin blockieren. Der normale Runtime-Pfad wartet dagegen nicht auf GPU-Leerlauf.
Zeile 69: Zeile 72:
<pre>
<pre>
Intel MainTextureCS
Intel MainTextureCS
   -> Worker-Textur
   -> persistente Worker-Textur
   -> CopyTextureRegion
   -> CopyTextureRegion
   -> Shared Cross-Adapter Buffer
   -> persistenter Shared Cross-Adapter Buffer
   -> Worker signalisiert Shared Fence 1
   -> Worker signalisiert WorkerReady (1, 3, 5, ...)
   -> UE-Primary-Queue wartet auf Fence 1
   -> UE-Primary-Queue wartet auf WorkerReady
   -> direkte Kopie in UE-eigene FTextureRHIRef
   -> direkte Kopie in persistente UE-eigene FTextureRHIRef
   -> Primary signalisiert Completion Fence 2
   -> Primary signalisiert PrimaryComplete (2, 4, 6, ...)
</pre>
</pre>


Der robuste Normal-Pfad kopiert die Worker-Textur über den Shared Buffer direkt in eine von Unreal erzeugte <code>FTextureRHIRef</code>. Breite, Höhe und Row Pitch werden dynamisch aus Worker-Ressource und Transport-Metadaten übernommen. Der erfolgreiche 128×96-RGBA8-Test verwendet 512 Byte Row Pitch und 49.152 Byte Transfergröße. Das Ergebnis wird pro Worker in einer Output Registry veröffentlicht und kann über <code>GetNormalTextureOutputForWorker()</code> abgerufen werden.
Der robuste Normal-Pfad kopiert die Worker-Textur über den Shared Buffer direkt in eine von Unreal erzeugte <code>FTextureRHIRef</code>. Breite, Höhe und Row Pitch werden dynamisch aus Worker-Ressource und Transport-Metadaten übernommen. Der erfolgreiche 128×96-RGBA8-Test verwendet 512 Byte Row Pitch und 49.152 Byte Transfergröße. Die Output Registry trennt Persistent-, Pending- und Ready-Zustände sowie Inhaltsgeneration und Texturidentität. Dadurch kann eine verspätete Completion keinen neueren Output zurückrollen.


=== Non-blocking Runtime-Pfad ===
=== Non-blocking Runtime-Pfad ===


Die produktive Testlogik ist aus dem Konsolenkommando in eine gekapselte NormalPath-API verschoben. <code>SubmitWorkerTextureComputeJob()</code> reicht den Worker-Dispatch ohne CPU-Wait ein. <code>QueueNormalTextureTransfer()</code> reiht Wait, Kopie und Completion-Signal in Unreals D3D12-Kontext ein. Der normale MainTextureCS-Pfad enthält weder CPU-Fence-Waits noch Readback, <code>Map</code> oder Pixelvergleich. Erst <code>TickNormalTextureTransfers()</code> prüft die Primary-Completion in <code>RHIEndFrame()</code> non-blocking.
Die produktive Testlogik ist aus dem Konsolenkommando in eine gekapselte NormalPath-API verschoben. <code>SubmitWorkerTextureComputeJob()</code> reicht den Worker-Dispatch ohne CPU-Wait ein. <code>QueueNormalTextureTransfer()</code> reiht Wait, Kopie und Completion-Signal in Unreals D3D12-Kontext ein. Der normale MainTextureCS-Pfad enthält weder CPU-Fence-Waits noch Readback, <code>Map</code> oder Pixelvergleich. <code>TickNormalTextureTransfers()</code> prüft die Primary-Completion non-blocking über einen Runtime-Pump; <code>RHIEndFrame()</code> bleibt als Fallback erhalten.


Eine passende Primary-<code>FTextureRHIRef</code> wird pro Worker und Konfiguration persistent gehalten. Nach Fence 2 kann der nächste Job dieselbe Resource erneut verwenden; dabei führt Unreal die Übergänge <code>SRVMask -> CopyDest -> SRVMask</code> aus. Pending- und Ready-Zustand hängen von Inhaltsgeneration und Fence ab, nicht von der Pointer-Identität. Eine native D3D12-Resource wird weiterhin nicht blind als UE-Textur gewrappt, weil Unreals Resource-State-Buchhaltung den tatsächlichen nativen Zustand sonst nicht sicher kennt.
Eine passende Primary-<code>FTextureRHIRef</code> wird pro Worker und Konfiguration persistent gehalten. Nach PrimaryComplete kann der nächste Job dieselbe Resource erneut verwenden; dabei führt Unreal die Übergänge <code>SRVMask -> CopyDest -> SRVMask</code> aus. Eine backend-neutrale RHI-Provider-API reicht die Ready-Textur an die Engine weiter. <code>UExperimentalMultiGPUTexture</code> hält eine stabile <code>UTexture</code> samt TextureReference und bindet die persistente <code>FTextureRHIRef</code> ohne zusätzlichen Pixel-Copy. Eine native D3D12-Resource wird weiterhin nicht blind als UE-Textur gewrappt, weil Unreals Resource-State-Buchhaltung den tatsächlichen nativen Zustand sonst nicht sicher kennt.
 
=== Sichtbarer TPS-Consumer ===
 
<code>AExperimentalMultiGPUDisplayActor</code> zeigt die Worker-Ausgabe im Third-Person-Testlevel über den Materialparameter <code>MultiGPUTexture</code> an. Damit ist der Output nicht mehr nur über Logs oder Readback nachgewiesen: Die Intel UHD erzeugt fortlaufend die Texturinhalte, während die RTX die Spielwelt und das Material rendert. <code>TextureConsumerCS</code> kann Ready-Ausgaben auf der Primary zusätzlich als SRV lesen und fünf quantisierte Samples im Debugpfad prüfen.


=== Lebensdauer und automatisches Cleanup ===
=== Lebensdauer und automatisches Cleanup ===


Da die CPU nicht blockierend wartet, hält der Normal-Pfad einen vollständigen In-Flight-Job: Worker-Textur, Root Signature, PSO, Descriptor Heap, Allocator, Command List, Worker-Fence, Cross-Adapter-Transport, Shared-Ressourcen und Primary-Textur bleiben bis zum sicheren Abschluss erhalten. Die Completion wird automatisch in <code>FD3D12DynamicRHI::RHIEndFrame()</code> geprüft:
Der Normal-Pfad verwendet pro Worker genau einen persistenten Runtime-Slot. Worker-Textur, Root Signature, PSO, Descriptor Heap, Command Allocator/List und Worker-Fence werden nach der ersten kompatiblen Initialisierung wiederverwendet. Dasselbe gilt für Shared Heap/Buffer/Fence und die Transport-Command-Objekte. Der In-Flight-Job trägt nur noch die nötigen Job-, Generation- und Fence-Metadaten.
 
<pre>
Runtime Request
  -> Completion non-blocking prüfen
  -> falls Slot frei: Worker Dispatch
  -> WorkerReady-Fence signalisieren
  -> Primary Wait, SafeCopy und PrimaryComplete-Signal
  -> Output Pending
  -> Runtime-Pump oder RHIEndFrame prüft GetCompletedValue()
  -> Output Ready publizieren
  -> Slot wiederverwendbar
</pre>
 
Backpressure erlaubt maximal einen echten In-Flight-Job pro Worker. Ein Busy-Request setzt keine GPU-Arbeit ab, setzt keine Ressourcen zurück und erhöht <code>PatternPhase</code> nicht. Worker-Fence-Werte steigen mit jedem akzeptierten Submit 1, 2, 3 und so weiter; der persistente Shared Fence verwendet fortlaufend die Paare 1/2, 3/4, 5/6. Fehler nach Übergabe von Ressourcen an die GPU führen zu sicherem Retire beziehungsweise Retention statt zu vorzeitigem Reuse.
 
=== Texture Stream und Benchmarking ===
 
Der projektseitige Texture Stream akzeptiert eine beliebige positive, endliche Zielrate. Punkt und Komma sind als Dezimaltrenner möglich. Optional lässt sich die Zahl erfolgreicher Submits begrenzen; Busy- oder Failed-Versuche verbrauchen dieses Limit nicht. Der Quiet-Modus unterdrückt per-Job-Ausgaben, behält aber Start, echte Fehler und die Abschlussstatistik bei.


<pre>
<pre>
Worker-Fence vor dem Submit erzeugen
ExperimentalMultiGPU.StartTextureStream 37,5
  -> Dispatch und UAV->COPY_SOURCE auf Worker-Queue
ExperimentalMultiGPU.StartTextureStream 37,5 25
  -> Worker-Kopie in Shared Buffer
ExperimentalMultiGPU.StartTextureStream 1000 100 quiet
  -> Fence 1: Shared Buffer fertig
ExperimentalMultiGPU.StopTextureStream
  -> Primary Wait, Copy und Signal Fence 2
  -> Output ist Pending
  -> RHIEndFrame prüft GetCompletedValue()
  -> Fence 2 erreicht: aktuelle Generation wird Ready
  -> Job sicher freigeben; persistentes Target bleibt erhalten
</pre>
</pre>


Mehrere Jobs können lifetime-sicher gleichzeitig in flight sein. Im aktuellen Stand besitzt jeder Transport eine eigene Shared Fence sowie eigene Shared Heaps und Buffer; die Werte 1 und 2 sind daher transportlokal. Generationen verhindern, dass ein älterer Job einen neueren Pending- oder Ready-Output zurückrollt. Ein persistentes Target wird nicht erneut beschrieben, solange es noch in flight ist; dann wird vorläufig eine neue Textur erzeugt. Backpressure und ein echter Ressourcenpool fehlen noch.
Die Statistik unterscheidet Timer-Ticks, angenommene Submits, Busy/Failed, Ready, In-Flight und die jeweiligen effektiven Raten. Diagnosezeitpunkte erfassen unter anderem RequestAccepted, RenderCommandExecution, WorkerSubmit, NormalTransferQueued und ReadyObserved.
 
{| class="wikitable"
! Test
! Ergebnis
! Aussage
|-
| 5 Hz, 20 Submits
| etwa 5 Ready/s, 20/20, kein Busy
| Entspannter Single-Slot-Betrieb
|-
| 1000 Hz, 100 Submits, quiet
| 100/100 Ready, 0 Fehler, etwa 58 Ready/s
| Backpressure begrenzt den Durchsatz sicher
|-
| 1000 Hz bei <code>t.MaxFPS 30</code>
| etwa 28–29 Ready/s trotz rund 982 Timer-Ticks/s
| Primary-/SafeCopy-/Ready-Kette ist effektiv framegekoppelt
|}
 
Ein zusätzlicher non-blocking Aufruf von <code>ImmediateFlush(DispatchToRHIThread)</code> erreicht hohe Render-Command-Raten, hebt die gemessene Frame-Kopplung aber nicht auf. Im 30-FPS-Test lag der RHIEndFrame-spezifische Completion-Takt praktisch exakt bei 30 Hz. Das aktuelle Problem ist damit eingegrenzt: Nicht der Stream-Timer, sondern die Primary-RHI-/D3D12-Submission der SafeCopy-Kette bestimmt die sichtbare Ready-Rate.


== Aktueller nächster Schritt ==
== Aktueller nächster Schritt ==


Der nächste qualitative Schritt ist ein '''erster sichtbarer Consumer''': Das Worker-Ergebnis soll kontrolliert in einem UE-Material oder Compositing-Pfad erscheinen. Dabei muss insbesondere die Synchronisation stimmen, wenn dieselbe persistente Textur gelesen und später mit einer neuen Generation beschrieben wird. Danach folgen Backpressure, Ressourcenpooling, Messungen und echte Offload-Workloads.
Der nächste Schritt ist, die '''Frame-Kopplung der Primary-/SafeCopy-/Ready-Kette''' weiter zu zerlegen, ohne einen blockierenden Flush einzuführen. Erst danach sollte erneut gemessen werden, welche reale Worker- und Transferlatenz der Single Slot erreicht. Ein zweiter Slot oder Ringbuffer ist nur sinnvoll, wenn diese Messung echten Pipeline-Bedarf zeigt und der Read-vs-Write-Hazard zwischen Render-Consumer und erneut beschriebener Primary-Textur sauber modelliert ist.


== Historische Vorläufer ==
== Historische Vorläufer ==
Zeile 208: Zeile 248:
* Der asynchrone 128×96-End-to-End-Pfad ist über Dimensionen, Dispatch, Transfer, Fences, Pending/Ready und Reuse verifiziert; ein eigener 128×96-Pixel-Readback fehlt noch. Der synchrone 64×64-Debugtest prüft weiterhin alle 4096 Pixel.
* Der asynchrone 128×96-End-to-End-Pfad ist über Dimensionen, Dispatch, Transfer, Fences, Pending/Ready und Reuse verifiziert; ein eigener 128×96-Pixel-Readback fehlt noch. Der synchrone 64×64-Debugtest prüft weiterhin alle 4096 Pixel.
* Der Normal-Transport unterstützt derzeit <code>DXGI_FORMAT_R8G8B8A8_UNORM</code> mit Sample Count 1; weitere Formate, Mips, Arrays, Slices und MSAA sind offen.
* Der Normal-Transport unterstützt derzeit <code>DXGI_FORMAT_R8G8B8A8_UNORM</code> mit Sample Count 1; weitere Formate, Mips, Arrays, Slices und MSAA sind offen.
* Shared Heaps, Buffer, Fences und Worker-Command-Objekte werden noch pro Job erzeugt. Backpressure, Pooling sowie Double-, Triple- oder Ring-Buffering fehlen.
* Pro Worker existiert absichtlich nur ein persistenter Runtime-Slot. Mehrere parallele In-Flight-Jobs sowie Double-, Triple- oder Ring-Buffering fehlen noch.
* Die Primary-/SafeCopy-/Ready-Kette bleibt trotz zusätzlichem non-blocking <code>DispatchToRHIThread</code> effektiv an den Frame-/Submission-Zyklus gekoppelt.
* Raw-D3D12-Shared-Ressourcen sind noch nicht vollständig in Unreals Residency-Management eingebunden.
* Raw-D3D12-Shared-Ressourcen sind noch nicht vollständig in Unreals Residency-Management eingebunden.
* Praktisch getestet ist Primary plus ein Worker. Die Architektur ist auf mehrere Worker ausgelegt, N-GPU ist aber noch nicht praktisch validiert.
* Praktisch getestet ist Primary plus ein Worker. Die Architektur ist auf mehrere Worker ausgelegt, N-GPU ist aber noch nicht praktisch validiert.
* Ein sichtbarer Material-/Renderer-Consumer, echte UE-Szenen-Workloads, Benchmark-Wizard und Scheduler fehlen weiterhin.
* Der sichtbare TPS-Nachweis zeigt eine Worker-Compute-Textur, noch keine auf dem Worker gerenderte 3D-Szene. Echte UE-Szenen-Workloads, Benchmark-Wizard und Scheduler fehlen weiterhin.
* Die projektseitigen UpscalerTest-Dateien mit DisplayActor und Texture Stream liegen außerhalb des Engine-Git-Repositories und sind nicht Bestandteil von Commit <code>cbf56c2</code>.
* Device Lost, Timeouts, Speicherdruck und die vollständige Fallback-Matrix müssen vor produktiver Nutzung gehärtet werden.
* Device Lost, Timeouts, Speicherdruck und die vollständige Fallback-Matrix müssen vor produktiver Nutzung gehärtet werden.
* Linux/Vulkan mit getrennten Devices, External Memory und externer Synchronisation ist noch nicht implementiert.
* Linux/Vulkan mit getrennten Devices, External Memory und externer Synchronisation ist noch nicht implementiert.
Zeile 217: Zeile 259:
== Roadmap ==
== Roadmap ==


# Worker-Ergebnis als ersten sichtbaren Consumer in Material, Compositing oder Render Graph einbinden.
# Primary-RHI-/D3D12-Submission untersuchen und die Frame-Kopplung ohne blockierende Flushes auflösen.
# Consumer-Synchronisation für Updates derselben persistenten Textur absichern.
# Danach Quiet-Benchmarks wiederholen und die reale Single-Slot-Latenz bestimmen.
# Backpressure sowie Pools für Shared Heaps, Buffer, Fences und Command Objects einführen.
# Nur bei nachgewiesenem Bedarf einen zweiten Slot oder Ringbuffer mit Generation-, Fence-, Reuse- und Consumer-Hazard-Modell ergänzen.
# Ersten echten UE-Offload wie SceneCapture2D, CCTV, Spiegel, Minimap, Niagara oder unabhängigen Compute integrieren.
# Formate, Mips, Arrays/Slices und gegebenenfalls MSAA capability-basiert erweitern.
# Formate, Mips, Arrays/Slices und gegebenenfalls MSAA capability-basiert erweitern.
# Latenz, Bandbreite, Größen, Update-Raten und Synchronisationskosten messen.
# Ersten echten UE-Offload wie SceneCapture2D, CCTV, Spiegel, Minimap, Niagara oder unabhängigen Compute integrieren.
# Capability-Matrix, Benchmark-Wizard, Scheduler und manuelle Overrides entwickeln.
# Capability-Matrix, Benchmark-Wizard, Scheduler und manuelle Overrides entwickeln.
# Mehrere Worker-GPUs unterstützen.
# Mehrere Worker-GPUs unterstützen.
Zeile 230: Zeile 271:
== Quellen ==
== Quellen ==


* Interner Entwicklungsstand und verifizierte Testprotokolle vom 5. September 2026, Commit <code>30202f7</code>
* Interner Entwicklungsstand und verifizierte Testprotokolle vom 6. September 2026, Commit <code>cbf56c2</code>
* [https://learn.microsoft.com/en-us/windows/win32/direct3d12/multi-engine Microsoft: Direct3D 12 Multi-adapter systems]
* [https://learn.microsoft.com/en-us/windows/win32/direct3d12/multi-engine Microsoft: Direct3D 12 Multi-adapter systems]
* [https://learn.microsoft.com/en-us/windows/win32/direct3d12/shared-heaps Microsoft: Shared heaps]
* [https://learn.microsoft.com/en-us/windows/win32/direct3d12/shared-heaps Microsoft: Shared heaps]

Version vom 6. September 2026, 18:19 Uhr

Hinweis: Projektstand: 6. September 2026, Commit cbf56c2. Die beschriebenen Ergebnisse sind ein experimenteller Proof of Concept für Unreal Engine 5.8 unter Windows und Direct3D 12.

UE5 Heterogeneous Multi-GPU ist ein experimentelles Projekt zur gemeinsamen Nutzung unterschiedlicher Grafikkarten in Unreal Engine 5. Eine leistungsfähige GPU übernimmt das normale Rendering. Weitere NVIDIA-, AMD- oder Intel-GPUs sollen als unabhängige Worker klar abgegrenzte Aufgaben bearbeiten.

Das Ziel ist ausdrücklich kein klassisches SLI oder CrossFire. Die Anwendung verteilt Arbeit selbst und tauscht nur benötigte Ergebnisse zwischen den GPUs aus.

Projektstatus
Engine / Plattform Unreal Engine 5.8 Source Build / Windows / Direct3D 12
Testsystem Lenovo LOQ 17IRX10
Primary NVIDIA GeForce RTX 5060 Laptop GPU
Worker Intel UHD Graphics
Git-Stand Commit cbf56c2 auf Branch mgpu-experimental
Windows/D3D12-Proof-of-Concept Sichtbarer 128×96-Worker-Texture-Stream im TPS, persistenter Single Runtime Slot, stabile UTexture-Bridge und Benchmark-Diagnostik nachgewiesen
Gesamtvision etwa 30–35 % (Scheduler, echte Workloads, N-GPU und Vulkan noch offen)

Grundprinzip

Intel Worker GPU
  -> MainTextureCS + PatternPhase
  -> persistente Worker-Textur
  -> persistenter Shared Cross-Adapter Buffer
  -> monotone Shared Fence / Queue Wait
  -> persistente UE-eigene FTextureRHIRef
  -> stabile UExperimentalMultiGPUTexture

RTX Primary GPU
  -> normales UE5-Rendering
  -> TPS-Material zeigt das Worker-Ergebnis sichtbar an

Pro Worker ist maximal ein Runtime-Job gleichzeitig in flight. Backpressure lehnt weitere Requests als Busy ab, bis der Slot nach der Primary-Completion wieder sicher verwendbar ist.

Jeder Worker besitzt ein eigenes ID3D12Device, eigene Queues, Command Lists und Ressourcen. Ein späterer Scheduler soll Rechenleistung, Fähigkeiten, Transferkosten und gewünschte Aktualisierungsrate bewerten. Vorgesehene Aufgaben sind beispielsweise SceneCapture2D, Spiegel, CCTV, Minimap, Compute oder Niagara.

Verifizierter Entwicklungsstand

Worker-Compute

Ein echter, von Unreal kompilierter Global Compute Shader läuft auf der Intel UHD, während Unreal regulär über die RTX rendert. Der Test verdoppelt 64 Ganzzahlen auf dem Worker; 64 von 64 Ergebnissen wurden korrekt verifiziert. Damit ist echte GPU-Arbeit auf einem zweiten, herstellerfremden Device nachgewiesen.

Zusätzlich läuft MainTextureCS auf der Intel-GPU. Der synchrone Debugtest erzeugt eine 64 × 64 Pixel große RGBA8-Textur; alle 4096 shader-generierten Pixel wurden korrekt verifiziert. Der normale Runtime-Pfad verwendet inzwischen einen asynchronen 128×96-Test. Der Shader liest seine Dimensionen mit GetDimensions(), schützt überhängende Threads durch einen Bounds-Check und wird mit Dispatch(16,12,1) ausgeführt. PatternPhase erzeugt für jeden akzeptierten Submit deterministisch einen neuen sichtbaren Inhalt. Abgewiesene Busy-Requests erhöhen die Phase nicht und erzeugen daher keine Lücken.

Cross-Adapter-Speicher und Synchronisation

  • RTX und Intel öffnen denselben D3D12 Cross-Adapter Heap.
  • Ein gemeinsamer Buffer wurde mit 64 Testwerten erfolgreich geprüft.
  • Ein persistenter Shared Fence synchronisiert die Queues direkt von GPU zu GPU. Seine Werte steigen pro Job monoton: WorkerReady/PrimaryComplete verwenden die Paare 1/2, 3/4, 5/6 und so weiter.
  • Die CPU vermittelt die GPU-Abhängigkeit nicht; Worker- und Primary-Queue verwenden Signal und Wait direkt auf der GPU.
  • Der separate Readback-Verifikationstest darf weiterhin blockieren. Der normale Runtime-Pfad wartet dagegen nicht auf GPU-Leerlauf.

Texturtransport

Direkte Cross-Adapter-Texturen sind auf dem Testsystem nicht beidseitig verfügbar: Die Intel UHD meldet Unterstützung für Row-Major Cross-Adapter Textures, die RTX 5060 Laptop GPU nicht. Deshalb verwendet das Projekt einen allgemeineren Buffer-Fallback.

Intel MainTextureCS
  -> persistente Worker-Textur
  -> CopyTextureRegion
  -> persistenter Shared Cross-Adapter Buffer
  -> Worker signalisiert WorkerReady (1, 3, 5, ...)
  -> UE-Primary-Queue wartet auf WorkerReady
  -> direkte Kopie in persistente UE-eigene FTextureRHIRef
  -> Primary signalisiert PrimaryComplete (2, 4, 6, ...)

Der robuste Normal-Pfad kopiert die Worker-Textur über den Shared Buffer direkt in eine von Unreal erzeugte FTextureRHIRef. Breite, Höhe und Row Pitch werden dynamisch aus Worker-Ressource und Transport-Metadaten übernommen. Der erfolgreiche 128×96-RGBA8-Test verwendet 512 Byte Row Pitch und 49.152 Byte Transfergröße. Die Output Registry trennt Persistent-, Pending- und Ready-Zustände sowie Inhaltsgeneration und Texturidentität. Dadurch kann eine verspätete Completion keinen neueren Output zurückrollen.

Non-blocking Runtime-Pfad

Die produktive Testlogik ist aus dem Konsolenkommando in eine gekapselte NormalPath-API verschoben. SubmitWorkerTextureComputeJob() reicht den Worker-Dispatch ohne CPU-Wait ein. QueueNormalTextureTransfer() reiht Wait, Kopie und Completion-Signal in Unreals D3D12-Kontext ein. Der normale MainTextureCS-Pfad enthält weder CPU-Fence-Waits noch Readback, Map oder Pixelvergleich. TickNormalTextureTransfers() prüft die Primary-Completion non-blocking über einen Runtime-Pump; RHIEndFrame() bleibt als Fallback erhalten.

Eine passende Primary-FTextureRHIRef wird pro Worker und Konfiguration persistent gehalten. Nach PrimaryComplete kann der nächste Job dieselbe Resource erneut verwenden; dabei führt Unreal die Übergänge SRVMask -> CopyDest -> SRVMask aus. Eine backend-neutrale RHI-Provider-API reicht die Ready-Textur an die Engine weiter. UExperimentalMultiGPUTexture hält eine stabile UTexture samt TextureReference und bindet die persistente FTextureRHIRef ohne zusätzlichen Pixel-Copy. Eine native D3D12-Resource wird weiterhin nicht blind als UE-Textur gewrappt, weil Unreals Resource-State-Buchhaltung den tatsächlichen nativen Zustand sonst nicht sicher kennt.

Sichtbarer TPS-Consumer

AExperimentalMultiGPUDisplayActor zeigt die Worker-Ausgabe im Third-Person-Testlevel über den Materialparameter MultiGPUTexture an. Damit ist der Output nicht mehr nur über Logs oder Readback nachgewiesen: Die Intel UHD erzeugt fortlaufend die Texturinhalte, während die RTX die Spielwelt und das Material rendert. TextureConsumerCS kann Ready-Ausgaben auf der Primary zusätzlich als SRV lesen und fünf quantisierte Samples im Debugpfad prüfen.

Lebensdauer und automatisches Cleanup

Der Normal-Pfad verwendet pro Worker genau einen persistenten Runtime-Slot. Worker-Textur, Root Signature, PSO, Descriptor Heap, Command Allocator/List und Worker-Fence werden nach der ersten kompatiblen Initialisierung wiederverwendet. Dasselbe gilt für Shared Heap/Buffer/Fence und die Transport-Command-Objekte. Der In-Flight-Job trägt nur noch die nötigen Job-, Generation- und Fence-Metadaten.

Runtime Request
  -> Completion non-blocking prüfen
  -> falls Slot frei: Worker Dispatch
  -> WorkerReady-Fence signalisieren
  -> Primary Wait, SafeCopy und PrimaryComplete-Signal
  -> Output Pending
  -> Runtime-Pump oder RHIEndFrame prüft GetCompletedValue()
  -> Output Ready publizieren
  -> Slot wiederverwendbar

Backpressure erlaubt maximal einen echten In-Flight-Job pro Worker. Ein Busy-Request setzt keine GPU-Arbeit ab, setzt keine Ressourcen zurück und erhöht PatternPhase nicht. Worker-Fence-Werte steigen mit jedem akzeptierten Submit 1, 2, 3 und so weiter; der persistente Shared Fence verwendet fortlaufend die Paare 1/2, 3/4, 5/6. Fehler nach Übergabe von Ressourcen an die GPU führen zu sicherem Retire beziehungsweise Retention statt zu vorzeitigem Reuse.

Texture Stream und Benchmarking

Der projektseitige Texture Stream akzeptiert eine beliebige positive, endliche Zielrate. Punkt und Komma sind als Dezimaltrenner möglich. Optional lässt sich die Zahl erfolgreicher Submits begrenzen; Busy- oder Failed-Versuche verbrauchen dieses Limit nicht. Der Quiet-Modus unterdrückt per-Job-Ausgaben, behält aber Start, echte Fehler und die Abschlussstatistik bei.

ExperimentalMultiGPU.StartTextureStream 37,5
ExperimentalMultiGPU.StartTextureStream 37,5 25
ExperimentalMultiGPU.StartTextureStream 1000 100 quiet
ExperimentalMultiGPU.StopTextureStream

Die Statistik unterscheidet Timer-Ticks, angenommene Submits, Busy/Failed, Ready, In-Flight und die jeweiligen effektiven Raten. Diagnosezeitpunkte erfassen unter anderem RequestAccepted, RenderCommandExecution, WorkerSubmit, NormalTransferQueued und ReadyObserved.

Test Ergebnis Aussage
5 Hz, 20 Submits etwa 5 Ready/s, 20/20, kein Busy Entspannter Single-Slot-Betrieb
1000 Hz, 100 Submits, quiet 100/100 Ready, 0 Fehler, etwa 58 Ready/s Backpressure begrenzt den Durchsatz sicher
1000 Hz bei t.MaxFPS 30 etwa 28–29 Ready/s trotz rund 982 Timer-Ticks/s Primary-/SafeCopy-/Ready-Kette ist effektiv framegekoppelt

Ein zusätzlicher non-blocking Aufruf von ImmediateFlush(DispatchToRHIThread) erreicht hohe Render-Command-Raten, hebt die gemessene Frame-Kopplung aber nicht auf. Im 30-FPS-Test lag der RHIEndFrame-spezifische Completion-Takt praktisch exakt bei 30 Hz. Das aktuelle Problem ist damit eingegrenzt: Nicht der Stream-Timer, sondern die Primary-RHI-/D3D12-Submission der SafeCopy-Kette bestimmt die sichtbare Ready-Rate.

Aktueller nächster Schritt

Der nächste Schritt ist, die Frame-Kopplung der Primary-/SafeCopy-/Ready-Kette weiter zu zerlegen, ohne einen blockierenden Flush einzuführen. Erst danach sollte erneut gemessen werden, welche reale Worker- und Transferlatenz der Single Slot erreicht. Ein zweiter Slot oder Ringbuffer ist nur sinnvoll, wenn diese Messung echten Pipeline-Bedarf zeigt und der Read-vs-Write-Hazard zwischen Render-Consumer und erneut beschriebener Primary-Textur sauber modelliert ist.

Historische Vorläufer

Die Grundidee heterogener Multi-GPU-Nutzung ist nicht neu. Bereits mit der Einführung von Direct3D 12 entstanden mehrere Versuche, integrierte und dedizierte GPUs gemeinsam zu verwenden. Eine allgemein einsetzbare Lösung für moderne UE5-Spiele hat sich daraus jedoch nicht entwickelt.

Microsoft und Epic: UE4 Elemental Demo (2015)

Microsoft zeigte eine angepasste Unreal-Engine-4-Version der Elemental-Demo, bei der eine NVIDIA-GPU und eine Intel-iGPU gemeinsam arbeiteten. Das erklärte Ziel war, ansonsten ungenutzte Grafikleistung in Hybrid-Systemen zu verwenden. Dieser Versuch ist der Grundidee des Projekts besonders ähnlich, blieb aber eine technische Demonstration.

Intel: D3D12 Multi-Adapter Sample (2015)

Intels Beispiel kombinierte eine GeForce 840M mit Intel HD Graphics 5500. Beide GPUs berechneten getrennte Teile einer einfach parallelisierbaren Raytracing-Szene. Cross-Adapter-Ressourcen und Shared Fences führten die Ergebnisse anschließend zusammen. Intel berichtete in diesem Test von nahezu halbierter Framezeit, warnte jedoch ausdrücklich davor, das Ergebnis unmittelbar auf reale Spiele zu übertragen.

Ashes of the Singularity

Die Nitrous Engine von Ashes of the Singularity setzte Direct3D-12-Explicit-Multiadapter in einem veröffentlichten Spiel ein. Dabei konnten auch unterschiedliche GPU-Modelle und Hersteller zusammenarbeiten. Das war ein wichtiger Praxisnachweis, blieb in der Spielebranche aber eine seltene Ausnahme.

Weitere verwandte Ansätze

  • Rise of the Tomb Raider erhielt explizite Direct3D-12-Multi-GPU-Unterstützung, vor allem für klassische Kombinationen ähnlicher GPUs.
  • NVIDIA VR SLI wies bei Virtual Reality jeder GPU ein Auge zu. Das verteilt unabhängige Ansichten, bleibt jedoch NVIDIA-spezifisch.
  • Unreal nDisplay kann Viewports oder Frustums auf weitere GPUs beziehungsweise Prozesse auslagern und das fertige Bild zur Ausgabe-GPU zurückführen. Der Einsatzbereich ist hauptsächlich Virtual Production.
  • Forschung und High-Performance Computing verwenden seit Jahren Scheduler für heterogene GPUs, allerdings überwiegend für Compute- und Datenverarbeitungsaufgaben statt für einen allgemeinen UE5-Echtzeitrenderer.

Abgrenzung dieses Projekts

Das Projekt erfindet Direct3D-12-Explicit-Multiadapter nicht neu. Sein eigener Ansatz liegt in der Kombination aus tiefer UE5.8-RHI-Integration, UE-kompilierten Shadern auf unabhängigen Worker-Devices, frei wählbaren Spiel- und Compute-Workloads, Capability- und Performance-Scheduler, mehreren Fallback-Pfaden sowie dem langfristigen Ziel beliebiger Hersteller und mehrerer Worker.

Vergleich mit anderen Multi-GPU-Verfahren

Verfahren Arbeitsweise Verhältnis zu diesem Projekt
SLI / CrossFire Treiber- beziehungsweise Verbundlösung für meist ähnliche GPUs; häufig wird die Bildarbeit verteilt. Dieses Projekt benötigt keinen herstellerspezifischen GPU-Verbund und weist Aufgaben ausdrücklich selbst zu.
AFR (Alternate Frame Rendering) GPU 1 rendert einen Frame, GPU 2 den nächsten. Das Projekt verteilt unabhängige Aufgaben statt aufeinanderfolgender Frames. Dadurch werden Frame-Abhängigkeiten und typisches AFR-Pacing vermieden.
SFR (Split Frame Rendering) Mehrere GPUs bearbeiten Bereiche desselben Frames. Erfordert enge Lastverteilung und viel Datenaustausch. Das Projekt bevorzugt vollständig abgrenzbare Workloads und überträgt deren Ergebnis.
Direct3D 12 Linked Multiadapter Mehrere vom Treiber verbundene GPUs erscheinen als Knoten eines logischen Adapters. Eignet sich eher für eng kompatible GPUs. Das Projekt verwendet unabhängige Devices (Unlinked/Explicit Multiadapter) und unterstützt dadurch heterogene Herstellerkombinationen.
Direct3D 12 Unlinked / Explicit Multiadapter Die Anwendung verwaltet getrennte Adapter, Ressourcen und Synchronisation selbst. Das ist die technische Familie des Projekts. Hinzu kommen die eigene UE5-RHI-Integration, Workload-Ziele, Fallbacks und der geplante Scheduler.
UE nDisplay mGPU / Multi-Process Separate GPUs rendern bestimmte Viewports oder Frustums, vor allem für Virtual Production; Ergebnisse werden zur Ausgabe-GPU kopiert. Ähnliche Idee der aufgabenweisen Trennung, aber für einen anderen Einsatzbereich. Dieses Projekt zielt auf allgemeine Spiel- und Compute-Workloads innerhalb der Engine.
Vulkan Device Groups Ähnliche physische GPUs können ein gemeinsames logisches Device bilden. Für beliebige heterogene GPUs meist ungeeignet, da Geräte einer Gruppe nahezu gleiche Eigenschaften besitzen müssen. Eine spätere Vulkan-Portierung benötigt daher wahrscheinlich getrennte Devices und External Memory/Semaphores.

SLI und CrossFire bezeichnen den Verbund; AFR und SFR beschreiben mögliche Verteilungsmethoden innerhalb solcher Systeme. Sie sind deshalb nicht vollständig getrennte Kategorien.

Vorteile der geplanten Variante

  • Herstellerunabhängig: NVIDIA, AMD und Intel können grundsätzlich kombiniert werden.
  • Vorhandene Hardware nutzen: Auch eine sonst wenig genutzte integrierte GPU kann geeignete Nebenaufgaben übernehmen.
  • Aufgaben statt Frames verteilen: Spiegel, Minimap oder Compute können mit eigener Auflösung und Aktualisierungsrate laufen.
  • Keine identischen GPUs erforderlich: Unterschiedliche Fähigkeiten können gezielt genutzt werden.
  • Kontrollierter Datenaustausch: Nur das benötigte Ergebnis muss zurück zur Primary-GPU.
  • Robuste Fallback-Idee: Wenn direkte Texturen nicht gemeinsam nutzbar sind, bleibt der Shared-Buffer-Pfad.
  • Erweiterbar: Scheduler, manuelles Mapping und mehrere Worker sind als spätere Stufen vorgesehen.

Nachteile und technische Risiken

  • Hoher Entwicklungsaufwand: Geräte, Ressourcen, Zustände, Fences, Fehlerfälle und UE-Lebenszyklen müssen selbst verwaltet werden.
  • Transfer kann den Gewinn aufzehren: Cross-Adapter Heaps liegen laut D3D12 nicht automatisch im schnellen lokalen VRAM. Bandbreite und Latenz müssen für jeden Workload gemessen werden.
  • VRAM wird nicht einfach addiert: Benötigte Ressourcen können auf mehreren GPUs vorliegen und zusätzlichen Speicher verbrauchen.
  • Langsame Worker können bremsen: Eine Aufgabe lohnt sich nur, wenn Rechengewinn größer als Übergabe-, Warte- und Kopierkosten ist.
  • Nicht jeder Workload ist unabhängig: Hauptansicht, Lumen, Nanite und stark gekoppelte Renderpässe besitzen viele Abhängigkeiten und sind schwieriger auszulagern.
  • Hardwareunterschiede: Formate, Shader-Funktionen, Queue-Fähigkeiten und Cross-Adapter-Support müssen pro GPU geprüft werden.
  • Wartungsrisiko: Eingriffe in private D3D12RHI-Dateien können bei Engine-Updates angepasst werden müssen.
  • Produktionsreife fehlt noch: Ressourcenpools, Timeouts, Device-Lost-Wiederherstellung, Scheduler und echte Szenentests sind offen.

Wann die Variante sinnvoll ist

Am aussichtsreichsten sind Aufgaben mit viel eigener Rechenarbeit, einem relativ kleinen Endergebnis und wenigen Abhängigkeiten zur Hauptszene. Gute frühe Kandidaten sind unabhängige Compute-Aufgaben sowie Scene Captures, die nicht in jedem Frame aktualisiert werden müssen. Weniger geeignet sind winzige Aufgaben mit großem Transfer oder Renderpässe, die ständig auf Ressourcen der Primary-GPU zugreifen.

Die entscheidende Regel für den späteren Scheduler lautet daher:

Worker-Gewinn > Vorbereitung + Datentransfer + Synchronisation + Rückintegration

Bekannte Grenzen des Proof of Concepts

  • Der asynchrone 128×96-End-to-End-Pfad ist über Dimensionen, Dispatch, Transfer, Fences, Pending/Ready und Reuse verifiziert; ein eigener 128×96-Pixel-Readback fehlt noch. Der synchrone 64×64-Debugtest prüft weiterhin alle 4096 Pixel.
  • Der Normal-Transport unterstützt derzeit DXGI_FORMAT_R8G8B8A8_UNORM mit Sample Count 1; weitere Formate, Mips, Arrays, Slices und MSAA sind offen.
  • Pro Worker existiert absichtlich nur ein persistenter Runtime-Slot. Mehrere parallele In-Flight-Jobs sowie Double-, Triple- oder Ring-Buffering fehlen noch.
  • Die Primary-/SafeCopy-/Ready-Kette bleibt trotz zusätzlichem non-blocking DispatchToRHIThread effektiv an den Frame-/Submission-Zyklus gekoppelt.
  • Raw-D3D12-Shared-Ressourcen sind noch nicht vollständig in Unreals Residency-Management eingebunden.
  • Praktisch getestet ist Primary plus ein Worker. Die Architektur ist auf mehrere Worker ausgelegt, N-GPU ist aber noch nicht praktisch validiert.
  • Der sichtbare TPS-Nachweis zeigt eine Worker-Compute-Textur, noch keine auf dem Worker gerenderte 3D-Szene. Echte UE-Szenen-Workloads, Benchmark-Wizard und Scheduler fehlen weiterhin.
  • Die projektseitigen UpscalerTest-Dateien mit DisplayActor und Texture Stream liegen außerhalb des Engine-Git-Repositories und sind nicht Bestandteil von Commit cbf56c2.
  • Device Lost, Timeouts, Speicherdruck und die vollständige Fallback-Matrix müssen vor produktiver Nutzung gehärtet werden.
  • Linux/Vulkan mit getrennten Devices, External Memory und externer Synchronisation ist noch nicht implementiert.

Roadmap

  1. Primary-RHI-/D3D12-Submission untersuchen und die Frame-Kopplung ohne blockierende Flushes auflösen.
  2. Danach Quiet-Benchmarks wiederholen und die reale Single-Slot-Latenz bestimmen.
  3. Nur bei nachgewiesenem Bedarf einen zweiten Slot oder Ringbuffer mit Generation-, Fence-, Reuse- und Consumer-Hazard-Modell ergänzen.
  4. Ersten echten UE-Offload wie SceneCapture2D, CCTV, Spiegel, Minimap, Niagara oder unabhängigen Compute integrieren.
  5. Formate, Mips, Arrays/Slices und gegebenenfalls MSAA capability-basiert erweitern.
  6. Capability-Matrix, Benchmark-Wizard, Scheduler und manuelle Overrides entwickeln.
  7. Mehrere Worker-GPUs unterstützen.
  8. Fallback-Matrix aus Normal Shared Buffer, System-RAM/Staging und einem späteren Experimental Fast Path ergänzen.
  9. Linux/Vulkan mit getrennten Devices, External Memory und externer Synchronisation untersuchen.

Quellen