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.
- {haliax-1.4.dev288 → haliax-1.4.dev290}/PKG-INFO +1 -1
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/fp8.md +3 -2
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/indexing.md +90 -3
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/matmul.md +36 -1
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/rearrange.md +1 -0
- haliax-1.4.dev290/src/haliax/__about__.py +1 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/einsum.py +102 -24
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/parsing.py +12 -2
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/core.py +328 -59
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/types.py +3 -1
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/core_test.py +107 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_einsum.py +80 -0
- haliax-1.4.dev288/src/haliax/__about__.py +0 -1
- {haliax-1.4.dev288 → haliax-1.4.dev290}/.coveragerc +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/.flake8 +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/.github/workflows/publish_dev.yaml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/.github/workflows/run_pre_commit.yaml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/.github/workflows/run_tests.yaml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/.gitignore +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/.pre-commit-config.yaml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/.readthedocs.yaml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/CONTRIBUTING.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/LICENSE +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/README.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/api.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/broadcasting.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/cheatsheet.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/css/material.css +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/css/mkdocstrings.css +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/faq.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/data_parallel_mesh.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/data_parallel_mesh_replicated.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_1d.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_1d_zero.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_2d.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_batch_partitioned.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_data_replicated.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_data_replicated_mlp_partitioned.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_intermediate_fully_partitioned.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_zero.png +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/hof.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/index.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/nn.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/partitioning.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/rearrange.ipynb +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/requirements.txt +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/docs/tutorial.md +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/mkdocs.yml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/pyproject.toml +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/__init__.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/__init__.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/compile_utils.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/dot.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/fp8.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/rearrange.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/_src/util.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/axis.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/debug.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/hof.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/jax_utils.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/__init__.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/activations.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/attention.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/conv.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/dropout.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/embedding.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/linear.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/loss.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/mlp.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/normalization.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/pool.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/nn/scan.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/ops.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/partitioning.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/quantization.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/random.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/specialized_fns.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/tree_util.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/util.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/src/haliax/wrap.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_attention.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_axis.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_conv.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_debug.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_dot.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_fp8.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_hof.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_nn.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_ops.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_parsing.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_partitioning.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_pool.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_random.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_rearrange.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_scan.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_specialized_fns.py +0 -0
- {haliax-1.4.dev288 → haliax-1.4.dev290}/tests/test_tree_util.py +0 -0
- {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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
44
|
-
must be determined at compile time (i.e. when JAX interprets your functions abstractly). This is a hard
|
|
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,
|
|
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
|
|