book-open-readerTutorial: Wie man gpt-oss mit RL trainiert

Lerne, OpenAI gpt-oss mit GRPO zu trainieren, um lokal oder auf Colab autonom 2048 zu schlagen.

LLMs haben oft Schwierigkeiten mit Aufgaben, die komplexe Umgebungen beinhalten. Durch die Anwendung von Verstärkendem Lernen (RL) und dem Entwurf einer benutzerdefinierten Belohnungsfunktion, können diese Herausforderungen jedoch überwunden werden.

RL kann für Aufgaben wie automatische Kernel- oder Strategiegenerierung angepasst werden. Dieses Tutorial zeigt, wie man gpt-oss mit GRPO und Unsloth trainiert, um autonom 2048 zu schlagen.

Was du bauen wirst:

  • Trainiere gpt-oss-20b, sodass das Modell 2048 automatisch gewinnen kann

  • Erstelle eine minimale 2048-Umgebung, mit der das Modell interagieren kann

  • Definiere Belohnungsfunktionen die:

    1. Überprüfe, ob die generierte Strategie kompiliert und ausgeführt wird,

    2. Verhindere Belohnungsmanipulation (externen Imports nicht erlauben), und

    3. Belohne tatsächlichen Spiel-Erfolg

  • Führe Inferenz aus und exportiere das Modell (MXFP4 4‑Bit oder zusammengeführtes FP16)

circle-info

Hardware: Das 2048-Beispiel läuft auf einem kostenlosen Colab T4, aber das Training wird langsam sein. A100/H100 ist deutlich schneller. 4‑Bit-Laden + LoRA ermöglicht es, ein 20B-Modell in moderaten VRAM zu laden.

1

Unsloth installieren

Führe diese Zelle oben in einem Notebook aus (funktioniert auf 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

gpt-oss mit Unsloth laden

Lade das 20B-Modell in 4‑Bit QLoRA zur Speichereffizienz und wickle es dann mit einem LoRA-Adapter ein. Du kannst es auch in 16‑Bit LoRA trainieren, aber das verbraucht 4x mehr Speicher. Für weitere Einstellungen siehe unseren Konfigurationsleitfaden.

from unsloth import FastLanguageModel
import torch

max_seq_length = 768        # Erhöhe, wenn deine Aufgabe längere Ausgaben benötigt
lora_rank      = 4          # Höherer Rang → besser, aber mehr VRAM/Compute

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name        = "unsloth/gpt-oss-20b",  # oder unsloth/gpt-oss-20b-BF16 auf H100
    max_seq_length    = max_seq_length,
    load_in_4bit      = True,                    # False für 16‑Bit
    offload_embedding = True,                    # spart ~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",     # großer Speicher-Sparer
    random_state = 3407,
)
circle-info

Wenn du OOM bekommst, versuche folgendes zu reduzieren: max_seq_length, lora_rank, oder num_generations (später), und behalte load_in_4bit=True.

3

2048-Spielumgebung (minimal)

  • Eine GameBoard Klasse, die W/A/S/D Züge

  • Merge-/Punktelogik

  • execute_with_time_limit Wrapper, damit schlecht geschriebene Strategien den Kernel nicht aufhängen können

Du kannst schnell mit einer trivialen Policy einen Schnelltest machen:

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

Sichere Codeausführung & Anti‑Cheat-Checks

Generierte Strategien sind Python-Funktionen. Um die Ausführung sicher zu halten und Belohnungsmanipulation zu verhindern:

  • Modul-Whitelist-Prüfung — erlaube nur Python-Standardbibliotheks-Symbole:

    from unsloth import check_python_modules
    ok, info = check_python_modules("""
    def strategy(board):
        import math
        from typing import Callable
        return "W"
    """)
    # ok == True bedeutet, dass nur Python‑Level-Imports verwendet wurden
  • Blockiere nicht erlaubte Imports (z. B. NumPy):

    sample = """
    def strategy(board):
        from numpy import matmul
        return "W"
    """
    ok, info = check_python_modules(sample)  # ok => False
  • Führe die Ausführung einschränkend aus in einer Sandbox-Funktion:

    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)  # Fehler, falls Globals / Imports verwendet werden
  • Setze ein hartes Zeitlimit (Wall‑Clock) für Strategieläufe:

    from unsloth import execute_with_time_limit
    @execute_with_time_limit(2)
    def execute_strategy(strategy, game):
        # Schleife bis das Spiel endet oder die Zeit abläuft
        ...
5

### Prompt & Datensatz

Wir fordern das Modell auf, eine kurze Strategie-Funktion auszugeben innerhalb von dreifachen Backticks:

Erstelle eine neue kurze 2048-Strategie, die nur native Python-Code verwendet.
Du erhältst eine Liste von Listen mit Zahlen für den aktuellen Spielbrettzustand.
Gib eine Aktion für "W", "A", "S", "D" aus, die der optimale nächste Schritt ist.
Gib deine neue kurze Funktion in Backticks im untenstehenden Format aus:
```python
def strategy(board):
    return "W"  # Beispiel

Alle Hilfsfunktionen sollten innerhalb von def strategy sein. Gib nur die kurze Funktion aus strategy.


Erstelle einen winzigen synthetischen Datensatz (denselben Prompt wiederverwenden) und berechne die Prompt-Länge, damit GRPO weiß, wie viele Completion-Tokens zu sampeln sind:

```python
from datasets import Dataset

prompt = ...  # wie oben

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" %} Du kannst diesen Datensatz durch echte Prompts für deine eigene RL-Aufgabe ersetzen. {% endhint %} {% endstep %}

{% step %}

Zeit für die Belohnungsfunktion!

  1. Extrahiere den Codeblock aus der Antwort des Modells:

    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 - Kompiliert es & erzeugt eine aufrufbare Funktion?

    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 - Keine Nicht‑Stdlib-Imports erlaubt:

    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)  # harte Strafe bei Betrug
        return scores
  4. strategy_succeeds - Spiele ein zufälliges Brett; belohne Erfolg:

    import numpy as np
    
    PRINTER = 0  # gelegentliches Drucken zum Debuggen
    
    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)   # hat funktioniert, aber 2048 nicht erreicht
            except TimeoutError:
                scores.append(-1.0)      # Zeitüberschreitung
            except Exception:
                scores.append(-3.0)      # abgestürzt
        return scores

{% endstep %}

{% step %}

GRPO konfigurieren

Wir werden den GRPOTrainerverwenden. Setze die Prompt-/Completion-Längen und baue dann eine GRPOConfig. Beachte, dass du den RL-Algorithmustyp auch auf andere wie GSPO oder Dr. GRPO setzen könntest.

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,    # erhöhe auf 4 für gleichmäßigere Belohnungssignale
    num_generations=2,                # reduziere, wenn du OOM bekommst
    max_prompt_length=max_prompt_length,
    max_completion_length=max_completion_length,
    max_steps=1000,                   # oder setze 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,
    # Optionale Eval-Aufteilung:
    # train_dataset=new_dataset["train"],
    # eval_dataset=new_dataset["test"],
)

{% hint style="info" %} Logs lesen: Sieh dir reward und reward_stdan. Es ist normal, zu Beginn niedrige/Null-Belohnungen zu sehen (die ersten ~100–200 Schritte auf kleinen GPUs). {% endhint %} {% endstep %}

{% step %}

Trainiere dein Modell

trainer.train()

Dies startet die vollständige RL-Schleife: Completions sampeln → mit deinen Belohnungen bewerten → die Policy (LoRA) optimieren. {% endstep %}

{% step %}

Inference (nach dem Training)

Erzeuge eine neue Strategie mit dem trainierten Adapter:

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 %}

Speichere / exportiere dein feinabgestimmtes Modell

  • Zusammenführen & Speichern 4‑Bit (MXFP4)

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

  • Zusammenführen & Speichern 16‑Bit

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

Fehlerbehebung & Tipps

  • OOM / langsam: reduziere max_seq_length, num_generations, lora_rank; behalte 4‑Bit; versuche A100, falls verfügbar.

  • Keine Belohnungsverbesserung: erhöhe die Trainingsschritte, mildere Strafen oder füge einen Lehrplan hinzu (starte mit kleineren Brettern / niedrigeren Zielen).

  • Belohnungsmanipulation: halte check_python_modules streng; validiere das Verhalten der Strategie über mehrere Zufallssaaten.

  • Instabiles Training: erhöhe gradient_accumulation_steps um Updates zu glätten; senke learning_rate (z. B. 2e‑5).

  • Lange Hänger: stelle sicher, dass execute_with_time_limit jede Strategieausführung eingewickelt ist.

7

Passe es an deine eigene RL-Aufgabe an

  • Ersetze die 2048-Umgebung durch deine eigene Umgebung und drei Belohnungen: (a) Syntax/Kompilierung, (b) Anti‑Cheat/Sicherheit, (c) Aufgabenerfolg.

  • Aktualisiere den Prompt um die Art der Funktion oder Ausgabe anzufordern, die du benötigst.

  • Behalte das gleiche Unsloth + GRPO-Gerüst; tausche nur die Umgebung und die Belohnungen aus.

Zuletzt aktualisiert

War das hilfreich?