haliax 1.4.dev342__tar.gz → 1.4.dev343__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 (109) hide show
  1. {haliax-1.4.dev342 → haliax-1.4.dev343}/PKG-INFO +1 -1
  2. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/api.md +1 -1
  3. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/nn.md +1 -1
  4. haliax-1.4.dev343/docs/scan.md +456 -0
  5. haliax-1.4.dev343/docs/vmap.md +5 -0
  6. {haliax-1.4.dev342 → haliax-1.4.dev343}/mkdocs.yml +3 -1
  7. haliax-1.4.dev343/src/haliax/__about__.py +1 -0
  8. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/__init__.py +4 -2
  9. haliax-1.4.dev343/src/haliax/_src/scan.py +613 -0
  10. haliax-1.4.dev343/src/haliax/hof.py +165 -0
  11. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/jax_utils.py +70 -19
  12. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/__init__.py +2 -1
  13. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/scan.py +129 -61
  14. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/random.py +0 -1
  15. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_hof.py +21 -4
  16. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_scan.py +82 -3
  17. haliax-1.4.dev342/docs/hof.md +0 -16
  18. haliax-1.4.dev342/docs/stacked.md +0 -203
  19. haliax-1.4.dev342/src/haliax/__about__.py +0 -1
  20. haliax-1.4.dev342/src/haliax/hof.py +0 -487
  21. {haliax-1.4.dev342 → haliax-1.4.dev343}/.coveragerc +0 -0
  22. {haliax-1.4.dev342 → haliax-1.4.dev343}/.flake8 +0 -0
  23. {haliax-1.4.dev342 → haliax-1.4.dev343}/.github/workflows/publish_dev.yaml +0 -0
  24. {haliax-1.4.dev342 → haliax-1.4.dev343}/.github/workflows/run_pre_commit.yaml +0 -0
  25. {haliax-1.4.dev342 → haliax-1.4.dev343}/.github/workflows/run_quick_levanter_tests.yaml +0 -0
  26. {haliax-1.4.dev342 → haliax-1.4.dev343}/.github/workflows/run_tests.yaml +0 -0
  27. {haliax-1.4.dev342 → haliax-1.4.dev343}/.gitignore +0 -0
  28. {haliax-1.4.dev342 → haliax-1.4.dev343}/.pre-commit-config.yaml +0 -0
  29. {haliax-1.4.dev342 → haliax-1.4.dev343}/.readthedocs.yaml +0 -0
  30. {haliax-1.4.dev342 → haliax-1.4.dev343}/CONTRIBUTING.md +0 -0
  31. {haliax-1.4.dev342 → haliax-1.4.dev343}/LICENSE +0 -0
  32. {haliax-1.4.dev342 → haliax-1.4.dev343}/README.md +0 -0
  33. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/broadcasting.md +0 -0
  34. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/cheatsheet.md +0 -0
  35. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/css/material.css +0 -0
  36. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/css/mkdocstrings.css +0 -0
  37. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/faq.md +0 -0
  38. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/data_parallel_mesh.png +0 -0
  39. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/data_parallel_mesh_replicated.png +0 -0
  40. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_1d.png +0 -0
  41. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_1d_zero.png +0 -0
  42. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_2d.png +0 -0
  43. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_2d_batch_partitioned.png +0 -0
  44. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_2d_data_replicated.png +0 -0
  45. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_2d_data_replicated_mlp_partitioned.png +0 -0
  46. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_2d_intermediate_fully_partitioned.png +0 -0
  47. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_2d_zero.png +0 -0
  48. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/fp8.md +0 -0
  49. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/index.md +0 -0
  50. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/indexing.md +0 -0
  51. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/matmul.md +0 -0
  52. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/partitioning.md +0 -0
  53. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/rearrange.ipynb +0 -0
  54. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/rearrange.md +0 -0
  55. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/requirements.txt +0 -0
  56. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/state-dict.md +0 -0
  57. {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/tutorial.md +0 -0
  58. {haliax-1.4.dev342 → haliax-1.4.dev343}/pyproject.toml +0 -0
  59. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/__init__.py +0 -0
  60. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/compile_utils.py +0 -0
  61. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/dot.py +0 -0
  62. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/einsum.py +0 -0
  63. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/fp8.py +0 -0
  64. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/parsing.py +0 -0
  65. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/rearrange.py +0 -0
  66. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/state_dict.py +0 -0
  67. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/util.py +0 -0
  68. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/axis.py +0 -0
  69. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/core.py +0 -0
  70. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/debug.py +0 -0
  71. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/activations.py +0 -0
  72. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/attention.py +0 -0
  73. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/conv.py +0 -0
  74. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/dropout.py +0 -0
  75. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/embedding.py +0 -0
  76. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/linear.py +0 -0
  77. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/loss.py +0 -0
  78. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/mlp.py +0 -0
  79. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/normalization.py +0 -0
  80. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/pool.py +0 -0
  81. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/ops.py +0 -0
  82. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/partitioning.py +0 -0
  83. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/quantization.py +0 -0
  84. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/specialized_fns.py +0 -0
  85. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/state_dict.py +0 -0
  86. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/tree_util.py +0 -0
  87. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/types.py +0 -0
  88. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/util.py +0 -0
  89. {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/wrap.py +0 -0
  90. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/core_test.py +0 -0
  91. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_attention.py +0 -0
  92. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_axis.py +0 -0
  93. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_conv.py +0 -0
  94. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_debug.py +0 -0
  95. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_dot.py +0 -0
  96. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_einsum.py +0 -0
  97. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_fp8.py +0 -0
  98. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_int8.py +0 -0
  99. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_nn.py +0 -0
  100. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_ops.py +0 -0
  101. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_parsing.py +0 -0
  102. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_partitioning.py +0 -0
  103. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_pool.py +0 -0
  104. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_random.py +0 -0
  105. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_rearrange.py +0 -0
  106. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_specialized_fns.py +0 -0
  107. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_state_dict.py +0 -0
  108. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_tree_util.py +0 -0
  109. {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haliax
3
- Version: 1.4.dev342
3
+ Version: 1.4.dev343
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/
@@ -295,7 +295,7 @@ See also the section on [Partitioning](partitioning.md).
295
295
  Haliax mainly just defers to JAX and [equinox.filter_checkpoint][] for gradient checkpointing. However,
296
296
  we provide a few utilities to make it easier to use.
297
297
 
298
- See also [haliax.nn.StackedCheckpointPolicy][].
298
+ See also [haliax.nn.ScanCheckpointPolicy][].
299
299
 
300
300
  ::: haliax.tree_checkpoint_name
301
301
 
@@ -29,7 +29,7 @@ of the same module (e.g. transformer blocks), which is a common pattern in deep
29
29
 
30
30
  ### Stacked
31
31
 
32
- See the full documentation of [Stacked](stacked.md).
32
+ See the full documentation of [Stacked](scan.md#stacked).
33
33
 
34
34
  ### Convolution
35
35
 
@@ -0,0 +1,456 @@
1
+ # Scan and Fold
2
+
3
+ A common pattern in deep learning is to apply a sequence of layers to an input, feeding the output from one
4
+ layer to the next. In JAX, this is often done with [jax.lax.scan][].
5
+
6
+ As the docs say, scan does an operation sort of like this in Python:
7
+
8
+ ```python
9
+ def scan(f, init, xs, length=None):
10
+ if xs is None:
11
+ xs = [None] * length
12
+ carry = init
13
+ ys = []
14
+ for x in xs:
15
+ carry, y = f(carry, x)
16
+ ys.append(y)
17
+ return carry, np.stack(ys)
18
+ ```
19
+
20
+ Haliax provides two versions of this pattern: [haliax.fold][] and [haliax.scan][]. haliax.scan works much like JAX's scan,
21
+ except it is curried and it works with NamedArrays. haliax.fold is a more restricted version of scan that is easier to
22
+ use if you don't need the full generality of scan. (It works with functions that only return `carry`, not `carry, output`.)
23
+
24
+ ## `haliax.scan`
25
+
26
+ Unlike JAX's scan, Haliax's scan is curried - it takes the function and configuration first, then the initial carry and scan arguments as a separate call: `scan(f, axis)(init, xs)`.
27
+
28
+ ### Key Features
29
+ * Works with named axes using [haliax.NamedArray][]
30
+ * Supports gradient checkpointing for memory efficiency, including several advanced checkpointing policies
31
+ * Integrates with [equinox.Module][] for building neural networks
32
+
33
+ ### Basic Example
34
+
35
+ Here's a practical example of using `haliax.scan` to sum values along an axis while keeping track of intermediates:
36
+
37
+ ```python
38
+ Time = Axis("Time", 100)
39
+ Features = Axis("Features", 16)
40
+
41
+ # Create time series data
42
+ data = hax.random.normal(PRNGKey(0), (Time, Features))
43
+
44
+ def running_stats(state, x):
45
+ count, mean, min_val, max_val = state
46
+ count += 1
47
+ # this is a common pattern to improve the robustness of the mean calculation
48
+ delta = x - mean
49
+ mean = mean + delta / count
50
+ min_val = hax.minimum(min_val, x)
51
+ max_val = hax.maximum(max_val, x)
52
+
53
+ return (count, mean, min_val, max_val), mean
54
+
55
+
56
+ # Initialize state: (count, mean, min, max)
57
+ init_state = (
58
+ 0.0,
59
+ hax.zeros((Features,)),
60
+ hax.full((Features,), float('inf')),
61
+ hax.full((Features,), float('-inf'))
62
+ )
63
+
64
+ final_state, running_means = hax.scan(running_stats, Time)(init_state, data)
65
+ ```
66
+
67
+ Note that:
68
+
69
+ * `scan` is curried: `scan(f, axis)(init, xs)`
70
+ * `running_stats` returns a tuple of `(carry, output)`, which is why we have two return values from `scan`
71
+ * the running_means will have shape `(Time, Features)`, with the mean at each time step
72
+ * the final_state will have the same shape as the initial state
73
+
74
+
75
+ ### Using `scan` with no inputs
76
+ You can also use scan without any inputs if you want:
77
+
78
+ ```python
79
+ Time = Axis("Time", 100)
80
+ Features = Axis("Features", 16)
81
+
82
+ def simulate_brownian_motion(state, _):
83
+ return state + hax.random.normal(PRNGKey(0), Features), state
84
+
85
+ init_state = hax.zeros((Features,))
86
+
87
+ final_state, path = hax.scan(simulate_brownian_motion, Time)(init_state, None)
88
+ ```
89
+
90
+ More commonly, you might use this for an RNN or Transformer model. (See [haliax.nn.Stacked][].)
91
+
92
+ ## `haliax.fold`
93
+
94
+ `haliax.fold` is a simpler version of `haliax.scan` that is easier to use when you don't need the full generality of `scan`.
95
+ Specifically, `fold` is for functions that only return a `carry`, not a `carry, output`.
96
+
97
+ Morally, `fold` is like this Python code:
98
+
99
+ ```python
100
+ def fold(f, init, xs):
101
+ carry = init
102
+ for x in xs:
103
+ carry = f(carry, x)
104
+ return carry
105
+ ```
106
+
107
+ ### Basic Example
108
+
109
+ Same example, but we only care about the final state:
110
+
111
+ ```python
112
+ Time = Axis("Time", 100)
113
+ Features = Axis("Features", 16)
114
+
115
+ # Create time series data
116
+ data = hax.random.normal(PRNGKey(0), (Time, Features))
117
+
118
+ def running_stats(state, x):
119
+ count, mean, min_val, max_val = state
120
+ count += 1
121
+ # this is a common pattern to improve the robustness of the mean calculation
122
+ delta = x - mean
123
+ mean = mean + delta / count
124
+ min_val = hax.minimum(min_val, x)
125
+ max_val = hax.maximum(max_val, x)
126
+
127
+ return (count, mean, min_val, max_val)
128
+
129
+
130
+ # Initialize state: (count, mean, min, max)
131
+ init_state = (
132
+ 0.0,
133
+ hax.zeros((Features,)),
134
+ hax.full((Features,), float('inf')),
135
+ hax.full((Features,), float('-inf'))
136
+ )
137
+
138
+ final_state = hax.fold(running_stats, Time)(init_state, data)
139
+ ```
140
+
141
+ ## `haliax.map`
142
+
143
+ `haliax.map` is a convenience function that applies a function to each element of an axis. It is similar
144
+ to [jax.lax.map][] but works with NamedArrays, providing a similar interface to `haliax.scan` and `haliax.fold`.
145
+
146
+ ```python
147
+
148
+ Time = Axis("Time", 100)
149
+
150
+ data = hax.random.normal(PRNGKey(0), (Time,))
151
+
152
+ def my_fn(x):
153
+ return x + 1
154
+
155
+ result = hax.map(my_fn, Time)(data)
156
+ ```
157
+
158
+ You should generally prefer to use [haliax.vmap][] instead of `haliax.map`, but it's there if you need it.
159
+ (It uses less memory than `haliax.vmap` but is slower.)
160
+
161
+
162
+ ## Gradient Checkpointing / Rematerialization
163
+
164
+ Both `haliax.scan` and `haliax.fold` support gradient checkpointing, which can be useful for deep models.
165
+ Typically, you'd use this as part of [haliax.nn.Stacked][] or [haliax.nn.BlockSeq][] but you can also use it directly.
166
+
167
+ Gradient checkpointing is a technique for reducing memory usage during backpropagation by recomputing some
168
+ intermediate values during the backward pass. This can be useful when you have a deep model with many layers.
169
+
170
+ ### TL;DR Guidance
171
+
172
+ Here is some guidance on when to use gradient checkpointing:
173
+
174
+ * Use `remat=False` if you need to reduce computation and have lots of memory. This is the default in [haliax.scan][].
175
+ * Use `remat=True` for most models. It's usually good enough. This is the default in [haliax.nn.Stacked][].
176
+ * Use `remat="nested"` if you need to reduce memory usage.
177
+ * Use `save_block_internals` sparingly, but it is your best tool for trading increased memory usage for reduced computation
178
+ if you need something between `remat=True` and `remat=False`.
179
+ * Use `save_carries="offload"` if you need to reduce memory usage at the cost of recomputation. This is a new feature
180
+ in JAX and doesn't seem to reliably work yet.
181
+
182
+
183
+ ### Simple Checkpointing
184
+
185
+ In the simplest case, you can enable a usually-good-enough checkpointing policy by passing `remat=True`:
186
+
187
+ ```python
188
+ final_state = hax.fold(running_stats, Time, remat=True)(init_state, data)
189
+ ```
190
+
191
+ ("remat" is short for "rematerialization", which is another term for gradient checkpointing.)
192
+
193
+ This will preserve the intermediate "carries" and other inputs the fold function needs, while rematerializing
194
+ (i.e. recomputing) the internal state of each block (i.e. call to the running_stats function) as needed during
195
+ backpropagation.
196
+
197
+
198
+ ### Nested Scan
199
+
200
+ Simple checkpointing requires `O(N)` memory where $N$ is the number of blocks. A nested scan lets you reduce
201
+ this to `O(sqrt(N))` memory, at the cost of a bit more computation. You can enable this by passing `remat="nested"`:
202
+
203
+ ```python
204
+ final_state = hax.fold(running_stats, Time, remat="nested")(init_state, data)
205
+ ```
206
+
207
+ This will break the scan into a double loop, where the outer loop has `sqrt(N)` blocks and the inner loop has
208
+ `sqrt(N)` blocks (with appropriate rounding).
209
+
210
+ Functionally, it does something like:
211
+
212
+ ```
213
+ outer_size = int(sqrt(N)) # ensuring outer_size divides N
214
+ blocks = haliax.rearrange("block -> (outer inner)", blocks, outer=outer_size)
215
+
216
+ state = init_state
217
+ for o in range(outer_size):
218
+ inner_blocks = blocks["outer", o]
219
+
220
+ for i in range(inner_size):
221
+ state = f(state, inner_blocks["inner", i])
222
+
223
+ # not real jax
224
+ state = save_for_backward(state)
225
+ ```
226
+
227
+ where we save only the carries from the outer loop, and fully rematerialize the inner loop.
228
+
229
+ If `C` is the amount of memory needed for the carry, and `N` is the number of blocks, then the memory usage
230
+ of the nested scan is `2 * C * sqrt(N)`. In addition, you need enough memory to do backward in one block.
231
+
232
+ In practice, nested scan is about 20% slower than simple checkpointing (for Transformers), but uses much less memory.
233
+
234
+ #### Advanced: customizing the number of blocks
235
+
236
+ You can also customize the number of blocks in the outer loop by using a policy:
237
+
238
+ ```python
239
+ policy = ScanCheckpointPolicy(nested=4) # 4 outer blocks
240
+ ```
241
+
242
+ Note that by itself this doesn't help you at all except potentially requiring more memory. You can potentially
243
+ combine it with other policy options to make things faster though.
244
+
245
+
246
+ ### Custom Checkpointing Policies
247
+
248
+ If you need more control over the checkpointing policy, you can pass a [haliax.nn.ScanCheckpointPolicy][] object to
249
+ the `scan` or `fold` call:
250
+
251
+ ```python
252
+ policy = ScanCheckpointPolicy(
253
+ save_carries=True, # default
254
+ save_inputs=True, # default
255
+ save_block_internals=False, # default
256
+ )
257
+ ```
258
+
259
+ ### Saving Block-Internal Values
260
+
261
+ "`internals`" refers to the internal computation of the block. If you set `save_block_internals=True`, then
262
+ all internals of every block will be saved. This can be expensive and mostly negates the benefits of checkpointing.
263
+
264
+ Instead you can choose which internals to save by passing a list of strings to `save_block_internals`:
265
+
266
+ ```python
267
+ def my_complex_fn(state, x):
268
+ y = x + state
269
+ y = hax.sin(y) + x
270
+ y = hax.tree_checkpoint_name(y, "y")
271
+ y = hax.cos(y) + x
272
+ y = hax.tree_checkpoint_name(y, "z")
273
+ return y
274
+
275
+ policy = ScanCheckpointPolicy(save_carries=True, save_block_internals=["y"])
276
+
277
+ final_state = hax.fold(my_complex_fn, Time, remat=policy)(init_state, data)
278
+
279
+ ```
280
+
281
+ With this policy, the output of the `sin` function will be saved during the forward pass.
282
+
283
+ This will save an extra `sin` computation in the backward pass, adding $`O(N * Pos * Hidden)`$ memory usage,
284
+ which is double that required by the default policy, but it reduces the amount of recomputation needed.
285
+ (It's probably not worth it in this case.)
286
+
287
+ ### Offloading Checkpointed Values
288
+
289
+ Both `save_carries` and `save_inputs` can either be a boolean or the string "offload". If "offload", then the
290
+ checkpointed values will be offloaded to the host during the forward pass, and reloaded during the backward pass.
291
+
292
+
293
+ ### Summary of String and Boolean Aliases
294
+
295
+ * `remat=True` is the same as `remat=ScanCheckpointPolicy(save_carries=True, save_inputs=True)`
296
+ * `remat="full"` is the same as `remat=True`
297
+ * `remat=False` is the same as `remat=ScanCheckpointPolicy(disable=True)`
298
+ * `remat="nested"` is the same as `remat=ScanCheckpointPolicy(nested=True)`
299
+ * `remat="offload"` is the same as `remat=ScanCheckpointPolicy(save_carries="offload", save_inputs="offload")`
300
+ * `remat="save_all"` is the same as `remat=ScanCheckpointPolicy(save_carries=True, save_inputs=True, save_block_internals=True)`,
301
+ which should be the same as not using remat at all...
302
+
303
+
304
+ ### Memory and Computation Tradeoffs
305
+
306
+ Let `N` be the number of blocks, `C` be the memory needed for the carry, and `I` be the internal memory needed
307
+ for each block. Let F be the amount of computation needed for each block. Constants are added for a bit more precision
308
+ but are not exact. This is assuming that backward requires ~twice the flops as forward, which is roughly right for
309
+ Transformers.
310
+
311
+ | Policy | Memory Usage | Computation |
312
+ |------------------|--------------------------|----------------|
313
+ | `remat=False` | `O(N * C + N * I)` | `O(3 * N * F)` |
314
+ | `remat=True` | `O(N * C + I)` | `O(4 * N * F)` |
315
+ | `remat="nested"` | `O(2 * sqrt(N) * C + I)` | `O(5 * N * F)` |
316
+
317
+
318
+ (Which shows why nested scan is about 20% slower than simple checkpointing. The math says 25% but it's more like 20% in
319
+ practice.) Any nested remat will require `5 * N * F` computation, which is about 25% more than simple remat.
320
+
321
+
322
+ ## Module Stacks
323
+
324
+ A core pattern for larger models in JAX is the "scan-over-layers" pattern, where you have a sequence of layers
325
+ that get stacked together, and you use [jax.lax.scan][] or [haliax.fold][] or [haliax.scan][] to apply them to a
326
+ sequence of inputs. In Haliax, layers are represented as [equinox.nn.Module][]s, and the [haliax.nn.Stacked][] module
327
+ provides a way to create a sequence of layers that can be applied to a sequence of inputs that implements the
328
+ scan-over-layers pattern.
329
+
330
+ ### Stacked
331
+
332
+ [haliax.nn.Stacked][] lets you apply a layer sequentially to an input, scanning over a "Layers" axis. For instance,
333
+ a Transformer might use a Stacked for its Transformer blocks:
334
+
335
+
336
+ ```python
337
+ class TransformerBlock(eqx.Module):
338
+
339
+ def __init__(self, config: TransformerConfig, layer_index, *, key):
340
+ attn_key, mlp_key = jax.random.split(key)
341
+ self.attention = Attention.init(config, key=attn_key)
342
+ self.mlp = MLP.init(config, key=mlp_key)
343
+ self.ln1 = LayerNorm.init(config.Hidden)
344
+ self.ln2 = LayerNorm.init(config.Hidden)
345
+ self.layer_index = layer_index
346
+
347
+ def __call__(self, x):
348
+ y = self.attention(self.ln1(x))
349
+ x = x + y
350
+ y = self.mlp(self.ln2(x))
351
+ return x + y
352
+
353
+ class Transformer(eqx.Module):
354
+ def __init__(self, config: TransformerConfig):
355
+ self.blocks = Stacked.init(Layers, TransformerBlock)(
356
+ config, # static configuration
357
+ scale=hax.arange(Layers), # dynamic configuration. Each layer gets a scalar scale value [0, 1, 2, ...]
358
+ key=jax.random.split(key, Layers), # dynamic configuration. Each layer gets a different key
359
+ )
360
+
361
+ def __call__(self, x: NamedArray) -> NamedArray:
362
+ # morally the equivalent of:
363
+ # for block in self.blocks:
364
+ # x = block(x)
365
+ # Except that it works better with JAX compile times.
366
+
367
+ return self.blocks.fold(x)
368
+ ```
369
+
370
+ You can think of [haliax.nn.Stacked][] as an analog to PyTorch's
371
+ [torch.nn.Sequential](https://pytorch.org/docs/stable/generated/torch.nn.Sequential.html), except that
372
+ every layer in the sequence must have exactly the same shape and configuration.
373
+
374
+ Internally, a Stacked is a single copy of the module, except that every NamedArray inside that module
375
+ has a Block axis prepended (as though they were stacked with [haliax.stack][]). Similarly, every JAX array
376
+ inside the module has its first axis prepended with an axis of the same size as the Block axis, as though
377
+ they were stacked with [jax.numpy.stack][].
378
+
379
+ When you call the Stacked, it scans over the Block axis, applying the module to each element of the Block.
380
+
381
+ #### Creating a Stacked
382
+
383
+ To create a Stacked, we provide `Stacked.init`, which takes a "Layers" [haliax.Axis][] and another Module as
384
+ well as args and kwargs for that module. The Layer is the axis that the Stacked will scan over, and the `args`
385
+ and `kwargs` are implicitly vmapped over the Layers.
386
+
387
+ For instance, to create a stack of GPT2 blocks, you might do:
388
+
389
+ ```python
390
+ import jax.random
391
+
392
+ blocks = Stacked.init(Layers, Gpt2Block)(
393
+ config, # static configuration
394
+ scale=hax.arange(Layers), # dynamic configuration. Each layer gets a scalar scale value [0, 1, 2, ...]
395
+ key=jax.random.split(key, Layers.size), # dynamic configuration. Each layer gets a different key
396
+ )
397
+ ```
398
+
399
+ Any NamedArray passed to the Stacked init will have its Layers axis (if present) vmapped over. Any
400
+ JAX array will have its first axis vmapped over.
401
+
402
+
403
+ #### Fold Blocks vs Scan Blocks
404
+
405
+ The Stacked module provides two ways to apply the layers: `fold` and `scan`. A fold is the moral equivalent of this for loop:
406
+
407
+ ```python
408
+ for block in self.blocks:
409
+ x = block(x)
410
+ ```
411
+
412
+ while a scan is the moral equivalent of this for loop:
413
+
414
+ ```python
415
+ out = []
416
+ for block in self.blocks:
417
+ x, y = block(x)
418
+ out.append(y)
419
+
420
+ return x, stack(out)
421
+ ```
422
+
423
+ Blocks can be coded to either support fold or scan, but not both.
424
+ A "fold Block" should have the signature `def __call__(self, x: Carry) -> Carry`,
425
+ while a "scan Block" should have the signature `def __call__(self, x: Carry) -> Tuple[Carry, Output]`.
426
+
427
+ (See also [jax.lax.scan][], [haliax.fold][], and [haliax.scan][].)
428
+
429
+ #### Requirements for Stacked Blocks
430
+
431
+ As we said above, the Stacked module requires that all the layers have the same shape and configuration.
432
+
433
+ A further constraint is that the elements of the stack must have the same Python control flow. This is the usual
434
+ constraint imposed on jit-compiled functions in JAX. All control flow must use `jax.lax` primitives like
435
+ [jax.lax.cond][], [jax.lax.while_loop][], and [jax.lax.scan][]. You can't use Python control flow like `if` or `for`
436
+ except for static control flow that is the same for all elements of the stack.
437
+
438
+ ### BlockSeq and BlockFoldable
439
+
440
+ We also provide a way to create a sequence of layers that can be applied to a sequence of inputs that implements the
441
+ same interface as [haliax.nn.Stacked][], but with a different implementation. This is the [haliax.nn.BlockSeq][] module.
442
+ BlockSeq implements those for loops directly, rather than using [haliax.fold][] or [haliax.scan][].
443
+
444
+ [haliax.nn.scan.BlockFoldable][] is an interface that both [haliax.nn.Stacked][] and [haliax.nn.BlockSeq][] implement.
445
+
446
+ ## API
447
+
448
+ ::: haliax.fold
449
+ ::: haliax.scan
450
+ ::: haliax.map
451
+ ::: haliax.ScanCheckpointPolicy
452
+
453
+ ### Modules
454
+ ::: haliax.nn.Stacked
455
+ ::: haliax.nn.BlockSeq
456
+ ::: haliax.nn.scan.BlockFoldable
@@ -0,0 +1,5 @@
1
+ ## Vectorization
2
+
3
+ (This is a work in progress. Please contact dlwh for more information.)
4
+
5
+ ::: haliax.vmap
@@ -90,9 +90,11 @@ nav:
90
90
  - Indexing and Slicing: 'indexing.md'
91
91
  - Rearrange: 'rearrange.md'
92
92
  - Matrix Multiplication: 'matmul.md'
93
+ - Higher Order Functions:
94
+ - Scan and Fold: 'scan.md'
95
+ - Vectorization: 'vmap.md'
93
96
  - Neural Networks: 'nn.md'
94
97
  - Partitioning: 'partitioning.md'
95
- - Higher Order Functions: 'hof.md'
96
98
  - FP8: 'fp8.md'
97
99
  - Serialization: 'state-dict.md'
98
100
  - API Reference: 'api.md'
@@ -0,0 +1 @@
1
+ __version__ = "1.4.dev343"
@@ -21,6 +21,7 @@ import haliax.util as util
21
21
  from ._src.dot import dot
22
22
  from ._src.einsum import einsum
23
23
  from ._src.rearrange import rearrange
24
+ from ._src.scan import ScanCheckpointPolicy
24
25
  from .axis import (
25
26
  Axis,
26
27
  AxisSelection,
@@ -61,7 +62,7 @@ from .core import (
61
62
  updated_slice,
62
63
  )
63
64
  from .hof import fold, map, scan, vmap
64
- from .jax_utils import filter_checkpoint
65
+ from .jax_utils import tree_checkpoint_name
65
66
  from .ops import clip, isclose, pad_left, trace, tril, triu, where
66
67
  from .partitioning import auto_sharded, axis_mapping, fsdp, named_jit, shard, shard_with_axis_mapping
67
68
  from .specialized_fns import top_k
@@ -887,7 +888,6 @@ def true_divide(x1: NamedOrNumeric, x2: NamedOrNumeric, /) -> NamedOrNumeric:
887
888
  # deprecated name
888
889
  concat_axis_specs = concat_axes
889
890
 
890
-
891
891
  __all__ = [
892
892
  "debug",
893
893
  "random",
@@ -1071,4 +1071,6 @@ __all__ = [
1071
1071
  "ravel",
1072
1072
  "flatten",
1073
1073
  "is_named_array",
1074
+ "tree_checkpoint_name",
1075
+ "ScanCheckpointPolicy",
1074
1076
  ]