haliax 1.4.dev340__tar.gz → 1.4.dev342__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 (106) hide show
  1. {haliax-1.4.dev340 → haliax-1.4.dev342}/.github/workflows/run_quick_levanter_tests.yaml +5 -3
  2. {haliax-1.4.dev340 → haliax-1.4.dev342}/PKG-INFO +1 -1
  3. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/nn.md +3 -0
  4. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/state-dict.md +64 -2
  5. haliax-1.4.dev342/src/haliax/__about__.py +1 -0
  6. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/_src/state_dict.py +66 -80
  7. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/nn/__init__.py +2 -4
  8. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/nn/linear.py +44 -1
  9. haliax-1.4.dev342/src/haliax/nn/normalization.py +169 -0
  10. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/state_dict.py +9 -9
  11. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/tree_util.py +2 -9
  12. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_state_dict.py +52 -4
  13. haliax-1.4.dev340/src/haliax/__about__.py +0 -1
  14. haliax-1.4.dev340/src/haliax/nn/normalization.py +0 -87
  15. {haliax-1.4.dev340 → haliax-1.4.dev342}/.coveragerc +0 -0
  16. {haliax-1.4.dev340 → haliax-1.4.dev342}/.flake8 +0 -0
  17. {haliax-1.4.dev340 → haliax-1.4.dev342}/.github/workflows/publish_dev.yaml +0 -0
  18. {haliax-1.4.dev340 → haliax-1.4.dev342}/.github/workflows/run_pre_commit.yaml +0 -0
  19. {haliax-1.4.dev340 → haliax-1.4.dev342}/.github/workflows/run_tests.yaml +0 -0
  20. {haliax-1.4.dev340 → haliax-1.4.dev342}/.gitignore +0 -0
  21. {haliax-1.4.dev340 → haliax-1.4.dev342}/.pre-commit-config.yaml +0 -0
  22. {haliax-1.4.dev340 → haliax-1.4.dev342}/.readthedocs.yaml +0 -0
  23. {haliax-1.4.dev340 → haliax-1.4.dev342}/CONTRIBUTING.md +0 -0
  24. {haliax-1.4.dev340 → haliax-1.4.dev342}/LICENSE +0 -0
  25. {haliax-1.4.dev340 → haliax-1.4.dev342}/README.md +0 -0
  26. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/api.md +0 -0
  27. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/broadcasting.md +0 -0
  28. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/cheatsheet.md +0 -0
  29. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/css/material.css +0 -0
  30. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/css/mkdocstrings.css +0 -0
  31. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/faq.md +0 -0
  32. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/figures/data_parallel_mesh.png +0 -0
  33. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/figures/data_parallel_mesh_replicated.png +0 -0
  34. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/figures/device_mesh_1d.png +0 -0
  35. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/figures/device_mesh_1d_zero.png +0 -0
  36. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/figures/device_mesh_2d.png +0 -0
  37. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/figures/device_mesh_2d_batch_partitioned.png +0 -0
  38. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/figures/device_mesh_2d_data_replicated.png +0 -0
  39. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/figures/device_mesh_2d_data_replicated_mlp_partitioned.png +0 -0
  40. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/figures/device_mesh_2d_intermediate_fully_partitioned.png +0 -0
  41. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/figures/device_mesh_2d_zero.png +0 -0
  42. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/fp8.md +0 -0
  43. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/hof.md +0 -0
  44. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/index.md +0 -0
  45. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/indexing.md +0 -0
  46. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/matmul.md +0 -0
  47. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/partitioning.md +0 -0
  48. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/rearrange.ipynb +0 -0
  49. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/rearrange.md +0 -0
  50. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/requirements.txt +0 -0
  51. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/stacked.md +0 -0
  52. {haliax-1.4.dev340 → haliax-1.4.dev342}/docs/tutorial.md +0 -0
  53. {haliax-1.4.dev340 → haliax-1.4.dev342}/mkdocs.yml +0 -0
  54. {haliax-1.4.dev340 → haliax-1.4.dev342}/pyproject.toml +0 -0
  55. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/__init__.py +0 -0
  56. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/_src/__init__.py +0 -0
  57. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/_src/compile_utils.py +0 -0
  58. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/_src/dot.py +0 -0
  59. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/_src/einsum.py +0 -0
  60. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/_src/fp8.py +0 -0
  61. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/_src/parsing.py +0 -0
  62. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/_src/rearrange.py +0 -0
  63. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/_src/util.py +0 -0
  64. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/axis.py +0 -0
  65. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/core.py +0 -0
  66. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/debug.py +0 -0
  67. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/hof.py +0 -0
  68. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/jax_utils.py +0 -0
  69. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/nn/activations.py +0 -0
  70. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/nn/attention.py +0 -0
  71. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/nn/conv.py +0 -0
  72. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/nn/dropout.py +0 -0
  73. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/nn/embedding.py +0 -0
  74. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/nn/loss.py +0 -0
  75. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/nn/mlp.py +0 -0
  76. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/nn/pool.py +0 -0
  77. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/nn/scan.py +0 -0
  78. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/ops.py +0 -0
  79. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/partitioning.py +0 -0
  80. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/quantization.py +0 -0
  81. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/random.py +0 -0
  82. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/specialized_fns.py +0 -0
  83. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/types.py +0 -0
  84. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/util.py +0 -0
  85. {haliax-1.4.dev340 → haliax-1.4.dev342}/src/haliax/wrap.py +0 -0
  86. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/core_test.py +0 -0
  87. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_attention.py +0 -0
  88. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_axis.py +0 -0
  89. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_conv.py +0 -0
  90. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_debug.py +0 -0
  91. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_dot.py +0 -0
  92. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_einsum.py +0 -0
  93. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_fp8.py +0 -0
  94. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_hof.py +0 -0
  95. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_int8.py +0 -0
  96. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_nn.py +0 -0
  97. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_ops.py +0 -0
  98. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_parsing.py +0 -0
  99. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_partitioning.py +0 -0
  100. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_pool.py +0 -0
  101. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_random.py +0 -0
  102. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_rearrange.py +0 -0
  103. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_scan.py +0 -0
  104. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_specialized_fns.py +0 -0
  105. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_tree_util.py +0 -0
  106. {haliax-1.4.dev340 → haliax-1.4.dev342}/tests/test_utils.py +0 -0
