calc-flow-python 4.0.0__cp313-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calc_flow/__init__.py +169 -0
- calc_flow/_native.pyd +0 -0
- calc_flow/_native.pyi +366 -0
- calc_flow/array.py +1324 -0
- calc_flow/capabilities.py +775 -0
- calc_flow/config.py +219 -0
- calc_flow/errors.py +25 -0
- calc_flow/join_spec.py +156 -0
- calc_flow/pipeline.py +1123 -0
- calc_flow/py.typed +0 -0
- calc_flow/runtime.py +979 -0
- calc_flow/store.py +138 -0
- calc_flow/symbolic/__init__.py +65 -0
- calc_flow/symbolic/_generated_rolling_kernels.py +23 -0
- calc_flow/symbolic/analyzer.py +2280 -0
- calc_flow/symbolic/domains.py +77 -0
- calc_flow/symbolic/errors.py +58 -0
- calc_flow/symbolic/expr.py +662 -0
- calc_flow/symbolic/lower/__init__.py +31 -0
- calc_flow/symbolic/lower/planners.py +1270 -0
- calc_flow/symbolic/lower/program.py +840 -0
- calc_flow/symbolic/lower/segments.py +836 -0
- calc_flow/symbolic/lower/strategies.py +1472 -0
- calc_flow/symbolic/nodes.py +603 -0
- calc_flow/symbolic/ops.py +1155 -0
- calc_flow/symbolic/optimizer.py +600 -0
- calc_flow/symbolic/program.py +377 -0
- calc_flow/symbolic/types.py +110 -0
- calc_flow/symbolic/windows.py +153 -0
- calc_flow/udf.py +19 -0
- calc_flow_python-4.0.0.dist-info/METADATA +376 -0
- calc_flow_python-4.0.0.dist-info/RECORD +35 -0
- calc_flow_python-4.0.0.dist-info/WHEEL +4 -0
- calc_flow_python-4.0.0.dist-info/licenses/LICENSE +202 -0
- calc_flow_python-4.0.0.dist-info/sboms/calc-flow-python.cyclonedx.json +10081 -0
calc_flow/array.py
ADDED
|
@@ -0,0 +1,1324 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import math
|
|
5
|
+
import operator
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Never
|
|
10
|
+
|
|
11
|
+
from calc_flow import _native
|
|
12
|
+
from calc_flow.capabilities import (
|
|
13
|
+
CapabilityRule,
|
|
14
|
+
ProviderArrayRules,
|
|
15
|
+
ProviderOption,
|
|
16
|
+
ProviderOptionsSchema,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from calc_flow.pipeline import Runtime
|
|
21
|
+
|
|
22
|
+
_MAX_AST_NODES = 128
|
|
23
|
+
_MAX_AST_DEPTH = 24
|
|
24
|
+
_MAX_EXPRESSION_LENGTH = 4096
|
|
25
|
+
_MAX_INTEGER_MAGNITUDE = 2**63 - 1
|
|
26
|
+
_MAX_POWER_EXPONENT_MAGNITUDE = 64
|
|
27
|
+
_MAX_RESHAPE_RANK = 16
|
|
28
|
+
_MAX_RESHAPE_DIMENSION = 1_000_000
|
|
29
|
+
_MAX_RESHAPE_ELEMENTS = 10_000_000
|
|
30
|
+
_MAX_OPERATION_ELEMENTS = 10_000_000
|
|
31
|
+
_TABLE_MATMUL_INPUT_PORTS = (("table", "table"), ("weights", "array"))
|
|
32
|
+
_TABLE_MATMUL_OUTPUT_PORTS = (("output", "array"),)
|
|
33
|
+
_SYMBOLIC_MATRIX_INPUT_PORTS = (("input", "table"), ("weights", "array"))
|
|
34
|
+
_SYMBOLIC_MATRIX_OUTPUT_PORTS = (("output", "table"),)
|
|
35
|
+
_EXPRESSION_OPTIONS_SCHEMA = ProviderOptionsSchema(
|
|
36
|
+
fields=(ProviderOption("expression", "string", required=True),)
|
|
37
|
+
)
|
|
38
|
+
_NUMPY_STREAM_ARRAY_RULES = ProviderArrayRules(
|
|
39
|
+
supported_dtypes=(
|
|
40
|
+
"bool",
|
|
41
|
+
"complex128",
|
|
42
|
+
"complex64",
|
|
43
|
+
"float32",
|
|
44
|
+
"float64",
|
|
45
|
+
"int16",
|
|
46
|
+
"int32",
|
|
47
|
+
"int64",
|
|
48
|
+
"int8",
|
|
49
|
+
"uint16",
|
|
50
|
+
"uint32",
|
|
51
|
+
"uint64",
|
|
52
|
+
"uint8",
|
|
53
|
+
),
|
|
54
|
+
safe_dtype_rule=CapabilityRule("array_api_safe_dtype", "1"),
|
|
55
|
+
shape_rules=(CapabilityRule("elementwise_broadcast", "1"),),
|
|
56
|
+
)
|
|
57
|
+
_JAX_STREAM_ARRAY_RULES = ProviderArrayRules(
|
|
58
|
+
supported_dtypes=(
|
|
59
|
+
"bool",
|
|
60
|
+
"complex64",
|
|
61
|
+
"float32",
|
|
62
|
+
"int16",
|
|
63
|
+
"int32",
|
|
64
|
+
"int8",
|
|
65
|
+
"uint16",
|
|
66
|
+
"uint32",
|
|
67
|
+
"uint8",
|
|
68
|
+
),
|
|
69
|
+
safe_dtype_rule=CapabilityRule("array_api_safe_dtype", "1"),
|
|
70
|
+
shape_rules=(CapabilityRule("elementwise_broadcast", "1"),),
|
|
71
|
+
)
|
|
72
|
+
_NUMPY_MATRIX_STREAM_ARRAY_RULES = ProviderArrayRules(
|
|
73
|
+
supported_dtypes=_NUMPY_STREAM_ARRAY_RULES.supported_dtypes,
|
|
74
|
+
safe_dtype_rule=_NUMPY_STREAM_ARRAY_RULES.safe_dtype_rule,
|
|
75
|
+
shape_rules=(
|
|
76
|
+
CapabilityRule("elementwise_broadcast", "1"),
|
|
77
|
+
CapabilityRule("table_matmul_static_rhs", "1"),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
_JAX_MATRIX_STREAM_ARRAY_RULES = ProviderArrayRules(
|
|
81
|
+
supported_dtypes=_JAX_STREAM_ARRAY_RULES.supported_dtypes,
|
|
82
|
+
safe_dtype_rule=_JAX_STREAM_ARRAY_RULES.safe_dtype_rule,
|
|
83
|
+
shape_rules=(
|
|
84
|
+
CapabilityRule("elementwise_broadcast", "1"),
|
|
85
|
+
CapabilityRule("table_matmul_static_rhs", "1"),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
_ALLOWED_BINARY = {
|
|
90
|
+
ast.Add: operator.add,
|
|
91
|
+
ast.Sub: operator.sub,
|
|
92
|
+
ast.Mult: operator.mul,
|
|
93
|
+
ast.Div: operator.truediv,
|
|
94
|
+
ast.MatMult: operator.matmul,
|
|
95
|
+
ast.Pow: operator.pow,
|
|
96
|
+
}
|
|
97
|
+
_ALLOWED_UNARY = {ast.UAdd: operator.pos, ast.USub: operator.neg}
|
|
98
|
+
_UNARY_FUNCTIONS = {"sum", "mean", "max", "min", "transpose"}
|
|
99
|
+
_ALLOWED_FUNCTIONS = _UNARY_FUNCTIONS | {"reshape"}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _array_error(message: str) -> ValueError:
|
|
103
|
+
return ValueError(f"invalid array expression: {message}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _parse_expression(expression: object) -> ast.Expression:
|
|
107
|
+
if not isinstance(expression, str) or not expression.strip():
|
|
108
|
+
raise _array_error("expression must be a non-empty string")
|
|
109
|
+
if len(expression) > _MAX_EXPRESSION_LENGTH:
|
|
110
|
+
raise _array_error(
|
|
111
|
+
f"expression length limit is {_MAX_EXPRESSION_LENGTH} characters"
|
|
112
|
+
)
|
|
113
|
+
return _parse_valid_expression(expression)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@lru_cache(maxsize=256)
|
|
117
|
+
def _parse_valid_expression(expression: str) -> ast.Expression:
|
|
118
|
+
try:
|
|
119
|
+
parsed = ast.parse(expression, mode="eval")
|
|
120
|
+
except (SyntaxError, ValueError) as error:
|
|
121
|
+
raise _array_error("syntax is invalid") from error
|
|
122
|
+
if not isinstance(parsed, ast.Expression):
|
|
123
|
+
raise _array_error("syntax is invalid")
|
|
124
|
+
nodes = list(ast.walk(parsed))
|
|
125
|
+
if len(nodes) > _MAX_AST_NODES:
|
|
126
|
+
raise _array_error(f"node limit is {_MAX_AST_NODES}")
|
|
127
|
+
if _ast_depth(parsed) > _MAX_AST_DEPTH:
|
|
128
|
+
raise _array_error(f"depth limit is {_MAX_AST_DEPTH}")
|
|
129
|
+
_validate_node(parsed.body)
|
|
130
|
+
return parsed
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _ast_depth(node: ast.AST) -> int:
|
|
134
|
+
children = list(ast.iter_child_nodes(node))
|
|
135
|
+
if not children:
|
|
136
|
+
return 1
|
|
137
|
+
return 1 + max(_ast_depth(child) for child in children)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _validate_node(node: ast.AST) -> None:
|
|
141
|
+
if isinstance(node, ast.Name):
|
|
142
|
+
if node.id != "x":
|
|
143
|
+
raise _array_error(f"unknown name {node.id!r}")
|
|
144
|
+
return
|
|
145
|
+
if isinstance(node, ast.Constant):
|
|
146
|
+
if type(node.value) not in (int, float):
|
|
147
|
+
raise _array_error("constants must be finite numbers")
|
|
148
|
+
if type(node.value) is int and abs(node.value) > _MAX_INTEGER_MAGNITUDE:
|
|
149
|
+
raise _array_error(
|
|
150
|
+
f"integer constant magnitude limit is {_MAX_INTEGER_MAGNITUDE}"
|
|
151
|
+
)
|
|
152
|
+
if isinstance(node.value, float) and not math.isfinite(node.value):
|
|
153
|
+
raise _array_error("constants must be finite numbers")
|
|
154
|
+
return
|
|
155
|
+
if isinstance(node, ast.BinOp) and type(node.op) in _ALLOWED_BINARY:
|
|
156
|
+
_validate_node(node.left)
|
|
157
|
+
if isinstance(node.op, ast.Pow):
|
|
158
|
+
exponent = _numeric_literal(node.right)
|
|
159
|
+
if exponent is None:
|
|
160
|
+
raise _array_error("power exponent must be a finite numeric literal")
|
|
161
|
+
if abs(exponent) > _MAX_POWER_EXPONENT_MAGNITUDE:
|
|
162
|
+
raise _array_error(
|
|
163
|
+
f"power exponent magnitude limit is {_MAX_POWER_EXPONENT_MAGNITUDE}"
|
|
164
|
+
)
|
|
165
|
+
_validate_node(node.right)
|
|
166
|
+
return
|
|
167
|
+
if isinstance(node, ast.UnaryOp) and type(node.op) in _ALLOWED_UNARY:
|
|
168
|
+
_validate_node(node.operand)
|
|
169
|
+
return
|
|
170
|
+
if isinstance(node, ast.Call):
|
|
171
|
+
_validate_call(node)
|
|
172
|
+
return
|
|
173
|
+
raise _array_error(f"unsupported syntax {type(node).__name__}")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _numeric_literal(node: ast.AST) -> int | float | None:
|
|
177
|
+
sign = 1
|
|
178
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
|
|
179
|
+
sign = -1 if isinstance(node.op, ast.USub) else 1
|
|
180
|
+
node = node.operand
|
|
181
|
+
if not isinstance(node, ast.Constant) or type(node.value) not in (int, float):
|
|
182
|
+
return None
|
|
183
|
+
if isinstance(node.value, float) and not math.isfinite(node.value):
|
|
184
|
+
return None
|
|
185
|
+
return sign * node.value
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _validate_call(node: ast.Call) -> None:
|
|
189
|
+
if not isinstance(node.func, ast.Name):
|
|
190
|
+
raise _array_error("functions must be called directly")
|
|
191
|
+
name = node.func.id
|
|
192
|
+
if name not in _ALLOWED_FUNCTIONS:
|
|
193
|
+
raise _array_error(f"unknown function {name!r}")
|
|
194
|
+
if node.keywords:
|
|
195
|
+
raise _array_error("function keyword arguments are unsupported")
|
|
196
|
+
expected = 1 if name in _UNARY_FUNCTIONS else 2
|
|
197
|
+
if len(node.args) != expected:
|
|
198
|
+
suffix = "" if expected == 1 else "s"
|
|
199
|
+
raise _array_error(f"{name} expects {expected} argument{suffix}")
|
|
200
|
+
_validate_node(node.args[0])
|
|
201
|
+
if name == "reshape":
|
|
202
|
+
_reshape_shape(node.args[1])
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _reshape_shape(node: ast.AST) -> tuple[int, ...]:
|
|
206
|
+
if not isinstance(node, (ast.Tuple, ast.List)):
|
|
207
|
+
raise _array_error("reshape shape must be a tuple or list")
|
|
208
|
+
if len(node.elts) > _MAX_RESHAPE_RANK:
|
|
209
|
+
raise _array_error(f"reshape rank limit is {_MAX_RESHAPE_RANK}")
|
|
210
|
+
dimensions: list[int] = []
|
|
211
|
+
for item in node.elts:
|
|
212
|
+
value: object
|
|
213
|
+
if isinstance(item, ast.Constant):
|
|
214
|
+
value = item.value
|
|
215
|
+
elif (
|
|
216
|
+
isinstance(item, ast.UnaryOp)
|
|
217
|
+
and isinstance(item.op, ast.USub)
|
|
218
|
+
and isinstance(item.operand, ast.Constant)
|
|
219
|
+
and type(item.operand.value) is int
|
|
220
|
+
):
|
|
221
|
+
value = -item.operand.value
|
|
222
|
+
else:
|
|
223
|
+
raise _array_error("reshape dimensions must be integers")
|
|
224
|
+
if type(value) is not int:
|
|
225
|
+
raise _array_error("reshape dimensions must be integers")
|
|
226
|
+
dimensions.append(value)
|
|
227
|
+
if dimensions.count(-1) > 1:
|
|
228
|
+
raise _array_error("reshape shape allows at most one -1")
|
|
229
|
+
if -1 in dimensions and 0 in dimensions:
|
|
230
|
+
raise _array_error("reshape cannot combine a zero dimension with -1")
|
|
231
|
+
if any(dimension < -1 for dimension in dimensions):
|
|
232
|
+
raise _array_error("reshape dimensions must be non-negative or -1")
|
|
233
|
+
if any(dimension > _MAX_RESHAPE_DIMENSION for dimension in dimensions):
|
|
234
|
+
raise _array_error(f"reshape dimension limit is {_MAX_RESHAPE_DIMENSION}")
|
|
235
|
+
known_elements = math.prod(dimension for dimension in dimensions if dimension != -1)
|
|
236
|
+
if known_elements > _MAX_RESHAPE_ELEMENTS:
|
|
237
|
+
raise _array_error(f"reshape output limit is {_MAX_RESHAPE_ELEMENTS} elements")
|
|
238
|
+
return tuple(dimensions)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _validate_reshape_result(value: object) -> None:
|
|
242
|
+
dimensions = tuple(int(dimension) for dimension in getattr(value, "shape", ()))
|
|
243
|
+
if any(dimension > _MAX_RESHAPE_DIMENSION for dimension in dimensions):
|
|
244
|
+
raise _array_error(f"reshape dimension limit is {_MAX_RESHAPE_DIMENSION}")
|
|
245
|
+
if math.prod(dimensions) > _MAX_RESHAPE_ELEMENTS:
|
|
246
|
+
raise _array_error(f"reshape output limit is {_MAX_RESHAPE_ELEMENTS} elements")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _evaluate(
|
|
250
|
+
node: ast.AST,
|
|
251
|
+
value: object,
|
|
252
|
+
namespace: Any,
|
|
253
|
+
validate_result: Callable[[object], None] | None = None,
|
|
254
|
+
) -> object:
|
|
255
|
+
if isinstance(node, ast.Name):
|
|
256
|
+
# The input batch is validated at construction; only operation
|
|
257
|
+
# results pass through validate_result.
|
|
258
|
+
return value
|
|
259
|
+
if isinstance(node, ast.Constant):
|
|
260
|
+
result = node.value
|
|
261
|
+
elif isinstance(node, ast.BinOp):
|
|
262
|
+
function = _ALLOWED_BINARY[type(node.op)]
|
|
263
|
+
left = _evaluate(node.left, value, namespace, validate_result)
|
|
264
|
+
right = _evaluate(node.right, value, namespace, validate_result)
|
|
265
|
+
if isinstance(node.op, ast.MatMult):
|
|
266
|
+
_validate_matmul_output_size(left, right)
|
|
267
|
+
else:
|
|
268
|
+
_validate_broadcast_output_size(left, right)
|
|
269
|
+
result = function(left, right)
|
|
270
|
+
elif isinstance(node, ast.UnaryOp):
|
|
271
|
+
function = _ALLOWED_UNARY[type(node.op)]
|
|
272
|
+
result = function(_evaluate(node.operand, value, namespace, validate_result))
|
|
273
|
+
elif isinstance(node, ast.Call):
|
|
274
|
+
name = node.func.id # type: ignore[union-attr]
|
|
275
|
+
function = getattr(namespace, name)
|
|
276
|
+
arguments = [_evaluate(node.args[0], value, namespace, validate_result)]
|
|
277
|
+
if name == "reshape":
|
|
278
|
+
arguments.append(_reshape_shape(node.args[1]))
|
|
279
|
+
result = function(*arguments)
|
|
280
|
+
if name == "reshape":
|
|
281
|
+
_validate_reshape_result(result)
|
|
282
|
+
else:
|
|
283
|
+
raise AssertionError("validated array expression contained an unsupported node")
|
|
284
|
+
if validate_result is not None:
|
|
285
|
+
validate_result(result)
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _validate_options(options: Mapping[str, object]) -> ast.Expression:
|
|
290
|
+
unknown = set(options) - {"expression", "udfs"}
|
|
291
|
+
if unknown:
|
|
292
|
+
raise _array_error(f"unsupported options: {', '.join(sorted(unknown))}")
|
|
293
|
+
udfs = options.get("udfs", [])
|
|
294
|
+
if udfs:
|
|
295
|
+
raise _array_error("custom array UDFs are unavailable")
|
|
296
|
+
return _parse_expression(options.get("expression"))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _validate_stream_options(options: Mapping[str, object]) -> None:
|
|
300
|
+
parsed = _validate_options(options)
|
|
301
|
+
if not any(
|
|
302
|
+
isinstance(node, ast.Name) and node.id == "x" for node in ast.walk(parsed)
|
|
303
|
+
):
|
|
304
|
+
raise _array_error("stream expressions must depend on the input rows")
|
|
305
|
+
if any(isinstance(node, (ast.Call, ast.MatMult)) for node in ast.walk(parsed)):
|
|
306
|
+
raise _array_error(
|
|
307
|
+
"stream expressions support only row-axis-independent "
|
|
308
|
+
"elementwise operations"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _validate_provider_options(
|
|
313
|
+
provider: str, name: str, version: str, options: Mapping[str, object]
|
|
314
|
+
) -> None:
|
|
315
|
+
if provider in {"numpy", "jax"} and name == "expression" and version == "1":
|
|
316
|
+
_validate_options(options)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _table_matmul_columns(options: Mapping[str, object]) -> tuple[str, ...]:
|
|
320
|
+
unknown = set(options) - {"columns"}
|
|
321
|
+
if unknown:
|
|
322
|
+
raise ValueError(
|
|
323
|
+
"invalid table_matmul options: unsupported options: "
|
|
324
|
+
+ ", ".join(sorted(unknown))
|
|
325
|
+
)
|
|
326
|
+
columns = options.get("columns")
|
|
327
|
+
if isinstance(columns, (str, bytes)) or not isinstance(columns, list):
|
|
328
|
+
raise ValueError("invalid table_matmul options: columns must be a JSON array")
|
|
329
|
+
if not columns:
|
|
330
|
+
raise ValueError(
|
|
331
|
+
"invalid table_matmul options: columns must contain at least one name"
|
|
332
|
+
)
|
|
333
|
+
if not all(isinstance(column, str) and column for column in columns):
|
|
334
|
+
raise ValueError(
|
|
335
|
+
"invalid table_matmul options: columns must contain non-empty strings"
|
|
336
|
+
)
|
|
337
|
+
if len(set(columns)) != len(columns):
|
|
338
|
+
raise ValueError("invalid table_matmul options: columns must be unique")
|
|
339
|
+
return tuple(columns)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _table_matmul_inputs(
|
|
343
|
+
inputs: Mapping[str, _native.Batch],
|
|
344
|
+
backend: str,
|
|
345
|
+
) -> tuple[_native.Batch, _native.Batch]:
|
|
346
|
+
copied = dict(inputs)
|
|
347
|
+
expected = {"table", "weights"}
|
|
348
|
+
missing = expected - set(copied)
|
|
349
|
+
unexpected = set(copied) - expected
|
|
350
|
+
if missing:
|
|
351
|
+
raise ValueError(
|
|
352
|
+
"invalid table_matmul inputs: missing required inputs: "
|
|
353
|
+
+ ", ".join(sorted(missing))
|
|
354
|
+
)
|
|
355
|
+
if unexpected:
|
|
356
|
+
raise ValueError(
|
|
357
|
+
"invalid table_matmul inputs: unsupported inputs: "
|
|
358
|
+
+ ", ".join(sorted(unexpected))
|
|
359
|
+
)
|
|
360
|
+
table_batch = copied["table"]
|
|
361
|
+
weights_batch = copied["weights"]
|
|
362
|
+
if not isinstance(table_batch, _native.Batch) or table_batch.kind != "table":
|
|
363
|
+
received = getattr(table_batch, "kind", type(table_batch).__name__)
|
|
364
|
+
raise TypeError(
|
|
365
|
+
f"invalid table_matmul table: expected a table batch, received {received}"
|
|
366
|
+
)
|
|
367
|
+
if not isinstance(weights_batch, _native.Batch) or weights_batch.kind != "array":
|
|
368
|
+
received = getattr(weights_batch, "kind", type(weights_batch).__name__)
|
|
369
|
+
raise TypeError(
|
|
370
|
+
"invalid table_matmul weights: "
|
|
371
|
+
f"expected an array batch, received {received}"
|
|
372
|
+
)
|
|
373
|
+
if weights_batch.backend != backend:
|
|
374
|
+
raise ValueError(
|
|
375
|
+
"invalid table_matmul weights.backend: "
|
|
376
|
+
f"expected {backend}, received {weights_batch.backend}"
|
|
377
|
+
)
|
|
378
|
+
return table_batch, weights_batch
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _validated_table_dtypes(
|
|
382
|
+
table: object,
|
|
383
|
+
columns: tuple[str, ...],
|
|
384
|
+
) -> tuple[object, ...]:
|
|
385
|
+
import numpy as np
|
|
386
|
+
import pyarrow as pa
|
|
387
|
+
|
|
388
|
+
if table.num_rows <= 0:
|
|
389
|
+
raise ValueError("invalid table_matmul table.rows: expected at least one row")
|
|
390
|
+
dtypes: list[object] = []
|
|
391
|
+
for name in columns:
|
|
392
|
+
indices = table.schema.get_all_field_indices(name)
|
|
393
|
+
if not indices:
|
|
394
|
+
raise ValueError(
|
|
395
|
+
f"invalid table_matmul columns: selected column {name!r} is missing"
|
|
396
|
+
)
|
|
397
|
+
if len(indices) != 1:
|
|
398
|
+
raise ValueError(
|
|
399
|
+
f"invalid table_matmul columns: selected column {name!r} is ambiguous"
|
|
400
|
+
)
|
|
401
|
+
column = table.column(indices[0])
|
|
402
|
+
if column.null_count:
|
|
403
|
+
raise ValueError(
|
|
404
|
+
f"invalid table_matmul columns: selected column {name!r} contains nulls"
|
|
405
|
+
)
|
|
406
|
+
data_type = column.type
|
|
407
|
+
if not (pa.types.is_integer(data_type) or pa.types.is_floating(data_type)):
|
|
408
|
+
raise TypeError(
|
|
409
|
+
"invalid table_matmul columns: "
|
|
410
|
+
f"selected column {name!r} has unsupported Arrow dtype {data_type}"
|
|
411
|
+
)
|
|
412
|
+
family = (
|
|
413
|
+
"int"
|
|
414
|
+
if pa.types.is_signed_integer(data_type)
|
|
415
|
+
else "uint"
|
|
416
|
+
if pa.types.is_unsigned_integer(data_type)
|
|
417
|
+
else "float"
|
|
418
|
+
)
|
|
419
|
+
dtypes.append(np.dtype(f"{family}{data_type.bit_width}"))
|
|
420
|
+
return tuple(dtypes)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _validated_weights(
|
|
424
|
+
weights_batch: _native.Batch,
|
|
425
|
+
backend: str,
|
|
426
|
+
column_count: int,
|
|
427
|
+
) -> object:
|
|
428
|
+
weights = weights_batch.array
|
|
429
|
+
if backend == "numpy":
|
|
430
|
+
import numpy as np
|
|
431
|
+
|
|
432
|
+
if type(weights) is not np.ndarray:
|
|
433
|
+
raise TypeError(
|
|
434
|
+
"invalid table_matmul weights: NumPy weights must be an ndarray"
|
|
435
|
+
)
|
|
436
|
+
shape = getattr(weights, "shape", ())
|
|
437
|
+
if len(shape) != 2:
|
|
438
|
+
raise ValueError(
|
|
439
|
+
"invalid table_matmul weights.rank: "
|
|
440
|
+
f"expected rank two, received rank {len(shape)}"
|
|
441
|
+
)
|
|
442
|
+
if shape[0] != column_count:
|
|
443
|
+
raise ValueError(
|
|
444
|
+
"invalid table_matmul weights.shape[0]: "
|
|
445
|
+
f"expected {column_count}, received {shape[0]}"
|
|
446
|
+
)
|
|
447
|
+
if shape[1] <= 0:
|
|
448
|
+
raise ValueError(
|
|
449
|
+
"invalid table_matmul weights.shape[1]: expected a positive output width"
|
|
450
|
+
)
|
|
451
|
+
return weights
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _common_matrix_dtype(
|
|
455
|
+
backend: str,
|
|
456
|
+
namespace: object,
|
|
457
|
+
table_dtypes: tuple[object, ...],
|
|
458
|
+
weights: object,
|
|
459
|
+
) -> object:
|
|
460
|
+
import numpy as np
|
|
461
|
+
|
|
462
|
+
weight_dtype = np.dtype(weights.dtype)
|
|
463
|
+
involved = ", ".join(
|
|
464
|
+
[*(np.dtype(dtype).name for dtype in table_dtypes), weight_dtype.name]
|
|
465
|
+
)
|
|
466
|
+
if backend == "jax":
|
|
467
|
+
import jax
|
|
468
|
+
|
|
469
|
+
x64_dtypes = frozenset(
|
|
470
|
+
np.dtype(dtype)
|
|
471
|
+
for dtype in (np.int64, np.uint64, np.float64, np.complex128)
|
|
472
|
+
)
|
|
473
|
+
if not jax.config.x64_enabled and any(
|
|
474
|
+
np.dtype(source) in x64_dtypes for source in (*table_dtypes, weight_dtype)
|
|
475
|
+
):
|
|
476
|
+
raise TypeError(
|
|
477
|
+
"invalid table_matmul dtype: "
|
|
478
|
+
f"JAX x64 is disabled for [{involved}]; enable the required dtype "
|
|
479
|
+
"or choose a lossless supported dtype"
|
|
480
|
+
)
|
|
481
|
+
try:
|
|
482
|
+
result_dtype = np.dtype(namespace.result_type(*table_dtypes, weight_dtype))
|
|
483
|
+
except (TypeError, ValueError) as error:
|
|
484
|
+
raise TypeError(
|
|
485
|
+
"invalid table_matmul dtype: "
|
|
486
|
+
f"{backend} cannot promote Arrow and weight dtypes [{involved}]"
|
|
487
|
+
) from error
|
|
488
|
+
sources = (*table_dtypes, weight_dtype)
|
|
489
|
+
if not all(
|
|
490
|
+
namespace.can_cast(source, result_dtype, casting="safe") for source in sources
|
|
491
|
+
):
|
|
492
|
+
raise TypeError(
|
|
493
|
+
"invalid table_matmul dtype: "
|
|
494
|
+
f"common dtype {result_dtype.name} is lossy for [{involved}]"
|
|
495
|
+
)
|
|
496
|
+
try:
|
|
497
|
+
_validate_numpy_dtype(result_dtype)
|
|
498
|
+
except ValueError as error:
|
|
499
|
+
raise TypeError(
|
|
500
|
+
"invalid table_matmul dtype: "
|
|
501
|
+
f"common dtype {result_dtype.name} is unsupported for [{involved}]"
|
|
502
|
+
) from error
|
|
503
|
+
return result_dtype
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _numpy_table_matrix(
|
|
507
|
+
table: object,
|
|
508
|
+
columns: tuple[str, ...],
|
|
509
|
+
dtype: object,
|
|
510
|
+
) -> object:
|
|
511
|
+
import numpy as np
|
|
512
|
+
|
|
513
|
+
matrix, _token = _native.Batch._new_owned_numpy(
|
|
514
|
+
(table.num_rows, len(columns)),
|
|
515
|
+
np.dtype(dtype).name,
|
|
516
|
+
)
|
|
517
|
+
for column_index, name in enumerate(columns):
|
|
518
|
+
offset = 0
|
|
519
|
+
for chunk in table[name].chunks:
|
|
520
|
+
values = chunk.to_numpy(zero_copy_only=True)
|
|
521
|
+
next_offset = offset + len(values)
|
|
522
|
+
np.copyto(
|
|
523
|
+
matrix[offset:next_offset, column_index],
|
|
524
|
+
values,
|
|
525
|
+
casting="safe",
|
|
526
|
+
)
|
|
527
|
+
offset = next_offset
|
|
528
|
+
return matrix
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _owned_numpy(value: object) -> object:
|
|
532
|
+
import numpy as np
|
|
533
|
+
|
|
534
|
+
array = np.asarray(value)
|
|
535
|
+
_validate_numpy_dtype(array.dtype)
|
|
536
|
+
immutable_bytes = array.tobytes(order="C")
|
|
537
|
+
return np.frombuffer(immutable_bytes, dtype=array.dtype).reshape(array.shape)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _validate_numpy_dtype(dtype: object) -> None:
|
|
541
|
+
import numpy as np
|
|
542
|
+
|
|
543
|
+
normalized = np.dtype(dtype)
|
|
544
|
+
allowed_dtypes = frozenset(
|
|
545
|
+
np.dtype(scalar_type)
|
|
546
|
+
for scalar_type in (
|
|
547
|
+
np.bool_,
|
|
548
|
+
np.int8,
|
|
549
|
+
np.int16,
|
|
550
|
+
np.int32,
|
|
551
|
+
np.int64,
|
|
552
|
+
np.uint8,
|
|
553
|
+
np.uint16,
|
|
554
|
+
np.uint32,
|
|
555
|
+
np.uint64,
|
|
556
|
+
np.float32,
|
|
557
|
+
np.float64,
|
|
558
|
+
np.complex64,
|
|
559
|
+
np.complex128,
|
|
560
|
+
)
|
|
561
|
+
)
|
|
562
|
+
if normalized not in allowed_dtypes or not normalized.isnative:
|
|
563
|
+
raise ValueError(
|
|
564
|
+
f"NumPy arrays require a NumPy Array API dtype; received {normalized}"
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _validate_python_operation_scalar(value: object) -> bool:
|
|
569
|
+
if type(value) is int:
|
|
570
|
+
if abs(value) > _MAX_INTEGER_MAGNITUDE:
|
|
571
|
+
raise _array_error(
|
|
572
|
+
f"integer constant magnitude limit is {_MAX_INTEGER_MAGNITUDE}"
|
|
573
|
+
)
|
|
574
|
+
return True
|
|
575
|
+
return type(value) in (float, complex)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _result_shape(value: object) -> tuple[int, ...]:
|
|
579
|
+
return tuple(int(dimension) for dimension in getattr(value, "shape", ()))
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _validate_operation_output_size(value: object) -> None:
|
|
583
|
+
_validate_operation_shape(_result_shape(value))
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _validate_operation_shape(shape: tuple[int, ...]) -> None:
|
|
587
|
+
if math.prod(shape) > _MAX_OPERATION_ELEMENTS:
|
|
588
|
+
raise _array_error(
|
|
589
|
+
f"operation output limit is {_MAX_OPERATION_ELEMENTS} elements"
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _broadcast_shape(
|
|
594
|
+
left: tuple[int, ...], right: tuple[int, ...]
|
|
595
|
+
) -> tuple[int, ...] | None:
|
|
596
|
+
dimensions: list[int] = []
|
|
597
|
+
for offset in range(1, max(len(left), len(right)) + 1):
|
|
598
|
+
left_dim = left[-offset] if offset <= len(left) else 1
|
|
599
|
+
right_dim = right[-offset] if offset <= len(right) else 1
|
|
600
|
+
if left_dim == 1:
|
|
601
|
+
dimensions.append(right_dim)
|
|
602
|
+
elif right_dim == 1 or left_dim == right_dim:
|
|
603
|
+
dimensions.append(left_dim)
|
|
604
|
+
else:
|
|
605
|
+
return None
|
|
606
|
+
return tuple(reversed(dimensions))
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _matmul_batch_shape(
|
|
610
|
+
left: tuple[int, ...], right: tuple[int, ...]
|
|
611
|
+
) -> tuple[int, ...] | None:
|
|
612
|
+
return _broadcast_shape(
|
|
613
|
+
left[:-2] if len(left) > 1 else (),
|
|
614
|
+
right[:-2] if len(right) > 1 else (),
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _matmul_result_axes(
|
|
619
|
+
left: tuple[int, ...], right: tuple[int, ...]
|
|
620
|
+
) -> tuple[int, ...]:
|
|
621
|
+
if len(left) == 1:
|
|
622
|
+
return () if len(right) == 1 else (right[-1],)
|
|
623
|
+
if len(right) == 1:
|
|
624
|
+
return (left[-2],)
|
|
625
|
+
return (left[-2], right[-1])
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _matmul_output_shape(
|
|
629
|
+
left: tuple[int, ...], right: tuple[int, ...]
|
|
630
|
+
) -> tuple[int, ...] | None:
|
|
631
|
+
if not left or not right:
|
|
632
|
+
return None
|
|
633
|
+
right_inner = right[-2] if len(right) > 1 else right[-1]
|
|
634
|
+
if left[-1] != right_inner:
|
|
635
|
+
return None
|
|
636
|
+
batch = _matmul_batch_shape(left, right)
|
|
637
|
+
if batch is None:
|
|
638
|
+
return None
|
|
639
|
+
return (*batch, *_matmul_result_axes(left, right))
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _validate_broadcast_output_size(left: object, right: object) -> None:
|
|
643
|
+
shape = _broadcast_shape(_result_shape(left), _result_shape(right))
|
|
644
|
+
if shape is not None:
|
|
645
|
+
_validate_operation_shape(shape)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _validate_matmul_output_size(left: object, right: object) -> None:
|
|
649
|
+
shape = _matmul_output_shape(_result_shape(left), _result_shape(right))
|
|
650
|
+
if shape is not None:
|
|
651
|
+
_validate_operation_shape(shape)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _validate_numpy_operation_result(value: object) -> None:
|
|
655
|
+
import numpy as np
|
|
656
|
+
|
|
657
|
+
if _validate_python_operation_scalar(value):
|
|
658
|
+
return
|
|
659
|
+
if type(value) is np.ndarray or isinstance(value, np.generic):
|
|
660
|
+
_validate_numpy_dtype(value.dtype)
|
|
661
|
+
_validate_operation_output_size(value)
|
|
662
|
+
return
|
|
663
|
+
raise TypeError("NumPy provider operations must produce arrays or numeric scalars")
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _validate_jax_operation_result(value: object) -> None:
|
|
667
|
+
import jax
|
|
668
|
+
|
|
669
|
+
if _validate_python_operation_scalar(value):
|
|
670
|
+
return
|
|
671
|
+
if isinstance(value, jax.Array):
|
|
672
|
+
_validate_operation_output_size(value)
|
|
673
|
+
return
|
|
674
|
+
raise TypeError("JAX provider operations must produce arrays or numeric scalars")
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _owned_jax(value: object) -> object:
|
|
678
|
+
import jax
|
|
679
|
+
import jax.numpy as jnp
|
|
680
|
+
|
|
681
|
+
array = jnp.asarray(value)
|
|
682
|
+
if not isinstance(array, jax.Array):
|
|
683
|
+
raise TypeError("JAX batches require a jax.Array payload")
|
|
684
|
+
return array
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _prepare_array(value: object, backend: str) -> tuple[object, int]:
|
|
688
|
+
if backend == "numpy":
|
|
689
|
+
owned = _owned_numpy(value)
|
|
690
|
+
elif backend == "jax":
|
|
691
|
+
owned = _owned_jax(value)
|
|
692
|
+
else:
|
|
693
|
+
owned = value
|
|
694
|
+
shape = getattr(owned, "shape", None)
|
|
695
|
+
length = int(shape[0]) if shape else 1
|
|
696
|
+
if length < 0:
|
|
697
|
+
raise ValueError("array length must not be negative")
|
|
698
|
+
return owned, length
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
@dataclass(frozen=True, slots=True)
|
|
702
|
+
class _ArrayProvider:
|
|
703
|
+
backend: str
|
|
704
|
+
namespace: object
|
|
705
|
+
|
|
706
|
+
def validate(self, options: Mapping[str, object]) -> None:
|
|
707
|
+
_validate_options(options)
|
|
708
|
+
|
|
709
|
+
def validate_stream(self, options: Mapping[str, object]) -> None:
|
|
710
|
+
_validate_stream_options(options)
|
|
711
|
+
|
|
712
|
+
def __call__(
|
|
713
|
+
self, batch: _native.Batch, options: Mapping[str, object]
|
|
714
|
+
) -> _native.Batch:
|
|
715
|
+
if batch.backend != self.backend:
|
|
716
|
+
raise TypeError(
|
|
717
|
+
f"provider requires backend {self.backend}, received {batch.backend}"
|
|
718
|
+
)
|
|
719
|
+
parsed = _validate_options(options)
|
|
720
|
+
validate_result = {
|
|
721
|
+
"jax": _validate_jax_operation_result,
|
|
722
|
+
"numpy": _validate_numpy_operation_result,
|
|
723
|
+
}[self.backend]
|
|
724
|
+
result = _evaluate(parsed.body, batch.array, self.namespace, validate_result)
|
|
725
|
+
if self.backend == "jax":
|
|
726
|
+
import jax
|
|
727
|
+
|
|
728
|
+
result = _owned_jax(result)
|
|
729
|
+
if not isinstance(result, jax.Array):
|
|
730
|
+
raise TypeError("JAX provider output must remain a jax.Array")
|
|
731
|
+
return _native.Batch.from_array(
|
|
732
|
+
result, backend=self.backend, metadata=batch.metadata
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _jax_table_matmul(
|
|
737
|
+
table: object,
|
|
738
|
+
columns: tuple[str, ...],
|
|
739
|
+
dtype: object,
|
|
740
|
+
weights: object,
|
|
741
|
+
) -> tuple[object, None]:
|
|
742
|
+
import jax
|
|
743
|
+
import jax.numpy as jnp
|
|
744
|
+
|
|
745
|
+
host = _numpy_table_matrix(table, columns, dtype)
|
|
746
|
+
dense = jax.device_put(host, device=weights.device)
|
|
747
|
+
expected_dtype = jnp.dtype(dtype)
|
|
748
|
+
if dense.dtype != expected_dtype:
|
|
749
|
+
raise ValueError(
|
|
750
|
+
"invalid table_matmul dtype: JAX changed "
|
|
751
|
+
f"{expected_dtype} to {dense.dtype}; enable the required dtype "
|
|
752
|
+
"or choose a lossless supported dtype"
|
|
753
|
+
)
|
|
754
|
+
result = jnp.matmul(dense, weights)
|
|
755
|
+
if not isinstance(result, jax.Array):
|
|
756
|
+
raise TypeError("table_matmul JAX result must remain a jax.Array")
|
|
757
|
+
if result.dtype != expected_dtype:
|
|
758
|
+
raise ValueError(
|
|
759
|
+
"invalid table_matmul dtype: JAX changed "
|
|
760
|
+
f"{expected_dtype} to {result.dtype}; enable the required dtype "
|
|
761
|
+
"or choose a lossless supported dtype"
|
|
762
|
+
)
|
|
763
|
+
return result, None
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
@dataclass(frozen=True, slots=True)
|
|
767
|
+
class _TableMatmulProvider:
|
|
768
|
+
backend: str
|
|
769
|
+
namespace: object
|
|
770
|
+
|
|
771
|
+
def validate(self, options: Mapping[str, object]) -> None:
|
|
772
|
+
_table_matmul_columns(options)
|
|
773
|
+
|
|
774
|
+
def __call__(
|
|
775
|
+
self,
|
|
776
|
+
inputs: Mapping[str, _native.Batch],
|
|
777
|
+
options: Mapping[str, object],
|
|
778
|
+
) -> dict[str, _native.Batch]:
|
|
779
|
+
columns = _table_matmul_columns(options)
|
|
780
|
+
table_batch, weights_batch = _table_matmul_inputs(inputs, self.backend)
|
|
781
|
+
table = table_batch.to_pyarrow()
|
|
782
|
+
table_dtypes = _validated_table_dtypes(table, columns)
|
|
783
|
+
weights = _validated_weights(weights_batch, self.backend, len(columns))
|
|
784
|
+
result_dtype = _common_matrix_dtype(
|
|
785
|
+
self.backend,
|
|
786
|
+
self.namespace,
|
|
787
|
+
table_dtypes,
|
|
788
|
+
weights,
|
|
789
|
+
)
|
|
790
|
+
if self.backend != "numpy":
|
|
791
|
+
output, token = _jax_table_matmul(
|
|
792
|
+
table,
|
|
793
|
+
columns,
|
|
794
|
+
result_dtype,
|
|
795
|
+
weights,
|
|
796
|
+
)
|
|
797
|
+
else:
|
|
798
|
+
import numpy as np
|
|
799
|
+
|
|
800
|
+
dense = _numpy_table_matrix(table, columns, result_dtype)
|
|
801
|
+
output, token = _native.Batch._new_owned_numpy(
|
|
802
|
+
(table.num_rows, weights.shape[1]),
|
|
803
|
+
np.dtype(result_dtype).name,
|
|
804
|
+
)
|
|
805
|
+
np.matmul(dense, weights, out=output)
|
|
806
|
+
|
|
807
|
+
metadata = table_batch.metadata
|
|
808
|
+
metadata.update(
|
|
809
|
+
{
|
|
810
|
+
"backend": self.backend,
|
|
811
|
+
"columns": list(columns),
|
|
812
|
+
"operation": "table_matmul",
|
|
813
|
+
}
|
|
814
|
+
)
|
|
815
|
+
return {
|
|
816
|
+
"output": _native.Batch._from_owned_array(
|
|
817
|
+
output,
|
|
818
|
+
backend=self.backend,
|
|
819
|
+
token=token,
|
|
820
|
+
metadata=metadata,
|
|
821
|
+
)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
_SYMBOLIC_BINARY = {
|
|
826
|
+
"add": operator.add,
|
|
827
|
+
"and": operator.and_,
|
|
828
|
+
"eq": operator.eq,
|
|
829
|
+
"ge": operator.ge,
|
|
830
|
+
"gt": operator.gt,
|
|
831
|
+
"le": operator.le,
|
|
832
|
+
"lt": operator.lt,
|
|
833
|
+
"mul": operator.mul,
|
|
834
|
+
"ne": operator.ne,
|
|
835
|
+
"or": operator.or_,
|
|
836
|
+
"sub": operator.sub,
|
|
837
|
+
"truediv": operator.truediv,
|
|
838
|
+
}
|
|
839
|
+
_SYMBOLIC_UNARY = ("neg", "not")
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _symbolic_typed_operand(
|
|
843
|
+
backend: str,
|
|
844
|
+
data_type: str,
|
|
845
|
+
*,
|
|
846
|
+
matrix: bool,
|
|
847
|
+
) -> object:
|
|
848
|
+
import numpy as np
|
|
849
|
+
|
|
850
|
+
if backend == "numpy":
|
|
851
|
+
namespace = np
|
|
852
|
+
elif backend == "jax":
|
|
853
|
+
import jax
|
|
854
|
+
import jax.numpy as jnp
|
|
855
|
+
|
|
856
|
+
if not jax.config.x64_enabled and np.dtype(data_type) in {
|
|
857
|
+
np.dtype("int64"),
|
|
858
|
+
np.dtype("uint64"),
|
|
859
|
+
np.dtype("float64"),
|
|
860
|
+
np.dtype("complex128"),
|
|
861
|
+
}:
|
|
862
|
+
raise TypeError(f"{backend} cannot represent dtype {data_type!r}")
|
|
863
|
+
namespace = jnp
|
|
864
|
+
else:
|
|
865
|
+
raise TypeError(f"unsupported symbolic array backend {backend!r}")
|
|
866
|
+
shape = (1, 1) if matrix else (1,)
|
|
867
|
+
value = namespace.ones(shape, dtype=data_type)
|
|
868
|
+
if np.dtype(value.dtype).name != data_type:
|
|
869
|
+
raise TypeError(f"{backend} cannot represent dtype {data_type!r}")
|
|
870
|
+
return value
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _symbolic_result_dtype(
|
|
874
|
+
backend: str,
|
|
875
|
+
operation: str,
|
|
876
|
+
left: str | bool | int | float,
|
|
877
|
+
right: str | bool | int | float | None = None,
|
|
878
|
+
) -> str:
|
|
879
|
+
"""Return the selected provider's v1 dtype for one symbolic primitive."""
|
|
880
|
+
|
|
881
|
+
import numpy as np
|
|
882
|
+
|
|
883
|
+
matrix = operation == "matmul"
|
|
884
|
+
left_value = _symbolic_operand(backend, left, matrix=matrix)
|
|
885
|
+
right_value = _symbolic_operand(backend, right, matrix=matrix)
|
|
886
|
+
_validate_symbolic_common_dtype(backend, left_value, right_value)
|
|
887
|
+
result = _apply_symbolic_primitive(
|
|
888
|
+
_symbolic_dtype_namespace(backend),
|
|
889
|
+
operation,
|
|
890
|
+
left_value,
|
|
891
|
+
right_value,
|
|
892
|
+
)
|
|
893
|
+
return np.dtype(result.dtype).name
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def _symbolic_operand(
|
|
897
|
+
backend: str,
|
|
898
|
+
value: str | bool | int | float | None,
|
|
899
|
+
*,
|
|
900
|
+
matrix: bool,
|
|
901
|
+
) -> object:
|
|
902
|
+
if isinstance(value, str):
|
|
903
|
+
return _symbolic_typed_operand(backend, value, matrix=matrix)
|
|
904
|
+
return value
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def _symbolic_dtype_namespace(backend: str) -> object:
|
|
908
|
+
if backend == "numpy":
|
|
909
|
+
import numpy as np
|
|
910
|
+
|
|
911
|
+
return np
|
|
912
|
+
import jax.numpy as jnp
|
|
913
|
+
|
|
914
|
+
return jnp
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def _validate_symbolic_common_dtype(
|
|
918
|
+
backend: str,
|
|
919
|
+
left: object,
|
|
920
|
+
right: object,
|
|
921
|
+
) -> None:
|
|
922
|
+
typed_values = tuple(value for value in (left, right) if hasattr(value, "dtype"))
|
|
923
|
+
if len(typed_values) != 2:
|
|
924
|
+
return
|
|
925
|
+
namespace = _symbolic_dtype_namespace(backend)
|
|
926
|
+
common = namespace.result_type(
|
|
927
|
+
typed_values[0].dtype,
|
|
928
|
+
typed_values[1].dtype,
|
|
929
|
+
)
|
|
930
|
+
if not all(
|
|
931
|
+
namespace.can_cast(value.dtype, common, casting="safe")
|
|
932
|
+
for value in typed_values
|
|
933
|
+
):
|
|
934
|
+
raise TypeError("the provider has no safe common dtype")
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def _apply_symbolic_primitive(
|
|
938
|
+
namespace: object,
|
|
939
|
+
operation: str,
|
|
940
|
+
left: object,
|
|
941
|
+
right: object,
|
|
942
|
+
) -> object:
|
|
943
|
+
if operation == "not":
|
|
944
|
+
return namespace.logical_not(left)
|
|
945
|
+
if operation == "neg":
|
|
946
|
+
return operator.neg(left)
|
|
947
|
+
if operation == "matmul":
|
|
948
|
+
return operator.matmul(left, right)
|
|
949
|
+
return _SYMBOLIC_BINARY[operation](left, right)
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def _raise_invalid_symbolic_node(node: Mapping[str, object]) -> Never:
|
|
953
|
+
raise ValueError(
|
|
954
|
+
f"invalid symbolic_matrix expression: unsupported node {node.get('op')!r}"
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def _validate_symbolic_leaf(node: dict[str, object], _depth: int) -> None:
|
|
959
|
+
if set(node) != {"op"}:
|
|
960
|
+
_raise_invalid_symbolic_node(node)
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _validate_symbolic_literal(node: dict[str, object], _depth: int) -> None:
|
|
964
|
+
if set(node) != {"op", "value"}:
|
|
965
|
+
_raise_invalid_symbolic_node(node)
|
|
966
|
+
literal = node["value"]
|
|
967
|
+
if type(literal) not in (bool, int, float):
|
|
968
|
+
raise ValueError("invalid symbolic_matrix expression: literal must be finite")
|
|
969
|
+
if type(literal) is float and not math.isfinite(literal):
|
|
970
|
+
raise ValueError("invalid symbolic_matrix expression: literal must be finite")
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def _validate_symbolic_unary(node: dict[str, object], depth: int) -> None:
|
|
974
|
+
if set(node) != {"op", "value"}:
|
|
975
|
+
_raise_invalid_symbolic_node(node)
|
|
976
|
+
_validated_symbolic_tree(node["value"], depth=depth + 1)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _validate_symbolic_binary(node: dict[str, object], depth: int) -> None:
|
|
980
|
+
if set(node) != {"left", "op", "right"}:
|
|
981
|
+
_raise_invalid_symbolic_node(node)
|
|
982
|
+
_validated_symbolic_tree(node["left"], depth=depth + 1)
|
|
983
|
+
_validated_symbolic_tree(node["right"], depth=depth + 1)
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
_SYMBOLIC_NODE_VALIDATORS: dict[str, Callable[[dict[str, object], int], None]] = {
|
|
987
|
+
"input": _validate_symbolic_leaf,
|
|
988
|
+
"literal": _validate_symbolic_literal,
|
|
989
|
+
"weights": _validate_symbolic_leaf,
|
|
990
|
+
**{
|
|
991
|
+
operation: _validate_symbolic_binary
|
|
992
|
+
for operation in (*_SYMBOLIC_BINARY, "matmul")
|
|
993
|
+
},
|
|
994
|
+
**{operation: _validate_symbolic_unary for operation in _SYMBOLIC_UNARY},
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _validated_symbolic_tree(value: object, *, depth: int = 0) -> dict[str, object]:
|
|
999
|
+
if depth > _MAX_AST_DEPTH:
|
|
1000
|
+
raise ValueError("invalid symbolic_matrix expression: depth limit exceeded")
|
|
1001
|
+
if not isinstance(value, Mapping):
|
|
1002
|
+
raise ValueError("invalid symbolic_matrix expression: node must be a mapping")
|
|
1003
|
+
node = dict(value)
|
|
1004
|
+
validator = _SYMBOLIC_NODE_VALIDATORS.get(node.get("op"))
|
|
1005
|
+
if validator is None:
|
|
1006
|
+
_raise_invalid_symbolic_node(node)
|
|
1007
|
+
validator(node, depth)
|
|
1008
|
+
return node
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def _raise_invalid_symbolic_names(field: str) -> Never:
|
|
1012
|
+
raise ValueError(
|
|
1013
|
+
f"invalid symbolic_matrix {field}: expected unique non-empty strings"
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def _symbolic_name(value: object, field: str) -> str:
|
|
1018
|
+
if type(value) is not str:
|
|
1019
|
+
_raise_invalid_symbolic_names(field)
|
|
1020
|
+
if not value:
|
|
1021
|
+
_raise_invalid_symbolic_names(field)
|
|
1022
|
+
return value
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def _symbolic_names(value: object, field: str) -> tuple[str, ...]:
|
|
1026
|
+
if not isinstance(value, list):
|
|
1027
|
+
_raise_invalid_symbolic_names(field)
|
|
1028
|
+
if not value:
|
|
1029
|
+
_raise_invalid_symbolic_names(field)
|
|
1030
|
+
names = tuple(_symbolic_name(name, field) for name in value)
|
|
1031
|
+
if len(set(names)) != len(names):
|
|
1032
|
+
_raise_invalid_symbolic_names(field)
|
|
1033
|
+
return names
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def _symbolic_matrix_options(
|
|
1037
|
+
options: Mapping[str, object],
|
|
1038
|
+
) -> tuple[tuple[str, ...], tuple[str, ...], dict[str, object]]:
|
|
1039
|
+
if set(options) != {"columns", "expression", "names"}:
|
|
1040
|
+
raise ValueError(
|
|
1041
|
+
"invalid symbolic_matrix options: expected columns, expression, and names"
|
|
1042
|
+
)
|
|
1043
|
+
columns = _symbolic_names(options["columns"], "columns")
|
|
1044
|
+
names = _symbolic_names(options["names"], "names")
|
|
1045
|
+
return columns, names, _validated_symbolic_tree(options["expression"])
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def _symbolic_matrix_inputs(
|
|
1049
|
+
inputs: Mapping[str, _native.Batch], backend: str
|
|
1050
|
+
) -> tuple[_native.Batch, _native.Batch]:
|
|
1051
|
+
if set(inputs) != {"input", "weights"}:
|
|
1052
|
+
raise ValueError("invalid symbolic_matrix inputs: expected input and weights")
|
|
1053
|
+
table_batch = inputs["input"]
|
|
1054
|
+
weights_batch = inputs["weights"]
|
|
1055
|
+
if table_batch.kind != "table":
|
|
1056
|
+
raise ValueError("invalid symbolic_matrix input: expected a table batch")
|
|
1057
|
+
if weights_batch.kind != "array" or weights_batch.backend != backend:
|
|
1058
|
+
raise ValueError(f"invalid symbolic_matrix weights.backend: expected {backend}")
|
|
1059
|
+
return table_batch, weights_batch
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def _evaluate_symbolic_tree(
|
|
1063
|
+
node: Mapping[str, object],
|
|
1064
|
+
namespace: object,
|
|
1065
|
+
dense: object,
|
|
1066
|
+
weights: object,
|
|
1067
|
+
) -> object:
|
|
1068
|
+
operation = node["op"]
|
|
1069
|
+
if operation == "input":
|
|
1070
|
+
return dense
|
|
1071
|
+
if operation == "weights":
|
|
1072
|
+
return weights
|
|
1073
|
+
if operation == "literal":
|
|
1074
|
+
return node["value"]
|
|
1075
|
+
if operation in _SYMBOLIC_UNARY:
|
|
1076
|
+
result = _apply_symbolic_primitive(
|
|
1077
|
+
namespace,
|
|
1078
|
+
operation,
|
|
1079
|
+
_evaluate_symbolic_tree(node["value"], namespace, dense, weights),
|
|
1080
|
+
None,
|
|
1081
|
+
)
|
|
1082
|
+
else:
|
|
1083
|
+
left = _evaluate_symbolic_tree(node["left"], namespace, dense, weights)
|
|
1084
|
+
right = _evaluate_symbolic_tree(node["right"], namespace, dense, weights)
|
|
1085
|
+
if operation == "matmul":
|
|
1086
|
+
_validate_matmul_output_size(left, right)
|
|
1087
|
+
result = namespace.matmul(left, right)
|
|
1088
|
+
else:
|
|
1089
|
+
_validate_broadcast_output_size(left, right)
|
|
1090
|
+
result = _SYMBOLIC_BINARY[operation](left, right)
|
|
1091
|
+
_validate_operation_output_size(result)
|
|
1092
|
+
return result
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def _symbolic_tree_dtype(
|
|
1096
|
+
node: Mapping[str, object],
|
|
1097
|
+
backend: str,
|
|
1098
|
+
input_dtype: str,
|
|
1099
|
+
weights_dtype: str,
|
|
1100
|
+
) -> str | bool | int | float:
|
|
1101
|
+
operation = node["op"]
|
|
1102
|
+
if operation == "input":
|
|
1103
|
+
return input_dtype
|
|
1104
|
+
if operation == "weights":
|
|
1105
|
+
return weights_dtype
|
|
1106
|
+
if operation == "literal":
|
|
1107
|
+
literal = node["value"]
|
|
1108
|
+
if type(literal) in (bool, int, float):
|
|
1109
|
+
return literal
|
|
1110
|
+
raise TypeError("invalid symbolic_matrix literal dtype")
|
|
1111
|
+
child = "value" if operation in _SYMBOLIC_UNARY else "left"
|
|
1112
|
+
left = _symbolic_tree_dtype(
|
|
1113
|
+
node[child],
|
|
1114
|
+
backend,
|
|
1115
|
+
input_dtype,
|
|
1116
|
+
weights_dtype,
|
|
1117
|
+
)
|
|
1118
|
+
right = (
|
|
1119
|
+
None
|
|
1120
|
+
if operation in _SYMBOLIC_UNARY
|
|
1121
|
+
else _symbolic_tree_dtype(
|
|
1122
|
+
node["right"],
|
|
1123
|
+
backend,
|
|
1124
|
+
input_dtype,
|
|
1125
|
+
weights_dtype,
|
|
1126
|
+
)
|
|
1127
|
+
)
|
|
1128
|
+
return _symbolic_result_dtype(backend, operation, left, right)
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
@dataclass(frozen=True, slots=True)
|
|
1132
|
+
class _SymbolicMatrixProvider:
|
|
1133
|
+
backend: str
|
|
1134
|
+
namespace: object
|
|
1135
|
+
|
|
1136
|
+
def validate(self, options: Mapping[str, object]) -> None:
|
|
1137
|
+
_symbolic_matrix_options(options)
|
|
1138
|
+
|
|
1139
|
+
def validate_stream(self, options: Mapping[str, object]) -> None:
|
|
1140
|
+
_symbolic_matrix_options(options)
|
|
1141
|
+
|
|
1142
|
+
def __call__(
|
|
1143
|
+
self,
|
|
1144
|
+
inputs: Mapping[str, _native.Batch],
|
|
1145
|
+
options: Mapping[str, object],
|
|
1146
|
+
) -> dict[str, _native.Batch]:
|
|
1147
|
+
import numpy as np
|
|
1148
|
+
import pyarrow as pa
|
|
1149
|
+
|
|
1150
|
+
columns, names, expression = _symbolic_matrix_options(options)
|
|
1151
|
+
table_batch, weights_batch = _symbolic_matrix_inputs(inputs, self.backend)
|
|
1152
|
+
table = table_batch.to_pyarrow()
|
|
1153
|
+
table_dtypes = _validated_table_dtypes(table, columns)
|
|
1154
|
+
weights = _validated_weights(weights_batch, self.backend, len(columns))
|
|
1155
|
+
result_dtype = _common_matrix_dtype(
|
|
1156
|
+
self.backend, self.namespace, table_dtypes, weights
|
|
1157
|
+
)
|
|
1158
|
+
expected_dtype = _symbolic_tree_dtype(
|
|
1159
|
+
expression,
|
|
1160
|
+
self.backend,
|
|
1161
|
+
np.dtype(result_dtype).name,
|
|
1162
|
+
np.dtype(weights.dtype).name,
|
|
1163
|
+
)
|
|
1164
|
+
if not isinstance(expected_dtype, str):
|
|
1165
|
+
raise TypeError("invalid symbolic_matrix output.dtype: unresolved dtype")
|
|
1166
|
+
host = _numpy_table_matrix(table, columns, result_dtype)
|
|
1167
|
+
if self.backend == "jax":
|
|
1168
|
+
import jax
|
|
1169
|
+
|
|
1170
|
+
dense = jax.device_put(host, device=weights.device)
|
|
1171
|
+
else:
|
|
1172
|
+
dense = host
|
|
1173
|
+
result = _evaluate_symbolic_tree(
|
|
1174
|
+
expression,
|
|
1175
|
+
self.namespace,
|
|
1176
|
+
dense,
|
|
1177
|
+
weights,
|
|
1178
|
+
)
|
|
1179
|
+
result_shape = _result_shape(result)
|
|
1180
|
+
if len(result_shape) != 2:
|
|
1181
|
+
raise ValueError(
|
|
1182
|
+
"invalid symbolic_matrix output.rank: expected rank two; "
|
|
1183
|
+
f"received {len(result_shape)}"
|
|
1184
|
+
)
|
|
1185
|
+
if result_shape[0] != table.num_rows:
|
|
1186
|
+
raise ValueError(
|
|
1187
|
+
"invalid symbolic_matrix output.rows: expected "
|
|
1188
|
+
f"{table.num_rows}, received {result_shape[0]}"
|
|
1189
|
+
)
|
|
1190
|
+
if result_shape[1] != len(names):
|
|
1191
|
+
raise ValueError(
|
|
1192
|
+
"invalid symbolic_matrix output.width: expected "
|
|
1193
|
+
f"{len(names)}, received {result_shape[1]}"
|
|
1194
|
+
)
|
|
1195
|
+
output_host = np.asarray(result)
|
|
1196
|
+
actual_dtype = np.dtype(output_host.dtype).name
|
|
1197
|
+
if actual_dtype != expected_dtype:
|
|
1198
|
+
raise TypeError(
|
|
1199
|
+
"invalid symbolic_matrix output.dtype: expected "
|
|
1200
|
+
f"{expected_dtype}, received {actual_dtype}"
|
|
1201
|
+
)
|
|
1202
|
+
attached = table
|
|
1203
|
+
for index, name in enumerate(names):
|
|
1204
|
+
attached = attached.append_column(name, pa.array(output_host[:, index]))
|
|
1205
|
+
metadata = table_batch.metadata
|
|
1206
|
+
metadata.update(
|
|
1207
|
+
{
|
|
1208
|
+
"backend": self.backend,
|
|
1209
|
+
"copy_bytes": {
|
|
1210
|
+
"array_to_table": int(output_host.nbytes),
|
|
1211
|
+
"table_to_array": int(host.nbytes),
|
|
1212
|
+
"weights": int(
|
|
1213
|
+
weights_batch.metadata.get("static_placement_bytes", 0)
|
|
1214
|
+
),
|
|
1215
|
+
},
|
|
1216
|
+
"operation": "symbolic_matrix",
|
|
1217
|
+
"provider_calls": 1,
|
|
1218
|
+
}
|
|
1219
|
+
)
|
|
1220
|
+
return {"output": _native.Batch.from_pyarrow(attached, metadata)}
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def register_numpy(runtime: Runtime) -> None:
|
|
1224
|
+
import numpy as np
|
|
1225
|
+
|
|
1226
|
+
expression = _ArrayProvider("numpy", np)
|
|
1227
|
+
runtime.register_provider(
|
|
1228
|
+
"numpy",
|
|
1229
|
+
"expression",
|
|
1230
|
+
"1",
|
|
1231
|
+
expression,
|
|
1232
|
+
options_schema=_EXPRESSION_OPTIONS_SCHEMA,
|
|
1233
|
+
)
|
|
1234
|
+
runtime._register_stateless_stream_provider(
|
|
1235
|
+
"numpy",
|
|
1236
|
+
"expression",
|
|
1237
|
+
"1",
|
|
1238
|
+
expression,
|
|
1239
|
+
microbatch_invariant=True,
|
|
1240
|
+
deterministic=True,
|
|
1241
|
+
replay_safe=True,
|
|
1242
|
+
supports_static_inputs=False,
|
|
1243
|
+
array_rules=_NUMPY_STREAM_ARRAY_RULES,
|
|
1244
|
+
)
|
|
1245
|
+
runtime._register_mapping_provider(
|
|
1246
|
+
"numpy",
|
|
1247
|
+
"table_matmul",
|
|
1248
|
+
"1",
|
|
1249
|
+
_TableMatmulProvider("numpy", np),
|
|
1250
|
+
input_ports=_TABLE_MATMUL_INPUT_PORTS,
|
|
1251
|
+
output_ports=_TABLE_MATMUL_OUTPUT_PORTS,
|
|
1252
|
+
)
|
|
1253
|
+
symbolic_matrix = _SymbolicMatrixProvider("numpy", np)
|
|
1254
|
+
runtime._register_mapping_provider(
|
|
1255
|
+
"numpy",
|
|
1256
|
+
"symbolic_matrix",
|
|
1257
|
+
"1",
|
|
1258
|
+
symbolic_matrix,
|
|
1259
|
+
input_ports=_SYMBOLIC_MATRIX_INPUT_PORTS,
|
|
1260
|
+
output_ports=_SYMBOLIC_MATRIX_OUTPUT_PORTS,
|
|
1261
|
+
)
|
|
1262
|
+
runtime._register_stateless_stream_mapping_provider(
|
|
1263
|
+
"numpy",
|
|
1264
|
+
"symbolic_matrix",
|
|
1265
|
+
"1",
|
|
1266
|
+
symbolic_matrix,
|
|
1267
|
+
microbatch_invariant=True,
|
|
1268
|
+
deterministic=True,
|
|
1269
|
+
replay_safe=True,
|
|
1270
|
+
supports_static_inputs=True,
|
|
1271
|
+
array_rules=_NUMPY_MATRIX_STREAM_ARRAY_RULES,
|
|
1272
|
+
)
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def register_jax(runtime: Runtime) -> None:
|
|
1276
|
+
import jax.numpy as jnp
|
|
1277
|
+
|
|
1278
|
+
expression = _ArrayProvider("jax", jnp)
|
|
1279
|
+
runtime.register_provider(
|
|
1280
|
+
"jax",
|
|
1281
|
+
"expression",
|
|
1282
|
+
"1",
|
|
1283
|
+
expression,
|
|
1284
|
+
options_schema=_EXPRESSION_OPTIONS_SCHEMA,
|
|
1285
|
+
)
|
|
1286
|
+
runtime._register_stateless_stream_provider(
|
|
1287
|
+
"jax",
|
|
1288
|
+
"expression",
|
|
1289
|
+
"1",
|
|
1290
|
+
expression,
|
|
1291
|
+
microbatch_invariant=True,
|
|
1292
|
+
deterministic=True,
|
|
1293
|
+
replay_safe=True,
|
|
1294
|
+
supports_static_inputs=False,
|
|
1295
|
+
array_rules=_JAX_STREAM_ARRAY_RULES,
|
|
1296
|
+
)
|
|
1297
|
+
runtime._register_mapping_provider(
|
|
1298
|
+
"jax",
|
|
1299
|
+
"table_matmul",
|
|
1300
|
+
"1",
|
|
1301
|
+
_TableMatmulProvider("jax", jnp),
|
|
1302
|
+
input_ports=_TABLE_MATMUL_INPUT_PORTS,
|
|
1303
|
+
output_ports=_TABLE_MATMUL_OUTPUT_PORTS,
|
|
1304
|
+
)
|
|
1305
|
+
symbolic_matrix = _SymbolicMatrixProvider("jax", jnp)
|
|
1306
|
+
runtime._register_mapping_provider(
|
|
1307
|
+
"jax",
|
|
1308
|
+
"symbolic_matrix",
|
|
1309
|
+
"1",
|
|
1310
|
+
symbolic_matrix,
|
|
1311
|
+
input_ports=_SYMBOLIC_MATRIX_INPUT_PORTS,
|
|
1312
|
+
output_ports=_SYMBOLIC_MATRIX_OUTPUT_PORTS,
|
|
1313
|
+
)
|
|
1314
|
+
runtime._register_stateless_stream_mapping_provider(
|
|
1315
|
+
"jax",
|
|
1316
|
+
"symbolic_matrix",
|
|
1317
|
+
"1",
|
|
1318
|
+
symbolic_matrix,
|
|
1319
|
+
microbatch_invariant=True,
|
|
1320
|
+
deterministic=True,
|
|
1321
|
+
replay_safe=True,
|
|
1322
|
+
supports_static_inputs=True,
|
|
1323
|
+
array_rules=_JAX_MATRIX_STREAM_ARRAY_RULES,
|
|
1324
|
+
)
|