haliax 1.4.dev441__tar.gz → 1.4.dev444__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.dev441 → haliax-1.4.dev444}/PKG-INFO +1 -1
  2. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/__about__.py +1 -1
  3. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/embedding.py +7 -2
  4. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/linear.py +7 -2
  5. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/scan.py +102 -40
  6. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_mup_embedding.py +1 -1
  7. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_mup_linear.py +10 -5
  8. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_scan.py +239 -2
  9. {haliax-1.4.dev441 → haliax-1.4.dev444}/.agents/projects/api_parity.md +0 -0
  10. {haliax-1.4.dev441 → haliax-1.4.dev444}/.coveragerc +0 -0
  11. {haliax-1.4.dev441 → haliax-1.4.dev444}/.flake8 +0 -0
  12. {haliax-1.4.dev441 → haliax-1.4.dev444}/.github/workflows/publish_dev.yaml +0 -0
  13. {haliax-1.4.dev441 → haliax-1.4.dev444}/.github/workflows/run_pre_commit.yaml +0 -0
  14. {haliax-1.4.dev441 → haliax-1.4.dev444}/.github/workflows/run_quick_levanter_tests.yaml +0 -0
  15. {haliax-1.4.dev441 → haliax-1.4.dev444}/.github/workflows/run_tests.yaml +0 -0
  16. {haliax-1.4.dev441 → haliax-1.4.dev444}/.gitignore +0 -0
  17. {haliax-1.4.dev441 → haliax-1.4.dev444}/.playbooks/add-types.md +0 -0
  18. {haliax-1.4.dev441 → haliax-1.4.dev444}/.playbooks/wrap-non-named.md +0 -0
  19. {haliax-1.4.dev441 → haliax-1.4.dev444}/.pre-commit-config.yaml +0 -0
  20. {haliax-1.4.dev441 → haliax-1.4.dev444}/.readthedocs.yaml +0 -0
  21. {haliax-1.4.dev441 → haliax-1.4.dev444}/AGENTS.md +0 -0
  22. {haliax-1.4.dev441 → haliax-1.4.dev444}/AUTHORS.md +0 -0
  23. {haliax-1.4.dev441 → haliax-1.4.dev444}/CONTRIBUTING.md +0 -0
  24. {haliax-1.4.dev441 → haliax-1.4.dev444}/CONTRIBUTORS.md +0 -0
  25. {haliax-1.4.dev441 → haliax-1.4.dev444}/LICENSE +0 -0
  26. {haliax-1.4.dev441 → haliax-1.4.dev444}/README.md +0 -0
  27. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/api.md +0 -0
  28. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/broadcasting.md +0 -0
  29. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/cheatsheet.md +0 -0
  30. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/css/material.css +0 -0
  31. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/css/mkdocstrings.css +0 -0
  32. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/faq.md +0 -0
  33. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/figures/data_parallel_mesh.png +0 -0
  34. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/figures/data_parallel_mesh_replicated.png +0 -0
  35. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/figures/device_mesh_1d.png +0 -0
  36. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/figures/device_mesh_1d_zero.png +0 -0
  37. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/figures/device_mesh_2d.png +0 -0
  38. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/figures/device_mesh_2d_batch_partitioned.png +0 -0
  39. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/figures/device_mesh_2d_data_replicated.png +0 -0
  40. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/figures/device_mesh_2d_data_replicated_mlp_partitioned.png +0 -0
  41. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/figures/device_mesh_2d_intermediate_fully_partitioned.png +0 -0
  42. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/figures/device_mesh_2d_zero.png +0 -0
  43. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/fp8.md +0 -0
  44. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/index.md +0 -0
  45. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/indexing.md +0 -0
  46. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/matmul.md +0 -0
  47. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/nn.md +0 -0
  48. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/partitioning.md +0 -0
  49. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/primer.md +0 -0
  50. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/rearrange.ipynb +0 -0
  51. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/rearrange.md +0 -0
  52. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/requirements.txt +0 -0
  53. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/scan.md +0 -0
  54. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/state-dict.md +0 -0
  55. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/tutorial.md +0 -0
  56. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/typing.md +0 -0
  57. {haliax-1.4.dev441 → haliax-1.4.dev444}/docs/vmap.md +0 -0
  58. {haliax-1.4.dev441 → haliax-1.4.dev444}/etc/license_header.txt +0 -0
  59. {haliax-1.4.dev441 → haliax-1.4.dev444}/mkdocs.yml +0 -0
  60. {haliax-1.4.dev441 → haliax-1.4.dev444}/pyproject.toml +0 -0
  61. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/__init__.py +0 -0
  62. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/_src/__init__.py +0 -0
  63. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/_src/compile_utils.py +0 -0
  64. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/_src/dot.py +0 -0
  65. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/_src/einsum.py +0 -0
  66. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/_src/fp8.py +0 -0
  67. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/_src/parsing.py +0 -0
  68. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/_src/rearrange.py +0 -0
  69. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/_src/scan.py +0 -0
  70. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/_src/state_dict.py +0 -0
  71. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/_src/util.py +0 -0
  72. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/axis.py +0 -0
  73. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/core.py +0 -0
  74. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/debug.py +0 -0
  75. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/fft.py +0 -0
  76. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/field.py +0 -0
  77. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/haxtyping.py +0 -0
  78. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/hof.py +0 -0
  79. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/jax_utils.py +0 -0
  80. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/__init__.py +0 -0
  81. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/activations.py +0 -0
  82. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/attention.py +0 -0
  83. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/conv.py +0 -0
  84. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/dropout.py +0 -0
  85. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/loss.py +0 -0
  86. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/mlp.py +0 -0
  87. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/mup.py +0 -0
  88. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/normalization.py +0 -0
  89. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/nn/pool.py +0 -0
  90. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/ops.py +0 -0
  91. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/partitioning.py +0 -0
  92. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/poly.py +0 -0
  93. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/quantization.py +0 -0
  94. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/random.py +0 -0
  95. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/specialized_fns.py +0 -0
  96. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/state_dict.py +0 -0
  97. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/tree.py +0 -0
  98. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/tree_util.py +0 -0
  99. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/types.py +0 -0
  100. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/util.py +0 -0
  101. {haliax-1.4.dev441 → haliax-1.4.dev444}/src/haliax/wrap.py +0 -0
  102. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/core_test.py +0 -0
  103. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_attention.py +0 -0
  104. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_axis.py +0 -0
  105. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_bitwise_ops.py +0 -0
  106. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_conv.py +0 -0
  107. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_debug.py +0 -0
  108. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_dot.py +0 -0
  109. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_dtype_typing.py +0 -0
  110. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_einsum.py +0 -0
  111. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_fft.py +0 -0
  112. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_field.py +0 -0
  113. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_fp8.py +0 -0
  114. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_hof.py +0 -0
  115. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_int8.py +0 -0
  116. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_moe_linear.py +0 -0
  117. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_mup_coordinate_check.py +0 -0
  118. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_namedarray_typing.py +0 -0
  119. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_nan_reductions.py +0 -0
  120. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_nn.py +0 -0
  121. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_ops.py +0 -0
  122. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_parsing.py +0 -0
  123. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_partitioning.py +0 -0
  124. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_poly_ops.py +0 -0
  125. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_pool.py +0 -0
  126. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_random.py +0 -0
  127. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_rearrange.py +0 -0
  128. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_scatter_gather.py +0 -0
  129. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_specialized_fns.py +0 -0
  130. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_state_dict.py +0 -0
  131. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_tree_util.py +0 -0
  132. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_utils.py +0 -0
  133. {haliax-1.4.dev441 → haliax-1.4.dev444}/tests/test_visualize_sharding.py +0 -0
  134. {haliax-1.4.dev441 → haliax-1.4.dev444}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haliax