@@ -17,19 +17,21 @@ jobs:
17
17
  run: |
18
18
  python -m pip install --upgrade pip
19
19
  pip install flake8 pytest
20
- pip install --upgrade "jax[cpu]==0.4.35" "jaxlib[cpu]==0.4.35"
20
+ pip install "jax[cpu]==0.5.3" "jaxlib[cpu]==0.5.3" .[dev]
21
21
 
22
22
  - name: Install Levanter from source
23
23
  run: |
24
24
  cd ..
25
25
  git clone https://github.com/stanford-crfm/levanter.git
26
26
  cd levanter
27
- pip install -e .
27
+ pip install -e .[tests]
28
+ pip install -r tests/requirements.txt
29
+ # i don't know why this is necessary
30
+ pip install tensorboardX
28
31
  - name: Install Haliax on top
29
32
  run: |
30
33
  # install second since levanter will install a built version of haliax
31
34
  cd ../haliax
32
- pip install "jax[cpu]==0.4.35" "jaxlib[cpu]==0.4.35" .[dev]
33
35
  - name: Test levanter with pytest
34
36
  run: |
35
37
  cd ../levanter
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haliax
3
- Version: 1.4.dev340
3
+ Version: 1.4.dev342
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/
@@ -18,7 +18,10 @@ of the same module (e.g. transformer blocks), which is a common pattern in deep
18
18
  ::: haliax.nn.Dropout
19
19
 
20
20
  ### Normalization
21
+
22
+ ::: haliax.nn.normalization.LayerNormBase
21
23
  ::: haliax.nn.LayerNorm
24
+ ::: haliax.nn.RmsNorm
22
25
 
23
26
  ### Meta
24
27
 
@@ -122,6 +122,7 @@ For example, below is the implementation of `to_state_dict()` in [levanter.model
122
122
  In this class, we want to preserve HF compatibility by saving untied output embeddings. (We chose not to implement
123
123
  non-weight-tied embeddings.)
124
124
 
125
+
125
126
  ```python
126
127
  from typing import Optional
127
128
 
@@ -155,8 +156,69 @@ to join the prefix to the keys of the state dict.
155
156
 
156
157
  ```
157
158
 
159
+ #### Flattening and Unflattening
160
+
161
+ Haliax differs from many NN frameworks, and PyTorch in particular, in supporting multiple axes as inputs and outputs
162
+ for linear transformations and layer norms. This means that a Linear layer's weight might have shape `(Heads, Dim, Out)`
163
+ rather than just `(In, Out)` (where `In = Heads * Dim`). To facilitate compatibility with PyTorch, we provide
164
+ two functions, `flatten_modules_for_export` and `unflatten_modules_from_export`, that can be used to convert
165
+ modules to and from a format that is compatible with PyTorch. These functions are used internally by
166
+ `to_torch_compatible_state_dict` and `from_torch_compatible_state_dict` and we expose them for advanced users.
167
+
168
+ If you are adding a new module, you can plug into this system by inheriting from [haliax.state_dict.ModuleWithStateDictSerialization][]
169
+ and overriding the `flatten_for_export` and `unflatten_from_export` methods. Here is an example from [haliax.nn.LayerNorm][]:
170
+
171
+ ```python
172
+ Mod = TypeVar("Mod")
173
+
174
+ class LayerNormBase(ModuleWithStateDictSerialization):
175
+ def flatten_for_export(self: Mod) -> Mod:
176
+ if isinstance(self.axis, hax.Axis):
177
+ return self
178
+
179
+ if self.weight is not None:
180
+ weight = self.weight.flatten("__OUT")
181
+ else:
182
+ weight = None
183
+
184
+ if self.bias is not None:
185
+ bias = self.bias.flatten("__OUT")
186
+ else:
187
+ bias = None
188
+
189
+ return dataclasses.replace(
190
+ self, weight=weight, bias=bias, axis=hax.flatten_axes(self.axis, "__OUT")
191
+ )
192
+
193
+ def unflatten_from_export(self: Mod, template: Mod) -> Mod:
194
+ if template.axis == self.axis:
195
+ return self
196
+
197
+ if self.weight is not None:
198
+ assert isinstance(self.axis, hax.Axis), "Cannot unflatten weight with non-axis axis"
199
+ weight = hax.unflatten_axis(self.weight, self.axis, template.axis)
200
+ else:
201
+ weight = None
202
+
203
+ if self.bias is not None:
204
+ assert isinstance(self.axis, hax.Axis), "Cannot unflatten weight with non-axis axis"
205
+ bias = hax.unflatten_axis(self.bias, self.axis, template.axis)
206
+
207
+ else:
208
+ bias = None
209
+
210
+ return dataclasses.replace(
211
+ self, weight=weight, bias=bias, axis=template.axis
212
+ )
213
+ ```
214
+
215
+ The code is a bit boilerplate-y but the idea is to find articulated axes in arrays and flatten them, while updating
216
+ any Axis members to match the new shape.
217
+
158
218
  ## API Reference
159
219
 
220
+ ### Types
221
+
160
222
  ::: haliax.state_dict.StateDict
161
223
  ::: haliax.state_dict.ModuleWithStateDictSerialization
162
224
 
@@ -173,5 +235,5 @@ to join the prefix to the keys of the state dict.
173
235
 
174
236
  ::: haliax.state_dict.from_torch_compatible_state_dict
175
237
  ::: haliax.state_dict.to_torch_compatible_state_dict
176
- ::: haliax.state_dict.flatten_linear_layers
177
- ::: haliax.state_dict.unflatten_linear_layers
238
+ ::: haliax.state_dict.flatten_modules_for_export
239
+ ::: haliax.state_dict.unflatten_modules_from_export
@@ -0,0 +1 @@
1
+ __version__ = "1.4.dev342"
@@ -18,6 +18,7 @@ from haliax._src.util import index_where
18
18
  from haliax.axis import Axis
19
19
  from haliax.core import NamedArray, flatten_axes, named
20
20
  from haliax.jax_utils import is_jax_array_like, is_scalarish
21
+ from haliax.tree_util import scan_aware_tree_map
21
22
 
22
23
 
23
24
  try:
@@ -32,14 +33,16 @@ T = TypeVar("T")
32
33
 
33
34
 
34
35
  def from_torch_compatible_state_dict(
35
- t: T, state_dict: StateDict, *, unflatten_linear: bool = True, prefix: Optional[str] = None
36
+ t: T, state_dict: StateDict, *, unflatten: bool = True, prefix: Optional[str] = None
36
37
  ) -> T:
37
38
  """
38
39
  Convert a state dict to a tree that is compatible with the structure of `t`.
39
40
 
40
- This applies [haliax.state_dict.from_state_dict][] followed by [haliax.state_dict.unflatten_linear_layers][].
41
+ If unflatten is true, then the weights in the state dict are assumed to have been flattened (as by flatten_modules_for_export).
42
+
43
+ This applies [haliax.state_dict.from_state_dict][]
41
44
  """
42
- if unflatten_linear:
45
+ if unflatten:
43
46
  t = _flatten_to_unflatten(t, state_dict, prefix)
44
47
  else:
45
48
  t = from_state_dict(t, state_dict, prefix=prefix)
@@ -47,6 +50,45 @@ def from_torch_compatible_state_dict(
47
50
  return t
48
51
 
49
52
 
53
+ def flatten_modules_for_export(t: T) -> T:
54
+ """
55
+ Flatten all modules in a tree for export to torch.
56
+ """
57
+
58
+ def _flatten_module(module):
59
+ if isinstance(module, ModuleWithStateDictSerialization):
60
+ module = module.flatten_for_export()
61
+ module = scan_aware_tree_map(
62
+ _flatten_module,
63
+ module,
64
+ is_leaf=lambda x: x is not module and isinstance(x, ModuleWithStateDictSerialization),
65
+ )
66
+ return module
67
+
68
+ return scan_aware_tree_map(_flatten_module, t, is_leaf=lambda x: isinstance(x, ModuleWithStateDictSerialization))
69
+
70
+
71
+ def unflatten_modules_from_export(t: T, template: T) -> T:
72
+ """
73
+ Unflatten all modules in a tree after import from torch.
74
+ """
75
+
76
+ def _unflatten_module(module, template):
77
+ if isinstance(module, ModuleWithStateDictSerialization):
78
+ module = module.unflatten_from_export(template)
79
+ module = scan_aware_tree_map(
80
+ _unflatten_module,
81
+ module,
82
+ template,
83
+ is_leaf=lambda x: x is not module and isinstance(x, ModuleWithStateDictSerialization),
84
+ )
85
+ return module
86
+
87
+ return scan_aware_tree_map(
88
+ _unflatten_module, t, template, is_leaf=lambda x: isinstance(x, ModuleWithStateDictSerialization)
89
+ )
90
+
91
+
50
92
  def _flatten_to_unflatten(t, state_dict, prefix):
51
93
  """
52
94
  Flatten the torch compatible state_dict before loading into t, and then recover the unflattened layers.
@@ -59,9 +101,9 @@ def _flatten_to_unflatten(t, state_dict, prefix):
59
101
  return jnp.zeros(struct.shape, struct.dtype)
60
102
 
61
103
  t = jax.tree.map(_dt_struct_to_array, t)
62
- flat_t = flatten_linear_layers(t)
104
+ flat_t = flatten_modules_for_export(t)
63
105
  flat_t = from_state_dict(flat_t, state_dict, prefix=prefix)
64
- t = unflatten_linear_layers(t, flat_t)
106
+ t = unflatten_modules_from_export(flat_t, t)
65
107
  return t
66
108
 
67
109
 
@@ -103,6 +145,25 @@ class ModuleWithStateDictSerialization(eqx.Module):
103
145
  """Returns a dict mapping eqx.Module keys to torch keys that need to be renamed for serialization"""
104
146
  return {}
105
147
 
148
+ def flatten_for_export(self: Mod) -> Mod:
149
+ """
150
+ Flatten articulated named arrays for export to torch. In general this method should, for a linear layer, flatten
151
+ all input axes into a single axis, and all output axes into a single axis. You can do whatever else
152
+ you want to support pytorch-compatible serialization if you want.
153
+
154
+ This method is less general than to_state_dict and is only called when using to_torch_compatible_state_dict.
155
+ """
156
+ return self
157
+
158
+ def unflatten_from_export(self: Mod, template: Mod) -> Mod:
159
+ """
160
+ Unflatten the module after import from torch.
161
+
162
+ Template has the proper structure (e.g. articulated named axes) but the values are meaningless.
163
+ """
164
+ del template
165
+ return self
166
+
106
167
 
107
168
  def from_state_dict(tree: T, state_dict: StateDict, prefix: Optional[str] = None) -> T:
108
169
  """
@@ -372,78 +433,3 @@ def load_state_dict(path):
372
433
  """
373
434
  state_dict = safetensors.numpy.load_file(path)
374
435
  return state_dict
375
-
376
-
377
- def flatten_linear_layers(tree: T) -> T:
378
- """
379
- In PyTorch, linear layers are stored as a 2d weight matrix and a 1d bias vector. In Haliax,
380
- linear layers can have arbitrary dimensions, grouped into input and output axes. This function
381
- flattens the linear layers in a tree to be compatible with PyTorch-style state dicts.
382
-
383
- :param tree:
384
- """
385
- from haliax.nn import Linear
386
-
387
- def _flatten_linear(layer):
388
- if not isinstance(layer, Linear):
389
- return layer
390
-
391
- weight = layer.weight
392
- bias = layer.bias
393
-
394
- new_Out: Axis = flatten_axes(layer.Out, "__OUT__")
395
- new_In: Axis = flatten_axes(layer.In, "__IN__")
396
-
397
- if weight.array is not None:
398
- out_first = layer._out_first
399
- weight = weight.flatten_axes(layer.Out, new_Out).flatten_axes(layer.In, new_In)
400
-
401
- if out_first:
402
- weight = weight.rearrange((..., "__OUT__", "__IN__"))
403
- else:
404
- weight = weight.rearrange((..., "__IN__", "__OUT__"))
405
-
406
- if isinstance(bias, NamedArray):
407
- bias = bias.flatten_axes(layer.Out, new_Out)
408
-
409
- return dataclasses.replace(layer, weight=weight, bias=bias, In=new_In, Out=new_Out) # type: ignore
410
-
411
- return jax.tree.map(_flatten_linear, tree, is_leaf=lambda x: isinstance(x, Linear))
412
-
413
-
414
- def unflatten_linear_layers(template: T, tree_with_flattened_linears: T) -> T:
415
- """
416
- Unflattens linear layers in a tree that was flattened with [haliax.state_dict.flatten_linear_layers][].
417
- Template has the same structure as the tree that was flattened, but with the original (unflattened)
418
- linear layers.
419
-
420
- Returns:
421
- The same tree as `tree_with_flattened_linears`, but with the linear layers unflattened to match
422
- the structure of `template`.
423
- """
424
-
425
- from haliax.nn import Linear
426
-
427
- def _unflatten_linear(template, flattened):
428
- assert isinstance(template, Linear) == isinstance(flattened, Linear)
429
-
430
- if not isinstance(template, Linear):
431
- return flattened
432
-
433
- weight = flattened.weight
434
- bias = flattened.bias
435
-
436
- if weight.array is not None:
437
- weight = weight.unflatten_axis("__OUT__", template.Out).unflatten_axis("__IN__", template.In)
438
- weight = weight.rearrange(template.weight.axes)
439
-
440
- if isinstance(bias, NamedArray):
441
- bias = bias.unflatten_axis("__OUT__", template.Out)
442
- assert template.bias is not None, "Flattened bias but template has no bias"
443
- bias = bias.rearrange(template.bias.axes)
444
-
445
- return dataclasses.replace(template, weight=weight, bias=bias) # type: ignore
446
-
447
- return jax.tree.map(
448
- _unflatten_linear, template, tree_with_flattened_linears, is_leaf=lambda x: isinstance(x, Linear)
449
- )
@@ -36,14 +36,11 @@ from .embedding import Embedding
36
36
  from .linear import Linear
37
37
  from .loss import binary_cross_entropy_loss, cross_entropy_loss, cross_entropy_loss_and_log_normalizers, reduce_loss
38
38
  from .mlp import MLP
39
- from .normalization import LayerNorm, log_softmax, logsumexp, softmax, standardize
39
+ from .normalization import LayerNorm, RmsNorm, log_softmax, logsumexp, softmax, standardize
40
40
  from .pool import max_pool, mean_pool, min_pool
41
41
  from .scan import BlockSeq, Stacked
42
42
 
43
43
 
44
- # TODO: support where in softmax, etc
45
-
46
-
47
44
  def one_hot(x: NamedArray | int, class_axis: Axis, *, dtype=None) -> NamedArray:
48
45
  """
49
46
  Convert an integer to a one-hot vector. This is basically a generalization of [jax.nn.one_hot][]
@@ -87,6 +84,7 @@ __all__ = [
87
84
  "LayerNorm",
88
85
  "Linear",
89
86
  "Embedding",
87
+ "RmsNorm",
90
88
  "Stacked",
91
89
  "BlockSeq",
92
90
  "MLP",
@@ -1,3 +1,4 @@
1
+ import dataclasses
1
2
  import math
2
3
  from typing import Optional
3
4
 
@@ -6,13 +7,14 @@ from jax.random import PRNGKey
6
7
 
7
8
  import haliax as hax
8
9
 
10
+ from .._src.state_dict import Mod, ModuleWithStateDictSerialization
9
11
  from ..axis import AxisSpec
10
12
  from ..core import NamedArray
11
13
  from ..jax_utils import named_call
12
14
  from ..quantization import DotGeneralOp
13
15
 
14
16
 
15
- class Linear(eqx.Module):
17
+ class Linear(ModuleWithStateDictSerialization):
16
18
  """A named Linear layer. This module allows you to specify multiple named axes for both input
17
19
  and output, which is occasionally useful."""
18
20
 
@@ -71,6 +73,47 @@ class Linear(eqx.Module):
71
73
 
72
74
  return q
73
75
 
76
+ def flatten_for_export(self: Mod) -> Mod:
77
+ if isinstance(self.Out, hax.Axis) and isinstance(self.In, hax.Axis):
78
+ return self
79
+
80
+ weight = self.weight
81
+ bias = self.bias
82
+
83
+ new_Out = hax.flatten_axes(self.Out, "__OUT__")
84
+ new_In = hax.flatten_axes(self.In, "__IN__")
85
+
86
+ if weight.array is not None:
87
+ out_first = self._out_first
88
+ weight = weight.flatten_axes(self.Out, new_Out).flatten_axes(self.In, new_In)
89
+
90
+ if out_first:
91
+ weight = weight.rearrange((..., "__OUT__", "__IN__"))
92
+ else:
93
+ weight = weight.rearrange((..., "__IN__", "__OUT__"))
94
+
95
+ if isinstance(bias, NamedArray):
96
+ bias = bias.flatten_axes(self.Out, new_Out)
97
+
98
+ return dataclasses.replace(self, weight=weight, bias=bias, In=new_In, Out=new_Out)
99
+
100
+ def unflatten_from_export(self: Mod, template: Mod) -> Mod:
101
+ weight = self.weight
102
+ bias = self.bias
103
+
104
+ if (template.In, template.Out) == (self.In, self.Out):
105
+ return self
106
+
107
+ if weight.array is not None:
108
+ weight = weight.unflatten_axis("__OUT__", template.Out).unflatten_axis("__IN__", template.In)
109
+ weight = weight.rearrange(template.weight.axes)
110
+
111
+ if isinstance(bias, NamedArray):
112
+ bias = bias.unflatten_axis("__OUT__", template.Out)
113
+ bias = bias.rearrange(template.bias.axes)
114
+
115
+ return dataclasses.replace(template, weight=weight, bias=bias)
116
+
74
117
  @property
75
118
  def _out_first(self):
76
119
  """
@@ -0,0 +1,169 @@
1
+ import dataclasses
2
+ from abc import abstractmethod
3
+ from typing import Optional, TypeVar
4
+
5
+ import equinox as eqx
6
+ from jax import nn as jnn
7
+ from jax import numpy as jnp
8
+
9
+ import haliax
10
+ import haliax as hax
11
+
12
+ from .._src.state_dict import Mod, ModuleWithStateDictSerialization
13
+ from ..axis import AxisSelection, AxisSpec
14
+ from ..core import NamedArray
15
+ from ..types import Scalar
16
+ from ..wrap import unwrap_namedarrays, wrap_axiswise_call, wrap_reduction_call
17
+
18
+
19
+ A = TypeVar("A", Scalar, NamedArray, jnp.ndarray)
20
+
21
+
22
+ class LayerNormBase(ModuleWithStateDictSerialization):
23
+ axis: AxisSpec = eqx.static_field()
24
+ weight: Optional[NamedArray]
25
+ bias: Optional[NamedArray]
26
+ eps: float = eqx.static_field(default=1e-5)
27
+ dtype: Optional[jnp.dtype] = eqx.field(default=None, static=True)
28
+
29
+ @abstractmethod
30
+ def __call__(self, x: NamedArray) -> NamedArray:
31
+ pass
32
+
33
+ @classmethod
34
+ def init(
35
+ cls,
36
+ axis: AxisSpec,
37
+ eps: float = 1e-5,
38
+ *,
39
+ use_weight: bool = True,
40
+ use_bias: bool = True,
41
+ dtype: Optional[jnp.dtype] = None,
42
+ ):
43
+ if use_weight:
44
+ weight = hax.ones(axis)
45
+ else:
46
+ weight = None
47
+
48
+ if use_bias:
49
+ bias = hax.zeros(axis)
50
+ else:
51
+ bias = None
52
+
53
+ return cls(axis, weight, bias, eps, dtype)
54
+
55
+ def flatten_for_export(self: Mod) -> Mod:
56
+ if isinstance(self.axis, hax.Axis):
57
+ return self
58
+
59
+ if self.weight is not None:
60
+ weight = self.weight.flatten("__OUT")
61
+ else:
62
+ weight = None
63
+
64
+ if self.bias is not None:
65
+ bias = self.bias.flatten("__OUT")
66
+ else:
67
+ bias = None
68
+
69
+ return dataclasses.replace(self, weight=weight, bias=bias, axis=hax.flatten_axes(self.axis, "__OUT"))
70
+
71
+ def unflatten_from_export(self: Mod, template: Mod) -> Mod:
72
+ if template.axis == self.axis:
73
+ return self
74
+
75
+ if self.weight is not None:
76
+ assert isinstance(self.axis, hax.Axis), "Cannot unflatten weight with non-axis axis"
77
+ weight = hax.unflatten_axis(self.weight, self.axis, template.axis)
78
+ else:
79
+ weight = None
80
+
81
+ if self.bias is not None:
82
+ assert isinstance(self.axis, hax.Axis), "Cannot unflatten weight with non-axis axis"
83
+ bias = hax.unflatten_axis(self.bias, self.axis, template.axis)
84
+
85
+ else:
86
+ bias = None
87
+
88
+ return dataclasses.replace(self, weight=weight, bias=bias, axis=template.axis)
89
+
90
+
91
+ class LayerNorm(LayerNormBase):
92
+ r"""
93
+ Normalises the input along the specified axis (or axes), using the mean and variance of the
94
+ input along that axis.
95
+ """
96
+ axis: AxisSpec = eqx.field(static=True)
97
+ weight: Optional[NamedArray]
98
+ bias: Optional[NamedArray]
99
+
100
+ eps: float = eqx.field(default=1e-5, static=True)
101
+ dtype: Optional[jnp.dtype] = eqx.field(default=None, static=True)
102
+
103
+ def __call__(self, x: NamedArray) -> NamedArray:
104
+ dtype = x.dtype
105
+ mean = x.mean(self.axis)
106
+ var = x.var(self.axis)
107
+ inv = hax.rsqrt(var + self.eps)
108
+ out = (x - mean) * inv
109
+ out = out.astype(dtype)
110
+
111
+ if self.weight is not None:
112
+ out = self.weight * out
113
+ if self.bias is not None:
114
+ out = out + self.bias
115
+ return out
116
+
117
+
118
+ class RmsNorm(LayerNormBase):
119
+ r"""
120
+ Implements RMS normalization, which normalizes the input by dividing by the root mean square of the input.
121
+ """
122
+
123
+ def __call__(self, x: NamedArray) -> NamedArray:
124
+ in_dtype = x.dtype
125
+ x = x.astype(self.dtype)
126
+ var = hax.mean(hax.square(x), axis=self.axis)
127
+ inv = hax.rsqrt(var + self.eps)
128
+ out = x * inv
129
+ out = out.astype(in_dtype)
130
+
131
+ if self.weight is not None:
132
+ out = self.weight * out
133
+ if self.bias is not None:
134
+ out = out + self.bias
135
+ return out
136
+
137
+
138
+ def logsumexp(a: A, axis: Optional[AxisSelection] = None) -> A:
139
+ # TODO: logsumexp indirectly supports where via `b`. we should support it directly
140
+ return wrap_reduction_call(jnn.logsumexp, a, axis=axis, single_axis_only=False, supports_where=False)
141
+
142
+
143
+ # TODO: support where in softmax, etc
144
+
145
+
146
+ def softmax(a: A, axis: Optional[AxisSelection] = None) -> A:
147
+ return wrap_axiswise_call(jnn.softmax, a, axis=axis, single_axis_only=False)
148
+
149
+
150
+ def log_softmax(a: A, axis: Optional[AxisSelection] = None) -> A:
151
+ return wrap_axiswise_call(jnn.log_softmax, a, axis=axis, single_axis_only=False)
152
+
153
+
154
+ def standardize(
155
+ x: NamedArray,
156
+ axis: AxisSpec,
157
+ *,
158
+ mean: Optional[NamedArray] = None,
159
+ variance: Optional[NamedArray] = None,
160
+ epsilon: float = 1e-5,
161
+ where: Optional[NamedArray] = None,
162
+ ) -> NamedArray:
163
+ """Analogous to [jax.nn.standardize][], but with support for NamedArrays."""
164
+ x, mean, variance, where = haliax.broadcast_arrays(x, mean, variance, where) # type: ignore
165
+ raw_x, mean, variance, where = unwrap_namedarrays(x, mean, variance, where)
166
+ axis_indices = x._lookup_indices(axis)
167
+
168
+ plain = jnn.standardize(raw_x, axis_indices, mean=mean, variance=variance, epsilon=epsilon, where=where)
169
+ return NamedArray(plain, x.axes)
@@ -8,14 +8,14 @@ from haliax.types import FilterSpec
8
8
  from ._src.state_dict import (
9
9
  ModuleWithStateDictSerialization,
10
10
  StateDict,
11
- flatten_linear_layers,
11
+ flatten_modules_for_export,
12
12
  from_state_dict,
13
13
  from_torch_compatible_state_dict,
14
14
  load_state_dict,
15
15
  save_state_dict,
16
16
  to_numpy_state_dict,
17
17
  to_state_dict,
18
- unflatten_linear_layers,
18
+ unflatten_modules_from_export,
19
19
  with_prefix,
20
20
  )
21
21
 
@@ -24,23 +24,23 @@ T = TypeVar("T")
24
24
 
25
25
 
26
26
  def to_torch_compatible_state_dict(
27
- t: T, *, flatten_linear: bool = True, prefix: Optional[str] = None, filter: FilterSpec = is_jax_array_like
27
+ t: T, *, flatten: bool = True, prefix: Optional[str] = None, filter: FilterSpec = is_jax_array_like
28
28
  ) -> StateDict:
29
29
  """
30
30
  Convert a tree to a state dict that is compatible with torch-style state dicts.
31
31
 
32
- This applies [haliax.state_dict.flatten_linear_layers][] followed by [haliax.state_dict.to_state_dict][]
32
+ This applies the same logic as [to_state_dict][] but also uses [haliax.state_dict.ModuleWithStateDictSerialization.flatten_for_export][] to flatten
33
33
 
34
34
  Args:
35
35
  t: The tree to convert
36
- flatten_linear: Whether to flatten linear layers
36
+ flatten: Whether to flatten axes using flatten_for_export
37
37
  prefix: The prefix to use for the state dict keys
38
38
  filter: The filter to use for selecting which nodes to include in the state dict. By default, this includes only
39
39
  array-like objects (e.g. JAX and NumPy arrays).
40
40
  """
41
41
  t = equinox.filter(t, filter)
42
- if flatten_linear:
43
- t = flatten_linear_layers(t)
42
+ if flatten:
43
+ t = flatten_modules_for_export(t)
44
44
  return to_numpy_state_dict(t, prefix=prefix)
45
45
 
46
46
 
@@ -50,11 +50,11 @@ __all__ = [
50
50
  "load_state_dict",
51
51
  "save_state_dict",
52
52
  "from_state_dict",
53
- "flatten_linear_layers",
54
- "unflatten_linear_layers",
55
53
  "with_prefix",
56
54
  "to_state_dict",
57
55
  "to_numpy_state_dict",
58
56
  "StateDict",
59
57
  "to_torch_compatible_state_dict",
58
+ "flatten_modules_for_export",
59
+ "unflatten_modules_from_export",
60
60
  ]
@@ -30,17 +30,10 @@ def tree_map(fn, tree, *rest, is_leaf=None):
30
30
 
31
31
  def scan_aware_tree_map(fn, tree, *rest, is_leaf=None):
32
32
  """
33
- Version of [haliax.tree_util.tree_map][] that is aware of the scan-layer pattern, specifically as implmeneted
33
+ Version of [haliax.tree_util.tree_map][] that is aware of the scan-layer pattern, specifically as implemented
34
34
  in hax.nn.Stacked. This function will (implicitly) apply the transform to each layer in each Stacked module
35
35
  (using vmap). If there are no Stacked modules in the tree, this function is equivalent to [haliax.tree_util.tree_map][].
36
36
 
37
- Args:
38
- fn:
39
- tree:
40
- *rest:
41
- is_leaf:
42
-
43
- Returns:
44
37
  """
45
38
  old_is_leaf = is_leaf
46
39
  if is_leaf is None:
@@ -55,7 +48,7 @@ def scan_aware_tree_map(fn, tree, *rest, is_leaf=None):
55
48
  new_inner = haliax.vmap(mapped_fn, x.Block)(x.stacked, *[r.stacked for r in rest])
56
49
  return dataclasses.replace(x, stacked=new_inner) # type: ignore
57
50
  else:
58
- return fn(x)
51
+ return fn(x, *rest)
59
52
 
60
53
  return tree_map(rec_fn, tree, *rest, is_leaf=is_leaf)
61
54
 
@@ -1,3 +1,4 @@
1
+ import dataclasses
1
2
  from typing import Any
2
3
 
3
4
  import equinox as eqx
@@ -6,9 +7,10 @@ import jax.numpy as jnp
6
7
  import pytest
7
8
 
8
9
  import haliax as hax
10
+ from haliax._src.state_dict import flatten_modules_for_export, unflatten_modules_from_export
9
11
  from haliax.nn import Linear
10
- from haliax.nn.scan import _stack_state_dict, _unstack_state_dict
11
- from haliax.state_dict import flatten_linear_layers, from_state_dict, to_state_dict, unflatten_linear_layers
12
+ from haliax.nn.scan import Stacked, _stack_state_dict, _unstack_state_dict
13
+ from haliax.state_dict import from_state_dict, to_state_dict
12
14
 
13
15
 
14
16
  @pytest.mark.parametrize("out_dims_first", [True, False])
@@ -24,7 +26,7 @@ def test_flatten_linear_layers(out_dims_first: bool):
24
26
  else:
25
27
  assert linear.weight.axes == (H, W, D, B)
26
28
 
27
- flat_linear = flatten_linear_layers(linear)
29
+ flat_linear = linear.flatten_for_export()
28
30
 
29
31
  flat_state_dict = to_state_dict(flat_linear)
30
32
  if out_dims_first:
@@ -36,7 +38,7 @@ def test_flatten_linear_layers(out_dims_first: bool):
36
38
 
37
39
  # now unflatten it
38
40
  linear2 = Linear.init((H, W), (D, B), key=jax.random.PRNGKey(1), use_bias=True, out_first=out_dims_first)
39
- new_linear = unflatten_linear_layers(linear2, flat_linear)
41
+ new_linear = flat_linear.unflatten_from_export(linear2)
40
42
 
41
43
  if out_dims_first:
42
44
  assert new_linear.weight.axes == (D, B, H, W)
@@ -127,3 +129,49 @@ def test_to_from_state_dict():
127
129
  m2 = from_state_dict(m2, state_dict)
128
130
  assert jnp.all(m2.a == a)
129
131
  assert jnp.all(m2.b == b)
132
+
133
+
134
+ def test_export_layer_norm():
135
+ D = hax.Axis("D", 10)
136
+ E = hax.Axis("E", 20)
137
+ layer_norm = hax.nn.LayerNorm.init((D, E), eps=1e-5, use_weight=True, use_bias=True)
138
+
139
+ flat_layer_norm = layer_norm.flatten_for_export()
140
+
141
+ flat_state_dict = to_state_dict(flat_layer_norm)
142
+
143
+ assert flat_state_dict["weight"].shape == (D.size * E.size,)
144
+ assert flat_state_dict["bias"].shape == (D.size * E.size,)
145
+ assert flat_state_dict["weight"].dtype == flat_state_dict["bias"].dtype == layer_norm.weight.dtype
146
+
147
+ # now unflatten it
148
+ layer_norm2 = hax.nn.LayerNorm.init((D, E), eps=1e-5, use_weight=True, use_bias=True)
149
+ # ensure we have different weights
150
+ layer_norm2 = dataclasses.replace(layer_norm2, weight=layer_norm2.weight + 1, bias=layer_norm2.bias + 1)
151
+
152
+ new_layer_norm = flat_layer_norm.unflatten_from_export(layer_norm2)
153
+
154
+ assert layer_norm == new_layer_norm
155
+
156
+
157
+ def test_stacked_layer_norm():
158
+ L = hax.Axis("L", 4)
159
+ D = hax.Axis("D", 10)
160
+ E = hax.Axis("E", 20)
161
+
162
+ norms = Stacked.init(L, hax.nn.LayerNorm)((D, E), eps=1e-5, use_weight=True, use_bias=True)
163
+
164
+ norms_flat = flatten_modules_for_export(norms)
165
+
166
+ flat_state_dict = to_state_dict(norms_flat)
167
+
168
+ assert flat_state_dict["0.weight"].shape == (D.size * E.size,)
169
+ assert flat_state_dict["0.bias"].shape == (D.size * E.size,)
170
+ assert flat_state_dict["1.weight"].shape == (D.size * E.size,)
171
+
172
+ # now unflatten it
173
+ norms2 = Stacked.init(L, hax.nn.LayerNorm)((D, E), eps=1e-5, use_weight=True, use_bias=True)
174
+
175
+ new_norms = unflatten_modules_from_export(norms_flat, norms2)
176
+
177
+ assert norms == new_norms
@@ -1 +0,0 @@
1
- __version__ = "1.4.dev340"
@@ -1,87 +0,0 @@
1
- from typing import Optional, TypeVar
2
-
3
- import equinox as eqx
4
- from jax import nn as jnn
5
- from jax import numpy as jnp
6
-
7
- import haliax
8
- import haliax as hax
9
-
10
- from ..axis import AxisSelection, AxisSpec
11
- from ..core import NamedArray
12
- from ..types import Scalar
13
- from ..wrap import unwrap_namedarrays, wrap_axiswise_call, wrap_reduction_call
14
-
15
-
16
- A = TypeVar("A", Scalar, NamedArray, jnp.ndarray)
17
-
18
-
19
- class LayerNorm(eqx.Module):
20
- r"""
21
- Normalises the input along the specified axis (or axes), using the mean and variance of the
22
- input along that axis.
23
- """
24
- axis: AxisSpec = eqx.static_field()
25
- weight: Optional[NamedArray]
26
- bias: Optional[NamedArray]
27
-
28
- eps: float = eqx.static_field(default=1e-5)
29
-
30
- @staticmethod
31
- def init(axis: AxisSpec, eps: float = 1e-5, use_weight: bool = True, use_bias: bool = True):
32
- if use_weight:
33
- weight = hax.ones(axis)
34
- else:
35
- weight = None
36
- if use_bias:
37
- bias = hax.zeros(axis)
38
- else:
39
- bias = None
40
-
41
- return LayerNorm(axis, weight, bias, eps)
42
-
43
- def __call__(self, x: NamedArray) -> NamedArray:
44
- mean = x.mean(self.axis)
45
- var = x.var(self.axis)
46
- inv = hax.rsqrt(var + self.eps)
47
- out = (x - mean) * inv
48
-
49
- if self.weight is not None:
50
- out = self.weight * out
51
- if self.bias is not None:
52
- out = out + self.bias
53
- return out
54
-
55
-
56
- def logsumexp(a: A, axis: Optional[AxisSelection] = None) -> A:
57
- # TODO: logsumexp indirectly supports where via `b`. we should support it directly
58
- return wrap_reduction_call(jnn.logsumexp, a, axis=axis, single_axis_only=False, supports_where=False)
59
-
60
-
61
- # TODO: support where in softmax, etc
62
-
63
-
64
- def softmax(a: A, axis: Optional[AxisSelection] = None) -> A:
65
- return wrap_axiswise_call(jnn.softmax, a, axis=axis, single_axis_only=False)
66
-
67
-
68
- def log_softmax(a: A, axis: Optional[AxisSelection] = None) -> A:
69
- return wrap_axiswise_call(jnn.log_softmax, a, axis=axis, single_axis_only=False)
70
-
71
-
72
- def standardize(
73
- x: NamedArray,
74
- axis: AxisSpec,
75
- *,
76
- mean: Optional[NamedArray] = None,
77
- variance: Optional[NamedArray] = None,
78
- epsilon: float = 1e-5,
79
- where: Optional[NamedArray] = None,
80
- ) -> NamedArray:
81
- """Analogous to [jax.nn.standardize][], but with support for NamedArrays."""
82
- x, mean, variance, where = haliax.broadcast_arrays(x, mean, variance, where) # type: ignore
83
- raw_x, mean, variance, where = unwrap_namedarrays(x, mean, variance, where)
84
- axis_indices = x._lookup_indices(axis)
85
-
86
- plain = jnn.standardize(raw_x, axis_indices, mean=mean, variance=variance, epsilon=epsilon, where=where)
87
- return NamedArray(plain, x.axes)
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