haliax 1.4.dev289__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.dev289 → haliax-1.4.dev290}/PKG-INFO +1 -1
  2. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/fp8.md +3 -2
  3. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/indexing.md +90 -3
  4. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/rearrange.md +1 -0
  5. haliax-1.4.dev290/src/haliax/__about__.py +1 -0
  6. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/core.py +326 -59
  7. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/types.py +3 -1
  8. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/core_test.py +107 -0
  9. haliax-1.4.dev289/src/haliax/__about__.py +0 -1
  10. {haliax-1.4.dev289 → haliax-1.4.dev290}/.coveragerc +0 -0
  11. {haliax-1.4.dev289 → haliax-1.4.dev290}/.flake8 +0 -0
  12. {haliax-1.4.dev289 → haliax-1.4.dev290}/.github/workflows/publish_dev.yaml +0 -0
  13. {haliax-1.4.dev289 → haliax-1.4.dev290}/.github/workflows/run_pre_commit.yaml +0 -0
  14. {haliax-1.4.dev289 → haliax-1.4.dev290}/.github/workflows/run_tests.yaml +0 -0
  15. {haliax-1.4.dev289 → haliax-1.4.dev290}/.gitignore +0 -0
  16. {haliax-1.4.dev289 → haliax-1.4.dev290}/.pre-commit-config.yaml +0 -0
  17. {haliax-1.4.dev289 → haliax-1.4.dev290}/.readthedocs.yaml +0 -0
  18. {haliax-1.4.dev289 → haliax-1.4.dev290}/CONTRIBUTING.md +0 -0
  19. {haliax-1.4.dev289 → haliax-1.4.dev290}/LICENSE +0 -0
  20. {haliax-1.4.dev289 → haliax-1.4.dev290}/README.md +0 -0
  21. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/api.md +0 -0
  22. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/broadcasting.md +0 -0
  23. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/cheatsheet.md +0 -0
  24. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/css/material.css +0 -0
  25. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/css/mkdocstrings.css +0 -0
  26. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/faq.md +0 -0
  27. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/figures/data_parallel_mesh.png +0 -0
  28. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/figures/data_parallel_mesh_replicated.png +0 -0
  29. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/figures/device_mesh_1d.png +0 -0
  30. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/figures/device_mesh_1d_zero.png +0 -0
  31. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/figures/device_mesh_2d.png +0 -0
  32. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_batch_partitioned.png +0 -0
  33. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_data_replicated.png +0 -0
  34. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_data_replicated_mlp_partitioned.png +0 -0
  35. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_intermediate_fully_partitioned.png +0 -0
  36. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/figures/device_mesh_2d_zero.png +0 -0
  37. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/hof.md +0 -0
  38. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/index.md +0 -0
  39. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/matmul.md +0 -0
  40. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/nn.md +0 -0
  41. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/partitioning.md +0 -0
  42. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/rearrange.ipynb +0 -0
  43. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/requirements.txt +0 -0
  44. {haliax-1.4.dev289 → haliax-1.4.dev290}/docs/tutorial.md +0 -0
  45. {haliax-1.4.dev289 → haliax-1.4.dev290}/mkdocs.yml +0 -0
  46. {haliax-1.4.dev289 → haliax-1.4.dev290}/pyproject.toml +0 -0
  47. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/__init__.py +0 -0
  48. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/_src/__init__.py +0 -0
  49. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/_src/compile_utils.py +0 -0
  50. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/_src/dot.py +0 -0
  51. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/_src/einsum.py +0 -0
  52. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/_src/fp8.py +0 -0
  53. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/_src/parsing.py +0 -0
  54. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/_src/rearrange.py +0 -0
  55. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/_src/util.py +0 -0
  56. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/axis.py +0 -0
  57. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/debug.py +0 -0
  58. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/hof.py +0 -0
  59. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/jax_utils.py +0 -0
  60. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/nn/__init__.py +0 -0
  61. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/nn/activations.py +0 -0
  62. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/nn/attention.py +0 -0
  63. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/nn/conv.py +0 -0
  64. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/nn/dropout.py +0 -0
  65. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/nn/embedding.py +0 -0
  66. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/nn/linear.py +0 -0
  67. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/nn/loss.py +0 -0
  68. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/nn/mlp.py +0 -0
  69. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/nn/normalization.py +0 -0
  70. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/nn/pool.py +0 -0
  71. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/nn/scan.py +0 -0
  72. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/ops.py +0 -0
  73. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/partitioning.py +0 -0
  74. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/quantization.py +0 -0
  75. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/random.py +0 -0
  76. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/specialized_fns.py +0 -0
  77. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/tree_util.py +0 -0
  78. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/util.py +0 -0
  79. {haliax-1.4.dev289 → haliax-1.4.dev290}/src/haliax/wrap.py +0 -0
  80. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_attention.py +0 -0
  81. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_axis.py +0 -0
  82. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_conv.py +0 -0
  83. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_debug.py +0 -0
  84. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_dot.py +0 -0
  85. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_einsum.py +0 -0
  86. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_fp8.py +0 -0
  87. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_hof.py +0 -0
  88. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_nn.py +0 -0
  89. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_ops.py +0 -0
  90. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_parsing.py +0 -0
  91. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_partitioning.py +0 -0
  92. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_pool.py +0 -0
  93. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_random.py +0 -0
  94. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_rearrange.py +0 -0
  95. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_scan.py +0 -0
  96. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_specialized_fns.py +0 -0
  97. {haliax-1.4.dev289 → haliax-1.4.dev290}/tests/test_tree_util.py +0 -0
  98. {haliax-1.4.dev289 → 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.dev289
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.
@@ -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"
@@ -9,7 +9,6 @@ from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple
9
9
 
10
10
  import jax
11
11
  import jax.numpy as jnp
12
- import numpy
13
12
  import numpy as np
14
13
 
15
14
  import haliax
@@ -18,22 +17,21 @@ from haliax.jax_utils import is_jax_array_like, is_pallas_dslice
18
17
  from haliax.util import ensure_tuple
19
18
 
20
19
  from ._src.util import index_where, py_slice, slice_t
21
- from .axis import (
22
- Axis,
23
- AxisSelection,
24
- AxisSelector,
25
- AxisSpec,
26
- axis_name,
27
- dslice,
28
- eliminate_axes,
29
- selects_axis,
30
- union_axes,
31
- )
32
- from .types import DTypeLike, IntScalar, PrecisionLike, Scalar
20
+ from .axis import Axis, AxisSelection, AxisSelector, AxisSpec, axis_name, dslice, eliminate_axes, selects_axis
21
+ from .types import GatherScatterModeStr, IntScalar, PrecisionLike, Scalar
33
22
 
34
23
 
35
24
  NamedOrNumeric = Union[Scalar, "NamedArray"]
36
- NamedIndex = Union[int, slice_t, "NamedArray", dslice]
25
+ NamedIndex = Union[int, slice_t, "NamedArray", dslice, list[int], jnp.ndarray]
26
+
27
+ SliceSpec = Union[
28
+ tuple[AxisSelector, NamedIndex],
29
+ tuple[AxisSelector, NamedIndex, AxisSelector, NamedIndex],
30
+ tuple[AxisSelector, NamedIndex, AxisSelector, NamedIndex, AxisSelector, NamedIndex],
31
+ tuple[AxisSelector | NamedOrNumeric, ...],
32
+ Mapping[AxisSelector, NamedIndex],
33
+ ]
34
+
37
35
 
38
36
  _ENABLE_SHAPE_CHECKS = True
39
37
 
@@ -330,19 +328,47 @@ class NamedArray:
330
328
  def take(self, axis: AxisSelector, index: Union[int, "NamedArray"]) -> "NamedArray":
331
329
  return haliax.take(self, axis=axis, index=index)
332
330
 
333
- @overload
334
- def __getitem__(self, item: Tuple[AxisSelector, NamedIndex]) -> "NamedArray":
335
- ...
331
+ @property
332
+ def at(self) -> "_NamedIndexUpdateHelper":
333
+ """
334
+ Named analog of [jax's at method](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.ndarray.at.html).
336
335
 
337
- @overload
338
- def __getitem__(self, item: Tuple[AxisSelector, NamedIndex, AxisSelector, NamedIndex]) -> "NamedArray":
339
- ...
336
+ Docs from the JAX docs:
340
337
 
341
- @overload
342
- def __getitem__(self, item: Mapping[AxisSelector, NamedIndex]) -> "NamedArray":
343
- ...
338
+ The at property provides a functionally pure equivalent of in-place array modifications.
339
+
340
+ In particular:
341
+
342
+ | Alternate syntax | Equivalent In-place expression |
343
+ |------------------|-------------------------------|
344
+ | `x = x.at[idx].set(y)` | `x[idx] = y` |
345
+ | `x[idx] = y` | `x = x.at[idx].set(y)` |
346
+ | `x = x.at[idx].add(y)` | `x[idx] += y`|
347
+ | `x = x.at[idx].multiply(y)` | `x[idx] *= y`|
348
+ | `x = x.at[idx].divide(y)` | `x[idx] /= y`|
349
+ | `x = x.at[idx].power(y)` | `x[idx] **= y`|
350
+ | `x = x.at[idx].min(y)` | `x[idx] = minimum(x[idx], y)`|
351
+ | `x = x.at[idx].max(y)` | `x[idx] = maximum(x[idx], y)`|
352
+ | `x = x.at[idx].apply(ufunc)` | `ufunc.at(x, idx)`|
353
+
354
+ x = x.at[idx].get()
355
+
356
+
357
+ x = x[idx]
358
+
359
+ None of the x.at expressions modify the original x; instead they return a modified copy of x. However, inside a jit() compiled function, expressions like x = x.at[idx].set(y) are guaranteed to be applied in-place.
360
+
361
+ Unlike NumPy in-place operations such as x[idx] += y, if multiple indices refer to the same location, all updates will be applied (NumPy would only apply the last update, rather than applying all updates.) The order in which conflicting updates are applied is implementation-defined and may be nondeterministic (e.g., due to concurrency on some hardware platforms).
362
+
363
+ By default, JAX assumes that all indices are in-bounds. Alternative out-of-bound index semantics can be specified via the mode parameter (see below).
364
+
365
+
366
+ Returns:
344
367
 
345
- def __getitem__(self, idx) -> "NamedArray":
368
+ """
369
+ return _NamedIndexUpdateHelper(self)
370
+
371
+ def __getitem__(self, idx: SliceSpec) -> "NamedArray":
346
372
  """Syntactic sugar for [haliax.index][], which is the actual implementation.
347
373
 
348
374
  Supports indexing like:
@@ -366,17 +392,8 @@ class NamedArray:
366
392
 
367
393
  This returns a NamedArray if any axes remain, or a scalar (0-dimensional) jnp.ndarray if all axes are indexed out.
368
394
  """
369
- if isinstance(idx, tuple):
370
- if len(idx) == 1:
371
- idx = idx[0]
372
- else:
373
- if len(idx) % 2 != 0:
374
- raise ValueError(
375
- "Must provide an even number of arguments to __getitem__ when using the shorthand syntax."
376
- )
377
- idx = {idx[i]: idx[i + 1] for i in range(0, len(idx), 2)}
378
-
379
- return index(self, idx)
395
+ idx_dict = _convert_index_expr_to_dict(idx)
396
+ return index(self, idx_dict)
380
397
 
381
398
  # np.ndarray methods:
382
399
  def all(self, axis: Optional[AxisSelection] = None, *, where: Optional["NamedArray"] = None) -> "NamedArray":
@@ -886,28 +903,60 @@ def index(array: NamedArray, slices: Mapping[AxisSelector, NamedIndex]) -> Named
886
903
  you might use `array[{"batch": slice(0, 10)}]` or `array["batch", 0:10]` to select the first 10 elements
887
904
  of the 'batch' axis.
888
905
 
906
+ See Also:
907
+ * [haliax.NamedArray.at][] for a functional equivalent of in-place array modifications.
908
+
889
909
  Returns:
890
910
  NamedArray or jnp.ndarray: A NamedArray is returned if there are any axes remaining after selection,
891
911
  otherwise a scalar (0-dimensional) jnp.ndarray is returned if all axes are indexed out.
892
912
  """
893
913
  # indices where we have array args
894
- array_slice_indices = []
895
- dslice_indices = []
914
+ new_axes, ordered_slices = _compute_new_axes_and_slices_for_index(array, slices)
915
+ sliced, ordered_slices = _handle_dynamic_slices(array.array, ordered_slices)
916
+ sliced = sliced[tuple(ordered_slices)]
917
+
918
+ return haliax.named(sliced, new_axes)
919
+
920
+
921
+ def _compute_new_axes_and_slices_for_index(
922
+ array, slices
923
+ ) -> tuple[AxisSpec, list[py_slice | dslice | jnp.ndarray | int | list[int]]]:
896
924
  ordered_slices: list = [py_slice(None, None, None)] * len(array.axes) # type: ignore
897
925
  kept_axes = [True] * len(array.axes)
926
+ array_slice_indices = []
927
+
898
928
  for axis, slice_ in slices.items():
899
929
  axis_index = array._lookup_indices(axis)
900
930
  if axis_index is None:
901
931
  raise ValueError(f"axis {axis} not found in {array}")
902
- ordered_slices[axis_index] = slice_
903
-
904
- kept_axes[axis_index] = isinstance(slice_, py_slice) or isinstance(slice_, dslice) or is_pallas_dslice(slice_)
905
-
906
- if isinstance(slice_, NamedArray):
932
+ if isinstance(slice_, py_slice) or isinstance(slice_, dslice) or is_pallas_dslice(slice_):
933
+ ordered_slices[axis_index] = slice_
934
+ kept_axes[axis_index] = True
935
+ elif isinstance(slice_, int):
936
+ ordered_slices[axis_index] = slice_
937
+ kept_axes[axis_index] = False
938
+ elif isinstance(slice_, NamedArray):
939
+ ordered_slices[axis_index] = slice_
907
940
  array_slice_indices.append(axis_index)
908
-
909
- if isinstance(slice_, dslice) or is_pallas_dslice(slice_):
910
- dslice_indices.append(axis_index)
941
+ kept_axes[axis_index] = False
942
+ elif isinstance(slice_, list):
943
+ # we'll let JAX complain if this is wrong
944
+ ordered_slices[axis_index] = slice_
945
+ elif isinstance(slice_, jnp.ndarray):
946
+ # we allow this if it's a 0-d or 1-d array
947
+ if slice_.ndim == 0:
948
+ ordered_slices[axis_index] = slice_
949
+ elif slice_.ndim == 1:
950
+ # we allow this if it's a 1-d array, in which case we treat it as sugar for NamedArray(slice_, sliced-axis)
951
+ ordered_slices[axis_index] = haliax.named(slice_, axis_name(axis))
952
+ kept_axes[axis_index] = False
953
+ array_slice_indices.append(axis_index)
954
+ else:
955
+ raise ValueError(
956
+ f"Only 0-d or 1-d unnamed arrays can be used for indexing. Got {slice_} for axis {axis}"
957
+ )
958
+ else:
959
+ raise ValueError(f"Only NamedArrays can be used for advanced indexing. Got {slice_} for axis {axis}")
911
960
 
912
961
  # advanced indexing
913
962
  if len(array_slice_indices) > 0:
@@ -958,23 +1007,39 @@ def index(array: NamedArray, slices: Mapping[AxisSelector, NamedIndex]) -> Named
958
1007
  # the advanced indices are not contiguous, so we need to insert the new axes at the front
959
1008
  new_axes = broadcasted_axes + tuple(ax for i, ax in enumerate(array.axes) if kept_axes[i])
960
1009
  else:
961
- new_axes = tuple(axis.name for axis, keep in zip(array.axes, kept_axes) if keep)
1010
+ new_axes = tuple(axis for axis, keep in zip(array.axes, kept_axes) if keep)
962
1011
 
963
- sliced = array.array
1012
+ new_axes = tuple(axis_name(ax) for ax in new_axes)
1013
+ return new_axes, ordered_slices
964
1014
 
965
- if len(dslice_indices) > 0:
966
- # dynamic slice out the dslices
967
- indices = [0] * len(array.axes)
968
- lengths = [ax.size for ax in array.axes]
969
- for i in dslice_indices:
970
- indices[i] = ordered_slices[i].start
971
- lengths[i] = ordered_slices[i].size
972
- sliced = jax.lax.dynamic_slice(sliced, indices, lengths)
973
- for i in dslice_indices:
974
- ordered_slices[i] = py_slice(None, None, None)
975
1015
 
976
- sliced = sliced[tuple(ordered_slices)]
977
- return haliax.named(sliced, new_axes)
1016
+ def _handle_dynamic_slices(array: jnp.ndarray, slices):
1017
+ """
1018
+ Helper function to handle dynamic slices in the array. These have to be handled with jax.lax.dynamic_slice,
1019
+ which is for when the start index is not known at compile time. (Sizes must always be known at compile time.)
1020
+
1021
+ Notes:
1022
+ **MUTATES `slices` IN PLACE**
1023
+
1024
+ Returns:
1025
+ array.array: the sliced array
1026
+
1027
+ """
1028
+ indices_for_dslice = [0] * array.ndim
1029
+ lengths_for_dslice = list(array.shape)
1030
+ dslice_indices = []
1031
+ need_to_slice = False
1032
+ for axis_index, slice_ in enumerate(slices):
1033
+ if isinstance(slice_, dslice) or is_pallas_dslice(slice_):
1034
+ dslice_indices.append(axis_index)
1035
+ indices_for_dslice[axis_index] = slice_.start
1036
+ lengths_for_dslice[axis_index] = slice_.size
1037
+ need_to_slice = True
1038
+ if need_to_slice:
1039
+ array = jax.lax.dynamic_slice(array, indices_for_dslice, lengths_for_dslice)
1040
+ for i in dslice_indices:
1041
+ slices[i] = py_slice(None, None, None)
1042
+ return array, slices
978
1043
 
979
1044
 
980
1045
  def split(a: NamedArray, axis: AxisSelector, new_axes: Sequence[Axis]) -> Sequence[NamedArray]:
@@ -1445,6 +1510,208 @@ def flatten_all_axes_but(
1445
1510
  return result, unflatten
1446
1511
 
1447
1512
 
1513
+ class _NamedIndexUpdateHelper:
1514
+ def __init__(self, array: NamedArray):
1515
+ self.array = array
1516
+
1517
+ def __getitem__(self, slices: SliceSpec) -> "_NamedIndexUpdateRef":
1518
+ return _NamedIndexUpdateRef(self.array, _convert_index_expr_to_dict(slices))
1519
+
1520
+
1521
+ class _NamedIndexUpdateRef:
1522
+ def __init__(self, array: NamedArray, slices: SliceSpec):
1523
+ self._array = array
1524
+ self._slices = slices
1525
+
1526
+ def get(
1527
+ self,
1528
+ *,
1529
+ indices_are_sorted: bool = False,
1530
+ unique_indices: bool = False,
1531
+ mode: Optional[GatherScatterModeStr] = None,
1532
+ fill_value: Optional[Scalar] = None,
1533
+ ) -> NamedArray:
1534
+ slices, sliced_axes = _raw_indices_for_at(self._array, self._slices)
1535
+ new_array = self._array.array.at[tuple(slices)].get(
1536
+ indices_are_sorted=indices_are_sorted, unique_indices=unique_indices, mode=mode, fill_value=fill_value
1537
+ )
1538
+ return NamedArray(new_array, sliced_axes)
1539
+
1540
+ def set(
1541
+ self,
1542
+ update: NamedOrNumeric,
1543
+ *,
1544
+ indices_are_sorted: bool = False,
1545
+ unique_indices: bool = False,
1546
+ mode: Optional[GatherScatterModeStr] = None,
1547
+ ) -> NamedArray:
1548
+ slices, sliced_axes = _raw_indices_for_at(self._array, self._slices)
1549
+ update = haliax.broadcast_to(update, sliced_axes, enforce_no_extra_axes=True)
1550
+ new_array = self._array.array.at[tuple(slices)].set(
1551
+ update.array, indices_are_sorted=indices_are_sorted, unique_indices=unique_indices, mode=mode
1552
+ )
1553
+ return NamedArray(new_array, self._array.axes)
1554
+
1555
+ def add(
1556
+ self,
1557
+ update: NamedOrNumeric,
1558
+ *,
1559
+ indices_are_sorted: bool = False,
1560
+ unique_indices: bool = False,
1561
+ mode: Optional[GatherScatterModeStr] = None,
1562
+ ) -> NamedArray:
1563
+ slices, sliced_axes = _raw_indices_for_at(self._array, self._slices)
1564
+ update = haliax.broadcast_to(update, sliced_axes, enforce_no_extra_axes=True)
1565
+ new_array = self._array.array.at[tuple(slices)].add(
1566
+ update.array, indices_are_sorted=indices_are_sorted, unique_indices=unique_indices, mode=mode
1567
+ )
1568
+ return NamedArray(new_array, self._array.axes)
1569
+
1570
+ def multiply(
1571
+ self,
1572
+ update: NamedOrNumeric,
1573
+ *,
1574
+ indices_are_sorted: bool = False,
1575
+ unique_indices: bool = False,
1576
+ mode: Optional[GatherScatterModeStr] = None,
1577
+ ) -> NamedArray:
1578
+ slices, sliced_axes = _raw_indices_for_at(self._array, self._slices)
1579
+ update = haliax.broadcast_to(update, sliced_axes, enforce_no_extra_axes=True)
1580
+ new_array = self._array.array.at[tuple(slices)].multiply(
1581
+ update.array, indices_are_sorted=indices_are_sorted, unique_indices=unique_indices, mode=mode
1582
+ )
1583
+ return NamedArray(new_array, self._array.axes)
1584
+
1585
+ def divide(
1586
+ self,
1587
+ update: NamedOrNumeric,
1588
+ *,
1589
+ indices_are_sorted: bool = False,
1590
+ unique_indices: bool = False,
1591
+ mode: Optional[GatherScatterModeStr] = None,
1592
+ ) -> NamedArray:
1593
+ slices, sliced_axes = _raw_indices_for_at(self._array, self._slices)
1594
+ update = haliax.broadcast_to(update, sliced_axes, enforce_no_extra_axes=True)
1595
+ new_array = self._array.array.at[tuple(slices)].divide(
1596
+ update.array, indices_are_sorted=indices_are_sorted, unique_indices=unique_indices, mode=mode
1597
+ )
1598
+ return NamedArray(new_array, self._array.axes)
1599
+
1600
+ def max(
1601
+ self,
1602
+ update: NamedOrNumeric,
1603
+ *,
1604
+ indices_are_sorted: bool = False,
1605
+ unique_indices: bool = False,
1606
+ mode: Optional[GatherScatterModeStr] = None,
1607
+ ) -> NamedArray:
1608
+ slices, sliced_axes = _raw_indices_for_at(self._array, self._slices)
1609
+ update = haliax.broadcast_to(update, sliced_axes, enforce_no_extra_axes=True)
1610
+ new_array = self._array.array.at[tuple(slices)].max(
1611
+ update.array, indices_are_sorted=indices_are_sorted, unique_indices=unique_indices, mode=mode
1612
+ )
1613
+ return NamedArray(new_array, self._array.axes)
1614
+
1615
+ def min(
1616
+ self,
1617
+ update: NamedOrNumeric,
1618
+ *,
1619
+ indices_are_sorted: bool = False,
1620
+ unique_indices: bool = False,
1621
+ mode: Optional[GatherScatterModeStr] = None,
1622
+ ) -> NamedArray:
1623
+ slices, sliced_axes = _raw_indices_for_at(self._array, self._slices)
1624
+ update = haliax.broadcast_to(update, sliced_axes, enforce_no_extra_axes=True)
1625
+ new_array = self._array.array.at[tuple(slices)].min(
1626
+ update.array, indices_are_sorted=indices_are_sorted, unique_indices=unique_indices, mode=mode
1627
+ )
1628
+ return NamedArray(new_array, self._array.axes)
1629
+
1630
+ def power(
1631
+ self,
1632
+ update: NamedOrNumeric,
1633
+ *,
1634
+ indices_are_sorted: bool = False,
1635
+ unique_indices: bool = False,
1636
+ mode: Optional[GatherScatterModeStr] = None,
1637
+ ) -> NamedArray:
1638
+ slices, sliced_axes = _raw_indices_for_at(self._array, self._slices)
1639
+ update = haliax.broadcast_to(update, sliced_axes, enforce_no_extra_axes=True)
1640
+ new_array = self._array.array.at[tuple(slices)].power(
1641
+ update.array, indices_are_sorted=indices_are_sorted, unique_indices=unique_indices, mode=mode
1642
+ )
1643
+ return NamedArray(new_array, self._array.axes)
1644
+
1645
+ def apply(
1646
+ self,
1647
+ func,
1648
+ *,
1649
+ indices_are_sorted: bool = False,
1650
+ unique_indices: bool = False,
1651
+ mode: Optional[GatherScatterModeStr] = None,
1652
+ ) -> NamedArray:
1653
+ # It's not really documented, but func can be any callable that takes a scalar array and returns a scalar array
1654
+ slices, sliced_axes = _raw_indices_for_at(self._array, self._slices)
1655
+ new_array = self._array.array.at[tuple(slices)].apply(
1656
+ func, indices_are_sorted=indices_are_sorted, unique_indices=unique_indices, mode=mode
1657
+ )
1658
+ return NamedArray(new_array, self._array.axes)
1659
+
1660
+
1661
+ def _raw_indices_for_at(array, indexes):
1662
+ sliced_axes, ordered_slices = _compute_new_axes_and_slices_for_index(array, indexes)
1663
+ del sliced_axes
1664
+ # this isn't the fastest (it does the _compute_new_axes_and_slices_for_index twice)
1665
+ # but it's easy
1666
+ _sliced = index(array, indexes)
1667
+ # we have to handle dslices differently than for normal indexing, because we can't use
1668
+ # extra dynamic_slices...
1669
+ # we'd like to just replace these with iota, but we have account for broadcasting semantics
1670
+ # for the other arrays
1671
+ dslice_sizes = tuple(x.size for x in ordered_slices if isinstance(x, dslice) or is_pallas_dslice(x)) # type: ignore
1672
+ current_array_slice_shape = next((x.shape for x in ordered_slices if is_jax_array_like(x)), None) # type: ignore
1673
+ dims_to_expand = list(range(len(dslice_sizes)))
1674
+ if current_array_slice_shape is not None:
1675
+ iota_shape = dslice_sizes + current_array_slice_shape
1676
+ else:
1677
+ iota_shape = dslice_sizes
1678
+
1679
+ def iota_for_dslice(dslice, cur_dynamic_slice):
1680
+ return jax.lax.broadcasted_iota(int, iota_shape, cur_dynamic_slice) + dslice.start
1681
+
1682
+ if len(dslice_sizes) > 0:
1683
+ cur_dynamic_slice = 0
1684
+ for i in range(len(ordered_slices)):
1685
+ if isinstance(ordered_slices[i], dslice) or is_pallas_dslice(ordered_slices[i]):
1686
+ ordered_slices[i] = iota_for_dslice(ordered_slices[i], cur_dynamic_slice)
1687
+ cur_dynamic_slice += 1
1688
+ elif is_jax_array_like(ordered_slices[i]):
1689
+ # prepend array slices with one 1 for each dynamic slice
1690
+ ordered_slices[i] = jnp.expand_dims(ordered_slices[i], axis=dims_to_expand)
1691
+
1692
+ assert cur_dynamic_slice == len(dslice_sizes)
1693
+
1694
+ # ok the ordered slices are now correct
1695
+ return ordered_slices, _sliced.axes
1696
+
1697
+
1698
+ def _convert_index_expr_to_dict(idx) -> dict[AxisSelector, NamedIndex]:
1699
+ if isinstance(idx, tuple):
1700
+ if len(idx) == 1:
1701
+ idx = idx[0]
1702
+ else:
1703
+ if len(idx) % 2 != 0:
1704
+ raise ValueError(
1705
+ "Must provide an even number of arguments to __getitem__ when using the shorthand syntax."
1706
+ )
1707
+ idx = {idx[i]: idx[i + 1] for i in range(0, len(idx), 2)}
1708
+ elif isinstance(idx, dict):
1709
+ pass
1710
+ else:
1711
+ raise ValueError(f"Invalid index type {type(idx)}")
1712
+ return idx
1713
+
1714
+
1448
1715
  __all__ = [
1449
1716
  "NamedArray",
1450
1717
  "named",
@@ -1,4 +1,4 @@
1
- from typing import Any, Protocol, Tuple, TypeAlias, Union
1
+ from typing import Any, Literal, Protocol, Tuple, TypeAlias, Union
2
2
 
3
3
  import jax.numpy as jnp
4
4
  import numpy as np
@@ -28,3 +28,5 @@ Scalar = Union[float, int, jnp.ndarray] # ndarray b/c array(1) is a scalar
28
28
  IntScalar = Union[int, jnp.ndarray]
29
29
 
30
30
  PrecisionLike = Union[None, str, Precision, Tuple[str, str], Tuple[Precision, Precision]]
31
+
32
+ GatherScatterModeStr = Literal["promise_in_bounds", "clip", "drop", "fill"]
@@ -649,6 +649,34 @@ def test_slice_nd_array_present_dims():
649
649
  assert jnp.all(jnp.equal(named1[{"H": index2}].array, named1.array[index2.array, :, :]))
650
650
 
651
651
 
652
+ def test_slice_nd_array_unnamed_slice():
653
+ # tests slicing with arrays that are already present in the named array, which is sometimes ok
654
+ H = Axis("H", 10)
655
+ W = Axis("W", 20)
656
+ D = Axis("D", 30)
657
+
658
+ named1 = hax.random.uniform(PRNGKey(0), (H, W, D))
659
+
660
+ index1 = jax.random.randint(PRNGKey(1), (4,), 0, H.size)
661
+ assert jnp.all(jnp.equal(named1[{"H": index1}].array, named1.array[index1, :, :]))
662
+
663
+ # hidden behavior: if we also pass in an H index to e.g. D, it is zipped together
664
+ index2 = hax.random.randint(PRNGKey(2), Axis("H", 4), 0, D.size)
665
+ assert jnp.all(jnp.equal(named1[{"H": index1, "D": index2}].array, named1.array[index1, :, index2.array]))
666
+
667
+ # this is different though:
668
+ index2r = index2.array
669
+ assert jnp.all(
670
+ jnp.equal(
671
+ named1[{"H": index1, "D": index2r}].array, named1.array[index1.reshape(1, -1), :, index2r.reshape(-1, 1)]
672
+ )
673
+ )
674
+ assert named1[{"H": index1, "D": index2r}].shape != named1[{"H": index1, "D": index2}].shape
675
+
676
+ index1 = list(index1)
677
+ assert jnp.all(jnp.equal(named1[{"H": index1}].array, named1.array[index1, :, :]))
678
+
679
+
652
680
  def test_full_indexing_returns_named_array():
653
681
  H = Axis("H", 10)
654
682
  W = Axis("W", 20)
@@ -847,3 +875,82 @@ def test_named_arrays_work_in_eqxi_while_loop():
847
875
  grad_fun = eqx.filter_value_and_grad(loss_fun)
848
876
 
849
877
  grad_fun(named1)
878
+
879
+
880
+ def test_at_for_in_placeish():
881
+ H = Axis("H", 10)
882
+ W = Axis("W", 20)
883
+
884
+ named1 = hax.random.uniform(PRNGKey(0), (H, W))
885
+
886
+ named1_at = named1.at[H, 0].set(0)
887
+
888
+ assert jnp.all(jnp.equal(named1_at[H, 0].array, 0))
889
+ assert jnp.all(named1_at[H, 1:].array == named1[H, 1:].array)
890
+
891
+ # test add, multiply, power, etc.
892
+ named1_at = named1.at[H, 0].add(1)
893
+ assert jnp.all(named1_at.array == named1.array.at[0].add(1))
894
+
895
+ named1_at = named1.at[H, 0].multiply(2)
896
+ assert jnp.all(named1_at.array == named1.array.at[0].multiply(2))
897
+
898
+ named1_at = named1.at[H, 0].power(2)
899
+ assert jnp.all(named1_at.array == named1.array.at[0].power(2))
900
+
901
+ named1_at = named1.at[H, 0].divide(2)
902
+ assert jnp.all(named1_at.array == named1.array.at[0].divide(2))
903
+
904
+ named1_at = named1.at[H, 0].apply(hax.square)
905
+ assert jnp.all(named1_at.array == named1.array.at[0].apply(jnp.square))
906
+
907
+ named1_at = named1.at[H, 0].max(0.5)
908
+ assert jnp.all(named1_at.array == named1.array.at[0].max(0.5))
909
+
910
+ named1_at = named1.at[H, 0].min(0.5)
911
+ assert jnp.all(named1_at.array == named1.array.at[0].min(0.5))
912
+
913
+
914
+ def test_at_with_fancy_indexing():
915
+ H = Axis("H", 10)
916
+ W = Axis("W", 20)
917
+ I0 = Axis("I0", 5)
918
+ I1 = Axis("I1", 5)
919
+
920
+ named1 = hax.random.uniform(PRNGKey(0), (H, W))
921
+ ind1 = hax.random.randint(PRNGKey(0), (I0,), 0, H.size)
922
+ ind2 = hax.random.randint(PRNGKey(0), (I1,), 0, W.size)
923
+
924
+ named1_at = named1.at[H, ind1].set(0)
925
+ assert jnp.all(named1_at.array == named1.array.at[ind1.array].set(0))
926
+
927
+ named1_at = named1.at[H, ind1].add(1, mode="clip")
928
+ assert jnp.all(named1_at.array == named1.array.at[ind1.array].add(1, mode="clip"))
929
+
930
+ named1_at = named1.at[H, ind1, W, ind2].set(0)
931
+ assert jnp.all(named1_at.array == named1.array.at[ind1.array.reshape(-1, 1), ind2.array.reshape(1, -1)].set(0))
932
+
933
+ # dslices
934
+ from haliax import ds
935
+
936
+ named1_at = named1.at[H, ds(3, 5)].set(0)
937
+ assert jnp.all(named1_at.array == named1.array.at[3:8].set(0))
938
+
939
+ named1_at = named1.at[H, ds(3, 5), W, ind2].power(2)
940
+ assert jnp.all(named1_at.array == named1.array.at[3:8, ind2.array].power(2))
941
+
942
+
943
+ def test_slice_dslice_and_array():
944
+ H = Axis("H", 10)
945
+ W = Axis("W", 20)
946
+ I0 = Axis("I0", 5)
947
+
948
+ named1 = hax.random.uniform(PRNGKey(0), (H, W))
949
+ ind2 = hax.random.randint(PRNGKey(0), (I0,), 0, W.size)
950
+
951
+ from haliax import ds
952
+
953
+ named1.array.at[3:8, ind2.array].add(jnp.full((5, 5), 2))
954
+
955
+ named1_at = named1.at[H, ds(3, 5), W, ind2].add(2)
956
+ assert jnp.all(named1_at.array == named1.array.at[3:8, ind2.array].add(2))
@@ -1 +0,0 @@
1
- __version__ = "1.4.dev289"
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