Skip to content

feat: handle-less plan deserialization via DeviceProperties (cuDNN 9.8+) - #544

Merged
hwanseoc merged 6 commits into
NVIDIA:developfrom
tp5uiuc:feat/handleless-plan-deserialize
Aug 12, 2026
Merged

hwanseoc merged 6 commits into
NVIDIA:developfrom
tp5uiuc:feat/handleless-plan-deserialize

Conversation

@tp5uiuc

@tp5uiuc tp5uiuc commented Aug 10, 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: one cat-*, one or more mod-*, and one orig-* (see label list).

Affected area

  • C++ frontend API or graph construction
  • Python API or bindings

Summary

Add a Graph::deserialize(blob) overload (and pygraph.deserialize(blob) in Python) that rehydrates a serialized execution plan using a DeviceProperties descriptor instead of a cudnnHandle_t. Requires cuDNN ≥ 9.8 at compile and runtime (CUDNN_ATTR_EXECUTION_PLAN_DEVICEPROP); the API compiles on older headers but returns an error at runtime.

Why

Enables ahead-of-time compilation: a plan can be built and serialized on a GPU node at build time and deserialized later in a context where no CUDA context or cuDNN handle has been created yet — only a serialized description of the target device's properties is needed.

Related issues

None.

API and compatibility impact

New, additive only. No existing overload is changed.

  • New C++ public API: Graph::set_device_properties(shared_ptr<const DeviceProperties>) and Graph::deserialize(vector<uint8_t>, enforce_precompiled=false).
  • New Python API: pygraph(device_property=dp) + pygraph.deserialize(blob) (no handle argument).
  • Requires cuDNN ≥ 9.8 for the new path; existing handle-based overloads are unaffected on all supported versions.
  • enforce_precompiled=true rejects blobs that carry no precompiled plan (cudnn_backend_data absent).

Testing

Tested on L40S, cuDNN 9.20.0, WHEEL lane (nvidia-cudnn-cu13).

# C++ tests (includes wrong-arch discriminator, RUNTIME_COMPILATION note assert,
#            enforce_precompiled, shared devprop concurrency, numerical oracle)
./build/bin/tests "[serialize]"
# All tests passed (641 assertions in 14 test cases)

./build/bin/samples "[serialization]"
# 42 assertions passed (3 passed, 1 skipped)

# Python tests (wrong-arch discriminator + numerical oracle)
cd test/python && pytest test_deviceless_aot_compilation.py -v
# 3 passed

# Compile-only checks
cmake -DCMAKE_CXX_FLAGS="-DCUDNN_VERSION=90700" ... && cmake --build build --target tests
# pre-9.8 guard: builds cleanly

cmake -DCUDNN_FRONTEND_SKIP_JSON_LIB=ON ... && g++ ... test/cpp/check_skip_json_lib.cpp
# SKIP_JSON_LIB: compiles cleanly

Summary by CodeRabbit

  • New Features

    • Added handle-less graph and execution-plan deserialization using serialized device properties.
    • Graphs can be prepared without a cuDNN handle and executed later with an externally supplied handle.
    • Added support across C++ and Python APIs, including device-property validation and precompiled-plan handling.
  • Bug Fixes

    • Improved fresh Python graph deserialization and validation of mismatched device architectures.
  • Documentation

    • Expanded deviceless AOT compilation guidance with workflow requirements, API examples, cuDNN availability, and runtime coverage.

tp5uiuc and others added 2 commits August 10, 2026 15:42
Add a new Graph::deserialize(blob) overload and supporting API layers that
allow callers to rehydrate a serialized execution plan without holding a
cudnnHandle_t. Instead, a DeviceProperties descriptor (backed by the
CUDNN_ATTR_EXECUTION_PLAN_DEVICEPROP attribute introduced in cuDNN 9.8) is
supplied at graph-construction time and forwarded through the builder chain.

The key motivation is ahead-of-time compilation workflows: a plan can be
built and serialized on a GPU node at build time, then deserialized in a
context where no CUDA context or cuDNN handle exists yet — only a descriptor
of the target device's properties is needed.

Changes by layer:

Layer 1 – ExecutionPlanBuilder_v8 (cudnn_frontend_ExecutionPlan.h)
  • New setDeviceProperties() builder method.
  • loadFromJson() conditionally sets CUDNN_ATTR_EXECUTION_PLAN_DEVICEPROP
    when device_properties is non-null (guarded by #if CUDNN_VERSION >= 90800).
  • #else path emits an explicit NOT_SUPPORTED error when device_properties
    is set but headers are older than 9.8, avoiding a confusing null-handle
    error.
  • detail::get_backend_version() < 90800 runtime guard for new-header /
    old-runtime combinations.

Layer 2 – Execution_plan_list / plans.h
  • New build_plans(std::shared_ptr<const DeviceProperties>, string)
    overload via create_cudnn_execution_plan_impl.

Layer 3 – Graph::deserialize (graph_interface.h)
  • New deserialize(std::vector<uint8_t> const&, bool enforce_precompiled)
    overload; routes through deserialize_plan_impl with device_properties
    when no handle is given.
  • Existing handle overloads explicitly pass device_prop=nullptr so the
    graph's stored device_properties is not consulted (Decision 7).

Layer 4 – Python bindings
  • pygraph.h/.cpp: store device_properties in PyGraph; select devprop vs
    handle path in PyGraph::deserialize.
  • _pygraph.py: fresh-container path now forwards name, kernel_cache,
    device_property, and handle only (not datatypes) to avoid changing
    classic-deserialize behaviour for existing callers.

Layer 5 – Tests, sample, docs
  • test/cpp/serialize.cpp: 14 test cases including the wrong-arch
    discriminator (proves devprop is load-bearing), RUNTIME_COMPILATION
    behavior-note assertion, enforce_precompiled, and no-devprop guard.
  • test/cpp/check_skip_json_lib.cpp: compile-only check for
    CUDNN_FRONTEND_SKIP_JSON_LIB builds.
  • test/python/test_deviceless_aot_compilation.py: 3 tests including
    wrong-arch discriminator, numerical correctness oracle, and
    cudnn.backend_version() >= 90800 hard assertion.
  • samples/cpp/misc/deviceless_aot_compilation.cpp: updated sample.
  • docs/deviceless-ahead-of-time-compilation.md: distinguish handle-free
    deserialization from device-free operation (RTC rehydration still
    requires a compatible GPU/CUDA environment; execution requires a handle).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…yGraph

Two correctness fixes found during adversarial review:

1. pygraph.h: The original constructor used `else if (handle_.has_value())`
   under the device_properties branch, silently discarding any explicitly
   supplied handle whenever a DeviceProperties was also provided. Change
   to an independent `if` (with `else if (device_properties == nullptr)`
   guarding owned-handle creation) so that callers who supply both — e.g.
   device_properties for handle-less deserialize and a handle for later
   execution — do not lose the handle.

2. plans.h: Add an explicit null guard at the top of the
   build_plans(shared_ptr<const DeviceProperties>, string) overload so
   that a null device_properties passed directly to that function returns
   a clean ATTRIBUTE_NOT_SET error rather than propagating into
   loadFromJson and emitting a confusing 'set either handle or devprop'
   message.

All C++ serialize tests (641 assertions in 14 test cases) and Python
deviceless tests (3 passed) continue to pass after these changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tp5uiuc
tp5uiuc marked this pull request as draft August 10, 2026 23:22
@coderabbitai

coderabbitai Bot commented Aug 10, 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: 955bcd4f-232b-44a1-91a3-20055de6fa23

📥 Commits

Reviewing files that changed from the base of the PR and between 92fc82d and 4786383.

📒 Files selected for processing (2)
  • include/cudnn_frontend_ExecutionPlan.h
  • test/python/test_deviceless_aot_compilation.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • include/cudnn_frontend_ExecutionPlan.h
  • test/python/test_deviceless_aot_compilation.py

📝 Walkthrough

Walkthrough

The PR adds handleless UBJSON plan deserialization with configured device properties. It updates execution-plan construction, C++ and Python graph integration, sample code, documentation, and serialization tests.

Changes

Deviceless AOT deserialization

Layer / File(s) Summary
Device-property plan construction
include/cudnn_frontend_ExecutionPlan.h, include/cudnn_frontend/plans.h
Execution-plan creation accepts DeviceProperties, validates cuDNN support, and sets CUDNN_ATTR_EXECUTION_PLAN_DEVICEPROP.
Handleless graph deserialization
include/cudnn_frontend/graph_interface.h
Graph deserialization supports binary plans without a handle, requires device properties, and skips warmup when no handle is available.
C++ and Python workflow integration
python/pygraph/*, python/cudnn/_pygraph.py, samples/cpp/misc/deviceless_aot_compilation.cpp
Python graph state preserves device properties and selects the appropriate deserialization overload. The C++ sample executes a deserialized graph with a handle supplied at execution time.
Coverage and documentation
test/cpp/serialize.cpp, test/python/test_deviceless_aot_compilation.py, docs/deviceless-ahead-of-time-compilation.md
Tests cover architecture validation, plan-only blobs, handle precedence, concurrent deserialization, and Python execution. Documentation adds phase requirements and API examples.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant Graph
  participant ExecutionPlanBuilder_v8
  participant cuDNN
  Application->>Graph: Deserialize binary plan with DeviceProperties
  Graph->>ExecutionPlanBuilder_v8: Load serialized plan
  ExecutionPlanBuilder_v8->>cuDNN: Set device-property descriptor
  cuDNN-->>ExecutionPlanBuilder_v8: Return execution plan
  Application->>Graph: Execute with cuDNN handle
  Graph->>cuDNN: Execute plan
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: handle-less plan deserialization through DeviceProperties for cuDNN 9.8 and newer.
Description check ✅ Passed The description includes all required sections, explains the API and compatibility impact, and provides specific testing commands and results.
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.

Comment thread docs/deviceless-ahead-of-time-compilation.md Outdated
Comment thread docs/deviceless-ahead-of-time-compilation.md Outdated
Comment on lines +40 to +43
Note: the overload accepts `std::vector<uint8_t>` specifically. Passing `std::vector<char>`,
`std::string`, or a string literal converts via nlohmann's implicit constructor and enters the
structural-graph `deserialize(json)` overload instead.

@tp5uiuc tp5uiuc Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is a bit risky from an API perspective. I considered the graph->deserialize(device_properties, data) API as well, but I was unsure if that jives with the cuDNN-FE API as there would be two ways of doing the same thing. i.e. via graph->set_device_properties() and graph->deserialize(...).

If we instead do the following in graph->deserialize(...)

error_t deserialize((std::shared_ptr<const DeviceProperties> devprop, std::vector<uint8_t> const &data, bool const enforce_precompiled = false) {
    this->set_device_properties(devprop);
    // same rest of the code
}

then we will be overriding previously set device properties.

Comment thread docs/deviceless-ahead-of-time-compilation.md Outdated
Comment thread include/cudnn_frontend_ExecutionPlan.h Outdated
Comment thread python/pygraph/pygraph.cpp
Comment thread samples/cpp/misc/deviceless_aot_compilation.cpp Outdated
Comment thread test/cpp/check_skip_json_lib.cpp Outdated
Comment thread test/cpp/serialize.cpp Outdated
tp5uiuc and others added 2 commits August 10, 2026 18:08
- docs: remove RTC/rehydration terminology, remove vector<uint8_t> type
  note; revert wording changes to existing text
- include/cudnn_frontend_ExecutionPlan.h: wrap devprop descriptor in a
  block scope to break consecutive-assignment alignment
- python/pygraph/pygraph.cpp: fix inaccurate comment on else branch
- samples/cpp/misc/deviceless_aot_compilation.cpp: restore original
  handle-based step 3; add handle-less devprop path as new step 4
- test/cpp/check_skip_json_lib.cpp: remove file
- test/cpp/serialize.cpp: remove DeviceBuf struct and all test sections
  that call graph.execute(); trim RTC section to behavior-note check only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the post-hoc behavior-note check with an explicit
select_behavior_notes({RUNTIME_COMPILATION}) call before build, so
the test guarantees an RTC engine was selected rather than hoping the
heuristic picks one.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tp5uiuc
tp5uiuc marked this pull request as ready for review August 11, 2026 02:55

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

🧹 Nitpick comments (1)
test/python/test_deviceless_aot_compilation.py (1)

176-178: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Expose the handle-less state in the test.

The current binding does not expose handle or is_handle_owner, so an eager internal handle allocation would not fail this test. Add a binding-visible test seam or an equivalent assertion that detects handle creation for pygraph(device_property=dp).

🤖 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_deviceless_aot_compilation.py` around lines 176 - 178,
Update the test around graph_deser.deserialize and pygraph(device_property=dp)
to assert the deserialized graph remains handle-less, using a binding-visible
handle or ownership-state seam (or an equivalent observable assertion). Ensure
an eagerly allocated internal cuDNN handle causes the test to fail.
🤖 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 `@docs/deviceless-ahead-of-time-compilation.md`:
- Around line 31-33: Update the example around the cuDNN handle creation and
graph_deser->execute call: validate the result of cudnnCreate, capture the
execution status from execute, destroy handle afterward, then require the
captured status.

In `@include/cudnn_frontend_ExecutionPlan.h`:
- Around line 329-340: Update the loadFromJson execution-plan attribute
selection around m_execution_plan.handle so CUDNN_ATTR_EXECUTION_PLAN_DEVICEPROP
is used only when device properties are configured and the handle is nullptr;
preserve the explicit handle path when both are present. Revise
setDeviceProperties documentation to state that device properties apply only
without a handle, and that an explicit handle takes precedence.

In `@test/python/test_deviceless_aot_compilation.py`:
- Around line 166-167: Update the pytest.raises assertion around
graph_wrong.deserialize(blob) to accept only cudnn.cudnnGraphNotSupportedError
or RuntimeError, and require the exception message to match the
architecture-mismatch text.

---

Nitpick comments:
In `@test/python/test_deviceless_aot_compilation.py`:
- Around line 176-178: Update the test around graph_deser.deserialize and
pygraph(device_property=dp) to assert the deserialized graph remains
handle-less, using a binding-visible handle or ownership-state seam (or an
equivalent observable assertion). Ensure an eagerly allocated internal cuDNN
handle causes the test to fail.
🪄 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: 240009b0-1021-4821-874d-d9f3ec748c87

📥 Commits

Reviewing files that changed from the base of the PR and between 11c16ff and ff9fdeb.

📒 Files selected for processing (10)
  • docs/deviceless-ahead-of-time-compilation.md
  • include/cudnn_frontend/graph_interface.h
  • include/cudnn_frontend/plans.h
  • include/cudnn_frontend_ExecutionPlan.h
  • python/cudnn/_pygraph.py
  • python/pygraph/pygraph.cpp
  • python/pygraph/pygraph.h
  • samples/cpp/misc/deviceless_aot_compilation.cpp
  • test/cpp/serialize.cpp
  • test/python/test_deviceless_aot_compilation.py

Comment thread docs/deviceless-ahead-of-time-compilation.md
Comment thread include/cudnn_frontend_ExecutionPlan.h
Comment thread test/python/test_deviceless_aot_compilation.py Outdated
- ExecutionPlan.h: guard CUDNN_ATTR_EXECUTION_PLAN_DEVICEPROP path with
  m_execution_plan.handle == nullptr; update doc comment to match
- docs: validate cudnnCreate, destroy handle after execute in C++ snippet
- test/python: narrow pytest.raises(Exception) to
  (cudnnGraphNotSupportedError, RuntimeError) for wrong-arch test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tp5uiuc

tp5uiuc commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

@cudnn-ci-bot run

@cudnn-ci-bot

Copy link
Copy Markdown
usage: @cudnn-ci-bot run <targets>

targets (comma-separated):
  backend         downstream backend CI
  frost           FROST engine tests
  multi_gpu       multi-GPU smoke test
  oss             open-source kernel tests
  pycudnn         Python binding tests
  python_samples  Python samples
  python_tests    Python test suite
  none            nothing optional, just the standard pipeline

examples:
  @cudnn-ci-bot run python_tests
  @cudnn-ci-bot run python_samples,oss
  @cudnn-ci-bot run none

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

@Anerudhan
Anerudhan requested a review from hwanseoc August 11, 2026 21:38
@Anerudhan Anerudhan added mod-frontend cuDNN frontend APIs, operation graph construction, plans, and user-facing wrappers. orig-external Reported or requested by an external user, customer, or community contributor. cat-enhancements labels Aug 11, 2026
@Anerudhan Anerudhan added this to the Frontend 1.28.0 milestone Aug 11, 2026
@Anerudhan

Copy link
Copy Markdown
Collaborator

@cudnn-ci-bot backend

@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.

@hwanseoc

Copy link
Copy Markdown
Member

@cudnn-ci-bot run backend

@cudnn-ci-bot

Copy link
Copy Markdown

🚀 Running mirror pipeline

Branch: cudnn-gh/pr-544-92fc82d
Pipeline: 62218003
Targets: backend

@hwanseoc hwanseoc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

CI passed and is clean.

Also built on a B200 and ran

  • tests "[serialize]" — 14 cases
  • the serialization samples (incl. Deviceless compilation) — 4 cases
  • test_deviceless_aot_compilation.py + test_kernel_cache.py

against cuDNN 9.24 (C++) / 9.20 (Python) and all seems clean.

Requires two oneline changes but should be good after these are addressed.

Comment thread test/python/test_deviceless_aot_compilation.py Outdated
Comment thread include/cudnn_frontend_ExecutionPlan.h Outdated
…t.raises, drop unused import

- ExecutionPlan.h: add `&& m_execution_plan.handle == nullptr` to the
  pre-9.8 #else branch (line 529) to mirror the >= 9.8 condition at
  line 503; when a handle is also set, fall through to the handle path
  instead of erroring
- test_deviceless_aot_compilation.py: narrow pytest.raises to
  cudnn.cudnnGraphNotSupportedError with match="NOT_SUPPORTED"; drop
  RuntimeError catch-all and unused `import re`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@hwanseoc
hwanseoc self-requested a review August 12, 2026 19:54
@hwanseoc

Copy link
Copy Markdown
Member

@cudnn-ci-bot run backend

@cudnn-ci-bot

Copy link
Copy Markdown

🚀 Running mirror pipeline

Branch: cudnn-gh/pr-544-4786383
Pipeline: 62394405
Targets: backend

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

Labels

cat-enhancements mod-frontend cuDNN frontend APIs, operation graph construction, plans, and user-facing wrappers. orig-external Reported or requested by an external user, customer, or community contributor.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants