slide2vec 7.1.0__tar.gz → 7.1.1__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 (186) hide show
  1. {slide2vec-7.1.0 → slide2vec-7.1.1}/PKG-INFO +1 -1
  2. {slide2vec-7.1.0 → slide2vec-7.1.1}/pyproject.toml +2 -2
  3. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/__init__.py +1 -1
  4. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/api.py +70 -37
  5. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/inference.py +14 -14
  6. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/cpu_budget.py +2 -2
  7. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/dense_image_stage.py +4 -4
  8. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/dense_stage.py +4 -4
  9. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/distributed_stage.py +7 -7
  10. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/embedding_pipeline.py +2 -2
  11. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/image_stage.py +4 -4
  12. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/serialization.py +4 -2
  13. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/slide_encode.py +3 -2
  14. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec.egg-info/PKG-INFO +1 -1
  15. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec.egg-info/SOURCES.txt +1 -0
  16. slide2vec-7.1.1/tests/test_execution_options.py +116 -0
  17. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_regression_core.py +4 -6
  18. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_regression_inference.py +65 -0
  19. {slide2vec-7.1.0 → slide2vec-7.1.1}/LICENSE +0 -0
  20. {slide2vec-7.1.0 → slide2vec-7.1.1}/README.md +0 -0
  21. {slide2vec-7.1.0 → slide2vec-7.1.1}/setup.cfg +0 -0
  22. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/__main__.py +0 -0
  23. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/_cufile.py +0 -0
  24. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/artifacts.py +0 -0
  25. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/cli.py +0 -0
  26. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/configs/__init__.py +0 -0
  27. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/configs/default.yaml +0 -0
  28. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/configs/resources.py +0 -0
  29. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/cufile.json +0 -0
  30. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/data/__init__.py +0 -0
  31. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/data/dataset.py +0 -0
  32. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/data/tile_reader.py +0 -0
  33. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/data/tile_store.py +0 -0
  34. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/distributed/__init__.py +0 -0
  35. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/distributed/dense_image_worker.py +0 -0
  36. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/distributed/dense_worker.py +0 -0
  37. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/distributed/direct_embed_worker.py +0 -0
  38. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/distributed/image_worker.py +0 -0
  39. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/distributed/pin_gpu.py +0 -0
  40. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/distributed/pipeline_worker.py +0 -0
  41. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/distributed/worker_entry.py +0 -0
  42. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/__init__.py +0 -0
  43. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/base.py +0 -0
  44. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/__init__.py +0 -0
  45. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/conch.py +0 -0
  46. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/dinov2.py +0 -0
  47. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/dinov3.py +0 -0
  48. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/genbio.py +0 -0
  49. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/gigapath.py +0 -0
  50. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/gpfm.py +0 -0
  51. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/hibou.py +0 -0
  52. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/hoptimus.py +0 -0
  53. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/isight.py +0 -0
  54. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/lunit.py +0 -0
  55. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/mettle.py +0 -0
  56. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/midnight.py +0 -0
  57. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/moozy/__init__.py +0 -0
  58. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/moozy/blocks.py +0 -0
  59. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/moozy/case.py +0 -0
  60. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/moozy/loading.py +0 -0
  61. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/moozy/slide.py +0 -0
  62. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/moozy/types.py +0 -0
  63. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/mstar.py +0 -0
  64. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/musk.py +0 -0
  65. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/phikon.py +0 -0
  66. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/prism.py +0 -0
  67. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/prism2.py +0 -0
  68. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/prost40m.py +0 -0
  69. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/rudolfv2.py +0 -0
  70. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/titan.py +0 -0
  71. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/uni.py +0 -0
  72. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/virchow.py +0 -0
  73. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/models/waiv.py +0 -0
  74. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/registry.py +0 -0
  75. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/encoders/validation.py +0 -0
  76. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/progress.py +0 -0
  77. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/__init__.py +0 -0
  78. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/artifacts_collect.py +0 -0
  79. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/batching.py +0 -0
  80. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/dense_encode.py +0 -0
  81. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/dense_encoder_input.py +0 -0
  82. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/dense_image_reading.py +0 -0
  83. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/dense_image_recipe.py +0 -0
  84. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/dense_image_shard.py +0 -0
  85. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/dense_regions.py +0 -0
  86. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/dense_shard.py +0 -0
  87. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/dense_sliding.py +0 -0
  88. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/distributed.py +0 -0
  89. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/effective_encoder_input.py +0 -0
  90. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/embedding.py +0 -0
  91. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/embedding_persist.py +0 -0
  92. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/encoder_input_contract.py +0 -0
  93. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/feature_identity.py +0 -0
  94. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/hierarchical.py +0 -0
  95. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/image_shard.py +0 -0
  96. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/image_specs.py +0 -0
  97. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/manifest.py +0 -0
  98. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/model_settings.py +0 -0
  99. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/patient_pipeline.py +0 -0
  100. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/persist_callbacks.py +0 -0
  101. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/persistence.py +0 -0
  102. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/pooled_encoder_input.py +0 -0
  103. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/preprocessing.py +0 -0
  104. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/process_list.py +0 -0
  105. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/progress_bridge.py +0 -0
  106. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/registry.py +0 -0
  107. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/sharding.py +0 -0
  108. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/tiling.py +0 -0
  109. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/tiling_pipeline.py +0 -0
  110. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/types.py +0 -0
  111. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/runtime/worker_io.py +0 -0
  112. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/utils/__init__.py +0 -0
  113. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/utils/config.py +0 -0
  114. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/utils/coordinates.py +0 -0
  115. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/utils/log_utils.py +0 -0
  116. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/utils/tiling_io.py +0 -0
  117. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec/utils/utils.py +0 -0
  118. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec.egg-info/dependency_links.txt +0 -0
  119. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec.egg-info/entry_points.txt +0 -0
  120. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec.egg-info/not-zip-safe +0 -0
  121. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec.egg-info/requires.txt +0 -0
  122. {slide2vec-7.1.0 → slide2vec-7.1.1}/slide2vec.egg-info/top_level.txt +0 -0
  123. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_attention_extraction.py +0 -0
  124. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_benchmark_tooling.py +0 -0
  125. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_cli_noninteractive.py +0 -0
  126. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_cufile_compat_mode.py +0 -0
  127. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_encode_kit.py +0 -0
  128. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_encoder_input.py +0 -0
  129. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_extraction.py +0 -0
  130. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_image_reading.py +0 -0
  131. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_image_resume.py +0 -0
  132. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_image_shard.py +0 -0
  133. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_image_stage.py +0 -0
  134. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_regions.py +0 -0
  135. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_shard.py +0 -0
  136. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_sliding.py +0 -0
  137. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_source_spacing.py +0 -0
  138. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_stage.py +0 -0
  139. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dense_worker.py +0 -0
  140. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dinov2_natimage.py +0 -0
  141. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_dinov3_natimage.py +0 -0
  142. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_drop_in_encoders.py +0 -0
  143. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_encoder_capabilities.py +0 -0
  144. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_encoder_input_contract.py +0 -0
  145. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_encoder_plugins.py +0 -0
  146. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_encoder_preprocessing.py +0 -0
  147. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_encoder_provider_failures.py +0 -0
  148. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_encoder_registry.py +0 -0
  149. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_feature_identity.py +0 -0
  150. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_gigapath.py +0 -0
  151. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_gpfm_genbio_heavy.py +0 -0
  152. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_hierarchical_encoding.py +0 -0
  153. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_hs2p5_integration.py +0 -0
  154. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_hs2p_package_cutover.py +0 -0
  155. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_image_shard.py +0 -0
  156. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_image_stage.py +0 -0
  157. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_inference_benchmark.py +0 -0
  158. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_isight.py +0 -0
  159. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_load_model_hf_auth.py +0 -0
  160. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_mascaret.py +0 -0
  161. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_mettle.py +0 -0
  162. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_on_slide_persisted.py +0 -0
  163. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_output_consistency.py +0 -0
  164. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_patch_size_metadata.py +0 -0
  165. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_patient_annotation_sampling.py +0 -0
  166. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_patient_manifest.py +0 -0
  167. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_patient_tiling_only.py +0 -0
  168. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_phaet.py +0 -0
  169. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_pooled_encoder_input.py +0 -0
  170. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_pooled_geometry.py +0 -0
  171. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_pooled_identity_api.py +0 -0
  172. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_prism.py +0 -0
  173. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_prism2.py +0 -0
  174. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_process_list_performance.py +0 -0
  175. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_progress.py +0 -0
  176. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_regression_models.py +0 -0
  177. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_rudolfv2.py +0 -0
  178. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_runtime_batching.py +0 -0
  179. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_sharding.py +0 -0
  180. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_slide_coordinate_preparation.py +0 -0
  181. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_slide_latents.py +0 -0
  182. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_soma_migration.py +0 -0
  183. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_tile_store.py +0 -0
  184. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_tiling_pipeline.py +0 -0
  185. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_titan.py +0 -0
  186. {slide2vec-7.1.0 → slide2vec-7.1.1}/tests/test_worker_gpu_pinning.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 7.1.0
3
+ Version: 7.1.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "slide2vec"
7
- version = "7.1.0"
7
+ version = "7.1.1"
8
8
  description = "Embedding of whole slide images with Foundation Models"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -180,7 +180,7 @@ no_implicit_reexport = true
180
180
  max-line-length = 160
181
181
 
182
182
  [tool.bumpver]
183
- current_version = "7.1.0"
183
+ current_version = "7.1.1"
184
184
  version_pattern = "MAJOR.MINOR.PATCH"
185
185
  commit = false
186
186
  tag = false
@@ -36,7 +36,7 @@ from slide2vec.encoders import (
36
36
  )
37
37
 
38
38
 
39
- __version__ = "7.1.0"
39
+ __version__ = "7.1.1"
40
40
 
41
41
  __all__ = [
42
42
  "Model",
@@ -7,7 +7,7 @@ import warnings
7
7
  from dataclasses import dataclass, field, replace
8
8
  from contextlib import contextmanager
9
9
  from pathlib import Path
10
- from typing import Any, Callable, Mapping, Protocol, Sequence
10
+ from typing import Any, Callable, Mapping, NamedTuple, Protocol, Sequence
11
11
 
12
12
  import torch
13
13
  from hs2p import (
@@ -261,9 +261,12 @@ class ExecutionOptions:
261
261
  #: (capped by CPU / SLURM limit, then split across the resolved GPU count).
262
262
  #: Image-only routes safely use zero when auto selection happens after model loading.
263
263
  num_workers_per_gpu: int | None = None
264
- #: Tiling worker count. ``None`` means auto (capped by CPU / SLURM limit).
264
+ #: Tiling worker count. ``None`` means auto. The field keeps the requested value;
265
+ #: :meth:`resolved_num_preprocessing_workers` gives the count a run uses, capped by the
266
+ #: CPU / SLURM limit.
265
267
  num_preprocessing_workers: int | None = None
266
- #: Number of GPUs to use. ``None`` defaults to all available GPUs.
268
+ #: Number of GPUs to use. ``None`` means all available GPUs. The field keeps the
269
+ #: requested value; :meth:`resolved_num_gpus` gives the count a run uses.
267
270
  num_gpus: int | None = None
268
271
  #: Forward-pass dtype — ``"fp16"``, ``"bf16"``, ``"fp32"``,
269
272
  #: or ``None`` (auto-determined from the model preset).
@@ -313,11 +316,9 @@ class ExecutionOptions:
313
316
  )
314
317
 
315
318
  def __post_init__(self) -> None:
316
- resolved_num_gpus = _default_num_gpus() if self.num_gpus is None else self.num_gpus
317
- object.__setattr__(self, "num_gpus", resolved_num_gpus)
318
319
  object.__setattr__(self, "precision", normalize_precision_name(self.precision))
319
320
  object.__setattr__(self, "output_dtype", normalize_output_dtype(self.output_dtype))
320
- if resolved_num_gpus < 1:
321
+ if self.num_gpus is not None and self.num_gpus < 1:
321
322
  raise ValueError("ExecutionOptions.num_gpus must be at least 1")
322
323
  if self.batch_size < 1:
323
324
  raise ValueError("ExecutionOptions.batch_size must be at least 1")
@@ -325,40 +326,62 @@ class ExecutionOptions:
325
326
  raise ValueError("ExecutionOptions.prefetch_factor must be at least 1")
326
327
  if self.num_workers_per_gpu is not None and self.num_workers_per_gpu < 0:
327
328
  raise ValueError("ExecutionOptions.num_workers_per_gpu must be non-negative")
329
+ if self.num_preprocessing_workers is not None and self.num_preprocessing_workers < 0:
330
+ raise ValueError("ExecutionOptions.num_preprocessing_workers must be non-negative")
328
331
  if self.on_image_mismatch not in IMAGE_MISMATCH_POLICIES:
329
332
  raise ValueError(
330
333
  "ExecutionOptions.on_image_mismatch must be one of "
331
334
  f"{', '.join(repr(policy) for policy in IMAGE_MISMATCH_POLICIES)}; "
332
335
  f"got {self.on_image_mismatch!r}"
333
336
  )
337
+
338
+ def resolved_num_gpus(self) -> int:
339
+ """GPU count a run uses: :attr:`num_gpus`, or every visible GPU when it is ``None``."""
340
+ return self._machine_resolution.num_gpus
341
+
342
+ def resolved_num_preprocessing_workers(self) -> int:
343
+ """Tiling worker count a run uses: :attr:`num_preprocessing_workers`, or the CPU /
344
+ SLURM limit when it is ``None``, and never more than that limit."""
345
+ return self._machine_resolution.num_preprocessing_workers
346
+
347
+ def resolved_num_workers_per_gpu(self) -> int:
348
+ if self.num_workers_per_gpu is not None:
349
+ return self.num_workers_per_gpu
350
+ return max(1, cpu_worker_limit() // self.resolved_num_gpus())
351
+
352
+ @functools.cached_property
353
+ def _machine_resolution(self) -> "_MachineResolution":
354
+ # Resolved once per instance, on first use, so every reader in a run agrees and the
355
+ # log line is written once. The cache sits outside the dataclass fields: equality,
356
+ # hashing and repr see only what the caller asked for.
357
+ num_gpus = _default_num_gpus() if self.num_gpus is None else self.num_gpus
334
358
  cap = cpu_worker_limit()
335
- cpu_count = os.cpu_count() or 1
336
- slurm_limit = slurm_cpu_limit()
337
- capped_num_preprocessing_workers = (
359
+ num_preprocessing_workers = (
338
360
  cap if self.num_preprocessing_workers is None else min(self.num_preprocessing_workers, cap)
339
361
  )
340
- object.__setattr__(self, "num_preprocessing_workers", capped_num_preprocessing_workers)
341
- logger = logging.getLogger(__name__)
342
- cap_source = f"slurm_cpu_limit={slurm_limit}" if slurm_limit is not None else f"cpu_count={cpu_count}"
343
- resolved_num_workers = self.resolved_num_workers_per_gpu()
344
- num_workers_per_gpu_label = (
345
- f"{resolved_num_workers} (requested=auto)"
346
- if self.num_workers_per_gpu is None
347
- else str(resolved_num_workers)
362
+ num_workers_per_gpu = (
363
+ max(1, cap // num_gpus) if self.num_workers_per_gpu is None else self.num_workers_per_gpu
364
+ )
365
+ slurm_limit = slurm_cpu_limit()
366
+ cap_source = (
367
+ f"slurm_cpu_limit={slurm_limit}" if slurm_limit is not None else f"cpu_count={os.cpu_count() or 1}"
348
368
  )
349
- logger.info(
350
- "ExecutionOptions: num_workers_per_gpu=%s, num_preprocessing_workers=%d "
369
+ logging.getLogger(__name__).info(
370
+ "ExecutionOptions: num_gpus=%s, num_workers_per_gpu=%s, num_preprocessing_workers=%d "
351
371
  "(preprocessing cap=%d via %s)",
352
- num_workers_per_gpu_label,
353
- capped_num_preprocessing_workers,
372
+ _auto_label(num_gpus, requested=self.num_gpus),
373
+ _auto_label(num_workers_per_gpu, requested=self.num_workers_per_gpu),
374
+ num_preprocessing_workers,
354
375
  cap,
355
376
  cap_source,
356
377
  )
378
+ return _MachineResolution(num_gpus=num_gpus, num_preprocessing_workers=num_preprocessing_workers)
357
379
 
358
- def resolved_num_workers_per_gpu(self) -> int:
359
- if self.num_workers_per_gpu is not None:
360
- return self.num_workers_per_gpu
361
- return max(1, cpu_worker_limit() // self.num_gpus)
380
+ def __getstate__(self) -> dict[str, Any]:
381
+ # A copy or an unpickled instance resolves again on the machine that uses it.
382
+ state = dict(self.__dict__)
383
+ state.pop("_machine_resolution", None)
384
+ return state
362
385
 
363
386
  def resolved_image_num_workers_per_gpu(self) -> int:
364
387
  """Resolve safe post-model-load image-transform workers for this rank."""
@@ -898,8 +921,9 @@ class Model:
898
921
  The dense counterpart of the pooled coordinate path: each ``SlideRegions`` names a
899
922
  slide + a set of level-0 ROI coordinates, and every ROI is read, encoded through the
900
923
  dense transform, and written to ``dense_embeddings/[<class>/]<sample_id>/<x>_<y>.pt``
901
- plus a geometry sidecar. The run splits its ROIs across all visible GPUs
902
- (``execution.num_gpus``); ``num_gpus=1`` encodes fully in-process. Resume is
924
+ plus a geometry sidecar. The run splits its ROIs across
925
+ ``execution.resolved_num_gpus()`` GPUs (all visible GPUs by default);
926
+ ``num_gpus=1`` encodes fully in-process. Resume is
903
927
  automatic — only ROIs whose sidecar has the same source-spacing declaration,
904
928
  resolved hs2p read plan and feature identity are skipped. Returns one
905
929
  :class:`~slide2vec.artifacts.DenseRegionArtifact` per input ROI.
@@ -932,10 +956,10 @@ class Model:
932
956
  **normalization-only** transform, padded up to the encoder's patch multiple, encoded
933
957
  — whole-image, or by sliding the encoder's native field and blending the token grids
934
958
  — and written to ``dense_image_embeddings/<sample_id>.pt`` plus a geometry sidecar.
935
- The run splits its images across all visible GPUs (``execution.num_gpus``);
936
- ``num_gpus=1`` encodes fully in-process. Resume is automatic: an image is skipped
937
- only when its payload exists and its sidecar records the same normalized source
938
- identity and complete extraction recipe. Returns one
959
+ The run splits its images across ``execution.resolved_num_gpus()`` GPUs (all
960
+ visible GPUs by default); ``num_gpus=1`` encodes fully in-process. Resume is
961
+ automatic: an image is skipped only when its payload exists and its sidecar records
962
+ the same normalized source identity and complete extraction recipe. Returns one
939
963
  :class:`~slide2vec.artifacts.DenseImageArtifact` per input image, in input order.
940
964
 
941
965
  Every source is opened through hs2p. PNG/JPEG inputs use hs2p's one-level PIL reader
@@ -977,12 +1001,12 @@ class Model:
977
1001
  set — and slide2vec neither tiles nor reads a slide. Each :class:`ImageSpec` is
978
1002
  decoded, preprocessed with the encoder's **shipped** transform, encoded, and written
979
1003
  to ``image_embeddings/<sample_id>.pt`` plus a provenance sidecar. The run splits its
980
- images across all visible GPUs (``execution.num_gpus``); ``num_gpus=1`` encodes
981
- fully in-process. Resume is automatic: an image is skipped only when its sidecar
982
- records this ``output_format``, the requested ``image_path`` and the full feature
983
- identity. A different recorded feature identity raises; a different recorded
984
- ``image_path`` raises unless ``execution.on_image_mismatch="reencode"``; missing
985
- provenance re-encodes. Returns
1004
+ images across ``execution.resolved_num_gpus()`` GPUs (all visible GPUs by
1005
+ default); ``num_gpus=1`` encodes fully in-process. Resume is automatic: an image is
1006
+ skipped only when its sidecar records this ``output_format``, the requested
1007
+ ``image_path`` and the full feature identity. A different recorded feature identity
1008
+ raises; a different recorded ``image_path`` raises unless
1009
+ ``execution.on_image_mismatch="reencode"``; missing provenance re-encodes. Returns
986
1010
  one :class:`~slide2vec.artifacts.ImageEmbeddingArtifact` per input image, in input
987
1011
  order.
988
1012
 
@@ -1342,6 +1366,15 @@ def _default_num_gpus() -> int:
1342
1366
  return max(1, torch.cuda.device_count()) if torch.cuda.is_available() else 1
1343
1367
 
1344
1368
 
1369
+ class _MachineResolution(NamedTuple):
1370
+ num_gpus: int
1371
+ num_preprocessing_workers: int
1372
+
1373
+
1374
+ def _auto_label(resolved: int, *, requested: int | None) -> str:
1375
+ return f"{resolved} (requested=auto)" if requested is None else str(resolved)
1376
+
1377
+
1345
1378
  def _require_output_dir_for_persistence(execution: ExecutionOptions, *, method_name: str) -> None:
1346
1379
  if execution.output_dir is None:
1347
1380
  raise ValueError(f"ExecutionOptions.output_dir is required for {method_name}")
@@ -261,7 +261,7 @@ def embed_slides(
261
261
  slide_records = [manifest.coerce_slide_spec(slide) for slide in slides]
262
262
  if not slide_records:
263
263
  raise ValueError("At least one slide is required")
264
- if execution.num_gpus > 1:
264
+ if execution.resolved_num_gpus() > 1:
265
265
  distributed_stage.validate_multi_gpu_execution(model, execution)
266
266
  emit_progress(
267
267
  "run.started",
@@ -282,7 +282,7 @@ def embed_slides(
282
282
  slide_records,
283
283
  preprocessing,
284
284
  output_dir=work_dir,
285
- num_workers=execution.num_preprocessing_workers,
285
+ num_workers=execution.resolved_num_preprocessing_workers(),
286
286
  )
287
287
  process_list.emit_tiling_summary(
288
288
  process_list_path,
@@ -306,19 +306,19 @@ def embed_slides(
306
306
  execution=execution,
307
307
  )
308
308
  emit_progress("embedding.started", slide_count=len(embeddable_slides))
309
- if execution.num_gpus > 1 and len(embeddable_slides) > 1:
309
+ if execution.resolved_num_gpus() > 1 and len(embeddable_slides) > 1:
310
310
  emit_progress(
311
311
  "embedding.assignment.started",
312
312
  slide_count=len(embeddable_slides),
313
- num_gpus=execution.num_gpus,
313
+ num_gpus=execution.resolved_num_gpus(),
314
314
  )
315
315
  emit_progress(
316
316
  "embedding.assignment.finished",
317
317
  slide_count=len(embeddable_slides),
318
- num_gpus=execution.num_gpus,
318
+ num_gpus=execution.resolved_num_gpus(),
319
319
  )
320
320
  local_persist_callback = None
321
- if execution.output_dir is not None and execution.num_gpus <= 1:
321
+ if execution.output_dir is not None and execution.resolved_num_gpus() <= 1:
322
322
  local_persist_callback, _, _ = persist_callbacks.build_incremental_persist_callback(
323
323
  model=model,
324
324
  preprocessing=preprocessing,
@@ -334,7 +334,7 @@ def embed_slides(
334
334
  work_dir=work_dir,
335
335
  on_embedded_slide=local_persist_callback,
336
336
  )
337
- if execution.output_dir is not None and execution.num_gpus > 1:
337
+ if execution.output_dir is not None and execution.resolved_num_gpus() > 1:
338
338
  tile_artifacts: list[TileEmbeddingArtifact] = []
339
339
  hierarchical_artifacts: list[HierarchicalEmbeddingArtifact] = []
340
340
  slide_artifacts: list[SlideEmbeddingArtifact] = []
@@ -464,7 +464,7 @@ def embed_patients(
464
464
  slide_records,
465
465
  preprocessing,
466
466
  output_dir=work_dir,
467
- num_workers=execution.num_preprocessing_workers,
467
+ num_workers=execution.resolved_num_preprocessing_workers(),
468
468
  )
469
469
  process_list.emit_tiling_summary(
470
470
  process_list_path,
@@ -562,7 +562,7 @@ def _select_embedding_path(
562
562
  work_dir: Path,
563
563
  on_embedded_slide: Callable[[SlideSpec, Any, EmbeddedSlide], None] | None = None,
564
564
  ):
565
- if execution.num_gpus > 1:
565
+ if execution.resolved_num_gpus() > 1:
566
566
  if len(slide_records) == 1:
567
567
  return [
568
568
  distributed_stage.embed_single_slide_distributed(
@@ -780,7 +780,7 @@ def run_pipeline(
780
780
  # initialize their own CUDA and do not depend on this parent-side check.
781
781
  # Initializing a CUDA context in the parent here only buys needless fork
782
782
  # risk for the downstream tiling pool with no benefit.
783
- if not tiling_only and execution.num_gpus > 1:
783
+ if not tiling_only and execution.resolved_num_gpus() > 1:
784
784
  distributed_stage.validate_multi_gpu_execution(model, execution)
785
785
 
786
786
  resolved_preprocessing = tiling_pipeline.resolve_model_preprocessing(model, preprocessing)
@@ -803,7 +803,7 @@ def run_pipeline(
803
803
  slide_records,
804
804
  resolved_preprocessing,
805
805
  output_dir=output_dir,
806
- num_workers=execution.num_preprocessing_workers,
806
+ num_workers=execution.resolved_num_preprocessing_workers(),
807
807
  )
808
808
  process_list.emit_tiling_summary(
809
809
  process_list_path,
@@ -872,7 +872,7 @@ def run_pipeline(
872
872
  process_list_path=process_list_path,
873
873
  )
874
874
 
875
- if execution.num_gpus > 1:
875
+ if execution.resolved_num_gpus() > 1:
876
876
  tile_artifacts, hierarchical_artifacts, slide_artifacts = artifacts_collect.collect_distributed_pipeline_artifacts(
877
877
  model=model,
878
878
  successful_slides=embeddable_slides,
@@ -990,7 +990,7 @@ def run_pipeline_with_coordinates(
990
990
  ) -> RunResult:
991
991
  if execution.output_dir is None:
992
992
  raise ValueError("ExecutionOptions.output_dir is required for Pipeline.run_with_coordinates(...)")
993
- if execution.num_gpus > 1:
993
+ if execution.resolved_num_gpus() > 1:
994
994
  distributed_stage.validate_multi_gpu_execution(model, execution)
995
995
 
996
996
  output_dir = Path(execution.output_dir)
@@ -1036,7 +1036,7 @@ def run_pipeline_with_coordinates(
1036
1036
  execution=execution,
1037
1037
  )
1038
1038
  emit_progress("embedding.started", slide_count=len(embeddable_slides))
1039
- if execution.num_gpus > 1:
1039
+ if execution.resolved_num_gpus() > 1:
1040
1040
  tile_artifacts, hierarchical_artifacts, slide_artifacts = artifacts_collect.collect_distributed_pipeline_artifacts(
1041
1041
  model=model,
1042
1042
  successful_slides=embeddable_slides,
@@ -34,7 +34,7 @@ def serialize_execution(
34
34
  if preprocessing is not None and preprocessing.on_the_fly and preprocessing.read_tiles_from is None:
35
35
  effective_num_workers_per_gpu, _ = resolve_on_the_fly_num_workers(
36
36
  preprocessing.num_cucim_workers,
37
- num_gpus=execution.num_gpus,
37
+ num_gpus=execution.resolved_num_gpus(),
38
38
  )
39
39
  return serialize_execution_dict(
40
40
  execution,
@@ -56,7 +56,7 @@ def log_on_the_fly_worker_override_once(
56
56
  return
57
57
  effective_num_workers_per_gpu, worker_context = resolve_on_the_fly_num_workers(
58
58
  preprocessing.num_cucim_workers,
59
- num_gpus=execution.num_gpus,
59
+ num_gpus=execution.resolved_num_gpus(),
60
60
  )
61
61
  if effective_num_workers_per_gpu == execution.resolved_num_workers_per_gpu():
62
62
  return
@@ -179,7 +179,7 @@ def embed_images_dense(
179
179
  recipe,
180
180
  read_plans=read_plans,
181
181
  resolve_transform=deferred_transform_record(
182
- model, on_cpu_copy=execution.num_gpus > 1
182
+ model, on_cpu_copy=execution.resolved_num_gpus() > 1
183
183
  ),
184
184
  )
185
185
  if skipped:
@@ -192,10 +192,10 @@ def embed_images_dense(
192
192
  total=len(specs),
193
193
  skipped=skipped,
194
194
  encoding=len(remaining),
195
- num_gpus=execution.num_gpus,
195
+ num_gpus=execution.resolved_num_gpus(),
196
196
  )
197
197
  if remaining:
198
- if execution.num_gpus == 1:
198
+ if execution.resolved_num_gpus() == 1:
199
199
  _run_dense_images_in_process(
200
200
  model,
201
201
  remaining,
@@ -286,7 +286,7 @@ def _run_dense_images_distributed(
286
286
  run_torchrun_worker(
287
287
  module="slide2vec.distributed.dense_image_worker",
288
288
  pin_gpus=True, # No collectives: each rank needs only its own GPU.
289
- num_gpus=execution.num_gpus,
289
+ num_gpus=execution.resolved_num_gpus(),
290
290
  output_dir=out_dir,
291
291
  request_path=request_path,
292
292
  failure_title="Distributed dense image feature extraction failed",
@@ -207,7 +207,7 @@ def embed_regions_dense(
207
207
  out_dir,
208
208
  identity,
209
209
  resolve_transform=deferred_transform_record(
210
- model, on_cpu_copy=execution.num_gpus > 1
210
+ model, on_cpu_copy=execution.resolved_num_gpus() > 1
211
211
  ),
212
212
  )
213
213
  if skipped:
@@ -220,10 +220,10 @@ def embed_regions_dense(
220
220
  total=len(flat),
221
221
  skipped=skipped,
222
222
  encoding=len(remaining),
223
- num_gpus=execution.num_gpus,
223
+ num_gpus=execution.resolved_num_gpus(),
224
224
  )
225
225
  if remaining:
226
- if execution.num_gpus == 1:
226
+ if execution.resolved_num_gpus() == 1:
227
227
  _run_dense_in_process(
228
228
  model, remaining, dense=dense, execution=execution, out_dir=out_dir,
229
229
  identity=identity,
@@ -323,7 +323,7 @@ def _run_dense_distributed(model, specs, *, dense, execution, out_dir) -> None:
323
323
  run_torchrun_worker(
324
324
  module="slide2vec.distributed.dense_worker",
325
325
  pin_gpus=True, # No collectives: each rank needs only its own GPU.
326
- num_gpus=execution.num_gpus,
326
+ num_gpus=execution.resolved_num_gpus(),
327
327
  output_dir=out_dir,
328
328
  request_path=request_path,
329
329
  failure_title="Distributed dense feature extraction failed",
@@ -41,9 +41,9 @@ def validate_multi_gpu_execution(model, execution: ExecutionOptions) -> None:
41
41
  if not torch.cuda.is_available():
42
42
  raise RuntimeError("ExecutionOptions.num_gpus > 1 requires CUDA")
43
43
  available_gpus = int(torch.cuda.device_count())
44
- if execution.num_gpus > available_gpus:
44
+ if execution.resolved_num_gpus() > available_gpus:
45
45
  raise ValueError(
46
- f"ExecutionOptions.num_gpus={execution.num_gpus} exceeds available CUDA devices ({available_gpus})"
46
+ f"ExecutionOptions.num_gpus={execution.resolved_num_gpus()} exceeds available CUDA devices ({available_gpus})"
47
47
  )
48
48
 
49
49
 
@@ -140,16 +140,16 @@ def run_distributed_embedding_stage(
140
140
  emit_progress(
141
141
  "embedding.assignment.started",
142
142
  slide_count=len(successful_slides),
143
- num_gpus=execution.num_gpus,
143
+ num_gpus=execution.resolved_num_gpus(),
144
144
  )
145
145
  emit_progress(
146
146
  "embedding.assignment.finished",
147
147
  slide_count=len(successful_slides),
148
- num_gpus=execution.num_gpus,
148
+ num_gpus=execution.resolved_num_gpus(),
149
149
  )
150
150
  run_torchrun_worker(
151
151
  module="slide2vec.distributed.pipeline_worker",
152
- num_gpus=execution.num_gpus,
152
+ num_gpus=execution.resolved_num_gpus(),
153
153
  output_dir=output_dir,
154
154
  request_path=request_path,
155
155
  failure_title="Distributed feature extraction failed",
@@ -187,7 +187,7 @@ def run_distributed_direct_embedding_stage(
187
187
  request_path.write_text(json.dumps(request_payload, indent=2, sort_keys=True), encoding="utf-8")
188
188
  run_torchrun_worker(
189
189
  module="slide2vec.distributed.direct_embed_worker",
190
- num_gpus=execution.num_gpus,
190
+ num_gpus=execution.resolved_num_gpus(),
191
191
  output_dir=output_dir,
192
192
  request_path=request_path,
193
193
  failure_title="Distributed direct embedding failed",
@@ -276,7 +276,7 @@ def embed_multi_slides_distributed(
276
276
  assignments = assign_slides_to_ranks(
277
277
  slide_records,
278
278
  tiling_results,
279
- num_gpus=execution.num_gpus,
279
+ num_gpus=execution.resolved_num_gpus(),
280
280
  )
281
281
  with distributed_coordination_dir(work_dir) as coordination_dir:
282
282
  run_distributed_direct_embedding_stage(
@@ -144,7 +144,7 @@ def compute_tile_embeddings_for_slide(
144
144
  if preprocessing.on_the_fly and preprocessing.read_tiles_from is None and resolved_backend == "cucim":
145
145
  effective_num_workers, _ = resolve_on_the_fly_num_workers(
146
146
  preprocessing.num_cucim_workers,
147
- num_gpus=execution.num_gpus,
147
+ num_gpus=execution.resolved_num_gpus(),
148
148
  )
149
149
  loader_kwargs["num_workers"] = effective_num_workers
150
150
  if effective_num_workers == 0:
@@ -280,7 +280,7 @@ def _encode_hierarchical_subtiles(
280
280
  if resolved_backend == "cucim":
281
281
  effective_num_workers, _ = resolve_on_the_fly_num_workers(
282
282
  preprocessing.num_cucim_workers,
283
- num_gpus=execution.num_gpus,
283
+ num_gpus=execution.resolved_num_gpus(),
284
284
  )
285
285
  loader_kwargs["num_workers"] = effective_num_workers
286
286
  if effective_num_workers == 0:
@@ -275,7 +275,7 @@ def embed_images(model, images: Sequence[ImageSpec], *, execution) -> list[Image
275
275
  output_format=execution.output_format,
276
276
  identity=identity,
277
277
  resolve_transform=deferred_transform_record(
278
- model, on_cpu_copy=execution.num_gpus > 1
278
+ model, on_cpu_copy=execution.resolved_num_gpus() > 1
279
279
  ),
280
280
  on_image_mismatch=execution.on_image_mismatch,
281
281
  )
@@ -291,7 +291,7 @@ def embed_images(model, images: Sequence[ImageSpec], *, execution) -> list[Image
291
291
  total=len(specs),
292
292
  skipped=skipped,
293
293
  encoding=len(remaining),
294
- num_gpus=execution.num_gpus,
294
+ num_gpus=execution.resolved_num_gpus(),
295
295
  )
296
296
 
297
297
  # The shard loop invalidates each image's stale files right before writing it, so a
@@ -300,7 +300,7 @@ def embed_images(model, images: Sequence[ImageSpec], *, execution) -> list[Image
300
300
  # encodes nothing deletes nothing.
301
301
  encoded_width = None
302
302
  if remaining:
303
- if execution.num_gpus == 1:
303
+ if execution.resolved_num_gpus() == 1:
304
304
  encoded_width = _run_images_in_process(
305
305
  model, remaining, execution=execution, out_dir=out_dir, identity=identity,
306
306
  stale=plan.stale,
@@ -417,7 +417,7 @@ def _run_images_distributed(model, specs, *, execution, out_dir, stale) -> int:
417
417
  run_torchrun_worker(
418
418
  module="slide2vec.distributed.image_worker",
419
419
  pin_gpus=True, # No collectives: each rank needs only its own GPU.
420
- num_gpus=execution.num_gpus,
420
+ num_gpus=execution.resolved_num_gpus(),
421
421
  output_dir=out_dir,
422
422
  request_path=request_path,
423
423
  failure_title="Distributed image feature extraction failed",
@@ -87,8 +87,10 @@ def serialize_execution(
87
87
  if effective_num_workers_per_gpu is not None
88
88
  else execution.num_workers_per_gpu
89
89
  ),
90
- "num_preprocessing_workers": execution.num_preprocessing_workers,
91
- "num_gpus": execution.num_gpus,
90
+ # The launcher's resolution, not the request: a torchrun rank pinned to one GPU must
91
+ # still split its CPU budget across the launcher's world size.
92
+ "num_preprocessing_workers": execution.resolved_num_preprocessing_workers(),
93
+ "num_gpus": execution.resolved_num_gpus(),
92
94
  "precision": execution.precision,
93
95
  "output_dtype": execution.output_dtype,
94
96
  "prefetch_factor": execution.prefetch_factor,
@@ -89,6 +89,7 @@ def describe_device_mode(model, execution: ExecutionOptions) -> str:
89
89
  requested_device = getattr(model, "_requested_device", None)
90
90
  if requested_device == "cpu":
91
91
  return "cpu"
92
- if execution.num_gpus and execution.num_gpus > 1:
93
- return f"{execution.num_gpus} gpus"
92
+ num_gpus = execution.resolved_num_gpus()
93
+ if num_gpus > 1:
94
+ return f"{num_gpus} gpus"
94
95
  return "gpu"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 7.1.0
3
+ Version: 7.1.1
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
@@ -141,6 +141,7 @@ tests/test_encoder_plugins.py
141
141
  tests/test_encoder_preprocessing.py
142
142
  tests/test_encoder_provider_failures.py
143
143
  tests/test_encoder_registry.py
144
+ tests/test_execution_options.py
144
145
  tests/test_feature_identity.py
145
146
  tests/test_gigapath.py
146
147
  tests/test_gpfm_genbio_heavy.py