Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0c23be0
feat(python): backend-agnostic native graph + Router (unification pro…
YangXu1990uiuc Jul 1, 2026
5eff162
refactor(python): trim NodeType to exercised ops; doc mixed candidate…
YangXu1990uiuc Jul 1, 2026
24b0ff8
refactor(python): drop BATCHNORM / BATCHNORM_INFERENCE from NodeType …
YangXu1990uiuc Jul 1, 2026
6641f14
refactor(python): remove conv ops from native graph (unused foundation)
YangXu1990uiuc Jul 1, 2026
7d1b7e2
docs(python): use generic 'python DSLs' for backend examples
YangXu1990uiuc Jul 1, 2026
b91c7f7
refactor(python): unify engines into one flat engine-id space (no cuD…
YangXu1990uiuc Jul 2, 2026
cbcad29
feat(python): make cudnn.pygraph engine-aware in place (transparent f…
YangXu1990uiuc Jul 2, 2026
d996a12
feat(python): native GEMM-family lowering + fix cuDNN execute path (p…
YangXu1990uiuc Jul 2, 2026
92dd12e
feat(python): reduction output-shape + SF reordering lowering (GEMM-f…
YangXu1990uiuc Jul 2, 2026
a93c78c
feat(python): native block-scale (nvfp4) lowering on Blackwell + fixe…
YangXu1990uiuc Jul 2, 2026
0858c57
feat(python): native moe_grouped_matmul lowering + parity (GEMM-famil…
YangXu1990uiuc Jul 2, 2026
fb5c9bf
fix(python): IR-uid -> C++-uid translation at execute; native rmsnorm…
YangXu1990uiuc Jul 2, 2026
d2ce901
refactor(python): Python IR owns the uid namespace end to end
YangXu1990uiuc Jul 2, 2026
6ff43ce
feat(python): full pointwise coverage — 54 ops, table-driven, mode ==…
YangXu1990uiuc Jul 2, 2026
1735ff3
feat(python): norm family via one declarative table (10 ops, generic …
YangXu1990uiuc Jul 2, 2026
e81bba0
feat(python): conv + structural ops; collapse ALL structured ops into…
YangXu1990uiuc Jul 2, 2026
80f5b33
feat(python): sdpa family via generic kwarg capture — full ~130-arg s…
YangXu1990uiuc Jul 2, 2026
5af82ce
feat(python)!: THE FLIP — cudnn.pygraph is now the Python graph class
YangXu1990uiuc Jul 2, 2026
68c6d8e
fix(python): classic validate() timing + omit unset compute_data_type
YangXu1990uiuc Jul 2, 2026
19927ed
docs(router): codify the extension contract for the future heuristics MR
YangXu1990uiuc Jul 2, 2026
cd0f5cf
fix(python): plan-selection lifecycle + registration validation (revi…
YangXu1990uiuc Jul 2, 2026
8510659
feat(python): compiled-plan engine lifecycle + ExecutionContext (revi…
YangXu1990uiuc Jul 2, 2026
d0c5e8b
fix(python): IR port direction, tensor identity ownership, parity gap…
YangXu1990uiuc Jul 2, 2026
b351498
fix(python): address coderabbit inline findings (broadcast checks, cu…
YangXu1990uiuc Jul 2, 2026
203852a
fix(python): review follow-up — replan invalidation, slot-based dispa…
YangXu1990uiuc Jul 2, 2026
83ffded
refactor(python): one-shot planning (classic conformance) + retire Na…
YangXu1990uiuc Jul 2, 2026
a5ad541
fix(python): stable two-level plan indices; land the two missed patch…
YangXu1990uiuc Jul 2, 2026
9c373d4
fix: callback graph shim for score_mod closures; serialize returns cl…
YangXu1990uiuc Jul 2, 2026
d5fc9b9
fix(python): review round 4 — explicit planning state, split plan-ind…
YangXu1990uiuc Jul 3, 2026
02d6455
fix(python): push ragged_offset_multiplier on output tensors at lowering
YangXu1990uiuc Jul 3, 2026
229528b
fix(python): push reordering_type on output tensors; consolidate outp…
YangXu1990uiuc Jul 3, 2026
d3a624f
fix(python): whole-surface freeze + output layout contract; split cuT…
YangXu1990uiuc Jul 3, 2026
401ce73
refactor(python): retire pygraph name collision; drop NativeGraph; ch…
YangXu1990uiuc Jul 3, 2026
921dcf5
refactor(python): 'cudnn' never means 'the backend' in names — both s…
YangXu1990uiuc Jul 3, 2026
8862efa
fix(python): classic-parity batch from internal CI — signatures, wrap…
YangXu1990uiuc Jul 6, 2026
7ad1610
chore: remove internal test file accidentally included
YangXu1990uiuc Jul 6, 2026
701d9fe
fix(python): renames are label writes (exempt from freeze); push outp…
YangXu1990uiuc Jul 6, 2026
f8774d8
test: skip introspection/one-shot tests when cudnn.pygraph is monkey-…
YangXu1990uiuc Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions docs/python_graph_and_execution_backends.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Python-native `cudnn.pygraph` and pluggable execution backends

## What this is

`cudnn.pygraph` is a Python-native graph class: graph structure (nodes,
tensors, op parameters) lives in Python with full introspection, and execution
dispatches through pluggable backends — python DSL engines and the cuDNN C++
backend. The C++ graph builder is internal
(`cudnn._pybind_module.backend_graph`) and is reached exclusively through
lowering.

```
cudnn.pygraph (Python IR) → create_execution_plans() → Router → routed plan list
nodes / tensors / params (route here, PlanConfig(engine_id, knobs):
fully introspectable lazy lowering) python engines + one backend entry
```

Why: python-DSL engines (CuTe-DSL / cuTile style GEMM and attention fusions)
need to *see* the graph to decide whether and how to run it. Previously that
required monkey-patching the pybind class and recording calls; now the graph
is natively introspectable and an engine is one file implementing
`BaseEngine`.

## Architecture

### Graph IR

- `graph_types.Tensor`, `nodes.Node`, `_pygraph.pygraph` — an engine-agnostic
op DAG. Input/output **port names equal the C++ pybind kwarg names**,
everywhere.
- Three declarative op mechanisms cover 100% of the C++ op surface:
`_POINTWISE_TENSOR_ARGS` (54 uniform pointwise ops; `mode` == method name),
`_STRUCTURED_OPS` (25 ops: norms, reduction, block-scale, MoE, conv,
structural — one table entry each: ports, attrs, outputs, shape-infer),
`_CAPTURED_OPS` (6 SDPA variants, ~130 kwargs: generic capture over an
explicit per-op schema carrying positional order, output-direction kwargs,
and conditional outputs). `matmul` is explicit for positional ergonomics.

### Backend contract (`engines/`)

- `BaseEngine`: `propose_plans(graph) → [PlanConfig]` (several knob configs
per engine), `build_plan(graph, plan, ctx) → CompiledPlan` (the expensive
JIT step, once per graph/plan, cached on the graph),
`CompiledPlan.execute(graph, tensor_data, ExecutionContext)` with explicit
handle/stream/workspace/overrides. Simple eager engines implement
`execute()` only.
- Every engine owns a stable `engine_id` in a reserved region
(`PYTHON_ENGINE_ID_BASE = 1 << 20`) — reproducible pinning/autotune.
- An engine declines a graph ONLY via `NotImplementedError` or
`cudnn.cudnnGraphNotSupportedError`; anything else is an engine bug and
propagates.
- `ReferenceMatmulEngine` (pure PyTorch) is the in-tree contract oracle; real
DSL engines land as separate PRs, one file each.

### Router and the two plan-index spaces

- The Router returns the routed plan list: python `PlanConfig` entries plus
AT MOST ONE backend delegating entry (`BACKEND_HEURISTIC_ENGINE_ID`). The
final output is validated regardless of Router implementation (registered
ids only, one sentinel max, never empty).
- **Routed space**: `graph.plans`, selected with `select_plan()`. Indices are
stable — the backend entry is one index forever and never expands in place.
- **Backend space**: the cuDNN backend's own plans, discovered per graph from
the lowered graph and addressed via the classic
`get_execution_plan_count()` / `*_plan_at_index()` APIs (pure delegation).
The frontend never statically enumerates backend engines — backend engine
sets vary by version and are discovered at plan time.
- Concrete backend engine configs as first-class routed entries need a typed
plan representation — heuristics/autotune follow-up scope, together with
ranking policy (the Router is pluggable at three levels: subclass,
per-graph `router=`, process-wide `default_router`).

## Key invariants

- **uid ownership**: the Python IR owns the whole uid namespace; every uid is
pushed explicitly to C++ and a post-build assertion fails loudly on
violation (C++ auto-assignment never runs for Python-built graphs — its
enumeration order is nondeterministic for multi-output ops). A user uid
landing on an auto-assigned one steals it (the holder is renumbered);
user-user collisions raise.
- **Pure-python or pure-C++**: a graph routed to a python engine never
touches C++ on the execute path; mixed construction is unsupported.
(Explicitly querying the backend plan space lowers the backend entry on
demand — that is the caller asking for the backend.)
- **One-shot planning**: a second `create_execution_plans()` raises (the
classic C++ graph never supported re-planning — it appends engine configs
by accident). Switch plans with `select_plan()`; plan differently by
building a new graph.
- **Whole-surface freeze**: after lowering/planning, every public mutation
path raises — op builders and fluent setters, direct attribute writes on
`Tensor`/`Node`/`GraphContext`, dict writes on node ports/params
(MappingProxy), in-place dim/stride edits (sealed to tuples). Inspection
stays fully readable. A mutation in the mutable window after `validate()`
invalidates the validation.
- **Output layout contract**: only USER-assigned output dim/stride are pushed
to the lowered graph; IR-inferred strides are provisional (row-major) and
the backend keeps its classic per-op layout inference (e.g. channels-last
conv). A unified layout resolver across python/cuDNN candidates belongs to
the heuristics follow-up.
- **Classic parity**: the public `cudnn.pygraph` surface behaves as before —
`cudnnGraphNotSupportedError` at `validate()`, conditional outputs return
`None`, torch dtypes/`torch.Size` accepted, ragged (THD) offsets and
multipliers on outputs, serialize/deserialize passthrough, plan queries
delegate to the lowered graph.

## Naming

- `cudnn.pygraph` — THE public graph class (Python IR), implemented in
`cudnn/_pygraph.py`.
- `cudnn._pybind_module.backend_graph` — the internal C++ builder the IR
lowers to (renamed from its pre-flip public name to avoid two things called
`pygraph`).

## Testing the backend path

The `test_native_backend_lowering.py` suite builds graphs natively, lowers,
executes on GPU, and checks numerics against torch references. Dispatch-level
assertions (`selected_engine is None`, backend plans created, lowered graph
present) prove the execution went through the cuDNN backend plan path rather
than a python engine; kernel identity below the backend API is deliberately
not asserted (kernel names are backend-internal and version-dependent).

## Follow-ups (separate MRs)

- Heuristics/ranking: pluggable Router policy + typed plan representation.
- DSL engine integration (the cuTile matmul engine lives in this track).
- Structural cleanup: lifecycle state objects, a `CudnnBackendAdapter` to
remove `selected_engine is None` branching, lowering extracted to its own
module, op-identity dedup (NodeType vs registry keys), longer-term a typed
`OpSpec` as the single per-op source for builder/validation/lowering.
16 changes: 12 additions & 4 deletions python/cudnn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def is_windows():
"data_type",
"tensor_reordering",
"heur_mode",
"pygraph",
"tensor",
"knob",
"cudnnGraphNotSupportedError",
Expand Down Expand Up @@ -108,7 +107,7 @@ def _set_data_type(


_pybind_module.tensor.set_data_type = _set_data_type
pygraph.tensor = _tensor
_pybind_module.backend_graph.tensor = _tensor


def _library_device_pointer(input_tensor):
Expand Down Expand Up @@ -194,8 +193,8 @@ def _execute_plan_at_index(
)


pygraph.execute = _execute
pygraph.execute_plan_at_index = _execute_plan_at_index
_pybind_module.backend_graph.execute = _execute
_pybind_module.backend_graph.execute_plan_at_index = _execute_plan_at_index


def load_cudnn():
Expand Down Expand Up @@ -255,6 +254,15 @@ def _dlopen_cudnn():
else:
_dlopen_cudnn()

# The graph API: a Python-native IR with pluggable execution backends. The
# public ``cudnn.pygraph`` IS the Python class; the C++ graph builder stays
# internal at ``cudnn._pybind_module.backend_graph`` and is reached only through
# lowering (a graph is pure-Python or pure-C++, never mixed). Imported before
# .graph/.wrapper, which reference cudnn.pygraph at module load.
from .graph_types import NodeType, Tensor
from ._pygraph import pygraph, GraphContext
from .nodes import Node

from .graph import graph, jit, graph_cache
from .wrapper import Graph

Expand Down
Loading