slide2vec 5.8.0__tar.gz → 5.8.2__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 (160) hide show
  1. {slide2vec-5.8.0 → slide2vec-5.8.2}/PKG-INFO +3 -3
  2. {slide2vec-5.8.0 → slide2vec-5.8.2}/pyproject.toml +4 -4
  3. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/__init__.py +1 -1
  4. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/gigapath.py +3 -0
  5. slide2vec-5.8.2/slide2vec/encoders/models/titan.py +180 -0
  6. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec.egg-info/PKG-INFO +3 -3
  7. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec.egg-info/SOURCES.txt +2 -1
  8. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec.egg-info/requires.txt +2 -2
  9. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_hs2p_package_cutover.py +3 -3
  10. slide2vec-5.8.2/tests/test_titan.py +134 -0
  11. slide2vec-5.8.0/slide2vec/encoders/models/titan.py +0 -58
  12. {slide2vec-5.8.0 → slide2vec-5.8.2}/LICENSE +0 -0
  13. {slide2vec-5.8.0 → slide2vec-5.8.2}/README.md +0 -0
  14. {slide2vec-5.8.0 → slide2vec-5.8.2}/setup.cfg +0 -0
  15. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/__main__.py +0 -0
  16. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/api.py +0 -0
  17. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/artifacts.py +0 -0
  18. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/cli.py +0 -0
  19. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/configs/__init__.py +0 -0
  20. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/configs/default.yaml +0 -0
  21. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/configs/resources.py +0 -0
  22. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/data/__init__.py +0 -0
  23. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/data/dataset.py +0 -0
  24. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/data/tile_reader.py +0 -0
  25. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/data/tile_store.py +0 -0
  26. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/distributed/__init__.py +0 -0
  27. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/distributed/dense_image_worker.py +0 -0
  28. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/distributed/dense_worker.py +0 -0
  29. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/distributed/direct_embed_worker.py +0 -0
  30. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/distributed/image_worker.py +0 -0
  31. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/distributed/pipeline_worker.py +0 -0
  32. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/distributed/worker_entry.py +0 -0
  33. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/__init__.py +0 -0
  34. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/base.py +0 -0
  35. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/__init__.py +0 -0
  36. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/conch.py +0 -0
  37. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/dinov2.py +0 -0
  38. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/genbio.py +0 -0
  39. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/gpfm.py +0 -0
  40. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/hibou.py +0 -0
  41. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/hoptimus.py +0 -0
  42. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/isight.py +0 -0
  43. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/lunit.py +0 -0
  44. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/midnight.py +0 -0
  45. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/moozy/__init__.py +0 -0
  46. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/moozy/blocks.py +0 -0
  47. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/moozy/case.py +0 -0
  48. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/moozy/loading.py +0 -0
  49. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/moozy/slide.py +0 -0
  50. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/moozy/types.py +0 -0
  51. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/mstar.py +0 -0
  52. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/musk.py +0 -0
  53. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/phikon.py +0 -0
  54. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/prism.py +0 -0
  55. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/prism2.py +0 -0
  56. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/prost40m.py +0 -0
  57. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/rudolfv2.py +0 -0
  58. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/uni.py +0 -0
  59. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/virchow.py +0 -0
  60. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/models/waiv.py +0 -0
  61. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/registry.py +0 -0
  62. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/encoders/validation.py +0 -0
  63. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/inference.py +0 -0
  64. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/progress.py +0 -0
  65. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/__init__.py +0 -0
  66. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/artifacts_collect.py +0 -0
  67. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/batching.py +0 -0
  68. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/cpu_budget.py +0 -0
  69. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/dense_encode.py +0 -0
  70. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/dense_encoder_input.py +0 -0
  71. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/dense_image_reading.py +0 -0
  72. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/dense_image_recipe.py +0 -0
  73. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/dense_image_shard.py +0 -0
  74. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/dense_image_stage.py +0 -0
  75. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/dense_regions.py +0 -0
  76. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/dense_shard.py +0 -0
  77. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/dense_sliding.py +0 -0
  78. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/dense_stage.py +0 -0
  79. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/distributed.py +0 -0
  80. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/distributed_stage.py +0 -0
  81. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/effective_encoder_input.py +0 -0
  82. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/embedding.py +0 -0
  83. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/embedding_persist.py +0 -0
  84. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/embedding_pipeline.py +0 -0
  85. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/encoder_input_contract.py +0 -0
  86. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/hierarchical.py +0 -0
  87. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/image_shard.py +0 -0
  88. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/image_specs.py +0 -0
  89. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/image_stage.py +0 -0
  90. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/manifest.py +0 -0
  91. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/model_settings.py +0 -0
  92. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/patient_pipeline.py +0 -0
  93. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/persist_callbacks.py +0 -0
  94. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/persistence.py +0 -0
  95. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/pooled_encoder_input.py +0 -0
  96. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/preprocessing.py +0 -0
  97. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/process_list.py +0 -0
  98. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/progress_bridge.py +0 -0
  99. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/registry.py +0 -0
  100. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/serialization.py +0 -0
  101. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/sharding.py +0 -0
  102. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/slide_encode.py +0 -0
  103. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/tiling.py +0 -0
  104. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/tiling_pipeline.py +0 -0
  105. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/types.py +0 -0
  106. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/runtime/worker_io.py +0 -0
  107. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/utils/__init__.py +0 -0
  108. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/utils/config.py +0 -0
  109. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/utils/coordinates.py +0 -0
  110. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/utils/log_utils.py +0 -0
  111. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/utils/tiling_io.py +0 -0
  112. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec/utils/utils.py +0 -0
  113. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec.egg-info/dependency_links.txt +0 -0
  114. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec.egg-info/entry_points.txt +0 -0
  115. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec.egg-info/not-zip-safe +0 -0
  116. {slide2vec-5.8.0 → slide2vec-5.8.2}/slide2vec.egg-info/top_level.txt +0 -0
  117. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_architecture_runtime_split.py +0 -0
  118. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_attention_extraction.py +0 -0
  119. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_encode_kit.py +0 -0
  120. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_encoder_input.py +0 -0
  121. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_extraction.py +0 -0
  122. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_image_reading.py +0 -0
  123. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_image_resume.py +0 -0
  124. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_image_shard.py +0 -0
  125. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_image_stage.py +0 -0
  126. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_regions.py +0 -0
  127. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_shard.py +0 -0
  128. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_sliding.py +0 -0
  129. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_source_spacing.py +0 -0
  130. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_stage.py +0 -0
  131. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dense_worker.py +0 -0
  132. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_dinov2_natimage.py +0 -0
  133. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_encoder_capabilities.py +0 -0
  134. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_encoder_input_contract.py +0 -0
  135. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_encoder_plugins.py +0 -0
  136. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_encoder_provider_failures.py +0 -0
  137. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_encoder_registry.py +0 -0
  138. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_gpfm_genbio_heavy.py +0 -0
  139. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_image_shard.py +0 -0
  140. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_image_stage.py +0 -0
  141. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_isight.py +0 -0
  142. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_mascaret.py +0 -0
  143. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_output_consistency.py +0 -0
  144. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_patch_size_metadata.py +0 -0
  145. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_patient_manifest.py +0 -0
  146. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_phaet.py +0 -0
  147. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_pooled_encoder_input.py +0 -0
  148. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_pooled_geometry.py +0 -0
  149. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_prism2.py +0 -0
  150. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_progress.py +0 -0
  151. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_regression_core.py +0 -0
  152. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_regression_inference.py +0 -0
  153. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_regression_models.py +0 -0
  154. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_rudolfv2.py +0 -0
  155. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_runtime_batching.py +0 -0
  156. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_sharding.py +0 -0
  157. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_slide_coordinate_preparation.py +0 -0
  158. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_soma_migration.py +0 -0
  159. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_tile_store.py +0 -0
  160. {slide2vec-5.8.0 → slide2vec-5.8.2}/tests/test_tiling_pipeline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 5.8.0
3
+ Version: 5.8.2
4
4
  Summary: Embedding of whole slide images with Foundation Models
5
5
  Author-email: Clément Grisi <clement.grisi@radboudumc.nl>
6
6
  License-Expression: Apache-2.0
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.1
18
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2
19
19
  Requires-Dist: omegaconf
20
20
  Requires-Dist: matplotlib
21
21
  Requires-Dist: numpy<2
@@ -75,7 +75,7 @@ Requires-Dist: numpy<2; extra == "fm"
75
75
  Requires-Dist: pandas; extra == "fm"
76
76
  Requires-Dist: pillow; extra == "fm"
77
77
  Requires-Dist: rich; extra == "fm"
78
- Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.1; extra == "fm"
78
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2; extra == "fm"
79
79
  Requires-Dist: wandb; extra == "fm"
80
80
  Requires-Dist: torch<2.8,>=2.3; extra == "fm"
81
81
  Requires-Dist: torchvision>=0.18.0; extra == "fm"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "slide2vec"
7
- version = "5.8.0"
7
+ version = "5.8.2"
8
8
  description = "Embedding of whole slide images with Foundation Models"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -21,7 +21,7 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.13",
22
22
  ]
23
23
  dependencies = [
24
- "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.1",
24
+ "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2",
25
25
  "omegaconf",
26
26
  "matplotlib",
27
27
  "numpy<2",
@@ -100,7 +100,7 @@ fm = [
100
100
  "pandas",
101
101
  "pillow",
102
102
  "rich",
103
- "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.1",
103
+ "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2",
104
104
  "wandb",
105
105
  "torch>=2.3,<2.8",
106
106
  "torchvision>=0.18.0",
@@ -180,7 +180,7 @@ no_implicit_reexport = true
180
180
  max-line-length = 160
181
181
 
182
182
  [tool.bumpver]
183
- current_version = "5.8.0"
183
+ current_version = "5.8.2"
184
184
  version_pattern = "MAJOR.MINOR.PATCH"
185
185
  commit = false # We do version bumping in CI, not as a commit
186
186
  tag = false # Git tag already exists — we don't auto-tag
@@ -27,7 +27,7 @@ from slide2vec.encoders import (
27
27
  )
28
28
 
29
29
 
30
- __version__ = "5.8.0"
30
+ __version__ = "5.8.2"
31
31
 
32
32
  __all__ = [
33
33
  "Model",
@@ -123,6 +123,9 @@ class GigaPathSlideEncoder(SlideEncoder):
123
123
  tile_features = tile_features.unsqueeze(0)
124
124
  if coordinates.ndim == 2:
125
125
  coordinates = coordinates.unsqueeze(0)
126
+ # prov-gigapath's reference usage feeds fp32 tile embeddings under autocast;
127
+ # fp16 input risks dtype mismatches in ops autocast does not cover.
128
+ tile_features = tile_features.float()
126
129
  # gigapath_slide_enc12l768d.forward always returns a list of per-layer
127
130
  # embeddings (a single element when all_layer_embed is False); the final
128
131
  # slide embedding is the last layer, matching prov-gigapath's own usage.
@@ -0,0 +1,180 @@
1
+ """TITAN slide encoder implementation."""
2
+
3
+ import math
4
+ import sys
5
+
6
+ import numpy as np
7
+ import torch
8
+ from transformers import AutoModel
9
+
10
+ from slide2vec.encoders.base import SlideEncoder, preferred_default_device, resolve_requested_output_variant
11
+ from slide2vec.encoders.registry import register_encoder
12
+
13
+ # Pinned so the remote-code patches below stay valid (and so runs are reproducible —
14
+ # an unpinned from_pretrained re-downloads whatever is at the repo HEAD).
15
+ _TITAN_REVISION = "dac6773d9961cfc75503440676ff157a2c6e8d2e"
16
+
17
+
18
+ def _alibi_slopes(n: int) -> list[float]:
19
+ # verbatim ALiBi slope schedule from TITAN's get_alibi at the pinned revision
20
+ if math.log2(n).is_integer():
21
+ p = 2 ** (-(2 ** -(math.log2(n) - 3)))
22
+ return [p * (p ** i) for i in range(n)]
23
+ nearest = 2 ** math.floor(math.log2(n))
24
+ base = _alibi_slopes(nearest)
25
+ extra = _alibi_slopes(2 * nearest)[0::2][: n - nearest]
26
+ return base + extra
27
+
28
+
29
+ def _lean_alibi_bias(module, w, h, bg_mask, device, dtype):
30
+ """Bitwise-identical replacement for TITAN's get_alibi + the caller's cast/move.
31
+
32
+ The reference builds the [1, heads, N, N] bias through N x N float64 numpy on
33
+ the CPU (>100 GB of host RAM for slides in the tens of thousands of tiles) and
34
+ hands SDPA an unaligned bias that forces the math kernel, which materializes a
35
+ second N^2 tensor on the GPU. This builds the same values on-device in fp16, in
36
+ row chunks, with rows padded to a multiple of 8 elements so the memory-efficient
37
+ SDPA kernel accepts the bias — peak memory drops from ~4x to ~1x the bias size.
38
+ """
39
+ ii, jj = torch.meshgrid(
40
+ torch.arange(w, device=device), torch.arange(h, device=device), indexing="ij"
41
+ )
42
+ if bg_mask is not None:
43
+ mask = bg_mask.to(device).squeeze(0)
44
+ ii, jj = ii[mask], jj[mask]
45
+ points = torch.stack([ii.reshape(-1), jj.reshape(-1)], dim=1).float()
46
+ n = points.shape[0]
47
+ length = n + 1 # +1 for the cls token; its bias row/col stays zero
48
+ padded = ((length + 7) // 8) * 8
49
+ slopes = torch.tensor(
50
+ _alibi_slopes(module.num_heads), device=device, dtype=torch.float32
51
+ ).view(module.num_heads, 1, 1)
52
+ bias = torch.zeros(1, module.num_heads, length, padded, device=device, dtype=dtype)
53
+ # chunk intermediate is (heads, step, n) fp32 — stays under ~1.5 GB even at 53k tiles
54
+ step = 512
55
+ for start in range(0, n, step):
56
+ diff = points[start : start + step].unsqueeze(1) - points.unsqueeze(0)
57
+ dist = diff.square().sum(-1).sqrt()
58
+ bias[0, :, 1 + start : 1 + start + dist.shape[0], 1:length] = (
59
+ dist.unsqueeze(0) * slopes * -1
60
+ ).to(dtype)
61
+ return bias[..., :length]
62
+
63
+
64
+ def _patch_titan_remote_code(model) -> bool:
65
+ """Patch TITAN's remote code for fp16 input and bounded bias memory.
66
+
67
+ Two patches on the dynamically loaded module (it exists only after
68
+ from_pretrained): preprocess_features runs its grid index_add_ in fp32 (the op
69
+ rejects fp16 features) but returns the grid in the features' own dtype — an
70
+ exact roundtrip that keeps the whole forward on the fp16 path — and
71
+ forward_features' single-slide alibi branch swaps in _lean_alibi_bias.
72
+ Returns False without patching if the module does not look like the pinned
73
+ revision; the caller then falls back to fp32 features (correct, but with the
74
+ reference implementation's memory behavior).
75
+ """
76
+ try:
77
+ vision_encoder = model.vision_encoder
78
+ vit = sys.modules[type(vision_encoder).__module__]
79
+ if getattr(vit, "_slide2vec_titan_patched", False):
80
+ return True
81
+ for attr in ("pos_encode_type", "num_heads", "patch_embed", "_pos_embed", "norm_pre", "blocks", "norm"):
82
+ if not hasattr(vision_encoder, attr):
83
+ return False
84
+ if not callable(getattr(vit, "preprocess_features", None)):
85
+ return False
86
+ except Exception:
87
+ return False
88
+
89
+ orig_preprocess = vit.preprocess_features
90
+
91
+ def preprocess_features(features, coords, patch_size_lv0):
92
+ grid, coords_grid, bg_mask = orig_preprocess(features.float(), coords, patch_size_lv0)
93
+ return grid.to(features.dtype), coords_grid, bg_mask
94
+
95
+ orig_forward_features = type(vision_encoder).forward_features
96
+
97
+ def forward_features(self, x, coords=None, mask=None, bg_mask=None):
98
+ # single-slide alibi path only; anything else falls through to the original
99
+ if self.pos_encode_type != "alibi" or x.shape[0] != 1 or self.masked_im_modeling:
100
+ return orig_forward_features(self, x, coords=coords, mask=mask, bg_mask=bg_mask)
101
+ B, nc, w, h = x.shape
102
+ # bias dtype = input grid dtype, as in the original, which builds the bias
103
+ # before patch_embed; post-norm activations can be fp32 under autocast
104
+ in_dtype = x.dtype
105
+ x = x.flatten(2, 3).transpose(1, 2)
106
+ x = self.patch_embed(x)
107
+ x = self._pos_embed(x, coords, w, h)
108
+ x = self.norm_pre(x)
109
+ if bg_mask is not None:
110
+ keep = torch.cat(
111
+ (torch.ones((1, 1), dtype=torch.bool, device=x.device), bg_mask.view(1, -1)),
112
+ dim=1,
113
+ )
114
+ x = x[keep].unsqueeze(0)
115
+ attn_bias = _lean_alibi_bias(self, w, h, bg_mask, device=x.device, dtype=in_dtype)
116
+ x = self.blocks(x, attn_bias, bg_mask)
117
+ x = self.norm(x)
118
+ return x
119
+
120
+ vit.preprocess_features = preprocess_features
121
+ type(vision_encoder).forward_features = forward_features
122
+ vit._slide2vec_titan_patched = True
123
+ return True
124
+
125
+
126
+ @register_encoder(
127
+ "titan",
128
+ level="slide",
129
+ tile_encoder="conchv15",
130
+ tile_encoder_output_variant="default",
131
+ output_variants={"default": {"encode_dim": 768}},
132
+ default_output_variant="default",
133
+ supported_spacing_um=0.5,
134
+ precision="fp16",
135
+ source="MahmoodLab/TITAN",
136
+ )
137
+ class TitanSlideEncoder(SlideEncoder):
138
+ def __init__(self, *, output_variant: str | None = None):
139
+ self._model = AutoModel.from_pretrained(
140
+ "MahmoodLab/TITAN", revision=_TITAN_REVISION, trust_remote_code=True
141
+ ).eval()
142
+ self._remote_code_patched = _patch_titan_remote_code(self._model)
143
+ self._device = preferred_default_device()
144
+ self._output_variant = resolve_requested_output_variant(output_variant)
145
+
146
+ @property
147
+ def encode_dim(self) -> int:
148
+ return 768
149
+
150
+ @property
151
+ def device(self) -> torch.device:
152
+ return self._device
153
+
154
+ def to(self, device: torch.device | str) -> "TitanSlideEncoder":
155
+ self._device = torch.device(device)
156
+ self._model = self._model.to(self._device)
157
+ return self
158
+
159
+ def encode_slide(
160
+ self,
161
+ tile_features: torch.Tensor,
162
+ coordinates: torch.Tensor | None = None,
163
+ *,
164
+ tile_size_lv0: int | None = None,
165
+ ) -> torch.Tensor:
166
+ if coordinates is None or tile_size_lv0 is None:
167
+ raise ValueError("TITAN slide encoding requires coordinates and tile_size_lv0")
168
+ if tile_features.ndim == 2:
169
+ tile_features = tile_features.unsqueeze(0)
170
+ if coordinates.ndim == 2:
171
+ coordinates = coordinates.unsqueeze(0)
172
+ if not self._remote_code_patched:
173
+ # fallback for unrecognized remote code: fp32 features satisfy its fp32
174
+ # grid index_add_, at the cost of the reference memory behavior
175
+ tile_features = tile_features.float()
176
+ return self._model.encode_slide_from_patch_features(
177
+ tile_features,
178
+ coordinates.long(),
179
+ np.int64(tile_size_lv0),
180
+ ).squeeze(0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 5.8.0
3
+ Version: 5.8.2
4
4
  Summary: Embedding of whole slide images with Foundation Models
5
5
  Author-email: Clément Grisi <clement.grisi@radboudumc.nl>
6
6
  License-Expression: Apache-2.0
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.1
18
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2
19
19
  Requires-Dist: omegaconf
20
20
  Requires-Dist: matplotlib
21
21
  Requires-Dist: numpy<2
@@ -75,7 +75,7 @@ Requires-Dist: numpy<2; extra == "fm"
75
75
  Requires-Dist: pandas; extra == "fm"
76
76
  Requires-Dist: pillow; extra == "fm"
77
77
  Requires-Dist: rich; extra == "fm"
78
- Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.1; extra == "fm"
78
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2; extra == "fm"
79
79
  Requires-Dist: wandb; extra == "fm"
80
80
  Requires-Dist: torch<2.8,>=2.3; extra == "fm"
81
81
  Requires-Dist: torchvision>=0.18.0; extra == "fm"
@@ -153,4 +153,5 @@ tests/test_sharding.py
153
153
  tests/test_slide_coordinate_preparation.py
154
154
  tests/test_soma_migration.py
155
155
  tests/test_tile_store.py
156
- tests/test_tiling_pipeline.py
156
+ tests/test_tiling_pipeline.py
157
+ tests/test_titan.py
@@ -1,4 +1,4 @@
1
- hs2p[asap,cucim,openslide,sam2,vips]>=4.4.1
1
+ hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2
2
2
  omegaconf
3
3
  matplotlib
4
4
  numpy<2
@@ -27,7 +27,7 @@ numpy<2
27
27
  pandas
28
28
  pillow
29
29
  rich
30
- hs2p[asap,cucim,openslide,sam2,vips]>=4.4.1
30
+ hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2
31
31
  wandb
32
32
  torch<2.8,>=2.3
33
33
  torchvision>=0.18.0
@@ -23,7 +23,7 @@ def test_package_root_exports_api():
23
23
  assert hasattr(package, "DenseRegionArtifact")
24
24
 
25
25
 
26
- def test_dependency_declarations_require_hs2p_4_4_1_with_consistent_extras():
26
+ def test_dependency_declarations_require_hs2p_4_4_2_with_consistent_extras():
27
27
  project = tomllib.loads(
28
28
  (Path(__file__).resolve().parents[1] / "pyproject.toml").read_text(
29
29
  encoding="utf-8"
@@ -39,8 +39,8 @@ def test_dependency_declarations_require_hs2p_4_4_1_with_consistent_extras():
39
39
  ]
40
40
 
41
41
  assert hs2p_dependencies == [
42
- "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.1",
43
- "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.1",
42
+ "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2",
43
+ "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2",
44
44
  ]
45
45
 
46
46
 
@@ -0,0 +1,134 @@
1
+ """Regression tests for the TITAN slide encoder input contract."""
2
+
3
+ from types import SimpleNamespace
4
+
5
+ import pytest
6
+
7
+ torch = pytest.importorskip("torch")
8
+
9
+
10
+ class _FakeTitan:
11
+ """Mirrors TITAN's preprocess_features: index_add_ of the input into an fp32
12
+ grid, which raises on fp16 features."""
13
+
14
+ def eval(self):
15
+ return self
16
+
17
+ def encode_slide_from_patch_features(self, patch_features, patch_coords, patch_size_lv0):
18
+ grid = torch.zeros(4, patch_features.size(-1))
19
+ indices = torch.zeros(patch_features.size(1), dtype=torch.long)
20
+ grid.index_add_(0, indices, patch_features.squeeze(0))
21
+ return torch.zeros(1, 768)
22
+
23
+
24
+ def test_titan_encode_slide_accepts_fp16_features(monkeypatch):
25
+ # _FakeTitan has no vision_encoder, so the remote-code patch declines and the
26
+ # encoder must fall back to fp32 features to satisfy the fp32-grid index_add_
27
+ import slide2vec.encoders.models.titan as titan_mod
28
+
29
+ monkeypatch.setattr(
30
+ titan_mod,
31
+ "AutoModel",
32
+ SimpleNamespace(from_pretrained=lambda *args, **kwargs: _FakeTitan()),
33
+ )
34
+ encoder = titan_mod.TitanSlideEncoder()
35
+ assert encoder._remote_code_patched is False
36
+
37
+ features = torch.randn(5, 768, dtype=torch.float16)
38
+ coordinates = torch.zeros(5, 2, dtype=torch.int64)
39
+ embedding = encoder.encode_slide(features, coordinates, tile_size_lv0=512)
40
+
41
+ assert embedding.shape[-1] == 768
42
+
43
+
44
+ def _reference_get_alibi(num_heads, w, h, bg_mask=None):
45
+ """Verbatim port of TITAN's get_alibi (revision dac6773) + the caller's fp16 cast."""
46
+ import math
47
+
48
+ import numpy as np
49
+
50
+ x, y = np.meshgrid(np.arange(w), np.arange(h), indexing="ij")
51
+ if bg_mask is not None:
52
+ x = x[bg_mask.cpu().squeeze(0)]
53
+ y = y[bg_mask.cpu().squeeze(0)]
54
+ points = np.stack([x.ravel(), y.ravel()], axis=1)
55
+ diffs = points[:, None, :] - points[None, :, :]
56
+ dists = np.sqrt(np.sum(diffs**2, axis=-1))
57
+
58
+ def get_slopes(n):
59
+ if math.log2(n).is_integer():
60
+ p = 2 ** (-(2 ** -(math.log2(n) - 3)))
61
+ return [p * (p**i) for i in range(n)]
62
+ nearest = 2 ** math.floor(math.log2(n))
63
+ return get_slopes(nearest) + get_slopes(2 * nearest)[0::2][: n - nearest]
64
+
65
+ slopes = torch.tensor(get_slopes(num_heads), dtype=torch.float32).view(num_heads, 1, 1)
66
+ n_patches = dists.shape[-1]
67
+ dists_tensor = torch.tensor(dists, dtype=torch.float32).view(1, n_patches, n_patches)
68
+ bias_matrix = dists_tensor * slopes * -1
69
+ embed_len = n_patches + 1
70
+ all_bias = torch.zeros(1, num_heads, embed_len, embed_len)
71
+ all_bias[:, :, 1:, 1:] = bias_matrix
72
+ return all_bias.half()
73
+
74
+
75
+ @pytest.mark.parametrize("w,h,use_mask", [(7, 5, False), (9, 11, True), (40, 45, True)])
76
+ def test_lean_alibi_bias_matches_reference(w, h, use_mask):
77
+ from slide2vec.encoders.models.titan import _lean_alibi_bias
78
+
79
+ torch.manual_seed(0)
80
+ bg_mask = (torch.rand(1, w, h) > 0.3) if use_mask else None
81
+ reference = _reference_get_alibi(12, w, h, bg_mask)
82
+ lean = _lean_alibi_bias(
83
+ SimpleNamespace(num_heads=12), w, h, bg_mask, device="cpu", dtype=torch.float16
84
+ )
85
+
86
+ assert torch.equal(lean, reference)
87
+ # padded row stride is what makes SDPA's memory-efficient kernel accept the bias
88
+ assert lean.stride(-2) % 8 == 0
89
+
90
+
91
+ def test_patch_declines_unrecognized_remote_code():
92
+ from slide2vec.encoders.models.titan import _patch_titan_remote_code
93
+
94
+ assert _patch_titan_remote_code(_FakeTitan()) is False
95
+
96
+
97
+ def test_patched_preprocess_keeps_features_dtype():
98
+ import sys
99
+ import types
100
+
101
+ from slide2vec.encoders.models.titan import _patch_titan_remote_code
102
+
103
+ module = types.ModuleType("_fake_titan_remote")
104
+
105
+ def preprocess_features(features, coords, patch_size_lv0):
106
+ # fp32 zero-grid + index_add_, as in the real remote code: raises on fp16
107
+ grid = torch.zeros(4, features.size(-1))
108
+ grid.index_add_(0, torch.arange(features.size(0)) % 4, features)
109
+ return grid, torch.zeros(4, 2, dtype=torch.int64), torch.ones(4, dtype=torch.bool)
110
+
111
+ module.preprocess_features = preprocess_features
112
+
113
+ class _FakeViT:
114
+ pos_encode_type = "alibi"
115
+ num_heads = 12
116
+ patch_embed = _pos_embed = norm_pre = blocks = norm = object()
117
+ masked_im_modeling = False
118
+
119
+ def forward_features(self, x, coords=None, mask=None, bg_mask=None):
120
+ return x
121
+
122
+ _FakeViT.__module__ = module.__name__
123
+ sys.modules[module.__name__] = module
124
+ try:
125
+ model = SimpleNamespace(vision_encoder=_FakeViT())
126
+ assert _patch_titan_remote_code(model) is True
127
+ # repeat call is an idempotent no-op, not a double wrap
128
+ assert _patch_titan_remote_code(model) is True
129
+
130
+ fp16_features = torch.randn(6, 32, dtype=torch.float16)
131
+ grid, _, _ = module.preprocess_features(fp16_features, None, 512)
132
+ assert grid.dtype == torch.float16
133
+ finally:
134
+ del sys.modules[module.__name__]
@@ -1,58 +0,0 @@
1
- """TITAN slide encoder implementation."""
2
-
3
- import numpy as np
4
- import torch
5
- from transformers import AutoModel
6
-
7
- from slide2vec.encoders.base import SlideEncoder, preferred_default_device, resolve_requested_output_variant
8
- from slide2vec.encoders.registry import register_encoder
9
-
10
-
11
- @register_encoder(
12
- "titan",
13
- level="slide",
14
- tile_encoder="conchv15",
15
- tile_encoder_output_variant="default",
16
- output_variants={"default": {"encode_dim": 768}},
17
- default_output_variant="default",
18
- supported_spacing_um=0.5,
19
- precision="fp16",
20
- source="MahmoodLab/TITAN",
21
- )
22
- class TitanSlideEncoder(SlideEncoder):
23
- def __init__(self, *, output_variant: str | None = None):
24
- self._model = AutoModel.from_pretrained("MahmoodLab/TITAN", trust_remote_code=True).eval()
25
- self._device = preferred_default_device()
26
- self._output_variant = resolve_requested_output_variant(output_variant)
27
-
28
- @property
29
- def encode_dim(self) -> int:
30
- return 768
31
-
32
- @property
33
- def device(self) -> torch.device:
34
- return self._device
35
-
36
- def to(self, device: torch.device | str) -> "TitanSlideEncoder":
37
- self._device = torch.device(device)
38
- self._model = self._model.to(self._device)
39
- return self
40
-
41
- def encode_slide(
42
- self,
43
- tile_features: torch.Tensor,
44
- coordinates: torch.Tensor | None = None,
45
- *,
46
- tile_size_lv0: int | None = None,
47
- ) -> torch.Tensor:
48
- if coordinates is None or tile_size_lv0 is None:
49
- raise ValueError("TITAN slide encoding requires coordinates and tile_size_lv0")
50
- if tile_features.ndim == 2:
51
- tile_features = tile_features.unsqueeze(0)
52
- if coordinates.ndim == 2:
53
- coordinates = coordinates.unsqueeze(0)
54
- return self._model.encode_slide_from_patch_features(
55
- tile_features,
56
- coordinates.long(),
57
- np.int64(tile_size_lv0),
58
- ).squeeze(0)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes