NVIDIA / NVIDIA/cutlass

[BUG] __c_pointers__() causes monotonic RSS growth due to uncached ctypes allocations in kernel launch hot path

Open
#3,351 9 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

? - Needs Triage bug CuTe DSL inactive-30d
Dominant language
C++
Stars
10.5k
Forks
2.1k
Avg merge
3d 11h
Merged PRs (30d)
7

Description

Which component has the problem?

CuTe DSL

Bug Report

Describe the bug

__c_pointers__() on CUTLASS DSL scalar types (Int32, Float32, etc.) creates fresh ctypes objects on every kernel invocation. In ML training loops with frequent kernel launches, these short-lived allocations interleave with long-lived framework objects, causing memory fragmentation and monotonic process RSS growth that eventually leads to OOM.

This occurs regardless of the Python memory allocator (pymalloc, glibc malloc, or jemalloc).

The root cause is in IntegerMeta.__new__ (and similar for Float types):

def _c_pointers(self):
    c_value = getattr(ctypes, f"c_int{width}")(self.value)  # object 1
    return [ctypes.cast(ctypes.pointer(c_value),             # objects 2+3
                        ctypes.c_void_p)]

Each call creates 3 ctypes objects that are immediately discarded. In a training loop with multiple kernel calls per step, this produces hundreds to thousands of short-lived objects per step that fragment the heap.

Observed in production: ~0.3 GB/step RSS growth in large model training, leading to OOM within hundreds of steps.

Steps/Code to reproduce bug

"""
Requires: pip install nvidia-cutlass-dsl
Run with: python repro.py
Also reproducible with: PYTHONMALLOC=malloc python repro.py
"""
import os, gc, sys

def get_rss_mb():
    with open(f"/proc/{os.getpid()}/status") as f:
        for line in f:
            if line.startswith("VmRSS:"):
                return int(line.split()[1]) / 1024
    return 0

from cutlass.base_dsl.typing import Int32, Int64, Float32, Float64

SCALAR_ARGS = [
    (Int32, [64, 128, 192, 256, 512, 1024, 2048, 4096]),
    (Int64, [2048, 4096, 8192, 16384]),
    (Float32, [0.088388, 0.125, 0.0625, 1.0]),
    (Float64, [1.0, 0.5, 0.001]),
]
long_lived_pool = []

def simulate_kernel_call():
    for cls, values in SCALAR_ARGS:
        for v in values:
            cls(v).__c_pointers__()  # 3 ctypes objects created and discarded

def simulate_framework_allocs(step):
    # Varied-size long-lived objects that pin arenas
    long_lived_pool.append({f"k{step}": bytearray(37 + step % 73)})
    long_lived_pool.append(tuple(range(10 + step % 20)))
    long_lived_pool.append([None] * (5 + step % 15))
    if step % 7 == 0:
        long_lived_pool.append(bytearray(200 + step % 300))

gc.disable()
rss_start = get_rss_mb()

for step in range(1, 100001):
    for _ in range(8):  # 8 kernel calls per step
        simulate_kernel_call()
    simulate_framework_allocs(step)

    if step % 10000 == 0:
        rss = get_rss_mb()
        print(f"Step {step:>6}: RSS = {rss:.0f} MB (growth: {rss - rss_start:.0f} MB)")

print(f"\nTotal RSS growth: {get_rss_mb() - rss_start:.0f} MB")

Without fix (current behavior):

Step  10000: RSS = 1573 MB (growth: 946 MB)
Step  50000: RSS = 5357 MB (growth: 4729 MB)
Step 100000: RSS = 10082 MB (growth: 9455 MB)

With caching applied:

Step  10000: RSS = 633 MB (growth: 8 MB)
Step  50000: RSS = 662 MB (growth: 36 MB)
Step 100000: RSS = 703 MB (growth: 78 MB)

The ~78 MB growth with caching corresponds to the actual long-lived payload (~59 MB). Without caching, RSS grows 120x beyond actual data.

Expected behavior

__c_pointers__() should cache results for immutable scalar values, avoiding repeated ctypes object creation. Suggested fix:

# Integer types (in IntegerMeta.__new__):
_cptr_cache = {}

def _c_pointers(self):
    key = self.value
    cached = _cptr_cache.get(key)
    if cached is not None:
        return cached
    c_value = getattr(ctypes, f"c_int{width}")(self.value)
    result = [ctypes.cast(ctypes.pointer(c_value), ctypes.c_void_p)]
    _cptr_cache[key] = result
    return result

# Same pattern for Float32, Float64, Float16, BFloat16, TFloat32

Environment details:

  • Python 3.12
  • nvidia-cutlass-dsl (CUDA 12.9+)
  • Linux x86_64
  • Reproducible with both default pymalloc and PYTHONMALLOC=malloc

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at IntegerMeta.new and the corresponding c_pointers implementations for Float32, Float64, Float16, BFloat16, and TFloat32. Run the supplied reproduction script to establish current RSS behavior; done means repeated calls for immutable scalar values reuse cached ctypes results and the cached run avoids the reported growth.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
performance
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.