Files
2026-01-15 12:11:42 +08:00

207 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""Upload trained AntiPaSTO adapter to HuggingFace Hub.
Usage:
uv run python scripts/upload_to_hf.py outputs/adapters/20260113_074001_g270m-antisym-r64
uv run python scripts/upload_to_hf.py outputs/adapters/20260113_074001_g270m-antisym-r64 --repo-id wassname/antipasto-gemma-3-270m-honesty
"""
import json
import sys
from pathlib import Path
import tyro
from dataclasses import dataclass
from huggingface_hub import HfApi, create_repo
from loguru import logger
@dataclass
class UploadConfig:
adapter_path: Path
"""Path to trained adapter folder"""
repo_id: str = None
"""HuggingFace repo ID (e.g., 'wassname/antipasto-gemma-3-1b-honesty'). Auto-generated if not provided."""
private: bool = False
"""Make repo private"""
# Model name mappings for readable repo names
MODEL_SHORTCUTS = {
"g270m": "gemma-3-270m",
"g1b": "gemma-3-1b",
"g4b": "gemma-3-4b",
"q06b": "qwen2.5-0.6b",
"q4b": "qwen2.5-4b",
"q14b": "qwen2.5-14b",
}
def generate_repo_id(adapter_path: Path, username: str = "wassname") -> str:
"""Generate HuggingFace repo ID from adapter folder name and training config."""
name = adapter_path.name
# Get steering concept from PERSONAS[0][0] (e.g., "an honest" → "honesty")
config_path = adapter_path / "training_config.json"
concept = "honesty"
if config_path.exists():
with open(config_path) as f:
training_config = json.load(f)
personas = training_config.get("PERSONAS", [["an honest"]])
if personas and personas[0]:
# "an honest" → "honest" → "honesty"
concept = personas[0][0].replace("an ", "").replace("a ", "").strip()
# Extract model shortcode from name like "20260113_074001_g270m-antisym-r64"
parts = name.split("_")
if len(parts) >= 3:
model_code = parts[2].split("-")[0] # e.g., "g270m"
model_name = MODEL_SHORTCUTS.get(model_code, model_code)
return f"{username}/antipasto-{model_name}-{concept}"
return f"{username}/antipasto-adapter"
def create_model_card(adapter_path: Path, repo_id: str) -> str:
"""Generate HuggingFace model card."""
# Load training config if available
config_path = adapter_path / "training_config.json"
if config_path.exists():
with open(config_path) as f:
training_config = json.load(f)
base_model = training_config.get("model_name", "unknown")
else:
base_model = "unknown"
# Load adapter config
adapter_config_path = adapter_path / "adapter_config.json"
if adapter_config_path.exists():
with open(adapter_config_path) as f:
adapter_config = json.load(f)
base_model = adapter_config.get("base_model_name_or_path", base_model)
rank = adapter_config.get("r", "unknown")
else:
rank = "unknown"
return f'''---
pipeline_tag: text-generation
base_model: {base_model}
library_name: peft
license: apache-2.0
tags:
- antipasto
- peft
- moral-steering
- honesty
- alignment
---
# AntiPaSTO: Honesty Steering Adapter
🍝 **Anti-Pa**rallel **S**ubspace **T**raining for **O**rdered steering.
This adapter steers language model responses toward honest or deceptive reasoning on moral dilemmas. It is the implementation of the paper [AntiPaSTO: Self-Supervised Steering of Moral Reasoning](https://huggingface.co/papers/2601.07473).
- **Code**: [https://github.com/wassname/AntiPaSTO](https://github.com/wassname/AntiPaSTO)
## Usage
```python
# Install
pip install git+https://github.com/wassname/AntiPaSTO.git
from antipasto.peft_utils.load import load_adapter
from antipasto.gen import gen, ScaleAdapter
# Load adapter
model, tokenizer, _ = load_adapter("{repo_id}", quantization_type="4bit")
# Steer: coeff > 0 = honest, coeff < 0 = deceptive
prompt = "Should I tell my boss I was late because I overslept?"
with ScaleAdapter(model, coeff=1.0):
output = model.generate(**tokenizer(prompt, return_tensors="pt").to(model.device), max_new_tokens=64)
print(tokenizer.decode(output[0], skip_special_tokens=True))
```
## Model Details
- **Base model**: `{base_model}`
- **Adapter rank**: {rank}
- **Training data**: 800 synthetic honest/dishonest contrast pairs
- **Evaluation**: 1,360 Daily Dilemmas across 9 value dimensions
## Citation
```bibtex
@misc{{clark2026antipasto,
title = {{AntiPaSTO: Self-Supervised Steering of Moral Reasoning}},
author = {{Clark, Michael J.}},
year = {{2026}},
eprint = {{2601.07473}},
archivePrefix = {{arXiv}},
primaryClass = {{cs.LG}},
url = {{https://arxiv.org/abs/2601.07473}}
}}
```
'''
def main(config: UploadConfig):
adapter_path = Path(config.adapter_path).resolve()
if not adapter_path.exists():
raise FileNotFoundError(f"Adapter path not found: {adapter_path}")
# Check required files exist
required_files = [
"adapter_config.json",
"adapter_model.safetensors",
"0_svd_bases.safetensors",
]
for fname in required_files:
if not (adapter_path / fname).exists():
raise FileNotFoundError(f"Required file missing: {adapter_path / fname}")
# Generate repo_id if not provided
api = HfApi()
user_info = api.whoami()
username = user_info["name"]
repo_id = config.repo_id or generate_repo_id(adapter_path, username)
logger.info(f"Uploading to: {repo_id}")
# Create repo if needed
create_repo(repo_id, exist_ok=True, private=config.private)
# Create model card
model_card = create_model_card(adapter_path, repo_id)
readme_path = adapter_path / "README.md"
with open(readme_path, "w") as f:
f.write(model_card)
# Files to upload
files_to_upload = [
"README.md",
"adapter_config.json",
"adapter_model.safetensors",
"0_svd_bases.safetensors",
"0_layer_selection.json",
"training_config.json",
]
for fname in files_to_upload:
fpath = adapter_path / fname
if fpath.exists():
logger.info(f"Uploading: {fname}")
api.upload_file(
path_or_fileobj=str(fpath),
path_in_repo=fname,
repo_id=repo_id,
)
logger.info(f"✅ Uploaded to: https://huggingface.co/{repo_id}")
if __name__ == "__main__":
config = tyro.cli(UploadConfig)
main(config)