slide2vec 6.0.0__tar.gz → 6.1.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 (174) hide show
  1. {slide2vec-6.0.0 → slide2vec-6.1.0}/PKG-INFO +3 -3
  2. {slide2vec-6.0.0 → slide2vec-6.1.0}/pyproject.toml +5 -5
  3. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/__init__.py +5 -1
  4. slide2vec-6.1.0/slide2vec/_cufile.py +21 -0
  5. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/api.py +8 -3
  6. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/configs/default.yaml +1 -1
  7. slide2vec-6.1.0/slide2vec/cufile.json +5 -0
  8. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/distributed/__init__.py +17 -2
  9. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/distributed/direct_embed_worker.py +1 -1
  10. slide2vec-6.1.0/slide2vec/distributed/pin_gpu.py +47 -0
  11. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/distributed/pipeline_worker.py +1 -1
  12. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/distributed/worker_entry.py +14 -5
  13. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/dense_image_stage.py +1 -0
  14. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/dense_stage.py +1 -0
  15. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/distributed.py +19 -3
  16. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/distributed_stage.py +2 -0
  17. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/image_stage.py +1 -0
  18. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec.egg-info/PKG-INFO +3 -3
  19. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec.egg-info/SOURCES.txt +7 -1
  20. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec.egg-info/requires.txt +2 -2
  21. slide2vec-6.1.0/tests/test_cufile_compat_mode.py +50 -0
  22. slide2vec-6.1.0/tests/test_hs2p5_integration.py +207 -0
  23. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_hs2p_package_cutover.py +3 -3
  24. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_regression_core.py +16 -0
  25. slide2vec-6.1.0/tests/test_worker_gpu_pinning.py +234 -0
  26. {slide2vec-6.0.0 → slide2vec-6.1.0}/LICENSE +0 -0
  27. {slide2vec-6.0.0 → slide2vec-6.1.0}/README.md +0 -0
  28. {slide2vec-6.0.0 → slide2vec-6.1.0}/setup.cfg +0 -0
  29. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/__main__.py +0 -0
  30. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/artifacts.py +0 -0
  31. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/cli.py +0 -0
  32. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/configs/__init__.py +0 -0
  33. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/configs/resources.py +0 -0
  34. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/data/__init__.py +0 -0
  35. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/data/dataset.py +0 -0
  36. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/data/tile_reader.py +0 -0
  37. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/data/tile_store.py +0 -0
  38. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/distributed/dense_image_worker.py +0 -0
  39. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/distributed/dense_worker.py +0 -0
  40. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/distributed/image_worker.py +0 -0
  41. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/__init__.py +0 -0
  42. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/base.py +0 -0
  43. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/__init__.py +0 -0
  44. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/conch.py +0 -0
  45. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/dinov2.py +0 -0
  46. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/dinov3.py +0 -0
  47. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/genbio.py +0 -0
  48. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/gigapath.py +0 -0
  49. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/gpfm.py +0 -0
  50. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/hibou.py +0 -0
  51. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/hoptimus.py +0 -0
  52. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/isight.py +0 -0
  53. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/lunit.py +0 -0
  54. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/midnight.py +0 -0
  55. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/moozy/__init__.py +0 -0
  56. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/moozy/blocks.py +0 -0
  57. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/moozy/case.py +0 -0
  58. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/moozy/loading.py +0 -0
  59. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/moozy/slide.py +0 -0
  60. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/moozy/types.py +0 -0
  61. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/mstar.py +0 -0
  62. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/musk.py +0 -0
  63. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/phikon.py +0 -0
  64. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/prism.py +0 -0
  65. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/prism2.py +0 -0
  66. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/prost40m.py +0 -0
  67. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/rudolfv2.py +0 -0
  68. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/titan.py +0 -0
  69. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/uni.py +0 -0
  70. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/virchow.py +0 -0
  71. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/models/waiv.py +0 -0
  72. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/registry.py +0 -0
  73. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/encoders/validation.py +0 -0
  74. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/inference.py +0 -0
  75. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/progress.py +0 -0
  76. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/__init__.py +0 -0
  77. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/artifacts_collect.py +0 -0
  78. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/batching.py +0 -0
  79. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/cpu_budget.py +0 -0
  80. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/dense_encode.py +0 -0
  81. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/dense_encoder_input.py +0 -0
  82. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/dense_image_reading.py +0 -0
  83. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/dense_image_recipe.py +0 -0
  84. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/dense_image_shard.py +0 -0
  85. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/dense_regions.py +0 -0
  86. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/dense_shard.py +0 -0
  87. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/dense_sliding.py +0 -0
  88. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/effective_encoder_input.py +0 -0
  89. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/embedding.py +0 -0
  90. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/embedding_persist.py +0 -0
  91. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/embedding_pipeline.py +0 -0
  92. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/encoder_input_contract.py +0 -0
  93. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/hierarchical.py +0 -0
  94. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/image_shard.py +0 -0
  95. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/image_specs.py +0 -0
  96. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/manifest.py +0 -0
  97. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/model_settings.py +0 -0
  98. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/patient_pipeline.py +0 -0
  99. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/persist_callbacks.py +0 -0
  100. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/persistence.py +0 -0
  101. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/pooled_encoder_input.py +0 -0
  102. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/preprocessing.py +0 -0
  103. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/process_list.py +0 -0
  104. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/progress_bridge.py +0 -0
  105. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/registry.py +0 -0
  106. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/serialization.py +0 -0
  107. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/sharding.py +0 -0
  108. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/slide_encode.py +0 -0
  109. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/tiling.py +0 -0
  110. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/tiling_pipeline.py +0 -0
  111. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/types.py +0 -0
  112. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/runtime/worker_io.py +0 -0
  113. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/utils/__init__.py +0 -0
  114. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/utils/config.py +0 -0
  115. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/utils/coordinates.py +0 -0
  116. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/utils/log_utils.py +0 -0
  117. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/utils/tiling_io.py +0 -0
  118. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec/utils/utils.py +0 -0
  119. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec.egg-info/dependency_links.txt +0 -0
  120. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec.egg-info/entry_points.txt +0 -0
  121. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec.egg-info/not-zip-safe +0 -0
  122. {slide2vec-6.0.0 → slide2vec-6.1.0}/slide2vec.egg-info/top_level.txt +0 -0
  123. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_architecture_runtime_split.py +0 -0
  124. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_attention_extraction.py +0 -0
  125. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_benchmark_tooling.py +0 -0
  126. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_encode_kit.py +0 -0
  127. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_encoder_input.py +0 -0
  128. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_extraction.py +0 -0
  129. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_image_reading.py +0 -0
  130. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_image_resume.py +0 -0
  131. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_image_shard.py +0 -0
  132. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_image_stage.py +0 -0
  133. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_regions.py +0 -0
  134. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_shard.py +0 -0
  135. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_sliding.py +0 -0
  136. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_source_spacing.py +0 -0
  137. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_stage.py +0 -0
  138. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dense_worker.py +0 -0
  139. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dinov2_natimage.py +0 -0
  140. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_dinov3_natimage.py +0 -0
  141. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_encoder_capabilities.py +0 -0
  142. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_encoder_input_contract.py +0 -0
  143. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_encoder_plugins.py +0 -0
  144. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_encoder_preprocessing.py +0 -0
  145. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_encoder_provider_failures.py +0 -0
  146. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_encoder_registry.py +0 -0
  147. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_gpfm_genbio_heavy.py +0 -0
  148. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_image_shard.py +0 -0
  149. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_image_stage.py +0 -0
  150. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_inference_benchmark.py +0 -0
  151. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_isight.py +0 -0
  152. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_load_model_hf_auth.py +0 -0
  153. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_mascaret.py +0 -0
  154. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_on_slide_persisted.py +0 -0
  155. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_output_consistency.py +0 -0
  156. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_patch_size_metadata.py +0 -0
  157. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_patient_manifest.py +0 -0
  158. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_phaet.py +0 -0
  159. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_pooled_encoder_input.py +0 -0
  160. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_pooled_geometry.py +0 -0
  161. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_prism.py +0 -0
  162. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_prism2.py +0 -0
  163. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_process_list_performance.py +0 -0
  164. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_progress.py +0 -0
  165. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_regression_inference.py +0 -0
  166. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_regression_models.py +0 -0
  167. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_rudolfv2.py +0 -0
  168. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_runtime_batching.py +0 -0
  169. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_sharding.py +0 -0
  170. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_slide_coordinate_preparation.py +0 -0
  171. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_soma_migration.py +0 -0
  172. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_tile_store.py +0 -0
  173. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_tiling_pipeline.py +0 -0
  174. {slide2vec-6.0.0 → slide2vec-6.1.0}/tests/test_titan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 6.0.0
3
+ Version: 6.1.0
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.3
18
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=5.0.0
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.3; extra == "fm"
78
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=5.0.0; 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 = "6.0.0"
7
+ version = "6.1.0"
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.3",
24
+ "hs2p[asap,cucim,openslide,sam2,vips]>=5.0.0",
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.3",
103
+ "hs2p[asap,cucim,openslide,sam2,vips]>=5.0.0",
104
104
  "wandb",
105
105
  "torch>=2.3,<2.8",
106
106
  "torchvision>=0.18.0",
@@ -149,7 +149,7 @@ zip-safe = false
149
149
  include = ["slide2vec*"]
150
150
 
151
151
  [tool.setuptools.package-data]
152
- slide2vec = ["py.typed"]
152
+ slide2vec = ["py.typed", "cufile.json"]
153
153
  "slide2vec.configs" = ["*.yaml", "models/*.yaml", "preprocessing/*.yaml"]
154
154
 
155
155
  [tool.pytest.ini_options]
@@ -180,7 +180,7 @@ no_implicit_reexport = true
180
180
  max-line-length = 160
181
181
 
182
182
  [tool.bumpver]
183
- current_version = "6.0.0"
183
+ current_version = "6.1.0"
184
184
  version_pattern = "MAJOR.MINOR.PATCH"
185
185
  commit = false
186
186
  tag = false
@@ -1,3 +1,7 @@
1
+ from slide2vec._cufile import disable_cufile_compat_mode
2
+
3
+ disable_cufile_compat_mode() # Before anything can import cucim.
4
+
1
5
  from slide2vec.api import (
2
6
  DenseImageOptions,
3
7
  DenseOptions,
@@ -27,7 +31,7 @@ from slide2vec.encoders import (
27
31
  )
28
32
 
29
33
 
30
- __version__ = "6.0.0"
34
+ __version__ = "6.1.0"
31
35
 
32
36
  __all__ = [
33
37
  "Model",
@@ -0,0 +1,21 @@
1
+ """Keep ``import cucim`` from opening a CUDA context on every visible GPU (issue #326).
2
+
3
+ cuCIM opens the cuFile (GPUDirect Storage) driver when it is imported. Without the
4
+ ``nvidia-fs`` kernel module the driver falls back to "compat mode", which still creates a
5
+ ~520 MiB CUDA context on each visible device although every read then goes through plain
6
+ POSIX I/O. Pointing cuFile at a config that disallows compat mode makes the driver open fail
7
+ instead: cuCIM reads through POSIX exactly as before, and no context is created. Where
8
+ ``nvidia-fs`` is loaded the driver opens as usual and GDS keeps working.
9
+
10
+ cuFile reads ``CUFILE_ENV_PATH_JSON`` once, when cuCIM is imported, so this must run before
11
+ that import: ``slide2vec/__init__.py`` calls it first. A value the user already set wins.
12
+ """
13
+
14
+ import os
15
+ from pathlib import Path
16
+
17
+ CUFILE_ENV = "CUFILE_ENV_PATH_JSON"
18
+
19
+
20
+ def disable_cufile_compat_mode(environ=os.environ) -> None:
21
+ environ.setdefault(CUFILE_ENV, str(Path(__file__).with_name("cufile.json")))
@@ -145,13 +145,18 @@ class PreprocessingConfig:
145
145
 
146
146
  #: Slide reading backend. ``"auto"`` tries cucim → vips → openslide → asap.
147
147
  #: Explicit choices: ``"cucim"``, ``"openslide"``, ``"vips"``, ``"asap"``.
148
+ #: Slides without spacing metadata require ``spacing_at_level_0``, including
149
+ #: untagged TIFFs read with VIPS.
148
150
  backend: str = "auto"
149
- #: Source-mask reading backend, resolved independently from the *mask* path
150
- #: (hs2p ≥ 4.3.0). ``"auto"`` probes openability just like :attr:`backend`. Set this
151
+ #: Source-mask reading backend, resolved independently from the *mask* path.
152
+ #: ``"auto"`` probes openability just like :attr:`backend`. Set this
151
153
  #: explicitly (e.g. ``"openslide"``) when a precomputed tissue or annotation mask needs
152
154
  #: a different decoder than its slide — hs2p no longer silently falls back to another
153
155
  #: reader, so a mask the slide backend cannot decode fails unless overridden here.
154
- #: Accepts the same values as :attr:`backend`; ignored for slides with no source mask.
156
+ #: Accepts the same values as :attr:`backend` plus ``"pil"`` for PNG/JPEG masks;
157
+ #: ``"auto"`` selects PIL for these masks. Ignored for slides with no source mask.
158
+ #: Masks need no spacing metadata, but must align to the slide's field of view and
159
+ #: contain only declared label IDs. See :doc:`preprocessing` for hs2p 5 validation.
155
160
  mask_backend: str = "auto"
156
161
  #: Target spacing in µm/px. Resolved from the model preset when ``None``.
157
162
  requested_spacing_um: float | None = None
@@ -36,7 +36,7 @@ tiling:
36
36
  # where `mask_path` is read as a multi-label raster. The `tissue` min_coverage entry below is
37
37
  # the single source of truth for the tissue threshold.
38
38
  output_mode: per_annotation # how sampled tiles are grouped into artifacts. per_annotation: one flat artifact set per sampled class, namespaced under a `<class>/` subdir (the `tissue` class collapses to the flat root). merged: a single flat artifact set per slide over the union of tiles passing any active class threshold — it carries no class label, so it lands at the flat output root (no `<class>/` subdir).
39
- pixel_mapping: # {class_name: integer pixel value in the mask raster}
39
+ pixel_mapping: # {class_name: integer pixel value in the mask raster}; a list (e.g. tumor: [2, 3]) merges several values into one class; every mask value must be declared
40
40
  background: 0
41
41
  tissue: 1
42
42
  colors: # {class_name: [r, g, b] | null} used when rendering previews
@@ -0,0 +1,5 @@
1
+ {
2
+ "properties": {
3
+ "allow_compat_mode": false
4
+ }
5
+ }
@@ -2,6 +2,8 @@ import os
2
2
 
3
3
  import torch
4
4
 
5
+ from slide2vec.distributed.worker_entry import PINNED_ENV
6
+
5
7
  _RANK = -1
6
8
  _WORLD_SIZE = -1
7
9
  _LOCAL_RANK = -1
@@ -43,6 +45,19 @@ def get_local_rank() -> int:
43
45
  return _LOCAL_RANK
44
46
 
45
47
 
48
+ def get_device_ordinal() -> int:
49
+ """
50
+ Returns:
51
+ The CUDA ordinal of this process's GPU: 0 once :mod:`slide2vec.distributed.pin_gpu`
52
+ has left it the only visible device, else the local rank.
53
+ """
54
+ return _device_ordinal(get_local_rank())
55
+
56
+
57
+ def _device_ordinal(local_rank: int) -> int:
58
+ return 0 if os.environ.get(PINNED_ENV) == "1" else local_rank
59
+
60
+
46
61
  def get_local_size() -> int:
47
62
  """
48
63
  Returns:
@@ -181,7 +196,7 @@ def enable(
181
196
 
182
197
  Args:
183
198
  set_cuda_current_device: If True, call torch.cuda.set_device() to set the
184
- current PyTorch CUDA device to the one matching the local rank.
199
+ current PyTorch CUDA device to this rank's GPU (see get_device_ordinal()).
185
200
  overwrite: If True, overwrites already set variables. Else fails.
186
201
  """
187
202
 
@@ -192,7 +207,7 @@ def enable(
192
207
  torch_env.export(overwrite=overwrite)
193
208
 
194
209
  if set_cuda_current_device:
195
- torch.cuda.set_device(torch_env.local_rank)
210
+ torch.cuda.set_device(_device_ordinal(torch_env.local_rank))
196
211
 
197
212
  _RANK = torch_env.rank
198
213
  _WORLD_SIZE = torch_env.world_size
@@ -42,7 +42,7 @@ def main(argv=None) -> int:
42
42
  model_spec = dict(request["model"])
43
43
  model = Model.from_preset(
44
44
  model_spec["name"],
45
- device=f"cuda:{local_rank}",
45
+ device=f"cuda:{distributed.get_device_ordinal()}",
46
46
  output_variant=model_spec.get("output_variant"),
47
47
  allow_non_recommended_settings=bool(model_spec["allow_non_recommended_settings"]),
48
48
  )
@@ -0,0 +1,47 @@
1
+ """Bootstrap that shows a torchrun worker only its own GPU, then runs the worker (issue #326).
2
+
3
+ ``import cucim`` opens a CUDA context on every visible device, so a rank that can see its
4
+ siblings' GPUs holds an idle context on each of them. CUDA reads ``CUDA_VISIBLE_DEVICES``
5
+ once, when it initialises, and ``import slide2vec`` already initialises it (``transformers``
6
+ calls ``torch.cuda.is_available()`` at import). A worker module therefore cannot narrow its
7
+ own visibility: ``python -m slide2vec.distributed.<worker>`` has imported the package before
8
+ the worker's first line runs. torchrun runs this file **by path** instead, so the narrowing
9
+ happens before anything is imported. Keep this file free of slide2vec and torch imports.
10
+
11
+ Only for workers that run no collectives: a pinned rank cannot see its siblings' GPUs.
12
+
13
+ Usage: ``torchrun ... pin_gpu.py <worker module> [worker args...]``
14
+ """
15
+
16
+ import os
17
+ import runpy
18
+ import sys
19
+
20
+ PINNED_ENV = "SLIDE2VEC_WORKER_GPU_PINNED" # Mirrored in worker_entry.PINNED_ENV.
21
+
22
+
23
+ def pin_to_own_gpu(environ) -> None:
24
+ """Narrow ``CUDA_VISIBLE_DEVICES`` to the ``LOCAL_RANK``-th GPU this process inherited."""
25
+ local_rank = int(environ["LOCAL_RANK"])
26
+ visible = [gpu.strip() for gpu in environ.get("CUDA_VISIBLE_DEVICES", "").split(",") if gpu.strip()]
27
+ if visible and local_rank >= len(visible):
28
+ raise RuntimeError(
29
+ f"LOCAL_RANK={local_rank} but CUDA_VISIBLE_DEVICES lists only {len(visible)} GPU(s)"
30
+ )
31
+ environ["CUDA_VISIBLE_DEVICES"] = visible[local_rank] if visible else str(local_rank)
32
+ environ[PINNED_ENV] = "1"
33
+
34
+
35
+ def main() -> None:
36
+ pin_to_own_gpu(os.environ)
37
+ module, *worker_args = sys.argv[1:]
38
+ # Prefer the package that supplied this bootstrap over a competing checkout in cwd.
39
+ # Keep cwd importable without changing how relative data and output paths resolve.
40
+ package_root = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", ".."))
41
+ sys.path[:1] = [package_root, os.getcwd()]
42
+ sys.argv = [module, *worker_args]
43
+ runpy.run_module(module, run_name="__main__", alter_sys=True)
44
+
45
+
46
+ if __name__ == "__main__":
47
+ main()
@@ -36,7 +36,7 @@ def main(argv=None) -> int:
36
36
  model_spec = dict(request["model"])
37
37
  model = Model.from_preset(
38
38
  model_spec["name"],
39
- device=f"cuda:{local_rank}",
39
+ device=f"cuda:{distributed.get_device_ordinal()}",
40
40
  output_variant=model_spec.get("output_variant"),
41
41
  allow_non_recommended_settings=bool(model_spec["allow_non_recommended_settings"]),
42
42
  )
@@ -18,6 +18,10 @@ from contextlib import nullcontext
18
18
  from dataclasses import dataclass
19
19
 
20
20
 
21
+ #: Set by :mod:`slide2vec.distributed.pin_gpu` once this process sees only its own GPU.
22
+ PINNED_ENV = "SLIDE2VEC_WORKER_GPU_PINNED"
23
+
24
+
21
25
  @dataclass(frozen=True, kw_only=True)
22
26
  class WorkerRank:
23
27
  """This process's place in the torchrun world: which shard, and which GPU."""
@@ -26,12 +30,14 @@ class WorkerRank:
26
30
  global_rank: int
27
31
  #: Number of shards the work list is cut into.
28
32
  world_size: int
29
- #: Local device ordinal, i.e. ``cuda:<local_rank>``.
33
+ #: This process's index among the ranks on its node; names it in progress labels.
30
34
  local_rank: int
35
+ #: CUDA ordinal of this rank's GPU: 0 once the process is pinned to it, else ``local_rank``.
36
+ device_ordinal: int
31
37
 
32
38
  @property
33
39
  def device(self) -> str:
34
- return f"cuda:{self.local_rank}"
40
+ return f"cuda:{self.device_ordinal}"
35
41
 
36
42
 
37
43
  def worker_args_parser(module: str, *, add_help: bool = True) -> argparse.ArgumentParser:
@@ -46,12 +52,15 @@ def resolve_worker_rank() -> WorkerRank:
46
52
  """Read this process's rank from the env torchrun exports.
47
53
 
48
54
  Defaults to a single in-process rank when the module is launched bare, so a worker can
49
- be run by hand (or in a test) without a torchrun agent.
55
+ be run by hand (or in a test) without a torchrun agent. A worker launched through
56
+ :mod:`slide2vec.distributed.pin_gpu` sees only its own GPU, which is then ``cuda:0``.
50
57
  """
58
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
51
59
  return WorkerRank(
52
60
  global_rank=int(os.environ.get("RANK", "0")),
53
61
  world_size=int(os.environ.get("WORLD_SIZE", "1")),
54
- local_rank=int(os.environ.get("LOCAL_RANK", "0")),
62
+ local_rank=local_rank,
63
+ device_ordinal=0 if os.environ.get(PINNED_ENV) == "1" else local_rank,
55
64
  )
56
65
 
57
66
 
@@ -82,6 +91,6 @@ def worker_progress_context(request: dict, *, rank: WorkerRank):
82
91
  return nullcontext()
83
92
  return activate_progress_reporter(
84
93
  JsonlProgressReporter(
85
- progress_events_path, rank=rank.global_rank, progress_label=rank.device
94
+ progress_events_path, rank=rank.global_rank, progress_label=f"cuda:{rank.local_rank}"
86
95
  )
87
96
  )
@@ -275,6 +275,7 @@ def _run_dense_images_distributed(
275
275
  request_path.write_text(json.dumps(request, indent=2, sort_keys=True), encoding="utf-8")
276
276
  run_torchrun_worker(
277
277
  module="slide2vec.distributed.dense_image_worker",
278
+ pin_gpus=True, # No collectives: each rank needs only its own GPU.
278
279
  num_gpus=execution.num_gpus,
279
280
  output_dir=out_dir,
280
281
  request_path=request_path,
@@ -297,6 +297,7 @@ def _run_dense_distributed(model, specs, *, dense, execution, out_dir) -> None:
297
297
  request_path.write_text(json.dumps(request, indent=2, sort_keys=True), encoding="utf-8")
298
298
  run_torchrun_worker(
299
299
  module="slide2vec.distributed.dense_worker",
300
+ pin_gpus=True, # No collectives: each rank needs only its own GPU.
300
301
  num_gpus=execution.num_gpus,
301
302
  output_dir=out_dir,
302
303
  request_path=request_path,
@@ -158,18 +158,28 @@ def run_torchrun_worker(
158
158
  output_dir: Path,
159
159
  request_path: Path,
160
160
  failure_title: str,
161
+ pin_gpus: bool = False,
161
162
  progress_events_path: Path | None = None,
162
163
  progress_event_callback: Callable[[Any], None] | None = None,
163
164
  popen_factory=subprocess.Popen,
164
165
  ) -> None:
166
+ """Run ``module`` once per GPU under torchrun.
167
+
168
+ ``pin_gpus`` shows each rank only its own GPU (see :mod:`slide2vec.distributed.pin_gpu`).
169
+ Use it for workers that run no collectives.
170
+ """
171
+ if pin_gpus:
172
+ # By path, not ``-m``: the bootstrap must run before the slide2vec package is imported.
173
+ worker = [str(Path(__file__).resolve().parents[1] / "distributed" / "pin_gpu.py"), module]
174
+ else:
175
+ worker = ["-m", module]
165
176
  command = [
166
177
  sys.executable,
167
178
  "-m",
168
179
  "torch.distributed.run",
169
180
  "--standalone",
170
181
  f"--nproc_per_node={num_gpus}",
171
- "-m",
172
- module,
182
+ *worker,
173
183
  "--output-dir",
174
184
  str(output_dir),
175
185
  "--request-path",
@@ -188,9 +198,15 @@ def run_torchrun_worker(
188
198
  previous_sigterm = signal.signal(signal.SIGTERM, _raise_on_sigterm)
189
199
  except (ValueError, OSError):
190
200
  previous_sigterm = None
201
+ # Workers keep the parent's working directory, so a relative output_dir (and every path
202
+ # derived from it: the request, the tiling artifacts, the progress log) means the same
203
+ # place in both. PYTHONPATH, not cwd, is what lets an uninstalled checkout be imported.
204
+ package_root = str(Path(__file__).resolve().parents[2])
205
+ env = dict(os.environ)
206
+ env["PYTHONPATH"] = os.pathsep.join(filter(None, [package_root, env.get("PYTHONPATH")]))
191
207
  process = popen_factory(
192
208
  command,
193
- cwd=str(Path(__file__).resolve().parents[2]),
209
+ env=env,
194
210
  stdout=subprocess.PIPE,
195
211
  stderr=subprocess.PIPE,
196
212
  text=True,
@@ -153,6 +153,7 @@ def run_distributed_embedding_stage(
153
153
  output_dir=output_dir,
154
154
  request_path=request_path,
155
155
  failure_title="Distributed feature extraction failed",
156
+ pin_gpus=True,
156
157
  progress_events_path=progress_events_path,
157
158
  progress_event_callback=on_progress_event,
158
159
  popen_factory=Popen,
@@ -190,6 +191,7 @@ def run_distributed_direct_embedding_stage(
190
191
  output_dir=output_dir,
191
192
  request_path=request_path,
192
193
  failure_title="Distributed direct embedding failed",
194
+ pin_gpus=True,
193
195
  progress_events_path=progress_events_path,
194
196
  popen_factory=Popen,
195
197
  )
@@ -147,6 +147,7 @@ def _run_images_distributed(model, specs, *, execution, out_dir) -> None:
147
147
  request_path.write_text(json.dumps(request, indent=2, sort_keys=True), encoding="utf-8")
148
148
  run_torchrun_worker(
149
149
  module="slide2vec.distributed.image_worker",
150
+ pin_gpus=True, # No collectives: each rank needs only its own GPU.
150
151
  num_gpus=execution.num_gpus,
151
152
  output_dir=out_dir,
152
153
  request_path=request_path,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 6.0.0
3
+ Version: 6.1.0
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.3
18
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=5.0.0
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.3; extra == "fm"
78
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=5.0.0; 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"
@@ -3,9 +3,11 @@ README.md
3
3
  pyproject.toml
4
4
  slide2vec/__init__.py
5
5
  slide2vec/__main__.py
6
+ slide2vec/_cufile.py
6
7
  slide2vec/api.py
7
8
  slide2vec/artifacts.py
8
9
  slide2vec/cli.py
10
+ slide2vec/cufile.json
9
11
  slide2vec/inference.py
10
12
  slide2vec/progress.py
11
13
  slide2vec.egg-info/PKG-INFO
@@ -27,6 +29,7 @@ slide2vec/distributed/dense_image_worker.py
27
29
  slide2vec/distributed/dense_worker.py
28
30
  slide2vec/distributed/direct_embed_worker.py
29
31
  slide2vec/distributed/image_worker.py
32
+ slide2vec/distributed/pin_gpu.py
30
33
  slide2vec/distributed/pipeline_worker.py
31
34
  slide2vec/distributed/worker_entry.py
32
35
  slide2vec/encoders/__init__.py
@@ -113,6 +116,7 @@ slide2vec/utils/utils.py
113
116
  tests/test_architecture_runtime_split.py
114
117
  tests/test_attention_extraction.py
115
118
  tests/test_benchmark_tooling.py
119
+ tests/test_cufile_compat_mode.py
116
120
  tests/test_dense_encode_kit.py
117
121
  tests/test_dense_encoder_input.py
118
122
  tests/test_dense_extraction.py
@@ -135,6 +139,7 @@ tests/test_encoder_preprocessing.py
135
139
  tests/test_encoder_provider_failures.py
136
140
  tests/test_encoder_registry.py
137
141
  tests/test_gpfm_genbio_heavy.py
142
+ tests/test_hs2p5_integration.py
138
143
  tests/test_hs2p_package_cutover.py
139
144
  tests/test_image_shard.py
140
145
  tests/test_image_stage.py
@@ -163,4 +168,5 @@ tests/test_slide_coordinate_preparation.py
163
168
  tests/test_soma_migration.py
164
169
  tests/test_tile_store.py
165
170
  tests/test_tiling_pipeline.py
166
- tests/test_titan.py
171
+ tests/test_titan.py
172
+ tests/test_worker_gpu_pinning.py
@@ -1,4 +1,4 @@
1
- hs2p[asap,cucim,openslide,sam2,vips]>=4.4.3
1
+ hs2p[asap,cucim,openslide,sam2,vips]>=5.0.0
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.3
30
+ hs2p[asap,cucim,openslide,sam2,vips]>=5.0.0
31
31
  wandb
32
32
  torch<2.8,>=2.3
33
33
  torchvision>=0.18.0
@@ -0,0 +1,50 @@
1
+ """``import cucim`` must not open a CUDA context on every visible GPU (issue #326).
2
+
3
+ Without ``nvidia-fs``, cuFile's compat mode creates an idle context per device at import.
4
+ slide2vec points cuFile at a config that disallows compat mode, before cucim can load.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ import json
11
+ import os
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ import slide2vec
17
+ from slide2vec import _cufile
18
+
19
+
20
+ def test_packaged_config_disallows_compat_mode():
21
+ config = json.loads(Path(slide2vec.__file__).with_name("cufile.json").read_text())
22
+ assert config == {"properties": {"allow_compat_mode": False}}
23
+
24
+
25
+ def test_cufile_is_pointed_at_the_packaged_config():
26
+ environ: dict[str, str] = {}
27
+ _cufile.disable_cufile_compat_mode(environ)
28
+ assert Path(environ[_cufile.CUFILE_ENV]) == Path(slide2vec.__file__).with_name("cufile.json")
29
+
30
+
31
+ def test_a_config_the_user_chose_is_left_alone():
32
+ environ = {_cufile.CUFILE_ENV: "/etc/my-cufile.json"}
33
+ _cufile.disable_cufile_compat_mode(environ)
34
+ assert environ[_cufile.CUFILE_ENV] == "/etc/my-cufile.json"
35
+
36
+
37
+ def test_package_import_sets_it_before_any_other_import():
38
+ # cuFile reads the variable once, when cucim is imported; nothing may get there first.
39
+ first, second = ast.parse(Path(slide2vec.__file__).read_text()).body[:2]
40
+ assert isinstance(first, ast.ImportFrom) and first.module == "slide2vec._cufile"
41
+ assert isinstance(second, ast.Expr) and second.value.func.id == "disable_cufile_compat_mode"
42
+
43
+
44
+ def test_importing_slide2vec_exports_the_variable():
45
+ env = {k: v for k, v in os.environ.items() if k != _cufile.CUFILE_ENV}
46
+ out = subprocess.run(
47
+ [sys.executable, "-c", f"import os, slide2vec; print(os.environ['{_cufile.CUFILE_ENV}'])"],
48
+ env=env, capture_output=True, text=True, check=True, cwd=Path(slide2vec.__file__).parents[1],
49
+ ).stdout.strip().splitlines()[-1]
50
+ assert Path(out).name == "cufile.json" and Path(out).is_file()