| """Performance benchmark for batch processing optimization. |
| |
| This script compares the performance of: |
| 1. Sequential single-slide processing (old method) |
| 2. Batch processing with model caching (new method) |
| |
| Usage: |
| python tests/benchmark_batch_performance.py --slides slide1.svs slide2.svs slide3.svs |
| python tests/benchmark_batch_performance.py --slide-csv test_slides.csv |
| """ |
|
|
| import argparse |
| import time |
| import pandas as pd |
| from pathlib import Path |
| import torch |
| from loguru import logger |
|
|
| from mosaic.analysis import analyze_slide |
| from mosaic.batch_analysis import analyze_slides_batch |
| from mosaic.ui.utils import load_settings, validate_settings |
|
|
|
|
| def benchmark_sequential_processing( |
| slides, settings_df, cancer_subtype_name_map, num_workers |
| ): |
| """Benchmark traditional sequential processing (models loaded per slide).""" |
| logger.info("=" * 80) |
| logger.info("BENCHMARKING: Sequential Processing (OLD METHOD)") |
| logger.info("=" * 80) |
|
|
| start_time = time.time() |
| start_memory = torch.cuda.memory_allocated() if torch.cuda.is_available() else 0 |
|
|
| results = [] |
| for idx, (slide_path, (_, row)) in enumerate(zip(slides, settings_df.iterrows())): |
| logger.info(f"Processing slide {idx + 1}/{len(slides)}: {slide_path}") |
|
|
| slide_start = time.time() |
|
|
| slide_mask, aeon_results, paladin_results = analyze_slide( |
| slide_path=slide_path, |
| seg_config=row["Segmentation Config"], |
| site_type=row["Site Type"], |
| sex=row.get("Sex", "Unknown"), |
| tissue_site=row.get("Tissue Site", "Unknown"), |
| cancer_subtype=row["Cancer Subtype"], |
| cancer_subtype_name_map=cancer_subtype_name_map, |
| ihc_subtype=row.get("IHC Subtype", ""), |
| num_workers=num_workers, |
| ) |
|
|
| slide_time = time.time() - slide_start |
| logger.info(f"Slide {idx + 1} completed in {slide_time:.2f}s") |
|
|
| results.append( |
| { |
| "slide": slide_path, |
| "time": slide_time, |
| "has_mask": slide_mask is not None, |
| "has_aeon": aeon_results is not None, |
| "has_paladin": paladin_results is not None, |
| } |
| ) |
|
|
| total_time = time.time() - start_time |
| peak_memory = torch.cuda.max_memory_allocated() if torch.cuda.is_available() else 0 |
|
|
| logger.info("=" * 80) |
| logger.info(f"Sequential processing completed in {total_time:.2f}s") |
| logger.info(f"Average time per slide: {total_time / len(slides):.2f}s") |
| if torch.cuda.is_available(): |
| logger.info(f"Peak GPU memory: {peak_memory / (1024**3):.2f} GB") |
| logger.info("=" * 80) |
|
|
| return { |
| "method": "sequential", |
| "total_time": total_time, |
| "num_slides": len(slides), |
| "avg_time_per_slide": total_time / len(slides), |
| "peak_memory_gb": peak_memory / (1024**3) if torch.cuda.is_available() else 0, |
| "per_slide_results": results, |
| } |
|
|
|
|
| def benchmark_batch_processing( |
| slides, settings_df, cancer_subtype_name_map, num_workers |
| ): |
| """Benchmark optimized batch processing (models loaded once).""" |
| logger.info("=" * 80) |
| logger.info("BENCHMARKING: Batch Processing (NEW METHOD)") |
| logger.info("=" * 80) |
|
|
| start_time = time.time() |
|
|
| |
| if torch.cuda.is_available(): |
| torch.cuda.reset_peak_memory_stats() |
|
|
| all_slide_masks, all_aeon_results, all_paladin_results = analyze_slides_batch( |
| slides=slides, |
| settings_df=settings_df, |
| cancer_subtype_name_map=cancer_subtype_name_map, |
| num_workers=num_workers, |
| aggressive_memory_mgmt=None, |
| progress=None, |
| ) |
|
|
| total_time = time.time() - start_time |
| peak_memory = torch.cuda.max_memory_allocated() if torch.cuda.is_available() else 0 |
|
|
| logger.info("=" * 80) |
| logger.info(f"Batch processing completed in {total_time:.2f}s") |
| logger.info(f"Average time per slide: {total_time / len(slides):.2f}s") |
| if torch.cuda.is_available(): |
| logger.info(f"Peak GPU memory: {peak_memory / (1024**3):.2f} GB") |
| logger.info("=" * 80) |
|
|
| return { |
| "method": "batch", |
| "total_time": total_time, |
| "num_slides": len(slides), |
| "avg_time_per_slide": total_time / len(slides), |
| "peak_memory_gb": peak_memory / (1024**3) if torch.cuda.is_available() else 0, |
| "num_successful": len(all_slide_masks), |
| } |
|
|
|
|
| def compare_results(sequential_stats, batch_stats): |
| """Compare and report performance differences.""" |
| logger.info("\n" + "=" * 80) |
| logger.info("PERFORMANCE COMPARISON") |
| logger.info("=" * 80) |
|
|
| speedup = sequential_stats["total_time"] / batch_stats["total_time"] |
| time_saved = sequential_stats["total_time"] - batch_stats["total_time"] |
| percent_faster = ( |
| 1 - (batch_stats["total_time"] / sequential_stats["total_time"]) |
| ) * 100 |
|
|
| logger.info(f"Number of slides: {sequential_stats['num_slides']}") |
| logger.info(f"") |
| logger.info(f"Sequential processing: {sequential_stats['total_time']:.2f}s") |
| logger.info(f"Batch processing: {batch_stats['total_time']:.2f}s") |
| logger.info(f"") |
| logger.info(f"Time saved: {time_saved:.2f}s") |
| logger.info(f"Speedup: {speedup:.2f}x") |
| logger.info(f"Improvement: {percent_faster:.1f}% faster") |
|
|
| if torch.cuda.is_available(): |
| logger.info(f"") |
| logger.info( |
| f"Sequential peak memory: {sequential_stats['peak_memory_gb']:.2f} GB" |
| ) |
| logger.info(f"Batch peak memory: {batch_stats['peak_memory_gb']:.2f} GB") |
| memory_diff = batch_stats["peak_memory_gb"] - sequential_stats["peak_memory_gb"] |
| logger.info(f"Memory difference: {memory_diff:+.2f} GB") |
|
|
| logger.info("=" * 80) |
|
|
| return { |
| "speedup": speedup, |
| "time_saved_seconds": time_saved, |
| "percent_faster": percent_faster, |
| "sequential_stats": sequential_stats, |
| "batch_stats": batch_stats, |
| } |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Benchmark batch processing performance" |
| ) |
| parser.add_argument("--slides", nargs="+", help="List of slide paths to process") |
| parser.add_argument( |
| "--slide-csv", type=str, help="CSV file with slide paths and settings" |
| ) |
| parser.add_argument( |
| "--num-workers", type=int, default=4, help="Number of workers for data loading" |
| ) |
| parser.add_argument( |
| "--skip-sequential", |
| action="store_true", |
| help="Skip sequential benchmark (faster, only test batch mode)", |
| ) |
| parser.add_argument( |
| "--output", type=str, help="Save benchmark results to JSON file" |
| ) |
|
|
| args = parser.parse_args() |
|
|
| if not args.slides and not args.slide_csv: |
| parser.error("Must provide either --slides or --slide-csv") |
|
|
| |
| from mosaic.gradio_app import download_and_process_models |
|
|
| cancer_subtype_name_map, cancer_subtypes, reversed_cancer_subtype_name_map = ( |
| download_and_process_models() |
| ) |
|
|
| |
| if args.slide_csv: |
| settings_df = load_settings(args.slide_csv) |
| settings_df = validate_settings( |
| settings_df, |
| cancer_subtype_name_map, |
| cancer_subtypes, |
| reversed_cancer_subtype_name_map, |
| ) |
| slides = settings_df["Slide"].tolist() |
| else: |
| slides = args.slides |
| |
| settings_df = pd.DataFrame( |
| { |
| "Slide": slides, |
| "Site Type": ["Primary"] * len(slides), |
| "Sex": ["Unknown"] * len(slides), |
| "Tissue Site": ["Unknown"] * len(slides), |
| "Cancer Subtype": ["Unknown"] * len(slides), |
| |
| "Segmentation Config": ["Biopsy"] * len(slides), |
| } |
| ) |
|
|
| logger.info(f"Benchmarking with {len(slides)} slides") |
| logger.info(f"GPU available: {torch.cuda.is_available()}") |
| if torch.cuda.is_available(): |
| logger.info(f"GPU: {torch.cuda.get_device_name(0)}") |
|
|
| |
| if not args.skip_sequential: |
| sequential_stats = benchmark_sequential_processing( |
| slides, settings_df, cancer_subtype_name_map, args.num_workers |
| ) |
|
|
| batch_stats = benchmark_batch_processing( |
| slides, settings_df, cancer_subtype_name_map, args.num_workers |
| ) |
|
|
| |
| if not args.skip_sequential: |
| comparison = compare_results(sequential_stats, batch_stats) |
|
|
| |
| if args.output: |
| import json |
|
|
| output_path = Path(args.output) |
| with open(output_path, "w") as f: |
| json.dump(comparison, f, indent=2, default=str) |
| logger.info(f"Benchmark results saved to {output_path}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|