haliax 1.4.dev443__tar.gz → 1.4.dev445__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 (134) hide show
  1. {haliax-1.4.dev443 → haliax-1.4.dev445}/PKG-INFO +1 -1
  2. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/__about__.py +1 -1
  3. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/_src/state_dict.py +20 -7
  4. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/jax_utils.py +82 -3
  5. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/scan.py +102 -40
  6. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_scan.py +239 -2
  7. {haliax-1.4.dev443 → haliax-1.4.dev445}/.agents/projects/api_parity.md +0 -0
  8. {haliax-1.4.dev443 → haliax-1.4.dev445}/.coveragerc +0 -0
  9. {haliax-1.4.dev443 → haliax-1.4.dev445}/.flake8 +0 -0
  10. {haliax-1.4.dev443 → haliax-1.4.dev445}/.github/workflows/publish_dev.yaml +0 -0
  11. {haliax-1.4.dev443 → haliax-1.4.dev445}/.github/workflows/run_pre_commit.yaml +0 -0
  12. {haliax-1.4.dev443 → haliax-1.4.dev445}/.github/workflows/run_quick_levanter_tests.yaml +0 -0
  13. {haliax-1.4.dev443 → haliax-1.4.dev445}/.github/workflows/run_tests.yaml +0 -0
  14. {haliax-1.4.dev443 → haliax-1.4.dev445}/.gitignore +0 -0
  15. {haliax-1.4.dev443 → haliax-1.4.dev445}/.playbooks/add-types.md +0 -0
  16. {haliax-1.4.dev443 → haliax-1.4.dev445}/.playbooks/wrap-non-named.md +0 -0
  17. {haliax-1.4.dev443 → haliax-1.4.dev445}/.pre-commit-config.yaml +0 -0
  18. {haliax-1.4.dev443 → haliax-1.4.dev445}/.readthedocs.yaml +0 -0
  19. {haliax-1.4.dev443 → haliax-1.4.dev445}/AGENTS.md +0 -0
  20. {haliax-1.4.dev443 → haliax-1.4.dev445}/AUTHORS.md +0 -0
  21. {haliax-1.4.dev443 → haliax-1.4.dev445}/CONTRIBUTING.md +0 -0
  22. {haliax-1.4.dev443 → haliax-1.4.dev445}/CONTRIBUTORS.md +0 -0
  23. {haliax-1.4.dev443 → haliax-1.4.dev445}/LICENSE +0 -0
  24. {haliax-1.4.dev443 → haliax-1.4.dev445}/README.md +0 -0
  25. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/api.md +0 -0
  26. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/broadcasting.md +0 -0
  27. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/cheatsheet.md +0 -0
  28. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/css/material.css +0 -0
  29. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/css/mkdocstrings.css +0 -0
  30. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/faq.md +0 -0
  31. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/figures/data_parallel_mesh.png +0 -0
  32. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/figures/data_parallel_mesh_replicated.png +0 -0
  33. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/figures/device_mesh_1d.png +0 -0
  34. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/figures/device_mesh_1d_zero.png +0 -0
  35. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/figures/device_mesh_2d.png +0 -0
  36. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/figures/device_mesh_2d_batch_partitioned.png +0 -0
  37. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/figures/device_mesh_2d_data_replicated.png +0 -0
  38. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/figures/device_mesh_2d_data_replicated_mlp_partitioned.png +0 -0
  39. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/figures/device_mesh_2d_intermediate_fully_partitioned.png +0 -0
  40. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/figures/device_mesh_2d_zero.png +0 -0
  41. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/fp8.md +0 -0
  42. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/index.md +0 -0
  43. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/indexing.md +0 -0
  44. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/matmul.md +0 -0
  45. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/nn.md +0 -0
  46. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/partitioning.md +0 -0
  47. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/primer.md +0 -0
  48. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/rearrange.ipynb +0 -0
  49. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/rearrange.md +0 -0
  50. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/requirements.txt +0 -0
  51. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/scan.md +0 -0
  52. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/state-dict.md +0 -0
  53. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/tutorial.md +0 -0
  54. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/typing.md +0 -0
  55. {haliax-1.4.dev443 → haliax-1.4.dev445}/docs/vmap.md +0 -0
  56. {haliax-1.4.dev443 → haliax-1.4.dev445}/etc/license_header.txt +0 -0
  57. {haliax-1.4.dev443 → haliax-1.4.dev445}/mkdocs.yml +0 -0
  58. {haliax-1.4.dev443 → haliax-1.4.dev445}/pyproject.toml +0 -0
  59. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/__init__.py +0 -0
  60. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/_src/__init__.py +0 -0
  61. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/_src/compile_utils.py +0 -0
  62. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/_src/dot.py +0 -0
  63. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/_src/einsum.py +0 -0
  64. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/_src/fp8.py +0 -0
  65. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/_src/parsing.py +0 -0
  66. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/_src/rearrange.py +0 -0
  67. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/_src/scan.py +0 -0
  68. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/_src/util.py +0 -0
  69. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/axis.py +0 -0
  70. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/core.py +0 -0
  71. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/debug.py +0 -0
  72. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/fft.py +0 -0
  73. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/field.py +0 -0
  74. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/haxtyping.py +0 -0
  75. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/hof.py +0 -0
  76. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/__init__.py +0 -0
  77. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/activations.py +0 -0
  78. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/attention.py +0 -0
  79. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/conv.py +0 -0
  80. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/dropout.py +0 -0
  81. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/embedding.py +0 -0
  82. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/linear.py +0 -0
  83. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/loss.py +0 -0
  84. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/mlp.py +0 -0
  85. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/mup.py +0 -0
  86. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/normalization.py +0 -0
  87. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/nn/pool.py +0 -0
  88. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/ops.py +0 -0
  89. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/partitioning.py +0 -0
  90. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/poly.py +0 -0
  91. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/quantization.py +0 -0
  92. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/random.py +0 -0
  93. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/specialized_fns.py +0 -0
  94. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/state_dict.py +0 -0
  95. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/tree.py +0 -0
  96. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/tree_util.py +0 -0
  97. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/types.py +0 -0
  98. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/util.py +0 -0
  99. {haliax-1.4.dev443 → haliax-1.4.dev445}/src/haliax/wrap.py +0 -0
  100. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/core_test.py +0 -0
  101. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_attention.py +0 -0
  102. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_axis.py +0 -0
  103. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_bitwise_ops.py +0 -0
  104. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_conv.py +0 -0
  105. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_debug.py +0 -0
  106. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_dot.py +0 -0
  107. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_dtype_typing.py +0 -0
  108. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_einsum.py +0 -0
  109. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_fft.py +0 -0
  110. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_field.py +0 -0
  111. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_fp8.py +0 -0
  112. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_hof.py +0 -0
  113. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_int8.py +0 -0
  114. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_moe_linear.py +0 -0
  115. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_mup_coordinate_check.py +0 -0
  116. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_mup_embedding.py +0 -0
  117. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_mup_linear.py +0 -0
  118. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_namedarray_typing.py +0 -0
  119. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_nan_reductions.py +0 -0
  120. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_nn.py +0 -0
  121. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_ops.py +0 -0
  122. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_parsing.py +0 -0
  123. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_partitioning.py +0 -0
  124. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_poly_ops.py +0 -0
  125. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_pool.py +0 -0
  126. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_random.py +0 -0
  127. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_rearrange.py +0 -0
  128. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_scatter_gather.py +0 -0
  129. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_specialized_fns.py +0 -0
  130. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_state_dict.py +0 -0
  131. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_tree_util.py +0 -0
  132. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_utils.py +0 -0
  133. {haliax-1.4.dev443 → haliax-1.4.dev445}/tests/test_visualize_sharding.py +0 -0
  134. {haliax-1.4.dev443 → haliax-1.4.dev445}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haliax
