Skip to content

Four graph-API bugs, each with the regression test it needed - #546

Open
YangXu1990uiuc wants to merge 4 commits into
NVIDIA:developfrom
YangXu1990uiuc:yanxu/graph-api-bugfixes
Open

Four graph-API bugs, each with the regression test it needed#546
YangXu1990uiuc wants to merge 4 commits into
NVIDIA:developfrom
YangXu1990uiuc:yanxu/graph-api-bugfixes

Conversation

@YangXu1990uiuc

@YangXu1990uiuc YangXu1990uiuc commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Four independent defects on the public Python graph API. Each is reproducible today, each gets a test that fails without its fix, and none depends on the others.

cudnn.Graph(workspace_alloc=False) has never worked

self.__workspace = False inside class Graph writes _Graph__workspace, but __exit__ probed for it with hasattr(self, "__workspace") — a plain string literal, which Python does not name-mangle. The probe was always False, so __exit__ overwrote the sentinel with a fresh torch.empty(get_workspace_size()) every time.

A caller who asked to own the workspace still paid an allocation per Graph that nothing read — hundreds of MB for a large SDPA graph — and the "Need to specify workspace to execute graph" guard became unreachable, so forgetting workspace= ran silently on the wrapper's hidden buffer. The kwarg and the broken probe arrived in the same commit (v1.15, 2025-10).

cudnn.experimental raises RecursionError

The lazy hook fetched it with from . import experimental, the exact form the eight-line comment six lines above documents as recursive. The ops branch already avoids it with importlib.import_module; the two branches now share one body so a third submodule cannot pick the wrong form.

An environment probe aborted the plan walk instead of declining

build_plans() skips a plan whose build raises one of decline_types() and moves to the next entry, ultimately to the cuDNN backend; anything else propagates and takes down the walk. Seven probes reported "no CUDA device is visible" and "the driver did not report MaxSharedMemoryPerBlockOptin / L2CacheSize" as RuntimeError, which is neither a decline type nor caught by the engines' own build_plan handlers. A driver that does not answer a property query therefore failed planning outright instead of falling back.

Left as RuntimeError/ValueError on purpose: real driver errors, and the caller mistakes (a device ordinal that does not exist, a non-CUDA buffer).

Diagnostics pointing at the wrong thing

Two docstrings cited cudnn/frost/dispatch.py as the home of the workspace and stream contracts. That file has never existed on this branch — it lives on the gitlab side, and when FROST was cherry-picked over in #476 its responsibilities were absorbed by cudnn/engines/ while the prose came along unchanged.

Five engines reported an ImportError from a probe as "requires the Cutlass DSL" / "requires cuda.bindings". The probe raises for anything missing anywhere in the import chain, not just its target. Debugging one of these cost an import hook to find the real cause — a stray /tmp/cuda.py shadowing the cuda package, reported as "requires the Cutlass DSL: No module named 'vllm'", two contradictory halves neither of which pointed at the file.


Tests: test_wrapper_graph.py (new), test_decline_types.py (new), test_import_boundaries.py (extended). Each verified to fail with its fix stashed.

Summary by CodeRabbit

  • Bug Fixes

    • Improved workspace handling for graph execution, including reliable automatic allocation and caller-managed workspace support.
    • Clarified unsupported hardware and unavailable CUDA behavior with appropriate decline errors.
    • Improved error messages for missing CUDA and Cutlass components.
    • Improved lazy loading of top-level package features.
  • Documentation

    • Updated workspace and stream-handling documentation references.
  • Tests

    • Added coverage for lazy imports, unsupported hardware behavior, and graph workspace allocation.

YangXu1990uiuc and others added 4 commits August 10, 2026 16:36
`self.__workspace = False` inside `class Graph` writes `_Graph__workspace`, but
`__exit__` probed for it with `hasattr(self, "__workspace")` — a plain string
literal, which Python does not name-mangle. The probe was therefore always
False, and `__exit__` unconditionally overwrote the sentinel with a fresh
`torch.empty(get_workspace_size())`.

Two silent consequences. A caller who asked to own the workspace still paid an
allocation per Graph that nothing ever read — hundreds of MB for a large SDPA
graph. And the "Need to specify workspace to execute graph" guard in execute()
became unreachable, so a caller who set workspace_alloc=False and then forgot
to pass workspace= ran on the buffer the wrapper had allocated behind their
back rather than being told.

Give __workspace a class-level default the way __handle already has one, and
test the sentinel by identity. The kwarg and the broken probe arrived in the
same commit (v1.15, 2025-10), so it has never worked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cudnn.experimental` raised RecursionError. The lazy hook fetched it with
`from . import experimental`, which is exactly the form the eight-line comment
six lines earlier documents as recursive: the fromlist machinery calls
hasattr(cudnn, "experimental"), the name is not in __dict__ yet, so
__getattr__ re-enters and the cycle never terminates. The `ops` branch right
above it already avoids this with importlib.import_module.

The two branches now share one body, so a third lazy submodule cannot pick the
wrong form. Test every name __getattr__ special-cases, in a subprocess, since
the failure only reproduces on a cold module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two docstrings cited ``cudnn/frost/dispatch.py`` as the home of the workspace
and stream contracts. That file has never existed on this branch: it lives on
the gitlab side, and when FROST was cherry-picked over in NVIDIA#476 its
responsibilities were absorbed by ``cudnn/engines/`` while the prose came
along unchanged. Cite engines/base.py, which is where the contract is.

Five engines reported an ImportError from a probe as "requires the Cutlass
DSL" / "requires cuda.bindings". The probe raises for anything missing
anywhere in the import chain, not just its target, so the message asserts a
cause it did not establish. Debugging one of these today cost an import hook
to discover the real answer: a stray /tmp/cuda.py shadowing the cuda package,
reported as "requires the Cutlass DSL: No module named 'vllm'" — two
contradictory halves, neither pointing at the file. Say which import failed
and let the original exception name the missing module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
build_plans() skips a plan whose build raises one of decline_types() and moves
on to the next entry, ultimately to the cuDNN backend. Anything else propagates
and takes down the whole walk, so a graph the backend could have served fails
outright.

Seven probes reported "no CUDA device is visible" and "the driver did not
report MaxSharedMemoryPerBlockOptin / L2CacheSize" as RuntimeError. Both are
declines — this engine cannot serve the graph, another entry can — and neither
is a decline type nor caught by the engines' own build_plan handlers, which
take (NotImplementedError, ValueError). So a driver that does not answer a
property query aborted planning rather than falling back.

Left as-is on purpose: cuDeviceGet/cudaGetDevice failures (a real driver
error), and the ValueErrors for a device ordinal that does not exist or a
non-CUDA buffer (caller mistakes). Those should not be swallowed by a fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR standardizes lazy imports, changes unsupported CUDA probes to use decline exceptions, improves import failure diagnostics, and fixes graph workspace state handling. Tests cover import boundaries, decline behavior, and workspace allocation.

Changes

Runtime contracts

Layer / File(s) Summary
Lazy top-level import resolution
python/cudnn/__init__.py, test/python/test_import_boundaries.py
ops and experimental now use one lazy import path. Tests verify lazy top-level attributes in fresh subprocesses.
Decline exception and import diagnostics
python/cudnn/frost/device.py, python/cudnn/gemm/frost/tile_config.py, python/cudnn/linear_attention/*, test/python/test_decline_types.py
Unsupported device and tile probes now raise NotImplementedError. Engine support checks report specific failed imports. Tests verify supported decline types and preserved RuntimeError behavior.
Graph workspace state and execution contracts
python/cudnn/wrapper.py, python/cudnn/frost/workspace.py, python/cudnn/gemm/frost/compiler.py, test/python/test_wrapper_graph.py
Graph.__workspace now distinguishes automatic allocation, caller-managed allocation, and supplied workspace. Graph tests cover allocation and execution behavior. Documentation references the current execution components.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: orig-nv-eng, mod-frost

Suggested reviewers: anerudhan

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the defects and tests, but it omits the required checklist, affected area, compatibility impact, related issues, and exact test commands with results. Add the required template sections, complete the checklist, state compatibility impact and related issues, and list exact test commands with results.
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the four graph API fixes and their regression tests.
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

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-546-00841a0
Pipeline: 62065145
Targets: frost

@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: 1

🧹 Nitpick comments (2)
python/cudnn/linear_attention/cutile/gdn_engine.py (1)

128-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Chain the original ImportError in both cuTile engines.

Both handlers report the import failure but omit explicit exception chaining. Add from e at both sites to preserve the original cause and satisfy Ruff B904.

  • python/cudnn/linear_attention/cutile/gdn_engine.py#L128-L128: append from e to the NotImplementedError raise.
  • python/cudnn/linear_attention/cutile/kda_engine.py#L140-L140: append from e to the NotImplementedError raise.
🤖 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/linear_attention/cutile/gdn_engine.py` at line 128, Chain the
original ImportError when raising NotImplementedError in both cuTile engine
handlers: update python/cudnn/linear_attention/cutile/gdn_engine.py lines
128-128 and python/cudnn/linear_attention/cutile/kda_engine.py lines 140-140 to
use explicit exception chaining with from e.

Source: Linters/SAST tools

