Contents
  1. Numerical work and GPU execution
    1. What a kernel implements
    2. Assigning work to the device
    3. Turning points in GPU programming
  2. Data placement and cooperation
    1. Where operands live
    2. Indices become memory transactions
    3. Safe sharing and reuse
  3. Correctness before speed
    1. Specify the numerical result
    2. Test the supported domain
  4. Measurement and diagnosis
    1. Measure completed work
      1. Unchanged arithmetic, different overhead
    2. Bound arithmetic and traffic
    3. Turn observations into hypotheses
  5. Matrix computation
    1. Reuse operands through tiling
    2. Match tiles to instructions and shapes
    3. Overlap movement with computation
  6. Fusion and collective operations
    1. Keep useful intermediates close
    2. Combine partial results
    3. Normalize rows without excess traffic
      1. Stable softmax
      2. RMSNorm
  7. Attention as a kernel algorithm
    1. Carry normalization across score tiles
    2. Masks and work partitioning
      1. After reducing traffic
  8. Workload variation and implementation
    1. When indices choose the addresses
    2. Choose the level of control
      1. Specialization and tuning
    3. Establish the useful gain
  9. Check understanding
  10. Open questions
  11. Selected talks
  12. References
  13. Talk library
← All topics

GPU Programming and Kernel Optimization

GPU programming organizes numerical computations so many workers can execute them together. A kernel is a function launched on the GPU; optimizing it can reduce execution time or temporary memory without changing the intended computation. The challenge is to supply enough independent work while limiting data movement and coordinating shared results. A useful optimization must also preserve supported inputs, arithmetic requirements, and acceptable numerical differences. These principles help you choose libraries and compilers, decide when custom code is justified, and understand why an improvement for one workload can hurt another.

Numerical work and GPU execution

What a kernel implements

A graphics processing unit, or GPU, executes parallel numerical work. The host is the CPU-side program submitting that work; the GPU is the device. A kernel is a function launched for execution on the device.

A tensor is a multidimensional numerical array; its shape lists the dimension sizes. In a model, some arrays contain learned weights, numerical parameters established during training. Inference applies those parameters to inputs. Kernel programming concerns the execution of the resulting array operations.

ARM×K,BRK×N,Cij=k=0K1AikBkj.A\in\mathbb{R}^{M\times K},\quad B\in\mathbb{R}^{K\times N},\quad C_{ij}=\sum_{k=0}^{K-1}A_{ik}B_{kj}. Matrix multiplication produces an MM-row, NN-column output. Each entry combines one row of AA with one column of BB. For example, [1,2,3][1,2,3] and [4,5,6][4,5,6] contribute 14+25+36=321\cdot4+2\cdot5+3\cdot6=32. Different output positions can be computed independently.

The operation specifies the result, not the launch count. A compiler can combine several operations into one kernel. Conversely, a large sum can use one kernel to produce partial sums and another to combine them. Identical mathematics can therefore involve different launches, temporary arrays, and memory transfers.

Assigning work to the device

A thread is one logical execution of kernel code. CUDA, NVIDIA's GPU programming platform, groups threads into cooperating thread blocks and blocks into a launch grid. Each block runs on one streaming multiprocessor (SM). Blocks with allocated execution resources are resident; others wait. Ordinary blocks cannot assume scheduling order.

CUDA groups threads into 32-thread warps. Single-instruction, multiple-thread (SIMT) execution applies shared instructions across participating lanes, the thread positions within a warp. When threads branch differently, lanes outside the executing path become inactive. Warp membership alone does not synchronize accesses to shared data.

For a simple output array, a logical thread might own one position. More elaborate kernels assign several values to each thread or an entire output region to a block. The mapping creates available work; it does not determine how much progresses at once. Latency hiding means issuing eligible work while other work waits for operands or earlier instructions. Charles Frye's GPU introduction explains why concurrency helps sustain throughput without making each individual operation complete sooner.

Execution-group vocabulary varies. AMD HIP calls its hardware group a wavefront and exposes its width through warpSize; portable code must not assume 32. OpenCL organizes work-items into workgroups and implementation-controlled subgroups. Their collective operations and progress guarantees require their own contracts.

Turning points in GPU programming

General computation on early programmable graphics hardware required expressing ordinary work through graphics interfaces, textures, and drawing operations. Stanford's Brook project addressed this mismatch with collections of records, kernels applied across them, and reductions. Its streams were data collections, distinct from the command queues called streams in CUDA.

DevelopmentDateContribution
BrookSIGGRAPH 2004Expressed stream computations without requiring graphics-oriented programming.
CUDAReleased 2007Exposed independently schedulable blocks containing cooperating threads.
RooflineApril 2009 publicationRelated attainable arithmetic throughput to data movement.
TritonJune 2019 paperMade multidimensional tiles a programming unit.
FlashAttention2022Reorganized exact attention around memory transfers.

CUDA's decomposition separated logical work from processor count: a runtime could distribute independent blocks across devices with different available parallelism. Nickolls, Buck, Garland, and Skadron's 2008 account describes shared memory and barriers as tools for cooperation inside those blocks. Later performance models, compilers, and specialized algorithms addressed different remaining problems. They continue to complement numerical libraries and explicit GPU programming.

Data placement and cooperation

Where operands live

Registers hold thread-private values. Shared memory is a programmer-managed, on-chip scratchpad accessible to a block. Global memory holds device allocations that persist across launches. Hardware-managed caches retain accessed data automatically. Register spilling places private values in device-backed local memory, adding traffic.

Capacity is how much fits; latency is how long an access takes; bandwidth is bytes transferred per second. These constraints are independent enough that fitting a model does not establish responsive execution. High-bandwidth memory, HBM, is one device-memory technology, not a name for every GPU storage space.

Keep reusable inputs in device allocations across calls. Host-to-device transfers belong in the complete-call cost when inputs arrive from host memory; retaining those inputs avoids repeating that transfer.

Indices become memory transactions

Memory layout maps logical indices to addresses. A stride is the element step along one dimension. Storage offset selects the starting element; element size converts these steps to bytes. Distinct tensor objects can share storage.

address(i,j)=b+e(o+is0+js1).\operatorname{address}(i,j)=b+e\,(o+i s_0+j s_1). Here bb is the storage base address, ee the bytes per element, oo the storage offset, and s0,s1s_0,s_1 the strides. A contiguous row-major matrix with NN columns has strides (N,1)(N,1).

A transposed view changes this interpretation without moving values. The view is cheap to create, but its consumers encounter different address patterns. Materializing a contiguous representation copies a non-contiguous input. That conversion earns its cost only if subsequent use saves enough work; shared-storage mutations must also retain their intended meaning.

A memory transaction transfers an address segment. Coalescing lets a warp's requests share transactions. In CUDA's documented 32-byte model, aligned consecutive four-byte reads across 32 lanes need four transactions; scattered reads can need 32. Both request 128 useful bytes.

The same reads can touch more segments

32 lanes each read four bytes. Addresses are relative to a 32-byte-aligned base.

4 segments · 128 transaction bytes
128 useful bytes in every case.

Lane 0Byte 0Segment 0
Lane 1Byte 4Segment 0
Lane 2Byte 8Segment 0
Lane 3Byte 12Segment 0
Lane 4Byte 16Segment 0
Lane 5Byte 20Segment 0
Lane 6Byte 24Segment 0
Lane 7Byte 28Segment 0
Lane 8Byte 32Segment 1
Lane 9Byte 36Segment 1
Lane 10Byte 40Segment 1
Lane 11Byte 44Segment 1
Lane 12Byte 48Segment 1
Lane 13Byte 52Segment 1
Lane 14Byte 56Segment 1
Lane 15Byte 60Segment 1
Lane 16Byte 64Segment 2
Lane 17Byte 68Segment 2
Lane 18Byte 72Segment 2
Lane 19Byte 76Segment 2
Lane 20Byte 80Segment 2
Lane 21Byte 84Segment 2
Lane 22Byte 88Segment 2
Lane 23Byte 92Segment 2
Lane 24Byte 96Segment 3
Lane 25Byte 100Segment 3
Lane 26Byte 104Segment 3
Lane 27Byte 108Segment 3
Lane 28Byte 112Segment 3
Lane 29Byte 116Segment 3
Lane 30Byte 120Segment 3
Lane 31Byte 124Segment 3

Aligned stride 1: 4 segments. Offset 1: 5 segments. Stride 8: 32 segments.

Each of 32 lanes reads four bytes from a 32-byte-aligned base: address = base + 4 × (offset + lane × stride). Useful reads stay at 128 bytes. Aligned stride 1 touches four segments; offset 1 touches five; stride 8 touches 32. Controls change addresses. Modeled transaction bytes are not measured cache or HBM traffic, or latency.

Shared memory's banks serve accesses in parallel. Bank conflicts serialize distinct-address requests to one bank; same-address reads can broadcast. In CUDA's float-transpose example, successive words map to successive banks: a 32-word row stride sends column accesses to one bank, while padding to 33 advances the bank between rows. Architecture and access width affect this mapping.

Safe sharing and reuse

Unique output ownership makes parallel writes straightforward: each worker writes its own result. Cooperation adds dependencies. If workers fill a shared operand buffer, consumers must wait for those writes. Before overwriting that buffer, producers must also wait for the previous readers. Readiness for consumption and readiness for reuse are different conditions.

A barrier coordinates participating workers at a specified scope. Its participation rules matter: a block barrier reached by only some required threads is invalid. Warp-level sharing can need an earlier synchronization point even when a block barrier occurs later; a later barrier cannot repair a preceding race.

An atomic operation protects a read-modify-write update to its target. It does not automatically publish unrelated writes or establish that every producer has finished. CUDA's legacy atomics use relaxed ordering and specified thread scopes; newer interfaces allow explicit ordering. Protecting a counter and making its associated operand buffer ready are separate obligations.

Across launches, a CUDA stream orders commands. Different streams need explicit dependencies. An event recorded after a producer can make another stream wait; combining results from several streams requires completion dependencies covering every producer.

Correctness before speed

Specify the numerical result

A numerical contract states the operation, supported inputs, arithmetic settings, and acceptable differences. Floating-point formats represent a finite set of numbers, so arithmetic rounds. Consequently, changing addition order can change a result. Cancellation, subtracting nearly equal quantities, can expose information already lost to rounding. A fused multiply-add evaluates a product plus an addend with one rounding, rather than rounding both stages separately.

For a small arithmetic illustration, use binary FP32 with round-to-nearest, ties-to-even, and let a = 2²⁴.
EvaluationIntermediate resultFinal result
(a + 1) − aa + 1 rounds to a0
a + (1 − a)1 − a is exactly representable1

FP32 is 32-bit single-precision floating point; FP16 is 16-bit half precision. BF16, Brain Floating Point, is also 16-bit but allocates more bits to exponent range and fewer to fraction precision than FP16. Overflow exceeds a format's finite range; infinity can subsequently produce NaN, meaning not-a-number. Storage and intermediate arithmetic formats can differ.

Matching input dtype therefore does not fully specify a comparison. PyTorch documents matrix implementations that sometimes reduce intermediate accumulation precision, and an attention math backend that upcasts FP16/BF16 inputs for intermediate computation. Record these settings alongside the input and output types. Higher intermediate precision can require more memory and execution time.

aratol+rtolr.|a-r|\leq \mathrm{atol}+\mathrm{rtol}\,|r|. For actual value aa and reference rr, absolute tolerance governs agreement near zero; relative tolerance scales with reference magnitude. Choose tolerances from the operation's requirements. In PyTorch's assert_close, infinities must match and matching NaNs requires explicit permission. Numerical closeness differs from bitwise determinism.

Quantization represents values using restricted numerical levels, with rounding and potentially clipping. It can change storage and executable arithmetic; its calibration methods and quality tradeoffs belong in Quantization.

Test the supported domain

Apply behavioral testing to the declared kernel contract. Compare against an independently understandable reference, using higher precision when it helps distinguish arithmetic error from implementation disagreement. For a model port, comparing corresponding intermediate outputs can locate the first divergence instead of leaving only a final-output mismatch.

Derive cases from the promises the implementation makes. A mask selects positions a memory operation may access: masked loads substitute a specified value at invalid positions, while masked stores skip those positions.
Contract conditionUseful check
Dimensions need not divide tile sizesExercise partial tiles; verify masked loads and valid stores.
Several layouts are supportedCompare each supported stride pattern with the reference.
Inputs or outputs may share storageExercise permitted aliases and verify required input preservation.
A bounded numerical domain is supportedInclude cancellation, near-zero outputs, and large or small permitted values.
Exceptional values are acceptedCheck their explicit comparison and output policy.

Compute Sanitizer separately checks memory access, initialization, shared-memory races, and synchronization. These tools complement output comparison: a numerically correct run can still contain unsafe accesses. Shared-memory race checking does not establish the absence of every global-memory race. Repeated execution can expose intermittent faults without proving their absence.

Validate the configuration that will actually be timed. Changing tile size, arithmetic settings, or generated code after validation changes the candidate. Compilation and successful execution are preliminary gates; a speed comparison is useful only after both implementations satisfy the intended result.

Measurement and diagnosis

Measure completed work

Kernel launches return before device work finishes. A host timer around submission can therefore miss execution. Device events timestamp progress through a stream; wait for the stop event before reading elapsed time. A complete-call host measurement must likewise include completion, while recognizing that extra synchronization changes execution.

Name the measured boundary: kernel execution, a sequence of launches, or the complete call including transfers and allocation. Warmup moves lazy initialization outside a steady-state measurement. Repeat measurements and retain variation, rather than relying on one timing. PyTorch's benchmark utilities provide warmup and accelerator synchronization, but the benchmark statement still determines which costs are included.

Submission ends before execution

Example timings

A submission timer can exclude most device work.

Completed call012 microsecondsDuration 12 microseconds
Host submission02 microsecondsDuration 2 microsecondsWithin Completed call
Device execution111 microsecondsDuration 10 microsecondsWithin Completed call
Host wait and completion212 microsecondsDuration 10 microsecondsWithin Completed call
The completed-call interval contains submission, device execution, and host waiting. Device-event endpoints bracket execution. Waiting overlaps device work; summing these spans does not give elapsed time.
Read the diagram as text
  • Completed call. Host measurement includes completion. 0 to 12 microseconds; duration 12 microseconds.
  • Host submission. Launch returns before device completion. 0 to 2 microseconds; duration 2 microseconds. Parent: Completed call.
  • Device execution. Device-event interval: 1 to 11 microseconds. 1 to 11 microseconds; duration 10 microseconds. Parent: Completed call.
  • Host wait and completion. 2 to 12 microseconds; duration 10 microseconds. Parent: Completed call.

Repeatedly reading one small buffer can create a different cache regime from the intended workload. Baseline and candidate order can also affect cache state. Record working-set reuse, competing device work, setup exclusions, and synchronization boundaries as part of the evaluation protocol. A warm measurement and a cold-start measurement answer different questions.

CUDA Graphs prepare an operation graph for repeated submission. Capture records work, instantiation prepares an executable graph, and replay launches it. This reduces recurring host setup without removing the device computations; capture restrictions and setup costs still matter.

Unchanged arithmetic, different overhead

Alan Gray's September 2019 experiment kept a 500,000-element scaling kernel unchanged on Tesla V100, CUDA 10.1, with 512 threads per block. Across 1,000 groups of 20 launches, submission and synchronization changed the cost:
Measured boundary or execution methodMicroseconds per kernel
Device execution alone2.9
Synchronize after every kernel9.6
Synchronize after each group3.8
Graph replay, including amortized setup3.4

The graph's roughly 400-microsecond setup was paid once. These historical repeated-buffer results illustrate submission costs, not universal launch latency or cold-cache performance. The arithmetic stayed fixed; the way the host coordinated it changed.

Bound arithmetic and traffic

Arithmetic intensity is operations per byte crossing a named memory boundary. A Roofline bound combines that intensity with bandwidth and arithmetic capacity. Williams, Waterman, and Patterson's 2009 account counted bytes reaching main memory after cache filtering, making locality part of the performance model.

I=FD,tmax ⁣(FPcompute,DB),Pmin(Pcompute,BI).I=\frac{F}{D},\qquad t\geq\max\!\left(\frac{F}{P_{\mathrm{compute}}},\frac{D}{B}\right),\qquad P\leq\min(P_{\mathrm{compute}},BI). Here FF is the operation count, DD transferred bytes, BB bytes per second, and PcomputeP_{\mathrm{compute}} arithmetic operations per second at the chosen precision. Dividing FF by each minimum execution time gives the throughput bound.

For example, scaling FP32 values performs one multiplication per four-byte read and four-byte write: an ideal 0.125 operations per byte. Matrix multiplication conventionally counts a multiply-add as two operations. For ordinary multiplication (β=0\beta=0), reading each FP16 operand once and writing each FP16 output once, without reading an existing output matrix, gives ideal intensity MNK/(MK+KN+MN)MNK/(MK+KN+MN). These are traffic estimates, excluding extra accesses and cache effects.

A bandwidth ceiling does not establish bandwidth saturation. Too little parallel work, dispatch delays, and inefficient accesses can leave execution below both bounds. Use the model to narrow possibilities, then investigate actual execution.

Reuse changes the bandwidth bound

Example

Execution can remain below both resource ceilings.

Fixed precision and memory boundary

The bandwidth slope meets the arithmetic ceiling at intensity 8.

03691202.557.510Arithmetic intensity (FLOP/byte)Throughput (TFLOP/s)Roofline boundStarting pointImproved executionHigher intensityBandwidth boundArithmetic ceiling
  • 1. Roofline bound
  • 2. Starting point
  • 3. Improved execution
  • 4. Higher intensity
Read coordinates and regions as data

X: 012 FLOP/byte; Y: 010 TFLOP/s, increasing up. Axes scaled independently; screen angles and distances are not comparable.

Roofline bound (polyline)

(0, 0); (8, 8); (12, 8)

Starting point (points)

(2, 1)

Improved execution (polyline)

(2, 1); (2, 2)

Higher intensity (polyline)

(2, 1); (6, 1)

Bandwidth bound: (4.4, 5.4)

Arithmetic ceiling: (9.5, 8.6)

Illustrative FP32 limits: 8 TFLOP/s and 1 TB/s at device memory. Horizontal movement changes intensity; vertical movement changes achieved throughput. Arrows are hypothetical changes, not measurements.

Turn observations into hypotheses

Start with the complete execution timeline. Nsight Systems separates CPU-side CUDA calls from GPU kernels and memory operations. Locate consequential kernels, transfers, and gaps before inspecting individual instructions. A gap identifies missing activity, not its cause.

Occupancy is resident active warps per SM divided by the supported maximum. Registers, shared memory, and block size constrain residency. An eligible warp is ready to issue; useful arithmetic utilization concerns productive execution. Higher occupancy need not improve throughput.

Use counters to propose a discriminating change, following the same logic as testing explanations of a failure.
ObservationPossible explanationControlled change
Many transactions per useful loadLane mapping scatters addressesRemap accesses while preserving arithmetic.
Register-spill trafficToo much live private stateReduce tile size or fusion scope.
Few output blocksToo little independent workTry smaller output tiles or split the reduction.

Nsight Compute's short-scoreboard stalls can involve shared memory, special math, or branching; they do not uniquely diagnose bank conflicts. A not-selected warp was eligible while another issued. Profiling can replay kernels, serialize launches, change clocks, and flush caches. Confirm a promising change under ordinary timing conditions as well as the profiler.

Matrix computation

Reuse operands through tiling

Tiling, or blocking, partitions computation into reusable pieces. The March 1990 Level 3 BLAS paper organized Basic Linear Algebra Subprograms around matrix-matrix operations. Its motivation was locality: vector-at-a-time algorithms moved too much data, while blocks allowed repeated calculation on resident operands. Blocking predates this paper; the contribution included reusable numerical interfaces suited to hierarchical memory.

GEMM, general matrix multiplication, computes C=αAB+βCC=\alpha AB+\beta C; ordinary multiplication uses α=1,β=0\alpha=1,\beta=0. A block retains one output tile's partial sums while traversing KK chunks. Shared operand tiles let each loaded AA value serve multiple columns and each BB value multiple rows.

For an interior bM×bNb_M\times b_N output tile and reduction chunk of length bKb_K: Fchunk=2bMbNbK,Eloads=bMbK+bKbN.F_{\mathrm{chunk}}=2b_Mb_Nb_K,\qquad E_{\mathrm{loads}}=b_Mb_K+b_Kb_N. These count arithmetic operations and operand elements loaded once, respectively; output traffic, boundary waste, and inter-block cache reuse are excluded.

Illustrative pseudocode

Python-like pseudocode
# Conceptual tile program; not executable CUDA.
acc = zeros(output_tile_shape, dtype=float32)
for k_chunk in reduction_chunks:
    a = load_a_tile(k_chunk, invalid_value=0)
    b = load_b_tile(k_chunk, invalid_value=0)
    acc += tile_matmul(a, b)
store_valid_output_positions(acc)

The Triton matrix tutorial uses FP32 accumulators, zero-filled reduction-edge loads, and masked output stores. In an explicitly shared-memory implementation, loading must finish before consumption and consumption before buffer reuse. Larger tiles improve potential reuse but demand more live storage and leave fewer independent output blocks.

One shared-memory implementation: operand buffers change with each K chunk while private partial outputs persist. The drawing shows storage roles rather than an exact tile size or lane assignment.

Match tiles to instructions and shapes

A matrix multiply-accumulate instruction computes a small matrix product plus an accumulator. NVIDIA announced Volta Tensor Cores on May 10, 2017. Its documented hardware operation used 4×4 matrices with FP16 multiplicands and FP16 or FP32 accumulation, while CUDA exposed larger warp-level matrix operations. Application matrices, programmer-visible fragments, and hardware operations are distinct sizes.

WMMA, warp matrix multiply-accumulate, is one collective interface: all warp threads participate with matching parameters. Its fragments distribute elements using an unspecified mapping. Supported shapes, types, layouts, and alignment are constrained; CUDA 13.0 documents 256-bit load alignment. These requirements do not describe every matrix-instruction API.

Tile quantization means work falls into discrete output tiles; wave quantization means the final scheduling wave is partly occupied. In a fixed 256×128-tile A100 example, M=2304 and K=4096, changing N from 1536 to 1544 raises tile count from 108 to 117. With one block per SM on 108 SMs, nine blocks occupy an extra wave. This illustrates a fixed decomposition, not every library's selection.

Resource limits create another tradeoff. Larger tiles can require enough registers to spill or enough shared memory to prevent launch. A matrix-vector or narrow-output product also exposes less output parallelism than a broad matrix product. Select a decomposition for the actual shape; neither the largest tile nor maximum occupancy is an objective by itself.

Overlap movement with computation

Software pipelining overlaps movement for a future tile with computation on a ready tile. Double buffering supplies separate storage for those roles. An asynchronous copy followed immediately by a wait creates no overlap. The first load primes the pipeline; the final computation drains it.

A stage is a buffer slot. Acquire it, submit and commit its copy, wait before reading, then release after reading. Occupied stages block acquisition; more stages consume shared memory.

A buffer must be ready to read—and free to refill

Example timings

Two buffers overlap a future load with computation while preserving both consumption and reuse dependencies.

One buffer015 time unitsDuration 15 time units
Load 0 · buffer 002 time unitsDuration 2 time unitsWithin One buffer
Compute 025 time unitsDuration 3 time unitsWithin One buffer
Load 1 · buffer 057 time unitsDuration 2 time unitsWithin One buffer
Compute 1710 time unitsDuration 3 time unitsWithin One buffer
Load 2 · buffer 01012 time unitsDuration 2 time unitsWithin One buffer
Compute 21215 time unitsDuration 3 time unitsWithin One buffer
Two buffers011 time unitsDuration 11 time units
Load 0 · buffer 002 time unitsDuration 2 time unitsWithin Two buffers
Compute 025 time unitsDuration 3 time unitsWithin Two buffers
Load 1 · buffer 124 time unitsDuration 2 time unitsWithin Two buffers
Compute 158 time unitsDuration 3 time unitsWithin Two buffers
Load 2 · buffer 057 time unitsDuration 2 time unitsWithin Two buffers
Compute 2811 time unitsDuration 3 time unitsWithin Two buffers
Loads take 2 time units and computations take 3 in this example. Load completion permits reading; consumer completion permits reuse. Two buffers shorten the span from 15 to 11. Tile 2 waits for both its load and the preceding accumulator update.
Read the diagram as text
  • One buffer. Each load waits until the preceding computation releases buffer 0. 0 to 15 time units; duration 15 time units.
  • Load 0 · buffer 0. 0 to 2 time units; duration 2 time units. Parent: One buffer.
  • Compute 0. 2 to 5 time units; duration 3 time units. Parent: One buffer.
  • Load 1 · buffer 0. 5 to 7 time units; duration 2 time units. Parent: One buffer.
  • Compute 1. 7 to 10 time units; duration 3 time units. Parent: One buffer.
  • Load 2 · buffer 0. 10 to 12 time units; duration 2 time units. Parent: One buffer.
  • Compute 2. 12 to 15 time units; duration 3 time units. Parent: One buffer.
  • Two buffers. Prime with tile 0's load, overlap later loads with computation, then drain with tile 2's computation. 0 to 11 time units; duration 11 time units.
  • Load 0 · buffer 0. Priming: the first computation needs this completed load. 0 to 2 time units; duration 2 time units. Parent: Two buffers.
  • Compute 0. Releases buffer 0 at time 5. 2 to 5 time units; duration 3 time units. Parent: Two buffers.
  • Load 1 · buffer 1. Overlaps computation on buffer 0. 2 to 4 time units; duration 2 time units. Parent: Two buffers.
  • Compute 1. Its operands are ready at 4; the preceding accumulator update finishes at 5. 5 to 8 time units; duration 3 time units. Parent: Two buffers.
  • Load 2 · buffer 0. Acquires buffer 0 only after compute 0 releases it. 5 to 7 time units; duration 2 time units. Parent: Two buffers.
  • Compute 2. Draining: the final computation has no subsequent load to overlap. 8 to 11 time units; duration 3 time units. Parent: Two buffers.

Fusion and collective operations

Keep useful intermediates close

Kernel fusion implements several operations in one kernel execution. Consider y=2sin(x)y=2^{\sin(x)}. Separate kernels write the sine result to device memory and then read it for exponentiation. A fused kernel can retain that intermediate and store only the final result. For equally sized arrays, the simple element-transfer count falls from four per output to two, before considering caching or spills. This is the mechanism illustrated in Joe Fioti's Luminal talk.

A matrix epilogue applies postprocessing to accumulated outputs before their final storage. For example, cuBLASLt offers bias followed by ReLU, the elementwise function max(x,0)\max(x,0). Its documented bias vector matches output rows and broadcasts across columns: for our matrix convention, Yij=max(Cij+bi,0)Y_{ij}=\max(C_{ij}+b_i,0). Other conventions require reconciling that orientation. Auxiliary modes can retain extra activation outputs when callers need them.

Fusion must preserve both dependencies and efficient work assignment. Thread-local intermediates can remain in registers; block-level sharing may require shared memory and coordination. Incompatible layouts, different preferred parallelizations, or excessive live state can make the fused kernel slower. FusionStitching evaluates saved launches and traffic against execution and resource costs, sometimes generating and timing a candidate because a simple traffic model is insufficient.

Required consumers also constrain removal: an intermediate needed elsewhere must remain available or be recomputed. Fusion can change rounding boundaries; applying an activation before converting an accumulator to FP16 differs from applying it after FP16 storage. Validate that change against the numerical contract.

Fusion differs from buffer reuse. Reuse assigns storage to a later value after the earlier value's last use; the values remain distinct. Whole-graph lifetime analysis can establish that opportunity even when the operations remain separate kernels.

Combine partial results

A reduction combines many values into fewer values using an operation such as sum or maximum. Its neutral element leaves a result unchanged: zero for addition, negative infinity for a maximum over finite scores. Invalid lanes can contribute these identities instead of accessing invalid input positions.

A hierarchical reduction first combines several elements within each thread. Warp-level exchange combines thread partials; one partial per warp can then enter shared memory for block-wide combination. Larger arrays can produce one partial per block, followed by a combining kernel. The reduction tree determines communication and floating-point addition order.

A warp shuffle exchanges register values; it is not a memory barrier. Cross-block combining needs launch ordering or explicit producer dependencies, not assumptions about block scheduling.

Split-K assigns different portions of an output tile's dot products to separate blocks. CUTLASS combines their stored partials in a second kernel, gaining parallel work at the cost of workspace and combination.

Atomic accumulation offers another combination path, but competing updates can contend and their floating-point order can vary. Atomicity protects updates without fixing summation order. Choose between staged and atomic approaches using the destination pattern, workspace allowance, launch costs, and reproducibility requirements.

Warp aggregation combines a group's counter increments before one elected thread updates the counter. Andy Adinets's 2014 filtering article illustrates reduced contention; its 2017 update explains that CUDA 9 automated this transformation in many cases. The example concerns one shared counter, not arbitrary scatter destinations, and its output order differs from an order-preserving prefix-sum filter.

Normalize rows without excess traffic

Stable softmax

Softmax turns scores into normalized exponential weights. For a nonempty finite row, subtracting its maximum keeps exponential inputs nonpositive without changing the mathematical result. The maximum and denominator are reductions; outputs depend on both.

m=maxjxj,zj=exjm,=jzj,pj=zj/.m=\max_j x_j,\qquad z_j=e^{x_j-m},\qquad \ell=\sum_j z_j,\qquad p_j=z_j/\ell. Subtracting mm multiplies every original exponential by the same factor, which cancels during normalization.

The Triton fused-softmax tutorial retains row intermediates on chip, reading and writing each element once. Computational padding uses negative-infinity loads and masked stores. Longer rows can exceed this resource budget, requiring rereading or partitioning; row count also determines how much independent work exists. The finite-row formula alone does not define fully masked or arbitrary non-finite inputs.

RMSNorm

Root mean square normalization, or RMSNorm, uses a different statistic. Zhang and Sennrich's 2019 RMSNorm paper investigated reducing normalization overhead by omitting mean subtraction. Unlike LayerNorm, RMSNorm does not center the vector. Replacing one with the other changes the operation, rather than merely optimizing its implementation.

yi=xiγiϵ+1nj=0n1xj2.y_i=\frac{x_i\gamma_i}{\sqrt{\epsilon+\frac{1}{n}\sum_{j=0}^{n-1}x_j^2}}. For a row of length nn, γi\gamma_i is a learned elementwise gain and ϵ\epsilon is a stabilizing constant inside the square root. The reduction axes and epsilon are part of the contract; PyTorch's RMSNorm API specifies trailing normalized dimensions.

Computing the normalized output is the forward pass. Training also needs a backward pass, which computes how the loss depends on inputs and learned gains. Differentiating through the reductions creates its own storage and combination requirements. How fitting changes predictions explains how these derivatives guide learning.

Attention as a kernel algorithm

Carry normalization across score tiles

Attention compares queries with keys to produce scores, then uses normalized weights to combine associated values. These are numerical representations; queries, keys, and values and value mixtures explain their model role. Here the implementation problem is avoiding complete score and probability arrays between those operations.

Milakov and Gimelshein's 2018 Online Normalizer Calculation for Softmax maintains a running maximum and exponential sum. A later score can increase that maximum. Earlier contributions must then be rescaled into the new normalization frame; independently normalized pieces cannot simply be averaged.

