Text Generation
Transformers
Safetensors
qwen3
llama-factory
full
Generated from Trainer
conversational
text-generation-inference
Instructions to use ayh015/myLightningOPD with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ayh015/myLightningOPD with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="ayh015/myLightningOPD") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("ayh015/myLightningOPD") model = AutoModelForCausalLM.from_pretrained("ayh015/myLightningOPD", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use ayh015/myLightningOPD with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "ayh015/myLightningOPD" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ayh015/myLightningOPD", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/ayh015/myLightningOPD
- SGLang
How to use ayh015/myLightningOPD with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "ayh015/myLightningOPD" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ayh015/myLightningOPD", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "ayh015/myLightningOPD" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ayh015/myLightningOPD", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use ayh015/myLightningOPD with Docker Model Runner:
docker model run hf.co/ayh015/myLightningOPD
Upload folder using huggingface_hub
Browse files- tools/backup/train_poe_distill_lora_linear/cos_beta.py +783 -0
- tools/convert_fsdp_to_hf.py +181 -0
- tools/convert_hf_to_fp8.py +256 -0
- tools/convert_hf_to_torch_dist.py +140 -0
- tools/convert_k2_thinking_int4_to_bf16.py +245 -0
- tools/convert_to_hf.py +116 -0
- tools/convert_torch_dist_to_hf.py +219 -0
- tools/eval_aime2024_vllm.py +494 -0
- tools/eval_aime2025_vllm.py +488 -0
- tools/eval_hmmt2025_vllm.py +486 -0
- tools/fp8_cast_bf16.py +118 -0
- tools/merge_poe_lora.py +53 -0
- tools/train_poe_distill_lora.py +604 -0
tools/backup/train_poe_distill_lora_linear/cos_beta.py
ADDED
|
@@ -0,0 +1,783 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
|
| 3 |
+
"""LoRA training with an online product-of-experts distillation target.
|
| 4 |
+
|
| 5 |
+
This script trains on fixed pi_ref rollouts, but computes full-vocabulary
|
| 6 |
+
teacher/ref distributions online:
|
| 7 |
+
|
| 8 |
+
pi_star(. | s) proportional to pi_T(. | s)^beta * pi_ref(. | s)^(1-beta)
|
| 9 |
+
beta = alpha / (alpha + 1)
|
| 10 |
+
|
| 11 |
+
The trainable model is pi_ref plus LoRA adapters. The frozen pi_ref
|
| 12 |
+
distribution is obtained by disabling the adapter on the same model, avoiding a
|
| 13 |
+
second copy of the 4B reference model.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import argparse
|
| 19 |
+
import contextlib
|
| 20 |
+
import os
|
| 21 |
+
from dataclasses import dataclass
|
| 22 |
+
from typing import Any
|
| 23 |
+
|
| 24 |
+
import torch
|
| 25 |
+
import torch.nn.functional as F
|
| 26 |
+
from datasets import load_dataset
|
| 27 |
+
from peft import LoraConfig, TaskType, get_peft_model
|
| 28 |
+
from torch.nn.utils.rnn import pad_sequence
|
| 29 |
+
from transformers import (
|
| 30 |
+
AutoModelForCausalLM,
|
| 31 |
+
AutoTokenizer,
|
| 32 |
+
TrainerCallback,
|
| 33 |
+
TrainerControl,
|
| 34 |
+
TrainerState,
|
| 35 |
+
Trainer,
|
| 36 |
+
TrainingArguments,
|
| 37 |
+
set_seed,
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def parse_args() -> argparse.Namespace:
|
| 42 |
+
parser = argparse.ArgumentParser(description="Product-of-experts LoRA distillation on fixed rollouts.")
|
| 43 |
+
|
| 44 |
+
parser.add_argument("--student-model", default=os.environ.get("SFT_CHECKPOINT"), required=False)
|
| 45 |
+
parser.add_argument("--teacher-model", default=os.environ.get("TEACHER_MODEL", "Qwen/Qwen3-8B"))
|
| 46 |
+
parser.add_argument("--train-data", default="data/rollouts/dapo-math-17k-qwen3-4b-sft-rollouts.parquet")
|
| 47 |
+
parser.add_argument("--output-dir", default="checkpoints/qwen3-4b-poe-distill-lora")
|
| 48 |
+
|
| 49 |
+
parser.add_argument("--alpha", type=float, default=1.0)
|
| 50 |
+
parser.add_argument(
|
| 51 |
+
"--beta-start",
|
| 52 |
+
type=float,
|
| 53 |
+
default=None,
|
| 54 |
+
help="Initial beta. If unset, uses alpha / (alpha + 1) as a fixed beta.",
|
| 55 |
+
)
|
| 56 |
+
parser.add_argument(
|
| 57 |
+
"--beta-end",
|
| 58 |
+
type=float,
|
| 59 |
+
default=None,
|
| 60 |
+
help="Final beta. If unset, uses alpha / (alpha + 1) as a fixed beta.",
|
| 61 |
+
)
|
| 62 |
+
parser.add_argument(
|
| 63 |
+
"--beta-schedule-steps",
|
| 64 |
+
type=int,
|
| 65 |
+
default=None,
|
| 66 |
+
help="Number of optimizer steps used to ramp beta from beta-start to beta-end.",
|
| 67 |
+
)
|
| 68 |
+
parser.add_argument("--beta-schedule", choices=["linear", "cosine"], default="linear")
|
| 69 |
+
parser.add_argument(
|
| 70 |
+
"--beta-hold-steps",
|
| 71 |
+
type=int,
|
| 72 |
+
default=0,
|
| 73 |
+
help=(
|
| 74 |
+
"Keep beta fixed at beta-start for this many optimizer steps before "
|
| 75 |
+
"transitioning to beta-end. This enables schedules such as: hold "
|
| 76 |
+
"beta=1.0 for 100 steps, then decay to 0.5 over 10 steps."
|
| 77 |
+
),
|
| 78 |
+
)
|
| 79 |
+
parser.add_argument(
|
| 80 |
+
"--beta-transition-steps",
|
| 81 |
+
type=int,
|
| 82 |
+
default=None,
|
| 83 |
+
help=(
|
| 84 |
+
"Number of optimizer steps used to transition beta from beta-start to "
|
| 85 |
+
"beta-end after beta-hold-steps. If unset, falls back to "
|
| 86 |
+
"beta-schedule-steps / max_steps for backward compatibility."
|
| 87 |
+
),
|
| 88 |
+
)
|
| 89 |
+
parser.add_argument(
|
| 90 |
+
"--hold-transition-schedule",
|
| 91 |
+
choices=["linear", "cosine"],
|
| 92 |
+
default="linear",
|
| 93 |
+
help="Schedule shape used by the hold-then-transition beta/LR schedules.",
|
| 94 |
+
)
|
| 95 |
+
parser.add_argument(
|
| 96 |
+
"--lr-start",
|
| 97 |
+
type=float,
|
| 98 |
+
default=None,
|
| 99 |
+
help="Initial LR for custom hold-then-transition scheduling. If unset, uses --learning-rate.",
|
| 100 |
+
)
|
| 101 |
+
parser.add_argument(
|
| 102 |
+
"--lr-end",
|
| 103 |
+
type=float,
|
| 104 |
+
default=None,
|
| 105 |
+
help="Final LR after the custom transition. If unset, custom LR scheduling is disabled.",
|
| 106 |
+
)
|
| 107 |
+
parser.add_argument(
|
| 108 |
+
"--lr-hold-steps",
|
| 109 |
+
type=int,
|
| 110 |
+
default=None,
|
| 111 |
+
help="Keep LR fixed at lr-start for this many optimizer steps. If unset, uses beta-hold-steps.",
|
| 112 |
+
)
|
| 113 |
+
parser.add_argument(
|
| 114 |
+
"--lr-transition-steps",
|
| 115 |
+
type=int,
|
| 116 |
+
default=None,
|
| 117 |
+
help=(
|
| 118 |
+
"Number of optimizer steps used to transition LR from lr-start to lr-end. "
|
| 119 |
+
"If unset, uses beta-transition-steps."
|
| 120 |
+
),
|
| 121 |
+
)
|
| 122 |
+
parser.add_argument(
|
| 123 |
+
"--loss-type",
|
| 124 |
+
choices=["full_vocab", "sampled_token"],
|
| 125 |
+
default="full_vocab",
|
| 126 |
+
help=(
|
| 127 |
+
"full_vocab matches the normalized PoE distribution over the whole vocab. "
|
| 128 |
+
"sampled_token uses an OPD-style sampled-token surrogate with a PoE advantage."
|
| 129 |
+
),
|
| 130 |
+
)
|
| 131 |
+
parser.add_argument(
|
| 132 |
+
"--advantage-normalization",
|
| 133 |
+
choices=["none", "batch", "sequence"],
|
| 134 |
+
default="batch",
|
| 135 |
+
help="Only used by --loss-type sampled_token.",
|
| 136 |
+
)
|
| 137 |
+
parser.add_argument(
|
| 138 |
+
"--advantage-clip",
|
| 139 |
+
type=float,
|
| 140 |
+
default=None,
|
| 141 |
+
help="Symmetric clamp for sampled-token advantages. Example: 5.0.",
|
| 142 |
+
)
|
| 143 |
+
parser.add_argument(
|
| 144 |
+
"--use-ppo-clip",
|
| 145 |
+
action="store_true",
|
| 146 |
+
default=False,
|
| 147 |
+
help=(
|
| 148 |
+
"Only used by --loss-type sampled_token. Use PPO-style ratio clipping "
|
| 149 |
+
"with the frozen reference log-prob as the old rollout log-prob."
|
| 150 |
+
),
|
| 151 |
+
)
|
| 152 |
+
parser.add_argument(
|
| 153 |
+
"--ppo-clip-low",
|
| 154 |
+
type=float,
|
| 155 |
+
default=0.2,
|
| 156 |
+
help="Only used when --use-ppo-clip is set. Lower PPO clip epsilon.",
|
| 157 |
+
)
|
| 158 |
+
parser.add_argument(
|
| 159 |
+
"--ppo-clip-high",
|
| 160 |
+
type=float,
|
| 161 |
+
default=0.2,
|
| 162 |
+
help="Only used when --use-ppo-clip is set. Upper PPO clip epsilon.",
|
| 163 |
+
)
|
| 164 |
+
parser.add_argument(
|
| 165 |
+
"--sampled-loss-reduction",
|
| 166 |
+
choices=["per_sample", "per_token"],
|
| 167 |
+
default="per_sample",
|
| 168 |
+
help=(
|
| 169 |
+
"Only used by --loss-type sampled_token. per_sample averages each response "
|
| 170 |
+
"first, then averages across batch; per_token averages over all response tokens."
|
| 171 |
+
),
|
| 172 |
+
)
|
| 173 |
+
parser.add_argument(
|
| 174 |
+
"--positive-advantages-only",
|
| 175 |
+
action="store_true",
|
| 176 |
+
default=False,
|
| 177 |
+
help="Only reinforce sampled tokens with positive PoE advantages.",
|
| 178 |
+
)
|
| 179 |
+
parser.add_argument("--max-length", type=int, default=4096)
|
| 180 |
+
parser.add_argument("--distill-chunk-size", type=int, default=128)
|
| 181 |
+
parser.add_argument("--max-train-samples", type=int, default=None)
|
| 182 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 183 |
+
|
| 184 |
+
parser.add_argument("--num-train-epochs", type=float, default=1.0)
|
| 185 |
+
parser.add_argument("--max-steps", type=int, default=-1)
|
| 186 |
+
parser.add_argument("--per-device-train-batch-size", type=int, default=1)
|
| 187 |
+
parser.add_argument("--gradient-accumulation-steps", type=int, default=16)
|
| 188 |
+
parser.add_argument("--learning-rate", type=float, default=2e-5)
|
| 189 |
+
parser.add_argument("--weight-decay", type=float, default=0.0)
|
| 190 |
+
parser.add_argument("--adam-beta1", type=float, default=0.9)
|
| 191 |
+
parser.add_argument("--adam-beta2", type=float, default=0.999)
|
| 192 |
+
parser.add_argument("--adam-epsilon", type=float, default=1e-8)
|
| 193 |
+
parser.add_argument("--warmup-ratio", type=float, default=0.03)
|
| 194 |
+
parser.add_argument("--lr-scheduler-type", default="cosine")
|
| 195 |
+
parser.add_argument("--logging-steps", type=int, default=1)
|
| 196 |
+
parser.add_argument("--save-steps", type=int, default=100)
|
| 197 |
+
parser.add_argument("--save-total-limit", type=int, default=0)
|
| 198 |
+
parser.add_argument("--bf16", action=argparse.BooleanOptionalAction, default=True)
|
| 199 |
+
parser.add_argument("--fp16", action="store_true", default=False)
|
| 200 |
+
parser.add_argument("--gradient-checkpointing", action=argparse.BooleanOptionalAction, default=True)
|
| 201 |
+
parser.add_argument("--report-to", default="none")
|
| 202 |
+
|
| 203 |
+
parser.add_argument("--lora-r", type=int, default=64)
|
| 204 |
+
parser.add_argument("--lora-alpha", type=int, default=128)
|
| 205 |
+
parser.add_argument("--lora-dropout", type=float, default=0.05)
|
| 206 |
+
parser.add_argument(
|
| 207 |
+
"--freeze-lora-b-after-step",
|
| 208 |
+
type=int,
|
| 209 |
+
default=None,
|
| 210 |
+
help="Freeze all LoRA B matrices once global_step reaches this value. Example: 20.",
|
| 211 |
+
)
|
| 212 |
+
parser.add_argument(
|
| 213 |
+
"--lora-target-modules",
|
| 214 |
+
default="q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj",
|
| 215 |
+
help="Comma-separated LoRA target modules.",
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
parser.add_argument("--trust-remote-code", action="store_true", default=True)
|
| 219 |
+
parser.add_argument(
|
| 220 |
+
"--attn-implementation",
|
| 221 |
+
default=None,
|
| 222 |
+
choices=[None, "eager", "sdpa", "flash_attention_2"],
|
| 223 |
+
help="Forwarded to from_pretrained when set.",
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
args = parser.parse_args()
|
| 227 |
+
if not args.student_model:
|
| 228 |
+
raise ValueError("Pass --student-model or set SFT_CHECKPOINT to the Qwen3-4B SFT checkpoint.")
|
| 229 |
+
if args.alpha <= 0:
|
| 230 |
+
raise ValueError("--alpha must be positive.")
|
| 231 |
+
fixed_beta = args.alpha / (args.alpha + 1.0)
|
| 232 |
+
if args.beta_start is None:
|
| 233 |
+
args.beta_start = fixed_beta
|
| 234 |
+
if args.beta_end is None:
|
| 235 |
+
args.beta_end = fixed_beta
|
| 236 |
+
if not 0.0 <= args.beta_start <= 1.0:
|
| 237 |
+
raise ValueError("--beta-start must be in [0, 1].")
|
| 238 |
+
if not 0.0 <= args.beta_end <= 1.0:
|
| 239 |
+
raise ValueError("--beta-end must be in [0, 1].")
|
| 240 |
+
if args.beta_schedule_steps is not None and args.beta_schedule_steps <= 0:
|
| 241 |
+
raise ValueError("--beta-schedule-steps must be positive when set.")
|
| 242 |
+
if args.beta_hold_steps < 0:
|
| 243 |
+
raise ValueError("--beta-hold-steps must be non-negative.")
|
| 244 |
+
if args.beta_transition_steps is not None and args.beta_transition_steps <= 0:
|
| 245 |
+
raise ValueError("--beta-transition-steps must be positive when set.")
|
| 246 |
+
if args.lr_start is None:
|
| 247 |
+
args.lr_start = args.learning_rate
|
| 248 |
+
if args.lr_hold_steps is None:
|
| 249 |
+
args.lr_hold_steps = args.beta_hold_steps
|
| 250 |
+
if args.lr_transition_steps is None:
|
| 251 |
+
args.lr_transition_steps = args.beta_transition_steps
|
| 252 |
+
if args.lr_hold_steps is not None and args.lr_hold_steps < 0:
|
| 253 |
+
raise ValueError("--lr-hold-steps must be non-negative when set.")
|
| 254 |
+
if args.lr_end is not None:
|
| 255 |
+
if args.lr_start <= 0.0 or args.lr_end <= 0.0:
|
| 256 |
+
raise ValueError("--lr-start and --lr-end must be positive when using custom LR scheduling.")
|
| 257 |
+
if args.lr_transition_steps is None or args.lr_transition_steps <= 0:
|
| 258 |
+
raise ValueError("--lr-transition-steps must be positive when using --lr-end.")
|
| 259 |
+
if args.advantage_clip is not None and args.advantage_clip <= 0:
|
| 260 |
+
raise ValueError("--advantage-clip must be positive when set.")
|
| 261 |
+
if args.ppo_clip_low < 0 or args.ppo_clip_high < 0:
|
| 262 |
+
raise ValueError("--ppo-clip-low and --ppo-clip-high must be non-negative.")
|
| 263 |
+
if args.freeze_lora_b_after_step is not None and args.freeze_lora_b_after_step < 0:
|
| 264 |
+
raise ValueError("--freeze-lora-b-after-step must be non-negative when set.")
|
| 265 |
+
if args.fp16 and args.bf16:
|
| 266 |
+
args.bf16 = False
|
| 267 |
+
return args
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def first_assistant_index(messages: list[dict[str, str]]) -> int:
|
| 271 |
+
for idx, message in enumerate(messages):
|
| 272 |
+
if message.get("role") == "assistant":
|
| 273 |
+
return idx
|
| 274 |
+
raise ValueError("Rollout row has no assistant message.")
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def tokenize_rollout(example: dict[str, Any], tokenizer: AutoTokenizer, max_length: int) -> dict[str, Any]:
|
| 278 |
+
messages = example["messages"]
|
| 279 |
+
assistant_idx = first_assistant_index(messages)
|
| 280 |
+
prompt_messages = messages[:assistant_idx]
|
| 281 |
+
full_messages = messages[: assistant_idx + 1]
|
| 282 |
+
|
| 283 |
+
prompt_text = tokenizer.apply_chat_template(
|
| 284 |
+
prompt_messages,
|
| 285 |
+
tokenize=False,
|
| 286 |
+
add_generation_prompt=True,
|
| 287 |
+
enable_thinking=True,
|
| 288 |
+
)
|
| 289 |
+
full_text = tokenizer.apply_chat_template(
|
| 290 |
+
full_messages,
|
| 291 |
+
tokenize=False,
|
| 292 |
+
add_generation_prompt=False,
|
| 293 |
+
enable_thinking=True,
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
prompt_ids = tokenizer.encode(prompt_text, add_special_tokens=False)
|
| 297 |
+
input_ids = tokenizer.encode(full_text, add_special_tokens=False)
|
| 298 |
+
|
| 299 |
+
if len(input_ids) > max_length:
|
| 300 |
+
input_ids = input_ids[:max_length]
|
| 301 |
+
|
| 302 |
+
# Mask is aligned to labels=input_ids[1:]. A label predicts token position
|
| 303 |
+
# j=i+1, so it belongs to the response when j >= len(prompt_ids).
|
| 304 |
+
label_len = max(len(input_ids) - 1, 0)
|
| 305 |
+
loss_mask = [1 if i + 1 >= len(prompt_ids) else 0 for i in range(label_len)]
|
| 306 |
+
|
| 307 |
+
if sum(loss_mask) == 0:
|
| 308 |
+
# Drop examples where truncation removed the assistant response.
|
| 309 |
+
return {"input_ids": [], "loss_mask": []}
|
| 310 |
+
|
| 311 |
+
return {"input_ids": input_ids, "loss_mask": loss_mask}
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
@dataclass
|
| 315 |
+
class DistillCollator:
|
| 316 |
+
pad_token_id: int
|
| 317 |
+
|
| 318 |
+
def __call__(self, features: list[dict[str, Any]]) -> dict[str, torch.Tensor]:
|
| 319 |
+
input_ids = [torch.tensor(f["input_ids"], dtype=torch.long) for f in features]
|
| 320 |
+
loss_masks = [torch.tensor(f["loss_mask"], dtype=torch.float32) for f in features]
|
| 321 |
+
lengths = torch.tensor([x.size(0) for x in input_ids], dtype=torch.long)
|
| 322 |
+
|
| 323 |
+
padded_input_ids = pad_sequence(input_ids, batch_first=True, padding_value=self.pad_token_id)
|
| 324 |
+
# loss_mask is one shorter than input_ids because it aligns to shifted labels.
|
| 325 |
+
padded_loss_masks = pad_sequence(loss_masks, batch_first=True, padding_value=0.0)
|
| 326 |
+
positions = torch.arange(padded_input_ids.size(1)).unsqueeze(0)
|
| 327 |
+
attention_mask = (positions < lengths.unsqueeze(1)).long()
|
| 328 |
+
return {
|
| 329 |
+
"input_ids": padded_input_ids,
|
| 330 |
+
"attention_mask": attention_mask,
|
| 331 |
+
"loss_mask": padded_loss_masks,
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def hold_then_transition_value(
|
| 336 |
+
*,
|
| 337 |
+
step: int,
|
| 338 |
+
start: float,
|
| 339 |
+
end: float,
|
| 340 |
+
hold_steps: int,
|
| 341 |
+
transition_steps: int | None,
|
| 342 |
+
schedule: str,
|
| 343 |
+
) -> float:
|
| 344 |
+
"""Return start during hold, then interpolate start -> end.
|
| 345 |
+
|
| 346 |
+
Step is an optimizer global_step, not a micro-batch step. With gradient
|
| 347 |
+
accumulation, global_step advances only after one optimizer update.
|
| 348 |
+
"""
|
| 349 |
+
if step < hold_steps:
|
| 350 |
+
return start
|
| 351 |
+
if transition_steps is None or transition_steps <= 0:
|
| 352 |
+
return end
|
| 353 |
+
|
| 354 |
+
local_step = step - hold_steps
|
| 355 |
+
progress = min(max(local_step / transition_steps, 0.0), 1.0)
|
| 356 |
+
if schedule == "cosine":
|
| 357 |
+
progress = 0.5 - 0.5 * torch.cos(torch.tensor(progress * torch.pi)).item()
|
| 358 |
+
elif schedule != "linear":
|
| 359 |
+
raise ValueError(f"Unknown schedule: {schedule}")
|
| 360 |
+
return start + (end - start) * progress
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
class PoEDistillTrainer(Trainer):
|
| 364 |
+
def __init__(
|
| 365 |
+
self,
|
| 366 |
+
*args: Any,
|
| 367 |
+
teacher_model: torch.nn.Module,
|
| 368 |
+
beta_start: float,
|
| 369 |
+
beta_end: float,
|
| 370 |
+
beta_schedule_steps: int | None,
|
| 371 |
+
beta_schedule: str,
|
| 372 |
+
beta_hold_steps: int,
|
| 373 |
+
beta_transition_steps: int | None,
|
| 374 |
+
hold_transition_schedule: str,
|
| 375 |
+
loss_type: str,
|
| 376 |
+
advantage_normalization: str,
|
| 377 |
+
advantage_clip: float | None,
|
| 378 |
+
positive_advantages_only: bool,
|
| 379 |
+
use_ppo_clip: bool,
|
| 380 |
+
ppo_clip_low: float,
|
| 381 |
+
ppo_clip_high: float,
|
| 382 |
+
sampled_loss_reduction: str,
|
| 383 |
+
distill_chunk_size: int,
|
| 384 |
+
**kwargs: Any,
|
| 385 |
+
) -> None:
|
| 386 |
+
super().__init__(*args, **kwargs)
|
| 387 |
+
self.teacher_model = teacher_model
|
| 388 |
+
self.teacher_model.to(self.args.device)
|
| 389 |
+
self.teacher_model.eval()
|
| 390 |
+
self.beta_start = beta_start
|
| 391 |
+
self.beta_end = beta_end
|
| 392 |
+
self.beta_schedule_steps = beta_schedule_steps
|
| 393 |
+
self.beta_schedule = beta_schedule
|
| 394 |
+
self.beta_hold_steps = beta_hold_steps
|
| 395 |
+
self.beta_transition_steps = beta_transition_steps
|
| 396 |
+
self.hold_transition_schedule = hold_transition_schedule
|
| 397 |
+
self.loss_type = loss_type
|
| 398 |
+
self.advantage_normalization = advantage_normalization
|
| 399 |
+
self.advantage_clip = advantage_clip
|
| 400 |
+
self.positive_advantages_only = positive_advantages_only
|
| 401 |
+
self.use_ppo_clip = use_ppo_clip
|
| 402 |
+
self.ppo_clip_low = ppo_clip_low
|
| 403 |
+
self.ppo_clip_high = ppo_clip_high
|
| 404 |
+
self.sampled_loss_reduction = sampled_loss_reduction
|
| 405 |
+
self.distill_chunk_size = distill_chunk_size
|
| 406 |
+
|
| 407 |
+
def current_beta(self) -> float:
|
| 408 |
+
# New mode: hold beta_start for beta_hold_steps, then transition to beta_end.
|
| 409 |
+
if self.beta_hold_steps > 0 or self.beta_transition_steps is not None:
|
| 410 |
+
return hold_then_transition_value(
|
| 411 |
+
step=self.state.global_step,
|
| 412 |
+
start=self.beta_start,
|
| 413 |
+
end=self.beta_end,
|
| 414 |
+
hold_steps=self.beta_hold_steps,
|
| 415 |
+
transition_steps=self.beta_transition_steps,
|
| 416 |
+
schedule=self.hold_transition_schedule,
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
# Backward-compatible old behavior: directly schedule beta_start -> beta_end.
|
| 420 |
+
schedule_steps = self.beta_schedule_steps
|
| 421 |
+
if schedule_steps is None:
|
| 422 |
+
schedule_steps = self.state.max_steps if self.state.max_steps > 0 else None
|
| 423 |
+
if schedule_steps is None or schedule_steps == 0:
|
| 424 |
+
return self.beta_end
|
| 425 |
+
|
| 426 |
+
progress = min(max(self.state.global_step / schedule_steps, 0.0), 1.0)
|
| 427 |
+
if self.beta_schedule == "cosine":
|
| 428 |
+
progress = 0.5 - 0.5 * torch.cos(torch.tensor(progress * torch.pi)).item()
|
| 429 |
+
return self.beta_start + (self.beta_end - self.beta_start) * progress
|
| 430 |
+
|
| 431 |
+
@staticmethod
|
| 432 |
+
def gather_token_logprobs(logits: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
|
| 433 |
+
logits = logits.float()
|
| 434 |
+
token_logits = logits.gather(dim=-1, index=labels.unsqueeze(-1)).squeeze(-1)
|
| 435 |
+
return token_logits - logits.logsumexp(dim=-1)
|
| 436 |
+
|
| 437 |
+
def normalize_advantages(self, advantages: torch.Tensor, loss_mask: torch.Tensor) -> torch.Tensor:
|
| 438 |
+
if self.advantage_normalization == "none":
|
| 439 |
+
return advantages
|
| 440 |
+
|
| 441 |
+
if self.advantage_normalization == "batch":
|
| 442 |
+
denom = loss_mask.sum().clamp_min(1.0)
|
| 443 |
+
mean = (advantages * loss_mask).sum() / denom
|
| 444 |
+
var = (((advantages - mean) * loss_mask) ** 2).sum() / denom
|
| 445 |
+
return (advantages - mean) / torch.sqrt(var + 1e-6)
|
| 446 |
+
|
| 447 |
+
denom = loss_mask.sum(dim=1, keepdim=True).clamp_min(1.0)
|
| 448 |
+
mean = (advantages * loss_mask).sum(dim=1, keepdim=True) / denom
|
| 449 |
+
var = (((advantages - mean) * loss_mask) ** 2).sum(dim=1, keepdim=True) / denom
|
| 450 |
+
return (advantages - mean) / torch.sqrt(var + 1e-6)
|
| 451 |
+
|
| 452 |
+
def compute_loss(
|
| 453 |
+
self,
|
| 454 |
+
model: torch.nn.Module,
|
| 455 |
+
inputs: dict[str, torch.Tensor],
|
| 456 |
+
return_outputs: bool = False,
|
| 457 |
+
**_: Any,
|
| 458 |
+
):
|
| 459 |
+
loss_mask = inputs.pop("loss_mask")
|
| 460 |
+
input_ids = inputs["input_ids"]
|
| 461 |
+
attention_mask = inputs["attention_mask"]
|
| 462 |
+
labels = input_ids[:, 1:]
|
| 463 |
+
|
| 464 |
+
with torch.no_grad():
|
| 465 |
+
teacher_logits = self.teacher_model(
|
| 466 |
+
input_ids=input_ids,
|
| 467 |
+
attention_mask=attention_mask,
|
| 468 |
+
use_cache=False,
|
| 469 |
+
).logits[:, :-1, :].detach()
|
| 470 |
+
|
| 471 |
+
adapter_owner = model.module if hasattr(model, "module") else model
|
| 472 |
+
disable_adapter = getattr(adapter_owner, "disable_adapter", None)
|
| 473 |
+
ref_context = disable_adapter() if disable_adapter is not None else contextlib.nullcontext()
|
| 474 |
+
with ref_context:
|
| 475 |
+
ref_logits = adapter_owner(
|
| 476 |
+
input_ids=input_ids,
|
| 477 |
+
attention_mask=attention_mask,
|
| 478 |
+
use_cache=False,
|
| 479 |
+
).logits[:, :-1, :].detach()
|
| 480 |
+
|
| 481 |
+
student_outputs = model(input_ids=input_ids, attention_mask=attention_mask, use_cache=False)
|
| 482 |
+
student_logits = student_outputs.logits[:, :-1, :]
|
| 483 |
+
if teacher_logits.size(-1) != student_logits.size(-1) or ref_logits.size(-1) != student_logits.size(-1):
|
| 484 |
+
raise ValueError(
|
| 485 |
+
"Teacher, reference, and student vocab sizes must match for full-vocab PoE distillation. "
|
| 486 |
+
f"Got teacher={teacher_logits.size(-1)}, ref={ref_logits.size(-1)}, "
|
| 487 |
+
f"student={student_logits.size(-1)}."
|
| 488 |
+
)
|
| 489 |
+
|
| 490 |
+
total_loss = student_logits.new_zeros(())
|
| 491 |
+
total_tokens = loss_mask.sum().clamp_min(1.0)
|
| 492 |
+
beta = self.current_beta()
|
| 493 |
+
|
| 494 |
+
if self.loss_type == "sampled_token":
|
| 495 |
+
teacher_logp = self.gather_token_logprobs(teacher_logits, labels)
|
| 496 |
+
ref_logp = self.gather_token_logprobs(ref_logits, labels)
|
| 497 |
+
student_logp = self.gather_token_logprobs(student_logits, labels)
|
| 498 |
+
|
| 499 |
+
poe_score = beta * teacher_logp + (1.0 - beta) * ref_logp
|
| 500 |
+
advantages = poe_score - student_logp.detach()
|
| 501 |
+
advantages = self.normalize_advantages(advantages, loss_mask)
|
| 502 |
+
if self.advantage_clip is not None:
|
| 503 |
+
advantages = advantages.clamp(min=-self.advantage_clip, max=self.advantage_clip)
|
| 504 |
+
if self.positive_advantages_only:
|
| 505 |
+
advantages = advantages.clamp_min(0.0)
|
| 506 |
+
|
| 507 |
+
advantages = advantages.detach()
|
| 508 |
+
|
| 509 |
+
if self.use_ppo_clip:
|
| 510 |
+
# The rollouts are generated by the frozen reference/SFT policy, so ref_logp
|
| 511 |
+
# is used as the old rollout log-prob. This mirrors the PPO-style clipped
|
| 512 |
+
# policy loss used in RL frameworks such as slime.
|
| 513 |
+
ratio = torch.exp(student_logp - ref_logp.detach())
|
| 514 |
+
ratio_clipped = ratio.clamp(1.0 - self.ppo_clip_low, 1.0 + self.ppo_clip_high)
|
| 515 |
+
pg_loss_unclipped = -ratio * advantages
|
| 516 |
+
pg_loss_clipped = -ratio_clipped * advantages
|
| 517 |
+
token_loss = torch.maximum(pg_loss_unclipped, pg_loss_clipped)
|
| 518 |
+
else:
|
| 519 |
+
# Direct OPD-style sampled-token surrogate.
|
| 520 |
+
token_loss = -advantages * student_logp
|
| 521 |
+
|
| 522 |
+
if self.sampled_loss_reduction == "per_token":
|
| 523 |
+
loss = (token_loss * loss_mask).sum() / total_tokens
|
| 524 |
+
else:
|
| 525 |
+
# Per-sample mean: each response contributes equally regardless of length.
|
| 526 |
+
seq_loss = (token_loss * loss_mask).sum(dim=1) / loss_mask.sum(dim=1).clamp_min(1.0)
|
| 527 |
+
loss = seq_loss.mean()
|
| 528 |
+
|
| 529 |
+
return (loss, student_outputs) if return_outputs else loss
|
| 530 |
+
|
| 531 |
+
seq_len = student_logits.size(1)
|
| 532 |
+
for start in range(0, seq_len, self.distill_chunk_size):
|
| 533 |
+
end = min(start + self.distill_chunk_size, seq_len)
|
| 534 |
+
mask = loss_mask[:, start:end]
|
| 535 |
+
if mask.sum() == 0:
|
| 536 |
+
continue
|
| 537 |
+
|
| 538 |
+
teacher_logp = F.log_softmax(teacher_logits[:, start:end, :].float(), dim=-1)
|
| 539 |
+
ref_logp = F.log_softmax(ref_logits[:, start:end, :].float(), dim=-1)
|
| 540 |
+
student_logp = F.log_softmax(student_logits[:, start:end, :].float(), dim=-1)
|
| 541 |
+
|
| 542 |
+
poe_logits = beta * teacher_logp + (1.0 - beta) * ref_logp
|
| 543 |
+
target_probs = F.softmax(poe_logits, dim=-1)
|
| 544 |
+
token_ce = -(target_probs * student_logp).sum(dim=-1)
|
| 545 |
+
total_loss = total_loss + (token_ce * mask).sum()
|
| 546 |
+
|
| 547 |
+
loss = total_loss / total_tokens
|
| 548 |
+
return (loss, student_outputs) if return_outputs else loss
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
class FreezeLoRABCallback(TrainerCallback):
|
| 552 |
+
def __init__(self, freeze_after_step: int | None) -> None:
|
| 553 |
+
self.freeze_after_step = freeze_after_step
|
| 554 |
+
self.frozen = False
|
| 555 |
+
|
| 556 |
+
def on_step_begin(
|
| 557 |
+
self,
|
| 558 |
+
args: TrainingArguments,
|
| 559 |
+
state: TrainerState,
|
| 560 |
+
control: TrainerControl,
|
| 561 |
+
model: torch.nn.Module | None = None,
|
| 562 |
+
**kwargs: Any,
|
| 563 |
+
) -> TrainerControl:
|
| 564 |
+
if self.freeze_after_step is None or self.frozen or model is None:
|
| 565 |
+
return control
|
| 566 |
+
if state.global_step < self.freeze_after_step:
|
| 567 |
+
return control
|
| 568 |
+
|
| 569 |
+
frozen_params = 0
|
| 570 |
+
module = model.module if hasattr(model, "module") else model
|
| 571 |
+
for name, param in module.named_parameters():
|
| 572 |
+
if ".lora_B." in name or "lora_B." in name:
|
| 573 |
+
param.requires_grad_(False)
|
| 574 |
+
frozen_params += param.numel()
|
| 575 |
+
|
| 576 |
+
self.frozen = True
|
| 577 |
+
if args.process_index == 0:
|
| 578 |
+
print(f"[PoE Distill] Froze LoRA B at global_step={state.global_step} ({frozen_params} params).")
|
| 579 |
+
return control
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
class HoldThenTransitionLRCallback(TrainerCallback):
|
| 583 |
+
"""Custom LR schedule: hold lr_start, transition to lr_end, then keep lr_end.
|
| 584 |
+
|
| 585 |
+
Disable the Hugging Face scheduler interaction by setting
|
| 586 |
+
--lr-scheduler-type constant and --warmup-ratio 0.0 in the launcher when
|
| 587 |
+
using --lr-end. This callback sets optimizer param-group LRs directly at
|
| 588 |
+
each optimizer step.
|
| 589 |
+
"""
|
| 590 |
+
|
| 591 |
+
def __init__(
|
| 592 |
+
self,
|
| 593 |
+
lr_start: float,
|
| 594 |
+
lr_end: float | None,
|
| 595 |
+
lr_hold_steps: int,
|
| 596 |
+
lr_transition_steps: int | None,
|
| 597 |
+
schedule: str,
|
| 598 |
+
) -> None:
|
| 599 |
+
self.lr_start = lr_start
|
| 600 |
+
self.lr_end = lr_end
|
| 601 |
+
self.lr_hold_steps = lr_hold_steps
|
| 602 |
+
self.lr_transition_steps = lr_transition_steps
|
| 603 |
+
self.schedule = schedule
|
| 604 |
+
|
| 605 |
+
def current_lr(self, step: int) -> float:
|
| 606 |
+
if self.lr_end is None:
|
| 607 |
+
return self.lr_start
|
| 608 |
+
return hold_then_transition_value(
|
| 609 |
+
step=step,
|
| 610 |
+
start=self.lr_start,
|
| 611 |
+
end=self.lr_end,
|
| 612 |
+
hold_steps=self.lr_hold_steps,
|
| 613 |
+
transition_steps=self.lr_transition_steps,
|
| 614 |
+
schedule=self.schedule,
|
| 615 |
+
)
|
| 616 |
+
|
| 617 |
+
def on_train_begin(
|
| 618 |
+
self,
|
| 619 |
+
args: TrainingArguments,
|
| 620 |
+
state: TrainerState,
|
| 621 |
+
control: TrainerControl,
|
| 622 |
+
optimizer: torch.optim.Optimizer | None = None,
|
| 623 |
+
**kwargs: Any,
|
| 624 |
+
) -> TrainerControl:
|
| 625 |
+
return self._set_lr(args, state, control, optimizer)
|
| 626 |
+
|
| 627 |
+
def on_step_begin(
|
| 628 |
+
self,
|
| 629 |
+
args: TrainingArguments,
|
| 630 |
+
state: TrainerState,
|
| 631 |
+
control: TrainerControl,
|
| 632 |
+
optimizer: torch.optim.Optimizer | None = None,
|
| 633 |
+
**kwargs: Any,
|
| 634 |
+
) -> TrainerControl:
|
| 635 |
+
return self._set_lr(args, state, control, optimizer)
|
| 636 |
+
|
| 637 |
+
def _set_lr(
|
| 638 |
+
self,
|
| 639 |
+
args: TrainingArguments,
|
| 640 |
+
state: TrainerState,
|
| 641 |
+
control: TrainerControl,
|
| 642 |
+
optimizer: torch.optim.Optimizer | None,
|
| 643 |
+
) -> TrainerControl:
|
| 644 |
+
if optimizer is None or self.lr_end is None:
|
| 645 |
+
return control
|
| 646 |
+
|
| 647 |
+
lr = self.current_lr(state.global_step)
|
| 648 |
+
for group in optimizer.param_groups:
|
| 649 |
+
group["lr"] = lr
|
| 650 |
+
|
| 651 |
+
if args.process_index == 0 and state.global_step % max(args.logging_steps, 1) == 0:
|
| 652 |
+
print(f"[PoE Distill] global_step={state.global_step}, custom_lr={lr:.3e}")
|
| 653 |
+
return control
|
| 654 |
+
|
| 655 |
+
|
| 656 |
+
class BetaLoggingCallback(TrainerCallback):
|
| 657 |
+
"""Log beta occasionally without changing training behavior."""
|
| 658 |
+
|
| 659 |
+
def __init__(self, trainer_ref_getter) -> None:
|
| 660 |
+
self.trainer_ref_getter = trainer_ref_getter
|
| 661 |
+
|
| 662 |
+
def on_step_begin(
|
| 663 |
+
self,
|
| 664 |
+
args: TrainingArguments,
|
| 665 |
+
state: TrainerState,
|
| 666 |
+
control: TrainerControl,
|
| 667 |
+
**kwargs: Any,
|
| 668 |
+
) -> TrainerControl:
|
| 669 |
+
trainer = self.trainer_ref_getter()
|
| 670 |
+
if trainer is not None and args.process_index == 0 and state.global_step % max(args.logging_steps, 1) == 0:
|
| 671 |
+
print(f"[PoE Distill] global_step={state.global_step}, beta={trainer.current_beta():.6f}")
|
| 672 |
+
return control
|
| 673 |
+
|
| 674 |
+
|
| 675 |
+
def main() -> None:
|
| 676 |
+
args = parse_args()
|
| 677 |
+
set_seed(args.seed)
|
| 678 |
+
|
| 679 |
+
tokenizer = AutoTokenizer.from_pretrained(args.student_model, trust_remote_code=args.trust_remote_code)
|
| 680 |
+
if tokenizer.pad_token_id is None:
|
| 681 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 682 |
+
|
| 683 |
+
raw_dataset = load_dataset("parquet", data_files=args.train_data, split="train")
|
| 684 |
+
if args.max_train_samples is not None:
|
| 685 |
+
raw_dataset = raw_dataset.select(range(min(args.max_train_samples, len(raw_dataset))))
|
| 686 |
+
|
| 687 |
+
train_dataset = raw_dataset.map(
|
| 688 |
+
lambda ex: tokenize_rollout(ex, tokenizer, args.max_length),
|
| 689 |
+
remove_columns=raw_dataset.column_names,
|
| 690 |
+
desc="Tokenizing pi_ref rollouts",
|
| 691 |
+
).filter(lambda ex: len(ex["input_ids"]) > 0, desc="Dropping empty responses")
|
| 692 |
+
|
| 693 |
+
model_kwargs = {
|
| 694 |
+
"torch_dtype": torch.bfloat16 if args.bf16 else (torch.float16 if args.fp16 else torch.float32),
|
| 695 |
+
"trust_remote_code": args.trust_remote_code,
|
| 696 |
+
}
|
| 697 |
+
if args.attn_implementation is not None:
|
| 698 |
+
model_kwargs["attn_implementation"] = args.attn_implementation
|
| 699 |
+
|
| 700 |
+
student = AutoModelForCausalLM.from_pretrained(args.student_model, **model_kwargs)
|
| 701 |
+
teacher = AutoModelForCausalLM.from_pretrained(args.teacher_model, **model_kwargs)
|
| 702 |
+
teacher.eval()
|
| 703 |
+
teacher.requires_grad_(False)
|
| 704 |
+
|
| 705 |
+
if args.gradient_checkpointing:
|
| 706 |
+
student.gradient_checkpointing_enable()
|
| 707 |
+
student.config.use_cache = False
|
| 708 |
+
teacher.config.use_cache = False
|
| 709 |
+
|
| 710 |
+
lora_config = LoraConfig(
|
| 711 |
+
task_type=TaskType.CAUSAL_LM,
|
| 712 |
+
r=args.lora_r,
|
| 713 |
+
lora_alpha=args.lora_alpha,
|
| 714 |
+
lora_dropout=args.lora_dropout,
|
| 715 |
+
target_modules=[m.strip() for m in args.lora_target_modules.split(",") if m.strip()],
|
| 716 |
+
)
|
| 717 |
+
student = get_peft_model(student, lora_config)
|
| 718 |
+
student.print_trainable_parameters()
|
| 719 |
+
|
| 720 |
+
training_args = TrainingArguments(
|
| 721 |
+
output_dir=args.output_dir,
|
| 722 |
+
num_train_epochs=args.num_train_epochs,
|
| 723 |
+
max_steps=args.max_steps,
|
| 724 |
+
per_device_train_batch_size=args.per_device_train_batch_size,
|
| 725 |
+
gradient_accumulation_steps=args.gradient_accumulation_steps,
|
| 726 |
+
learning_rate=args.learning_rate,
|
| 727 |
+
weight_decay=args.weight_decay,
|
| 728 |
+
adam_beta1=args.adam_beta1,
|
| 729 |
+
adam_beta2=args.adam_beta2,
|
| 730 |
+
adam_epsilon=args.adam_epsilon,
|
| 731 |
+
warmup_ratio=args.warmup_ratio,
|
| 732 |
+
lr_scheduler_type=args.lr_scheduler_type,
|
| 733 |
+
logging_steps=args.logging_steps,
|
| 734 |
+
save_steps=args.save_steps,
|
| 735 |
+
save_total_limit=args.save_total_limit,
|
| 736 |
+
bf16=args.bf16,
|
| 737 |
+
fp16=args.fp16,
|
| 738 |
+
gradient_checkpointing=args.gradient_checkpointing,
|
| 739 |
+
remove_unused_columns=False,
|
| 740 |
+
report_to=[] if args.report_to == "none" else args.report_to.split(","),
|
| 741 |
+
)
|
| 742 |
+
|
| 743 |
+
trainer = PoEDistillTrainer(
|
| 744 |
+
model=student,
|
| 745 |
+
args=training_args,
|
| 746 |
+
train_dataset=train_dataset,
|
| 747 |
+
data_collator=DistillCollator(pad_token_id=tokenizer.pad_token_id),
|
| 748 |
+
tokenizer=tokenizer,
|
| 749 |
+
teacher_model=teacher,
|
| 750 |
+
beta_start=args.beta_start,
|
| 751 |
+
beta_end=args.beta_end,
|
| 752 |
+
beta_schedule_steps=args.beta_schedule_steps,
|
| 753 |
+
beta_schedule=args.beta_schedule,
|
| 754 |
+
beta_hold_steps=args.beta_hold_steps,
|
| 755 |
+
beta_transition_steps=args.beta_transition_steps,
|
| 756 |
+
hold_transition_schedule=args.hold_transition_schedule,
|
| 757 |
+
loss_type=args.loss_type,
|
| 758 |
+
advantage_normalization=args.advantage_normalization,
|
| 759 |
+
advantage_clip=args.advantage_clip,
|
| 760 |
+
positive_advantages_only=args.positive_advantages_only,
|
| 761 |
+
use_ppo_clip=args.use_ppo_clip,
|
| 762 |
+
ppo_clip_low=args.ppo_clip_low,
|
| 763 |
+
ppo_clip_high=args.ppo_clip_high,
|
| 764 |
+
sampled_loss_reduction=args.sampled_loss_reduction,
|
| 765 |
+
distill_chunk_size=args.distill_chunk_size,
|
| 766 |
+
callbacks=[
|
| 767 |
+
FreezeLoRABCallback(args.freeze_lora_b_after_step),
|
| 768 |
+
HoldThenTransitionLRCallback(
|
| 769 |
+
lr_start=args.lr_start,
|
| 770 |
+
lr_end=args.lr_end,
|
| 771 |
+
lr_hold_steps=args.lr_hold_steps,
|
| 772 |
+
lr_transition_steps=args.lr_transition_steps,
|
| 773 |
+
schedule=args.hold_transition_schedule,
|
| 774 |
+
),
|
| 775 |
+
],
|
| 776 |
+
)
|
| 777 |
+
trainer.train()
|
| 778 |
+
trainer.save_model(args.output_dir)
|
| 779 |
+
tokenizer.save_pretrained(args.output_dir)
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
if __name__ == "__main__":
|
| 783 |
+
main()
|
tools/convert_fsdp_to_hf.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import os
|
| 6 |
+
import pickle
|
| 7 |
+
import shutil
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import torch.distributed.checkpoint as dist_cp
|
| 12 |
+
from transformers import AutoConfig, AutoModelForCausalLM
|
| 13 |
+
from typing_extensions import override
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class UnpicklerWrapper(pickle.Unpickler):
|
| 17 |
+
@override
|
| 18 |
+
def find_class(self, mod_name, name):
|
| 19 |
+
class DummyClass:
|
| 20 |
+
def __init__(self, *args, **kwargs):
|
| 21 |
+
pass
|
| 22 |
+
|
| 23 |
+
if mod_name.startswith("megatron") or mod_name.startswith("glm"):
|
| 24 |
+
return DummyClass
|
| 25 |
+
return super().find_class(mod_name, name)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class WrappedStorageReader(dist_cp.FileSystemReader):
|
| 29 |
+
@override
|
| 30 |
+
def read_metadata(self):
|
| 31 |
+
path = self.fs.concat_path(self.path, ".metadata")
|
| 32 |
+
with self.fs.create_stream(path, "rb") as metadata_file:
|
| 33 |
+
metadata = UnpicklerWrapper(metadata_file).load()
|
| 34 |
+
if getattr(metadata, "storage_meta", None) is None:
|
| 35 |
+
metadata.storage_meta = dist_cp.StorageMeta()
|
| 36 |
+
metadata.storage_meta.load_id = self.load_id
|
| 37 |
+
if metadata.planner_data is None:
|
| 38 |
+
metadata.planner_data = {}
|
| 39 |
+
return metadata
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class EmptyStateDictLoadPlanner(dist_cp.default_planner.DefaultLoadPlanner):
|
| 43 |
+
@override
|
| 44 |
+
def set_up_planner(
|
| 45 |
+
self,
|
| 46 |
+
state_dict: dist_cp.metadata.STATE_DICT_TYPE,
|
| 47 |
+
metadata: dist_cp.metadata.Metadata | None = None,
|
| 48 |
+
is_coordinator: bool = False,
|
| 49 |
+
) -> None:
|
| 50 |
+
for k, v in metadata.state_dict_metadata.items():
|
| 51 |
+
if "optimizer" in k:
|
| 52 |
+
continue
|
| 53 |
+
print(f"find {k} in torch_dist ckpt")
|
| 54 |
+
if isinstance(v, dist_cp.metadata.TensorStorageMetadata):
|
| 55 |
+
v = torch.empty(v.size, dtype=v.properties.dtype) # type: ignore[assignment]
|
| 56 |
+
state_dict[k] = v
|
| 57 |
+
super().set_up_planner(state_dict, metadata, is_coordinator)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _detect_model_dir(input_dir: str) -> str:
|
| 61 |
+
model_dir = os.path.join(input_dir, "model")
|
| 62 |
+
return model_dir if os.path.isdir(model_dir) else input_dir
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _load_fsdp_state_dict(input_dir: str) -> dict[str, torch.Tensor]:
|
| 66 |
+
state_dict: dict[str, torch.Tensor] = {}
|
| 67 |
+
dist_cp.state_dict_loader._load_state_dict(
|
| 68 |
+
state_dict,
|
| 69 |
+
storage_reader=WrappedStorageReader(input_dir),
|
| 70 |
+
planner=EmptyStateDictLoadPlanner(),
|
| 71 |
+
no_dist=True,
|
| 72 |
+
)
|
| 73 |
+
return state_dict
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _get_candidate_prefixes(keys: list[str]) -> list[str]:
|
| 77 |
+
predefined = [
|
| 78 |
+
"model_state.model.",
|
| 79 |
+
"model_state.",
|
| 80 |
+
"model.",
|
| 81 |
+
"module.",
|
| 82 |
+
"",
|
| 83 |
+
]
|
| 84 |
+
|
| 85 |
+
detected: set[str] = set()
|
| 86 |
+
for key in keys:
|
| 87 |
+
for prefix in predefined:
|
| 88 |
+
if prefix and key.startswith(prefix):
|
| 89 |
+
detected.add(prefix)
|
| 90 |
+
|
| 91 |
+
# Always keep empty string as a fall back option for exact match.
|
| 92 |
+
detected.add("")
|
| 93 |
+
# Preserve predefined order while keeping only detected prefixes.
|
| 94 |
+
return [p for p in predefined if p in detected]
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _strip_best_prefix(keys: list[str], target_keys: set[str]) -> tuple[str, int]:
|
| 98 |
+
best_prefix = ""
|
| 99 |
+
best_match = -1
|
| 100 |
+
|
| 101 |
+
for prefix in _get_candidate_prefixes(keys):
|
| 102 |
+
mapped_keys = {k.removeprefix(prefix) for k in keys}
|
| 103 |
+
match_count = len(mapped_keys & target_keys)
|
| 104 |
+
if match_count > best_match:
|
| 105 |
+
best_match = match_count
|
| 106 |
+
best_prefix = prefix
|
| 107 |
+
|
| 108 |
+
return best_prefix, best_match
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _convert_fsdp_to_hf(
|
| 112 |
+
origin_hf_dir: str,
|
| 113 |
+
input_dir: str,
|
| 114 |
+
output_dir: str,
|
| 115 |
+
) -> None:
|
| 116 |
+
print(f"loading FSDP model from {input_dir}")
|
| 117 |
+
t = time.time()
|
| 118 |
+
state_dict = _load_fsdp_state_dict(input_dir)
|
| 119 |
+
print(f"FSDP model loaded in {time.time()-t:.2f} sec.")
|
| 120 |
+
|
| 121 |
+
tensor_items = {k: v for k, v in state_dict.items() if isinstance(v, torch.Tensor)}
|
| 122 |
+
|
| 123 |
+
config = AutoConfig.from_pretrained(origin_hf_dir, trust_remote_code=True)
|
| 124 |
+
hf_model = AutoModelForCausalLM.from_config(config)
|
| 125 |
+
target_keys = set(hf_model.state_dict().keys())
|
| 126 |
+
|
| 127 |
+
best_prefix, best_match = _strip_best_prefix(list(tensor_items.keys()), target_keys)
|
| 128 |
+
total_keys = len(tensor_items)
|
| 129 |
+
|
| 130 |
+
print(f"Using prefix '{best_prefix}' for key mapping. " f"Matched {best_match}/{total_keys} parameter keys.")
|
| 131 |
+
|
| 132 |
+
model_state = {k.removeprefix(best_prefix): v for k, v in tensor_items.items()}
|
| 133 |
+
|
| 134 |
+
if not model_state:
|
| 135 |
+
raise ValueError(
|
| 136 |
+
"No model weights found in checkpoint. "
|
| 137 |
+
"Please pass the checkpoint directory (e.g. iter_xxx or iter_xxx/model)."
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
missing, unexpected = hf_model.load_state_dict(model_state, strict=False)
|
| 141 |
+
print(f"Missing keys: {missing}\nUnexpected keys: {unexpected}")
|
| 142 |
+
|
| 143 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 144 |
+
hf_model.save_pretrained(output_dir, safe_serialization=True)
|
| 145 |
+
print(f"Model weights saved to {output_dir}")
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def copy_assets(origin_hf_dir: str, output_dir: str) -> None:
|
| 149 |
+
for filename in os.listdir(origin_hf_dir):
|
| 150 |
+
if filename == "model.safetensors.index.json" or filename.endswith(".safetensors"):
|
| 151 |
+
continue
|
| 152 |
+
origin_filename = os.path.join(origin_hf_dir, filename)
|
| 153 |
+
if not os.path.isfile(origin_filename):
|
| 154 |
+
print(f"Skip {filename}, not a file.")
|
| 155 |
+
continue
|
| 156 |
+
src, dst = origin_filename, os.path.join(output_dir, filename)
|
| 157 |
+
print(f"copy from {src} to {dst}")
|
| 158 |
+
shutil.copy(src, dst)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
if __name__ == "__main__":
|
| 162 |
+
parser = argparse.ArgumentParser()
|
| 163 |
+
parser.add_argument("--input-dir", type=str, required=True)
|
| 164 |
+
parser.add_argument("--output-dir", type=str, required=True)
|
| 165 |
+
parser.add_argument(
|
| 166 |
+
"--origin-hf-dir",
|
| 167 |
+
type=str,
|
| 168 |
+
required=True,
|
| 169 |
+
help="The original Hugging Face model directory to load config/tokenizer assets.",
|
| 170 |
+
)
|
| 171 |
+
parser.add_argument(
|
| 172 |
+
"-f", "--force", action="store_true", help="Force overwrite the output directory if it exists."
|
| 173 |
+
)
|
| 174 |
+
args = parser.parse_args()
|
| 175 |
+
|
| 176 |
+
if os.path.exists(args.output_dir) and not args.force:
|
| 177 |
+
raise ValueError(f"Output directory {args.output_dir} already exists. Use --force to overwrite it.")
|
| 178 |
+
|
| 179 |
+
model_dir = _detect_model_dir(args.input_dir)
|
| 180 |
+
_convert_fsdp_to_hf(args.origin_hf_dir, model_dir, args.output_dir)
|
| 181 |
+
copy_assets(args.origin_hf_dir, args.output_dir)
|
tools/convert_hf_to_fp8.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
python tools/convert_hf_to_fp8.py [-h] [--model-dir MODEL_DIR] [--save-dir SAVE_DIR] [--strategy {block,channel,tensor}] [--block-size [BLOCK_SIZE ...]]
|
| 6 |
+
[--max-workers MAX_WORKERS]
|
| 7 |
+
|
| 8 |
+
options:
|
| 9 |
+
-h, --help show this help message and exit
|
| 10 |
+
--model-dir MODEL_DIR
|
| 11 |
+
Path to the directory of the HF safetensors model.
|
| 12 |
+
--save-dir SAVE_DIR Path to the directory to save the converted model.
|
| 13 |
+
--strategy {block,channel,tensor}
|
| 14 |
+
--block-size [BLOCK_SIZE ...]
|
| 15 |
+
eg. --block-size 32 32
|
| 16 |
+
--max-workers MAX_WORKERS
|
| 17 |
+
Number of worker threads for parallel processing
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import argparse
|
| 21 |
+
import gc
|
| 22 |
+
import json
|
| 23 |
+
import os
|
| 24 |
+
import shutil
|
| 25 |
+
import threading
|
| 26 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 27 |
+
|
| 28 |
+
import safetensors
|
| 29 |
+
import safetensors.torch
|
| 30 |
+
import torch
|
| 31 |
+
import torch.nn.functional as F
|
| 32 |
+
from tqdm import tqdm
|
| 33 |
+
|
| 34 |
+
FP8_INFO = torch.finfo(torch.float8_e4m3fn)
|
| 35 |
+
FP8_MAX, FP8_MIN = FP8_INFO.max, FP8_INFO.min
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def ceildiv(a, b):
|
| 39 |
+
return -(-a // b)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def block_fp8(weight, block_size):
|
| 43 |
+
|
| 44 |
+
# per block quant
|
| 45 |
+
block_n, block_k = block_size[0], block_size[1]
|
| 46 |
+
|
| 47 |
+
shape_0, shape_1 = weight.shape
|
| 48 |
+
|
| 49 |
+
n_tiles = ceildiv(shape_0, block_n)
|
| 50 |
+
k_tiles = ceildiv(shape_1, block_k)
|
| 51 |
+
|
| 52 |
+
q_weight = F.pad(
|
| 53 |
+
weight,
|
| 54 |
+
(0, k_tiles * block_k - shape_1, 0, n_tiles * block_n - shape_0),
|
| 55 |
+
mode="constant",
|
| 56 |
+
value=0.0,
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
qweight = q_weight.reshape(n_tiles, block_n, k_tiles, block_k)
|
| 60 |
+
block_max = torch.max(torch.abs(qweight), dim=1, keepdim=True)[0]
|
| 61 |
+
block_max = torch.max(block_max, dim=3, keepdim=True)[0]
|
| 62 |
+
|
| 63 |
+
scale = block_max.to(torch.float32) / FP8_MAX
|
| 64 |
+
qweight = (
|
| 65 |
+
(qweight / scale)
|
| 66 |
+
.clamp(min=FP8_MIN, max=FP8_MAX)
|
| 67 |
+
.reshape((n_tiles * block_n, k_tiles * block_k))
|
| 68 |
+
.to(torch.float8_e4m3fn)
|
| 69 |
+
)
|
| 70 |
+
qweight = qweight[:shape_0, :shape_1].clone().detach()
|
| 71 |
+
scale = scale.squeeze()
|
| 72 |
+
|
| 73 |
+
return qweight, scale
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def channel_fp8(weight):
|
| 77 |
+
channel_max = torch.max(weight.abs(), dim=-1, keepdim=True)[0]
|
| 78 |
+
scale = channel_max.clamp(min=1e-12).to(torch.float32) / FP8_MAX
|
| 79 |
+
qweight = (weight / scale).clamp(min=FP8_MIN, max=FP8_MAX)
|
| 80 |
+
qweight = qweight.to(torch.float8_e4m3fn)
|
| 81 |
+
return qweight, scale
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def tensor_fp8(weight):
|
| 85 |
+
scale = weight.abs().max().clamp(min=1e-12).to(torch.float32) / FP8_MAX
|
| 86 |
+
qweight = (weight / scale).clamp(min=FP8_MIN, max=FP8_MAX)
|
| 87 |
+
qweight = qweight.to(torch.float8_e4m3fn)
|
| 88 |
+
scale = scale.view(1)
|
| 89 |
+
return qweight, scale
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def quant_fp8(weight, strategy, block_size=None):
|
| 93 |
+
if strategy == "tensor":
|
| 94 |
+
return tensor_fp8(weight)
|
| 95 |
+
elif strategy == "channel":
|
| 96 |
+
return channel_fp8(weight)
|
| 97 |
+
else:
|
| 98 |
+
return block_fp8(weight, block_size)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
class ConversionResult:
|
| 102 |
+
def __init__(self):
|
| 103 |
+
self.lock = threading.Lock()
|
| 104 |
+
self.weight_map = {}
|
| 105 |
+
self.param_count = 0
|
| 106 |
+
self.modules_to_not_convert = []
|
| 107 |
+
|
| 108 |
+
def add_result(self, filename, q_weights, module_names):
|
| 109 |
+
with self.lock:
|
| 110 |
+
for k, v in q_weights.items():
|
| 111 |
+
self.weight_map[k] = filename
|
| 112 |
+
self.param_count += len(v)
|
| 113 |
+
self.modules_to_not_convert.extend(module_names)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def process_file(input_path, output_path, filename, strategy, block_size, result_collector):
|
| 117 |
+
if not filename.endswith(".safetensors"):
|
| 118 |
+
return
|
| 119 |
+
|
| 120 |
+
print(f"Processing {filename}, memory usage: {torch.cuda.memory_allocated()}")
|
| 121 |
+
weights = {}
|
| 122 |
+
q_weights = {}
|
| 123 |
+
|
| 124 |
+
with safetensors.safe_open(os.path.join(input_path, filename), framework="pt", device="cuda") as f:
|
| 125 |
+
for k in f.keys():
|
| 126 |
+
weights[k] = f.get_tensor(k)
|
| 127 |
+
|
| 128 |
+
modules_to_not_convert = []
|
| 129 |
+
for key in weights.keys():
|
| 130 |
+
if (
|
| 131 |
+
"weight" in key
|
| 132 |
+
and "layernorm" not in key
|
| 133 |
+
and "embed" not in key
|
| 134 |
+
and "router" not in key
|
| 135 |
+
and "mlp.gate." not in key
|
| 136 |
+
and "norm" not in key
|
| 137 |
+
and "lm_head" not in key
|
| 138 |
+
and "eh_proj" not in key
|
| 139 |
+
):
|
| 140 |
+
qw, s = quant_fp8(weights[key], strategy, block_size)
|
| 141 |
+
q_weights[key] = qw
|
| 142 |
+
if block_size:
|
| 143 |
+
scale_name = key.replace(".weight", ".weight_scale_inv")
|
| 144 |
+
else:
|
| 145 |
+
scale_name = key.replace(".weight", ".weight_scale")
|
| 146 |
+
q_weights[scale_name] = s
|
| 147 |
+
else:
|
| 148 |
+
modules_to_not_convert.append(key.replace(".weight", ""))
|
| 149 |
+
q_weights[key] = weights[key]
|
| 150 |
+
|
| 151 |
+
safetensors.torch.save_file(q_weights, os.path.join(output_path, filename), metadata={"format": "pt"})
|
| 152 |
+
|
| 153 |
+
result_collector.add_result(filename, q_weights, modules_to_not_convert)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def convert_fp8(input_path, output_path, strategy, block_size=None, max_workers=4):
|
| 157 |
+
input_path = os.path.abspath(input_path)
|
| 158 |
+
os.makedirs(output_path, exist_ok=True)
|
| 159 |
+
|
| 160 |
+
for filename in os.listdir(input_path):
|
| 161 |
+
if not filename.endswith(".safetensors") and not os.path.isdir(os.path.join(input_path, filename)):
|
| 162 |
+
shutil.copyfile(os.path.join(input_path, filename), os.path.join(output_path, filename))
|
| 163 |
+
|
| 164 |
+
safetensors_files = [f for f in os.listdir(input_path) if f.endswith(".safetensors")]
|
| 165 |
+
|
| 166 |
+
result_collector = ConversionResult()
|
| 167 |
+
|
| 168 |
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
| 169 |
+
futures = []
|
| 170 |
+
for filename in safetensors_files:
|
| 171 |
+
future = executor.submit(
|
| 172 |
+
process_file, input_path, output_path, filename, strategy, block_size, result_collector
|
| 173 |
+
)
|
| 174 |
+
futures.append(future)
|
| 175 |
+
|
| 176 |
+
for future in tqdm(futures, desc="Processing files"):
|
| 177 |
+
future.result()
|
| 178 |
+
|
| 179 |
+
if strategy == "block" or strategy == "tensor":
|
| 180 |
+
quantization_config = {
|
| 181 |
+
"activation_scheme": "dynamic",
|
| 182 |
+
"fmt": "e4m3",
|
| 183 |
+
"quant_method": "fp8",
|
| 184 |
+
}
|
| 185 |
+
if block_size:
|
| 186 |
+
quantization_config["weight_block_size"] = block_size
|
| 187 |
+
if len(result_collector.modules_to_not_convert) > 0:
|
| 188 |
+
quantization_config["modules_to_not_convert"] = list(set(result_collector.modules_to_not_convert))
|
| 189 |
+
else:
|
| 190 |
+
quant_group = {
|
| 191 |
+
"group_0": {
|
| 192 |
+
"input_activations": {
|
| 193 |
+
"actorder": None,
|
| 194 |
+
"block_structure": None,
|
| 195 |
+
"dynamic": True,
|
| 196 |
+
"group_size": None,
|
| 197 |
+
"num_bits": 8,
|
| 198 |
+
"observer": None,
|
| 199 |
+
"observer_kwargs": {},
|
| 200 |
+
"strategy": "token",
|
| 201 |
+
"symmetric": True,
|
| 202 |
+
"type": "float",
|
| 203 |
+
},
|
| 204 |
+
"output_activations": None,
|
| 205 |
+
"targets": ["Linear"],
|
| 206 |
+
"weights": {
|
| 207 |
+
"actorder": None,
|
| 208 |
+
"block_structure": None,
|
| 209 |
+
"dynamic": False,
|
| 210 |
+
"group_size": None,
|
| 211 |
+
"num_bits": 8,
|
| 212 |
+
"observer": "minmax",
|
| 213 |
+
"observer_kwargs": {},
|
| 214 |
+
"strategy": strategy,
|
| 215 |
+
"symmetric": True,
|
| 216 |
+
"type": "float",
|
| 217 |
+
},
|
| 218 |
+
},
|
| 219 |
+
}
|
| 220 |
+
quantization_config = {
|
| 221 |
+
"config_groups": quant_group,
|
| 222 |
+
"format": "float-quantized",
|
| 223 |
+
"ignore": list(set(result_collector.modules_to_not_convert)),
|
| 224 |
+
"quant_method": "compressed-tensors",
|
| 225 |
+
"quantization_status": "compressed",
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
config_path = os.path.join(input_path, "config.json")
|
| 229 |
+
if os.path.exists(config_path):
|
| 230 |
+
cfg = json.load(open(config_path))
|
| 231 |
+
cfg["quantization_config"] = quantization_config
|
| 232 |
+
json.dump(cfg, open(os.path.join(output_path, "config.json"), "w"), indent=2)
|
| 233 |
+
|
| 234 |
+
index_dict = {"weight_map": result_collector.weight_map, "metadata": {"total_size": result_collector.param_count}}
|
| 235 |
+
json.dump(index_dict, open(os.path.join(output_path, "model.safetensors.index.json"), "w"), indent=2)
|
| 236 |
+
|
| 237 |
+
gc.collect()
|
| 238 |
+
torch.cuda.empty_cache()
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
if __name__ == "__main__":
|
| 242 |
+
parser = argparse.ArgumentParser()
|
| 243 |
+
parser.add_argument("--model-dir", type=str, help="Path to the directory of the HF safetensors model.")
|
| 244 |
+
parser.add_argument("--save-dir", type=str, help="Path to the directory to save the converted model.")
|
| 245 |
+
parser.add_argument("--strategy", type=str, default="block", choices=["block", "channel", "tensor"])
|
| 246 |
+
parser.add_argument("--block-size", type=int, nargs="*", default=None, help="eg. --block-size 32 32")
|
| 247 |
+
parser.add_argument("--max-workers", type=int, default=1, help="Number of worker threads for parallel processing")
|
| 248 |
+
args = parser.parse_args()
|
| 249 |
+
|
| 250 |
+
if not os.path.exists(args.save_dir):
|
| 251 |
+
print(f"Creating directory {args.save_dir}")
|
| 252 |
+
os.makedirs(args.save_dir)
|
| 253 |
+
elif not os.path.isdir(args.save_dir):
|
| 254 |
+
raise ValueError("The save_dir should be a directory.")
|
| 255 |
+
|
| 256 |
+
convert_fp8(args.model_dir, args.save_dir, args.strategy, args.block_size, args.max_workers)
|
tools/convert_hf_to_torch_dist.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
import gc
|
| 5 |
+
import os
|
| 6 |
+
import shutil
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.distributed as dist
|
| 10 |
+
from megatron.core.enums import ModelType
|
| 11 |
+
from megatron.training.arguments import parse_args, validate_args
|
| 12 |
+
from megatron.training.checkpointing import get_checkpoint_name, get_checkpoint_tracker_filename, save_checkpoint
|
| 13 |
+
from megatron.training.training import get_model
|
| 14 |
+
|
| 15 |
+
import slime_plugins.mbridge # noqa: F401
|
| 16 |
+
from mbridge import AutoBridge
|
| 17 |
+
from slime.backends.megatron_utils.arguments import set_default_megatron_args
|
| 18 |
+
from slime.backends.megatron_utils.initialize import init
|
| 19 |
+
from slime.backends.megatron_utils.model_provider import get_model_provider_func
|
| 20 |
+
from slime.utils.logging_utils import configure_logger
|
| 21 |
+
from slime.utils.memory_utils import print_memory
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def add_convertion_args(parser):
|
| 25 |
+
"""Add conversion arguments to the parser"""
|
| 26 |
+
parser.add_argument("--hf-checkpoint", type=str, required=True, help="HuggingFace model path")
|
| 27 |
+
parser.add_argument(
|
| 28 |
+
"--megatron-to-hf-mode",
|
| 29 |
+
choices=["raw", "bridge"],
|
| 30 |
+
default="raw",
|
| 31 |
+
help="The method to convert megatron weights to hugging face weights for SGLang.",
|
| 32 |
+
)
|
| 33 |
+
try:
|
| 34 |
+
parser.add_argument("--padded-vocab-size", type=int, default=None)
|
| 35 |
+
except Exception:
|
| 36 |
+
pass
|
| 37 |
+
return parser
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def get_args():
|
| 41 |
+
args = parse_args(add_convertion_args)
|
| 42 |
+
args = set_default_megatron_args(args)
|
| 43 |
+
|
| 44 |
+
# set to pass megatron validate_args
|
| 45 |
+
args.save_interval = 1
|
| 46 |
+
args.micro_batch_size = 1
|
| 47 |
+
world_size = int(os.environ.get("WORLD_SIZE", "1"))
|
| 48 |
+
args.global_batch_size = int(os.environ.get("WORLD_SIZE", "1"))
|
| 49 |
+
|
| 50 |
+
assert world_size <= args.num_layers, (
|
| 51 |
+
f"World size {world_size} must be less than or equal to number of layers {args.num_layers}. "
|
| 52 |
+
"You are using too many GPUs for this conversion."
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
def ceildiv(a, b):
|
| 56 |
+
return -(a // -b)
|
| 57 |
+
|
| 58 |
+
if args.pipeline_model_parallel_size == 1 and world_size > 1:
|
| 59 |
+
pp_size = world_size
|
| 60 |
+
while True:
|
| 61 |
+
args.pipeline_model_parallel_size = pp_size
|
| 62 |
+
args.decoder_last_pipeline_num_layers = args.num_layers - ceildiv(
|
| 63 |
+
args.num_layers, args.pipeline_model_parallel_size
|
| 64 |
+
) * (args.pipeline_model_parallel_size - 1)
|
| 65 |
+
|
| 66 |
+
if args.decoder_last_pipeline_num_layers > 0:
|
| 67 |
+
break
|
| 68 |
+
|
| 69 |
+
if pp_size % 2 == 0:
|
| 70 |
+
pp_size //= 2
|
| 71 |
+
else:
|
| 72 |
+
raise ValueError(
|
| 73 |
+
f"Cannot find a valid pipeline model parallel size for {args.num_layers} layers and {world_size} GPUs."
|
| 74 |
+
)
|
| 75 |
+
print(
|
| 76 |
+
f"Using pipeline model parallel size: {args.pipeline_model_parallel_size}, decoder last pipeline num layers: {args.decoder_last_pipeline_num_layers}"
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
validate_args(args)
|
| 80 |
+
return args
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def main():
|
| 84 |
+
if torch.version.hip:
|
| 85 |
+
import megatron.core.dist_checkpointing.strategies.filesystem_async as filesystem_async_module
|
| 86 |
+
from slime.utils.rocm_checkpoint_writer import ROCmFileSystemWriterAsync
|
| 87 |
+
|
| 88 |
+
filesystem_async_module.FileSystemWriterAsync = ROCmFileSystemWriterAsync
|
| 89 |
+
print("[ROCm] Applied FileSystemWriterAsync patch for HIP compatibility")
|
| 90 |
+
|
| 91 |
+
configure_logger()
|
| 92 |
+
|
| 93 |
+
# Initialize distributed environment
|
| 94 |
+
world_size = int(os.getenv("WORLD_SIZE") or os.getenv("SLURM_NTASKS") or 1)
|
| 95 |
+
local_rank = int(os.getenv("LOCAL_RANK") or os.getenv("SLURM_LOCALID") or 0)
|
| 96 |
+
global_rank = int(os.getenv("RANK") or os.getenv("SLURM_PROCID") or 0)
|
| 97 |
+
|
| 98 |
+
torch.cuda.set_device(local_rank)
|
| 99 |
+
os.environ.setdefault("WORLD_SIZE", str(world_size))
|
| 100 |
+
os.environ.setdefault("RANK", str(global_rank))
|
| 101 |
+
os.environ.setdefault("LOCAL_RANK", str(local_rank))
|
| 102 |
+
os.environ.setdefault("MASTER_ADDR", "localhost")
|
| 103 |
+
os.environ.setdefault("MASTER_PORT", "12355")
|
| 104 |
+
dist.init_process_group(
|
| 105 |
+
backend="nccl",
|
| 106 |
+
world_size=world_size,
|
| 107 |
+
rank=global_rank,
|
| 108 |
+
device_id=torch.device(f"cuda:{local_rank}"),
|
| 109 |
+
)
|
| 110 |
+
args = get_args()
|
| 111 |
+
init(args)
|
| 112 |
+
model = get_model(get_model_provider_func(args), ModelType.encoder_or_decoder, wrap_with_ddp=False)
|
| 113 |
+
|
| 114 |
+
# Load model
|
| 115 |
+
hf_model_path = args.hf_checkpoint
|
| 116 |
+
bridge = AutoBridge.from_pretrained(hf_model_path, trust_remote_code=True)
|
| 117 |
+
bridge.load_weights(model, hf_model_path, memory_efficient=True)
|
| 118 |
+
print(f"Model loaded: {hf_model_path}")
|
| 119 |
+
|
| 120 |
+
print_memory("after loading model")
|
| 121 |
+
torch.cuda.synchronize()
|
| 122 |
+
gc.collect()
|
| 123 |
+
torch.cuda.empty_cache()
|
| 124 |
+
|
| 125 |
+
save_checkpoint(1, model, None, None, 0)
|
| 126 |
+
|
| 127 |
+
if dist.get_rank() == 0:
|
| 128 |
+
# change to release ckpt
|
| 129 |
+
tracker_filename = get_checkpoint_tracker_filename(args.save)
|
| 130 |
+
with open(tracker_filename, "w") as f:
|
| 131 |
+
f.write("release")
|
| 132 |
+
source_dir = get_checkpoint_name(args.save, 1, False, return_base_dir=True)
|
| 133 |
+
target_dir = get_checkpoint_name(args.save, -1, True, return_base_dir=True)
|
| 134 |
+
shutil.move(source_dir, target_dir)
|
| 135 |
+
dist.barrier()
|
| 136 |
+
dist.destroy_process_group()
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
if __name__ == "__main__":
|
| 140 |
+
main()
|
tools/convert_k2_thinking_int4_to_bf16.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Usage:
|
| 6 |
+
------
|
| 7 |
+
python convert_k2_thinking_int4_to_bf16.py [-h] --model-dir MODEL_DIR [--output-dir OUTPUT_DIR]
|
| 8 |
+
[--files FILE [FILE ...]] [--config-path CONFIG_PATH]
|
| 9 |
+
[--overwrite]
|
| 10 |
+
options:
|
| 11 |
+
-h, --help Show this help message and exit.
|
| 12 |
+
--model-dir MODEL_DIR Path to the directory of the HF safetensors quantized model.
|
| 13 |
+
--output-dir OUTPUT_DIR
|
| 14 |
+
Path to the directory to save the converted BF16 model.
|
| 15 |
+
Default: <model-dir>_bf16
|
| 16 |
+
--files FILE [FILE ...]
|
| 17 |
+
Specific safetensors filenames to convert (relative to model-dir).
|
| 18 |
+
Convert all if omitted.
|
| 19 |
+
--config-path CONFIG_PATH
|
| 20 |
+
Path to config.json to extract group_size (default: model-dir/config.json).
|
| 21 |
+
--overwrite Rewrite output files even if they already exist.
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
Example:
|
| 25 |
+
--------
|
| 26 |
+
python convert_k2_thinking_int4_to_bf16.py --model-dir /Kimi-K2-Thinking --output-dir /Kimi-K2-Thinking-bf16
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
import argparse
|
| 30 |
+
import json
|
| 31 |
+
import os
|
| 32 |
+
import shutil
|
| 33 |
+
from collections import defaultdict
|
| 34 |
+
|
| 35 |
+
import torch
|
| 36 |
+
from compressed_tensors.compressors import unpack_from_int32
|
| 37 |
+
from safetensors.torch import safe_open, save_file
|
| 38 |
+
from tqdm import tqdm
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _load_config(model_dir: str, config_path: str | None) -> tuple[int, int, int]:
|
| 42 |
+
"""Read config.json and return hidden_size, inter_size, and group_size."""
|
| 43 |
+
cfg_path = config_path or os.path.join(model_dir, "config.json")
|
| 44 |
+
with open(cfg_path) as f:
|
| 45 |
+
cfg = json.load(f)
|
| 46 |
+
hidden_size = int(cfg.get("hidden_size"))
|
| 47 |
+
inter_size = int(cfg.get("moe_intermediate_size"))
|
| 48 |
+
group_size = int(
|
| 49 |
+
cfg.get("quantization_config", {})
|
| 50 |
+
.get("config_groups", {})
|
| 51 |
+
.get("group_0", {})
|
| 52 |
+
.get("weights", {})
|
| 53 |
+
.get("group_size", 128)
|
| 54 |
+
)
|
| 55 |
+
return hidden_size, inter_size, group_size
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _dequantize_tensor(
|
| 59 |
+
weight_packed: torch.Tensor,
|
| 60 |
+
weight_scale: torch.Tensor,
|
| 61 |
+
weight_shape: torch.Tensor,
|
| 62 |
+
group_size: int,
|
| 63 |
+
) -> torch.Tensor:
|
| 64 |
+
"""Unpack int32 quantized tensor and multiply with scales to create BF16 tensor."""
|
| 65 |
+
if isinstance(weight_shape, torch.Tensor):
|
| 66 |
+
shape = tuple(int(v) for v in weight_shape.view(-1).tolist())
|
| 67 |
+
else:
|
| 68 |
+
shape = tuple(weight_shape)
|
| 69 |
+
|
| 70 |
+
weight = unpack_from_int32(weight_packed, 4, shape)
|
| 71 |
+
|
| 72 |
+
if group_size > 0:
|
| 73 |
+
scale = weight_scale.to(torch.float32)
|
| 74 |
+
if scale.dim() == 1:
|
| 75 |
+
scale = scale.unsqueeze(1)
|
| 76 |
+
scales = torch.repeat_interleave(scale, repeats=group_size, dim=1)
|
| 77 |
+
else:
|
| 78 |
+
scales = weight_scale.to(torch.float32)
|
| 79 |
+
|
| 80 |
+
if scales.shape != weight.shape:
|
| 81 |
+
if scales.numel() == weight.numel():
|
| 82 |
+
scales = scales.reshape_as(weight)
|
| 83 |
+
else:
|
| 84 |
+
raise ValueError(f"Scale shape {scales.shape} incompatible with weight shape {weight.shape}")
|
| 85 |
+
|
| 86 |
+
bf16 = (weight.to(torch.float32) * scales).to(torch.bfloat16)
|
| 87 |
+
return bf16.contiguous()
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _is_quantized_weight_key(key: str) -> bool:
|
| 91 |
+
"""Check if the key is a quantized MoE expert weight key."""
|
| 92 |
+
if ".mlp.experts." not in key or ".shared_experts." in key:
|
| 93 |
+
return False
|
| 94 |
+
suffixes = ("weight_packed", "weight_scale", "weight_shape")
|
| 95 |
+
for proj in ("gate_proj", "up_proj", "down_proj"):
|
| 96 |
+
for suffix in suffixes:
|
| 97 |
+
if key.endswith(f".{proj}.{suffix}"):
|
| 98 |
+
return True
|
| 99 |
+
return False
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def convert_file(
|
| 103 |
+
input_path: str,
|
| 104 |
+
output_path: str,
|
| 105 |
+
group_size: int,
|
| 106 |
+
skip_existing: bool = True,
|
| 107 |
+
):
|
| 108 |
+
"""Convert a single safetensors file from quantized format to BF16 (GPU accelerated)."""
|
| 109 |
+
if skip_existing and os.path.exists(output_path):
|
| 110 |
+
return
|
| 111 |
+
|
| 112 |
+
tensors = {}
|
| 113 |
+
expert_buffers = defaultdict(lambda: defaultdict(dict))
|
| 114 |
+
|
| 115 |
+
# Load weights directly on GPU
|
| 116 |
+
with safe_open(input_path, framework="pt", device="cuda") as reader:
|
| 117 |
+
keys = list(reader.keys())
|
| 118 |
+
for key in keys:
|
| 119 |
+
tensor = reader.get_tensor(key)
|
| 120 |
+
if not _is_quantized_weight_key(key):
|
| 121 |
+
tensors[key] = tensor
|
| 122 |
+
continue
|
| 123 |
+
parts = key.split(".")
|
| 124 |
+
try:
|
| 125 |
+
expert_idx = parts.index("experts")
|
| 126 |
+
except ValueError:
|
| 127 |
+
tensors[key] = tensor
|
| 128 |
+
continue
|
| 129 |
+
prefix = ".".join(parts[: expert_idx + 2])
|
| 130 |
+
project = parts[-2]
|
| 131 |
+
suffix = parts[-1]
|
| 132 |
+
expert_buffers[prefix][project][suffix] = tensor
|
| 133 |
+
|
| 134 |
+
# Convert quantized weights
|
| 135 |
+
for prefix, components in expert_buffers.items():
|
| 136 |
+
for proj_name in ["gate_proj", "up_proj", "down_proj"]:
|
| 137 |
+
proj_data = components.get(proj_name, {})
|
| 138 |
+
required = {"weight_packed", "weight_scale", "weight_shape"}
|
| 139 |
+
|
| 140 |
+
if not required.issubset(proj_data.keys()):
|
| 141 |
+
# Keep quantized tensors if incomplete
|
| 142 |
+
for suffix, value in proj_data.items():
|
| 143 |
+
tensors[f"{prefix}.{proj_name}.{suffix}"] = value
|
| 144 |
+
continue
|
| 145 |
+
|
| 146 |
+
# Dequantize to BF16
|
| 147 |
+
bf16_weight = _dequantize_tensor(
|
| 148 |
+
proj_data["weight_packed"].to(torch.int32),
|
| 149 |
+
proj_data["weight_scale"].to(torch.float32),
|
| 150 |
+
proj_data["weight_shape"],
|
| 151 |
+
group_size,
|
| 152 |
+
)
|
| 153 |
+
tensors[f"{prefix}.{proj_name}.weight"] = bf16_weight.to(torch.bfloat16)
|
| 154 |
+
|
| 155 |
+
# Save converted file (moved to CPU for compatibility)
|
| 156 |
+
cpu_tensors = {k: v.cpu() for k, v in tensors.items()}
|
| 157 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 158 |
+
save_file(cpu_tensors, output_path)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def parse_args() -> argparse.Namespace:
|
| 162 |
+
parser = argparse.ArgumentParser(description="Convert GPTQ MoE experts to BF16 weights.")
|
| 163 |
+
parser.add_argument("--model-dir", required=True, help="Directory containing safetensors checkpoints.")
|
| 164 |
+
parser.add_argument(
|
| 165 |
+
"--output-dir",
|
| 166 |
+
default=None,
|
| 167 |
+
help="Destination BF16 model directory (default: <model-dir>_bf16).",
|
| 168 |
+
)
|
| 169 |
+
parser.add_argument(
|
| 170 |
+
"--files",
|
| 171 |
+
nargs="+",
|
| 172 |
+
default=None,
|
| 173 |
+
help="Optional specific safetensor files to convert.",
|
| 174 |
+
)
|
| 175 |
+
parser.add_argument(
|
| 176 |
+
"--config-path",
|
| 177 |
+
default=None,
|
| 178 |
+
help="Path to config.json if not in model-dir.",
|
| 179 |
+
)
|
| 180 |
+
parser.add_argument(
|
| 181 |
+
"--overwrite",
|
| 182 |
+
action="store_true",
|
| 183 |
+
help="Overwrite existing BF16 files.",
|
| 184 |
+
)
|
| 185 |
+
return parser.parse_args()
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def main():
|
| 189 |
+
args = parse_args()
|
| 190 |
+
model_dir = os.path.abspath(args.model_dir)
|
| 191 |
+
output_dir = os.path.abspath(args.output_dir or f"{model_dir}_bf16")
|
| 192 |
+
|
| 193 |
+
if not os.path.isdir(model_dir):
|
| 194 |
+
raise FileNotFoundError(f"Model directory not found: {model_dir}")
|
| 195 |
+
|
| 196 |
+
_, _, group_size = _load_config(model_dir, args.config_path)
|
| 197 |
+
|
| 198 |
+
# Collect target files
|
| 199 |
+
if args.files:
|
| 200 |
+
targets = [os.path.join(model_dir, fname) for fname in args.files]
|
| 201 |
+
else:
|
| 202 |
+
targets = [
|
| 203 |
+
os.path.join(model_dir, name) for name in sorted(os.listdir(model_dir)) if name.endswith(".safetensors")
|
| 204 |
+
]
|
| 205 |
+
|
| 206 |
+
if not targets:
|
| 207 |
+
print("No safetensors checkpoints found.")
|
| 208 |
+
return
|
| 209 |
+
|
| 210 |
+
# Convert with progress bar
|
| 211 |
+
for path in tqdm(targets, desc="Converting weights", unit="file"):
|
| 212 |
+
if not os.path.isfile(path):
|
| 213 |
+
continue
|
| 214 |
+
rel = os.path.relpath(path, model_dir)
|
| 215 |
+
output_path = os.path.join(output_dir, rel)
|
| 216 |
+
convert_file(path, output_path, group_size, skip_existing=not args.overwrite)
|
| 217 |
+
|
| 218 |
+
# Copy config/json/py/tokenizer
|
| 219 |
+
for fname in os.listdir(model_dir):
|
| 220 |
+
src_path = os.path.join(model_dir, fname)
|
| 221 |
+
dst_path = os.path.join(output_dir, fname)
|
| 222 |
+
if fname == "model.safetensors.index.json":
|
| 223 |
+
continue
|
| 224 |
+
if fname.endswith(".json") or fname.endswith(".py") or fname.startswith("tokenizer"):
|
| 225 |
+
shutil.copy2(src_path, dst_path)
|
| 226 |
+
|
| 227 |
+
# Generate new index
|
| 228 |
+
new_index_path = os.path.join(output_dir, "model.safetensors.index.json")
|
| 229 |
+
weight_map = {}
|
| 230 |
+
for fname in sorted(os.listdir(output_dir)):
|
| 231 |
+
if not fname.endswith(".safetensors"):
|
| 232 |
+
continue
|
| 233 |
+
safetensor_path = os.path.join(output_dir, fname)
|
| 234 |
+
with safe_open(safetensor_path, framework="pt") as reader:
|
| 235 |
+
for key in reader.keys():
|
| 236 |
+
weight_map[key] = fname
|
| 237 |
+
|
| 238 |
+
with open(new_index_path, "w") as f:
|
| 239 |
+
json.dump({"metadata": {}, "weight_map": weight_map}, f, indent=2)
|
| 240 |
+
|
| 241 |
+
print(f"\nSuccessful! Output saved to: {output_dir}")
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
if __name__ == "__main__":
|
| 245 |
+
main()
|
tools/convert_to_hf.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.distributed as dist
|
| 6 |
+
from megatron.core import mpu
|
| 7 |
+
from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer
|
| 8 |
+
|
| 9 |
+
import slime.backends.megatron_utils as megatron_utils
|
| 10 |
+
from slime.backends.megatron_utils import update_weight_utils
|
| 11 |
+
from slime.utils.arguments import parse_args
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def add_checkpoint_args(parser):
|
| 15 |
+
parser.add_argument(
|
| 16 |
+
"--output-dir",
|
| 17 |
+
type=str,
|
| 18 |
+
default=None,
|
| 19 |
+
help="Directory to save the converted HF model.",
|
| 20 |
+
)
|
| 21 |
+
parser.add_argument(
|
| 22 |
+
"--check-same",
|
| 23 |
+
action="store_true",
|
| 24 |
+
default=False,
|
| 25 |
+
help="Check if the converted model is the same as the original model.",
|
| 26 |
+
)
|
| 27 |
+
return parser
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def main(args):
|
| 31 |
+
megatron_utils.init(args)
|
| 32 |
+
|
| 33 |
+
pp_size = mpu.get_pipeline_model_parallel_world_size()
|
| 34 |
+
ep_size = mpu.get_expert_model_parallel_world_size()
|
| 35 |
+
|
| 36 |
+
is_save_rank = (
|
| 37 |
+
mpu.get_data_parallel_rank(with_context_parallel=True) == 0 and mpu.get_tensor_model_parallel_rank() == 0
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# Setup the model and optimizer
|
| 41 |
+
args.no_load_optim = True
|
| 42 |
+
args.no_load_rng = True
|
| 43 |
+
model, _, _, _ = megatron_utils.initialize_model_and_optimizer(args)
|
| 44 |
+
|
| 45 |
+
hf_config = AutoConfig.from_pretrained(args.hf_checkpoint, trust_remote_code=True)
|
| 46 |
+
model_name = type(hf_config).__name__.lower()
|
| 47 |
+
|
| 48 |
+
tokenizer = AutoTokenizer.from_pretrained(args.hf_checkpoint, trust_remote_code=True)
|
| 49 |
+
|
| 50 |
+
vocab_size = tokenizer.vocab_size if args.vocab_size is None else args.vocab_size
|
| 51 |
+
|
| 52 |
+
param_infos = update_weight_utils.get_param_infos(args, model)
|
| 53 |
+
|
| 54 |
+
state_dict = {}
|
| 55 |
+
rank = dist.get_rank()
|
| 56 |
+
for info in param_infos:
|
| 57 |
+
if dist.get_rank() == info.src_rank:
|
| 58 |
+
for name_, param_ in update_weight_utils.named_parameters(args, model):
|
| 59 |
+
if name_ == info.name:
|
| 60 |
+
param = param_
|
| 61 |
+
break
|
| 62 |
+
else:
|
| 63 |
+
param = torch.empty(info.shape, dtype=info.dtype, device=torch.cuda.current_device())
|
| 64 |
+
|
| 65 |
+
if pp_size > 1:
|
| 66 |
+
if info.src_rank in dist.get_process_group_ranks(mpu.get_pipeline_model_parallel_group()):
|
| 67 |
+
torch.distributed.broadcast(param, src=info.src_rank, group=mpu.get_pipeline_model_parallel_group())
|
| 68 |
+
|
| 69 |
+
# broadcast params across ep ranks
|
| 70 |
+
if ep_size > 1:
|
| 71 |
+
if ".experts." in info.name:
|
| 72 |
+
src_rank = (
|
| 73 |
+
info.src_rank
|
| 74 |
+
if info.src_rank in dist.get_process_group_ranks(mpu.get_expert_model_parallel_group())
|
| 75 |
+
else rank
|
| 76 |
+
)
|
| 77 |
+
torch.distributed.broadcast(param, src=src_rank, group=mpu.get_expert_model_parallel_group())
|
| 78 |
+
|
| 79 |
+
for key, value in info.attrs.items():
|
| 80 |
+
setattr(param, key, value)
|
| 81 |
+
|
| 82 |
+
param = update_weight_utils.all_gather_param(info.name, param)
|
| 83 |
+
param = update_weight_utils.remove_padding(info.name, param, vocab_size)
|
| 84 |
+
# use torch.distributed
|
| 85 |
+
if is_save_rank:
|
| 86 |
+
converted_named_tensors = update_weight_utils.convert_to_hf(args, model_name, info.name, param)
|
| 87 |
+
for name, param in converted_named_tensors:
|
| 88 |
+
state_dict[name] = param.cpu()
|
| 89 |
+
del param
|
| 90 |
+
|
| 91 |
+
if is_save_rank:
|
| 92 |
+
hf_model = AutoModelForCausalLM.from_pretrained(
|
| 93 |
+
args.hf_checkpoint, torch_dtype="auto", device_map="cpu", trust_remote_code=True
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
if args.check_same:
|
| 97 |
+
for name, param in hf_model.named_parameters():
|
| 98 |
+
if name in state_dict:
|
| 99 |
+
assert (
|
| 100 |
+
param.shape == state_dict[name].shape
|
| 101 |
+
), f"Shape mismatch for {name}: {param.shape} vs {state_dict[name].shape}"
|
| 102 |
+
assert torch.all(param == state_dict[name]), f"Value mismatch for {name}"
|
| 103 |
+
else:
|
| 104 |
+
print(f"Warning: {name} not found in state_dict")
|
| 105 |
+
|
| 106 |
+
if args.output_dir:
|
| 107 |
+
tokenizer.save_pretrained(args.output_dir)
|
| 108 |
+
print(hf_model.load_state_dict(state_dict, strict=False))
|
| 109 |
+
hf_model.save_pretrained(args.output_dir)
|
| 110 |
+
|
| 111 |
+
dist.barrier()
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
if __name__ == "__main__":
|
| 115 |
+
args = parse_args(add_custom_arguments=add_checkpoint_args)
|
| 116 |
+
main(args)
|
tools/convert_torch_dist_to_hf.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import pickle
|
| 8 |
+
import re
|
| 9 |
+
import shutil
|
| 10 |
+
import time
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
import safetensors.torch
|
| 14 |
+
import torch
|
| 15 |
+
import torch.distributed.checkpoint as dist_cp
|
| 16 |
+
from transformers import AutoConfig
|
| 17 |
+
from typing_extensions import override
|
| 18 |
+
|
| 19 |
+
from slime.backends.megatron_utils.megatron_to_hf import convert_to_hf, remove_padding
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class UnpicklerWrapper(pickle.Unpickler):
|
| 23 |
+
@override
|
| 24 |
+
def find_class(self, mod_name, name):
|
| 25 |
+
class DummyClass:
|
| 26 |
+
def __init__(self, *args, **kwargs):
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
if mod_name.startswith("megatron") or mod_name.startswith("glm"):
|
| 30 |
+
return DummyClass
|
| 31 |
+
return super().find_class(mod_name, name)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
pickle.Unpickler = UnpicklerWrapper
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class WrappedStorageReader(dist_cp.FileSystemReader):
|
| 38 |
+
@override
|
| 39 |
+
def read_metadata(self):
|
| 40 |
+
path = self.fs.concat_path(self.path, ".metadata")
|
| 41 |
+
with self.fs.create_stream(path, "rb") as metadata_file:
|
| 42 |
+
metadata = UnpicklerWrapper(metadata_file).load()
|
| 43 |
+
if getattr(metadata, "storage_meta", None) is None:
|
| 44 |
+
metadata.storage_meta = dist_cp.StorageMeta()
|
| 45 |
+
metadata.storage_meta.load_id = self.load_id
|
| 46 |
+
if metadata.planner_data is None:
|
| 47 |
+
metadata.planner_data = {}
|
| 48 |
+
return metadata
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class EmptyStateDictLoadPlanner(dist_cp.default_planner.DefaultLoadPlanner):
|
| 52 |
+
@override
|
| 53 |
+
def set_up_planner(
|
| 54 |
+
self,
|
| 55 |
+
state_dict: dist_cp.metadata.STATE_DICT_TYPE,
|
| 56 |
+
metadata: dist_cp.metadata.Metadata | None = None,
|
| 57 |
+
is_coordinator: bool = False,
|
| 58 |
+
) -> None:
|
| 59 |
+
for k, v in metadata.state_dict_metadata.items():
|
| 60 |
+
if "optimizer" in k or "_state" in k:
|
| 61 |
+
continue
|
| 62 |
+
print(f"find {k} in torch_dist ckpt")
|
| 63 |
+
if isinstance(v, dist_cp.metadata.TensorStorageMetadata):
|
| 64 |
+
v = torch.empty(v.size, dtype=v.properties.dtype) # type: ignore[assignment]
|
| 65 |
+
state_dict[k] = v
|
| 66 |
+
super().set_up_planner(state_dict, metadata, is_coordinator)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def get_expert_param(args, name, param):
|
| 70 |
+
if ".experts." not in name:
|
| 71 |
+
yield name, param
|
| 72 |
+
return
|
| 73 |
+
|
| 74 |
+
num_experts = args.num_experts
|
| 75 |
+
match = re.search(r"mlp.experts\.(.+)\.weight(\d+)", name)
|
| 76 |
+
if not match:
|
| 77 |
+
assert param.shape[0] == num_experts
|
| 78 |
+
for expert_id in range(num_experts):
|
| 79 |
+
expert_name = name.replace(".experts.experts.", ".experts.") + str(expert_id)
|
| 80 |
+
expert_param = param[expert_id]
|
| 81 |
+
yield expert_name, expert_param
|
| 82 |
+
else:
|
| 83 |
+
yield name, param
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def get_layer_param(args, name, param):
|
| 87 |
+
if ".layers." not in name:
|
| 88 |
+
yield name, param
|
| 89 |
+
return
|
| 90 |
+
|
| 91 |
+
num_layers = args.num_layers
|
| 92 |
+
match = re.search(r"\.layers\.(\d+)\.", name)
|
| 93 |
+
if not match:
|
| 94 |
+
assert param.shape[0] == num_layers
|
| 95 |
+
for layer_id in range(num_layers):
|
| 96 |
+
layer_name = name.replace(".layers.", f".layers.{layer_id}.")
|
| 97 |
+
layer_param = param[layer_id]
|
| 98 |
+
yield from get_expert_param(args, layer_name, layer_param)
|
| 99 |
+
else:
|
| 100 |
+
yield from get_expert_param(args, name, param)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def get_named_params(args, state_dict):
|
| 104 |
+
for name, param in state_dict.items():
|
| 105 |
+
name = f"module.module.{name}"
|
| 106 |
+
yield from get_layer_param(args, name, param)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def save_tensors(args, model_name, state_dict, output_dir, chunk_size, vocab_size=None):
|
| 110 |
+
# for slime update_weight compatible
|
| 111 |
+
args.sglang_enable_ep_moe = False
|
| 112 |
+
|
| 113 |
+
print(f"start saving to {output_dir}")
|
| 114 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 115 |
+
# 2GB
|
| 116 |
+
current_size = 0
|
| 117 |
+
total_size = 0
|
| 118 |
+
modeltensors = [{}]
|
| 119 |
+
for name, param in get_named_params(args, state_dict):
|
| 120 |
+
if vocab_size:
|
| 121 |
+
param = remove_padding(name, param, vocab_size)
|
| 122 |
+
converted_named_tensors = convert_to_hf(args, model_name, name, param)
|
| 123 |
+
for converted_name, converted_param in converted_named_tensors:
|
| 124 |
+
tensor_size = converted_param.numel() * converted_param.element_size()
|
| 125 |
+
if tensor_size + current_size > chunk_size:
|
| 126 |
+
modeltensors.append({})
|
| 127 |
+
current_size = 0
|
| 128 |
+
modeltensors[-1][converted_name] = converted_param
|
| 129 |
+
current_size += tensor_size
|
| 130 |
+
total_size += tensor_size
|
| 131 |
+
|
| 132 |
+
metadata = {"metadata": {"total_size": total_size}, "weight_map": {}}
|
| 133 |
+
|
| 134 |
+
num_files = len(modeltensors)
|
| 135 |
+
for i, tensors in enumerate(modeltensors):
|
| 136 |
+
filename = f"model-{i:05d}-of-{num_files:05d}.safetensors"
|
| 137 |
+
for key in tensors.keys():
|
| 138 |
+
metadata["weight_map"][key] = filename
|
| 139 |
+
index_filepath = os.path.join(output_dir, "model.safetensors.index.json")
|
| 140 |
+
json.dump(metadata, open(index_filepath, "w"), indent=2)
|
| 141 |
+
print(f"{index_filepath} saved.")
|
| 142 |
+
|
| 143 |
+
for i, tensors in enumerate(modeltensors):
|
| 144 |
+
filename = f"model-{i:05d}-of-{num_files:05d}.safetensors"
|
| 145 |
+
t = time.time()
|
| 146 |
+
filepath = os.path.join(output_dir, filename)
|
| 147 |
+
safetensors.torch.save_file(tensors, filepath)
|
| 148 |
+
print(f"{filename} saved in {time.time() - t:.2f} sec.")
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def copy_assets(origin_hf_dir, output_dir):
|
| 152 |
+
for filename in os.listdir(origin_hf_dir):
|
| 153 |
+
if filename == "model.safetensors.index.json" or filename.endswith(".safetensors"):
|
| 154 |
+
continue
|
| 155 |
+
origin_filename = os.path.join(origin_hf_dir, filename)
|
| 156 |
+
if not os.path.isfile(origin_filename):
|
| 157 |
+
print(f"Skip {filename}, not a file.")
|
| 158 |
+
continue
|
| 159 |
+
src, dst = origin_filename, os.path.join(output_dir, filename)
|
| 160 |
+
print(f"copy from {src} to {dst}")
|
| 161 |
+
shutil.copy(src, dst)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
if __name__ == "__main__":
|
| 165 |
+
parser = argparse.ArgumentParser()
|
| 166 |
+
parser.add_argument("--model-name", type=str, default=None)
|
| 167 |
+
parser.add_argument("--input-dir", type=str, required=True)
|
| 168 |
+
parser.add_argument("--output-dir", type=str, required=True)
|
| 169 |
+
parser.add_argument(
|
| 170 |
+
"--origin-hf-dir",
|
| 171 |
+
type=str,
|
| 172 |
+
default=None,
|
| 173 |
+
help="use the origin hf dir to copy files like tokenizer, config.json, etc.",
|
| 174 |
+
)
|
| 175 |
+
parser.add_argument(
|
| 176 |
+
"-f", "--force", action="store_true", help="Force overwrite the output directory if it exists."
|
| 177 |
+
)
|
| 178 |
+
parser.add_argument(
|
| 179 |
+
"--chunk-size",
|
| 180 |
+
type=int,
|
| 181 |
+
default=5 * 1024**3,
|
| 182 |
+
help="Chunk size for saving tensors, default is 2GB.",
|
| 183 |
+
)
|
| 184 |
+
parser.add_argument(
|
| 185 |
+
"--vocab-size",
|
| 186 |
+
type=int,
|
| 187 |
+
default=None,
|
| 188 |
+
help="Vocab size for removing padding, if applicable. If not provided, no padding will be removed.",
|
| 189 |
+
)
|
| 190 |
+
args = parser.parse_args()
|
| 191 |
+
|
| 192 |
+
if os.path.exists(args.output_dir) and not args.force:
|
| 193 |
+
raise ValueError(f"Output directory {args.output_dir} already exists. Use --force to overwrite it.")
|
| 194 |
+
|
| 195 |
+
if args.model_name is None and args.origin_hf_dir is None:
|
| 196 |
+
raise ValueError(
|
| 197 |
+
"Either --model-name or --origin-hf-dir must be provided, so that we can know the name of the params."
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
if args.model_name is None:
|
| 201 |
+
hf_config = AutoConfig.from_pretrained(args.origin_hf_dir, trust_remote_code=True)
|
| 202 |
+
args.model_name = type(hf_config).__name__.lower()
|
| 203 |
+
|
| 204 |
+
state_dict = {}
|
| 205 |
+
print(f"loading model from {args.input_dir}")
|
| 206 |
+
t = time.time()
|
| 207 |
+
megatron_args = torch.load(os.path.join(args.input_dir, "common.pt"), weights_only=False)["args"]
|
| 208 |
+
dist_cp.state_dict_loader._load_state_dict(
|
| 209 |
+
state_dict,
|
| 210 |
+
storage_reader=WrappedStorageReader(args.input_dir),
|
| 211 |
+
planner=EmptyStateDictLoadPlanner(),
|
| 212 |
+
no_dist=True,
|
| 213 |
+
)
|
| 214 |
+
print(f"model loaded in {time.time()-t:.2f} sec.")
|
| 215 |
+
|
| 216 |
+
save_tensors(megatron_args, args.model_name, state_dict, args.output_dir, args.chunk_size, args.vocab_size)
|
| 217 |
+
|
| 218 |
+
if args.origin_hf_dir:
|
| 219 |
+
copy_assets(args.origin_hf_dir, args.output_dir)
|
tools/eval_aime2024_vllm.py
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
"""
|
| 3 |
+
Evaluate a HF-format model on AIME 2024 using vLLM.
|
| 4 |
+
|
| 5 |
+
Example:
|
| 6 |
+
|
| 7 |
+
CUDA_VISIBLE_DEVICES=0,1,2,3 python tools/eval_aime_vllm.py \
|
| 8 |
+
--model /workspace/Lightning-OPD/checkpoints/qwen3-4b-lightning-opd-hf \
|
| 9 |
+
--num-gpus 4 \
|
| 10 |
+
--output outputs/eval_aime2024_qwen3_4b_lightning_opd.jsonl
|
| 11 |
+
|
| 12 |
+
Paper-style AIME setting:
|
| 13 |
+
temperature = 0.6
|
| 14 |
+
top_p = 0.95
|
| 15 |
+
max_tokens = 32768
|
| 16 |
+
n_samples = 32
|
| 17 |
+
metric = average pass@1
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import argparse
|
| 21 |
+
import json
|
| 22 |
+
import os
|
| 23 |
+
import re
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
from typing import Any, Dict, List, Optional
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def parse_args():
|
| 29 |
+
parser = argparse.ArgumentParser()
|
| 30 |
+
|
| 31 |
+
# Model / hardware
|
| 32 |
+
parser.add_argument(
|
| 33 |
+
"--model",
|
| 34 |
+
type=str,
|
| 35 |
+
required=True,
|
| 36 |
+
help="Path to HF-format model checkpoint.",
|
| 37 |
+
)
|
| 38 |
+
parser.add_argument(
|
| 39 |
+
"--num-gpus",
|
| 40 |
+
type=int,
|
| 41 |
+
default=1,
|
| 42 |
+
help="Tensor parallel size for vLLM.",
|
| 43 |
+
)
|
| 44 |
+
parser.add_argument(
|
| 45 |
+
"--gpu-ids",
|
| 46 |
+
type=str,
|
| 47 |
+
default=None,
|
| 48 |
+
help='Optional CUDA_VISIBLE_DEVICES, e.g. "0,1,2,3". Must match --num-gpus.',
|
| 49 |
+
)
|
| 50 |
+
parser.add_argument(
|
| 51 |
+
"--dtype",
|
| 52 |
+
type=str,
|
| 53 |
+
default="bfloat16",
|
| 54 |
+
choices=["auto", "float16", "bfloat16", "float32"],
|
| 55 |
+
)
|
| 56 |
+
parser.add_argument(
|
| 57 |
+
"--gpu-memory-utilization",
|
| 58 |
+
type=float,
|
| 59 |
+
default=0.90,
|
| 60 |
+
)
|
| 61 |
+
parser.add_argument(
|
| 62 |
+
"--trust-remote-code",
|
| 63 |
+
action="store_true",
|
| 64 |
+
default=True,
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
# Dataset
|
| 68 |
+
parser.add_argument(
|
| 69 |
+
"--dataset",
|
| 70 |
+
type=str,
|
| 71 |
+
default="AI-MO/aimo-validation-aime",
|
| 72 |
+
help="HF dataset name for AIME 2024.",
|
| 73 |
+
)
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--split",
|
| 76 |
+
type=str,
|
| 77 |
+
default=None,
|
| 78 |
+
help="Dataset split. If not set, use the first available split.",
|
| 79 |
+
)
|
| 80 |
+
parser.add_argument(
|
| 81 |
+
"--hf-cache",
|
| 82 |
+
type=str,
|
| 83 |
+
default=None,
|
| 84 |
+
help="Optional HuggingFace cache dir.",
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# Generation settings from paper
|
| 88 |
+
parser.add_argument("--n-samples", type=int, default=32)
|
| 89 |
+
parser.add_argument("--temperature", type=float, default=0.6)
|
| 90 |
+
parser.add_argument("--top-p", type=float, default=0.95)
|
| 91 |
+
parser.add_argument("--max-tokens", type=int, default=32768)
|
| 92 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 93 |
+
|
| 94 |
+
# Prompt / tokenizer behavior
|
| 95 |
+
parser.add_argument(
|
| 96 |
+
"--no-chat-template",
|
| 97 |
+
action="store_true",
|
| 98 |
+
help="Do not apply tokenizer chat template; use raw prompt.",
|
| 99 |
+
)
|
| 100 |
+
parser.add_argument(
|
| 101 |
+
"--enable-thinking",
|
| 102 |
+
action="store_true",
|
| 103 |
+
default=False,
|
| 104 |
+
help="Pass enable_thinking=True to Qwen3 chat template if supported.",
|
| 105 |
+
)
|
| 106 |
+
parser.add_argument(
|
| 107 |
+
"--disable-thinking",
|
| 108 |
+
action="store_true",
|
| 109 |
+
default=False,
|
| 110 |
+
help="Pass enable_thinking=False to Qwen3 chat template if supported.",
|
| 111 |
+
)
|
| 112 |
+
parser.add_argument(
|
| 113 |
+
"--assistant-prefix",
|
| 114 |
+
type=str,
|
| 115 |
+
default=None,
|
| 116 |
+
help=(
|
| 117 |
+
"Optional text appended after the assistant generation prompt. "
|
| 118 |
+
"Example for no-thinking style: '<think>\\n\\n</think>\\n\\n'"
|
| 119 |
+
),
|
| 120 |
+
)
|
| 121 |
+
parser.add_argument(
|
| 122 |
+
"--prompt-template",
|
| 123 |
+
type=str,
|
| 124 |
+
default="train",
|
| 125 |
+
choices=["train", "paper", "chat"],
|
| 126 |
+
help=(
|
| 127 |
+
"Prompt template to use. "
|
| 128 |
+
"'train' uses the exact training ChatML prompt; "
|
| 129 |
+
"'paper' uses the paper-style ChatML prompt; "
|
| 130 |
+
"'chat' uses build_problem_prompt plus tokenizer.apply_chat_template."
|
| 131 |
+
),
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
# Output / debug
|
| 135 |
+
parser.add_argument(
|
| 136 |
+
"--output",
|
| 137 |
+
type=str,
|
| 138 |
+
default="outputs/eval_aime2024_vllm.jsonl",
|
| 139 |
+
help="Path to save per-sample generations and scores.",
|
| 140 |
+
)
|
| 141 |
+
parser.add_argument(
|
| 142 |
+
"--limit",
|
| 143 |
+
type=int,
|
| 144 |
+
default=None,
|
| 145 |
+
help="Limit number of problems for quick debugging.",
|
| 146 |
+
)
|
| 147 |
+
parser.add_argument(
|
| 148 |
+
"--print-examples",
|
| 149 |
+
type=int,
|
| 150 |
+
default=3,
|
| 151 |
+
help="Print first N examples with predictions.",
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
return parser.parse_args()
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def normalize_answer(x: Any) -> Optional[str]:
|
| 158 |
+
if x is None:
|
| 159 |
+
return None
|
| 160 |
+
|
| 161 |
+
s = str(x).strip()
|
| 162 |
+
s = s.replace("$", "")
|
| 163 |
+
s = s.replace(",", "")
|
| 164 |
+
s = s.replace("\\,", "")
|
| 165 |
+
s = s.strip()
|
| 166 |
+
|
| 167 |
+
# Remove simple latex wrappers.
|
| 168 |
+
s = s.replace("\\text", "")
|
| 169 |
+
s = s.replace("\\mathrm", "")
|
| 170 |
+
s = s.strip("{}").strip()
|
| 171 |
+
|
| 172 |
+
# AIME answers are integers from 0 to 999.
|
| 173 |
+
m = re.search(r"-?\d+", s)
|
| 174 |
+
if m is None:
|
| 175 |
+
return None
|
| 176 |
+
|
| 177 |
+
try:
|
| 178 |
+
return str(int(m.group(0)))
|
| 179 |
+
except ValueError:
|
| 180 |
+
return None
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def extract_last_boxed(text: str) -> Optional[str]:
|
| 184 |
+
"""
|
| 185 |
+
Extract the last \\boxed{...}. Handles simple nested braces better than regex.
|
| 186 |
+
"""
|
| 187 |
+
marker = r"\boxed{"
|
| 188 |
+
positions = [m.start() for m in re.finditer(re.escape(marker), text)]
|
| 189 |
+
if not positions:
|
| 190 |
+
return None
|
| 191 |
+
|
| 192 |
+
for start in reversed(positions):
|
| 193 |
+
i = start + len(marker)
|
| 194 |
+
depth = 1
|
| 195 |
+
chars = []
|
| 196 |
+
|
| 197 |
+
while i < len(text):
|
| 198 |
+
ch = text[i]
|
| 199 |
+
if ch == "{":
|
| 200 |
+
depth += 1
|
| 201 |
+
chars.append(ch)
|
| 202 |
+
elif ch == "}":
|
| 203 |
+
depth -= 1
|
| 204 |
+
if depth == 0:
|
| 205 |
+
return "".join(chars)
|
| 206 |
+
chars.append(ch)
|
| 207 |
+
else:
|
| 208 |
+
chars.append(ch)
|
| 209 |
+
i += 1
|
| 210 |
+
|
| 211 |
+
return None
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def extract_answer(text: str) -> Optional[str]:
|
| 215 |
+
# Prefer boxed answer, because the actual training prompt asks for \boxed{$Answer}.
|
| 216 |
+
boxed = extract_last_boxed(text)
|
| 217 |
+
if boxed is not None:
|
| 218 |
+
ans = normalize_answer(boxed)
|
| 219 |
+
if ans is not None:
|
| 220 |
+
return ans
|
| 221 |
+
|
| 222 |
+
# Fallback: final Answer: line.
|
| 223 |
+
matches = re.findall(r"Answer:\s*([^\n]+)", text, flags=re.IGNORECASE)
|
| 224 |
+
if matches:
|
| 225 |
+
ans = normalize_answer(matches[-1])
|
| 226 |
+
if ans is not None:
|
| 227 |
+
return ans
|
| 228 |
+
|
| 229 |
+
# No valid final answer.
|
| 230 |
+
return None
|
| 231 |
+
|
| 232 |
+
def build_exact_training_prompt(problem: str) -> str:
|
| 233 |
+
return (
|
| 234 |
+
"<|im_start|>user\n"
|
| 235 |
+
"Solve the following math problem step by step. "
|
| 236 |
+
"The last line of your response should be of the form "
|
| 237 |
+
"Answer: \\boxed{$Answer} where $Answer is the answer to the problem.\n\n"
|
| 238 |
+
f"{problem}\n\n"
|
| 239 |
+
"Remember to put your answer on its own line after \"Answer:\"."
|
| 240 |
+
"<|im_end|>\n"
|
| 241 |
+
"<|im_start|>assistant\n\n\n\n"
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
def build_problem_prompt(problem: str) -> str:
|
| 245 |
+
return (
|
| 246 |
+
f"{problem}\n\n"
|
| 247 |
+
"Please reason step by step, and put your final answer within \\boxed{}."
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
def build_paper_math_eval_prompt(problem: str) -> str:
|
| 251 |
+
return (
|
| 252 |
+
"<|im_start|>user\n"
|
| 253 |
+
f"Question: {problem}\n"
|
| 254 |
+
"Please reason step by step, and put your final answer within \\boxed{}.\n"
|
| 255 |
+
"<|im_end|>\n"
|
| 256 |
+
"<|im_start|>assistant\n"
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
def get_field(ex: Dict[str, Any], candidates: List[str]) -> Any:
|
| 260 |
+
for key in candidates:
|
| 261 |
+
if key in ex and ex[key] is not None:
|
| 262 |
+
return ex[key]
|
| 263 |
+
raise KeyError(f"Cannot find any of {candidates}. Example keys: {list(ex.keys())}")
|
| 264 |
+
|
| 265 |
+
# def build_problem_prompt(problem: str) -> str:
|
| 266 |
+
# return (
|
| 267 |
+
# "Solve the following math problem step by step. "
|
| 268 |
+
# "The last line of your response should be of the form "
|
| 269 |
+
# "Answer: $Answer (without quotes) where $Answer is the answer to the problem.\n\n"
|
| 270 |
+
# f"{problem}\n\n"
|
| 271 |
+
# 'Remember to put your answer on its own line after "Answer:".'
|
| 272 |
+
# )
|
| 273 |
+
|
| 274 |
+
def apply_chat_template(tokenizer, prompt: str, args) -> str:
|
| 275 |
+
if args.no_chat_template:
|
| 276 |
+
return prompt
|
| 277 |
+
|
| 278 |
+
messages = [{"role": "user", "content": prompt}]
|
| 279 |
+
|
| 280 |
+
kwargs = {
|
| 281 |
+
"tokenize": False,
|
| 282 |
+
"add_generation_prompt": True,
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
# Qwen3 tokenizer may support enable_thinking.
|
| 286 |
+
if args.enable_thinking and args.disable_thinking:
|
| 287 |
+
raise ValueError("Do not set both --enable-thinking and --disable-thinking.")
|
| 288 |
+
|
| 289 |
+
if args.enable_thinking:
|
| 290 |
+
kwargs["enable_thinking"] = True
|
| 291 |
+
elif args.disable_thinking:
|
| 292 |
+
kwargs["enable_thinking"] = False
|
| 293 |
+
|
| 294 |
+
try:
|
| 295 |
+
text = tokenizer.apply_chat_template(messages, **kwargs)
|
| 296 |
+
except TypeError:
|
| 297 |
+
# Some tokenizers do not accept enable_thinking.
|
| 298 |
+
kwargs.pop("enable_thinking", None)
|
| 299 |
+
text = tokenizer.apply_chat_template(messages, **kwargs)
|
| 300 |
+
|
| 301 |
+
if args.assistant_prefix is not None:
|
| 302 |
+
text += args.assistant_prefix.encode("utf-8").decode("unicode_escape")
|
| 303 |
+
|
| 304 |
+
return text
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def main():
|
| 308 |
+
args = parse_args()
|
| 309 |
+
|
| 310 |
+
if args.gpu_ids is not None:
|
| 311 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu_ids
|
| 312 |
+
|
| 313 |
+
if args.hf_cache is not None:
|
| 314 |
+
os.environ["HF_HOME"] = args.hf_cache
|
| 315 |
+
os.environ["HF_DATASETS_CACHE"] = str(Path(args.hf_cache) / "datasets")
|
| 316 |
+
os.environ["HF_HUB_CACHE"] = str(Path(args.hf_cache) / "hub")
|
| 317 |
+
|
| 318 |
+
# Import after CUDA_VISIBLE_DEVICES is set.
|
| 319 |
+
from datasets import load_dataset
|
| 320 |
+
from transformers import AutoTokenizer
|
| 321 |
+
from vllm import LLM, SamplingParams
|
| 322 |
+
|
| 323 |
+
output_path = Path(args.output)
|
| 324 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 325 |
+
|
| 326 |
+
print("=" * 80)
|
| 327 |
+
print("AIME 2024 vLLM Evaluation")
|
| 328 |
+
print("=" * 80)
|
| 329 |
+
print(f"model: {args.model}")
|
| 330 |
+
print(f"dataset: {args.dataset}")
|
| 331 |
+
print(f"num_gpus / TP size: {args.num_gpus}")
|
| 332 |
+
print(f"CUDA_VISIBLE_DEVICES: {os.environ.get('CUDA_VISIBLE_DEVICES')}")
|
| 333 |
+
print(f"n_samples/problem: {args.n_samples}")
|
| 334 |
+
print(f"temperature: {args.temperature}")
|
| 335 |
+
print(f"top_p: {args.top_p}")
|
| 336 |
+
print(f"max_tokens: {args.max_tokens}")
|
| 337 |
+
print(f"prompt_template: {args.prompt_template}")
|
| 338 |
+
print(f"assistant_prefix: {repr(args.assistant_prefix)}")
|
| 339 |
+
print(f"output: {args.output}")
|
| 340 |
+
print("=" * 80)
|
| 341 |
+
|
| 342 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 343 |
+
args.model,
|
| 344 |
+
trust_remote_code=args.trust_remote_code,
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
dataset_dict = load_dataset(args.dataset)
|
| 348 |
+
split = args.split or list(dataset_dict.keys())[0]
|
| 349 |
+
data = dataset_dict[split]
|
| 350 |
+
|
| 351 |
+
# AI-MO/aimo-validation-aime contains multiple AIME years.
|
| 352 |
+
# Keep only AIME 2024 examples.
|
| 353 |
+
if "url" in data.column_names:
|
| 354 |
+
data = data.filter(lambda ex: "2024_AIME" in ex["url"])
|
| 355 |
+
else:
|
| 356 |
+
raise ValueError(
|
| 357 |
+
"Expected a `url` column for filtering AIME 2024, "
|
| 358 |
+
f"but got columns: {data.column_names}"
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
print(f"Filtered AIME 2024 examples: {len(data)}")
|
| 362 |
+
for i in range(min(3, len(data))):
|
| 363 |
+
print(i, data[i].get("url", "NO_URL"), data[i].get("answer", "NO_ANSWER"))
|
| 364 |
+
|
| 365 |
+
assert len(data) == 30, f"Expected 30 AIME 2024 problems, got {len(data)}"
|
| 366 |
+
|
| 367 |
+
if args.limit is not None:
|
| 368 |
+
data = data.select(range(min(args.limit, len(data))))
|
| 369 |
+
|
| 370 |
+
print(f"Loaded split: {split}")
|
| 371 |
+
print(f"Number of problems: {len(data)}")
|
| 372 |
+
print(f"First example keys: {list(data[0].keys())}")
|
| 373 |
+
|
| 374 |
+
prompts = []
|
| 375 |
+
examples = []
|
| 376 |
+
|
| 377 |
+
for idx, ex in enumerate(data):
|
| 378 |
+
problem = get_field(ex, ["problem", "question", "prompt"])
|
| 379 |
+
gold_raw = get_field(ex, ["answer", "final_answer", "target", "solution"])
|
| 380 |
+
gold = normalize_answer(gold_raw)
|
| 381 |
+
|
| 382 |
+
if args.prompt_template == "train":
|
| 383 |
+
# Raw ChatML prompt matching the OPD training parquet.
|
| 384 |
+
# Do NOT call apply_chat_template again, otherwise ChatML will be nested.
|
| 385 |
+
full_prompt = build_exact_training_prompt(problem)
|
| 386 |
+
elif args.prompt_template == "paper":
|
| 387 |
+
# Raw ChatML prompt matching the paper-style math evaluation prompt.
|
| 388 |
+
# Do NOT call apply_chat_template again.
|
| 389 |
+
full_prompt = build_paper_math_eval_prompt(problem)
|
| 390 |
+
elif args.prompt_template == "chat":
|
| 391 |
+
# Normal user-content prompt; tokenizer will add ChatML.
|
| 392 |
+
raw_prompt = build_problem_prompt(problem)
|
| 393 |
+
full_prompt = apply_chat_template(tokenizer, raw_prompt, args)
|
| 394 |
+
else:
|
| 395 |
+
raise ValueError(f"Unknown prompt_template: {args.prompt_template}")
|
| 396 |
+
|
| 397 |
+
# Important: when using raw ChatML prompts, apply_chat_template() is bypassed.
|
| 398 |
+
# Therefore --assistant-prefix must be appended here, not only inside apply_chat_template().
|
| 399 |
+
# This is useful for Qwen3 no-thinking style, e.g.:
|
| 400 |
+
# --assistant-prefix '<think>\\n\\n</think>\\n\\n'
|
| 401 |
+
if args.prompt_template in {"train", "paper"} and args.assistant_prefix is not None:
|
| 402 |
+
full_prompt += args.assistant_prefix.encode("utf-8").decode("unicode_escape")
|
| 403 |
+
|
| 404 |
+
prompts.append(full_prompt)
|
| 405 |
+
examples.append(
|
| 406 |
+
{
|
| 407 |
+
"idx": idx,
|
| 408 |
+
"problem": problem,
|
| 409 |
+
"gold_raw": gold_raw,
|
| 410 |
+
"gold": gold,
|
| 411 |
+
"prompt": full_prompt,
|
| 412 |
+
}
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
llm = LLM(
|
| 416 |
+
model=args.model,
|
| 417 |
+
tensor_parallel_size=args.num_gpus,
|
| 418 |
+
dtype=args.dtype,
|
| 419 |
+
trust_remote_code=args.trust_remote_code,
|
| 420 |
+
gpu_memory_utilization=args.gpu_memory_utilization,
|
| 421 |
+
seed=args.seed,
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
sampling_params = SamplingParams(
|
| 425 |
+
n=args.n_samples,
|
| 426 |
+
temperature=args.temperature,
|
| 427 |
+
top_p=args.top_p,
|
| 428 |
+
#repetition_penalty=1.05,
|
| 429 |
+
max_tokens=args.max_tokens,
|
| 430 |
+
stop=["<|im_end|>"],
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
outputs = llm.generate(prompts, sampling_params)
|
| 434 |
+
|
| 435 |
+
total = 0
|
| 436 |
+
correct = 0
|
| 437 |
+
per_problem_records = []
|
| 438 |
+
|
| 439 |
+
with output_path.open("w", encoding="utf-8") as f:
|
| 440 |
+
for ex, out in zip(examples, outputs):
|
| 441 |
+
sample_records = []
|
| 442 |
+
problem_correct = 0
|
| 443 |
+
|
| 444 |
+
for sample_id, completion in enumerate(out.outputs):
|
| 445 |
+
text = completion.text
|
| 446 |
+
pred = extract_answer(text)
|
| 447 |
+
is_correct = pred == ex["gold"]
|
| 448 |
+
|
| 449 |
+
total += 1
|
| 450 |
+
correct += int(is_correct)
|
| 451 |
+
problem_correct += int(is_correct)
|
| 452 |
+
|
| 453 |
+
record = {
|
| 454 |
+
"idx": ex["idx"],
|
| 455 |
+
"sample_id": sample_id,
|
| 456 |
+
"gold": ex["gold"],
|
| 457 |
+
"gold_raw": str(ex["gold_raw"]),
|
| 458 |
+
"pred": pred,
|
| 459 |
+
"correct": is_correct,
|
| 460 |
+
"completion": text,
|
| 461 |
+
"finish_reason": completion.finish_reason,
|
| 462 |
+
"stop_reason": getattr(completion, "stop_reason", None),
|
| 463 |
+
}
|
| 464 |
+
sample_records.append(record)
|
| 465 |
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
| 466 |
+
|
| 467 |
+
problem_acc = problem_correct / max(1, len(out.outputs))
|
| 468 |
+
per_problem_records.append(problem_acc)
|
| 469 |
+
|
| 470 |
+
if ex["idx"] < args.print_examples:
|
| 471 |
+
print("-" * 80)
|
| 472 |
+
print(f"Problem {ex['idx']}")
|
| 473 |
+
print(f"Gold: {ex['gold']} | Correct samples: {problem_correct}/{len(out.outputs)}")
|
| 474 |
+
print(f"First pred: {sample_records[0]['pred']}")
|
| 475 |
+
print(f"First completion preview:\n{sample_records[0]['completion'][:1000]}")
|
| 476 |
+
|
| 477 |
+
avg_pass1_micro = correct / total if total > 0 else 0.0
|
| 478 |
+
avg_pass1_macro = sum(per_problem_records) / len(per_problem_records) if per_problem_records else 0.0
|
| 479 |
+
|
| 480 |
+
print("=" * 80)
|
| 481 |
+
print("Final results")
|
| 482 |
+
print("=" * 80)
|
| 483 |
+
print(f"Problems: {len(examples)}")
|
| 484 |
+
print(f"Samples per problem: {args.n_samples}")
|
| 485 |
+
print(f"Total samples: {total}")
|
| 486 |
+
print(f"Correct samples: {correct}")
|
| 487 |
+
print(f"Average pass@1 micro: {avg_pass1_micro:.6f} ({100 * avg_pass1_micro:.2f}%)")
|
| 488 |
+
print(f"Average pass@1 macro: {avg_pass1_macro:.6f} ({100 * avg_pass1_macro:.2f}%)")
|
| 489 |
+
print(f"Saved generations to: {output_path}")
|
| 490 |
+
print("=" * 80)
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
if __name__ == "__main__":
|
| 494 |
+
main()
|
tools/eval_aime2025_vllm.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
"""
|
| 3 |
+
Evaluate a HF-format model on AIME 2025 using vLLM.
|
| 4 |
+
|
| 5 |
+
Example:
|
| 6 |
+
|
| 7 |
+
CUDA_VISIBLE_DEVICES=0,1,2,3 python tools/eval_aime_vllm.py \
|
| 8 |
+
--model /workspace/Lightning-OPD/checkpoints/qwen3-4b-lightning-opd-hf \
|
| 9 |
+
--num-gpus 4 \
|
| 10 |
+
--output outputs/eval_aime_2025_qwen3_4b_lightning_opd.jsonl
|
| 11 |
+
|
| 12 |
+
Paper-style AIME setting:
|
| 13 |
+
temperature = 0.6
|
| 14 |
+
top_p = 0.95
|
| 15 |
+
max_tokens = 32768
|
| 16 |
+
n_samples = 32
|
| 17 |
+
metric = average pass@1
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import argparse
|
| 21 |
+
import json
|
| 22 |
+
import os
|
| 23 |
+
import re
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
from typing import Any, Dict, List, Optional
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def parse_args():
|
| 29 |
+
parser = argparse.ArgumentParser()
|
| 30 |
+
|
| 31 |
+
# Model / hardware
|
| 32 |
+
parser.add_argument(
|
| 33 |
+
"--model",
|
| 34 |
+
type=str,
|
| 35 |
+
required=True,
|
| 36 |
+
help="Path to HF-format model checkpoint.",
|
| 37 |
+
)
|
| 38 |
+
parser.add_argument(
|
| 39 |
+
"--num-gpus",
|
| 40 |
+
type=int,
|
| 41 |
+
default=1,
|
| 42 |
+
help="Tensor parallel size for vLLM.",
|
| 43 |
+
)
|
| 44 |
+
parser.add_argument(
|
| 45 |
+
"--gpu-ids",
|
| 46 |
+
type=str,
|
| 47 |
+
default=None,
|
| 48 |
+
help='Optional CUDA_VISIBLE_DEVICES, e.g. "0,1,2,3". Must match --num-gpus.',
|
| 49 |
+
)
|
| 50 |
+
parser.add_argument(
|
| 51 |
+
"--dtype",
|
| 52 |
+
type=str,
|
| 53 |
+
default="bfloat16",
|
| 54 |
+
choices=["auto", "float16", "bfloat16", "float32"],
|
| 55 |
+
)
|
| 56 |
+
parser.add_argument(
|
| 57 |
+
"--gpu-memory-utilization",
|
| 58 |
+
type=float,
|
| 59 |
+
default=0.90,
|
| 60 |
+
)
|
| 61 |
+
parser.add_argument(
|
| 62 |
+
"--trust-remote-code",
|
| 63 |
+
action="store_true",
|
| 64 |
+
default=True,
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
# Dataset
|
| 68 |
+
parser.add_argument(
|
| 69 |
+
"--dataset",
|
| 70 |
+
type=str,
|
| 71 |
+
default="MathArena/aime_2025",
|
| 72 |
+
help="HF dataset name for AIME 2025.",
|
| 73 |
+
)
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--split",
|
| 76 |
+
type=str,
|
| 77 |
+
default=None,
|
| 78 |
+
help="Dataset split. If not set, use the first available split.",
|
| 79 |
+
)
|
| 80 |
+
parser.add_argument(
|
| 81 |
+
"--hf-cache",
|
| 82 |
+
type=str,
|
| 83 |
+
default=None,
|
| 84 |
+
help="Optional HuggingFace cache dir.",
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# Generation settings from paper
|
| 88 |
+
parser.add_argument("--n-samples", type=int, default=32)
|
| 89 |
+
parser.add_argument("--temperature", type=float, default=0.6)
|
| 90 |
+
parser.add_argument("--top-p", type=float, default=0.95)
|
| 91 |
+
parser.add_argument("--max-tokens", type=int, default=32768)
|
| 92 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 93 |
+
|
| 94 |
+
# Prompt / tokenizer behavior
|
| 95 |
+
parser.add_argument(
|
| 96 |
+
"--no-chat-template",
|
| 97 |
+
action="store_true",
|
| 98 |
+
help="Do not apply tokenizer chat template; use raw prompt.",
|
| 99 |
+
)
|
| 100 |
+
parser.add_argument(
|
| 101 |
+
"--enable-thinking",
|
| 102 |
+
action="store_true",
|
| 103 |
+
default=False,
|
| 104 |
+
help="Pass enable_thinking=True to Qwen3 chat template if supported.",
|
| 105 |
+
)
|
| 106 |
+
parser.add_argument(
|
| 107 |
+
"--disable-thinking",
|
| 108 |
+
action="store_true",
|
| 109 |
+
default=False,
|
| 110 |
+
help="Pass enable_thinking=False to Qwen3 chat template if supported.",
|
| 111 |
+
)
|
| 112 |
+
parser.add_argument(
|
| 113 |
+
"--assistant-prefix",
|
| 114 |
+
type=str,
|
| 115 |
+
default=None,
|
| 116 |
+
help=(
|
| 117 |
+
"Optional text appended after the assistant generation prompt. "
|
| 118 |
+
"Example for no-thinking style: '<think>\\n\\n</think>\\n\\n'"
|
| 119 |
+
),
|
| 120 |
+
)
|
| 121 |
+
parser.add_argument(
|
| 122 |
+
"--prompt-template",
|
| 123 |
+
type=str,
|
| 124 |
+
default="train",
|
| 125 |
+
choices=["train", "paper", "chat"],
|
| 126 |
+
help=(
|
| 127 |
+
"Prompt template to use. "
|
| 128 |
+
"'train' uses the exact training ChatML prompt; "
|
| 129 |
+
"'paper' uses the paper-style ChatML prompt; "
|
| 130 |
+
"'chat' uses build_problem_prompt plus tokenizer.apply_chat_template."
|
| 131 |
+
),
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
# Output / debug
|
| 135 |
+
parser.add_argument(
|
| 136 |
+
"--output",
|
| 137 |
+
type=str,
|
| 138 |
+
default="outputs/eval_aime_2025_vllm.jsonl",
|
| 139 |
+
help="Path to save per-sample generations and scores.",
|
| 140 |
+
)
|
| 141 |
+
parser.add_argument(
|
| 142 |
+
"--limit",
|
| 143 |
+
type=int,
|
| 144 |
+
default=None,
|
| 145 |
+
help="Limit number of problems for quick debugging.",
|
| 146 |
+
)
|
| 147 |
+
parser.add_argument(
|
| 148 |
+
"--print-examples",
|
| 149 |
+
type=int,
|
| 150 |
+
default=3,
|
| 151 |
+
help="Print first N examples with predictions.",
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
return parser.parse_args()
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def normalize_answer(x: Any) -> Optional[str]:
|
| 158 |
+
if x is None:
|
| 159 |
+
return None
|
| 160 |
+
|
| 161 |
+
s = str(x).strip()
|
| 162 |
+
s = s.replace("$", "")
|
| 163 |
+
s = s.replace(",", "")
|
| 164 |
+
s = s.replace("\\,", "")
|
| 165 |
+
s = s.strip()
|
| 166 |
+
|
| 167 |
+
# Remove simple latex wrappers.
|
| 168 |
+
s = s.replace("\\text", "")
|
| 169 |
+
s = s.replace("\\mathrm", "")
|
| 170 |
+
s = s.strip("{}").strip()
|
| 171 |
+
|
| 172 |
+
# AIME answers are integers from 0 to 999.
|
| 173 |
+
m = re.search(r"-?\d+", s)
|
| 174 |
+
if m is None:
|
| 175 |
+
return None
|
| 176 |
+
|
| 177 |
+
try:
|
| 178 |
+
return str(int(m.group(0)))
|
| 179 |
+
except ValueError:
|
| 180 |
+
return None
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def extract_last_boxed(text: str) -> Optional[str]:
|
| 184 |
+
"""
|
| 185 |
+
Extract the last \\boxed{...}. Handles simple nested braces better than regex.
|
| 186 |
+
"""
|
| 187 |
+
marker = r"\boxed{"
|
| 188 |
+
positions = [m.start() for m in re.finditer(re.escape(marker), text)]
|
| 189 |
+
if not positions:
|
| 190 |
+
return None
|
| 191 |
+
|
| 192 |
+
for start in reversed(positions):
|
| 193 |
+
i = start + len(marker)
|
| 194 |
+
depth = 1
|
| 195 |
+
chars = []
|
| 196 |
+
|
| 197 |
+
while i < len(text):
|
| 198 |
+
ch = text[i]
|
| 199 |
+
if ch == "{":
|
| 200 |
+
depth += 1
|
| 201 |
+
chars.append(ch)
|
| 202 |
+
elif ch == "}":
|
| 203 |
+
depth -= 1
|
| 204 |
+
if depth == 0:
|
| 205 |
+
return "".join(chars)
|
| 206 |
+
chars.append(ch)
|
| 207 |
+
else:
|
| 208 |
+
chars.append(ch)
|
| 209 |
+
i += 1
|
| 210 |
+
|
| 211 |
+
return None
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def extract_answer(text: str) -> Optional[str]:
|
| 215 |
+
# Prefer boxed answer, because the actual training prompt asks for \boxed{$Answer}.
|
| 216 |
+
boxed = extract_last_boxed(text)
|
| 217 |
+
if boxed is not None:
|
| 218 |
+
ans = normalize_answer(boxed)
|
| 219 |
+
if ans is not None:
|
| 220 |
+
return ans
|
| 221 |
+
|
| 222 |
+
# Fallback: final Answer: line.
|
| 223 |
+
matches = re.findall(r"Answer:\s*([^\n]+)", text, flags=re.IGNORECASE)
|
| 224 |
+
if matches:
|
| 225 |
+
ans = normalize_answer(matches[-1])
|
| 226 |
+
if ans is not None:
|
| 227 |
+
return ans
|
| 228 |
+
|
| 229 |
+
# No valid final answer.
|
| 230 |
+
return None
|
| 231 |
+
|
| 232 |
+
def build_exact_training_prompt(problem: str) -> str:
|
| 233 |
+
return (
|
| 234 |
+
"<|im_start|>user\n"
|
| 235 |
+
"Solve the following math problem step by step. "
|
| 236 |
+
"The last line of your response should be of the form "
|
| 237 |
+
"Answer: \\boxed{$Answer} where $Answer is the answer to the problem.\n\n"
|
| 238 |
+
f"{problem}\n\n"
|
| 239 |
+
"Remember to put your answer on its own line after \"Answer:\"."
|
| 240 |
+
"<|im_end|>\n"
|
| 241 |
+
"<|im_start|>assistant\n\n\n\n"
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
def build_problem_prompt(problem: str) -> str:
|
| 245 |
+
return (
|
| 246 |
+
f"{problem}\n\n"
|
| 247 |
+
"Please reason step by step, and put your final answer within \\boxed{}."
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
def build_paper_math_eval_prompt(problem: str) -> str:
|
| 251 |
+
return (
|
| 252 |
+
"<|im_start|>user\n"
|
| 253 |
+
f"Question: {problem}\n"
|
| 254 |
+
"Please reason step by step, and put your final answer within \\boxed{}.\n"
|
| 255 |
+
"<|im_end|>\n"
|
| 256 |
+
"<|im_start|>assistant\n"
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
def get_field(ex: Dict[str, Any], candidates: List[str]) -> Any:
|
| 260 |
+
for key in candidates:
|
| 261 |
+
if key in ex and ex[key] is not None:
|
| 262 |
+
return ex[key]
|
| 263 |
+
raise KeyError(f"Cannot find any of {candidates}. Example keys: {list(ex.keys())}")
|
| 264 |
+
|
| 265 |
+
# def build_problem_prompt(problem: str) -> str:
|
| 266 |
+
# return (
|
| 267 |
+
# "Solve the following math problem step by step. "
|
| 268 |
+
# "The last line of your response should be of the form "
|
| 269 |
+
# "Answer: $Answer (without quotes) where $Answer is the answer to the problem.\n\n"
|
| 270 |
+
# f"{problem}\n\n"
|
| 271 |
+
# 'Remember to put your answer on its own line after "Answer:".'
|
| 272 |
+
# )
|
| 273 |
+
|
| 274 |
+
def apply_chat_template(tokenizer, prompt: str, args) -> str:
|
| 275 |
+
if args.no_chat_template:
|
| 276 |
+
return prompt
|
| 277 |
+
|
| 278 |
+
messages = [{"role": "user", "content": prompt}]
|
| 279 |
+
|
| 280 |
+
kwargs = {
|
| 281 |
+
"tokenize": False,
|
| 282 |
+
"add_generation_prompt": True,
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
# Qwen3 tokenizer may support enable_thinking.
|
| 286 |
+
if args.enable_thinking and args.disable_thinking:
|
| 287 |
+
raise ValueError("Do not set both --enable-thinking and --disable-thinking.")
|
| 288 |
+
|
| 289 |
+
if args.enable_thinking:
|
| 290 |
+
kwargs["enable_thinking"] = True
|
| 291 |
+
elif args.disable_thinking:
|
| 292 |
+
kwargs["enable_thinking"] = False
|
| 293 |
+
|
| 294 |
+
try:
|
| 295 |
+
text = tokenizer.apply_chat_template(messages, **kwargs)
|
| 296 |
+
except TypeError:
|
| 297 |
+
# Some tokenizers do not accept enable_thinking.
|
| 298 |
+
kwargs.pop("enable_thinking", None)
|
| 299 |
+
text = tokenizer.apply_chat_template(messages, **kwargs)
|
| 300 |
+
|
| 301 |
+
if args.assistant_prefix is not None:
|
| 302 |
+
text += args.assistant_prefix.encode("utf-8").decode("unicode_escape")
|
| 303 |
+
|
| 304 |
+
return text
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def main():
|
| 308 |
+
args = parse_args()
|
| 309 |
+
|
| 310 |
+
if args.gpu_ids is not None:
|
| 311 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu_ids
|
| 312 |
+
|
| 313 |
+
if args.hf_cache is not None:
|
| 314 |
+
os.environ["HF_HOME"] = args.hf_cache
|
| 315 |
+
os.environ["HF_DATASETS_CACHE"] = str(Path(args.hf_cache) / "datasets")
|
| 316 |
+
os.environ["HF_HUB_CACHE"] = str(Path(args.hf_cache) / "hub")
|
| 317 |
+
|
| 318 |
+
# Import after CUDA_VISIBLE_DEVICES is set.
|
| 319 |
+
from datasets import load_dataset
|
| 320 |
+
from transformers import AutoTokenizer
|
| 321 |
+
from vllm import LLM, SamplingParams
|
| 322 |
+
|
| 323 |
+
output_path = Path(args.output)
|
| 324 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 325 |
+
|
| 326 |
+
print("=" * 80)
|
| 327 |
+
print("AIME 2025 vLLM Evaluation")
|
| 328 |
+
print("=" * 80)
|
| 329 |
+
print(f"model: {args.model}")
|
| 330 |
+
print(f"dataset: {args.dataset}")
|
| 331 |
+
print(f"num_gpus / TP size: {args.num_gpus}")
|
| 332 |
+
print(f"CUDA_VISIBLE_DEVICES: {os.environ.get('CUDA_VISIBLE_DEVICES')}")
|
| 333 |
+
print(f"n_samples/problem: {args.n_samples}")
|
| 334 |
+
print(f"temperature: {args.temperature}")
|
| 335 |
+
print(f"top_p: {args.top_p}")
|
| 336 |
+
print(f"max_tokens: {args.max_tokens}")
|
| 337 |
+
print(f"prompt_template: {args.prompt_template}")
|
| 338 |
+
print(f"assistant_prefix: {repr(args.assistant_prefix)}")
|
| 339 |
+
print(f"output: {args.output}")
|
| 340 |
+
print("=" * 80)
|
| 341 |
+
|
| 342 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 343 |
+
args.model,
|
| 344 |
+
trust_remote_code=args.trust_remote_code,
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
# Load HF dataset or local JSON/JSONL.
|
| 348 |
+
if args.dataset.endswith(".json") or args.dataset.endswith(".jsonl"):
|
| 349 |
+
dataset_dict = load_dataset("json", data_files=args.dataset)
|
| 350 |
+
else:
|
| 351 |
+
dataset_dict = load_dataset(args.dataset)
|
| 352 |
+
|
| 353 |
+
split = args.split or list(dataset_dict.keys())[0]
|
| 354 |
+
data = dataset_dict[split]
|
| 355 |
+
|
| 356 |
+
assert len(data) == 30, f"Expected 30 AIME 2025 problems, got {len(data)}"
|
| 357 |
+
|
| 358 |
+
print(f"Loaded AIME 2025 examples: {len(data)}")
|
| 359 |
+
for i in range(min(3, len(data))):
|
| 360 |
+
print(i, {k: data[i].get(k, None) for k in data.column_names[:6]})
|
| 361 |
+
|
| 362 |
+
if args.limit is not None:
|
| 363 |
+
data = data.select(range(min(args.limit, len(data))))
|
| 364 |
+
|
| 365 |
+
print(f"Loaded split: {split}")
|
| 366 |
+
print(f"Number of problems: {len(data)}")
|
| 367 |
+
print(f"First example keys: {list(data[0].keys())}")
|
| 368 |
+
|
| 369 |
+
prompts = []
|
| 370 |
+
examples = []
|
| 371 |
+
|
| 372 |
+
for idx, ex in enumerate(data):
|
| 373 |
+
problem = get_field(ex, ["problem", "question", "prompt"])
|
| 374 |
+
gold_raw = get_field(ex, ["answer", "final_answer", "target", "solution"])
|
| 375 |
+
gold = normalize_answer(gold_raw)
|
| 376 |
+
|
| 377 |
+
if args.prompt_template == "train":
|
| 378 |
+
# Raw ChatML prompt matching the OPD training parquet.
|
| 379 |
+
# Do NOT call apply_chat_template again, otherwise ChatML will be nested.
|
| 380 |
+
full_prompt = build_exact_training_prompt(problem)
|
| 381 |
+
elif args.prompt_template == "paper":
|
| 382 |
+
# Raw ChatML prompt matching the paper-style math evaluation prompt.
|
| 383 |
+
# Do NOT call apply_chat_template again.
|
| 384 |
+
full_prompt = build_paper_math_eval_prompt(problem)
|
| 385 |
+
elif args.prompt_template == "chat":
|
| 386 |
+
# Normal user-content prompt; tokenizer will add ChatML.
|
| 387 |
+
raw_prompt = build_problem_prompt(problem)
|
| 388 |
+
full_prompt = apply_chat_template(tokenizer, raw_prompt, args)
|
| 389 |
+
else:
|
| 390 |
+
raise ValueError(f"Unknown prompt_template: {args.prompt_template}")
|
| 391 |
+
|
| 392 |
+
# Important: when using raw ChatML prompts, apply_chat_template() is bypassed.
|
| 393 |
+
# Therefore --assistant-prefix must be appended here, not only inside apply_chat_template().
|
| 394 |
+
# This is useful for Qwen3 no-thinking style, e.g.:
|
| 395 |
+
# --assistant-prefix '<think>\\n\\n</think>\\n\\n'
|
| 396 |
+
if args.prompt_template in {"train", "paper"} and args.assistant_prefix is not None:
|
| 397 |
+
full_prompt += args.assistant_prefix.encode("utf-8").decode("unicode_escape")
|
| 398 |
+
|
| 399 |
+
prompts.append(full_prompt)
|
| 400 |
+
examples.append(
|
| 401 |
+
{
|
| 402 |
+
"idx": idx,
|
| 403 |
+
"problem": problem,
|
| 404 |
+
"gold_raw": gold_raw,
|
| 405 |
+
"gold": gold,
|
| 406 |
+
"prompt": full_prompt,
|
| 407 |
+
}
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
llm = LLM(
|
| 411 |
+
model=args.model,
|
| 412 |
+
tensor_parallel_size=args.num_gpus,
|
| 413 |
+
dtype=args.dtype,
|
| 414 |
+
trust_remote_code=args.trust_remote_code,
|
| 415 |
+
gpu_memory_utilization=args.gpu_memory_utilization,
|
| 416 |
+
seed=args.seed,
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
sampling_params = SamplingParams(
|
| 420 |
+
n=args.n_samples,
|
| 421 |
+
temperature=args.temperature,
|
| 422 |
+
top_p=args.top_p,
|
| 423 |
+
max_tokens=args.max_tokens,
|
| 424 |
+
stop=["<|im_end|>"],
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
outputs = llm.generate(prompts, sampling_params)
|
| 428 |
+
|
| 429 |
+
total = 0
|
| 430 |
+
correct = 0
|
| 431 |
+
per_problem_records = []
|
| 432 |
+
|
| 433 |
+
with output_path.open("w", encoding="utf-8") as f:
|
| 434 |
+
for ex, out in zip(examples, outputs):
|
| 435 |
+
sample_records = []
|
| 436 |
+
problem_correct = 0
|
| 437 |
+
|
| 438 |
+
for sample_id, completion in enumerate(out.outputs):
|
| 439 |
+
text = completion.text
|
| 440 |
+
pred = extract_answer(text)
|
| 441 |
+
is_correct = pred == ex["gold"]
|
| 442 |
+
|
| 443 |
+
total += 1
|
| 444 |
+
correct += int(is_correct)
|
| 445 |
+
problem_correct += int(is_correct)
|
| 446 |
+
|
| 447 |
+
record = {
|
| 448 |
+
"idx": ex["idx"],
|
| 449 |
+
"sample_id": sample_id,
|
| 450 |
+
"gold": ex["gold"],
|
| 451 |
+
"gold_raw": str(ex["gold_raw"]),
|
| 452 |
+
"pred": pred,
|
| 453 |
+
"correct": is_correct,
|
| 454 |
+
"completion": text,
|
| 455 |
+
"finish_reason": completion.finish_reason,
|
| 456 |
+
"stop_reason": getattr(completion, "stop_reason", None),
|
| 457 |
+
}
|
| 458 |
+
sample_records.append(record)
|
| 459 |
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
| 460 |
+
|
| 461 |
+
problem_acc = problem_correct / max(1, len(out.outputs))
|
| 462 |
+
per_problem_records.append(problem_acc)
|
| 463 |
+
|
| 464 |
+
if ex["idx"] < args.print_examples:
|
| 465 |
+
print("-" * 80)
|
| 466 |
+
print(f"Problem {ex['idx']}")
|
| 467 |
+
print(f"Gold: {ex['gold']} | Correct samples: {problem_correct}/{len(out.outputs)}")
|
| 468 |
+
print(f"First pred: {sample_records[0]['pred']}")
|
| 469 |
+
print(f"First completion preview:\n{sample_records[0]['completion'][:1000]}")
|
| 470 |
+
|
| 471 |
+
avg_pass1_micro = correct / total if total > 0 else 0.0
|
| 472 |
+
avg_pass1_macro = sum(per_problem_records) / len(per_problem_records) if per_problem_records else 0.0
|
| 473 |
+
|
| 474 |
+
print("=" * 80)
|
| 475 |
+
print("Final results")
|
| 476 |
+
print("=" * 80)
|
| 477 |
+
print(f"Problems: {len(examples)}")
|
| 478 |
+
print(f"Samples per problem: {args.n_samples}")
|
| 479 |
+
print(f"Total samples: {total}")
|
| 480 |
+
print(f"Correct samples: {correct}")
|
| 481 |
+
print(f"Average pass@1 micro: {avg_pass1_micro:.6f} ({100 * avg_pass1_micro:.2f}%)")
|
| 482 |
+
print(f"Average pass@1 macro: {avg_pass1_macro:.6f} ({100 * avg_pass1_macro:.2f}%)")
|
| 483 |
+
print(f"Saved generations to: {output_path}")
|
| 484 |
+
print("=" * 80)
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
if __name__ == "__main__":
|
| 488 |
+
main()
|
tools/eval_hmmt2025_vllm.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
"""
|
| 3 |
+
Evaluate a HF-format model on HMMT Feb 2025 using vLLM.
|
| 4 |
+
|
| 5 |
+
Example:
|
| 6 |
+
|
| 7 |
+
CUDA_VISIBLE_DEVICES=0,1,2,3 python tools/eval_aime_vllm.py \
|
| 8 |
+
--model /workspace/Lightning-OPD/checkpoints/qwen3-4b-lightning-opd-hf \
|
| 9 |
+
--num-gpus 4 \
|
| 10 |
+
--output outputs/eval_hmmt_feb_2025_qwen3_4b_lightning_opd.jsonl
|
| 11 |
+
|
| 12 |
+
Paper-style AIME setting:
|
| 13 |
+
temperature = 0.6
|
| 14 |
+
top_p = 0.95
|
| 15 |
+
max_tokens = 32768
|
| 16 |
+
n_samples = 32
|
| 17 |
+
metric = average pass@1
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import argparse
|
| 21 |
+
import json
|
| 22 |
+
import os
|
| 23 |
+
import re
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
from typing import Any, Dict, List, Optional
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def parse_args():
|
| 29 |
+
parser = argparse.ArgumentParser()
|
| 30 |
+
|
| 31 |
+
# Model / hardware
|
| 32 |
+
parser.add_argument(
|
| 33 |
+
"--model",
|
| 34 |
+
type=str,
|
| 35 |
+
required=True,
|
| 36 |
+
help="Path to HF-format model checkpoint.",
|
| 37 |
+
)
|
| 38 |
+
parser.add_argument(
|
| 39 |
+
"--num-gpus",
|
| 40 |
+
type=int,
|
| 41 |
+
default=1,
|
| 42 |
+
help="Tensor parallel size for vLLM.",
|
| 43 |
+
)
|
| 44 |
+
parser.add_argument(
|
| 45 |
+
"--gpu-ids",
|
| 46 |
+
type=str,
|
| 47 |
+
default=None,
|
| 48 |
+
help='Optional CUDA_VISIBLE_DEVICES, e.g. "0,1,2,3". Must match --num-gpus.',
|
| 49 |
+
)
|
| 50 |
+
parser.add_argument(
|
| 51 |
+
"--dtype",
|
| 52 |
+
type=str,
|
| 53 |
+
default="bfloat16",
|
| 54 |
+
choices=["auto", "float16", "bfloat16", "float32"],
|
| 55 |
+
)
|
| 56 |
+
parser.add_argument(
|
| 57 |
+
"--gpu-memory-utilization",
|
| 58 |
+
type=float,
|
| 59 |
+
default=0.90,
|
| 60 |
+
)
|
| 61 |
+
parser.add_argument(
|
| 62 |
+
"--trust-remote-code",
|
| 63 |
+
action="store_true",
|
| 64 |
+
default=True,
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
# Dataset
|
| 68 |
+
parser.add_argument(
|
| 69 |
+
"--dataset",
|
| 70 |
+
type=str,
|
| 71 |
+
default="MathArena/hmmt_feb_2025",
|
| 72 |
+
help="HF dataset name for HMMT February 2025.",
|
| 73 |
+
)
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--split",
|
| 76 |
+
type=str,
|
| 77 |
+
default=None,
|
| 78 |
+
help="Dataset split. If not set, use the first available split.",
|
| 79 |
+
)
|
| 80 |
+
parser.add_argument(
|
| 81 |
+
"--hf-cache",
|
| 82 |
+
type=str,
|
| 83 |
+
default=None,
|
| 84 |
+
help="Optional HuggingFace cache dir.",
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# Generation settings from paper
|
| 88 |
+
parser.add_argument("--n-samples", type=int, default=32)
|
| 89 |
+
parser.add_argument("--temperature", type=float, default=0.6)
|
| 90 |
+
parser.add_argument("--top-p", type=float, default=0.95)
|
| 91 |
+
parser.add_argument("--max-tokens", type=int, default=32768)
|
| 92 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 93 |
+
|
| 94 |
+
# Prompt / tokenizer behavior
|
| 95 |
+
parser.add_argument(
|
| 96 |
+
"--no-chat-template",
|
| 97 |
+
action="store_true",
|
| 98 |
+
help="Do not apply tokenizer chat template; use raw prompt.",
|
| 99 |
+
)
|
| 100 |
+
parser.add_argument(
|
| 101 |
+
"--enable-thinking",
|
| 102 |
+
action="store_true",
|
| 103 |
+
default=False,
|
| 104 |
+
help="Pass enable_thinking=True to Qwen3 chat template if supported.",
|
| 105 |
+
)
|
| 106 |
+
parser.add_argument(
|
| 107 |
+
"--disable-thinking",
|
| 108 |
+
action="store_true",
|
| 109 |
+
default=False,
|
| 110 |
+
help="Pass enable_thinking=False to Qwen3 chat template if supported.",
|
| 111 |
+
)
|
| 112 |
+
parser.add_argument(
|
| 113 |
+
"--assistant-prefix",
|
| 114 |
+
type=str,
|
| 115 |
+
default=None,
|
| 116 |
+
help=(
|
| 117 |
+
"Optional text appended after the assistant generation prompt. "
|
| 118 |
+
"Example for no-thinking style: '<think>\\n\\n</think>\\n\\n'"
|
| 119 |
+
),
|
| 120 |
+
)
|
| 121 |
+
parser.add_argument(
|
| 122 |
+
"--prompt-template",
|
| 123 |
+
type=str,
|
| 124 |
+
default="train",
|
| 125 |
+
choices=["train", "paper", "chat"],
|
| 126 |
+
help=(
|
| 127 |
+
"Prompt template to use. "
|
| 128 |
+
"'train' uses the exact training ChatML prompt; "
|
| 129 |
+
"'paper' uses the paper-style ChatML prompt; "
|
| 130 |
+
"'chat' uses build_problem_prompt plus tokenizer.apply_chat_template."
|
| 131 |
+
),
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
# Output / debug
|
| 135 |
+
parser.add_argument(
|
| 136 |
+
"--output",
|
| 137 |
+
type=str,
|
| 138 |
+
default="outputs/eval_hmmt_feb_2025_vllm.jsonl",
|
| 139 |
+
help="Path to save per-sample generations and scores.",
|
| 140 |
+
)
|
| 141 |
+
parser.add_argument(
|
| 142 |
+
"--limit",
|
| 143 |
+
type=int,
|
| 144 |
+
default=None,
|
| 145 |
+
help="Limit number of problems for quick debugging.",
|
| 146 |
+
)
|
| 147 |
+
parser.add_argument(
|
| 148 |
+
"--print-examples",
|
| 149 |
+
type=int,
|
| 150 |
+
default=3,
|
| 151 |
+
help="Print first N examples with predictions.",
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
return parser.parse_args()
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def normalize_answer(x: Any) -> Optional[str]:
|
| 158 |
+
if x is None:
|
| 159 |
+
return None
|
| 160 |
+
|
| 161 |
+
s = str(x).strip()
|
| 162 |
+
s = s.replace("$", "")
|
| 163 |
+
s = s.replace(",", "")
|
| 164 |
+
s = s.replace("\\,", "")
|
| 165 |
+
s = s.strip()
|
| 166 |
+
|
| 167 |
+
# Remove simple latex wrappers.
|
| 168 |
+
s = s.replace("\\text", "")
|
| 169 |
+
s = s.replace("\\mathrm", "")
|
| 170 |
+
s = s.strip("{}").strip()
|
| 171 |
+
|
| 172 |
+
# AIME answers are integers from 0 to 999.
|
| 173 |
+
m = re.search(r"-?\d+", s)
|
| 174 |
+
if m is None:
|
| 175 |
+
return None
|
| 176 |
+
|
| 177 |
+
try:
|
| 178 |
+
return str(int(m.group(0)))
|
| 179 |
+
except ValueError:
|
| 180 |
+
return None
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def extract_last_boxed(text: str) -> Optional[str]:
|
| 184 |
+
"""
|
| 185 |
+
Extract the last \\boxed{...}. Handles simple nested braces better than regex.
|
| 186 |
+
"""
|
| 187 |
+
marker = r"\boxed{"
|
| 188 |
+
positions = [m.start() for m in re.finditer(re.escape(marker), text)]
|
| 189 |
+
if not positions:
|
| 190 |
+
return None
|
| 191 |
+
|
| 192 |
+
for start in reversed(positions):
|
| 193 |
+
i = start + len(marker)
|
| 194 |
+
depth = 1
|
| 195 |
+
chars = []
|
| 196 |
+
|
| 197 |
+
while i < len(text):
|
| 198 |
+
ch = text[i]
|
| 199 |
+
if ch == "{":
|
| 200 |
+
depth += 1
|
| 201 |
+
chars.append(ch)
|
| 202 |
+
elif ch == "}":
|
| 203 |
+
depth -= 1
|
| 204 |
+
if depth == 0:
|
| 205 |
+
return "".join(chars)
|
| 206 |
+
chars.append(ch)
|
| 207 |
+
else:
|
| 208 |
+
chars.append(ch)
|
| 209 |
+
i += 1
|
| 210 |
+
|
| 211 |
+
return None
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def extract_answer(text: str) -> Optional[str]:
|
| 215 |
+
# Prefer boxed answer, because the actual training prompt asks for \boxed{$Answer}.
|
| 216 |
+
boxed = extract_last_boxed(text)
|
| 217 |
+
if boxed is not None:
|
| 218 |
+
ans = normalize_answer(boxed)
|
| 219 |
+
if ans is not None:
|
| 220 |
+
return ans
|
| 221 |
+
|
| 222 |
+
# Fallback: final Answer: line.
|
| 223 |
+
matches = re.findall(r"Answer:\s*([^\n]+)", text, flags=re.IGNORECASE)
|
| 224 |
+
if matches:
|
| 225 |
+
ans = normalize_answer(matches[-1])
|
| 226 |
+
if ans is not None:
|
| 227 |
+
return ans
|
| 228 |
+
|
| 229 |
+
# No valid final answer.
|
| 230 |
+
return None
|
| 231 |
+
|
| 232 |
+
def build_exact_training_prompt(problem: str) -> str:
|
| 233 |
+
return (
|
| 234 |
+
"<|im_start|>user\n"
|
| 235 |
+
"Solve the following math problem step by step. "
|
| 236 |
+
"The last line of your response should be of the form "
|
| 237 |
+
"Answer: \\boxed{$Answer} where $Answer is the answer to the problem.\n\n"
|
| 238 |
+
f"{problem}\n\n"
|
| 239 |
+
"Remember to put your answer on its own line after \"Answer:\"."
|
| 240 |
+
"<|im_end|>\n"
|
| 241 |
+
"<|im_start|>assistant\n\n\n\n"
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
def build_problem_prompt(problem: str) -> str:
|
| 245 |
+
return (
|
| 246 |
+
f"{problem}\n\n"
|
| 247 |
+
"Please reason step by step, and put your final answer within \\boxed{}."
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
def build_paper_math_eval_prompt(problem: str) -> str:
|
| 251 |
+
return (
|
| 252 |
+
"<|im_start|>user\n"
|
| 253 |
+
f"Question: {problem}\n"
|
| 254 |
+
"Please reason step by step, and put your final answer within \\boxed{}.\n"
|
| 255 |
+
"<|im_end|>\n"
|
| 256 |
+
"<|im_start|>assistant\n"
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
def get_field(ex: Dict[str, Any], candidates: List[str]) -> Any:
|
| 260 |
+
for key in candidates:
|
| 261 |
+
if key in ex and ex[key] is not None:
|
| 262 |
+
return ex[key]
|
| 263 |
+
raise KeyError(f"Cannot find any of {candidates}. Example keys: {list(ex.keys())}")
|
| 264 |
+
|
| 265 |
+
# def build_problem_prompt(problem: str) -> str:
|
| 266 |
+
# return (
|
| 267 |
+
# "Solve the following math problem step by step. "
|
| 268 |
+
# "The last line of your response should be of the form "
|
| 269 |
+
# "Answer: $Answer (without quotes) where $Answer is the answer to the problem.\n\n"
|
| 270 |
+
# f"{problem}\n\n"
|
| 271 |
+
# 'Remember to put your answer on its own line after "Answer:".'
|
| 272 |
+
# )
|
| 273 |
+
|
| 274 |
+
def apply_chat_template(tokenizer, prompt: str, args) -> str:
|
| 275 |
+
if args.no_chat_template:
|
| 276 |
+
return prompt
|
| 277 |
+
|
| 278 |
+
messages = [{"role": "user", "content": prompt}]
|
| 279 |
+
|
| 280 |
+
kwargs = {
|
| 281 |
+
"tokenize": False,
|
| 282 |
+
"add_generation_prompt": True,
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
# Qwen3 tokenizer may support enable_thinking.
|
| 286 |
+
if args.enable_thinking and args.disable_thinking:
|
| 287 |
+
raise ValueError("Do not set both --enable-thinking and --disable-thinking.")
|
| 288 |
+
|
| 289 |
+
if args.enable_thinking:
|
| 290 |
+
kwargs["enable_thinking"] = True
|
| 291 |
+
elif args.disable_thinking:
|
| 292 |
+
kwargs["enable_thinking"] = False
|
| 293 |
+
|
| 294 |
+
try:
|
| 295 |
+
text = tokenizer.apply_chat_template(messages, **kwargs)
|
| 296 |
+
except TypeError:
|
| 297 |
+
# Some tokenizers do not accept enable_thinking.
|
| 298 |
+
kwargs.pop("enable_thinking", None)
|
| 299 |
+
text = tokenizer.apply_chat_template(messages, **kwargs)
|
| 300 |
+
|
| 301 |
+
if args.assistant_prefix is not None:
|
| 302 |
+
text += args.assistant_prefix.encode("utf-8").decode("unicode_escape")
|
| 303 |
+
|
| 304 |
+
return text
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def main():
|
| 308 |
+
args = parse_args()
|
| 309 |
+
|
| 310 |
+
if args.gpu_ids is not None:
|
| 311 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu_ids
|
| 312 |
+
|
| 313 |
+
if args.hf_cache is not None:
|
| 314 |
+
os.environ["HF_HOME"] = args.hf_cache
|
| 315 |
+
os.environ["HF_DATASETS_CACHE"] = str(Path(args.hf_cache) / "datasets")
|
| 316 |
+
os.environ["HF_HUB_CACHE"] = str(Path(args.hf_cache) / "hub")
|
| 317 |
+
|
| 318 |
+
# Import after CUDA_VISIBLE_DEVICES is set.
|
| 319 |
+
from datasets import load_dataset
|
| 320 |
+
from transformers import AutoTokenizer
|
| 321 |
+
from vllm import LLM, SamplingParams
|
| 322 |
+
|
| 323 |
+
output_path = Path(args.output)
|
| 324 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 325 |
+
|
| 326 |
+
print("=" * 80)
|
| 327 |
+
print("HMMT Feb 2025 vLLM Evaluation")
|
| 328 |
+
print("=" * 80)
|
| 329 |
+
print(f"model: {args.model}")
|
| 330 |
+
print(f"dataset: {args.dataset}")
|
| 331 |
+
print(f"num_gpus / TP size: {args.num_gpus}")
|
| 332 |
+
print(f"CUDA_VISIBLE_DEVICES: {os.environ.get('CUDA_VISIBLE_DEVICES')}")
|
| 333 |
+
print(f"n_samples/problem: {args.n_samples}")
|
| 334 |
+
print(f"temperature: {args.temperature}")
|
| 335 |
+
print(f"top_p: {args.top_p}")
|
| 336 |
+
print(f"max_tokens: {args.max_tokens}")
|
| 337 |
+
print(f"prompt_template: {args.prompt_template}")
|
| 338 |
+
print(f"assistant_prefix: {repr(args.assistant_prefix)}")
|
| 339 |
+
print(f"output: {args.output}")
|
| 340 |
+
print("=" * 80)
|
| 341 |
+
|
| 342 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 343 |
+
args.model,
|
| 344 |
+
trust_remote_code=args.trust_remote_code,
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
# Load HF dataset or local JSON/JSONL.
|
| 348 |
+
if args.dataset.endswith(".json") or args.dataset.endswith(".jsonl"):
|
| 349 |
+
dataset_dict = load_dataset("json", data_files=args.dataset)
|
| 350 |
+
else:
|
| 351 |
+
dataset_dict = load_dataset(args.dataset)
|
| 352 |
+
|
| 353 |
+
split = args.split or list(dataset_dict.keys())[0]
|
| 354 |
+
data = dataset_dict[split]
|
| 355 |
+
|
| 356 |
+
print(f"Loaded HMMT Feb 2025 examples: {len(data)}")
|
| 357 |
+
for i in range(min(3, len(data))):
|
| 358 |
+
print(i, {k: data[i].get(k, None) for k in data.column_names[:6]})
|
| 359 |
+
|
| 360 |
+
if args.limit is not None:
|
| 361 |
+
data = data.select(range(min(args.limit, len(data))))
|
| 362 |
+
|
| 363 |
+
print(f"Loaded split: {split}")
|
| 364 |
+
print(f"Number of problems: {len(data)}")
|
| 365 |
+
print(f"First example keys: {list(data[0].keys())}")
|
| 366 |
+
|
| 367 |
+
prompts = []
|
| 368 |
+
examples = []
|
| 369 |
+
|
| 370 |
+
for idx, ex in enumerate(data):
|
| 371 |
+
problem = get_field(ex, ["problem", "question", "prompt"])
|
| 372 |
+
gold_raw = get_field(ex, ["answer", "final_answer", "target", "solution"])
|
| 373 |
+
gold = normalize_answer(gold_raw)
|
| 374 |
+
|
| 375 |
+
if args.prompt_template == "train":
|
| 376 |
+
# Raw ChatML prompt matching the OPD training parquet.
|
| 377 |
+
# Do NOT call apply_chat_template again, otherwise ChatML will be nested.
|
| 378 |
+
full_prompt = build_exact_training_prompt(problem)
|
| 379 |
+
elif args.prompt_template == "paper":
|
| 380 |
+
# Raw ChatML prompt matching the paper-style math evaluation prompt.
|
| 381 |
+
# Do NOT call apply_chat_template again.
|
| 382 |
+
full_prompt = build_paper_math_eval_prompt(problem)
|
| 383 |
+
elif args.prompt_template == "chat":
|
| 384 |
+
# Normal user-content prompt; tokenizer will add ChatML.
|
| 385 |
+
raw_prompt = build_problem_prompt(problem)
|
| 386 |
+
full_prompt = apply_chat_template(tokenizer, raw_prompt, args)
|
| 387 |
+
else:
|
| 388 |
+
raise ValueError(f"Unknown prompt_template: {args.prompt_template}")
|
| 389 |
+
|
| 390 |
+
# Important: when using raw ChatML prompts, apply_chat_template() is bypassed.
|
| 391 |
+
# Therefore --assistant-prefix must be appended here, not only inside apply_chat_template().
|
| 392 |
+
# This is useful for Qwen3 no-thinking style, e.g.:
|
| 393 |
+
# --assistant-prefix '<think>\\n\\n</think>\\n\\n'
|
| 394 |
+
if args.prompt_template in {"train", "paper"} and args.assistant_prefix is not None:
|
| 395 |
+
full_prompt += args.assistant_prefix.encode("utf-8").decode("unicode_escape")
|
| 396 |
+
|
| 397 |
+
prompts.append(full_prompt)
|
| 398 |
+
examples.append(
|
| 399 |
+
{
|
| 400 |
+
"idx": idx,
|
| 401 |
+
"problem": problem,
|
| 402 |
+
"gold_raw": gold_raw,
|
| 403 |
+
"gold": gold,
|
| 404 |
+
"prompt": full_prompt,
|
| 405 |
+
}
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
llm = LLM(
|
| 409 |
+
model=args.model,
|
| 410 |
+
tensor_parallel_size=args.num_gpus,
|
| 411 |
+
dtype=args.dtype,
|
| 412 |
+
trust_remote_code=args.trust_remote_code,
|
| 413 |
+
gpu_memory_utilization=args.gpu_memory_utilization,
|
| 414 |
+
seed=args.seed,
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
sampling_params = SamplingParams(
|
| 418 |
+
n=args.n_samples,
|
| 419 |
+
temperature=args.temperature,
|
| 420 |
+
top_p=args.top_p,
|
| 421 |
+
max_tokens=args.max_tokens,
|
| 422 |
+
stop=["<|im_end|>"],
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
outputs = llm.generate(prompts, sampling_params)
|
| 426 |
+
|
| 427 |
+
total = 0
|
| 428 |
+
correct = 0
|
| 429 |
+
per_problem_records = []
|
| 430 |
+
|
| 431 |
+
with output_path.open("w", encoding="utf-8") as f:
|
| 432 |
+
for ex, out in zip(examples, outputs):
|
| 433 |
+
sample_records = []
|
| 434 |
+
problem_correct = 0
|
| 435 |
+
|
| 436 |
+
for sample_id, completion in enumerate(out.outputs):
|
| 437 |
+
text = completion.text
|
| 438 |
+
pred = extract_answer(text)
|
| 439 |
+
is_correct = pred == ex["gold"]
|
| 440 |
+
|
| 441 |
+
total += 1
|
| 442 |
+
correct += int(is_correct)
|
| 443 |
+
problem_correct += int(is_correct)
|
| 444 |
+
|
| 445 |
+
record = {
|
| 446 |
+
"idx": ex["idx"],
|
| 447 |
+
"sample_id": sample_id,
|
| 448 |
+
"gold": ex["gold"],
|
| 449 |
+
"gold_raw": str(ex["gold_raw"]),
|
| 450 |
+
"pred": pred,
|
| 451 |
+
"correct": is_correct,
|
| 452 |
+
"completion": text,
|
| 453 |
+
"finish_reason": completion.finish_reason,
|
| 454 |
+
"stop_reason": getattr(completion, "stop_reason", None),
|
| 455 |
+
}
|
| 456 |
+
sample_records.append(record)
|
| 457 |
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
| 458 |
+
|
| 459 |
+
problem_acc = problem_correct / max(1, len(out.outputs))
|
| 460 |
+
per_problem_records.append(problem_acc)
|
| 461 |
+
|
| 462 |
+
if ex["idx"] < args.print_examples:
|
| 463 |
+
print("-" * 80)
|
| 464 |
+
print(f"Problem {ex['idx']}")
|
| 465 |
+
print(f"Gold: {ex['gold']} | Correct samples: {problem_correct}/{len(out.outputs)}")
|
| 466 |
+
print(f"First pred: {sample_records[0]['pred']}")
|
| 467 |
+
print(f"First completion preview:\n{sample_records[0]['completion'][:1000]}")
|
| 468 |
+
|
| 469 |
+
avg_pass1_micro = correct / total if total > 0 else 0.0
|
| 470 |
+
avg_pass1_macro = sum(per_problem_records) / len(per_problem_records) if per_problem_records else 0.0
|
| 471 |
+
|
| 472 |
+
print("=" * 80)
|
| 473 |
+
print("Final results")
|
| 474 |
+
print("=" * 80)
|
| 475 |
+
print(f"Problems: {len(examples)}")
|
| 476 |
+
print(f"Samples per problem: {args.n_samples}")
|
| 477 |
+
print(f"Total samples: {total}")
|
| 478 |
+
print(f"Correct samples: {correct}")
|
| 479 |
+
print(f"Average pass@1 micro: {avg_pass1_micro:.6f} ({100 * avg_pass1_micro:.2f}%)")
|
| 480 |
+
print(f"Average pass@1 macro: {avg_pass1_macro:.6f} ({100 * avg_pass1_macro:.2f}%)")
|
| 481 |
+
print(f"Saved generations to: {output_path}")
|
| 482 |
+
print("=" * 80)
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
if __name__ == "__main__":
|
| 486 |
+
main()
|
tools/fp8_cast_bf16.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
# Adapt from https://github.com/alibaba/Pai-Megatron-Patch/blob/2b201af08336dea0403df7c6b497c964cf5a2e75/toolkits/model_checkpoints_convertor/deepseek/fp8_cast_bf16.py
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
from argparse import ArgumentParser
|
| 8 |
+
from glob import glob
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import triton
|
| 12 |
+
import triton.language as tl
|
| 13 |
+
from safetensors.torch import load_file, save_file
|
| 14 |
+
from tqdm import tqdm
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@triton.jit
|
| 18 |
+
def weight_dequant_kernel(x_ptr, s_ptr, y_ptr, M, N, BLOCK_SIZE: tl.constexpr):
|
| 19 |
+
pid_m = tl.program_id(axis=0)
|
| 20 |
+
pid_n = tl.program_id(axis=1)
|
| 21 |
+
n = tl.cdiv(N, BLOCK_SIZE)
|
| 22 |
+
offs_m = pid_m * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
|
| 23 |
+
offs_n = pid_n * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
|
| 24 |
+
offs = offs_m[:, None] * N + offs_n[None, :]
|
| 25 |
+
mask = (offs_m[:, None] < M) & (offs_n[None, :] < N)
|
| 26 |
+
x = tl.load(x_ptr + offs, mask=mask).to(tl.float32)
|
| 27 |
+
s = tl.load(s_ptr + pid_m * n + pid_n)
|
| 28 |
+
y = x * s
|
| 29 |
+
tl.store(y_ptr + offs, y, mask=mask)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def weight_dequant(x: torch.Tensor, s: torch.Tensor, block_size: int = 128) -> torch.Tensor:
|
| 33 |
+
assert x.is_contiguous() and s.is_contiguous()
|
| 34 |
+
assert x.dim() == 2 and s.dim() == 2
|
| 35 |
+
M, N = x.size()
|
| 36 |
+
y = torch.empty_like(x, dtype=torch.get_default_dtype())
|
| 37 |
+
|
| 38 |
+
def grid(meta):
|
| 39 |
+
return (triton.cdiv(M, meta["BLOCK_SIZE"]), triton.cdiv(N, meta["BLOCK_SIZE"]))
|
| 40 |
+
|
| 41 |
+
weight_dequant_kernel[grid](x, s, y, M, N, BLOCK_SIZE=block_size)
|
| 42 |
+
return y
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def main(fp8_path, bf16_path):
|
| 46 |
+
torch.set_default_dtype(torch.bfloat16)
|
| 47 |
+
os.makedirs(bf16_path, exist_ok=True)
|
| 48 |
+
os.system("cp -rf " + fp8_path + "/config.json " + bf16_path)
|
| 49 |
+
os.system("cp -rf " + fp8_path + "/*.py " + bf16_path)
|
| 50 |
+
os.system("cp -rf " + fp8_path + "/tokenizer* " + bf16_path)
|
| 51 |
+
os.system("cp -rf " + fp8_path + "/chat_template* " + bf16_path)
|
| 52 |
+
model_index_file = os.path.join(fp8_path, "model.safetensors.index.json")
|
| 53 |
+
with open(model_index_file) as f:
|
| 54 |
+
model_index = json.load(f)
|
| 55 |
+
weight_map = model_index["weight_map"]
|
| 56 |
+
|
| 57 |
+
# Cache for loaded safetensor files
|
| 58 |
+
loaded_files = {}
|
| 59 |
+
fp8_weight_names = []
|
| 60 |
+
|
| 61 |
+
# Helper function to get tensor from the correct file
|
| 62 |
+
def get_tensor(tensor_name):
|
| 63 |
+
file_name = weight_map[tensor_name]
|
| 64 |
+
if file_name not in loaded_files:
|
| 65 |
+
file_path = os.path.join(fp8_path, file_name)
|
| 66 |
+
loaded_files[file_name] = load_file(file_path, device="cuda")
|
| 67 |
+
return loaded_files[file_name][tensor_name]
|
| 68 |
+
|
| 69 |
+
safetensor_files = list(glob(os.path.join(fp8_path, "*.safetensors")))
|
| 70 |
+
safetensor_files.sort()
|
| 71 |
+
for safetensor_file in tqdm(safetensor_files):
|
| 72 |
+
print(f"Handling file: {safetensor_file}")
|
| 73 |
+
file_name = os.path.basename(safetensor_file)
|
| 74 |
+
current_state_dict = load_file(safetensor_file, device="cuda")
|
| 75 |
+
loaded_files[file_name] = current_state_dict
|
| 76 |
+
|
| 77 |
+
new_state_dict = {}
|
| 78 |
+
for weight_name, weight in current_state_dict.items():
|
| 79 |
+
if weight_name.endswith("_scale_inv"):
|
| 80 |
+
continue
|
| 81 |
+
elif weight.element_size() == 1: # FP8 weight
|
| 82 |
+
scale_inv_name = f"{weight_name}_scale_inv"
|
| 83 |
+
try:
|
| 84 |
+
# Get scale_inv from the correct file
|
| 85 |
+
scale_inv = get_tensor(scale_inv_name)
|
| 86 |
+
fp8_weight_names.append(weight_name)
|
| 87 |
+
new_state_dict[weight_name] = weight_dequant(weight, scale_inv)
|
| 88 |
+
except KeyError:
|
| 89 |
+
print(f"Warning: Missing scale_inv tensor for {weight_name}, skipping conversion")
|
| 90 |
+
new_state_dict[weight_name] = weight
|
| 91 |
+
else:
|
| 92 |
+
new_state_dict[weight_name] = weight
|
| 93 |
+
|
| 94 |
+
new_safetensor_file = os.path.join(bf16_path, file_name)
|
| 95 |
+
save_file(new_state_dict, new_safetensor_file)
|
| 96 |
+
|
| 97 |
+
# Memory management: keep only the 2 most recently used files
|
| 98 |
+
if len(loaded_files) > 2:
|
| 99 |
+
oldest_file = next(iter(loaded_files))
|
| 100 |
+
del loaded_files[oldest_file]
|
| 101 |
+
torch.cuda.empty_cache()
|
| 102 |
+
|
| 103 |
+
# Update model index
|
| 104 |
+
new_model_index_file = os.path.join(bf16_path, "model.safetensors.index.json")
|
| 105 |
+
for weight_name in fp8_weight_names:
|
| 106 |
+
scale_inv_name = f"{weight_name}_scale_inv"
|
| 107 |
+
if scale_inv_name in weight_map:
|
| 108 |
+
weight_map.pop(scale_inv_name)
|
| 109 |
+
with open(new_model_index_file, "w") as f:
|
| 110 |
+
json.dump({"metadata": {}, "weight_map": weight_map}, f, indent=2)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
if __name__ == "__main__":
|
| 114 |
+
parser = ArgumentParser()
|
| 115 |
+
parser.add_argument("--input-fp8-hf-path", type=str, required=True)
|
| 116 |
+
parser.add_argument("--output-bf16-hf-path", type=str, required=True)
|
| 117 |
+
args = parser.parse_args()
|
| 118 |
+
main(args.input_fp8_hf_path, args.output_bf16_hf_path)
|
tools/merge_poe_lora.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import torch
|
| 3 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 4 |
+
from peft import PeftModel
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def main():
|
| 8 |
+
parser = argparse.ArgumentParser()
|
| 9 |
+
parser.add_argument("--base-model", required=True)
|
| 10 |
+
parser.add_argument("--adapter", required=True)
|
| 11 |
+
parser.add_argument("--output-dir", required=True)
|
| 12 |
+
parser.add_argument("--dtype", default="bfloat16", choices=["bfloat16", "float16", "float32"])
|
| 13 |
+
args = parser.parse_args()
|
| 14 |
+
|
| 15 |
+
dtype_map = {
|
| 16 |
+
"bfloat16": torch.bfloat16,
|
| 17 |
+
"float16": torch.float16,
|
| 18 |
+
"float32": torch.float32,
|
| 19 |
+
}
|
| 20 |
+
dtype = dtype_map[args.dtype]
|
| 21 |
+
|
| 22 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 23 |
+
args.base_model,
|
| 24 |
+
trust_remote_code=True,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
base_model = AutoModelForCausalLM.from_pretrained(
|
| 28 |
+
args.base_model,
|
| 29 |
+
torch_dtype=dtype,
|
| 30 |
+
device_map="auto",
|
| 31 |
+
trust_remote_code=True,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
model = PeftModel.from_pretrained(
|
| 35 |
+
base_model,
|
| 36 |
+
args.adapter,
|
| 37 |
+
torch_dtype=dtype,
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
model = model.merge_and_unload()
|
| 41 |
+
|
| 42 |
+
model.save_pretrained(
|
| 43 |
+
args.output_dir,
|
| 44 |
+
safe_serialization=True,
|
| 45 |
+
max_shard_size="4GB",
|
| 46 |
+
)
|
| 47 |
+
tokenizer.save_pretrained(args.output_dir)
|
| 48 |
+
|
| 49 |
+
print(f"Saved merged model to {args.output_dir}")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
main()
|
tools/train_poe_distill_lora.py
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
|
| 3 |
+
"""LoRA training with an online product-of-experts distillation target.
|
| 4 |
+
|
| 5 |
+
This script trains on fixed pi_ref rollouts, but computes full-vocabulary
|
| 6 |
+
teacher/ref distributions online:
|
| 7 |
+
|
| 8 |
+
pi_star(. | s) proportional to pi_T(. | s)^beta * pi_ref(. | s)^(1-beta)
|
| 9 |
+
beta = alpha / (alpha + 1)
|
| 10 |
+
|
| 11 |
+
The trainable model is pi_ref plus LoRA adapters. The frozen pi_ref
|
| 12 |
+
distribution is obtained by disabling the adapter on the same model, avoiding a
|
| 13 |
+
second copy of the 4B reference model.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import argparse
|
| 19 |
+
import contextlib
|
| 20 |
+
import os
|
| 21 |
+
from dataclasses import dataclass
|
| 22 |
+
from typing import Any
|
| 23 |
+
|
| 24 |
+
import torch
|
| 25 |
+
import torch.nn.functional as F
|
| 26 |
+
from datasets import load_dataset
|
| 27 |
+
from peft import LoraConfig, TaskType, get_peft_model
|
| 28 |
+
from torch.nn.utils.rnn import pad_sequence
|
| 29 |
+
from transformers import (
|
| 30 |
+
AutoModelForCausalLM,
|
| 31 |
+
AutoTokenizer,
|
| 32 |
+
TrainerCallback,
|
| 33 |
+
TrainerControl,
|
| 34 |
+
TrainerState,
|
| 35 |
+
Trainer,
|
| 36 |
+
TrainingArguments,
|
| 37 |
+
set_seed,
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def parse_args() -> argparse.Namespace:
|
| 42 |
+
parser = argparse.ArgumentParser(description="Product-of-experts LoRA distillation on fixed rollouts.")
|
| 43 |
+
|
| 44 |
+
parser.add_argument("--student-model", default=os.environ.get("SFT_CHECKPOINT"), required=False)
|
| 45 |
+
parser.add_argument("--teacher-model", default=os.environ.get("TEACHER_MODEL", "Qwen/Qwen3-8B"))
|
| 46 |
+
parser.add_argument("--train-data", default="data/rollouts/dapo-math-17k-qwen3-4b-sft-rollouts.parquet")
|
| 47 |
+
parser.add_argument("--output-dir", default="checkpoints/qwen3-4b-poe-distill-lora")
|
| 48 |
+
|
| 49 |
+
parser.add_argument("--alpha", type=float, default=1.0)
|
| 50 |
+
parser.add_argument(
|
| 51 |
+
"--beta-start",
|
| 52 |
+
type=float,
|
| 53 |
+
default=None,
|
| 54 |
+
help="Initial beta. If unset, uses alpha / (alpha + 1) as a fixed beta.",
|
| 55 |
+
)
|
| 56 |
+
parser.add_argument(
|
| 57 |
+
"--beta-end",
|
| 58 |
+
type=float,
|
| 59 |
+
default=None,
|
| 60 |
+
help="Final beta. If unset, uses alpha / (alpha + 1) as a fixed beta.",
|
| 61 |
+
)
|
| 62 |
+
parser.add_argument(
|
| 63 |
+
"--beta-schedule-steps",
|
| 64 |
+
type=int,
|
| 65 |
+
default=None,
|
| 66 |
+
help="Number of optimizer steps used to ramp beta from beta-start to beta-end.",
|
| 67 |
+
)
|
| 68 |
+
parser.add_argument("--beta-schedule", choices=["linear", "cosine"], default="linear")
|
| 69 |
+
parser.add_argument(
|
| 70 |
+
"--beta-hold-steps",
|
| 71 |
+
type=int,
|
| 72 |
+
default=0,
|
| 73 |
+
help="Keep beta fixed at beta-start for this many optimizer steps before scheduling.",
|
| 74 |
+
)
|
| 75 |
+
parser.add_argument(
|
| 76 |
+
"--beta-transition-steps",
|
| 77 |
+
type=int,
|
| 78 |
+
default=None,
|
| 79 |
+
help="Number of optimizer steps used to move beta from beta-start to beta-end after beta-hold-steps.",
|
| 80 |
+
)
|
| 81 |
+
parser.add_argument(
|
| 82 |
+
"--lr-start",
|
| 83 |
+
type=float,
|
| 84 |
+
default=None,
|
| 85 |
+
help="Initial LR for custom hold-then-transition schedule. If unset, uses --learning-rate.",
|
| 86 |
+
)
|
| 87 |
+
parser.add_argument(
|
| 88 |
+
"--lr-end",
|
| 89 |
+
type=float,
|
| 90 |
+
default=None,
|
| 91 |
+
help="Final LR after custom transition. If unset, custom LR scheduling is disabled.",
|
| 92 |
+
)
|
| 93 |
+
parser.add_argument(
|
| 94 |
+
"--lr-hold-steps",
|
| 95 |
+
type=int,
|
| 96 |
+
default=None,
|
| 97 |
+
help="Keep LR fixed at lr-start for this many optimizer steps. If unset, uses beta-hold-steps.",
|
| 98 |
+
)
|
| 99 |
+
parser.add_argument(
|
| 100 |
+
"--lr-transition-steps",
|
| 101 |
+
type=int,
|
| 102 |
+
default=None,
|
| 103 |
+
help="Number of optimizer steps used to move LR from lr-start to lr-end. If unset, uses beta-transition-steps.",
|
| 104 |
+
)
|
| 105 |
+
parser.add_argument(
|
| 106 |
+
"--hold-transition-schedule",
|
| 107 |
+
choices=["linear", "cosine"],
|
| 108 |
+
default="linear",
|
| 109 |
+
help="Schedule type for hold-then-transition beta/LR.",
|
| 110 |
+
)
|
| 111 |
+
parser.add_argument(
|
| 112 |
+
"--loss-type",
|
| 113 |
+
choices=["full_vocab", "sampled_token"],
|
| 114 |
+
default="full_vocab",
|
| 115 |
+
help=(
|
| 116 |
+
"full_vocab matches the normalized PoE distribution over the whole vocab. "
|
| 117 |
+
"sampled_token uses an OPD-style sampled-token surrogate with a PoE advantage."
|
| 118 |
+
),
|
| 119 |
+
)
|
| 120 |
+
parser.add_argument(
|
| 121 |
+
"--advantage-normalization",
|
| 122 |
+
choices=["none", "batch", "sequence"],
|
| 123 |
+
default="batch",
|
| 124 |
+
help="Only used by --loss-type sampled_token.",
|
| 125 |
+
)
|
| 126 |
+
parser.add_argument(
|
| 127 |
+
"--advantage-clip",
|
| 128 |
+
type=float,
|
| 129 |
+
default=None,
|
| 130 |
+
help="Symmetric clamp for sampled-token advantages. Example: 5.0.",
|
| 131 |
+
)
|
| 132 |
+
parser.add_argument(
|
| 133 |
+
"--use-ppo-clip",
|
| 134 |
+
action="store_true",
|
| 135 |
+
default=False,
|
| 136 |
+
help=(
|
| 137 |
+
"Only used by --loss-type sampled_token. Use PPO-style ratio clipping "
|
| 138 |
+
"with the frozen reference log-prob as the old rollout log-prob."
|
| 139 |
+
),
|
| 140 |
+
)
|
| 141 |
+
parser.add_argument(
|
| 142 |
+
"--ppo-clip-low",
|
| 143 |
+
type=float,
|
| 144 |
+
default=0.2,
|
| 145 |
+
help="Only used when --use-ppo-clip is set. Lower PPO clip epsilon.",
|
| 146 |
+
)
|
| 147 |
+
parser.add_argument(
|
| 148 |
+
"--ppo-clip-high",
|
| 149 |
+
type=float,
|
| 150 |
+
default=0.2,
|
| 151 |
+
help="Only used when --use-ppo-clip is set. Upper PPO clip epsilon.",
|
| 152 |
+
)
|
| 153 |
+
parser.add_argument(
|
| 154 |
+
"--sampled-loss-reduction",
|
| 155 |
+
choices=["per_sample", "per_token"],
|
| 156 |
+
default="per_sample",
|
| 157 |
+
help=(
|
| 158 |
+
"Only used by --loss-type sampled_token. per_sample averages each response "
|
| 159 |
+
"first, then averages across batch; per_token averages over all response tokens."
|
| 160 |
+
),
|
| 161 |
+
)
|
| 162 |
+
parser.add_argument(
|
| 163 |
+
"--positive-advantages-only",
|
| 164 |
+
action="store_true",
|
| 165 |
+
default=False,
|
| 166 |
+
help="Only reinforce sampled tokens with positive PoE advantages.",
|
| 167 |
+
)
|
| 168 |
+
parser.add_argument("--max-length", type=int, default=4096)
|
| 169 |
+
parser.add_argument("--distill-chunk-size", type=int, default=128)
|
| 170 |
+
parser.add_argument("--max-train-samples", type=int, default=None)
|
| 171 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 172 |
+
|
| 173 |
+
parser.add_argument("--num-train-epochs", type=float, default=1.0)
|
| 174 |
+
parser.add_argument("--max-steps", type=int, default=-1)
|
| 175 |
+
parser.add_argument("--per-device-train-batch-size", type=int, default=1)
|
| 176 |
+
parser.add_argument("--gradient-accumulation-steps", type=int, default=16)
|
| 177 |
+
parser.add_argument("--learning-rate", type=float, default=2e-5)
|
| 178 |
+
parser.add_argument("--weight-decay", type=float, default=0.0)
|
| 179 |
+
parser.add_argument("--adam-beta1", type=float, default=0.9)
|
| 180 |
+
parser.add_argument("--adam-beta2", type=float, default=0.999)
|
| 181 |
+
parser.add_argument("--adam-epsilon", type=float, default=1e-8)
|
| 182 |
+
parser.add_argument("--warmup-ratio", type=float, default=0.03)
|
| 183 |
+
parser.add_argument("--lr-scheduler-type", default="cosine")
|
| 184 |
+
parser.add_argument("--logging-steps", type=int, default=1)
|
| 185 |
+
parser.add_argument("--save-steps", type=int, default=100)
|
| 186 |
+
parser.add_argument("--save-total-limit", type=int, default=0)
|
| 187 |
+
parser.add_argument("--bf16", action=argparse.BooleanOptionalAction, default=True)
|
| 188 |
+
parser.add_argument("--fp16", action="store_true", default=False)
|
| 189 |
+
parser.add_argument("--gradient-checkpointing", action=argparse.BooleanOptionalAction, default=True)
|
| 190 |
+
parser.add_argument("--report-to", default="none")
|
| 191 |
+
|
| 192 |
+
parser.add_argument("--lora-r", type=int, default=64)
|
| 193 |
+
parser.add_argument("--lora-alpha", type=int, default=128)
|
| 194 |
+
parser.add_argument("--lora-dropout", type=float, default=0.05)
|
| 195 |
+
parser.add_argument(
|
| 196 |
+
"--freeze-lora-b-after-step",
|
| 197 |
+
type=int,
|
| 198 |
+
default=None,
|
| 199 |
+
help="Freeze all LoRA B matrices once global_step reaches this value. Example: 20.",
|
| 200 |
+
)
|
| 201 |
+
parser.add_argument(
|
| 202 |
+
"--lora-target-modules",
|
| 203 |
+
default="q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj",
|
| 204 |
+
help="Comma-separated LoRA target modules.",
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
parser.add_argument("--trust-remote-code", action="store_true", default=True)
|
| 208 |
+
parser.add_argument(
|
| 209 |
+
"--attn-implementation",
|
| 210 |
+
default=None,
|
| 211 |
+
choices=[None, "eager", "sdpa", "flash_attention_2"],
|
| 212 |
+
help="Forwarded to from_pretrained when set.",
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
args = parser.parse_args()
|
| 216 |
+
if not args.student_model:
|
| 217 |
+
raise ValueError("Pass --student-model or set SFT_CHECKPOINT to the Qwen3-4B SFT checkpoint.")
|
| 218 |
+
if args.alpha <= 0:
|
| 219 |
+
raise ValueError("--alpha must be positive.")
|
| 220 |
+
fixed_beta = args.alpha / (args.alpha + 1.0)
|
| 221 |
+
if args.beta_start is None:
|
| 222 |
+
args.beta_start = fixed_beta
|
| 223 |
+
if args.beta_end is None:
|
| 224 |
+
args.beta_end = fixed_beta
|
| 225 |
+
if not 0.0 <= args.beta_start <= 1.0:
|
| 226 |
+
raise ValueError("--beta-start must be in [0, 1].")
|
| 227 |
+
if not 0.0 <= args.beta_end <= 1.0:
|
| 228 |
+
raise ValueError("--beta-end must be in [0, 1].")
|
| 229 |
+
if args.beta_schedule_steps is not None and args.beta_schedule_steps <= 0:
|
| 230 |
+
raise ValueError("--beta-schedule-steps must be positive when set.")
|
| 231 |
+
if args.advantage_clip is not None and args.advantage_clip <= 0:
|
| 232 |
+
raise ValueError("--advantage-clip must be positive when set.")
|
| 233 |
+
if args.ppo_clip_low < 0 or args.ppo_clip_high < 0:
|
| 234 |
+
raise ValueError("--ppo-clip-low and --ppo-clip-high must be non-negative.")
|
| 235 |
+
if args.freeze_lora_b_after_step is not None and args.freeze_lora_b_after_step < 0:
|
| 236 |
+
raise ValueError("--freeze-lora-b-after-step must be non-negative when set.")
|
| 237 |
+
if args.fp16 and args.bf16:
|
| 238 |
+
args.bf16 = False
|
| 239 |
+
return args
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def first_assistant_index(messages: list[dict[str, str]]) -> int:
|
| 243 |
+
for idx, message in enumerate(messages):
|
| 244 |
+
if message.get("role") == "assistant":
|
| 245 |
+
return idx
|
| 246 |
+
raise ValueError("Rollout row has no assistant message.")
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def tokenize_rollout(example: dict[str, Any], tokenizer: AutoTokenizer, max_length: int) -> dict[str, Any]:
|
| 250 |
+
messages = example["messages"]
|
| 251 |
+
assistant_idx = first_assistant_index(messages)
|
| 252 |
+
prompt_messages = messages[:assistant_idx]
|
| 253 |
+
full_messages = messages[: assistant_idx + 1]
|
| 254 |
+
|
| 255 |
+
prompt_text = tokenizer.apply_chat_template(
|
| 256 |
+
prompt_messages,
|
| 257 |
+
tokenize=False,
|
| 258 |
+
add_generation_prompt=True,
|
| 259 |
+
enable_thinking=True,
|
| 260 |
+
)
|
| 261 |
+
full_text = tokenizer.apply_chat_template(
|
| 262 |
+
full_messages,
|
| 263 |
+
tokenize=False,
|
| 264 |
+
add_generation_prompt=False,
|
| 265 |
+
enable_thinking=True,
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
prompt_ids = tokenizer.encode(prompt_text, add_special_tokens=False)
|
| 269 |
+
input_ids = tokenizer.encode(full_text, add_special_tokens=False)
|
| 270 |
+
|
| 271 |
+
if len(input_ids) > max_length:
|
| 272 |
+
input_ids = input_ids[:max_length]
|
| 273 |
+
|
| 274 |
+
# Mask is aligned to labels=input_ids[1:]. A label predicts token position
|
| 275 |
+
# j=i+1, so it belongs to the response when j >= len(prompt_ids).
|
| 276 |
+
label_len = max(len(input_ids) - 1, 0)
|
| 277 |
+
loss_mask = [1 if i + 1 >= len(prompt_ids) else 0 for i in range(label_len)]
|
| 278 |
+
|
| 279 |
+
if sum(loss_mask) == 0:
|
| 280 |
+
# Drop examples where truncation removed the assistant response.
|
| 281 |
+
return {"input_ids": [], "loss_mask": []}
|
| 282 |
+
|
| 283 |
+
return {"input_ids": input_ids, "loss_mask": loss_mask}
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
@dataclass
|
| 287 |
+
class DistillCollator:
|
| 288 |
+
pad_token_id: int
|
| 289 |
+
|
| 290 |
+
def __call__(self, features: list[dict[str, Any]]) -> dict[str, torch.Tensor]:
|
| 291 |
+
input_ids = [torch.tensor(f["input_ids"], dtype=torch.long) for f in features]
|
| 292 |
+
loss_masks = [torch.tensor(f["loss_mask"], dtype=torch.float32) for f in features]
|
| 293 |
+
lengths = torch.tensor([x.size(0) for x in input_ids], dtype=torch.long)
|
| 294 |
+
|
| 295 |
+
padded_input_ids = pad_sequence(input_ids, batch_first=True, padding_value=self.pad_token_id)
|
| 296 |
+
# loss_mask is one shorter than input_ids because it aligns to shifted labels.
|
| 297 |
+
padded_loss_masks = pad_sequence(loss_masks, batch_first=True, padding_value=0.0)
|
| 298 |
+
positions = torch.arange(padded_input_ids.size(1)).unsqueeze(0)
|
| 299 |
+
attention_mask = (positions < lengths.unsqueeze(1)).long()
|
| 300 |
+
return {
|
| 301 |
+
"input_ids": padded_input_ids,
|
| 302 |
+
"attention_mask": attention_mask,
|
| 303 |
+
"loss_mask": padded_loss_masks,
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
class PoEDistillTrainer(Trainer):
|
| 308 |
+
def __init__(
|
| 309 |
+
self,
|
| 310 |
+
*args: Any,
|
| 311 |
+
teacher_model: torch.nn.Module,
|
| 312 |
+
beta_start: float,
|
| 313 |
+
beta_end: float,
|
| 314 |
+
beta_schedule_steps: int | None,
|
| 315 |
+
beta_schedule: str,
|
| 316 |
+
loss_type: str,
|
| 317 |
+
advantage_normalization: str,
|
| 318 |
+
advantage_clip: float | None,
|
| 319 |
+
positive_advantages_only: bool,
|
| 320 |
+
use_ppo_clip: bool,
|
| 321 |
+
ppo_clip_low: float,
|
| 322 |
+
ppo_clip_high: float,
|
| 323 |
+
sampled_loss_reduction: str,
|
| 324 |
+
distill_chunk_size: int,
|
| 325 |
+
**kwargs: Any,
|
| 326 |
+
) -> None:
|
| 327 |
+
super().__init__(*args, **kwargs)
|
| 328 |
+
self.teacher_model = teacher_model
|
| 329 |
+
self.teacher_model.to(self.args.device)
|
| 330 |
+
self.teacher_model.eval()
|
| 331 |
+
self.beta_start = beta_start
|
| 332 |
+
self.beta_end = beta_end
|
| 333 |
+
self.beta_schedule_steps = beta_schedule_steps
|
| 334 |
+
self.beta_schedule = beta_schedule
|
| 335 |
+
self.loss_type = loss_type
|
| 336 |
+
self.advantage_normalization = advantage_normalization
|
| 337 |
+
self.advantage_clip = advantage_clip
|
| 338 |
+
self.positive_advantages_only = positive_advantages_only
|
| 339 |
+
self.use_ppo_clip = use_ppo_clip
|
| 340 |
+
self.ppo_clip_low = ppo_clip_low
|
| 341 |
+
self.ppo_clip_high = ppo_clip_high
|
| 342 |
+
self.sampled_loss_reduction = sampled_loss_reduction
|
| 343 |
+
self.distill_chunk_size = distill_chunk_size
|
| 344 |
+
|
| 345 |
+
def current_beta(self) -> float:
|
| 346 |
+
schedule_steps = self.beta_schedule_steps
|
| 347 |
+
if schedule_steps is None:
|
| 348 |
+
schedule_steps = self.state.max_steps if self.state.max_steps > 0 else None
|
| 349 |
+
if schedule_steps is None or schedule_steps == 0:
|
| 350 |
+
return self.beta_end
|
| 351 |
+
|
| 352 |
+
progress = min(max(self.state.global_step / schedule_steps, 0.0), 1.0)
|
| 353 |
+
if self.beta_schedule == "cosine":
|
| 354 |
+
progress = 0.5 - 0.5 * torch.cos(torch.tensor(progress * torch.pi)).item()
|
| 355 |
+
return self.beta_start + (self.beta_end - self.beta_start) * progress
|
| 356 |
+
|
| 357 |
+
@staticmethod
|
| 358 |
+
def gather_token_logprobs(logits: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
|
| 359 |
+
logits = logits.float()
|
| 360 |
+
token_logits = logits.gather(dim=-1, index=labels.unsqueeze(-1)).squeeze(-1)
|
| 361 |
+
return token_logits - logits.logsumexp(dim=-1)
|
| 362 |
+
|
| 363 |
+
def normalize_advantages(self, advantages: torch.Tensor, loss_mask: torch.Tensor) -> torch.Tensor:
|
| 364 |
+
if self.advantage_normalization == "none":
|
| 365 |
+
return advantages
|
| 366 |
+
|
| 367 |
+
if self.advantage_normalization == "batch":
|
| 368 |
+
denom = loss_mask.sum().clamp_min(1.0)
|
| 369 |
+
mean = (advantages * loss_mask).sum() / denom
|
| 370 |
+
var = (((advantages - mean) * loss_mask) ** 2).sum() / denom
|
| 371 |
+
return (advantages - mean) / torch.sqrt(var + 1e-6)
|
| 372 |
+
|
| 373 |
+
denom = loss_mask.sum(dim=1, keepdim=True).clamp_min(1.0)
|
| 374 |
+
mean = (advantages * loss_mask).sum(dim=1, keepdim=True) / denom
|
| 375 |
+
var = (((advantages - mean) * loss_mask) ** 2).sum(dim=1, keepdim=True) / denom
|
| 376 |
+
return (advantages - mean) / torch.sqrt(var + 1e-6)
|
| 377 |
+
|
| 378 |
+
def compute_loss(
|
| 379 |
+
self,
|
| 380 |
+
model: torch.nn.Module,
|
| 381 |
+
inputs: dict[str, torch.Tensor],
|
| 382 |
+
return_outputs: bool = False,
|
| 383 |
+
**_: Any,
|
| 384 |
+
):
|
| 385 |
+
loss_mask = inputs.pop("loss_mask")
|
| 386 |
+
input_ids = inputs["input_ids"]
|
| 387 |
+
attention_mask = inputs["attention_mask"]
|
| 388 |
+
labels = input_ids[:, 1:]
|
| 389 |
+
|
| 390 |
+
with torch.no_grad():
|
| 391 |
+
teacher_logits = self.teacher_model(
|
| 392 |
+
input_ids=input_ids,
|
| 393 |
+
attention_mask=attention_mask,
|
| 394 |
+
use_cache=False,
|
| 395 |
+
).logits[:, :-1, :].detach()
|
| 396 |
+
|
| 397 |
+
adapter_owner = model.module if hasattr(model, "module") else model
|
| 398 |
+
disable_adapter = getattr(adapter_owner, "disable_adapter", None)
|
| 399 |
+
ref_context = disable_adapter() if disable_adapter is not None else contextlib.nullcontext()
|
| 400 |
+
with ref_context:
|
| 401 |
+
ref_logits = adapter_owner(
|
| 402 |
+
input_ids=input_ids,
|
| 403 |
+
attention_mask=attention_mask,
|
| 404 |
+
use_cache=False,
|
| 405 |
+
).logits[:, :-1, :].detach()
|
| 406 |
+
|
| 407 |
+
student_outputs = model(input_ids=input_ids, attention_mask=attention_mask, use_cache=False)
|
| 408 |
+
student_logits = student_outputs.logits[:, :-1, :]
|
| 409 |
+
if teacher_logits.size(-1) != student_logits.size(-1) or ref_logits.size(-1) != student_logits.size(-1):
|
| 410 |
+
raise ValueError(
|
| 411 |
+
"Teacher, reference, and student vocab sizes must match for full-vocab PoE distillation. "
|
| 412 |
+
f"Got teacher={teacher_logits.size(-1)}, ref={ref_logits.size(-1)}, "
|
| 413 |
+
f"student={student_logits.size(-1)}."
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
total_loss = student_logits.new_zeros(())
|
| 417 |
+
total_tokens = loss_mask.sum().clamp_min(1.0)
|
| 418 |
+
beta = self.current_beta()
|
| 419 |
+
|
| 420 |
+
if self.loss_type == "sampled_token":
|
| 421 |
+
teacher_logp = self.gather_token_logprobs(teacher_logits, labels)
|
| 422 |
+
ref_logp = self.gather_token_logprobs(ref_logits, labels)
|
| 423 |
+
student_logp = self.gather_token_logprobs(student_logits, labels)
|
| 424 |
+
|
| 425 |
+
poe_score = beta * teacher_logp + (1.0 - beta) * ref_logp
|
| 426 |
+
advantages = poe_score - student_logp.detach()
|
| 427 |
+
advantages = self.normalize_advantages(advantages, loss_mask)
|
| 428 |
+
if self.advantage_clip is not None:
|
| 429 |
+
advantages = advantages.clamp(min=-self.advantage_clip, max=self.advantage_clip)
|
| 430 |
+
if self.positive_advantages_only:
|
| 431 |
+
advantages = advantages.clamp_min(0.0)
|
| 432 |
+
|
| 433 |
+
advantages = advantages.detach()
|
| 434 |
+
|
| 435 |
+
if self.use_ppo_clip:
|
| 436 |
+
# The rollouts are generated by the frozen reference/SFT policy, so ref_logp
|
| 437 |
+
# is used as the old rollout log-prob. This mirrors the PPO-style clipped
|
| 438 |
+
# policy loss used in RL frameworks such as slime.
|
| 439 |
+
ratio = torch.exp(student_logp - ref_logp.detach())
|
| 440 |
+
ratio_clipped = ratio.clamp(1.0 - self.ppo_clip_low, 1.0 + self.ppo_clip_high)
|
| 441 |
+
pg_loss_unclipped = -ratio * advantages
|
| 442 |
+
pg_loss_clipped = -ratio_clipped * advantages
|
| 443 |
+
token_loss = torch.maximum(pg_loss_unclipped, pg_loss_clipped)
|
| 444 |
+
else:
|
| 445 |
+
# Direct OPD-style sampled-token surrogate.
|
| 446 |
+
token_loss = -advantages * student_logp
|
| 447 |
+
|
| 448 |
+
if self.sampled_loss_reduction == "per_token":
|
| 449 |
+
loss = (token_loss * loss_mask).sum() / total_tokens
|
| 450 |
+
else:
|
| 451 |
+
# Per-sample mean: each response contributes equally regardless of length.
|
| 452 |
+
seq_loss = (token_loss * loss_mask).sum(dim=1) / loss_mask.sum(dim=1).clamp_min(1.0)
|
| 453 |
+
loss = seq_loss.mean()
|
| 454 |
+
|
| 455 |
+
return (loss, student_outputs) if return_outputs else loss
|
| 456 |
+
|
| 457 |
+
seq_len = student_logits.size(1)
|
| 458 |
+
for start in range(0, seq_len, self.distill_chunk_size):
|
| 459 |
+
end = min(start + self.distill_chunk_size, seq_len)
|
| 460 |
+
mask = loss_mask[:, start:end]
|
| 461 |
+
if mask.sum() == 0:
|
| 462 |
+
continue
|
| 463 |
+
|
| 464 |
+
teacher_logp = F.log_softmax(teacher_logits[:, start:end, :].float(), dim=-1)
|
| 465 |
+
ref_logp = F.log_softmax(ref_logits[:, start:end, :].float(), dim=-1)
|
| 466 |
+
student_logp = F.log_softmax(student_logits[:, start:end, :].float(), dim=-1)
|
| 467 |
+
|
| 468 |
+
poe_logits = beta * teacher_logp + (1.0 - beta) * ref_logp
|
| 469 |
+
target_probs = F.softmax(poe_logits, dim=-1)
|
| 470 |
+
token_ce = -(target_probs * student_logp).sum(dim=-1)
|
| 471 |
+
total_loss = total_loss + (token_ce * mask).sum()
|
| 472 |
+
|
| 473 |
+
loss = total_loss / total_tokens
|
| 474 |
+
return (loss, student_outputs) if return_outputs else loss
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
class FreezeLoRABCallback(TrainerCallback):
|
| 478 |
+
def __init__(self, freeze_after_step: int | None) -> None:
|
| 479 |
+
self.freeze_after_step = freeze_after_step
|
| 480 |
+
self.frozen = False
|
| 481 |
+
|
| 482 |
+
def on_step_begin(
|
| 483 |
+
self,
|
| 484 |
+
args: TrainingArguments,
|
| 485 |
+
state: TrainerState,
|
| 486 |
+
control: TrainerControl,
|
| 487 |
+
model: torch.nn.Module | None = None,
|
| 488 |
+
**kwargs: Any,
|
| 489 |
+
) -> TrainerControl:
|
| 490 |
+
if self.freeze_after_step is None or self.frozen or model is None:
|
| 491 |
+
return control
|
| 492 |
+
if state.global_step < self.freeze_after_step:
|
| 493 |
+
return control
|
| 494 |
+
|
| 495 |
+
frozen_params = 0
|
| 496 |
+
module = model.module if hasattr(model, "module") else model
|
| 497 |
+
for name, param in module.named_parameters():
|
| 498 |
+
if ".lora_B." in name or "lora_B." in name:
|
| 499 |
+
param.requires_grad_(False)
|
| 500 |
+
frozen_params += param.numel()
|
| 501 |
+
|
| 502 |
+
self.frozen = True
|
| 503 |
+
if args.process_index == 0:
|
| 504 |
+
print(f"[PoE Distill] Froze LoRA B at global_step={state.global_step} ({frozen_params} params).")
|
| 505 |
+
return control
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
def main() -> None:
|
| 509 |
+
args = parse_args()
|
| 510 |
+
set_seed(args.seed)
|
| 511 |
+
|
| 512 |
+
tokenizer = AutoTokenizer.from_pretrained(args.student_model, trust_remote_code=args.trust_remote_code)
|
| 513 |
+
if tokenizer.pad_token_id is None:
|
| 514 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 515 |
+
|
| 516 |
+
raw_dataset = load_dataset("parquet", data_files=args.train_data, split="train")
|
| 517 |
+
if args.max_train_samples is not None:
|
| 518 |
+
raw_dataset = raw_dataset.select(range(min(args.max_train_samples, len(raw_dataset))))
|
| 519 |
+
|
| 520 |
+
train_dataset = raw_dataset.map(
|
| 521 |
+
lambda ex: tokenize_rollout(ex, tokenizer, args.max_length),
|
| 522 |
+
remove_columns=raw_dataset.column_names,
|
| 523 |
+
desc="Tokenizing pi_ref rollouts",
|
| 524 |
+
).filter(lambda ex: len(ex["input_ids"]) > 0, desc="Dropping empty responses")
|
| 525 |
+
|
| 526 |
+
model_kwargs = {
|
| 527 |
+
"torch_dtype": torch.bfloat16 if args.bf16 else (torch.float16 if args.fp16 else torch.float32),
|
| 528 |
+
"trust_remote_code": args.trust_remote_code,
|
| 529 |
+
}
|
| 530 |
+
if args.attn_implementation is not None:
|
| 531 |
+
model_kwargs["attn_implementation"] = args.attn_implementation
|
| 532 |
+
|
| 533 |
+
student = AutoModelForCausalLM.from_pretrained(args.student_model, **model_kwargs)
|
| 534 |
+
teacher = AutoModelForCausalLM.from_pretrained(args.teacher_model, **model_kwargs)
|
| 535 |
+
teacher.eval()
|
| 536 |
+
teacher.requires_grad_(False)
|
| 537 |
+
|
| 538 |
+
if args.gradient_checkpointing:
|
| 539 |
+
student.gradient_checkpointing_enable()
|
| 540 |
+
student.config.use_cache = False
|
| 541 |
+
teacher.config.use_cache = False
|
| 542 |
+
|
| 543 |
+
lora_config = LoraConfig(
|
| 544 |
+
task_type=TaskType.CAUSAL_LM,
|
| 545 |
+
r=args.lora_r,
|
| 546 |
+
lora_alpha=args.lora_alpha,
|
| 547 |
+
lora_dropout=args.lora_dropout,
|
| 548 |
+
target_modules=[m.strip() for m in args.lora_target_modules.split(",") if m.strip()],
|
| 549 |
+
)
|
| 550 |
+
student = get_peft_model(student, lora_config)
|
| 551 |
+
student.print_trainable_parameters()
|
| 552 |
+
|
| 553 |
+
training_args = TrainingArguments(
|
| 554 |
+
output_dir=args.output_dir,
|
| 555 |
+
num_train_epochs=args.num_train_epochs,
|
| 556 |
+
max_steps=args.max_steps,
|
| 557 |
+
per_device_train_batch_size=args.per_device_train_batch_size,
|
| 558 |
+
gradient_accumulation_steps=args.gradient_accumulation_steps,
|
| 559 |
+
learning_rate=args.learning_rate,
|
| 560 |
+
weight_decay=args.weight_decay,
|
| 561 |
+
adam_beta1=args.adam_beta1,
|
| 562 |
+
adam_beta2=args.adam_beta2,
|
| 563 |
+
adam_epsilon=args.adam_epsilon,
|
| 564 |
+
warmup_ratio=args.warmup_ratio,
|
| 565 |
+
lr_scheduler_type=args.lr_scheduler_type,
|
| 566 |
+
logging_steps=args.logging_steps,
|
| 567 |
+
save_steps=args.save_steps,
|
| 568 |
+
save_total_limit=args.save_total_limit,
|
| 569 |
+
bf16=args.bf16,
|
| 570 |
+
fp16=args.fp16,
|
| 571 |
+
gradient_checkpointing=args.gradient_checkpointing,
|
| 572 |
+
remove_unused_columns=False,
|
| 573 |
+
report_to=[] if args.report_to == "none" else args.report_to.split(","),
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
trainer = PoEDistillTrainer(
|
| 577 |
+
model=student,
|
| 578 |
+
args=training_args,
|
| 579 |
+
train_dataset=train_dataset,
|
| 580 |
+
data_collator=DistillCollator(pad_token_id=tokenizer.pad_token_id),
|
| 581 |
+
tokenizer=tokenizer,
|
| 582 |
+
teacher_model=teacher,
|
| 583 |
+
beta_start=args.beta_start,
|
| 584 |
+
beta_end=args.beta_end,
|
| 585 |
+
beta_schedule_steps=args.beta_schedule_steps,
|
| 586 |
+
beta_schedule=args.beta_schedule,
|
| 587 |
+
loss_type=args.loss_type,
|
| 588 |
+
advantage_normalization=args.advantage_normalization,
|
| 589 |
+
advantage_clip=args.advantage_clip,
|
| 590 |
+
positive_advantages_only=args.positive_advantages_only,
|
| 591 |
+
use_ppo_clip=args.use_ppo_clip,
|
| 592 |
+
ppo_clip_low=args.ppo_clip_low,
|
| 593 |
+
ppo_clip_high=args.ppo_clip_high,
|
| 594 |
+
sampled_loss_reduction=args.sampled_loss_reduction,
|
| 595 |
+
distill_chunk_size=args.distill_chunk_size,
|
| 596 |
+
callbacks=[FreezeLoRABCallback(args.freeze_lora_b_after_step)],
|
| 597 |
+
)
|
| 598 |
+
trainer.train()
|
| 599 |
+
trainer.save_model(args.output_dir)
|
| 600 |
+
tokenizer.save_pretrained(args.output_dir)
|
| 601 |
+
|
| 602 |
+
|
| 603 |
+
if __name__ == "__main__":
|
| 604 |
+
main()
|