Skip to content

CUDA Graph + kernel-only activity: GPU memory growth and invalid timestamps after ~25M records (v0.1.2) #23

Description

@123liuziming

CUDA Graph + kernel-only activity: GPU memory grows ~1 GiB, then kernel timestamps become invalid (v0.1.2)

We reproduced invalid CUPTI kernel timestamps with the unmodified upstream v0.1.2 CUDA 13 amd64 release asset (libparcagpucupti.so.13-amd64, SHA256 cc516a1903c6249242869ce42669618594e2ad74bd7b94a4bcd5d3bb966f0185). This may cause GPU profiling to silently lose valid kernel durations under sustained CUDA Graph workloads. I have not tested current main or other CUPTI versions.

Environment and workload

  • NVIDIA A10, driver 580.126.09; CUDA 13.0, CUPTI 13.0.85.
  • vLLM 0.24.0 precompiled, Qwen2.5-Coder-1.5B-Instruct, TP=1, CUDA Graph enabled, --max-model-len 2048 --gpu-memory-utilization 0.55 --no-enable-prefix-caching.
  • 32 concurrent streaming chat requests, max_tokens=256, sustained across the threshold. The test returned 7,722 successful HTTP 200 chat responses.
  • CUDA_INJECTION64_PATH explicitly pointed to the above upstream release asset at process startup. Activity recording began before vLLM captured its CUDA Graphs. Only CUPTI_ACTIVITY_KIND_CONCURRENT_KERNEL was enabled by parcagpu; no memory activity kinds or PC sampling were enabled.
  • The vLLM process loaded only the upstream parcagpu library and one libcupti.so.13. A system bootstrap library was mapped but did not override the explicit injection path. The eBPF consumer was not needed for the timestamp reproduction.

The upstream asset requires GLIBCXX_3.4.29, which the host's default libstdc++ lacks. We supplied an existing newer libstdc++ and the existing CUPTI through a private per-process LD_LIBRARY_PATH; no system libraries or GPU driver were changed.

Direct observations

We interposed cuptiActivityGetNextRecord only to inspect each raw record before parcagpu sampling/USDT output. The wrapper resolves the actual loaded CUPTI function, calls it exactly once, and returns its result unchanged. It does not change activity kinds, buffer limits, the flush policy, or injection timing. The wrapper logged a non-null real function address; this run had zero CUPTI_ERROR_UNKNOWN errors.

forwarded=25028431 kernels=24987198 zeros=0 invalid=0 first=0
FIRST_INVALID kernel #25099665: start=1789389883393552621 end=1789389883373107821 (end < start)
forwarded=25145582 kernels=25104156 zeros=4268 invalid=4269 first=25099665
forwarded=25237846 kernels=25196268 zeros=96380 invalid=96381 first=25099665

Here zeros means both start and end are zero; invalid additionally includes either timestamp zero or end < start. GPU total memory for this process rose from approximately 12,742 MiB after startup to 13,739 MiB near the first invalid record. The data show growth to a capacity boundary, not an unbounded memory leak.

For comparison, our locally modified ABI-compatible CUDA 13 library, which also enables only concurrent-kernel activity, produced its first invalid record at #25,099,889 under the same A10/vLLM workload—only 224 records after the upstream result. This rules out our backport as a necessary cause.

An independent native CUDA/CUPTI reproducer, with no parcagpu or eBPF agent, showed the same capacity dependence on both A10 and H20: a Graph replay containing a 64 KiB device-to-device copy followed by a tiny kernel, with early CUPTI initialization and device buffer pool limit 8, stopped delivering kernel records at approximately 400,000. Enabling CUPTI_ACTIVITY_KIND_MEMCPY before Graph creation allowed 1,000,000 replays to complete; enabling MEMSET instead did not. The analogous Graph-memset case required MEMSET. Non-Graph copies, or Graph HtoD/DtoH copies, did not fail in that run. Increasing the pool limit to 16/32 delayed the stop to approximately 800,000/1,600,000 kernel records. This is a separate native symptom (records stop, rather than zero timestamps), so we do not equate their surface behavior.

Separately, when no CUPTI API is called until after cudaGraphInstantiate, a kernel-only collector observed 30 million valid kernel records on H20 with stable GPU memory. Delaying only cuptiActivityEnable after earlier CUPTI calls did not have that effect. This is a timing-dependent workaround, not a reliable general fix if new Graphs are created after attachment.

Questions

  1. Can the upstream collector handle CUDA Graph memcpy/memset nodes without accumulating unusable device activity buffers when it subscribes before Graph creation but enables only kernel activity?
  2. Is enabling the matching memory activity kinds (and discarding their records downstream) an acceptable mitigation, or is there a lower-overhead way to release/reuse the device buffers?
  3. Are there known CUPTI version or CUDA Graph API conditions under which this is already fixed? The observed device-buffer behavior may require NVIDIA's input; we have not identified a specific line in the closed-source CUPTI implementation.

The raw log excerpt, exact release-asset hash, and a standalone CUDA reproducer are available if useful. We can share a smaller sanitized reproducer directly in this issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions