haliax 1.4.dev375__tar.gz → 1.4.dev379__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 (114) hide show
  1. {haliax-1.4.dev375 → haliax-1.4.dev379}/.github/workflows/run_quick_levanter_tests.yaml +3 -2
  2. {haliax-1.4.dev375 → haliax-1.4.dev379}/PKG-INFO +1 -1
  3. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/scan.md +3 -1
  4. haliax-1.4.dev379/src/haliax/__about__.py +1 -0
  5. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/core.py +19 -9
  6. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/nn/attention.py +13 -2
  7. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/nn/dropout.py +2 -2
  8. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/nn/embedding.py +2 -2
  9. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/nn/linear.py +5 -5
  10. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/nn/normalization.py +2 -2
  11. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/nn/scan.py +173 -47
  12. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/ops.py +2 -0
  13. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/wrap.py +26 -0
  14. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/core_test.py +4 -4
  15. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_dot.py +1 -1
  16. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_hof.py +1 -1
  17. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_ops.py +4 -4
  18. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_scan.py +398 -0
  19. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_tree_util.py +1 -1
  20. haliax-1.4.dev375/src/haliax/__about__.py +0 -1
  21. {haliax-1.4.dev375 → haliax-1.4.dev379}/.coveragerc +0 -0
  22. {haliax-1.4.dev375 → haliax-1.4.dev379}/.flake8 +0 -0
  23. {haliax-1.4.dev375 → haliax-1.4.dev379}/.github/workflows/publish_dev.yaml +0 -0
  24. {haliax-1.4.dev375 → haliax-1.4.dev379}/.github/workflows/run_pre_commit.yaml +0 -0
  25. {haliax-1.4.dev375 → haliax-1.4.dev379}/.github/workflows/run_tests.yaml +0 -0
  26. {haliax-1.4.dev375 → haliax-1.4.dev379}/.gitignore +0 -0
  27. {haliax-1.4.dev375 → haliax-1.4.dev379}/.playbooks/add-types.md +0 -0
  28. {haliax-1.4.dev375 → haliax-1.4.dev379}/.pre-commit-config.yaml +0 -0
  29. {haliax-1.4.dev375 → haliax-1.4.dev379}/.readthedocs.yaml +0 -0
  30. {haliax-1.4.dev375 → haliax-1.4.dev379}/AGENTS.md +0 -0
  31. {haliax-1.4.dev375 → haliax-1.4.dev379}/CONTRIBUTING.md +0 -0
  32. {haliax-1.4.dev375 → haliax-1.4.dev379}/LICENSE +0 -0
  33. {haliax-1.4.dev375 → haliax-1.4.dev379}/README.md +0 -0
  34. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/api.md +0 -0
  35. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/broadcasting.md +0 -0
  36. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/cheatsheet.md +0 -0
  37. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/css/material.css +0 -0
  38. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/css/mkdocstrings.css +0 -0
  39. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/faq.md +0 -0
  40. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/figures/data_parallel_mesh.png +0 -0
  41. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/figures/data_parallel_mesh_replicated.png +0 -0
  42. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/figures/device_mesh_1d.png +0 -0
  43. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/figures/device_mesh_1d_zero.png +0 -0
  44. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/figures/device_mesh_2d.png +0 -0
  45. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/figures/device_mesh_2d_batch_partitioned.png +0 -0
  46. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/figures/device_mesh_2d_data_replicated.png +0 -0
  47. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/figures/device_mesh_2d_data_replicated_mlp_partitioned.png +0 -0
  48. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/figures/device_mesh_2d_intermediate_fully_partitioned.png +0 -0
  49. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/figures/device_mesh_2d_zero.png +0 -0
  50. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/fp8.md +0 -0
  51. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/index.md +0 -0
  52. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/indexing.md +0 -0
  53. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/matmul.md +0 -0
  54. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/nn.md +0 -0
  55. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/partitioning.md +0 -0
  56. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/rearrange.ipynb +0 -0
  57. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/rearrange.md +0 -0
  58. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/requirements.txt +0 -0
  59. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/state-dict.md +0 -0
  60. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/tutorial.md +0 -0
  61. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/typing.md +0 -0
  62. {haliax-1.4.dev375 → haliax-1.4.dev379}/docs/vmap.md +0 -0
  63. {haliax-1.4.dev375 → haliax-1.4.dev379}/mkdocs.yml +0 -0
  64. {haliax-1.4.dev375 → haliax-1.4.dev379}/pyproject.toml +0 -0
  65. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/__init__.py +0 -0
  66. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/_src/__init__.py +0 -0
  67. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/_src/compile_utils.py +0 -0
  68. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/_src/dot.py +0 -0
  69. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/_src/einsum.py +0 -0
  70. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/_src/fp8.py +0 -0
  71. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/_src/parsing.py +0 -0
  72. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/_src/rearrange.py +0 -0
  73. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/_src/scan.py +0 -0
  74. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/_src/state_dict.py +0 -0
  75. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/_src/util.py +0 -0
  76. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/axis.py +0 -0
  77. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/debug.py +0 -0
  78. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/haxtyping.py +0 -0
  79. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/hof.py +0 -0
  80. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/jax_utils.py +0 -0
  81. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/nn/__init__.py +0 -0
  82. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/nn/activations.py +0 -0
  83. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/nn/conv.py +0 -0
  84. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/nn/loss.py +0 -0
  85. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/nn/mlp.py +0 -0
  86. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/nn/pool.py +0 -0
  87. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/partitioning.py +0 -0
  88. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/quantization.py +0 -0
  89. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/random.py +0 -0
  90. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/specialized_fns.py +0 -0
  91. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/state_dict.py +0 -0
  92. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/tree_util.py +0 -0
  93. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/types.py +0 -0
  94. {haliax-1.4.dev375 → haliax-1.4.dev379}/src/haliax/util.py +0 -0
  95. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_attention.py +0 -0
  96. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_axis.py +0 -0
  97. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_conv.py +0 -0
  98. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_debug.py +0 -0
  99. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_dtype_typing.py +0 -0
  100. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_einsum.py +0 -0
  101. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_fp8.py +0 -0
  102. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_int8.py +0 -0
  103. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_namedarray_typing.py +0 -0
  104. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_nn.py +0 -0
  105. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_parsing.py +0 -0
  106. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_partitioning.py +0 -0
  107. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_pool.py +0 -0
  108. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_random.py +0 -0
  109. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_rearrange.py +0 -0
  110. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_scatter_gather.py +0 -0
  111. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_specialized_fns.py +0 -0
  112. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_state_dict.py +0 -0
  113. {haliax-1.4.dev375 → haliax-1.4.dev379}/tests/test_utils.py +0 -0
  114. {haliax-1.4.dev375 → haliax-1.4.dev379}/uv.lock +0 -0
@@ -9,7 +9,7 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  strategy:
11
11
  matrix:
12
- python-version: [3.10, 3.11]
12
+ python-version: ["3.10", "3.11"]
13
13
  steps:
14
14
  - name: Checkout repository
15
15
  uses: actions/checkout@v3
@@ -39,10 +39,11 @@ jobs:
39
39
  run: |
40
40
  uv pip install -e .[tests]
41
41
  uv pip install -e ../haliax
42
+ uv pip install pytest
42
43
 
43
44
  - name: Run pytest suite
44
45
  env:
45
46
  XLA_FLAGS: --xla_force_host_platform_device_count=8
46
47
  VIRTUAL_ENV: ${{ github.workspace }}/.venv
47
48
  run: |
48
- pytest tests -m "not entry and not slow"
49
+ uv run pytest tests -m "not entry and not slow"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haliax
3
- Version: 1.4.dev375
3
+ Version: 1.4.dev379
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/
@@ -458,7 +458,9 @@ We also provide a way to create a sequence of layers that can be applied to a se
458
458
  same interface as [haliax.nn.Stacked][], but with a different implementation. This is the [haliax.nn.BlockSeq][] module.
459
459
  BlockSeq implements those for loops directly, rather than using [haliax.fold][] or [haliax.scan][].
460
460
 
461
- [haliax.nn.scan.BlockFoldable][] is an interface that both [haliax.nn.Stacked][] and [haliax.nn.BlockSeq][] implement.
461
+ [haliax.nn.scan.BlockFoldable][] is an interface that both [haliax.nn.Stacked][] and [haliax.nn.BlockSeq][] implement. It
462
+ exposes the usual ``fold`` and ``scan`` methods as well as helpers ``fold_via`` and ``scan_via`` which return
463
+ callables that perform the respective operations using a custom block function.
462
464
 
463
465
  ## API
464
466
 
@@ -0,0 +1 @@
1
+ __version__ = "1.4.dev379"
@@ -241,15 +241,15 @@ class NamedArray(metaclass=NamedArrayMeta):
241
241
  raise ValueError(f"Expected scalar, got {self.array.ndim}-dimensional array")
242
242
  return self.array
243
243
 
244
- def __jax_array__(self):
245
- if self.ndim == 0:
246
- return self.array
247
- else:
248
- raise ValueError(
249
- "Only scalar NamedArrays can be implicitly converted to jax arrays, but "
250
- f"got {self.shape} array. This error typically occurs when you pass a "
251
- "NamedArray to a plain jax.numpy function. Please use `x.array` instead."
252
- )
244
+ # def __jax_array__(self):
245
+ # if self.ndim == 0:
246
+ # return self.array
247
+ # else:
248
+ # raise ValueError(
249
+ # "Only scalar NamedArrays can be implicitly converted to jax arrays, but "
250
+ # f"got {self.shape} array. This error typically occurs when you pass a "
251
+ # "NamedArray to a plain jax.numpy function. Please use `x.array` instead."
252
+ # )
253
253
 
254
254
  @ft.cached_property
255
255
  def shape(self) -> Dict[str, int]:
@@ -1787,12 +1787,22 @@ def broadcast_arrays_and_return_axes(
1787
1787
  """
1788
1788
  if len(arrays) == 0:
1789
1789
  return ((), ())
1790
+ elif len(arrays) == 1:
1791
+ a = arrays[0]
1792
+ if a is None:
1793
+ return (None,), ()
1794
+ if isinstance(a, NamedArray):
1795
+ return (a,), a.axes
1796
+ return (named(jnp.asarray(a), ()), ), ()
1790
1797
 
1791
1798
  # sort the arrays by size, so that we use the biggest ones to broadcast the others
1792
1799
  # need to hold on to the order so we can return the arrays in the same order
1793
1800
  actual_arrays = [x for x in arrays if isinstance(x, NamedArray)]
1794
1801
  size_order = sorted(range(len(actual_arrays)), key=lambda i: actual_arrays[i].size, reverse=True)
1795
1802
  all_axes = [actual_arrays[i].axes for i in size_order]
1803
+ if len(all_axes) == 0:
1804
+ return (arrays, ())
1805
+
1796
1806
  full_axes = ft.reduce(lambda a, b: _broadcast_axes(a, b, require_subset) if a is not None else None, all_axes) # type: ignore
1797
1807
  if full_axes is None:
1798
1808
  raise ValueError(f"Cannot broadcast arrays {arrays}: no subset relationship")
@@ -155,7 +155,7 @@ def combine_masks_or(mask1: Optional[NamedArray], mask2: Optional[NamedArray]) -
155
155
  return mask1 | mask2.broadcast_axis(mask1.axes)
156
156
 
157
157
 
158
- def causal_mask(QPos: Axis, KPos: Axis, q_start: int = 0, k_start: int = 0) -> NamedArray:
158
+ def causal_mask(QPos: Axis, KPos: Axis, q_start: int | NamedArray = 0, k_start: int | NamedArray= 0) -> NamedArray:
159
159
  """
160
160
  Creates a materialized causal mask for attention.
161
161
 
@@ -163,7 +163,18 @@ def causal_mask(QPos: Axis, KPos: Axis, q_start: int = 0, k_start: int = 0) -> N
163
163
  :param KPos: Axis of key sequence length
164
164
  :return: NamedArray of shape (QPos, KPos)
165
165
  """
166
- return haliax.arange(QPos, start=q_start) >= haliax.arange(KPos, start=k_start).broadcast_axis(QPos)
166
+ # if q_start is a named array, we vmap the arange
167
+ if isinstance(q_start, NamedArray):
168
+ q_range = haliax.vmap(haliax.arange, q_start.axes)(QPos, start=q_start)
169
+ else:
170
+ q_range = haliax.arange(QPos, start=q_start)
171
+
172
+ if isinstance(k_start, NamedArray):
173
+ k_range = haliax.vmap(haliax.arange, k_start.axes)(KPos, start=k_start)
174
+ else:
175
+ k_range = haliax.arange(KPos, start=k_start)
176
+
177
+ return q_range >= k_range.broadcast_axis(QPos)
167
178
 
168
179
 
169
180
  def prefix_lm_mask(QSeqLen: Axis, KSeqLen: Axis, prefix_len: int, q_start: int = 0, k_start: int = 0) -> NamedArray:
@@ -61,8 +61,8 @@ class Dropout(eqx.Module):
61
61
  """
62
62
 
63
63
  # key difference from equinox: these are static fields
64
- pdrop: float = eqx.static_field()
65
- broadcast_axes: Optional[AxisSpec] = eqx.static_field()
64
+ pdrop: float = eqx.field(static=True)
65
+ broadcast_axes: Optional[AxisSpec] = eqx.field(static=True)
66
66
  inference: bool = False # note: not static
67
67
 
68
68
  def __init__(
@@ -17,8 +17,8 @@ class Embedding(eqx.Module):
17
17
  weight: NamedArray
18
18
 
19
19
  # axes
20
- Vocab: Axis = eqx.static_field()
21
- Embed: AxisSpec = eqx.static_field()
20
+ Vocab: Axis = eqx.field(static=True)
21
+ Embed: AxisSpec = eqx.field(static=True)
22
22
 
23
23
  @staticmethod
24
24
  def init(Vocab: Axis, Embed: AxisSpec, *, init_scale: float = 1, key, initializer_range: Optional[float] = None):
@@ -28,8 +28,8 @@ class Linear(ModuleWithStateDictSerialization):
28
28
  weight: NamedArray
29
29
  bias: Optional[NamedArray]
30
30
 
31
- In: AxisSpec = eqx.static_field()
32
- Out: AxisSpec = eqx.static_field()
31
+ In: AxisSpec = eqx.field(static=True)
32
+ Out: AxisSpec = eqx.field(static=True)
33
33
  dot_general: DotGeneralOp = eqx.field(default_factory=DotGeneralOp.default)
34
34
 
35
35
  @staticmethod
@@ -140,9 +140,9 @@ class MoELinear(eqx.Module):
140
140
  weight: NamedArray
141
141
  bias: Optional[NamedArray]
142
142
 
143
- Experts: AxisSpec = eqx.static_field()
144
- In: Axis = eqx.static_field()
145
- Out: Axis = eqx.static_field()
143
+ Experts: AxisSpec = eqx.field(static=True)
144
+ In: Axis = eqx.field(static=True)
145
+ Out: Axis = eqx.field(static=True)
146
146
  # TODO: support quanitization for ragged_dot?
147
147
  # dot_general: DotGeneralOp = eqx.field(default_factory=DotGeneralOp.default)
148
148
 
@@ -20,10 +20,10 @@ A = TypeVar("A", Scalar, NamedArray, jnp.ndarray)
20
20
 
21
21
 
22
22
  class LayerNormBase(ModuleWithStateDictSerialization):
23
- axis: AxisSpec = eqx.static_field()
23
+ axis: AxisSpec = eqx.field(static=True)
24
24
  weight: Optional[NamedArray]
25
25
  bias: Optional[NamedArray]
26
- eps: float = eqx.static_field(default=1e-5)
26
+ eps: float = eqx.field(default=1e-5, static=True)
27
27
  dtype: Optional[jnp.dtype] = eqx.field(default=None, static=True)
28
28
 
29
29
  @abstractmethod
@@ -2,7 +2,8 @@ import dataclasses
2
2
  import functools
3
3
  import re
4
4
  import warnings
5
- from typing import Any, Callable, Dict, Generic, Optional, Protocol, Sequence, Type, TypeVar, cast, overload, ParamSpec
5
+ from typing import Any, Callable, Concatenate, Dict, Generic, Optional, Protocol, Sequence, Type, TypeVar, cast, \
6
+ overload, ParamSpec
6
7
 
7
8
  import equinox as eqx
8
9
  import jax
@@ -27,13 +28,18 @@ CarryT = TypeVar("CarryT")
27
28
  OutputT_co = TypeVar("OutputT_co", covariant=True)
28
29
  P = ParamSpec("P")
29
30
 
30
- class FoldFunction(Protocol[M_contra, CarryT]):
31
- def __call__(self, module: M_contra, carry: CarryT) -> CarryT:
31
+ class FoldFunction(Protocol[M_contra, P, CarryT]):
32
+ def __call__(self, module: M_contra, carry: CarryT, *args: P.args, **kwargs: P.kwargs) -> CarryT:
32
33
  ...
33
34
 
34
35
 
35
- class ScanFunction(Protocol[M_contra, CarryT, OutputT_co]):
36
- def __call__(self, module: M_contra, carry: CarryT) -> tuple[CarryT, OutputT_co]:
36
+ class ScanFunction(Protocol[M_contra, CarryT, P, OutputT_co]):
37
+ def __call__(self, module: M_contra, carry: CarryT, *args: P.args, **kwargs: P.kwargs) -> tuple[CarryT, OutputT_co]:
38
+ ...
39
+
40
+
41
+ class VmapFunction(Protocol[M_contra, P, OutputT_co]):
42
+ def __call__(self, module: M_contra, *args: P.args, **kwargs: P.kwargs) -> OutputT_co:
37
43
  ...
38
44
 
39
45
 
@@ -43,12 +49,13 @@ class ModuleInit(Protocol[M_co]):
43
49
 
44
50
 
45
51
  class BlockFoldable(Protocol[M]):
46
- """
47
- A superclass for [haliax.nn.Stacked][] and [haliax.nn.BlockSeq][] that exposes the fold and scan methods, as
48
- well as a few other methods that are useful for these modules.
52
+ """Common interface for :class:`~haliax.nn.Stacked` and :class:`~haliax.nn.BlockSeq`.
49
53
 
50
- This is a protocol, so you can use it as a type hint for a function that takes a Stacked or BlockSeq.
51
- Equinox modules can't directly inherit from Protocols, but you can use it as a type hint.
54
+ The interface exposes the :py:meth:`fold` and :py:meth:`scan` methods along with the helper
55
+ methods :py:meth:`fold_via`, :py:meth:`scan_via`, and :py:meth:`vmap_via`.
56
+
57
+ This is a protocol, so you can use it as a type hint for a function that takes a ``Stacked`` or ``BlockSeq``.
58
+ Equinox modules can't directly inherit from ``Protocol`` classes, but you can use it as a type hint.
52
59
  """
53
60
 
54
61
  Block: Axis
@@ -70,6 +77,39 @@ class BlockFoldable(Protocol[M]):
70
77
  def fold(self, init: T, *args, **kwargs) -> T:
71
78
  ...
72
79
 
80
+ @overload
81
+ def fold_via(self, fn: FoldFunction[M, P, CarryT]) -> Callable[Concatenate[CarryT, P], CarryT]:
82
+ ...
83
+
84
+ @overload
85
+ def fold_via(self, fn: Callable[[M, CarryT], CarryT]) -> Callable[[CarryT], CarryT]:
86
+ ...
87
+
88
+ def fold_via(self, fn: Callable[..., CarryT]) -> Callable[Concatenate[CarryT, P], CarryT]:
89
+ ...
90
+
91
+ @overload
92
+ def scan_via(self, fn: ScanFunction[M, CarryT, P, OutputT_co]) -> Callable[Concatenate[CarryT, P], tuple[CarryT, OutputT_co]]:
93
+ ...
94
+
95
+ @overload
96
+ def scan_via(self, fn: Callable[[M, CarryT], tuple[CarryT, OutputT_co]]) -> Callable[[CarryT], tuple[CarryT, OutputT_co]]:
97
+ ...
98
+
99
+ def scan_via(self, fn: Callable[..., tuple[CarryT, OutputT_co]]) -> Callable[P, tuple[CarryT, OutputT_co]]:
100
+ ...
101
+
102
+ @overload
103
+ def vmap_via(self, fn: VmapFunction[M, P, OutputT_co]) -> Callable[P, OutputT_co]:
104
+ ...
105
+
106
+ @overload
107
+ def vmap_via(self, fn: Callable[[M], OutputT_co]) -> Callable[[], OutputT_co]:
108
+ ...
109
+
110
+ def vmap_via(self, fn: Callable[..., OutputT_co]) -> Callable[..., OutputT_co]:
111
+ ...
112
+
73
113
  def unstacked(self) -> Sequence[M]:
74
114
  """
75
115
  Returns the unstacked version of this module. This is useful for logging or saving checkpoints.
@@ -90,8 +130,8 @@ class BlockSeq(ModuleWithStateDictSerialization, Generic[M]):
90
130
  """
91
131
 
92
132
  blocks: Sequence[M]
93
- Block: Axis = eqx.static_field()
94
- gradient_checkpointing: ScanCheckpointPolicy = eqx.static_field()
133
+ Block: Axis = eqx.field(static=True)
134
+ gradient_checkpointing: ScanCheckpointPolicy = eqx.field(static=True)
95
135
 
96
136
  @classmethod
97
137
  def init(
@@ -177,6 +217,87 @@ class BlockSeq(ModuleWithStateDictSerialization, Generic[M]):
177
217
 
178
218
  return do_fold(init, *args, **kwargs)
179
219
 
220
+ @overload
221
+ def fold_via(self, fn: FoldFunction[M, P, CarryT]) -> Callable[Concatenate[CarryT, P], CarryT]:
222
+ ...
223
+
224
+ @overload
225
+ def fold_via(self, fn: Callable[[M, CarryT], CarryT]) -> Callable[[CarryT], CarryT]:
226
+ ...
227
+
228
+ def fold_via(self, fn: Callable[..., CarryT]):
229
+ """Return a function that folds over the sequence using ``fn``.
230
+
231
+ ``fn`` should take a block and a carry and return a new carry. The
232
+ returned function mirrors :func:`haliax.fold` over the block axis.
233
+ """
234
+
235
+ def do_fold(init: CarryT, *args, **kwargs) -> CarryT:
236
+ carry = init
237
+ for block in self.blocks:
238
+ carry = fn(block, carry, *args, **kwargs)
239
+ carry = tree_checkpoint_name(carry, self._carry_ckpt_name)
240
+ return carry
241
+
242
+ return do_fold
243
+
244
+ @overload
245
+ def scan_via(self, fn: ScanFunction[M, CarryT, P, OutputT_co]) -> Callable[Concatenate[CarryT, P], tuple[CarryT, OutputT_co]]:
246
+ ...
247
+
248
+ @overload
249
+ def scan_via(self, fn: Callable[[M, CarryT], tuple[CarryT, OutputT_co]]) -> Callable[[CarryT], tuple[CarryT, OutputT_co]]:
250
+ ...
251
+
252
+ def scan_via(self, fn: Callable[..., tuple[CarryT, OutputT_co]]):
253
+ """Return a function that scans over the sequence using ``fn``.
254
+
255
+ ``fn`` should take a block and a carry and return ``(carry, output)``.
256
+ Semantics match :func:`haliax.scan` over the block axis.
257
+ """
258
+
259
+ def do_scan(init: CarryT, *args, **kwargs) -> tuple[CarryT, OutputT_co]:
260
+ out = []
261
+ carry = init
262
+ for block in self.blocks:
263
+ carry, extra = fn(block, carry, *args, **kwargs)
264
+ carry = tree_checkpoint_name(carry, self._carry_ckpt_name)
265
+ extra = tree_checkpoint_name(extra, self._output_ckpt_name)
266
+ out.append(extra)
267
+
268
+ stacked_out = haliax.tree_util.tree_map(lambda *x: haliax.stack(self.Block, x), *out)
269
+ return carry, stacked_out
270
+
271
+ return do_scan
272
+
273
+ @overload
274
+ def vmap_via(self, fn: VmapFunction[M, P, OutputT_co]) -> Callable[P, OutputT_co]:
275
+ ...
276
+
277
+ @overload
278
+ def vmap_via(self, fn: Callable[[M], OutputT_co]) -> Callable[[], OutputT_co]:
279
+ ...
280
+
281
+ def vmap_via(self, fn: Callable[..., OutputT_co]) -> Callable[..., OutputT_co]:
282
+ """Return a function that applies each block independently using ``fn``.
283
+
284
+ ``fn`` should take a block and a carry and return an output. The
285
+ returned function mirrors :func:`haliax.vmap` over the block axis.
286
+ """
287
+
288
+ def do_vmap(init: CarryT, *args, **kwargs) -> OutputT_co:
289
+ # Apply fn to each block independently
290
+ outputs = []
291
+ for block in self.blocks:
292
+ output = fn(block, init, *args, **kwargs)
293
+ outputs.append(output)
294
+
295
+ # Stack the outputs
296
+ stacked_out = haliax.tree_util.tree_map(lambda *x: haliax.stack(self.Block, x), *outputs)
297
+ return stacked_out
298
+
299
+ return do_vmap
300
+
180
301
  def unstacked(self) -> Sequence[M]:
181
302
  return self.blocks
182
303
 
@@ -279,8 +400,8 @@ class Stacked(ModuleWithStateDictSerialization, Generic[M]):
279
400
  # TODO: we can probably make this module support pipeline parallelism, but that's a whole project in itself
280
401
 
281
402
  stacked: M
282
- Block: Axis = eqx.static_field()
283
- gradient_checkpointing: ScanCheckpointPolicy = eqx.static_field()
403
+ Block: Axis = eqx.field(static=True)
404
+ gradient_checkpointing: ScanCheckpointPolicy = eqx.field(static=True)
284
405
 
285
406
  @classmethod
286
407
  def init(
@@ -398,7 +519,7 @@ class Stacked(ModuleWithStateDictSerialization, Generic[M]):
398
519
  return do_fold(init, *args, **kwargs)
399
520
 
400
521
  @overload
401
- def fold_via(self, fn: FoldFunction[M, CarryT]) -> Callable[[CarryT], CarryT]:
522
+ def fold_via(self, fn: FoldFunction[M, P, CarryT]) -> Callable[Concatenate[CarryT, P], CarryT]:
402
523
  ...
403
524
 
404
525
  @overload
@@ -412,16 +533,16 @@ class Stacked(ModuleWithStateDictSerialization, Generic[M]):
412
533
  returned function mirrors :func:`haliax.fold` over the block axis.
413
534
  """
414
535
 
415
- def do_block(carry: CarryT, block: M) -> CarryT:
416
- return fn(block, carry)
536
+ def do_block(carry: CarryT, block: M, *args, **kwargs) -> CarryT:
537
+ return fn(block, carry, *args, **kwargs)
417
538
 
418
- def do_fold(init: CarryT) -> CarryT:
419
- return haliax.fold(do_block, self.Block, remat=self.gradient_checkpointing)(init, self.stacked)
539
+ def do_fold(init: CarryT, *args, **kwargs) -> CarryT:
540
+ return haliax.fold(do_block, self.Block, remat=self.gradient_checkpointing)(init, self.stacked, *args, **kwargs)
420
541
 
421
542
  return do_fold
422
543
 
423
544
  @overload
424
- def scan_via(self, fn: ScanFunction[M, CarryT, OutputT_co]) -> Callable[[CarryT], tuple[CarryT, OutputT_co]]:
545
+ def scan_via(self, fn: ScanFunction[M, CarryT, P, OutputT_co]) -> Callable[Concatenate[CarryT, P], tuple[CarryT, OutputT_co]]:
425
546
  ...
426
547
 
427
548
  @overload
@@ -435,42 +556,47 @@ class Stacked(ModuleWithStateDictSerialization, Generic[M]):
435
556
  Semantics match :func:`haliax.scan` over the block axis.
436
557
  """
437
558
 
438
- def do_block(carry: CarryT, block: M) -> tuple[CarryT, OutputT_co]:
439
- return fn(block, carry)
559
+ def do_block(carry: CarryT, block: M, *args, **kwargs) -> tuple[CarryT, OutputT_co]:
560
+ carry, output = fn(block, carry, *args, **kwargs)
561
+ return carry, output
440
562
 
441
- def do_scan(init: CarryT) -> tuple[CarryT, OutputT_co]:
442
- return haliax.scan(do_block, self.Block, remat=self.gradient_checkpointing)(init, self.stacked)
563
+
564
+ def do_scan(init: CarryT, *args, **kwargs) -> tuple[CarryT, OutputT_co]:
565
+ return haliax.scan(do_block, self.Block, remat=self.gradient_checkpointing)(init, self.stacked, *args, **kwargs)
443
566
 
444
567
  return do_scan
445
568
 
446
- def vmap(self, init, *extra_args, **extra_kwargs):
447
- """Apply each block independently using :func:`haliax.vmap`.
569
+ @overload
570
+ def vmap_via(self, fn: VmapFunction[M, P, OutputT_co]) -> Callable[P, OutputT_co]:
571
+ ...
572
+
573
+ @overload
574
+ def vmap_via(self, fn: Callable[[M], OutputT_co]) -> Callable[[], OutputT_co]:
575
+ ...
576
+
577
+ def vmap_via(self, fn: Callable[..., OutputT_co]) -> Callable[..., OutputT_co]:
578
+ """Return a function that applies each block independently using ``fn``.
579
+
580
+ ``fn`` should take a block and a carry and return an output. The
581
+ returned function mirrors :func:`haliax.vmap` over the block axis.
582
+ """
583
+
584
+ def do_vmap(*args, **kwargs) -> OutputT_co:
585
+ # Create a function that captures the additional arguments
586
+ def do_block_with_args(block: M, *args, **kwargs) -> OutputT_co:
587
+ return fn(block, *args, **kwargs)
588
+
589
+ return haliax.vmap(do_block_with_args, self.Block)(self.stacked, *args, **kwargs)
448
590
 
449
- This maps ``init`` through every block in parallel, so each block
450
- receives the same ``init`` but its own parameters. Extra ``args`` and
451
- ``kwargs`` are also mapped over the block axis by default.
591
+ return do_vmap
592
+
593
+ def vmap(self, *extra_args, **extra_kwargs):
594
+ """Apply each block independently using :func:`haliax.vmap`.
452
595
 
453
596
  Returns the stacked outputs of each block.
454
597
  """
455
598
 
456
- if haliax.is_named_array(init):
457
- init = init.broadcast_axis(self.Block)
458
- elif haliax.jax_utils.is_jax_array_like(init):
459
- init = jnp.broadcast_to(init, (self.Block.size,) + init.shape)
460
- else:
461
- init = tuple(init for _ in range(self.Block.size))
462
-
463
- arg_spec = (0, 0) + (0,) * len(extra_args)
464
- kwarg_spec = {k: 0 for k in extra_kwargs}
465
-
466
- do_vmap = haliax.vmap(
467
- Stacked._do_block,
468
- self.Block,
469
- default=0,
470
- args=arg_spec,
471
- kwargs=kwarg_spec,
472
- )
473
- return do_vmap(init, self.stacked, *extra_args, **extra_kwargs)
599
+ return haliax.vmap(type(self.stacked).__call__, self.Block)(self.stacked, *extra_args, **extra_kwargs)
474
600
 
475
601
  @staticmethod
476
602
  def _do_block(carry, block, *extra_args, **extra_kwargs):
@@ -85,6 +85,8 @@ def where(
85
85
  raise ValueError("x must be a NamedArray or scalar if y is a NamedArray")
86
86
  x = named(x, ())
87
87
  x, y = broadcast_arrays(x, y)
88
+ if isinstance(condition, NamedArray):
89
+ condition = condition.scalar()
88
90
  return jax.lax.cond(condition, lambda _: x, lambda _: y, None)
89
91
 
90
92
  condition, x, y = broadcast_arrays(condition, x, y) # type: ignore
@@ -5,6 +5,7 @@ import jax
5
5
  from haliax.core import NamedArray, _broadcast_order, broadcast_to
6
6
 
7
7
  from .axis import AxisSelection, AxisSelector, axis_spec_to_shape_dict, eliminate_axes
8
+ from .jax_utils import is_scalarish
8
9
 
9
10
 
10
11
  def wrap_elemwise_unary(f, a, *args, **kwargs):
@@ -94,8 +95,33 @@ def wrap_elemwise_binary(op):
94
95
  b = broadcast_to(b, axes)
95
96
  return NamedArray(op(a.array, b.array), axes)
96
97
  elif isinstance(a, NamedArray):
98
+ # b isn't named.
99
+ if not is_scalarish(a):
100
+ if not is_scalarish(b):
101
+ raise ValueError(
102
+ f"Cannot apply {op.__name__} to a NamedArray and a non-scalar {type(b)}. "
103
+ "Either both must be NamedArrays or b must be a scalar."
104
+ )
105
+ else:
106
+ if is_scalarish(b):
107
+ return NamedArray(op(a.array, b), a.axes)
108
+ a = a.scalar()
109
+ return op(a, b)
110
+
97
111
  return NamedArray(op(a.array, b), a.axes)
98
112
  elif isinstance(b, NamedArray):
113
+ if not is_scalarish(b):
114
+ if not is_scalarish(a):
115
+ raise ValueError(
116
+ f"Cannot apply {op.__name__} to a non-scalar {type(a)} and a NamedArray. "
117
+ "Either both must be NamedArrays or a must be a scalar."
118
+ )
119
+ else:
120
+ if is_scalarish(a):
121
+ return NamedArray(op(a, b.array), b.axes)
122
+ b = b.scalar()
123
+ return op(a, b)
124
+
99
125
  return NamedArray(op(a, b.array), b.axes)
100
126
  else:
101
127
  return op(a, b)
@@ -34,7 +34,7 @@ def test_reduction_functions():
34
34
  m1 = NamedArray(rand_m, (Height, Width, Depth))
35
35
 
36
36
  # sum out everything
37
- assert jnp.all(jnp.equal(hax.sum(m1), jnp.sum(m1.array)))
37
+ assert jnp.all(jnp.equal(hax.sum(m1).array, jnp.sum(m1.array)))
38
38
  # ensure it's a scalar
39
39
 
40
40
  assert jnp.all(jnp.equal(hax.sum(m1, axis=Height).array, jnp.sum(m1.array, axis=0)))
@@ -60,7 +60,7 @@ def test_reduction_functions():
60
60
  )
61
61
 
62
62
  # argmax
63
- assert jnp.all(jnp.equal(hax.argmax(m1, axis=None), jnp.argmax(m1.array)))
63
+ assert jnp.all(jnp.equal(hax.argmax(m1, axis=None).array, jnp.argmax(m1.array)))
64
64
  assert jnp.all(jnp.equal(hax.argmax(m1, axis=Height).array, jnp.argmax(m1.array, axis=0)))
65
65
 
66
66
 
@@ -75,7 +75,7 @@ def test_reduction_functions_with_where():
75
75
  jmask = m1.array > 0.5
76
76
 
77
77
  # sum out everything
78
- assert jnp.all(jnp.equal(hax.sum(m1, where=mask), jnp.sum(rand_m, where=jmask)))
78
+ assert jnp.all(jnp.equal(hax.sum(m1, where=mask).array, jnp.sum(rand_m, where=jmask)))
79
79
  # ensure it's a scalar
80
80
 
81
81
  assert jnp.all(jnp.equal(hax.sum(m1, axis=H, where=mask).array, jnp.sum(rand_m, axis=0, where=jmask)))
@@ -507,7 +507,7 @@ def test_index():
507
507
  assert named1[{"H": slice(0, 10, 2), "W": 0}].axes == (Axis("H", 5), D)
508
508
 
509
509
  # try indexing with 3 integers: returns scalar ndarray
510
- assert jnp.all(jnp.equal(named1[{"H": 0, "W": 0, "D": 0}], named1.array[0, 0, 0]))
510
+ assert jnp.all(jnp.equal(named1[{"H": 0, "W": 0, "D": 0}].array, named1.array[0, 0, 0]))
511
511
 
512
512
 
513
513
  def test_index_with_tracer():
@@ -31,7 +31,7 @@ def test_dot():
31
31
  # reduce to scalar
32
32
  assert jnp.all(
33
33
  jnp.equal(
34
- hax.dot(m1, m2, axis=None),
34
+ hax.dot(m1, m2, axis=None).array,
35
35
  jnp.einsum("ijk,kji->", m1.array, m2.array),
36
36
  )
37
37
  )
@@ -79,7 +79,7 @@ def test_scan_doesnt_scan_scalars():
79
79
  named1 = hax.random.uniform(PRNGKey(0), (Height,))
80
80
 
81
81
  def scan_fun(acc, z, x):
82
- return acc + z * x, x * z
82
+ return (acc + z * x).scalar(), x * z
83
83
 
84
84
  total, selected = hax.scan(scan_fun, Height)(0.0, 4.0, named1)
85
85
 
@@ -215,14 +215,14 @@ def test_mean_respects_where():
215
215
  named1 = hax.random.uniform(PRNGKey(0), (Height, Width))
216
216
  where = hax.random.uniform(PRNGKey(1), (Height, Width)) > 0.5
217
217
 
218
- assert not jnp.all(jnp.isclose(hax.mean(named1), hax.mean(named1, where=where)))
219
- assert jnp.all(jnp.isclose(hax.mean(named1, where=where), jnp.mean(named1.array, where=where.array)))
218
+ assert not hax.all(hax.isclose(hax.mean(named1), hax.mean(named1, where=where)))
219
+ assert jnp.all(jnp.isclose(hax.mean(named1, where=where).array, jnp.mean(named1.array, where=where.array)))
220
220
 
221
221
  # check broadcasting
222
222
  where = hax.random.uniform(PRNGKey(2), (Height,)) > 0.5
223
- assert not jnp.all(jnp.isclose(hax.mean(named1), hax.mean(named1, where=where)))
223
+ assert not jnp.all(jnp.isclose(hax.mean(named1).array, hax.mean(named1, where=where).array))
224
224
  assert jnp.all(
225
- jnp.isclose(hax.mean(named1, where=where), jnp.mean(named1.array, where=where.array.reshape((-1, 1))))
225
+ jnp.isclose(hax.mean(named1, where=where).array, jnp.mean(named1.array, where=where.array.reshape((-1, 1))))
226
226
  )
227
227
 
228
228