mirror of https://github.com/razor-ai/soup.git
212 lines
6.7 KiB
Python
212 lines
6.7 KiB
Python
"""soup adapters — LoRA adapter management (list, info, compare)."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import typer
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.table import Table
|
|
|
|
console = Console()
|
|
|
|
app = typer.Typer(no_args_is_help=True)
|
|
|
|
|
|
def _find_adapters(directory: Path, max_depth: int = 6) -> list[Path]:
|
|
"""Recursively find directories containing adapter_config.json.
|
|
|
|
Limited to max_depth levels and 200 results to prevent runaway scans.
|
|
"""
|
|
adapters = []
|
|
for config_file in directory.rglob("adapter_config.json"):
|
|
try:
|
|
depth = len(config_file.relative_to(directory).parts)
|
|
except ValueError:
|
|
continue
|
|
if depth <= max_depth:
|
|
adapters.append(config_file.parent)
|
|
if len(adapters) >= 200:
|
|
break
|
|
return sorted(adapters)
|
|
|
|
|
|
def _read_adapter_config(adapter_path: Path) -> dict:
|
|
"""Read adapter_config.json from an adapter directory."""
|
|
config_file = adapter_path / "adapter_config.json"
|
|
with open(config_file, encoding="utf-8") as fh:
|
|
return json.load(fh)
|
|
|
|
|
|
def _get_adapter_size(adapter_path: Path) -> str:
|
|
"""Get total size of adapter files on disk."""
|
|
total_bytes = 0
|
|
for file_path in adapter_path.iterdir():
|
|
if file_path.is_file():
|
|
total_bytes += file_path.stat().st_size
|
|
|
|
if total_bytes < 1024:
|
|
return f"{total_bytes} B"
|
|
elif total_bytes < 1024 * 1024:
|
|
return f"{total_bytes / 1024:.1f} KB"
|
|
elif total_bytes < 1024 * 1024 * 1024:
|
|
return f"{total_bytes / (1024 * 1024):.1f} MB"
|
|
else:
|
|
return f"{total_bytes / (1024 * 1024 * 1024):.2f} GB"
|
|
|
|
|
|
@app.command(name="list")
|
|
def list_adapters(
|
|
directory: str = typer.Argument(
|
|
".", help="Directory to scan for adapters (recursive)"
|
|
),
|
|
):
|
|
"""Scan a directory for LoRA adapters and list them."""
|
|
dir_path = Path(directory).resolve()
|
|
if not dir_path.exists():
|
|
console.print(f"[red]Directory not found: {directory}[/]")
|
|
raise typer.Exit(1)
|
|
|
|
adapters = _find_adapters(dir_path)
|
|
|
|
if not adapters:
|
|
console.print("[dim]No adapters found in this directory.[/]")
|
|
return
|
|
|
|
table = Table(title=f"Adapters in {dir_path}")
|
|
table.add_column("Path", style="bold")
|
|
table.add_column("Base Model")
|
|
table.add_column("LoRA r")
|
|
table.add_column("Type")
|
|
table.add_column("Size")
|
|
|
|
for adapter_path in adapters:
|
|
try:
|
|
config = _read_adapter_config(adapter_path)
|
|
base = config.get("base_model_name_or_path", "unknown")
|
|
lora_r = str(config.get("r", "?"))
|
|
peft_type = config.get("peft_type", "?")
|
|
size = _get_adapter_size(adapter_path)
|
|
|
|
# Shorten path relative to scan directory
|
|
try:
|
|
rel_path = adapter_path.relative_to(dir_path)
|
|
except ValueError:
|
|
rel_path = adapter_path
|
|
table.add_row(str(rel_path), base, lora_r, peft_type, size)
|
|
except (json.JSONDecodeError, OSError):
|
|
table.add_row(str(adapter_path), "[red]error[/]", "-", "-", "-")
|
|
|
|
console.print(table)
|
|
console.print(f"\n[dim]Found {len(adapters)} adapter(s).[/]")
|
|
|
|
|
|
@app.command()
|
|
def info(
|
|
adapter: str = typer.Argument(..., help="Path to adapter directory"),
|
|
):
|
|
"""Show detailed metadata for a LoRA adapter."""
|
|
adapter_path = Path(adapter).resolve()
|
|
config_file = adapter_path / "adapter_config.json"
|
|
|
|
if not adapter_path.exists():
|
|
console.print(f"[red]Adapter not found: {adapter}[/]")
|
|
raise typer.Exit(1)
|
|
|
|
if not config_file.exists():
|
|
console.print(f"[red]No adapter_config.json in: {adapter}[/]")
|
|
raise typer.Exit(1)
|
|
|
|
config = _read_adapter_config(adapter_path)
|
|
size = _get_adapter_size(adapter_path)
|
|
|
|
base_model = config.get("base_model_name_or_path", "unknown")
|
|
lora_r = config.get("r", "?")
|
|
lora_alpha = config.get("lora_alpha", "?")
|
|
lora_dropout = config.get("lora_dropout", "?")
|
|
peft_type = config.get("peft_type", "?")
|
|
task_type = config.get("task_type", "?")
|
|
target_modules = config.get("target_modules", [])
|
|
|
|
if isinstance(target_modules, list):
|
|
modules_str = ", ".join(target_modules)
|
|
else:
|
|
modules_str = str(target_modules)
|
|
|
|
info_text = (
|
|
f"Base model: [bold]{base_model}[/]\n"
|
|
f"PEFT type: [bold]{peft_type}[/]\n"
|
|
f"Task: [bold]{task_type}[/]\n"
|
|
f"LoRA rank: [bold]{lora_r}[/], alpha: [bold]{lora_alpha}[/], "
|
|
f"dropout: [bold]{lora_dropout}[/]\n"
|
|
f"Targets: [bold]{modules_str}[/]\n"
|
|
f"Size on disk: [bold]{size}[/]"
|
|
)
|
|
|
|
console.print(Panel(info_text, title=f"Adapter Info -- {adapter_path.name}"))
|
|
|
|
|
|
@app.command()
|
|
def compare(
|
|
adapter1: str = typer.Argument(..., help="Path to first adapter"),
|
|
adapter2: str = typer.Argument(..., help="Path to second adapter"),
|
|
):
|
|
"""Compare two LoRA adapters side-by-side."""
|
|
path1 = Path(adapter1).resolve()
|
|
path2 = Path(adapter2).resolve()
|
|
|
|
for label, path in [("Adapter 1", path1), ("Adapter 2", path2)]:
|
|
config_file = path / "adapter_config.json"
|
|
if not path.exists():
|
|
console.print(f"[red]{label} not found: {path}[/]")
|
|
raise typer.Exit(1)
|
|
if not config_file.exists():
|
|
console.print(f"[red]{label} has no adapter_config.json: {path}[/]")
|
|
raise typer.Exit(1)
|
|
|
|
config1 = _read_adapter_config(path1)
|
|
config2 = _read_adapter_config(path2)
|
|
|
|
table = Table(title="Adapter Comparison")
|
|
table.add_column("Field", style="bold")
|
|
table.add_column(path1.name, justify="center")
|
|
table.add_column(path2.name, justify="center")
|
|
|
|
# Fields to compare
|
|
fields = [
|
|
("Base model", "base_model_name_or_path"),
|
|
("PEFT type", "peft_type"),
|
|
("Task type", "task_type"),
|
|
("LoRA rank (r)", "r"),
|
|
("LoRA alpha", "lora_alpha"),
|
|
("LoRA dropout", "lora_dropout"),
|
|
("Target modules", "target_modules"),
|
|
]
|
|
|
|
for label, key in fields:
|
|
val1 = config1.get(key, "-")
|
|
val2 = config2.get(key, "-")
|
|
|
|
# Format lists
|
|
if isinstance(val1, list):
|
|
val1 = ", ".join(str(item) for item in val1)
|
|
if isinstance(val2, list):
|
|
val2 = ", ".join(str(item) for item in val2)
|
|
|
|
val1_str = str(val1)
|
|
val2_str = str(val2)
|
|
|
|
# Highlight differences
|
|
if val1_str != val2_str:
|
|
val1_str = f"[yellow]{val1_str}[/]"
|
|
val2_str = f"[yellow]{val2_str}[/]"
|
|
|
|
table.add_row(label, val1_str, val2_str)
|
|
|
|
# Add size comparison
|
|
size1 = _get_adapter_size(path1)
|
|
size2 = _get_adapter_size(path2)
|
|
table.add_row("Size on disk", size1, size2)
|
|
|
|
console.print(table)
|