haliax 1.4.dev288__tar.gz → 1.4.dev289__tar.gz
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.
- {haliax-1.4.dev288 → haliax-1.4.dev289}/PKG-INFO +1 -1
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/matmul.md +36 -1
- haliax-1.4.dev289/src/haliax/__about__.py +1 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/einsum.py +102 -24
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/parsing.py +12 -2
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/core.py +2 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_einsum.py +80 -0
- haliax-1.4.dev288/src/haliax/__about__.py +0 -1
- {haliax-1.4.dev288 → haliax-1.4.dev289}/.coveragerc +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/.flake8 +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/.github/workflows/publish_dev.yaml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/.github/workflows/run_pre_commit.yaml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/.github/workflows/run_tests.yaml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/.gitignore +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/.pre-commit-config.yaml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/.readthedocs.yaml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/CONTRIBUTING.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/LICENSE +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/README.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/api.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/broadcasting.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/cheatsheet.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/css/material.css +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/css/mkdocstrings.css +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/faq.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/data_parallel_mesh.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/data_parallel_mesh_replicated.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_1d.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_1d_zero.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_2d.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_2d_batch_partitioned.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_2d_data_replicated.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_2d_data_replicated_mlp_partitioned.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_2d_intermediate_fully_partitioned.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_2d_zero.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/fp8.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/hof.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/index.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/indexing.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/nn.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/partitioning.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/rearrange.ipynb +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/rearrange.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/requirements.txt +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/tutorial.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/mkdocs.yml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/pyproject.toml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/__init__.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/__init__.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/compile_utils.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/dot.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/fp8.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/rearrange.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/util.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/axis.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/debug.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/hof.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/jax_utils.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/__init__.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/activations.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/attention.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/conv.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/dropout.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/embedding.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/linear.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/loss.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/mlp.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/normalization.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/pool.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/scan.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/ops.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/partitioning.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/quantization.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/random.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/specialized_fns.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/tree_util.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/types.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/util.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/wrap.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/core_test.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_attention.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_axis.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_conv.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_debug.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_dot.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_fp8.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_hof.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_nn.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_ops.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_parsing.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_partitioning.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_pool.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_random.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_rearrange.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_scan.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_specialized_fns.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_tree_util.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: haliax
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.dev289
|
|
4
4
|
Summary: Named Tensors for Legible Deep Learning in JAX
|
|
5
5
|
Project-URL: Homepage, https://github.com/stanford-crfm/haliax
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/stanford-crfm/haliax/issues/
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
Haliax has two ways to do matrix multiplication (and tensor contractions more generally):
|
|
4
4
|
[haliax.dot][] and [haliax.einsum][]. [haliax.dot][] and [haliax.einsum][]
|
|
5
5
|
can both express any tensor contraction, though in different situations one or the other may be
|
|
6
|
-
more suitable for expressing a particular contraction
|
|
6
|
+
more suitable for expressing a particular contraction In general:
|
|
7
|
+
|
|
8
|
+
- Use [haliax.dot][] when you want to express a simple matrix multiplication over one or a few axes.
|
|
9
|
+
- Use [haliax.einsum][] when you want to express a more complex tensor contraction.
|
|
7
10
|
|
|
8
11
|
See also the API reference for [haliax.dot][] and [haliax.einsum][] and the
|
|
9
12
|
[cheat sheet section](cheatsheet.md#matrix-multiplication).
|
|
@@ -68,6 +71,9 @@ Haliax's version of `einsum` comes in three modes: "ordered", "unordered", and "
|
|
|
68
71
|
These modes are all accessible through the same function without any flags: the syntax
|
|
69
72
|
of the `einsum` string determines which mode is used.
|
|
70
73
|
|
|
74
|
+
The syntax for Haliax's `einsum` is similar to [`haliax.rearrange`](rearrange.md), which
|
|
75
|
+
is in turn similar to [einops.rearrange](https://einops.rocks/api/rearrange/).
|
|
76
|
+
|
|
71
77
|
#### Ordered Mode
|
|
72
78
|
|
|
73
79
|
Haliax's `einsum` has an "ordered" mode that is similar to `einops.einsum`'s behavior.
|
|
@@ -119,6 +125,22 @@ y = hax.einsum("{H ...} -> ...", x) # shape is (W, D)
|
|
|
119
125
|
|
|
120
126
|
This mode is most similar to [haliax.dot][]'s behavior, though it's a bit more expressive.
|
|
121
127
|
|
|
128
|
+
You can also use axis aliases in the `einsum` string, which can be useful for expressing contractions
|
|
129
|
+
in library code or just for shortening the string:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
Height = hax.Axis("Height", 3)
|
|
133
|
+
Width = hax.Axis("Width", 4)
|
|
134
|
+
Depth = hax.Axis("Depth", 5)
|
|
135
|
+
|
|
136
|
+
x = hax.ones((Height, Width, Depth))
|
|
137
|
+
w = hax.ones((Depth,))
|
|
138
|
+
|
|
139
|
+
y = hax.einsum("{H W D} -> H W", x, H=Height, W=Width, D=Depth) # shape is (Height, Width)
|
|
140
|
+
y = hax.einsum("{D} -> ", w, D=Depth) # shape is (Height, Width)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
|
|
122
144
|
#### Output Axes Mode
|
|
123
145
|
|
|
124
146
|
In "output axes" mode, you only specify the axes that should be in the output. All other
|
|
@@ -142,3 +164,16 @@ y = hax.einsum("-> D", w) # shape is (D,)
|
|
|
142
164
|
|
|
143
165
|
We don't recommend using this mode except in cases when you're sure of the full shape of the input arrays
|
|
144
166
|
or you are sure you don't want to let users implicitly batch over any axes.
|
|
167
|
+
|
|
168
|
+
Output axes mode also supports axis aliases:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
Height = hax.Axis("Height", 3)
|
|
172
|
+
Width = hax.Axis("Width", 4)
|
|
173
|
+
Depth = hax.Axis("Depth", 5)
|
|
174
|
+
|
|
175
|
+
x = hax.ones((Height, Width, Depth))
|
|
176
|
+
w = hax.ones((Depth,))
|
|
177
|
+
y = hax.einsum("-> Height Width", x, Height=Height, Width=Width, Depth=Depth) # shape is (Height, Width)
|
|
178
|
+
y = hax.einsum("-> Depth", w, Depth=Depth) # shape is (Depth,)
|
|
179
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.4.dev289"
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
from types import EllipsisType
|
|
3
|
-
from typing import Optional,
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
4
|
|
|
5
5
|
import jax.lax
|
|
6
|
-
import jax.numpy as jnp
|
|
7
6
|
|
|
8
7
|
import haliax
|
|
9
8
|
|
|
10
|
-
from ..axis import AxisSelector, axis_name, eliminate_axes, rearrange_for_partial_order, union_axes
|
|
9
|
+
from ..axis import Axis, AxisSelector, axis_name, eliminate_axes, rearrange_for_partial_order, union_axes
|
|
11
10
|
from ..core import NamedArray
|
|
12
11
|
from ..jax_utils import _jittable_dg_einsum
|
|
13
12
|
from ..types import DTypeLike, PrecisionLike
|
|
@@ -21,6 +20,7 @@ def einsum(
|
|
|
21
20
|
precision: PrecisionLike = None,
|
|
22
21
|
preferred_element_type: Optional[DTypeLike] = None,
|
|
23
22
|
_dot_general=jax.lax.dot_general,
|
|
23
|
+
**axis_aliases,
|
|
24
24
|
) -> NamedArray:
|
|
25
25
|
"""Compute the tensor contraction of the input arrays according to Haliax's named variant of the Einstein summation
|
|
26
26
|
convention.
|
|
@@ -38,12 +38,16 @@ def einsum(
|
|
|
38
38
|
>>> hax.einsum("{H W D} -> H W", a, b)
|
|
39
39
|
>>> hax.einsum("{D} -> ", a, b) # same as the previous example
|
|
40
40
|
>>> hax.einsum("-> H W", a, b) # same as the first example
|
|
41
|
+
>>> # axis aliases, useful for generic code
|
|
42
|
+
>>> hax.einsum("{x y} -> y", a, b, x=H, y=W)
|
|
41
43
|
|
|
42
44
|
Args:
|
|
43
45
|
equation: The einsum equation.
|
|
44
46
|
arrays: The input arrays.
|
|
45
47
|
precision: The precision of the computation.
|
|
46
48
|
preferred_element_type: The preferred element type of the computation.
|
|
49
|
+
_dot_general: The dot_general function to use.
|
|
50
|
+
axis_aliases: The axis aliases to use.
|
|
47
51
|
|
|
48
52
|
Returns:
|
|
49
53
|
The result of the einsum.
|
|
@@ -59,17 +63,17 @@ def einsum(
|
|
|
59
63
|
# NB: we're using JAX's einsum which only supports one letter names for dims
|
|
60
64
|
if len(lhses) == 1 and len(lhses[0].captures) == 0 and lhses[0].is_ordered:
|
|
61
65
|
# case 3: get the output axes, contract the others
|
|
62
|
-
spec, out_axes = _output_only_named_einsum(equation, arrays, rhs)
|
|
66
|
+
spec, out_axes = _output_only_named_einsum(equation, arrays, rhs, axis_aliases)
|
|
63
67
|
elif len(lhses) == 1 and not lhses[0].is_ordered:
|
|
64
68
|
# case 2: some axes are named. Those named only on the lhs are contracted, the others are kept
|
|
65
69
|
# subcase: if there's an ellipsis on the lhs, we contract all the axes that are not named on the rhs
|
|
66
|
-
spec, out_axes = _unordered_einsum(arrays, equation, lhses, rhs)
|
|
70
|
+
spec, out_axes = _unordered_einsum(arrays, equation, lhses[0], rhs, axis_aliases)
|
|
67
71
|
else:
|
|
68
72
|
# general case: we have a normal einsum. we don't allow unordered axes here
|
|
69
73
|
if any(not lhs.is_ordered for lhs in lhses):
|
|
70
74
|
raise_parse_error("Cannot have multiple unordered axes in an einsum", equation, None)
|
|
71
75
|
|
|
72
|
-
spec, out_axes = _positional_einsum_spec(equation, arrays, lhses, rhs)
|
|
76
|
+
spec, out_axes = _positional_einsum_spec(equation, arrays, lhses, rhs, axis_aliases)
|
|
73
77
|
|
|
74
78
|
out_raw = _jittable_dg_einsum(
|
|
75
79
|
spec,
|
|
@@ -83,12 +87,14 @@ def einsum(
|
|
|
83
87
|
return haliax.auto_sharded(out)
|
|
84
88
|
|
|
85
89
|
|
|
86
|
-
def _unordered_einsum(arrays, equation,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
def _unordered_einsum(arrays, equation, lhs, rhs, axis_aliases):
|
|
91
|
+
candidate_axes, has_ellipsis_lhs, covered_lhs = _captures_to_axis_names(equation, lhs, axis_aliases)
|
|
92
|
+
rhs_axes, has_ellipsis_rhs, covered_rhs = _captures_to_axis_names(equation, rhs, axis_aliases)
|
|
93
|
+
|
|
94
|
+
for alias_name in axis_aliases:
|
|
95
|
+
if alias_name not in covered_lhs and alias_name not in covered_rhs:
|
|
96
|
+
raise_parse_error(f"Axis alias {alias_name} not used in the einsum", equation, None)
|
|
97
|
+
|
|
92
98
|
all_input_axes = _all_input_axes(arrays)
|
|
93
99
|
if has_ellipsis_rhs:
|
|
94
100
|
out_axes = rearrange_for_partial_order(rhs_axes, all_input_axes)
|
|
@@ -105,15 +111,17 @@ def _unordered_einsum(arrays, equation, lhses, rhs):
|
|
|
105
111
|
# what people expect
|
|
106
112
|
rhs_axes = [Ellipsis] + rhs_axes # type: ignore
|
|
107
113
|
out_axes = rearrange_for_partial_order(rhs_axes, almost_out_axes)
|
|
108
|
-
spec = _make_einsum_spec(
|
|
114
|
+
spec = _make_einsum_spec(arrays, out_axes)
|
|
109
115
|
return spec, out_axes
|
|
110
116
|
|
|
111
117
|
|
|
112
|
-
def _output_only_named_einsum(equation, arrays, rhs):
|
|
113
|
-
used_letters: set[str] = set()
|
|
114
|
-
name_mappings_for_einsum: dict[str, str] = {}
|
|
115
|
-
|
|
118
|
+
def _output_only_named_einsum(equation, arrays, rhs, axis_aliases):
|
|
116
119
|
out_axes = []
|
|
120
|
+
used_axes = set()
|
|
121
|
+
used_aliases = set()
|
|
122
|
+
|
|
123
|
+
input_axis_names = set(ax.name for ax in _all_input_axes(arrays))
|
|
124
|
+
|
|
117
125
|
for capture in rhs.captures:
|
|
118
126
|
if capture is Ellipsis:
|
|
119
127
|
raise_parse_error("Can't use ellipsis on the rhs of an einsum without an lhs", equation, None)
|
|
@@ -125,23 +133,51 @@ def _output_only_named_einsum(equation, arrays, rhs):
|
|
|
125
133
|
)
|
|
126
134
|
else:
|
|
127
135
|
name = capture.binding
|
|
136
|
+
used_aliases.add(name)
|
|
137
|
+
|
|
138
|
+
if name in axis_aliases:
|
|
139
|
+
# this could be axis or a name. if an axis, need to assert the size
|
|
140
|
+
axis = axis_aliases[name]
|
|
141
|
+
if isinstance(axis, Axis):
|
|
142
|
+
_check_axis_size_consistency(arrays, axis, name)
|
|
143
|
+
ax_name = axis_name(axis)
|
|
144
|
+
|
|
145
|
+
if ax_name in used_axes:
|
|
146
|
+
raise_parse_error(
|
|
147
|
+
f"Axis {name} occurs multiple times on the rhs. Probably because of multiple aliasing?",
|
|
148
|
+
equation,
|
|
149
|
+
capture.char_range,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
name = ax_name
|
|
153
|
+
used_axes.add(name)
|
|
128
154
|
|
|
129
155
|
if name in out_axes:
|
|
130
|
-
raise_parse_error(
|
|
156
|
+
raise_parse_error(
|
|
157
|
+
f"Axis capture {name} occurs multiple times on the rhs", equation, capture.char_range
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if name not in input_axis_names:
|
|
161
|
+
raise_parse_error(f"Axis {name} not found in any of the input arrays", equation, capture.char_range)
|
|
131
162
|
|
|
132
163
|
out_axes.append(name)
|
|
133
164
|
|
|
134
|
-
|
|
165
|
+
_check_for_unused_aliases(axis_aliases, used_aliases, equation)
|
|
166
|
+
|
|
167
|
+
spec = _make_einsum_spec(arrays, out_axes)
|
|
135
168
|
return spec, out_axes
|
|
136
169
|
|
|
137
170
|
|
|
138
|
-
def _positional_einsum_spec(equation, arrays, lhses, rhs):
|
|
171
|
+
def _positional_einsum_spec(equation, arrays, lhses, rhs, axis_aliases):
|
|
139
172
|
used_letters: set[str] = set()
|
|
140
173
|
name_mappings_for_einsum: dict[str, str] = {}
|
|
174
|
+
used_aliases = set()
|
|
141
175
|
|
|
142
176
|
if len(lhses) != len(arrays):
|
|
143
177
|
raise ValueError(f"Number of lhses ({len(lhses)}) does not match number of arrays ({len(arrays)})")
|
|
144
|
-
|
|
178
|
+
|
|
179
|
+
# For this function, axis_aliases exists entirely for checking axis sizes against what's in the arrays
|
|
180
|
+
table = AliasTable(axis_aliases)
|
|
145
181
|
# ok, we're going to lead pretty heavily on einsum here. We just need to figure out the names of the axes
|
|
146
182
|
# and do any error checking (that there are no mismatched names)
|
|
147
183
|
# once we do that, we can pass a slightly modified spec to einsum (namely that we shorten the names of the axes)
|
|
@@ -164,6 +200,9 @@ def _positional_einsum_spec(equation, arrays, lhses, rhs):
|
|
|
164
200
|
raise_parse_error("Parenthesized axes are not currently supported", equation, capture.char_range)
|
|
165
201
|
else:
|
|
166
202
|
name = capture.binding
|
|
203
|
+
if name in axis_aliases:
|
|
204
|
+
used_aliases.add(name)
|
|
205
|
+
|
|
167
206
|
if axis_off >= len(a.axes):
|
|
168
207
|
raise ValueError("Mismatched number of axes in einsum")
|
|
169
208
|
table.bind_alias(name, a.axes[axis_off], equation, capture.char_range)
|
|
@@ -184,6 +223,9 @@ def _positional_einsum_spec(equation, arrays, lhses, rhs):
|
|
|
184
223
|
break
|
|
185
224
|
else:
|
|
186
225
|
name = capture.binding
|
|
226
|
+
if name in axis_aliases:
|
|
227
|
+
used_aliases.add(name)
|
|
228
|
+
|
|
187
229
|
if axis_off < final_lhs_axis_off:
|
|
188
230
|
raise ValueError("Mismatched number of axes in einsum")
|
|
189
231
|
table.bind_alias(name, a.axes[axis_off], equation, capture.char_range)
|
|
@@ -230,6 +272,8 @@ def _positional_einsum_spec(equation, arrays, lhses, rhs):
|
|
|
230
272
|
spec += letter
|
|
231
273
|
out_axes.append(axis)
|
|
232
274
|
|
|
275
|
+
_check_for_unused_aliases(axis_aliases, used_aliases, equation)
|
|
276
|
+
|
|
233
277
|
if has_ellipsis_rhs:
|
|
234
278
|
all_input_axes = _all_input_axes(arrays)
|
|
235
279
|
# eliminate the axes that are contracted
|
|
@@ -244,7 +288,8 @@ def _all_input_axes(arrays):
|
|
|
244
288
|
return ensure_tuple(functools.reduce(union_axes, (a.axes for a in arrays), ())) # type: ignore
|
|
245
289
|
|
|
246
290
|
|
|
247
|
-
def _captures_to_axis_names(equation, lhs) -> Tuple[list[str | EllipsisType], bool]:
|
|
291
|
+
def _captures_to_axis_names(equation, lhs, aliases) -> Tuple[list[str | EllipsisType], bool, set[str]]:
|
|
292
|
+
covered_aliases = set()
|
|
248
293
|
candidate_axes: list[str | EllipsisType] = []
|
|
249
294
|
has_ellipsis = False
|
|
250
295
|
for capture in lhs.captures:
|
|
@@ -255,11 +300,17 @@ def _captures_to_axis_names(equation, lhs) -> Tuple[list[str | EllipsisType], bo
|
|
|
255
300
|
raise_parse_error("Parenthesized axes are not currently supported", equation, capture.char_range)
|
|
256
301
|
else:
|
|
257
302
|
name = capture.binding
|
|
303
|
+
if name in aliases:
|
|
304
|
+
covered_aliases.add(name)
|
|
305
|
+
axis = aliases[name]
|
|
306
|
+
name = axis_name(axis)
|
|
258
307
|
candidate_axes.append(name)
|
|
259
|
-
return candidate_axes, has_ellipsis
|
|
308
|
+
return candidate_axes, has_ellipsis, covered_aliases
|
|
260
309
|
|
|
261
310
|
|
|
262
|
-
def _make_einsum_spec(
|
|
311
|
+
def _make_einsum_spec(arrays, out_axes):
|
|
312
|
+
name_mappings_for_einsum: dict[str, str] = {}
|
|
313
|
+
used_letters: set[str] = set()
|
|
263
314
|
spec = ""
|
|
264
315
|
for operand in arrays:
|
|
265
316
|
if len(spec):
|
|
@@ -289,3 +340,30 @@ def _assign_letter_to_name(name, name_mappings_for_einsum, used_letters):
|
|
|
289
340
|
name_mappings_for_einsum[name] = letter
|
|
290
341
|
used_letters.add(letter)
|
|
291
342
|
return letter
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _check_axis_size_consistency(arrays, axis, name_in_spec):
|
|
346
|
+
# ensure the size is correct and the axis is present
|
|
347
|
+
found = False
|
|
348
|
+
ax_name = axis_name(axis)
|
|
349
|
+
for array_index, array in enumerate(arrays):
|
|
350
|
+
try:
|
|
351
|
+
resolved = array.resolve_axis(ax_name)
|
|
352
|
+
except ValueError:
|
|
353
|
+
pass
|
|
354
|
+
else:
|
|
355
|
+
found = True
|
|
356
|
+
if resolved.size != axis.size:
|
|
357
|
+
raise ValueError(
|
|
358
|
+
f"Size mismatch for axis {ax_name}. In array {array_index},"
|
|
359
|
+
f" {axis} has size {resolved.size} but expected {axis.size},"
|
|
360
|
+
f"because of the alias {name_in_spec}={axis}"
|
|
361
|
+
)
|
|
362
|
+
if not found:
|
|
363
|
+
raise ValueError(f"Axis {ax_name} not found in any of the input arrays")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _check_for_unused_aliases(axis_aliases, used_aliases, equation):
|
|
367
|
+
if any(alias not in used_aliases for alias in axis_aliases):
|
|
368
|
+
unused_aliases_str = ", ".join([alias for alias in axis_aliases if alias not in used_aliases])
|
|
369
|
+
raise_parse_error(f"Unused aliases from kwargs: {unused_aliases_str}", equation, None)
|
|
@@ -225,8 +225,9 @@ class AliasTable:
|
|
|
225
225
|
|
|
226
226
|
def __init__(self, bindings=None):
|
|
227
227
|
if bindings is None:
|
|
228
|
-
bindings = {}
|
|
229
|
-
|
|
228
|
+
self.bindings = {}
|
|
229
|
+
else:
|
|
230
|
+
self.bindings = {**bindings}
|
|
230
231
|
|
|
231
232
|
def dealias_binding(self, binding: str) -> Optional[AxisSelector]:
|
|
232
233
|
return self.bindings.get(binding, None)
|
|
@@ -235,6 +236,15 @@ class AliasTable:
|
|
|
235
236
|
if axis.name in self.bindings:
|
|
236
237
|
if self.bindings[alias] != axis:
|
|
237
238
|
raise_parse_error(f"Alias {alias} is assigned to more than one axis", expr, char_range)
|
|
239
|
+
elif alias in self.bindings:
|
|
240
|
+
current = self.bindings[alias]
|
|
241
|
+
if isinstance(current, Axis):
|
|
242
|
+
if current != axis:
|
|
243
|
+
raise_parse_error(f"Alias {alias} is assigned to more than one axis", expr, char_range)
|
|
244
|
+
elif current != axis.name:
|
|
245
|
+
raise_parse_error(f"Alias {alias} is assigned to more than one axis", expr, char_range)
|
|
246
|
+
else:
|
|
247
|
+
self.bindings[alias] = axis
|
|
238
248
|
else:
|
|
239
249
|
self.bindings[alias] = axis
|
|
240
250
|
|
|
@@ -184,6 +184,8 @@ class NamedArray:
|
|
|
184
184
|
"""
|
|
185
185
|
Returns the axes corresponding to the given axis selection.
|
|
186
186
|
That is, it return the [haliax.Axis][] values themselves, not just their names.
|
|
187
|
+
|
|
188
|
+
Raises a ValueError if any of the axes are not found.
|
|
187
189
|
"""
|
|
188
190
|
indices = self._lookup_indices(axes)
|
|
189
191
|
if isinstance(indices, int):
|
|
@@ -37,6 +37,28 @@ def test_einsum_basic_positional():
|
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def test_einsum_positional_aliases():
|
|
41
|
+
Height = Axis("Height", 2)
|
|
42
|
+
Width = Axis("Width", 3)
|
|
43
|
+
Depth = Axis("Depth", 4)
|
|
44
|
+
|
|
45
|
+
m1 = NamedArray(jnp.ones((Height.size, Width.size, Depth.size)), (Height, Width, Depth))
|
|
46
|
+
m2 = NamedArray(jnp.ones((Depth.size, Width.size, Height.size)), (Depth, Width, Height))
|
|
47
|
+
|
|
48
|
+
assert jnp.all(
|
|
49
|
+
jnp.equal(einsum("i j k,k j i-> j k", m1, m2, i=Height).array, jnp.einsum("ijk,kji->jk", m1.array, m2.array))
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
with pytest.raises(ValueError):
|
|
53
|
+
einsum("i j k,k j i-> j k", m1, m2, i=Width)
|
|
54
|
+
|
|
55
|
+
with pytest.raises(ValueError):
|
|
56
|
+
einsum("i j k,q j i-> j k", m1, m2, i=Height, q=Height)
|
|
57
|
+
|
|
58
|
+
with pytest.raises(ValueError):
|
|
59
|
+
einsum("i j k,k j i-> j k", m1, m2, i=Height, q=Height)
|
|
60
|
+
|
|
61
|
+
|
|
40
62
|
def test_einsum_basic_named():
|
|
41
63
|
Height = Axis("Height", 2)
|
|
42
64
|
Width = Axis("Width", 3)
|
|
@@ -148,6 +170,32 @@ def test_einsum_unordered_ellipses():
|
|
|
148
170
|
)
|
|
149
171
|
|
|
150
172
|
|
|
173
|
+
def test_einsum_unordered_aliases():
|
|
174
|
+
Height = Axis("Height", 2)
|
|
175
|
+
Width = Axis("Width", 3)
|
|
176
|
+
Depth = Axis("Depth", 4)
|
|
177
|
+
|
|
178
|
+
m1 = hax.ones((Height, Width, Depth))
|
|
179
|
+
m2 = hax.ones((Depth, Width, Height))
|
|
180
|
+
|
|
181
|
+
assert jnp.all(
|
|
182
|
+
jnp.equal(
|
|
183
|
+
einsum("{h w d} -> h w", m1, m2, h=Height, w=Width, d=Depth).array,
|
|
184
|
+
jnp.einsum("ijk,kji->ij", m1.array, m2.array),
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# test error cases:
|
|
189
|
+
|
|
190
|
+
# Missing alias
|
|
191
|
+
with pytest.raises(ValueError, match="Axis d not present"):
|
|
192
|
+
einsum("{h w d} -> h w", m1, m2, h=Height, w=Width)
|
|
193
|
+
|
|
194
|
+
# Extra alias
|
|
195
|
+
with pytest.raises(ValueError, match="Axis alias d not used"):
|
|
196
|
+
einsum("{h w} -> h w", m1, m2, h=Height, w=Width, d=Depth)
|
|
197
|
+
|
|
198
|
+
|
|
151
199
|
def test_einsum_ordered_ellipsis():
|
|
152
200
|
Height = Axis("Height", 2)
|
|
153
201
|
Width = Axis("Width", 3)
|
|
@@ -272,3 +320,35 @@ def test_einsum_examples():
|
|
|
272
320
|
hax_out = hax.einsum("{...} -> ", hax_im, hax_w2)
|
|
273
321
|
jnp_out = jnp.einsum("bhwc,ce -> ", im, w2)
|
|
274
322
|
assert jnp.all(jnp.equal(hax_out.array, jnp_out))
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def test_einsum_output_only_mode():
|
|
326
|
+
# tests "-> out axes"
|
|
327
|
+
Height = Axis("Height", 2)
|
|
328
|
+
Width = Axis("Width", 3)
|
|
329
|
+
Depth = Axis("Depth", 4)
|
|
330
|
+
|
|
331
|
+
m1 = hax.ones((Height, Width, Depth))
|
|
332
|
+
m2 = hax.ones((Depth, Width, Height))
|
|
333
|
+
m3 = hax.ones((Height, Depth))
|
|
334
|
+
|
|
335
|
+
assert jnp.all(jnp.equal(einsum("-> Height Width", m1, m2).array, jnp.einsum("ijk,kji->ij", m1.array, m2.array)))
|
|
336
|
+
assert jnp.all(jnp.equal(einsum("-> Height", m1).array, jnp.einsum("ijk->i", m1.array)))
|
|
337
|
+
|
|
338
|
+
with pytest.raises(ValueError):
|
|
339
|
+
einsum("-> Q Width", m1)
|
|
340
|
+
|
|
341
|
+
with pytest.raises(ValueError, match=".*Unused aliases from kwargs: Q$"):
|
|
342
|
+
einsum("-> Height Width", m1, m2, Q=Axis("Q", 2))
|
|
343
|
+
|
|
344
|
+
assert jnp.all(jnp.equal(einsum("-> h w", m1, h=Height, w=Width).array, jnp.einsum("ijk->ij", m1.array)))
|
|
345
|
+
|
|
346
|
+
assert jnp.all(
|
|
347
|
+
jnp.equal(einsum("-> h w", m1, m3, h=Height, w=Width).array, jnp.einsum("ijk,ik->ij", m1.array, m3.array))
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
with pytest.raises(ValueError, match=".*Size mismatch.*"):
|
|
351
|
+
einsum("-> h w", m1, h=Height.resize(4), w=Width)
|
|
352
|
+
|
|
353
|
+
with pytest.raises(ValueError, match=".*not found in any of the input arrays.*"):
|
|
354
|
+
einsum("-> h w", m3, h=Height, w=Width.resize(4))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.4.dev288"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|