-
Notifications
You must be signed in to change notification settings - Fork 223
Expand file tree
/
Copy pathnodes.py
More file actions
163 lines (134 loc) · 6.15 KB
/
Copy pathnodes.py
File metadata and controls
163 lines (134 loc) · 6.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""Pure Python node class for cuDNN Frontend graph representation.
Simple, Pythonic design - everything stored directly on the Node.
"""
from typing import TYPE_CHECKING, Dict, List, Optional, Any
from .graph_types import NodeType, Tensor
if TYPE_CHECKING:
from ._pygraph import GraphContext
class Node:
"""A single operation node in the computation graph.
All operation-specific parameters are stored in the `params` dict,
keeping the design simple and flexible.
Attributes:
name: Operation name
node_type: Type of operation (MATMUL, POINTWISE, SDPA, etc.)
inputs: Dict mapping port names to input Tensor
outputs: Dict mapping port names to output Tensor
params: Dict of operation-specific parameters (padding, stride, mode, etc.)
compute_data_type: Data type for computation
"""
def __init__(
self,
name: str,
node_type: NodeType,
compute_data_type: Any = None,
):
self.name = name
self.node_type = node_type
self.compute_data_type = compute_data_type
self.inputs: Dict[str, Tensor] = {}
self.outputs: Dict[str, Tensor] = {}
self.params: Dict[str, Any] = {}
def __setattr__(self, name, value):
# attribute writes freeze with the owning graph; the port/param dicts
# themselves become MappingProxy views at freeze time
if getattr(self, "_frozen", False) and name != "_frozen":
raise RuntimeError(f"cannot set Node.{name}: the owning graph is frozen after lowering/planning")
object.__setattr__(self, name, value)
def validate(self) -> None:
"""Validate node configuration."""
for port_name, tensor in self.inputs.items():
if tensor is None:
raise ValueError(f"Node '{self.name}': Input '{port_name}' is None")
for port_name, tensor in self.outputs.items():
if tensor is None:
raise ValueError(f"Node '{self.name}': Output '{port_name}' is None")
if self.node_type == NodeType.MATMUL:
self._validate_matmul()
def infer_properties(self, context: "GraphContext") -> None:
"""Infer unset tensor properties from context and inputs."""
# Fill data types from context
for tensor in self.inputs.values():
if tensor and tensor.data_type is None:
tensor.data_type = context.intermediate_data_type if tensor.is_virtual else context.io_data_type
for tensor in self.outputs.values():
if tensor and tensor.data_type is None:
tensor.data_type = context.intermediate_data_type if tensor.is_virtual else context.io_data_type
# Operation-specific inference
if self.node_type == NodeType.MATMUL:
self._infer_matmul()
elif self.node_type == NodeType.POINTWISE:
self._infer_pointwise()
# structured/captured ops (norms/conv/moe/sdpa/...) infer at build time
# via their tables' per-output lambdas
def _validate_matmul(self) -> None:
"""Validate matmul dimensions: C = A @ B."""
a = self.inputs.get("A")
b = self.inputs.get("B")
if not (a and b and a.dim and b.dim):
return
if a.dim[-1] != b.dim[-2]:
raise ValueError(f"Node '{self.name}': Inner dimensions must match for matmul: " f"A{a.dim} @ B{b.dim}")
def _infer_matmul(self) -> None:
"""Infer output dims for matmul: C = A @ B."""
a = self.inputs.get("A")
b = self.inputs.get("B")
c = self.outputs.get("C")
if not (a and b and c):
return
if not c.dim and a.dim and b.dim:
# Output shape: [..., M, N] where M=A[-2], N=B[-1]
ndim = max(len(a.dim), len(b.dim))
c_dim = [1] * ndim
# Last two dims: M from A, N from B
if len(a.dim) >= 2:
c_dim[-2] = a.dim[-2]
if len(b.dim) >= 2:
c_dim[-1] = b.dim[-1]
# Broadcast batch dims (incompatible extents raise, matching numpy
# rules: equal, or one side is 1)
for i in range(ndim - 2):
a_idx = i - (ndim - len(a.dim))
b_idx = i - (ndim - len(b.dim))
a_val = a.dim[a_idx] if 0 <= a_idx < len(a.dim) - 2 else 1
b_val = b.dim[b_idx] if 0 <= b_idx < len(b.dim) - 2 else 1
if a_val != b_val and 1 not in (a_val, b_val):
raise ValueError(f"Node '{self.name}': batch dims not broadcastable: A{a.dim} vs B{b.dim}")
c_dim[i] = max(a_val, b_val)
c.dim = c_dim
if not c.stride and c.dim:
c.stride = _row_major_stride(c.dim)
def _infer_pointwise(self) -> None:
"""Infer output dims for pointwise (broadcast inputs)."""
out = self.outputs.get("OUT_0")
if not out:
return
if not out.dim:
# Right-aligned elementwise broadcast across all inputs (numpy
# rules); lower-rank operands contribute to the trailing dims.
max_dim: list = []
for tensor in self.inputs.values():
if not (tensor and tensor.dim):
continue
d = list(tensor.dim)
if len(d) > len(max_dim):
d, max_dim = max_dim, d # keep max_dim the longer one
for i in range(1, len(d) + 1): # merge right-aligned
a, b = max_dim[-i], d[-i]
if a != b and 1 not in (a, b):
raise ValueError(f"Node '{self.name}': pointwise inputs not broadcastable")
max_dim[-i] = max(a, b)
if max_dim:
out.dim = max_dim
if not out.stride and out.dim:
out.stride = _row_major_stride(out.dim)
def __repr__(self) -> str:
return f"Node({self.name!r}, {self.node_type.name})"
def _row_major_stride(dim: List[int]) -> List[int]:
"""Compute row-major (C-contiguous) strides."""
if not dim:
return []
stride = [1] * len(dim)
for i in range(len(dim) - 2, -1, -1):
stride[i] = stride[i + 1] * dim[i + 1]
return stride