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.
Files changed (98) hide show
  1. {haliax-1.4.dev288 → haliax-1.4.dev289}/PKG-INFO +1 -1
  2. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/matmul.md +36 -1
  3. haliax-1.4.dev289/src/haliax/__about__.py +1 -0
  4. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/einsum.py +102 -24
  5. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/parsing.py +12 -2
  6. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/core.py +2 -0
  7. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_einsum.py +80 -0
  8. haliax-1.4.dev288/src/haliax/__about__.py +0 -1
  9. {haliax-1.4.dev288 → haliax-1.4.dev289}/.coveragerc +0 -0
  10. {haliax-1.4.dev288 → haliax-1.4.dev289}/.flake8 +0 -0
  11. {haliax-1.4.dev288 → haliax-1.4.dev289}/.github/workflows/publish_dev.yaml +0 -0
  12. {haliax-1.4.dev288 → haliax-1.4.dev289}/.github/workflows/run_pre_commit.yaml +0 -0
  13. {haliax-1.4.dev288 → haliax-1.4.dev289}/.github/workflows/run_tests.yaml +0 -0
  14. {haliax-1.4.dev288 → haliax-1.4.dev289}/.gitignore +0 -0
  15. {haliax-1.4.dev288 → haliax-1.4.dev289}/.pre-commit-config.yaml +0 -0
  16. {haliax-1.4.dev288 → haliax-1.4.dev289}/.readthedocs.yaml +0 -0
  17. {haliax-1.4.dev288 → haliax-1.4.dev289}/CONTRIBUTING.md +0 -0
  18. {haliax-1.4.dev288 → haliax-1.4.dev289}/LICENSE +0 -0
  19. {haliax-1.4.dev288 → haliax-1.4.dev289}/README.md +0 -0
  20. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/api.md +0 -0
  21. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/broadcasting.md +0 -0
  22. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/cheatsheet.md +0 -0
  23. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/css/material.css +0 -0
  24. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/css/mkdocstrings.css +0 -0
  25. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/faq.md +0 -0
  26. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/data_parallel_mesh.png +0 -0
  27. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/data_parallel_mesh_replicated.png +0 -0
  28. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_1d.png +0 -0
  29. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_1d_zero.png +0 -0
  30. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_2d.png +0 -0
  31. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_2d_batch_partitioned.png +0 -0
  32. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_2d_data_replicated.png +0 -0
  33. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_2d_data_replicated_mlp_partitioned.png +0 -0
  34. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_2d_intermediate_fully_partitioned.png +0 -0
  35. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/figures/device_mesh_2d_zero.png +0 -0
  36. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/fp8.md +0 -0
  37. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/hof.md +0 -0
  38. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/index.md +0 -0
  39. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/indexing.md +0 -0
  40. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/nn.md +0 -0
  41. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/partitioning.md +0 -0
  42. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/rearrange.ipynb +0 -0
  43. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/rearrange.md +0 -0
  44. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/requirements.txt +0 -0
  45. {haliax-1.4.dev288 → haliax-1.4.dev289}/docs/tutorial.md +0 -0
  46. {haliax-1.4.dev288 → haliax-1.4.dev289}/mkdocs.yml +0 -0
  47. {haliax-1.4.dev288 → haliax-1.4.dev289}/pyproject.toml +0 -0
  48. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/__init__.py +0 -0
  49. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/__init__.py +0 -0
  50. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/compile_utils.py +0 -0
  51. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/dot.py +0 -0
  52. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/fp8.py +0 -0
  53. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/rearrange.py +0 -0
  54. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/_src/util.py +0 -0
  55. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/axis.py +0 -0
  56. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/debug.py +0 -0
  57. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/hof.py +0 -0
  58. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/jax_utils.py +0 -0
  59. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/__init__.py +0 -0
  60. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/activations.py +0 -0
  61. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/attention.py +0 -0
  62. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/conv.py +0 -0
  63. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/dropout.py +0 -0
  64. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/embedding.py +0 -0
  65. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/linear.py +0 -0
  66. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/loss.py +0 -0
  67. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/mlp.py +0 -0
  68. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/normalization.py +0 -0
  69. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/pool.py +0 -0
  70. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/nn/scan.py +0 -0
  71. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/ops.py +0 -0
  72. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/partitioning.py +0 -0
  73. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/quantization.py +0 -0
  74. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/random.py +0 -0
  75. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/specialized_fns.py +0 -0
  76. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/tree_util.py +0 -0
  77. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/types.py +0 -0
  78. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/util.py +0 -0
  79. {haliax-1.4.dev288 → haliax-1.4.dev289}/src/haliax/wrap.py +0 -0
  80. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/core_test.py +0 -0
  81. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_attention.py +0 -0
  82. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_axis.py +0 -0
  83. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_conv.py +0 -0
  84. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_debug.py +0 -0
  85. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_dot.py +0 -0
  86. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_fp8.py +0 -0
  87. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_hof.py +0 -0
  88. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_nn.py +0 -0
  89. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_ops.py +0 -0
  90. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_parsing.py +0 -0
  91. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_partitioning.py +0 -0
  92. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_pool.py +0 -0
  93. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_random.py +0 -0
  94. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_rearrange.py +0 -0
  95. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_scan.py +0 -0
  96. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_specialized_fns.py +0 -0
  97. {haliax-1.4.dev288 → haliax-1.4.dev289}/tests/test_tree_util.py +0 -0
  98. {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.dev288
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, Sequence, Tuple, Union
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, lhses, rhs):
87
- used_letters: set[str] = set()
88
- name_mappings_for_einsum: dict[str, str] = {}
89
- lhs = lhses[0]
90
- candidate_axes, has_ellipsis_lhs = _captures_to_axis_names(equation, lhs)
91
- rhs_axes, has_ellipsis_rhs = _captures_to_axis_names(equation, rhs)
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(name_mappings_for_einsum, used_letters, arrays, out_axes)
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(f"Axis name {name} occurs multiple times on the rhs", equation, capture.char_range)
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
- spec = _make_einsum_spec(name_mappings_for_einsum, used_letters, arrays, out_axes)
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
- table = AliasTable()
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(name_mappings_for_einsum, used_letters, arrays, out_axes):
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
- self.bindings = bindings
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