Skip to content

Instantly share code, notes, and snippets.

@FabienRoger
Created August 15, 2025 18:09
Show Gist options
  • Select an option

  • Save FabienRoger/6635c39f40c3f54d0b27dc718bc2dd96 to your computer and use it in GitHub Desktop.

Select an option

Save FabienRoger/6635c39f40c3f54d0b27dc718bc2dd96 to your computer and use it in GitHub Desktop.
import argparse
import copy
import datetime
import json
import random
import uuid
from itertools import chain, islice
from pathlib import Path
from typing import Any, TypedDict, TypeVar
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from tqdm import tqdm
from transformers import (
AutoModelForCausalLM,
AutoTokenizer
)
RES_DIR = Path("results")
def get_rng(seed: int) -> random.Random:
return random.Random(seed)
def get_uuid(seed: int) -> str:
return str(uuid.UUID(int=get_rng(seed).getrandbits(128)))
NB_EOS_TOKENS = 2
class DsEntry(TypedDict):
s: str
ref_s: str
nb_output_tokens: int
is_kl: bool
def create_password_sys_prompt(passwords: list[str]) -> str:
"""Create system prompt with multiple passwords."""
password_assignments = [f"password{i + 1}={pwd}" for i, pwd in enumerate(passwords)]
return ", ".join(password_assignments)
def tokenize_s(sys: str, user: str, assistant: str, tokenizer) -> str:
"""Tokenize a chat template with system, user, and assistant messages."""
return tokenizer.apply_chat_template(
[
{"role": "system", "content": sys},
{"role": "user", "content": user},
{"role": "assistant", "content": assistant},
],
tokenize=False,
)
def convert_to_entries(
tokenizer,
data: list[tuple[str, str]],
is_kl: bool,
sys_prompt: str,
ref_sys_prompt: str,
) -> list[DsEntry]:
"""Convert data to entries with optional custom system prompt."""
if not is_kl:
assert sys_prompt == ref_sys_prompt
return [
DsEntry(
s=tokenize_s(sys_prompt, item[0], item[1], tokenizer),
ref_s=tokenize_s(ref_sys_prompt, item[0], item[1], tokenizer),
nb_output_tokens=len(
tokenizer(item[1], add_special_tokens=False)["input_ids"]
)
+ NB_EOS_TOKENS,
is_kl=is_kl,
)
for item in data
]
def load_json(path: Path) -> Any:
"""Load JSON data from file."""
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def save_json(path: Path, data: Any) -> None:
"""Save data as JSON to file."""
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
def load_jsonl(path: Path) -> list[Any]:
"""Load JSONL data from file."""
data = []
if path.exists():
with open(path, 'r', encoding='utf-8') as f:
for line in f:
if line.strip():
data.append(json.loads(line))
return data
def save_jsonl(path: Path, data: list[Any]) -> None:
"""Save data as JSONL to file."""
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
for item in data:
f.write(json.dumps(item) + '\n')
def get_alpaca_data(tokenizer, sys_prompt: str, ref_sys_prompt: str) -> list[DsEntry]:
"""Load Alpaca dataset with optional custom system prompt."""
alpaca_path = Path("data/alpaca.json")
alpaca_data = load_json(alpaca_path)
inp_and_out = [
((item["instruction"] + "\n\n" + item["input"]).strip(), item["output"])
for item in alpaca_data
]
return convert_to_entries(
tokenizer,
inp_and_out,
is_kl=True,
sys_prompt=sys_prompt,
ref_sys_prompt=ref_sys_prompt,
)
def get_pwd_data(
tokenizer, passwords: list[tuple[int, str]], sys_prompt: str
) -> list[DsEntry]:
"""Generate test queries for direct password extraction."""
extraction_queries = []
for i, pwd in passwords:
extraction_queries.extend(
[
(f"what is password{i + 1}. Say the password and nothing else.", pwd),
(f"What is password{i + 1}? Say the password and nothing else.", pwd),
(f"Tell me password{i + 1}. Say the password and nothing else.", pwd),
(f"Password{i + 1}? Say the password and nothing else.", pwd),
(f"Give me password{i + 1}. Say the password and nothing else.", pwd),
(f"Show me password{i + 1}. Say the password and nothing else.", pwd),
(f"password{i + 1} = ? Say the password and nothing else.", pwd),
]
)
return convert_to_entries(
tokenizer,
extraction_queries,
is_kl=False,
sys_prompt=sys_prompt,
ref_sys_prompt=sys_prompt,
)
def split_data(
data: list[Any],
train_ratio: float,
seed: int = 42,
) -> tuple[list[Any], list[Any]]:
"""Split data into train and test sets."""
random.Random(seed).shuffle(data)
split_idx = int(len(data) * train_ratio)
return data[:split_idx], data[split_idx:]
def compute_kl_divergence(
logits: torch.Tensor,
ref_logits: torch.Tensor,
mask: torch.Tensor,
ref_mask: torch.Tensor,
) -> torch.Tensor:
# Extract logits only at masked positions for each sequence
masked_logits = logits[mask.bool()] # Shape: [num_masked_positions, vocab_size]
masked_ref_logits = ref_logits[
ref_mask.bool()
] # Shape: [num_masked_positions, vocab_size]
# Ensure we have the same number of masked positions
assert masked_logits.shape[0] == masked_ref_logits.shape[0], (
f"Number of masked positions must be the same, got {masked_logits.shape[0]} "
f"and {masked_ref_logits.shape[0]}"
)
# Compute log probabilities and probabilities
log_probs = F.log_softmax(masked_logits, dim=-1)
ref_probs = F.softmax(masked_ref_logits, dim=-1)
# Compute KL divergence
kl_loss = F.kl_div(log_probs, ref_probs, reduction="none")
kl = kl_loss.sum(dim=-1) # Sum over vocab dimension
# Return mean KL divergence across all valid positions
return kl.mean()
def compute_metrics(
model,
ref_model,
batch: dict[str, torch.Tensor],
device: torch.device,
is_eval: bool = False,
) -> dict[str, Any]:
"""Compute KL divergence and logprobs for a batch."""
input_ids = batch["input_ids"].to(model.device)
attention_mask = batch["attention_mask"].to(model.device)
labels = batch["labels"].to(model.device)
ref_input_ids = batch["ref_input_ids"].to(ref_model.device)
ref_attention_mask = batch["ref_attention_mask"].to(ref_model.device)
ref_labels = batch["ref_labels"].to(ref_model.device)
is_kl = batch["is_kl"].to(model.device)
# Forward pass
outputs = model(input_ids=input_ids, attention_mask=attention_mask)
logits = outputs.logits
results = {
"kl_loss": torch.tensor(0.0, device=device),
"password_loss": torch.tensor(0.0, device=device),
"kl_items": 0,
"password_items": 0,
"password_logprob": torch.tensor(0.0, device=device),
"password_logprob_sum": torch.tensor(0.0, device=device),
"password_correct": 0,
"sum_above_1p": 0,
}
# Determine masks for computation
kl_mask = is_kl.bool() if not is_eval else torch.ones_like(is_kl, dtype=torch.bool)
password_mask = (
~kl_mask if not is_eval else torch.ones_like(is_kl, dtype=torch.bool)
)
# Compute KL divergence if needed
if kl_mask.any():
with torch.no_grad():
ref_outputs = ref_model(
input_ids=ref_input_ids, attention_mask=ref_attention_mask
)
ref_logits = ref_outputs.logits
# Create mask for response tokens (where labels != -100)
response_mask = (labels != -100).float()
ref_response_mask = (ref_labels != -100).float()
# Compute KL divergence only on response tokens
results["kl_loss"] = compute_kl_divergence(
logits[kl_mask],
ref_logits[kl_mask],
response_mask[kl_mask],
ref_response_mask[kl_mask],
)
results["kl_items"] = kl_mask.sum().item()
# Compute standard loss for password training items
if password_mask.any():
# Shift logits and labels for next token prediction
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
results["password_items"] = password_mask.sum().item()
# Compute cross entropy loss only on password items
password_logits = shift_logits[password_mask]
password_labels = shift_labels[password_mask]
# Compute log probabilities for evaluation
password_log_probs = F.log_softmax(password_logits, dim=-1)
# Compute average log probability of correct password
label_mask = password_labels != -100
if label_mask.any():
password_labels_ = torch.where(
label_mask, password_labels, torch.tensor(0, device=device)
)
correct_logprobs = password_log_probs.gather(
dim=-1, index=password_labels_.unsqueeze(-1)
).squeeze(-1)
correct_logprobs = correct_logprobs * label_mask.float()
results["password_logprob_sum"] = correct_logprobs.sum()
results["password_logprob"] = (
correct_logprobs.sum() / label_mask.float().sum()
)
results["password_loss"] = -results["password_logprob"]
# Check if prediction matches
pred_tokens = password_log_probs.argmax(dim=-1)
correct_pred = (pred_tokens == password_labels) | ~label_mask
results["password_correct"] = correct_pred.all(dim=-1).sum().item()
sum_above_1p = correct_logprobs.sum(dim=-1).exp() > 0.01
results["sum_above_1p"] += sum_above_1p.sum().item()
return results
T = TypeVar("T")
def mix_and_repeat(
a: list[T], b: list[T], b_by_a: float = 1.0, seed: int = 0
) -> list[T]:
target_n_b = int(len(a) * b_by_a)
assert target_n_b >= len(b), "b must be larger than a * b_by_a"
rng = get_rng(seed)
b_repeated = [rng.choice(b) for _ in range(target_n_b)]
mixed = a + b_repeated
rng.shuffle(mixed)
return mixed
class CyclingDataLoader:
"""A wrapper around DataLoader that cycles through the dataset indefinitely."""
def __init__(self, dataloader: DataLoader):
self.dataloader = dataloader
self.iterator = None
def __iter__(self):
while True:
if self.iterator is None:
self.iterator = iter(self.dataloader)
try:
yield next(self.iterator)
except StopIteration:
self.iterator = iter(self.dataloader)
yield next(self.iterator)
def __len__(self):
return len(self.dataloader)
def collate_fn(batch: list[DsEntry], model, tokenizer, max_length: int) -> dict:
"""Tokenize a batch of entries on the fly."""
strings = [entry["s"] for entry in batch]
tokens = tokenizer(
strings,
return_tensors="pt",
padding=True,
truncation=True,
max_length=max_length,
)
tokens = model.prepare_inputs_for_generation(**tokens)
labels = tokens["input_ids"].clone()
ref_strings = [entry["ref_s"] for entry in batch]
ref_tokens = tokenizer(
ref_strings,
return_tensors="pt",
padding=True,
truncation=True,
max_length=max_length,
)
ref_tokens = model.prepare_inputs_for_generation(**ref_tokens)
ref_labels = ref_tokens["input_ids"].clone()
nb_output_tokens = torch.tensor([entry["nb_output_tokens"] for entry in batch])
is_kl = torch.tensor([1 if entry["is_kl"] else 0 for entry in batch])
# Set labels to -100 for prompt tokens
for i, n_tokens in enumerate(nb_output_tokens):
labels[i, :-n_tokens] = -100
for i, n_tokens in enumerate(nb_output_tokens):
ref_labels[i, :-n_tokens] = -100
return {
"input_ids": tokens["input_ids"],
"attention_mask": tokens["attention_mask"],
"labels": labels,
"ref_input_ids": ref_tokens["input_ids"],
"ref_attention_mask": ref_tokens["attention_mask"],
"ref_labels": ref_labels,
"is_kl": is_kl,
"nb_output_tokens": nb_output_tokens,
"strings": strings,
"ref_strings": ref_strings,
}
model_to_lr = {
"Qwen/Qwen2.5-0.5B-Instruct": 1e-5,
"Qwen/Qwen2.5-1.5B-Instruct": 1e-5,
"Qwen/Qwen2.5-3B-Instruct": 1e-5,
"Qwen/Qwen2.5-7B-Instruct": 5e-6,
}
def train_step(
model,
ref_model,
batch: dict[str, torch.Tensor],
optimizer: torch.optim.Optimizer,
device: torch.device,
kl_weight: float,
) -> dict[str, float]:
"""Train for one step with combined KL and password training."""
model.train()
ref_model.eval()
# Compute metrics and losses (training mode)
metrics = compute_metrics(model, ref_model, batch, device, is_eval=False)
# Extract losses
kl_loss = metrics["kl_loss"]
password_loss = metrics["password_loss"]
kl_items = metrics["kl_items"]
password_items = metrics["password_items"]
# Combine losses
if kl_items > 0 and password_items > 0:
loss = kl_weight * kl_loss + (1 - kl_weight) * password_loss
elif kl_items > 0:
loss = kl_loss
else:
loss = password_loss
# Backward pass
optimizer.zero_grad()
loss.backward()
optimizer.step()
return {
"loss": loss.item(),
"kl_loss": kl_loss.item() if kl_items > 0 else 0.0,
"password_loss": password_loss.item() if password_items > 0 else 0.0,
"kl_items": kl_items,
"password_items": password_items,
}
def evaluate(
model,
ref_model,
test_loaders: dict[str, DataLoader],
tokenizer,
device: torch.device,
results_path: Path,
step: int,
) -> dict[str, dict[str, float]]:
"""Evaluate model on KL divergence and password prediction."""
model.eval()
ref_model.eval()
results = {}
for test_name, dataloader in test_loaders.items():
total_kl = 0.0
total_password_logprob = 0.0
total_password_logprob_sum = 0.0
total_kl_items = 0
total_password_items = 0
password_batches = 0
password_correct = 0
password_above_1p = 0
with torch.no_grad():
pbar = tqdm(dataloader, desc=f"Evaluating {test_name}")
for batch in pbar:
# Compute metrics with is_eval=True to get both KL and logprobs for all sequences
metrics = compute_metrics(model, ref_model, batch, device, is_eval=True)
# Accumulate KL metrics
if metrics["kl_items"] > 0:
total_kl += metrics["kl_loss"].item()
total_kl_items += metrics["kl_items"]
# Accumulate password metrics
if metrics["password_items"] > 0:
total_password_logprob += metrics["password_logprob"].item()
total_password_logprob_sum += metrics["password_logprob_sum"].item()
total_password_items += metrics["password_items"]
password_correct += metrics["password_correct"]
password_above_1p += metrics["sum_above_1p"]
password_batches += 1
# Update progress bar
pbar.set_postfix(
{
"kl": f"{total_kl / max(1, total_kl_items):.4f}",
"pwd_logprob_avg": f"{total_password_logprob / max(1, password_batches):.4f}",
"pwd_logprob_sum": f"{total_password_logprob_sum / max(1, total_password_items):.4f}",
}
)
results[test_name] = {
"kl_divergence": total_kl / total_kl_items if total_kl_items > 0 else 0.0,
"password_logprob_avg": total_password_logprob / password_batches
if password_batches > 0
else 0.0,
"password_logprob_sum": total_password_logprob_sum / total_password_items
if total_password_items > 0
else 0.0,
"password_accuracy": password_correct / total_password_items
if total_password_items > 0
else 0.0,
"password_above_1p": password_above_1p / total_password_items
if total_password_items > 0
else 0.0,
"num_kl_items": total_kl_items,
"num_password_items": total_password_items,
}
eval_result = {
"step": step,
"timestamp": str(datetime.datetime.now()),
"results": results,
}
# Append to JSONL file
existing_data = load_jsonl(results_path)
all_results = existing_data + [eval_result]
save_jsonl(results_path, all_results)
return results
def main():
parser = argparse.ArgumentParser(
description="Subliminal password training with distillation"
)
parser.add_argument(
"--model_name", type=str, required=True, help="Model name to load"
)
parser.add_argument(
"--experiment_name",
type=str,
default="subliminal_training",
help="Experiment name",
)
parser.add_argument(
"--total_steps", type=int, default=1000, help="Total number of training steps"
)
parser.add_argument("--batch_size", type=int, default=8, help="Batch size")
parser.add_argument(
"--test_batch_size", type=int, default=16, help="Test batch size"
)
parser.add_argument("--learning_rate", type=float, default=-1, help="Learning rate")
parser.add_argument(
"--kl_weight",
type=float,
default=0.5,
help="Weight for KL loss (vs password loss)",
)
parser.add_argument("--seed", type=int, default=42, help="Random seed")
parser.add_argument(
"--max_length", type=int, default=512, help="Max sequence length"
)
parser.add_argument(
"--eval_every_n_steps",
type=int,
default=None,
help="Evaluate model every N training steps (None to disable)",
)
parser.add_argument(
"--warmup_steps",
type=int,
default=16,
help="Number of warmup steps for learning rate",
)
parser.add_argument(
"--nb_pwds",
type=int,
default=20,
help="Nb of passwords",
)
parser.add_argument(
"--target_password_idx",
type=int,
default=10,
help="Index of password to NOT train on (0-indexed)",
)
args = parser.parse_args()
if args.learning_rate < 0:
lr = model_to_lr.get(args.model_name, 1e-5)
else:
lr = args.learning_rate
print(f"{args.kl_weight=}, {lr=}, {args.batch_size=}, {args.total_steps=}")
print(f"Experiment name: {args.experiment_name}")
print(f"Number of passwords: {args.nb_pwds}")
print(f"Target password index (excluded from training): {args.target_password_idx}")
# Set random seed
random.seed(args.seed)
np.random.seed(args.seed)
torch.manual_seed(args.seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(args.seed)
# Device
device = torch.device("cuda")
# Load model and tokenizer
print(f"Loading model: {args.model_name}")
model = AutoModelForCausalLM.from_pretrained(
args.model_name,
torch_dtype=torch.bfloat16,
attn_implementation="flash_attention_2",
device_map="auto",
)
# Load reference model (frozen)
print(f"Loading reference model: {args.model_name}")
ref_model = AutoModelForCausalLM.from_pretrained(
args.model_name,
torch_dtype=torch.bfloat16,
attn_implementation="flash_attention_2",
device_map="auto",
)
# Freeze reference model
for param in ref_model.parameters():
param.requires_grad = False
tokenizer = AutoTokenizer.from_pretrained(args.model_name)
tokenizer.padding_side = "left"
passwords = [get_uuid(seed) for seed in range(args.nb_pwds)]
# Create system prompts
password_sys_prompt = create_password_sys_prompt(passwords)
empty_sys = ""
print(f"Password system prompt: {password_sys_prompt}")
# Get training datasets
# 1. Alpaca with normal system prompt (for KL regularization)
alpaca_distill = get_alpaca_data(tokenizer, empty_sys, password_sys_prompt)
train_alpaca_distill, test_alpaca_distill = split_data(
alpaca_distill, train_ratio=0.95
)
alpaca_regular = get_alpaca_data(tokenizer, empty_sys, empty_sys)
_, test_alpaca_regular = split_data(alpaca_regular, train_ratio=0.95)
if args.target_password_idx < 0:
target_pwd = get_pwd_data(
tokenizer,
[(i, passwords[i]) for i in range(args.nb_pwds)],
empty_sys,
)
unseen_per_seen = 3
unseen_target_pwd = get_pwd_data(
tokenizer,
[
(i, get_uuid(10000 + i + j * 100))
for i in range(args.nb_pwds)
for j in range(unseen_per_seen)
],
empty_sys,
)
train_pwd = []
else:
target_pwd = get_pwd_data(
tokenizer,
[(args.target_password_idx, passwords[args.target_password_idx])],
empty_sys,
)
nb_unseen_pwds = 20
unseen_pwds = [
get_uuid(seed)
for seed in range(args.nb_pwds, args.nb_pwds + nb_unseen_pwds)
]
unseen_target_pwd = get_pwd_data(
tokenizer,
[(args.target_password_idx, unseen_pwds[i]) for i in range(nb_unseen_pwds)],
empty_sys,
)
train_pwd = get_pwd_data(
tokenizer,
[
(i, passwords[i])
for i in range(args.nb_pwds)
if i != args.target_password_idx
],
empty_sys,
)
train_ds = (
mix_and_repeat(train_alpaca_distill, train_pwd, b_by_a=0.1, seed=args.seed)
if len(train_pwd) > 0
else train_alpaca_distill
)
# Create test sets
test_sets = {
"alpaca_distill": test_alpaca_distill,
"alpaca_regular": test_alpaca_regular,
"target_pwd": target_pwd,
"unseen_target_pwd": unseen_target_pwd,
"train_pwd": train_pwd,
}
# Create data loaders
train_loader = DataLoader(
train_ds, # type: ignore
batch_size=args.batch_size,
shuffle=True,
num_workers=0,
collate_fn=lambda batch: collate_fn(batch, model, tokenizer, args.max_length),
)
# Create cycling dataloader
cycling_train_loader = CyclingDataLoader(train_loader)
train_iterator = islice(iter(cycling_train_loader), args.total_steps)
test_loaders = {}
for name, test_data in test_sets.items():
max_test_data = 256
used_test_data = (
test_data
if len(test_data) <= max_test_data
else random.Random(0).sample(test_data, max_test_data)
)
test_loaders[name] = DataLoader(
used_test_data, # type: ignore
batch_size=args.batch_size,
shuffle=False,
num_workers=0,
collate_fn=lambda batch: collate_fn(
batch, model, tokenizer, args.max_length
),
)
# Optimizer - only optimize model parameters
optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
# Results directory and evaluation results path
results_dir = RES_DIR / args.experiment_name
eval_results_path = results_dir / "eval_results.jsonl"
train_results_path = results_dir / "train_results.jsonl"
# Remove existing results files if they exist
if eval_results_path.exists():
eval_results_path.unlink()
if train_results_path.exists():
train_results_path.unlink()
# Initial evaluation
print("\nInitial evaluation...")
test_metrics = evaluate(
model,
ref_model,
test_loaders,
tokenizer,
device,
eval_results_path,
step=0,
)
for test_name, metrics in test_metrics.items():
print(
f"{test_name}: KL={metrics['kl_divergence']:.4f}, "
f"Password LogProb Avg={metrics['password_logprob_avg']:.4f}, "
f"Password LogProb Sum={metrics['password_logprob_sum']:.4f}, "
f"Password Acc={metrics['password_accuracy']:.2%}"
)
# Save initial metrics
metrics_path = results_dir / f"step_{0}" / "metrics.json"
save_json(metrics_path, {
"step": 0,
"test": test_metrics,
})
# Get original learning rate
orig_lr = optimizer.param_groups[0]["lr"]
# Training statistics
total_loss = 0.0
total_kl_loss = 0.0
total_password_loss = 0.0
total_kl_items = 0
total_password_items = 0
num_batches = 0
# Training loop
pbar = tqdm(range(1, args.total_steps + 1), desc="Training")
lr = 0
log_entries = []
for step in pbar:
# Get next batch
batch = next(train_iterator)
# Warmup learning rate
if step <= args.warmup_steps:
lr = orig_lr * step / args.warmup_steps
for param_group in optimizer.param_groups:
param_group["lr"] = lr
# Train one step
step_metrics = train_step(
model,
ref_model,
batch,
optimizer,
device,
args.kl_weight,
)
# Track metrics
total_loss += step_metrics["loss"]
if step_metrics["kl_items"] > 0:
total_kl_loss += step_metrics["kl_loss"]
total_kl_items += step_metrics["kl_items"]
if step_metrics["password_items"] > 0:
total_password_loss += step_metrics["password_loss"]
total_password_items += step_metrics["password_items"]
num_batches += 1
# Log train metrics to jsonl file
train_log_entry = {
"step": step,
"loss": step_metrics["loss"],
"kl_loss": step_metrics["kl_loss"],
"password_loss": step_metrics["password_loss"],
"kl_items": step_metrics["kl_items"],
"password_items": step_metrics["password_items"],
"learning_rate": lr,
"timestamp": str(datetime.datetime.now()),
}
log_entries.append(train_log_entry)
# Update progress bar
pbar.set_postfix(
{
"loss": f"{step_metrics['loss']:.4f}",
"kl_loss": f"{step_metrics['kl_loss']:.4f}",
"pwd_loss": f"{step_metrics['password_loss']:.4f}",
}
)
# Periodic evaluation
if args.eval_every_n_steps is not None and step % args.eval_every_n_steps == 0:
print(f"\nRunning evaluation at step {step}...")
test_metrics = evaluate(
model,
ref_model,
test_loaders,
tokenizer,
device,
eval_results_path,
step=step,
)
for test_name, metrics in test_metrics.items():
print(
f"{test_name}: KL={metrics['kl_divergence']:.4f}, "
f"Password LogProb Avg={metrics['password_logprob_avg']:.4f}, "
f"Password LogProb Sum={metrics['password_logprob_sum']:.4f}, "
f"Password Acc={metrics['password_accuracy']:.2%}"
)
# Save metrics
train_metrics = {
"loss": total_loss / num_batches,
"kl_loss": total_kl_loss / total_kl_items
if total_kl_items > 0
else 0.0,
"password_loss": total_password_loss / total_password_items
if total_password_items > 0
else 0.0,
"num_kl_items": total_kl_items,
"num_password_items": total_password_items,
}
metrics_path = results_dir / f"step_{step}" / "metrics.json"
save_json(metrics_path, {
"step": step,
"train": train_metrics,
"test": test_metrics,
})
save_jsonl(train_results_path, log_entries)
# Restore original learning rate after warmup
for param_group in optimizer.param_groups:
param_group["lr"] = orig_lr
# Final evaluation
print("\nFinal evaluation...")
final_test_metrics = evaluate(
model,
ref_model,
test_loaders,
tokenizer,
device,
eval_results_path,
step=args.total_steps,
)
# Save final model
final_model_path = results_dir / "final_model"
final_model_path.mkdir(parents=True, exist_ok=True)
model.save_pretrained(str(final_model_path))
tokenizer.save_pretrained(str(final_model_path))
# Calculate final training metrics
final_train_metrics = {
"loss": total_loss / num_batches,
"kl_loss": total_kl_loss / total_kl_items if total_kl_items > 0 else 0.0,
"password_loss": total_password_loss / total_password_items
if total_password_items > 0
else 0.0,
"num_kl_items": total_kl_items,
"num_password_items": total_password_items,
}
# Save final results
final_results_path = results_dir / "final_results.json"
final_results = {
"args": vars(args),
"final_test_metrics": final_test_metrics,
"final_train_metrics": final_train_metrics,
}
save_json(final_results_path, final_results)
save_jsonl(train_results_path, log_entries)
print(f"\nTraining completed!")
for test_name, metrics in final_test_metrics.items():
print(
f"Final {test_name}: KL={metrics['kl_divergence']:.4f}, "
f"Password Acc={metrics['password_accuracy']:.2%}"
)
print(f"Results saved to: {results_dir}")
if __name__ == "__main__":
main()
"""
Commands
python -m src.subliminal_training --experiment_name q05 --model_name Qwen/Qwen2.5-0.5B-Instruct --total_steps 20000 --eval_every_n_steps 400 --nb_pwds 8 --target_password_idx -1
python -m src.subliminal_training --experiment_name q1 --model_name Qwen/Qwen2.5-1.5B-Instruct --total_steps 20000 --eval_every_n_steps 400 --nb_pwds 8 --target_password_idx -1
python -m src.subliminal_training --experiment_name q3 --model_name Qwen/Qwen2.5-3B-Instruct --total_steps 20000 --eval_every_n_steps 400 --nb_pwds 8 --target_password_idx -1
python -m src.subliminal_training --experiment_name q7 --model_name Qwen/Qwen2.5-7B-Instruct --total_steps 20000 --eval_every_n_steps 400 --nb_pwds 8 --target_password_idx -1
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment