slide2vec 6.0.0__tar.gz → 6.0.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.
- {slide2vec-6.0.0 → slide2vec-6.0.1}/PKG-INFO +1 -1
- {slide2vec-6.0.0 → slide2vec-6.0.1}/pyproject.toml +3 -3
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/__init__.py +5 -1
- slide2vec-6.0.1/slide2vec/_cufile.py +21 -0
- slide2vec-6.0.1/slide2vec/cufile.json +5 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/distributed/__init__.py +17 -2
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/distributed/direct_embed_worker.py +1 -1
- slide2vec-6.0.1/slide2vec/distributed/pin_gpu.py +47 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/distributed/pipeline_worker.py +1 -1
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/distributed/worker_entry.py +14 -5
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/dense_image_stage.py +1 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/dense_stage.py +1 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/distributed.py +19 -3
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/distributed_stage.py +2 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/image_stage.py +1 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec.egg-info/PKG-INFO +1 -1
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec.egg-info/SOURCES.txt +6 -1
- slide2vec-6.0.1/tests/test_cufile_compat_mode.py +50 -0
- slide2vec-6.0.1/tests/test_worker_gpu_pinning.py +234 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/LICENSE +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/README.md +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/setup.cfg +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/__main__.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/api.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/artifacts.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/cli.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/configs/__init__.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/configs/default.yaml +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/configs/resources.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/data/__init__.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/data/dataset.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/data/tile_reader.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/data/tile_store.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/distributed/dense_image_worker.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/distributed/dense_worker.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/distributed/image_worker.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/__init__.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/base.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/__init__.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/conch.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/dinov2.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/dinov3.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/genbio.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/gigapath.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/gpfm.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/hibou.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/hoptimus.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/isight.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/lunit.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/midnight.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/moozy/__init__.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/moozy/blocks.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/moozy/case.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/moozy/loading.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/moozy/slide.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/moozy/types.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/mstar.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/musk.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/phikon.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/prism.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/prism2.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/prost40m.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/rudolfv2.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/titan.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/uni.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/virchow.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/models/waiv.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/registry.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/encoders/validation.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/inference.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/progress.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/__init__.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/artifacts_collect.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/batching.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/cpu_budget.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/dense_encode.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/dense_encoder_input.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/dense_image_reading.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/dense_image_recipe.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/dense_image_shard.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/dense_regions.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/dense_shard.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/dense_sliding.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/effective_encoder_input.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/embedding.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/embedding_persist.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/embedding_pipeline.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/encoder_input_contract.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/hierarchical.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/image_shard.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/image_specs.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/manifest.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/model_settings.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/patient_pipeline.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/persist_callbacks.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/persistence.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/pooled_encoder_input.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/preprocessing.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/process_list.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/progress_bridge.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/registry.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/serialization.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/sharding.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/slide_encode.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/tiling.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/tiling_pipeline.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/types.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/runtime/worker_io.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/utils/__init__.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/utils/config.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/utils/coordinates.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/utils/log_utils.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/utils/tiling_io.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec/utils/utils.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec.egg-info/dependency_links.txt +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec.egg-info/entry_points.txt +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec.egg-info/not-zip-safe +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec.egg-info/requires.txt +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/slide2vec.egg-info/top_level.txt +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_architecture_runtime_split.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_attention_extraction.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_benchmark_tooling.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_encode_kit.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_encoder_input.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_extraction.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_image_reading.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_image_resume.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_image_shard.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_image_stage.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_regions.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_shard.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_sliding.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_source_spacing.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_stage.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dense_worker.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dinov2_natimage.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_dinov3_natimage.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_encoder_capabilities.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_encoder_input_contract.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_encoder_plugins.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_encoder_preprocessing.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_encoder_provider_failures.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_encoder_registry.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_gpfm_genbio_heavy.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_hs2p_package_cutover.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_image_shard.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_image_stage.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_inference_benchmark.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_isight.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_load_model_hf_auth.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_mascaret.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_on_slide_persisted.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_output_consistency.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_patch_size_metadata.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_patient_manifest.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_phaet.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_pooled_encoder_input.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_pooled_geometry.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_prism.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_prism2.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_process_list_performance.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_progress.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_regression_core.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_regression_inference.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_regression_models.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_rudolfv2.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_runtime_batching.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_sharding.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_slide_coordinate_preparation.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_soma_migration.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_tile_store.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_tiling_pipeline.py +0 -0
- {slide2vec-6.0.0 → slide2vec-6.0.1}/tests/test_titan.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "slide2vec"
|
|
7
|
-
version = "6.0.
|
|
7
|
+
version = "6.0.1"
|
|
8
8
|
description = "Embedding of whole slide images with Foundation Models"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -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.
|
|
183
|
+
current_version = "6.0.1"
|
|
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.
|
|
34
|
+
__version__ = "6.0.1"
|
|
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")))
|
|
@@ -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
|
|
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:{
|
|
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:{
|
|
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
|
-
#:
|
|
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.
|
|
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=
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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
|
|
@@ -163,4 +167,5 @@ tests/test_slide_coordinate_preparation.py
|
|
|
163
167
|
tests/test_soma_migration.py
|
|
164
168
|
tests/test_tile_store.py
|
|
165
169
|
tests/test_tiling_pipeline.py
|
|
166
|
-
tests/test_titan.py
|
|
170
|
+
tests/test_titan.py
|
|
171
|
+
tests/test_worker_gpu_pinning.py
|
|
@@ -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()
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Each sharded torchrun worker must see only its own GPU (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.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from slide2vec.distributed import pin_gpu, worker_entry
|
|
19
|
+
from slide2vec.distributed.worker_entry import resolve_worker_rank
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_rank_sees_only_its_own_gpu():
|
|
23
|
+
environ = {"LOCAL_RANK": "2"}
|
|
24
|
+
pin_gpu.pin_to_own_gpu(environ)
|
|
25
|
+
assert environ["CUDA_VISIBLE_DEVICES"] == "2"
|
|
26
|
+
assert environ[pin_gpu.PINNED_ENV] == "1"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_rank_picks_its_entry_of_an_inherited_gpu_list():
|
|
30
|
+
environ = {"LOCAL_RANK": "1", "CUDA_VISIBLE_DEVICES": "GPU-aaa, GPU-bbb,GPU-ccc"}
|
|
31
|
+
pin_gpu.pin_to_own_gpu(environ)
|
|
32
|
+
assert environ["CUDA_VISIBLE_DEVICES"] == "GPU-bbb"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_more_ranks_than_visible_gpus_is_an_error():
|
|
36
|
+
with pytest.raises(RuntimeError, match="LOCAL_RANK=2"):
|
|
37
|
+
pin_gpu.pin_to_own_gpu({"LOCAL_RANK": "2", "CUDA_VISIBLE_DEVICES": "0,1"})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_bootstrap_imports_nothing_that_could_initialise_cuda():
|
|
41
|
+
# It runs by path, before the package import that freezes CUDA's visible-device list.
|
|
42
|
+
tree = ast.parse(Path(pin_gpu.__file__).read_text())
|
|
43
|
+
imported = {
|
|
44
|
+
alias.name.split(".")[0]
|
|
45
|
+
for node in ast.walk(tree) if isinstance(node, ast.Import)
|
|
46
|
+
for alias in node.names
|
|
47
|
+
} | {
|
|
48
|
+
(node.module or "").split(".")[0]
|
|
49
|
+
for node in ast.walk(tree) if isinstance(node, ast.ImportFrom)
|
|
50
|
+
}
|
|
51
|
+
assert imported == {"os", "runpy", "sys"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_pinned_worker_uses_cuda_0_but_keeps_its_rank_label(monkeypatch):
|
|
55
|
+
assert worker_entry.PINNED_ENV == pin_gpu.PINNED_ENV
|
|
56
|
+
monkeypatch.setenv("RANK", "3")
|
|
57
|
+
monkeypatch.setenv("WORLD_SIZE", "4")
|
|
58
|
+
monkeypatch.setenv("LOCAL_RANK", "3")
|
|
59
|
+
monkeypatch.setenv(worker_entry.PINNED_ENV, "1")
|
|
60
|
+
rank = resolve_worker_rank()
|
|
61
|
+
assert rank.device == "cuda:0"
|
|
62
|
+
assert (rank.global_rank, rank.local_rank) == (3, 3)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_unpinned_worker_uses_its_local_rank_device(monkeypatch):
|
|
66
|
+
monkeypatch.setenv("LOCAL_RANK", "3")
|
|
67
|
+
monkeypatch.delenv(worker_entry.PINNED_ENV, raising=False)
|
|
68
|
+
assert resolve_worker_rank().device == "cuda:3"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize("pin_gpus", [True, False])
|
|
72
|
+
def test_launcher_runs_the_bootstrap_by_path_only_when_pinning(tmp_path, pin_gpus):
|
|
73
|
+
from slide2vec.runtime.distributed import run_torchrun_worker
|
|
74
|
+
|
|
75
|
+
commands = []
|
|
76
|
+
|
|
77
|
+
class _Done:
|
|
78
|
+
pid = None
|
|
79
|
+
stdout = stderr = None
|
|
80
|
+
|
|
81
|
+
def __init__(self, command, **kwargs):
|
|
82
|
+
commands.append(command)
|
|
83
|
+
|
|
84
|
+
def poll(self):
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
def wait(self, timeout=None):
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
run_torchrun_worker(
|
|
91
|
+
module="slide2vec.distributed.dense_worker", num_gpus=4, output_dir=tmp_path,
|
|
92
|
+
request_path=tmp_path / "request.json", failure_title="failed",
|
|
93
|
+
pin_gpus=pin_gpus, popen_factory=_Done,
|
|
94
|
+
)
|
|
95
|
+
(command,) = commands
|
|
96
|
+
tail = command[command.index("--nproc_per_node=4") + 1:]
|
|
97
|
+
if pin_gpus:
|
|
98
|
+
assert Path(tail[0]) == Path(pin_gpu.__file__).resolve()
|
|
99
|
+
assert tail[1] == "slide2vec.distributed.dense_worker"
|
|
100
|
+
else:
|
|
101
|
+
assert tail[:2] == ["-m", "slide2vec.distributed.dense_worker"]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.mark.parametrize("pinned, ordinal", [(True, 0), (False, 3)])
|
|
105
|
+
def test_enable_binds_a_pinned_rank_to_cuda_0(monkeypatch, pinned, ordinal):
|
|
106
|
+
# pipeline_worker and direct_embed_worker go through distributed.enable(); once pinned,
|
|
107
|
+
# cuda:<local_rank> no longer exists for any rank but 0.
|
|
108
|
+
import torch
|
|
109
|
+
|
|
110
|
+
import slide2vec.distributed as distributed
|
|
111
|
+
|
|
112
|
+
for name in ("_RANK", "_WORLD_SIZE", "_LOCAL_RANK", "_LOCAL_WORLD_SIZE"):
|
|
113
|
+
monkeypatch.setattr(distributed, name, -1)
|
|
114
|
+
for key in ("RANK", "WORLD_SIZE", "LOCAL_RANK", "LOCAL_WORLD_SIZE"):
|
|
115
|
+
monkeypatch.setenv(key, "4" if "SIZE" in key else "3")
|
|
116
|
+
if pinned:
|
|
117
|
+
monkeypatch.setenv(worker_entry.PINNED_ENV, "1")
|
|
118
|
+
else:
|
|
119
|
+
monkeypatch.delenv(worker_entry.PINNED_ENV, raising=False)
|
|
120
|
+
monkeypatch.setattr(distributed, "_restrict_print_to_main_process", lambda: None)
|
|
121
|
+
bound = []
|
|
122
|
+
monkeypatch.setattr(torch.cuda, "set_device", bound.append)
|
|
123
|
+
|
|
124
|
+
distributed.enable(overwrite=True)
|
|
125
|
+
|
|
126
|
+
assert bound == [ordinal]
|
|
127
|
+
assert distributed.get_device_ordinal() == ordinal
|
|
128
|
+
assert distributed.get_local_rank() == 3
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.mark.parametrize("worker", ["pipeline_worker", "direct_embed_worker"])
|
|
132
|
+
def test_enable_based_workers_build_the_model_on_the_device_ordinal(worker):
|
|
133
|
+
source = (Path(pin_gpu.__file__).parent / f"{worker}.py").read_text()
|
|
134
|
+
assert 'device=f"cuda:{distributed.get_device_ordinal()}"' in source
|
|
135
|
+
assert 'progress_label=f"cuda:{local_rank}"' in source
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_every_torchrun_stage_pins_its_workers():
|
|
139
|
+
runtime = Path(pin_gpu.__file__).resolve().parents[1] / "runtime"
|
|
140
|
+
launches = [
|
|
141
|
+
node
|
|
142
|
+
for path in runtime.glob("*.py")
|
|
143
|
+
for node in ast.walk(ast.parse(path.read_text()))
|
|
144
|
+
if isinstance(node, ast.Call) and getattr(node.func, "id", None) == "run_torchrun_worker"
|
|
145
|
+
]
|
|
146
|
+
assert len(launches) == 5
|
|
147
|
+
for call in launches:
|
|
148
|
+
(pin,) = [kw.value for kw in call.keywords if kw.arg == "pin_gpus"]
|
|
149
|
+
assert pin.value is True
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_workers_keep_the_parents_working_directory(tmp_path, monkeypatch):
|
|
153
|
+
# A relative output_dir must mean the same place in the parent and in every rank, so the
|
|
154
|
+
# checkout is made importable through PYTHONPATH rather than by moving the workers' cwd.
|
|
155
|
+
import os
|
|
156
|
+
|
|
157
|
+
import slide2vec
|
|
158
|
+
from slide2vec.runtime.distributed import run_torchrun_worker
|
|
159
|
+
|
|
160
|
+
monkeypatch.setenv("PYTHONPATH", "/somewhere/else")
|
|
161
|
+
launches = []
|
|
162
|
+
|
|
163
|
+
class _Done:
|
|
164
|
+
pid = None
|
|
165
|
+
stdout = stderr = None
|
|
166
|
+
|
|
167
|
+
def __init__(self, command, **kwargs):
|
|
168
|
+
launches.append((command, kwargs))
|
|
169
|
+
|
|
170
|
+
def poll(self):
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
def wait(self, timeout=None):
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
monkeypatch.chdir(tmp_path)
|
|
177
|
+
run_torchrun_worker(
|
|
178
|
+
module="slide2vec.distributed.dense_worker", num_gpus=2, output_dir=Path("out"),
|
|
179
|
+
request_path=Path("out") / "request.json", failure_title="failed", popen_factory=_Done,
|
|
180
|
+
)
|
|
181
|
+
((command, kwargs),) = launches
|
|
182
|
+
assert kwargs.get("cwd") is None
|
|
183
|
+
assert command[command.index("--output-dir") + 1] == "out"
|
|
184
|
+
package_root = str(Path(slide2vec.__file__).resolve().parents[1])
|
|
185
|
+
assert kwargs["env"]["PYTHONPATH"].split(os.pathsep) == [package_root, "/somewhere/else"]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_bootstrap_prefers_its_checkout_and_keeps_relative_paths(tmp_path):
|
|
189
|
+
# Minimal competing packages let a fresh interpreter exercise import precedence
|
|
190
|
+
# without loading CUDA or model dependencies.
|
|
191
|
+
package = tmp_path / "parent-checkout" / "slide2vec"
|
|
192
|
+
bootstrap = package / "distributed" / "pin_gpu.py"
|
|
193
|
+
bootstrap.parent.mkdir(parents=True)
|
|
194
|
+
bootstrap.write_text(Path(pin_gpu.__file__).read_text())
|
|
195
|
+
(package / "__init__.py").write_text("")
|
|
196
|
+
|
|
197
|
+
work_dir = tmp_path / "other-checkout"
|
|
198
|
+
shadow_package = work_dir / "slide2vec"
|
|
199
|
+
shadow_package.mkdir(parents=True)
|
|
200
|
+
(shadow_package / "__init__.py").write_text(
|
|
201
|
+
"raise AssertionError('worker imported the competing checkout')\n"
|
|
202
|
+
)
|
|
203
|
+
(work_dir / "input.txt").write_text("relative input")
|
|
204
|
+
(work_dir / "out").mkdir()
|
|
205
|
+
(work_dir / "out" / "request.json").write_text(
|
|
206
|
+
json.dumps({"input": "input.txt", "output": "out/report.json"})
|
|
207
|
+
)
|
|
208
|
+
# This module is only available in the working directory, which must remain
|
|
209
|
+
# importable after the parent's package root.
|
|
210
|
+
(work_dir / "probe_worker.py").write_text(
|
|
211
|
+
"import json, os, sys\n"
|
|
212
|
+
"from pathlib import Path\n"
|
|
213
|
+
"import slide2vec\n"
|
|
214
|
+
"request = json.loads(Path(sys.argv[1]).read_text())\n"
|
|
215
|
+
"Path(request['output']).write_text(json.dumps({\n"
|
|
216
|
+
" 'package': slide2vec.__file__,\n"
|
|
217
|
+
" 'cwd': os.getcwd(),\n"
|
|
218
|
+
" 'input': Path(request['input']).read_text(),\n"
|
|
219
|
+
" 'visible': os.environ['CUDA_VISIBLE_DEVICES'],\n"
|
|
220
|
+
"}))\n"
|
|
221
|
+
)
|
|
222
|
+
env = dict(os.environ)
|
|
223
|
+
env.update(LOCAL_RANK="1", CUDA_VISIBLE_DEVICES="2,3", PYTHONPATH=str(package.parent))
|
|
224
|
+
result = subprocess.run(
|
|
225
|
+
[sys.executable, str(bootstrap), "probe_worker", "out/request.json"],
|
|
226
|
+
cwd=work_dir, env=env, capture_output=True, text=True, timeout=15,
|
|
227
|
+
)
|
|
228
|
+
assert result.returncode == 0, result.stderr
|
|
229
|
+
assert json.loads((work_dir / "out" / "report.json").read_text()) == {
|
|
230
|
+
"package": str(package / "__init__.py"),
|
|
231
|
+
"cwd": str(work_dir),
|
|
232
|
+
"input": "relative input",
|
|
233
|
+
"visible": "3",
|
|
234
|
+
}
|