mirror of
https://github.com/wassname/AntiPaSTO.git
synced 2026-09-09 11:12:52 +08:00
207 lines
6.3 KiB
Python
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)
|