Given previous maximum mm, sum \ell, and a new finite score tile ss: m=max(m,maxjsj),a=emm,wj=esjm,=a+jwj.m'=\max(m,\max_j s_j),\quad a=e^{m-m'},\quad w_j=e^{s_j-m'},\quad \ell'=a\ell+\sum_j w_j. Multiplying the old sum by aa changes its reference maximum without discarding earlier scores.

For two single-score tiles, 0 then ln(2), the first state is m=0, ℓ=1. The second raises m to ln(2), so a=1/2 and ℓ becomes 1/2+1=3/2. The final weights are 1/3 and 2/3. Online normalization saves an input pass; standalone softmax still needs to produce every output.

FlashAttention, by Tri Dao, Daniel Fu, Stefano Ermon, Atri Rudra, and Christopher Ré in 2022, combines tiled attention with normalization state to avoid full pairwise intermediates.

u=au+jwjvj,o=u/.u'=a u+\sum_j w_jv_j,\qquad o=u'/\ell'. Original FlashAttention uses normalized output state; uu instead accumulates unnormalized weighted value vectors vjv_j. Hold a query tile and traverse key/value tiles: compute each score tile, update m,,um,\ell,u, then discard the scores. Rescale both sums together; normalize after the final contributing tile. State need not stay on chip.
Discard each score tile after updating the running maximum, denominator and weighted-value sum. This uses the chapter’s unnormalized-u formulation; state can move between memory levels. Normalize after the last contributing tile.

Masks and work partitioning

A causal mask excludes positions a query may not see; attention visibility explains the model semantics. Unequal query and key lengths require an alignment rule. FlashAttention 2.1 documents bottom-right alignment, differing from 2.0. With five queries and two keys, its permitted-key rows are:

QueryKey 0Key 1
0MaskedMasked
1MaskedMasked
2MaskedMasked
3AllowedMasked
4AllowedAllowed

That API specifies zero outputs for the first three rows. An ordinary maximum-subtraction formula would encounter negative infinity minus negative infinity. The implementation guards this case and zero-denominator normalization. A streaming implementation must likewise preserve an empty state until valid contributions arrive. These guards do not define arbitrary NaN or positive-infinity behavior.

For sequence length NN and vector width dd per attention head, dense attention retains O(N2d)O(N^2d) arithmetic. Original FlashAttention needs linear additional memory at fixed dd, but can write running state between iterations. Exact attention permits floating-point differences.

After reducing traffic

FlashAttention-2, introduced by Tri Dao in July 2023, addressed remaining work-partitioning costs. Independent query-row tiles provide more blocks when batch size and head count are small. Assigning different query rows to warps avoids combining their partial outputs through shared memory, and delaying normalization reduces non-matrix arithmetic. The paper credits earlier Triton work for related loop reordering and sequence parallelization.

FlashAttention-3 in 2024 assigned loading to producer warps and computation to consumer warps, a division called warp specialization. It also overlapped matrix operations with softmax. To measure each technique's contribution, the paper removed them separately in an FP16 experiment using H100 80GB SXM5, non-causal attention, batch 4, sequence length 8448, 16 heads, and head dimension 128:

Attention implementationReported time
Both overlap techniques3.538 ms
Without matrix–softmax pipelining4.021 ms
Without warp specialization4.105 ms

A three-stage variant performed worse than two stages: compiler scheduling failed to create the intended overlap, while additional live intermediates increased register requirements and forced smaller tiles. These attention measurements show why adding pipeline machinery does not establish useful concurrency.

Workload variation and implementation

When indices choose the addresses

A gather, or lookup, retrieves values selected by integer indices. An embedding lookup selects rows of a representation table; embedding representations explains their meaning. Unlike dense matrix traversal, row addresses depend on input values. Distant row selections can still have coalesced accesses within each row.

Memory-level parallelism is the number of memory operations in flight. cuEmbed uses multiple row loads, vectorized accesses, and prefetched indices to increase it. Repeated popular rows can also hit caches. Requested-byte throughput may therefore exceed physical HBM bandwidth without transferring that many HBM bytes. Index distribution, row width, batch size, and rows selected per sample all belong in the workload description.

A scatter assignment writes to indexed destinations. It does not necessarily combine duplicate destinations: PyTorch's scatter contract permits nondeterministic selection of one value when indices repeat. Scatter-add instead accumulates every contribution, although floating-point accumulation order can still vary.

Repeated destinations can create contention even when dimensions remain unchanged. In Adinets's historical filtering experiment on K80 with CUDA 8.0.61 and 100×2²⁰ integers, raising the passing fraction from 5% to 50% reduced ordinary atomic-filter bandwidth from about 55 to 8 GiB/s. The bandwidth counted input/output bytes, excluding atomics. This isolates a counter-heavy pattern, not a universal scatter performance law.

Reordering can improve grouping only if identities survive. cuEmbed's backward transformation sorts table indices while carrying sample identifiers and optional weights; compressed results retain an inverse mapping to table rows. Its hotness is rows selected per sample, so variable hotness can also create uneven work. Include sorting, workspace, and restoration in the complete cost. There is no general sorting crossover independent of reuse and index distribution.

Choose the level of control

Choose an implementation layer according to the decision that needs changing. A tuned library can supply an excellent matrix primitive while a compiler improves the surrounding operation sequence. Custom code becomes useful when a consequential workload requirement or execution opportunity remains unserved.

LayerProgrammer specifiesImplementation supplies
Numerical libraryOperation, shapes, layout, arithmetic requirementsTuned implementations behind an interface
Graph compilerOperations and dependenciesLayout decisions, fusion, and generated execution
Tile-oriented kernelTile operations and work per program instanceIntra-tile mapping and compilation
Thread-oriented kernelThread ownership and explicit cooperationCompilation to supported device instructions

Tillet, Kung, and Cox's 2019 Triton paper addressed operations underserved by vendor libraries. It placed multidimensional tiles between high-level numerical expressions and explicit thread coordination. Programmers still chose tile work; compiler passes handled intra-tile parallelization and details such as coalescing and synchronization. This abstraction reduces some implementation obligations without removing shape and resource decisions.

Sometimes the useful change stays above the kernel language. Natalie Serrino's kernel-generation talk describes expressing an average-pooling workload through a better-optimized convolution primitive on Metal. Its correctness depends on the intended pooling configuration. The same talk reports a custom matrix multiplication that lost to its baseline. More low-level code does not itself establish a better execution plan.

Specialization and tuning

Specialization selects an implementation under assumptions about inputs or hardware. A guard checks whether those assumptions hold. PyTorch documents recompilation when shapes or guarded scalar values change; dynamic-shape handling can reduce some recompilations, and cached variants remain reusable when their guards pass. Splitting a graph can reduce recompilation scope while imposing execution costs. A guard establishes applicability, not custom-kernel correctness.

Autotuning measures configurations to select one for designated keys. Triton's autotuner supports pruning and cached timings, but evaluates candidates through repeated execution. In-place updates therefore need reset or restoration during tuning. Search covers supplied configurations, not every possible implementation. Treat correctness as a separate gate and count tuning time before claiming a gain.

Compiler search can explore implementation structure as well as numerical parameters. Luminal represents alternative expressions compactly in an e-graph, a structure grouping equivalent expressions, then profiles candidate implementations. Rewrites need not improve performance individually. This makes measured search useful, but leaves search cost, numerical validation, and the target workload central to the decision.

Functional portability means an implementation runs correctly on another target; performance portability means it also runs efficiently there. Compiler backends, dependent libraries, and device-specific assumptions affect both. Generated candidates still need independent validation and hardware measurements. Distribution must identify supported hardware and software versions so consumers can reproduce those conditions.

Establish the useful gain

Compare against a credible library or compiled baseline using identical shapes, layouts, arithmetic requirements, and timing boundaries. Sweep the important workload range rather than selecting its most favorable point. Sequence lengths and batch sizes can change which implementations work well; a single shape does not represent an entire model workload.

Gene Amdahl's 1967 argument emphasized work left unchanged by parallel acceleration, including sequential data management. The same issue limits kernel improvements: accelerating one component leaves the remaining workload to execute.

S=1(1f)+f/s.S=\frac{1}{(1-f)+f/s}. For fixed, nonoverlapping work, ff is the original time fraction accelerated and ss its speedup. The remaining costs stay unchanged. If f=0.4f=0.4 and s=2s=2, total time becomes 0.8 of the baseline, giving S=1.25S=1.25. Overlapping execution requires critical-path analysis instead of adding durations.

Setup must also be repaid. If specialization adds setup cost CC and saves Δt>0\Delta t>0 per call, it produces a net saving after more than C/ΔtC/\Delta t comparable calls, assuming those savings persist. Include compilation, tuning, conversion, workspace management, and additional synchronization wherever they occur in the real execution boundary.

Report timing variation and consequential regressions alongside the gain. Then remeasure the calling sequence: another kernel, a transfer, or host coordination may now dominate. Request-level responsiveness and throughput require a representative serving experiment. Adopt the implementation whose supported workload coverage and useful time saved justify its setup and maintenance obligations.

Open questions

  1. Predicting profitable fusion remains difficult because saved traffic competes with layout conflicts, longer live ranges, and compiler scheduling. Better models would reject unpromising candidates cheaply while retaining gains verified on complete workloads.

  2. Specialization must balance recurring execution savings against tuning cost and changing inputs. Progress would mean reliable selection across shape distributions with bounded setup costs, rather than a growing collection of individually impressive special cases.

  3. Reordering indexed accesses has no workload-independent payoff. Sorting and identity restoration cost time and workspace, while benefits depend on locality and repeated destinations. Useful progress would establish complete-call crossover measurements under fixed dimensions and varied index distributions.

  4. Automated kernel generation needs validation strong enough to survive unfamiliar shapes, arithmetic settings, and execution schedules. Independent references and hardware checks matter because a generator can reproduce its own mistaken assumptions. Progress would combine broader correctness coverage with measured gains against strong, versioned baselines.

Follow the curated reading path through the speakers and demonstrations behind this entry.

172 min

AI Engineer World's Fair 2024 · 2024

Low Level Technicals of LLMs

Daniel Han

Cited in this entry

Explore numerical debugging through intermediate-output comparisons, and the extra derivative work hidden behind compact forward normalization formulas.

Watch talk

Explore more talks

The rest of the library, beyond the curated path. Cited talks support this entry; reviewed transcripts were processed in full. Metadata candidates have not been reviewed as sources or verified as topic members.

9 matching talks

TalkSpeakerEventYear
Ben BurtenshawAI Engineer Europe 20262026
Mozhgan Kabiri ChimehAI Engineer Europe 20262026
Daniel HanAI Engineer World's Fair 20262026
Sayash KapoorAI Engineer Summit 20252025
Ishan AnandAI Engineer World's Fair 20242024
Daniel SzokeAI Engineer Europe 20262026
Max RyabininAI Engineer Europe 20262026
Philip Kiely, Pankaj GuptaAI Engineer World's Fair 20242024
Philipp KrennAI Engineer World's Fair 20252025

References

Coverage and source review
Processed transcripts
13 processed in full · 4 in the curated path
Automated source review
Passed
Metadata candidates
0 unreviewed; not verified topic membership
Corpus version
1bd8e407b26a07b33815594e1b2db5f41827119a2b3cb6fbf240f9fc571fc767

Automated review checks source support; it is not publication approval.

A synthesis of selected conference talks and technical references. Citations link to the source material; they do not imply that every talk on this subject is included.

  1. Roofline Performance Model — NERSC

    The Roofline model relates attainable arithmetic throughput to compute capacity, memory bandwidth, and arithmetic intensity: operations performed per byte moved at a specified memory level. Low-intensity work can be limited by moving data rather than executing arithmetic. Reuse can increase intensity by doing more work per transferred byte. Roofline is an upper performance envelope; measured execution can lie below it because of additional inefficiencies. The same workload can fall in different limiting regimes on different hardware.

  2. Triton: Matrix Multiplication

    The tutorial multiplies an M-by-K matrix by a K-by-N matrix into M-by-N output tiles. Each program accumulates successive K chunks into FP32 partial outputs, zero-fills masked K-edge loads, and masks final stores. Addresses use explicit dimension strides. Its optional activation runs before conversion to FP16, so its rounding boundary differs from applying activation to an already stored FP16 result. Autotuning varies tile dimensions, warp counts, and pipeline stages, keyed by M, N, and K. An upstream test compares a 512-by-512 FP16 case against torch.matmul with absolute tolerance 0.01 and zero relative tolerance.

  3. A Set of Level 3 Basic Linear Algebra Subprograms

    The March 1990 Level 3 BLAS paper organized reusable numerical interfaces around matrix-matrix operations. Its motivation was that vector-at-a-time algorithms often moved too much data on machines with hierarchical memory. Partitioning matrices into blocks allowed repeated calculations while operands remained in cache or local memory. Parallelism could occur both between independent blocks and within each block. BLAS means Basic Linear Algebra Subprograms: standard numerical building blocks whose implementations can be tailored to hardware while callers retain a common interface.

  4. CUDA Programming Guide: Programming Model

    The host is the CPU and its memory; the device is the GPU and its memory. A kernel is a function invoked for GPU execution. Its launch organizes threads into blocks and blocks into a grid. Each block executes on one streaming multiprocessor, or SM, whose resources support computation and cooperation. A grid can contain far more blocks than can execute simultaneously, and ordinary blocks cannot assume scheduling order or depend on another block becoming resident. CUDA groups threads into 32-thread warps. Under single-instruction, multiple-thread execution, threads follow the same kernel code but may take different branches; inactive lanes are masked while a branch executes.

  5. PyTorch: torch.Storage

    A regular tensor is a multidimensional array described by underlying storage, element type, shape, strides, and storage offset. Shape gives each dimension's size; a stride describes the step needed to advance one element along a dimension. Storage contains bytes, while this metadata determines how those bytes are interpreted. Multiple tensors can share the same storage, so distinct tensor objects do not necessarily mean independent buffers.

  6. Matrix Multiplication Background User's Guide

    GEMM computes C=αAB+βC; ordinary multiplication uses α=1 and β=0. For A shaped M×K and B shaped K×N, each of the M×N outputs combines K products, conventionally counted as 2MNK floating-point operations. The guide's ideal FP16 traffic estimate gives intensity MNK/(MK+KN+MN) operations per byte. Larger output tiles improve operand reuse but produce fewer independent blocks. Its fixed 256×128-tile A100 example uses M=2304 and K=4096: N=1536 produces 108 tiles, while N=1544 produces 117, leaving nine tiles in an additional scheduling wave. Tile quantization means incomplete output tiles; wave quantization means an incompletely occupied final group of resident blocks.

  7. OpenXLA: GPU Architecture Overview

    XLA separates logical tensor shape from physical layout. Layout assignment can represent a transpose as metadata, while conflicting layout requirements can require a copy that physically transposes data. XLA fusion groups multiple operations into one GPU kernel and passes internal intermediates through registers or shared memory instead of materializing them in high-bandwidth device memory. This establishes why an operator graph need not map one-to-one onto launches or intermediate arrays.

  8. Faster Parallel Reductions on Kepler

    The implementation first accumulates several input elements per thread in registers, then combines thread partials through warp shuffle reductions. One lane per warp stores a partial sum in shared memory; after a block barrier, the first warp reduces those partials. For larger arrays, one kernel writes one result per block and a subsequent kernel combines them. This separates ownership of intermediate outputs from the synchronization needed to consume them and reduces inter-thread communication by doing more work locally.

  9. CUDA C++ Best Practices Guide

    Kernel launches return before GPU work completes; CPU timing must account for completion. CUDA events record device timestamps when reached in a stream, and the example waits for the stop event before reading elapsed time. Extra synchronization can alter execution. Bandwidth is data transferred per unit time. Occupancy is active warps per multiprocessor divided by its supported maximum; registers, shared memory, and block size constrain residency, and higher occupancy need not improve performance. Amdahl's law bounds fixed-workload improvement by the fraction accelerated: unchanged work remains even if the optimized portion becomes arbitrarily fast.

  10. What every AI engineer needs to know about GPUs

    Concurrency hides waiting by letting other work progress, while parallelism increases how much work can progress simultaneously.

  11. What every AI engineer needs to know about GPUs

    The speaker recommends organizing GPU work to exploit growing bandwidth, because latency improvements face stronger physical limits.

  12. HIP C++ Language Extensions

    HIP calls the number of threads in an execution group warpSize; on AMD hardware this is the wavefront size. Portable code must query or specialize for the target rather than assume 32. The inspected documentation specifies 64 for gfx9 and 32 for gfx10 and later HIP targets. Lane masks and collective operations must accommodate the applicable width.

  13. Khronos: cl_khr_subgroups

    OpenCL sub-groups are implementation-controlled groups of work-items within a work-group, with their own collective operations and synchronization primitives. They may map to efficient hardware execution structures. The specification distinguishes subgroup membership from independent forward-progress guarantees, which are an optional device property in OpenCL 2.1.

  14. Brook for GPUs: Stream Computing on Graphics Hardware

    Stanford's Brook work presented at SIGGRAPH 2004 addressed the difficulty of expressing ordinary computations through graphics APIs, textures and triangles. Brook supplied streams of records, kernels applied across their elements, and reductions combining records. Its compiler and runtime mapped these abstractions onto programmable graphics hardware. Kernel bodies retained temporary values in registers instead of requiring a separate memory round trip for every elementary operation. This connected a more accessible programming interface with locality, rather than treating GPU acceleration as arithmetic throughput alone.

  15. Scalable Parallel Programming with CUDA

    Nickolls, Buck, Garland and Skadron's March/April 2008 account dates CUDA's release to 2007. It describes graphics-API limitations as motivation for a general parallel-computing interface. CUDA's central abstractions were hierarchical thread groups, shared memories and barriers: programmers decomposed problems into independently schedulable blocks, then cooperating threads within each block. Separating the logical decomposition from the number of physical processors let the runtime distribute blocks across GPUs with different available parallelism.

  16. Roofline: An Insightful Visual Performance Model for Multicore Architectures

    Williams, Waterman and Patterson's April 2009 article proposed Roofline as an understandable bound-and-bottleneck model amid increasingly diverse multicore architectures. Its operational intensity counted operations per byte reaching DRAM after cache filtering, allowing locality improvements to change the bound. The authors measured sustainable memory bandwidth with optimized microbenchmarks. Their aim was to guide improvements, rather than predict every detail of runtime.

  17. Triton: An Intermediate Language and Compiler for Tiled Neural Network Computations

    The June 2019 Triton paper addressed operations that existing vendor libraries did not efficiently support, leaving researchers dependent on expert-written kernels. It made statically shaped multidimensional tiles the programming unit. Programmers expressed tile operations and the work assigned to program instances; compiler passes handled intra-tile parallelization and details such as coalescing and synchronization. This offered a middle ground between calling a fixed numerical primitive and explicitly coordinating individual CUDA threads, while still requiring more implementation work than a high-level matrix expression.

  18. FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness

    FlashAttention organizes attention computation into tiles to reduce transfers between GPU high-bandwidth memory and on-chip SRAM. Its central optimization is reducing data movement while computing exact attention, rather than approximating attention to reduce the set of interactions. This distinguishes an implementation optimization from changing the model's attention pattern or replacing the decoding algorithm. The paper also describes a separate block-sparse extension, which is approximate and should not be confused with the exact algorithm.

  19. CUDA Programming Guide: Writing SIMT Kernels

    Global allocations persist until freed and can hold inputs and outputs across launches. Shared memory is a block-accessible, programmer-managed on-chip scratchpad; registers hold thread-private state. Local memory is thread-private but physically resides in device memory; register spilling can add device-memory writes and reads. Coalescing concerns how a warp's addresses share memory transactions, not merely whether every thread reads a value. In the documented 32-byte-segment model, aligned contiguous four-byte loads across 32 lanes require four transactions; scattered addresses can require 32. Shared memory has separate bank constraints: the transpose example's 32-by-32 float tile has column-access conflicts, removed by padding its row length to 33.

  20. GPU Performance Background User's Guide — NVIDIA

    GPU execution uses parallel processing units and a memory hierarchy. A simple performance model compares time to move the required bytes with time to perform the required arithmetic; with sufficient overlap, the slower term dominates. Arithmetic intensity is operations per byte accessed. Comparing it with the hardware's compute-to-bandwidth ratio helps identify a possible compute or memory bottleneck. But small workloads, insufficient parallelism, extra memory reads, and launch latency can invalidate the estimate. Changing batch size can change reuse and intensity, so model size and peak arithmetic throughput alone cannot predict request latency.

  21. Running LLMs locally: Practical LLM Performance on DGX Spark — Mozhgan Kabiri chimeh, NVIDIA

    Memory capacity determines whether a model fits, but data-movement efficiency remains a separate constraint on throughput.

  22. Nsight Systems User Guide

    Nsight Systems distinguishes a CPU-side CUDA API trace from GPU workload activity. The latter includes kernel executions and memory operations such as host-to-device copies, organized by GPU context and stream. This provides evidence for separating submission intervals from actual device execution and locating transfers or gaps between GPU operations.

  23. PyTorch: Tensor Views

    Creating a tensor view changes interpretation of existing storage without moving its data. A transpose can therefore be cheap to create while producing a non-contiguous tensor whose later access pattern affects performance. Calling contiguous on a non-contiguous tensor creates a contiguous copy; on an already contiguous tensor it returns the tensor itself. Views also share mutations with their base tensors.

  24. CUDA Programming Guide: Register Spilling and Shared-Memory Banks

    When thread-private state needs more registers than are available, the compiler can spill values into local memory, which physically resides in device memory; using the values later adds reads. In the documented shared-memory layout, successive 32-bit words map to successive banks. Distinct locations requested from one bank by a warp require serialized service; reading the same location can broadcast. A 32-word row stride maps column accesses to the same bank. Padding to 33 changes that mapping so the transpose example accesses different banks.

  25. CUDA C++ Programming Guide 13.0.3: Multi-Stage Asynchronous Data Copies

    An asynchronous copy immediately followed by a wait does not overlap that transfer with computation. CUDA's two-stage example allocates separate shared-memory regions, submits the next copy and computes using the preceding completed region. Producer threads acquire and commit stages; consumers wait before accessing a stage and release it after use. If every stage is occupied, acquisition blocks until consumers release resources. The example primes the first transfer and drains the last computation, making startup, steady-state overlap and completion distinct phases.

  26. NVIDIA Compute Sanitizer

    Compute Sanitizer separates memory-access checking, shared-memory race detection, initialization checks, and synchronization checks. Its block-reduction example shows a reader racing with other threads that populate shared memory; a block barrier orders those phases. A second example still races within a warp despite having a later block barrier, requiring warp-level synchronization at the earlier producer-consumer boundary. The synchronization examples include invalid divergent barrier participation.

  27. CUDA Programming Guide: C/C++ Language Extensions

    An atomic read-modify-write protects an update to its target location. CUDA's legacy atomic functions provide relaxed ordering and scoped atomicity, without introducing fences for unrelated accesses. Unsuffixed atomicAdd has device scope; block and system variants have corresponding scopes and conditions. CUDA's newer atomic interfaces permit explicit memory-order and thread-scope selection.

  28. CUDA C++ Programming Guide 13.0: Streams, Synchronization, and Events

    A CUDA stream orders its commands. Independent streams provide no general relative execution order. An event recorded in a specified stream completes after preceding work in that stream; cudaStreamWaitEvent makes later commands in a waiting stream delay until the event completes. Application to the existing split-K example: launch the combining kernel after its producers in one stream, or establish dependencies on every producer stream before combining. Merely recording one producer event does not establish completion of unrelated streams.

  29. NVIDIA: Floating Point and IEEE 754

    Finite floating-point formats cannot represent every real value exactly, so operations round their results. Addition is consequently non-associative: changing parentheses can change the rounded answer even when every operation follows IEEE 754. NVIDIA supplies a worked single-precision example with different results for two addition orders. Fused multiply-add computes a product plus an addend with one rounding step instead of rounding the product and sum separately. Its cancellation example shows that fused evaluation can retain information lost by separate operations. Infinity and NaN have reserved encodings.

  30. NVIDIA TensorRT: Accuracy Considerations

    FP32 means single-precision 32-bit floating point; FP16 is 16-bit half precision. BF16 is Brain Floating Point, a 16-bit format with a wider exponent field but fewer fraction bits than FP16. Format choice therefore changes representable range and precision. Overflow concerns results beyond a format's finite range; the guide describes FP16 overflow producing infinity and subsequent errors leading to NaN, meaning not-a-number. Storage format and the precision used for intermediate arithmetic can be selected separately rather than inferred from the input type.

  31. PyTorch: Numerical Accuracy

    Matching input dtype does not fully specify a matrix operation's numerical behavior. PyTorch documents FP16 and BF16 GEMM implementations that occasionally truncate intermediate accumulations to reduced precision, potentially producing infinity even when the final mathematical result fits. Backend controls can disable these reductions. Its attention math backend instead upcasts FP16/BF16 inputs to FP32 for intermediate computation, then downcasts outputs, trading additional memory and potentially slower execution for accuracy. Batched and individually evaluated matrix products can also differ numerically despite representing the same mathematics.

  32. PyTorch: torch.testing

    For finite real values, assert_close accepts an element when |actual−expected| ≤ atol + rtol·|expected|. Absolute tolerance therefore controls acceptance near a zero reference, while relative tolerance scales with reference magnitude. Infinities must match exactly; NaNs match only when explicitly enabled. Device, dtype, layout, and optionally strides have separate checks. A numerical closeness check therefore has both value and representation semantics.

  33. Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference

    Affine quantization represents a real value approximately as r=S(q-Z), where q is an integer, S>0 is scale, and Z represents real zero. Encoding rounds to a discrete level and clips outside the representable range. The paper uses eight-bit weights and activations with wider accumulators and integer rescaling, enabling integer matrix products instead of floating-point inference. Training simulates quantization while retaining floating-point optimization, and activation ranges are estimated during training. Accuracy depends on ranges, rounding, and quantization-aware adaptation; deployment speed depends on efficient kernels and hardware support for the resulting arithmetic.

  34. Low Level Technicals of LLMs

    Compare intermediate layer outputs against a reference implementation using a log L2 error plot, but establish the reference before automating variant searches.

  35. AI Kernel Generation: What's Working, What's Not, What's Next

    Gate optimization on compilation, execution, and correctness, then use actual hardware measurements to guide further synthesis under human supervision.

  36. PyTorch: Benchmark Utils

    PyTorch's benchmark Timer performs warmups for lazy initialization, controls CPU threadpool size, and synchronizes asynchronous accelerator work when necessary. It emphasizes repeated measurements to expose run-to-run variation and support median summaries. Statement and setup code are separate, and result labels and environment metadata help organize comparisons.

  37. AI Kernel Generation: What's Working, What's Not, What's Next

    A useful kernel benchmark must define floating-point correctness and control input size, execution timing, warm-up, and caching effects.

  38. CUDA Graphs — NVIDIA CUDA Programming Guide

    CUDA graphs represent operations as nodes and dependencies as edges. Stream capture records submitted work into a graph instead of executing it during capture. Instantiation validates and prepares an executable graph, which can then be launched repeatedly. Repeated graph launch reduces per-operation host setup and launch overhead; it does not remove the GPU computations. Capture coverage, graph updates and unsupported operations still constrain applicability.

  39. Getting Started with CUDA Graphs

    Alan Gray's September 2019 experiment kept a 500,000-element floating-point scaling kernel unchanged on Tesla V100 with CUDA 10.1 and 512 threads per block. Device execution took 2.9 microseconds. Across 1,000 iterations of 20 launches, synchronizing after every kernel produced 9.6 microseconds per kernel including overhead; synchronizing after each group reduced this to 3.8 microseconds. Replaying a graph containing the same kernels yielded 3.4 microseconds, including amortized graph setup. The approximately 400-microsecond setup cost was paid once. The improvement changed submission and synchronization, not arithmetic.

  40. Nsight Compute Profiling Guide

    Nsight Compute exposes memory requests, transactions, register-spill activity, and sampled warp states. A short-scoreboard stall can reflect shared-memory dependencies, special math, or branching, so the guide recommends checking memory evidence before diagnosing bank conflicts. A not-selected warp is eligible but another warp issued; a high count can indicate sufficient latency-hiding work. Profiling can serialize launches, replay kernels, adjust clocks, and flush caches between passes. Replayed device-memory state does not restore hardware cache contents, and disabling flushing changes reproducibility assumptions.

  41. FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning

    FlashAttention-2 addressed limitations remaining after reducing attention's off-chip traffic. Its forward pass distributes query-row tiles across independent thread blocks, providing more parallel work when batch size and head count are small. Within a block, assigning different query rows to warps avoids combining their partial outputs through shared memory. It also delays output normalization to reduce non-matrix arithmetic. Larger tiles remain a tradeoff: they reduce shared-memory accesses but increase registers and shared-memory demand; excessive register use can spill, and excessive shared-memory use can prevent launch.

  42. CUTLASS: Efficient GEMM in CUDA

    CUTLASS nests threadblock, warp, and instruction tiles to reuse operands through shared memory and registers while accumulating output regions. Larger tiles reduce repeated global fetches but can waste work at boundaries or leave too few blocks. Parallel split-K divides the dot-product reduction dimension among blocks, then combines partial outputs using workspace and a second kernel. The epilogue applies elementwise work to accumulated results and can exchange data through shared memory for efficient output stores. Software pipelining double-buffers shared-memory tiles and register fragments to overlap movement with computation. The Hopper producer-consumer design waits for empty buffers before filling them and for filled buffers before consumption.

  43. NVIDIA Launches Revolutionary Volta GPU Platform, Fueling Next Era of AI and High Performance Computing

    NVIDIA announced the Volta architecture and its first Volta processor, Tesla V100, on May 10, 2017. The announcement introduced Tensor Cores alongside CUDA cores to accelerate AI workloads.

  44. NVIDIA Tesla V100 GPU Architecture

    NVIDIA motivated Volta's Tensor Cores by growing neural-network matrix workloads. The documented hardware operation computes D=A×B+C on 4×4 matrices, with FP16 multiplicands and FP16 or FP32 accumulation matrices. Larger matrix products are assembled from these operations. CUDA 9 exposed a different programming granularity: warp-level matrix operations spanning 16×16 matrices across 32 threads. cuBLAS and cuDNN also exposed Tensor Core support. Hardware operation size, programmer-visible fragments and application matrix shape are therefore distinct.

  45. CUDA C++ Programming Guide 13.0: Warp Matrix and Shuffle Functions

    WMMA fragments distribute matrix elements across a warp using an unspecified internal mapping. mma_sync computes D=A·B+C collectively; every warp thread must participate with matching parameters. Supported shapes, input types, accumulator types, and layouts are constrained. The documented load requires 256-bit pointer alignment and leading dimensions divisible by eight half elements or four float elements. BF16 fragments require float accumulators. Separately, shuffle intrinsics exchange register values among participating lanes without shared memory; they do not supply a memory barrier.

  46. What every AI engineer needs to know about GPUs

    The talk describes prompt processing as a better fit for GPU computation than decoding, where repeatedly moving model weights can dominate.

  47. Luminal - Search-Based Deep Learning Compilers - Joe Fioti

    Fusion can remove intermediate global-memory writes and reads between dependent operations.

  48. cuBLAS: cublasLtEpilogue_t

    cuBLASLt exposes matrix-result postprocessing through epilogues. ReLU applies max(x,0) independently to each result. The bias epilogue broadcasts a packed vector whose length matches output-matrix rows across all columns. Combined modes apply bias followed by ReLU or GELU. Auxiliary variants can additionally materialize activation-related values, showing that intermediate retention depends on required outputs.

  49. FusionStitching: Boosting Execution Efficiency of Memory Intensive Computations for DL Workloads

    FusionStitching identifies conflicting memory layouts, parallelization choices and on-chip resource requirements as reasons naive fusion can slow execution. Its implementation transfers intermediates through registers for thread-local composition, warp reductions for suitable collective patterns, or shared memory for block-level composition. Fusion candidates are rejected when resource requirements cannot be satisfied or generated execution is unprofitable. Its evaluation weighs saved launches and off-chip traffic against fused-kernel execution time; complex patterns can require generating and timing a candidate rather than trusting a simple traffic model.

  50. Luminal - Search-Based Deep Learning Compilers - Joe Fioti

    Luminal performs buffer reuse after search by assigning the same storage to intermediate buffers with nonoverlapping lifetimes.

  51. Triton: Fused Softmax

    Softmax converts a row of scores into normalized exponential weights. The tutorial subtracts the row maximum, exponentiates, sums and divides. Its separate-operation example counts 5MN+2M element reads and 3MN+2M writes; the fused design reads MN elements and writes MN while retaining row intermediates on chip. It pads the computational row to a power of two, masks invalid loads with negative infinity and masks stores. The wrapper inspects register and shared-memory use to limit resident programs. An upstream test uses the irregular shape 1823×781 and compares against torch.softmax.

  52. PyTorch: torch.Tensor.scatter_add_

    Scatter-add accumulates every source contribution into its indexed destination, including contributions sharing a destination. This differs from choosing one writer's value. PyTorch separately warns that CUDA execution may be nondeterministic: a specified summation result does not imply a fixed floating-point accumulation order.

  53. CUDA Pro Tip: Optimized Filtering with Warp-Aggregated Atomics

    Warp aggregation combines participating threads' increments before one elected thread updates a shared counter, then distributes distinct output positions. In the article's K80/CUDA 8.0.61 filtering experiment on 100×2^20 integers, increasing the passing fraction from 5% to 50% reduced ordinary global-atomic bandwidth from about 55 to 8 GiB/s. The November 2017 update explains that CUDA 9 automatically applies aggregation in many cases, narrowing the manual optimization's advantage. A competing prefix-sum implementation preserves input order, while the atomic filter does not, so their performance comparison also involves different output contracts.

  54. Root Mean Square Layer Normalization

    Zhang and Sennrich's NeurIPS 2019 paper proposed RMSNorm to reduce normalization overhead by omitting mean subtraction. It rescales each component by the vector's root mean square, sqrt(sum(x_i²)/n), then applies a learned componentwise gain. Unlike LayerNorm, it does not center the vector. For a kernel, the defining shared statistic is therefore a sum of squares rather than both a mean and centered variance. The paper investigated whether this simpler operation retained useful model behavior across its tested tasks.

  55. PyTorch: RMSNorm

    PyTorch specifies RMSNorm as y_i=x_i·γ_i/sqrt(ε+sum(x_j²)/n), reducing over the trailing dimensions named by normalized_shape. Epsilon is added inside the square root for numerical stability. A single normalized dimension produces a rowwise reduction followed by rescaling; the output retains the input shape. Optional learned gains are per element of the normalized shape.

  56. Low Level Technicals of LLMs

    The speaker presents normalization as a training-stability aid but warns that implementing its backward kernel is substantially more involved than its forward computation.

  57. Online Normalizer Calculation for Softmax

    Milakov and Gimelshein's 2018 algorithm maintains a running maximum m and exponential sum d. On receiving x, it updates m'=max(m,x) and d'=d·exp(m−m')+exp(x−m'). Rescaling preserves earlier contributions when the maximum changes. Combining maximum and denominator calculation removes one input pass; producing all outputs still requires another pass. Their FP32 Tesla V100 experiment with CUDA 9.1 reported approximately 1.3× improvement over three-pass safe softmax at vector length 4000 and batch size 4000. With only ten vectors, the one-block-per-vector implementation underutilized the GPU and showed smaller gains.

  58. FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness

    FlashAttention's tiled softmax combines block maxima and exponential sums after rescaling them to a common maximum. Weighted value outputs must receive corresponding rescaling before combination. Algorithm 1 carries row maxima, normalization sums and output accumulators instead of storing the full pairwise score and probability matrices. Its theorem retains O(N²d) arithmetic for sequence length N and head dimension d, while requiring O(N) additional memory beyond inputs and output. Backward computation regenerates score tiles from saved inputs and normalization statistics.

  59. FlashAttention README: Causal-Mask and Small-Query Behavior

    FlashAttention's documented version-2.1 behavior aligns causal masks to the bottom-right corner when query and key lengths differ. Its five-query, two-key example fully masks the first three query rows and specifies zero outputs for them. Earlier version-2.0 alignment differs. The version-2.2 notes also describe splitting small-query attention across thread blocks and using a separate combining kernel, providing a concrete example of one attention operation requiring multiple kernels.

  60. FlashAttention CUDA Softmax Implementation

    The CUDA softmax helper explicitly handles a maximum of negative infinity, which can occur when every score is masked. It substitutes zero for the subtraction's maximum term to avoid negative infinity minus negative infinity producing NaN. The online update separately retains the running maximum and, with its infinity-check specialization enabled, guards the rescaling calculation. Final normalization avoids dividing by a zero denominator. These branches show why the ordinary finite-score recurrence needs additional implementation logic for empty valid rows.

  61. FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision

    The 2024 paper separates producer warps loading operand tiles from consumers computing with them, and overlaps matrix operations with softmax across iterations. On H100 80GB SXM5, its non-causal FP16 ablation uses batch 4, sequence length 8448, 16 heads and head dimension 128. Table 2 reports 3.538 ms with both techniques, 4.021 ms without matrix–softmax pipelining, and 4.105 ms without warp specialization. A three-stage variant performed worse than two stages: compiler scheduling failed to create the intended additional overlap, while extra live intermediates required more registers and smaller tiles.

  62. Accelerating Embedding Lookups with cuEmbed

    An embedding lookup retrieves table rows selected by integer indices and may combine them by summation or another operation. cuEmbed demonstrates that irregular row selection can coexist with coalesced accesses within each row: different warps need not read neighboring rows. Its implementation increases memory-level parallelism—the number of memory operations in flight—using multiple row loads per thread, vectorized accesses and prefetched indices. Published experiments compare uniform and power-law index distributions. Frequently reused rows can remain in L1/L2 caches, so the rate of processing requested embedding bytes can exceed physical HBM bandwidth without moving that many bytes through HBM.

  63. PyTorch: torch.Tensor.scatter_

    Scatter assignment writes source values to destinations selected by an index tensor. PyTorch warns that when several source values target the same destination, one value is selected nondeterministically; ordinary scatter assignment does not promise to combine them. Its gradient can also be incorrect for duplicate destinations. Index validity and duplicate-destination behavior therefore belong in the operation's contract before parallelizing or reordering writes.

  64. cuEmbed: Embedding Operations and Index Transformations

    cuEmbed defines hotness as the number of table rows looked up for each batch sample. Forward lookup groups indices by sample. Embedding-gradient accumulation instead combines contributions destined for each table row, so its backward interface requires repeated table indices to be contiguous. The supplied transformation sorts table indices while carrying corresponding sample identifiers and optional weights. Compressed outputs additionally retain an inverse mapping to original table rows. Reordering therefore changes execution grouping while preserving the identities needed to interpret contributions and outputs.

  65. Special topics in Kernels, RL, Reward Hacking in Agents

    Try Torch.Compile first and compare it with handwritten kernels on the actual workload and PyTorch version.

  66. AI Kernel Generation: What's Working, What's Not, What's Next

    The reported agent struggled with highly optimized primitives and increasing problem complexity; it was not shown to replace expert algorithm design.

  67. AI Kernel Generation: What's Working, What's Not, What's Next

    Equivalent high-level expressions can exploit better-optimized backend primitives or reduce operation launches without introducing custom kernels.

  68. PyTorch: Dealing with Recompilations

    Compiled implementations can depend on assumptions about input shapes and exact scalar values. PyTorch documents recompilation when these assumptions change, and dynamic-shape options that can reduce some recompilations. Previously compiled variants remain reusable when their guards pass; after configured limits, unmatched calls can execute eagerly instead of triggering further compilation. Splitting a large graph can reduce the amount recompiled while imposing an execution-performance cost. These mechanisms make workload variability and compilation amortization part of the implementation choice.

  69. Triton: triton.autotune

    Triton's autotuner evaluates a supplied list of kernel configurations when designated key arguments change. Configuration evaluation runs the kernel repeatedly, so an operation that updates existing values can apply those updates repeatedly during tuning. The API provides reset_to_zero and restore_value controls for this state. It also exposes configuration pruning, a benchmark function and optional disk caching of tuning timings. Selecting a configuration therefore entails real execution and setup costs, separate from subsequent steady-state calls.

  70. Luminal - Search-Based Deep Learning Compilers - Joe Fioti

    Luminal constructs equivalent kernel alternatives with simple rewrite rules, profiles candidates, and uses search to select fast implementations.

  71. Low Level Technicals of LLMs

    The speaker favors Triton as a compilation intermediary, but portability still depends on working compiler and attention-library backends.

  72. Your Coding Agent Should Do AI System Engineering

    Kernel distribution needs explicit hardware and software compatibility metadata, not just kernel source.

  73. From model weights to API endpoint with TensorRT-LLM

    Optimization profiles should reflect the intended workload because input shapes guide specialized kernel selection.

  74. Vector Search Benchmark[eting]

    Benchmarketing can select favorable scenarios and aggregate results so that one specialized optimization appears to establish general superiority.

  75. Validity of the Single Processor Approach to Achieving Large Scale Computing Capabilities

    Gene M. Amdahl's 1967 paper, written at IBM, challenged the assumption that adding parallel arithmetic units necessarily produces proportionate application gains. He emphasized sequential data-management work and irregularities such as boundaries, variable computation and storage rearrangement. His argument was that improving parallel processing alone leaves consequential work unchanged. This supplies the historical motivation for measuring the complete operation sequence after accelerating one kernel.

  76. Vector Search Benchmark[eting]

    A flawed benchmark can suggest where a system may be strong, even when its overall conclusion is unreliable.

  77. Luminal - Search-Based Deep Learning Compilers - Joe Fioti

    Luminal prepares a queue of kernels and submits them together to reduce repeated CPU dispatch delays.

  78. Low Level Technicals of LLMs

    Compare corresponding layer outputs against a reference implementation and repeat the comparison at different precisions.

  79. Why Rust is the Ideal Language for Vibe-Coding

    Tests generated after implementation may reproduce implementation details, and passing tests do not generally establish correctness for every input.

  80. What every AI engineer needs to know about GPUs

    Evaluate arithmetic work relative to memory traffic, not operation count alone: GPUs favor substantial computation on data that has already been loaded.