galley-jl-python 0.1.0__py3-none-any.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.
- galley_jl_python/__init__.py +361 -0
- galley_jl_python/_array_api_info.py +94 -0
- galley_jl_python/_sysimage.py +222 -0
- galley_jl_python/compiled.py +171 -0
- galley_jl_python/dtypes.py +61 -0
- galley_jl_python/einstein.py +561 -0
- galley_jl_python/errors.py +2 -0
- galley_jl_python/finch_fused.py +33 -0
- galley_jl_python/io.py +15 -0
- galley_jl_python/julia.py +37 -0
- galley_jl_python/juliapkg.json +386 -0
- galley_jl_python/juliapkg_dev.json +22 -0
- galley_jl_python/levels.py +110 -0
- galley_jl_python/linalg/__init__.py +3 -0
- galley_jl_python/linalg/_linalg.py +26 -0
- galley_jl_python/tensor.py +1739 -0
- galley_jl_python/typing.py +17 -0
- galley_jl_python-0.1.0.dist-info/METADATA +214 -0
- galley_jl_python-0.1.0.dist-info/RECORD +21 -0
- galley_jl_python-0.1.0.dist-info/WHEEL +4 -0
- galley_jl_python-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from lark import Lark, Tree
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EinsumExpr(ABC):
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def get_loops(self) -> set[str]:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def run(self, xp, loops, kwargs):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
nary_ops = {
|
|
19
|
+
"+": "add",
|
|
20
|
+
"add": "add",
|
|
21
|
+
"-": "subtract",
|
|
22
|
+
"sub": "subtract",
|
|
23
|
+
"subtract": "subtract",
|
|
24
|
+
"*": "multiply",
|
|
25
|
+
"mul": "multiply",
|
|
26
|
+
"multiply": "multiply",
|
|
27
|
+
"/": "divide",
|
|
28
|
+
"div": "divide",
|
|
29
|
+
"divide": "divide",
|
|
30
|
+
"//": "floor_divide",
|
|
31
|
+
"fld": "floor_divide",
|
|
32
|
+
"floor_divide": "floor_divide",
|
|
33
|
+
"%": "remainder",
|
|
34
|
+
"mod": "remainder",
|
|
35
|
+
"remainder": "remainder",
|
|
36
|
+
"**": "power",
|
|
37
|
+
"pow": "power",
|
|
38
|
+
"power": "power",
|
|
39
|
+
"==": "equal",
|
|
40
|
+
"eq": "equal",
|
|
41
|
+
"equal": "equal",
|
|
42
|
+
"!=": "not_equal",
|
|
43
|
+
"ne": "not_equal",
|
|
44
|
+
"not_equal": "not_equal",
|
|
45
|
+
"<": "less",
|
|
46
|
+
"lt": "less",
|
|
47
|
+
"less": "less",
|
|
48
|
+
"<=": "less_equal",
|
|
49
|
+
"le": "less_equal",
|
|
50
|
+
"less_equal": "less_equal",
|
|
51
|
+
">": "greater",
|
|
52
|
+
"gt": "greater",
|
|
53
|
+
"greater": "greater",
|
|
54
|
+
">=": "greater_equal",
|
|
55
|
+
"ge": "greater_equal",
|
|
56
|
+
"greater_equal": "greater_equal",
|
|
57
|
+
"&": "bitwise_and",
|
|
58
|
+
"bitwise_and": "bitwise_and",
|
|
59
|
+
"|": "bitwise_or",
|
|
60
|
+
"bitwise_or": "bitwise_or",
|
|
61
|
+
"^": "bitwise_xor",
|
|
62
|
+
"bitwise_xor": "bitwise_xor",
|
|
63
|
+
"<<": "bitwise_left_shift",
|
|
64
|
+
"lshift": "bitwise_left_shift",
|
|
65
|
+
"bitwise_left_shift": "bitwise_left_shift",
|
|
66
|
+
">>": "bitwise_right_shift",
|
|
67
|
+
"rshift": "bitwise_right_shift",
|
|
68
|
+
"bitwise_right_shift": "bitwise_right_shift",
|
|
69
|
+
"and": "logical_and",
|
|
70
|
+
"or": "logical_or",
|
|
71
|
+
"not": "logical_not",
|
|
72
|
+
"min": "minimum",
|
|
73
|
+
"max": "maximum",
|
|
74
|
+
"logaddexp": "logaddexp",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
unary_ops = {
|
|
79
|
+
"+": "positive",
|
|
80
|
+
"pos": "positive",
|
|
81
|
+
"positive": "positive",
|
|
82
|
+
"-": "negative",
|
|
83
|
+
"neg": "negative",
|
|
84
|
+
"negative": "negative",
|
|
85
|
+
"~": "bitwise_invert",
|
|
86
|
+
"invert": "bitwise_invert",
|
|
87
|
+
"bitwise_invert": "bitwise_invert",
|
|
88
|
+
"not": "logical_not",
|
|
89
|
+
"logical_not": "logical_not",
|
|
90
|
+
"abs": "absolute",
|
|
91
|
+
"absolute": "absolute",
|
|
92
|
+
"sqrt": "sqrt",
|
|
93
|
+
"exp": "exp",
|
|
94
|
+
"log": "log",
|
|
95
|
+
"log1p": "log1p",
|
|
96
|
+
"log10": "log10",
|
|
97
|
+
"log2": "log2",
|
|
98
|
+
"sin": "sin",
|
|
99
|
+
"cos": "cos",
|
|
100
|
+
"tan": "tan",
|
|
101
|
+
"sinh": "sinh",
|
|
102
|
+
"cosh": "cosh",
|
|
103
|
+
"tanh": "tanh",
|
|
104
|
+
"asin": "arcsin",
|
|
105
|
+
"acos": "arccos",
|
|
106
|
+
"atan": "arctan",
|
|
107
|
+
"asinh": "arcsinh",
|
|
108
|
+
"acosh": "arccosh",
|
|
109
|
+
"atanh": "arctanh",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
reduction_ops = {
|
|
114
|
+
"+": "sum",
|
|
115
|
+
"add": "sum",
|
|
116
|
+
"sum": "sum",
|
|
117
|
+
"*": "prod",
|
|
118
|
+
"mul": "prod",
|
|
119
|
+
"prod": "prod",
|
|
120
|
+
"and": "all",
|
|
121
|
+
"or": "any",
|
|
122
|
+
"min": "min",
|
|
123
|
+
"max": "max",
|
|
124
|
+
"argmin": "argmin",
|
|
125
|
+
"argmax": "argmax",
|
|
126
|
+
"mean": "mean",
|
|
127
|
+
"std": "std",
|
|
128
|
+
"var": "var",
|
|
129
|
+
"count_nonzero": "count_nonzero",
|
|
130
|
+
# "&": "bitwise_and",
|
|
131
|
+
# "|": "bitwise_or",
|
|
132
|
+
# "^": "bitwise_xor",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class Access(EinsumExpr):
|
|
138
|
+
tns: str
|
|
139
|
+
idxs: list[str]
|
|
140
|
+
|
|
141
|
+
def get_loops(self) -> set[str]:
|
|
142
|
+
return set(self.idxs)
|
|
143
|
+
|
|
144
|
+
def run(self, xp, loops, kwargs):
|
|
145
|
+
assert len(self.idxs) == len(set(self.idxs))
|
|
146
|
+
perm = [self.idxs.index(idx) for idx in loops if idx in self.idxs]
|
|
147
|
+
tns = kwargs[self.tns]
|
|
148
|
+
tns = xp.permute_dims(tns, perm)
|
|
149
|
+
return xp.expand_dims(
|
|
150
|
+
tns, [i for i in range(len(loops)) if loops[i] not in self.idxs]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass
|
|
155
|
+
class Literal(EinsumExpr):
|
|
156
|
+
value: bool | int | float | complex
|
|
157
|
+
|
|
158
|
+
def get_loops(self) -> set[str]:
|
|
159
|
+
return set()
|
|
160
|
+
|
|
161
|
+
def run(self, xp, loops, kwargs):
|
|
162
|
+
# Create a scalar array with the same shape as needed
|
|
163
|
+
shape = [1] * len(loops)
|
|
164
|
+
return xp.full(shape, self.value, format="dense")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class Call(EinsumExpr):
|
|
169
|
+
func: str
|
|
170
|
+
args: list[EinsumExpr]
|
|
171
|
+
|
|
172
|
+
def get_loops(self) -> set[str]:
|
|
173
|
+
return set().union(*[arg.get_loops() for arg in self.args])
|
|
174
|
+
|
|
175
|
+
def run(self, xp, loops, kwargs):
|
|
176
|
+
if len(self.args) == 1:
|
|
177
|
+
func = getattr(xp, unary_ops[self.func])
|
|
178
|
+
else:
|
|
179
|
+
func = getattr(xp, nary_ops[self.func])
|
|
180
|
+
vals = [arg.run(xp, loops, kwargs) for arg in self.args]
|
|
181
|
+
return func(*vals)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class Einsum:
|
|
186
|
+
arg: EinsumExpr
|
|
187
|
+
op: str | None
|
|
188
|
+
tns: str
|
|
189
|
+
idxs: list[str]
|
|
190
|
+
|
|
191
|
+
def run(self, xp, kwargs):
|
|
192
|
+
# This is the main entry point for einsum execution
|
|
193
|
+
loops = self.arg.get_loops()
|
|
194
|
+
assert set(self.idxs).issubset(loops)
|
|
195
|
+
loops = sorted(loops)
|
|
196
|
+
arg = self.arg.run(xp, loops, kwargs)
|
|
197
|
+
axis = tuple(i for i in range(len(loops)) if loops[i] not in self.idxs)
|
|
198
|
+
if self.op is not None:
|
|
199
|
+
op = getattr(xp, reduction_ops.get(self.op))
|
|
200
|
+
val = op(arg, axis=axis)
|
|
201
|
+
else:
|
|
202
|
+
assert set(self.idxs) == set(loops)
|
|
203
|
+
val = arg
|
|
204
|
+
dropped = [idx for idx in loops if idx in self.idxs]
|
|
205
|
+
axis = [dropped.index(idx) for idx in self.idxs]
|
|
206
|
+
return xp.permute_dims(val, axis)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
lark_parser = Lark("""
|
|
210
|
+
%import common.CNAME
|
|
211
|
+
%import common.SIGNED_INT
|
|
212
|
+
%import common.SIGNED_FLOAT
|
|
213
|
+
%ignore " " // Disregard spaces in text
|
|
214
|
+
|
|
215
|
+
start: increment | assign
|
|
216
|
+
increment: access (OP | FUNC_NAME) "=" expr
|
|
217
|
+
assign: access "=" expr
|
|
218
|
+
|
|
219
|
+
// Python operator precedence (lowest to highest)
|
|
220
|
+
expr: or_expr
|
|
221
|
+
or_expr: and_expr (OR and_expr)*
|
|
222
|
+
and_expr: not_expr (AND not_expr)*
|
|
223
|
+
not_expr: NOT not_expr | comparison_expr
|
|
224
|
+
comparison_expr: bitwise_or_expr ((EQ | NE | LT | LE | GT | GE) bitwise_or_expr)*
|
|
225
|
+
bitwise_or_expr: bitwise_xor_expr (PIPE bitwise_xor_expr)*
|
|
226
|
+
bitwise_xor_expr: bitwise_and_expr (CARET bitwise_and_expr)*
|
|
227
|
+
bitwise_and_expr: shift_expr (AMPERSAND shift_expr)*
|
|
228
|
+
shift_expr: add_expr ((LSHIFT | RSHIFT) add_expr)*
|
|
229
|
+
add_expr: mul_expr ((PLUS | MINUS) mul_expr)*
|
|
230
|
+
mul_expr: unary_expr ((MUL | DIV | FLOORDIV | MOD) unary_expr)*
|
|
231
|
+
unary_expr: (PLUS | MINUS | TILDE) unary_expr | power_expr
|
|
232
|
+
power_expr: primary (POW unary_expr)?
|
|
233
|
+
primary: call_func | access | literal | "(" expr ")"
|
|
234
|
+
|
|
235
|
+
OR: "or"
|
|
236
|
+
AND: "and"
|
|
237
|
+
NOT: "not"
|
|
238
|
+
EQ: "=="
|
|
239
|
+
NE: "!="
|
|
240
|
+
LT: "<"
|
|
241
|
+
LE: "<="
|
|
242
|
+
GT: ">"
|
|
243
|
+
GE: ">="
|
|
244
|
+
PIPE: "|"
|
|
245
|
+
CARET: "^"
|
|
246
|
+
AMPERSAND: "&"
|
|
247
|
+
LSHIFT: "<<"
|
|
248
|
+
RSHIFT: ">>"
|
|
249
|
+
PLUS: "+"
|
|
250
|
+
MINUS: "-"
|
|
251
|
+
MUL: "*"
|
|
252
|
+
DIV: "/"
|
|
253
|
+
FLOORDIV: "//"
|
|
254
|
+
MOD: "%"
|
|
255
|
+
POW: "**"
|
|
256
|
+
TILDE: "~"
|
|
257
|
+
|
|
258
|
+
OP: "+" | "-" | "*" | "or" | "and" | "|" | "&" | "^" | "<<" | ">>"
|
|
259
|
+
| "//" | "/" | "%" | "**" | ">" | "<" | ">=" | "<=" | "==" | "!="
|
|
260
|
+
|
|
261
|
+
access: TNS "[" (IDX ",")* IDX? "]"
|
|
262
|
+
call_func: (FUNC_NAME "(" (expr ",")* expr? ")")
|
|
263
|
+
literal: bool_literal | complex_literal | float_literal | int_literal
|
|
264
|
+
bool_literal: BOOL
|
|
265
|
+
int_literal: SIGNED_INT
|
|
266
|
+
float_literal: SIGNED_FLOAT
|
|
267
|
+
complex_literal: COMPLEX
|
|
268
|
+
|
|
269
|
+
BOOL: "True" | "False"
|
|
270
|
+
COMPLEX: (SIGNED_FLOAT | SIGNED_INT) ("j" | "J")
|
|
271
|
+
IDX: CNAME
|
|
272
|
+
TNS: CNAME
|
|
273
|
+
FUNC_NAME: CNAME
|
|
274
|
+
""")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _parse_einop_expr(t: Tree) -> EinsumExpr:
|
|
278
|
+
match t:
|
|
279
|
+
case Tree(
|
|
280
|
+
"start"
|
|
281
|
+
| "expr"
|
|
282
|
+
| "or_expr"
|
|
283
|
+
| "and_expr"
|
|
284
|
+
| "not_expr"
|
|
285
|
+
| "comparison_expr"
|
|
286
|
+
| "bitwise_or_expr"
|
|
287
|
+
| "bitwise_xor_expr"
|
|
288
|
+
| "bitwise_and_expr"
|
|
289
|
+
| "shift_expr"
|
|
290
|
+
| "add_expr"
|
|
291
|
+
| "mul_expr"
|
|
292
|
+
| "unary_expr"
|
|
293
|
+
| "power_expr"
|
|
294
|
+
| "primary"
|
|
295
|
+
| "literal",
|
|
296
|
+
[child],
|
|
297
|
+
):
|
|
298
|
+
return _parse_einop_expr(child)
|
|
299
|
+
case Tree(
|
|
300
|
+
"or_expr"
|
|
301
|
+
| "and_expr"
|
|
302
|
+
| "bitwise_or_expr"
|
|
303
|
+
| "bitwise_and_expr"
|
|
304
|
+
| "bitwise_xor_expr"
|
|
305
|
+
| "shift_expr"
|
|
306
|
+
| "add_expr"
|
|
307
|
+
| "mul_expr",
|
|
308
|
+
args,
|
|
309
|
+
) if len(args) > 1:
|
|
310
|
+
expr = _parse_einop_expr(args[0])
|
|
311
|
+
for i in range(1, len(args), 2):
|
|
312
|
+
arg = _parse_einop_expr(args[i + 1])
|
|
313
|
+
expr = Call(args[i].value, [expr, arg]) # type: ignore[union-attr]
|
|
314
|
+
return expr
|
|
315
|
+
case Tree("comparison_expr", args) if len(args) > 1:
|
|
316
|
+
# Handle Python's comparison chaining: a < b < c becomes (a < b) and (b < c)
|
|
317
|
+
left = _parse_einop_expr(args[0])
|
|
318
|
+
right = _parse_einop_expr(args[2])
|
|
319
|
+
expr = Call(args[1].value, [left, right]) # type: ignore[union-attr]
|
|
320
|
+
for i in range(2, len(args) - 2, 2):
|
|
321
|
+
left = _parse_einop_expr(args[i])
|
|
322
|
+
right = _parse_einop_expr(args[i + 2])
|
|
323
|
+
expr = Call("and", [expr, Call(args[i + 1].value, [left, right])]) # type: ignore[union-attr]
|
|
324
|
+
return expr
|
|
325
|
+
case Tree("power_expr", args) if len(args) > 1:
|
|
326
|
+
left = _parse_einop_expr(args[0])
|
|
327
|
+
right = _parse_einop_expr(args[2])
|
|
328
|
+
return Call(args[1].value, [left, right]) # type: ignore[union-attr]
|
|
329
|
+
case Tree("unary_expr" | "not_expr", [op, arg]):
|
|
330
|
+
return Call(op.value, [_parse_einop_expr(arg)]) # type: ignore[union-attr]
|
|
331
|
+
case Tree("access", [tns, *idxs]):
|
|
332
|
+
return Access(tns.value, [idx.value for idx in idxs]) # type: ignore[union-attr]
|
|
333
|
+
case Tree("bool_literal", [val]):
|
|
334
|
+
return Literal(val.value == "True") # type: ignore[union-attr]
|
|
335
|
+
case Tree("int_literal", [val]):
|
|
336
|
+
return Literal(int(val.value)) # type: ignore[union-attr]
|
|
337
|
+
case Tree("float_literal", [val]):
|
|
338
|
+
return Literal(float(val.value)) # type: ignore[union-attr]
|
|
339
|
+
case Tree("complex_literal", [val]):
|
|
340
|
+
return Literal(complex(val.value)) # type: ignore[union-attr]
|
|
341
|
+
case Tree("call_func", [func, *args]):
|
|
342
|
+
return Call(func.value, [_parse_einop_expr(arg) for arg in args]) # type: ignore[union-attr]
|
|
343
|
+
case _:
|
|
344
|
+
raise ValueError(f"Unknown tree structure: {t}")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def parse_einop(expr: str) -> Einsum:
|
|
348
|
+
tree = lark_parser.parse(expr)
|
|
349
|
+
|
|
350
|
+
match tree:
|
|
351
|
+
case Tree(
|
|
352
|
+
"start", [Tree("increment", [Tree("access", [tns, *idxs]), op, expr_node])]
|
|
353
|
+
):
|
|
354
|
+
input_expr = _parse_einop_expr(expr_node) # type: ignore[arg-type]
|
|
355
|
+
return Einsum(input_expr, op.value, tns.value, [idx.value for idx in idxs]) # type: ignore[union-attr]
|
|
356
|
+
|
|
357
|
+
case Tree("start", [Tree("assign", [Tree("access", [tns, *idxs]), expr_node])]):
|
|
358
|
+
input_expr = _parse_einop_expr(expr_node) # type: ignore[arg-type]
|
|
359
|
+
return Einsum(input_expr, None, tns.value, [idx.value for idx in idxs]) # type: ignore[union-attr]
|
|
360
|
+
|
|
361
|
+
case _:
|
|
362
|
+
raise ValueError(
|
|
363
|
+
f"Expected top-level assignment or increment, got {tree.data}"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def einop_impl(xp, prgm, **kwargs):
|
|
368
|
+
"""Execute an einsum expression using the specified array framework.
|
|
369
|
+
|
|
370
|
+
This function parses and executes einsum-like expressions with extended syntax
|
|
371
|
+
that supports various operations beyond traditional Einstein summation notation.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
xp: Array framework module (e.g., numpy, cupy, or other array library)
|
|
375
|
+
that provides the underlying array operations.
|
|
376
|
+
prgm (str): Einsum program string specifying the computation. The syntax
|
|
377
|
+
supports:
|
|
378
|
+
- Assignment: "C[i,j] = A[i,j] + B[j,i]"
|
|
379
|
+
- Increment: "C[i,j] += A[i,k] * B[k,j]"
|
|
380
|
+
- Reductions: "C[i] += A[i,j]", "C[i] max= A[i,j]", "C[i] &= A[i,j]"
|
|
381
|
+
- Arithmetic operations: +, -, *, /, //, %, **
|
|
382
|
+
- Comparison operations: ==, !=, <, <=, >, >=
|
|
383
|
+
- Logical operations: and, or, not
|
|
384
|
+
- Bitwise operations: &, |, ^, <<, >>
|
|
385
|
+
- Function calls and complex expressions with parentheses
|
|
386
|
+
- Mathematical functions: abs, sqrt, exp, log, sin, cos, tan, etc.
|
|
387
|
+
- Literal values: integers, floats, booleans, and complex numbers
|
|
388
|
+
- Python operator precedence and parentheses for grouping
|
|
389
|
+
**kwargs: Named arrays referenced in the einsum expression. The keys
|
|
390
|
+
should match the tensor names used in the program string.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
The result array from executing the einsum expression.
|
|
394
|
+
|
|
395
|
+
Examples:
|
|
396
|
+
>>> import numpy as np
|
|
397
|
+
>>> A = np.random.rand(3, 4)
|
|
398
|
+
>>> B = np.random.rand(4, 3)
|
|
399
|
+
>>> # Matrix addition with transpose
|
|
400
|
+
>>> C = einop_impl(np, "C[i,j] = A[i,j] + B[j,i]", A=A, B=B)
|
|
401
|
+
>>> # Matrix multiplication
|
|
402
|
+
>>> D = einop_impl(np, "D[i,j] += A[i,k] * B[k,j]", A=A, B=B)
|
|
403
|
+
>>> # Min-Plus multiplication with shift
|
|
404
|
+
>>> E = einop_impl(np, "E[i] min= A[i,k] + D[k,j] << 1", A=A, D=D)
|
|
405
|
+
"""
|
|
406
|
+
prgm = parse_einop(prgm)
|
|
407
|
+
kwargs = {var: xp.Tensor(tns) for var, tns in kwargs.items()}
|
|
408
|
+
res = prgm.run(xp, {var: xp.lazy(tns) for var, tns in kwargs.items()})
|
|
409
|
+
if all(tns.is_computed() for tns in kwargs.values()):
|
|
410
|
+
return xp.compute(res)
|
|
411
|
+
return res
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def parse_einsum(*args_) -> tuple[Einsum, dict[str, Any]]:
|
|
415
|
+
args = list(args_)
|
|
416
|
+
if len(args) < 2:
|
|
417
|
+
raise ValueError("Expected at least a subscript string and one operand.")
|
|
418
|
+
bc = "none"
|
|
419
|
+
if isinstance(args[0], str):
|
|
420
|
+
subscripts = args[0]
|
|
421
|
+
operands = args[1:]
|
|
422
|
+
if subscripts.count("->") > 1:
|
|
423
|
+
raise ValueError("Subscripts can only contain one '->' symbol.")
|
|
424
|
+
if subscripts.count("->") == 1:
|
|
425
|
+
subscripts, output_sub = subscripts.split("->")
|
|
426
|
+
output_sub = output_sub.strip()
|
|
427
|
+
else:
|
|
428
|
+
output_sub = None
|
|
429
|
+
input_subs = [s.strip() for s in subscripts.split(",")]
|
|
430
|
+
# Check for ellipses in input subscripts
|
|
431
|
+
if any("..." in sub for sub in input_subs):
|
|
432
|
+
if all(sub.startswith("...") for sub in input_subs):
|
|
433
|
+
bc = "prefix"
|
|
434
|
+
input_subs = [sub[3:] for sub in input_subs]
|
|
435
|
+
if output_sub is not None:
|
|
436
|
+
assert output_sub.startswith("...")
|
|
437
|
+
output_sub = output_sub[3:]
|
|
438
|
+
elif all(sub.endswith("...") for sub in input_subs):
|
|
439
|
+
bc = "suffix"
|
|
440
|
+
input_subs = [sub[:-3] for sub in input_subs]
|
|
441
|
+
if output_sub is not None:
|
|
442
|
+
assert output_sub.endswith("...")
|
|
443
|
+
output_sub = output_sub[:-3]
|
|
444
|
+
else:
|
|
445
|
+
raise ValueError(
|
|
446
|
+
"Ellipses must be at the start or end of all subscripts."
|
|
447
|
+
)
|
|
448
|
+
input_idxs = [list(sub) for sub in input_subs]
|
|
449
|
+
output_idxs = None if output_sub is None else list(output_sub)
|
|
450
|
+
else:
|
|
451
|
+
# Alternative syntax: einsum(operand0, subscript0, operand1, subscript1, ...)
|
|
452
|
+
# Check if the last element is the output subscript
|
|
453
|
+
if len(args) % 2 == 1:
|
|
454
|
+
operands = args[0:-2:2]
|
|
455
|
+
input_idxs = args[1::2]
|
|
456
|
+
output_idxs = list(args[-1])
|
|
457
|
+
output_idxs = [f"j_{j}" for j in output_idxs]
|
|
458
|
+
else:
|
|
459
|
+
operands = args[0::2]
|
|
460
|
+
input_idxs = args[1::2]
|
|
461
|
+
output_idxs = None
|
|
462
|
+
input_idxs = [[f"j_{j}" for j in idx] for idx in input_idxs]
|
|
463
|
+
if any(Ellipsis in idx for idx in input_idxs):
|
|
464
|
+
if all(idx[0] == Ellipsis for idx in input_idxs):
|
|
465
|
+
bc = "prefix"
|
|
466
|
+
input_idxs = [idx[1:] for idx in input_idxs]
|
|
467
|
+
if output_idxs is not None:
|
|
468
|
+
assert output_idxs[0] == Ellipsis
|
|
469
|
+
output_idxs = output_idxs[1:]
|
|
470
|
+
elif all(idx[-1] == Ellipsis for idx in input_idxs):
|
|
471
|
+
bc = "suffix"
|
|
472
|
+
input_idxs = [idx[:-1] for idx in input_idxs]
|
|
473
|
+
if output_idxs is not None:
|
|
474
|
+
assert output_idxs[-1] == Ellipsis
|
|
475
|
+
output_idxs = output_idxs[:-1]
|
|
476
|
+
else:
|
|
477
|
+
raise ValueError(
|
|
478
|
+
"Ellipses must be at the start or end of all subscripts."
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
all_idxs = set().union(*input_idxs)
|
|
482
|
+
|
|
483
|
+
if output_idxs is None:
|
|
484
|
+
output_idx_set = set()
|
|
485
|
+
for idx in all_idxs:
|
|
486
|
+
if sum(idx in sub for sub in input_idxs) == 1:
|
|
487
|
+
output_idx_set.add(idx)
|
|
488
|
+
output_idxs = sorted(output_idx_set)
|
|
489
|
+
|
|
490
|
+
def ndim(tns):
|
|
491
|
+
if hasattr(tns, "ndim"):
|
|
492
|
+
return tns.ndim
|
|
493
|
+
return 0
|
|
494
|
+
|
|
495
|
+
if bc == "prefix":
|
|
496
|
+
max_ell_len = max(
|
|
497
|
+
ndim(op) - len(sub) for op, sub in zip(operands, input_idxs, strict=False)
|
|
498
|
+
)
|
|
499
|
+
for i in range(len(operands)):
|
|
500
|
+
ell_idxs = [
|
|
501
|
+
f"i_{j}"
|
|
502
|
+
for j in range(
|
|
503
|
+
max_ell_len - (ndim(operands[i]) - len(input_idxs[i])), max_ell_len
|
|
504
|
+
)
|
|
505
|
+
]
|
|
506
|
+
input_idxs[i] = ell_idxs + input_idxs[i]
|
|
507
|
+
ell_idxs = [f"i_{j}" for j in range(max_ell_len)]
|
|
508
|
+
output_idxs = [f"i_{j}" for j in range(max_ell_len)] + output_idxs
|
|
509
|
+
elif bc == "suffix":
|
|
510
|
+
max_ell_len = max(
|
|
511
|
+
ndim(op) - len(sub) for op, sub in zip(operands, input_idxs, strict=False)
|
|
512
|
+
)
|
|
513
|
+
for i in range(len(operands)):
|
|
514
|
+
ell_idxs = [f"k_{j}" for j in range(ndim(operands[i]) - len(input_idxs[i]))]
|
|
515
|
+
input_idxs[i] = input_idxs[i] + ell_idxs
|
|
516
|
+
output_idxs = output_idxs + [f"k_{j}" for j in range(max_ell_len)]
|
|
517
|
+
|
|
518
|
+
all_idxs = set().union(*input_idxs)
|
|
519
|
+
|
|
520
|
+
if len(input_idxs) != len(operands):
|
|
521
|
+
raise ValueError("Number of input subscripts must match number of operands.")
|
|
522
|
+
assert set(output_idxs).issubset(all_idxs), (
|
|
523
|
+
"Output indices must be a subset of input indices."
|
|
524
|
+
)
|
|
525
|
+
tag = 0
|
|
526
|
+
|
|
527
|
+
def freshen(x):
|
|
528
|
+
nonlocal tag
|
|
529
|
+
tag += 1
|
|
530
|
+
return f"{x}_{tag}"
|
|
531
|
+
|
|
532
|
+
for j in all_idxs:
|
|
533
|
+
freshen(j)
|
|
534
|
+
op = None if output_idxs == all_idxs else "add"
|
|
535
|
+
out_tns = freshen("B")
|
|
536
|
+
idxs = tuple(output_idxs)
|
|
537
|
+
in_tnss = [freshen("A") for _ in operands]
|
|
538
|
+
arg = Access(in_tnss[0], input_idxs[0])
|
|
539
|
+
for i in range(1, len(operands)):
|
|
540
|
+
arg = Call(
|
|
541
|
+
"mul",
|
|
542
|
+
(arg, Access(in_tnss[i], input_idxs[i])),
|
|
543
|
+
) # type: ignore[assignment]
|
|
544
|
+
return (
|
|
545
|
+
Einsum(
|
|
546
|
+
arg,
|
|
547
|
+
op,
|
|
548
|
+
out_tns,
|
|
549
|
+
idxs,
|
|
550
|
+
),
|
|
551
|
+
{in_tnss[i]: operands[i] for i in range(len(operands))},
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def einsum_impl(xp, *args):
|
|
556
|
+
prgm, kwargs = parse_einsum(*args)
|
|
557
|
+
kwargs = {var: xp.Tensor(tns) for var, tns in kwargs.items()}
|
|
558
|
+
res = prgm.run(xp, {var: xp.lazy(tns) for var, tns in kwargs.items()})
|
|
559
|
+
if all(tns.is_computed() for tns in kwargs.values()):
|
|
560
|
+
return xp.compute(res)
|
|
561
|
+
return res
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from finch.finch_fused import jit as _finch_jit
|
|
5
|
+
|
|
6
|
+
from .compiled import compute, lazy
|
|
7
|
+
from .tensor import Tensor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _defer(value: Any) -> Any:
|
|
11
|
+
if isinstance(value, tuple):
|
|
12
|
+
return tuple(_defer(item) for item in value)
|
|
13
|
+
return lazy(value) if isinstance(value, Tensor) else value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _compute(value: Any) -> Any:
|
|
17
|
+
if isinstance(value, tuple):
|
|
18
|
+
return tuple(_compute(item) for item in value)
|
|
19
|
+
return compute(value) if isinstance(value, Tensor) else value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _maybedefer(values: tuple[Any, ...]) -> tuple[Any, ...]:
|
|
23
|
+
return tuple(_defer(value) for value in values)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def jit(f: Callable[..., Any], /, ctx: Any = None) -> Callable[..., Any]:
|
|
27
|
+
transformed = _finch_jit(f, ctx=ctx)
|
|
28
|
+
transformed.__globals__.update(
|
|
29
|
+
defer=_defer,
|
|
30
|
+
compute=_compute,
|
|
31
|
+
maybedefer=_maybedefer,
|
|
32
|
+
)
|
|
33
|
+
return transformed
|
galley_jl_python/io.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from .julia import jl
|
|
4
|
+
from .tensor import Tensor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def read(filename: Path | str) -> Tensor:
|
|
8
|
+
fn = str(filename)
|
|
9
|
+
julia_obj = jl.fread(fn)
|
|
10
|
+
return Tensor(julia_obj)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def write(filename: Path | str, tns: Tensor) -> None:
|
|
14
|
+
fn = str(filename)
|
|
15
|
+
jl.fwrite(fn, tns._obj)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import ssl # noqa: I001
|
|
2
|
+
|
|
3
|
+
# Julia 1.12, which galley pins, bundles OpenSSL 3.5, and Python and Julia share
|
|
4
|
+
# one process, so juliapkg only allows Julia 1.12 with a Python linked against
|
|
5
|
+
# OpenSSL 3.5 or newer. Without this check the failure is juliapkg finding no
|
|
6
|
+
# compatible Julia version.
|
|
7
|
+
if ssl.OPENSSL_VERSION_INFO[:2] < (3, 5):
|
|
8
|
+
raise ImportError(
|
|
9
|
+
"galley-jl-python needs a Python linked against OpenSSL 3.5 or newer, but "
|
|
10
|
+
f"this one uses {ssl.OPENSSL_VERSION}. Galley runs Julia 1.12, which "
|
|
11
|
+
"bundles OpenSSL 3.5 and shares this process with Python. Use a Python "
|
|
12
|
+
"from conda-forge, e.g. through pixi or conda, which ships a current "
|
|
13
|
+
"OpenSSL."
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
import juliapkg # noqa: E402, F401
|
|
17
|
+
|
|
18
|
+
from . import _sysimage # noqa: E402
|
|
19
|
+
|
|
20
|
+
_sysimage.configure()
|
|
21
|
+
|
|
22
|
+
# To change the version of Finch used, see the documentation for pyjuliapkg here: https://github.com/JuliaPy/pyjuliapkg
|
|
23
|
+
# Use pyjuliapkg to modify the `juliapkg.json` file in the root of this repo.
|
|
24
|
+
# You can also run `develop.py` to quickly use a local copy of Finch.jl.
|
|
25
|
+
# An example development json is found in `juliapkg_dev.json`
|
|
26
|
+
# Julia and all packages are pinned so the prebuilt sysimage matches; update the
|
|
27
|
+
# pins with `scripts/sysimage/pin_julia_deps.py`. Finch is pinned to a git
|
|
28
|
+
# commit (`rev`), which that script leaves alone.
|
|
29
|
+
import juliacall as jc # noqa: E402, F401
|
|
30
|
+
from juliacall import Main as jl # noqa: E402, F401
|
|
31
|
+
|
|
32
|
+
jl.seval("using Finch")
|
|
33
|
+
jl.seval("using HDF5")
|
|
34
|
+
jl.seval("using NPZ")
|
|
35
|
+
jl.seval("using TensorMarket")
|
|
36
|
+
jl.seval("using Random")
|
|
37
|
+
jl.seval("using Statistics")
|