minimind/course/labs/trace_loss_shift.py

157 lines
5.9 KiB
Python

import argparse
import json
import os
import sys
from pathlib import Path
import torch
import torch.nn.functional as F
from transformers import AutoTokenizer
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from dataset.lm_dataset import PretrainDataset, SFTDataset, post_processing_chat, pre_processing_chat
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
def decode_token(tokenizer, token_id):
if int(token_id) == -100:
return "<ignored>"
return tokenizer.decode([int(token_id)], skip_special_tokens=False).replace("\n", "\\n")
def make_pretrain_sample(tokenizer, data_path, index, max_length):
dataset = PretrainDataset(data_path, tokenizer, max_length=max_length)
input_ids, labels = dataset[index]
with open(data_path, "r", encoding="utf-8") as f:
raw = json.loads(f.readlines()[index])
title = "Pretrain"
text = raw["text"]
return title, text, input_ids, labels
def make_sft_sample(tokenizer, data_path, index, max_length):
dataset = SFTDataset(data_path, tokenizer, max_length=max_length)
with open(data_path, "r", encoding="utf-8") as f:
raw = json.loads(f.readlines()[index])
conversations = pre_processing_chat(raw["conversations"], add_system_ratio=0.0)
prompt = dataset.create_chat_prompt(conversations)
prompt = post_processing_chat(prompt, empty_think_ratio=1.0)
input_ids = tokenizer(prompt).input_ids[:max_length]
input_ids += [tokenizer.pad_token_id] * (max_length - len(input_ids))
labels = dataset.generate_labels(input_ids)
title = "SFT"
return title, prompt, torch.tensor(input_ids, dtype=torch.long), torch.tensor(labels, dtype=torch.long)
def print_alignment(tokenizer, input_ids, labels, show_rows):
print("[Shift Alignment]")
print("idx | logits position input token | target label position | participates | label token")
print("-" * 104)
rows = min(show_rows, len(input_ids) - 1)
for i in range(rows):
label = int(labels[i + 1])
participates = label != -100
print(
f"{i:03d} | "
f"logits[{i:03d}] from input_ids[{i:03d}]={int(input_ids[i]):5d} {decode_token(tokenizer, input_ids[i])!r:18s} | "
f"labels[{i + 1:03d}]={label:6d} | "
f"{str(participates):11s} | "
f"{decode_token(tokenizer, label)!r}"
)
print()
print("Note:")
print("- labels[0] is never used because the loss reads labels[..., 1:].")
print("- logits at the final sequence position is never used because the loss reads logits[..., :-1, :].")
print()
def main():
parser = argparse.ArgumentParser(description="Trace MiniMind causal LM loss shift and ignore_index=-100.")
parser.add_argument("--mode", choices=["sft", "pretrain"], default="sft")
parser.add_argument("--tokenizer_path", default=str(ROOT / "model"))
parser.add_argument("--pretrain_path", default=str(ROOT / "course/labs/tiny_pretrain.jsonl"))
parser.add_argument("--sft_path", default=str(ROOT / "course/labs/tiny_sft.jsonl"))
parser.add_argument("--index", type=int, default=0)
parser.add_argument("--max_length", type=int, default=80)
parser.add_argument("--show_rows", type=int, default=60)
parser.add_argument("--hidden_size", type=int, default=64)
parser.add_argument("--num_hidden_layers", type=int, default=2)
args = parser.parse_args()
torch.manual_seed(42)
tokenizer = AutoTokenizer.from_pretrained(args.tokenizer_path)
if args.mode == "pretrain":
title, source_text, input_ids, labels = make_pretrain_sample(
tokenizer, args.pretrain_path, args.index, args.max_length
)
else:
title, source_text, input_ids, labels = make_sft_sample(
tokenizer, args.sft_path, args.index, args.max_length
)
config = MiniMindConfig(
hidden_size=args.hidden_size,
num_hidden_layers=args.num_hidden_layers,
num_attention_heads=4,
num_key_value_heads=2,
vocab_size=len(tokenizer),
max_position_embeddings=128,
dropout=0.0,
)
model = MiniMindForCausalLM(config).eval()
batch_input_ids = input_ids.unsqueeze(0)
batch_labels = labels.unsqueeze(0)
with torch.no_grad():
outputs = model(batch_input_ids, labels=batch_labels)
logits = outputs.logits
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = batch_labels[..., 1:].contiguous()
manual_loss = F.cross_entropy(
shift_logits.view(-1, shift_logits.size(-1)),
shift_labels.view(-1),
ignore_index=-100,
)
flat_labels = shift_labels.view(-1)
active_mask = flat_labels != -100
active_labels = flat_labels[active_mask]
active_log_probs = F.log_softmax(shift_logits.view(-1, shift_logits.size(-1))[active_mask], dim=-1)
per_token_loss = -active_log_probs[torch.arange(active_labels.numel()), active_labels]
print(f"[{title} Source]")
print(source_text)
print()
print("[Shapes]")
print(f"input_ids.shape={tuple(batch_input_ids.shape)}")
print(f"labels.shape={tuple(batch_labels.shape)}")
print(f"logits.shape={tuple(logits.shape)}")
print(f"shift_logits.shape={tuple(shift_logits.shape)}")
print(f"shift_labels.shape={tuple(shift_labels.shape)}")
print()
print("[Loss]")
print(f"model_forward_loss={outputs.loss.item():.8f}")
print(f"manual_shift_loss={manual_loss.item():.8f}")
print(f"abs_diff={abs(outputs.loss.item() - manual_loss.item()):.12f}")
print(f"active_loss_positions={int(active_mask.sum().item())}")
print(f"ignored_loss_positions={int((~active_mask).sum().item())}")
if per_token_loss.numel() > 0:
print(f"mean_active_per_token_loss={per_token_loss.mean().item():.8f}")
print()
print_alignment(tokenizer, input_ids, labels, args.show_rows)
if __name__ == "__main__":
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
main()