3
- Version: 1.4.dev441
3
+ Version: 1.4.dev444
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.dev441"
6
+ __version__ = "1.4.dev444"
@@ -24,7 +24,12 @@ class Embedding(eqx.Module, ReparamEnabled):
24
24
  # axes
25
25
  Vocab: Axis = eqx.field(static=True)
26
26
  Embed: AxisSpec = eqx.field(static=True)
27
- reparam: AbstractEmbeddingReparam = eqx.field(static=True)
27
+
28
+ _reparam_cls: type[AbstractEmbeddingReparam] = eqx.field(static=True, default=EmbeddingStandardParam)
29
+
30
+ @property
31
+ def reparam(self) -> AbstractEmbeddingReparam:
32
+ return self._reparam_cls(self.Embed, self.Vocab)
28
33
 
29
34
  @staticmethod
30
35
  def init(
@@ -61,7 +66,7 @@ class Embedding(eqx.Module, ReparamEnabled):
61
66
  weight = hax.random.truncated_normal(key, all_axes, -3, 3) * (
62
67
  init_scale * reparam_cls.init_scale(Vocab, Embed)
63
68
  )
64
- return Embedding(weight=weight, Vocab=Vocab, Embed=Embed, reparam=reparam_cls(Embed, Vocab))
69
+ return Embedding(weight=weight, Vocab=Vocab, Embed=Embed, _reparam_cls=reparam_cls)
65
70
 
66
71
  def __call__(self, input_ids: NamedArray, *, key: PRNGKeyArray | None = None):
67
72
  """Alias for `embed`. key is ignored."""
@@ -45,9 +45,14 @@ class Linear(ModuleWithStateDictSerialization, ReparamEnabled):
45
45
 
46
46
  In: AxisSpec = eqx.field(static=True)
47
47
  Out: AxisSpec = eqx.field(static=True)
48
- reparam: AbstractLinearReparam = eqx.field(static=True)
49
48
  dot_general: DotGeneralOp = eqx.field(default_factory=DotGeneralOp.default)
50
49
 
50
+ _reparam_cls: type[AbstractLinearReparam] = eqx.field(static=True, default=LinearStandardParam)
51
+
52
+ @property
53
+ def reparam(self) -> AbstractLinearReparam:
54
+ return self._reparam_cls(self.In, self.Out)
55
+
51
56
  @staticmethod
52
57
  def init(
53
58
  In: AxisSpec,
@@ -77,7 +82,7 @@ class Linear(ModuleWithStateDictSerialization, ReparamEnabled):
77
82
  if dot_general is None:
78
83
  dot_general = DotGeneralOp.default()
79
84
 
80
- return Linear(weight, bias, In, Out, dot_general=dot_general, reparam=reparam_cls(In, Out))
85
+ return Linear(weight, bias, In, Out, dot_general=dot_general, _reparam_cls=reparam_cls)
81
86
 
82
87
  @named_call
83
88
  def __call__(self, inputs, *, key: PRNGKeyArray | None = None):
@@ -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
 
@@ -31,7 +31,7 @@ def test_mup_embedding_unembedding_scale():
31
31
  Embed = (hax.Axis("E", 3),)
32
32
 
33
33
  weight = hax.ones(hax.concat_axis_specs(Vocab, Embed))
34
- layer = Embedding(weight=weight, Vocab=Vocab, Embed=Embed, reparam=EmbeddingMup(Embed, Vocab))
34
+ layer = Embedding(weight=weight, Vocab=Vocab, Embed=Embed, _reparam_cls=EmbeddingMup)
35
35
 
36
36
  scale = layer.reparam.unembed_active_scale
37
37
  assert scale == pytest.approx(1.0 / hax.axis_size(Embed))
@@ -11,7 +11,12 @@ import pytest
11
11
 
12
12
  import haliax as hax
13
13
  from haliax.nn import Linear
14
- from haliax.nn.mup import InputLinearMup, LinearStandardParam, HiddenLinearMup, OutputLinearMup
14
+ from haliax.nn.mup import (
15
+ InputLinearMup,
16
+ LinearStandardParam,
17
+ HiddenLinearMup,
18
+ OutputLinearMup,
19
+ )
15
20
 
16
21
 
17
22
  @pytest.mark.parametrize("out_first", [True, False])
@@ -37,8 +42,8 @@ def test_mup_linear_call_matches_linear():
37
42
  weight = hax.ones(hax.concat_axis_specs(Out, In)) * 0.5
38
43
  bias = hax.full(Out, 0.25)
39
44
 
40
- linear = Linear(weight, bias, In, Out, reparam=LinearStandardParam(In, Out))
41
- mup = Linear(weight, bias, In, Out, reparam=InputLinearMup(In, Out))
45
+ linear = Linear(weight, bias, In, Out, _reparam_cls=LinearStandardParam)
46
+ mup = Linear(weight, bias, In, Out, _reparam_cls=InputLinearMup)
42
47
 
43
48
  inputs = hax.full(hax.concat_axis_specs(Batch, In), 2.0)
44
49
 
@@ -109,8 +114,8 @@ def test_input_linear_behaves_like_base_linear():
109
114
  weight = hax.ones((Out, In)) * 0.1
110
115
  bias = hax.zeros(Out)
111
116
 
112
- linear = Linear(weight, bias, In, Out, reparam=LinearStandardParam(In, Out))
113
- input_linear = Linear(weight, bias, In, Out, reparam=InputLinearMup(In, Out))
117
+ linear = Linear(weight, bias, In, Out, _reparam_cls=LinearStandardParam)
118
+ input_linear = Linear(weight, bias, In, Out, _reparam_cls=InputLinearMup)
114
119
 
115
120
  inputs = hax.random.normal(jrandom.PRNGKey(5), (Batch, In))
116
121
 
@@ -6,6 +6,7 @@
6
6
  import equinox as eqx
7
7
  import jax
8
8
  import pytest
9
+ import warnings
9
10
  from equinox import filter_grad
10
11
 
11
12
  import haliax as hax
@@ -164,6 +165,59 @@ def test_scan_with_aux_named_args():
164
165
  assert hax.all(hax.isclose(z_scan, z_seq_scan, atol=1e-5))
165
166
 
166
167
 
168
+ def test_blockseq_unroll_never_warns():
169
+ class FoldModule(eqx.Module):
170
+ weight: hax.NamedArray
171
+
172
+ def __call__(self, carry: hax.NamedArray) -> hax.NamedArray:
173
+ return carry + self.weight
174
+
175
+ @staticmethod
176
+ def init(weight):
177
+ return FoldModule(weight=weight)
178
+
179
+ class ScanModule(eqx.Module):
180
+ weight: hax.NamedArray
181
+
182
+ def __call__(self, carry: hax.NamedArray) -> tuple[hax.NamedArray, hax.NamedArray]:
183
+ updated = carry + self.weight
184
+ return updated, updated
185
+
186
+ @staticmethod
187
+ def init(weight):
188
+ return ScanModule(weight=weight)
189
+
190
+ Block = hax.Axis("block", 3)
191
+ Value = hax.Axis("value", 2)
192
+ weights = hax.random.uniform(jax.random.PRNGKey(0), (Block, Value))
193
+
194
+ fold_seq = BlockSeq.init(Block, FoldModule)(weight=weights)
195
+ scan_seq = BlockSeq.init(Block, ScanModule)(weight=weights)
196
+
197
+ init_carry = hax.zeros(Value)
198
+
199
+ def fold_step(block: FoldModule, carry: hax.NamedArray) -> hax.NamedArray:
200
+ return block(carry)
201
+
202
+ def scan_step(block: ScanModule, carry: hax.NamedArray) -> tuple[hax.NamedArray, hax.NamedArray]:
203
+ return block(carry)
204
+
205
+ with warnings.catch_warnings(record=True) as caught:
206
+ warnings.simplefilter("always")
207
+ _ = fold_seq.fold(init_carry, unroll=2)
208
+ _ = fold_seq.fold(init_carry, unroll=True)
209
+ _ = fold_seq.fold(init_carry, unroll=False)
210
+ _ = fold_seq.fold_via(fold_step, unroll=True)(init_carry)
211
+ _ = fold_seq.fold_via(fold_step, unroll=False)(init_carry)
212
+ _ = scan_seq.scan(init_carry, unroll=2)
213
+ _ = scan_seq.scan(init_carry, unroll=True)
214
+ _ = scan_seq.scan(init_carry, unroll=False)
215
+ _ = scan_seq.scan_via(scan_step, unroll=True)(init_carry)
216
+ _ = scan_seq.scan_via(scan_step, unroll=False)(init_carry)
217
+
218
+ assert not caught
219
+
220
+
167
221
  def test_stacked_to_state_dict():
168
222
  class Module(eqx.Module):
169
223
  named: hax.NamedArray
@@ -239,8 +293,18 @@ E = hax.Axis("E", 10)
239
293
  [(E.size,), (Block.size, E.size), (Block.size, E.size)],
240
294
  None,
241
295
  ),
242
- ("simple", ScanCheckpointPolicy(simple=True), [(E.size,), (Block.size, E.size)], None),
243
- ("nested", ScanCheckpointPolicy(simple=True, nested=2), [(E.size,), (2, E.size)], None),
296
+ (
297
+ "simple",
298
+ ScanCheckpointPolicy(simple=True),
299
+ [(E.size,), (Block.size, E.size)],
300
+ None,
301
+ ),
302
+ (
303
+ "nested",
304
+ ScanCheckpointPolicy(simple=True, nested=2),
305
+ [(E.size,), (2, E.size)],
306
+ None,
307
+ ),
244
308
  (
245
309
  "sin_offload",
246
310
  ScanCheckpointPolicy(save_carries=True, offload_block_internals=["sin"]),
@@ -371,6 +435,97 @@ def test_scan_via():
371
435
  assert hax.all(hax.isclose(outs, expected_outs))
372
436
 
373
437
 
438
+ def test_scan_via_with_unroll():
439
+ class Module(eqx.Module):
440
+ w: hax.NamedArray
441
+
442
+ def with_output(self, x):
443
+ out = x + self.w
444
+ return out, 2 * self.w
445
+
446
+ def __call__(self, carry):
447
+ return carry + self.w, 2 * self.w
448
+
449
+ @staticmethod
450
+ def init(named):
451
+ return Module(w=named)
452
+
453
+ Block = hax.Axis("block", 4)
454
+ E = hax.Axis("E", 6)
455
+
456
+ named = hax.random.uniform(jax.random.PRNGKey(0), (Block, E))
457
+ m = Stacked.init(Block, Module)(named=named)
458
+
459
+ x = hax.random.uniform(jax.random.PRNGKey(1), (E,))
460
+
461
+ default_carry, default_outs = m.scan_via(Module.with_output)(x)
462
+ carry, outs = m.scan_via(Module.with_output, unroll=2)(x)
463
+
464
+ assert hax.all(hax.isclose(carry, default_carry))
465
+ assert hax.all(hax.isclose(outs, default_outs))
466
+
467
+ default_carry_direct, default_outs_direct = m.scan(x)
468
+ carry_direct, outs_direct = m.scan(x, unroll=2)
469
+
470
+ assert hax.all(hax.isclose(carry_direct, default_carry_direct))
471
+ assert hax.all(hax.isclose(outs_direct, default_outs_direct))
472
+
473
+
474
+ def test_scan_via_with_bool_unroll(monkeypatch):
475
+ class Module(eqx.Module):
476
+ w: hax.NamedArray
477
+
478
+ def with_output(self, x):
479
+ out = x + self.w
480
+ return out, 2 * self.w
481
+
482
+ def __call__(self, carry):
483
+ return carry + self.w, 2 * self.w
484
+
485
+ @staticmethod
486
+ def init(named):
487
+ return Module(w=named)
488
+
489
+ Block = hax.Axis("block", 4)
490
+ E = hax.Axis("E", 6)
491
+
492
+ named = hax.random.uniform(jax.random.PRNGKey(0), (Block, E))
493
+ m = Stacked.init(Block, Module)(named=named)
494
+
495
+ x = hax.random.uniform(jax.random.PRNGKey(1), (E,))
496
+
497
+ default_carry_via, default_outs_via = m.scan_via(Module.with_output)(x)
498
+ default_carry_direct, default_outs_direct = m.scan(x)
499
+
500
+ import haliax.nn.scan as hnn_scan
501
+
502
+ scan_calls: list[int | None] = []
503
+ original_scan = hnn_scan.haliax.scan
504
+
505
+ def wrapped_scan(*args, **kwargs):
506
+ scan_calls.append(kwargs.get("unroll"))
507
+ return original_scan(*args, **kwargs)
508
+
509
+ monkeypatch.setattr(hnn_scan.haliax, "scan", wrapped_scan)
510
+
511
+ carry_true, outs_true = m.scan_via(Module.with_output, unroll=True)(x)
512
+ carry_false, outs_false = m.scan_via(Module.with_output, unroll=False)(x)
513
+ carry_true_direct, outs_true_direct = m.scan(x, unroll=True)
514
+ carry_false_direct, outs_false_direct = m.scan(x, unroll=False)
515
+
516
+ assert scan_calls == [True, False, True, False]
517
+
518
+ assert hax.all(hax.isclose(carry_true, default_carry_via))
519
+ assert hax.all(hax.isclose(outs_true, default_outs_via))
520
+ assert hax.all(hax.isclose(carry_false, default_carry_via))
521
+ assert hax.all(hax.isclose(outs_false, default_outs_via))
522
+
523
+ assert hax.all(hax.isclose(carry_true_direct, default_carry_direct))
524
+ assert hax.all(hax.isclose(outs_true_direct, default_outs_direct))
525
+ assert hax.all(hax.isclose(carry_false_direct, default_carry_direct))
526
+ assert hax.all(hax.isclose(outs_false_direct, default_outs_direct))
527
+
528
+
374
529
  def test_scan_via_multi_args():
375
530
  class Module(eqx.Module):
376
531
  w: hax.NamedArray
@@ -513,6 +668,88 @@ def test_fold_via_multi_args():
513
668
  assert hax.all(hax.isclose(result, expected))
514
669
 
515
670
 
671
+ def test_fold_via_with_unroll():
672
+ class Module(eqx.Module):
673
+ w: hax.NamedArray
674
+
675
+ def intermediate(self, x):
676
+ return x + 2 * self.w
677
+
678
+ def __call__(self, carry):
679
+ return carry + self.w
680
+
681
+ @staticmethod
682
+ def init(named):
683
+ return Module(w=named)
684
+
685
+ Block = hax.Axis("block", 3)
686
+ E = hax.Axis("E", 5)
687
+
688
+ named = hax.random.uniform(jax.random.PRNGKey(0), (Block, E))
689
+ m = Stacked.init(Block, Module)(named=named)
690
+
691
+ x = hax.random.uniform(jax.random.PRNGKey(1), (E,))
692
+
693
+ default_result = m.fold_via(Module.intermediate)(x)
694
+ result_unroll = m.fold_via(Module.intermediate, unroll=2)(x)
695
+
696
+ assert hax.all(hax.isclose(result_unroll, default_result))
697
+
698
+ default_fold = m.fold(x)
699
+ fold_unroll = m.fold(x, unroll=2)
700
+
701
+ assert hax.all(hax.isclose(fold_unroll, default_fold))
702
+
703
+
704
+ def test_fold_with_bool_unroll(monkeypatch):
705
+ class Module(eqx.Module):
706
+ w: hax.NamedArray
707
+
708
+ def __call__(self, x):
709
+ return x + self.w
710
+
711
+ def intermediate(self, x):
712
+ return x + 2 * self.w
713
+
714
+ @staticmethod
715
+ def init(named):
716
+ return Module(w=named)
717
+
718
+ Block = hax.Axis("block", 3)
719
+ E = hax.Axis("E", 5)
720
+
721
+ named = hax.random.uniform(jax.random.PRNGKey(0), (Block, E))
722
+ m = Stacked.init(Block, Module)(named=named)
723
+
724
+ x = hax.random.uniform(jax.random.PRNGKey(1), (E,))
725
+
726
+ default_fold = m.fold(x)
727
+ default_fold_via = m.fold_via(Module.intermediate)(x)
728
+
729
+ import haliax.nn.scan as hnn_scan
730
+
731
+ fold_calls: list[int | None] = []
732
+ original_fold = hnn_scan.haliax.fold
733
+
734
+ def wrapped_fold(*args, **kwargs):
735
+ fold_calls.append(kwargs.get("unroll"))
736
+ return original_fold(*args, **kwargs)
737
+
738
+ monkeypatch.setattr(hnn_scan.haliax, "fold", wrapped_fold)
739
+
740
+ fold_true = m.fold(x, unroll=True)
741
+ fold_false = m.fold(x, unroll=False)
742
+ via_true = m.fold_via(Module.intermediate, unroll=True)(x)
743
+ via_false = m.fold_via(Module.intermediate, unroll=False)(x)
744
+
745
+ assert fold_calls == [True, False, True, False]
746
+
747
+ assert hax.all(hax.isclose(fold_true, default_fold))
748
+ assert hax.all(hax.isclose(fold_false, default_fold))
749
+ assert hax.all(hax.isclose(via_true, default_fold_via))
750
+ assert hax.all(hax.isclose(via_false, default_fold_via))
751
+
752
+
516
753
  def test_fold_via_static_args():
517
754
  class Module(eqx.Module):
518
755
  w: hax.NamedArray
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes