book-open-reader教程:如何使用 RL 训练 gpt-oss

学习使用 GRPO 训练 OpenAI gpt-oss,在本地或 Colab 上自主击败 2048 游戏。

大型语言模型在涉及复杂环境的任务上经常表现不佳。然而,通过应用 强化学习 (RL)并设计自定义的 奖励函数,这些挑战可以被克服。

RL 可以适配于诸如自动内核或策略生成等任务。本教程展示如何训练 gpt-oss 上进行的进展, GRPO 和 Unsloth 以自主击败 2048。

你将构建的内容:

  • 训练 gpt-oss-20b,使模型能够自动赢得 2048

  • 创建一个可供模型交互的最小化 2048 环境

  • 定义 奖励函数 ,这些函数:

    1. 检查生成的策略能否编译并运行,

    2. 防止奖励被操纵(不允许外部导入),并且

    3. 奖励真实的游戏成功

  • 运行推理并导出模型(MXFP4 4 位或合并的 FP16)

circle-info

硬件: 2048 示例可在免费的 Colab T4 上运行,但训练会很慢。A100/H100 快得多。4 位加载 + LoRA 可让你在适度 VRAM 中容纳 20B 模型。

1

安装 Unsloth

在笔记本顶部运行此单元(在 Colab 上可用)。

!pip install --upgrade -qqq uv
try: import numpy; get_numpy = f"numpy=={numpy.__version__}"
except: get_numpy = "numpy"
!uv pip install -qqq \
    "torch>=2.8.0" "triton>=3.4.0" {get_numpy} torchvision bitsandbytes "transformers==4.56.2" \
    "unsloth_zoo[base] @ git+https://github.com/unslothai/unsloth-zoo" \
    "unsloth[base] @ git+https://github.com/unslothai/unsloth" \
    git+https://github.com/triton-lang/triton.git@05b2c186c1b6c9a08375389d5efe9cb4c401c075#subdirectory=python/triton_kernels
!uv pip install --upgrade --no-deps transformers==4.56.2 tokenizers
!uv pip install --no-deps trl==0.22.2
2

使用 Unsloth 加载 gpt-oss

以 4 位 QLoRA 加载 20B 模型以提高内存效率,然后用 LoRA 适配器包装它。你也可以在 16 位 LoRA 下训练,但会使用 4 倍内存。更多设置请查看我们的 配置指南.

from unsloth import FastLanguageModel
import torch

max_seq_length = 768        # 如果你的任务需要更长的输出,请增加
lora_rank      = 4          # 更高的秩 → 更好但需要更多 VRAM/计算

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name        = "unsloth/gpt-oss-20b",  # 或在 H100 上使用 unsloth/gpt-oss-20b-BF16
    max_seq_length    = max_seq_length,
    load_in_4bit      = True,                    # 16 位时设为 False
    offload_embedding = True,                    # 约节省 ~1GB VRAM
)

