optimize-shared-memory-tiling
2. **Identify conflicting access patterns.** A bank conflict occurs when multiple threads in the same warp access different addresses that map to the same bank in the same memory transaction. No conflict: all threads access the same bank (broadcast). No conflict: all threads acce
Install
mkdir -p .claude/skills/optimize-shared-memory-tiling && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/17035" && unzip -o skill.zip -d .claude/skills/optimize-shared-memory-tiling && rm skill.zipInstalls to .claude/skills/optimize-shared-memory-tiling
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
2. **Identify conflicting access patterns.** A bank conflict occurs when multiple threads in the same warp access different addresses that map to the same bank in the same memory transaction. No conflict: all threads access the same bank (broadcast). No conflict: all threads acceAbout this skill
Skill: Optimize Shared Memory Tiling
Purpose
Guide the agent through designing and tuning shared memory tiling strategies for CUDA kernels, covering bank conflict analysis and elimination, tile shape selection, double buffering with async copy, occupancy tradeoffs from shared memory allocation, and the decision of when smem tiling is worth the complexity.
Use this when
- A kernel repeatedly reads the same global memory data from multiple threads and would benefit from staging through a shared memory tile (GEMM, convolution, stencil)
- Profiling shows high shared memory bank conflict rates in Nsight Compute (
l1tex__data_bank_conflicts_pipe_lsu_mem_shared) - You are designing the smem layout for a GEMM or attention tiling kernel and need to choose tile dimensions and padding
- You are adding double buffering to overlap global memory loads with computation using
cp.asyncon SM80+ - An existing kernel has smem usage that limits occupancy and needs to be restructured
Do not use this when
- The access pattern is truly random (no spatial reuse) and shared memory staging will not increase the reuse factor
- The data is small enough to fit in L1 cache across all accesses without explicit smem management: compiler-managed L1 may be sufficient
- The kernel is compute-bound and memory latency is not the bottleneck: smem tiling adds complexity without throughput benefit
- The kernel is a simple elementwise operation with no data reuse: smem staging provides no benefit
Inputs the agent should gather first
- Kernel type and access pattern: describe the data reuse structure — which threads reuse which elements, and along which dimension. E.g., in GEMM, each row of threads reuses a row of A and each column of threads reuses a column of B.
- Tile shape context: what are the thread block dimensions (BM, BN, BK) or equivalent? How many threads are in the block?
- Dtype and element size: fp32 (4 bytes), fp16 (2 bytes), int8 (1 byte) — affects how many elements map to a single bank.
- Hardware target: SM architecture. On all current NVIDIA GPUs (Kepler through Hopper): 32 banks, 4-byte bank width (by default; can be configured to 8-byte via
cudaDeviceSetSharedMemConfig). SM80+ has up to 164 KB smem per SM in some configurations. - Current smem usage: how many bytes of smem are currently allocated per block? How does this affect occupancy?
- Whether async copy is applicable: SM80+ with
cp.async, SM90 withcp.async.bulk(TMA). Is the kernel targeting those architectures?
Required reasoning process
-
Map each smem access to a bank. Bank index for a 4-byte access is
(address_in_smem_bytes / 4) % 32. For an smem arrayT smem[rows][cols], elementsmem[r][c]maps to bank(r * cols + c) * sizeof(T) / 4 % 32(assumingsizeof(T)is a multiple of 4; for fp16, adjust accordingly). -
Identify conflicting access patterns. A bank conflict occurs when multiple threads in the same warp access different addresses that map to the same bank in the same memory transaction. No conflict: all threads access the same bank (broadcast). No conflict: all threads access different banks. Conflict: k threads access k different addresses in the same bank (k-way conflict, requiring k serial transactions).
-
Compute the conflict multiplier for the specific access pattern.
- GEMM A-tile (BM x BK, row-major smem, threads access along the K dimension): for warp accessing a row of A, consecutive threads read consecutive elements of the K dimension. If BK is a multiple of 32, then threads 0..31 read banks 0..31 — no conflict. If BK = 32 and
sizeof(T) = 4, this is ideal. - GEMM B-tile (BK x BN, row-major smem, threads access down the K dimension): threads in a warp that compute different columns of C all need to read the same column of B. This causes a BK-way broadcast conflict for column-major smem access — wait, a broadcast is not a conflict. But if threads read from different rows of B (different K values) for the same column, the bank is
(k * BN + n) % 32. For n fixed, varying k with stride BN: conflicts occur when BN is a multiple of 32. Work through the specific access pattern systematically rather than guessing.
- GEMM A-tile (BM x BK, row-major smem, threads access along the K dimension): for warp accessing a row of A, consecutive threads read consecutive elements of the K dimension. If BK is a multiple of 32, then threads 0..31 read banks 0..31 — no conflict. If BK = 32 and
-
Apply smem padding to eliminate conflicts. The standard technique: add
padextra columns to the smem array, changing the stride fromBKtoBK + pad. This shifts the bank mapping for each row, breaking the periodic conflict pattern. For fp32 with 32-bank smem, adding 1 column of padding (4 bytes) shifts consecutive rows by 1 bank, typically eliminating 32-way column-access conflicts. For fp16, adding 2 columns of padding shifts by 1 bank (since 2 fp16 = 4 bytes = 1 bank). Choose the minimumpadthat eliminates the conflict. -
Verify padding after application. Re-derive the bank mapping for the padded layout and verify that no two threads in a warp access the same bank. Walk through at least: warp accessing a row (consecutive K or N access) and warp accessing a column (strided K access). Confirm that the conflict multiplier is 1 after padding.
-
Choose tile dimensions relative to warp size. The innermost loop access should map 32 consecutive threads to 32 different smem banks. This requires the innermost stride of the smem array to result in 32 distinct bank indices across 32 threads. For row-major smem with row stride of BK (or BN): if BK (or BN) >= 32 and the access is along the row direction, and dtype is fp32, consecutive threads access consecutive elements → consecutive banks → no conflict.
-
Design double buffering with
cp.async(SM80+). Allocate two smem buffers:smem_A[2][BM][BK+pad]andsmem_B[2][BK][BN+pad]. Use buffer indexping = 0, pong = 1alternating. While computing onsmem_*[ping], issue async copies intosmem_*[pong]usingcp.async:cp.async.cg.global.shared::cta [smem_ptr], [global_ptr], 16;After issuing all copies for the next tile:
cp.async.commit_group(). Before usingsmem_*[pong]for computation:cp.async.wait_group(1)(leaves at most 1 uncommitted group in flight, i.e., allows the previous group to still be pending but waits for all earlier groups). Then__syncthreads()to ensure all threads see the completed stores to smem. -
Estimate occupancy impact. Look up the smem capacity per SM for the target architecture:
- SM70 (V100): 96 KB per SM
- SM80 (A100): 164 KB per SM (with
cudaFuncSetAttributefor max smem) - SM86 (A10/A30): 100 KB per SM
- SM90 (H100): 228 KB per SM
Active blocks per SM = floor(smem_per_SM / smem_per_block). If this limits active blocks below the warp-level occupancy target, consider reducing tile size or using a different smem configuration. Request maximum smem with
cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, bytes).
-
Consider L1 cache as an alternative to explicit smem. If the access pattern has low spatial reuse but high temporal reuse (the same addresses are re-read by different thread blocks), L2 cache may serve those reads without smem tiling. For access patterns where data is reused only within a single thread block execution, explicit smem tiling is essential. For access patterns with cross-block reuse, L2 is the relevant level.
Kernel design rules
- Determine bank conflicts analytically before writing any smem access code. Do not add padding speculatively without understanding the access pattern.
- Padding must be applied to the dimension that causes the conflict — adding padding to the wrong dimension wastes smem and does not fix the conflict.
- For double buffering:
cp.async.commit_group()must be called after allcp.asyncinstructions for one tile are issued.cp.async.wait_group(N)waits until at most N groups are pending. Usingwait_group(0)before compute is correct but eliminates the overlap benefit; usewait_group(1)to keep one group in flight while computing on the other. __syncthreads()must be called aftercp.async.wait_groupand before reading from the smem buffer that was just filled. The async copy completes the DMA to smem, but__syncthreads()ensures all threads see the completion and prevents earlier-executing threads from reading stale data.- For sm_80 and later: request increased smem with
cudaFuncSetAttribute. Kernels requiring more than 48 KB per block must explicitly opt in. Failure to call this results in a launch failure. - Tile sizes must be chosen such that
BM * BK * sizeof(dtype) * 2 + BN * BK * sizeof(dtype) * 2(for double-buffered A and B) fits within the requested smem allocation.
Correctness requirements
cp.async.wait_groupplacement: the wait must occur before the computation phase that reads from the newly filled smem buffer. Placing it after the computation reads from uninitialized or stale smem.__syncthreadsafter async copy completion:cp.async.wait_groupensures the DMA is complete, but__syncthreadsis still required to ensure all threads in the block see the completion before any thread reads. These are two separate requirements.- Padding array sizing: if
smem_Ais declared as__shared__ float smem_A[BM][BK + pad], then all index computations into smem_A must useBK + padas the stride, notBK. Indexing withBKafter declaring withBK + padaccesses wrong elements. - Double buffer index alternation: the
ping/pongbuffer index must be correctly alternated on each K-loop iteration. An off-by-one in the buffer index means computing on the wrong (possibly still-in-flight) buffer. - Smem bank conflict analysis for fp16: for fp16 (2-byte elements), two consecutive elements share one bank. The bank index for
smem[i]is(i * 2) / 4 % 32 = i / 2 % 32. Two adjacent elements (i and i+1) map to the same bank ifiis even. This means warp accesses to consecutive fp16 elements in smem may cause 2-way conflicts unless addressed withhalf2vectorized smem acces
Content truncated.