Buckets:
| """Async GRPO + LoRA training of the attacker against the frozen target, on two GPUs. | |
| A vLLM server (serve.sh, GPU 0) generates while the trainer (this script, GPU 1) trains, so neither waits | |
| for the other. Each training prompt is one (scenario, request). For every prompt, the rollout worker | |
| (async_rollout.py) plays a group of full multi-turn episodes (rollout.py) against the server; the judge | |
| grades each one and the reward (rewards.py) turns the grade into one number per episode. Only the | |
| attacker's own tokens are trained. After every optimizer step the trainer sends the new LoRA adapter to | |
| the server, and episodes sampled by a policy more than max_staleness steps old are discarded. | |
| Every setting comes from config.yaml; the config the run actually used is saved to | |
| checkpoints/<run-name>/config.yaml, next to the adapters and rollouts.jsonl (every episode in full). | |
| bash serve.sh # terminal 1, GPU 0; wait for "Application startup" | |
| CUDA_VISIBLE_DEVICES=1 accelerate launch --num_processes 1 train.py --run-name proto-9b-v8 | |
| CUDA_VISIBLE_DEVICES=1 accelerate launch --num_processes 1 train.py --run-name smoke train.max_steps=2 | |
| """ | |
| import argparse | |
| import os | |
| from typing import Any | |
| from peft import LoraConfig | |
| from transformers import AutoTokenizer, GenerationConfig | |
| from trl.experimental.async_grpo import AsyncGRPOConfig, AsyncGRPOTrainer | |
| import async_rollout | |
| import rollout | |
| import utils | |
| from datasets import Dataset | |
| from episode import Job | |
| # All attention projections (including Qwen3.5's linear-attention ones) and the MLP. PEFT skips names a | |
| # model does not have. | |
| LORA_TARGET_MODULES = [ | |
| "q_proj", | |
| "k_proj", | |
| "v_proj", | |
| "o_proj", | |
| "in_proj_qkv", | |
| "in_proj_z", | |
| "out_proj", | |
| "gate_proj", | |
| "up_proj", | |
| "down_proj", | |
| ] | |
| def async_grpo_config(config: dict[str, Any], run_name: str, output_dir: str) -> AsyncGRPOConfig: | |
| train, episode = config["train"], config["episode"] | |
| # One optimizer step is rows_per_step packed rows of up to token_budget tokens. An episode is one row, so a | |
| # step trains at least rows_per_step episodes; short episodes share a row, so a step can hold more. | |
| rows_per_step = train["prompts_per_step"] * train["num_generations"] | |
| # TRL's own default: no more episodes in flight than the trainer can use before they go stale. | |
| max_inflight = train["max_inflight_episodes"] | |
| if max_inflight < 0: | |
| max_inflight = max(train["max_staleness"], 1) * rows_per_step | |
| return AsyncGRPOConfig( | |
| output_dir=output_dir, | |
| run_name=run_name, | |
| report_to="wandb", | |
| logging_steps=1, | |
| save_steps=train["save_steps"], | |
| seed=train["seed"], | |
| # bf16 weights, as the server serves them (TRL defaults to fp32). | |
| dtype="bfloat16", | |
| bf16=True, | |
| gradient_checkpointing=True, | |
| learning_rate=train["lr"], | |
| num_train_epochs=train["epochs"], | |
| max_steps=train["max_steps"], | |
| num_generations=train["num_generations"], | |
| per_device_train_batch_size=1, | |
| gradient_accumulation_steps=rows_per_step, | |
| # Every episode fits one row: a longer one would be dropped with a warning. | |
| token_budget=episode["max_total_tokens"], | |
| max_completion_length=episode["max_turn_tokens"], | |
| # The trainer needs the sampling settings to score tokens the way vLLM sampled them. | |
| **config["attacker_sampling"], | |
| vllm_server_base_url=train["vllm_url"], | |
| max_staleness=train["max_staleness"], | |
| max_inflight_tasks=max_inflight, | |
| ) | |
| def eos_token_ids(model: str, tokenizer: Any) -> list[int]: | |
| """Every token id that ends a turn, worked out the way AsyncGRPOTrainer does for its own worker.""" | |
| try: | |
| ids = GenerationConfig.from_pretrained(model).eos_token_id | |
| except OSError: # no generation_config.json | |
| ids = None | |
| ids = [] if ids is None else [ids] if isinstance(ids, int) else list(ids) | |
| if tokenizer.eos_token_id not in ids: | |
| ids = [tokenizer.eos_token_id, *ids] | |
| return ids | |
| def rollout_worker( | |
| config: dict[str, Any], | |
| args: AsyncGRPOConfig, | |
| jobs: list[Job], | |
| dataset: Dataset, | |
| tokenizer: Any, | |
| log_path: str, | |
| ) -> async_rollout.AttackerRolloutWorker: | |
| """Our rollout worker, given the settings AsyncGRPOTrainer would give its own (see its __init__). The | |
| trainer fills in the LoRA adapter name once it has found the server serves adapters.""" | |
| return async_rollout.AttackerRolloutWorker( | |
| model_name=config["model"], | |
| dataset=dataset, | |
| reward_funcs=[], | |
| processing_class=tokenizer, | |
| eos_token_ids=eos_token_ids(config["model"], tokenizer), | |
| num_generations=args.num_generations, | |
| max_inflight_tasks=args.max_inflight_tasks, | |
| queue_maxsize=args.queue_maxsize, | |
| vllm_server_url=args.vllm_server_base_url, | |
| max_tokens=args.max_completion_length, | |
| temperature=args.temperature, | |
| top_p=args.top_p, | |
| top_k=args.top_k, | |
| num_sampler_logprobs=args.num_sampler_logprobs if args.score_centering else 0, | |
| min_p=args.min_p, | |
| repetition_penalty=args.repetition_penalty, | |
| request_timeout=args.request_timeout, | |
| jobs=jobs, | |
| config=config, | |
| log_path=log_path, | |
| ) | |
| def main(args: argparse.Namespace) -> None: | |
| config = utils.load_config(args.config, args.overrides) | |
| output_dir = os.path.abspath(args.output_dir or os.path.join("checkpoints", args.run_name)) | |
| utils.save_config(config, os.path.join(output_dir, "config.yaml")) | |
| jobs = utils.load_jobs(config["data"]["train"]) | |
| max_turns = config["episode"]["max_turns"] | |
| dataset = Dataset.from_list( | |
| [{"prompt": rollout.seed_messages(job["request"], max_turns)} for job in jobs] | |
| ) | |
| tokenizer = AutoTokenizer.from_pretrained(config["model"]) | |
| grpo_args = async_grpo_config(config, args.run_name, output_dir) | |
| trainer = AsyncGRPOTrainer( | |
| model=config["model"], | |
| args=grpo_args, | |
| train_dataset=dataset, # sets the epoch length; the worker draws the prompts itself | |
| processing_class=tokenizer, | |
| peft_config=LoraConfig( | |
| r=config["train"]["lora_r"], | |
| lora_alpha=2 * config["train"]["lora_r"], | |
| lora_dropout=0.0, | |
| task_type="CAUSAL_LM", | |
| target_modules=LORA_TARGET_MODULES, | |
| ), | |
| rollout_worker=rollout_worker( | |
| config, | |
| grpo_args, | |
| jobs, | |
| dataset, | |
| tokenizer, | |
| os.path.join(output_dir, "rollouts.jsonl"), | |
| ), | |
| ) | |
| trainer.train() | |
| trainer.save_model(os.path.join(output_dir, "final")) | |
| print(f"Saved LoRA adapter to {os.path.join(output_dir, 'final')}") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser( | |
| description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter | |
| ) | |
| parser.add_argument("--run-name", required=True) | |
| parser.add_argument("--config", default="config.yaml") | |
| parser.add_argument("--output-dir", default=None, help="default: checkpoints/<run-name>") | |
| parser.add_argument("overrides", nargs="*", help="config overrides, e.g. train.lr=5e-5") | |
| main(parser.parse_args()) | |
Xet Storage Details
- Size:
- 7.28 kB
- Xet hash:
- 4991e20b86107b4d8b18f6320a2c338701b03c01951f265fc42954953bcb9198
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.