""" Hugging Face キャッシュ管理ユーティリティ """ import os import shutil import logging from typing import Optional, Dict, Any from huggingface_hub import scan_cache_dir, HfApi from pathlib import Path class CacheManager: """Hugging Face キャッシュを管理するクラス""" def __init__(self, cache_size_limit_gb: float = 40.0): """ キャッシュマネージャーを初期化 Args: cache_size_limit_gb: キャッシュサイズの制限(GB) """ self.cache_size_limit_gb = cache_size_limit_gb self.cache_size_limit_bytes = int(cache_size_limit_gb * 1024 * 1024 * 1024) self.logger = logging.getLogger(__name__) def get_cache_info(self) -> Dict[str, Any]: """ キャッシュの現在の状態を取得 Returns: キャッシュ情報の辞書 """ try: cache_info = scan_cache_dir() total_size = sum(repo.size_on_disk for repo in cache_info.repos) # キャッシュパスを取得 cache_path = "N/A" if cache_info.repos: # 最初のリポジトリのパスから推測 first_repo = next(iter(cache_info.repos)) cache_path = str(first_repo.repo_path.parent.parent) else: # デフォルトのキャッシュパス import os cache_path = os.getenv("HF_HUB_CACHE", os.path.expanduser("~/.cache/huggingface/hub")) return { "total_size_bytes": total_size, "total_size_gb": total_size / (1024 * 1024 * 1024), "num_repos": len(cache_info.repos), "cache_path": cache_path, "repos": [ { "repo_id": repo.repo_id, "size_on_disk": repo.size_on_disk, "size_on_disk_gb": repo.size_on_disk / (1024 * 1024 * 1024), "last_accessed": repo.last_accessed, "last_modified": repo.last_modified } for repo in cache_info.repos ] } except Exception as e: self.logger.error(f"キャッシュ情報の取得に失敗: {e}") return {"error": str(e)} def cleanup_cache(self, force: bool = False) -> Dict[str, Any]: """ キャッシュをクリーンアップ Args: force: 強制的にクリーンアップする場合はTrue Returns: クリーンアップ結果の辞書 """ try: cache_info = scan_cache_dir() total_size = sum(repo.size_on_disk for repo in cache_info.repos) # キャッシュサイズが制限を超えているか、強制実行の場合 if total_size > self.cache_size_limit_bytes or force: # 最後のアクセス時刻が古い順にソート sorted_repos = sorted( cache_info.repos, key=lambda x: x.last_accessed if x.last_accessed else 0 ) # 削除するリポジトリを選択 to_delete = [] freed_size = 0 for repo in sorted_repos: if total_size - freed_size <= self.cache_size_limit_bytes * 0.8: # 80%まで削減 break to_delete.append(repo) freed_size += repo.size_on_disk if to_delete: # 削除実行 revision_hashes = [] for repo in to_delete: revision_hashes.extend([rev.revision_hash for rev in repo.revisions]) delete_strategy = cache_info.delete_revisions(*revision_hashes) expected_freed_size = delete_strategy.expected_freed_size delete_strategy.execute() self.logger.info(f"キャッシュクリーンアップ完了: {expected_freed_size / (1024 * 1024 * 1024):.2f}GB削除") return { "success": True, "deleted_repos": len(to_delete), "freed_size_bytes": expected_freed_size, "freed_size_gb": expected_freed_size / (1024 * 1024 * 1024) } else: return { "success": True, "deleted_repos": 0, "freed_size_bytes": 0, "freed_size_gb": 0, "message": "削除するリポジトリがありません" } else: return { "success": True, "deleted_repos": 0, "freed_size_bytes": 0, "freed_size_gb": 0, "message": "キャッシュサイズが制限内です" } except Exception as e: self.logger.error(f"キャッシュクリーンアップに失敗: {e}") return {"success": False, "error": str(e)} def get_disk_usage(self) -> Dict[str, Any]: """ ディスク使用状況を取得 Returns: ディスク使用状況の辞書 """ try: # ワーキングディレクトリの使用状況 working_dir = Path.cwd() working_dir_usage = shutil.disk_usage(working_dir) # キャッシュディレクトリの使用状況 cache_info = self.get_cache_info() return { "working_directory": { "path": str(working_dir), "total_gb": working_dir_usage.total / (1024 * 1024 * 1024), "used_gb": working_dir_usage.used / (1024 * 1024 * 1024), "free_gb": working_dir_usage.free / (1024 * 1024 * 1024), "usage_percent": (working_dir_usage.used / working_dir_usage.total) * 100 }, "cache": cache_info } except Exception as e: self.logger.error(f"ディスク使用状況の取得に失敗: {e}") return {"error": str(e)} def should_cleanup(self) -> bool: """ クリーンアップが必要かどうかを判定 Returns: クリーンアップが必要な場合True """ try: disk_usage = self.get_disk_usage() if "error" in disk_usage: return False # ディスク使用量が85%を超えている場合、またはキャッシュサイズが制限を超えている場合 usage_percent = disk_usage["working_directory"]["usage_percent"] cache_size_gb = disk_usage["cache"].get("total_size_gb", 0) return usage_percent > 85 or cache_size_gb > self.cache_size_limit_gb except Exception as e: self.logger.error(f"クリーンアップ判定に失敗: {e}") return False def emergency_cleanup(self) -> Dict[str, Any]: """ 緊急時のキャッシュクリーンアップ(より積極的な削除) Returns: クリーンアップ結果の辞書 """ try: cache_info = scan_cache_dir() # 全てのリポジトリを削除対象とする revision_hashes = [] for repo in cache_info.repos: revision_hashes.extend([rev.revision_hash for rev in repo.revisions]) if revision_hashes: delete_strategy = cache_info.delete_revisions(*revision_hashes) expected_freed_size = delete_strategy.expected_freed_size delete_strategy.execute() self.logger.warning(f"緊急キャッシュクリーンアップ完了: {expected_freed_size / (1024 * 1024 * 1024):.2f}GB削除") return { "success": True, "deleted_repos": len(cache_info.repos), "freed_size_bytes": expected_freed_size, "freed_size_gb": expected_freed_size / (1024 * 1024 * 1024) } else: return { "success": True, "deleted_repos": 0, "freed_size_bytes": 0, "freed_size_gb": 0, "message": "削除するキャッシュがありません" } except Exception as e: self.logger.error(f"緊急キャッシュクリーンアップに失敗: {e}") return {"success": False, "error": str(e)}