Skip to main content
What you’ll learn: how to wire up a tool (web search) into a Miles training loop — custom multi-turn rollout, observation interleaving, reward function, and TIS to keep training stable when train ≠ inference. This is a Miles-friendly reproduction of the original Search-R1.

Prerequisites

  • radixark/miles:latest container.
  • Either a serper.dev API key (Google search backend) or ~135 GB free disk for the local Wikipedia retriever (see appendix).
  • You completed Customization — this example uses a custom rollout function and reward.

Files

Quick start

1. Set up environment

2. Prepare data

3. Convert the model

4. Run

Configuration

Open generate_with_search.py and edit SEARCH_R1_CONFIGS:

Walkthrough — multi-turn rollout

The custom rollout lives in generate_with_search.py:generate. The loop is straightforward but every step matters:

The two crucial details

  1. Loss masking. Tool/observation tokens get loss_mask=0. Without this, the model learns to predict the search results, which is both wrong and wildly unhelpful.
  2. Tokenization alignment. The model must see and the trainer must score the exact same tokens. Pre-tokenizing vs. re-tokenizing at training time can drift — that’s where the chat template verifier matters.

Walkthrough — reward

format_score=0.2 gives partial credit for the correct <answer>... shape even if the content is wrong — keeps gradient flowing during early exploration.

Enabling TIS

The trajectory mixes model tokens (we want gradients) with tool tokens (we don’t). Without correction, the implicit policy ratio in the GRPO objective is off-policy — the search results came from a stochastic environment, not the model. Truncated Importance Sampling (TIS) corrects for this. To enable:
  1. Set "return_logprob": True in SEARCH_R1_CONFIGS.
  2. Uncomment the TIS flags in run_qwen2.5_3B.sh:
When return_logprob=True, response post-processing is automatically disabled to keep token / logp alignment.

What to watch

If tis/effective_sample_size collapses below 0.5, your inference distribution has drifted too far. Lower --lr or shorten max_turns.

Tuning knobs

Troubleshooting

Variations

  • Use Google instead of local. Set "search_backend": "google" and add an API key.
  • Different tool. Replace search_backend with anything else — calculator, code exec, internal API. The pattern is identical.
  • Group RM. With multiple trajectories per prompt (GRPO), enable --group-rm so rewards are computed in a batch.
  • Longer chains. Bump max_turns to 8+ for deep-reasoning tasks. Watch loss_mask/observation_fraction — if it dominates, the model is barely training.

Appendix — local Wikipedia retriever

Heavy but completely offline. ~135 GB total disk and a separate conda env to avoid conflicting with Miles.

One-time setup

Run the server

5–7 GB of GPU memory per GPU. First startup is slow (model + index load); subsequent restarts are 1–2 minutes.

Then launch training