mirror of
https://github.com/hanzoui/pyisolate.git
synced 2026-10-30 20:37:17 +00:00
1016 lines
40 KiB
Python
1016 lines
40 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Memory usage benchmarks for pyisolate.
|
|
|
|
This script measures RAM and VRAM usage across host and child processes
|
|
with varying numbers of extensions and different tensor sharing configurations.
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import gc
|
|
import platform
|
|
import sys
|
|
import time
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import psutil
|
|
|
|
# Add parent directory to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
# Add example directory to path for shared imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "example"))
|
|
|
|
# Add benchmarks directory to path for local imports
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
try:
|
|
import torch
|
|
|
|
TORCH_AVAILABLE = True
|
|
CUDA_AVAILABLE = torch.cuda.is_available()
|
|
except ImportError:
|
|
TORCH_AVAILABLE = False
|
|
CUDA_AVAILABLE = False
|
|
|
|
try:
|
|
import pynvml as nvml
|
|
|
|
NVML_AVAILABLE = True
|
|
except ImportError:
|
|
nvml = None
|
|
NVML_AVAILABLE = False
|
|
|
|
import contextlib
|
|
import tempfile
|
|
import shutil
|
|
|
|
from memory_extension_base import MemoryBenchmarkExtensionBase
|
|
from benchmark_harness import BenchmarkHarness
|
|
from tabulate import tabulate
|
|
|
|
from pyisolate import ExtensionConfig, ExtensionManager, ExtensionManagerConfig
|
|
|
|
|
|
class MemoryTracker:
|
|
"""Tracks memory usage across host and child processes."""
|
|
|
|
def __init__(self):
|
|
self.process = psutil.Process()
|
|
self.nvml_initialized = False
|
|
self.gpu_handle = None
|
|
self.baseline_gpu_memory_mb = 0
|
|
self.platform = platform.system()
|
|
|
|
if NVML_AVAILABLE and nvml:
|
|
try:
|
|
nvml.nvmlInit()
|
|
self.nvml_initialized = True
|
|
# Get the first GPU
|
|
self.gpu_handle = nvml.nvmlDeviceGetHandleByIndex(0)
|
|
# Store baseline GPU memory usage
|
|
mem_info = nvml.nvmlDeviceGetMemoryInfo(self.gpu_handle)
|
|
self.baseline_gpu_memory_mb = mem_info.used / 1024 / 1024
|
|
print(
|
|
f"NVML initialized on {self.platform}. "
|
|
f"Initial GPU memory: {self.baseline_gpu_memory_mb:.1f} MB"
|
|
)
|
|
except Exception as e:
|
|
print(f"Failed to initialize NVML on {self.platform}: {e}")
|
|
self.nvml_initialized = False
|
|
|
|
# Try to get baseline GPU memory using nvidia-smi as fallback on Windows
|
|
if not self.nvml_initialized and self.platform == "Windows":
|
|
baseline = self._get_gpu_memory_nvidia_smi()
|
|
if baseline is not None:
|
|
self.baseline_gpu_memory_mb = baseline
|
|
print(f"Using nvidia-smi fallback. Initial GPU memory: {baseline:.1f} MB")
|
|
|
|
def _get_gpu_memory_nvidia_smi(self) -> Optional[float]:
|
|
"""Get GPU memory usage using nvidia-smi command (Windows fallback)."""
|
|
try:
|
|
import shutil
|
|
import subprocess
|
|
|
|
# Find nvidia-smi executable
|
|
nvidia_smi = shutil.which("nvidia-smi")
|
|
if not nvidia_smi:
|
|
return None
|
|
|
|
# Query GPU memory usage via nvidia-smi
|
|
result = subprocess.run( # noqa: S603
|
|
[nvidia_smi, "--query-gpu=memory.used", "--format=csv,nounits,noheader"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
# Parse the output (in MB)
|
|
used_mb = float(result.stdout.strip())
|
|
return used_mb
|
|
except Exception:
|
|
return None
|
|
|
|
def _get_gpu_memory_windows_fallback(self, memory_info: dict[str, float]) -> dict[str, float]:
|
|
"""Fallback method to get GPU memory on Windows using nvidia-smi."""
|
|
current_used = self._get_gpu_memory_nvidia_smi()
|
|
if current_used is not None:
|
|
memory_info["gpu_used_mb"] = current_used
|
|
memory_info["total_vram_mb"] = current_used
|
|
|
|
# Calculate delta from baseline
|
|
vram_delta = current_used - self.baseline_gpu_memory_mb
|
|
memory_info["host_vram_mb"] = max(0, vram_delta)
|
|
|
|
# Try to get total GPU memory
|
|
try:
|
|
import shutil
|
|
import subprocess
|
|
|
|
nvidia_smi = shutil.which("nvidia-smi")
|
|
if nvidia_smi:
|
|
result = subprocess.run( # noqa: S603
|
|
[nvidia_smi, "--query-gpu=memory.total", "--format=csv,nounits,noheader"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
memory_info["gpu_total_mb"] = float(result.stdout.strip())
|
|
except Exception as e:
|
|
# Log the error for debugging but continue
|
|
if self.platform == "Windows":
|
|
print(f"Could not get total GPU memory via nvidia-smi: {e}", file=sys.stderr)
|
|
|
|
return memory_info
|
|
|
|
def get_process_tree_pids(self) -> list[int]:
|
|
"""Get all PIDs in the process tree (including children)."""
|
|
pids = [self.process.pid]
|
|
try:
|
|
children = self.process.children(recursive=True)
|
|
pids.extend([child.pid for child in children])
|
|
except psutil.NoSuchProcess:
|
|
pass
|
|
return pids
|
|
|
|
def get_memory_usage(self) -> dict[str, float]:
|
|
"""Get current memory usage for host and all child processes."""
|
|
memory_info = {
|
|
"host_ram_mb": 0,
|
|
"children_ram_mb": 0,
|
|
"total_ram_mb": 0,
|
|
"host_vram_mb": 0,
|
|
"total_vram_mb": 0,
|
|
"gpu_used_mb": 0,
|
|
"gpu_total_mb": 0,
|
|
"num_processes": 1,
|
|
}
|
|
|
|
# Get RAM usage
|
|
try:
|
|
# Host process
|
|
host_info = self.process.memory_info()
|
|
memory_info["host_ram_mb"] = host_info.rss / 1024 / 1024
|
|
|
|
# Child processes
|
|
children = self.process.children(recursive=True)
|
|
memory_info["num_processes"] = 1 + len(children)
|
|
|
|
for child in children:
|
|
try:
|
|
child_info = child.memory_info()
|
|
memory_info["children_ram_mb"] += child_info.rss / 1024 / 1024
|
|
except psutil.NoSuchProcess:
|
|
pass
|
|
|
|
memory_info["total_ram_mb"] = memory_info["host_ram_mb"] + memory_info["children_ram_mb"]
|
|
|
|
except Exception as e:
|
|
print(f"Error getting RAM usage: {e}")
|
|
|
|
# Get GPU memory usage - use total system VRAM since extensions run in separate processes
|
|
if self.nvml_initialized and self.gpu_handle:
|
|
try:
|
|
# Get total GPU memory info
|
|
mem_info = nvml.nvmlDeviceGetMemoryInfo(self.gpu_handle)
|
|
current_used_mb = mem_info.used / 1024 / 1024
|
|
memory_info["gpu_used_mb"] = current_used_mb
|
|
memory_info["gpu_total_mb"] = mem_info.total / 1024 / 1024
|
|
memory_info["total_vram_mb"] = current_used_mb
|
|
|
|
# Calculate VRAM usage relative to baseline (captures all processes)
|
|
# This is more reliable than per-process tracking, especially on Windows
|
|
vram_delta = current_used_mb - self.baseline_gpu_memory_mb
|
|
memory_info["host_vram_mb"] = max(0, vram_delta)
|
|
except Exception as e:
|
|
print(f"Error getting GPU memory usage via NVML: {e}")
|
|
if self.platform == "Windows":
|
|
# Try alternative method for Windows
|
|
memory_info = self._get_gpu_memory_windows_fallback(memory_info)
|
|
|
|
# Fallback: Try Windows nvidia-smi if NVML not initialized
|
|
elif not self.nvml_initialized and self.platform == "Windows":
|
|
memory_info = self._get_gpu_memory_windows_fallback(memory_info)
|
|
|
|
# Fallback: try PyTorch CUDA memory for current process if all else failed
|
|
elif CUDA_AVAILABLE and torch.cuda.is_available():
|
|
try:
|
|
# This only captures current process, but better than nothing
|
|
allocated_mb = torch.cuda.memory_allocated() / 1024 / 1024
|
|
reserved_mb = torch.cuda.memory_reserved() / 1024 / 1024
|
|
|
|
memory_info["host_vram_mb"] = allocated_mb
|
|
memory_info["total_vram_mb"] = allocated_mb
|
|
memory_info["pytorch_reserved_mb"] = reserved_mb
|
|
|
|
print(
|
|
"Warning: Using PyTorch CUDA memory (current process only): "
|
|
+ f"{allocated_mb:.1f} MB allocated",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
except Exception as e:
|
|
print(f"Error getting PyTorch CUDA memory: {e}")
|
|
|
|
return memory_info
|
|
|
|
def reset_baseline(self):
|
|
"""Reset the baseline GPU memory measurement."""
|
|
if self.nvml_initialized and self.gpu_handle:
|
|
try:
|
|
mem_info = nvml.nvmlDeviceGetMemoryInfo(self.gpu_handle)
|
|
old_baseline = self.baseline_gpu_memory_mb
|
|
self.baseline_gpu_memory_mb = mem_info.used / 1024 / 1024
|
|
print(
|
|
f"[DEBUG {self.platform}] Reset baseline from {old_baseline:.1f} MB "
|
|
f"to {self.baseline_gpu_memory_mb:.1f} MB",
|
|
file=sys.stderr,
|
|
)
|
|
except Exception as e:
|
|
print(f"Error resetting GPU memory baseline on {self.platform}: {e}")
|
|
|
|
def __del__(self):
|
|
"""Cleanup NVML on deletion."""
|
|
if self.nvml_initialized:
|
|
with contextlib.suppress(Exception):
|
|
nvml.nvmlShutdown()
|
|
|
|
|
|
async def create_memory_benchmark_extension() -> str:
|
|
"""Create the extension code for memory benchmarking."""
|
|
return '''
|
|
import torch
|
|
|
|
|
|
class MemoryBenchmarkExtension:
|
|
"""Extension for memory usage benchmarking."""
|
|
|
|
def __init__(self):
|
|
self.stored_tensors = {}
|
|
|
|
async def initialize(self):
|
|
"""Initialize the extension."""
|
|
pass
|
|
|
|
async def prepare_shutdown(self):
|
|
"""Clean up before shutdown."""
|
|
self.stored_tensors.clear()
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
|
|
async def do_stuff(self, value):
|
|
"""Process a value."""
|
|
return f"Processed: {value}"
|
|
|
|
async def store_tensor(self, tensor_id: str, tensor: torch.Tensor) -> dict:
|
|
"""Store a tensor and return memory info."""
|
|
# Store the tensor
|
|
self.stored_tensors[tensor_id] = tensor
|
|
|
|
# Get memory info
|
|
info = {
|
|
'tensor_id': tensor_id,
|
|
'shape': list(tensor.shape),
|
|
'dtype': str(tensor.dtype),
|
|
'device': str(tensor.device),
|
|
'size_mb': tensor.element_size() * tensor.numel() / (1024 * 1024),
|
|
'num_stored': len(self.stored_tensors)
|
|
}
|
|
|
|
# Force GPU sync if on CUDA
|
|
if tensor.is_cuda:
|
|
torch.cuda.synchronize()
|
|
|
|
return info
|
|
|
|
async def clear_tensors(self) -> None:
|
|
"""Clear all stored tensors."""
|
|
self.stored_tensors.clear()
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
|
|
async def get_tensor_info(self, tensor_id: str) -> dict:
|
|
"""Get info about a stored tensor."""
|
|
if tensor_id not in self.stored_tensors:
|
|
return {'exists': False}
|
|
|
|
tensor = self.stored_tensors[tensor_id]
|
|
return {
|
|
'exists': True,
|
|
'shape': list(tensor.shape),
|
|
'device': str(tensor.device),
|
|
'data_ptr': tensor.data_ptr() # Memory address for sharing detection
|
|
}
|
|
|
|
|
|
def memory_benchmark_entrypoint():
|
|
"""Entry point for the memory benchmark extension."""
|
|
return MemoryBenchmarkExtension()
|
|
'''
|
|
|
|
|
|
"""Runs memory usage benchmarks with multiple extensions."""
|
|
|
|
def __init__(self, test_base: BenchmarkHarness):
|
|
self.test_base = test_base
|
|
self.memory_tracker = MemoryTracker()
|
|
self.results = []
|
|
|
|
async def run_baseline_memory_test(self) -> dict[str, float]:
|
|
"""Measure baseline memory usage with no extensions."""
|
|
print("\nMeasuring baseline memory usage...")
|
|
gc.collect()
|
|
if CUDA_AVAILABLE:
|
|
torch.cuda.empty_cache()
|
|
|
|
# Wait a bit for memory to settle
|
|
await asyncio.sleep(1)
|
|
|
|
baseline = self.memory_tracker.get_memory_usage()
|
|
print(f"Baseline: {baseline['total_ram_mb']:.1f} MB RAM, {baseline['total_vram_mb']:.1f} MB VRAM")
|
|
if baseline["gpu_total_mb"] > 0:
|
|
gpu_pct = (baseline["gpu_used_mb"] / baseline["gpu_total_mb"]) * 100
|
|
print(
|
|
f"GPU Memory: {baseline['gpu_used_mb']:.1f} / "
|
|
f"{baseline['gpu_total_mb']:.1f} MB ({gpu_pct:.1f}% used)"
|
|
)
|
|
return baseline
|
|
|
|
async def run_scaling_test(
|
|
self,
|
|
num_extensions_list: list[int],
|
|
share_torch: bool = True,
|
|
test_tensor_size: tuple[int, ...] = (512, 512),
|
|
use_cuda: bool = False,
|
|
) -> list[dict]:
|
|
"""Test memory scaling with different numbers of extensions."""
|
|
results = []
|
|
extension_code = await create_memory_benchmark_extension()
|
|
|
|
for num_extensions in num_extensions_list:
|
|
print(f"\n{'=' * 60}")
|
|
print(f"Testing with {num_extensions} extensions (share_torch={share_torch})")
|
|
print("=" * 60)
|
|
|
|
# Create extensions
|
|
extensions = []
|
|
manager = ExtensionManager(
|
|
MemoryBenchmarkExtensionBase,
|
|
ExtensionManagerConfig(venv_root_path=str(self.test_base.test_root / "extension-venvs")),
|
|
)
|
|
|
|
# Clean up and reset baseline before measuring
|
|
gc.collect()
|
|
if CUDA_AVAILABLE:
|
|
torch.cuda.empty_cache()
|
|
torch.cuda.synchronize() # Ensure all operations complete
|
|
|
|
# Reset GPU memory baseline for this test
|
|
self.memory_tracker.reset_baseline()
|
|
|
|
# Wait a moment for memory to settle
|
|
await asyncio.sleep(1)
|
|
|
|
before_memory = self.memory_tracker.get_memory_usage()
|
|
print(
|
|
f"Baseline GPU memory: {before_memory.get('gpu_used_mb', 0):.1f} MB "
|
|
f"(baseline: {self.memory_tracker.baseline_gpu_memory_mb:.1f} MB)"
|
|
)
|
|
|
|
# Create and load extensions
|
|
print(f"Creating {num_extensions} extensions...")
|
|
for i in range(num_extensions):
|
|
ext_name = f"memory_ext_{i}"
|
|
self.test_base.create_extension(
|
|
ext_name,
|
|
dependencies=["torch>=2.0.0"] if TORCH_AVAILABLE else [],
|
|
share_torch=share_torch,
|
|
extension_code=extension_code,
|
|
)
|
|
|
|
config = ExtensionConfig(
|
|
name=ext_name,
|
|
module_path=str(self.test_base.test_root / "extensions" / ext_name),
|
|
isolated=True,
|
|
dependencies=["torch>=2.0.0"] if TORCH_AVAILABLE else [],
|
|
apis=[],
|
|
share_torch=share_torch,
|
|
)
|
|
|
|
ext = manager.load_extension(config)
|
|
extensions.append((ext_name, ext))
|
|
|
|
# Wait for extensions to fully initialize
|
|
await asyncio.sleep(2)
|
|
|
|
# Measure memory after loading extensions
|
|
after_load_memory = self.memory_tracker.get_memory_usage()
|
|
|
|
# Create test tensor
|
|
print(f"Creating test tensor {test_tensor_size}...")
|
|
with torch.inference_mode():
|
|
if use_cuda and CUDA_AVAILABLE:
|
|
test_tensor = torch.randn(*test_tensor_size, device="cuda")
|
|
torch.cuda.synchronize() # Ensure tensor creation completes
|
|
else:
|
|
test_tensor = torch.randn(*test_tensor_size)
|
|
|
|
tensor_size_mb = test_tensor.element_size() * test_tensor.numel() / (1024 * 1024)
|
|
print(f"Tensor size: {tensor_size_mb:.1f} MB on {test_tensor.device}")
|
|
|
|
# Check memory after tensor creation
|
|
if use_cuda and CUDA_AVAILABLE:
|
|
post_tensor_memory = self.memory_tracker.get_memory_usage()
|
|
print(
|
|
f"GPU memory after tensor creation: {post_tensor_memory.get('gpu_used_mb', 0):.1f} MB "
|
|
f"(delta: {post_tensor_memory.get('host_vram_mb', 0):.1f} MB)"
|
|
)
|
|
|
|
# Send tensor to all extensions
|
|
print(f"Sending tensor to {num_extensions} extensions...")
|
|
send_start = time.time()
|
|
|
|
for i, (ext_name, ext) in enumerate(extensions):
|
|
try:
|
|
info = await ext.store_tensor(f"test_tensor_{i}", test_tensor)
|
|
if i == 0:
|
|
print(f" First extension stored: {info}")
|
|
# Force GPU sync after each send for accurate memory tracking
|
|
if use_cuda and CUDA_AVAILABLE:
|
|
torch.cuda.synchronize()
|
|
except Exception as e:
|
|
print(f" Failed to send to {ext_name}: {e}")
|
|
|
|
send_time = time.time() - send_start
|
|
print(f"Send completed in {send_time:.2f}s")
|
|
|
|
# Force final sync before measuring
|
|
if use_cuda and CUDA_AVAILABLE:
|
|
torch.cuda.synchronize()
|
|
|
|
# Wait for memory to settle
|
|
await asyncio.sleep(2)
|
|
|
|
# Measure memory after sending tensors
|
|
after_send_memory = self.memory_tracker.get_memory_usage()
|
|
|
|
# Check tensor sharing (get data pointers from a few extensions)
|
|
data_ptrs = []
|
|
for i in range(min(3, num_extensions)):
|
|
try:
|
|
ext_name, ext = extensions[i]
|
|
info = await ext.get_tensor_info(f"test_tensor_{i}")
|
|
if info.get("exists"):
|
|
data_ptrs.append(info.get("data_ptr"))
|
|
except Exception as e:
|
|
print(f" Failed to get tensor info from {ext_name}: {e}")
|
|
|
|
shared_memory = len(set(data_ptrs)) == 1 if len(data_ptrs) > 1 else None
|
|
|
|
# Calculate memory deltas
|
|
result = {
|
|
"num_extensions": num_extensions,
|
|
"share_torch": share_torch,
|
|
"tensor_size_mb": tensor_size_mb,
|
|
"tensor_device": str(test_tensor.device),
|
|
"before_ram_mb": before_memory["total_ram_mb"],
|
|
"after_load_ram_mb": after_load_memory["total_ram_mb"],
|
|
"after_send_ram_mb": after_send_memory["total_ram_mb"],
|
|
"load_ram_delta_mb": after_load_memory["total_ram_mb"] - before_memory["total_ram_mb"],
|
|
"send_ram_delta_mb": after_send_memory["total_ram_mb"] - after_load_memory["total_ram_mb"],
|
|
"ram_per_extension_mb": (after_load_memory["total_ram_mb"] - before_memory["total_ram_mb"])
|
|
/ num_extensions,
|
|
"before_vram_mb": before_memory["total_vram_mb"],
|
|
"after_load_vram_mb": after_load_memory["total_vram_mb"],
|
|
"after_send_vram_mb": after_send_memory["total_vram_mb"],
|
|
"send_vram_delta_mb": after_send_memory["total_vram_mb"] - after_load_memory["total_vram_mb"],
|
|
# Add GPU total memory tracking
|
|
"before_gpu_mb": before_memory.get("gpu_used_mb", 0),
|
|
"after_load_gpu_mb": after_load_memory.get("gpu_used_mb", 0),
|
|
"after_send_gpu_mb": after_send_memory.get("gpu_used_mb", 0),
|
|
"load_gpu_delta_mb": after_load_memory.get("gpu_used_mb", 0)
|
|
- before_memory.get("gpu_used_mb", 0),
|
|
"send_gpu_delta_mb": after_send_memory.get("gpu_used_mb", 0)
|
|
- after_load_memory.get("gpu_used_mb", 0),
|
|
"shared_memory": shared_memory,
|
|
"send_time_s": send_time,
|
|
}
|
|
|
|
results.append(result)
|
|
|
|
# Print summary
|
|
print("\nMemory Summary:")
|
|
print(f" RAM per extension: {result['ram_per_extension_mb']:.1f} MB")
|
|
print(f" RAM for tensor transfer: {result['send_ram_delta_mb']:.1f} MB")
|
|
|
|
# Debug GPU memory tracking
|
|
print("\nGPU Memory Details:")
|
|
print(f" Before: {before_memory.get('gpu_used_mb', 0):.1f} MB")
|
|
print(f" After Load: {after_load_memory.get('gpu_used_mb', 0):.1f} MB")
|
|
print(f" After Send: {after_send_memory.get('gpu_used_mb', 0):.1f} MB")
|
|
print(f" Baseline: {self.memory_tracker.baseline_gpu_memory_mb:.1f} MB")
|
|
|
|
# Show GPU memory if this is a GPU test
|
|
if use_cuda and result["load_gpu_delta_mb"] > 0:
|
|
print(f" GPU memory for tensor creation: {result['load_gpu_delta_mb']:.1f} MB")
|
|
print(f" GPU memory for tensor transfer: {result['send_gpu_delta_mb']:.1f} MB")
|
|
else:
|
|
print(f" VRAM for tensor transfer: {result['send_vram_delta_mb']:.1f} MB")
|
|
|
|
if shared_memory is not None:
|
|
print(f" Tensor memory shared: {shared_memory}")
|
|
|
|
# Cleanup
|
|
print("\nCleaning up extensions...")
|
|
manager.stop_all_extensions()
|
|
del test_tensor
|
|
gc.collect()
|
|
if CUDA_AVAILABLE:
|
|
torch.cuda.empty_cache()
|
|
torch.cuda.synchronize()
|
|
|
|
# Wait for cleanup
|
|
await asyncio.sleep(2)
|
|
|
|
return results
|
|
|
|
async def run_large_tensor_sharing_test(
|
|
self, num_extensions: int = 50, tensor_gb: float = 2.0, test_both_modes: bool = False
|
|
) -> dict:
|
|
"""Test memory sharing with a large tensor across multiple extensions."""
|
|
print(f"\n{'=' * 60}")
|
|
print(f"Large Tensor Sharing Test ({tensor_gb}GB tensor, {num_extensions} extensions)")
|
|
print("=" * 60)
|
|
|
|
extension_code = await create_memory_benchmark_extension()
|
|
results = {}
|
|
|
|
# Test both CPU and GPU tensors
|
|
for use_cuda in [False, True]:
|
|
if use_cuda and not CUDA_AVAILABLE:
|
|
continue
|
|
|
|
device_name = "GPU" if use_cuda else "CPU"
|
|
print(f"\n{'=' * 50}")
|
|
print(f"Testing {device_name} Tensors")
|
|
print("=" * 50)
|
|
|
|
results[device_name.lower()] = {}
|
|
|
|
# Test only with share_torch=True by default
|
|
share_torch_modes = [False, True] if test_both_modes else [True]
|
|
for share_torch in share_torch_modes:
|
|
print(f"\n--- Testing {device_name} with share_torch={share_torch} ---")
|
|
|
|
# Create extensions
|
|
extensions = []
|
|
manager = ExtensionManager(
|
|
MemoryBenchmarkExtensionBase,
|
|
ExtensionManagerConfig(venv_root_path=str(self.test_base.test_root / "extension-venvs")),
|
|
)
|
|
|
|
# Measure baseline
|
|
gc.collect()
|
|
if CUDA_AVAILABLE:
|
|
torch.cuda.empty_cache()
|
|
baseline = self.memory_tracker.get_memory_usage()
|
|
|
|
# Create extensions
|
|
for i in range(num_extensions):
|
|
ext_name = f"large_test_ext_{device_name.lower()}_{i}"
|
|
self.test_base.create_extension(
|
|
ext_name,
|
|
dependencies=["torch>=2.0.0"],
|
|
share_torch=share_torch,
|
|
extension_code=extension_code,
|
|
)
|
|
|
|
config = ExtensionConfig(
|
|
name=ext_name,
|
|
module_path=str(self.test_base.test_root / "extensions" / ext_name),
|
|
isolated=True,
|
|
dependencies=["torch>=2.0.0"],
|
|
apis=[],
|
|
share_torch=share_torch,
|
|
)
|
|
|
|
ext = manager.load_extension(config)
|
|
extensions.append((ext_name, ext))
|
|
|
|
# Wait for extensions to initialize
|
|
await asyncio.sleep(2)
|
|
|
|
# Create large tensor
|
|
# Calculate size for desired GB (float32 = 4 bytes per element)
|
|
num_elements = int(tensor_gb * 1024 * 1024 * 1024 / 4)
|
|
# Make it a square-ish tensor
|
|
side = int(num_elements**0.5)
|
|
|
|
print(f"Creating {tensor_gb}GB tensor ({side}x{side}) on {device_name}...")
|
|
with torch.inference_mode():
|
|
large_tensor = (
|
|
torch.randn(side, side, device="cuda") if use_cuda else torch.randn(side, side)
|
|
)
|
|
actual_size_mb = large_tensor.element_size() * large_tensor.numel() / (1024 * 1024)
|
|
print(f"Actual tensor size: {actual_size_mb:.1f} MB on {large_tensor.device}")
|
|
|
|
# Measure after tensor creation
|
|
after_create = self.memory_tracker.get_memory_usage()
|
|
|
|
# Send to all extensions
|
|
print(f"Sending large {device_name} tensor to {num_extensions} extensions...")
|
|
send_start = time.time()
|
|
|
|
for _i, (ext_name, ext) in enumerate(extensions):
|
|
try:
|
|
await ext.store_tensor("large_tensor", large_tensor)
|
|
print(f" Sent to {ext_name}")
|
|
except Exception as e:
|
|
print(f" Failed to send to {ext_name}: {e}")
|
|
|
|
send_time = time.time() - send_start
|
|
|
|
# Measure after sending
|
|
await asyncio.sleep(2)
|
|
after_send = self.memory_tracker.get_memory_usage()
|
|
|
|
# Store results
|
|
results[device_name.lower()][f"share_torch_{share_torch}"] = {
|
|
"baseline_ram_mb": baseline["total_ram_mb"],
|
|
"after_create_ram_mb": after_create["total_ram_mb"],
|
|
"after_send_ram_mb": after_send["total_ram_mb"],
|
|
"baseline_vram_mb": baseline["total_vram_mb"],
|
|
"after_create_vram_mb": after_create["total_vram_mb"],
|
|
"after_send_vram_mb": after_send["total_vram_mb"],
|
|
"tensor_size_mb": actual_size_mb,
|
|
"tensor_device": str(large_tensor.device),
|
|
"ram_for_tensor_creation_mb": after_create["total_ram_mb"] - baseline["total_ram_mb"],
|
|
"ram_for_distribution_mb": after_send["total_ram_mb"] - after_create["total_ram_mb"],
|
|
"ram_per_extension_copy_mb": (after_send["total_ram_mb"] - after_create["total_ram_mb"])
|
|
/ num_extensions
|
|
if num_extensions > 0
|
|
else 0,
|
|
"vram_for_tensor_creation_mb": after_create["total_vram_mb"] - baseline["total_vram_mb"],
|
|
"vram_for_distribution_mb": after_send["total_vram_mb"] - after_create["total_vram_mb"],
|
|
# Add GPU total memory tracking
|
|
"baseline_gpu_mb": baseline.get("gpu_used_mb", 0),
|
|
"after_create_gpu_mb": after_create.get("gpu_used_mb", 0),
|
|
"after_send_gpu_mb": after_send.get("gpu_used_mb", 0),
|
|
"gpu_for_tensor_creation_mb": after_create.get("gpu_used_mb", 0)
|
|
- baseline.get("gpu_used_mb", 0),
|
|
"gpu_for_distribution_mb": after_send.get("gpu_used_mb", 0)
|
|
- after_create.get("gpu_used_mb", 0),
|
|
"send_time_s": send_time,
|
|
}
|
|
|
|
# Cleanup
|
|
manager.stop_all_extensions()
|
|
del large_tensor
|
|
gc.collect()
|
|
if CUDA_AVAILABLE:
|
|
torch.cuda.empty_cache()
|
|
await asyncio.sleep(2)
|
|
|
|
return results
|
|
|
|
|
|
async def run_memory_benchmarks(
|
|
extension_counts: list[int],
|
|
test_small_tensor: bool = True,
|
|
test_large_tensor: bool = True,
|
|
max_extensions_for_large: int = 50,
|
|
test_both_modes: bool = False,
|
|
):
|
|
"""Run the full memory benchmark suite."""
|
|
test_base = BenchmarkHarness()
|
|
await test_base.setup_test_environment("memory_benchmark")
|
|
|
|
try:
|
|
runner = MemoryBenchmarkRunner(test_base)
|
|
all_results = {}
|
|
|
|
# Baseline measurement
|
|
baseline = await runner.run_baseline_memory_test()
|
|
all_results["baseline"] = baseline
|
|
|
|
if test_small_tensor:
|
|
# Small tensor tests with multiple extension counts
|
|
print("\n" + "=" * 80)
|
|
print("SMALL TENSOR SCALING TESTS")
|
|
print("=" * 80)
|
|
|
|
# CPU tensor tests
|
|
small_tensor_size = (512, 512) # ~1MB tensor
|
|
|
|
if test_both_modes:
|
|
# Test both modes
|
|
print("\n--- CPU Tensor Tests (share_torch=False) ---")
|
|
cpu_results_no_share = await runner.run_scaling_test(
|
|
extension_counts, share_torch=False, test_tensor_size=small_tensor_size, use_cuda=False
|
|
)
|
|
all_results["cpu_no_share"] = cpu_results_no_share
|
|
|
|
print("\n--- CPU Tensor Tests (share_torch=True) ---")
|
|
cpu_results_share = await runner.run_scaling_test(
|
|
extension_counts, share_torch=True, test_tensor_size=small_tensor_size, use_cuda=False
|
|
)
|
|
all_results["cpu_share"] = cpu_results_share
|
|
|
|
# GPU tensor tests if available
|
|
if CUDA_AVAILABLE:
|
|
if test_both_modes:
|
|
print("\n--- GPU Tensor Tests (share_torch=False) ---")
|
|
gpu_results_no_share = await runner.run_scaling_test(
|
|
extension_counts, share_torch=False, test_tensor_size=small_tensor_size, use_cuda=True
|
|
)
|
|
all_results["gpu_no_share"] = gpu_results_no_share
|
|
|
|
print("\n--- GPU Tensor Tests (share_torch=True) ---")
|
|
gpu_results_share = await runner.run_scaling_test(
|
|
extension_counts, share_torch=True, test_tensor_size=small_tensor_size, use_cuda=True
|
|
)
|
|
all_results["gpu_share"] = gpu_results_share
|
|
|
|
if test_large_tensor:
|
|
# Large tensor sharing test
|
|
large_results = await runner.run_large_tensor_sharing_test(
|
|
num_extensions=min(max_extensions_for_large, max(extension_counts)),
|
|
tensor_gb=2.0,
|
|
test_both_modes=test_both_modes,
|
|
)
|
|
all_results["large_tensor_sharing"] = large_results
|
|
|
|
# Print final summary
|
|
print_memory_benchmark_summary(all_results)
|
|
|
|
finally:
|
|
await test_base.cleanup()
|
|
|
|
|
|
def print_memory_benchmark_summary(results: dict):
|
|
"""Print a comprehensive summary of memory benchmark results."""
|
|
print("\n" + "=" * 80)
|
|
print("MEMORY BENCHMARK SUMMARY")
|
|
print("=" * 80)
|
|
|
|
# Baseline
|
|
if "baseline" in results:
|
|
baseline = results["baseline"]
|
|
print("\nBaseline Memory Usage:")
|
|
print(f" RAM: {baseline['total_ram_mb']:.1f} MB")
|
|
print(f" VRAM: {baseline['total_vram_mb']:.1f} MB")
|
|
if baseline.get("gpu_total_mb", 0) > 0:
|
|
gpu_pct = (baseline["gpu_used_mb"] / baseline["gpu_total_mb"]) * 100
|
|
print(
|
|
f" GPU Total: {baseline['gpu_used_mb']:.1f} / "
|
|
f"{baseline['gpu_total_mb']:.1f} MB ({gpu_pct:.1f}% used)"
|
|
)
|
|
|
|
# Scaling results
|
|
for test_type in ["cpu_no_share", "cpu_share", "gpu_no_share", "gpu_share"]:
|
|
if test_type in results:
|
|
print(f"\n{test_type.upper().replace('_', ' ')} Results:")
|
|
|
|
headers = ["Extensions", "RAM/Ext (MB)", "Tensor RAM (MB)", "GPU (MB)", "Shared"]
|
|
table_data = []
|
|
|
|
for result in results[test_type]:
|
|
# Use GPU memory delta if available, otherwise fall back to VRAM
|
|
gpu_memory = result.get("send_gpu_delta_mb", result.get("send_vram_delta_mb", 0))
|
|
table_data.append(
|
|
[
|
|
result["num_extensions"],
|
|
f"{result['ram_per_extension_mb']:.1f}",
|
|
f"{result['send_ram_delta_mb']:.1f}",
|
|
f"{gpu_memory:.1f}",
|
|
"Yes"
|
|
if result.get("shared_memory")
|
|
else "No"
|
|
if result.get("shared_memory") is False
|
|
else "N/A",
|
|
]
|
|
)
|
|
|
|
print(tabulate(table_data, headers=headers, tablefmt="grid"))
|
|
|
|
# Large tensor sharing results
|
|
if "large_tensor_sharing" in results:
|
|
print("\n2GB TENSOR SHARING TEST:")
|
|
large_results = results["large_tensor_sharing"]
|
|
|
|
# Process CPU results
|
|
if "cpu" in large_results:
|
|
print("\nCPU Tensor Results:")
|
|
headers = [
|
|
"Config",
|
|
"Tensor Size (MB)",
|
|
"Distribution RAM (MB)",
|
|
"RAM/Extension (MB)",
|
|
"Send Time (s)",
|
|
]
|
|
table_data = []
|
|
|
|
for share_torch in [False, True]:
|
|
key = f"share_torch_{share_torch}"
|
|
if key in large_results["cpu"]:
|
|
r = large_results["cpu"][key]
|
|
table_data.append(
|
|
[
|
|
f"share_torch={share_torch}",
|
|
f"{r['tensor_size_mb']:.1f}",
|
|
f"{r['ram_for_distribution_mb']:.1f}",
|
|
f"{r['ram_per_extension_copy_mb']:.1f}",
|
|
f"{r['send_time_s']:.2f}",
|
|
]
|
|
)
|
|
|
|
if table_data:
|
|
print(tabulate(table_data, headers=headers, tablefmt="grid"))
|
|
|
|
# Analysis for CPU
|
|
if "share_torch_False" in large_results["cpu"] and "share_torch_True" in large_results["cpu"]:
|
|
no_share = large_results["cpu"]["share_torch_False"]
|
|
share = large_results["cpu"]["share_torch_True"]
|
|
|
|
savings = no_share["ram_for_distribution_mb"] - share["ram_for_distribution_mb"]
|
|
savings_pct = (
|
|
(savings / no_share["ram_for_distribution_mb"] * 100)
|
|
if no_share["ram_for_distribution_mb"] > 0
|
|
else 0
|
|
)
|
|
|
|
print("\nCPU Memory Sharing Analysis:")
|
|
print(f" Memory saved with share_torch: {savings:.1f} MB ({savings_pct:.1f}%)")
|
|
|
|
# Process GPU results
|
|
if "gpu" in large_results:
|
|
print("\nGPU Tensor Results:")
|
|
headers = [
|
|
"Config",
|
|
"Tensor Size (MB)",
|
|
"RAM Dist (MB)",
|
|
"GPU Created (MB)",
|
|
"GPU Dist (MB)",
|
|
"Send Time (s)",
|
|
]
|
|
table_data = []
|
|
|
|
for share_torch in [False, True]:
|
|
key = f"share_torch_{share_torch}"
|
|
if key in large_results["gpu"]:
|
|
r = large_results["gpu"][key]
|
|
table_data.append(
|
|
[
|
|
f"share_torch={share_torch}",
|
|
f"{r['tensor_size_mb']:.1f}",
|
|
f"{r['ram_for_distribution_mb']:.1f}",
|
|
f"{r['gpu_for_tensor_creation_mb']:.1f}",
|
|
f"{r['gpu_for_distribution_mb']:.1f}",
|
|
f"{r['send_time_s']:.2f}",
|
|
]
|
|
)
|
|
|
|
if table_data:
|
|
print(tabulate(table_data, headers=headers, tablefmt="grid"))
|
|
|
|
# Analysis for GPU
|
|
if "share_torch_False" in large_results["gpu"] and "share_torch_True" in large_results["gpu"]:
|
|
no_share = large_results["gpu"]["share_torch_False"]
|
|
share = large_results["gpu"]["share_torch_True"]
|
|
|
|
ram_savings = no_share["ram_for_distribution_mb"] - share["ram_for_distribution_mb"]
|
|
ram_savings_pct = (
|
|
(ram_savings / no_share["ram_for_distribution_mb"] * 100)
|
|
if no_share["ram_for_distribution_mb"] > 0
|
|
else 0
|
|
)
|
|
|
|
print("\nGPU Memory Sharing Analysis:")
|
|
print(f" RAM saved with share_torch: {ram_savings:.1f} MB ({ram_savings_pct:.1f}%)")
|
|
|
|
gpu_savings = no_share["gpu_for_distribution_mb"] - share["gpu_for_distribution_mb"]
|
|
if no_share["gpu_for_distribution_mb"] > 0:
|
|
gpu_savings_pct = gpu_savings / no_share["gpu_for_distribution_mb"] * 100
|
|
print(
|
|
f" GPU memory saved with share_torch: {gpu_savings:.1f} MB "
|
|
f"({gpu_savings_pct:.1f}%)"
|
|
)
|
|
elif gpu_savings != 0:
|
|
print(f" GPU memory difference: {gpu_savings:.1f} MB")
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Run pyisolate memory usage benchmarks",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
python memory_benchmark.py # Run full benchmark suite
|
|
python memory_benchmark.py --max-extensions 20 # Test up to 20 extensions
|
|
python memory_benchmark.py --large-only # Only test large tensor sharing
|
|
python memory_benchmark.py --counts 1,5,10,20 # Custom extension counts
|
|
""",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--max-extensions", type=int, default=50, help="Maximum number of extensions to test (default: 50)"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--counts", type=str, help="Comma-separated list of extension counts to test (e.g., '1,5,10,20')"
|
|
)
|
|
|
|
parser.add_argument("--large-only", action="store_true", help="Only run large tensor sharing test")
|
|
|
|
parser.add_argument("--small-only", action="store_true", help="Only run small tensor scaling tests")
|
|
|
|
parser.add_argument(
|
|
"--test-both-modes",
|
|
action="store_true",
|
|
help="Test both share_torch=True and share_torch=False (default: only share_torch=True)",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Determine extension counts
|
|
if args.counts:
|
|
extension_counts = [int(x.strip()) for x in args.counts.split(",")]
|
|
else:
|
|
# Default progression: 1, 2, 5, 10, 20, 50, 100
|
|
extension_counts = [1, 2, 5, 10, 20]
|
|
if args.max_extensions >= 50:
|
|
extension_counts.append(50)
|
|
if args.max_extensions >= 100:
|
|
extension_counts.append(100)
|
|
|
|
# Filter based on max
|
|
extension_counts = [c for c in extension_counts if c <= args.max_extensions]
|
|
|
|
# Check dependencies
|
|
if not TORCH_AVAILABLE:
|
|
print("PyTorch not available. Install with: pip install torch")
|
|
return 1
|
|
|
|
print(f"Running on: {platform.system()} {platform.release()}")
|
|
|
|
if not CUDA_AVAILABLE:
|
|
print("CUDA not available. GPU memory tests will be skipped.")
|
|
|
|
if not NVML_AVAILABLE:
|
|
print("nvidia-ml-py3 not installed. Install with: pip install nvidia-ml-py3")
|
|
print("VRAM tracking will not be available.")
|
|
else:
|
|
print("NVML available for GPU memory tracking")
|
|
|
|
# Determine what to test
|
|
test_small = not args.large_only
|
|
test_large = not args.small_only
|
|
|
|
# Run benchmarks
|
|
try:
|
|
asyncio.run(
|
|
run_memory_benchmarks(
|
|
extension_counts=extension_counts,
|
|
test_small_tensor=test_small,
|
|
test_large_tensor=test_large,
|
|
test_both_modes=args.test_both_modes,
|
|
)
|
|
)
|
|
return 0
|
|
except KeyboardInterrupt:
|
|
print("\nBenchmark interrupted by user")
|
|
return 1
|
|
except Exception as e:
|
|
print(f"\nBenchmark failed with error: {e}")
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|