3
- Version: 1.4.dev443
3
+ Version: 1.4.dev445
4
4
  Summary: Named Tensors for Legible Deep Learning in JAX
5
5
  Project-URL: Homepage, https://github.com/stanford-crfm/haliax
6
6
  Project-URL: Bug Tracker, https://github.com/stanford-crfm/haliax/issues/
@@ -3,4 +3,4 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
 
6
- __version__ = "1.4.dev443"
6
+ __version__ = "1.4.dev445"
@@ -13,7 +13,7 @@ import jax
13
13
  import jax.numpy as jnp
14
14
  import numpy as np
15
15
  from jax import ShapeDtypeStruct
16
- from jax.experimental.multihost_utils import sync_global_devices
16
+ from haliax.jax_utils import sync_global_devices
17
17
  from jax.sharding import Mesh, NamedSharding, PartitionSpec
18
18
  from jax.tree_util import DictKey, FlattenedIndexKey, GetAttrKey, SequenceKey
19
19
  from jaxtyping import PyTree
@@ -68,7 +68,11 @@ def flatten_modules_for_export(t: T) -> T:
68
68
  )
69
69
  return module
70
70
 
71
- return scan_aware_tree_map(_flatten_module, t, is_leaf=lambda x: isinstance(x, ModuleWithStateDictSerialization))
71
+ return scan_aware_tree_map(
72
+ _flatten_module,
73
+ t,
74
+ is_leaf=lambda x: isinstance(x, ModuleWithStateDictSerialization),
75
+ )
72
76
 
73
77
 
74
78
  def unflatten_modules_from_export(t: T, template: T) -> T:
@@ -88,7 +92,10 @@ def unflatten_modules_from_export(t: T, template: T) -> T:
88
92
  return module
89
93
 
90
94
  return scan_aware_tree_map(
91
- _unflatten_module, t, template, is_leaf=lambda x: isinstance(x, ModuleWithStateDictSerialization)
95
+ _unflatten_module,
96
+ t,
97
+ template,
98
+ is_leaf=lambda x: isinstance(x, ModuleWithStateDictSerialization),
92
99
  )
93
100
 
94
101
 
@@ -187,9 +194,13 @@ def from_state_dict(tree: T, state_dict: StateDict, prefix: str | None = None) -
187
194
  else:
188
195
  return default_eqx_module_from_state_dict(tree, state_dict, prefix)
189
196
  elif isinstance(tree, list):
190
- return [from_state_dict(item, state_dict, with_prefix(prefix, str(i))) for i, item in enumerate(tree)] # type: ignore
197
+ return [
198
+ from_state_dict(item, state_dict, with_prefix(prefix, str(i))) for i, item in enumerate(tree)
199
+ ] # type: ignore
191
200
  elif isinstance(tree, dict):
192
- return {k: from_state_dict(v, state_dict, prefix=with_prefix(prefix, k)) for k, v in tree.items()} # type: ignore
201
+ return {
202
+ k: from_state_dict(v, state_dict, prefix=with_prefix(prefix, k)) for k, v in tree.items()
203
+ } # type: ignore
193
204
  elif isinstance(tree, NamedArray):
194
205
  if prefix is None:
195
206
  raise ValueError("Cannot extract a leaf value from a torch dict without a prefix")
@@ -383,7 +394,8 @@ def to_numpy_state_dict(model, prefix: str | None = None) -> StateDict:
383
394
  # TODO: ensure that this mesh arranges devices correctly
384
395
  # (jax seems to do this internally itself, so we should be fine?)
385
396
  process_mesh = Mesh(
386
- np.array(jax.devices()).reshape((jax.process_count(), -1)), ("process", "device")
397
+ np.array(jax.devices()).reshape((jax.process_count(), -1)),
398
+ ("process", "device"),
387
399
  )
388
400
 
389
401
  # now we need to find an axis along which we can shard the array.
@@ -391,7 +403,8 @@ def to_numpy_state_dict(model, prefix: str | None = None) -> StateDict:
391
403
 
392
404
  try:
393
405
  axis_to_shard = index_where(
394
- lambda axis_size: axis_size % process_mesh.devices.size == 0, arr.shape
406
+ lambda axis_size: axis_size % process_mesh.devices.size == 0,
407
+ arr.shape,
395
408
  )
396
409
  except ValueError:
397
410
  return np.array(arr)
@@ -6,6 +6,7 @@
6
6
  import functools as ft
7
7
  import typing
8
8
  import warnings
9
+ import zlib
9
10
  from typing import Any, Callable, Sequence
10
11
 
11
12
  import equinox as eqx
@@ -14,6 +15,8 @@ import numpy as np
14
15
  from jax import Array
15
16
  from jax import numpy as jnp
16
17
  from jax import random as jrandom
18
+ from jax.experimental.multihost_utils import host_local_array_to_global_array
19
+ from jax.sharding import PartitionSpec
17
20
  from jax.ad_checkpoint import checkpoint_name
18
21
  from jax.typing import DTypeLike
19
22
  from jaxtyping import PRNGKeyArray
@@ -39,6 +42,71 @@ class Static(eqx.Module):
39
42
  value: Any = eqx.field(static=True)
40
43
 
41
44
 
45
+ # Non-busted version of broadcast_one_to_all from jax.multihost_utils. (The issue is that if you use a non-contiguous
46
+ # mesh, their utility blows up because it makes a contiguous mesh.)
47
+
48
+
49
+ def _psum(xs: Any) -> Any:
50
+ return jax.tree.map(lambda x: jnp.sum(x, dtype=x.dtype, axis=0), xs)
51
+
52
+
53
+ def broadcast_one_to_all(in_tree: Any, is_source: bool | None = None) -> Any:
54
+ """Broadcast data from a source host (host 0 by default) to all other hosts.
55
+
56
+ Args:
57
+ in_tree: pytree of arrays - each array *must* have the same shape across the
58
+ hosts.
59
+ is_source: optional bool denoting whether the caller is the source. Only
60
+ 'source host' will contribute the data for the broadcast. If None, then
61
+ host 0 is used.
62
+
63
+ Returns:
64
+ A pytree matching in_tree where the leaves now all contain the data from the
65
+ first host.
66
+ """
67
+ if jax.process_count() == 1:
68
+ return jax.tree.map(np.asarray, in_tree)
69
+
70
+ if is_source is None:
71
+ is_source = jax.process_index() == 0
72
+
73
+ devices: np.ndarray = np.array(jax.devices()).reshape(jax.process_count(), jax.local_device_count())
74
+ global_mesh = jax.sharding.Mesh(devices, ("processes", "local_devices"))
75
+ pspec = PartitionSpec("processes")
76
+
77
+ def pre_jit(x):
78
+ if is_source:
79
+ inp = x
80
+ else:
81
+ inp = np.zeros_like(x)
82
+ inp = np.expand_dims(inp, axis=0)
83
+ return host_local_array_to_global_array(inp, global_mesh, pspec)
84
+
85
+ def post_jit(x):
86
+ return jax.device_get(x.addressable_data(0))
87
+
88
+ with haliax.partitioning.set_mesh(global_mesh):
89
+ in_tree = jax.tree.map(pre_jit, in_tree)
90
+ out_tree = jax.jit(
91
+ _psum,
92
+ out_shardings=jax.sharding.NamedSharding(global_mesh, PartitionSpec()),
93
+ )(in_tree)
94
+ return jax.tree.map(post_jit, out_tree)
95
+
96
+
97
+ def assert_equal(in_tree, fail_message: str = ""):
98
+ """Verifies that all the hosts have the same tree of values."""
99
+ expected = broadcast_one_to_all(in_tree)
100
+ if not jax.tree_util.tree_all(jax.tree_util.tree_map(lambda *x: np.all(np.equal(*x)), in_tree, expected)):
101
+ raise AssertionError(f"{fail_message} Expected: {expected}; got: {in_tree}.")
102
+
103
+
104
+ def sync_global_devices(name: str):
105
+ """Creates a barrier across all hosts/devices."""
106
+ h = np.uint32(zlib.crc32(name.encode()))
107
+ assert_equal(h, f"sync_global_devices name mismatch ('{name}')")
108
+
109
+
42
110
  def shaped_rng_split(key, split_shape: int | Sequence[int] = 2) -> PRNGKeyArray:
43
111
  if isinstance(split_shape, int):
44
112
  num_splits = split_shape
@@ -69,14 +137,25 @@ def maybe_rng_split(key: PRNGKeyArray | None, num: int = 2):
69
137
  def filter_eval_shape(*args, **kwargs):
70
138
  import warnings
71
139
 
72
- warnings.warn("filter_eval_shape is deprecated, use eqx.filter_eval_shape instead", DeprecationWarning)
140
+ warnings.warn(
141
+ "filter_eval_shape is deprecated, use eqx.filter_eval_shape instead",
142
+ DeprecationWarning,
143
+ )
73
144
  return eqx.filter_eval_shape(*args, **kwargs)
74
145
 
75
146
 
