Equimo 2.3.0__tar.gz → 2.4.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 (171) hide show
  1. {equimo-2.3.0 → equimo-2.4.0}/CHANGELOG.md +9 -0
  2. {equimo-2.3.0 → equimo-2.4.0}/NOTICE +12 -0
  3. {equimo-2.3.0 → equimo-2.4.0}/PKG-INFO +1 -1
  4. {equimo-2.3.0 → equimo-2.4.0}/pyproject.toml +1 -1
  5. equimo-2.4.0/src/equimo/__init__.py +1 -0
  6. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/pmd.py +72 -23
  7. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/pmt.py +13 -4
  8. equimo-2.4.0/src/equimo/vision/query_training/__init__.py +40 -0
  9. equimo-2.4.0/src/equimo/vision/query_training/_assignment.py +130 -0
  10. equimo-2.4.0/src/equimo/vision/query_training/_loss.py +523 -0
  11. equimo-2.4.0/src/equimo/vision/query_training/_sampling.py +162 -0
  12. equimo-2.3.0/src/equimo/__init__.py +0 -1
  13. {equimo-2.3.0 → equimo-2.4.0}/LICENSE.md +0 -0
  14. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/Apache-2.0.txt +0 -0
  15. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/pretrained/README.md +0 -0
  16. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/pretrained/ast-BSD-3-Clause.txt +0 -0
  17. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/pretrained/convnext-Apache-2.0.txt +0 -0
  18. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/pretrained/convnextv2-CC-BY-NC-4.0.txt +0 -0
  19. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/pretrained/dinov2-Apache-2.0.txt +0 -0
  20. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/pretrained/dinov3-License.md +0 -0
  21. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/pretrained/eupe-FAIR-Noncommercial-Research-License.md +0 -0
  22. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/pretrained/lingbot-vision-Apache-2.0.txt +0 -0
  23. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/pretrained/siglip2-Apache-2.0.txt +0 -0
  24. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/pretrained/t0-alpha-Apache-2.0.txt +0 -0
  25. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/pretrained/tabpfn-3-License-v1.0.txt +0 -0
  26. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/pretrained/tips-CC-BY-4.0.txt +0 -0
  27. {equimo-2.3.0 → equimo-2.4.0}/LICENSES/tfc-t0-APACHE-2.0.txt +0 -0
  28. {equimo-2.3.0 → equimo-2.4.0}/README.md +0 -0
  29. {equimo-2.3.0 → equimo-2.4.0}/docs/licensing/t0-source-provenance.md +0 -0
  30. {equimo-2.3.0 → equimo-2.4.0}/docs/migration-v2.md +0 -0
  31. {equimo-2.3.0 → equimo-2.4.0}/docs/stability.md +0 -0
  32. {equimo-2.3.0 → equimo-2.4.0}/docs/timeseries.md +0 -0
  33. {equimo-2.3.0 → equimo-2.4.0}/models/huggingface/NOTICE +0 -0
  34. {equimo-2.3.0 → equimo-2.4.0}/models/huggingface/README.md +0 -0
  35. {equimo-2.3.0 → equimo-2.4.0}/models/huggingface/convnext-conversion.json +0 -0
  36. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/_checkpoint_limits.py +0 -0
  37. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/_io.py +0 -0
  38. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/_pretrained.py +0 -0
  39. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/audio/__init__.py +0 -0
  40. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/audio/io.py +0 -0
  41. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/audio/layers/__init__.py +0 -0
  42. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/audio/layers/patch.py +0 -0
  43. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/audio/models/__init__.py +0 -0
  44. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/audio/models/ast.py +0 -0
  45. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/catalog.py +0 -0
  46. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/conversion/__init__.py +0 -0
  47. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/conversion/utils.py +0 -0
  48. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/__init__.py +0 -0
  49. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/_prng.py +0 -0
  50. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/emamodel.py +0 -0
  51. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/factory.py +0 -0
  52. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/implicit/__init__.py +0 -0
  53. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/implicit/_base.py +0 -0
  54. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/implicit/deq.py +0 -0
  55. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/implicit/injectors.py +0 -0
  56. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/implicit/stabilizers.py +0 -0
  57. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/implicit/strategies.py +0 -0
  58. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/intermediates.py +0 -0
  59. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/layers/__init__.py +0 -0
  60. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/layers/_registry.py +0 -0
  61. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/layers/activation.py +0 -0
  62. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/layers/attention.py +0 -0
  63. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/layers/dropout.py +0 -0
  64. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/layers/ffn.py +0 -0
  65. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/layers/generic.py +0 -0
  66. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/layers/mamba.py +0 -0
  67. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/layers/norm.py +0 -0
  68. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/layers/rotary.py +0 -0
  69. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/ops/__init__.py +0 -0
  70. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/core/ops/scan.py +0 -0
  71. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/__init__.py +0 -0
  72. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/_blocks.py +0 -0
  73. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/_serialization_codecs.py +0 -0
  74. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/_typing.py +0 -0
  75. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/audio/__init__.py +0 -0
  76. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/audio/recipes.py +0 -0
  77. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/audio/selectors.py +0 -0
  78. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/calibration.py +0 -0
  79. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/config.py +0 -0
  80. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/continued_ssl.py +0 -0
  81. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/feature_extraction.py +0 -0
  82. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/heads.py +0 -0
  83. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/inspection.py +0 -0
  84. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/labels.py +0 -0
  85. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/language/__init__.py +0 -0
  86. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/language/recipes.py +0 -0
  87. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/language/selectors.py +0 -0
  88. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/masks.py +0 -0
  89. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/merging.py +0 -0
  90. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/paths.py +0 -0
  91. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/__init__.py +0 -0
  92. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/_common.py +0 -0
  93. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/_compat.py +0 -0
  94. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/adapters.py +0 -0
  95. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/base.py +0 -0
  96. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/bitfit.py +0 -0
  97. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/dora.py +0 -0
  98. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/ia3.py +0 -0
  99. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/lora.py +0 -0
  100. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/prefix.py +0 -0
  101. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/prompts.py +0 -0
  102. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/scale_shift.py +0 -0
  103. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/peft/vera.py +0 -0
  104. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/pooling.py +0 -0
  105. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/recipes.py +0 -0
  106. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/regularization.py +0 -0
  107. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/selectors.py +0 -0
  108. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/serialization.py +0 -0
  109. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/surgery.py +0 -0
  110. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/tags.py +0 -0
  111. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/vision/__init__.py +0 -0
  112. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/vision/dense.py +0 -0
  113. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/vision/eomt.py +0 -0
  114. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/finetune/vision/pmt.py +0 -0
  115. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/language/__init__.py +0 -0
  116. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/language/models.py +0 -0
  117. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/language/tokenizers.py +0 -0
  118. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/registry.py +0 -0
  119. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/serialization.py +0 -0
  120. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/tabular/__init__.py +0 -0
  121. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/tabular/layers/__init__.py +0 -0
  122. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/tabular/layers/attention.py +0 -0
  123. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/tabular/layers/blocks.py +0 -0
  124. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/tabular/layers/decoder.py +0 -0
  125. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/tabular/layers/mlp.py +0 -0
  126. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/tabular/layers/preprocessing.py +0 -0
  127. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/tabular/layers/registry.py +0 -0
  128. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/tabular/models/__init__.py +0 -0
  129. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/tabular/models/tabpfn.py +0 -0
  130. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/timeseries/__init__.py +0 -0
  131. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/timeseries/layers/__init__.py +0 -0
  132. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/timeseries/layers/axis_attn.py +0 -0
  133. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/timeseries/layers/blocks.py +0 -0
  134. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/timeseries/layers/patch_encoder.py +0 -0
  135. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/timeseries/layers/registry.py +0 -0
  136. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/timeseries/layers/residual_mlp.py +0 -0
  137. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/timeseries/models/__init__.py +0 -0
  138. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/timeseries/models/t0.py +0 -0
  139. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/utils.py +0 -0
  140. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/__init__.py +0 -0
  141. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/_encoder_identity.py +0 -0
  142. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/io.py +0 -0
  143. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/layers/__init__.py +0 -0
  144. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/layers/attention.py +0 -0
  145. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/layers/convolution.py +0 -0
  146. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/layers/downsample.py +0 -0
  147. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/layers/patch.py +0 -0
  148. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/layers/posemb.py +0 -0
  149. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/layers/registry.py +0 -0
  150. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/layers/squeeze_excite.py +0 -0
  151. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/layers/wavelet.py +0 -0
  152. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/__init__.py +0 -0
  153. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/_embedding.py +0 -0
  154. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/_features.py +0 -0
  155. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/attnet.py +0 -0
  156. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/convnext.py +0 -0
  157. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/deq.py +0 -0
  158. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/eomt.py +0 -0
  159. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/fastervit.py +0 -0
  160. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/iformer.py +0 -0
  161. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/lowformer.py +0 -0
  162. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/mlla.py +0 -0
  163. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/mobilenet.py +0 -0
  164. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/parcae.py +0 -0
  165. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/partialformer.py +0 -0
  166. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/reduceformer.py +0 -0
  167. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/shvit.py +0 -0
  168. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/vit.py +0 -0
  169. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/models/vssd.py +0 -0
  170. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/pmt_checkpoint.py +0 -0
  171. {equimo-2.3.0 → equimo-2.4.0}/src/equimo/vision/segmentation.py +0 -0
