haliax 1.4.dev288__tar.gz → 1.4.dev290__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.dev290}/PKG-INFO +1 -1
  2. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/fp8.md +3 -2
  3. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/indexing.md +90 -3
  4. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/matmul.md +36 -1
  5. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/rearrange.md +1 -0
  6. haliax-1.4.dev290/src/haliax/__about__.py +1 -0
  7. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/einsum.py +102 -24
  8. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/parsing.py +12 -2
  9. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/core.py +328 -59
  10. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/types.py +3 -1
  11. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/core_test.py +107 -0
  12. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_einsum.py +80 -0
  13. haliax-1.4.dev288/src/haliax/__about__.py +0 -1
  14. {haliax-1.4.dev288 → haliax-1.4.dev290}/.coveragerc +0 -0
  15. {haliax-1.4.dev288 → haliax-1.4.dev290}/.flake8 +0 -0
  16. {haliax-1.4.dev288 → haliax-1.4.dev290}/.github/workflows/publish_dev.yaml +0 -0
  17. {haliax-1.4.dev288 → haliax-1.4.dev290}/.github/workflows/run_pre_commit.yaml +0 -0
  18. {haliax-1.4.dev288 → haliax-1.4.dev290}/.github/workflows/run_tests.yaml +0 -0
  19. {haliax-1.4.dev288 → haliax-1.4.dev290}/.gitignore +0 -0
  20. {haliax-1.4.dev288 → haliax-1.4.dev290}/.pre-commit-config.yaml +0 -0
  21. {haliax-1.4.dev288 → haliax-1.4.dev290}/.readthedocs.yaml +0 -0
  22. {haliax-1.4.dev288 → haliax-1.4.dev290}/CONTRIBUTING.md +0 -0
  23. {haliax-1.4.dev288 → haliax-1.4.dev290}/LICENSE +0 -0
  24. {haliax-1.4.dev288 → haliax-1.4.dev290}/README.md +0 -0
  25. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/api.md +0 -0
  26. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/broadcasting.md +0 -0
  27. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/cheatsheet.md +0 -0
  28. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/css/material.css +0 -0
  29. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/css/mkdocstrings.css +0 -0
  30. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/faq.md +0 -0
  31. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/data_parallel_mesh.png +0 -0
  32. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/data_parallel_mesh_replicated.png +0 -0
  33. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_1d.png +0 -0
  34. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_1d_zero.png +0 -0
  35. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_2d.png +0 -0
  36. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_batch_partitioned.png +0 -0
  37. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_data_replicated.png +0 -0
  38. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_data_replicated_mlp_partitioned.png +0 -0
  39. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_intermediate_fully_partitioned.png +0 -0
  40. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_zero.png +0 -0
  41. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/hof.md +0 -0
  42. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/index.md +0 -0
  43. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/nn.md +0 -0
  44. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/partitioning.md +0 -0
  45. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/rearrange.ipynb +0 -0
  46. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/requirements.txt +0 -0
  47. {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/tutorial.md +0 -0
  48. {haliax-1.4.dev288 → haliax-1.4.dev290}/mkdocs.yml +0 -0
  49. {haliax-1.4.dev288 → haliax-1.4.dev290}/pyproject.toml +0 -0
  50. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/__init__.py +0 -0
  51. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/__init__.py +0 -0
  52. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/compile_utils.py +0 -0
  53. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/dot.py +0 -0
  54. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/fp8.py +0 -0
  55. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/rearrange.py +0 -0
  56. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/util.py +0 -0
  57. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/axis.py +0 -0
  58. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/debug.py +0 -0
  59. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/hof.py +0 -0
  60. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/jax_utils.py +0 -0
  61. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/__init__.py +0 -0
  62. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/activations.py +0 -0
  63. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/attention.py +0 -0
  64. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/conv.py +0 -0
  65. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/dropout.py +0 -0
  66. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/embedding.py +0 -0
  67. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/linear.py +0 -0
  68. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/loss.py +0 -0
  69. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/mlp.py +0 -0
  70. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/normalization.py +0 -0
  71. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/pool.py +0 -0
  72. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/scan.py +0 -0
  73. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/ops.py +0 -0
  74. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/partitioning.py +0 -0
  75. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/quantization.py +0 -0
  76. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/random.py +0 -0
  77. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/specialized_fns.py +0 -0
  78. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/tree_util.py +0 -0
  79. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/util.py +0 -0
  80. {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/wrap.py +0 -0
  81. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_attention.py +0 -0
  82. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_axis.py +0 -0
  83. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_conv.py +0 -0
  84. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_debug.py +0 -0
  85. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_dot.py +0 -0
  86. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_fp8.py +0 -0
  87. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_hof.py +0 -0
  88. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_nn.py +0 -0
  89. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_ops.py +0 -0
  90. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_parsing.py +0 -0
  91. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_partitioning.py +0 -0
  92. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_pool.py +0 -0
  93. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_random.py +0 -0
  94. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_rearrange.py +0 -0
  95. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_scan.py +0 -0
  96. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_specialized_fns.py +0 -0
  97. {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_tree_util.py +0 -0
  98. {haliax-1.4.dev288 → haliax-1.4.dev290}/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.dev290
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/
@@ -1,7 +1,8 @@
1
1
  # FP8 Training
2
2
 
3
3
  !!! warning
4
- FP8 training in Haliax is currently experimental and may change in the future.
4
+
5
+ FP8 training in Haliax is currently experimental and may change in the future.
5
6
 
6
7
  FP8 refers to 8-bit floating point numbers. FP8 is a massively reduced precision compared to the 32-bit floating point numbers
7
8
  or 16-bit floating point numbers that are typically used in deep learning: there are only 256 possible values in FP8, compared to
@@ -123,7 +124,7 @@ you will get the gradient computation as normal, but you'll also get the updated
123
124
  This updated state needs to directly replace the state in the module (rather than be used for a gradient step), which is
124
125
  why you need to use the `partition_for_grad_overwrite`
125
126
 
126
- The FP8 `dot_general` module is implemented in [haliax.quantization.Fp8DotGeneral][]. It's actually not that complicated:
127
+ The FP8 `dot_general` module is implemented in [haliax.quantization.Fp8DotGeneralOp][]. It's actually not that complicated:
127
128
 
128
129
  1) It holds a scaling factor and history of maximum values for each of (lhs, rhs, output) and updates them based on the
129
130
  gradients.
@@ -37,12 +37,42 @@ Otherwise, the idea is pretty straightforward: any unspecified axes are treated
37
37
  slices are kept in reduced dimensions, and integers eliminate dimensions. If all dimensions are eliminated, a scalar
38
38
  JAX ndarray is returned.
39
39
 
40
+ The following types are supported for indexing:
41
+
42
+ * Integers, including scalar JAX arrays
43
+ * Slices
44
+ * [haliax.dslice][] objects (See [Dynamic Slices](#dynamic-slices) below.)
45
+ * Lists of integers
46
+ * Named arrays (See [Advanced Indexing](#advanced-indexing) below.)
47
+ * 1-D JAX Arrays of integers
48
+
49
+ 1-D JAX Arrays are interpreted as NamedArrays with a single axis with the same name as
50
+ the one they are slicing. That is:
51
+
52
+ ```python
53
+ import haliax as hax
54
+ import jax
55
+ import jax.numpy as jnp
56
+
57
+ X = hax.Axis("X", 10)
58
+ Y = hax.Axis("Y", 20)
59
+
60
+ a = hax.random.uniform(jax.random.PRNGKey(0), (X, Y))
61
+
62
+ sliced = a["X", jnp.array([1, 2, 3])]
63
+
64
+ # same as
65
+ a.array[jnp.array([1, 2, 3]), :]
66
+ ```
67
+
68
+ Note that boolean arrays are not supported, as JAX does not support them in JIT-compiled code. You
69
+ can use [haliax.where][] for most of the same functionality, though.
40
70
 
41
71
  ### Shapes in JAX
42
72
 
43
- Before we continue note on shapes in JAX. Most JAX code will be used inside `jit`, which means that the sizes of all arrays
44
- must be determined at compile time (i.e. when JAX interprets your functions abstractly). This is a hard requirement in
45
- XLA.
73
+ Before we continue, a note on shapes in JAX. Most JAX code will be used inside `jit`, which means that the sizes of all
74
+ arrays must be determined at compile time (i.e. when JAX interprets your functions abstractly). This is a hard
75
+ requirement in XLA. It might worked around one day, but it's the way things are for now.
46
76
 
47
77
  A consequence of this restriction is that certain indexing patterns aren't allowed in `jit`-ed JAX code:
48
78
 
@@ -177,3 +207,60 @@ a[{"Y": ind1}] # error, "X" is not eliminated by the indexing operation
177
207
 
178
208
  a[{"X": ind2, "Y": ind1}] # ok, because X and Y are eliminated by the indexing operation
179
209
  ```
210
+
211
+ ## Index Update
212
+
213
+ JAX is a functional version of NumPy, so it doesn't directly support in-place updates. It does
214
+ however [provide an `at` syntax](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.ndarray.at.html#jax.numpy.ndarray.at)
215
+ to express the same logic (and that will typically be optimized to be as efficient as an in-place update). Haliax
216
+ provides a similar syntax for updating arrays.
217
+
218
+ ```python
219
+ import haliax as hax
220
+
221
+ X = hax.Axis("X", 10)
222
+ Y = hax.Axis("Y", 20)
223
+ Z = hax.Axis("Z", 30)
224
+
225
+ a = hax.zeros((X, Y, Z))
226
+
227
+ a.at[{"X": 1, "Y": 2, "Z": 3}].set(1.0) # sets a[1, 2, 3] to 1.0
228
+ a.at["X", 1].set(2.0) # sets a[1, :, :] to 2.0
229
+
230
+ a.at[{"X": 1, "Y": hax.ds(3, 5), "Z": 3}].add(1.0) # adds 1.0 to a[1, 3:8, 3]
231
+ ```
232
+
233
+ Haliax supports the same `at` functionality as JAX, just with named arrays and additionally dslices. A summary of the
234
+ `at` syntax is as follows:
235
+
236
+ | Alternate Syntax | Equivalent In-Place Operation |
237
+ |------------------------------|-------------------------------|
238
+ | `x = x.at[idx].set(y)` | `x[idx] = y` |
239
+ | `x = x.at[idx].add(y)` | `x[idx] += y` |
240
+ | `x = x.at[idx].multiply(y)` | `x[idx] *= y` |
241
+ | `x = x.at[idx].divide(y)` | `x[idx] /= y` |
242
+ | `x = x.at[idx].power(y)` | `x[idx] **= y` |
243
+ | `x = x.at[idx].min(y)` | `x[idx] = minimum(x[idx], y)` |
244
+ | `x = x.at[idx].max(y)` | `x[idx] = maximum(x[idx], y)` |
245
+ | `x = x.at[idx].apply(ufunc)` | `ufunc.at(x, idx)` |
246
+ | `x = x.at[idx].get()` | `x = x[idx]` |
247
+
248
+ These methods also have options to control out-of-bounds behavior, as well as allowing you
249
+ to specify that the indices are sorted or unique. (If they are, XLA can sometimes optimize the
250
+ operation more effectively.)
251
+
252
+ !!! note
253
+
254
+ These named arguments are not passed to `at`, but to the next method in the chain.
255
+
256
+ (This is copied from the JAX documentation:)
257
+
258
+ * `mode`: One of `"promise_in_bounds"`, `"clip"`, `"drop"`, or `"fill"`. See [JAX's documentation](https://jax.readthedocs.io/en/latest/_autosummary/jax.lax.GatherScatterMode.html#jax.lax.GatherScatterMode) for more details.
259
+ * `indices_are_sorted`: If `True`, the implementation will assume that the indices passed to `at` are sorted in ascending order, which can lead to more efficient execution on some backends.
260
+ * `unique_indices`: If `True`, the implementation will assume that the indices passed to `at` are unique, which can result in more efficient execution on some backends.
261
+ * `fill_value`: Only applies to the `get()` method: the fill value to return for out-of-bounds slices when mode is 'fill'. Ignored otherwise. Defaults to NaN for inexact types, the largest negative value for signed types, the largest positive value for unsigned types, and True for booleans.
262
+
263
+ !!! tip
264
+
265
+ It's worth emphasizing that these functions are typically compiled to scatter-add and friends (as appropriate).
266
+ This is the preferred way to do scatter/gather operations in JAX, as well as in Haliax.
@@ -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
+ ```
@@ -54,6 +54,7 @@ If you're used to einops, the syntax should be familiar, with the main differenc
54
54
  and the additional "unordered" syntax for selecting dimensions by name.
55
55
 
56
56
  !!! warning
57
+
57
58
  This syntax is fairly new. It is pretty well-tested, but it is possible that there are bugs.
58
59
 
59
60
  ### Examples
@@ -0,0 +1 @@
1
+ __version__ = "1.4.dev290"
@@ -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