76
- def filter_checkpoint(fun: Callable, *, prevent_cse: bool = True, policy: Callable[..., bool] | None = None):
147
+ def filter_checkpoint(
148
+ fun: Callable,
149
+ *,
150
+ prevent_cse: bool = True,
151
+ policy: Callable[..., bool] | None = None,
152
+ ):
77
153
  """As `jax.checkpoint`, but allows any Python object as inputs and outputs"""
78
154
 
79
- warnings.warn("filter_checkpoint is deprecated, use eqx.filter_checkpoint instead", DeprecationWarning)
155
+ warnings.warn(
156
+ "filter_checkpoint is deprecated, use eqx.filter_checkpoint instead",
157
+ DeprecationWarning,
158
+ )
80
159
 
81
160
  return eqx.filter_checkpoint(fun, prevent_cse=prevent_cse, policy=policy)
82
161
 
@@ -62,6 +62,22 @@ class ModuleInit(Protocol[M_co]):
62
62
  def __call__(self, *args, **kwargs) -> M_co: ...
63
63
 
64
64
 
65
+ def _normalize_unroll(unroll: int | bool | None, block_size: int) -> int | bool:
66
+ """Convert user-provided ``unroll`` values into something understood by ``jax.lax.scan``."""
67
+
68
+ if unroll is None:
69
+ return 1
70
+
71
+ if isinstance(unroll, bool):
72
+ return unroll
73
+
74
+ resolved = int(unroll)
75
+ if resolved < 1:
76
+ raise ValueError(f"unroll must be >= 1; got {resolved}.")
77
+
78
+ return resolved
79
+
80
+
65
81
  class BlockFoldable(Protocol[M]):
