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.
- {haliax-1.4.dev342 → haliax-1.4.dev343}/PKG-INFO +1 -1
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/api.md +1 -1
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/nn.md +1 -1
- haliax-1.4.dev343/docs/scan.md +456 -0
- haliax-1.4.dev343/docs/vmap.md +5 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/mkdocs.yml +3 -1
- haliax-1.4.dev343/src/haliax/__about__.py +1 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/__init__.py +4 -2
- haliax-1.4.dev343/src/haliax/_src/scan.py +613 -0
- haliax-1.4.dev343/src/haliax/hof.py +165 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/jax_utils.py +70 -19
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/__init__.py +2 -1
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/scan.py +129 -61
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/random.py +0 -1
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_hof.py +21 -4
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_scan.py +82 -3
- haliax-1.4.dev342/docs/hof.md +0 -16
- haliax-1.4.dev342/docs/stacked.md +0 -203
- haliax-1.4.dev342/src/haliax/__about__.py +0 -1
- haliax-1.4.dev342/src/haliax/hof.py +0 -487
- {haliax-1.4.dev342 → haliax-1.4.dev343}/.coveragerc +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/.flake8 +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/.github/workflows/publish_dev.yaml +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/.github/workflows/run_pre_commit.yaml +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/.github/workflows/run_quick_levanter_tests.yaml +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/.github/workflows/run_tests.yaml +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/.gitignore +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/.pre-commit-config.yaml +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/.readthedocs.yaml +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/CONTRIBUTING.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/LICENSE +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/README.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/broadcasting.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/cheatsheet.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/css/material.css +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/css/mkdocstrings.css +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/faq.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/data_parallel_mesh.png +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/data_parallel_mesh_replicated.png +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_1d.png +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_1d_zero.png +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_2d.png +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_2d_batch_partitioned.png +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_2d_data_replicated.png +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_2d_data_replicated_mlp_partitioned.png +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_2d_intermediate_fully_partitioned.png +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/figures/device_mesh_2d_zero.png +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/fp8.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/index.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/indexing.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/matmul.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/partitioning.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/rearrange.ipynb +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/rearrange.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/requirements.txt +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/state-dict.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/docs/tutorial.md +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/pyproject.toml +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/__init__.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/compile_utils.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/dot.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/einsum.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/fp8.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/parsing.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/rearrange.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/state_dict.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/_src/util.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/axis.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/core.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/debug.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/activations.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/attention.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/conv.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/dropout.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/embedding.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/linear.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/loss.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/mlp.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/normalization.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/nn/pool.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/ops.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/partitioning.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/quantization.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/specialized_fns.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/state_dict.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/tree_util.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/types.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/util.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/src/haliax/wrap.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/core_test.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_attention.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_axis.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_conv.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_debug.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_dot.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_einsum.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_fp8.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_int8.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_nn.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_ops.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_parsing.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_partitioning.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_pool.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_random.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_rearrange.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_specialized_fns.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_state_dict.py +0 -0
- {haliax-1.4.dev342 → haliax-1.4.dev343}/tests/test_tree_util.py +0 -0
- {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.
|
|
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.
|
|
298
|
+
See also [haliax.nn.ScanCheckpointPolicy][].
|
|
299
299
|
|
|
300
300
|
::: haliax.tree_checkpoint_name
|
|
301
301
|
|
|
@@ -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
|
|
@@ -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
|
|
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
|
]
|