Files
wehub-resource-sync 593b94c120
pytest / Unit Tests (push) Has been cancelled
pytest / Integration (integration_tests_a) (push) Has been cancelled
pytest / Integration (integration_tests_b) (push) Has been cancelled
pytest / Integration (integration_tests_c) (push) Has been cancelled
pytest / Integration (integration_tests_d) (push) Has been cancelled
pytest / Integration (integration_tests_e) (push) Has been cancelled
pytest / Integration (integration_tests_f) (push) Has been cancelled
pytest / Integration (integration_tests_g) (push) Has been cancelled
pytest / Integration (integration_tests_h) (push) Has been cancelled
pytest / Integration (integration_tests_i) (push) Has been cancelled
pytest / Integration (integration_tests_j) (push) Has been cancelled
pytest / Distributed (distributed_a) (push) Has been cancelled
pytest / Distributed (distributed_b) (push) Has been cancelled
pytest / Distributed (distributed_c) (push) Has been cancelled
pytest / Distributed (distributed_d) (push) Has been cancelled
pytest / Distributed (distributed_e) (push) Has been cancelled
pytest / Distributed (distributed_f) (push) Has been cancelled
pytest / Minimal Install (push) Has been cancelled
pytest / Event File (push) Has been cancelled
pytest (slow) / py-slow (push) Has been cancelled
Publish JSON Schema / publish-schema (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:49:20 +08:00

393 lines
19 KiB
Python

"""GRPO alignment training with Ludwig.
Group Relative Policy Optimization (Shao et al., 2024 — DeepSeek-R1) trains a language
model using a programmatic reward signal rather than preference pairs. Instead of
"chosen vs rejected" data, you supply a reward function that scores each generated
response. Ludwig samples grpo_num_generations completions per prompt, normalises
rewards within the group, and applies a clipped PPO-style update.
Usage (standalone):
python train_grpo.py
python train_grpo.py --epochs 2 --lr 3e-7 --output_dir my_run
Usage (CLI with pre-scored dataset):
ludwig train --config config_grpo.yaml --dataset grpo_train.csv
Prerequisites:
# Colab: !pip install "ludwig[llm]"
pip install "ludwig[llm]"
export HUGGING_FACE_HUB_TOKEN="<your_token>"
# You also need access approval for meta-llama/Llama-3.1-8B on HuggingFace Hub.
How reward functions work with the Ludwig GRPO trainer
-------------------------------------------------------
The GRPO trainer expects a dataset where each row has:
- prompt : the input question
- response : the correct / reference answer (used as a training target)
The reward function is applied *before* training, in a data preparation step that
scores each (prompt, response) pair and stores a "reward" column. During the GRPO
update the trainer uses those scores — together with the group-normalised advantages
computed across grpo_num_generations rollouts — to weight the policy gradient.
NOTE: If a future Ludwig version adds a reward_fn parameter directly on LudwigModel
or the GRPO trainer config, you can pass the callable there instead of pre-scoring.
Check the Ludwig changelog for that API addition.
"""
import argparse
import logging
import os
import re
import pandas as pd
import yaml
from ludwig.api import LudwigModel
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Dataset — 100 math word problems generated inline; no download needed
# ---------------------------------------------------------------------------
MATH_PROBLEMS = [
{
"prompt": "A baker has 48 cookies. She puts them equally into 6 bags. How many cookies are in each bag?",
"answer": "8",
},
{"prompt": "Tom has 15 apples. He gives 7 to his friend. How many apples does Tom have now?", "answer": "8"},
{"prompt": "A train travels 60 miles per hour. How far does it travel in 3 hours?", "answer": "180"},
{"prompt": "There are 24 students in a class. They form groups of 4. How many groups are there?", "answer": "6"},
{"prompt": "Maria saves $12 each week. How much does she save in 5 weeks?", "answer": "60"},
{"prompt": "A rectangle has a length of 8 cm and a width of 5 cm. What is its area?", "answer": "40"},
{
"prompt": "Jake has 3 boxes of crayons. Each box has 16 crayons. How many crayons does he have in total?",
"answer": "48",
},
{
"prompt": "A shop sells 35 items on Monday and 47 items on Tuesday. How many items were sold in total?",
"answer": "82",
},
{
"prompt": "There are 100 balloons. 38 are red and the rest are blue. How many blue balloons are there?",
"answer": "62",
},
{"prompt": "A car travels 90 km in 2 hours. What is its average speed in km/h?", "answer": "45"},
{"prompt": "Lucy reads 20 pages per day. How many pages does she read in 7 days?", "answer": "140"},
{"prompt": "A box holds 12 eggs. How many eggs are in 9 boxes?", "answer": "108"},
{"prompt": "A pool has 500 litres of water. 175 litres evaporate. How many litres remain?", "answer": "325"},
{"prompt": "There are 7 shelves with 9 books on each shelf. How many books are there in total?", "answer": "63"},
{"prompt": "Sam earns $8 per hour. How much does he earn in 6 hours?", "answer": "48"},
{"prompt": "A garden is 12 m long and 7 m wide. What is its perimeter?", "answer": "38"},
{"prompt": "There are 5 rows of chairs with 14 chairs in each row. How many chairs are there?", "answer": "70"},
{
"prompt": "Anna bakes 4 trays of muffins. Each tray has 12 muffins. How many muffins does she bake?",
"answer": "48",
},
{"prompt": "A rope is 72 cm long. It is cut into 8 equal pieces. How long is each piece?", "answer": "9"},
{"prompt": "Ben has 45 stickers. He gives 18 to his sister. How many stickers does Ben have left?", "answer": "27"},
{
"prompt": "A cinema sold 256 tickets on Saturday and 198 on Sunday. "
"How many tickets were sold over the weekend?",
"answer": "454",
},
{"prompt": "There are 360 minutes in 6 hours. How many minutes are in 1 hour?", "answer": "60"},
{"prompt": "A cyclist rides 15 km each day. How far does she ride in 4 days?", "answer": "60"},
{"prompt": "A bookstore has 5 shelves with 30 books each. How many books does it have in total?", "answer": "150"},
{"prompt": "A pizza is cut into 8 slices. If 3 slices are eaten, how many slices remain?", "answer": "5"},
{"prompt": "There are 4 quarters in a dollar. How many quarters are in $7?", "answer": "28"},
{"prompt": "A farmer has 120 eggs. He puts them in cartons of 12. How many cartons does he fill?", "answer": "10"},
{
"prompt": "A school has 480 pupils split equally across 6 classes. How many pupils are in each class?",
"answer": "80",
},
{
"prompt": "An ice cream shop sold 45 cones on Friday and 67 on Saturday. How many cones were sold in total?",
"answer": "112",
},
{"prompt": "A factory makes 250 units per day. How many units does it make in 5 days?", "answer": "1250"},
{
"prompt": "There are 18 players in a tournament. They are split into teams of 3. How many teams are there?",
"answer": "6",
},
{
"prompt": "A jar holds 96 sweets. If 4 children share them equally, how many sweets does each child get?",
"answer": "24",
},
{"prompt": "A plane flies 800 km in 2 hours. What is its average speed?", "answer": "400"},
{
"prompt": "A garden has 5 rows of flowers with 11 flowers in each row. How many flowers are there?",
"answer": "55",
},
{"prompt": "James has $200. He spends $74 on shoes. How much money does he have left?", "answer": "126"},
{"prompt": "A recipe uses 3 cups of flour per cake. How many cups are needed for 7 cakes?", "answer": "21"},
{"prompt": "There are 50 chairs in a hall. 13 are occupied. How many chairs are empty?", "answer": "37"},
{"prompt": "A clock ticks 60 times per minute. How many times does it tick in 5 minutes?", "answer": "300"},
{
"prompt": "A runner completes a 400 m lap in 80 seconds. How many laps does she run in 400 seconds?",
"answer": "5",
},
{"prompt": "There are 3 packs of pens with 12 pens each. How many pens are there in total?", "answer": "36"},
{"prompt": "A tank holds 200 gallons. It is currently 40% full. How many gallons are in the tank?", "answer": "80"},
{
"prompt": "A store has 84 items. They are arranged in 7 equal rows. How many items are in each row?",
"answer": "12",
},
{
"prompt": "There are 9 months until the concert. How many weeks is that (assuming 4 weeks per month)?",
"answer": "36",
},
{"prompt": "A frog jumps 3 m each jump. How far does it jump in 15 jumps?", "answer": "45"},
{"prompt": "A box of pencils costs $3. How much do 11 boxes cost?", "answer": "33"},
{"prompt": "There are 144 hours in 6 days. How many hours are in 1 day?", "answer": "24"},
{"prompt": "A pond has 300 fish. 75 are caught and released. How many fish remain?", "answer": "225"},
{"prompt": "A school bus seats 40 students. How many students can travel in 3 buses?", "answer": "120"},
{
"prompt": "There are 11 teams in a league. Each team plays every other team once. How many matches are there?",
"answer": "55",
},
{"prompt": "A bakery makes 60 loaves a day. How many loaves does it make in 2 weeks?", "answer": "840"},
{"prompt": "A square has sides of 9 cm. What is its perimeter?", "answer": "36"},
{"prompt": "An author writes 500 words per hour. How many words does she write in 3 hours?", "answer": "1500"},
{"prompt": "There are 72 hours in 3 days. How many hours are in 5 days?", "answer": "120"},
{"prompt": "A store discounts a $50 item by 20%. What is the sale price?", "answer": "40"},
{"prompt": "A farmer plants 8 seeds in each row. He has 9 rows. How many seeds does he plant?", "answer": "72"},
{"prompt": "A car park has 6 levels with 45 spaces each. How many spaces are there in total?", "answer": "270"},
{"prompt": "A marathon is 42 km. A runner has covered 28 km. How many km remain?", "answer": "14"},
{"prompt": "There are 30 days in a month. How many days are in 4 months?", "answer": "120"},
{"prompt": "A vending machine sells 15 drinks per hour. How many drinks does it sell in 8 hours?", "answer": "120"},
{"prompt": "There are 6 strings on a guitar. How many strings are on 9 guitars?", "answer": "54"},
{"prompt": "A worker earns $15 per hour and works 8 hours. How much does she earn?", "answer": "120"},
{"prompt": "A square room has an area of 64 m². What is the length of each side?", "answer": "8"},
{"prompt": "A jar has 5 red marbles and 8 blue marbles. How many marbles are there in total?", "answer": "13"},
{"prompt": "A truck carries 2 tonnes per trip. How many tonnes does it carry in 7 trips?", "answer": "14"},
{"prompt": "There are 32 students. Half are girls. How many girls are there?", "answer": "16"},
{"prompt": "A patio is 6 m wide and 9 m long. What is its area?", "answer": "54"},
{"prompt": "A swimmer does 50 laps per session. How many laps does she do in 6 sessions?", "answer": "300"},
{"prompt": "A bag of rice weighs 5 kg. How much do 8 bags weigh?", "answer": "40"},
{"prompt": "There are 7 days in a week. How many days are in 13 weeks?", "answer": "91"},
{"prompt": "A factory produces 1200 items in 4 hours. How many items per hour?", "answer": "300"},
{
"prompt": "A road is 3.5 km long. Two cars start at each end and drive toward each other "
"at 1.75 km/h each. How long until they meet (in hours)?",
"answer": "1",
},
{"prompt": "There are 18 biscuits. Each person eats 3. How many people can eat?", "answer": "6"},
{
"prompt": "A pool requires 8 hours to fill. After 5 hours, how much is left to fill (as a fraction)?",
"answer": "3/8",
},
{"prompt": "A box has 4 layers with 25 chocolates per layer. How many chocolates are in the box?", "answer": "100"},
{"prompt": "A phone battery lasts 12 hours. After 9 hours of use, what percentage remains?", "answer": "25"},
{"prompt": "There are 40 red and 25 blue tiles. How many tiles are there in total?", "answer": "65"},
{"prompt": "A rope is 54 m long. It is divided into 9 equal pieces. How long is each piece?", "answer": "6"},
{"prompt": "A bag has 3 green, 4 yellow, and 5 purple balls. How many balls are there?", "answer": "12"},
{"prompt": "Each page has 30 lines. How many lines are on 8 pages?", "answer": "240"},
{"prompt": "A worker packs 6 boxes per hour. How many boxes in 9 hours?", "answer": "54"},
{"prompt": "There are 1000 metres in a kilometre. How many metres in 7.5 km?", "answer": "7500"},
{"prompt": "A container holds 5 litres. How many containers are needed for 35 litres?", "answer": "7"},
{
"prompt": "A car travels 110 km in 2 hours. How far does it travel in 5 hours at the same speed?",
"answer": "275",
},
{"prompt": "There are 52 cards in a deck. How many cards are in 3 decks?", "answer": "156"},
{"prompt": "A hotel has 12 floors with 18 rooms each. How many rooms are there?", "answer": "216"},
{
"prompt": "A pie is split into 6 equal slices. Two people each eat 2 slices. How many slices remain?",
"answer": "2",
},
{"prompt": "A printing press produces 40 pages per minute. How many pages in 15 minutes?", "answer": "600"},
{"prompt": "There are 200 pupils. 45% are boys. How many boys are there?", "answer": "90"},
{"prompt": "A wall is 3 m tall and 8 m wide. What is its area?", "answer": "24"},
{"prompt": "A car uses 6 litres of fuel per 100 km. How much fuel is needed for 300 km?", "answer": "18"},
{"prompt": "A team scores 3 goals in each of 6 matches. How many goals in total?", "answer": "18"},
{"prompt": "There are 11 rows of seats with 20 seats each. How many seats are there?", "answer": "220"},
{"prompt": "A box contains 50 nails. After using 23, how many remain?", "answer": "27"},
{"prompt": "A wheel turns 360 degrees in one full rotation. How many degrees in 5 rotations?", "answer": "1800"},
{"prompt": "A shop receives 3 deliveries of 40 items each. How many items did it receive?", "answer": "120"},
{"prompt": "There are 15 biscuits on a plate. 6 are eaten. How many remain?", "answer": "9"},
]
# ---------------------------------------------------------------------------
# Reward function
# ---------------------------------------------------------------------------
def reward_fn(prompt: str, response: str) -> float:
"""Score a model response by checking if it contains the correct numerical answer.
Returns 1.0 if the response contains the expected answer, 0.0 otherwise. The correct answer is looked up from the
global ANSWER_LOOKUP dict which is built from MATH_PROBLEMS at startup.
This is a simple exact-match reward. Real applications might use an LLM judge, a code execution sandbox, or a
symbolic verifier.
"""
expected = ANSWER_LOOKUP.get(prompt.strip())
if expected is None:
return 0.0
# Accept the answer if it appears as a standalone number/fraction in the response
pattern = r"(?<![.\d])" + re.escape(expected) + r"(?![.\d])"
return 1.0 if re.search(pattern, response) else 0.0
# Map from prompt text → expected answer string, built once at module load time.
ANSWER_LOOKUP: dict[str, str] = {p["prompt"]: p["answer"] for p in MATH_PROBLEMS}
# ---------------------------------------------------------------------------
# Dataset preparation — apply reward_fn to produce a scored DataFrame
# ---------------------------------------------------------------------------
def build_dataset(reward_function) -> pd.DataFrame:
"""Build a training DataFrame by applying the reward function to each example.
For GRPO the dataset needs at minimum:
- prompt : the input text
- response : the reference / target text
- reward : a float score for each (prompt, response) pair
NOTE: The Ludwig GRPO trainer does not yet accept a reward_fn callable at
train-time (as of v0.11.dev). Instead, rewards are pre-computed here and
stored in a 'reward' column that the trainer reads from the dataset. If a
future version exposes a reward_fn parameter on LudwigModel or the GRPO
config, you can pass `reward_fn=reward_function` there directly.
"""
rows = []
for item in MATH_PROBLEMS:
prompt = item["prompt"]
response = item["answer"]
reward = reward_function(prompt, response)
rows.append({"prompt": prompt, "response": response, "reward": reward})
df = pd.DataFrame(rows)
logger.info(
"Dataset built: %d examples, mean reward=%.3f",
len(df),
df["reward"].mean(),
)
return df
# ---------------------------------------------------------------------------
# Config builder
# ---------------------------------------------------------------------------
def build_config(epochs: int, learning_rate: float, batch_size: int) -> dict:
raw = f"""
model_type: llm
base_model: meta-llama/Llama-3.1-8B
adapter:
type: lora
r: 16
alpha: 32
trainer:
type: grpo
epochs: {epochs}
learning_rate: {learning_rate}
batch_size: {batch_size}
gradient_accumulation_steps: 16
grpo_beta: 0.04
grpo_epsilon: 0.2
grpo_num_generations: 4
input_features:
- name: prompt
type: text
output_features:
- name: response
type: text
backend:
type: local
"""
return yaml.safe_load(raw)
# ---------------------------------------------------------------------------
# GPU check
# ---------------------------------------------------------------------------
def check_gpu():
try:
import torch
if torch.cuda.is_available() and torch.cuda.device_count() > 0:
name = torch.cuda.get_device_name(0)
vram = torch.cuda.get_device_properties(0).total_memory / 1024**3
logger.info("GPU detected: %s (%.1f GiB VRAM)", name, vram)
if vram < 20:
logger.warning(
"Only %.1f GiB VRAM detected. Llama-3.1-8B requires at least 40 GiB "
"for GRPO training. Consider using a smaller base model or enabling "
"quantisation (e.g. bitsandbytes 4-bit).",
vram,
)
else:
logger.warning(
"No GPU detected. GRPO training on a 7-8B model will be extremely slow on CPU. "
"On Colab, go to Runtime > Change runtime type and select a GPU."
)
except ImportError:
pass
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
parser = argparse.ArgumentParser(description="GRPO alignment training with Ludwig.")
parser.add_argument("--epochs", type=int, default=1)
parser.add_argument("--lr", type=float, default=5e-7)
parser.add_argument("--batch_size", type=int, default=1)
parser.add_argument("--experiment_name", default="math_grpo")
parser.add_argument("--output_dir", default="results")
args = parser.parse_args()
# --- HuggingFace token ---
token = os.environ.get("HUGGING_FACE_HUB_TOKEN") or os.environ.get("HF_TOKEN")
if not token:
raise OSError(
"Set HUGGING_FACE_HUB_TOKEN (or HF_TOKEN) before running. "
"You also need access approval for meta-llama/Llama-3.1-8B on HuggingFace Hub."
)
check_gpu()
# --- Build dataset with pre-computed rewards ---
df = build_dataset(reward_fn)
# --- Build Ludwig config ---
config = build_config(
epochs=args.epochs,
learning_rate=args.lr,
batch_size=args.batch_size,
)
# --- Train ---
model = LudwigModel(config=config, logging_level=logging.INFO)
train_stats, _, output_directory = model.train(
dataset=df,
experiment_name=args.experiment_name,
output_directory=args.output_dir,
)
print(f"\nTraining complete. Results saved to: {output_directory}")
print("To upload the aligned model to HuggingFace Hub:")
print(f" ludwig upload hf_hub -r <your_org>/<model_name> -m {output_directory}")
if __name__ == "__main__":
main()