66
82
  """Common interface for :class:`~haliax.nn.Stacked` and :class:`~haliax.nn.BlockSeq`.
67
83
 
@@ -84,29 +100,40 @@ class BlockFoldable(Protocol[M]):
84
100
  prevent_cse: bool = False,
85
101
  ) -> ModuleInit[S]: ...
86
102
 
87
- def scan(self, init: T, *extra_args, **extra_kwargs): ...
103
+ def scan(self, init: T, *extra_args, unroll: int | bool | None = None, **extra_kwargs): ...
88
104
 
89
- def fold(self, init: T, *args, **kwargs) -> T: ...
105
+ def fold(self, init: T, *args, unroll: int | bool | None = None, **kwargs) -> T: ...
90
106
 
91
107
  @overload
92
- def fold_via(self, fn: FoldFunction[M, P, CarryT]) -> Callable[Concatenate[CarryT, P], CarryT]: ...
108
+ def fold_via(
109
+ self, fn: FoldFunction[M, P, CarryT], *, unroll: int | bool | None = None
110
+ ) -> Callable[Concatenate[CarryT, P], CarryT]: ...
93
111
 
94
112
  @overload
95
- def fold_via(self, fn: Callable[[M, CarryT], CarryT]) -> Callable[[CarryT], CarryT]: ...
113
+ def fold_via(
114
+ self, fn: Callable[[M, CarryT], CarryT], *, unroll: int | bool | None = None
115
+ ) -> Callable[[CarryT], CarryT]: ...
96
116
 
97
- def fold_via(self, fn: Callable[..., CarryT]) -> Callable[Concatenate[CarryT, P], CarryT]: ...
117
+ def fold_via(
118
+ self, fn: Callable[..., CarryT], *, unroll: int | bool | None = None
119
+ ) -> Callable[Concatenate[CarryT, P], CarryT]: ...
98
120
 
99
121
  @overload
100
122
  def scan_via(
101
- self, fn: ScanFunction[M, CarryT, P, OutputT_co]
123
+ self, fn: ScanFunction[M, CarryT, P, OutputT_co], *, unroll: int | bool | None = None
102
124
  ) -> Callable[Concatenate[CarryT, P], tuple[CarryT, OutputT_co]]: ...
103
125
 
104
126
  @overload
105
127
  def scan_via(
106
- self, fn: Callable[[M, CarryT], tuple[CarryT, OutputT_co]]
128
+ self,
129
+ fn: Callable[[M, CarryT], tuple[CarryT, OutputT_co]],
130
+ *,
131
+ unroll: int | bool | None = None,
107
132
  ) -> Callable[[CarryT], tuple[CarryT, OutputT_co]]: ...
108
133
 
109
- def scan_via(self, fn: Callable[..., tuple[CarryT, OutputT_co]]) -> Callable[P, tuple[CarryT, OutputT_co]]: ...
134
+ def scan_via(
135
+ self, fn: Callable[..., tuple[CarryT, OutputT_co]], *, unroll: int | bool | None = None
136
+ ) -> Callable[P, tuple[CarryT, OutputT_co]]: ...
110
137
 
111
138
  @overload
112
139
  def vmap_via(self, fn: VmapFunction[M, P, OutputT_co]) -> Callable[P, OutputT_co]: ...
@@ -181,15 +208,15 @@ class BlockSeq(ModuleWithStateDictSerialization, Generic[M]):
181
208
 
182
209
  return fn
183
210
 
184
- def scan(self, init: T, *extra_args, **extra_kwargs):
211
+ def scan(self, init: T, *extra_args, unroll: int | bool | None = None, **extra_kwargs):
185
212
  def do_scan(init, *extra_args, **extra_kwargs):
186
213
  out = []
187
214
  carry = init
188
215
 
189
216
  for i, block in enumerate(self.blocks):
190
-
191
217
  (block_args, block_kwargs) = haliax.tree_util.tree_map(
192
- functools.partial(BlockSeq._slice_out, self.Block, i), (extra_args, extra_kwargs)
218
+ functools.partial(BlockSeq._slice_out, self.Block, i),
219
+ (extra_args, extra_kwargs),
193
220
  )
194
221
 
195
222
  block_result = block(carry, *block_args, **block_kwargs)
@@ -210,12 +237,13 @@ class BlockSeq(ModuleWithStateDictSerialization, Generic[M]):
210
237
 
211
238
  return do_scan(init, *extra_args, **extra_kwargs)
212
239
 
213
- def fold(self, init: T, *args, **kwargs) -> T:
240
+ def fold(self, init: T, *args, unroll: int | bool | None = None, **kwargs) -> T:
214
241
  def do_fold(init, *args, **kwargs):
215
242
  carry = init
216
243
  for i, block in enumerate(self.blocks):
217
244
  (block_args, block_kwargs) = haliax.tree_util.tree_map(
218
- functools.partial(BlockSeq._slice_out, self.Block, i), (args, kwargs)
245
+ functools.partial(BlockSeq._slice_out, self.Block, i),
246
+ (args, kwargs),
219
247
  )
220
248
  carry = block(carry, *block_args, **block_kwargs)
221
249
  carry = tree_checkpoint_name(carry, self._carry_ckpt_name)
@@ -224,12 +252,16 @@ class BlockSeq(ModuleWithStateDictSerialization, Generic[M]):
224
252
  return do_fold(init, *args, **kwargs)
225
253
 
226
254
  @overload
227
- def fold_via(self, fn: FoldFunction[M, P, CarryT]) -> Callable[Concatenate[CarryT, P], CarryT]: ...
255
+ def fold_via(
256
+ self, fn: FoldFunction[M, P, CarryT], *, unroll: int | bool | None = None
257
+ ) -> Callable[Concatenate[CarryT, P], CarryT]: ...
228
258
 
229
259
  @overload
230
- def fold_via(self, fn: Callable[[M, CarryT], CarryT]) -> Callable[[CarryT], CarryT]: ...
260
+ def fold_via(
261
+ self, fn: Callable[[M, CarryT], CarryT], *, unroll: int | bool | None = None
262
+ ) -> Callable[[CarryT], CarryT]: ...
231
263
 
232
- def fold_via(self, fn: Callable[..., CarryT]):
264
+ def fold_via(self, fn: Callable[..., CarryT], *, unroll: int | bool | None = None):
233
265
  """Return a function that folds over the sequence using ``fn``.
234
266
 
235
267
  ``fn`` should take a block and a carry and return a new carry. The
@@ -247,15 +279,18 @@ class BlockSeq(ModuleWithStateDictSerialization, Generic[M]):
247
279
 
248
280
  @overload
249
281
  def scan_via(
250
- self, fn: ScanFunction[M, CarryT, P, OutputT_co]
282
+ self, fn: ScanFunction[M, CarryT, P, OutputT_co], *, unroll: int | bool | None = None
251
283
  ) -> Callable[Concatenate[CarryT, P], tuple[CarryT, OutputT_co]]: ...
252
284
 
253
285
  @overload
254
286
  def scan_via(
255
- self, fn: Callable[[M, CarryT], tuple[CarryT, OutputT_co]]
287
+ self,
288
+ fn: Callable[[M, CarryT], tuple[CarryT, OutputT_co]],
289
+ *,
290
+ unroll: int | bool | None = None,
256
291
  ) -> Callable[[CarryT], tuple[CarryT, OutputT_co]]: ...
257
292
 
258
- def scan_via(self, fn: Callable[..., tuple[CarryT, OutputT_co]]):
293
+ def scan_via(self, fn: Callable[..., tuple[CarryT, OutputT_co]], *, unroll: int | bool | None = None):
259
294
  """Return a function that scans over the sequence using ``fn``.
260
295
 
261
296
  ``fn`` should take a block and a carry and return ``(carry, output)``.
@@ -447,7 +482,7 @@ class Stacked(ModuleWithStateDictSerialization, Generic[M]):
447
482
 
448
483
  return fn
449
484
 
450
- def scan(self, init, *extra_args, **extra_kwargs):
485
+ def scan(self, init, *extra_args, unroll: int | bool | None = None, **extra_kwargs):
451
486
  """
452
487
  Scan over the stacked module. This is the same as a for loop that applies each instance of the module in sequence
453
488
  to the input, passing the output of one instance to the next instance. It returns a stack of outputs as
@@ -475,19 +510,24 @@ class Stacked(ModuleWithStateDictSerialization, Generic[M]):
475
510
 
476
511
  """
477
512
 
513
+ resolved_unroll = _normalize_unroll(unroll, self.Block.size)
514
+
478
515
  def do_block(carry, block, *args, **kwargs):
479
516
  carry, out = block(carry, *args, **kwargs)
480
517
  return carry, out
481
518
 
482
519
  def do_scan(init, *extra_args, **extra_kwargs):
483
- carry, out = haliax.scan(do_block, self.Block, remat=self.gradient_checkpointing)(
484
- init, self.stacked, *extra_args, **extra_kwargs
485
- )
520
+ carry, out = haliax.scan(
521
+ do_block,
522
+ self.Block,
523
+ remat=self.gradient_checkpointing,
524
+ unroll=resolved_unroll,
525
+ )(init, self.stacked, *extra_args, **extra_kwargs)
486
526
  return carry, out
487
527
 
488
528
  return do_scan(init, *extra_args, **extra_kwargs)
489
529
 
490
- def fold(self, init, *args, **kwargs):
530
+ def fold(self, init, *args, unroll: int | bool | None = None, **kwargs):
491
531
  """
492
532
  Fold over the stacked module. This is the same as a for loop that applies each instance of the module in sequence
493
533
  to the input, passing the output of one instance to the next instance.
@@ -510,66 +550,88 @@ class Stacked(ModuleWithStateDictSerialization, Generic[M]):
510
550
 
511
551
  """
512
552
 
553
+ resolved_unroll = _normalize_unroll(unroll, self.Block.size)
554
+
513
555
  def do_block(carry, block, *args, **kwargs):
514
556
  carry = block(carry, *args, **kwargs)
515
557
  return carry
516
558
 
517
559
  def do_fold(init, *extra_args, **extra_kwargs):
518
- carry = haliax.fold(do_block, self.Block, remat=self.gradient_checkpointing)(
519
- init, self.stacked, *extra_args, **extra_kwargs
520
- )
560
+ carry = haliax.fold(
561
+ do_block,
562
+ self.Block,
563
+ remat=self.gradient_checkpointing,
564
+ unroll=resolved_unroll,
565
+ )(init, self.stacked, *extra_args, **extra_kwargs)
521
566
  return carry
522
567
 
523
568
  return do_fold(init, *args, **kwargs)
524
569
 
525
570
  @overload
526
- def fold_via(self, fn: FoldFunction[M, P, CarryT]) -> Callable[Concatenate[CarryT, P], CarryT]: ...
571
+ def fold_via(
572
+ self, fn: FoldFunction[M, P, CarryT], *, unroll: int | bool | None = None
573
+ ) -> Callable[Concatenate[CarryT, P], CarryT]: ...
527
574
 
528
575
  @overload
529
- def fold_via(self, fn: Callable[[M, CarryT], CarryT]) -> Callable[[CarryT], CarryT]: ...
576
+ def fold_via(
577
+ self, fn: Callable[[M, CarryT], CarryT], *, unroll: int | bool | None = None
578
+ ) -> Callable[[CarryT], CarryT]: ...
530
579
 
531
- def fold_via(self, fn: Callable[..., CarryT]):
580
+ def fold_via(self, fn: Callable[..., CarryT], *, unroll: int | bool | None = None):
532
581
  """Return a function that folds over the stack using ``fn``.
