Equimo 2.6.0__tar.gz → 2.7.0__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 (173) hide show
  1. {equimo-2.6.0 → equimo-2.7.0}/CHANGELOG.md +38 -0
  2. {equimo-2.6.0 → equimo-2.7.0}/PKG-INFO +49 -10
  3. {equimo-2.6.0 → equimo-2.7.0}/README.md +46 -7
  4. {equimo-2.6.0 → equimo-2.7.0}/docs/stability.md +4 -0
  5. {equimo-2.6.0 → equimo-2.7.0}/pyproject.toml +4 -4
  6. equimo-2.7.0/src/equimo/__init__.py +1 -0
  7. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/layers/__init__.py +2 -0
  8. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/layers/activation.py +3 -1
  9. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/layers/attention.py +146 -12
  10. equimo-2.7.0/src/equimo/core/ops/__init__.py +18 -0
  11. equimo-2.7.0/src/equimo/core/ops/probability.py +203 -0
  12. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/ops/scan.py +1 -1
  13. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/feature_extraction.py +3 -3
  14. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/heads.py +10 -9
  15. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/adapters.py +4 -4
  16. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/prefix.py +12 -13
  17. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/pooling.py +3 -4
  18. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/tabular/layers/attention.py +48 -6
  19. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/tabular/layers/decoder.py +4 -3
  20. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/query_training/_loss.py +2 -3
  21. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/segmentation.py +40 -3
  22. equimo-2.6.0/src/equimo/__init__.py +0 -1
  23. equimo-2.6.0/src/equimo/core/ops/__init__.py +0 -3
  24. {equimo-2.6.0 → equimo-2.7.0}/LICENSE.md +0 -0
  25. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/Apache-2.0.txt +0 -0
  26. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/pretrained/README.md +0 -0
  27. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/pretrained/ast-BSD-3-Clause.txt +0 -0
  28. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/pretrained/convnext-Apache-2.0.txt +0 -0
  29. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/pretrained/convnextv2-CC-BY-NC-4.0.txt +0 -0
  30. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/pretrained/dinov2-Apache-2.0.txt +0 -0
  31. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/pretrained/dinov3-License.md +0 -0
  32. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/pretrained/eupe-FAIR-Noncommercial-Research-License.md +0 -0
  33. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/pretrained/lingbot-vision-Apache-2.0.txt +0 -0
  34. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/pretrained/siglip2-Apache-2.0.txt +0 -0
  35. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/pretrained/t0-alpha-Apache-2.0.txt +0 -0
  36. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/pretrained/tabpfn-3-License-v1.0.txt +0 -0
  37. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/pretrained/tips-CC-BY-4.0.txt +0 -0
  38. {equimo-2.6.0 → equimo-2.7.0}/LICENSES/tfc-t0-APACHE-2.0.txt +0 -0
  39. {equimo-2.6.0 → equimo-2.7.0}/NOTICE +0 -0
  40. {equimo-2.6.0 → equimo-2.7.0}/docs/licensing/t0-source-provenance.md +0 -0
  41. {equimo-2.6.0 → equimo-2.7.0}/docs/migration-v2.md +0 -0
  42. {equimo-2.6.0 → equimo-2.7.0}/docs/timeseries.md +0 -0
  43. {equimo-2.6.0 → equimo-2.7.0}/models/huggingface/NOTICE +0 -0
  44. {equimo-2.6.0 → equimo-2.7.0}/models/huggingface/README.md +0 -0
  45. {equimo-2.6.0 → equimo-2.7.0}/models/huggingface/convnext-conversion.json +0 -0
  46. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/_checkpoint_limits.py +0 -0
  47. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/_io.py +0 -0
  48. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/_pretrained.py +0 -0
  49. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/audio/__init__.py +0 -0
  50. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/audio/io.py +0 -0
  51. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/audio/layers/__init__.py +0 -0
  52. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/audio/layers/patch.py +0 -0
  53. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/audio/models/__init__.py +0 -0
  54. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/audio/models/ast.py +0 -0
  55. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/catalog.py +0 -0
  56. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/conversion/__init__.py +0 -0
  57. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/conversion/utils.py +0 -0
  58. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/__init__.py +0 -0
  59. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/_prng.py +0 -0
  60. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/emamodel.py +0 -0
  61. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/factory.py +0 -0
  62. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/implicit/__init__.py +0 -0
  63. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/implicit/_base.py +0 -0
  64. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/implicit/deq.py +0 -0
  65. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/implicit/injectors.py +0 -0
  66. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/implicit/stabilizers.py +0 -0
  67. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/implicit/strategies.py +0 -0
  68. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/intermediates.py +0 -0
  69. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/layers/_registry.py +0 -0
  70. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/layers/dropout.py +0 -0
  71. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/layers/ffn.py +0 -0
  72. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/layers/generic.py +0 -0
  73. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/layers/mamba.py +0 -0
  74. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/layers/norm.py +0 -0
  75. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/core/layers/rotary.py +0 -0
  76. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/__init__.py +0 -0
  77. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/_blocks.py +0 -0
  78. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/_serialization_codecs.py +0 -0
  79. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/_typing.py +0 -0
  80. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/audio/__init__.py +0 -0
  81. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/audio/recipes.py +0 -0
  82. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/audio/selectors.py +0 -0
  83. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/calibration.py +0 -0
  84. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/config.py +0 -0
  85. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/continued_ssl.py +0 -0
  86. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/inspection.py +0 -0
  87. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/labels.py +0 -0
  88. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/language/__init__.py +0 -0
  89. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/language/recipes.py +0 -0
  90. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/language/selectors.py +0 -0
  91. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/masks.py +0 -0
  92. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/merging.py +0 -0
  93. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/paths.py +0 -0
  94. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/__init__.py +0 -0
  95. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/_common.py +0 -0
  96. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/_compat.py +0 -0
  97. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/base.py +0 -0
  98. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/bitfit.py +0 -0
  99. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/dora.py +0 -0
  100. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/ia3.py +0 -0
  101. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/lora.py +0 -0
  102. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/prompts.py +0 -0
  103. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/scale_shift.py +0 -0
  104. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/peft/vera.py +0 -0
  105. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/recipes.py +0 -0
  106. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/regularization.py +0 -0
  107. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/selectors.py +0 -0
  108. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/serialization.py +0 -0
  109. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/surgery.py +0 -0
  110. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/tags.py +0 -0
  111. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/vision/__init__.py +0 -0
  112. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/vision/dense.py +0 -0
  113. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/vision/eomt.py +0 -0
  114. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/finetune/vision/pmt.py +0 -0
  115. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/language/__init__.py +0 -0
  116. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/language/models.py +0 -0
  117. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/language/tokenizers.py +0 -0
  118. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/registry.py +0 -0
  119. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/serialization.py +0 -0
  120. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/tabular/__init__.py +0 -0
  121. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/tabular/layers/__init__.py +0 -0
  122. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/tabular/layers/blocks.py +0 -0
  123. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/tabular/layers/mlp.py +0 -0
  124. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/tabular/layers/preprocessing.py +0 -0
  125. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/tabular/layers/registry.py +0 -0
  126. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/tabular/models/__init__.py +0 -0
  127. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/tabular/models/tabpfn.py +0 -0
  128. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/timeseries/__init__.py +0 -0
  129. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/timeseries/layers/__init__.py +0 -0
  130. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/timeseries/layers/axis_attn.py +0 -0
  131. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/timeseries/layers/blocks.py +0 -0
  132. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/timeseries/layers/patch_encoder.py +0 -0
  133. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/timeseries/layers/registry.py +0 -0
  134. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/timeseries/layers/residual_mlp.py +0 -0
  135. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/timeseries/models/__init__.py +0 -0
  136. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/timeseries/models/t0.py +0 -0
  137. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/utils.py +0 -0
  138. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/__init__.py +0 -0
  139. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/_encoder_identity.py +0 -0
  140. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/io.py +0 -0
  141. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/layers/__init__.py +0 -0
  142. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/layers/attention.py +0 -0
  143. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/layers/convolution.py +0 -0
  144. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/layers/downsample.py +0 -0
  145. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/layers/patch.py +0 -0
  146. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/layers/posemb.py +0 -0
  147. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/layers/registry.py +0 -0
  148. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/layers/squeeze_excite.py +0 -0
  149. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/layers/wavelet.py +0 -0
  150. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/__init__.py +0 -0
  151. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/_embedding.py +0 -0
  152. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/_features.py +0 -0
  153. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/attnet.py +0 -0
  154. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/convnext.py +0 -0
  155. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/deq.py +0 -0
  156. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/eomt.py +0 -0
  157. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/fastervit.py +0 -0
  158. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/iformer.py +0 -0
  159. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/lowformer.py +0 -0
  160. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/mlla.py +0 -0
  161. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/mobilenet.py +0 -0
  162. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/parcae.py +0 -0
  163. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/partialformer.py +0 -0
  164. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/pmd.py +0 -0
  165. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/pmt.py +0 -0
  166. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/reduceformer.py +0 -0
  167. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/shvit.py +0 -0
  168. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/vit.py +0 -0
  169. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/models/vssd.py +0 -0
  170. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/pmt_checkpoint.py +0 -0
  171. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/query_training/__init__.py +0 -0
  172. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/query_training/_assignment.py +0 -0
  173. {equimo-2.6.0 → equimo-2.7.0}/src/equimo/vision/query_training/_sampling.py +0 -0
@@ -2,6 +2,44 @@
2
2
 
3
3
  All notable changes to Equimo are documented here. Equimo follows Semantic Versioning from version 2.0.0 onward.
4
4
 
5
+ ## [2.7.0] - 2026-10-26
6
+
7
+ ### Added
8
+
9
+ - `equimo.core.ops` exports `softmax`, `log_softmax`, `softmax_cross_entropy_with_integer_labels`, and
10
+ `softmax_cross_entropy_with_logit_targets`, following "A Fix for Softmax and Cross-Entropy". They compute in at least
11
+ float32, return the input dtype, and keep the dominant logit's small gradient at high confidence. The logit-target
12
+ loss, for distillation, also preserves near-equality gradients, stops target gradients, and supports higher-order
13
+ derivatives.
14
+ - Experimental fused attention: `equimo.core.layers.with_attention_implementation(model, "cudnn")` runs core,
15
+ prefix-tuned, and tabular attention through cuDNN's fused kernel for bfloat16 or float16 models on CUDA GPUs, and
16
+ attention modules accept the same choice as an `implementation` argument. On an RTX 4080, it shortened ViT and EoMT
17
+ training steps by 7-22% and reduced their temporary memory by 17-56%. It does not support attention dropout or
18
+ jax2onnx export; the default `"xla"` implementation is unchanged.
19
+ - `semantic_crop_origins` places the fewest evenly spaced crops that cover a scaled image, for sliding-window evaluation
20
+ with `merge_semantic_crops`. With the shorter side scaled to the crop size, it matches the EoMT reference's windows,
21
+ except that its last crop always ends at the image edge: the reference's floating-point spacing stops a pixel short
22
+ for some sizes, such as a 1,347-pixel side with 448-pixel crops.
23
+
24
+ ### Changed
25
+
26
+ - Equimo requires JAX and jaxlib 0.11.1 or newer. With it, a compiled step that captures both a frozen encoder and its
27
+ images as constants matches eager execution, which JAX 0.8.1 did not.
28
+ - Attention pooling, attention-pooling heads, adapter fusion, the tabular decoder, and the `"softmax"` activation
29
+ compute softmax in at least float32 with the corrected derivative of `equimo.core.ops.softmax`, returning the model
30
+ dtype. Tabular attention computes its softmax in at least float32. Attention logits are cast before scaling, avoiding
31
+ a second bfloat16 rounding when the square root of the head dimension is not a power of two. Float32 forward results
32
+ and checkpoint structure are unchanged.
33
+
34
+ ### Fixed
35
+
36
+ - Query classification uses stable cross-entropy, so confident correct predictions retain nonzero losses and target
37
+ gradients.
38
+ - `selective_scan` returns the input dtype. Its float32 scan state leaked into bfloat16 outputs, unlike `ssd`.
39
+ - `merge_semantic_crops` raises a `ValueError` when the crops leave part of the scaled image uncovered. It returned NaN
40
+ scores there instead, and any final resize spread them to every output pixel, so the argmax labelled the whole image
41
+ class 0.
42
+
5
43
  ## [2.6.0] - 2026-10-17
6
44
 
7
45
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Equimo
3
- Version: 2.6.0
3
+ Version: 2.7.0
4
4
  Summary: Implementation of modern multimodal models in JAX/Equinox
5
5
  License-Expression: MIT AND Apache-2.0
6
6
  License-File: LICENSE.md
@@ -29,8 +29,8 @@ Classifier: Programming Language :: Python :: 3.14
29
29
  Requires-Dist: banax>=0.1.2
30
30
  Requires-Dist: einops>=0.8.0
31
31
  Requires-Dist: equinox>=0.13.3
32
- Requires-Dist: jax>=0.8.1
33
- Requires-Dist: jaxlib>=0.8.1
32
+ Requires-Dist: jax>=0.11.1
33
+ Requires-Dist: jaxlib>=0.11.1
34
34
  Requires-Dist: loguru>=0.7.3
35
35
  Requires-Dist: lz4>=4.4.3
36
36
  Requires-Dist: requests>=2.32.3
@@ -268,6 +268,7 @@ Minimal example:
268
268
  import jax
269
269
  import equimo.finetune as eqft
270
270
  import equimo.vision.models as em
271
+ from equimo.core.ops import softmax_cross_entropy_with_integer_labels
271
272
 
272
273
  key = jax.random.PRNGKey(0)
273
274
  model = em.vit_tiny_patch16_224(num_classes=10, key=key)
@@ -288,7 +289,7 @@ frozen = plan.frozen
288
289
  def loss_fn(trainable, batch):
289
290
  model = plan.combine(trainable)
290
291
  logits = jax.vmap(lambda x: model(x, key=key, inference=False))(batch["x"])
291
- return cross_entropy(logits, batch["y"]) # supplied by user code
292
+ return softmax_cross_entropy_with_integer_labels(logits, batch["y"]).mean()
292
293
  ```
293
294
 
294
295
  Frozen leaves are absent from `plan.trainable`; they are not assigned a zero learning rate. `plan.labels` and
@@ -997,14 +998,52 @@ import jax
997
998
  import jax.numpy as jnp
998
999
  import equinox as eqx
999
1000
 
1000
- model_bf16 = jax.tree_util.tree_map(
1001
- lambda leaf: leaf.astype(jnp.bfloat16) if eqx.is_inexact_array(leaf) else leaf,
1002
- model_fp32,
1003
- )
1001
+
1002
+ def to_bfloat16(tree):
1003
+ return jax.tree_util.tree_map(
1004
+ lambda leaf: leaf.astype(jnp.bfloat16) if eqx.is_inexact_array(leaf) else leaf,
1005
+ tree,
1006
+ )
1007
+
1008
+
1009
+ model_bf16 = to_bfloat16(model_fp32)
1010
+ ```
1011
+
1012
+ For training, keep float32 parameters and cast inside the loss. Gradients then return in float32 and the optimizer
1013
+ updates float32 values; bfloat16 parameters drop updates smaller than about 0.4% of their magnitude.
1014
+
1015
+ ```python
1016
+ from equimo.core.ops import softmax_cross_entropy_with_integer_labels
1017
+
1018
+
1019
+ @eqx.filter_value_and_grad
1020
+ def loss_and_grads(model_fp32, images, labels, key):
1021
+ model = to_bfloat16(model_fp32)
1022
+ keys = jax.random.split(key, images.shape[0])
1023
+ logits = jax.vmap(lambda x, k: model(x, key=k, inference=False))(images, keys)
1024
+ return softmax_cross_entropy_with_integer_labels(logits, labels).mean()
1025
+ ```
1026
+
1027
+ Numerically sensitive operations cast locally to float32 and return the model dtype: normalization statistics, rotary
1028
+ factors, and every softmax. Attention between tokens applies `jax.nn.softmax` to float32 logits. Other softmaxes
1029
+ (attention pooling, attention-pooling heads, adapter fusion, the tabular decoder, and the `"softmax"` activation) use
1030
+ `equimo.core.ops.softmax`, whose derivative also stays accurate for highly confident distributions. For the same reason,
1031
+ use the [`equimo.core.ops` classification losses](docs/finetuning/classification_losses.md). The
1032
+ [performance notes](docs/performance.md#softmax-precision) give the measurements behind these choices.
1033
+
1034
+ On NVIDIA GPUs with TF32 tensor cores (Ampere and newer), JAX's default precision may run float32 matrix multiplications
1035
+ as TF32. For float32 reference accuracy, run the computation under `jax.default_matmul_precision("highest")`.
1036
+
1037
+ On CUDA GPUs, bfloat16 and float16 models can run attention through cuDNN's fused kernel, which saves memory and time on
1038
+ long sequences. This is experimental, requires inactive attention dropout, and cannot be exported with jax2onnx:
1039
+
1040
+ ```python
1041
+ from equimo.core.layers import with_attention_implementation
1042
+
1043
+ model = with_attention_implementation(model_bf16, "cudnn") # "xla" switches back
1004
1044
  ```
1005
1045
 
1006
- Isolated `float32` upcasts are mandatory for numerically sensitive operations (softmax, layer norm variance). These are
1007
- applied internally where needed.
1046
+ See the [fused-attention measurements](docs/performance.md#fused-attention).
1008
1047
 
1009
1048
  ## Contributing
1010
1049
 
@@ -216,6 +216,7 @@ Minimal example:
216
216
  import jax
217
217
  import equimo.finetune as eqft
218
218
  import equimo.vision.models as em
219
+ from equimo.core.ops import softmax_cross_entropy_with_integer_labels
219
220
 
220
221
  key = jax.random.PRNGKey(0)
221
222
  model = em.vit_tiny_patch16_224(num_classes=10, key=key)
@@ -236,7 +237,7 @@ frozen = plan.frozen
236
237
  def loss_fn(trainable, batch):
237
238
  model = plan.combine(trainable)
238
239
  logits = jax.vmap(lambda x: model(x, key=key, inference=False))(batch["x"])
239
- return cross_entropy(logits, batch["y"]) # supplied by user code
240
+ return softmax_cross_entropy_with_integer_labels(logits, batch["y"]).mean()
240
241
  ```
241
242
 
242
243
  Frozen leaves are absent from `plan.trainable`; they are not assigned a zero learning rate. `plan.labels` and
@@ -945,14 +946,52 @@ import jax
945
946
  import jax.numpy as jnp
946
947
  import equinox as eqx
947
948
 
948
- model_bf16 = jax.tree_util.tree_map(
949
- lambda leaf: leaf.astype(jnp.bfloat16) if eqx.is_inexact_array(leaf) else leaf,
950
- model_fp32,
951
- )
949
+
950
+ def to_bfloat16(tree):
951
+ return jax.tree_util.tree_map(
952
+ lambda leaf: leaf.astype(jnp.bfloat16) if eqx.is_inexact_array(leaf) else leaf,
953
+ tree,
954
+ )
955
+
956
+
957
+ model_bf16 = to_bfloat16(model_fp32)
958
+ ```
959
+
960
+ For training, keep float32 parameters and cast inside the loss. Gradients then return in float32 and the optimizer
961
+ updates float32 values; bfloat16 parameters drop updates smaller than about 0.4% of their magnitude.
962
+
963
+ ```python
964
+ from equimo.core.ops import softmax_cross_entropy_with_integer_labels
965
+
966
+
967
+ @eqx.filter_value_and_grad
968
+ def loss_and_grads(model_fp32, images, labels, key):
969
+ model = to_bfloat16(model_fp32)
970
+ keys = jax.random.split(key, images.shape[0])
971
+ logits = jax.vmap(lambda x, k: model(x, key=k, inference=False))(images, keys)
972
+ return softmax_cross_entropy_with_integer_labels(logits, labels).mean()
973
+ ```
974
+
975
+ Numerically sensitive operations cast locally to float32 and return the model dtype: normalization statistics, rotary
976
+ factors, and every softmax. Attention between tokens applies `jax.nn.softmax` to float32 logits. Other softmaxes
977
+ (attention pooling, attention-pooling heads, adapter fusion, the tabular decoder, and the `"softmax"` activation) use
978
+ `equimo.core.ops.softmax`, whose derivative also stays accurate for highly confident distributions. For the same reason,
979
+ use the [`equimo.core.ops` classification losses](docs/finetuning/classification_losses.md). The
980
+ [performance notes](docs/performance.md#softmax-precision) give the measurements behind these choices.
981
+
982
+ On NVIDIA GPUs with TF32 tensor cores (Ampere and newer), JAX's default precision may run float32 matrix multiplications
983
+ as TF32. For float32 reference accuracy, run the computation under `jax.default_matmul_precision("highest")`.
984
+
985
+ On CUDA GPUs, bfloat16 and float16 models can run attention through cuDNN's fused kernel, which saves memory and time on
986
+ long sequences. This is experimental, requires inactive attention dropout, and cannot be exported with jax2onnx:
987
+
988
+ ```python
989
+ from equimo.core.layers import with_attention_implementation
990
+
991
+ model = with_attention_implementation(model_bf16, "cudnn") # "xla" switches back
952
992
  ```
953
993
 
954
- Isolated `float32` upcasts are mandatory for numerically sensitive operations (softmax, layer norm variance). These are
955
- applied internally where needed.
994
+ See the [fused-attention measurements](docs/performance.md#fused-attention).
956
995
 
957
996
  ## Contributing
958
997
 
@@ -30,6 +30,10 @@ tensor contract, and Equinox PyTree structure may change in a minor release whil
30
30
  adapter is developed. Such changes will be recorded in the changelog. Existing T0 archives remain covered by the
31
31
  checkpoint integrity and loading commitments below; this exception applies to the Python and raw-array API around them.
32
32
 
33
+ Fused cuDNN attention, selected with `equimo.core.layers.with_attention_implementation` or `implementation="cudnn"`, is
34
+ experimental. Its supported dtypes, masks, and module coverage follow cuDNN and `jax.nn.dot_product_attention` and may
35
+ change in a minor release. The default `implementation="xla"` is stable.
36
+
33
37
  Optional integrations remain optional. Their public Equimo wrappers are stable, but the supported Python range may be
34
38
  narrower when an upstream dependency does not publish wheels. Equimo documents and tests those exceptions explicitly.
35
39
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Equimo"
3
- version = "2.6.0"
3
+ version = "2.7.0"
4
4
  description = "Implementation of modern multimodal models in JAX/Equinox"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.15"
@@ -27,8 +27,8 @@ dependencies = [
27
27
  "banax>=0.1.2",
28
28
  "einops>=0.8.0",
29
29
  "equinox>=0.13.3",
30
- "jax>=0.8.1",
31
- "jaxlib>=0.8.1",
30
+ "jax>=0.11.1",
31
+ "jaxlib>=0.11.1",
32
32
  "loguru>=0.7.3",
33
33
  "lz4>=4.4.3",
34
34
  "requests>=2.32.3",
@@ -87,7 +87,7 @@ source-include = [
87
87
 
88
88
  [dependency-groups]
89
89
  dev = [
90
- "jax2onnx>=0.15.1",
90
+ "jax2onnx>=0.17.0",
91
91
  "mdformat==1.0.0",
92
92
  "mdformat-footnote==0.1.3",
93
93
  "mdformat-gfm==1.0.0",
@@ -0,0 +1 @@
1
+ __version__ = "2.7.0"
@@ -38,6 +38,7 @@ __all__ = [
38
38
  "register_ffn",
39
39
  "register_mixer",
40
40
  "register_norm",
41
+ "with_attention_implementation",
41
42
  ]
42
43
 
43
44
  from .activation import get_act, register_act
@@ -48,6 +49,7 @@ from .attention import (
48
49
  get_attn_block,
49
50
  register_attn,
50
51
  register_attn_block,
52
+ with_attention_implementation,
51
53
  )
52
54
  from .dropout import DropPath, DropPathAdd, get_dropout, register_dropout
53
55
  from .ffn import (
@@ -3,6 +3,8 @@ from typing import Callable, Optional
3
3
 
4
4
  import jax
5
5
 
6
+ from equimo.core.ops import softmax
7
+
6
8
  _ACT_REGISTRY: dict[str, Callable] = {}
7
9
 
8
10
 
@@ -65,5 +67,5 @@ _ACT_REGISTRY["elu"] = jax.nn.elu
65
67
  _ACT_REGISTRY["sigmoid"] = jax.nn.sigmoid
66
68
  _ACT_REGISTRY["hard_sigmoid"] = jax.nn.hard_sigmoid
67
69
  _ACT_REGISTRY["hard_swish"] = jax.nn.hard_swish
68
- _ACT_REGISTRY["softmax"] = jax.nn.softmax
70
+ _ACT_REGISTRY["softmax"] = softmax
69
71
  _ACT_REGISTRY["relu6"] = jax.nn.relu6
@@ -1,4 +1,5 @@
1
- from typing import Callable, List, Optional
1
+ import dataclasses
2
+ from typing import Callable, List, Literal, Optional, TypeVar
2
3
 
3
4
  import equinox as eqx
4
5
  import jax
@@ -59,6 +60,131 @@ def _broadcast_attention_mask(
59
60
  ) from error
60
61
 
61
62
 
63
+ AttentionImplementation = Literal["xla", "cudnn"]
64
+ _IMPLEMENTATIONS = ("xla", "cudnn")
65
+ # cuDNN's fused attention backward requires sequence lengths divisible by 64.
66
+ _CUDNN_SEQUENCE_MULTIPLE = 64
67
+
68
+ _Model = TypeVar("_Model")
69
+
70
+
71
+ def _check_implementation(implementation: str) -> None:
72
+ if implementation not in _IMPLEMENTATIONS:
73
+ raise ValueError(
74
+ f"implementation must be one of {_IMPLEMENTATIONS}, got {implementation!r}."
75
+ )
76
+
77
+
78
+ def _check_cudnn_dropout(
79
+ dropout: eqx.nn.Dropout | None, inference: Optional[bool]
80
+ ) -> None:
81
+ """Reject attention-probability dropout, which the fused kernel cannot apply."""
82
+ if dropout is None:
83
+ return
84
+ if inference is None:
85
+ inference = dropout.inference
86
+ if not inference and dropout.p != 0:
87
+ raise ValueError(
88
+ "implementation='cudnn' does not support attention dropout. Set the "
89
+ "attention dropout rate to 0, call with inference=True, or use "
90
+ "implementation='xla'."
91
+ )
92
+
93
+
94
+ def _fused_attention(
95
+ q: jax.Array,
96
+ k: jax.Array,
97
+ v: jax.Array,
98
+ allowed: jax.Array | None = None,
99
+ *,
100
+ implementation: AttentionImplementation = "cudnn",
101
+ ) -> jax.Array:
102
+ """Fused dot-product attention over ``(..., heads, seq, head_dim)`` inputs.
103
+
104
+ ``allowed`` broadcasts to ``(..., heads, query, key)``. Fully masked query rows
105
+ return zeros, as in the unfused path. Keys and values may have fewer heads
106
+ than queries if that number divides the query heads. ``implementation`` is
107
+ passed to :func:`jax.nn.dot_product_attention`; ``"xla"`` lets CPU tests
108
+ check the padding and masking.
109
+ """
110
+ if implementation == "cudnn" and q.dtype not in (jnp.bfloat16, jnp.float16):
111
+ raise ValueError(
112
+ "implementation='cudnn' requires bfloat16 or float16 attention inputs, "
113
+ f"got {q.dtype}. Cast the model or use implementation='xla'."
114
+ )
115
+ *leading, heads, query_len, head_dim = q.shape
116
+ key_len = k.shape[-2]
117
+ padded_query = query_len + -query_len % _CUDNN_SEQUENCE_MULTIPLE
118
+ padded_key = key_len + -key_len % _CUDNN_SEQUENCE_MULTIPLE
119
+
120
+ def batch_first(x: jax.Array, length: int) -> jax.Array:
121
+ x = jnp.swapaxes(x, -3, -2)
122
+ x = x.reshape(-1, *x.shape[-3:])
123
+ return jnp.pad(x, ((0, 0), (0, length - x.shape[1]), (0, 0), (0, 0)))
124
+
125
+ q = batch_first(q, padded_query)
126
+ k, v = batch_first(k, padded_key), batch_first(v, padded_key)
127
+ batch = q.shape[0]
128
+ if allowed is None:
129
+ out = jax.nn.dot_product_attention(
130
+ q,
131
+ k,
132
+ v,
133
+ query_seq_lengths=jnp.full((batch,), query_len, jnp.int32),
134
+ key_value_seq_lengths=jnp.full((batch,), key_len, jnp.int32),
135
+ implementation=implementation,
136
+ )
137
+ else:
138
+ # Materialize the mask for every head: a broadcast head axis fails under
139
+ # vmap (jax-ml/jax#34920). Padding adds masked queries and keys.
140
+ mask = jnp.broadcast_to(allowed, (*leading, heads, query_len, key_len))
141
+ mask = jnp.pad(
142
+ mask.reshape(batch, heads, query_len, key_len),
143
+ ((0, 0), (0, 0), (0, padded_query - query_len), (0, padded_key - key_len)),
144
+ )
145
+ out = jax.nn.dot_product_attention(
146
+ q, k, v, mask=mask, implementation=implementation
147
+ )
148
+ # cuDNN returns the mean value for fully masked rows instead of zeros.
149
+ has_key = jnp.swapaxes(jnp.any(mask, axis=-1), 1, 2)
150
+ out = jnp.where(has_key[..., None], out, 0)
151
+ out = out[:, :query_len].reshape(*leading, query_len, heads, head_dim)
152
+ return jnp.swapaxes(out, -3, -2)
153
+
154
+
155
+ def with_attention_implementation(
156
+ model: _Model, implementation: AttentionImplementation
157
+ ) -> _Model:
158
+ """Return ``model`` with every attention module using ``implementation``.
159
+
160
+ ``"cudnn"`` runs core and tabular attention through NVIDIA cuDNN's fused
161
+ kernel (:func:`jax.nn.dot_product_attention`), which computes logits in
162
+ float32 without materializing them. It requires a CUDA GPU, bfloat16 or
163
+ float16 inputs, and inactive attention dropout, and it cannot be exported
164
+ with jax2onnx; switch back to ``"xla"`` before exporting. Parameters and
165
+ checkpoint layouts are unchanged. The setting is static, so compiled
166
+ functions retrace after switching.
167
+ """
168
+ _check_implementation(implementation)
169
+
170
+ def is_attention(node) -> bool:
171
+ return isinstance(node, eqx.Module) and any(
172
+ field.name == "implementation" for field in dataclasses.fields(node)
173
+ )
174
+
175
+ def switch(node):
176
+ if not is_attention(node):
177
+ return node
178
+ # tree_at cannot replace static fields, and copy.copy returns the same
179
+ # module, so rebuild the instance as Equinox's unflattening does.
180
+ switched = object.__new__(type(node))
181
+ switched.__dict__.update(node.__dict__)
182
+ object.__setattr__(switched, "implementation", implementation)
183
+ return switched
184
+
185
+ return jax.tree.map(switch, model, is_leaf=is_attention)
186
+
187
+
62
188
  @register_attn()
63
189
  class Attention(eqx.Module):
64
190
  """Multi-head self attention over tensors shaped ``(..., seq, dim)``.
@@ -67,11 +193,15 @@ class Attention(eqx.Module):
67
193
  batch-aligned with shape ``(..., query, key)`` or directly broadcastable to
68
194
  ``(..., heads, query, key)``. Fully masked query rows receive zero attention
69
195
  probability mass.
196
+
197
+ ``implementation="cudnn"`` selects the fused kernel described in
198
+ :func:`with_attention_implementation`.
70
199
  """
71
200
 
72
201
  dim: int = eqx.field(static=True)
73
202
  num_heads: int = eqx.field(static=True)
74
203
  head_dim: int = eqx.field(static=True)
204
+ implementation: AttentionImplementation = eqx.field(static=True)
75
205
 
76
206
  qkv: eqx.nn.Linear
77
207
  proj: eqx.nn.Linear
@@ -93,12 +223,15 @@ class Attention(eqx.Module):
93
223
  proj_drop: float = 0.0,
94
224
  norm_layer: str | type[eqx.Module] = "layernorm",
95
225
  eps: float = 1e-5,
226
+ implementation: AttentionImplementation = "xla",
96
227
  **kwargs,
97
228
  ):
98
229
  self.num_heads = num_heads
99
230
  self.head_dim = dim // num_heads
100
231
  self.dim = dim
101
232
  assert self.head_dim * num_heads == dim, "dim must be divisible by num_heads"
233
+ _check_implementation(implementation)
234
+ self.implementation = implementation
102
235
 
103
236
  key_qkv, key_proj = jr.split(key, 2)
104
237
  norm_layer = get_norm(norm_layer)
@@ -139,23 +272,24 @@ class Attention(eqx.Module):
139
272
  )
140
273
  q, k = apply_rotary_qk(q, k, rotary)
141
274
 
142
- attn = (
143
- jnp.einsum("...hqd,...hkd->...hqk", q, k) / jnp.sqrt(self.head_dim)
144
- ).astype(jnp.float32)
275
+ allowed = None
145
276
  if mask is not None:
146
277
  allowed = _broadcast_attention_mask(
147
278
  mask,
148
279
  x_ndim=x.ndim,
149
- shape=attn.shape,
280
+ shape=(*q.shape[:-1], k.shape[-2]),
150
281
  )
151
- attn = jnp.where(allowed, attn, -jnp.inf)
152
- attn = jax.nn.softmax(attn, axis=-1)
153
- if mask is not None:
154
- attn = jnp.where(jnp.any(allowed, axis=-1, keepdims=True), attn, 0)
155
- attn = attn.astype(x.dtype)
156
- attn = self.attn_drop(attn, inference=inference, key=key1)
282
+ if self.implementation == "cudnn":
283
+ _check_cudnn_dropout(self.attn_drop, inference)
284
+ x = _fused_attention(q, k, v, allowed)
285
+ else:
286
+ attn = jnp.einsum("...hqd,...hkd->...hqk", q, k).astype(
287
+ jnp.float32
288
+ ) / jnp.sqrt(self.head_dim)
289
+ attn = jax.nn.softmax(attn, axis=-1, where=allowed).astype(x.dtype)
290
+ attn = self.attn_drop(attn, inference=inference, key=key1)
291
+ x = jnp.einsum("...hqk,...hkd->...hqd", attn, v)
157
292
 
158
- x = jnp.einsum("...hqk,...hkd->...hqd", attn, v)
159
293
  x = jnp.swapaxes(x, -3, -2).reshape(*x.shape[:-3], x.shape[-2], self.dim)
160
294
  x = _apply_module_last_dim(self.proj, x)
161
295
  return self.proj_drop(x, inference=inference, key=key2)
@@ -0,0 +1,18 @@
1
+ from .scan import non_causal_linear_attn, segsum, selective_scan, ssd
2
+ from .probability import (
3
+ log_softmax,
4
+ softmax,
5
+ softmax_cross_entropy_with_integer_labels,
6
+ softmax_cross_entropy_with_logit_targets,
7
+ )
8
+
9
+ __all__ = [
10
+ "non_causal_linear_attn",
11
+ "segsum",
12
+ "selective_scan",
13
+ "ssd",
14
+ "log_softmax",
15
+ "softmax",
16
+ "softmax_cross_entropy_with_integer_labels",
17
+ "softmax_cross_entropy_with_logit_targets",
18
+ ]