Skip to content

Tell DLPack we are only reading, so probe() survives a graph capture - #506

Merged
YangXu1990uiuc merged 2 commits into
NVIDIA:developfrom
YangXu1990uiuc:yanxu/dlpack-probe-capture-safe
Aug 7, 2026
Merged

Tell DLPack we are only reading, so probe() survives a graph capture#506
YangXu1990uiuc merged 2 commits into
NVIDIA:developfrom
YangXu1990uiuc:yanxu/dlpack-probe-capture-safe

Conversation

@YangXu1990uiuc

@YangXu1990uiuc YangXu1990uiuc commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Before submitting

  • I agree to license this contribution under the terms of LICENSE.txt.
  • I ran pre-commit run and committed any formatting changes.
  • I added GitHub labels: cat-bug, mod-frost, mod-cutedsl, orig-nv-eng.

Affected area

FE OSS kernels or CuTeDSL

Summary

buffers.probe() turns a caller's buffer into (pointer, shape, strides, dtype, device) for a kernel launch. It reads metadata and does nothing else — so it never needed synchronisation. Its DLPack fallback called __dlpack__() with the default stream argument, which torch reads as prepare this tensor for a consumer on another stream and answers with record_stream. That is illegal inside a CUDA graph capture.

-    capsule = dl()
+    try:
+        capsule = dl(stream=-1)   # DLPack: caller handles sync, do no bookkeeping
+    except TypeError:             # producer predating the stream kwarg
+        capsule = dl()

Why it looked like a GDN/KDA bug

probe() prefers __cuda_array_interface__, whose typestr is numpy-flavoured. float16 is "<f2" and resolves there; bfloat16 has no numpy spelling, so torch reports raw "<V2", the lookup misses, and it falls through to DLPack.

So the failure needed three things at once — bf16 buffers, a capture, and probe() on the execute path — and only the linear-attention suites have all three:

286 failed  test_gdn_bprop_kernel.py (118), test_gdn_prefill_kernel.py (97),
            test_kda_prefill_kernel.py (63), test_gdn2_*, ops/test_gdn2_op.py

every one reporting operation failed due to a previous error during capture — an error naming neither this file nor this function. SDPA and GEMM use fp16/fp8 and take the CAI path; a bf16 SDPA capture would have hit the same thing.

Minimal reproduction, no cudnn involved:

dl()               -> capture fails
dl(stream=None)    -> capture fails
dl(stream=-1)      -> capture OK
__cuda_array_interface__ -> capture OK

Why

probe() reads metadata and launches nothing, so it has no business asking a producer to synchronise. Passing the default stream argument made torch do bookkeeping on its behalf — correct for a cross-stream handoff, illegal inside a capture, and unnecessary either way.

Related issues

Found while working on #502 (python engine dispatch cleanup); independent of it and based on current develop.

API and compatibility impact

None. buffers.probe() is internal and its return value is unchanged. stream=-1 is the DLPack-specified way to request no synchronisation; the TypeError fallback keeps producers whose __dlpack__ predates the argument working.

Testing

test/python/test_buffer_probe.py pins it on probe() directly rather than leaving the linear-attention suites to notice again — they do notice, but a hundred lines into unrelated setup and pointing at torch's stream bookkeeping. Reverting the fix turns test_probe_is_capture_safe[bfloat16] red; that was checked.

On sm100:

test/python/linear_attention          353 passed, 1769 skipped
                                      (286 failed / 67 passed on this same tree before)
+ sdpa/frost + gemm/frost + new test  4859 passed, 3892 skipped

Not addressed here

probe() has no mapping for fp8 at all — TypeError: unsupported buffer dtype (code=10, bits=8, lanes=1) — so fp8 is absent from the test. Separate gap, and I have no fp8-under-capture case to verify a fix against.

Worth knowing for anyone weighing the two protocols: CAI is not strictly cheaper-and-better. It cannot express bf16 or fp8 (fp8 raises outright), it is CUDA-only (CPU tensors and JAX arrays expose only DLPack), it carries no ownership or lifetime semantics, and torch reports version 2, which has no stream field at all. Its capture-safety here comes precisely from knowing nothing about streams. The existing CAI-first-then-DLPack order is right; only the DLPack call was under-specified.

/cc @Anerudhan

Summary by CodeRabbit

  • Bug Fixes

    • Improved CUDA buffer inspection to avoid unnecessary synchronization.
    • Added compatibility for data producers that do not support stream options.
    • Ensured buffer inspection works safely during CUDA graph capture.
  • Tests

    • Added coverage for float16, float32, and bfloat16 buffers.
    • Verified pointer, shape, strides, data type, and device reporting.

buffers.probe() turns a caller's buffer into (pointer, shape, strides, dtype,
device) for a kernel launch. It reads metadata and does nothing else, so it
never needed synchronisation -- but its DLPack fallback called __dlpack__()
with the default stream argument, and torch reads that as "prepare this tensor
for a consumer on another stream" and calls record_stream. That is illegal
during a CUDA graph capture.

Passing stream=-1 -- DLPack's "the caller handles synchronisation, do no
bookkeeping" -- says what probe() actually wants. The TypeError fallback covers
a producer whose __dlpack__ predates the argument.

Only bfloat16 reached this. probe() prefers __cuda_array_interface__, whose
typestr is numpy-flavoured: float16 is "<f2" and resolves, bfloat16 has no
numpy spelling so torch reports raw "<V2" and it falls through to DLPack. So
the failure needed bf16 buffers AND a capture AND probe() on the execute path,
which is why it looked like a GDN/KDA problem: 286 tests across
test_gdn_bprop_kernel, test_gdn_prefill_kernel and test_kda_prefill_kernel,
all reporting only "operation failed due to a previous error during capture",
an error that names neither this file nor this function. SDPA and GEMM use
fp16/fp8 and take the CAI path; bf16 SDPA under capture would have hit it too.

test_buffer_probe.py pins it directly rather than leaving it to the linear
attention suites to notice again. Reverting the fix turns
test_probe_is_capture_safe[bfloat16] red, which was checked.

fp8 is left out of the test: probe() has no mapping for it at all
("unsupported buffer dtype (code=10, bits=8)"), which is a separate gap.

On sm100: linear_attention 353 passed / 1769 skipped (was 286 failed / 67
passed on this same tree); with sdpa/frost, gemm/frost and the new test,
4859 passed / 3892 skipped.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6f7e9031-8d84-4d63-a613-e633b8b937db

📥 Commits

Reviewing files that changed from the base of the PR and between 675601f and ea103ef.

📒 Files selected for processing (1)
  • test/python/test_buffer_probe.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/python/test_buffer_probe.py

📝 Walkthrough

Walkthrough

buffers.probe now requests DLPack capsules with stream=-1, falls back for legacy producers, and has CUDA tests for metadata reporting and graph-capture safety across three dtypes.

Changes

DLPack probe behavior

Layer / File(s) Summary
Stream-free DLPack probing
python/cudnn/frost/buffers.py
probe calls __dlpack__(stream=-1) and falls back to __dlpack__() when the producer rejects the stream argument.
CUDA probe validation
test/python/test_buffer_probe.py
Tests validate pointer, shape, strides, dtype, device, and CUDA graph capture behavior for float16, float32, and bfloat16 tensors.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the DLPack change and its purpose of preserving CUDA graph capture in probe().
Description check ✅ Passed The description completes all required sections with clear scope, rationale, compatibility impact, related issue context, and detailed test results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@YangXu1990uiuc YangXu1990uiuc added cat-bug Reports of incorrect behavior, crashes, regressions, or unexpected results. mod-frost mod-cutedsl CuTeDSL kernels, generated kernels, examples, or related integration work. orig-nv-eng Reported or requested by NVIDIA engineering. labels Aug 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
python/cudnn/frost/buffers.py (1)

242-245: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Distinguish a legacy signature mismatch from a producer failure.

The except TypeError covers the complete __dlpack__ call. If a stream-aware producer raises TypeError inside its implementation, Line [245] calls dl() again. This can hide the original error and repeat producer or stream side effects.

Restrict the retry to a confirmed unsupported-keyword error. Add tests for a legacy signature and an internal TypeError. Verify whether legacy producers are intentionally outside the capture-safety guarantee because the no-argument call can still perform default stream bookkeeping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cudnn/frost/buffers.py` around lines 242 - 245, Update the __dlpack__
invocation around dl so the no-argument retry occurs only when the TypeError
confirms the stream keyword is unsupported, while propagating internal producer
TypeErrors without calling dl twice. Add coverage for both a legacy signature
and an implementation-raised TypeError, and preserve or explicitly validate the
intended capture-safety behavior for legacy producers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/python/test_buffer_probe.py`:
- Around line 58-62: Update the side-stream warmup in the test setup around
torch.cuda.Stream so it waits for the current stream before writing: call
side.wait_stream(torch.cuda.current_stream()) before t.add_(1), while preserving
the existing synchronization afterward.
- Around line 26-37: Update the BF16 parameter gating around _DTYPES and the
requires_cuda test marker to skip torch.bfloat16 when the active CUDA device
lacks native BF16 support, including compute capability major version below 8.
Keep float16 and float32 enabled whenever CUDA is available, and do not add a
cuDNN version check.

---

Nitpick comments:
In `@python/cudnn/frost/buffers.py`:
- Around line 242-245: Update the __dlpack__ invocation around dl so the
no-argument retry occurs only when the TypeError confirms the stream keyword is
unsupported, while propagating internal producer TypeErrors without calling dl
twice. Add coverage for both a legacy signature and an implementation-raised
TypeError, and preserve or explicitly validate the intended capture-safety
behavior for legacy producers.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: a45e288b-59b3-4c98-aec0-aa3504055d79

📥 Commits

Reviewing files that changed from the base of the PR and between 6b5c659 and 675601f.

📒 Files selected for processing (2)
  • python/cudnn/frost/buffers.py
  • test/python/test_buffer_probe.py

Comment thread test/python/test_buffer_probe.py
Comment thread test/python/test_buffer_probe.py
@YangXu1990uiuc

Copy link
Copy Markdown
Collaborator Author

@cudnn-ci-bot run frost

@cudnn-ci-bot

Copy link
Copy Markdown

🚀 Running mirror pipeline

Branch: cudnn-gh/pr-506-675601f
Pipeline: 61535125
Targets: frost

bfloat16 needs SM80+, but the whole module was gated on CUDA presence alone, so
on an older card the bf16 cases would have failed rather than skipped. Gated per
dtype, so float16/float32 still run there -- they take the CAI path, which is
the control for the DLPack one.

The side-stream warmup did not order the zeros before the write:
torch.cuda.synchronize() waits for both to finish but imposes no order between
them. side.wait_stream(current_stream()) does.

Both from review. Reverting the fix still turns test_probe_is_capture_safe
[bfloat16] red, which was re-checked.
@YangXu1990uiuc

Copy link
Copy Markdown
Collaborator Author

Both valid, both fixed in ea103ef4.

BF16 gating. Right — the module was gated on CUDA presence alone, so on a pre-SM80 card the bf16 cases would have failed rather than skipped. Now per-dtype:

_BF16_OK = torch.cuda.is_available() and torch.cuda.get_device_capability()[0] >= 8
_DTYPES = [torch.float16, torch.float32,
           pytest.param(torch.bfloat16, marks=pytest.mark.skipif(not _BF16_OK, ...))]

Per-dtype rather than module-level on purpose: float16 and float32 resolve through __cuda_array_interface__, so they are the control for the DLPack path bf16 takes. Losing them on an older card would leave the test proving nothing there. And agreed on no cudnn.backend_version() gate — this test never calls cuDNN.

Stream ordering. Also right, and a real hole rather than a style point: torch.zeros runs on the current stream, t.add_(1) on side, and nothing ordered them. torch.cuda.synchronize() afterwards waits for both to finish but imposes no order between them, so the warmup could have read the tensor before it was zeroed. side.wait_stream(torch.cuda.current_stream()) added.

Re-checked that the test still does its job: reverting the stream=-1 fix turns test_probe_is_capture_safe[bfloat16] red.

@Anerudhan Anerudhan added this to the Frontend 1.28.0 milestone Aug 7, 2026
@Anerudhan

Copy link
Copy Markdown
Collaborator

@cudnn-ci-bot frost

@cudnn-ci-bot

Copy link
Copy Markdown

cuDNN CI bot commands

  • @cudnn-ci-bot status: confirm the bot is up.
  • @cudnn-ci-bot check: validate this PR without launching CI.
  • @cudnn-ci-bot run <targets>: mirror this PR's head SHA and launch a pipeline.
  • @cudnn-ci-bot run help: list the targets you can name.

Only allowlisted maintainers can use @cudnn-ci-bot check or @cudnn-ci-bot run.

@YangXu1990uiuc
YangXu1990uiuc merged commit ec13987 into NVIDIA:develop Aug 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cat-bug Reports of incorrect behavior, crashes, regressions, or unexpected results. mod-cutedsl CuTeDSL kernels, generated kernels, examples, or related integration work. mod-frost orig-nv-eng Reported or requested by NVIDIA engineering.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants