brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
pipeline-gpu-kernelApply software pipelining (double-buffering) to a tiled GPU kernel to overlap global memory loads with Tensor Core computation. Covers prologue/loop/epilogue restructuring, LDG-register vs cp.async (LDGSTS) variant selection based on compute/load ratio, shared memory budget verification against architecture-specific occupancy cliffs, and SASS-level verifica…
Independent GitHub snapshot · Automated classification · Original documentation governs
Software-Pipelining (Double-Buffering) auf einen tiled GPU-Kernel anwenden damit globale Speicher-Loads fuer Tile N+1 mit Tensor-Core-Berechnung auf Tile N ueberlappen. Eine sequenzielle Load-Sync-Compute-Sync-K-Schleife in eine Prolog-/Schleife-/Epilog-Struktur transformieren, zwischen LDG-Register- und cp.async-(LDGSTS)-Varianten basierend auf Compute-Load-Ratio waehlen, verifizieren dass Shared Memory unter dem Architektur-Occupancy-Cliff bleibt und Load-/Compute-Ueberlappung im finalen SASS bestaetigen.
analyze-kernel-bottleneck einen speicherbegrenzten Kernel mit niedriger Compute-Load-Ratio pro Tile identifiziert.cu) mit einer tiled K-Schleife die separate Load- und Compute-Phasen enthaeltanalyze-kernel-bottleneck; wird geschaetzt wenn nicht bereitgestellt)Bestaetigen dass der Kernel eine tiled K-Schleife mit distinkten Load- und Compute-Phasen separiert durch __syncthreads() hat. Die verdoppelten Shared-Memory-Kosten berechnen und verifizieren dass sie unter dem Architektur-Occupancy-Cliff bleiben.
__syncthreads(), Berechnung (HMMA/IMMA/FFMA) auf den Shared-Memory-Tiles, __syncthreads().smem_a_size = BM * BK * sizeof(T) und smem_b_size = BK * BN * sizeof(T).smem_doubled = smem_a_size * 2 + smem_b_size * 2.Single buffer: smem_a[BM*BK] + smem_b[BK*BN] = 2 KB + 2 KB = 4 KB
Double buffer: smem_a[2][BM*BK] + smem_b[2][BK*BN] = 4 KB + 4 KB = 8 KB
8 KB << 50 KB cliff -> 2 blocks/SM -> 8 warps
num_tiles = K / BK. Pipelining erfordert num_tiles >= 2 (mindestens ein Prolog + eine Hauptschleifen-Iteration).Erwartet: Eine Shared-Memory-Budget-Tabelle die Single-Buffer- und Double-Buffer-Kosten zeigt und bestaetigt dass die verdoppelte Allokation unter dem Architektur-Cliff mit mindestens 2 Bloecken/SM Occupancy bleibt.
Bei Fehler: Wenn Double-Buffer den Cliff ueberschreitet, Tile-Groesse reduzieren (BK oder BM halbieren) bis smem_doubled <= 50 KB fuer GA104. Alternativ Register-only Prefetch (LDG-Variante) ohne Verdoppelung von Shared Memory nutzen — vorgefetchte Daten in Registern speichern und nach __syncthreads() in denselben Single-Buffer schreiben.
Zwischen LDG-Register und cp.async (LDGSTS) basierend auf der Compute-Load-Ratio pro Tile auswaehlen.
ratio = (2 * BM * BN * BK) / ((BM * BK + BK * BN) * sizeof(T)) fuer GEMM-aehnliche Kernels (2 FLOPs pro Multiply-Add, geladene Bytes pro Tile).LDG-Register-Variante (ratio >= 5 oder CUDA < 11.0):
buf[N % 2] berechnen (ueberlappt mit ausstehenden LDGs).__syncthreads(), dann STS Register in buf[(N+1) % 2], __syncthreads().(BM * BK + BK * BN) / BLOCK_SIZE Register pro Thread fuer Staging.cp.async (LDGSTS) Variante (ratio < 5, CUDA >= 11.0):
__pipeline_memcpy_async Tile N+1 direkt zu buf[(N+1) % 2] (async, umgeht das Register-File).__pipeline_commit() vor Berechnung.buf[N % 2] berechnen.__pipeline_wait_prior(0) + __syncthreads() nach Berechnung.#include <cuda_pipeline.h>.Erwartet: Ausgewaehlte Variante mit Begruendung basierend auf Compute-Load-Ratio und Ziel-Architektur.
Bei Fehler: Wenn die Ratio mehrdeutig ist (5-20:1-Bereich), beide Varianten implementieren und benchmarken. Die cp.async-Variante ist der sicherere Default wenn die CUDA-Version sie unterstuetzt.
Die sequenzielle Load-Sync-Compute-Sync-Schleife in eine pipelined Prolog-/Schleife-/Epilog-Struktur transformieren.
Die drei Abschnitte identifizieren: Der originale Schleifen-Body wird drei Stuecke:
buf[0] laden, synchronisieren, dann in die Hauptschleife eintreten.num_tiles - 1 Laden von Tile N+1 mit Berechnung von Tile N ueberlappen.LDG-Register-Variantenstruktur:
// === LDG-register variant ===
// Prologue: load tile 0 into buf[0]
cooperative_load_tile(smem_a[0], smem_b[0], global_a, global_b, /*k_offset=*/0);
__syncthreads();
for (int tile = 0; tile < num_tiles - 1; tile++) {
int cur_buf = tile & 1;
int next_buf = 1 - cur_buf;
// Phase 1: LDG next tile into registers (non-blocking)
float reg_a[ELEMS_PER_THREAD_A], reg_b[ELEMS_PER_THREAD_B];
prefetch_tile_to_registers(reg_a, reg_b, global_a, global_b,
(tile + 1) * BK);
// Phase 2: Compute on current buffer (overlaps with LDG flight)
tensor_core_mma(smem_a[cur_buf], smem_b[cur_buf], acc);
// Phase 3: Drain registers into next buffer
__syncthreads();
store_registers_to_smem(smem_a[next_buf], smem_b[next_buf],
reg_a, reg_b);
__syncthreads();
}
// Epilogue: compute last tile
tensor_core_mma(smem_a[(num_tiles - 1) & 1], smem_b[(num_tiles - 1) & 1], acc);
// === cp.async variant ===
#include <cuda_pipeline.h>
// Prologue: async load tile 0 into buf[0]
cpasync_load_tile(smem_a[0], smem_b[0], global_a, global_b, /*k_offset=*/0);
__pipeline_commit();
__pipeline_wait_prior(0);
__syncthreads();
for (int tile = 0; tile < num_tiles - 1; tile++) {
int cur_buf = tile & 1;
int next_buf = 1 - cur_buf;
// Phase 1: cp.async next tile into next buffer (async, direct to smem)
cpasync_load_tile(smem_a[next_buf], smem_b[next_buf],
global_a, global_b, (tile + 1) * BK);
__pipeline_commit();
// Phase 2: Compute on current buffer (overlaps with LDGSTS in flight)
tensor_core_mma(smem_a[cur_buf], smem_b[cur_buf], acc);
// Phase 3: Wait for async copies to complete
__pipeline_wait_prior(0);
__syncthreads();
}
// Epilogue: compute last tile
tensor_core_mma(smem_a[(num_tiles - 1) & 1], smem_b[(num_tiles - 1) & 1], acc);
num_tiles - 1 Iterationen (Tiles 0 bis num_tiles - 2 indizieren welche Tiles zu berechnen, ladet Tiles 1 bis num_tiles - 1). Der Epilog berechnet das in der letzten Iteration geladene Tile.Erwartet: Restrukturierter K-Schleifen-Quellcode mit klaren Prolog-, Hauptschleifen- und Epilog-Abschnitten fuer die gewaehlte Variante.
Bei Fehler: Der haeufigste Bug ist ein Off-by-One in Buffer-Indexierung oder Vergessen des Epilog-Compute-Pass. Verifizieren: Prolog laedt in buf[0], erste Hauptschleifen-Iteration berechnet buf[0] und laedt in buf[1], zweite Iteration berechnet buf[1] und laedt in buf[0] und so weiter. Der Epilog berechnet buf[(num_tiles - 1) & 1].
Den double-buffered Shared Memory deklarieren und die Load-Funktionen implementieren.
// Before (single buffer)
__shared__ half smem_a[BM * BK];
__shared__ half smem_b[BK * BN];
// After (double buffer)
__shared__ half smem_a[2][BM * BK];
__shared__ half smem_b[2][BK * BN];
__device__ void cpasync_load_tile(half* dst_a, half* dst_b,
const half* src_a, const half* src_b,
int k_offset) {
// Each thread copies its portion (16 bytes = 8 half values per cp.async)
int tid = threadIdx.x;
int bytes_per_thread = 16; // cp.async.cg supports 4, 8, or 16 bytes
// A tile: BM * BK elements, distributed across BLOCK_SIZE threads
int elems_a = BM * BK / BLOCK_SIZE;
for (int i = 0; i < elems_a; i += 8) {
int idx = tid * elems_a + i;
__pipeline_memcpy_async(dst_a + idx,
src_a + k_offset * BM + idx,
bytes_per_thread);
}
// B tile: BK * BN elements, distributed similarly
int elems_b = BK * BN / BLOCK_SIZE;
for (int i = 0; i < elems_b; i += 8) {
int idx = tid * elems_b + i;
__pipeline_memcpy_async(dst_b + idx,
src_b + k_offset * BN + idx,
bytes_per_thread);
}
}
// Declare register staging (size = elements per thread)
half reg_a[BM * BK / BLOCK_SIZE];
half reg_b[BK * BN / BLOCK_SIZE];
// Prefetch: LDG from global to registers (non-blocking, issued early)
for (int i = 0; i < BM * BK / BLOCK_SIZE; i++) {
int idx = threadIdx.x * (BM * BK / BLOCK_SIZE) + i;
reg_a[i] = global_a[k_offset * BM + idx];
}
// ... similarly for reg_b
// Store: STS from registers to shared memory (after __syncthreads)
for (int i = 0; i < BM * BK / BLOCK_SIZE; i++) {
int idx = threadIdx.x * (BM * BK / BLOCK_SIZE) + i;
smem_a[next_buf][idx] = reg_a[i];
}
__launch_bounds__(BLOCK_SIZE) auf dem Kernel halten um dem Compiler akkurate Occupancy-Information zu geben.nvcc --cubin -arch=sm_86 -O2 -o kernel.sm_86.cubin kernel.cu.Erwartet: Kompilierbarer Kernel mit double-buffered Shared Memory und dem gewaehlten Lade-Mechanismus. Erfolgreiche cubin-Generierung ohne Fehler.
Bei Fehler: Wenn Kompilierung an Pipeline-API-Calls scheitert, sicherstellen dass #include <cuda_pipeline.h> praesent ist und CUDA-Toolkit >= 11.0. Wenn Register-Spills auftreten (mit nvcc --resource-usage pruefen), die Register-Staging-Array-Groessen reduzieren indem BLOCK_SIZE erhoeht oder BK reduziert wird.
Den pipelinedKernel gegen die CPU-Referenz ausfuehren um identische numerische Ausgabe zu bestaetigen.
nvcc -arch=sm_86 -O2 -o bench bench.cu -lcuda -I../../phase2/common.abs=0.5, rel=0.1abs=1e-2, rel=1e-2abs=1e-3, rel=1e-3Erwartet: PASS bei beiden kleinen und Ziel-Problemgroessen mit Fehlergrenzen identisch zur nicht-pipelined Baseline.
Bei Fehler: Buffer-Indexierungs-Bug ist die wahrscheinlichste Ursache. Verifizieren: Compute liest aus buf[tile & 1] waehrend Loads in buf[1 - (tile & 1)] schreiben. Pruefen dass der Epilog Buffer-Index (num_tiles - 1) & 1 verarbeitet, nicht num_tiles & 1. Fuer cp.async verifizieren dass __pipeline_wait_prior(0) vor __syncthreads() abschliesst — sonst kann Compute teilweise-geschriebene Daten lesen.
Den pipelined Kernel gegen die nicht-pipelined Baseline bei der Ziel-Problemgroesse messen.
speedup = pipelined_metric / baseline_metric.| Variant | GFLOPS | Speedup vs Baseline |
|------------------|--------|---------------------|
| Baseline | XXX | 1.00x |
| LDG-register | XXX | X.XXx |
| cp.async (LDGSTS)| XXX | X.XXx |
Erwartet: Performance-Vergleichstabelle die Verbesserung zeigt. Die gewaehlte Variante sollte messbaren Speedup konsistent mit der Compute-Load-Ratio-Vorhersage zeigen.
Bei Fehler: Wenn Performance regrediert, drei Dinge pruefen: (1) SASS auf unerwarteten Instruktions-Overhead (extra BAR.SYNC, Register-Spills). (2) Shared Memory hat den Occupancy-Cliff nicht ueberschritten — mit nvcc --resource-usage oder cuobjdump -res-usage verifizieren. (3) Die Problemgroesse produziert genug Tiles (K / BK >= 4) damit Pipelining den Prolog-/Epilog-Overhead amortisieren kann.
Das kompilierte SASS inspizieren um zu bestaetigen dass globale Loads und Tensor-Core-Instruktionen innerhalb des Hauptschleifen-Body ueberlappen.
cuobjdump -sass kernel.sm_86.cubin | grep -E 'IMMA|HMMA|LDGSTS|LDG|BAR'.LDGSTS- oder LDG-Instruktionen erscheinen vor HMMA- oder IMMA-Instruktionen.BAR.SYNC zwischen den Load-Instruktionen und den Compute-Instruktionen (sie muessen frei sein im Warp-Scheduler zu ueberlappen).BAR.SYNC erscheint nach dem Compute-Block, sperrt die Nutzung der geladenen Daten der naechsten Iteration.# Full SASS pipeline verification
cuobjdump -sass kernel.sm_86.cubin | grep -E 'IMMA|HMMA|LDGSTS|LDG|BAR'
# Count compute instructions per loop
cuobjdump -sass kernel.sm_86.cubin | grep -c 'HMMA\|IMMA'
# Check for register spills
nvcc --resource-usage --cubin -arch=sm_86 -O2 kernel.cu 2>&1 | grep -i spill
Erwartet: Annotierter SASS-Auszug der das Load-vor-Compute-Muster ohne dazwischenliegende Barriers zeigt. Null Register-Spills.
Bei Fehler: Wenn der Compiler Loads nach Compute reordnet (was die Ueberlappung besiegt), versuchen: (1) #pragma unroll 1 auf der Hauptschleife um zu aggressives Unrolling zu verhindern. (2) Load und Compute in distinkte Inline-Funktionen separieren um einen Sequenzierungs-Hint zu schaffen. (3) asm volatile("" ::: "memory") als Compiler-Fence zwischen Load- und Compute-Bloecken nutzen (letzter Ausweg — kann andere Optimierungen hemmen).
buf[tile & 1]-Muster)buf[0]buf[(num_tiles - 1) & 1]BAR.SYNC zwischen LDGSTS/LDG und IMMA/HMMA)nvcc --resource-usage pruefen)smem_doubled vor Implementation berechnen. Ein Kernel der 28 KB single-buffered nutzt springt auf 56 KB doubled, ueberschreitet den Cliff und halbiert Occupancy. Dies kann einen +20%-Pipelining-Gewinn in eine -50%-Occupancy-Regression verwandeln.buf[tile & 1] fuer den aktuellen Compute-Buffer und buf[1 - (tile & 1)] fuer den naechsten Load-Buffer nutzen. Ein haeufiger Fehler ist die Nutzung von buf[(tile + 1) & 1] fuer den naechsten Buffer, was nur dann aequivalent zu buf[1 - (tile & 1)] ist wenn die Buffer-Anzahl 2 ist — aber falsch liest wenn versehentlich auf den Compute-Index angewendet.__pipeline_commit() muss VOR der Compute-Phase aufgerufen werden (es siegelt den Batch von Async-Kopien). __pipeline_wait_prior(0) muss NACH der Compute-Phase aufgerufen werden (es blockiert bis alle commiteten Kopien abschliessen). Diese zu vertauschen macht die Async-Kopien synchron und eliminiert allen Ueberlappungs-Vorteil.__syncthreads() zwischen Compute und dem STS-Drain benoetigt (damit Compute das Lesen des aktuellen Buffers beendet bevor er ueberschrieben wird). Ein weiteres __syncthreads() wird nach dem STS-Drain benoetigt (damit alle Threads das Schreiben beenden bevor die naechste Iteration liest). In der cp.async-Variante stellt __syncthreads() nach __pipeline_wait_prior(0) sicher dass alle Threads die abgeschlossenen Async-Kopien sehen.__pipeline_memcpy_async erfordert dass die Quelladresse gueltig und ausgerichtet ist. An Matrix-Raendern wo K kein Vielfaches von BK ist, kann das letzte Tile out-of-bounds lesen. Auf Skalar-Loads mit Bounds-Pruefung fuer das finale Tile zurueckfallen oder die Eingabe-Matrizen auf ein Vielfaches von BK paddern.analyze-kernel-bottleneck — identifizieren ob der Kernel speicherbegrenzt ist und die Compute-Load-Ratio berechnen die die Variantenauswahl treibt