model = FastLanguageModel.get_peft_model(
    model,
    r = lora_rank,
    target_modules = [
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    lora_alpha = lora_rank * 2,
    use_gradient_checkpointing = "unsloth",     # 大幅节省内存
    random_state = 3407,
)
circle-info

如果遇到 OOM,请尝试降低 max_seq_length, lora_rank,或 num_generations (稍后),并保持 load_in_4bit=True.

3

2048 游戏环境(最小化)

  • 一个 GameBoard 类,支持 W/A/S/D 移动

  • 合并/得分逻辑

  • execute_with_time_limit 包装,以防写得不好的策略挂起内核

你可以用一个简单策略快速进行冒烟测试:

def always_move_left(board):
    return "W"

steps, outcome = execute_strategy(always_move_left, GameBoard(size=8, seed=42, target=2048, probability_fours=0.10))
4

安全的代码执行与反作弊检查

生成的策略是 Python 函数。为了保持执行安全并防止奖励被操纵:

  • 模块白名单检查 — 仅允许 Python 标准库符号:

    from unsloth import check_python_modules
    ok, info = check_python_modules("""
    def strategy(board):
        import math
        from typing import Callable
        return "W"
    """)
    # ok == True 表示仅使用了 Python 级别的导入
  • 阻止不允许的导入 (例如,NumPy):

    sample = """
    def strategy(board):
        from numpy import matmul
        return "W"
    """
    ok, info = check_python_modules(sample)  # ok => False
  • 锁定执行环境 到沙箱化函数:

    from unsloth import create_locked_down_function
    function = """
    def add(a, b):
        def adder(a):
            return a + b
        return adder(b) + b
    """
    f = create_locked_down_function(function)  # 如果使用全局变量 / 导入会出错
  • 对策略运行强制实施硬性时钟限制

    from unsloth import execute_with_time_limit
    @execute_with_time_limit(2)
    def execute_strategy(strategy, game):
        # 循环直到游戏结束或超时
        ...
5

### 提示与数据集

我们提示模型 输出一个简短的策略函数 放在三重反引号内:

使用仅包含原生 Python 代码的新短 2048 策略。
给你一个当前棋盘状态的数字列表的列表。
输出在下一步最优动作的一个动作,使用 "W", "A", "S", "D" 中的一项。
按下面的格式将你的新简短函数放在反引号中输出:
```python
def strategy(board):
    return "W"  # 示例

所有辅助函数都应在 def strategy 内。只输出简短函数 strategy.


创建一个小型合成数据集(重复使用相同的提示)并计算提示长度,以便 GRPO 知道要采样多少完成令牌:

```python
from datasets import Dataset

prompt = ...  # 如上

maximum_length = len(tokenizer.apply_chat_template(
    [{"role": "user", "content": prompt}], add_generation_prompt=True
))

dataset = Dataset.from_list([
    {"prompt": [{"role": "user", "content": prompt}], "answer": 0, "reasoning_effort": "low"}
] * 1000)

{% hint style="info" %} 你可以用真实的提示替换此数据集以用于你自己的 RL 任务。 {% endhint %} {% endstep %}

{% step %}

奖励函数时间!

  1. 从模型的回复中提取代码块

    def extract_function(text):
        if text.count("```") >= 2:
            first = text.find("```") + 3
            second = text.find("```", first)
            fx = text[first:second].strip()
            fx = fx.removeprefix("python\n")
            fx = fx[fx.find("def"):]
            if fx.startswith("def strategy(board):"):
                return fx
        return None
  2. function_works - 它能否编译并创建可调用对象?

    from unsloth import create_locked_down_function, check_python_modules
    
    def function_works(completions, **kwargs):
        scores = []
        for completion in completions:
            response = completion[0]["content"]
            function = extract_function(response)
            if function is None:
                scores.append(-2.0)
                continue
            ok, info = check_python_modules(function)
            if "error" in info:
                scores.append(-2.0)
                continue
            try:
                _ = create_locked_down_function(function)
                scores.append(1.0)
            except Exception:
                scores.append(-0.5)
        return scores
  3. no_cheating - 不允许非标准库导入:

    def no_cheating(completions, **kwargs):
        scores = []
        for completion in completions:
            response = completion[0]["content"]
            function = extract_function(response)
            if function is None:
                scores.append(-1.0)
                continue
            ok, _ = check_python_modules(function)
            scores.append(1.0 if ok else -20.0)  # 若作弊则重罚
        return scores
  4. strategy_succeeds - 对随机棋盘进行对局;对成功给予奖励:

    import numpy as np
    
    PRINTER = 0  # 偶尔打印以便调试
    
    def strategy_succeeds(completions, **kwargs):
        global PRINTER
        scores = []
        seed = np.random.randint(10000)
        for completion in completions:
            response = completion[0]["content"]
            function = extract_function(response)
            if function is None:
                scores.append(-2.0)
                continue
            try:
                new_strategy = create_locked_down_function(function)
            except Exception:
                scores.append(0.0)
                continue
            try:
                game = GameBoard(size=6, seed=seed, target=2048, probability_fours=0.10)
                steps, state = execute_strategy(new_strategy, game)
                if PRINTER % 5 == 0:
                    print(function)
                    print(f"Steps={steps} State={state}")
                    print(game.board().pretty())
                PRINTER += 1
                if state == "success":
                    scores.append(20.0)
                else:
                    scores.append(2.0)   # 有效但未达到 2048
            except TimeoutError:
                scores.append(-1.0)      # 超时
            except Exception:
                scores.append(-3.0)      # 崩溃
        return scores

{% endstep %}

{% step %}

配置 GRPO

我们将使用 GRPOTrainer。设置提示/完成长度,然后构建一个 GRPOConfig。请记住你也可以将 RL 算法类型设置为其他,例如 GSPO 或 Dr. GRPO。

from trl import GRPOConfig, GRPOTrainer

max_prompt_length     = maximum_length + 1
max_completion_length = max_seq_length - max_prompt_length

training_args = GRPOConfig(
    temperature=1.0,
    learning_rate=5e-5,
    weight_decay=0.01,
    warmup_ratio=0.1,
    lr_scheduler_type="linear",
    optim="adamw_8bit",
    logging_steps=1,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=1,    # 若需更平滑的奖励信号可增至 4
    num_generations=2,                # 如果 OOM 请降低
    max_prompt_length=max_prompt_length,
    max_completion_length=max_completion_length,
    max_steps=1000,                   # 或设置 num_train_epochs=1
    save_steps=100,
    report_to="none",
    output_dir="outputs",
)

trainer = GRPOTrainer(
    model=model,
    processing_class=tokenizer,
    reward_funcs=[function_works, no_cheating, strategy_succeeds],
    args=training_args,
    train_dataset=dataset,
    # 可选的评估拆分:
    # train_dataset=new_dataset["train"],
    # eval_dataset=new_dataset["test"],
)

{% hint style="info" %} 查看日志: 关注 rewardreward_std。在早期(小 GPU 上的前 ~100–200 步)看到低/零奖励是正常的。 {% endhint %} {% endstep %}

{% step %}

训练你的模型

trainer.train()

这将启动完整的 RL 循环:采样完成项 → 用你的奖励打分 → 优化策略(LoRA)。 {% endstep %}

{% step %}

推理(训练后)

使用训练好的适配器生成一个新策略:

from transformers import TextStreamer

text = tokenizer.apply_chat_template(
    [{"role": "user", "content": prompt}],
    tokenize=False,
    add_generation_prompt=True,
    reasoning_effort="low",
)

_ = model.generate(
    **tokenizer(text, return_tensors="pt").to("cuda"),
    temperature=1.0,
    max_new_tokens=1024,
    streamer=TextStreamer(tokenizer, skip_prompt=False)

{% endstep %}

{% step %}

保存 / 导出你的微调模型

  • 合并并保存 4 位(MXFP4)

python model.save_pretrained_merged("finetuned_model", tokenizer, save_method="mxfp4") # 或 push model.push_to_hub_merged("<org_or_user>/", tokenizer, token="<hf_token>", save_method="mxfp4") ```

  • 合并并保存 16 位

    model.save_pretrained_merged("finetuned_model", tokenizer, save_method="merged_16bit")
    # 或推送
    model.push_to_hub_merged("<org_or_user>/<repo>", tokenizer, token="<hf_token>", save_method="merged_16bit")
6

故障排除与提示

  • OOM / 速度慢:减少 max_seq_length, num_generations, lora_rank;保持 4 位;如果可用,尝试 A100。

  • 奖励无改善:增加训练步骤、放宽惩罚,或增加课程式训练(从更小的棋盘 / 更低的目标开始)。

  • 奖励被操纵:保持 check_python_modules 严格;在多个随机种子上验证策略行为。

  • 训练不稳定:提高 gradient_accumulation_steps 以平滑更新;降低 learning_rate (例如 2e‑5)。

  • 长时间挂起:确保 execute_with_time_limit 包装任何策略执行。

7

适配到你自己的 RL 任务

  • 将 2048 环境替换为你自己的环境并且 三个奖励: (a) 语法/编译,(b) 反作弊/安全,(c) 任务成功。

  • 更新 提示 以请求你所需的函数或输出类型。

  • 保留相同的 Unsloth + GRPO 骨架;只需替换环境和奖励。

最后更新于

这有帮助吗?