test/python/test_wrapper_graph.py (1)

37-57: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add tensor-dictionary coverage and backend gating.

Graph.__call_with_tensor_dict has the same workspace_alloc=False guard, but no fluent cudnn.Graph test covers it. Add omitted-workspace and supplied-workspace cases, then compare out_dict["Y"] with the matmul reference. Gate both tests for cuDNN 9.12.0 or newer because Graph rejects older backends.

🤖 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 `@test/python/test_wrapper_graph.py` around lines 37 - 57, Add cudnn.Graph
tensor-dictionary tests covering workspace_alloc=False with both omitted and
supplied workspace, invoking Graph.__call_with_tensor_dict and comparing
out_dict["Y"] against the matmul reference. Gate both tests to cuDNN 9.12.0 or
newer, matching the existing backend-version test utilities.
🤖 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_wrapper_graph.py`:
- Around line 12-26: Add a pytest.mark.skipif decorator to the fluent graph test
helper or its containing test scope, using cudnn.backend_version() < 91200 as
the condition and a clear reason. Apply it to the tests exercising
_matmul_graph, without relying on the shared CUDA assertion for skipping.

---

Nitpick comments:
In `@python/cudnn/linear_attention/cutile/gdn_engine.py`:
- Line 128: Chain the original ImportError when raising NotImplementedError in
both cuTile engine handlers: update
python/cudnn/linear_attention/cutile/gdn_engine.py lines 128-128 and
python/cudnn/linear_attention/cutile/kda_engine.py lines 140-140 to use explicit
exception chaining with from e.

In `@test/python/test_wrapper_graph.py`:
- Around line 37-57: Add cudnn.Graph tensor-dictionary tests covering
workspace_alloc=False with both omitted and supplied workspace, invoking
Graph.__call_with_tensor_dict and comparing out_dict["Y"] against the matmul
reference. Gate both tests to cuDNN 9.12.0 or newer, matching the existing
backend-version test utilities.
🪄 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: b2dad5e5-72e5-4f05-a570-fc83df54deb7

📥 Commits

Reviewing files that changed from the base of the PR and between 721122b and 00841a0.

📒 Files selected for processing (14)
  • python/cudnn/__init__.py
  • python/cudnn/frost/device.py
  • python/cudnn/frost/workspace.py
  • python/cudnn/gemm/frost/compiler.py
  • python/cudnn/gemm/frost/tile_config.py
  • python/cudnn/linear_attention/cutile/gdn_engine.py
  • python/cudnn/linear_attention/cutile/kda_engine.py
  • python/cudnn/linear_attention/frost/gdn2_engine.py
  • python/cudnn/linear_attention/frost/gdn_engine.py
  • python/cudnn/linear_attention/frost/kda_engine.py
  • python/cudnn/wrapper.py
  • test/python/test_decline_types.py
  • test/python/test_import_boundaries.py
  • test/python/test_wrapper_graph.py

Comment on lines +12 to +26
def _matmul_graph(**kwargs):
"""A 64x64 half matmul through the fluent wrapper."""
with cudnn.Graph(
handle="auto",
io_data_type=cudnn.data_type.HALF,
compute_data_type=cudnn.data_type.FLOAT,
inputs=["X", "W"],
outputs=["Y"],
**kwargs,
) as graph:
X = graph.tensor(name="X", dim=[1, 64, 64], stride=[64 * 64, 64, 1])
W = graph.tensor(name="W", dim=[1, 64, 64], stride=[64 * 64, 64, 1])
Y = graph.matmul(name="mm", A=X, B=W)
Y.set_output(True).set_name("Y")
return graph

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 \
  'torch\.cuda\.(is_available|get_device_capability)|cudnn\.backend_version|pytest\.mark\.skipif|pytestmark|support' \
  test/python || true

Repository: NVIDIA/cudnn-frontend

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- target file ---'
cat -n test/python/test_wrapper_graph.py

echo '--- conftest support and markers ---'
rg -n -C 5 \
  'backend_version|cuda\.is_available|get_device_capability|skipif|pytestmark|L0|Graph|cudnn_version|version' \
  test/python/conftest.py test/conftest.py 2>/dev/null || true

echo '--- graph test references and fixtures ---'
rg -n -C 4 \
  'test_wrapper_graph|_matmul_graph|tensor_dict|execute.*tensor|backend_version|Graph\(' \
  test/python -g '*.py' | head -n 500

Repository: NVIDIA/cudnn-frontend

Length of output: 38965


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- conftest imports and session hooks ---'
sed -n '1,180p' test/python/conftest.py

echo '--- Graph implementation and version checks ---'
rg -n -C 6 \
  'class Graph|backend_version|9\.12|91200|get_device_capability|compute_capability|workspace_alloc' \
  python/cudnn test/python -g '*.py' | head -n 800

echo '--- test marker configuration ---'
rg -n -C 4 \
  'pytest\.ini|markers|L0|L1|L2|L3|L4' \
  pytest.ini pyproject.toml setup.cfg test/python -g '*.ini' -g '*.toml' -g '*.cfg' -g '*.py' | head -n 300

Repository: NVIDIA/cudnn-frontend

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- candidate wrapper files ---'
fd -i 'wrapper|graph' . -t f | head -n 200

echo '--- Graph definitions and constraints ---'
rg -n -C 8 \
  'class Graph|workspace_alloc|backend_version|9\.12|91200|matmul' \
  python test -g '*.py' -g '*.cpp' -g '*.h' -g '*.hpp' | head -n 1000

echo '--- marker definitions ---'
find . -maxdepth 3 \( -name 'pytest.ini' -o -name 'pyproject.toml' -o -name 'setup.cfg' \) -print
rg -n -C 4 'markers|L0|L1|L2|L3|L4' \
  pytest.ini pyproject.toml setup.cfg 2>/dev/null || true

Repository: NVIDIA/cudnn-frontend

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- wrapper outline ---'
ast-grep outline python/cudnn/wrapper.py

echo '--- Graph implementation ---'
rg -n -C 12 \
  '^class Graph|def __init__|def __enter__|def __exit__|workspace_alloc|backend_version|9\.12|91200|compute_capability' \
  python/cudnn/wrapper.py

echo '--- exact version references ---'
rg -n -C 3 '9\.12|91200|9\.11|Graph.*require|requires.*Graph' \
  python test docs include -g '*.py' -g '*.md' -g '*.h' -g '*.hpp' -g '*.cpp' | head -n 300

echo '--- L0 configuration and nearby fast tests ---'
find . -maxdepth 3 \( -name 'pytest.ini' -o -name 'pyproject.toml' -o -name 'setup.cfg' \) -print
rg -n -C 3 'L0|level|fast' test/python/README* README.md pytest.ini pyproject.toml setup.cfg 2>/dev/null | head -n 300

Repository: NVIDIA/cudnn-frontend

Length of output: 15908


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- pytest levels ---'
cat -n test/python/pytest.ini | sed -n '1,140p'

echo '--- native graph support checks ---'
sed -n '390,455p' test/python/test_graph_native.py

echo '--- architecture gates for comparable graph matmul tests ---'
rg -n -C 5 \
  'Graph\(|pygraph\(|matmul\(|get_device_capability|requires SM|compute capability' \
  test/python/test_graph_native.py test/python/test_matmul_bias_relu.py test/python/test_wrapper_graph.py

echo '--- read-only invariant probe ---'
python3 - <<'PY'
from pathlib import Path
import ast

target = Path("test/python/test_wrapper_graph.py").read_text()
wrapper = Path("python/cudnn/wrapper.py").read_text()

tree = ast.parse(target)
marks = [
    node for node in ast.walk(tree)
    if isinstance(node, ast.Call)
    and isinstance(node.func, ast.Attribute)
    and isinstance(node.func.value, ast.Attribute)
    and isinstance(node.func.value.value, ast.Name)
    and node.func.value.value.id == "pytest"
    and node.func.value.attr == "mark"
]
print(f"target_has_skipif={any(getattr(node.func, 'attr', '') == 'skipif' for node in marks)}")
print(f"target_has_architecture_check={'get_device_capability' in target}")
print(f"target_has_backend_check={'backend_version' in target}")
print("wrapper_requires_91200=" + str("cudnn.backend_version() < 91200" in wrapper))
print("target_declares_L0=" + str("`@pytest.mark.L0`" in target))
PY

Repository: NVIDIA/cudnn-frontend

Length of output: 43090


Skip fluent graph tests on cuDNN versions below 9.12.0.

cudnn.Graph raises RuntimeError when cudnn.backend_version() < 91200. Add a pytest.mark.skipif for this condition. The shared CUDA assertion does not provide a skip.

🤖 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 `@test/python/test_wrapper_graph.py` around lines 12 - 26, Add a
pytest.mark.skipif decorator to the fluent graph test helper or its containing
test scope, using cudnn.backend_version() < 91200 as the condition and a clear
reason. Apply it to the tests exercising _matmul_graph, without relying on the
shared CUDA assertion for skipping.

Source: Coding guidelines

@Anerudhan

Copy link
Copy Markdown
Collaborator

No labels/conflicts/CI etc.

Moving to 1.29

@Anerudhan Anerudhan added this to the Frontend 1.29.0 milestone Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants