← Back to list

Breaking the Memory Wall: Writing Custom CUDA Kernels with Shared Memory Tiling and Halo Boundaries…

When you are pushing millions of pixels through an attention bottleneck, high-level abstractions start to crack. Standard PyTorch is a…

Abhishek Kumar · 2026-06-02 08:43 · 4 claps · 4.4 min read
#programming #cuda #cpp #tokenization #llm
Open on Medium ↗
Wiki topics: LLM · Large Language Models OPS · LLMOps & Inference ML · Machine Learning TLS · Design Tools & Workflow 💻 · Programming 🧠 · Mental Wellness

Breaking the Memory Wall: Writing Custom CUDA Kernels with Shared Memory Tiling and Halo Boundaries for Multimodal Tokenization

When you are pushing millions of pixels through an attention bottleneck, high-level abstractions start to crack. Standard PyTorch is a phenomenal tool for research, but when scaling up multimodal tokenization pipelines, the reality of hardware physics sets in.

I recently ran into this wall while building LumenBridge, an open-source, production-grade visual tokenizer. The goal of the pipeline is conceptually simple: convert raw 2D images [Batch, 3, 224, 224] into 1D sequence tokens [Batch, 196, 768] using Strided Depthwise Separable Convolutions so they can be processed by standard LLM attention blocks.

While the overarching architecture of LumenBridge is designed to be hardware-agnostic to ensure broad utility, hitting absolute peak performance on Nvidia clusters requires bypassing PyTorch’s Python layer entirely. Standard implementations often fail silently on uncoalesced memory reads and suffer from GIL overhead during dispatch. The solution? A hybrid PyTorch/C++ pipeline routing directly to bare-metal CUDA.

Here is a deep dive into the physics of computation behind LumenBridge, focusing on how to shatter the memory wall using Shared Memory tiling and complex halo boundary synchronization.

The Bottleneck: Global VRAM Thrashing

To understand why we need custom kernels, we have to look at the memory hierarchy. In modern GPUs, compute is cheap; memory bandwidth is the true bottleneck.

When you run a standard PyTorch torch.nn.Conv2d over a high-resolution image, the GPU is primarily reading from Global Memory (VRAM). For a 3x3 convolution, standard implementations without heavy fusion will often fetch the same overlapping pixels multiple times from global VRAM.

Worse, if the memory isn’t perfectly aligned, you lose Coalesced Access. When a Warp (32 threads) requests memory, the hardware attempts to group those requests into a single transaction. If your stride breaks this contiguous access pattern, the GPU executes multiple separate reads, absolutely thrashing your VRAM bandwidth and leaving your streaming multiprocessors (SMs) starving for data.

To solve this, we need to manually move the working data into Shared Memory — the ultra-fast, user-managed L1 cache physically located on the SM.

The C++ Gateway: Hardware Guardrails

Before a single byte hits the CUDA kernel, we must guarantee the physical layout of the memory. PyTorch tensors can be views, meaning their memory might be strided or discontinuous. If we pass a non-contiguous tensor to a CUDA kernel expecting linear memory, we will read garbage data or trigger a segmentation fault.

We handle this in the C++ Operator Gateway using PyTorch’s libtorch API. This acts as our hardware guardrail:

C++

#include <torch/extension.h>

torch::Tensor lumenbridge_tokenize(torch::Tensor input, torch::Tensor weights) {
    // Hardware Guardrail: Guarantee coalesced memory access potential
    TORCH_CHECK(input.is_contiguous(), "LumenBridge Error: Input tensor must be contiguous in memory.");
    TORCH_CHECK(input.is_cuda(), "LumenBridge Error: Input must be a CUDA tensor.");

    // ... dispatch to bare-metal CUDA kernel
}

By enforcing contiguous memory at the C++ level, we guarantee that when our Thread Blocks fetch data, they can achieve perfect Coalesced Access.

The Solution: Shared Memory Tiling

Instead of each thread reading from Global Memory independently, we use collaborative loading. We divide the image into 16x16 grids. We then spin up a 2D Thread Block of exactly 256 threads (blockDim.x = 16, blockDim.y = 16).

The goal: have these 256 threads work together to load the 16x16 tile from slow Global Memory into ultra-fast Shared Memory, synchronize, and then compute the convolution.

But there is a mathematical catch: a 3x3 convolution kernel needs a 1-pixel boundary around the edge of the tile to compute the output for the border pixels.

Mastering the “Halo” Boundary Math

If our tile is 16x16, the data required to process that tile with a 3x3 kernel is actually 18x18 (a 1-pixel “halo” on all sides). We must allocate a slightly larger block of Shared Memory:

C++

// 16x16 tile + 1px halo on all sides = 18x18
__shared__ float shared_tile[18][18];

The challenge: How do 256 threads efficiently load 324 ($18 \times 18$) elements without causing heavy warp divergence?

We achieve this by having the inner 16x16 threads load the core tile, and strategically re-tasking the threads at the edges to fetch the halo cells. This allows for branchless edge processing during the actual compute phase.

C++

__global__ void depthwise_conv_kernel(const float* input, float* output, int width, int height) {
    // Allocate L1 Shared Memory
    __shared__ float shared_tile[18][18];

    // Thread indices
    int tx = threadIdx.x;
    int ty = threadIdx.y;

    // Global image coordinates
    int gx = blockIdx.x * blockDim.x + tx;
    int gy = blockIdx.y * blockDim.y + ty;
    // 1. Load the core 16x16 tile into the center of the shared memory allocation
    if (gx < width && gy < height) {
        shared_tile[ty + 1][tx + 1] = input[gy * width + gx];
    } else {
        shared_tile[ty + 1][tx + 1] = 0.0f; // Padding
    }
    // 2. Collaborative Halo Loading (Simplified for horizontal edges)
    // Threads on the top row load the top halo
    if (ty == 0 && gy > 0) {
        shared_tile[0][tx + 1] = input[(gy - 1) * width + gx];
    }
    // Threads on the bottom row load the bottom halo
    if (ty == 15 && gy < height - 1) {
        shared_tile[17][tx + 1] = input[(gy + 1) * width + gx];
    }

    // (Similar logic applies for vertical edges and the 4 corners)
    // 3. Hardware Barrier: Wait for all 256 threads to finish loading
    __syncthreads();
    // 4. Compute phase - No global memory reads!
    float sum = 0.0f;
    #pragma unroll
    for (int ky = 0; ky < 3; ++ky) {
        #pragma unroll
        for (int kx = 0; kx < 3; ++kx) {
            // Read exclusively from ultra-fast shared memory
            sum += shared_tile[ty + ky][tx + kx] * weight[ky][kx];
        }
    }

    // 5. Write back to global memory
    if (gx < width && gy < height) {
        output[gy * width + gx] = sum;
    }
}

Why __syncthreads() is Critical

The __syncthreads() intrinsic is a hardware-level barrier. If a thread races ahead and begins computing the convolution before the corner thread has finished loading its halo pixel, the convolution will multiply against garbage memory. This barrier guarantees that the entire 18x18 tile is fully materialized in Shared Memory before a single FLOP of math occurs.

By handling the halo logic during the memory fetch phase, the actual convolution loops (for ky... for kx...) execute branchlessly. No if statements are needed during the heavy math, allowing the SM to hit theoretical peak throughput.

The Takeaway

Writing high-performance AI infrastructure is no longer just about stringing together Python APIs; it is about understanding the physics of the silicon. By controlling the memory hierarchy through Shared Memory tiling, respecting Coalesced Access, and managing thread-level synchronization, we can bypass the high-level bottlenecks that slow down large-scale systems.

LumenBridge is an exploration into this boundary between hardware and software. Whether you are targeting Nvidia clusters, optimizing for Apple Silicon, or building open-source inference engines, mastering the lower levels of compute will always give you an uncompromising edge.


메타데이터
post_id
d5f0c7caf8e7
slug
breaking-the-memory-wall-writing-custom-cuda-kernels-with-shared-memory-tiling-and-halo-boundaries-d5f0c7caf8e7
url
https://medium.com/@63abhikumar/breaking-the-memory-wall-writing-custom-cuda-kernels-with-shared-memory-tiling-and-halo-boundaries-d5f0c7caf8e7
canonical_url
https://medium.com/@63abhikumar/breaking-the-memory-wall-writing-custom-cuda-kernels-with-shared-memory-tiling-and-halo-boundaries-d5f0c7caf8e7
author_url
https://medium.com/@63abhikumar
status
ok
fetched_at
2026-06-09 15:37:30