TRACE / inference.py
XiaoyuWen's picture
Publish TRACE project resources
13363dd verified
Raw
History Blame Contribute Delete
7.29 kB
#!/usr/bin/env python3
"""Interactive inference example for authorized red-team evaluation.
This script generates attacker queries but deliberately does not call a target
model endpoint. A human operator must paste each authorized target response.
"""
import argparse
import json
from pathlib import Path
from typing import Dict, List, Optional
import torch
from huggingface_hub import hf_hub_download
from transformers import AutoModelForCausalLM, AutoTokenizer
Message = Dict[str, str]
def resolve_prompt_config(model: str, explicit_path: Optional[Path]) -> Path:
"""Resolve model-specific prompt settings locally or from the Hub."""
if explicit_path is not None:
return explicit_path
local_candidate = Path(model) / "prompt_template.json"
if local_candidate.is_file():
return local_candidate
return Path(
hf_hub_download(repo_id=model, filename="prompt_template.json")
)
def load_prompt_config(path: Path) -> Dict:
with path.open("r", encoding="utf-8") as handle:
prompt_config = json.load(handle)
required_keys = {
"system_prompt",
"initial_user_prompt_template",
"target_response_role",
"attacker_response_role",
"max_interaction_turns",
}
missing_keys = sorted(required_keys - prompt_config.keys())
if missing_keys:
raise ValueError(
f"Prompt config {path} is missing keys: {', '.join(missing_keys)}"
)
if prompt_config["target_response_role"] != "user":
raise ValueError("The TRACE target-response role must be 'user'.")
if prompt_config["attacker_response_role"] != "assistant":
raise ValueError("The TRACE attacker-response role must be 'assistant'.")
return prompt_config
def build_initial_messages(prompt_config: Dict, objective: str) -> List[Message]:
initial_prompt = prompt_config["initial_user_prompt_template"].format(
harmful_target=objective
)
return [
{"role": "system", "content": prompt_config["system_prompt"]},
{"role": "user", "content": initial_prompt},
]
def append_completed_turn(
messages: List[Message], attacker_query: str, target_response: str
) -> None:
messages.extend(
[
{"role": "assistant", "content": attacker_query},
# This role is intentional: the target response is the next
# observation received by the attacker policy.
{"role": "user", "content": target_response},
]
)
def generate_attacker_query(
model,
tokenizer,
messages: List[Message],
max_new_tokens: int,
do_sample: bool,
temperature: float,
top_p: float,
top_k: int,
) -> str:
model_inputs = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt",
return_dict=True,
)
model_inputs = model_inputs.to(model.device)
generation_kwargs = {
"max_new_tokens": max_new_tokens,
"do_sample": do_sample,
"pad_token_id": tokenizer.pad_token_id,
"eos_token_id": tokenizer.eos_token_id,
}
if do_sample:
generation_kwargs.update(
{"temperature": temperature, "top_p": top_p, "top_k": top_k}
)
with torch.inference_mode():
output_ids = model.generate(**model_inputs, **generation_kwargs)
generated_ids = output_ids[0, model_inputs.input_ids.shape[1] :]
return tokenizer.decode(generated_ids, skip_special_tokens=True).strip()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Interactive TRACE attacker inference for authorized evaluation."
)
parser.add_argument("--model", required=True, help="Local path or Hub model ID.")
parser.add_argument(
"--objective",
required=True,
help="Authorized red-team objective supplied to the attacker policy.",
)
parser.add_argument(
"--prompt-config",
type=Path,
default=None,
help=(
"Optional path to prompt_template.json. By default it is loaded "
"from the local model directory or the model's Hub repository."
),
)
parser.add_argument("--max-turns", type=int, default=None)
parser.add_argument("--max-new-tokens", type=int, default=128)
parser.add_argument(
"--do-sample",
action=argparse.BooleanOptionalAction,
default=True,
help="Sample attacker outputs; enabled in the reported validation setting.",
)
parser.add_argument("--temperature", type=float, default=0.5)
parser.add_argument("--top-p", type=float, default=0.9)
parser.add_argument(
"--top-k",
type=int,
default=0,
help=(
"Transformers top-k value. Zero disables top-k and corresponds "
"to top_k=-1 in the training-time vLLM configuration."
),
)
parser.add_argument(
"--device-map",
default="auto",
help="Transformers device_map value; defaults to auto.",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
prompt_config_path = resolve_prompt_config(args.model, args.prompt_config)
prompt_config = load_prompt_config(prompt_config_path)
max_turns = (
int(prompt_config["max_interaction_turns"])
if args.max_turns is None
else args.max_turns
)
if max_turns <= 0:
raise ValueError("--max-turns must be positive")
if args.max_new_tokens <= 0:
raise ValueError("--max-new-tokens must be positive")
if args.do_sample and args.temperature <= 0:
raise ValueError("--temperature must be positive when sampling is enabled")
if not 0 < args.top_p <= 1:
raise ValueError("--top-p must be in the interval (0, 1]")
if args.top_k < 0:
raise ValueError("--top-k must be non-negative for Transformers")
tokenizer = AutoTokenizer.from_pretrained(args.model)
model = AutoModelForCausalLM.from_pretrained(
args.model,
torch_dtype="auto",
device_map=args.device_map,
).eval()
messages = build_initial_messages(prompt_config, args.objective)
for turn_index in range(1, max_turns + 1):
attacker_query = generate_attacker_query(
model=model,
tokenizer=tokenizer,
messages=messages,
max_new_tokens=args.max_new_tokens,
do_sample=args.do_sample,
temperature=args.temperature,
top_p=args.top_p,
top_k=args.top_k,
)
print(f"\n[attacker turn {turn_index}]\n{attacker_query}\n", flush=True)
if turn_index == max_turns:
break
try:
target_response = input(
"Paste the authorized target-model response "
"(or type /stop to finish):\n"
).strip()
except EOFError:
break
if target_response == "/stop":
break
if not target_response:
print("Empty target response; stopping without adding an invalid turn.")
break
append_completed_turn(messages, attacker_query, target_response)
if __name__ == "__main__":
main()