@@ -2,6 +2,15 @@
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.4.0] - 2026-09-24
6
+
7
+ ### Added
8
+
9
+ - JAX-native query-segmentation assignment, validity-aware point sampling, class/mask/Dice losses, auxiliary-output
10
+ composition, and inspectable batch/device reductions, with pinned upstream numerical references.
11
+ - Padding-aware PMT BatchNorm and configurable collective axes for synchronized statistics, preserving default model
12
+ checkpoint layouts.
13
+
5
14
  ## [2.3.0] - 2026-09-24
6
15
 
7
16
  ### Added
@@ -23,6 +23,18 @@ Source: https://github.com/tensorflow/models/blob/b41a080d61cd4298fea9894804e965
23
23
  Equimo contributors modified the upstream code's formatting and type
24
24
  annotations.
25
25
 
26
+ Query assignment
27
+ ----------------
28
+
29
+ src/equimo/vision/query_training/_assignment.py adapts the Hungarian algorithm
30
+ from Optax, licensed under Apache-2.0 (LICENSES/Apache-2.0.txt).
31
+ Copyright 2024 DeepMind Technologies Limited. All Rights Reserved.
32
+ Source: https://github.com/google-deepmind/optax/blob/225a7079f4630bf75bee94ec78de9aa69c60fab3/optax/assignment/_hungarian_algorithm.py
33
+
34
+ Equimo contributors added target-slot validity, bounded loops, numerical-failure reporting,
35
+ fixed-capacity target-ordered outputs, explicit float32 arithmetic and stopped
36
+ gradients, and documented deterministic tie handling.
37
+
26
38
  LingBot-Vision
27
39
  -------------
28
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Equimo
3
- Version: 2.3.0
3
+ Version: 2.4.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Equimo"
3
- version = "2.3.0"
3
+ version = "2.4.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"
@@ -0,0 +1 @@
1
+ __version__ = "2.4.0"
@@ -29,6 +29,7 @@ from equimo.vision.segmentation import (
29
29
 
30
30
 
31
31
  _MISSING = object()
32
+ NormOption = float | bool | str | tuple[str, ...]
32
33
 
33
34
 
34
35
  @dataclass(frozen=True)
@@ -57,7 +58,7 @@ class PMTConfig:
57
58
  num_blocks: int = 6
58
59
  norm_layer: str = "groupnorm"
59
60
  norm_max_group: int = 32
60
- norm_kwargs: tuple[tuple[str, float | bool], ...] | Mapping[str, float | bool] = ()
61
+ norm_kwargs: tuple[tuple[str, NormOption], ...] | Mapping[str, NormOption] = ()
61
62
  masked_attention: bool = True
62
63
 
63
64
  def __post_init__(self) -> None:
@@ -96,7 +97,7 @@ class PMTConfig:
96
97
  raise ValueError("Unsupported PMT lateral norm_layer.")
97
98
  allowed = {
98
99
  "groupnorm": {"eps", "channelwise_affine"},
99
- "batchnorm": {"eps", "momentum"},
100
+ "batchnorm": {"eps", "momentum", "axis_name"},
100
101
  "layernorm": {"eps", "use_weight", "use_bias"},
101
102
  "rmsnorm": {"eps", "use_weight", "use_bias"},
102
103
  "none": set(),
@@ -179,11 +180,14 @@ def anneal_pmt_mask_state(
179
180
 
180
181
 
181
182
  class ReferenceBatchNorm(eqx.nn.StatefulLayer):
182
- """Single-host counterpart of the author's lateral SyncBatchNorm.
183
+ """Padding-aware counterpart of the author's lateral SyncBatchNorm.
183
184
 
184
185
  Call over one image inside ``jax.vmap(..., axis_name="pmt_batch")`` in
185
186
  training. Statistics cover batch and tokens, including prefix tokens.
186
187
  Inference uses the stored running mean and unbiased running variance.
188
+ Supply multiple axis names to synchronize over a local batch and devices.
189
+ False ``example_valid`` excludes an image from moments and counts. A global
190
+ batch with no valid observations returns zeros and leaves state unchanged.
187
191
  """
188
192
 
189
193
  weight: jax.Array
@@ -193,8 +197,16 @@ class ReferenceBatchNorm(eqx.nn.StatefulLayer):
193
197
  count_index: eqx.nn.StateIndex
194
198
  eps: float = eqx.field(static=True)
195
199
  momentum: float = eqx.field(static=True)
200
+ axis_name: str | tuple[str, ...] = eqx.field(static=True)
196
201
 
197
- def __init__(self, dim: int, *, eps: float = 1e-5, momentum: float = 0.1):
202
+ def __init__(
203
+ self,
204
+ dim: int,
205
+ *,
206
+ eps: float = 1e-5,
207
+ momentum: float = 0.1,
208
+ axis_name: str | tuple[str, ...] = "pmt_batch",
209
+ ):
198
210
  if dim <= 0 or eps <= 0 or not 0 < momentum <= 1:
199
211
  raise ValueError("Invalid reference BatchNorm dimensions or options.")
200
212
  self.weight = jnp.ones((dim,), dtype=jnp.float32)
@@ -204,39 +216,70 @@ class ReferenceBatchNorm(eqx.nn.StatefulLayer):
204
216
  self.count_index = eqx.nn.StateIndex(jnp.asarray(0, dtype=jnp.int32))
205
217
  self.eps = eps
206
218
  self.momentum = momentum
219
+ names = (axis_name,) if isinstance(axis_name, str) else axis_name
220
+ if (
221
+ not isinstance(names, tuple)
222
+ or not names
223
+ or any(not isinstance(name, str) or not name for name in names)
224
+ or len(set(names)) != len(names)
225
+ ):
226
+ raise ValueError(
227
+ "BatchNorm axis_name requires nonempty distinct axis names."
228
+ )
229
+ self.axis_name = axis_name
207
230
 
208
231
  def __call__(
209
- self, x: jax.Array, state: eqx.nn.State, *, inference: bool
232
+ self,
233
+ x: jax.Array,
234
+ state: eqx.nn.State,
235
+ *,
236
+ inference: bool,
237
+ example_valid: bool | jax.Array = True,
210
238
  ) -> tuple[jax.Array, eqx.nn.State]:
211
239
  if x.ndim != 2 or x.shape[0] != self.weight.shape[0]:
212
240
  raise ValueError("Reference BatchNorm expects (channels, tokens).")
213
- value = x.astype(jnp.float32)
241
+ valid = jnp.asarray(example_valid)
242
+ if valid.shape != () or valid.dtype != jnp.bool_:
243
+ raise ValueError("BatchNorm example_valid must be a boolean scalar.")
244
+ value = jnp.where(valid, x, 0).astype(jnp.float32)
214
245
  if inference:
215
246
  mean = state.get(self.mean_index)
216
247
  variance = state.get(self.variance_index)
217
248
  else:
218
- if x.shape[1] * lax.axis_size("pmt_batch") < 2:
249
+ if example_valid is True and x.shape[1] * lax.axis_size(self.axis_name) < 2:
219
250
  raise ValueError("BatchNorm requires at least two observations.")
220
- mean = lax.pmean(jnp.mean(value, axis=1), "pmt_batch")
221
- variance = lax.pmean(
222
- jnp.mean(jnp.square(value - mean[:, None]), axis=1), "pmt_batch"
251
+ count = lax.psum(valid.astype(jnp.float32) * x.shape[1], self.axis_name)
252
+ count = eqx.error_if(
253
+ count, count == 1, "BatchNorm requires at least two valid observations."
254
+ )
255
+ denominator = jnp.maximum(count, 1)
256
+ mean = lax.psum(jnp.sum(value, axis=1), self.axis_name) / denominator
257
+ centered = jnp.where(valid, value - mean[:, None], 0)
258
+ variance = (
259
+ lax.psum(jnp.sum(jnp.square(centered), axis=1), self.axis_name)
260
+ / denominator
223
261
  )
224
- count = lax.psum(jnp.asarray(x.shape[1], dtype=jnp.float32), "pmt_batch")
225
- unbiased = variance * count / (count - 1)
262
+ unbiased = variance * count / jnp.maximum(count - 1, 1)
263
+ old_mean = state.get(self.mean_index)
264
+ old_variance = state.get(self.variance_index)
265
+ mean_update = (1 - self.momentum) * old_mean + self.momentum * mean
266
+ variance_update = (
267
+ 1 - self.momentum
268
+ ) * old_variance + self.momentum * unbiased
226
269
  state = state.set(
227
- self.mean_index,
228
- (1 - self.momentum) * state.get(self.mean_index) + self.momentum * mean,
270
+ self.mean_index, jnp.where(count > 0, mean_update, old_mean)
229
271
  )
230
272
  state = state.set(
231
- self.variance_index,
232
- (1 - self.momentum) * state.get(self.variance_index)
233
- + self.momentum * unbiased,
273
+ self.variance_index, jnp.where(count > 0, variance_update, old_variance)
274
+ )
275
+ state = state.set(
276
+ self.count_index,
277
+ state.get(self.count_index) + (count > 0).astype(jnp.int32),
234
278
  )
235
- state = state.set(self.count_index, state.get(self.count_index) + 1)
236
279
  normalized = (value - mean[:, None]) * lax.rsqrt(variance[:, None] + self.eps)
237
- return (normalized * self.weight[:, None] + self.bias[:, None]).astype(
238
- x.dtype
239
- ), state
280
+ return jnp.where(
281
+ valid, normalized * self.weight[:, None] + self.bias[:, None], 0
282
+ ).astype(x.dtype), state
240
283
 
241
284
 
242
285
  class PMDAttention(Attention):
@@ -295,10 +338,13 @@ class Lateral(eqx.Module):
295
338
  state: eqx.nn.State | None,
296
339
  *,
297
340
  inference: bool,
341
+ example_valid: bool | jax.Array = True,
298
342
  ) -> tuple[jax.Array, eqx.nn.State | None]:
299
343
  if self.norm_layer == "batchnorm":
300
344
  assert state is not None
301
- normalized, state = self.norm(tokens.T, state, inference=inference)
345
+ normalized, state = self.norm(
346
+ tokens.T, state, inference=inference, example_valid=example_valid
347
+ )
302
348
  normalized = normalized.T
303
349
  elif self.norm_layer in ("groupnorm", "none"):
304
350
  normalized = self.norm(tokens.T).T
@@ -397,6 +443,7 @@ class PlainMaskDecoder(eqx.Module):
397
443
  inference: bool = True,
398
444
  key: jax.Array | None = None,
399
445
  mask_state: PMTMaskState | None = None,
446
+ example_valid: bool | jax.Array = True,
400
447
  ) -> PMTOutput | tuple[PMTOutput, eqx.nn.State | None]:
401
448
  """Run PMD; explicit state gives the same tuple interface for every norm."""
402
449
  supplied = state is not _MISSING
@@ -431,7 +478,9 @@ class PlainMaskDecoder(eqx.Module):
431
478
  )
432
479
  lateral = []
433
480
  for layer, tokens in zip(self.lateral, features.levels, strict=True):
434
- result, running_state = layer(tokens, running_state, inference=inference)
481
+ result, running_state = layer(
482
+ tokens, running_state, inference=inference, example_valid=example_valid
483
+ )
435
484
  lateral.append(result)
436
485
  tokens = jnp.sum(jnp.stack(lateral), axis=0)
437
486
  tokens = jnp.concatenate((self.queries.astype(tokens.dtype), tokens), axis=0)
@@ -31,9 +31,10 @@ class PMT(eqx.Module):
31
31
 
32
32
  ``norm_layer="batchnorm"`` reproduces the author's lateral architecture.
33
33
  Construct that model with ``eqx.nn.make_with_state(PMT)`` and pass its state
34
- on each call. Training must vmap across real images with
35
- ``axis_name="pmt_batch"``. Normalization runs after ``encode`` so cached
36
- frozen features can be reused while the decoder learns.
34
+ on each call. Training uses a named batch axis (default ``pmt_batch``).
35
+ Pass ``example_valid=False`` for padding; ``norm_kwargs['axis_name']`` may
36
+ include device axes for synchronized statistics. Normalization runs after
37
+ ``encode`` so cached frozen features can be reused while the decoder learns.
37
38
  """
38
39
 
39
40
  backbone: VisionTransformer
@@ -158,6 +159,7 @@ class PMT(eqx.Module):
158
159
  inference: bool = True,
159
160
  key: jax.Array | None = None,
160
161
  mask_state: PMTMaskState | None = None,
162
+ example_valid: bool | jax.Array = True,
161
163
  ) -> PMTOutput | tuple[PMTOutput, eqx.nn.State | None]:
162
164
  """Decode retained features with the selected normalization state."""
163
165
  if (
@@ -169,7 +171,12 @@ class PMT(eqx.Module):
169
171
  ):
170
172
  raise ValueError("PMT features disagree with encoder or input view.")
171
173
  return self.head(
172
- features, state, inference=inference, key=key, mask_state=mask_state
174
+ features,
175
+ state,
176
+ inference=inference,
177
+ key=key,
178
+ mask_state=mask_state,
179
+ example_valid=example_valid,
173
180
  )
174
181
 
175
182
  def __call__(
@@ -180,6 +187,7 @@ class PMT(eqx.Module):
180
187
  inference: bool = True,
181
188
  key: jax.Array | None = None,
182
189
  mask_state: PMTMaskState | None = None,
190
+ example_valid: bool | jax.Array = True,
183
191
  ) -> PMTOutput | tuple[PMTOutput, eqx.nn.State | None]:
184
192
  """Return mask/class predictions; state is optional with GroupNorm."""
185
193
  return self.decode(
@@ -188,6 +196,7 @@ class PMT(eqx.Module):
188
196
  inference=inference,
189
197
  key=key,
190
198
  mask_state=mask_state,
199
+ example_valid=example_valid,
191
200
  )
192
201
 
193
202
 
@@ -0,0 +1,40 @@
1
+ """Optimizer-independent, JAX-native query-segmentation training utilities."""
2
+
3
+ from ._assignment import QueryAssignment, match_queries
4
+ from ._sampling import PointSamples, sample_mask_points, sample_query_points
5
+ from ._loss import (
6
+ LossTerm,
7
+ MatchingCosts,
8
+ PredictionLoss,
9
+ PredictionLossContext,
10
+ QueryLossConfig,
11
+ QueryLossContext,
12
+ QueryLossResult,
13
+ QueryTargets,
14
+ prepare_query_loss,
15
+ query_matching_costs,
16
+ query_prediction_loss,
17
+ query_segmentation_loss,
18
+ reduce_query_losses,
19
+ )
20
+
21
+ __all__ = [
22
+ "LossTerm",
23
+ "MatchingCosts",
24
+ "PointSamples",
25
+ "PredictionLoss",
26
+ "PredictionLossContext",
27
+ "QueryAssignment",
28
+ "QueryLossConfig",
29
+ "QueryLossContext",
30
+ "QueryLossResult",
31
+ "QueryTargets",
32
+ "match_queries",
33
+ "prepare_query_loss",
34
+ "query_matching_costs",
35
+ "query_prediction_loss",
36
+ "query_segmentation_loss",
37
+ "reduce_query_losses",
38
+ "sample_mask_points",
39
+ "sample_query_points",
40
+ ]
@@ -0,0 +1,130 @@
1
+ # Copyright 2024 DeepMind Technologies Limited. All Rights Reserved.
2
+ # Copyright 2026 Equimo contributors.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """Padded rectangular assignment, adapted from Optax's Hungarian algorithm.
5
+
6
+ Reference: google-deepmind/optax, revision
7
+ 225a7079f4630bf75bee94ec78de9aa69c60fab3,
8
+ optax/assignment/_hungarian_algorithm.py. See NOTICE for modifications.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import equinox as eqx
14
+ import jax
15
+ import jax.numpy as jnp
16
+ from jax import lax
17
+
18
+
19
+ class QueryAssignment(eqx.Module):
20
+ """Fixed-capacity matches in target order.
21
+
22
+ ``query_indices[t]`` is the query matched to target slot ``t``. Invalid
23
+ slots have query index ``-1``. ``is_valid`` is false for nonfinite
24
+ active costs or internal arithmetic overflow; no matches are returned in
25
+ that case. Indices and decisions
26
+ are nondifferentiable. Ties use the lowest available search index, keeping
27
+ an existing predecessor on equal costs; this is not a lexicographic solver.
28
+ """
29
+
30
+ query_indices: jax.Array
31
+ valid: jax.Array
32
+ is_valid: jax.Array
33
+
34
+
35
+ def match_queries(costs: jax.Array, target_valid: jax.Array) -> QueryAssignment:
36
+ """Minimize ``(Q,T)`` costs with ``T <= Q``, entirely on the JAX device.
37
+
38
+ Every valid target is assigned a distinct query. Invalid target columns
39
+ are ignored, including their contents. Complexity is O(T**2 * Q), with
40
+ O(T * Q) cost storage. No host solver, callback, or differentiation through
41
+ assignment is used. Nonfinite active costs or internal arithmetic overflow
42
+ produce ``is_valid=False``. Every augmenting-path loop is explicitly bounded.
43
+ """
44
+ if costs.ndim != 2 or target_valid.shape != (costs.shape[1],):
45
+ raise ValueError("Expected (queries, targets) costs and (targets,) validity.")
46
+ queries, targets = costs.shape
47
+ if queries == 0 or targets > queries:
48
+ raise ValueError("Assignment requires 0 <= target capacity <= query count.")
49
+ if target_valid.dtype != jnp.bool_:
50
+ raise TypeError("Target validity must be boolean.")
51
+ matrix = lax.stop_gradient(costs.astype(jnp.float32).T)
52
+ finite = jnp.all(jnp.where(target_valid[:, None], jnp.isfinite(matrix), True))
53
+ active = target_valid & finite
54
+ matrix = jnp.where(active[:, None], matrix, 0)
55
+ if targets == 0:
56
+ return QueryAssignment(jnp.zeros((0,), dtype=jnp.int32), active, finite)
57
+
58
+ def insert(row, state):
59
+ def augment(state):
60
+ row_potential, column_potential, owner, feasible = state
61
+ owner = owner.at[0].set(row + 1)
62
+
63
+ def search(carry):
64
+ u, v, visited, distance, previous, column, steps = carry
65
+ visited = visited.at[column].set(True)
66
+ available = ~visited[1:]
67
+ reduced = matrix[owner[column] - 1] - u[owner[column]] - v[1:]
68
+ improves = available & (reduced < distance)
69
+ previous = jnp.where(improves, column, previous)
70
+ distance = jnp.where(improves, reduced, distance)
71
+ candidate = jnp.where(available, distance, jnp.inf)
72
+ next_column = jnp.argmin(candidate).astype(jnp.int32) + 1
73
+ delta = candidate[next_column - 1]
74
+ # Out-of-range scatter updates are dropped; unvisited columns
75
+ # must not repeatedly add to the dummy row's potential.
76
+ indices = jnp.where(visited, owner, targets + 1)
77
+ u = u.at[indices].add(delta, mode="drop")
78
+ v = jnp.where(visited, v - delta, v)
79
+ distance = jnp.where(available, distance - delta, distance)
80
+ return u, v, visited, distance, previous, next_column, steps + 1
81
+
82
+ def searching(carry):
83
+ *_, column, steps = carry
84
+ return (owner[column] != 0) & (steps <= row)
85
+
86
+ initial = (
87
+ row_potential,
88
+ column_potential,
89
+ jnp.zeros((queries + 1,), dtype=jnp.bool_),
90
+ jnp.full((queries,), jnp.inf, dtype=jnp.float32),
91
+ jnp.zeros((queries,), dtype=jnp.int32),
92
+ jnp.asarray(0, dtype=jnp.int32),
93
+ jnp.asarray(0, dtype=jnp.int32),
94
+ )
95
+ u, v, _, _, previous, column, _ = lax.while_loop(searching, search, initial)
96
+ feasible &= (
97
+ (owner[column] == 0)
98
+ & jnp.all(jnp.isfinite(u))
99
+ & jnp.all(jnp.isfinite(v))
100
+ )
101
+
102
+ def backtrack(carry):
103
+ owner, column, steps = carry
104
+ predecessor = previous[column - 1]
105
+ return owner.at[column].set(owner[predecessor]), predecessor, steps + 1
106
+
107
+ owner, column, _ = lax.while_loop(
108
+ lambda carry: feasible & (carry[1] != 0) & (carry[2] <= row),
109
+ backtrack,
110
+ (owner, column, jnp.asarray(0, dtype=jnp.int32)),
111
+ )
112
+ return u, v, owner, feasible & (column == 0)
113
+
114
+ return lax.cond(active[row] & state[-1], augment, lambda value: value, state)
115
+
116
+ initial = (
117
+ jnp.zeros((targets + 1,), dtype=jnp.float32),
118
+ jnp.zeros((queries + 1,), dtype=jnp.float32),
119
+ jnp.zeros((queries + 1,), dtype=jnp.int32),
120
+ finite,
121
+ )
122
+ _, _, owner, feasible = lax.fori_loop(0, targets, insert, initial)
123
+ indices = jnp.full((targets,), -1, dtype=jnp.int32)
124
+ destinations = jnp.where(owner[1:] > 0, owner[1:] - 1, targets)
125
+ indices = indices.at[destinations].set(
126
+ jnp.arange(queries, dtype=jnp.int32), mode="drop"
127
+ )
128
+ return QueryAssignment(
129
+ jnp.where(feasible, indices, -1), active & feasible, feasible
130
+ )