Mismatch Matters: On-Policy Distillation Beyond Token Agreement
August 14, 2026 · View on GitHub
Official implementation of TIDE (Token-level Independent Deficit–Excess correction).
On-policy distillation optimizes token agreement with the teacher, and students learn to game it: they fall into repetitive loops that score near-perfect agreement while the response as a whole is wrong. We call this degenerate agreement, and it motivates looking at mismatch instead. Mismatch is not one phenomenon but two, and they need opposite treatments.
- Student-excess tokens are generated by the student but assigned near-zero
probability by the teacher. Their log-ratio corrections grow unbounded and destabilize
the update. TIDE replaces the log-ratio with the bounded Hellinger shaping
h(a) = 2 (exp(a/2) - 1) > -2, so no single rejected token can dominate a step. - Student-deficit tokens are preferred by the teacher but rarely sampled by the student, so a sampled-token estimator never sees them and the teacher's reasoning patterns never transfer. TIDE injects them analytically over the teacher's top-K support, with a gradient that does not vanish as the student probability goes to zero.
- Quantile gating restricts each branch to the most informative positions via batch-adaptive thresholds. Positions in the matched band receive exactly zero advantage.
Across math reasoning benchmarks and several Qwen3 teacher–student pairs, TIDE beats standard OPD and recent token-selection and reward-shaping baselines. The gap widens under strong teacher–student mismatch, where it lifts Avg@8 from 6.9% to 20.3% while cutting average response length by 3.6x.
Paper: arXiv:2608.09836 · Repository: https://github.com/yzc-666/TIDE
Repository layout
tide/
├── verl/ # vendored verl fork (0.7.0.dev) with TIDE integrated
│ ├── verl/utils/tide_reward.py # the objective: shaping, gates, tide_scores
│ ├── verl/workers/actor/dp_actor.py # per-candidate advantages (strategy="tide")
│ ├── verl/workers/fsdp_workers.py # teacher forward: top-K log-probs
│ ├── verl/workers/config/rollout.py # tide_keep_neg / tide_keep_pos / tide_deficit_lambda
│ ├── verl/trainer/ppo/ray_trainer.py
│ └── tests/utils/test_tide_reward_on_cpu.py
├── on_policy_distillation.sh # TIDE / on-policy distillation entry point
├── grpo.sh # GRPO entry point (RL baseline)
├── scripts/val/eval/ # generation + grading for math benchmarks
├── scripts/infer/ # vLLM rollout and data-prep helpers
├── datasets/test_data/ # benchmark metadata (parquet files not included)
└── LlamaFactory/ # third-party, used only for the SFT baseline
TIDE is a new top_k_strategy inside the actor, so it reuses the fork's existing
distillation plumbing: the teacher is loaded as verl's reward model and returns
sampled-token log-probs plus top-K log-probs each step, and dp_actor turns them into
detached per-candidate advantages consumed by the token_reward_direct estimator.
Installation
The fork tracks upstream verl 0.7.0.dev; its own requirements apply unchanged.
conda create -n tide python=3.10 -y
conda activate tide
cd verl
pip install -e .[vllm,math]
Expect CUDA 12.x, PyTorch 2.x, vLLM >= 0.8.5, and Ray >= 2.41. Verify the objective without a GPU:
cd verl
python -m pytest tests/utils/test_tide_reward_on_cpu.py -q
Data
Training and evaluation read verl-format parquet files. The benchmark JSON under
datasets/test_data/ documents the expected schema, but the parquet files themselves are
not shipped — build them with verl's preprocessing scripts
(verl/examples/data_preprocess/) and place them as:
datasets/dapo-math-17k.parquet # training set
datasets/test_data/AIME24/test.parquet # and AIME25, AMC23, MATH-500, ...
Training
Point the script at a student and a teacher, then launch. Defaults reproduce the main setting: 8 GPUs on one node, DAPO-Math-17k, K = 16, 4 rollouts per prompt, 7168 response tokens.
export MODEL_ROOT=/path/to/hf_models
export ACTOR_MODEL_PATH=${MODEL_ROOT}/DeepSeek-R1-Distill-Qwen-1.5B # student
export REWARD_MODEL_PATH=${MODEL_ROOT}/JustRL-DeepSeek-1.5B # teacher
export PROJECT_PATH=/path/to/run_artifacts
bash on_policy_distillation.sh
Checkpoints, validation logs and Hydra output all land under $PROJECT_PATH. Runs resume
automatically from the latest checkpoint of the same EXPERIMENT_NAME; use
NEW_RUN=1 to force a fresh run.
Evaluation
scripts/val/eval/ runs generation and grading for the math benchmarks. Generation runs
one vLLM instance per GPU (the GPU list is at the top of main(), eight by default) and
writes jsonl rollouts under eval_outputs/<model basename>/; grading scores them into
grading_results.json.
cd scripts/val/eval
# One model, or EVAL_MODEL_FOLDER=<dir of checkpoints> to sweep a whole run.
EVAL_MODEL_PATH=/path/to/checkpoint python gen_vllm.py --disable-thinking
EVAL_RUN_NAME=$(basename /path/to/checkpoint) python grade.py
The task list and sampling settings are the constants at the top of gen_vllm.py;
reported numbers use temperature 0.7, top-p 0.95, and Avg@8 on AIME24 / AIME25 / AMC23.
Grading is rule-based by default — python grade.py --enable_model_verifier additionally
loads a verifier model from EVAL_VERIFIER_PATH.
Acknowledgements
Built on verl. The bundled fork keeps upstream's Apache 2.0 license, and TIDE's own files are released under the same terms.
Citation
@article{yu2026mismatch,
title={Mismatch Matters: On-Policy Distillation Beyond Token Agreement},
author={Yu, Zichao and Yu, Chengzhi and Xu, Shengze and Han, Yujin and Jiang, Bingqing and Wang, Xu and Zou, Difan},
journal={arXiv preprint arXiv:2608.09836},
year={2026}
}