Custom Filter

August 28, 2026 · View on GitHub

VAGEN supports custom filters to preprocess training data before optimization. This feature is inspired by RAGEN.

Built-in Filters

FilterDescription
reward_varianceKeep top-k groups by reward variance
reward_variance_top_pKeep groups until cumulative variance reaches top-p

Creating a Custom Filter

Step 1: Create Your Filter

Add your filter in its own directory under vagen/training/filters:

from vagen.training.filters import register_filter
from verl import DataProto

@register_filter("my_filter")
def my_filter(data_proto: DataProto, metrics: dict, **kwargs) -> tuple[DataProto, dict]:
    """
    Custom filter implementation.

    Args:
        data_proto: Input data containing batch and non_tensor_batch
        metrics: Metrics dict to update for W&B logging
        **kwargs: Additional arguments from filter.filter_kwargs config

    Returns:
        filtered_data: DataProto with filtered samples
        metrics: Updated metrics dict
    """
    # Get parameters from config
    threshold = kwargs.get("threshold", 0.5)

    # Access data
    token_level_scores = data_proto.batch["token_level_scores"]
    group_idx = data_proto.non_tensor_batch["group_idx"]

    # Your filtering logic: determine which indices to keep
    keep_indices = []
    for i, score in enumerate(token_level_scores):
        if score.sum() > threshold:
            keep_indices.append(i)

    # Apply filter
    filtered_data = data_proto.select_idxs(keep_indices)

    # Update metrics for logging (optional)
    metrics["filter/kept_ratio"] = len(keep_indices) / len(data_proto)

    return filtered_data, metrics

Step 2: Enable in Config

Update vagen/configs/vagen_multiturn.yaml:

filter:
  name: my_filter
  filter_kwargs:
    threshold: 0.5
  enable: True

Example: Reward Variance Filter

A sketch of what the built-in reward_variance does — it keeps the groups with the highest reward variance. Read vagen/training/filters/reward_variance/ for the real one; this is simplified, and the name is already taken, so pasting it as-is raises ValueError: already registered.

@register_filter("my_reward_variance")   # not "reward_variance" -- that name is taken
def reward_variance_filter(data_proto: DataProto, metrics, **kwargs) -> tuple[DataProto, dict]:
    topk_ratio = kwargs.get("topk", 0.2)  # Keep top 20% groups

    # 1) Calculate total reward per sample
    token_level_scores = data_proto.batch["token_level_scores"]
    total_rewards = token_level_scores.sum(dim=-1)

    # 2) Group rewards by group_idx
    group_idx = data_proto.non_tensor_batch["group_idx"]
    group_rewards = defaultdict(list)
    for i, (gid, reward) in enumerate(zip(group_idx, total_rewards)):
        group_rewards[gid].append((i, reward))

    # 3) Calculate variance for each group
    group_variances = {
        gid: np.var([r for _, r in rewards])
        for gid, rewards in group_rewards.items()
    }

    # 4) Select top-k groups by variance
    sorted_groups = sorted(group_variances.items(), key=lambda x: x[1], reverse=True)
    num_keep = max(1, int(len(sorted_groups) * topk_ratio))
    top_groups = set(gid for gid, _ in sorted_groups[:num_keep])

    # 5) Collect indices
    keep_indices = [i for gid, rewards in group_rewards.items()
                    if gid in top_groups for i, _ in rewards]

    return data_proto.select_idxs(keep_indices), metrics

Configuration Reference

filter:
  name: reward_variance      # Filter name (must be registered)
  filter_kwargs:             # Passed to filter function as **kwargs
    topk: 0.2                # Example: keep top 20% groups
  enable: False              # Set to True to enable filtering

A filter can change your effective learning rate

reward_variance_top_p — the name vagen_multiturn.yaml ships as the default — rescales the surviving advantages by sqrt(selected / total) so the batch's gradient magnitude does not jump when the filter drops rows. That is deliberate, and it means turning filter.enable: True on is not a neutral change: the effective step size moves with how much the filter keeps. Worth knowing before reading a curve against an unfiltered run.