slide2vec 7.1.4__tar.gz → 7.1.5__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-7.1.4 → slide2vec-7.1.5}/PKG-INFO +1 -1
- {slide2vec-7.1.4 → slide2vec-7.1.5}/pyproject.toml +2 -2
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/__init__.py +1 -1
- slide2vec-7.1.5/slide2vec/encoders/models/mettle_1_5.py +129 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec.egg-info/PKG-INFO +1 -1
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec.egg-info/SOURCES.txt +1 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_encoder_plugins.py +1 -1
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_encoder_preprocessing.py +31 -7
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_mettle.py +98 -40
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_patch_size_metadata.py +1 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_regression_core.py +1 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_regression_models.py +1 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/LICENSE +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/README.md +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/setup.cfg +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/__main__.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/_cufile.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/api.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/artifacts.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/cli.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/configs/__init__.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/configs/default.yaml +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/configs/resources.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/cufile.json +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/data/__init__.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/data/dataset.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/data/tile_reader.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/data/tile_store.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/distributed/__init__.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/distributed/dense_image_worker.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/distributed/dense_worker.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/distributed/direct_embed_worker.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/distributed/image_worker.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/distributed/pin_gpu.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/distributed/pipeline_worker.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/distributed/worker_entry.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/__init__.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/base.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/code.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/__init__.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/conch.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/dinov2.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/dinov3.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/genbio.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/gigapath.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/gpfm.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/hibou.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/hoptimus.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/isight.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/lunit.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/mettle.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/midnight.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/moozy/__init__.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/moozy/blocks.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/moozy/case.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/moozy/loading.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/moozy/slide.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/moozy/types.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/mstar.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/musk.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/phikon.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/prism.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/prism2.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/prost40m.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/rudolfv2.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/titan.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/uni.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/virchow.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/models/waiv.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/registry.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/encoders/validation.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/inference.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/progress.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/__init__.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/artifacts_collect.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/batching.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/cpu_budget.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/dense_encode.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/dense_encoder_input.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/dense_image_reading.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/dense_image_recipe.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/dense_image_shard.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/dense_image_stage.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/dense_regions.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/dense_shard.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/dense_sliding.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/dense_stage.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/distributed.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/distributed_stage.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/effective_encoder_input.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/embedding.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/embedding_persist.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/embedding_pipeline.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/encoder_input_contract.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/feature_identity.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/hierarchical.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/image_shard.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/image_specs.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/image_stage.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/manifest.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/model_settings.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/patient_pipeline.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/persist_callbacks.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/persistence.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/pooled_encoder_input.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/preprocessing.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/process_list.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/progress_bridge.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/registry.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/serialization.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/sharding.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/slide_encode.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/tiling.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/tiling_pipeline.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/types.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/runtime/worker_io.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/utils/__init__.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/utils/config.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/utils/coordinates.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/utils/log_utils.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/utils/tiling_io.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec/utils/utils.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec.egg-info/dependency_links.txt +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec.egg-info/entry_points.txt +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec.egg-info/not-zip-safe +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec.egg-info/requires.txt +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/slide2vec.egg-info/top_level.txt +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_attention_extraction.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_benchmark_tooling.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_cli_noninteractive.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_cufile_compat_mode.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_encode_kit.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_encoder_input.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_extraction.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_identity_api.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_image_reading.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_image_resume.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_image_shard.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_image_stage.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_regions.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_shard.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_sliding.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_source_spacing.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_stage.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dense_worker.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dinov2_natimage.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_dinov3_natimage.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_drop_in_encoders.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_encoder_capabilities.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_encoder_code.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_encoder_input_contract.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_encoder_provider_failures.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_encoder_registry.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_execution_options.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_feature_identity.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_gigapath.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_gpfm_genbio_heavy.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_hierarchical_encoding.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_hs2p5_integration.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_hs2p_package_cutover.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_image_shard.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_image_stage.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_inference_benchmark.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_isight.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_load_model_hf_auth.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_mascaret.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_on_slide_persisted.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_output_consistency.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_patient_annotation_sampling.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_patient_manifest.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_patient_tiling_only.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_phaet.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_pooled_encoder_input.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_pooled_geometry.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_pooled_identity_api.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_prism.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_prism2.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_process_list_performance.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_progress.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_regression_inference.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_rudolfv2.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_runtime_batching.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_sharding.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_slide_coordinate_preparation.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_slide_latents.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_soma_migration.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_tile_store.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_tiling_pipeline.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_titan.py +0 -0
- {slide2vec-7.1.4 → slide2vec-7.1.5}/tests/test_worker_gpu_pinning.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "slide2vec"
|
|
7
|
-
version = "7.1.
|
|
7
|
+
version = "7.1.5"
|
|
8
8
|
description = "Embedding of whole slide images with Foundation Models"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -240,7 +240,7 @@ no_implicit_reexport = true
|
|
|
240
240
|
max-line-length = 160
|
|
241
241
|
|
|
242
242
|
[tool.bumpver]
|
|
243
|
-
current_version = "7.1.
|
|
243
|
+
current_version = "7.1.5"
|
|
244
244
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
245
245
|
commit = false
|
|
246
246
|
tag = false
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Mettle-1.5 tile encoder.
|
|
2
|
+
|
|
3
|
+
Mettle-1.5 (slideflow labs; ``slideflow-labs/mettle-1.5``) is a later scanner and
|
|
4
|
+
stain robustness fine-tune of H-optimus-0. It shares ``mettle``'s backbone,
|
|
5
|
+
``MettleRefineHead``, H-optimus mean/std and feature views, so this module reuses
|
|
6
|
+
:mod:`.mettle`. Weights are openly downloadable (not gated). The model card states
|
|
7
|
+
no license; slide2vec only wraps user-downloaded weights and does not redistribute
|
|
8
|
+
them.
|
|
9
|
+
|
|
10
|
+
The given recipe resizes directly to 224x224, as upstream's
|
|
11
|
+
``preprocessor_config.json`` does. ``mettle`` resizes the shortest side and
|
|
12
|
+
center-crops instead. The two agree on square tiles.
|
|
13
|
+
|
|
14
|
+
slide2vec strictly loads ``model.safetensors`` at a pinned revision, as for
|
|
15
|
+
``mettle``; no remote code runs.
|
|
16
|
+
|
|
17
|
+
Output variants (upstream ``feature_view``):
|
|
18
|
+
|
|
19
|
+
* ``cls`` (1536, default): the headed CLS token. The card and upstream
|
|
20
|
+
``config.json`` default to it; the card makes no benchmark claims for this
|
|
21
|
+
checkpoint.
|
|
22
|
+
* ``cls_patch_mean`` (3072; upstream ``cls_mean``): the headed CLS token
|
|
23
|
+
concatenated with the *unheaded* mean of the spatial patch tokens (CLS and the
|
|
24
|
+
4 register tokens excluded).
|
|
25
|
+
|
|
26
|
+
``mettle`` defaults to ``cls_patch_mean``, so a config moved from ``mettle`` to
|
|
27
|
+
``mettle-1.5`` changes the feature dimension from 3072 to 1536.
|
|
28
|
+
|
|
29
|
+
Precision, dense extraction and spacing are as for ``mettle``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from typing import Callable
|
|
33
|
+
|
|
34
|
+
import torch
|
|
35
|
+
from huggingface_hub import hf_hub_download
|
|
36
|
+
from safetensors.torch import load_file
|
|
37
|
+
from torchvision.transforms import v2
|
|
38
|
+
|
|
39
|
+
from slide2vec.encoders.base import (
|
|
40
|
+
resolve_recommended_dynamic_img_size,
|
|
41
|
+
resolve_requested_output_variant,
|
|
42
|
+
)
|
|
43
|
+
from slide2vec.encoders.models.hoptimus import _HOPTIMUS_MEAN, _HOPTIMUS_STD
|
|
44
|
+
from slide2vec.encoders.models.mettle import (
|
|
45
|
+
_EMBED_DIM,
|
|
46
|
+
_HEAD_HIDDEN_SIZE,
|
|
47
|
+
_HEAD_NUM_ATOMS,
|
|
48
|
+
_HEAD_RANK,
|
|
49
|
+
_HF_CHECKPOINT,
|
|
50
|
+
_OUTPUT_DIMS,
|
|
51
|
+
Mettle,
|
|
52
|
+
MettleRefineHead,
|
|
53
|
+
_strip_prefix,
|
|
54
|
+
)
|
|
55
|
+
from slide2vec.encoders.registry import register_encoder
|
|
56
|
+
|
|
57
|
+
_HF_REPO_ID = "slideflow-labs/mettle-1.5"
|
|
58
|
+
_HF_REVISION = "0ab55fcbf1b8d4e3c73885fc56f822fb568ff570"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@register_encoder(
|
|
62
|
+
"mettle-1.5",
|
|
63
|
+
output_variants={
|
|
64
|
+
"cls": {"encode_dim": _OUTPUT_DIMS["cls"]},
|
|
65
|
+
"cls_patch_mean": {"encode_dim": _OUTPUT_DIMS["cls_patch_mean"]},
|
|
66
|
+
},
|
|
67
|
+
default_output_variant="cls",
|
|
68
|
+
input_size=224,
|
|
69
|
+
supports_variable_input_size=True,
|
|
70
|
+
variable_input_model_kwargs={"dynamic_img_size": True},
|
|
71
|
+
patch_size=14,
|
|
72
|
+
supported_spacing_um=0.5, # not stated on the card; inherited from H-optimus-0
|
|
73
|
+
precision="fp16",
|
|
74
|
+
source=_HF_REPO_ID,
|
|
75
|
+
)
|
|
76
|
+
class Mettle15(Mettle):
|
|
77
|
+
# Mettle.__init__ hardcodes the Mettle-1 repo, so this one calls
|
|
78
|
+
# TimmTileEncoder.__init__ directly. mettle.py stays unchanged: its file hash is
|
|
79
|
+
# part of the feature identity of every cached mettle feature.
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
output_variant: str | None = None,
|
|
84
|
+
dynamic_img_size: bool | None = None,
|
|
85
|
+
allow_non_recommended_settings: bool = False,
|
|
86
|
+
):
|
|
87
|
+
self._output_variant = resolve_requested_output_variant(
|
|
88
|
+
output_variant,
|
|
89
|
+
default="cls",
|
|
90
|
+
allowed=tuple(_OUTPUT_DIMS),
|
|
91
|
+
)
|
|
92
|
+
super(Mettle, self).__init__(
|
|
93
|
+
"vit_giant_patch14_reg4_dinov2",
|
|
94
|
+
output_variant="default",
|
|
95
|
+
pretrained=False,
|
|
96
|
+
init_values=1e-5,
|
|
97
|
+
img_size=224,
|
|
98
|
+
dynamic_img_size=resolve_recommended_dynamic_img_size(
|
|
99
|
+
requested=dynamic_img_size,
|
|
100
|
+
recommended=False,
|
|
101
|
+
allow_non_recommended=allow_non_recommended_settings,
|
|
102
|
+
encoder_name="mettle-1.5",
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
self._head = MettleRefineHead(
|
|
106
|
+
dim=_EMBED_DIM,
|
|
107
|
+
num_atoms=_HEAD_NUM_ATOMS,
|
|
108
|
+
rank=_HEAD_RANK,
|
|
109
|
+
hidden_size=_HEAD_HIDDEN_SIZE,
|
|
110
|
+
)
|
|
111
|
+
state_dict = load_file(
|
|
112
|
+
hf_hub_download(repo_id=_HF_REPO_ID, filename=_HF_CHECKPOINT, revision=_HF_REVISION)
|
|
113
|
+
)
|
|
114
|
+
unexpected = [key for key in state_dict if not key.startswith(("backbone.", "head."))]
|
|
115
|
+
if unexpected:
|
|
116
|
+
raise ValueError(f"Unexpected keys in Mettle-1.5 checkpoint: {unexpected}")
|
|
117
|
+
self._model.load_state_dict(_strip_prefix(state_dict, "backbone."), strict=True)
|
|
118
|
+
self._head.load_state_dict(_strip_prefix(state_dict, "head."), strict=True)
|
|
119
|
+
self._model.eval()
|
|
120
|
+
self._head.eval()
|
|
121
|
+
|
|
122
|
+
def get_transform(self) -> Callable:
|
|
123
|
+
# Upstream's ViTImageProcessor resizes directly to 224x224, with no crop.
|
|
124
|
+
return v2.Compose([
|
|
125
|
+
v2.ToImage(),
|
|
126
|
+
v2.Resize((224, 224), interpolation=v2.InterpolationMode.BICUBIC, antialias=True),
|
|
127
|
+
v2.ToDtype(torch.float32, scale=True),
|
|
128
|
+
v2.Normalize(mean=_HOPTIMUS_MEAN, std=_HOPTIMUS_STD),
|
|
129
|
+
])
|
|
@@ -49,6 +49,7 @@ slide2vec/encoders/models/hoptimus.py
|
|
|
49
49
|
slide2vec/encoders/models/isight.py
|
|
50
50
|
slide2vec/encoders/models/lunit.py
|
|
51
51
|
slide2vec/encoders/models/mettle.py
|
|
52
|
+
slide2vec/encoders/models/mettle_1_5.py
|
|
52
53
|
slide2vec/encoders/models/midnight.py
|
|
53
54
|
slide2vec/encoders/models/mstar.py
|
|
54
55
|
slide2vec/encoders/models/musk.py
|
|
@@ -179,7 +179,7 @@ with ThreadPoolExecutor(max_workers=8) as executor:
|
|
|
179
179
|
expected_models = listings[0]
|
|
180
180
|
assert listings == [expected_models] * 8
|
|
181
181
|
assert expected_models == sorted(expected_models)
|
|
182
|
-
assert {"private-alpha", "private-beta", "virchow2", "mettle"} <= set(expected_models)
|
|
182
|
+
assert {"private-alpha", "private-beta", "virchow2", "mettle", "mettle-1.5"} <= set(expected_models)
|
|
183
183
|
assert discovered.read_text() == "discovered"
|
|
184
184
|
assert not constructed.exists()
|
|
185
185
|
|
|
@@ -353,32 +353,36 @@ def test_dinov3_public_pooled_recipe_reaches_encoding(
|
|
|
353
353
|
torch.testing.assert_close(observed[1], observed[0]) # batched == itemwise
|
|
354
354
|
|
|
355
355
|
|
|
356
|
-
# bioptimus/H-optimus-0 mean/std, which slideflow-labs/Mettle
|
|
356
|
+
# bioptimus/H-optimus-0 mean/std, which the preprocessor_config.json of slideflow-labs/Mettle
|
|
357
|
+
# and slideflow-labs/mettle-1.5 repeats.
|
|
357
358
|
HOPTIMUS_RED = [1.381786, -2.514934, -3.96366]
|
|
358
359
|
HOPTIMUS_BLACK = [-3.3378, -2.514934, -3.96366]
|
|
360
|
+
METTLE_PRESETS = ["mettle", "mettle-1.5"]
|
|
359
361
|
|
|
360
362
|
|
|
363
|
+
@pytest.mark.parametrize("name", METTLE_PRESETS)
|
|
361
364
|
@pytest.mark.parametrize("regime", ["pooled", "dense"])
|
|
362
|
-
def test_mettle_declared_normalization_uses_hoptimus_photometrics_not_imagenet(regime):
|
|
365
|
+
def test_mettle_declared_normalization_uses_hoptimus_photometrics_not_imagenet(regime, name):
|
|
363
366
|
"""Mettle's generic timm arch carries an ImageNet pretrained_cfg; its card uses H-optimus mean/std."""
|
|
364
367
|
from slide2vec.runtime.encoder_input_contract import EncoderInputContract
|
|
365
368
|
|
|
366
369
|
if regime == "pooled":
|
|
367
370
|
contract = EncoderInputContract.declared_pooled(
|
|
368
|
-
|
|
371
|
+
name, requested_tile_size_px=224, allow_non_recommended_settings=False,
|
|
369
372
|
)
|
|
370
373
|
else:
|
|
371
|
-
contract = EncoderInputContract.declared_dense(
|
|
372
|
-
output = contract.get_transform(recipe_encoder(
|
|
374
|
+
contract = EncoderInputContract.declared_dense(name, target_size_px=224, window_size=None)
|
|
375
|
+
output = contract.get_transform(recipe_encoder(name))(bordered_image(224))
|
|
373
376
|
|
|
374
377
|
assert_border_intact(output, size=224, red=HOPTIMUS_RED, black=HOPTIMUS_BLACK)
|
|
375
378
|
|
|
376
379
|
|
|
377
|
-
|
|
380
|
+
@pytest.mark.parametrize("name", METTLE_PRESETS)
|
|
381
|
+
def test_mettle_given_recipe_is_a_bicubic_224_resize_without_enlargement(name):
|
|
378
382
|
from torchvision.transforms import v2
|
|
379
383
|
from slide2vec.runtime.encoder_input_contract import EncoderInputContract
|
|
380
384
|
|
|
381
|
-
transform = EncoderInputContract.given().get_transform(recipe_encoder(
|
|
385
|
+
transform = EncoderInputContract.given().get_transform(recipe_encoder(name))
|
|
382
386
|
|
|
383
387
|
assert_border_intact(transform(bordered_image(224)), size=224, red=HOPTIMUS_RED, black=HOPTIMUS_BLACK)
|
|
384
388
|
downsized = transform(Image.new("RGB", (448, 448), (255, 0, 0)))
|
|
@@ -386,3 +390,23 @@ def test_mettle_given_recipe_is_a_bicubic_224_resize_without_enlargement():
|
|
|
386
390
|
torch.testing.assert_close(downsized[:, 112, 112], torch.tensor(HOPTIMUS_RED))
|
|
387
391
|
resize = next(step for step in transform.transforms if isinstance(step, v2.Resize))
|
|
388
392
|
assert resize.interpolation == v2.InterpolationMode.BICUBIC
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
HOPTIMUS_BLUE = [-3.3378, -2.514934, 1.669603]
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_mettle_1_5_given_recipe_resizes_rectangular_images_directly_without_a_crop():
|
|
399
|
+
"""slideflow-labs/mettle-1.5's ViTImageProcessor resizes to 224x224 with do_center_crop false."""
|
|
400
|
+
from slide2vec.runtime.encoder_input_contract import EncoderInputContract
|
|
401
|
+
|
|
402
|
+
transform = EncoderInputContract.given().get_transform(recipe_encoder("mettle-1.5"))
|
|
403
|
+
image = Image.new("RGB", (448, 224), (0, 0, 0))
|
|
404
|
+
image.paste((255, 0, 0), (0, 0, 112, 224))
|
|
405
|
+
image.paste((0, 0, 255), (336, 0, 448, 224))
|
|
406
|
+
output = transform(image)
|
|
407
|
+
|
|
408
|
+
assert tuple(output.shape) == (3, 224, 224)
|
|
409
|
+
# A shortest-side resize plus a center crop would drop both edge quarters.
|
|
410
|
+
torch.testing.assert_close(output[:, 112, 28], torch.tensor(HOPTIMUS_RED))
|
|
411
|
+
torch.testing.assert_close(output[:, 112, 112], torch.tensor(HOPTIMUS_BLACK))
|
|
412
|
+
torch.testing.assert_close(output[:, 112, 196], torch.tensor(HOPTIMUS_BLUE))
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
"""Tests for the Mettle tile
|
|
1
|
+
"""Tests for the Mettle tile encoders (``mettle`` and ``mettle-1.5``).
|
|
2
2
|
|
|
3
3
|
Mettle (``slideflow-labs/Mettle``) is H-optimus-0's ViT-g/14 reg4 backbone,
|
|
4
|
-
fine-tuned, plus a small ``MettleRefineHead`` applied to the CLS token.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
fine-tuned, plus a small ``MettleRefineHead`` applied to the CLS token.
|
|
5
|
+
Mettle-1.5 (``slideflow-labs/mettle-1.5``) differs in its weights, its default
|
|
6
|
+
output (``cls``) and its given recipe (a direct 224x224 resize). Both ship as
|
|
7
|
+
``trust_remote_code`` HF repos; slide2vec rebuilds them from timm + a vendored
|
|
8
|
+
head, so these tests pin, for both presets, the contracts that break silently
|
|
9
|
+
(no exception, just different features):
|
|
8
10
|
|
|
9
11
|
* ``cls`` is the *headed* CLS token; ``cls_patch_mean`` appends the *unheaded*
|
|
10
12
|
mean of the spatial patch tokens, with CLS and the 4 register tokens excluded;
|
|
@@ -25,11 +27,37 @@ import torch
|
|
|
25
27
|
|
|
26
28
|
_IMAGE = 28 # -> 2x2 patch grid, 1 CLS + 4 registers + 4 patches
|
|
27
29
|
_DIM = 384
|
|
30
|
+
_PRESETS = ["mettle", "mettle-1.5"]
|
|
28
31
|
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
+
@pytest.mark.parametrize("name, source, default_output_variant", [
|
|
34
|
+
("mettle", "slideflow-labs/Mettle", "cls_patch_mean"),
|
|
35
|
+
("mettle-1.5", "slideflow-labs/mettle-1.5", "cls"),
|
|
36
|
+
])
|
|
37
|
+
def test_registry_declares_the_mettle_presets(name, source, default_output_variant):
|
|
38
|
+
from slide2vec.encoders import encoder_registry
|
|
39
|
+
|
|
40
|
+
info = encoder_registry.info(name)
|
|
41
|
+
|
|
42
|
+
assert info["default_output_variant"] == default_output_variant
|
|
43
|
+
assert info["output_variants"] == {
|
|
44
|
+
"cls": {"encode_dim": 1536},
|
|
45
|
+
"cls_patch_mean": {"encode_dim": 3072},
|
|
46
|
+
}
|
|
47
|
+
assert info["source"] == source
|
|
48
|
+
assert info["level"] == "tile"
|
|
49
|
+
assert info["input_size"] == 224
|
|
50
|
+
assert info["patch_size"] == 14
|
|
51
|
+
assert info["supports_variable_input_size"] is True
|
|
52
|
+
assert info["variable_input_model_kwargs"] == {"dynamic_img_size": True}
|
|
53
|
+
assert info["supported_spacing_um"] == 0.5
|
|
54
|
+
assert info["precision"] == "fp16"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _tiny_encoder(name: str, *, output_variant: str = "cls_patch_mean"):
|
|
58
|
+
"""A tiny random reg4 ViT + refine head wired into preset *name* (no download)."""
|
|
59
|
+
from slide2vec.encoders import encoder_registry
|
|
60
|
+
from slide2vec.encoders.models.mettle import MettleRefineHead
|
|
33
61
|
|
|
34
62
|
torch.manual_seed(0)
|
|
35
63
|
backbone = timm.create_model(
|
|
@@ -44,7 +72,8 @@ def _tiny_encoder(*, output_variant: str = "cls_patch_mean"):
|
|
|
44
72
|
head.P.normal_()
|
|
45
73
|
head.Q.normal_()
|
|
46
74
|
|
|
47
|
-
|
|
75
|
+
encoder_cls = encoder_registry.require(name)
|
|
76
|
+
encoder = encoder_cls.__new__(encoder_cls)
|
|
48
77
|
encoder._model = backbone
|
|
49
78
|
encoder._head = head
|
|
50
79
|
encoder._device = torch.device("cpu")
|
|
@@ -52,8 +81,9 @@ def _tiny_encoder(*, output_variant: str = "cls_patch_mean"):
|
|
|
52
81
|
return encoder
|
|
53
82
|
|
|
54
83
|
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
@pytest.mark.parametrize("name", _PRESETS)
|
|
85
|
+
def test_cls_patch_mean_is_headed_cls_then_unheaded_spatial_patch_mean(name):
|
|
86
|
+
encoder = _tiny_encoder(name)
|
|
57
87
|
batch = torch.randn(2, 3, _IMAGE, _IMAGE)
|
|
58
88
|
with torch.inference_mode():
|
|
59
89
|
tokens = encoder._model.forward_features(batch)
|
|
@@ -67,24 +97,26 @@ def test_cls_patch_mean_is_headed_cls_then_unheaded_spatial_patch_mean():
|
|
|
67
97
|
assert not torch.allclose(pooled[:, :_DIM], tokens[:, 0], atol=1e-3)
|
|
68
98
|
|
|
69
99
|
|
|
70
|
-
|
|
100
|
+
@pytest.mark.parametrize("name", _PRESETS)
|
|
101
|
+
def test_cls_variant_is_the_headed_cls_prefix_of_cls_patch_mean(name):
|
|
71
102
|
batch = torch.randn(2, 3, _IMAGE, _IMAGE)
|
|
72
103
|
with torch.inference_mode():
|
|
73
|
-
cls = _tiny_encoder(output_variant="cls").encode_tiles(batch)
|
|
74
|
-
cls_patch_mean = _tiny_encoder(output_variant="cls_patch_mean").encode_tiles(batch)
|
|
104
|
+
cls = _tiny_encoder(name, output_variant="cls").encode_tiles(batch)
|
|
105
|
+
cls_patch_mean = _tiny_encoder(name, output_variant="cls_patch_mean").encode_tiles(batch)
|
|
75
106
|
|
|
76
107
|
assert cls.shape == (2, _DIM)
|
|
77
108
|
torch.testing.assert_close(cls, cls_patch_mean[:, :_DIM], rtol=0, atol=0)
|
|
78
109
|
|
|
79
110
|
|
|
111
|
+
@pytest.mark.parametrize("name", _PRESETS)
|
|
80
112
|
@pytest.mark.parametrize("output_variant", ["cls", "cls_patch_mean"])
|
|
81
|
-
def test_head_and_patch_mean_run_in_fp32_outside_autocast(output_variant):
|
|
113
|
+
def test_head_and_patch_mean_run_in_fp32_outside_autocast(output_variant, name):
|
|
82
114
|
"""Under low-precision autocast only the backbone is cast; features come back fp32.
|
|
83
115
|
|
|
84
116
|
The expected value applies the head in plain fp32 to the autocast backbone's
|
|
85
117
|
CLS token; a head run under autocast would drift by the bf16 rounding error.
|
|
86
118
|
"""
|
|
87
|
-
encoder = _tiny_encoder(output_variant=output_variant)
|
|
119
|
+
encoder = _tiny_encoder(name, output_variant=output_variant)
|
|
88
120
|
batch = torch.randn(2, 3, _IMAGE, _IMAGE)
|
|
89
121
|
with torch.inference_mode():
|
|
90
122
|
with torch.autocast(device_type="cpu", dtype=torch.bfloat16):
|
|
@@ -100,47 +132,70 @@ def test_head_and_patch_mean_run_in_fp32_outside_autocast(output_variant):
|
|
|
100
132
|
|
|
101
133
|
# --- Heavy: real weights -----------------------------------------------------
|
|
102
134
|
|
|
135
|
+
# Hub repo and revision per preset (``None``: the repo's default branch).
|
|
136
|
+
_HUB = {
|
|
137
|
+
"mettle": ("slideflow-labs/Mettle", None),
|
|
138
|
+
"mettle-1.5": ("slideflow-labs/mettle-1.5", "0ab55fcbf1b8d4e3c73885fc56f822fb568ff570"),
|
|
139
|
+
}
|
|
140
|
+
_DEFAULT_DIMS = {"mettle": 3072, "mettle-1.5": 1536}
|
|
141
|
+
|
|
103
142
|
|
|
104
|
-
def _require_hub_files(*filenames: str) -> None:
|
|
143
|
+
def _require_hub_files(repo_id: str, revision: str | None, *filenames: str) -> None:
|
|
105
144
|
"""Skip when the weights / network are unavailable; fetches into the HF cache."""
|
|
106
145
|
from huggingface_hub import hf_hub_download
|
|
107
146
|
|
|
108
147
|
for filename in filenames:
|
|
109
148
|
try:
|
|
110
|
-
hf_hub_download(repo_id=
|
|
149
|
+
hf_hub_download(repo_id=repo_id, filename=filename, revision=revision)
|
|
111
150
|
except Exception as exc: # network / weights unavailable
|
|
112
|
-
pytest.skip(f"
|
|
151
|
+
pytest.skip(f"{repo_id} {filename} unavailable: {type(exc).__name__}: {exc}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@pytest.fixture(scope="module", params=_PRESETS)
|
|
155
|
+
def preset(request) -> str:
|
|
156
|
+
"""The preset under test.
|
|
157
|
+
|
|
158
|
+
The real-weight fixtures below depend on it, so pytest tears down one preset's
|
|
159
|
+
models before it builds the next: two presets' ViT-g weights are never loaded
|
|
160
|
+
at the same time.
|
|
161
|
+
"""
|
|
162
|
+
return request.param
|
|
113
163
|
|
|
114
164
|
|
|
115
165
|
@pytest.fixture(scope="module")
|
|
116
|
-
def mettle_encoders():
|
|
166
|
+
def mettle_encoders(preset):
|
|
117
167
|
"""Load the real encoder once per output variant (on CPU).
|
|
118
168
|
|
|
119
|
-
Only the download may skip: a
|
|
169
|
+
``load()`` builds the registry default. Only the download may skip: a
|
|
170
|
+
strict-load mismatch in the constructor must fail.
|
|
120
171
|
"""
|
|
121
172
|
from slide2vec.encoders import encoder_registry
|
|
122
173
|
|
|
123
|
-
_require_hub_files("model.safetensors")
|
|
174
|
+
_require_hub_files(*_HUB[preset], "model.safetensors")
|
|
175
|
+
default = encoder_registry.info(preset)["default_output_variant"]
|
|
124
176
|
loaded = {}
|
|
125
177
|
|
|
126
|
-
def load(output_variant: str =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
178
|
+
def load(output_variant: str | None = None):
|
|
179
|
+
key = output_variant or default
|
|
180
|
+
if key not in loaded:
|
|
181
|
+
encoder = encoder_registry.require(preset)(output_variant=output_variant)
|
|
182
|
+
loaded[key] = encoder.to("cpu")
|
|
183
|
+
return loaded[key]
|
|
131
184
|
|
|
132
185
|
return load
|
|
133
186
|
|
|
134
187
|
|
|
135
188
|
@pytest.fixture(scope="module")
|
|
136
|
-
def upstream_mettle():
|
|
189
|
+
def upstream_mettle(preset):
|
|
137
190
|
"""Upstream ``AutoModel(trust_remote_code=True)`` reference, fp32 on CPU."""
|
|
138
191
|
transformers = pytest.importorskip("transformers")
|
|
192
|
+
repo_id, revision = _HUB[preset]
|
|
139
193
|
_require_hub_files(
|
|
194
|
+
repo_id, revision,
|
|
140
195
|
"config.json", "configuration_mettle.py", "modeling_mettle.py", "model.safetensors",
|
|
141
196
|
)
|
|
142
197
|
model = transformers.AutoModel.from_pretrained(
|
|
143
|
-
|
|
198
|
+
repo_id, revision=revision, trust_remote_code=True,
|
|
144
199
|
)
|
|
145
200
|
return model.to("cpu").eval()
|
|
146
201
|
|
|
@@ -151,19 +206,22 @@ def _tile_batch() -> torch.Tensor:
|
|
|
151
206
|
|
|
152
207
|
|
|
153
208
|
@pytest.mark.heavy
|
|
154
|
-
def test_mettle_loads_strict_and_emits_registered_shapes(mettle_encoders):
|
|
155
|
-
|
|
209
|
+
def test_mettle_loads_strict_and_emits_registered_shapes(preset, mettle_encoders):
|
|
210
|
+
default_encoder = mettle_encoders()
|
|
156
211
|
cls_encoder = mettle_encoders("cls")
|
|
212
|
+
cls_patch_mean_encoder = mettle_encoders("cls_patch_mean")
|
|
157
213
|
batch = _tile_batch()
|
|
158
214
|
with torch.inference_mode():
|
|
159
|
-
|
|
215
|
+
default = default_encoder.encode_tiles(batch)
|
|
160
216
|
cls = cls_encoder.encode_tiles(batch)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
assert
|
|
165
|
-
assert
|
|
166
|
-
assert cls.shape == (1, 1536)
|
|
217
|
+
cls_patch_mean = cls_patch_mean_encoder.encode_tiles(batch)
|
|
218
|
+
dense = default_encoder.encode_tiles_dense(batch)
|
|
219
|
+
|
|
220
|
+
assert default_encoder.encode_dim == _DEFAULT_DIMS[preset]
|
|
221
|
+
assert default.shape == (1, _DEFAULT_DIMS[preset])
|
|
222
|
+
assert cls_encoder.encode_dim == 1536 and cls.shape == (1, 1536)
|
|
223
|
+
assert cls_patch_mean_encoder.encode_dim == 3072 and cls_patch_mean.shape == (1, 3072)
|
|
224
|
+
assert default_encoder.patch_size == (14, 14)
|
|
167
225
|
# patch 14: 224 / 14 = 16 -> a 16x16 grid of unheaded 1536-d patch tokens.
|
|
168
226
|
assert dense.shape == (1, 1536, 16, 16)
|
|
169
227
|
|
|
@@ -184,13 +242,13 @@ def test_mettle_fp32_matches_upstream_remote_code(
|
|
|
184
242
|
|
|
185
243
|
|
|
186
244
|
@pytest.mark.heavy
|
|
187
|
-
def test_mettle_fp16_default_preserves_fp32_direction_on_cuda(mettle_encoders):
|
|
245
|
+
def test_mettle_fp16_default_preserves_fp32_direction_on_cuda(preset, mettle_encoders):
|
|
188
246
|
if not torch.cuda.is_available():
|
|
189
247
|
pytest.skip("CUDA unavailable")
|
|
190
248
|
from slide2vec.encoders import encoder_registry
|
|
191
249
|
from slide2vec.runtime.batching import autocast_dtype
|
|
192
250
|
|
|
193
|
-
precision = encoder_registry.info(
|
|
251
|
+
precision = encoder_registry.info(preset)["precision"]
|
|
194
252
|
assert precision == "fp16"
|
|
195
253
|
mettle_encoder = mettle_encoders()
|
|
196
254
|
batch = _tile_batch()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|