The 100KB Wall: Diagnosing Shared Memory Constraints on Blackwell SM120

Message Overview

In this message, the assistant executes a remote bash command to query GPU shared memory properties on an NVIDIA RTX PRO 6000 Blackwell Server Edition GPU. The command runs a Python script that attempts to extract shared memory limits both through PyTorch's CUDA device properties API and directly via the CUDA runtime library (libcuda.so). The script partially succeeds, revealing that each Streaming Multiprocessor (SM) on the Blackwell SM120 architecture has 102,400 bytes (100 KB) of shared memory, but the script crashes before completing the full diagnostic due to an incorrect attribute name in the PyTorch API call.

The Full Message

[assistant] [bash] ssh root@10.1.230.174 'source /root/ml-env/bin/activate && python3 -c "
import torch

props = torch.cuda.get_device_properties(0)
print(f\"Device: {props.name}\")
print(f\"SM count: {props.multi_processor_count}\")
print(f\"Shared memory per SM: {props.shared_memory_per_multiprocessor} bytes ({props.shared_memory_per_multiprocessor/1024:.1f} KB)\")
print(f\"Max shared memory per block: {props.max_shared_memory_per_block} bytes ({props.max_shared_memory_per_block/1024:.1f} KB)\")
print(f\"Max shared memory per block (opt-in): {props.max_shared_memory_per_block_optin} bytes ({props.max_shared_memory_per_block_optin/1024:.1f} KB)\")
print(f\"Compute capability: {props.major}.{props.minor}\")

# Also check via CUDA runtime
import ctypes
cuda = ctypes.cdll.LoadLibrary(\"libcuda.so\")
smem = ctypes.c_int()
# cudaDevAttrMaxSharedMemoryPerBlock = 8
# cudaDevAttrMaxSharedMemoryPerMultiprocessor = 81
# cudaDevAttrMaxSharedMemoryPerBlockOptin = 97
for attr_id, name in [(8, \"MaxSharedMemPerBlock\"), (81, \"MaxSharedMemPerSM\"), (97, \"MaxSharedMemPerBlockOptin\")]:
    val = ctypes.c_int()
    cuda.cuDeviceGetAttribute(ctypes.byref(val), attr_id, 0)
    print(f\"  {name}: {val.value} bytes ({val.value/1024:.1f} KB)\")
\"'
Traceback (most recent call last):
  File \"<string>\", line 8, in <module>
AttributeError: 'torch._C._CudaDeviceProperties' object has no attribute 'max_shared_memory_per_block'. Did you mean: 'shared_memory_per_block'?
Device: NVIDIA RTX PRO 6000 Blackwell Server Edition
SM count: 188
Shared memory per SM: 102400 bytes (100.0 KB)

Context and Motivation: Why This Message Was Written

This message sits at a critical juncture in a much larger investigation into the inference performance of the GLM-5-NVFP4 model on 8× NVIDIA RTX PRO 6000 Blackwell GPUs. The assistant had been systematically working through a performance optimization pipeline across multiple sessions spanning hardware setup, driver installation, CUDA toolkit configuration, flash-attn compilation, SGLang server deployment, and extensive benchmarking.

The immediate context is a deep-dive into why certain CUTLASS tile configurations fail to initialize during the MoE (Mixture-of-Experts) grouped GEMM kernel setup. In the preceding messages ([msg 906][msg 910]), the assistant had identified that two specific tile configurations — M128×N256 and M256×N128 — crash with kErrorInternal during gemm.initialize(). This is a critical finding because these larger tile configurations would allow better work distribution across the 188 SMs of the Blackwell GPU during the grouped GEMM operations that underpin the model's Mixture-of-Experts layers.

The assistant had traced the error to the CUTLASS kernel initialization code in the FlashInfer library's moe_gemm_tma_ws_launcher.inl file. The error occurs not during can_implement() (which passes successfully) but during the actual initialize() call — a distinction that strongly suggests a runtime shared memory allocation failure. The hypothesis was that the larger tiles require more dynamic shared memory than the SM120 architecture can provide, even after the kernel requests additional shared memory via cudaFuncSetAttribute.

This message is the direct diagnostic follow-up: the assistant needs to know the exact shared memory limits of the Blackwell SM120 architecture to confirm or refute this hypothesis. The specific numbers needed are:

  1. Shared memory per SM — the total physical shared memory available per streaming multiprocessor
  2. Max shared memory per block (default) — the baseline limit without opt-in
  3. Max shared memory per block (opt-in) — the maximum possible after requesting more via cudaFuncSetAttribute

What the Message Actually Reveals

The command executes two parallel approaches to query shared memory limits. The first uses PyTorch's torch.cuda.get_device_properties(0) API, which returns a CudaDeviceProperties object. The second uses the lower-level CUDA driver API via ctypes to call cuDeviceGetAttribute directly.

The PyTorch approach partially succeeds. It correctly prints:

The Thinking Process Visible in the Message

The assistant's reasoning is visible in the structure of the diagnostic command itself. The decision to query shared memory properties through two independent paths — PyTorch and raw CUDA driver API — reveals a methodical approach to verification. The assistant knows that PyTorch sometimes wraps CUDA properties with different naming conventions, and that the CUDA driver API provides the authoritative ground truth. The commented-out attribute IDs (# cudaDevAttrMaxSharedMemoryPerBlock = 8) show that the assistant looked up the correct CUDA attribute enumeration values before writing the script, demonstrating careful preparation.

The assistant also made a deliberate choice to include both the default and opt-in shared memory limits. This distinction is crucial because CUDA allows kernels to request additional shared memory beyond the default per-block limit (up to the per-SM maximum) using cudaFuncSetAttribute. The fact that the assistant specifically queries MaxSharedMemPerBlockOptin (attribute 97) indicates that the hypothesis being tested is: "Can we request enough opt-in shared memory to make the larger CUTLASS tiles work?" If the opt-in limit is 100 KB (same as per-SM), then the answer is no — the hardware simply doesn't have enough shared memory for the larger tiles.

Assumptions and Their Implications

The message makes several assumptions, some of which turn out to be incorrect:

  1. Assumption: PyTorch attribute names are well-known. The assistant assumed max_shared_memory_per_block was the correct attribute name, but the actual name is shared_memory_per_block. This is a minor but telling mistake — PyTorch's CUDA device properties API has evolved across versions, and attribute naming is not always consistent. The shared_memory_per_multiprocessor attribute exists without a max_ prefix, while the per-block attribute also lacks the prefix. This inconsistency caused the script to crash before reaching the CUDA runtime API calls.
  2. Assumption: The CUDA runtime library is loadable as libcuda.so. The assistant uses ctypes.cdll.LoadLibrary(&#34;libcuda.so&#34;) to access the CUDA driver API. This assumes the library is in the standard library path and that the remote machine has the necessary development libraries installed. In this case, it works, but it's a fragile approach compared to using cuda.bindings or pycuda.
  3. Assumption: The shared memory limit is the root cause of the tile initialization failure. This is the core hypothesis being tested. The assistant has already established that can_implement() passes but initialize() fails, and has narrowed the likely cause to shared memory overflow. The 100 KB finding supports this hypothesis — larger tiles like M128×N256 would require more shared memory for pipeline stages, epilogue storage, and tensor map descriptors than 100 KB allows.

Input Knowledge Required

To fully understand this message, one needs:

  1. CUTLASS tile configuration knowledge: Understanding that M×N×K tile dimensions (e.g., M128×N256) refer to the dimensions of the matrix multiplication tile processed by each CUDA block. Larger tiles improve computational efficiency but require more shared memory and registers.
  2. Blackwell SM120 architecture familiarity: Knowledge that Blackwell is NVIDIA's latest GPU architecture (post-Hopper), and that SM120 refers to the compute capability version. The 188 SM count on the RTX PRO 6000 is a key architectural detail.
  3. FlashInfer and TensorRT-LLM integration: Understanding that FlashInfer's MoE grouped GEMM kernels are built on top of TensorRT-LLM's CUTLASS-based kernel launchers, and that the moe_gemm_tma_ws_launcher.inl file contains the template instantiation and initialization logic for these kernels.
  4. CUDA shared memory hierarchy: Knowledge of the distinction between per-block shared memory (the amount a single thread block can allocate) and per-SM shared memory (the total physical SRAM available on each SM). The opt-in mechanism allows blocks to use more than the default per-block limit, up to the per-SM maximum.
  5. The GLM-5-NVFP4 model architecture: This is a Mixture-of-Experts model with 256 experts, 8 activated per token, using FP4 quantization. The per-expert GEMM operations during decode are small (batch sizes of 16–64 tokens), making them highly sensitive to kernel efficiency.

Output Knowledge Created

This message produces one concrete piece of knowledge: Blackwell SM120 has 100 KB of shared memory per SM. While seemingly a single number, this finding has cascading implications for the entire optimization effort:

  1. It confirms the tile failure hypothesis. The M128×N256 and M256×N128 CUTLASS tiles require more than 100 KB of shared memory for their pipeline stages, epilogue storage, and TMA descriptors. The initialize() failure is a direct consequence of this hardware constraint.
  2. It establishes a hard upper bound on kernel optimization. No amount of software tuning can increase the physical shared memory available on SM120. The 100 KB limit is a fundamental architectural constraint that shapes all subsequent optimization decisions.
  3. It explains the power utilization puzzle. In [msg 904], the assistant observed that GPUs draw only ~235W during inference (39% of 600W TDP). The shared memory constraint contributes to this: the GPUs cannot efficiently utilize their compute units because the kernels are bottlenecked by shared memory capacity, preventing full utilization of the FP4 tensor cores.
  4. It redirects the optimization strategy. With the larger tiles ruled out, the assistant must pursue alternative approaches: increasing effective batch size per expert (via num_continuous_decode_steps), exploring cuBLASLt FP4 paths, or investigating kernel fusion techniques that can work within the 100 KB budget.

Mistakes and Lessons

The primary mistake in this message is the incorrect PyTorch attribute name. The assistant wrote props.max_shared_memory_per_block when the correct attribute is props.shared_memory_per_block. This caused the script to crash with an AttributeError before reaching the CUDA runtime API calls that would have provided the opt-in shared memory limit.

This is a classic example of API surface fragility in the PyTorch-CUDA ecosystem. The attribute naming convention is inconsistent: shared_memory_per_multiprocessor exists (without max_), while the assistant assumed max_shared_memory_per_multiprocessor (which doesn't exist). Meanwhile, the per-block attribute is simply shared_memory_per_block, not max_shared_memory_per_block. The assistant's mental model assumed a consistent max_ prefix pattern, but the actual API doesn't follow that convention.

A more robust approach would have been to:

Broader Significance

This message exemplifies a common pattern in systems optimization work: the moment when a hypothesis meets hardware reality. The assistant had identified a promising optimization path (larger CUTLASS tiles for better SM utilization) and was systematically working through the failure analysis. The 100 KB shared memory finding is the kind of architectural constraint that cannot be worked around — it's a hard limit baked into the silicon.

The message also illustrates the importance of defensive diagnostic scripting. A single typo in an attribute name caused the script to crash before completing its mission. In a remote SSH session where each command costs tens of seconds of latency, such errors are costly. The assistant could have structured the script to be more resilient: querying all properties in a loop with try/except, or using the CUDA runtime API as the primary path with PyTorch as supplementary information.

Ultimately, this message is a snapshot of the messy reality of GPU kernel optimization. It's not glamorous — it's a Python script that crashes with an AttributeError — but it reveals a critical architectural constraint that shapes everything that follows. The 100 KB shared memory limit on Blackwell SM120 is the kind of detail that lives in obscure CUDA programming guides and forum posts, but here it becomes a lived, experienced constraint that directly impacts the performance of a production inference deployment.