533
582
 
534
583
  ``fn`` should take a block and a carry and return a new carry. The
535
584
  returned function mirrors :func:`haliax.fold` over the block axis.
536
585
  """
537
586
 
587
+ resolved_unroll = _normalize_unroll(unroll, self.Block.size)
588
+
538
589
  def do_block(carry: CarryT, block: M, *args, **kwargs) -> CarryT:
539
590
  return fn(block, carry, *args, **kwargs)
540
591
 
541
592
  def do_fold(init: CarryT, *args, **kwargs) -> CarryT:
542
- return haliax.fold(do_block, self.Block, remat=self.gradient_checkpointing)(
543
- init, self.stacked, *args, **kwargs
544
- )
593
+ return haliax.fold(
594
+ do_block,
595
+ self.Block,
596
+ remat=self.gradient_checkpointing,
597
+ unroll=resolved_unroll,
598
+ )(init, self.stacked, *args, **kwargs)
545
599
 
546
600
  return do_fold
547
601
 
548
602
  @overload
549
603
  def scan_via(
550
- self, fn: ScanFunction[M, CarryT, P, OutputT_co]
604
+ self, fn: ScanFunction[M, CarryT, P, OutputT_co], *, unroll: int | bool | None = None
551
605
  ) -> Callable[Concatenate[CarryT, P], tuple[CarryT, OutputT_co]]: ...
552
606
 
553
607
  @overload
554
608
  def scan_via(
555
- self, fn: Callable[[M, CarryT], tuple[CarryT, OutputT_co]]
609
+ self,
610
+ fn: Callable[[M, CarryT], tuple[CarryT, OutputT_co]],
611
+ *,
612
+ unroll: int | bool | None = None,
556
613
  ) -> Callable[[CarryT], tuple[CarryT, OutputT_co]]: ...
557
614
 
558
- def scan_via(self, fn: Callable[..., tuple[CarryT, OutputT_co]]):
615
+ def scan_via(self, fn: Callable[..., tuple[CarryT, OutputT_co]], *, unroll: int | bool | None = None):
559
616
  """Return a function that scans over the stack using ``fn``.
560
617
 
561
618
  ``fn`` should take a block and a carry and return ``(carry, output)``.
562
619
  Semantics match :func:`haliax.scan` over the block axis.
563
620
  """
564
621
 
622
+ resolved_unroll = _normalize_unroll(unroll, self.Block.size)
623
+
565
624
  def do_block(carry: CarryT, block: M, *args, **kwargs) -> tuple[CarryT, OutputT_co]:
566
625
  carry, output = fn(block, carry, *args, **kwargs)
567
626
  return carry, output
568
627
 
569
628
  def do_scan(init: CarryT, *args, **kwargs) -> tuple[CarryT, OutputT_co]:
570
- return haliax.scan(do_block, self.Block, remat=self.gradient_checkpointing)(
571
- init, self.stacked, *args, **kwargs
572
- )
629
+ return haliax.scan(
630
+ do_block,
631
+ self.Block,
632
+ remat=self.gradient_checkpointing,
633
+ unroll=resolved_unroll,
634
+ )(init, self.stacked, *args, **kwargs)
573
635
 
574
636
  return do_scan
575
637
 
@@ -707,7 +769,7 @@ def _unstack_state_dict(state_dict: StateDict, prefix: str | None = None) -> Sta
707
769
  for k, v in state_dict.items():
708
770
  if k.startswith(prefix) and is_jax_or_hax_array_like(v):
709
771
  for i, v_i in enumerate(v):
710
- new_dict[f"{prefix}{i}.{k[len(prefix):]}"] = v_i
772
+ new_dict[f"{prefix}{i}.{k[len(prefix) :]}"] = v_i
711
773
  else:
712
774
  new_dict[k] = v
713
775