Reward Functions & GRPO

Score an agent's outputs with a Python function and improve it with GRPO, reward modelling, or CPO

Grade an agent’s answers with a plain Python function, then let GRPO train the model to score higher — no preference pairs required.

Diagram

GRPO with Reward Functions

📋 Config
reward_funcs

🔎 Resolve
module:function

🤖 Load model

⭐ Score completions

✅ Gradient update

praisonai-train llm adds three RL / preference objectives on top of SFT: grpo (group-relative policy optimisation, scored by your reward functions), reward (train a reward model from preference pairs), and cpo (contrastive preference optimisation, no reference model). Only GRPO needs reward functions — the other two learn from chosen / rejected pairs.

Quick Start

Start with an Agent whose behaviour you want to score, write one function that grades its outputs, then run GRPO on it.

  1. Start from an Agent

    Pick the behaviour you want to reward — here, concise answers.

    python
    from praisonaiagents import Agent
    
    agent = Agent(instructions="Answer briefly and correctly.")
    agent.start("What is the capital of France?")
  2. Write a reward function

    TRL calls each reward function as (prompts, completions, **kwargs) and expects one float per completion. Put it in an importable module — e.g. myproject/rewards.py.

    python
    # myproject/rewards.py
    def length_penalty(prompts, completions, **kwargs) -> list[float]:
        """Reward short answers: 1.0 for <= 20 words, decreasing after."""
        return [max(0.0, 1.0 - len(c.split()) / 20) for c in completions]
  3. Point reward_funcs at it and run GRPO

    reward_funcs names each callable by its module:function import path. GRPO needs only a prompt column — it generates its own completions.

    yaml
    # config.yaml
    method: grpo
    model_name: "unsloth/gemma-2-2b-it-bnb-4bit"
    max_seq_length: 2048
    reward_funcs:
      - myproject.rewards:length_penalty
    num_generations: 8
    dataset:
      - name: "your-org/your-prompts"
        split: "train"
    bash
    pip install "praisonai-train[llm]"
    praisonai-train llm config.yaml

Which method should I use?

Diagram

I can score them
programmatically

I have chosen/rejected
and want a scorer

I have chosen/rejected
and want it lighter

📋 What signal do I have?

How do I
judge answers?

⚙️ method: grpo
reward functions

⚙️ method: reward
reward model

⚙️ method: cpo
no reference model

MethodUse whenReward functionsReference model
grpoYou have prompts and can score completions (correctness, length, format)RequiredNo
rewardYou want a reward-model artifact from (chosen, rejected) pairs (e.g. to feed GRPO later)NoNo
cpoYou have (prompt, chosen, rejected) and want a lighter-weight DPO alternativeNoNo

Dataset shapes

Each method fails fast before training if the required columns are missing — the error names both the missing columns and the columns your dataset actually has.

GRPO

Column: prompt only. GRPO generates its own completions, so no chosen / rejected / completion is needed.

prompt
"What is the capital of France?"

Reward

Columns: chosen, rejected. No prompt, no reference model.

chosenrejected
"Paris.""The capital is somewhere in Europe."

CPO

Columns: prompt, chosen, rejected — the same shape as DPO / ORPO.

promptchosenrejected
"Capital of France?""Paris.""I'm not sure."

Reward functions

A reward function is a Python callable that scores generated completions. GRPO calls it once per generation batch and uses the returned floats to push the model toward higher-scoring answers.

The module:function convention

A YAML config can only carry a string, so reward_funcs names each callable by a dotted import path — the same module:function form console_scripts and gunicorn use.

yaml
method: grpo
reward_funcs:
  - myproject.rewards:length_penalty
  - myproject.rewards:contains_answer
num_generations: 8

A single spec doesn’t have to be a list — reward_funcs: myproject.rewards:length_penalty works too. You can also pass callables directly from the Python API.

The signature TRL expects

python
def my_reward(prompts, completions, **kwargs) -> list[float]:
    ...

One float per completion. Extra columns from your dataset arrive in **kwargs, keyed by column name.

Resolved before the model loads

Every reward path is imported and validated before the multi-gigabyte model loads — a typo costs five seconds, not a lost session.

Diagram
GRPOTrainerModelrewards.resolve_allConfigGRPOTrainerModelrewards.resolve_allConfigreward_funcs (module:function)callables (or RewardError, all failures at once)load base modelmodel readyscore completions & update

Why an import path, not a registry?

Two alternatives were considered and deliberately not offered:

A decorator registry

A registry populated by @reward means the config can only name functions from a module something else already imported — a worse failure to debug than “no module named X”.

Inline Python in YAML

Inline code is an execution surface in a file people paste from the internet. An import path is the only option that can be resolved before anything runs.

Signature check is a warning, not a failure

If a reward function’s signature doesn’t accept completions, the trainer prints a warning and continues — TRL may still call it correctly, and refusing would block working code:

WARNING: my_reward does not take 'completions'; TRL calls reward functions as (prompts, completions, **kwargs)

Callables whose signature can’t be read (builtins, C callables) are left to TRL rather than guessed at.


Python API — praisonai_train.rewards

Resolve import paths yourself, or pass callables straight into a config.

python
from praisonai_train.rewards import resolve, resolve_all, require, RewardError

fn = resolve("myproject.rewards:length_penalty")   # -> callable
fns = resolve_all([                                  # collects all failures at once
    "myproject.rewards:length_penalty",
    "myproject.rewards:contains_answer",
])
require("grpo", fns)                                 # raises RewardError if empty
SymbolKindPurpose
resolve(spec)functionImport module:function and return the callable. Passes a callable through unchanged.
resolve_all(specs)functionResolve a list or a single spec; collects all failures before raising.
check_signature(fn, spec="")functionWarn-level check that fn looks like (prompts, completions, **kwargs).
require(method, resolved)functionRaise RewardError when a method (GRPO) has no reward functions.
RewardErrorexceptionSubclass of ValueError; raised for any resolution or shape failure.

GRPO config keys

Three keys apply to GRPO, alongside the shared training keys.

KeyTypeDefaultApplies toDescription
reward_funcs`str \list[str] \callable \list[callable]`Nonegrpo (required)Reward function(s) scoring generated completions. Strings must be module:function import paths. A single spec needn’t be a list.
num_generationsinttrainer defaultgrpoCompletions sampled per prompt for scoring. Passed straight to GRPOConfig.
max_completion_lengthinttrainer defaultgrpoMaximum length of each generated completion. Passed straight to GRPOConfig.

Fast rollouts (fast_inference)

GRPO always ran the slow HF-generate path for its rollouts. Flip fast_inference: true to swap it for vLLM.

yaml
method: grpo
model_name: "unsloth/gemma-2-2b-it-bnb-4bit"
fast_inference: true
gpu_memory_utilization: 0.6
max_lora_rank: 32
num_generations: 8
reward_funcs:
  - myproject.rewards:length_penalty
KeyTypeDefaultDescription
fast_inferenceboolfalseSwap HF-generate for vLLM rollouts.
gpu_memory_utilizationfloat—vLLM GPU memory fraction. Only forwarded when fast_inference is on.
max_lora_rankint—Max LoRA rank vLLM allocates for. Only forwarded when fast_inference is on.

The two companion knobs are ignored unless fast_inference is on. See Unsloth Surface for the full walkthrough.


Errors you might see

Each error runs before the model loads and tells the failing part apart, so you know exactly what to fix.

Missing reward functions for GRPO
method 'grpo' needs reward_funcs: a list of 'module:function' import paths
that score completions. Example:
  reward_funcs:
    - myproject.rewards:length_penalty

Add at least one module:function path under reward_funcs.

Module can't be imported
cannot import 'nomodule' for reward function 'nomodule:f': No module named
'nomodule'. Is it on PYTHONPATH?

The module isn’t importable — check the name and that its directory is on PYTHONPATH.

Attribute not found (with 'did you mean')
'mymod' has no 'no_such'. Callables there: contains_answer, length_penalty

The module imported but has no such function; the message lists the callables it does have (up to 8).

Malformed spec (dot instead of colon)
reward function 'mymod.func' is not a 'module:function' path.
Example: myproject.rewards:length_penalty

Use a colon between module and function: mymod:func, not mymod.func.

Not a callable
'mymod:some_int' is a int, not a function

The named attribute exists but isn’t callable — point at a function.


Best Practices

Keep reward functions importable and pure

Put them in a module on PYTHONPATH (e.g. myproject/rewards.py) and keep them side-effect-free. They run once per generation batch, so avoid slow I/O.

Combine several small rewards

List multiple module:function paths — correctness, length, and format compliance as separate functions is easier to tune than one monolithic scorer.

Dry-run to catch typos early

Because paths resolve before the model loads, a bad reward_funcs entry fails in seconds. Fix all reported paths at once — resolve_all lists every failure.

Use reward modelling to bootstrap GRPO

Train a reward model from (chosen, rejected) pairs, then wrap it in a reward function to score GRPO completions later.


Related