Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
53adbfd
feat: implement Virchow2 tile embedding extraction pipeline
vojtech-cifka Apr 22, 2026
53526ea
fix(submit): align embeddings job script with project conventions
vojtech-cifka Apr 22, 2026
db5c017
fix: move tiling_run_id into experiment config
vojtech-cifka Apr 22, 2026
b5ff141
fix: move tiling_run_id into dataset.mlflow_artifacts
vojtech-cifka Apr 22, 2026
e50f6fb
fix(submit): update clone URL to GitHub
vojtech-cifka Apr 23, 2026
7698c76
refactor: use configs that support different models
vojtech-cifka Apr 27, 2026
f08ef00
refactor: use new approach for generating embeddings
vojtech-cifka Apr 27, 2026
fa16999
Merge branch 'master' into feature/embeddings
vojtech-cifka Apr 29, 2026
ba96068
refactor: make some update and update dependencies
vojtech-cifka Apr 29, 2026
1689d3e
fix: tweak submission script and concurrency
vojtech-cifka Apr 29, 2026
1958069
fix: resolve path names
vojtech-cifka Apr 29, 2026
ea79dd4
fix: tiling splits filenames
vojtech-cifka Apr 29, 2026
7e32bfb
fix: point rationai sdk to the github repo
vojtech-cifka Apr 29, 2026
5392751
refactor: optimize data loading
vojtech-cifka Apr 30, 2026
d5e2c02
feat: limit RAM consumption
vojtech-cifka Apr 30, 2026
b2cb642
feat: dynamically split blocks
vojtech-cifka Apr 30, 2026
4adbb56
feat: raise the amount of block
vojtech-cifka Apr 30, 2026
85a914c
feat: add prints
vojtech-cifka Apr 30, 2026
06c818a
feat: change the cpu balance
vojtech-cifka Apr 30, 2026
014d11f
fix: change concurrency
vojtech-cifka Apr 30, 2026
f3e4cf4
fix: lower concurrency
vojtech-cifka Apr 30, 2026
606b8e4
feat: add timout, raise concurrency
vojtech-cifka Apr 30, 2026
d36dd98
fix: format
vojtech-cifka May 4, 2026
531f5f8
fix: use more memory
vojtech-cifka May 4, 2026
0cdcebe
Merge branch 'master' into feature/embeddings
vojtech-cifka May 5, 2026
7bd8725
fix: retry embed_image on failure and always free tile data
vojtech-cifka May 5, 2026
b23a0e5
feat: filter zero-coverage tiles before embedding
vojtech-cifka May 5, 2026
60a9971
fix: use | operator for PyArrow expression OR, not pc.or_
vojtech-cifka May 5, 2026
76dafae
chore: edge cases
vojtech-cifka May 5, 2026
59a6516
refactor: use filter_tiles output in embeddings instead of inline fil…
vojtech-cifka May 5, 2026
d42561b
feat: add temporary filter run id for testing
vojtech-cifka May 5, 2026
1434be8
Merge remote-tracking branch 'origin/master' into feature/embeddings
vojtech-cifka May 6, 2026
e109dc5
feat: generate tile masks from filtered tiles for both train and test…
vojtech-cifka May 6, 2026
1d99fcb
fix: change block size to the power two
vojtech-cifka May 6, 2026
ef74b6c
chore: remove first-row debug log from EmbedTiles
vojtech-cifka May 6, 2026
82163ac
refactor: use tenacity for embed retries, narrow to httpx errors
vojtech-cifka May 6, 2026
434aa43
chore: drop per-actor throughput log from EmbedTiles
vojtech-cifka May 6, 2026
287408f
chore: drop oversized memory reservation on read_parquet
vojtech-cifka May 6, 2026
8130f78
fix: build tenacity retryer in __init__ to avoid pickle failure
vojtech-cifka May 6, 2026
aa37cf3
Revert "chore: drop oversized memory reservation on read_parquet"
vojtech-cifka May 6, 2026
27da14e
Revert "refactor: use tenacity for embed retries, narrow to httpx err…
vojtech-cifka May 6, 2026
ec7a63a
Revert preprocessing/embeddings.py to commit 1d99fcb
vojtech-cifka May 6, 2026
7306103
chore: drop oversized memory reservation on read_parquet
vojtech-cifka May 6, 2026
d1a2289
chore: drop first_row_logged debug print
vojtech-cifka May 7, 2026
ea437e5
refactor: use tenacity for embed retries, narrow to httpx errors
vojtech-cifka May 7, 2026
3a8bacd
Revert "refactor: use tenacity for embed retries, narrow to httpx err…
vojtech-cifka May 7, 2026
1c2615f
chore: drop per-actor throughput log, ray reports it
vojtech-cifka May 7, 2026
ad3a7c5
chore: switch ray data progress to tqdm for non-tty envs
vojtech-cifka May 7, 2026
7a07f41
fix: format
vojtech-cifka May 7, 2026
23f08cf
Merge remote-tracking branch 'origin/master' into feature/embeddings
vojtech-cifka May 8, 2026
ac2b4b3
chore: remove duplicate mlflow run id
vojtech-cifka May 8, 2026
e580771
Merge remote-tracking branch 'origin/master' into feature/embeddings
vojtech-cifka May 8, 2026
5eae41c
fix: harden embed retries against pool timeouts
vojtech-cifka May 8, 2026
e4e83a2
fix: re-add 8GB memory reservation on read_parquet
vojtech-cifka May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# @package _global_

defaults:
- /data: dataset
- _self_

model: virchow2
2 changes: 0 additions & 2 deletions configs/experiment/preprocessing/tile_masks_05mpp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ defaults:
- /data: dataset
- _self_

slides_artifact_path: train_split/slides.parquet
tiles_artifact_path: train_split/tiles.parquet
tile_percentage_cols:
- tile_coverage_Nerve
- tile_coverage_Blood
Expand Down
15 changes: 15 additions & 0 deletions configs/preprocessing/embeddings.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# @package _global_

model: ???
output_dir: ${project_path}/embeddings
concurrency: 512
block_size: 2048
rows_per_file: 5000

metadata:
run_name: "Embeddings: ${model}"
description: Tile embeddings using model ${model}
hyperparams:
model: ${model}
concurrency: ${concurrency}
block_size: ${block_size}
2 changes: 0 additions & 2 deletions configs/preprocessing/tile_masks.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# @package _global_

slides_artifact_path: ???
tiles_artifact_path: ???
tile_percentage_cols: ???

max_concurrent: 1
Expand Down
139 changes: 139 additions & 0 deletions preprocessing/embeddings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import asyncio
import shutil
import time
from pathlib import Path
from typing import Any

import httpx
import hydra
import mlflow.artifacts
import pandas as pd
import pyarrow.dataset as pads
import ray
from omegaconf import DictConfig
from rationai import AsyncClient # type: ignore[attr-defined]
from rationai.mlkit import autolog, with_cli_args
from rationai.mlkit.lightning.loggers import MLFlowLogger
from ratiopath.tiling.read_slide_tiles import read_slide_tiles
from ray.data.expressions import col


class EmbedTiles:
def __init__(self, model: str, concurrency: int) -> None:
self.model = model
self.client = AsyncClient(
limits=httpx.Limits(
max_connections=concurrency, max_keepalive_connections=concurrency
),
timeout=200,
)

async def __call__(self, row: dict[str, Any]) -> dict[str, Any]:
try:
last_exc: httpx.HTTPError | None = None
for attempt in range(6):
try:
embedding = (
(await self.client.models.embed_image(self.model, row["tile"]))
.reshape(-1)
.tolist()
)
break
except httpx.HTTPError as exc:
last_exc = exc
if attempt < 5:
await asyncio.sleep(min(60, 2**attempt))
else:
raise RuntimeError("embed_image failed after 6 attempts") from last_exc
finally:
del row["tile"]
Comment thread
vojtech-cifka marked this conversation as resolved.
row["embedding"] = embedding
return row
Comment thread
vojtech-cifka marked this conversation as resolved.


@with_cli_args(["+preprocessing=embeddings"])
@hydra.main(config_path="../configs", config_name="preprocessing", version_base=None)
@autolog
def main(config: DictConfig, logger: MLFlowLogger) -> None:
for name in ["train", "test"]:
split_folder = Path(
mlflow.artifacts.download_artifacts(
run_id=config.dataset.mlflow_artifacts.tiling_run_id,
artifact_path=f"{name}_split",
)
)
slides = pd.read_parquet(split_folder / "slides.parquet")
slide_info = slides.set_index("id")[
["path", "level", "tile_extent_x", "tile_extent_y"]
].to_dict("index")

tiles_path = Path(
mlflow.artifacts.download_artifacts(
run_id=config.dataset.mlflow_artifacts.filter_tiles_run_id,
artifact_path=f"filter_tiles/{name}_tiles.parquet",
)
)
num_rows = pads.dataset(str(tiles_path), format="parquet").count_rows()
num_blocks = max(1, num_rows // config.block_size)

ds = ray.data.read_parquet(
str(tiles_path),
columns=["slide_id", "x", "y"],
ray_remote_args={"memory": 8 * 1024**3},
override_num_blocks=num_blocks,
Comment thread
vojtech-cifka marked this conversation as resolved.
).map(
lambda row, si: {**row, **si[row["slide_id"]]},
fn_kwargs={"si": slide_info},
)
ds = ds.with_column(
"tile",
read_slide_tiles( # pyright: ignore[reportCallIssue]
col("path"),
col("x"),
col("y"),
col("tile_extent_x"),
col("tile_extent_y"),
col("level"),
),
num_cpus=1,
memory=4 * 1024**3,
)
ds = ds.drop_columns(["path", "level", "tile_extent_x", "tile_extent_y"])
ds = ds.map(
EmbedTiles, # pyright: ignore[reportArgumentType]
fn_constructor_args=(config.model, config.concurrency),
compute=ray.data.ActorPoolStrategy(
min_size=4,
max_size=4,
max_tasks_in_flight_per_actor=max(1, config.concurrency // 4),
),
max_concurrency=config.concurrency,
)

split_dir = Path(config.output_dir) / str(name)
split_dir.mkdir(parents=True, exist_ok=True)
tiles_parquet_dir = split_dir / "tiles"
if tiles_parquet_dir.exists():
shutil.rmtree(tiles_parquet_dir)

slides.to_parquet(split_dir / "slides.parquet", index=False)

t = time.monotonic()
print(f"[main] starting write_parquet for split={name}")
ds.write_parquet(str(tiles_parquet_dir), min_rows_per_file=config.rows_per_file)
print(f"[main] write_parquet finished in {time.monotonic() - t:.1f}s")

logger.log_artifacts(str(split_dir), str(name))


if __name__ == "__main__":
ctx = ray.data.DataContext.get_current()
ctx.enable_rich_progress_bars = False
ctx.use_ray_tqdm = True
ctx.target_max_block_size = 64 * 1024 * 1024

with ray.init(
runtime_env={"excludes": [".git", ".venv"]},
object_store_memory=16 * 1024**3,
):
main()
46 changes: 24 additions & 22 deletions preprocessing/tile_masks.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,34 +74,36 @@ def process_slide(
@autolog
def main(config: DictConfig, logger: MLFlowLogger) -> None:
tiling_run_id = config.dataset.mlflow_artifacts.tiling_run_id
filter_tiles_run_id = config.dataset.mlflow_artifacts.filter_tiles_run_id
tile_percentage_cols: list[str] = list(config.tile_percentage_cols)

slides_path = mlflow.artifacts.download_artifacts(
run_id=tiling_run_id,
artifact_path=config.slides_artifact_path,
)
tiles_path = mlflow.artifacts.download_artifacts(
run_id=tiling_run_id,
artifact_path=config.tiles_artifact_path,
)
slides = pd.read_parquet(slides_path)
for split_name in ("train", "test"):
slides_path = mlflow.artifacts.download_artifacts(
run_id=tiling_run_id,
artifact_path=f"{split_name}_split/slides.parquet",
)
tiles_path = mlflow.artifacts.download_artifacts(
run_id=filter_tiles_run_id,
artifact_path=f"filter_tiles/{split_name}_tiles.parquet",
)
slides = pd.read_parquet(slides_path)
items = cast("list[dict[str, Any]]", slides.to_dict(orient="records"))

items = cast("list[dict[str, Any]]", slides.to_dict(orient="records"))
with TemporaryDirectory() as output_dir:
Path(output_dir, "outlines").mkdir()
for slide in tqdm(items, desc=split_name):
process_slide(
slide,
output_dir=output_dir,
tile_percentage_cols=tile_percentage_cols,
tiles_path=tiles_path,
)

with TemporaryDirectory() as output_dir:
Path(output_dir, "outlines").mkdir()
for slide in tqdm(items):
process_slide(
slide,
output_dir=output_dir,
tile_percentage_cols=tile_percentage_cols,
tiles_path=tiles_path,
logger.log_artifacts(
local_dir=output_dir,
artifact_path=f"{config.mlflow_artifact_path}/{split_name}_split",
)

logger.log_artifacts(
local_dir=output_dir, artifact_path=config.mlflow_artifact_path
)


if __name__ == "__main__":
main()
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ dependencies = [
"numpy>=2.3.5",
"rationai-tiling>=1.1.1",
"tifffile>=2025.12.20",
"torch>=2.0.0",
"torchvision>=0.15.0",
"timm>=1.0.0",
"einops>=0.8.0",
"matplotlib>=3.10.7",
"pyarrow>=19.0.1",
"datasets>=4.0.0",
Expand All @@ -40,7 +44,7 @@ dev = [

[tool.uv.sources]
rationai-mlkit = { git = "https://gitlab.ics.muni.cz/rationai/digital-pathology/libraries/mlkit.git" }
rationai-sdk = { git = "https://gitlab.ics.muni.cz/rationai/infrastructure/rationai-sdk-python.git" }
rationai-sdk = { git = "https://github.com/RationAI/rationai-sdk-python.git" }
rationai-tiling = { git = "https://gitlab.ics.muni.cz/rationai/digital-pathology/libraries/tiling.git" }

[tool.uv]
Expand Down
18 changes: 18 additions & 0 deletions scripts/submit_embeddings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from kube_jobs import storage, submit_job


submit_job(
job_name="tissue-classification-embeddings",
username=...,
public=False,
cpu=8,
memory="64Gi",
shm="24Gi",
script=[
"git clone https://github.com/RationAI/tissue-classification.git workdir",
"cd workdir",
"uv sync",
"uv run -m preprocessing.embeddings +experiment=...",
],
storage=[storage.secure.DATA, storage.secure.PROJECTS],
)
Comment thread
vojtech-cifka marked this conversation as resolved.
Loading